Page MenuHomec4science

No OneTemporary

File Metadata

Created
Mon, Jun 23, 02:37
This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/bin/conduit b/bin/conduit
new file mode 120000
index 000000000..9221340a9
--- /dev/null
+++ b/bin/conduit
@@ -0,0 +1 @@
+../scripts/setup/manage_conduit.php
\ No newline at end of file
diff --git a/bin/lock b/bin/lock
new file mode 120000
index 000000000..686fa3879
--- /dev/null
+++ b/bin/lock
@@ -0,0 +1 @@
+../scripts/setup/manage_lock.php
\ No newline at end of file
diff --git a/bin/webhook b/bin/webhook
new file mode 120000
index 000000000..d32033687
--- /dev/null
+++ b/bin/webhook
@@ -0,0 +1 @@
+../scripts/setup/manage_webhook.php
\ No newline at end of file
diff --git a/webroot/rsrc/favicons/apple-touch-icon-120x120.png b/resources/builtin/favicon/default-120x120.png
similarity index 100%
rename from webroot/rsrc/favicons/apple-touch-icon-120x120.png
rename to resources/builtin/favicon/default-120x120.png
diff --git a/webroot/rsrc/favicons/favicon-128.png b/resources/builtin/favicon/default-128x128.png
similarity index 100%
rename from webroot/rsrc/favicons/favicon-128.png
rename to resources/builtin/favicon/default-128x128.png
diff --git a/webroot/rsrc/favicons/apple-touch-icon-152x152.png b/resources/builtin/favicon/default-152x152.png
similarity index 100%
rename from webroot/rsrc/favicons/apple-touch-icon-152x152.png
rename to resources/builtin/favicon/default-152x152.png
diff --git a/webroot/rsrc/favicons/apple-touch-icon-76x76.png b/resources/builtin/favicon/default-76x76.png
similarity index 100%
rename from webroot/rsrc/favicons/apple-touch-icon-76x76.png
rename to resources/builtin/favicon/default-76x76.png
diff --git a/resources/builtin/favicon/dot-pink-64x64.png b/resources/builtin/favicon/dot-pink-64x64.png
new file mode 100644
index 000000000..99595c8e1
Binary files /dev/null and b/resources/builtin/favicon/dot-pink-64x64.png differ
diff --git a/resources/builtin/favicon/dot-red-64x64.png b/resources/builtin/favicon/dot-red-64x64.png
new file mode 100644
index 000000000..e727375be
Binary files /dev/null and b/resources/builtin/favicon/dot-red-64x64.png differ
diff --git a/resources/celerity/map.php b/resources/celerity/map.php
index 1ea417d53..e0bb41b8a 100644
--- a/resources/celerity/map.php
+++ b/resources/celerity/map.php
@@ -1,2491 +1,2394 @@
<?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' => '7095731b',
- 'core.pkg.js' => '4c79d74f',
- 'darkconsole.pkg.js' => '1f9a31bc',
- 'differential.pkg.css' => '45951e9e',
- 'differential.pkg.js' => '19ee9979',
+ 'core.pkg.css' => '49b87886',
+ 'core.pkg.js' => '1ea38af8',
+ 'differential.pkg.css' => '113e692c',
+ 'differential.pkg.js' => 'f6d809c0',
'diffusion.pkg.css' => 'a2d17c7d',
'diffusion.pkg.js' => '6134c5a1',
- 'favicon.ico' => '30672e08',
'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' => 'f7b071f1',
+ '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' => 'faf6a6fc',
+ 'rsrc/css/aphront/phabricator-nav-view.css' => 'a9e3e6d5',
'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/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' => '7dae7f18',
+ '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' => 'bf84345b',
'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-source.css' => '5f35a3bd',
'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' => 'f491c9f4',
+ 'rsrc/css/application/harbormaster/harbormaster.css' => '730a4a3c',
'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' => '2f00933b',
+ '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' => '8561f3c3',
+ 'rsrc/css/core/remarkup.css' => '1828e2ad',
'rsrc/css/core/syntax.css' => 'cae95e89',
'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' => 'fccf9f82',
- 'rsrc/css/layout/phabricator-source-code-view.css' => 'aea41829',
+ 'rsrc/css/layout/phabricator-filetree-view.css' => 'b912ad97',
+ 'rsrc/css/layout/phabricator-source-code-view.css' => '31ee3c83',
'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/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' => '6ae18df0',
+ 'rsrc/css/phui/object-item/phui-oi-list-view.css' => 'ae1404ba',
'rsrc/css/phui/object-item/phui-oi-simple-ui.css' => 'a8beebea',
- 'rsrc/css/phui/phui-action-list.css' => 'f7f61a34',
+ '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-curtain-view.css' => '2bdaf026',
'rsrc/css/phui/phui-document-pro.css' => '8af7ea27',
'rsrc/css/phui/phui-document-summary.css' => '9ca48bdf',
- 'rsrc/css/phui/phui-document.css' => '391e1e84',
+ 'rsrc/css/phui/phui-document.css' => '878c2f52',
'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.css' => '7aaa04e3',
'rsrc/css/phui/phui-head-thing.css' => 'fd311e5f',
'rsrc/css/phui/phui-header-view.css' => '31dc6c72',
'rsrc/css/phui/phui-hovercard.css' => 'f0592bcf',
'rsrc/css/phui/phui-icon-set-selector.css' => '87db8fee',
- 'rsrc/css/phui/phui-icon.css' => '5c4a5de6',
+ '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' => '2dc7993f',
+ 'rsrc/css/phui/phui-property-list-view.css' => 'de4754d8',
'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' => 'e2ef62b1',
+ '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' => '6ad39b6f',
+ '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' => '3010e992',
+ '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' => '1e911d0f',
+ '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/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' => '0fcf201c',
+ 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js' => 'ab9e0a82',
'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadStaticSource.js' => '6c0e62fa',
- 'rsrc/favicons/apple-touch-icon-114x114.png' => '12a24178',
- 'rsrc/favicons/apple-touch-icon-120x120.png' => '0d1543c7',
- 'rsrc/favicons/apple-touch-icon-144x144.png' => '8043b5a5',
- 'rsrc/favicons/apple-touch-icon-152x152.png' => '65905ecd',
- 'rsrc/favicons/apple-touch-icon-57x57.png' => '2bfc7b0a',
- 'rsrc/favicons/apple-touch-icon-60x60.png' => '8ff52925',
- 'rsrc/favicons/apple-touch-icon-72x72.png' => 'a2bb65d6',
- 'rsrc/favicons/apple-touch-icon-76x76.png' => '2d061a11',
- 'rsrc/favicons/favicon-128.png' => '72f7e812',
'rsrc/favicons/favicon-16x16.png' => 'fc6275ba',
- 'rsrc/favicons/favicon-196x196.png' => '95db275e',
- 'rsrc/favicons/favicon-32x32.png' => '5bd18b6c',
- 'rsrc/favicons/favicon-96x96.png' => '7242c8e9',
- 'rsrc/favicons/favicon-mention.ico' => '1fdd0fa4',
- 'rsrc/favicons/favicon-message.ico' => '115bc010',
- 'rsrc/favicons/favicon.ico' => 'cdb11121',
'rsrc/favicons/mask-icon.svg' => 'e132a80f',
- 'rsrc/favicons/mstile-144x144.png' => '310c2ee5',
- 'rsrc/favicons/mstile-150x150.png' => '74bf5133',
- 'rsrc/favicons/mstile-310x150.png' => '4a49d3ee',
- 'rsrc/favicons/mstile-310x310.png' => 'a52ab264',
- 'rsrc/favicons/mstile-70x70.png' => '5edce7b8',
'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' => '4cc4f460',
+ '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' => '99abf4cd',
- 'rsrc/js/application/diff/DiffChangesetList.js' => '3b77efdd',
+ 'rsrc/js/application/diff/DiffChangeset.js' => 'b49b59d6',
+ 'rsrc/js/application/diff/DiffChangesetList.js' => 'e74b7517',
'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-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-jump-to.js' => '73d09eef',
'rsrc/js/application/diffusion/behavior-load-blame.js' => '42126667',
'rsrc/js/application/diffusion/behavior-locate-file.js' => '6d3e1947',
'rsrc/js/application/diffusion/behavior-pull-lastmodified.js' => 'f01586dc',
- 'rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js' => 'e5822781',
+ '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' => '9108ee1a',
'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/herald/HeraldRuleEditor.js' => 'dca75c0e',
- 'rsrc/js/application/herald/PathTypeahead.js' => 'f7fc67ec',
+ 'rsrc/js/application/herald/PathTypeahead.js' => '662e9cea',
'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' => 'aa1733d0',
+ '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' => '2ab10a76',
'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-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' => '008faf9c',
+ '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-error-log.js' => '6882e80a',
'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' => '560f41da',
- 'rsrc/js/core/behavior-line-linker.js' => '1499a8cb',
+ 'rsrc/js/core/behavior-lightbox-attachments.js' => '6b31879a',
+ 'rsrc/js/core/behavior-line-linker.js' => '13e39479',
'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' => '947753e0',
+ 'rsrc/js/core/behavior-phabricator-nav.js' => '836f966d',
'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' => 'd0a99ab4',
+ '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' => '17bb8539',
+ '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' => '442efd08',
- 'rsrc/js/phuix/PHUIXAutocomplete.js' => 'e0731603',
+ 'rsrc/js/phuix/PHUIXActionView.js' => '8d4a8c72',
+ 'rsrc/js/phuix/PHUIXAutocomplete.js' => 'df1bbd34',
'rsrc/js/phuix/PHUIXButtonView.js' => '8a91e1ac',
'rsrc/js/phuix/PHUIXDropdownMenu.js' => '04b2ae03',
'rsrc/js/phuix/PHUIXExample.js' => '68af71ca',
- 'rsrc/js/phuix/PHUIXFormControl.js' => '1dd0870c',
+ 'rsrc/js/phuix/PHUIXFormControl.js' => '210a16c1',
'rsrc/js/phuix/PHUIXIconView.js' => 'bff6884b',
),
'symbols' => array(
'almanac-css' => 'dbb9b3af',
'aphront-bars' => '231ac33c',
- 'aphront-dark-console-css' => 'f7b071f1',
+ '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' => 'bf84345b',
'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',
'diffusion-source-css' => '5f35a3bd',
'diviner-shared-css' => '896f1d43',
'font-fontawesome' => 'e838e088',
'font-lato' => 'c7ccd872',
'global-drag-and-drop-css' => 'b556a948',
- 'harbormaster-css' => 'f491c9f4',
+ 'harbormaster-css' => '730a4a3c',
'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' => '4cc4f460',
+ '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-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' => '17bb8539',
+ '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-user-select' => 'a8d8459d',
'javelin-behavior-diffusion-browse-file' => '054a0f0b',
'javelin-behavior-diffusion-commit-branches' => 'bdaf4d04',
'javelin-behavior-diffusion-commit-graph' => '75b83cbb',
'javelin-behavior-diffusion-jump-to' => '73d09eef',
'javelin-behavior-diffusion-locate-file' => '6d3e1947',
'javelin-behavior-diffusion-pull-lastmodified' => 'f01586dc',
- 'javelin-behavior-doorkeeper-tag' => 'e5822781',
+ 'javelin-behavior-document-engine' => '9108ee1a',
+ '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-error-log' => '6882e80a',
'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-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' => '560f41da',
+ 'javelin-behavior-lightbox-attachments' => '6b31879a',
'javelin-behavior-line-chart' => 'e4232876',
'javelin-behavior-load-blame' => '42126667',
'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' => '1499a8cb',
- 'javelin-behavior-phabricator-nav' => '947753e0',
+ 'javelin-behavior-phabricator-line-linker' => '13e39479',
+ 'javelin-behavior-phabricator-nav' => '836f966d',
'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' => 'd0a99ab4',
+ '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' => '2ab10a76',
'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' => '3010e992',
+ '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' => '6ad39b6f',
+ 'javelin-stratcom' => '327f418a',
'javelin-tokenizer' => '8d3bc1b2',
'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' => '0fcf201c',
+ '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' => '1e911d0f',
- 'katex-css' => '297123ca',
+ 'javelin-workflow' => '6a726c55',
'maniphest-report-css' => '9b9580b7',
'maniphest-task-edit-css' => 'fda62a9b',
'maniphest-task-summary-css' => '11cc5344',
'multirow-row-manager' => 'b5d57730',
- 'owners-path-editor' => 'aa1733d0',
- 'owners-path-editor-css' => '2f00933b',
+ 'owners-path-editor' => 'c96502cf',
+ 'owners-path-editor-css' => '9c136c29',
'paste-css' => '9fcc9773',
- 'path-typeahead' => 'f7fc67ec',
+ 'path-typeahead' => '662e9cea',
'people-picture-menu-item-css' => 'a06f7f34',
'people-profile-css' => '4df76faf',
- 'phabricator-action-list-view-css' => 'f7f61a34',
+ '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' => '99abf4cd',
- 'phabricator-diff-changeset-list' => '3b77efdd',
+ 'phabricator-diff-changeset' => 'b49b59d6',
+ 'phabricator-diff-changeset-list' => 'e74b7517',
'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' => 'fccf9f82',
+ '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' => 'faf6a6fc',
- 'phabricator-notification' => '008faf9c',
+ 'phabricator-nav-view-css' => 'a9e3e6d5',
+ 'phabricator-notification' => '4f774dac',
'phabricator-notification-css' => '457861ec',
'phabricator-notification-menu-css' => '10685bd4',
'phabricator-object-selector-css' => '85ee8ce6',
'phabricator-phtize' => 'd254d646',
'phabricator-prefab' => '77b0ae28',
- 'phabricator-remarkup-css' => '8561f3c3',
+ 'phabricator-remarkup-css' => '1828e2ad',
'phabricator-search-results-css' => '505dd8cf',
'phabricator-shaped-request' => '7cbe244b',
'phabricator-slowvote-css' => 'a94b7230',
- 'phabricator-source-code-view-css' => 'aea41829',
+ 'phabricator-source-code-view-css' => '31ee3c83',
'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-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-curtain-view-css' => '2bdaf026',
'phui-document-summary-view-css' => '9ca48bdf',
- 'phui-document-view-css' => '391e1e84',
+ 'phui-document-view-css' => '878c2f52',
'phui-document-view-pro-css' => '8af7ea27',
'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-head-thing-view-css' => 'fd311e5f',
'phui-header-view-css' => '31dc6c72',
'phui-hovercard' => '1bd28176',
'phui-hovercard-view-css' => 'f0592bcf',
'phui-icon-set-selector-css' => '87db8fee',
- 'phui-icon-view-css' => '5c4a5de6',
+ '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' => '6ae18df0',
+ 'phui-oi-list-view-css' => 'ae1404ba',
'phui-oi-simple-ui-css' => 'a8beebea',
'phui-pager-css' => 'edcbc226',
'phui-pinboard-view-css' => '2495140e',
- 'phui-property-list-view-css' => '2dc7993f',
+ 'phui-property-list-view-css' => 'de4754d8',
'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' => 'e2ef62b1',
+ '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' => '442efd08',
- 'phuix-autocomplete' => 'e0731603',
+ 'phuix-action-view' => '8d4a8c72',
+ 'phuix-autocomplete' => 'df1bbd34',
'phuix-button-view' => '8a91e1ac',
'phuix-dropdown-menu' => '04b2ae03',
- 'phuix-form-control-view' => '1dd0870c',
+ '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' => '7dae7f18',
+ 'setup-issue-css' => '30ee0173',
'sprite-login-css' => '396f3c3a',
'sprite-tokens-css' => '9cdfd599',
'syntax-default-css' => '9923583c',
'syntax-highlighting-css' => 'cae95e89',
'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',
),
- '008faf9c' => array(
- 'javelin-install',
- 'javelin-dom',
- 'javelin-stratcom',
- 'javelin-util',
- 'phabricator-notification-css',
- ),
'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',
+ ),
+ '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',
),
- '0fcf201c' => array(
- 'javelin-install',
- 'javelin-util',
- 'javelin-dom',
- 'javelin-typeahead-normalizer',
- ),
- '1499a8cb' => array(
+ '13e39479' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-history',
),
'15d5ff71' => array(
'aphront-typeahead-control-css',
'phui-tag-view-css',
),
- '17bb8539' => array(
- 'javelin-behavior',
- 'javelin-stratcom',
- 'javelin-util',
- 'javelin-dom',
- 'javelin-request',
- 'phabricator-keyboard-shortcut',
- 'phabricator-darklog',
- 'phabricator-darkmessage',
- ),
'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',
),
- '1dd0870c' => array(
- 'javelin-install',
- 'javelin-dom',
- ),
- '1e911d0f' => array(
- 'javelin-stratcom',
- 'javelin-request',
+ '1db13e70' => array(
+ 'javelin-behavior',
'javelin-dom',
- 'javelin-vector',
- 'javelin-install',
- 'javelin-util',
- 'javelin-mask',
- 'javelin-uri',
- 'javelin-routable',
+ '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',
- ),
'2ab10a76' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
'javelin-uri',
),
'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',
- ),
'3ab51e2c' => array(
'javelin-behavior',
'javelin-behavior-device',
'javelin-stratcom',
'javelin-vector',
'javelin-dom',
'javelin-magical-init',
),
- '3b77efdd' => array(
- 'javelin-install',
- 'phuix-button-view',
- ),
'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',
),
42126667 => array(
'javelin-behavior',
'javelin-dom',
'javelin-request',
),
'4250a34e' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'javelin-vector',
'javelin-stratcom',
'javelin-workflow',
'javelin-workboard-controller',
),
- '442efd08' => array(
- 'javelin-install',
- 'javelin-dom',
- 'javelin-util',
- ),
'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',
),
- '4cc4f460' => 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',
- ),
'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',
),
'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',
),
- '560f41da' => array(
- 'javelin-behavior',
- 'javelin-stratcom',
- 'javelin-dom',
- 'javelin-mask',
- 'javelin-util',
- 'phuix-icon-view',
- 'phabricator-busy',
- ),
'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',
+ ),
'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',
),
- '6882e80a' => array(
- 'javelin-dom',
- ),
'68af71ca' => array(
'javelin-install',
'javelin-dom',
'phuix-button-view',
),
'69adf288' => array(
'javelin-install',
),
- '6ad39b6f' => array(
+ '6a726c55' => array(
+ 'javelin-stratcom',
+ 'javelin-request',
+ 'javelin-dom',
+ 'javelin-vector',
'javelin-install',
- 'javelin-event',
'javelin-util',
- 'javelin-magical-init',
+ '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',
),
'70baed2f' => array(
'javelin-install',
'javelin-dom',
'javelin-vector',
'javelin-util',
),
71237763 => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
'javelin-workflow',
'phabricator-draggable-list',
),
'7319e029' => array(
'javelin-behavior',
'javelin-dom',
),
'73d09eef' => array(
'javelin-behavior',
'javelin-vector',
'javelin-dom',
),
'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',
),
'834a1173' => array(
'javelin-behavior',
'javelin-scrollbar',
),
+ '836f966d' => array(
+ 'javelin-behavior',
+ 'javelin-behavior-device',
+ 'javelin-stratcom',
+ 'javelin-dom',
+ 'javelin-magical-init',
+ 'javelin-vector',
+ 'javelin-request',
+ 'javelin-util',
+ ),
'8499b6ab' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
),
- '8561f3c3' => array(
- 'katex-css',
- ),
'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(
+ '9108ee1a' => array(
'javelin-behavior',
- 'javelin-stratcom',
'javelin-dom',
+ 'javelin-stratcom',
),
- '93d0c9e3' => array(
+ '92b9ec77' => array(
'javelin-behavior',
'javelin-stratcom',
- 'javelin-workflow',
'javelin-dom',
),
- '947753e0' => array(
+ '93d0c9e3' => array(
'javelin-behavior',
- 'javelin-behavior-device',
'javelin-stratcom',
+ 'javelin-workflow',
'javelin-dom',
- 'javelin-magical-init',
- 'javelin-vector',
- 'javelin-request',
- 'javelin-util',
),
'949c0fe5' => array(
'javelin-install',
),
'94b750d2' => array(
'javelin-install',
'javelin-stratcom',
'javelin-util',
'javelin-behavior',
'javelin-json',
'javelin-dom',
'javelin-resource',
'javelin-routable',
),
'960f6a39' => array(
'javelin-behavior',
'javelin-dom',
'javelin-uri',
'javelin-mask',
'phabricator-drag-and-drop-file-upload',
),
- '99abf4cd' => array(
- 'javelin-dom',
- 'javelin-util',
- 'javelin-stratcom',
- 'javelin-install',
- 'javelin-workflow',
- 'javelin-router',
- 'javelin-behavior-device',
- 'javelin-vector',
- 'phabricator-diff-inline',
- ),
'9a6dd75c' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-workflow',
'javelin-dom',
'phuix-form-control-view',
'phuix-icon-view',
'javelin-behavior-phabricator-gesture',
),
'9bbf3762' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'javelin-workflow',
'javelin-stratcom',
),
'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',
),
- 'aa1733d0' => array(
- 'multirow-row-manager',
- 'javelin-install',
- 'path-typeahead',
- 'javelin-dom',
- 'javelin-util',
- 'phabricator-prefab',
- ),
'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',
),
'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',
),
'bf84345b' => array(
'phui-inline-comment-view-css',
),
'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',
),
'cae95e89' => array(
'syntax-default-css',
),
'cd2b9b77' => array(
'phui-oi-list-view-css',
),
'd057e45a' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
'javelin-workflow',
'javelin-util',
'phabricator-notification',
'conpherence-thread-manager',
),
- 'd0a99ab4' => array(
- 'javelin-behavior',
- 'javelin-typeahead-ondemand-source',
- 'javelin-typeahead',
- 'javelin-dom',
- 'javelin-uri',
- 'javelin-util',
- 'javelin-stratcom',
- 'phabricator-prefab',
- 'phuix-icon-view',
- ),
'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',
),
'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',
),
- 'e0731603' => array(
+ 'df1bbd34' => array(
'javelin-install',
'javelin-dom',
'phuix-icon-view',
'phabricator-prefab',
),
'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',
),
- 'e5822781' => array(
- 'javelin-behavior',
- 'javelin-dom',
- 'javelin-json',
- 'javelin-workflow',
- 'javelin-magical-init',
+ 'e74b7517' => array(
+ 'javelin-install',
+ 'phuix-button-view',
),
'e83d28f3' => array(
'javelin-dom',
),
'e9581f08' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-workflow',
'javelin-dom',
'phabricator-draggable-list',
),
'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',
),
'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',
),
- 'f7fc67ec' => array(
- 'javelin-install',
- 'javelin-typeahead',
- 'javelin-dom',
- 'javelin-request',
- 'javelin-typeahead-ondemand-source',
- 'javelin-util',
- ),
'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',
),
- 'darkconsole.pkg.js' => array(
- 'javelin-behavior-dark-console',
- 'javelin-behavior-error-log',
- ),
'differential.pkg.css' => array(
'differential-core-view-css',
'differential-changeset-view-css',
'differential-revision-history-css',
'differential-revision-list-css',
'differential-table-of-contents-css',
'differential-revision-comment-css',
'differential-revision-add-comment-css',
'phabricator-object-selector-css',
'phabricator-content-source-view-css',
'inline-comment-summary-css',
'phui-inline-comment-view-css',
'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-load-blame',
'javelin-behavior-differential-user-select',
'javelin-behavior-aphront-more',
'phabricator-diff-inline',
'phabricator-diff-changeset',
'phabricator-diff-changeset-list',
),
'diffusion.pkg.css' => array(
'diffusion-icons-css',
),
'diffusion.pkg.js' => array(
'javelin-behavior-diffusion-pull-lastmodified',
'javelin-behavior-diffusion-commit-graph',
'javelin-behavior-audit-preview',
),
'maniphest.pkg.css' => array(
'maniphest-task-summary-css',
),
'maniphest.pkg.js' => array(
'javelin-behavior-maniphest-batch-selector',
'javelin-behavior-maniphest-subpriority-editor',
'javelin-behavior-maniphest-list-editor',
),
),
);
diff --git a/resources/celerity/packages.php b/resources/celerity/packages.php
index abbf1d77e..3cec50867 100644
--- a/resources/celerity/packages.php
+++ b/resources/celerity/packages.php
@@ -1,231 +1,227 @@
<?php
return array(
'core.pkg.js' => array(
'javelin-util',
'javelin-install',
'javelin-event',
'javelin-stratcom',
'javelin-behavior',
'javelin-resource',
'javelin-request',
'javelin-vector',
'javelin-dom',
'javelin-json',
'javelin-uri',
'javelin-workflow',
'javelin-mask',
'javelin-typeahead',
'javelin-typeahead-normalizer',
'javelin-typeahead-source',
'javelin-typeahead-preloaded-source',
'javelin-typeahead-ondemand-source',
'javelin-tokenizer',
'javelin-history',
'javelin-router',
'javelin-routable',
'javelin-behavior-aphront-basic-tokenizer',
'javelin-behavior-workflow',
'javelin-behavior-aphront-form-disable-on-submit',
'phabricator-keyboard-shortcut-manager',
'phabricator-keyboard-shortcut',
'javelin-behavior-phabricator-keyboard-shortcuts',
'javelin-behavior-refresh-csrf',
'javelin-behavior-phabricator-watch-anchor',
'javelin-behavior-phabricator-autofocus',
'phuix-dropdown-menu',
'phuix-action-list-view',
'phuix-action-view',
'phuix-icon-view',
'phabricator-phtize',
'javelin-behavior-phabricator-oncopy',
'phabricator-tooltip',
'javelin-behavior-phabricator-tooltips',
'phabricator-prefab',
'javelin-behavior-device',
'javelin-behavior-toggle-class',
'javelin-behavior-lightbox-attachments',
'phabricator-busy',
'javelin-sound',
'javelin-aphlict',
'phabricator-notification',
'javelin-behavior-aphlict-listen',
'javelin-behavior-phabricator-search-typeahead',
'javelin-behavior-aphlict-dropdown',
'javelin-behavior-history-install',
'javelin-behavior-phabricator-gesture',
'javelin-behavior-phabricator-active-nav',
'javelin-behavior-phabricator-nav',
'javelin-behavior-phabricator-remarkup-assist',
'phabricator-textareautils',
'phabricator-file-upload',
'javelin-behavior-global-drag-and-drop',
'javelin-behavior-phabricator-reveal-content',
'phui-hovercard',
'javelin-behavior-phui-hovercards',
'javelin-color',
'javelin-fx',
'phabricator-draggable-list',
'javelin-behavior-phabricator-transaction-list',
'javelin-behavior-phabricator-show-older-transactions',
'javelin-behavior-phui-dropdown-menu',
'javelin-behavior-doorkeeper-tag',
'phabricator-title',
'javelin-leader',
'javelin-websocket',
'javelin-behavior-dashboard-async-panel',
'javelin-behavior-dashboard-tab-panel',
'javelin-quicksand',
'javelin-behavior-quicksand-blacklist',
'javelin-behavior-high-security-warning',
'javelin-behavior-read-only-warning',
'javelin-scrollbar',
'javelin-behavior-scrollbar',
'javelin-behavior-durable-column',
'conpherence-thread-manager',
'javelin-behavior-detect-timezone',
'javelin-behavior-setup-check-https',
'javelin-behavior-aphlict-status',
'javelin-behavior-user-menu',
'phabricator-favicon',
),
'core.pkg.css' => array(
'phabricator-core-css',
'phabricator-zindex-css',
'phui-button-css',
'phui-button-simple-css',
'phui-theme-css',
'phabricator-standard-page-view',
'aphront-dialog-view-css',
'phui-form-view-css',
'aphront-panel-view-css',
'aphront-table-view-css',
'aphront-tokenizer-control-css',
'aphront-typeahead-control-css',
'aphront-list-filter-view-css',
'application-search-view-css',
'phabricator-remarkup-css',
'syntax-highlighting-css',
'syntax-default-css',
'phui-pager-css',
'aphront-tooltip-css',
'phabricator-flag-css',
'phui-info-view-css',
'phabricator-main-menu-view',
'phabricator-notification-css',
'phabricator-notification-menu-css',
'phui-lightbox-css',
'phui-comment-panel-css',
'phui-header-view-css',
'phabricator-nav-view-css',
'phui-basic-nav-view-css',
'phui-crumbs-view-css',
'phui-oi-list-view-css',
'phui-oi-color-css',
'phui-oi-big-ui-css',
'phui-oi-drag-ui-css',
'phui-oi-simple-ui-css',
'phui-oi-flush-ui-css',
'global-drag-and-drop-css',
'phui-spacing-css',
'phui-form-css',
'phui-icon-view-css',
'phabricator-action-list-view-css',
'phui-property-list-view-css',
'phui-tag-view-css',
'phui-list-view-css',
'font-fontawesome',
'font-lato',
'phui-font-icon-base-css',
'phui-fontkit-css',
'phui-box-css',
'phui-object-box-css',
'phui-timeline-view-css',
'phui-two-column-view-css',
'phui-curtain-view-css',
'sprite-login-css',
'sprite-tokens-css',
'tokens-css',
'auth-css',
'phui-status-list-view-css',
'phui-feed-story-css',
'phabricator-feed-css',
'phabricator-dashboard-css',
'aphront-multi-column-view-css',
),
'conpherence.pkg.css' => array(
'conpherence-durable-column-view',
'conpherence-menu-css',
'conpherence-color-css',
'conpherence-message-pane-css',
'conpherence-notification-css',
'conpherence-transaction-css',
'conpherence-participant-pane-css',
'conpherence-header-pane-css',
),
'conpherence.pkg.js' => array(
'javelin-behavior-conpherence-menu',
'javelin-behavior-conpherence-participant-pane',
'javelin-behavior-conpherence-pontificate',
'javelin-behavior-toggle-widget',
),
'differential.pkg.css' => array(
'differential-core-view-css',
'differential-changeset-view-css',
'differential-revision-history-css',
'differential-revision-list-css',
'differential-table-of-contents-css',
'differential-revision-comment-css',
'differential-revision-add-comment-css',
'phabricator-object-selector-css',
'phabricator-content-source-view-css',
'inline-comment-summary-css',
'phui-inline-comment-view-css',
'phabricator-filetree-view-css',
),
'differential.pkg.js' => array(
'phabricator-drag-and-drop-file-upload',
'phabricator-shaped-request',
'javelin-behavior-differential-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-load-blame',
'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',
),
- 'darkconsole.pkg.js' => array(
- 'javelin-behavior-dark-console',
- 'javelin-behavior-error-log',
- ),
);
diff --git a/resources/emoji/manifest.json b/resources/emoji/manifest.json
index 2d8388d27..47568fb43 100644
--- a/resources/emoji/manifest.json
+++ b/resources/emoji/manifest.json
@@ -1,1626 +1,1630 @@
{
"8ball": "\ud83c\udfb1",
"a": "\ud83c\udd70",
"ab": "\ud83c\udd8e",
"abc": "\ud83d\udd24",
"abcd": "\ud83d\udd21",
"accept": "\ud83c\ude51",
"aerial_tramway": "\ud83d\udea1",
"airplane": "\u2708",
"airplane_arriving": "\ud83d\udeec",
"airplane_departure": "\ud83d\udeeb",
"airplane_small": "\ud83d\udee9",
"alarm_clock": "\u23f0",
"alembic": "\u2697",
"alien": "\ud83d\udc7d",
"ambulance": "\ud83d\ude91",
"amphora": "\ud83c\udffa",
"anchor": "\u2693",
"angel": "\ud83d\udc7c",
"angel_tone1": "\ud83d\udc7c\ud83c\udffb",
"angel_tone2": "\ud83d\udc7c\ud83c\udffc",
"angel_tone3": "\ud83d\udc7c\ud83c\udffd",
"angel_tone4": "\ud83d\udc7c\ud83c\udffe",
"angel_tone5": "\ud83d\udc7c\ud83c\udfff",
"anger": "\ud83d\udca2",
"anger_right": "\ud83d\uddef",
"angry": "\ud83d\ude20",
"anguished": "\ud83d\ude27",
"ant": "\ud83d\udc1c",
"apple": "\ud83c\udf4e",
"aquarius": "\u2652",
"aries": "\u2648",
"arrow_backward": "\u25c0",
"arrow_double_down": "\u23ec",
"arrow_double_up": "\u23eb",
"arrow_down": "\u2b07",
"arrow_down_small": "\ud83d\udd3d",
"arrow_forward": "\u25b6",
"arrow_heading_down": "\u2935",
"arrow_heading_up": "\u2934",
"arrow_left": "\u2b05",
"arrow_lower_left": "\u2199",
"arrow_lower_right": "\u2198",
"arrow_right": "\u27a1",
"arrow_right_hook": "\u21aa",
"arrow_up": "\u2b06",
"arrow_up_down": "\u2195",
"arrow_up_small": "\ud83d\udd3c",
"arrow_upper_left": "\u2196",
"arrow_upper_right": "\u2197",
"arrows_clockwise": "\ud83d\udd03",
"arrows_counterclockwise": "\ud83d\udd04",
"art": "\ud83c\udfa8",
"articulated_lorry": "\ud83d\ude9b",
"asterisk": "*\u20e3",
"astonished": "\ud83d\ude32",
"athletic_shoe": "\ud83d\udc5f",
"atm": "\ud83c\udfe7",
"atom": "\u269b",
"b": "\ud83c\udd71",
"baby": "\ud83d\udc76",
"baby_bottle": "\ud83c\udf7c",
"baby_chick": "\ud83d\udc24",
"baby_symbol": "\ud83d\udebc",
"baby_tone1": "\ud83d\udc76\ud83c\udffb",
"baby_tone2": "\ud83d\udc76\ud83c\udffc",
"baby_tone3": "\ud83d\udc76\ud83c\udffd",
"baby_tone4": "\ud83d\udc76\ud83c\udffe",
"baby_tone5": "\ud83d\udc76\ud83c\udfff",
"back": "\ud83d\udd19",
"badminton": "\ud83c\udff8",
"baggage_claim": "\ud83d\udec4",
"balloon": "\ud83c\udf88",
"ballot_box": "\ud83d\uddf3",
"ballot_box_with_check": "\u2611",
"bamboo": "\ud83c\udf8d",
"banana": "\ud83c\udf4c",
"bangbang": "\u203c",
"bank": "\ud83c\udfe6",
"bar_chart": "\ud83d\udcca",
"barber": "\ud83d\udc88",
"baseball": "\u26be",
"basketball": "\ud83c\udfc0",
"basketball_player": "\u26f9",
"basketball_player_tone1": "\u26f9\ud83c\udffb",
"basketball_player_tone2": "\u26f9\ud83c\udffc",
"basketball_player_tone3": "\u26f9\ud83c\udffd",
"basketball_player_tone4": "\u26f9\ud83c\udffe",
"basketball_player_tone5": "\u26f9\ud83c\udfff",
"bath": "\ud83d\udec0",
"bath_tone1": "\ud83d\udec0\ud83c\udffb",
"bath_tone2": "\ud83d\udec0\ud83c\udffc",
"bath_tone3": "\ud83d\udec0\ud83c\udffd",
"bath_tone4": "\ud83d\udec0\ud83c\udffe",
"bath_tone5": "\ud83d\udec0\ud83c\udfff",
"bathtub": "\ud83d\udec1",
"battery": "\ud83d\udd0b",
"beach": "\ud83c\udfd6",
"beach_umbrella": "\u26f1",
"bear": "\ud83d\udc3b",
"bed": "\ud83d\udecf",
"bee": "\ud83d\udc1d",
"beer": "\ud83c\udf7a",
"beers": "\ud83c\udf7b",
"beetle": "\ud83d\udc1e",
"beginner": "\ud83d\udd30",
"bell": "\ud83d\udd14",
"bellhop": "\ud83d\udece",
"bento": "\ud83c\udf71",
"bicyclist": "\ud83d\udeb4",
"bicyclist_tone1": "\ud83d\udeb4\ud83c\udffb",
"bicyclist_tone2": "\ud83d\udeb4\ud83c\udffc",
"bicyclist_tone3": "\ud83d\udeb4\ud83c\udffd",
"bicyclist_tone4": "\ud83d\udeb4\ud83c\udffe",
"bicyclist_tone5": "\ud83d\udeb4\ud83c\udfff",
"bike": "\ud83d\udeb2",
"bikini": "\ud83d\udc59",
"biohazard": "\u2623",
"bird": "\ud83d\udc26",
"birthday": "\ud83c\udf82",
"black_circle": "\u26ab",
"black_joker": "\ud83c\udccf",
"black_large_square": "\u2b1b",
"black_medium_small_square": "\u25fe",
"black_medium_square": "\u25fc",
"black_nib": "\u2712",
"black_small_square": "\u25aa",
"black_square_button": "\ud83d\udd32",
"blossom": "\ud83c\udf3c",
"blowfish": "\ud83d\udc21",
"blue_book": "\ud83d\udcd8",
"blue_car": "\ud83d\ude99",
"blue_heart": "\ud83d\udc99",
"blush": "\ud83d\ude0a",
"boar": "\ud83d\udc17",
"bomb": "\ud83d\udca3",
"book": "\ud83d\udcd6",
"bookmark": "\ud83d\udd16",
"bookmark_tabs": "\ud83d\udcd1",
"books": "\ud83d\udcda",
"boom": "\ud83d\udca5",
"boot": "\ud83d\udc62",
"bouquet": "\ud83d\udc90",
"bow": "\ud83d\ude47",
"bow_and_arrow": "\ud83c\udff9",
"bow_tone1": "\ud83d\ude47\ud83c\udffb",
"bow_tone2": "\ud83d\ude47\ud83c\udffc",
"bow_tone3": "\ud83d\ude47\ud83c\udffd",
"bow_tone4": "\ud83d\ude47\ud83c\udffe",
"bow_tone5": "\ud83d\ude47\ud83c\udfff",
"bowling": "\ud83c\udfb3",
"boy": "\ud83d\udc66",
"boy_tone1": "\ud83d\udc66\ud83c\udffb",
"boy_tone2": "\ud83d\udc66\ud83c\udffc",
"boy_tone3": "\ud83d\udc66\ud83c\udffd",
"boy_tone4": "\ud83d\udc66\ud83c\udffe",
"boy_tone5": "\ud83d\udc66\ud83c\udfff",
"bread": "\ud83c\udf5e",
"bride_with_veil": "\ud83d\udc70",
"bride_with_veil_tone1": "\ud83d\udc70\ud83c\udffb",
"bride_with_veil_tone2": "\ud83d\udc70\ud83c\udffc",
"bride_with_veil_tone3": "\ud83d\udc70\ud83c\udffd",
"bride_with_veil_tone4": "\ud83d\udc70\ud83c\udffe",
"bride_with_veil_tone5": "\ud83d\udc70\ud83c\udfff",
"bridge_at_night": "\ud83c\udf09",
"briefcase": "\ud83d\udcbc",
"broken_heart": "\ud83d\udc94",
"bug": "\ud83d\udc1b",
"bulb": "\ud83d\udca1",
"bullettrain_front": "\ud83d\ude85",
"bullettrain_side": "\ud83d\ude84",
"burrito": "\ud83c\udf2f",
"bus": "\ud83d\ude8c",
"busstop": "\ud83d\ude8f",
"bust_in_silhouette": "\ud83d\udc64",
"busts_in_silhouette": "\ud83d\udc65",
"cactus": "\ud83c\udf35",
"cake": "\ud83c\udf70",
"calendar": "\ud83d\udcc6",
"calendar_spiral": "\ud83d\uddd3",
"calling": "\ud83d\udcf2",
"camel": "\ud83d\udc2b",
"camera": "\ud83d\udcf7",
"camera_with_flash": "\ud83d\udcf8",
"camping": "\ud83c\udfd5",
"cancer": "\u264b",
"candle": "\ud83d\udd6f",
"candy": "\ud83c\udf6c",
"capital_abcd": "\ud83d\udd20",
"capricorn": "\u2651",
"card_box": "\ud83d\uddc3",
"card_index": "\ud83d\udcc7",
"carousel_horse": "\ud83c\udfa0",
"cat": "\ud83d\udc31",
"cat2": "\ud83d\udc08",
"cd": "\ud83d\udcbf",
"chains": "\u26d3",
"champagne": "\ud83c\udf7e",
"chart": "\ud83d\udcb9",
"chart_with_downwards_trend": "\ud83d\udcc9",
"chart_with_upwards_trend": "\ud83d\udcc8",
"checkered_flag": "\ud83c\udfc1",
"cheese": "\ud83e\uddc0",
"cherries": "\ud83c\udf52",
"cherry_blossom": "\ud83c\udf38",
"chestnut": "\ud83c\udf30",
"chicken": "\ud83d\udc14",
"children_crossing": "\ud83d\udeb8",
"chipmunk": "\ud83d\udc3f",
"chocolate_bar": "\ud83c\udf6b",
"christmas_tree": "\ud83c\udf84",
"church": "\u26ea",
"cinema": "\ud83c\udfa6",
"circus_tent": "\ud83c\udfaa",
"city_dusk": "\ud83c\udf06",
"city_sunset": "\ud83c\udf07",
"cityscape": "\ud83c\udfd9",
"cl": "\ud83c\udd91",
"clap": "\ud83d\udc4f",
"clap_tone1": "\ud83d\udc4f\ud83c\udffb",
"clap_tone2": "\ud83d\udc4f\ud83c\udffc",
"clap_tone3": "\ud83d\udc4f\ud83c\udffd",
"clap_tone4": "\ud83d\udc4f\ud83c\udffe",
"clap_tone5": "\ud83d\udc4f\ud83c\udfff",
"clapper": "\ud83c\udfac",
"classical_building": "\ud83c\udfdb",
"clipboard": "\ud83d\udccb",
"clock": "\ud83d\udd70",
"clock1": "\ud83d\udd50",
"clock10": "\ud83d\udd59",
"clock1030": "\ud83d\udd65",
"clock11": "\ud83d\udd5a",
"clock1130": "\ud83d\udd66",
"clock12": "\ud83d\udd5b",
"clock1230": "\ud83d\udd67",
"clock130": "\ud83d\udd5c",
"clock2": "\ud83d\udd51",
"clock230": "\ud83d\udd5d",
"clock3": "\ud83d\udd52",
"clock330": "\ud83d\udd5e",
"clock4": "\ud83d\udd53",
"clock430": "\ud83d\udd5f",
"clock5": "\ud83d\udd54",
"clock530": "\ud83d\udd60",
"clock6": "\ud83d\udd55",
"clock630": "\ud83d\udd61",
"clock7": "\ud83d\udd56",
"clock730": "\ud83d\udd62",
"clock8": "\ud83d\udd57",
"clock830": "\ud83d\udd63",
"clock9": "\ud83d\udd58",
"clock930": "\ud83d\udd64",
"closed_book": "\ud83d\udcd5",
"closed_lock_with_key": "\ud83d\udd10",
"closed_umbrella": "\ud83c\udf02",
"cloud": "\u2601",
"cloud_lightning": "\ud83c\udf29",
"cloud_rain": "\ud83c\udf27",
"cloud_snow": "\ud83c\udf28",
"cloud_tornado": "\ud83c\udf2a",
"clubs": "\u2663",
"cocktail": "\ud83c\udf78",
"coffee": "\u2615",
"coffin": "\u26b0",
"cold_sweat": "\ud83d\ude30",
"comet": "\u2604",
"compression": "\ud83d\udddc",
"computer": "\ud83d\udcbb",
"confetti_ball": "\ud83c\udf8a",
"confounded": "\ud83d\ude16",
"confused": "\ud83d\ude15",
"congratulations": "\u3297",
"construction": "\ud83d\udea7",
"construction_site": "\ud83c\udfd7",
"construction_worker": "\ud83d\udc77",
"construction_worker_tone1": "\ud83d\udc77\ud83c\udffb",
"construction_worker_tone2": "\ud83d\udc77\ud83c\udffc",
"construction_worker_tone3": "\ud83d\udc77\ud83c\udffd",
"construction_worker_tone4": "\ud83d\udc77\ud83c\udffe",
"construction_worker_tone5": "\ud83d\udc77\ud83c\udfff",
"control_knobs": "\ud83c\udf9b",
"convenience_store": "\ud83c\udfea",
"cookie": "\ud83c\udf6a",
"cool": "\ud83c\udd92",
"cop": "\ud83d\udc6e",
"cop_tone1": "\ud83d\udc6e\ud83c\udffb",
"cop_tone2": "\ud83d\udc6e\ud83c\udffc",
"cop_tone3": "\ud83d\udc6e\ud83c\udffd",
"cop_tone4": "\ud83d\udc6e\ud83c\udffe",
"cop_tone5": "\ud83d\udc6e\ud83c\udfff",
"copyright": "\u00a9",
"corn": "\ud83c\udf3d",
"couch": "\ud83d\udecb",
"couple": "\ud83d\udc6b",
"couple_mm": "\ud83d\udc68\u2764\ud83d\udc68",
"couple_with_heart": "\ud83d\udc91",
"couple_ww": "\ud83d\udc69\u2764\ud83d\udc69",
"couplekiss": "\ud83d\udc8f",
"cow": "\ud83d\udc2e",
"cow2": "\ud83d\udc04",
"crab": "\ud83e\udd80",
"crayon": "\ud83d\udd8d",
"credit_card": "\ud83d\udcb3",
"crescent_moon": "\ud83c\udf19",
"cricket": "\ud83c\udfcf",
"crocodile": "\ud83d\udc0a",
"cross": "\u271d",
"crossed_flags": "\ud83c\udf8c",
"crossed_swords": "\u2694",
"crown": "\ud83d\udc51",
"cruise_ship": "\ud83d\udef3",
"cry": "\ud83d\ude22",
"crying_cat_face": "\ud83d\ude3f",
"crystal_ball": "\ud83d\udd2e",
"cupid": "\ud83d\udc98",
"curly_loop": "\u27b0",
"currency_exchange": "\ud83d\udcb1",
"curry": "\ud83c\udf5b",
"custard": "\ud83c\udf6e",
"customs": "\ud83d\udec3",
"cyclone": "\ud83c\udf00",
"dagger": "\ud83d\udde1",
"dancer": "\ud83d\udc83",
"dancer_tone1": "\ud83d\udc83\ud83c\udffb",
"dancer_tone2": "\ud83d\udc83\ud83c\udffc",
"dancer_tone3": "\ud83d\udc83\ud83c\udffd",
"dancer_tone4": "\ud83d\udc83\ud83c\udffe",
"dancer_tone5": "\ud83d\udc83\ud83c\udfff",
"dancers": "\ud83d\udc6f",
"dango": "\ud83c\udf61",
"dark_sunglasses": "\ud83d\udd76",
"dart": "\ud83c\udfaf",
"dash": "\ud83d\udca8",
"date": "\ud83d\udcc5",
"deciduous_tree": "\ud83c\udf33",
"department_store": "\ud83c\udfec",
"desert": "\ud83c\udfdc",
"desktop": "\ud83d\udda5",
"diamond_shape_with_a_dot_inside": "\ud83d\udca0",
"diamonds": "\u2666",
"disappointed": "\ud83d\ude1e",
"disappointed_relieved": "\ud83d\ude25",
"dividers": "\ud83d\uddc2",
"dizzy": "\ud83d\udcab",
"dizzy_face": "\ud83d\ude35",
"do_not_litter": "\ud83d\udeaf",
"dog": "\ud83d\udc36",
"dog2": "\ud83d\udc15",
"dollar": "\ud83d\udcb5",
"dolls": "\ud83c\udf8e",
"dolphin": "\ud83d\udc2c",
"door": "\ud83d\udeaa",
"doughnut": "\ud83c\udf69",
"dove": "\ud83d\udd4a",
"dragon": "\ud83d\udc09",
"dragon_face": "\ud83d\udc32",
"dress": "\ud83d\udc57",
"dromedary_camel": "\ud83d\udc2a",
"droplet": "\ud83d\udca7",
"dvd": "\ud83d\udcc0",
"e-mail": "\ud83d\udce7",
"ear": "\ud83d\udc42",
"ear_of_rice": "\ud83c\udf3e",
"ear_tone1": "\ud83d\udc42\ud83c\udffb",
"ear_tone2": "\ud83d\udc42\ud83c\udffc",
"ear_tone3": "\ud83d\udc42\ud83c\udffd",
"ear_tone4": "\ud83d\udc42\ud83c\udffe",
"ear_tone5": "\ud83d\udc42\ud83c\udfff",
"earth_africa": "\ud83c\udf0d",
"earth_americas": "\ud83c\udf0e",
"earth_asia": "\ud83c\udf0f",
"egg": "\ud83c\udf73",
"eggplant": "\ud83c\udf46",
"eight": "8\u20e3",
"eight_pointed_black_star": "\u2734",
"eight_spoked_asterisk": "\u2733",
"electric_plug": "\ud83d\udd0c",
"elephant": "\ud83d\udc18",
"end": "\ud83d\udd1a",
"envelope": "\u2709",
"envelope_with_arrow": "\ud83d\udce9",
"euro": "\ud83d\udcb6",
"european_castle": "\ud83c\udff0",
"european_post_office": "\ud83c\udfe4",
"evergreen_tree": "\ud83c\udf32",
"exclamation": "\u2757",
"expressionless": "\ud83d\ude11",
"eye": "\ud83d\udc41",
"eye_in_speech_bubble": "\ud83d\udc41\ud83d\udde8",
"eyeglasses": "\ud83d\udc53",
"eyes": "\ud83d\udc40",
"factory": "\ud83c\udfed",
"fallen_leaf": "\ud83c\udf42",
"family": "\ud83d\udc6a",
"family_mmb": "\ud83d\udc68\ud83d\udc68\ud83d\udc66",
"family_mmbb": "\ud83d\udc68\ud83d\udc68\ud83d\udc66\ud83d\udc66",
"family_mmg": "\ud83d\udc68\ud83d\udc68\ud83d\udc67",
"family_mmgb": "\ud83d\udc68\ud83d\udc68\ud83d\udc67\ud83d\udc66",
"family_mmgg": "\ud83d\udc68\ud83d\udc68\ud83d\udc67\ud83d\udc67",
"family_mwbb": "\ud83d\udc68\ud83d\udc69\ud83d\udc66\ud83d\udc66",
"family_mwg": "\ud83d\udc68\ud83d\udc69\ud83d\udc67",
"family_mwgb": "\ud83d\udc68\ud83d\udc69\ud83d\udc67\ud83d\udc66",
"family_mwgg": "\ud83d\udc68\ud83d\udc69\ud83d\udc67\ud83d\udc67",
"family_wwb": "\ud83d\udc69\ud83d\udc69\ud83d\udc66",
"family_wwbb": "\ud83d\udc69\ud83d\udc69\ud83d\udc66\ud83d\udc66",
"family_wwg": "\ud83d\udc69\ud83d\udc69\ud83d\udc67",
"family_wwgb": "\ud83d\udc69\ud83d\udc69\ud83d\udc67\ud83d\udc66",
"family_wwgg": "\ud83d\udc69\ud83d\udc69\ud83d\udc67\ud83d\udc67",
"fast_forward": "\u23e9",
"fax": "\ud83d\udce0",
"fearful": "\ud83d\ude28",
"feet": "\ud83d\udc3e",
"ferris_wheel": "\ud83c\udfa1",
"ferry": "\u26f4",
"field_hockey": "\ud83c\udfd1",
"file_cabinet": "\ud83d\uddc4",
"file_folder": "\ud83d\udcc1",
"film_frames": "\ud83c\udf9e",
"fire": "\ud83d\udd25",
"fire_engine": "\ud83d\ude92",
"fireworks": "\ud83c\udf86",
"first_quarter_moon": "\ud83c\udf13",
"first_quarter_moon_with_face": "\ud83c\udf1b",
"fish": "\ud83d\udc1f",
"fish_cake": "\ud83c\udf65",
"fishing_pole_and_fish": "\ud83c\udfa3",
"fist": "\u270a",
"fist_tone1": "\u270a\ud83c\udffb",
"fist_tone2": "\u270a\ud83c\udffc",
"fist_tone3": "\u270a\ud83c\udffd",
"fist_tone4": "\u270a\ud83c\udffe",
"fist_tone5": "\u270a\ud83c\udfff",
"five": "5\u20e3",
"flag_ac": "\ud83c\udde6\ud83c\udde8",
"flag_ad": "\ud83c\udde6\ud83c\udde9",
"flag_ae": "\ud83c\udde6\ud83c\uddea",
"flag_af": "\ud83c\udde6\ud83c\uddeb",
"flag_ag": "\ud83c\udde6\ud83c\uddec",
"flag_ai": "\ud83c\udde6\ud83c\uddee",
"flag_al": "\ud83c\udde6\ud83c\uddf1",
"flag_am": "\ud83c\udde6\ud83c\uddf2",
"flag_ao": "\ud83c\udde6\ud83c\uddf4",
"flag_aq": "\ud83c\udde6\ud83c\uddf6",
"flag_ar": "\ud83c\udde6\ud83c\uddf7",
"flag_as": "\ud83c\udde6\ud83c\uddf8",
"flag_at": "\ud83c\udde6\ud83c\uddf9",
"flag_au": "\ud83c\udde6\ud83c\uddfa",
"flag_aw": "\ud83c\udde6\ud83c\uddfc",
"flag_ax": "\ud83c\udde6\ud83c\uddfd",
"flag_az": "\ud83c\udde6\ud83c\uddff",
"flag_ba": "\ud83c\udde7\ud83c\udde6",
"flag_bb": "\ud83c\udde7\ud83c\udde7",
"flag_bd": "\ud83c\udde7\ud83c\udde9",
"flag_be": "\ud83c\udde7\ud83c\uddea",
"flag_bf": "\ud83c\udde7\ud83c\uddeb",
"flag_bg": "\ud83c\udde7\ud83c\uddec",
"flag_bh": "\ud83c\udde7\ud83c\udded",
"flag_bi": "\ud83c\udde7\ud83c\uddee",
"flag_bj": "\ud83c\udde7\ud83c\uddef",
"flag_bl": "\ud83c\udde7\ud83c\uddf1",
"flag_black": "\ud83c\udff4",
"flag_bm": "\ud83c\udde7\ud83c\uddf2",
"flag_bn": "\ud83c\udde7\ud83c\uddf3",
"flag_bo": "\ud83c\udde7\ud83c\uddf4",
"flag_bq": "\ud83c\udde7\ud83c\uddf6",
"flag_br": "\ud83c\udde7\ud83c\uddf7",
"flag_bs": "\ud83c\udde7\ud83c\uddf8",
"flag_bt": "\ud83c\udde7\ud83c\uddf9",
"flag_bv": "\ud83c\udde7\ud83c\uddfb",
"flag_bw": "\ud83c\udde7\ud83c\uddfc",
"flag_by": "\ud83c\udde7\ud83c\uddfe",
"flag_bz": "\ud83c\udde7\ud83c\uddff",
"flag_ca": "\ud83c\udde8\ud83c\udde6",
"flag_cc": "\ud83c\udde8\ud83c\udde8",
"flag_cd": "\ud83c\udde8\ud83c\udde9",
"flag_cf": "\ud83c\udde8\ud83c\uddeb",
"flag_cg": "\ud83c\udde8\ud83c\uddec",
"flag_ch": "\ud83c\udde8\ud83c\udded",
"flag_ci": "\ud83c\udde8\ud83c\uddee",
"flag_ck": "\ud83c\udde8\ud83c\uddf0",
"flag_cl": "\ud83c\udde8\ud83c\uddf1",
"flag_cm": "\ud83c\udde8\ud83c\uddf2",
"flag_cn": "\ud83c\udde8\ud83c\uddf3",
"flag_co": "\ud83c\udde8\ud83c\uddf4",
"flag_cp": "\ud83c\udde8\ud83c\uddf5",
"flag_cr": "\ud83c\udde8\ud83c\uddf7",
"flag_cu": "\ud83c\udde8\ud83c\uddfa",
"flag_cv": "\ud83c\udde8\ud83c\uddfb",
"flag_cw": "\ud83c\udde8\ud83c\uddfc",
"flag_cx": "\ud83c\udde8\ud83c\uddfd",
"flag_cy": "\ud83c\udde8\ud83c\uddfe",
"flag_cz": "\ud83c\udde8\ud83c\uddff",
"flag_de": "\ud83c\udde9\ud83c\uddea",
"flag_dg": "\ud83c\udde9\ud83c\uddec",
"flag_dj": "\ud83c\udde9\ud83c\uddef",
"flag_dk": "\ud83c\udde9\ud83c\uddf0",
"flag_dm": "\ud83c\udde9\ud83c\uddf2",
"flag_do": "\ud83c\udde9\ud83c\uddf4",
"flag_dz": "\ud83c\udde9\ud83c\uddff",
"flag_ea": "\ud83c\uddea\ud83c\udde6",
"flag_ec": "\ud83c\uddea\ud83c\udde8",
"flag_ee": "\ud83c\uddea\ud83c\uddea",
"flag_eg": "\ud83c\uddea\ud83c\uddec",
"flag_eh": "\ud83c\uddea\ud83c\udded",
"flag_er": "\ud83c\uddea\ud83c\uddf7",
"flag_es": "\ud83c\uddea\ud83c\uddf8",
"flag_et": "\ud83c\uddea\ud83c\uddf9",
"flag_eu": "\ud83c\uddea\ud83c\uddfa",
"flag_fi": "\ud83c\uddeb\ud83c\uddee",
"flag_fj": "\ud83c\uddeb\ud83c\uddef",
"flag_fk": "\ud83c\uddeb\ud83c\uddf0",
"flag_fm": "\ud83c\uddeb\ud83c\uddf2",
"flag_fo": "\ud83c\uddeb\ud83c\uddf4",
"flag_fr": "\ud83c\uddeb\ud83c\uddf7",
"flag_ga": "\ud83c\uddec\ud83c\udde6",
"flag_gb": "\ud83c\uddec\ud83c\udde7",
"flag_gd": "\ud83c\uddec\ud83c\udde9",
"flag_ge": "\ud83c\uddec\ud83c\uddea",
"flag_gf": "\ud83c\uddec\ud83c\uddeb",
"flag_gg": "\ud83c\uddec\ud83c\uddec",
"flag_gh": "\ud83c\uddec\ud83c\udded",
"flag_gi": "\ud83c\uddec\ud83c\uddee",
"flag_gl": "\ud83c\uddec\ud83c\uddf1",
"flag_gm": "\ud83c\uddec\ud83c\uddf2",
"flag_gn": "\ud83c\uddec\ud83c\uddf3",
"flag_gp": "\ud83c\uddec\ud83c\uddf5",
"flag_gq": "\ud83c\uddec\ud83c\uddf6",
"flag_gr": "\ud83c\uddec\ud83c\uddf7",
"flag_gs": "\ud83c\uddec\ud83c\uddf8",
"flag_gt": "\ud83c\uddec\ud83c\uddf9",
"flag_gu": "\ud83c\uddec\ud83c\uddfa",
"flag_gw": "\ud83c\uddec\ud83c\uddfc",
"flag_gy": "\ud83c\uddec\ud83c\uddfe",
"flag_hk": "\ud83c\udded\ud83c\uddf0",
"flag_hm": "\ud83c\udded\ud83c\uddf2",
"flag_hn": "\ud83c\udded\ud83c\uddf3",
"flag_hr": "\ud83c\udded\ud83c\uddf7",
"flag_ht": "\ud83c\udded\ud83c\uddf9",
"flag_hu": "\ud83c\udded\ud83c\uddfa",
"flag_ic": "\ud83c\uddee\ud83c\udde8",
"flag_id": "\ud83c\uddee\ud83c\udde9",
"flag_ie": "\ud83c\uddee\ud83c\uddea",
"flag_il": "\ud83c\uddee\ud83c\uddf1",
"flag_im": "\ud83c\uddee\ud83c\uddf2",
"flag_in": "\ud83c\uddee\ud83c\uddf3",
"flag_io": "\ud83c\uddee\ud83c\uddf4",
"flag_iq": "\ud83c\uddee\ud83c\uddf6",
"flag_ir": "\ud83c\uddee\ud83c\uddf7",
"flag_is": "\ud83c\uddee\ud83c\uddf8",
"flag_it": "\ud83c\uddee\ud83c\uddf9",
"flag_je": "\ud83c\uddef\ud83c\uddea",
"flag_jm": "\ud83c\uddef\ud83c\uddf2",
"flag_jo": "\ud83c\uddef\ud83c\uddf4",
"flag_jp": "\ud83c\uddef\ud83c\uddf5",
"flag_ke": "\ud83c\uddf0\ud83c\uddea",
"flag_kg": "\ud83c\uddf0\ud83c\uddec",
"flag_kh": "\ud83c\uddf0\ud83c\udded",
"flag_ki": "\ud83c\uddf0\ud83c\uddee",
"flag_km": "\ud83c\uddf0\ud83c\uddf2",
"flag_kn": "\ud83c\uddf0\ud83c\uddf3",
"flag_kp": "\ud83c\uddf0\ud83c\uddf5",
"flag_kr": "\ud83c\uddf0\ud83c\uddf7",
"flag_kw": "\ud83c\uddf0\ud83c\uddfc",
"flag_ky": "\ud83c\uddf0\ud83c\uddfe",
"flag_kz": "\ud83c\uddf0\ud83c\uddff",
"flag_la": "\ud83c\uddf1\ud83c\udde6",
"flag_lb": "\ud83c\uddf1\ud83c\udde7",
"flag_lc": "\ud83c\uddf1\ud83c\udde8",
"flag_li": "\ud83c\uddf1\ud83c\uddee",
"flag_lk": "\ud83c\uddf1\ud83c\uddf0",
"flag_lr": "\ud83c\uddf1\ud83c\uddf7",
"flag_ls": "\ud83c\uddf1\ud83c\uddf8",
"flag_lt": "\ud83c\uddf1\ud83c\uddf9",
"flag_lu": "\ud83c\uddf1\ud83c\uddfa",
"flag_lv": "\ud83c\uddf1\ud83c\uddfb",
"flag_ly": "\ud83c\uddf1\ud83c\uddfe",
"flag_ma": "\ud83c\uddf2\ud83c\udde6",
"flag_mc": "\ud83c\uddf2\ud83c\udde8",
"flag_md": "\ud83c\uddf2\ud83c\udde9",
"flag_me": "\ud83c\uddf2\ud83c\uddea",
"flag_mf": "\ud83c\uddf2\ud83c\uddeb",
"flag_mg": "\ud83c\uddf2\ud83c\uddec",
"flag_mh": "\ud83c\uddf2\ud83c\udded",
"flag_mk": "\ud83c\uddf2\ud83c\uddf0",
"flag_ml": "\ud83c\uddf2\ud83c\uddf1",
"flag_mm": "\ud83c\uddf2\ud83c\uddf2",
"flag_mn": "\ud83c\uddf2\ud83c\uddf3",
"flag_mo": "\ud83c\uddf2\ud83c\uddf4",
"flag_mp": "\ud83c\uddf2\ud83c\uddf5",
"flag_mq": "\ud83c\uddf2\ud83c\uddf6",
"flag_mr": "\ud83c\uddf2\ud83c\uddf7",
"flag_ms": "\ud83c\uddf2\ud83c\uddf8",
"flag_mt": "\ud83c\uddf2\ud83c\uddf9",
"flag_mu": "\ud83c\uddf2\ud83c\uddfa",
"flag_mv": "\ud83c\uddf2\ud83c\uddfb",
"flag_mw": "\ud83c\uddf2\ud83c\uddfc",
"flag_mx": "\ud83c\uddf2\ud83c\uddfd",
"flag_my": "\ud83c\uddf2\ud83c\uddfe",
"flag_mz": "\ud83c\uddf2\ud83c\uddff",
"flag_na": "\ud83c\uddf3\ud83c\udde6",
"flag_nc": "\ud83c\uddf3\ud83c\udde8",
"flag_ne": "\ud83c\uddf3\ud83c\uddea",
"flag_nf": "\ud83c\uddf3\ud83c\uddeb",
"flag_ng": "\ud83c\uddf3\ud83c\uddec",
"flag_ni": "\ud83c\uddf3\ud83c\uddee",
"flag_nl": "\ud83c\uddf3\ud83c\uddf1",
"flag_no": "\ud83c\uddf3\ud83c\uddf4",
"flag_np": "\ud83c\uddf3\ud83c\uddf5",
"flag_nr": "\ud83c\uddf3\ud83c\uddf7",
"flag_nu": "\ud83c\uddf3\ud83c\uddfa",
"flag_nz": "\ud83c\uddf3\ud83c\uddff",
"flag_om": "\ud83c\uddf4\ud83c\uddf2",
"flag_pa": "\ud83c\uddf5\ud83c\udde6",
"flag_pe": "\ud83c\uddf5\ud83c\uddea",
"flag_pf": "\ud83c\uddf5\ud83c\uddeb",
"flag_pg": "\ud83c\uddf5\ud83c\uddec",
"flag_ph": "\ud83c\uddf5\ud83c\udded",
"flag_pk": "\ud83c\uddf5\ud83c\uddf0",
"flag_pl": "\ud83c\uddf5\ud83c\uddf1",
"flag_pm": "\ud83c\uddf5\ud83c\uddf2",
"flag_pn": "\ud83c\uddf5\ud83c\uddf3",
"flag_pr": "\ud83c\uddf5\ud83c\uddf7",
"flag_ps": "\ud83c\uddf5\ud83c\uddf8",
"flag_pt": "\ud83c\uddf5\ud83c\uddf9",
"flag_pw": "\ud83c\uddf5\ud83c\uddfc",
"flag_py": "\ud83c\uddf5\ud83c\uddfe",
"flag_qa": "\ud83c\uddf6\ud83c\udde6",
"flag_re": "\ud83c\uddf7\ud83c\uddea",
"flag_ro": "\ud83c\uddf7\ud83c\uddf4",
"flag_rs": "\ud83c\uddf7\ud83c\uddf8",
"flag_ru": "\ud83c\uddf7\ud83c\uddfa",
"flag_rw": "\ud83c\uddf7\ud83c\uddfc",
"flag_sa": "\ud83c\uddf8\ud83c\udde6",
"flag_sb": "\ud83c\uddf8\ud83c\udde7",
"flag_sc": "\ud83c\uddf8\ud83c\udde8",
"flag_sd": "\ud83c\uddf8\ud83c\udde9",
"flag_se": "\ud83c\uddf8\ud83c\uddea",
"flag_sg": "\ud83c\uddf8\ud83c\uddec",
"flag_sh": "\ud83c\uddf8\ud83c\udded",
"flag_si": "\ud83c\uddf8\ud83c\uddee",
"flag_sj": "\ud83c\uddf8\ud83c\uddef",
"flag_sk": "\ud83c\uddf8\ud83c\uddf0",
"flag_sl": "\ud83c\uddf8\ud83c\uddf1",
"flag_sm": "\ud83c\uddf8\ud83c\uddf2",
"flag_sn": "\ud83c\uddf8\ud83c\uddf3",
"flag_so": "\ud83c\uddf8\ud83c\uddf4",
"flag_sr": "\ud83c\uddf8\ud83c\uddf7",
"flag_ss": "\ud83c\uddf8\ud83c\uddf8",
"flag_st": "\ud83c\uddf8\ud83c\uddf9",
"flag_sv": "\ud83c\uddf8\ud83c\uddfb",
"flag_sx": "\ud83c\uddf8\ud83c\uddfd",
"flag_sy": "\ud83c\uddf8\ud83c\uddfe",
"flag_sz": "\ud83c\uddf8\ud83c\uddff",
"flag_ta": "\ud83c\uddf9\ud83c\udde6",
"flag_tc": "\ud83c\uddf9\ud83c\udde8",
"flag_td": "\ud83c\uddf9\ud83c\udde9",
"flag_tf": "\ud83c\uddf9\ud83c\uddeb",
"flag_tg": "\ud83c\uddf9\ud83c\uddec",
"flag_th": "\ud83c\uddf9\ud83c\udded",
"flag_tj": "\ud83c\uddf9\ud83c\uddef",
"flag_tk": "\ud83c\uddf9\ud83c\uddf0",
"flag_tl": "\ud83c\uddf9\ud83c\uddf1",
"flag_tm": "\ud83c\uddf9\ud83c\uddf2",
"flag_tn": "\ud83c\uddf9\ud83c\uddf3",
"flag_to": "\ud83c\uddf9\ud83c\uddf4",
"flag_tr": "\ud83c\uddf9\ud83c\uddf7",
"flag_tt": "\ud83c\uddf9\ud83c\uddf9",
"flag_tv": "\ud83c\uddf9\ud83c\uddfb",
"flag_tw": "\ud83c\uddf9\ud83c\uddfc",
"flag_tz": "\ud83c\uddf9\ud83c\uddff",
"flag_ua": "\ud83c\uddfa\ud83c\udde6",
"flag_ug": "\ud83c\uddfa\ud83c\uddec",
"flag_um": "\ud83c\uddfa\ud83c\uddf2",
"flag_us": "\ud83c\uddfa\ud83c\uddf8",
"flag_uy": "\ud83c\uddfa\ud83c\uddfe",
"flag_uz": "\ud83c\uddfa\ud83c\uddff",
"flag_va": "\ud83c\uddfb\ud83c\udde6",
"flag_vc": "\ud83c\uddfb\ud83c\udde8",
"flag_ve": "\ud83c\uddfb\ud83c\uddea",
"flag_vg": "\ud83c\uddfb\ud83c\uddec",
"flag_vi": "\ud83c\uddfb\ud83c\uddee",
"flag_vn": "\ud83c\uddfb\ud83c\uddf3",
"flag_vu": "\ud83c\uddfb\ud83c\uddfa",
"flag_wf": "\ud83c\uddfc\ud83c\uddeb",
"flag_white": "\ud83c\udff3",
"flag_ws": "\ud83c\uddfc\ud83c\uddf8",
"flag_xk": "\ud83c\uddfd\ud83c\uddf0",
"flag_ye": "\ud83c\uddfe\ud83c\uddea",
"flag_yt": "\ud83c\uddfe\ud83c\uddf9",
"flag_za": "\ud83c\uddff\ud83c\udde6",
"flag_zm": "\ud83c\uddff\ud83c\uddf2",
"flag_zw": "\ud83c\uddff\ud83c\uddfc",
"flags": "\ud83c\udf8f",
"flashlight": "\ud83d\udd26",
"fleur-de-lis": "\u269c",
"floppy_disk": "\ud83d\udcbe",
"flower_playing_cards": "\ud83c\udfb4",
"flushed": "\ud83d\ude33",
"fog": "\ud83c\udf2b",
"foggy": "\ud83c\udf01",
"football": "\ud83c\udfc8",
"footprints": "\ud83d\udc63",
"fork_and_knife": "\ud83c\udf74",
"fork_knife_plate": "\ud83c\udf7d",
"fountain": "\u26f2",
"four": "4\u20e3",
"four_leaf_clover": "\ud83c\udf40",
"frame_photo": "\ud83d\uddbc",
"free": "\ud83c\udd93",
"fried_shrimp": "\ud83c\udf64",
"fries": "\ud83c\udf5f",
"frog": "\ud83d\udc38",
"frowning": "\ud83d\ude26",
"frowning2": "\u2639",
"fuelpump": "\u26fd",
"full_moon": "\ud83c\udf15",
"full_moon_with_face": "\ud83c\udf1d",
"game_die": "\ud83c\udfb2",
"gear": "\u2699",
"gem": "\ud83d\udc8e",
"gemini": "\u264a",
"ghost": "\ud83d\udc7b",
"gift": "\ud83c\udf81",
"gift_heart": "\ud83d\udc9d",
"girl": "\ud83d\udc67",
"girl_tone1": "\ud83d\udc67\ud83c\udffb",
"girl_tone2": "\ud83d\udc67\ud83c\udffc",
"girl_tone3": "\ud83d\udc67\ud83c\udffd",
"girl_tone4": "\ud83d\udc67\ud83c\udffe",
"girl_tone5": "\ud83d\udc67\ud83c\udfff",
"globe_with_meridians": "\ud83c\udf10",
"goat": "\ud83d\udc10",
"golf": "\u26f3",
"golfer": "\ud83c\udfcc",
"grapes": "\ud83c\udf47",
"green_apple": "\ud83c\udf4f",
"green_book": "\ud83d\udcd7",
"green_heart": "\ud83d\udc9a",
"grey_exclamation": "\u2755",
"grey_question": "\u2754",
"grimacing": "\ud83d\ude2c",
"grin": "\ud83d\ude01",
"grinning": "\ud83d\ude00",
"guardsman": "\ud83d\udc82",
"guardsman_tone1": "\ud83d\udc82\ud83c\udffb",
"guardsman_tone2": "\ud83d\udc82\ud83c\udffc",
"guardsman_tone3": "\ud83d\udc82\ud83c\udffd",
"guardsman_tone4": "\ud83d\udc82\ud83c\udffe",
"guardsman_tone5": "\ud83d\udc82\ud83c\udfff",
"guitar": "\ud83c\udfb8",
"gun": "\ud83d\udd2b",
"haircut": "\ud83d\udc87",
"haircut_tone1": "\ud83d\udc87\ud83c\udffb",
"haircut_tone2": "\ud83d\udc87\ud83c\udffc",
"haircut_tone3": "\ud83d\udc87\ud83c\udffd",
"haircut_tone4": "\ud83d\udc87\ud83c\udffe",
"haircut_tone5": "\ud83d\udc87\ud83c\udfff",
"hamburger": "\ud83c\udf54",
"hammer": "\ud83d\udd28",
"hammer_pick": "\u2692",
"hamster": "\ud83d\udc39",
"hand_splayed": "\ud83d\udd90",
"hand_splayed_tone1": "\ud83d\udd90\ud83c\udffb",
"hand_splayed_tone2": "\ud83d\udd90\ud83c\udffc",
"hand_splayed_tone3": "\ud83d\udd90\ud83c\udffd",
"hand_splayed_tone4": "\ud83d\udd90\ud83c\udffe",
"hand_splayed_tone5": "\ud83d\udd90\ud83c\udfff",
"handbag": "\ud83d\udc5c",
"hash": "#\u20e3",
"hatched_chick": "\ud83d\udc25",
"hatching_chick": "\ud83d\udc23",
"head_bandage": "\ud83e\udd15",
"headphones": "\ud83c\udfa7",
"hear_no_evil": "\ud83d\ude49",
"heart": "\u2764",
"heart_decoration": "\ud83d\udc9f",
"heart_exclamation": "\u2763",
"heart_eyes": "\ud83d\ude0d",
"heart_eyes_cat": "\ud83d\ude3b",
"heartbeat": "\ud83d\udc93",
"heartpulse": "\ud83d\udc97",
"hearts": "\u2665",
"heavy_check_mark": "\u2714",
"heavy_division_sign": "\u2797",
"heavy_dollar_sign": "\ud83d\udcb2",
"heavy_minus_sign": "\u2796",
"heavy_multiplication_x": "\u2716",
"heavy_plus_sign": "\u2795",
"helicopter": "\ud83d\ude81",
"helmet_with_cross": "\u26d1",
"herb": "\ud83c\udf3f",
"hibiscus": "\ud83c\udf3a",
"high_brightness": "\ud83d\udd06",
"high_heel": "\ud83d\udc60",
"hockey": "\ud83c\udfd2",
"hole": "\ud83d\udd73",
"homes": "\ud83c\udfd8",
"honey_pot": "\ud83c\udf6f",
"horse": "\ud83d\udc34",
"horse_racing": "\ud83c\udfc7",
"horse_racing_tone1": "\ud83c\udfc7\ud83c\udffb",
"horse_racing_tone2": "\ud83c\udfc7\ud83c\udffc",
"horse_racing_tone3": "\ud83c\udfc7\ud83c\udffd",
"horse_racing_tone4": "\ud83c\udfc7\ud83c\udffe",
"horse_racing_tone5": "\ud83c\udfc7\ud83c\udfff",
"hospital": "\ud83c\udfe5",
"hot_pepper": "\ud83c\udf36",
"hotdog": "\ud83c\udf2d",
"hotel": "\ud83c\udfe8",
"hotsprings": "\u2668",
"hourglass": "\u231b",
"hourglass_flowing_sand": "\u23f3",
"house": "\ud83c\udfe0",
"house_abandoned": "\ud83c\udfda",
"house_with_garden": "\ud83c\udfe1",
"hugging": "\ud83e\udd17",
"hushed": "\ud83d\ude2f",
"ice_cream": "\ud83c\udf68",
"ice_skate": "\u26f8",
"icecream": "\ud83c\udf66",
"id": "\ud83c\udd94",
"ideograph_advantage": "\ud83c\ude50",
"imp": "\ud83d\udc7f",
"inbox_tray": "\ud83d\udce5",
"incoming_envelope": "\ud83d\udce8",
"information_desk_person": "\ud83d\udc81",
"information_desk_person_tone1": "\ud83d\udc81\ud83c\udffb",
"information_desk_person_tone2": "\ud83d\udc81\ud83c\udffc",
"information_desk_person_tone3": "\ud83d\udc81\ud83c\udffd",
"information_desk_person_tone4": "\ud83d\udc81\ud83c\udffe",
"information_desk_person_tone5": "\ud83d\udc81\ud83c\udfff",
"information_source": "\u2139",
"innocent": "\ud83d\ude07",
"interrobang": "\u2049",
"iphone": "\ud83d\udcf1",
"island": "\ud83c\udfdd",
"izakaya_lantern": "\ud83c\udfee",
"jack_o_lantern": "\ud83c\udf83",
"japan": "\ud83d\uddfe",
"japanese_castle": "\ud83c\udfef",
"japanese_goblin": "\ud83d\udc7a",
"japanese_ogre": "\ud83d\udc79",
"jeans": "\ud83d\udc56",
"joy": "\ud83d\ude02",
"joy_cat": "\ud83d\ude39",
"joystick": "\ud83d\udd79",
"kaaba": "\ud83d\udd4b",
"key": "\ud83d\udd11",
"key2": "\ud83d\udddd",
"keyboard": "\u2328",
"kimono": "\ud83d\udc58",
"kiss": "\ud83d\udc8b",
"kiss_mm": "\ud83d\udc68\u2764\ud83d\udc8b\ud83d\udc68",
"kiss_ww": "\ud83d\udc69\u2764\ud83d\udc8b\ud83d\udc69",
"kissing": "\ud83d\ude17",
"kissing_cat": "\ud83d\ude3d",
"kissing_closed_eyes": "\ud83d\ude1a",
"kissing_heart": "\ud83d\ude18",
"kissing_smiling_eyes": "\ud83d\ude19",
"knife": "\ud83d\udd2a",
"koala": "\ud83d\udc28",
"koko": "\ud83c\ude01",
"label": "\ud83c\udff7",
"large_blue_circle": "\ud83d\udd35",
"large_blue_diamond": "\ud83d\udd37",
"large_orange_diamond": "\ud83d\udd36",
"last_quarter_moon": "\ud83c\udf17",
"last_quarter_moon_with_face": "\ud83c\udf1c",
"laughing": "\ud83d\ude06",
"leaves": "\ud83c\udf43",
"ledger": "\ud83d\udcd2",
"left_luggage": "\ud83d\udec5",
"left_right_arrow": "\u2194",
"leftwards_arrow_with_hook": "\u21a9",
"lemon": "\ud83c\udf4b",
"leo": "\u264c",
"leopard": "\ud83d\udc06",
"level_slider": "\ud83c\udf9a",
"levitate": "\ud83d\udd74",
"libra": "\u264e",
"lifter": "\ud83c\udfcb",
"lifter_tone1": "\ud83c\udfcb\ud83c\udffb",
"lifter_tone2": "\ud83c\udfcb\ud83c\udffc",
"lifter_tone3": "\ud83c\udfcb\ud83c\udffd",
"lifter_tone4": "\ud83c\udfcb\ud83c\udffe",
"lifter_tone5": "\ud83c\udfcb\ud83c\udfff",
"light_rail": "\ud83d\ude88",
"link": "\ud83d\udd17",
"lion_face": "\ud83e\udd81",
"lips": "\ud83d\udc44",
"lipstick": "\ud83d\udc84",
"lock": "\ud83d\udd12",
"lock_with_ink_pen": "\ud83d\udd0f",
"lollipop": "\ud83c\udf6d",
"loop": "\u27bf",
"loud_sound": "\ud83d\udd0a",
"loudspeaker": "\ud83d\udce2",
"love_hotel": "\ud83c\udfe9",
"love_letter": "\ud83d\udc8c",
"low_brightness": "\ud83d\udd05",
"m": "\u24c2",
"mag": "\ud83d\udd0d",
"mag_right": "\ud83d\udd0e",
"mahjong": "\ud83c\udc04",
"mailbox": "\ud83d\udceb",
"mailbox_closed": "\ud83d\udcea",
"mailbox_with_mail": "\ud83d\udcec",
"mailbox_with_no_mail": "\ud83d\udced",
"man": "\ud83d\udc68",
"man_tone1": "\ud83d\udc68\ud83c\udffb",
"man_tone2": "\ud83d\udc68\ud83c\udffc",
"man_tone3": "\ud83d\udc68\ud83c\udffd",
"man_tone4": "\ud83d\udc68\ud83c\udffe",
"man_tone5": "\ud83d\udc68\ud83c\udfff",
"man_with_gua_pi_mao": "\ud83d\udc72",
"man_with_gua_pi_mao_tone1": "\ud83d\udc72\ud83c\udffb",
"man_with_gua_pi_mao_tone2": "\ud83d\udc72\ud83c\udffc",
"man_with_gua_pi_mao_tone3": "\ud83d\udc72\ud83c\udffd",
"man_with_gua_pi_mao_tone4": "\ud83d\udc72\ud83c\udffe",
"man_with_gua_pi_mao_tone5": "\ud83d\udc72\ud83c\udfff",
"man_with_turban": "\ud83d\udc73",
"man_with_turban_tone1": "\ud83d\udc73\ud83c\udffb",
"man_with_turban_tone2": "\ud83d\udc73\ud83c\udffc",
"man_with_turban_tone3": "\ud83d\udc73\ud83c\udffd",
"man_with_turban_tone4": "\ud83d\udc73\ud83c\udffe",
"man_with_turban_tone5": "\ud83d\udc73\ud83c\udfff",
"mans_shoe": "\ud83d\udc5e",
"map": "\ud83d\uddfa",
"maple_leaf": "\ud83c\udf41",
"mask": "\ud83d\ude37",
"massage": "\ud83d\udc86",
"massage_tone1": "\ud83d\udc86\ud83c\udffb",
"massage_tone2": "\ud83d\udc86\ud83c\udffc",
"massage_tone3": "\ud83d\udc86\ud83c\udffd",
"massage_tone4": "\ud83d\udc86\ud83c\udffe",
"massage_tone5": "\ud83d\udc86\ud83c\udfff",
"meat_on_bone": "\ud83c\udf56",
"medal": "\ud83c\udfc5",
"mega": "\ud83d\udce3",
"melon": "\ud83c\udf48",
"menorah": "\ud83d\udd4e",
"mens": "\ud83d\udeb9",
"metal": "\ud83e\udd18",
"metal_tone1": "\ud83e\udd18\ud83c\udffb",
"metal_tone2": "\ud83e\udd18\ud83c\udffc",
"metal_tone3": "\ud83e\udd18\ud83c\udffd",
"metal_tone4": "\ud83e\udd18\ud83c\udffe",
"metal_tone5": "\ud83e\udd18\ud83c\udfff",
"metro": "\ud83d\ude87",
"microphone": "\ud83c\udfa4",
"microphone2": "\ud83c\udf99",
"microscope": "\ud83d\udd2c",
"middle_finger": "\ud83d\udd95",
"middle_finger_tone1": "\ud83d\udd95\ud83c\udffb",
"middle_finger_tone2": "\ud83d\udd95\ud83c\udffc",
"middle_finger_tone3": "\ud83d\udd95\ud83c\udffd",
"middle_finger_tone4": "\ud83d\udd95\ud83c\udffe",
"middle_finger_tone5": "\ud83d\udd95\ud83c\udfff",
"military_medal": "\ud83c\udf96",
"milky_way": "\ud83c\udf0c",
"minibus": "\ud83d\ude90",
"minidisc": "\ud83d\udcbd",
"mobile_phone_off": "\ud83d\udcf4",
"money_mouth": "\ud83e\udd11",
"money_with_wings": "\ud83d\udcb8",
"moneybag": "\ud83d\udcb0",
"monkey": "\ud83d\udc12",
"monkey_face": "\ud83d\udc35",
"monorail": "\ud83d\ude9d",
"mortar_board": "\ud83c\udf93",
"mosque": "\ud83d\udd4c",
"motorboat": "\ud83d\udee5",
"motorcycle": "\ud83c\udfcd",
"motorway": "\ud83d\udee3",
"mount_fuji": "\ud83d\uddfb",
"mountain": "\u26f0",
"mountain_bicyclist": "\ud83d\udeb5",
"mountain_bicyclist_tone1": "\ud83d\udeb5\ud83c\udffb",
"mountain_bicyclist_tone2": "\ud83d\udeb5\ud83c\udffc",
"mountain_bicyclist_tone3": "\ud83d\udeb5\ud83c\udffd",
"mountain_bicyclist_tone4": "\ud83d\udeb5\ud83c\udffe",
"mountain_bicyclist_tone5": "\ud83d\udeb5\ud83c\udfff",
"mountain_cableway": "\ud83d\udea0",
"mountain_railway": "\ud83d\ude9e",
"mountain_snow": "\ud83c\udfd4",
"mouse": "\ud83d\udc2d",
"mouse2": "\ud83d\udc01",
"mouse_three_button": "\ud83d\uddb1",
"movie_camera": "\ud83c\udfa5",
"moyai": "\ud83d\uddff",
"muscle": "\ud83d\udcaa",
"muscle_tone1": "\ud83d\udcaa\ud83c\udffb",
"muscle_tone2": "\ud83d\udcaa\ud83c\udffc",
"muscle_tone3": "\ud83d\udcaa\ud83c\udffd",
"muscle_tone4": "\ud83d\udcaa\ud83c\udffe",
"muscle_tone5": "\ud83d\udcaa\ud83c\udfff",
"mushroom": "\ud83c\udf44",
"musical_keyboard": "\ud83c\udfb9",
"musical_note": "\ud83c\udfb5",
"musical_score": "\ud83c\udfbc",
"mute": "\ud83d\udd07",
"nail_care": "\ud83d\udc85",
"nail_care_tone1": "\ud83d\udc85\ud83c\udffb",
"nail_care_tone2": "\ud83d\udc85\ud83c\udffc",
"nail_care_tone3": "\ud83d\udc85\ud83c\udffd",
"nail_care_tone4": "\ud83d\udc85\ud83c\udffe",
"nail_care_tone5": "\ud83d\udc85\ud83c\udfff",
"name_badge": "\ud83d\udcdb",
"necktie": "\ud83d\udc54",
"negative_squared_cross_mark": "\u274e",
"nerd": "\ud83e\udd13",
"neutral_face": "\ud83d\ude10",
"new": "\ud83c\udd95",
"new_moon": "\ud83c\udf11",
"new_moon_with_face": "\ud83c\udf1a",
"newspaper": "\ud83d\udcf0",
"newspaper2": "\ud83d\uddde",
"ng": "\ud83c\udd96",
"night_with_stars": "\ud83c\udf03",
"nine": "9\u20e3",
"no_bell": "\ud83d\udd15",
"no_bicycles": "\ud83d\udeb3",
"no_entry": "\u26d4",
"no_entry_sign": "\ud83d\udeab",
"no_good": "\ud83d\ude45",
"no_good_tone1": "\ud83d\ude45\ud83c\udffb",
"no_good_tone2": "\ud83d\ude45\ud83c\udffc",
"no_good_tone3": "\ud83d\ude45\ud83c\udffd",
"no_good_tone4": "\ud83d\ude45\ud83c\udffe",
"no_good_tone5": "\ud83d\ude45\ud83c\udfff",
"no_mobile_phones": "\ud83d\udcf5",
"no_mouth": "\ud83d\ude36",
"no_pedestrians": "\ud83d\udeb7",
"no_smoking": "\ud83d\udead",
"non-potable_water": "\ud83d\udeb1",
"nose": "\ud83d\udc43",
"nose_tone1": "\ud83d\udc43\ud83c\udffb",
"nose_tone2": "\ud83d\udc43\ud83c\udffc",
"nose_tone3": "\ud83d\udc43\ud83c\udffd",
"nose_tone4": "\ud83d\udc43\ud83c\udffe",
"nose_tone5": "\ud83d\udc43\ud83c\udfff",
"notebook": "\ud83d\udcd3",
"notebook_with_decorative_cover": "\ud83d\udcd4",
"notepad_spiral": "\ud83d\uddd2",
"notes": "\ud83c\udfb6",
"nut_and_bolt": "\ud83d\udd29",
"o": "\u2b55",
"o2": "\ud83c\udd7e",
"ocean": "\ud83c\udf0a",
"octopus": "\ud83d\udc19",
"oden": "\ud83c\udf62",
"office": "\ud83c\udfe2",
"oil": "\ud83d\udee2",
"ok": "\ud83c\udd97",
"ok_hand": "\ud83d\udc4c",
"ok_hand_tone1": "\ud83d\udc4c\ud83c\udffb",
"ok_hand_tone2": "\ud83d\udc4c\ud83c\udffc",
"ok_hand_tone3": "\ud83d\udc4c\ud83c\udffd",
"ok_hand_tone4": "\ud83d\udc4c\ud83c\udffe",
"ok_hand_tone5": "\ud83d\udc4c\ud83c\udfff",
"ok_woman": "\ud83d\ude46",
"ok_woman_tone1": "\ud83d\ude46\ud83c\udffb",
"ok_woman_tone2": "\ud83d\ude46\ud83c\udffc",
"ok_woman_tone3": "\ud83d\ude46\ud83c\udffd",
"ok_woman_tone4": "\ud83d\ude46\ud83c\udffe",
"ok_woman_tone5": "\ud83d\ude46\ud83c\udfff",
"older_man": "\ud83d\udc74",
"older_man_tone1": "\ud83d\udc74\ud83c\udffb",
"older_man_tone2": "\ud83d\udc74\ud83c\udffc",
"older_man_tone3": "\ud83d\udc74\ud83c\udffd",
"older_man_tone4": "\ud83d\udc74\ud83c\udffe",
"older_man_tone5": "\ud83d\udc74\ud83c\udfff",
"older_woman": "\ud83d\udc75",
"older_woman_tone1": "\ud83d\udc75\ud83c\udffb",
"older_woman_tone2": "\ud83d\udc75\ud83c\udffc",
"older_woman_tone3": "\ud83d\udc75\ud83c\udffd",
"older_woman_tone4": "\ud83d\udc75\ud83c\udffe",
"older_woman_tone5": "\ud83d\udc75\ud83c\udfff",
"om_symbol": "\ud83d\udd49",
"on": "\ud83d\udd1b",
"oncoming_automobile": "\ud83d\ude98",
"oncoming_bus": "\ud83d\ude8d",
"oncoming_police_car": "\ud83d\ude94",
"oncoming_taxi": "\ud83d\ude96",
"one": "1\u20e3",
"open_file_folder": "\ud83d\udcc2",
"open_hands": "\ud83d\udc50",
"open_hands_tone1": "\ud83d\udc50\ud83c\udffb",
"open_hands_tone2": "\ud83d\udc50\ud83c\udffc",
"open_hands_tone3": "\ud83d\udc50\ud83c\udffd",
"open_hands_tone4": "\ud83d\udc50\ud83c\udffe",
"open_hands_tone5": "\ud83d\udc50\ud83c\udfff",
"open_mouth": "\ud83d\ude2e",
"ophiuchus": "\u26ce",
"orange_book": "\ud83d\udcd9",
"orthodox_cross": "\u2626",
"outbox_tray": "\ud83d\udce4",
"ox": "\ud83d\udc02",
"package": "\ud83d\udce6",
"page_facing_up": "\ud83d\udcc4",
"page_with_curl": "\ud83d\udcc3",
"pager": "\ud83d\udcdf",
"paintbrush": "\ud83d\udd8c",
"palm_tree": "\ud83c\udf34",
"panda_face": "\ud83d\udc3c",
"paperclip": "\ud83d\udcce",
"paperclips": "\ud83d\udd87",
"park": "\ud83c\udfde",
"parking": "\ud83c\udd7f",
"part_alternation_mark": "\u303d",
"partly_sunny": "\u26c5",
"passport_control": "\ud83d\udec2",
"pause_button": "\u23f8",
"peace": "\u262e",
"peach": "\ud83c\udf51",
"pear": "\ud83c\udf50",
"pen_ballpoint": "\ud83d\udd8a",
"pen_fountain": "\ud83d\udd8b",
"pencil": "\ud83d\udcdd",
"pencil2": "\u270f",
"penguin": "\ud83d\udc27",
"pensive": "\ud83d\ude14",
"performing_arts": "\ud83c\udfad",
"persevere": "\ud83d\ude23",
"person_frowning": "\ud83d\ude4d",
"person_frowning_tone1": "\ud83d\ude4d\ud83c\udffb",
"person_frowning_tone2": "\ud83d\ude4d\ud83c\udffc",
"person_frowning_tone3": "\ud83d\ude4d\ud83c\udffd",
"person_frowning_tone4": "\ud83d\ude4d\ud83c\udffe",
"person_frowning_tone5": "\ud83d\ude4d\ud83c\udfff",
"person_with_blond_hair": "\ud83d\udc71",
"person_with_blond_hair_tone1": "\ud83d\udc71\ud83c\udffb",
"person_with_blond_hair_tone2": "\ud83d\udc71\ud83c\udffc",
"person_with_blond_hair_tone3": "\ud83d\udc71\ud83c\udffd",
"person_with_blond_hair_tone4": "\ud83d\udc71\ud83c\udffe",
"person_with_blond_hair_tone5": "\ud83d\udc71\ud83c\udfff",
"person_with_pouting_face": "\ud83d\ude4e",
"person_with_pouting_face_tone1": "\ud83d\ude4e\ud83c\udffb",
"person_with_pouting_face_tone2": "\ud83d\ude4e\ud83c\udffc",
"person_with_pouting_face_tone3": "\ud83d\ude4e\ud83c\udffd",
"person_with_pouting_face_tone4": "\ud83d\ude4e\ud83c\udffe",
"person_with_pouting_face_tone5": "\ud83d\ude4e\ud83c\udfff",
"pick": "\u26cf",
"pig": "\ud83d\udc37",
"pig2": "\ud83d\udc16",
"pig_nose": "\ud83d\udc3d",
"pill": "\ud83d\udc8a",
"pineapple": "\ud83c\udf4d",
"ping_pong": "\ud83c\udfd3",
"pisces": "\u2653",
"pizza": "\ud83c\udf55",
"place_of_worship": "\ud83d\uded0",
"play_pause": "\u23ef",
"point_down": "\ud83d\udc47",
"point_down_tone1": "\ud83d\udc47\ud83c\udffb",
"point_down_tone2": "\ud83d\udc47\ud83c\udffc",
"point_down_tone3": "\ud83d\udc47\ud83c\udffd",
"point_down_tone4": "\ud83d\udc47\ud83c\udffe",
"point_down_tone5": "\ud83d\udc47\ud83c\udfff",
"point_left": "\ud83d\udc48",
"point_left_tone1": "\ud83d\udc48\ud83c\udffb",
"point_left_tone2": "\ud83d\udc48\ud83c\udffc",
"point_left_tone3": "\ud83d\udc48\ud83c\udffd",
"point_left_tone4": "\ud83d\udc48\ud83c\udffe",
"point_left_tone5": "\ud83d\udc48\ud83c\udfff",
"point_right": "\ud83d\udc49",
"point_right_tone1": "\ud83d\udc49\ud83c\udffb",
"point_right_tone2": "\ud83d\udc49\ud83c\udffc",
"point_right_tone3": "\ud83d\udc49\ud83c\udffd",
"point_right_tone4": "\ud83d\udc49\ud83c\udffe",
"point_right_tone5": "\ud83d\udc49\ud83c\udfff",
"point_up": "\u261d",
"point_up_2": "\ud83d\udc46",
"point_up_2_tone1": "\ud83d\udc46\ud83c\udffb",
"point_up_2_tone2": "\ud83d\udc46\ud83c\udffc",
"point_up_2_tone3": "\ud83d\udc46\ud83c\udffd",
"point_up_2_tone4": "\ud83d\udc46\ud83c\udffe",
"point_up_2_tone5": "\ud83d\udc46\ud83c\udfff",
"point_up_tone1": "\u261d\ud83c\udffb",
"point_up_tone2": "\u261d\ud83c\udffc",
"point_up_tone3": "\u261d\ud83c\udffd",
"point_up_tone4": "\u261d\ud83c\udffe",
"point_up_tone5": "\u261d\ud83c\udfff",
"police_car": "\ud83d\ude93",
"poodle": "\ud83d\udc29",
"poop": "\ud83d\udca9",
"popcorn": "\ud83c\udf7f",
"post_office": "\ud83c\udfe3",
"postal_horn": "\ud83d\udcef",
"postbox": "\ud83d\udcee",
"potable_water": "\ud83d\udeb0",
"pouch": "\ud83d\udc5d",
"poultry_leg": "\ud83c\udf57",
"pound": "\ud83d\udcb7",
"pouting_cat": "\ud83d\ude3e",
"pray": "\ud83d\ude4f",
"pray_tone1": "\ud83d\ude4f\ud83c\udffb",
"pray_tone2": "\ud83d\ude4f\ud83c\udffc",
"pray_tone3": "\ud83d\ude4f\ud83c\udffd",
"pray_tone4": "\ud83d\ude4f\ud83c\udffe",
"pray_tone5": "\ud83d\ude4f\ud83c\udfff",
"prayer_beads": "\ud83d\udcff",
"princess": "\ud83d\udc78",
"princess_tone1": "\ud83d\udc78\ud83c\udffb",
"princess_tone2": "\ud83d\udc78\ud83c\udffc",
"princess_tone3": "\ud83d\udc78\ud83c\udffd",
"princess_tone4": "\ud83d\udc78\ud83c\udffe",
"princess_tone5": "\ud83d\udc78\ud83c\udfff",
"printer": "\ud83d\udda8",
"projector": "\ud83d\udcfd",
"punch": "\ud83d\udc4a",
"punch_tone1": "\ud83d\udc4a\ud83c\udffb",
"punch_tone2": "\ud83d\udc4a\ud83c\udffc",
"punch_tone3": "\ud83d\udc4a\ud83c\udffd",
"punch_tone4": "\ud83d\udc4a\ud83c\udffe",
"punch_tone5": "\ud83d\udc4a\ud83c\udfff",
"purple_heart": "\ud83d\udc9c",
"purse": "\ud83d\udc5b",
"pushpin": "\ud83d\udccc",
"put_litter_in_its_place": "\ud83d\udeae",
"question": "\u2753",
"rabbit": "\ud83d\udc30",
"rabbit2": "\ud83d\udc07",
"race_car": "\ud83c\udfce",
"racehorse": "\ud83d\udc0e",
"radio": "\ud83d\udcfb",
"radio_button": "\ud83d\udd18",
"radioactive": "\u2622",
"rage": "\ud83d\ude21",
"railway_car": "\ud83d\ude83",
"railway_track": "\ud83d\udee4",
"rainbow": "\ud83c\udf08",
"raised_hand": "\u270b",
"raised_hand_tone1": "\u270b\ud83c\udffb",
"raised_hand_tone2": "\u270b\ud83c\udffc",
"raised_hand_tone3": "\u270b\ud83c\udffd",
"raised_hand_tone4": "\u270b\ud83c\udffe",
"raised_hand_tone5": "\u270b\ud83c\udfff",
"raised_hands": "\ud83d\ude4c",
"raised_hands_tone1": "\ud83d\ude4c\ud83c\udffb",
"raised_hands_tone2": "\ud83d\ude4c\ud83c\udffc",
"raised_hands_tone3": "\ud83d\ude4c\ud83c\udffd",
"raised_hands_tone4": "\ud83d\ude4c\ud83c\udffe",
"raised_hands_tone5": "\ud83d\ude4c\ud83c\udfff",
"raising_hand": "\ud83d\ude4b",
"raising_hand_tone1": "\ud83d\ude4b\ud83c\udffb",
"raising_hand_tone2": "\ud83d\ude4b\ud83c\udffc",
"raising_hand_tone3": "\ud83d\ude4b\ud83c\udffd",
"raising_hand_tone4": "\ud83d\ude4b\ud83c\udffe",
"raising_hand_tone5": "\ud83d\ude4b\ud83c\udfff",
"ram": "\ud83d\udc0f",
"ramen": "\ud83c\udf5c",
"rat": "\ud83d\udc00",
"record_button": "\u23fa",
"recycle": "\u267b",
"red_car": "\ud83d\ude97",
"red_circle": "\ud83d\udd34",
"registered": "\u00ae",
"relaxed": "\u263a",
"relieved": "\ud83d\ude0c",
"reminder_ribbon": "\ud83c\udf97",
"repeat": "\ud83d\udd01",
"repeat_one": "\ud83d\udd02",
"restroom": "\ud83d\udebb",
"revolving_hearts": "\ud83d\udc9e",
"rewind": "\u23ea",
"ribbon": "\ud83c\udf80",
"rice": "\ud83c\udf5a",
"rice_ball": "\ud83c\udf59",
"rice_cracker": "\ud83c\udf58",
"rice_scene": "\ud83c\udf91",
"ring": "\ud83d\udc8d",
"robot": "\ud83e\udd16",
"rocket": "\ud83d\ude80",
"roller_coaster": "\ud83c\udfa2",
"rolling_eyes": "\ud83d\ude44",
"rooster": "\ud83d\udc13",
"rose": "\ud83c\udf39",
"rosette": "\ud83c\udff5",
"rotating_light": "\ud83d\udea8",
"round_pushpin": "\ud83d\udccd",
"rowboat": "\ud83d\udea3",
"rowboat_tone1": "\ud83d\udea3\ud83c\udffb",
"rowboat_tone2": "\ud83d\udea3\ud83c\udffc",
"rowboat_tone3": "\ud83d\udea3\ud83c\udffd",
"rowboat_tone4": "\ud83d\udea3\ud83c\udffe",
"rowboat_tone5": "\ud83d\udea3\ud83c\udfff",
"rugby_football": "\ud83c\udfc9",
"runner": "\ud83c\udfc3",
"runner_tone1": "\ud83c\udfc3\ud83c\udffb",
"runner_tone2": "\ud83c\udfc3\ud83c\udffc",
"runner_tone3": "\ud83c\udfc3\ud83c\udffd",
"runner_tone4": "\ud83c\udfc3\ud83c\udffe",
"runner_tone5": "\ud83c\udfc3\ud83c\udfff",
"running_shirt_with_sash": "\ud83c\udfbd",
"sa": "\ud83c\ude02",
"sagittarius": "\u2650",
"sailboat": "\u26f5",
"sake": "\ud83c\udf76",
"sandal": "\ud83d\udc61",
"santa": "\ud83c\udf85",
"santa_tone1": "\ud83c\udf85\ud83c\udffb",
"santa_tone2": "\ud83c\udf85\ud83c\udffc",
"santa_tone3": "\ud83c\udf85\ud83c\udffd",
"santa_tone4": "\ud83c\udf85\ud83c\udffe",
"santa_tone5": "\ud83c\udf85\ud83c\udfff",
"satellite": "\ud83d\udce1",
"satellite_orbital": "\ud83d\udef0",
"saxophone": "\ud83c\udfb7",
"scales": "\u2696",
"school": "\ud83c\udfeb",
"school_satchel": "\ud83c\udf92",
"scissors": "\u2702",
"scorpion": "\ud83e\udd82",
"scorpius": "\u264f",
"scream": "\ud83d\ude31",
"scream_cat": "\ud83d\ude40",
"scroll": "\ud83d\udcdc",
"seat": "\ud83d\udcba",
"secret": "\u3299",
"see_no_evil": "\ud83d\ude48",
"seedling": "\ud83c\udf31",
"seven": "7\u20e3",
"shamrock": "\u2618",
"shaved_ice": "\ud83c\udf67",
"sheep": "\ud83d\udc11",
"shell": "\ud83d\udc1a",
"shield": "\ud83d\udee1",
"shinto_shrine": "\u26e9",
"ship": "\ud83d\udea2",
"shirt": "\ud83d\udc55",
"shopping_bags": "\ud83d\udecd",
"shower": "\ud83d\udebf",
"signal_strength": "\ud83d\udcf6",
"six": "6\u20e3",
"six_pointed_star": "\ud83d\udd2f",
"ski": "\ud83c\udfbf",
"skier": "\u26f7",
"skull": "\ud83d\udc80",
"skull_crossbones": "\u2620",
"sleeping": "\ud83d\ude34",
"sleeping_accommodation": "\ud83d\udecc",
"sleepy": "\ud83d\ude2a",
"slight_frown": "\ud83d\ude41",
"slight_smile": "\ud83d\ude42",
"slot_machine": "\ud83c\udfb0",
"small_blue_diamond": "\ud83d\udd39",
"small_orange_diamond": "\ud83d\udd38",
"small_red_triangle": "\ud83d\udd3a",
"small_red_triangle_down": "\ud83d\udd3b",
"smile": "\ud83d\ude04",
"smile_cat": "\ud83d\ude38",
"smiley": "\ud83d\ude03",
"smiley_cat": "\ud83d\ude3a",
"smiling_imp": "\ud83d\ude08",
"smirk": "\ud83d\ude0f",
"smirk_cat": "\ud83d\ude3c",
"smoking": "\ud83d\udeac",
"snail": "\ud83d\udc0c",
"snake": "\ud83d\udc0d",
"snowboarder": "\ud83c\udfc2",
"snowflake": "\u2744",
"snowman": "\u26c4",
"snowman2": "\u2603",
"sob": "\ud83d\ude2d",
"soccer": "\u26bd",
"soon": "\ud83d\udd1c",
"sos": "\ud83c\udd98",
"sound": "\ud83d\udd09",
"space_invader": "\ud83d\udc7e",
"spades": "\u2660",
"spaghetti": "\ud83c\udf5d",
"sparkle": "\u2747",
"sparkler": "\ud83c\udf87",
"sparkles": "\u2728",
"sparkling_heart": "\ud83d\udc96",
"speak_no_evil": "\ud83d\ude4a",
"speaker": "\ud83d\udd08",
"speaking_head": "\ud83d\udde3",
"speech_balloon": "\ud83d\udcac",
"speedboat": "\ud83d\udea4",
"spider": "\ud83d\udd77",
"spider_web": "\ud83d\udd78",
"spy": "\ud83d\udd75",
"spy_tone1": "\ud83d\udd75\ud83c\udffb",
"spy_tone2": "\ud83d\udd75\ud83c\udffc",
"spy_tone3": "\ud83d\udd75\ud83c\udffd",
"spy_tone4": "\ud83d\udd75\ud83c\udffe",
"spy_tone5": "\ud83d\udd75\ud83c\udfff",
"stadium": "\ud83c\udfdf",
"star": "\u2b50",
"star2": "\ud83c\udf1f",
"star_and_crescent": "\u262a",
"star_of_david": "\u2721",
"stars": "\ud83c\udf20",
"station": "\ud83d\ude89",
"statue_of_liberty": "\ud83d\uddfd",
"steam_locomotive": "\ud83d\ude82",
"stew": "\ud83c\udf72",
"stop_button": "\u23f9",
"stopwatch": "\u23f1",
"straight_ruler": "\ud83d\udccf",
"strawberry": "\ud83c\udf53",
"stuck_out_tongue": "\ud83d\ude1b",
"stuck_out_tongue_closed_eyes": "\ud83d\ude1d",
"stuck_out_tongue_winking_eye": "\ud83d\ude1c",
"sun_with_face": "\ud83c\udf1e",
"sunflower": "\ud83c\udf3b",
"sunglasses": "\ud83d\ude0e",
"sunny": "\u2600",
"sunrise": "\ud83c\udf05",
"sunrise_over_mountains": "\ud83c\udf04",
"surfer": "\ud83c\udfc4",
"surfer_tone1": "\ud83c\udfc4\ud83c\udffb",
"surfer_tone2": "\ud83c\udfc4\ud83c\udffc",
"surfer_tone3": "\ud83c\udfc4\ud83c\udffd",
"surfer_tone4": "\ud83c\udfc4\ud83c\udffe",
"surfer_tone5": "\ud83c\udfc4\ud83c\udfff",
"sushi": "\ud83c\udf63",
"suspension_railway": "\ud83d\ude9f",
"sweat": "\ud83d\ude13",
"sweat_drops": "\ud83d\udca6",
"sweat_smile": "\ud83d\ude05",
"sweet_potato": "\ud83c\udf60",
"swimmer": "\ud83c\udfca",
"swimmer_tone1": "\ud83c\udfca\ud83c\udffb",
"swimmer_tone2": "\ud83c\udfca\ud83c\udffc",
"swimmer_tone3": "\ud83c\udfca\ud83c\udffd",
"swimmer_tone4": "\ud83c\udfca\ud83c\udffe",
"swimmer_tone5": "\ud83c\udfca\ud83c\udfff",
"symbols": "\ud83d\udd23",
"synagogue": "\ud83d\udd4d",
"syringe": "\ud83d\udc89",
"taco": "\ud83c\udf2e",
"tada": "\ud83c\udf89",
"tanabata_tree": "\ud83c\udf8b",
"tangerine": "\ud83c\udf4a",
"taurus": "\u2649",
"taxi": "\ud83d\ude95",
"tea": "\ud83c\udf75",
"telephone": "\u260e",
"telephone_receiver": "\ud83d\udcde",
"telescope": "\ud83d\udd2d",
"ten": "\ud83d\udd1f",
"tennis": "\ud83c\udfbe",
"tent": "\u26fa",
"thermometer": "\ud83c\udf21",
"thermometer_face": "\ud83e\udd12",
"thinking": "\ud83e\udd14",
"thought_balloon": "\ud83d\udcad",
"three": "3\u20e3",
"thumbsdown": "\ud83d\udc4e",
"thumbsdown_tone1": "\ud83d\udc4e\ud83c\udffb",
"thumbsdown_tone2": "\ud83d\udc4e\ud83c\udffc",
"thumbsdown_tone3": "\ud83d\udc4e\ud83c\udffd",
"thumbsdown_tone4": "\ud83d\udc4e\ud83c\udffe",
"thumbsdown_tone5": "\ud83d\udc4e\ud83c\udfff",
"thumbsup": "\ud83d\udc4d",
"thumbsup_tone1": "\ud83d\udc4d\ud83c\udffb",
"thumbsup_tone2": "\ud83d\udc4d\ud83c\udffc",
"thumbsup_tone3": "\ud83d\udc4d\ud83c\udffd",
"thumbsup_tone4": "\ud83d\udc4d\ud83c\udffe",
"thumbsup_tone5": "\ud83d\udc4d\ud83c\udfff",
"thunder_cloud_rain": "\u26c8",
"ticket": "\ud83c\udfab",
"tickets": "\ud83c\udf9f",
"tiger": "\ud83d\udc2f",
"tiger2": "\ud83d\udc05",
"timer": "\u23f2",
"tired_face": "\ud83d\ude2b",
"tm": "\u2122",
"toilet": "\ud83d\udebd",
"tokyo_tower": "\ud83d\uddfc",
"tomato": "\ud83c\udf45",
"tone1": "\ud83c\udffb",
"tone2": "\ud83c\udffc",
"tone3": "\ud83c\udffd",
"tone4": "\ud83c\udffe",
"tone5": "\ud83c\udfff",
"tongue": "\ud83d\udc45",
"tools": "\ud83d\udee0",
"top": "\ud83d\udd1d",
"tophat": "\ud83c\udfa9",
"track_next": "\u23ed",
"track_previous": "\u23ee",
"trackball": "\ud83d\uddb2",
"tractor": "\ud83d\ude9c",
"traffic_light": "\ud83d\udea5",
"train": "\ud83d\ude8b",
"train2": "\ud83d\ude86",
"tram": "\ud83d\ude8a",
"triangular_flag_on_post": "\ud83d\udea9",
"triangular_ruler": "\ud83d\udcd0",
"trident": "\ud83d\udd31",
"triumph": "\ud83d\ude24",
"trolleybus": "\ud83d\ude8e",
"trophy": "\ud83c\udfc6",
"tropical_drink": "\ud83c\udf79",
"tropical_fish": "\ud83d\udc20",
"truck": "\ud83d\ude9a",
"trumpet": "\ud83c\udfba",
"tulip": "\ud83c\udf37",
"turkey": "\ud83e\udd83",
"turtle": "\ud83d\udc22",
"tv": "\ud83d\udcfa",
"twisted_rightwards_arrows": "\ud83d\udd00",
"two": "2\u20e3",
"two_hearts": "\ud83d\udc95",
"two_men_holding_hands": "\ud83d\udc6c",
"two_women_holding_hands": "\ud83d\udc6d",
"u5272": "\ud83c\ude39",
"u5408": "\ud83c\ude34",
"u55b6": "\ud83c\ude3a",
"u6307": "\ud83c\ude2f",
"u6708": "\ud83c\ude37",
"u6709": "\ud83c\ude36",
"u6e80": "\ud83c\ude35",
"u7121": "\ud83c\ude1a",
"u7533": "\ud83c\ude38",
"u7981": "\ud83c\ude32",
"u7a7a": "\ud83c\ude33",
"umbrella": "\u2614",
"umbrella2": "\u2602",
"unamused": "\ud83d\ude12",
"underage": "\ud83d\udd1e",
"unicorn": "\ud83e\udd84",
"unlock": "\ud83d\udd13",
"up": "\ud83c\udd99",
"upside_down": "\ud83d\ude43",
"urn": "\u26b1",
"v": "\u270c",
"v_tone1": "\u270c\ud83c\udffb",
"v_tone2": "\u270c\ud83c\udffc",
"v_tone3": "\u270c\ud83c\udffd",
"v_tone4": "\u270c\ud83c\udffe",
"v_tone5": "\u270c\ud83c\udfff",
"vertical_traffic_light": "\ud83d\udea6",
"vhs": "\ud83d\udcfc",
"vibration_mode": "\ud83d\udcf3",
"video_camera": "\ud83d\udcf9",
"video_game": "\ud83c\udfae",
"violin": "\ud83c\udfbb",
"virgo": "\u264d",
"volcano": "\ud83c\udf0b",
"volleyball": "\ud83c\udfd0",
"vs": "\ud83c\udd9a",
"vulcan": "\ud83d\udd96",
"vulcan_tone1": "\ud83d\udd96\ud83c\udffb",
"vulcan_tone2": "\ud83d\udd96\ud83c\udffc",
"vulcan_tone3": "\ud83d\udd96\ud83c\udffd",
"vulcan_tone4": "\ud83d\udd96\ud83c\udffe",
"vulcan_tone5": "\ud83d\udd96\ud83c\udfff",
"walking": "\ud83d\udeb6",
"walking_tone1": "\ud83d\udeb6\ud83c\udffb",
"walking_tone2": "\ud83d\udeb6\ud83c\udffc",
"walking_tone3": "\ud83d\udeb6\ud83c\udffd",
"walking_tone4": "\ud83d\udeb6\ud83c\udffe",
"walking_tone5": "\ud83d\udeb6\ud83c\udfff",
"waning_crescent_moon": "\ud83c\udf18",
"waning_gibbous_moon": "\ud83c\udf16",
"warning": "\u26a0",
"wastebasket": "\ud83d\uddd1",
"watch": "\u231a",
"water_buffalo": "\ud83d\udc03",
"watermelon": "\ud83c\udf49",
"wave": "\ud83d\udc4b",
"wave_tone1": "\ud83d\udc4b\ud83c\udffb",
"wave_tone2": "\ud83d\udc4b\ud83c\udffc",
"wave_tone3": "\ud83d\udc4b\ud83c\udffd",
"wave_tone4": "\ud83d\udc4b\ud83c\udffe",
"wave_tone5": "\ud83d\udc4b\ud83c\udfff",
"wavy_dash": "\u3030",
"waxing_crescent_moon": "\ud83c\udf12",
"waxing_gibbous_moon": "\ud83c\udf14",
"wc": "\ud83d\udebe",
"weary": "\ud83d\ude29",
"wedding": "\ud83d\udc92",
"whale": "\ud83d\udc33",
"whale2": "\ud83d\udc0b",
"wheel_of_dharma": "\u2638",
"wheelchair": "\u267f",
"white_check_mark": "\u2705",
"white_circle": "\u26aa",
"white_flower": "\ud83d\udcae",
"white_large_square": "\u2b1c",
"white_medium_small_square": "\u25fd",
"white_medium_square": "\u25fb",
"white_small_square": "\u25ab",
"white_square_button": "\ud83d\udd33",
"white_sun_cloud": "\ud83c\udf25",
"white_sun_rain_cloud": "\ud83c\udf26",
"white_sun_small_cloud": "\ud83c\udf24",
"wind_blowing_face": "\ud83c\udf2c",
"wind_chime": "\ud83c\udf90",
"wine_glass": "\ud83c\udf77",
"wink": "\ud83d\ude09",
"wolf": "\ud83d\udc3a",
"woman": "\ud83d\udc69",
"woman_tone1": "\ud83d\udc69\ud83c\udffb",
"woman_tone2": "\ud83d\udc69\ud83c\udffc",
"woman_tone3": "\ud83d\udc69\ud83c\udffd",
"woman_tone4": "\ud83d\udc69\ud83c\udffe",
"woman_tone5": "\ud83d\udc69\ud83c\udfff",
"womans_clothes": "\ud83d\udc5a",
"womans_hat": "\ud83d\udc52",
"womens": "\ud83d\udeba",
"worried": "\ud83d\ude1f",
"wrench": "\ud83d\udd27",
"writing_hand": "\u270d",
"writing_hand_tone1": "\u270d\ud83c\udffb",
"writing_hand_tone2": "\u270d\ud83c\udffc",
"writing_hand_tone3": "\u270d\ud83c\udffd",
"writing_hand_tone4": "\u270d\ud83c\udffe",
"writing_hand_tone5": "\u270d\ud83c\udfff",
"x": "\u274c",
"yellow_heart": "\ud83d\udc9b",
"yen": "\ud83d\udcb4",
"yin_yang": "\u262f",
"yum": "\ud83d\ude0b",
"zap": "\u26a1",
"zero": "0\u20e3",
"zipper_mouth": "\ud83e\udd10",
"zzz": "\ud83d\udca4",
"100": "\ud83d\udcaf",
- "1234": "\ud83d\udd22"
+ "1234": "\ud83d\udd22",
+
+ "party": "\ud83c\udf89",
+ "celebration": "\ud83c\udf89",
+ "confetti": "\ud83c\udf89"
}
diff --git a/resources/sql/autopatches/20140212.dx.1.armageddon.php b/resources/sql/autopatches/20140212.dx.1.armageddon.php
index 021e88662..a7e978b55 100644
--- a/resources/sql/autopatches/20140212.dx.1.armageddon.php
+++ b/resources/sql/autopatches/20140212.dx.1.armageddon.php
@@ -1,221 +1,221 @@
<?php
$conn_w = id(new DifferentialRevision())->establishConnection('w');
$rows = new LiskRawMigrationIterator($conn_w, 'differential_comment');
$content_source = PhabricatorContentSource::newForSource(
PhabricatorOldWorldContentSource::SOURCECONST)->serialize();
echo pht('Migrating Differential comments to modern storage...')."\n";
foreach ($rows as $row) {
$id = $row['id'];
echo pht('Migrating comment %d...', $id)."\n";
$revision = id(new DifferentialRevision())->load($row['revisionID']);
if (!$revision) {
echo pht('No revision, continuing.')."\n";
continue;
}
$revision_phid = $revision->getPHID();
$comments = queryfx_all(
$conn_w,
'SELECT * FROM %T WHERE legacyCommentID = %d',
'differential_transaction_comment',
$id);
$main_comments = array();
$inline_comments = array();
foreach ($comments as $comment) {
if ($comment['changesetID']) {
$inline_comments[] = $comment;
} else {
$main_comments[] = $comment;
}
}
$metadata = json_decode($row['metadata'], true);
if (!is_array($metadata)) {
$metadata = array();
}
$key_cc = 'added-ccs';
$key_add_rev = 'added-reviewers';
$key_rem_rev = 'removed-reviewers';
$key_diff_id = 'diff-id';
$xactions = array();
// Build the main action transaction.
switch ($row['action']) {
case DifferentialAction::ACTION_COMMENT:
case DifferentialAction::ACTION_ADDREVIEWERS:
case DifferentialAction::ACTION_ADDCCS:
case DifferentialAction::ACTION_UPDATE:
case DifferentialTransaction::TYPE_INLINE:
// These actions will have their transactions created by other rules.
break;
default:
// Otherwise, this is a normal action (like an accept or reject).
$xactions[] = array(
'type' => DifferentialTransaction::TYPE_ACTION,
'old' => null,
'new' => $row['action'],
);
break;
}
// Build the diff update transaction, if one exists.
$diff_id = idx($metadata, $key_diff_id);
if (!is_scalar($diff_id)) {
$diff_id = null;
}
if ($diff_id || $row['action'] == DifferentialAction::ACTION_UPDATE) {
$xactions[] = array(
- 'type' => DifferentialTransaction::TYPE_UPDATE,
+ 'type' => DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE,
'old' => null,
'new' => $diff_id,
);
}
// Build the add/remove reviewers transaction, if one exists.
$add_rev = idx($metadata, $key_add_rev, array());
if (!is_array($add_rev)) {
$add_rev = array();
}
$rem_rev = idx($metadata, $key_rem_rev, array());
if (!is_array($rem_rev)) {
$rem_rev = array();
}
if ($add_rev || $rem_rev) {
$old = array();
foreach ($rem_rev as $phid) {
if (!is_scalar($phid)) {
continue;
}
$old[$phid] = array(
'src' => $revision_phid,
'type' => DifferentialRevisionHasReviewerEdgeType::EDGECONST,
'dst' => $phid,
);
}
$new = array();
foreach ($add_rev as $phid) {
if (!is_scalar($phid)) {
continue;
}
$new[$phid] = array(
'src' => $revision_phid,
'type' => DifferentialRevisionHasReviewerEdgeType::EDGECONST,
'dst' => $phid,
);
}
$xactions[] = array(
'type' => PhabricatorTransactions::TYPE_EDGE,
'old' => $old,
'new' => $new,
'meta' => array(
'edge:type' => DifferentialRevisionHasReviewerEdgeType::EDGECONST,
),
);
}
// Build the CC transaction, if one exists.
$add_cc = idx($metadata, $key_cc, array());
if (!is_array($add_cc)) {
$add_cc = array();
}
if ($add_cc) {
$xactions[] = array(
'type' => PhabricatorTransactions::TYPE_SUBSCRIBERS,
'old' => array(),
'new' => array_fuse($add_cc),
);
}
// Build the main comment transaction.
foreach ($main_comments as $main) {
$xactions[] = array(
'type' => PhabricatorTransactions::TYPE_COMMENT,
'old' => null,
'new' => null,
'phid' => $main['transactionPHID'],
'comment' => $main,
);
}
// Build inline comment transactions.
foreach ($inline_comments as $inline) {
$xactions[] = array(
'type' => DifferentialTransaction::TYPE_INLINE,
'old' => null,
'new' => null,
'phid' => $inline['transactionPHID'],
'comment' => $inline,
);
}
foreach ($xactions as $xaction) {
// Generate a new PHID, if we don't already have one from the comment
// table. We pregenerated into the comment table to make this a little
// easier, so we only need to write to one table.
$xaction_phid = idx($xaction, 'phid');
if (!$xaction_phid) {
$xaction_phid = PhabricatorPHID::generateNewPHID(
PhabricatorApplicationTransactionTransactionPHIDType::TYPECONST,
DifferentialRevisionPHIDType::TYPECONST);
}
unset($xaction['phid']);
$comment_phid = null;
$comment_version = 0;
if (idx($xaction, 'comment')) {
$comment_phid = $xaction['comment']['phid'];
$comment_version = 1;
}
$old = idx($xaction, 'old');
$new = idx($xaction, 'new');
$meta = idx($xaction, 'meta', array());
queryfx(
$conn_w,
'INSERT INTO %T (phid, authorPHID, objectPHID, viewPolicy, editPolicy,
commentPHID, commentVersion, transactionType, oldValue, newValue,
contentSource, metadata, dateCreated, dateModified)
VALUES (%s, %s, %s, %s, %s, %ns, %d, %s, %ns, %ns, %s, %s, %d, %d)',
'differential_transaction',
// PHID, authorPHID, objectPHID
$xaction_phid,
(string)$row['authorPHID'],
$revision_phid,
// viewPolicy, editPolicy, commentPHID, commentVersion
'public',
(string)$row['authorPHID'],
$comment_phid,
$comment_version,
// transactionType, oldValue, newValue, contentSource, metadata
$xaction['type'],
json_encode($old),
json_encode($new),
$content_source,
json_encode($meta),
// dates
$row['dateCreated'],
$row['dateModified']);
}
}
echo pht('Done.')."\n";
diff --git a/resources/sql/autopatches/20161213.diff.01.hunks.php b/resources/sql/autopatches/20161213.diff.01.hunks.php
index 574adb3b4..a3863275f 100644
--- a/resources/sql/autopatches/20161213.diff.01.hunks.php
+++ b/resources/sql/autopatches/20161213.diff.01.hunks.php
@@ -1,39 +1,39 @@
<?php
$conn = id(new DifferentialRevision())->establishConnection('w');
$src_table = 'differential_hunk';
$dst_table = 'differential_hunk_modern';
echo tsprintf(
"%s\n",
pht('Migrating old hunks...'));
foreach (new LiskRawMigrationIterator($conn, $src_table) as $row) {
queryfx(
$conn,
'INSERT INTO %T
(changesetID, oldOffset, oldLen, newOffset, newLen,
dataType, dataEncoding, dataFormat, data,
dateCreated, dateModified)
VALUES
(%d, %d, %d, %d, %d,
%s, %s, %s, %s,
%d, %d)',
$dst_table,
$row['changesetID'],
$row['oldOffset'],
$row['oldLen'],
$row['newOffset'],
$row['newLen'],
- DifferentialModernHunk::DATATYPE_TEXT,
+ DifferentialHunk::DATATYPE_TEXT,
'utf8',
- DifferentialModernHunk::DATAFORMAT_RAW,
+ DifferentialHunk::DATAFORMAT_RAW,
// In rare cases, this could be NULL. See T12090.
(string)$row['changes'],
$row['dateCreated'],
$row['dateModified']);
}
echo tsprintf(
"%s\n",
pht('Done.'));
diff --git a/resources/sql/autopatches/20180207.mail.01.task.sql b/resources/sql/autopatches/20180207.mail.01.task.sql
new file mode 100644
index 000000000..f04b90c80
--- /dev/null
+++ b/resources/sql/autopatches/20180207.mail.01.task.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_maniphest.maniphest_task
+ DROP originalTitle;
diff --git a/resources/sql/autopatches/20180207.mail.02.revision.sql b/resources/sql/autopatches/20180207.mail.02.revision.sql
new file mode 100644
index 000000000..881efbcc9
--- /dev/null
+++ b/resources/sql/autopatches/20180207.mail.02.revision.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_differential.differential_revision
+ DROP originalTitle;
diff --git a/resources/sql/autopatches/20180207.mail.03.mock.sql b/resources/sql/autopatches/20180207.mail.03.mock.sql
new file mode 100644
index 000000000..360d7cf9a
--- /dev/null
+++ b/resources/sql/autopatches/20180207.mail.03.mock.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_pholio.pholio_mock
+ DROP originalName;
diff --git a/resources/sql/autopatches/20180208.maniphest.01.close.sql b/resources/sql/autopatches/20180208.maniphest.01.close.sql
new file mode 100644
index 000000000..856300e9b
--- /dev/null
+++ b/resources/sql/autopatches/20180208.maniphest.01.close.sql
@@ -0,0 +1,5 @@
+ALTER TABLE {$NAMESPACE}_maniphest.maniphest_task
+ ADD closedEpoch INT UNSIGNED;
+
+ALTER TABLE {$NAMESPACE}_maniphest.maniphest_task
+ ADD closerPHID VARBINARY(64);
diff --git a/resources/sql/autopatches/20180208.maniphest.02.populate.php b/resources/sql/autopatches/20180208.maniphest.02.populate.php
new file mode 100644
index 000000000..e11267496
--- /dev/null
+++ b/resources/sql/autopatches/20180208.maniphest.02.populate.php
@@ -0,0 +1,65 @@
+<?php
+
+$table = new ManiphestTask();
+$conn = $table->establishConnection('w');
+$viewer = PhabricatorUser::getOmnipotentUser();
+
+foreach (new LiskMigrationIterator($table) as $task) {
+ if ($task->getClosedEpoch()) {
+ // Task already has a closed date.
+ continue;
+ }
+
+ $status = $task->getStatus();
+ if (!ManiphestTaskStatus::isClosedStatus($status)) {
+ // Task isn't closed.
+ continue;
+ }
+
+ // Look through the transactions from newest to oldest until we find one
+ // where the task was closed. A merge also counts as a close, even though
+ // it doesn't currently produce a separate transaction.
+
+ $type_status = ManiphestTaskStatusTransaction::TRANSACTIONTYPE;
+ $type_merge = ManiphestTaskMergedIntoTransaction::TRANSACTIONTYPE;
+
+ $xactions = id(new ManiphestTransactionQuery())
+ ->setViewer($viewer)
+ ->withObjectPHIDs(array($task->getPHID()))
+ ->withTransactionTypes(
+ array(
+ $type_merge,
+ $type_status,
+ ))
+ ->execute();
+ foreach ($xactions as $xaction) {
+ $old = $xaction->getOldValue();
+ $new = $xaction->getNewValue();
+
+ $type = $xaction->getTransactionType();
+
+ // If this is a status change, but is not a close, don't use it.
+ // (We always use merges, even though it's possible to merge a task which
+ // was previously closed: we can't tell when this happens very easily.)
+ if ($type === $type_status) {
+ if (!ManiphestTaskStatus::isClosedStatus($new)) {
+ continue;
+ }
+
+ if ($old && ManiphestTaskStatus::isClosedStatus($old)) {
+ continue;
+ }
+ }
+
+ queryfx(
+ $conn,
+ 'UPDATE %T SET closedEpoch = %d, closerPHID = %ns
+ WHERE id = %d',
+ $table->getTableName(),
+ $xaction->getDateCreated(),
+ $xaction->getAuthorPHID(),
+ $task->getID());
+
+ break;
+ }
+}
diff --git a/resources/sql/autopatches/20180209.hook.01.hook.sql b/resources/sql/autopatches/20180209.hook.01.hook.sql
new file mode 100644
index 000000000..58b79227a
--- /dev/null
+++ b/resources/sql/autopatches/20180209.hook.01.hook.sql
@@ -0,0 +1,12 @@
+CREATE TABLE {$NAMESPACE}_herald.herald_webhook (
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ phid VARBINARY(64) NOT NULL,
+ name VARCHAR(128) NOT NULL COLLATE {$COLLATE_TEXT},
+ webhookURI VARCHAR(255) NOT NULL COLLATE {$COLLATE_TEXT},
+ viewPolicy VARBINARY(64) NOT NULL,
+ editPolicy VARBINARY(64) NOT NULL,
+ status VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT},
+ hmacKey VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT},
+ dateCreated INT UNSIGNED NOT NULL,
+ dateModified INT UNSIGNED NOT NULL
+) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20180209.hook.02.hookxaction.sql b/resources/sql/autopatches/20180209.hook.02.hookxaction.sql
new file mode 100644
index 000000000..8da594f6b
--- /dev/null
+++ b/resources/sql/autopatches/20180209.hook.02.hookxaction.sql
@@ -0,0 +1,19 @@
+CREATE TABLE {$NAMESPACE}_herald.herald_webhooktransaction (
+ 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) COLLATE {$COLLATE_TEXT} NOT NULL,
+ oldValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL,
+ newValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL,
+ contentSource LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL,
+ metadata LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL,
+ dateCreated INT UNSIGNED NOT NULL,
+ dateModified INT UNSIGNED NOT NULL,
+ UNIQUE KEY `key_phid` (`phid`),
+ KEY `key_object` (`objectPHID`)
+) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20180209.hook.03.hookrequest.sql b/resources/sql/autopatches/20180209.hook.03.hookrequest.sql
new file mode 100644
index 000000000..f20b3a549
--- /dev/null
+++ b/resources/sql/autopatches/20180209.hook.03.hookrequest.sql
@@ -0,0 +1,12 @@
+CREATE TABLE {$NAMESPACE}_herald.herald_webhookrequest (
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ phid VARBINARY(64) NOT NULL,
+ webhookPHID VARBINARY(64) NOT NULL,
+ objectPHID VARBINARY(64) NOT NULL,
+ status VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT},
+ properties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT},
+ lastRequestResult VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT},
+ lastRequestEpoch INT UNSIGNED NOT NULL,
+ dateCreated INT UNSIGNED NOT NULL,
+ dateModified INT UNSIGNED NOT NULL
+) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20180210.hunk.01.droplegacy.sql b/resources/sql/autopatches/20180210.hunk.01.droplegacy.sql
new file mode 100644
index 000000000..129d3927d
--- /dev/null
+++ b/resources/sql/autopatches/20180210.hunk.01.droplegacy.sql
@@ -0,0 +1 @@
+DROP TABLE {$NAMESPACE}_differential.differential_hunk;
diff --git a/resources/sql/autopatches/20180210.hunk.02.renamemodern.sql b/resources/sql/autopatches/20180210.hunk.02.renamemodern.sql
new file mode 100644
index 000000000..d341fbedf
--- /dev/null
+++ b/resources/sql/autopatches/20180210.hunk.02.renamemodern.sql
@@ -0,0 +1,2 @@
+RENAME TABLE {$NAMESPACE}_differential.differential_hunk_modern
+ TO {$NAMESPACE}_differential.differential_hunk;
diff --git a/resources/sql/autopatches/20180212.harbor.01.receiver.sql b/resources/sql/autopatches/20180212.harbor.01.receiver.sql
new file mode 100644
index 000000000..84e9611db
--- /dev/null
+++ b/resources/sql/autopatches/20180212.harbor.01.receiver.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildmessage
+ CHANGE buildTargetPHID receiverPHID VARBINARY(64) NOT NULL;
diff --git a/resources/sql/autopatches/20180214.harbor.01.aborted.php b/resources/sql/autopatches/20180214.harbor.01.aborted.php
new file mode 100644
index 000000000..365f375dc
--- /dev/null
+++ b/resources/sql/autopatches/20180214.harbor.01.aborted.php
@@ -0,0 +1,28 @@
+<?php
+
+$table = new HarbormasterBuildable();
+$conn = $table->establishConnection('w');
+
+foreach (new LiskMigrationIterator($table) as $buildable) {
+ if ($buildable->getBuildableStatus() !== 'building') {
+ continue;
+ }
+
+ $aborted = queryfx_one(
+ $conn,
+ 'SELECT * FROM %T WHERE buildablePHID = %s AND buildStatus = %s
+ LIMIT 1',
+ id(new HarbormasterBuild())->getTableName(),
+ $buildable->getPHID(),
+ 'aborted');
+ if (!$aborted) {
+ continue;
+ }
+
+ queryfx(
+ $conn,
+ 'UPDATE %T SET buildableStatus = %s WHERE id = %d',
+ $table->getTableName(),
+ 'failed',
+ $buildable->getID());
+}
diff --git a/resources/sql/autopatches/20180215.phriction.01.phidcol.sql b/resources/sql/autopatches/20180215.phriction.01.phidcol.sql
new file mode 100644
index 000000000..658b05d9e
--- /dev/null
+++ b/resources/sql/autopatches/20180215.phriction.01.phidcol.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_phriction.phriction_content
+ ADD phid VARBINARY(64) NOT NULL;
diff --git a/resources/sql/autopatches/20180215.phriction.02.phidvalues.php b/resources/sql/autopatches/20180215.phriction.02.phidvalues.php
new file mode 100644
index 000000000..c0a55cac8
--- /dev/null
+++ b/resources/sql/autopatches/20180215.phriction.02.phidvalues.php
@@ -0,0 +1,17 @@
+<?php
+
+$table = new PhrictionContent();
+$conn = $table->establishConnection('w');
+
+foreach (new LiskMigrationIterator($table) as $row) {
+ if (strlen($row->getPHID())) {
+ continue;
+ }
+
+ queryfx(
+ $conn,
+ 'UPDATE %T SET phid = %s WHERE id = %d',
+ $table->getTableName(),
+ $table->generatePHID(),
+ $row->getID());
+}
diff --git a/resources/sql/autopatches/20180215.phriction.03.descempty.sql b/resources/sql/autopatches/20180215.phriction.03.descempty.sql
new file mode 100644
index 000000000..c41df5285
--- /dev/null
+++ b/resources/sql/autopatches/20180215.phriction.03.descempty.sql
@@ -0,0 +1,2 @@
+UPDATE {$NAMESPACE}_phriction.phriction_content
+ SET description = '' WHERE description IS NULL;
diff --git a/resources/sql/autopatches/20180215.phriction.04.descnull.sql b/resources/sql/autopatches/20180215.phriction.04.descnull.sql
new file mode 100644
index 000000000..3ff017cd6
--- /dev/null
+++ b/resources/sql/autopatches/20180215.phriction.04.descnull.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_phriction.phriction_content
+ CHANGE description description LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20180215.phriction.05.statustext.sql b/resources/sql/autopatches/20180215.phriction.05.statustext.sql
new file mode 100644
index 000000000..756f7ac96
--- /dev/null
+++ b/resources/sql/autopatches/20180215.phriction.05.statustext.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_phriction.phriction_document
+ CHANGE status status VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20180215.phriction.06.statusvalue.sql b/resources/sql/autopatches/20180215.phriction.06.statusvalue.sql
new file mode 100644
index 000000000..381de7764
--- /dev/null
+++ b/resources/sql/autopatches/20180215.phriction.06.statusvalue.sql
@@ -0,0 +1,11 @@
+UPDATE {$NAMESPACE}_phriction.phriction_document
+ SET status = 'active' WHERE status = '0';
+
+UPDATE {$NAMESPACE}_phriction.phriction_document
+ SET status = 'deleted' WHERE status = '1';
+
+UPDATE {$NAMESPACE}_phriction.phriction_document
+ SET status = 'moved' WHERE status = '2';
+
+UPDATE {$NAMESPACE}_phriction.phriction_document
+ SET status = 'stub' WHERE status = '3';
diff --git a/resources/sql/autopatches/20180218.fact.01.dim.key.sql b/resources/sql/autopatches/20180218.fact.01.dim.key.sql
new file mode 100644
index 000000000..3a8191502
--- /dev/null
+++ b/resources/sql/autopatches/20180218.fact.01.dim.key.sql
@@ -0,0 +1,5 @@
+CREATE TABLE {$NAMESPACE}_fact.fact_keydimension (
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ factKey VARCHAR(64) NOT NULL COLLATE {$COLLATE_TEXT},
+ UNIQUE KEY `key_factkey` (factKey)
+) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20180218.fact.02.dim.obj.sql b/resources/sql/autopatches/20180218.fact.02.dim.obj.sql
new file mode 100644
index 000000000..6b38062b2
--- /dev/null
+++ b/resources/sql/autopatches/20180218.fact.02.dim.obj.sql
@@ -0,0 +1,5 @@
+CREATE TABLE {$NAMESPACE}_fact.fact_objectdimension (
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ objectPHID VARBINARY(64) NOT NULL,
+ UNIQUE KEY `key_object` (objectPHID)
+) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20180218.fact.03.data.int.sql b/resources/sql/autopatches/20180218.fact.03.data.int.sql
new file mode 100644
index 000000000..d93d54673
--- /dev/null
+++ b/resources/sql/autopatches/20180218.fact.03.data.int.sql
@@ -0,0 +1,8 @@
+CREATE TABLE {$NAMESPACE}_fact.fact_intdatapoint (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ keyID INT UNSIGNED NOT NULL,
+ objectID INT UNSIGNED NOT NULL,
+ dimensionID INT UNSIGNED,
+ value BIGINT SIGNED NOT NULL,
+ epoch INT UNSIGNED NOT NULL
+) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20180222.log.01.filephid.sql b/resources/sql/autopatches/20180222.log.01.filephid.sql
new file mode 100644
index 000000000..a7ef2f2b3
--- /dev/null
+++ b/resources/sql/autopatches/20180222.log.01.filephid.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildlog
+ ADD filePHID VARBINARY(64);
diff --git a/resources/sql/autopatches/20180223.log.01.bytelength.sql b/resources/sql/autopatches/20180223.log.01.bytelength.sql
new file mode 100644
index 000000000..a4c350562
--- /dev/null
+++ b/resources/sql/autopatches/20180223.log.01.bytelength.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildlog
+ ADD byteLength BIGINT UNSIGNED NOT NULL;
diff --git a/resources/sql/autopatches/20180223.log.02.chunkformat.sql b/resources/sql/autopatches/20180223.log.02.chunkformat.sql
new file mode 100644
index 000000000..a15676a95
--- /dev/null
+++ b/resources/sql/autopatches/20180223.log.02.chunkformat.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildlog
+ ADD chunkFormat VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20180223.log.03.chunkdefault.sql b/resources/sql/autopatches/20180223.log.03.chunkdefault.sql
new file mode 100644
index 000000000..2a1f2c812
--- /dev/null
+++ b/resources/sql/autopatches/20180223.log.03.chunkdefault.sql
@@ -0,0 +1,2 @@
+UPDATE {$NAMESPACE}_harbormaster.harbormaster_buildlog
+ SET chunkFormat = 'text' WHERE chunkFormat = '';
diff --git a/resources/sql/autopatches/20180223.log.04.linemap.sql b/resources/sql/autopatches/20180223.log.04.linemap.sql
new file mode 100644
index 000000000..75ed27cf7
--- /dev/null
+++ b/resources/sql/autopatches/20180223.log.04.linemap.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildlog
+ ADD lineMap LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20180223.log.05.linemapdefault.sql b/resources/sql/autopatches/20180223.log.05.linemapdefault.sql
new file mode 100644
index 000000000..59b4dc9a6
--- /dev/null
+++ b/resources/sql/autopatches/20180223.log.05.linemapdefault.sql
@@ -0,0 +1,2 @@
+UPDATE {$NAMESPACE}_harbormaster.harbormaster_buildlog
+ SET lineMap = '[]' WHERE lineMap = '';
diff --git a/resources/sql/autopatches/20180228.log.01.offset.sql b/resources/sql/autopatches/20180228.log.01.offset.sql
new file mode 100644
index 000000000..db20fc292
--- /dev/null
+++ b/resources/sql/autopatches/20180228.log.01.offset.sql
@@ -0,0 +1,5 @@
+ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildlogchunk
+ ADD headOffset BIGINT UNSIGNED NOT NULL;
+
+ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildlogchunk
+ ADD tailOffset BIGINT UNSIGNED NOT NULL;
diff --git a/resources/sql/autopatches/20180305.lock.01.locklog.sql b/resources/sql/autopatches/20180305.lock.01.locklog.sql
new file mode 100644
index 000000000..fa10c21c0
--- /dev/null
+++ b/resources/sql/autopatches/20180305.lock.01.locklog.sql
@@ -0,0 +1,9 @@
+CREATE TABLE {$NAMESPACE}_daemon.daemon_locklog (
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ lockName VARCHAR(64) NOT NULL COLLATE {$COLLATE_TEXT},
+ lockReleased INT UNSIGNED,
+ lockParameters LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT},
+ lockContext LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT},
+ dateCreated INT UNSIGNED NOT NULL,
+ dateModified INT UNSIGNED NOT NULL
+) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20180306.opath.01.digest.sql b/resources/sql/autopatches/20180306.opath.01.digest.sql
new file mode 100644
index 000000000..2418366fc
--- /dev/null
+++ b/resources/sql/autopatches/20180306.opath.01.digest.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_owners.owners_path
+ ADD pathIndex BINARY(12) NOT NULL;
diff --git a/resources/sql/autopatches/20180306.opath.02.digestpopulate.php b/resources/sql/autopatches/20180306.opath.02.digestpopulate.php
new file mode 100644
index 000000000..a6b817fc4
--- /dev/null
+++ b/resources/sql/autopatches/20180306.opath.02.digestpopulate.php
@@ -0,0 +1,19 @@
+<?php
+
+$table = new PhabricatorOwnersPath();
+$conn = $table->establishConnection('w');
+
+foreach (new LiskMigrationIterator($table) as $path) {
+ $index = PhabricatorHash::digestForIndex($path->getPath());
+
+ if ($index === $path->getPathIndex()) {
+ continue;
+ }
+
+ queryfx(
+ $conn,
+ 'UPDATE %T SET pathIndex = %s WHERE id = %d',
+ $table->getTableName(),
+ $index,
+ $path->getID());
+}
diff --git a/resources/sql/autopatches/20180306.opath.03.purge.php b/resources/sql/autopatches/20180306.opath.03.purge.php
new file mode 100644
index 000000000..91a15e2a5
--- /dev/null
+++ b/resources/sql/autopatches/20180306.opath.03.purge.php
@@ -0,0 +1,22 @@
+<?php
+
+$table = new PhabricatorOwnersPath();
+$conn = $table->establishConnection('w');
+
+$seen = array();
+foreach (new LiskMigrationIterator($table) as $path) {
+ $package_id = $path->getPackageID();
+ $repository_phid = $path->getRepositoryPHID();
+ $path_index = $path->getPathIndex();
+
+ if (!isset($seen[$package_id][$repository_phid][$path_index])) {
+ $seen[$package_id][$repository_phid][$path_index] = true;
+ continue;
+ }
+
+ queryfx(
+ $conn,
+ 'DELETE FROM %T WHERE id = %d',
+ $table->getTableName(),
+ $path->getID());
+}
diff --git a/resources/sql/autopatches/20180306.opath.04.unique.sql b/resources/sql/autopatches/20180306.opath.04.unique.sql
new file mode 100644
index 000000000..2349533b1
--- /dev/null
+++ b/resources/sql/autopatches/20180306.opath.04.unique.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_owners.owners_path
+ ADD UNIQUE KEY `key_path` (packageID, repositoryPHID, pathIndex);
diff --git a/resources/sql/autopatches/20180306.opath.05.longpath.sql b/resources/sql/autopatches/20180306.opath.05.longpath.sql
new file mode 100644
index 000000000..79ff2f7a7
--- /dev/null
+++ b/resources/sql/autopatches/20180306.opath.05.longpath.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_owners.owners_path
+ CHANGE path path LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20180306.opath.06.pathdisplay.sql b/resources/sql/autopatches/20180306.opath.06.pathdisplay.sql
new file mode 100644
index 000000000..b9b336ecd
--- /dev/null
+++ b/resources/sql/autopatches/20180306.opath.06.pathdisplay.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_owners.owners_path
+ ADD pathDisplay LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20180306.opath.07.copypaths.sql b/resources/sql/autopatches/20180306.opath.07.copypaths.sql
new file mode 100644
index 000000000..74ebecfa9
--- /dev/null
+++ b/resources/sql/autopatches/20180306.opath.07.copypaths.sql
@@ -0,0 +1,2 @@
+UPDATE {$NAMESPACE}_owners.owners_path
+ SET pathDisplay = path WHERE pathDisplay = '';
diff --git a/resources/sql/autopatches/20180309.owners.01.primaryowner.sql b/resources/sql/autopatches/20180309.owners.01.primaryowner.sql
new file mode 100644
index 000000000..a5eb4368f
--- /dev/null
+++ b/resources/sql/autopatches/20180309.owners.01.primaryowner.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_owners.owners_package
+ DROP primaryOwnerPHID;
diff --git a/resources/sql/autopatches/20180312.reviewers.01.options.sql b/resources/sql/autopatches/20180312.reviewers.01.options.sql
new file mode 100644
index 000000000..159426614
--- /dev/null
+++ b/resources/sql/autopatches/20180312.reviewers.01.options.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_differential.differential_reviewer
+ ADD options LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20180312.reviewers.02.optionsdefault.sql b/resources/sql/autopatches/20180312.reviewers.02.optionsdefault.sql
new file mode 100644
index 000000000..d509011f7
--- /dev/null
+++ b/resources/sql/autopatches/20180312.reviewers.02.optionsdefault.sql
@@ -0,0 +1,2 @@
+UPDATE {$NAMESPACE}_differential.differential_reviewer
+ SET options = '{}' WHERE options = '';
diff --git a/resources/sql/autopatches/20180322.lock.01.identifier.sql b/resources/sql/autopatches/20180322.lock.01.identifier.sql
new file mode 100644
index 000000000..b115a691f
--- /dev/null
+++ b/resources/sql/autopatches/20180322.lock.01.identifier.sql
@@ -0,0 +1,5 @@
+ALTER TABLE {$NAMESPACE}_repository.repository_pushevent
+ ADD requestIdentifier VARBINARY(12);
+
+ALTER TABLE {$NAMESPACE}_repository.repository_pushevent
+ ADD UNIQUE KEY `key_request` (requestIdentifier);
diff --git a/resources/sql/autopatches/20180322.lock.02.wait.sql b/resources/sql/autopatches/20180322.lock.02.wait.sql
new file mode 100644
index 000000000..cba7cc64d
--- /dev/null
+++ b/resources/sql/autopatches/20180322.lock.02.wait.sql
@@ -0,0 +1,8 @@
+ALTER TABLE {$NAMESPACE}_repository.repository_pushevent
+ ADD writeWait BIGINT UNSIGNED;
+
+ALTER TABLE {$NAMESPACE}_repository.repository_pushevent
+ ADD readWait BIGINT UNSIGNED;
+
+ALTER TABLE {$NAMESPACE}_repository.repository_pushevent
+ ADD hostWait BIGINT UNSIGNED;
diff --git a/resources/sql/autopatches/20180326.lock.03.nonunique.sql b/resources/sql/autopatches/20180326.lock.03.nonunique.sql
new file mode 100644
index 000000000..9e12d7e86
--- /dev/null
+++ b/resources/sql/autopatches/20180326.lock.03.nonunique.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_repository.repository_pushevent
+ DROP KEY `key_request`;
diff --git a/resources/sql/autopatches/20180403.draft.01.broadcast.php b/resources/sql/autopatches/20180403.draft.01.broadcast.php
new file mode 100644
index 000000000..b237894c9
--- /dev/null
+++ b/resources/sql/autopatches/20180403.draft.01.broadcast.php
@@ -0,0 +1,20 @@
+<?php
+
+$table = new DifferentialRevision();
+$conn = $table->establishConnection('w');
+
+$drafts = $table->loadAllWhere(
+ 'status = %s',
+ DifferentialRevisionStatus::DRAFT);
+foreach ($drafts as $draft) {
+ $properties = $draft->getProperties();
+
+ $properties[DifferentialRevision::PROPERTY_SHOULD_BROADCAST] = false;
+
+ queryfx(
+ $conn,
+ 'UPDATE %T SET properties = %s WHERE id = %d',
+ id(new DifferentialRevision())->getTableName(),
+ phutil_json_encode($properties),
+ $draft->getID());
+}
diff --git a/scripts/drydock/drydock_control.php b/scripts/drydock/drydock_control.php
index 21ff9f8ee..f22032b28 100755
--- a/scripts/drydock/drydock_control.php
+++ b/scripts/drydock/drydock_control.php
@@ -1,21 +1,21 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
-require_once $root.'/scripts/__init_script__.php';
+require_once $root.'/scripts/init/init-script-with-signals.php';
$args = new PhutilArgumentParser($argv);
$args->setTagline(pht('manage drydock software resources'));
$args->setSynopsis(<<<EOSYNOPSIS
**drydock** __commmand__ [__options__]
Manage Drydock stuff. NEW AND EXPERIMENTAL.
EOSYNOPSIS
);
$args->parseStandardArguments();
$workflows = id(new PhutilClassMapQuery())
->setAncestorClass('DrydockManagementWorkflow')
->execute();
$workflows[] = new PhutilHelpArgumentWorkflow();
$args->parseWorkflows($workflows);
diff --git a/scripts/init/init-script-with-signals.php b/scripts/init/init-script-with-signals.php
new file mode 100644
index 000000000..a479c4b75
--- /dev/null
+++ b/scripts/init/init-script-with-signals.php
@@ -0,0 +1,11 @@
+<?php
+
+// Initialize a script that will handle signals.
+
+if (function_exists('pcntl_async_signals')) {
+ pcntl_async_signals(true);
+} else {
+ declare(ticks = 1);
+}
+
+require_once dirname(__FILE__).'/init-script.php';
diff --git a/scripts/install/install_ubuntu.sh b/scripts/install/install_ubuntu.sh
index b3e678450..5408cdb3e 100755
--- a/scripts/install/install_ubuntu.sh
+++ b/scripts/install/install_ubuntu.sh
@@ -1,92 +1,92 @@
#!/bin/bash
confirm() {
echo "Press RETURN to continue, or ^C to cancel.";
read -e ignored
}
GIT='git'
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 "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
set +x
sudo apt-get -qq update
sudo apt-get install \
$GIT mysql-server apache2 dpkg-dev \
- php5 php5-mysql php5-gd php5-dev php5-curl php-apc php5-cli php5-json
+ php5 php5-mysqlnd php5-gd php5-dev php5-curl php-apc php5-cli php5-json
# Enable mod_rewrite
sudo a2enmod rewrite
HAVEPCNTL=`php -r "echo extension_loaded('pcntl');"`
if [ $HAVEPCNTL != "1" ]
then
echo "Installing pcntl...";
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";
fi
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.";
diff --git a/scripts/repository/commit_hook.php b/scripts/repository/commit_hook.php
index 51abcb6c8..07d6d7cfa 100755
--- a/scripts/repository/commit_hook.php
+++ b/scripts/repository/commit_hook.php
@@ -1,229 +1,234 @@
#!/usr/bin/env php
<?php
// NOTE: This script will sometimes emit a warning like this on startup:
//
// No entry for terminal type "unknown";
// using dumb terminal settings.
//
// This can be fixed by adding "TERM=dumb" to the shebang line, but doing so
// causes some systems to hang mysteriously. See T7119.
// Commit hooks execute in an unusual context where the environment may be
// unavailable, particularly in SVN. The first parameter to this script is
// either a bare repository identifier ("X"), or a repository identifier
// followed by an instance identifier ("X:instance"). If we have an instance
// identifier, unpack it into the environment before we start up. This allows
// subclasses of PhabricatorConfigSiteSource to read it and build an instance
// environment.
if ($argc > 1) {
$context = $argv[1];
$context = explode(':', $context, 2);
$argv[1] = $context[0];
if (count($context) > 1) {
$_ENV['PHABRICATOR_INSTANCE'] = $context[1];
putenv('PHABRICATOR_INSTANCE='.$context[1]);
}
}
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
if ($argc < 2) {
throw new Exception(pht('usage: commit-hook <repository>'));
}
$engine = new DiffusionCommitHookEngine();
$repository = id(new PhabricatorRepositoryQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withIdentifiers(array($argv[1]))
->needProjectPHIDs(true)
->executeOne();
if (!$repository) {
throw new Exception(pht('No such repository "%s"!', $argv[1]));
}
if (!$repository->isHosted()) {
// In Mercurial, the "pretxnchangegroup" hook fires for both pulls and
// pushes. Normally we only install the hook for hosted repositories, but
// if a hosted repository is later converted into an observed repository we
// can end up with an observed repository that has the hook installed.
// If we're running hooks from an observed repository, just exit without
// taking action. For more discussion, see PHI24.
return 0;
}
$engine->setRepository($repository);
$args = new PhutilArgumentParser($argv);
$args->parsePartial(
array(
array(
'name' => 'hook-mode',
'param' => 'mode',
'help' => pht('Hook execution mode.'),
),
));
$argv = array_merge(
array($argv[0]),
$args->getUnconsumedArgumentVector());
// Figure out which user is writing the commit.
$hook_mode = $args->getArg('hook-mode');
if ($hook_mode !== null) {
$known_modes = array(
'svn-revprop' => true,
);
if (empty($known_modes[$hook_mode])) {
throw new Exception(
pht(
'Invalid Hook Mode: This hook was invoked in "%s" mode, but this '.
'is not a recognized hook mode. Valid modes are: %s.',
$hook_mode,
implode(', ', array_keys($known_modes))));
}
}
$is_svnrevprop = ($hook_mode == 'svn-revprop');
if ($is_svnrevprop) {
// For now, we let these through if the repository allows dangerous changes
// and prevent them if it doesn't. See T11208 for discussion.
$revprop_key = $argv[5];
if ($repository->shouldAllowDangerousChanges()) {
$err = 0;
} else {
$err = 1;
$console = PhutilConsole::getConsole();
$console->writeErr(
pht(
"DANGEROUS CHANGE: Dangerous change protection is enabled for this ".
"repository, so you can not change revision properties (you are ".
"attempting to edit \"%s\").\n".
"Edit the repository configuration before making dangerous changes.",
$revprop_key));
}
exit($err);
} else if ($repository->isGit() || $repository->isHg()) {
$username = getenv(DiffusionCommitHookEngine::ENV_USER);
if (!strlen($username)) {
throw new Exception(
pht(
'No Direct Pushes: You are pushing directly to a repository hosted '.
'by Phabricator. This will not work. See "No Direct Pushes" in the '.
'documentation for more information.'));
}
if ($repository->isHg()) {
// We respond to several different hooks in Mercurial.
$engine->setMercurialHook($argv[2]);
}
} else if ($repository->isSVN()) {
// NOTE: In Subversion, the entire environment gets wiped so we can't read
// DiffusionCommitHookEngine::ENV_USER. Instead, we've set "--tunnel-user" to
// specify the correct user; read this user out of the commit log.
if ($argc < 4) {
throw new Exception(pht('usage: commit-hook <repository> <repo> <txn>'));
}
$svn_repo = $argv[2];
$svn_txn = $argv[3];
list($username) = execx('svnlook author -t %s %s', $svn_txn, $svn_repo);
$username = rtrim($username, "\n");
$engine->setSubversionTransactionInfo($svn_txn, $svn_repo);
} else {
throw new Exception(pht('Unknown repository type.'));
}
$user = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withUsernames(array($username))
->executeOne();
if (!$user) {
throw new Exception(pht('No such user "%s"!', $username));
}
$engine->setViewer($user);
// Read stdin for the hook engine.
if ($repository->isHg()) {
// Mercurial leaves stdin open, so we can't just read it until EOF.
$stdin = '';
} else {
// Git and Subversion write data into stdin and then close it. Read the
// data.
$stdin = @file_get_contents('php://stdin');
if ($stdin === false) {
throw new Exception(pht('Failed to read stdin!'));
}
}
$engine->setStdin($stdin);
$engine->setOriginalArgv(array_slice($argv, 2));
$remote_address = getenv(DiffusionCommitHookEngine::ENV_REMOTE_ADDRESS);
if (strlen($remote_address)) {
$engine->setRemoteAddress($remote_address);
}
$remote_protocol = getenv(DiffusionCommitHookEngine::ENV_REMOTE_PROTOCOL);
if (strlen($remote_protocol)) {
$engine->setRemoteProtocol($remote_protocol);
}
+$request_identifier = getenv(DiffusionCommitHookEngine::ENV_REQUEST);
+if (strlen($request_identifier)) {
+ $engine->setRequestIdentifier($request_identifier);
+}
+
try {
$err = $engine->execute();
} catch (DiffusionCommitHookRejectException $ex) {
$console = PhutilConsole::getConsole();
if (PhabricatorEnv::getEnvConfig('phabricator.serious-business')) {
$preamble = pht('*** PUSH REJECTED BY COMMIT HOOK ***');
} else {
$preamble = pht(<<<EOTXT
+---------------------------------------------------------------+
| * * * PUSH REJECTED BY EVIL DRAGON BUREAUCRATS * * * |
+---------------------------------------------------------------+
\
\ ^ /^
\ / \ // \
\ |\___/| / \// .\
\ /V V \__ / // | \ \ *----*
/ / \/_/ // | \ \ \ |
@___@` \/_ // | \ \ \/\ \
0/0/| \/_ // | \ \ \ \
0/0/0/0/| \/// | \ \ | |
0/0/0/0/0/_|_ / ( // | \ _\ | /
0/0/0/0/0/0/`/,_ _ _/ ) ; -. | _ _\.-~ / /
,-} _ *-.|.-~-. .~ ~
\ \__/ `/\ / ~-. _ .-~ /
\____(Oo) *. } { /
( (--) .----~-.\ \-` .~
//__\\\\ \ DENIED! ///.----..< \ _ -~
// \\\\ ///-._ _ _ _ _ _ _{^ - - - - ~
EOTXT
);
}
$console->writeErr("%s\n\n", $preamble);
$console->writeErr("%s\n\n", $ex->getMessage());
$err = 1;
}
exit($err);
diff --git a/scripts/drydock/drydock_control.php b/scripts/setup/manage_conduit.php
similarity index 56%
copy from scripts/drydock/drydock_control.php
copy to scripts/setup/manage_conduit.php
index 21ff9f8ee..07384e7ed 100755
--- a/scripts/drydock/drydock_control.php
+++ b/scripts/setup/manage_conduit.php
@@ -1,21 +1,21 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
-require_once $root.'/scripts/__init_script__.php';
+require_once $root.'/scripts/init/init-script.php';
$args = new PhutilArgumentParser($argv);
-$args->setTagline(pht('manage drydock software resources'));
+$args->setTagline(pht('manage Conduit'));
$args->setSynopsis(<<<EOSYNOPSIS
-**drydock** __commmand__ [__options__]
- Manage Drydock stuff. NEW AND EXPERIMENTAL.
+**conduit** __command__ [__options__]
+ Manage Conduit.
EOSYNOPSIS
-);
+ );
$args->parseStandardArguments();
$workflows = id(new PhutilClassMapQuery())
- ->setAncestorClass('DrydockManagementWorkflow')
+ ->setAncestorClass('PhabricatorConduitManagementWorkflow')
->execute();
$workflows[] = new PhutilHelpArgumentWorkflow();
$args->parseWorkflows($workflows);
diff --git a/scripts/drydock/drydock_control.php b/scripts/setup/manage_lock.php
similarity index 56%
copy from scripts/drydock/drydock_control.php
copy to scripts/setup/manage_lock.php
index 21ff9f8ee..ec5405ec0 100755
--- a/scripts/drydock/drydock_control.php
+++ b/scripts/setup/manage_lock.php
@@ -1,21 +1,21 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
-require_once $root.'/scripts/__init_script__.php';
+require_once $root.'/scripts/init/init-script.php';
$args = new PhutilArgumentParser($argv);
-$args->setTagline(pht('manage drydock software resources'));
+$args->setTagline(pht('manage locks'));
$args->setSynopsis(<<<EOSYNOPSIS
-**drydock** __commmand__ [__options__]
- Manage Drydock stuff. NEW AND EXPERIMENTAL.
+**lock** __command__ [__options__]
+ Manage locks.
EOSYNOPSIS
-);
+ );
$args->parseStandardArguments();
$workflows = id(new PhutilClassMapQuery())
- ->setAncestorClass('DrydockManagementWorkflow')
+ ->setAncestorClass('PhabricatorLockManagementWorkflow')
->execute();
$workflows[] = new PhutilHelpArgumentWorkflow();
$args->parseWorkflows($workflows);
diff --git a/scripts/drydock/drydock_control.php b/scripts/setup/manage_webhook.php
similarity index 56%
copy from scripts/drydock/drydock_control.php
copy to scripts/setup/manage_webhook.php
index 21ff9f8ee..afe662617 100755
--- a/scripts/drydock/drydock_control.php
+++ b/scripts/setup/manage_webhook.php
@@ -1,21 +1,21 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
-require_once $root.'/scripts/__init_script__.php';
+require_once $root.'/scripts/init/init-script.php';
$args = new PhutilArgumentParser($argv);
-$args->setTagline(pht('manage drydock software resources'));
+$args->setTagline(pht('manage webhooks'));
$args->setSynopsis(<<<EOSYNOPSIS
-**drydock** __commmand__ [__options__]
- Manage Drydock stuff. NEW AND EXPERIMENTAL.
+**webhook** __command__ [__options__]
+ Manage webhooks.
EOSYNOPSIS
-);
+ );
$args->parseStandardArguments();
$workflows = id(new PhutilClassMapQuery())
- ->setAncestorClass('DrydockManagementWorkflow')
+ ->setAncestorClass('HeraldWebhookManagementWorkflow')
->execute();
$workflows[] = new PhutilHelpArgumentWorkflow();
$args->parseWorkflows($workflows);
diff --git a/scripts/ssh/ssh-exec.php b/scripts/ssh/ssh-exec.php
index 89a9ba2c1..ad25d2dde 100755
--- a/scripts/ssh/ssh-exec.php
+++ b/scripts/ssh/ssh-exec.php
@@ -1,306 +1,313 @@
#!/usr/bin/env php
<?php
$ssh_start_time = microtime(true);
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$ssh_log = PhabricatorSSHLog::getLog();
+$request_identifier = Filesystem::readRandomCharacters(12);
+$ssh_log->setData(
+ array(
+ 'Q' => $request_identifier,
+ ));
+
$args = new PhutilArgumentParser($argv);
$args->setTagline(pht('execute SSH requests'));
$args->setSynopsis(<<<EOSYNOPSIS
**ssh-exec** --phabricator-ssh-user __user__ [--ssh-command __commmand__]
**ssh-exec** --phabricator-ssh-device __device__ [--ssh-command __commmand__]
Execute authenticated SSH requests. This script is normally invoked
via SSHD, but can be invoked manually for testing.
EOSYNOPSIS
);
$args->parseStandardArguments();
$args->parse(
array(
array(
'name' => 'phabricator-ssh-user',
'param' => 'username',
'help' => pht(
'If the request authenticated with a user key, the name of the '.
'user.'),
),
array(
'name' => 'phabricator-ssh-device',
'param' => 'name',
'help' => pht(
'If the request authenticated with a device key, the name of the '.
'device.'),
),
array(
'name' => 'phabricator-ssh-key',
'param' => 'id',
'help' => pht(
'The ID of the SSH key which authenticated this request. This is '.
'used to allow logs to report when specific keys were used, to make '.
'it easier to manage credentials.'),
),
array(
'name' => 'ssh-command',
'param' => 'command',
'help' => pht(
'Provide a command to execute. This makes testing this script '.
'easier. When running normally, the command is read from the '.
'environment (%s), which is populated by sshd.',
'SSH_ORIGINAL_COMMAND'),
),
));
try {
$remote_address = null;
$ssh_client = getenv('SSH_CLIENT');
if ($ssh_client) {
// This has the format "<ip> <remote-port> <local-port>". Grab the IP.
$remote_address = head(explode(' ', $ssh_client));
$ssh_log->setData(
array(
'r' => $remote_address,
));
}
$key_id = $args->getArg('phabricator-ssh-key');
if ($key_id) {
$ssh_log->setData(
array(
'k' => $key_id,
));
}
$user_name = $args->getArg('phabricator-ssh-user');
$device_name = $args->getArg('phabricator-ssh-device');
$user = null;
$device = null;
$is_cluster_request = false;
if ($user_name && $device_name) {
throw new Exception(
pht(
'The %s and %s flags are mutually exclusive. You can not '.
'authenticate as both a user ("%s") and a device ("%s"). '.
'Specify one or the other, but not both.',
'--phabricator-ssh-user',
'--phabricator-ssh-device',
$user_name,
$device_name));
} else if (strlen($user_name)) {
$user = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withUsernames(array($user_name))
->executeOne();
if (!$user) {
throw new Exception(
pht(
'Invalid username ("%s"). There is no user with this username.',
$user_name));
}
id(new PhabricatorAuthSessionEngine())
->willServeRequestForUser($user);
} else if (strlen($device_name)) {
if (!$remote_address) {
throw new Exception(
pht(
'Unable to identify remote address from the %s environment '.
'variable. Device authentication is accepted only from trusted '.
'sources.',
'SSH_CLIENT'));
}
if (!PhabricatorEnv::isClusterAddress($remote_address)) {
throw new Exception(
pht(
'This request originates from outside of the Phabricator cluster '.
'address range. Requests signed with a trusted device key must '.
'originate from trusted hosts.'));
}
$device = id(new AlmanacDeviceQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withNames(array($device_name))
->executeOne();
if (!$device) {
throw new Exception(
pht(
'Invalid device name ("%s"). There is no device with this name.',
$device_name));
}
// We're authenticated as a device, but we're going to read the user out of
// the command below.
$is_cluster_request = true;
} else {
throw new Exception(
pht(
'This script must be invoked with either the %s or %s flag.',
'--phabricator-ssh-user',
'--phabricator-ssh-device'));
}
if ($args->getArg('ssh-command')) {
$original_command = $args->getArg('ssh-command');
} else {
$original_command = getenv('SSH_ORIGINAL_COMMAND');
}
$original_argv = id(new PhutilShellLexer())
->splitArguments($original_command);
if ($device) {
// If we're authenticating as a device, the first argument may be a
// "@username" argument to act as a particular user.
$first_argument = head($original_argv);
if (preg_match('/^@/', $first_argument)) {
$act_as_name = array_shift($original_argv);
$act_as_name = substr($act_as_name, 1);
$user = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withUsernames(array($act_as_name))
->executeOne();
if (!$user) {
throw new Exception(
pht(
'Device request identifies an acting user with an invalid '.
'username ("%s"). There is no user with this username.',
$act_as_name));
}
} else {
$user = PhabricatorUser::getOmnipotentUser();
}
}
if ($user->isOmnipotent() && $device) {
$user_name = 'device/'.$device->getName();
} else {
$user_name = $user->getUsername();
}
$ssh_log->setData(
array(
'u' => $user_name,
'P' => $user->getPHID(),
));
if (!$device) {
if (!$user->canEstablishSSHSessions()) {
throw new Exception(
pht(
'Your account ("%s") does not have permission to establish SSH '.
'sessions. Visit the web interface for more information.',
$user_name));
}
}
$workflows = id(new PhutilClassMapQuery())
->setAncestorClass('PhabricatorSSHWorkflow')
->setUniqueMethod('getName')
->execute();
if (!$original_argv) {
throw new Exception(
pht(
"Welcome to Phabricator.\n\n".
"You are logged in as %s.\n\n".
"You haven't specified a command to run. This means you're requesting ".
"an interactive shell, but Phabricator does not provide an ".
"interactive shell over SSH.\n\n".
"Usually, you should run a command like `%s` or `%s` ".
"rather than connecting directly with SSH.\n\n".
"Supported commands are: %s.",
$user_name,
'git clone',
'hg push',
implode(', ', array_keys($workflows))));
}
$log_argv = implode(' ', $original_argv);
$log_argv = id(new PhutilUTF8StringTruncator())
->setMaximumCodepoints(128)
->truncateString($log_argv);
$ssh_log->setData(
array(
'C' => $original_argv[0],
'U' => $log_argv,
));
$command = head($original_argv);
$parseable_argv = $original_argv;
array_unshift($parseable_argv, 'phabricator-ssh-exec');
$parsed_args = new PhutilArgumentParser($parseable_argv);
if (empty($workflows[$command])) {
throw new Exception(pht('Invalid command.'));
}
$workflow = $parsed_args->parseWorkflows($workflows);
$workflow->setSSHUser($user);
$workflow->setOriginalArguments($original_argv);
$workflow->setIsClusterRequest($is_cluster_request);
+ $workflow->setRequestIdentifier($request_identifier);
$sock_stdin = fopen('php://stdin', 'r');
if (!$sock_stdin) {
throw new Exception(pht('Unable to open stdin.'));
}
$sock_stdout = fopen('php://stdout', 'w');
if (!$sock_stdout) {
throw new Exception(pht('Unable to open stdout.'));
}
$sock_stderr = fopen('php://stderr', 'w');
if (!$sock_stderr) {
throw new Exception(pht('Unable to open stderr.'));
}
$socket_channel = new PhutilSocketChannel(
$sock_stdin,
$sock_stdout);
$error_channel = new PhutilSocketChannel(null, $sock_stderr);
$metrics_channel = new PhutilMetricsChannel($socket_channel);
$workflow->setIOChannel($metrics_channel);
$workflow->setErrorChannel($error_channel);
$rethrow = null;
try {
$err = $workflow->execute($parsed_args);
$metrics_channel->flush();
$error_channel->flush();
} catch (Exception $ex) {
$rethrow = $ex;
}
// Always write this if we got as far as building a metrics channel.
$ssh_log->setData(
array(
'i' => $metrics_channel->getBytesRead(),
'o' => $metrics_channel->getBytesWritten(),
));
if ($rethrow) {
throw $rethrow;
}
} catch (Exception $ex) {
fwrite(STDERR, "phabricator-ssh-exec: ".$ex->getMessage()."\n");
$err = 1;
}
$ssh_log->setData(
array(
'c' => $err,
'T' => (int)(1000000 * (microtime(true) - $ssh_start_time)),
));
exit($err);
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index 1ad356580..391b8fb1f 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -1,10882 +1,11182 @@
<?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(
'AlamancServiceEditConduitAPIMethod' => 'applications/almanac/conduit/AlamancServiceEditConduitAPIMethod.php',
'AlmanacAddress' => 'applications/almanac/util/AlmanacAddress.php',
'AlmanacBinding' => 'applications/almanac/storage/AlmanacBinding.php',
'AlmanacBindingDisableController' => 'applications/almanac/controller/AlmanacBindingDisableController.php',
'AlmanacBindingEditController' => 'applications/almanac/controller/AlmanacBindingEditController.php',
'AlmanacBindingEditor' => 'applications/almanac/editor/AlmanacBindingEditor.php',
'AlmanacBindingPHIDType' => 'applications/almanac/phid/AlmanacBindingPHIDType.php',
'AlmanacBindingPropertyEditEngine' => 'applications/almanac/editor/AlmanacBindingPropertyEditEngine.php',
'AlmanacBindingQuery' => 'applications/almanac/query/AlmanacBindingQuery.php',
'AlmanacBindingTableView' => 'applications/almanac/view/AlmanacBindingTableView.php',
'AlmanacBindingTransaction' => 'applications/almanac/storage/AlmanacBindingTransaction.php',
'AlmanacBindingTransactionQuery' => 'applications/almanac/query/AlmanacBindingTransactionQuery.php',
'AlmanacBindingViewController' => 'applications/almanac/controller/AlmanacBindingViewController.php',
'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',
'AlmanacDevice' => 'applications/almanac/storage/AlmanacDevice.php',
'AlmanacDeviceController' => 'applications/almanac/controller/AlmanacDeviceController.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',
'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',
'AlmanacDeviceTransaction' => 'applications/almanac/storage/AlmanacDeviceTransaction.php',
'AlmanacDeviceTransactionQuery' => 'applications/almanac/query/AlmanacDeviceTransactionQuery.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',
'AlmanacInterfaceDatasource' => 'applications/almanac/typeahead/AlmanacInterfaceDatasource.php',
'AlmanacInterfaceDeleteController' => 'applications/almanac/controller/AlmanacInterfaceDeleteController.php',
'AlmanacInterfaceEditController' => 'applications/almanac/controller/AlmanacInterfaceEditController.php',
'AlmanacInterfacePHIDType' => 'applications/almanac/phid/AlmanacInterfacePHIDType.php',
'AlmanacInterfaceQuery' => 'applications/almanac/query/AlmanacInterfaceQuery.php',
'AlmanacInterfaceTableView' => 'applications/almanac/view/AlmanacInterfaceTableView.php',
'AlmanacKeys' => 'applications/almanac/util/AlmanacKeys.php',
'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',
'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',
'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',
'AlmanacNamespacePHIDType' => 'applications/almanac/phid/AlmanacNamespacePHIDType.php',
'AlmanacNamespaceQuery' => 'applications/almanac/query/AlmanacNamespaceQuery.php',
'AlmanacNamespaceSearchEngine' => 'applications/almanac/query/AlmanacNamespaceSearchEngine.php',
'AlmanacNamespaceTransaction' => 'applications/almanac/storage/AlmanacNamespaceTransaction.php',
'AlmanacNamespaceTransactionQuery' => 'applications/almanac/query/AlmanacNamespaceTransactionQuery.php',
'AlmanacNamespaceViewController' => 'applications/almanac/controller/AlmanacNamespaceViewController.php',
'AlmanacNetwork' => 'applications/almanac/storage/AlmanacNetwork.php',
'AlmanacNetworkController' => 'applications/almanac/controller/AlmanacNetworkController.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',
'AlmanacNetworkPHIDType' => 'applications/almanac/phid/AlmanacNetworkPHIDType.php',
'AlmanacNetworkQuery' => 'applications/almanac/query/AlmanacNetworkQuery.php',
'AlmanacNetworkSearchEngine' => 'applications/almanac/query/AlmanacNetworkSearchEngine.php',
'AlmanacNetworkTransaction' => 'applications/almanac/storage/AlmanacNetworkTransaction.php',
'AlmanacNetworkTransactionQuery' => 'applications/almanac/query/AlmanacNetworkTransactionQuery.php',
'AlmanacNetworkViewController' => 'applications/almanac/controller/AlmanacNetworkViewController.php',
'AlmanacPropertiesDestructionEngineExtension' => 'applications/almanac/engineextension/AlmanacPropertiesDestructionEngineExtension.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',
'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',
'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',
'AlmanacServiceTransaction' => 'applications/almanac/storage/AlmanacServiceTransaction.php',
'AlmanacServiceTransactionQuery' => 'applications/almanac/query/AlmanacServiceTransactionQuery.php',
'AlmanacServiceType' => 'applications/almanac/servicetype/AlmanacServiceType.php',
'AlmanacServiceTypeDatasource' => 'applications/almanac/typeahead/AlmanacServiceTypeDatasource.php',
'AlmanacServiceTypeTestCase' => 'applications/almanac/servicetype/__tests__/AlmanacServiceTypeTestCase.php',
'AlmanacServiceViewController' => 'applications/almanac/controller/AlmanacServiceViewController.php',
'AlmanacTransaction' => 'applications/almanac/storage/AlmanacTransaction.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',
'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',
'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',
'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',
- 'DifferentialLegacyHunk' => 'applications/differential/storage/DifferentialLegacyHunk.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',
- 'DifferentialModernHunk' => 'applications/differential/storage/DifferentialModernHunk.php',
'DifferentialParseCacheGarbageCollector' => 'applications/differential/garbagecollector/DifferentialParseCacheGarbageCollector.php',
'DifferentialParseCommitMessageConduitAPIMethod' => 'applications/differential/conduit/DifferentialParseCommitMessageConduitAPIMethod.php',
'DifferentialParseRenderTestCase' => 'applications/differential/__tests__/DifferentialParseRenderTestCase.php',
'DifferentialPathField' => 'applications/differential/customfield/DifferentialPathField.php',
'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',
'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',
'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',
'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',
'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',
'DiffusionInlineCommentController' => 'applications/diffusion/controller/DiffusionInlineCommentController.php',
'DiffusionInlineCommentPreviewController' => 'applications/diffusion/controller/DiffusionInlineCommentPreviewController.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',
- 'DiffusionQuickSearchEngineExtension' => 'applications/diffusion/engineextension/DiffusionQuickSearchEngineExtension.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',
'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',
- 'DrydockConstants' => 'applications/drydock/constants/DrydockConstants.php',
'DrydockController' => 'applications/drydock/controller/DrydockController.php',
'DrydockCreateBlueprintsCapability' => 'applications/drydock/capability/DrydockCreateBlueprintsCapability.php',
'DrydockDAO' => 'applications/drydock/storage/DrydockDAO.php',
'DrydockDefaultEditCapability' => 'applications/drydock/capability/DrydockDefaultEditCapability.php',
'DrydockDefaultViewCapability' => 'applications/drydock/capability/DrydockDefaultViewCapability.php',
'DrydockFilesystemInterface' => 'applications/drydock/interface/filesystem/DrydockFilesystemInterface.php',
'DrydockInterface' => 'applications/drydock/interface/DrydockInterface.php',
'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',
'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',
'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',
'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',
'HeraldRuleController' => 'applications/herald/controller/HeraldRuleController.php',
'HeraldRuleDatasource' => 'applications/herald/typeahead/HeraldRuleDatasource.php',
'HeraldRuleEditor' => 'applications/herald/editor/HeraldRuleEditor.php',
'HeraldRuleListController' => 'applications/herald/controller/HeraldRuleListController.php',
'HeraldRulePHIDType' => 'applications/herald/phid/HeraldRulePHIDType.php',
'HeraldRuleQuery' => 'applications/herald/query/HeraldRuleQuery.php',
'HeraldRuleSearchEngine' => 'applications/herald/query/HeraldRuleSearchEngine.php',
'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',
'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',
- 'ManiphestExcelDefaultFormat' => 'applications/maniphest/export/ManiphestExcelDefaultFormat.php',
- 'ManiphestExcelFormat' => 'applications/maniphest/export/ManiphestExcelFormat.php',
- 'ManiphestExcelFormatTestCase' => 'applications/maniphest/export/__tests__/ManiphestExcelFormatTestCase.php',
- 'ManiphestExportController' => 'applications/maniphest/controller/ManiphestExportController.php',
'ManiphestGetTaskTransactionsConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestGetTaskTransactionsConduitAPIMethod.php',
'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',
'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',
'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',
'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/PhabricatorCSVExportFormat.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',
'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',
'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',
'PhabricatorDoorkeeperApplication' => 'applications/doorkeeper/application/PhabricatorDoorkeeperApplication.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/PhabricatorEpochExportField.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',
- 'PhabricatorExportField' => 'infrastructure/export/PhabricatorExportField.php',
- 'PhabricatorExportFormat' => 'infrastructure/export/PhabricatorExportFormat.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',
- 'PhabricatorFactCountEngine' => 'applications/fact/engine/PhabricatorFactCountEngine.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',
- 'PhabricatorFactLastUpdatedEngine' => 'applications/fact/engine/PhabricatorFactLastUpdatedEngine.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',
- 'PhabricatorFactManagementStatusWorkflow' => 'applications/fact/management/PhabricatorFactManagementStatusWorkflow.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',
- 'PhabricatorFactSimpleSpec' => 'applications/fact/spec/PhabricatorFactSimpleSpec.php',
- 'PhabricatorFactSpec' => 'applications/fact/spec/PhabricatorFactSpec.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',
'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',
- 'PhabricatorFileInfoController' => 'applications/files/controller/PhabricatorFileInfoController.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',
- 'PhabricatorFilesComposeAvatarExample' => 'applications/uiexample/examples/PhabricatorFilesComposeAvatarExample.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/PhabricatorIDExportField.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',
- 'PhabricatorIntExportField' => 'infrastructure/export/PhabricatorIntExportField.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',
- 'PhabricatorJSONExportFormat' => 'infrastructure/export/PhabricatorJSONExportFormat.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',
- 'PhabricatorJumpNavHandler' => 'applications/search/engine/PhabricatorJumpNavHandler.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',
'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',
'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',
- 'PhabricatorMonogramQuickSearchEngineExtension' => 'applications/typeahead/engineextension/PhabricatorMonogramQuickSearchEngineExtension.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',
'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/PhabricatorPHIDExportField.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',
- 'PhabricatorPeopleQuickSearchEngineExtension' => 'applications/people/engineextension/PhabricatorPeopleQuickSearchEngineExtension.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',
'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',
'PhabricatorProjectsAncestorsSearchEngineAttachment' => 'applications/project/engineextension/PhabricatorProjectsAncestorsSearchEngineAttachment.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',
'PhabricatorQueryOrderItem' => 'infrastructure/query/order/PhabricatorQueryOrderItem.php',
'PhabricatorQueryOrderTestCase' => 'infrastructure/query/order/__tests__/PhabricatorQueryOrderTestCase.php',
'PhabricatorQueryOrderVector' => 'infrastructure/query/order/PhabricatorQueryOrderVector.php',
- 'PhabricatorQuickSearchApplicationEngineExtension' => 'applications/meta/engineextension/PhabricatorQuickSearchApplicationEngineExtension.php',
- 'PhabricatorQuickSearchEngine' => 'applications/search/engine/PhabricatorQuickSearchEngine.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',
'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',
'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/PhabricatorStringExportField.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',
- 'PhabricatorSystemFaviconController' => 'applications/system/controller/PhabricatorSystemFaviconController.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/PhabricatorTextExportFormat.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',
'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',
'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',
'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',
'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',
'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',
'PhrictionEditConduitAPIMethod' => 'applications/phriction/conduit/PhrictionEditConduitAPIMethod.php',
'PhrictionEditController' => 'applications/phriction/controller/PhrictionEditController.php',
'PhrictionHistoryConduitAPIMethod' => 'applications/phriction/conduit/PhrictionHistoryConduitAPIMethod.php',
'PhrictionHistoryController' => 'applications/phriction/controller/PhrictionHistoryController.php',
'PhrictionInfoConduitAPIMethod' => 'applications/phriction/conduit/PhrictionInfoConduitAPIMethod.php',
'PhrictionListController' => 'applications/phriction/controller/PhrictionListController.php',
'PhrictionMarkupPreviewController' => 'applications/phriction/controller/PhrictionMarkupPreviewController.php',
'PhrictionMoveController' => 'applications/phriction/controller/PhrictionMoveController.php',
'PhrictionNewController' => 'applications/phriction/controller/PhrictionNewController.php',
'PhrictionRemarkupRule' => 'applications/phriction/markup/PhrictionRemarkupRule.php',
'PhrictionReplyHandler' => 'applications/phriction/mail/PhrictionReplyHandler.php',
'PhrictionSchemaSpec' => 'applications/phriction/storage/PhrictionSchemaSpec.php',
- 'PhrictionSearchEngine' => 'applications/phriction/query/PhrictionSearchEngine.php',
'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',
- 'ProjectQuickSearchEngineExtension' => 'applications/project/engineextension/ProjectQuickSearchEngineExtension.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',
'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(
'AlamancServiceEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'AlmanacAddress' => 'Phobject',
'AlmanacBinding' => array(
'AlmanacDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'AlmanacPropertyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorExtendedPolicyInterface',
),
'AlmanacBindingDisableController' => 'AlmanacServiceController',
'AlmanacBindingEditController' => 'AlmanacServiceController',
'AlmanacBindingEditor' => 'AlmanacEditor',
'AlmanacBindingPHIDType' => 'PhabricatorPHIDType',
'AlmanacBindingPropertyEditEngine' => 'AlmanacPropertyEditEngine',
'AlmanacBindingQuery' => 'AlmanacQuery',
'AlmanacBindingTableView' => 'AphrontView',
'AlmanacBindingTransaction' => 'AlmanacTransaction',
'AlmanacBindingTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'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',
'AlmanacDevice' => array(
'AlmanacDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorProjectInterface',
'PhabricatorSSHPublicKeyInterface',
'AlmanacPropertyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorNgramsInterface',
'PhabricatorConduitResultInterface',
'PhabricatorExtendedPolicyInterface',
),
'AlmanacDeviceController' => 'AlmanacController',
'AlmanacDeviceEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'AlmanacDeviceEditController' => 'AlmanacDeviceController',
'AlmanacDeviceEditEngine' => 'PhabricatorEditEngine',
'AlmanacDeviceEditor' => 'AlmanacEditor',
'AlmanacDeviceListController' => 'AlmanacDeviceController',
'AlmanacDeviceNameNgrams' => 'PhabricatorSearchNgrams',
'AlmanacDevicePHIDType' => 'PhabricatorPHIDType',
'AlmanacDevicePropertyEditEngine' => 'AlmanacPropertyEditEngine',
'AlmanacDeviceQuery' => 'AlmanacQuery',
'AlmanacDeviceSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'AlmanacDeviceSearchEngine' => 'PhabricatorApplicationSearchEngine',
'AlmanacDeviceTransaction' => 'AlmanacTransaction',
'AlmanacDeviceTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'AlmanacDeviceViewController' => 'AlmanacDeviceController',
'AlmanacDrydockPoolServiceType' => 'AlmanacServiceType',
'AlmanacEditor' => 'PhabricatorApplicationTransactionEditor',
'AlmanacInterface' => array(
'AlmanacDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorExtendedPolicyInterface',
),
'AlmanacInterfaceDatasource' => 'PhabricatorTypeaheadDatasource',
'AlmanacInterfaceDeleteController' => 'AlmanacDeviceController',
'AlmanacInterfaceEditController' => 'AlmanacDeviceController',
'AlmanacInterfacePHIDType' => 'PhabricatorPHIDType',
'AlmanacInterfaceQuery' => 'AlmanacQuery',
'AlmanacInterfaceTableView' => 'AphrontView',
'AlmanacKeys' => 'Phobject',
'AlmanacManageClusterServicesCapability' => 'PhabricatorPolicyCapability',
'AlmanacManagementRegisterWorkflow' => 'AlmanacManagementWorkflow',
'AlmanacManagementTrustKeyWorkflow' => 'AlmanacManagementWorkflow',
'AlmanacManagementUntrustKeyWorkflow' => 'AlmanacManagementWorkflow',
'AlmanacManagementWorkflow' => 'PhabricatorManagementWorkflow',
'AlmanacNames' => 'Phobject',
'AlmanacNamesTestCase' => 'PhabricatorTestCase',
'AlmanacNamespace' => array(
'AlmanacDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorProjectInterface',
'AlmanacPropertyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorNgramsInterface',
),
'AlmanacNamespaceController' => 'AlmanacController',
'AlmanacNamespaceEditController' => 'AlmanacNamespaceController',
'AlmanacNamespaceEditEngine' => 'PhabricatorEditEngine',
'AlmanacNamespaceEditor' => 'PhabricatorApplicationTransactionEditor',
'AlmanacNamespaceListController' => 'AlmanacNamespaceController',
'AlmanacNamespaceNameNgrams' => 'PhabricatorSearchNgrams',
'AlmanacNamespacePHIDType' => 'PhabricatorPHIDType',
'AlmanacNamespaceQuery' => 'AlmanacQuery',
'AlmanacNamespaceSearchEngine' => 'PhabricatorApplicationSearchEngine',
'AlmanacNamespaceTransaction' => 'PhabricatorApplicationTransaction',
'AlmanacNamespaceTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'AlmanacNamespaceViewController' => 'AlmanacNamespaceController',
'AlmanacNetwork' => array(
'AlmanacDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorNgramsInterface',
),
'AlmanacNetworkController' => 'AlmanacController',
'AlmanacNetworkEditController' => 'AlmanacNetworkController',
'AlmanacNetworkEditEngine' => 'PhabricatorEditEngine',
'AlmanacNetworkEditor' => 'PhabricatorApplicationTransactionEditor',
'AlmanacNetworkListController' => 'AlmanacNetworkController',
'AlmanacNetworkNameNgrams' => 'PhabricatorSearchNgrams',
'AlmanacNetworkPHIDType' => 'PhabricatorPHIDType',
'AlmanacNetworkQuery' => 'AlmanacQuery',
'AlmanacNetworkSearchEngine' => 'PhabricatorApplicationSearchEngine',
'AlmanacNetworkTransaction' => 'PhabricatorApplicationTransaction',
'AlmanacNetworkTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'AlmanacNetworkViewController' => 'AlmanacNetworkController',
'AlmanacPropertiesDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'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',
'AlmanacServiceEditController' => 'AlmanacServiceController',
'AlmanacServiceEditEngine' => 'PhabricatorEditEngine',
'AlmanacServiceEditor' => 'AlmanacEditor',
'AlmanacServiceListController' => 'AlmanacServiceController',
'AlmanacServiceNameNgrams' => 'PhabricatorSearchNgrams',
'AlmanacServicePHIDType' => 'PhabricatorPHIDType',
'AlmanacServicePropertyEditEngine' => 'AlmanacPropertyEditEngine',
'AlmanacServiceQuery' => 'AlmanacQuery',
'AlmanacServiceSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'AlmanacServiceSearchEngine' => 'PhabricatorApplicationSearchEngine',
'AlmanacServiceTransaction' => 'AlmanacTransaction',
'AlmanacServiceTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'AlmanacServiceType' => 'Phobject',
'AlmanacServiceTypeDatasource' => 'PhabricatorTypeaheadDatasource',
'AlmanacServiceTypeTestCase' => 'PhabricatorTestCase',
'AlmanacServiceViewController' => 'AlmanacServiceController',
'AlmanacTransaction' => 'PhabricatorApplicationTransaction',
'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',
'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',
'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',
'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',
- 'DifferentialLegacyHunk' => 'DifferentialHunk',
'DifferentialLegacyQuery' => 'Phobject',
'DifferentialLineAdjustmentMap' => 'Phobject',
'DifferentialLintField' => 'DifferentialHarbormasterField',
'DifferentialLintStatus' => 'Phobject',
'DifferentialLocalCommitsView' => 'AphrontView',
+ 'DifferentialMailEngineExtension' => 'PhabricatorMailEngineExtension',
'DifferentialMailView' => 'Phobject',
'DifferentialManiphestTasksField' => 'DifferentialCoreCustomField',
- 'DifferentialModernHunk' => 'DifferentialHunk',
'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',
'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',
'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',
'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',
'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',
'DiffusionInlineCommentController' => 'PhabricatorInlineCommentController',
'DiffusionInlineCommentPreviewController' => 'PhabricatorInlineCommentPreviewController',
'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',
- 'DiffusionQuickSearchEngineExtension' => 'PhabricatorQuickSearchEngineExtension',
'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',
'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',
- 'DrydockConstants' => 'Phobject',
'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' => 'DrydockConstants',
+ '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' => 'DrydockConstants',
+ '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',
'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',
'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',
'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',
),
'HeraldRuleController' => 'HeraldController',
'HeraldRuleDatasource' => 'PhabricatorTypeaheadDatasource',
'HeraldRuleEditor' => 'PhabricatorApplicationTransactionEditor',
'HeraldRuleListController' => 'HeraldController',
'HeraldRulePHIDType' => 'PhabricatorPHIDType',
'HeraldRuleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HeraldRuleSearchEngine' => 'PhabricatorApplicationSearchEngine',
'HeraldRuleSerializer' => 'Phobject',
'HeraldRuleTestCase' => 'PhabricatorTestCase',
'HeraldRuleTransaction' => 'PhabricatorApplicationTransaction',
'HeraldRuleTransactionComment' => 'PhabricatorApplicationTransactionComment',
'HeraldRuleTranscript' => 'Phobject',
'HeraldRuleTypeConfig' => 'Phobject',
'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',
- 'ManiphestExcelDefaultFormat' => 'ManiphestExcelFormat',
- 'ManiphestExcelFormat' => 'Phobject',
- 'ManiphestExcelFormatTestCase' => 'PhabricatorTestCase',
- 'ManiphestExportController' => 'ManiphestController',
'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',
'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',
'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',
'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',
'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',
),
'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',
'PhabricatorDifferentialRevisionTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorDiffusionApplication' => 'PhabricatorApplication',
'PhabricatorDiffusionBlameSetting' => 'PhabricatorInternalSetting',
'PhabricatorDiffusionConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorDisabledUserController' => 'PhabricatorAuthController',
'PhabricatorDisplayPreferencesSettingsPanel' => 'PhabricatorEditEngineSettingsPanel',
'PhabricatorDisqusAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorDividerEditField' => 'PhabricatorEditField',
'PhabricatorDividerProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorDivinerApplication' => 'PhabricatorApplication',
+ 'PhabricatorDocumentEngine' => 'Phobject',
+ 'PhabricatorDocumentRef' => 'Phobject',
'PhabricatorDoorkeeperApplication' => 'PhabricatorApplication',
'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',
- 'PhabricatorFactCountEngine' => 'PhabricatorFactEngine',
'PhabricatorFactCursor' => 'PhabricatorFactDAO',
'PhabricatorFactDAO' => 'PhabricatorLiskDAO',
'PhabricatorFactDaemon' => 'PhabricatorDaemon',
+ 'PhabricatorFactDatapointQuery' => 'Phobject',
+ 'PhabricatorFactDimension' => 'PhabricatorFactDAO',
'PhabricatorFactEngine' => 'Phobject',
'PhabricatorFactEngineTestCase' => 'PhabricatorTestCase',
'PhabricatorFactHomeController' => 'PhabricatorFactController',
- 'PhabricatorFactLastUpdatedEngine' => 'PhabricatorFactEngine',
+ 'PhabricatorFactIntDatapoint' => 'PhabricatorFactDAO',
+ 'PhabricatorFactKeyDimension' => 'PhabricatorFactDimension',
'PhabricatorFactManagementAnalyzeWorkflow' => 'PhabricatorFactManagementWorkflow',
'PhabricatorFactManagementCursorsWorkflow' => 'PhabricatorFactManagementWorkflow',
'PhabricatorFactManagementDestroyWorkflow' => 'PhabricatorFactManagementWorkflow',
'PhabricatorFactManagementListWorkflow' => 'PhabricatorFactManagementWorkflow',
- 'PhabricatorFactManagementStatusWorkflow' => 'PhabricatorFactManagementWorkflow',
'PhabricatorFactManagementWorkflow' => 'PhabricatorManagementWorkflow',
+ 'PhabricatorFactObjectController' => 'PhabricatorFactController',
+ 'PhabricatorFactObjectDimension' => 'PhabricatorFactDimension',
'PhabricatorFactRaw' => 'PhabricatorFactDAO',
- 'PhabricatorFactSimpleSpec' => 'PhabricatorFactSpec',
- 'PhabricatorFactSpec' => 'Phobject',
'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' => 'PhabricatorFeedDAO',
+ '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',
'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',
- 'PhabricatorFileInfoController' => 'PhabricatorFileController',
'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',
- 'PhabricatorFilesComposeAvatarExample' => 'PhabricatorUIExample',
'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',
'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',
- 'PhabricatorJumpNavHandler' => 'Phobject',
+ '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',
'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',
'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',
- 'PhabricatorMonogramQuickSearchEngineExtension' => 'PhabricatorQuickSearchEngineExtension',
+ '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',
'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',
- 'PhabricatorPeopleQuickSearchEngineExtension' => 'PhabricatorQuickSearchEngineExtension',
'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',
'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',
),
'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',
'PhabricatorProjectsAncestorsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'PhabricatorProjectsCurtainExtension' => 'PHUICurtainExtension',
'PhabricatorProjectsEditEngineExtension' => 'PhabricatorEditEngineExtension',
'PhabricatorProjectsEditField' => 'PhabricatorTokenizerEditField',
+ 'PhabricatorProjectsExportEngineExtension' => 'PhabricatorExportEngineExtension',
'PhabricatorProjectsFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension',
+ 'PhabricatorProjectsMailEngineExtension' => 'PhabricatorMailEngineExtension',
'PhabricatorProjectsMembersSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'PhabricatorProjectsMembershipIndexEngineExtension' => 'PhabricatorIndexEngineExtension',
'PhabricatorProjectsPolicyRule' => 'PhabricatorPolicyRule',
'PhabricatorProjectsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'PhabricatorProjectsSearchEngineExtension' => 'PhabricatorSearchEngineExtension',
'PhabricatorProjectsWatchersSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'PhabricatorPronounSetting' => 'PhabricatorSelectSetting',
'PhabricatorPygmentSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorQuery' => 'Phobject',
'PhabricatorQueryConstraint' => 'Phobject',
'PhabricatorQueryOrderItem' => 'Phobject',
'PhabricatorQueryOrderTestCase' => 'PhabricatorTestCase',
'PhabricatorQueryOrderVector' => array(
'Phobject',
'Iterator',
),
- 'PhabricatorQuickSearchApplicationEngineExtension' => 'PhabricatorQuickSearchEngineExtension',
- 'PhabricatorQuickSearchEngine' => 'Phobject',
- 'PhabricatorQuickSearchEngineExtension' => 'Phobject',
+ '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',
'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',
'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',
- 'PhabricatorSystemFaviconController' => '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',
'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',
'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',
'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',
- 'PhabricatorMarkupInterface',
+ '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',
),
'PhrictionDocumentAuthorHeraldField' => 'PhrictionDocumentHeraldField',
'PhrictionDocumentContentHeraldField' => 'PhrictionDocumentHeraldField',
'PhrictionDocumentContentTransaction' => 'PhrictionDocumentTransactionType',
'PhrictionDocumentController' => 'PhrictionController',
+ 'PhrictionDocumentDatasource' => 'PhabricatorTypeaheadDatasource',
'PhrictionDocumentDeleteTransaction' => 'PhrictionDocumentTransactionType',
'PhrictionDocumentFerretEngine' => 'PhabricatorFerretEngine',
'PhrictionDocumentFulltextEngine' => 'PhabricatorFulltextEngine',
'PhrictionDocumentHeraldAdapter' => 'HeraldAdapter',
'PhrictionDocumentHeraldField' => 'HeraldField',
'PhrictionDocumentHeraldFieldGroup' => 'HeraldFieldGroup',
'PhrictionDocumentMoveAwayTransaction' => 'PhrictionDocumentTransactionType',
'PhrictionDocumentMoveToTransaction' => 'PhrictionDocumentTransactionType',
'PhrictionDocumentPHIDType' => 'PhabricatorPHIDType',
'PhrictionDocumentPathHeraldField' => 'PhrictionDocumentHeraldField',
'PhrictionDocumentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
- 'PhrictionDocumentStatus' => 'PhrictionConstants',
+ 'PhrictionDocumentSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
+ 'PhrictionDocumentSearchEngine' => 'PhabricatorApplicationSearchEngine',
+ 'PhrictionDocumentStatus' => 'PhabricatorObjectStatus',
'PhrictionDocumentTitleHeraldField' => 'PhrictionDocumentHeraldField',
'PhrictionDocumentTitleTransaction' => 'PhrictionDocumentTransactionType',
'PhrictionDocumentTransactionType' => 'PhabricatorModularTransactionType',
'PhrictionEditConduitAPIMethod' => 'PhrictionConduitAPIMethod',
'PhrictionEditController' => 'PhrictionController',
'PhrictionHistoryConduitAPIMethod' => 'PhrictionConduitAPIMethod',
'PhrictionHistoryController' => 'PhrictionController',
'PhrictionInfoConduitAPIMethod' => 'PhrictionConduitAPIMethod',
'PhrictionListController' => 'PhrictionController',
'PhrictionMarkupPreviewController' => 'PhabricatorController',
'PhrictionMoveController' => 'PhrictionController',
'PhrictionNewController' => 'PhrictionController',
'PhrictionRemarkupRule' => 'PhutilRemarkupRule',
'PhrictionReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhrictionSchemaSpec' => 'PhabricatorConfigSchemaSpec',
- 'PhrictionSearchEngine' => 'PhabricatorApplicationSearchEngine',
'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',
- 'ProjectQuickSearchEngineExtension' => 'PhabricatorQuickSearchEngineExtension',
'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',
'UserEnableConduitAPIMethod' => 'UserConduitAPIMethod',
'UserFindConduitAPIMethod' => 'UserConduitAPIMethod',
'UserQueryConduitAPIMethod' => 'UserConduitAPIMethod',
'UserSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'UserWhoAmIConduitAPIMethod' => 'UserConduitAPIMethod',
),
));
diff --git a/src/aphront/AphrontRequest.php b/src/aphront/AphrontRequest.php
index 9d806a851..c1dc0d130 100644
--- a/src/aphront/AphrontRequest.php
+++ b/src/aphront/AphrontRequest.php
@@ -1,853 +1,901 @@
<?php
/**
* @task data Accessing Request Data
* @task cookie Managing Cookies
* @task cluster Working With a Phabricator Cluster
*/
final class AphrontRequest extends Phobject {
// NOTE: These magic request-type parameters are automatically included in
// certain requests (e.g., by phabricator_form(), JX.Request,
// JX.Workflow, and ConduitClient) and help us figure out what sort of
// response the client expects.
const TYPE_AJAX = '__ajax__';
const TYPE_FORM = '__form__';
const TYPE_CONDUIT = '__conduit__';
const TYPE_WORKFLOW = '__wflow__';
const TYPE_CONTINUE = '__continue__';
const TYPE_PREVIEW = '__preview__';
const TYPE_HISEC = '__hisec__';
const TYPE_QUICKSAND = '__quicksand__';
private $host;
private $path;
private $requestData;
private $user;
private $applicationConfiguration;
private $site;
private $controller;
private $uriData = array();
private $cookiePrefix;
public function __construct($host, $path) {
$this->host = $host;
$this->path = $path;
}
public function setURIMap(array $uri_data) {
$this->uriData = $uri_data;
return $this;
}
public function getURIMap() {
return $this->uriData;
}
public function getURIData($key, $default = null) {
return idx($this->uriData, $key, $default);
}
+ /**
+ * Read line range parameter data from the request.
+ *
+ * Applications like Paste, Diffusion, and Harbormaster use "$12-14" in the
+ * URI to allow users to link to particular lines.
+ *
+ * @param string URI data key to pull line range information from.
+ * @param int|null Maximum length of the range.
+ * @return null|pair<int, int> Null, or beginning and end of the range.
+ */
+ public function getURILineRange($key, $limit) {
+ $range = $this->getURIData($key);
+ if (!strlen($range)) {
+ return null;
+ }
+
+ $range = explode('-', $range, 2);
+
+ foreach ($range as $key => $value) {
+ $value = (int)$value;
+ if (!$value) {
+ // If either value is "0", discard the range.
+ return null;
+ }
+ $range[$key] = $value;
+ }
+
+ // If the range is like "$10", treat it like "$10-10".
+ if (count($range) == 1) {
+ $range[] = head($range);
+ }
+
+ // If the range is "$7-5", treat it like "$5-7".
+ if ($range[1] < $range[0]) {
+ $range = array_reverse($range);
+ }
+
+ // If the user specified something like "$1-999999999" and we have a limit,
+ // clamp it to a more reasonable range.
+ if ($limit !== null) {
+ if ($range[1] - $range[0] > $limit) {
+ $range[1] = $range[0] + $limit;
+ }
+ }
+
+ return $range;
+ }
+
public function setApplicationConfiguration(
$application_configuration) {
$this->applicationConfiguration = $application_configuration;
return $this;
}
public function getApplicationConfiguration() {
return $this->applicationConfiguration;
}
public function setPath($path) {
$this->path = $path;
return $this;
}
public function getPath() {
return $this->path;
}
public function getHost() {
// The "Host" header may include a port number, or may be a malicious
// header in the form "realdomain.com:ignored@evil.com". Invoke the full
// parser to extract the real domain correctly. See here for coverage of
// a similar issue in Django:
//
// https://www.djangoproject.com/weblog/2012/oct/17/security/
$uri = new PhutilURI('http://'.$this->host);
return $uri->getDomain();
}
public function setSite(AphrontSite $site) {
$this->site = $site;
return $this;
}
public function getSite() {
return $this->site;
}
public function setController(AphrontController $controller) {
$this->controller = $controller;
return $this;
}
public function getController() {
return $this->controller;
}
/* -( Accessing Request Data )--------------------------------------------- */
/**
* @task data
*/
public function setRequestData(array $request_data) {
$this->requestData = $request_data;
return $this;
}
/**
* @task data
*/
public function getRequestData() {
return $this->requestData;
}
/**
* @task data
*/
public function getInt($name, $default = null) {
if (isset($this->requestData[$name])) {
// Converting from array to int is "undefined". Don't rely on whatever
// PHP decides to do.
if (is_array($this->requestData[$name])) {
return $default;
}
return (int)$this->requestData[$name];
} else {
return $default;
}
}
/**
* @task data
*/
public function getBool($name, $default = null) {
if (isset($this->requestData[$name])) {
if ($this->requestData[$name] === 'true') {
return true;
} else if ($this->requestData[$name] === 'false') {
return false;
} else {
return (bool)$this->requestData[$name];
}
} else {
return $default;
}
}
/**
* @task data
*/
public function getStr($name, $default = null) {
if (isset($this->requestData[$name])) {
$str = (string)$this->requestData[$name];
// Normalize newline craziness.
$str = str_replace(
array("\r\n", "\r"),
array("\n", "\n"),
$str);
return $str;
} else {
return $default;
}
}
/**
* @task data
*/
public function getArr($name, $default = array()) {
if (isset($this->requestData[$name]) &&
is_array($this->requestData[$name])) {
return $this->requestData[$name];
} else {
return $default;
}
}
/**
* @task data
*/
public function getStrList($name, $default = array()) {
if (!isset($this->requestData[$name])) {
return $default;
}
$list = $this->getStr($name);
$list = preg_split('/[\s,]+/', $list, $limit = -1, PREG_SPLIT_NO_EMPTY);
return $list;
}
/**
* @task data
*/
public function getExists($name) {
return array_key_exists($name, $this->requestData);
}
public function getFileExists($name) {
return isset($_FILES[$name]) &&
(idx($_FILES[$name], 'error') !== UPLOAD_ERR_NO_FILE);
}
public function isHTTPGet() {
return ($_SERVER['REQUEST_METHOD'] == 'GET');
}
public function isHTTPPost() {
return ($_SERVER['REQUEST_METHOD'] == 'POST');
}
public function isAjax() {
return $this->getExists(self::TYPE_AJAX) && !$this->isQuicksand();
}
public function isWorkflow() {
return $this->getExists(self::TYPE_WORKFLOW) && !$this->isQuicksand();
}
public function isQuicksand() {
return $this->getExists(self::TYPE_QUICKSAND);
}
public function isConduit() {
return $this->getExists(self::TYPE_CONDUIT);
}
public static function getCSRFTokenName() {
return '__csrf__';
}
public static function getCSRFHeaderName() {
return 'X-Phabricator-Csrf';
}
public static function getViaHeaderName() {
return 'X-Phabricator-Via';
}
public function validateCSRF() {
$token_name = self::getCSRFTokenName();
$token = $this->getStr($token_name);
// No token in the request, check the HTTP header which is added for Ajax
// requests.
if (empty($token)) {
$token = self::getHTTPHeader(self::getCSRFHeaderName());
}
$valid = $this->getUser()->validateCSRFToken($token);
if (!$valid) {
// Add some diagnostic details so we can figure out if some CSRF issues
// are JS problems or people accessing Ajax URIs directly with their
// browsers.
$info = array();
$info[] = pht(
'You are trying to save some data to Phabricator, but the request '.
'your browser made included an incorrect token. Reload the page '.
'and try again. You may need to clear your cookies.');
if ($this->isAjax()) {
$info[] = pht('This was an Ajax request.');
} else {
$info[] = pht('This was a Web request.');
}
if ($token) {
$info[] = pht('This request had an invalid CSRF token.');
} else {
$info[] = pht('This request had no CSRF token.');
}
// Give a more detailed explanation of how to avoid the exception
// in developer mode.
if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {
// TODO: Clean this up, see T1921.
$info[] = pht(
"To avoid this error, use %s to construct forms. If you are already ".
"using %s, make sure the form 'action' uses a relative URI (i.e., ".
"begins with a '%s'). Forms using absolute URIs do not include CSRF ".
"tokens, to prevent leaking tokens to external sites.\n\n".
"If this page performs writes which do not require CSRF protection ".
"(usually, filling caches or logging), you can use %s to ".
"temporarily bypass CSRF protection while writing. You should use ".
"this only for writes which can not be protected with normal CSRF ".
"mechanisms.\n\n".
"Some UI elements (like %s) also have methods which will allow you ".
"to render links as forms (like %s).",
'phabricator_form()',
'phabricator_form()',
'/',
'AphrontWriteGuard::beginScopedUnguardedWrites()',
'PhabricatorActionListView',
'setRenderAsForm(true)');
}
$message = implode("\n", $info);
// This should only be able to happen if you load a form, pull your
// internet for 6 hours, and then reconnect and immediately submit,
// but give the user some indication of what happened since the workflow
// is incredibly confusing otherwise.
throw new AphrontMalformedRequestException(
pht('Invalid Request (CSRF)'),
$message,
true);
}
return true;
}
public function isFormPost() {
$post = $this->getExists(self::TYPE_FORM) &&
!$this->getExists(self::TYPE_HISEC) &&
$this->isHTTPPost();
if (!$post) {
return false;
}
return $this->validateCSRF();
}
public function isFormOrHisecPost() {
$post = $this->getExists(self::TYPE_FORM) &&
$this->isHTTPPost();
if (!$post) {
return false;
}
return $this->validateCSRF();
}
public function setCookiePrefix($prefix) {
$this->cookiePrefix = $prefix;
return $this;
}
private function getPrefixedCookieName($name) {
if (strlen($this->cookiePrefix)) {
return $this->cookiePrefix.'_'.$name;
} else {
return $name;
}
}
public function getCookie($name, $default = null) {
$name = $this->getPrefixedCookieName($name);
$value = idx($_COOKIE, $name, $default);
// Internally, PHP deletes cookies by setting them to the value 'deleted'
// with an expiration date in the past.
// At least in Safari, the browser may send this cookie anyway in some
// circumstances. After logging out, the 302'd GET to /login/ consistently
// includes deleted cookies on my local install. If a cookie value is
// literally 'deleted', pretend it does not exist.
if ($value === 'deleted') {
return null;
}
return $value;
}
public function clearCookie($name) {
$this->setCookieWithExpiration($name, '', time() - (60 * 60 * 24 * 30));
unset($_COOKIE[$name]);
}
/**
* Get the domain which cookies should be set on for this request, or null
* if the request does not correspond to a valid cookie domain.
*
* @return PhutilURI|null Domain URI, or null if no valid domain exists.
*
* @task cookie
*/
private function getCookieDomainURI() {
if (PhabricatorEnv::getEnvConfig('security.require-https') &&
!$this->isHTTPS()) {
return null;
}
$host = $this->getHost();
// If there's no base domain configured, just use whatever the request
// domain is. This makes setup easier, and we'll tell administrators to
// configure a base domain during the setup process.
$base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
if (!strlen($base_uri)) {
return new PhutilURI('http://'.$host.'/');
}
$alternates = PhabricatorEnv::getEnvConfig('phabricator.allowed-uris');
$allowed_uris = array_merge(
array($base_uri),
$alternates);
foreach ($allowed_uris as $allowed_uri) {
$uri = new PhutilURI($allowed_uri);
if ($uri->getDomain() == $host) {
return $uri;
}
}
return null;
}
/**
* Determine if security policy rules will allow cookies to be set when
* responding to the request.
*
* @return bool True if setCookie() will succeed. If this method returns
* false, setCookie() will throw.
*
* @task cookie
*/
public function canSetCookies() {
return (bool)$this->getCookieDomainURI();
}
/**
* Set a cookie which does not expire for a long time.
*
* To set a temporary cookie, see @{method:setTemporaryCookie}.
*
* @param string Cookie name.
* @param string Cookie value.
* @return this
* @task cookie
*/
public function setCookie($name, $value) {
$far_future = time() + (60 * 60 * 24 * 365 * 5);
return $this->setCookieWithExpiration($name, $value, $far_future);
}
/**
* Set a cookie which expires soon.
*
* To set a durable cookie, see @{method:setCookie}.
*
* @param string Cookie name.
* @param string Cookie value.
* @return this
* @task cookie
*/
public function setTemporaryCookie($name, $value) {
return $this->setCookieWithExpiration($name, $value, 0);
}
/**
* Set a cookie with a given expiration policy.
*
* @param string Cookie name.
* @param string Cookie value.
* @param int Epoch timestamp for cookie expiration.
* @return this
* @task cookie
*/
private function setCookieWithExpiration(
$name,
$value,
$expire) {
$is_secure = false;
$base_domain_uri = $this->getCookieDomainURI();
if (!$base_domain_uri) {
$configured_as = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
$accessed_as = $this->getHost();
throw new AphrontMalformedRequestException(
pht('Bad Host Header'),
pht(
'This Phabricator install is configured as "%s", but you are '.
'using the domain name "%s" to access a page which is trying to '.
'set a cookie. Access Phabricator on the configured primary '.
'domain or a configured alternate domain. Phabricator will not '.
'set cookies on other domains for security reasons.',
$configured_as,
$accessed_as),
true);
}
$base_domain = $base_domain_uri->getDomain();
$is_secure = ($base_domain_uri->getProtocol() == 'https');
$name = $this->getPrefixedCookieName($name);
if (php_sapi_name() == 'cli') {
// Do nothing, to avoid triggering "Cannot modify header information"
// warnings.
// TODO: This is effectively a test for whether we're running in a unit
// test or not. Move this actual call to HTTPSink?
} else {
setcookie(
$name,
$value,
$expire,
$path = '/',
$base_domain,
$is_secure,
$http_only = true);
}
$_COOKIE[$name] = $value;
return $this;
}
public function setUser($user) {
$this->user = $user;
return $this;
}
public function getUser() {
return $this->user;
}
public function getViewer() {
return $this->user;
}
public function getRequestURI() {
$get = $_GET;
unset($get['__path__']);
$path = phutil_escape_uri($this->getPath());
return id(new PhutilURI($path))->setQueryParams($get);
}
public function getAbsoluteRequestURI() {
$uri = $this->getRequestURI();
$uri->setDomain($this->getHost());
if ($this->isHTTPS()) {
$protocol = 'https';
} else {
$protocol = 'http';
}
$uri->setProtocol($protocol);
// If the request used a nonstandard port, preserve it while building the
// absolute URI.
// First, get the default port for the request protocol.
$default_port = id(new PhutilURI($protocol.'://example.com/'))
->getPortWithProtocolDefault();
// NOTE: See note in getHost() about malicious "Host" headers. This
// construction defuses some obscure potential attacks.
$port = id(new PhutilURI($protocol.'://'.$this->host))
->getPort();
if (($port !== null) && ($port !== $default_port)) {
$uri->setPort($port);
}
return $uri;
}
public function isDialogFormPost() {
return $this->isFormPost() && $this->getStr('__dialog__');
}
public function getRemoteAddress() {
$address = PhabricatorEnv::getRemoteAddress();
if (!$address) {
return null;
}
return $address->getAddress();
}
public function isHTTPS() {
if (empty($_SERVER['HTTPS'])) {
return false;
}
if (!strcasecmp($_SERVER['HTTPS'], 'off')) {
return false;
}
return true;
}
public function isContinueRequest() {
return $this->isFormPost() && $this->getStr('__continue__');
}
public function isPreviewRequest() {
return $this->isFormPost() && $this->getStr('__preview__');
}
/**
* Get application request parameters in a flattened form suitable for
* inclusion in an HTTP request, excluding parameters with special meanings.
* This is primarily useful if you want to ask the user for more input and
* then resubmit their request.
*
* @return dict<string, string> Original request parameters.
*/
public function getPassthroughRequestParameters($include_quicksand = false) {
return self::flattenData(
$this->getPassthroughRequestData($include_quicksand));
}
/**
* Get request data other than "magic" parameters.
*
* @return dict<string, wild> Request data, with magic filtered out.
*/
public function getPassthroughRequestData($include_quicksand = false) {
$data = $this->getRequestData();
// Remove magic parameters like __dialog__ and __ajax__.
foreach ($data as $key => $value) {
if ($include_quicksand && $key == self::TYPE_QUICKSAND) {
continue;
}
if (!strncmp($key, '__', 2)) {
unset($data[$key]);
}
}
return $data;
}
/**
* Flatten an array of key-value pairs (possibly including arrays as values)
* into a list of key-value pairs suitable for submitting via HTTP request
* (with arrays flattened).
*
* @param dict<string, wild> Data to flatten.
* @return dict<string, string> Flat data suitable for inclusion in an HTTP
* request.
*/
public static function flattenData(array $data) {
$result = array();
foreach ($data as $key => $value) {
if (is_array($value)) {
foreach (self::flattenData($value) as $fkey => $fvalue) {
$fkey = '['.preg_replace('/(?=\[)|$/', ']', $fkey, $limit = 1);
$result[$key.$fkey] = $fvalue;
}
} else {
$result[$key] = (string)$value;
}
}
ksort($result);
return $result;
}
/**
* Read the value of an HTTP header from `$_SERVER`, or a similar datasource.
*
* This function accepts a canonical header name, like `"Accept-Encoding"`,
* and looks up the appropriate value in `$_SERVER` (in this case,
* `"HTTP_ACCEPT_ENCODING"`).
*
* @param string Canonical header name, like `"Accept-Encoding"`.
* @param wild Default value to return if header is not present.
* @param array? Read this instead of `$_SERVER`.
* @return string|wild Header value if present, or `$default` if not.
*/
public static function getHTTPHeader($name, $default = null, $data = null) {
// PHP mangles HTTP headers by uppercasing them and replacing hyphens with
// underscores, then prepending 'HTTP_'.
$php_index = strtoupper($name);
$php_index = str_replace('-', '_', $php_index);
$try_names = array();
$try_names[] = 'HTTP_'.$php_index;
if ($php_index == 'CONTENT_TYPE' || $php_index == 'CONTENT_LENGTH') {
// These headers may be available under alternate names. See
// http://www.php.net/manual/en/reserved.variables.server.php#110763
$try_names[] = $php_index;
}
if ($data === null) {
$data = $_SERVER;
}
foreach ($try_names as $try_name) {
if (array_key_exists($try_name, $data)) {
return $data[$try_name];
}
}
return $default;
}
/* -( Working With a Phabricator Cluster )--------------------------------- */
/**
* Is this a proxied request originating from within the Phabricator cluster?
*
* IMPORTANT: This means the request is dangerous!
*
* These requests are **more dangerous** than normal requests (they can not
* be safely proxied, because proxying them may cause a loop). Cluster
* requests are not guaranteed to come from a trusted source, and should
* never be treated as safer than normal requests. They are strictly less
* safe.
*/
public function isProxiedClusterRequest() {
return (bool)self::getHTTPHeader('X-Phabricator-Cluster');
}
/**
* Build a new @{class:HTTPSFuture} which proxies this request to another
* node in the cluster.
*
* IMPORTANT: This is very dangerous!
*
* The future forwards authentication information present in the request.
* Proxied requests must only be sent to trusted hosts. (We attempt to
* enforce this.)
*
* This is not a general-purpose proxying method; it is a specialized
* method with niche applications and severe security implications.
*
* @param string URI identifying the host we are proxying the request to.
* @return HTTPSFuture New proxy future.
*
* @phutil-external-symbol class PhabricatorStartup
*/
public function newClusterProxyFuture($uri) {
$uri = new PhutilURI($uri);
$domain = $uri->getDomain();
$ip = gethostbyname($domain);
if (!$ip) {
throw new Exception(
pht(
'Unable to resolve domain "%s"!',
$domain));
}
if (!PhabricatorEnv::isClusterAddress($ip)) {
throw new Exception(
pht(
'Refusing to proxy a request to IP address ("%s") which is not '.
'in the cluster address block (this address was derived by '.
'resolving the domain "%s").',
$ip,
$domain));
}
$uri->setPath($this->getPath());
$uri->setQueryParams(self::flattenData($_GET));
$input = PhabricatorStartup::getRawInput();
$future = id(new HTTPSFuture($uri))
->addHeader('Host', self::getHost())
->addHeader('X-Phabricator-Cluster', true)
->setMethod($_SERVER['REQUEST_METHOD'])
->write($input);
if (isset($_SERVER['PHP_AUTH_USER'])) {
$future->setHTTPBasicAuthCredentials(
$_SERVER['PHP_AUTH_USER'],
new PhutilOpaqueEnvelope(idx($_SERVER, 'PHP_AUTH_PW', '')));
}
$headers = array();
$seen = array();
// NOTE: apache_request_headers() might provide a nicer way to do this,
// but isn't available under FCGI until PHP 5.4.0.
foreach ($_SERVER as $key => $value) {
if (!preg_match('/^HTTP_/', $key)) {
continue;
}
// Unmangle the header as best we can.
$key = substr($key, strlen('HTTP_'));
$key = str_replace('_', ' ', $key);
$key = strtolower($key);
$key = ucwords($key);
$key = str_replace(' ', '-', $key);
// By default, do not forward headers.
$should_forward = false;
// Forward "X-Hgarg-..." headers.
if (preg_match('/^X-Hgarg-/', $key)) {
$should_forward = true;
}
if ($should_forward) {
$headers[] = array($key, $value);
$seen[$key] = true;
}
}
// In some situations, this may not be mapped into the HTTP_X constants.
// CONTENT_LENGTH is similarly affected, but we trust cURL to take care
// of that if it matters, since we're handing off a request body.
if (empty($seen['Content-Type'])) {
if (isset($_SERVER['CONTENT_TYPE'])) {
$headers[] = array('Content-Type', $_SERVER['CONTENT_TYPE']);
}
}
foreach ($headers as $header) {
list($key, $value) = $header;
switch ($key) {
case 'Host':
case 'Authorization':
// Don't forward these headers, we've already handled them elsewhere.
unset($headers[$key]);
break;
default:
break;
}
}
foreach ($headers as $header) {
list($key, $value) = $header;
$future->addHeader($key, $value);
}
return $future;
}
}
diff --git a/src/aphront/response/AphrontRedirectResponse.php b/src/aphront/response/AphrontRedirectResponse.php
index 5b4b00979..390ad193c 100644
--- a/src/aphront/response/AphrontRedirectResponse.php
+++ b/src/aphront/response/AphrontRedirectResponse.php
@@ -1,166 +1,176 @@
<?php
/**
* TODO: Should be final but isn't because of AphrontReloadResponse.
*/
class AphrontRedirectResponse extends AphrontResponse {
private $uri;
private $stackWhenCreated;
private $isExternal;
+ private $closeDialogBeforeRedirect;
public function setIsExternal($external) {
$this->isExternal = $external;
return $this;
}
public function __construct() {
if ($this->shouldStopForDebugging()) {
// If we're going to stop, capture the stack so we can print it out.
$this->stackWhenCreated = id(new Exception())->getTrace();
}
}
public function setURI($uri) {
$this->uri = $uri;
return $this;
}
public function getURI() {
// NOTE: When we convert a RedirectResponse into an AjaxResponse, we pull
// the URI through this method. Make sure it passes checks before we
// hand it over to callers.
return self::getURIForRedirect($this->uri, $this->isExternal);
}
public function shouldStopForDebugging() {
return PhabricatorEnv::getEnvConfig('debug.stop-on-redirect');
}
+ public function setCloseDialogBeforeRedirect($close) {
+ $this->closeDialogBeforeRedirect = $close;
+ return $this;
+ }
+
+ public function getCloseDialogBeforeRedirect() {
+ return $this->closeDialogBeforeRedirect;
+ }
+
public function getHeaders() {
$headers = array();
if (!$this->shouldStopForDebugging()) {
$uri = self::getURIForRedirect($this->uri, $this->isExternal);
$headers[] = array('Location', $uri);
}
$headers = array_merge(parent::getHeaders(), $headers);
return $headers;
}
public function buildResponseString() {
if ($this->shouldStopForDebugging()) {
$request = $this->getRequest();
$viewer = $request->getUser();
$view = new PhabricatorStandardPageView();
$view->setRequest($this->getRequest());
$view->setApplicationName(pht('Debug'));
$view->setTitle(pht('Stopped on Redirect'));
$dialog = new AphrontDialogView();
$dialog->setUser($viewer);
$dialog->setTitle(pht('Stopped on Redirect'));
$dialog->appendParagraph(
pht(
'You were stopped here because %s is set in your configuration.',
phutil_tag('tt', array(), 'debug.stop-on-redirect')));
$dialog->appendParagraph(
pht(
'You are being redirected to: %s',
phutil_tag('tt', array(), $this->getURI())));
$dialog->addCancelButton($this->getURI(), pht('Continue'));
$dialog->appendChild(phutil_tag('br'));
$dialog->appendChild(
id(new AphrontStackTraceView())
->setUser($viewer)
->setTrace($this->stackWhenCreated));
$dialog->setIsStandalone(true);
$dialog->setWidth(AphrontDialogView::WIDTH_FULL);
$box = id(new PHUIBoxView())
->addMargin(PHUI::MARGIN_LARGE)
->appendChild($dialog);
$view->appendChild($box);
return $view->render();
}
return '';
}
/**
* Format a URI for use in a "Location:" header.
*
* Verifies that a URI redirects to the expected type of resource (local or
* remote) and formats it for use in a "Location:" header.
*
* The HTTP spec says "Location:" headers must use absolute URIs. Although
* browsers work with relative URIs, we return absolute URIs to avoid
* ambiguity. For example, Chrome interprets "Location: /\evil.com" to mean
* "perform a protocol-relative redirect to evil.com".
*
* @param string URI to redirect to.
* @param bool True if this URI identifies a remote resource.
* @return string URI for use in a "Location:" header.
*/
public static function getURIForRedirect($uri, $is_external) {
$uri_object = new PhutilURI($uri);
if ($is_external) {
// If this is a remote resource it must have a domain set. This
// would also be caught below, but testing for it explicitly first allows
// us to raise a better error message.
if (!strlen($uri_object->getDomain())) {
throw new Exception(
pht(
'Refusing to redirect to external URI "%s". This URI '.
'is not fully qualified, and is missing a domain name. To '.
'redirect to a local resource, remove the external flag.',
(string)$uri));
}
// Check that it's a valid remote resource.
if (!PhabricatorEnv::isValidURIForLink($uri)) {
throw new Exception(
pht(
'Refusing to redirect to external URI "%s". This URI '.
'is not a valid remote web resource.',
(string)$uri));
}
} else {
// If this is a local resource, it must not have a domain set. This allows
// us to raise a better error message than the check below can.
if (strlen($uri_object->getDomain())) {
throw new Exception(
pht(
'Refusing to redirect to local resource "%s". The URI has a '.
'domain, but the redirect is not marked external. Mark '.
'redirects as external to allow redirection off the local '.
'domain.',
(string)$uri));
}
// If this is a local resource, it must be a valid local resource.
if (!PhabricatorEnv::isValidLocalURIForLink($uri)) {
throw new Exception(
pht(
'Refusing to redirect to local resource "%s". This URI is not '.
'formatted in a recognizable way.',
(string)$uri));
}
// Fully qualify the result URI.
$uri = PhabricatorEnv::getURI((string)$uri);
}
return (string)$uri;
}
}
diff --git a/src/aphront/response/AphrontResponse.php b/src/aphront/response/AphrontResponse.php
index 6c52e417e..fe1e80318 100644
--- a/src/aphront/response/AphrontResponse.php
+++ b/src/aphront/response/AphrontResponse.php
@@ -1,264 +1,426 @@
<?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());
}
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');
+ } 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/applications/almanac/management/AlmanacManagementTrustKeyWorkflow.php b/src/applications/almanac/management/AlmanacManagementTrustKeyWorkflow.php
index c0bbc59ff..631cef8c9 100644
--- a/src/applications/almanac/management/AlmanacManagementTrustKeyWorkflow.php
+++ b/src/applications/almanac/management/AlmanacManagementTrustKeyWorkflow.php
@@ -1,90 +1,92 @@
<?php
final class AlmanacManagementTrustKeyWorkflow
extends AlmanacManagementWorkflow {
protected function didConstruct() {
$this
->setName('trust-key')
->setSynopsis(pht('Mark a public key as trusted.'))
->setArguments(
array(
array(
'name' => 'id',
'param' => 'id',
'help' => pht('ID of the key to trust.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$id = $args->getArg('id');
if (!$id) {
throw new PhutilArgumentUsageException(
pht('Specify a public key to trust with --id.'));
}
$key = id(new PhabricatorAuthSSHKeyQuery())
->setViewer($this->getViewer())
->withIDs(array($id))
->executeOne();
if (!$key) {
throw new PhutilArgumentUsageException(
pht('No public key exists with ID "%s".', $id));
}
if (!$key->getIsActive()) {
throw new PhutilArgumentUsageException(
pht('Public key "%s" is not an active key.', $id));
}
if ($key->getIsTrusted()) {
throw new PhutilArgumentUsageException(
pht('Public key with ID %s is already trusted.', $id));
}
if (!($key->getObject() instanceof AlmanacDevice)) {
throw new PhutilArgumentUsageException(
pht('You can only trust keys associated with Almanac devices.'));
}
$handle = id(new PhabricatorHandleQuery())
->setViewer($this->getViewer())
->withPHIDs(array($key->getObject()->getPHID()))
->executeOne();
$console->writeOut(
"**<bg:red> %s </bg>**\n\n%s\n\n%s\n\n%s",
pht('IMPORTANT!'),
phutil_console_wrap(
pht(
'Trusting a public key gives anyone holding the corresponding '.
'private key complete, unrestricted access to all data in '.
'Phabricator. The private key will be able to sign requests that '.
'skip policy and security checks.')),
phutil_console_wrap(
pht(
'This is an advanced feature which should normally be used only '.
'when building a Phabricator cluster. This feature is very '.
'dangerous if misused.')),
pht('This key is associated with device "%s".', $handle->getName()));
$prompt = pht(
'Really trust this key?');
if (!phutil_console_confirm($prompt)) {
throw new PhutilArgumentUsageException(
pht('User aborted workflow.'));
}
$key->setIsTrusted(1);
$key->save();
+ PhabricatorAuthSSHKeyQuery::deleteSSHKeyCache();
+
$console->writeOut(
"**<bg:green> %s </bg>** %s\n",
pht('TRUSTED'),
pht('Key %s has been marked as trusted.', $id));
}
}
diff --git a/src/applications/almanac/management/AlmanacManagementUntrustKeyWorkflow.php b/src/applications/almanac/management/AlmanacManagementUntrustKeyWorkflow.php
index 6ad427eea..6dc7a21aa 100644
--- a/src/applications/almanac/management/AlmanacManagementUntrustKeyWorkflow.php
+++ b/src/applications/almanac/management/AlmanacManagementUntrustKeyWorkflow.php
@@ -1,52 +1,54 @@
<?php
final class AlmanacManagementUntrustKeyWorkflow
extends AlmanacManagementWorkflow {
protected function didConstruct() {
$this
->setName('untrust-key')
->setSynopsis(pht('Revoke trust of a public key.'))
->setArguments(
array(
array(
'name' => 'id',
'param' => 'id',
'help' => pht('ID of the key to revoke trust for.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$id = $args->getArg('id');
if (!$id) {
throw new PhutilArgumentUsageException(
pht('Specify a public key to revoke trust for with --id.'));
}
$key = id(new PhabricatorAuthSSHKeyQuery())
->setViewer($this->getViewer())
->withIDs(array($id))
->executeOne();
if (!$key) {
throw new PhutilArgumentUsageException(
pht('No public key exists with ID "%s".', $id));
}
if (!$key->getIsTrusted()) {
throw new PhutilArgumentUsageException(
pht('Public key with ID %s is not trusted.', $id));
}
$key->setIsTrusted(0);
$key->save();
+ PhabricatorAuthSSHKeyQuery::deleteSSHKeyCache();
+
$console->writeOut(
"**<bg:green> %s </bg>** %s\n",
pht('TRUST REVOKED'),
pht('Trust has been revoked for public key %s.', $id));
}
}
diff --git a/src/applications/audit/editor/PhabricatorAuditEditor.php b/src/applications/audit/editor/PhabricatorAuditEditor.php
index 0cf633923..984e2c147 100644
--- a/src/applications/audit/editor/PhabricatorAuditEditor.php
+++ b/src/applications/audit/editor/PhabricatorAuditEditor.php
@@ -1,805 +1,883 @@
<?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;
}
$state_map = PhabricatorTransactions::getInlineStateMap();
$inlines = id(new DiffusionDiffInlineCommentQuery())
->setViewer($this->getActor())
->withCommitPHIDs(array($object->getPHID()))
->withFixedStates(array_keys($state_map))
->execute();
if (!$inlines) {
break;
}
$old_value = mpull($inlines, 'getFixedState', 'getPHID');
$new_value = array();
foreach ($old_value as $key => $state) {
$new_value[$key] = $state_map[$state];
}
$xactions[] = id(new PhabricatorAuditTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_INLINESTATE)
->setIgnoreOnNoEffect(true)
->setOldValue($old_value)
->setNewValue($new_value);
break;
}
}
return $xactions;
}
private function createAuditRequestTransactionFromCommitMessage(
PhabricatorRepositoryCommit $commit) {
$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) {
- // we are only really trying to find unmentionable phids here...
- // don't bother with this outside initial commit (i.e. create)
- // transaction
+ $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;
}
}
- // "result" is always an array....
- $result = array();
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();
- $monogram = $repository->getMonogram();
$summary = $object->getSummary();
$name = $repository->formatCommitName($identifier);
$subject = "{$name}: {$summary}";
- $thread_topic = "Commit {$monogram}{$identifier}";
$template = id(new PhabricatorMetaMTAMail())
- ->setSubject($subject)
- ->addHeader('Thread-Topic', $thread_topic);
+ ->setSubject($subject);
$this->attachPatch(
$template,
$object);
return $template;
}
protected function getMailTo(PhabricatorLiskDAO $object) {
+ $this->requireAuditors($object);
+
$phids = array();
if ($object->getAuthorPHID()) {
$phids[] = $object->getAuthorPHID();
}
- $status_resigned = PhabricatorAuditStatusConstants::RESIGNED;
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->getAuditStatus() != $status_resigned) {
+ 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/auth/controller/PhabricatorAuthSSHKeyGenerateController.php b/src/applications/auth/controller/PhabricatorAuthSSHKeyGenerateController.php
index f39707618..206a2f1c4 100644
--- a/src/applications/auth/controller/PhabricatorAuthSSHKeyGenerateController.php
+++ b/src/applications/auth/controller/PhabricatorAuthSSHKeyGenerateController.php
@@ -1,109 +1,119 @@
<?php
final class PhabricatorAuthSSHKeyGenerateController
extends PhabricatorAuthSSHKeyController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$key = $this->newKeyForObjectPHID($request->getStr('objectPHID'));
if (!$key) {
return new Aphront404Response();
}
$cancel_uri = $key->getObject()->getSSHPublicKeyManagementURI($viewer);
$token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$viewer,
$request,
$cancel_uri);
if ($request->isFormPost()) {
$default_name = $key->getObject()->getSSHKeyDefaultName();
$keys = PhabricatorSSHKeyGenerator::generateKeypair();
list($public_key, $private_key) = $keys;
+ $key_name = $default_name.'.key';
+
$file = PhabricatorFile::newFromFileData(
$private_key,
array(
- 'name' => $default_name.'.key',
+ 'name' => $key_name,
'ttl.relative' => phutil_units('10 minutes in seconds'),
'viewPolicy' => $viewer->getPHID(),
));
$public_key = PhabricatorAuthSSHPublicKey::newFromRawKey($public_key);
$type = $public_key->getType();
$body = $public_key->getBody();
$comment = pht('Generated');
$entire_key = "{$type} {$body} {$comment}";
$type_create = PhabricatorTransactions::TYPE_CREATE;
$type_name = PhabricatorAuthSSHKeyTransaction::TYPE_NAME;
$type_key = PhabricatorAuthSSHKeyTransaction::TYPE_KEY;
$xactions = array();
$xactions[] = id(new PhabricatorAuthSSHKeyTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_CREATE);
$xactions[] = id(new PhabricatorAuthSSHKeyTransaction())
->setTransactionType($type_name)
->setNewValue($default_name);
$xactions[] = id(new PhabricatorAuthSSHKeyTransaction())
->setTransactionType($type_key)
->setNewValue($entire_key);
$editor = id(new PhabricatorAuthSSHKeyEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->applyTransactions($key, $xactions);
- // NOTE: We're disabling workflow on submit so the download works. We're
- // disabling workflow on cancel so the page reloads, showing the new
- // key.
+ $download_link = phutil_tag(
+ 'a',
+ array(
+ 'href' => $file->getDownloadURI(),
+ ),
+ array(
+ id(new PHUIIconView())->setIcon('fa-download'),
+ ' ',
+ pht('Download Private Key (%s)', $key_name),
+ ));
+ $download_link = phutil_tag('strong', array(), $download_link);
+
+ // NOTE: We're disabling workflow on cancel so the page reloads, showing
+ // the new key.
return $this->newDialog()
->setTitle(pht('Download Private Key'))
- ->setDisableWorkflowOnCancel(true)
- ->setDisableWorkflowOnSubmit(true)
- ->setSubmitURI($file->getDownloadURI())
->appendParagraph(
pht(
'A keypair has been generated, and the public key has been '.
- 'added as a recognized key. Use the button below to download '.
- 'the private key.'))
+ 'added as a recognized key.'))
+ ->appendParagraph($download_link)
->appendParagraph(
pht(
'After you download the private key, it will be destroyed. '.
'You will not be able to retrieve it if you lose your copy.'))
- ->addSubmitButton(pht('Download Private Key'))
+ ->setDisableWorkflowOnCancel(true)
->addCancelButton($cancel_uri, pht('Done'));
}
try {
PhabricatorSSHKeyGenerator::assertCanGenerateKeypair();
return $this->newDialog()
->setTitle(pht('Generate New Keypair'))
->addHiddenInput('objectPHID', $key->getObject()->getPHID())
->appendParagraph(
pht(
'This workflow will generate a new SSH keypair, add the public '.
'key, and let you download the private key.'))
->appendParagraph(
pht('Phabricator will not retain a copy of the private key.'))
->addSubmitButton(pht('Generate New Keypair'))
->addCancelButton($cancel_uri);
} catch (Exception $ex) {
return $this->newDialog()
->setTitle(pht('Unable to Generate Keys'))
->appendParagraph($ex->getMessage())
->addCancelButton($cancel_uri);
}
}
}
diff --git a/src/applications/auth/editor/PhabricatorAuthSSHKeyEditor.php b/src/applications/auth/editor/PhabricatorAuthSSHKeyEditor.php
index 569c37403..3f178c985 100644
--- a/src/applications/auth/editor/PhabricatorAuthSSHKeyEditor.php
+++ b/src/applications/auth/editor/PhabricatorAuthSSHKeyEditor.php
@@ -1,307 +1,305 @@
<?php
final class PhabricatorAuthSSHKeyEditor
extends PhabricatorApplicationTransactionEditor {
private $isAdministrativeEdit;
public function setIsAdministrativeEdit($is_administrative_edit) {
$this->isAdministrativeEdit = $is_administrative_edit;
return $this;
}
public function getIsAdministrativeEdit() {
return $this->isAdministrativeEdit;
}
public function getEditorApplicationClass() {
return 'PhabricatorAuthApplication';
}
public function getEditorObjectsDescription() {
return pht('SSH Keys');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorAuthSSHKeyTransaction::TYPE_NAME;
$types[] = PhabricatorAuthSSHKeyTransaction::TYPE_KEY;
$types[] = PhabricatorAuthSSHKeyTransaction::TYPE_DEACTIVATE;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorAuthSSHKeyTransaction::TYPE_NAME:
return $object->getName();
case PhabricatorAuthSSHKeyTransaction::TYPE_KEY:
return $object->getEntireKey();
case PhabricatorAuthSSHKeyTransaction::TYPE_DEACTIVATE:
return !$object->getIsActive();
}
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorAuthSSHKeyTransaction::TYPE_NAME:
case PhabricatorAuthSSHKeyTransaction::TYPE_KEY:
return $xaction->getNewValue();
case PhabricatorAuthSSHKeyTransaction::TYPE_DEACTIVATE:
return (bool)$xaction->getNewValue();
}
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$value = $xaction->getNewValue();
switch ($xaction->getTransactionType()) {
case PhabricatorAuthSSHKeyTransaction::TYPE_NAME:
$object->setName($value);
return;
case PhabricatorAuthSSHKeyTransaction::TYPE_KEY:
$public_key = PhabricatorAuthSSHPublicKey::newFromRawKey($value);
$type = $public_key->getType();
$body = $public_key->getBody();
$comment = $public_key->getComment();
$object->setKeyType($type);
$object->setKeyBody($body);
$object->setKeyComment($comment);
return;
case PhabricatorAuthSSHKeyTransaction::TYPE_DEACTIVATE:
if ($value) {
$new = null;
} else {
$new = 1;
}
$object->setIsActive($new);
return;
}
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
return;
}
protected function validateTransaction(
PhabricatorLiskDAO $object,
$type,
array $xactions) {
$errors = parent::validateTransaction($object, $type, $xactions);
$viewer = $this->requireActor();
switch ($type) {
case PhabricatorAuthSSHKeyTransaction::TYPE_NAME:
$missing = $this->validateIsEmptyTextField(
$object->getName(),
$xactions);
if ($missing) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Required'),
pht('SSH key name is required.'),
nonempty(last($xactions), null));
$error->setIsMissingFieldError(true);
$errors[] = $error;
}
break;
case PhabricatorAuthSSHKeyTransaction::TYPE_KEY;
$missing = $this->validateIsEmptyTextField(
$object->getName(),
$xactions);
if ($missing) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Required'),
pht('SSH key material is required.'),
nonempty(last($xactions), null));
$error->setIsMissingFieldError(true);
$errors[] = $error;
} else {
foreach ($xactions as $xaction) {
$new = $xaction->getNewValue();
try {
$public_key = PhabricatorAuthSSHPublicKey::newFromRawKey($new);
} catch (Exception $ex) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
$ex->getMessage(),
$xaction);
continue;
}
// The database does not have a unique key on just the <keyBody>
// column because we allow multiple accounts to revoke the same
// key, so we can't rely on database constraints to prevent users
// from adding keys that are on the revocation list back to their
// accounts. Explicitly check for a revoked copy of the key.
$revoked_keys = id(new PhabricatorAuthSSHKeyQuery())
->setViewer($viewer)
->withObjectPHIDs(array($object->getObjectPHID()))
->withIsActive(0)
->withKeys(array($public_key))
->execute();
if ($revoked_keys) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Revoked'),
pht(
'This key has been revoked. Choose or generate a new, '.
'unique key.'),
$xaction);
continue;
}
}
}
break;
case PhabricatorAuthSSHKeyTransaction::TYPE_DEACTIVATE:
foreach ($xactions as $xaction) {
if (!$xaction->getNewValue()) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht('SSH keys can not be reactivated.'),
$xaction);
}
}
break;
}
return $errors;
}
protected function didCatchDuplicateKeyException(
PhabricatorLiskDAO $object,
array $xactions,
Exception $ex) {
$errors = array();
$errors[] = new PhabricatorApplicationTransactionValidationError(
PhabricatorAuthSSHKeyTransaction::TYPE_KEY,
pht('Duplicate'),
pht(
'This public key is already associated with another user or device. '.
'Each key must unambiguously identify a single unique owner.'),
null);
throw new PhabricatorApplicationTransactionValidationException($errors);
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function getMailSubjectPrefix() {
return pht('[SSH Key]');
}
protected function getMailThreadID(PhabricatorLiskDAO $object) {
return 'ssh-key-'.$object->getPHID();
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
// After making any change to an SSH key, drop the authfile cache so it
// is regenerated the next time anyone authenticates.
PhabricatorAuthSSHKeyQuery::deleteSSHKeyCache();
return $xactions;
}
protected function getMailTo(PhabricatorLiskDAO $object) {
return $object->getObject()->getSSHKeyNotifyPHIDs();
}
protected function getMailCC(PhabricatorLiskDAO $object) {
return array();
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new PhabricatorAuthSSHKeyReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
$name = $object->getName();
- $phid = $object->getPHID();
$mail = id(new PhabricatorMetaMTAMail())
- ->setSubject(pht('SSH Key %d: %s', $id, $name))
- ->addHeader('Thread-Topic', $phid);
+ ->setSubject(pht('SSH Key %d: %s', $id, $name));
// The primary value of this mail is alerting users to account compromises,
// so force delivery. In particular, this mail should still be delivered
// even if "self mail" is disabled.
$mail->setForceDelivery(true);
return $mail;
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
if (!$this->getIsAdministrativeEdit()) {
$body->addTextSection(
pht('SECURITY WARNING'),
pht(
'If you do not recognize this change, it may indicate your account '.
'has been compromised.'));
}
$detail_uri = $object->getURI();
$detail_uri = PhabricatorEnv::getProductionURI($detail_uri);
$body->addLinkSection(pht('SSH KEY DETAIL'), $detail_uri);
return $body;
}
protected function getCustomWorkerState() {
return array(
'isAdministrativeEdit' => $this->isAdministrativeEdit,
);
}
protected function loadCustomWorkerState(array $state) {
$this->isAdministrativeEdit = idx($state, 'isAdministrativeEdit');
return $this;
}
}
diff --git a/src/applications/auth/engine/PhabricatorAuthPasswordEngine.php b/src/applications/auth/engine/PhabricatorAuthPasswordEngine.php
index c09aa8830..067cea30e 100644
--- a/src/applications/auth/engine/PhabricatorAuthPasswordEngine.php
+++ b/src/applications/auth/engine/PhabricatorAuthPasswordEngine.php
@@ -1,320 +1,324 @@
<?php
final class PhabricatorAuthPasswordEngine
extends Phobject {
private $viewer;
private $contentSource;
private $object;
private $passwordType;
private $upgradeHashers = true;
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source;
return $this;
}
public function getContentSource() {
return $this->contentSource;
}
public function setObject(PhabricatorAuthPasswordHashInterface $object) {
$this->object = $object;
return $this;
}
public function getObject() {
return $this->object;
}
public function setPasswordType($password_type) {
$this->passwordType = $password_type;
return $this;
}
public function getPasswordType() {
return $this->passwordType;
}
public function setUpgradeHashers($upgrade_hashers) {
$this->upgradeHashers = $upgrade_hashers;
return $this;
}
public function getUpgradeHashers() {
return $this->upgradeHashers;
}
public function checkNewPassword(
PhutilOpaqueEnvelope $password,
PhutilOpaqueEnvelope $confirm,
$can_skip = false) {
$raw_password = $password->openEnvelope();
if (!strlen($raw_password)) {
if ($can_skip) {
throw new PhabricatorAuthPasswordException(
pht('You must choose a password or skip this step.'),
pht('Required'));
} else {
throw new PhabricatorAuthPasswordException(
pht('You must choose a password.'),
pht('Required'));
}
}
$min_len = PhabricatorEnv::getEnvConfig('account.minimum-password-length');
$min_len = (int)$min_len;
if ($min_len) {
if (strlen($raw_password) < $min_len) {
throw new PhabricatorAuthPasswordException(
pht(
'The selected password is too short. Passwords must be a minimum '.
'of %s characters long.',
new PhutilNumber($min_len)),
pht('Too Short'));
}
}
$raw_confirm = $confirm->openEnvelope();
if (!strlen($raw_confirm)) {
throw new PhabricatorAuthPasswordException(
pht('You must confirm the selected password.'),
null,
pht('Required'));
}
if ($raw_password !== $raw_confirm) {
throw new PhabricatorAuthPasswordException(
pht('The password and confirmation do not match.'),
pht('Invalid'),
pht('Invalid'));
}
if (PhabricatorCommonPasswords::isCommonPassword($raw_password)) {
throw new PhabricatorAuthPasswordException(
pht(
'The selected password is very weak: it is one of the most common '.
'passwords in use. Choose a stronger password.'),
pht('Very Weak'));
}
// If we're creating a brand new object (like registering a new user)
// and it does not have a PHID yet, it isn't possible for it to have any
// revoked passwords or colliding passwords either, so we can skip these
// checks.
if ($this->getObject()->getPHID()) {
if ($this->isRevokedPassword($password)) {
throw new PhabricatorAuthPasswordException(
pht(
'The password you entered has been revoked. You can not reuse '.
'a password which has been revoked. Choose a new password.'),
pht('Revoked'));
}
if (!$this->isUniquePassword($password)) {
throw new PhabricatorAuthPasswordException(
pht(
'The password you entered is the same as another password '.
'associated with your account. Each password must be unique.'),
pht('Not Unique'));
}
}
}
public function isValidPassword(PhutilOpaqueEnvelope $envelope) {
$this->requireSetup();
$password_type = $this->getPasswordType();
$passwords = $this->newQuery()
->withPasswordTypes(array($password_type))
->withIsRevoked(false)
->execute();
$matches = $this->getMatches($envelope, $passwords);
if (!$matches) {
return false;
}
if ($this->shouldUpgradeHashers()) {
$this->upgradeHashers($envelope, $matches);
}
return true;
}
public function isUniquePassword(PhutilOpaqueEnvelope $envelope) {
$this->requireSetup();
$password_type = $this->getPasswordType();
// To test that the password is unique, we're loading all active and
// revoked passwords for all roles for the given user, then throwing out
// the active passwords for the current role (so a password can't
// collide with itself).
// Note that two different objects can have the same password (say,
// users @alice and @bailey). We're only preventing @alice from using
// the same password for everything.
$passwords = $this->newQuery()
->execute();
foreach ($passwords as $key => $password) {
$same_type = ($password->getPasswordType() === $password_type);
$is_active = !$password->getIsRevoked();
if ($same_type && $is_active) {
unset($passwords[$key]);
}
}
$matches = $this->getMatches($envelope, $passwords);
return !$matches;
}
public function isRevokedPassword(PhutilOpaqueEnvelope $envelope) {
$this->requireSetup();
// To test if a password is revoked, we're loading all revoked passwords
// across all roles for the given user. If a password was revoked in one
// role, you can't reuse it in a different role.
$passwords = $this->newQuery()
->withIsRevoked(true)
->execute();
$matches = $this->getMatches($envelope, $passwords);
return (bool)$matches;
}
private function requireSetup() {
if (!$this->getObject()) {
throw new PhutilInvalidStateException('setObject');
}
if (!$this->getPasswordType()) {
throw new PhutilInvalidStateException('setPasswordType');
}
if (!$this->getViewer()) {
throw new PhutilInvalidStateException('setViewer');
}
if ($this->shouldUpgradeHashers()) {
if (!$this->getContentSource()) {
throw new PhutilInvalidStateException('setContentSource');
}
}
}
private function shouldUpgradeHashers() {
if (!$this->getUpgradeHashers()) {
return false;
}
if (PhabricatorEnv::isReadOnly()) {
// Don't try to upgrade hashers if we're in read-only mode, since we
// won't be able to write the new hash to the database.
return false;
}
return true;
}
private function newQuery() {
$viewer = $this->getViewer();
$object = $this->getObject();
$password_type = $this->getPasswordType();
return id(new PhabricatorAuthPasswordQuery())
->setViewer($viewer)
->withObjectPHIDs(array($object->getPHID()));
}
private function getMatches(
PhutilOpaqueEnvelope $envelope,
array $passwords) {
$object = $this->getObject();
$matches = array();
foreach ($passwords as $password) {
try {
$is_match = $password->comparePassword($envelope, $object);
} catch (PhabricatorPasswordHasherUnavailableException $ex) {
$is_match = false;
}
if ($is_match) {
$matches[] = $password;
}
}
return $matches;
}
private function upgradeHashers(
PhutilOpaqueEnvelope $envelope,
array $passwords) {
assert_instances_of($passwords, 'PhabricatorAuthPassword');
$need_upgrade = array();
foreach ($passwords as $password) {
if (!$password->canUpgrade()) {
continue;
}
$need_upgrade[] = $password;
}
if (!$need_upgrade) {
return;
}
$upgrade_type = PhabricatorAuthPasswordUpgradeTransaction::TRANSACTIONTYPE;
$viewer = $this->getViewer();
$content_source = $this->getContentSource();
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
foreach ($need_upgrade as $password) {
// This does the actual upgrade. We then apply a transaction to make
// the upgrade more visible and auditable.
$old_hasher = $password->getHasher();
$password->upgradePasswordHasher($envelope, $this->getObject());
$new_hasher = $password->getHasher();
+ // NOTE: We must save the change before applying transactions because
+ // the editor will reload the object to obtain a read lock.
+ $password->save();
+
$xactions = array();
$xactions[] = $password->getApplicationTransactionTemplate()
->setTransactionType($upgrade_type)
->setNewValue($new_hasher->getHashName());
$editor = $password->getApplicationTransactionEditor()
->setActor($viewer)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->setContentSource($content_source)
->setOldHasher($old_hasher)
->applyTransactions($password, $xactions);
}
unset($unguarded);
}
}
diff --git a/src/applications/auth/provider/PhabricatorAuthProvider.php b/src/applications/auth/provider/PhabricatorAuthProvider.php
index d655efd79..0525edad5 100644
--- a/src/applications/auth/provider/PhabricatorAuthProvider.php
+++ b/src/applications/auth/provider/PhabricatorAuthProvider.php
@@ -1,508 +1,519 @@
<?php
abstract class PhabricatorAuthProvider extends Phobject {
private $providerConfig;
public function attachProviderConfig(PhabricatorAuthProviderConfig $config) {
$this->providerConfig = $config;
return $this;
}
public function hasProviderConfig() {
return (bool)$this->providerConfig;
}
public function getProviderConfig() {
if ($this->providerConfig === null) {
throw new PhutilInvalidStateException('attachProviderConfig');
}
return $this->providerConfig;
}
public function getConfigurationHelp() {
return null;
}
public function getDefaultProviderConfig() {
return id(new PhabricatorAuthProviderConfig())
->setProviderClass(get_class($this))
->setIsEnabled(1)
->setShouldAllowLogin(1)
->setShouldAllowRegistration(1)
->setShouldAllowLink(1)
->setShouldAllowUnlink(1);
}
public function getNameForCreate() {
return $this->getProviderName();
}
public function getDescriptionForCreate() {
return null;
}
public function getProviderKey() {
return $this->getAdapter()->getAdapterKey();
}
public function getProviderType() {
return $this->getAdapter()->getAdapterType();
}
public function getProviderDomain() {
return $this->getAdapter()->getAdapterDomain();
}
public static function getAllBaseProviders() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->execute();
}
public static function getAllProviders() {
static $providers;
if ($providers === null) {
$objects = self::getAllBaseProviders();
$configs = id(new PhabricatorAuthProviderConfigQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->execute();
$providers = array();
foreach ($configs as $config) {
if (!isset($objects[$config->getProviderClass()])) {
// This configuration is for a provider which is not installed.
continue;
}
$object = clone $objects[$config->getProviderClass()];
$object->attachProviderConfig($config);
$key = $object->getProviderKey();
if (isset($providers[$key])) {
throw new Exception(
pht(
"Two authentication providers use the same provider key ".
"('%s'). Each provider must be identified by a unique key.",
$key));
}
$providers[$key] = $object;
}
}
return $providers;
}
public static function getAllEnabledProviders() {
$providers = self::getAllProviders();
foreach ($providers as $key => $provider) {
if (!$provider->isEnabled()) {
unset($providers[$key]);
}
}
return $providers;
}
public static function getEnabledProviderByKey($provider_key) {
return idx(self::getAllEnabledProviders(), $provider_key);
}
abstract public function getProviderName();
abstract public function getAdapter();
public function isEnabled() {
return $this->getProviderConfig()->getIsEnabled();
}
public function shouldAllowLogin() {
return $this->getProviderConfig()->getShouldAllowLogin();
}
public function shouldAllowRegistration() {
if (!$this->shouldAllowLogin()) {
return false;
}
return $this->getProviderConfig()->getShouldAllowRegistration();
}
public function shouldAllowAccountLink() {
return $this->getProviderConfig()->getShouldAllowLink();
}
public function shouldAllowAccountUnlink() {
return $this->getProviderConfig()->getShouldAllowUnlink();
}
public function shouldTrustEmails() {
return $this->shouldAllowEmailTrustConfiguration() &&
$this->getProviderConfig()->getShouldTrustEmails();
}
/**
* Should we allow the adapter to be marked as "trusted". This is true for
* all adapters except those that allow the user to type in emails (see
* @{class:PhabricatorPasswordAuthProvider}).
*/
public function shouldAllowEmailTrustConfiguration() {
return true;
}
public function buildLoginForm(PhabricatorAuthStartController $controller) {
return $this->renderLoginForm($controller->getRequest(), $mode = 'start');
}
public function buildInviteForm(PhabricatorAuthStartController $controller) {
return $this->renderLoginForm($controller->getRequest(), $mode = 'invite');
}
abstract public function processLoginRequest(
PhabricatorAuthLoginController $controller);
public function buildLinkForm(PhabricatorAuthLinkController $controller) {
return $this->renderLoginForm($controller->getRequest(), $mode = 'link');
}
public function shouldAllowAccountRefresh() {
return true;
}
public function buildRefreshForm(
PhabricatorAuthLinkController $controller) {
return $this->renderLoginForm($controller->getRequest(), $mode = 'refresh');
}
protected function renderLoginForm(AphrontRequest $request, $mode) {
throw new PhutilMethodNotImplementedException();
}
public function createProviders() {
return array($this);
}
protected function willSaveAccount(PhabricatorExternalAccount $account) {
return;
}
public function willRegisterAccount(PhabricatorExternalAccount $account) {
return;
}
protected function loadOrCreateAccount($account_id) {
if (!strlen($account_id)) {
throw new Exception(pht('Empty account ID!'));
}
$adapter = $this->getAdapter();
$adapter_class = get_class($adapter);
if (!strlen($adapter->getAdapterType())) {
throw new Exception(
pht(
"AuthAdapter (of class '%s') has an invalid implementation: ".
"no adapter type.",
$adapter_class));
}
if (!strlen($adapter->getAdapterDomain())) {
throw new Exception(
pht(
"AuthAdapter (of class '%s') has an invalid implementation: ".
"no adapter domain.",
$adapter_class));
}
$account = id(new PhabricatorExternalAccount())->loadOneWhere(
'accountType = %s AND accountDomain = %s AND accountID = %s',
$adapter->getAdapterType(),
$adapter->getAdapterDomain(),
$account_id);
if (!$account) {
$account = id(new PhabricatorExternalAccount())
->setAccountType($adapter->getAdapterType())
->setAccountDomain($adapter->getAdapterDomain())
->setAccountID($account_id);
}
$account->setUsername($adapter->getAccountName());
$account->setRealName($adapter->getAccountRealName());
$account->setEmail($adapter->getAccountEmail());
$account->setAccountURI($adapter->getAccountURI());
$account->setProfileImagePHID(null);
$image_uri = $adapter->getAccountImageURI();
if ($image_uri) {
try {
$name = PhabricatorSlug::normalize($this->getProviderName());
$name = $name.'-profile.jpg';
// TODO: If the image has not changed, we do not need to make a new
// file entry for it, but there's no convenient way to do this with
// PhabricatorFile right now. The storage will get shared, so the impact
// here is negligible.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$image_file = PhabricatorFile::newFromFileDownload(
$image_uri,
array(
'name' => $name,
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
));
if ($image_file->isViewableImage()) {
$image_file
->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy())
->setCanCDN(true)
->save();
$account->setProfileImagePHID($image_file->getPHID());
} else {
$image_file->delete();
}
unset($unguarded);
} catch (Exception $ex) {
// Log this but proceed, it's not especially important that we
// be able to pull profile images.
phlog($ex);
}
}
$this->willSaveAccount($account);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$account->save();
unset($unguarded);
return $account;
}
public function getLoginURI() {
$app = PhabricatorApplication::getByClass('PhabricatorAuthApplication');
return $app->getApplicationURI('/login/'.$this->getProviderKey().'/');
}
public function getSettingsURI() {
return '/settings/panel/external/';
}
public function getStartURI() {
$app = PhabricatorApplication::getByClass('PhabricatorAuthApplication');
$uri = $app->getApplicationURI('/start/');
return $uri;
}
public function isDefaultRegistrationProvider() {
return false;
}
public function shouldRequireRegistrationPassword() {
return false;
}
public function getDefaultExternalAccount() {
throw new PhutilMethodNotImplementedException();
}
public function getLoginOrder() {
return '500-'.$this->getProviderName();
}
protected function getLoginIcon() {
return 'Generic';
}
public function isLoginFormAButton() {
return false;
}
public function renderConfigPropertyTransactionTitle(
PhabricatorAuthProviderConfigTransaction $xaction) {
return null;
}
public function readFormValuesFromProvider() {
return array();
}
public function readFormValuesFromRequest(AphrontRequest $request) {
return array();
}
public function processEditForm(
AphrontRequest $request,
array $values) {
$errors = array();
$issues = array();
return array($errors, $issues, $values);
}
public function extendEditForm(
AphrontRequest $request,
AphrontFormView $form,
array $values,
array $issues) {
return;
}
public function willRenderLinkedAccount(
PhabricatorUser $viewer,
PHUIObjectItemView $item,
PhabricatorExternalAccount $account) {
$account_view = id(new PhabricatorAuthAccountView())
->setExternalAccount($account)
->setAuthProvider($this);
$item->appendChild(
phutil_tag(
'div',
array(
'class' => 'mmr mml mst mmb',
),
$account_view));
}
/**
* Return true to use a two-step configuration (setup, configure) instead of
* the default single-step configuration. In practice, this means that
* creating a new provider instance will redirect back to the edit page
* instead of the provider list.
*
* @return bool True if this provider uses two-step configuration.
*/
public function hasSetupStep() {
return false;
}
/**
* Render a standard login/register button element.
*
* The `$attributes` parameter takes these keys:
*
* - `uri`: URI the button should take the user to when clicked.
* - `method`: Optional HTTP method the button should use, defaults to GET.
*
* @param AphrontRequest HTTP request.
* @param string Request mode string.
* @param map Additional parameters, see above.
* @return wild Log in button.
*/
protected function renderStandardLoginButton(
AphrontRequest $request,
$mode,
array $attributes = array()) {
PhutilTypeSpec::checkMap(
$attributes,
array(
'method' => 'optional string',
'uri' => 'string',
'sigil' => 'optional string',
));
$viewer = $request->getUser();
$adapter = $this->getAdapter();
if ($mode == 'link') {
$button_text = pht('Link External Account');
} else if ($mode == 'refresh') {
$button_text = pht('Refresh Account Link');
} else if ($mode == 'invite') {
$button_text = pht('Register Account');
} else if ($this->shouldAllowRegistration()) {
$button_text = pht('Log In or Register');
} else {
$button_text = pht('Log In');
}
$icon = id(new PHUIIconView())
->setSpriteSheet(PHUIIconView::SPRITE_LOGIN)
->setSpriteIcon($this->getLoginIcon());
$button = id(new PHUIButtonView())
->setSize(PHUIButtonView::BIG)
->setColor(PHUIButtonView::GREY)
->setIcon($icon)
->setText($button_text)
->setSubtext($this->getProviderName());
$uri = $attributes['uri'];
$uri = new PhutilURI($uri);
$params = $uri->getQueryParams();
$uri->setQueryParams(array());
$content = array($button);
foreach ($params as $key => $value) {
$content[] = phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => $key,
'value' => $value,
));
}
+ $static_response = CelerityAPI::getStaticResourceResponse();
+ $static_response->addContentSecurityPolicyURI('form-action', (string)$uri);
+
+ foreach ($this->getContentSecurityPolicyFormActions() as $csp_uri) {
+ $static_response->addContentSecurityPolicyURI('form-action', $csp_uri);
+ }
+
return phabricator_form(
$viewer,
array(
'method' => idx($attributes, 'method', 'GET'),
'action' => (string)$uri,
'sigil' => idx($attributes, 'sigil'),
),
$content);
}
public function renderConfigurationFooter() {
return null;
}
public function getAuthCSRFCode(AphrontRequest $request) {
$phcid = $request->getCookie(PhabricatorCookies::COOKIE_CLIENTID);
if (!strlen($phcid)) {
throw new AphrontMalformedRequestException(
pht('Missing Client ID Cookie'),
pht(
'Your browser did not submit a "%s" cookie with client state '.
'information in the request. Check that cookies are enabled. '.
'If this problem persists, you may need to clear your cookies.',
PhabricatorCookies::COOKIE_CLIENTID),
true);
}
return PhabricatorHash::weakDigest($phcid);
}
protected function verifyAuthCSRFCode(AphrontRequest $request, $actual) {
$expect = $this->getAuthCSRFCode($request);
if (!strlen($actual)) {
throw new Exception(
pht(
'The authentication provider did not return a client state '.
'parameter in its response, but one was expected. If this '.
'problem persists, you may need to clear your cookies.'));
}
if (!phutil_hashes_are_identical($actual, $expect)) {
throw new Exception(
pht(
'The authentication provider did not return the correct client '.
'state parameter in its response. If this problem persists, you may '.
'need to clear your cookies.'));
}
}
public function supportsAutoLogin() {
return false;
}
public function getAutoLoginURI(AphrontRequest $request) {
throw new PhutilMethodNotImplementedException();
}
+ protected function getContentSecurityPolicyFormActions() {
+ return array();
+ }
+
}
diff --git a/src/applications/auth/provider/PhabricatorOAuth1AuthProvider.php b/src/applications/auth/provider/PhabricatorOAuth1AuthProvider.php
index c62983de2..2fbc63746 100644
--- a/src/applications/auth/provider/PhabricatorOAuth1AuthProvider.php
+++ b/src/applications/auth/provider/PhabricatorOAuth1AuthProvider.php
@@ -1,280 +1,283 @@
<?php
abstract class PhabricatorOAuth1AuthProvider
extends PhabricatorOAuthAuthProvider {
protected $adapter;
const PROPERTY_CONSUMER_KEY = 'oauth1:consumer:key';
const PROPERTY_CONSUMER_SECRET = 'oauth1:consumer:secret';
const PROPERTY_PRIVATE_KEY = 'oauth1:private:key';
protected function getIDKey() {
return self::PROPERTY_CONSUMER_KEY;
}
protected function getSecretKey() {
return self::PROPERTY_CONSUMER_SECRET;
}
protected function configureAdapter(PhutilOAuth1AuthAdapter $adapter) {
$config = $this->getProviderConfig();
$adapter->setConsumerKey($config->getProperty(self::PROPERTY_CONSUMER_KEY));
$secret = $config->getProperty(self::PROPERTY_CONSUMER_SECRET);
if (strlen($secret)) {
$adapter->setConsumerSecret(new PhutilOpaqueEnvelope($secret));
}
$adapter->setCallbackURI(PhabricatorEnv::getURI($this->getLoginURI()));
return $adapter;
}
protected function renderLoginForm(AphrontRequest $request, $mode) {
$attributes = array(
'method' => 'POST',
'uri' => $this->getLoginURI(),
);
return $this->renderStandardLoginButton($request, $mode, $attributes);
}
public function processLoginRequest(
PhabricatorAuthLoginController $controller) {
$request = $controller->getRequest();
$adapter = $this->getAdapter();
$account = null;
$response = null;
if ($request->isHTTPPost()) {
// Add a CSRF code to the callback URI, which we'll verify when
// performing the login.
$client_code = $this->getAuthCSRFCode($request);
$callback_uri = $adapter->getCallbackURI();
$callback_uri = $callback_uri.$client_code.'/';
$adapter->setCallbackURI($callback_uri);
$uri = $adapter->getClientRedirectURI();
$this->saveHandshakeTokenSecret(
$client_code,
$adapter->getTokenSecret());
$response = id(new AphrontRedirectResponse())
->setIsExternal(true)
->setURI($uri);
return array($account, $response);
}
$denied = $request->getStr('denied');
if (strlen($denied)) {
// Twitter indicates that the user cancelled the login attempt by
// returning "denied" as a parameter.
throw new PhutilAuthUserAbortedException();
}
// NOTE: You can get here via GET, this should probably be a bit more
// user friendly.
$this->verifyAuthCSRFCode($request, $controller->getExtraURIData());
$token = $request->getStr('oauth_token');
$verifier = $request->getStr('oauth_verifier');
if (!$token) {
throw new Exception(pht("Expected '%s' in request!", 'oauth_token'));
}
if (!$verifier) {
throw new Exception(pht("Expected '%s' in request!", 'oauth_verifier'));
}
$adapter->setToken($token);
$adapter->setVerifier($verifier);
$client_code = $this->getAuthCSRFCode($request);
$token_secret = $this->loadHandshakeTokenSecret($client_code);
$adapter->setTokenSecret($token_secret);
// NOTE: As a side effect, this will cause the OAuth adapter to request
// an access token.
try {
$account_id = $adapter->getAccountID();
} catch (Exception $ex) {
// TODO: Handle this in a more user-friendly way.
throw $ex;
}
if (!strlen($account_id)) {
$response = $controller->buildProviderErrorResponse(
$this,
pht(
'The OAuth provider failed to retrieve an account ID.'));
return array($account, $response);
}
return array($this->loadOrCreateAccount($account_id), $response);
}
public function processEditForm(
AphrontRequest $request,
array $values) {
$key_ckey = self::PROPERTY_CONSUMER_KEY;
$key_csecret = self::PROPERTY_CONSUMER_SECRET;
return $this->processOAuthEditForm(
$request,
$values,
pht('Consumer key is required.'),
pht('Consumer secret is required.'));
}
public function extendEditForm(
AphrontRequest $request,
AphrontFormView $form,
array $values,
array $issues) {
return $this->extendOAuthEditForm(
$request,
$form,
$values,
$issues,
pht('OAuth Consumer Key'),
pht('OAuth Consumer Secret'));
}
public function renderConfigPropertyTransactionTitle(
PhabricatorAuthProviderConfigTransaction $xaction) {
$author_phid = $xaction->getAuthorPHID();
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
$key = $xaction->getMetadataValue(
PhabricatorAuthProviderConfigTransaction::PROPERTY_KEY);
switch ($key) {
case self::PROPERTY_CONSUMER_KEY:
if (strlen($old)) {
return pht(
'%s updated the OAuth consumer key for this provider from '.
'"%s" to "%s".',
$xaction->renderHandleLink($author_phid),
$old,
$new);
} else {
return pht(
'%s set the OAuth consumer key for this provider to '.
'"%s".',
$xaction->renderHandleLink($author_phid),
$new);
}
case self::PROPERTY_CONSUMER_SECRET:
if (strlen($old)) {
return pht(
'%s updated the OAuth consumer secret for this provider.',
$xaction->renderHandleLink($author_phid));
} else {
return pht(
'%s set the OAuth consumer secret for this provider.',
$xaction->renderHandleLink($author_phid));
}
}
return parent::renderConfigPropertyTransactionTitle($xaction);
}
protected function synchronizeOAuthAccount(
PhabricatorExternalAccount $account) {
$adapter = $this->getAdapter();
$oauth_token = $adapter->getToken();
$oauth_token_secret = $adapter->getTokenSecret();
$account->setProperty('oauth1.token', $oauth_token);
$account->setProperty('oauth1.token.secret', $oauth_token_secret);
}
public function willRenderLinkedAccount(
PhabricatorUser $viewer,
PHUIObjectItemView $item,
PhabricatorExternalAccount $account) {
$item->addAttribute(pht('OAuth1 Account'));
parent::willRenderLinkedAccount($viewer, $item, $account);
}
+ protected function getContentSecurityPolicyFormActions() {
+ return $this->getAdapter()->getContentSecurityPolicyFormActions();
+ }
/* -( Temporary Secrets )-------------------------------------------------- */
private function saveHandshakeTokenSecret($client_code, $secret) {
$secret_type = PhabricatorOAuth1SecretTemporaryTokenType::TOKENTYPE;
$key = $this->getHandshakeTokenKeyFromClientCode($client_code);
$type = $this->getTemporaryTokenType($secret_type);
// Wipe out an existing token, if one exists.
$token = id(new PhabricatorAuthTemporaryTokenQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withTokenResources(array($key))
->withTokenTypes(array($type))
->executeOne();
if ($token) {
$token->delete();
}
// Save the new secret.
id(new PhabricatorAuthTemporaryToken())
->setTokenResource($key)
->setTokenType($type)
->setTokenExpires(time() + phutil_units('1 hour in seconds'))
->setTokenCode($secret)
->save();
}
private function loadHandshakeTokenSecret($client_code) {
$secret_type = PhabricatorOAuth1SecretTemporaryTokenType::TOKENTYPE;
$key = $this->getHandshakeTokenKeyFromClientCode($client_code);
$type = $this->getTemporaryTokenType($secret_type);
$token = id(new PhabricatorAuthTemporaryTokenQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withTokenResources(array($key))
->withTokenTypes(array($type))
->withExpired(false)
->executeOne();
if (!$token) {
throw new Exception(
pht(
'Unable to load your OAuth1 token secret from storage. It may '.
'have expired. Try authenticating again.'));
}
return $token->getTokenCode();
}
private function getTemporaryTokenType($core_type) {
// Namespace the type so that multiple providers don't step on each
// others' toes if a user starts Mediawiki and Bitbucket auth at the
// same time.
// TODO: This isn't really a proper use of the table and should get
// cleaned up some day: the type should be constant.
return $core_type.':'.$this->getProviderConfig()->getID();
}
private function getHandshakeTokenKeyFromClientCode($client_code) {
// NOTE: This is very slightly coercive since the TemporaryToken table
// expects an "objectPHID" as an identifier, but nothing about the storage
// is bound to PHIDs.
return 'oauth1:secret/'.$client_code;
}
}
diff --git a/src/applications/auth/view/PhabricatorAuthAccountView.php b/src/applications/auth/view/PhabricatorAuthAccountView.php
index 8eb73144a..3f04281c8 100644
--- a/src/applications/auth/view/PhabricatorAuthAccountView.php
+++ b/src/applications/auth/view/PhabricatorAuthAccountView.php
@@ -1,116 +1,117 @@
<?php
final class PhabricatorAuthAccountView extends AphrontView {
private $externalAccount;
private $provider;
public function setExternalAccount(
PhabricatorExternalAccount $external_account) {
$this->externalAccount = $external_account;
return $this;
}
public function setAuthProvider(PhabricatorAuthProvider $provider) {
$this->provider = $provider;
return $this;
}
public function render() {
$account = $this->externalAccount;
$provider = $this->provider;
require_celerity_resource('auth-css');
$content = array();
$dispname = $account->getDisplayName();
$username = $account->getUsername();
$realname = $account->getRealName();
$use_name = null;
if (strlen($dispname)) {
$use_name = $dispname;
} else if (strlen($username) && strlen($realname)) {
$use_name = $username.' ('.$realname.')';
} else if (strlen($username)) {
$use_name = $username;
} else if (strlen($realname)) {
$use_name = $realname;
} else {
$use_name = $account->getAccountID();
}
$content[] = phutil_tag(
'div',
array(
'class' => 'auth-account-view-name',
),
$use_name);
if ($provider) {
$prov_name = pht('%s Account', $provider->getProviderName());
} else {
$prov_name = pht('"%s" Account', $account->getProviderType());
}
$content[] = phutil_tag(
'div',
array(
'class' => 'auth-account-view-provider-name',
),
array(
$prov_name,
" \xC2\xB7 ",
$account->getAccountID(),
));
$account_uri = $account->getAccountURI();
if (strlen($account_uri)) {
// Make sure we don't link a "javascript:" URI if a user somehow
// managed to get one here.
if (PhabricatorEnv::isValidRemoteURIForLink($account_uri)) {
$account_uri = phutil_tag(
'a',
array(
'href' => $account_uri,
'target' => '_blank',
+ 'rel' => 'noreferrer',
),
$account_uri);
}
$content[] = phutil_tag(
'div',
array(
'class' => 'auth-account-view-account-uri',
),
$account_uri);
}
$image_file = $account->getProfileImageFile();
$xform = PhabricatorFileTransform::getTransformByKey(
PhabricatorFileThumbnailTransform::TRANSFORM_PROFILE);
$image_uri = $image_file->getURIForTransform($xform);
list($x, $y) = $xform->getTransformedDimensions($image_file);
$profile_image = phutil_tag(
'div',
array(
'class' => 'auth-account-view-profile-image',
'style' => 'background-image: url('.$image_uri.');',
));
return phutil_tag(
'div',
array(
'class' => 'auth-account-view',
),
array(
$profile_image,
$content,
));
}
}
diff --git a/src/applications/badges/editor/PhabricatorBadgesEditor.php b/src/applications/badges/editor/PhabricatorBadgesEditor.php
index fddc55747..785d8c989 100644
--- a/src/applications/badges/editor/PhabricatorBadgesEditor.php
+++ b/src/applications/badges/editor/PhabricatorBadgesEditor.php
@@ -1,162 +1,160 @@
<?php
final class PhabricatorBadgesEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorBadgesApplication';
}
public function getEditorObjectsDescription() {
return pht('Badges');
}
public function getCreateObjectTitle($author, $object) {
return pht('%s created this badge.', $author);
}
public function getCreateObjectTitleForFeed($author, $object) {
return pht('%s created %s.', $author, $object);
}
protected function supportsSearch() {
return true;
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorTransactions::TYPE_EDGE;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
return $types;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
public function getMailTagsMap() {
return array(
PhabricatorBadgesTransaction::MAILTAG_DETAILS =>
pht('Someone changes the badge\'s details.'),
PhabricatorBadgesTransaction::MAILTAG_COMMENT =>
pht('Someone comments on a badge.'),
PhabricatorBadgesTransaction::MAILTAG_OTHER =>
pht('Other badge activity not listed above occurs.'),
);
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function expandTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$actor = $this->getActor();
$actor_phid = $actor->getPHID();
$results = parent::expandTransactions($object, $xactions);
// Automatically subscribe the author when they create a badge.
if ($this->getIsNewObject()) {
if ($actor_phid) {
$results[] = id(new PhabricatorBadgesTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
->setNewValue(
array(
'+' => array($actor_phid => $actor_phid),
));
}
}
return $results;
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new PhabricatorBadgesReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$name = $object->getName();
$id = $object->getID();
- $topic = pht('Badge %d', $id);
$subject = pht('Badge %d: %s', $id, $name);
return id(new PhabricatorMetaMTAMail())
- ->setSubject($subject)
- ->addHeader('Thread-Topic', $topic);
+ ->setSubject($subject);
}
protected function getMailTo(PhabricatorLiskDAO $object) {
return array(
$object->getCreatorPHID(),
$this->requireActor()->getPHID(),
);
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
$body->addLinkSection(
pht('BADGE DETAIL'),
PhabricatorEnv::getProductionURI('/badges/view/'.$object->getID().'/'));
return $body;
}
protected function getMailSubjectPrefix() {
return pht('[Badge]');
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
$badge_phid = $object->getPHID();
$user_phids = array();
$clear_everything = false;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorBadgesBadgeAwardTransaction::TRANSACTIONTYPE:
case PhabricatorBadgesBadgeRevokeTransaction::TRANSACTIONTYPE:
foreach ($xaction->getNewValue() as $user_phid) {
$user_phids[] = $user_phid;
}
break;
default:
$clear_everything = true;
break;
}
}
if ($clear_everything) {
$awards = id(new PhabricatorBadgesAwardQuery())
->setViewer($this->getActor())
->withBadgePHIDs(array($badge_phid))
->execute();
foreach ($awards as $award) {
$user_phids[] = $award->getRecipientPHID();
}
}
if ($user_phids) {
PhabricatorUserCache::clearCaches(
PhabricatorUserBadgesCacheType::KEY_BADGES,
$user_phids);
}
return $xactions;
}
}
diff --git a/src/applications/base/controller/PhabricatorController.php b/src/applications/base/controller/PhabricatorController.php
index 5952a806e..df0c94c13 100644
--- a/src/applications/base/controller/PhabricatorController.php
+++ b/src/applications/base/controller/PhabricatorController.php
@@ -1,631 +1,632 @@
<?php
abstract class PhabricatorController extends AphrontController {
private $handles;
public function shouldRequireLogin() {
return true;
}
public function shouldRequireAdmin() {
return false;
}
public function shouldRequireEnabledUser() {
return true;
}
public function shouldAllowPublic() {
return false;
}
public function shouldAllowPartialSessions() {
return false;
}
public function shouldRequireEmailVerification() {
return PhabricatorUserEmail::isEmailVerificationRequired();
}
public function shouldAllowRestrictedParameter($parameter_name) {
return false;
}
public function shouldRequireMultiFactorEnrollment() {
if (!$this->shouldRequireLogin()) {
return false;
}
if (!$this->shouldRequireEnabledUser()) {
return false;
}
if ($this->shouldAllowPartialSessions()) {
return false;
}
$user = $this->getRequest()->getUser();
if (!$user->getIsStandardUser()) {
return false;
}
return PhabricatorEnv::getEnvConfig('security.require-multi-factor-auth');
}
public function shouldAllowLegallyNonCompliantUsers() {
return false;
}
public function isGlobalDragAndDropUploadEnabled() {
return false;
}
public function willBeginExecution() {
$request = $this->getRequest();
if ($request->getUser()) {
// NOTE: Unit tests can set a user explicitly. Normal requests are not
// permitted to do this.
PhabricatorTestCase::assertExecutingUnitTests();
$user = $request->getUser();
} else {
$user = new PhabricatorUser();
$session_engine = new PhabricatorAuthSessionEngine();
$phsid = $request->getCookie(PhabricatorCookies::COOKIE_SESSION);
if (strlen($phsid)) {
$session_user = $session_engine->loadUserForSession(
PhabricatorAuthSession::TYPE_WEB,
$phsid);
if ($session_user) {
$user = $session_user;
}
} else {
// If the client doesn't have a session token, generate an anonymous
// session. This is used to provide CSRF protection to logged-out users.
$phsid = $session_engine->establishSession(
PhabricatorAuthSession::TYPE_WEB,
null,
$partial = false);
// This may be a resource request, in which case we just don't set
// the cookie.
if ($request->canSetCookies()) {
$request->setCookie(PhabricatorCookies::COOKIE_SESSION, $phsid);
}
}
if (!$user->isLoggedIn()) {
$user->attachAlternateCSRFString(PhabricatorHash::weakDigest($phsid));
}
$request->setUser($user);
}
id(new PhabricatorAuthSessionEngine())
->willServeRequestForUser($user);
if (PhabricatorEnv::getEnvConfig('darkconsole.enabled')) {
$dark_console = PhabricatorDarkConsoleSetting::SETTINGKEY;
if ($user->getUserSetting($dark_console) ||
PhabricatorEnv::getEnvConfig('darkconsole.always-on')) {
$console = new DarkConsoleCore();
$request->getApplicationConfiguration()->setConsole($console);
}
}
// NOTE: We want to set up the user first so we can render a real page
// here, but fire this before any real logic.
$restricted = array(
'code',
);
foreach ($restricted as $parameter) {
if ($request->getExists($parameter)) {
if (!$this->shouldAllowRestrictedParameter($parameter)) {
throw new Exception(
pht(
'Request includes restricted parameter "%s", but this '.
'controller ("%s") does not whitelist it. Refusing to '.
'serve this request because it might be part of a redirection '.
'attack.',
$parameter,
get_class($this)));
}
}
}
if ($this->shouldRequireEnabledUser()) {
if ($user->getIsDisabled()) {
$controller = new PhabricatorDisabledUserController();
return $this->delegateToController($controller);
}
}
$auth_class = 'PhabricatorAuthApplication';
$auth_application = PhabricatorApplication::getByClass($auth_class);
// Require partial sessions to finish login before doing anything.
if (!$this->shouldAllowPartialSessions()) {
if ($user->hasSession() &&
$user->getSession()->getIsPartial()) {
$login_controller = new PhabricatorAuthFinishController();
$this->setCurrentApplication($auth_application);
return $this->delegateToController($login_controller);
}
}
// Require users sign Legalpad documents before we check if they have
// MFA. If we don't do this, they can get stuck in a state where they
// can't add MFA until they sign, and can't sign until they add MFA.
// See T13024 and PHI223.
$result = $this->requireLegalpadSignatures();
if ($result !== null) {
return $result;
}
// Check if the user needs to configure MFA.
$need_mfa = $this->shouldRequireMultiFactorEnrollment();
$have_mfa = $user->getIsEnrolledInMultiFactor();
if ($need_mfa && !$have_mfa) {
// Check if the cache is just out of date. Otherwise, roadblock the user
// and require MFA enrollment.
$user->updateMultiFactorEnrollment();
if (!$user->getIsEnrolledInMultiFactor()) {
$mfa_controller = new PhabricatorAuthNeedsMultiFactorController();
$this->setCurrentApplication($auth_application);
return $this->delegateToController($mfa_controller);
}
}
if ($this->shouldRequireLogin()) {
// This actually means we need either:
// - a valid user, or a public controller; and
// - permission to see the application; and
// - permission to see at least one Space if spaces are configured.
$allow_public = $this->shouldAllowPublic() &&
PhabricatorEnv::getEnvConfig('policy.allow-public');
// If this controller isn't public, and the user isn't logged in, require
// login.
if (!$allow_public && !$user->isLoggedIn()) {
$login_controller = new PhabricatorAuthStartController();
$this->setCurrentApplication($auth_application);
return $this->delegateToController($login_controller);
}
if ($user->isLoggedIn()) {
if ($this->shouldRequireEmailVerification()) {
if (!$user->getIsEmailVerified()) {
$controller = new PhabricatorMustVerifyEmailController();
$this->setCurrentApplication($auth_application);
return $this->delegateToController($controller);
}
}
}
// If Spaces are configured, require that the user have access to at
// least one. If we don't do this, they'll get confusing error messages
// later on.
$spaces = PhabricatorSpacesNamespaceQuery::getSpacesExist();
if ($spaces) {
$viewer_spaces = PhabricatorSpacesNamespaceQuery::getViewerSpaces(
$user);
if (!$viewer_spaces) {
$controller = new PhabricatorSpacesNoAccessController();
return $this->delegateToController($controller);
}
}
// If the user doesn't have access to the application, don't let them use
// any of its controllers. We query the application in order to generate
// a policy exception if the viewer doesn't have permission.
$application = $this->getCurrentApplication();
if ($application) {
id(new PhabricatorApplicationQuery())
->setViewer($user)
->withPHIDs(array($application->getPHID()))
->executeOne();
}
// If users need approval, require they wait here. We do this near the
// end so they can take other actions (like verifying email, signing
// documents, and enrolling in MFA) while waiting for an admin to take a
// look at things. See T13024 for more discussion.
if ($this->shouldRequireEnabledUser()) {
if ($user->isLoggedIn() && !$user->getIsApproved()) {
$controller = new PhabricatorAuthNeedsApprovalController();
return $this->delegateToController($controller);
}
}
}
// NOTE: We do this last so that users get a login page instead of a 403
// if they need to login.
if ($this->shouldRequireAdmin() && !$user->getIsAdmin()) {
return new Aphront403Response();
}
}
public function getApplicationURI($path = '') {
if (!$this->getCurrentApplication()) {
throw new Exception(pht('No application!'));
}
return $this->getCurrentApplication()->getApplicationURI($path);
}
public function willSendResponse(AphrontResponse $response) {
$request = $this->getRequest();
if ($response instanceof AphrontDialogResponse) {
if (!$request->isAjax() && !$request->isQuicksand()) {
$dialog = $response->getDialog();
$title = $dialog->getTitle();
$short = $dialog->getShortTitle();
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(coalesce($short, $title));
$page_content = array(
$crumbs,
$response->buildResponseString(),
);
$view = id(new PhabricatorStandardPageView())
->setRequest($request)
->setController($this)
->setDeviceReady(true)
->setTitle($title)
->appendChild($page_content);
$response = id(new AphrontWebpageResponse())
->setContent($view->render())
->setHTTPResponseCode($response->getHTTPResponseCode());
} else {
$response->getDialog()->setIsStandalone(true);
return id(new AphrontAjaxResponse())
->setContent(array(
'dialog' => $response->buildResponseString(),
));
}
} else if ($response instanceof AphrontRedirectResponse) {
if ($request->isAjax() || $request->isQuicksand()) {
return id(new AphrontAjaxResponse())
->setContent(
array(
'redirect' => $response->getURI(),
+ 'close' => $response->getCloseDialogBeforeRedirect(),
));
}
}
return $response;
}
/**
* WARNING: Do not call this in new code.
*
* @deprecated See "Handles Technical Documentation".
*/
protected function loadViewerHandles(array $phids) {
return id(new PhabricatorHandleQuery())
->setViewer($this->getRequest()->getUser())
->withPHIDs($phids)
->execute();
}
public function buildApplicationMenu() {
return null;
}
protected function buildApplicationCrumbs() {
$crumbs = array();
$application = $this->getCurrentApplication();
if ($application) {
$icon = $application->getIcon();
if (!$icon) {
$icon = 'fa-puzzle';
}
$crumbs[] = id(new PHUICrumbView())
->setHref($this->getApplicationURI())
->setName($application->getName())
->setIcon($icon);
}
$view = new PHUICrumbsView();
foreach ($crumbs as $crumb) {
$view->addCrumb($crumb);
}
return $view;
}
protected function hasApplicationCapability($capability) {
return PhabricatorPolicyFilter::hasCapability(
$this->getRequest()->getUser(),
$this->getCurrentApplication(),
$capability);
}
protected function requireApplicationCapability($capability) {
PhabricatorPolicyFilter::requireCapability(
$this->getRequest()->getUser(),
$this->getCurrentApplication(),
$capability);
}
protected function explainApplicationCapability(
$capability,
$positive_message,
$negative_message) {
$can_act = $this->hasApplicationCapability($capability);
if ($can_act) {
$message = $positive_message;
$icon_name = 'fa-play-circle-o lightgreytext';
} else {
$message = $negative_message;
$icon_name = 'fa-lock';
}
$icon = id(new PHUIIconView())
->setIcon($icon_name);
require_celerity_resource('policy-css');
$phid = $this->getCurrentApplication()->getPHID();
$explain_uri = "/policy/explain/{$phid}/{$capability}/";
$message = phutil_tag(
'div',
array(
'class' => 'policy-capability-explanation',
),
array(
$icon,
javelin_tag(
'a',
array(
'href' => $explain_uri,
'sigil' => 'workflow',
),
$message),
));
return array($can_act, $message);
}
public function getDefaultResourceSource() {
return 'phabricator';
}
/**
* Create a new @{class:AphrontDialogView} with defaults filled in.
*
* @return AphrontDialogView New dialog.
*/
public function newDialog() {
$submit_uri = new PhutilURI($this->getRequest()->getRequestURI());
$submit_uri = $submit_uri->getPath();
return id(new AphrontDialogView())
->setUser($this->getRequest()->getUser())
->setSubmitURI($submit_uri);
}
public function newPage() {
$page = id(new PhabricatorStandardPageView())
->setRequest($this->getRequest())
->setController($this)
->setDeviceReady(true);
$application = $this->getCurrentApplication();
if ($application) {
$page->setApplicationName($application->getName());
if ($application->getTitleGlyph()) {
$page->setGlyph($application->getTitleGlyph());
}
}
$viewer = $this->getRequest()->getUser();
if ($viewer) {
$page->setUser($viewer);
}
return $page;
}
public function newApplicationMenu() {
return id(new PHUIApplicationMenuView())
->setViewer($this->getViewer());
}
public function newCurtainView($object = null) {
$viewer = $this->getViewer();
$action_id = celerity_generate_unique_node_id();
$action_list = id(new PhabricatorActionListView())
->setViewer($viewer)
->setID($action_id);
// NOTE: Applications (objects of class PhabricatorApplication) can't
// currently be set here, although they don't need any of the extensions
// anyway. This should probably work differently than it does, though.
if ($object) {
if ($object instanceof PhabricatorLiskDAO) {
$action_list->setObject($object);
}
}
$curtain = id(new PHUICurtainView())
->setViewer($viewer)
->setActionList($action_list);
if ($object) {
$panels = PHUICurtainExtension::buildExtensionPanels($viewer, $object);
foreach ($panels as $panel) {
$curtain->addPanel($panel);
}
}
return $curtain;
}
protected function buildTransactionTimeline(
PhabricatorApplicationTransactionInterface $object,
PhabricatorApplicationTransactionQuery $query,
PhabricatorMarkupEngine $engine = null,
$render_data = array()) {
$viewer = $this->getRequest()->getUser();
$xaction = $object->getApplicationTransactionTemplate();
$view = $xaction->getApplicationTransactionViewObject();
$pager = id(new AphrontCursorPagerView())
->readFromRequest($this->getRequest())
->setURI(new PhutilURI(
'/transactions/showolder/'.$object->getPHID().'/'));
$xactions = $query
->setViewer($viewer)
->withObjectPHIDs(array($object->getPHID()))
->needComments(true)
->executeWithCursorPager($pager);
$xactions = array_reverse($xactions);
if ($engine) {
foreach ($xactions as $xaction) {
if ($xaction->getComment()) {
$engine->addObject(
$xaction->getComment(),
PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT);
}
}
$engine->process();
$view->setMarkupEngine($engine);
}
$timeline = $view
->setUser($viewer)
->setObjectPHID($object->getPHID())
->setTransactions($xactions)
->setPager($pager)
->setRenderData($render_data)
->setQuoteTargetID($this->getRequest()->getStr('quoteTargetID'))
->setQuoteRef($this->getRequest()->getStr('quoteRef'));
$object->willRenderTimeline($timeline, $this->getRequest());
return $timeline;
}
public function buildApplicationCrumbsForEditEngine() {
// TODO: This is kind of gross, I'm basically just making this public so
// I can use it in EditEngine. We could do this without making it public
// by using controller delegation, or make it properly public.
return $this->buildApplicationCrumbs();
}
private function requireLegalpadSignatures() {
if (!$this->shouldRequireLogin()) {
return null;
}
if ($this->shouldAllowLegallyNonCompliantUsers()) {
return null;
}
$viewer = $this->getViewer();
if (!$viewer->hasSession()) {
return null;
}
$session = $viewer->getSession();
if ($session->getIsPartial()) {
// If the user hasn't made it through MFA yet, require they survive
// MFA first.
return null;
}
if ($session->getSignedLegalpadDocuments()) {
return null;
}
if (!$viewer->isLoggedIn()) {
return null;
}
$must_sign_docs = array();
$sign_docs = array();
$legalpad_class = 'PhabricatorLegalpadApplication';
$legalpad_installed = PhabricatorApplication::isClassInstalledForViewer(
$legalpad_class,
$viewer);
if ($legalpad_installed) {
$sign_docs = id(new LegalpadDocumentQuery())
->setViewer($viewer)
->withSignatureRequired(1)
->needViewerSignatures(true)
->setOrder('oldest')
->execute();
foreach ($sign_docs as $sign_doc) {
if (!$sign_doc->getUserSignature($viewer->getPHID())) {
$must_sign_docs[] = $sign_doc;
}
}
}
if (!$must_sign_docs) {
// If nothing needs to be signed (either because there are no documents
// which require a signature, or because the user has already signed
// all of them) mark the session as good and continue.
$engine = id(new PhabricatorAuthSessionEngine())
->signLegalpadDocuments($viewer, $sign_docs);
return null;
}
$request = $this->getRequest();
$request->setURIMap(
array(
'id' => head($must_sign_docs)->getID(),
));
$application = PhabricatorApplication::getByClass($legalpad_class);
$this->setCurrentApplication($application);
$controller = new LegalpadDocumentSignController();
return $this->delegateToController($controller);
}
/* -( Deprecated )--------------------------------------------------------- */
/**
* DEPRECATED. Use @{method:newPage}.
*/
public function buildStandardPageView() {
return $this->newPage();
}
/**
* DEPRECATED. Use @{method:newPage}.
*/
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->appendChild($view);
return $page->produceAphrontResponse();
}
}
diff --git a/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php b/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php
index 4ab13fd36..f1b72dc0e 100644
--- a/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php
+++ b/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php
@@ -1,377 +1,375 @@
<?php
final class PhabricatorCalendarEventEditor
extends PhabricatorApplicationTransactionEditor {
private $oldIsAllDay;
private $newIsAllDay;
public function getEditorApplicationClass() {
return 'PhabricatorCalendarApplication';
}
public function getEditorObjectsDescription() {
return pht('Calendar');
}
public function getCreateObjectTitle($author, $object) {
return pht('%s created this event.', $author);
}
public function getCreateObjectTitleForFeed($author, $object) {
return pht('%s created %s.', $author, $object);
}
protected function shouldApplyInitialEffects(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
public function getOldIsAllDay() {
return $this->oldIsAllDay;
}
public function getNewIsAllDay() {
return $this->newIsAllDay;
}
protected function applyInitialEffects(
PhabricatorLiskDAO $object,
array $xactions) {
$actor = $this->requireActor();
if ($object->getIsStub()) {
$this->materializeStub($object);
}
// Before doing anything, figure out if the event will be an all day event
// or not after the edit. This affects how we store datetime values, and
// whether we render times or not.
$old_allday = $object->getIsAllDay();
$new_allday = $old_allday;
$type_allday = PhabricatorCalendarEventAllDayTransaction::TRANSACTIONTYPE;
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() != $type_allday) {
continue;
}
$new_allday = (bool)$xaction->getNewValue();
}
$this->oldIsAllDay = $old_allday;
$this->newIsAllDay = $new_allday;
}
private function materializeStub(PhabricatorCalendarEvent $event) {
if (!$event->getIsStub()) {
throw new Exception(
pht('Can not materialize an event stub: this event is not a stub.'));
}
$actor = $this->getActor();
$invitees = $event->getInvitees();
$event->copyFromParent($actor);
$event->setIsStub(0);
$event->openTransaction();
$event->save();
foreach ($invitees as $invitee) {
$invitee
->setEventPHID($event->getPHID())
->save();
}
$event->saveTransaction();
$event->attachInvitees($invitees);
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
return $types;
}
protected function adjustObjectForPolicyChecks(
PhabricatorLiskDAO $object,
array $xactions) {
$copy = parent::adjustObjectForPolicyChecks($object, $xactions);
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorCalendarEventHostTransaction::TRANSACTIONTYPE:
$copy->setHostPHID($xaction->getNewValue());
break;
case PhabricatorCalendarEventInviteTransaction::TRANSACTIONTYPE:
PhabricatorPolicyRule::passTransactionHintToRule(
$copy,
new PhabricatorCalendarEventInviteesPolicyRule(),
array_fuse($xaction->getNewValue()));
break;
}
}
return $copy;
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
// Clear the availability caches for users whose availability is affected
// by this edit.
$phids = mpull($object->getInvitees(), 'getInviteePHID');
$phids = array_fuse($phids);
$invalidate_all = false;
$invalidate_phids = array();
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorCalendarEventUntilDateTransaction::TRANSACTIONTYPE:
case PhabricatorCalendarEventStartDateTransaction::TRANSACTIONTYPE:
case PhabricatorCalendarEventEndDateTransaction::TRANSACTIONTYPE:
case PhabricatorCalendarEventCancelTransaction::TRANSACTIONTYPE:
case PhabricatorCalendarEventAllDayTransaction::TRANSACTIONTYPE:
// For these kinds of changes, we need to invalidate the availabilty
// caches for all attendees.
$invalidate_all = true;
break;
case PhabricatorCalendarEventAcceptTransaction::TRANSACTIONTYPE:
case PhabricatorCalendarEventDeclineTransaction::TRANSACTIONTYPE:
$acting_phid = $this->getActingAsPHID();
$invalidate_phids[$acting_phid] = $acting_phid;
break;
case PhabricatorCalendarEventInviteTransaction::TRANSACTIONTYPE:
foreach ($xaction->getOldValue() as $phid) {
// Add the possibly un-invited user to the list of potentially
// affected users if they are't already present.
$phids[$phid] = $phid;
$invalidate_phids[$phid] = $phid;
}
foreach ($xaction->getNewValue() as $phid) {
$invalidate_phids[$phid] = $phid;
}
break;
}
}
if (!$invalidate_all) {
$phids = array_select_keys($phids, $invalidate_phids);
}
if ($phids) {
$object->applyViewerTimezone($this->getActor());
$user = new PhabricatorUser();
$conn_w = $user->establishConnection('w');
queryfx(
$conn_w,
'UPDATE %T SET availabilityCacheTTL = NULL
WHERE phid IN (%Ls)',
$user->getTableName(),
$phids);
}
return $xactions;
}
protected function validateAllTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$start_date_xaction =
PhabricatorCalendarEventStartDateTransaction::TRANSACTIONTYPE;
$end_date_xaction =
PhabricatorCalendarEventEndDateTransaction::TRANSACTIONTYPE;
$is_recurrence_xaction =
PhabricatorCalendarEventRecurringTransaction::TRANSACTIONTYPE;
$recurrence_end_xaction =
PhabricatorCalendarEventUntilDateTransaction::TRANSACTIONTYPE;
$start_date = $object->getStartDateTimeEpoch();
$end_date = $object->getEndDateTimeEpoch();
$recurrence_end = $object->getUntilDateTimeEpoch();
$is_recurring = $object->getIsRecurring();
$errors = array();
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() == $start_date_xaction) {
$start_date = $xaction->getNewValue()->getEpoch();
} else if ($xaction->getTransactionType() == $end_date_xaction) {
$end_date = $xaction->getNewValue()->getEpoch();
} else if ($xaction->getTransactionType() == $recurrence_end_xaction) {
$recurrence_end = $xaction->getNewValue()->getEpoch();
} else if ($xaction->getTransactionType() == $is_recurrence_xaction) {
$is_recurring = $xaction->getNewValue();
}
}
if ($start_date > $end_date) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$end_date_xaction,
pht('Invalid'),
pht('End date must be after start date.'),
null);
}
if ($recurrence_end && !$is_recurring) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$recurrence_end_xaction,
pht('Invalid'),
pht('Event must be recurring to have a recurrence end date.').
null);
}
return $errors;
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
if ($object->isImportedEvent()) {
return false;
}
return true;
}
protected function supportsSearch() {
return true;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
if ($object->isImportedEvent()) {
return false;
}
return true;
}
protected function getMailSubjectPrefix() {
return pht('[Calendar]');
}
protected function getMailTo(PhabricatorLiskDAO $object) {
$phids = array();
if ($object->getHostPHID()) {
$phids[] = $object->getHostPHID();
}
$phids[] = $this->getActingAsPHID();
$invitees = $object->getInvitees();
foreach ($invitees as $invitee) {
$status = $invitee->getStatus();
if ($status === PhabricatorCalendarEventInvitee::STATUS_ATTENDING
|| $status === PhabricatorCalendarEventInvitee::STATUS_INVITED) {
$phids[] = $invitee->getInviteePHID();
}
}
$phids = array_unique($phids);
return $phids;
}
public function getMailTagsMap() {
return array(
PhabricatorCalendarEventTransaction::MAILTAG_CONTENT =>
pht(
"An event's name, status, invite list, ".
"icon, and description changes."),
PhabricatorCalendarEventTransaction::MAILTAG_RESCHEDULE =>
pht(
"An event's start and end date ".
"and cancellation status changes."),
PhabricatorCalendarEventTransaction::MAILTAG_OTHER =>
pht('Other event activity not listed above occurs.'),
);
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new PhabricatorCalendarReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
- $id = $object->getID();
$name = $object->getName();
$monogram = $object->getMonogram();
return id(new PhabricatorMetaMTAMail())
- ->setSubject("{$monogram}: {$name}")
- ->addHeader('Thread-Topic', $monogram);
+ ->setSubject("{$monogram}: {$name}");
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
$description = $object->getDescription();
if ($this->getIsNewObject()) {
if (strlen($description)) {
$body->addRemarkupSection(
pht('EVENT DESCRIPTION'),
$description);
}
}
$body->addLinkSection(
pht('EVENT DETAIL'),
PhabricatorEnv::getProductionURI($object->getURI()));
$ics_attachment = $this->newICSAttachment($object);
$body->addAttachment($ics_attachment);
return $body;
}
protected function shouldApplyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
return id(new PhabricatorCalendarEventHeraldAdapter())
->setObject($object);
}
private function newICSAttachment(
PhabricatorCalendarEvent $event) {
$actor = $this->getActor();
$ics_data = id(new PhabricatorCalendarICSWriter())
->setViewer($actor)
->setEvents(array($event))
->writeICSDocument();
$ics_attachment = new PhabricatorMetaMTAAttachment(
$ics_data,
$event->getICSFilename(),
'text/calendar');
return $ics_attachment;
}
}
diff --git a/src/applications/calendar/herald/PhabricatorCalendarEventHeraldAdapter.php b/src/applications/calendar/herald/PhabricatorCalendarEventHeraldAdapter.php
index 1fc3a5631..49d5f3895 100644
--- a/src/applications/calendar/herald/PhabricatorCalendarEventHeraldAdapter.php
+++ b/src/applications/calendar/herald/PhabricatorCalendarEventHeraldAdapter.php
@@ -1,63 +1,56 @@
<?php
final class PhabricatorCalendarEventHeraldAdapter extends HeraldAdapter {
private $object;
public function getAdapterApplicationClass() {
return 'PhabricatorCalendarApplication';
}
public function getAdapterContentDescription() {
return pht('React to events being created or updated.');
}
protected function newObject() {
return new PhabricatorCalendarEvent();
}
public function isTestAdapterForObject($object) {
return ($object instanceof PhabricatorCalendarEvent);
}
public function getAdapterTestDescription() {
return pht(
'Test rules which run when an event is created or updated.');
}
public function setObject($object) {
$this->object = $object;
return $this;
}
public function getObject() {
return $this->object;
}
public function getAdapterContentName() {
return pht('Calendar Events');
}
public function supportsRuleType($rule_type) {
switch ($rule_type) {
case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
return true;
case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
default:
return false;
}
}
- public function getRepetitionOptions() {
- return array(
- HeraldRepetitionPolicyConfig::EVERY,
- HeraldRepetitionPolicyConfig::FIRST,
- );
- }
-
public function getHeraldName() {
return $this->getObject()->getMonogram();
}
}
diff --git a/src/applications/calendar/import/PhabricatorCalendarICSURIImportEngine.php b/src/applications/calendar/import/PhabricatorCalendarICSURIImportEngine.php
index 6174603c8..bd52ec5bc 100644
--- a/src/applications/calendar/import/PhabricatorCalendarICSURIImportEngine.php
+++ b/src/applications/calendar/import/PhabricatorCalendarICSURIImportEngine.php
@@ -1,120 +1,121 @@
<?php
final class PhabricatorCalendarICSURIImportEngine
extends PhabricatorCalendarICSImportEngine {
const ENGINETYPE = 'icsuri';
public function getImportEngineName() {
return pht('Import .ics URI');
}
public function getImportEngineTypeName() {
return pht('.ics URI');
}
public function getImportEngineHint() {
return pht('Import or subscribe to a calendar in .ics format by URI.');
}
public function supportsTriggers(PhabricatorCalendarImport $import) {
return true;
}
public function appendImportProperties(
PhabricatorUser $viewer,
PhabricatorCalendarImport $import,
PHUIPropertyListView $properties) {
$uri_key = PhabricatorCalendarImportICSURITransaction::PARAMKEY_URI;
$uri = $import->getParameter($uri_key);
// Since the URI may contain a secret hash, don't show it to users who
// can not edit the import.
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$import,
PhabricatorPolicyCapability::CAN_EDIT);
if (!$can_edit) {
$uri_display = phutil_tag('em', array(), pht('Restricted'));
} else if (!PhabricatorEnv::isValidRemoteURIForLink($uri)) {
$uri_display = $uri;
} else {
$uri_display = phutil_tag(
'a',
array(
'href' => $uri,
'target' => '_blank',
+ 'rel' => 'noreferrer',
),
$uri);
}
$properties->addProperty(pht('Source URI'), $uri_display);
}
public function newEditEngineFields(
PhabricatorEditEngine $engine,
PhabricatorCalendarImport $import) {
$fields = array();
if ($engine->getIsCreate()) {
$fields[] = id(new PhabricatorTextEditField())
->setKey('uri')
->setLabel(pht('URI'))
->setDescription(pht('URI to import.'))
->setTransactionType(
PhabricatorCalendarImportICSURITransaction::TRANSACTIONTYPE)
->setConduitDescription(pht('URI to import.'))
->setConduitTypeDescription(pht('New URI.'));
}
return $fields;
}
public function getDisplayName(PhabricatorCalendarImport $import) {
return pht('ICS URI');
}
public function importEventsFromSource(
PhabricatorUser $viewer,
PhabricatorCalendarImport $import,
$should_queue) {
$uri_key = PhabricatorCalendarImportICSURITransaction::PARAMKEY_URI;
$uri = $import->getParameter($uri_key);
PhabricatorSystemActionEngine::willTakeAction(
array($viewer->getPHID()),
new PhabricatorFilesOutboundRequestAction(),
1);
$file = PhabricatorFile::newFromFileDownload(
$uri,
array(
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
'authorPHID' => $import->getAuthorPHID(),
'canCDN' => true,
));
$import->newLogMessage(
PhabricatorCalendarImportFetchLogType::LOGTYPE,
array(
'file.phid' => $file->getPHID(),
));
$data = $file->loadFileData();
if ($should_queue && $this->shouldQueueDataImport($data)) {
return $this->queueDataImport($import, $data);
}
return $this->importICSData($viewer, $import, $data);
}
public function canDisable(
PhabricatorUser $viewer,
PhabricatorCalendarImport $import) {
return true;
}
}
diff --git a/src/applications/celerity/CelerityStaticResourceResponse.php b/src/applications/celerity/CelerityStaticResourceResponse.php
index 7db6c2741..783dd9657 100644
--- a/src/applications/celerity/CelerityStaticResourceResponse.php
+++ b/src/applications/celerity/CelerityStaticResourceResponse.php
@@ -1,347 +1,378 @@
<?php
/**
* Tracks and resolves dependencies the page declares with
* @{function:require_celerity_resource}, and then builds appropriate HTML or
* Ajax responses.
*/
final class CelerityStaticResourceResponse extends Phobject {
private $symbols = array();
private $needsResolve = true;
private $resolved;
private $packaged;
private $metadata = array();
private $metadataBlock = 0;
private $metadataLocked;
private $behaviors = array();
private $hasRendered = array();
private $postprocessorKey;
+ private $contentSecurityPolicyURIs = array();
public function __construct() {
if (isset($_REQUEST['__metablock__'])) {
$this->metadataBlock = (int)$_REQUEST['__metablock__'];
}
}
public function addMetadata($metadata) {
if ($this->metadataLocked) {
throw new Exception(
pht(
'Attempting to add more metadata after metadata has been '.
'locked.'));
}
$id = count($this->metadata);
$this->metadata[$id] = $metadata;
return $this->metadataBlock.'_'.$id;
}
+ public function addContentSecurityPolicyURI($kind, $uri) {
+ $this->contentSecurityPolicyURIs[$kind][] = $uri;
+ return $this;
+ }
+
+ public function getContentSecurityPolicyURIMap() {
+ return $this->contentSecurityPolicyURIs;
+ }
+
public function getMetadataBlock() {
return $this->metadataBlock;
}
public function setPostprocessorKey($postprocessor_key) {
$this->postprocessorKey = $postprocessor_key;
return $this;
}
public function getPostprocessorKey() {
return $this->postprocessorKey;
}
/**
* Register a behavior for initialization.
*
* NOTE: If `$config` is empty, a behavior will execute only once even if it
* is initialized multiple times. If `$config` is nonempty, the behavior will
* be invoked once for each configuration.
*/
public function initBehavior(
$behavior,
array $config = array(),
$source_name = null) {
$this->requireResource('javelin-behavior-'.$behavior, $source_name);
if (empty($this->behaviors[$behavior])) {
$this->behaviors[$behavior] = array();
}
if ($config) {
$this->behaviors[$behavior][] = $config;
}
return $this;
}
public function requireResource($symbol, $source_name) {
if (isset($this->symbols[$source_name][$symbol])) {
return $this;
}
// Verify that the resource exists.
$map = CelerityResourceMap::getNamedInstance($source_name);
$name = $map->getResourceNameForSymbol($symbol);
if ($name === null) {
throw new Exception(
pht(
'No resource with symbol "%s" exists in source "%s"!',
$symbol,
$source_name));
}
$this->symbols[$source_name][$symbol] = true;
$this->needsResolve = true;
return $this;
}
private function resolveResources() {
if ($this->needsResolve) {
$this->packaged = array();
foreach ($this->symbols as $source_name => $symbols_map) {
$symbols = array_keys($symbols_map);
$map = CelerityResourceMap::getNamedInstance($source_name);
$packaged = $map->getPackagedNamesForSymbols($symbols);
$this->packaged[$source_name] = $packaged;
}
$this->needsResolve = false;
}
return $this;
}
public function renderSingleResource($symbol, $source_name) {
$map = CelerityResourceMap::getNamedInstance($source_name);
$packaged = $map->getPackagedNamesForSymbols(array($symbol));
return $this->renderPackagedResources($map, $packaged);
}
public function renderResourcesOfType($type) {
$this->resolveResources();
$result = array();
foreach ($this->packaged as $source_name => $resource_names) {
$map = CelerityResourceMap::getNamedInstance($source_name);
$resources_of_type = array();
foreach ($resource_names as $resource_name) {
$resource_type = $map->getResourceTypeForName($resource_name);
if ($resource_type == $type) {
$resources_of_type[] = $resource_name;
}
}
$result[] = $this->renderPackagedResources($map, $resources_of_type);
}
return phutil_implode_html('', $result);
}
private function renderPackagedResources(
CelerityResourceMap $map,
array $resources) {
$output = array();
foreach ($resources as $name) {
if (isset($this->hasRendered[$name])) {
continue;
}
$this->hasRendered[$name] = true;
$output[] = $this->renderResource($map, $name);
}
return $output;
}
private function renderResource(
CelerityResourceMap $map,
$name) {
$uri = $this->getURI($map, $name);
$type = $map->getResourceTypeForName($name);
$multimeter = MultimeterControl::getInstance();
if ($multimeter) {
$event_type = MultimeterEvent::TYPE_STATIC_RESOURCE;
$multimeter->newEvent($event_type, 'rsrc.'.$name, 1);
}
switch ($type) {
case 'css':
return phutil_tag(
'link',
array(
'rel' => 'stylesheet',
'type' => 'text/css',
'href' => $uri,
));
case 'js':
return phutil_tag(
'script',
array(
'type' => 'text/javascript',
'src' => $uri,
),
'');
}
throw new Exception(
pht(
'Unable to render resource "%s", which has unknown type "%s".',
$name,
$type));
}
- public function renderHTMLFooter() {
+ public function renderHTMLFooter($is_frameable) {
$this->metadataLocked = true;
- $data = array();
- if ($this->metadata) {
- $json_metadata = AphrontResponse::encodeJSONForHTTPResponse(
- $this->metadata);
- $this->metadata = array();
- } else {
- $json_metadata = '{}';
- }
- // Even if there is no metadata on the page, Javelin uses the mergeData()
- // call to start dispatching the event queue.
- $data[] = 'JX.Stratcom.mergeData('.$this->metadataBlock.', '.
- $json_metadata.');';
+ $merge_data = array(
+ 'block' => $this->metadataBlock,
+ 'data' => $this->metadata,
+ );
+ $this->metadata = array();
- $onload = array();
+ $behavior_lists = array();
if ($this->behaviors) {
$behaviors = $this->behaviors;
$this->behaviors = array();
$higher_priority_names = array(
'refresh-csrf',
'aphront-basic-tokenizer',
'dark-console',
'history-install',
);
$higher_priority_behaviors = array_select_keys(
$behaviors,
$higher_priority_names);
foreach ($higher_priority_names as $name) {
unset($behaviors[$name]);
}
$behavior_groups = array(
$higher_priority_behaviors,
$behaviors,
);
foreach ($behavior_groups as $group) {
if (!$group) {
continue;
}
- $group_json = AphrontResponse::encodeJSONForHTTPResponse(
- $group);
- $onload[] = 'JX.initBehaviors('.$group_json.')';
+ $behavior_lists[] = $group;
}
}
- if ($onload) {
- foreach ($onload as $func) {
- $data[] = 'JX.onload(function(){'.$func.'});';
- }
+ $initializers = array();
+
+ // Even if there is no metadata on the page, Javelin uses the mergeData()
+ // call to start dispatching the event queue, so we always want to include
+ // this initializer.
+ $initializers[] = array(
+ 'kind' => 'merge',
+ 'data' => $merge_data,
+ );
+
+ foreach ($behavior_lists as $behavior_list) {
+ $initializers[] = array(
+ 'kind' => 'behaviors',
+ 'data' => $behavior_list,
+ );
}
- if ($data) {
- $data = implode("\n", $data);
- return self::renderInlineScript($data);
- } else {
- return '';
+ if ($is_frameable) {
+ $initializers[] = array(
+ 'data' => 'frameable',
+ 'kind' => (bool)$is_frameable,
+ );
+ }
+
+ $tags = array();
+ foreach ($initializers as $initializer) {
+ $data = $initializer['data'];
+ if (is_array($data)) {
+ $json_data = AphrontResponse::encodeJSONForHTTPResponse($data);
+ } else {
+ $json_data = json_encode($data);
+ }
+
+ $tags[] = phutil_tag(
+ 'data',
+ array(
+ 'data-javelin-init-kind' => $initializer['kind'],
+ 'data-javelin-init-data' => $json_data,
+ ));
}
+
+ return $tags;
}
public static function renderInlineScript($data) {
if (stripos($data, '</script>') !== false) {
throw new Exception(
pht(
'Literal %s is not allowed inside inline script.',
'</script>'));
}
if (strpos($data, '<!') !== false) {
throw new Exception(
pht(
'Literal %s is not allowed inside inline script.',
'<!'));
}
// We don't use <![CDATA[ ]]> because it is ignored by HTML parsers. We
// would need to send the document with XHTML content type.
return phutil_tag(
'script',
array('type' => 'text/javascript'),
phutil_safe_html($data));
}
public function buildAjaxResponse($payload, $error = null) {
$response = array(
'error' => $error,
'payload' => $payload,
);
if ($this->metadata) {
$response['javelin_metadata'] = $this->metadata;
$this->metadata = array();
}
if ($this->behaviors) {
$response['javelin_behaviors'] = $this->behaviors;
$this->behaviors = array();
}
$this->resolveResources();
$resources = array();
foreach ($this->packaged as $source_name => $resource_names) {
$map = CelerityResourceMap::getNamedInstance($source_name);
foreach ($resource_names as $resource_name) {
$resources[] = $this->getURI($map, $resource_name);
}
}
if ($resources) {
$response['javelin_resources'] = $resources;
}
return $response;
}
public function getURI(
CelerityResourceMap $map,
$name,
$use_primary_domain = false) {
$uri = $map->getURIForName($name);
// If we have a postprocessor selected, add it to the URI.
$postprocessor_key = $this->getPostprocessorKey();
if ($postprocessor_key) {
$uri = preg_replace('@^/res/@', '/res/'.$postprocessor_key.'X/', $uri);
}
// In developer mode, we dump file modification times into the URI. When a
// page is reloaded in the browser, any resources brought in by Ajax calls
// do not trigger revalidation, so without this it's very difficult to get
// changes to Ajaxed-in CSS to work (you must clear your cache or rerun
// the map script). In production, we can assume the map script gets run
// after changes, and safely skip this.
if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {
$mtime = $map->getModifiedTimeForName($name);
$uri = preg_replace('@^/res/@', '/res/'.$mtime.'T/', $uri);
}
if ($use_primary_domain) {
return PhabricatorEnv::getURI($uri);
} else {
return PhabricatorEnv::getCDNURI($uri);
}
}
}
diff --git a/src/applications/celerity/controller/CelerityResourceController.php b/src/applications/celerity/controller/CelerityResourceController.php
index 730e5ddc9..702fcefb2 100644
--- a/src/applications/celerity/controller/CelerityResourceController.php
+++ b/src/applications/celerity/controller/CelerityResourceController.php
@@ -1,207 +1,212 @@
<?php
abstract class CelerityResourceController extends PhabricatorController {
protected function buildResourceTransformer() {
return null;
}
public function shouldRequireLogin() {
return false;
}
public function shouldRequireEnabledUser() {
return false;
}
public function shouldAllowPartialSessions() {
return true;
}
public function shouldAllowLegallyNonCompliantUsers() {
return true;
}
abstract public function getCelerityResourceMap();
protected function serveResource(array $spec) {
$path = $spec['path'];
$hash = idx($spec, 'hash');
// Sanity checking to keep this from exposing anything sensitive, since it
// ultimately boils down to disk reads.
if (preg_match('@(//|\.\.)@', $path)) {
return new Aphront400Response();
}
$type = CelerityResourceTransformer::getResourceType($path);
$type_map = self::getSupportedResourceTypes();
if (empty($type_map[$type])) {
throw new Exception(pht('Only static resources may be served.'));
}
$dev_mode = PhabricatorEnv::getEnvConfig('phabricator.developer-mode');
$map = $this->getCelerityResourceMap();
$expect_hash = $map->getHashForName($path);
// Test if the URI hash is correct for our current resource map. If it
// is not, refuse to cache this resource. This avoids poisoning caches
// and CDNs if we're getting a request for a new resource to an old node
// shortly after a push.
$is_cacheable = ($hash === $expect_hash);
$is_locally_cacheable = $this->isLocallyCacheableResourceType($type);
if (AphrontRequest::getHTTPHeader('If-Modified-Since') && $is_cacheable) {
// Return a "304 Not Modified". We don't care about the value of this
// field since we never change what resource is served by a given URI.
return $this->makeResponseCacheable(new Aphront304Response());
}
$cache = null;
$data = null;
if ($is_cacheable && $is_locally_cacheable && !$dev_mode) {
$cache = PhabricatorCaches::getImmutableCache();
$request_path = $this->getRequest()->getPath();
$cache_key = $this->getCacheKey($request_path);
$data = $cache->getKey($cache_key);
}
if ($data === null) {
if ($map->isPackageResource($path)) {
$resource_names = $map->getResourceNamesForPackageName($path);
if (!$resource_names) {
return new Aphront404Response();
}
try {
$data = array();
foreach ($resource_names as $resource_name) {
$data[] = $map->getResourceDataForName($resource_name);
}
$data = implode("\n\n", $data);
} catch (Exception $ex) {
return new Aphront404Response();
}
} else {
try {
$data = $map->getResourceDataForName($path);
} catch (Exception $ex) {
return new Aphront404Response();
}
}
$xformer = $this->buildResourceTransformer();
if ($xformer) {
$data = $xformer->transformResource($path, $data);
}
if ($cache) {
$cache->setKey($cache_key, $data);
}
}
$response = id(new AphrontFileResponse())
->setMimeType($type_map[$type]);
+ // The "Content-Security-Policy" header has no effect on the actual
+ // resources, only on the main request. Disable it on the resource
+ // responses to limit confusion.
+ $response->setDisableContentSecurityPolicy(true);
+
$range = AphrontRequest::getHTTPHeader('Range');
if (strlen($range)) {
$response->setContentLength(strlen($data));
list($range_begin, $range_end) = $response->parseHTTPRange($range);
if ($range_begin !== null) {
if ($range_end !== null) {
$data = substr($data, $range_begin, ($range_end - $range_begin));
} else {
$data = substr($data, $range_begin);
}
}
$response->setContentIterator(array($data));
} else {
$response
->setContent($data)
->setCompressResponse(true);
}
// NOTE: This is a piece of magic required to make WOFF fonts work in
// Firefox and IE. Possibly we should generalize this more.
$cross_origin_types = array(
'woff' => true,
'woff2' => true,
'eot' => true,
);
if (isset($cross_origin_types[$type])) {
// We could be more tailored here, but it's not currently trivial to
// generate a comprehensive list of valid origins (an install may have
// arbitrarily many Phame blogs, for example), and we lose nothing by
// allowing access from anywhere.
$response->addAllowOrigin('*');
}
if ($is_cacheable) {
$response = $this->makeResponseCacheable($response);
}
return $response;
}
public static function getSupportedResourceTypes() {
return array(
'css' => 'text/css; charset=utf-8',
'js' => 'text/javascript; charset=utf-8',
'png' => 'image/png',
'svg' => 'image/svg+xml',
'gif' => 'image/gif',
'jpg' => 'image/jpeg',
'swf' => 'application/x-shockwave-flash',
'woff' => 'font/woff',
'woff2' => 'font/woff2',
'eot' => 'font/eot',
'ttf' => 'font/ttf',
'mp3' => 'audio/mpeg',
'ico' => 'image/x-icon',
);
}
private function makeResponseCacheable(AphrontResponse $response) {
$response->setCacheDurationInSeconds(60 * 60 * 24 * 30);
$response->setLastModified(time());
$response->setCanCDN(true);
return $response;
}
/**
* Is it appropriate to cache the data for this resource type in the fast
* immutable cache?
*
* Generally, text resources (which are small, and expensive to process)
* are cached, while other types of resources (which are large, and cheap
* to process) are not.
*
* @param string Resource type.
* @return bool True to enable caching.
*/
private function isLocallyCacheableResourceType($type) {
$types = array(
'js' => true,
'css' => true,
);
return isset($types[$type]);
}
protected function getCacheKey($path) {
return 'celerity:'.PhabricatorHash::digestToLength($path, 64);
}
}
diff --git a/src/applications/conduit/management/PhabricatorConduitCallManagementWorkflow.php b/src/applications/conduit/management/PhabricatorConduitCallManagementWorkflow.php
new file mode 100644
index 000000000..6cb3bd240
--- /dev/null
+++ b/src/applications/conduit/management/PhabricatorConduitCallManagementWorkflow.php
@@ -0,0 +1,66 @@
+<?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.'),
+ ),
+ ));
+ }
+
+ 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".'));
+ }
+
+ 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)
+ ->execute();
+
+ $output = array(
+ 'result' => $result,
+ );
+
+ echo tsprintf(
+ "%B\n",
+ id(new PhutilJSON())->encodeFormatted($output));
+
+ return 0;
+ }
+
+}
diff --git a/src/applications/conduit/management/PhabricatorConduitManagementWorkflow.php b/src/applications/conduit/management/PhabricatorConduitManagementWorkflow.php
new file mode 100644
index 000000000..4abb250fc
--- /dev/null
+++ b/src/applications/conduit/management/PhabricatorConduitManagementWorkflow.php
@@ -0,0 +1,4 @@
+<?php
+
+abstract class PhabricatorConduitManagementWorkflow
+ extends PhabricatorManagementWorkflow {}
diff --git a/src/applications/conduit/parametertype/ConduitPHIDParameterType.php b/src/applications/conduit/parametertype/ConduitPHIDParameterType.php
index 3bb45697d..eafee4d45 100644
--- a/src/applications/conduit/parametertype/ConduitPHIDParameterType.php
+++ b/src/applications/conduit/parametertype/ConduitPHIDParameterType.php
@@ -1,35 +1,62 @@
<?php
final class ConduitPHIDParameterType
extends ConduitParameterType {
+ private $isNullable;
+
+ public function setIsNullable($is_nullable) {
+ $this->isNullable = $is_nullable;
+ return $this;
+ }
+
+ public function getIsNullable() {
+ return $this->isNullable;
+ }
+
protected function getParameterValue(array $request, $key, $strict) {
$value = parent::getParameterValue($request, $key, $strict);
+ if ($this->getIsNullable()) {
+ if ($value === null) {
+ return $value;
+ }
+ }
+
if (!is_string($value)) {
$this->raiseValidationException(
$request,
$key,
pht('Expected PHID, got something else.'));
}
return $value;
}
protected function getParameterTypeName() {
- return 'phid';
+ if ($this->getIsNullable()) {
+ return 'phid|null';
+ } else {
+ return 'phid';
+ }
}
protected function getParameterFormatDescriptions() {
return array(
pht('A PHID.'),
);
}
protected function getParameterExamples() {
- return array(
+ $examples = array(
'"PHID-WXYZ-1111222233334444"',
);
+
+ if ($this->getIsNullable()) {
+ $examples[] = 'null';
+ }
+
+ return $examples;
}
}
diff --git a/src/applications/config/check/PhabricatorMailSetupCheck.php b/src/applications/config/check/PhabricatorMailSetupCheck.php
index 2b8e4e12d..b3b6143ad 100644
--- a/src/applications/config/check/PhabricatorMailSetupCheck.php
+++ b/src/applications/config/check/PhabricatorMailSetupCheck.php
@@ -1,100 +1,104 @@
<?php
final class PhabricatorMailSetupCheck extends PhabricatorSetupCheck {
public function getDefaultGroup() {
return self::GROUP_OTHER;
}
protected function executeChecks() {
+ if (PhabricatorEnv::getEnvConfig('cluster.mailers')) {
+ return;
+ }
+
$adapter = PhabricatorEnv::getEnvConfig('metamta.mail-adapter');
switch ($adapter) {
case 'PhabricatorMailImplementationPHPMailerLiteAdapter':
if (!Filesystem::pathExists('/usr/bin/sendmail') &&
!Filesystem::pathExists('/usr/sbin/sendmail')) {
$message = pht(
'Mail is configured to send via sendmail, but this system has '.
'no sendmail binary. Install sendmail or choose a different '.
'mail adapter.');
$this->newIssue('config.metamta.mail-adapter')
->setShortName(pht('Missing Sendmail'))
->setName(pht('No Sendmail Binary Found'))
->setMessage($message)
->addRelatedPhabricatorConfig('metamta.mail-adapter');
}
break;
case 'PhabricatorMailImplementationAmazonSESAdapter':
if (PhabricatorEnv::getEnvConfig('metamta.can-send-as-user')) {
$message = pht(
'Amazon SES does not support sending email as users. Disable '.
'send as user, or choose a different mail adapter.');
$this->newIssue('config.can-send-as-user')
->setName(pht("SES Can't Send As User"))
->setMessage($message)
->addRelatedPhabricatorConfig('metamta.mail-adapter')
->addPhabricatorConfig('metamta.can-send-as-user');
}
if (!PhabricatorEnv::getEnvConfig('amazon-ses.access-key')) {
$message = pht(
'Amazon SES is selected as the mail adapter, but no SES access '.
'key is configured. Provide an SES access key, or choose a '.
'different mail adapter.');
$this->newIssue('config.amazon-ses.access-key')
->setName(pht('Amazon SES Access Key Not Set'))
->setMessage($message)
->addRelatedPhabricatorConfig('metamta.mail-adapter')
->addPhabricatorConfig('amazon-ses.access-key');
}
if (!PhabricatorEnv::getEnvConfig('amazon-ses.secret-key')) {
$message = pht(
'Amazon SES is selected as the mail adapter, but no SES secret '.
'key is configured. Provide an SES secret key, or choose a '.
'different mail adapter.');
$this->newIssue('config.amazon-ses.secret-key')
->setName(pht('Amazon SES Secret Key Not Set'))
->setMessage($message)
->addRelatedPhabricatorConfig('metamta.mail-adapter')
->addPhabricatorConfig('amazon-ses.secret-key');
}
if (!PhabricatorEnv::getEnvConfig('amazon-ses.endpoint')) {
$message = pht(
'Amazon SES is selected as the mail adapter, but no SES endpoint '.
'is configured. Provide an SES endpoint or choose a different '.
'mail adapter.');
$this->newIssue('config.amazon-ses.endpoint')
->setName(pht('Amazon SES Endpoint Not Set'))
->setMessage($message)
->addRelatedPhabricatorConfig('metamta.mail-adapter')
->addPhabricatorConfig('amazon-ses.endpoint');
}
$address_key = 'metamta.default-address';
$options = PhabricatorApplicationConfigOptions::loadAllOptions();
$default = $options[$address_key]->getDefault();
$value = PhabricatorEnv::getEnvConfig($address_key);
if ($default === $value) {
$message = pht(
'Amazon SES requires verification of the "From" address, but '.
'you have not configured a "From" address. Configure and verify '.
'a "From" address, or choose a different mail adapter.');
$this->newIssue('config.metamta.default-address')
->setName(pht('No SES From Address Configured'))
->setMessage($message)
->addRelatedPhabricatorConfig('metamta.mail-adapter')
->addPhabricatorConfig('metamta.default-address');
}
break;
}
}
}
diff --git a/src/applications/config/check/PhabricatorPHPConfigSetupCheck.php b/src/applications/config/check/PhabricatorPHPConfigSetupCheck.php
index 5382a27b7..cf3148b5b 100644
--- a/src/applications/config/check/PhabricatorPHPConfigSetupCheck.php
+++ b/src/applications/config/check/PhabricatorPHPConfigSetupCheck.php
@@ -1,72 +1,117 @@
<?php
/**
* Noncritical PHP configuration checks.
*
* For critical checks, see @{class:PhabricatorPHPPreflightSetupCheck}.
*/
final class PhabricatorPHPConfigSetupCheck extends PhabricatorSetupCheck {
public function getDefaultGroup() {
return self::GROUP_PHP;
}
protected function executeChecks() {
if (empty($_SERVER['REMOTE_ADDR'])) {
$doc_href = PhabricatorEnv::getDoclink('Configuring a Preamble Script');
$summary = pht(
'You likely need to fix your preamble script so '.
'REMOTE_ADDR is no longer empty.');
$message = pht(
'No REMOTE_ADDR is available, so Phabricator cannot determine the '.
'origin address for requests. This will prevent Phabricator from '.
'performing important security checks. This most often means you '.
'have a mistake in your preamble script. Consult the documentation '.
'(%s) and double-check that the script is written correctly.',
phutil_tag(
'a',
array(
'href' => $doc_href,
'target' => '_blank',
),
pht('Configuring a Preamble Script')));
$this->newIssue('php.remote_addr')
->setName(pht('No REMOTE_ADDR available'))
->setSummary($summary)
->setMessage($message);
}
if (version_compare(phpversion(), '7', '>=')) {
// This option was removed in PHP7.
$raw_post_data = -1;
} else {
$raw_post_data = (int)ini_get('always_populate_raw_post_data');
}
if ($raw_post_data != -1) {
$summary = pht(
'PHP setting "%s" should be set to "-1" to avoid deprecation '.
'warnings.',
'always_populate_raw_post_data');
$message = pht(
'The "%s" key is set to some value other than "-1" in your PHP '.
'configuration. This can cause PHP to raise deprecation warnings '.
'during process startup. Set this option to "-1" to prevent these '.
'warnings from appearing.',
'always_populate_raw_post_data');
$this->newIssue('php.always_populate_raw_post_data')
->setName(pht('Disable PHP %s', 'always_populate_raw_post_data'))
->setSummary($summary)
->setMessage($message)
->addPHPConfig('always_populate_raw_post_data');
}
+ if (!extension_loaded('mysqli')) {
+ $summary = pht(
+ 'Install the MySQLi extension to improve database behavior.');
+
+ $message = pht(
+ 'PHP is currently using the very old "mysql" extension to interact '.
+ 'with the database. You should install the newer "mysqli" extension '.
+ 'to improve behaviors (like error handling and query timeouts).'.
+ "\n\n".
+ 'Phabricator will work with the older extension, but upgrading to the '.
+ 'newer extension is recommended.'.
+ "\n\n".
+ 'You may be able to install the extension with a command like: %s',
+
+ // NOTE: We're intentionally telling you to install "mysqlnd" here; on
+ // Ubuntu, there's no separate "mysqli" package.
+ phutil_tag('tt', array(), 'sudo apt-get install php5-mysqlnd'));
+
+ $this->newIssue('php.mysqli')
+ ->setName(pht('MySQLi Extension Not Available'))
+ ->setSummary($summary)
+ ->setMessage($message);
+ } else if (!defined('MYSQLI_ASYNC')) {
+ $summary = pht(
+ 'Configure the MySQL Native Driver to improve database behavior.');
+
+ $message = pht(
+ 'PHP is currently using the older MySQL external driver instead of '.
+ 'the newer MySQL native driver. The older driver lacks options and '.
+ 'features (like support for query timeouts) which allow Phabricator '.
+ 'to interact better with the database.'.
+ "\n\n".
+ 'Phabricator will work with the older driver, but upgrading to the '.
+ 'native driver is recommended.'.
+ "\n\n".
+ 'You may be able to install the native driver with a command like: %s',
+ phutil_tag('tt', array(), 'sudo apt-get install php5-mysqlnd'));
+
+
+ $this->newIssue('php.myqlnd')
+ ->setName(pht('MySQL Native Driver Not Available'))
+ ->setSummary($summary)
+ ->setMessage($message);
+ }
+
}
}
diff --git a/src/applications/config/controller/PhabricatorConfigController.php b/src/applications/config/controller/PhabricatorConfigController.php
index e8f1ffecc..ad5ea6cd2 100644
--- a/src/applications/config/controller/PhabricatorConfigController.php
+++ b/src/applications/config/controller/PhabricatorConfigController.php
@@ -1,96 +1,95 @@
<?php
abstract class PhabricatorConfigController extends PhabricatorController {
public function shouldRequireAdmin() {
return true;
}
public function buildSideNavView($filter = null, $for_app = false) {
$guide_href = new PhutilURI('/guides/');
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI($this->getApplicationURI()));
$nav->addFilter('/',
pht('Core Settings'), null, 'fa-gear');
$nav->addFilter('application/',
pht('Application Settings'), null, 'fa-globe');
$nav->addFilter('history/',
pht('Settings History'), null, 'fa-history');
$nav->addFilter('version/',
pht('Version Information'), null, 'fa-download');
$nav->addFilter('all/',
pht('All Settings'), null, 'fa-list-ul');
$nav->addLabel(pht('Setup'));
$nav->addFilter('issue/',
pht('Setup Issues'), null, 'fa-warning');
$nav->addFilter(null,
pht('Installation Guide'), $guide_href, 'fa-book');
$nav->addLabel(pht('Database'));
$nav->addFilter('database/',
pht('Database Status'), null, 'fa-heartbeat');
$nav->addFilter('dbissue/',
pht('Database Issues'), null, 'fa-exclamation-circle');
$nav->addLabel(pht('Cache'));
$nav->addFilter('cache/',
pht('Cache Status'), null, 'fa-home');
$nav->addLabel(pht('Cluster'));
$nav->addFilter('cluster/databases/',
pht('Database Servers'), null, 'fa-database');
$nav->addFilter('cluster/notifications/',
pht('Notification Servers'), null, 'fa-bell-o');
$nav->addFilter('cluster/repositories/',
pht('Repository Servers'), null, 'fa-code');
$nav->addFilter('cluster/search/',
pht('Search Servers'), null, 'fa-search');
$nav->addLabel(pht('Modules'));
-
$modules = PhabricatorConfigModule::getAllModules();
foreach ($modules as $key => $module) {
$nav->addFilter('module/'.$key.'/',
$module->getModuleName(), null, 'fa-puzzle-piece');
}
return $nav;
}
public function buildApplicationMenu() {
return $this->buildSideNavView(null, true)->getMenu();
}
public function buildHeaderView($text, $action = null) {
$viewer = $this->getViewer();
$file = PhabricatorFile::loadBuiltin($viewer, 'projects/v3/manage.png');
$image = $file->getBestURI($file);
$header = id(new PHUIHeaderView())
->setHeader($text)
->setProfileHeader(true)
->setImage($image);
if ($action) {
$header->addActionLink($action);
}
return $header;
}
public function buildConfigBoxView($title, $content, $action = null) {
$header = id(new PHUIHeaderView())
->setHeader($title);
if ($action) {
$header->addActionItem($action);
}
$view = id(new PHUIObjectBoxView())
->setHeader($header)
->appendChild($content)
->setBackground(PHUIObjectBoxView::WHITE_CONFIG);
return $view;
}
}
diff --git a/src/applications/config/management/PhabricatorConfigManagementSetWorkflow.php b/src/applications/config/management/PhabricatorConfigManagementSetWorkflow.php
index 9f50d2eee..22b760872 100644
--- a/src/applications/config/management/PhabricatorConfigManagementSetWorkflow.php
+++ b/src/applications/config/management/PhabricatorConfigManagementSetWorkflow.php
@@ -1,130 +1,150 @@
<?php
final class PhabricatorConfigManagementSetWorkflow
extends PhabricatorConfigManagementWorkflow {
protected function didConstruct() {
$this
->setName('set')
- ->setExamples('**set** __key__ __value__')
+ ->setExamples(
+ "**set** __key__ __value__\n".
+ "**set** __key__ --stdin < value.json")
->setSynopsis(pht('Set a local configuration value.'))
->setArguments(
array(
array(
'name' => 'database',
'help' => pht(
'Update configuration in the database instead of '.
'in local configuration.'),
),
+ array(
+ 'name' => 'stdin',
+ 'help' => pht('Read option value from stdin.'),
+ ),
array(
'name' => 'args',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$argv = $args->getArg('args');
if (count($argv) == 0) {
throw new PhutilArgumentUsageException(
pht('Specify a configuration key and a value to set it to.'));
}
+ $is_stdin = $args->getArg('stdin');
+
$key = $argv[0];
- if (count($argv) == 1) {
- throw new PhutilArgumentUsageException(
- pht(
- "Specify a value to set the key '%s' to.",
- $key));
- }
+ if ($is_stdin) {
+ if (count($argv) > 1) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Too many arguments: expected only a key when using "--stdin".'));
+ }
- $value = $argv[1];
+ fprintf(STDERR, tsprintf("%s\n", pht('Reading value from stdin...')));
+ $value = file_get_contents('php://stdin');
+ } else {
+ if (count($argv) == 1) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ "Specify a value to set the key '%s' to.",
+ $key));
+ }
- if (count($argv) > 2) {
- throw new PhutilArgumentUsageException(
- pht(
- 'Too many arguments: expected one key and one value.'));
+ if (count($argv) > 2) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Too many arguments: expected one key and one value.'));
+ }
+
+ $value = $argv[1];
}
+
$options = PhabricatorApplicationConfigOptions::loadAllOptions();
if (empty($options[$key])) {
throw new PhutilArgumentUsageException(
pht(
"No such configuration key '%s'! Use `%s` to list all keys.",
$key,
'config list'));
}
$option = $options[$key];
$type = $option->newOptionType();
if ($type) {
try {
$value = $type->newValueFromCommandLineValue(
$option,
$value);
$type->validateStoredValue($option, $value);
} catch (PhabricatorConfigValidationException $ex) {
throw new PhutilArgumentUsageException($ex->getMessage());
}
} else {
// NOTE: For now, this handles both "wild" values and custom types.
$type = $option->getType();
switch ($type) {
default:
$value = json_decode($value, true);
if (!is_array($value)) {
switch ($type) {
default:
$message = pht(
'Config key "%s" is of type "%s". Specify it in JSON.',
$key,
$type);
break;
}
throw new PhutilArgumentUsageException($message);
}
break;
}
}
$use_database = $args->getArg('database');
if ($option->getLocked() && $use_database) {
throw new PhutilArgumentUsageException(
pht(
'Config key "%s" is locked and can only be set in local '.
'configuration. To learn more, see "%s" in the documentation.',
$key,
pht('Configuration Guide: Locked and Hidden Configuration')));
}
try {
$option->getGroup()->validateOption($option, $value);
} catch (PhabricatorConfigValidationException $validation) {
// Convert this into a usage exception so we don't dump a stack trace.
throw new PhutilArgumentUsageException($validation->getMessage());
}
if ($use_database) {
$config_type = 'database';
$config_entry = PhabricatorConfigEntry::loadConfigEntry($key);
$config_entry->setValue($value);
// If the entry has been deleted, resurrect it.
$config_entry->setIsDeleted(0);
$config_entry->save();
} else {
$config_type = 'local';
id(new PhabricatorConfigLocalSource())
->setKeys(array($key => $value));
}
$console->writeOut(
"%s\n",
pht("Set '%s' in %s configuration.", $key, $config_type));
}
}
diff --git a/src/applications/config/option/PhabricatorAccessLogConfigOptions.php b/src/applications/config/option/PhabricatorAccessLogConfigOptions.php
index 9ae60825e..2b9c4bbc7 100644
--- a/src/applications/config/option/PhabricatorAccessLogConfigOptions.php
+++ b/src/applications/config/option/PhabricatorAccessLogConfigOptions.php
@@ -1,136 +1,140 @@
<?php
final class PhabricatorAccessLogConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Access Logs');
}
public function getDescription() {
return pht('Configure the access logs, which log HTTP/SSH requests.');
}
public function getIcon() {
return 'fa-list';
}
public function getGroup() {
return 'core';
}
public function getOptions() {
$common_map = array(
'C' => pht('The controller or workflow which handled the request.'),
'c' => pht('The HTTP response code or process exit code.'),
'D' => pht('The request date.'),
'e' => pht('Epoch timestamp.'),
'h' => pht("The webserver's host name."),
'p' => pht('The PID of the server process.'),
'r' => pht('The remote IP.'),
'T' => pht('The request duration, in microseconds.'),
'U' => pht('The request path, or request target.'),
'm' => pht('For conduit, the Conduit method which was invoked.'),
'u' => pht('The logged-in username, if one is logged in.'),
'P' => pht('The logged-in user PHID, if one is logged in.'),
'i' => pht('Request input, in bytes.'),
'o' => pht('Request output, in bytes.'),
'I' => pht('Cluster instance name, if configured.'),
);
$http_map = $common_map + array(
'R' => pht('The HTTP referrer.'),
'M' => pht('The HTTP method.'),
);
$ssh_map = $common_map + array(
's' => pht('The system user.'),
'S' => pht('The system sudo user.'),
'k' => pht('ID of the SSH key used to authenticate the request.'),
+
+ // TODO: This is a reasonable thing to support in the HTTP access
+ // log, too.
+ 'Q' => pht('A random, unique string which identifies the request.'),
);
$http_desc = pht(
'Format for the HTTP access log. Use `%s` to set the path. '.
'Available variables are:',
'log.access.path');
$http_desc .= "\n\n";
$http_desc .= $this->renderMapHelp($http_map);
$ssh_desc = pht(
'Format for the SSH access log. Use %s to set the path. '.
'Available variables are:',
'log.ssh.path');
$ssh_desc .= "\n\n";
$ssh_desc .= $this->renderMapHelp($ssh_map);
return array(
$this->newOption('log.access.path', 'string', null)
->setLocked(true)
->setSummary(pht('Access log location.'))
->setDescription(
pht(
"To enable the Phabricator access log, specify a path. The ".
"Phabricator access than normal HTTP access logs (for instance, ".
"it can show logged-in users, controllers, and other application ".
"data).\n\n".
"If not set, no log will be written."))
->addExample(
null,
pht('Disable access log.'))
->addExample(
'/var/log/phabricator/access.log',
pht('Write access log here.')),
$this->newOption(
'log.access.format',
// NOTE: This is 'wild' intead of 'string' so "\t" and such can be
// specified.
'wild',
"[%D]\t%p\t%h\t%r\t%u\t%C\t%m\t%U\t%R\t%c\t%T")
->setLocked(true)
->setSummary(pht('Access log format.'))
->setDescription($http_desc),
$this->newOption('log.ssh.path', 'string', null)
->setLocked(true)
->setSummary(pht('SSH log location.'))
->setDescription(
pht(
"To enable the Phabricator SSH log, specify a path. The ".
"access log can provide more detailed information about SSH ".
"access than a normal SSH log (for instance, it can show ".
"logged-in users, commands, and other application data).\n\n".
"If not set, no log will be written."))
->addExample(
null,
pht('Disable SSH log.'))
->addExample(
'/var/log/phabricator/ssh.log',
pht('Write SSH log here.')),
$this->newOption(
'log.ssh.format',
'wild',
"[%D]\t%p\t%h\t%r\t%s\t%S\t%u\t%C\t%U\t%c\t%T\t%i\t%o")
->setLocked(true)
->setSummary(pht('SSH log format.'))
->setDescription($ssh_desc),
);
}
private function renderMapHelp(array $map) {
$desc = '';
foreach ($map as $key => $kdesc) {
$desc .= " - `%".$key."` ".$kdesc."\n";
}
$desc .= "\n";
$desc .= pht(
"If a variable isn't available (for example, %%m appears in the file ".
"format but the request is not a Conduit request), it will be rendered ".
"as '-'");
$desc .= "\n\n";
$desc .= pht(
"Note that the default format is subject to change in the future, so ".
"if you rely on the log's format, specify it explicitly.");
return $desc;
}
}
diff --git a/src/applications/config/option/PhabricatorCoreConfigOptions.php b/src/applications/config/option/PhabricatorCoreConfigOptions.php
index 6efd27d9a..0c9e69ebf 100644
--- a/src/applications/config/option/PhabricatorCoreConfigOptions.php
+++ b/src/applications/config/option/PhabricatorCoreConfigOptions.php
@@ -1,320 +1,324 @@
<?php
final class PhabricatorCoreConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Core');
}
public function getDescription() {
return pht('Configure core options, including URIs.');
}
public function getIcon() {
return 'fa-bullseye';
}
public function getGroup() {
return 'core';
}
public function getOptions() {
if (phutil_is_windows()) {
$paths = array();
} else {
$paths = array(
'/bin',
'/usr/bin',
'/usr/local/bin',
);
}
$path = getenv('PATH');
$proto_doc_href = PhabricatorEnv::getDoclink(
'User Guide: Prototype Applications');
$proto_doc_name = pht('User Guide: Prototype Applications');
$applications_app_href = '/applications/';
+ $silent_description = $this->deformat(pht(<<<EOREMARKUP
+This option allows you to stop Phabricator from sending data to most external
+services: it will disable email, SMS, repository mirroring, remote builds,
+Doorkeeper writes, and webhooks.
+
+This option is intended to allow a Phabricator instance to be exported, copied,
+imported, and run in a test environment without impacting users. For example,
+if you are migrating to new hardware, you could perform a test migration first
+with this flag set, make sure things work, and then do a production cutover
+later with higher confidence and less disruption.
+
+Without making use of this flag to silence the temporary test environment,
+users would receive duplicate email during the time the test instance and old
+production instance were both in operation.
+EOREMARKUP
+ ));
+
+
return array(
$this->newOption('phabricator.base-uri', 'string', null)
->setLocked(true)
->setSummary(pht('URI where Phabricator is installed.'))
->setDescription(
pht(
'Set the URI where Phabricator is installed. Setting this '.
'improves security by preventing cookies from being set on other '.
'domains, and allows daemons to send emails with links that have '.
'the correct domain.'))
->addExample('http://phabricator.example.com/', pht('Valid Setting')),
$this->newOption('phabricator.production-uri', 'string', null)
->setSummary(
pht('Primary install URI, for multi-environment installs.'))
->setDescription(
pht(
'If you have multiple Phabricator environments (like a '.
'development/staging environment for working on testing '.
'Phabricator, and a production environment for deploying it), '.
'set the production environment URI here so that emails and other '.
'durable URIs will always generate with links pointing at the '.
'production environment. If unset, defaults to `%s`. Most '.
'installs do not need to set this option.',
'phabricator.base-uri'))
->addExample('http://phabricator.example.com/', pht('Valid Setting')),
$this->newOption('phabricator.allowed-uris', 'list<string>', array())
->setLocked(true)
->setSummary(pht('Alternative URIs that can access Phabricator.'))
->setDescription(
pht(
"These alternative URIs will be able to access 'normal' pages ".
"on your Phabricator install. Other features such as OAuth ".
"won't work. The major use case for this is moving installs ".
"across domains."))
->addExample(
"http://phabricator2.example.com/\n".
"http://phabricator3.example.com/",
pht('Valid Setting')),
$this->newOption('phabricator.timezone', 'string', null)
->setSummary(
pht('The timezone Phabricator should use.'))
->setDescription(
pht(
"PHP requires that you set a timezone in your php.ini before ".
"using date functions, or it will emit a warning. If this isn't ".
"possible (for instance, because you are using HPHP) you can set ".
"some valid constant for %s here and Phabricator will set it on ".
"your behalf, silencing the warning.",
'date_default_timezone_set()'))
->addExample('America/New_York', pht('US East (EDT)'))
->addExample('America/Chicago', pht('US Central (CDT)'))
->addExample('America/Boise', pht('US Mountain (MDT)'))
->addExample('America/Los_Angeles', pht('US West (PDT)')),
$this->newOption('phabricator.cookie-prefix', 'string', null)
->setLocked(true)
->setSummary(
pht(
'Set a string Phabricator should use to prefix cookie names.'))
->setDescription(
pht(
'Cookies set for x.com are also sent for y.x.com. Assuming '.
'Phabricator instances are running on both domains, this will '.
'create a collision preventing you from logging in.'))
->addExample('dev', pht('Prefix cookie with "%s"', 'dev')),
$this->newOption('phabricator.show-prototypes', 'bool', false)
->setLocked(true)
->setBoolOptions(
array(
pht('Enable Prototypes'),
pht('Disable Prototypes'),
))
->setSummary(
pht(
'Install applications which are still under development.'))
->setDescription(
pht(
"IMPORTANT: The upstream does not provide support for prototype ".
"applications.".
"\n\n".
"Phabricator includes prototype applications which are in an ".
"**early stage of development**. By default, prototype ".
"applications are not installed, because they are often not yet ".
"developed enough to be generally usable. You can enable ".
"this option to install them if you're developing Phabricator ".
"or are interested in previewing upcoming features.".
"\n\n".
"To learn more about prototypes, see [[ %s | %s ]].".
"\n\n".
"After enabling prototypes, you can selectively uninstall them ".
"(like normal applications).",
$proto_doc_href,
$proto_doc_name)),
$this->newOption('phabricator.serious-business', 'bool', false)
->setBoolOptions(
array(
pht('Serious business'),
pht('Shenanigans'), // That should be interesting to translate. :P
))
->setSummary(
pht('Allows you to remove levity and jokes from the UI.'))
->setDescription(
pht(
'By default, Phabricator includes some flavor text in the UI, '.
'like a prompt to "Weigh In" rather than "Add Comment" in '.
'Maniphest. If you\'d prefer more traditional UI strings like '.
'"Add Comment", you can set this flag to disable most of the '.
'extra flavor.')),
$this->newOption('remarkup.ignored-object-names', 'string', '/^(Q|V)\d$/')
->setSummary(
pht('Text values that match this regex and are also object names '.
'will not be linked.'))
->setDescription(
pht(
'By default, Phabricator links object names in Remarkup fields '.
'to the corresponding object. This regex can be used to modify '.
'this behavior; object names that match this regex will not be '.
'linked.')),
$this->newOption('environment.append-paths', 'list<string>', $paths)
->setSummary(
pht(
'These paths get appended to your %s environment variable.',
'$PATH'))
->setDescription(
pht(
"Phabricator occasionally shells out to other binaries on the ".
"server. An example of this is the `%s` command, used to ".
"syntax-highlight code written in languages other than PHP. By ".
"default, it is assumed that these binaries are in the %s of the ".
"user running Phabricator (normally 'apache', 'httpd', or ".
"'nobody'). Here you can add extra directories to the %s ".
"environment variable, for when these binaries are in ".
"non-standard locations.\n\n".
"Note that you can also put binaries in `%s` (for example, by ".
"symlinking them).\n\n".
"The current value of PATH after configuration is applied is:\n\n".
" lang=text\n".
" %s",
'pygmentize',
'$PATH',
'$PATH',
'phabricator/support/bin/',
$path))
->setLocked(true)
->addExample('/usr/local/bin', pht('Add One Path'))
->addExample("/usr/bin\n/usr/local/bin", pht('Add Multiple Paths')),
$this->newOption('config.lock', 'set', array())
->setLocked(true)
->setDescription(pht('Additional configuration options to lock.')),
$this->newOption('config.hide', 'set', array())
->setLocked(true)
->setDescription(pht('Additional configuration options to hide.')),
$this->newOption('config.ignore-issues', 'set', array())
->setLocked(true)
->setDescription(pht('Setup issues to ignore.')),
$this->newOption('phabricator.env', 'string', null)
->setLocked(true)
->setDescription(pht('Internal.')),
$this->newOption('test.value', 'wild', null)
->setLocked(true)
->setDescription(pht('Unit test value.')),
$this->newOption('phabricator.uninstalled-applications', 'set', array())
->setLocked(true)
->setLockedMessage(pht(
'Use the %s to manage installed applications.',
phutil_tag(
'a',
array(
'href' => $applications_app_href,
),
pht('Applications application'))))
->setDescription(
pht('Array containing list of uninstalled applications.')),
$this->newOption('phabricator.application-settings', 'wild', array())
->setLocked(true)
->setDescription(
pht('Customized settings for Phabricator applications.')),
$this->newOption('phabricator.cache-namespace', 'string', 'phabricator')
->setLocked(true)
->setDescription(pht('Cache namespace.')),
$this->newOption('phabricator.allow-email-users', 'bool', false)
->setBoolOptions(
array(
pht('Allow'),
pht('Disallow'),
))
->setDescription(
pht('Allow non-members to interact with tasks over email.')),
$this->newOption('phabricator.silent', 'bool', false)
->setLocked(true)
->setBoolOptions(
array(
pht('Run Silently'),
pht('Run Normally'),
))
->setSummary(pht('Stop Phabricator from sending any email, etc.'))
- ->setDescription(
- pht(
- 'This option allows you to stop Phabricator from sending '.
- 'any data to external services. Among other things, it will '.
- 'disable email, SMS, repository mirroring, and HTTP hooks.'.
- "\n\n".
- 'This option is intended to allow a Phabricator instance to '.
- 'be exported, copied, imported, and run in a test environment '.
- 'without impacting users. For example, if you are migrating '.
- 'to new hardware, you could perform a test migration first, '.
- 'make sure things work, and then do a production cutover '.
- 'later with higher confidence and less disruption. Without '.
- 'this flag, users would receive duplicate email during the '.
- 'time the test instance and old production instance were '.
- 'both in operation.')),
+ ->setDescription($silent_description),
);
}
protected function didValidateOption(
PhabricatorConfigOption $option,
$value) {
$key = $option->getKey();
if ($key == 'phabricator.base-uri' ||
$key == 'phabricator.production-uri') {
$uri = new PhutilURI($value);
$protocol = $uri->getProtocol();
if ($protocol !== 'http' && $protocol !== 'https') {
throw new PhabricatorConfigValidationException(
pht(
"Config option '%s' is invalid. The URI must start with ".
"%s' or '%s'.",
'http://',
'https://',
$key));
}
$domain = $uri->getDomain();
if (strpos($domain, '.') === false) {
throw new PhabricatorConfigValidationException(
pht(
"Config option '%s' is invalid. The URI must contain a dot ".
"('%s'), like '%s', not just a bare name like '%s'. Some web ".
"browsers will not set cookies on domains with no TLD.",
'.',
'http://example.com/',
'http://example/',
$key));
}
$path = $uri->getPath();
if ($path !== '' && $path !== '/') {
throw new PhabricatorConfigValidationException(
pht(
"Config option '%s' is invalid. The URI must NOT have a path, ".
"e.g. '%s' is OK, but '%s' is not. Phabricator must be installed ".
"on an entire domain; it can not be installed on a path.",
$key,
'http://phabricator.example.com/',
'http://example.com/phabricator/'));
}
}
if ($key === 'phabricator.timezone') {
$old = date_default_timezone_get();
$ok = @date_default_timezone_set($value);
@date_default_timezone_set($old);
if (!$ok) {
throw new PhabricatorConfigValidationException(
pht(
"Config option '%s' is invalid. The timezone identifier must ".
"be a valid timezone identifier recognized by PHP, like '%s'. "."
You can find a list of valid identifiers here: %s",
$key,
'America/Los_Angeles',
'http://php.net/manual/timezones.php'));
}
}
}
}
diff --git a/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php b/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php
index 6c846daa5..8a236a883 100644
--- a/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php
+++ b/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php
@@ -1,329 +1,337 @@
<?php
final class PhabricatorMetaMTAConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Mail');
}
public function getDescription() {
return pht('Configure Mail.');
}
public function getIcon() {
return 'fa-send';
}
public function getGroup() {
return 'core';
}
public function getOptions() {
$send_as_user_desc = $this->deformat(pht(<<<EODOC
When a user takes an action which generates an email notification (like
commenting on a Differential revision), Phabricator can either send that mail
"From" the user's email address (like "alincoln@logcabin.com") or "From" the
'%s' address.
The user experience is generally better if Phabricator uses the user's real
address as the "From" since the messages are easier to organize when they appear
in mail clients, but this will only work if the server is authorized to send
email on behalf of the "From" domain. Practically, this means:
- If you are doing an install for Example Corp and all the users will have
corporate @corp.example.com addresses and any hosts Phabricator is running
on are authorized to send email from corp.example.com, you can enable this
to make the user experience a little better.
- If you are doing an install for an open source project and your users will
be registering via Facebook and using personal email addresses, you probably
should not enable this or all of your outgoing email might vanish into SFP
blackholes.
- If your install is anything else, you're safer leaving this off, at least
initially, since the risk in turning it on is that your outgoing mail will
never arrive.
EODOC
,
'metamta.default-address'));
$one_mail_per_recipient_desc = $this->deformat(pht(<<<EODOC
When a message is sent to multiple recipients (for example, several reviewers on
a code review), Phabricator can either deliver one email to everyone (e.g., "To:
alincoln, usgrant, htaft") or separate emails to each user (e.g., "To:
alincoln", "To: usgrant", "To: htaft"). The major advantages and disadvantages
of each approach are:
- One mail to everyone:
- This violates policy controls. The body of the mail is generated without
respect for object policies.
- Recipients can see To/Cc at a glance.
- If you use mailing lists, you won't get duplicate mail if you're
a normal recipient and also Cc'd on a mailing list.
- Getting threading to work properly is harder, and probably requires
making mail less useful by turning off options.
- Sometimes people will "Reply All", which can send mail to too many
recipients. Phabricator will try not to send mail to users who already
received a similar message, but can not prevent all stray email arising
from "Reply All".
- Not supported with a private reply-to address.
- - Mails are sent in the server default translation.
+ - Mail messages are sent in the server default translation.
+ - Mail that must be delivered over secure channels will leak the recipient
+ list in the "To" and "Cc" headers.
- One mail to each user:
- Policy controls work correctly and are enforced per-user.
- Recipients need to look in the mail body to see To/Cc.
- If you use mailing lists, recipients may sometimes get duplicate
mail.
- Getting threading to work properly is easier, and threading settings
can be customzied by each user.
- "Reply All" will never send extra mail to other users involved in the
thread.
- Required if private reply-to addresses are configured.
- - Mails are sent in the language of user preference.
+ - Mail messages are sent in the language of user preference.
EODOC
));
$herald_hints_description = $this->deformat(pht(<<<EODOC
You can disable the Herald hints in email if users prefer smaller messages.
These are the links under the header "WHY DID I GET THIS EMAIL?". If you set
this to `false`, they will not appear in any mail. Users can still navigate to
the links via the web interface.
EODOC
));
$reply_hints_description = $this->deformat(pht(<<<EODOC
You can disable the hints under "REPLY HANDLER ACTIONS" if users prefer
smaller messages. The actions themselves will still work properly.
EODOC
));
$recipient_hints_description = $this->deformat(pht(<<<EODOC
You can disable the "To:" and "Cc:" footers in mail if users prefer smaller
messages.
EODOC
));
$email_preferences_description = $this->deformat(pht(<<<EODOC
You can disable the email preference link in emails if users prefer smaller
emails.
EODOC
));
$re_prefix_description = $this->deformat(pht(<<<EODOC
Mail.app on OS X Lion won't respect threading headers unless the subject is
prefixed with "Re:". If you enable this option, Phabricator will add "Re:" to
the subject line of all mail which is expected to thread. If you've set
'metamta.one-mail-per-recipient', users can override this setting in their
preferences.
EODOC
));
$vary_subjects_description = $this->deformat(pht(<<<EODOC
If true, allow MetaMTA to change mail subjects to put text like '[Accepted]' and
'[Commented]' in them. This makes subjects more useful, but might break
threading on some clients. If you've set '%s', users can override this setting
in their preferences.
EODOC
,
'metamta.one-mail-per-recipient'));
$reply_to_description = $this->deformat(pht(<<<EODOC
If you enable `%s`, Phabricator uses "From" to authenticate users. You can
additionally enable this setting to try to authenticate with 'Reply-To'. Note
that this is completely spoofable and insecure (any user can set any 'Reply-To'
address) but depending on the nature of your install or other deliverability
conditions this might be okay. Generally, you can't do much more by spoofing
Reply-To than be annoying (you can write but not read content). But this is
still **COMPLETELY INSECURE**.
EODOC
,
'metamta.public-replies'));
- $adapter_doc_href = PhabricatorEnv::getDoclink(
- 'Configuring Outbound Email');
- $adapter_doc_name = pht('Configuring Outbound Email');
$adapter_description = $this->deformat(pht(<<<EODOC
Adapter class to use to transmit mail to the MTA. The default uses
PHPMailerLite, which will invoke "sendmail". This is appropriate if sendmail
actually works on your host, but if you haven't configured mail it may not be so
great. A number of other mailers are available (e.g., SES, SendGrid, SMTP,
-custom mailers) - consult [[ %s | %s ]] for details.
+custom mailers). This option is deprecated in favor of 'cluster.mailers'.
EODOC
- ,
- $adapter_doc_href,
- $adapter_doc_name));
+));
$placeholder_description = $this->deformat(pht(<<<EODOC
-When sending a message that has no To recipient (i.e. all recipients are CC'd,
-for example when multiplexing mail), set the To field to the following value. If
-no value is set, messages with no To will have their CCs upgraded to To.
+When sending a message that has no To recipient (i.e. all recipients are CC'd),
+set the To field to the following value. If no value is set, messages with no
+To will have their CCs upgraded to To.
EODOC
));
$public_replies_description = $this->deformat(pht(<<<EODOC
By default, Phabricator generates unique reply-to addresses and sends a separate
email to each recipient when you enable reply handling. This is more secure than
using "From" to establish user identity, but can mean users may receive multiple
emails when they are on mailing lists. Instead, you can use a single, non-unique
reply to address and authenticate users based on the "From" address by setting
this to 'true'. This trades away a little bit of security for convenience, but
it's reasonable in many installs. Object interactions are still protected using
hashes in the single public email address, so objects can not be replied to
blindly.
EODOC
));
$single_description = $this->deformat(pht(<<<EODOC
If you want to use a single mailbox for Phabricator reply mail, you can use this
and set a common prefix for reply addresses generated by Phabricator. It will
make use of the fact that a mail-address such as
`phabricator+D123+1hjk213h@example.com` will be delivered to the `phabricator`
user's mailbox. Set this to the left part of the email address and it will be
prepended to all generated reply addresses.
For example, if you want to use `phabricator@example.com`, this should be set
to `phabricator`.
EODOC
));
$address_description = $this->deformat(pht(<<<EODOC
When email is sent, what format should Phabricator use for user's email
addresses? Valid values are:
- `short`: 'gwashington <gwashington@example.com>'
- `real`: 'George Washington <gwashington@example.com>'
- `full`: 'gwashington (George Washington) <gwashington@example.com>'
The default is `full`.
EODOC
));
+ $mailers_description = $this->deformat(pht(<<<EODOC
+Define one or more mail transmission services. For help with configuring
+mailers, see **[[ %s | %s ]]** in the documentation.
+EODOC
+ ,
+ PhabricatorEnv::getDoclink('Configuring Outbound Email'),
+ pht('Configuring Outbound Email')));
+
return array(
+ $this->newOption('cluster.mailers', 'cluster.mailers', null)
+ ->setHidden(true)
+ ->setDescription($mailers_description),
$this->newOption(
'metamta.default-address',
'string',
'noreply@phabricator.example.com')
->setDescription(pht('Default "From" address.')),
$this->newOption(
'metamta.domain',
'string',
'phabricator.example.com')
->setDescription(pht('Domain used to generate Message-IDs.')),
$this->newOption(
'metamta.mail-adapter',
'class',
'PhabricatorMailImplementationPHPMailerLiteAdapter')
->setBaseClass('PhabricatorMailImplementationAdapter')
->setSummary(pht('Control how mail is sent.'))
->setDescription($adapter_description),
$this->newOption(
'metamta.one-mail-per-recipient',
'bool',
true)
->setLocked(true)
->setBoolOptions(
array(
pht('Send Mail To Each Recipient'),
pht('Send Mail To All Recipients'),
))
->setSummary(
pht(
'Controls whether Phabricator sends one email with multiple '.
'recipients in the "To:" line, or multiple emails, each with a '.
'single recipient in the "To:" line.'))
->setDescription($one_mail_per_recipient_desc),
$this->newOption('metamta.can-send-as-user', 'bool', false)
->setBoolOptions(
array(
pht('Send as User Taking Action'),
pht('Send as Phabricator'),
))
->setSummary(
pht(
'Controls whether Phabricator sends email "From" users.'))
->setDescription($send_as_user_desc),
$this->newOption(
'metamta.reply-handler-domain',
'string',
null)
->setLocked(true)
->setDescription(pht('Domain used for reply email addresses.'))
->addExample('phabricator.example.com', ''),
$this->newOption('metamta.herald.show-hints', 'bool', true)
->setBoolOptions(
array(
pht('Show Herald Hints'),
pht('No Herald Hints'),
))
->setSummary(pht('Show hints about Herald rules in email.'))
->setDescription($herald_hints_description),
$this->newOption('metamta.recipients.show-hints', 'bool', true)
->setBoolOptions(
array(
pht('Show Recipient Hints'),
pht('No Recipient Hints'),
))
->setSummary(pht('Show "To:" and "Cc:" footer hints in email.'))
->setDescription($recipient_hints_description),
$this->newOption('metamta.email-preferences', 'bool', true)
->setBoolOptions(
array(
pht('Show Email Preferences Link'),
pht('No Email Preferences Link'),
))
->setSummary(pht('Show email preferences link in email.'))
->setDescription($email_preferences_description),
$this->newOption('metamta.insecure-auth-with-reply-to', 'bool', false)
->setBoolOptions(
array(
pht('Allow Insecure Reply-To Auth'),
pht('Disallow Reply-To Auth'),
))
->setSummary(pht('Trust "Reply-To" headers for authentication.'))
->setDescription($reply_to_description),
$this->newOption('metamta.placeholder-to-recipient', 'string', null)
->setSummary(pht('Placeholder for mail with only CCs.'))
->setDescription($placeholder_description),
$this->newOption('metamta.public-replies', 'bool', false)
->setBoolOptions(
array(
pht('Use Public Replies (Less Secure)'),
pht('Use Private Replies (More Secure)'),
))
->setSummary(
pht(
'Phabricator can use less-secure but mailing list friendly public '.
'reply addresses.'))
->setDescription($public_replies_description),
$this->newOption('metamta.single-reply-handler-prefix', 'string', null)
->setSummary(
pht('Allow Phabricator to use a single mailbox for all replies.'))
->setDescription($single_description),
$this->newOption('metamta.user-address-format', 'enum', 'full')
->setEnumOptions(
array(
'short' => pht('Short'),
'real' => pht('Real'),
'full' => pht('Full'),
))
->setSummary(pht('Control how Phabricator renders user names in mail.'))
->setDescription($address_description)
->addExample('gwashington <gwashington@example.com>', 'short')
->addExample('George Washington <gwashington@example.com>', 'real')
->addExample(
'gwashington (George Washington) <gwashington@example.com>',
'full'),
$this->newOption('metamta.email-body-limit', 'int', 524288)
->setDescription(
pht(
'You can set a limit for the maximum byte size of outbound mail. '.
'Mail which is larger than this limit will be truncated before '.
'being sent. This can be useful if your MTA rejects mail which '.
'exceeds some limit (this is reasonably common). Specify a value '.
'in bytes.'))
->setSummary(pht('Global cap for size of generated emails (bytes).'))
->addExample(524288, pht('Truncate at 512KB'))
->addExample(1048576, pht('Truncate at 1MB')),
);
}
}
diff --git a/src/applications/config/option/PhabricatorUIConfigOptions.php b/src/applications/config/option/PhabricatorUIConfigOptions.php
index 7b2823f6b..391ba7e12 100644
--- a/src/applications/config/option/PhabricatorUIConfigOptions.php
+++ b/src/applications/config/option/PhabricatorUIConfigOptions.php
@@ -1,86 +1,90 @@
<?php
final class PhabricatorUIConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('User Interface');
}
public function getDescription() {
return pht('Configure the Phabricator UI, including colors.');
}
public function getIcon() {
return 'fa-magnet';
}
public function getGroup() {
return 'core';
}
public function getOptions() {
$options = array(
'blindigo' => pht('Blindigo'),
'red' => pht('Red'),
'blue' => pht('Blue'),
'green' => pht('Green'),
'indigo' => pht('Indigo'),
'dark' => pht('Dark'),
);
$example = <<<EOJSON
[
{
"name" : "Copyright 2199 Examplecorp"
},
{
"name" : "Privacy Policy",
"href" : "http://www.example.org/privacy/"
},
{
"name" : "Terms and Conditions",
"href" : "http://www.example.org/terms/"
}
]
EOJSON;
$logo_type = 'custom:PhabricatorCustomLogoConfigType';
$footer_type = 'custom:PhabricatorCustomUIFooterConfigType';
return array(
$this->newOption('ui.header-color', 'enum', 'blindigo')
->setDescription(
pht('Sets the default color scheme of Phabricator.'))
->setEnumOptions($options),
$this->newOption('ui.logo', $logo_type, array())
->setSummary(
pht('Customize the logo and wordmark text in the header.'))
->setDescription(
pht(
"Customize the logo image and text which appears in the main ".
"site header:\n\n".
" - **Logo Image**: Upload a new 80 x 80px image to replace the ".
"Phabricator logo in the site header.\n\n".
" - **Wordmark**: Choose new text to display next to the logo. ".
"By default, the header displays //Phabricator//.\n\n")),
+ $this->newOption('ui.favicons', 'wild', array())
+ ->setSummary(pht('Customize favicons.'))
+ ->setDescription(pht('Customize favicons.'))
+ ->setLocked(true),
$this->newOption('ui.footer-items', $footer_type, array())
->setSummary(
pht(
'Allows you to add footer links on most pages.'))
->setDescription(
pht(
"Allows you to add a footer with links in it to most ".
"pages. You might want to use these links to point at legal ".
"information or an about page.\n\n".
"Specify a list of dictionaries. Each dictionary describes ".
"a footer item. These keys are supported:\n\n".
" - `name` The name of the item.\n".
" - `href` Optionally, the link target of the item. You can ".
" omit this if you just want a piece of text, like a copyright ".
" notice."))
->addExample($example, pht('Basic Example')),
);
}
}
diff --git a/src/applications/conpherence/editor/ConpherenceEditor.php b/src/applications/conpherence/editor/ConpherenceEditor.php
index 29ffc2225..3d451e78d 100644
--- a/src/applications/conpherence/editor/ConpherenceEditor.php
+++ b/src/applications/conpherence/editor/ConpherenceEditor.php
@@ -1,312 +1,292 @@
<?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);
}
- /**
- * We really only need a read lock if we have a comment. In that case, we
- * must update the messagesCount field on the conpherence and
- * seenMessagesCount(s) for the participant(s).
- */
- protected function shouldReadLock(
- PhabricatorLiskDAO $object,
- PhabricatorApplicationTransaction $xaction) {
-
- $lock = false;
- switch ($xaction->getTransactionType()) {
- case PhabricatorTransactions::TYPE_COMMENT:
- $lock = true;
- break;
- }
-
- return $lock;
- }
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());
}
- $phid = $object->getPHID();
return id(new PhabricatorMetaMTAMail())
- ->setSubject("Z{$id}: {$title}")
- ->addHeader('Thread-Topic', "Z{$id}: {$phid}");
+ ->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/console/plugin/DarkConsoleErrorLogPlugin.php b/src/applications/console/plugin/DarkConsoleErrorLogPlugin.php
index 0d9604dcb..213b578c7 100644
--- a/src/applications/console/plugin/DarkConsoleErrorLogPlugin.php
+++ b/src/applications/console/plugin/DarkConsoleErrorLogPlugin.php
@@ -1,99 +1,102 @@
<?php
final class DarkConsoleErrorLogPlugin extends DarkConsolePlugin {
public function getName() {
$count = count($this->getData());
if ($count) {
return pht('Error Log (%d)', $count);
}
return pht('Error Log');
}
public function getOrder() {
return 0;
}
public function getColor() {
if (count($this->getData())) {
return '#ff0000';
}
return null;
}
public function getDescription() {
return pht('Shows errors and warnings.');
}
public function generateData() {
return DarkConsoleErrorLogPluginAPI::getErrors();
}
public function renderPanel() {
$data = $this->getData();
$rows = array();
$details = array();
foreach ($data as $index => $row) {
$file = $row['file'];
$line = $row['line'];
- $tag = phutil_tag(
+ $tag = javelin_tag(
'a',
array(
- 'onclick' => jsprintf('show_details(%d)', $index),
+ 'sigil' => 'darkconsole-expand',
+ 'meta' => array(
+ 'expandID' => 'row-details-'.$index,
+ ),
),
$row['str'].' at ['.basename($file).':'.$line.']');
$rows[] = array($tag);
$details[] = hsprintf(
'<div class="dark-console-panel-error-details" id="row-details-%s">'.
"%s\nStack trace:\n",
$index,
$row['details']);
foreach ($row['trace'] as $key => $entry) {
$line = '';
if (isset($entry['class'])) {
$line .= $entry['class'].'::';
}
$line .= idx($entry, 'function', '');
$href = null;
if (isset($entry['file'])) {
$line .= ' called at ['.$entry['file'].':'.$entry['line'].']';
try {
$user = $this->getRequest()->getUser();
$href = $user->loadEditorLink($entry['file'], $entry['line'], null);
} catch (Exception $ex) {
// The database can be inaccessible.
}
}
$details[] = phutil_tag(
'a',
array(
'href' => $href,
),
$line);
$details[] = "\n";
}
$details[] = hsprintf('</div>');
}
$table = new AphrontTableView($rows);
$table->setClassName('error-log');
$table->setHeaders(array(pht('Error')));
$table->setNoDataString(pht('No errors.'));
return phutil_tag(
'div',
array(),
array(
phutil_tag('div', array(), $table->render()),
phutil_tag('pre', array('class' => 'PhabricatorMonospaced'), $details),
));
}
}
diff --git a/src/applications/console/plugin/DarkConsoleServicesPlugin.php b/src/applications/console/plugin/DarkConsoleServicesPlugin.php
index 7d779027d..4a26665e0 100644
--- a/src/applications/console/plugin/DarkConsoleServicesPlugin.php
+++ b/src/applications/console/plugin/DarkConsoleServicesPlugin.php
@@ -1,293 +1,315 @@
<?php
final class DarkConsoleServicesPlugin extends DarkConsolePlugin {
protected $observations;
public function getName() {
return pht('Services');
}
public function getDescription() {
return pht('Information about services.');
}
public static function getQueryAnalyzerHeader() {
return 'X-Phabricator-QueryAnalyzer';
}
public static function isQueryAnalyzerRequested() {
if (!empty($_REQUEST['__analyze__'])) {
return true;
}
$header = AphrontRequest::getHTTPHeader(self::getQueryAnalyzerHeader());
if ($header) {
return true;
}
return false;
}
+ public function didStartup() {
+ $should_analyze = self::isQueryAnalyzerRequested();
+
+ if ($should_analyze) {
+ PhutilServiceProfiler::getInstance()
+ ->setCollectStackTraces(true);
+ }
+
+ return null;
+ }
+
+
/**
* @phutil-external-symbol class PhabricatorStartup
*/
public function generateData() {
$should_analyze = self::isQueryAnalyzerRequested();
$log = PhutilServiceProfiler::getInstance()->getServiceCallLog();
foreach ($log as $key => $entry) {
$config = idx($entry, 'config', array());
unset($log[$key]['config']);
if (!$should_analyze) {
$log[$key]['explain'] = array(
'sev' => 7,
'size' => null,
'reason' => pht('Disabled'),
);
// Query analysis is disabled for this request, so don't do any of it.
continue;
}
if ($entry['type'] != 'query') {
continue;
}
// For each SELECT query, go issue an EXPLAIN on it so we can flag stuff
// causing table scans, etc.
if (preg_match('/^\s*SELECT\b/i', $entry['query'])) {
$conn = PhabricatorDatabaseRef::newRawConnection($entry['config']);
try {
$explain = queryfx_all(
$conn,
'EXPLAIN %Q',
$entry['query']);
$badness = 0;
$size = 1;
$reason = null;
foreach ($explain as $table) {
$size *= (int)$table['rows'];
switch ($table['type']) {
case 'index':
$cur_badness = 1;
$cur_reason = 'Index';
break;
case 'const':
$cur_badness = 1;
$cur_reason = 'Const';
break;
case 'eq_ref';
$cur_badness = 2;
$cur_reason = 'EqRef';
break;
case 'range':
$cur_badness = 3;
$cur_reason = 'Range';
break;
case 'ref':
$cur_badness = 3;
$cur_reason = 'Ref';
break;
case 'fulltext':
$cur_badness = 3;
$cur_reason = 'Fulltext';
break;
case 'ALL':
if (preg_match('/Using where/', $table['Extra'])) {
if ($table['rows'] < 256 && !empty($table['possible_keys'])) {
$cur_badness = 2;
$cur_reason = pht('Small Table Scan');
} else {
$cur_badness = 6;
$cur_reason = pht('TABLE SCAN!');
}
} else {
$cur_badness = 3;
$cur_reason = pht('Whole Table');
}
break;
default:
if (preg_match('/No tables used/i', $table['Extra'])) {
$cur_badness = 1;
$cur_reason = pht('No Tables');
} else if (preg_match('/Impossible/i', $table['Extra'])) {
$cur_badness = 1;
$cur_reason = pht('Empty');
} else {
$cur_badness = 4;
$cur_reason = pht("Can't Analyze");
}
break;
}
if ($cur_badness > $badness) {
$badness = $cur_badness;
$reason = $cur_reason;
}
}
$log[$key]['explain'] = array(
'sev' => $badness,
'size' => $size,
'reason' => $reason,
);
} catch (Exception $ex) {
$log[$key]['explain'] = array(
'sev' => 5,
'size' => null,
'reason' => $ex->getMessage(),
);
}
}
}
return array(
'start' => PhabricatorStartup::getStartTime(),
'end' => microtime(true),
'log' => $log,
'analyzeURI' => (string)$this
->getRequestURI()
->alter('__analyze__', true),
'didAnalyze' => $should_analyze,
);
}
public function renderPanel() {
$data = $this->getData();
$log = $data['log'];
$results = array();
$results[] = phutil_tag(
'div',
array('class' => 'dark-console-panel-header'),
array(
phutil_tag(
'a',
array(
'href' => $data['analyzeURI'],
'class' => $data['didAnalyze'] ?
'disabled button' : 'button button-green',
),
pht('Analyze Query Plans')),
phutil_tag('h1', array(), pht('Calls to External Services')),
phutil_tag('div', array('style' => 'clear: both;')),
));
$page_total = $data['end'] - $data['start'];
$totals = array();
$counts = array();
foreach ($log as $row) {
$totals[$row['type']] = idx($totals, $row['type'], 0) + $row['duration'];
$counts[$row['type']] = idx($counts, $row['type'], 0) + 1;
}
$totals['All Services'] = array_sum($totals);
$counts['All Services'] = array_sum($counts);
$totals['Entire Page'] = $page_total;
$counts['Entire Page'] = 0;
$summary = array();
foreach ($totals as $type => $total) {
$summary[] = array(
$type,
number_format($counts[$type]),
pht('%s us', new PhutilNumber((int)(1000000 * $totals[$type]))),
sprintf('%.1f%%', 100 * $totals[$type] / $page_total),
);
}
$summary_table = new AphrontTableView($summary);
$summary_table->setColumnClasses(
array(
'',
'n',
'n',
'wide',
));
$summary_table->setHeaders(
array(
pht('Type'),
pht('Count'),
pht('Total Cost'),
pht('Page Weight'),
));
$results[] = $summary_table->render();
$rows = array();
foreach ($log as $row) {
$analysis = null;
switch ($row['type']) {
case 'query':
$info = $row['query'];
$info = wordwrap($info, 128, "\n", true);
if (!empty($row['explain'])) {
$analysis = phutil_tag(
'span',
array(
'class' => 'explain-sev-'.$row['explain']['sev'],
),
$row['explain']['reason']);
}
break;
case 'connect':
$info = $row['host'].':'.$row['database'];
break;
case 'exec':
$info = $row['command'];
break;
case 's3':
case 'conduit':
$info = $row['method'];
break;
case 'http':
$info = $row['uri'];
break;
default:
$info = '-';
break;
}
$offset = ($row['begin'] - $data['start']);
$rows[] = array(
$row['type'],
pht('+%s ms', new PhutilNumber(1000 * $offset)),
pht('%s us', new PhutilNumber(1000000 * $row['duration'])),
$info,
$analysis,
);
+
+ if (isset($row['trace'])) {
+ $rows[] = array(
+ null,
+ null,
+ null,
+ $row['trace'],
+ null,
+ );
+ }
}
$table = new AphrontTableView($rows);
$table->setColumnClasses(
array(
null,
'n',
'n',
- 'wide',
+ 'wide prewrap',
'',
));
$table->setHeaders(
array(
pht('Event'),
pht('Start'),
pht('Duration'),
pht('Details'),
pht('Analysis'),
));
$results[] = $table->render();
return phutil_implode_html("\n", $results);
}
}
diff --git a/src/applications/countdown/editor/PhabricatorCountdownEditor.php b/src/applications/countdown/editor/PhabricatorCountdownEditor.php
index 322b2ee2c..2102b3785 100644
--- a/src/applications/countdown/editor/PhabricatorCountdownEditor.php
+++ b/src/applications/countdown/editor/PhabricatorCountdownEditor.php
@@ -1,97 +1,96 @@
<?php
final class PhabricatorCountdownEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorCountdownApplication';
}
public function getEditorObjectsDescription() {
return pht('Countdown');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_EDGE;
$types[] = PhabricatorTransactions::TYPE_SPACE;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
$types[] = PhabricatorTransactions::TYPE_COMMENT;
return $types;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
public function getMailTagsMap() {
return array(
PhabricatorCountdownTransaction::MAILTAG_DETAILS =>
pht('Someone changes the countdown details.'),
PhabricatorCountdownTransaction::MAILTAG_COMMENT =>
pht('Someone comments on a countdown.'),
PhabricatorCountdownTransaction::MAILTAG_OTHER =>
pht('Other countdown activity not listed above occurs.'),
);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$monogram = $object->getMonogram();
$name = $object->getTitle();
return id(new PhabricatorMetaMTAMail())
- ->setSubject("{$monogram}: {$name}")
- ->addHeader('Thread-Topic', $monogram);
+ ->setSubject("{$monogram}: {$name}");
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
$description = $object->getDescription();
if (strlen($description)) {
$body->addRemarkupSection(
pht('COUNTDOWN DESCRIPTION'),
$object->getDescription());
}
$body->addLinkSection(
pht('COUNTDOWN DETAIL'),
PhabricatorEnv::getProductionURI('/'.$object->getMonogram()));
return $body;
}
protected function getMailTo(PhabricatorLiskDAO $object) {
return array(
$object->getAuthorPHID(),
$this->requireActor()->getPHID(),
);
}
protected function getMailSubjectPrefix() {
return '[Countdown]';
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new PhabricatorCountdownReplyHandler())
->setMailReceiver($object);
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function supportsSearch() {
return true;
}
}
diff --git a/src/applications/daemon/controller/PhabricatorDaemonBulkJobViewController.php b/src/applications/daemon/controller/PhabricatorDaemonBulkJobViewController.php
index f7aa396e9..00fb297fe 100644
--- a/src/applications/daemon/controller/PhabricatorDaemonBulkJobViewController.php
+++ b/src/applications/daemon/controller/PhabricatorDaemonBulkJobViewController.php
@@ -1,89 +1,81 @@
<?php
final class PhabricatorDaemonBulkJobViewController
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();
}
$title = pht('Bulk Job %d', $job->getID());
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($title);
$crumbs->setBorder(true);
$properties = $this->renderProperties($job);
$curtain = $this->buildCurtainView($job);
$box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Details'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->addPropertyList($properties);
$timeline = $this->buildTransactionTimeline(
$job,
new PhabricatorWorkerBulkJobTransactionQuery());
$timeline->setShouldTerminate(true);
$header = id(new PHUIHeaderView())
->setHeader($title)
->setHeaderIcon('fa-hourglass');
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setCurtain($curtain)
->setMainColumn(array(
$box,
$timeline,
));
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
}
private function renderProperties(PhabricatorWorkerBulkJob $job) {
$viewer = $this->getViewer();
$view = id(new PHUIPropertyListView())
->setUser($viewer)
->setObject($job);
$view->addProperty(
pht('Author'),
$viewer->renderHandle($job->getAuthorPHID()));
$view->addProperty(pht('Status'), $job->getStatusName());
return $view;
}
private function buildCurtainView(PhabricatorWorkerBulkJob $job) {
$viewer = $this->getViewer();
$curtain = $this->newCurtainView($job);
- if ($job->isConfirming()) {
- $continue_uri = $job->getMonitorURI();
- } else {
- $continue_uri = $job->getDoneURI();
+ foreach ($job->getCurtainActions($viewer) as $action) {
+ $curtain->addAction($action);
}
- $curtain->addAction(
- id(new PhabricatorActionView())
- ->setHref($continue_uri)
- ->setIcon('fa-arrow-circle-o-right')
- ->setName(pht('Continue')));
-
return $curtain;
}
}
diff --git a/src/applications/daemon/garbagecollector/PhabricatorDaemonLockLogGarbageCollector.php b/src/applications/daemon/garbagecollector/PhabricatorDaemonLockLogGarbageCollector.php
new file mode 100644
index 000000000..1fc62b986
--- /dev/null
+++ b/src/applications/daemon/garbagecollector/PhabricatorDaemonLockLogGarbageCollector.php
@@ -0,0 +1,29 @@
+<?php
+
+final class PhabricatorDaemonLockLogGarbageCollector
+ extends PhabricatorGarbageCollector {
+
+ const COLLECTORCONST = 'daemon.lock-log';
+
+ public function getCollectorName() {
+ return pht('Lock Logs');
+ }
+
+ public function getDefaultRetentionPolicy() {
+ return 0;
+ }
+
+ protected function collectGarbage() {
+ $table = new PhabricatorDaemonLockLog();
+ $conn = $table->establishConnection('w');
+
+ queryfx(
+ $conn,
+ 'DELETE FROM %T WHERE dateCreated < %d LIMIT 100',
+ $table->getTableName(),
+ $this->getGarbageEpoch());
+
+ return ($conn->getAffectedRows() == 100);
+ }
+
+}
diff --git a/src/applications/daemon/management/PhabricatorLockLogManagementWorkflow.php b/src/applications/daemon/management/PhabricatorLockLogManagementWorkflow.php
new file mode 100644
index 000000000..5602bda30
--- /dev/null
+++ b/src/applications/daemon/management/PhabricatorLockLogManagementWorkflow.php
@@ -0,0 +1,222 @@
+<?php
+
+final class PhabricatorLockLogManagementWorkflow
+ extends PhabricatorLockManagementWorkflow {
+
+ protected function didConstruct() {
+ $this
+ ->setName('log')
+ ->setSynopsis(pht('Enable, disable, or show the lock log.'))
+ ->setArguments(
+ array(
+ array(
+ 'name' => 'enable',
+ 'help' => pht('Enable the lock log.'),
+ ),
+ array(
+ 'name' => 'disable',
+ 'help' => pht('Disable the lock log.'),
+ ),
+ array(
+ 'name' => 'name',
+ 'param' => 'name',
+ 'help' => pht('Review logs for a specific lock.'),
+ ),
+ ));
+ }
+
+ public function execute(PhutilArgumentParser $args) {
+ $is_enable = $args->getArg('enable');
+ $is_disable = $args->getArg('disable');
+
+ if ($is_enable && $is_disable) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'You can not both "--enable" and "--disable" the lock log.'));
+ }
+
+ $with_name = $args->getArg('name');
+
+ if ($is_enable || $is_disable) {
+ if (strlen($with_name)) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'You can not both "--enable" or "--disable" with search '.
+ 'parameters like "--name".'));
+ }
+
+ $gc = new PhabricatorDaemonLockLogGarbageCollector();
+ $is_enabled = (bool)$gc->getRetentionPolicy();
+
+ $config_key = 'phd.garbage-collection';
+ $const = $gc->getCollectorConstant();
+ $value = PhabricatorEnv::getEnvConfig($config_key);
+
+ if ($is_disable) {
+ if (!$is_enabled) {
+ echo tsprintf(
+ "%s\n",
+ pht('Lock log is already disabled.'));
+ return 0;
+ }
+ echo tsprintf(
+ "%s\n",
+ pht('Disabling the lock log.'));
+
+ unset($value[$const]);
+ } else {
+ if ($is_enabled) {
+ echo tsprintf(
+ "%s\n",
+ pht('Lock log is already enabled.'));
+ return 0;
+ }
+ echo tsprintf(
+ "%s\n",
+ pht('Enabling the lock log.'));
+
+ $value[$const] = phutil_units('24 hours in seconds');
+ }
+
+ id(new PhabricatorConfigLocalSource())
+ ->setKeys(
+ array(
+ $config_key => $value,
+ ));
+
+ echo tsprintf(
+ "%s\n",
+ pht('Done.'));
+
+ echo tsprintf(
+ "%s\n",
+ pht('Restart daemons to apply changes.'));
+
+ return 0;
+ }
+
+ $table = new PhabricatorDaemonLockLog();
+ $conn = $table->establishConnection('r');
+
+ $parts = array();
+ if (strlen($with_name)) {
+ $parts[] = qsprintf(
+ $conn,
+ 'lockName = %s',
+ $with_name);
+ }
+
+ if (!$parts) {
+ $constraint = '1 = 1';
+ } else {
+ $constraint = '('.implode(') AND (', $parts).')';
+ }
+
+ $logs = $table->loadAllWhere(
+ '%Q ORDER BY id DESC LIMIT 100',
+ $constraint);
+ $logs = array_reverse($logs);
+
+ if (!$logs) {
+ echo tsprintf(
+ "%s\n",
+ pht('No matching lock logs.'));
+ return 0;
+ }
+
+ $table = id(new PhutilConsoleTable())
+ ->setBorders(true)
+ ->addColumn(
+ 'id',
+ array(
+ 'title' => pht('Lock'),
+ ))
+ ->addColumn(
+ 'name',
+ array(
+ 'title' => pht('Name'),
+ ))
+ ->addColumn(
+ 'acquired',
+ array(
+ 'title' => pht('Acquired'),
+ ))
+ ->addColumn(
+ 'released',
+ array(
+ 'title' => pht('Released'),
+ ))
+ ->addColumn(
+ 'held',
+ array(
+ 'title' => pht('Held'),
+ ))
+ ->addColumn(
+ 'parameters',
+ array(
+ 'title' => pht('Parameters'),
+ ))
+ ->addColumn(
+ 'context',
+ array(
+ 'title' => pht('Context'),
+ ));
+
+ $viewer = $this->getViewer();
+
+ foreach ($logs as $log) {
+ $created = $log->getDateCreated();
+ $released = $log->getLockReleased();
+
+ if ($released) {
+ $held = '+'.($released - $created);
+ } else {
+ $held = null;
+ }
+
+ $created = phabricator_datetime($created, $viewer);
+ $released = phabricator_datetime($released, $viewer);
+
+ $parameters = $log->getLockParameters();
+ $context = $log->getLockContext();
+
+ $table->addRow(
+ array(
+ 'id' => $log->getID(),
+ 'name' => $log->getLockName(),
+ 'acquired' => $created,
+ 'released' => $released,
+ 'held' => $held,
+ 'parameters' => $this->flattenParameters($parameters),
+ 'context' => $this->flattenParameters($context),
+ ));
+ }
+
+ $table->draw();
+
+ return 0;
+ }
+
+ private function flattenParameters(array $params, $keys = true) {
+ $flat = array();
+ foreach ($params as $key => $value) {
+ if (is_array($value)) {
+ $value = $this->flattenParameters($value, false);
+ }
+ if ($keys) {
+ $flat[] = "{$key}={$value}";
+ } else {
+ $flat[] = "{$value}";
+ }
+ }
+
+ if ($keys) {
+ $flat = implode(', ', $flat);
+ } else {
+ $flat = implode(' ', $flat);
+ }
+
+ return $flat;
+ }
+
+}
diff --git a/src/applications/daemon/management/PhabricatorLockManagementWorkflow.php b/src/applications/daemon/management/PhabricatorLockManagementWorkflow.php
new file mode 100644
index 000000000..e791f5109
--- /dev/null
+++ b/src/applications/daemon/management/PhabricatorLockManagementWorkflow.php
@@ -0,0 +1,4 @@
+<?php
+
+abstract class PhabricatorLockManagementWorkflow
+ extends PhabricatorManagementWorkflow {}
diff --git a/src/applications/daemon/storage/PhabricatorDaemonLockLog.php b/src/applications/daemon/storage/PhabricatorDaemonLockLog.php
new file mode 100644
index 000000000..f9d80138c
--- /dev/null
+++ b/src/applications/daemon/storage/PhabricatorDaemonLockLog.php
@@ -0,0 +1,32 @@
+<?php
+
+final class PhabricatorDaemonLockLog
+ extends PhabricatorDaemonDAO {
+
+ protected $lockName;
+ protected $lockReleased;
+ protected $lockParameters = array();
+ protected $lockContext = array();
+
+ protected function getConfiguration() {
+ return array(
+ self::CONFIG_SERIALIZATION => array(
+ 'lockParameters' => self::SERIALIZATION_JSON,
+ 'lockContext' => self::SERIALIZATION_JSON,
+ ),
+ self::CONFIG_COLUMN_SCHEMA => array(
+ 'lockName' => 'text64',
+ 'lockReleased' => 'epoch?',
+ ),
+ self::CONFIG_KEY_SCHEMA => array(
+ 'key_lock' => array(
+ 'columns' => array('lockName'),
+ ),
+ 'key_created' => array(
+ 'columns' => array('dateCreated'),
+ ),
+ ),
+ ) + parent::getConfiguration();
+ }
+
+}
diff --git a/src/applications/dashboard/storage/PhabricatorDashboard.php b/src/applications/dashboard/storage/PhabricatorDashboard.php
index b79fded93..2e673de19 100644
--- a/src/applications/dashboard/storage/PhabricatorDashboard.php
+++ b/src/applications/dashboard/storage/PhabricatorDashboard.php
@@ -1,194 +1,202 @@
<?php
/**
* A collection of dashboard panels with a specific layout.
*/
final class PhabricatorDashboard extends PhabricatorDashboardDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorFlaggableInterface,
PhabricatorDestructibleInterface,
PhabricatorProjectInterface,
PhabricatorNgramsInterface {
protected $name;
protected $authorPHID;
protected $viewPolicy;
protected $editPolicy;
protected $status;
protected $icon;
protected $layoutConfig = array();
const STATUS_ACTIVE = 'active';
const STATUS_ARCHIVED = 'archived';
private $panelPHIDs = self::ATTACHABLE;
private $panels = self::ATTACHABLE;
private $edgeProjectPHIDs = self::ATTACHABLE;
public static function initializeNewDashboard(PhabricatorUser $actor) {
return id(new PhabricatorDashboard())
->setName('')
->setIcon('fa-dashboard')
->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy())
->setEditPolicy($actor->getPHID())
->setStatus(self::STATUS_ACTIVE)
->setAuthorPHID($actor->getPHID())
->attachPanels(array())
->attachPanelPHIDs(array());
}
public static function getStatusNameMap() {
return array(
self::STATUS_ACTIVE => pht('Active'),
self::STATUS_ARCHIVED => pht('Archived'),
);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'layoutConfig' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'sort255',
'status' => 'text32',
'icon' => 'text32',
'authorPHID' => 'phid',
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorDashboardDashboardPHIDType::TYPECONST);
}
public function getLayoutConfigObject() {
return PhabricatorDashboardLayoutConfig::newFromDictionary(
$this->getLayoutConfig());
}
public function setLayoutConfigFromObject(
PhabricatorDashboardLayoutConfig $object) {
+
$this->setLayoutConfig($object->toDictionary());
+
+ // See PHI385. Dashboard panel mutations rely on changes to the Dashboard
+ // object persisting when transactions are applied, but this assumption is
+ // no longer valid after T13054. For now, just save the dashboard
+ // explicitly.
+ $this->save();
+
return $this;
}
public function getProjectPHIDs() {
return $this->assertAttached($this->edgeProjectPHIDs);
}
public function attachProjectPHIDs(array $phids) {
$this->edgeProjectPHIDs = $phids;
return $this;
}
public function attachPanelPHIDs(array $phids) {
$this->panelPHIDs = $phids;
return $this;
}
public function getPanelPHIDs() {
return $this->assertAttached($this->panelPHIDs);
}
public function attachPanels(array $panels) {
assert_instances_of($panels, 'PhabricatorDashboardPanel');
$this->panels = $panels;
return $this;
}
public function getPanels() {
return $this->assertAttached($this->panels);
}
public function isArchived() {
return ($this->getStatus() == self::STATUS_ARCHIVED);
}
public function getViewURI() {
return '/dashboard/view/'.$this->getID().'/';
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorDashboardTransactionEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorDashboardTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$installs = id(new PhabricatorDashboardInstall())->loadAllWhere(
'dashboardPHID = %s',
$this->getPHID());
foreach ($installs as $install) {
$install->delete();
}
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorNgramInterface )------------------------------------------ */
public function newNgrams() {
return array(
id(new PhabricatorDashboardNgrams())
->setValue($this->getName()),
);
}
}
diff --git a/src/applications/dashboard/typeahead/PhabricatorDashboardPanelDatasource.php b/src/applications/dashboard/typeahead/PhabricatorDashboardPanelDatasource.php
index d534d32ad..8bbdb1a5e 100644
--- a/src/applications/dashboard/typeahead/PhabricatorDashboardPanelDatasource.php
+++ b/src/applications/dashboard/typeahead/PhabricatorDashboardPanelDatasource.php
@@ -1,64 +1,66 @@
<?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 = 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($impl->getIcon())
+ ->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 1c8926f58..da4073957 100644
--- a/src/applications/differential/application/PhabricatorDifferentialApplication.php
+++ b/src/applications/differential/application/PhabricatorDifferentialApplication.php
@@ -1,150 +1,150 @@
<?php
final class PhabricatorDifferentialApplication extends PhabricatorApplication {
public function getBaseURI() {
return '/differential/';
}
public function getName() {
return pht('Differential');
}
public function getMenuName() {
return pht('Code Review');
}
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 getFactObjectsForAnalysis() {
- return array(
- new DifferentialRevision(),
- );
- }
-
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',
'/differential/' => array(
'(?:query/(?P<queryKey>[^/]+)/)?'
=> 'DifferentialRevisionListController',
'diff/' => array(
- '(?P<id>[1-9]\d*)/' => 'DifferentialDiffViewController',
+ '(?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/conduit/DifferentialConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialConduitAPIMethod.php
index 34843b560..13ed71c96 100644
--- a/src/applications/differential/conduit/DifferentialConduitAPIMethod.php
+++ b/src/applications/differential/conduit/DifferentialConduitAPIMethod.php
@@ -1,195 +1,195 @@
<?php
abstract class DifferentialConduitAPIMethod extends ConduitAPIMethod {
final public function getApplication() {
return PhabricatorApplication::getByClass(
'PhabricatorDifferentialApplication');
}
protected function buildDiffInfoDictionary(DifferentialDiff $diff) {
$uri = '/differential/diff/'.$diff->getID().'/';
$uri = PhabricatorEnv::getProductionURI($uri);
return array(
'id' => $diff->getID(),
'phid' => $diff->getPHID(),
'uri' => $uri,
);
}
protected function buildInlineInfoDictionary(
DifferentialInlineComment $inline,
DifferentialChangeset $changeset = null) {
$file_path = null;
$diff_id = null;
if ($changeset) {
$file_path = $inline->getIsNewFile()
? $changeset->getFilename()
: $changeset->getOldFile();
$diff_id = $changeset->getDiffID();
}
return array(
'id' => $inline->getID(),
'authorPHID' => $inline->getAuthorPHID(),
'filePath' => $file_path,
'isNewFile' => $inline->getIsNewFile(),
'lineNumber' => $inline->getLineNumber(),
'lineLength' => $inline->getLineLength(),
'diffID' => $diff_id,
'content' => $inline->getContent(),
);
}
protected function applyFieldEdit(
ConduitAPIRequest $request,
DifferentialRevision $revision,
DifferentialDiff $diff,
array $fields,
$message) {
$viewer = $request->getUser();
// We're going to build the body of a "differential.revision.edit" API
// request, then just call that code directly.
$xactions = array();
$xactions[] = array(
- 'type' => DifferentialRevisionEditEngine::KEY_UPDATE,
+ 'type' => DifferentialRevisionUpdateTransaction::EDITKEY,
'value' => $diff->getPHID(),
);
$field_map = DifferentialCommitMessageField::newEnabledFields($viewer);
$values = $request->getValue('fields', array());
foreach ($values as $key => $value) {
$field = idx($field_map, $key);
if (!$field) {
// NOTE: We're just ignoring fields we don't know about. This isn't
// ideal, but the way the workflow currently works involves us getting
// several read-only fields, like the revision ID field, which we should
// just skip.
continue;
}
// The transaction itself will be validated so this is somewhat
// redundant, but this validator will sometimes give us a better error
// message or a better reaction to a bad value type.
$value = $field->readFieldValueFromConduit($value);
foreach ($field->getFieldTransactions($value) as $xaction) {
$xactions[] = $xaction;
}
}
$message = $request->getValue('message');
if (strlen($message)) {
// This is a little awkward, and should move elsewhere or be removed. It
// largely exists for legacy reasons. See some discussion in T7899.
$first_line = head(phutil_split_lines($message, false));
$first_line = id(new PhutilUTF8StringTruncator())
->setMaximumBytes(250)
->setMaximumGlyphs(80)
->truncateString($first_line);
$diff->setDescription($first_line);
$diff->save();
$xactions[] = array(
'type' => PhabricatorCommentEditEngineExtension::EDITKEY,
'value' => $message,
);
}
$method = 'differential.revision.edit';
$params = array(
'transactions' => $xactions,
);
if ($revision->getID()) {
$params['objectIdentifier'] = $revision->getID();
}
return id(new ConduitCall($method, $params, $strict = true))
->setUser($viewer)
->execute();
}
protected function loadCustomFieldsForRevisions(
PhabricatorUser $viewer,
array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
if (!$revisions) {
return array();
}
$field_lists = array();
foreach ($revisions as $revision) {
$revision_phid = $revision->getPHID();
$field_list = PhabricatorCustomField::getObjectFields(
$revision,
PhabricatorCustomField::ROLE_CONDUIT);
$field_list
->setViewer($viewer)
->readFieldsFromObject($revision);
$field_lists[$revision_phid] = $field_list;
}
$all_fields = array();
foreach ($field_lists as $field_list) {
foreach ($field_list->getFields() as $field) {
$all_fields[] = $field;
}
}
id(new PhabricatorCustomFieldStorageQuery())
->addFields($all_fields)
->execute();
$results = array();
foreach ($field_lists as $revision_phid => $field_list) {
$results[$revision_phid] = array();
foreach ($field_list->getFields() as $field) {
$field_key = $field->getFieldKeyForConduit();
$value = $field->getConduitDictionaryValue();
$results[$revision_phid][$field_key] = $value;
}
}
// For compatibility, fill in these "custom fields" by querying for them
// efficiently. See T11404 for discussion.
$legacy_edge_map = array(
'phabricator:projects' =>
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
'phabricator:depends-on' =>
DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST,
);
$query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array_keys($results))
->withEdgeTypes($legacy_edge_map);
$query->execute();
foreach ($results as $revision_phid => $dict) {
foreach ($legacy_edge_map as $edge_key => $edge_type) {
$phid_list = $query->getDestinationPHIDs(
array($revision_phid),
array($edge_type));
$results[$revision_phid][$edge_key] = $phid_list;
}
}
return $results;
}
}
diff --git a/src/applications/differential/conduit/DifferentialCreateRawDiffConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialCreateRawDiffConduitAPIMethod.php
index 6f8c2e864..ae55135e9 100644
--- a/src/applications/differential/conduit/DifferentialCreateRawDiffConduitAPIMethod.php
+++ b/src/applications/differential/conduit/DifferentialCreateRawDiffConduitAPIMethod.php
@@ -1,96 +1,93 @@
<?php
final class DifferentialCreateRawDiffConduitAPIMethod
extends DifferentialConduitAPIMethod {
public function getAPIMethodName() {
return 'differential.createrawdiff';
}
public function getMethodDescription() {
return pht('Create a new Differential diff from a raw diff source.');
}
protected function defineParamTypes() {
return array(
'diff' => 'required string',
'repositoryPHID' => 'optional string',
'viewPolicy' => 'optional string',
);
}
protected function defineReturnType() {
return 'nonempty dict';
}
protected function execute(ConduitAPIRequest $request) {
$viewer = $request->getUser();
$raw_diff = $request->getValue('diff');
$repository_phid = $request->getValue('repositoryPHID');
if ($repository_phid) {
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->withPHIDs(array($repository_phid))
->executeOne();
if (!$repository) {
throw new Exception(
pht('No such repository "%s"!', $repository_phid));
}
}
$parser = new ArcanistDiffParser();
$changes = $parser->parseDiff($raw_diff);
$diff = DifferentialDiff::newFromRawChanges($viewer, $changes);
// We're bounded by doing INSERTs for all the hunks and changesets, so
// estimate the number of inserts we'll require.
$size = 0;
foreach ($diff->getChangesets() as $changeset) {
$hunks = $changeset->getHunks();
$size += 1 + count($hunks);
}
$raw_limit = 10000;
if ($size > $raw_limit) {
throw new Exception(
pht(
'The raw diff you have submitted is too large to parse (it affects '.
- 'more than %s paths and hunks). Differential should only be used '.
- 'for changes which are small enough to receive detailed human '.
- 'review. See "Differential User Guide: Large Changes" in the '.
- 'documentation for more information.',
+ 'more than %s paths and hunks).',
new PhutilNumber($raw_limit)));
}
$diff_data_dict = array(
'creationMethod' => 'web',
'authorPHID' => $viewer->getPHID(),
'repositoryPHID' => $repository_phid,
'lintStatus' => DifferentialLintStatus::LINT_SKIP,
'unitStatus' => DifferentialUnitStatus::UNIT_SKIP,
);
$xactions = array(
id(new DifferentialDiffTransaction())
->setTransactionType(DifferentialDiffTransaction::TYPE_DIFF_CREATE)
->setNewValue($diff_data_dict),
);
if ($request->getValue('viewPolicy')) {
$xactions[] = id(new DifferentialDiffTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
->setNewValue($request->getValue('viewPolicy'));
}
id(new DifferentialDiffEditor())
->setActor($viewer)
->setContentSource($request->newContentSource())
->setContinueOnNoEffect(true)
->setLookupRepository(false) // respect user choice
->applyTransactions($diff, $xactions);
return $this->buildDiffInfoDictionary($diff);
}
}
diff --git a/src/applications/differential/constants/DifferentialRevisionStatus.php b/src/applications/differential/constants/DifferentialRevisionStatus.php
index 70f0e33b2..e3acace3f 100644
--- a/src/applications/differential/constants/DifferentialRevisionStatus.php
+++ b/src/applications/differential/constants/DifferentialRevisionStatus.php
@@ -1,184 +1,184 @@
<?php
final class DifferentialRevisionStatus extends Phobject {
const NEEDS_REVIEW = 'needs-review';
const NEEDS_REVISION = 'needs-revision';
const CHANGES_PLANNED = 'changes-planned';
const ACCEPTED = 'accepted';
const PUBLISHED = 'published';
const ABANDONED = 'abandoned';
const DRAFT = 'draft';
private $key;
private $spec = array();
public function getKey() {
return $this->key;
}
public function getLegacyKey() {
return idx($this->spec, 'legacy');
}
public function getIcon() {
return idx($this->spec, 'icon');
}
public function getIconColor() {
return idx($this->spec, 'color.icon', 'bluegrey');
}
public function getTagColor() {
return idx($this->spec, 'color.tag', 'bluegrey');
}
public function getTimelineIcon() {
return idx($this->spec, 'icon.timeline');
}
public function getTimelineColor() {
return idx($this->spec, 'color.timeline');
}
public function getANSIColor() {
return idx($this->spec, 'color.ansi');
}
public function getDisplayName() {
return idx($this->spec, 'name');
}
public function isClosedStatus() {
return idx($this->spec, 'closed');
}
public function isAbandoned() {
return ($this->key === self::ABANDONED);
}
public function isAccepted() {
return ($this->key === self::ACCEPTED);
}
public function isNeedsReview() {
return ($this->key === self::NEEDS_REVIEW);
}
public function isNeedsRevision() {
return ($this->key === self::NEEDS_REVISION);
}
public function isPublished() {
return ($this->key === self::PUBLISHED);
}
public function isChangePlanned() {
return ($this->key === self::CHANGES_PLANNED);
}
public function isDraft() {
return ($this->key === self::DRAFT);
}
public static function newForStatus($status) {
$result = new self();
$map = self::getMap();
if (isset($map[$status])) {
$result->key = $status;
$result->spec = $map[$status];
}
return $result;
}
public static function getAll() {
$result = array();
foreach (self::getMap() as $key => $spec) {
$result[$key] = self::newForStatus($key);
}
return $result;
}
private static function getMap() {
$close_on_accept = PhabricatorEnv::getEnvConfig(
'differential.close-on-accept');
return array(
self::NEEDS_REVIEW => array(
'name' => pht('Needs Review'),
'legacy' => 0,
'icon' => 'fa-code',
'icon.timeline' => 'fa-undo',
'closed' => false,
'color.icon' => 'grey',
'color.tag' => 'bluegrey',
'color.ansi' => 'magenta',
'color.timeline' => 'orange',
),
self::NEEDS_REVISION => array(
'name' => pht('Needs Revision'),
'legacy' => 1,
'icon' => 'fa-refresh',
'icon.timeline' => 'fa-times',
'closed' => false,
'color.icon' => 'red',
'color.tag' => 'red',
'color.ansi' => 'red',
'color.timeline' => 'red',
),
self::CHANGES_PLANNED => array(
'name' => pht('Changes Planned'),
'legacy' => 5,
'icon' => 'fa-headphones',
'closed' => false,
'color.icon' => 'red',
'color.tag' => 'red',
'color.ansi' => 'red',
),
self::ACCEPTED => array(
'name' => pht('Accepted'),
'legacy' => 2,
'icon' => 'fa-check',
'icon.timeline' => 'fa-check',
'closed' => $close_on_accept,
'color.icon' => 'green',
'color.tag' => 'green',
'color.ansi' => 'green',
'color.timeline' => 'green',
),
self::PUBLISHED => array(
'name' => pht('Closed'),
'legacy' => 3,
'icon' => 'fa-check-square-o',
'closed' => true,
'color.icon' => 'black',
'color.tag' => 'indigo',
'color.ansi' => 'cyan',
),
self::ABANDONED => array(
'name' => pht('Abandoned'),
'legacy' => 4,
'icon' => 'fa-plane',
'closed' => true,
'color.icon' => 'black',
'color.tag' => 'indigo',
'color.ansi' => null,
),
self::DRAFT => array(
'name' => pht('Draft'),
// For legacy clients, treat this as though it is "Needs Review".
'legacy' => 0,
- 'icon' => 'fa-file-text-o',
+ 'icon' => 'fa-spinner',
'closed' => false,
- 'color.icon' => 'grey',
- 'color.tag' => 'grey',
+ 'color.icon' => 'pink',
+ 'color.tag' => 'pink',
'color.ansi' => null,
),
);
}
}
diff --git a/src/applications/differential/controller/DifferentialChangesetListController.php b/src/applications/differential/controller/DifferentialChangesetListController.php
new file mode 100644
index 000000000..aa807da9e
--- /dev/null
+++ b/src/applications/differential/controller/DifferentialChangesetListController.php
@@ -0,0 +1,51 @@
+<?php
+
+final class DifferentialChangesetListController
+ extends DifferentialController {
+
+ private $diff;
+
+ public function shouldAllowPublic() {
+ return true;
+ }
+
+ public function handleRequest(AphrontRequest $request) {
+ $viewer = $this->getViewer();
+
+ $diff = id(new DifferentialDiffQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($request->getURIData('id')))
+ ->executeOne();
+ if (!$diff) {
+ return new Aphront404Response();
+ }
+ $this->diff = $diff;
+
+ return id(new DifferentialChangesetSearchEngine())
+ ->setController($this)
+ ->setDiff($diff)
+ ->buildResponse();
+ }
+
+ protected function buildApplicationCrumbs() {
+ $crumbs = parent::buildApplicationCrumbs();
+
+ $diff = $this->diff;
+ if ($diff) {
+ $revision = $diff->getRevision();
+ if ($revision) {
+ $crumbs->addTextCrumb(
+ $revision->getMonogram(),
+ $revision->getURI());
+ }
+
+ $crumbs->addTextCrumb(
+ pht('Diff %d', $diff->getID()),
+ $diff->getURI());
+ }
+
+ return $crumbs;
+ }
+
+
+}
diff --git a/src/applications/differential/controller/DifferentialChangesetViewController.php b/src/applications/differential/controller/DifferentialChangesetViewController.php
index c2bd4bf8d..9d02202f9 100644
--- a/src/applications/differential/controller/DifferentialChangesetViewController.php
+++ b/src/applications/differential/controller/DifferentialChangesetViewController.php
@@ -1,445 +1,455 @@
<?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)
->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(1)
- ->setLineLength(0);
+ ->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/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php
index e1da5ed32..9b015887a 100644
--- a/src/applications/differential/controller/DifferentialRevisionViewController.php
+++ b/src/applications/differential/controller/DifferentialRevisionViewController.php
@@ -1,1149 +1,1213 @@
<?php
final class DifferentialRevisionViewController extends DifferentialController {
private $revisionID;
+ private $veryLargeDiff;
public function shouldAllowPublic() {
return true;
}
+ public function isVeryLargeDiff() {
+ return $this->veryLargeDiff;
+ }
+
+ public function getVeryLargeDiffLimit() {
+ return 1000;
+ }
+
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));
$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;
+ }
+
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);
$request_uri = $request->getRequestURI();
$limit = 100;
$large = $request->getStr('large');
if (count($changesets) > $limit && !$large) {
$count = count($changesets);
$warning = new PHUIInfoView();
- $warning->setTitle(pht('Very Large Diff'));
+ $warning->setTitle(pht('Large Diff'));
$warning->setSeverity(PHUIInfoView::SEVERITY_WARNING);
$warning->appendChild(hsprintf(
'%s <strong>%s</strong>',
pht(
- 'This diff is very large and affects %s files. '.
+ 'This diff is large and affects %s files. '.
'You may load each file individually or ',
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();
$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];
}
}
} else {
$warning = null;
$visible_changesets = $changesets;
}
$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);
} 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());
- $changeset_view = id(new DifferentialChangesetListView())
- ->setChangesets($changesets)
- ->setVisibleChangesets($visible_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);
+ 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)
+ ->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);
- }
+ $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().'/');
+ 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) {
+ 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);
- $toc_view = $this->buildTableOfContents(
- $changesets,
- $visible_changesets,
- $target->loadCoverageMap($viewer));
+ if ($this->isVeryLargeDiff()) {
+ $toc_view = null;
+ } else {
+ $toc_view = $this->buildTableOfContents(
+ $changesets,
+ $visible_changesets,
+ $target->loadCoverageMap($viewer));
+ }
// Attach changesets to each reviewer so we can show which Owners package
// reviewers own no files.
foreach ($revision->getReviewers() as $reviewer) {
$reviewer_phid = $reviewer->getReviewerPHID();
$reviewer_changesets = $this->getPackageChangesets($reviewer_phid);
$reviewer->attachChangesets($reviewer_changesets);
}
- $tab_group = id(new PHUITabGroupView())
- ->addTab(
+ $tab_group = id(new PHUITabGroupView());
+
+ if ($toc_view) {
+ $tab_group->addTab(
id(new PHUITabView())
->setName(pht('Files'))
->setKey('files')
- ->appendChild($toc_view))
+ ->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));
$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())
- ->setHeaderText(pht('Revision Contents'))
+ ->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,
$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) {
+ 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);
+
$nav = id(new DifferentialChangesetFileTreeSideNavBuilder())
->setTitle($monogram)
->setBaseURI(new PhutilURI($revision->getURI()))
->setCollapsed((bool)$collapsed_value)
+ ->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->getStatusIconColor())
+ ->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) {
$load_diffs = array($target);
if ($diff_vs) {
$load_diffs[] = $diff_vs;
}
$raw_changesets = id(new DifferentialChangesetQuery())
->setViewer($this->getRequest()->getUser())
->withDiffs($load_diffs)
->execute();
$changeset_groups = mgroup($raw_changesets, 'getDiffID');
$changesets = idx($changeset_groups, $target->getID(), array());
$changesets = mpull($changesets, null, 'getID');
$refs = array();
$vs_map = array();
$vs_changesets = array();
if ($diff_vs) {
$vs_id = $diff_vs->getID();
$vs_changesets_path_map = array();
foreach (idx($changeset_groups, $vs_id, array()) as $changeset) {
$path = $changeset->getAbsoluteRepositoryPath($repository, $diff_vs);
$vs_changesets_path_map[$path] = $changeset;
$vs_changesets[$changeset->getID()] = $changeset;
}
foreach ($changesets as $key => $changeset) {
$path = $changeset->getAbsoluteRepositoryPath($repository, $target);
if (isset($vs_changesets_path_map[$path])) {
$vs_map[$changeset->getID()] =
$vs_changesets_path_map[$path]->getID();
$refs[$changeset->getID()] =
$changeset->getID().'/'.$vs_changesets_path_map[$path]->getID();
unset($vs_changesets_path_map[$path]);
} else {
$refs[$changeset->getID()] = $changeset->getID();
}
}
foreach ($vs_changesets_path_map as $path => $changeset) {
$changesets[$changeset->getID()] = $changeset;
$vs_map[$changeset->getID()] = -1;
$refs[$changeset->getID()] = $changeset->getID().'/-1';
}
} else {
foreach ($changesets as $changeset) {
$refs[$changeset->getID()] = $changeset->getID();
}
}
$changesets = msort($changesets, 'getSortKey');
return array($changesets, $vs_map, $vs_changesets, $refs);
}
private function buildSymbolIndexes(
PhabricatorRepository $repository,
array $visible_changesets) {
assert_instances_of($visible_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) {
$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'));
- $view = id(new DifferentialRevisionListView())
+ return id(new DifferentialRevisionListView())
+ ->setViewer($viewer)
->setRevisions($revisions)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
- ->setNoBox(true)
- ->setUser($viewer);
-
- $phids = $view->getRequiredHandlePHIDs();
- $handles = $this->loadViewerHandles($phids);
- $view->setHandles($handles);
-
- return $view;
+ ->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)
->setExcuse($excuse)
->setBuildable($diff->getBuildable())
->setUnitMessages($diff->getUnitMessages())
->setLimit(5)
->setShowViewAll(true);
}
}
diff --git a/src/applications/differential/customfield/DifferentialDraftField.php b/src/applications/differential/customfield/DifferentialDraftField.php
index 5d625e3ce..f89007590 100644
--- a/src/applications/differential/customfield/DifferentialDraftField.php
+++ b/src/applications/differential/customfield/DifferentialDraftField.php
@@ -1,96 +1,116 @@
<?php
final class DifferentialDraftField
extends DifferentialCoreCustomField {
public function getFieldKey() {
return 'differential:draft';
}
public function getFieldName() {
return pht('Draft');
}
public function getFieldDescription() {
return pht('Show a warning about draft revisions.');
}
protected function readValueFromRevision(
DifferentialRevision $revision) {
return null;
}
public function shouldAppearInPropertyView() {
return true;
}
public function renderPropertyViewValue(array $handles) {
return null;
}
public function getWarningsForRevisionHeader(array $handles) {
$viewer = $this->getViewer();
$revision = $this->getObject();
if (!$revision->isDraft()) {
return array();
}
// If the author has held this revision as a draft explicitly, don't
- // show any misleading messages about it autosubmitting later.
+ // show any misleading messages about it autosubmitting later. We do show
+ // reminder text.
if ($revision->getHoldAsDraft()) {
- return array();
+ return array(
+ pht(
+ 'This is a draft revision that has not yet been submitted for '.
+ 'review.'),
+ );
}
$warnings = array();
$blocking_map = array(
HarbormasterBuildStatus::STATUS_FAILED,
HarbormasterBuildStatus::STATUS_ABORTED,
HarbormasterBuildStatus::STATUS_ERROR,
HarbormasterBuildStatus::STATUS_PAUSED,
HarbormasterBuildStatus::STATUS_DEADLOCKED,
);
$blocking_map = array_fuse($blocking_map);
- $builds = $revision->loadActiveBuilds($viewer);
+ $builds = $revision->loadImpactfulBuilds($viewer);
$waiting = array();
$blocking = array();
foreach ($builds as $build) {
if (isset($blocking_map[$build->getBuildStatus()])) {
$blocking[] = $build;
} else {
$waiting[] = $build;
}
}
$blocking_list = $viewer->renderHandleList(mpull($blocking, 'getPHID'))
->setAsInline(true);
$waiting_list = $viewer->renderHandleList(mpull($waiting, 'getPHID'))
->setAsInline(true);
if ($blocking) {
$warnings[] = pht(
'This draft revision will not be submitted for review because %s '.
'build(s) failed: %s.',
phutil_count($blocking),
$blocking_list);
$warnings[] = pht(
'Fix build failures and update the revision.');
} else if ($waiting) {
$warnings[] = pht(
'This draft revision will be sent for review once %s '.
'build(s) pass: %s.',
phutil_count($waiting),
$waiting_list);
} else {
$warnings[] = pht(
'This is a draft revision that has not yet been submitted for '.
'review.');
}
return $warnings;
}
+ public function getWarningsForDetailView() {
+ $revision = $this->getObject();
+
+ if ($revision->getShouldBroadcast()) {
+ return array();
+ }
+
+ return array(
+ pht(
+ 'This revision is currently a draft. You can leave comments, but '.
+ 'no one will be notified until the revision is submitted for '.
+ 'review.'),
+ );
+ }
+
}
diff --git a/src/applications/differential/customfield/DifferentialUnitField.php b/src/applications/differential/customfield/DifferentialUnitField.php
index 7b3d491cb..4c1eb72cc 100644
--- a/src/applications/differential/customfield/DifferentialUnitField.php
+++ b/src/applications/differential/customfield/DifferentialUnitField.php
@@ -1,100 +1,100 @@
<?php
final class DifferentialUnitField
extends DifferentialCustomField {
public function getFieldKey() {
return 'differential:unit';
}
public function getFieldName() {
return pht('Unit');
}
public function getFieldDescription() {
return pht('Shows unit test results.');
}
public function shouldAppearInPropertyView() {
return true;
}
public function renderPropertyViewValue(array $handles) {
return null;
}
public function shouldAppearInDiffPropertyView() {
return true;
}
public function renderDiffPropertyViewLabel(DifferentialDiff $diff) {
return $this->getFieldName();
}
public function getWarningsForDetailView() {
$warnings = array();
$viewer = $this->getViewer();
$diff = $this->getObject()->getActiveDiff();
$buildable = id(new HarbormasterBuildableQuery())
->setViewer($viewer)
->withBuildablePHIDs(array($diff->getPHID()))
->withManualBuildables(false)
->executeOne();
if ($buildable) {
switch ($buildable->getBuildableStatus()) {
- case HarbormasterBuildable::STATUS_BUILDING:
+ case HarbormasterBuildableStatus::STATUS_BUILDING:
$warnings[] = pht(
'These changes have not finished building yet and may have build '.
'failures.');
break;
- case HarbormasterBuildable::STATUS_FAILED:
+ case HarbormasterBuildableStatus::STATUS_FAILED:
$warnings[] = pht(
'These changes have failed to build.');
break;
}
}
$status = $this->getObject()->getActiveDiff()->getUnitStatus();
if ($status < DifferentialUnitStatus::UNIT_WARN) {
// Don't show any warnings.
} else if ($status == DifferentialUnitStatus::UNIT_AUTO_SKIP) {
// Don't show any warnings.
} else if ($status == DifferentialUnitStatus::UNIT_SKIP) {
$warnings[] = pht(
'Unit tests were skipped when generating these changes.');
} else {
$warnings[] = pht('These changes have unit test problems.');
}
return $warnings;
}
public function renderDiffPropertyViewValue(DifferentialDiff $diff) {
$colors = array(
DifferentialUnitStatus::UNIT_NONE => 'grey',
DifferentialUnitStatus::UNIT_OKAY => 'green',
DifferentialUnitStatus::UNIT_WARN => 'yellow',
DifferentialUnitStatus::UNIT_FAIL => 'red',
DifferentialUnitStatus::UNIT_SKIP => 'blue',
DifferentialUnitStatus::UNIT_AUTO_SKIP => 'blue',
);
$icon_color = idx($colors, $diff->getUnitStatus(), 'grey');
$message = DifferentialRevisionUpdateHistoryView::getDiffUnitMessage(
$diff->getUnitStatus());
$status = id(new PHUIStatusListView())
->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_STAR, $icon_color)
->setTarget($message));
return $status;
}
}
diff --git a/src/applications/differential/editor/DifferentialRevisionEditEngine.php b/src/applications/differential/editor/DifferentialRevisionEditEngine.php
index 2bd3a5cdc..0404bd620 100644
--- a/src/applications/differential/editor/DifferentialRevisionEditEngine.php
+++ b/src/applications/differential/editor/DifferentialRevisionEditEngine.php
@@ -1,339 +1,346 @@
<?php
final class DifferentialRevisionEditEngine
extends PhabricatorEditEngine {
private $diff;
const ENGINECONST = 'differential.revision';
- const KEY_UPDATE = 'update';
-
const ACTIONGROUP_REVIEW = 'review';
const ACTIONGROUP_REVISION = 'revision';
public function getEngineName() {
return pht('Revisions');
}
public function getSummaryHeader() {
return pht('Configure Revision Forms');
}
public function getSummaryText() {
return pht(
'Configure creation and editing revision forms in Differential.');
}
public function getEngineApplicationClass() {
return 'PhabricatorDifferentialApplication';
}
public function isEngineConfigurable() {
return false;
}
protected function newEditableObject() {
$viewer = $this->getViewer();
return DifferentialRevision::initializeNewRevision($viewer);
}
protected function newObjectQuery() {
return id(new DifferentialRevisionQuery())
->needActiveDiffs(true)
->needReviewers(true)
->needReviewerAuthority(true);
}
protected function getObjectCreateTitleText($object) {
return pht('Create New Revision');
}
protected function getObjectEditTitleText($object) {
$monogram = $object->getMonogram();
$title = $object->getTitle();
$diff = $this->getDiff();
if ($diff) {
return pht('Update Revision %s: %s', $monogram, $title);
} else {
return pht('Edit Revision %s: %s', $monogram, $title);
}
}
protected function getObjectEditShortText($object) {
return $object->getMonogram();
}
protected function getObjectCreateShortText() {
return pht('Create Revision');
}
protected function getObjectName() {
return pht('Revision');
}
+ protected function getCommentViewButtonText($object) {
+ if ($object->isDraft()) {
+ return pht('Submit Quietly');
+ }
+
+ return parent::getCommentViewButtonText($object);
+ }
+
protected function getObjectViewURI($object) {
return $object->getURI();
}
protected function getEditorURI() {
return $this->getApplication()->getApplicationURI('revision/edit/');
}
public function setDiff(DifferentialDiff $diff) {
$this->diff = $diff;
return $this;
}
public function getDiff() {
return $this->diff;
}
protected function newCommentActionGroups() {
return array(
id(new PhabricatorEditEngineCommentActionGroup())
->setKey(self::ACTIONGROUP_REVIEW)
->setLabel(pht('Review Actions')),
id(new PhabricatorEditEngineCommentActionGroup())
->setKey(self::ACTIONGROUP_REVISION)
->setLabel(pht('Revision Actions')),
);
}
protected function buildCustomEditFields($object) {
$viewer = $this->getViewer();
$plan_required = PhabricatorEnv::getEnvConfig(
'differential.require-test-plan-field');
$plan_enabled = $this->isCustomFieldEnabled(
$object,
'differential:test-plan');
$diff = $this->getDiff();
if ($diff) {
$diff_phid = $diff->getPHID();
} else {
$diff_phid = null;
}
$is_create = $this->getIsCreate();
$is_update = ($diff && !$is_create);
$fields = array();
$fields[] = id(new PhabricatorHandlesEditField())
- ->setKey(self::KEY_UPDATE)
+ ->setKey(DifferentialRevisionUpdateTransaction::EDITKEY)
->setLabel(pht('Update Diff'))
->setDescription(pht('New diff to create or update the revision with.'))
->setConduitDescription(pht('Create or update a revision with a diff.'))
->setConduitTypeDescription(pht('PHID of the diff.'))
- ->setTransactionType(DifferentialTransaction::TYPE_UPDATE)
+ ->setTransactionType(
+ DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE)
->setHandleParameterType(new AphrontPHIDListHTTPParameterType())
->setSingleValue($diff_phid)
->setIsConduitOnly(!$diff)
->setIsReorderable(false)
->setIsDefaultable(false)
->setIsInvisible(true)
->setIsLockable(false);
if ($is_update) {
$fields[] = id(new PhabricatorInstructionsEditField())
->setKey('update.help')
->setValue(pht('Describe the updates you have made to the diff.'));
$fields[] = id(new PhabricatorCommentEditField())
->setKey('update.comment')
->setLabel(pht('Comment'))
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->setIsWebOnly(true)
->setDescription(pht('Comments providing context for the update.'));
$fields[] = id(new PhabricatorSubmitEditField())
->setKey('update.submit')
->setValue($this->getObjectEditButtonText($object));
$fields[] = id(new PhabricatorDividerEditField())
->setKey('update.note');
}
$fields[] = id(new PhabricatorTextEditField())
->setKey(DifferentialRevisionTitleTransaction::EDITKEY)
->setLabel(pht('Title'))
->setIsRequired(true)
->setTransactionType(
DifferentialRevisionTitleTransaction::TRANSACTIONTYPE)
->setDescription(pht('The title of the revision.'))
->setConduitDescription(pht('Retitle the revision.'))
->setConduitTypeDescription(pht('New revision title.'))
->setValue($object->getTitle());
$fields[] = id(new PhabricatorRemarkupEditField())
->setKey(DifferentialRevisionSummaryTransaction::EDITKEY)
->setLabel(pht('Summary'))
->setTransactionType(
DifferentialRevisionSummaryTransaction::TRANSACTIONTYPE)
->setDescription(pht('The summary of the revision.'))
->setConduitDescription(pht('Change the revision summary.'))
->setConduitTypeDescription(pht('New revision summary.'))
->setValue($object->getSummary());
if ($plan_enabled) {
$fields[] = id(new PhabricatorRemarkupEditField())
->setKey(DifferentialRevisionTestPlanTransaction::EDITKEY)
->setLabel(pht('Test Plan'))
->setIsRequired($plan_required)
->setTransactionType(
DifferentialRevisionTestPlanTransaction::TRANSACTIONTYPE)
->setDescription(
pht('Actions performed to verify the behavior of the change.'))
->setConduitDescription(pht('Update the revision test plan.'))
->setConduitTypeDescription(pht('New test plan.'))
->setValue($object->getTestPlan());
}
$fields[] = id(new PhabricatorDatasourceEditField())
->setKey(DifferentialRevisionReviewersTransaction::EDITKEY)
->setLabel(pht('Reviewers'))
->setDatasource(new DifferentialReviewerDatasource())
->setUseEdgeTransactions(true)
->setTransactionType(
DifferentialRevisionReviewersTransaction::TRANSACTIONTYPE)
->setCommentActionLabel(pht('Change Reviewers'))
->setDescription(pht('Reviewers for this revision.'))
->setConduitDescription(pht('Change the reviewers for this revision.'))
->setConduitTypeDescription(pht('New reviewers.'))
->setValue($object->getReviewerPHIDsForEdit());
$fields[] = id(new PhabricatorDatasourceEditField())
->setKey('repositoryPHID')
->setLabel(pht('Repository'))
->setDatasource(new DiffusionRepositoryDatasource())
->setTransactionType(
DifferentialRevisionRepositoryTransaction::TRANSACTIONTYPE)
->setDescription(pht('The repository the revision belongs to.'))
->setConduitDescription(pht('Change the repository for this revision.'))
->setConduitTypeDescription(pht('New repository.'))
->setSingleValue($object->getRepositoryPHID());
// This is a little flimsy, but allows "Maniphest Tasks: ..." to continue
// working properly in commit messages until we fully sort out T5873.
$fields[] = id(new PhabricatorHandlesEditField())
->setKey('tasks')
->setUseEdgeTransactions(true)
->setIsConduitOnly(true)
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue(
'edge:type',
DifferentialRevisionHasTaskEdgeType::EDGECONST)
->setDescription(pht('Tasks associated with this revision.'))
->setConduitDescription(pht('Change associated tasks.'))
->setConduitTypeDescription(pht('List of tasks.'))
->setValue(array());
$actions = DifferentialRevisionActionTransaction::loadAllActions();
$actions = msortv($actions, 'getRevisionActionOrderVector');
foreach ($actions as $key => $action) {
$fields[] = $action->newEditField($object, $viewer);
}
$fields[] = id(new PhabricatorBoolEditField())
->setKey('draft')
->setLabel(pht('Hold as Draft'))
->setIsConduitOnly(true)
->setOptions(
pht('Autosubmit Once Builds Finish'),
pht('Hold as Draft'))
->setTransactionType(
DifferentialRevisionHoldDraftTransaction::TRANSACTIONTYPE)
->setDescription(pht('Hold revision as as draft.'))
->setConduitDescription(
pht(
'Change autosubmission from draft state after builds finish.'))
->setConduitTypeDescription(pht('New "Hold as Draft" setting.'))
->setValue($object->getHoldAsDraft());
return $fields;
}
private function isCustomFieldEnabled(DifferentialRevision $revision, $key) {
$field_list = PhabricatorCustomField::getObjectFields(
$revision,
PhabricatorCustomField::ROLE_VIEW);
$fields = $field_list->getFields();
return isset($fields[$key]);
}
protected function newAutomaticCommentTransactions($object) {
$viewer = $this->getViewer();
$xactions = array();
$inlines = DifferentialTransactionQuery::loadUnsubmittedInlineComments(
$viewer,
$object);
$inlines = msort($inlines, 'getID');
foreach ($inlines as $inline) {
$xactions[] = id(new DifferentialTransaction())
->setTransactionType(DifferentialTransaction::TYPE_INLINE)
->attachComment($inline);
}
$viewer_phid = $viewer->getPHID();
$viewer_is_author = ($object->getAuthorPHID() == $viewer_phid);
if ($viewer_is_author) {
$state_map = PhabricatorTransactions::getInlineStateMap();
$inlines = id(new DifferentialDiffInlineCommentQuery())
->setViewer($viewer)
->withRevisionPHIDs(array($object->getPHID()))
->withFixedStates(array_keys($state_map))
->execute();
if ($inlines) {
$old_value = mpull($inlines, 'getFixedState', 'getPHID');
$new_value = array();
foreach ($old_value as $key => $state) {
$new_value[$key] = $state_map[$state];
}
$xactions[] = id(new DifferentialTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_INLINESTATE)
->setIgnoreOnNoEffect(true)
->setOldValue($old_value)
->setNewValue($new_value);
}
}
return $xactions;
}
protected function newCommentPreviewContent($object, array $xactions) {
$viewer = $this->getViewer();
$type_inline = DifferentialTransaction::TYPE_INLINE;
$inlines = array();
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() === $type_inline) {
$inlines[] = $xaction->getComment();
}
}
$content = array();
if ($inlines) {
$inline_preview = id(new PHUIDiffInlineCommentPreviewListView())
->setViewer($viewer)
->setInlineComments($inlines);
$content[] = phutil_tag(
'div',
array(
'id' => 'inline-comment-preview',
),
$inline_preview);
}
return $content;
}
}
diff --git a/src/applications/differential/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php
index ecd1e6c95..24f0fe7a0 100644
--- a/src/applications/differential/editor/DifferentialTransactionEditor.php
+++ b/src/applications/differential/editor/DifferentialTransactionEditor.php
@@ -1,1696 +1,1666 @@
<?php
final class DifferentialTransactionEditor
extends PhabricatorApplicationTransactionEditor {
private $changedPriorToCommitURI;
private $isCloseByCommit;
private $repositoryPHIDOverride = false;
private $didExpandInlineState = false;
- private $hasReviewTransaction = false;
private $affectedPaths;
private $firstBroadcast = false;
- private $wasDraft = false;
+ private $wasBroadcasting;
+ private $isDraftDemotion;
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 = DifferentialTransaction::TYPE_UPDATE;
+ $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;
- $types[] = DifferentialTransaction::TYPE_UPDATE;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_INLINE:
return null;
- case DifferentialTransaction::TYPE_UPDATE:
- if ($this->getIsNewObject()) {
- return null;
- } else {
- return $object->getActiveDiff()->getPHID();
- }
}
return parent::getCustomTransactionOldValue($object, $xaction);
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
- case DifferentialTransaction::TYPE_UPDATE:
- return $xaction->getNewValue();
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;
- case DifferentialTransaction::TYPE_UPDATE:
- if (!$this->getIsCloseByCommit()) {
- if ($object->isNeedsRevision() ||
- $object->isChangePlanned() ||
- $object->isAbandoned()) {
- $object->setModernRevisionStatus(
- DifferentialRevisionStatus::NEEDS_REVIEW);
- }
- }
-
- $diff = $this->requireDiff($xaction->getNewValue());
-
- $this->updateRevisionLineCounts($object, $diff);
-
- if ($this->repositoryPHIDOverride !== false) {
- $object->setRepositoryPHID($this->repositoryPHIDOverride);
- } else {
- $object->setRepositoryPHID($diff->getRepositoryPHID());
- }
-
- $object->attachActiveDiff($diff);
- $object->setActiveDiffPHID($diff->getPHID());
- 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 DifferentialRevisionAcceptTransaction::TRANSACTIONTYPE:
- case DifferentialRevisionRejectTransaction::TRANSACTIONTYPE:
- case DifferentialRevisionResignTransaction::TRANSACTIONTYPE:
- // If we have a review transaction, we'll skip marking the user
- // as "Commented" later. This should get cleaner after T10967.
- $this->hasReviewTransaction = true;
+ case DifferentialRevisionPlanChangesTransaction::TRANSACTIONTYPE:
+ if ($xaction->getMetadataValue('draft.demote')) {
+ $this->isDraftDemotion = true;
+ }
break;
}
}
- $this->wasDraft = $object->isDraft();
+ $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;
- $is_sticky_accept = PhabricatorEnv::getEnvConfig(
- 'differential.sticky-accept');
-
- $downgrade_rejects = false;
- $downgrade_accepts = false;
+ $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 DifferentialTransaction::TYPE_UPDATE:
- $downgrade_rejects = true;
- if (!$is_sticky_accept) {
- // If "sticky accept" is disabled, also downgrade the accepts.
- $downgrade_accepts = true;
- }
+ case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE:
+ $want_downgrade[] = DifferentialReviewerStatus::STATUS_ACCEPTED;
+ $want_downgrade[] = DifferentialReviewerStatus::STATUS_REJECTED;
break;
case DifferentialRevisionRequestReviewTransaction::TRANSACTIONTYPE:
- $downgrade_rejects = true;
- if ((!$is_sticky_accept) ||
- (!$object->isChangePlanned())) {
- // If the old state isn't "changes planned", downgrade the accepts.
- // This exception allows an accepted revision to go through
- // "Plan Changes" -> "Request Review" and return to "accepted" if
- // the author didn't update the revision, essentially undoing the
- // "Plan Changes".
- $downgrade_accepts = true;
+ 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;
}
}
- $new_accept = DifferentialReviewerStatus::STATUS_ACCEPTED;
- $new_reject = DifferentialReviewerStatus::STATUS_REJECTED;
- $old_accept = DifferentialReviewerStatus::STATUS_ACCEPTED_OLDER;
- $old_reject = DifferentialReviewerStatus::STATUS_REJECTED_OLDER;
-
- $downgrade = array();
- if ($downgrade_accepts) {
- $downgrade[] = DifferentialReviewerStatus::STATUS_ACCEPTED;
- }
-
- if ($downgrade_rejects) {
- $downgrade[] = DifferentialReviewerStatus::STATUS_REJECTED;
- }
-
- if ($downgrade) {
+ if ($want_downgrade) {
$void_type = DifferentialRevisionVoidTransaction::TRANSACTIONTYPE;
$results[] = id(new DifferentialTransaction())
->setTransactionType($void_type)
->setIgnoreOnNoEffect(true)
- ->setNewValue($downgrade);
+ ->setMetadataValue('void.force', $must_downgrade)
+ ->setNewValue($want_downgrade);
}
$is_commandeer = false;
switch ($xaction->getTransactionType()) {
- case DifferentialTransaction::TYPE_UPDATE:
+ 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 DifferentialTransaction::TYPE_UPDATE:
+ 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;
}
$state_map = PhabricatorTransactions::getInlineStateMap();
$inlines = id(new DifferentialDiffInlineCommentQuery())
->setViewer($this->getActor())
->withRevisionPHIDs(array($object->getPHID()))
->withFixedStates(array_keys($state_map))
->execute();
if (!$inlines) {
break;
}
$old_value = mpull($inlines, 'getFixedState', 'getPHID');
$new_value = array();
foreach ($old_value as $key => $state) {
$new_value[$key] = $state_map[$state];
}
$results[] = id(new DifferentialTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_INLINESTATE)
->setIgnoreOnNoEffect(true)
->setOldValue($old_value)
->setNewValue($new_value);
break;
}
}
return $results;
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_INLINE:
$reply = $xaction->getComment()->getReplyToComment();
if ($reply && !$reply->getHasReplies()) {
$reply->setHasReplies(1)->save();
}
return;
- case DifferentialTransaction::TYPE_UPDATE:
- // Now that we're inside the transaction, do a final check.
- $diff = $this->requireDiff($xaction->getNewValue());
-
- // TODO: It would be slightly cleaner to just revalidate this
- // transaction somehow using the same validation code, but that's
- // not easy to do at the moment.
-
- $revision_id = $diff->getRevisionID();
- if ($revision_id && ($revision_id != $object->getID())) {
- throw new Exception(
- pht(
- 'Diff is already attached to another revision. You lost '.
- 'a race?'));
- }
-
- // TODO: This can race with diff updates, particularly those from
- // Harbormaster. See discussion in T8650.
- $diff->setRevisionID($object->getID());
- $diff->save();
-
- // Update Harbormaster to set the containerPHID correctly for any
- // existing buildables. We may otherwise have buildables stuck with
- // the old (`null`) container.
-
- // TODO: This is a bit iffy, maybe we can find a cleaner approach?
- // In particular, this could (rarely) be overwritten by Harbormaster
- // workers.
- $table = new HarbormasterBuildable();
- $conn_w = $table->establishConnection('w');
- queryfx(
- $conn_w,
- 'UPDATE %T SET containerPHID = %s WHERE buildablePHID = %s',
- $table->getTableName(),
- $object->getPHID(),
- $diff->getPHID());
-
- return;
}
return parent::applyCustomExternalTransaction($object, $xaction);
}
protected function applyBuiltinExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_INLINESTATE:
$table = new DifferentialTransactionComment();
$conn_w = $table->establishConnection('w');
foreach ($xaction->getNewValue() as $phid => $state) {
queryfx(
$conn_w,
'UPDATE %T SET fixedState = %s WHERE phid = %s',
$table->getTableName(),
$state,
$phid);
}
break;
}
return parent::applyBuiltinExternalTransaction($object, $xaction);
}
protected function 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 DifferentialTransaction::TYPE_UPDATE:
+ 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 validateTransaction(
- PhabricatorLiskDAO $object,
- $type,
- array $xactions) {
-
- $errors = parent::validateTransaction($object, $type, $xactions);
-
- $config_self_accept_key = 'differential.allow-self-accept';
- $allow_self_accept = PhabricatorEnv::getEnvConfig($config_self_accept_key);
-
- foreach ($xactions as $xaction) {
- switch ($type) {
- case DifferentialTransaction::TYPE_UPDATE:
- $diff = $this->loadDiff($xaction->getNewValue());
- if (!$diff) {
- $errors[] = new PhabricatorApplicationTransactionValidationError(
- $type,
- pht('Invalid'),
- pht('The specified diff does not exist.'),
- $xaction);
- } else if (($diff->getRevisionID()) &&
- ($diff->getRevisionID() != $object->getID())) {
- $errors[] = new PhabricatorApplicationTransactionValidationError(
- $type,
- pht('Invalid'),
- pht(
- 'You can not update this revision to the specified diff, '.
- 'because the diff is already attached to another revision.'),
- $xaction);
- }
- break;
- }
- }
-
- return $errors;
- }
-
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->shouldBroadcast()) {
+ if (!$object->getShouldBroadcast()) {
return false;
}
return true;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
+ return true;
+ }
- if (!$object->shouldBroadcast()) {
- return false;
+ 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;
}
- return true;
+ // 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 getMailTo(PhabricatorLiskDAO $object) {
+ protected function getMailCC(PhabricatorLiskDAO $object) {
+ if (!$object->getShouldBroadcast()) {
+ return array();
+ }
+
+ return parent::getMailCC($object);
+ }
+
+ protected function newMailUnexpandablePHIDs(PhabricatorLiskDAO $object) {
+ $this->requireReviewers($object);
+
$phids = array();
- $phids[] = $object->getAuthorPHID();
+
foreach ($object->getReviewers() as $reviewer) {
if ($reviewer->isResigned()) {
- continue;
+ $phids[] = $reviewer->getReviewerPHID();
}
-
- $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 = DifferentialTransaction::TYPE_UPDATE;
+ $type_update = DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE;
if ($strongest->getTransactionType() == $type_update) {
$show_lines = true;
}
}
if ($show_lines) {
$count = new PhutilNumber($object->getLineCount());
- $action = pht('%s, %s line(s)', $action, $count);
+ $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();
$title = $object->getTitle();
-
- $original_title = $object->getOriginalTitle();
-
$subject = "D{$id}: {$title}";
- $thread_topic = "D{$id}: {$original_title}";
return id(new PhabricatorMetaMTAMail())
- ->setSubject($subject)
- ->addHeader('Thread-Topic', $thread_topic);
+ ->setSubject($subject);
}
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());
$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);
}
$changed_uri = $this->getChangedPriorToCommitURI();
if ($changed_uri) {
$body->addLinkSection(
pht('CHANGED PRIOR TO COMMIT'),
$changed_uri);
}
$this->addCustomFieldsToMailBody($body, $object, $xactions);
$body->addLinkSection(
pht('REVISION DETAIL'),
$revision_uri);
$update_xaction = null;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
- case DifferentialTransaction::TYPE_UPDATE:
+ 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) {
$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) {
// 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;
}
}
- $this->setUnmentionablePHIDMap(array_merge($task_phids, $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();
+ }
+
+ $this->setUnmentionablePHIDMap(
+ array_merge(
+ $task_phids,
+ $rev_phids,
+ $revert_phids));
$result = array();
foreach ($edges as $type => $specs) {
$result[] = id(new DifferentialTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $type)
->setNewValue(array('+' => $specs));
}
return $result;
}
private function appendInlineCommentsForMail(
PhabricatorLiskDAO $object,
array $inlines,
PhabricatorMetaMTAMailBody $body) {
$section = id(new DifferentialInlineCommentMailView())
->setViewer($this->getActor())
->setInlines($inlines)
->buildMailSection();
$header = pht('INLINE COMMENTS');
$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 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();
}
- private function requireDiff($phid, $need_changesets = false) {
+ 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) {
return array();
}
$packages = PhabricatorOwnersPackage::loadAffectedPackages(
$repository,
$this->affectedPaths);
if (!$packages) {
return array();
}
- // Remove packages that the revision author is an owner of. If you own
- // code, you don't need another owner to review it.
- $authority = id(new PhabricatorOwnersPackageQuery())
- ->setViewer(PhabricatorUser::getOmnipotentUser())
- ->withPHIDs(mpull($packages, 'getPHID'))
- ->withAuthorityPHIDs(array($object->getAuthorPHID()))
- ->execute();
- $authority = mpull($authority, null, 'getPHID');
+ // 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();
- foreach ($packages as $key => $package) {
- $package_phid = $package->getPHID();
- if (isset($authority[$package_phid])) {
- unset($packages[$key]);
+ $spec = idx($autoreview_map, $autoreview_setting);
+ if (!$spec) {
continue;
}
+
+ if (idx($spec, 'authority')) {
+ $need_authority[$package->getPHID()] = $package->getPHID();
+ }
}
- if (!$packages) {
- return array();
+ 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_SUBSCRIBE:
- $auto_subscribe[] = $package;
- break;
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->shouldBroadcast()) {
+ 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 = DifferentialTransaction::TYPE_UPDATE;
+ $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;
// 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) {
$format = PhabricatorEnv::getEnvConfig('metamta.differential.patch-format');
return id(new DifferentialRawDiffRenderer())
->setViewer($this->getActor())
->setFormat($format)
->setChangesets($diff->getChangesets())
->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) {
- $active_builds = $this->hasActiveBuilds($object);
- if (!$active_builds) {
+ $status = $this->loadCompletedBuildableStatus($object);
+
+ $is_passed = ($status === HarbormasterBuildableStatus::STATUS_PASSED);
+ $is_failed = ($status === HarbormasterBuildableStatus::STATUS_FAILED);
+
+ if ($is_passed) {
// 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) {
+ // 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_draft = $this->wasDraft;
+ $was_broadcasting = $this->wasBroadcasting;
- if (!$object->isDraft() && ($was_draft || $is_new)) {
- if (!$object->getHasBroadcast()) {
+ 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;
-
- $object
- ->setHasBroadcast(true)
- ->save();
}
}
return $xactions;
}
- private function hasActiveBuilds($object) {
+ private function loadCompletedBuildableStatus(
+ DifferentialRevision $revision) {
$viewer = $this->requireActor();
-
- $builds = $object->loadActiveBuilds($viewer);
- if (!$builds) {
- return false;
- }
-
- return true;
+ $builds = $revision->loadImpactfulBuilds($viewer);
+ return $revision->newBuildableStatusForBuilds($builds);
}
- private function updateRevisionLineCounts(
- DifferentialRevision $revision,
- DifferentialDiff $diff) {
-
- $revision->setLineCount($diff->getLineCount());
-
- $conn = $revision->establishConnection('r');
-
- $row = queryfx_one(
- $conn,
- 'SELECT SUM(addLines) A, SUM(delLines) D FROM %T
- WHERE diffID = %d',
- id(new DifferentialChangeset())->getTableName(),
- $diff->getID());
+ private function requireReviewers(DifferentialRevision $revision) {
+ if ($revision->hasAttachedReviewers()) {
+ return;
+ }
- if ($row) {
- $revision->setAddedLineCount((int)$row['A']);
- $revision->setRemovedLineCount((int)$row['D']);
+ $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/DifferentialDiffExtractionEngine.php b/src/applications/differential/engine/DifferentialDiffExtractionEngine.php
index c426c411e..d7d7c767e 100644
--- a/src/applications/differential/engine/DifferentialDiffExtractionEngine.php
+++ b/src/applications/differential/engine/DifferentialDiffExtractionEngine.php
@@ -1,315 +1,318 @@
<?php
final class DifferentialDiffExtractionEngine extends Phobject {
private $viewer;
private $authorPHID;
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setAuthorPHID($author_phid) {
$this->authorPHID = $author_phid;
return $this;
}
public function getAuthorPHID() {
return $this->authorPHID;
}
public function newDiffFromCommit(PhabricatorRepositoryCommit $commit) {
$viewer = $this->getViewer();
// If we already have an unattached diff for this commit, just reuse it.
// This stops us from repeatedly generating diffs if something goes wrong
// later in the process. See T10968 for context.
$existing_diffs = id(new DifferentialDiffQuery())
->setViewer($viewer)
->withCommitPHIDs(array($commit->getPHID()))
->withHasRevision(false)
->needChangesets(true)
->execute();
if ($existing_diffs) {
return head($existing_diffs);
}
$repository = $commit->getRepository();
$identifier = $commit->getCommitIdentifier();
$monogram = $commit->getMonogram();
$drequest = DiffusionRequest::newFromDictionary(
array(
'user' => $viewer,
'repository' => $repository,
));
$diff_info = DiffusionQuery::callConduitWithDiffusionRequest(
$viewer,
$drequest,
'diffusion.rawdiffquery',
array(
'commit' => $identifier,
));
$file_phid = $diff_info['filePHID'];
$diff_file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($file_phid))
->executeOne();
if (!$diff_file) {
throw new Exception(
pht(
'Failed to load file ("%s") returned by "%s".',
$file_phid,
'diffusion.rawdiffquery'));
}
$raw_diff = $diff_file->loadFileData();
// TODO: Support adds, deletes and moves under SVN.
if (strlen($raw_diff)) {
$changes = id(new ArcanistDiffParser())->parseDiff($raw_diff);
} else {
// This is an empty diff, maybe made with `git commit --allow-empty`.
// NOTE: These diffs have the same tree hash as their ancestors, so
// they may attach to revisions in an unexpected way. Just let this
// happen for now, although it might make sense to special case it
// eventually.
$changes = array();
}
$diff = DifferentialDiff::newFromRawChanges($viewer, $changes)
->setRepositoryPHID($repository->getPHID())
->setCommitPHID($commit->getPHID())
->setCreationMethod('commit')
->setSourceControlSystem($repository->getVersionControlSystem())
->setLintStatus(DifferentialLintStatus::LINT_AUTO_SKIP)
->setUnitStatus(DifferentialUnitStatus::UNIT_AUTO_SKIP)
->setDateCreated($commit->getEpoch())
->setDescription($monogram);
$author_phid = $this->getAuthorPHID();
if ($author_phid !== null) {
$diff->setAuthorPHID($author_phid);
}
$parents = DiffusionQuery::callConduitWithDiffusionRequest(
$viewer,
$drequest,
'diffusion.commitparentsquery',
array(
'commit' => $identifier,
));
if ($parents) {
$diff->setSourceControlBaseRevision(head($parents));
}
// TODO: Attach binary files.
return $diff->save();
}
public function isDiffChangedBeforeCommit(
PhabricatorRepositoryCommit $commit,
DifferentialDiff $old,
DifferentialDiff $new) {
$viewer = $this->getViewer();
$repository = $commit->getRepository();
$identifier = $commit->getCommitIdentifier();
$vs_changesets = array();
foreach ($old->getChangesets() as $changeset) {
$path = $changeset->getAbsoluteRepositoryPath($repository, $old);
$path = ltrim($path, '/');
$vs_changesets[$path] = $changeset;
}
$changesets = array();
foreach ($new->getChangesets() as $changeset) {
$path = $changeset->getAbsoluteRepositoryPath($repository, $new);
$path = ltrim($path, '/');
$changesets[$path] = $changeset;
}
if (array_fill_keys(array_keys($changesets), true) !=
array_fill_keys(array_keys($vs_changesets), true)) {
return true;
}
$file_phids = array();
foreach ($vs_changesets as $changeset) {
$metadata = $changeset->getMetadata();
$file_phid = idx($metadata, 'new:binary-phid');
if ($file_phid) {
$file_phids[$file_phid] = $file_phid;
}
}
$files = array();
if ($file_phids) {
$files = id(new PhabricatorFileQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs($file_phids)
->execute();
$files = mpull($files, null, 'getPHID');
}
foreach ($changesets as $path => $changeset) {
$vs_changeset = $vs_changesets[$path];
$file_phid = idx($vs_changeset->getMetadata(), 'new:binary-phid');
if ($file_phid) {
if (!isset($files[$file_phid])) {
return true;
}
$drequest = DiffusionRequest::newFromDictionary(
array(
'user' => $viewer,
'repository' => $repository,
));
$response = DiffusionQuery::callConduitWithDiffusionRequest(
$viewer,
$drequest,
'diffusion.filecontentquery',
array(
'commit' => $identifier,
'path' => $path,
));
$new_file_phid = $response['filePHID'];
if (!$new_file_phid) {
return true;
}
$new_file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($new_file_phid))
->executeOne();
if (!$new_file) {
return true;
}
if ($files[$file_phid]->loadFileData() != $new_file->loadFileData()) {
return true;
}
} else {
$context = implode("\n", $changeset->makeChangesWithContext());
$vs_context = implode("\n", $vs_changeset->makeChangesWithContext());
// We couldn't just compare $context and $vs_context because following
// diffs will be considered different:
//
// -(empty line)
// -echo 'test';
// (empty line)
//
// (empty line)
// -echo "test";
// -(empty line)
- $hunk = id(new DifferentialModernHunk())->setChanges($context);
- $vs_hunk = id(new DifferentialModernHunk())->setChanges($vs_context);
+ $hunk = id(new DifferentialHunk())->setChanges($context);
+ $vs_hunk = id(new DifferentialHunk())->setChanges($vs_context);
if ($hunk->makeOldFile() != $vs_hunk->makeOldFile() ||
$hunk->makeNewFile() != $vs_hunk->makeNewFile()) {
return true;
}
}
}
return false;
}
public function updateRevisionWithCommit(
DifferentialRevision $revision,
PhabricatorRepositoryCommit $commit,
array $more_xactions,
PhabricatorContentSource $content_source) {
$viewer = $this->getViewer();
$result_data = array();
$new_diff = $this->newDiffFromCommit($commit);
$old_diff = $revision->getActiveDiff();
$changed_uri = null;
if ($old_diff) {
$old_diff = id(new DifferentialDiffQuery())
->setViewer($viewer)
->withIDs(array($old_diff->getID()))
->needChangesets(true)
->executeOne();
if ($old_diff) {
$has_changed = $this->isDiffChangedBeforeCommit(
$commit,
$old_diff,
$new_diff);
if ($has_changed) {
$result_data['vsDiff'] = $old_diff->getID();
$revision_monogram = $revision->getMonogram();
$old_id = $old_diff->getID();
$new_id = $new_diff->getID();
$changed_uri = "/{$revision_monogram}?vs={$old_id}&id={$new_id}#toc";
$changed_uri = PhabricatorEnv::getProductionURI($changed_uri);
}
}
}
$xactions = array();
// If the revision isn't closed or "Accepted", write a warning into the
// transaction log. This makes it more clear when users bend the rules.
if (!$revision->isClosed() && !$revision->isAccepted()) {
$wrong_type = DifferentialRevisionWrongStateTransaction::TRANSACTIONTYPE;
$xactions[] = id(new DifferentialTransaction())
->setTransactionType($wrong_type)
->setNewValue($revision->getModernRevisionStatus());
}
+ $type_update = DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE;
+
$xactions[] = id(new DifferentialTransaction())
- ->setTransactionType(DifferentialTransaction::TYPE_UPDATE)
+ ->setTransactionType($type_update)
->setIgnoreOnNoEffect(true)
->setNewValue($new_diff->getPHID())
- ->setMetadataValue('isCommitUpdate', true);
+ ->setMetadataValue('isCommitUpdate', true)
+ ->setMetadataValue('commitPHIDs', array($commit->getPHID()));
foreach ($more_xactions as $more_xaction) {
$xactions[] = $more_xaction;
}
$editor = id(new DifferentialTransactionEditor())
->setActor($viewer)
->setContinueOnMissingFields(true)
->setContentSource($content_source)
->setChangedPriorToCommitURI($changed_uri)
->setIsCloseByCommit(true);
$author_phid = $this->getAuthorPHID();
if ($author_phid !== null) {
$editor->setActingAsPHID($author_phid);
}
try {
$editor->applyTransactions($revision, $xactions);
} catch (PhabricatorApplicationTransactionNoEffectException $ex) {
// NOTE: We've marked transactions other than the CLOSE transaction
// as ignored when they don't have an effect, so this means that we
// lost a race to close the revision. That's perfectly fine, we can
// just continue normally.
}
return $result_data;
}
}
diff --git a/src/applications/differential/engineextension/DifferentialMailEngineExtension.php b/src/applications/differential/engineextension/DifferentialMailEngineExtension.php
new file mode 100644
index 000000000..29019b9e7
--- /dev/null
+++ b/src/applications/differential/engineextension/DifferentialMailEngineExtension.php
@@ -0,0 +1,80 @@
+<?php
+
+final class DifferentialMailEngineExtension
+ extends PhabricatorMailEngineExtension {
+
+ const EXTENSIONKEY = 'differential';
+
+ public function supportsObject($object) {
+ return ($object instanceof DifferentialRevision);
+ }
+
+ public function newMailStampTemplates($object) {
+ return array(
+ id(new PhabricatorPHIDMailStamp())
+ ->setKey('author')
+ ->setLabel(pht('Author')),
+ id(new PhabricatorPHIDMailStamp())
+ ->setKey('reviewer')
+ ->setLabel(pht('Reviewer')),
+ id(new PhabricatorPHIDMailStamp())
+ ->setKey('blocking-reviewer')
+ ->setLabel(pht('Reviewer')),
+ id(new PhabricatorPHIDMailStamp())
+ ->setKey('resigned-reviewer')
+ ->setLabel(pht('Reviewer')),
+ id(new PhabricatorPHIDMailStamp())
+ ->setKey('revision-repository')
+ ->setLabel(pht('Revision Repository')),
+ id(new PhabricatorPHIDMailStamp())
+ ->setKey('revision-status')
+ ->setLabel(pht('Revision Status')),
+ );
+ }
+
+ public function newMailStamps($object, array $xactions) {
+ $editor = $this->getEditor();
+ $viewer = $this->getViewer();
+
+ $revision = id(new DifferentialRevisionQuery())
+ ->setViewer($viewer)
+ ->needReviewers(true)
+ ->withPHIDs(array($object->getPHID()))
+ ->executeOne();
+
+ $reviewers = array();
+ $blocking = array();
+ $resigned = array();
+ foreach ($revision->getReviewers() as $reviewer) {
+ $reviewer_phid = $reviewer->getReviewerPHID();
+
+ if ($reviewer->isResigned()) {
+ $resigned[] = $reviewer_phid;
+ } else {
+ $reviewers[] = $reviewer_phid;
+ if ($reviewer->isBlocking()) {
+ $blocking[] = $reviewer_phid;
+ }
+ }
+ }
+
+ $this->getMailStamp('author')
+ ->setValue($revision->getAuthorPHID());
+
+ $this->getMailStamp('reviewer')
+ ->setValue($reviewers);
+
+ $this->getMailStamp('blocking-reviewer')
+ ->setValue($blocking);
+
+ $this->getMailStamp('resigned-reviewer')
+ ->setValue($resigned);
+
+ $this->getMailStamp('revision-repository')
+ ->setValue($revision->getRepositoryPHID());
+
+ $this->getMailStamp('revision-status')
+ ->setValue($revision->getModernRevisionStatus());
+ }
+
+}
diff --git a/src/applications/differential/harbormaster/DifferentialBuildableEngine.php b/src/applications/differential/harbormaster/DifferentialBuildableEngine.php
new file mode 100644
index 000000000..df38885ed
--- /dev/null
+++ b/src/applications/differential/harbormaster/DifferentialBuildableEngine.php
@@ -0,0 +1,57 @@
+<?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));
+ }
+
+}
diff --git a/src/applications/differential/herald/HeraldDifferentialDiffAdapter.php b/src/applications/differential/herald/HeraldDifferentialDiffAdapter.php
index a6b2e7c36..966b30658 100644
--- a/src/applications/differential/herald/HeraldDifferentialDiffAdapter.php
+++ b/src/applications/differential/herald/HeraldDifferentialDiffAdapter.php
@@ -1,60 +1,64 @@
<?php
final class HeraldDifferentialDiffAdapter extends HeraldDifferentialAdapter {
public function getAdapterApplicationClass() {
return 'PhabricatorDifferentialApplication';
}
protected function initializeNewAdapter() {
$this->setDiff(new DifferentialDiff());
}
public function isSingleEventAdapter() {
return true;
}
protected function loadChangesets() {
return $this->loadChangesetsWithHunks();
}
protected function loadChangesetsWithHunks() {
return $this->getDiff()->getChangesets();
}
public function getObject() {
return $this->getDiff();
}
public function getAdapterContentType() {
return 'differential.diff';
}
public function getAdapterContentName() {
return pht('Differential Diffs');
}
public function getAdapterContentDescription() {
return pht(
"React to new diffs being uploaded, before writes occur.\n".
"These rules can reject diffs before they are written to permanent ".
"storage, to prevent users from accidentally uploading private keys or ".
"other sensitive information.");
}
public function supportsRuleType($rule_type) {
switch ($rule_type) {
case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
return true;
case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
default:
return false;
}
}
public function getHeraldName() {
return pht('New Diff');
}
+ public function supportsWebhooks() {
+ return false;
+ }
+
}
diff --git a/src/applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php b/src/applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php
index c37974fa2..2443fe965 100644
--- a/src/applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php
+++ b/src/applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php
@@ -1,107 +1,107 @@
<?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(DifferentialTransaction::TYPE_UPDATE)
+ ->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);
$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 9c0e0071e..99125645e 100644
--- a/src/applications/differential/management/PhabricatorDifferentialMigrateHunkWorkflow.php
+++ b/src/applications/differential/management/PhabricatorDifferentialMigrateHunkWorkflow.php
@@ -1,86 +1,86 @@
<?php
final class PhabricatorDifferentialMigrateHunkWorkflow
extends PhabricatorDifferentialManagementWorkflow {
protected function didConstruct() {
$this
->setName('migrate-hunk')
->setExamples('**migrate-hunk** --id __hunk__ --to __storage__')
->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.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$id = $args->getArg('id');
if (!$id) {
throw new PhutilArgumentUsageException(
pht('Specify a hunk to migrate with --id.'));
}
$storage = $args->getArg('to');
switch ($storage) {
- case DifferentialModernHunk::DATATYPE_TEXT:
- case DifferentialModernHunk::DATATYPE_FILE:
+ case DifferentialHunk::DATATYPE_TEXT:
+ case DifferentialHunk::DATATYPE_FILE:
break;
default:
throw new PhutilArgumentUsageException(
pht('Specify a hunk storage engine with --to.'));
}
$hunk = $this->loadHunk($id);
$old_data = $hunk->getChanges();
switch ($storage) {
- case DifferentialModernHunk::DATATYPE_TEXT:
+ case DifferentialHunk::DATATYPE_TEXT:
$hunk->saveAsText();
$this->logOkay(
pht('TEXT'),
pht('Convereted hunk to text storage.'));
break;
- case DifferentialModernHunk::DATATYPE_FILE:
+ case DifferentialHunk::DATATYPE_FILE:
$hunk->saveAsFile();
$this->logOkay(
pht('FILE'),
pht('Convereted hunk to file storage.'));
break;
}
$hunk = $this->loadHunk($id);
$new_data = $hunk->getChanges();
if ($old_data !== $new_data) {
throw new Exception(
pht(
'Integrity check failed: new file data differs fom old data!'));
}
return 0;
}
private function loadHunk($id) {
- $hunk = id(new DifferentialModernHunk())->load($id);
+ $hunk = id(new DifferentialHunk())->load($id);
if (!$hunk) {
throw new PhutilArgumentUsageException(
pht(
'No hunk exists with ID "%s".',
$id));
}
return $hunk;
}
}
diff --git a/src/applications/differential/parser/__tests__/DifferentialChangesetParserTestCase.php b/src/applications/differential/parser/__tests__/DifferentialChangesetParserTestCase.php
index 9ca88db8c..d8d10169b 100644
--- a/src/applications/differential/parser/__tests__/DifferentialChangesetParserTestCase.php
+++ b/src/applications/differential/parser/__tests__/DifferentialChangesetParserTestCase.php
@@ -1,36 +1,36 @@
<?php
final class DifferentialChangesetParserTestCase extends PhabricatorTestCase {
public function testDiffChangesets() {
- $hunk = new DifferentialModernHunk();
+ $hunk = new DifferentialHunk();
$hunk->setChanges("+a\n b\n-c");
$hunk->setNewOffset(1);
$hunk->setNewLen(2);
$left = new DifferentialChangeset();
$left->attachHunks(array($hunk));
$tests = array(
"+a\n b\n-c" => array(array(), array()),
"+a\n x\n-c" => array(array(), array()),
"+aa\n b\n-c" => array(array(1), array(11)),
" b\n-c" => array(array(1), array()),
"+a\n b\n c" => array(array(), array(13)),
"+a\n x\n c" => array(array(), array(13)),
);
foreach ($tests as $changes => $expected) {
- $hunk = new DifferentialModernHunk();
+ $hunk = new DifferentialHunk();
$hunk->setChanges($changes);
$hunk->setNewOffset(11);
$hunk->setNewLen(3);
$right = new DifferentialChangeset();
$right->attachHunks(array($hunk));
$parser = new DifferentialChangesetParser();
$parser->setOriginals($left, $right);
$this->assertEqual($expected, $parser->diffOriginals(), $changes);
}
}
}
diff --git a/src/applications/differential/parser/__tests__/DifferentialHunkParserTestCase.php b/src/applications/differential/parser/__tests__/DifferentialHunkParserTestCase.php
index d1d2501b9..644e7c0b8 100644
--- a/src/applications/differential/parser/__tests__/DifferentialHunkParserTestCase.php
+++ b/src/applications/differential/parser/__tests__/DifferentialHunkParserTestCase.php
@@ -1,291 +1,291 @@
<?php
final class DifferentialHunkParserTestCase extends PhabricatorTestCase {
private function createComment() {
$comment = new DifferentialInlineComment();
return $comment;
}
private function createHunk(
$old_offset,
$old_len,
$new_offset,
$new_len,
$changes) {
- $hunk = id(new DifferentialModernHunk())
+ $hunk = id(new DifferentialHunk())
->setOldOffset($old_offset)
->setOldLen($old_len)
->setNewOffset($new_offset)
->setNewLen($new_len)
->setChanges($changes);
return $hunk;
}
// Returns a change that consists of a single hunk, starting at line 1.
private function createSingleChange($old_lines, $new_lines, $changes) {
return array(
0 => $this->createHunk(1, $old_lines, 1, $new_lines, $changes),
);
}
private function createHunksFromFile($name) {
$data = Filesystem::readFile(dirname(__FILE__).'/data/'.$name);
$parser = new ArcanistDiffParser();
$changes = $parser->parseDiff($data);
if (count($changes) !== 1) {
throw new Exception(pht("Expected 1 changeset for '%s'!", $name));
}
$diff = DifferentialDiff::newFromRawChanges(
PhabricatorUser::getOmnipotentUser(),
$changes);
return head($diff->getChangesets())->getHunks();
}
public function testOneLineOldComment() {
$parser = new DifferentialHunkParser();
$hunks = $this->createSingleChange(1, 0, '-a');
$context = $parser->makeContextDiff(
$hunks,
0,
1,
0,
0);
$this->assertEqual("@@ -1,1 @@\n-a", $context);
}
public function testOneLineNewComment() {
$parser = new DifferentialHunkParser();
$hunks = $this->createSingleChange(0, 1, '+a');
$context = $parser->makeContextDiff(
$hunks,
1,
1,
0,
0);
$this->assertEqual("@@ +1,1 @@\n+a", $context);
}
public function testCannotFindContext() {
$parser = new DifferentialHunkParser();
$hunks = $this->createSingleChange(0, 1, '+a');
$context = $parser->makeContextDiff(
$hunks,
1,
2,
0,
0);
$this->assertEqual('', $context);
}
public function testOverlapFromStartOfHunk() {
$parser = new DifferentialHunkParser();
$hunks = array(
0 => $this->createHunk(23, 2, 42, 2, " 1\n 2"),
);
$context = $parser->makeContextDiff(
$hunks,
1,
41,
1,
0);
$this->assertEqual("@@ -23,1 +42,1 @@\n 1", $context);
}
public function testOverlapAfterEndOfHunk() {
$parser = new DifferentialHunkParser();
$hunks = array(
0 => $this->createHunk(23, 2, 42, 2, " 1\n 2"),
);
$context = $parser->makeContextDiff(
$hunks,
1,
43,
1,
0);
$this->assertEqual("@@ -24,1 +43,1 @@\n 2", $context);
}
public function testInclusionOfNewFileInOldCommentFromStart() {
$parser = new DifferentialHunkParser();
$hunks = $this->createSingleChange(2, 3,
"+n1\n".
" e1/2\n".
"-o2\n".
"+n3\n");
$context = $parser->makeContextDiff(
$hunks,
0,
1,
1,
0);
$this->assertEqual(
"@@ -1,2 +2,1 @@\n".
" e1/2\n".
"-o2", $context);
}
public function testInclusionOfOldFileInNewCommentFromStart() {
$parser = new DifferentialHunkParser();
$hunks = $this->createSingleChange(2, 2,
"-o1\n".
" e2/1\n".
"-o3\n".
"+n2\n");
$context = $parser->makeContextDiff(
$hunks,
1,
1,
1,
0);
$this->assertEqual(
"@@ -2,1 +1,2 @@\n".
" e2/1\n".
"+n2", $context);
}
public function testNoNewlineAtEndOfFile() {
$parser = new DifferentialHunkParser();
$hunks = $this->createSingleChange(0, 1,
"+a\n".
"\\No newline at end of file");
// Note that this only works with additional context.
$context = $parser->makeContextDiff(
$hunks,
1,
2,
0,
1);
$this->assertEqual(
"@@ +1,1 @@\n".
"+a\n".
"\\No newline at end of file", $context);
}
public function testMultiLineNewComment() {
$parser = new DifferentialHunkParser();
$hunks = $this->createSingleChange(7, 7,
" e1\n".
" e2\n".
"-o3\n".
"-o4\n".
"+n3\n".
" e5/4\n".
" e6/5\n".
"+n6\n".
" e7\n");
$context = $parser->makeContextDiff(
$hunks,
1,
2,
4,
0);
$this->assertEqual(
"@@ -2,5 +2,5 @@\n".
" e2\n".
"-o3\n".
"-o4\n".
"+n3\n".
" e5/4\n".
" e6/5\n".
"+n6", $context);
}
public function testMultiLineOldComment() {
$parser = new DifferentialHunkParser();
$hunks = $this->createSingleChange(7, 7,
" e1\n".
" e2\n".
"-o3\n".
"-o4\n".
"+n3\n".
" e5/4\n".
" e6/5\n".
"+n6\n".
" e7\n");
$context = $parser->makeContextDiff(
$hunks,
0,
2,
4,
0);
$this->assertEqual(
"@@ -2,5 +2,4 @@\n".
" e2\n".
"-o3\n".
"-o4\n".
"+n3\n".
" e5/4\n".
" e6/5", $context);
}
public function testInclusionOfNewFileInOldCommentFromStartWithContext() {
$parser = new DifferentialHunkParser();
$hunks = $this->createSingleChange(2, 3,
"+n1\n".
" e1/2\n".
"-o2\n".
"+n3\n");
$context = $parser->makeContextDiff(
$hunks,
0,
1,
1,
1);
$this->assertEqual(
"@@ -1,2 +1,2 @@\n".
"+n1\n".
" e1/2\n".
"-o2", $context);
}
public function testInclusionOfOldFileInNewCommentFromStartWithContext() {
$parser = new DifferentialHunkParser();
$hunks = $this->createSingleChange(2, 2,
"-o1\n".
" e2/1\n".
"-o3\n".
"+n2\n");
$context = $parser->makeContextDiff(
$hunks,
1,
1,
1,
1);
$this->assertEqual(
"@@ -1,3 +1,2 @@\n".
"-o1\n".
" e2/1\n".
"-o3\n".
"+n2", $context);
}
public function testMissingContext() {
$tests = array(
'missing_context.diff' => array(
4 => true,
),
'missing_context_2.diff' => array(
5 => true,
),
'missing_context_3.diff' => array(
4 => true,
13 => true,
),
);
foreach ($tests as $name => $expect) {
$hunks = $this->createHunksFromFile($name);
$parser = new DifferentialHunkParser();
$actual = $parser->getHunkStartLines($hunks);
$this->assertEqual($expect, $actual, $name);
}
}
}
diff --git a/src/applications/differential/query/DifferentialChangesetQuery.php b/src/applications/differential/query/DifferentialChangesetQuery.php
index 7b9f2e6d1..8f69c61a4 100644
--- a/src/applications/differential/query/DifferentialChangesetQuery.php
+++ b/src/applications/differential/query/DifferentialChangesetQuery.php
@@ -1,153 +1,144 @@
<?php
final class DifferentialChangesetQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $diffs;
private $needAttachToDiffs;
private $needHunks;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withDiffs(array $diffs) {
assert_instances_of($diffs, 'DifferentialDiff');
$this->diffs = $diffs;
return $this;
}
public function needAttachToDiffs($attach) {
$this->needAttachToDiffs = $attach;
return $this;
}
public function needHunks($need) {
$this->needHunks = $need;
return $this;
}
protected function willExecute() {
// If we fail to load any changesets (which is possible in the case of an
// empty commit) we'll never call didFilterPage(). Attach empty changeset
// lists now so that we end up with the right result.
if ($this->needAttachToDiffs) {
foreach ($this->diffs as $diff) {
$diff->attachChangesets(array());
}
}
}
+ public function newResultObject() {
+ return new DifferentialChangeset();
+ }
+
protected function loadPage() {
- $table = new DifferentialChangeset();
- $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 willFilterPage(array $changesets) {
// First, attach all the diffs we already have. We can just do this
// directly without worrying about querying for them. When we don't have
// a diff, record that we need to load it.
if ($this->diffs) {
$have_diffs = mpull($this->diffs, null, 'getID');
} else {
$have_diffs = array();
}
$must_load = array();
foreach ($changesets as $key => $changeset) {
$diff_id = $changeset->getDiffID();
if (isset($have_diffs[$diff_id])) {
$changeset->attachDiff($have_diffs[$diff_id]);
} else {
$must_load[$key] = $changeset;
}
}
// Load all the diffs we don't have.
$need_diff_ids = mpull($must_load, 'getDiffID');
$more_diffs = array();
if ($need_diff_ids) {
$more_diffs = id(new DifferentialDiffQuery())
->setViewer($this->getViewer())
->setParentQuery($this)
->withIDs($need_diff_ids)
->execute();
$more_diffs = mpull($more_diffs, null, 'getID');
}
// Attach the diffs we loaded.
foreach ($must_load as $key => $changeset) {
$diff_id = $changeset->getDiffID();
if (isset($more_diffs[$diff_id])) {
$changeset->attachDiff($more_diffs[$diff_id]);
} else {
// We didn't have the diff, and could not load it (it does not exist,
// or we can't see it), so filter this result out.
unset($changesets[$key]);
}
}
return $changesets;
}
protected function didFilterPage(array $changesets) {
if ($this->needAttachToDiffs) {
$changeset_groups = mgroup($changesets, 'getDiffID');
foreach ($this->diffs as $diff) {
$diff_changesets = idx($changeset_groups, $diff->getID(), array());
$diff->attachChangesets($diff_changesets);
}
}
if ($this->needHunks) {
id(new DifferentialHunkQuery())
->setViewer($this->getViewer())
->setParentQuery($this)
->withChangesets($changesets)
->needAttachToChangesets(true)
->execute();
}
return $changesets;
}
- protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
- $where = array();
+ protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
+ $where = parent::buildWhereClauseParts($conn);
if ($this->diffs !== null) {
$where[] = qsprintf(
- $conn_r,
+ $conn,
'diffID IN (%Ld)',
mpull($this->diffs, 'getID'));
}
if ($this->ids !== null) {
$where[] = qsprintf(
- $conn_r,
+ $conn,
'id IN (%Ld)',
$this->ids);
}
- $where[] = $this->buildPagingClause($conn_r);
-
- return $this->formatWhereClause($where);
+ return $where;
}
public function getQueryApplicationClass() {
return 'PhabricatorDifferentialApplication';
}
}
diff --git a/src/applications/differential/query/DifferentialChangesetSearchEngine.php b/src/applications/differential/query/DifferentialChangesetSearchEngine.php
new file mode 100644
index 000000000..0dfec94a5
--- /dev/null
+++ b/src/applications/differential/query/DifferentialChangesetSearchEngine.php
@@ -0,0 +1,133 @@
+<?php
+
+final class DifferentialChangesetSearchEngine
+ extends PhabricatorApplicationSearchEngine {
+
+ private $diff;
+
+ public function setDiff(DifferentialDiff $diff) {
+ $this->diff = $diff;
+ return $this;
+ }
+
+ public function getDiff() {
+ return $this->diff;
+ }
+
+ public function getResultTypeDescription() {
+ return pht('Differential Changesets');
+ }
+
+ public function getApplicationClassName() {
+ return 'PhabricatorDifferentialApplication';
+ }
+
+ public function newQuery() {
+ $query = id(new DifferentialChangesetQuery());
+
+ if ($this->diff) {
+ $query->withDiffs(array($this->diff));
+ }
+
+ return $query;
+ }
+
+ protected function buildQueryFromParameters(array $map) {
+ $query = $this->newQuery();
+ return $query;
+ }
+
+ protected function buildCustomSearchFields() {
+ return array();
+ }
+
+ protected function getURI($path) {
+ $diff = $this->getDiff();
+ if ($diff) {
+ return '/differential/diff/'.$diff->getID().'/changesets/'.$path;
+ }
+
+ throw new PhutilMethodNotImplementedException();
+ }
+
+ protected function getBuiltinQueryNames() {
+ $names = array();
+ $names['all'] = pht('All Changesets');
+ return $names;
+ }
+
+ public function buildSavedQueryFromBuiltin($query_key) {
+ $query = $this->newSavedQuery();
+ $query->setQueryKey($query_key);
+
+ $viewer = $this->requireViewer();
+
+ switch ($query_key) {
+ case 'all':
+ return $query->setParameter('order', 'oldest');
+ }
+
+ return parent::buildSavedQueryFromBuiltin($query_key);
+ }
+
+ protected function renderResultList(
+ array $changesets,
+ PhabricatorSavedQuery $query,
+ array $handles) {
+
+ assert_instances_of($changesets, 'DifferentialChangeset');
+ $viewer = $this->requireViewer();
+
+ $rows = array();
+ foreach ($changesets as $changeset) {
+ $link = phutil_tag(
+ 'a',
+ array(
+ 'href' => '/differential/changeset/?ref='.$changeset->getID(),
+ ),
+ $changeset->getDisplayFilename());
+
+ $type = $changeset->getChangeType();
+
+ $title = DifferentialChangeType::getFullNameForChangeType($type);
+
+ $add_lines = $changeset->getAddLines();
+ if (!$add_lines) {
+ $add_lines = null;
+ } else {
+ $add_lines = '+'.$add_lines;
+ }
+
+ $rem_lines = $changeset->getDelLines();
+ if (!$rem_lines) {
+ $rem_lines = null;
+ } else {
+ $rem_lines = '-'.$rem_lines;
+ }
+
+ $rows[] = array(
+ $changeset->newFileTreeIcon(),
+ $title,
+ $link,
+ );
+ }
+
+ $table = id(new AphrontTableView($rows))
+ ->setHeaders(
+ array(
+ null,
+ pht('Change'),
+ pht('Path'),
+ ))
+ ->setColumnClasses(
+ array(
+ null,
+ null,
+ 'pri wide',
+ ));
+
+ return id(new PhabricatorApplicationSearchResultView())
+ ->setTable($table);
+ }
+
+}
diff --git a/src/applications/differential/query/DifferentialHunkQuery.php b/src/applications/differential/query/DifferentialHunkQuery.php
index c8e35cb60..981c2b478 100644
--- a/src/applications/differential/query/DifferentialHunkQuery.php
+++ b/src/applications/differential/query/DifferentialHunkQuery.php
@@ -1,108 +1,93 @@
<?php
final class DifferentialHunkQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $changesets;
private $shouldAttachToChangesets;
public function withChangesets(array $changesets) {
assert_instances_of($changesets, 'DifferentialChangeset');
$this->changesets = $changesets;
return $this;
}
public function needAttachToChangesets($attach) {
$this->shouldAttachToChangesets = $attach;
return $this;
}
protected function willExecute() {
// If we fail to load any hunks at all (for example, because all of
// the requested changesets are directories or empty files and have no
// hunks) we'll never call didFilterPage(), and thus never have an
// opportunity to attach hunks. Attach empty hunk lists now so that we
// end up with the right result.
if ($this->shouldAttachToChangesets) {
foreach ($this->changesets as $changeset) {
$changeset->attachHunks(array());
}
}
}
+ public function newResultObject() {
+ return new DifferentialHunk();
+ }
+
protected function loadPage() {
- $all_results = array();
-
- // Load modern hunks.
- $table = new DifferentialModernHunk();
- $conn_r = $table->establishConnection('r');
-
- $modern_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));
- $modern_results = $table->loadAllFromArray($modern_data);
-
- // Strip all the IDs off since they're not unique and nothing should be
- // using them.
- return array_values($modern_results);
+ return $this->loadStandardPage($this->newResultObject());
}
protected function willFilterPage(array $hunks) {
$changesets = mpull($this->changesets, null, 'getID');
foreach ($hunks as $key => $hunk) {
$changeset = idx($changesets, $hunk->getChangesetID());
if (!$changeset) {
unset($hunks[$key]);
}
$hunk->attachChangeset($changeset);
}
return $hunks;
}
protected function didFilterPage(array $hunks) {
if ($this->shouldAttachToChangesets) {
$hunk_groups = mgroup($hunks, 'getChangesetID');
foreach ($this->changesets as $changeset) {
$hunks = idx($hunk_groups, $changeset->getID(), array());
$changeset->attachHunks($hunks);
}
}
return $hunks;
}
- protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
- $where = array();
+ protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
+ $where = parent::buildWhereClauseParts($conn);
if (!$this->changesets) {
throw new Exception(
pht(
'You must load hunks via changesets, with %s!',
'withChangesets()'));
}
$where[] = qsprintf(
- $conn_r,
+ $conn,
'changesetID IN (%Ld)',
mpull($this->changesets, 'getID'));
- $where[] = $this->buildPagingClause($conn_r);
-
- return $this->formatWhereClause($where);
+ return $where;
}
public function getQueryApplicationClass() {
return 'PhabricatorDifferentialApplication';
}
protected function getDefaultOrderVector() {
// TODO: Do we need this?
return array('-id');
}
}
diff --git a/src/applications/differential/query/DifferentialRevisionSearchEngine.php b/src/applications/differential/query/DifferentialRevisionSearchEngine.php
index e2f4bfc42..835291fba 100644
--- a/src/applications/differential/query/DifferentialRevisionSearchEngine.php
+++ b/src/applications/differential/query/DifferentialRevisionSearchEngine.php
@@ -1,288 +1,276 @@
<?php
final class DifferentialRevisionSearchEngine
extends PhabricatorApplicationSearchEngine {
public function getResultTypeDescription() {
return pht('Differential Revisions');
}
public function getApplicationClassName() {
return 'PhabricatorDifferentialApplication';
}
protected function newResultBuckets() {
return DifferentialRevisionResultBucket::getAllResultBuckets();
}
public function newQuery() {
return id(new DifferentialRevisionQuery())
->needFlags(true)
->needDrafts(true)
->needReviewers(true);
}
protected function buildQueryFromParameters(array $map) {
$query = $this->newQuery();
if ($map['responsiblePHIDs']) {
$query->withResponsibleUsers($map['responsiblePHIDs']);
}
if ($map['authorPHIDs']) {
$query->withAuthors($map['authorPHIDs']);
}
if ($map['reviewerPHIDs']) {
$query->withReviewers($map['reviewerPHIDs']);
}
if ($map['repositoryPHIDs']) {
$query->withRepositoryPHIDs($map['repositoryPHIDs']);
}
if ($map['statuses']) {
$query->withStatuses($map['statuses']);
}
return $query;
}
protected function buildCustomSearchFields() {
return array(
id(new PhabricatorSearchDatasourceField())
->setLabel(pht('Responsible Users'))
->setKey('responsiblePHIDs')
->setAliases(array('responsiblePHID', 'responsibles', 'responsible'))
->setDatasource(new DifferentialResponsibleDatasource())
->setDescription(
pht('Find revisions that a given user is responsible for.')),
id(new PhabricatorUsersSearchField())
->setLabel(pht('Authors'))
->setKey('authorPHIDs')
->setAliases(array('author', 'authors', 'authorPHID'))
->setDescription(
pht('Find revisions with specific authors.')),
id(new PhabricatorSearchDatasourceField())
->setLabel(pht('Reviewers'))
->setKey('reviewerPHIDs')
->setAliases(array('reviewer', 'reviewers', 'reviewerPHID'))
->setDatasource(new DiffusionAuditorFunctionDatasource())
->setDescription(
pht('Find revisions with specific reviewers.')),
id(new PhabricatorSearchDatasourceField())
->setLabel(pht('Repositories'))
->setKey('repositoryPHIDs')
->setAliases(array('repository', 'repositories', 'repositoryPHID'))
->setDatasource(new DiffusionRepositoryFunctionDatasource())
->setDescription(
pht('Find revisions from specific repositories.')),
id(new PhabricatorSearchDatasourceField())
->setLabel(pht('Statuses'))
->setKey('statuses')
->setAliases(array('status'))
->setDatasource(new DifferentialRevisionStatusFunctionDatasource())
->setDescription(
pht('Find revisions with particular statuses.')),
);
}
protected function getURI($path) {
return '/differential/'.$path;
}
protected function getBuiltinQueryNames() {
$names = array();
if ($this->requireViewer()->isLoggedIn()) {
$names['active'] = pht('Active Revisions');
$names['authored'] = pht('Authored');
}
$names['all'] = pht('All Revisions');
return $names;
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
$viewer = $this->requireViewer();
switch ($query_key) {
case 'active':
$bucket_key = DifferentialRevisionRequiredActionResultBucket::BUCKETKEY;
return $query
->setParameter('responsiblePHIDs', array($viewer->getPHID()))
->setParameter('statuses', array('open()'))
->setParameter('bucket', $bucket_key);
case 'authored':
return $query
->setParameter('authorPHIDs', array($viewer->getPHID()));
case 'all':
return $query;
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
private function getStatusOptions() {
return array(
DifferentialLegacyQuery::STATUS_ANY => pht('All'),
DifferentialLegacyQuery::STATUS_OPEN => pht('Open'),
DifferentialLegacyQuery::STATUS_ACCEPTED => pht('Accepted'),
DifferentialLegacyQuery::STATUS_NEEDS_REVIEW => pht('Needs Review'),
DifferentialLegacyQuery::STATUS_NEEDS_REVISION => pht('Needs Revision'),
DifferentialLegacyQuery::STATUS_CLOSED => pht('Closed'),
DifferentialLegacyQuery::STATUS_ABANDONED => pht('Abandoned'),
);
}
protected function renderResultList(
array $revisions,
PhabricatorSavedQuery $query,
array $handles) {
assert_instances_of($revisions, 'DifferentialRevision');
$viewer = $this->requireViewer();
$template = id(new DifferentialRevisionListView())
- ->setUser($viewer)
+ ->setViewer($viewer)
->setNoBox($this->isPanelContext());
$bucket = $this->getResultBucket($query);
$unlanded = $this->loadUnlandedDependencies($revisions);
$views = array();
if ($bucket) {
$bucket->setViewer($viewer);
try {
$groups = $bucket->newResultGroups($query, $revisions);
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())
->setRevisions($group->getObjects());
}
}
} catch (Exception $ex) {
$this->addError($ex->getMessage());
}
} else {
$views[] = id(clone $template)
- ->setRevisions($revisions)
- ->setHandles(array());
+ ->setRevisions($revisions);
}
if (!$views) {
$views[] = id(new DifferentialRevisionListView())
- ->setUser($viewer)
- ->setNoDataString(pht('No revisions found.'));
- }
-
- $phids = array_mergev(mpull($views, 'getRequiredHandlePHIDs'));
- if ($phids) {
- $handles = id(new PhabricatorHandleQuery())
->setViewer($viewer)
- ->withPHIDs($phids)
- ->execute();
- } else {
- $handles = array();
+ ->setNoDataString(pht('No revisions found.'));
}
foreach ($views as $view) {
- $view->setHandles($handles);
$view->setUnlandedDependencies($unlanded);
}
if (count($views) == 1) {
// Reduce this to a PHUIObjectItemListView so we can get the free
// support from ApplicationSearch.
$list = head($views)->render();
} else {
$list = $views;
}
$result = new PhabricatorApplicationSearchResultView();
$result->setContent($list);
return $result;
}
protected function getNewUserBody() {
$create_button = id(new PHUIButtonView())
->setTag('a')
->setText(pht('Create a Diff'))
->setHref('/differential/diff/create/')
->setColor(PHUIButtonView::GREEN);
$icon = $this->getApplication()->getIcon();
$app_name = $this->getApplication()->getName();
$view = id(new PHUIBigInfoView())
->setIcon($icon)
->setTitle(pht('Welcome to %s', $app_name))
->setDescription(
pht('Pre-commit code review. Revisions that are waiting on your input '.
'will appear here.'))
->addAction($create_button);
return $view;
}
private function loadUnlandedDependencies(array $revisions) {
$phids = array();
foreach ($revisions as $revision) {
if (!$revision->isAccepted()) {
continue;
}
$phids[] = $revision->getPHID();
}
if (!$phids) {
return array();
}
$query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($phids)
->withEdgeTypes(
array(
DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST,
));
$query->execute();
$revision_phids = $query->getDestinationPHIDs();
if (!$revision_phids) {
return array();
}
$viewer = $this->requireViewer();
$blocking_revisions = id(new DifferentialRevisionQuery())
->setViewer($viewer)
->withPHIDs($revision_phids)
->withIsOpen(true)
->execute();
$blocking_revisions = mpull($blocking_revisions, null, 'getPHID');
$result = array();
foreach ($revisions as $revision) {
$revision_phid = $revision->getPHID();
$blocking_phids = $query->getDestinationPHIDs(array($revision_phid));
$blocking = array_select_keys($blocking_revisions, $blocking_phids);
if ($blocking) {
$result[$revision_phid] = $blocking;
}
}
return $result;
}
}
diff --git a/src/applications/differential/storage/DifferentialChangeset.php b/src/applications/differential/storage/DifferentialChangeset.php
index ebdaeacd0..4832f452e 100644
--- a/src/applications/differential/storage/DifferentialChangeset.php
+++ b/src/applications/differential/storage/DifferentialChangeset.php
@@ -1,263 +1,308 @@
<?php
final class DifferentialChangeset
extends DifferentialDAO
implements
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface {
protected $diffID;
protected $oldFile;
protected $filename;
protected $awayPaths;
protected $changeType;
protected $fileType;
protected $metadata;
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';
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();
- $modern_hunks = id(new DifferentialModernHunk())->loadAllWhere(
+ $hunks = id(new DifferentialHunk())->loadAllWhere(
'changesetID = %d',
$this->getID());
- foreach ($modern_hunks as $modern_hunk) {
- $modern_hunk->delete();
+ 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;
}
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;
+ }
+
/* -( 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 DifferentialModernHunk())->loadAllWhere(
+ $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 d70cdbbdf..728ec4b53 100644
--- a/src/applications/differential/storage/DifferentialDiff.php
+++ b/src/applications/differential/storage/DifferentialDiff.php
@@ -1,822 +1,822 @@
<?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 DifferentialModernHunk();
+ $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);
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 getHarbormasterPublishablePHID() {
- return $this->getHarbormasterContainerPHID();
- }
-
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();
}
}
diff --git a/src/applications/differential/storage/DifferentialHunk.php b/src/applications/differential/storage/DifferentialHunk.php
index a57d0035e..3defb3566 100644
--- a/src/applications/differential/storage/DifferentialHunk.php
+++ b/src/applications/differential/storage/DifferentialHunk.php
@@ -1,244 +1,479 @@
<?php
-abstract class DifferentialHunk
+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 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/DifferentialLegacyHunk.php b/src/applications/differential/storage/DifferentialLegacyHunk.php
deleted file mode 100644
index ae4673e46..000000000
--- a/src/applications/differential/storage/DifferentialLegacyHunk.php
+++ /dev/null
@@ -1,37 +0,0 @@
-<?php
-
-final class DifferentialLegacyHunk extends DifferentialHunk {
-
- protected $changes;
-
- protected function getConfiguration() {
- return array(
- self::CONFIG_COLUMN_SCHEMA => array(
- 'changes' => 'text?',
- 'oldOffset' => 'uint32',
- 'oldLen' => 'uint32',
- 'newOffset' => 'uint32',
- 'newLen' => 'uint32',
- ),
- self::CONFIG_KEY_SCHEMA => array(
- 'changesetID' => array(
- 'columns' => array('changesetID'),
- ),
- ),
- ) + parent::getConfiguration();
- }
-
- public function getTableName() {
- return 'differential_hunk';
- }
-
- public function getDataEncoding() {
- return 'utf8';
- }
-
- public function forceEncoding($encoding) {
- // Not supported, these are always utf8.
- return $this;
- }
-
-}
diff --git a/src/applications/differential/storage/DifferentialModernHunk.php b/src/applications/differential/storage/DifferentialModernHunk.php
deleted file mode 100644
index c3675c8ad..000000000
--- a/src/applications/differential/storage/DifferentialModernHunk.php
+++ /dev/null
@@ -1,249 +0,0 @@
-<?php
-
-final class DifferentialModernHunk extends DifferentialHunk {
-
- const DATATYPE_TEXT = 'text';
- const DATATYPE_FILE = 'file';
-
- const DATAFORMAT_RAW = 'byte';
- const DATAFORMAT_DEFLATED = 'gzde';
-
- protected $dataType;
- protected $dataEncoding;
- protected $dataFormat;
- protected $data;
-
- private $rawData;
- private $forcedEncoding;
- private $fileData;
-
- public function getTableName() {
- return 'differential_hunk_modern';
- }
-
- 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 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 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;
- }
-
-
- public function destroyObjectPermanently(
- PhabricatorDestructionEngine $engine) {
-
- $type = $this->getDataType();
- $data = $this->getData();
-
- $this->destroyData($type, $data, $engine);
-
- return parent::destroyObjectPermanently($engine);
- }
-
-
- 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;
- }
- }
-
-}
diff --git a/src/applications/differential/storage/DifferentialReviewer.php b/src/applications/differential/storage/DifferentialReviewer.php
index 9df149e78..823047f21 100644
--- a/src/applications/differential/storage/DifferentialReviewer.php
+++ b/src/applications/differential/storage/DifferentialReviewer.php
@@ -1,137 +1,155 @@
<?php
final class DifferentialReviewer
extends DifferentialDAO {
protected $revisionPHID;
protected $reviewerPHID;
protected $reviewerStatus;
protected $lastActionDiffPHID;
protected $lastCommentDiffPHID;
protected $lastActorPHID;
protected $voidedPHID;
+ protected $options = array();
private $authority = array();
private $changesets = self::ATTACHABLE;
protected function getConfiguration() {
return array(
+ self::CONFIG_SERIALIZATION => array(
+ 'options' => self::SERIALIZATION_JSON,
+ ),
self::CONFIG_COLUMN_SCHEMA => array(
'reviewerStatus' => 'text64',
'lastActionDiffPHID' => 'phid?',
'lastCommentDiffPHID' => 'phid?',
'lastActorPHID' => 'phid?',
'voidedPHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_revision' => array(
'columns' => array('revisionPHID', 'reviewerPHID'),
'unique' => true,
),
'key_reviewer' => array(
'columns' => array('reviewerPHID', 'revisionPHID'),
),
),
) + parent::getConfiguration();
}
public function isUser() {
$user_type = PhabricatorPeopleUserPHIDType::TYPECONST;
return (phid_get_type($this->getReviewerPHID()) == $user_type);
}
public function isPackage() {
$package_type = PhabricatorOwnersPackagePHIDType::TYPECONST;
return (phid_get_type($this->getReviewerPHID()) == $package_type);
}
public function attachAuthority(PhabricatorUser $user, $has_authority) {
$this->authority[$user->getCacheFragment()] = $has_authority;
return $this;
}
public function hasAuthority(PhabricatorUser $viewer) {
$cache_fragment = $viewer->getCacheFragment();
return $this->assertAttachedKey($this->authority, $cache_fragment);
}
public function attachChangesets(array $changesets) {
$this->changesets = $changesets;
return $this;
}
public function getChangesets() {
return $this->assertAttached($this->changesets);
}
+ public function setOption($key, $value) {
+ $this->options[$key] = $value;
+ return $this;
+ }
+
+ public function getOption($key, $default = null) {
+ return idx($this->options, $key, $default);
+ }
+
public function isResigned() {
$status_resigned = DifferentialReviewerStatus::STATUS_RESIGNED;
return ($this->getReviewerStatus() == $status_resigned);
}
+ public function isBlocking() {
+ $status_blocking = DifferentialReviewerStatus::STATUS_BLOCKING;
+ return ($this->getReviewerStatus() == $status_blocking);
+ }
+
public function isRejected($diff_phid) {
$status_rejected = DifferentialReviewerStatus::STATUS_REJECTED;
if ($this->getReviewerStatus() != $status_rejected) {
return false;
}
if ($this->getVoidedPHID()) {
return false;
}
if ($this->isCurrentAction($diff_phid)) {
return true;
}
return false;
}
public function isAccepted($diff_phid) {
$status_accepted = DifferentialReviewerStatus::STATUS_ACCEPTED;
if ($this->getReviewerStatus() != $status_accepted) {
return false;
}
// If this accept has been voided (for example, but a reviewer using
// "Request Review"), don't count it as a real "Accept" even if it is
// against the current diff PHID.
if ($this->getVoidedPHID()) {
return false;
}
if ($this->isCurrentAction($diff_phid)) {
return true;
}
$sticky_key = 'differential.sticky-accept';
$is_sticky = PhabricatorEnv::getEnvConfig($sticky_key);
if ($is_sticky) {
return true;
}
return false;
}
private function isCurrentAction($diff_phid) {
if (!$diff_phid) {
return true;
}
$action_phid = $this->getLastActionDiffPHID();
if (!$action_phid) {
return true;
}
if ($action_phid == $diff_phid) {
return true;
}
return false;
}
}
diff --git a/src/applications/differential/storage/DifferentialRevision.php b/src/applications/differential/storage/DifferentialRevision.php
index 2c82de164..2f7cee110 100644
--- a/src/applications/differential/storage/DifferentialRevision.php
+++ b/src/applications/differential/storage/DifferentialRevision.php
@@ -1,1065 +1,1188 @@
<?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 $originalTitle;
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_HAS_BROADCAST = 'draft.broadcast';
+ 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);
+ ->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',
- 'originalTitle' => '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 setTitle($title) {
- $this->title = $title;
- if (!$this->getID()) {
- $this->originalTitle = $title;
- }
- return $this;
- }
-
public function loadIDsByCommitPHIDs($phids) {
if (!$phids) {
return array();
}
$revision_ids = queryfx_all(
$this->establishConnection('r'),
'SELECT * FROM %T WHERE commitPHID IN (%Ls)',
self::TABLE_COMMIT,
$phids);
return ipull($revision_ids, 'revisionID', 'commitPHID');
}
public function loadCommitPHIDs() {
if (!$this->getID()) {
return ($this->commits = array());
}
$commits = queryfx_all(
$this->establishConnection('r'),
'SELECT commitPHID FROM %T WHERE revisionID = %d',
self::TABLE_COMMIT,
$this->getID());
$commits = ipull($commits, 'commitPHID');
return ($this->commits = $commits);
}
public function getCommitPHIDs() {
return $this->assertAttached($this->commits);
}
public function getActiveDiff() {
// TODO: Because it's currently technically possible to create a revision
// without an associated diff, we allow an attached-but-null active diff.
// It would be good to get rid of this once we make diff-attaching
// transactional.
return $this->assertAttached($this->activeDiff);
}
public function attachActiveDiff($diff) {
$this->activeDiff = $diff;
return $this;
}
public function getDiffIDs() {
return $this->assertAttached($this->diffIDs);
}
public function attachDiffIDs(array $ids) {
rsort($ids);
$this->diffIDs = array_values($ids);
return $this;
}
public function attachCommitPHIDs(array $phids) {
$this->commits = array_values($phids);
return $this;
}
public function getAttachedPHIDs($type) {
return array_keys(idx($this->attached, $type, array()));
}
public function setAttachedPHIDs($type, array $phids) {
$this->attached[$type] = array_fill_keys($phids, array());
return $this;
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
DifferentialRevisionPHIDType::TYPECONST);
}
public function loadActiveDiff() {
return id(new DifferentialDiff())->loadOneWhere(
'revisionID = %d ORDER BY id DESC LIMIT 1',
$this->getID());
}
public function save() {
if (!$this->getMailKey()) {
$this->mailKey = Filesystem::readRandomCharacters(40);
}
return parent::save();
}
public function 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 shouldBroadcast() {
- if (!$this->isDraft()) {
- return true;
- }
-
- return false;
- }
-
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 getHasBroadcast() {
- return $this->getProperty(self::PROPERTY_HAS_BROADCAST, false);
+ public function getShouldBroadcast() {
+ return $this->getProperty(self::PROPERTY_SHOULD_BROADCAST, true);
}
- public function setHasBroadcast($has_broadcast) {
- return $this->setProperty(self::PROPERTY_HAS_BROADCAST, $has_broadcast);
+ 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 loadActiveBuilds(PhabricatorUser $viewer) {
+ 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)
- ->withContainerPHIDs(array($this->getPHID()))
->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(mpull($buildables, 'getPHID'))
+ ->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 getHarbormasterPublishablePHID() {
- 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) {
- return true;
+ 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.')),
);
}
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(),
);
}
public function getConduitSearchAttachments() {
return array(
id(new DifferentialReviewersSearchEngineAttachment())
->setAttachmentKey('reviewers'),
);
}
/* -( PhabricatorDraftInterface )------------------------------------------ */
public function newDraftEngine() {
return new DifferentialRevisionDraftEngine();
}
}
diff --git a/src/applications/differential/storage/DifferentialTransaction.php b/src/applications/differential/storage/DifferentialTransaction.php
index f7dcf2986..53fdc71a1 100644
--- a/src/applications/differential/storage/DifferentialTransaction.php
+++ b/src/applications/differential/storage/DifferentialTransaction.php
@@ -1,644 +1,586 @@
<?php
final class DifferentialTransaction
extends PhabricatorModularTransaction {
private $isCommandeerSideEffect;
const TYPE_INLINE = 'differential:inline';
- const TYPE_UPDATE = 'differential:update';
const TYPE_ACTION = 'differential:action';
const MAILTAG_REVIEWERS = 'differential-reviewers';
const MAILTAG_CLOSED = 'differential-committed';
const MAILTAG_CC = 'differential-cc';
const MAILTAG_COMMENT = 'differential-comment';
const MAILTAG_UPDATED = 'differential-updated';
const MAILTAG_REVIEW_REQUEST = 'differential-review-request';
const MAILTAG_OTHER = 'differential-other';
public function getBaseTransactionClass() {
return 'DifferentialRevisionTransactionType';
}
protected function newFallbackModularTransactionType() {
// TODO: This allows us to render modern strings for older transactions
// without doing a migration. At some point, we should do a migration and
// throw this away.
// NOTE: Old reviewer edits are raw edge transactions. They could be
// migrated to modular transactions when the rest of this migrates.
$xaction_type = $this->getTransactionType();
if ($xaction_type == PhabricatorTransactions::TYPE_CUSTOMFIELD) {
switch ($this->getMetadataValue('customfield:key')) {
case 'differential:title':
return new DifferentialRevisionTitleTransaction();
case 'differential:test-plan':
return new DifferentialRevisionTestPlanTransaction();
case 'differential:repository':
return new DifferentialRevisionRepositoryTransaction();
}
}
return parent::newFallbackModularTransactionType();
}
public function setIsCommandeerSideEffect($is_side_effect) {
$this->isCommandeerSideEffect = $is_side_effect;
return $this;
}
public function getIsCommandeerSideEffect() {
return $this->isCommandeerSideEffect;
}
public function getApplicationName() {
return 'differential';
}
public function getApplicationTransactionType() {
return DifferentialRevisionPHIDType::TYPECONST;
}
public function getApplicationTransactionCommentObject() {
return new DifferentialTransactionComment();
}
public function getApplicationTransactionViewObject() {
return new DifferentialTransactionView();
}
public function shouldHide() {
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
- case self::TYPE_UPDATE:
- // Older versions of this transaction have an ID for the new value,
- // and/or do not record the old value. Only hide the transaction if
- // the new value is a PHID, indicating that this is a newer style
- // transaction.
- if ($old === null) {
- if (phid_get_type($new) == DifferentialDiffPHIDType::TYPECONST) {
- return true;
- }
- }
- break;
-
case DifferentialRevisionRequestReviewTransaction::TRANSACTIONTYPE:
// Don't hide the initial "X requested review: ..." transaction from
// mail or feed even when it occurs during creation. We need this
// transaction to survive so we'll generate mail and feed stories when
// revisions immediately leave the draft state. See T13035 for
// discussion.
return false;
}
return parent::shouldHide();
}
public function shouldHideForMail(array $xactions) {
switch ($this->getTransactionType()) {
case DifferentialRevisionReviewersTransaction::TRANSACTIONTYPE:
// Don't hide the initial "X added reviewers: ..." transaction during
// object creation from mail. See T12118 and PHI54.
return false;
}
return parent::shouldHideForMail($xactions);
}
public function isInlineCommentTransaction() {
switch ($this->getTransactionType()) {
case self::TYPE_INLINE:
return true;
}
return parent::isInlineCommentTransaction();
}
public function getRequiredHandlePHIDs() {
$phids = parent::getRequiredHandlePHIDs();
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_ACTION:
if ($new == DifferentialAction::ACTION_CLOSE &&
$this->getMetadataValue('isCommitClose')) {
$phids[] = $this->getMetadataValue('commitPHID');
if ($this->getMetadataValue('committerPHID')) {
$phids[] = $this->getMetadataValue('committerPHID');
}
if ($this->getMetadataValue('authorPHID')) {
$phids[] = $this->getMetadataValue('authorPHID');
}
}
break;
- case self::TYPE_UPDATE:
- if ($new) {
- $phids[] = $new;
- }
- break;
}
return $phids;
}
public function getActionStrength() {
switch ($this->getTransactionType()) {
case self::TYPE_ACTION:
return 3;
- case self::TYPE_UPDATE:
- return 2;
}
return parent::getActionStrength();
}
public function getActionName() {
switch ($this->getTransactionType()) {
case self::TYPE_INLINE:
return pht('Commented On');
- case self::TYPE_UPDATE:
- $old = $this->getOldValue();
- if ($old === null) {
- return pht('Request');
- } else {
- return pht('Updated');
- }
case self::TYPE_ACTION:
$map = array(
DifferentialAction::ACTION_ACCEPT => pht('Accepted'),
DifferentialAction::ACTION_REJECT => pht('Requested Changes To'),
DifferentialAction::ACTION_RETHINK => pht('Planned Changes To'),
DifferentialAction::ACTION_ABANDON => pht('Abandoned'),
DifferentialAction::ACTION_CLOSE => pht('Closed'),
DifferentialAction::ACTION_REQUEST => pht('Requested A Review Of'),
DifferentialAction::ACTION_RESIGN => pht('Resigned From'),
DifferentialAction::ACTION_ADDREVIEWERS => pht('Added Reviewers'),
DifferentialAction::ACTION_CLAIM => pht('Commandeered'),
DifferentialAction::ACTION_REOPEN => pht('Reopened'),
);
$name = idx($map, $this->getNewValue());
if ($name !== null) {
return $name;
}
break;
}
return parent::getActionName();
}
public function getMailTags() {
$tags = array();
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_SUBSCRIBERS;
$tags[] = self::MAILTAG_CC;
break;
case self::TYPE_ACTION:
switch ($this->getNewValue()) {
case DifferentialAction::ACTION_CLOSE:
$tags[] = self::MAILTAG_CLOSED;
break;
}
break;
- case self::TYPE_UPDATE:
+ case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE:
$old = $this->getOldValue();
if ($old === null) {
$tags[] = self::MAILTAG_REVIEW_REQUEST;
} else {
$tags[] = self::MAILTAG_UPDATED;
}
break;
case PhabricatorTransactions::TYPE_COMMENT:
case self::TYPE_INLINE:
$tags[] = self::MAILTAG_COMMENT;
break;
case DifferentialRevisionReviewersTransaction::TRANSACTIONTYPE:
$tags[] = self::MAILTAG_REVIEWERS;
break;
case DifferentialRevisionCloseTransaction::TRANSACTIONTYPE:
$tags[] = self::MAILTAG_CLOSED;
break;
}
if (!$tags) {
$tags[] = self::MAILTAG_OTHER;
}
return $tags;
}
public function getTitle() {
$author_phid = $this->getAuthorPHID();
$author_handle = $this->renderHandleLink($author_phid);
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_INLINE:
return pht(
'%s added inline comments.',
$author_handle);
- case self::TYPE_UPDATE:
- if ($this->getMetadataValue('isCommitUpdate')) {
- return pht(
- 'This revision was automatically updated to reflect the '.
- 'committed changes.');
- } else if ($new) {
- // TODO: Migrate to PHIDs and use handles here?
- if (phid_get_type($new) == DifferentialDiffPHIDType::TYPECONST) {
- return pht(
- '%s updated this revision to %s.',
- $author_handle,
- $this->renderHandleLink($new));
- } else {
- return pht(
- '%s updated this revision.',
- $author_handle);
- }
- } else {
- return pht(
- '%s updated this revision.',
- $author_handle);
- }
case self::TYPE_ACTION:
switch ($new) {
case DifferentialAction::ACTION_CLOSE:
if (!$this->getMetadataValue('isCommitClose')) {
return DifferentialAction::getBasicStoryText(
$new,
$author_handle);
}
$commit_name = $this->renderHandleLink(
$this->getMetadataValue('commitPHID'));
$committer_phid = $this->getMetadataValue('committerPHID');
$author_phid = $this->getMetadataValue('authorPHID');
if ($this->getHandleIfExists($committer_phid)) {
$committer_name = $this->renderHandleLink($committer_phid);
} else {
$committer_name = $this->getMetadataValue('committerName');
}
if ($this->getHandleIfExists($author_phid)) {
$author_name = $this->renderHandleLink($author_phid);
} else {
$author_name = $this->getMetadataValue('authorName');
}
if ($committer_name && ($committer_name != $author_name)) {
return pht(
'Closed by commit %s (authored by %s, committed by %s).',
$commit_name,
$author_name,
$committer_name);
} else {
return pht(
'Closed by commit %s (authored by %s).',
$commit_name,
$author_name);
}
break;
default:
return DifferentialAction::getBasicStoryText($new, $author_handle);
}
break;
}
return parent::getTitle();
}
public function renderExtraInformationLink() {
if ($this->getMetadataValue('revisionMatchData')) {
$details_href =
'/differential/revision/closedetails/'.$this->getPHID().'/';
$details_link = javelin_tag(
'a',
array(
'href' => $details_href,
'sigil' => 'workflow',
),
pht('Explain Why'));
return $details_link;
}
return parent::renderExtraInformationLink();
}
public function getTitleForFeed() {
$author_phid = $this->getAuthorPHID();
$object_phid = $this->getObjectPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
$author_link = $this->renderHandleLink($author_phid);
$object_link = $this->renderHandleLink($object_phid);
switch ($this->getTransactionType()) {
case self::TYPE_INLINE:
return pht(
'%s added inline comments to %s.',
$author_link,
$object_link);
- case self::TYPE_UPDATE:
- return pht(
- '%s updated the diff for %s.',
- $author_link,
- $object_link);
case self::TYPE_ACTION:
switch ($new) {
case DifferentialAction::ACTION_ACCEPT:
return pht(
'%s accepted %s.',
$author_link,
$object_link);
case DifferentialAction::ACTION_REJECT:
return pht(
'%s requested changes to %s.',
$author_link,
$object_link);
case DifferentialAction::ACTION_RETHINK:
return pht(
'%s planned changes to %s.',
$author_link,
$object_link);
case DifferentialAction::ACTION_ABANDON:
return pht(
'%s abandoned %s.',
$author_link,
$object_link);
case DifferentialAction::ACTION_CLOSE:
if (!$this->getMetadataValue('isCommitClose')) {
return pht(
'%s closed %s.',
$author_link,
$object_link);
} else {
$commit_name = $this->renderHandleLink(
$this->getMetadataValue('commitPHID'));
$committer_phid = $this->getMetadataValue('committerPHID');
$author_phid = $this->getMetadataValue('authorPHID');
if ($this->getHandleIfExists($committer_phid)) {
$committer_name = $this->renderHandleLink($committer_phid);
} else {
$committer_name = $this->getMetadataValue('committerName');
}
if ($this->getHandleIfExists($author_phid)) {
$author_name = $this->renderHandleLink($author_phid);
} else {
$author_name = $this->getMetadataValue('authorName');
}
// Check if the committer and author are the same. They're the
// same if both resolved and are the same user, or if neither
// resolved and the text is identical.
if ($committer_phid && $author_phid) {
$same_author = ($committer_phid == $author_phid);
} else if (!$committer_phid && !$author_phid) {
$same_author = ($committer_name == $author_name);
} else {
$same_author = false;
}
if ($committer_name && !$same_author) {
return pht(
'%s closed %s by committing %s (authored by %s).',
$author_link,
$object_link,
$commit_name,
$author_name);
} else {
return pht(
'%s closed %s by committing %s.',
$author_link,
$object_link,
$commit_name);
}
}
break;
case DifferentialAction::ACTION_REQUEST:
return pht(
'%s requested review of %s.',
$author_link,
$object_link);
case DifferentialAction::ACTION_RECLAIM:
return pht(
'%s reclaimed %s.',
$author_link,
$object_link);
case DifferentialAction::ACTION_RESIGN:
return pht(
'%s resigned from %s.',
$author_link,
$object_link);
case DifferentialAction::ACTION_CLAIM:
return pht(
'%s commandeered %s.',
$author_link,
$object_link);
case DifferentialAction::ACTION_REOPEN:
return pht(
'%s reopened %s.',
$author_link,
$object_link);
}
break;
}
return parent::getTitleForFeed();
}
public function getIcon() {
switch ($this->getTransactionType()) {
case self::TYPE_INLINE:
return 'fa-comment';
- case self::TYPE_UPDATE:
- return 'fa-refresh';
case self::TYPE_ACTION:
switch ($this->getNewValue()) {
case DifferentialAction::ACTION_CLOSE:
return 'fa-check';
case DifferentialAction::ACTION_ACCEPT:
return 'fa-check-circle-o';
case DifferentialAction::ACTION_REJECT:
return 'fa-times-circle-o';
case DifferentialAction::ACTION_ABANDON:
return 'fa-plane';
case DifferentialAction::ACTION_RETHINK:
return 'fa-headphones';
case DifferentialAction::ACTION_REQUEST:
return 'fa-refresh';
case DifferentialAction::ACTION_RECLAIM:
case DifferentialAction::ACTION_REOPEN:
return 'fa-bullhorn';
case DifferentialAction::ACTION_RESIGN:
return 'fa-flag';
case DifferentialAction::ACTION_CLAIM:
return 'fa-flag';
}
case PhabricatorTransactions::TYPE_EDGE:
switch ($this->getMetadataValue('edge:type')) {
case DifferentialRevisionHasReviewerEdgeType::EDGECONST:
return 'fa-user';
}
}
return parent::getIcon();
}
public function shouldDisplayGroupWith(array $group) {
// Never group status changes with other types of actions, they're indirect
// and don't make sense when combined with direct actions.
if ($this->isStatusTransaction($this)) {
return false;
}
foreach ($group as $xaction) {
if ($this->isStatusTransaction($xaction)) {
return false;
}
}
return parent::shouldDisplayGroupWith($group);
}
private function isStatusTransaction($xaction) {
$status_type = DifferentialRevisionStatusTransaction::TRANSACTIONTYPE;
if ($xaction->getTransactionType() == $status_type) {
return true;
}
return false;
}
public function getColor() {
switch ($this->getTransactionType()) {
- case self::TYPE_UPDATE:
- return PhabricatorTransactions::COLOR_SKY;
case self::TYPE_ACTION:
switch ($this->getNewValue()) {
case DifferentialAction::ACTION_CLOSE:
return PhabricatorTransactions::COLOR_INDIGO;
case DifferentialAction::ACTION_ACCEPT:
return PhabricatorTransactions::COLOR_GREEN;
case DifferentialAction::ACTION_REJECT:
return PhabricatorTransactions::COLOR_RED;
case DifferentialAction::ACTION_ABANDON:
return PhabricatorTransactions::COLOR_INDIGO;
case DifferentialAction::ACTION_RETHINK:
return PhabricatorTransactions::COLOR_RED;
case DifferentialAction::ACTION_REQUEST:
return PhabricatorTransactions::COLOR_SKY;
case DifferentialAction::ACTION_RECLAIM:
return PhabricatorTransactions::COLOR_SKY;
case DifferentialAction::ACTION_REOPEN:
return PhabricatorTransactions::COLOR_SKY;
case DifferentialAction::ACTION_RESIGN:
return PhabricatorTransactions::COLOR_ORANGE;
case DifferentialAction::ACTION_CLAIM:
return PhabricatorTransactions::COLOR_YELLOW;
}
}
return parent::getColor();
}
public function getNoEffectDescription() {
switch ($this->getTransactionType()) {
case self::TYPE_ACTION:
switch ($this->getNewValue()) {
case DifferentialAction::ACTION_CLOSE:
return pht('This revision is already closed.');
case DifferentialAction::ACTION_ABANDON:
return pht('This revision has already been abandoned.');
case DifferentialAction::ACTION_RECLAIM:
return pht(
'You can not reclaim this revision because his revision is '.
'not abandoned.');
case DifferentialAction::ACTION_REOPEN:
return pht(
'You can not reopen this revision because this revision is '.
'not closed.');
case DifferentialAction::ACTION_RETHINK:
return pht('This revision already requires changes.');
case DifferentialAction::ACTION_CLAIM:
return pht(
'You can not commandeer this revision because you already own '.
'it.');
}
break;
}
return parent::getNoEffectDescription();
}
public function renderAsTextForDoorkeeper(
DoorkeeperFeedStoryPublisher $publisher,
PhabricatorFeedStory $story,
array $xactions) {
$body = parent::renderAsTextForDoorkeeper($publisher, $story, $xactions);
$inlines = array();
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() == self::TYPE_INLINE) {
$inlines[] = $xaction;
}
}
// TODO: This is a bit gross, but far less bad than it used to be. It
// could be further cleaned up at some point.
if ($inlines) {
$engine = PhabricatorMarkupEngine::newMarkupEngine(array())
->setConfig('viewer', new PhabricatorUser())
->setMode(PhutilRemarkupEngine::MODE_TEXT);
$body .= "\n\n";
$body .= pht('Inline Comments');
$body .= "\n";
$changeset_ids = array();
foreach ($inlines as $inline) {
$changeset_ids[] = $inline->getComment()->getChangesetID();
}
$changesets = id(new DifferentialChangeset())->loadAllWhere(
'id IN (%Ld)',
$changeset_ids);
foreach ($inlines as $inline) {
$comment = $inline->getComment();
$changeset = idx($changesets, $comment->getChangesetID());
if (!$changeset) {
continue;
}
$filename = $changeset->getDisplayFilename();
$linenumber = $comment->getLineNumber();
$inline_text = $engine->markupText($comment->getContent());
$inline_text = rtrim($inline_text);
$body .= "{$filename}:{$linenumber} {$inline_text}\n";
}
}
return $body;
}
}
diff --git a/src/applications/differential/storage/__tests__/DifferentialHunkTestCase.php b/src/applications/differential/storage/__tests__/DifferentialHunkTestCase.php
index 49866ac03..e334891e6 100644
--- a/src/applications/differential/storage/__tests__/DifferentialHunkTestCase.php
+++ b/src/applications/differential/storage/__tests__/DifferentialHunkTestCase.php
@@ -1,353 +1,353 @@
<?php
final class DifferentialHunkTestCase extends PhutilTestCase {
public function testMakeChanges() {
$root = dirname(__FILE__).'/hunk/';
- $hunk = new DifferentialModernHunk();
+ $hunk = new DifferentialHunk();
$hunk->setChanges(Filesystem::readFile($root.'basic.diff'));
$hunk->setOldOffset(1);
$hunk->setNewOffset(11);
$old = Filesystem::readFile($root.'old.txt');
$this->assertEqual($old, $hunk->makeOldFile());
$new = Filesystem::readFile($root.'new.txt');
$this->assertEqual($new, $hunk->makeNewFile());
$added = array(
12 => "1 quack\n",
13 => "1 quack\n",
16 => "5 drake\n",
);
$this->assertEqual($added, $hunk->getAddedLines());
- $hunk = new DifferentialModernHunk();
+ $hunk = new DifferentialHunk();
$hunk->setChanges(Filesystem::readFile($root.'newline.diff'));
$hunk->setOldOffset(1);
$hunk->setNewOffset(11);
$this->assertEqual("a\n", $hunk->makeOldFile());
$this->assertEqual('a', $hunk->makeNewFile());
$this->assertEqual(array(11 => 'a'), $hunk->getAddedLines());
}
public function testMakeStructuredChanges1() {
$hunk = $this->loadHunk('fruit1.diff');
$this->assertEqual(
array(
1 => array(
'type' => ' ',
'text' => "apple\n",
),
2 => array(
'type' => ' ',
'text' => "cherry\n",
),
),
$hunk->getStructuredOldFile());
$this->assertEqual(
array(
1 => array(
'type' => ' ',
'text' => "apple\n",
),
2 => array(
'type' => '+',
'text' => "banana\n",
),
3 => array(
'type' => '+',
'text' => "plum\n",
),
4 => array(
'type' => ' ',
'text' => "cherry\n",
),
),
$hunk->getStructuredNewFile());
}
public function testMakeStructuredChanges2() {
$hunk = $this->loadHunk('fruit2.diff');
$this->assertEqual(
array(
1 => array(
'type' => ' ',
'text' => "apple\n",
),
2 => array(
'type' => ' ',
'text' => "banana\n",
),
3 => array(
'type' => '-',
'text' => "plum\n",
),
4 => array(
'type' => ' ',
'text' => "cherry\n",
),
),
$hunk->getStructuredOldFile());
$this->assertEqual(
array(
1 => array(
'type' => ' ',
'text' => "apple\n",
),
2 => array(
'type' => ' ',
'text' => "banana\n",
),
3 => array(
'type' => ' ',
'text' => "cherry\n",
),
),
$hunk->getStructuredNewFile());
}
public function testMakeStructuredNewlineAdded() {
$hunk = $this->loadHunk('trailing_newline_added.diff');
$this->assertEqual(
array(
1 => array(
'type' => ' ',
'text' => "quack\n",
),
2 => array(
'type' => ' ',
'text' => "quack\n",
),
3 => array(
'type' => '-',
'text' => 'quack',
),
4 => array(
'type' => '\\',
'text' => " No newline at end of file\n",
),
),
$hunk->getStructuredOldFile());
$this->assertEqual(
array(
1 => array(
'type' => ' ',
'text' => "quack\n",
),
2 => array(
'type' => ' ',
'text' => "quack\n",
),
3 => array(
'type' => '+',
'text' => "quack\n",
),
),
$hunk->getStructuredNewFile());
}
public function testMakeStructuredNewlineRemoved() {
$hunk = $this->loadHunk('trailing_newline_removed.diff');
$this->assertEqual(
array(
1 => array(
'type' => ' ',
'text' => "quack\n",
),
2 => array(
'type' => ' ',
'text' => "quack\n",
),
3 => array(
'type' => '-',
'text' => "quack\n",
),
),
$hunk->getStructuredOldFile());
$this->assertEqual(
array(
1 => array(
'type' => ' ',
'text' => "quack\n",
),
2 => array(
'type' => ' ',
'text' => "quack\n",
),
3 => array(
'type' => '+',
'text' => 'quack',
),
4 => array(
'type' => '\\',
'text' => " No newline at end of file\n",
),
),
$hunk->getStructuredNewFile());
}
public function testMakeStructuredNewlineAbsent() {
$hunk = $this->loadHunk('trailing_newline_absent.diff');
$this->assertEqual(
array(
1 => array(
'type' => '-',
'text' => "quack\n",
),
2 => array(
'type' => ' ',
'text' => "quack\n",
),
3 => array(
'type' => ' ',
'text' => 'quack',
),
4 => array(
'type' => '\\',
'text' => " No newline at end of file\n",
),
),
$hunk->getStructuredOldFile());
$this->assertEqual(
array(
1 => array(
'type' => '+',
'text' => "meow\n",
),
2 => array(
'type' => ' ',
'text' => "quack\n",
),
3 => array(
'type' => ' ',
'text' => 'quack',
),
4 => array(
'type' => '\\',
'text' => " No newline at end of file\n",
),
),
$hunk->getStructuredNewFile());
}
public function testMakeStructuredOffset() {
$hunk = $this->loadHunk('offset.diff');
$this->assertEqual(
array(
76 => array(
'type' => ' ',
'text' => " \$bits .= '0';\n",
),
77 => array(
'type' => ' ',
'text' => " }\n",
),
78 => array(
'type' => ' ',
'text' => " }\n",
),
79 => array(
'type' => ' ',
'text' => " }\n",
),
80 => array(
'type' => '-',
'text' => " \$this->bits = \$bits;\n",
),
81 => array(
'type' => ' ',
'text' => " }\n",
),
82 => array(
'type' => ' ',
'text' => " return \$this->bits;\n",
),
),
$hunk->getStructuredOldFile());
$this->assertEqual(
array(
76 => array(
'type' => ' ',
'text' => " \$bits .= '0';\n",
),
77 => array(
'type' => ' ',
'text' => " }\n",
),
78 => array(
'type' => ' ',
'text' => " }\n",
),
79 => array(
'type' => '+',
'text' => " break;\n",
),
80 => array(
'type' => ' ',
'text' => " }\n",
),
81 => array(
'type' => '+',
'text' => " \$this->bits = \$bytes;\n",
),
82 => array(
'type' => ' ',
'text' => " }\n",
),
83 => array(
'type' => ' ',
'text' => " return \$this->bits;\n",
),
),
$hunk->getStructuredNewFile());
}
private function loadHunk($name) {
$root = dirname(__FILE__).'/hunk/';
$data = Filesystem::readFile($root.$name);
$parser = new ArcanistDiffParser();
$changes = $parser->parseDiff($data);
$viewer = PhabricatorUser::getOmnipotentUser();
$diff = DifferentialDiff::newFromRawChanges($viewer, $changes);
$changesets = $diff->getChangesets();
if (count($changesets) !== 1) {
throw new Exception(
pht(
'Expected exactly one changeset from "%s".',
$name));
}
$changeset = head($changesets);
$hunks = $changeset->getHunks();
if (count($hunks) !== 1) {
throw new Exception(
pht(
'Expected exactly one hunk from "%s".',
$name));
}
$hunk = head($hunks);
return $hunk;
}
}
diff --git a/src/applications/differential/view/DifferentialChangesetDetailView.php b/src/applications/differential/view/DifferentialChangesetDetailView.php
index d4a13745d..cb697c2e9 100644
--- a/src/applications/differential/view/DifferentialChangesetDetailView.php
+++ b/src/applications/differential/view/DifferentialChangesetDetailView.php
@@ -1,238 +1,239 @@
<?php
final class DifferentialChangesetDetailView extends AphrontView {
private $changeset;
private $buttons = array();
private $editable;
private $symbolIndex;
private $id;
private $vsChangesetID;
private $renderURI;
private $whitespace;
private $renderingRef;
private $autoload;
private $loaded;
private $renderer;
public function setAutoload($autoload) {
$this->autoload = $autoload;
return $this;
}
public function getAutoload() {
return $this->autoload;
}
public function setLoaded($loaded) {
$this->loaded = $loaded;
return $this;
}
public function getLoaded() {
return $this->loaded;
}
public function setRenderingRef($rendering_ref) {
$this->renderingRef = $rendering_ref;
return $this;
}
public function getRenderingRef() {
return $this->renderingRef;
}
public function setWhitespace($whitespace) {
$this->whitespace = $whitespace;
return $this;
}
public function getWhitespace() {
return $this->whitespace;
}
public function setRenderURI($render_uri) {
$this->renderURI = $render_uri;
return $this;
}
public function getRenderURI() {
return $this->renderURI;
}
public function setChangeset($changeset) {
$this->changeset = $changeset;
return $this;
}
public function addButton($button) {
$this->buttons[] = $button;
return $this;
}
public function setEditable($editable) {
$this->editable = $editable;
return $this;
}
public function setSymbolIndex($symbol_index) {
$this->symbolIndex = $symbol_index;
return $this;
}
public function setRenderer($renderer) {
$this->renderer = $renderer;
return $this;
}
public function getRenderer() {
return $this->renderer;
}
public function getID() {
if (!$this->id) {
$this->id = celerity_generate_unique_node_id();
}
return $this->id;
}
public function setID($id) {
$this->id = $id;
return $this;
}
public function setVsChangesetID($vs_changeset_id) {
$this->vsChangesetID = $vs_changeset_id;
return $this;
}
public function getVsChangesetID() {
return $this->vsChangesetID;
}
public function render() {
$this->requireResource('differential-changeset-view-css');
$this->requireResource('syntax-highlighting-css');
Javelin::initBehavior('phabricator-oncopy', array());
$changeset = $this->changeset;
$class = 'differential-changeset';
if (!$this->editable) {
$class .= ' differential-changeset-immutable';
}
$buttons = null;
if ($this->buttons) {
$buttons = phutil_tag(
'div',
array(
'class' => 'differential-changeset-buttons',
),
$this->buttons);
}
$id = $this->getID();
if ($this->symbolIndex) {
Javelin::initBehavior(
'repository-crossreference',
array(
'container' => $id,
) + $this->symbolIndex);
}
$display_filename = $changeset->getDisplayFilename();
$display_icon = FileTypeIcon::getFileIcon($display_filename);
$icon = id(new PHUIIconView())
->setIcon($display_icon);
$renderer = DifferentialChangesetHTMLRenderer::getHTMLRendererByKey(
$this->getRenderer());
$changeset_id = $this->changeset->getID();
$vs_id = $this->getVsChangesetID();
if (!$vs_id) {
// Showing a changeset normally.
$left_id = $changeset_id;
$right_id = $changeset_id;
} else if ($vs_id == -1) {
// Showing a synthetic "deleted" changeset for a file which was
// removed between changes.
$left_id = $changeset_id;
$right_id = null;
} else {
// Showing a diff-of-diffs.
$left_id = $vs_id;
$right_id = $changeset_id;
}
// In the persistent banner, emphasize the current filename.
$path_part = dirname($display_filename);
$file_part = basename($display_filename);
$display_parts = array();
if (strlen($path_part)) {
$path_part = $path_part.'/';
$display_parts[] = phutil_tag(
'span',
array(
'class' => 'diff-banner-path',
),
$path_part);
}
$display_parts[] = phutil_tag(
'span',
array(
'class' => 'diff-banner-file',
),
$file_part);
return javelin_tag(
'div',
array(
'sigil' => 'differential-changeset',
'meta' => array(
'left' => $left_id,
'right' => $right_id,
'renderURI' => $this->getRenderURI(),
'whitespace' => $this->getWhitespace(),
'highlight' => null,
'renderer' => $this->getRenderer(),
'ref' => $this->getRenderingRef(),
'autoload' => $this->getAutoload(),
'loaded' => $this->getLoaded(),
'undoTemplates' => hsprintf('%s', $renderer->renderUndoTemplates()),
'displayPath' => hsprintf('%s', $display_parts),
'path' => $display_filename,
'icon' => $display_icon,
+ 'treeNodeID' => 'tree-node-'.$changeset->getAnchorName(),
),
'class' => $class,
'id' => $id,
),
array(
id(new PhabricatorAnchorView())
->setAnchorName($changeset->getAnchorName())
->setNavigationMarker(true)
->render(),
$buttons,
phutil_tag('h1',
array(
'class' => 'differential-file-icon-header',
),
array(
$icon,
$display_filename,
)),
javelin_tag(
'div',
array(
'class' => 'changeset-view-content',
'sigil' => 'changeset-view-content',
),
$this->renderChildren()),
));
}
}
diff --git a/src/applications/differential/view/DifferentialChangesetFileTreeSideNavBuilder.php b/src/applications/differential/view/DifferentialChangesetFileTreeSideNavBuilder.php
index 14050e942..e8852a5c5 100644
--- a/src/applications/differential/view/DifferentialChangesetFileTreeSideNavBuilder.php
+++ b/src/applications/differential/view/DifferentialChangesetFileTreeSideNavBuilder.php
@@ -1,137 +1,158 @@
<?php
final class DifferentialChangesetFileTreeSideNavBuilder extends Phobject {
private $title;
private $baseURI;
private $anchorName;
private $collapsed = false;
+ private $width;
public function setAnchorName($anchor_name) {
$this->anchorName = $anchor_name;
return $this;
}
public function getAnchorName() {
return $this->anchorName;
}
public function setBaseURI(PhutilURI $base_uri) {
$this->baseURI = $base_uri;
return $this;
}
public function getBaseURI() {
return $this->baseURI;
}
public function setTitle($title) {
$this->title = $title;
return $this;
}
public function getTitle() {
return $this->title;
}
public function setCollapsed($collapsed) {
$this->collapsed = $collapsed;
return $this;
}
+ public function setWidth($width) {
+ $this->width = $width;
+ return $this;
+ }
+
public function build(array $changesets) {
assert_instances_of($changesets, 'DifferentialChangeset');
- $nav = new AphrontSideNavFilterView();
- $nav->setBaseURI($this->getBaseURI());
- $nav->setFlexible(true);
- $nav->setCollapsed($this->collapsed);
+ $nav = id(new AphrontSideNavFilterView())
+ ->setBaseURI($this->getBaseURI())
+ ->setFlexible(true)
+ ->setCollapsed($this->collapsed)
+ ->setWidth($this->width);
$anchor = $this->getAnchorName();
$tree = new PhutilFileTree();
foreach ($changesets as $changeset) {
try {
$tree->addPath($changeset->getFilename(), $changeset);
} catch (Exception $ex) {
// TODO: See T1702. When viewing the versus diff of diffs, we may
// have files with the same filename. For example, if you have a setup
// like this in SVN:
//
// a/
// README
// b/
// README
//
// ...and you run "arc diff" once from a/, and again from b/, you'll
// get two diffs with path README. However, in the versus diff view we
// will compute their absolute repository paths and detect that they
// aren't really the same file. This is correct, but causes us to
// throw when inserting them.
//
// We should probably compute the smallest unique path for each file
// and show these as "a/README" and "b/README" when diffed against
// one another. However, we get this wrong in a lot of places (the
// other TOC shows two "README" files, and we generate the same anchor
// hash for both) so I'm just stopping the bleeding until we can get
// a proper fix in place.
}
}
require_celerity_resource('phabricator-filetree-view-css');
$filetree = array();
$path = $tree;
while (($path = $path->getNextNode())) {
$data = $path->getData();
+ $classes = array();
+ $classes[] = 'phabricator-filetree-item';
+
$name = $path->getName();
$style = 'padding-left: '.(2 + (3 * $path->getDepth())).'px';
$href = null;
if ($data) {
$href = '#'.$data->getAnchorName();
$title = $name;
- $icon = id(new PHUIIconView())
- ->setIcon('fa-file-text-o bluetext');
+
+ $icon = $data->newFileTreeIcon();
+ $classes[] = $data->getFileTreeClass();
+
+ $count = phutil_tag(
+ 'span',
+ array(
+ 'class' => 'filetree-progress-hint',
+ 'id' => 'tree-node-'.$data->getAnchorName(),
+ ));
} else {
$name .= '/';
$title = $path->getFullPath().'/';
$icon = id(new PHUIIconView())
->setIcon('fa-folder-open blue');
+
+ $count = null;
}
$name_element = phutil_tag(
'span',
array(
'class' => 'phabricator-filetree-name',
),
$name);
+
$filetree[] = javelin_tag(
$href ? 'a' : 'span',
array(
'href' => $href,
'style' => $style,
'title' => $title,
- 'class' => 'phabricator-filetree-item',
+ 'class' => implode(' ', $classes),
),
- array($icon, $name_element));
+ array($count, $icon, $name_element));
}
$tree->destroy();
$filetree = phutil_tag(
'div',
array(
'class' => 'phabricator-filetree',
),
$filetree);
Javelin::initBehavior('phabricator-file-tree', array());
$nav->addLabel(pht('Changed Files'));
$nav->addCustomBlock($filetree);
$nav->setActive(true);
$nav->selectFilter(null);
return $nav;
}
}
diff --git a/src/applications/differential/view/DifferentialRevisionListView.php b/src/applications/differential/view/DifferentialRevisionListView.php
index ed9743574..4c69d7627 100644
--- a/src/applications/differential/view/DifferentialRevisionListView.php
+++ b/src/applications/differential/view/DifferentialRevisionListView.php
@@ -1,179 +1,253 @@
<?php
/**
* Render a table of Differential revisions.
*/
final class DifferentialRevisionListView extends AphrontView {
private $revisions = array();
- private $handles;
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 getRequiredHandlePHIDs() {
- $phids = array();
- foreach ($this->revisions as $revision) {
- $phids[] = array($revision->getAuthorPHID());
- $phids[] = $revision->getReviewerPHIDs();
- }
- return array_mergev($phids);
- }
-
- public function setHandles(array $handles) {
- assert_instances_of($handles, 'PhabricatorObjectHandle');
- $this->handles = $handles;
- return $this;
- }
-
public function render() {
$viewer = $this->getViewer();
$this->initBehavior('phabricator-tooltips', array());
$this->requireResource('aphront-tooltip-css');
- $list = new PHUIObjectItemListView();
+ $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');
- foreach ($this->revisions as $revision) {
+ $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())
- ->setUser($viewer);
+ ->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,
),
'');
}
- if ($revision->getHasDraft($viewer)) {
- $icons['draft'] = true;
- }
-
$modified = $revision->getDateModified();
if (isset($icons['flag'])) {
$item->addHeadIcon($icons['flag']);
}
- $item->setObjectName('D'.$revision->getID());
+ $item->setObjectName($revision->getMonogram());
$item->setHeader($revision->getTitle());
- $item->setHref('/D'.$revision->getID());
+ $item->setHref($revision->getURI());
+
+ $size = $this->renderRevisionSize($revision);
+ if ($size !== null) {
+ $item->addAttribute($size);
+ }
- if (isset($icons['draft'])) {
+ 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
- $author_handle = $this->handles[$revision->getAuthorPHID()];
+ $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'),
));
}
- $reviewers = array();
- foreach ($revision->getReviewerPHIDs() as $reviewer) {
- $reviewers[] = $this->handles[$reviewer]->renderLink();
+ $more = null;
+ if ($reviewer_more[$key]) {
+ $more = pht(', ...');
+ } else {
+ $more = null;
}
- if (!$reviewers) {
- $reviewers = phutil_tag('em', array(), pht('None'));
+
+ if ($reviewer_phids[$key]) {
+ $item->addAttribute(
+ array(
+ pht('Reviewers:'),
+ ' ',
+ $viewer->renderHandleList($reviewer_phids[$key])
+ ->setAsInline(true),
+ $more,
+ ));
} else {
- $reviewers = phutil_implode_html(', ', $reviewers);
+ $item->addAttribute(phutil_tag('em', array(), pht('No Reviewers')));
}
- $item->addAttribute(pht('Reviewers: %s', $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';
+
+ if ($plus_count <= 1) {
+ $classes[] = 'differential-revision-small';
+ }
+
+ if ($plus_count >= 4) {
+ $classes[] = 'differential-revision-large';
+ }
+
+ return javelin_tag(
+ 'span',
+ array(
+ 'class' => implode(' ', $classes),
+ 'sigil' => 'has-tooltip',
+ 'meta' => array(
+ 'tip' => pht('%s Lines', new PhutilNumber($n)),
+ 'align' => 'E',
+ ),
+ ),
+ $size);
+ }
+
}
diff --git a/src/applications/differential/xaction/DifferentialRevisionActionTransaction.php b/src/applications/differential/xaction/DifferentialRevisionActionTransaction.php
index a1e66379f..cf2b55776 100644
--- a/src/applications/differential/xaction/DifferentialRevisionActionTransaction.php
+++ b/src/applications/differential/xaction/DifferentialRevisionActionTransaction.php
@@ -1,181 +1,194 @@
<?php
abstract class DifferentialRevisionActionTransaction
extends DifferentialRevisionTransactionType {
final public function getRevisionActionKey() {
return $this->getPhobjectClassConstant('ACTIONKEY', 32);
}
public function isActionAvailable($object, PhabricatorUser $viewer) {
try {
$this->validateAction($object, $viewer);
return true;
} catch (Exception $ex) {
return false;
}
}
abstract protected function validateAction($object, PhabricatorUser $viewer);
abstract protected function getRevisionActionLabel();
protected function validateOptionValue($object, $actor, array $value) {
return null;
}
public function getCommandKeyword() {
return null;
}
public function getCommandAliases() {
return array();
}
public function getCommandSummary() {
return null;
}
protected function getRevisionActionOrder() {
return 1000;
}
public function getActionStrength() {
return 3;
}
public function getRevisionActionOrderVector() {
return id(new PhutilSortVector())
->addInt($this->getRevisionActionOrder());
}
protected function getRevisionActionGroupKey() {
return DifferentialRevisionEditEngine::ACTIONGROUP_REVISION;
}
protected function getRevisionActionDescription(
DifferentialRevision $revision) {
return null;
}
public static function loadAllActions() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getRevisionActionKey')
->execute();
}
protected function isViewerRevisionAuthor(
DifferentialRevision $revision,
PhabricatorUser $viewer) {
if (!$viewer->getPHID()) {
return false;
}
return ($viewer->getPHID() === $revision->getAuthorPHID());
}
protected function getActionOptions(
PhabricatorUser $viewer,
DifferentialRevision $revision) {
return array(
array(),
- null,
+ array(),
);
}
public function newEditField(
DifferentialRevision $revision,
PhabricatorUser $viewer) {
// Actions in the "review" group, like "Accept Revision", do not require
// that the actor be able to edit the revision.
$group_review = DifferentialRevisionEditEngine::ACTIONGROUP_REVIEW;
$is_review = ($this->getRevisionActionGroupKey() == $group_review);
$field = id(new PhabricatorApplyEditField())
->setKey($this->getRevisionActionKey())
->setTransactionType($this->getTransactionTypeConstant())
->setCanApplyWithoutEditCapability($is_review)
->setValue(true);
if ($this->isActionAvailable($revision, $viewer)) {
$label = $this->getRevisionActionLabel();
if ($label !== null) {
$field->setCommentActionLabel($label);
$description = $this->getRevisionActionDescription($revision);
$field->setActionDescription($description);
$group_key = $this->getRevisionActionGroupKey();
$field->setCommentActionGroupKey($group_key);
// Currently, every revision action conflicts with every other
// revision action: for example, you can not simultaneously Accept and
// Reject a revision.
// Under some configurations, some combinations of actions are sort of
// technically permissible. For example, you could reasonably Reject
// and Abandon a revision if "anyone can abandon anything" is enabled.
// It's not clear that these combinations are actually useful, so just
// keep things simple for now.
$field->setActionConflictKey('revision.action');
list($options, $value) = $this->getActionOptions($viewer, $revision);
// Show the options if the user can select on behalf of two or more
// reviewers, or can force-accept on behalf of one or more reviewers,
// or can accept on behalf of a reviewer other than themselves (see
// T12533).
$can_multi = (count($options) > 1);
$can_force = (count($value) < count($options));
$not_self = (head_key($options) != $viewer->getPHID());
if ($can_multi || $can_force || $not_self) {
$field->setOptions($options);
$field->setValue($value);
}
}
}
return $field;
}
public function validateTransactions($object, array $xactions) {
$errors = array();
$actor = $this->getActor();
$action_exception = null;
- try {
- $this->validateAction($object, $actor);
- } catch (Exception $ex) {
- $action_exception = $ex;
+ foreach ($xactions as $xaction) {
+ // If this is a draft demotion action, let it skip all the normal
+ // validation. This is a little hacky and should perhaps move down
+ // into the actual action implementations, but currently we can not
+ // apply this rule in validateAction() because it doesn't operate on
+ // the actual transaction.
+ if ($xaction->getMetadataValue('draft.demote')) {
+ continue;
+ }
+
+ try {
+ $this->validateAction($object, $actor);
+ } catch (Exception $ex) {
+ $action_exception = $ex;
+ }
+
+ break;
}
foreach ($xactions as $xaction) {
if ($action_exception) {
$errors[] = $this->newInvalidError(
$action_exception->getMessage(),
$xaction);
continue;
}
$new = $xaction->getNewValue();
if (!is_array($new)) {
continue;
}
try {
$this->validateOptionValue($object, $actor, $new);
} catch (Exception $ex) {
$errors[] = $this->newInvalidError(
$ex->getMessage(),
$xaction);
}
}
return $errors;
}
}
diff --git a/src/applications/differential/xaction/DifferentialRevisionBuildableTransaction.php b/src/applications/differential/xaction/DifferentialRevisionBuildableTransaction.php
new file mode 100644
index 000000000..3c4eaff72
--- /dev/null
+++ b/src/applications/differential/xaction/DifferentialRevisionBuildableTransaction.php
@@ -0,0 +1,93 @@
+<?php
+
+final class DifferentialRevisionBuildableTransaction
+ extends DifferentialRevisionTransactionType {
+
+ // NOTE: This uses an older constant for compatibility. We should perhaps
+ // migrate these at some point.
+ const TRANSACTIONTYPE = 'harbormaster:buildable';
+
+ public function generateNewValue($object, $value) {
+ return $value;
+ }
+
+ public function generateOldValue($object) {
+ return $object->getBuildableStatus($this->getBuildablePHID());
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $object->setBuildableStatus($this->getBuildablePHID(), $value);
+ }
+
+ public function getIcon() {
+ return $this->newBuildableStatus()->getIcon();
+ }
+
+ public function getColor() {
+ return $this->newBuildableStatus()->getColor();
+ }
+
+ public function getActionName() {
+ return $this->newBuildableStatus()->getActionName();
+ }
+
+ public function shouldHideForFeed() {
+ return !$this->newBuildableStatus()->isFailed();
+ }
+
+ public function shouldHideForMail() {
+ return !$this->newBuildableStatus()->isFailed();
+ }
+
+ public function getTitle() {
+ $new = $this->getNewValue();
+ $buildable_phid = $this->getBuildablePHID();
+
+ switch ($new) {
+ case HarbormasterBuildableStatus::STATUS_PASSED:
+ return pht(
+ '%s completed remote builds in %s.',
+ $this->renderAuthor(),
+ $this->renderHandle($buildable_phid));
+ case HarbormasterBuildableStatus::STATUS_FAILED:
+ return pht(
+ '%s failed remote builds in %s!',
+ $this->renderAuthor(),
+ $this->renderHandle($buildable_phid));
+ }
+
+ return null;
+ }
+
+ public function getTitleForFeed() {
+ $new = $this->getNewValue();
+ $buildable_phid = $this->getBuildablePHID();
+
+ switch ($new) {
+ case HarbormasterBuildableStatus::STATUS_PASSED:
+ return pht(
+ '%s completed remote builds in %s for %s.',
+ $this->renderAuthor(),
+ $this->renderHandle($buildable_phid),
+ $this->renderObject());
+ case HarbormasterBuildableStatus::STATUS_FAILED:
+ return pht(
+ '%s failed remote builds in %s for %s!',
+ $this->renderAuthor(),
+ $this->renderHandle($buildable_phid),
+ $this->renderObject());
+ }
+
+ return null;
+ }
+
+ private function newBuildableStatus() {
+ $new = $this->getNewValue();
+ return HarbormasterBuildableStatus::newBuildableStatusObject($new);
+ }
+
+ private function getBuildablePHID() {
+ return $this->getMetadataValue('harbormaster:buildablePHID');
+ }
+
+}
diff --git a/src/applications/differential/xaction/DifferentialRevisionCloseTransaction.php b/src/applications/differential/xaction/DifferentialRevisionCloseTransaction.php
index b1c796dca..7ef0f2ba6 100644
--- a/src/applications/differential/xaction/DifferentialRevisionCloseTransaction.php
+++ b/src/applications/differential/xaction/DifferentialRevisionCloseTransaction.php
@@ -1,139 +1,158 @@
<?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 4169c3e4d..561e57c7c 100644
--- a/src/applications/differential/xaction/DifferentialRevisionCommandeerTransaction.php
+++ b/src/applications/differential/xaction/DifferentialRevisionCommandeerTransaction.php
@@ -1,95 +1,90 @@
<?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 ($object->isDraft()) {
- throw new Exception(
- pht('You can not commandeer a draft revision.'));
- }
-
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());
}
}
diff --git a/src/applications/differential/xaction/DifferentialRevisionHoldDraftTransaction.php b/src/applications/differential/xaction/DifferentialRevisionHoldDraftTransaction.php
index 2562f1820..4d31f39f0 100644
--- a/src/applications/differential/xaction/DifferentialRevisionHoldDraftTransaction.php
+++ b/src/applications/differential/xaction/DifferentialRevisionHoldDraftTransaction.php
@@ -1,56 +1,58 @@
<?php
final class DifferentialRevisionHoldDraftTransaction
extends DifferentialRevisionTransactionType {
const TRANSACTIONTYPE = 'draft';
const EDITKEY = 'draft';
public function generateOldValue($object) {
return (bool)$object->getHoldAsDraft();
}
public function generateNewValue($object, $value) {
return (bool)$value;
}
public function applyInternalEffects($object, $value) {
$object->setHoldAsDraft($value);
// If draft isn't the default state but we're creating a new revision
// and holding it as a draft, put it in draft mode. See PHI206.
// TODO: This can probably be removed once Draft is the universal default.
if ($this->isNewObject()) {
if ($object->isNeedsReview()) {
- $object->setModernRevisionStatus(DifferentialRevisionStatus::DRAFT);
+ $object
+ ->setModernRevisionStatus(DifferentialRevisionStatus::DRAFT)
+ ->setShouldBroadcast(false);
}
}
}
public function getTitle() {
if ($this->getNewValue()) {
return pht(
'%s held this revision as a draft.',
$this->renderAuthor());
} else {
return pht(
'%s set this revision to automatically submit once builds complete.',
$this->renderAuthor());
}
}
public function getTitleForFeed() {
if ($this->getNewValue()) {
return pht(
'%s held %s as a draft.',
$this->renderAuthor(),
$this->renderObject());
} else {
return pht(
'%s set %s to automatically submit once builds complete.',
$this->renderAuthor(),
$this->renderObject());
}
}
}
diff --git a/src/applications/differential/xaction/DifferentialRevisionPlanChangesTransaction.php b/src/applications/differential/xaction/DifferentialRevisionPlanChangesTransaction.php
index 231adc5bf..bdbe28d5c 100644
--- a/src/applications/differential/xaction/DifferentialRevisionPlanChangesTransaction.php
+++ b/src/applications/differential/xaction/DifferentialRevisionPlanChangesTransaction.php
@@ -1,100 +1,122 @@
<?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()) {
- throw new Exception(
- pht('You can not plan changes to a draft revision.'));
+
+ // 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() {
- return pht(
- '%s planned changes to this revision.',
- $this->renderAuthor());
+ 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');
+ }
+
}
diff --git a/src/applications/differential/xaction/DifferentialRevisionReclaimTransaction.php b/src/applications/differential/xaction/DifferentialRevisionReclaimTransaction.php
index 4a4744e2e..6d74589b6 100644
--- a/src/applications/differential/xaction/DifferentialRevisionReclaimTransaction.php
+++ b/src/applications/differential/xaction/DifferentialRevisionReclaimTransaction.php
@@ -1,84 +1,88 @@
<?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) {
- $status_review = DifferentialRevisionStatus::NEEDS_REVIEW;
- $object->setModernRevisionStatus($status_review);
+ 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());
}
}
diff --git a/src/applications/differential/xaction/DifferentialRevisionRequestReviewTransaction.php b/src/applications/differential/xaction/DifferentialRevisionRequestReviewTransaction.php
index ed0d20a0b..08c590a9a 100644
--- a/src/applications/differential/xaction/DifferentialRevisionRequestReviewTransaction.php
+++ b/src/applications/differential/xaction/DifferentialRevisionRequestReviewTransaction.php
@@ -1,85 +1,87 @@
<?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);
+ $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());
}
}
diff --git a/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php b/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php
index b112eb638..8aec75d2f 100644
--- a/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php
+++ b/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php
@@ -1,266 +1,275 @@
<?php
abstract class DifferentialRevisionReviewTransaction
extends DifferentialRevisionActionTransaction {
protected function getRevisionActionGroupKey() {
return DifferentialRevisionEditEngine::ACTIONGROUP_REVIEW;
}
public function generateNewValue($object, $value) {
if (!is_array($value)) {
return true;
}
// If the list of options is the same as the default list, just treat this
// as a "take the default action" transaction.
$viewer = $this->getActor();
list($options, $default) = $this->getActionOptions($viewer, $object);
// Remove reviewers which aren't actionable. In the case of "Accept", we
// may allow the transaction to proceed with some reviewers who have
// already accepted, to avoid race conditions where two reviewers fill
// out the form at the same time and accept on behalf of the same package.
// It's okay for these reviewers to survive validation, but they should
// not survive beyond this point.
$value = array_fuse($value);
$value = array_intersect($value, array_keys($options));
$value = array_values($value);
sort($default);
sort($value);
if ($default === $value) {
return true;
}
return $value;
}
protected function isViewerAnyReviewer(
DifferentialRevision $revision,
PhabricatorUser $viewer) {
return ($this->getViewerReviewerStatus($revision, $viewer) !== null);
}
protected function isViewerAnyAuthority(
DifferentialRevision $revision,
PhabricatorUser $viewer) {
$reviewers = $revision->getReviewers();
foreach ($revision->getReviewers() as $reviewer) {
if ($reviewer->hasAuthority($viewer)) {
return true;
}
}
return false;
}
protected function isViewerFullyAccepted(
DifferentialRevision $revision,
PhabricatorUser $viewer) {
return $this->isViewerReviewerStatusFully(
$revision,
$viewer,
DifferentialReviewerStatus::STATUS_ACCEPTED);
}
protected function isViewerFullyRejected(
DifferentialRevision $revision,
PhabricatorUser $viewer) {
return $this->isViewerReviewerStatusFully(
$revision,
$viewer,
DifferentialReviewerStatus::STATUS_REJECTED);
}
protected function getViewerReviewerStatus(
DifferentialRevision $revision,
PhabricatorUser $viewer) {
if (!$viewer->getPHID()) {
return null;
}
foreach ($revision->getReviewers() as $reviewer) {
if ($reviewer->getReviewerPHID() != $viewer->getPHID()) {
continue;
}
return $reviewer->getReviewerStatus();
}
return null;
}
private function isViewerReviewerStatusFully(
DifferentialRevision $revision,
PhabricatorUser $viewer,
$require_status) {
// If the user themselves is not a reviewer, the reviews they have
// authority over can not all be in any set of states since their own
// personal review has no state.
$status = $this->getViewerReviewerStatus($revision, $viewer);
if ($status === null) {
return false;
}
$active_phid = $this->getActiveDiffPHID($revision);
$status_accepted = DifferentialReviewerStatus::STATUS_ACCEPTED;
$status_rejected = DifferentialReviewerStatus::STATUS_REJECTED;
$is_accepted = ($require_status == $status_accepted);
$is_rejected = ($require_status == $status_rejected);
// Otherwise, check that all reviews they have authority over are in
// the desired set of states.
foreach ($revision->getReviewers() as $reviewer) {
if (!$reviewer->hasAuthority($viewer)) {
$can_force = false;
if ($is_accepted) {
if ($revision->canReviewerForceAccept($viewer, $reviewer)) {
$can_force = true;
}
}
if (!$can_force) {
continue;
}
}
$status = $reviewer->getReviewerStatus();
if ($status != $require_status) {
return false;
}
// Here, we're primarily testing if we can remove a void on the review.
if ($is_accepted) {
if (!$reviewer->isAccepted($active_phid)) {
return false;
}
}
if ($is_rejected) {
if (!$reviewer->isRejected($active_phid)) {
return false;
}
}
// This is a broader check to see if we can update the diff where the
// last action occurred.
if ($reviewer->getLastActionDiffPHID() != $active_phid) {
return false;
}
}
return true;
}
protected function applyReviewerEffect(
DifferentialRevision $revision,
PhabricatorUser $viewer,
$value,
- $status) {
+ $status,
+ array $reviewer_options = array()) {
+
+ PhutilTypeSpec::checkMap(
+ $reviewer_options,
+ array(
+ 'sticky' => 'optional bool',
+ ));
$map = array();
// When you accept or reject, you may accept or reject on behalf of all
// reviewers you have authority for. When you resign, you only affect
// yourself.
$with_authority = ($status != DifferentialReviewerStatus::STATUS_RESIGNED);
$with_force = ($status == DifferentialReviewerStatus::STATUS_ACCEPTED);
if ($with_authority) {
foreach ($revision->getReviewers() as $reviewer) {
if (!$reviewer->hasAuthority($viewer)) {
if (!$with_force) {
continue;
}
if (!$revision->canReviewerForceAccept($viewer, $reviewer)) {
continue;
}
}
$map[$reviewer->getReviewerPHID()] = $status;
}
}
// In all cases, you affect yourself.
$map[$viewer->getPHID()] = $status;
// If we're applying an "accept the defaults" transaction, and this
// transaction type uses checkboxes, replace the value with the list of
// defaults.
if (!is_array($value)) {
list($options, $default) = $this->getActionOptions($viewer, $revision);
if ($options) {
$value = $default;
}
}
// If we have a specific list of reviewers to act on, usually because the
// user has submitted a specific list of reviewers to act as by
// unchecking some checkboxes under "Accept", only affect those reviewers.
if (is_array($value)) {
$map = array_select_keys($map, $value);
}
// Now, do the new write.
if ($map) {
$diff = $this->getEditor()->getActiveDiff($revision);
if ($diff) {
$diff_phid = $diff->getPHID();
} else {
$diff_phid = null;
}
$table = new DifferentialReviewer();
$src_phid = $revision->getPHID();
$reviewers = $table->loadAllWhere(
'revisionPHID = %s AND reviewerPHID IN (%Ls)',
$src_phid,
array_keys($map));
$reviewers = mpull($reviewers, null, 'getReviewerPHID');
foreach (array_keys($map) as $dst_phid) {
$reviewer = idx($reviewers, $dst_phid);
if (!$reviewer) {
$reviewer = id(new DifferentialReviewer())
->setRevisionPHID($src_phid)
->setReviewerPHID($dst_phid);
}
$old_status = $reviewer->getReviewerStatus();
$reviewer->setReviewerStatus($status);
if ($diff_phid) {
$reviewer->setLastActionDiffPHID($diff_phid);
}
if ($old_status !== $status) {
$reviewer->setLastActorPHID($this->getActingAsPHID());
}
// Clear any outstanding void on this reviewer. A void may be placed
// by the author using "Request Review" when a reviewer has already
// accepted.
$reviewer->setVoidedPHID(null);
+ $reviewer->setOption('sticky', idx($reviewer_options, 'sticky'));
+
try {
$reviewer->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
// At least for now, just ignore it if we lost a race.
}
}
}
}
}
diff --git a/src/applications/differential/xaction/DifferentialRevisionUpdateTransaction.php b/src/applications/differential/xaction/DifferentialRevisionUpdateTransaction.php
new file mode 100644
index 000000000..bf1e6de87
--- /dev/null
+++ b/src/applications/differential/xaction/DifferentialRevisionUpdateTransaction.php
@@ -0,0 +1,232 @@
+<?php
+
+final class DifferentialRevisionUpdateTransaction
+ extends DifferentialRevisionTransactionType {
+
+ const TRANSACTIONTYPE = 'differential:update';
+ const EDITKEY = 'update';
+
+ public function generateOldValue($object) {
+ return $object->getActiveDiffPHID();
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $should_review = $this->shouldRequestReviewAfterUpdate($object);
+ if ($should_review) {
+ // If we're updating a non-broadcasting revision, put it back in draft
+ // rather than moving it directly to "Needs Review".
+ if ($object->getShouldBroadcast()) {
+ $new_status = DifferentialRevisionStatus::NEEDS_REVIEW;
+ } else {
+ $new_status = DifferentialRevisionStatus::DRAFT;
+ }
+ $object->setModernRevisionStatus($new_status);
+ }
+
+ $editor = $this->getEditor();
+ $diff = $editor->requireDiff($value);
+
+ $this->updateRevisionLineCounts($object, $diff);
+
+ $object->setRepositoryPHID($diff->getRepositoryPHID());
+ $object->setActiveDiffPHID($diff->getPHID());
+ $object->attachActiveDiff($diff);
+ }
+
+ private function shouldRequestReviewAfterUpdate($object) {
+ if ($this->isCommitUpdate()) {
+ return false;
+ }
+
+ $should_update =
+ $object->isNeedsRevision() ||
+ $object->isChangePlanned() ||
+ $object->isAbandoned();
+ if ($should_update) {
+ return true;
+ }
+
+ return false;
+ }
+
+ public function applyExternalEffects($object, $value) {
+ $editor = $this->getEditor();
+ $diff = $editor->requireDiff($value);
+
+ // TODO: This can race with diff updates, particularly those from
+ // Harbormaster. See discussion in T8650.
+ $diff->setRevisionID($object->getID());
+ $diff->save();
+
+ // If there are any outstanding buildables for this diff, tell
+ // Harbormaster that their containers need to be updated. This is
+ // common, because `arc` creates buildables so it can upload lint
+ // and unit results.
+
+ $buildables = id(new HarbormasterBuildableQuery())
+ ->setViewer(PhabricatorUser::getOmnipotentUser())
+ ->withManualBuildables(false)
+ ->withBuildablePHIDs(array($diff->getPHID()))
+ ->execute();
+ foreach ($buildables as $buildable) {
+ $buildable->sendMessage(
+ $this->getActor(),
+ HarbormasterMessageType::BUILDABLE_CONTAINER,
+ true);
+ }
+ }
+
+ public function getColor() {
+ return 'sky';
+ }
+
+ public function getIcon() {
+ return 'fa-refresh';
+ }
+
+ public function getActionName() {
+ if ($this->isCreateTransaction()) {
+ return pht('Request');
+ } else {
+ return pht('Updated');
+ }
+ }
+
+ public function getActionStrength() {
+ return 2;
+ }
+
+ public function getTitle() {
+ $old = $this->getOldValue();
+ $new = $this->getNewValue();
+
+ if ($this->isCommitUpdate()) {
+ return pht(
+ 'This revision was automatically updated to reflect the '.
+ 'committed changes.');
+ }
+
+ // NOTE: Very, very old update transactions did not have a new value or
+ // did not use a diff PHID as a new value. This was changed years ago,
+ // but wasn't migrated. We might consider migrating if this causes issues.
+
+ return pht(
+ '%s updated this revision to %s.',
+ $this->renderAuthor(),
+ $this->renderNewHandle());
+ }
+
+ public function getTitleForFeed() {
+ return pht(
+ '%s updated the diff for %s.',
+ $this->renderAuthor(),
+ $this->renderObject());
+ }
+
+ public function validateTransactions($object, array $xactions) {
+ $errors = array();
+
+ $diff_phid = null;
+ foreach ($xactions as $xaction) {
+ $diff_phid = $xaction->getNewValue();
+
+ $diff = id(new DifferentialDiffQuery())
+ ->withPHIDs(array($diff_phid))
+ ->setViewer($this->getActor())
+ ->executeOne();
+ if (!$diff) {
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'Specified diff ("%s") does not exist.',
+ $diff_phid),
+ $xaction);
+ continue;
+ }
+
+ $is_attached =
+ ($diff->getRevisionID()) &&
+ ($diff->getRevisionID() == $object->getID());
+ if ($is_attached) {
+ $is_active = ($diff_phid == $object->getActiveDiffPHID());
+ } else {
+ $is_active = false;
+ }
+
+ if ($is_attached) {
+ if ($is_active) {
+ // This is a no-op: we're reattaching the current active diff to the
+ // revision it is already attached to. This is valid and will just
+ // be dropped later on in the process.
+ } else {
+ // At least for now, there's no support for "undoing" a diff and
+ // reverting to an older proposed change without just creating a
+ // new diff from whole cloth.
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'You can not update this revision with the specified diff '.
+ '("%s") because this diff is already attached to the revision '.
+ 'as an older version of the change.',
+ $diff_phid),
+ $xaction);
+ continue;
+ }
+ } else if ($diff->getRevisionID()) {
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'You can not update this revision with the specified diff ("%s") '.
+ 'because the diff is already attached to another revision.',
+ $diff_phid),
+ $xaction);
+ continue;
+ }
+ }
+
+ if (!$diff_phid && !$object->getActiveDiffPHID()) {
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'You must specify an initial diff when creating a revision.'));
+ }
+
+ return $errors;
+ }
+
+ public function isCommitUpdate() {
+ return (bool)$this->getMetadataValue('isCommitUpdate');
+ }
+
+ private function updateRevisionLineCounts(
+ DifferentialRevision $revision,
+ DifferentialDiff $diff) {
+
+ $revision->setLineCount($diff->getLineCount());
+
+ $conn = $revision->establishConnection('r');
+
+ $row = queryfx_one(
+ $conn,
+ 'SELECT SUM(addLines) A, SUM(delLines) D FROM %T
+ WHERE diffID = %d',
+ id(new DifferentialChangeset())->getTableName(),
+ $diff->getID());
+
+ if ($row) {
+ $revision->setAddedLineCount((int)$row['A']);
+ $revision->setRemovedLineCount((int)$row['D']);
+ }
+ }
+
+ public function getTransactionTypeForConduit($xaction) {
+ return 'update';
+ }
+
+ public function getFieldValuesForConduit($object, $data) {
+ $commit_phids = $object->getMetadataValue('commitPHIDs', array());
+
+ return array(
+ 'old' => $object->getOldValue(),
+ 'new' => $object->getNewValue(),
+ 'commitPHIDs' => $commit_phids,
+ );
+ }
+
+}
diff --git a/src/applications/differential/xaction/DifferentialRevisionVoidTransaction.php b/src/applications/differential/xaction/DifferentialRevisionVoidTransaction.php
index 5073a9c46..331c637aa 100644
--- a/src/applications/differential/xaction/DifferentialRevisionVoidTransaction.php
+++ b/src/applications/differential/xaction/DifferentialRevisionVoidTransaction.php
@@ -1,70 +1,92 @@
<?php
/**
* This is an internal transaction type used to void reviews.
*
* For example, "Request Review" voids any open accepts, so they no longer
* act as current accepts.
*/
final class DifferentialRevisionVoidTransaction
extends DifferentialRevisionTransactionType {
const TRANSACTIONTYPE = 'differential.revision.void';
public function generateOldValue($object) {
return false;
}
public function generateNewValue($object, $value) {
- $table = new DifferentialReviewer();
- $table_name = $table->getTableName();
- $conn = $table->establishConnection('w');
-
- $rows = queryfx_all(
- $conn,
- 'SELECT reviewerPHID FROM %T
- WHERE revisionPHID = %s
- AND voidedPHID IS NULL
- AND reviewerStatus IN (%Ls)',
- $table_name,
+ $reviewers = id(new DifferentialReviewer())->loadAllWhere(
+ 'revisionPHID = %s
+ AND voidedPHID IS NULL
+ AND reviewerStatus IN (%Ls)',
$object->getPHID(),
$value);
- return ipull($rows, 'reviewerPHID');
+ $must_downgrade = $this->getMetadataValue('void.force', array());
+ $must_downgrade = array_fuse($must_downgrade);
+
+ $default = PhabricatorEnv::getEnvConfig('differential.sticky-accept');
+ foreach ($reviewers as $key => $reviewer) {
+ $status = $reviewer->getReviewerStatus();
+
+ // If this void is forced, always downgrade. For example, this happens
+ // when an author chooses "Request Review": existing reviews are always
+ // voided, even if they're sticky.
+ if (isset($must_downgrade[$status])) {
+ continue;
+ }
+
+ // Otherwise, if this is a sticky accept, don't void it. Accepts may be
+ // explicitly sticky or unsticky, or they'll use the default value if
+ // no value is specified.
+ $is_sticky = $reviewer->getOption('sticky');
+ $is_sticky = coalesce($is_sticky, $default);
+
+ if ($status === DifferentialReviewerStatus::STATUS_ACCEPTED) {
+ if ($is_sticky) {
+ unset($reviewers[$key]);
+ continue;
+ }
+ }
+ }
+
+
+ return mpull($reviewers, 'getReviewerPHID');
}
public function getTransactionHasEffect($object, $old, $new) {
return (bool)$new;
}
public function applyExternalEffects($object, $value) {
$table = new DifferentialReviewer();
$table_name = $table->getTableName();
$conn = $table->establishConnection('w');
queryfx(
$conn,
'UPDATE %T SET voidedPHID = %s
WHERE revisionPHID = %s
AND voidedPHID IS NULL
AND reviewerPHID IN (%Ls)',
$table_name,
$this->getActingAsPHID(),
$object->getPHID(),
$value);
}
public function shouldHide() {
// This is an internal transaction, so don't show it in feeds or
// transaction logs.
return true;
}
private function getVoidableStatuses() {
return array(
DifferentialReviewerStatus::STATUS_ACCEPTED,
DifferentialReviewerStatus::STATUS_REJECTED,
);
}
}
diff --git a/src/applications/differential/xaction/DifferentialRevisionWrongStateTransaction.php b/src/applications/differential/xaction/DifferentialRevisionWrongStateTransaction.php
index e19e54199..47678e886 100644
--- a/src/applications/differential/xaction/DifferentialRevisionWrongStateTransaction.php
+++ b/src/applications/differential/xaction/DifferentialRevisionWrongStateTransaction.php
@@ -1,41 +1,42 @@
<?php
final class DifferentialRevisionWrongStateTransaction
extends DifferentialRevisionTransactionType {
const TRANSACTIONTYPE = 'differential.revision.wrong';
public function generateOldValue($object) {
return null;
}
public function generateNewValue($object, $value) {
return $value;
}
public function getIcon() {
return 'fa-exclamation';
}
public function getColor() {
return 'pink';
}
public function getActionStrength() {
return 4;
}
public function getTitle() {
$new_value = $this->getNewValue();
$status = DifferentialRevisionStatus::newForStatus($new_value);
return pht(
'This revision was not accepted when it landed; it landed in state %s.',
$this->renderValue($status->getDisplayName()));
}
- public function getTitleForFeed() {
- return null;
+ public function shouldHideForFeed() {
+ return true;
}
+
}
diff --git a/src/applications/diffusion/application/PhabricatorDiffusionApplication.php b/src/applications/diffusion/application/PhabricatorDiffusionApplication.php
index d809815ec..8f1e7b6df 100644
--- a/src/applications/diffusion/application/PhabricatorDiffusionApplication.php
+++ b/src/applications/diffusion/application/PhabricatorDiffusionApplication.php
@@ -1,204 +1,198 @@
<?php
final class PhabricatorDiffusionApplication extends PhabricatorApplication {
public function getName() {
return pht('Diffusion');
}
public function getMenuName() {
return pht('Repositories');
}
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 getFactObjectsForAnalysis() {
- return array(
- new PhabricatorRepositoryCommit(),
- );
- }
-
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',
+ 'jobs/(?P<dblob>.*)' => 'DiffusionJenkinsJobController', // c4s custo
'browse/(?P<dblob>.*)' => 'DiffusionBrowseController',
'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(
- '(?:query/(?P<queryKey>[^/]+)/)?' => 'DiffusionPushLogListController',
+ $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,
'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/DiffusionQueryPathsConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionQueryPathsConduitAPIMethod.php
index be2f07f2c..09c07ec28 100644
--- a/src/applications/diffusion/conduit/DiffusionQueryPathsConduitAPIMethod.php
+++ b/src/applications/diffusion/conduit/DiffusionQueryPathsConduitAPIMethod.php
@@ -1,104 +1,108 @@
<?php
final class DiffusionQueryPathsConduitAPIMethod
extends DiffusionQueryConduitAPIMethod {
public function getAPIMethodName() {
return 'diffusion.querypaths';
}
public function getMethodDescription() {
return pht('Filename search on a repository.');
}
protected function defineReturnType() {
return 'list<string>';
}
protected function defineCustomParamTypes() {
return array(
'path' => 'required string',
'commit' => 'required string',
'pattern' => 'optional string',
'limit' => 'optional int',
'offset' => 'optional int',
);
}
protected function getResult(ConduitAPIRequest $request) {
$results = parent::getResult($request);
$offset = $request->getValue('offset');
return array_slice($results, $offset);
}
protected function getGitResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$path = $drequest->getPath();
$commit = $request->getValue('commit');
$repository = $drequest->getRepository();
- // http://comments.gmane.org/gmane.comp.version-control.git/197735
+ // Recent versions of Git don't work if you pass the empty string, and
+ // require "." to list everything.
+ if (!strlen($path)) {
+ $path = '.';
+ }
$future = $repository->getLocalCommandFuture(
'ls-tree --name-only -r -z %s -- %s',
$commit,
$path);
$lines = id(new LinesOfALargeExecFuture($future))->setDelimiter("\0");
return $this->filterResults($lines, $request);
}
protected function getMercurialResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$path = $request->getValue('path');
$commit = $request->getValue('commit');
$entire_manifest = id(new DiffusionLowLevelMercurialPathsQuery())
->setRepository($repository)
->withCommit($commit)
->withPath($path)
->execute();
$match_against = trim($path, '/');
$match_len = strlen($match_against);
$lines = array();
foreach ($entire_manifest as $path) {
if (strlen($path) && !strncmp($path, $match_against, $match_len)) {
$lines[] = $path;
}
}
return $this->filterResults($lines, $request);
}
protected function filterResults($lines, ConduitAPIRequest $request) {
$pattern = $request->getValue('pattern');
$limit = (int)$request->getValue('limit');
$offset = (int)$request->getValue('offset');
if (strlen($pattern)) {
// Add delimiters to the regex pattern.
$pattern = '('.$pattern.')';
}
$results = array();
$count = 0;
foreach ($lines as $line) {
if (strlen($pattern) && !preg_match($pattern, $line)) {
continue;
}
$results[] = $line;
$count++;
if ($limit && ($count >= ($offset + $limit))) {
break;
}
}
return $results;
}
}
diff --git a/src/applications/diffusion/controller/DiffusionBrowseController.php b/src/applications/diffusion/controller/DiffusionBrowseController.php
index 8df5579a6..a2380aab4 100644
--- a/src/applications/diffusion/controller/DiffusionBrowseController.php
+++ b/src/applications/diffusion/controller/DiffusionBrowseController.php
@@ -1,2042 +1,2046 @@
<?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();
} else {
$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();
$blame_key = PhabricatorDiffusionBlameSetting::SETTINGKEY;
$show_blame = $request->getBool(
'blame',
$viewer->getUserSetting($blame_key));
$view = $request->getStr('view');
if ($request->isFormPost() && $view != 'raw' && $viewer->isLoggedIn()) {
$preferences = PhabricatorUserPreferences::loadUserPreferences($viewer);
$editor = id(new PhabricatorUserPreferencesEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
$xactions = array();
$xactions[] = $preferences->newTransaction($blame_key, $show_blame);
$editor->applyTransactions($preferences, $xactions);
$uri = $request->getRequestURI()
->alter('blame', null);
return id(new AphrontRedirectResponse())->setURI($uri);
}
// We need the blame information if blame is on and this is an Ajax request.
// If blame is on and this is a colorized request, we don't show blame at
// first (we ajax it in afterward) so we don't need to query for it.
$needs_blame = ($show_blame && $request->isAjax());
$params = array(
'commit' => $drequest->getCommit(),
'path' => $drequest->getPath(),
);
$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();
} else {
$corpus = $this->buildGitLFSCorpus($lfs_ref);
}
} else if (ArcanistDiffUtils::isHeuristicBinaryFile($data)) {
$file_uri = $file->getBestURI();
if ($file->isViewableImage()) {
$corpus = $this->buildImageCorpus($file_uri);
} else {
$corpus = $this->buildBinaryCorpus($file_uri, $data);
}
} else {
$this->loadLintMessages();
$this->coverage = $drequest->loadCoverage();
$show_editor = true;
// Build the content of the file.
$corpus = $this->buildCorpus(
$show_blame,
$data,
$needs_blame,
$drequest,
$path,
$data);
}
}
if ($request->isAjax()) {
return id(new AphrontAjaxResponse())->setContent($corpus);
}
require_celerity_resource('diffusion-source-css');
// Render the page.
$bar = $this->buildButtonBar($drequest, $show_blame, $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 loadLintMessages() {
$drequest = $this->getDiffusionRequest();
$branch = $drequest->loadBranch();
if (!$branch || !$branch->getLintCommit()) {
return;
}
$this->lintCommit = $branch->getLintCommit();
$conn = id(new PhabricatorRepository())->establishConnection('r');
$where = '';
if ($drequest->getLint()) {
$where = qsprintf(
$conn,
'AND code = %s',
$drequest->getLint());
}
$this->lintMessages = queryfx_all(
$conn,
'SELECT * FROM %T WHERE branchID = %d %Q AND path = %s',
PhabricatorRepository::TABLE_LINTMESSAGE,
$branch->getID(),
$where,
'/'.$drequest->getPath());
}
private function buildCorpus(
$show_blame,
$file_corpus,
$needs_blame,
DiffusionRequest $drequest,
$path,
$data) {
$viewer = $this->getViewer();
$blame_timeout = 15;
$blame_failed = false;
$highlight_limit = DifferentialChangesetParser::HIGHLIGHT_BYTE_LIMIT;
$blame_limit = DifferentialChangesetParser::HIGHLIGHT_BYTE_LIMIT;
$can_highlight = (strlen($file_corpus) <= $highlight_limit);
$can_blame = (strlen($file_corpus) <= $blame_limit);
if ($needs_blame && $can_blame) {
$blame = $this->loadBlame($path, $drequest->getCommit(), $blame_timeout);
list($blame_list, $blame_commits) = $blame;
if ($blame_list === null) {
$blame_failed = true;
$blame_list = array();
}
} else {
$blame_list = array();
$blame_commits = array();
}
require_celerity_resource('syntax-highlighting-css');
if ($can_highlight) {
$highlighted = PhabricatorSyntaxHighlighter::highlightWithFilename(
$path,
$file_corpus);
} else {
// Highlight as plain text to escape the content properly.
$highlighted = PhabricatorSyntaxHighlighter::highlightWithLanguage(
'txt',
$file_corpus);
}
$lines = phutil_split_lines($highlighted);
$rows = $this->buildDisplayRows(
$lines,
$blame_list,
$blame_commits,
$show_blame);
$corpus_table = javelin_tag(
'table',
array(
'class' => 'diffusion-source remarkup-code PhabricatorMonospaced',
'sigil' => 'phabricator-source',
+ 'meta' => array(
+ 'uri' => $this->getLineNumberBaseURI(),
+ ),
),
$rows);
$corpus_table = phutil_tag_div('diffusion-source-wrap', $corpus_table);
if ($this->getRequest()->isAjax()) {
return $corpus_table;
}
$id = celerity_generate_unique_node_id();
$repo = $drequest->getRepository();
$symbol_repos = nonempty($repo->getSymbolSources(), array());
$symbol_repos[] = $repo->getPHID();
$lang = last(explode('.', $drequest->getPath()));
$repo_languages = $repo->getSymbolLanguages();
$repo_languages = nonempty($repo_languages, array());
$repo_languages = array_fill_keys($repo_languages, true);
$needs_symbols = true;
if ($repo_languages && $symbol_repos) {
$have_symbols = id(new DiffusionSymbolQuery())
->existsSymbolsInRepository($repo->getPHID());
if (!$have_symbols) {
$needs_symbols = false;
}
}
if ($needs_symbols && $repo_languages) {
$needs_symbols = isset($repo_languages[$lang]);
}
if ($needs_symbols) {
Javelin::initBehavior(
'repository-crossreference',
array(
'container' => $id,
'lang' => $lang,
'repositories' => $symbol_repos,
));
}
$corpus = phutil_tag(
'div',
array(
'id' => $id,
),
$corpus_table);
Javelin::initBehavior('load-blame', array('id' => $id));
$this->corpusButtons[] = $this->renderFileButton();
$title = basename($this->getDiffusionRequest()->getPath());
$icon = 'fa-file-code-o';
$drequest = $this->getDiffusionRequest();
$this->buildActionButtons($drequest);
$header = $this->buildPanelHeaderView($title, $icon);
$corpus = id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($corpus)
->addClass('diffusion-mobile-view')
->addSigil('diffusion-file-content-view')
->setMetadata(
array(
'path' => $this->getDiffusionRequest()->getPath(),
))
->setCollapsed(true);
$messages = array();
if (!$can_highlight) {
$messages[] = pht(
'This file is larger than %s, so syntax highlighting is disabled '.
'by default.',
phutil_format_bytes($highlight_limit));
}
if ($show_blame && !$can_blame) {
$messages[] = pht(
'This file is larger than %s, so blame is disabled.',
phutil_format_bytes($blame_limit));
}
if ($blame_failed) {
$messages[] = pht(
'Failed to load blame information for this file in %s second(s).',
new PhutilNumber($blame_timeout));
}
if ($messages) {
$corpus->setInfoView(
id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors($messages));
}
return $corpus;
}
private function buildButtonBar(
DiffusionRequest $drequest,
$show_blame,
$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 ($show_blame) {
$blame_text = pht('Disable Blame');
$blame_icon = 'fa-exclamation-circle lightgreytext';
$blame_value = 0;
} else {
$blame_text = pht('Enable Blame');
$blame_icon = 'fa-exclamation-circle';
$blame_value = 1;
}
$blame = id(new PHUIButtonView())
->setText($blame_text)
->setIcon($blame_icon)
->setUser($viewer)
->setSelected(!$blame_value)
->setColor(PHUIButtonView::GREY);
if ($viewer->isLoggedIn()) {
$blame = phabricator_form(
$viewer,
array(
'action' => $base_uri->alter('blame', $blame_value),
'method' => 'POST',
'style' => 'display: inline-block;',
),
$blame);
} else {
$blame->setTag('a');
$blame->setHref($base_uri->alter('blame', $blame_value));
}
$buttons[] = $blame;
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);
}
$href = null;
$show_lint = true;
if ($this->getRequest()->getStr('lint') !== null) {
$lint_text = pht('Hide Lint');
$href = $base_uri->alter('lint', null);
} else if ($this->lintCommit === null) {
$show_lint = false;
} else {
$lint_text = pht('Show Lint');
$href = $this->getDiffusionRequest()->generateURI(array(
'action' => 'browse',
'commit' => $this->lintCommit,
))->alter('lint', '');
}
if ($show_lint) {
$buttons[] =
id(new PHUIButtonView())
->setTag('a')
->setText($lint_text)
->setHref($href)
->setIcon('fa-exclamation-triangle')
->setDisabled(!$href)
->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 buildDisplayRows(
array $lines,
array $blame_list,
array $blame_commits,
$show_blame) {
$request = $this->getRequest();
$viewer = $this->getViewer();
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$revision_map = array();
$revisions = array();
if ($blame_commits) {
$commit_map = mpull($blame_commits, 'getCommitIdentifier', 'getPHID');
$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;
}
}
$phids = array();
foreach ($blame_commits as $commit) {
$author_phid = $commit->getAuthorPHID();
if ($author_phid === null) {
continue;
}
$phids[$author_phid] = $author_phid;
}
foreach ($revisions as $revision) {
$author_phid = $revision->getAuthorPHID();
if ($author_phid === null) {
continue;
}
$phids[$author_phid] = $author_phid;
}
$handles = $viewer->loadHandles($phids);
$author_phids = array();
$author_map = array();
foreach ($blame_commits as $commit) {
$commit_identifier = $commit->getCommitIdentifier();
$author_phid = '';
if (isset($revision_map[$commit_identifier])) {
$revision_id = $revision_map[$commit_identifier];
$revision = $revisions[$revision_id];
$author_phid = $revision->getAuthorPHID();
} else {
$author_phid = $commit->getAuthorPHID();
}
$author_map[$commit_identifier] = $author_phid;
$author_phids[$author_phid] = $author_phid;
}
$colors = array();
if ($blame_commits) {
$epochs = array();
foreach ($blame_commits as $identifier => $commit) {
$epochs[$identifier] = $commit->getEpoch();
}
$epoch_list = array_filter($epochs);
$epoch_list = array_unique($epoch_list);
$epoch_list = array_values($epoch_list);
$epoch_min = min($epoch_list);
$epoch_max = max($epoch_list);
$epoch_range = ($epoch_max - $epoch_min) + 1;
foreach ($blame_commits as $identifier => $commit) {
$epoch = $epochs[$identifier];
if (!$epoch) {
$color = '#ffffdd'; // Warning color, missing data.
} else {
$color_ratio = ($epoch - $epoch_min) / $epoch_range;
$color_value = 0xE6 * (1.0 - $color_ratio);
$color = sprintf(
'#%02x%02x%02x',
$color_value,
0xF6,
$color_value);
}
$colors[$identifier] = $color;
}
}
$display = array();
$last_identifier = null;
$last_color = null;
foreach ($lines as $line_index => $line) {
$color = '#f6f6f6';
$duplicate = false;
if (isset($blame_list[$line_index])) {
$identifier = $blame_list[$line_index];
if (isset($colors[$identifier])) {
$color = $colors[$identifier];
}
if ($identifier === $last_identifier) {
$duplicate = true;
} else {
$last_identifier = $identifier;
}
}
$display[$line_index] = array(
'data' => $line,
'target' => false,
'highlighted' => false,
'color' => $color,
'duplicate' => $duplicate,
);
}
$line_arr = array();
$line_str = $drequest->getLine();
$ranges = explode(',', $line_str);
foreach ($ranges as $range) {
if (strpos($range, '-') !== false) {
list($min, $max) = explode('-', $range, 2);
$line_arr[] = array(
'min' => min($min, $max),
'max' => max($min, $max),
);
} else if (strlen($range)) {
$line_arr[] = array(
'min' => $range,
'max' => $range,
);
}
}
// Mark the first highlighted line as the target line.
if ($line_arr) {
$target_line = $line_arr[0]['min'];
if (isset($display[$target_line - 1])) {
$display[$target_line - 1]['target'] = true;
}
}
// Mark all other highlighted lines as highlighted.
foreach ($line_arr as $range) {
for ($ii = $range['min']; $ii <= $range['max']; $ii++) {
if (isset($display[$ii - 1])) {
$display[$ii - 1]['highlighted'] = true;
}
}
}
$engine = null;
$inlines = array();
if ($this->getRequest()->getStr('lint') !== null && $this->lintMessages) {
$engine = new PhabricatorMarkupEngine();
$engine->setViewer($viewer);
foreach ($this->lintMessages as $message) {
$inline = id(new PhabricatorAuditInlineComment())
->setSyntheticAuthor(
ArcanistLintSeverity::getStringForSeverity($message['severity']).
' '.$message['code'].' ('.$message['name'].')')
->setLineNumber($message['line'])
->setContent($message['description']);
$inlines[$message['line']][] = $inline;
$engine->addObject(
$inline,
PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY);
}
$engine->process();
require_celerity_resource('differential-changeset-view-css');
}
$rows = $this->renderInlines(
idx($inlines, 0, array()),
$show_blame,
(bool)$this->coverage,
$engine);
// NOTE: We're doing this manually because rendering is otherwise
// dominated by URI generation for very large files.
- $line_base = (string)$drequest->generateURI(
- array(
- 'action' => 'browse',
- 'stable' => true,
- ));
+ $line_base = $this->getLineNumberBaseURI();
require_celerity_resource('aphront-tooltip-css');
Javelin::initBehavior('phabricator-oncopy');
Javelin::initBehavior('phabricator-tooltips');
Javelin::initBehavior('phabricator-line-linker');
// Render these once, since they tend to get repeated many times in large
// blame outputs.
$commit_links = $this->renderCommitLinks($blame_commits, $handles);
$revision_links = $this->renderRevisionLinks($revisions, $handles);
$author_links = $this->renderAuthorLinks($author_map, $handles);
if ($this->coverage) {
require_celerity_resource('differential-changeset-view-css');
Javelin::initBehavior(
'diffusion-browse-file',
array(
'labels' => array(
'cov-C' => pht('Covered'),
'cov-N' => pht('Not Covered'),
'cov-U' => pht('Not Executable'),
),
));
}
$skip_text = pht('Skip Past This Commit');
$skip_icon = id(new PHUIIconView())
->setIcon('fa-caret-square-o-left');
foreach ($display as $line_index => $line) {
$row = array();
$line_number = $line_index + 1;
$line_href = $line_base.'$'.$line_number;
if (isset($blame_list[$line_index])) {
$identifier = $blame_list[$line_index];
} else {
$identifier = null;
}
$revision_link = null;
$commit_link = null;
$author_link = null;
$before_link = null;
$style = 'background: '.$line['color'].';';
if ($identifier && !$line['duplicate']) {
if (isset($commit_links[$identifier])) {
$commit_link = $commit_links[$identifier];
$author_link = $author_links[$author_map[$identifier]];
}
if (isset($revision_map[$identifier])) {
$revision_id = $revision_map[$identifier];
if (isset($revision_links[$revision_id])) {
$revision_link = $revision_links[$revision_id];
}
}
$skip_href = $line_href.'?before='.$identifier.'&view=blame';
$before_link = javelin_tag(
'a',
array(
'href' => $skip_href,
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => $skip_text,
'align' => 'E',
'size' => 300,
),
),
$skip_icon);
}
if ($show_blame) {
$row[] = phutil_tag(
'th',
array(
'class' => 'diffusion-blame-link',
),
$before_link);
$object_links = array();
$object_links[] = $author_link;
$object_links[] = $commit_link;
if ($revision_link) {
$object_links[] = phutil_tag('span', array(), '/');
$object_links[] = $revision_link;
}
$row[] = phutil_tag(
'th',
array(
'class' => 'diffusion-rev-link',
),
$object_links);
}
$line_link = phutil_tag(
'a',
array(
'href' => $line_href,
'style' => $style,
),
$line_number);
$row[] = javelin_tag(
'th',
array(
'class' => 'diffusion-line-link',
'sigil' => 'phabricator-source-line',
'style' => $style,
),
$line_link);
if ($line['target']) {
Javelin::initBehavior(
'diffusion-jump-to',
array(
'target' => 'scroll_target',
));
$anchor_text = phutil_tag(
'a',
array(
'id' => 'scroll_target',
),
'');
} else {
$anchor_text = null;
}
$row[] = phutil_tag(
'td',
array(
),
array(
$anchor_text,
// NOTE: See phabricator-oncopy behavior.
"\xE2\x80\x8B",
// TODO: [HTML] Not ideal.
phutil_safe_html(str_replace("\t", ' ', $line['data'])),
));
if ($this->coverage) {
$cov_index = $line_index;
if (isset($this->coverage[$cov_index])) {
$cov_class = $this->coverage[$cov_index];
} else {
$cov_class = 'N';
}
$row[] = phutil_tag(
'td',
array(
'class' => 'cov cov-'.$cov_class,
),
'');
}
$rows[] = phutil_tag(
'tr',
array(
'class' => ($line['highlighted'] ?
'phabricator-source-highlight' :
null),
),
$row);
$cur_inlines = $this->renderInlines(
idx($inlines, $line_number, array()),
$show_blame,
$this->coverage,
$engine);
foreach ($cur_inlines as $cur_inline) {
$rows[] = $cur_inline;
}
}
return $rows;
}
private function renderInlines(
array $inlines,
$show_blame,
$has_coverage,
$engine) {
$rows = array();
foreach ($inlines as $inline) {
// TODO: This should use modern scaffolding code.
$inline_view = id(new PHUIDiffInlineCommentDetailView())
->setUser($this->getViewer())
->setMarkupEngine($engine)
->setInlineComment($inline)
->render();
$row = array_fill(0, ($show_blame ? 3 : 1), phutil_tag('th'));
$row[] = phutil_tag('td', array(), $inline_view);
if ($has_coverage) {
$row[] = phutil_tag(
'td',
array(
'class' => 'cov cov-I',
));
}
$rows[] = phutil_tag('tr', array('class' => 'inline'), $row);
}
return $rows;
}
private function buildImageCorpus($file_uri) {
$properties = new PHUIPropertyListView();
$properties->addImageContent(
phutil_tag(
'img',
array(
'src' => $file_uri,
)));
$this->corpusButtons[] = $this->renderFileButton($file_uri);
$title = basename($this->getDiffusionRequest()->getPath());
$icon = 'fa-file-image-o';
$drequest = $this->getDiffusionRequest();
$this->buildActionButtons($drequest);
$header = $this->buildPanelHeaderView($title, $icon);
return id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->addClass('diffusion-mobile-view')
->addPropertyList($properties);
}
private function buildBinaryCorpus($file_uri, $data) {
$size = new PhutilNumber(strlen($data));
$text = pht('This is a binary file. It is %s byte(s) in length.', $size);
$text = id(new PHUIBoxView())
->addPadding(PHUI::PADDING_LARGE)
->appendChild($text);
$this->corpusButtons[] = $this->renderFileButton($file_uri);
$title = basename($this->getDiffusionRequest()->getPath());
$icon = 'fa-file';
$drequest = $this->getDiffusionRequest();
$this->buildActionButtons($drequest);
$header = $this->buildPanelHeaderView($title, $icon);
$box = id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->addClass('diffusion-mobile-view')
->appendChild($text);
return $box;
}
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);
}
private function renderRevisionTooltip(
DifferentialRevision $revision,
$handles) {
$viewer = $this->getRequest()->getUser();
$date = phabricator_date($revision->getDateModified(), $viewer);
$id = $revision->getID();
$title = $revision->getTitle();
$header = "D{$id} {$title}";
$author = $handles[$revision->getAuthorPHID()]->getName();
return "{$header}\n{$date} \xC2\xB7 {$author}";
}
private function renderCommitTooltip(
PhabricatorRepositoryCommit $commit,
$author) {
$viewer = $this->getRequest()->getUser();
$date = phabricator_date($commit->getEpoch(), $viewer);
$summary = trim($commit->getSummary());
return "{$summary}\n{$date} \xC2\xB7 {$author}";
}
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)
- ->setUser($viewer)
->setNoBox(true);
- $phids = $list->getRequiredHandlePHIDs();
- $handles = $this->loadViewerHandles($phids);
- $list->setHandles($handles);
-
$view = id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->addClass('diffusion-mobile-view')
->appendChild($list);
return $view;
}
private function loadBlame($path, $commit, $timeout) {
$blame = $this->callConduitWithDiffusionRequest(
'diffusion.blame',
array(
'commit' => $commit,
'paths' => array($path),
'timeout' => $timeout,
));
$identifiers = idx($blame, $path, null);
if ($identifiers) {
$viewer = $this->getViewer();
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$commits = id(new DiffusionCommitQuery())
->setViewer($viewer)
->withRepository($repository)
->withIdentifiers($identifiers)
// TODO: We only fetch this to improve author display behavior, but
// shouldn't really need to?
->needCommitData(true)
->execute();
$commits = mpull($commits, null, 'getCommitIdentifier');
} else {
$commits = array();
}
return array($identifiers, $commits);
}
private function renderAuthorLinks(array $authors, $handles) {
$links = array();
foreach ($authors as $phid) {
if (!strlen($phid)) {
// This means we couldn't identify an author for the commit or the
// revision. We just render a blank for alignment.
$style = null;
$href = null;
$sigil = null;
$meta = null;
} else {
$src = $handles[$phid]->getImageURI();
$style = 'background-image: url('.$src.');';
$href = $handles[$phid]->getURI();
$sigil = 'has-tooltip';
$meta = array(
'tip' => $handles[$phid]->getName(),
'align' => 'E',
);
}
$links[$phid] = javelin_tag(
$href ? 'a' : 'span',
array(
'class' => 'diffusion-author-link',
'style' => $style,
'href' => $href,
'sigil' => $sigil,
'meta' => $meta,
));
}
return $links;
}
private function renderCommitLinks(array $commits, $handles) {
$links = array();
foreach ($commits as $identifier => $commit) {
$tooltip = $this->renderCommitTooltip(
$commit,
$commit->renderAuthorShortName($handles));
$commit_link = javelin_tag(
'a',
array(
'href' => $commit->getURI(),
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => $tooltip,
'align' => 'E',
'size' => 600,
),
),
$commit->getLocalName());
$links[$identifier] = $commit_link;
}
return $links;
}
private function renderRevisionLinks(array $revisions, $handles) {
$links = array();
foreach ($revisions as $revision) {
$revision_id = $revision->getID();
$tooltip = $this->renderRevisionTooltip($revision, $handles);
$revision_link = javelin_tag(
'a',
array(
'href' => '/'.$revision->getMonogram(),
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => $tooltip,
'align' => 'E',
'size' => 600,
),
),
$revision->getMonogram());
$links[$revision_id] = $revision_link;
}
return $links;
}
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);
}
+ private function getLineNumberBaseURI() {
+ $drequest = $this->getDiffusionRequest();
+
+ return (string)$drequest->generateURI(
+ array(
+ 'action' => 'browse',
+ 'stable' => true,
+ ));
+ }
}
diff --git a/src/applications/diffusion/controller/DiffusionCommitController.php b/src/applications/diffusion/controller/DiffusionCommitController.php
index b4d06989c..67ffb7335 100644
--- a/src/applications/diffusion/controller/DiffusionCommitController.php
+++ b/src/applications/diffusion/controller/DiffusionCommitController.php
@@ -1,1167 +1,1171 @@
<?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)
->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);
- $pref_collapse = PhabricatorFiletreeVisibleSetting::SETTINGKEY;
- $collapsed = $viewer->getUserSetting($pref_collapse);
-
$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);
+ ->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');
}
// 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);
}
$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_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/DiffusionController.php b/src/applications/diffusion/controller/DiffusionController.php
index b08331e09..54a575611 100644
--- a/src/applications/diffusion/controller/DiffusionController.php
+++ b/src/applications/diffusion/controller/DiffusionController.php
@@ -1,587 +1,592 @@
<?php
abstract class DiffusionController extends PhabricatorController {
private $diffusionRequest;
protected function getDiffusionRequest() {
if (!$this->diffusionRequest) {
throw new PhutilInvalidStateException('loadDiffusionContext');
}
return $this->diffusionRequest;
}
protected function hasDiffusionRequest() {
return (bool)$this->diffusionRequest;
}
public function willBeginExecution() {
$request = $this->getRequest();
// Check if this is a VCS request, e.g. from "git clone", "hg clone", or
// "svn checkout". If it is, we jump off into repository serving code to
// process the request.
$serve_controller = new DiffusionServeController();
if ($serve_controller->isVCSRequest($request)) {
return $this->delegateToController($serve_controller);
}
return parent::willBeginExecution();
}
protected function loadDiffusionContextForEdit() {
return $this->loadContext(
array(
'edit' => true,
));
}
protected function loadDiffusionContext() {
return $this->loadContext(array());
}
private function loadContext(array $options) {
$request = $this->getRequest();
$viewer = $this->getViewer();
require_celerity_resource('diffusion-repository-css');
$identifier = $this->getRepositoryIdentifierFromRequest($request);
$params = $options + array(
'repository' => $identifier,
'user' => $viewer,
'blob' => $this->getDiffusionBlobFromRequest($request),
'commit' => $request->getURIData('commit'),
'path' => $request->getURIData('path'),
'line' => $request->getURIData('line'),
'branch' => $request->getURIData('branch'),
'lint' => $request->getStr('lint'),
);
$drequest = DiffusionRequest::newFromDictionary($params);
if (!$drequest) {
return new Aphront404Response();
}
// If the client is making a request like "/diffusion/1/...", but the
// repository has a different canonical path like "/diffusion/XYZ/...",
// redirect them to the canonical path.
- $request_path = $request->getPath();
- $repository = $drequest->getRepository();
+ // Skip this redirect if the request is an AJAX request, like the requests
+ // that Owners makes to complete and validate paths.
+
+ if (!$request->isAjax()) {
+ $request_path = $request->getPath();
+ $repository = $drequest->getRepository();
- $canonical_path = $repository->getCanonicalPath($request_path);
- if ($canonical_path !== null) {
- if ($canonical_path != $request_path) {
- return id(new AphrontRedirectResponse())->setURI($canonical_path);
+ $canonical_path = $repository->getCanonicalPath($request_path);
+ if ($canonical_path !== null) {
+ if ($canonical_path != $request_path) {
+ return id(new AphrontRedirectResponse())->setURI($canonical_path);
+ }
}
}
$this->diffusionRequest = $drequest;
return null;
}
protected function getDiffusionBlobFromRequest(AphrontRequest $request) {
return $request->getURIData('dblob');
}
protected function getRepositoryIdentifierFromRequest(
AphrontRequest $request) {
$short_name = $request->getURIData('repositoryShortName');
if (strlen($short_name)) {
// If the short name ends in ".git", ignore it.
$short_name = preg_replace('/\\.git\z/', '', $short_name);
return $short_name;
}
$identifier = $request->getURIData('repositoryCallsign');
if (strlen($identifier)) {
return $identifier;
}
$id = $request->getURIData('repositoryID');
if (strlen($id)) {
return (int)$id;
}
return null;
}
public function buildCrumbs(array $spec = array()) {
$crumbs = $this->buildApplicationCrumbs();
$crumb_list = $this->buildCrumbList($spec);
foreach ($crumb_list as $crumb) {
$crumbs->addCrumb($crumb);
}
return $crumbs;
}
private function buildCrumbList(array $spec = array()) {
$spec = $spec + array(
'commit' => null,
'tags' => null,
'branches' => null,
'view' => null,
);
$crumb_list = array();
// On the home page, we don't have a DiffusionRequest.
if ($this->hasDiffusionRequest()) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
} else {
$drequest = null;
$repository = null;
}
if (!$repository) {
return $crumb_list;
}
$repository_name = $repository->getName();
if (!$spec['commit'] && !$spec['tags'] && !$spec['branches']) {
$branch_name = $drequest->getBranch();
if (strlen($branch_name)) {
$repository_name .= ' ('.$branch_name.')';
}
}
$crumb = id(new PHUICrumbView())
->setName($repository_name);
if (!$spec['view'] && !$spec['commit'] &&
!$spec['tags'] && !$spec['branches']) {
$crumb_list[] = $crumb;
return $crumb_list;
}
$crumb->setHref(
$drequest->generateURI(
array(
'action' => 'branch',
'path' => '/',
)));
$crumb_list[] = $crumb;
$stable_commit = $drequest->getStableCommit();
$commit_name = $repository->formatCommitName($stable_commit, $local = true);
$commit_uri = $repository->getCommitURI($stable_commit);
if ($spec['tags']) {
$crumb = new PHUICrumbView();
if ($spec['commit']) {
$crumb->setName(pht('Tags for %s', $commit_name));
$crumb->setHref($commit_uri);
} else {
$crumb->setName(pht('Tags'));
}
$crumb_list[] = $crumb;
return $crumb_list;
}
if ($spec['branches']) {
$crumb = id(new PHUICrumbView())
->setName(pht('Branches'));
$crumb_list[] = $crumb;
return $crumb_list;
}
if ($spec['commit']) {
$crumb = id(new PHUICrumbView())
->setName($commit_name);
$crumb_list[] = $crumb;
return $crumb_list;
}
$crumb = new PHUICrumbView();
$view = $spec['view'];
switch ($view) {
case 'history':
$view_name = pht('History');
break;
case 'graph':
$view_name = pht('Graph');
break;
- case 'jobs':
+ case 'jobs': // c4science custo
$view_name = pht('Jenkins Job');
break;
case 'browse':
$view_name = pht('Browse');
break;
case 'lint':
$view_name = pht('Lint');
break;
case 'change':
$view_name = pht('Change');
break;
case 'compare':
$view_name = pht('Compare');
break;
}
$crumb = id(new PHUICrumbView())
->setName($view_name);
$crumb_list[] = $crumb;
return $crumb_list;
}
protected function callConduitWithDiffusionRequest(
$method,
array $params = array()) {
$user = $this->getRequest()->getUser();
$drequest = $this->getDiffusionRequest();
return DiffusionQuery::callConduitWithDiffusionRequest(
$user,
$drequest,
$method,
$params);
}
protected function callConduitMethod($method, array $params = array()) {
$user = $this->getViewer();
$drequest = $this->getDiffusionRequest();
return DiffusionQuery::callConduitWithDiffusionRequest(
$user,
$drequest,
$method,
$params,
true);
}
protected function getRepositoryControllerURI(
PhabricatorRepository $repository,
$path) {
return $repository->getPathURI($path);
}
protected function renderPathLinks(DiffusionRequest $drequest, $action) {
$path = $drequest->getPath();
$path_parts = array_filter(explode('/', trim($path, '/')));
$divider = phutil_tag(
'span',
array(
'class' => 'phui-header-divider',
),
'/');
$links = array();
if ($path_parts) {
$links[] = phutil_tag(
'a',
array(
'href' => $drequest->generateURI(
array(
'action' => $action,
'path' => '',
)),
),
$drequest->getRepository()->getDisplayName());
$links[] = $divider;
$accum = '';
$last_key = last_key($path_parts);
foreach ($path_parts as $key => $part) {
$accum .= '/'.$part;
if ($key === $last_key) {
$links[] = $part;
} else {
$links[] = phutil_tag(
'a',
array(
'href' => $drequest->generateURI(
array(
'action' => $action,
'path' => $accum.'/',
)),
),
$part);
$links[] = $divider;
}
}
} else {
$links[] = $drequest->getRepository()->getDisplayName();
$links[] = $divider;
}
return $links;
}
protected function renderStatusMessage($title, $body) {
return id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->setTitle($title)
->setFlush(true)
->appendChild($body);
}
protected function renderCommitHashTag(DiffusionRequest $drequest) {
$stable_commit = $drequest->getStableCommit();
$commit = phutil_tag(
'a',
array(
'href' => $drequest->generateURI(
array(
'action' => 'commit',
'commit' => $stable_commit,
)),
),
$drequest->getRepository()->formatCommitName($stable_commit, true));
$tag = id(new PHUITagView())
->setName($commit)
->setColor(PHUITagView::COLOR_INDIGO)
->setBorder(PHUITagView::BORDER_NONE)
->setType(PHUITagView::TYPE_SHADE);
return $tag;
}
protected function renderBranchTag(DiffusionRequest $drequest) {
$branch = $drequest->getBranch();
$branch = id(new PhutilUTF8StringTruncator())
->setMaximumGlyphs(24)
->truncateString($branch);
$tag = id(new PHUITagView())
->setName($branch)
->setColor(PHUITagView::COLOR_INDIGO)
->setBorder(PHUITagView::BORDER_NONE)
->setType(PHUITagView::TYPE_OUTLINE)
->addClass('diffusion-header-branch-tag');
return $tag;
}
protected function renderSymbolicCommit(DiffusionRequest $drequest) {
$symbolic_tag = $drequest->getSymbolicCommit();
$symbolic_tag = id(new PhutilUTF8StringTruncator())
->setMaximumGlyphs(24)
->truncateString($symbolic_tag);
$tag = id(new PHUITagView())
->setName($symbolic_tag)
->setIcon('fa-tag')
->setColor(PHUITagView::COLOR_INDIGO)
->setBorder(PHUITagView::BORDER_NONE)
->setType(PHUITagView::TYPE_SHADE);
return $tag;
}
protected function renderDirectoryReadme(DiffusionBrowseResultSet $browse) {
$readme_path = $browse->getReadmePath();
if ($readme_path === null) {
return null;
}
$drequest = $this->getDiffusionRequest();
$viewer = $this->getViewer();
$repository = $drequest->getRepository();
$repository_phid = $repository->getPHID();
$stable_commit = $drequest->getStableCommit();
$stable_commit_hash = PhabricatorHash::digestForIndex($stable_commit);
$readme_path_hash = PhabricatorHash::digestForIndex($readme_path);
$cache = PhabricatorCaches::getMutableStructureCache();
$cache_key = "diffusion".
".repository({$repository_phid})".
".commit({$stable_commit_hash})".
".readme({$readme_path_hash})";
$readme_cache = $cache->getKey($cache_key);
if (!$readme_cache) {
try {
$result = $this->callConduitWithDiffusionRequest(
'diffusion.filecontentquery',
array(
'path' => $readme_path,
'commit' => $drequest->getStableCommit(),
));
} catch (Exception $ex) {
return null;
}
$file_phid = $result['filePHID'];
if (!$file_phid) {
return null;
}
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($file_phid))
->executeOne();
if (!$file) {
return null;
}
$corpus = $file->loadFileData();
$readme_cache = array(
'corpus' => $corpus,
);
$cache->setKey($cache_key, $readme_cache);
}
$readme_corpus = $readme_cache['corpus'];
if (!strlen($readme_corpus)) {
return null;
}
return id(new DiffusionReadmeView())
->setUser($this->getViewer())
->setPath($readme_path)
->setContent($readme_corpus);
}
protected function renderSearchForm($path = '/') {
$drequest = $this->getDiffusionRequest();
$viewer = $this->getViewer();
switch ($drequest->getRepository()->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
return null;
}
$search_term = $this->getRequest()->getStr('grep');
require_celerity_resource('diffusion-icons-css');
require_celerity_resource('diffusion-css');
$href = $drequest->generateURI(array(
'action' => 'browse',
'path' => $path,
));
$bar = javelin_tag(
'input',
array(
'type' => 'text',
'id' => 'diffusion-search-input',
'name' => 'grep',
'class' => 'diffusion-search-input',
'sigil' => 'diffusion-search-input',
'placeholder' => pht('Pattern Search'),
'value' => $search_term,
));
$form = phabricator_form(
$viewer,
array(
'method' => 'GET',
'action' => $href,
'sigil' => 'diffusion-search-form',
'class' => 'diffusion-search-form',
'id' => 'diffusion-search-form',
),
array(
$bar,
));
$form_view = phutil_tag(
'div',
array(
'class' => 'diffusion-search-form-view',
),
$form);
return $form_view;
}
protected function buildTabsView($key) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$view = new PHUIListView();
$view->addMenuItem(
id(new PHUIListItemView())
->setKey('code')
->setName(pht('Code'))
->setIcon('fa-code')
->setHref($drequest->generateURI(
array(
'action' => 'branch',
'path' => '/',
)))
->setSelected($key == 'code'));
if (!$repository->isSVN()) {
$view->addMenuItem(
id(new PHUIListItemView())
->setKey('branch')
->setName(pht('Branches'))
->setIcon('fa-code-fork')
->setHref($drequest->generateURI(
array(
'action' => 'branches',
)))
->setSelected($key == 'branch'));
}
if (!$repository->isSVN()) {
$view->addMenuItem(
id(new PHUIListItemView())
->setKey('tags')
->setName(pht('Tags'))
->setIcon('fa-tags')
->setHref($drequest->generateURI(
array(
'action' => 'tags',
)))
->setSelected($key == 'tags'));
}
$view->addMenuItem(
id(new PHUIListItemView())
->setKey('history')
->setName(pht('History'))
->setIcon('fa-history')
->setHref($drequest->generateURI(
array(
'action' => 'history',
)))
->setSelected($key == 'history'));
$view->addMenuItem(
id(new PHUIListItemView())
->setKey('graph')
->setName(pht('Graph'))
->setIcon('fa-code-fork')
->setHref($drequest->generateURI(
array(
'action' => 'graph',
)))
->setSelected($key == 'graph'));
// c4science customization
$viewer = $this->getViewer();
$have_jenkins = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorJenkinsJobApplication', $viewer);
if ($have_jenkins) {
$view->addMenuItem(
id(new PHUIListItemView())
->setKey('jobs')
->setName(pht('Jenkins Jobs'))
->setIcon('fa-cogs')
->setHref($drequest->generateURI(
array(
'action' => 'jobs',
)))
->setSelected($key == 'jobs'));
}
return $view;
}
}
diff --git a/src/applications/diffusion/controller/DiffusionPathValidateController.php b/src/applications/diffusion/controller/DiffusionPathValidateController.php
index e6fbc4113..62e9e0479 100644
--- a/src/applications/diffusion/controller/DiffusionPathValidateController.php
+++ b/src/applications/diffusion/controller/DiffusionPathValidateController.php
@@ -1,63 +1,50 @@
<?php
final class DiffusionPathValidateController extends DiffusionController {
protected function getRepositoryIdentifierFromRequest(
AphrontRequest $request) {
return $request->getStr('repositoryPHID');
}
public function handleRequest(AphrontRequest $request) {
$response = $this->loadDiffusionContext();
if ($response) {
return $response;
}
$viewer = $this->getViewer();
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$path = $request->getStr('path');
$path = ltrim($path, '/');
$browse_results = DiffusionBrowseResultSet::newFromConduit(
$this->callConduitWithDiffusionRequest(
'diffusion.browsequery',
array(
'path' => $path,
'commit' => $drequest->getCommit(),
'needValidityOnly' => true,
)));
$valid = $browse_results->isValidResults();
if (!$valid) {
switch ($browse_results->getReasonForEmptyResultSet()) {
case DiffusionBrowseResultSet::REASON_IS_FILE:
$valid = true;
break;
case DiffusionBrowseResultSet::REASON_IS_EMPTY:
$valid = true;
break;
}
}
$output = array(
'valid' => (bool)$valid,
);
- if (!$valid) {
- $branch = $drequest->getBranch();
- if ($branch) {
- $message = pht('Not found in %s', $branch);
- } else {
- $message = pht('Not found at %s', 'HEAD');
- }
- } else {
- $message = pht('OK');
- }
-
- $output['message'] = $message;
-
return id(new AphrontAjaxResponse())->setContent($output);
}
}
diff --git a/src/applications/diffusion/controller/DiffusionPullLogListController.php b/src/applications/diffusion/controller/DiffusionPullLogListController.php
index 29712be2a..f3b570c81 100644
--- a/src/applications/diffusion/controller/DiffusionPullLogListController.php
+++ b/src/applications/diffusion/controller/DiffusionPullLogListController.php
@@ -1,12 +1,17 @@
<?php
final class DiffusionPullLogListController
extends DiffusionLogController {
public function handleRequest(AphrontRequest $request) {
return id(new DiffusionPullLogSearchEngine())
->setController($this)
->buildResponse();
}
+ protected function buildApplicationCrumbs() {
+ return parent::buildApplicationCrumbs()
+ ->addTextCrumb(pht('Pull Logs'), $this->getApplicationURI('pulllog/'));
+ }
+
}
diff --git a/src/applications/diffusion/controller/DiffusionPushLogListController.php b/src/applications/diffusion/controller/DiffusionPushLogListController.php
index 658e21674..a6c612eeb 100644
--- a/src/applications/diffusion/controller/DiffusionPushLogListController.php
+++ b/src/applications/diffusion/controller/DiffusionPushLogListController.php
@@ -1,12 +1,17 @@
<?php
final class DiffusionPushLogListController
extends DiffusionLogController {
public function handleRequest(AphrontRequest $request) {
return id(new PhabricatorRepositoryPushLogSearchEngine())
->setController($this)
->buildResponse();
}
+ protected function buildApplicationCrumbs() {
+ return parent::buildApplicationCrumbs()
+ ->addTextCrumb(pht('Push Logs'), $this->getApplicationURI('pushlog/'));
+ }
+
}
diff --git a/src/applications/diffusion/controller/DiffusionRepositoryURIViewController.php b/src/applications/diffusion/controller/DiffusionRepositoryURIViewController.php
index 91ffbb473..e923ebfc2 100644
--- a/src/applications/diffusion/controller/DiffusionRepositoryURIViewController.php
+++ b/src/applications/diffusion/controller/DiffusionRepositoryURIViewController.php
@@ -1,315 +1,311 @@
<?php
final class DiffusionRepositoryURIViewController
extends DiffusionController {
public function handleRequest(AphrontRequest $request) {
$response = $this->loadDiffusionContext();
if ($response) {
return $response;
}
$viewer = $this->getViewer();
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$id = $request->getURIData('id');
$uri = id(new PhabricatorRepositoryURIQuery())
->setViewer($viewer)
->withIDs(array($id))
->withRepositories(array($repository))
->executeOne();
if (!$uri) {
return new Aphront404Response();
}
- // For display, reload the URI by loading it through the repository. This
+ // For display, access the URI by loading it through the repository. This
// may adjust builtin URIs for repository configuration, so we may end up
// with a different view of builtin URIs than we'd see if we loaded them
// directly from the database. See T12884.
- $repository_with_uris = id(new PhabricatorRepositoryQuery())
- ->setViewer($viewer)
- ->needURIs(true)
- ->execute();
$repository_uris = $repository->getURIs();
$repository_uris = mpull($repository_uris, null, 'getID');
$uri = idx($repository_uris, $uri->getID());
if (!$uri) {
return new Aphront404Response();
}
$title = array(
pht('URI'),
$repository->getDisplayName(),
);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->setBorder(true);
$crumbs->addTextCrumb(
$repository->getDisplayName(),
$repository->getURI());
$crumbs->addTextCrumb(
pht('Manage'),
$repository->getPathURI('manage/'));
$panel_label = id(new DiffusionRepositoryURIsManagementPanel())
->getManagementPanelLabel();
$panel_uri = $repository->getPathURI('manage/uris/');
$crumbs->addTextCrumb($panel_label, $panel_uri);
$crumbs->addTextCrumb(pht('URI %d', $uri->getID()));
$header_text = pht(
'%s: URI %d',
$repository->getDisplayName(),
$uri->getID());
$header = id(new PHUIHeaderView())
->setHeader($header_text)
->setHeaderIcon('fa-pencil');
if ($uri->getIsDisabled()) {
$header->setStatus('fa-ban', 'dark', pht('Disabled'));
} else {
$header->setStatus('fa-check', 'bluegrey', pht('Active'));
}
$curtain = $this->buildCurtain($uri);
$details = $this->buildPropertySection($uri);
$timeline = $this->buildTransactionTimeline(
$uri,
new PhabricatorRepositoryURITransactionQuery());
$timeline->setShouldTerminate(true);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setMainColumn(
array(
$details,
$timeline,
))
->setCurtain($curtain);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
}
private function buildCurtain(PhabricatorRepositoryURI $uri) {
$viewer = $this->getViewer();
$repository = $uri->getRepository();
$id = $uri->getID();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$uri,
PhabricatorPolicyCapability::CAN_EDIT);
$curtain = $this->newCurtainView($uri);
$edit_uri = $uri->getEditURI();
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setName(pht('Edit URI'))
->setHref($edit_uri)
->setWorkflow(!$can_edit)
->setDisabled(!$can_edit));
$credential_uri = $repository->getPathURI("uri/credential/{$id}/edit/");
$remove_uri = $repository->getPathURI("uri/credential/{$id}/remove/");
$has_credential = (bool)$uri->getCredentialPHID();
if ($uri->isBuiltin()) {
$can_credential = false;
} else if (!$uri->newCommandEngine()->isCredentialSupported()) {
$can_credential = false;
} else {
$can_credential = true;
}
$can_update = ($can_edit && $can_credential);
$can_remove = ($can_edit && $has_credential);
if ($has_credential) {
$credential_name = pht('Update Credential');
} else {
$credential_name = pht('Set Credential');
}
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-key')
->setName($credential_name)
->setHref($credential_uri)
->setWorkflow(true)
->setDisabled(!$can_edit));
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-times')
->setName(pht('Remove Credential'))
->setHref($remove_uri)
->setWorkflow(true)
->setDisabled(!$can_remove));
if ($uri->getIsDisabled()) {
$disable_name = pht('Enable URI');
$disable_icon = 'fa-check';
} else {
$disable_name = pht('Disable URI');
$disable_icon = 'fa-ban';
}
$can_disable = ($can_edit && !$uri->isBuiltin());
$disable_uri = $repository->getPathURI("uri/disable/{$id}/");
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon($disable_icon)
->setName($disable_name)
->setHref($disable_uri)
->setWorkflow(true)
->setDisabled(!$can_disable));
return $curtain;
}
private function buildPropertySection(PhabricatorRepositoryURI $uri) {
$viewer = $this->getViewer();
$properties = id(new PHUIPropertyListView())
->setUser($viewer);
$properties->addProperty(pht('URI'), $uri->getDisplayURI());
$credential_phid = $uri->getCredentialPHID();
$command_engine = $uri->newCommandEngine();
$is_optional = $command_engine->isCredentialOptional();
$is_supported = $command_engine->isCredentialSupported();
$is_builtin = $uri->isBuiltin();
if ($is_builtin) {
$credential_icon = 'fa-circle-o';
$credential_color = 'grey';
$credential_label = pht('Builtin');
$credential_note = pht('Builtin URIs do not use credentials.');
} else if (!$is_supported) {
$credential_icon = 'fa-circle-o';
$credential_color = 'grey';
$credential_label = pht('Not Supported');
$credential_note = pht('This protocol does not support authentication.');
} else if (!$credential_phid) {
if ($is_optional) {
$credential_icon = 'fa-circle-o';
$credential_color = 'green';
$credential_label = pht('No Credential');
$credential_note = pht('Configured for anonymous access.');
} else {
$credential_icon = 'fa-times';
$credential_color = 'red';
$credential_label = pht('Required');
$credential_note = pht('Credential required but not configured.');
}
} else {
// Don't raise a policy exception if we can't see the credential.
$credentials = id(new PassphraseCredentialQuery())
->setViewer($viewer)
->withPHIDs(array($credential_phid))
->execute();
$credential = head($credentials);
if (!$credential) {
$handles = $viewer->loadHandles(array($credential_phid));
$handle = $handles[$credential_phid];
if ($handle->getPolicyFiltered()) {
$credential_icon = 'fa-lock';
$credential_color = 'grey';
$credential_label = pht('Restricted');
$credential_note = pht(
'You do not have permission to view the configured '.
'credential.');
} else {
$credential_icon = 'fa-times';
$credential_color = 'red';
$credential_label = pht('Invalid');
$credential_note = pht('Configured credential is invalid.');
}
} else {
$provides = $credential->getProvidesType();
$needs = $command_engine->getPassphraseProvidesCredentialType();
if ($provides != $needs) {
$credential_icon = 'fa-times';
$credential_color = 'red';
$credential_label = pht('Wrong Type');
} else {
$credential_icon = 'fa-check';
$credential_color = 'green';
$credential_label = $command_engine->getPassphraseCredentialLabel();
}
$credential_note = $viewer->renderHandle($credential_phid);
}
}
$credential_item = id(new PHUIStatusItemView())
->setIcon($credential_icon, $credential_color)
->setTarget(phutil_tag('strong', array(), $credential_label))
->setNote($credential_note);
$credential_view = id(new PHUIStatusListView())
->addItem($credential_item);
$properties->addProperty(pht('Credential'), $credential_view);
$io_type = $uri->getEffectiveIOType();
$io_map = PhabricatorRepositoryURI::getIOTypeMap();
$io_spec = idx($io_map, $io_type, array());
$io_icon = idx($io_spec, 'icon');
$io_color = idx($io_spec, 'color');
$io_label = idx($io_spec, 'label', $io_type);
$io_note = idx($io_spec, 'note');
$io_item = id(new PHUIStatusItemView())
->setIcon($io_icon, $io_color)
->setTarget(phutil_tag('strong', array(), $io_label))
->setNote($io_note);
$io_view = id(new PHUIStatusListView())
->addItem($io_item);
$properties->addProperty(pht('I/O'), $io_view);
$display_type = $uri->getEffectiveDisplayType();
$display_map = PhabricatorRepositoryURI::getDisplayTypeMap();
$display_spec = idx($display_map, $display_type, array());
$display_icon = idx($display_spec, 'icon');
$display_color = idx($display_spec, 'color');
$display_label = idx($display_spec, 'label', $display_type);
$display_note = idx($display_spec, 'note');
$display_item = id(new PHUIStatusItemView())
->setIcon($display_icon, $display_color)
->setTarget(phutil_tag('strong', array(), $display_label))
->setNote($display_note);
$display_view = id(new PHUIStatusListView())
->addItem($display_item);
$properties->addProperty(pht('Display'), $display_view);
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Details'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($properties);
}
}
diff --git a/src/applications/diffusion/controller/DiffusionServeController.php b/src/applications/diffusion/controller/DiffusionServeController.php
index c0f72ae40..ac0b993b2 100644
--- a/src/applications/diffusion/controller/DiffusionServeController.php
+++ b/src/applications/diffusion/controller/DiffusionServeController.php
@@ -1,1239 +1,1239 @@
<?php
final class DiffusionServeController extends DiffusionController {
private $serviceViewer;
private $serviceRepository;
private $isGitLFSRequest;
private $gitLFSToken;
private $gitLFSInput;
public function setServiceViewer(PhabricatorUser $viewer) {
$this->getRequest()->setUser($viewer);
$this->serviceViewer = $viewer;
return $this;
}
public function getServiceViewer() {
return $this->serviceViewer;
}
public function setServiceRepository(PhabricatorRepository $repository) {
$this->serviceRepository = $repository;
return $this;
}
public function getServiceRepository() {
return $this->serviceRepository;
}
public function getIsGitLFSRequest() {
return $this->isGitLFSRequest;
}
public function getGitLFSToken() {
return $this->gitLFSToken;
}
public function isVCSRequest(AphrontRequest $request) {
$identifier = $this->getRepositoryIdentifierFromRequest($request);
if ($identifier === null) {
return null;
}
$content_type = $request->getHTTPHeader('Content-Type');
$user_agent = idx($_SERVER, 'HTTP_USER_AGENT');
$request_type = $request->getHTTPHeader('X-Phabricator-Request-Type');
// This may have a "charset" suffix, so only match the prefix.
$lfs_pattern = '(^application/vnd\\.git-lfs\\+json(;|\z))';
$vcs = null;
if ($request->getExists('service')) {
$service = $request->getStr('service');
// We get this initially for `info/refs`.
// Git also gives us a User-Agent like "git/1.8.2.3".
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
} else if (strncmp($user_agent, 'git/', 4) === 0) {
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
} else if ($content_type == 'application/x-git-upload-pack-request') {
// We get this for `git-upload-pack`.
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
} else if ($content_type == 'application/x-git-receive-pack-request') {
// We get this for `git-receive-pack`.
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
} else if (preg_match($lfs_pattern, $content_type)) {
// This is a Git LFS HTTP API request.
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
$this->isGitLFSRequest = true;
} else if ($request_type == 'git-lfs') {
// This is a Git LFS object content request.
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
$this->isGitLFSRequest = true;
} else if ($request->getExists('cmd')) {
// Mercurial also sends an Accept header like
// "application/mercurial-0.1", and a User-Agent like
// "mercurial/proto-1.0".
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL;
} else {
// Subversion also sends an initial OPTIONS request (vs GET/POST), and
// has a User-Agent like "SVN/1.8.3 (x86_64-apple-darwin11.4.2)
// serf/1.3.2".
$dav = $request->getHTTPHeader('DAV');
$dav = new PhutilURI($dav);
if ($dav->getDomain() === 'subversion.tigris.org') {
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_SVN;
}
}
return $vcs;
}
public function handleRequest(AphrontRequest $request) {
$service_exception = null;
$response = null;
try {
$response = $this->serveRequest($request);
} catch (Exception $ex) {
$service_exception = $ex;
}
try {
$remote_addr = $request->getRemoteAddress();
if ($request->isHTTPS()) {
$remote_protocol = PhabricatorRepositoryPullEvent::PROTOCOL_HTTPS;
} else {
$remote_protocol = PhabricatorRepositoryPullEvent::PROTOCOL_HTTP;
}
$pull_event = id(new PhabricatorRepositoryPullEvent())
->setEpoch(PhabricatorTime::getNow())
->setRemoteAddress($remote_addr)
->setRemoteProtocol($remote_protocol);
if ($response) {
$response_code = $response->getHTTPResponseCode();
if ($response_code == 200) {
$pull_event
->setResultType(PhabricatorRepositoryPullEvent::RESULT_PULL)
->setResultCode($response_code);
} else {
$pull_event
->setResultType(PhabricatorRepositoryPullEvent::RESULT_ERROR)
->setResultCode($response_code);
}
if ($response instanceof PhabricatorVCSResponse) {
$pull_event->setProperties(
array(
'response.message' => $response->getMessage(),
));
}
} else {
$pull_event
->setResultType(PhabricatorRepositoryPullEvent::RESULT_EXCEPTION)
->setResultCode(500)
->setProperties(
array(
'exception.class' => get_class($ex),
'exception.message' => $ex->getMessage(),
));
}
$viewer = $this->getServiceViewer();
if ($viewer) {
$pull_event->setPullerPHID($viewer->getPHID());
}
$repository = $this->getServiceRepository();
if ($repository) {
$pull_event->setRepositoryPHID($repository->getPHID());
}
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$pull_event->save();
unset($unguarded);
} catch (Exception $ex) {
if ($service_exception) {
throw $service_exception;
}
throw $ex;
}
if ($service_exception) {
throw $service_exception;
}
return $response;
}
private function serveRequest(AphrontRequest $request) {
$identifier = $this->getRepositoryIdentifierFromRequest($request);
// If authentication credentials have been provided, try to find a user
// that actually matches those credentials.
// We require both the username and password to be nonempty, because Git
// won't prompt users who provide a username but no password otherwise.
// See T10797 for discussion.
$have_user = strlen(idx($_SERVER, 'PHP_AUTH_USER'));
$have_pass = strlen(idx($_SERVER, 'PHP_AUTH_PW'));
if ($have_user && $have_pass) {
$username = $_SERVER['PHP_AUTH_USER'];
$password = new PhutilOpaqueEnvelope($_SERVER['PHP_AUTH_PW']);
// Try Git LFS auth first since we can usually reject it without doing
// any queries, since the username won't match the one we expect or the
// request won't be LFS.
$viewer = $this->authenticateGitLFSUser($username, $password);
// If that failed, try normal auth. Note that we can use normal auth on
// LFS requests, so this isn't strictly an alternative to LFS auth.
if (!$viewer) {
$viewer = $this->authenticateHTTPRepositoryUser($username, $password);
}
if (!$viewer) {
return new PhabricatorVCSResponse(
403,
pht('Invalid credentials.'));
}
} else {
// User hasn't provided credentials, which means we count them as
// being "not logged in".
$viewer = new PhabricatorUser();
}
$this->setServiceViewer($viewer);
$allow_public = PhabricatorEnv::getEnvConfig('policy.allow-public');
$allow_auth = PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth');
if (!$allow_public) {
if (!$viewer->isLoggedIn()) {
if ($allow_auth) {
return new PhabricatorVCSResponse(
401,
pht('You must log in to access repositories.'));
} else {
return new PhabricatorVCSResponse(
403,
pht('Public and authenticated HTTP access are both forbidden.'));
}
}
}
try {
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->withIdentifiers(array($identifier))
->needURIs(true)
->executeOne();
if (!$repository) {
return new PhabricatorVCSResponse(
404,
pht('No such repository exists.'));
}
} catch (PhabricatorPolicyException $ex) {
if ($viewer->isLoggedIn()) {
return new PhabricatorVCSResponse(
403,
pht('You do not have permission to access this repository.'));
} else {
if ($allow_auth) {
return new PhabricatorVCSResponse(
401,
pht('You must log in to access this repository.'));
} else {
return new PhabricatorVCSResponse(
403,
pht(
'This repository requires authentication, which is forbidden '.
'over HTTP.'));
}
}
}
$response = $this->validateGitLFSRequest($repository, $viewer);
if ($response) {
return $response;
}
$this->setServiceRepository($repository);
if (!$repository->isTracked()) {
return new PhabricatorVCSResponse(
403,
pht('This repository is inactive.'));
}
$is_push = !$this->isReadOnlyRequest($repository);
if ($this->getIsGitLFSRequest() && $this->getGitLFSToken()) {
// We allow git LFS requests over HTTP even if the repository does not
// otherwise support HTTP reads or writes, as long as the user is using a
// token from SSH. If they're using HTTP username + password auth, they
// have to obey the normal HTTP rules.
} else {
// For now, we don't distinguish between HTTP and HTTPS-originated
// requests that are proxied within the cluster, so the user can connect
// with HTTPS but we may be on HTTP by the time we reach this part of
// the code. Allow things to move forward as long as either protocol
// can be served.
$proto_https = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTPS;
$proto_http = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTP;
$can_read =
$repository->canServeProtocol($proto_https, false) ||
$repository->canServeProtocol($proto_http, false);
if (!$can_read) {
return new PhabricatorVCSResponse(
403,
pht('This repository is not available over HTTP.'));
}
if ($is_push) {
$can_write =
$repository->canServeProtocol($proto_https, true) ||
$repository->canServeProtocol($proto_http, true);
if (!$can_write) {
return new PhabricatorVCSResponse(
403,
pht('This repository is read-only over HTTP.'));
}
}
}
if ($is_push) {
$can_push = PhabricatorPolicyFilter::hasCapability(
$viewer,
$repository,
DiffusionPushCapability::CAPABILITY);
if (!$can_push) {
if ($viewer->isLoggedIn()) {
$error_code = 403;
$error_message = pht(
'You do not have permission to push to this repository ("%s").',
$repository->getDisplayName());
if ($this->getIsGitLFSRequest()) {
return DiffusionGitLFSResponse::newErrorResponse(
$error_code,
$error_message);
} else {
return new PhabricatorVCSResponse(
$error_code,
$error_message);
}
} else {
if ($allow_auth) {
return new PhabricatorVCSResponse(
401,
pht('You must log in to push to this repository.'));
} else {
return new PhabricatorVCSResponse(
403,
pht(
'Pushing to this repository requires authentication, '.
'which is forbidden over HTTP.'));
}
}
}
}
$vcs_type = $repository->getVersionControlSystem();
$req_type = $this->isVCSRequest($request);
if ($vcs_type != $req_type) {
switch ($req_type) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$result = new PhabricatorVCSResponse(
500,
pht(
'This repository ("%s") is not a Git repository.',
$repository->getDisplayName()));
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$result = new PhabricatorVCSResponse(
500,
pht(
'This repository ("%s") is not a Mercurial repository.',
$repository->getDisplayName()));
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$result = new PhabricatorVCSResponse(
500,
pht(
'This repository ("%s") is not a Subversion repository.',
$repository->getDisplayName()));
break;
default:
$result = new PhabricatorVCSResponse(
500,
pht('Unknown request type.'));
break;
}
} else {
switch ($vcs_type) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$result = $this->serveVCSRequest($repository, $viewer);
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$result = new PhabricatorVCSResponse(
500,
pht(
'Phabricator does not support HTTP access to Subversion '.
'repositories.'));
break;
default:
$result = new PhabricatorVCSResponse(
500,
pht('Unknown version control system.'));
break;
}
}
$code = $result->getHTTPResponseCode();
if ($is_push && ($code == 200)) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$repository->writeStatusMessage(
PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE,
PhabricatorRepositoryStatusMessage::CODE_OKAY);
unset($unguarded);
}
return $result;
}
private function serveVCSRequest(
PhabricatorRepository $repository,
PhabricatorUser $viewer) {
// We can serve Git LFS requests first, since we don't need to proxy them.
// It's also important that LFS requests never fall through to standard
// service pathways, because that would let you use LFS tokens to read
// normal repository data.
if ($this->getIsGitLFSRequest()) {
return $this->serveGitLFSRequest($repository, $viewer);
}
// If this repository is hosted on a service, we need to proxy the request
// to a host which can serve it.
$is_cluster_request = $this->getRequest()->isProxiedClusterRequest();
$uri = $repository->getAlmanacServiceURI(
$viewer,
$is_cluster_request,
array(
'http',
'https',
));
if ($uri) {
$future = $this->getRequest()->newClusterProxyFuture($uri);
return id(new AphrontHTTPProxyResponse())
->setHTTPFuture($future);
}
// Otherwise, we're going to handle the request locally.
$vcs_type = $repository->getVersionControlSystem();
switch ($vcs_type) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$result = $this->serveGitRequest($repository, $viewer);
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$result = $this->serveMercurialRequest($repository, $viewer);
break;
}
return $result;
}
private function isReadOnlyRequest(
PhabricatorRepository $repository) {
$request = $this->getRequest();
$method = $_SERVER['REQUEST_METHOD'];
// TODO: This implementation is safe by default, but very incomplete.
if ($this->getIsGitLFSRequest()) {
return $this->isGitLFSReadOnlyRequest($repository);
}
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$service = $request->getStr('service');
$path = $this->getRequestDirectoryPath($repository);
// NOTE: Service names are the reverse of what you might expect, as they
// are from the point of view of the server. The main read service is
// "git-upload-pack", and the main write service is "git-receive-pack".
if ($method == 'GET' &&
$path == '/info/refs' &&
$service == 'git-upload-pack') {
return true;
}
if ($path == '/git-upload-pack') {
return true;
}
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$cmd = $request->getStr('cmd');
if ($cmd == 'batch') {
$cmds = idx($this->getMercurialArguments(), 'cmds');
return DiffusionMercurialWireProtocol::isReadOnlyBatchCommand($cmds);
}
return DiffusionMercurialWireProtocol::isReadOnlyCommand($cmd);
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
break;
}
return false;
}
/**
* @phutil-external-symbol class PhabricatorStartup
*/
private function serveGitRequest(
PhabricatorRepository $repository,
PhabricatorUser $viewer) {
$request = $this->getRequest();
$request_path = $this->getRequestDirectoryPath($repository);
$repository_root = $repository->getLocalPath();
// Rebuild the query string to strip `__magic__` parameters and prevent
// issues where we might interpret inputs like "service=read&service=write"
// differently than the server does and pass it an unsafe command.
// NOTE: This does not use getPassthroughRequestParameters() because
// that code is HTTP-method agnostic and will encode POST data.
$query_data = $_GET;
foreach ($query_data as $key => $value) {
if (!strncmp($key, '__', 2)) {
unset($query_data[$key]);
}
}
$query_string = http_build_query($query_data, '', '&');
// We're about to wipe out PATH with the rest of the environment, so
// resolve the binary first.
$bin = Filesystem::resolveBinary('git-http-backend');
if (!$bin) {
throw new Exception(
pht(
'Unable to find `%s` in %s!',
'git-http-backend',
'$PATH'));
}
// NOTE: We do not set HTTP_CONTENT_ENCODING here, because we already
// decompressed the request when we read the request body, so the body is
// just plain data with no encoding.
$env = array(
'REQUEST_METHOD' => $_SERVER['REQUEST_METHOD'],
'QUERY_STRING' => $query_string,
'CONTENT_TYPE' => $request->getHTTPHeader('Content-Type'),
'REMOTE_ADDR' => $_SERVER['REMOTE_ADDR'],
'GIT_PROJECT_ROOT' => $repository_root,
'GIT_HTTP_EXPORT_ALL' => '1',
'PATH_INFO' => $request_path,
'REMOTE_USER' => $viewer->getUsername(),
// TODO: Set these correctly.
// GIT_COMMITTER_NAME
// GIT_COMMITTER_EMAIL
) + $this->getCommonEnvironment($viewer);
$input = PhabricatorStartup::getRawInput();
$command = csprintf('%s', $bin);
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$cluster_engine = id(new DiffusionRepositoryClusterEngine())
->setViewer($viewer)
->setRepository($repository);
$did_write_lock = false;
if ($this->isReadOnlyRequest($repository)) {
$cluster_engine->synchronizeWorkingCopyBeforeRead();
} else {
$did_write_lock = true;
$cluster_engine->synchronizeWorkingCopyBeforeWrite();
}
$caught = null;
try {
list($err, $stdout, $stderr) = id(new ExecFuture('%C', $command))
->setEnv($env, true)
->write($input)
->resolve();
} catch (Exception $ex) {
$caught = $ex;
}
if ($did_write_lock) {
$cluster_engine->synchronizeWorkingCopyAfterWrite();
}
unset($unguarded);
if ($caught) {
throw $caught;
}
if ($err) {
if ($this->isValidGitShallowCloneResponse($stdout, $stderr)) {
// Ignore the error if the response passes this special check for
// validity.
$err = 0;
}
}
if ($err) {
return new PhabricatorVCSResponse(
500,
pht(
'Error %d: %s',
$err,
phutil_utf8ize($stderr)));
}
return id(new DiffusionGitResponse())->setGitData($stdout);
}
private function getRequestDirectoryPath(PhabricatorRepository $repository) {
$request = $this->getRequest();
$request_path = $request->getRequestURI()->getPath();
$info = PhabricatorRepository::parseRepositoryServicePath(
$request_path,
$repository->getVersionControlSystem());
$base_path = $info['path'];
// For Git repositories, strip an optional directory component if it
// isn't the name of a known Git resource. This allows users to clone
// repositories as "/diffusion/X/anything.git", for example.
if ($repository->isGit()) {
$known = array(
'info',
'git-upload-pack',
'git-receive-pack',
);
foreach ($known as $key => $path) {
$known[$key] = preg_quote($path, '@');
}
$known = implode('|', $known);
if (preg_match('@^/([^/]+)/('.$known.')(/|$)@', $base_path)) {
$base_path = preg_replace('@^/([^/]+)@', '', $base_path);
}
}
return $base_path;
}
private function authenticateGitLFSUser(
$username,
PhutilOpaqueEnvelope $password) {
// Never accept these credentials for requests which aren't LFS requests.
if (!$this->getIsGitLFSRequest()) {
return null;
}
// If we have the wrong username, don't bother checking if the token
// is right.
if ($username !== DiffusionGitLFSTemporaryTokenType::HTTP_USERNAME) {
return null;
}
$lfs_pass = $password->openEnvelope();
$lfs_hash = PhabricatorHash::weakDigest($lfs_pass);
$token = id(new PhabricatorAuthTemporaryTokenQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withTokenTypes(array(DiffusionGitLFSTemporaryTokenType::TOKENTYPE))
->withTokenCodes(array($lfs_hash))
->withExpired(false)
->executeOne();
if (!$token) {
return null;
}
$user = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($token->getUserPHID()))
->executeOne();
if (!$user) {
return null;
}
if (!$user->isUserActivated()) {
return null;
}
$this->gitLFSToken = $token;
return $user;
}
private function authenticateHTTPRepositoryUser(
$username,
PhutilOpaqueEnvelope $password) {
if (!PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth')) {
// No HTTP auth permitted.
return null;
}
if (!strlen($username)) {
// No username.
return null;
}
if (!strlen($password->openEnvelope())) {
// No password.
return null;
}
$user = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withUsernames(array($username))
->executeOne();
if (!$user) {
// Username doesn't match anything.
return null;
}
if (!$user->isUserActivated()) {
// User is not activated.
return null;
}
$request = $this->getRequest();
$content_source = PhabricatorContentSource::newFromRequest($request);
$engine = id(new PhabricatorAuthPasswordEngine())
->setViewer($user)
->setContentSource($content_source)
->setPasswordType(PhabricatorAuthPassword::PASSWORD_TYPE_VCS)
->setObject($user);
if (!$engine->isValidPassword($password)) {
return null;
}
return $user;
}
private function serveMercurialRequest(
PhabricatorRepository $repository,
PhabricatorUser $viewer) {
$request = $this->getRequest();
$bin = Filesystem::resolveBinary('hg');
if (!$bin) {
throw new Exception(
pht(
'Unable to find `%s` in %s!',
'hg',
'$PATH'));
}
$env = $this->getCommonEnvironment($viewer);
$input = PhabricatorStartup::getRawInput();
$cmd = $request->getStr('cmd');
$args = $this->getMercurialArguments();
$args = $this->formatMercurialArguments($cmd, $args);
if (strlen($input)) {
$input = strlen($input)."\n".$input."0\n";
}
$command = csprintf(
'%s -R %s serve --stdio',
$bin,
$repository->getLocalPath());
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
list($err, $stdout, $stderr) = id(new ExecFuture('%C', $command))
->setEnv($env, true)
->setCWD($repository->getLocalPath())
->write("{$cmd}\n{$args}{$input}")
->resolve();
if ($err) {
return new PhabricatorVCSResponse(
500,
pht('Error %d: %s', $err, $stderr));
}
if ($cmd == 'getbundle' ||
$cmd == 'changegroup' ||
$cmd == 'changegroupsubset') {
// We're not completely sure that "changegroup" and "changegroupsubset"
// actually work, they're for very old Mercurial.
$body = gzcompress($stdout);
} else if ($cmd == 'unbundle') {
// This includes diagnostic information and anything echoed by commit
// hooks. We ignore `stdout` since it just has protocol garbage, and
// substitute `stderr`.
$body = strlen($stderr)."\n".$stderr;
} else {
list($length, $body) = explode("\n", $stdout, 2);
if ($cmd == 'capabilities') {
$body = DiffusionMercurialWireProtocol::filterBundle2Capability($body);
}
}
return id(new DiffusionMercurialResponse())->setContent($body);
}
private function getMercurialArguments() {
// Mercurial sends arguments in HTTP headers. "Why?", you might wonder,
// "Why would you do this?".
$args_raw = array();
for ($ii = 1;; $ii++) {
$header = 'HTTP_X_HGARG_'.$ii;
if (!array_key_exists($header, $_SERVER)) {
break;
}
$args_raw[] = $_SERVER[$header];
}
$args_raw = implode('', $args_raw);
return id(new PhutilQueryStringParser())
->parseQueryString($args_raw);
}
private function formatMercurialArguments($command, array $arguments) {
$spec = DiffusionMercurialWireProtocol::getCommandArgs($command);
$out = array();
// Mercurial takes normal arguments like this:
//
// name <length(value)>
// value
$has_star = false;
foreach ($spec as $arg_key) {
if ($arg_key == '*') {
$has_star = true;
continue;
}
if (isset($arguments[$arg_key])) {
$value = $arguments[$arg_key];
$size = strlen($value);
$out[] = "{$arg_key} {$size}\n{$value}";
unset($arguments[$arg_key]);
}
}
if ($has_star) {
// Mercurial takes arguments for variable argument lists roughly like
// this:
//
// * <count(args)>
// argname1 <length(argvalue1)>
// argvalue1
// argname2 <length(argvalue2)>
// argvalue2
$count = count($arguments);
$out[] = "* {$count}\n";
foreach ($arguments as $key => $value) {
if (in_array($key, $spec)) {
// We already added this argument above, so skip it.
continue;
}
$size = strlen($value);
$out[] = "{$key} {$size}\n{$value}";
}
}
return implode('', $out);
}
private function isValidGitShallowCloneResponse($stdout, $stderr) {
// If you execute `git clone --depth N ...`, git sends a request which
// `git-http-backend` responds to by emitting valid output and then exiting
// with a failure code and an error message. If we ignore this error,
// everything works.
// This is a pretty funky fix: it would be nice to more precisely detect
// that a request is a `--depth N` clone request, but we don't have any code
// to decode protocol frames yet. Instead, look for reasonable evidence
// in the error and output that we're looking at a `--depth` clone.
// For evidence this isn't completely crazy, see:
// https://github.com/schacon/grack/pull/7
$stdout_regexp = '(^Content-Type: application/x-git-upload-pack-result)m';
$stderr_regexp = '(The remote end hung up unexpectedly)';
$has_pack = preg_match($stdout_regexp, $stdout);
$is_hangup = preg_match($stderr_regexp, $stderr);
return $has_pack && $is_hangup;
}
private function getCommonEnvironment(PhabricatorUser $viewer) {
$remote_address = $this->getRequest()->getRemoteAddress();
return array(
DiffusionCommitHookEngine::ENV_USER => $viewer->getUsername(),
DiffusionCommitHookEngine::ENV_REMOTE_ADDRESS => $remote_address,
DiffusionCommitHookEngine::ENV_REMOTE_PROTOCOL => 'http',
);
}
private function validateGitLFSRequest(
PhabricatorRepository $repository,
PhabricatorUser $viewer) {
if (!$this->getIsGitLFSRequest()) {
return null;
}
if (!$repository->canUseGitLFS()) {
return new PhabricatorVCSResponse(
403,
pht(
'The requested repository ("%s") does not support Git LFS.',
$repository->getDisplayName()));
}
// If this is using an LFS token, sanity check that we're using it on the
// correct repository. This shouldn't really matter since the user could
// just request a proper token anyway, but it suspicious and should not
// be permitted.
$token = $this->getGitLFSToken();
if ($token) {
$resource = $token->getTokenResource();
if ($resource !== $repository->getPHID()) {
return new PhabricatorVCSResponse(
403,
pht(
'The authentication token provided in the request is bound to '.
'a different repository than the requested repository ("%s").',
$repository->getDisplayName()));
}
}
return null;
}
private function serveGitLFSRequest(
PhabricatorRepository $repository,
PhabricatorUser $viewer) {
if (!$this->getIsGitLFSRequest()) {
throw new Exception(pht('This is not a Git LFS request!'));
}
$path = $this->getGitLFSRequestPath($repository);
$matches = null;
if (preg_match('(^upload/(.*)\z)', $path, $matches)) {
$oid = $matches[1];
return $this->serveGitLFSUploadRequest($repository, $viewer, $oid);
} else if ($path == 'objects/batch') {
return $this->serveGitLFSBatchRequest($repository, $viewer);
} else {
return DiffusionGitLFSResponse::newErrorResponse(
404,
pht(
'Git LFS operation "%s" is not supported by this server.',
$path));
}
}
private function serveGitLFSBatchRequest(
PhabricatorRepository $repository,
PhabricatorUser $viewer) {
$input = $this->getGitLFSInput();
$operation = idx($input, 'operation');
switch ($operation) {
case 'upload':
$want_upload = true;
break;
case 'download':
$want_upload = false;
break;
default:
return DiffusionGitLFSResponse::newErrorResponse(
404,
pht(
'Git LFS batch operation "%s" is not supported by this server.',
$operation));
}
$objects = idx($input, 'objects', array());
$hashes = array();
foreach ($objects as $object) {
$hashes[] = idx($object, 'oid');
}
if ($hashes) {
$refs = id(new PhabricatorRepositoryGitLFSRefQuery())
->setViewer($viewer)
->withRepositoryPHIDs(array($repository->getPHID()))
->withObjectHashes($hashes)
->execute();
$refs = mpull($refs, null, 'getObjectHash');
} else {
$refs = array();
}
$file_phids = mpull($refs, 'getFilePHID');
if ($file_phids) {
$files = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs($file_phids)
->execute();
$files = mpull($files, null, 'getPHID');
} else {
$files = array();
}
$authorization = null;
$output = array();
foreach ($objects as $object) {
$oid = idx($object, 'oid');
$size = idx($object, 'size');
$ref = idx($refs, $oid);
$error = null;
// NOTE: If we already have a ref for this object, we only emit a
// "download" action. The client should not upload the file again.
$actions = array();
if ($ref) {
$file = idx($files, $ref->getFilePHID());
if ($file) {
// Git LFS may prompt users for authentication if the action does
// not provide an "Authorization" header and does not have a query
// parameter named "token". See here for discussion:
// <https://github.com/github/git-lfs/issues/1088>
$no_authorization = 'Basic '.base64_encode('none');
- $get_uri = $file->getCDNURI();
+ $get_uri = $file->getCDNURI('data');
$actions['download'] = array(
'href' => $get_uri,
'header' => array(
'Authorization' => $no_authorization,
'X-Phabricator-Request-Type' => 'git-lfs',
),
);
} else {
$error = array(
'code' => 404,
'message' => pht(
'Object "%s" was previously uploaded, but no longer exists '.
'on this server.',
$oid),
);
}
} else if ($want_upload) {
if (!$authorization) {
// Here, we could reuse the existing authorization if we have one,
// but it's a little simpler to just generate a new one
// unconditionally.
$authorization = $this->newGitLFSHTTPAuthorization(
$repository,
$viewer,
$operation);
}
$put_uri = $repository->getGitLFSURI("info/lfs/upload/{$oid}");
$actions['upload'] = array(
'href' => $put_uri,
'header' => array(
'Authorization' => $authorization,
'X-Phabricator-Request-Type' => 'git-lfs',
),
);
}
$object = array(
'oid' => $oid,
'size' => $size,
);
if ($actions) {
$object['actions'] = $actions;
}
if ($error) {
$object['error'] = $error;
}
$output[] = $object;
}
$output = array(
'objects' => $output,
);
return id(new DiffusionGitLFSResponse())
->setContent($output);
}
private function serveGitLFSUploadRequest(
PhabricatorRepository $repository,
PhabricatorUser $viewer,
$oid) {
$ref = id(new PhabricatorRepositoryGitLFSRefQuery())
->setViewer($viewer)
->withRepositoryPHIDs(array($repository->getPHID()))
->withObjectHashes(array($oid))
->executeOne();
if ($ref) {
return DiffusionGitLFSResponse::newErrorResponse(
405,
pht(
'Content for object "%s" is already known to this server. It can '.
'not be uploaded again.',
$oid));
}
// Remove the execution time limit because uploading large files may take
// a while.
set_time_limit(0);
$request_stream = new AphrontRequestStream();
$request_iterator = $request_stream->getIterator();
$hashing_iterator = id(new PhutilHashingIterator($request_iterator))
->setAlgorithm('sha256');
$source = id(new PhabricatorIteratorFileUploadSource())
->setName('lfs-'.$oid)
->setViewPolicy(PhabricatorPolicies::POLICY_NOONE)
->setIterator($hashing_iterator);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$file = $source->uploadFile();
unset($unguarded);
$hash = $hashing_iterator->getHash();
if ($hash !== $oid) {
return DiffusionGitLFSResponse::newErrorResponse(
400,
pht(
'Uploaded data is corrupt or invalid. Expected hash "%s", actual '.
'hash "%s".',
$oid,
$hash));
}
$ref = id(new PhabricatorRepositoryGitLFSRef())
->setRepositoryPHID($repository->getPHID())
->setObjectHash($hash)
->setByteSize($file->getByteSize())
->setAuthorPHID($viewer->getPHID())
->setFilePHID($file->getPHID());
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
// Attach the file to the repository to give users permission
// to access it.
$file->attachToObject($repository->getPHID());
$ref->save();
unset($unguarded);
// This is just a plain HTTP 200 with no content, which is what `git lfs`
// expects.
return new DiffusionGitLFSResponse();
}
private function newGitLFSHTTPAuthorization(
PhabricatorRepository $repository,
PhabricatorUser $viewer,
$operation) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$authorization = DiffusionGitLFSTemporaryTokenType::newHTTPAuthorization(
$repository,
$viewer,
$operation);
unset($unguarded);
return $authorization;
}
private function getGitLFSRequestPath(PhabricatorRepository $repository) {
$request_path = $this->getRequestDirectoryPath($repository);
$matches = null;
if (preg_match('(^/info/lfs(?:\z|/)(.*))', $request_path, $matches)) {
return $matches[1];
}
return null;
}
private function getGitLFSInput() {
if (!$this->gitLFSInput) {
$input = PhabricatorStartup::getRawInput();
$input = phutil_json_decode($input);
$this->gitLFSInput = $input;
}
return $this->gitLFSInput;
}
private function isGitLFSReadOnlyRequest(PhabricatorRepository $repository) {
if (!$this->getIsGitLFSRequest()) {
return false;
}
$path = $this->getGitLFSRequestPath($repository);
if ($path === 'objects/batch') {
$input = $this->getGitLFSInput();
$operation = idx($input, 'operation');
switch ($operation) {
case 'download':
return true;
default:
return false;
}
}
return false;
}
}
diff --git a/src/applications/diffusion/edge/DiffusionCommitRevertedByCommitEdgeType.php b/src/applications/diffusion/edge/DiffusionCommitRevertedByCommitEdgeType.php
index 53c39e54c..ae59a3b1e 100644
--- a/src/applications/diffusion/edge/DiffusionCommitRevertedByCommitEdgeType.php
+++ b/src/applications/diffusion/edge/DiffusionCommitRevertedByCommitEdgeType.php
@@ -1,104 +1,104 @@
<?php
final class DiffusionCommitRevertedByCommitEdgeType
extends PhabricatorEdgeType {
const EDGECONST = 56;
public function getInverseEdgeConstant() {
return DiffusionCommitRevertsCommitEdgeType::EDGECONST;
}
public function shouldWriteInverseTransactions() {
return true;
}
public function getTransactionAddString(
$actor,
$add_count,
$add_edges) {
return pht(
- '%s added %s reverting commit(s): %s.',
+ '%s added %s reverting change(s): %s.',
$actor,
$add_count,
$add_edges);
}
public function getTransactionRemoveString(
$actor,
$rem_count,
$rem_edges) {
return pht(
- '%s removed %s reverting commit(s): %s.',
+ '%s removed %s reverting change(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 reverting commit(s), added %s: %s; removed %s: %s.',
+ '%s edited reverting change(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 reverting commit(s) for %s: %s.',
+ '%s added %s reverting change(s) for %s: %s.',
$actor,
$add_count,
$object,
$add_edges);
}
public function getFeedRemoveString(
$actor,
$object,
$rem_count,
$rem_edges) {
return pht(
- '%s removed %s reverting commit(s) for %s: %s.',
+ '%s removed %s reverting change(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 reverting commit(s) for %s, added %s: %s; removed %s: %s.',
+ '%s edited reverting change(s) for %s, added %s: %s; removed %s: %s.',
$actor,
$object,
$add_count,
$add_edges,
$rem_count,
$rem_edges);
}
}
diff --git a/src/applications/diffusion/edge/DiffusionCommitRevertsCommitEdgeType.php b/src/applications/diffusion/edge/DiffusionCommitRevertsCommitEdgeType.php
index 8f3279717..ee0223c96 100644
--- a/src/applications/diffusion/edge/DiffusionCommitRevertsCommitEdgeType.php
+++ b/src/applications/diffusion/edge/DiffusionCommitRevertsCommitEdgeType.php
@@ -1,107 +1,107 @@
<?php
final class DiffusionCommitRevertsCommitEdgeType extends PhabricatorEdgeType {
const EDGECONST = 55;
public function getInverseEdgeConstant() {
return DiffusionCommitRevertedByCommitEdgeType::EDGECONST;
}
public function shouldWriteInverseTransactions() {
return true;
}
public function shouldPreventCycles() {
return true;
}
public function getTransactionAddString(
$actor,
$add_count,
$add_edges) {
return pht(
- '%s added %s reverted commit(s): %s.',
+ '%s added %s reverted change(s): %s.',
$actor,
$add_count,
$add_edges);
}
public function getTransactionRemoveString(
$actor,
$rem_count,
$rem_edges) {
return pht(
- '%s removed %s reverted commit(s): %s.',
+ '%s removed %s reverted change(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 reverted commit(s), added %s: %s; removed %s: %s.',
+ '%s edited reverted change(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 reverted commit(s) for %s: %s.',
+ '%s added %s reverted change(s) for %s: %s.',
$actor,
$add_count,
$object,
$add_edges);
}
public function getFeedRemoveString(
$actor,
$object,
$rem_count,
$rem_edges) {
return pht(
- '%s removed %s reverted commit(s) for %s: %s.',
+ '%s removed %s reverted change(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 reverted commit(s) for %s, added %s: %s; removed %s: %s.',
+ '%s edited reverted change(s) for %s, added %s: %s; removed %s: %s.',
$actor,
$object,
$add_count,
$add_edges,
$rem_count,
$rem_edges);
}
}
diff --git a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php
index 32d7a8ca9..baffc82b1 100644
--- a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php
+++ b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php
@@ -1,1324 +1,1350 @@
<?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();
/* -( 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;
}
- $this->applyHeraldRefRules($ref_updates, $all_updates);
-
$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 {
- $this->rejectEnormousChanges($content_updates);
+ 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;
}
- $all_updates = array_merge($all_updates, $content_updates);
-
- $this->applyHeraldContentRules($content_updates, $all_updates);
+ 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, detect pushes which are actually imports
- // of an existing repository rather than an addition of new commits. If
- // this push is importing a bunch of stuff, set the importing flag on
- // the repository. It will be cleared once we fully process everything.
+ // If this went through cleanly and was an import, set the importing flag
+ // on the repository. It will be cleared once we fully process everything.
- if ($this->isInitialImport($all_updates)) {
+ 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,
- array $all_updates) {
+ private function applyHeraldRefRules(array $ref_updates) {
$this->applyHeraldRules(
$ref_updates,
- new HeraldPreCommitRefAdapter(),
- $all_updates);
+ new HeraldPreCommitRefAdapter());
}
- private function applyHeraldContentRules(
- array $content_updates,
- array $all_updates) {
+ private function applyHeraldContentRules(array $content_updates) {
$this->applyHeraldRules(
$content_updates,
- new HeraldPreCommitContentAdapter(),
- $all_updates);
+ new HeraldPreCommitContentAdapter());
}
private function applyHeraldRules(
array $updates,
- HeraldAdapter $adapter_template,
- array $all_updates) {
+ HeraldAdapter $adapter_template) {
if (!$updates) {
return;
}
- $adapter_template->setHookEngine($this);
+ $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(time());
+ ->setEpoch(PhabricatorTime::getNow());
}
private function newPushEvent() {
$viewer = $this->getViewer();
- return PhabricatorRepositoryPushEvent::initializeNewEvent($viewer)
+
+ $event = PhabricatorRepositoryPushEvent::initializeNewEvent($viewer)
->setRepositoryPHID($this->getRepository()->getPHID())
->setRemoteAddress($this->getRemoteAddress())
->setRemoteProtocol($this->getRemoteProtocol())
- ->setEpoch(time());
+ ->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;
}
foreach ($content_updates as $update) {
$identifier = $update->getRefNew();
try {
$changesets = $this->loadChangesetsForCommit($identifier);
$this->changesets[$identifier] = $changesets;
} 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();
}
$parser = new ArcanistDiffParser();
$changes = $parser->parseDiff($raw_diff);
$diff = DifferentialDiff::newEphemeralFromRawChanges(
$changes);
return $diff->getChangesets();
}
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/engineextension/DiffusionDatasourceEngineExtension.php b/src/applications/diffusion/engineextension/DiffusionDatasourceEngineExtension.php
new file mode 100644
index 000000000..9678b627b
--- /dev/null
+++ b/src/applications/diffusion/engineextension/DiffusionDatasourceEngineExtension.php
@@ -0,0 +1,82 @@
+<?php
+
+final class DiffusionDatasourceEngineExtension
+ extends PhabricatorDatasourceEngineExtension {
+
+ public function newQuickSearchDatasources() {
+ return array(
+ new DiffusionRepositoryDatasource(),
+ new DiffusionSymbolDatasource(),
+ );
+ }
+
+ public function newJumpURI($query) {
+ $viewer = $this->getViewer();
+
+ // Send "r" to Diffusion.
+ if (preg_match('/^r\z/i', $query)) {
+ return '/diffusion/';
+ }
+
+ // Send "a" to the commit list ("Audit").
+ if (preg_match('/^a\z/i', $query)) {
+ return '/diffusion/commit/';
+ }
+
+ // Send "r <string>" to a search for a matching repository.
+ $matches = null;
+ if (preg_match('/^r\s+(.+)\z/i', $query, $matches)) {
+ $raw_query = $matches[1];
+
+ $engine = id(new PhabricatorRepository())
+ ->newFerretEngine();
+
+ $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;
+ }
+
+ $repositories = id(new PhabricatorRepositoryQuery())
+ ->setViewer($viewer)
+ ->withFerretConstraint($engine, $fulltext_tokens)
+ ->execute();
+ if (count($repositories) == 1) {
+ // Just one match, jump to repository.
+ return head($repositories)->getURI();
+ } else {
+ // More than one match, jump to search.
+ return urisprintf(
+ '/diffusion/?order=relevance&query=%s#R',
+ $raw_query);
+ }
+ }
+
+ // Send "s <string>" to a symbol search.
+ $matches = null;
+ if (preg_match('/^s\s+(.+)\z/i', $query, $matches)) {
+ $symbol = $matches[1];
+
+ $parts = null;
+ if (preg_match('/(.*)(?:\\.|::|->)(.*)/', $symbol, $parts)) {
+ return urisprintf(
+ '/diffusion/symbol/%s/?jump=true&context=%s',
+ $parts[2],
+ $parts[1]);
+ } else {
+ return urisprintf(
+ '/diffusion/symbol/%s/?jump=true',
+ $symbol);
+ }
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/applications/diffusion/engineextension/DiffusionQuickSearchEngineExtension.php b/src/applications/diffusion/engineextension/DiffusionQuickSearchEngineExtension.php
deleted file mode 100644
index 80c705b4f..000000000
--- a/src/applications/diffusion/engineextension/DiffusionQuickSearchEngineExtension.php
+++ /dev/null
@@ -1,12 +0,0 @@
-<?php
-
-final class DiffusionQuickSearchEngineExtension
- extends PhabricatorQuickSearchEngineExtension {
-
- public function newQuickSearchDatasources() {
- return array(
- new DiffusionRepositoryDatasource(),
- new DiffusionSymbolDatasource(),
- );
- }
-}
diff --git a/src/applications/diffusion/gitlfs/DiffusionGitLFSAuthenticateWorkflow.php b/src/applications/diffusion/gitlfs/DiffusionGitLFSAuthenticateWorkflow.php
index 41c009455..56297f4d8 100644
--- a/src/applications/diffusion/gitlfs/DiffusionGitLFSAuthenticateWorkflow.php
+++ b/src/applications/diffusion/gitlfs/DiffusionGitLFSAuthenticateWorkflow.php
@@ -1,110 +1,110 @@
<?php
final class DiffusionGitLFSAuthenticateWorkflow
extends DiffusionGitSSHWorkflow {
protected function didConstruct() {
$this->setName('git-lfs-authenticate');
$this->setArguments(
array(
array(
'name' => 'argv',
'wildcard' => true,
),
));
}
protected function identifyRepository() {
return $this->loadRepositoryWithPath(
$this->getLFSPathArgument(),
PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
}
private function getLFSPathArgument() {
return $this->getLFSArgument(0);
}
private function getLFSOperationArgument() {
return $this->getLFSArgument(1);
}
private function getLFSArgument($position) {
$args = $this->getArgs();
$argv = $args->getArg('argv');
if (!isset($argv[$position])) {
throw new Exception(
pht(
'Expected `git-lfs-authenticate <path> <operation>`, but received '.
'too few arguments.'));
}
return $argv[$position];
}
protected function executeRepositoryOperations() {
$operation = $this->getLFSOperationArgument();
// NOTE: We aren't checking write access here, even for "upload". The
// HTTP endpoint should be able to do that for us.
switch ($operation) {
case 'upload':
case 'download':
break;
default:
throw new Exception(
pht(
'Git LFS operation "%s" is not supported by this server.',
$operation));
}
$repository = $this->getRepository();
if (!$repository->isGit()) {
throw new Exception(
pht(
'Repository "%s" is not a Git repository. Git LFS is only '.
'supported for Git repositories.',
$repository->getDisplayName()));
}
if (!$repository->canUseGitLFS()) {
throw new Exception(
pht('Git LFS is not enabled for this repository.'));
}
// NOTE: This is usually the same as the default URI (which does not
// need to be specified in the response), but the protocol or domain may
// differ in some situations.
$lfs_uri = $repository->getGitLFSURI('info/lfs');
// Generate a temporary token to allow the user to access LFS over HTTP.
// This works even if normal HTTP repository operations are not available
// on this host, and does not require the user to have a VCS password.
- $user = $this->getUser();
+ $user = $this->getSSHUser();
$authorization = DiffusionGitLFSTemporaryTokenType::newHTTPAuthorization(
$repository,
$user,
$operation);
$headers = array(
'authorization' => $authorization,
);
$result = array(
'header' => $headers,
'href' => $lfs_uri,
);
$result = phutil_json_encode($result);
$this->writeIO($result);
$this->waitForGitClient();
return 0;
}
}
diff --git a/src/applications/diffusion/harbormaster/DiffusionBuildableEngine.php b/src/applications/diffusion/harbormaster/DiffusionBuildableEngine.php
new file mode 100644
index 000000000..bb222df28
--- /dev/null
+++ b/src/applications/diffusion/harbormaster/DiffusionBuildableEngine.php
@@ -0,0 +1,37 @@
+<?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));
+ }
+
+}
diff --git a/src/applications/diffusion/herald/DiffusionCommitAuthorProjectsHeraldField.php b/src/applications/diffusion/herald/DiffusionCommitAuthorProjectsHeraldField.php
new file mode 100644
index 000000000..a90480d50
--- /dev/null
+++ b/src/applications/diffusion/herald/DiffusionCommitAuthorProjectsHeraldField.php
@@ -0,0 +1,38 @@
+<?php
+
+final class DiffusionCommitAuthorProjectsHeraldField
+ extends DiffusionCommitHeraldField {
+
+ const FIELDCONST = 'diffusion.commit.author.projects';
+
+ public function getHeraldFieldName() {
+ return pht("Author's projects");
+ }
+
+ public function getHeraldFieldValue($object) {
+ $adapter = $this->getAdapter();
+
+ $phid = $object->getCommitData()->getCommitDetail('authorPHID');
+ if (!$phid) {
+ return array();
+ }
+
+ $viewer = $adapter->getViewer();
+
+ $projects = id(new PhabricatorProjectQuery())
+ ->setViewer($viewer)
+ ->withMemberPHIDs(array($phid))
+ ->execute();
+
+ return mpull($projects, 'getPHID');
+ }
+
+ protected function getHeraldFieldStandardType() {
+ return self::STANDARD_PHID_LIST;
+ }
+
+ protected function getDatasource() {
+ return new PhabricatorProjectDatasource();
+ }
+
+}
diff --git a/src/applications/diffusion/herald/DiffusionCommitBranchesHeraldField.php b/src/applications/diffusion/herald/DiffusionCommitBranchesHeraldField.php
index 03c4e3be6..064830ebc 100644
--- a/src/applications/diffusion/herald/DiffusionCommitBranchesHeraldField.php
+++ b/src/applications/diffusion/herald/DiffusionCommitBranchesHeraldField.php
@@ -1,34 +1,36 @@
<?php
final class DiffusionCommitBranchesHeraldField
extends DiffusionCommitHeraldField {
const FIELDCONST = 'diffusion.commit.branches';
public function getHeraldFieldName() {
return pht('Branches');
}
public function getHeraldFieldValue($object) {
+ $viewer = $this->getAdapter()->getViewer();
+
$commit = $object;
$repository = $object->getRepository();
$params = array(
'repository' => $repository->getPHID(),
'contains' => $commit->getCommitIdentifier(),
);
$result = id(new ConduitCall('diffusion.branchquery', $params))
- ->setUser(PhabricatorUser::getOmnipotentUser())
+ ->setUser($viewer)
->execute();
$refs = DiffusionRepositoryRef::loadAllFromDictionaries($result);
return mpull($refs, 'getShortName');
}
protected function getHeraldFieldStandardType() {
return self::STANDARD_TEXT_LIST;
}
}
diff --git a/src/applications/diffusion/herald/DiffusionCommitCommitterProjectsHeraldField.php b/src/applications/diffusion/herald/DiffusionCommitCommitterProjectsHeraldField.php
new file mode 100644
index 000000000..97e604a48
--- /dev/null
+++ b/src/applications/diffusion/herald/DiffusionCommitCommitterProjectsHeraldField.php
@@ -0,0 +1,38 @@
+<?php
+
+final class DiffusionCommitCommitterProjectsHeraldField
+ extends DiffusionCommitHeraldField {
+
+ const FIELDCONST = 'diffusion.commit.committer.projects';
+
+ public function getHeraldFieldName() {
+ return pht("Committer's projects");
+ }
+
+ public function getHeraldFieldValue($object) {
+ $adapter = $this->getAdapter();
+
+ $phid = $object->getCommitData()->getCommitDetail('committerPHID');
+ if (!$phid) {
+ return array();
+ }
+
+ $viewer = $adapter->getViewer();
+
+ $projects = id(new PhabricatorProjectQuery())
+ ->setViewer($viewer)
+ ->withMemberPHIDs(array($phid))
+ ->execute();
+
+ return mpull($projects, 'getPHID');
+ }
+
+ protected function getHeraldFieldStandardType() {
+ return self::STANDARD_PHID_LIST;
+ }
+
+ protected function getDatasource() {
+ return new PhabricatorProjectDatasource();
+ }
+
+}
diff --git a/src/applications/diffusion/herald/DiffusionPreCommitContentAuthorProjectsHeraldField.php b/src/applications/diffusion/herald/DiffusionPreCommitContentAuthorProjectsHeraldField.php
new file mode 100644
index 000000000..ec138c9bd
--- /dev/null
+++ b/src/applications/diffusion/herald/DiffusionPreCommitContentAuthorProjectsHeraldField.php
@@ -0,0 +1,38 @@
+<?php
+
+final class DiffusionPreCommitContentAuthorProjectsHeraldField
+ extends DiffusionPreCommitContentHeraldField {
+
+ const FIELDCONST = 'diffusion.pre.commit.author.projects';
+
+ public function getHeraldFieldName() {
+ return pht("Author's projects");
+ }
+
+ public function getHeraldFieldValue($object) {
+ $adapter = $this->getAdapter();
+
+ $phid = $adapter->getAuthorPHID();
+ if (!$phid) {
+ return array();
+ }
+
+ $viewer = $adapter->getViewer();
+
+ $projects = id(new PhabricatorProjectQuery())
+ ->setViewer($viewer)
+ ->withMemberPHIDs(array($phid))
+ ->execute();
+
+ return mpull($projects, 'getPHID');
+ }
+
+ protected function getHeraldFieldStandardType() {
+ return self::STANDARD_PHID_LIST;
+ }
+
+ protected function getDatasource() {
+ return new PhabricatorProjectDatasource();
+ }
+
+}
diff --git a/src/applications/diffusion/herald/DiffusionPreCommitContentCommitterProjectsHeraldField.php b/src/applications/diffusion/herald/DiffusionPreCommitContentCommitterProjectsHeraldField.php
new file mode 100644
index 000000000..9b0f012f3
--- /dev/null
+++ b/src/applications/diffusion/herald/DiffusionPreCommitContentCommitterProjectsHeraldField.php
@@ -0,0 +1,38 @@
+<?php
+
+final class DiffusionPreCommitContentCommitterProjectsHeraldField
+ extends DiffusionPreCommitContentHeraldField {
+
+ const FIELDCONST = 'diffusion.pre.commit.committer.projects';
+
+ public function getHeraldFieldName() {
+ return pht("Committer's projects");
+ }
+
+ public function getHeraldFieldValue($object) {
+ $adapter = $this->getAdapter();
+
+ $phid = $adapter->getCommitterPHID();
+ if (!$phid) {
+ return array();
+ }
+
+ $viewer = $adapter->getViewer();
+
+ $projects = id(new PhabricatorProjectQuery())
+ ->setViewer($viewer)
+ ->withMemberPHIDs(array($phid))
+ ->execute();
+
+ return mpull($projects, 'getPHID');
+ }
+
+ protected function getHeraldFieldStandardType() {
+ return self::STANDARD_PHID_LIST;
+ }
+
+ protected function getDatasource() {
+ return new PhabricatorProjectDatasource();
+ }
+
+}
diff --git a/src/applications/diffusion/herald/DiffusionPreCommitContentPackageHeraldField.php b/src/applications/diffusion/herald/DiffusionPreCommitContentPackageHeraldField.php
new file mode 100644
index 000000000..084d3b2d0
--- /dev/null
+++ b/src/applications/diffusion/herald/DiffusionPreCommitContentPackageHeraldField.php
@@ -0,0 +1,29 @@
+<?php
+
+final class DiffusionPreCommitContentPackageHeraldField
+ extends DiffusionPreCommitContentHeraldField {
+
+ const FIELDCONST = 'diffusion.pre.content.package';
+
+ public function getHeraldFieldName() {
+ return pht('Affected packages');
+ }
+
+ public function getFieldGroupKey() {
+ return HeraldRelatedFieldGroup::FIELDGROUPKEY;
+ }
+
+ public function getHeraldFieldValue($object) {
+ $packages = $this->getAdapter()->loadAffectedPackages();
+ return mpull($packages, 'getPHID');
+ }
+
+ protected function getHeraldFieldStandardType() {
+ return self::STANDARD_PHID_LIST;
+ }
+
+ protected function getDatasource() {
+ return new PhabricatorOwnersPackageDatasource();
+ }
+
+}
diff --git a/src/applications/diffusion/herald/DiffusionPreCommitContentPackageOwnerHeraldField.php b/src/applications/diffusion/herald/DiffusionPreCommitContentPackageOwnerHeraldField.php
new file mode 100644
index 000000000..564ef3682
--- /dev/null
+++ b/src/applications/diffusion/herald/DiffusionPreCommitContentPackageOwnerHeraldField.php
@@ -0,0 +1,34 @@
+<?php
+
+final class DiffusionPreCommitContentPackageOwnerHeraldField
+ extends DiffusionPreCommitContentHeraldField {
+
+ const FIELDCONST = 'diffusion.pre.content.package.owners';
+
+ public function getHeraldFieldName() {
+ return pht('Affected package owners');
+ }
+
+ public function getFieldGroupKey() {
+ return HeraldRelatedFieldGroup::FIELDGROUPKEY;
+ }
+
+ public function getHeraldFieldValue($object) {
+ $packages = $this->getAdapter()->loadAffectedPackages();
+ if (!$packages) {
+ return array();
+ }
+
+ $owners = PhabricatorOwnersOwner::loadAllForPackages($packages);
+ return mpull($owners, 'getUserPHID');
+ }
+
+ protected function getHeraldFieldStandardType() {
+ return self::STANDARD_PHID_LIST;
+ }
+
+ protected function getDatasource() {
+ return new PhabricatorProjectOrUserDatasource();
+ }
+
+}
diff --git a/src/applications/diffusion/herald/DiffusionPreCommitContentPusherProjectsHeraldField.php b/src/applications/diffusion/herald/DiffusionPreCommitContentPusherProjectsHeraldField.php
index 6a8c79c10..eef82124c 100644
--- a/src/applications/diffusion/herald/DiffusionPreCommitContentPusherProjectsHeraldField.php
+++ b/src/applications/diffusion/herald/DiffusionPreCommitContentPusherProjectsHeraldField.php
@@ -1,26 +1,26 @@
<?php
final class DiffusionPreCommitContentPusherProjectsHeraldField
extends DiffusionPreCommitContentHeraldField {
const FIELDCONST = 'diffusion.pre.content.pusher.projects';
public function getHeraldFieldName() {
- return pht('Pusher projects');
+ return pht("Pusher's projects");
}
public function getHeraldFieldValue($object) {
return $this->getAdapter()
->getHookEngine()
->loadViewerProjectPHIDsForHerald();
}
protected function getHeraldFieldStandardType() {
return HeraldField::STANDARD_PHID_LIST;
}
protected function getDatasource() {
return new PhabricatorProjectDatasource();
}
}
diff --git a/src/applications/diffusion/herald/DiffusionPreCommitRefPusherProjectsHeraldField.php b/src/applications/diffusion/herald/DiffusionPreCommitRefPusherProjectsHeraldField.php
index 6fff0a357..937e758ac 100644
--- a/src/applications/diffusion/herald/DiffusionPreCommitRefPusherProjectsHeraldField.php
+++ b/src/applications/diffusion/herald/DiffusionPreCommitRefPusherProjectsHeraldField.php
@@ -1,26 +1,26 @@
<?php
final class DiffusionPreCommitRefPusherProjectsHeraldField
extends DiffusionPreCommitRefHeraldField {
const FIELDCONST = 'diffusion.pre.ref.pusher.projects';
public function getHeraldFieldName() {
- return pht('Pusher projects');
+ return pht("Pusher's projects");
}
public function getHeraldFieldValue($object) {
return $this->getAdapter()
->getHookEngine()
->loadViewerProjectPHIDsForHerald();
}
protected function getHeraldFieldStandardType() {
return HeraldField::STANDARD_PHID_LIST;
}
protected function getDatasource() {
return new PhabricatorProjectDatasource();
}
}
diff --git a/src/applications/diffusion/herald/DiffusionPreCommitRefRepositoryProjectsHeraldField.php b/src/applications/diffusion/herald/DiffusionPreCommitRefRepositoryProjectsHeraldField.php
index df262524f..17670a1c4 100644
--- a/src/applications/diffusion/herald/DiffusionPreCommitRefRepositoryProjectsHeraldField.php
+++ b/src/applications/diffusion/herald/DiffusionPreCommitRefRepositoryProjectsHeraldField.php
@@ -1,26 +1,26 @@
<?php
final class DiffusionPreCommitRefRepositoryProjectsHeraldField
extends DiffusionPreCommitRefHeraldField {
const FIELDCONST = 'diffusion.pre.ref.repository.projects';
public function getHeraldFieldName() {
return pht('Repository projects');
}
public function getHeraldFieldValue($object) {
return PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->getAdapter()->getHookEngine()->getRepository()->getPHID(),
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
}
protected function getHeraldFieldStandardType() {
- return HeraldField::STANDARD_PHID_LIST;
+ return self::STANDARD_PHID_LIST;
}
protected function getDatasource() {
return new PhabricatorProjectDatasource();
}
}
diff --git a/src/applications/diffusion/herald/HeraldCommitAdapter.php b/src/applications/diffusion/herald/HeraldCommitAdapter.php
index e0e15352b..fc3c8c4f5 100644
--- a/src/applications/diffusion/herald/HeraldCommitAdapter.php
+++ b/src/applications/diffusion/herald/HeraldCommitAdapter.php
@@ -1,366 +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,
- PhabricatorUser::getOmnipotentUser());
+ $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(PhabricatorUser::getOmnipotentUser())
+ ->setViewer($viewer)
->needReviewers(true)
->executeOne();
if ($revision) {
$this->affectedRevision = $revision;
}
}
}
+
return $this->affectedRevision;
}
public static function getEnormousByteLimit() {
return 1024 * 1024 * 1024; // 1GB
}
public static function getEnormousTimeLimit() {
return 60 * 15; // 15 Minutes
}
private function loadCommitDiff() {
$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 = PhabricatorUser::getOmnipotentUser();
+ $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/herald/HeraldPreCommitAdapter.php b/src/applications/diffusion/herald/HeraldPreCommitAdapter.php
index 3f1bc6bfd..f63f647da 100644
--- a/src/applications/diffusion/herald/HeraldPreCommitAdapter.php
+++ b/src/applications/diffusion/herald/HeraldPreCommitAdapter.php
@@ -1,90 +1,94 @@
<?php
abstract class HeraldPreCommitAdapter extends HeraldAdapter {
private $log;
private $hookEngine;
abstract public function isPreCommitRefAdapter();
public function setPushLog(PhabricatorRepositoryPushLog $log) {
$this->log = $log;
return $this;
}
public function setHookEngine(DiffusionCommitHookEngine $engine) {
$this->hookEngine = $engine;
return $this;
}
public function getHookEngine() {
return $this->hookEngine;
}
public function getAdapterApplicationClass() {
return 'PhabricatorDiffusionApplication';
}
public function isTestAdapterForObject($object) {
return ($object instanceof PhabricatorRepositoryCommit);
}
public function canCreateTestAdapterForObject($object) {
return false;
}
public function getAdapterTestDescription() {
return pht(
'Commit hook events depend on repository state which is only available '.
'at push time, and can not be run in test mode.');
}
protected function initializeNewAdapter() {
$this->log = new PhabricatorRepositoryPushLog();
}
public function isSingleEventAdapter() {
return true;
}
public function getObject() {
return $this->log;
}
public function supportsRuleType($rule_type) {
switch ($rule_type) {
case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
return true;
default:
return false;
}
}
public function canTriggerOnObject($object) {
if ($object instanceof PhabricatorRepository) {
return true;
}
if ($object instanceof PhabricatorProject) {
return true;
}
return false;
}
public function explainValidTriggerObjects() {
return pht('This rule can trigger for **repositories** or **projects**.');
}
public function getTriggerObjectPHIDs() {
return array_merge(
array(
$this->hookEngine->getRepository()->getPHID(),
$this->getPHID(),
),
$this->hookEngine->getRepository()->getProjectPHIDs());
}
+ public function supportsWebhooks() {
+ return false;
+ }
+
}
diff --git a/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php b/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php
index c93ba3234..cee5f9741 100644
--- a/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php
+++ b/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php
@@ -1,226 +1,240 @@
<?php
final class HeraldPreCommitContentAdapter extends HeraldPreCommitAdapter {
private $changesets;
private $commitRef;
private $fields;
private $revision = false;
+ private $affectedPackages;
+
public function getAdapterContentName() {
return pht('Commit Hook: Commit Content');
}
public function getAdapterSortOrder() {
return 2500;
}
public function getAdapterContentDescription() {
return pht(
"React to commits being pushed to hosted repositories.\n".
"Hook rules can block changes and send push summary mail.");
}
public function isPreCommitRefAdapter() {
return false;
}
public function getHeraldName() {
return pht('Push Log (Content)');
}
public function isDiffEnormous() {
$this->getDiffContent('*');
return ($this->changesets instanceof Exception);
}
public function getDiffContent($type) {
if ($this->changesets === null) {
try {
$this->changesets = $this->getHookEngine()->getChangesetsForCommit(
$this->getObject()->getRefNew());
} catch (Exception $ex) {
$this->changesets = $ex;
}
}
if ($this->changesets instanceof Exception) {
$ex_class = get_class($this->changesets);
$ex_message = $this->changesets->getMessage();
if ($type === 'name') {
return array("<{$ex_class}: {$ex_message}>");
} else {
return array("<{$ex_class}>" => $ex_message);
}
}
$result = array();
if ($type === 'name') {
foreach ($this->changesets as $change) {
$result[] = $change->getFilename();
}
} else {
foreach ($this->changesets as $change) {
$lines = array();
foreach ($change->getHunks() as $hunk) {
switch ($type) {
case '-':
$lines[] = $hunk->makeOldFile();
break;
case '+':
$lines[] = $hunk->makeNewFile();
break;
case '*':
default:
$lines[] = $hunk->makeChanges();
break;
}
}
$result[$change->getFilename()] = implode('', $lines);
}
}
return $result;
}
public function getCommitRef() {
if ($this->commitRef === null) {
$this->commitRef = $this->getHookEngine()->loadCommitRefForCommit(
$this->getObject()->getRefNew());
}
return $this->commitRef;
}
public function getAuthorPHID() {
$repository = $this->getHookEngine()->getRepository();
$vcs = $repository->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$ref = $this->getCommitRef();
$author = $ref->getAuthor();
if (!strlen($author)) {
return null;
}
return $this->lookupUser($author);
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
// In Subversion, the pusher is always the author.
return $this->getHookEngine()->getViewer()->getPHID();
}
}
public function getCommitterPHID() {
$repository = $this->getHookEngine()->getRepository();
$vcs = $repository->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
// If there's no committer information, we're going to return the
// author instead. However, if there's committer information and we
// can't resolve it, return `null`.
$ref = $this->getCommitRef();
$committer = $ref->getCommitter();
if (!strlen($committer)) {
return $this->getAuthorPHID();
}
return $this->lookupUser($committer);
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
// In Subversion, the pusher is always the committer.
return $this->getHookEngine()->getViewer()->getPHID();
}
}
public function getAuthorRaw() {
$repository = $this->getHookEngine()->getRepository();
$vcs = $repository->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$ref = $this->getCommitRef();
return $ref->getAuthor();
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
// In Subversion, the pusher is always the author.
return $this->getHookEngine()->getViewer()->getUsername();
}
}
public function getCommitterRaw() {
$repository = $this->getHookEngine()->getRepository();
$vcs = $repository->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
// Here, if there's no committer, we're going to return the author
// instead.
$ref = $this->getCommitRef();
$committer = $ref->getCommitter();
if (strlen($committer)) {
return $committer;
}
return $ref->getAuthor();
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
// In Subversion, the pusher is always the committer.
return $this->getHookEngine()->getViewer()->getUsername();
}
}
private function lookupUser($author) {
return id(new DiffusionResolveUserQuery())
->withName($author)
->execute();
}
private function getCommitFields() {
if ($this->fields === null) {
$this->fields = id(new DiffusionLowLevelCommitFieldsQuery())
->setRepository($this->getHookEngine()->getRepository())
->withCommitRef($this->getCommitRef())
->execute();
}
return $this->fields;
}
public function getRevision() {
if ($this->revision === false) {
$fields = $this->getCommitFields();
$revision_id = idx($fields, 'revisionID');
if (!$revision_id) {
$this->revision = null;
} else {
$this->revision = id(new DifferentialRevisionQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withIDs(array($revision_id))
->needReviewers(true)
->executeOne();
}
}
return $this->revision;
}
public function getIsMergeCommit() {
$repository = $this->getHookEngine()->getRepository();
$vcs = $repository->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$parents = id(new DiffusionLowLevelParentsQuery())
->setRepository($repository)
->withIdentifier($this->getObject()->getRefNew())
->execute();
return (count($parents) > 1);
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
// NOTE: For now, we ignore "svn:mergeinfo" at all levels. We might
// change this some day, but it's not nearly as clear a signal as
// ancestry is in Git/Mercurial.
return false;
}
}
public function getBranches() {
return $this->getHookEngine()->loadBranches(
$this->getObject()->getRefNew());
}
+ public function loadAffectedPackages() {
+ if ($this->affectedPackages === null) {
+ $packages = PhabricatorOwnersPackage::loadAffectedPackages(
+ $this->getHookEngine()->getRepository(),
+ $this->getDiffContent('name'));
+ $this->affectedPackages = $packages;
+ }
+
+ return $this->affectedPackages;
+ }
+
+
}
diff --git a/src/applications/diffusion/protocol/DiffusionCommandEngine.php b/src/applications/diffusion/protocol/DiffusionCommandEngine.php
index 53a086db3..d7da62b11 100644
--- a/src/applications/diffusion/protocol/DiffusionCommandEngine.php
+++ b/src/applications/diffusion/protocol/DiffusionCommandEngine.php
@@ -1,298 +1,303 @@
<?php
abstract class DiffusionCommandEngine extends Phobject {
private $repository;
private $protocol;
private $credentialPHID;
private $argv;
private $passthru;
private $connectAsDevice;
private $sudoAsDaemon;
private $uri;
public static function newCommandEngine(PhabricatorRepository $repository) {
$engines = self::newCommandEngines();
foreach ($engines as $engine) {
if ($engine->canBuildForRepository($repository)) {
return id(clone $engine)
->setRepository($repository);
}
}
throw new Exception(
pht(
'No registered command engine can build commands for this '.
'repository ("%s").',
$repository->getDisplayName()));
}
private static function newCommandEngines() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->execute();
}
abstract protected function canBuildForRepository(
PhabricatorRepository $repository);
abstract protected function newFormattedCommand($pattern, array $argv);
abstract protected function newCustomEnvironment();
public function setRepository(PhabricatorRepository $repository) {
$this->repository = $repository;
return $this;
}
public function getRepository() {
return $this->repository;
}
public function setURI(PhutilURI $uri) {
$this->uri = $uri;
$this->setProtocol($uri->getProtocol());
return $this;
}
public function getURI() {
return $this->uri;
}
public function setProtocol($protocol) {
$this->protocol = $protocol;
return $this;
}
public function getProtocol() {
return $this->protocol;
}
public function getDisplayProtocol() {
return $this->getProtocol().'://';
}
public function setCredentialPHID($credential_phid) {
$this->credentialPHID = $credential_phid;
return $this;
}
public function getCredentialPHID() {
return $this->credentialPHID;
}
public function setArgv(array $argv) {
$this->argv = $argv;
return $this;
}
public function getArgv() {
return $this->argv;
}
public function setPassthru($passthru) {
$this->passthru = $passthru;
return $this;
}
public function getPassthru() {
return $this->passthru;
}
public function setConnectAsDevice($connect_as_device) {
$this->connectAsDevice = $connect_as_device;
return $this;
}
public function getConnectAsDevice() {
return $this->connectAsDevice;
}
public function setSudoAsDaemon($sudo_as_daemon) {
$this->sudoAsDaemon = $sudo_as_daemon;
return $this;
}
public function getSudoAsDaemon() {
return $this->sudoAsDaemon;
}
public function newFuture() {
$argv = $this->newCommandArgv();
$env = $this->newCommandEnvironment();
if ($this->getSudoAsDaemon()) {
$command = call_user_func_array('csprintf', $argv);
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
$argv = array('%C', $command);
}
if ($this->getPassthru()) {
$future = newv('PhutilExecPassthru', $argv);
} else {
$future = newv('ExecFuture', $argv);
}
$future->setEnv($env);
+ // See T13108. By default, don't let any cluster command run indefinitely
+ // to try to avoid cases where `git fetch` hangs for some reason and we're
+ // left sitting with a held lock forever.
+ $future->setTimeout(phutil_units('15 minutes in seconds'));
+
return $future;
}
private function newCommandArgv() {
$argv = $this->argv;
$pattern = $argv[0];
$argv = array_slice($argv, 1);
list($pattern, $argv) = $this->newFormattedCommand($pattern, $argv);
return array_merge(array($pattern), $argv);
}
private function newCommandEnvironment() {
$env = $this->newCommonEnvironment() + $this->newCustomEnvironment();
foreach ($env as $key => $value) {
if ($value === null) {
unset($env[$key]);
}
}
return $env;
}
private function newCommonEnvironment() {
$repository = $this->getRepository();
$env = array();
// NOTE: Force the language to "en_US.UTF-8", which overrides locale
// settings. This makes stuff print in English instead of, e.g., French,
// so we can parse the output of some commands, error messages, etc.
$env['LANG'] = 'en_US.UTF-8';
// Propagate PHABRICATOR_ENV explicitly. For discussion, see T4155.
$env['PHABRICATOR_ENV'] = PhabricatorEnv::getSelectedEnvironmentName();
$as_device = $this->getConnectAsDevice();
$credential_phid = $this->getCredentialPHID();
if ($as_device) {
$device = AlmanacKeys::getLiveDevice();
if (!$device) {
throw new Exception(
pht(
'Attempting to build a repository command (for repository "%s") '.
'as device, but this host ("%s") is not configured as a cluster '.
'device.',
$repository->getDisplayName(),
php_uname('n')));
}
if ($credential_phid) {
throw new Exception(
pht(
'Attempting to build a repository command (for repository "%s"), '.
'but the CommandEngine is configured to connect as both the '.
'current cluster device ("%s") and with a specific credential '.
'("%s"). These options are mutually exclusive. Connections must '.
'authenticate as one or the other, not both.',
$repository->getDisplayName(),
$device->getName(),
$credential_phid));
}
}
if ($this->isAnySSHProtocol()) {
if ($credential_phid) {
$env['PHABRICATOR_CREDENTIAL'] = $credential_phid;
}
if ($as_device) {
$env['PHABRICATOR_AS_DEVICE'] = 1;
}
}
$env += $repository->getPassthroughEnvironmentalVariables();
return $env;
}
public function isSSHProtocol() {
return ($this->getProtocol() == 'ssh');
}
public function isSVNProtocol() {
return ($this->getProtocol() == 'svn');
}
public function isSVNSSHProtocol() {
return ($this->getProtocol() == 'svn+ssh');
}
public function isHTTPProtocol() {
return ($this->getProtocol() == 'http');
}
public function isHTTPSProtocol() {
return ($this->getProtocol() == 'https');
}
public function isAnyHTTPProtocol() {
return ($this->isHTTPProtocol() || $this->isHTTPSProtocol());
}
public function isAnySSHProtocol() {
return ($this->isSSHProtocol() || $this->isSVNSSHProtocol());
}
public function isCredentialSupported() {
return ($this->getPassphraseProvidesCredentialType() !== null);
}
public function isCredentialOptional() {
if ($this->isAnySSHProtocol()) {
return false;
}
return true;
}
public function getPassphraseCredentialLabel() {
if ($this->isAnySSHProtocol()) {
return pht('SSH Key');
}
if ($this->isAnyHTTPProtocol() || $this->isSVNProtocol()) {
return pht('Password');
}
return null;
}
public function getPassphraseDefaultCredentialType() {
if ($this->isAnySSHProtocol()) {
return PassphraseSSHPrivateKeyTextCredentialType::CREDENTIAL_TYPE;
}
if ($this->isAnyHTTPProtocol() || $this->isSVNProtocol()) {
return PassphrasePasswordCredentialType::CREDENTIAL_TYPE;
}
return null;
}
public function getPassphraseProvidesCredentialType() {
if ($this->isAnySSHProtocol()) {
return PassphraseSSHPrivateKeyCredentialType::PROVIDES_TYPE;
}
if ($this->isAnyHTTPProtocol() || $this->isSVNProtocol()) {
return PassphrasePasswordCredentialType::PROVIDES_TYPE;
}
return null;
}
protected function getSSHWrapper() {
$root = dirname(phutil_get_library_root('phabricator'));
return $root.'/bin/ssh-connect';
}
}
diff --git a/src/applications/diffusion/protocol/DiffusionRepositoryClusterEngine.php b/src/applications/diffusion/protocol/DiffusionRepositoryClusterEngine.php
index 35bb4d4ac..d5ba74a30 100644
--- a/src/applications/diffusion/protocol/DiffusionRepositoryClusterEngine.php
+++ b/src/applications/diffusion/protocol/DiffusionRepositoryClusterEngine.php
@@ -1,766 +1,824 @@
<?php
/**
* Manages repository synchronization for cluster repositories.
*
* @task config Configuring Synchronization
* @task sync Cluster Synchronization
* @task internal Internals
*/
final class DiffusionRepositoryClusterEngine extends Phobject {
private $repository;
private $viewer;
private $logger;
private $clusterWriteLock;
private $clusterWriteVersion;
private $clusterWriteOwner;
/* -( Configuring Synchronization )---------------------------------------- */
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 setLog(DiffusionRepositoryClusterEngineLogInterface $log) {
$this->logger = $log;
return $this;
}
/* -( Cluster Synchronization )-------------------------------------------- */
/**
* Synchronize repository version information after creating a repository.
*
* This initializes working copy versions for all currently bound devices to
* 0, so that we don't get stuck making an ambiguous choice about which
* devices are leaders when we later synchronize before a read.
*
* @task sync
*/
public function synchronizeWorkingCopyAfterCreation() {
if (!$this->shouldEnableSynchronization(false)) {
return;
}
$repository = $this->getRepository();
$repository_phid = $repository->getPHID();
$service = $repository->loadAlmanacService();
if (!$service) {
throw new Exception(pht('Failed to load repository cluster service.'));
}
$bindings = $service->getActiveBindings();
foreach ($bindings as $binding) {
PhabricatorRepositoryWorkingCopyVersion::updateVersion(
$repository_phid,
$binding->getDevicePHID(),
0);
}
return $this;
}
/**
* @task sync
*/
public function synchronizeWorkingCopyAfterHostingChange() {
if (!$this->shouldEnableSynchronization(false)) {
return;
}
$repository = $this->getRepository();
$repository_phid = $repository->getPHID();
$versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions(
$repository_phid);
$versions = mpull($versions, null, 'getDevicePHID');
// After converting a hosted repository to observed, or vice versa, we
// need to reset version numbers because the clocks for observed and hosted
// repositories run on different units.
// We identify all the cluster leaders and reset their version to 0.
// We identify all the cluster followers and demote them.
// This allows the cluster to start over again at version 0 but keep the
// same leaders.
if ($versions) {
$max_version = (int)max(mpull($versions, 'getRepositoryVersion'));
foreach ($versions as $version) {
$device_phid = $version->getDevicePHID();
if ($version->getRepositoryVersion() == $max_version) {
PhabricatorRepositoryWorkingCopyVersion::updateVersion(
$repository_phid,
$device_phid,
0);
} else {
PhabricatorRepositoryWorkingCopyVersion::demoteDevice(
$repository_phid,
$device_phid);
}
}
}
return $this;
}
/**
* @task sync
*/
public function synchronizeWorkingCopyBeforeRead() {
if (!$this->shouldEnableSynchronization(true)) {
return;
}
$repository = $this->getRepository();
$repository_phid = $repository->getPHID();
$device = AlmanacKeys::getLiveDevice();
$device_phid = $device->getPHID();
$read_lock = PhabricatorRepositoryWorkingCopyVersion::getReadLock(
$repository_phid,
$device_phid);
$lock_wait = phutil_units('2 minutes in seconds');
$this->logLine(
pht(
- 'Waiting up to %s second(s) for a cluster read lock on "%s"...',
- new PhutilNumber($lock_wait),
+ 'Acquiring read lock for repository "%s" on device "%s"...',
+ $repository->getDisplayName(),
$device->getName()));
try {
$start = PhabricatorTime::getNow();
$read_lock->lock($lock_wait);
$waited = (PhabricatorTime::getNow() - $start);
if ($waited) {
$this->logLine(
pht(
'Acquired read lock after %s second(s).',
new PhutilNumber($waited)));
} else {
$this->logLine(
pht(
'Acquired read lock immediately.'));
}
} catch (Exception $ex) {
throw new PhutilProxyException(
pht(
'Failed to acquire read lock after waiting %s second(s). You '.
'may be able to retry later.',
new PhutilNumber($lock_wait)),
$ex);
}
$versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions(
$repository_phid);
$versions = mpull($versions, null, 'getDevicePHID');
$this_version = idx($versions, $device_phid);
if ($this_version) {
$this_version = (int)$this_version->getRepositoryVersion();
} else {
$this_version = -1;
}
if ($versions) {
// This is the normal case, where we have some version information and
// can identify which nodes are leaders. If the current node is not a
// leader, we want to fetch from a leader and then update our version.
$max_version = (int)max(mpull($versions, 'getRepositoryVersion'));
if ($max_version > $this_version) {
if ($repository->isHosted()) {
$fetchable = array();
foreach ($versions as $version) {
if ($version->getRepositoryVersion() == $max_version) {
$fetchable[] = $version->getDevicePHID();
}
}
$this->synchronizeWorkingCopyFromDevices($fetchable);
} else {
$this->synchornizeWorkingCopyFromRemote();
}
PhabricatorRepositoryWorkingCopyVersion::updateVersion(
$repository_phid,
$device_phid,
$max_version);
} else {
$this->logLine(
pht(
'Device "%s" is already a cluster leader and does not need '.
'to be synchronized.',
$device->getName()));
}
$result_version = $max_version;
} else {
// If no version records exist yet, we need to be careful, because we
// can not tell which nodes are leaders.
// There might be several nodes with arbitrary existing data, and we have
// no way to tell which one has the "right" data. If we pick wrong, we
// might erase some or all of the data in the repository.
// Since this is dangerous, we refuse to guess unless there is only one
// device. If we're the only device in the group, we obviously must be
// a leader.
$service = $repository->loadAlmanacService();
if (!$service) {
throw new Exception(pht('Failed to load repository cluster service.'));
}
$bindings = $service->getActiveBindings();
$device_map = array();
foreach ($bindings as $binding) {
$device_map[$binding->getDevicePHID()] = true;
}
if (count($device_map) > 1) {
throw new Exception(
pht(
'Repository "%s" exists on more than one device, but no device '.
'has any repository version information. Phabricator can not '.
'guess which copy of the existing data is authoritative. Promote '.
'a device or see "Ambiguous Leaders" in the documentation.',
$repository->getDisplayName()));
}
if (empty($device_map[$device->getPHID()])) {
throw new Exception(
pht(
'Repository "%s" is being synchronized on device "%s", but '.
'this device is not bound to the corresponding cluster '.
'service ("%s").',
$repository->getDisplayName(),
$device->getName(),
$service->getName()));
}
// The current device is the only device in service, so it must be a
// leader. We can safely have any future nodes which come online read
// from it.
PhabricatorRepositoryWorkingCopyVersion::updateVersion(
$repository_phid,
$device_phid,
0);
$result_version = 0;
}
$read_lock->unlock();
return $result_version;
}
/**
* @task sync
*/
public function synchronizeWorkingCopyBeforeWrite() {
if (!$this->shouldEnableSynchronization(true)) {
return;
}
$repository = $this->getRepository();
$viewer = $this->getViewer();
$repository_phid = $repository->getPHID();
$device = AlmanacKeys::getLiveDevice();
$device_phid = $device->getPHID();
$table = new PhabricatorRepositoryWorkingCopyVersion();
$locked_connection = $table->establishConnection('w');
$write_lock = PhabricatorRepositoryWorkingCopyVersion::getWriteLock(
$repository_phid);
$write_lock->useSpecificConnection($locked_connection);
- $lock_wait = phutil_units('2 minutes in seconds');
-
$this->logLine(
pht(
- 'Waiting up to %s second(s) for a cluster write lock...',
- new PhutilNumber($lock_wait)));
+ 'Acquiring write lock for repository "%s"...',
+ $repository->getDisplayName()));
+ $lock_wait = phutil_units('2 minutes in seconds');
try {
+ $write_wait_start = microtime(true);
+
$start = PhabricatorTime::getNow();
- $write_lock->lock($lock_wait);
- $waited = (PhabricatorTime::getNow() - $start);
+ $step_wait = 1;
+
+ while (true) {
+ try {
+ $write_lock->lock((int)floor($step_wait));
+ $write_wait_end = microtime(true);
+ break;
+ } catch (PhutilLockException $ex) {
+ $waited = (PhabricatorTime::getNow() - $start);
+ if ($waited > $lock_wait) {
+ throw $ex;
+ }
+ $this->logActiveWriter($viewer, $repository);
+ }
+ // Wait a little longer before the next message we print.
+ $step_wait = $step_wait + 0.5;
+ $step_wait = min($step_wait, 3);
+ }
+
+ $waited = (PhabricatorTime::getNow() - $start);
if ($waited) {
$this->logLine(
pht(
'Acquired write lock after %s second(s).',
new PhutilNumber($waited)));
} else {
$this->logLine(
pht(
'Acquired write lock immediately.'));
}
} catch (Exception $ex) {
throw new PhutilProxyException(
pht(
'Failed to acquire write lock after waiting %s second(s). You '.
'may be able to retry later.',
new PhutilNumber($lock_wait)),
$ex);
}
$versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions(
$repository_phid);
foreach ($versions as $version) {
if (!$version->getIsWriting()) {
continue;
}
throw new Exception(
pht(
'An previous write to this repository was interrupted; refusing '.
'new writes. This issue requires operator intervention to resolve, '.
'see "Write Interruptions" in the "Cluster: Repositories" in the '.
'documentation for instructions.'));
}
+ $read_wait_start = microtime(true);
try {
$max_version = $this->synchronizeWorkingCopyBeforeRead();
} catch (Exception $ex) {
$write_lock->unlock();
throw $ex;
}
+ $read_wait_end = microtime(true);
$pid = getmypid();
$hash = Filesystem::readRandomCharacters(12);
$this->clusterWriteOwner = "{$pid}.{$hash}";
PhabricatorRepositoryWorkingCopyVersion::willWrite(
$locked_connection,
$repository_phid,
$device_phid,
array(
'userPHID' => $viewer->getPHID(),
'epoch' => PhabricatorTime::getNow(),
'devicePHID' => $device_phid,
),
$this->clusterWriteOwner);
$this->clusterWriteVersion = $max_version;
$this->clusterWriteLock = $write_lock;
+
+ $write_wait = ($write_wait_end - $write_wait_start);
+ $read_wait = ($read_wait_end - $read_wait_start);
+
+ $log = $this->logger;
+ if ($log) {
+ $log->writeClusterEngineLogProperty('readWait', $read_wait);
+ $log->writeClusterEngineLogProperty('writeWait', $write_wait);
+ }
}
public function synchronizeWorkingCopyAfterDiscovery($new_version) {
if (!$this->shouldEnableSynchronization(true)) {
return;
}
$repository = $this->getRepository();
$repository_phid = $repository->getPHID();
if ($repository->isHosted()) {
return;
}
$viewer = $this->getViewer();
$device = AlmanacKeys::getLiveDevice();
$device_phid = $device->getPHID();
// NOTE: We are not holding a lock here because this method is only called
// from PhabricatorRepositoryDiscoveryEngine, which already holds a device
// lock. Even if we do race here and record an older version, the
// consequences are mild: we only do extra work to correct it later.
$versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions(
$repository_phid);
$versions = mpull($versions, null, 'getDevicePHID');
$this_version = idx($versions, $device_phid);
if ($this_version) {
$this_version = (int)$this_version->getRepositoryVersion();
} else {
$this_version = -1;
}
if ($new_version > $this_version) {
PhabricatorRepositoryWorkingCopyVersion::updateVersion(
$repository_phid,
$device_phid,
$new_version);
}
}
/**
* @task sync
*/
public function synchronizeWorkingCopyAfterWrite() {
if (!$this->shouldEnableSynchronization(true)) {
return;
}
if (!$this->clusterWriteLock) {
throw new Exception(
pht(
'Trying to synchronize after write, but not holding a write '.
'lock!'));
}
$repository = $this->getRepository();
$repository_phid = $repository->getPHID();
$device = AlmanacKeys::getLiveDevice();
$device_phid = $device->getPHID();
// It is possible that we've lost the global lock while receiving the push.
// For example, the master database may have been restarted between the
// time we acquired the global lock and now, when the push has finished.
// We wrote a durable lock while we were holding the the global lock,
// essentially upgrading our lock. We can still safely release this upgraded
// lock even if we're no longer holding the global lock.
// If we fail to release the lock, the repository will be frozen until
// an operator can figure out what happened, so we try pretty hard to
// reconnect to the database and release the lock.
$now = PhabricatorTime::getNow();
$duration = phutil_units('5 minutes in seconds');
$try_until = $now + $duration;
$did_release = false;
$already_failed = false;
while (PhabricatorTime::getNow() <= $try_until) {
try {
// NOTE: This means we're still bumping the version when pushes fail. We
// could select only un-rejected events instead to bump a little less
// often.
$new_log = id(new PhabricatorRepositoryPushEventQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withRepositoryPHIDs(array($repository_phid))
->setLimit(1)
->executeOne();
$old_version = $this->clusterWriteVersion;
if ($new_log) {
$new_version = $new_log->getID();
} else {
$new_version = $old_version;
}
PhabricatorRepositoryWorkingCopyVersion::didWrite(
$repository_phid,
$device_phid,
$this->clusterWriteVersion,
$new_version,
$this->clusterWriteOwner);
$did_release = true;
break;
} catch (AphrontConnectionQueryException $ex) {
$connection_exception = $ex;
} catch (AphrontConnectionLostQueryException $ex) {
$connection_exception = $ex;
}
if (!$already_failed) {
$already_failed = true;
$this->logLine(
pht('CRITICAL. Failed to release cluster write lock!'));
$this->logLine(
pht(
'The connection to the master database was lost while receiving '.
'the write.'));
$this->logLine(
pht(
'This process will spend %s more second(s) attempting to '.
'recover, then give up.',
new PhutilNumber($duration)));
}
sleep(1);
}
if ($did_release) {
if ($already_failed) {
$this->logLine(
pht('RECOVERED. Link to master database was restored.'));
}
$this->logLine(pht('Released cluster write lock.'));
} else {
throw new Exception(
pht(
'Failed to reconnect to master database and release held write '.
'lock ("%s") on device "%s" for repository "%s" after trying '.
'for %s seconds(s). This repository will be frozen.',
$this->clusterWriteOwner,
$device->getName(),
$this->getDisplayName(),
new PhutilNumber($duration)));
}
// We can continue even if we've lost this lock, everything is still
// consistent.
try {
$this->clusterWriteLock->unlock();
} catch (Exception $ex) {
// Ignore.
}
$this->clusterWriteLock = null;
$this->clusterWriteOwner = null;
}
/* -( Internals )---------------------------------------------------------- */
/**
* @task internal
*/
private function shouldEnableSynchronization($require_device) {
$repository = $this->getRepository();
$service_phid = $repository->getAlmanacServicePHID();
if (!$service_phid) {
return false;
}
if (!$repository->supportsSynchronization()) {
return false;
}
if ($require_device) {
$device = AlmanacKeys::getLiveDevice();
if (!$device) {
return false;
}
}
return true;
}
/**
* @task internal
*/
private function synchornizeWorkingCopyFromRemote() {
$repository = $this->getRepository();
$device = AlmanacKeys::getLiveDevice();
$local_path = $repository->getLocalPath();
$fetch_uri = $repository->getRemoteURIEnvelope();
if ($repository->isGit()) {
$this->requireWorkingCopy();
$argv = array(
'fetch --prune -- %P %s',
$fetch_uri,
'+refs/*:refs/*',
);
} else {
throw new Exception(pht('Remote sync only supported for git!'));
}
$future = DiffusionCommandEngine::newCommandEngine($repository)
->setArgv($argv)
->setSudoAsDaemon(true)
->setCredentialPHID($repository->getCredentialPHID())
->setURI($repository->getRemoteURIObject())
->newFuture();
$future->setCWD($local_path);
try {
$future->resolvex();
} catch (Exception $ex) {
$this->logLine(
pht(
'Synchronization of "%s" from remote failed: %s',
$device->getName(),
$ex->getMessage()));
throw $ex;
}
}
/**
* @task internal
*/
private function synchronizeWorkingCopyFromDevices(array $device_phids) {
$repository = $this->getRepository();
$service = $repository->loadAlmanacService();
if (!$service) {
throw new Exception(pht('Failed to load repository cluster service.'));
}
$device_map = array_fuse($device_phids);
$bindings = $service->getActiveBindings();
$fetchable = array();
foreach ($bindings as $binding) {
// We can't fetch from nodes which don't have the newest version.
$device_phid = $binding->getDevicePHID();
if (empty($device_map[$device_phid])) {
continue;
}
// TODO: For now, only fetch over SSH. We could support fetching over
// HTTP eventually.
if ($binding->getAlmanacPropertyValue('protocol') != 'ssh') {
continue;
}
$fetchable[] = $binding;
}
if (!$fetchable) {
throw new Exception(
pht(
'Leader lost: no up-to-date nodes in repository cluster are '.
'fetchable.'));
}
$caught = null;
foreach ($fetchable as $binding) {
try {
$this->synchronizeWorkingCopyFromBinding($binding);
$caught = null;
break;
} catch (Exception $ex) {
$caught = $ex;
}
}
if ($caught) {
throw $caught;
}
}
/**
* @task internal
*/
private function synchronizeWorkingCopyFromBinding($binding) {
$repository = $this->getRepository();
$device = AlmanacKeys::getLiveDevice();
$this->logLine(
pht(
'Synchronizing this device ("%s") from cluster leader ("%s") before '.
'read.',
$device->getName(),
$binding->getDevice()->getName()));
$fetch_uri = $repository->getClusterRepositoryURIFromBinding($binding);
$local_path = $repository->getLocalPath();
if ($repository->isGit()) {
$this->requireWorkingCopy();
$argv = array(
'fetch --prune -- %s %s',
$fetch_uri,
'+refs/*:refs/*',
);
} else {
throw new Exception(pht('Binding sync only supported for git!'));
}
$future = DiffusionCommandEngine::newCommandEngine($repository)
->setArgv($argv)
->setConnectAsDevice(true)
->setSudoAsDaemon(true)
->setURI($fetch_uri)
->newFuture();
$future->setCWD($local_path);
try {
$future->resolvex();
} catch (Exception $ex) {
$this->logLine(
pht(
'Synchronization of "%s" from leader "%s" failed: %s',
$device->getName(),
$binding->getDevice()->getName(),
$ex->getMessage()));
throw $ex;
}
}
/**
* @task internal
*/
private function logLine($message) {
return $this->logText("# {$message}\n");
}
/**
* @task internal
*/
private function logText($message) {
$log = $this->logger;
if ($log) {
$log->writeClusterEngineLogMessage($message);
}
return $this;
}
private function requireWorkingCopy() {
$repository = $this->getRepository();
$local_path = $repository->getLocalPath();
if (!Filesystem::pathExists($local_path)) {
$device = AlmanacKeys::getLiveDevice();
throw new Exception(
pht(
'Repository "%s" does not have a working copy on this device '.
'yet, so it can not be synchronized. Wait for the daemons to '.
'construct one or run `bin/repository update %s` on this host '.
'("%s") to build it explicitly.',
$repository->getDisplayName(),
$repository->getMonogram(),
$device->getName()));
}
}
+ private function logActiveWriter(
+ PhabricatorUser $viewer,
+ PhabricatorRepository $repository) {
+
+ $writer = PhabricatorRepositoryWorkingCopyVersion::loadWriter(
+ $repository->getPHID());
+ if (!$writer) {
+ $this->logLine(pht('Waiting on another user to finish writing...'));
+ return;
+ }
+
+ $user_phid = $writer->getWriteProperty('userPHID');
+ $device_phid = $writer->getWriteProperty('devicePHID');
+ $epoch = $writer->getWriteProperty('epoch');
+
+ $phids = array($user_phid, $device_phid);
+ $handles = $viewer->loadHandles($phids);
+
+ $duration = (PhabricatorTime::getNow() - $epoch) + 1;
+
+ $this->logLine(
+ pht(
+ 'Waiting for %s to finish writing (on device "%s" for %ss)...',
+ $handles[$user_phid]->getName(),
+ $handles[$device_phid]->getName(),
+ new PhutilNumber($duration)));
+ }
+
}
diff --git a/src/applications/diffusion/protocol/DiffusionRepositoryClusterEngineLogInterface.php b/src/applications/diffusion/protocol/DiffusionRepositoryClusterEngineLogInterface.php
index 9b1fe9a50..3e43d7277 100644
--- a/src/applications/diffusion/protocol/DiffusionRepositoryClusterEngineLogInterface.php
+++ b/src/applications/diffusion/protocol/DiffusionRepositoryClusterEngineLogInterface.php
@@ -1,7 +1,8 @@
<?php
interface DiffusionRepositoryClusterEngineLogInterface {
public function writeClusterEngineLogMessage($message);
+ public function writeClusterEngineLogProperty($key, $value);
}
diff --git a/src/applications/diffusion/query/DiffusionPullLogSearchEngine.php b/src/applications/diffusion/query/DiffusionPullLogSearchEngine.php
index 8d6102d4e..dfdfceb51 100644
--- a/src/applications/diffusion/query/DiffusionPullLogSearchEngine.php
+++ b/src/applications/diffusion/query/DiffusionPullLogSearchEngine.php
@@ -1,166 +1,186 @@
<?php
final class DiffusionPullLogSearchEngine
extends PhabricatorApplicationSearchEngine {
public function getResultTypeDescription() {
return pht('Pull Logs');
}
public function getApplicationClassName() {
return 'PhabricatorDiffusionApplication';
}
public function newQuery() {
return new PhabricatorRepositoryPullEventQuery();
}
protected function buildQueryFromParameters(array $map) {
$query = $this->newQuery();
if ($map['repositoryPHIDs']) {
$query->withRepositoryPHIDs($map['repositoryPHIDs']);
}
if ($map['pullerPHIDs']) {
$query->withPullerPHIDs($map['pullerPHIDs']);
}
+ if ($map['createdStart'] || $map['createdEnd']) {
+ $query->withEpochBetween(
+ $map['createdStart'],
+ $map['createdEnd']);
+ }
+
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.')),
id(new PhabricatorUsersSearchField())
->setKey('pullerPHIDs')
->setAliases(array('puller', 'pullers', 'pullerPHID'))
->setLabel(pht('Pullers'))
->setDescription(
pht('Search for pull logs by specific users.')),
+ id(new PhabricatorSearchDateField())
+ ->setLabel(pht('Created After'))
+ ->setKey('createdStart'),
+ id(new PhabricatorSearchDateField())
+ ->setLabel(pht('Created Before'))
+ ->setKey('createdEnd'),
);
}
protected function newExportFields() {
- return array(
- id(new PhabricatorIDExportField())
- ->setKey('id')
- ->setLabel(pht('ID')),
- id(new PhabricatorPHIDExportField())
- ->setKey('phid')
- ->setLabel(pht('PHID')),
+ $viewer = $this->requireViewer();
+
+ $fields = array(
id(new PhabricatorPHIDExportField())
->setKey('repositoryPHID')
->setLabel(pht('Repository PHID')),
id(new PhabricatorStringExportField())
->setKey('repository')
->setLabel(pht('Repository')),
id(new PhabricatorPHIDExportField())
->setKey('pullerPHID')
->setLabel(pht('Puller PHID')),
id(new PhabricatorStringExportField())
->setKey('puller')
->setLabel(pht('Puller')),
id(new PhabricatorStringExportField())
->setKey('protocol')
->setLabel(pht('Protocol')),
id(new PhabricatorStringExportField())
->setKey('result')
->setLabel(pht('Result')),
id(new PhabricatorIntExportField())
->setKey('code')
->setLabel(pht('Code')),
id(new PhabricatorEpochExportField())
->setKey('date')
->setLabel(pht('Date')),
);
+
+ if ($viewer->getIsAdmin()) {
+ $fields[] = id(new PhabricatorStringExportField())
+ ->setKey('remoteAddress')
+ ->setLabel(pht('Remote Address'));
+ }
+
+ return $fields;
}
- public function newExport(array $events) {
+ protected function newExportData(array $events) {
$viewer = $this->requireViewer();
$phids = array();
foreach ($events as $event) {
if ($event->getPullerPHID()) {
$phids[] = $event->getPullerPHID();
}
}
$handles = $viewer->loadHandles($phids);
$export = array();
foreach ($events as $event) {
$repository = $event->getRepository();
if ($repository) {
$repository_phid = $repository->getPHID();
$repository_name = $repository->getDisplayName();
} else {
$repository_phid = null;
$repository_name = null;
}
$puller_phid = $event->getPullerPHID();
if ($puller_phid) {
$puller_name = $handles[$puller_phid]->getName();
} else {
$puller_name = null;
}
- $export[] = array(
- 'id' => $event->getID(),
- 'phid' => $event->getPHID(),
+ $map = array(
'repositoryPHID' => $repository_phid,
'repository' => $repository_name,
'pullerPHID' => $puller_phid,
'puller' => $puller_name,
'protocol' => $event->getRemoteProtocol(),
'result' => $event->getResultType(),
'code' => $event->getResultCode(),
'date' => $event->getEpoch(),
);
+
+ if ($viewer->getIsAdmin()) {
+ $map['remoteAddress'] = $event->getRemoteAddress();
+ }
+
+ $export[] = $map;
}
return $export;
}
protected function getURI($path) {
return '/diffusion/pulllog/'.$path;
}
protected function getBuiltinQueryNames() {
return array(
'all' => pht('All Pull 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 DiffusionPullLogListView())
->setViewer($this->requireViewer())
->setLogs($logs);
return id(new PhabricatorApplicationSearchResultView())
->setTable($table);
}
}
diff --git a/src/applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php
index 91c823871..400340abe 100644
--- a/src/applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php
+++ b/src/applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php
@@ -1,92 +1,135 @@
<?php
final class DiffusionGitReceivePackSSHWorkflow extends DiffusionGitSSHWorkflow {
protected function didConstruct() {
$this->setName('git-receive-pack');
$this->setArguments(
array(
array(
'name' => 'dir',
'wildcard' => true,
),
));
}
protected function executeRepositoryOperations() {
+ $host_wait_start = microtime(true);
+
$repository = $this->getRepository();
$viewer = $this->getSSHUser();
$device = AlmanacKeys::getLiveDevice();
// This is a write, and must have write access.
$this->requireWriteAccess();
$cluster_engine = id(new DiffusionRepositoryClusterEngine())
->setViewer($viewer)
->setRepository($repository)
->setLog($this);
if ($this->shouldProxy()) {
$command = $this->getProxyCommand();
$did_synchronize = false;
if ($device) {
$this->writeClusterEngineLogMessage(
pht(
"# Push received by \"%s\", forwarding to cluster host.\n",
$device->getName()));
}
} else {
$command = csprintf('git-receive-pack %s', $repository->getLocalPath());
$did_synchronize = true;
$cluster_engine->synchronizeWorkingCopyBeforeWrite();
if ($device) {
$this->writeClusterEngineLogMessage(
pht(
"# Ready to receive on cluster host \"%s\".\n",
$device->getName()));
}
}
$caught = null;
try {
$err = $this->executeRepositoryCommand($command);
} catch (Exception $ex) {
$caught = $ex;
}
// We've committed the write (or rejected it), so we can release the lock
// without waiting for the client to receive the acknowledgement.
if ($did_synchronize) {
$cluster_engine->synchronizeWorkingCopyAfterWrite();
}
if ($caught) {
throw $caught;
}
if (!$err) {
$repository->writeStatusMessage(
PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE,
PhabricatorRepositoryStatusMessage::CODE_OKAY);
$this->waitForGitClient();
+
+ $host_wait_end = microtime(true);
+
+ $this->updatePushLogWithTimingInformation(
+ $this->getClusterEngineLogProperty('writeWait'),
+ $this->getClusterEngineLogProperty('readWait'),
+ ($host_wait_end - $host_wait_start));
+
}
return $err;
}
private function executeRepositoryCommand($command) {
$repository = $this->getRepository();
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
$future = id(new ExecFuture('%C', $command))
->setEnv($this->getEnvironment());
return $this->newPassthruCommand()
->setIOChannel($this->getIOChannel())
->setCommandChannelFromExecFuture($future)
->execute();
}
+ private function updatePushLogWithTimingInformation(
+ $write_wait,
+ $read_wait,
+ $host_wait) {
+
+ if ($write_wait !== null) {
+ $write_wait = (int)(1000000 * $write_wait);
+ }
+
+ if ($read_wait !== null) {
+ $read_wait = (int)(1000000 * $read_wait);
+ }
+
+ if ($host_wait !== null) {
+ $host_wait = (int)(1000000 * $host_wait);
+ }
+
+ $identifier = $this->getRequestIdentifier();
+
+ $event = new PhabricatorRepositoryPushEvent();
+ $conn = $event->establishConnection('w');
+
+ queryfx(
+ $conn,
+ 'UPDATE %T SET writeWait = %nd, readWait = %nd, hostWait = %nd
+ WHERE requestIdentifier = %s',
+ $event->getTableName(),
+ $write_wait,
+ $read_wait,
+ $host_wait,
+ $identifier);
+ }
+
}
diff --git a/src/applications/diffusion/ssh/DiffusionGitSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionGitSSHWorkflow.php
index 6de16e723..6bc56d767 100644
--- a/src/applications/diffusion/ssh/DiffusionGitSSHWorkflow.php
+++ b/src/applications/diffusion/ssh/DiffusionGitSSHWorkflow.php
@@ -1,48 +1,58 @@
<?php
abstract class DiffusionGitSSHWorkflow
extends DiffusionSSHWorkflow
implements DiffusionRepositoryClusterEngineLogInterface {
+ private $engineLogProperties = array();
+
protected function writeError($message) {
// Git assumes we'll add our own newlines.
return parent::writeError($message."\n");
}
public function writeClusterEngineLogMessage($message) {
parent::writeError($message);
$this->getErrorChannel()->update();
}
+ public function writeClusterEngineLogProperty($key, $value) {
+ $this->engineLogProperties[$key] = $value;
+ }
+
+ protected function getClusterEngineLogProperty($key, $default = null) {
+ return idx($this->engineLogProperties, $key, $default);
+ }
+
protected function identifyRepository() {
$args = $this->getArgs();
$path = head($args->getArg('dir'));
return $this->loadRepositoryWithPath(
$path,
PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
}
protected function waitForGitClient() {
$io_channel = $this->getIOChannel();
// If we don't wait for the client to close the connection, `git` will
// consider it an early abort and fail. Sit around until Git is comfortable
// that it really received all the data.
while ($io_channel->isOpenForReading()) {
$io_channel->update();
$this->getErrorChannel()->flush();
PhutilChannel::waitForAny(array($io_channel));
}
}
protected function raiseWrongVCSException(
PhabricatorRepository $repository) {
throw new Exception(
pht(
'This repository ("%s") is not a Git repository. Use "%s" to '.
'interact with this repository.',
$repository->getDisplayName(),
$repository->getVersionControlSystem()));
}
}
diff --git a/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php
index 6b219c177..62396f6b0 100644
--- a/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php
+++ b/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php
@@ -1,281 +1,286 @@
<?php
abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow {
private $args;
private $repository;
private $hasWriteAccess;
private $proxyURI;
private $baseRequestPath;
public function getRepository() {
if (!$this->repository) {
throw new Exception(pht('Repository is not available yet!'));
}
return $this->repository;
}
private function setRepository(PhabricatorRepository $repository) {
$this->repository = $repository;
return $this;
}
public function getArgs() {
return $this->args;
}
public function getEnvironment() {
$env = array(
DiffusionCommitHookEngine::ENV_USER => $this->getSSHUser()->getUsername(),
DiffusionCommitHookEngine::ENV_REMOTE_PROTOCOL => 'ssh',
);
+ $identifier = $this->getRequestIdentifier();
+ if ($identifier !== null) {
+ $env[DiffusionCommitHookEngine::ENV_REQUEST] = $identifier;
+ }
+
$remote_address = $this->getSSHRemoteAddress();
if ($remote_address !== null) {
$env[DiffusionCommitHookEngine::ENV_REMOTE_ADDRESS] = $remote_address;
}
return $env;
}
/**
* Identify and load the affected repository.
*/
abstract protected function identifyRepository();
abstract protected function executeRepositoryOperations();
abstract protected function raiseWrongVCSException(
PhabricatorRepository $repository);
protected function getBaseRequestPath() {
return $this->baseRequestPath;
}
protected function writeError($message) {
$this->getErrorChannel()->write($message);
return $this;
}
protected function getCurrentDeviceName() {
$device = AlmanacKeys::getLiveDevice();
if ($device) {
return $device->getName();
}
return php_uname('n');
}
protected function getTargetDeviceName() {
// TODO: This should use the correct device identity.
$uri = new PhutilURI($this->proxyURI);
return $uri->getDomain();
}
protected function shouldProxy() {
return (bool)$this->proxyURI;
}
protected function getProxyCommand() {
$uri = new PhutilURI($this->proxyURI);
$username = AlmanacKeys::getClusterSSHUser();
if ($username === null) {
throw new Exception(
pht(
'Unable to determine the username to connect with when trying '.
'to proxy an SSH request within the Phabricator cluster.'));
}
$port = $uri->getPort();
$host = $uri->getDomain();
$key_path = AlmanacKeys::getKeyPath('device.key');
if (!Filesystem::pathExists($key_path)) {
throw new Exception(
pht(
'Unable to proxy this SSH request within the cluster: this device '.
'is not registered and has a missing device key (expected to '.
'find key at "%s").',
$key_path));
}
$options = array();
$options[] = '-o';
$options[] = 'StrictHostKeyChecking=no';
$options[] = '-o';
$options[] = 'UserKnownHostsFile=/dev/null';
// This is suppressing "added <address> to the list of known hosts"
// messages, which are confusing and irrelevant when they arise from
// proxied requests. It might also be suppressing lots of useful errors,
// of course. Ideally, we would enforce host keys eventually.
$options[] = '-o';
$options[] = 'LogLevel=quiet';
// NOTE: We prefix the command with "@username", which the far end of the
// connection will parse in order to act as the specified user. This
// behavior is only available to cluster requests signed by a trusted
// device key.
return csprintf(
'ssh %Ls -l %s -i %s -p %s %s -- %s %Ls',
$options,
$username,
$key_path,
$port,
$host,
'@'.$this->getSSHUser()->getUsername(),
$this->getOriginalArguments());
}
final public function execute(PhutilArgumentParser $args) {
$this->args = $args;
$viewer = $this->getSSHUser();
$have_diffusion = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorDiffusionApplication',
$viewer);
if (!$have_diffusion) {
throw new Exception(
pht(
'You do not have permission to access the Diffusion application, '.
'so you can not interact with repositories over SSH.'));
}
$repository = $this->identifyRepository();
$this->setRepository($repository);
$is_cluster_request = $this->getIsClusterRequest();
$uri = $repository->getAlmanacServiceURI(
$viewer,
$is_cluster_request,
array(
'ssh',
));
if ($uri) {
$this->proxyURI = $uri;
}
try {
return $this->executeRepositoryOperations();
} catch (Exception $ex) {
$this->writeError(get_class($ex).': '.$ex->getMessage());
return 1;
}
}
protected function loadRepositoryWithPath($path, $vcs) {
$viewer = $this->getSSHUser();
$info = PhabricatorRepository::parseRepositoryServicePath($path, $vcs);
if ($info === null) {
throw new Exception(
pht(
'Unrecognized repository path "%s". Expected a path like "%s", '.
'"%s", or "%s".',
$path,
'/diffusion/X/',
'/diffusion/123/',
'/source/thaumaturgy.git'));
}
$identifier = $info['identifier'];
$base = $info['base'];
$this->baseRequestPath = $base;
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->withIdentifiers(array($identifier))
->needURIs(true)
->executeOne();
if (!$repository) {
throw new Exception(
pht('No repository "%s" exists!', $identifier));
}
$protocol = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_SSH;
if (!$repository->canServeProtocol($protocol, false)) {
throw new Exception(
pht(
'This repository ("%s") is not available over SSH.',
$repository->getDisplayName()));
}
if ($repository->getVersionControlSystem() != $vcs) {
$this->raiseWrongVCSException($repository);
}
return $repository;
}
protected function requireWriteAccess($protocol_command = null) {
if ($this->hasWriteAccess === true) {
return;
}
$repository = $this->getRepository();
$viewer = $this->getSSHUser();
// c4science custo
//if ($viewer->isOmnipotent()) {
// throw new Exception(
// pht(
// 'This request is authenticated as a cluster device, but is '.
// 'performing a write. Writes must be performed with a real '.
// 'user account.'));
//}
$protocol = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_SSH;
if ($repository->canServeProtocol($protocol, true)) {
$can_push = PhabricatorPolicyFilter::hasCapability(
$viewer,
$repository,
DiffusionPushCapability::CAPABILITY);
if (!$can_push) {
throw new Exception(
pht('You do not have permission to push to this repository.'));
}
} else {
if ($protocol_command !== null) {
throw new Exception(
pht(
'This repository is read-only over SSH (tried to execute '.
'protocol command "%s").',
$protocol_command));
} else {
throw new Exception(
pht('This repository is read-only over SSH.'));
}
}
$this->hasWriteAccess = true;
return $this->hasWriteAccess;
}
protected function shouldSkipReadSynchronization() {
$viewer = $this->getSSHUser();
// Currently, the only case where devices interact over SSH without
// assuming user credentials is when synchronizing before a read. These
// synchronizing reads do not themselves need to be synchronized.
if ($viewer->isOmnipotent()) {
return true;
}
return false;
}
protected function newPullEvent() {
$viewer = $this->getSSHUser();
$repository = $this->getRepository();
$remote_address = $this->getSSHRemoteAddress();
return id(new PhabricatorRepositoryPullEvent())
->setEpoch(PhabricatorTime::getNow())
->setRemoteAddress($remote_address)
->setRemoteProtocol(PhabricatorRepositoryPullEvent::PROTOCOL_SSH)
->setPullerPHID($viewer->getPHID())
->setRepositoryPHID($repository->getPHID());
}
}
diff --git a/src/applications/diffusion/view/DiffusionPullLogListView.php b/src/applications/diffusion/view/DiffusionPullLogListView.php
index f2e3280eb..8df35e292 100644
--- a/src/applications/diffusion/view/DiffusionPullLogListView.php
+++ b/src/applications/diffusion/view/DiffusionPullLogListView.php
@@ -1,115 +1,105 @@
<?php
final class DiffusionPullLogListView extends AphrontView {
private $logs;
public function setLogs(array $logs) {
assert_instances_of($logs, 'PhabricatorRepositoryPullEvent');
$this->logs = $logs;
return $this;
}
public function render() {
$events = $this->logs;
$viewer = $this->getViewer();
$handle_phids = array();
foreach ($events as $event) {
if ($event->getPullerPHID()) {
$handle_phids[] = $event->getPullerPHID();
}
}
$handles = $viewer->loadHandles($handle_phids);
- // Figure out which repositories are editable. We only let you see remote
- // IPs if you have edit capability on a repository.
- $editable_repos = array();
- if ($events) {
- $editable_repos = id(new PhabricatorRepositoryQuery())
- ->setViewer($viewer)
- ->requireCapabilities(
- array(
- PhabricatorPolicyCapability::CAN_VIEW,
- PhabricatorPolicyCapability::CAN_EDIT,
- ))
- ->withPHIDs(mpull($events, 'getRepositoryPHID'))
- ->execute();
- $editable_repos = mpull($editable_repos, null, 'getPHID');
- }
+ // Only administrators can view remote addresses.
+ $remotes_visible = $viewer->getIsAdmin();
$rows = array();
- $any_host = false;
foreach ($events as $event) {
if ($event->getRepositoryPHID()) {
$repository = $event->getRepository();
} else {
$repository = null;
}
- // Reveal this if it's valid and the user can edit the repository. For
- // invalid requests you currently have to go fishing in the database.
- $remote_address = '-';
- if ($repository) {
- if (isset($editable_repos[$event->getRepositoryPHID()])) {
- $remote_address = $event->getRemoteAddress();
- }
+ if ($remotes_visible) {
+ $remote_address = $event->getRemoteAddress();
+ } else {
+ $remote_address = null;
}
$event_id = $event->getID();
$repository_link = null;
if ($repository) {
$repository_link = phutil_tag(
'a',
array(
'href' => $repository->getURI(),
),
$repository->getDisplayName());
}
$puller_link = null;
if ($event->getPullerPHID()) {
$puller_link = $viewer->renderHandle($event->getPullerPHID());
}
$rows[] = array(
$event_id,
$repository_link,
$puller_link,
$remote_address,
$event->getRemoteProtocolDisplayName(),
$event->newResultIcon(),
$event->getResultCode(),
phabricator_datetime($event->getEpoch(), $viewer),
);
}
$table = id(new AphrontTableView($rows))
->setHeaders(
array(
pht('Pull'),
pht('Repository'),
pht('Puller'),
pht('From'),
pht('Via'),
null,
pht('Code'),
pht('Date'),
))
->setColumnClasses(
array(
'n',
'',
'',
'n',
'wide',
'',
'n',
'right',
+ ))
+ ->setColumnVisibility(
+ array(
+ true,
+ true,
+ true,
+ $remotes_visible,
));
return $table;
}
}
diff --git a/src/applications/diffusion/view/DiffusionPushLogListView.php b/src/applications/diffusion/view/DiffusionPushLogListView.php
index 303f0519f..e10162575 100644
--- a/src/applications/diffusion/view/DiffusionPushLogListView.php
+++ b/src/applications/diffusion/view/DiffusionPushLogListView.php
@@ -1,153 +1,160 @@
<?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();
$handle_phids = array();
foreach ($logs as $log) {
$handle_phids[] = $log->getPusherPHID();
$device_phid = $log->getDevicePHID();
if ($device_phid) {
$handle_phids[] = $device_phid;
}
}
$handles = $viewer->loadHandles($handle_phids);
- // Figure out which repositories are editable. We only let you see remote
- // IPs if you have edit capability on a repository.
- $editable_repos = array();
- if ($logs) {
- $editable_repos = id(new PhabricatorRepositoryQuery())
- ->setViewer($viewer)
- ->requireCapabilities(
- array(
- PhabricatorPolicyCapability::CAN_VIEW,
- PhabricatorPolicyCapability::CAN_EDIT,
- ))
- ->withPHIDs(mpull($logs, 'getRepositoryPHID'))
- ->execute();
- $editable_repos = mpull($editable_repos, null, 'getPHID');
- }
+ // 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();
- // Reveal this if it's valid and the user can edit the repository.
- $remote_address = '-';
- if (isset($editable_repos[$log->getRepositoryPHID()])) {
+ 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));
+
$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()),
-
- // TODO: Make these human-readable.
- $log->getChangeFlags(),
- $log->getPushEvent()->getRejectCode(),
+ $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('Code'),
+ pht('Result'),
pht('Date'),
))
->setColumnClasses(
array(
'',
'',
'',
'',
'',
'',
'',
'wide',
'n',
'n',
+ '',
+ '',
'right',
))
->setColumnVisibility(
array(
true,
true,
true,
- true,
+ $remotes_visible,
true,
$any_host,
));
return $table;
}
}
diff --git a/src/applications/diffusion/view/DiffusionView.php b/src/applications/diffusion/view/DiffusionView.php
index eb43e49f3..eb7f3eb72 100644
--- a/src/applications/diffusion/view/DiffusionView.php
+++ b/src/applications/diffusion/view/DiffusionView.php
@@ -1,266 +1,265 @@
<?php
abstract class DiffusionView extends AphrontView {
private $diffusionRequest;
final public function setDiffusionRequest(DiffusionRequest $request) {
$this->diffusionRequest = $request;
return $this;
}
final public function getDiffusionRequest() {
return $this->diffusionRequest;
}
final public function linkHistory($path) {
$href = $this->getDiffusionRequest()->generateURI(
array(
'action' => 'history',
'path' => $path,
));
return $this->renderHistoryLink($href);
}
final public function linkBranchHistory($branch) {
$href = $this->getDiffusionRequest()->generateURI(
array(
'action' => 'history',
'branch' => $branch,
));
return $this->renderHistoryLink($href);
}
final public function linkTagHistory($tag) {
$href = $this->getDiffusionRequest()->generateURI(
array(
'action' => 'history',
'commit' => $tag,
));
return $this->renderHistoryLink($href);
}
private function renderHistoryLink($href) {
return javelin_tag(
'a',
array(
'href' => $href,
'class' => 'diffusion-link-icon',
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => pht('History'),
'align' => 'E',
),
),
id(new PHUIIconView())->setIcon('fa-history bluegrey'));
}
final public function linkBrowse(
$path,
array $details = array(),
$button = false) {
require_celerity_resource('diffusion-icons-css');
Javelin::initBehavior('phabricator-tooltips');
$file_type = idx($details, 'type');
unset($details['type']);
$display_name = idx($details, 'name');
unset($details['name']);
if (strlen($display_name)) {
$display_name = phutil_tag(
'span',
array(
'class' => 'diffusion-browse-name',
),
$display_name);
}
if (isset($details['external'])) {
$href = id(new PhutilURI('/diffusion/external/'))
->setQueryParams(
array(
'uri' => idx($details, 'external'),
'id' => idx($details, 'hash'),
));
$tip = pht('Browse External');
} else {
$href = $this->getDiffusionRequest()->generateURI(
$details + array(
'action' => 'browse',
'path' => $path,
));
$tip = pht('Browse');
}
$icon = DifferentialChangeType::getIconForFileType($file_type);
$color = DifferentialChangeType::getIconColorForFileType($file_type);
$icon_view = id(new PHUIIconView())
->setIcon($icon.' '.$color);
// If we're rendering a file or directory name, don't show the tooltip.
if ($display_name !== null) {
$sigil = null;
$meta = null;
} else {
$sigil = 'has-tooltip';
$meta = array(
'tip' => $tip,
'align' => 'E',
);
}
if ($button) {
return id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-code')
->setHref($href)
->setToolTip(pht('Browse'))
->setButtonType(PHUIButtonView::BUTTONTYPE_SIMPLE);
}
return javelin_tag(
'a',
array(
'href' => $href,
'class' => 'diffusion-link-icon',
'sigil' => $sigil,
'meta' => $meta,
),
array(
$icon_view,
$display_name,
));
}
final public static function linkCommit(
PhabricatorRepository $repository,
$commit,
$summary = '') {
$commit_name = $repository->formatCommitName($commit, $local = true);
if (strlen($summary)) {
$commit_name .= ': '.$summary;
}
return phutil_tag(
'a',
array(
'href' => $repository->getCommitURI($commit),
),
$commit_name);
}
final public static function linkDetail(
PhabricatorRepository $repository,
$commit,
$detail) {
return phutil_tag(
'a',
array(
'href' => $repository->getCommitURI($commit),
),
$detail);
}
final public static function linkRevision($id) {
if (!$id) {
return null;
}
return phutil_tag(
'a',
array(
'href' => "/D{$id}",
),
"D{$id}");
}
final public static function renderName($name) {
$email = new PhutilEmailAddress($name);
if ($email->getDisplayName() && $email->getDomainName()) {
Javelin::initBehavior('phabricator-tooltips', array());
require_celerity_resource('aphront-tooltip-css');
return javelin_tag(
'span',
array(
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => $email->getAddress(),
'align' => 'S',
'size' => 'auto',
),
),
$email->getDisplayName());
}
return hsprintf('%s', $name);
}
final protected function renderBuildable(
HarbormasterBuildable $buildable,
$type = null) {
- $status = $buildable->getBuildableStatus();
Javelin::initBehavior('phabricator-tooltips');
- $icon = HarbormasterBuildable::getBuildableStatusIcon($status);
- $color = HarbormasterBuildable::getBuildableStatusColor($status);
- $name = HarbormasterBuildable::getBuildableStatusName($status);
+ $icon = $buildable->getStatusIcon();
+ $color = $buildable->getStatusColor();
+ $name = $buildable->getStatusDisplayName();
if ($type == 'button') {
return id(new PHUIButtonView())
->setTag('a')
->setText($name)
->setIcon($icon)
->setColor($color)
->setHref('/'.$buildable->getMonogram())
->addClass('mmr')
->setButtonType(PHUIButtonView::BUTTONTYPE_SIMPLE)
->addClass('diffusion-list-build-status');
}
return id(new PHUIIconView())
->setIcon($icon.' '.$color)
->addSigil('has-tooltip')
->setHref('/'.$buildable->getMonogram())
->setMetadata(
array(
'tip' => $name,
));
}
final protected function loadBuildables(array $commits) {
assert_instances_of($commits, 'PhabricatorRepositoryCommit');
if (!$commits) {
return array();
}
$viewer = $this->getUser();
$harbormaster_app = 'PhabricatorHarbormasterApplication';
$have_harbormaster = PhabricatorApplication::isClassInstalledForViewer(
$harbormaster_app,
$viewer);
if ($have_harbormaster) {
$buildables = id(new HarbormasterBuildableQuery())
->setViewer($viewer)
->withBuildablePHIDs(mpull($commits, 'getPHID'))
->withManualBuildables(false)
->execute();
$buildables = mpull($buildables, null, 'getBuildablePHID');
} else {
$buildables = array();
}
return $buildables;
}
}
diff --git a/src/applications/diffusion/xaction/DiffusionCommitBuildableTransaction.php b/src/applications/diffusion/xaction/DiffusionCommitBuildableTransaction.php
new file mode 100644
index 000000000..91b69fa39
--- /dev/null
+++ b/src/applications/diffusion/xaction/DiffusionCommitBuildableTransaction.php
@@ -0,0 +1,89 @@
+<?php
+
+final class DiffusionCommitBuildableTransaction
+ extends DiffusionCommitTransactionType {
+
+ // NOTE: This uses an older constant for compatibility. We should perhaps
+ // migrate these at some point.
+ const TRANSACTIONTYPE = 'harbormaster:buildable';
+
+ public function generateNewValue($object, $value) {
+ return $value;
+ }
+
+ public function generateOldValue($object) {
+ return null;
+ }
+
+ public function getIcon() {
+ return $this->newBuildableStatus()->getIcon();
+ }
+
+ public function getColor() {
+ return $this->newBuildableStatus()->getColor();
+ }
+
+ public function getActionName() {
+ return $this->newBuildableStatus()->getActionName();
+ }
+
+ public function shouldHideForFeed() {
+ return !$this->newBuildableStatus()->isFailed();
+ }
+
+ public function shouldHideForMail() {
+ return !$this->newBuildableStatus()->isFailed();
+ }
+
+ public function getTitle() {
+ $new = $this->getNewValue();
+ $buildable_phid = $this->getBuildablePHID();
+
+ switch ($new) {
+ case HarbormasterBuildableStatus::STATUS_PASSED:
+ return pht(
+ '%s completed building %s.',
+ $this->renderAuthor(),
+ $this->renderHandle($buildable_phid));
+ case HarbormasterBuildableStatus::STATUS_FAILED:
+ return pht(
+ '%s failed to build %s!',
+ $this->renderAuthor(),
+ $this->renderHandle($buildable_phid));
+ }
+
+ return null;
+ }
+
+ public function getTitleForFeed() {
+ $new = $this->getNewValue();
+ $buildable_phid = $this->getBuildablePHID();
+
+ switch ($new) {
+ case HarbormasterBuildableStatus::STATUS_PASSED:
+ return pht(
+ '%s completed building %s for %s.',
+ $this->renderAuthor(),
+ $this->renderHandle($buildable_phid),
+ $this->renderObject());
+ case HarbormasterBuildableStatus::STATUS_FAILED:
+ return pht(
+ '%s failed to build %s for %s!',
+ $this->renderAuthor(),
+ $this->renderHandle($buildable_phid),
+ $this->renderObject());
+ }
+
+ return null;
+ }
+
+ private function newBuildableStatus() {
+ $new = $this->getNewValue();
+ return HarbormasterBuildableStatus::newBuildableStatusObject($new);
+ }
+
+ private function getBuildablePHID() {
+ return $this->getMetadataValue('harbormaster:buildablePHID');
+ }
+
+}
diff --git a/src/applications/doorkeeper/bridge/DoorkeeperBridgeJIRA.php b/src/applications/doorkeeper/bridge/DoorkeeperBridgeJIRA.php
index 199f049dc..920f2eeb9 100644
--- a/src/applications/doorkeeper/bridge/DoorkeeperBridgeJIRA.php
+++ b/src/applications/doorkeeper/bridge/DoorkeeperBridgeJIRA.php
@@ -1,145 +1,146 @@
<?php
final class DoorkeeperBridgeJIRA extends DoorkeeperBridge {
const APPTYPE_JIRA = 'jira';
const OBJTYPE_ISSUE = 'jira:issue';
public function canPullRef(DoorkeeperObjectRef $ref) {
if ($ref->getApplicationType() != self::APPTYPE_JIRA) {
return false;
}
$types = array(
self::OBJTYPE_ISSUE => true,
);
return isset($types[$ref->getObjectType()]);
}
public function pullRefs(array $refs) {
$id_map = mpull($refs, 'getObjectID', 'getObjectKey');
$viewer = $this->getViewer();
$provider = PhabricatorJIRAAuthProvider::getJIRAProvider();
if (!$provider) {
return;
}
$accounts = id(new PhabricatorExternalAccountQuery())
->setViewer($viewer)
->withUserPHIDs(array($viewer->getPHID()))
->withAccountTypes(array($provider->getProviderType()))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->execute();
if (!$accounts) {
return $this->didFailOnMissingLink();
}
// TODO: When we support multiple JIRA instances, we need to disambiguate
// issues (perhaps with additional configuration) or cast a wide net
// (by querying all instances). For now, just query the one instance.
$account = head($accounts);
$futures = array();
foreach ($id_map as $key => $id) {
$futures[$key] = $provider->newJIRAFuture(
$account,
'rest/api/2/issue/'.phutil_escape_uri($id),
'GET');
}
$results = array();
$failed = array();
foreach (new FutureIterator($futures) as $key => $future) {
try {
$results[$key] = $future->resolveJSON();
} catch (Exception $ex) {
if (($ex instanceof HTTPFutureResponseStatus) &&
($ex->getStatusCode() == 404)) {
// This indicates that the object has been deleted (or never existed,
// or isn't visible to the current user) but it's a successful sync of
// an object which isn't visible.
} else {
// This is something else, so consider it a synchronization failure.
phlog($ex);
$failed[$key] = $ex;
}
}
}
foreach ($refs as $ref) {
$ref->setAttribute('name', pht('JIRA %s', $ref->getObjectID()));
$did_fail = idx($failed, $ref->getObjectKey());
if ($did_fail) {
$ref->setSyncFailed(true);
continue;
}
$result = idx($results, $ref->getObjectKey());
if (!$result) {
continue;
}
$fields = idx($result, 'fields', array());
$ref->setIsVisible(true);
$ref->setAttribute(
'fullname',
pht('JIRA %s %s', $result['key'], idx($fields, 'summary')));
$ref->setAttribute('title', idx($fields, 'summary'));
$ref->setAttribute('description', idx($result, 'description'));
+ $ref->setAttribute('shortname', $result['key']);
$obj = $ref->getExternalObject();
if ($obj->getID()) {
continue;
}
$this->fillObjectFromData($obj, $result);
$this->saveExternalObject($ref, $obj);
}
}
public function fillObjectFromData(DoorkeeperExternalObject $obj, $result) {
// Convert the "self" URI, which points at the REST endpoint, into a
// browse URI.
$self = idx($result, 'self');
$object_id = $obj->getObjectID();
$uri = self::getJIRAIssueBrowseURIFromJIRARestURI($self, $object_id);
if ($uri !== null) {
$obj->setObjectURI($uri);
}
}
public static function getJIRAIssueBrowseURIFromJIRARestURI(
$uri,
$object_id) {
$uri = new PhutilURI($uri);
// The JIRA install might not be at the domain root, so we may need to
// keep an initial part of the path, like "/jira/". Find the API specific
// part of the URI, strip it off, then replace it with the web version.
$path = $uri->getPath();
$pos = strrpos($path, 'rest/api/2/issue/');
if ($pos === false) {
return null;
}
$path = substr($path, 0, $pos);
$path = $path.'browse/'.$object_id;
$uri->setPath($path);
return (string)$uri;
}
}
diff --git a/src/applications/doorkeeper/controller/DoorkeeperTagsController.php b/src/applications/doorkeeper/controller/DoorkeeperTagsController.php
index 9957b8202..5a886cba3 100644
--- a/src/applications/doorkeeper/controller/DoorkeeperTagsController.php
+++ b/src/applications/doorkeeper/controller/DoorkeeperTagsController.php
@@ -1,69 +1,76 @@
<?php
final class DoorkeeperTagsController extends PhabricatorController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$tags = $request->getStr('tags');
try {
$tags = phutil_json_decode($tags);
} catch (PhutilJSONParserException $ex) {
$tags = array();
}
$refs = array();
- $id_map = array();
- foreach ($tags as $tag_spec) {
+ foreach ($tags as $key => $tag_spec) {
$tag = $tag_spec['ref'];
$ref = id(new DoorkeeperObjectRef())
->setApplicationType($tag[0])
->setApplicationDomain($tag[1])
->setObjectType($tag[2])
->setObjectID($tag[3]);
-
- $key = $ref->getObjectKey();
- $id_map[$key] = $tag_spec['id'];
$refs[$key] = $ref;
}
$refs = id(new DoorkeeperImportEngine())
->setViewer($viewer)
->setRefs($refs)
->execute();
$results = array();
foreach ($refs as $key => $ref) {
if (!$ref->getIsVisible()) {
continue;
}
$uri = $ref->getExternalObject()->getObjectURI();
if (!$uri) {
continue;
}
- $id = $id_map[$key];
+ $tag_spec = $tags[$key];
+
+ $id = $tag_spec['id'];
+ $view = idx($tag_spec, 'view');
+
+ $is_short = ($view == 'short');
+
+ if ($is_short) {
+ $name = $ref->getShortName();
+ } else {
+ $name = $ref->getFullName();
+ }
$tag = id(new PHUITagView())
->setID($id)
- ->setName($ref->getFullName())
+ ->setName($name)
->setHref($uri)
->setType(PHUITagView::TYPE_OBJECT)
->setExternal(true)
->render();
$results[] = array(
- 'id' => $id,
- 'markup' => $tag,
+ 'id' => $id,
+ 'markup' => $tag,
);
}
return id(new AphrontAjaxResponse())->setContent(
array(
'tags' => $results,
));
}
}
diff --git a/src/applications/doorkeeper/engine/DoorkeeperObjectRef.php b/src/applications/doorkeeper/engine/DoorkeeperObjectRef.php
index 3cf2a8dbe..bbccc7a5d 100644
--- a/src/applications/doorkeeper/engine/DoorkeeperObjectRef.php
+++ b/src/applications/doorkeeper/engine/DoorkeeperObjectRef.php
@@ -1,125 +1,132 @@
<?php
final class DoorkeeperObjectRef extends Phobject {
private $objectKey;
private $applicationType;
private $applicationDomain;
private $objectType;
private $objectID;
private $attributes = array();
private $isVisible;
private $syncFailed;
private $externalObject;
public function newExternalObject() {
return id(new DoorkeeperExternalObject())
->setApplicationType($this->getApplicationType())
->setApplicationDomain($this->getApplicationDomain())
->setObjectType($this->getObjectType())
->setObjectID($this->getObjectID())
->setViewPolicy(PhabricatorPolicies::POLICY_USER);
}
public function attachExternalObject(
DoorkeeperExternalObject $external_object) {
$this->externalObject = $external_object;
return $this;
}
public function getExternalObject() {
if (!$this->externalObject) {
throw new PhutilInvalidStateException('attachExternalObject');
}
return $this->externalObject;
}
public function setIsVisible($is_visible) {
$this->isVisible = $is_visible;
return $this;
}
public function getIsVisible() {
return $this->isVisible;
}
public function setSyncFailed($sync_failed) {
$this->syncFailed = $sync_failed;
return $this;
}
public function getSyncFailed() {
return $this->syncFailed;
}
public function getAttribute($key, $default = null) {
return idx($this->attributes, $key, $default);
}
public function setAttribute($key, $value) {
$this->attributes[$key] = $value;
return $this;
}
public function setObjectID($object_id) {
$this->objectID = $object_id;
return $this;
}
public function getObjectID() {
return $this->objectID;
}
public function setObjectType($object_type) {
$this->objectType = $object_type;
return $this;
}
public function getObjectType() {
return $this->objectType;
}
public function setApplicationDomain($application_domain) {
$this->applicationDomain = $application_domain;
return $this;
}
public function getApplicationDomain() {
return $this->applicationDomain;
}
public function setApplicationType($application_type) {
$this->applicationType = $application_type;
return $this;
}
public function getApplicationType() {
return $this->applicationType;
}
public function getFullName() {
return coalesce(
$this->getAttribute('fullname'),
$this->getAttribute('name'),
pht('External Object'));
}
+ public function getShortName() {
+ return coalesce(
+ $this->getAttribute('shortname'),
+ $this->getAttribute('name'),
+ pht('External Object'));
+ }
+
public function getObjectKey() {
if (!$this->objectKey) {
$this->objectKey = PhabricatorHash::digestForIndex(
implode(
':',
array(
$this->getApplicationType(),
$this->getApplicationDomain(),
$this->getObjectType(),
$this->getObjectID(),
)));
}
return $this->objectKey;
}
}
diff --git a/src/applications/doorkeeper/remarkup/DoorkeeperRemarkupRule.php b/src/applications/doorkeeper/remarkup/DoorkeeperRemarkupRule.php
index b24d53705..9654d12bb 100644
--- a/src/applications/doorkeeper/remarkup/DoorkeeperRemarkupRule.php
+++ b/src/applications/doorkeeper/remarkup/DoorkeeperRemarkupRule.php
@@ -1,64 +1,103 @@
<?php
abstract class DoorkeeperRemarkupRule extends PhutilRemarkupRule {
const KEY_TAGS = 'doorkeeper.tags';
+ const VIEW_FULL = 'full';
+ const VIEW_SHORT = 'short';
+
public function getPriority() {
return 350.0;
}
protected function addDoorkeeperTag(array $spec) {
+ PhutilTypeSpec::checkMap(
+ $spec,
+ array(
+ 'href' => 'string',
+ 'tag' => 'map<string, wild>',
+
+ 'name' => 'optional string',
+ 'view' => 'optional string',
+ ));
+
+ $spec = $spec + array(
+ 'view' => self::VIEW_FULL,
+ );
+
+ $views = array(
+ self::VIEW_FULL,
+ self::VIEW_SHORT,
+ );
+ $views = array_fuse($views);
+ if (!isset($views[$spec['view']])) {
+ throw new Exception(
+ pht(
+ 'Unsupported Doorkeeper tag view mode "%s". Supported modes are: %s.',
+ $spec['view'],
+ implode(', ', $views)));
+ }
+
$key = self::KEY_TAGS;
$engine = $this->getEngine();
$token = $engine->storeText(get_class($this));
$tags = $engine->getTextMetadata($key, array());
$tags[] = array(
'token' => $token,
) + $spec + array(
'extra' => array(),
);
$engine->setTextMetadata($key, $tags);
return $token;
}
public function didMarkupText() {
$key = self::KEY_TAGS;
$engine = $this->getEngine();
$tags = $engine->getTextMetadata($key, array());
if (!$tags) {
return;
}
$refs = array();
foreach ($tags as $spec) {
- $tag_id = celerity_generate_unique_node_id();
+ $href = $spec['href'];
+ $name = idx($spec, 'name', $href);
- $refs[] = array(
- 'id' => $tag_id,
- ) + $spec['tag'];
+ $this->assertFlatText($href);
+ $this->assertFlatText($name);
if ($this->getEngine()->isTextMode()) {
- $view = $spec['href'];
+ $view = "{$name} <{$href}>";
} else {
+ $tag_id = celerity_generate_unique_node_id();
+
+ $refs[] = array(
+ 'id' => $tag_id,
+ 'view' => $spec['view'],
+ ) + $spec['tag'];
+
$view = id(new PHUITagView())
->setID($tag_id)
- ->setName($this->assertFlatText($spec['href']))
- ->setHref($this->assertFlatText($spec['href']))
+ ->setName($name)
+ ->setHref($href)
->setType(PHUITagView::TYPE_OBJECT)
->setExternal(true);
}
$engine->overwriteStoredText($spec['token'], $view);
}
- Javelin::initBehavior('doorkeeper-tag', array('tags' => $refs));
+ if ($refs) {
+ Javelin::initBehavior('doorkeeper-tag', array('tags' => $refs));
+ }
$engine->setTextMetadata($key, array());
}
}
diff --git a/src/applications/draft/storage/PhabricatorVersionedDraft.php b/src/applications/draft/storage/PhabricatorVersionedDraft.php
index 0daf13e8c..58cdb6759 100644
--- a/src/applications/draft/storage/PhabricatorVersionedDraft.php
+++ b/src/applications/draft/storage/PhabricatorVersionedDraft.php
@@ -1,99 +1,96 @@
<?php
final class PhabricatorVersionedDraft extends PhabricatorDraftDAO {
const KEY_VERSION = 'draft.version';
protected $objectPHID;
protected $authorPHID;
protected $version;
protected $properties = array();
protected function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'version' => 'uint32',
),
self::CONFIG_KEY_SCHEMA => array(
'key_object' => array(
'columns' => array('objectPHID', 'authorPHID', 'version'),
'unique' => true,
),
),
) + 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 static function loadDraft(
$object_phid,
$viewer_phid) {
return id(new PhabricatorVersionedDraft())->loadOneWhere(
'objectPHID = %s AND authorPHID = %s ORDER BY version DESC LIMIT 1',
$object_phid,
$viewer_phid);
}
public static function loadOrCreateDraft(
$object_phid,
$viewer_phid,
$version) {
$draft = self::loadDraft($object_phid, $viewer_phid);
if ($draft) {
return $draft;
}
try {
return id(new self())
->setObjectPHID($object_phid)
->setAuthorPHID($viewer_phid)
->setVersion((int)$version)
->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
$duplicate_exception = $ex;
}
// In rare cases we can race ourselves, and at one point there was a bug
// which caused the browser to submit two preview requests at exactly
// the same time. If the insert failed with a duplicate key exception,
// try to load the colliding row to recover from it.
$draft = self::loadDraft($object_phid, $viewer_phid);
if ($draft) {
return $draft;
}
throw $duplicate_exception;
}
public static function purgeDrafts(
$object_phid,
- $viewer_phid,
- $version) {
+ $viewer_phid) {
$draft = new PhabricatorVersionedDraft();
$conn_w = $draft->establishConnection('w');
queryfx(
$conn_w,
- 'DELETE FROM %T WHERE objectPHID = %s AND authorPHID = %s
- AND version <= %d',
+ 'DELETE FROM %T WHERE objectPHID = %s AND authorPHID = %s',
$draft->getTableName(),
$object_phid,
- $viewer_phid,
- $version);
+ $viewer_phid);
}
}
diff --git a/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php
index a050a859e..5acf63bf6 100644
--- a/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php
+++ b/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php
@@ -1,489 +1,494 @@
<?php
/**
* @task lease Lease Acquisition
* @task resource Resource Allocation
* @task interface Resource Interfaces
* @task log Logging
*/
abstract class DrydockBlueprintImplementation extends Phobject {
abstract public function getType();
abstract public function isEnabled();
abstract public function getBlueprintName();
abstract public function getDescription();
public function getBlueprintIcon() {
return 'fa-map-o';
}
public function getFieldSpecifications() {
$fields = array();
$fields += $this->getCustomFieldSpecifications();
if ($this->shouldUseConcurrentResourceLimit()) {
$fields += array(
'allocator.limit' => array(
'name' => pht('Limit'),
'caption' => pht(
'Maximum number of resources this blueprint can have active '.
'concurrently.'),
'type' => 'int',
),
);
}
return $fields;
}
protected function getCustomFieldSpecifications() {
return array();
}
public function getViewer() {
return PhabricatorUser::getOmnipotentUser();
}
/* -( Lease Acquisition )-------------------------------------------------- */
/**
* Enforce basic checks on lease/resource compatibility. Allows resources to
* reject leases if they are incompatible, even if the resource types match.
*
* For example, if a resource represents a 32-bit host, this method might
* reject leases that need a 64-bit host. The blueprint might also reject
* a resource if the lease needs 8GB of RAM and the resource only has 6GB
* free.
*
* This method should not acquire locks or expect anything to be locked. This
* is a coarse compatibility check between a lease and a resource.
*
* @param DrydockBlueprint Concrete blueprint to allocate for.
* @param DrydockResource Candidate resource to allocate the lease on.
* @param DrydockLease Pending lease that wants to allocate here.
* @return bool True if the resource and lease are compatible.
* @task lease
*/
abstract public function canAcquireLeaseOnResource(
DrydockBlueprint $blueprint,
DrydockResource $resource,
DrydockLease $lease);
/**
* Acquire a lease. Allows resources to perform setup as leases are brought
* online.
*
* If acquisition fails, throw an exception.
*
* @param DrydockBlueprint Blueprint which built the resource.
* @param DrydockResource Resource to acquire a lease on.
* @param DrydockLease Requested lease.
* @return void
* @task lease
*/
abstract public function acquireLease(
DrydockBlueprint $blueprint,
DrydockResource $resource,
DrydockLease $lease);
/**
* @return void
* @task lease
*/
public function activateLease(
DrydockBlueprint $blueprint,
DrydockResource $resource,
DrydockLease $lease) {
throw new PhutilMethodNotImplementedException();
}
/**
* React to a lease being released.
*
* This callback is primarily useful for automatically releasing resources
* once all leases are released.
*
* @param DrydockBlueprint Blueprint which built the resource.
* @param DrydockResource Resource a lease was released on.
* @param DrydockLease Recently released lease.
* @return void
* @task lease
*/
abstract public function didReleaseLease(
DrydockBlueprint $blueprint,
DrydockResource $resource,
DrydockLease $lease);
/**
* Destroy any temporary data associated with a lease.
*
* If a lease creates temporary state while held, destroy it here.
*
* @param DrydockBlueprint Blueprint which built the resource.
* @param DrydockResource Resource the lease is acquired on.
* @param DrydockLease The lease being destroyed.
* @return void
* @task lease
*/
abstract public function destroyLease(
DrydockBlueprint $blueprint,
DrydockResource $resource,
DrydockLease $lease);
/* -( Resource Allocation )------------------------------------------------ */
/**
* Enforce fundamental implementation/lease checks. Allows implementations to
* reject a lease which no concrete blueprint can ever satisfy.
*
* For example, if a lease only builds ARM hosts and the lease needs a
* PowerPC host, it may be rejected here.
*
* This is the earliest rejection phase, and followed by
* @{method:canEverAllocateResourceForLease}.
*
* This method should not actually check if a resource can be allocated
* right now, or even if a blueprint which can allocate a suitable resource
* really exists, only if some blueprint may conceivably exist which could
* plausibly be able to build a suitable resource.
*
* @param DrydockLease Requested lease.
* @return bool True if some concrete blueprint of this implementation's
* type might ever be able to build a resource for the lease.
* @task resource
*/
abstract public function canAnyBlueprintEverAllocateResourceForLease(
DrydockLease $lease);
/**
* Enforce basic blueprint/lease checks. Allows blueprints to reject a lease
* which they can not build a resource for.
*
* This is the second rejection phase. It follows
* @{method:canAnyBlueprintEverAllocateResourceForLease} and is followed by
* @{method:canAllocateResourceForLease}.
*
* This method should not check if a resource can be built right now, only
* if the blueprint as configured may, at some time, be able to build a
* suitable resource.
*
* @param DrydockBlueprint Blueprint which may be asked to allocate a
* resource.
* @param DrydockLease Requested lease.
* @return bool True if this blueprint can eventually build a suitable
* resource for the lease, as currently configured.
* @task resource
*/
abstract public function canEverAllocateResourceForLease(
DrydockBlueprint $blueprint,
DrydockLease $lease);
/**
* Enforce basic availability limits. Allows blueprints to reject resource
* allocation if they are currently overallocated.
*
* This method should perform basic capacity/limit checks. For example, if
* it has a limit of 6 resources and currently has 6 resources allocated,
* it might reject new leases.
*
* This method should not acquire locks or expect locks to be acquired. This
* is a coarse check to determine if the operation is likely to succeed
* right now without needing to acquire locks.
*
* It is expected that this method will sometimes return `true` (indicating
* that a resource can be allocated) but find that another allocator has
* eaten up free capacity by the time it actually tries to build a resource.
* This is normal and the allocator will recover from it.
*
* @param DrydockBlueprint The blueprint which may be asked to allocate a
* resource.
* @param DrydockLease Requested lease.
* @return bool True if this blueprint appears likely to be able to allocate
* a suitable resource.
* @task resource
*/
abstract public function canAllocateResourceForLease(
DrydockBlueprint $blueprint,
DrydockLease $lease);
/**
* Allocate a suitable resource for a lease.
*
* This method MUST acquire, hold, and manage locks to prevent multiple
* allocations from racing. World state is not locked before this method is
* called. Blueprints are entirely responsible for any lock handling they
* need to perform.
*
* @param DrydockBlueprint The blueprint which should allocate a resource.
* @param DrydockLease Requested lease.
* @return DrydockResource Allocated resource.
* @task resource
*/
abstract public function allocateResource(
DrydockBlueprint $blueprint,
DrydockLease $lease);
/**
* @task resource
*/
public function activateResource(
DrydockBlueprint $blueprint,
DrydockResource $resource) {
throw new PhutilMethodNotImplementedException();
}
/**
* Destroy any temporary data associated with a resource.
*
* If a resource creates temporary state when allocated, destroy that state
* here. For example, you might shut down a virtual host or destroy a working
* copy on disk.
*
* @param DrydockBlueprint Blueprint which built the resource.
* @param DrydockResource Resource being destroyed.
* @return void
* @task resource
*/
abstract public function destroyResource(
DrydockBlueprint $blueprint,
DrydockResource $resource);
/**
* Get a human readable name for a resource.
*
* @param DrydockBlueprint Blueprint which built the resource.
* @param DrydockResource Resource to get the name of.
* @return string Human-readable resource name.
* @task resource
*/
abstract public function getResourceName(
DrydockBlueprint $blueprint,
DrydockResource $resource);
/* -( Resource Interfaces )------------------------------------------------ */
abstract public function getInterface(
DrydockBlueprint $blueprint,
DrydockResource $resource,
DrydockLease $lease,
$type);
/* -( Logging )------------------------------------------------------------ */
public static function getAllBlueprintImplementations() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->execute();
}
public static function getNamedImplementation($class) {
return idx(self::getAllBlueprintImplementations(), $class);
}
protected function newResourceTemplate(DrydockBlueprint $blueprint) {
$resource = id(new DrydockResource())
->setBlueprintPHID($blueprint->getPHID())
->attachBlueprint($blueprint)
->setType($this->getType())
->setStatus(DrydockResourceStatus::STATUS_PENDING);
// Pre-allocate the resource PHID.
$resource->setPHID($resource->generatePHID());
return $resource;
}
protected function newLease(DrydockBlueprint $blueprint) {
return DrydockLease::initializeNewLease()
->setAuthorizingPHID($blueprint->getPHID());
}
protected function requireActiveLease(DrydockLease $lease) {
$lease_status = $lease->getStatus();
switch ($lease_status) {
case DrydockLeaseStatus::STATUS_PENDING:
case DrydockLeaseStatus::STATUS_ACQUIRED:
throw new PhabricatorWorkerYieldException(15);
case DrydockLeaseStatus::STATUS_ACTIVE:
return;
default:
throw new Exception(
pht(
'Lease ("%s") is in bad state ("%s"), expected "%s".',
$lease->getPHID(),
$lease_status,
DrydockLeaseStatus::STATUS_ACTIVE));
}
}
/**
* Does this implementation use concurrent resource limits?
*
* Implementations can override this method to opt into standard limit
* behavior, which provides a simple concurrent resource limit.
*
* @return bool True to use limits.
*/
protected function shouldUseConcurrentResourceLimit() {
return false;
}
/**
* Get the effective concurrent resource limit for this blueprint.
*
* @param DrydockBlueprint Blueprint to get the limit for.
* @return int|null Limit, or `null` for no limit.
*/
protected function getConcurrentResourceLimit(DrydockBlueprint $blueprint) {
if ($this->shouldUseConcurrentResourceLimit()) {
$limit = $blueprint->getFieldValue('allocator.limit');
$limit = (int)$limit;
if ($limit > 0) {
return $limit;
} else {
return null;
}
}
return null;
}
protected function getConcurrentResourceLimitSlotLock(
DrydockBlueprint $blueprint) {
$limit = $this->getConcurrentResourceLimit($blueprint);
if ($limit === null) {
return;
}
$blueprint_phid = $blueprint->getPHID();
// TODO: This logic shouldn't do anything awful, but is a little silly. It
// would be nice to unify the "huge limit" and "small limit" cases
// eventually but it's a little tricky.
// If the limit is huge, just pick a random slot. This is just stopping
// us from exploding if someone types a billion zillion into the box.
if ($limit > 1024) {
$slot = mt_rand(0, $limit - 1);
return "allocator({$blueprint_phid}).limit({$slot})";
}
// For reasonable limits, actually check for an available slot.
- $locks = DrydockSlotLock::loadLocks($blueprint_phid);
- $locks = mpull($locks, null, 'getLockKey');
-
$slots = range(0, $limit - 1);
shuffle($slots);
+ $lock_names = array();
foreach ($slots as $slot) {
- $slot_lock = "allocator({$blueprint_phid}).limit({$slot})";
- if (empty($locks[$slot_lock])) {
- return $slot_lock;
+ $lock_names[] = "allocator({$blueprint_phid}).limit({$slot})";
+ }
+
+ $locks = DrydockSlotLock::loadHeldLocks($lock_names);
+ $locks = mpull($locks, null, 'getLockKey');
+
+ foreach ($lock_names as $lock_name) {
+ if (empty($locks[$lock_name])) {
+ return $lock_name;
}
}
// If we found no free slot, just return whatever we checked last (which
// is just a random slot). There's a small chance we'll get lucky and the
// lock will be free by the time we try to take it, but usually we'll just
// fail to grab the lock, throw an appropriate lock exception, and get back
// on the right path to retry later.
- return $slot_lock;
+
+ return $lock_name;
}
/**
* Apply standard limits on resource allocation rate.
*
* @param DrydockBlueprint The blueprint requesting an allocation.
* @return bool True if further allocations should be limited.
*/
protected function shouldLimitAllocatingPoolSize(
DrydockBlueprint $blueprint) {
// TODO: If this mechanism sticks around, these values should be
// configurable by the blueprint implementation.
// Limit on total number of active resources.
$total_limit = $this->getConcurrentResourceLimit($blueprint);
// Always allow at least this many allocations to be in flight at once.
$min_allowed = 1;
// Allow this fraction of allocating resources as a fraction of active
// resources.
$growth_factor = 0.25;
$resource = new DrydockResource();
$conn_r = $resource->establishConnection('r');
$counts = queryfx_all(
$conn_r,
'SELECT status, COUNT(*) N FROM %T
WHERE blueprintPHID = %s AND status != %s
GROUP BY status',
$resource->getTableName(),
$blueprint->getPHID(),
DrydockResourceStatus::STATUS_DESTROYED);
$counts = ipull($counts, 'N', 'status');
$n_alloc = idx($counts, DrydockResourceStatus::STATUS_PENDING, 0);
$n_active = idx($counts, DrydockResourceStatus::STATUS_ACTIVE, 0);
$n_broken = idx($counts, DrydockResourceStatus::STATUS_BROKEN, 0);
$n_released = idx($counts, DrydockResourceStatus::STATUS_RELEASED, 0);
// If we're at the limit on total active resources, limit additional
// allocations.
if ($total_limit !== null) {
$n_total = ($n_alloc + $n_active + $n_broken + $n_released);
if ($n_total >= $total_limit) {
return true;
}
}
// If the number of in-flight allocations is fewer than the minimum number
// of allowed allocations, don't impose a limit.
if ($n_alloc < $min_allowed) {
return false;
}
$allowed_alloc = (int)ceil($n_active * $growth_factor);
// If the number of in-flight allocation is fewer than the number of
// allowed allocations according to the pool growth factor, don't impose
// a limit.
if ($n_alloc < $allowed_alloc) {
return false;
}
return true;
}
}
diff --git a/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php
index a5d61ec06..f45c4f2ad 100644
--- a/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php
+++ b/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php
@@ -1,519 +1,519 @@
<?php
final class DrydockWorkingCopyBlueprintImplementation
extends DrydockBlueprintImplementation {
const PHASE_SQUASHMERGE = 'squashmerge';
const PHASE_REMOTEFETCH = 'blueprint.workingcopy.fetch.remote';
const PHASE_MERGEFETCH = 'blueprint.workingcopy.fetch.staging';
public function isEnabled() {
return true;
}
public function getBlueprintName() {
return pht('Working Copy');
}
public function getBlueprintIcon() {
return 'fa-folder-open';
}
public function getDescription() {
return pht('Allows Drydock to check out working copies of repositories.');
}
public function canAnyBlueprintEverAllocateResourceForLease(
DrydockLease $lease) {
return true;
}
public function canEverAllocateResourceForLease(
DrydockBlueprint $blueprint,
DrydockLease $lease) {
return true;
}
public function canAllocateResourceForLease(
DrydockBlueprint $blueprint,
DrydockLease $lease) {
$viewer = $this->getViewer();
if ($this->shouldLimitAllocatingPoolSize($blueprint)) {
return false;
}
// TODO: If we have a pending resource which is compatible with the
// configuration for this lease, prevent a new allocation? Otherwise the
// queue can fill up with copies of requests from the same lease. But
// maybe we can deal with this with "pre-leasing"?
return true;
}
public function canAcquireLeaseOnResource(
DrydockBlueprint $blueprint,
DrydockResource $resource,
DrydockLease $lease) {
// Don't hand out leases on working copies which have not activated, since
// it may take an arbitrarily long time for them to acquire a host.
if (!$resource->isActive()) {
return false;
}
$need_map = $lease->getAttribute('repositories.map');
if (!is_array($need_map)) {
return false;
}
$have_map = $resource->getAttribute('repositories.map');
if (!is_array($have_map)) {
return false;
}
$have_as = ipull($have_map, 'phid');
$need_as = ipull($need_map, 'phid');
foreach ($need_as as $need_directory => $need_phid) {
if (empty($have_as[$need_directory])) {
// This resource is missing a required working copy.
return false;
}
if ($have_as[$need_directory] != $need_phid) {
// This resource has a required working copy, but it contains
// the wrong repository.
return false;
}
unset($have_as[$need_directory]);
}
if ($have_as && $lease->getAttribute('repositories.strict')) {
// This resource has extra repositories, but the lease is strict about
// which repositories are allowed to exist.
return false;
}
if (!DrydockSlotLock::isLockFree($this->getLeaseSlotLock($resource))) {
return false;
}
return true;
}
public function acquireLease(
DrydockBlueprint $blueprint,
DrydockResource $resource,
DrydockLease $lease) {
$lease
->needSlotLock($this->getLeaseSlotLock($resource))
->acquireOnResource($resource);
}
private function getLeaseSlotLock(DrydockResource $resource) {
$resource_phid = $resource->getPHID();
return "workingcopy.lease({$resource_phid})";
}
public function allocateResource(
DrydockBlueprint $blueprint,
DrydockLease $lease) {
$resource = $this->newResourceTemplate($blueprint);
$resource_phid = $resource->getPHID();
$blueprint_phids = $blueprint->getFieldValue('blueprintPHIDs');
$host_lease = $this->newLease($blueprint)
->setResourceType('host')
->setOwnerPHID($resource_phid)
->setAttribute('workingcopy.resourcePHID', $resource_phid)
->setAllowedBlueprintPHIDs($blueprint_phids);
$resource->setAttribute('host.leasePHID', $host_lease->getPHID());
$map = $lease->getAttribute('repositories.map');
foreach ($map as $key => $value) {
$map[$key] = array_select_keys(
$value,
array(
'phid',
));
}
$resource->setAttribute('repositories.map', $map);
$slot_lock = $this->getConcurrentResourceLimitSlotLock($blueprint);
if ($slot_lock !== null) {
$resource->needSlotLock($slot_lock);
}
$resource->allocateResource();
$host_lease->queueForActivation();
return $resource;
}
public function activateResource(
DrydockBlueprint $blueprint,
DrydockResource $resource) {
$lease = $this->loadHostLease($resource);
$this->requireActiveLease($lease);
$command_type = DrydockCommandInterface::INTERFACE_TYPE;
$interface = $lease->getInterface($command_type);
// TODO: Make this configurable.
$resource_id = $resource->getID();
$root = "/var/drydock/workingcopy-{$resource_id}";
$map = $resource->getAttribute('repositories.map');
$repositories = $this->loadRepositories(ipull($map, 'phid'));
foreach ($map as $directory => $spec) {
// TODO: Validate directory isn't goofy like "/etc" or "../../lol"
// somewhere?
$repository = $repositories[$spec['phid']];
$path = "{$root}/repo/{$directory}/";
// TODO: Run these in parallel?
$interface->execx(
'git clone -- %s %s',
(string)$repository->getCloneURIObject(),
$path);
}
$resource
->setAttribute('workingcopy.root', $root)
->activateResource();
}
public function destroyResource(
DrydockBlueprint $blueprint,
DrydockResource $resource) {
try {
$lease = $this->loadHostLease($resource);
} catch (Exception $ex) {
// If we can't load the lease, assume we don't need to take any actions
// to destroy it.
return;
}
// Destroy the lease on the host.
- $lease->releaseOnDestruction();
+ $lease->setReleaseOnDestruction(true);
if ($lease->isActive()) {
// Destroy the working copy on disk.
$command_type = DrydockCommandInterface::INTERFACE_TYPE;
$interface = $lease->getInterface($command_type);
$root_key = 'workingcopy.root';
$root = $resource->getAttribute($root_key);
if (strlen($root)) {
$interface->execx('rm -rf -- %s', $root);
}
}
}
public function getResourceName(
DrydockBlueprint $blueprint,
DrydockResource $resource) {
return pht('Working Copy');
}
public function activateLease(
DrydockBlueprint $blueprint,
DrydockResource $resource,
DrydockLease $lease) {
$host_lease = $this->loadHostLease($resource);
$command_type = DrydockCommandInterface::INTERFACE_TYPE;
$interface = $host_lease->getInterface($command_type);
$map = $lease->getAttribute('repositories.map');
$root = $resource->getAttribute('workingcopy.root');
$default = null;
foreach ($map as $directory => $spec) {
$interface->pushWorkingDirectory("{$root}/repo/{$directory}/");
$cmd = array();
$arg = array();
$cmd[] = 'git clean -d --force';
$cmd[] = 'git fetch';
$commit = idx($spec, 'commit');
$branch = idx($spec, 'branch');
$ref = idx($spec, 'ref');
// Reset things first, in case previous builds left anything staged or
// dirty.
$cmd[] = 'git reset --hard HEAD';
if ($commit !== null) {
$cmd[] = 'git checkout %s --';
$arg[] = $commit;
} else if ($branch !== null) {
$cmd[] = 'git checkout %s --';
$arg[] = $branch;
$cmd[] = 'git reset --hard origin/%s';
$arg[] = $branch;
}
$this->execxv($interface, $cmd, $arg);
if (idx($spec, 'default')) {
$default = $directory;
}
// If we're fetching a ref from a remote, do that separately so we can
// raise a more tailored error.
if ($ref) {
$cmd = array();
$arg = array();
$ref_uri = $ref['uri'];
$ref_ref = $ref['ref'];
$cmd[] = 'git fetch --no-tags -- %s +%s:%s';
$arg[] = $ref_uri;
$arg[] = $ref_ref;
$arg[] = $ref_ref;
$cmd[] = 'git checkout %s --';
$arg[] = $ref_ref;
try {
$this->execxv($interface, $cmd, $arg);
} catch (CommandException $ex) {
$display_command = csprintf(
'git fetch %R %R',
$ref_uri,
$ref_ref);
$error = DrydockCommandError::newFromCommandException($ex)
->setPhase(self::PHASE_REMOTEFETCH)
->setDisplayCommand($display_command);
$lease->setAttribute(
'workingcopy.vcs.error',
$error->toDictionary());
throw $ex;
}
}
$merges = idx($spec, 'merges');
if ($merges) {
foreach ($merges as $merge) {
$this->applyMerge($lease, $interface, $merge);
}
}
$interface->popWorkingDirectory();
}
if ($default === null) {
$default = head_key($map);
}
// TODO: Use working storage?
$lease->setAttribute('workingcopy.default', "{$root}/repo/{$default}/");
$lease->activateOnResource($resource);
}
public function didReleaseLease(
DrydockBlueprint $blueprint,
DrydockResource $resource,
DrydockLease $lease) {
// We leave working copies around even if there are no leases on them,
// since the cost to maintain them is nearly zero but rebuilding them is
// moderately expensive and it's likely that they'll be reused.
return;
}
public function destroyLease(
DrydockBlueprint $blueprint,
DrydockResource $resource,
DrydockLease $lease) {
// When we activate a lease we just reset the working copy state and do
// not create any new state, so we don't need to do anything special when
// destroying a lease.
return;
}
public function getType() {
return 'working-copy';
}
public function getInterface(
DrydockBlueprint $blueprint,
DrydockResource $resource,
DrydockLease $lease,
$type) {
switch ($type) {
case DrydockCommandInterface::INTERFACE_TYPE:
$host_lease = $this->loadHostLease($resource);
$command_interface = $host_lease->getInterface($type);
$path = $lease->getAttribute('workingcopy.default');
$command_interface->pushWorkingDirectory($path);
return $command_interface;
}
}
private function loadRepositories(array $phids) {
$viewer = $this->getViewer();
$repositories = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->withPHIDs($phids)
->execute();
$repositories = mpull($repositories, null, 'getPHID');
foreach ($phids as $phid) {
if (empty($repositories[$phid])) {
throw new Exception(
pht(
'Repository PHID "%s" does not exist.',
$phid));
}
}
foreach ($repositories as $repository) {
$repository_vcs = $repository->getVersionControlSystem();
switch ($repository_vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
break;
default:
throw new Exception(
pht(
'Repository ("%s") has unsupported VCS ("%s").',
$repository->getPHID(),
$repository_vcs));
}
}
return $repositories;
}
private function loadHostLease(DrydockResource $resource) {
$viewer = $this->getViewer();
$lease_phid = $resource->getAttribute('host.leasePHID');
$lease = id(new DrydockLeaseQuery())
->setViewer($viewer)
->withPHIDs(array($lease_phid))
->executeOne();
if (!$lease) {
throw new Exception(
pht(
'Unable to load lease ("%s").',
$lease_phid));
}
return $lease;
}
protected function getCustomFieldSpecifications() {
return array(
'blueprintPHIDs' => array(
'name' => pht('Use Blueprints'),
'type' => 'blueprints',
'required' => true,
),
);
}
protected function shouldUseConcurrentResourceLimit() {
return true;
}
private function applyMerge(
DrydockLease $lease,
DrydockCommandInterface $interface,
array $merge) {
$src_uri = $merge['src.uri'];
$src_ref = $merge['src.ref'];
try {
$interface->execx(
'git fetch --no-tags -- %s +%s:%s',
$src_uri,
$src_ref,
$src_ref);
} catch (CommandException $ex) {
$display_command = csprintf(
'git fetch %R +%R:%R',
$src_uri,
$src_ref,
$src_ref);
$error = DrydockCommandError::newFromCommandException($ex)
->setPhase(self::PHASE_MERGEFETCH)
->setDisplayCommand($display_command);
$lease->setAttribute('workingcopy.vcs.error', $error->toDictionary());
throw $ex;
}
// NOTE: This can never actually generate a commit because we pass
// "--squash", but git sometimes runs code to check that a username and
// email are configured anyway.
$real_command = csprintf(
'git -c user.name=%s -c user.email=%s merge --no-stat --squash -- %R',
'drydock',
'drydock@phabricator',
$src_ref);
try {
$interface->execx('%C', $real_command);
} catch (CommandException $ex) {
$display_command = csprintf(
'git merge --squash %R',
$src_ref);
$error = DrydockCommandError::newFromCommandException($ex)
->setPhase(self::PHASE_SQUASHMERGE)
->setDisplayCommand($display_command);
$lease->setAttribute('workingcopy.vcs.error', $error->toDictionary());
throw $ex;
}
}
public function getCommandError(DrydockLease $lease) {
return $lease->getAttribute('workingcopy.vcs.error');
}
private function execxv(
DrydockCommandInterface $interface,
array $commands,
array $arguments) {
$commands = implode(' && ', $commands);
$argv = array_merge(array($commands), $arguments);
return call_user_func_array(array($interface, 'execx'), $argv);
}
}
diff --git a/src/applications/drydock/constants/DrydockConstants.php b/src/applications/drydock/constants/DrydockConstants.php
deleted file mode 100644
index 48d06329b..000000000
--- a/src/applications/drydock/constants/DrydockConstants.php
+++ /dev/null
@@ -1,3 +0,0 @@
-<?php
-
-abstract class DrydockConstants extends Phobject {}
diff --git a/src/applications/drydock/constants/DrydockLeaseStatus.php b/src/applications/drydock/constants/DrydockLeaseStatus.php
index f37e4ab9b..360b0b0f9 100644
--- a/src/applications/drydock/constants/DrydockLeaseStatus.php
+++ b/src/applications/drydock/constants/DrydockLeaseStatus.php
@@ -1,32 +1,114 @@
<?php
-final class DrydockLeaseStatus extends DrydockConstants {
+final class DrydockLeaseStatus
+ extends PhabricatorObjectStatus {
const STATUS_PENDING = 'pending';
const STATUS_ACQUIRED = 'acquired';
const STATUS_ACTIVE = 'active';
const STATUS_RELEASED = 'released';
const STATUS_BROKEN = 'broken';
const STATUS_DESTROYED = 'destroyed';
+ public static function newStatusObject($key) {
+ return new self($key, id(new self())->getStatusSpecification($key));
+ }
+
public static function getStatusMap() {
- return array(
- self::STATUS_PENDING => pht('Pending'),
- self::STATUS_ACQUIRED => pht('Acquired'),
- self::STATUS_ACTIVE => pht('Active'),
- self::STATUS_RELEASED => pht('Released'),
- self::STATUS_BROKEN => pht('Broken'),
- self::STATUS_DESTROYED => pht('Destroyed'),
- );
+ $map = id(new self())->getStatusSpecifications();
+ return ipull($map, 'name', 'key');
}
public static function getNameForStatus($status) {
- $map = self::getStatusMap();
- return idx($map, $status, pht('Unknown'));
+ $map = id(new self())->getStatusSpecification($status);
+ return $map['name'];
}
public static function getAllStatuses() {
- return array_keys(self::getStatusMap());
+ return array_keys(id(new self())->getStatusSpecifications());
+ }
+
+ public function isActivating() {
+ return $this->getStatusProperty('isActivating');
+ }
+
+ public function isActive() {
+ return ($this->getKey() === self::STATUS_ACTIVE);
+ }
+
+ public function canRelease() {
+ return $this->getStatusProperty('isReleasable');
+ }
+
+ public function canReceiveCommands() {
+ return $this->getStatusProperty('isCommandable');
+ }
+
+ protected function newStatusSpecifications() {
+ return array(
+ array(
+ 'key' => self::STATUS_PENDING,
+ 'name' => pht('Pending'),
+ 'icon' => 'fa-clock-o',
+ 'color' => 'blue',
+ 'isReleasable' => true,
+ 'isCommandable' => true,
+ 'isActivating' => true,
+ ),
+ array(
+ 'key' => self::STATUS_ACQUIRED,
+ 'name' => pht('Acquired'),
+ 'icon' => 'fa-refresh',
+ 'color' => 'blue',
+ 'isReleasable' => true,
+ 'isCommandable' => true,
+ 'isActivating' => true,
+ ),
+ array(
+ 'key' => self::STATUS_ACTIVE,
+ 'name' => pht('Active'),
+ 'icon' => 'fa-check',
+ 'color' => 'green',
+ 'isReleasable' => true,
+ 'isCommandable' => true,
+ 'isActivating' => false,
+ ),
+ array(
+ 'key' => self::STATUS_RELEASED,
+ 'name' => pht('Released'),
+ 'icon' => 'fa-circle-o',
+ 'color' => 'blue',
+ 'isReleasable' => false,
+ 'isCommandable' => false,
+ 'isActivating' => false,
+ ),
+ array(
+ 'key' => self::STATUS_BROKEN,
+ 'name' => pht('Broken'),
+ 'icon' => 'fa-times',
+ 'color' => 'indigo',
+ 'isReleasable' => true,
+ 'isCommandable' => true,
+ 'isActivating' => false,
+ ),
+ array(
+ 'key' => self::STATUS_DESTROYED,
+ 'name' => pht('Destroyed'),
+ 'icon' => 'fa-times',
+ 'color' => 'grey',
+ 'isReleasable' => false,
+ 'isCommandable' => false,
+ 'isActivating' => false,
+ ),
+ );
+ }
+
+ protected function newUnknownStatusSpecification($status) {
+ return array(
+ 'isReleasable' => false,
+ 'isCommandable' => false,
+ 'isActivating' => false,
+ );
}
}
diff --git a/src/applications/drydock/constants/DrydockResourceStatus.php b/src/applications/drydock/constants/DrydockResourceStatus.php
index d8a860d6a..197a22063 100644
--- a/src/applications/drydock/constants/DrydockResourceStatus.php
+++ b/src/applications/drydock/constants/DrydockResourceStatus.php
@@ -1,30 +1,94 @@
<?php
-final class DrydockResourceStatus extends DrydockConstants {
+final class DrydockResourceStatus
+ extends PhabricatorObjectStatus {
const STATUS_PENDING = 'pending';
const STATUS_ACTIVE = 'active';
const STATUS_RELEASED = 'released';
const STATUS_BROKEN = 'broken';
const STATUS_DESTROYED = 'destroyed';
+ public static function newStatusObject($key) {
+ return new self($key, id(new self())->getStatusSpecification($key));
+ }
+
public static function getStatusMap() {
- return array(
- self::STATUS_PENDING => pht('Pending'),
- self::STATUS_ACTIVE => pht('Active'),
- self::STATUS_RELEASED => pht('Released'),
- self::STATUS_BROKEN => pht('Broken'),
- self::STATUS_DESTROYED => pht('Destroyed'),
- );
+ $map = id(new self())->getStatusSpecifications();
+ return ipull($map, 'name', 'key');
}
public static function getNameForStatus($status) {
- $map = self::getStatusMap();
- return idx($map, $status, pht('Unknown'));
+ $map = id(new self())->getStatusSpecification($status);
+ return $map['name'];
}
public static function getAllStatuses() {
- return array_keys(self::getStatusMap());
+ return array_keys(id(new self())->getStatusSpecifications());
+ }
+
+ public function isActive() {
+ return ($this->getKey() === self::STATUS_ACTIVE);
+ }
+
+ public function canRelease() {
+ return $this->getStatusProperty('isReleasable');
+ }
+
+ public function canReceiveCommands() {
+ return $this->getStatusProperty('isCommandable');
+ }
+
+ protected function newStatusSpecifications() {
+ return array(
+ array(
+ 'key' => self::STATUS_PENDING,
+ 'name' => pht('Pending'),
+ 'icon' => 'fa-clock-o',
+ 'color' => 'blue',
+ 'isReleasable' => true,
+ 'isCommandable' => true,
+ ),
+ array(
+ 'key' => self::STATUS_ACTIVE,
+ 'name' => pht('Active'),
+ 'icon' => 'fa-check',
+ 'color' => 'green',
+ 'isReleasable' => true,
+ 'isCommandable' => true,
+ ),
+ array(
+ 'key' => self::STATUS_RELEASED,
+ 'name' => pht('Released'),
+ 'icon' => 'fa-circle-o',
+ 'color' => 'blue',
+ 'isReleasable' => false,
+ 'isCommandable' => false,
+ ),
+ array(
+ 'key' => self::STATUS_BROKEN,
+ 'name' => pht('Broken'),
+ 'icon' => 'fa-times',
+ 'color' => 'indigo',
+ 'isReleasable' => true,
+ 'isCommandable' => false,
+ ),
+ array(
+ 'key' => self::STATUS_DESTROYED,
+ 'name' => pht('Destroyed'),
+ 'icon' => 'fa-times',
+ 'color' => 'grey',
+ 'isReleasable' => false,
+ 'isCommandable' => false,
+ ),
+ );
+ }
+
+ protected function newUnknownStatusSpecification($status) {
+ return array(
+ 'isReleasable' => false,
+ 'isCommandable' => false,
+ );
}
}
diff --git a/src/applications/drydock/controller/DrydockLeaseViewController.php b/src/applications/drydock/controller/DrydockLeaseViewController.php
index 1583d28df..91a911277 100644
--- a/src/applications/drydock/controller/DrydockLeaseViewController.php
+++ b/src/applications/drydock/controller/DrydockLeaseViewController.php
@@ -1,173 +1,178 @@
<?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');
+ ->setHeaderIcon('fa-link')
+ ->setStatus(
+ $lease->getStatusIcon(),
+ $lease->getStatusColor(),
+ $lease->getStatusDisplayName());
if ($lease->isReleasing()) {
- $header->setStatus('fa-exclamation-triangle', 'red', pht('Releasing'));
+ $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('Status'),
- DrydockLeaseStatus::getNameForStatus($lease->getStatus()));
-
$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);
$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/controller/DrydockResourceViewController.php b/src/applications/drydock/controller/DrydockResourceViewController.php
index c6771007b..8718caa9e 100644
--- a/src/applications/drydock/controller/DrydockResourceViewController.php
+++ b/src/applications/drydock/controller/DrydockResourceViewController.php
@@ -1,205 +1,208 @@
<?php
final class DrydockResourceViewController extends DrydockResourceController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$resource = id(new DrydockResourceQuery())
->setViewer($viewer)
->withIDs(array($id))
->needUnconsumedCommands(true)
->executeOne();
if (!$resource) {
return new Aphront404Response();
}
$title = pht(
'Resource %s %s',
$resource->getID(),
$resource->getResourceName());
$header = id(new PHUIHeaderView())
->setUser($viewer)
->setPolicyObject($resource)
->setHeader($title)
- ->setHeaderIcon('fa-map');
+ ->setHeaderIcon('fa-map')
+ ->setStatus(
+ $resource->getStatusIcon(),
+ $resource->getStatusColor(),
+ $resource->getStatusDisplayName());
if ($resource->isReleasing()) {
- $header->setStatus('fa-exclamation-triangle', 'red', pht('Releasing'));
+ $header->addTag(
+ id(new PHUITagView())
+ ->setType(PHUITagView::TYPE_SHADE)
+ ->setIcon('fa-exclamation-triangle')
+ ->setColor('red')
+ ->setName('Releasing'));
}
$curtain = $this->buildCurtain($resource);
$properties = $this->buildPropertyListView($resource);
$id = $resource->getID();
$resource_uri = $this->getApplicationURI("resource/{$id}/");
$log_query = id(new DrydockLogQuery())
->withResourcePHIDs(array($resource->getPHID()));
$log_box = $this->buildLogBox(
$log_query,
$this->getApplicationURI("resource/{$id}/logs/query/all/"));
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Resource %d', $resource->getID()));
$crumbs->setBorder(true);
$locks = $this->buildLocksTab($resource->getPHID());
$commands = $this->buildCommandsTab($resource->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);
$lease_box = $this->buildLeaseBox($resource);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setCurtain($curtain)
->setMainColumn(array(
$object_box,
$lease_box,
$log_box,
));
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild(
array(
$view,
));
}
private function buildCurtain(DrydockResource $resource) {
$viewer = $this->getViewer();
$curtain = $this->newCurtainView($resource);
$can_release = $resource->canRelease();
if ($resource->isReleasing()) {
$can_release = false;
}
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$resource,
PhabricatorPolicyCapability::CAN_EDIT);
$uri = '/resource/'.$resource->getID().'/release/';
$uri = $this->getApplicationURI($uri);
$curtain->addAction(
id(new PhabricatorActionView())
->setHref($uri)
->setName(pht('Release Resource'))
->setIcon('fa-times')
->setWorkflow(true)
->setDisabled(!$can_release || !$can_edit));
return $curtain;
}
private function buildPropertyListView(
DrydockResource $resource) {
$viewer = $this->getViewer();
$view = new PHUIPropertyListView();
- $status = $resource->getStatus();
- $status = DrydockResourceStatus::getNameForStatus($status);
-
- $view->addProperty(
- pht('Status'),
- $status);
$until = $resource->getUntil();
if ($until) {
$until_display = phabricator_datetime($until, $viewer);
} else {
$until_display = phutil_tag('em', array(), pht('Never'));
}
$view->addProperty(pht('Expires'), $until_display);
$view->addProperty(
pht('Resource Type'),
$resource->getType());
$view->addProperty(
pht('Blueprint'),
$viewer->renderHandle($resource->getBlueprintPHID()));
$attributes = $resource->getAttributes();
if ($attributes) {
$view->addSectionHeader(
pht('Attributes'), 'fa-list-ul');
foreach ($attributes as $key => $value) {
$view->addProperty($key, $value);
}
}
return $view;
}
private function buildLeaseBox(DrydockResource $resource) {
$viewer = $this->getViewer();
$leases = id(new DrydockLeaseQuery())
->setViewer($viewer)
->withResourcePHIDs(array($resource->getPHID()))
->withStatuses(
array(
DrydockLeaseStatus::STATUS_PENDING,
DrydockLeaseStatus::STATUS_ACQUIRED,
DrydockLeaseStatus::STATUS_ACTIVE,
))
->setLimit(100)
->execute();
$id = $resource->getID();
$leases_uri = "resource/{$id}/leases/query/all/";
$leases_uri = $this->getApplicationURI($leases_uri);
$lease_header = id(new PHUIHeaderView())
->setHeader(pht('Active Leases'))
->addActionLink(
id(new PHUIButtonView())
->setTag('a')
->setHref($leases_uri)
->setIcon('fa-search')
->setText(pht('View All')));
$lease_list = id(new DrydockLeaseListView())
->setUser($viewer)
->setLeases($leases)
->render()
->setNoDataString(pht('This resource has no active leases.'));
return id(new PHUIObjectBoxView())
->setHeader($lease_header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setObjectList($lease_list);
}
}
diff --git a/src/applications/drydock/exception/DrydockAcquiredBrokenResourceException.php b/src/applications/drydock/exception/DrydockAcquiredBrokenResourceException.php
new file mode 100644
index 000000000..12b839d46
--- /dev/null
+++ b/src/applications/drydock/exception/DrydockAcquiredBrokenResourceException.php
@@ -0,0 +1,4 @@
+<?php
+
+final class DrydockAcquiredBrokenResourceException
+ extends Exception {}
diff --git a/src/applications/drydock/exception/DrydockResourceLockException.php b/src/applications/drydock/exception/DrydockResourceLockException.php
new file mode 100644
index 000000000..2b54538e0
--- /dev/null
+++ b/src/applications/drydock/exception/DrydockResourceLockException.php
@@ -0,0 +1,4 @@
+<?php
+
+final class DrydockResourceLockException
+ extends Exception {}
diff --git a/src/applications/drydock/logtype/DrydockLeaseAllocationFailureLogType.php b/src/applications/drydock/logtype/DrydockLeaseAllocationFailureLogType.php
new file mode 100644
index 000000000..33ad881f4
--- /dev/null
+++ b/src/applications/drydock/logtype/DrydockLeaseAllocationFailureLogType.php
@@ -0,0 +1,26 @@
+<?php
+
+final class DrydockLeaseAllocationFailureLogType extends DrydockLogType {
+
+ const LOGCONST = 'core.lease.allocation-failure';
+
+ public function getLogTypeName() {
+ return pht('Allocation Failed');
+ }
+
+ public function getLogTypeIcon(array $data) {
+ return 'fa-times red';
+ }
+
+ public function renderLog(array $data) {
+ $class = idx($data, 'class');
+ $message = idx($data, 'message');
+
+ return pht(
+ 'One or more blueprints promised a new resource, but failed when '.
+ 'allocating: [%s] %s',
+ $class,
+ $message);
+ }
+
+}
diff --git a/src/applications/drydock/logtype/DrydockLeaseNoAuthorizationsLogType.php b/src/applications/drydock/logtype/DrydockLeaseNoAuthorizationsLogType.php
index b5a7ca1b1..cce98956a 100644
--- a/src/applications/drydock/logtype/DrydockLeaseNoAuthorizationsLogType.php
+++ b/src/applications/drydock/logtype/DrydockLeaseNoAuthorizationsLogType.php
@@ -1,26 +1,25 @@
<?php
final class DrydockLeaseNoAuthorizationsLogType extends DrydockLogType {
const LOGCONST = 'core.lease.no-authorizations';
public function getLogTypeName() {
return pht('No Authorizations');
}
public function getLogTypeIcon(array $data) {
return 'fa-map-o red';
}
public function renderLog(array $data) {
- $viewer = $this->getViewer();
$authorizing_phid = idx($data, 'authorizingPHID');
return pht(
'The object which authorized this lease (%s) is not authorized to use '.
'any of the blueprints the lease lists. Approve the authorizations '.
'before using the lease.',
- $viewer->renderHandle($authorizing_phid)->render());
+ $this->renderHandle($authorizing_phid));
}
}
diff --git a/src/applications/drydock/logtype/DrydockLeaseReacquireLogType.php b/src/applications/drydock/logtype/DrydockLeaseReacquireLogType.php
new file mode 100644
index 000000000..13e759690
--- /dev/null
+++ b/src/applications/drydock/logtype/DrydockLeaseReacquireLogType.php
@@ -0,0 +1,26 @@
+<?php
+
+final class DrydockLeaseReacquireLogType extends DrydockLogType {
+
+ const LOGCONST = 'core.lease.reacquire';
+
+ public function getLogTypeName() {
+ return pht('Reacquiring Resource');
+ }
+
+ public function getLogTypeIcon(array $data) {
+ return 'fa-refresh yellow';
+ }
+
+ public function renderLog(array $data) {
+ $class = idx($data, 'class');
+ $message = idx($data, 'message');
+
+ return pht(
+ 'Lease acquired a resource but failed to activate; acquisition '.
+ 'will be retried: [%s] %s',
+ $class,
+ $message);
+ }
+
+}
diff --git a/src/applications/drydock/logtype/DrydockLeaseReclaimLogType.php b/src/applications/drydock/logtype/DrydockLeaseReclaimLogType.php
index 8dbc13d9d..6e145a24a 100644
--- a/src/applications/drydock/logtype/DrydockLeaseReclaimLogType.php
+++ b/src/applications/drydock/logtype/DrydockLeaseReclaimLogType.php
@@ -1,25 +1,23 @@
<?php
final class DrydockLeaseReclaimLogType extends DrydockLogType {
const LOGCONST = 'core.lease.reclaim';
public function getLogTypeName() {
return pht('Reclaimed Resources');
}
public function getLogTypeIcon(array $data) {
return 'fa-refresh yellow';
}
public function renderLog(array $data) {
- $viewer = $this->getViewer();
-
$resource_phids = idx($data, 'resourcePHIDs', array());
return pht(
'Reclaimed resource %s.',
- $viewer->renderHandleList($resource_phids)->render());
+ $this->renderHandleList($resource_phids));
}
}
diff --git a/src/applications/drydock/logtype/DrydockLeaseWaitingForResourcesLogType.php b/src/applications/drydock/logtype/DrydockLeaseWaitingForResourcesLogType.php
index 46ab965b3..bb91acb03 100644
--- a/src/applications/drydock/logtype/DrydockLeaseWaitingForResourcesLogType.php
+++ b/src/applications/drydock/logtype/DrydockLeaseWaitingForResourcesLogType.php
@@ -1,25 +1,23 @@
<?php
final class DrydockLeaseWaitingForResourcesLogType extends DrydockLogType {
const LOGCONST = 'core.lease.waiting-for-resources';
public function getLogTypeName() {
return pht('Waiting For Resource');
}
public function getLogTypeIcon(array $data) {
return 'fa-clock-o yellow';
}
public function renderLog(array $data) {
- $viewer = $this->getViewer();
-
$blueprint_phids = idx($data, 'blueprintPHIDs', array());
return pht(
'Waiting for available resources from: %s.',
- $viewer->renderHandleList($blueprint_phids)->render());
+ $this->renderHandleList($blueprint_phids));
}
}
diff --git a/src/applications/drydock/logtype/DrydockLogType.php b/src/applications/drydock/logtype/DrydockLogType.php
index 7faab42df..690ffc567 100644
--- a/src/applications/drydock/logtype/DrydockLogType.php
+++ b/src/applications/drydock/logtype/DrydockLogType.php
@@ -1,41 +1,74 @@
<?php
abstract class DrydockLogType extends Phobject {
private $viewer;
private $log;
+ private $renderingMode = 'text';
abstract public function getLogTypeName();
abstract public function getLogTypeIcon(array $data);
abstract public function renderLog(array $data);
- public function setViewer(PhabricatorUser $viewer) {
+ final public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
- public function getViewer() {
+ final public function getViewer() {
return $this->viewer;
}
final public function setLog(DrydockLog $log) {
$this->log = $log;
return $this;
}
final public function getLog() {
return $this->log;
}
final public function getLogTypeConstant() {
return $this->getPhobjectClassConstant('LOGCONST', 64);
}
final public static function getAllLogTypes() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getLogTypeConstant')
->execute();
}
+ final public function renderLogForText($data) {
+ $this->renderingMode = 'text';
+ return $this->renderLog($data);
+ }
+
+ final public function renderLogForHTML($data) {
+ $this->renderingMode = 'html';
+ return $this->renderLog($data);
+ }
+
+ final protected function renderHandle($phid) {
+ $viewer = $this->getViewer();
+ $handle = $viewer->renderHandle($phid);
+
+ if ($this->renderingMode == 'html') {
+ return $handle->render();
+ } else {
+ return $handle->setAsText(true)->render();
+ }
+ }
+
+ final protected function renderHandleList(array $phids) {
+ $viewer = $this->getViewer();
+ $handle_list = $viewer->renderHandleList($phids);
+
+ if ($this->renderingMode == 'html') {
+ return $handle_list->render();
+ } else {
+ return $handle_list->setAsText(true)->render();
+ }
+ }
+
}
diff --git a/src/applications/drydock/logtype/DrydockResourceAllocationFailureLogType.php b/src/applications/drydock/logtype/DrydockResourceAllocationFailureLogType.php
new file mode 100644
index 000000000..f1e4cf463
--- /dev/null
+++ b/src/applications/drydock/logtype/DrydockResourceAllocationFailureLogType.php
@@ -0,0 +1,26 @@
+<?php
+
+final class DrydockResourceAllocationFailureLogType extends DrydockLogType {
+
+ const LOGCONST = 'core.resource.allocation-failure';
+
+ public function getLogTypeName() {
+ return pht('Allocation Failed');
+ }
+
+ public function getLogTypeIcon(array $data) {
+ return 'fa-times red';
+ }
+
+ public function renderLog(array $data) {
+ $class = idx($data, 'class');
+ $message = idx($data, 'message');
+
+ return pht(
+ 'Blueprint failed to allocate a resource after claiming it would '.
+ 'be able to: [%s] %s',
+ $class,
+ $message);
+ }
+
+}
diff --git a/src/applications/drydock/logtype/DrydockResourceReclaimLogType.php b/src/applications/drydock/logtype/DrydockResourceReclaimLogType.php
index 9e9d5fdef..aa989e97d 100644
--- a/src/applications/drydock/logtype/DrydockResourceReclaimLogType.php
+++ b/src/applications/drydock/logtype/DrydockResourceReclaimLogType.php
@@ -1,24 +1,23 @@
<?php
final class DrydockResourceReclaimLogType extends DrydockLogType {
const LOGCONST = 'core.resource.reclaim';
public function getLogTypeName() {
return pht('Reclaimed');
}
public function getLogTypeIcon(array $data) {
return 'fa-refresh red';
}
public function renderLog(array $data) {
- $viewer = $this->getViewer();
$reclaimer_phid = idx($data, 'reclaimerPHID');
return pht(
'Resource reclaimed by %s.',
- $viewer->renderHandle($reclaimer_phid)->render());
+ $this->renderHandle($reclaimer_phid));
}
}
diff --git a/src/applications/drydock/management/DrydockManagementCommandWorkflow.php b/src/applications/drydock/management/DrydockManagementCommandWorkflow.php
index bc66966f8..ae0bd711b 100644
--- a/src/applications/drydock/management/DrydockManagementCommandWorkflow.php
+++ b/src/applications/drydock/management/DrydockManagementCommandWorkflow.php
@@ -1,66 +1,66 @@
<?php
final class DrydockManagementCommandWorkflow
extends DrydockManagementWorkflow {
protected function didConstruct() {
$this
->setName('command')
->setSynopsis(pht('Run a command on a leased resource.'))
->setArguments(
array(
array(
'name' => 'lease',
'param' => 'id',
'help' => pht('Lease ID.'),
),
array(
'name' => 'argv',
'wildcard' => true,
'help' => pht('Command to execute.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$lease_id = $args->getArg('lease');
if (!$lease_id) {
throw new PhutilArgumentUsageException(
pht(
'Use %s to specify a lease.',
'--lease'));
}
$argv = $args->getArg('argv');
if (!$argv) {
throw new PhutilArgumentUsageException(
pht(
'Specify a command to run.'));
}
$lease = id(new DrydockLeaseQuery())
->setViewer($this->getViewer())
->withIDs(array($lease_id))
->executeOne();
if (!$lease) {
throw new Exception(
pht(
'Unable to load lease with ID "%s"!',
$lease_id));
}
// TODO: Check lease state, etc.
$interface = $lease->getInterface(DrydockCommandInterface::INTERFACE_TYPE);
list($stdout, $stderr) = call_user_func_array(
array($interface, 'execx'),
array('%Ls', $argv));
- fprintf(STDOUT, $stdout);
- fprintf(STDERR, $stderr);
+ fwrite(STDOUT, $stdout);
+ fwrite(STDERR, $stderr);
return 0;
}
}
diff --git a/src/applications/drydock/management/DrydockManagementLeaseWorkflow.php b/src/applications/drydock/management/DrydockManagementLeaseWorkflow.php
index 4992833c5..75636dab0 100644
--- a/src/applications/drydock/management/DrydockManagementLeaseWorkflow.php
+++ b/src/applications/drydock/management/DrydockManagementLeaseWorkflow.php
@@ -1,102 +1,208 @@
<?php
final class DrydockManagementLeaseWorkflow
extends DrydockManagementWorkflow {
protected function didConstruct() {
$this
->setName('lease')
->setSynopsis(pht('Lease a resource.'))
->setArguments(
array(
array(
'name' => 'type',
'param' => 'resource_type',
'help' => pht('Resource type.'),
),
array(
'name' => 'until',
'param' => 'time',
'help' => pht('Set lease expiration time.'),
),
array(
'name' => 'attributes',
'param' => 'name=value,...',
'help' => pht('Resource specification.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$viewer = $this->getViewer();
$resource_type = $args->getArg('type');
if (!$resource_type) {
throw new PhutilArgumentUsageException(
pht(
'Specify a resource type with `%s`.',
'--type'));
}
$until = $args->getArg('until');
if (strlen($until)) {
$until = strtotime($until);
if ($until <= 0) {
throw new PhutilArgumentUsageException(
pht(
'Unable to parse argument to "%s".',
'--until'));
}
}
$attributes = $args->getArg('attributes');
if ($attributes) {
$options = new PhutilSimpleOptions();
$options->setCaseSensitive(true);
$attributes = $options->parse($attributes);
}
$lease = id(new DrydockLease())
->setResourceType($resource_type);
$drydock_phid = id(new PhabricatorDrydockApplication())->getPHID();
$lease->setAuthorizingPHID($drydock_phid);
+ if ($attributes) {
+ $lease->setAttributes($attributes);
+ }
+
// TODO: This is not hugely scalable, although this is a debugging workflow
// so maybe it's fine. Do we even need `bin/drydock lease` in the long run?
$all_blueprints = id(new DrydockBlueprintQuery())
->setViewer($viewer)
->execute();
$allowed_phids = mpull($all_blueprints, 'getPHID');
if (!$allowed_phids) {
throw new Exception(
pht(
'No blueprints exist which can plausibly allocate resources to '.
'satisfy the requested lease.'));
}
$lease->setAllowedBlueprintPHIDs($allowed_phids);
- if ($attributes) {
- $lease->setAttributes($attributes);
- }
-
if ($until) {
$lease->setUntil($until);
}
+ // If something fatals or the user interrupts the process (for example,
+ // with "^C"), release the lease. We'll cancel this below, if the lease
+ // actually activates.
+ $lease->setReleaseOnDestruction(true);
+
+ // TODO: This would probably be better handled with PhutilSignalRouter,
+ // but it currently doesn't route SIGINT. We're initializing it to setup
+ // SIGTERM handling and make eventual migration easier.
+ $router = PhutilSignalRouter::getRouter();
+ pcntl_signal(SIGINT, array($this, 'didReceiveInterrupt'));
+
+ $t_start = microtime(true);
$lease->queueForActivation();
echo tsprintf(
- "%s\n",
+ "%s\n\n __%s__\n\n%s\n",
+ pht('Queued lease for activation:'),
+ PhabricatorEnv::getProductionURI($lease->getURI()),
pht('Waiting for daemons to activate lease...'));
- $lease->waitUntilActive();
+ $this->waitUntilActive($lease);
+
+ // Now that we've survived activation and the lease is good, make it
+ // durable.
+ $lease->setReleaseOnDestruction(false);
+ $t_end = microtime(true);
echo tsprintf(
- "%s\n",
- pht('Activated lease "%s".', $lease->getID()));
+ "%s\n\n %s\n\n%s\n",
+ pht(
+ 'Activation complete. This lease is permanent until manually '.
+ 'released with:'),
+ pht('$ ./bin/drydock release-lease --id %d', $lease->getID()),
+ pht(
+ 'Lease activated in %sms.',
+ new PhutilNumber((int)(($t_end - $t_start) * 1000))));
return 0;
}
+ public function didReceiveInterrupt($signo) {
+ // Doing this makes us run destructors, particularly the "release on
+ // destruction" trigger on the lease.
+ exit(128 + $signo);
+ }
+
+ private function waitUntilActive(DrydockLease $lease) {
+ $viewer = $this->getViewer();
+
+ $log_cursor = 0;
+ $log_types = DrydockLogType::getAllLogTypes();
+
+ $is_active = false;
+ while (!$is_active) {
+ $lease->reload();
+
+ // While we're waiting, show the user any logs which the daemons have
+ // generated to give them some clue about what's going on.
+ $logs = id(new DrydockLogQuery())
+ ->setViewer($viewer)
+ ->withLeasePHIDs(array($lease->getPHID()))
+ ->setBeforeID($log_cursor)
+ ->execute();
+ if ($logs) {
+ $logs = mpull($logs, null, 'getID');
+ ksort($logs);
+ $log_cursor = last_key($logs);
+ }
+
+ foreach ($logs as $log) {
+ $type_key = $log->getType();
+ if (isset($log_types[$type_key])) {
+ $type_object = id(clone $log_types[$type_key])
+ ->setLog($log)
+ ->setViewer($viewer);
+
+ $log_data = $log->getData();
+
+ $type = $type_object->getLogTypeName();
+ $data = $type_object->renderLogForText($log_data);
+ } else {
+ $type = pht('Unknown ("%s")', $type_key);
+ $data = null;
+ }
+
+ echo tsprintf(
+ "<%s> %B\n",
+ $type,
+ $data);
+ }
+
+ $status = $lease->getStatus();
+
+ switch ($status) {
+ case DrydockLeaseStatus::STATUS_ACTIVE:
+ $is_active = true;
+ break;
+ case DrydockLeaseStatus::STATUS_RELEASED:
+ throw new Exception(pht('Lease has already been released!'));
+ case DrydockLeaseStatus::STATUS_DESTROYED:
+ throw new Exception(pht('Lease has already been destroyed!'));
+ case DrydockLeaseStatus::STATUS_BROKEN:
+ throw new Exception(pht('Lease has been broken!'));
+ case DrydockLeaseStatus::STATUS_PENDING:
+ case DrydockLeaseStatus::STATUS_ACQUIRED:
+ break;
+ default:
+ throw new Exception(
+ pht(
+ 'Lease has unknown status "%s".',
+ $status));
+ }
+
+ if ($is_active) {
+ break;
+ } else {
+ sleep(1);
+ }
+ }
+ }
+
}
diff --git a/src/applications/drydock/storage/DrydockLease.php b/src/applications/drydock/storage/DrydockLease.php
index e60529afe..4cee7a5f1 100644
--- a/src/applications/drydock/storage/DrydockLease.php
+++ b/src/applications/drydock/storage/DrydockLease.php
@@ -1,503 +1,518 @@
<?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;
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 releaseOnDestruction() {
- $this->releaseOnDestruction = true;
+ 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 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?',
),
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 isActivating() {
- switch ($this->getStatus()) {
- case DrydockLeaseStatus::STATUS_PENDING:
- case DrydockLeaseStatus::STATUS_ACQUIRED:
- return true;
- }
-
- return false;
- }
-
- public function isActive() {
- switch ($this->getStatus()) {
- case DrydockLeaseStatus::STATUS_ACTIVE:
- return true;
- }
-
- return false;
- }
-
- public function waitUntilActive() {
- while (true) {
- $lease = $this->reload();
- if (!$lease) {
- throw new Exception(pht('Failed to reload lease.'));
- }
-
- $status = $lease->getStatus();
-
- switch ($status) {
- case DrydockLeaseStatus::STATUS_ACTIVE:
- return;
- case DrydockLeaseStatus::STATUS_RELEASED:
- throw new Exception(pht('Lease has already been released!'));
- case DrydockLeaseStatus::STATUS_DESTROYED:
- throw new Exception(pht('Lease has already been destroyed!'));
- case DrydockLeaseStatus::STATUS_BROKEN:
- throw new Exception(pht('Lease has been broken!'));
- case DrydockLeaseStatus::STATUS_PENDING:
- case DrydockLeaseStatus::STATUS_ACQUIRED:
- break;
- default:
- throw new Exception(
- pht(
- 'Lease has unknown status "%s".',
- $status));
- }
-
- sleep(1);
- }
- }
-
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.'));
}
}
- $this->openTransaction();
+ // 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 {
- DrydockSlotLock::acquireLocks($this->getPHID(), $this->slotLocks);
- $this->slotLocks = array();
- } catch (DrydockSlotLockException $ex) {
- $this->killTransaction();
+ $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()));
+ }
- $this->logEvent(
- DrydockSlotLockFailureLogType::LOGCONST,
- array(
- 'locks' => $ex->getLockMap(),
- ));
+ $resource->reload();
- throw $ex;
+ 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()));
}
- $this
- ->setResourcePHID($resource->getPHID())
- ->attachResource($resource)
- ->setStatus($new_status)
- ->save();
+ $caught = null;
+ try {
+ $this->openTransaction();
- $this->saveTransaction();
+ 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 canRelease() {
- if (!$this->getID()) {
- return false;
- }
-
- switch ($this->getStatus()) {
- case DrydockLeaseStatus::STATUS_RELEASED:
- case DrydockLeaseStatus::STATUS_DESTROYED:
- return false;
- default:
- return true;
- }
- }
-
- public function canReceiveCommands() {
- switch ($this->getStatus()) {
- case DrydockLeaseStatus::STATUS_RELEASED:
- case DrydockLeaseStatus::STATUS_DESTROYED:
- return false;
- default:
- return true;
- }
- }
-
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/DrydockResource.php b/src/applications/drydock/storage/DrydockResource.php
index 3eceec8b7..8ec63cb09 100644
--- a/src/applications/drydock/storage/DrydockResource.php
+++ b/src/applications/drydock/storage/DrydockResource.php
@@ -1,341 +1,343 @@
<?php
final class DrydockResource extends DrydockDAO
implements PhabricatorPolicyInterface {
protected $id;
protected $phid;
protected $blueprintPHID;
protected $status;
protected $until;
protected $type;
protected $attributes = array();
protected $capabilities = array();
protected $ownerPHID;
private $blueprint = self::ATTACHABLE;
private $unconsumedCommands = self::ATTACHABLE;
private $isAllocated = false;
private $isActivated = false;
private $activateWhenAllocated = false;
private $slotLocks = array();
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'attributes' => self::SERIALIZATION_JSON,
'capabilities' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'ownerPHID' => 'phid?',
'status' => 'text32',
'type' => 'text64',
'until' => 'epoch?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_type' => array(
'columns' => array('type', 'status'),
),
'key_blueprint' => array(
'columns' => array('blueprintPHID', 'status'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(DrydockResourcePHIDType::TYPECONST);
}
public function getResourceName() {
return $this->getBlueprint()->getResourceName($this);
}
public function getAttribute($key, $default = null) {
return idx($this->attributes, $key, $default);
}
public function getAttributesForTypeSpec(array $attribute_names) {
return array_select_keys($this->attributes, $attribute_names);
}
public function setAttribute($key, $value) {
$this->attributes[$key] = $value;
return $this;
}
public function getCapability($key, $default = null) {
return idx($this->capbilities, $key, $default);
}
public function getInterface(DrydockLease $lease, $type) {
return $this->getBlueprint()->getInterface($this, $lease, $type);
}
public function getBlueprint() {
return $this->assertAttached($this->blueprint);
}
public function attachBlueprint(DrydockBlueprint $blueprint) {
$this->blueprint = $blueprint;
return $this;
}
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 setActivateWhenAllocated($activate) {
$this->activateWhenAllocated = $activate;
return $this;
}
public function needSlotLock($key) {
$this->slotLocks[] = $key;
return $this;
}
public function allocateResource() {
// We expect resources to have a pregenerated PHID, as they should have
// been created by a call to DrydockBlueprint->newResourceTemplate().
if (!$this->getPHID()) {
throw new Exception(
pht(
'Trying to allocate a resource with no generated PHID. Use "%s" to '.
'create new resource templates.',
'newResourceTemplate()'));
}
$expect_status = DrydockResourceStatus::STATUS_PENDING;
$actual_status = $this->getStatus();
if ($actual_status != $expect_status) {
throw new Exception(
pht(
'Trying to allocate a resource from the wrong status. Status must '.
'be "%s", actually "%s".',
$expect_status,
$actual_status));
}
if ($this->activateWhenAllocated) {
$new_status = DrydockResourceStatus::STATUS_ACTIVE;
} else {
$new_status = DrydockResourceStatus::STATUS_PENDING;
}
$this->openTransaction();
try {
DrydockSlotLock::acquireLocks($this->getPHID(), $this->slotLocks);
$this->slotLocks = array();
} catch (DrydockSlotLockException $ex) {
$this->killTransaction();
if ($this->getID()) {
$log_target = $this;
} else {
// If we don't have an ID, we have to log this on the blueprint, as the
// resource is not going to be saved so the PHID will vanish.
$log_target = $this->getBlueprint();
}
$log_target->logEvent(
DrydockSlotLockFailureLogType::LOGCONST,
array(
'locks' => $ex->getLockMap(),
));
throw $ex;
}
$this
->setStatus($new_status)
->save();
$this->saveTransaction();
$this->isAllocated = true;
if ($new_status == DrydockResourceStatus::STATUS_ACTIVE) {
$this->didActivate();
}
return $this;
}
public function isAllocatedResource() {
return $this->isAllocated;
}
public function activateResource() {
if (!$this->getID()) {
throw new Exception(
pht(
'Trying to activate a resource which has not yet been persisted.'));
}
$expect_status = DrydockResourceStatus::STATUS_PENDING;
$actual_status = $this->getStatus();
if ($actual_status != $expect_status) {
throw new Exception(
pht(
'Trying to activate a resource from the wrong status. Status must '.
'be "%s", actually "%s".',
$expect_status,
$actual_status));
}
$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(DrydockResourceStatus::STATUS_ACTIVE)
->save();
$this->saveTransaction();
$this->isActivated = true;
$this->didActivate();
return $this;
}
public function isActivatedResource() {
return $this->isActivated;
}
- public function canRelease() {
- switch ($this->getStatus()) {
- case DrydockResourceStatus::STATUS_RELEASED:
- case DrydockResourceStatus::STATUS_DESTROYED:
- return false;
- default:
- return true;
- }
- }
-
public function scheduleUpdate($epoch = null) {
PhabricatorWorker::scheduleTask(
'DrydockResourceUpdateWorker',
array(
'resourcePHID' => $this->getPHID(),
'isExpireTask' => ($epoch !== null),
),
array(
'objectPHID' => $this->getPHID(),
'delayUntil' => ($epoch ? (int)$epoch : null),
));
}
private function didActivate() {
$viewer = PhabricatorUser::getOmnipotentUser();
$need_update = false;
$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);
}
}
- public function canReceiveCommands() {
- switch ($this->getStatus()) {
- case DrydockResourceStatus::STATUS_RELEASED:
- case DrydockResourceStatus::STATUS_BROKEN:
- case DrydockResourceStatus::STATUS_DESTROYED:
- return false;
- default:
- return true;
- }
- }
-
- public function isActive() {
- switch ($this->getStatus()) {
- case DrydockResourceStatus::STATUS_ACTIVE:
- return true;
- }
-
- return false;
- }
-
public function logEvent($type, array $data = array()) {
$log = id(new DrydockLog())
->setEpoch(PhabricatorTime::getNow())
->setType($type)
->setData($data);
$log->setResourcePHID($this->getPHID());
$log->setBlueprintPHID($this->getBlueprintPHID());
return $log->save();
}
+/* -( Status )------------------------------------------------------------- */
+
+
+ public function getStatusObject() {
+ return DrydockResourceStatus::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 canRelease() {
+ return $this->getStatusObject()->canRelease();
+ }
+
+ public function canReceiveCommands() {
+ return $this->getStatusObject()->canReceiveCommands();
+ }
+
+ public function isActive() {
+ return $this->getStatusObject()->isActive();
+ }
+
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return $this->getBlueprint()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getBlueprint()->hasAutomaticCapability(
$capability,
$viewer);
}
public function describeAutomaticCapability($capability) {
return pht('Resources inherit the policies of their blueprints.');
}
}
diff --git a/src/applications/drydock/view/DrydockLeaseListView.php b/src/applications/drydock/view/DrydockLeaseListView.php
index 8c161b00c..fb11457e8 100644
--- a/src/applications/drydock/view/DrydockLeaseListView.php
+++ b/src/applications/drydock/view/DrydockLeaseListView.php
@@ -1,50 +1,50 @@
<?php
final class DrydockLeaseListView extends AphrontView {
private $leases;
public function setLeases(array $leases) {
assert_instances_of($leases, 'DrydockLease');
$this->leases = $leases;
return $this;
}
public function render() {
$leases = $this->leases;
$viewer = $this->getUser();
$view = new PHUIObjectItemListView();
foreach ($leases as $lease) {
$item = id(new PHUIObjectItemView())
->setUser($viewer)
->setHeader($lease->getLeaseName())
->setHref('/drydock/lease/'.$lease->getID().'/');
$resource_phid = $lease->getResourcePHID();
if ($resource_phid) {
$item->addAttribute(
$viewer->renderHandle($resource_phid));
+ } else {
+ $item->addAttribute(
+ pht(
+ 'Resource: %s',
+ $lease->getResourceType()));
}
- $status = DrydockLeaseStatus::getNameForStatus($lease->getStatus());
- $item->addAttribute($status);
$item->setEpoch($lease->getDateCreated());
- // TODO: Tailor this for clarity.
- if ($lease->isActivating()) {
- $item->setStatusIcon('fa-dot-circle-o yellow');
- } else if ($lease->isActive()) {
- $item->setStatusIcon('fa-dot-circle-o green');
- } else {
- $item->setStatusIcon('fa-dot-circle-o red');
- }
+ $icon = $lease->getStatusIcon();
+ $color = $lease->getStatusColor();
+ $label = $lease->getStatusDisplayName();
+
+ $item->setStatusIcon("{$icon} {$color}", $label);
$view->addItem($item);
}
return $view;
}
}
diff --git a/src/applications/drydock/view/DrydockLogListView.php b/src/applications/drydock/view/DrydockLogListView.php
index 845457f9b..61fde7cee 100644
--- a/src/applications/drydock/view/DrydockLogListView.php
+++ b/src/applications/drydock/view/DrydockLogListView.php
@@ -1,107 +1,108 @@
<?php
final class DrydockLogListView extends AphrontView {
private $logs;
public function setLogs(array $logs) {
assert_instances_of($logs, 'DrydockLog');
$this->logs = $logs;
return $this;
}
public function render() {
$logs = $this->logs;
$viewer = $this->getUser();
$view = new PHUIObjectItemListView();
$types = DrydockLogType::getAllLogTypes();
$rows = array();
foreach ($logs as $log) {
$blueprint_phid = $log->getBlueprintPHID();
if ($blueprint_phid) {
$blueprint = $viewer->renderHandle($blueprint_phid);
} else {
$blueprint = null;
}
$resource_phid = $log->getResourcePHID();
if ($resource_phid) {
$resource = $viewer->renderHandle($resource_phid);
} else {
$resource = null;
}
$lease_phid = $log->getLeasePHID();
if ($lease_phid) {
$lease = $viewer->renderHandle($lease_phid);
} else {
$lease = null;
}
if ($log->isComplete()) {
$type_key = $log->getType();
if (isset($types[$type_key])) {
$type_object = id(clone $types[$type_key])
->setLog($log)
->setViewer($viewer);
$log_data = $log->getData();
$type = $type_object->getLogTypeName();
$icon = $type_object->getLogTypeIcon($log_data);
- $data = $type_object->renderLog($log_data);
+ $data = $type_object->renderLogForHTML($log_data);
+ $data = phutil_escape_html_newlines($data);
} else {
$type = pht('<Unknown: %s>', $type_key);
$data = null;
$icon = 'fa-question-circle red';
}
} else {
$type = phutil_tag('em', array(), pht('Restricted'));
$data = phutil_tag(
'em',
array(),
pht('You do not have permission to view this log event.'));
$icon = 'fa-lock grey';
}
$rows[] = array(
$blueprint,
$resource,
$lease,
id(new PHUIIconView())->setIcon($icon),
$type,
$data,
phabricator_datetime($log->getEpoch(), $viewer),
);
}
$table = id(new AphrontTableView($rows))
->setDeviceReadyTable(true)
->setHeaders(
array(
pht('Blueprint'),
pht('Resource'),
pht('Lease'),
null,
pht('Type'),
pht('Data'),
pht('Date'),
))
->setColumnClasses(
array(
'',
'',
'',
'icon',
'',
'wide',
'',
));
return $table;
}
}
diff --git a/src/applications/drydock/view/DrydockResourceListView.php b/src/applications/drydock/view/DrydockResourceListView.php
index 739b464bd..f2d105ecc 100644
--- a/src/applications/drydock/view/DrydockResourceListView.php
+++ b/src/applications/drydock/view/DrydockResourceListView.php
@@ -1,50 +1,38 @@
<?php
final class DrydockResourceListView extends AphrontView {
private $resources;
public function setResources(array $resources) {
assert_instances_of($resources, 'DrydockResource');
$this->resources = $resources;
return $this;
}
public function render() {
$resources = $this->resources;
$viewer = $this->getUser();
$view = new PHUIObjectItemListView();
foreach ($resources as $resource) {
$id = $resource->getID();
$item = id(new PHUIObjectItemView())
->setHref("/drydock/resource/{$id}/")
->setObjectName(pht('Resource %d', $id))
->setHeader($resource->getResourceName());
- $status = DrydockResourceStatus::getNameForStatus($resource->getStatus());
- $item->addAttribute($status);
-
- switch ($resource->getStatus()) {
- case DrydockResourceStatus::STATUS_PENDING:
- $item->setStatusIcon('fa-dot-circle-o yellow');
- break;
- case DrydockResourceStatus::STATUS_ACTIVE:
- $item->setStatusIcon('fa-dot-circle-o green');
- break;
- case DrydockResourceStatus::STATUS_DESTROYED:
- $item->setStatusIcon('fa-times-circle-o black');
- break;
- default:
- $item->setStatusIcon('fa-dot-circle-o red');
- break;
- }
+ $icon = $resource->getStatusIcon();
+ $color = $resource->getStatusColor();
+ $label = $resource->getStatusDisplayName();
+
+ $item->setStatusIcon("{$icon} {$color}", $label);
$view->addItem($item);
}
return $view;
}
}
diff --git a/src/applications/drydock/worker/DrydockLeaseUpdateWorker.php b/src/applications/drydock/worker/DrydockLeaseUpdateWorker.php
index d73506f28..4a0cba8de 100644
--- a/src/applications/drydock/worker/DrydockLeaseUpdateWorker.php
+++ b/src/applications/drydock/worker/DrydockLeaseUpdateWorker.php
@@ -1,854 +1,927 @@
<?php
/**
* @task update Updating Leases
* @task command Processing Commands
* @task allocator Drydock Allocator
* @task acquire Acquiring Leases
* @task activate Activating Leases
* @task release Releasing Leases
* @task break Breaking Leases
* @task destroy Destroying Leases
*/
final class DrydockLeaseUpdateWorker extends DrydockWorker {
protected function doWork() {
$lease_phid = $this->getTaskDataValue('leasePHID');
$hash = PhabricatorHash::digestForIndex($lease_phid);
$lock_key = 'drydock.lease:'.$hash;
$lock = PhabricatorGlobalLock::newLock($lock_key)
->lock(1);
try {
$lease = $this->loadLease($lease_phid);
$this->handleUpdate($lease);
} catch (Exception $ex) {
$lock->unlock();
$this->flushDrydockTaskQueue();
throw $ex;
}
$lock->unlock();
}
/* -( Updating Leases )---------------------------------------------------- */
/**
* @task update
*/
private function handleUpdate(DrydockLease $lease) {
try {
$this->updateLease($lease);
+ } catch (DrydockAcquiredBrokenResourceException $ex) {
+ // If this lease acquired a resource but failed to activate, we don't
+ // need to break the lease. We can throw it back in the pool and let
+ // it take another shot at acquiring a new resource.
+
+ // Before we throw it back, release any locks the lease is holding.
+ DrydockSlotLock::releaseLocks($lease->getPHID());
+
+ $lease
+ ->setStatus(DrydockLeaseStatus::STATUS_PENDING)
+ ->setResourcePHID(null)
+ ->save();
+
+ $lease->logEvent(
+ DrydockLeaseReacquireLogType::LOGCONST,
+ array(
+ 'class' => get_class($ex),
+ 'message' => $ex->getMessage(),
+ ));
+
+ $this->yieldLease($lease, $ex);
} catch (Exception $ex) {
if ($this->isTemporaryException($ex)) {
$this->yieldLease($lease, $ex);
} else {
$this->breakLease($lease, $ex);
}
}
}
/**
* @task update
*/
private function updateLease(DrydockLease $lease) {
$this->processLeaseCommands($lease);
$lease_status = $lease->getStatus();
switch ($lease_status) {
case DrydockLeaseStatus::STATUS_PENDING:
$this->executeAllocator($lease);
break;
case DrydockLeaseStatus::STATUS_ACQUIRED:
$this->activateLease($lease);
break;
case DrydockLeaseStatus::STATUS_ACTIVE:
// Nothing to do.
break;
case DrydockLeaseStatus::STATUS_RELEASED:
case DrydockLeaseStatus::STATUS_BROKEN:
$this->destroyLease($lease);
break;
case DrydockLeaseStatus::STATUS_DESTROYED:
break;
}
$this->yieldIfExpiringLease($lease);
}
/**
* @task update
*/
private function yieldLease(DrydockLease $lease, Exception $ex) {
$duration = $this->getYieldDurationFromException($ex);
$lease->logEvent(
DrydockLeaseActivationYieldLogType::LOGCONST,
array(
'duration' => $duration,
));
throw new PhabricatorWorkerYieldException($duration);
}
/* -( Processing Commands )------------------------------------------------ */
/**
* @task command
*/
private function processLeaseCommands(DrydockLease $lease) {
if (!$lease->canReceiveCommands()) {
return;
}
$this->checkLeaseExpiration($lease);
$commands = $this->loadCommands($lease->getPHID());
foreach ($commands as $command) {
if (!$lease->canReceiveCommands()) {
break;
}
$this->processLeaseCommand($lease, $command);
$command
->setIsConsumed(true)
->save();
}
}
/**
* @task command
*/
private function processLeaseCommand(
DrydockLease $lease,
DrydockCommand $command) {
switch ($command->getCommand()) {
case DrydockCommand::COMMAND_RELEASE:
$this->releaseLease($lease);
break;
}
}
/* -( Drydock Allocator )-------------------------------------------------- */
/**
* Find or build a resource which can satisfy a given lease request, then
* acquire the lease.
*
* @param DrydockLease Requested lease.
* @return void
* @task allocator
*/
private function executeAllocator(DrydockLease $lease) {
$blueprints = $this->loadBlueprintsForAllocatingLease($lease);
// If we get nothing back, that means no blueprint is defined which can
// ever build the requested resource. This is a permanent failure, since
// we don't expect to succeed no matter how many times we try.
if (!$blueprints) {
throw new PhabricatorWorkerPermanentFailureException(
pht(
'No active Drydock blueprint exists which can ever allocate a '.
'resource for lease "%s".',
$lease->getPHID()));
}
// First, try to find a suitable open resource which we can acquire a new
// lease on.
$resources = $this->loadResourcesForAllocatingLease($blueprints, $lease);
// If no resources exist yet, see if we can build one.
if (!$resources) {
$usable_blueprints = $this->removeOverallocatedBlueprints(
$blueprints,
$lease);
// If we get nothing back here, some blueprint claims it can eventually
// satisfy the lease, just not right now. This is a temporary failure,
// and we expect allocation to succeed eventually.
if (!$usable_blueprints) {
$blueprints = $this->rankBlueprints($blueprints, $lease);
// Try to actively reclaim unused resources. If we succeed, jump back
// into the queue in an effort to claim it.
foreach ($blueprints as $blueprint) {
$reclaimed = $this->reclaimResources($blueprint, $lease);
if ($reclaimed) {
$lease->logEvent(
DrydockLeaseReclaimLogType::LOGCONST,
array(
'resourcePHIDs' => array($reclaimed->getPHID()),
));
throw new PhabricatorWorkerYieldException(15);
}
}
$lease->logEvent(
DrydockLeaseWaitingForResourcesLogType::LOGCONST,
array(
'blueprintPHIDs' => mpull($blueprints, 'getPHID'),
));
throw new PhabricatorWorkerYieldException(15);
}
$usable_blueprints = $this->rankBlueprints($usable_blueprints, $lease);
$exceptions = array();
foreach ($usable_blueprints as $blueprint) {
try {
$resources[] = $this->allocateResource($blueprint, $lease);
// Bail after allocating one resource, we don't need any more than
// this.
break;
} catch (Exception $ex) {
+ // This failure is not normally expected, so log it. It can be
+ // caused by something mundane and recoverable, however (see below
+ // for discussion).
+
+ // We log to the blueprint separately from the log to the lease:
+ // the lease is not attached to a blueprint yet so the lease log
+ // will not show up on the blueprint; more than one blueprint may
+ // fail; and the lease is not really impacted (and won't log) if at
+ // least one blueprint actually works.
+
+ $blueprint->logEvent(
+ DrydockResourceAllocationFailureLogType::LOGCONST,
+ array(
+ 'class' => get_class($ex),
+ 'message' => $ex->getMessage(),
+ ));
+
$exceptions[] = $ex;
}
}
if (!$resources) {
- throw new PhutilAggregateException(
+ // If one or more blueprints claimed that they would be able to
+ // allocate resources but none are actually able to allocate resources,
+ // log the failure and yield so we try again soon.
+
+ // This can happen if some unexpected issue occurs during allocation
+ // (for example, a call to build a VM fails for some reason) or if we
+ // raced another allocator and the blueprint is now full.
+
+ $ex = new PhutilAggregateException(
pht(
'All blueprints failed to allocate a suitable new resource when '.
- 'trying to allocate lease "%s".',
+ 'trying to allocate lease ("%s").',
$lease->getPHID()),
$exceptions);
+
+ $lease->logEvent(
+ DrydockLeaseAllocationFailureLogType::LOGCONST,
+ array(
+ 'class' => get_class($ex),
+ 'message' => $ex->getMessage(),
+ ));
+
+ throw new PhabricatorWorkerYieldException(15);
}
$resources = $this->removeUnacquirableResources($resources, $lease);
if (!$resources) {
// If we make it here, we just built a resource but aren't allowed
// to acquire it. We expect this during routine operation if the
// resource prevents acquisition until it activates. Yield and wait
// for activation.
throw new PhabricatorWorkerYieldException(15);
}
// NOTE: We have not acquired the lease yet, so it is possible that the
// resource we just built will be snatched up by some other lease before
// we can acquire it. This is not problematic: we'll retry a little later
// and should succeed eventually.
}
$resources = $this->rankResources($resources, $lease);
$exceptions = array();
+ $yields = array();
$allocated = false;
foreach ($resources as $resource) {
try {
$this->acquireLease($resource, $lease);
$allocated = true;
break;
+ } catch (DrydockResourceLockException $ex) {
+ // We need to lock the resource to actually acquire it. If we aren't
+ // able to acquire the lock quickly enough, we can yield and try again
+ // later.
+ $yields[] = $ex;
+ } catch (DrydockAcquiredBrokenResourceException $ex) {
+ // If a resource was reclaimed or destroyed by the time we actually
+ // got around to acquiring it, we just got unlucky. We can yield and
+ // try again later.
+ $yields[] = $ex;
} catch (Exception $ex) {
$exceptions[] = $ex;
}
}
if (!$allocated) {
- throw new PhutilAggregateException(
- pht(
- 'Unable to acquire lease "%s" on any resource.',
- $lease->getPHID()),
- $exceptions);
+ if ($yields) {
+ throw new PhabricatorWorkerYieldException(15);
+ } else {
+ throw new PhutilAggregateException(
+ pht(
+ 'Unable to acquire lease "%s" on any resource.',
+ $lease->getPHID()),
+ $exceptions);
+ }
}
}
/**
* Get all the @{class:DrydockBlueprintImplementation}s which can possibly
* build a resource to satisfy a lease.
*
* This method returns blueprints which might, at some time, be able to
* build a resource which can satisfy the lease. They may not be able to
* build that resource right now.
*
* @param DrydockLease Requested lease.
* @return list<DrydockBlueprintImplementation> List of qualifying blueprint
* implementations.
* @task allocator
*/
private function loadBlueprintImplementationsForAllocatingLease(
DrydockLease $lease) {
$impls = DrydockBlueprintImplementation::getAllBlueprintImplementations();
$keep = array();
foreach ($impls as $key => $impl) {
// Don't use disabled blueprint types.
if (!$impl->isEnabled()) {
continue;
}
// Don't use blueprint types which can't allocate the correct kind of
// resource.
if ($impl->getType() != $lease->getResourceType()) {
continue;
}
if (!$impl->canAnyBlueprintEverAllocateResourceForLease($lease)) {
continue;
}
$keep[$key] = $impl;
}
return $keep;
}
/**
* Get all the concrete @{class:DrydockBlueprint}s which can possibly
* build a resource to satisfy a lease.
*
* @param DrydockLease Requested lease.
* @return list<DrydockBlueprint> List of qualifying blueprints.
* @task allocator
*/
private function loadBlueprintsForAllocatingLease(
DrydockLease $lease) {
$viewer = $this->getViewer();
$impls = $this->loadBlueprintImplementationsForAllocatingLease($lease);
if (!$impls) {
return array();
}
$blueprint_phids = $lease->getAllowedBlueprintPHIDs();
if (!$blueprint_phids) {
$lease->logEvent(DrydockLeaseNoBlueprintsLogType::LOGCONST);
return array();
}
$query = id(new DrydockBlueprintQuery())
->setViewer($viewer)
->withPHIDs($blueprint_phids)
->withBlueprintClasses(array_keys($impls))
->withDisabled(false);
// The Drydock application itself is allowed to authorize anything. This
// is primarily used for leases generated by CLI administrative tools.
$drydock_phid = id(new PhabricatorDrydockApplication())->getPHID();
$authorizing_phid = $lease->getAuthorizingPHID();
if ($authorizing_phid != $drydock_phid) {
$blueprints = id(clone $query)
->withAuthorizedPHIDs(array($authorizing_phid))
->execute();
if (!$blueprints) {
// If we didn't hit any blueprints, check if this is an authorization
// problem: re-execute the query without the authorization constraint.
// If the second query hits blueprints, the overall configuration is
// fine but this is an authorization problem. If the second query also
// comes up blank, this is some other kind of configuration issue so
// we fall through to the default pathway.
$all_blueprints = $query->execute();
if ($all_blueprints) {
$lease->logEvent(
DrydockLeaseNoAuthorizationsLogType::LOGCONST,
array(
'authorizingPHID' => $authorizing_phid,
));
return array();
}
}
} else {
$blueprints = $query->execute();
}
$keep = array();
foreach ($blueprints as $key => $blueprint) {
if (!$blueprint->canEverAllocateResourceForLease($lease)) {
continue;
}
$keep[$key] = $blueprint;
}
return $keep;
}
/**
* Load a list of all resources which a given lease can possibly be
* allocated against.
*
* @param list<DrydockBlueprint> Blueprints which may produce suitable
* resources.
* @param DrydockLease Requested lease.
* @return list<DrydockResource> Resources which may be able to allocate
* the lease.
* @task allocator
*/
private function loadResourcesForAllocatingLease(
array $blueprints,
DrydockLease $lease) {
assert_instances_of($blueprints, 'DrydockBlueprint');
$viewer = $this->getViewer();
$resources = id(new DrydockResourceQuery())
->setViewer($viewer)
->withBlueprintPHIDs(mpull($blueprints, 'getPHID'))
->withTypes(array($lease->getResourceType()))
->withStatuses(
array(
DrydockResourceStatus::STATUS_PENDING,
DrydockResourceStatus::STATUS_ACTIVE,
))
->execute();
return $this->removeUnacquirableResources($resources, $lease);
}
/**
* Remove resources which can not be acquired by a given lease from a list.
*
* @param list<DrydockResource> Candidate resources.
* @param DrydockLease Acquiring lease.
* @return list<DrydockResource> Resources which the lease may be able to
* acquire.
* @task allocator
*/
private function removeUnacquirableResources(
array $resources,
DrydockLease $lease) {
$keep = array();
foreach ($resources as $key => $resource) {
$blueprint = $resource->getBlueprint();
if (!$blueprint->canAcquireLeaseOnResource($resource, $lease)) {
continue;
}
$keep[$key] = $resource;
}
return $keep;
}
/**
* Remove blueprints which are too heavily allocated to build a resource for
* a lease from a list of blueprints.
*
* @param list<DrydockBlueprint> List of blueprints.
* @return list<DrydockBlueprint> List with blueprints that can not allocate
* a resource for the lease right now removed.
* @task allocator
*/
private function removeOverallocatedBlueprints(
array $blueprints,
DrydockLease $lease) {
assert_instances_of($blueprints, 'DrydockBlueprint');
$keep = array();
foreach ($blueprints as $key => $blueprint) {
if (!$blueprint->canAllocateResourceForLease($lease)) {
continue;
}
$keep[$key] = $blueprint;
}
return $keep;
}
/**
* Rank blueprints by suitability for building a new resource for a
* particular lease.
*
* @param list<DrydockBlueprint> List of blueprints.
* @param DrydockLease Requested lease.
* @return list<DrydockBlueprint> Ranked list of blueprints.
* @task allocator
*/
private function rankBlueprints(array $blueprints, DrydockLease $lease) {
assert_instances_of($blueprints, 'DrydockBlueprint');
// TODO: Implement improvements to this ranking algorithm if they become
// available.
shuffle($blueprints);
return $blueprints;
}
/**
* Rank resources by suitability for allocating a particular lease.
*
* @param list<DrydockResource> List of resources.
* @param DrydockLease Requested lease.
* @return list<DrydockResource> Ranked list of resources.
* @task allocator
*/
private function rankResources(array $resources, DrydockLease $lease) {
assert_instances_of($resources, 'DrydockResource');
// TODO: Implement improvements to this ranking algorithm if they become
// available.
shuffle($resources);
return $resources;
}
/**
* Perform an actual resource allocation with a particular blueprint.
*
* @param DrydockBlueprint The blueprint to allocate a resource from.
* @param DrydockLease Requested lease.
* @return DrydockResource Allocated resource.
* @task allocator
*/
private function allocateResource(
DrydockBlueprint $blueprint,
DrydockLease $lease) {
$resource = $blueprint->allocateResource($lease);
$this->validateAllocatedResource($blueprint, $resource, $lease);
// If this resource was allocated as a pending resource, queue a task to
// activate it.
if ($resource->getStatus() == DrydockResourceStatus::STATUS_PENDING) {
PhabricatorWorker::scheduleTask(
'DrydockResourceUpdateWorker',
array(
'resourcePHID' => $resource->getPHID(),
),
array(
'objectPHID' => $resource->getPHID(),
));
}
return $resource;
}
/**
* Check that the resource a blueprint allocated is roughly the sort of
* object we expect.
*
* @param DrydockBlueprint Blueprint which built the resource.
* @param wild Thing which the blueprint claims is a valid resource.
* @param DrydockLease Lease the resource was allocated for.
* @return void
* @task allocator
*/
private function validateAllocatedResource(
DrydockBlueprint $blueprint,
$resource,
DrydockLease $lease) {
if (!($resource instanceof DrydockResource)) {
throw new Exception(
pht(
'Blueprint "%s" (of type "%s") is not properly implemented: %s must '.
'return an object of type %s or throw, but returned something else.',
$blueprint->getBlueprintName(),
$blueprint->getClassName(),
'allocateResource()',
'DrydockResource'));
}
if (!$resource->isAllocatedResource()) {
throw new Exception(
pht(
'Blueprint "%s" (of type "%s") is not properly implemented: %s '.
'must actually allocate the resource it returns.',
$blueprint->getBlueprintName(),
$blueprint->getClassName(),
'allocateResource()'));
}
$resource_type = $resource->getType();
$lease_type = $lease->getResourceType();
if ($resource_type !== $lease_type) {
throw new Exception(
pht(
'Blueprint "%s" (of type "%s") is not properly implemented: it '.
'built a resource of type "%s" to satisfy a lease requesting a '.
'resource of type "%s".',
$blueprint->getBlueprintName(),
$blueprint->getClassName(),
$resource_type,
$lease_type));
}
}
private function reclaimResources(
DrydockBlueprint $blueprint,
DrydockLease $lease) {
$viewer = $this->getViewer();
$resources = id(new DrydockResourceQuery())
->setViewer($viewer)
->withBlueprintPHIDs(array($blueprint->getPHID()))
->withStatuses(
array(
DrydockResourceStatus::STATUS_ACTIVE,
))
->execute();
// TODO: We could be much smarter about this and try to release long-unused
// resources, resources with many similar copies, old resources, resources
// that are cheap to rebuild, etc.
shuffle($resources);
foreach ($resources as $resource) {
if ($this->canReclaimResource($resource)) {
$this->reclaimResource($resource, $lease);
return $resource;
}
}
return null;
}
/* -( Acquiring Leases )--------------------------------------------------- */
/**
* Perform an actual lease acquisition on a particular resource.
*
* @param DrydockResource Resource to acquire a lease on.
* @param DrydockLease Lease to acquire.
* @return void
* @task acquire
*/
private function acquireLease(
DrydockResource $resource,
DrydockLease $lease) {
$blueprint = $resource->getBlueprint();
$blueprint->acquireLease($resource, $lease);
$this->validateAcquiredLease($blueprint, $resource, $lease);
// If this lease has been acquired but not activated, queue a task to
// activate it.
if ($lease->getStatus() == DrydockLeaseStatus::STATUS_ACQUIRED) {
$this->queueTask(
__CLASS__,
array(
'leasePHID' => $lease->getPHID(),
),
array(
'objectPHID' => $lease->getPHID(),
));
}
}
/**
* Make sure that a lease was really acquired properly.
*
* @param DrydockBlueprint Blueprint which created the resource.
* @param DrydockResource Resource which was acquired.
* @param DrydockLease The lease which was supposedly acquired.
* @return void
* @task acquire
*/
private function validateAcquiredLease(
DrydockBlueprint $blueprint,
DrydockResource $resource,
DrydockLease $lease) {
if (!$lease->isAcquiredLease()) {
throw new Exception(
pht(
'Blueprint "%s" (of type "%s") is not properly implemented: it '.
'returned from "%s" without acquiring a lease.',
$blueprint->getBlueprintName(),
$blueprint->getClassName(),
'acquireLease()'));
}
$lease_phid = $lease->getResourcePHID();
$resource_phid = $resource->getPHID();
if ($lease_phid !== $resource_phid) {
throw new Exception(
pht(
'Blueprint "%s" (of type "%s") is not properly implemented: it '.
'returned from "%s" with a lease acquired on the wrong resource.',
$blueprint->getBlueprintName(),
$blueprint->getClassName(),
'acquireLease()'));
}
}
/* -( Activating Leases )-------------------------------------------------- */
/**
* @task activate
*/
private function activateLease(DrydockLease $lease) {
$resource = $lease->getResource();
if (!$resource) {
throw new Exception(
pht('Trying to activate lease with no resource.'));
}
$resource_status = $resource->getStatus();
if ($resource_status == DrydockResourceStatus::STATUS_PENDING) {
throw new PhabricatorWorkerYieldException(15);
}
if ($resource_status != DrydockResourceStatus::STATUS_ACTIVE) {
- throw new Exception(
+ throw new DrydockAcquiredBrokenResourceException(
pht(
- 'Trying to activate lease on a dead resource (in status "%s").',
+ 'Trying to activate lease ("%s") on a resource ("%s") in '.
+ 'the wrong status ("%s").',
+ $lease->getPHID(),
+ $resource->getPHID(),
$resource_status));
}
// NOTE: We can race resource destruction here. Between the time we
// performed the read above and now, the resource might have closed, so
// we may activate leases on dead resources. At least for now, this seems
// fine: a resource dying right before we activate a lease on it should not
// be distinguishable from a resource dying right after we activate a lease
// on it. We end up with an active lease on a dead resource either way, and
// can not prevent resources dying from lightning strikes.
$blueprint = $resource->getBlueprint();
$blueprint->activateLease($resource, $lease);
$this->validateActivatedLease($blueprint, $resource, $lease);
}
/**
* @task activate
*/
private function validateActivatedLease(
DrydockBlueprint $blueprint,
DrydockResource $resource,
DrydockLease $lease) {
if (!$lease->isActivatedLease()) {
throw new Exception(
pht(
'Blueprint "%s" (of type "%s") is not properly implemented: it '.
'returned from "%s" without activating a lease.',
$blueprint->getBlueprintName(),
$blueprint->getClassName(),
'acquireLease()'));
}
}
/* -( Releasing Leases )--------------------------------------------------- */
/**
* @task release
*/
private function releaseLease(DrydockLease $lease) {
$lease
->setStatus(DrydockLeaseStatus::STATUS_RELEASED)
->save();
$lease->logEvent(DrydockLeaseReleasedLogType::LOGCONST);
$resource = $lease->getResource();
if ($resource) {
$blueprint = $resource->getBlueprint();
$blueprint->didReleaseLease($resource, $lease);
}
$this->destroyLease($lease);
}
/* -( Breaking Leases )---------------------------------------------------- */
/**
* @task break
*/
protected function breakLease(DrydockLease $lease, Exception $ex) {
switch ($lease->getStatus()) {
case DrydockLeaseStatus::STATUS_BROKEN:
case DrydockLeaseStatus::STATUS_RELEASED:
case DrydockLeaseStatus::STATUS_DESTROYED:
throw new PhutilProxyException(
pht(
'Unexpected failure while destroying lease ("%s").',
$lease->getPHID()),
$ex);
}
$lease
->setStatus(DrydockLeaseStatus::STATUS_BROKEN)
->save();
$lease->logEvent(
DrydockLeaseActivationFailureLogType::LOGCONST,
array(
'class' => get_class($ex),
'message' => $ex->getMessage(),
));
$lease->awakenTasks();
$this->queueTask(
__CLASS__,
array(
'leasePHID' => $lease->getPHID(),
),
array(
'objectPHID' => $lease->getPHID(),
));
throw new PhabricatorWorkerPermanentFailureException(
pht(
'Permanent failure while activating lease ("%s"): %s',
$lease->getPHID(),
$ex->getMessage()));
}
/* -( Destroying Leases )-------------------------------------------------- */
/**
* @task destroy
*/
private function destroyLease(DrydockLease $lease) {
$resource = $lease->getResource();
if ($resource) {
$blueprint = $resource->getBlueprint();
$blueprint->destroyLease($resource, $lease);
}
DrydockSlotLock::releaseLocks($lease->getPHID());
$lease
->setStatus(DrydockLeaseStatus::STATUS_DESTROYED)
->save();
$lease->logEvent(DrydockLeaseDestroyedLogType::LOGCONST);
$lease->awakenTasks();
}
}
diff --git a/src/applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php b/src/applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php
index 9038b3b6d..dfd603fd1 100644
--- a/src/applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php
+++ b/src/applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php
@@ -1,189 +1,189 @@
<?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->releaseOnDestruction(true);
+ $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/fact/application/PhabricatorFactApplication.php b/src/applications/fact/application/PhabricatorFactApplication.php
index 305ed3abc..6444fe700 100644
--- a/src/applications/fact/application/PhabricatorFactApplication.php
+++ b/src/applications/fact/application/PhabricatorFactApplication.php
@@ -1,38 +1,39 @@
<?php
final class PhabricatorFactApplication extends PhabricatorApplication {
public function getShortDescription() {
return pht('Chart and Analyze Data');
}
public function getName() {
return pht('Facts');
}
public function getBaseURI() {
return '/fact/';
}
public function getIcon() {
return 'fa-line-chart';
}
public function getApplicationGroup() {
return self::GROUP_UTILITIES;
}
public function isPrototype() {
return true;
}
public function getRoutes() {
return array(
'/fact/' => array(
'' => 'PhabricatorFactHomeController',
'chart/' => 'PhabricatorFactChartController',
+ 'object/(?<phid>[^/]+)/' => 'PhabricatorFactObjectController',
),
);
}
}
diff --git a/src/applications/fact/controller/PhabricatorFactChartController.php b/src/applications/fact/controller/PhabricatorFactChartController.php
index 16df8fca6..7612cf156 100644
--- a/src/applications/fact/controller/PhabricatorFactChartController.php
+++ b/src/applications/fact/controller/PhabricatorFactChartController.php
@@ -1,89 +1,97 @@
<?php
final class PhabricatorFactChartController extends PhabricatorFactController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
- $table = new PhabricatorFactRaw();
- $conn_r = $table->establishConnection('r');
- $table_name = $table->getTableName();
-
$series = $request->getStr('y1');
- $specs = PhabricatorFactSpec::newSpecsForFactTypes(
- PhabricatorFactEngine::loadAllEngines(),
- array($series));
- $spec = idx($specs, $series);
+ $facts = PhabricatorFact::getAllFacts();
+ $fact = idx($facts, $series);
+
+ if (!$fact) {
+ return new Aphront404Response();
+ }
+
+ $key_id = id(new PhabricatorFactKeyDimension())
+ ->newDimensionID($fact->getKey());
+ if (!$key_id) {
+ return new Aphront404Response();
+ }
+
+ $table = $fact->newDatapoint();
+ $conn_r = $table->establishConnection('r');
+ $table_name = $table->getTableName();
$data = queryfx_all(
$conn_r,
- 'SELECT valueX, epoch FROM %T WHERE factType = %s ORDER BY epoch ASC',
+ 'SELECT value, epoch FROM %T WHERE keyID = %d ORDER BY epoch ASC',
$table_name,
- $series);
+ $key_id);
$points = array();
$sum = 0;
foreach ($data as $key => $row) {
- $sum += (int)$row['valueX'];
+ $sum += (int)$row['value'];
$points[(int)$row['epoch']] = $sum;
}
if (!$points) {
throw new Exception('No data to show!');
}
// Limit amount of data passed to browser.
$count = count($points);
$limit = 2000;
if ($count > $limit) {
$i = 0;
$every = ceil($count / $limit);
foreach ($points as $epoch => $sum) {
$i++;
if ($i % $every && $i != $count) {
unset($points[$epoch]);
}
}
}
$x = array_keys($points);
$y = array_values($points);
$id = celerity_generate_unique_node_id();
$chart = phutil_tag(
'div',
array(
'id' => $id,
'style' => 'background: #ffffff; '.
'height: 480px; ',
),
'');
require_celerity_resource('d3');
Javelin::initBehavior('line-chart', array(
'hardpoint' => $id,
'x' => array($x),
'y' => array($y),
'xformat' => 'epoch',
'colors' => array('#0000ff'),
));
$box = id(new PHUIObjectBoxView())
- ->setHeaderText(pht('Count of %s', $spec->getName()))
+ ->setHeaderText(pht('Count of %s', $fact->getName()))
->appendChild($chart);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Chart'));
$title = pht('Chart');
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($box);
}
}
diff --git a/src/applications/fact/controller/PhabricatorFactHomeController.php b/src/applications/fact/controller/PhabricatorFactHomeController.php
index f4eeca038..82f6a0905 100644
--- a/src/applications/fact/controller/PhabricatorFactHomeController.php
+++ b/src/applications/fact/controller/PhabricatorFactHomeController.php
@@ -1,126 +1,59 @@
<?php
final class PhabricatorFactHomeController extends PhabricatorFactController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
if ($request->isFormPost()) {
$uri = new PhutilURI('/fact/chart/');
$uri->setQueryParam('y1', $request->getStr('y1'));
return id(new AphrontRedirectResponse())->setURI($uri);
}
- $types = array(
- '+N:*',
- '+N:DREV',
- 'updated',
- );
-
- $engines = PhabricatorFactEngine::loadAllEngines();
- $specs = PhabricatorFactSpec::newSpecsForFactTypes($engines, $types);
-
- $facts = id(new PhabricatorFactAggregate())->loadAllWhere(
- 'factType IN (%Ls)',
- $types);
-
- $rows = array();
- foreach ($facts as $fact) {
- $spec = $specs[$fact->getFactType()];
-
- $name = $spec->getName();
- $value = $spec->formatValueForDisplay($viewer, $fact->getValueX());
-
- $rows[] = array($name, $value);
- }
-
- $table = new AphrontTableView($rows);
- $table->setHeaders(
- array(
- pht('Fact'),
- pht('Value'),
- ));
- $table->setColumnClasses(
- array(
- 'wide',
- 'n',
- ));
-
- $panel = new PHUIObjectBoxView();
- $panel->setHeaderText(pht('Facts'));
- $panel->setTable($table);
-
$chart_form = $this->buildChartForm();
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Home'));
$title = pht('Facts');
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
- ->appendChild(array(
- $chart_form,
- $panel,
- ));
-
+ ->appendChild(
+ array(
+ $chart_form,
+ ));
}
private function buildChartForm() {
$request = $this->getRequest();
$viewer = $request->getUser();
- $table = new PhabricatorFactRaw();
- $conn_r = $table->establishConnection('r');
- $table_name = $table->getTableName();
-
- $facts = queryfx_all(
- $conn_r,
- 'SELECT DISTINCT factType from %T',
- $table_name);
-
- $specs = PhabricatorFactSpec::newSpecsForFactTypes(
- PhabricatorFactEngine::loadAllEngines(),
- ipull($facts, 'factType'));
-
- $options = array();
- foreach ($specs as $spec) {
- if ($spec->getUnit() == PhabricatorFactSpec::UNIT_COUNT) {
- $options[$spec->getType()] = $spec->getName();
- }
- }
-
- if (!$options) {
- return id(new PHUIInfoView())
- ->setSeverity(PHUIInfoView::SEVERITY_NODATA)
- ->setTitle(pht('No Chartable Facts'))
- ->appendChild(phutil_tag(
- 'p',
- array(),
- pht('There are no facts that can be plotted yet.')));
- }
+ $specs = PhabricatorFact::getAllFacts();
+ $options = mpull($specs, 'getName', 'getKey');
$form = id(new AphrontFormView())
->setUser($viewer)
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Y-Axis'))
->setName('y1')
->setOptions($options))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Plot Chart')));
$panel = new PHUIObjectBoxView();
$panel->setForm($form);
$panel->setHeaderText(pht('Plot Chart'));
return $panel;
}
}
diff --git a/src/applications/fact/controller/PhabricatorFactObjectController.php b/src/applications/fact/controller/PhabricatorFactObjectController.php
new file mode 100644
index 000000000..bffc1ef15
--- /dev/null
+++ b/src/applications/fact/controller/PhabricatorFactObjectController.php
@@ -0,0 +1,323 @@
+<?php
+
+final class PhabricatorFactObjectController
+ extends PhabricatorFactController {
+
+ public function handleRequest(AphrontRequest $request) {
+ $viewer = $request->getViewer();
+
+ $phid = $request->getURIData('phid');
+ $object = id(new PhabricatorObjectQuery())
+ ->setViewer($viewer)
+ ->withNames(array($phid))
+ ->executeOne();
+ if (!$object) {
+ return new Aphront404Response();
+ }
+
+ $engines = PhabricatorFactEngine::loadAllEngines();
+ foreach ($engines as $key => $engine) {
+ $engine = id(clone $engine)
+ ->setViewer($viewer);
+ $engines[$key] = $engine;
+
+ if (!$engine->supportsDatapointsForObject($object)) {
+ unset($engines[$key]);
+ }
+ }
+
+ if (!$engines) {
+ return $this->newDialog()
+ ->setTitle(pht('No Engines'))
+ ->appendParagraph(
+ pht(
+ 'No fact engines support generating facts for this object.'))
+ ->addCancelButton($this->getApplicationURI());
+ }
+
+ $key_dimension = new PhabricatorFactKeyDimension();
+ $object_phid = $object->getPHID();
+
+ $facts = array();
+ $generated_datapoints = array();
+ $timings = array();
+ foreach ($engines as $key => $engine) {
+ $engine_facts = $engine->newFacts();
+ $engine_facts = mpull($engine_facts, null, 'getKey');
+ $facts[$key] = $engine_facts;
+
+ $t_start = microtime(true);
+ $generated_datapoints[$key] = $engine->newDatapointsForObject($object);
+ $t_end = microtime(true);
+
+ $timings[$key] = ($t_end - $t_start);
+ }
+
+ $object_id = id(new PhabricatorFactObjectDimension())
+ ->newDimensionID($object_phid, true);
+
+ $stored_datapoints = id(new PhabricatorFactDatapointQuery())
+ ->withFacts(array_mergev($facts))
+ ->withObjectPHIDs(array($object_phid))
+ ->needVectors(true)
+ ->execute();
+
+ $stored_groups = array();
+ foreach ($stored_datapoints as $stored_datapoint) {
+ $stored_groups[$stored_datapoint['key']][] = $stored_datapoint;
+ }
+
+ $stored_map = array();
+ foreach ($engines as $key => $engine) {
+ $stored_map[$key] = array();
+ foreach ($facts[$key] as $fact) {
+ $fact_datapoints = idx($stored_groups, $fact->getKey(), array());
+ $fact_datapoints = igroup($fact_datapoints, 'vector');
+ $stored_map[$key] += $fact_datapoints;
+ }
+ }
+
+ $handle_phids = array();
+ $handle_phids[] = $object->getPHID();
+ foreach ($generated_datapoints as $key => $datapoint_set) {
+ foreach ($datapoint_set as $datapoint) {
+ $dimension_phid = $datapoint->getDimensionPHID();
+ if ($dimension_phid !== null) {
+ $handle_phids[$dimension_phid] = $dimension_phid;
+ }
+ }
+ }
+
+ foreach ($stored_map as $key => $stored_datapoints) {
+ foreach ($stored_datapoints as $vector_key => $datapoints) {
+ foreach ($datapoints as $datapoint) {
+ $dimension_phid = $datapoint['dimensionPHID'];
+ if ($dimension_phid !== null) {
+ $handle_phids[$dimension_phid] = $dimension_phid;
+ }
+ }
+ }
+ }
+
+ $handles = $viewer->loadHandles($handle_phids);
+
+ $dimension_map = id(new PhabricatorFactObjectDimension())
+ ->newDimensionMap($handle_phids, true);
+
+ $content = array();
+
+ $object_list = id(new PHUIPropertyListView())
+ ->setViewer($viewer)
+ ->addProperty(
+ pht('Object'),
+ $handles[$object->getPHID()]->renderLink());
+
+ $total_cost = array_sum($timings);
+ $total_cost = pht('%sms', new PhutilNumber((int)(1000 * $total_cost)));
+ $object_list->addProperty(pht('Total Cost'), $total_cost);
+
+ $object_box = id(new PHUIObjectBoxView())
+ ->setHeaderText(pht('Fact Extraction Report'))
+ ->addPropertyList($object_list);
+
+ $content[] = $object_box;
+
+ $icon_fact = id(new PHUIIconView())
+ ->setIcon('fa-line-chart green')
+ ->setTooltip(pht('Consistent Fact'));
+
+ $icon_nodata = id(new PHUIIconView())
+ ->setIcon('fa-question-circle-o violet')
+ ->setTooltip(pht('No Stored Datapoints'));
+
+ $icon_new = id(new PHUIIconView())
+ ->setIcon('fa-plus red')
+ ->setTooltip(pht('Not Stored'));
+
+ $icon_surplus = id(new PHUIIconView())
+ ->setIcon('fa-minus red')
+ ->setTooltip(pht('Not Generated'));
+
+ foreach ($engines as $key => $engine) {
+ $rows = array();
+ foreach ($generated_datapoints[$key] as $datapoint) {
+ $dimension_phid = $datapoint->getDimensionPHID();
+ if ($dimension_phid !== null) {
+ $dimension = $handles[$datapoint->getDimensionPHID()]->renderLink();
+ } else {
+ $dimension = null;
+ }
+
+ $fact_key = $datapoint->getKey();
+
+ $fact = idx($facts[$key], $fact_key, null);
+ if ($fact) {
+ $fact_label = $fact->getName();
+ } else {
+ $fact_label = $fact_key;
+ }
+
+ $vector_key = $datapoint->newDatapointVector();
+ if (isset($stored_map[$key][$vector_key])) {
+ unset($stored_map[$key][$vector_key]);
+ $icon = $icon_fact;
+ } else {
+ $icon = $icon_new;
+ }
+
+ $rows[] = array(
+ $icon,
+ $fact_label,
+ $dimension,
+ $datapoint->getValue(),
+ phabricator_datetime($datapoint->getEpoch(), $viewer),
+ );
+ }
+
+ foreach ($stored_map[$key] as $vector_key => $datapoints) {
+ foreach ($datapoints as $datapoint) {
+ $dimension_phid = $datapoint['dimensionPHID'];
+ if ($dimension_phid !== null) {
+ $dimension = $handles[$dimension_phid]->renderLink();
+ } else {
+ $dimension = null;
+ }
+
+ $fact_key = $datapoint['key'];
+ $fact = idx($facts[$key], $fact_key, null);
+ if ($fact) {
+ $fact_label = $fact->getName();
+ } else {
+ $fact_label = $fact_key;
+ }
+
+ $rows[] = array(
+ $icon_surplus,
+ $fact_label,
+ $dimension,
+ $datapoint['value'],
+ phabricator_datetime($datapoint['epoch'], $viewer),
+ );
+ }
+ }
+
+ foreach ($facts[$key] as $fact) {
+ $has_any = id(new PhabricatorFactDatapointQuery())
+ ->withFacts(array($fact))
+ ->setLimit(1)
+ ->execute();
+ if ($has_any) {
+ continue;
+ }
+
+ if (!$has_any) {
+ $rows[] = array(
+ $icon_nodata,
+ $fact->getName(),
+ null,
+ null,
+ null,
+ );
+ }
+ }
+
+ $table = id(new AphrontTableView($rows))
+ ->setHeaders(
+ array(
+ null,
+ pht('Fact'),
+ pht('Dimension'),
+ pht('Value'),
+ pht('Date'),
+ ))
+ ->setColumnClasses(
+ array(
+ '',
+ '',
+ '',
+ 'n wide right',
+ 'right',
+ ));
+
+ $extraction_cost = $timings[$key];
+ $extraction_cost = pht(
+ '%sms',
+ new PhutilNumber((int)(1000 * $extraction_cost)));
+
+ $header = pht(
+ '%s (%s)',
+ get_class($engine),
+ $extraction_cost);
+
+ $box = id(new PHUIObjectBoxView())
+ ->setHeaderText($header)
+ ->setTable($table);
+
+ $content[] = $box;
+
+ if ($engine instanceof PhabricatorTransactionFactEngine) {
+ $groups = $engine->newTransactionGroupsForObject($object);
+ $groups = array_values($groups);
+
+ $xaction_phids = array();
+ foreach ($groups as $group_key => $xactions) {
+ foreach ($xactions as $xaction) {
+ $xaction_phids[] = $xaction->getAuthorPHID();
+ }
+ }
+ $xaction_handles = $viewer->loadHandles($xaction_phids);
+
+ $rows = array();
+ foreach ($groups as $group_key => $xactions) {
+ foreach ($xactions as $xaction) {
+ $rows[] = array(
+ $group_key,
+ $xaction->getTransactionType(),
+ $xaction_handles[$xaction->getAuthorPHID()]->renderLink(),
+ phabricator_datetime($xaction->getDateCreated(), $viewer),
+ );
+ }
+ }
+
+ $table = id(new AphrontTableView($rows))
+ ->setHeaders(
+ array(
+ pht('Group'),
+ pht('Type'),
+ pht('Author'),
+ pht('Date'),
+ ))
+ ->setColumnClasses(
+ array(
+ null,
+ 'pri',
+ 'wide',
+ 'right',
+ ));
+
+ $header = pht(
+ '%s (Transactions)',
+ get_class($engine));
+
+ $xaction_box = id(new PHUIObjectBoxView())
+ ->setHeaderText($header)
+ ->setTable($table);
+
+ $content[] = $xaction_box;
+ }
+
+ }
+
+ $crumbs = $this->buildApplicationCrumbs()
+ ->addTextCrumb(pht('Chart'));
+
+ $title = pht('Chart');
+
+ return $this->newPage()
+ ->setTitle($title)
+ ->setCrumbs($crumbs)
+ ->appendChild($content);
+
+ }
+
+}
diff --git a/src/applications/fact/daemon/PhabricatorFactDaemon.php b/src/applications/fact/daemon/PhabricatorFactDaemon.php
index 384bb56df..3fa6c6f0f 100644
--- a/src/applications/fact/daemon/PhabricatorFactDaemon.php
+++ b/src/applications/fact/daemon/PhabricatorFactDaemon.php
@@ -1,207 +1,201 @@
<?php
final class PhabricatorFactDaemon extends PhabricatorDaemon {
private $engines;
- const RAW_FACT_BUFFER_LIMIT = 128;
-
protected function run() {
$this->setEngines(PhabricatorFactEngine::loadAllEngines());
while (!$this->shouldExit()) {
PhabricatorCaches::destroyRequestCache();
$iterators = $this->getAllApplicationIterators();
foreach ($iterators as $iterator_name => $iterator) {
$this->processIteratorWithCursor($iterator_name, $iterator);
}
- $this->processAggregates();
$this->log(pht('Zzz...'));
$this->sleep(60 * 5);
}
}
public static function getAllApplicationIterators() {
$apps = PhabricatorApplication::getAllInstalledApplications();
$iterators = array();
foreach ($apps as $app) {
foreach ($app->getFactObjectsForAnalysis() as $object) {
$iterator = new PhabricatorFactUpdateIterator($object);
$iterators[get_class($object)] = $iterator;
}
}
return $iterators;
}
public function processIteratorWithCursor($iterator_name, $iterator) {
$this->log(pht("Processing cursor '%s'.", $iterator_name));
$cursor = id(new PhabricatorFactCursor())->loadOneWhere(
'name = %s',
$iterator_name);
if (!$cursor) {
$cursor = new PhabricatorFactCursor();
$cursor->setName($iterator_name);
$position = null;
} else {
$position = $cursor->getPosition();
}
if ($position) {
$iterator->setPosition($position);
}
$new_cursor_position = $this->processIterator($iterator);
if ($new_cursor_position) {
$cursor->setPosition($new_cursor_position);
$cursor->save();
}
}
public function setEngines(array $engines) {
assert_instances_of($engines, 'PhabricatorFactEngine');
+ $viewer = PhabricatorUser::getOmnipotentUser();
+ foreach ($engines as $engine) {
+ $engine->setViewer($viewer);
+ }
+
$this->engines = $engines;
return $this;
}
public function processIterator($iterator) {
$result = null;
- $raw_facts = array();
+ $datapoints = array();
+ $count = 0;
foreach ($iterator as $key => $object) {
$phid = $object->getPHID();
$this->log(pht('Processing %s...', $phid));
- $raw_facts[$phid] = $this->computeRawFacts($object);
- if (count($raw_facts) > self::RAW_FACT_BUFFER_LIMIT) {
- $this->updateRawFacts($raw_facts);
- $raw_facts = array();
+ $object_datapoints = $this->newDatapoints($object);
+ $count += count($object_datapoints);
+
+ $datapoints[$phid] = $object_datapoints;
+
+ if ($count > 1024) {
+ $this->updateDatapoints($datapoints);
+ $datapoints = array();
+ $count = 0;
}
+
$result = $key;
}
- if ($raw_facts) {
- $this->updateRawFacts($raw_facts);
- $raw_facts = array();
+ if ($count) {
+ $this->updateDatapoints($datapoints);
+ $datapoints = array();
+ $count = 0;
}
return $result;
}
- public function processAggregates() {
- $this->log(pht('Processing aggregates.'));
-
- $facts = $this->computeAggregateFacts();
- $this->updateAggregateFacts($facts);
- }
-
- private function computeAggregateFacts() {
+ private function newDatapoints(PhabricatorLiskDAO $object) {
$facts = array();
foreach ($this->engines as $engine) {
- if (!$engine->shouldComputeAggregateFacts()) {
+ if (!$engine->supportsDatapointsForObject($object)) {
continue;
}
- $facts[] = $engine->computeAggregateFacts();
- }
- return array_mergev($facts);
- }
-
- private function computeRawFacts(PhabricatorLiskDAO $object) {
- $facts = array();
- foreach ($this->engines as $engine) {
- if (!$engine->shouldComputeRawFactsForObject($object)) {
- continue;
- }
- $facts[] = $engine->computeRawFactsForObject($object);
+ $facts[] = $engine->newDatapointsForObject($object);
}
return array_mergev($facts);
}
- private function updateRawFacts(array $map) {
+ private function updateDatapoints(array $map) {
foreach ($map as $phid => $facts) {
- assert_instances_of($facts, 'PhabricatorFactRaw');
+ assert_instances_of($facts, 'PhabricatorFactIntDatapoint');
}
$phids = array_keys($map);
if (!$phids) {
return;
}
- $table = new PhabricatorFactRaw();
+ $fact_keys = array();
+ $objects = array();
+ foreach ($map as $phid => $facts) {
+ foreach ($facts as $fact) {
+ $fact_keys[$fact->getKey()] = true;
+
+ $object_phid = $fact->getObjectPHID();
+ $objects[$object_phid] = $object_phid;
+
+ $dimension_phid = $fact->getDimensionPHID();
+ if ($dimension_phid !== null) {
+ $objects[$dimension_phid] = $dimension_phid;
+ }
+ }
+ }
+
+ $key_map = id(new PhabricatorFactKeyDimension())
+ ->newDimensionMap(array_keys($fact_keys), true);
+ $object_map = id(new PhabricatorFactObjectDimension())
+ ->newDimensionMap(array_keys($objects), true);
+
+ $table = new PhabricatorFactIntDatapoint();
$conn = $table->establishConnection('w');
$table_name = $table->getTableName();
$sql = array();
foreach ($map as $phid => $facts) {
foreach ($facts as $fact) {
+ $key_id = $key_map[$fact->getKey()];
+ $object_id = $object_map[$fact->getObjectPHID()];
+
+ $dimension_phid = $fact->getDimensionPHID();
+ if ($dimension_phid !== null) {
+ $dimension_id = $object_map[$dimension_phid];
+ } else {
+ $dimension_id = null;
+ }
+
$sql[] = qsprintf(
$conn,
- '(%s, %s, %s, %d, %d, %d)',
- $fact->getFactType(),
- $fact->getObjectPHID(),
- $fact->getObjectA(),
- $fact->getValueX(),
- $fact->getValueY(),
+ '(%d, %d, %nd, %d, %d)',
+ $key_id,
+ $object_id,
+ $dimension_id,
+ $fact->getValue(),
$fact->getEpoch());
}
}
+ $rebuilt_ids = array_select_keys($object_map, $phids);
+
$table->openTransaction();
queryfx(
$conn,
- 'DELETE FROM %T WHERE objectPHID IN (%Ls)',
+ 'DELETE FROM %T WHERE objectID IN (%Ld)',
$table_name,
- $phids);
+ $rebuilt_ids);
if ($sql) {
- foreach (array_chunk($sql, 256) as $chunk) {
+ foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
queryfx(
$conn,
'INSERT INTO %T
- (factType, objectPHID, objectA, valueX, valueY, epoch)
+ (keyID, objectID, dimensionID, value, epoch)
VALUES %Q',
$table_name,
- implode(', ', $chunk));
+ $chunk);
}
}
$table->saveTransaction();
}
- private function updateAggregateFacts(array $facts) {
- if (!$facts) {
- return;
- }
-
- $table = new PhabricatorFactAggregate();
- $conn = $table->establishConnection('w');
- $table_name = $table->getTableName();
-
- $sql = array();
- foreach ($facts as $fact) {
- $sql[] = qsprintf(
- $conn,
- '(%s, %s, %d)',
- $fact->getFactType(),
- $fact->getObjectPHID(),
- $fact->getValueX());
- }
-
- foreach (array_chunk($sql, 256) as $chunk) {
- queryfx(
- $conn,
- 'INSERT INTO %T (factType, objectPHID, valueX) VALUES %Q
- ON DUPLICATE KEY UPDATE valueX = VALUES(valueX)',
- $table_name,
- implode(', ', $chunk));
- }
-
- }
-
}
diff --git a/src/applications/fact/engine/PhabricatorFactCountEngine.php b/src/applications/fact/engine/PhabricatorFactCountEngine.php
deleted file mode 100644
index f24068646..000000000
--- a/src/applications/fact/engine/PhabricatorFactCountEngine.php
+++ /dev/null
@@ -1,86 +0,0 @@
-<?php
-
-/**
- * Simple fact engine which counts objects.
- */
-final class PhabricatorFactCountEngine extends PhabricatorFactEngine {
-
- public function getFactSpecs(array $fact_types) {
- $results = array();
- foreach ($fact_types as $type) {
- if (!strncmp($type, '+N:', 3)) {
- if ($type == '+N:*') {
- $name = pht('Total Objects');
- } else {
- $name = pht('Total Objects of type %s', substr($type, 3));
- }
-
- $results[] = id(new PhabricatorFactSimpleSpec($type))
- ->setName($name)
- ->setUnit(PhabricatorFactSimpleSpec::UNIT_COUNT);
- }
-
- if (!strncmp($type, 'N:', 2)) {
- if ($type == 'N:*') {
- $name = pht('Objects');
- } else {
- $name = pht('Objects of type %s', substr($type, 2));
- }
- $results[] = id(new PhabricatorFactSimpleSpec($type))
- ->setName($name)
- ->setUnit(PhabricatorFactSimpleSpec::UNIT_COUNT);
- }
-
- }
- return $results;
- }
-
- public function shouldComputeRawFactsForObject(PhabricatorLiskDAO $object) {
- return true;
- }
-
- public function computeRawFactsForObject(PhabricatorLiskDAO $object) {
- $facts = array();
-
- $phid = $object->getPHID();
- $type = phid_get_type($phid);
-
- foreach (array('N:*', 'N:'.$type) as $fact_type) {
- $facts[] = id(new PhabricatorFactRaw())
- ->setFactType($fact_type)
- ->setObjectPHID($phid)
- ->setValueX(1)
- ->setEpoch($object->getDateCreated());
- }
-
- return $facts;
- }
-
- public function shouldComputeAggregateFacts() {
- return true;
- }
-
- public function computeAggregateFacts() {
- $table = new PhabricatorFactRaw();
- $table_name = $table->getTableName();
- $conn = $table->establishConnection('r');
-
- $counts = queryfx_all(
- $conn,
- 'SELECT factType, SUM(valueX) N FROM %T WHERE factType LIKE %>
- GROUP BY factType',
- $table_name,
- 'N:');
-
- $facts = array();
- foreach ($counts as $count) {
- $facts[] = id(new PhabricatorFactAggregate())
- ->setFactType('+'.$count['factType'])
- ->setValueX($count['N']);
- }
-
- return $facts;
- }
-
-
-}
diff --git a/src/applications/fact/engine/PhabricatorFactEngine.php b/src/applications/fact/engine/PhabricatorFactEngine.php
index a87cabf56..ba2cf966e 100644
--- a/src/applications/fact/engine/PhabricatorFactEngine.php
+++ b/src/applications/fact/engine/PhabricatorFactEngine.php
@@ -1,31 +1,52 @@
<?php
abstract class PhabricatorFactEngine extends Phobject {
+ private $factMap;
+ private $viewer;
+
final public static function loadAllEngines() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->execute();
}
- public function getFactSpecs(array $fact_types) {
- return array();
- }
+ abstract public function newFacts();
- public function shouldComputeRawFactsForObject(PhabricatorLiskDAO $object) {
- return false;
- }
+ abstract public function supportsDatapointsForObject(
+ PhabricatorLiskDAO $object);
+
+ abstract public function newDatapointsForObject(PhabricatorLiskDAO $object);
- public function computeRawFactsForObject(PhabricatorLiskDAO $object) {
- return array();
+ final protected function getFact($key) {
+ if ($this->factMap === null) {
+ $facts = $this->newFacts();
+ $facts = mpull($facts, null, 'getKey');
+ $this->factMap = $facts;
+ }
+
+ if (!isset($this->factMap[$key])) {
+ throw new Exception(
+ pht(
+ 'Unknown fact ("%s") for engine "%s".',
+ $key,
+ get_class($this)));
+ }
+
+ return $this->factMap[$key];
}
- public function shouldComputeAggregateFacts() {
- return false;
+ public function setViewer(PhabricatorUser $viewer) {
+ $this->viewer = $viewer;
+ return $this;
}
- public function computeAggregateFacts() {
- return array();
+ public function getViewer() {
+ if (!$this->viewer) {
+ throw new PhutilInvalidStateException('setViewer');
+ }
+
+ return $this->viewer;
}
}
diff --git a/src/applications/fact/engine/PhabricatorFactLastUpdatedEngine.php b/src/applications/fact/engine/PhabricatorFactLastUpdatedEngine.php
deleted file mode 100644
index 5ea99e623..000000000
--- a/src/applications/fact/engine/PhabricatorFactLastUpdatedEngine.php
+++ /dev/null
@@ -1,34 +0,0 @@
-<?php
-
-/**
- * Engine that records the time facts were last updated.
- */
-final class PhabricatorFactLastUpdatedEngine extends PhabricatorFactEngine {
-
- public function getFactSpecs(array $fact_types) {
- $results = array();
- foreach ($fact_types as $type) {
- if ($type == 'updated') {
- $results[] = id(new PhabricatorFactSimpleSpec($type))
- ->setName(pht('Facts Last Updated'))
- ->setUnit(PhabricatorFactSimpleSpec::UNIT_EPOCH);
- }
- }
- return $results;
- }
-
- public function shouldComputeAggregateFacts() {
- return true;
- }
-
- public function computeAggregateFacts() {
- $facts = array();
-
- $facts[] = id(new PhabricatorFactAggregate())
- ->setFactType('updated')
- ->setValueX(time());
-
- return $facts;
- }
-
-}
diff --git a/src/applications/fact/engine/PhabricatorManiphestTaskFactEngine.php b/src/applications/fact/engine/PhabricatorManiphestTaskFactEngine.php
new file mode 100644
index 000000000..dffceedb7
--- /dev/null
+++ b/src/applications/fact/engine/PhabricatorManiphestTaskFactEngine.php
@@ -0,0 +1,411 @@
+<?php
+
+final class PhabricatorManiphestTaskFactEngine
+ extends PhabricatorTransactionFactEngine {
+
+ public function newFacts() {
+ return array(
+ id(new PhabricatorCountFact())
+ ->setKey('tasks.count.create'),
+
+ id(new PhabricatorCountFact())
+ ->setKey('tasks.open-count.create'),
+ id(new PhabricatorCountFact())
+ ->setKey('tasks.open-count.status'),
+
+ id(new PhabricatorCountFact())
+ ->setKey('tasks.count.create.project'),
+ id(new PhabricatorCountFact())
+ ->setKey('tasks.count.assign.project'),
+ id(new PhabricatorCountFact())
+ ->setKey('tasks.open-count.create.project'),
+ id(new PhabricatorCountFact())
+ ->setKey('tasks.open-count.status.project'),
+ id(new PhabricatorCountFact())
+ ->setKey('tasks.open-count.assign.project'),
+
+ id(new PhabricatorCountFact())
+ ->setKey('tasks.count.create.owner'),
+ id(new PhabricatorCountFact())
+ ->setKey('tasks.count.assign.owner'),
+ id(new PhabricatorCountFact())
+ ->setKey('tasks.open-count.create.owner'),
+ id(new PhabricatorCountFact())
+ ->setKey('tasks.open-count.status.owner'),
+ id(new PhabricatorCountFact())
+ ->setKey('tasks.open-count.assign.owner'),
+
+ id(new PhabricatorPointsFact())
+ ->setKey('tasks.points.create'),
+ id(new PhabricatorPointsFact())
+ ->setKey('tasks.points.score'),
+
+ id(new PhabricatorPointsFact())
+ ->setKey('tasks.open-points.create'),
+ id(new PhabricatorPointsFact())
+ ->setKey('tasks.open-points.status'),
+ id(new PhabricatorPointsFact())
+ ->setKey('tasks.open-points.score'),
+
+ id(new PhabricatorPointsFact())
+ ->setKey('tasks.points.create.project'),
+ id(new PhabricatorPointsFact())
+ ->setKey('tasks.points.assign.project'),
+ id(new PhabricatorPointsFact())
+ ->setKey('tasks.points.score.project'),
+ id(new PhabricatorPointsFact())
+ ->setKey('tasks.open-points.create.project'),
+ id(new PhabricatorPointsFact())
+ ->setKey('tasks.open-points.status.project'),
+ id(new PhabricatorPointsFact())
+ ->setKey('tasks.open-points.score.project'),
+ id(new PhabricatorPointsFact())
+ ->setKey('tasks.open-points.assign.project'),
+
+ id(new PhabricatorPointsFact())
+ ->setKey('tasks.points.create.owner'),
+ id(new PhabricatorPointsFact())
+ ->setKey('tasks.points.assign.owner'),
+ id(new PhabricatorPointsFact())
+ ->setKey('tasks.points.score.owner'),
+ id(new PhabricatorPointsFact())
+ ->setKey('tasks.open-points.create.owner'),
+ id(new PhabricatorPointsFact())
+ ->setKey('tasks.open-points.status.owner'),
+ id(new PhabricatorPointsFact())
+ ->setKey('tasks.open-points.score.owner'),
+ id(new PhabricatorPointsFact())
+ ->setKey('tasks.open-points.assign.owner'),
+ );
+ }
+
+ public function supportsDatapointsForObject(PhabricatorLiskDAO $object) {
+ return ($object instanceof ManiphestTask);
+ }
+
+ public function newDatapointsForObject(PhabricatorLiskDAO $object) {
+ $xaction_groups = $this->newTransactionGroupsForObject($object);
+
+ $old_open = false;
+ $old_points = 0;
+ $old_owner = null;
+ $project_map = array();
+ $object_phid = $object->getPHID();
+ $is_create = true;
+
+ $specs = array();
+ $datapoints = array();
+ foreach ($xaction_groups as $xaction_group) {
+ $add_projects = array();
+ $rem_projects = array();
+
+ $new_open = $old_open;
+ $new_points = $old_points;
+ $new_owner = $old_owner;
+
+ if ($is_create) {
+ // Assume tasks start open.
+ // TODO: This might be a questionable assumption?
+ $new_open = true;
+ }
+
+ $group_epoch = last($xaction_group)->getDateCreated();
+ foreach ($xaction_group as $xaction) {
+ $old_value = $xaction->getOldValue();
+ $new_value = $xaction->getNewValue();
+ switch ($xaction->getTransactionType()) {
+ case ManiphestTaskStatusTransaction::TRANSACTIONTYPE:
+ $new_open = !ManiphestTaskStatus::isClosedStatus($new_value);
+ break;
+ case ManiphestTaskMergedIntoTransaction::TRANSACTIONTYPE:
+ // When a task is merged into another task, it is changed to a
+ // closed status without generating a separate status transaction.
+ $new_open = false;
+ break;
+ case ManiphestTaskPointsTransaction::TRANSACTIONTYPE:
+ $new_points = (int)$xaction->getNewValue();
+ break;
+ case ManiphestTaskOwnerTransaction::TRANSACTIONTYPE:
+ $new_owner = $xaction->getNewValue();
+ break;
+ case PhabricatorTransactions::TYPE_EDGE:
+ $edge_type = $xaction->getMetadataValue('edge:type');
+ switch ($edge_type) {
+ case PhabricatorProjectObjectHasProjectEdgeType::EDGECONST:
+ $record = PhabricatorEdgeChangeRecord::newFromTransaction(
+ $xaction);
+ $add_projects += array_fuse($record->getAddedPHIDs());
+ $rem_projects += array_fuse($record->getRemovedPHIDs());
+ break;
+ }
+ break;
+ }
+ }
+
+ // If a project was both added and removed, moot it.
+ $mix_projects = array_intersect_key($add_projects, $rem_projects);
+ $add_projects = array_diff_key($add_projects, $mix_projects);
+ $rem_projects = array_diff_key($rem_projects, $mix_projects);
+
+ $project_sets = array(
+ array(
+ 'phids' => $rem_projects,
+ 'scale' => -1,
+ ),
+ array(
+ 'phids' => $add_projects,
+ 'scale' => 1,
+ ),
+ );
+
+ if ($is_create) {
+ $action = 'create';
+ $action_points = $new_points;
+ $include_open = $new_open;
+ } else {
+ $action = 'assign';
+ $action_points = $old_points;
+ $include_open = $old_open;
+ }
+
+ foreach ($project_sets as $project_set) {
+ $scale = $project_set['scale'];
+ foreach ($project_set['phids'] as $project_phid) {
+ if ($include_open) {
+ $specs[] = array(
+ "tasks.open-count.{$action}.project",
+ 1 * $scale,
+ $project_phid,
+ );
+
+ $specs[] = array(
+ "tasks.open-points.{$action}.project",
+ $action_points * $scale,
+ $project_phid,
+ );
+ }
+
+ $specs[] = array(
+ "tasks.count.{$action}.project",
+ 1 * $scale,
+ $project_phid,
+ );
+
+ $specs[] = array(
+ "tasks.points.{$action}.project",
+ $action_points * $scale,
+ $project_phid,
+ );
+
+ if ($scale < 0) {
+ unset($project_map[$project_phid]);
+ } else {
+ $project_map[$project_phid] = $project_phid;
+ }
+ }
+ }
+
+ if ($new_owner !== $old_owner) {
+ $owner_sets = array(
+ array(
+ 'phid' => $old_owner,
+ 'scale' => -1,
+ ),
+ array(
+ 'phid' => $new_owner,
+ 'scale' => 1,
+ ),
+ );
+
+ foreach ($owner_sets as $owner_set) {
+ $owner_phid = $owner_set['phid'];
+ if ($owner_phid === null) {
+ continue;
+ }
+
+ $scale = $owner_set['scale'];
+
+ if ($old_open != $new_open) {
+ $specs[] = array(
+ "tasks.open-count.{$action}.owner",
+ 1 * $scale,
+ $owner_phid,
+ );
+
+ $specs[] = array(
+ "tasks.open-points.{$action}.owner",
+ $action_points * $scale,
+ $owner_phid,
+ );
+ }
+
+ $specs[] = array(
+ "tasks.count.{$action}.owner",
+ 1 * $scale,
+ $owner_phid,
+ );
+
+ if ($action_points) {
+ $specs[] = array(
+ "tasks.points.{$action}.owner",
+ $action_points * $scale,
+ $owner_phid,
+ );
+ }
+ }
+ }
+
+ if ($is_create) {
+ $specs[] = array(
+ 'tasks.count.create',
+ 1,
+ );
+
+ $specs[] = array(
+ 'tasks.points.create',
+ $new_points,
+ );
+
+ if ($new_open) {
+ $specs[] = array(
+ 'tasks.open-count.create',
+ 1,
+ );
+ $specs[] = array(
+ 'tasks.open-points.create',
+ $new_points,
+ );
+ }
+ } else if ($new_open !== $old_open) {
+ if ($new_open) {
+ $scale = 1;
+ } else {
+ $scale = -1;
+ }
+
+ $specs[] = array(
+ 'tasks.open-count.status',
+ 1 * $scale,
+ );
+
+ $specs[] = array(
+ 'tasks.open-points.status',
+ $action_points * $scale,
+ );
+
+ if ($new_owner !== null) {
+ $specs[] = array(
+ 'tasks.open-count.status.owner',
+ 1 * $scale,
+ $new_owner,
+ );
+ $specs[] = array(
+ 'tasks.open-points.status.owner',
+ $action_points * $scale,
+ $new_owner,
+ );
+ }
+
+ foreach ($project_map as $project_phid) {
+ $specs[] = array(
+ 'tasks.open-count.status.project',
+ 1 * $scale,
+ $project_phid,
+ );
+ $specs[] = array(
+ 'tasks.open-points.status.project',
+ $action_points * $scale,
+ $project_phid,
+ );
+ }
+ }
+
+ // The "score" facts only apply to rescoring tasks which already
+ // exist, so we skip them if the task is being created.
+ if (($new_points !== $old_points) && !$is_create) {
+ $delta = ($new_points - $old_points);
+
+ $specs[] = array(
+ 'tasks.points.score',
+ $delta,
+ );
+
+ foreach ($project_map as $project_phid) {
+ $specs[] = array(
+ 'tasks.points.score.project',
+ $delta,
+ $project_phid,
+ );
+
+ if ($old_open && $new_open) {
+ $specs[] = array(
+ 'tasks.open-points.score.project',
+ $delta,
+ $project_phid,
+ );
+ }
+ }
+
+ if ($new_owner !== null) {
+ $specs[] = array(
+ 'tasks.points.score.owner',
+ $delta,
+ $new_owner,
+ );
+
+ if ($old_open && $new_open) {
+ $specs[] = array(
+ 'tasks.open-points.score.owner',
+ $delta,
+ $new_owner,
+ );
+ }
+ }
+
+ if ($old_open && $new_open) {
+ $specs[] = array(
+ 'tasks.open-points.score',
+ $delta,
+ );
+ }
+ }
+
+ $old_points = $new_points;
+ $old_open = $new_open;
+ $old_owner = $new_owner;
+
+ foreach ($specs as $spec) {
+ $spec_key = $spec[0];
+ $spec_value = $spec[1];
+
+ // Don't write any facts with a value of 0. The "count" facts never
+ // have a value of 0, and the "points" facts aren't meaningful if
+ // they have a value of 0.
+ if ($spec_value == 0) {
+ continue;
+ }
+
+ $datapoint = $this->getFact($spec_key)
+ ->newDatapoint();
+
+ $datapoint
+ ->setObjectPHID($object_phid)
+ ->setValue($spec_value)
+ ->setEpoch($group_epoch);
+
+ if (isset($spec[2])) {
+ $datapoint->setDimensionPHID($spec[2]);
+ }
+
+ $datapoints[] = $datapoint;
+ }
+
+ $specs = array();
+ $is_create = false;
+ }
+
+ return $datapoints;
+ }
+
+
+}
diff --git a/src/applications/fact/engine/PhabricatorTransactionFactEngine.php b/src/applications/fact/engine/PhabricatorTransactionFactEngine.php
new file mode 100644
index 000000000..20fb7a6f6
--- /dev/null
+++ b/src/applications/fact/engine/PhabricatorTransactionFactEngine.php
@@ -0,0 +1,84 @@
+<?php
+
+abstract class PhabricatorTransactionFactEngine
+ extends PhabricatorFactEngine {
+
+ public function newTransactionGroupsForObject(PhabricatorLiskDAO $object) {
+ $viewer = $this->getViewer();
+
+ $xaction_query = PhabricatorApplicationTransactionQuery::newQueryForObject(
+ $object);
+ $xactions = $xaction_query
+ ->setViewer($viewer)
+ ->withObjectPHIDs(array($object->getPHID()))
+ ->execute();
+
+ $xactions = msortv($xactions, 'newChronologicalSortVector');
+
+ return $this->groupTransactions($xactions);
+ }
+
+ protected function groupTransactions(array $xactions) {
+ // These grouping rules are generally much looser than the display grouping
+ // rules. As long as the same user is editing the task and they don't leave
+ // it alone for a particularly long time, we'll group things together.
+
+ $breaks = array();
+
+ $touch_window = phutil_units('15 minutes in seconds');
+ $user_type = PhabricatorPeopleUserPHIDType::TYPECONST;
+
+ $last_actor = null;
+ $last_epoch = null;
+
+ foreach ($xactions as $key => $xaction) {
+ $this_actor = $xaction->getAuthorPHID();
+ if (phid_get_type($this_actor) != $user_type) {
+ $this_actor = null;
+ }
+
+ if ($this_actor && $last_actor && ($this_actor != $last_actor)) {
+ $breaks[$key] = true;
+ }
+
+ // If too much time passed between changes, group them separately.
+ $this_epoch = $xaction->getDateCreated();
+ if ($last_epoch) {
+ if (($this_epoch - $last_epoch) > $touch_window) {
+ $breaks[$key] = true;
+ }
+ }
+
+ // The clock gets reset every time the same real user touches the
+ // task, but does not reset if an automated actor touches things.
+ if (!$last_actor || ($this_actor == $last_actor)) {
+ $last_epoch = $this_epoch;
+ }
+
+ if ($this_actor && ($last_actor != $this_actor)) {
+ $last_actor = $this_actor;
+ $last_epoch = $this_epoch;
+ }
+ }
+
+ $groups = array();
+ $group = array();
+ foreach ($xactions as $key => $xaction) {
+ if (isset($breaks[$key])) {
+ if ($group) {
+ $groups[] = $group;
+ $group = array();
+ }
+ }
+
+ $group[] = $xaction;
+ }
+
+ if ($group) {
+ $groups[] = $group;
+ }
+
+ return $groups;
+ }
+
+}
diff --git a/src/applications/fact/fact/PhabricatorCountFact.php b/src/applications/fact/fact/PhabricatorCountFact.php
new file mode 100644
index 000000000..41e6fd4de
--- /dev/null
+++ b/src/applications/fact/fact/PhabricatorCountFact.php
@@ -0,0 +1,9 @@
+<?php
+
+final class PhabricatorCountFact extends PhabricatorFact {
+
+ protected function newTemplateDatapoint() {
+ return new PhabricatorFactIntDatapoint();
+ }
+
+}
diff --git a/src/applications/fact/fact/PhabricatorFact.php b/src/applications/fact/fact/PhabricatorFact.php
new file mode 100644
index 000000000..2e33a029f
--- /dev/null
+++ b/src/applications/fact/fact/PhabricatorFact.php
@@ -0,0 +1,40 @@
+<?php
+
+abstract class PhabricatorFact extends Phobject {
+
+ private $key;
+
+ public static function getAllFacts() {
+ $engines = PhabricatorFactEngine::loadAllEngines();
+
+ $map = array();
+ foreach ($engines as $engine) {
+ $facts = $engine->newFacts();
+ $facts = mpull($facts, null, 'getKey');
+ $map += $facts;
+ }
+
+ return $map;
+ }
+
+ final public function setKey($key) {
+ $this->key = $key;
+ return $this;
+ }
+
+ final public function getKey() {
+ return $this->key;
+ }
+
+ final public function getName() {
+ return pht('Fact "%s"', $this->getKey());
+ }
+
+ final public function newDatapoint() {
+ return $this->newTemplateDatapoint()
+ ->setKey($this->getKey());
+ }
+
+ abstract protected function newTemplateDatapoint();
+
+}
diff --git a/src/applications/fact/fact/PhabricatorPointsFact.php b/src/applications/fact/fact/PhabricatorPointsFact.php
new file mode 100644
index 000000000..a80f45d13
--- /dev/null
+++ b/src/applications/fact/fact/PhabricatorPointsFact.php
@@ -0,0 +1,9 @@
+<?php
+
+final class PhabricatorPointsFact extends PhabricatorFact {
+
+ protected function newTemplateDatapoint() {
+ return new PhabricatorFactIntDatapoint();
+ }
+
+}
diff --git a/src/applications/fact/management/PhabricatorFactManagementAnalyzeWorkflow.php b/src/applications/fact/management/PhabricatorFactManagementAnalyzeWorkflow.php
index 6b316f8c7..2d2a0bba6 100644
--- a/src/applications/fact/management/PhabricatorFactManagementAnalyzeWorkflow.php
+++ b/src/applications/fact/management/PhabricatorFactManagementAnalyzeWorkflow.php
@@ -1,68 +1,64 @@
<?php
final class PhabricatorFactManagementAnalyzeWorkflow
extends PhabricatorFactManagementWorkflow {
protected function didConstruct() {
$this
->setName('analyze')
->setSynopsis(pht('Manually invoke fact analyzers.'))
->setArguments(
array(
array(
'name' => 'iterator',
'param' => 'name',
'repeat' => true,
'help' => pht('Process only iterator __name__.'),
),
array(
'name' => 'all',
'help' => pht('Analyze from the beginning, ignoring cursors.'),
),
array(
'name' => 'skip-aggregates',
'help' => pht('Skip analysis of aggregate facts.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$daemon = new PhabricatorFactDaemon(array());
$daemon->setVerbose(true);
$daemon->setEngines(PhabricatorFactEngine::loadAllEngines());
$iterators = PhabricatorFactDaemon::getAllApplicationIterators();
$selected = $args->getArg('iterator');
if ($selected) {
$use = array();
foreach ($selected as $iterator_name) {
if (isset($iterators[$iterator_name])) {
$use[$iterator_name] = $iterators[$iterator_name];
} else {
$console->writeErr(
"%s\n",
pht("Iterator '%s' does not exist.", $iterator_name));
}
}
$iterators = $use;
}
foreach ($iterators as $iterator_name => $iterator) {
if ($args->getArg('all')) {
$daemon->processIterator($iterator);
} else {
$daemon->processIteratorWithCursor($iterator_name, $iterator);
}
}
- if (!$args->getArg('skip-aggregates')) {
- $daemon->processAggregates();
- }
-
return 0;
}
}
diff --git a/src/applications/fact/management/PhabricatorFactManagementDestroyWorkflow.php b/src/applications/fact/management/PhabricatorFactManagementDestroyWorkflow.php
index 8a2e5f1ed..7a4997ab3 100644
--- a/src/applications/fact/management/PhabricatorFactManagementDestroyWorkflow.php
+++ b/src/applications/fact/management/PhabricatorFactManagementDestroyWorkflow.php
@@ -1,43 +1,48 @@
<?php
final class PhabricatorFactManagementDestroyWorkflow
extends PhabricatorFactManagementWorkflow {
protected function didConstruct() {
$this
->setName('destroy')
->setSynopsis(pht('Destroy all facts.'))
->setArguments(array());
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$question = pht(
'Really destroy all facts? They will need to be rebuilt through '.
'analysis, which may take some time.');
$ok = $console->confirm($question, $default = false);
if (!$ok) {
return 1;
}
$tables = array();
- $tables[] = new PhabricatorFactRaw();
- $tables[] = new PhabricatorFactAggregate();
+ $tables[] = new PhabricatorFactCursor();
+
+ $tables[] = new PhabricatorFactIntDatapoint();
+
+ $tables[] = new PhabricatorFactObjectDimension();
+ $tables[] = new PhabricatorFactKeyDimension();
+
foreach ($tables as $table) {
$conn = $table->establishConnection('w');
$name = $table->getTableName();
$console->writeOut("%s\n", pht("Destroying table '%s'...", $name));
queryfx(
$conn,
'TRUNCATE TABLE %T',
$name);
}
$console->writeOut("%s\n", pht('Done.'));
}
}
diff --git a/src/applications/fact/management/PhabricatorFactManagementStatusWorkflow.php b/src/applications/fact/management/PhabricatorFactManagementStatusWorkflow.php
deleted file mode 100644
index 604a40b6e..000000000
--- a/src/applications/fact/management/PhabricatorFactManagementStatusWorkflow.php
+++ /dev/null
@@ -1,47 +0,0 @@
-<?php
-
-final class PhabricatorFactManagementStatusWorkflow
- extends PhabricatorFactManagementWorkflow {
-
- protected function didConstruct() {
- $this
- ->setName('status')
- ->setSynopsis(pht('Show status of fact data.'))
- ->setArguments(array());
- }
-
- public function execute(PhutilArgumentParser $args) {
- $console = PhutilConsole::getConsole();
-
- $map = array(
- 'raw' => new PhabricatorFactRaw(),
- 'agg' => new PhabricatorFactAggregate(),
- );
-
- foreach ($map as $type => $table) {
- $conn = $table->establishConnection('r');
- $name = $table->getTableName();
-
- $row = queryfx_one(
- $conn,
- 'SELECT COUNT(*) N FROM %T',
- $name);
-
- $n = $row['N'];
-
- switch ($type) {
- case 'raw':
- $desc = pht('There are %d raw fact(s) in storage.', $n);
- break;
- case 'agg':
- $desc = pht('There are %d aggregate fact(s) in storage.', $n);
- break;
- }
-
- $console->writeOut("%s\n", $desc);
- }
-
- return 0;
- }
-
-}
diff --git a/src/applications/fact/query/PhabricatorFactDatapointQuery.php b/src/applications/fact/query/PhabricatorFactDatapointQuery.php
new file mode 100644
index 000000000..fee7c86d1
--- /dev/null
+++ b/src/applications/fact/query/PhabricatorFactDatapointQuery.php
@@ -0,0 +1,181 @@
+<?php
+
+final class PhabricatorFactDatapointQuery extends Phobject {
+
+ private $facts;
+ private $objectPHIDs;
+ private $limit;
+
+ private $needVectors;
+
+ private $keyMap = array();
+ private $dimensionMap = array();
+
+ public function withFacts(array $facts) {
+ $this->facts = $facts;
+ return $this;
+ }
+
+ public function withObjectPHIDs(array $object_phids) {
+ $this->objectPHIDs = $object_phids;
+ return $this;
+ }
+
+ public function setLimit($limit) {
+ $this->limit = $limit;
+ return $this;
+ }
+
+ public function needVectors($need) {
+ $this->needVectors = $need;
+ return $this;
+ }
+
+ public function execute() {
+ $facts = mpull($this->facts, null, 'getKey');
+ if (!$facts) {
+ throw new Exception(pht('Executing a fact query requires facts.'));
+ }
+
+ $table_map = array();
+ foreach ($facts as $fact) {
+ $datapoint = $fact->newDatapoint();
+ $table = $datapoint->getTableName();
+
+ if (!isset($table_map[$table])) {
+ $table_map[$table] = array(
+ 'table' => $datapoint,
+ 'facts' => array(),
+ );
+ }
+
+ $table_map[$table]['facts'][] = $fact;
+ }
+
+ $rows = array();
+ foreach ($table_map as $spec) {
+ $rows[] = $this->executeWithTable($spec);
+ }
+ $rows = array_mergev($rows);
+
+ $key_unmap = array_flip($this->keyMap);
+ $dimension_unmap = array_flip($this->dimensionMap);
+
+ $groups = array();
+ $need_phids = array();
+ foreach ($rows as $row) {
+ $groups[$row['keyID']][] = $row;
+
+ $object_id = $row['objectID'];
+ if (!isset($dimension_unmap[$object_id])) {
+ $need_phids[$object_id] = $object_id;
+ }
+
+ $dimension_id = $row['dimensionID'];
+ if ($dimension_id && !isset($dimension_unmap[$dimension_id])) {
+ $need_phids[$dimension_id] = $dimension_id;
+ }
+ }
+
+ $dimension_unmap += id(new PhabricatorFactObjectDimension())
+ ->newDimensionUnmap($need_phids);
+
+ $results = array();
+ foreach ($groups as $key_id => $rows) {
+ $key = $key_unmap[$key_id];
+ $fact = $facts[$key];
+ $datapoint = $fact->newDatapoint();
+ foreach ($rows as $row) {
+ $dimension_id = $row['dimensionID'];
+ if ($dimension_id) {
+ if (!isset($dimension_unmap[$dimension_id])) {
+ continue;
+ } else {
+ $dimension_phid = $dimension_unmap[$dimension_id];
+ }
+ } else {
+ $dimension_phid = null;
+ }
+
+ $object_id = $row['objectID'];
+ if (!isset($dimension_unmap[$object_id])) {
+ continue;
+ } else {
+ $object_phid = $dimension_unmap[$object_id];
+ }
+
+ $result = array(
+ 'key' => $key,
+ 'objectPHID' => $object_phid,
+ 'dimensionPHID' => $dimension_phid,
+ 'value' => (int)$row['value'],
+ 'epoch' => $row['epoch'],
+ );
+
+ if ($this->needVectors) {
+ $result['vector'] = $datapoint->newRawVector($result);
+ }
+
+ $results[] = $result;
+ }
+ }
+
+ return $results;
+ }
+
+ private function executeWithTable(array $spec) {
+ $table = $spec['table'];
+ $facts = $spec['facts'];
+ $conn = $table->establishConnection('r');
+
+ $fact_keys = mpull($facts, 'getKey');
+ $this->keyMap = id(new PhabricatorFactKeyDimension())
+ ->newDimensionMap($fact_keys);
+
+ if (!$this->keyMap) {
+ return array();
+ }
+
+ $where = array();
+
+ $where[] = qsprintf(
+ $conn,
+ 'keyID IN (%Ld)',
+ $this->keyMap);
+
+ if ($this->objectPHIDs) {
+ $object_map = id(new PhabricatorFactObjectDimension())
+ ->newDimensionMap($this->objectPHIDs);
+ if (!$object_map) {
+ return array();
+ }
+
+ $this->dimensionMap = $object_map;
+
+ $where[] = qsprintf(
+ $conn,
+ 'objectID IN (%Ld)',
+ $this->dimensionMap);
+ }
+
+ $where = '('.implode(') AND (', $where).')';
+
+ if ($this->limit) {
+ $limit = qsprintf(
+ $conn,
+ 'LIMIT %d',
+ $this->limit);
+ } else {
+ $limit = '';
+ }
+
+ return queryfx_all(
+ $conn,
+ 'SELECT keyID, objectID, dimensionID, value, epoch
+ FROM %T WHERE %Q %Q',
+ $table->getTableName(),
+ $where,
+ $limit);
+ }
+
+}
diff --git a/src/applications/fact/spec/PhabricatorFactSimpleSpec.php b/src/applications/fact/spec/PhabricatorFactSimpleSpec.php
deleted file mode 100644
index 350b6367f..000000000
--- a/src/applications/fact/spec/PhabricatorFactSimpleSpec.php
+++ /dev/null
@@ -1,38 +0,0 @@
-<?php
-
-final class PhabricatorFactSimpleSpec extends PhabricatorFactSpec {
-
- private $type;
- private $name;
- private $unit;
-
- public function __construct($type) {
- $this->type = $type;
- }
-
- public function getType() {
- return $this->type;
- }
-
- public function setUnit($unit) {
- $this->unit = $unit;
- return $this;
- }
-
- public function getUnit() {
- return $this->unit;
- }
-
- public function setName($name) {
- $this->name = $name;
- return $this;
- }
-
- public function getName() {
- if ($this->name !== null) {
- return $this->name;
- }
- return parent::getName();
- }
-
-}
diff --git a/src/applications/fact/spec/PhabricatorFactSpec.php b/src/applications/fact/spec/PhabricatorFactSpec.php
deleted file mode 100644
index 47fcc01d8..000000000
--- a/src/applications/fact/spec/PhabricatorFactSpec.php
+++ /dev/null
@@ -1,53 +0,0 @@
-<?php
-
-abstract class PhabricatorFactSpec extends Phobject {
-
- const UNIT_COUNT = 'unit-count';
- const UNIT_EPOCH = 'unit-epoch';
-
- public static function newSpecsForFactTypes(
- array $engines,
- array $fact_types) {
- assert_instances_of($engines, 'PhabricatorFactEngine');
-
- $map = array();
- foreach ($engines as $engine) {
- $specs = $engine->getFactSpecs($fact_types);
- $specs = mpull($specs, null, 'getType');
- $map += $specs;
- }
-
- foreach ($fact_types as $type) {
- if (empty($map[$type])) {
- $map[$type] = new PhabricatorFactSimpleSpec($type);
- }
- }
-
- return $map;
- }
-
- abstract public function getType();
-
- public function getUnit() {
- return null;
- }
-
- public function getName() {
- return pht(
- 'Fact (%s)',
- $this->getType());
- }
-
- public function formatValueForDisplay(PhabricatorUser $user, $value) {
- $unit = $this->getUnit();
- switch ($unit) {
- case self::UNIT_COUNT:
- return number_format($value);
- case self::UNIT_EPOCH:
- return phabricator_datetime($value, $user);
- default:
- return $value;
- }
- }
-
-}
diff --git a/src/applications/fact/storage/PhabricatorFactDimension.php b/src/applications/fact/storage/PhabricatorFactDimension.php
new file mode 100644
index 000000000..9c05121c9
--- /dev/null
+++ b/src/applications/fact/storage/PhabricatorFactDimension.php
@@ -0,0 +1,110 @@
+<?php
+
+abstract class PhabricatorFactDimension extends PhabricatorFactDAO {
+
+ abstract protected function getDimensionColumnName();
+
+ final public function newDimensionID($key, $create = false) {
+ $map = $this->newDimensionMap(array($key), $create);
+ return idx($map, $key);
+ }
+
+ final public function newDimensionUnmap(array $ids) {
+ if (!$ids) {
+ return array();
+ }
+
+ $conn = $this->establishConnection('r');
+ $column = $this->getDimensionColumnName();
+
+ $rows = queryfx_all(
+ $conn,
+ 'SELECT id, %C FROM %T WHERE id IN (%Ld)',
+ $column,
+ $this->getTableName(),
+ $ids);
+ $rows = ipull($rows, $column, 'id');
+
+ return $rows;
+ }
+
+ final public function newDimensionMap(array $keys, $create = false) {
+ if (!$keys) {
+ return array();
+ }
+
+ $conn = $this->establishConnection('r');
+ $column = $this->getDimensionColumnName();
+
+ $rows = queryfx_all(
+ $conn,
+ 'SELECT id, %C FROM %T WHERE %C IN (%Ls)',
+ $column,
+ $this->getTableName(),
+ $column,
+ $keys);
+ $rows = ipull($rows, 'id', $column);
+
+ $map = array();
+ $need = array();
+ foreach ($keys as $key) {
+ if (isset($rows[$key])) {
+ $map[$key] = (int)$rows[$key];
+ } else {
+ $need[] = $key;
+ }
+ }
+
+ if (!$need) {
+ return $map;
+ }
+
+ if (!$create) {
+ return $map;
+ }
+
+ $sql = array();
+ foreach ($need as $key) {
+ $sql[] = qsprintf(
+ $conn,
+ '(%s)',
+ $key);
+ }
+
+ $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
+ foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
+ queryfx(
+ $conn,
+ 'INSERT IGNORE INTO %T (%C) VALUES %Q',
+ $this->getTableName(),
+ $column,
+ $chunk);
+ }
+ unset($unguarded);
+
+ $rows = queryfx_all(
+ $conn,
+ 'SELECT id, %C FROM %T WHERE %C IN (%Ls)',
+ $column,
+ $this->getTableName(),
+ $column,
+ $need);
+ $rows = ipull($rows, 'id', $column);
+
+ foreach ($need as $key) {
+ if (isset($rows[$key])) {
+ $map[$key] = (int)$rows[$key];
+ } else {
+ throw new Exception(
+ pht(
+ 'Failed to load or generate dimension ID ("%s") for dimension '.
+ 'key "%s".',
+ get_class($this),
+ $key));
+ }
+ }
+
+ return $map;
+ }
+
+}
diff --git a/src/applications/fact/storage/PhabricatorFactIntDatapoint.php b/src/applications/fact/storage/PhabricatorFactIntDatapoint.php
new file mode 100644
index 000000000..87a7c68ad
--- /dev/null
+++ b/src/applications/fact/storage/PhabricatorFactIntDatapoint.php
@@ -0,0 +1,87 @@
+<?php
+
+final class PhabricatorFactIntDatapoint extends PhabricatorFactDAO {
+
+ protected $keyID;
+ protected $objectID;
+ protected $dimensionID;
+ protected $value;
+ protected $epoch;
+
+ private $key;
+ private $objectPHID;
+ private $dimensionPHID;
+
+ protected function getConfiguration() {
+ return array(
+ self::CONFIG_TIMESTAMPS => false,
+ self::CONFIG_COLUMN_SCHEMA => array(
+ 'id' => 'auto64',
+ 'dimensionID' => 'id?',
+ 'value' => 'sint64',
+ ),
+ self::CONFIG_KEY_SCHEMA => array(
+ 'key_dimension' => array(
+ 'columns' => array('keyID', 'dimensionID'),
+ ),
+ 'key_object' => array(
+ 'columns' => array('objectID'),
+ ),
+ ),
+ ) + parent::getConfiguration();
+ }
+
+ public function setKey($key) {
+ $this->key = $key;
+ return $this;
+ }
+
+ public function getKey() {
+ return $this->key;
+ }
+
+ public function setObjectPHID($object_phid) {
+ $this->objectPHID = $object_phid;
+ return $this;
+ }
+
+ public function getObjectPHID() {
+ return $this->objectPHID;
+ }
+
+ public function setDimensionPHID($dimension_phid) {
+ $this->dimensionPHID = $dimension_phid;
+ return $this;
+ }
+
+ public function getDimensionPHID() {
+ return $this->dimensionPHID;
+ }
+
+ public function newDatapointVector() {
+ return $this->formatVector(
+ array(
+ $this->key,
+ $this->objectPHID,
+ $this->dimensionPHID,
+ $this->value,
+ $this->epoch,
+ ));
+ }
+
+ public function newRawVector(array $spec) {
+ return $this->formatVector(
+ array(
+ $spec['key'],
+ $spec['objectPHID'],
+ $spec['dimensionPHID'],
+ $spec['value'],
+ $spec['epoch'],
+ ));
+ }
+
+ private function formatVector(array $vector) {
+ return implode(':', $vector);
+ }
+
+}
diff --git a/src/applications/fact/storage/PhabricatorFactKeyDimension.php b/src/applications/fact/storage/PhabricatorFactKeyDimension.php
new file mode 100644
index 000000000..b58ba9440
--- /dev/null
+++ b/src/applications/fact/storage/PhabricatorFactKeyDimension.php
@@ -0,0 +1,27 @@
+<?php
+
+final class PhabricatorFactKeyDimension
+ extends PhabricatorFactDimension {
+
+ protected $factKey;
+
+ protected function getConfiguration() {
+ return array(
+ self::CONFIG_TIMESTAMPS => false,
+ self::CONFIG_COLUMN_SCHEMA => array(
+ 'factKey' => 'text64',
+ ),
+ self::CONFIG_KEY_SCHEMA => array(
+ 'key_factkey' => array(
+ 'columns' => array('factKey'),
+ 'unique' => true,
+ ),
+ ),
+ ) + parent::getConfiguration();
+ }
+
+ protected function getDimensionColumnName() {
+ return 'factKey';
+ }
+
+}
diff --git a/src/applications/fact/storage/PhabricatorFactObjectDimension.php b/src/applications/fact/storage/PhabricatorFactObjectDimension.php
new file mode 100644
index 000000000..e7319724a
--- /dev/null
+++ b/src/applications/fact/storage/PhabricatorFactObjectDimension.php
@@ -0,0 +1,25 @@
+<?php
+
+final class PhabricatorFactObjectDimension
+ extends PhabricatorFactDimension {
+
+ protected $objectPHID;
+
+ protected function getConfiguration() {
+ return array(
+ self::CONFIG_TIMESTAMPS => false,
+ self::CONFIG_COLUMN_SCHEMA => array(),
+ self::CONFIG_KEY_SCHEMA => array(
+ 'key_object' => array(
+ 'columns' => array('objectPHID'),
+ 'unique' => true,
+ ),
+ ),
+ ) + parent::getConfiguration();
+ }
+
+ protected function getDimensionColumnName() {
+ return 'objectPHID';
+ }
+
+}
diff --git a/src/applications/feed/PhabricatorFeedStoryPublisher.php b/src/applications/feed/PhabricatorFeedStoryPublisher.php
index 8d018c61b..47dde8c98 100644
--- a/src/applications/feed/PhabricatorFeedStoryPublisher.php
+++ b/src/applications/feed/PhabricatorFeedStoryPublisher.php
@@ -1,305 +1,341 @@
<?php
final class PhabricatorFeedStoryPublisher extends Phobject {
private $relatedPHIDs;
private $storyType;
private $storyData;
private $storyTime;
private $storyAuthorPHID;
private $primaryObjectPHID;
private $subscribedPHIDs = array();
private $mailRecipientPHIDs = array();
private $notifyAuthor;
private $mailTags = array();
+ private $unexpandablePHIDs = array();
public function setMailTags(array $mail_tags) {
$this->mailTags = $mail_tags;
return $this;
}
public function getMailTags() {
return $this->mailTags;
}
public function setNotifyAuthor($notify_author) {
$this->notifyAuthor = $notify_author;
return $this;
}
public function getNotifyAuthor() {
return $this->notifyAuthor;
}
public function setRelatedPHIDs(array $phids) {
$this->relatedPHIDs = $phids;
return $this;
}
public function setSubscribedPHIDs(array $phids) {
$this->subscribedPHIDs = $phids;
return $this;
}
public function setPrimaryObjectPHID($phid) {
$this->primaryObjectPHID = $phid;
return $this;
}
+ public function setUnexpandablePHIDs(array $unexpandable_phids) {
+ $this->unexpandablePHIDs = $unexpandable_phids;
+ return $this;
+ }
+
+ public function getUnexpandablePHIDs() {
+ return $this->unexpandablePHIDs;
+ }
+
public function setStoryType($story_type) {
$this->storyType = $story_type;
return $this;
}
public function setStoryData(array $data) {
$this->storyData = $data;
return $this;
}
public function setStoryTime($time) {
$this->storyTime = $time;
return $this;
}
public function setStoryAuthorPHID($phid) {
$this->storyAuthorPHID = $phid;
return $this;
}
public function setMailRecipientPHIDs(array $phids) {
$this->mailRecipientPHIDs = $phids;
return $this;
}
public function publish() {
$class = $this->storyType;
if (!$class) {
throw new Exception(
pht(
'Call %s before publishing!',
'setStoryType()'));
}
if (!class_exists($class)) {
throw new Exception(
pht(
"Story type must be a valid class name and must subclass %s. ".
"'%s' is not a loadable class.",
'PhabricatorFeedStory',
$class));
}
if (!is_subclass_of($class, 'PhabricatorFeedStory')) {
throw new Exception(
pht(
"Story type must be a valid class name and must subclass %s. ".
"'%s' is not a subclass of %s.",
'PhabricatorFeedStory',
$class,
'PhabricatorFeedStory'));
}
$chrono_key = $this->generateChronologicalKey();
$story = new PhabricatorFeedStoryData();
$story->setStoryType($this->storyType);
$story->setStoryData($this->storyData);
$story->setAuthorPHID((string)$this->storyAuthorPHID);
$story->setChronologicalKey($chrono_key);
$story->save();
if ($this->relatedPHIDs) {
$ref = new PhabricatorFeedStoryReference();
$sql = array();
$conn = $ref->establishConnection('w');
foreach (array_unique($this->relatedPHIDs) as $phid) {
$sql[] = qsprintf(
$conn,
'(%s, %s)',
$phid,
$chrono_key);
}
queryfx(
$conn,
'INSERT INTO %T (objectPHID, chronologicalKey) VALUES %Q',
$ref->getTableName(),
implode(', ', $sql));
}
$subscribed_phids = $this->subscribedPHIDs;
if ($subscribed_phids) {
$subscribed_phids = $this->filterSubscribedPHIDs($subscribed_phids);
$this->insertNotifications($chrono_key, $subscribed_phids);
$this->sendNotification($chrono_key, $subscribed_phids);
}
PhabricatorWorker::scheduleTask(
'FeedPublisherWorker',
array(
'key' => $chrono_key,
));
return $story;
}
private function insertNotifications($chrono_key, array $subscribed_phids) {
if (!$this->primaryObjectPHID) {
throw new Exception(
pht(
'You must call %s if you %s!',
'setPrimaryObjectPHID()',
'setSubscribedPHIDs()'));
}
$notif = new PhabricatorFeedStoryNotification();
$sql = array();
$conn = $notif->establishConnection('w');
$will_receive_mail = array_fill_keys($this->mailRecipientPHIDs, true);
$user_phids = array_unique($subscribed_phids);
foreach ($user_phids as $user_phid) {
if (isset($will_receive_mail[$user_phid])) {
$mark_read = 1;
} else {
$mark_read = 0;
}
$sql[] = qsprintf(
$conn,
'(%s, %s, %s, %d)',
$this->primaryObjectPHID,
$user_phid,
$chrono_key,
$mark_read);
}
if ($sql) {
queryfx(
$conn,
'INSERT INTO %T '.
'(primaryObjectPHID, userPHID, chronologicalKey, hasViewed) '.
'VALUES %Q',
$notif->getTableName(),
implode(', ', $sql));
}
PhabricatorUserCache::clearCaches(
PhabricatorUserNotificationCountCacheType::KEY_COUNT,
$user_phids);
}
private function sendNotification($chrono_key, array $subscribed_phids) {
$data = array(
'key' => (string)$chrono_key,
'type' => 'notification',
'subscribers' => $subscribed_phids,
);
PhabricatorNotificationClient::tryToPostMessage($data);
}
/**
* Remove PHIDs who should not receive notifications from a subscriber list.
*
* @param list<phid> List of potential subscribers.
* @return list<phid> List of actual subscribers.
*/
private function filterSubscribedPHIDs(array $phids) {
$phids = $this->expandRecipients($phids);
$tags = $this->getMailTags();
if ($tags) {
$all_prefs = id(new PhabricatorUserPreferencesQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withUserPHIDs($phids)
->needSyntheticPreferences(true)
->execute();
$all_prefs = mpull($all_prefs, null, 'getUserPHID');
}
$pref_default = PhabricatorEmailTagsSetting::VALUE_EMAIL;
$pref_ignore = PhabricatorEmailTagsSetting::VALUE_IGNORE;
$keep = array();
foreach ($phids as $phid) {
if (($phid == $this->storyAuthorPHID) && !$this->getNotifyAuthor()) {
continue;
}
if ($tags && isset($all_prefs[$phid])) {
$mailtags = $all_prefs[$phid]->getSettingValue(
PhabricatorEmailTagsSetting::SETTINGKEY);
$notify = false;
foreach ($tags as $tag) {
// If this is set to "email" or "notify", notify the user.
if ((int)idx($mailtags, $tag, $pref_default) != $pref_ignore) {
$notify = true;
break;
}
}
if (!$notify) {
continue;
}
}
$keep[] = $phid;
}
return array_values(array_unique($keep));
}
private function expandRecipients(array $phids) {
- return id(new PhabricatorMetaMTAMemberQuery())
+ $expanded_phids = id(new PhabricatorMetaMTAMemberQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs($phids)
->executeExpansion();
+
+ // Filter out unexpandable PHIDs from the results. The typical case for
+ // this is that resigned reviewers should not be notified just because
+ // they are a member of some project or package reviewer.
+
+ $original_map = array_fuse($phids);
+ $unexpandable_map = array_fuse($this->unexpandablePHIDs);
+
+ foreach ($expanded_phids as $key => $phid) {
+ // We can keep this expanded PHID if it was present originally.
+ if (isset($original_map[$phid])) {
+ continue;
+ }
+
+ // We can also keep it if it isn't marked as unexpandable.
+ if (!isset($unexpandable_map[$phid])) {
+ continue;
+ }
+
+ // If it's unexpandable and we produced it by expanding recipients,
+ // throw it away.
+ unset($expanded_phids[$key]);
+ }
+ $expanded_phids = array_values($expanded_phids);
+
+ return $expanded_phids;
}
/**
* We generate a unique chronological key for each story type because we want
* to be able to page through the stream with a cursor (i.e., select stories
* after ID = X) so we can efficiently perform filtering after selecting data,
* and multiple stories with the same ID make this cumbersome without putting
* a bunch of logic in the client. We could use the primary key, but that
* would prevent publishing stories which happened in the past. Since it's
* potentially useful to do that (e.g., if you're importing another data
* source) build a unique key for each story which has chronological ordering.
*
* @return string A unique, time-ordered key which identifies the story.
*/
private function generateChronologicalKey() {
// Use the epoch timestamp for the upper 32 bits of the key. Default to
// the current time if the story doesn't have an explicit timestamp.
$time = nonempty($this->storyTime, time());
// Generate a random number for the lower 32 bits of the key.
$rand = head(unpack('L', Filesystem::readRandomBytes(4)));
// On 32-bit machines, we have to get creative.
if (PHP_INT_SIZE < 8) {
// We're on a 32-bit machine.
if (function_exists('bcadd')) {
// Try to use the 'bc' extension.
return bcadd(bcmul($time, bcpow(2, 32)), $rand);
} else {
// Do the math in MySQL. TODO: If we formalize a bc dependency, get
// rid of this.
$conn_r = id(new PhabricatorFeedStoryData())->establishConnection('r');
$result = queryfx_one(
$conn_r,
'SELECT (%d << 32) + %d as N',
$time,
$rand);
return $result['N'];
}
} else {
// This is a 64 bit machine, so we can just do the math.
return ($time << 32) + $rand;
}
}
}
diff --git a/src/applications/feed/storage/PhabricatorFeedStoryData.php b/src/applications/feed/storage/PhabricatorFeedStoryData.php
index 33d19fa61..051359002 100644
--- a/src/applications/feed/storage/PhabricatorFeedStoryData.php
+++ b/src/applications/feed/storage/PhabricatorFeedStoryData.php
@@ -1,69 +1,97 @@
<?php
-final class PhabricatorFeedStoryData extends PhabricatorFeedDAO {
+final class PhabricatorFeedStoryData
+ extends PhabricatorFeedDAO
+ implements PhabricatorDestructibleInterface {
protected $phid;
protected $storyType;
protected $storyData;
protected $authorPHID;
protected $chronologicalKey;
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'storyData' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'chronologicalKey' => 'uint64',
'storyType' => 'text64',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'chronologicalKey' => array(
'columns' => array('chronologicalKey'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPHIDConstants::PHID_TYPE_STRY);
}
public function getEpoch() {
if (PHP_INT_SIZE < 8) {
// We're on a 32-bit machine.
if (function_exists('bcadd')) {
// Try to use the 'bc' extension.
return bcdiv($this->chronologicalKey, bcpow(2, 32));
} else {
// Do the math in MySQL. TODO: If we formalize a bc dependency, get
// rid of this.
// See: PhabricatorFeedStoryPublisher::generateChronologicalKey()
$conn_r = id($this->establishConnection('r'));
$result = queryfx_one(
$conn_r,
// Insert the chronologicalKey as a string since longs don't seem to
// be supported by qsprintf and ints get maxed on 32 bit machines.
'SELECT (%s >> 32) as N',
$this->chronologicalKey);
return $result['N'];
}
} else {
return $this->chronologicalKey >> 32;
}
}
public function getValue($key, $default = null) {
return idx($this->storyData, $key, $default);
}
+
+/* -( PhabricatorDestructibleInterface )----------------------------------- */
+
+
+ public function destroyObjectPermanently(
+ PhabricatorDestructionEngine $engine) {
+
+ $this->openTransaction();
+ $conn = $this->establishConnection('w');
+
+ queryfx(
+ $conn,
+ 'DELETE FROM %T WHERE chronologicalKey = %s',
+ id(new PhabricatorFeedStoryNotification())->getTableName(),
+ $this->getChronologicalKey());
+
+ queryfx(
+ $conn,
+ 'DELETE FROM %T WHERE chronologicalKey = %s',
+ id(new PhabricatorFeedStoryReference())->getTableName(),
+ $this->getChronologicalKey());
+
+ $this->delete();
+ $this->saveTransaction();
+ }
+
}
diff --git a/src/applications/files/PhabricatorImageTransformer.php b/src/applications/files/PhabricatorImageTransformer.php
index df4243cfd..1075969a4 100644
--- a/src/applications/files/PhabricatorImageTransformer.php
+++ b/src/applications/files/PhabricatorImageTransformer.php
@@ -1,396 +1,138 @@
<?php
/**
* @task enormous Detecting Enormous Images
* @task save Saving Image Data
*/
final class PhabricatorImageTransformer extends Phobject {
- public function executeMemeTransform(
- PhabricatorFile $file,
- $upper_text,
- $lower_text) {
- $image = $this->applyMemeToFile($file, $upper_text, $lower_text);
- return PhabricatorFile::newFromFileData(
- $image,
- array(
- 'name' => 'meme-'.$file->getName(),
- 'ttl.relative' => phutil_units('24 hours in seconds'),
- 'canCDN' => true,
- ));
- }
-
- public function executeConpherenceTransform(
- PhabricatorFile $file,
- $top,
- $left,
- $width,
- $height) {
-
- $image = $this->crasslyCropTo(
- $file,
- $top,
- $left,
- $width,
- $height);
-
- return PhabricatorFile::newFromFileData(
- $image,
- array(
- 'name' => 'conpherence-'.$file->getName(),
- 'profile' => true,
- 'canCDN' => true,
- ));
- }
-
- private function crasslyCropTo(PhabricatorFile $file, $top, $left, $w, $h) {
- $data = $file->loadFileData();
- $src = imagecreatefromstring($data);
- $dst = $this->getBlankDestinationFile($w, $h);
-
- $scale = self::getScaleForCrop($file, $w, $h);
- $orig_x = $left / $scale;
- $orig_y = $top / $scale;
- $orig_w = $w / $scale;
- $orig_h = $h / $scale;
-
- imagecopyresampled(
- $dst,
- $src,
- 0, 0,
- $orig_x, $orig_y,
- $w, $h,
- $orig_w, $orig_h);
-
- return self::saveImageDataInAnyFormat($dst, $file->getMimeType());
- }
-
- private function getBlankDestinationFile($dx, $dy) {
- $dst = imagecreatetruecolor($dx, $dy);
- imagesavealpha($dst, true);
- imagefill($dst, 0, 0, imagecolorallocatealpha($dst, 255, 255, 255, 127));
-
- return $dst;
- }
-
- public static function getScaleForCrop(
- PhabricatorFile $file,
- $des_width,
- $des_height) {
-
- $metadata = $file->getMetadata();
- $width = $metadata[PhabricatorFile::METADATA_IMAGE_WIDTH];
- $height = $metadata[PhabricatorFile::METADATA_IMAGE_HEIGHT];
-
- if ($height < $des_height) {
- $scale = $height / $des_height;
- } else if ($width < $des_width) {
- $scale = $width / $des_width;
- } else {
- $scale_x = $des_width / $width;
- $scale_y = $des_height / $height;
- $scale = max($scale_x, $scale_y);
- }
-
- return $scale;
- }
-
- private function applyMemeToFile(
- PhabricatorFile $file,
- $upper_text,
- $lower_text) {
- $data = $file->loadFileData();
-
- $img_type = $file->getMimeType();
- $imagemagick = PhabricatorEnv::getEnvConfig('files.enable-imagemagick');
-
- if ($img_type != 'image/gif' || $imagemagick == false) {
- return $this->applyMemeTo(
- $data, $upper_text, $lower_text, $img_type);
- }
-
- $data = $file->loadFileData();
- $input = new TempFile();
- Filesystem::writeFile($input, $data);
-
- list($out) = execx('convert %s info:', $input);
- $split = phutil_split_lines($out);
- if (count($split) > 1) {
- return $this->applyMemeWithImagemagick(
- $input,
- $upper_text,
- $lower_text,
- count($split),
- $img_type);
- } else {
- return $this->applyMemeTo($data, $upper_text, $lower_text, $img_type);
- }
- }
-
- private function applyMemeTo(
- $data,
- $upper_text,
- $lower_text,
- $mime_type) {
- $img = imagecreatefromstring($data);
-
- // Some PNGs have color palettes, and allocating the dark border color
- // fails and gives us whatever's first in the color table. Copy the image
- // to a fresh truecolor canvas before working with it.
-
- $truecolor = imagecreatetruecolor(imagesx($img), imagesy($img));
- imagecopy($truecolor, $img, 0, 0, 0, 0, imagesx($img), imagesy($img));
- $img = $truecolor;
-
- $phabricator_root = dirname(phutil_get_library_root('phabricator'));
- $font_root = $phabricator_root.'/resources/font/';
- $font_path = $font_root.'tuffy.ttf';
- if (Filesystem::pathExists($font_root.'impact.ttf')) {
- $font_path = $font_root.'impact.ttf';
- }
- $text_color = imagecolorallocate($img, 255, 255, 255);
- $border_color = imagecolorallocatealpha($img, 0, 0, 0, 110);
- $border_width = 4;
- $font_max = 200;
- $font_min = 5;
- for ($i = $font_max; $i > $font_min; $i--) {
- $fit = $this->doesTextBoundingBoxFitInImage(
- $img,
- $upper_text,
- $i,
- $font_path);
- if ($fit['doesfit']) {
- $x = ($fit['imgwidth'] - $fit['txtwidth']) / 2;
- $y = $fit['txtheight'] + 10;
- $this->makeImageWithTextBorder($img,
- $i,
- $x,
- $y,
- $text_color,
- $border_color,
- $border_width,
- $font_path,
- $upper_text);
- break;
- }
- }
- for ($i = $font_max; $i > $font_min; $i--) {
- $fit = $this->doesTextBoundingBoxFitInImage($img,
- $lower_text, $i, $font_path);
- if ($fit['doesfit']) {
- $x = ($fit['imgwidth'] - $fit['txtwidth']) / 2;
- $y = $fit['imgheight'] - 10;
- $this->makeImageWithTextBorder(
- $img,
- $i,
- $x,
- $y,
- $text_color,
- $border_color,
- $border_width,
- $font_path,
- $lower_text);
- break;
- }
- }
- return self::saveImageDataInAnyFormat($img, $mime_type);
- }
-
- private function makeImageWithTextBorder($img, $font_size, $x, $y,
- $color, $stroke_color, $bw, $font, $text) {
- $angle = 0;
- $bw = abs($bw);
- for ($c1 = $x - $bw; $c1 <= $x + $bw; $c1++) {
- for ($c2 = $y - $bw; $c2 <= $y + $bw; $c2++) {
- if (!(($c1 == $x - $bw || $x + $bw) &&
- $c2 == $y - $bw || $c2 == $y + $bw)) {
- $bg = imagettftext($img, $font_size,
- $angle, $c1, $c2, $stroke_color, $font, $text);
- }
- }
- }
- imagettftext($img, $font_size, $angle,
- $x , $y, $color , $font, $text);
- }
-
- private function doesTextBoundingBoxFitInImage($img,
- $text, $font_size, $font_path) {
- // Default Angle = 0
- $angle = 0;
-
- $bbox = imagettfbbox($font_size, $angle, $font_path, $text);
- $text_height = abs($bbox[3] - $bbox[5]);
- $text_width = abs($bbox[0] - $bbox[2]);
- return array(
- 'doesfit' => ($text_height * 1.05 <= imagesy($img) / 2
- && $text_width * 1.05 <= imagesx($img)),
- 'txtwidth' => $text_width,
- 'txtheight' => $text_height,
- 'imgwidth' => imagesx($img),
- 'imgheight' => imagesy($img),
- );
- }
-
- private function applyMemeWithImagemagick(
- $input,
- $above,
- $below,
- $count,
- $img_type) {
-
- $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 < $count; $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->applyMemeTo(
- $frame_data,
- $above,
- $below,
- $img_type);
- Filesystem::writeFile($output_name, $memed_frame_data);
- }
-
- $future = new ExecFuture('convert -loop 0 %Ls %s', $output_files, $output);
- $future->setTimeout(10)->resolvex();
-
- return Filesystem::readFile($output);
- }
-
/* -( Saving Image Data )-------------------------------------------------- */
/**
* Save an image resource to a string representation suitable for storage or
* transmission as an image file.
*
* Optionally, you can specify a preferred MIME type like `"image/png"`.
* Generally, you should specify the MIME type of the original file if you're
* applying file transformations. The MIME type may not be honored if
* Phabricator can not encode images in the given format (based on available
* extensions), but can save images in another format.
*
* @param resource GD image resource.
* @param string? Optionally, preferred mime type.
* @return string Bytes of an image file.
* @task save
*/
public static function saveImageDataInAnyFormat($data, $preferred_mime = '') {
$preferred = null;
switch ($preferred_mime) {
case 'image/gif':
$preferred = self::saveImageDataAsGIF($data);
break;
case 'image/png':
$preferred = self::saveImageDataAsPNG($data);
break;
}
if ($preferred !== null) {
return $preferred;
}
$data = self::saveImageDataAsJPG($data);
if ($data !== null) {
return $data;
}
$data = self::saveImageDataAsPNG($data);
if ($data !== null) {
return $data;
}
$data = self::saveImageDataAsGIF($data);
if ($data !== null) {
return $data;
}
throw new Exception(pht('Failed to save image data into any format.'));
}
/**
* Save an image in PNG format, returning the file data as a string.
*
* @param resource GD image resource.
* @return string|null PNG file as a string, or null on failure.
* @task save
*/
private static function saveImageDataAsPNG($image) {
if (!function_exists('imagepng')) {
return null;
}
+ // NOTE: Empirically, the highest compression level (9) seems to take
+ // up to twice as long as the default compression level (6) but produce
+ // only slightly smaller files (10% on avatars, 3% on screenshots).
+
ob_start();
- $result = imagepng($image, null, 9);
+ $result = imagepng($image, null, 6);
$output = ob_get_clean();
if (!$result) {
return null;
}
return $output;
}
/**
* Save an image in GIF format, returning the file data as a string.
*
* @param resource GD image resource.
* @return string|null GIF file as a string, or null on failure.
* @task save
*/
private static function saveImageDataAsGIF($image) {
if (!function_exists('imagegif')) {
return null;
}
ob_start();
$result = imagegif($image);
$output = ob_get_clean();
if (!$result) {
return null;
}
return $output;
}
/**
* Save an image in JPG format, returning the file data as a string.
*
* @param resource GD image resource.
* @return string|null JPG file as a string, or null on failure.
* @task save
*/
private static function saveImageDataAsJPG($image) {
if (!function_exists('imagejpeg')) {
return null;
}
ob_start();
$result = imagejpeg($image);
$output = ob_get_clean();
if (!$result) {
return null;
}
return $output;
}
}
diff --git a/src/applications/files/application/PhabricatorFilesApplication.php b/src/applications/files/application/PhabricatorFilesApplication.php
index 1010818c1..5bf1ebccc 100644
--- a/src/applications/files/application/PhabricatorFilesApplication.php
+++ b/src/applications/files/application/PhabricatorFilesApplication.php
@@ -1,140 +1,146 @@
<?php
final class PhabricatorFilesApplication extends PhabricatorApplication {
public function getBaseURI() {
return '/file/';
}
public function getName() {
return pht('Files');
}
public function getShortDescription() {
return pht('Store and Share Files');
}
public function getIcon() {
return 'fa-file';
}
public function getTitleGlyph() {
return "\xE2\x87\xAA";
}
public function getFlavorText() {
return pht('Blob store for Pokemon pictures.');
}
public function getApplicationGroup() {
return self::GROUP_UTILITIES;
}
public function canUninstall() {
return false;
}
public function getRemarkupRules() {
return array(
new PhabricatorEmbedFileRemarkupRule(),
new PhabricatorImageRemarkupRule(),
);
}
public function supportsEmailIntegration() {
return true;
}
public function getAppEmailBlurb() {
return pht(
'Send emails with file attachments to these addresses to upload '.
'files. %s',
phutil_tag(
'a',
array(
'href' => $this->getInboundEmailSupportLink(),
),
pht('Learn More')));
}
protected function getCustomCapabilities() {
return array(
FilesDefaultViewCapability::CAPABILITY => array(
'caption' => pht('Default view policy for newly created files.'),
'template' => PhabricatorFileFilePHIDType::TYPECONST,
'capability' => PhabricatorPolicyCapability::CAN_VIEW,
),
);
}
public function getRoutes() {
return array(
- '/F(?P<id>[1-9]\d*)' => 'PhabricatorFileInfoController',
+ '/F(?P<id>[1-9]\d*)(?:\$(?P<lines>\d+(?:-\d+)?))?'
+ => 'PhabricatorFileViewController',
'/file/' => array(
'(query/(?P<queryKey>[^/]+)/)?' => 'PhabricatorFileListController',
+ 'view/(?P<id>[^/]+)/'.
+ '(?:(?P<engineKey>[^/]+)/)?'.
+ '(?:\$(?P<lines>\d+(?:-\d+)?))?'
+ => 'PhabricatorFileViewController',
+ 'info/(?P<phid>[^/]+)/' => 'PhabricatorFileViewController',
'upload/' => 'PhabricatorFileUploadController',
'dropupload/' => 'PhabricatorFileDropUploadController',
'compose/' => 'PhabricatorFileComposeController',
'comment/(?P<id>[1-9]\d*)/' => 'PhabricatorFileCommentController',
'thread/(?P<phid>[^/]+)/' => 'PhabricatorFileLightboxController',
'delete/(?P<id>[1-9]\d*)/' => 'PhabricatorFileDeleteController',
$this->getEditRoutePattern('edit/')
=> 'PhabricatorFileEditController',
- 'info/(?P<phid>[^/]+)/' => 'PhabricatorFileInfoController',
'imageproxy/' => 'PhabricatorFileImageProxyController',
'transforms/(?P<id>[1-9]\d*)/' =>
'PhabricatorFileTransformListController',
'uploaddialog/(?P<single>single/)?'
=> 'PhabricatorFileUploadDialogController',
- 'download/(?P<phid>[^/]+)/' => 'PhabricatorFileDialogController',
'iconset/(?P<key>[^/]+)/' => array(
'select/' => 'PhabricatorFileIconSetSelectController',
),
+ 'document/(?P<engineKey>[^/]+)/(?P<phid>[^/]+)/'
+ => 'PhabricatorFileDocumentController',
) + $this->getResourceSubroutes(),
);
}
public function getResourceRoutes() {
return array(
'/file/' => $this->getResourceSubroutes(),
);
}
private function getResourceSubroutes() {
return array(
- 'data/'.
+ '(?P<kind>data|download)/'.
'(?:@(?P<instance>[^/]+)/)?'.
'(?P<key>[^/]+)/'.
'(?P<phid>[^/]+)/'.
'(?:(?P<token>[^/]+)/)?'.
'.*'
=> 'PhabricatorFileDataController',
'xform/'.
'(?:@(?P<instance>[^/]+)/)?'.
'(?P<transform>[^/]+)/'.
'(?P<phid>[^/]+)/'.
'(?P<key>[^/]+)/'
=> 'PhabricatorFileTransformController',
);
}
public function getMailCommandObjects() {
return array(
'file' => array(
'name' => pht('Email Commands: Files'),
'header' => pht('Interacting with Files'),
'object' => new PhabricatorFile(),
'summary' => pht(
'This page documents the commands you can use to interact with '.
'files.'),
),
);
}
public function getQuicksandURIPatternBlacklist() {
return array(
- '/file/data/.*',
+ '/file/(data|download)/.*',
);
}
}
diff --git a/src/applications/files/builtin/PhabricatorFilesComposeAvatarBuiltinFile.php b/src/applications/files/builtin/PhabricatorFilesComposeAvatarBuiltinFile.php
index fcd6f9760..4f827f760 100644
--- a/src/applications/files/builtin/PhabricatorFilesComposeAvatarBuiltinFile.php
+++ b/src/applications/files/builtin/PhabricatorFilesComposeAvatarBuiltinFile.php
@@ -1,453 +1,469 @@
<?php
final class PhabricatorFilesComposeAvatarBuiltinFile
extends PhabricatorFilesBuiltinFile {
private $icon;
private $color;
private $border;
+ private $maps = array();
+
const VERSION = 'v1';
+ public function updateUser(PhabricatorUser $user) {
+ $username = $user->getUsername();
+
+ $image_map = $this->getMap('image');
+ $initial = phutil_utf8_strtoupper(substr($username, 0, 1));
+ $pack = $this->pickMap('pack', $username);
+ $icon = "alphanumeric/{$pack}/{$initial}.png";
+ if (!isset($image_map[$icon])) {
+ $icon = "alphanumeric/{$pack}/_default.png";
+ }
+
+ $border = $this->pickMap('border', $username);
+ $color = $this->pickMap('color', $username);
+
+ $data = $this->composeImage($color, $icon, $border);
+ $name = $this->getImageDisplayName($color, $icon, $border);
+
+ $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
+
+ $file = PhabricatorFile::newFromFileData(
+ $data,
+ array(
+ 'name' => $name,
+ 'profile' => true,
+ 'canCDN' => true,
+ ));
+
+ $user
+ ->setDefaultProfileImagePHID($file->getPHID())
+ ->setDefaultProfileImageVersion(self::VERSION)
+ ->saveWithoutIndex();
+
+ unset($unguarded);
+
+ return $file;
+ }
+
+ private function getMap($map_key) {
+ if (!isset($this->maps[$map_key])) {
+ switch ($map_key) {
+ case 'pack':
+ $map = $this->newPackMap();
+ break;
+ case 'image':
+ $map = $this->newImageMap();
+ break;
+ case 'color':
+ $map = $this->newColorMap();
+ break;
+ case 'border':
+ $map = $this->newBorderMap();
+ break;
+ default:
+ throw new Exception(pht('Unknown map "%s".', $map_key));
+ }
+ $this->maps[$map_key] = $map;
+ }
+
+ return $this->maps[$map_key];
+ }
+
+ private function pickMap($map_key, $username) {
+ $map = $this->getMap($map_key);
+ $seed = $username.'_'.$map_key;
+ $key = PhabricatorHash::digestToRange($seed, 0, count($map) - 1);
+ return $map[$key];
+ }
+
+
public function setIcon($icon) {
$this->icon = $icon;
return $this;
}
public function getIcon() {
return $this->icon;
}
public function setColor($color) {
$this->color = $color;
return $this;
}
public function getColor() {
return $this->color;
}
public function setBorder($border) {
$this->border = $border;
return $this;
}
public function getBorder() {
return $this->border;
}
public function getBuiltinFileKey() {
$icon = $this->getIcon();
$color = $this->getColor();
$border = implode(',', $this->getBorder());
$desc = "compose(icon={$icon}, color={$color}, border={$border}";
$hash = PhabricatorHash::digestToLength($desc, 40);
return "builtin:{$hash}";
}
public function getBuiltinDisplayName() {
- $icon = $this->getIcon();
- $color = $this->getColor();
- $border = implode(',', $this->getBorder());
+ return $this->getImageDisplayName(
+ $this->getIcon(),
+ $this->getColor(),
+ $this->getBorder());
+ }
+
+ private function getImageDisplayName($icon, $color, $border) {
+ $border = implode(',', $border);
return "{$icon}-{$color}-{$border}.png";
}
public function loadBuiltinFileData() {
return $this->composeImage(
- $this->getColor(), $this->getIcon(), $this->getBorder());
+ $this->getColor(),
+ $this->getIcon(),
+ $this->getBorder());
}
private function composeImage($color, $image, $border) {
// If we don't have the GD extension installed, just return a static
// default profile image rather than trying to compose a dynamic one.
if (!function_exists('imagecreatefromstring')) {
$root = dirname(phutil_get_library_root('phabricator'));
$default_path = $root.'/resources/builtin/profile.png';
return Filesystem::readFile($default_path);
}
$color_const = hexdec(trim($color, '#'));
$true_border = self::rgba2gd($border);
- $image_map = self::getImageMap();
+ $image_map = $this->getMap('image');
$data = Filesystem::readFile($image_map[$image]);
$img = imagecreatefromstring($data);
// 4 pixel border at 50x50, 32 pixel border at 400x400
$canvas = imagecreatetruecolor(400, 400);
$image_fill = imagefill($canvas, 0, 0, $color_const);
if (!$image_fill) {
throw new Exception(
pht('Failed to save builtin avatar image data (imagefill).'));
}
$border_thickness = imagesetthickness($canvas, 64);
if (!$border_thickness) {
throw new Exception(
pht('Failed to save builtin avatar image data (imagesetthickness).'));
}
$image_rectangle = imagerectangle($canvas, 0, 0, 400, 400, $true_border);
if (!$image_rectangle) {
throw new Exception(
pht('Failed to save builtin avatar image data (imagerectangle).'));
}
$image_copy = imagecopy($canvas, $img, 0, 0, 0, 0, 400, 400);
if (!$image_copy) {
throw new Exception(
pht('Failed to save builtin avatar image data (imagecopy).'));
}
return PhabricatorImageTransformer::saveImageDataInAnyFormat(
$canvas,
'image/png');
}
private static function rgba2gd($rgba) {
$r = $rgba[0];
$g = $rgba[1];
$b = $rgba[2];
$a = $rgba[3];
$a = (1 - $a) * 255;
- return ($a << 24) | ($r << 16) | ($g << 8) | $b;
+ return ($a << 24) | ($r << 16) | ($g << 8) | $b;
}
- public static function getImageMap() {
+ private function newImageMap() {
$root = dirname(phutil_get_library_root('phabricator'));
$root = $root.'/resources/builtin/alphanumeric/';
$map = array();
$list = id(new FileFinder($root))
->withType('f')
->withFollowSymlinks(true)
->find();
foreach ($list as $file) {
$map['alphanumeric/'.$file] = $root.$file;
}
return $map;
}
- public function getUniqueProfileImage($username) {
- $pack_map = $this->getImagePackMap();
- $image_map = $this->getImageMap();
- $color_map = $this->getColorMap();
- $border_map = $this->getBorderMap();
- $file = phutil_utf8_strtoupper(substr($username, 0, 1));
-
- $pack_count = count($pack_map);
- $color_count = count($color_map);
- $border_count = count($border_map);
-
- $pack_seed = $username.'_pack';
- $color_seed = $username.'_color';
- $border_seed = $username.'_border';
-
- $pack_key =
- PhabricatorHash::digestToRange($pack_seed, 0, $pack_count - 1);
- $color_key =
- PhabricatorHash::digestToRange($color_seed, 0, $color_count - 1);
- $border_key =
- PhabricatorHash::digestToRange($border_seed, 0, $border_count - 1);
-
- $pack = $pack_map[$pack_key];
- $icon = 'alphanumeric/'.$pack.'/'.$file.'.png';
- $color = $color_map[$color_key];
- $border = $border_map[$border_key];
-
- if (!isset($image_map[$icon])) {
- $icon = 'alphanumeric/'.$pack.'/_default.png';
- }
-
- return array('color' => $color, 'icon' => $icon, 'border' => $border);
- }
-
- public function getUserProfileImageFile($username) {
- $unique = $this->getUniqueProfileImage($username);
-
- $composer = id(new self())
- ->setIcon($unique['icon'])
- ->setColor($unique['color'])
- ->setBorder($unique['border']);
-
- $data = $composer->loadBuiltinFileData();
-
- $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
- $file = PhabricatorFile::newFromFileData(
- $data,
- array(
- 'name' => $composer->getBuiltinDisplayName(),
- 'profile' => true,
- 'canCDN' => true,
- ));
- unset($unguarded);
-
- return $file;
- }
-
- public static function getImagePackMap() {
+ private function newPackMap() {
$root = dirname(phutil_get_library_root('phabricator'));
$root = $root.'/resources/builtin/alphanumeric/';
$map = id(new FileFinder($root))
->withType('d')
->withFollowSymlinks(false)
->find();
+ $map = array_values($map);
- return array_values($map);
+ return $map;
}
- public static function getBorderMap() {
-
- $map = array(
+ private function newBorderMap() {
+ return array(
array(0, 0, 0, 0),
array(0, 0, 0, 0.3),
array(255, 255, 255, 0.4),
array(255, 255, 255, 0.7),
);
-
- return $map;
}
- public static function getColorMap() {
- //
- // Generated Colors
- // http://tools.medialab.sciences-po.fr/iwanthue/
- //
- $map = array(
+ private function newColorMap() {
+ // Via: http://tools.medialab.sciences-po.fr/iwanthue/
+
+ return array(
'#335862',
'#2d5192',
'#3c5da0',
'#99cd86',
'#704889',
'#5ac59e',
'#984060',
'#33d4d1',
'#9c4050',
'#20d8fd',
'#944937',
'#4bd0e3',
'#a25542',
'#4eb4f3',
'#6da8ec',
'#545608',
'#829ce5',
'#68681d',
'#607bc2',
'#4b69ad',
'#236ead',
'#31a0de',
'#4f8ed0',
'#846f2a',
'#bdb0f0',
'#518342',
'#9166aa',
'#5e904e',
'#f79dcc',
'#158e6b',
'#e189b7',
'#3ba984',
'#a85582',
'#4cccb7',
'#863d67',
'#84c08c',
'#7f4c7f',
'#a1bb7a',
'#65558f',
'#445082',
'#c9ca8e',
'#265582',
'#f4b189',
'#265582',
'#40b8e1',
'#814a28',
'#80c8f6',
'#cf7b5d',
'#1db5c7',
'#c0606e',
'#299a89',
'#ef8ead',
'#296437',
'#d39edb',
'#507436',
'#b888c9',
'#476025',
'#9987c5',
'#7867a3',
'#769b5a',
'#c46e9d',
'#437d4e',
'#d17492',
'#115e41',
'#ec8794',
'#297153',
'#d67381',
'#57c2c3',
'#bc607f',
'#86ceac',
'#7e3e53',
'#72c8b8',
'#884349',
'#45a998',
'#faa38c',
'#265582',
'#265582',
'#e4b788',
'#265582',
'#bbbc81',
'#265582',
'#ccb781',
'#265582',
'#eb957f',
'#15729c',
'#cf996f',
'#369bc5',
'#b6685d',
'#2da0a1',
'#d38275',
'#217e70',
'#ec9da1',
'#146268',
'#e8aa95',
'#3c6796',
'#8da667',
'#935f93',
'#69a573',
'#ae78ad',
'#569160',
'#d898be',
'#8eb4e8',
'#5e622c',
'#929ad3',
'#6c8548',
'#576196',
'#aed0a0',
'#694e79',
'#9abb8d',
'#8c5175',
'#6bb391',
'#8b4a5f',
'#519878',
'#ae7196',
'#3d8465',
'#e69eb3',
'#48663d',
'#cdaede',
'#71743d',
'#63acda',
'#7b5d30',
'#66bed6',
'#3585b0',
'#5880b0',
'#739acc',
'#48a3ba',
'#9d565b',
'#7fc4ca',
'#99566b',
'#94cabf',
'#7b4b49',
'#b1c8eb',
'#4e5632',
'#ecb2c3',
'#2d6158',
'#cf8287',
'#25889f',
'#b2696d',
'#6bafb6',
'#8c5744',
'#84b9d6',
'#9db3d6',
'#777cad',
'#826693',
'#86a779',
'#9d7fad',
'#b193c2',
'#547348',
'#d5adcb',
'#3f674d',
'#c98398',
'#66865a',
'#b2add6',
'#5a623d',
'#9793bb',
'#3c5472',
'#d5c5a1',
'#5e5a7f',
'#2c647e',
'#d8b194',
'#49607f',
'#c7b794',
'#335862',
'#e3aba7',
'#335862',
'#d9b9ad',
'#335862',
'#c48975',
'#347b81',
'#ad697e',
'#799a6d',
'#916b88',
'#69536b',
'#b4c4ad',
'#845865',
'#96b89d',
'#706d92',
'#9aa27a',
'#5b7292',
'#bc967b',
'#417792',
'#ce9793',
'#335862',
'#c898a5',
'#527a5f',
'#b38ba9',
'#648d72',
'#986b78',
'#79afa4',
'#966461',
'#50959b',
'#b27d7a',
'#335862',
'#335862',
'#bcadc4',
'#706343',
'#749ebc',
'#8c6a50',
'#92b8c4',
'#758cad',
'#868e67',
'#335862',
'#335862',
'#335862',
'#ac7e8b',
'#77a185',
'#807288',
'#636f51',
'#a192a9',
'#467a70',
'#9b7d73',
'#335862',
'#335862',
'#8c9c85',
'#335862',
'#81645a',
'#5f9489',
'#335862',
'#789da8',
'#335862',
'#72826c',
'#335862',
'#5c8596',
'#335862',
'#456a74',
'#335862',
'#335862',
'#335862',
);
- return $map;
}
}
diff --git a/src/applications/files/config/PhabricatorFilesConfigOptions.php b/src/applications/files/config/PhabricatorFilesConfigOptions.php
index 1493c54f5..751a06ffd 100644
--- a/src/applications/files/config/PhabricatorFilesConfigOptions.php
+++ b/src/applications/files/config/PhabricatorFilesConfigOptions.php
@@ -1,220 +1,222 @@
<?php
final class PhabricatorFilesConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Files');
}
public function getDescription() {
return pht('Configure files and file storage.');
}
public function getIcon() {
return 'fa-file';
}
public function getGroup() {
return 'apps';
}
public function getOptions() {
$viewable_default = array(
'image/jpeg' => 'image/jpeg',
'image/jpg' => 'image/jpg',
'image/png' => 'image/png',
'image/gif' => 'image/gif',
'text/plain' => 'text/plain; charset=utf-8',
'text/x-diff' => 'text/plain; charset=utf-8',
// ".ico" favicon files, which have mime type diversity. See:
// http://en.wikipedia.org/wiki/ICO_(file_format)#MIME_type
'image/x-ico' => 'image/x-icon',
'image/x-icon' => 'image/x-icon',
'image/vnd.microsoft.icon' => 'image/x-icon',
// This is a generic type for both OGG video and OGG audio.
'application/ogg' => 'application/ogg',
'audio/x-wav' => 'audio/x-wav',
'audio/mpeg' => 'audio/mpeg',
'audio/ogg' => 'audio/ogg',
'video/mp4' => 'video/mp4',
'video/ogg' => 'video/ogg',
'video/webm' => 'video/webm',
'video/quicktime' => 'video/quicktime',
+
+ 'application/pdf' => 'application/pdf',
);
$image_default = array(
'image/jpeg' => true,
'image/jpg' => true,
'image/png' => true,
'image/gif' => true,
'image/x-ico' => true,
'image/x-icon' => true,
'image/vnd.microsoft.icon' => true,
);
// The "application/ogg" type is listed as both an audio and video type,
// because it may contain either type of content.
$audio_default = array(
'audio/x-wav' => true,
'audio/mpeg' => true,
'audio/ogg' => true,
// These are video or ambiguous types, but can be forced to render as
// audio with `media=audio`, which seems to work properly in browsers.
// (For example, you can embed a music video as audio if you just want
// to set the mood for your task without distracting viewers.)
'video/mp4' => true,
'video/ogg' => true,
'video/quicktime' => true,
'application/ogg' => true,
);
$video_default = array(
'video/mp4' => true,
'video/ogg' => true,
'video/webm' => true,
'video/quicktime' => true,
'application/ogg' => true,
);
// largely lifted from http://en.wikipedia.org/wiki/Internet_media_type
$icon_default = array(
// audio file icon
'audio/basic' => 'fa-file-audio-o',
'audio/L24' => 'fa-file-audio-o',
'audio/mp4' => 'fa-file-audio-o',
'audio/mpeg' => 'fa-file-audio-o',
'audio/ogg' => 'fa-file-audio-o',
'audio/vorbis' => 'fa-file-audio-o',
'audio/vnd.rn-realaudio' => 'fa-file-audio-o',
'audio/vnd.wave' => 'fa-file-audio-o',
'audio/webm' => 'fa-file-audio-o',
// movie file icon
'video/mpeg' => 'fa-file-movie-o',
'video/mp4' => 'fa-file-movie-o',
'application/ogg' => 'fa-file-movie-o',
'video/ogg' => 'fa-file-movie-o',
'video/quicktime' => 'fa-file-movie-o',
'video/webm' => 'fa-file-movie-o',
'video/x-matroska' => 'fa-file-movie-o',
'video/x-ms-wmv' => 'fa-file-movie-o',
'video/x-flv' => 'fa-file-movie-o',
// pdf file icon
'application/pdf' => 'fa-file-pdf-o',
// zip file icon
'application/zip' => 'fa-file-zip-o',
// msword icon
'application/msword' => 'fa-file-word-o',
// msexcel
'application/vnd.ms-excel' => 'fa-file-excel-o',
// mspowerpoint
'application/vnd.ms-powerpoint' => 'fa-file-powerpoint-o',
) + array_fill_keys(array_keys($image_default), 'fa-file-image-o');
// NOTE: These options are locked primarily because adding "text/plain"
// as an image MIME type increases SSRF vulnerability by allowing users
// to load text files from remote servers as "images" (see T6755 for
// discussion).
return array(
$this->newOption('files.viewable-mime-types', 'wild', $viewable_default)
->setLocked(true)
->setSummary(
pht('Configure which MIME types are viewable in the browser.'))
->setDescription(
pht(
"Configure which uploaded file types may be viewed directly ".
"in the browser. Other file types will be downloaded instead ".
"of displayed. This is mainly a usability consideration, since ".
"browsers tend to freak out when viewing very large binary files.".
"\n\n".
"The keys in this map are viewable MIME types; the values are ".
"the MIME types they are delivered as when they are viewed in ".
"the browser.")),
$this->newOption('files.image-mime-types', 'set', $image_default)
->setLocked(true)
->setSummary(pht('Configure which MIME types are images.'))
->setDescription(
pht(
'List of MIME types which can be used as the `%s` for an `%s` tag.',
'src',
'<img />')),
$this->newOption('files.audio-mime-types', 'set', $audio_default)
->setLocked(true)
->setSummary(pht('Configure which MIME types are audio.'))
->setDescription(
pht(
'List of MIME types which can be rendered with an `%s` tag.',
'<audio />')),
$this->newOption('files.video-mime-types', 'set', $video_default)
->setLocked(true)
->setSummary(pht('Configure which MIME types are video.'))
->setDescription(
pht(
'List of MIME types which can be rendered with a `%s` tag.',
'<video />')),
$this->newOption('files.icon-mime-types', 'wild', $icon_default)
->setLocked(true)
->setSummary(pht('Configure which MIME types map to which icons.'))
->setDescription(
pht(
'Map of MIME type to icon name. MIME types which can not be '.
'found default to icon `%s`.',
'doc_files')),
$this->newOption('storage.mysql-engine.max-size', 'int', 1000000)
->setSummary(
pht(
'Configure the largest file which will be put into the MySQL '.
'storage engine.')),
$this->newOption('storage.local-disk.path', 'string', null)
->setLocked(true)
->setSummary(pht('Local storage disk path.'))
->setDescription(
pht(
"Phabricator provides a local disk storage engine, which just ".
"writes files to some directory on local disk. The webserver ".
"must have read/write permissions on this directory. This is ".
"straightforward and suitable for most installs, but will not ".
"scale past one web frontend unless the path is actually an NFS ".
"mount, since you'll end up with some of the files written to ".
"each web frontend and no way for them to share. To use the ".
"local disk storage engine, specify the path to a directory ".
"here. To disable it, specify null.")),
$this->newOption('storage.s3.bucket', 'string', null)
->setSummary(pht('Amazon S3 bucket.'))
->setDescription(
pht(
"Set this to a valid Amazon S3 bucket to store files there. You ".
"must also configure S3 access keys in the 'Amazon Web Services' ".
"group.")),
$this->newOption(
'metamta.files.subject-prefix',
'string',
'[File]')
->setDescription(pht('Subject prefix for Files email.')),
$this->newOption('files.enable-imagemagick', 'bool', false)
->setBoolOptions(
array(
pht('Enable'),
pht('Disable'),
))
->setDescription(
pht(
'This option will use Imagemagick to rescale images, so animated '.
'GIFs can be thumbnailed and set as profile pictures. Imagemagick '.
'must be installed and the "%s" binary must be available to '.
'the webserver for this to work.',
'convert')),
);
}
}
diff --git a/src/applications/files/controller/PhabricatorFileDataController.php b/src/applications/files/controller/PhabricatorFileDataController.php
index 98f2cdbd5..bd3d93328 100644
--- a/src/applications/files/controller/PhabricatorFileDataController.php
+++ b/src/applications/files/controller/PhabricatorFileDataController.php
@@ -1,195 +1,210 @@
<?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());
+ ->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);
}
- $is_viewable = $file->isViewableInBrowser();
- $force_download = $request->getExists('download');
+ if (!$file->isViewableInBrowser()) {
+ $is_download = true;
+ }
$request_type = $request->getHTTPHeader('X-Phabricator-Request-Type');
$is_lfs = ($request_type == 'git-lfs');
- if ($is_viewable && !$force_download) {
+ if (!$is_download) {
$response->setMimeType($file->getViewableMimeType());
} else {
- $is_public = !$viewer->isLoggedIn();
$is_post = $request->isHTTPPost();
- // NOTE: Require POST to download files from the primary domain if the
- // request includes credentials. The "Download File" links we generate
- // in the web UI are forms which use POST to satisfy this requirement.
-
- // The intent is to make attacks based on tags like "<iframe />" and
- // "<script />" (which can issue GET requests, but can not easily issue
- // POST requests) more difficult to execute.
+ // 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.
- // The best defense against these attacks is to use an alternate file
- // domain, which is why we strongly recommend doing so.
-
- $is_safe = ($is_alternate_domain || $is_lfs || $is_post || $is_public);
+ $is_safe = ($is_alternate_domain || $is_lfs || $is_post);
if (!$is_safe) {
- // This is marked as "external" because it is fully qualified.
- return id(new AphrontRedirectResponse())
- ->setIsExternal(true)
- ->setURI(PhabricatorEnv::getProductionURI($file->getBestURI()));
+ 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/controller/PhabricatorFileDocumentController.php b/src/applications/files/controller/PhabricatorFileDocumentController.php
new file mode 100644
index 000000000..15fadaa28
--- /dev/null
+++ b/src/applications/files/controller/PhabricatorFileDocumentController.php
@@ -0,0 +1,127 @@
+<?php
+
+final class PhabricatorFileDocumentController
+ extends PhabricatorFileController {
+
+ private $file;
+ private $engine;
+ private $ref;
+
+ public function shouldAllowPublic() {
+ return true;
+ }
+
+ public function handleRequest(AphrontRequest $request) {
+ $viewer = $request->getViewer();
+
+ $file_phid = $request->getURIData('phid');
+
+ $file = id(new PhabricatorFileQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(array($file_phid))
+ ->executeOne();
+ if (!$file) {
+ return $this->newErrorResponse(
+ pht(
+ 'This file ("%s") does not exist or could not be loaded.',
+ $file_phid));
+ }
+ $this->file = $file;
+
+ $ref = id(new PhabricatorDocumentRef())
+ ->setFile($file);
+ $this->ref = $ref;
+
+ $engines = PhabricatorDocumentEngine::getEnginesForRef($viewer, $ref);
+ $engine_key = $request->getURIData('engineKey');
+ 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->engine = $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);
+ }
+
+ try {
+ $content = $engine->newDocument($ref);
+ } catch (Exception $ex) {
+ return $this->newErrorResponse($ex->getMessage());
+ }
+
+ return $this->newContentResponse($content);
+ }
+
+ private 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) {
+ $viewer = $this->getViewer();
+ $request = $this->getRequest();
+
+ $file = $this->file;
+ $engine = $this->engine;
+ $ref = $this->ref;
+
+ if ($request->isAjax()) {
+ return id(new AphrontAjaxResponse())
+ ->setContent(
+ array(
+ 'markup' => hsprintf('%s', $content),
+ ));
+ }
+
+ $crumbs = $this->buildApplicationCrumbs();
+ if ($file) {
+ $crumbs->addTextCrumb($file->getMonogram(), $file->getInfoURI());
+ }
+
+ $label = $engine->getViewAsLabel($ref);
+ if ($label) {
+ $crumbs->addTextCrumb($label);
+ }
+
+ $crumbs->setBorder(true);
+
+ $content_frame = id(new PHUIObjectBoxView())
+ ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+ ->appendChild($content);
+
+ $page_frame = id(new PHUITwoColumnView())
+ ->setFooter($content_frame);
+
+ return $this->newPage()
+ ->setCrumbs($crumbs)
+ ->setTitle(
+ array(
+ $ref->getName(),
+ pht('Standalone'),
+ ))
+ ->appendChild($page_frame);
+ }
+
+}
diff --git a/src/applications/files/controller/PhabricatorFileImageProxyController.php b/src/applications/files/controller/PhabricatorFileImageProxyController.php
index 44573aea4..05a49c7f3 100644
--- a/src/applications/files/controller/PhabricatorFileImageProxyController.php
+++ b/src/applications/files/controller/PhabricatorFileImageProxyController.php
@@ -1,118 +1,147 @@
<?php
final class PhabricatorFileImageProxyController
extends PhabricatorFileController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
-
- $show_prototypes = PhabricatorEnv::getEnvConfig(
- 'phabricator.show-prototypes');
- if (!$show_prototypes) {
- throw new Exception(
- pht('Show prototypes is disabled.
- Set `phabricator.show-prototypes` to `true` to use the image proxy'));
- }
-
$viewer = $request->getViewer();
$img_uri = $request->getStr('uri');
// Validate the URI before doing anything
PhabricatorEnv::requireValidRemoteURIForLink($img_uri);
$uri = new PhutilURI($img_uri);
$proto = $uri->getProtocol();
- if (!in_array($proto, array('http', 'https'))) {
+
+ $allowed_protocols = array(
+ 'http',
+ 'https',
+ );
+ if (!in_array($proto, $allowed_protocols)) {
throw new Exception(
- pht('The provided image URI must be either http or https'));
+ pht(
+ 'The provided image URI must use one of these protocols: %s.',
+ implode(', ', $allowed_protocols)));
}
// Check if we already have the specified image URI downloaded
$cached_request = id(new PhabricatorFileExternalRequest())->loadOneWhere(
'uriIndex = %s',
PhabricatorHash::digestForIndex($img_uri));
if ($cached_request) {
return $this->getExternalResponse($cached_request);
}
$ttl = PhabricatorTime::getNow() + phutil_units('7 days in seconds');
$external_request = id(new PhabricatorFileExternalRequest())
->setURI($img_uri)
->setTTL($ttl);
+ // Cache missed, so we'll need to validate and download the image.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
- // Cache missed so we'll need to validate and download the image
+ $save_request = false;
try {
// Rate limit outbound fetches to make this mechanism less useful for
// scanning networks and ports.
PhabricatorSystemActionEngine::willTakeAction(
array($viewer->getPHID()),
new PhabricatorFilesOutboundRequestAction(),
1);
$file = PhabricatorFile::newFromFileDownload(
$uri,
array(
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
'canCDN' => true,
));
+
if (!$file->isViewableImage()) {
$mime_type = $file->getMimeType();
$engine = new PhabricatorDestructionEngine();
$engine->destroyObject($file);
$file = null;
throw new Exception(
pht(
- 'The URI "%s" does not correspond to a valid image file, got '.
- 'a file with MIME type "%s". You must specify the URI of a '.
+ 'The URI "%s" does not correspond to a valid image file (got '.
+ 'a file with MIME type "%s"). You must specify the URI of a '.
'valid image file.',
$uri,
$mime_type));
- } else {
- $file->save();
}
- $external_request->setIsSuccessful(true)
- ->setFilePHID($file->getPHID())
- ->save();
- unset($unguarded);
- return $this->getExternalResponse($external_request);
+ $file->save();
+
+ $external_request
+ ->setIsSuccessful(1)
+ ->setFilePHID($file->getPHID());
+
+ $save_request = true;
} catch (HTTPFutureHTTPResponseStatus $status) {
- $external_request->setIsSuccessful(false)
- ->setResponseMessage($status->getMessage())
- ->save();
- return $this->getExternalResponse($external_request);
+ $external_request
+ ->setIsSuccessful(0)
+ ->setResponseMessage($status->getMessage());
+
+ $save_request = true;
} catch (Exception $ex) {
// Not actually saving the request in this case
$external_request->setResponseMessage($ex->getMessage());
- return $this->getExternalResponse($external_request);
}
+
+ if ($save_request) {
+ try {
+ $external_request->save();
+ } catch (AphrontDuplicateKeyQueryException $ex) {
+ // We may have raced against another identical request. If we did,
+ // just throw our result away and use the winner's result.
+ $external_request = $external_request->loadOneWhere(
+ 'uriIndex = %s',
+ PhabricatorHash::digestForIndex($img_uri));
+ if (!$external_request) {
+ throw new Exception(
+ pht(
+ 'Hit duplicate key collision when saving proxied image, but '.
+ 'failed to load duplicate row (for URI "%s").',
+ $img_uri));
+ }
+ }
+ }
+
+ unset($unguarded);
+
+
+ return $this->getExternalResponse($external_request);
}
private function getExternalResponse(
PhabricatorFileExternalRequest $request) {
- if ($request->getIsSuccessful()) {
- $file = id(new PhabricatorFileQuery())
- ->setViewer(PhabricatorUser::getOmnipotentUser())
- ->withPHIDs(array($request->getFilePHID()))
- ->executeOne();
- if (!$file) {
- throw new Exception(pht(
+ if (!$request->getIsSuccessful()) {
+ throw new Exception(
+ pht(
+ 'Request to "%s" failed: %s',
+ $request->getURI(),
+ $request->getResponseMessage()));
+ }
+
+ $file = id(new PhabricatorFileQuery())
+ ->setViewer(PhabricatorUser::getOmnipotentUser())
+ ->withPHIDs(array($request->getFilePHID()))
+ ->executeOne();
+ if (!$file) {
+ throw new Exception(
+ pht(
'The underlying file does not exist, but the cached request was '.
- 'successful. This likely means the file record was manually deleted '.
- 'by an administrator.'));
- }
- return id(new AphrontRedirectResponse())
- ->setIsExternal(true)
- ->setURI($file->getViewURI());
- } else {
- throw new Exception(pht(
- "The request to get the external file from '%s' was unsuccessful:\n %s",
- $request->getURI(),
- $request->getResponseMessage()));
+ 'successful. This likely means the file record was manually '.
+ 'deleted by an administrator.'));
}
+
+ return id(new AphrontAjaxResponse())
+ ->setContent(
+ array(
+ 'imageURI' => $file->getViewURI(),
+ ));
}
}
diff --git a/src/applications/files/controller/PhabricatorFileInfoController.php b/src/applications/files/controller/PhabricatorFileViewController.php
similarity index 71%
rename from src/applications/files/controller/PhabricatorFileInfoController.php
rename to src/applications/files/controller/PhabricatorFileViewController.php
index f30421c13..9d32ff6fb 100644
--- a/src/applications/files/controller/PhabricatorFileInfoController.php
+++ b/src/applications/files/controller/PhabricatorFileViewController.php
@@ -1,458 +1,524 @@
<?php
-final class PhabricatorFileInfoController extends PhabricatorFileController {
+final class PhabricatorFileViewController extends PhabricatorFileController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$phid = $request->getURIData('phid');
if ($phid) {
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($phid))
->withIsDeleted(false)
->executeOne();
if (!$file) {
return new Aphront404Response();
}
return id(new AphrontRedirectResponse())->setURI($file->getInfoURI());
}
+
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withIDs(array($id))
->withIsDeleted(false)
->executeOne();
if (!$file) {
return new Aphront404Response();
}
$phid = $file->getPHID();
$header = id(new PHUIHeaderView())
->setUser($viewer)
->setPolicyObject($file)
->setHeader($file->getName())
->setHeaderIcon('fa-file-o');
$ttl = $file->getTTL();
if ($ttl !== null) {
$ttl_tag = id(new PHUITagView())
->setType(PHUITagView::TYPE_SHADE)
->setColor(PHUITagView::COLOR_YELLOW)
->setName(pht('Temporary'));
$header->addTag($ttl_tag);
}
$partial = $file->getIsPartial();
if ($partial) {
$partial_tag = id(new PHUITagView())
->setType(PHUITagView::TYPE_SHADE)
->setColor(PHUITagView::COLOR_ORANGE)
->setName(pht('Partial Upload'));
$header->addTag($partial_tag);
}
$curtain = $this->buildCurtainView($file);
$timeline = $this->buildTransactionView($file);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(
- 'F'.$file->getID(),
- $this->getApplicationURI("/info/{$phid}/"));
+ $file->getMonogram(),
+ $file->getInfoURI());
$crumbs->setBorder(true);
$object_box = id(new PHUIObjectBoxView())
- ->setHeaderText(pht('File'))
+ ->setHeaderText(pht('File Metadata'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY);
$this->buildPropertyViews($object_box, $file);
$title = $file->getName();
+ $file_content = $this->newFileContent($file);
+
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setCurtain($curtain)
- ->setMainColumn(array(
- $object_box,
- $timeline,
- ));
+ ->setMainColumn(
+ array(
+ $object_box,
+ $file_content,
+ $timeline,
+ ));
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->setPageObjectPHIDs(array($file->getPHID()))
->appendChild($view);
-
}
private function buildTransactionView(PhabricatorFile $file) {
$viewer = $this->getViewer();
$timeline = $this->buildTransactionTimeline(
$file,
new PhabricatorFileTransactionQuery());
$comment_view = id(new PhabricatorFileEditEngine())
->setViewer($viewer)
->buildEditEngineCommentView($file);
$monogram = $file->getMonogram();
$timeline->setQuoteRef($monogram);
$comment_view->setTransactionTimeline($timeline);
return array(
$timeline,
$comment_view,
);
}
private function buildCurtainView(PhabricatorFile $file) {
$viewer = $this->getViewer();
$id = $file->getID();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$file,
PhabricatorPolicyCapability::CAN_EDIT);
$curtain = $this->newCurtainView($file);
$can_download = !$file->getIsPartial();
if ($file->isViewableInBrowser()) {
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('View File'))
->setIcon('fa-file-o')
->setHref($file->getViewURI())
->setDisabled(!$can_download)
->setWorkflow(!$can_download));
} else {
$curtain->addAction(
id(new PhabricatorActionView())
->setUser($viewer)
- ->setRenderAsForm($can_download)
->setDownload($can_download)
->setName(pht('Download File'))
->setIcon('fa-download')
- ->setHref($file->getViewURI())
+ ->setHref($file->getDownloadURI())
->setDisabled(!$can_download)
->setWorkflow(!$can_download));
}
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit File'))
->setIcon('fa-pencil')
->setHref($this->getApplicationURI("/edit/{$id}/"))
->setWorkflow(!$can_edit)
->setDisabled(!$can_edit));
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Delete File'))
->setIcon('fa-times')
->setHref($this->getApplicationURI("/delete/{$id}/"))
->setWorkflow(true)
->setDisabled(!$can_edit));
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('View Transforms'))
->setIcon('fa-crop')
->setHref($this->getApplicationURI("/transforms/{$id}/")));
return $curtain;
}
private function buildPropertyViews(
PHUIObjectBoxView $box,
PhabricatorFile $file) {
$request = $this->getRequest();
$viewer = $request->getUser();
$tab_group = id(new PHUITabGroupView());
$box->addTabGroup($tab_group);
$properties = id(new PHUIPropertyListView());
$tab_group->addTab(
id(new PHUITabView())
->setName(pht('Details'))
->setKey('details')
->appendChild($properties));
if ($file->getAuthorPHID()) {
$properties->addProperty(
pht('Author'),
$viewer->renderHandle($file->getAuthorPHID()));
}
$properties->addProperty(
pht('Created'),
phabricator_datetime($file->getDateCreated(), $viewer));
$finfo = id(new PHUIPropertyListView());
$tab_group->addTab(
id(new PHUITabView())
->setName(pht('File Info'))
->setKey('info')
->appendChild($finfo));
$finfo->addProperty(
pht('Size'),
phutil_format_bytes($file->getByteSize()));
$finfo->addProperty(
pht('Mime Type'),
$file->getMimeType());
$ttl = $file->getTtl();
if ($ttl) {
$delta = $ttl - PhabricatorTime::getNow();
$finfo->addProperty(
pht('Expires'),
pht(
'%s (%s)',
phabricator_datetime($ttl, $viewer),
phutil_format_relative_time_detailed($delta)));
}
$width = $file->getImageWidth();
if ($width) {
$finfo->addProperty(
pht('Width'),
pht('%s px', new PhutilNumber($width)));
}
$height = $file->getImageHeight();
if ($height) {
$finfo->addProperty(
pht('Height'),
pht('%s px', new PhutilNumber($height)));
}
$is_image = $file->isViewableImage();
if ($is_image) {
$image_string = pht('Yes');
$cache_string = $file->getCanCDN() ? pht('Yes') : pht('No');
} else {
$image_string = pht('No');
$cache_string = pht('Not Applicable');
}
$types = array();
if ($file->isViewableImage()) {
$types[] = pht('Image');
}
if ($file->isVideo()) {
$types[] = pht('Video');
}
if ($file->isAudio()) {
$types[] = pht('Audio');
}
if ($file->getCanCDN()) {
$types[] = pht('Can CDN');
}
$builtin = $file->getBuiltinName();
if ($builtin !== null) {
$types[] = pht('Builtin ("%s")', $builtin);
}
if ($file->getIsProfileImage()) {
$types[] = pht('Profile');
}
if ($types) {
$types = implode(', ', $types);
$finfo->addProperty(pht('Attributes'), $types);
}
$storage_properties = new PHUIPropertyListView();
$tab_group->addTab(
id(new PHUITabView())
->setName(pht('Storage'))
->setKey('storage')
->appendChild($storage_properties));
$storage_properties->addProperty(
pht('Engine'),
$file->getStorageEngine());
$engine = $this->loadStorageEngine($file);
if ($engine && $engine->isChunkEngine()) {
$format_name = pht('Chunks');
} else {
$format_key = $file->getStorageFormat();
$format = PhabricatorFileStorageFormat::getFormat($format_key);
if ($format) {
$format_name = $format->getStorageFormatName();
} else {
$format_name = pht('Unknown ("%s")', $format_key);
}
}
$storage_properties->addProperty(pht('Format'), $format_name);
$storage_properties->addProperty(
pht('Handle'),
$file->getStorageHandle());
$phids = $file->getObjectPHIDs();
if ($phids) {
$attached = new PHUIPropertyListView();
$tab_group->addTab(
id(new PHUITabView())
->setName(pht('Attached'))
->setKey('attached')
->appendChild($attached));
$attached->addProperty(
pht('Attached To'),
$viewer->renderHandleList($phids));
}
- if ($file->isViewableImage()) {
- $image = phutil_tag(
- 'img',
- array(
- 'src' => $file->getViewURI(),
- 'class' => 'phui-property-list-image',
- ));
-
- $linked_image = phutil_tag(
- 'a',
- array(
- 'href' => $file->getViewURI(),
- ),
- $image);
-
- $media = id(new PHUIPropertyListView())
- ->addImageContent($linked_image);
-
- $box->addPropertyList($media);
- } else if ($file->isVideo()) {
- $video = phutil_tag(
- 'video',
- array(
- 'controls' => 'controls',
- 'class' => 'phui-property-list-video',
- ),
- phutil_tag(
- 'source',
- array(
- 'src' => $file->getViewURI(),
- 'type' => $file->getMimeType(),
- )));
- $media = id(new PHUIPropertyListView())
- ->addImageContent($video);
-
- $box->addPropertyList($media);
- } else if ($file->isAudio()) {
- $audio = phutil_tag(
- 'audio',
- array(
- 'controls' => 'controls',
- 'class' => 'phui-property-list-audio',
- ),
- phutil_tag(
- 'source',
- array(
- 'src' => $file->getViewURI(),
- 'type' => $file->getMimeType(),
- )));
- $media = id(new PHUIPropertyListView())
- ->addImageContent($audio);
-
- $box->addPropertyList($media);
- }
-
$engine = $this->loadStorageEngine($file);
if ($engine) {
if ($engine->isChunkEngine()) {
$chunkinfo = new PHUIPropertyListView();
$tab_group->addTab(
id(new PHUITabView())
->setName(pht('Chunks'))
->setKey('chunks')
->appendChild($chunkinfo));
$chunks = id(new PhabricatorFileChunkQuery())
->setViewer($viewer)
->withChunkHandles(array($file->getStorageHandle()))
->execute();
$chunks = msort($chunks, 'getByteStart');
$rows = array();
$completed = array();
foreach ($chunks as $chunk) {
$is_complete = $chunk->getDataFilePHID();
$rows[] = array(
$chunk->getByteStart(),
$chunk->getByteEnd(),
($is_complete ? pht('Yes') : pht('No')),
);
if ($is_complete) {
$completed[] = $chunk;
}
}
$table = id(new AphrontTableView($rows))
->setHeaders(
array(
pht('Offset'),
pht('End'),
pht('Complete'),
))
->setColumnClasses(
array(
'',
'',
'wide',
));
$chunkinfo->addProperty(
pht('Total Chunks'),
count($chunks));
$chunkinfo->addProperty(
pht('Completed Chunks'),
count($completed));
$chunkinfo->addRawContent($table);
}
}
}
private function loadStorageEngine(PhabricatorFile $file) {
$engine = null;
try {
$engine = $file->instantiateStorageEngine();
} catch (Exception $ex) {
// Don't bother raising this anywhere for now.
}
return $engine;
}
+ private function newFileContent(PhabricatorFile $file) {
+ $viewer = $this->getViewer();
+ $request = $this->getRequest();
+
+ $ref = id(new PhabricatorDocumentRef())
+ ->setFile($file);
+
+ $engines = PhabricatorDocumentEngine::getEnginesForRef($viewer, $ref);
+
+ $engine_key = $request->getURIData('engineKey');
+ if (!isset($engines[$engine_key])) {
+ $engine_key = head_key($engines);
+ }
+ $engine = $engines[$engine_key];
+
+ $lines = $request->getURILineRange('lines', 1000);
+ 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);
+ }
+
+ $views = array();
+ foreach ($engines as $candidate_key => $candidate_engine) {
+ $label = $candidate_engine->getViewAsLabel($ref);
+ if ($label === null) {
+ continue;
+ }
+
+ $view_uri = '/file/view/'.$file->getID().'/'.$candidate_key.'/';
+
+ $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' => $candidate_engine->getRenderURI($ref),
+ 'viewURI' => $view_uri,
+ 'loadingMarkup' => hsprintf('%s', $loading),
+ 'canEncode' => $candidate_engine->canConfigureEncoding($ref),
+ 'canHighlight' => $candidate_engine->CanConfigureHighlighting($ref),
+ );
+ }
+
+ $viewport_id = celerity_generate_unique_node_id();
+ $control_id = celerity_generate_unique_node_id();
+ $icon = $engine->newDocumentIcon($ref);
+
+ if ($engine->shouldRenderAsync($ref)) {
+ $content = $engine->newLoadingContent($ref);
+ $config = array(
+ 'renderControlID' => $control_id,
+ );
+ } else {
+ $content = $engine->newDocument($ref);
+ $config = array();
+ }
+
+ Javelin::initBehavior('document-engine', $config);
+
+ $viewport = phutil_tag(
+ 'div',
+ array(
+ 'id' => $viewport_id,
+ ),
+ $content);
+
+ $meta = array(
+ 'viewportID' => $viewport_id,
+ 'viewKey' => $engine->getDocumentEngineKey(),
+ 'views' => $views,
+ 'standaloneURI' => $engine->getRenderURI($ref),
+ '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,
+ ),
+ );
+
+ $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);
+ }
}
diff --git a/src/applications/files/document/PhabricatorAudioDocumentEngine.php b/src/applications/files/document/PhabricatorAudioDocumentEngine.php
new file mode 100644
index 000000000..859f16762
--- /dev/null
+++ b/src/applications/files/document/PhabricatorAudioDocumentEngine.php
@@ -0,0 +1,69 @@
+<?php
+
+final class PhabricatorAudioDocumentEngine
+ extends PhabricatorDocumentEngine {
+
+ const ENGINEKEY = 'audio';
+
+ public function getViewAsLabel(PhabricatorDocumentRef $ref) {
+ return pht('View as Audio');
+ }
+
+ protected function getDocumentIconIcon(PhabricatorDocumentRef $ref) {
+ return 'fa-file-sound-o';
+ }
+
+ protected function getByteLengthLimit() {
+ return null;
+ }
+
+ protected function canRenderDocumentType(PhabricatorDocumentRef $ref) {
+ $file = $ref->getFile();
+ if ($file) {
+ return $file->isAudio();
+ }
+
+ $viewable_types = PhabricatorEnv::getEnvConfig('files.viewable-mime-types');
+ $viewable_types = array_keys($viewable_types);
+
+ $audio_types = PhabricatorEnv::getEnvConfig('files.audio-mime-types');
+ $audio_types = array_keys($audio_types);
+
+ return
+ $ref->hasAnyMimeType($viewable_types) &&
+ $ref->hasAnyMimeType($audio_types);
+ }
+
+ protected function newDocumentContent(PhabricatorDocumentRef $ref) {
+ $file = $ref->getFile();
+ if ($file) {
+ $source_uri = $file->getViewURI();
+ } else {
+ throw new PhutilMethodNotImplementedException();
+ }
+
+ $mime_type = $ref->getMimeType();
+
+ $audio = phutil_tag(
+ 'audio',
+ array(
+ 'controls' => 'controls',
+ ),
+ phutil_tag(
+ 'source',
+ array(
+ 'src' => $source_uri,
+ 'type' => $mime_type,
+ )));
+
+ $container = phutil_tag(
+ 'div',
+ array(
+ 'class' => 'document-engine-audio',
+ ),
+ $audio);
+
+ return $container;
+ }
+
+}
diff --git a/src/applications/files/document/PhabricatorDocumentEngine.php b/src/applications/files/document/PhabricatorDocumentEngine.php
new file mode 100644
index 000000000..c3a1a7317
--- /dev/null
+++ b/src/applications/files/document/PhabricatorDocumentEngine.php
@@ -0,0 +1,242 @@
+<?php
+
+abstract class PhabricatorDocumentEngine
+ extends Phobject {
+
+ private $viewer;
+ private $highlightedLines = array();
+ private $encodingConfiguration;
+ private $highlightingConfiguration;
+
+ 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;
+ }
+
+ 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;
+ }
+
+ 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;
+ }
+
+ public function getRenderURI(PhabricatorDocumentRef $ref) {
+ $file = $ref->getFile();
+ if (!$file) {
+ throw new PhutilMethodNotImplementedException();
+ }
+
+ $engine_key = $this->getDocumentEngineKey();
+ $file_phid = $file->getPHID();
+
+ return "/file/document/{$engine_key}/{$file_phid}/";
+ }
+
+ 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
new file mode 100644
index 000000000..cca0c102e
--- /dev/null
+++ b/src/applications/files/document/PhabricatorDocumentRef.php
@@ -0,0 +1,134 @@
+<?php
+
+final class PhabricatorDocumentRef
+ extends Phobject {
+
+ private $name;
+ private $mimeType;
+ private $file;
+ private $byteLength;
+ private $snippet;
+
+ 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)) {
+ return false;
+ }
+
+ return phutil_is_utf8($snippet);
+ }
+
+ public function getSnippet() {
+ if ($this->snippet === null) {
+ $this->snippet = $this->loadData(null, (1024 * 1024 * 1));
+ }
+
+ return $this->snippet;
+ }
+
+}
diff --git a/src/applications/files/document/PhabricatorHexdumpDocumentEngine.php b/src/applications/files/document/PhabricatorHexdumpDocumentEngine.php
new file mode 100644
index 000000000..4217c24d6
--- /dev/null
+++ b/src/applications/files/document/PhabricatorHexdumpDocumentEngine.php
@@ -0,0 +1,114 @@
+<?php
+
+final class PhabricatorHexdumpDocumentEngine
+ extends PhabricatorDocumentEngine {
+
+ const ENGINEKEY = 'hexdump';
+
+ public function getViewAsLabel(PhabricatorDocumentRef $ref) {
+ return pht('View as Hexdump');
+ }
+
+ protected function getDocumentIconIcon(PhabricatorDocumentRef $ref) {
+ return 'fa-microchip';
+ }
+
+ protected function getByteLengthLimit() {
+ return (1024 * 1024 * 1);
+ }
+
+ protected function getContentScore(PhabricatorDocumentRef $ref) {
+ return 500;
+ }
+
+ protected function canRenderDocumentType(PhabricatorDocumentRef $ref) {
+ return true;
+ }
+
+ protected function canRenderPartialDocument(PhabricatorDocumentRef $ref) {
+ return true;
+ }
+
+ protected function newDocumentContent(PhabricatorDocumentRef $ref) {
+ $limit = $this->getByteLengthLimit();
+ $length = $ref->getByteLength();
+
+ $is_partial = false;
+ if ($limit) {
+ if ($length > $limit) {
+ $is_partial = true;
+ $length = $limit;
+ }
+ }
+
+ $content = $ref->loadData(null, $length);
+
+ $output = array();
+ $offset = 0;
+
+ $lines = str_split($content, 16);
+ foreach ($lines as $line) {
+ $output[] = sprintf(
+ '%08x %- 23s %- 23s %- 16s',
+ $offset,
+ $this->renderHex(substr($line, 0, 8)),
+ $this->renderHex(substr($line, 8)),
+ $this->renderBytes($line));
+
+ $offset += 16;
+ }
+
+ $output = implode("\n", $output);
+
+ $container = phutil_tag(
+ 'div',
+ array(
+ 'class' => 'document-engine-hexdump PhabricatorMonospaced',
+ ),
+ $output);
+
+ $message = null;
+ if ($is_partial) {
+ $message = $this->newMessage(
+ pht(
+ 'This document is too large to be completely rendered inline. The '.
+ 'first %s bytes are shown.',
+ new PhutilNumber($limit)));
+ }
+
+ return array(
+ $message,
+ $container,
+ );
+ }
+
+ private function renderHex($bytes) {
+ $length = strlen($bytes);
+
+ $output = array();
+ for ($ii = 0; $ii < $length; $ii++) {
+ $output[] = sprintf('%02x', ord($bytes[$ii]));
+ }
+
+ return implode(' ', $output);
+ }
+
+ private function renderBytes($bytes) {
+ $length = strlen($bytes);
+
+ $output = array();
+ for ($ii = 0; $ii < $length; $ii++) {
+ $chr = $bytes[$ii];
+ $ord = ord($chr);
+
+ if ($ord < 0x20 || $ord >= 0x7F) {
+ $chr = '.';
+ }
+
+ $output[] = $chr;
+ }
+
+ return implode('', $output);
+ }
+
+}
diff --git a/src/applications/files/document/PhabricatorImageDocumentEngine.php b/src/applications/files/document/PhabricatorImageDocumentEngine.php
new file mode 100644
index 000000000..d0b2099dd
--- /dev/null
+++ b/src/applications/files/document/PhabricatorImageDocumentEngine.php
@@ -0,0 +1,71 @@
+<?php
+
+final class PhabricatorImageDocumentEngine
+ extends PhabricatorDocumentEngine {
+
+ const ENGINEKEY = 'image';
+
+ public function getViewAsLabel(PhabricatorDocumentRef $ref) {
+ return pht('View as Image');
+ }
+
+ protected function getDocumentIconIcon(PhabricatorDocumentRef $ref) {
+ return 'fa-file-image-o';
+ }
+
+ protected function getByteLengthLimit() {
+ return (1024 * 1024 * 64);
+ }
+
+ protected function canRenderDocumentType(PhabricatorDocumentRef $ref) {
+ $file = $ref->getFile();
+ if ($file) {
+ return $file->isViewableImage();
+ }
+
+ $viewable_types = PhabricatorEnv::getEnvConfig('files.viewable-mime-types');
+ $viewable_types = array_keys($viewable_types);
+
+ $image_types = PhabricatorEnv::getEnvConfig('files.image-mime-types');
+ $image_types = array_keys($image_types);
+
+ return
+ $ref->hasAnyMimeType($viewable_types) &&
+ $ref->hasAnyMimeType($image_types);
+ }
+
+ protected function newDocumentContent(PhabricatorDocumentRef $ref) {
+ $file = $ref->getFile();
+ if ($file) {
+ $source_uri = $file->getViewURI();
+ } else {
+ // We could use a "data:" URI here. It's not yet clear if or when we'll
+ // have a ref but no backing file.
+ throw new PhutilMethodNotImplementedException();
+ }
+
+ $image = phutil_tag(
+ 'img',
+ array(
+ 'src' => $source_uri,
+ ));
+
+ $linked_image = phutil_tag(
+ 'a',
+ array(
+ 'href' => $source_uri,
+ 'rel' => 'noreferrer',
+ ),
+ $image);
+
+ $container = phutil_tag(
+ 'div',
+ array(
+ 'class' => 'document-engine-image',
+ ),
+ $linked_image);
+
+ return $container;
+ }
+
+}
diff --git a/src/applications/files/document/PhabricatorJSONDocumentEngine.php b/src/applications/files/document/PhabricatorJSONDocumentEngine.php
new file mode 100644
index 000000000..331a7e682
--- /dev/null
+++ b/src/applications/files/document/PhabricatorJSONDocumentEngine.php
@@ -0,0 +1,59 @@
+<?php
+
+final class PhabricatorJSONDocumentEngine
+ extends PhabricatorTextDocumentEngine {
+
+ const ENGINEKEY = 'json';
+
+ public function getViewAsLabel(PhabricatorDocumentRef $ref) {
+ return pht('View as JSON');
+ }
+
+ protected function getDocumentIconIcon(PhabricatorDocumentRef $ref) {
+ return 'fa-database';
+ }
+
+ protected function getContentScore(PhabricatorDocumentRef $ref) {
+ if (preg_match('/\.json\z/', $ref->getName())) {
+ return 2000;
+ }
+
+ if ($ref->isProbablyJSON()) {
+ return 1750;
+ }
+
+ return 500;
+ }
+
+ protected function newDocumentContent(PhabricatorDocumentRef $ref) {
+ $raw_data = $this->loadTextData($ref);
+
+ try {
+ $data = phutil_json_decode($raw_data);
+
+ if (preg_match('/^\s*\[/', $raw_data)) {
+ $content = id(new PhutilJSON())->encodeAsList($data);
+ } else {
+ $content = id(new PhutilJSON())->encodeFormatted($data);
+ }
+
+ $message = null;
+ $content = PhabricatorSyntaxHighlighter::highlightWithLanguage(
+ 'json',
+ $content);
+ } catch (PhutilJSONParserException $ex) {
+ $message = $this->newMessage(
+ pht(
+ 'This document is not valid JSON: %s',
+ $ex->getMessage()));
+
+ $content = $raw_data;
+ }
+
+ return array(
+ $message,
+ $this->newTextDocumentContent($content),
+ );
+ }
+
+}
diff --git a/src/applications/files/document/PhabricatorJupyterDocumentEngine.php b/src/applications/files/document/PhabricatorJupyterDocumentEngine.php
new file mode 100644
index 000000000..90b9d33c0
--- /dev/null
+++ b/src/applications/files/document/PhabricatorJupyterDocumentEngine.php
@@ -0,0 +1,312 @@
+<?php
+
+final class PhabricatorJupyterDocumentEngine
+ extends PhabricatorDocumentEngine {
+
+ const ENGINEKEY = 'jupyter';
+
+ public function getViewAsLabel(PhabricatorDocumentRef $ref) {
+ return pht('View as Jupyter Notebook');
+ }
+
+ protected function getDocumentIconIcon(PhabricatorDocumentRef $ref) {
+ return 'fa-sun-o';
+ }
+
+ protected function getDocumentRenderingText(PhabricatorDocumentRef $ref) {
+ return pht('Rendering Jupyter Notebook...');
+ }
+
+ public function shouldRenderAsync(PhabricatorDocumentRef $ref) {
+ return true;
+ }
+
+ protected function getContentScore(PhabricatorDocumentRef $ref) {
+ $name = $ref->getName();
+
+ if (preg_match('/\\.ipynb\z/i', $name)) {
+ return 2000;
+ }
+
+ return 500;
+ }
+
+ protected function canRenderDocumentType(PhabricatorDocumentRef $ref) {
+ return $ref->isProbablyJSON();
+ }
+
+ protected function newDocumentContent(PhabricatorDocumentRef $ref) {
+ $viewer = $this->getViewer();
+ $content = $ref->loadData();
+
+ try {
+ $data = phutil_json_decode($content);
+ } catch (PhutilJSONParserException $ex) {
+ return $this->newMessage(
+ pht(
+ 'This is not a valid JSON document and can not be rendered as '.
+ 'a Jupyter notebook: %s.',
+ $ex->getMessage()));
+ }
+
+ if (!is_array($data)) {
+ return $this->newMessage(
+ pht(
+ 'This document does not encode a valid JSON object and can not '.
+ 'be rendered as a Jupyter notebook.'));
+ }
+
+
+ $nbformat = idx($data, 'nbformat');
+ if (!strlen($nbformat)) {
+ return $this->newMessage(
+ pht(
+ 'This document is missing an "nbformat" field. Jupyter notebooks '.
+ 'must have this field.'));
+ }
+
+ if ($nbformat !== 4) {
+ return $this->newMessage(
+ pht(
+ 'This Jupyter notebook uses an unsupported version of the file '.
+ 'format (found version %s, expected version 4).',
+ $nbformat));
+ }
+
+ $cells = idx($data, 'cells');
+ if (!is_array($cells)) {
+ return $this->newMessage(
+ pht(
+ 'This Jupyter notebook does not specify a list of "cells".'));
+ }
+
+ if (!$cells) {
+ return $this->newMessage(
+ pht(
+ 'This Jupyter notebook does not specify any notebook cells.'));
+ }
+
+ $rows = array();
+ foreach ($cells as $cell) {
+ $rows[] = $this->renderJupyterCell($viewer, $cell);
+ }
+
+ $notebook_table = phutil_tag(
+ 'table',
+ array(
+ 'class' => 'jupyter-notebook',
+ ),
+ $rows);
+
+ $container = phutil_tag(
+ 'div',
+ array(
+ 'class' => 'document-engine-jupyter',
+ ),
+ $notebook_table);
+
+ return $container;
+ }
+
+ private function renderJupyterCell(
+ PhabricatorUser $viewer,
+ array $cell) {
+
+ list($label, $content) = $this->renderJupyterCellContent($viewer, $cell);
+
+ $label_cell = phutil_tag(
+ 'th',
+ array(),
+ $label);
+
+ $content_cell = phutil_tag(
+ 'td',
+ array(),
+ $content);
+
+ return phutil_tag(
+ 'tr',
+ array(),
+ array(
+ $label_cell,
+ $content_cell,
+ ));
+ }
+
+ private function renderJupyterCellContent(
+ PhabricatorUser $viewer,
+ array $cell) {
+
+ $cell_type = idx($cell, 'cell_type');
+ switch ($cell_type) {
+ case 'markdown':
+ return $this->newMarkdownCell($cell);
+ case 'code':
+ return $this->newCodeCell($cell);
+ }
+
+ return $this->newRawCell(id(new PhutilJSON())->encodeFormatted($cell));
+ }
+
+ private function newRawCell($content) {
+ return array(
+ null,
+ phutil_tag(
+ 'div',
+ array(
+ 'class' => 'jupyter-cell-raw PhabricatorMonospaced',
+ ),
+ $content),
+ );
+ }
+
+ private function newMarkdownCell(array $cell) {
+ $content = idx($cell, 'source');
+ if (!is_array($content)) {
+ $content = array();
+ }
+
+ $content = implode('', $content);
+ $content = phutil_escape_html_newlines($content);
+
+ return array(
+ null,
+ phutil_tag(
+ 'div',
+ array(
+ 'class' => 'jupyter-cell-markdown',
+ ),
+ $content),
+ );
+ }
+
+ private function newCodeCell(array $cell) {
+ $execution_count = idx($cell, 'execution_count');
+ if ($execution_count) {
+ $label = 'In ['.$execution_count.']:';
+ } else {
+ $label = null;
+ }
+
+ $content = idx($cell, 'source');
+ if (!is_array($content)) {
+ $content = array();
+ }
+
+ $content = implode('', $content);
+
+ $content = PhabricatorSyntaxHighlighter::highlightWithLanguage(
+ 'py',
+ $content);
+
+ $outputs = array();
+ $output_list = idx($cell, 'outputs');
+ if (is_array($output_list)) {
+ foreach ($output_list as $output) {
+ $outputs[] = $this->newOutput($output);
+ }
+ }
+
+ return array(
+ $label,
+ array(
+ phutil_tag(
+ 'div',
+ array(
+ 'class' => 'jupyter-cell-code PhabricatorMonospaced remarkup-code',
+ ),
+ array(
+ $content,
+ )),
+ $outputs,
+ ),
+ );
+ }
+
+ private function newOutput(array $output) {
+ if (!is_array($output)) {
+ return pht('<Invalid Output>');
+ }
+
+ $classes = array(
+ 'jupyter-output',
+ 'PhabricatorMonospaced',
+ );
+
+ $output_name = idx($output, 'name');
+ switch ($output_name) {
+ case 'stderr':
+ $classes[] = 'jupyter-output-stderr';
+ break;
+ }
+
+ $output_type = idx($output, 'output_type');
+ switch ($output_type) {
+ case 'execute_result':
+ case 'display_data':
+ $data = idx($output, 'data');
+
+ $image_formats = array(
+ 'image/png',
+ 'image/jpeg',
+ 'image/jpg',
+ 'image/gif',
+ );
+
+ foreach ($image_formats as $image_format) {
+ if (!isset($data[$image_format])) {
+ continue;
+ }
+
+ $raw_data = $data[$image_format];
+ if (!is_array($raw_data)) {
+ $raw_data = array($raw_data);
+ }
+ $raw_data = implode('', $raw_data);
+
+ $content = phutil_tag(
+ 'img',
+ array(
+ 'src' => 'data:'.$image_format.';base64,'.$raw_data,
+ ));
+
+ break 2;
+ }
+
+ if (isset($data['text/html'])) {
+ $content = $data['text/html'];
+ $classes[] = 'jupyter-output-html';
+ break;
+ }
+
+ if (isset($data['application/javascript'])) {
+ $content = $data['application/javascript'];
+ $classes[] = 'jupyter-output-html';
+ break;
+ }
+
+ if (isset($data['text/plain'])) {
+ $content = $data['text/plain'];
+ break;
+ }
+
+ break;
+ case 'stream':
+ default:
+ $content = idx($output, 'text');
+ if (!is_array($content)) {
+ $content = array();
+ }
+ $content = implode('', $content);
+ break;
+ }
+
+ return phutil_tag(
+ 'div',
+ array(
+ 'class' => implode(' ', $classes),
+ ),
+ $content);
+ }
+
+}
diff --git a/src/applications/files/document/PhabricatorPDFDocumentEngine.php b/src/applications/files/document/PhabricatorPDFDocumentEngine.php
new file mode 100644
index 000000000..1e85bd4ae
--- /dev/null
+++ b/src/applications/files/document/PhabricatorPDFDocumentEngine.php
@@ -0,0 +1,57 @@
+<?php
+
+final class PhabricatorPDFDocumentEngine
+ extends PhabricatorDocumentEngine {
+
+ const ENGINEKEY = 'pdf';
+
+ public function getViewAsLabel(PhabricatorDocumentRef $ref) {
+ return pht('View as PDF');
+ }
+
+ protected function getDocumentIconIcon(PhabricatorDocumentRef $ref) {
+ return 'fa-file-pdf-o';
+ }
+
+ protected function canRenderDocumentType(PhabricatorDocumentRef $ref) {
+ // Since we just render a link to the document anyway, we don't need to
+ // check anything fancy in config to see if the MIME type is actually
+ // viewable.
+
+ return $ref->hasAnyMimeType(
+ array(
+ 'application/pdf',
+ ));
+ }
+
+ protected function newDocumentContent(PhabricatorDocumentRef $ref) {
+ $viewer = $this->getViewer();
+
+ $file = $ref->getFile();
+ if ($file) {
+ $source_uri = $file->getViewURI();
+ } else {
+ throw new PhutilMethodNotImplementedException();
+ }
+
+ $name = $ref->getName();
+ $length = $ref->getByteLength();
+
+ $link = id(new PhabricatorFileLinkView())
+ ->setViewer($viewer)
+ ->setFileName($name)
+ ->setFileViewURI($source_uri)
+ ->setFileViewable(true)
+ ->setFileSize(phutil_format_bytes($length));
+
+ $container = phutil_tag(
+ 'div',
+ array(
+ 'class' => 'document-engine-pdf',
+ ),
+ $link);
+
+ return $container;
+ }
+
+}
diff --git a/src/applications/files/document/PhabricatorRemarkupDocumentEngine.php b/src/applications/files/document/PhabricatorRemarkupDocumentEngine.php
new file mode 100644
index 000000000..296b78196
--- /dev/null
+++ b/src/applications/files/document/PhabricatorRemarkupDocumentEngine.php
@@ -0,0 +1,47 @@
+<?php
+
+final class PhabricatorRemarkupDocumentEngine
+ extends PhabricatorDocumentEngine {
+
+ const ENGINEKEY = 'remarkup';
+
+ public function getViewAsLabel(PhabricatorDocumentRef $ref) {
+ return pht('View as Remarkup');
+ }
+
+ protected function getDocumentIconIcon(PhabricatorDocumentRef $ref) {
+ return 'fa-file-text-o';
+ }
+
+ protected function getContentScore(PhabricatorDocumentRef $ref) {
+ $name = $ref->getName();
+ if (preg_match('/\\.remarkup\z/i', $name)) {
+ return 2000;
+ }
+
+ return 500;
+ }
+
+ protected function canRenderDocumentType(PhabricatorDocumentRef $ref) {
+ return $ref->isProbablyText();
+ }
+
+ protected function newDocumentContent(PhabricatorDocumentRef $ref) {
+ $viewer = $this->getViewer();
+
+ $content = $ref->loadData();
+ $content = phutil_utf8ize($content);
+
+ $remarkup = new PHUIRemarkupView($viewer, $content);
+
+ $container = phutil_tag(
+ 'div',
+ array(
+ 'class' => 'document-engine-remarkup',
+ ),
+ $remarkup);
+
+ return $container;
+ }
+
+}
diff --git a/src/applications/files/document/PhabricatorSourceDocumentEngine.php b/src/applications/files/document/PhabricatorSourceDocumentEngine.php
new file mode 100644
index 000000000..cd7c2af92
--- /dev/null
+++ b/src/applications/files/document/PhabricatorSourceDocumentEngine.php
@@ -0,0 +1,41 @@
+<?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;
+ }
+
+ protected function getDocumentIconIcon(PhabricatorDocumentRef $ref) {
+ return 'fa-code';
+ }
+
+ protected function getContentScore(PhabricatorDocumentRef $ref) {
+ return 1500;
+ }
+
+ protected function newDocumentContent(PhabricatorDocumentRef $ref) {
+ $content = $this->loadTextData($ref);
+
+ $highlighting = $this->getHighlightingConfiguration();
+ if ($highlighting !== null) {
+ $content = PhabricatorSyntaxHighlighter::highlightWithLanguage(
+ $highlighting,
+ $content);
+ } else {
+ $content = PhabricatorSyntaxHighlighter::highlightWithFilename(
+ $ref->getName(),
+ $content);
+ }
+
+ return $this->newTextDocumentContent($content);
+ }
+
+}
diff --git a/src/applications/files/document/PhabricatorTextDocumentEngine.php b/src/applications/files/document/PhabricatorTextDocumentEngine.php
new file mode 100644
index 000000000..08a5cabe5
--- /dev/null
+++ b/src/applications/files/document/PhabricatorTextDocumentEngine.php
@@ -0,0 +1,89 @@
+<?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($content) {
+ $lines = phutil_split_lines($content);
+
+ $view = id(new PhabricatorSourceCodeView())
+ ->setHighlights($this->getHighlightedLines())
+ ->setLines($lines);
+
+ $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/PhabricatorVideoDocumentEngine.php b/src/applications/files/document/PhabricatorVideoDocumentEngine.php
new file mode 100644
index 000000000..4e03c62eb
--- /dev/null
+++ b/src/applications/files/document/PhabricatorVideoDocumentEngine.php
@@ -0,0 +1,75 @@
+<?php
+
+final class PhabricatorVideoDocumentEngine
+ extends PhabricatorDocumentEngine {
+
+ const ENGINEKEY = 'video';
+
+ public function getViewAsLabel(PhabricatorDocumentRef $ref) {
+ return pht('View as Video');
+ }
+
+ protected function getContentScore(PhabricatorDocumentRef $ref) {
+ // Some video documents can be rendered as either video or audio, but we
+ // want to prefer video.
+ return 2500;
+ }
+
+ protected function getByteLengthLimit() {
+ return null;
+ }
+
+ protected function getDocumentIconIcon(PhabricatorDocumentRef $ref) {
+ return 'fa-film';
+ }
+
+ protected function canRenderDocumentType(PhabricatorDocumentRef $ref) {
+ $file = $ref->getFile();
+ if ($file) {
+ return $file->isVideo();
+ }
+
+ $viewable_types = PhabricatorEnv::getEnvConfig('files.viewable-mime-types');
+ $viewable_types = array_keys($viewable_types);
+
+ $video_types = PhabricatorEnv::getEnvConfig('files.video-mime-types');
+ $video_types = array_keys($video_types);
+
+ return
+ $ref->hasAnyMimeType($viewable_types) &&
+ $ref->hasAnyMimeType($video_types);
+ }
+
+ protected function newDocumentContent(PhabricatorDocumentRef $ref) {
+ $file = $ref->getFile();
+ if ($file) {
+ $source_uri = $file->getViewURI();
+ } else {
+ throw new PhutilMethodNotImplementedException();
+ }
+
+ $mime_type = $ref->getMimeType();
+
+ $video = phutil_tag(
+ 'video',
+ array(
+ 'controls' => 'controls',
+ ),
+ phutil_tag(
+ 'source',
+ array(
+ 'src' => $source_uri,
+ 'type' => $mime_type,
+ )));
+
+ $container = phutil_tag(
+ 'div',
+ array(
+ 'class' => 'document-engine-video',
+ ),
+ $video);
+
+ return $container;
+ }
+
+}
diff --git a/src/applications/files/document/PhabricatorVoidDocumentEngine.php b/src/applications/files/document/PhabricatorVoidDocumentEngine.php
new file mode 100644
index 000000000..c5395632e
--- /dev/null
+++ b/src/applications/files/document/PhabricatorVoidDocumentEngine.php
@@ -0,0 +1,42 @@
+<?php
+
+final class PhabricatorVoidDocumentEngine
+ extends PhabricatorDocumentEngine {
+
+ const ENGINEKEY = 'void';
+
+ public function getViewAsLabel(PhabricatorDocumentRef $ref) {
+ return null;
+ }
+
+ protected function getDocumentIconIcon(PhabricatorDocumentRef $ref) {
+ return 'fa-file';
+ }
+
+ protected function getContentScore(PhabricatorDocumentRef $ref) {
+ return 1000;
+ }
+
+ protected function getByteLengthLimit() {
+ return null;
+ }
+
+ protected function canRenderDocumentType(PhabricatorDocumentRef $ref) {
+ return true;
+ }
+
+ protected function newDocumentContent(PhabricatorDocumentRef $ref) {
+ $message = pht(
+ 'No document engine can render the contents of this file.');
+
+ $container = phutil_tag(
+ 'div',
+ array(
+ 'class' => 'document-engine-message',
+ ),
+ $message);
+
+ return $container;
+ }
+
+}
diff --git a/src/applications/files/editor/PhabricatorFileEditor.php b/src/applications/files/editor/PhabricatorFileEditor.php
index 6a2b797b4..db974cec6 100644
--- a/src/applications/files/editor/PhabricatorFileEditor.php
+++ b/src/applications/files/editor/PhabricatorFileEditor.php
@@ -1,77 +1,76 @@
<?php
final class PhabricatorFileEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorFilesApplication';
}
public function getEditorObjectsDescription() {
return pht('Files');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
return $types;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function getMailSubjectPrefix() {
return PhabricatorEnv::getEnvConfig('metamta.files.subject-prefix');
}
protected function getMailTo(PhabricatorLiskDAO $object) {
return array(
$object->getAuthorPHID(),
$this->requireActor()->getPHID(),
);
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new FileReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
$name = $object->getName();
return id(new PhabricatorMetaMTAMail())
- ->setSubject("F{$id}: {$name}")
- ->addHeader('Thread-Topic', "F{$id}");
+ ->setSubject("F{$id}: {$name}");
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
$body->addTextSection(
pht('FILE DETAIL'),
PhabricatorEnv::getProductionURI($object->getInfoURI()));
return $body;
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function supportsSearch() {
return true;
}
}
diff --git a/src/applications/files/favicon/PhabricatorFaviconRef.php b/src/applications/files/favicon/PhabricatorFaviconRef.php
new file mode 100644
index 000000000..d6c2fd80a
--- /dev/null
+++ b/src/applications/files/favicon/PhabricatorFaviconRef.php
@@ -0,0 +1,447 @@
+<?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();
+
+ 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/favicon/PhabricatorFaviconRefQuery.php b/src/applications/files/favicon/PhabricatorFaviconRefQuery.php
new file mode 100644
index 000000000..f4e555615
--- /dev/null
+++ b/src/applications/files/favicon/PhabricatorFaviconRefQuery.php
@@ -0,0 +1,55 @@
+<?php
+
+final class PhabricatorFaviconRefQuery extends Phobject {
+
+ private $refs;
+
+ public function withRefs(array $refs) {
+ assert_instances_of($refs, 'PhabricatorFaviconRef');
+ $this->refs = $refs;
+ return $this;
+ }
+
+ public function execute() {
+ $viewer = PhabricatorUser::getOmnipotentUser();
+
+ $refs = $this->refs;
+
+ $config_digest = PhabricatorFaviconRef::newConfigurationDigest();
+
+ $ref_map = array();
+ foreach ($refs as $ref) {
+ $ref_digest = $ref->newDigest();
+ $ref_key = "favicon({$config_digest},{$ref_digest},8)";
+
+ $ref
+ ->setViewer($viewer)
+ ->setCacheKey($ref_key);
+
+ $ref_map[$ref_key] = $ref;
+ }
+
+ $cache = PhabricatorCaches::getImmutableCache();
+ $ref_hits = $cache->getKeys(array_keys($ref_map));
+
+ foreach ($ref_hits as $ref_key => $ref_uri) {
+ $ref_map[$ref_key]->setURI($ref_uri);
+ unset($ref_map[$ref_key]);
+ }
+
+ if ($ref_map) {
+ $new_map = array();
+ foreach ($ref_map as $ref_key => $ref) {
+ $ref_uri = $ref->newURI();
+ $ref->setURI($ref_uri);
+ $new_map[$ref_key] = $ref_uri;
+ }
+
+ $cache->setKeys($new_map);
+ }
+
+ return $refs;
+ }
+
+
+}
diff --git a/src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php b/src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php
index e4fe826f5..9cbc119c8 100644
--- a/src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php
+++ b/src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php
@@ -1,307 +1,307 @@
<?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();
+ $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;
}
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(),
)));
}
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/markup/PhabricatorImageRemarkupRule.php b/src/applications/files/markup/PhabricatorImageRemarkupRule.php
index 9e91bdc09..5d1979ed3 100644
--- a/src/applications/files/markup/PhabricatorImageRemarkupRule.php
+++ b/src/applications/files/markup/PhabricatorImageRemarkupRule.php
@@ -1,75 +1,171 @@
<?php
final class PhabricatorImageRemarkupRule extends PhutilRemarkupRule {
+
+ const KEY_RULE_EXTERNAL_IMAGE = 'rule.external-image';
+
public function getPriority() {
return 200.0;
}
public function apply($text) {
return preg_replace_callback(
'@{(image|img) ((?:[^}\\\\]+|\\\\.)*)}@m',
array($this, 'markupImage'),
$text);
}
public function markupImage(array $matches) {
if (!$this->isFlatText($matches[0])) {
return $matches[0];
}
+
$args = array();
$defaults = array(
'uri' => null,
'alt' => null,
- 'href' => null,
'width' => null,
'height' => null,
);
+
$trimmed_match = trim($matches[2]);
if ($this->isURI($trimmed_match)) {
- $args['uri'] = new PhutilURI($trimmed_match);
+ $args['uri'] = $trimmed_match;
} else {
$parser = new PhutilSimpleOptions();
$keys = $parser->parse($trimmed_match);
$uri_key = '';
foreach (array('src', 'uri', 'url') as $key) {
if (array_key_exists($key, $keys)) {
$uri_key = $key;
}
}
if ($uri_key) {
- $args['uri'] = new PhutilURI($keys[$uri_key]);
+ $args['uri'] = $keys[$uri_key];
}
$args += $keys;
}
$args += $defaults;
- if ($args['href'] && !PhabricatorEnv::isValidURIForLink($args['href'])) {
- $args['href'] = null;
+ if (!strlen($args['uri'])) {
+ return $matches[0];
}
- if ($args['uri']) {
- $src_uri = id(new PhutilURI('/file/imageproxy/'))
- ->setQueryParam('uri', (string)$args['uri']);
- $img = $this->newTag(
- 'img',
- array(
- 'src' => $src_uri,
- 'alt' => $args['alt'],
- 'href' => $args['href'],
- 'width' => $args['width'],
- 'height' => $args['height'],
- ));
- return $this->getEngine()->storeText($img);
- } else {
+ // Make sure this is something that looks roughly like a real URI. We'll
+ // validate it more carefully before proxying it, but if whatever the user
+ // has typed isn't even close, just decline to activate the rule behavior.
+ try {
+ $uri = new PhutilURI($args['uri']);
+
+ if (!strlen($uri->getProtocol())) {
+ return $matches[0];
+ }
+
+ $args['uri'] = (string)$uri;
+ } catch (Exception $ex) {
return $matches[0];
}
+
+ $engine = $this->getEngine();
+ $metadata_key = self::KEY_RULE_EXTERNAL_IMAGE;
+ $metadata = $engine->getTextMetadata($metadata_key, array());
+
+ $token = $engine->storeText('<img>');
+
+ $metadata[] = array(
+ 'token' => $token,
+ 'args' => $args,
+ );
+
+ $engine->setTextMetadata($metadata_key, $metadata);
+
+ return $token;
+ }
+
+ public function didMarkupText() {
+ $engine = $this->getEngine();
+ $metadata_key = self::KEY_RULE_EXTERNAL_IMAGE;
+ $images = $engine->getTextMetadata($metadata_key, array());
+ $engine->setTextMetadata($metadata_key, array());
+
+ if (!$images) {
+ return;
+ }
+
+ // Look for images we've already successfully fetched that aren't about
+ // to get eaten by the GC. For any we find, we can just emit a normal
+ // "<img />" tag pointing directly to the file.
+
+ // For files which we don't hit in the cache, we emit a placeholder
+ // instead and use AJAX to actually perform the fetch.
+
+ $digests = array();
+ foreach ($images as $image) {
+ $uri = $image['args']['uri'];
+ $digests[] = PhabricatorHash::digestForIndex($uri);
+ }
+
+ $caches = id(new PhabricatorFileExternalRequest())->loadAllWhere(
+ 'uriIndex IN (%Ls) AND isSuccessful = 1 AND ttl > %d',
+ $digests,
+ PhabricatorTime::getNow() + phutil_units('1 hour in seconds'));
+
+ $file_phids = array();
+ foreach ($caches as $cache) {
+ $file_phids[$cache->getFilePHID()] = $cache->getURI();
+ }
+
+ $file_map = array();
+ if ($file_phids) {
+ $files = id(new PhabricatorFileQuery())
+ ->setViewer(PhabricatorUser::getOmnipotentUser())
+ ->withPHIDs(array_keys($file_phids))
+ ->execute();
+ foreach ($files as $file) {
+ $phid = $file->getPHID();
+
+ $file_remote_uri = $file_phids[$phid];
+ $file_view_uri = $file->getViewURI();
+
+ $file_map[$file_remote_uri] = $file_view_uri;
+ }
+ }
+
+ foreach ($images as $image) {
+ $args = $image['args'];
+ $uri = $args['uri'];
+
+ $direct_uri = idx($file_map, $uri);
+ if ($direct_uri) {
+ $img = phutil_tag(
+ 'img',
+ array(
+ 'src' => $direct_uri,
+ 'alt' => $args['alt'],
+ 'width' => $args['width'],
+ 'height' => $args['height'],
+ ));
+ } else {
+ $src_uri = id(new PhutilURI('/file/imageproxy/'))
+ ->setQueryParam('uri', $uri);
+
+ $img = id(new PHUIRemarkupImageView())
+ ->setURI($src_uri)
+ ->setAlt($args['alt'])
+ ->setWidth($args['width'])
+ ->setHeight($args['height']);
+ }
+
+ $engine->overwriteStoredText($image['token'], $img);
+ }
}
private function isURI($uri_string) {
// Very simple check to make sure it starts with either http or https.
// If it does, we'll try to treat it like a valid URI
return preg_match('~^https?\:\/\/.*\z~i', $uri_string);
}
+
}
diff --git a/src/applications/files/query/PhabricatorFileQuery.php b/src/applications/files/query/PhabricatorFileQuery.php
index 071ee0399..9f1659a7f 100644
--- a/src/applications/files/query/PhabricatorFileQuery.php
+++ b/src/applications/files/query/PhabricatorFileQuery.php
@@ -1,448 +1,483 @@
<?php
final class PhabricatorFileQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $authorPHIDs;
private $explicitUploads;
private $transforms;
private $dateCreatedAfter;
private $dateCreatedBefore;
private $contentHashes;
private $minLength;
private $maxLength;
private $names;
private $isPartial;
private $isDeleted;
private $needTransforms;
private $builtinKeys;
private $isBuiltin;
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;
}
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 withContentHashes(array $content_hashes) {
$this->contentHashes = $content_hashes;
return $this;
}
public function withBuiltinKeys(array $keys) {
$this->builtinKeys = $keys;
return $this;
}
public function withIsBuiltin($is_builtin) {
$this->isBuiltin = $is_builtin;
return $this;
}
/**
* Select files which are transformations of some other file. For example,
* you can use this query to find previously generated thumbnails of an image
* file.
*
* As a parameter, provide a list of transformation specifications. Each
* specification is a dictionary with the keys `originalPHID` and `transform`.
* The `originalPHID` is the PHID of the original file (the file which was
* transformed) and the `transform` is the name of the transform to query
* for. If you pass `true` as the `transform`, all transformations of the
* file will be selected.
*
* For example:
*
* array(
* array(
* 'originalPHID' => 'PHID-FILE-aaaa',
* 'transform' => 'sepia',
* ),
* array(
* 'originalPHID' => 'PHID-FILE-bbbb',
* 'transform' => true,
* ),
* )
*
* This selects the `"sepia"` transformation of the file with PHID
* `PHID-FILE-aaaa` and all transformations of the file with PHID
* `PHID-FILE-bbbb`.
*
* @param list<dict> List of transform specifications, described above.
* @return this
*/
public function withTransforms(array $specs) {
foreach ($specs as $spec) {
if (!is_array($spec) ||
empty($spec['originalPHID']) ||
empty($spec['transform'])) {
throw new Exception(
pht(
"Transform specification must be a dictionary with keys ".
"'%s' and '%s'!",
'originalPHID',
'transform'));
}
}
$this->transforms = $specs;
return $this;
}
public function withLengthBetween($min, $max) {
$this->minLength = $min;
$this->maxLength = $max;
return $this;
}
public function withNames(array $names) {
$this->names = $names;
return $this;
}
public function withIsPartial($partial) {
$this->isPartial = $partial;
return $this;
}
public function withIsDeleted($deleted) {
$this->isDeleted = $deleted;
return $this;
}
public function withNameNgrams($ngrams) {
return $this->withNgramsConstraint(
id(new PhabricatorFileNameNgrams()),
$ngrams);
}
public function showOnlyExplicitUploads($explicit_uploads) {
$this->explicitUploads = $explicit_uploads;
return $this;
}
public function needTransforms(array $transforms) {
$this->needTransforms = $transforms;
return $this;
}
public function newResultObject() {
return new PhabricatorFile();
}
protected function loadPage() {
- $files = $this->loadStandardPage(new PhabricatorFile());
+ $files = $this->loadStandardPage($this->newResultObject());
if (!$files) {
return $files;
}
- $viewer = $this->getViewer();
- $is_omnipotent = $viewer->isOmnipotent();
-
- // We need to load attached objects to perform policy checks for files.
- // First, load the edges.
+ // Figure out which files we need to load attached objects for. In most
+ // cases, we need to load attached objects to perform policy checks for
+ // files.
- $edge_type = PhabricatorFileHasObjectEdgeType::EDGECONST;
- $file_phids = mpull($files, 'getPHID');
- $edges = id(new PhabricatorEdgeQuery())
- ->withSourcePHIDs($file_phids)
- ->withEdgeTypes(array($edge_type))
- ->execute();
-
- $object_phids = array();
+ // However, in some special cases where we know files will always be
+ // visible, we skip this. See T8478 and T13106.
+ $need_objects = array();
+ $need_xforms = array();
foreach ($files as $file) {
- $phids = array_keys($edges[$file->getPHID()][$edge_type]);
- $file->attachObjectPHIDs($phids);
+ $always_visible = false;
if ($file->getIsProfileImage()) {
- // If this is a profile image, don't bother loading related files.
- // It will always be visible, and we can get into trouble if we try
- // to load objects and end up stuck in a cycle. See T8478.
- continue;
+ $always_visible = true;
}
- if ($is_omnipotent) {
- // If the viewer is omnipotent, we don't need to load the associated
- // objects either since they can certainly see the object. Skipping
- // this can improve performance and prevent cycles.
+ if ($file->isBuiltin()) {
+ $always_visible = true;
+ }
+
+ if ($always_visible) {
+ // We just treat these files as though they aren't attached to
+ // anything. This saves a query in common cases when we're loading
+ // profile images or builtins. We could be slightly more nuanced
+ // about this and distinguish between "not attached to anything" and
+ // "might be attached but policy checks don't need to care".
+ $file->attachObjectPHIDs(array());
continue;
}
- foreach ($phids as $phid) {
- $object_phids[$phid] = true;
+ $need_objects[] = $file;
+ $need_xforms[] = $file;
+ }
+
+ $viewer = $this->getViewer();
+ $is_omnipotent = $viewer->isOmnipotent();
+
+ // If we have any files left which do need objects, load the edges now.
+ $object_phids = array();
+ if ($need_objects) {
+ $edge_type = PhabricatorFileHasObjectEdgeType::EDGECONST;
+ $file_phids = mpull($need_objects, 'getPHID');
+
+ $edges = id(new PhabricatorEdgeQuery())
+ ->withSourcePHIDs($file_phids)
+ ->withEdgeTypes(array($edge_type))
+ ->execute();
+
+ foreach ($need_objects as $file) {
+ $phids = array_keys($edges[$file->getPHID()][$edge_type]);
+ $file->attachObjectPHIDs($phids);
+
+ if ($is_omnipotent) {
+ // If the viewer is omnipotent, we don't need to load the associated
+ // objects either since the viewer can certainly see the object.
+ // Skipping this can improve performance and prevent cycles. This
+ // could possibly become part of the profile/builtin code above which
+ // short circuits attacment policy checks in cases where we know them
+ // to be unnecessary.
+ continue;
+ }
+
+ foreach ($phids as $phid) {
+ $object_phids[$phid] = true;
+ }
}
}
// If this file is a transform of another file, load that file too. If you
// can see the original file, you can see the thumbnail.
- // TODO: It might be nice to put this directly on PhabricatorFile and remove
- // the PhabricatorTransformedFile table, which would be a little simpler.
+ // TODO: It might be nice to put this directly on PhabricatorFile and
+ // remove the PhabricatorTransformedFile table, which would be a little
+ // simpler.
- $xforms = id(new PhabricatorTransformedFile())->loadAllWhere(
- 'transformedPHID IN (%Ls)',
- $file_phids);
- $xform_phids = mpull($xforms, 'getOriginalPHID', 'getTransformedPHID');
- foreach ($xform_phids as $derived_phid => $original_phid) {
- $object_phids[$original_phid] = true;
+ if ($need_xforms) {
+ $xforms = id(new PhabricatorTransformedFile())->loadAllWhere(
+ 'transformedPHID IN (%Ls)',
+ mpull($need_xforms, 'getPHID'));
+ $xform_phids = mpull($xforms, 'getOriginalPHID', 'getTransformedPHID');
+ foreach ($xform_phids as $derived_phid => $original_phid) {
+ $object_phids[$original_phid] = true;
+ }
+ } else {
+ $xform_phids = array();
}
$object_phids = array_keys($object_phids);
// Now, load the objects.
$objects = array();
if ($object_phids) {
// NOTE: We're explicitly turning policy exceptions off, since the rule
// here is "you can see the file if you can see ANY associated object".
// Without this explicit flag, we'll incorrectly throw unless you can
// see ALL associated objects.
$objects = id(new PhabricatorObjectQuery())
->setParentQuery($this)
->setViewer($this->getViewer())
->withPHIDs($object_phids)
->setRaisePolicyExceptions(false)
->execute();
$objects = mpull($objects, null, 'getPHID');
}
foreach ($files as $file) {
$file_objects = array_select_keys($objects, $file->getObjectPHIDs());
$file->attachObjects($file_objects);
}
foreach ($files as $key => $file) {
$original_phid = idx($xform_phids, $file->getPHID());
if ($original_phid == PhabricatorPHIDConstants::PHID_VOID) {
// This is a special case for builtin files, which are handled
// oddly.
$original = null;
} else if ($original_phid) {
$original = idx($objects, $original_phid);
if (!$original) {
// If the viewer can't see the original file, also prevent them from
// seeing the transformed file.
$this->didRejectResult($file);
unset($files[$key]);
continue;
}
} else {
$original = null;
}
$file->attachOriginalFile($original);
}
return $files;
}
protected function didFilterPage(array $files) {
$xform_keys = $this->needTransforms;
if ($xform_keys !== null) {
$xforms = id(new PhabricatorTransformedFile())->loadAllWhere(
'originalPHID IN (%Ls) AND transform IN (%Ls)',
mpull($files, 'getPHID'),
$xform_keys);
if ($xforms) {
$xfiles = id(new PhabricatorFile())->loadAllWhere(
'phid IN (%Ls)',
mpull($xforms, 'getTransformedPHID'));
$xfiles = mpull($xfiles, null, 'getPHID');
}
$xform_map = array();
foreach ($xforms as $xform) {
$xfile = idx($xfiles, $xform->getTransformedPHID());
if (!$xfile) {
continue;
}
$original_phid = $xform->getOriginalPHID();
$xform_key = $xform->getTransform();
$xform_map[$original_phid][$xform_key] = $xfile;
}
$default_xforms = array_fill_keys($xform_keys, null);
foreach ($files as $file) {
$file_xforms = idx($xform_map, $file->getPHID(), array());
$file_xforms += $default_xforms;
$file->attachTransforms($file_xforms);
}
}
return $files;
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = parent::buildJoinClauseParts($conn);
if ($this->transforms) {
$joins[] = qsprintf(
$conn,
'JOIN %T t ON t.transformedPHID = f.phid',
id(new PhabricatorTransformedFile())->getTableName());
}
return $joins;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'f.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'f.phid IN (%Ls)',
$this->phids);
}
if ($this->authorPHIDs !== null) {
$where[] = qsprintf(
$conn,
'f.authorPHID IN (%Ls)',
$this->authorPHIDs);
}
if ($this->explicitUploads !== null) {
$where[] = qsprintf(
$conn,
'f.isExplicitUpload = %d',
(int)$this->explicitUploads);
}
if ($this->transforms !== null) {
$clauses = array();
foreach ($this->transforms as $transform) {
if ($transform['transform'] === true) {
$clauses[] = qsprintf(
$conn,
'(t.originalPHID = %s)',
$transform['originalPHID']);
} else {
$clauses[] = qsprintf(
$conn,
'(t.originalPHID = %s AND t.transform = %s)',
$transform['originalPHID'],
$transform['transform']);
}
}
$where[] = qsprintf($conn, '(%Q)', implode(') OR (', $clauses));
}
if ($this->dateCreatedAfter !== null) {
$where[] = qsprintf(
$conn,
'f.dateCreated >= %d',
$this->dateCreatedAfter);
}
if ($this->dateCreatedBefore !== null) {
$where[] = qsprintf(
$conn,
'f.dateCreated <= %d',
$this->dateCreatedBefore);
}
if ($this->contentHashes !== null) {
$where[] = qsprintf(
$conn,
'f.contentHash IN (%Ls)',
$this->contentHashes);
}
if ($this->minLength !== null) {
$where[] = qsprintf(
$conn,
'byteSize >= %d',
$this->minLength);
}
if ($this->maxLength !== null) {
$where[] = qsprintf(
$conn,
'byteSize <= %d',
$this->maxLength);
}
if ($this->names !== null) {
$where[] = qsprintf(
$conn,
'name in (%Ls)',
$this->names);
}
if ($this->isPartial !== null) {
$where[] = qsprintf(
$conn,
'isPartial = %d',
(int)$this->isPartial);
}
if ($this->isDeleted !== null) {
$where[] = qsprintf(
$conn,
'isDeleted = %d',
(int)$this->isDeleted);
}
if ($this->builtinKeys !== null) {
$where[] = qsprintf(
$conn,
'builtinKey IN (%Ls)',
$this->builtinKeys);
}
if ($this->isBuiltin !== null) {
if ($this->isBuiltin) {
$where[] = qsprintf(
$conn,
'builtinKey IS NOT NULL');
} else {
$where[] = qsprintf(
$conn,
'builtinKey IS NULL');
}
}
return $where;
}
protected function getPrimaryTableAlias() {
return 'f';
}
public function getQueryApplicationClass() {
return 'PhabricatorFilesApplication';
}
}
diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php
index d3dc5208d..a8d16b765 100644
--- a/src/applications/files/storage/PhabricatorFile.php
+++ b/src/applications/files/storage/PhabricatorFile.php
@@ -1,1650 +1,1682 @@
<?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());
return $this;
}
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. This matters for large media files like video.
- $file->setMimeType('application/octet-stream');
+ // 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(
$engine,
$data,
$params);
$this->setStorageHandle($new_handle);
$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.
$params = $params + array(
'name' => basename($uri),
);
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();
+ return $this->getCDNURI('data');
}
- public function getCDNURI() {
+ 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[] = 'data';
+ $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() {
- $uri = id(new PhutilURI($this->getViewURI()))
- ->setQueryParam('download', true);
- return (string)$uri;
+ 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(),
- 'ttl.relative' => phutil_units('7 days in seconds'),
'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(),
+ '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/files/uploadsource/PhabricatorFileUploadSource.php b/src/applications/files/uploadsource/PhabricatorFileUploadSource.php
index bf213a417..87a448686 100644
--- a/src/applications/files/uploadsource/PhabricatorFileUploadSource.php
+++ b/src/applications/files/uploadsource/PhabricatorFileUploadSource.php
@@ -1,272 +1,302 @@
<?php
abstract class PhabricatorFileUploadSource
extends Phobject {
private $name;
private $relativeTTL;
private $viewPolicy;
+ private $mimeType;
+ private $authorPHID;
private $rope;
private $data;
private $shouldChunk;
private $didRewind;
private $totalBytesWritten = 0;
private $totalBytesRead = 0;
private $byteLimit = 0;
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
public function setRelativeTTL($relative_ttl) {
$this->relativeTTL = $relative_ttl;
return $this;
}
public function getRelativeTTL() {
return $this->relativeTTL;
}
public function setViewPolicy($view_policy) {
$this->viewPolicy = $view_policy;
return $this;
}
public function getViewPolicy() {
return $this->viewPolicy;
}
public function setByteLimit($byte_limit) {
$this->byteLimit = $byte_limit;
return $this;
}
public function getByteLimit() {
return $this->byteLimit;
}
+ public function setMIMEType($mime_type) {
+ $this->mimeType = $mime_type;
+ return $this;
+ }
+
+ public function getMIMEType() {
+ return $this->mimeType;
+ }
+
+ public function setAuthorPHID($author_phid) {
+ $this->authorPHID = $author_phid;
+ return $this;
+ }
+
+ public function getAuthorPHID() {
+ return $this->authorPHID;
+ }
+
public function uploadFile() {
if (!$this->shouldChunkFile()) {
return $this->writeSingleFile();
} else {
return $this->writeChunkedFile();
}
}
private function getDataIterator() {
if (!$this->data) {
$this->data = $this->newDataIterator();
}
return $this->data;
}
private function getRope() {
if (!$this->rope) {
$this->rope = new PhutilRope();
}
return $this->rope;
}
abstract protected function newDataIterator();
abstract protected function getDataLength();
private function readFileData() {
$data = $this->getDataIterator();
if (!$this->didRewind) {
$data->rewind();
$this->didRewind = true;
} else {
if ($data->valid()) {
$data->next();
}
}
if (!$data->valid()) {
return false;
}
$read_bytes = $data->current();
$this->totalBytesRead += strlen($read_bytes);
if ($this->byteLimit && ($this->totalBytesRead > $this->byteLimit)) {
throw new PhabricatorFileUploadSourceByteLimitException();
}
$rope = $this->getRope();
$rope->append($read_bytes);
return true;
}
private function shouldChunkFile() {
if ($this->shouldChunk !== null) {
return $this->shouldChunk;
}
$threshold = PhabricatorFileStorageEngine::getChunkThreshold();
if ($threshold === null) {
// If there are no chunk engines available, we clearly can't chunk the
// file.
$this->shouldChunk = false;
} else {
// If we don't know how large the file is, we're going to read some data
// from it until we know whether it's a small file or not. This will give
// us enough information to make a decision about chunking.
$length = $this->getDataLength();
if ($length === null) {
$rope = $this->getRope();
while ($this->readFileData()) {
$length = $rope->getByteLength();
if ($length > $threshold) {
break;
}
}
}
$this->shouldChunk = ($length > $threshold);
}
return $this->shouldChunk;
}
private function writeSingleFile() {
while ($this->readFileData()) {
// Read the entire file.
}
$rope = $this->getRope();
$data = $rope->getAsString();
$parameters = $this->getNewFileParameters();
return PhabricatorFile::newFromFileData($data, $parameters);
}
private function writeChunkedFile() {
$engine = $this->getChunkEngine();
$parameters = $this->getNewFileParameters();
$data_length = $this->getDataLength();
if ($data_length !== null) {
$length = $data_length;
} else {
$length = 0;
}
$file = PhabricatorFile::newChunkedFile($engine, $length, $parameters);
$file->saveAndIndex();
$rope = $this->getRope();
// Read the source, writing chunks as we get enough data.
while ($this->readFileData()) {
while (true) {
$rope_length = $rope->getByteLength();
if ($rope_length < $engine->getChunkSize()) {
break;
}
$this->writeChunk($file, $engine);
}
}
// If we have extra bytes at the end, write them. Note that it's possible
// that we have more than one chunk of bytes left if the read was very
// fast.
while ($rope->getByteLength()) {
$this->writeChunk($file, $engine);
}
$file->setIsPartial(0);
if ($data_length === null) {
$file->setByteSize($this->getTotalBytesWritten());
}
$file->save();
return $file;
}
private function writeChunk(
PhabricatorFile $file,
PhabricatorFileStorageEngine $engine) {
$offset = $this->getTotalBytesWritten();
$max_length = $engine->getChunkSize();
$rope = $this->getRope();
$data = $rope->getPrefixBytes($max_length);
$actual_length = strlen($data);
$rope->removeBytesFromHead($actual_length);
$params = array(
'name' => $file->getMonogram().'.chunk-'.$offset,
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
'chunk' => true,
);
// If this isn't the initial chunk, provide a dummy MIME type so we do not
// try to detect it. See T12857.
if ($offset > 0) {
$params['mime-type'] = 'application/octet-stream';
}
$chunk_data = PhabricatorFile::newFromFileData($data, $params);
$chunk = PhabricatorFileChunk::initializeNewChunk(
$file->getStorageHandle(),
$offset,
$offset + $actual_length);
$chunk
->setDataFilePHID($chunk_data->getPHID())
->save();
$this->setTotalBytesWritten($offset + $actual_length);
return $chunk;
}
private function getNewFileParameters() {
$parameters = array(
'name' => $this->getName(),
'viewPolicy' => $this->getViewPolicy(),
);
$ttl = $this->getRelativeTTL();
if ($ttl !== null) {
$parameters['ttl.relative'] = $ttl;
}
+ $mime_type = $this->getMimeType();
+ if ($mime_type !== null) {
+ $parameters['mime-type'] = $mime_type;
+ }
+
+ $author_phid = $this->getAuthorPHID();
+ if ($author_phid !== null) {
+ $parameters['authorPHID'] = $author_phid;
+ }
+
return $parameters;
}
private function getChunkEngine() {
$chunk_engines = PhabricatorFileStorageEngine::loadWritableChunkEngines();
if (!$chunk_engines) {
throw new Exception(
pht(
'Unable to upload file: this server is not configured with any '.
'storage engine which can store large files.'));
}
return head($chunk_engines);
}
private function setTotalBytesWritten($total_bytes_written) {
$this->totalBytesWritten = $total_bytes_written;
return $this;
}
private function getTotalBytesWritten() {
return $this->totalBytesWritten;
}
}
diff --git a/src/applications/fund/editor/FundInitiativeEditor.php b/src/applications/fund/editor/FundInitiativeEditor.php
index e5c372fd1..9175156ff 100644
--- a/src/applications/fund/editor/FundInitiativeEditor.php
+++ b/src/applications/fund/editor/FundInitiativeEditor.php
@@ -1,93 +1,92 @@
<?php
final class FundInitiativeEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorFundApplication';
}
public function getEditorObjectsDescription() {
return pht('Fund Initiatives');
}
public function getCreateObjectTitle($author, $object) {
return pht('%s created this initiative.', $author);
}
public function getCreateObjectTitleForFeed($author, $object) {
return pht('%s created %s.', $author, $object);
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
$types[] = PhabricatorTransactions::TYPE_COMMENT;
return $types;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
public function getMailTagsMap() {
return array(
FundInitiativeTransaction::MAILTAG_BACKER =>
pht('Someone backs an initiative.'),
FundInitiativeTransaction::MAILTAG_STATUS =>
pht("An initiative's status changes."),
FundInitiativeTransaction::MAILTAG_OTHER =>
pht('Other initiative activity not listed above occurs.'),
);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$monogram = $object->getMonogram();
$name = $object->getName();
return id(new PhabricatorMetaMTAMail())
- ->setSubject("{$monogram}: {$name}")
- ->addHeader('Thread-Topic', $monogram);
+ ->setSubject("{$monogram}: {$name}");
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
$body->addLinkSection(
pht('INITIATIVE DETAIL'),
PhabricatorEnv::getProductionURI('/'.$object->getMonogram()));
return $body;
}
protected function getMailTo(PhabricatorLiskDAO $object) {
return array($object->getOwnerPHID());
}
protected function getMailSubjectPrefix() {
return 'Fund';
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new FundInitiativeReplyHandler())
->setMailReceiver($object);
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function supportsSearch() {
return true;
}
}
diff --git a/src/applications/harbormaster/__tests__/HarbormasterBuildLogTestCase.php b/src/applications/harbormaster/__tests__/HarbormasterBuildLogTestCase.php
new file mode 100644
index 000000000..6cbdd6b0d
--- /dev/null
+++ b/src/applications/harbormaster/__tests__/HarbormasterBuildLogTestCase.php
@@ -0,0 +1,117 @@
+<?php
+
+final class HarbormasterBuildLogTestCase
+ extends PhabricatorTestCase {
+
+ public function testBuildLogLineMaps() {
+ $snowman = "\xE2\x98\x83";
+
+ $inputs = array(
+ 'no_newlines.log' => array(
+ 64,
+ array(
+ str_repeat('AAAAAAAA', 32),
+ ),
+ array(
+ array(64, 0),
+ array(128, 0),
+ array(192, 0),
+ array(255, 0),
+ ),
+ ),
+ 'no_newlines_updated.log' => array(
+ 64,
+ array_fill(0, 32, 'AAAAAAAA'),
+ array(
+ array(64, 0),
+ array(128, 0),
+ array(192, 0),
+ ),
+ ),
+ 'one_newline.log' => array(
+ 64,
+ array(
+ str_repeat('AAAAAAAA', 16),
+ "\n",
+ str_repeat('AAAAAAAA', 16),
+ ),
+ array(
+ array(64, 0),
+ array(127, 0),
+ array(191, 1),
+ array(255, 1),
+ ),
+ ),
+ 'several_newlines.log' => array(
+ 64,
+ array_fill(0, 12, "AAAAAAAAAAAAAAAAAA\n"),
+ array(
+ array(56, 2),
+ array(113, 5),
+ array(170, 8),
+ array(227, 11),
+ ),
+ ),
+ 'mixed_newlines.log' => array(
+ 64,
+ array(
+ str_repeat('A', 63)."\r",
+ str_repeat('A', 63)."\r\n",
+ str_repeat('A', 63)."\n",
+ str_repeat('A', 63),
+ ),
+ array(
+ array(63, 0),
+ array(127, 1),
+ array(191, 2),
+ array(255, 3),
+ ),
+ ),
+ 'more_mixed_newlines.log' => array(
+ 64,
+ array(
+ str_repeat('A', 63)."\r",
+ str_repeat('A', 62)."\r\n",
+ str_repeat('A', 63)."\n",
+ str_repeat('A', 63),
+ ),
+ array(
+ array(63, 0),
+ array(128, 2),
+ array(191, 2),
+ array(254, 3),
+ ),
+ ),
+ 'emoji.log' => array(
+ 64,
+ array(
+ str_repeat($snowman, 64),
+ ),
+ array(
+ array(63, 0),
+ array(126, 0),
+ array(189, 0),
+ ),
+ ),
+ );
+
+ foreach ($inputs as $label => $input) {
+ list($distance, $parts, $expect) = $input;
+
+ $log = id(new HarbormasterBuildLog())
+ ->setByteLength(0);
+
+ foreach ($parts as $part) {
+ $log->updateLineMap($part, $distance);
+ }
+
+ list($actual) = $log->getLineMap();
+
+ $this->assertEqual(
+ $expect,
+ $actual,
+ pht('Line Map for "%s"', $label));
+ }
+ }
+
+}
diff --git a/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php b/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php
index ed164b2d3..80be90b37 100644
--- a/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php
+++ b/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php
@@ -1,120 +1,128 @@
<?php
final class PhabricatorHarbormasterApplication extends PhabricatorApplication {
public function getBaseURI() {
return '/harbormaster/';
}
public function getName() {
return pht('Harbormaster');
}
public function getShortDescription() {
return pht('Build/CI');
}
public function getIcon() {
return 'fa-ship';
}
public function getTitleGlyph() {
return "\xE2\x99\xBB";
}
public function getFlavorText() {
return pht('Ship Some Freight');
}
public function getApplicationGroup() {
return self::GROUP_UTILITIES;
}
public function getEventListeners() {
return array(
new HarbormasterUIEventListener(),
);
}
public function getRemarkupRules() {
return array(
new HarbormasterRemarkupRule(),
);
}
public function getHelpDocumentationArticles(PhabricatorUser $viewer) {
return array(
array(
'name' => pht('Harbormaster User Guide'),
'href' => PhabricatorEnv::getDoclink('Harbormaster User Guide'),
),
);
}
public function getRoutes() {
return array(
'/B(?P<id>[1-9]\d*)' => 'HarbormasterBuildableViewController',
'/harbormaster/' => array(
'(?:query/(?P<queryKey>[^/]+)/)?'
=> 'HarbormasterBuildableListController',
'step/' => array(
'add/(?:(?P<id>\d+)/)?' => 'HarbormasterStepAddController',
'new/(?P<plan>\d+)/(?P<class>[^/]+)/'
=> 'HarbormasterStepEditController',
'view/(?P<id>\d+)/' => 'HarbormasterStepViewController',
'edit/(?:(?P<id>\d+)/)?' => 'HarbormasterStepEditController',
'delete/(?:(?P<id>\d+)/)?' => 'HarbormasterStepDeleteController',
),
'buildable/' => array(
'(?P<id>\d+)/(?P<action>pause|resume|restart|abort)/'
=> 'HarbormasterBuildableActionController',
),
'build/' => array(
$this->getQueryRoutePattern() => 'HarbormasterBuildListController',
- '(?P<id>\d+)/' => 'HarbormasterBuildViewController',
+ '(?P<id>\d+)/(?:(?P<generation>\d+)/)?'
+ => 'HarbormasterBuildViewController',
'(?P<action>pause|resume|restart|abort)/'.
'(?P<id>\d+)/(?:(?P<via>[^/]+)/)?'
=> 'HarbormasterBuildActionController',
),
'plan/' => array(
$this->getQueryRoutePattern() => 'HarbormasterPlanListController',
$this->getEditRoutePattern('edit/')
=> 'HarbormasterPlanEditController',
'order/(?:(?P<id>\d+)/)?' => 'HarbormasterPlanOrderController',
'disable/(?P<id>\d+)/' => 'HarbormasterPlanDisableController',
'run/(?P<id>\d+)/' => 'HarbormasterPlanRunController',
'(?P<id>\d+)/' => 'HarbormasterPlanViewController',
),
'unit/' => array(
'(?P<id>\d+)/' => 'HarbormasterUnitMessageListController',
'view/(?P<id>\d+)/' => 'HarbormasterUnitMessageViewController',
),
'lint/' => array(
'(?P<id>\d+)/' => 'HarbormasterLintMessagesController',
),
'hook/' => array(
'circleci/' => 'HarbormasterCircleCIHookController',
'buildkite/' => 'HarbormasterBuildkiteHookController',
),
+ 'log/' => array(
+ 'view/(?P<id>\d+)/(?:\$(?P<lines>\d+(?:-\d+)?))?'
+ => 'HarbormasterBuildLogViewController',
+ 'render/(?P<id>\d+)/(?:\$(?P<lines>\d+(?:-\d+)?))?'
+ => 'HarbormasterBuildLogRenderController',
+ 'download/(?P<id>\d+)/' => 'HarbormasterBuildLogDownloadController',
+ ),
),
);
}
protected function getCustomCapabilities() {
return array(
HarbormasterCreatePlansCapability::CAPABILITY => array(
'default' => PhabricatorPolicies::POLICY_ADMIN,
),
HarbormasterBuildPlanDefaultViewCapability::CAPABILITY => array(
'template' => HarbormasterBuildPlanPHIDType::TYPECONST,
'capability' => PhabricatorPolicyCapability::CAN_VIEW,
),
HarbormasterBuildPlanDefaultEditCapability::CAPABILITY => array(
'template' => HarbormasterBuildPlanPHIDType::TYPECONST,
'capability' => PhabricatorPolicyCapability::CAN_EDIT,
'default' => PhabricatorPolicies::POLICY_ADMIN,
),
);
}
}
diff --git a/src/applications/harbormaster/artifact/HarbormasterURIArtifact.php b/src/applications/harbormaster/artifact/HarbormasterURIArtifact.php
index 345621f0f..93f756403 100644
--- a/src/applications/harbormaster/artifact/HarbormasterURIArtifact.php
+++ b/src/applications/harbormaster/artifact/HarbormasterURIArtifact.php
@@ -1,119 +1,120 @@
<?php
final class HarbormasterURIArtifact extends HarbormasterArtifact {
const ARTIFACTCONST = 'uri';
public function getArtifactTypeName() {
return pht('URI');
}
public function getArtifactTypeSummary() {
return pht('Stores a URI.');
}
public function getArtifactTypeDescription() {
return pht(
"Stores a URI.\n\n".
"With `ui.external`, you can use this artifact type to add links to ".
"build results in an external build system.");
}
public function getArtifactParameterSpecification() {
return array(
'uri' => 'string',
'name' => 'optional string',
'ui.external' => 'optional bool',
);
}
public function readArtifactHTTPParameter($key, $value) {
// TODO: This is hacky and artifact parameters should be replaced more
// broadly, likely with EditFields. See T11887.
switch ($key) {
case 'ui.external':
return (bool)$value;
}
return $value;
}
public function getArtifactParameterDescriptions() {
return array(
'uri' => pht('The URI to store.'),
'name' => pht('Optional label for this URI.'),
'ui.external' => pht(
'If true, display this URI in the UI as an link to '.
'additional build details in an external build system.'),
);
}
public function getArtifactDataExample() {
return array(
'uri' => 'https://buildserver.mycompany.com/build/123/',
'name' => pht('View External Build Results'),
'ui.external' => true,
);
}
public function renderArtifactSummary(PhabricatorUser $viewer) {
return $this->renderLink();
}
public function isExternalLink() {
$artifact = $this->getBuildArtifact();
return (bool)$artifact->getProperty('ui.external', false);
}
public function renderLink() {
$artifact = $this->getBuildArtifact();
$uri = $artifact->getProperty('uri');
try {
$this->validateURI($uri);
} catch (Exception $ex) {
return pht('<Invalid URI>');
}
$name = $artifact->getProperty('name', $uri);
return phutil_tag(
'a',
array(
'href' => $uri,
'target' => '_blank',
+ 'rel' => 'noreferrer',
),
$name);
}
public function willCreateArtifact(PhabricatorUser $actor) {
$artifact = $this->getBuildArtifact();
$uri = $artifact->getProperty('uri');
$this->validateURI($uri);
}
private function validateURI($raw_uri) {
$uri = new PhutilURI($raw_uri);
$protocol = $uri->getProtocol();
if (!strlen($protocol)) {
throw new Exception(
pht(
'Unable to identify the protocol for URI "%s". URIs must be '.
'fully qualified and have an identifiable protocol.',
$raw_uri));
}
$protocol_key = 'uri.allowed-protocols';
$protocols = PhabricatorEnv::getEnvConfig($protocol_key);
if (empty($protocols[$protocol])) {
throw new Exception(
pht(
'URI "%s" does not have an allowable protocol. Configure '.
'protocols in `%s`. Allowed protocols are: %s.',
$raw_uri,
$protocol_key,
implode(', ', array_keys($protocols))));
}
}
}
diff --git a/src/applications/harbormaster/conduit/HarbormasterBuildLogSearchConduitAPIMethod.php b/src/applications/harbormaster/conduit/HarbormasterBuildLogSearchConduitAPIMethod.php
new file mode 100644
index 000000000..e24b53956
--- /dev/null
+++ b/src/applications/harbormaster/conduit/HarbormasterBuildLogSearchConduitAPIMethod.php
@@ -0,0 +1,18 @@
+<?php
+
+final class HarbormasterBuildLogSearchConduitAPIMethod
+ extends PhabricatorSearchEngineAPIMethod {
+
+ public function getAPIMethodName() {
+ return 'harbormaster.log.search';
+ }
+
+ public function newSearchEngine() {
+ return new HarbormasterBuildLogSearchEngine();
+ }
+
+ public function getMethodSummary() {
+ return pht('Find out information about build logs.');
+ }
+
+}
diff --git a/src/applications/harbormaster/conduit/HarbormasterQueryBuildablesConduitAPIMethod.php b/src/applications/harbormaster/conduit/HarbormasterQueryBuildablesConduitAPIMethod.php
index 818b98353..cae06d47c 100644
--- a/src/applications/harbormaster/conduit/HarbormasterQueryBuildablesConduitAPIMethod.php
+++ b/src/applications/harbormaster/conduit/HarbormasterQueryBuildablesConduitAPIMethod.php
@@ -1,91 +1,91 @@
<?php
final class HarbormasterQueryBuildablesConduitAPIMethod
extends HarbormasterConduitAPIMethod {
public function getAPIMethodName() {
return 'harbormaster.querybuildables';
}
public function getMethodDescription() {
return pht('Query Harbormaster buildables.');
}
protected function defineParamTypes() {
return array(
'ids' => 'optional list<id>',
'phids' => 'optional list<phid>',
'buildablePHIDs' => 'optional list<phid>',
'containerPHIDs' => 'optional list<phid>',
'manualBuildables' => 'optional bool',
) + self::getPagerParamTypes();
}
protected function defineReturnType() {
return 'wild';
}
protected function execute(ConduitAPIRequest $request) {
$viewer = $request->getUser();
$query = id(new HarbormasterBuildableQuery())
->setViewer($viewer);
$ids = $request->getValue('ids');
if ($ids !== null) {
$query->withIDs($ids);
}
$phids = $request->getValue('phids');
if ($phids !== null) {
$query->withPHIDs($phids);
}
$buildable_phids = $request->getValue('buildablePHIDs');
if ($buildable_phids !== null) {
$query->withBuildablePHIDs($buildable_phids);
}
$container_phids = $request->getValue('containerPHIDs');
if ($container_phids !== null) {
$query->withContainerPHIDs($container_phids);
}
$manual = $request->getValue('manualBuildables');
if ($manual !== null) {
$query->withManualBuildables($manual);
}
$pager = $this->newPager($request);
$buildables = $query->executeWithCursorPager($pager);
$data = array();
foreach ($buildables as $buildable) {
$monogram = $buildable->getMonogram();
$status = $buildable->getBuildableStatus();
- $status_name = HarbormasterBuildable::getBuildableStatusName($status);
+ $status_name = $buildable->getStatusDisplayName();
$data[] = array(
'id' => $buildable->getID(),
'phid' => $buildable->getPHID(),
'monogram' => $monogram,
'uri' => PhabricatorEnv::getProductionURI('/'.$monogram),
'buildableStatus' => $status,
'buildableStatusName' => $status_name,
'buildablePHID' => $buildable->getBuildablePHID(),
'containerPHID' => $buildable->getContainerPHID(),
'isManualBuildable' => (bool)$buildable->getIsManualBuildable(),
);
}
$results = array(
'data' => $data,
);
$results = $this->addPagerResults($results, $pager);
return $results;
}
}
diff --git a/src/applications/harbormaster/conduit/HarbormasterSendMessageConduitAPIMethod.php b/src/applications/harbormaster/conduit/HarbormasterSendMessageConduitAPIMethod.php
index dbf1f18cf..fc83b7ab4 100644
--- a/src/applications/harbormaster/conduit/HarbormasterSendMessageConduitAPIMethod.php
+++ b/src/applications/harbormaster/conduit/HarbormasterSendMessageConduitAPIMethod.php
@@ -1,273 +1,273 @@
<?php
final class HarbormasterSendMessageConduitAPIMethod
extends HarbormasterConduitAPIMethod {
public function getAPIMethodName() {
return 'harbormaster.sendmessage';
}
public function getMethodSummary() {
return pht(
'Send a message about the status of a build target to Harbormaster, '.
'notifying the application of build results in an external system.');
}
public function getMethodDescription() {
$messages = HarbormasterMessageType::getAllMessages();
$head_type = pht('Constant');
$head_desc = pht('Description');
$head_key = pht('Key');
$head_type = pht('Type');
$head_name = pht('Name');
$rows = array();
$rows[] = "| {$head_type} | {$head_desc} |";
$rows[] = '|--------------|--------------|';
foreach ($messages as $message) {
$description = HarbormasterMessageType::getMessageDescription($message);
$rows[] = "| `{$message}` | {$description} |";
}
$message_table = implode("\n", $rows);
$rows = array();
$rows[] = "| {$head_key} | {$head_type} | {$head_desc} |";
$rows[] = '|-------------|--------------|--------------|';
$unit_spec = HarbormasterBuildUnitMessage::getParameterSpec();
foreach ($unit_spec as $key => $parameter) {
$type = idx($parameter, 'type');
$type = str_replace('|', ' '.pht('or').' ', $type);
$description = idx($parameter, 'description');
$rows[] = "| `{$key}` | //{$type}// | {$description} |";
}
$unit_table = implode("\n", $rows);
$rows = array();
$rows[] = "| {$head_key} | {$head_name} | {$head_desc} |";
$rows[] = '|-------------|--------------|--------------|';
$results = ArcanistUnitTestResult::getAllResultCodes();
foreach ($results as $result_code) {
$name = ArcanistUnitTestResult::getResultCodeName($result_code);
$description = ArcanistUnitTestResult::getResultCodeDescription(
$result_code);
$rows[] = "| `{$result_code}` | **{$name}** | {$description} |";
}
$result_table = implode("\n", $rows);
$rows = array();
$rows[] = "| {$head_key} | {$head_type} | {$head_desc} |";
$rows[] = '|-------------|--------------|--------------|';
$lint_spec = HarbormasterBuildLintMessage::getParameterSpec();
foreach ($lint_spec as $key => $parameter) {
$type = idx($parameter, 'type');
$type = str_replace('|', ' '.pht('or').' ', $type);
$description = idx($parameter, 'description');
$rows[] = "| `{$key}` | //{$type}// | {$description} |";
}
$lint_table = implode("\n", $rows);
$rows = array();
$rows[] = "| {$head_key} | {$head_name} |";
$rows[] = '|-------------|--------------|';
$severities = ArcanistLintSeverity::getLintSeverities();
foreach ($severities as $key => $name) {
$rows[] = "| `{$key}` | **{$name}** |";
}
$severity_table = implode("\n", $rows);
$valid_unit = array(
array(
'name' => 'PassingTest',
'result' => ArcanistUnitTestResult::RESULT_PASS,
),
array(
'name' => 'FailingTest',
'result' => ArcanistUnitTestResult::RESULT_FAIL,
),
);
$valid_lint = array(
array(
'name' => pht('Syntax Error'),
'code' => 'EXAMPLE1',
'severity' => ArcanistLintSeverity::SEVERITY_ERROR,
'path' => 'path/to/example.c',
'line' => 17,
'char' => 3,
),
array(
'name' => pht('Not A Haiku'),
'code' => 'EXAMPLE2',
'severity' => ArcanistLintSeverity::SEVERITY_ERROR,
'path' => 'path/to/source.cpp',
'line' => 23,
'char' => 1,
'description' => pht(
'This function definition is not a haiku.'),
),
);
$json = new PhutilJSON();
$valid_unit = $json->encodeAsList($valid_unit);
$valid_lint = $json->encodeAsList($valid_lint);
return pht(
"Send a message about the status of a build target to Harbormaster, ".
"notifying the application of build results in an external system.".
"\n\n".
"Sending Messages\n".
"================\n".
"If you run external builds, you can use this method to publish build ".
"results back into Harbormaster after the external system finishes work ".
"or as it makes progress.".
"\n\n".
"The simplest way to use this method is to call it once after the ".
"build finishes with a `pass` or `fail` message. This will record the ".
"build result, and continue the next step in the build if the build was ".
"waiting for a result.".
"\n\n".
"When you send a status message about a build target, you can ".
"optionally include detailed `lint` or `unit` results alongside the ".
"message. See below for details.".
"\n\n".
"If you want to report intermediate results but a build hasn't ".
"completed yet, you can use the `work` message. This message doesn't ".
"have any direct effects, but allows you to send additional data to ".
"update the progress of the build target. The target will continue ".
"waiting for a completion message, but the UI will update to show the ".
"progress which has been made.".
"\n\n".
"Message Types\n".
"=============\n".
"When you send Harbormaster a message, you must include a `type`, ".
"which describes the overall state of the build. For example, use ".
"`pass` to tell Harbormaster that a build completed successfully.".
"\n\n".
"Supported message types are:".
"\n\n".
"%s".
"\n\n".
"Unit Results\n".
"============\n".
"You can report test results alongside a message. The simplest way to ".
"do this is to report all the results alongside a `pass` or `fail` ".
"message, but you can also send a `work` message to report intermediate ".
"results.\n\n".
"To provide unit test results, pass a list of results in the `unit` ".
"parameter. Each result should be a dictionary with these keys:".
"\n\n".
"%s".
"\n\n".
"The `result` parameter recognizes these test results:".
"\n\n".
"%s".
"\n\n".
"This is a simple, valid value for the `unit` parameter. It reports ".
"one passing test and one failing test:\n\n".
"\n\n".
"```lang=json\n".
"%s".
"```".
"\n\n".
"Lint Results\n".
"============\n".
"Like unit test results, you can report lint results alongside a ".
"message. The `lint` parameter should contain results as a list of ".
"dictionaries with these keys:".
"\n\n".
"%s".
"\n\n".
"The `severity` parameter recognizes these severity levels:".
"\n\n".
"%s".
"\n\n".
"This is a simple, valid value for the `lint` parameter. It reports one ".
"error and one warning:".
"\n\n".
"```lang=json\n".
"%s".
"```".
"\n\n",
$message_table,
$unit_table,
$result_table,
$valid_unit,
$lint_table,
$severity_table,
$valid_lint);
}
protected function defineParamTypes() {
$messages = HarbormasterMessageType::getAllMessages();
$type_const = $this->formatStringConstants($messages);
return array(
'buildTargetPHID' => 'required phid',
'type' => 'required '.$type_const,
'unit' => 'optional list<wild>',
'lint' => 'optional list<wild>',
);
}
protected function defineReturnType() {
return 'void';
}
protected function execute(ConduitAPIRequest $request) {
$viewer = $request->getUser();
$build_target_phid = $request->getValue('buildTargetPHID');
$message_type = $request->getValue('type');
$build_target = id(new HarbormasterBuildTargetQuery())
->setViewer($viewer)
->withPHIDs(array($build_target_phid))
->executeOne();
if (!$build_target) {
throw new Exception(pht('No such build target!'));
}
$save = array();
$lint_messages = $request->getValue('lint', array());
foreach ($lint_messages as $lint) {
$save[] = HarbormasterBuildLintMessage::newFromDictionary(
$build_target,
$lint);
}
$unit_messages = $request->getValue('unit', array());
foreach ($unit_messages as $unit) {
$save[] = HarbormasterBuildUnitMessage::newFromDictionary(
$build_target,
$unit);
}
$save[] = HarbormasterBuildMessage::initializeNewMessage($viewer)
- ->setBuildTargetPHID($build_target->getPHID())
+ ->setReceiverPHID($build_target->getPHID())
->setType($message_type);
$build_target->openTransaction();
foreach ($save as $object) {
$object->save();
}
$build_target->saveTransaction();
// If the build has completely paused because all steps are blocked on
// waiting targets, this will resume it.
$build = $build_target->getBuild();
PhabricatorWorker::scheduleTask(
'HarbormasterBuildWorker',
array(
'buildID' => $build->getID(),
),
array(
'objectPHID' => $build->getPHID(),
));
return null;
}
}
diff --git a/src/applications/harbormaster/constants/HarbormasterBuildStatus.php b/src/applications/harbormaster/constants/HarbormasterBuildStatus.php
index b0e3105d7..dc63b5fd9 100644
--- a/src/applications/harbormaster/constants/HarbormasterBuildStatus.php
+++ b/src/applications/harbormaster/constants/HarbormasterBuildStatus.php
@@ -1,169 +1,204 @@
<?php
final class HarbormasterBuildStatus extends Phobject {
- /**
- * Not currently being built.
- */
const STATUS_INACTIVE = 'inactive';
-
- /**
- * Pending pick up by the Harbormaster daemon.
- */
const STATUS_PENDING = 'pending';
-
- /**
- * Current building the buildable.
- */
const STATUS_BUILDING = 'building';
-
- /**
- * The build has passed.
- */
const STATUS_PASSED = 'passed';
-
- /**
- * The build has failed.
- */
const STATUS_FAILED = 'failed';
-
- /**
- * The build has aborted.
- */
const STATUS_ABORTED = 'aborted';
-
- /**
- * The build encountered an unexpected error.
- */
const STATUS_ERROR = 'error';
-
- /**
- * The build has been paused.
- */
const STATUS_PAUSED = 'paused';
-
- /**
- * The build has been deadlocked.
- */
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 idx($spec, 'name', pht('Unknown ("%s")', $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 idx($spec, 'icon', 'fa-question-circle');
+ return $spec['icon'];
}
public static function getBuildStatusColor($status) {
$spec = self::getBuildStatusSpec($status);
- return idx($spec, 'color', 'bluegrey');
+ return $spec['color'];
}
public static function getBuildStatusANSIColor($status) {
$spec = self::getBuildStatusSpec($status);
- return idx($spec, 'color.ansi', 'magenta');
+ 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 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) {
- return idx(self::getBuildStatusSpecMap(), $status, array());
+ $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/constants/HarbormasterBuildableStatus.php b/src/applications/harbormaster/constants/HarbormasterBuildableStatus.php
new file mode 100644
index 000000000..d7e88ed29
--- /dev/null
+++ b/src/applications/harbormaster/constants/HarbormasterBuildableStatus.php
@@ -0,0 +1,109 @@
+<?php
+
+final class HarbormasterBuildableStatus extends Phobject {
+
+ const STATUS_PREPARING = 'preparing';
+ const STATUS_BUILDING = 'building';
+ const STATUS_PASSED = 'passed';
+ const STATUS_FAILED = 'failed';
+
+ private $key;
+ private $properties;
+
+ public function __construct($key, array $properties) {
+ $this->key = $key;
+ $this->properties = $properties;
+ }
+
+ public static function newBuildableStatusObject($status) {
+ $spec = self::getSpecification($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 buildable status property ("%s").',
+ $key));
+ }
+
+ return $this->properties[$key];
+ }
+
+ public function getIcon() {
+ return $this->getProperty('icon');
+ }
+
+ public function getDisplayName() {
+ return $this->getProperty('name');
+ }
+
+ public function getActionName() {
+ return $this->getProperty('name.action');
+ }
+
+ public function getColor() {
+ return $this->getProperty('color');
+ }
+
+ public function isPreparing() {
+ return ($this->key === self::STATUS_PREPARING);
+ }
+
+ public function isBuilding() {
+ return ($this->key === self::STATUS_BUILDING);
+ }
+
+ public function isFailed() {
+ return ($this->key === self::STATUS_FAILED);
+ }
+
+ public static function getOptionMap() {
+ return ipull(self::getSpecifications(), 'name');
+ }
+
+ private static function getSpecifications() {
+ return array(
+ self::STATUS_PREPARING => array(
+ 'name' => pht('Preparing'),
+ 'color' => 'blue',
+ 'icon' => 'fa-hourglass-o',
+ 'name.action' => pht('Build Preparing'),
+ ),
+ self::STATUS_BUILDING => array(
+ 'name' => pht('Building'),
+ 'color' => 'blue',
+ 'icon' => 'fa-chevron-circle-right',
+ 'name.action' => pht('Build Started'),
+ ),
+ self::STATUS_PASSED => array(
+ 'name' => pht('Passed'),
+ 'color' => 'green',
+ 'icon' => 'fa-check-circle',
+ 'name.action' => pht('Build Passed'),
+ ),
+ self::STATUS_FAILED => array(
+ 'name' => pht('Failed'),
+ 'color' => 'red',
+ 'icon' => 'fa-times-circle',
+ 'name.action' => pht('Build Failed'),
+ ),
+ );
+ }
+
+ private static function getSpecification($status) {
+ $map = self::getSpecifications();
+ if (isset($map[$status])) {
+ return $map[$status];
+ }
+
+ return array(
+ 'name' => pht('Unknown ("%s")', $status),
+ 'icon' => 'fa-question-circle',
+ 'color' => 'bluegrey',
+ 'name.action' => pht('Build Status'),
+ );
+ }
+
+}
diff --git a/src/applications/harbormaster/controller/HarbormasterBuildLogDownloadController.php b/src/applications/harbormaster/controller/HarbormasterBuildLogDownloadController.php
new file mode 100644
index 000000000..40313cdaf
--- /dev/null
+++ b/src/applications/harbormaster/controller/HarbormasterBuildLogDownloadController.php
@@ -0,0 +1,50 @@
+<?php
+
+final class HarbormasterBuildLogDownloadController
+ extends HarbormasterController {
+
+ 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
new file mode 100644
index 000000000..7a5050bec
--- /dev/null
+++ b/src/applications/harbormaster/controller/HarbormasterBuildLogRenderController.php
@@ -0,0 +1,893 @@
+<?php
+
+final class HarbormasterBuildLogRenderController
+ extends HarbormasterController {
+
+ 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
new file mode 100644
index 000000000..888b3bddb
--- /dev/null
+++ b/src/applications/harbormaster/controller/HarbormasterBuildLogViewController.php
@@ -0,0 +1,51 @@
+<?php
+
+final class HarbormasterBuildLogViewController
+ extends HarbormasterController {
+
+ 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 5b7171a20..0646bd930 100644
--- a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php
+++ b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php
@@ -1,667 +1,764 @@
<?php
final class HarbormasterBuildViewController
extends HarbormasterController {
public function handleRequest(AphrontRequest $request) {
$request = $this->getRequest();
$viewer = $request->getUser();
$id = $request->getURIData('id');
- $generation = $request->getInt('g');
$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');
- if ($build->isRestarting()) {
+ $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);
- if ($generation === null || $generation > $build->getBuildGeneration() ||
- $generation < 0) {
- $generation = $build->getBuildGeneration();
- }
-
$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)
- ->withBuildTargetPHIDs(mpull($build_targets, 'getPHID'))
+ ->withReceiverPHIDs(mpull($build_targets, 'getPHID'))
->execute();
- $messages = mgroup($messages, 'getBuildTargetPHID');
+ $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);
}
$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(
- $properties,
- $targets,
- $timeline,
- ));
+ ->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) {
$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);
+ $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($this->createLogHeader($build, $log))
->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) {
$request = $this->getRequest();
$limit = $request->getInt('l', 25);
$lines_25 = $this->getApplicationURI('/build/'.$build->getID().'/?l=25');
$lines_50 = $this->getApplicationURI('/build/'.$build->getID().'/?l=50');
$lines_100 =
$this->getApplicationURI('/build/'.$build->getID().'/?l=100');
$lines_0 = $this->getApplicationURI('/build/'.$build->getID().'/?l=0');
$link_25 = phutil_tag('a', array('href' => $lines_25), pht('25'));
$link_50 = phutil_tag('a', array('href' => $lines_50), pht('50'));
$link_100 = phutil_tag('a', array('href' => $lines_100), pht('100'));
$link_0 = phutil_tag('a', array('href' => $lines_0), pht('Unlimited'));
if ($limit === 25) {
$link_25 = phutil_tag('strong', array(), $link_25);
} else if ($limit === 50) {
$link_50 = phutil_tag('strong', array(), $link_50);
} else if ($limit === 100) {
$link_100 = phutil_tag('strong', array(), $link_100);
} else if ($limit === 0) {
$link_0 = phutil_tag('strong', array(), $link_0);
}
return phutil_tag(
'span',
array(),
array(
$link_25,
' - ',
$link_50,
' - ',
$link_100,
' - ',
$link_0,
' 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('Restarts'),
- $build->getBuildGeneration());
-
$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 6a47dae42..58e5cf8c4 100644
--- a/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php
+++ b/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php
@@ -1,351 +1,355 @@
<?php
final class HarbormasterBuildableViewController
extends HarbormasterController {
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)
->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())
->setBuildable($buildable)
->setUnitMessages($unit_data)
->setShowViewAll(true)
->setLimit(5);
} else {
$unit = null;
}
return array($lint, $unit);
}
}
diff --git a/src/applications/harbormaster/controller/HarbormasterPlanRunController.php b/src/applications/harbormaster/controller/HarbormasterPlanRunController.php
index 85bdbd8b4..fd227ee55 100644
--- a/src/applications/harbormaster/controller/HarbormasterPlanRunController.php
+++ b/src/applications/harbormaster/controller/HarbormasterPlanRunController.php
@@ -1,101 +1,107 @@
<?php
final class HarbormasterPlanRunController extends HarbormasterPlanController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$plan_id = $request->getURIData('id');
$plan = id(new HarbormasterBuildPlanQuery())
->setViewer($viewer)
->withIDs(array($plan_id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$plan) {
return new Aphront404Response();
}
$cancel_uri = $this->getApplicationURI("plan/{$plan_id}/");
if (!$plan->canRunManually()) {
return $this->newDialog()
->setTitle(pht('Can Not Run Plan'))
->appendParagraph(pht('This plan can not be run manually.'))
->addCancelButton($cancel_uri);
}
$e_name = true;
$v_name = null;
$errors = array();
if ($request->isFormPost()) {
$buildable = HarbormasterBuildable::initializeNewBuildable($viewer)
->setIsManualBuildable(true);
$v_name = $request->getStr('buildablePHID');
if ($v_name) {
$object = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withNames(array($v_name))
->executeOne();
if ($object instanceof HarbormasterBuildableInterface) {
$buildable
->setBuildablePHID($object->getHarbormasterBuildablePHID())
->setContainerPHID($object->getHarbormasterContainerPHID());
} else {
$e_name = pht('Invalid');
$errors[] = pht('Enter the name of a revision or commit.');
}
} else {
$e_name = pht('Required');
$errors[] = pht('You must choose a revision or commit to build.');
}
if (!$errors) {
$buildable->save();
+
+ $buildable->sendMessage(
+ $viewer,
+ HarbormasterMessageType::BUILDABLE_BUILD,
+ false);
+
$buildable->applyPlan($plan, array(), $viewer->getPHID());
$buildable_uri = '/B'.$buildable->getID();
return id(new AphrontRedirectResponse())->setURI($buildable_uri);
}
}
if ($errors) {
$errors = id(new PHUIInfoView())->setErrors($errors);
}
$title = pht('Run Build Plan Manually');
$save_button = pht('Run Plan Manually');
$form = id(new PHUIFormLayoutView())
->setUser($viewer)
->appendRemarkupInstructions(
pht(
"Enter the name of a commit or revision to run this plan on (for ".
"example, `rX123456` or `D123`).\n\n".
"For more detailed output, you can also run manual builds from ".
"the command line:\n\n".
" phabricator/ $ ./bin/harbormaster build <object> --plan %s",
$plan->getID()))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Buildable Name'))
->setName('buildablePHID')
->setError($e_name)
->setValue($v_name));
return $this->newDialog()
->setWidth(AphrontDialogView::WIDTH_FULL)
->setTitle($title)
->appendChild($form)
->addCancelButton($cancel_uri)
->addSubmitButton($save_button);
}
}
diff --git a/src/applications/harbormaster/engine/HarbormasterBuildEngine.php b/src/applications/harbormaster/engine/HarbormasterBuildEngine.php
index 0616e77b4..170e4c8a5 100644
--- a/src/applications/harbormaster/engine/HarbormasterBuildEngine.php
+++ b/src/applications/harbormaster/engine/HarbormasterBuildEngine.php
@@ -1,582 +1,605 @@
<?php
/**
* Moves a build forward by queuing build tasks, canceling or restarting the
* build, or failing it in response to task failures.
*/
final class HarbormasterBuildEngine extends Phobject {
private $build;
private $viewer;
private $newBuildTargets = array();
private $artifactReleaseQueue = array();
private $forceBuildableUpdate;
public function setForceBuildableUpdate($force_buildable_update) {
$this->forceBuildableUpdate = $force_buildable_update;
return $this;
}
public function shouldForceBuildableUpdate() {
return $this->forceBuildableUpdate;
}
public function queueNewBuildTarget(HarbormasterBuildTarget $target) {
$this->newBuildTargets[] = $target;
return $this;
}
public function getNewBuildTargets() {
return $this->newBuildTargets;
}
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setBuild(HarbormasterBuild $build) {
$this->build = $build;
return $this;
}
public function getBuild() {
return $this->build;
}
public function continueBuild() {
$build = $this->getBuild();
$lock_key = 'harbormaster.build:'.$build->getID();
$lock = PhabricatorGlobalLock::newLock($lock_key)->lock(15);
$build->reload();
$old_status = $build->getBuildStatus();
try {
$this->updateBuild($build);
} catch (Exception $ex) {
// If any exception is raised, the build is marked as a failure and the
// exception is re-thrown (this ensures we don't leave builds in an
// inconsistent state).
$build->setBuildStatus(HarbormasterBuildStatus::STATUS_ERROR);
$build->save();
$lock->unlock();
$this->releaseAllArtifacts($build);
throw $ex;
}
$lock->unlock();
// NOTE: We queue new targets after releasing the lock so that in-process
// execution via `bin/harbormaster` does not reenter the locked region.
foreach ($this->getNewBuildTargets() as $target) {
$task = PhabricatorWorker::scheduleTask(
'HarbormasterTargetWorker',
array(
'targetID' => $target->getID(),
),
array(
'objectPHID' => $target->getPHID(),
));
}
// If the build changed status, we might need to update the overall status
// on the buildable.
$new_status = $build->getBuildStatus();
if ($new_status != $old_status || $this->shouldForceBuildableUpdate()) {
$this->updateBuildable($build->getBuildable());
}
$this->releaseQueuedArtifacts();
// If we are no longer building for any reason, release all artifacts.
if (!$build->isBuilding()) {
$this->releaseAllArtifacts($build);
}
}
private function updateBuild(HarbormasterBuild $build) {
if ($build->isAborting()) {
$this->releaseAllArtifacts($build);
$build->setBuildStatus(HarbormasterBuildStatus::STATUS_ABORTED);
$build->save();
}
if (($build->getBuildStatus() == HarbormasterBuildStatus::STATUS_PENDING) ||
($build->isRestarting())) {
$this->restartBuild($build);
$build->setBuildStatus(HarbormasterBuildStatus::STATUS_BUILDING);
$build->save();
}
if ($build->isResuming()) {
$build->setBuildStatus(HarbormasterBuildStatus::STATUS_BUILDING);
$build->save();
}
if ($build->isPausing() && !$build->isComplete()) {
$build->setBuildStatus(HarbormasterBuildStatus::STATUS_PAUSED);
$build->save();
}
$build->deleteUnprocessedCommands();
if ($build->getBuildStatus() == HarbormasterBuildStatus::STATUS_BUILDING) {
$this->updateBuildSteps($build);
}
}
private function restartBuild(HarbormasterBuild $build) {
// We're restarting the build, so release all previous artifacts.
$this->releaseAllArtifacts($build);
// Increment the build generation counter on the build.
$build->setBuildGeneration($build->getBuildGeneration() + 1);
// Currently running targets should periodically check their build
// generation (which won't have changed) against the build's generation.
// If it is different, they will automatically stop what they're doing
// and abort.
- // Previously we used to delete targets, logs and artifacts here. Instead
+ // Previously we used to delete targets, logs and artifacts here. Instead,
// leave them around so users can view previous generations of this build.
}
private function updateBuildSteps(HarbormasterBuild $build) {
$all_targets = id(new HarbormasterBuildTargetQuery())
->setViewer($this->getViewer())
->withBuildPHIDs(array($build->getPHID()))
->withBuildGenerations(array($build->getBuildGeneration()))
->execute();
$this->updateWaitingTargets($all_targets);
$targets = mgroup($all_targets, 'getBuildStepPHID');
$steps = id(new HarbormasterBuildStepQuery())
->setViewer($this->getViewer())
->withBuildPlanPHIDs(array($build->getBuildPlan()->getPHID()))
->execute();
$steps = mpull($steps, null, 'getPHID');
// Identify steps which are in various states.
$queued = array();
$underway = array();
$waiting = array();
$complete = array();
$failed = array();
foreach ($steps as $step) {
$step_targets = idx($targets, $step->getPHID(), array());
if ($step_targets) {
$is_queued = false;
$is_underway = false;
foreach ($step_targets as $target) {
if ($target->isUnderway()) {
$is_underway = true;
break;
}
}
$is_waiting = false;
foreach ($step_targets as $target) {
if ($target->isWaiting()) {
$is_waiting = true;
break;
}
}
$is_complete = true;
foreach ($step_targets as $target) {
if (!$target->isComplete()) {
$is_complete = false;
break;
}
}
$is_failed = false;
foreach ($step_targets as $target) {
if ($target->isFailed()) {
$is_failed = true;
break;
}
}
} else {
$is_queued = true;
$is_underway = false;
$is_waiting = false;
$is_complete = false;
$is_failed = false;
}
if ($is_queued) {
$queued[$step->getPHID()] = true;
}
if ($is_underway) {
$underway[$step->getPHID()] = true;
}
if ($is_waiting) {
$waiting[$step->getPHID()] = true;
}
if ($is_complete) {
$complete[$step->getPHID()] = true;
}
if ($is_failed) {
$failed[$step->getPHID()] = true;
}
}
// If any step failed, fail the whole build, then bail.
if (count($failed)) {
$build->setBuildStatus(HarbormasterBuildStatus::STATUS_FAILED);
$build->save();
return;
}
// If every step is complete, we're done with this build. Mark it passed
// and bail.
if (count($complete) == count($steps)) {
$build->setBuildStatus(HarbormasterBuildStatus::STATUS_PASSED);
$build->save();
return;
}
// Release any artifacts which are not inputs to any remaining build
// step. We're done with these, so something else is free to use them.
$ongoing_phids = array_keys($queued + $waiting + $underway);
$ongoing_steps = array_select_keys($steps, $ongoing_phids);
$this->releaseUnusedArtifacts($all_targets, $ongoing_steps);
// Identify all the steps which are ready to run (because all their
// dependencies are complete).
$runnable = array();
foreach ($steps as $step) {
$dependencies = $step->getStepImplementation()->getDependencies($step);
if (isset($queued[$step->getPHID()])) {
$can_run = true;
foreach ($dependencies as $dependency) {
if (empty($complete[$dependency])) {
$can_run = false;
break;
}
}
if ($can_run) {
$runnable[] = $step;
}
}
}
if (!$runnable && !$waiting && !$underway) {
// This means the build is deadlocked, and the user has configured
// circular dependencies.
$build->setBuildStatus(HarbormasterBuildStatus::STATUS_DEADLOCKED);
$build->save();
return;
}
foreach ($runnable as $runnable_step) {
$target = HarbormasterBuildTarget::initializeNewBuildTarget(
$build,
$runnable_step,
$build->retrieveVariablesFromBuild());
$target->save();
$this->queueNewBuildTarget($target);
}
}
/**
* Release any artifacts which aren't used by any running or waiting steps.
*
* This releases artifacts as soon as they're no longer used. This can be
* particularly relevant when a build uses multiple hosts since it returns
* hosts to the pool more quickly.
*
* @param list<HarbormasterBuildTarget> Targets in the build.
* @param list<HarbormasterBuildStep> List of running and waiting steps.
* @return void
*/
private function releaseUnusedArtifacts(array $targets, array $steps) {
assert_instances_of($targets, 'HarbormasterBuildTarget');
assert_instances_of($steps, 'HarbormasterBuildStep');
if (!$targets || !$steps) {
return;
}
$target_phids = mpull($targets, 'getPHID');
$artifacts = id(new HarbormasterBuildArtifactQuery())
->setViewer($this->getViewer())
->withBuildTargetPHIDs($target_phids)
->withIsReleased(false)
->execute();
if (!$artifacts) {
return;
}
// Collect all the artifacts that remaining build steps accept as inputs.
$must_keep = array();
foreach ($steps as $step) {
$inputs = $step->getStepImplementation()->getArtifactInputs();
foreach ($inputs as $input) {
$artifact_key = $input['key'];
$must_keep[$artifact_key] = true;
}
}
// Queue unreleased artifacts which no remaining step uses for immediate
// release.
foreach ($artifacts as $artifact) {
$key = $artifact->getArtifactKey();
if (isset($must_keep[$key])) {
continue;
}
$this->artifactReleaseQueue[] = $artifact;
}
}
/**
* Process messages which were sent to these targets, kicking applicable
* targets out of "Waiting" and into either "Passed" or "Failed".
*
* @param list<HarbormasterBuildTarget> List of targets to process.
* @return void
*/
private function updateWaitingTargets(array $targets) {
assert_instances_of($targets, 'HarbormasterBuildTarget');
// We only care about messages for targets which are actually in a waiting
// state.
$waiting_targets = array();
foreach ($targets as $target) {
if ($target->isWaiting()) {
$waiting_targets[$target->getPHID()] = $target;
}
}
if (!$waiting_targets) {
return;
}
$messages = id(new HarbormasterBuildMessageQuery())
->setViewer($this->getViewer())
- ->withBuildTargetPHIDs(array_keys($waiting_targets))
+ ->withReceiverPHIDs(array_keys($waiting_targets))
->withConsumed(false)
->execute();
foreach ($messages as $message) {
- $target = $waiting_targets[$message->getBuildTargetPHID()];
+ $target = $waiting_targets[$message->getReceiverPHID()];
switch ($message->getType()) {
case HarbormasterMessageType::MESSAGE_PASS:
$new_status = HarbormasterBuildTarget::STATUS_PASSED;
break;
case HarbormasterMessageType::MESSAGE_FAIL:
$new_status = HarbormasterBuildTarget::STATUS_FAILED;
break;
case HarbormasterMessageType::MESSAGE_WORK:
default:
$new_status = null;
break;
}
if ($new_status !== null) {
$message->setIsConsumed(true);
$message->save();
$target->setTargetStatus($new_status);
if ($target->isComplete()) {
$target->setDateCompleted(PhabricatorTime::getNow());
}
$target->save();
}
}
}
/**
* Update the overall status of the buildable this build is attached to.
*
* After a build changes state (for example, passes or fails) it may affect
* the overall state of the associated buildable. Compute the new aggregate
* state and save it on the buildable.
*
* @param HarbormasterBuild The buildable to update.
* @return void
*/
- private function updateBuildable(HarbormasterBuildable $buildable) {
+ public function updateBuildable(HarbormasterBuildable $buildable) {
$viewer = $this->getViewer();
$lock_key = 'harbormaster.buildable:'.$buildable->getID();
$lock = PhabricatorGlobalLock::newLock($lock_key)->lock(15);
$buildable = id(new HarbormasterBuildableQuery())
->setViewer($viewer)
->withIDs(array($buildable->getID()))
->needBuilds(true)
->executeOne();
- $all_pass = true;
- $any_fail = false;
- foreach ($buildable->getBuilds() as $build) {
- if ($build->getBuildStatus() != HarbormasterBuildStatus::STATUS_PASSED) {
- $all_pass = false;
- }
- if (in_array($build->getBuildStatus(), array(
- HarbormasterBuildStatus::STATUS_FAILED,
- HarbormasterBuildStatus::STATUS_ERROR,
- HarbormasterBuildStatus::STATUS_DEADLOCKED,
- ))) {
+ $messages = id(new HarbormasterBuildMessageQuery())
+ ->setViewer($viewer)
+ ->withReceiverPHIDs(array($buildable->getPHID()))
+ ->withConsumed(false)
+ ->execute();
- $any_fail = true;
+ $done_preparing = false;
+ $update_container = false;
+ foreach ($messages as $message) {
+ switch ($message->getType()) {
+ case HarbormasterMessageType::BUILDABLE_BUILD:
+ $done_preparing = true;
+ break;
+ case HarbormasterMessageType::BUILDABLE_CONTAINER:
+ $update_container = true;
+ break;
+ default:
+ break;
}
+
+ $message
+ ->setIsConsumed(true)
+ ->save();
}
- if ($any_fail) {
- $new_status = HarbormasterBuildable::STATUS_FAILED;
- } else if ($all_pass) {
- $new_status = HarbormasterBuildable::STATUS_PASSED;
- } else {
- $new_status = HarbormasterBuildable::STATUS_BUILDING;
+ // If we received a "build" command, all builds are scheduled and we can
+ // move out of "preparing" into "building".
+ if ($done_preparing) {
+ if ($buildable->isPreparing()) {
+ $buildable
+ ->setBuildableStatus(HarbormasterBuildableStatus::STATUS_BUILDING)
+ ->save();
+ }
}
- $old_status = $buildable->getBuildableStatus();
- $did_update = ($old_status != $new_status);
- if ($did_update) {
- $buildable->setBuildableStatus($new_status);
- $buildable->save();
+ // If we've been informed that the container for the buildable has
+ // changed, update it.
+ if ($update_container) {
+ $object = id(new PhabricatorObjectQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(array($buildable->getBuildablePHID()))
+ ->executeOne();
+ if ($object) {
+ $buildable
+ ->setContainerPHID($object->getHarbormasterContainerPHID())
+ ->save();
+ }
}
- $lock->unlock();
+ $old = clone $buildable;
- // If we changed the buildable status, try to post a transaction to the
- // object about it. We can safely do this outside of the locked region.
+ // Don't update the buildable status if we're still preparing builds: more
+ // builds may still be scheduled shortly, so even if every build we know
+ // about so far has passed, that doesn't mean the buildable has actually
+ // passed everything it needs to.
- // NOTE: We only post transactions for automatic buildables, not for
- // manual ones: manual builds are test builds, whoever is doing tests
- // can look at the results themselves, and other users generally don't
- // care about the outcome.
+ if (!$buildable->isPreparing()) {
+ $all_pass = true;
+ $any_fail = false;
+ foreach ($buildable->getBuilds() as $build) {
+ if (!$build->isPassed()) {
+ $all_pass = false;
+ }
- $should_publish = $did_update &&
- $new_status != HarbormasterBuildable::STATUS_BUILDING &&
- !$buildable->getIsManualBuildable();
+ if ($build->isComplete() && !$build->isPassed()) {
+ $any_fail = true;
+ }
+ }
- if (!$should_publish) {
- return;
- }
+ if ($any_fail) {
+ $new_status = HarbormasterBuildableStatus::STATUS_FAILED;
+ } else if ($all_pass) {
+ $new_status = HarbormasterBuildableStatus::STATUS_PASSED;
+ } else {
+ $new_status = HarbormasterBuildableStatus::STATUS_BUILDING;
+ }
- $object = id(new PhabricatorObjectQuery())
- ->setViewer($viewer)
- ->withPHIDs(array($buildable->getBuildablePHID()))
- ->executeOne();
- if (!$object) {
- return;
+ $did_update = ($old->getBuildableStatus() !== $new_status);
+ if ($did_update) {
+ $buildable->setBuildableStatus($new_status);
+ $buildable->save();
+ }
}
- $publish_phid = $object->getHarbormasterPublishablePHID();
- if (!$publish_phid) {
+ $lock->unlock();
+
+ // Don't publish anything if we're still preparing builds.
+ if ($buildable->isPreparing()) {
return;
}
- if ($publish_phid === $object->getPHID()) {
- $publish = $object;
- } else {
- $publish = id(new PhabricatorObjectQuery())
- ->setViewer($viewer)
- ->withPHIDs(array($publish_phid))
- ->executeOne();
- if (!$publish) {
- return;
- }
- }
+ $this->publishBuildable($old, $buildable);
+ }
- if (!($publish instanceof PhabricatorApplicationTransactionInterface)) {
- return;
- }
+ public function publishBuildable(
+ HarbormasterBuildable $old,
+ HarbormasterBuildable $new) {
+
+ $viewer = $this->getViewer();
- $template = $publish->getApplicationTransactionTemplate();
- if (!$template) {
+ // Publish the buildable. We publish buildables even if they haven't
+ // changed status in Harbormaster because applications may care about
+ // different things than Harbormaster does. For example, Differential
+ // does not care about local lint and unit tests when deciding whether
+ // a revision should move out of draft or not.
+
+ // NOTE: We're publishing both automatic and manual buildables. Buildable
+ // objects should generally ignore manual buildables, but it's up to them
+ // to decide.
+
+ $object = id(new PhabricatorObjectQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(array($new->getBuildablePHID()))
+ ->executeOne();
+ if (!$object) {
return;
}
- $template
- ->setTransactionType(PhabricatorTransactions::TYPE_BUILDABLE)
- ->setMetadataValue(
- 'harbormaster:buildablePHID',
- $buildable->getPHID())
- ->setOldValue($old_status)
- ->setNewValue($new_status);
-
- $harbormaster_phid = id(new PhabricatorHarbormasterApplication())
- ->getPHID();
+ $engine = HarbormasterBuildableEngine::newForObject($object, $viewer);
$daemon_source = PhabricatorContentSource::newForSource(
PhabricatorDaemonContentSource::SOURCECONST);
- $editor = $publish->getApplicationTransactionEditor()
- ->setActor($viewer)
+ $harbormaster_phid = id(new PhabricatorHarbormasterApplication())
+ ->getPHID();
+
+ $engine
->setActingAsPHID($harbormaster_phid)
->setContentSource($daemon_source)
- ->setContinueOnNoEffect(true)
- ->setContinueOnMissingFields(true);
-
- $editor->applyTransactions(
- $publish->getApplicationTransactionObject(),
- array($template));
+ ->publishBuildable($old, $new);
}
private function releaseAllArtifacts(HarbormasterBuild $build) {
$targets = id(new HarbormasterBuildTargetQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withBuildPHIDs(array($build->getPHID()))
->withBuildGenerations(array($build->getBuildGeneration()))
->execute();
if (count($targets) === 0) {
return;
}
$target_phids = mpull($targets, 'getPHID');
$artifacts = id(new HarbormasterBuildArtifactQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withBuildTargetPHIDs($target_phids)
->withIsReleased(false)
->execute();
foreach ($artifacts as $artifact) {
$artifact->releaseArtifact();
}
}
private function releaseQueuedArtifacts() {
foreach ($this->artifactReleaseQueue as $key => $artifact) {
$artifact->releaseArtifact();
unset($this->artifactReleaseQueue[$key]);
}
}
}
diff --git a/src/applications/harbormaster/engine/HarbormasterBuildableEngine.php b/src/applications/harbormaster/engine/HarbormasterBuildableEngine.php
new file mode 100644
index 000000000..41d31673d
--- /dev/null
+++ b/src/applications/harbormaster/engine/HarbormasterBuildableEngine.php
@@ -0,0 +1,105 @@
+<?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);
+ }
+
+
+}
diff --git a/src/applications/harbormaster/engine/HarbormasterMessageType.php b/src/applications/harbormaster/engine/HarbormasterMessageType.php
index 5900817c6..135fb23ff 100644
--- a/src/applications/harbormaster/engine/HarbormasterMessageType.php
+++ b/src/applications/harbormaster/engine/HarbormasterMessageType.php
@@ -1,44 +1,47 @@
<?php
final class HarbormasterMessageType extends Phobject {
const MESSAGE_PASS = 'pass';
const MESSAGE_FAIL = 'fail';
const MESSAGE_WORK = 'work';
+ const BUILDABLE_BUILD = 'build';
+ const BUILDABLE_CONTAINER = 'container';
+
public static function getAllMessages() {
return array_keys(self::getMessageSpecifications());
}
public static function getMessageDescription($message) {
$spec = self::getMessageSpecification($message);
if (!$spec) {
return null;
}
return idx($spec, 'description');
}
private static function getMessageSpecification($message) {
$specs = self::getMessageSpecifications();
return idx($specs, $message);
}
private static function getMessageSpecifications() {
return array(
self::MESSAGE_PASS => array(
'description' => pht(
'Report that the target is complete, and the target has passed.'),
),
self::MESSAGE_FAIL => array(
'description' => pht(
'Report that the target is complete, and the target has failed.'),
),
self::MESSAGE_WORK => array(
'description' => pht(
'Report that work on the target is ongoing. This message can be '.
'used to report partial results during a build.'),
),
);
}
}
diff --git a/src/applications/harbormaster/event/HarbormasterUIEventListener.php b/src/applications/harbormaster/event/HarbormasterUIEventListener.php
index 219f556b3..f5e6aee3e 100644
--- a/src/applications/harbormaster/event/HarbormasterUIEventListener.php
+++ b/src/applications/harbormaster/event/HarbormasterUIEventListener.php
@@ -1,148 +1,153 @@
<?php
final class HarbormasterUIEventListener
extends PhabricatorEventListener {
public function register() {
$this->listen(PhabricatorEventType::TYPE_UI_WILLRENDERPROPERTIES);
}
public function handleEvent(PhutilEvent $event) {
switch ($event->getType()) {
case PhabricatorEventType::TYPE_UI_WILLRENDERPROPERTIES:
$this->handlePropertyEvent($event);
break;
}
}
private function handlePropertyEvent($ui_event) {
$viewer = $ui_event->getUser();
$object = $ui_event->getValue('object');
if (!$object || !$object->getPHID()) {
// No object, or the object has no PHID yet..
return;
}
if ($object instanceof HarbormasterBuildable) {
// Although HarbormasterBuildable implements the correct interface, it
// does not make sense to show a build's build status. In the best case
// it is meaningless, and in the worst case it's confusing.
return;
}
if ($object instanceof DifferentialRevision) {
// TODO: This is a bit hacky and we could probably find a cleaner fix
// eventually, but we show build status on each diff, immediately below
// this property list, so it's redundant to show it on the revision view.
return;
}
if (!($object instanceof HarbormasterBuildableInterface)) {
return;
}
$buildable_phid = $object->getHarbormasterBuildablePHID();
if (!$buildable_phid) {
return;
}
if (!$this->canUseApplication($ui_event->getUser())) {
return;
}
- $buildable = id(new HarbormasterBuildableQuery())
- ->setViewer($viewer)
- ->withManualBuildables(false)
- ->withBuildablePHIDs(array($buildable_phid))
- ->needBuilds(true)
- ->needTargets(true)
- ->executeOne();
+ try {
+ $buildable = id(new HarbormasterBuildableQuery())
+ ->setViewer($viewer)
+ ->withManualBuildables(false)
+ ->withBuildablePHIDs(array($buildable_phid))
+ ->needBuilds(true)
+ ->needTargets(true)
+ ->executeOne();
+ } catch (PhabricatorPolicyException $ex) {
+ // TODO: See PHI430. When this query raises a policy exception, it
+ // fatals the whole page because it happens very late in execution,
+ // during final page rendering. If the viewer can't see the buildable or
+ // some of the builds, just hide this element for now.
+ return;
+ }
+
if (!$buildable) {
return;
}
$builds = $buildable->getBuilds();
$targets = array();
foreach ($builds as $build) {
foreach ($build->getBuildTargets() as $target) {
$targets[] = $target;
}
}
if ($targets) {
$artifacts = id(new HarbormasterBuildArtifactQuery())
->setViewer($viewer)
->withBuildTargetPHIDs(mpull($targets, 'getPHID'))
->withArtifactTypes(
array(
HarbormasterURIArtifact::ARTIFACTCONST,
))
->execute();
$artifacts = mgroup($artifacts, 'getBuildTargetPHID');
} else {
$artifacts = array();
}
$status_view = new PHUIStatusListView();
- $buildable_status = $buildable->getBuildableStatus();
- $buildable_icon = HarbormasterBuildable::getBuildableStatusIcon(
- $buildable_status);
- $buildable_color = HarbormasterBuildable::getBuildableStatusColor(
- $buildable_status);
- $buildable_name = HarbormasterBuildable::getBuildableStatusName(
- $buildable_status);
+ $buildable_icon = $buildable->getStatusIcon();
+ $buildable_color = $buildable->getStatusColor();
+ $buildable_name = $buildable->getStatusDisplayName();
$target = phutil_tag(
'a',
array(
'href' => '/'.$buildable->getMonogram(),
),
pht('Buildable %d', $buildable->getID()));
$target = phutil_tag('strong', array(), $target);
$status_view
->addItem(
id(new PHUIStatusItemView())
->setIcon($buildable_icon, $buildable_color, $buildable_name)
->setTarget($target));
foreach ($builds as $build) {
$item = new PHUIStatusItemView();
$item->setTarget($viewer->renderHandle($build->getPHID()));
$links = array();
foreach ($build->getBuildTargets() as $build_target) {
$uris = idx($artifacts, $build_target->getPHID(), array());
foreach ($uris as $uri) {
$impl = $uri->getArtifactImplementation();
if ($impl->isExternalLink()) {
$links[] = $impl->renderLink();
}
}
}
if ($links) {
$links = phutil_implode_html(" \xC2\xB7 ", $links);
$item->setNote($links);
}
$status = $build->getBuildStatus();
$status_name = HarbormasterBuildStatus::getBuildStatusName($status);
$icon = HarbormasterBuildStatus::getBuildStatusIcon($status);
$color = HarbormasterBuildStatus::getBuildStatusColor($status);
$item->setIcon($icon, $color, $status_name);
$status_view->addItem($item);
}
$view = $ui_event->getValue('view');
$view->addProperty(pht('Build Status'), $status_view);
}
}
diff --git a/src/applications/harbormaster/interface/HarbormasterBuildableInterface.php b/src/applications/harbormaster/interface/HarbormasterBuildableInterface.php
index 598f2b61f..72ddf6ac9 100644
--- a/src/applications/harbormaster/interface/HarbormasterBuildableInterface.php
+++ b/src/applications/harbormaster/interface/HarbormasterBuildableInterface.php
@@ -1,39 +1,26 @@
<?php
interface HarbormasterBuildableInterface {
/**
* Get the object PHID which best identifies this buildable to humans.
*
* This object is the primary object associated with the buildable in the
* UI. The most human-readable object for a buildable varies: for example,
* for diffs the container (the revision) is more meaningful than the
* buildable (the diff), but for commits the buildable (the commit) is more
* meaningful than the container (the repository).
*
* @return phid Related object PHID most meaningful for human viewers.
*/
public function getHarbormasterBuildableDisplayPHID();
public function getHarbormasterBuildablePHID();
public function getHarbormasterContainerPHID();
-
- /**
- * Get the object PHID which build status should be published to.
- *
- * In some cases (like commits), this is the object itself. In other cases,
- * it is a different object: for example, diffs publish builds to revisions.
- *
- * This method can return `null` to disable publishing.
- *
- * @return phid|null Build status updates will be published to this object's
- * transaction timeline.
- */
- public function getHarbormasterPublishablePHID();
-
-
public function getBuildVariables();
public function getAvailableBuildVariables();
+ public function newBuildableEngine();
+
}
diff --git a/src/applications/harbormaster/management/HarbormasterManagementBuildWorkflow.php b/src/applications/harbormaster/management/HarbormasterManagementBuildWorkflow.php
index 6fd3cf2ff..2efd443a9 100644
--- a/src/applications/harbormaster/management/HarbormasterManagementBuildWorkflow.php
+++ b/src/applications/harbormaster/management/HarbormasterManagementBuildWorkflow.php
@@ -1,113 +1,118 @@
<?php
final class HarbormasterManagementBuildWorkflow
extends HarbormasterManagementWorkflow {
protected function didConstruct() {
$this
->setName('build')
->setExamples('**build** [__options__] __buildable__ --plan __id__')
->setSynopsis(pht('Run plan __id__ on __buildable__.'))
->setArguments(
array(
array(
'name' => 'plan',
'param' => 'id',
'help' => pht('ID of build plan to run.'),
),
array(
'name' => 'background',
'help' => pht(
'Submit builds into the build queue normally instead of '.
'running them in the foreground.'),
),
array(
'name' => 'buildable',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$viewer = $this->getViewer();
$names = $args->getArg('buildable');
if (count($names) != 1) {
throw new PhutilArgumentUsageException(
pht('Specify exactly one buildable object, by object name.'));
}
$name = head($names);
$buildable = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withNames($names)
->executeOne();
if (!$buildable) {
throw new PhutilArgumentUsageException(
pht('No such buildable "%s"!', $name));
}
if (!($buildable instanceof HarbormasterBuildableInterface)) {
throw new PhutilArgumentUsageException(
pht('Object "%s" is not a buildable!', $name));
}
$plan_id = $args->getArg('plan');
if (!$plan_id) {
throw new PhutilArgumentUsageException(
pht(
'Use %s to specify a build plan to run.',
'--plan'));
}
$plan = id(new HarbormasterBuildPlanQuery())
->setViewer($viewer)
->withIDs(array($plan_id))
->executeOne();
if (!$plan) {
throw new PhutilArgumentUsageException(
pht('Build plan "%s" does not exist.', $plan_id));
}
if (!$plan->canRunManually()) {
throw new PhutilArgumentUsageException(
pht('This build plan can not be run manually.'));
}
$console = PhutilConsole::getConsole();
$buildable = HarbormasterBuildable::initializeNewBuildable($viewer)
->setIsManualBuildable(true)
->setBuildablePHID($buildable->getHarbormasterBuildablePHID())
->setContainerPHID($buildable->getHarbormasterContainerPHID())
->save();
+ $buildable->sendMessage(
+ $viewer,
+ HarbormasterMessageType::BUILDABLE_BUILD,
+ false);
+
$console->writeOut(
"%s\n",
pht(
'Applying plan %s to new buildable %s...',
$plan->getID(),
'B'.$buildable->getID()));
$console->writeOut(
"\n %s\n\n",
PhabricatorEnv::getProductionURI('/B'.$buildable->getID()));
if (!$args->getArg('background')) {
PhabricatorWorker::setRunAllTasksInProcess(true);
}
if ($viewer->isOmnipotent()) {
$initiator = id(new PhabricatorHarbormasterApplication())->getPHID();
} else {
$initiator = $viewer->getPHID();
}
$buildable->applyPlan($plan, array(), $initiator);
$console->writeOut("%s\n", pht('Done.'));
return 0;
}
}
diff --git a/src/applications/harbormaster/management/HarbormasterManagementPublishWorkflow.php b/src/applications/harbormaster/management/HarbormasterManagementPublishWorkflow.php
new file mode 100644
index 000000000..df42d8632
--- /dev/null
+++ b/src/applications/harbormaster/management/HarbormasterManagementPublishWorkflow.php
@@ -0,0 +1,87 @@
+<?php
+
+final class HarbormasterManagementPublishWorkflow
+ extends HarbormasterManagementWorkflow {
+
+ protected function didConstruct() {
+ $this
+ ->setName('publish')
+ ->setExamples(pht('**publish** __buildable__ ...'))
+ ->setSynopsis(
+ pht(
+ 'Publish a buildable. This is primarily useful for developing '.
+ 'and debugging applications which have buildable objects.'))
+ ->setArguments(
+ array(
+ array(
+ 'name' => 'buildable',
+ 'wildcard' => true,
+ ),
+ ));
+ }
+
+ public function execute(PhutilArgumentParser $args) {
+ $viewer = $this->getViewer();
+
+ $buildable_names = $args->getArg('buildable');
+ if (!$buildable_names) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Name one or more buildables to publish, like "B123".'));
+ }
+
+ $query = id(new PhabricatorObjectQuery())
+ ->setViewer($viewer)
+ ->withNames($buildable_names);
+
+ $query->execute();
+
+ $result_map = $query->getNamedResults();
+
+ foreach ($buildable_names as $name) {
+ if (!isset($result_map[$name])) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Argument "%s" does not name a buildable. Provide one or more '.
+ 'valid buildable monograms or PHIDs.',
+ $name));
+ }
+ }
+
+ foreach ($result_map as $name => $result) {
+ if (!($result instanceof HarbormasterBuildable)) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Object "%s" is not a HarbormasterBuildable (it is a "%s"). '.
+ 'Name one or more buildables to publish, like "B123".',
+ get_class($result)));
+ }
+ }
+
+ foreach ($result_map as $buildable) {
+ echo tsprintf(
+ "%s\n",
+ pht(
+ 'Publishing "%s"...',
+ $buildable->getMonogram()));
+
+ // Reload the buildable to pick up builds.
+ $buildable = id(new HarbormasterBuildableQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($buildable->getID()))
+ ->needBuilds(true)
+ ->executeOne();
+
+ $engine = id(new HarbormasterBuildEngine())
+ ->setViewer($viewer)
+ ->publishBuildable($buildable, $buildable);
+ }
+
+ echo tsprintf(
+ "%s\n",
+ pht('Done.'));
+
+ return 0;
+ }
+
+}
diff --git a/src/applications/harbormaster/management/HarbormasterManagementRebuildLogWorkflow.php b/src/applications/harbormaster/management/HarbormasterManagementRebuildLogWorkflow.php
new file mode 100644
index 000000000..1dc91bcf0
--- /dev/null
+++ b/src/applications/harbormaster/management/HarbormasterManagementRebuildLogWorkflow.php
@@ -0,0 +1,102 @@
+<?php
+
+final class HarbormasterManagementRebuildLogWorkflow
+ extends HarbormasterManagementWorkflow {
+
+ protected function didConstruct() {
+ $this
+ ->setName('rebuild-log')
+ ->setExamples(
+ pht(
+ "**rebuild-log** --id __id__ [__options__]\n".
+ "**rebuild-log** --all"))
+ ->setSynopsis(
+ pht(
+ 'Rebuild the file and summary for a log. This is primarily '.
+ 'intended to make it easier to develop new log summarizers.'))
+ ->setArguments(
+ array(
+ array(
+ 'name' => 'id',
+ 'param' => 'id',
+ 'help' => pht('Log to rebuild.'),
+ ),
+ array(
+ 'name' => 'all',
+ 'help' => pht('Rebuild all logs.'),
+ ),
+ array(
+ 'name' => 'force',
+ 'help' => pht(
+ 'Force logs to rebuild even if they appear to be in good '.
+ 'shape already.'),
+ ),
+ ));
+ }
+
+ public function execute(PhutilArgumentParser $args) {
+ $viewer = $this->getViewer();
+
+ $is_force = $args->getArg('force');
+
+ $log_id = $args->getArg('id');
+ $is_all = $args->getArg('all');
+
+ if (!$is_all && !$log_id) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Choose a build log to rebuild with "--id", or rebuild all '.
+ 'logs with "--all".'));
+ }
+
+ if ($is_all && $log_id) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'You can not specify both "--id" and "--all". Choose one or '.
+ 'the other.'));
+ }
+
+ if ($log_id) {
+ $log = id(new HarbormasterBuildLogQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($log_id))
+ ->executeOne();
+ if (!$log) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Unable to load build log "%s".',
+ $log_id));
+ }
+ $logs = array($log);
+ } else {
+ $logs = new LiskMigrationIterator(new HarbormasterBuildLog());
+ }
+
+ PhabricatorWorker::setRunAllTasksInProcess(true);
+
+ foreach ($logs as $log) {
+ echo tsprintf(
+ "%s\n",
+ pht(
+ 'Rebuilding log "%s"...',
+ pht('Build Log %d', $log->getID())));
+
+ try {
+ $log->scheduleRebuild($is_force);
+ } catch (Exception $ex) {
+ if ($is_all) {
+ phlog($ex);
+ } else {
+ throw $ex;
+ }
+ }
+ }
+
+ echo tsprintf(
+ "%s\n",
+ pht('Done.'));
+
+ return 0;
+ }
+
+}
diff --git a/src/applications/harbormaster/management/HarbormasterManagementWriteLogWorkflow.php b/src/applications/harbormaster/management/HarbormasterManagementWriteLogWorkflow.php
new file mode 100644
index 000000000..3676b1415
--- /dev/null
+++ b/src/applications/harbormaster/management/HarbormasterManagementWriteLogWorkflow.php
@@ -0,0 +1,111 @@
+<?php
+
+final class HarbormasterManagementWriteLogWorkflow
+ extends HarbormasterManagementWorkflow {
+
+ protected function didConstruct() {
+ $this
+ ->setName('write-log')
+ ->setExamples('**write-log** --target __id__ [__options__]')
+ ->setSynopsis(
+ pht(
+ 'Write a new Harbormaster build log. This is primarily intended '.
+ 'to make development and testing easier.'))
+ ->setArguments(
+ array(
+ array(
+ 'name' => 'target',
+ 'param' => 'id',
+ 'help' => pht('Build Target ID to attach the log to.'),
+ ),
+ array(
+ 'name' => 'rate',
+ 'param' => 'bytes',
+ 'help' => pht(
+ 'Limit the rate at which the log is written, to test '.
+ 'live log streaming.'),
+ ),
+ ));
+ }
+
+ public function execute(PhutilArgumentParser $args) {
+ $viewer = $this->getViewer();
+
+ $target_id = $args->getArg('target');
+ if (!$target_id) {
+ throw new PhutilArgumentUsageException(
+ pht('Choose a build target to attach the log to with "--target".'));
+ }
+
+ $target = id(new HarbormasterBuildTargetQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($target_id))
+ ->executeOne();
+ if (!$target) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Unable to load build target "%s".',
+ $target_id));
+ }
+
+ $log = HarbormasterBuildLog::initializeNewBuildLog($target);
+ $log->openBuildLog();
+
+ echo tsprintf(
+ "%s\n\n __%s__\n\n",
+ pht('Opened a new build log:'),
+ PhabricatorEnv::getURI($log->getURI()));
+
+ echo tsprintf(
+ "%s\n",
+ pht('Reading log content from stdin...'));
+
+ $content = file_get_contents('php://stdin');
+
+ $rate = $args->getArg('rate');
+ if ($rate) {
+ if ($rate <= 0) {
+ throw new Exception(
+ pht(
+ 'Write rate must be more than 0 bytes/sec.'));
+ }
+
+ echo tsprintf(
+ "%s\n",
+ pht('Writing log, slowly...'));
+
+ $offset = 0;
+ $total = strlen($content);
+ $pieces = str_split($content, $rate);
+
+ $bar = id(new PhutilConsoleProgressBar())
+ ->setTotal($total);
+
+ foreach ($pieces as $piece) {
+ $log->append($piece);
+ $bar->update(strlen($piece));
+ sleep(1);
+ }
+
+ $bar->done();
+
+ } else {
+ $log->append($content);
+ }
+
+ echo tsprintf(
+ "%s\n",
+ pht('Write completed. Closing log...'));
+
+ PhabricatorWorker::setRunAllTasksInProcess(true);
+
+ $log->closeBuildLog();
+
+ echo tsprintf(
+ "%s\n",
+ pht('Done.'));
+
+ return 0;
+ }
+
+}
diff --git a/src/applications/harbormaster/phid/HarbormasterBuildLogPHIDType.php b/src/applications/harbormaster/phid/HarbormasterBuildLogPHIDType.php
index c0fba81c4..a2fa58fe7 100644
--- a/src/applications/harbormaster/phid/HarbormasterBuildLogPHIDType.php
+++ b/src/applications/harbormaster/phid/HarbormasterBuildLogPHIDType.php
@@ -1,37 +1,41 @@
<?php
final class HarbormasterBuildLogPHIDType extends PhabricatorPHIDType {
const TYPECONST = 'HMCL';
public function getTypeName() {
return pht('Build Log');
}
public function newObject() {
return new HarbormasterBuildLog();
}
public function getPHIDTypeApplicationClass() {
return 'PhabricatorHarbormasterApplication';
}
protected function buildQueryForObjects(
PhabricatorObjectQuery $query,
array $phids) {
return id(new HarbormasterBuildLogQuery())
->withPHIDs($phids);
}
public function loadHandles(
PhabricatorHandleQuery $query,
array $handles,
array $objects) {
foreach ($handles as $phid => $handle) {
$build_log = $objects[$phid];
+
+ $handle
+ ->setName(pht('Build Log %d', $build_log->getID()))
+ ->setURI($build_log->getURI());
}
}
}
diff --git a/src/applications/harbormaster/query/HarbormasterBuildLogQuery.php b/src/applications/harbormaster/query/HarbormasterBuildLogQuery.php
index 3d45de686..ad27d0958 100644
--- a/src/applications/harbormaster/query/HarbormasterBuildLogQuery.php
+++ b/src/applications/harbormaster/query/HarbormasterBuildLogQuery.php
@@ -1,99 +1,90 @@
<?php
final class HarbormasterBuildLogQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $buildPHIDs;
private $buildTargetPHIDs;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withBuildTargetPHIDs(array $build_target_phids) {
$this->buildTargetPHIDs = $build_target_phids;
return $this;
}
+ public function newResultObject() {
+ return new HarbormasterBuildLog();
+ }
+
protected function loadPage() {
- $table = new HarbormasterBuildLog();
- $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 willFilterPage(array $page) {
$build_targets = array();
$build_target_phids = array_filter(mpull($page, 'getBuildTargetPHID'));
if ($build_target_phids) {
$build_targets = id(new HarbormasterBuildTargetQuery())
->setViewer($this->getViewer())
->withPHIDs($build_target_phids)
->setParentQuery($this)
->execute();
$build_targets = mpull($build_targets, null, 'getPHID');
}
foreach ($page as $key => $build_log) {
$build_target_phid = $build_log->getBuildTargetPHID();
if (empty($build_targets[$build_target_phid])) {
unset($page[$key]);
continue;
}
$build_log->attachBuildTarget($build_targets[$build_target_phid]);
}
return $page;
}
- 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->buildTargetPHIDs) {
+ if ($this->buildTargetPHIDs !== null) {
$where[] = qsprintf(
- $conn_r,
+ $conn,
'buildTargetPHID IN (%Ls)',
$this->buildTargetPHIDs);
}
- $where[] = $this->buildPagingClause($conn_r);
-
- return $this->formatWhereClause($where);
+ return $where;
}
public function getQueryApplicationClass() {
return 'PhabricatorHarbormasterApplication';
}
}
diff --git a/src/applications/harbormaster/query/HarbormasterBuildLogSearchEngine.php b/src/applications/harbormaster/query/HarbormasterBuildLogSearchEngine.php
new file mode 100644
index 000000000..3698238b1
--- /dev/null
+++ b/src/applications/harbormaster/query/HarbormasterBuildLogSearchEngine.php
@@ -0,0 +1,79 @@
+<?php
+
+final class HarbormasterBuildLogSearchEngine
+ extends PhabricatorApplicationSearchEngine {
+
+ public function getResultTypeDescription() {
+ return pht('Harbormaster Build Logs');
+ }
+
+ public function getApplicationClassName() {
+ return 'PhabricatorHarbormasterApplication';
+ }
+
+ public function newQuery() {
+ return new HarbormasterBuildLogQuery();
+ }
+
+ protected function buildCustomSearchFields() {
+ return array(
+ id(new PhabricatorPHIDsSearchField())
+ ->setLabel(pht('Build Targets'))
+ ->setKey('buildTargetPHIDs')
+ ->setAliases(
+ array(
+ 'buildTargetPHID',
+ 'buildTargets',
+ 'buildTarget',
+ 'targetPHIDs',
+ 'targetPHID',
+ 'targets',
+ 'target',
+ ))
+ ->setDescription(
+ pht('Search for logs that belong to a particular build target.')),
+ );
+ }
+
+ protected function buildQueryFromParameters(array $map) {
+ $query = $this->newQuery();
+
+ if ($map['buildTargetPHIDs']) {
+ $query->withBuildTargetPHIDs($map['buildTargetPHIDs']);
+ }
+
+ return $query;
+ }
+
+ protected function getURI($path) {
+ return '/harbormaster/log/'.$path;
+ }
+
+ protected function getBuiltinQueryNames() {
+ return array(
+ 'all' => pht('All Builds'),
+ );
+ }
+
+ 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 $builds,
+ PhabricatorSavedQuery $query,
+ array $handles) {
+
+ // For now, this SearchEngine is only for driving the API.
+ throw new PhutilMethodNotImplementedException();
+ }
+
+}
diff --git a/src/applications/harbormaster/query/HarbormasterBuildMessageQuery.php b/src/applications/harbormaster/query/HarbormasterBuildMessageQuery.php
index 749fdd76d..134c68ce0 100644
--- a/src/applications/harbormaster/query/HarbormasterBuildMessageQuery.php
+++ b/src/applications/harbormaster/query/HarbormasterBuildMessageQuery.php
@@ -1,98 +1,92 @@
<?php
final class HarbormasterBuildMessageQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
- private $buildTargetPHIDs;
+ private $receiverPHIDs;
private $consumed;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
- public function withBuildTargetPHIDs(array $phids) {
- $this->buildTargetPHIDs = $phids;
+ public function withReceiverPHIDs(array $phids) {
+ $this->receiverPHIDs = $phids;
return $this;
}
public function withConsumed($consumed) {
$this->consumed = $consumed;
return $this;
}
+ public function newResultObject() {
+ return new HarbormasterBuildMessage();
+ }
+
protected function loadPage() {
- $table = new HarbormasterBuildMessage();
- $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 willFilterPage(array $page) {
- $build_target_phids = array_filter(mpull($page, 'getBuildTargetPHID'));
- if ($build_target_phids) {
- $build_targets = id(new PhabricatorObjectQuery())
+ $receiver_phids = array_filter(mpull($page, 'getReceiverPHID'));
+ if ($receiver_phids) {
+ $receivers = id(new PhabricatorObjectQuery())
->setViewer($this->getViewer())
- ->withPHIDs($build_target_phids)
+ ->withPHIDs($receiver_phids)
->setParentQuery($this)
->execute();
- $build_targets = mpull($build_targets, null, 'getPHID');
+ $receivers = mpull($receivers, null, 'getPHID');
} else {
- $build_targets = array();
+ $receivers = array();
}
foreach ($page as $key => $message) {
- $build_target_phid = $message->getBuildTargetPHID();
- if (empty($build_targets[$build_target_phid])) {
+ $receiver_phid = $message->getReceiverPHID();
+
+ if (empty($receivers[$receiver_phid])) {
unset($page[$key]);
+ $this->didRejectResult($message);
continue;
}
- $message->attachBuildTarget($build_targets[$build_target_phid]);
+
+ $message->attachReceiver($receivers[$receiver_phid]);
}
return $page;
}
- 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->buildTargetPHIDs) {
+ if ($this->receiverPHIDs !== null) {
$where[] = qsprintf(
- $conn_r,
- 'buildTargetPHID IN (%Ls)',
- $this->buildTargetPHIDs);
+ $conn,
+ 'receiverPHID IN (%Ls)',
+ $this->receiverPHIDs);
}
if ($this->consumed !== null) {
$where[] = qsprintf(
- $conn_r,
+ $conn,
'isConsumed = %d',
(int)$this->consumed);
}
- $where[] = $this->buildPagingClause($conn_r);
-
- return $this->formatWhereClause($where);
+ return $where;
}
public function getQueryApplicationClass() {
return 'PhabricatorHarbormasterApplication';
}
}
diff --git a/src/applications/harbormaster/query/HarbormasterBuildableSearchEngine.php b/src/applications/harbormaster/query/HarbormasterBuildableSearchEngine.php
index cfff27b1a..36f5dc89c 100644
--- a/src/applications/harbormaster/query/HarbormasterBuildableSearchEngine.php
+++ b/src/applications/harbormaster/query/HarbormasterBuildableSearchEngine.php
@@ -1,190 +1,188 @@
<?php
final class HarbormasterBuildableSearchEngine
extends PhabricatorApplicationSearchEngine {
public function getResultTypeDescription() {
return pht('Harbormaster Buildables');
}
public function getApplicationClassName() {
return 'PhabricatorHarbormasterApplication';
}
public function newQuery() {
return new HarbormasterBuildableQuery();
}
protected function buildCustomSearchFields() {
return array(
id(new PhabricatorSearchStringListField())
->setKey('objectPHIDs')
->setAliases(array('objects'))
->setLabel(pht('Objects'))
->setPlaceholder(pht('rXabcdef, PHID-DIFF-1234, ...'))
->setDescription(pht('Search for builds of particular objects.')),
id(new PhabricatorSearchStringListField())
->setKey('containerPHIDs')
->setAliases(array('containers'))
->setLabel(pht('Containers'))
->setPlaceholder(pht('rXYZ, R123, D456, ...'))
->setDescription(
pht('Search for builds by containing revision or repository.')),
id(new PhabricatorSearchCheckboxesField())
->setKey('statuses')
->setLabel(pht('Statuses'))
- ->setOptions(HarbormasterBuildable::getBuildStatusMap())
+ ->setOptions(HarbormasterBuildableStatus::getOptionMap())
->setDescription(pht('Search for builds by buildable status.')),
id(new PhabricatorSearchThreeStateField())
->setLabel(pht('Manual'))
->setKey('manual')
->setDescription(
pht('Search for only manual or automatic buildables.'))
->setOptions(
pht('(Show All)'),
pht('Show Only Manual Builds'),
pht('Show Only Automated Builds')),
);
}
private function resolvePHIDs(array $names) {
$viewer = $this->requireViewer();
$objects = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withNames($names)
->execute();
// TODO: Instead of using string lists, we should ideally be using some
// kind of smart field with resolver logic that can help users type the
// right stuff. For now, just return a bogus value here so nothing matches
// but the form doesn't explode.
if (!$objects) {
return array('-');
}
return mpull($objects, 'getPHID');
}
protected function buildQueryFromParameters(array $map) {
$query = $this->newQuery();
if ($map['objectPHIDs']) {
$phids = $this->resolvePHIDs($map['objectPHIDs']);
if ($phids) {
$query->withBuildablePHIDs($phids);
}
}
if ($map['containerPHIDs']) {
$phids = $this->resolvePHIDs($map['containerPHIDs']);
if ($phids) {
$query->withContainerPHIDs($phids);
}
}
if ($map['statuses']) {
$query->withStatuses($map['statuses']);
}
if ($map['manual'] !== null) {
$query->withManualBuildables($map['manual']);
}
return $query;
}
protected function getURI($path) {
return '/harbormaster/'.$path;
}
protected function getBuiltinQueryNames() {
return array(
'all' => pht('All Buildables'),
);
}
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 $buildables,
PhabricatorSavedQuery $query,
array $handles) {
assert_instances_of($buildables, 'HarbormasterBuildable');
$viewer = $this->requireViewer();
$phids = array();
foreach ($buildables as $buildable) {
$phids[] = $buildable->getBuildableObject()
->getHarbormasterBuildableDisplayPHID();
$phids[] = $buildable->getContainerPHID();
$phids[] = $buildable->getBuildablePHID();
}
$handles = $viewer->loadHandles($phids);
$list = new PHUIObjectItemListView();
foreach ($buildables as $buildable) {
$id = $buildable->getID();
$display_phid = $buildable->getBuildableObject()
->getHarbormasterBuildableDisplayPHID();
$container_phid = $buildable->getContainerPHID();
$buildable_phid = $buildable->getBuildablePHID();
$item = id(new PHUIObjectItemView())
->setObjectName(pht('Buildable %d', $buildable->getID()));
if ($display_phid) {
$handle = $handles[$display_phid];
$item->setHeader($handle->getFullName());
}
if ($container_phid && ($container_phid != $display_phid)) {
$handle = $handles[$container_phid];
$item->addAttribute($handle->getName());
}
if ($buildable_phid && ($buildable_phid != $display_phid)) {
$handle = $handles[$buildable_phid];
$item->addAttribute($handle->getFullName());
}
$item->setHref($buildable->getURI());
if ($buildable->getIsManualBuildable()) {
$item->addIcon('fa-wrench grey', pht('Manual'));
}
- $status = $buildable->getBuildableStatus();
-
- $status_icon = HarbormasterBuildable::getBuildableStatusIcon($status);
- $status_color = HarbormasterBuildable::getBuildableStatusColor($status);
- $status_label = HarbormasterBuildable::getBuildableStatusName($status);
+ $status_icon = $buildable->getStatusIcon();
+ $status_color = $buildable->getStatusColor();
+ $status_label = $buildable->getStatusDisplayName();
$item->setStatusIcon("{$status_icon} {$status_color}", $status_label);
$list->addItem($item);
}
$result = new PhabricatorApplicationSearchResultView();
$result->setObjectList($list);
$result->setNoDataString(pht('No buildables found.'));
return $result;
}
}
diff --git a/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php
index ca6f30bd3..47af99263 100644
--- a/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php
+++ b/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php
@@ -1,315 +1,327 @@
<?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\\./-]+)\\}@';
$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.'));
+ }
/* -( 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 3ecd6553d..89d4002ee 100644
--- a/src/applications/harbormaster/step/HarbormasterBuildkiteBuildStepImplementation.php
+++ b/src/applications/harbormaster/step/HarbormasterBuildkiteBuildStepImplementation.php
@@ -1,210 +1,215 @@
<?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(),
),
);
$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/step/HarbormasterCircleCIBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterCircleCIBuildStepImplementation.php
index e74e25a39..f0104779f 100644
--- a/src/applications/harbormaster/step/HarbormasterCircleCIBuildStepImplementation.php
+++ b/src/applications/harbormaster/step/HarbormasterCircleCIBuildStepImplementation.php
@@ -1,253 +1,258 @@
<?php
final class HarbormasterCircleCIBuildStepImplementation
extends HarbormasterBuildStepImplementation {
public function getName() {
return pht('Build with CircleCI');
}
public function getGenericDescription() {
return pht('Trigger a build in CircleCI.');
}
public function getBuildStepGroupKey() {
return HarbormasterExternalBuildStepGroup::GROUPKEY;
}
public function getDescription() {
return pht('Run a build in CircleCI.');
}
public function getEditInstructions() {
$hook_uri = '/harbormaster/hook/circleci/';
$hook_uri = PhabricatorEnv::getProductionURI($hook_uri);
return pht(<<<EOTEXT
WARNING: This build step is new and experimental!
To build **revisions** with CircleCI, they must:
- belong to a tracked repository;
- the repository must have a Staging Area configured;
- the Staging Area must be hosted on GitHub; and
- you must configure the webhook described below.
To build **commits** with CircleCI, they must:
- belong to a repository that is being imported from GitHub; and
- you must configure the webhook described below.
Webhook Configuration
=====================
Add this webhook to your `circle.yml` file to make CircleCI report results
to Harbormaster. Until you install this hook, builds will hang waiting for
a response from CircleCI.
```lang=yml
notify:
webhooks:
- url: %s
```
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 static function getGitHubPath($uri) {
$uri_object = new PhutilURI($uri);
$domain = $uri_object->getDomain();
$domain = phutil_utf8_strtolower($domain);
switch ($domain) {
case 'github.com':
case 'www.github.com':
return $uri_object->getPath();
default:
return null;
}
}
public function execute(
HarbormasterBuild $build,
HarbormasterBuildTarget $build_target) {
$viewer = PhabricatorUser::getOmnipotentUser();
+ if (PhabricatorEnv::getEnvConfig('phabricator.silent')) {
+ $this->logSilencedCall($build, $build_target, pht('CircleCI'));
+ throw new HarbormasterBuildFailureException();
+ }
+
$buildable = $build->getBuildable();
$object = $buildable->getBuildableObject();
$object_phid = $object->getPHID();
if (!($object instanceof HarbormasterCircleCIBuildableInterface)) {
throw new Exception(
pht(
'Object ("%s") does not implement interface "%s". Only objects '.
'which implement this interface can be built with CircleCI.',
$object_phid,
'HarbormasterCircleCIBuildableInterface'));
}
$github_uri = $object->getCircleCIGitHubRepositoryURI();
$build_type = $object->getCircleCIBuildIdentifierType();
$build_identifier = $object->getCircleCIBuildIdentifier();
$path = self::getGitHubPath($github_uri);
if ($path === null) {
throw new Exception(
pht(
'Object ("%s") claims "%s" is a GitHub repository URI, but the '.
'domain does not appear to be GitHub.',
$object_phid,
$github_uri));
}
$path_parts = trim($path, '/');
$path_parts = explode('/', $path_parts);
if (count($path_parts) < 2) {
throw new Exception(
pht(
'Object ("%s") claims "%s" is a GitHub repository URI, but the '.
'path ("%s") does not have enough components (expected at least '.
'two).',
$object_phid,
$github_uri,
$path));
}
list($github_namespace, $github_name) = $path_parts;
$github_name = preg_replace('(\\.git$)', '', $github_name);
$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));
}
// When we pass "revision", the branch is ignored (and does not even need
// to exist), and only shows up in the UI. Use a cute string which will
// certainly never break anything or cause any kind of problem.
$ship = "\xF0\x9F\x9A\xA2";
$branch = "{$ship}Harbormaster";
$token = $api_token->getSecret()->openEnvelope();
$parts = array(
'https://circleci.com/api/v1/project',
phutil_escape_uri($github_namespace),
phutil_escape_uri($github_name)."?circle-token={$token}",
);
$uri = implode('/', $parts);
$data_structure = array();
switch ($build_type) {
case 'tag':
$data_structure['tag'] = $build_identifier;
break;
case 'revision':
$data_structure['revision'] = $build_identifier;
break;
default:
throw new Exception(
pht(
'Unknown CircleCI build type "%s". Expected "%s" or "%s".',
$build_type,
'tag',
'revision'));
}
$data_structure['build_parameters'] = array(
'HARBORMASTER_BUILD_TARGET_PHID' => $build_target->getPHID(),
);
$json_data = phutil_json_encode($data_structure);
$future = id(new HTTPSFuture($uri, $json_data))
->setMethod('POST')
->addHeader('Content-Type', 'application/json')
->addHeader('Accept', 'application/json')
->setTimeout(60);
$this->resolveFutures(
$build,
$build_target,
array($future));
$this->logHTTPResponse($build, $build_target, $future, pht('CircleCI'));
list($status, $body) = $future->resolve();
if ($status->isError()) {
throw new HarbormasterBuildFailureException();
}
$response = phutil_json_decode($body);
$build_uri = idx($response, 'build_url');
if (!$build_uri) {
throw new Exception(
pht(
'CircleCI did not return a "%s"!',
'build_url'));
}
$target_phid = $build_target->getPHID();
// Write an artifact to create a link to the external build in CircleCI.
$api_method = 'harbormaster.createartifact';
$api_params = array(
'buildTargetPHID' => $target_phid,
'artifactType' => HarbormasterURIArtifact::ARTIFACTCONST,
'artifactKey' => 'circleci.uri',
'artifactData' => array(
'uri' => $build_uri,
'name' => pht('View in CircleCI'),
'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,
),
);
}
public function supportsWaitForMessage() {
// NOTE: We always wait for a message, but don't need to show the UI
// control since "Wait" is the only valid choice.
return false;
}
public function shouldWaitForMessage(HarbormasterBuildTarget $target) {
return true;
}
}
diff --git a/src/applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php
index 016f29642..59866d3b7 100644
--- a/src/applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php
+++ b/src/applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php
@@ -1,110 +1,116 @@
<?php
final class HarbormasterHTTPRequestBuildStepImplementation
extends HarbormasterBuildStepImplementation {
public function getName() {
return pht('Make HTTP Request');
}
public function getGenericDescription() {
return pht('Make an HTTP request.');
}
public function getBuildStepGroupKey() {
return HarbormasterExternalBuildStepGroup::GROUPKEY;
}
public function getDescription() {
$domain = null;
$uri = $this->getSetting('uri');
if ($uri) {
$domain = id(new PhutilURI($uri))->getDomain();
}
$method = $this->formatSettingForDescription('method', 'POST');
$domain = $this->formatValueForDescription($domain);
if ($this->getSetting('credential')) {
return pht(
'Make an authenticated HTTP %s request to %s.',
$method,
$domain);
} else {
return pht(
'Make an HTTP %s request to %s.',
$method,
$domain);
}
}
public function execute(
HarbormasterBuild $build,
HarbormasterBuildTarget $build_target) {
$viewer = PhabricatorUser::getOmnipotentUser();
+
+ if (PhabricatorEnv::getEnvConfig('phabricator.silent')) {
+ $this->logSilencedCall($build, $build_target, pht('HTTP Request'));
+ throw new HarbormasterBuildFailureException();
+ }
+
$settings = $this->getSettings();
$variables = $build_target->getVariables();
$uri = $this->mergeVariables(
'vurisprintf',
$settings['uri'],
$variables);
$method = nonempty(idx($settings, 'method'), 'POST');
$future = id(new HTTPSFuture($uri))
->setMethod($method)
->setTimeout(60);
$credential_phid = $this->getSetting('credential');
if ($credential_phid) {
$key = PassphrasePasswordKey::loadFromPHID(
$credential_phid,
$viewer);
$future->setHTTPBasicAuthCredentials(
$key->getUsernameEnvelope()->openEnvelope(),
$key->getPasswordEnvelope());
}
$this->resolveFutures(
$build,
$build_target,
array($future));
$this->logHTTPResponse($build, $build_target, $future, $uri);
list($status) = $future->resolve();
if ($status->isError()) {
throw new HarbormasterBuildFailureException();
}
}
public function getFieldSpecifications() {
return array(
'uri' => array(
'name' => pht('URI'),
'type' => 'text',
'required' => true,
),
'method' => array(
'name' => pht('HTTP Method'),
'type' => 'select',
'options' => array_fuse(array('POST', 'GET', 'PUT', 'DELETE')),
),
'credential' => array(
'name' => pht('Credentials'),
'type' => 'credential',
'credential.type'
=> PassphrasePasswordCredentialType::CREDENTIAL_TYPE,
'credential.provides'
=> PassphrasePasswordCredentialType::PROVIDES_TYPE,
),
);
}
public function supportsWaitForMessage() {
return true;
}
}
diff --git a/src/applications/harbormaster/storage/HarbormasterBuildMessage.php b/src/applications/harbormaster/storage/HarbormasterBuildMessage.php
index 20d138ad0..34aab3957 100644
--- a/src/applications/harbormaster/storage/HarbormasterBuildMessage.php
+++ b/src/applications/harbormaster/storage/HarbormasterBuildMessage.php
@@ -1,77 +1,89 @@
<?php
/**
* A message sent to an executing build target by an external system. We
* capture these messages and process them asynchronously to avoid race
* conditions where we receive a message before a build plan is ready to
* accept it.
*/
-final class HarbormasterBuildMessage extends HarbormasterDAO
- implements PhabricatorPolicyInterface {
+final class HarbormasterBuildMessage
+ extends HarbormasterDAO
+ implements
+ PhabricatorPolicyInterface,
+ PhabricatorDestructibleInterface {
protected $authorPHID;
- protected $buildTargetPHID;
+ protected $receiverPHID;
protected $type;
protected $isConsumed;
- private $buildTarget = self::ATTACHABLE;
+ private $receiver = self::ATTACHABLE;
public static function initializeNewMessage(PhabricatorUser $actor) {
$actor_phid = $actor->getPHID();
if (!$actor_phid) {
$actor_phid = id(new PhabricatorHarbormasterApplication())->getPHID();
}
return id(new HarbormasterBuildMessage())
->setAuthorPHID($actor_phid)
->setIsConsumed(0);
}
protected function getConfiguration() {
return array(
self::CONFIG_COLUMN_SCHEMA => array(
'type' => 'text16',
'isConsumed' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
- 'key_buildtarget' => array(
- 'columns' => array('buildTargetPHID'),
+ 'key_receiver' => array(
+ 'columns' => array('receiverPHID'),
),
),
) + parent::getConfiguration();
}
- public function getBuildTarget() {
- return $this->assertAttached($this->buildTarget);
+ public function getReceiver() {
+ return $this->assertAttached($this->receiver);
}
- public function attachBuildTarget(HarbormasterBuildTarget $target) {
- $this->buildTarget = $target;
+ public function attachReceiver($receiver) {
+ $this->receiver = $receiver;
return $this;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
- return $this->getBuildTarget()->getPolicy($capability);
+ return $this->getReceiver()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
- return $this->getBuildTarget()->hasAutomaticCapability(
+ return $this->getReceiver()->hasAutomaticCapability(
$capability,
$viewer);
}
public function describeAutomaticCapability($capability) {
- return pht('Build messages have the same policies as their targets.');
+ return pht('Build messages have the same policies as their receivers.');
+ }
+
+
+/* -( PhabricatorDestructibleInterface )----------------------------------- */
+
+
+ public function destroyObjectPermanently(
+ PhabricatorDestructionEngine $engine) {
+ $this->delete();
}
}
diff --git a/src/applications/harbormaster/storage/HarbormasterBuildable.php b/src/applications/harbormaster/storage/HarbormasterBuildable.php
index 9761fb287..7d4f2e7c8 100644
--- a/src/applications/harbormaster/storage/HarbormasterBuildable.php
+++ b/src/applications/harbormaster/storage/HarbormasterBuildable.php
@@ -1,333 +1,377 @@
<?php
-final class HarbormasterBuildable extends HarbormasterDAO
+final class HarbormasterBuildable
+ extends HarbormasterDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
- HarbormasterBuildableInterface {
+ HarbormasterBuildableInterface,
+ PhabricatorDestructibleInterface {
protected $buildablePHID;
protected $containerPHID;
protected $buildableStatus;
protected $isManualBuildable;
private $buildableObject = self::ATTACHABLE;
private $containerObject = self::ATTACHABLE;
private $builds = self::ATTACHABLE;
- const STATUS_BUILDING = 'building';
- const STATUS_PASSED = 'passed';
- const STATUS_FAILED = 'failed';
-
- public static function getBuildableStatusName($status) {
- $map = self::getBuildStatusMap();
- return idx($map, $status, pht('Unknown ("%s")', $status));
- }
-
- public static function getBuildStatusMap() {
- return array(
- self::STATUS_BUILDING => pht('Building'),
- self::STATUS_PASSED => pht('Passed'),
- self::STATUS_FAILED => pht('Failed'),
- );
- }
-
- public static function getBuildableStatusIcon($status) {
- switch ($status) {
- case self::STATUS_BUILDING:
- return PHUIStatusItemView::ICON_RIGHT;
- case self::STATUS_PASSED:
- return PHUIStatusItemView::ICON_ACCEPT;
- case self::STATUS_FAILED:
- return PHUIStatusItemView::ICON_REJECT;
- default:
- return PHUIStatusItemView::ICON_QUESTION;
- }
- }
-
- public static function getBuildableStatusColor($status) {
- switch ($status) {
- case self::STATUS_BUILDING:
- return 'blue';
- case self::STATUS_PASSED:
- return 'green';
- case self::STATUS_FAILED:
- return 'red';
- default:
- return 'bluegrey';
- }
- }
-
public static function initializeNewBuildable(PhabricatorUser $actor) {
return id(new HarbormasterBuildable())
->setIsManualBuildable(0)
- ->setBuildableStatus(self::STATUS_BUILDING);
+ ->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();
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 getHarbormasterPublishablePHID() {
- return $this->getBuildableObject()->getHarbormasterPublishablePHID();
- }
-
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 92d429391..ae0f6b13f 100644
--- a/src/applications/harbormaster/storage/build/HarbormasterBuild.php
+++ b/src/applications/harbormaster/storage/build/HarbormasterBuild.php
@@ -1,453 +1,488 @@
<?php
final class HarbormasterBuild extends HarbormasterDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
- PhabricatorConduitResultInterface {
+ 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->getBuildStatus() === HarbormasterBuildStatus::STATUS_PENDING ||
- $this->getBuildStatus() === HarbormasterBuildStatus::STATUS_BUILDING;
+ 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 in_array(
- $this->getBuildStatus(),
- HarbormasterBuildStatus::getCompletedStatusConstants());
+ return $this->getBuildStatusObject()->isComplete();
}
public function isPaused() {
- return ($this->getBuildStatus() == HarbormasterBuildStatus::STATUS_PAUSED);
+ 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);
}
}
/* -( 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/HarbormasterBuildArtifact.php b/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php
index 461ef4b06..7cd8d60b6 100644
--- a/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php
+++ b/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php
@@ -1,150 +1,168 @@
<?php
-final class HarbormasterBuildArtifact extends HarbormasterDAO
- implements PhabricatorPolicyInterface {
+final class HarbormasterBuildArtifact
+ extends HarbormasterDAO
+ implements
+ PhabricatorPolicyInterface,
+ PhabricatorDestructibleInterface {
protected $buildTargetPHID;
protected $artifactType;
protected $artifactIndex;
protected $artifactKey;
protected $artifactData = array();
protected $isReleased = 0;
private $buildTarget = self::ATTACHABLE;
private $artifactImplementation;
public static function initializeNewBuildArtifact(
HarbormasterBuildTarget $build_target) {
return id(new HarbormasterBuildArtifact())
->attachBuildTarget($build_target)
->setBuildTargetPHID($build_target->getPHID());
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'artifactData' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'artifactType' => 'text32',
'artifactIndex' => 'bytes12',
'artifactKey' => 'text255',
'isReleased' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_artifact' => array(
'columns' => array('artifactType', 'artifactIndex'),
'unique' => true,
),
'key_garbagecollect' => array(
'columns' => array('artifactType', 'dateCreated'),
),
'key_target' => array(
'columns' => array('buildTargetPHID', 'artifactType'),
),
'key_index' => array(
'columns' => array('artifactIndex'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
HarbormasterBuildArtifactPHIDType::TYPECONST);
}
public function attachBuildTarget(HarbormasterBuildTarget $build_target) {
$this->buildTarget = $build_target;
return $this;
}
public function getBuildTarget() {
return $this->assertAttached($this->buildTarget);
}
public function setArtifactKey($key) {
$target = $this->getBuildTarget();
$this->artifactIndex = self::getArtifactIndex($target, $key);
$this->artifactKey = $key;
return $this;
}
public static function getArtifactIndex(
HarbormasterBuildTarget $target,
$artifact_key) {
$build = $target->getBuild();
$parts = array(
$build->getPHID(),
$target->getBuildGeneration(),
$artifact_key,
);
$parts = implode("\0", $parts);
return PhabricatorHash::digestForIndex($parts);
}
public function releaseArtifact() {
if ($this->getIsReleased()) {
return $this;
}
$impl = $this->getArtifactImplementation();
if ($impl) {
$impl->releaseArtifact(PhabricatorUser::getOmnipotentUser());
}
return $this
->setIsReleased(1)
->save();
}
public function getArtifactImplementation() {
if ($this->artifactImplementation === null) {
$type = $this->getArtifactType();
$impl = HarbormasterArtifact::getArtifactType($type);
if (!$impl) {
return null;
}
$impl = clone $impl;
$impl->setBuildArtifact($this);
$this->artifactImplementation = $impl;
}
return $this->artifactImplementation;
}
public function getProperty($key, $default = null) {
return idx($this->artifactData, $key, $default);
}
/* -( 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 buildable to see its artifacts.');
}
+
+/* -( PhabricatorDestructibleInterface )----------------------------------- */
+
+
+ public function destroyObjectPermanently(
+ PhabricatorDestructionEngine $engine) {
+
+ $viewer = $this->getViewer();
+
+ $this->openTransaction();
+ $this->releaseArtifact($viewer);
+ $this->delete();
+ $this->saveTransaction();
+ }
+
}
diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php b/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php
index 290b288c6..5bc358ccc 100644
--- a/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php
+++ b/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php
@@ -1,306 +1,780 @@
<?php
final class HarbormasterBuildLog
extends HarbormasterDAO
- implements PhabricatorPolicyInterface {
+ 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 = 102400;
+ 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(0);
- }
-
- public function openBuildLog() {
- if ($this->isOpen) {
- throw new Exception(pht('This build log is already open!'));
- }
-
- $this->isOpen = true;
-
- return $this
->setLive(1)
- ->save();
+ ->setByteLength(0)
+ ->setChunkFormat(HarbormasterBuildLogChunk::CHUNK_ENCODING_TEXT);
}
- public function closeBuildLog() {
- if (!$this->isOpen) {
- throw new Exception(pht('This build log is not open!'));
- }
-
- if ($this->canCompressLog()) {
- $this->compressLog();
- }
-
- $start = $this->getDateCreated();
- $now = PhabricatorTime::getNow();
-
- return $this
- ->setDuration($now - $start)
- ->setLive(0)
- ->save();
+ 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 append($content) {
- if (!$this->getLive()) {
- throw new PhutilInvalidStateException('openBuildLog');
- }
+ public function newChunkIterator() {
+ return id(new HarbormasterBuildLogChunkIterator($this))
+ ->setPageSize(8);
+ }
- $content = (string)$content;
+ public function newDataIterator() {
+ return $this->newChunkIterator()
+ ->setAsString(true);
+ }
- $this->rope->append($content);
- $this->flush();
+ private function loadLastChunkInfo() {
+ $chunk_table = new HarbormasterBuildLogChunk();
+ $conn_w = $chunk_table->establishConnection('w');
- return $this;
+ 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());
}
- private function flush() {
+ public function loadData($offset, $length) {
+ $end = ($offset + $length);
- // 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.
+ $chunks = id(new HarbormasterBuildLogChunk())->loadAllWhere(
+ 'logID = %d AND headOffset < %d AND tailOffset >= %d
+ ORDER BY headOffset ASC',
+ $this->getID(),
+ $end,
+ $offset);
- $chunk_table = id(new HarbormasterBuildLogChunk())->getTableName();
- $chunk_limit = self::CHUNK_BYTE_LIMIT;
- $encoding_text = HarbormasterBuildLogChunk::CHUNK_ENCODING_TEXT;
-
- $rope = $this->rope;
+ // 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(),
+ );
+ }
- while (true) {
- $length = $rope->getByteLength();
- if (!$length) {
- break;
+ $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]);
}
+ }
- $conn_w = $this->establishConnection('w');
- $last = $this->loadLastChunkInfo();
+ if (count($ranges) !== 1) {
+ $display_ranges = array();
+ foreach ($ranges as $range) {
+ $display_ranges[] = pht(
+ '(%d - %d)',
+ $range['head'],
+ $range['tail']);
+ }
- $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;
+ if (!$display_ranges) {
+ $display_ranges[] = pht('<null>');
}
- $data_limit = ($chunk_limit - $prefix_size);
- $append_data = $rope->getPrefixBytes($data_limit);
- $data_size = strlen($append_data);
+ 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)));
+ }
- if ($append_id) {
- queryfx(
- $conn_w,
- 'UPDATE %T SET chunk = CONCAT(chunk, %B), size = %d WHERE id = %d',
- $chunk_table,
- $append_data,
- $prefix_size + $data_size,
- $append_id);
- } else {
- $this->writeChunk($encoding_text, $data_size, $append_data);
- }
+ $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']));
+ }
- $rope->removeBytesFromHead($data_size);
+ $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 newChunkIterator() {
- return id(new HarbormasterBuildLogChunkIterator($this))
- ->setPageSize(32);
+ 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);
}
- 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 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);
}
- private function canCompressLog() {
+
+ 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) {
- $this->writeEncodedChunk($rope, $byte_limit, $mode);
+ $offset += $this->writeEncodedChunk($rope, $offset, $byte_limit, $mode);
}
}
while ($rope->getByteLength()) {
- $this->writeEncodedChunk($rope, $byte_limit, $mode);
+ $offset += $this->writeEncodedChunk($rope, $offset, $byte_limit, $mode);
}
+ $this
+ ->setChunkFormat($mode)
+ ->save();
+
$this->saveTransaction();
}
- private function writeEncodedChunk(PhutilRope $rope, $length, $mode) {
+ 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, $size, $data);
+ $this->writeChunk($mode, $offset, $size, $data);
$rope->removeBytesFromHead($size);
+
+ return $size;
}
- private function writeChunk($encoding, $raw_size, $data) {
+ 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;
+
+ 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 it's build log.");
+ '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/HarbormasterBuildLogChunk.php b/src/applications/harbormaster/storage/build/HarbormasterBuildLogChunk.php
index e69bad4ca..ff3e817c5 100644
--- a/src/applications/harbormaster/storage/build/HarbormasterBuildLogChunk.php
+++ b/src/applications/harbormaster/storage/build/HarbormasterBuildLogChunk.php
@@ -1,61 +1,65 @@
<?php
final class HarbormasterBuildLogChunk
extends HarbormasterDAO {
protected $logID;
protected $encoding;
+ protected $headOffset;
+ protected $tailOffset;
protected $size;
protected $chunk;
const CHUNK_ENCODING_TEXT = 'text';
const CHUNK_ENCODING_GZIP = 'gzip';
protected function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_BINARY => array(
'chunk' => true,
),
self::CONFIG_COLUMN_SCHEMA => array(
'logID' => 'id',
'encoding' => 'text32',
+ 'headOffset' => 'uint64',
+ 'tailOffset' => 'uint64',
// T6203/NULLABILITY
// Both the type and nullability of this column are crazily wrong.
'size' => 'uint32?',
'chunk' => 'bytes',
),
self::CONFIG_KEY_SCHEMA => array(
- 'key_log' => array(
- 'columns' => array('logID'),
+ 'key_offset' => array(
+ 'columns' => array('logID', 'headOffset', 'tailOffset'),
),
),
) + parent::getConfiguration();
}
public function getChunkDisplayText() {
$data = $this->getChunk();
$encoding = $this->getEncoding();
switch ($encoding) {
case self::CHUNK_ENCODING_TEXT:
// Do nothing, data is already plaintext.
break;
case self::CHUNK_ENCODING_GZIP:
$data = gzinflate($data);
if ($data === false) {
throw new Exception(pht('Unable to inflate log chunk!'));
}
break;
default:
throw new Exception(
pht('Unknown log chunk encoding ("%s")!', $encoding));
}
return $data;
}
}
diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildLogChunkIterator.php b/src/applications/harbormaster/storage/build/HarbormasterBuildLogChunkIterator.php
index 754248cc6..514ad7013 100644
--- a/src/applications/harbormaster/storage/build/HarbormasterBuildLogChunkIterator.php
+++ b/src/applications/harbormaster/storage/build/HarbormasterBuildLogChunkIterator.php
@@ -1,49 +1,59 @@
<?php
final class HarbormasterBuildLogChunkIterator
extends PhutilBufferedIterator {
private $log;
private $cursor;
+ private $asString;
private $min = 0;
private $max = PHP_INT_MAX;
public function __construct(HarbormasterBuildLog $log) {
$this->log = $log;
}
protected function didRewind() {
$this->cursor = $this->min;
}
public function key() {
return $this->current()->getID();
}
public function setRange($min, $max) {
$this->min = (int)$min;
$this->max = (int)$max;
return $this;
}
+ public function setAsString($as_string) {
+ $this->asString = $as_string;
+ return $this;
+ }
+
protected function loadPage() {
if ($this->cursor > $this->max) {
return array();
}
$results = id(new HarbormasterBuildLogChunk())->loadAllWhere(
'logID = %d AND id >= %d AND id <= %d ORDER BY id ASC LIMIT %d',
$this->log->getID(),
$this->cursor,
$this->max,
$this->getPageSize());
if ($results) {
$this->cursor = last($results)->getID() + 1;
}
- return $results;
+ if ($this->asString) {
+ return mpull($results, 'getChunkDisplayText');
+ } else {
+ return $results;
+ }
}
}
diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php b/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php
index 8b47bdfc2..b559a6619 100644
--- a/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php
+++ b/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php
@@ -1,357 +1,415 @@
<?php
-final class HarbormasterBuildTarget extends HarbormasterDAO
- implements PhabricatorPolicyInterface {
+final class HarbormasterBuildTarget
+ extends HarbormasterDAO
+ implements
+ PhabricatorPolicyInterface,
+ PhabricatorDestructibleInterface {
protected $name;
protected $buildPHID;
protected $buildStepPHID;
protected $className;
protected $details;
protected $variables;
protected $targetStatus;
protected $dateStarted;
protected $dateCompleted;
protected $buildGeneration;
const STATUS_PENDING = 'target/pending';
const STATUS_BUILDING = 'target/building';
const STATUS_WAITING = 'target/waiting';
const STATUS_PASSED = 'target/passed';
const STATUS_FAILED = 'target/failed';
const STATUS_ABORTED = 'target/aborted';
private $build = self::ATTACHABLE;
private $buildStep = self::ATTACHABLE;
private $implementation;
public static function getBuildTargetStatusName($status) {
switch ($status) {
case self::STATUS_PENDING:
return pht('Pending');
case self::STATUS_BUILDING:
return pht('Building');
case self::STATUS_WAITING:
return pht('Waiting for Message');
case self::STATUS_PASSED:
return pht('Passed');
case self::STATUS_FAILED:
return pht('Failed');
case self::STATUS_ABORTED:
return pht('Aborted');
default:
return pht('Unknown');
}
}
public static function getBuildTargetStatusIcon($status) {
switch ($status) {
case self::STATUS_PENDING:
return PHUIStatusItemView::ICON_OPEN;
case self::STATUS_BUILDING:
case self::STATUS_WAITING:
return PHUIStatusItemView::ICON_RIGHT;
case self::STATUS_PASSED:
return PHUIStatusItemView::ICON_ACCEPT;
case self::STATUS_FAILED:
return PHUIStatusItemView::ICON_REJECT;
case self::STATUS_ABORTED:
return PHUIStatusItemView::ICON_MINUS;
default:
return PHUIStatusItemView::ICON_QUESTION;
}
}
public static function getBuildTargetStatusColor($status) {
switch ($status) {
case self::STATUS_PENDING:
case self::STATUS_BUILDING:
case self::STATUS_WAITING:
return 'blue';
case self::STATUS_PASSED:
return 'green';
case self::STATUS_FAILED:
case self::STATUS_ABORTED:
return 'red';
default:
return 'bluegrey';
}
}
public static function initializeNewBuildTarget(
HarbormasterBuild $build,
HarbormasterBuildStep $build_step,
array $variables) {
return id(new HarbormasterBuildTarget())
->setName($build_step->getName())
->setBuildPHID($build->getPHID())
->setBuildStepPHID($build_step->getPHID())
->setClassName($build_step->getClassName())
->setDetails($build_step->getDetails())
->setTargetStatus(self::STATUS_PENDING)
->setVariables($variables)
->setBuildGeneration($build->getBuildGeneration());
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'details' => self::SERIALIZATION_JSON,
'variables' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'className' => 'text255',
'targetStatus' => 'text64',
'dateStarted' => 'epoch?',
'dateCompleted' => 'epoch?',
'buildGeneration' => 'uint32',
// T6203/NULLABILITY
// This should not be nullable.
'name' => 'text255?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_build' => array(
'columns' => array('buildPHID', 'buildStepPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
HarbormasterBuildTargetPHIDType::TYPECONST);
}
public function attachBuild(HarbormasterBuild $build) {
$this->build = $build;
return $this;
}
public function getBuild() {
return $this->assertAttached($this->build);
}
public function attachBuildStep(HarbormasterBuildStep $step = null) {
$this->buildStep = $step;
return $this;
}
public function getBuildStep() {
return $this->assertAttached($this->buildStep);
}
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 getVariables() {
return parent::getVariables() + $this->getBuildTargetVariables();
}
public function getVariable($key, $default = null) {
return idx($this->variables, $key, $default);
}
public function setVariable($key, $value) {
$this->variables[$key] = $value;
return $this;
}
public function getImplementation() {
if ($this->implementation === null) {
$obj = HarbormasterBuildStepImplementation::requireImplementation(
$this->className);
$obj->loadSettings($this);
$this->implementation = $obj;
}
return $this->implementation;
}
public function isAutotarget() {
try {
return (bool)$this->getImplementation()->getBuildStepAutotargetPlanKey();
} catch (Exception $e) {
return false;
}
}
public function getName() {
if (strlen($this->name) && !$this->isAutotarget()) {
return $this->name;
}
try {
return $this->getImplementation()->getName();
} catch (Exception $e) {
return $this->getClassName();
}
}
private function getBuildTargetVariables() {
return array(
'target.phid' => $this->getPHID(),
);
}
public function createArtifact(
PhabricatorUser $actor,
$artifact_key,
$artifact_type,
array $artifact_data) {
$impl = HarbormasterArtifact::getArtifactType($artifact_type);
if (!$impl) {
throw new Exception(
pht(
'There is no implementation available for artifacts of type "%s".',
$artifact_type));
}
$impl->validateArtifactData($artifact_data);
$artifact = HarbormasterBuildArtifact::initializeNewBuildArtifact($this)
->setArtifactKey($artifact_key)
->setArtifactType($artifact_type)
->setArtifactData($artifact_data);
$impl = $artifact->getArtifactImplementation();
$impl->willCreateArtifact($actor);
return $artifact->save();
}
public function loadArtifact($artifact_key) {
$indexes = array();
$indexes[] = HarbormasterBuildArtifact::getArtifactIndex(
$this,
$artifact_key);
$artifact = id(new HarbormasterBuildArtifactQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withArtifactIndexes($indexes)
->executeOne();
if ($artifact === null) {
throw new Exception(
pht(
'Artifact "%s" not found!',
$artifact_key));
}
return $artifact;
}
public function newLog($log_source, $log_type) {
$log_source = id(new PhutilUTF8StringTruncator())
->setMaximumBytes(250)
->truncateString($log_source);
$log = HarbormasterBuildLog::initializeNewBuildLog($this)
->setLogSource($log_source)
->setLogType($log_type)
->openBuildLog();
return $log;
}
public function getFieldValue($key) {
$field_list = PhabricatorCustomField::getObjectFields(
$this->getBuildStep(),
PhabricatorCustomField::ROLE_VIEW);
$fields = $field_list->getFields();
$full_key = "std:harbormaster:core:{$key}";
$field = idx($fields, $full_key);
if (!$field) {
throw new Exception(
pht(
'Unknown build step field "%s"!',
$key));
}
$field = clone $field;
$field->setValueFromStorage($this->getDetail($key));
return $field->getBuildTargetFieldValue();
}
/* -( Status )------------------------------------------------------------- */
public function isComplete() {
switch ($this->getTargetStatus()) {
case self::STATUS_PASSED:
case self::STATUS_FAILED:
case self::STATUS_ABORTED:
return true;
}
return false;
}
public function isFailed() {
switch ($this->getTargetStatus()) {
case self::STATUS_FAILED:
case self::STATUS_ABORTED:
return true;
}
return false;
}
public function isWaiting() {
switch ($this->getTargetStatus()) {
case self::STATUS_WAITING:
return true;
}
return false;
}
public function isUnderway() {
switch ($this->getTargetStatus()) {
case self::STATUS_PENDING:
case self::STATUS_BUILDING:
return true;
}
return false;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
return $this->getBuild()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getBuild()->hasAutomaticCapability(
$capability,
$viewer);
}
public function describeAutomaticCapability($capability) {
return pht('Users must be able to see a build to view its build targets.');
}
+
+/* -( PhabricatorDestructibleInterface )----------------------------------- */
+
+
+ public function destroyObjectPermanently(
+ PhabricatorDestructionEngine $engine) {
+ $viewer = $engine->getViewer();
+
+ $this->openTransaction();
+
+ $lint_message = new HarbormasterBuildLintMessage();
+ $conn = $lint_message->establishConnection('w');
+ queryfx(
+ $conn,
+ 'DELETE FROM %T WHERE buildTargetPHID = %s',
+ $lint_message->getTableName(),
+ $this->getPHID());
+
+ $unit_message = new HarbormasterBuildUnitMessage();
+ $conn = $unit_message->establishConnection('w');
+ queryfx(
+ $conn,
+ 'DELETE FROM %T WHERE buildTargetPHID = %s',
+ $unit_message->getTableName(),
+ $this->getPHID());
+
+ $logs = id(new HarbormasterBuildLogQuery())
+ ->setViewer($viewer)
+ ->withBuildTargetPHIDs(array($this->getPHID()))
+ ->execute();
+ foreach ($logs as $log) {
+ $engine->destroyObject($log);
+ }
+
+ $artifacts = id(new HarbormasterBuildArtifactQuery())
+ ->setViewer($viewer)
+ ->withBuildTargetPHIDs(array($this->getPHID()))
+ ->execute();
+ foreach ($artifacts as $artifact) {
+ $engine->destroyObject($artifact);
+ }
+
+ $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/view/HarbormasterBuildLogView.php b/src/applications/harbormaster/view/HarbormasterBuildLogView.php
new file mode 100644
index 000000000..5eb32a84e
--- /dev/null
+++ b/src/applications/harbormaster/view/HarbormasterBuildLogView.php
@@ -0,0 +1,101 @@
+<?php
+
+final class HarbormasterBuildLogView extends AphrontView {
+
+ private $log;
+ private $highlightedLineRange;
+ private $enableHighlighter;
+
+ public function setBuildLog(HarbormasterBuildLog $log) {
+ $this->log = $log;
+ return $this;
+ }
+
+ public function getBuildLog() {
+ return $this->log;
+ }
+
+ public function setHighlightedLineRange($range) {
+ $this->highlightedLineRange = $range;
+ return $this;
+ }
+
+ public function getHighlightedLineRange() {
+ return $this->highlightedLineRange;
+ }
+
+ public function setEnableHighlighter($enable) {
+ $this->enableHighlighter = $enable;
+ return $this;
+ }
+
+ public function render() {
+ $viewer = $this->getViewer();
+ $log = $this->getBuildLog();
+ $id = $log->getID();
+
+ $header = id(new PHUIHeaderView())
+ ->setViewer($viewer)
+ ->setHeader(pht('Build Log %d', $id));
+
+ $download_uri = "/harbormaster/log/download/{$id}/";
+
+ $can_download = (bool)$log->getFilePHID();
+
+ $download_button = id(new PHUIButtonView())
+ ->setTag('a')
+ ->setHref($download_uri)
+ ->setIcon('fa-download')
+ ->setDisabled(!$can_download)
+ ->setWorkflow(!$can_download)
+ ->setText(pht('Download Log'));
+
+ $header->addActionLink($download_button);
+
+ $box_view = id(new PHUIObjectBoxView())
+ ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+ ->setHeader($header);
+
+ if ($this->enableHighlighter) {
+ Javelin::initBehavior('phabricator-line-linker');
+ }
+
+ $has_linemap = $log->getLineMap();
+ if ($has_linemap) {
+ $content_id = celerity_generate_unique_node_id();
+ $content_div = javelin_tag(
+ 'div',
+ array(
+ 'id' => $content_id,
+ 'class' => 'harbormaster-log-view-loading',
+ ),
+ pht('Loading...'));
+
+ require_celerity_resource('harbormaster-css');
+
+ Javelin::initBehavior(
+ 'harbormaster-log',
+ array(
+ 'contentNodeID' => $content_id,
+ 'initialURI' => $log->getRenderURI($this->getHighlightedLineRange()),
+ 'renderURI' => $log->getRenderURI(null),
+ ));
+
+ $box_view->appendChild($content_div);
+ } else {
+ $box_view->setFormErrors(
+ array(
+ pht(
+ 'This older log is missing required rendering data. To rebuild '.
+ 'rendering data, run: %s',
+ phutil_tag(
+ 'tt',
+ array(),
+ '$ bin/harbormaster rebuild-log --force --id '.$log->getID())),
+ ));
+ }
+
+ return $box_view;
+ }
+
+}
diff --git a/src/applications/harbormaster/worker/HarbormasterBuildWorker.php b/src/applications/harbormaster/worker/HarbormasterBuildWorker.php
index 67eb6b3e7..9ce9d50f1 100644
--- a/src/applications/harbormaster/worker/HarbormasterBuildWorker.php
+++ b/src/applications/harbormaster/worker/HarbormasterBuildWorker.php
@@ -1,45 +1,71 @@
<?php
/**
* Start a build.
*/
final class HarbormasterBuildWorker extends HarbormasterWorker {
public function renderForDisplay(PhabricatorUser $viewer) {
try {
$build = $this->loadBuild();
} catch (Exception $ex) {
return null;
}
return $viewer->renderHandle($build->getPHID());
}
protected function doWork() {
$viewer = $this->getViewer();
- $build = $this->loadBuild();
- id(new HarbormasterBuildEngine())
- ->setViewer($viewer)
- ->setBuild($build)
- ->continueBuild();
+ $engine = id(new HarbormasterBuildEngine())
+ ->setViewer($viewer);
+
+ $data = $this->getTaskData();
+ $build_id = idx($data, 'buildID');
+
+ if ($build_id) {
+ $build = $this->loadBuild();
+ $engine->setBuild($build);
+ $engine->continueBuild();
+ } else {
+ $buildable = $this->loadBuildable();
+ $engine->updateBuildable($buildable);
+ }
}
private function loadBuild() {
$data = $this->getTaskData();
$id = idx($data, 'buildID');
$viewer = $this->getViewer();
$build = id(new HarbormasterBuildQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if (!$build) {
throw new PhabricatorWorkerPermanentFailureException(
pht('Invalid build ID "%s".', $id));
}
return $build;
}
+ private function loadBuildable() {
+ $data = $this->getTaskData();
+ $phid = idx($data, 'buildablePHID');
+
+ $viewer = $this->getViewer();
+ $buildable = id(new HarbormasterBuildableQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(array($phid))
+ ->executeOne();
+ if (!$buildable) {
+ throw new PhabricatorWorkerPermanentFailureException(
+ pht('Invalid buildable PHID "%s".', $phid));
+ }
+
+ return $buildable;
+ }
+
}
diff --git a/src/applications/harbormaster/worker/HarbormasterLogWorker.php b/src/applications/harbormaster/worker/HarbormasterLogWorker.php
new file mode 100644
index 000000000..d9d7511c2
--- /dev/null
+++ b/src/applications/harbormaster/worker/HarbormasterLogWorker.php
@@ -0,0 +1,105 @@
+<?php
+
+final class HarbormasterLogWorker extends HarbormasterWorker {
+
+ protected function doWork() {
+ $viewer = $this->getViewer();
+
+ $data = $this->getTaskData();
+ $log_phid = idx($data, 'logPHID');
+
+ $log = id(new HarbormasterBuildLogQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(array($log_phid))
+ ->executeOne();
+ if (!$log) {
+ throw new PhabricatorWorkerPermanentFailureException(
+ pht(
+ 'Invalid build log PHID "%s".',
+ $log_phid));
+ }
+
+ $lock = $log->getLock();
+
+ try {
+ $lock->lock();
+ } catch (PhutilLockException $ex) {
+ throw new PhabricatorWorkerYieldException(15);
+ }
+
+ $caught = null;
+ try {
+ $log->reload();
+
+ if ($log->getLive()) {
+ throw new PhabricatorWorkerPermanentFailureException(
+ pht(
+ 'Log "%s" is still live. Logs can not be finalized until '.
+ 'they have closed.',
+ $log_phid));
+ }
+
+ $this->finalizeBuildLog($log);
+ } catch (Exception $ex) {
+ $caught = $ex;
+ }
+
+ $lock->unlock();
+
+ if ($caught) {
+ throw $caught;
+ }
+ }
+
+ private function finalizeBuildLog(HarbormasterBuildLog $log) {
+ $viewer = $this->getViewer();
+
+ $data = $this->getTaskData();
+ $is_force = idx($data, 'force');
+
+ if (!$log->getByteLength() || !$log->getLineMap() || $is_force) {
+ $iterator = $log->newDataIterator();
+
+ $log
+ ->setByteLength(0)
+ ->setLineMap(array());
+
+ foreach ($iterator as $block) {
+ $log->updateLineMap($block);
+ }
+
+ $log->save();
+ }
+
+ $format_text = HarbormasterBuildLogChunk::CHUNK_ENCODING_TEXT;
+ if (($log->getChunkFormat() === $format_text) || $is_force) {
+ if ($log->canCompressLog()) {
+ $log->compressLog();
+ }
+ }
+
+ if ($is_force) {
+ $log->destroyFile();
+ }
+
+ if (!$log->getFilePHID()) {
+ $iterator = $log->newDataIterator();
+
+ $source = id(new PhabricatorIteratorFileUploadSource())
+ ->setName('harbormaster-log-'.$log->getID().'.log')
+ ->setViewPolicy(PhabricatorPolicies::POLICY_NOONE)
+ ->setMIMEType('application/octet-stream')
+ ->setIterator($iterator);
+
+ $file = $source->uploadFile();
+
+ $file->attachToObject($log->getPHID());
+
+ $log
+ ->setFilePHID($file->getPHID())
+ ->save();
+ }
+
+ }
+
+}
diff --git a/src/applications/herald/action/HeraldCallWebhookAction.php b/src/applications/herald/action/HeraldCallWebhookAction.php
new file mode 100644
index 000000000..953958e5c
--- /dev/null
+++ b/src/applications/herald/action/HeraldCallWebhookAction.php
@@ -0,0 +1,66 @@
+<?php
+
+final class HeraldCallWebhookAction extends HeraldAction {
+
+ const ACTIONCONST = 'webhook';
+ const DO_WEBHOOK = 'do.call-webhook';
+
+ public function getHeraldActionName() {
+ return pht('Call webhooks');
+ }
+
+ public function getActionGroupKey() {
+ return HeraldUtilityActionGroup::ACTIONGROUPKEY;
+ }
+
+ public function supportsObject($object) {
+ if (!$this->getAdapter()->supportsWebhooks()) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public function supportsRuleType($rule_type) {
+ return ($rule_type !== HeraldRuleTypeConfig::RULE_TYPE_PERSONAL);
+ }
+
+ public function applyEffect($object, HeraldEffect $effect) {
+ $adapter = $this->getAdapter();
+ $rule = $effect->getRule();
+ $target = $effect->getTarget();
+
+ foreach ($target as $webhook_phid) {
+ $adapter->queueWebhook($webhook_phid, $rule->getPHID());
+ }
+
+ $this->logEffect(self::DO_WEBHOOK, $target);
+ }
+
+ public function getHeraldActionStandardType() {
+ return self::STANDARD_PHID_LIST;
+ }
+
+ protected function getActionEffectMap() {
+ return array(
+ self::DO_WEBHOOK => array(
+ 'icon' => 'fa-cloud-upload',
+ 'color' => 'green',
+ 'name' => pht('Called Webhooks'),
+ ),
+ );
+ }
+
+ public function renderActionDescription($value) {
+ return pht('Call webhooks: %s.', $this->renderHandleList($value));
+ }
+
+ protected function renderActionEffectDescription($type, $data) {
+ return pht('Called webhooks: %s.', $this->renderHandleList($data));
+ }
+
+ protected function getDatasource() {
+ return new HeraldWebhookDatasource();
+ }
+
+}
diff --git a/src/applications/herald/adapter/HeraldAdapter.php b/src/applications/herald/adapter/HeraldAdapter.php
index c9022d2c4..502d0fc69 100644
--- a/src/applications/herald/adapter/HeraldAdapter.php
+++ b/src/applications/herald/adapter/HeraldAdapter.php
@@ -1,1195 +1,1244 @@
<?php
abstract class HeraldAdapter extends Phobject {
const CONDITION_CONTAINS = 'contains';
const CONDITION_NOT_CONTAINS = '!contains';
const CONDITION_GREATER = 'greater'; //c4s custo
const CONDITION_LESS = 'less'; //c4s custo
const CONDITION_IS = 'is';
const CONDITION_IS_NOT = '!is';
const CONDITION_IS_ANY = 'isany';
const CONDITION_IS_NOT_ANY = '!isany';
const CONDITION_INCLUDE_ALL = 'all';
const CONDITION_INCLUDE_ANY = 'any';
const CONDITION_INCLUDE_NONE = 'none';
const CONDITION_IS_ME = 'me';
const CONDITION_IS_NOT_ME = '!me';
const CONDITION_REGEXP = 'regexp';
const CONDITION_NOT_REGEXP = '!regexp';
const CONDITION_RULE = 'conditions';
const CONDITION_NOT_RULE = '!conditions';
const CONDITION_EXISTS = 'exists';
const CONDITION_NOT_EXISTS = '!exists';
const CONDITION_UNCONDITIONALLY = 'unconditionally';
const CONDITION_NEVER = 'never';
const CONDITION_REGEXP_PAIR = 'regexp-pair';
const CONDITION_HAS_BIT = 'bit';
const CONDITION_NOT_BIT = '!bit';
const CONDITION_IS_TRUE = 'true';
const CONDITION_IS_FALSE = 'false';
private $contentSource;
private $isNewObject;
private $applicationEmail;
private $appliedTransactions = array();
private $queuedTransactions = array();
private $emailPHIDs = array();
private $forcedEmailPHIDs = array();
private $fieldMap;
private $actionMap;
private $edgeCache = array();
private $forbiddenActions = array();
private $viewer;
+ private $mustEncryptReasons = array();
+ private $actingAsPHID;
+ private $webhookMap = array();
public function getEmailPHIDs() {
return array_values($this->emailPHIDs);
}
public function getForcedEmailPHIDs() {
return array_values($this->forcedEmailPHIDs);
}
+ final public function setActingAsPHID($acting_as_phid) {
+ $this->actingAsPHID = $acting_as_phid;
+ return $this;
+ }
+
+ final public function getActingAsPHID() {
+ return $this->actingAsPHID;
+ }
+
public function addEmailPHID($phid, $force) {
$this->emailPHIDs[$phid] = $phid;
if ($force) {
$this->forcedEmailPHIDs[$phid] = $phid;
}
return $this;
}
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
// See PHI276. Normally, Herald runs without regard for policy checks.
// However, we use a real viewer during test console runs: this makes
// intracluster calls to Diffusion APIs work even if web nodes don't
// have privileged credentials.
if ($this->viewer) {
return $this->viewer;
}
return PhabricatorUser::getOmnipotentUser();
}
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source;
return $this;
}
public function getContentSource() {
return $this->contentSource;
}
public function getIsNewObject() {
if (is_bool($this->isNewObject)) {
return $this->isNewObject;
}
throw new Exception(
pht(
'You must %s to a boolean first!',
'setIsNewObject()'));
}
public function setIsNewObject($new) {
$this->isNewObject = (bool)$new;
return $this;
}
public function supportsApplicationEmail() {
return false;
}
public function setApplicationEmail(
PhabricatorMetaMTAApplicationEmail $email) {
$this->applicationEmail = $email;
return $this;
}
public function getApplicationEmail() {
return $this->applicationEmail;
}
public function getPHID() {
return $this->getObject()->getPHID();
}
abstract public function getHeraldName();
public function getHeraldField($field_key) {
return $this->requireFieldImplementation($field_key)
->getHeraldFieldValue($this->getObject());
}
public function applyHeraldEffects(array $effects) {
assert_instances_of($effects, 'HeraldEffect');
$result = array();
foreach ($effects as $effect) {
$result[] = $this->applyStandardEffect($effect);
}
return $result;
}
public function isAvailableToUser(PhabricatorUser $viewer) {
$applications = id(new PhabricatorApplicationQuery())
->setViewer($viewer)
->withInstalled(true)
->withClasses(array($this->getAdapterApplicationClass()))
->execute();
return !empty($applications);
}
/**
* Set the list of transactions which just took effect.
*
* These transactions are set by @{class:PhabricatorApplicationEditor}
* automatically, before it invokes Herald.
*
* @param list<PhabricatorApplicationTransaction> List of transactions.
* @return this
*/
final public function setAppliedTransactions(array $xactions) {
assert_instances_of($xactions, 'PhabricatorApplicationTransaction');
$this->appliedTransactions = $xactions;
return $this;
}
/**
* Get a list of transactions which just took effect.
*
* When an object is edited normally, transactions are applied and then
* Herald executes. You can call this method to examine the transactions
* if you want to react to them.
*
* @return list<PhabricatorApplicationTransaction> List of transactions.
*/
final public function getAppliedTransactions() {
return $this->appliedTransactions;
}
public function queueTransaction($transaction) {
$this->queuedTransactions[] = $transaction;
}
public function getQueuedTransactions() {
return $this->queuedTransactions;
}
public function newTransaction() {
$object = $this->newObject();
if (!($object instanceof PhabricatorApplicationTransactionInterface)) {
throw new Exception(
pht(
'Unable to build a new transaction for adapter object; it does '.
'not implement "%s".',
'PhabricatorApplicationTransactionInterface'));
}
return $object->getApplicationTransactionTemplate();
}
/**
* NOTE: You generally should not override this; it exists to support legacy
* adapters which had hard-coded content types.
*/
public function getAdapterContentType() {
return get_class($this);
}
abstract public function getAdapterContentName();
abstract public function getAdapterContentDescription();
abstract public function getAdapterApplicationClass();
abstract public function getObject();
/**
* Return a new characteristic object for this adapter.
*
* The adapter will use this object to test for interfaces, generate
* transactions, and interact with custom fields.
*
* Adapters must return an object from this method to enable custom
* field rules and various implicit actions.
*
* Normally, you'll return an empty version of the adapted object:
*
* return new ApplicationObject();
*
* @return null|object Template object.
*/
protected function newObject() {
return null;
}
public function supportsRuleType($rule_type) {
return false;
}
public function canTriggerOnObject($object) {
return false;
}
public function isTestAdapterForObject($object) {
return false;
}
public function canCreateTestAdapterForObject($object) {
return $this->isTestAdapterForObject($object);
}
public function newTestAdapter(PhabricatorUser $viewer, $object) {
return id(clone $this)
->setObject($object);
}
public function getAdapterTestDescription() {
return null;
}
public function explainValidTriggerObjects() {
return pht('This adapter can not trigger on objects.');
}
public function getTriggerObjectPHIDs() {
return array($this->getPHID());
}
public function getAdapterSortKey() {
return sprintf(
'%08d%s',
$this->getAdapterSortOrder(),
$this->getAdapterContentName());
}
public function getAdapterSortOrder() {
return 1000;
}
/* -( Fields )------------------------------------------------------------- */
private function getFieldImplementationMap() {
if ($this->fieldMap === null) {
// We can't use PhutilClassMapQuery here because field expansion
// depends on the adapter and object.
$object = $this->getObject();
$map = array();
$all = HeraldField::getAllFields();
foreach ($all as $key => $field) {
$field = id(clone $field)->setAdapter($this);
if (!$field->supportsObject($object)) {
continue;
}
$subfields = $field->getFieldsForObject($object);
foreach ($subfields as $subkey => $subfield) {
if (isset($map[$subkey])) {
throw new Exception(
pht(
'Two HeraldFields (of classes "%s" and "%s") have the same '.
'field key ("%s") after expansion for an object of class '.
'"%s" inside adapter "%s". Each field must have a unique '.
'field key.',
get_class($subfield),
get_class($map[$subkey]),
$subkey,
get_class($object),
get_class($this)));
}
$subfield = id(clone $subfield)->setAdapter($this);
$map[$subkey] = $subfield;
}
}
$this->fieldMap = $map;
}
return $this->fieldMap;
}
private function getFieldImplementation($key) {
return idx($this->getFieldImplementationMap(), $key);
}
public function getFields() {
return array_keys($this->getFieldImplementationMap());
}
public function getFieldNameMap() {
return mpull($this->getFieldImplementationMap(), 'getHeraldFieldName');
}
public function getFieldGroupKey($field_key) {
$field = $this->getFieldImplementation($field_key);
if (!$field) {
return null;
}
return $field->getFieldGroupKey();
}
/* -( Conditions )--------------------------------------------------------- */
public function getConditionNameMap() {
return array(
self::CONDITION_CONTAINS => pht('contains'),
self::CONDITION_NOT_CONTAINS => pht('does not contain'),
self::CONDITION_GREATER => pht('is greater than'), //c4s custo
self::CONDITION_LESS => pht('is less than'), //c4s custo
self::CONDITION_IS => pht('is'),
self::CONDITION_IS_NOT => pht('is not'),
self::CONDITION_IS_ANY => pht('is any of'),
self::CONDITION_IS_TRUE => pht('is true'),
self::CONDITION_IS_FALSE => pht('is false'),
self::CONDITION_IS_NOT_ANY => pht('is not any of'),
self::CONDITION_INCLUDE_ALL => pht('include all of'),
self::CONDITION_INCLUDE_ANY => pht('include any of'),
self::CONDITION_INCLUDE_NONE => pht('do not include'),
self::CONDITION_IS_ME => pht('is myself'),
self::CONDITION_IS_NOT_ME => pht('is not myself'),
self::CONDITION_REGEXP => pht('matches regexp'),
self::CONDITION_NOT_REGEXP => pht('does not match regexp'),
self::CONDITION_RULE => pht('matches:'),
self::CONDITION_NOT_RULE => pht('does not match:'),
self::CONDITION_EXISTS => pht('exists'),
self::CONDITION_NOT_EXISTS => pht('does not exist'),
self::CONDITION_UNCONDITIONALLY => '', // don't show anything!
self::CONDITION_NEVER => '', // don't show anything!
self::CONDITION_REGEXP_PAIR => pht('matches regexp pair'),
self::CONDITION_HAS_BIT => pht('has bit'),
self::CONDITION_NOT_BIT => pht('lacks bit'),
);
}
public function getConditionsForField($field) {
return $this->requireFieldImplementation($field)
->getHeraldFieldConditions();
}
private function requireFieldImplementation($field_key) {
$field = $this->getFieldImplementation($field_key);
if (!$field) {
throw new Exception(
pht(
'No field with key "%s" is available to Herald adapter "%s".',
$field_key,
get_class($this)));
}
return $field;
}
public function doesConditionMatch(
HeraldEngine $engine,
HeraldRule $rule,
HeraldCondition $condition,
$field_value) {
$condition_type = $condition->getFieldCondition();
$condition_value = $condition->getValue();
switch ($condition_type) {
case self::CONDITION_CONTAINS:
case self::CONDITION_NOT_CONTAINS:
// "Contains and "does not contain" can take an array of strings, as in
// "Any changed filename" for diffs.
$result_if_match = ($condition_type == self::CONDITION_CONTAINS);
foreach ((array)$field_value as $value) {
if (stripos($value, $condition_value) !== false) {
return $result_if_match;
}
}
return !$result_if_match;
case self::CONDITION_GREATER: //c4s custo
return ($field_value > $condition_value);
case self::CONDITION_LESS: //c4s custo
return ($field_value < $condition_value);
case self::CONDITION_IS:
return ($field_value == $condition_value);
case self::CONDITION_IS_NOT:
return ($field_value != $condition_value);
case self::CONDITION_IS_ME:
return ($field_value == $rule->getAuthorPHID());
case self::CONDITION_IS_NOT_ME:
return ($field_value != $rule->getAuthorPHID());
case self::CONDITION_IS_ANY:
if (!is_array($condition_value)) {
throw new HeraldInvalidConditionException(
pht('Expected condition value to be an array.'));
}
$condition_value = array_fuse($condition_value);
return isset($condition_value[$field_value]);
case self::CONDITION_IS_NOT_ANY:
if (!is_array($condition_value)) {
throw new HeraldInvalidConditionException(
pht('Expected condition value to be an array.'));
}
$condition_value = array_fuse($condition_value);
return !isset($condition_value[$field_value]);
case self::CONDITION_INCLUDE_ALL:
if (!is_array($field_value)) {
throw new HeraldInvalidConditionException(
pht('Object produced non-array value!'));
}
if (!is_array($condition_value)) {
throw new HeraldInvalidConditionException(
pht('Expected condition value to be an array.'));
}
$have = array_select_keys(array_fuse($field_value), $condition_value);
return (count($have) == count($condition_value));
case self::CONDITION_INCLUDE_ANY:
return (bool)array_select_keys(
array_fuse($field_value),
$condition_value);
case self::CONDITION_INCLUDE_NONE:
return !array_select_keys(
array_fuse($field_value),
$condition_value);
case self::CONDITION_EXISTS:
case self::CONDITION_IS_TRUE:
return (bool)$field_value;
case self::CONDITION_NOT_EXISTS:
case self::CONDITION_IS_FALSE:
return !$field_value;
case self::CONDITION_UNCONDITIONALLY:
return (bool)$field_value;
case self::CONDITION_NEVER:
return false;
case self::CONDITION_REGEXP:
case self::CONDITION_NOT_REGEXP:
$result_if_match = ($condition_type == self::CONDITION_REGEXP);
foreach ((array)$field_value as $value) {
// We add the 'S' flag because we use the regexp multiple times.
// It shouldn't cause any troubles if the flag is already there
// - /.*/S is evaluated same as /.*/SS.
$result = @preg_match($condition_value.'S', $value);
if ($result === false) {
throw new HeraldInvalidConditionException(
- pht('Regular expression is not valid!'));
+ pht(
+ 'Regular expression "%s" in Herald rule "%s" is not valid, '.
+ 'or exceeded backtracking or recursion limits while '.
+ 'executing. Verify the expression and correct it or rewrite '.
+ 'it with less backtracking.',
+ $condition_value,
+ $rule->getMonogram()));
}
if ($result) {
return $result_if_match;
}
}
return !$result_if_match;
case self::CONDITION_REGEXP_PAIR:
// Match a JSON-encoded pair of regular expressions against a
// dictionary. The first regexp must match the dictionary key, and the
// second regexp must match the dictionary value. If any key/value pair
// in the dictionary matches both regexps, the condition is satisfied.
$regexp_pair = null;
try {
$regexp_pair = phutil_json_decode($condition_value);
} catch (PhutilJSONParserException $ex) {
throw new HeraldInvalidConditionException(
pht('Regular expression pair is not valid JSON!'));
}
if (count($regexp_pair) != 2) {
throw new HeraldInvalidConditionException(
pht('Regular expression pair is not a pair!'));
}
$key_regexp = array_shift($regexp_pair);
$value_regexp = array_shift($regexp_pair);
foreach ((array)$field_value as $key => $value) {
$key_matches = @preg_match($key_regexp, $key);
if ($key_matches === false) {
throw new HeraldInvalidConditionException(
pht('First regular expression is invalid!'));
}
if ($key_matches) {
$value_matches = @preg_match($value_regexp, $value);
if ($value_matches === false) {
throw new HeraldInvalidConditionException(
pht('Second regular expression is invalid!'));
}
if ($value_matches) {
return true;
}
}
}
return false;
case self::CONDITION_RULE:
case self::CONDITION_NOT_RULE:
$rule = $engine->getRule($condition_value);
if (!$rule) {
throw new HeraldInvalidConditionException(
pht('Condition references a rule which does not exist!'));
}
$is_not = ($condition_type == self::CONDITION_NOT_RULE);
$result = $engine->doesRuleMatch($rule, $this);
if ($is_not) {
$result = !$result;
}
return $result;
case self::CONDITION_HAS_BIT:
return (($condition_value & $field_value) === (int)$condition_value);
case self::CONDITION_NOT_BIT:
return (($condition_value & $field_value) !== (int)$condition_value);
default:
throw new HeraldInvalidConditionException(
pht("Unknown condition '%s'.", $condition_type));
}
}
public function willSaveCondition(HeraldCondition $condition) {
$condition_type = $condition->getFieldCondition();
$condition_value = $condition->getValue();
switch ($condition_type) {
case self::CONDITION_REGEXP:
case self::CONDITION_NOT_REGEXP:
$ok = @preg_match($condition_value, '');
if ($ok === false) {
throw new HeraldInvalidConditionException(
pht(
'The regular expression "%s" is not valid. Regular expressions '.
'must have enclosing characters (e.g. "@/path/to/file@", not '.
'"/path/to/file") and be syntactically correct.',
$condition_value));
}
break;
case self::CONDITION_REGEXP_PAIR:
$json = null;
try {
$json = phutil_json_decode($condition_value);
} catch (PhutilJSONParserException $ex) {
throw new HeraldInvalidConditionException(
pht(
'The regular expression pair "%s" is not valid JSON. Enter a '.
'valid JSON array with two elements.',
$condition_value));
}
if (count($json) != 2) {
throw new HeraldInvalidConditionException(
pht(
'The regular expression pair "%s" must have exactly two '.
'elements.',
$condition_value));
}
$key_regexp = array_shift($json);
$val_regexp = array_shift($json);
$key_ok = @preg_match($key_regexp, '');
if ($key_ok === false) {
throw new HeraldInvalidConditionException(
pht(
'The first regexp in the regexp pair, "%s", is not a valid '.
'regexp.',
$key_regexp));
}
$val_ok = @preg_match($val_regexp, '');
if ($val_ok === false) {
throw new HeraldInvalidConditionException(
pht(
'The second regexp in the regexp pair, "%s", is not a valid '.
'regexp.',
$val_regexp));
}
break;
case self::CONDITION_CONTAINS:
case self::CONDITION_NOT_CONTAINS:
case self::CONDITION_GREATER: //c4s custo
case self::CONDITION_LESS: //c4s custo
case self::CONDITION_IS:
case self::CONDITION_IS_NOT:
case self::CONDITION_IS_ANY:
case self::CONDITION_IS_NOT_ANY:
case self::CONDITION_INCLUDE_ALL:
case self::CONDITION_INCLUDE_ANY:
case self::CONDITION_INCLUDE_NONE:
case self::CONDITION_IS_ME:
case self::CONDITION_IS_NOT_ME:
case self::CONDITION_RULE:
case self::CONDITION_NOT_RULE:
case self::CONDITION_EXISTS:
case self::CONDITION_NOT_EXISTS:
case self::CONDITION_UNCONDITIONALLY:
case self::CONDITION_NEVER:
case self::CONDITION_HAS_BIT:
case self::CONDITION_NOT_BIT:
case self::CONDITION_IS_TRUE:
case self::CONDITION_IS_FALSE:
// No explicit validation for these types, although there probably
// should be in some cases.
break;
default:
throw new HeraldInvalidConditionException(
pht(
'Unknown condition "%s"!',
$condition_type));
}
}
/* -( Actions )------------------------------------------------------------ */
private function getActionImplementationMap() {
if ($this->actionMap === null) {
// We can't use PhutilClassMapQuery here because action expansion
// depends on the adapter and object.
$object = $this->getObject();
$map = array();
$all = HeraldAction::getAllActions();
foreach ($all as $key => $action) {
$action = id(clone $action)->setAdapter($this);
if (!$action->supportsObject($object)) {
continue;
}
$subactions = $action->getActionsForObject($object);
foreach ($subactions as $subkey => $subaction) {
if (isset($map[$subkey])) {
throw new Exception(
pht(
'Two HeraldActions (of classes "%s" and "%s") have the same '.
'action key ("%s") after expansion for an object of class '.
'"%s" inside adapter "%s". Each action must have a unique '.
'action key.',
get_class($subaction),
get_class($map[$subkey]),
$subkey,
get_class($object),
get_class($this)));
}
$subaction = id(clone $subaction)->setAdapter($this);
$map[$subkey] = $subaction;
}
}
$this->actionMap = $map;
}
return $this->actionMap;
}
private function requireActionImplementation($action_key) {
$action = $this->getActionImplementation($action_key);
if (!$action) {
throw new Exception(
pht(
'No action with key "%s" is available to Herald adapter "%s".',
$action_key,
get_class($this)));
}
return $action;
}
private function getActionsForRuleType($rule_type) {
$actions = $this->getActionImplementationMap();
foreach ($actions as $key => $action) {
if (!$action->supportsRuleType($rule_type)) {
unset($actions[$key]);
}
}
return $actions;
}
public function getActionImplementation($key) {
return idx($this->getActionImplementationMap(), $key);
}
public function getActionKeys() {
return array_keys($this->getActionImplementationMap());
}
public function getActionGroupKey($action_key) {
$action = $this->getActionImplementation($action_key);
if (!$action) {
return null;
}
return $action->getActionGroupKey();
}
public function getActions($rule_type) {
$actions = array();
foreach ($this->getActionsForRuleType($rule_type) as $key => $action) {
$actions[] = $key;
}
return $actions;
}
public function getActionNameMap($rule_type) {
$map = array();
foreach ($this->getActionsForRuleType($rule_type) as $key => $action) {
$map[$key] = $action->getHeraldActionName();
}
return $map;
}
public function willSaveAction(
HeraldRule $rule,
HeraldActionRecord $action) {
$impl = $this->requireActionImplementation($action->getAction());
$target = $action->getTarget();
$target = $impl->willSaveActionValue($target);
$action->setTarget($target);
}
/* -( Values )------------------------------------------------------------- */
public function getValueTypeForFieldAndCondition($field, $condition) {
return $this->requireFieldImplementation($field)
->getHeraldFieldValueType($condition);
}
public function getValueTypeForAction($action, $rule_type) {
$impl = $this->requireActionImplementation($action);
return $impl->getHeraldActionValueType();
}
private function buildTokenizerFieldValue(
PhabricatorTypeaheadDatasource $datasource) {
$key = 'action.'.get_class($datasource);
return id(new HeraldTokenizerFieldValue())
->setKey($key)
->setDatasource($datasource);
}
/* -( Repetition )--------------------------------------------------------- */
public function getRepetitionOptions() {
$options = array();
$options[] = HeraldRule::REPEAT_EVERY;
// Some rules, like pre-commit rules, only ever fire once. It doesn't
// make sense to use state-based repetition policies like "only the first
// time" for these rules.
if (!$this->isSingleEventAdapter()) {
$options[] = HeraldRule::REPEAT_FIRST;
$options[] = HeraldRule::REPEAT_CHANGE;
}
return $options;
}
protected function initializeNewAdapter() {
$this->setObject($this->newObject());
return $this;
}
/**
* Does this adapter's event fire only once?
*
* Single use adapters (like pre-commit and diff adapters) only fire once,
* so fields like "Is new object" don't make sense to apply to their content.
*
* @return bool
*/
public function isSingleEventAdapter() {
return false;
}
public static function getAllAdapters() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getAdapterContentType')
->setSortMethod('getAdapterSortKey')
->execute();
}
public static function getAdapterForContentType($content_type) {
$adapters = self::getAllAdapters();
foreach ($adapters as $adapter) {
if ($adapter->getAdapterContentType() == $content_type) {
$adapter = id(clone $adapter);
$adapter->initializeNewAdapter();
return $adapter;
}
}
throw new Exception(
pht(
'No adapter exists for Herald content type "%s".',
$content_type));
}
public static function getEnabledAdapterMap(PhabricatorUser $viewer) {
$map = array();
$adapters = self::getAllAdapters();
foreach ($adapters as $adapter) {
if (!$adapter->isAvailableToUser($viewer)) {
continue;
}
$type = $adapter->getAdapterContentType();
$name = $adapter->getAdapterContentName();
$map[$type] = $name;
}
return $map;
}
public function getEditorValueForCondition(
PhabricatorUser $viewer,
HeraldCondition $condition) {
$field = $this->requireFieldImplementation($condition->getFieldName());
return $field->getEditorValue(
$viewer,
$condition->getFieldCondition(),
$condition->getValue());
}
public function getEditorValueForAction(
PhabricatorUser $viewer,
HeraldActionRecord $action_record) {
$action = $this->requireActionImplementation($action_record->getAction());
return $action->getEditorValue(
$viewer,
$action_record->getTarget());
}
public function renderRuleAsText(
HeraldRule $rule,
PhabricatorHandleList $handles,
PhabricatorUser $viewer) {
require_celerity_resource('herald-css');
$icon = id(new PHUIIconView())
->setIcon('fa-chevron-circle-right lightgreytext')
->addClass('herald-list-icon');
if ($rule->getMustMatchAll()) {
$match_text = pht('When all of these conditions are met:');
} else {
$match_text = pht('When any of these conditions are met:');
}
$match_title = phutil_tag(
'p',
array(
'class' => 'herald-list-description',
),
$match_text);
$match_list = array();
foreach ($rule->getConditions() as $condition) {
$match_list[] = phutil_tag(
'div',
array(
'class' => 'herald-list-item',
),
array(
$icon,
$this->renderConditionAsText($condition, $handles, $viewer),
));
}
if ($rule->isRepeatFirst()) {
$action_text = pht(
'Take these actions the first time this rule matches:');
} else if ($rule->isRepeatOnChange()) {
$action_text = pht(
'Take these actions if this rule did not match the last time:');
} else {
$action_text = pht(
'Take these actions every time this rule matches:');
}
$action_title = phutil_tag(
'p',
array(
'class' => 'herald-list-description',
),
$action_text);
$action_list = array();
foreach ($rule->getActions() as $action) {
$action_list[] = phutil_tag(
'div',
array(
'class' => 'herald-list-item',
),
array(
$icon,
$this->renderActionAsText($viewer, $action, $handles),
));
}
return array(
$match_title,
$match_list,
$action_title,
$action_list,
);
}
private function renderConditionAsText(
HeraldCondition $condition,
PhabricatorHandleList $handles,
PhabricatorUser $viewer) {
$field_type = $condition->getFieldName();
$field = $this->getFieldImplementation($field_type);
if (!$field) {
return pht('Unknown Field: "%s"', $field_type);
}
$field_name = $field->getHeraldFieldName();
$condition_type = $condition->getFieldCondition();
$condition_name = idx($this->getConditionNameMap(), $condition_type);
$value = $this->renderConditionValueAsText($condition, $handles, $viewer);
return array(
$field_name,
' ',
$condition_name,
' ',
$value,
);
}
private function renderActionAsText(
PhabricatorUser $viewer,
HeraldActionRecord $action,
PhabricatorHandleList $handles) {
$impl = $this->getActionImplementation($action->getAction());
if ($impl) {
$impl->setViewer($viewer);
$value = $action->getTarget();
return $impl->renderActionDescription($value);
}
$rule_global = HeraldRuleTypeConfig::RULE_TYPE_GLOBAL;
$action_type = $action->getAction();
$default = pht('(Unknown Action "%s") equals', $action_type);
$action_name = idx(
$this->getActionNameMap($rule_global),
$action_type,
$default);
$target = $this->renderActionTargetAsText($action, $handles);
return hsprintf(' %s %s', $action_name, $target);
}
private function renderConditionValueAsText(
HeraldCondition $condition,
PhabricatorHandleList $handles,
PhabricatorUser $viewer) {
$field = $this->requireFieldImplementation($condition->getFieldName());
return $field->renderConditionValue(
$viewer,
$condition->getFieldCondition(),
$condition->getValue());
}
private function renderActionTargetAsText(
HeraldActionRecord $action,
PhabricatorHandleList $handles) {
// TODO: This should be driven through HeraldAction.
$target = $action->getTarget();
if (!is_array($target)) {
$target = array($target);
}
foreach ($target as $index => $val) {
switch ($action->getAction()) {
default:
$handle = $handles->getHandleIfExists($val);
if ($handle) {
$target[$index] = $handle->renderLink();
}
break;
}
}
$target = phutil_implode_html(', ', $target);
return $target;
}
/**
* Given a @{class:HeraldRule}, this function extracts all the phids that
* we'll want to load as handles later.
*
* This function performs a somewhat hacky approach to figuring out what
* is and is not a phid - try to get the phid type and if the type is
* *not* unknown assume its a valid phid.
*
* Don't try this at home. Use more strongly typed data at home.
*
* Think of the children.
*/
public static function getHandlePHIDs(HeraldRule $rule) {
$phids = array($rule->getAuthorPHID());
foreach ($rule->getConditions() as $condition) {
$value = $condition->getValue();
if (!is_array($value)) {
$value = array($value);
}
foreach ($value as $val) {
if (phid_get_type($val) !=
PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
$phids[] = $val;
}
}
}
foreach ($rule->getActions() as $action) {
$target = $action->getTarget();
if (!is_array($target)) {
$target = array($target);
}
foreach ($target as $val) {
if (phid_get_type($val) !=
PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
$phids[] = $val;
}
}
}
if ($rule->isObjectRule()) {
$phids[] = $rule->getTriggerObjectPHID();
}
return $phids;
}
/* -( Applying Effects )--------------------------------------------------- */
/**
* @task apply
*/
protected function applyStandardEffect(HeraldEffect $effect) {
$action = $effect->getAction();
$rule_type = $effect->getRule()->getRuleType();
$impl = $this->getActionImplementation($action);
if (!$impl) {
return new HeraldApplyTranscript(
$effect,
false,
array(
array(
HeraldAction::DO_STANDARD_INVALID_ACTION,
$action,
),
));
}
if (!$impl->supportsRuleType($rule_type)) {
return new HeraldApplyTranscript(
$effect,
false,
array(
array(
HeraldAction::DO_STANDARD_WRONG_RULE_TYPE,
$rule_type,
),
));
}
$impl->applyEffect($this->getObject(), $effect);
return $impl->getApplyTranscript($effect);
}
public function loadEdgePHIDs($type) {
if (!isset($this->edgeCache[$type])) {
$phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->getObject()->getPHID(),
$type);
$this->edgeCache[$type] = array_fuse($phids);
}
return $this->edgeCache[$type];
}
/* -( Forbidden Actions )-------------------------------------------------- */
final public function getForbiddenActions() {
return array_keys($this->forbiddenActions);
}
final public function setForbiddenAction($action, $reason) {
$this->forbiddenActions[$action] = $reason;
return $this;
}
final public function getRequiredFieldStates($field_key) {
return $this->requireFieldImplementation($field_key)
->getRequiredAdapterStates();
}
final public function getRequiredActionStates($action_key) {
return $this->requireActionImplementation($action_key)
->getRequiredAdapterStates();
}
final public function getForbiddenReason($action) {
if (!isset($this->forbiddenActions[$action])) {
throw new Exception(
pht(
'Action "%s" is not forbidden!',
$action));
}
return $this->forbiddenActions[$action];
}
+
+/* -( Must Encrypt )------------------------------------------------------- */
+
+
+ final public function addMustEncryptReason($reason) {
+ $this->mustEncryptReasons[] = $reason;
+ return $this;
+ }
+
+ final public function getMustEncryptReasons() {
+ return $this->mustEncryptReasons;
+ }
+
+
+/* -( Webhooks )----------------------------------------------------------- */
+
+
+ public function supportsWebhooks() {
+ return true;
+ }
+
+
+ final public function queueWebhook($webhook_phid, $rule_phid) {
+ $this->webhookMap[$webhook_phid][] = $rule_phid;
+ return $this;
+ }
+
+ final public function getWebhookMap() {
+ return $this->webhookMap;
+ }
+
}
diff --git a/src/applications/herald/application/PhabricatorHeraldApplication.php b/src/applications/herald/application/PhabricatorHeraldApplication.php
index 9160b0e9d..753c03b26 100644
--- a/src/applications/herald/application/PhabricatorHeraldApplication.php
+++ b/src/applications/herald/application/PhabricatorHeraldApplication.php
@@ -1,78 +1,94 @@
<?php
final class PhabricatorHeraldApplication extends PhabricatorApplication {
public function getBaseURI() {
return '/herald/';
}
public function getIcon() {
return 'fa-bullhorn';
}
public function getName() {
return pht('Herald');
}
public function getShortDescription() {
return pht('Create Notification Rules');
}
public function getTitleGlyph() {
return "\xE2\x98\xBF";
}
public function getHelpDocumentationArticles(PhabricatorUser $viewer) {
return array(
array(
'name' => pht('Herald User Guide'),
'href' => PhabricatorEnv::getDoclink('Herald User Guide'),
),
+ array(
+ 'name' => pht('User Guide: Webhooks'),
+ 'href' => PhabricatorEnv::getDoclink('User Guide: Webhooks'),
+ ),
);
}
public function getFlavorText() {
return pht('Watch for danger!');
}
public function getApplicationGroup() {
return self::GROUP_UTILITIES;
}
public function getRemarkupRules() {
return array(
new HeraldRemarkupRule(),
);
}
public function getRoutes() {
return array(
'/H(?P<id>[1-9]\d*)' => 'HeraldRuleViewController',
'/herald/' => array(
'(?:query/(?P<queryKey>[^/]+)/)?' => 'HeraldRuleListController',
'new/' => 'HeraldNewController',
'create/' => 'HeraldNewController',
'edit/(?:(?P<id>[1-9]\d*)/)?' => 'HeraldRuleController',
'disable/(?P<id>[1-9]\d*)/(?P<action>\w+)/'
=> 'HeraldDisableController',
'test/' => 'HeraldTestConsoleController',
'transcript/' => array(
'' => 'HeraldTranscriptListController',
'(?:query/(?P<queryKey>[^/]+)/)?' => 'HeraldTranscriptListController',
'(?P<id>[1-9]\d*)/'
=> 'HeraldTranscriptController',
),
+ 'webhook/' => array(
+ $this->getQueryRoutePattern() => 'HeraldWebhookListController',
+ 'view/(?P<id>\d+)/(?:request/(?P<requestID>[^/]+)/)?' =>
+ 'HeraldWebhookViewController',
+ $this->getEditRoutePattern('edit/') => 'HeraldWebhookEditController',
+ 'test/(?P<id>\d+)/' => 'HeraldWebhookTestController',
+ 'key/(?P<action>view|cycle)/(?P<id>\d+)/' =>
+ 'HeraldWebhookKeyController',
+ ),
),
);
}
protected function getCustomCapabilities() {
return array(
HeraldManageGlobalRulesCapability::CAPABILITY => array(
'caption' => pht('Global rules can bypass access controls.'),
'default' => PhabricatorPolicies::POLICY_ADMIN,
),
+ HeraldCreateWebhooksCapability::CAPABILITY => array(
+ 'default' => PhabricatorPolicies::POLICY_ADMIN,
+ ),
);
}
}
diff --git a/src/applications/herald/capability/HeraldCreateWebhooksCapability.php b/src/applications/herald/capability/HeraldCreateWebhooksCapability.php
new file mode 100644
index 000000000..7537a6190
--- /dev/null
+++ b/src/applications/herald/capability/HeraldCreateWebhooksCapability.php
@@ -0,0 +1,16 @@
+<?php
+
+final class HeraldCreateWebhooksCapability
+ extends PhabricatorPolicyCapability {
+
+ const CAPABILITY = 'herald.webhooks';
+
+ public function getCapabilityName() {
+ return pht('Can Create Webhooks');
+ }
+
+ public function describeCapabilityRejection() {
+ return pht('You do not have permission to create webhooks.');
+ }
+
+}
diff --git a/src/applications/herald/controller/HeraldController.php b/src/applications/herald/controller/HeraldController.php
index 1a5d2084d..48d50b12b 100644
--- a/src/applications/herald/controller/HeraldController.php
+++ b/src/applications/herald/controller/HeraldController.php
@@ -1,40 +1,31 @@
<?php
abstract class HeraldController extends PhabricatorController {
public function buildApplicationMenu() {
return $this->buildSideNavView()->getMenu();
}
- protected function buildApplicationCrumbs() {
- $crumbs = parent::buildApplicationCrumbs();
-
- $crumbs->addAction(
- id(new PHUIListItemView())
- ->setName(pht('Create Herald Rule'))
- ->setHref($this->getApplicationURI('create/'))
- ->setIcon('fa-plus-square'));
-
- return $crumbs;
- }
-
public function buildSideNavView() {
$viewer = $this->getViewer();
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI($this->getApplicationURI()));
id(new HeraldRuleSearchEngine())
->setViewer($viewer)
->addNavigationItems($nav->getMenu());
$nav->addLabel(pht('Utilities'))
- ->addFilter('test', pht('Test Console'))
- ->addFilter('transcript', pht('Transcripts'));
+ ->addFilter('test', pht('Test Console'))
+ ->addFilter('transcript', pht('Transcripts'));
+
+ $nav->addLabel(pht('Webhooks'))
+ ->addFilter('webhook', pht('Webhooks'));
$nav->selectFilter(null);
return $nav;
}
}
diff --git a/src/applications/herald/controller/HeraldRuleController.php b/src/applications/herald/controller/HeraldRuleController.php
index c61c29e90..d400f8ae9 100644
--- a/src/applications/herald/controller/HeraldRuleController.php
+++ b/src/applications/herald/controller/HeraldRuleController.php
@@ -1,724 +1,733 @@
<?php
final class HeraldRuleController extends HeraldController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$content_type_map = HeraldAdapter::getEnabledAdapterMap($viewer);
$rule_type_map = HeraldRuleTypeConfig::getRuleTypeMap();
if ($id) {
$rule = id(new HeraldRuleQuery())
->setViewer($viewer)
->withIDs(array($id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$rule) {
return new Aphront404Response();
}
$cancel_uri = '/'.$rule->getMonogram();
} else {
$new_uri = $this->getApplicationURI('new/');
$rule = new HeraldRule();
$rule->setAuthorPHID($viewer->getPHID());
$rule->setMustMatchAll(1);
$content_type = $request->getStr('content_type');
$rule->setContentType($content_type);
$rule_type = $request->getStr('rule_type');
if (!isset($rule_type_map[$rule_type])) {
return $this->newDialog()
->setTitle(pht('Invalid Rule Type'))
->appendParagraph(
pht(
'The selected rule type ("%s") is not recognized by Herald.',
$rule_type))
->addCancelButton($new_uri);
}
$rule->setRuleType($rule_type);
try {
$adapter = HeraldAdapter::getAdapterForContentType(
$rule->getContentType());
} catch (Exception $ex) {
return $this->newDialog()
->setTitle(pht('Invalid Content Type'))
->appendParagraph(
pht(
'The selected content type ("%s") is not recognized by '.
'Herald.',
$rule->getContentType()))
->addCancelButton($new_uri);
}
if (!$adapter->supportsRuleType($rule->getRuleType())) {
return $this->newDialog()
->setTitle(pht('Rule/Content Mismatch'))
->appendParagraph(
pht(
'The selected rule type ("%s") is not supported by the selected '.
'content type ("%s").',
$rule->getRuleType(),
$rule->getContentType()))
->addCancelButton($new_uri);
}
if ($rule->isObjectRule()) {
$rule->setTriggerObjectPHID($request->getStr('targetPHID'));
$object = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withPHIDs(array($rule->getTriggerObjectPHID()))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$object) {
throw new Exception(
pht('No valid object provided for object rule!'));
}
if (!$adapter->canTriggerOnObject($object)) {
throw new Exception(
pht('Object is of wrong type for adapter!'));
}
}
$cancel_uri = $this->getApplicationURI();
}
if ($rule->isGlobalRule()) {
$this->requireApplicationCapability(
HeraldManageGlobalRulesCapability::CAPABILITY);
}
$adapter = HeraldAdapter::getAdapterForContentType($rule->getContentType());
$local_version = id(new HeraldRule())->getConfigVersion();
if ($rule->getConfigVersion() > $local_version) {
throw new Exception(
pht(
'This rule was created with a newer version of Herald. You can not '.
'view or edit it in this older version. Upgrade your Phabricator '.
'deployment.'));
}
// Upgrade rule version to our version, since we might add newly-defined
// conditions, etc.
$rule->setConfigVersion($local_version);
$rule_conditions = $rule->loadConditions();
$rule_actions = $rule->loadActions();
$rule->attachConditions($rule_conditions);
$rule->attachActions($rule_actions);
$e_name = true;
$errors = array();
if ($request->isFormPost() && $request->getStr('save')) {
list($e_name, $errors) = $this->saveRule($adapter, $rule, $request);
if (!$errors) {
$id = $rule->getID();
$uri = '/'.$rule->getMonogram();
return id(new AphrontRedirectResponse())->setURI($uri);
}
}
$must_match_selector = $this->renderMustMatchSelector($rule);
$repetition_selector = $this->renderRepetitionSelector($rule, $adapter);
$handles = $this->loadHandlesForRule($rule);
require_celerity_resource('herald-css');
$content_type_name = $content_type_map[$rule->getContentType()];
$rule_type_name = $rule_type_map[$rule->getRuleType()];
$form = id(new AphrontFormView())
->setUser($viewer)
->setID('herald-rule-edit-form')
->addHiddenInput('content_type', $rule->getContentType())
->addHiddenInput('rule_type', $rule->getRuleType())
->addHiddenInput('save', 1)
->appendChild(
// Build this explicitly (instead of using addHiddenInput())
// so we can add a sigil to it.
javelin_tag(
'input',
array(
'type' => 'hidden',
'name' => 'rule',
'sigil' => 'rule',
)))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Rule Name'))
->setName('name')
->setError($e_name)
->setValue($rule->getName()));
$trigger_object_control = false;
if ($rule->isObjectRule()) {
$trigger_object_control = id(new AphrontFormStaticControl())
->setValue(
pht(
'This rule triggers for %s.',
$handles[$rule->getTriggerObjectPHID()]->renderLink()));
}
$form
->appendChild(
id(new AphrontFormMarkupControl())
->setValue(pht(
'This %s rule triggers for %s.',
phutil_tag('strong', array(), $rule_type_name),
phutil_tag('strong', array(), $content_type_name))))
->appendChild($trigger_object_control)
->appendChild(
id(new PHUIFormInsetView())
->setTitle(pht('Conditions'))
->setRightButton(javelin_tag(
'a',
array(
'href' => '#',
'class' => 'button button-green',
'sigil' => 'create-condition',
'mustcapture' => true,
),
pht('New Condition')))
->setDescription(
pht('When %s these conditions are met:', $must_match_selector))
->setContent(javelin_tag(
'table',
array(
'sigil' => 'rule-conditions',
'class' => 'herald-condition-table',
),
'')))
->appendChild(
id(new PHUIFormInsetView())
->setTitle(pht('Action'))
->setRightButton(javelin_tag(
'a',
array(
'href' => '#',
'class' => 'button button-green',
'sigil' => 'create-action',
'mustcapture' => true,
),
pht('New Action')))
->setDescription(pht(
'Take these actions %s',
$repetition_selector))
->setContent(javelin_tag(
'table',
array(
'sigil' => 'rule-actions',
'class' => 'herald-action-table',
),
'')))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Save Rule'))
->addCancelButton($cancel_uri));
$this->setupEditorBehavior($rule, $handles, $adapter);
$title = $rule->getID()
? pht('Edit Herald Rule: %s', $rule->getName())
: pht('Create Herald Rule: %s', idx($content_type_map, $content_type));
$form_box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setBackground(PHUIObjectBoxView::WHITE_CONFIG)
->setFormErrors($errors)
->setForm($form);
$crumbs = $this
->buildApplicationCrumbs()
->addTextCrumb($title)
->setBorder(true);
$view = id(new PHUITwoColumnView())
->setFooter($form_box);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild(
array(
$view,
));
}
private function saveRule(HeraldAdapter $adapter, $rule, $request) {
$new_name = $request->getStr('name');
$match_all = ($request->getStr('must_match') == 'all');
- $repetition_policy_param = $request->getStr('repetition_policy');
+ $repetition_policy = $request->getStr('repetition_policy');
+
+ // If the user selected an invalid policy, or there's only one possible
+ // value so we didn't render a control, adjust the value to the first
+ // valid policy value.
+ $repetition_options = $this->getRepetitionOptionMap($adapter);
+ if (!isset($repetition_options[$repetition_policy])) {
+ $repetition_policy = head_key($repetition_options);
+ }
$e_name = true;
$errors = array();
if (!strlen($new_name)) {
$e_name = pht('Required');
$errors[] = pht('Rule must have a name.');
}
$data = null;
try {
$data = phutil_json_decode($request->getStr('rule'));
} catch (PhutilJSONParserException $ex) {
throw new PhutilProxyException(
pht('Failed to decode rule data.'),
$ex);
}
if (!is_array($data) ||
!$data['conditions'] ||
!$data['actions']) {
throw new Exception(pht('Failed to decode rule data.'));
}
$conditions = array();
foreach ($data['conditions'] as $condition) {
if ($condition === null) {
// We manage this as a sparse array on the client, so may receive
// NULL if conditions have been removed.
continue;
}
$obj = new HeraldCondition();
$obj->setFieldName($condition[0]);
$obj->setFieldCondition($condition[1]);
if (is_array($condition[2])) {
$obj->setValue(array_keys($condition[2]));
} else {
$obj->setValue($condition[2]);
}
try {
$adapter->willSaveCondition($obj);
} catch (HeraldInvalidConditionException $ex) {
$errors[] = $ex->getMessage();
}
$conditions[] = $obj;
}
$actions = array();
foreach ($data['actions'] as $action) {
if ($action === null) {
// Sparse on the client; removals can give us NULLs.
continue;
}
if (!isset($action[1])) {
// Legitimate for any action which doesn't need a target, like
// "Do nothing".
$action[1] = null;
}
$obj = new HeraldActionRecord();
$obj->setAction($action[0]);
$obj->setTarget($action[1]);
try {
$adapter->willSaveAction($rule, $obj);
} catch (HeraldInvalidActionException $ex) {
$errors[] = $ex->getMessage();
}
$actions[] = $obj;
}
if (!$errors) {
$new_state = id(new HeraldRuleSerializer())->serializeRuleComponents(
$match_all,
$conditions,
$actions,
- $repetition_policy_param);
+ $repetition_policy);
$xactions = array();
$xactions[] = id(new HeraldRuleTransaction())
->setTransactionType(HeraldRuleTransaction::TYPE_EDIT)
->setNewValue($new_state);
$xactions[] = id(new HeraldRuleTransaction())
->setTransactionType(HeraldRuleTransaction::TYPE_NAME)
->setNewValue($new_name);
try {
id(new HeraldRuleEditor())
->setActor($this->getViewer())
->setContinueOnNoEffect(true)
->setContentSourceFromRequest($request)
->applyTransactions($rule, $xactions);
return array(null, null);
} catch (Exception $ex) {
$errors[] = $ex->getMessage();
}
}
// mutate current rule, so it would be sent to the client in the right state
$rule->setMustMatchAll((int)$match_all);
$rule->setName($new_name);
- $rule->setRepetitionPolicyStringConstant($repetition_policy_param);
+ $rule->setRepetitionPolicyStringConstant($repetition_policy);
$rule->attachConditions($conditions);
$rule->attachActions($actions);
return array($e_name, $errors);
}
private function setupEditorBehavior(
HeraldRule $rule,
array $handles,
HeraldAdapter $adapter) {
$all_rules = $this->loadRulesThisRuleMayDependUpon($rule);
$all_rules = mpull($all_rules, 'getName', 'getPHID');
asort($all_rules);
$all_fields = $adapter->getFieldNameMap();
$all_conditions = $adapter->getConditionNameMap();
$all_actions = $adapter->getActionNameMap($rule->getRuleType());
$fields = $adapter->getFields();
$field_map = array_select_keys($all_fields, $fields);
// Populate any fields which exist in the rule but which we don't know the
// names of, so that saving a rule without touching anything doesn't change
// it.
foreach ($rule->getConditions() as $condition) {
$field_name = $condition->getFieldName();
if (empty($field_map[$field_name])) {
$field_map[$field_name] = pht('<Unknown Field "%s">', $field_name);
}
}
$actions = $adapter->getActions($rule->getRuleType());
$action_map = array_select_keys($all_actions, $actions);
// Populate any actions which exist in the rule but which we don't know the
// names of, so that saving a rule without touching anything doesn't change
// it.
foreach ($rule->getActions() as $action) {
$action_name = $action->getAction();
if (empty($action_map[$action_name])) {
$action_map[$action_name] = pht('<Unknown Action "%s">', $action_name);
}
}
$config_info = array();
$config_info['fields'] = $this->getFieldGroups($adapter, $field_map);
$config_info['conditions'] = $all_conditions;
$config_info['actions'] = $this->getActionGroups($adapter, $action_map);
$config_info['valueMap'] = array();
foreach ($field_map as $field => $name) {
try {
$field_conditions = $adapter->getConditionsForField($field);
} catch (Exception $ex) {
$field_conditions = array(HeraldAdapter::CONDITION_UNCONDITIONALLY);
}
$config_info['conditionMap'][$field] = $field_conditions;
}
foreach ($field_map as $field => $fname) {
foreach ($config_info['conditionMap'][$field] as $condition) {
$value_key = $adapter->getValueTypeForFieldAndCondition(
$field,
$condition);
if ($value_key instanceof HeraldFieldValue) {
$value_key->setViewer($this->getViewer());
$spec = $value_key->getControlSpecificationDictionary();
$value_key = $value_key->getFieldValueKey();
$config_info['valueMap'][$value_key] = $spec;
}
$config_info['values'][$field][$condition] = $value_key;
}
}
$config_info['rule_type'] = $rule->getRuleType();
foreach ($action_map as $action => $name) {
try {
$value_key = $adapter->getValueTypeForAction(
$action,
$rule->getRuleType());
} catch (Exception $ex) {
$value_key = new HeraldEmptyFieldValue();
}
if ($value_key instanceof HeraldFieldValue) {
$value_key->setViewer($this->getViewer());
$spec = $value_key->getControlSpecificationDictionary();
$value_key = $value_key->getFieldValueKey();
$config_info['valueMap'][$value_key] = $spec;
}
$config_info['targets'][$action] = $value_key;
}
$default_group = head($config_info['fields']);
$default_field = head_key($default_group['options']);
$default_condition = head($config_info['conditionMap'][$default_field]);
$default_actions = head($config_info['actions']);
$default_action = head_key($default_actions['options']);
if ($rule->getConditions()) {
$serial_conditions = array();
foreach ($rule->getConditions() as $condition) {
$value = $adapter->getEditorValueForCondition(
$this->getViewer(),
$condition);
$serial_conditions[] = array(
$condition->getFieldName(),
$condition->getFieldCondition(),
$value,
);
}
} else {
$serial_conditions = array(
array($default_field, $default_condition, null),
);
}
if ($rule->getActions()) {
$serial_actions = array();
foreach ($rule->getActions() as $action) {
$value = $adapter->getEditorValueForAction(
$this->getViewer(),
$action);
$serial_actions[] = array(
$action->getAction(),
$value,
);
}
} else {
$serial_actions = array(
array($default_action, null),
);
}
Javelin::initBehavior(
'herald-rule-editor',
array(
'root' => 'herald-rule-edit-form',
'default' => array(
'field' => $default_field,
'condition' => $default_condition,
'action' => $default_action,
),
'conditions' => (object)$serial_conditions,
'actions' => (object)$serial_actions,
'template' => $this->buildTokenizerTemplates() + array(
'rules' => $all_rules,
),
'info' => $config_info,
));
}
private function loadHandlesForRule($rule) {
$phids = array();
foreach ($rule->getActions() as $action) {
if (!is_array($action->getTarget())) {
continue;
}
foreach ($action->getTarget() as $target) {
$target = (array)$target;
foreach ($target as $phid) {
$phids[] = $phid;
}
}
}
foreach ($rule->getConditions() as $condition) {
$value = $condition->getValue();
if (is_array($value)) {
foreach ($value as $phid) {
$phids[] = $phid;
}
}
}
$phids[] = $rule->getAuthorPHID();
if ($rule->isObjectRule()) {
$phids[] = $rule->getTriggerObjectPHID();
}
return $this->loadViewerHandles($phids);
}
/**
* Render the selector for the "When (all of | any of) these conditions are
* met:" element.
*/
private function renderMustMatchSelector($rule) {
return AphrontFormSelectControl::renderSelectTag(
$rule->getMustMatchAll() ? 'all' : 'any',
array(
'all' => pht('all of'),
'any' => pht('any of'),
),
array(
'name' => 'must_match',
));
}
/**
* Render the selector for "Take these actions (every time | only the first
* time) this rule matches..." element.
*/
private function renderRepetitionSelector($rule, HeraldAdapter $adapter) {
$repetition_policy = $rule->getRepetitionPolicyStringConstant();
-
- $repetition_options = $adapter->getRepetitionOptions();
- $repetition_names = HeraldRule::getRepetitionPolicySelectOptionMap();
- $repetition_map = array_select_keys($repetition_names, $repetition_options);
-
+ $repetition_map = $this->getRepetitionOptionMap($adapter);
if (count($repetition_map) < 2) {
- return head($repetition_names);
+ return head($repetition_map);
} else {
return AphrontFormSelectControl::renderSelectTag(
$repetition_policy,
$repetition_map,
array(
'name' => 'repetition_policy',
));
}
}
+ private function getRepetitionOptionMap(HeraldAdapter $adapter) {
+ $repetition_options = $adapter->getRepetitionOptions();
+ $repetition_names = HeraldRule::getRepetitionPolicySelectOptionMap();
+ return array_select_keys($repetition_names, $repetition_options);
+ }
protected function buildTokenizerTemplates() {
$template = new AphrontTokenizerTemplateView();
$template = $template->render();
return array(
'markup' => $template,
);
}
/**
* Load rules for the "Another Herald rule..." condition dropdown, which
* allows one rule to depend upon the success or failure of another rule.
*/
private function loadRulesThisRuleMayDependUpon(HeraldRule $rule) {
$viewer = $this->getRequest()->getUser();
// Any rule can depend on a global rule.
$all_rules = id(new HeraldRuleQuery())
->setViewer($viewer)
->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_GLOBAL))
->withContentTypes(array($rule->getContentType()))
->execute();
if ($rule->isObjectRule()) {
// Object rules may depend on other rules for the same object.
$all_rules += id(new HeraldRuleQuery())
->setViewer($viewer)
->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_OBJECT))
->withContentTypes(array($rule->getContentType()))
->withTriggerObjectPHIDs(array($rule->getTriggerObjectPHID()))
->execute();
}
if ($rule->isPersonalRule()) {
// Personal rules may depend upon your other personal rules.
$all_rules += id(new HeraldRuleQuery())
->setViewer($viewer)
->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_PERSONAL))
->withContentTypes(array($rule->getContentType()))
->withAuthorPHIDs(array($rule->getAuthorPHID()))
->execute();
}
// mark disabled rules as disabled since they are not useful as such;
// don't filter though to keep edit cases sane / expected
foreach ($all_rules as $current_rule) {
if ($current_rule->getIsDisabled()) {
$current_rule->makeEphemeral();
$current_rule->setName($rule->getName().' '.pht('(Disabled)'));
}
}
// A rule can not depend upon itself.
unset($all_rules[$rule->getID()]);
return $all_rules;
}
private function getFieldGroups(HeraldAdapter $adapter, array $field_map) {
$group_map = array();
foreach ($field_map as $field_key => $field_name) {
$group_key = $adapter->getFieldGroupKey($field_key);
$group_map[$group_key][$field_key] = $field_name;
}
return $this->getGroups(
$group_map,
HeraldFieldGroup::getAllFieldGroups());
}
private function getActionGroups(HeraldAdapter $adapter, array $action_map) {
$group_map = array();
foreach ($action_map as $action_key => $action_name) {
$group_key = $adapter->getActionGroupKey($action_key);
$group_map[$group_key][$action_key] = $action_name;
}
return $this->getGroups(
$group_map,
HeraldActionGroup::getAllActionGroups());
}
private function getGroups(array $item_map, array $group_list) {
assert_instances_of($group_list, 'HeraldGroup');
$groups = array();
foreach ($item_map as $group_key => $options) {
asort($options);
$group_object = idx($group_list, $group_key);
if ($group_object) {
$group_label = $group_object->getGroupLabel();
$group_order = $group_object->getSortKey();
} else {
$group_label = nonempty($group_key, pht('Other'));
$group_order = 'Z';
}
$groups[] = array(
'label' => $group_label,
'options' => $options,
'order' => $group_order,
);
}
return array_values(isort($groups, 'order'));
}
}
diff --git a/src/applications/herald/controller/HeraldRuleListController.php b/src/applications/herald/controller/HeraldRuleListController.php
index 490d84212..846116333 100644
--- a/src/applications/herald/controller/HeraldRuleListController.php
+++ b/src/applications/herald/controller/HeraldRuleListController.php
@@ -1,21 +1,32 @@
<?php
final class HeraldRuleListController extends HeraldController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$querykey = $request->getURIData('queryKey');
$controller = id(new PhabricatorApplicationSearchController())
->setQueryKey($querykey)
->setSearchEngine(new HeraldRuleSearchEngine())
->setNavigation($this->buildSideNavView());
return $this->delegateToController($controller);
}
+ protected function buildApplicationCrumbs() {
+ $crumbs = parent::buildApplicationCrumbs();
+
+ $crumbs->addAction(
+ id(new PHUIListItemView())
+ ->setName(pht('Create Herald Rule'))
+ ->setHref($this->getApplicationURI('create/'))
+ ->setIcon('fa-plus-square'));
+
+ return $crumbs;
+ }
}
diff --git a/src/applications/herald/controller/HeraldTestConsoleController.php b/src/applications/herald/controller/HeraldTestConsoleController.php
index 8a7a94963..4ddab2669 100644
--- a/src/applications/herald/controller/HeraldTestConsoleController.php
+++ b/src/applications/herald/controller/HeraldTestConsoleController.php
@@ -1,220 +1,221 @@
<?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();
$adapter
->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);
}
}
diff --git a/src/applications/herald/controller/HeraldWebhookController.php b/src/applications/herald/controller/HeraldWebhookController.php
new file mode 100644
index 000000000..6c210640c
--- /dev/null
+++ b/src/applications/herald/controller/HeraldWebhookController.php
@@ -0,0 +1,15 @@
+<?php
+
+abstract class HeraldWebhookController extends HeraldController {
+
+ protected function buildApplicationCrumbs() {
+ $crumbs = parent::buildApplicationCrumbs();
+
+ $crumbs->addTextCrumb(
+ pht('Webhooks'),
+ $this->getApplicationURI('webhook/'));
+
+ return $crumbs;
+ }
+
+}
diff --git a/src/applications/diffusion/controller/DiffusionPullLogListController.php b/src/applications/herald/controller/HeraldWebhookEditController.php
similarity index 50%
copy from src/applications/diffusion/controller/DiffusionPullLogListController.php
copy to src/applications/herald/controller/HeraldWebhookEditController.php
index 29712be2a..94c24187e 100644
--- a/src/applications/diffusion/controller/DiffusionPullLogListController.php
+++ b/src/applications/herald/controller/HeraldWebhookEditController.php
@@ -1,12 +1,12 @@
<?php
-final class DiffusionPullLogListController
- extends DiffusionLogController {
+final class HeraldWebhookEditController
+ extends HeraldWebhookController {
public function handleRequest(AphrontRequest $request) {
- return id(new DiffusionPullLogSearchEngine())
+ return id(new HeraldWebhookEditEngine())
->setController($this)
->buildResponse();
}
}
diff --git a/src/applications/herald/controller/HeraldWebhookKeyController.php b/src/applications/herald/controller/HeraldWebhookKeyController.php
new file mode 100644
index 000000000..8e8bd03f4
--- /dev/null
+++ b/src/applications/herald/controller/HeraldWebhookKeyController.php
@@ -0,0 +1,56 @@
+<?php
+
+final class HeraldWebhookKeyController
+ extends HeraldWebhookController {
+
+ public function handleRequest(AphrontRequest $request) {
+ $viewer = $this->getViewer();
+
+ $hook = id(new HeraldWebhookQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($request->getURIData('id')))
+ ->requireCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ))
+ ->executeOne();
+ if (!$hook) {
+ return new Aphront404Response();
+ }
+
+ $action = $request->getURIData('action');
+ if ($action === 'cycle') {
+ if (!$request->isFormPost()) {
+ return $this->newDialog()
+ ->setTitle(pht('Regenerate HMAC Key'))
+ ->appendParagraph(
+ pht(
+ 'Regenerate the HMAC key used to sign requests made by this '.
+ 'webhook?'))
+ ->appendParagraph(
+ pht(
+ 'Requests which are currently authenticated with the old key '.
+ 'may fail.'))
+ ->addCancelButton($hook->getURI())
+ ->addSubmitButton(pht('Regnerate Key'));
+ } else {
+ $hook->regenerateHMACKey()->save();
+ }
+ }
+
+ $form = id(new AphrontFormView())
+ ->setViewer($viewer)
+ ->appendControl(
+ id(new AphrontFormTextControl())
+ ->setLabel(pht('HMAC Key'))
+ ->setValue($hook->getHMACKey()));
+
+ return $this->newDialog()
+ ->setTitle(pht('Webhook HMAC Key'))
+ ->appendForm($form)
+ ->addCancelButton($hook->getURI(), pht('Done'));
+ }
+
+
+}
diff --git a/src/applications/herald/controller/HeraldWebhookListController.php b/src/applications/herald/controller/HeraldWebhookListController.php
new file mode 100644
index 000000000..85c53b2fa
--- /dev/null
+++ b/src/applications/herald/controller/HeraldWebhookListController.php
@@ -0,0 +1,26 @@
+<?php
+
+final class HeraldWebhookListController
+ extends HeraldWebhookController {
+
+ public function shouldAllowPublic() {
+ return true;
+ }
+
+ public function handleRequest(AphrontRequest $request) {
+ return id(new HeraldWebhookSearchEngine())
+ ->setController($this)
+ ->buildResponse();
+ }
+
+ protected function buildApplicationCrumbs() {
+ $crumbs = parent::buildApplicationCrumbs();
+
+ id(new HeraldWebhookEditEngine())
+ ->setViewer($this->getViewer())
+ ->addActionToCrumbs($crumbs);
+
+ return $crumbs;
+ }
+
+}
diff --git a/src/applications/herald/controller/HeraldWebhookTestController.php b/src/applications/herald/controller/HeraldWebhookTestController.php
new file mode 100644
index 000000000..510e7f3f8
--- /dev/null
+++ b/src/applications/herald/controller/HeraldWebhookTestController.php
@@ -0,0 +1,94 @@
+<?php
+
+final class HeraldWebhookTestController
+ extends HeraldWebhookController {
+
+ public function handleRequest(AphrontRequest $request) {
+ $viewer = $this->getViewer();
+
+ $hook = id(new HeraldWebhookQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($request->getURIData('id')))
+ ->requireCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ))
+ ->executeOne();
+ if (!$hook) {
+ return new Aphront404Response();
+ }
+
+ $v_object = null;
+ $e_object = null;
+ $errors = array();
+ if ($request->isFormPost()) {
+
+ $v_object = $request->getStr('object');
+ if (!strlen($v_object)) {
+ $object = $hook;
+ } else {
+ $objects = id(new PhabricatorObjectQuery())
+ ->setViewer($viewer)
+ ->withNames(array($v_object))
+ ->execute();
+ if ($objects) {
+ $object = head($objects);
+ } else {
+ $e_object = pht('Invalid');
+ $errors[] = pht('Specified object could not be loaded.');
+ }
+ }
+
+ if (!$errors) {
+ $xaction_query =
+ PhabricatorApplicationTransactionQuery::newQueryForObject($object);
+
+ $xactions = $xaction_query
+ ->withObjectPHIDs(array($object->getPHID()))
+ ->setViewer($viewer)
+ ->setLimit(10)
+ ->execute();
+
+ $request = HeraldWebhookRequest::initializeNewWebhookRequest($hook)
+ ->setObjectPHID($object->getPHID())
+ ->setTriggerPHIDs(array($viewer->getPHID()))
+ ->setIsTestAction(true)
+ ->setTransactionPHIDs(mpull($xactions, 'getPHID'))
+ ->save();
+
+ $request->queueCall();
+
+ $next_uri = $hook->getURI().'request/'.$request->getID().'/';
+
+ return id(new AphrontRedirectResponse())->setURI($next_uri);
+ }
+ }
+
+ $instructions = <<<EOREMARKUP
+Optionally, choose an object to generate test data for (like `D123` or `T234`).
+
+The 10 most recent transactions for the object will be submitted to the webhook.
+EOREMARKUP;
+
+ $form = id(new AphrontFormView())
+ ->setViewer($viewer)
+ ->appendControl(
+ id(new AphrontFormTextControl())
+ ->setLabel(pht('Object'))
+ ->setName('object')
+ ->setError($e_object)
+ ->setValue($v_object));
+
+ return $this->newDialog()
+ ->setErrors($errors)
+ ->setWidth(AphrontDialogView::WIDTH_FORM)
+ ->setTitle(pht('New Test Request'))
+ ->appendParagraph(new PHUIRemarkupView($viewer, $instructions))
+ ->appendForm($form)
+ ->addCancelButton($hook->getURI())
+ ->addSubmitButton(pht('Test Webhook'));
+ }
+
+
+}
diff --git a/src/applications/herald/controller/HeraldWebhookViewController.php b/src/applications/herald/controller/HeraldWebhookViewController.php
new file mode 100644
index 000000000..9b11f5d43
--- /dev/null
+++ b/src/applications/herald/controller/HeraldWebhookViewController.php
@@ -0,0 +1,184 @@
+<?php
+
+final class HeraldWebhookViewController
+ extends HeraldWebhookController {
+
+ public function shouldAllowPublic() {
+ return true;
+ }
+
+ public function handleRequest(AphrontRequest $request) {
+ $viewer = $this->getViewer();
+
+ $hook = id(new HeraldWebhookQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($request->getURIData('id')))
+ ->executeOne();
+ if (!$hook) {
+ return new Aphront404Response();
+ }
+
+ $header = $this->buildHeaderView($hook);
+
+ $warnings = null;
+ if ($hook->isInErrorBackoff($viewer)) {
+ $message = pht(
+ 'Many requests to this webhook have failed recently (at least %s '.
+ 'errors in the last %s seconds). New requests are temporarily paused.',
+ $hook->getErrorBackoffThreshold(),
+ $hook->getErrorBackoffWindow());
+
+ $warnings = id(new PHUIInfoView())
+ ->setSeverity(PHUIInfoView::SEVERITY_WARNING)
+ ->setErrors(
+ array(
+ $message,
+ ));
+ }
+
+ $curtain = $this->buildCurtain($hook);
+ $properties_view = $this->buildPropertiesView($hook);
+
+ $timeline = $this->buildTransactionTimeline(
+ $hook,
+ new HeraldWebhookTransactionQuery());
+ $timeline->setShouldTerminate(true);
+
+ $requests = id(new HeraldWebhookRequestQuery())
+ ->setViewer($viewer)
+ ->withWebhookPHIDs(array($hook->getPHID()))
+ ->setLimit(20)
+ ->execute();
+
+ $requests_table = id(new HeraldWebhookRequestListView())
+ ->setViewer($viewer)
+ ->setRequests($requests)
+ ->setHighlightID($request->getURIData('requestID'));
+
+ $requests_view = id(new PHUIObjectBoxView())
+ ->setHeaderText(pht('Recent Requests'))
+ ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+ ->setTable($requests_table);
+
+ $hook_view = id(new PHUITwoColumnView())
+ ->setHeader($header)
+ ->setMainColumn(
+ array(
+ $warnings,
+ $properties_view,
+ $requests_view,
+ $timeline,
+ ))
+ ->setCurtain($curtain);
+
+ $crumbs = $this->buildApplicationCrumbs()
+ ->addTextCrumb(pht('Webhook %d', $hook->getID()))
+ ->setBorder(true);
+
+ return $this->newPage()
+ ->setTitle(
+ array(
+ pht('Webhook %d', $hook->getID()),
+ $hook->getName(),
+ ))
+ ->setCrumbs($crumbs)
+ ->setPageObjectPHIDs(
+ array(
+ $hook->getPHID(),
+ ))
+ ->appendChild($hook_view);
+ }
+
+ private function buildHeaderView(HeraldWebhook $hook) {
+ $viewer = $this->getViewer();
+
+ $title = $hook->getName();
+
+ $status_icon = $hook->getStatusIcon();
+ $status_color = $hook->getStatusColor();
+ $status_name = $hook->getStatusDisplayName();
+
+ $header = id(new PHUIHeaderView())
+ ->setHeader($title)
+ ->setViewer($viewer)
+ ->setPolicyObject($hook)
+ ->setStatus($status_icon, $status_color, $status_name)
+ ->setHeaderIcon('fa-cloud-upload');
+
+ return $header;
+ }
+
+
+ private function buildCurtain(HeraldWebhook $hook) {
+ $viewer = $this->getViewer();
+ $curtain = $this->newCurtainView($hook);
+
+ $can_edit = PhabricatorPolicyFilter::hasCapability(
+ $viewer,
+ $hook,
+ PhabricatorPolicyCapability::CAN_EDIT);
+
+ $id = $hook->getID();
+ $edit_uri = $this->getApplicationURI("webhook/edit/{$id}/");
+ $test_uri = $this->getApplicationURI("webhook/test/{$id}/");
+
+ $key_view_uri = $this->getApplicationURI("webhook/key/view/{$id}/");
+ $key_cycle_uri = $this->getApplicationURI("webhook/key/cycle/{$id}/");
+
+ $curtain->addAction(
+ id(new PhabricatorActionView())
+ ->setName(pht('Edit Webhook'))
+ ->setIcon('fa-pencil')
+ ->setDisabled(!$can_edit)
+ ->setWorkflow(!$can_edit)
+ ->setHref($edit_uri));
+
+ $curtain->addAction(
+ id(new PhabricatorActionView())
+ ->setName(pht('New Test Request'))
+ ->setIcon('fa-cloud-upload')
+ ->setDisabled(!$can_edit)
+ ->setWorkflow(true)
+ ->setHref($test_uri));
+
+ $curtain->addAction(
+ id(new PhabricatorActionView())
+ ->setName(pht('View HMAC Key'))
+ ->setIcon('fa-key')
+ ->setDisabled(!$can_edit)
+ ->setWorkflow(true)
+ ->setHref($key_view_uri));
+
+ $curtain->addAction(
+ id(new PhabricatorActionView())
+ ->setName(pht('Regenerate HMAC Key'))
+ ->setIcon('fa-refresh')
+ ->setDisabled(!$can_edit)
+ ->setWorkflow(true)
+ ->setHref($key_cycle_uri));
+
+ return $curtain;
+ }
+
+
+ private function buildPropertiesView(HeraldWebhook $hook) {
+ $viewer = $this->getViewer();
+
+ $properties = id(new PHUIPropertyListView())
+ ->setViewer($viewer);
+
+ $properties->addProperty(
+ pht('URI'),
+ $hook->getWebhookURI());
+
+ $properties->addProperty(
+ pht('Status'),
+ $hook->getStatusDisplayName());
+
+ return id(new PHUIObjectBoxView())
+ ->setHeaderText(pht('Details'))
+ ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+ ->appendChild($properties);
+ }
+
+}
diff --git a/src/applications/herald/editor/HeraldWebhookEditEngine.php b/src/applications/herald/editor/HeraldWebhookEditEngine.php
new file mode 100644
index 000000000..5bca0af54
--- /dev/null
+++ b/src/applications/herald/editor/HeraldWebhookEditEngine.php
@@ -0,0 +1,105 @@
+<?php
+
+final class HeraldWebhookEditEngine
+ extends PhabricatorEditEngine {
+
+ const ENGINECONST = 'herald.webhook';
+
+ public function isEngineConfigurable() {
+ return false;
+ }
+
+ public function getEngineName() {
+ return pht('Webhooks');
+ }
+
+ public function getSummaryHeader() {
+ return pht('Edit Webhook Configurations');
+ }
+
+ public function getSummaryText() {
+ return pht('This engine is used to edit webhooks.');
+ }
+
+ public function getEngineApplicationClass() {
+ return 'PhabricatorHeraldApplication';
+ }
+
+ protected function newEditableObject() {
+ $viewer = $this->getViewer();
+ return HeraldWebhook::initializeNewWebhook($viewer);
+ }
+
+ protected function newObjectQuery() {
+ return new HeraldWebhookQuery();
+ }
+
+ protected function getObjectCreateTitleText($object) {
+ return pht('Create Webhook');
+ }
+
+ protected function getObjectCreateButtonText($object) {
+ return pht('Create Webhook');
+ }
+
+ protected function getObjectEditTitleText($object) {
+ return pht('Edit Webhook: %s', $object->getName());
+ }
+
+ protected function getObjectEditShortText($object) {
+ return pht('Edit Webhook');
+ }
+
+ protected function getObjectCreateShortText() {
+ return pht('Create Webhook');
+ }
+
+ protected function getObjectName() {
+ return pht('Webhook');
+ }
+
+ protected function getEditorURI() {
+ return '/herald/webhook/edit/';
+ }
+
+ protected function getObjectCreateCancelURI($object) {
+ return '/herald/webhook/';
+ }
+
+ protected function getObjectViewURI($object) {
+ return $object->getURI();
+ }
+
+ protected function getCreateNewObjectPolicy() {
+ return $this->getApplication()->getPolicy(
+ HeraldCreateWebhooksCapability::CAPABILITY);
+ }
+
+ protected function buildCustomEditFields($object) {
+ return array(
+ id(new PhabricatorTextEditField())
+ ->setKey('name')
+ ->setLabel(pht('Name'))
+ ->setDescription(pht('Name of the webhook.'))
+ ->setTransactionType(HeraldWebhookNameTransaction::TRANSACTIONTYPE)
+ ->setIsRequired(true)
+ ->setValue($object->getName()),
+ id(new PhabricatorTextEditField())
+ ->setKey('uri')
+ ->setLabel(pht('URI'))
+ ->setDescription(pht('URI for the webhook.'))
+ ->setTransactionType(HeraldWebhookURITransaction::TRANSACTIONTYPE)
+ ->setIsRequired(true)
+ ->setValue($object->getWebhookURI()),
+ id(new PhabricatorSelectEditField())
+ ->setKey('status')
+ ->setLabel(pht('Status'))
+ ->setDescription(pht('Status mode for the webhook.'))
+ ->setTransactionType(HeraldWebhookStatusTransaction::TRANSACTIONTYPE)
+ ->setOptions(HeraldWebhook::getStatusDisplayNameMap())
+ ->setValue($object->getStatus()),
+
+ );
+ }
+
+}
diff --git a/src/applications/herald/editor/HeraldWebhookEditor.php b/src/applications/herald/editor/HeraldWebhookEditor.php
new file mode 100644
index 000000000..1f138e502
--- /dev/null
+++ b/src/applications/herald/editor/HeraldWebhookEditor.php
@@ -0,0 +1,31 @@
+<?php
+
+final class HeraldWebhookEditor
+ extends PhabricatorApplicationTransactionEditor {
+
+ public function getEditorApplicationClass() {
+ return 'PhabricatorHeraldApplication';
+ }
+
+ public function getEditorObjectsDescription() {
+ return pht('Webhooks');
+ }
+
+ public function getCreateObjectTitle($author, $object) {
+ return pht('%s created this webhook.', $author);
+ }
+
+ public function getCreateObjectTitleForFeed($author, $object) {
+ return pht('%s created %s.', $author, $object);
+ }
+
+ public function getTransactionTypes() {
+ $types = parent::getTransactionTypes();
+
+ $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
+ $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
+
+ return $types;
+ }
+
+}
diff --git a/src/applications/herald/field/HeraldActingUserField.php b/src/applications/herald/field/HeraldActingUserField.php
new file mode 100644
index 000000000..2245c7e9f
--- /dev/null
+++ b/src/applications/herald/field/HeraldActingUserField.php
@@ -0,0 +1,32 @@
+<?php
+
+final class HeraldActingUserField
+ extends HeraldField {
+
+ const FIELDCONST = 'herald.acting-user';
+
+ public function getHeraldFieldName() {
+ return pht('Acting user');
+ }
+
+ public function getHeraldFieldValue($object) {
+ return $this->getAdapter()->getActingAsPHID();
+ }
+
+ protected function getHeraldFieldStandardType() {
+ return self::STANDARD_PHID;
+ }
+
+ protected function getDatasource() {
+ return new PhabricatorPeopleDatasource();
+ }
+
+ public function supportsObject($object) {
+ return true;
+ }
+
+ public function getFieldGroupKey() {
+ return HeraldEditFieldGroup::FIELDGROUPKEY;
+ }
+
+}
diff --git a/src/applications/herald/garbagecollector/HeraldWebhookRequestGarbageCollector.php b/src/applications/herald/garbagecollector/HeraldWebhookRequestGarbageCollector.php
new file mode 100644
index 000000000..7626f70f0
--- /dev/null
+++ b/src/applications/herald/garbagecollector/HeraldWebhookRequestGarbageCollector.php
@@ -0,0 +1,29 @@
+<?php
+
+final class HeraldWebhookRequestGarbageCollector
+ extends PhabricatorGarbageCollector {
+
+ const COLLECTORCONST = 'herald.webhooks';
+
+ public function getCollectorName() {
+ return pht('Herald Webhooks');
+ }
+
+ public function getDefaultRetentionPolicy() {
+ return phutil_units('7 days in seconds');
+ }
+
+ protected function collectGarbage() {
+ $table = new HeraldWebhookRequest();
+ $conn_w = $table->establishConnection('w');
+
+ queryfx(
+ $conn_w,
+ 'DELETE FROM %T WHERE dateCreated < %d LIMIT 100',
+ $table->getTableName(),
+ $this->getGarbageEpoch());
+
+ return ($conn_w->getAffectedRows() == 100);
+ }
+
+}
diff --git a/src/applications/herald/management/HeraldWebhookCallManagementWorkflow.php b/src/applications/herald/management/HeraldWebhookCallManagementWorkflow.php
new file mode 100644
index 000000000..10249cca6
--- /dev/null
+++ b/src/applications/herald/management/HeraldWebhookCallManagementWorkflow.php
@@ -0,0 +1,106 @@
+<?php
+
+final class HeraldWebhookCallManagementWorkflow
+ extends HeraldWebhookManagementWorkflow {
+
+ protected function didConstruct() {
+ $this
+ ->setName('call')
+ ->setExamples('**call** --id __id__ [--object __object__]')
+ ->setSynopsis(pht('Call a webhook.'))
+ ->setArguments(
+ array(
+ array(
+ 'name' => 'id',
+ 'param' => 'id',
+ 'help' => pht('Webhook ID to call'),
+ ),
+ array(
+ 'name' => 'object',
+ 'param' => 'object',
+ 'help' => pht('Submit transactions for a particular object.'),
+ ),
+ array(
+ 'name' => 'silent',
+ 'help' => pht('Set the "silent" flag on the request.'),
+ ),
+ array(
+ 'name' => 'secure',
+ 'help' => pht('Set the "secure" flag on the request.'),
+ ),
+ ));
+ }
+
+ public function execute(PhutilArgumentParser $args) {
+ $viewer = $this->getViewer();
+
+ $id = $args->getArg('id');
+ if (!$id) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Specify a webhook to call with "--id".'));
+ }
+
+ $hook = id(new HeraldWebhookQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($id))
+ ->executeOne();
+ if (!$hook) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Unable to load specified webhook ("%s").',
+ $id));
+ }
+
+ $object_name = $args->getArg('object');
+ if ($object_name === null) {
+ $object = $hook;
+ } else {
+ $objects = id(new PhabricatorObjectQuery())
+ ->setViewer($viewer)
+ ->withNames(array($object_name))
+ ->execute();
+ if (!$objects) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Unable to load specified object ("%s").',
+ $object_name));
+ }
+ $object = head($objects);
+ }
+
+ $xaction_query =
+ PhabricatorApplicationTransactionQuery::newQueryForObject($object);
+
+ $xactions = $xaction_query
+ ->withObjectPHIDs(array($object->getPHID()))
+ ->setViewer($viewer)
+ ->setLimit(10)
+ ->execute();
+
+ $application_phid = id(new PhabricatorHeraldApplication())->getPHID();
+
+ $request = HeraldWebhookRequest::initializeNewWebhookRequest($hook)
+ ->setObjectPHID($object->getPHID())
+ ->setIsTestAction(true)
+ ->setIsSilentAction((bool)$args->getArg('silent'))
+ ->setIsSecureAction((bool)$args->getArg('secure'))
+ ->setTriggerPHIDs(array($application_phid))
+ ->setTransactionPHIDs(mpull($xactions, 'getPHID'))
+ ->save();
+
+ PhabricatorWorker::setRunAllTasksInProcess(true);
+ $request->queueCall();
+
+ $request->reload();
+
+ echo tsprintf(
+ "%s\n",
+ pht(
+ 'Success, got HTTP %s from webhook.',
+ $request->getErrorCode()));
+
+ return 0;
+ }
+
+}
diff --git a/src/applications/herald/management/HeraldWebhookManagementWorkflow.php b/src/applications/herald/management/HeraldWebhookManagementWorkflow.php
new file mode 100644
index 000000000..4bbfda79f
--- /dev/null
+++ b/src/applications/herald/management/HeraldWebhookManagementWorkflow.php
@@ -0,0 +1,4 @@
+<?php
+
+abstract class HeraldWebhookManagementWorkflow
+ extends PhabricatorManagementWorkflow {}
diff --git a/src/applications/herald/phid/HeraldWebhookPHIDType.php b/src/applications/herald/phid/HeraldWebhookPHIDType.php
new file mode 100644
index 000000000..bf16e22b1
--- /dev/null
+++ b/src/applications/herald/phid/HeraldWebhookPHIDType.php
@@ -0,0 +1,49 @@
+<?php
+
+final class HeraldWebhookPHIDType extends PhabricatorPHIDType {
+
+ const TYPECONST = 'HWBH';
+
+ public function getTypeName() {
+ return pht('Webhook');
+ }
+
+ public function newObject() {
+ return new HeraldWebhook();
+ }
+
+ public function getPHIDTypeApplicationClass() {
+ return 'PhabricatorHeraldApplication';
+ }
+
+ protected function buildQueryForObjects(
+ PhabricatorObjectQuery $query,
+ array $phids) {
+
+ return id(new HeraldWebhookQuery())
+ ->withPHIDs($phids);
+ }
+
+ public function loadHandles(
+ PhabricatorHandleQuery $query,
+ array $handles,
+ array $objects) {
+
+ foreach ($handles as $phid => $handle) {
+ $hook = $objects[$phid];
+
+ $name = $hook->getName();
+ $id = $hook->getID();
+
+ $handle
+ ->setName($name)
+ ->setURI($hook->getURI())
+ ->setFullName(pht('Webhook %d %s', $id, $name));
+
+ if ($hook->isDisabled()) {
+ $handle->setStatus(PhabricatorObjectHandle::STATUS_CLOSED);
+ }
+ }
+ }
+
+}
diff --git a/src/applications/harbormaster/phid/HarbormasterBuildLogPHIDType.php b/src/applications/herald/phid/HeraldWebhookRequestPHIDType.php
similarity index 55%
copy from src/applications/harbormaster/phid/HarbormasterBuildLogPHIDType.php
copy to src/applications/herald/phid/HeraldWebhookRequestPHIDType.php
index c0fba81c4..bcf5afb0d 100644
--- a/src/applications/harbormaster/phid/HarbormasterBuildLogPHIDType.php
+++ b/src/applications/herald/phid/HeraldWebhookRequestPHIDType.php
@@ -1,37 +1,38 @@
<?php
-final class HarbormasterBuildLogPHIDType extends PhabricatorPHIDType {
+final class HeraldWebhookRequestPHIDType extends PhabricatorPHIDType {
- const TYPECONST = 'HMCL';
+ const TYPECONST = 'HWBR';
public function getTypeName() {
- return pht('Build Log');
+ return pht('Webhook Request');
}
public function newObject() {
- return new HarbormasterBuildLog();
+ return new HeraldWebhook();
}
public function getPHIDTypeApplicationClass() {
- return 'PhabricatorHarbormasterApplication';
+ return 'PhabricatorHeraldApplication';
}
protected function buildQueryForObjects(
PhabricatorObjectQuery $query,
array $phids) {
- return id(new HarbormasterBuildLogQuery())
+ return id(new HeraldWebhookRequestQuery())
->withPHIDs($phids);
}
public function loadHandles(
PhabricatorHandleQuery $query,
array $handles,
array $objects) {
foreach ($handles as $phid => $handle) {
- $build_log = $objects[$phid];
+ $request = $objects[$phid];
+ $handle->setName(pht('Webhook Request %d', $request->getID()));
}
}
}
diff --git a/src/applications/herald/query/HeraldWebhookQuery.php b/src/applications/herald/query/HeraldWebhookQuery.php
new file mode 100644
index 000000000..ca4688061
--- /dev/null
+++ b/src/applications/herald/query/HeraldWebhookQuery.php
@@ -0,0 +1,64 @@
+<?php
+
+final class HeraldWebhookQuery
+ extends PhabricatorCursorPagedPolicyAwareQuery {
+
+ private $ids;
+ private $phids;
+ private $statuses;
+
+ public function withIDs(array $ids) {
+ $this->ids = $ids;
+ return $this;
+ }
+
+ public function withPHIDs(array $phids) {
+ $this->phids = $phids;
+ return $this;
+ }
+
+ public function withStatuses(array $statuses) {
+ $this->statuses = $statuses;
+ return $this;
+ }
+
+ public function newResultObject() {
+ return new HeraldWebhook();
+ }
+
+ protected function loadPage() {
+ return $this->loadStandardPage($this->newResultObject());
+ }
+
+ protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
+ $where = parent::buildWhereClauseParts($conn);
+
+ if ($this->ids !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'id IN (%Ld)',
+ $this->ids);
+ }
+
+ if ($this->phids !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'phid IN (%Ls)',
+ $this->phids);
+ }
+
+ if ($this->statuses !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'status IN (%Ls)',
+ $this->statuses);
+ }
+
+ return $where;
+ }
+
+ public function getQueryApplicationClass() {
+ return 'PhabricatorHeraldApplication';
+ }
+
+}
diff --git a/src/applications/herald/query/HeraldWebhookRequestQuery.php b/src/applications/herald/query/HeraldWebhookRequestQuery.php
new file mode 100644
index 000000000..4c71d48e0
--- /dev/null
+++ b/src/applications/herald/query/HeraldWebhookRequestQuery.php
@@ -0,0 +1,126 @@
+<?php
+
+final class HeraldWebhookRequestQuery
+ extends PhabricatorCursorPagedPolicyAwareQuery {
+
+ private $ids;
+ private $phids;
+ private $webhookPHIDs;
+ private $lastRequestEpochMin;
+ private $lastRequestEpochMax;
+ private $lastRequestResults;
+
+ public function withIDs(array $ids) {
+ $this->ids = $ids;
+ return $this;
+ }
+
+ public function withPHIDs(array $phids) {
+ $this->phids = $phids;
+ return $this;
+ }
+
+ public function withWebhookPHIDs(array $phids) {
+ $this->webhookPHIDs = $phids;
+ return $this;
+ }
+
+ public function newResultObject() {
+ return new HeraldWebhookRequest();
+ }
+
+ protected function loadPage() {
+ return $this->loadStandardPage($this->newResultObject());
+ }
+
+ public function withLastRequestEpochBetween($epoch_min, $epoch_max) {
+ $this->lastRequestEpochMin = $epoch_min;
+ $this->lastRequestEpochMax = $epoch_max;
+ return $this;
+ }
+
+ public function withLastRequestResults(array $results) {
+ $this->lastRequestResults = $results;
+ return $this;
+ }
+
+ 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->webhookPHIDs !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'webhookPHID IN (%Ls)',
+ $this->webhookPHIDs);
+ }
+
+ if ($this->lastRequestEpochMin !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'lastRequestEpoch >= %d',
+ $this->lastRequestEpochMin);
+ }
+
+ if ($this->lastRequestEpochMax !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'lastRequestEpoch <= %d',
+ $this->lastRequestEpochMax);
+ }
+
+ if ($this->lastRequestResults !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'lastRequestResult IN (%Ls)',
+ $this->lastRequestResults);
+ }
+
+ return $where;
+ }
+
+ protected function willFilterPage(array $requests) {
+ $hook_phids = mpull($requests, 'getWebhookPHID');
+
+ $hooks = id(new HeraldWebhookQuery())
+ ->setViewer($this->getViewer())
+ ->setParentQuery($this)
+ ->withPHIDs($hook_phids)
+ ->execute();
+ $hooks = mpull($hooks, null, 'getPHID');
+
+ foreach ($requests as $key => $request) {
+ $hook_phid = $request->getWebhookPHID();
+ $hook = idx($hooks, $hook_phid);
+
+ if (!$hook) {
+ unset($requests[$key]);
+ $this->didRejectResult($request);
+ continue;
+ }
+
+ $request->attachWebhook($hook);
+ }
+
+ return $requests;
+ }
+
+
+ public function getQueryApplicationClass() {
+ return 'PhabricatorHeraldApplication';
+ }
+
+}
diff --git a/src/applications/herald/query/HeraldWebhookSearchEngine.php b/src/applications/herald/query/HeraldWebhookSearchEngine.php
new file mode 100644
index 000000000..84997b60b
--- /dev/null
+++ b/src/applications/herald/query/HeraldWebhookSearchEngine.php
@@ -0,0 +1,102 @@
+<?php
+
+final class HeraldWebhookSearchEngine
+ extends PhabricatorApplicationSearchEngine {
+
+ public function getResultTypeDescription() {
+ return pht('Webhooks');
+ }
+
+ public function getApplicationClassName() {
+ return 'PhabricatorHeraldApplication';
+ }
+
+ public function newQuery() {
+ return new HeraldWebhookQuery();
+ }
+
+ protected function buildQueryFromParameters(array $map) {
+ $query = $this->newQuery();
+
+ if ($map['statuses']) {
+ $query->withStatuses($map['statuses']);
+ }
+
+ return $query;
+ }
+
+ protected function buildCustomSearchFields() {
+ return array(
+ id(new PhabricatorSearchCheckboxesField())
+ ->setKey('statuses')
+ ->setLabel(pht('Status'))
+ ->setDescription(
+ pht('Search for archived or active pastes.'))
+ ->setOptions(HeraldWebhook::getStatusDisplayNameMap()),
+ );
+ }
+
+ protected function getURI($path) {
+ return '/herald/webhook/'.$path;
+ }
+
+ protected function getBuiltinQueryNames() {
+ $names = array();
+
+ $names['active'] = pht('Active');
+ $names['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(
+ HeraldWebhook::HOOKSTATUS_FIREHOSE,
+ HeraldWebhook::HOOKSTATUS_ENABLED,
+ ));
+ }
+
+ return parent::buildSavedQueryFromBuiltin($query_key);
+ }
+
+ protected function renderResultList(
+ array $hooks,
+ PhabricatorSavedQuery $query,
+ array $handles) {
+ assert_instances_of($hooks, 'HeraldWebhook');
+
+ $viewer = $this->requireViewer();
+
+ $list = id(new PHUIObjectItemListView())
+ ->setViewer($viewer);
+ foreach ($hooks as $hook) {
+ $item = id(new PHUIObjectItemView())
+ ->setObjectName(pht('Webhook %d', $hook->getID()))
+ ->setHeader($hook->getName())
+ ->setHref($hook->getURI())
+ ->addAttribute($hook->getWebhookURI());
+
+ $item->addIcon($hook->getStatusIcon(), $hook->getStatusDisplayName());
+
+ if ($hook->isDisabled()) {
+ $item->setDisabled(true);
+ }
+
+ $list->addItem($item);
+ }
+
+ return id(new PhabricatorApplicationSearchResultView())
+ ->setObjectList($list)
+ ->setNoDataString(pht('No webhooks found.'));
+ }
+
+}
diff --git a/src/applications/herald/query/HeraldWebhookTransactionQuery.php b/src/applications/herald/query/HeraldWebhookTransactionQuery.php
new file mode 100644
index 000000000..b812305e5
--- /dev/null
+++ b/src/applications/herald/query/HeraldWebhookTransactionQuery.php
@@ -0,0 +1,10 @@
+<?php
+
+final class HeraldWebhookTransactionQuery
+ extends PhabricatorApplicationTransactionQuery {
+
+ public function getTemplateApplicationTransaction() {
+ return new HeraldWebhookTransaction();
+ }
+
+}
diff --git a/src/applications/herald/storage/HeraldWebhook.php b/src/applications/herald/storage/HeraldWebhook.php
new file mode 100644
index 000000000..05ec69e19
--- /dev/null
+++ b/src/applications/herald/storage/HeraldWebhook.php
@@ -0,0 +1,244 @@
+<?php
+
+final class HeraldWebhook
+ extends HeraldDAO
+ implements
+ PhabricatorPolicyInterface,
+ PhabricatorApplicationTransactionInterface,
+ PhabricatorDestructibleInterface,
+ PhabricatorProjectInterface {
+
+ protected $name;
+ protected $webhookURI;
+ protected $viewPolicy;
+ protected $editPolicy;
+ protected $status;
+ protected $hmacKey;
+
+ const HOOKSTATUS_FIREHOSE = 'firehose';
+ const HOOKSTATUS_ENABLED = 'enabled';
+ const HOOKSTATUS_DISABLED = 'disabled';
+
+ protected function getConfiguration() {
+ return array(
+ self::CONFIG_AUX_PHID => true,
+ self::CONFIG_COLUMN_SCHEMA => array(
+ 'name' => 'text128',
+ 'webhookURI' => 'text255',
+ 'status' => 'text32',
+ 'hmacKey' => 'text32',
+ ),
+ self::CONFIG_KEY_SCHEMA => array(
+ 'key_status' => array(
+ 'columns' => array('status'),
+ ),
+ ),
+ ) + parent::getConfiguration();
+ }
+
+ public function getPHIDType() {
+ return HeraldWebhookPHIDType::TYPECONST;
+ }
+
+ public static function initializeNewWebhook(PhabricatorUser $viewer) {
+ return id(new self())
+ ->setStatus(self::HOOKSTATUS_ENABLED)
+ ->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy())
+ ->setEditPolicy($viewer->getPHID())
+ ->regenerateHMACKey();
+ }
+
+ public function getURI() {
+ return '/herald/webhook/view/'.$this->getID().'/';
+ }
+
+ public function isDisabled() {
+ return ($this->getStatus() === self::HOOKSTATUS_DISABLED);
+ }
+
+ public static function getStatusDisplayNameMap() {
+ $specs = self::getStatusSpecifications();
+ return ipull($specs, 'name', 'key');
+ }
+
+ private static function getStatusSpecifications() {
+ $specs = array(
+ array(
+ 'key' => self::HOOKSTATUS_FIREHOSE,
+ 'name' => pht('Firehose'),
+ 'color' => 'orange',
+ 'icon' => 'fa-star-o',
+ ),
+ array(
+ 'key' => self::HOOKSTATUS_ENABLED,
+ 'name' => pht('Enabled'),
+ 'color' => 'bluegrey',
+ 'icon' => 'fa-check',
+ ),
+ array(
+ 'key' => self::HOOKSTATUS_DISABLED,
+ 'name' => pht('Disabled'),
+ 'color' => 'dark',
+ 'icon' => 'fa-ban',
+ ),
+ );
+
+ return ipull($specs, null, 'key');
+ }
+
+
+ private static function getSpecificationForStatus($status) {
+ $specs = self::getStatusSpecifications();
+
+ if (isset($specs[$status])) {
+ return $specs[$status];
+ }
+
+ return array(
+ 'key' => $status,
+ 'name' => pht('Unknown ("%s")', $status),
+ 'icon' => 'fa-question',
+ 'color' => 'indigo',
+ );
+ }
+
+ public static function getDisplayNameForStatus($status) {
+ $spec = self::getSpecificationForStatus($status);
+ return $spec['name'];
+ }
+
+ public static function getIconForStatus($status) {
+ $spec = self::getSpecificationForStatus($status);
+ return $spec['icon'];
+ }
+
+ public static function getColorForStatus($status) {
+ $spec = self::getSpecificationForStatus($status);
+ return $spec['color'];
+ }
+
+ public function getStatusDisplayName() {
+ $status = $this->getStatus();
+ return self::getDisplayNameForStatus($status);
+ }
+
+ public function getStatusIcon() {
+ $status = $this->getStatus();
+ return self::getIconForStatus($status);
+ }
+
+ public function getStatusColor() {
+ $status = $this->getStatus();
+ return self::getColorForStatus($status);
+ }
+
+ public function getErrorBackoffWindow() {
+ return phutil_units('5 minutes in seconds');
+ }
+
+ public function getErrorBackoffThreshold() {
+ return 10;
+ }
+
+ public function isInErrorBackoff(PhabricatorUser $viewer) {
+ $backoff_window = $this->getErrorBackoffWindow();
+ $backoff_threshold = $this->getErrorBackoffThreshold();
+
+ $now = PhabricatorTime::getNow();
+
+ $window_start = ($now - $backoff_window);
+
+ $requests = id(new HeraldWebhookRequestQuery())
+ ->setViewer($viewer)
+ ->withWebhookPHIDs(array($this->getPHID()))
+ ->withLastRequestEpochBetween($window_start, null)
+ ->withLastRequestResults(
+ array(
+ HeraldWebhookRequest::RESULT_FAIL,
+ ))
+ ->execute();
+
+ if (count($requests) >= $backoff_threshold) {
+ return true;
+ }
+
+ return false;
+ }
+
+ public function regenerateHMACKey() {
+ return $this->setHMACKey(Filesystem::readRandomCharacters(32));
+ }
+
+/* -( 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;
+ }
+
+
+/* -( PhabricatorApplicationTransactionInterface )------------------------- */
+
+
+ public function getApplicationTransactionEditor() {
+ return new HeraldWebhookEditor();
+ }
+
+ public function getApplicationTransactionObject() {
+ return $this;
+ }
+
+ public function getApplicationTransactionTemplate() {
+ return new HeraldWebhookTransaction();
+ }
+
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+ return $timeline;
+ }
+
+
+/* -( PhabricatorDestructibleInterface )----------------------------------- */
+
+
+ public function destroyObjectPermanently(
+ PhabricatorDestructionEngine $engine) {
+
+ while (true) {
+ $requests = id(new HeraldWebhookRequestQuery())
+ ->setViewer($engine->getViewer())
+ ->withWebhookPHIDs(array($this->getPHID()))
+ ->setLimit(100)
+ ->execute();
+
+ if (!$requests) {
+ break;
+ }
+
+ foreach ($requests as $request) {
+ $request->delete();
+ }
+ }
+
+ $this->delete();
+ }
+
+
+}
diff --git a/src/applications/herald/storage/HeraldWebhookRequest.php b/src/applications/herald/storage/HeraldWebhookRequest.php
new file mode 100644
index 000000000..5db5b2916
--- /dev/null
+++ b/src/applications/herald/storage/HeraldWebhookRequest.php
@@ -0,0 +1,223 @@
+<?php
+
+final class HeraldWebhookRequest
+ extends HeraldDAO
+ implements
+ PhabricatorPolicyInterface,
+ PhabricatorExtendedPolicyInterface {
+
+ protected $webhookPHID;
+ protected $objectPHID;
+ protected $status;
+ protected $properties = array();
+ protected $lastRequestResult;
+ protected $lastRequestEpoch;
+
+ private $webhook = self::ATTACHABLE;
+
+ const RETRY_NEVER = 'never';
+ const RETRY_FOREVER = 'forever';
+
+ const STATUS_QUEUED = 'queued';
+ const STATUS_FAILED = 'failed';
+ const STATUS_SENT = 'sent';
+
+ const RESULT_NONE = 'none';
+ const RESULT_OKAY = 'okay';
+ const RESULT_FAIL = 'fail';
+
+ protected function getConfiguration() {
+ return array(
+ self::CONFIG_AUX_PHID => true,
+ self::CONFIG_SERIALIZATION => array(
+ 'properties' => self::SERIALIZATION_JSON,
+ ),
+ self::CONFIG_COLUMN_SCHEMA => array(
+ 'status' => 'text32',
+ 'lastRequestResult' => 'text32',
+ 'lastRequestEpoch' => 'epoch',
+ ),
+ self::CONFIG_KEY_SCHEMA => array(
+ 'key_ratelimit' => array(
+ 'columns' => array(
+ 'webhookPHID',
+ 'lastRequestResult',
+ 'lastRequestEpoch',
+ ),
+ ),
+ 'key_collect' => array(
+ 'columns' => array('dateCreated'),
+ ),
+ ),
+ ) + parent::getConfiguration();
+ }
+
+ public function getPHIDType() {
+ return HeraldWebhookRequestPHIDType::TYPECONST;
+ }
+
+ public static function initializeNewWebhookRequest(HeraldWebhook $hook) {
+ return id(new self())
+ ->setWebhookPHID($hook->getPHID())
+ ->attachWebhook($hook)
+ ->setStatus(self::STATUS_QUEUED)
+ ->setRetryMode(self::RETRY_NEVER)
+ ->setLastRequestResult(self::RESULT_NONE)
+ ->setLastRequestEpoch(0);
+ }
+
+ public function getWebhook() {
+ return $this->assertAttached($this->webhook);
+ }
+
+ public function attachWebhook(HeraldWebhook $hook) {
+ $this->webhook = $hook;
+ return $this;
+ }
+
+ protected function setProperty($key, $value) {
+ $this->properties[$key] = $value;
+ return $this;
+ }
+
+ protected function getProperty($key, $default = null) {
+ return idx($this->properties, $key, $default);
+ }
+
+ public function setRetryMode($mode) {
+ return $this->setProperty('retry', $mode);
+ }
+
+ public function getRetryMode() {
+ return $this->getProperty('retry');
+ }
+
+ public function setErrorType($error_type) {
+ return $this->setProperty('errorType', $error_type);
+ }
+
+ public function getErrorType() {
+ return $this->getProperty('errorType');
+ }
+
+ public function setErrorCode($error_code) {
+ return $this->setProperty('errorCode', $error_code);
+ }
+
+ public function getErrorCode() {
+ return $this->getProperty('errorCode');
+ }
+
+ public function setTransactionPHIDs(array $phids) {
+ return $this->setProperty('transactionPHIDs', $phids);
+ }
+
+ public function getTransactionPHIDs() {
+ return $this->getProperty('transactionPHIDs', array());
+ }
+
+ public function setTriggerPHIDs(array $phids) {
+ return $this->setProperty('triggerPHIDs', $phids);
+ }
+
+ public function getTriggerPHIDs() {
+ return $this->getProperty('triggerPHIDs', array());
+ }
+
+ public function setIsSilentAction($bool) {
+ return $this->setProperty('silent', $bool);
+ }
+
+ public function getIsSilentAction() {
+ return $this->getProperty('silent', false);
+ }
+
+ public function setIsTestAction($bool) {
+ return $this->setProperty('test', $bool);
+ }
+
+ public function getIsTestAction() {
+ return $this->getProperty('test', false);
+ }
+
+ public function setIsSecureAction($bool) {
+ return $this->setProperty('secure', $bool);
+ }
+
+ public function getIsSecureAction() {
+ return $this->getProperty('secure', false);
+ }
+
+ public function queueCall() {
+ PhabricatorWorker::scheduleTask(
+ 'HeraldWebhookWorker',
+ array(
+ 'webhookRequestPHID' => $this->getPHID(),
+ ),
+ array(
+ 'objectPHID' => $this->getPHID(),
+ ));
+
+ return $this;
+ }
+
+ public function newStatusIcon() {
+ switch ($this->getStatus()) {
+ case self::STATUS_QUEUED:
+ $icon = 'fa-refresh';
+ $color = 'blue';
+ $tooltip = pht('Queued');
+ break;
+ case self::STATUS_SENT:
+ $icon = 'fa-check';
+ $color = 'green';
+ $tooltip = pht('Sent');
+ break;
+ case self::STATUS_FAILED:
+ default:
+ $icon = 'fa-times';
+ $color = 'red';
+ $tooltip = pht('Failed');
+ break;
+
+ }
+
+ return id(new PHUIIconView())
+ ->setIcon($icon, $color)
+ ->setTooltip($tooltip);
+ }
+
+
+/* -( PhabricatorPolicyInterface )----------------------------------------- */
+
+
+ public function getCapabilities() {
+ return array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ );
+ }
+
+ public function getPolicy($capability) {
+ switch ($capability) {
+ case PhabricatorPolicyCapability::CAN_VIEW:
+ return PhabricatorPolicies::getMostOpenPolicy();
+ }
+ }
+
+ public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
+ return false;
+ }
+
+
+/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
+
+
+ public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
+ return array(
+ array($this->getWebhook(), PhabricatorPolicyCapability::CAN_VIEW),
+ );
+ }
+
+
+
+}
diff --git a/src/applications/herald/storage/HeraldWebhookTransaction.php b/src/applications/herald/storage/HeraldWebhookTransaction.php
new file mode 100644
index 000000000..03c8cbb77
--- /dev/null
+++ b/src/applications/herald/storage/HeraldWebhookTransaction.php
@@ -0,0 +1,22 @@
+<?php
+
+final class HeraldWebhookTransaction
+ extends PhabricatorModularTransaction {
+
+ public function getApplicationName() {
+ return 'herald';
+ }
+
+ public function getApplicationTransactionType() {
+ return HeraldWebhookPHIDType::TYPECONST;
+ }
+
+ public function getApplicationTransactionCommentObject() {
+ return null;
+ }
+
+ public function getBaseTransactionClass() {
+ return 'HeraldWebhookTransactionType';
+ }
+
+}
diff --git a/src/applications/herald/typeahead/HeraldWebhookDatasource.php b/src/applications/herald/typeahead/HeraldWebhookDatasource.php
new file mode 100644
index 000000000..a66431d51
--- /dev/null
+++ b/src/applications/herald/typeahead/HeraldWebhookDatasource.php
@@ -0,0 +1,48 @@
+<?php
+
+final class HeraldWebhookDatasource
+ extends PhabricatorTypeaheadDatasource {
+
+ public function getPlaceholderText() {
+ return pht('Type a webhook name...');
+ }
+
+ public function getBrowseTitle() {
+ return pht('Browse Webhooks');
+ }
+
+ public function getDatasourceApplicationClass() {
+ return 'PhabricatorHeraldApplication';
+ }
+
+ public function loadResults() {
+ $viewer = $this->getViewer();
+ $raw_query = $this->getRawQuery();
+
+ $hooks = id(new HeraldWebhookQuery())
+ ->setViewer($viewer)
+ ->execute();
+
+ $handles = id(new PhabricatorHandleQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(mpull($hooks, 'getPHID'))
+ ->execute();
+
+ $results = array();
+ foreach ($hooks as $hook) {
+ $handle = $handles[$hook->getPHID()];
+
+ $result = id(new PhabricatorTypeaheadResult())
+ ->setName($handle->getFullName())
+ ->setPHID($handle->getPHID());
+
+ if ($hook->isDisabled()) {
+ $result->setClosed(pht('Disabled'));
+ }
+
+ $results[] = $result;
+ }
+
+ return $results;
+ }
+}
diff --git a/src/applications/herald/view/HeraldWebhookRequestListView.php b/src/applications/herald/view/HeraldWebhookRequestListView.php
new file mode 100644
index 000000000..4e0f6510b
--- /dev/null
+++ b/src/applications/herald/view/HeraldWebhookRequestListView.php
@@ -0,0 +1,88 @@
+<?php
+
+final class HeraldWebhookRequestListView
+ extends AphrontView {
+
+ private $requests;
+ private $highlightID;
+
+ public function setRequests(array $requests) {
+ assert_instances_of($requests, 'HeraldWebhookRequest');
+ $this->requests = $requests;
+ return $this;
+ }
+
+ public function setHighlightID($highlight_id) {
+ $this->highlightID = $highlight_id;
+ return $this;
+ }
+
+ public function getHighlightID() {
+ return $this->highlightID;
+ }
+
+ public function render() {
+ $viewer = $this->getViewer();
+ $requests = $this->requests;
+
+ $handle_phids = array();
+ foreach ($requests as $request) {
+ $handle_phids[] = $request->getObjectPHID();
+ }
+ $handles = $viewer->loadHandles($handle_phids);
+
+ $highlight_id = $this->getHighlightID();
+
+ $rows = array();
+ $rowc = array();
+ foreach ($requests as $request) {
+ $icon = $request->newStatusIcon();
+
+ if ($highlight_id == $request->getID()) {
+ $rowc[] = 'highlighted';
+ } else {
+ $rowc[] = null;
+ }
+
+ $last_epoch = $request->getLastRequestEpoch();
+ if ($request->getLastRequestEpoch()) {
+ $last_request = phabricator_datetime($last_epoch, $viewer);
+ } else {
+ $last_request = null;
+ }
+
+ $rows[] = array(
+ $request->getID(),
+ $icon,
+ $handles[$request->getObjectPHID()]->renderLink(),
+ $request->getErrorType(),
+ $request->getErrorCode(),
+ $last_request,
+ );
+ }
+
+ $table = id(new AphrontTableView($rows))
+ ->setRowClasses($rowc)
+ ->setHeaders(
+ array(
+ pht('ID'),
+ '',
+ pht('Object'),
+ pht('Type'),
+ pht('Code'),
+ pht('Requested At'),
+ ))
+ ->setColumnClasses(
+ array(
+ 'n',
+ '',
+ 'wide',
+ '',
+ '',
+ '',
+ ));
+
+ return $table;
+ }
+
+}
diff --git a/src/applications/herald/worker/HeraldWebhookWorker.php b/src/applications/herald/worker/HeraldWebhookWorker.php
new file mode 100644
index 000000000..150f98fd5
--- /dev/null
+++ b/src/applications/herald/worker/HeraldWebhookWorker.php
@@ -0,0 +1,250 @@
+<?php
+
+final class HeraldWebhookWorker
+ extends PhabricatorWorker {
+
+ protected function doWork() {
+ $viewer = PhabricatorUser::getOmnipotentUser();
+
+ $data = $this->getTaskData();
+ $request_phid = idx($data, 'webhookRequestPHID');
+
+ $request = id(new HeraldWebhookRequestQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(array($request_phid))
+ ->executeOne();
+ if (!$request) {
+ throw new PhabricatorWorkerPermanentFailureException(
+ pht(
+ 'Unable to load webhook request ("%s"). It may have been '.
+ 'garbage collected.',
+ $request_phid));
+ }
+
+ $status = $request->getStatus();
+ if ($status !== HeraldWebhookRequest::STATUS_QUEUED) {
+ throw new PhabricatorWorkerPermanentFailureException(
+ pht(
+ 'Webhook request ("%s") is not in "%s" status (actual '.
+ 'status is "%s"). Declining call to hook.',
+ $request_phid,
+ HeraldWebhookRequest::STATUS_QUEUED,
+ $status));
+ }
+
+ // If we're in silent mode, permanently fail the webhook request and then
+ // return to complete this task.
+ if (PhabricatorEnv::getEnvConfig('phabricator.silent')) {
+ $this->failRequest($request, 'hook', 'silent');
+ return;
+ }
+
+ $hook = $request->getWebhook();
+
+ if ($hook->isDisabled()) {
+ $this->failRequest($request, 'hook', 'disabled');
+ throw new PhabricatorWorkerPermanentFailureException(
+ pht(
+ 'Associated hook ("%s") for webhook request ("%s") is disabled.',
+ $hook->getPHID(),
+ $request_phid));
+ }
+
+ $uri = $hook->getWebhookURI();
+ try {
+ PhabricatorEnv::requireValidRemoteURIForFetch(
+ $uri,
+ array(
+ 'http',
+ 'https',
+ ));
+ } catch (Exception $ex) {
+ $this->failRequest($request, 'hook', 'uri');
+ throw new PhabricatorWorkerPermanentFailureException(
+ pht(
+ 'Associated hook ("%s") for webhook request ("%s") has invalid '.
+ 'fetch URI: %s',
+ $hook->getPHID(),
+ $request_phid,
+ $ex->getMessage()));
+ }
+
+ $object_phid = $request->getObjectPHID();
+
+ $object = id(new PhabricatorObjectQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(array($object_phid))
+ ->executeOne();
+ if (!$object) {
+ $this->failRequest($request, 'hook', 'object');
+ throw new PhabricatorWorkerPermanentFailureException(
+ pht(
+ 'Unable to load object ("%s") for webhook request ("%s").',
+ $object_phid,
+ $request_phid));
+ }
+
+ $xaction_query = PhabricatorApplicationTransactionQuery::newQueryForObject(
+ $object);
+ $xaction_phids = $request->getTransactionPHIDs();
+ if ($xaction_phids) {
+ $xactions = $xaction_query
+ ->setViewer($viewer)
+ ->withObjectPHIDs(array($object_phid))
+ ->withPHIDs($xaction_phids)
+ ->execute();
+ $xactions = mpull($xactions, null, 'getPHID');
+ } else {
+ $xactions = array();
+ }
+
+ // To prevent thundering herd issues for high volume webhooks (where
+ // a large number of workers might try to work through a request backlog
+ // simultaneously, before the error backoff can catch up), we never
+ // parallelize requests to a particular webhook.
+
+ $lock_key = 'webhook('.$hook->getPHID().')';
+ $lock = PhabricatorGlobalLock::newLock($lock_key);
+
+ try {
+ $lock->lock();
+ } catch (Exception $ex) {
+ phlog($ex);
+ throw new PhabricatorWorkerYieldException(15);
+ }
+
+ $caught = null;
+ try {
+ $this->callWebhookWithLock($hook, $request, $object, $xactions);
+ } catch (Exception $ex) {
+ $caught = $ex;
+ }
+
+ $lock->unlock();
+
+ if ($caught) {
+ throw $caught;
+ }
+ }
+
+ private function callWebhookWithLock(
+ HeraldWebhook $hook,
+ HeraldWebhookRequest $request,
+ $object,
+ array $xactions) {
+ $viewer = PhabricatorUser::getOmnipotentUser();
+
+ if ($hook->isInErrorBackoff($viewer)) {
+ throw new PhabricatorWorkerYieldException($hook->getErrorBackoffWindow());
+ }
+
+ $xaction_data = array();
+ foreach ($xactions as $xaction) {
+ $xaction_data[] = array(
+ 'phid' => $xaction->getPHID(),
+ );
+ }
+
+ $trigger_data = array();
+ foreach ($request->getTriggerPHIDs() as $trigger_phid) {
+ $trigger_data[] = array(
+ 'phid' => $trigger_phid,
+ );
+ }
+
+ $payload = array(
+ 'object' => array(
+ 'type' => phid_get_type($object->getPHID()),
+ 'phid' => $object->getPHID(),
+ ),
+ 'triggers' => $trigger_data,
+ 'action' => array(
+ 'test' => $request->getIsTestAction(),
+ 'silent' => $request->getIsSilentAction(),
+ 'secure' => $request->getIsSecureAction(),
+ 'epoch' => (int)$request->getDateCreated(),
+ ),
+ 'transactions' => $xaction_data,
+ );
+
+ $payload = id(new PhutilJSON())->encodeFormatted($payload);
+ $key = $hook->getHmacKey();
+ $signature = PhabricatorHash::digestHMACSHA256($payload, $key);
+ $uri = $hook->getWebhookURI();
+
+ $future = id(new HTTPSFuture($uri))
+ ->setMethod('POST')
+ ->addHeader('Content-Type', 'application/json')
+ ->addHeader('X-Phabricator-Webhook-Signature', $signature)
+ ->setTimeout(15)
+ ->setData($payload);
+
+ list($status) = $future->resolve();
+
+ if ($status->isTimeout()) {
+ $error_type = 'timeout';
+ } else {
+ $error_type = 'http';
+ }
+ $error_code = $status->getStatusCode();
+
+ $request
+ ->setErrorType($error_type)
+ ->setErrorCode($error_code)
+ ->setLastRequestEpoch(PhabricatorTime::getNow());
+
+ $retry_forever = HeraldWebhookRequest::RETRY_FOREVER;
+ if ($status->isTimeout() || $status->isError()) {
+ $should_retry = ($request->getRetryMode() === $retry_forever);
+
+ $request
+ ->setLastRequestResult(HeraldWebhookRequest::RESULT_FAIL);
+
+ if ($should_retry) {
+ $request->save();
+
+ throw new Exception(
+ pht(
+ 'Webhook request ("%s", to "%s") failed (%s / %s). The request '.
+ 'will be retried.',
+ $request->getPHID(),
+ $uri,
+ $error_type,
+ $error_code));
+ } else {
+ $request
+ ->setStatus(HeraldWebhookRequest::STATUS_FAILED)
+ ->save();
+
+ throw new PhabricatorWorkerPermanentFailureException(
+ pht(
+ 'Webhook request ("%s", to "%s") failed (%s / %s). The request '.
+ 'will not be retried.',
+ $request->getPHID(),
+ $uri,
+ $error_type,
+ $error_code));
+ }
+ } else {
+ $request
+ ->setLastRequestResult(HeraldWebhookRequest::RESULT_OKAY)
+ ->setStatus(HeraldWebhookRequest::STATUS_SENT)
+ ->save();
+ }
+ }
+
+ private function failRequest(
+ HeraldWebhookRequest $request,
+ $error_type,
+ $error_code) {
+
+ $request
+ ->setStatus(HeraldWebhookRequest::STATUS_FAILED)
+ ->setErrorType($error_type)
+ ->setErrorCode($error_code)
+ ->setLastRequestResult(HeraldWebhookRequest::RESULT_NONE)
+ ->setLastRequestEpoch(0)
+ ->save();
+ }
+
+}
diff --git a/src/applications/macro/xaction/PhabricatorMacroNameTransaction.php b/src/applications/herald/xaction/HeraldWebhookNameTransaction.php
similarity index 53%
copy from src/applications/macro/xaction/PhabricatorMacroNameTransaction.php
copy to src/applications/herald/xaction/HeraldWebhookNameTransaction.php
index 68710605a..622429271 100644
--- a/src/applications/macro/xaction/PhabricatorMacroNameTransaction.php
+++ b/src/applications/herald/xaction/HeraldWebhookNameTransaction.php
@@ -1,81 +1,60 @@
<?php
-final class PhabricatorMacroNameTransaction
- extends PhabricatorMacroTransactionType {
+final class HeraldWebhookNameTransaction
+ extends HeraldWebhookTransactionType {
- const TRANSACTIONTYPE = 'macro:name';
+ const TRANSACTIONTYPE = 'name';
public function generateOldValue($object) {
return $object->getName();
}
public function applyInternalEffects($object, $value) {
$object->setName($value);
}
public function getTitle() {
return pht(
- '%s renamed this macro from %s to %s.',
+ '%s renamed this webhook 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();
$viewer = $this->getActor();
if ($this->isEmptyTextTransaction($object->getName(), $xactions)) {
$errors[] = $this->newRequiredError(
- pht('Macros must have a name.'));
+ pht('Webhooks must have a name.'));
return $errors;
}
$max_length = $object->getColumnMaximumByteLength('name');
foreach ($xactions as $xaction) {
$old_value = $this->generateOldValue($object);
$new_value = $xaction->getNewValue();
$new_length = strlen($new_value);
if ($new_length > $max_length) {
$errors[] = $this->newInvalidError(
- pht('The name can be no longer than %s characters.',
- new PhutilNumber($max_length)));
+ pht(
+ 'Webhook names can be no longer than %s characters.',
+ new PhutilNumber($max_length)));
}
-
- if (!preg_match('/^[a-z0-9:_-]{3,}\z/', $new_value)) {
- $errors[] = $this->newInvalidError(
- pht('Macro name "%s" be at least three characters long and contain '.
- 'only lowercase letters, digits, hyphens, colons and '.
- 'underscores.',
- $new_value));
- }
-
- // Check name is unique when updating / creating
- if ($old_value != $new_value) {
- $macro = id(new PhabricatorMacroQuery())
- ->setViewer($viewer)
- ->withNames(array($new_value))
- ->executeOne();
-
- if ($macro) {
- $errors[] = $this->newInvalidError(
- pht('Macro "%s" already exists.', $new_value));
- }
- }
-
}
return $errors;
}
}
diff --git a/src/applications/herald/xaction/HeraldWebhookStatusTransaction.php b/src/applications/herald/xaction/HeraldWebhookStatusTransaction.php
new file mode 100644
index 000000000..951001e0b
--- /dev/null
+++ b/src/applications/herald/xaction/HeraldWebhookStatusTransaction.php
@@ -0,0 +1,67 @@
+<?php
+
+final class HeraldWebhookStatusTransaction
+ extends HeraldWebhookTransactionType {
+
+ const TRANSACTIONTYPE = 'status';
+
+ public function generateOldValue($object) {
+ return $object->getStatus();
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $object->setStatus($value);
+ }
+
+ public function getTitle() {
+ $old_value = $this->getOldValue();
+ $new_value = $this->getNewValue();
+
+ $old_status = HeraldWebhook::getDisplayNameForStatus($old_value);
+ $new_status = HeraldWebhook::getDisplayNameForStatus($new_value);
+
+ return pht(
+ '%s changed hook status from %s to %s.',
+ $this->renderAuthor(),
+ $this->renderValue($old_status),
+ $this->renderValue($new_status));
+ }
+
+ public function getTitleForFeed() {
+ $old_value = $this->getOldValue();
+ $new_value = $this->getNewValue();
+
+ $old_status = HeraldWebhook::getDisplayNameForStatus($old_value);
+ $new_status = HeraldWebhook::getDisplayNameForStatus($new_value);
+
+ return pht(
+ '%s changed %s from %s to %s.',
+ $this->renderAuthor(),
+ $this->renderObject(),
+ $this->renderValue($old_status),
+ $this->renderValue($new_status));
+ }
+
+ public function validateTransactions($object, array $xactions) {
+ $errors = array();
+ $viewer = $this->getActor();
+
+ $options = HeraldWebhook::getStatusDisplayNameMap();
+
+ foreach ($xactions as $xaction) {
+ $new_value = $xaction->getNewValue();
+
+ if (!isset($options[$new_value])) {
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'Webhook status "%s" is not valid. Valid statuses are: %s.',
+ $new_value,
+ implode(', ', array_keys($options))),
+ $xaction);
+ }
+ }
+
+ return $errors;
+ }
+
+}
diff --git a/src/applications/herald/xaction/HeraldWebhookTransactionType.php b/src/applications/herald/xaction/HeraldWebhookTransactionType.php
new file mode 100644
index 000000000..d49fcfed8
--- /dev/null
+++ b/src/applications/herald/xaction/HeraldWebhookTransactionType.php
@@ -0,0 +1,4 @@
+<?php
+
+abstract class HeraldWebhookTransactionType
+ extends PhabricatorModularTransactionType {}
diff --git a/src/applications/herald/xaction/HeraldWebhookURITransaction.php b/src/applications/herald/xaction/HeraldWebhookURITransaction.php
new file mode 100644
index 000000000..e0162085c
--- /dev/null
+++ b/src/applications/herald/xaction/HeraldWebhookURITransaction.php
@@ -0,0 +1,74 @@
+<?php
+
+final class HeraldWebhookURITransaction
+ extends HeraldWebhookTransactionType {
+
+ const TRANSACTIONTYPE = 'uri';
+
+ public function generateOldValue($object) {
+ return $object->getWebhookURI();
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $object->setWebhookURI($value);
+ }
+
+ public function getTitle() {
+ return pht(
+ '%s changed the URI for this webhook from %s to %s.',
+ $this->renderAuthor(),
+ $this->renderOldValue(),
+ $this->renderNewValue());
+ }
+
+ public function getTitleForFeed() {
+ return pht(
+ '%s changed the URI for %s from %s to %s.',
+ $this->renderAuthor(),
+ $this->renderObject(),
+ $this->renderOldValue(),
+ $this->renderNewValue());
+ }
+
+ public function validateTransactions($object, array $xactions) {
+ $errors = array();
+ $viewer = $this->getActor();
+
+ if ($this->isEmptyTextTransaction($object->getName(), $xactions)) {
+ $errors[] = $this->newRequiredError(
+ pht('Webhooks must have a URI.'));
+ return $errors;
+ }
+
+ $max_length = $object->getColumnMaximumByteLength('webhookURI');
+ foreach ($xactions as $xaction) {
+ $old_value = $this->generateOldValue($object);
+ $new_value = $xaction->getNewValue();
+
+ $new_length = strlen($new_value);
+ if ($new_length > $max_length) {
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'Webhook URIs can be no longer than %s characters.',
+ new PhutilNumber($max_length)),
+ $xaction);
+ }
+
+ try {
+ PhabricatorEnv::requireValidRemoteURIForFetch(
+ $new_value,
+ array(
+ 'http',
+ 'https',
+ ));
+ } catch (Exception $ex) {
+ $errors[] = $this->newInvalidError(
+ $ex->getMessage(),
+ $xaction);
+ }
+ }
+
+ return $errors;
+ }
+
+}
diff --git a/src/applications/legalpad/editor/LegalpadDocumentEditor.php b/src/applications/legalpad/editor/LegalpadDocumentEditor.php
index 35f2487a8..e4b43186e 100644
--- a/src/applications/legalpad/editor/LegalpadDocumentEditor.php
+++ b/src/applications/legalpad/editor/LegalpadDocumentEditor.php
@@ -1,185 +1,183 @@
<?php
final class LegalpadDocumentEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorLegalpadApplication';
}
public function getEditorObjectsDescription() {
return pht('Legalpad Documents');
}
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 document.', $author);
}
public function getCreateObjectTitleForFeed($author, $object) {
return pht('%s created %s.', $author, $object);
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
$is_contribution = false;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case LegalpadDocumentTitleTransaction::TRANSACTIONTYPE:
case LegalpadDocumentTextTransaction::TRANSACTIONTYPE:
$is_contribution = true;
break;
}
}
if ($is_contribution) {
$text = $object->getDocumentBody()->getText();
$title = $object->getDocumentBody()->getTitle();
$object->setVersions($object->getVersions() + 1);
$body = new LegalpadDocumentBody();
$body->setCreatorPHID($this->getActingAsPHID());
$body->setText($text);
$body->setTitle($title);
$body->setVersion($object->getVersions());
$body->setDocumentPHID($object->getPHID());
$body->save();
$object->setDocumentBodyPHID($body->getPHID());
$type = PhabricatorContributedToObjectEdgeType::EDGECONST;
id(new PhabricatorEdgeEditor())
->addEdge($this->getActingAsPHID(), $type, $object->getPHID())
->save();
$type = PhabricatorObjectHasContributorEdgeType::EDGECONST;
$contributors = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
$type);
$object->setRecentContributorPHIDs(array_slice($contributors, 0, 3));
$object->setContributorCount(count($contributors));
$object->save();
}
return $xactions;
}
protected function validateAllTransactions(PhabricatorLiskDAO $object,
array $xactions) {
$errors = array();
$is_required = (bool)$object->getRequireSignature();
$document_type = $object->getSignatureType();
$individual = LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case LegalpadDocumentRequireSignatureTransaction::TRANSACTIONTYPE:
$is_required = (bool)$xaction->getNewValue();
break;
case LegalpadDocumentSignatureTypeTransaction::TRANSACTIONTYPE:
$document_type = $xaction->getNewValue();
break;
}
}
if ($is_required && ($document_type != $individual)) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
LegalpadDocumentRequireSignatureTransaction::TRANSACTIONTYPE,
pht('Invalid'),
pht('Only documents with signature type "individual" may '.
'require signing to use Phabricator.'),
null);
}
return $errors;
}
/* -( Sending Mail )------------------------------------------------------- */
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new LegalpadReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
- $phid = $object->getPHID();
$title = $object->getDocumentBody()->getTitle();
return id(new PhabricatorMetaMTAMail())
- ->setSubject("L{$id}: {$title}")
- ->addHeader('Thread-Topic', "L{$id}: {$phid}");
+ ->setSubject("L{$id}: {$title}");
}
protected function getMailTo(PhabricatorLiskDAO $object) {
return array(
$object->getCreatorPHID(),
$this->requireActor()->getPHID(),
);
}
protected function shouldImplyCC(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case LegalpadDocumentTextTransaction::TRANSACTIONTYPE:
case LegalpadDocumentTitleTransaction::TRANSACTIONTYPE:
case LegalpadDocumentPreambleTransaction::TRANSACTIONTYPE:
case LegalpadDocumentRequireSignatureTransaction::TRANSACTIONTYPE:
return true;
}
return parent::shouldImplyCC($object, $xaction);
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
$body->addLinkSection(
pht('DOCUMENT DETAIL'),
PhabricatorEnv::getProductionURI('/legalpad/view/'.$object->getID().'/'));
return $body;
}
protected function getMailSubjectPrefix() {
return PhabricatorEnv::getEnvConfig('metamta.legalpad.subject-prefix');
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return false;
}
protected function supportsSearch() {
return false;
}
}
diff --git a/src/applications/legalpad/query/LegalpadDocumentSearchEngine.php b/src/applications/legalpad/query/LegalpadDocumentSearchEngine.php
index 1b608b02e..a24541748 100644
--- a/src/applications/legalpad/query/LegalpadDocumentSearchEngine.php
+++ b/src/applications/legalpad/query/LegalpadDocumentSearchEngine.php
@@ -1,195 +1,195 @@
<?php
final class LegalpadDocumentSearchEngine
extends PhabricatorApplicationSearchEngine {
public function getResultTypeDescription() {
return pht('Legalpad Documents');
}
public function getApplicationClassName() {
return 'PhabricatorLegalpadApplication';
}
public function newQuery() {
return id(new LegalpadDocumentQuery())
->needViewerSignatures(true);
}
protected function buildCustomSearchFields() {
return array(
id(new PhabricatorUsersSearchField())
->setLabel(pht('Signed By'))
->setKey('signerPHIDs')
->setAliases(array('signer', 'signers', 'signerPHID'))
->setDescription(
pht('Search for documents signed by given users.')),
id(new PhabricatorUsersSearchField())
->setLabel(pht('Creators'))
->setKey('creatorPHIDs')
->setAliases(array('creator', 'creators', 'creatorPHID'))
->setDescription(
pht('Search for documents with given creators.')),
id(new PhabricatorUsersSearchField())
->setLabel(pht('Contributors'))
->setKey('contributorPHIDs')
->setAliases(array('contributor', 'contributors', 'contributorPHID'))
->setDescription(
pht('Search for documents with given contributors.')),
id(new PhabricatorSearchDateField())
->setLabel(pht('Created After'))
->setKey('createdStart'),
id(new PhabricatorSearchDateField())
->setLabel(pht('Created Before'))
->setKey('createdEnd'),
);
}
protected function buildQueryFromParameters(array $map) {
$query = $this->newQuery();
if ($map['signerPHIDs']) {
$query->withSignerPHIDs($map['signerPHIDs']);
}
if ($map['contributorPHIDs']) {
$query->withContributorPHIDs($map['creatorPHIDs']);
}
if ($map['creatorPHIDs']) {
$query->withCreatorPHIDs($map['creatorPHIDs']);
}
if ($map['createdStart']) {
$query->withDateCreatedAfter($map['createdStart']);
}
if ($map['createdEnd']) {
$query->withDateCreatedAfter($map['createdStart']);
}
return $query;
}
protected function getURI($path) {
return '/legalpad/'.$path;
}
protected function getBuiltinQueryNames() {
$names = array();
if ($this->requireViewer()->isLoggedIn()) {
$names['signed'] = pht('Signed Documents');
}
$names['all'] = pht('All Documents');
return $names;
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
$viewer = $this->requireViewer();
switch ($query_key) {
case 'signed':
return $query->setParameter('signerPHIDs', array($viewer->getPHID()));
case 'all':
return $query;
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
protected function renderResultList(
array $documents,
PhabricatorSavedQuery $query,
array $handles) {
assert_instances_of($documents, 'LegalpadDocument');
$viewer = $this->requireViewer();
$list = new PHUIObjectItemListView();
$list->setUser($viewer);
foreach ($documents as $document) {
$last_updated = phabricator_date($document->getDateModified(), $viewer);
$title = $document->getTitle();
$item = id(new PHUIObjectItemView())
->setObjectName($document->getMonogram())
->setHeader($title)
->setHref('/'.$document->getMonogram())
->setObject($document);
$no_signatures = LegalpadDocument::SIGNATURE_TYPE_NONE;
if ($document->getSignatureType() == $no_signatures) {
$item->addIcon('none', pht('Not Signable'));
} else {
$type_name = $document->getSignatureTypeName();
$type_icon = $document->getSignatureTypeIcon();
$item->addIcon($type_icon, $type_name);
if ($viewer->getPHID()) {
$signature = $document->getUserSignature($viewer->getPHID());
} else {
$signature = null;
}
if ($signature) {
$item->addAttribute(
array(
id(new PHUIIconView())->setIcon('fa-check-square-o', 'green'),
' ',
pht(
'Signed on %s',
phabricator_date($signature->getDateCreated(), $viewer)),
));
} else {
$item->addAttribute(
array(
id(new PHUIIconView())->setIcon('fa-square-o', 'grey'),
' ',
pht('Not Signed'),
));
}
}
$item->addIcon(
'fa-pencil grey',
pht('Version %d (%s)', $document->getVersions(), $last_updated));
$list->addItem($item);
}
$result = new PhabricatorApplicationSearchResultView();
$result->setObjectList($list);
$result->setNoDataString(pht('No documents found.'));
return $result;
}
protected function getNewUserBody() {
$create_button = id(new PHUIButtonView())
->setTag('a')
->setText(pht('Create a Document'))
- ->setHref('/legalpad/create/')
+ ->setHref('/legalpad/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('Create documents and track signatures. Can also be re-used in '.
'other areas of Phabricator, like CLAs.'))
->addAction($create_button);
return $view;
}
}
diff --git a/src/applications/macro/conduit/MacroCreateMemeConduitAPIMethod.php b/src/applications/macro/conduit/MacroCreateMemeConduitAPIMethod.php
index e63d831a7..3c939f97d 100644
--- a/src/applications/macro/conduit/MacroCreateMemeConduitAPIMethod.php
+++ b/src/applications/macro/conduit/MacroCreateMemeConduitAPIMethod.php
@@ -1,57 +1,50 @@
<?php
final class MacroCreateMemeConduitAPIMethod extends MacroConduitAPIMethod {
public function getAPIMethodName() {
return 'macro.creatememe';
}
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodDescription() {
return pht('Generate a meme.');
}
protected function defineParamTypes() {
return array(
'macroName' => 'string',
'upperText' => 'optional string',
'lowerText' => 'optional string',
);
}
protected function defineReturnType() {
return 'string';
}
protected function defineErrorTypes() {
return array(
'ERR-NOT-FOUND' => pht('Macro was not found.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$user = $request->getUser();
- $macro_name = $request->getValue('macroName');
- $upper_text = $request->getValue('upperText');
- $lower_text = $request->getValue('lowerText');
-
- $uri = PhabricatorMacroMemeController::generateMacro(
- $user,
- $macro_name,
- $upper_text,
- $lower_text);
-
- if (!$uri) {
- throw new ConduitException('ERR-NOT-FOUND');
- }
+ $file = id(new PhabricatorMemeEngine())
+ ->setViewer($user)
+ ->setTemplate($request->getValue('macroName'))
+ ->setAboveText($request->getValue('upperText'))
+ ->setBelowText($request->getValue('lowerText'))
+ ->newAsset();
return array(
- 'uri' => $uri,
+ 'uri' => $file->getViewURI(),
);
}
}
diff --git a/src/applications/macro/controller/PhabricatorMacroMemeController.php b/src/applications/macro/controller/PhabricatorMacroMemeController.php
index 03b13f620..eb1dcbeb2 100644
--- a/src/applications/macro/controller/PhabricatorMacroMemeController.php
+++ b/src/applications/macro/controller/PhabricatorMacroMemeController.php
@@ -1,68 +1,30 @@
<?php
final class PhabricatorMacroMemeController
extends PhabricatorMacroController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$macro_name = $request->getStr('macro');
$upper_text = $request->getStr('uppertext');
$lower_text = $request->getStr('lowertext');
$viewer = $request->getViewer();
- $uri = self::generateMacro($viewer, $macro_name,
- $upper_text, $lower_text);
- if ($uri === false) {
- return new Aphront404Response();
- }
- return id(new AphrontRedirectResponse())
- ->setIsExternal(true)
- ->setURI($uri);
- }
-
- public static function generateMacro($viewer, $macro_name, $upper_text,
- $lower_text) {
- $macro = id(new PhabricatorMacroQuery())
+ $file = id(new PhabricatorMemeEngine())
->setViewer($viewer)
- ->withNames(array($macro_name))
- ->needFiles(true)
- ->executeOne();
- if (!$macro) {
- return false;
- }
- $file = $macro->getFile();
-
- $upper_text = strtoupper($upper_text);
- $lower_text = strtoupper($lower_text);
- $mixed_text = md5($upper_text).':'.md5($lower_text);
- $hash = 'meme'.hash('sha256', $mixed_text);
- $xform = id(new PhabricatorTransformedFile())
- ->loadOneWhere('originalphid=%s and transform=%s',
- $file->getPHID(), $hash);
+ ->setTemplate($macro_name)
+ ->setAboveText($request->getStr('above'))
+ ->setBelowText($request->getStr('below'))
+ ->newAsset();
- if ($xform) {
- $memefile = id(new PhabricatorFileQuery())
- ->setViewer($viewer)
- ->withPHIDs(array($xform->getTransformedPHID()))
- ->executeOne();
- if ($memefile) {
- return $memefile->getBestURI();
- }
- }
+ $content = array(
+ 'imageURI' => $file->getViewURI(),
+ );
- $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
- $transformers = (new PhabricatorImageTransformer());
- $newfile = $transformers
- ->executeMemeTransform($file, $upper_text, $lower_text);
- $xfile = new PhabricatorTransformedFile();
- $xfile->setOriginalPHID($file->getPHID());
- $xfile->setTransformedPHID($newfile->getPHID());
- $xfile->setTransform($hash);
- $xfile->save();
-
- return $newfile->getBestURI();
+ return id(new AphrontAjaxResponse())->setContent($content);
}
+
}
diff --git a/src/applications/macro/editor/PhabricatorMacroEditor.php b/src/applications/macro/editor/PhabricatorMacroEditor.php
index 5d28b78f5..f59c29b42 100644
--- a/src/applications/macro/editor/PhabricatorMacroEditor.php
+++ b/src/applications/macro/editor/PhabricatorMacroEditor.php
@@ -1,69 +1,68 @@
<?php
final class PhabricatorMacroEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorMacroApplication';
}
public function getEditorObjectsDescription() {
return pht('Macros');
}
public function getCreateObjectTitle($author, $object) {
return pht('%s created this macro.', $author);
}
public function getCreateObjectTitleForFeed($author, $object) {
return pht('%s created %s.', $author, $object);
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new PhabricatorMacroReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$name = $object->getName();
$name = 'Image Macro "'.$name.'"';
return id(new PhabricatorMetaMTAMail())
- ->setSubject($name)
- ->addHeader('Thread-Topic', $name);
+ ->setSubject($name);
}
protected function getMailTo(PhabricatorLiskDAO $object) {
return array(
$this->requireActor()->getPHID(),
);
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
$body->addLinkSection(
pht('MACRO DETAIL'),
PhabricatorEnv::getProductionURI('/macro/view/'.$object->getID().'/'));
return $body;
}
protected function getMailSubjectPrefix() {
return PhabricatorEnv::getEnvConfig('metamta.macro.subject-prefix');
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
}
diff --git a/src/applications/macro/engine/PhabricatorMemeEngine.php b/src/applications/macro/engine/PhabricatorMemeEngine.php
new file mode 100644
index 000000000..175aa2fb4
--- /dev/null
+++ b/src/applications/macro/engine/PhabricatorMemeEngine.php
@@ -0,0 +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()),
+ );
+
+ $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/macro/markup/PhabricatorImageMacroRemarkupRule.php b/src/applications/macro/markup/PhabricatorImageMacroRemarkupRule.php
index 8911181a7..4c50322da 100644
--- a/src/applications/macro/markup/PhabricatorImageMacroRemarkupRule.php
+++ b/src/applications/macro/markup/PhabricatorImageMacroRemarkupRule.php
@@ -1,157 +1,157 @@
<?php
final class PhabricatorImageMacroRemarkupRule extends PhutilRemarkupRule {
private $macros;
const KEY_RULE_MACRO = 'rule.macro';
public function apply($text) {
return preg_replace_callback(
- '@^\s*([a-zA-Z0-9:_\-]+)$@m',
+ '@^\s*([a-zA-Z0-9:_\x7f-\xff-]+)$@m',
array($this, 'markupImageMacro'),
$text);
}
public function markupImageMacro(array $matches) {
if ($this->macros === null) {
$this->macros = array();
$viewer = $this->getEngine()->getConfig('viewer');
$rows = id(new PhabricatorMacroQuery())
->setViewer($viewer)
->withStatus(PhabricatorMacroQuery::STATUS_ACTIVE)
->execute();
$this->macros = mpull($rows, 'getPHID', 'getName');
}
$name = (string)$matches[1];
if (empty($this->macros[$name])) {
return $matches[1];
}
$engine = $this->getEngine();
$metadata_key = self::KEY_RULE_MACRO;
$metadata = $engine->getTextMetadata($metadata_key, array());
$token = $engine->storeText('<macro>');
$metadata[] = array(
'token' => $token,
'phid' => $this->macros[$name],
'original' => $name,
);
$engine->setTextMetadata($metadata_key, $metadata);
return $token;
}
public function didMarkupText() {
$engine = $this->getEngine();
$metadata_key = self::KEY_RULE_MACRO;
$metadata = $engine->getTextMetadata($metadata_key, array());
if (!$metadata) {
return;
}
$phids = ipull($metadata, 'phid');
$viewer = $this->getEngine()->getConfig('viewer');
// Load all the macros.
$macros = id(new PhabricatorMacroQuery())
->setViewer($viewer)
->withStatus(PhabricatorMacroQuery::STATUS_ACTIVE)
->withPHIDs($phids)
->execute();
$macros = mpull($macros, null, 'getPHID');
// Load all the images and audio.
$file_phids = array_merge(
array_values(mpull($macros, 'getFilePHID')),
array_values(mpull($macros, 'getAudioPHID')));
$file_phids = array_filter($file_phids);
$files = array();
if ($file_phids) {
$files = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs($file_phids)
->execute();
$files = mpull($files, null, 'getPHID');
}
// Replace any macros that we couldn't load the macro or image for with
// the original text.
foreach ($metadata as $key => $spec) {
$macro = idx($macros, $spec['phid']);
if ($macro) {
$file = idx($files, $macro->getFilePHID());
if ($file) {
continue;
}
}
$engine->overwriteStoredText($spec['token'], $spec['original']);
unset($metadata[$key]);
}
foreach ($metadata as $spec) {
$macro = $macros[$spec['phid']];
$file = $files[$macro->getFilePHID()];
$src_uri = $file->getBestURI();
if ($this->getEngine()->isTextMode()) {
$result = $spec['original'].' <'.$src_uri.'>';
$engine->overwriteStoredText($spec['token'], $result);
continue;
} else if ($this->getEngine()->isHTMLMailMode()) {
$src_uri = PhabricatorEnv::getProductionURI($src_uri);
}
$id = null;
$audio = idx($files, $macro->getAudioPHID());
$should_play = ($audio && $macro->getAudioBehavior() !=
PhabricatorFileImageMacro::AUDIO_BEHAVIOR_NONE);
if ($should_play) {
$id = celerity_generate_unique_node_id();
$loop = null;
switch ($macro->getAudioBehavior()) {
case PhabricatorFileImageMacro::AUDIO_BEHAVIOR_LOOP:
$loop = true;
break;
}
Javelin::initBehavior(
'audio-source',
array(
'sourceID' => $id,
'audioURI' => $audio->getBestURI(),
'loop' => $loop,
));
}
$result = $this->newTag(
'img',
array(
'id' => $id,
'src' => $src_uri,
'alt' => $spec['original'],
'title' => $spec['original'],
'height' => $file->getImageHeight(),
'width' => $file->getImageWidth(),
'class' => 'phabricator-remarkup-macro',
));
$engine->overwriteStoredText($spec['token'], $result);
}
$engine->setTextMetadata($metadata_key, array());
}
}
diff --git a/src/applications/macro/markup/PhabricatorMemeRemarkupRule.php b/src/applications/macro/markup/PhabricatorMemeRemarkupRule.php
index 004e41545..eacb1a33e 100644
--- a/src/applications/macro/markup/PhabricatorMemeRemarkupRule.php
+++ b/src/applications/macro/markup/PhabricatorMemeRemarkupRule.php
@@ -1,65 +1,103 @@
<?php
final class PhabricatorMemeRemarkupRule extends PhutilRemarkupRule {
private $images;
public function getPriority() {
return 200.0;
}
public function apply($text) {
return preg_replace_callback(
'@{meme,((?:[^}\\\\]+|\\\\.)+)}@m',
array($this, 'markupMeme'),
$text);
}
public function markupMeme(array $matches) {
if (!$this->isFlatText($matches[0])) {
return $matches[0];
}
$options = array(
'src' => null,
'above' => null,
'below' => null,
);
$parser = new PhutilSimpleOptions();
$options = $parser->parse($matches[1]) + $options;
- $uri = id(new PhutilURI('/macro/meme/'))
- ->alter('macro', $options['src'])
- ->alter('uppertext', $options['above'])
- ->alter('lowertext', $options['below']);
+ $engine = id(new PhabricatorMemeEngine())
+ ->setViewer(PhabricatorUser::getOmnipotentUser())
+ ->setTemplate($options['src'])
+ ->setAboveText($options['above'])
+ ->setBelowText($options['below']);
- if ($this->getEngine()->isHTMLMailMode()) {
- $uri = PhabricatorEnv::getProductionURI($uri);
+ $asset = $engine->loadCachedFile();
+
+ $is_html_mail = $this->getEngine()->isHTMLMailMode();
+ $is_text = $this->getEngine()->isTextMode();
+ $must_inline = ($is_html_mail || $is_text);
+
+ if ($must_inline) {
+ if (!$asset) {
+ try {
+ $asset = $engine->newAsset();
+ } catch (Exception $ex) {
+ return $matches[0];
+ }
+ }
}
- if ($this->getEngine()->isTextMode()) {
- $img =
- ($options['above'] != '' ? "\"{$options['above']}\"\n" : '').
- $options['src'].' <'.PhabricatorEnv::getProductionURI($uri).'>'.
- ($options['below'] != '' ? "\n\"{$options['below']}\"" : '');
+ if ($asset) {
+ $uri = $asset->getViewURI();
} else {
- $alt_text = pht(
- 'Macro %s: %s %s',
- $options['src'],
- $options['above'],
- $options['below']);
+ $uri = $engine->getGenerateURI();
+ }
+
+ if ($is_text) {
+ $parts = array();
+
+ $above = $options['above'];
+ if (strlen($above)) {
+ $parts[] = pht('"%s"', $above);
+ }
+
+ $parts[] = $options['src'].' <'.$uri.'>';
+
+ $below = $options['below'];
+ if (strlen($below)) {
+ $parts[] = pht('"%s"', $below);
+ }
+ $parts = implode("\n", $parts);
+ return $this->getEngine()->storeText($parts);
+ }
+
+ $alt_text = pht(
+ 'Macro %s: %s %s',
+ $options['src'],
+ $options['above'],
+ $options['below']);
+
+ if ($asset) {
$img = $this->newTag(
'img',
array(
'src' => $uri,
- 'alt' => $alt_text,
'class' => 'phabricator-remarkup-macro',
+ 'alt' => $alt_text,
));
+ } else {
+ $img = id(new PHUIRemarkupImageView())
+ ->setURI($uri)
+ ->addClass('phabricator-remarkup-macro')
+ ->setAlt($alt_text);
}
return $this->getEngine()->storeText($img);
}
}
diff --git a/src/applications/macro/xaction/PhabricatorMacroNameTransaction.php b/src/applications/macro/xaction/PhabricatorMacroNameTransaction.php
index 68710605a..f55de443b 100644
--- a/src/applications/macro/xaction/PhabricatorMacroNameTransaction.php
+++ b/src/applications/macro/xaction/PhabricatorMacroNameTransaction.php
@@ -1,81 +1,116 @@
<?php
final class PhabricatorMacroNameTransaction
extends PhabricatorMacroTransactionType {
const TRANSACTIONTYPE = 'macro:name';
public function generateOldValue($object) {
return $object->getName();
}
public function applyInternalEffects($object, $value) {
$object->setName($value);
}
public function getTitle() {
return pht(
'%s renamed this macro 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();
$viewer = $this->getActor();
if ($this->isEmptyTextTransaction($object->getName(), $xactions)) {
$errors[] = $this->newRequiredError(
pht('Macros must have a name.'));
return $errors;
}
$max_length = $object->getColumnMaximumByteLength('name');
foreach ($xactions as $xaction) {
$old_value = $this->generateOldValue($object);
$new_value = $xaction->getNewValue();
$new_length = strlen($new_value);
if ($new_length > $max_length) {
$errors[] = $this->newInvalidError(
pht('The name can be no longer than %s characters.',
new PhutilNumber($max_length)));
}
- if (!preg_match('/^[a-z0-9:_-]{3,}\z/', $new_value)) {
- $errors[] = $this->newInvalidError(
- pht('Macro name "%s" be at least three characters long and contain '.
- 'only lowercase letters, digits, hyphens, colons and '.
- 'underscores.',
- $new_value));
+ if (!self::isValidMacroName($new_value)) {
+ // This says "emoji", but the actual rule we implement is "all other
+ // unicode characters are also fine".
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'Macro name "%s" be: at least three characters long; and contain '.
+ 'only lowercase letters, digits, hyphens, colons, underscores, '.
+ 'and emoji; and not be composed entirely of latin symbols.',
+ $new_value),
+ $xaction);
}
// Check name is unique when updating / creating
if ($old_value != $new_value) {
$macro = id(new PhabricatorMacroQuery())
->setViewer($viewer)
->withNames(array($new_value))
->executeOne();
if ($macro) {
$errors[] = $this->newInvalidError(
pht('Macro "%s" already exists.', $new_value));
}
}
}
return $errors;
}
+ public static function isValidMacroName($name) {
+ if (preg_match('/^[:_-]+\z/', $name)) {
+ return false;
+ }
+
+ // Accept trivial macro names.
+ if (preg_match('/^[a-z0-9:_-]{3,}\z/', $name)) {
+ return true;
+ }
+
+ // Reject names with fewer than 3 glyphs.
+ $length = phutil_utf8v_combined($name);
+ if (count($length) < 3) {
+ return false;
+ }
+
+ // Check character-by-character for any symbols that we don't want.
+ $characters = phutil_utf8v($name);
+ foreach ($characters as $character) {
+ if (ord($character[0]) > 0x7F) {
+ continue;
+ }
+
+ if (preg_match('/^[^a-z0-9:_-]/', $character)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
}
diff --git a/src/applications/macro/xaction/__tests__/PhabricatorMacroTestCase.php b/src/applications/macro/xaction/__tests__/PhabricatorMacroTestCase.php
new file mode 100644
index 000000000..aa13ec0ac
--- /dev/null
+++ b/src/applications/macro/xaction/__tests__/PhabricatorMacroTestCase.php
@@ -0,0 +1,46 @@
+<?php
+
+final class PhabricatorMacroTestCase
+ extends PhabricatorTestCase {
+
+ public function testMacroNames() {
+ $lit = "\xF0\x9F\x94\xA5";
+ $combining_diaeresis = "\xCC\x88";
+
+ $cases = array(
+ // Only 2 glyphs long.
+ "u{$combining_diaeresis}n" => false,
+ "{$lit}{$lit}" => false,
+
+ // Too short.
+ 'a' => false,
+ '' => false,
+
+ // Bad characters.
+ 'yes!' => false,
+ "{$lit} {$lit} {$lit}" => false,
+ "aaa\nbbb" => false,
+ 'aaa~' => false,
+ 'aaa`' => false,
+
+ // Special rejections for only latin symbols.
+ '---' => false,
+ '___' => false,
+ '-_-' => false,
+ ':::' => false,
+ '-_:' => false,
+
+ "{$lit}{$lit}{$lit}" => true,
+ 'bwahahaha' => true,
+ "u{$combining_diaeresis}nt" => true,
+ 'a-a-a-a' => true,
+ );
+
+ foreach ($cases as $input => $expect) {
+ $this->assertEqual(
+ $expect,
+ PhabricatorMacroNameTransaction::isValidMacroName($input),
+ pht('Validity of macro "%s"', $input));
+ }
+ }
+}
diff --git a/src/applications/maniphest/application/PhabricatorManiphestApplication.php b/src/applications/maniphest/application/PhabricatorManiphestApplication.php
index 6e4ac0a8f..007577086 100644
--- a/src/applications/maniphest/application/PhabricatorManiphestApplication.php
+++ b/src/applications/maniphest/application/PhabricatorManiphestApplication.php
@@ -1,122 +1,121 @@
<?php
final class PhabricatorManiphestApplication extends PhabricatorApplication {
public function getName() {
return pht('Maniphest');
}
public function getMenuName() {
return pht('Tasks');
}
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(
- '(?:query/(?P<queryKey>[^/]+)/)?' => 'ManiphestTaskListController',
+ $this->getQueryRoutePattern() => 'ManiphestTaskListController',
'report/(?:(?P<view>\w+)/)?' => 'ManiphestReportController',
$this->getBulkRoutePattern('bulk/') => 'ManiphestBulkEditController',
'task/' => array(
$this->getEditRoutePattern('edit/')
=> 'ManiphestTaskEditController',
),
- 'export/(?P<key>[^/]+)/' => 'ManiphestExportController',
'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/bulk/ManiphestTaskEditBulkJobType.php b/src/applications/maniphest/bulk/ManiphestTaskEditBulkJobType.php
deleted file mode 100644
index 1b083d88f..000000000
--- a/src/applications/maniphest/bulk/ManiphestTaskEditBulkJobType.php
+++ /dev/null
@@ -1,303 +0,0 @@
-<?php
-
-final class ManiphestTaskEditBulkJobType
- extends PhabricatorWorkerBulkJobType {
-
- public function getBulkJobTypeKey() {
- return 'maniphest.task.edit';
- }
-
- public function getJobName(PhabricatorWorkerBulkJob $job) {
- return pht('Maniphest Bulk Edit');
- }
-
- public function getDescriptionForConfirm(PhabricatorWorkerBulkJob $job) {
- return pht(
- 'You are about to apply a bulk edit to Maniphest which will affect '.
- '%s task(s).',
- new PhutilNumber($job->getSize()));
- }
-
- public function getJobSize(PhabricatorWorkerBulkJob $job) {
- return count($job->getParameter('taskPHIDs', array()));
- }
-
- public function getDoneURI(PhabricatorWorkerBulkJob $job) {
- return $job->getParameter('doneURI');
- }
-
- public function createTasks(PhabricatorWorkerBulkJob $job) {
- $tasks = array();
-
- foreach ($job->getParameter('taskPHIDs', array()) as $phid) {
- $tasks[] = PhabricatorWorkerBulkTask::initializeNewTask($job, $phid);
- }
-
- return $tasks;
- }
-
- public function runTask(
- PhabricatorUser $actor,
- PhabricatorWorkerBulkJob $job,
- PhabricatorWorkerBulkTask $task) {
-
- $object = id(new ManiphestTaskQuery())
- ->setViewer($actor)
- ->requireCapabilities(
- array(
- PhabricatorPolicyCapability::CAN_VIEW,
- PhabricatorPolicyCapability::CAN_EDIT,
- ))
- ->withPHIDs(array($task->getObjectPHID()))
- ->needProjectPHIDs(true)
- ->needSubscriberPHIDs(true)
- ->executeOne();
- if (!$object) {
- return;
- }
-
- $field_list = PhabricatorCustomField::getObjectFields(
- $object,
- PhabricatorCustomField::ROLE_EDIT);
- $field_list->readFieldsFromStorage($object);
-
- $actions = $job->getParameter('actions');
- $xactions = $this->buildTransactions($actions, $object);
-
- $editor = id(new ManiphestTransactionEditor())
- ->setActor($actor)
- ->setContentSource($job->newContentSource())
- ->setContinueOnNoEffect(true)
- ->setContinueOnMissingFields(true)
- ->applyTransactions($object, $xactions);
- }
-
- private function buildTransactions($actions, ManiphestTask $task) {
- $value_map = array();
- $type_map = array(
- 'add_comment' => PhabricatorTransactions::TYPE_COMMENT,
- 'assign' => ManiphestTaskOwnerTransaction::TRANSACTIONTYPE,
- 'status' => ManiphestTaskStatusTransaction::TRANSACTIONTYPE,
- 'priority' => ManiphestTaskPriorityTransaction::TRANSACTIONTYPE,
- 'add_project' => PhabricatorTransactions::TYPE_EDGE,
- 'remove_project' => PhabricatorTransactions::TYPE_EDGE,
- 'add_ccs' => PhabricatorTransactions::TYPE_SUBSCRIBERS,
- 'remove_ccs' => PhabricatorTransactions::TYPE_SUBSCRIBERS,
- 'space' => PhabricatorTransactions::TYPE_SPACE,
- );
-
- $edge_edit_types = array(
- 'add_project' => true,
- 'remove_project' => true,
- 'add_ccs' => true,
- 'remove_ccs' => true,
- );
-
- $xactions = array();
- foreach ($actions as $action) {
- if (empty($type_map[$action['action']])) {
- throw new Exception(pht("Unknown batch edit action '%s'!", $action));
- }
-
- $type = $type_map[$action['action']];
-
- // Figure out the current value, possibly after modifications by other
- // batch actions of the same type. For example, if the user chooses to
- // "Add Comment" twice, we should add both comments. More notably, if the
- // user chooses "Remove Project..." and also "Add Project...", we should
- // avoid restoring the removed project in the second transaction.
-
- if (array_key_exists($type, $value_map)) {
- $current = $value_map[$type];
- } else {
- switch ($type) {
- case PhabricatorTransactions::TYPE_COMMENT:
- $current = null;
- break;
- case ManiphestTaskOwnerTransaction::TRANSACTIONTYPE:
- $current = $task->getOwnerPHID();
- break;
- case ManiphestTaskStatusTransaction::TRANSACTIONTYPE:
- $current = $task->getStatus();
- break;
- case ManiphestTaskPriorityTransaction::TRANSACTIONTYPE:
- $current = $task->getPriority();
- break;
- case PhabricatorTransactions::TYPE_EDGE:
- $current = $task->getProjectPHIDs();
- break;
- case PhabricatorTransactions::TYPE_SUBSCRIBERS:
- $current = $task->getSubscriberPHIDs();
- break;
- case PhabricatorTransactions::TYPE_SPACE:
- $current = PhabricatorSpacesNamespaceQuery::getObjectSpacePHID(
- $task);
- break;
- }
- }
-
- // Check if the value is meaningful / provided, and normalize it if
- // necessary. This discards, e.g., empty comments and empty owner
- // changes.
-
- $value = $action['value'];
- switch ($type) {
- case PhabricatorTransactions::TYPE_COMMENT:
- if (!strlen($value)) {
- continue 2;
- }
- break;
- case PhabricatorTransactions::TYPE_SPACE:
- if (empty($value)) {
- continue 2;
- }
- $value = head($value);
- break;
- case ManiphestTaskOwnerTransaction::TRANSACTIONTYPE:
- if (empty($value)) {
- continue 2;
- }
- $value = head($value);
- $no_owner = PhabricatorPeopleNoOwnerDatasource::FUNCTION_TOKEN;
- if ($value === $no_owner) {
- $value = null;
- }
- break;
- case PhabricatorTransactions::TYPE_EDGE:
- if (empty($value)) {
- continue 2;
- }
- break;
- case PhabricatorTransactions::TYPE_SUBSCRIBERS:
- if (empty($value)) {
- continue 2;
- }
- break;
- }
-
- // If the edit doesn't change anything, go to the next action. This
- // check is only valid for changes like "owner", "status", etc, not
- // for edge edits, because we should still apply an edit like
- // "Remove Projects: A, B" to a task with projects "A, B".
-
- if (empty($edge_edit_types[$action['action']])) {
- if ($value == $current) {
- continue;
- }
- }
-
- // Apply the value change; for most edits this is just replacement, but
- // some need to merge the current and edited values (add/remove project).
-
- switch ($type) {
- case PhabricatorTransactions::TYPE_COMMENT:
- if (strlen($current)) {
- $value = $current."\n\n".$value;
- }
- break;
- case PhabricatorTransactions::TYPE_EDGE:
- $is_remove = $action['action'] == 'remove_project';
-
- $current = array_fill_keys($current, true);
- $value = array_fill_keys($value, true);
-
- $new = $current;
- $did_something = false;
-
- if ($is_remove) {
- foreach ($value as $phid => $ignored) {
- if (isset($new[$phid])) {
- unset($new[$phid]);
- $did_something = true;
- }
- }
- } else {
- foreach ($value as $phid => $ignored) {
- if (empty($new[$phid])) {
- $new[$phid] = true;
- $did_something = true;
- }
- }
- }
-
- if (!$did_something) {
- continue 2;
- }
-
- $value = array_keys($new);
- break;
- case PhabricatorTransactions::TYPE_SUBSCRIBERS:
- $is_remove = $action['action'] == 'remove_ccs';
-
- $current = array_fill_keys($current, true);
-
- $new = array();
- $did_something = false;
-
- if ($is_remove) {
- foreach ($value as $phid) {
- if (isset($current[$phid])) {
- $new[$phid] = true;
- $did_something = true;
- }
- }
- if ($new) {
- $value = array('-' => array_keys($new));
- }
- } else {
- $new = array();
- foreach ($value as $phid) {
- $new[$phid] = true;
- $did_something = true;
- }
- if ($new) {
- $value = array('+' => array_keys($new));
- }
- }
- if (!$did_something) {
- continue 2;
- }
-
- break;
- }
-
- $value_map[$type] = $value;
- }
-
- $template = new ManiphestTransaction();
-
- foreach ($value_map as $type => $value) {
- $xaction = clone $template;
- $xaction->setTransactionType($type);
-
- switch ($type) {
- case PhabricatorTransactions::TYPE_COMMENT:
- $xaction->attachComment(
- id(new ManiphestTransactionComment())
- ->setContent($value));
- break;
- case PhabricatorTransactions::TYPE_EDGE:
- $project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
- $xaction
- ->setMetadataValue('edge:type', $project_type)
- ->setNewValue(
- array(
- '=' => array_fuse($value),
- ));
- break;
- case ManiphestTaskPriorityTransaction::TRANSACTIONTYPE:
- $keyword_map = ManiphestTaskPriority::getTaskPriorityKeywordsMap();
- $keyword = head(idx($keyword_map, $value));
- $xaction->setNewValue($keyword);
- break;
- default:
- $xaction->setNewValue($value);
- break;
- }
-
- $xactions[] = $xaction;
- }
-
- return $xactions;
- }
-}
diff --git a/src/applications/maniphest/controller/ManiphestBatchEditController.php b/src/applications/maniphest/controller/ManiphestBatchEditController.php
deleted file mode 100644
index f2ec4b433..000000000
--- a/src/applications/maniphest/controller/ManiphestBatchEditController.php
+++ /dev/null
@@ -1,226 +0,0 @@
-<?php
-
-final class ManiphestBatchEditController extends ManiphestController {
-
- public function handleRequest(AphrontRequest $request) {
- $viewer = $this->getViewer();
-
- $this->requireApplicationCapability(
- ManiphestBulkEditCapability::CAPABILITY);
-
- $project = null;
- $board_id = $request->getInt('board');
- if ($board_id) {
- $project = id(new PhabricatorProjectQuery())
- ->setViewer($viewer)
- ->withIDs(array($board_id))
- ->executeOne();
- if (!$project) {
- return new Aphront404Response();
- }
- }
-
- $task_ids = $request->getArr('batch');
- if (!$task_ids) {
- $task_ids = $request->getStrList('batch');
- }
-
- if (!$task_ids) {
- throw new Exception(
- pht(
- 'No tasks are selected.'));
- }
-
- $tasks = id(new ManiphestTaskQuery())
- ->setViewer($viewer)
- ->withIDs($task_ids)
- ->requireCapabilities(
- array(
- PhabricatorPolicyCapability::CAN_VIEW,
- PhabricatorPolicyCapability::CAN_EDIT,
- ))
- ->needSubscriberPHIDs(true)
- ->needProjectPHIDs(true)
- ->execute();
-
- if (!$tasks) {
- throw new Exception(
- pht("You don't have permission to edit any of the selected tasks."));
- }
-
- if ($project) {
- $cancel_uri = '/project/board/'.$project->getID().'/';
- $redirect_uri = $cancel_uri;
- } else {
- $cancel_uri = '/maniphest/';
- $redirect_uri = '/maniphest/?ids='.implode(',', mpull($tasks, 'getID'));
- }
-
- $actions = $request->getStr('actions');
- if ($actions) {
- $actions = phutil_json_decode($actions);
- }
-
- if ($request->isFormPost() && $actions) {
- $job = PhabricatorWorkerBulkJob::initializeNewJob(
- $viewer,
- new ManiphestTaskEditBulkJobType(),
- array(
- 'taskPHIDs' => mpull($tasks, 'getPHID'),
- 'actions' => $actions,
- 'cancelURI' => $cancel_uri,
- 'doneURI' => $redirect_uri,
- ));
-
- $type_status = PhabricatorWorkerBulkJobTransaction::TYPE_STATUS;
-
- $xactions = array();
- $xactions[] = id(new PhabricatorWorkerBulkJobTransaction())
- ->setTransactionType($type_status)
- ->setNewValue(PhabricatorWorkerBulkJob::STATUS_CONFIRM);
-
- $editor = id(new PhabricatorWorkerBulkJobEditor())
- ->setActor($viewer)
- ->setContentSourceFromRequest($request)
- ->setContinueOnMissingFields(true)
- ->applyTransactions($job, $xactions);
-
- return id(new AphrontRedirectResponse())
- ->setURI($job->getMonitorURI());
- }
-
- $handles = ManiphestTaskListView::loadTaskHandles($viewer, $tasks);
-
- $list = new ManiphestTaskListView();
- $list->setTasks($tasks);
- $list->setUser($viewer);
- $list->setHandles($handles);
-
- $template = new AphrontTokenizerTemplateView();
- $template = $template->render();
-
- $projects_source = new PhabricatorProjectDatasource();
- $mailable_source = new PhabricatorMetaMTAMailableDatasource();
- $mailable_source->setViewer($viewer);
- $owner_source = new ManiphestAssigneeDatasource();
- $owner_source->setViewer($viewer);
- $spaces_source = id(new PhabricatorSpacesNamespaceDatasource())
- ->setViewer($viewer);
-
- require_celerity_resource('maniphest-batch-editor');
- Javelin::initBehavior(
- 'maniphest-batch-editor',
- array(
- 'root' => 'maniphest-batch-edit-form',
- 'tokenizerTemplate' => $template,
- 'sources' => array(
- 'project' => array(
- 'src' => $projects_source->getDatasourceURI(),
- 'placeholder' => $projects_source->getPlaceholderText(),
- 'browseURI' => $projects_source->getBrowseURI(),
- ),
- 'owner' => array(
- 'src' => $owner_source->getDatasourceURI(),
- 'placeholder' => $owner_source->getPlaceholderText(),
- 'browseURI' => $owner_source->getBrowseURI(),
- 'limit' => 1,
- ),
- 'cc' => array(
- 'src' => $mailable_source->getDatasourceURI(),
- 'placeholder' => $mailable_source->getPlaceholderText(),
- 'browseURI' => $mailable_source->getBrowseURI(),
- ),
- 'spaces' => array(
- 'src' => $spaces_source->getDatasourceURI(),
- 'placeholder' => $spaces_source->getPlaceholderText(),
- 'browseURI' => $spaces_source->getBrowseURI(),
- 'limit' => 1,
- ),
- ),
- 'input' => 'batch-form-actions',
- 'priorityMap' => ManiphestTaskPriority::getTaskPriorityMap(),
- 'statusMap' => ManiphestTaskStatus::getTaskStatusMap(),
- ));
-
- $form = id(new AphrontFormView())
- ->setUser($viewer)
- ->addHiddenInput('board', $board_id)
- ->setID('maniphest-batch-edit-form');
-
- foreach ($tasks as $task) {
- $form->appendChild(
- phutil_tag(
- 'input',
- array(
- 'type' => 'hidden',
- 'name' => 'batch[]',
- 'value' => $task->getID(),
- )));
- }
-
- $form->appendChild(
- phutil_tag(
- 'input',
- array(
- 'type' => 'hidden',
- 'name' => 'actions',
- 'id' => 'batch-form-actions',
- )));
- $form->appendChild(
- id(new PHUIFormInsetView())
- ->setTitle(pht('Actions'))
- ->setRightButton(javelin_tag(
- 'a',
- array(
- 'href' => '#',
- 'class' => 'button button-green',
- 'sigil' => 'add-action',
- 'mustcapture' => true,
- ),
- pht('Add Another Action')))
- ->setContent(javelin_tag(
- 'table',
- array(
- 'sigil' => 'maniphest-batch-actions',
- 'class' => 'maniphest-batch-actions-table',
- ),
- '')))
- ->appendChild(
- id(new AphrontFormSubmitControl())
- ->setValue(pht('Update Tasks'))
- ->addCancelButton($cancel_uri));
-
- $title = pht('Batch Editor');
-
- $crumbs = $this->buildApplicationCrumbs();
- $crumbs->addTextCrumb($title);
- $crumbs->setBorder(true);
-
- $header = id(new PHUIHeaderView())
- ->setHeader(pht('Batch Editor'))
- ->setHeaderIcon('fa-pencil-square-o');
-
- $task_box = id(new PHUIObjectBoxView())
- ->setHeaderText(pht('Selected Tasks'))
- ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
- ->setObjectList($list);
-
- $form_box = id(new PHUIObjectBoxView())
- ->setHeaderText(pht('Actions'))
- ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
- ->setForm($form);
-
- $view = id(new PHUITwoColumnView())
- ->setHeader($header)
- ->setFooter(array(
- $task_box,
- $form_box,
- ));
-
- return $this->newPage()
- ->setTitle($title)
- ->setCrumbs($crumbs)
- ->appendChild($view);
- }
-
-}
diff --git a/src/applications/maniphest/controller/ManiphestExportController.php b/src/applications/maniphest/controller/ManiphestExportController.php
deleted file mode 100644
index 1201c5047..000000000
--- a/src/applications/maniphest/controller/ManiphestExportController.php
+++ /dev/null
@@ -1,135 +0,0 @@
-<?php
-
-final class ManiphestExportController extends ManiphestController {
-
- /**
- * @phutil-external-symbol class PHPExcel
- * @phutil-external-symbol class PHPExcel_IOFactory
- * @phutil-external-symbol class PHPExcel_Style_NumberFormat
- * @phutil-external-symbol class PHPExcel_Cell_DataType
- */
- public function handleRequest(AphrontRequest $request) {
- $viewer = $this->getViewer();
- $key = $request->getURIData('key');
-
- $ok = @include_once 'PHPExcel.php';
- if (!$ok) {
- $dialog = $this->newDialog();
-
- $inst1 = pht(
- 'This system does not have PHPExcel installed. This software '.
- 'component is required to export tasks to Excel. Have your system '.
- 'administrator install it from:');
-
- $inst2 = pht(
- 'Your PHP "%s" needs to be updated to include the '.
- 'PHPExcel Classes directory.',
- 'include_path');
-
- $dialog->setTitle(pht('Excel Export Not Configured'));
- $dialog->appendChild(hsprintf(
- '<p>%s</p>'.
- '<br />'.
- '<p>'.
- '<a href="https://github.com/PHPOffice/PHPExcel">'.
- 'https://github.com/PHPOffice/PHPExcel'.
- '</a>'.
- '</p>'.
- '<br />'.
- '<p>%s</p>',
- $inst1,
- $inst2));
-
- $dialog->addCancelButton('/maniphest/');
- return id(new AphrontDialogResponse())->setDialog($dialog);
- }
-
- // TODO: PHPExcel has a dependency on the PHP zip extension. We should test
- // for that here, since it fatals if we don't have the ZipArchive class.
-
- $saved = id(new PhabricatorSavedQueryQuery())
- ->setViewer($viewer)
- ->withQueryKeys(array($key))
- ->executeOne();
- if (!$saved) {
- $engine = id(new ManiphestTaskSearchEngine())
- ->setViewer($viewer);
- if ($engine->isBuiltinQuery($key)) {
- $saved = $engine->buildSavedQueryFromBuiltin($key);
- }
- if (!$saved) {
- return new Aphront404Response();
- }
- }
-
- $formats = ManiphestExcelFormat::loadAllFormats();
- $export_formats = array();
- foreach ($formats as $format_class => $format_object) {
- $export_formats[$format_class] = $format_object->getName();
- }
-
- if (!$request->isDialogFormPost()) {
- $dialog = new AphrontDialogView();
- $dialog->setUser($viewer);
-
- $dialog->setTitle(pht('Export Tasks to Excel'));
- $dialog->appendChild(
- phutil_tag(
- 'p',
- array(),
- pht('Do you want to export the query results to Excel?')));
-
- $form = id(new PHUIFormLayoutView())
- ->appendChild(
- id(new AphrontFormSelectControl())
- ->setLabel(pht('Format:'))
- ->setName('excel-format')
- ->setOptions($export_formats));
-
- $dialog->appendChild($form);
-
- $dialog->addCancelButton('/maniphest/');
- $dialog->addSubmitButton(pht('Export to Excel'));
- return id(new AphrontDialogResponse())->setDialog($dialog);
- }
-
- $format = idx($formats, $request->getStr('excel-format'));
- if ($format === null) {
- throw new Exception(pht('Excel format object not found.'));
- }
-
- $saved->makeEphemeral();
- $saved->setParameter('limit', PHP_INT_MAX);
-
- $engine = id(new ManiphestTaskSearchEngine())
- ->setViewer($viewer);
-
- $query = $engine->buildQueryFromSavedQuery($saved);
- $query->setViewer($viewer);
- $tasks = $query->execute();
-
- $all_projects = array_mergev(mpull($tasks, 'getProjectPHIDs'));
- $all_assigned = mpull($tasks, 'getOwnerPHID');
-
- $handles = id(new PhabricatorHandleQuery())
- ->setViewer($viewer)
- ->withPHIDs(array_merge($all_projects, $all_assigned))
- ->execute();
-
- $workbook = new PHPExcel();
- $format->buildWorkbook($workbook, $tasks, $handles, $viewer);
- $writer = PHPExcel_IOFactory::createWriter($workbook, 'Excel2007');
-
- ob_start();
- $writer->save('php://output');
- $data = ob_get_clean();
-
- $mime = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
-
- return id(new AphrontFileResponse())
- ->setMimeType($mime)
- ->setDownload($format->getFileName().'.xlsx')
- ->setContent($data);
- }
-
-}
diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php
index ecfb723f9..a11cc4d85 100644
--- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php
+++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php
@@ -1,598 +1,595 @@
<?php
final class ManiphestTaskDetailController extends ManiphestController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
$task = id(new ManiphestTaskQuery())
->setViewer($viewer)
->withIDs(array($id))
->needSubscriberPHIDs(true)
->executeOne();
if (!$task) {
return new Aphront404Response();
}
$field_list = PhabricatorCustomField::getObjectFields(
$task,
PhabricatorCustomField::ROLE_VIEW);
$field_list
->setViewer($viewer)
->readFieldsFromStorage($task);
$edit_engine = id(new ManiphestEditEngine())
->setViewer($viewer)
->setTargetObject($task);
$edge_types = array(
ManiphestTaskHasCommitEdgeType::EDGECONST,
ManiphestTaskHasRevisionEdgeType::EDGECONST,
ManiphestTaskHasMockEdgeType::EDGECONST,
PhabricatorObjectMentionedByObjectEdgeType::EDGECONST,
PhabricatorObjectMentionsObjectEdgeType::EDGECONST,
ManiphestTaskHasDuplicateTaskEdgeType::EDGECONST,
);
$phid = $task->getPHID();
$query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($phid))
->withEdgeTypes($edge_types);
$edges = idx($query->execute(), $phid);
$phids = array_fill_keys($query->getDestinationPHIDs(), true);
if ($task->getOwnerPHID()) {
$phids[$task->getOwnerPHID()] = true;
}
$phids[$task->getAuthorPHID()] = true;
$phids = array_keys($phids);
$handles = $viewer->loadHandles($phids);
$timeline = $this->buildTransactionTimeline(
$task,
new ManiphestTransactionQuery());
$monogram = $task->getMonogram();
$crumbs = $this->buildApplicationCrumbs()
->addTextCrumb($monogram)
->setBorder(true);
$header = $this->buildHeaderView($task);
$details = $this->buildPropertyView($task, $field_list, $edges, $handles);
$description = $this->buildDescriptionView($task);
$curtain = $this->buildCurtain($task, $edit_engine);
$title = pht('%s %s', $monogram, $task->getTitle());
$comment_view = $edit_engine
->buildEditEngineCommentView($task);
$timeline->setQuoteRef($monogram);
$comment_view->setTransactionTimeline($timeline);
$related_tabs = array();
$graph_menu = null;
$graph_limit = 100;
$task_graph = id(new ManiphestTaskGraph())
->setViewer($viewer)
->setSeedPHID($task->getPHID())
->setLimit($graph_limit)
->loadGraph();
if (!$task_graph->isEmpty()) {
$parent_type = ManiphestTaskDependedOnByTaskEdgeType::EDGECONST;
$subtask_type = ManiphestTaskDependsOnTaskEdgeType::EDGECONST;
$parent_map = $task_graph->getEdges($parent_type);
$subtask_map = $task_graph->getEdges($subtask_type);
$parent_list = idx($parent_map, $task->getPHID(), array());
$subtask_list = idx($subtask_map, $task->getPHID(), array());
$has_parents = (bool)$parent_list;
$has_subtasks = (bool)$subtask_list;
$search_text = pht('Search...');
// First, get a count of direct parent tasks and subtasks. If there
// are too many of these, we just don't draw anything. You can use
// the search button to browse tasks with the search UI instead.
$direct_count = count($parent_list) + count($subtask_list);
if ($direct_count > $graph_limit) {
$message = pht(
'Task graph too large to display (this task is directly connected '.
'to more than %s other tasks). Use %s to explore connected tasks.',
$graph_limit,
phutil_tag('strong', array(), $search_text));
$message = phutil_tag('em', array(), $message);
$graph_table = id(new PHUIPropertyListView())
->addTextContent($message);
} else {
// If there aren't too many direct tasks, but there are too many total
// tasks, we'll only render directly connected tasks.
if ($task_graph->isOverLimit()) {
$task_graph->setRenderOnlyAdjacentNodes(true);
}
$graph_table = $task_graph->newGraphTable();
}
$parents_uri = urisprintf(
'/?subtaskIDs=%d#R',
$task->getID());
$parents_uri = $this->getApplicationURI($parents_uri);
$subtasks_uri = urisprintf(
'/?parentIDs=%d#R',
$task->getID());
$subtasks_uri = $this->getApplicationURI($subtasks_uri);
$dropdown_menu = id(new PhabricatorActionListView())
->setViewer($viewer)
->addAction(
id(new PhabricatorActionView())
->setHref($parents_uri)
->setName(pht('Search Parent Tasks'))
->setDisabled(!$has_parents)
->setIcon('fa-chevron-circle-up'))
->addAction(
id(new PhabricatorActionView())
->setHref($subtasks_uri)
->setName(pht('Search Subtasks'))
->setDisabled(!$has_subtasks)
->setIcon('fa-chevron-circle-down'));
$graph_menu = id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-search')
->setText($search_text)
->setDropdownMenu($dropdown_menu);
$related_tabs[] = id(new PHUITabView())
->setName(pht('Task Graph'))
->setKey('graph')
->appendChild($graph_table);
}
$related_tabs[] = $this->newMocksTab($task, $query);
$related_tabs[] = $this->newMentionsTab($task, $query);
$related_tabs[] = $this->newDuplicatesTab($task, $query);
$tab_view = null;
$related_tabs = array_filter($related_tabs);
if ($related_tabs) {
$tab_group = new PHUITabGroupView();
foreach ($related_tabs as $tab) {
$tab_group->addTab($tab);
}
$related_header = id(new PHUIHeaderView())
->setHeader(pht('Related Objects'));
if ($graph_menu) {
$related_header->addActionLink($graph_menu);
}
$tab_view = id(new PHUIObjectBoxView())
->setHeader($related_header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->addTabGroup($tab_group);
}
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setCurtain($curtain)
->setMainColumn(
array(
$tab_view,
$timeline,
$comment_view,
))
->addPropertySection(pht('Description'), $description)
->addPropertySection(pht('Details'), $details);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->setPageObjectPHIDs(
array(
$task->getPHID(),
))
->appendChild($view);
}
private function buildHeaderView(ManiphestTask $task) {
$view = id(new PHUIHeaderView())
->setHeader($task->getTitle())
->setUser($this->getRequest()->getUser())
->setPolicyObject($task);
$priority_name = ManiphestTaskPriority::getTaskPriorityName(
$task->getPriority());
$priority_color = ManiphestTaskPriority::getTaskPriorityColor(
$task->getPriority());
$status = $task->getStatus();
$status_name = ManiphestTaskStatus::renderFullDescription(
$status, $priority_name);
$view->addProperty(PHUIHeaderView::PROPERTY_STATUS, $status_name);
$view->setHeaderIcon(ManiphestTaskStatus::getStatusIcon(
$task->getStatus()).' '.$priority_color);
if (ManiphestTaskPoints::getIsEnabled()) {
$points = $task->getPoints();
if ($points !== null) {
$points_name = pht('%s %s',
$task->getPoints(),
ManiphestTaskPoints::getPointsLabel());
$tag = id(new PHUITagView())
->setName($points_name)
->setColor(PHUITagView::COLOR_BLUE)
->setType(PHUITagView::TYPE_SHADE);
$view->addTag($tag);
}
}
$subtype = $task->newSubtypeObject();
if ($subtype && $subtype->hasTagView()) {
$subtype_tag = $subtype->newTagView();
$view->addTag($subtype_tag);
}
return $view;
}
private function buildCurtain(
ManiphestTask $task,
PhabricatorEditEngine $edit_engine) {
$viewer = $this->getViewer();
$id = $task->getID();
$phid = $task->getPHID();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$task,
PhabricatorPolicyCapability::CAN_EDIT);
$can_interact = PhabricatorPolicyFilter::canInteract($viewer, $task);
// We expect a policy dialog if you can't edit the task, and expect a
// lock override dialog if you can't interact with it.
$workflow_edit = (!$can_edit || !$can_interact);
$curtain = $this->newCurtainView($task);
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Task'))
->setIcon('fa-pencil')
->setHref($this->getApplicationURI("/task/edit/{$id}/"))
->setDisabled(!$can_edit)
->setWorkflow($workflow_edit));
$edit_config = $edit_engine->loadDefaultEditConfiguration($task);
$can_create = (bool)$edit_config;
- $can_reassign = $edit_engine->hasEditAccessToTransaction(
- ManiphestTaskOwnerTransaction::TRANSACTIONTYPE);
-
if ($can_create) {
$form_key = $edit_config->getIdentifier();
$edit_uri = id(new PhutilURI("/task/edit/form/{$form_key}/"))
->setQueryParam('parent', $id)
->setQueryParam('template', $id)
->setQueryParam('status', ManiphestTaskStatus::getDefaultStatus());
$edit_uri = $this->getApplicationURI($edit_uri);
} else {
// TODO: This will usually give us a somewhat-reasonable error page, but
// could be a bit cleaner.
$edit_uri = "/task/edit/{$id}/";
$edit_uri = $this->getApplicationURI($edit_uri);
}
$subtask_item = id(new PhabricatorActionView())
->setName(pht('Create Subtask'))
->setHref($edit_uri)
->setIcon('fa-level-down')
->setDisabled(!$can_create)
->setWorkflow(!$can_create);
$relationship_list = PhabricatorObjectRelationshipList::newForObject(
$viewer,
$task);
$submenu_actions = array(
$subtask_item,
ManiphestTaskHasParentRelationship::RELATIONSHIPKEY,
ManiphestTaskHasSubtaskRelationship::RELATIONSHIPKEY,
ManiphestTaskMergeInRelationship::RELATIONSHIPKEY,
ManiphestTaskCloseAsDuplicateRelationship::RELATIONSHIPKEY,
);
$task_submenu = $relationship_list->newActionSubmenu($submenu_actions)
->setName(pht('Edit Related Tasks...'))
->setIcon('fa-anchor');
$curtain->addAction($task_submenu);
$relationship_submenu = $relationship_list->newActionMenu();
if ($relationship_submenu) {
$curtain->addAction($relationship_submenu);
}
$owner_phid = $task->getOwnerPHID();
$author_phid = $task->getAuthorPHID();
$handles = $viewer->loadHandles(array($owner_phid, $author_phid));
if ($owner_phid) {
$image_uri = $handles[$owner_phid]->getImageURI();
$image_href = $handles[$owner_phid]->getURI();
$owner = $viewer->renderHandle($owner_phid)->render();
$content = phutil_tag('strong', array(), $owner);
$assigned_to = id(new PHUIHeadThingView())
->setImage($image_uri)
->setImageHref($image_href)
->setContent($content);
} else {
$assigned_to = phutil_tag('em', array(), pht('None'));
}
$curtain->newPanel()
->setHeaderText(pht('Assigned To'))
->appendChild($assigned_to);
$author_uri = $handles[$author_phid]->getImageURI();
$author_href = $handles[$author_phid]->getURI();
$author = $viewer->renderHandle($author_phid)->render();
$content = phutil_tag('strong', array(), $author);
$date = phabricator_date($task->getDateCreated(), $viewer);
$content = pht('%s, %s', $content, $date);
$authored_by = id(new PHUIHeadThingView())
->setImage($author_uri)
->setImageHref($author_href)
->setContent($content);
$curtain->newPanel()
->setHeaderText(pht('Authored By'))
->appendChild($authored_by);
return $curtain;
}
private function buildPropertyView(
ManiphestTask $task,
PhabricatorCustomFieldList $field_list,
array $edges,
$handles) {
$viewer = $this->getRequest()->getUser();
$view = id(new PHUIPropertyListView())
->setUser($viewer);
$source = $task->getOriginalEmailSource();
if ($source) {
$subject = '[T'.$task->getID().'] '.$task->getTitle();
$view->addProperty(
pht('From Email'),
phutil_tag(
'a',
array(
'href' => 'mailto:'.$source.'?subject='.$subject,
),
$source));
}
$edge_types = array(
ManiphestTaskHasRevisionEdgeType::EDGECONST
=> pht('Differential Revisions'),
);
$revisions_commits = array();
$commit_phids = array_keys(
$edges[ManiphestTaskHasCommitEdgeType::EDGECONST]);
if ($commit_phids) {
$commit_drev = DiffusionCommitHasRevisionEdgeType::EDGECONST;
$drev_edges = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($commit_phids)
->withEdgeTypes(array($commit_drev))
->execute();
foreach ($commit_phids as $phid) {
$revisions_commits[$phid] = $handles->renderHandle($phid)
->setShowHovercard(true)
->setShowStateIcon(true);
$revision_phid = key($drev_edges[$phid][$commit_drev]);
$revision_handle = $handles->getHandleIfExists($revision_phid);
if ($revision_handle) {
$task_drev = ManiphestTaskHasRevisionEdgeType::EDGECONST;
unset($edges[$task_drev][$revision_phid]);
$revisions_commits[$phid] = hsprintf(
'%s / %s',
$revision_handle->renderHovercardLink($revision_handle->getName()),
$revisions_commits[$phid]);
}
}
}
foreach ($edge_types as $edge_type => $edge_name) {
if (!$edges[$edge_type]) {
continue;
}
$edge_handles = $viewer->loadHandles(array_keys($edges[$edge_type]));
$edge_list = $edge_handles->renderList()
->setShowStateIcons(true);
$view->addProperty($edge_name, $edge_list);
}
if ($revisions_commits) {
$view->addProperty(
pht('Commits'),
phutil_implode_html(phutil_tag('br'), $revisions_commits));
}
$field_list->appendFieldsToPropertyList(
$task,
$viewer,
$view);
if ($view->hasAnyProperties()) {
return $view;
}
return null;
}
private function buildDescriptionView(ManiphestTask $task) {
$viewer = $this->getViewer();
$section = null;
$description = $task->getDescription();
if (strlen($description)) {
$section = new PHUIPropertyListView();
$section->addTextContent(
phutil_tag(
'div',
array(
'class' => 'phabricator-remarkup',
),
id(new PHUIRemarkupView($viewer, $description))
->setContextObject($task)));
}
return $section;
}
private function newMocksTab(
ManiphestTask $task,
PhabricatorEdgeQuery $edge_query) {
$mock_type = ManiphestTaskHasMockEdgeType::EDGECONST;
$mock_phids = $edge_query->getDestinationPHIDs(array(), array($mock_type));
if (!$mock_phids) {
return null;
}
$viewer = $this->getViewer();
$handles = $viewer->loadHandles($mock_phids);
// TODO: It would be nice to render this as pinboard-style thumbnails,
// similar to "{M123}", instead of a list of links.
$view = id(new PHUIPropertyListView())
->addProperty(pht('Mocks'), $handles->renderList());
return id(new PHUITabView())
->setName(pht('Mocks'))
->setKey('mocks')
->appendChild($view);
}
private function newMentionsTab(
ManiphestTask $task,
PhabricatorEdgeQuery $edge_query) {
$in_type = PhabricatorObjectMentionedByObjectEdgeType::EDGECONST;
$out_type = PhabricatorObjectMentionsObjectEdgeType::EDGECONST;
$in_phids = $edge_query->getDestinationPHIDs(array(), array($in_type));
$out_phids = $edge_query->getDestinationPHIDs(array(), array($out_type));
// Filter out any mentioned users from the list. These are not generally
// very interesting to show in a relationship summary since they usually
// end up as subscribers anyway.
$user_type = PhabricatorPeopleUserPHIDType::TYPECONST;
foreach ($out_phids as $key => $out_phid) {
if (phid_get_type($out_phid) == $user_type) {
unset($out_phids[$key]);
}
}
if (!$in_phids && !$out_phids) {
return null;
}
$viewer = $this->getViewer();
$in_handles = $viewer->loadHandles($in_phids);
$out_handles = $viewer->loadHandles($out_phids);
$in_handles = $this->getCompleteHandles($in_handles);
$out_handles = $this->getCompleteHandles($out_handles);
if (!count($in_handles) && !count($out_handles)) {
return null;
}
$view = new PHUIPropertyListView();
if (count($in_handles)) {
$view->addProperty(pht('Mentioned In'), $in_handles->renderList());
}
if (count($out_handles)) {
$view->addProperty(pht('Mentioned Here'), $out_handles->renderList());
}
return id(new PHUITabView())
->setName(pht('Mentions'))
->setKey('mentions')
->appendChild($view);
}
private function newDuplicatesTab(
ManiphestTask $task,
PhabricatorEdgeQuery $edge_query) {
$in_type = ManiphestTaskHasDuplicateTaskEdgeType::EDGECONST;
$in_phids = $edge_query->getDestinationPHIDs(array(), array($in_type));
$viewer = $this->getViewer();
$in_handles = $viewer->loadHandles($in_phids);
$in_handles = $this->getCompleteHandles($in_handles);
$view = new PHUIPropertyListView();
if (!count($in_handles)) {
return null;
}
$view->addProperty(
pht('Duplicates Merged Here'), $in_handles->renderList());
return id(new PHUITabView())
->setName(pht('Duplicates'))
->setKey('duplicates')
->appendChild($view);
}
private function getCompleteHandles(PhabricatorHandleList $handles) {
$phids = array();
foreach ($handles as $phid => $handle) {
if (!$handle->isComplete()) {
continue;
}
$phids[] = $phid;
}
return $handles->newSublist($phids);
}
}
diff --git a/src/applications/maniphest/editor/ManiphestEditEngine.php b/src/applications/maniphest/editor/ManiphestEditEngine.php
index 359ba493d..c27010403 100644
--- a/src/applications/maniphest/editor/ManiphestEditEngine.php
+++ b/src/applications/maniphest/editor/ManiphestEditEngine.php
@@ -1,528 +1,529 @@
<?php
final class ManiphestEditEngine
extends PhabricatorEditEngine {
const ENGINECONST = 'maniphest.task';
public function getEngineName() {
return pht('Maniphest Tasks');
}
public function getSummaryHeader() {
return pht('Configure Maniphest Task Forms');
}
public function getSummaryText() {
return pht('Configure how users create and edit tasks.');
}
public function getEngineApplicationClass() {
return 'PhabricatorManiphestApplication';
}
public function isDefaultQuickCreateEngine() {
return true;
}
public function getQuickCreateOrderVector() {
return id(new PhutilSortVector())->addInt(100);
}
protected function newEditableObject() {
return ManiphestTask::initializeNewTask($this->getViewer());
}
protected function newObjectQuery() {
return id(new ManiphestTaskQuery());
}
protected function getObjectCreateTitleText($object) {
return pht('Create New Task');
}
protected function getObjectEditTitleText($object) {
return pht('Edit Task: %s', $object->getTitle());
}
protected function getObjectEditShortText($object) {
return $object->getMonogram();
}
protected function getObjectCreateShortText() {
return pht('Create Task');
}
protected function getObjectName() {
return pht('Task');
}
protected function getEditorURI() {
return $this->getApplication()->getApplicationURI('task/edit/');
}
protected function getCommentViewHeaderText($object) {
return pht('Weigh In');
}
protected function getCommentViewButtonText($object) {
return pht('Set Sail for Adventure');
}
protected function getObjectViewURI($object) {
return '/'.$object->getMonogram();
}
protected function buildCustomEditFields($object) {
$status_map = $this->getTaskStatusMap($object);
$priority_map = $this->getTaskPriorityMap($object);
$alias_map = ManiphestTaskPriority::getTaskPriorityAliasMap();
if ($object->isClosed()) {
$default_status = ManiphestTaskStatus::getDefaultStatus();
} else {
$default_status = ManiphestTaskStatus::getDefaultClosedStatus();
}
if ($object->getOwnerPHID()) {
$owner_value = array($object->getOwnerPHID());
} else {
$owner_value = array($this->getViewer()->getPHID());
}
$column_documentation = pht(<<<EODOCS
You can use this transaction type to create a task into a particular workboard
column, or move an existing task between columns.
The transaction value can be specified in several forms. Some are simpler but
less powerful, while others are more complex and more powerful.
The simplest valid value is a single column PHID:
```lang=json
"PHID-PCOL-1111"
```
This will move the task into that column, or create the task into that column
if you are creating a new task. If the task is currently on the board, it will
be moved out of any exclusive columns. If the task is not currently on the
board, it will be added to the board.
You can also perform multiple moves at the same time by passing a list of
PHIDs:
```lang=json
["PHID-PCOL-2222", "PHID-PCOL-3333"]
```
This is equivalent to performing each move individually.
The most complex and most powerful form uses a dictionary to provide additional
information about the move, including an optional specific position within the
column.
The target column should be identified as `columnPHID`, and you may select a
position by passing either `beforePHID` or `afterPHID`, specifying the PHID of
a task currently in the column that you want to move this task before or after:
```lang=json
[
{
"columnPHID": "PHID-PCOL-4444",
"beforePHID": "PHID-TASK-5555"
}
]
```
Note that this affects only the "natural" position of the task. The task
position when the board is sorted by some other attribute (like priority)
depends on that attribute value: change a task's priority to move it on
priority-sorted boards.
EODOCS
);
$column_map = $this->getColumnMap($object);
$fields = array(
id(new PhabricatorHandlesEditField())
->setKey('parent')
->setLabel(pht('Parent Task'))
->setDescription(pht('Task to make this a subtask of.'))
->setConduitDescription(pht('Create as a subtask of another task.'))
->setConduitTypeDescription(pht('PHID of the parent task.'))
->setAliases(array('parentPHID'))
->setTransactionType(ManiphestTaskParentTransaction::TRANSACTIONTYPE)
->setHandleParameterType(new ManiphestTaskListHTTPParameterType())
->setSingleValue(null)
->setIsReorderable(false)
->setIsDefaultable(false)
->setIsLockable(false),
id(new PhabricatorColumnsEditField())
->setKey('column')
->setLabel(pht('Column'))
->setDescription(pht('Create a task in a workboard column.'))
->setConduitDescription(
pht('Move a task to one or more workboard columns.'))
->setConduitTypeDescription(
pht('List of columns to move the task to.'))
->setConduitDocumentation($column_documentation)
->setAliases(array('columnPHID', 'columns', 'columnPHIDs'))
->setTransactionType(PhabricatorTransactions::TYPE_COLUMNS)
->setIsReorderable(false)
->setIsDefaultable(false)
->setIsLockable(false)
->setCommentActionLabel(pht('Move on Workboard'))
->setCommentActionOrder(2000)
->setColumnMap($column_map),
id(new PhabricatorTextEditField())
->setKey('title')
->setLabel(pht('Title'))
->setBulkEditLabel(pht('Set title to'))
->setDescription(pht('Name of the task.'))
->setConduitDescription(pht('Rename the task.'))
->setConduitTypeDescription(pht('New task name.'))
->setTransactionType(ManiphestTaskTitleTransaction::TRANSACTIONTYPE)
->setIsRequired(true)
->setValue($object->getTitle()),
id(new PhabricatorUsersEditField())
->setKey('owner')
->setAliases(array('ownerPHID', 'assign', 'assigned'))
->setLabel(pht('Assigned To'))
->setBulkEditLabel(pht('Assign to'))
->setDescription(pht('User who is responsible for the task.'))
->setConduitDescription(pht('Reassign the task.'))
->setConduitTypeDescription(
pht('New task owner, or `null` to unassign.'))
->setTransactionType(ManiphestTaskOwnerTransaction::TRANSACTIONTYPE)
->setIsCopyable(true)
+ ->setIsNullable(true)
->setSingleValue($object->getOwnerPHID())
->setCommentActionLabel(pht('Assign / Claim'))
->setCommentActionValue($owner_value),
id(new PhabricatorSelectEditField())
->setKey('status')
->setLabel(pht('Status'))
->setBulkEditLabel(pht('Set status to'))
->setDescription(pht('Status of the task.'))
->setConduitDescription(pht('Change the task status.'))
->setConduitTypeDescription(pht('New task status constant.'))
->setTransactionType(ManiphestTaskStatusTransaction::TRANSACTIONTYPE)
->setIsCopyable(true)
->setValue($object->getStatus())
->setOptions($status_map)
->setCommentActionLabel(pht('Change Status'))
->setCommentActionValue($default_status),
id(new PhabricatorSelectEditField())
->setKey('priority')
->setLabel(pht('Priority'))
->setBulkEditLabel(pht('Set priority to'))
->setDescription(pht('Priority of the task.'))
->setConduitDescription(pht('Change the priority of the task.'))
->setConduitTypeDescription(pht('New task priority constant.'))
->setTransactionType(ManiphestTaskPriorityTransaction::TRANSACTIONTYPE)
->setIsCopyable(true)
->setValue($object->getPriorityKeyword())
->setOptions($priority_map)
->setOptionAliases($alias_map)
->setCommentActionLabel(pht('Change Priority')),
);
if (ManiphestTaskPoints::getIsEnabled()) {
$points_label = ManiphestTaskPoints::getPointsLabel();
$action_label = ManiphestTaskPoints::getPointsActionLabel();
$fields[] = id(new PhabricatorPointsEditField())
->setKey('points')
->setLabel($points_label)
->setBulkEditLabel($action_label)
->setDescription(pht('Point value of the task.'))
->setConduitDescription(pht('Change the task point value.'))
->setConduitTypeDescription(pht('New task point value.'))
->setTransactionType(ManiphestTaskPointsTransaction::TRANSACTIONTYPE)
->setIsCopyable(true)
->setValue($object->getPoints())
->setCommentActionLabel($action_label);
}
$fields[] = id(new PhabricatorRemarkupEditField())
->setKey('description')
->setLabel(pht('Description'))
->setBulkEditLabel(pht('Set description to'))
->setDescription(pht('Task description.'))
->setConduitDescription(pht('Update the task description.'))
->setConduitTypeDescription(pht('New task description.'))
->setTransactionType(ManiphestTaskDescriptionTransaction::TRANSACTIONTYPE)
->setValue($object->getDescription())
->setPreviewPanel(
id(new PHUIRemarkupPreviewPanel())
->setHeader(pht('Description Preview')));
$parent_type = ManiphestTaskDependedOnByTaskEdgeType::EDGECONST;
$subtask_type = ManiphestTaskDependsOnTaskEdgeType::EDGECONST;
$src_phid = $object->getPHID();
if ($src_phid) {
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($src_phid))
->withEdgeTypes(
array(
$parent_type,
$subtask_type,
));
$edge_query->execute();
$parent_phids = $edge_query->getDestinationPHIDs(
array($src_phid),
array($parent_type));
$subtask_phids = $edge_query->getDestinationPHIDs(
array($src_phid),
array($subtask_type));
} else {
$parent_phids = array();
$subtask_phids = array();
}
$fields[] = id(new PhabricatorHandlesEditField())
->setKey('parents')
->setLabel(pht('Parents'))
->setDescription(pht('Parent tasks.'))
->setConduitDescription(pht('Change the parents of this task.'))
->setConduitTypeDescription(pht('List of parent task PHIDs.'))
->setUseEdgeTransactions(true)
->setIsConduitOnly(true)
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $parent_type)
->setValue($parent_phids);
$fields[] = id(new PhabricatorHandlesEditField())
->setKey('subtasks')
->setLabel(pht('Subtasks'))
->setDescription(pht('Subtasks.'))
->setConduitDescription(pht('Change the subtasks of this task.'))
->setConduitTypeDescription(pht('List of subtask PHIDs.'))
->setUseEdgeTransactions(true)
->setIsConduitOnly(true)
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $subtask_type)
->setValue($parent_phids);
return $fields;
}
private function getTaskStatusMap(ManiphestTask $task) {
$status_map = ManiphestTaskStatus::getTaskStatusMap();
$current_status = $task->getStatus();
// If the current status is something we don't recognize (maybe an older
// status which was deleted), put a dummy entry in the status map so that
// saving the form doesn't destroy any data by accident.
if (idx($status_map, $current_status) === null) {
$status_map[$current_status] = pht('<Unknown: %s>', $current_status);
}
$dup_status = ManiphestTaskStatus::getDuplicateStatus();
foreach ($status_map as $status => $status_name) {
// Always keep the task's current status.
if ($status == $current_status) {
continue;
}
// Don't allow tasks to be changed directly into "Closed, Duplicate"
// status. Instead, you have to merge them. See T4819.
if ($status == $dup_status) {
unset($status_map[$status]);
continue;
}
// Don't let new or existing tasks be moved into a disabled status.
if (ManiphestTaskStatus::isDisabledStatus($status)) {
unset($status_map[$status]);
continue;
}
}
return $status_map;
}
private function getTaskPriorityMap(ManiphestTask $task) {
$priority_map = ManiphestTaskPriority::getTaskPriorityMap();
$priority_keywords = ManiphestTaskPriority::getTaskPriorityKeywordsMap();
$current_priority = $task->getPriority();
$results = array();
foreach ($priority_map as $priority => $priority_name) {
$disabled = ManiphestTaskPriority::isDisabledPriority($priority);
if ($disabled && !($priority == $current_priority)) {
continue;
}
$keyword = head(idx($priority_keywords, $priority));
$results[$keyword] = $priority_name;
}
// If the current value isn't a legitimate one, put it in the dropdown
// anyway so saving the form doesn't cause any side effects.
if (idx($priority_map, $current_priority) === null) {
$results[ManiphestTaskPriority::UNKNOWN_PRIORITY_KEYWORD] = pht(
'<Unknown: %s>',
$current_priority);
}
return $results;
}
protected function newEditResponse(
AphrontRequest $request,
$object,
array $xactions) {
if ($request->isAjax()) {
// Reload the task to make sure we pick up the final task state.
$viewer = $this->getViewer();
$task = id(new ManiphestTaskQuery())
->setViewer($viewer)
->withIDs(array($object->getID()))
->needSubscriberPHIDs(true)
->needProjectPHIDs(true)
->executeOne();
switch ($request->getStr('responseType')) {
case 'card':
return $this->buildCardResponse($task);
default:
return $this->buildListResponse($task);
}
}
return parent::newEditResponse($request, $object, $xactions);
}
private function buildListResponse(ManiphestTask $task) {
$controller = $this->getController();
$payload = array(
'tasks' => $controller->renderSingleTask($task),
'data' => array(),
);
return id(new AphrontAjaxResponse())->setContent($payload);
}
private function buildCardResponse(ManiphestTask $task) {
$controller = $this->getController();
$request = $controller->getRequest();
$viewer = $request->getViewer();
$column_phid = $request->getStr('columnPHID');
$visible_phids = $request->getStrList('visiblePHIDs');
if (!$visible_phids) {
$visible_phids = array();
}
$column = id(new PhabricatorProjectColumnQuery())
->setViewer($viewer)
->withPHIDs(array($column_phid))
->executeOne();
if (!$column) {
return new Aphront404Response();
}
$board_phid = $column->getProjectPHID();
$object_phid = $task->getPHID();
return id(new PhabricatorBoardResponseEngine())
->setViewer($viewer)
->setBoardPHID($board_phid)
->setObjectPHID($object_phid)
->setVisiblePHIDs($visible_phids)
->buildResponse();
}
private function getColumnMap(ManiphestTask $task) {
$phid = $task->getPHID();
if (!$phid) {
return array();
}
$board_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$phid,
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
if (!$board_phids) {
return array();
}
$viewer = $this->getViewer();
$layout_engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($viewer)
->setBoardPHIDs($board_phids)
->setObjectPHIDs(array($task->getPHID()))
->executeLayout();
$map = array();
foreach ($board_phids as $board_phid) {
$in_columns = $layout_engine->getObjectColumns($board_phid, $phid);
$in_columns = mpull($in_columns, null, 'getPHID');
$all_columns = $layout_engine->getColumns($board_phid);
if (!$all_columns) {
// This could be a project with no workboard, or a project the viewer
// does not have permission to see.
continue;
}
$board = head($all_columns)->getProject();
$options = array();
foreach ($all_columns as $column) {
$name = $column->getDisplayName();
$is_hidden = $column->isHidden();
$is_selected = isset($in_columns[$column->getPHID()]);
// Don't show hidden, subproject or milestone columns in this map
// unless the object is currently in the column.
$skip_column = ($is_hidden || $column->getProxyPHID());
if ($skip_column) {
if (!$is_selected) {
continue;
}
}
if ($is_hidden) {
$name = pht('(%s)', $name);
}
if ($is_selected) {
$name = pht("\xE2\x97\x8F %s", $name);
} else {
$name = pht("\xE2\x97\x8B %s", $name);
}
$option = array(
'key' => $column->getPHID(),
'label' => $name,
'selected' => (bool)$is_selected,
);
$options[] = $option;
}
$map[] = array(
'label' => $board->getDisplayName(),
'options' => $options,
);
}
$map = isort($map, 'label');
$map = array_values($map);
return $map;
}
}
diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
index caf70b8f3..66247ca6d 100644
--- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php
+++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
@@ -1,1061 +1,1059 @@
<?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}")
- ->addHeader('Thread-Topic', "T{$id}: ".$object->getOriginalTitle());
+ ->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' => '""',
- 'originalTitle' => '""',
'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/engineextension/ManiphestMailEngineExtension.php b/src/applications/maniphest/engineextension/ManiphestMailEngineExtension.php
new file mode 100644
index 000000000..ee38bdf60
--- /dev/null
+++ b/src/applications/maniphest/engineextension/ManiphestMailEngineExtension.php
@@ -0,0 +1,58 @@
+<?php
+
+final class ManiphestMailEngineExtension
+ extends PhabricatorMailEngineExtension {
+
+ const EXTENSIONKEY = 'maniphest';
+
+ public function supportsObject($object) {
+ return ($object instanceof ManiphestTask);
+ }
+
+ public function newMailStampTemplates($object) {
+ return array(
+ id(new PhabricatorPHIDMailStamp())
+ ->setKey('author')
+ ->setLabel(pht('Author')),
+ id(new PhabricatorPHIDMailStamp())
+ ->setKey('task-owner')
+ ->setLabel(pht('Task Owner')),
+ id(new PhabricatorBoolMailStamp())
+ ->setKey('task-unassigned')
+ ->setLabel(pht('Task Unassigned')),
+ id(new PhabricatorStringMailStamp())
+ ->setKey('task-priority')
+ ->setLabel(pht('Task Priority')),
+ id(new PhabricatorStringMailStamp())
+ ->setKey('task-status')
+ ->setLabel(pht('Task Status')),
+ id(new PhabricatorStringMailStamp())
+ ->setKey('subtype')
+ ->setLabel(pht('Subtype')),
+ );
+ }
+
+ public function newMailStamps($object, array $xactions) {
+ $editor = $this->getEditor();
+ $viewer = $this->getViewer();
+
+ $this->getMailStamp('author')
+ ->setValue($object->getAuthorPHID());
+
+ $this->getMailStamp('task-owner')
+ ->setValue($object->getOwnerPHID());
+
+ $this->getMailStamp('task-unassigned')
+ ->setValue(!$object->getOwnerPHID());
+
+ $this->getMailStamp('task-priority')
+ ->setValue($object->getPriority());
+
+ $this->getMailStamp('task-status')
+ ->setValue($object->getStatus());
+
+ $this->getMailStamp('subtype')
+ ->setValue($object->getSubtype());
+ }
+
+}
diff --git a/src/applications/maniphest/export/ManiphestExcelDefaultFormat.php b/src/applications/maniphest/export/ManiphestExcelDefaultFormat.php
deleted file mode 100644
index 1451ae504..000000000
--- a/src/applications/maniphest/export/ManiphestExcelDefaultFormat.php
+++ /dev/null
@@ -1,140 +0,0 @@
-<?php
-
-final class ManiphestExcelDefaultFormat extends ManiphestExcelFormat {
-
- public function getName() {
- return pht('Default');
- }
-
- public function getFileName() {
- return 'maniphest_tasks_'.date('Ymd');
- }
-
- /**
- * @phutil-external-symbol class PHPExcel
- * @phutil-external-symbol class PHPExcel_IOFactory
- * @phutil-external-symbol class PHPExcel_Style_NumberFormat
- * @phutil-external-symbol class PHPExcel_Cell_DataType
- */
- public function buildWorkbook(
- PHPExcel $workbook,
- array $tasks,
- array $handles,
- PhabricatorUser $user) {
-
- $sheet = $workbook->setActiveSheetIndex(0);
- $sheet->setTitle(pht('Tasks'));
-
- $widths = array(
- null,
- 15,
- null,
- 10,
- 15,
- 15,
- 60,
- 30,
- 20,
- 100,
- );
-
- foreach ($widths as $col => $width) {
- if ($width !== null) {
- $sheet->getColumnDimension($this->col($col))->setWidth($width);
- }
- }
-
- $status_map = ManiphestTaskStatus::getTaskStatusMap();
- $pri_map = ManiphestTaskPriority::getTaskPriorityMap();
-
- $date_format = null;
-
- $rows = array();
- $rows[] = array(
- pht('ID'),
- pht('Owner'),
- pht('Status'),
- pht('Priority'),
- pht('Date Created'),
- pht('Date Updated'),
- pht('Title'),
- pht('Tags'),
- pht('URI'),
- pht('Description'),
- );
-
- $is_date = array(
- false,
- false,
- false,
- false,
- true,
- true,
- false,
- false,
- false,
- false,
- );
-
- $header_format = array(
- 'font' => array(
- 'bold' => true,
- ),
- );
-
- foreach ($tasks as $task) {
- $task_owner = null;
- if ($task->getOwnerPHID()) {
- $task_owner = $handles[$task->getOwnerPHID()]->getName();
- }
-
- $projects = array();
- foreach ($task->getProjectPHIDs() as $phid) {
- $projects[] = $handles[$phid]->getName();
- }
- $projects = implode(', ', $projects);
-
- $rows[] = array(
- 'T'.$task->getID(),
- $task_owner,
- idx($status_map, $task->getStatus(), '?'),
- idx($pri_map, $task->getPriority(), '?'),
- $this->computeExcelDate($task->getDateCreated()),
- $this->computeExcelDate($task->getDateModified()),
- $task->getTitle(),
- $projects,
- PhabricatorEnv::getProductionURI('/T'.$task->getID()),
- id(new PhutilUTF8StringTruncator())
- ->setMaximumBytes(512)
- ->truncateString($task->getDescription()),
- );
- }
-
- foreach ($rows as $row => $cols) {
- foreach ($cols as $col => $spec) {
- $cell_name = $this->col($col).($row + 1);
- $cell = $sheet
- ->setCellValue($cell_name, $spec, $return_cell = true);
-
- if ($row == 0) {
- $sheet->getStyle($cell_name)->applyFromArray($header_format);
- }
-
- if ($is_date[$col]) {
- $code = PHPExcel_Style_NumberFormat::FORMAT_DATE_YYYYMMDD2;
- $sheet
- ->getStyle($cell_name)
- ->getNumberFormat()
- ->setFormatCode($code);
- } else {
- $cell->setDataType(PHPExcel_Cell_DataType::TYPE_STRING);
- }
- }
- }
- }
-
- private function col($n) {
- return chr(ord('A') + $n);
- }
-
-}
diff --git a/src/applications/maniphest/export/ManiphestExcelFormat.php b/src/applications/maniphest/export/ManiphestExcelFormat.php
deleted file mode 100644
index e455a6365..000000000
--- a/src/applications/maniphest/export/ManiphestExcelFormat.php
+++ /dev/null
@@ -1,35 +0,0 @@
-<?php
-
-abstract class ManiphestExcelFormat extends Phobject {
-
- final public static function loadAllFormats() {
- return id(new PhutilClassMapQuery())
- ->setAncestorClass(__CLASS__)
- ->setSortMethod('getOrder')
- ->execute();
- }
-
- abstract public function getName();
- abstract public function getFileName();
-
- public function getOrder() {
- return 0;
- }
-
- protected function computeExcelDate($epoch) {
- $seconds_per_day = (60 * 60 * 24);
- $offset = ($seconds_per_day * 25569);
-
- return ($epoch + $offset) / $seconds_per_day;
- }
-
- /**
- * @phutil-external-symbol class PHPExcel
- */
- abstract public function buildWorkbook(
- PHPExcel $workbook,
- array $tasks,
- array $handles,
- PhabricatorUser $user);
-
-}
diff --git a/src/applications/maniphest/export/__tests__/ManiphestExcelFormatTestCase.php b/src/applications/maniphest/export/__tests__/ManiphestExcelFormatTestCase.php
deleted file mode 100644
index a3c312fcd..000000000
--- a/src/applications/maniphest/export/__tests__/ManiphestExcelFormatTestCase.php
+++ /dev/null
@@ -1,10 +0,0 @@
-<?php
-
-final class ManiphestExcelFormatTestCase extends PhabricatorTestCase {
-
- public function testLoadAllFormats() {
- ManiphestExcelFormat::loadAllFormats();
- $this->assertTrue(true);
- }
-
-}
diff --git a/src/applications/maniphest/query/ManiphestTaskQuery.php b/src/applications/maniphest/query/ManiphestTaskQuery.php
index cfc69722d..f7c1551be 100644
--- a/src/applications/maniphest/query/ManiphestTaskQuery.php
+++ b/src/applications/maniphest/query/ManiphestTaskQuery.php
@@ -1,849 +1,896 @@
<?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 $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) {
$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;
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 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);
}
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) {
$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 ec1956bd8..565bc7a8f 100644
--- a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php
+++ b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php
@@ -1,435 +1,584 @@
<?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 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']);
}
$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_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())
+ ->setKey('points')
+ ->setLabel('Points');
+ }
+
+ return $fields;
+ }
+
+ protected function newExportData(array $tasks) {
+ $viewer = $this->requireViewer();
+
+ $phids = array();
+ foreach ($tasks as $task) {
+ $phids[] = $task->getAuthorPHID();
+ $phids[] = $task->getOwnerPHID();
+ $phids[] = $task->getCloserPHID();
+ }
+ $handles = $viewer->loadHandles($phids);
+
+ $export = array();
+ foreach ($tasks as $task) {
+
+ $author_phid = $task->getAuthorPHID();
+ if ($author_phid) {
+ $author_name = $handles[$author_phid]->getName();
+ } else {
+ $author_name = null;
+ }
+
+ $owner_phid = $task->getOwnerPHID();
+ if ($owner_phid) {
+ $owner_name = $handles[$owner_phid]->getName();
+ } else {
+ $owner_name = null;
+ }
+
+ $closer_phid = $task->getCloserPHID();
+ if ($closer_phid) {
+ $closer_name = $handles[$closer_phid]->getName();
+ } else {
+ $closer_name = null;
+ }
+
+ $status_value = $task->getStatus();
+ $status_name = ManiphestTaskStatus::getTaskStatusName($status_value);
+
+ $priority_value = $task->getPriority();
+ $priority_name = ManiphestTaskPriority::getTaskPriorityName(
+ $priority_value);
+
+ $export[] = array(
+ 'monogram' => $task->getMonogram(),
+ 'authorPHID' => $author_phid,
+ 'author' => $author_name,
+ 'ownerPHID' => $owner_phid,
+ 'owner' => $owner_name,
+ 'status' => $status_value,
+ 'statusName' => $status_name,
+ 'priority' => $priority_value,
+ 'priorityName' => $priority_name,
+ 'points' => $task->getPoints(),
+ 'subtype' => $task->getSubtype(),
+ 'title' => $task->getTitle(),
+ 'uri' => PhabricatorEnv::getProductionURI($task->getURI()),
+ 'description' => $task->getDescription(),
+ 'dateClosed' => $task->getClosedEpoch(),
+ 'closerPHID' => $closer_phid,
+ 'closer' => $closer_name,
+ );
+ }
+
+ return $export;
+ }
}
diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php
index f72977c5b..a93fe58c3 100644
--- a/src/applications/maniphest/storage/ManiphestTask.php
+++ b/src/applications/maniphest/storage/ManiphestTask.php
@@ -1,615 +1,633 @@
<?php
final class ManiphestTask extends ManiphestDAO
implements
PhabricatorSubscribableInterface,
PhabricatorMarkupInterface,
PhabricatorPolicyInterface,
PhabricatorTokenReceiverInterface,
PhabricatorFlaggableInterface,
PhabricatorMentionableInterface,
PhrequentTrackableInterface,
PhabricatorCustomFieldInterface,
PhabricatorDestructibleInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorProjectInterface,
PhabricatorSpacesInterface,
PhabricatorConduitResultInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface,
DoorkeeperBridgedObjectInterface,
PhabricatorEditEngineSubtypeInterface,
PhabricatorEditEngineLockableInterface {
const MARKUP_FIELD_DESCRIPTION = 'markup:desc';
protected $authorPHID;
protected $ownerPHID;
protected $status;
protected $priority;
protected $subpriority = 0;
protected $title = '';
- protected $originalTitle = '';
protected $description = '';
protected $originalEmailSource;
protected $mailKey;
protected $viewPolicy = PhabricatorPolicies::POLICY_USER;
protected $editPolicy = PhabricatorPolicies::POLICY_USER;
protected $ownerOrdering;
protected $spacePHID;
protected $bridgedObjectPHID;
protected $properties = array();
protected $points;
protected $subtype;
+ protected $closedEpoch;
+ protected $closerPHID;
+
private $subscriberPHIDs = self::ATTACHABLE;
private $groupByProjectPHID = self::ATTACHABLE;
private $customFields = self::ATTACHABLE;
private $edgeProjectPHIDs = self::ATTACHABLE;
private $bridgedObject = self::ATTACHABLE;
public static function initializeNewTask(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorManiphestApplication'))
->executeOne();
$view_policy = $app->getPolicy(ManiphestDefaultViewCapability::CAPABILITY);
$edit_policy = $app->getPolicy(ManiphestDefaultEditCapability::CAPABILITY);
return id(new ManiphestTask())
->setStatus(ManiphestTaskStatus::getDefaultStatus())
->setPriority(ManiphestTaskPriority::getDefaultPriority())
->setAuthorPHID($actor->getPHID())
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy)
->setSpacePHID($actor->getDefaultSpacePHID())
->setSubtype(PhabricatorEditEngineSubtype::SUBTYPE_DEFAULT)
->attachProjectPHIDs(array())
->attachSubscriberPHIDs(array());
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'ownerPHID' => 'phid?',
'status' => 'text64',
'priority' => 'uint32',
'title' => 'sort',
- 'originalTitle' => 'text',
'description' => 'text',
'mailKey' => 'bytes20',
'ownerOrdering' => 'text64?',
'originalEmailSource' => 'text255?',
'subpriority' => 'double',
'points' => 'double?',
'bridgedObjectPHID' => 'phid?',
'subtype' => 'text64',
+ 'closedEpoch' => 'epoch?',
+ 'closerPHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'priority' => array(
'columns' => array('priority', 'status'),
),
'status' => array(
'columns' => array('status'),
),
'ownerPHID' => array(
'columns' => array('ownerPHID', 'status'),
),
'authorPHID' => array(
'columns' => array('authorPHID', 'status'),
),
'ownerOrdering' => array(
'columns' => array('ownerOrdering'),
),
'priority_2' => array(
'columns' => array('priority', 'subpriority'),
),
'key_dateCreated' => array(
'columns' => array('dateCreated'),
),
'key_dateModified' => array(
'columns' => array('dateModified'),
),
'key_title' => array(
'columns' => array('title(64)'),
),
'key_bridgedobject' => array(
'columns' => array('bridgedObjectPHID'),
'unique' => true,
),
'key_subtype' => array(
'columns' => array('subtype'),
),
+ 'key_closed' => array(
+ 'columns' => array('closedEpoch'),
+ ),
+ 'key_closer' => array(
+ 'columns' => array('closerPHID', 'closedEpoch'),
+ ),
),
) + parent::getConfiguration();
}
public function loadDependsOnTaskPHIDs() {
return PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->getPHID(),
ManiphestTaskDependsOnTaskEdgeType::EDGECONST);
}
public function loadDependedOnByTaskPHIDs() {
return PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->getPHID(),
ManiphestTaskDependedOnByTaskEdgeType::EDGECONST);
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(ManiphestTaskPHIDType::TYPECONST);
}
public function getSubscriberPHIDs() {
return $this->assertAttached($this->subscriberPHIDs);
}
public function getProjectPHIDs() {
return $this->assertAttached($this->edgeProjectPHIDs);
}
public function attachProjectPHIDs(array $phids) {
$this->edgeProjectPHIDs = $phids;
return $this;
}
public function attachSubscriberPHIDs(array $phids) {
$this->subscriberPHIDs = $phids;
return $this;
}
public function setOwnerPHID($phid) {
$this->ownerPHID = nonempty($phid, null);
return $this;
}
- public function setTitle($title) {
- $this->title = $title;
- if (!$this->getID()) {
- $this->originalTitle = $title;
- }
- return $this;
- }
-
public function getMonogram() {
return 'T'.$this->getID();
}
public function getURI() {
return '/'.$this->getMonogram();
}
public function attachGroupByProjectPHID($phid) {
$this->groupByProjectPHID = $phid;
return $this;
}
public function getGroupByProjectPHID() {
return $this->assertAttached($this->groupByProjectPHID);
}
public function save() {
if (!$this->mailKey) {
$this->mailKey = Filesystem::readRandomCharacters(20);
}
$result = parent::save();
return $result;
}
public function isClosed() {
return ManiphestTaskStatus::isClosedStatus($this->getStatus());
}
public function isLocked() {
return ManiphestTaskStatus::isLockedStatus($this->getStatus());
}
public function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function getProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function getCoverImageFilePHID() {
return idx($this->properties, 'cover.filePHID');
}
public function getCoverImageThumbnailPHID() {
return idx($this->properties, 'cover.thumbnailPHID');
}
public function getWorkboardOrderVectors() {
return array(
PhabricatorProjectColumn::ORDER_PRIORITY => array(
(int)-$this->getPriority(),
(double)-$this->getSubpriority(),
(int)-$this->getID(),
),
);
}
public function getPriorityKeyword() {
$priority = $this->getPriority();
$keyword = ManiphestTaskPriority::getKeywordForTaskPriority($priority);
if ($keyword !== null) {
return $keyword;
}
return ManiphestTaskPriority::UNKNOWN_PRIORITY_KEYWORD;
}
private function comparePriorityTo(ManiphestTask $other) {
$upri = $this->getPriority();
$vpri = $other->getPriority();
if ($upri != $vpri) {
return ($upri - $vpri);
}
$usub = $this->getSubpriority();
$vsub = $other->getSubpriority();
if ($usub != $vsub) {
return ($usub - $vsub);
}
$uid = $this->getID();
$vid = $other->getID();
if ($uid != $vid) {
return ($uid - $vid);
}
return 0;
}
public function isLowerPriorityThan(ManiphestTask $other) {
return ($this->comparePriorityTo($other) < 0);
}
public function isHigherPriorityThan(ManiphestTask $other) {
return ($this->comparePriorityTo($other) > 0);
}
public function getWorkboardProperties() {
return array(
'status' => $this->getStatus(),
'points' => (double)$this->getPoints(),
);
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return ($phid == $this->getOwnerPHID());
}
/* -( Markup Interface )--------------------------------------------------- */
/**
* @task markup
*/
public function getMarkupFieldKey($field) {
$content = $this->getMarkupText($field);
return PhabricatorMarkupEngine::digestRemarkupContent($this, $content);
}
/**
* @task markup
*/
public function getMarkupText($field) {
return $this->getDescription();
}
/**
* @task markup
*/
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newManiphestMarkupEngine();
}
/**
* @task markup
*/
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
return $output;
}
/**
* @task markup
*/
public function shouldUseMarkupCache($field) {
return (bool)$this->getID();
}
/* -( Policy Interface )--------------------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_INTERACT,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_INTERACT:
if ($this->isLocked()) {
return PhabricatorPolicies::POLICY_NOONE;
} else {
return $this->getViewPolicy();
}
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
// The owner of a task can always view and edit it.
$owner_phid = $this->getOwnerPHID();
if ($owner_phid) {
$user_phid = $user->getPHID();
if ($user_phid == $owner_phid) {
return true;
}
}
return false;
}
public function describeAutomaticCapability($capability) {
return pht('The owner of a task can always view and edit it.');
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
// Sort of ambiguous who this was intended for; just let them both know.
return array_filter(
array_unique(
array(
$this->getAuthorPHID(),
$this->getOwnerPHID(),
)));
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return PhabricatorEnv::getEnvConfig('maniphest.fields');
}
public function getCustomFieldBaseClass() {
return 'ManiphestCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new ManiphestTransactionEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new ManiphestTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorSpacesInterface )----------------------------------------- */
public function getSpacePHID() {
return $this->spacePHID;
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('title')
->setType('string')
->setDescription(pht('The title of the task.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('description')
->setType('remarkup')
->setDescription(pht('The task description.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('authorPHID')
->setType('phid')
->setDescription(pht('Original task author.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('ownerPHID')
->setType('phid?')
->setDescription(pht('Current task owner, if task is assigned.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('status')
->setType('map<string, wild>')
->setDescription(pht('Information about task status.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('priority')
->setType('map<string, wild>')
->setDescription(pht('Information about task priority.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('points')
->setType('points')
->setDescription(pht('Point value of the task.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('subtype')
->setType('string')
->setDescription(pht('Subtype of the task.')),
+ id(new PhabricatorConduitSearchFieldSpecification())
+ ->setKey('closerPHID')
+ ->setType('phid?')
+ ->setDescription(
+ pht('User who closed the task, if the task is closed.')),
+ id(new PhabricatorConduitSearchFieldSpecification())
+ ->setKey('dateClosed')
+ ->setType('int?')
+ ->setDescription(
+ pht('Epoch timestamp when the task was closed.')),
);
}
public function getFieldValuesForConduit() {
$status_value = $this->getStatus();
$status_info = array(
'value' => $status_value,
'name' => ManiphestTaskStatus::getTaskStatusName($status_value),
'color' => ManiphestTaskStatus::getStatusColor($status_value),
);
$priority_value = (int)$this->getPriority();
$priority_info = array(
'value' => $priority_value,
'subpriority' => (double)$this->getSubpriority(),
'name' => ManiphestTaskPriority::getTaskPriorityName($priority_value),
'color' => ManiphestTaskPriority::getTaskPriorityColor($priority_value),
);
+ $closed_epoch = $this->getClosedEpoch();
+ if ($closed_epoch !== null) {
+ $closed_epoch = (int)$closed_epoch;
+ }
+
return array(
'name' => $this->getTitle(),
'description' => array(
'raw' => $this->getDescription(),
),
'authorPHID' => $this->getAuthorPHID(),
'ownerPHID' => $this->getOwnerPHID(),
'status' => $status_info,
'priority' => $priority_info,
'points' => $this->getPoints(),
'subtype' => $this->getSubtype(),
+ 'closerPHID' => $this->getCloserPHID(),
+ 'dateClosed' => $closed_epoch,
);
}
public function getConduitSearchAttachments() {
return array(
id(new PhabricatorBoardColumnsSearchEngineAttachment())
->setAttachmentKey('columns'),
);
}
public function newSubtypeObject() {
$subtype_key = $this->getEditEngineSubtype();
$subtype_map = $this->newEditEngineSubtypeMap();
return idx($subtype_map, $subtype_key);
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new ManiphestTaskFulltextEngine();
}
/* -( DoorkeeperBridgedObjectInterface )----------------------------------- */
public function getBridgedObject() {
return $this->assertAttached($this->bridgedObject);
}
public function attachBridgedObject(
DoorkeeperExternalObject $object = null) {
$this->bridgedObject = $object;
return $this;
}
/* -( PhabricatorEditEngineSubtypeInterface )------------------------------ */
public function getEditEngineSubtype() {
return $this->getSubtype();
}
public function setEditEngineSubtype($value) {
return $this->setSubtype($value);
}
public function newEditEngineSubtypeMap() {
$config = PhabricatorEnv::getEnvConfig('maniphest.subtypes');
return PhabricatorEditEngineSubtype::newSubtypeMap($config);
}
/* -( PhabricatorEditEngineLockableInterface )----------------------------- */
public function newEditEngineLock() {
return new ManiphestTaskEditEngineLock();
}
/* -( PhabricatorFerretInterface )----------------------------------------- */
public function newFerretEngine() {
return new ManiphestTaskFerretEngine();
}
}
diff --git a/src/applications/maniphest/storage/ManiphestTransaction.php b/src/applications/maniphest/storage/ManiphestTransaction.php
index 630124105..e2739c1d0 100644
--- a/src/applications/maniphest/storage/ManiphestTransaction.php
+++ b/src/applications/maniphest/storage/ManiphestTransaction.php
@@ -1,240 +1,224 @@
<?php
final class ManiphestTransaction
extends PhabricatorModularTransaction {
const MAILTAG_STATUS = 'maniphest-status';
const MAILTAG_OWNER = 'maniphest-owner';
const MAILTAG_PRIORITY = 'maniphest-priority';
const MAILTAG_CC = 'maniphest-cc';
const MAILTAG_PROJECTS = 'maniphest-projects';
const MAILTAG_COMMENT = 'maniphest-comment';
const MAILTAG_COLUMN = 'maniphest-column';
const MAILTAG_UNBLOCK = 'maniphest-unblock';
const MAILTAG_OTHER = 'maniphest-other';
public function getApplicationName() {
return 'maniphest';
}
public function getApplicationTransactionType() {
return ManiphestTaskPHIDType::TYPECONST;
}
public function getApplicationTransactionCommentObject() {
return new ManiphestTransactionComment();
}
public function getBaseTransactionClass() {
return 'ManiphestTaskTransactionType';
}
public function shouldGenerateOldValue() {
switch ($this->getTransactionType()) {
case ManiphestTaskEdgeTransaction::TRANSACTIONTYPE:
case ManiphestTaskUnblockTransaction::TRANSACTIONTYPE:
return false;
}
return parent::shouldGenerateOldValue();
}
- public function shouldHideForFeed() {
- // NOTE: Modular transactions don't currently support this, and it has
- // very few callsites, and it's publish-time rather than display-time.
- // This should probably become a supported, display-time behavior. For
- // discussion, see T12787.
-
- // Hide "alice created X, a task blocking Y." from feed because it
- // will almost always appear adjacent to "alice created Y".
- $is_new = $this->getMetadataValue('blocker.new');
- if ($is_new) {
- return true;
- }
-
- return parent::shouldHideForFeed();
- }
-
public function getRequiredHandlePHIDs() {
$phids = parent::getRequiredHandlePHIDs();
$new = $this->getNewValue();
$old = $this->getOldValue();
switch ($this->getTransactionType()) {
case ManiphestTaskOwnerTransaction::TRANSACTIONTYPE:
if ($new) {
$phids[] = $new;
}
if ($old) {
$phids[] = $old;
}
break;
case ManiphestTaskMergedIntoTransaction::TRANSACTIONTYPE:
$phids[] = $new;
break;
case ManiphestTaskMergedFromTransaction::TRANSACTIONTYPE:
$phids = array_merge($phids, $new);
break;
case ManiphestTaskEdgeTransaction::TRANSACTIONTYPE:
$phids = array_mergev(
array(
$phids,
array_keys(nonempty($old, array())),
array_keys(nonempty($new, array())),
));
break;
case ManiphestTaskAttachTransaction::TRANSACTIONTYPE:
$old = nonempty($old, array());
$new = nonempty($new, array());
$phids = array_mergev(
array(
$phids,
array_keys(idx($new, 'FILE', array())),
array_keys(idx($old, 'FILE', array())),
));
break;
case ManiphestTaskUnblockTransaction::TRANSACTIONTYPE:
foreach (array_keys($new) as $phid) {
$phids[] = $phid;
}
break;
case ManiphestTaskStatusTransaction::TRANSACTIONTYPE:
$commit_phid = $this->getMetadataValue('commitPHID');
if ($commit_phid) {
$phids[] = $commit_phid;
}
break;
}
return $phids;
}
public function getActionName() {
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COLUMNS:
return pht('Changed Project Column');
}
return parent::getActionName();
}
public function getIcon() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COLUMNS:
return 'fa-columns';
}
return parent::getIcon();
}
public function getTitle() {
$author_phid = $this->getAuthorPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_SUBTYPE:
return pht(
'%s changed the subtype of this task from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$this->renderSubtypeName($old),
$this->renderSubtypeName($new));
break;
}
return parent::getTitle();
}
public function getTitleForFeed() {
$author_phid = $this->getAuthorPHID();
$object_phid = $this->getObjectPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_SUBTYPE:
return pht(
'%s changed the subtype of %s from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
$this->renderSubtypeName($old),
$this->renderSubtypeName($new));
}
return parent::getTitleForFeed();
}
public function getMailTags() {
$tags = array();
switch ($this->getTransactionType()) {
case ManiphestTaskMergedIntoTransaction::TRANSACTIONTYPE:
case ManiphestTaskStatusTransaction::TRANSACTIONTYPE:
$tags[] = self::MAILTAG_STATUS;
break;
case ManiphestTaskOwnerTransaction::TRANSACTIONTYPE:
$tags[] = self::MAILTAG_OWNER;
break;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$tags[] = self::MAILTAG_CC;
break;
case PhabricatorTransactions::TYPE_EDGE:
switch ($this->getMetadataValue('edge:type')) {
case PhabricatorProjectObjectHasProjectEdgeType::EDGECONST:
$tags[] = self::MAILTAG_PROJECTS;
break;
default:
$tags[] = self::MAILTAG_OTHER;
break;
}
break;
case ManiphestTaskPriorityTransaction::TRANSACTIONTYPE:
$tags[] = self::MAILTAG_PRIORITY;
break;
case ManiphestTaskUnblockTransaction::TRANSACTIONTYPE:
$tags[] = self::MAILTAG_UNBLOCK;
break;
case PhabricatorTransactions::TYPE_COLUMNS:
$tags[] = self::MAILTAG_COLUMN;
break;
case PhabricatorTransactions::TYPE_COMMENT:
$tags[] = self::MAILTAG_COMMENT;
break;
default:
$tags[] = self::MAILTAG_OTHER;
break;
}
return $tags;
}
public function getNoEffectDescription() {
switch ($this->getTransactionType()) {
case ManiphestTaskStatusTransaction::TRANSACTIONTYPE:
return pht('The task already has the selected status.');
case ManiphestTaskOwnerTransaction::TRANSACTIONTYPE:
return pht('The task already has the selected owner.');
case ManiphestTaskPriorityTransaction::TRANSACTIONTYPE:
return pht('The task already has the selected priority.');
}
return parent::getNoEffectDescription();
}
public function renderSubtypeName($value) {
$object = $this->getObject();
$map = $object->newEditEngineSubtypeMap();
if (!isset($map[$value])) {
return $value;
}
return $map[$value]->getName();
}
}
diff --git a/src/applications/maniphest/view/ManiphestTaskListView.php b/src/applications/maniphest/view/ManiphestTaskListView.php
index de6b386ac..ba17b8e25 100644
--- a/src/applications/maniphest/view/ManiphestTaskListView.php
+++ b/src/applications/maniphest/view/ManiphestTaskListView.php
@@ -1,167 +1,182 @@
<?php
final class ManiphestTaskListView extends ManiphestView {
private $tasks;
private $handles;
private $showBatchControls;
private $showSubpriorityControls;
private $noDataString;
public function setTasks(array $tasks) {
assert_instances_of($tasks, 'ManiphestTask');
$this->tasks = $tasks;
return $this;
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function setShowBatchControls($show_batch_controls) {
$this->showBatchControls = $show_batch_controls;
return $this;
}
public function setShowSubpriorityControls($show_subpriority_controls) {
$this->showSubpriorityControls = $show_subpriority_controls;
return $this;
}
public function setNoDataString($text) {
$this->noDataString = $text;
return $this;
}
public function render() {
$handles = $this->handles;
require_celerity_resource('maniphest-task-summary-css');
$list = new PHUIObjectItemListView();
if ($this->noDataString) {
$list->setNoDataString($this->noDataString);
} else {
$list->setNoDataString(pht('No tasks.'));
}
$status_map = ManiphestTaskStatus::getTaskStatusMap();
$color_map = ManiphestTaskPriority::getColorMap();
$priority_map = ManiphestTaskPriority::getTaskPriorityMap();
if ($this->showBatchControls) {
Javelin::initBehavior('maniphest-list-editor');
}
$subtype_map = id(new ManiphestTask())
->newEditEngineSubtypeMap();
foreach ($this->tasks as $task) {
$item = id(new PHUIObjectItemView())
->setUser($this->getUser())
->setObject($task)
->setObjectName('T'.$task->getID())
->setHeader($task->getTitle())
->setHref('/T'.$task->getID());
if ($task->getOwnerPHID()) {
$owner = $handles[$task->getOwnerPHID()];
$item->addByline(pht('Assigned: %s', $owner->renderLink()));
}
$status = $task->getStatus();
$pri = idx($priority_map, $task->getPriority());
$status_name = idx($status_map, $task->getStatus());
$tooltip = pht('%s, %s', $status_name, $pri);
$icon = ManiphestTaskStatus::getStatusIcon($task->getStatus());
$color = idx($color_map, $task->getPriority(), 'grey');
if ($task->isClosed()) {
$item->setDisabled(true);
$color = 'grey';
}
$item->setStatusIcon($icon.' '.$color, $tooltip);
- $item->addIcon(
- 'none',
- phabricator_datetime($task->getDateModified(), $this->getUser()));
+ if ($task->isClosed()) {
+ $closed_epoch = $task->getClosedEpoch();
+
+ // We don't expect a task to be closed without a closed epoch, but
+ // recover if we find one. This can happen with older objects or with
+ // lipsum test data.
+ if (!$closed_epoch) {
+ $closed_epoch = $task->getDateModified();
+ }
+
+ $item->addIcon(
+ 'fa-check-square-o grey',
+ phabricator_datetime($closed_epoch, $this->getUser()));
+ } else {
+ $item->addIcon(
+ 'none',
+ phabricator_datetime($task->getDateModified(), $this->getUser()));
+ }
if ($this->showSubpriorityControls) {
$item->setGrippable(true);
}
if ($this->showSubpriorityControls || $this->showBatchControls) {
$item->addSigil('maniphest-task');
}
$subtype = $task->newSubtypeObject();
if ($subtype && $subtype->hasTagView()) {
$subtype_tag = $subtype->newTagView()
->setSlimShady(true);
$item->addAttribute($subtype_tag);
}
$project_handles = array_select_keys(
$handles,
array_reverse($task->getProjectPHIDs()));
$item->addAttribute(
id(new PHUIHandleTagListView())
->setLimit(4)
->setNoDataString(pht('No Projects'))
->setSlim(true)
->setHandles($project_handles));
$item->setMetadata(
array(
'taskID' => $task->getID(),
));
if ($this->showBatchControls) {
$href = new PhutilURI('/maniphest/task/edit/'.$task->getID().'/');
if (!$this->showSubpriorityControls) {
$href->setQueryParam('ungrippable', 'true');
}
$item->addAction(
id(new PHUIListItemView())
->setIcon('fa-pencil')
->addSigil('maniphest-edit-task')
->setHref($href));
}
$list->addItem($item);
}
return $list;
}
public static function loadTaskHandles(
PhabricatorUser $viewer,
array $tasks) {
assert_instances_of($tasks, 'ManiphestTask');
$phids = array();
foreach ($tasks as $task) {
$assigned_phid = $task->getOwnerPHID();
if ($assigned_phid) {
$phids[] = $assigned_phid;
}
foreach ($task->getProjectPHIDs() as $project_phid) {
$phids[] = $project_phid;
}
}
if (!$phids) {
return array();
}
return id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs($phids)
->execute();
}
}
diff --git a/src/applications/maniphest/view/ManiphestTaskResultListView.php b/src/applications/maniphest/view/ManiphestTaskResultListView.php
index b4cf9a544..6aafcbdcc 100644
--- a/src/applications/maniphest/view/ManiphestTaskResultListView.php
+++ b/src/applications/maniphest/view/ManiphestTaskResultListView.php
@@ -1,271 +1,260 @@
<?php
final class ManiphestTaskResultListView extends ManiphestView {
private $tasks;
private $savedQuery;
private $canEditPriority;
private $canBatchEdit;
private $showBatchControls;
public function setSavedQuery(PhabricatorSavedQuery $query) {
$this->savedQuery = $query;
return $this;
}
public function setTasks(array $tasks) {
$this->tasks = $tasks;
return $this;
}
public function setCanEditPriority($can_edit_priority) {
$this->canEditPriority = $can_edit_priority;
return $this;
}
public function setCanBatchEdit($can_batch_edit) {
$this->canBatchEdit = $can_batch_edit;
return $this;
}
public function setShowBatchControls($show_batch_controls) {
$this->showBatchControls = $show_batch_controls;
return $this;
}
public function render() {
$viewer = $this->getUser();
$tasks = $this->tasks;
$query = $this->savedQuery;
// If we didn't match anything, just pick up the default empty state.
if (!$tasks) {
return id(new PHUIObjectItemListView())
->setUser($viewer)
->setNoDataString(pht('No tasks found.'));
}
$group_parameter = nonempty($query->getParameter('group'), 'priority');
$order_parameter = nonempty($query->getParameter('order'), 'priority');
$handles = ManiphestTaskListView::loadTaskHandles($viewer, $tasks);
$groups = $this->groupTasks(
$tasks,
$group_parameter,
$handles);
$can_edit_priority = $this->canEditPriority;
$can_drag = ($order_parameter == 'priority') &&
($can_edit_priority) &&
($group_parameter == 'none' || $group_parameter == 'priority');
if (!$viewer->isLoggedIn()) {
// TODO: (T7131) Eventually, we conceivably need to make each task
// draggable individually, since the user may be able to edit some but
// not others.
$can_drag = false;
}
$result = array();
$lists = array();
foreach ($groups as $group => $list) {
$task_list = new ManiphestTaskListView();
$task_list->setShowBatchControls($this->showBatchControls);
if ($can_drag) {
$task_list->setShowSubpriorityControls(true);
}
$task_list->setUser($viewer);
$task_list->setTasks($list);
$task_list->setHandles($handles);
$header = id(new PHUIHeaderView())
->addSigil('task-group')
->setMetadata(array('priority' => head($list)->getPriority()))
->setHeader(pht('%s (%s)', $group, phutil_count($list)));
$lists[] = id(new PHUIObjectBoxView())
->setHeader($header)
->setObjectList($task_list);
}
if ($can_drag) {
Javelin::initBehavior(
'maniphest-subpriority-editor',
array(
'uri' => '/maniphest/subpriority/',
));
}
return array(
$lists,
$this->showBatchControls ? $this->renderBatchEditor($query) : null,
);
}
private function groupTasks(array $tasks, $group, array $handles) {
assert_instances_of($tasks, 'ManiphestTask');
assert_instances_of($handles, 'PhabricatorObjectHandle');
$groups = $this->getTaskGrouping($tasks, $group);
$results = array();
foreach ($groups as $label_key => $tasks) {
$label = $this->getTaskLabelName($group, $label_key, $handles);
$results[$label][] = $tasks;
}
foreach ($results as $label => $task_groups) {
$results[$label] = array_mergev($task_groups);
}
return $results;
}
private function getTaskGrouping(array $tasks, $group) {
switch ($group) {
case 'priority':
return mgroup($tasks, 'getPriority');
case 'status':
return mgroup($tasks, 'getStatus');
case 'assigned':
return mgroup($tasks, 'getOwnerPHID');
case 'project':
return mgroup($tasks, 'getGroupByProjectPHID');
default:
return array(pht('Tasks') => $tasks);
}
}
private function getTaskLabelName($group, $label_key, array $handles) {
switch ($group) {
case 'priority':
return ManiphestTaskPriority::getTaskPriorityName($label_key);
case 'status':
return ManiphestTaskStatus::getTaskStatusFullName($label_key);
case 'assigned':
if ($label_key) {
return $handles[$label_key]->getFullName();
} else {
return pht('(Not Assigned)');
}
case 'project':
if ($label_key) {
return $handles[$label_key]->getFullName();
} else {
// This may mean "No Projects", or it may mean the query has project
// constraints but the task is only in constrained projects (in this
// case, we don't show the group because it would always have all
// of the tasks). Since distinguishing between these two cases is
// messy and the UI is reasonably clear, label generically.
return pht('(Ungrouped)');
}
default:
return pht('Tasks');
}
}
private function renderBatchEditor(PhabricatorSavedQuery $saved_query) {
$user = $this->getUser();
if (!$this->canBatchEdit) {
return null;
}
if (!$user->isLoggedIn()) {
- // Don't show the batch editor or excel export for logged-out users.
- // Technically we //could// let them export, but ehh.
+ // Don't show the batch editor for logged-out users.
return null;
}
Javelin::initBehavior(
'maniphest-batch-selector',
array(
'selectAll' => 'batch-select-all',
'selectNone' => 'batch-select-none',
'submit' => 'batch-select-submit',
'status' => 'batch-select-status-cell',
'idContainer' => 'batch-select-id-container',
'formID' => 'batch-select-form',
));
$select_all = javelin_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
'class' => 'button button-grey',
'id' => 'batch-select-all',
),
pht('Select All'));
$select_none = javelin_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
'class' => 'button button-grey',
'id' => 'batch-select-none',
),
pht('Clear Selection'));
$submit = phutil_tag(
'button',
array(
'id' => 'batch-select-submit',
'disabled' => 'disabled',
'class' => 'disabled',
),
pht("Bulk Edit Selected \xC2\xBB"));
- $export = javelin_tag(
- 'a',
- array(
- 'href' => '/maniphest/export/'.$saved_query->getQueryKey().'/',
- 'class' => 'button button-grey',
- ),
- pht('Export to Excel'));
-
$hidden = phutil_tag(
'div',
array(
'id' => 'batch-select-id-container',
),
'');
$editor = hsprintf(
'<table class="maniphest-batch-editor-layout">'.
'<tr>'.
'<td>%s%s</td>'.
- '<td>%s</td>'.
'<td id="batch-select-status-cell">%s</td>'.
'<td class="batch-select-submit-cell">%s%s</td>'.
'</tr>'.
'</table>',
$select_all,
$select_none,
- $export,
'',
$submit,
$hidden);
$editor = phabricator_form(
$user,
array(
'method' => 'POST',
'action' => '/maniphest/bulk/',
'id' => 'batch-select-form',
),
$editor);
$box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Batch Task Editor'))
->appendChild($editor);
$content = phutil_tag_div('maniphest-batch-editor', $box);
return $content;
}
}
diff --git a/src/applications/maniphest/xaction/ManiphestTaskMergedIntoTransaction.php b/src/applications/maniphest/xaction/ManiphestTaskMergedIntoTransaction.php
index cd0cad6a3..630f5190c 100644
--- a/src/applications/maniphest/xaction/ManiphestTaskMergedIntoTransaction.php
+++ b/src/applications/maniphest/xaction/ManiphestTaskMergedIntoTransaction.php
@@ -1,47 +1,47 @@
<?php
final class ManiphestTaskMergedIntoTransaction
extends ManiphestTaskTransactionType {
const TRANSACTIONTYPE = 'mergedinto';
public function generateOldValue($object) {
return null;
}
public function applyInternalEffects($object, $value) {
- $object->setStatus(ManiphestTaskStatus::getDuplicateStatus());
+ $this->updateStatus($object, ManiphestTaskStatus::getDuplicateStatus());
}
public function getActionName() {
return pht('Merged');
}
public function getTitle() {
$new = $this->getNewValue();
return pht(
'%s closed this task as a duplicate of %s.',
$this->renderAuthor(),
$this->renderHandle($new));
}
public function getTitleForFeed() {
$new = $this->getNewValue();
return pht(
'%s merged task %s into %s.',
$this->renderAuthor(),
$this->renderObject(),
$this->renderHandle($new));
}
public function getIcon() {
return 'fa-check';
}
public function getColor() {
return 'indigo';
}
}
diff --git a/src/applications/maniphest/xaction/ManiphestTaskStatusTransaction.php b/src/applications/maniphest/xaction/ManiphestTaskStatusTransaction.php
index dd51a6379..6f4b558e0 100644
--- a/src/applications/maniphest/xaction/ManiphestTaskStatusTransaction.php
+++ b/src/applications/maniphest/xaction/ManiphestTaskStatusTransaction.php
@@ -1,240 +1,240 @@
<?php
final class ManiphestTaskStatusTransaction
extends ManiphestTaskTransactionType {
const TRANSACTIONTYPE = 'status';
public function generateOldValue($object) {
return $object->getStatus();
}
public function applyInternalEffects($object, $value) {
- $object->setStatus($value);
+ $this->updateStatus($object, $value);
}
public function shouldHide() {
if ($this->getOldValue() === null) {
return true;
} else {
return false;
}
}
public function getActionStrength() {
return 1.3;
}
public function getActionName() {
$old = $this->getOldValue();
$new = $this->getNewValue();
$action = ManiphestTaskStatus::getStatusActionName($new);
if ($action) {
return $action;
}
$old_closed = ManiphestTaskStatus::isClosedStatus($old);
$new_closed = ManiphestTaskStatus::isClosedStatus($new);
if ($new_closed && !$old_closed) {
return pht('Closed');
} else if (!$new_closed && $old_closed) {
return pht('Reopened');
} else {
return pht('Changed Status');
}
}
public function getTitle() {
$old = $this->getOldValue();
$new = $this->getNewValue();
$old_closed = ManiphestTaskStatus::isClosedStatus($old);
$new_closed = ManiphestTaskStatus::isClosedStatus($new);
$old_name = ManiphestTaskStatus::getTaskStatusName($old);
$new_name = ManiphestTaskStatus::getTaskStatusName($new);
$commit_phid = $this->getMetadataValue('commitPHID');
if ($new_closed && !$old_closed) {
if ($new == ManiphestTaskStatus::getDuplicateStatus()) {
if ($commit_phid) {
return pht(
'%s closed this task as a duplicate by committing %s.',
$this->renderAuthor(),
$this->renderHandle($commit_phid));
} else {
return pht(
'%s closed this task as a duplicate.',
$this->renderAuthor());
}
} else {
if ($commit_phid) {
return pht(
'%s closed this task as %s by committing %s.',
$this->renderAuthor(),
$this->renderValue($new_name),
$this->renderHandle($commit_phid));
} else {
return pht(
'%s closed this task as %s.',
$this->renderAuthor(),
$this->renderValue($new_name));
}
}
} else if (!$new_closed && $old_closed) {
if ($commit_phid) {
return pht(
'%s reopened this task as %s by committing %s.',
$this->renderAuthor(),
$this->renderValue($new_name),
$this->renderHandle($commit_phid));
} else {
return pht(
'%s reopened this task as %s.',
$this->renderAuthor(),
$this->renderValue($new_name));
}
} else {
if ($commit_phid) {
return pht(
'%s changed the task status from %s to %s by committing %s.',
$this->renderAuthor(),
$this->renderValue($old_name),
$this->renderValue($new_name),
$this->renderHandle($commit_phid));
} else {
return pht(
'%s changed the task status from %s to %s.',
$this->renderAuthor(),
$this->renderValue($old_name),
$this->renderValue($new_name));
}
}
}
public function getTitleForFeed() {
$old = $this->getOldValue();
$new = $this->getNewValue();
$old_closed = ManiphestTaskStatus::isClosedStatus($old);
$new_closed = ManiphestTaskStatus::isClosedStatus($new);
$old_name = ManiphestTaskStatus::getTaskStatusName($old);
$new_name = ManiphestTaskStatus::getTaskStatusName($new);
$commit_phid = $this->getMetadataValue('commitPHID');
if ($new_closed && !$old_closed) {
if ($new == ManiphestTaskStatus::getDuplicateStatus()) {
if ($commit_phid) {
return pht(
'%s closed %s as a duplicate by committing %s.',
$this->renderAuthor(),
$this->renderObject(),
$this->renderHandle($commit_phid));
} else {
return pht(
'%s closed %s as a duplicate.',
$this->renderAuthor(),
$this->renderObject());
}
} else {
if ($commit_phid) {
return pht(
'%s closed %s as %s by committing %s.',
$this->renderAuthor(),
$this->renderObject(),
$this->renderValue($new_name),
$this->renderHandle($commit_phid));
} else {
return pht(
'%s closed %s as %s.',
$this->renderAuthor(),
$this->renderObject(),
$this->renderValue($new_name));
}
}
} else if (!$new_closed && $old_closed) {
if ($commit_phid) {
return pht(
'%s reopened %s as %s by committing %s.',
$this->renderAuthor(),
$this->renderObject(),
$this->renderValue($new_name),
$this->renderHandle($commit_phid));
} else {
return pht(
'%s reopened %s as "%s".',
$this->renderAuthor(),
$this->renderObject(),
$new_name);
}
} else {
if ($commit_phid) {
return pht(
'%s changed the status of %s from %s to %s by committing %s.',
$this->renderAuthor(),
$this->renderObject(),
$this->renderValue($old_name),
$this->renderValue($new_name),
$this->renderHandle($commit_phid));
} else {
return pht(
'%s changed the status 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();
$action = ManiphestTaskStatus::getStatusIcon($new);
if ($action !== null) {
return $action;
}
if (ManiphestTaskStatus::isClosedStatus($new)) {
return 'fa-check';
} else {
return 'fa-pencil';
}
}
public function getColor() {
$old = $this->getOldValue();
$new = $this->getNewValue();
$color = ManiphestTaskStatus::getStatusColor($new);
if ($color !== null) {
return $color;
}
if (ManiphestTaskStatus::isOpenStatus($new)) {
return 'green';
} else {
return 'indigo';
}
}
public function getTransactionTypeForConduit($xaction) {
return 'status';
}
public function getFieldValuesForConduit($xaction, $data) {
return array(
'old' => $xaction->getOldValue(),
'new' => $xaction->getNewValue(),
);
}
}
diff --git a/src/applications/maniphest/xaction/ManiphestTaskTransactionType.php b/src/applications/maniphest/xaction/ManiphestTaskTransactionType.php
index c59de163c..836e7765b 100644
--- a/src/applications/maniphest/xaction/ManiphestTaskTransactionType.php
+++ b/src/applications/maniphest/xaction/ManiphestTaskTransactionType.php
@@ -1,6 +1,29 @@
<?php
abstract class ManiphestTaskTransactionType
extends PhabricatorModularTransactionType {
+ protected function updateStatus($object, $new_value) {
+ $old_value = $object->getStatus();
+ $object->setStatus($new_value);
+
+ // If this status change closes or opens the task, update the closed
+ // date and actor PHID.
+ $old_closed = ManiphestTaskStatus::isClosedStatus($old_value);
+ $new_closed = ManiphestTaskStatus::isClosedStatus($new_value);
+
+ $is_close = ($new_closed && !$old_closed);
+ $is_open = (!$new_closed && $old_closed);
+
+ if ($is_close) {
+ $object
+ ->setClosedEpoch(PhabricatorTime::getNow())
+ ->setCloserPHID($this->getActingAsPHID());
+ } else if ($is_open) {
+ $object
+ ->setClosedEpoch(null)
+ ->setCloserPHID(null);
+ }
+ }
+
}
diff --git a/src/applications/maniphest/xaction/ManiphestTaskUnblockTransaction.php b/src/applications/maniphest/xaction/ManiphestTaskUnblockTransaction.php
index 1c7a88dec..8833e62b7 100644
--- a/src/applications/maniphest/xaction/ManiphestTaskUnblockTransaction.php
+++ b/src/applications/maniphest/xaction/ManiphestTaskUnblockTransaction.php
@@ -1,116 +1,126 @@
<?php
final class ManiphestTaskUnblockTransaction
extends ManiphestTaskTransactionType {
const TRANSACTIONTYPE = 'unblock';
public function generateOldValue($object) {
return null;
}
public function getActionName() {
$old = $this->getOldValue();
$new = $this->getNewValue();
$old_status = head($old);
$new_status = head($new);
$old_closed = ManiphestTaskStatus::isClosedStatus($old_status);
$new_closed = ManiphestTaskStatus::isClosedStatus($new_status);
if ($old_closed && !$new_closed) {
return pht('Block');
} else if (!$old_closed && $new_closed) {
return pht('Unblock');
} else {
return pht('Blocker');
}
}
public function getTitle() {
$old = $this->getOldValue();
$new = $this->getNewValue();
$blocker_phid = key($new);
$old_status = head($old);
$new_status = head($new);
$old_closed = ManiphestTaskStatus::isClosedStatus($old_status);
$new_closed = ManiphestTaskStatus::isClosedStatus($new_status);
$old_name = ManiphestTaskStatus::getTaskStatusName($old_status);
$new_name = ManiphestTaskStatus::getTaskStatusName($new_status);
if ($this->getMetadataValue('blocker.new')) {
return pht(
'%s created subtask %s.',
$this->renderAuthor(),
$this->renderHandle($blocker_phid));
} else if ($old_closed && !$new_closed) {
return pht(
'%s reopened subtask %s as %s.',
$this->renderAuthor(),
$this->renderHandle($blocker_phid),
$this->renderValue($new_name));
} else if (!$old_closed && $new_closed) {
return pht(
'%s closed subtask %s as %s.',
$this->renderAuthor(),
$this->renderHandle($blocker_phid),
$this->renderValue($new_name));
} else {
return pht(
'%s changed the status of subtask %s from %s to %s.',
$this->renderAuthor(),
$this->renderHandle($blocker_phid),
$this->renderValue($old_name),
$this->renderValue($new_name));
}
}
public function getTitleForFeed() {
$old = $this->getOldValue();
$new = $this->getNewValue();
$blocker_phid = key($new);
$old_status = head($old);
$new_status = head($new);
$old_closed = ManiphestTaskStatus::isClosedStatus($old_status);
$new_closed = ManiphestTaskStatus::isClosedStatus($new_status);
$old_name = ManiphestTaskStatus::getTaskStatusName($old_status);
$new_name = ManiphestTaskStatus::getTaskStatusName($new_status);
if ($old_closed && !$new_closed) {
return pht(
'%s reopened %s, a subtask of %s, as %s.',
$this->renderAuthor(),
$this->renderHandle($blocker_phid),
$this->renderObject(),
$this->renderValue($new_name));
} else if (!$old_closed && $new_closed) {
return pht(
'%s closed %s, a subtask of %s, as %s.',
$this->renderAuthor(),
$this->renderHandle($blocker_phid),
$this->renderObject(),
$this->renderValue($new_name));
} else {
return pht(
'%s changed the status of %s, a subtask of %s, '.
'from %s to %s.',
$this->renderAuthor(),
$this->renderHandle($blocker_phid),
$this->renderObject(),
$this->renderValue($old_name),
$this->renderValue($new_name));
}
}
public function getIcon() {
return 'fa-shield';
}
+ public function shouldHideForFeed() {
+ // Hide "alice created X, a task blocking Y." from feed because it
+ // will almost always appear adjacent to "alice created Y".
+ $is_new = $this->getMetadataValue('blocker.new');
+ if ($is_new) {
+ return true;
+ }
+
+ return parent::shouldHideForFeed();
+ }
}
diff --git a/src/applications/meta/engineextension/PhabricatorQuickSearchApplicationEngineExtension.php b/src/applications/meta/engineextension/PhabricatorDatasourceApplicationEngineExtension.php
similarity index 54%
rename from src/applications/meta/engineextension/PhabricatorQuickSearchApplicationEngineExtension.php
rename to src/applications/meta/engineextension/PhabricatorDatasourceApplicationEngineExtension.php
index 8161c863b..f5b008244 100644
--- a/src/applications/meta/engineextension/PhabricatorQuickSearchApplicationEngineExtension.php
+++ b/src/applications/meta/engineextension/PhabricatorDatasourceApplicationEngineExtension.php
@@ -1,11 +1,11 @@
<?php
-final class PhabricatorQuickSearchApplicationEngineExtension
- extends PhabricatorQuickSearchEngineExtension {
+final class PhabricatorDatasourceApplicationEngineExtension
+ extends PhabricatorDatasourceEngineExtension {
public function newQuickSearchDatasources() {
return array(
new PhabricatorApplicationDatasource(),
);
}
}
diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationAdapter.php
index 336330190..dfbe89165 100644
--- a/src/applications/metamta/adapter/PhabricatorMailImplementationAdapter.php
+++ b/src/applications/metamta/adapter/PhabricatorMailImplementationAdapter.php
@@ -1,35 +1,107 @@
<?php
abstract class PhabricatorMailImplementationAdapter extends Phobject {
+ private $key;
+ private $priority;
+ private $options = array();
+
+ 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 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/PhabricatorMailImplementationAmazonSESAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationAmazonSESAdapter.php
index 5b03cd86a..a26b074df 100644
--- a/src/applications/metamta/adapter/PhabricatorMailImplementationAmazonSESAdapter.php
+++ b/src/applications/metamta/adapter/PhabricatorMailImplementationAmazonSESAdapter.php
@@ -1,37 +1,68 @@
<?php
final class PhabricatorMailImplementationAmazonSESAdapter
extends PhabricatorMailImplementationPHPMailerLiteAdapter {
+ const ADAPTERTYPE = 'ses';
+
private $message;
private $isHTML;
- public function __construct() {
- parent::__construct();
+ public function prepareForSend() {
+ parent::prepareForSend();
$this->mailer->Mailer = 'amazon-ses';
$this->mailer->customMailer = $this;
}
public function supportsMessageIDHeader() {
// Amazon SES will ignore any Message-ID we provide.
return false;
}
+ protected function validateOptions(array $options) {
+ PhutilTypeSpec::checkMap(
+ $options,
+ array(
+ 'access-key' => 'string',
+ 'secret-key' => 'string',
+ 'endpoint' => 'string',
+ 'encoding' => 'string',
+ ));
+ }
+
+ public function newDefaultOptions() {
+ return array(
+ 'access-key' => null,
+ 'secret-key' => null,
+ 'endpoint' => null,
+ 'encoding' => 'base64',
+ );
+ }
+
+ public function newLegacyOptions() {
+ return array(
+ 'access-key' => PhabricatorEnv::getEnvConfig('amazon-ses.access-key'),
+ 'secret-key' => PhabricatorEnv::getEnvConfig('amazon-ses.secret-key'),
+ 'endpoint' => PhabricatorEnv::getEnvConfig('amazon-ses.endpoint'),
+ 'encoding' => PhabricatorEnv::getEnvConfig('phpmailer.smtp-encoding'),
+ );
+ }
+
/**
* @phutil-external-symbol class SimpleEmailService
*/
public function executeSend($body) {
- $key = PhabricatorEnv::getEnvConfig('amazon-ses.access-key');
- $secret = PhabricatorEnv::getEnvConfig('amazon-ses.secret-key');
- $endpoint = PhabricatorEnv::getEnvConfig('amazon-ses.endpoint');
+ $key = $this->getOption('access-key');
+ $secret = $this->getOption('secret-key');
+ $endpoint = $this->getOption('endpoint');
$root = phutil_get_library_root('phabricator');
$root = dirname($root);
require_once $root.'/externals/amazon-ses/ses.php';
$service = new SimpleEmailService($key, $secret, $endpoint);
$service->enableUseExceptions(true);
return $service->sendRawEmail($body);
}
}
diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php
index cfe6491fe..349dae2d2 100644
--- a/src/applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php
+++ b/src/applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php
@@ -1,143 +1,165 @@
<?php
/**
* Mail adapter that uses Mailgun's web API to deliver email.
*/
final class PhabricatorMailImplementationMailgunAdapter
extends PhabricatorMailImplementationAdapter {
+ const ADAPTERTYPE = 'mailgun';
+
private $params = array();
private $attachments = array();
public function setFrom($email, $name = '') {
$this->params['from'] = $email;
$this->params['from-name'] = $name;
return $this;
}
public function addReplyTo($email, $name = '') {
if (empty($this->params['reply-to'])) {
$this->params['reply-to'] = array();
}
- $this->params['reply-to'][] = "{$name} <{$email}>";
+ $this->params['reply-to'][] = $this->renderAddress($email, $name);
return $this;
}
public function addTos(array $emails) {
foreach ($emails as $email) {
$this->params['tos'][] = $email;
}
return $this;
}
public function addCCs(array $emails) {
foreach ($emails as $email) {
$this->params['ccs'][] = $email;
}
return $this;
}
public function addAttachment($data, $filename, $mimetype) {
$this->attachments[] = array(
'data' => $data,
'name' => $filename,
'type' => $mimetype,
);
return $this;
}
public function addHeader($header_name, $header_value) {
$this->params['headers'][] = array($header_name, $header_value);
return $this;
}
public function setBody($body) {
$this->params['body'] = $body;
return $this;
}
public function setHTMLBody($html_body) {
$this->params['html-body'] = $html_body;
return $this;
}
public function setSubject($subject) {
$this->params['subject'] = $subject;
return $this;
}
public function supportsMessageIDHeader() {
return true;
}
+ protected function validateOptions(array $options) {
+ PhutilTypeSpec::checkMap(
+ $options,
+ array(
+ 'api-key' => 'string',
+ 'domain' => 'string',
+ ));
+ }
+
+ public function newDefaultOptions() {
+ return array(
+ 'api-key' => null,
+ 'domain' => null,
+ );
+ }
+
+ public function newLegacyOptions() {
+ return array(
+ 'api-key' => PhabricatorEnv::getEnvConfig('mailgun.api-key'),
+ 'domain' => PhabricatorEnv::getEnvConfig('mailgun.domain'),
+ );
+ }
+
public function send() {
- $key = PhabricatorEnv::getEnvConfig('mailgun.api-key');
- $domain = PhabricatorEnv::getEnvConfig('mailgun.domain');
+ $key = $this->getOption('api-key');
+ $domain = $this->getOption('domain');
$params = array();
$params['to'] = implode(', ', idx($this->params, 'tos', array()));
$params['subject'] = idx($this->params, 'subject');
$params['text'] = idx($this->params, 'body');
if (idx($this->params, 'html-body')) {
$params['html'] = idx($this->params, 'html-body');
}
$from = idx($this->params, 'from');
- if (idx($this->params, 'from-name')) {
- $params['from'] = "\"{$this->params['from-name']}\" <{$from}>";
- } else {
- $params['from'] = $from;
- }
+ $from_name = idx($this->params, 'from-name');
+ $params['from'] = $this->renderAddress($from, $from_name);
if (idx($this->params, 'reply-to')) {
$replyto = $this->params['reply-to'];
$params['h:reply-to'] = implode(', ', $replyto);
}
if (idx($this->params, 'ccs')) {
$params['cc'] = implode(', ', $this->params['ccs']);
}
foreach (idx($this->params, 'headers', array()) as $header) {
list($name, $value) = $header;
$params['h:'.$name] = $value;
}
$future = new HTTPSFuture(
"https://api:{$key}@api.mailgun.net/v2/{$domain}/messages",
$params);
$future->setMethod('POST');
foreach ($this->attachments as $attachment) {
$future->attachFileData(
'attachment',
$attachment['data'],
$attachment['name'],
$attachment['type']);
}
list($body) = $future->resolvex();
$response = null;
try {
$response = phutil_json_decode($body);
} catch (PhutilJSONParserException $ex) {
throw new PhutilProxyException(
pht('Failed to JSON decode response.'),
$ex);
}
if (!idx($response, 'id')) {
$message = $response['message'];
throw new Exception(
pht(
'Request failed with errors: %s.',
$message));
}
return true;
}
}
diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerAdapter.php
index 2fadd6491..ae134a29b 100644
--- a/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerAdapter.php
+++ b/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerAdapter.php
@@ -1,123 +1,162 @@
<?php
final class PhabricatorMailImplementationPHPMailerAdapter
extends PhabricatorMailImplementationAdapter {
+ const ADAPTERTYPE = 'smtp';
+
private $mailer;
+ protected function validateOptions(array $options) {
+ PhutilTypeSpec::checkMap(
+ $options,
+ array(
+ 'host' => 'string|null',
+ 'port' => 'int',
+ 'user' => 'string|null',
+ 'password' => 'string|null',
+ 'protocol' => 'string|null',
+ 'encoding' => 'string',
+ 'mailer' => 'string',
+ ));
+ }
+
+ public function newDefaultOptions() {
+ return array(
+ 'host' => null,
+ 'port' => 25,
+ 'user' => null,
+ 'password' => null,
+ 'protocol' => null,
+ 'encoding' => 'base64',
+ 'mailer' => 'smtp',
+ );
+ }
+
+ public function newLegacyOptions() {
+ return array(
+ 'host' => PhabricatorEnv::getEnvConfig('phpmailer.smtp-host'),
+ 'port' => PhabricatorEnv::getEnvConfig('phpmailer.smtp-port'),
+ 'user' => PhabricatorEnv::getEnvConfig('phpmailer.smtp-user'),
+ 'password' => PhabricatorEnv::getEnvConfig('phpmailer.smtp-password'),
+ 'protocol' => PhabricatorEnv::getEnvConfig('phpmailer.smtp-protocol'),
+ 'encoding' => PhabricatorEnv::getEnvConfig('phpmailer.smtp-encoding'),
+ 'mailer' => PhabricatorEnv::getEnvConfig('phpmailer.mailer'),
+ );
+ }
+
/**
* @phutil-external-symbol class PHPMailer
*/
- public function __construct() {
+ public function prepareForSend() {
$root = phutil_get_library_root('phabricator');
$root = dirname($root);
require_once $root.'/externals/phpmailer/class.phpmailer.php';
$this->mailer = new PHPMailer($use_exceptions = true);
$this->mailer->CharSet = 'utf-8';
- $encoding = PhabricatorEnv::getEnvConfig('phpmailer.smtp-encoding');
+ $encoding = $this->getOption('encoding');
$this->mailer->Encoding = $encoding;
// By default, PHPMailer sends one mail per recipient. We handle
- // multiplexing higher in the stack, so tell it to send mail exactly
- // like we ask.
+ // combining or separating To and Cc higher in the stack, so tell it to
+ // send mail exactly like we ask.
$this->mailer->SingleTo = false;
- $mailer = PhabricatorEnv::getEnvConfig('phpmailer.mailer');
+ $mailer = $this->getOption('mailer');
if ($mailer == 'smtp') {
$this->mailer->IsSMTP();
- $this->mailer->Host = PhabricatorEnv::getEnvConfig('phpmailer.smtp-host');
- $this->mailer->Port = PhabricatorEnv::getEnvConfig('phpmailer.smtp-port');
- $user = PhabricatorEnv::getEnvConfig('phpmailer.smtp-user');
+ $this->mailer->Host = $this->getOption('host');
+ $this->mailer->Port = $this->getOption('port');
+ $user = $this->getOption('user');
if ($user) {
$this->mailer->SMTPAuth = true;
$this->mailer->Username = $user;
- $this->mailer->Password =
- PhabricatorEnv::getEnvConfig('phpmailer.smtp-password');
+ $this->mailer->Password = $this->getOption('password');
}
- $protocol = PhabricatorEnv::getEnvConfig('phpmailer.smtp-protocol');
+ $protocol = $this->getOption('protocol');
if ($protocol) {
$protocol = phutil_utf8_strtolower($protocol);
$this->mailer->SMTPSecure = $protocol;
}
} else if ($mailer == 'sendmail') {
$this->mailer->IsSendmail();
} else {
// Do nothing, by default PHPMailer send message using PHP mail()
// function.
}
}
public function supportsMessageIDHeader() {
return true;
}
public function setFrom($email, $name = '') {
$this->mailer->SetFrom($email, $name, $crazy_side_effects = false);
return $this;
}
public function addReplyTo($email, $name = '') {
$this->mailer->AddReplyTo($email, $name);
return $this;
}
public function addTos(array $emails) {
foreach ($emails as $email) {
$this->mailer->AddAddress($email);
}
return $this;
}
public function addCCs(array $emails) {
foreach ($emails as $email) {
$this->mailer->AddCC($email);
}
return $this;
}
public function addAttachment($data, $filename, $mimetype) {
$this->mailer->AddStringAttachment(
$data,
$filename,
'base64',
$mimetype);
return $this;
}
public function addHeader($header_name, $header_value) {
if (strtolower($header_name) == 'message-id') {
$this->mailer->MessageID = $header_value;
} else {
$this->mailer->AddCustomHeader($header_name.': '.$header_value);
}
return $this;
}
public function setBody($body) {
$this->mailer->IsHTML(false);
$this->mailer->Body = $body;
return $this;
}
public function setHTMLBody($html_body) {
$this->mailer->IsHTML(true);
$this->mailer->Body = $html_body;
return $this;
}
public function setSubject($subject) {
$this->mailer->Subject = $subject;
return $this;
}
public function hasValidRecipients() {
return true;
}
public function send() {
return $this->mailer->Send();
}
}
diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php
index f072e769c..1f21a993c 100644
--- a/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php
+++ b/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php
@@ -1,107 +1,129 @@
<?php
/**
* TODO: Should be final, but inherited by SES.
*/
class PhabricatorMailImplementationPHPMailerLiteAdapter
extends PhabricatorMailImplementationAdapter {
+ const ADAPTERTYPE = 'sendmail';
+
protected $mailer;
+ protected function validateOptions(array $options) {
+ PhutilTypeSpec::checkMap(
+ $options,
+ array(
+ 'encoding' => 'string',
+ ));
+ }
+
+ public function newDefaultOptions() {
+ return array(
+ 'encoding' => 'base64',
+ );
+ }
+
+ public function newLegacyOptions() {
+ return array(
+ 'encoding' => PhabricatorEnv::getEnvConfig('phpmailer.smtp-encoding'),
+ );
+ }
+
/**
* @phutil-external-symbol class PHPMailerLite
*/
- public function __construct() {
+ public function prepareForSend() {
$root = phutil_get_library_root('phabricator');
$root = dirname($root);
require_once $root.'/externals/phpmailer/class.phpmailer-lite.php';
$this->mailer = new PHPMailerLite($use_exceptions = true);
$this->mailer->CharSet = 'utf-8';
- $encoding = PhabricatorEnv::getEnvConfig('phpmailer.smtp-encoding');
+ $encoding = $this->getOption('encoding');
$this->mailer->Encoding = $encoding;
// By default, PHPMailerLite sends one mail per recipient. We handle
- // multiplexing higher in the stack, so tell it to send mail exactly
- // like we ask.
+ // combining or separating To and Cc higher in the stack, so tell it to
+ // send mail exactly like we ask.
$this->mailer->SingleTo = false;
}
public function supportsMessageIDHeader() {
return true;
}
public function setFrom($email, $name = '') {
$this->mailer->SetFrom($email, $name, $crazy_side_effects = false);
return $this;
}
public function addReplyTo($email, $name = '') {
$this->mailer->AddReplyTo($email, $name);
return $this;
}
public function addTos(array $emails) {
foreach ($emails as $email) {
$this->mailer->AddAddress($email);
}
return $this;
}
public function addCCs(array $emails) {
foreach ($emails as $email) {
$this->mailer->AddCC($email);
}
return $this;
}
public function addAttachment($data, $filename, $mimetype) {
$this->mailer->AddStringAttachment(
$data,
$filename,
'base64',
$mimetype);
return $this;
}
public function addHeader($header_name, $header_value) {
if (strtolower($header_name) == 'message-id') {
$this->mailer->MessageID = $header_value;
} else {
$this->mailer->AddCustomHeader($header_name.': '.$header_value);
}
return $this;
}
public function setBody($body) {
$this->mailer->Body = $body;
$this->mailer->IsHTML(false);
return $this;
}
/**
* Note: phpmailer-lite does NOT support sending messages with mixed version
* (plaintext and html). So for now lets just use HTML if it's available.
* @param $html
*/
public function setHTMLBody($html_body) {
$this->mailer->Body = $html_body;
$this->mailer->IsHTML(true);
return $this;
}
public function setSubject($subject) {
$this->mailer->Subject = $subject;
return $this;
}
public function hasValidRecipients() {
return true;
}
public function send() {
return $this->mailer->Send();
}
}
diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationPostmarkAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationPostmarkAdapter.php
new file mode 100644
index 000000000..5792ba08f
--- /dev/null
+++ b/src/applications/metamta/adapter/PhabricatorMailImplementationPostmarkAdapter.php
@@ -0,0 +1,124 @@
+<?php
+
+final class PhabricatorMailImplementationPostmarkAdapter
+ extends PhabricatorMailImplementationAdapter {
+
+ const ADAPTERTYPE = 'postmark';
+
+ private $parameters = array();
+
+ public function setFrom($email, $name = '') {
+ $this->parameters['From'] = $this->renderAddress($email, $name);
+ return $this;
+ }
+
+ public function addReplyTo($email, $name = '') {
+ $this->parameters['ReplyTo'] = $this->renderAddress($email, $name);
+ return $this;
+ }
+
+ public function addTos(array $emails) {
+ foreach ($emails as $email) {
+ $this->parameters['To'][] = $email;
+ }
+ return $this;
+ }
+
+ public function addCCs(array $emails) {
+ foreach ($emails as $email) {
+ $this->parameters['Cc'][] = $email;
+ }
+ return $this;
+ }
+
+ public function addAttachment($data, $filename, $mimetype) {
+ $this->parameters['Attachments'][] = array(
+ 'Name' => $filename,
+ 'ContentType' => $mimetype,
+ 'Content' => base64_encode($data),
+ );
+
+ return $this;
+ }
+
+ public function addHeader($header_name, $header_value) {
+ $this->parameters['Headers'][] = array(
+ 'Name' => $header_name,
+ 'Value' => $header_value,
+ );
+ return $this;
+ }
+
+ public function setBody($body) {
+ $this->parameters['TextBody'] = $body;
+ return $this;
+ }
+
+ public function setHTMLBody($html_body) {
+ $this->parameters['HtmlBody'] = $html_body;
+ return $this;
+ }
+
+ public function setSubject($subject) {
+ $this->parameters['Subject'] = $subject;
+ return $this;
+ }
+
+ public function supportsMessageIDHeader() {
+ return true;
+ }
+
+ protected function validateOptions(array $options) {
+ PhutilTypeSpec::checkMap(
+ $options,
+ array(
+ 'access-token' => 'string',
+ 'inbound-addresses' => 'list<string>',
+ ));
+
+ // Make sure this is properly formatted.
+ PhutilCIDRList::newList($options['inbound-addresses']);
+ }
+
+ public function newDefaultOptions() {
+ return array(
+ 'access-token' => null,
+ 'inbound-addresses' => array(
+ // Via Postmark support circa February 2018, see:
+ //
+ // https://postmarkapp.com/support/article/800-ips-for-firewalls
+ //
+ // "Configuring Outbound Email" should be updated if this changes.
+ '50.31.156.6/32',
+ ),
+ );
+ }
+
+ public function newLegacyOptions() {
+ return array();
+ }
+
+ public function send() {
+ $access_token = $this->getOption('access-token');
+
+ $parameters = $this->parameters;
+ $flatten = array(
+ 'To',
+ 'Cc',
+ );
+
+ foreach ($flatten as $key) {
+ if (isset($parameters[$key])) {
+ $parameters[$key] = implode(', ', $parameters[$key]);
+ }
+ }
+
+ id(new PhutilPostmarkFuture())
+ ->setAccessToken($access_token)
+ ->setMethod('email', $parameters)
+ ->resolve();
+
+ return true;
+ }
+
+}
diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php
index 566d33fd1..be2a83705 100644
--- a/src/applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php
+++ b/src/applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php
@@ -1,164 +1,189 @@
<?php
/**
* Mail adapter that uses SendGrid's web API to deliver email.
*/
final class PhabricatorMailImplementationSendGridAdapter
extends PhabricatorMailImplementationAdapter {
+ const ADAPTERTYPE = 'sendgrid';
+
private $params = array();
+ protected function validateOptions(array $options) {
+ PhutilTypeSpec::checkMap(
+ $options,
+ array(
+ 'api-user' => 'string',
+ 'api-key' => 'string',
+ ));
+ }
+
+ public function newDefaultOptions() {
+ return array(
+ 'api-user' => null,
+ 'api-key' => null,
+ );
+ }
+
+ public function newLegacyOptions() {
+ return array(
+ 'api-user' => PhabricatorEnv::getEnvConfig('sendgrid.api-user'),
+ 'api-key' => PhabricatorEnv::getEnvConfig('sendgrid.api-key'),
+ );
+ }
+
public function setFrom($email, $name = '') {
$this->params['from'] = $email;
$this->params['from-name'] = $name;
return $this;
}
public function addReplyTo($email, $name = '') {
if (empty($this->params['reply-to'])) {
$this->params['reply-to'] = array();
}
$this->params['reply-to'][] = array(
'email' => $email,
'name' => $name,
);
return $this;
}
public function addTos(array $emails) {
foreach ($emails as $email) {
$this->params['tos'][] = $email;
}
return $this;
}
public function addCCs(array $emails) {
foreach ($emails as $email) {
$this->params['ccs'][] = $email;
}
return $this;
}
public function addAttachment($data, $filename, $mimetype) {
if (empty($this->params['files'])) {
$this->params['files'] = array();
}
$this->params['files'][$filename] = $data;
}
public function addHeader($header_name, $header_value) {
$this->params['headers'][] = array($header_name, $header_value);
return $this;
}
public function setBody($body) {
$this->params['body'] = $body;
return $this;
}
public function setHTMLBody($body) {
$this->params['html-body'] = $body;
return $this;
}
public function setSubject($subject) {
$this->params['subject'] = $subject;
return $this;
}
public function supportsMessageIDHeader() {
return false;
}
public function send() {
- $user = PhabricatorEnv::getEnvConfig('sendgrid.api-user');
- $key = PhabricatorEnv::getEnvConfig('sendgrid.api-key');
+ $user = $this->getOption('api-user');
+ $key = $this->getOption('api-key');
if (!$user || !$key) {
throw new Exception(
pht(
"Configure '%s' and '%s' to use SendGrid for mail delivery.",
'sendgrid.api-user',
'sendgrid.api-key'));
}
$params = array();
$ii = 0;
foreach (idx($this->params, 'tos', array()) as $to) {
$params['to['.($ii++).']'] = $to;
}
$params['subject'] = idx($this->params, 'subject');
$params['text'] = idx($this->params, 'body');
if (idx($this->params, 'html-body')) {
$params['html'] = idx($this->params, 'html-body');
}
$params['from'] = idx($this->params, 'from');
if (idx($this->params, 'from-name')) {
$params['fromname'] = $this->params['from-name'];
}
if (idx($this->params, 'reply-to')) {
$replyto = $this->params['reply-to'];
// Pick off the email part, no support for the name part in this API.
$params['replyto'] = $replyto[0]['email'];
}
foreach (idx($this->params, 'files', array()) as $name => $data) {
$params['files['.$name.']'] = $data;
}
$headers = idx($this->params, 'headers', array());
// See SendGrid Support Ticket #29390; there's no explicit REST API support
// for CC right now but it works if you add a generic "Cc" header.
//
// SendGrid said this is supported:
// "You can use CC as you are trying to do there [by adding a generic
// header]. It is supported despite our limited documentation to this
// effect, I am glad you were able to figure it out regardless. ..."
if (idx($this->params, 'ccs')) {
$headers[] = array('Cc', implode(', ', $this->params['ccs']));
}
if ($headers) {
// Convert to dictionary.
$headers = ipull($headers, 1, 0);
$headers = json_encode($headers);
$params['headers'] = $headers;
}
$params['api_user'] = $user;
$params['api_key'] = $key;
$future = new HTTPSFuture(
'https://sendgrid.com/api/mail.send.json',
$params);
$future->setMethod('POST');
list($body) = $future->resolvex();
$response = null;
try {
$response = phutil_json_decode($body);
} catch (PhutilJSONParserException $ex) {
throw new PhutilProxyException(
pht('Failed to JSON decode response.'),
$ex);
}
if ($response['message'] !== 'success') {
$errors = implode(';', $response['errors']);
throw new Exception(pht('Request failed with errors: %s.', $errors));
}
return true;
}
}
diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php
index 0ea2af916..8a8d0de0c 100644
--- a/src/applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php
+++ b/src/applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php
@@ -1,110 +1,126 @@
<?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;
+ private $config = array();
+
+ protected function validateOptions(array $options) {
+ PhutilTypeSpec::checkMap(
+ $options,
+ array());
+ }
+
+ public function newDefaultOptions() {
+ return array();
+ }
+
+ public function newLegacyOptions() {
+ return array();
+ }
- public function __construct(array $config = 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;
}
}
diff --git a/src/applications/metamta/application/PhabricatorMetaMTAApplication.php b/src/applications/metamta/application/PhabricatorMetaMTAApplication.php
index adb08aaa2..f53af5503 100644
--- a/src/applications/metamta/application/PhabricatorMetaMTAApplication.php
+++ b/src/applications/metamta/application/PhabricatorMetaMTAApplication.php
@@ -1,53 +1,54 @@
<?php
final class PhabricatorMetaMTAApplication extends PhabricatorApplication {
public function getName() {
return pht('Mail');
}
public function getBaseURI() {
return '/mail/';
}
public function getIcon() {
return 'fa-send';
}
public function getShortDescription() {
return pht('Send and Receive Mail');
}
public function getFlavorText() {
return pht('Every program attempts to expand until it can read mail.');
}
public function getApplicationGroup() {
return self::GROUP_ADMIN;
}
public function canUninstall() {
return false;
}
public function getTypeaheadURI() {
return '/mail/';
}
public function getRoutes() {
return array(
'/mail/' => array(
'(query/(?P<queryKey>[^/]+)/)?' =>
'PhabricatorMetaMTAMailListController',
'detail/(?P<id>[1-9]\d*)/' => 'PhabricatorMetaMTAMailViewController',
'sendgrid/' => 'PhabricatorMetaMTASendGridReceiveController',
'mailgun/' => 'PhabricatorMetaMTAMailgunReceiveController',
+ 'postmark/' => 'PhabricatorMetaMTAPostmarkReceiveController',
),
);
}
public function getTitleGlyph() {
return '@';
}
}
diff --git a/src/applications/metamta/controller/PhabricatorMetaMTAMailViewController.php b/src/applications/metamta/controller/PhabricatorMetaMTAMailViewController.php
index 80da535a9..9b3339783 100644
--- a/src/applications/metamta/controller/PhabricatorMetaMTAMailViewController.php
+++ b/src/applications/metamta/controller/PhabricatorMetaMTAMailViewController.php
@@ -1,400 +1,450 @@
<?php
final class PhabricatorMetaMTAMailViewController
extends PhabricatorMetaMTAController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$mail = id(new PhabricatorMetaMTAMailQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->executeOne();
if (!$mail) {
return new Aphront404Response();
}
if ($mail->hasSensitiveContent()) {
$title = pht('Content Redacted');
} else {
$title = $mail->getSubject();
}
$header = id(new PHUIHeaderView())
->setHeader($title)
->setUser($viewer)
->setPolicyObject($mail)
->setHeaderIcon('fa-envelope');
$status = $mail->getStatus();
$name = PhabricatorMailOutboundStatus::getStatusName($status);
$icon = PhabricatorMailOutboundStatus::getStatusIcon($status);
$color = PhabricatorMailOutboundStatus::getStatusColor($status);
$header->setStatus($icon, $color, $name);
+ if ($mail->getMustEncrypt()) {
+ Javelin::initBehavior('phabricator-tooltips');
+ $header->addTag(
+ id(new PHUITagView())
+ ->setType(PHUITagView::TYPE_SHADE)
+ ->setColor('blue')
+ ->setName(pht('Must Encrypt'))
+ ->setIcon('fa-shield blue')
+ ->addSigil('has-tooltip')
+ ->setMetadata(
+ array(
+ 'tip' => pht(
+ 'Message content can only be transmitted over secure '.
+ 'channels.'),
+ )));
+ }
+
$crumbs = $this->buildApplicationCrumbs()
->addTextCrumb(pht('Mail %d', $mail->getID()))
->setBorder(true);
$tab_group = id(new PHUITabGroupView())
->addTab(
id(new PHUITabView())
->setName(pht('Message'))
->setKey('message')
->appendChild($this->buildMessageProperties($mail)))
->addTab(
id(new PHUITabView())
->setName(pht('Headers'))
->setKey('headers')
->appendChild($this->buildHeaderProperties($mail)))
->addTab(
id(new PHUITabView())
->setName(pht('Delivery'))
->setKey('delivery')
->appendChild($this->buildDeliveryProperties($mail)))
->addTab(
id(new PHUITabView())
->setName(pht('Metadata'))
->setKey('metadata')
->appendChild($this->buildMetadataProperties($mail)));
+ $header_view = id(new PHUIHeaderView())
+ ->setHeader(pht('Mail'));
+
+ $object_phid = $mail->getRelatedPHID();
+ if ($object_phid) {
+ $handles = $viewer->loadHandles(array($object_phid));
+ $handle = $handles[$object_phid];
+ if ($handle->isComplete() && $handle->getURI()) {
+ $view_button = id(new PHUIButtonView())
+ ->setTag('a')
+ ->setText(pht('View Object'))
+ ->setIcon('fa-chevron-right')
+ ->setHref($handle->getURI());
+
+ $header_view->addActionLink($view_button);
+ }
+ }
+
$object_box = id(new PHUIObjectBoxView())
- ->setHeaderText(pht('Mail'))
+ ->setHeader($header_view)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->addTabGroup($tab_group);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter($object_box);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->setPageObjectPHIDs(array($mail->getPHID()))
->appendChild($view);
}
private function buildMessageProperties(PhabricatorMetaMTAMail $mail) {
$viewer = $this->getViewer();
$properties = id(new PHUIPropertyListView())
->setUser($viewer)
->setObject($mail);
if ($mail->getFrom()) {
$from_str = $viewer->renderHandle($mail->getFrom());
} else {
$from_str = pht('Sent by Phabricator');
}
$properties->addProperty(
pht('From'),
$from_str);
if ($mail->getToPHIDs()) {
$to_list = $viewer->renderHandleList($mail->getToPHIDs());
} else {
$to_list = pht('None');
}
$properties->addProperty(
pht('To'),
$to_list);
if ($mail->getCcPHIDs()) {
$cc_list = $viewer->renderHandleList($mail->getCcPHIDs());
} else {
$cc_list = pht('None');
}
$properties->addProperty(
pht('Cc'),
$cc_list);
$properties->addProperty(
pht('Sent'),
phabricator_datetime($mail->getDateCreated(), $viewer));
$properties->addSectionHeader(
pht('Message'),
PHUIPropertyListView::ICON_SUMMARY);
if ($mail->hasSensitiveContent()) {
$body = phutil_tag(
'em',
array(),
pht(
'The content of this mail is sensitive and it can not be '.
'viewed from the web UI.'));
} else {
$body = phutil_tag(
'div',
array(
'style' => 'white-space: pre-wrap',
),
$mail->getBody());
}
$properties->addTextContent($body);
+ $file_phids = $mail->getAttachmentFilePHIDs();
+ if ($file_phids) {
+ $properties->addProperty(
+ pht('Attached Files'),
+ $viewer->loadHandles($file_phids)->renderList());
+ }
return $properties;
}
private function buildHeaderProperties(PhabricatorMetaMTAMail $mail) {
$viewer = $this->getViewer();
$properties = id(new PHUIPropertyListView())
->setUser($viewer)
->setStacked(true);
$headers = $mail->getDeliveredHeaders();
if ($headers === null) {
$headers = $mail->generateHeaders();
}
// Sort headers by name.
$headers = isort($headers, 0);
foreach ($headers as $header) {
list($key, $value) = $header;
$properties->addProperty($key, $value);
}
+ $encrypt_phids = $mail->getMustEncryptReasons();
+ if ($encrypt_phids) {
+ $properties->addProperty(
+ pht('Must Encrypt'),
+ $viewer->loadHandles($encrypt_phids)
+ ->renderList());
+ }
+
+
return $properties;
}
private function buildDeliveryProperties(PhabricatorMetaMTAMail $mail) {
$viewer = $this->getViewer();
$properties = id(new PHUIPropertyListView())
->setUser($viewer);
$actors = $mail->getDeliveredActors();
$reasons = null;
if (!$actors) {
if ($mail->getStatus() == PhabricatorMailOutboundStatus::STATUS_QUEUE) {
$delivery = $this->renderEmptyMessage(
pht(
'This message has not been delivered yet, so delivery information '.
'is not available.'));
} else {
$delivery = $this->renderEmptyMessage(
pht(
'This is an older message that predates recording delivery '.
'information, so none is available.'));
}
} else {
$actor = idx($actors, $viewer->getPHID());
if (!$actor) {
$delivery = phutil_tag(
'em',
array(),
pht('This message was not delivered to you.'));
} else {
$deliverable = $actor['deliverable'];
if ($deliverable) {
$delivery = pht('Delivered');
} else {
$delivery = pht('Voided');
}
$reasons = id(new PHUIStatusListView());
$reason_codes = $actor['reasons'];
if (!$reason_codes) {
$reason_codes = array(
PhabricatorMetaMTAActor::REASON_NONE,
);
}
$icon_yes = 'fa-check green';
$icon_no = 'fa-times red';
foreach ($reason_codes as $reason) {
$target = phutil_tag(
'strong',
array(),
PhabricatorMetaMTAActor::getReasonName($reason));
if (PhabricatorMetaMTAActor::isDeliveryReason($reason)) {
$icon = $icon_yes;
} else {
$icon = $icon_no;
}
$item = id(new PHUIStatusItemView())
->setIcon($icon)
->setTarget($target)
->setNote(PhabricatorMetaMTAActor::getReasonDescription($reason));
$reasons->addItem($item);
}
}
}
$properties->addProperty(pht('Delivery'), $delivery);
if ($reasons) {
$properties->addProperty(pht('Reasons'), $reasons);
$properties->addProperty(
null,
$this->renderEmptyMessage(
pht(
'Delivery reasons are listed from weakest to strongest.')));
}
$properties->addSectionHeader(
pht('Routing Rules'), 'fa-paper-plane-o');
$map = $mail->getDeliveredRoutingMap();
$routing_detail = null;
if ($map === null) {
if ($mail->getStatus() == PhabricatorMailOutboundStatus::STATUS_QUEUE) {
$routing_result = $this->renderEmptyMessage(
pht(
'This message has not been sent yet, so routing rules have '.
'not been computed.'));
} else {
$routing_result = $this->renderEmptyMessage(
pht(
'This is an older message which predates routing rules.'));
}
} else {
$rule = idx($map, $viewer->getPHID());
if ($rule === null) {
$rule = idx($map, 'default');
}
if ($rule === null) {
$routing_result = $this->renderEmptyMessage(
pht(
'No routing rules applied when delivering this message to you.'));
} else {
$rule_const = $rule['rule'];
$reason_phid = $rule['reason'];
switch ($rule_const) {
case PhabricatorMailRoutingRule::ROUTE_AS_NOTIFICATION:
$routing_result = pht(
'This message was routed as a notification because it '.
'matched %s.',
$viewer->renderHandle($reason_phid)->render());
break;
case PhabricatorMailRoutingRule::ROUTE_AS_MAIL:
$routing_result = pht(
'This message was routed as an email because it matched %s.',
$viewer->renderHandle($reason_phid)->render());
break;
default:
$routing_result = pht('Unknown routing rule "%s".', $rule_const);
break;
}
}
$routing_rules = $mail->getDeliveredRoutingRules();
if ($routing_rules) {
$rules = array();
foreach ($routing_rules as $rule) {
$phids = idx($rule, 'phids');
if ($phids === null) {
$rules[] = $rule;
} else if (in_array($viewer->getPHID(), $phids)) {
$rules[] = $rule;
}
}
// Reorder rules by strength.
foreach ($rules as $key => $rule) {
$const = $rule['routingRule'];
$phids = $rule['phids'];
if ($phids === null) {
$type = 'A';
} else {
$type = 'B';
}
$rules[$key]['strength'] = sprintf(
'~%s%08d',
$type,
PhabricatorMailRoutingRule::getRuleStrength($const));
}
$rules = isort($rules, 'strength');
$routing_detail = id(new PHUIStatusListView());
foreach ($rules as $rule) {
$const = $rule['routingRule'];
$phids = $rule['phids'];
$name = PhabricatorMailRoutingRule::getRuleName($const);
$icon = PhabricatorMailRoutingRule::getRuleIcon($const);
$color = PhabricatorMailRoutingRule::getRuleColor($const);
if ($phids === null) {
$kind = pht('Global');
} else {
$kind = pht('Personal');
}
$target = array($kind, ': ', $name);
$target = phutil_tag('strong', array(), $target);
$item = id(new PHUIStatusItemView())
->setTarget($target)
->setNote($viewer->renderHandle($rule['reasonPHID']))
->setIcon($icon, $color);
$routing_detail->addItem($item);
}
}
}
$properties->addProperty(pht('Effective Rule'), $routing_result);
if ($routing_detail !== null) {
$properties->addProperty(pht('All Matching Rules'), $routing_detail);
$properties->addProperty(
null,
$this->renderEmptyMessage(
pht(
'Matching rules are listed from weakest to strongest.')));
}
return $properties;
}
private function buildMetadataProperties(PhabricatorMetaMTAMail $mail) {
$viewer = $this->getViewer();
$properties = id(new PHUIPropertyListView())
->setUser($viewer);
$properties->addProperty(pht('Message PHID'), $mail->getPHID());
$details = $mail->getMessage();
if (!strlen($details)) {
$details = phutil_tag('em', array(), pht('None'));
}
$properties->addProperty(pht('Status Details'), $details);
$actor_phid = $mail->getActorPHID();
if ($actor_phid) {
$actor_str = $viewer->renderHandle($actor_phid);
} else {
$actor_str = pht('Generated by Phabricator');
}
$properties->addProperty(pht('Actor'), $actor_str);
$related_phid = $mail->getRelatedPHID();
if ($related_phid) {
$related = $viewer->renderHandle($mail->getRelatedPHID());
} else {
$related = phutil_tag('em', array(), pht('None'));
}
$properties->addProperty(pht('Related Object'), $related);
return $properties;
}
private function renderEmptyMessage($message) {
return phutil_tag('em', array(), $message);
}
}
diff --git a/src/applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php b/src/applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php
index 467995a18..3ca2711dc 100644
--- a/src/applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php
+++ b/src/applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php
@@ -1,93 +1,107 @@
<?php
final class PhabricatorMetaMTAMailgunReceiveController
extends PhabricatorMetaMTAController {
public function shouldRequireLogin() {
return false;
}
private function verifyMessage() {
- $api_key = PhabricatorEnv::getEnvConfig('mailgun.api-key');
$request = $this->getRequest();
$timestamp = $request->getStr('timestamp');
$token = $request->getStr('token');
$sig = $request->getStr('signature');
- $hash = hash_hmac('sha256', $timestamp.$token, $api_key);
- return phutil_hashes_are_identical($sig, $hash);
+ // 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(
+ 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
new file mode 100644
index 000000000..345cd93fe
--- /dev/null
+++ b/src/applications/metamta/controller/PhabricatorMetaMTAPostmarkReceiveController.php
@@ -0,0 +1,102 @@
+<?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(
+ 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 0a5e28fce..6651f85d6 100644
--- a/src/applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php
+++ b/src/applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php
@@ -1,60 +1,70 @@
<?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(
+ 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/engine/PhabricatorMailEngineExtension.php b/src/applications/metamta/engine/PhabricatorMailEngineExtension.php
new file mode 100644
index 000000000..36675dda4
--- /dev/null
+++ b/src/applications/metamta/engine/PhabricatorMailEngineExtension.php
@@ -0,0 +1,47 @@
+<?php
+
+abstract class PhabricatorMailEngineExtension
+ extends Phobject {
+
+ private $viewer;
+ private $editor;
+
+ final public function getExtensionKey() {
+ return $this->getPhobjectClassConstant('EXTENSIONKEY');
+ }
+
+ final public function setViewer($viewer) {
+ $this->viewer = $viewer;
+ return $this;
+ }
+
+ final public function getViewer() {
+ return $this->viewer;
+ }
+
+ final public function setEditor(
+ PhabricatorApplicationTransactionEditor $editor) {
+ $this->editor = $editor;
+ return $this;
+ }
+
+ final public function getEditor() {
+ return $this->editor;
+ }
+
+ abstract public function supportsObject($object);
+ abstract public function newMailStampTemplates($object);
+ abstract public function newMailStamps($object, array $xactions);
+
+ final public static function getAllExtensions() {
+ return id(new PhutilClassMapQuery())
+ ->setAncestorClass(__CLASS__)
+ ->setUniqueMethod('getExtensionKey')
+ ->execute();
+ }
+
+ final protected function getMailStamp($key) {
+ return $this->getEditor()->getMailStamp($key);
+ }
+
+}
diff --git a/src/applications/metamta/garbagecollector/MetaMTAMailSentGarbageCollector.php b/src/applications/metamta/garbagecollector/MetaMTAMailSentGarbageCollector.php
index c9ca27443..dacd46d18 100644
--- a/src/applications/metamta/garbagecollector/MetaMTAMailSentGarbageCollector.php
+++ b/src/applications/metamta/garbagecollector/MetaMTAMailSentGarbageCollector.php
@@ -1,28 +1,29 @@
<?php
final class MetaMTAMailSentGarbageCollector
extends PhabricatorGarbageCollector {
const COLLECTORCONST = 'metamta.sent';
public function getCollectorName() {
return pht('Mail (Sent)');
}
public function getDefaultRetentionPolicy() {
return phutil_units('90 days in seconds');
}
protected function collectGarbage() {
$mails = id(new PhabricatorMetaMTAMail())->loadAllWhere(
'dateCreated < %d LIMIT 100',
$this->getGarbageEpoch());
+ $engine = new PhabricatorDestructionEngine();
foreach ($mails as $mail) {
- $mail->delete();
+ $engine->destroyObject($mail);
}
return (count($mails) == 100);
}
}
diff --git a/src/applications/metamta/herald/PhabricatorMailMustEncryptHeraldAction.php b/src/applications/metamta/herald/PhabricatorMailMustEncryptHeraldAction.php
new file mode 100644
index 000000000..027e1bb73
--- /dev/null
+++ b/src/applications/metamta/herald/PhabricatorMailMustEncryptHeraldAction.php
@@ -0,0 +1,62 @@
+<?php
+
+final class PhabricatorMailMustEncryptHeraldAction
+ extends HeraldAction {
+
+ const DO_MUST_ENCRYPT = 'do.must-encrypt';
+
+ const ACTIONCONST = 'email.must-encrypt';
+
+ public function getHeraldActionName() {
+ return pht('Require secure email');
+ }
+
+ public function renderActionDescription($value) {
+ return pht(
+ 'Require mail content be transmitted only over secure channels.');
+ }
+ public function supportsObject($object) {
+ return PhabricatorMetaMTAEmailHeraldAction::isMailGeneratingObject($object);
+ }
+
+ public function getActionGroupKey() {
+ return HeraldUtilityActionGroup::ACTIONGROUPKEY;
+ }
+
+ public function supportsRuleType($rule_type) {
+ return ($rule_type != HeraldRuleTypeConfig::RULE_TYPE_PERSONAL);
+ }
+
+ public function getHeraldActionStandardType() {
+ return self::STANDARD_NONE;
+ }
+
+ public function applyEffect($object, HeraldEffect $effect) {
+ $rule_phid = $effect->getRule()->getPHID();
+
+ $adapter = $this->getAdapter();
+ $adapter->addMustEncryptReason($rule_phid);
+
+ $this->logEffect(self::DO_MUST_ENCRYPT, array($rule_phid));
+ }
+
+ protected function getActionEffectMap() {
+ return array(
+ self::DO_MUST_ENCRYPT => array(
+ 'icon' => 'fa-shield',
+ 'color' => 'blue',
+ 'name' => pht('Must Encrypt'),
+ ),
+ );
+ }
+
+ protected function renderActionEffectDescription($type, $data) {
+ switch ($type) {
+ case self::DO_MUST_ENCRYPT:
+ return pht(
+ 'Made it a requirement that mail content be transmitted only '.
+ 'over secure channels.');
+ }
+ }
+
+}
diff --git a/src/applications/metamta/herald/PhabricatorMailOutboundMailHeraldAdapter.php b/src/applications/metamta/herald/PhabricatorMailOutboundMailHeraldAdapter.php
index f96a6f86b..94c13c9e0 100644
--- a/src/applications/metamta/herald/PhabricatorMailOutboundMailHeraldAdapter.php
+++ b/src/applications/metamta/herald/PhabricatorMailOutboundMailHeraldAdapter.php
@@ -1,67 +1,71 @@
<?php
final class PhabricatorMailOutboundMailHeraldAdapter
extends HeraldAdapter {
private $mail;
public function getAdapterApplicationClass() {
return 'PhabricatorMetaMTAApplication';
}
public function getAdapterContentDescription() {
return pht('Route outbound email.');
}
protected function initializeNewAdapter() {
$this->mail = $this->newObject();
}
protected function newObject() {
return new PhabricatorMetaMTAMail();
}
public function isTestAdapterForObject($object) {
return ($object instanceof PhabricatorMetaMTAMail);
}
public function getAdapterTestDescription() {
return pht(
'Test rules which run when outbound mail is being prepared for '.
'delivery.');
}
public function getObject() {
return $this->mail;
}
public function setObject(PhabricatorMetaMTAMail $mail) {
$this->mail = $mail;
return $this;
}
public function getAdapterContentName() {
return pht('Outbound Mail');
}
public function isSingleEventAdapter() {
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 getHeraldName() {
return pht('Mail %d', $this->getObject()->getID());
}
+ public function supportsWebhooks() {
+ return false;
+ }
+
}
diff --git a/src/applications/metamta/herald/PhabricatorMetaMTAEmailHeraldAction.php b/src/applications/metamta/herald/PhabricatorMetaMTAEmailHeraldAction.php
index 74fb879fe..383b8ebd3 100644
--- a/src/applications/metamta/herald/PhabricatorMetaMTAEmailHeraldAction.php
+++ b/src/applications/metamta/herald/PhabricatorMetaMTAEmailHeraldAction.php
@@ -1,95 +1,99 @@
<?php
abstract class PhabricatorMetaMTAEmailHeraldAction
extends HeraldAction {
const DO_SEND = 'do.send';
const DO_FORCE = 'do.force';
public function getRequiredAdapterStates() {
return array(
HeraldMailableState::STATECONST,
);
}
public function supportsObject($object) {
+ return self::isMailGeneratingObject($object);
+ }
+
+ public static function isMailGeneratingObject($object) {
// NOTE: This implementation lacks generality, but there's no great way to
// figure out if something generates email right now.
if ($object instanceof DifferentialDiff) {
return false;
}
if ($object instanceof PhabricatorMetaMTAMail) {
return false;
}
return true;
}
public function getActionGroupKey() {
return HeraldNotifyActionGroup::ACTIONGROUPKEY;
}
protected function applyEmail(array $phids, $force) {
$adapter = $this->getAdapter();
$allowed_types = array(
PhabricatorPeopleUserPHIDType::TYPECONST,
PhabricatorProjectProjectPHIDType::TYPECONST,
);
// There's no stateful behavior for this action: we always just send an
// email.
$current = array();
$targets = $this->loadStandardTargets($phids, $allowed_types, $current);
if (!$targets) {
return;
}
$phids = array_fuse(array_keys($targets));
foreach ($phids as $phid) {
$adapter->addEmailPHID($phid, $force);
}
if ($force) {
$this->logEffect(self::DO_FORCE, $phids);
} else {
$this->logEffect(self::DO_SEND, $phids);
}
}
protected function getActionEffectMap() {
return array(
self::DO_SEND => array(
'icon' => 'fa-envelope',
'color' => 'green',
'name' => pht('Sent Mail'),
),
self::DO_FORCE => array(
'icon' => 'fa-envelope',
'color' => 'blue',
'name' => pht('Forced Mail'),
),
);
}
protected function renderActionEffectDescription($type, $data) {
switch ($type) {
case self::DO_SEND:
return pht(
'Queued email to be delivered to %s target(s): %s.',
phutil_count($data),
$this->renderHandleList($data));
case self::DO_FORCE:
return pht(
'Queued email to be delivered to %s target(s), ignoring their '.
'notification preferences: %s.',
phutil_count($data),
$this->renderHandleList($data));
}
}
}
diff --git a/src/applications/metamta/management/PhabricatorMailManagementListOutboundWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementListOutboundWorkflow.php
index b4aa76379..a83dafb0a 100644
--- a/src/applications/metamta/management/PhabricatorMailManagementListOutboundWorkflow.php
+++ b/src/applications/metamta/management/PhabricatorMailManagementListOutboundWorkflow.php
@@ -1,57 +1,59 @@
<?php
final class PhabricatorMailManagementListOutboundWorkflow
extends PhabricatorMailManagementWorkflow {
protected function didConstruct() {
$this
->setName('list-outbound')
->setSynopsis(pht('List outbound messages sent by Phabricator.'))
->setExamples(
'**list-outbound**')
->setArguments(
array(
array(
'name' => 'limit',
'param' => 'N',
'default' => 100,
'help' => pht(
'Show a specific number of messages (default 100).'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$viewer = $this->getViewer();
$mails = id(new PhabricatorMetaMTAMail())->loadAllWhere(
'1 = 1 ORDER BY id DESC LIMIT %d',
$args->getArg('limit'));
if (!$mails) {
$console->writeErr("%s\n", pht('No sent mail.'));
return 0;
}
$table = id(new PhutilConsoleTable())
->setShowHeader(false)
->addColumn('id', array('title' => pht('ID')))
+ ->addColumn('encrypt', array('title' => pht('#')))
->addColumn('status', array('title' => pht('Status')))
->addColumn('subject', array('title' => pht('Subject')));
foreach (array_reverse($mails) as $mail) {
$status = $mail->getStatus();
$table->addRow(array(
'id' => $mail->getID(),
+ 'encrypt' => ($mail->getMustEncrypt() ? '#' : ' '),
'status' => PhabricatorMailOutboundStatus::getStatusName($status),
'subject' => $mail->getSubject(),
));
}
$table->draw();
return 0;
}
}
diff --git a/src/applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php
index 9f4e91ca2..152b62fd3 100644
--- a/src/applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php
+++ b/src/applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php
@@ -1,181 +1,201 @@
<?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 = 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))));
+ }
+
+ $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/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php
index 54a91861a..0fc7dd14b 100644
--- a/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php
+++ b/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php
@@ -1,189 +1,226 @@
<?php
final class PhabricatorMailManagementShowOutboundWorkflow
extends PhabricatorMailManagementWorkflow {
protected function didConstruct() {
$this
->setName('show-outbound')
->setSynopsis(pht('Show diagnostic details about outbound mail.'))
->setExamples(
'**show-outbound** --id 1 --id 2')
->setArguments(
array(
array(
'name' => 'id',
'param' => 'id',
'help' => pht('Show details about outbound mail with given ID.'),
'repeat' => true,
),
array(
'name' => 'dump-html',
'help' => pht(
'Dump the HTML body of the mail. You can redirect it to a '.
'file and then open it in a browser.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$ids = $args->getArg('id');
if (!$ids) {
throw new PhutilArgumentUsageException(
pht(
"Use the '%s' flag to specify one or more messages to show.",
'--id'));
}
foreach ($ids as $id) {
if (!ctype_digit($id)) {
throw new PhutilArgumentUsageException(
pht(
'Argument "%s" is not a valid message ID.',
$id));
}
}
$messages = id(new PhabricatorMetaMTAMail())->loadAllWhere(
'id IN (%Ld)',
$ids);
if ($ids) {
$ids = array_fuse($ids);
$missing = array_diff_key($ids, $messages);
if ($missing) {
throw new PhutilArgumentUsageException(
pht(
'Some specified messages do not exist: %s',
implode(', ', array_keys($missing))));
}
}
$last_key = last_key($messages);
foreach ($messages as $message_key => $message) {
if ($args->getArg('dump-html')) {
$html_body = $message->getHTMLBody();
if (strlen($html_body)) {
$template =
"<!doctype html><html><body>{$html_body}</body></html>";
$console->writeOut("%s\n", $html_body);
} else {
$console->writeErr(
"%s\n",
pht('(This message has no HTML body.)'));
}
continue;
}
$info = array();
- $info[] = pht('PROPERTIES');
+ $info[] = $this->newSectionHeader(pht('PROPERTIES'));
$info[] = pht('ID: %d', $message->getID());
$info[] = pht('Status: %s', $message->getStatus());
$info[] = pht('Related PHID: %s', $message->getRelatedPHID());
$info[] = pht('Message: %s', $message->getMessage());
$ignore = array(
'body' => true,
+ 'body.sent' => true,
'html-body' => true,
'headers' => true,
'attachments' => true,
'headers.sent' => true,
+ 'headers.unfiltered' => true,
'authors.sent' => true,
);
$info[] = null;
- $info[] = pht('PARAMETERS');
+ $info[] = $this->newSectionHeader(pht('PARAMETERS'));
$parameters = $message->getParameters();
foreach ($parameters as $key => $value) {
if (isset($ignore[$key])) {
continue;
}
if (!is_scalar($value)) {
$value = json_encode($value);
}
$info[] = pht('%s: %s', $key, $value);
}
$info[] = null;
- $info[] = pht('HEADERS');
+ $info[] = $this->newSectionHeader(pht('HEADERS'));
$headers = $message->getDeliveredHeaders();
- if (!$headers) {
+ $unfiltered = $message->getUnfilteredHeaders();
+ if (!$unfiltered) {
$headers = $message->generateHeaders();
+ $unfiltered = $headers;
}
+ $header_map = array();
foreach ($headers as $header) {
list($name, $value) = $header;
- $info[] = "{$name}: {$value}";
+ $header_map[$name.':'.$value] = true;
+ }
+
+ foreach ($unfiltered as $header) {
+ list($name, $value) = $header;
+ $was_sent = isset($header_map[$name.':'.$value]);
+
+ if ($was_sent) {
+ $marker = ' ';
+ } else {
+ $marker = '#';
+ }
+
+ $info[] = "{$marker} {$name}: {$value}";
}
$attachments = idx($parameters, 'attachments');
if ($attachments) {
$info[] = null;
- $info[] = pht('ATTACHMENTS');
+
+ $info[] = $this->newSectionHeader(pht('ATTACHMENTS'));
+
foreach ($attachments as $attachment) {
$info[] = idx($attachment, 'filename', pht('Unnamed File'));
}
}
$all_actors = $message->loadAllActors();
$actors = $message->getDeliveredActors();
if ($actors) {
$info[] = null;
- $info[] = pht('RECIPIENTS');
+
+ $info[] = $this->newSectionHeader(pht('RECIPIENTS'));
+
foreach ($actors as $actor_phid => $actor_info) {
$actor = idx($all_actors, $actor_phid);
if ($actor) {
$actor_name = coalesce($actor->getName(), $actor_phid);
} else {
$actor_name = $actor_phid;
}
$deliverable = $actor_info['deliverable'];
if ($deliverable) {
$info[] = ' '.$actor_name;
} else {
$info[] = '! '.$actor_name;
}
$reasons = $actor_info['reasons'];
foreach ($reasons as $reason) {
$name = PhabricatorMetaMTAActor::getReasonName($reason);
$desc = PhabricatorMetaMTAActor::getReasonDescription($reason);
$info[] = ' - '.$name.': '.$desc;
}
}
}
$info[] = null;
- $info[] = pht('TEXT BODY');
+ $info[] = $this->newSectionHeader(pht('TEXT BODY'));
if (strlen($message->getBody())) {
- $info[] = $message->getBody();
+ $info[] = tsprintf('%B', $message->getBody());
} else {
$info[] = pht('(This message has no text body.)');
}
+ $delivered_body = $message->getDeliveredBody();
+ if ($delivered_body !== null) {
+ $info[] = null;
+ $info[] = $this->newSectionHeader(pht('BODY AS DELIVERED'), true);
+ $info[] = tsprintf('%B', $delivered_body);
+ }
+
$info[] = null;
- $info[] = pht('HTML BODY');
+ $info[] = $this->newSectionHeader(pht('HTML BODY'));
if (strlen($message->getHTMLBody())) {
$info[] = $message->getHTMLBody();
$info[] = null;
} else {
$info[] = pht('(This message has no HTML body.)');
}
$console->writeOut('%s', implode("\n", $info));
if ($message_key != $last_key) {
$console->writeOut("\n%s\n\n", str_repeat('-', 80));
}
}
}
+ private function newSectionHeader($label, $emphasize = false) {
+ if ($emphasize) {
+ return tsprintf('**<bg:yellow> %s </bg>**', $label);
+ } else {
+ return tsprintf('**<bg:blue> %s </bg>**', $label);
+ }
+ }
+
}
diff --git a/src/applications/metamta/query/PhabricatorMetaMTAActor.php b/src/applications/metamta/query/PhabricatorMetaMTAActor.php
index 1f4cc7da1..cf2060a8f 100644
--- a/src/applications/metamta/query/PhabricatorMetaMTAActor.php
+++ b/src/applications/metamta/query/PhabricatorMetaMTAActor.php
@@ -1,181 +1,185 @@
<?php
final class PhabricatorMetaMTAActor extends Phobject {
const STATUS_DELIVERABLE = 'deliverable';
const STATUS_UNDELIVERABLE = 'undeliverable';
const REASON_NONE = 'none';
const REASON_UNLOADABLE = 'unloadable';
const REASON_UNMAILABLE = 'unmailable';
const REASON_NO_ADDRESS = 'noaddress';
const REASON_DISABLED = 'disabled';
const REASON_MAIL_DISABLED = 'maildisabled';
const REASON_EXTERNAL_TYPE = 'exernaltype';
const REASON_RESPONSE = 'response';
const REASON_SELF = 'self';
const REASON_MAILTAGS = 'mailtags';
const REASON_BOT = 'bot';
const REASON_FORCE = 'force';
const REASON_FORCE_HERALD = 'force-herald';
const REASON_ROUTE_AS_NOTIFICATION = 'route-as-notification';
const REASON_ROUTE_AS_MAIL = 'route-as-mail';
const REASON_UNVERIFIED = 'unverified';
+ const REASON_MUTED = 'muted';
private $phid;
private $emailAddress;
private $name;
private $status = self::STATUS_DELIVERABLE;
private $reasons = array();
private $isVerified = false;
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
public function setEmailAddress($email_address) {
$this->emailAddress = $email_address;
return $this;
}
public function getEmailAddress() {
return $this->emailAddress;
}
public function setIsVerified($is_verified) {
$this->isVerified = $is_verified;
return $this;
}
public function getIsVerified() {
return $this->isVerified;
}
public function setPHID($phid) {
$this->phid = $phid;
return $this;
}
public function getPHID() {
return $this->phid;
}
public function setUndeliverable($reason) {
$this->reasons[] = $reason;
$this->status = self::STATUS_UNDELIVERABLE;
return $this;
}
public function setDeliverable($reason) {
$this->reasons[] = $reason;
$this->status = self::STATUS_DELIVERABLE;
return $this;
}
public function isDeliverable() {
return ($this->status === self::STATUS_DELIVERABLE);
}
public function getDeliverabilityReasons() {
return $this->reasons;
}
public static function isDeliveryReason($reason) {
switch ($reason) {
case self::REASON_NONE:
case self::REASON_FORCE:
case self::REASON_FORCE_HERALD:
case self::REASON_ROUTE_AS_MAIL:
return true;
default:
// All other reasons cause the message to not be delivered.
return false;
}
}
public static function getReasonName($reason) {
$names = array(
self::REASON_NONE => pht('None'),
self::REASON_DISABLED => pht('Disabled Recipient'),
self::REASON_BOT => pht('Bot Recipient'),
self::REASON_NO_ADDRESS => pht('No Address'),
self::REASON_EXTERNAL_TYPE => pht('External Recipient'),
self::REASON_UNMAILABLE => pht('Not Mailable'),
self::REASON_RESPONSE => pht('Similar Reply'),
self::REASON_SELF => pht('Self Mail'),
self::REASON_MAIL_DISABLED => pht('Mail Disabled'),
self::REASON_MAILTAGS => pht('Mail Tags'),
self::REASON_UNLOADABLE => pht('Bad Recipient'),
self::REASON_FORCE => pht('Forced Mail'),
self::REASON_FORCE_HERALD => pht('Forced by Herald'),
self::REASON_ROUTE_AS_NOTIFICATION => pht('Route as Notification'),
self::REASON_ROUTE_AS_MAIL => pht('Route as Mail'),
self::REASON_UNVERIFIED => pht('Address Not Verified'),
+ self::REASON_MUTED => pht('Muted'),
);
return idx($names, $reason, pht('Unknown ("%s")', $reason));
}
public static function getReasonDescription($reason) {
$descriptions = array(
self::REASON_NONE => pht(
'No special rules affected this mail.'),
self::REASON_DISABLED => pht(
'This user is disabled; disabled users do not receive mail.'),
self::REASON_BOT => pht(
'This user is a bot; bot accounts do not receive mail.'),
self::REASON_NO_ADDRESS => pht(
'Unable to load an email address for this PHID.'),
self::REASON_EXTERNAL_TYPE => pht(
'Only external accounts of type "email" are deliverable; this '.
'account has a different type.'),
self::REASON_UNMAILABLE => pht(
'This PHID type does not correspond to a mailable object.'),
self::REASON_RESPONSE => pht(
'This message is a response to another email message, and this '.
'recipient received the original email message, so we are not '.
'sending them this substantially similar message (for example, '.
'the sender used "Reply All" instead of "Reply" in response to '.
'mail from Phabricator).'),
self::REASON_SELF => pht(
'This recipient is the user whose actions caused delivery of '.
'this message, but they have set preferences so they do not '.
'receive mail about their own actions (Settings > Email '.
'Preferences > Self Actions).'),
self::REASON_MAIL_DISABLED => pht(
'This recipient has disabled all email notifications '.
'(Settings > Email Preferences > Email Notifications).'),
self::REASON_MAILTAGS => pht(
'This mail has tags which control which users receive it, and '.
'this recipient has not elected to receive mail with any of '.
'the tags on this message (Settings > Email Preferences).'),
self::REASON_UNLOADABLE => pht(
'Unable to load user record for this PHID.'),
self::REASON_FORCE => pht(
'Delivery of this mail is forced and ignores deliver preferences. '.
'Mail which uses forced delivery is usually related to account '.
'management or authentication. For example, password reset email '.
'ignores mail preferences.'),
self::REASON_FORCE_HERALD => pht(
'This recipient was added by a "Send me an Email" rule in Herald, '.
'which overrides some delivery settings.'),
self::REASON_ROUTE_AS_NOTIFICATION => pht(
'This message was downgraded to a notification by outbound mail '.
'rules in Herald.'),
self::REASON_ROUTE_AS_MAIL => pht(
'This message was upgraded to email by outbound mail rules '.
'in Herald.'),
self::REASON_UNVERIFIED => pht(
'This recipient does not have a verified primary email address.'),
+ self::REASON_MUTED => pht(
+ 'This recipient has muted notifications for this object.'),
);
return idx($descriptions, $reason, pht('Unknown Reason ("%s")', $reason));
}
}
diff --git a/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php b/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php
index b0ae2de49..f8dd784e3 100644
--- a/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php
+++ b/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php
@@ -1,406 +1,436 @@
<?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...
$hash = PhabricatorObjectMailReceiver::computeMailHash(
$receiver->getMailKey(),
$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();
$hash = PhabricatorObjectMailReceiver::computeMailHash(
$receiver->getMailKey(),
$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/replyhandler/PhabricatorMailTarget.php b/src/applications/metamta/replyhandler/PhabricatorMailTarget.php
index c607087b2..4bd510513 100644
--- a/src/applications/metamta/replyhandler/PhabricatorMailTarget.php
+++ b/src/applications/metamta/replyhandler/PhabricatorMailTarget.php
@@ -1,150 +1,182 @@
<?php
final class PhabricatorMailTarget extends Phobject {
private $viewer;
private $replyTo;
private $toMap = array();
private $ccMap = array();
private $rawToPHIDs;
private $rawCCPHIDs;
public function setRawToPHIDs(array $to_phids) {
$this->rawToPHIDs = $to_phids;
return $this;
}
public function setRawCCPHIDs(array $cc_phids) {
$this->rawCCPHIDs = $cc_phids;
return $this;
}
public function setCCMap(array $cc_map) {
$this->ccMap = $cc_map;
return $this;
}
public function getCCMap() {
return $this->ccMap;
}
public function setToMap(array $to_map) {
$this->toMap = $to_map;
return $this;
}
public function getToMap() {
return $this->toMap;
}
public function setReplyTo($reply_to) {
$this->replyTo = $reply_to;
return $this;
}
public function getReplyTo() {
return $this->replyTo;
}
public function setViewer($viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function willSendMail(PhabricatorMetaMTAMail $mail) {
$viewer = $this->getViewer();
+ $show_stamps = $mail->shouldRenderMailStampsInBody($viewer);
+
+ $body = $mail->getBody();
+ $html_body = $mail->getHTMLBody();
+ $has_html = (strlen($html_body) > 0);
+
+ if ($show_stamps) {
+ $stamps = $mail->getMailStamps();
+ if ($stamps) {
+ $body .= "\n";
+ $body .= pht('STAMPS');
+ $body .= "\n";
+ $body .= implode(' ', $stamps);
+ $body .= "\n";
+
+ if ($has_html) {
+ $html = array();
+ $html[] = phutil_tag('strong', array(), pht('STAMPS'));
+ $html[] = phutil_tag('br');
+ $html[] = phutil_tag(
+ 'span',
+ array(
+ 'style' => 'font-size: smaller; color: #92969D',
+ ),
+ phutil_implode_html(' ', $stamps));
+ $html[] = phutil_tag('br');
+ $html[] = phutil_tag('br');
+ $html = phutil_tag('div', array(), $html);
+ $html_body .= hsprintf('%s', $html);
+ }
+ }
+ }
+
$mail->addPHIDHeaders('X-Phabricator-To', $this->rawToPHIDs);
$mail->addPHIDHeaders('X-Phabricator-Cc', $this->rawCCPHIDs);
$to_handles = $viewer->loadHandles($this->rawToPHIDs);
$cc_handles = $viewer->loadHandles($this->rawCCPHIDs);
- $body = $mail->getBody();
$body .= "\n";
$body .= $this->getRecipientsSummary($to_handles, $cc_handles);
- $mail->setBody($body);
- $html_body = $mail->getHTMLBody();
- if (strlen($html_body)) {
+ if ($has_html) {
$html_body .= hsprintf(
'%s',
$this->getRecipientsSummaryHTML($to_handles, $cc_handles));
}
+
+ $mail->setBody($body);
$mail->setHTMLBody($html_body);
$reply_to = $this->getReplyTo();
if ($reply_to) {
$mail->setReplyTo($reply_to);
}
$to = array_keys($this->getToMap());
if ($to) {
$mail->addTos($to);
}
$cc = array_keys($this->getCCMap());
if ($cc) {
$mail->addCCs($cc);
}
return $mail;
}
private function getRecipientsSummary(
PhabricatorHandleList $to_handles,
PhabricatorHandleList $cc_handles) {
if (!PhabricatorEnv::getEnvConfig('metamta.recipients.show-hints')) {
return '';
}
$to_handles = iterator_to_array($to_handles);
$cc_handles = iterator_to_array($cc_handles);
$body = '';
if ($to_handles) {
$to_names = mpull($to_handles, 'getCommandLineObjectName');
$body .= "To: ".implode(', ', $to_names)."\n";
}
if ($cc_handles) {
$cc_names = mpull($cc_handles, 'getCommandLineObjectName');
$body .= "Cc: ".implode(', ', $cc_names)."\n";
}
return $body;
}
private function getRecipientsSummaryHTML(
PhabricatorHandleList $to_handles,
PhabricatorHandleList $cc_handles) {
if (!PhabricatorEnv::getEnvConfig('metamta.recipients.show-hints')) {
return '';
}
$to_handles = iterator_to_array($to_handles);
$cc_handles = iterator_to_array($cc_handles);
$body = array();
if ($to_handles) {
$body[] = phutil_tag('strong', array(), 'To: ');
$body[] = phutil_implode_html(', ', mpull($to_handles, 'getName'));
$body[] = phutil_tag('br');
}
if ($cc_handles) {
$body[] = phutil_tag('strong', array(), 'Cc: ');
$body[] = phutil_implode_html(', ', mpull($cc_handles, 'getName'));
$body[] = phutil_tag('br');
}
return phutil_tag('div', array(), $body);
}
}
diff --git a/src/applications/metamta/stamp/PhabricatorBoolMailStamp.php b/src/applications/metamta/stamp/PhabricatorBoolMailStamp.php
new file mode 100644
index 000000000..d274df67f
--- /dev/null
+++ b/src/applications/metamta/stamp/PhabricatorBoolMailStamp.php
@@ -0,0 +1,16 @@
+<?php
+
+final class PhabricatorBoolMailStamp
+ extends PhabricatorMailStamp {
+
+ const STAMPTYPE = 'bool';
+
+ public function renderStamps($value) {
+ if (!$value) {
+ return null;
+ }
+
+ return $this->renderStamp($this->getKey());
+ }
+
+}
diff --git a/src/applications/metamta/stamp/PhabricatorMailStamp.php b/src/applications/metamta/stamp/PhabricatorMailStamp.php
new file mode 100644
index 000000000..9b425a4bd
--- /dev/null
+++ b/src/applications/metamta/stamp/PhabricatorMailStamp.php
@@ -0,0 +1,88 @@
+<?php
+
+abstract class PhabricatorMailStamp
+ extends Phobject {
+
+ private $key;
+ private $value;
+ private $label;
+ private $viewer;
+
+ final public function getStampType() {
+ return $this->getPhobjectClassConstant('STAMPTYPE');
+ }
+
+ final public function setKey($key) {
+ $this->key = $key;
+ return $this;
+ }
+
+ final public function getKey() {
+ return $this->key;
+ }
+
+ final protected function setRawValue($value) {
+ $this->value = $value;
+ return $this;
+ }
+
+ final protected function getRawValue() {
+ return $this->value;
+ }
+
+ final public function setViewer(PhabricatorUser $viewer) {
+ $this->viewer = $viewer;
+ return $this;
+ }
+
+ final public function getViewer() {
+ return $this->viewer;
+ }
+
+ final public function setLabel($label) {
+ $this->label = $label;
+ return $this;
+ }
+
+ final public function getLabel() {
+ return $this->label;
+ }
+
+ public function setValue($value) {
+ return $this->setRawValue($value);
+ }
+
+ final public function toDictionary() {
+ return array(
+ 'type' => $this->getStampType(),
+ 'key' => $this->getKey(),
+ 'value' => $this->getValueForDictionary(),
+ );
+ }
+
+ final public static function getAllStamps() {
+ return id(new PhutilClassMapQuery())
+ ->setAncestorClass(__CLASS__)
+ ->setUniqueMethod('getStampType')
+ ->execute();
+ }
+
+ protected function getValueForDictionary() {
+ return $this->getRawValue();
+ }
+
+ public function setValueFromDictionary($value) {
+ return $this->setRawValue($value);
+ }
+
+ public function getValueForRendering() {
+ return $this->getRawValue();
+ }
+
+ abstract public function renderStamps($value);
+
+ final protected function renderStamp($key, $value = null) {
+ return $key.'('.$value.')';
+ }
+
+}
diff --git a/src/applications/metamta/stamp/PhabricatorPHIDMailStamp.php b/src/applications/metamta/stamp/PhabricatorPHIDMailStamp.php
new file mode 100644
index 000000000..ea7233fe5
--- /dev/null
+++ b/src/applications/metamta/stamp/PhabricatorPHIDMailStamp.php
@@ -0,0 +1,45 @@
+<?php
+
+final class PhabricatorPHIDMailStamp
+ extends PhabricatorMailStamp {
+
+ const STAMPTYPE = 'phid';
+
+ public function renderStamps($value) {
+ if ($value === null) {
+ return null;
+ }
+
+ $value = (array)$value;
+ if (!$value) {
+ return null;
+ }
+
+ // TODO: This recovers from a bug where blocking reviewers were serialized
+ // incorrectly into the flat mail stamp list in the worker queue as arrays.
+ // It can be removed some time after February 2018.
+ foreach ($value as $key => $v) {
+ if (is_array($v)) {
+ unset($value[$key]);
+ }
+ }
+
+ $viewer = $this->getViewer();
+ $handles = $viewer->loadHandles($value);
+
+ $results = array();
+ foreach ($value as $phid) {
+ $handle = $handles[$phid];
+
+ $mail_name = $handle->getMailStampName();
+ if ($mail_name === null) {
+ $mail_name = $handle->getPHID();
+ }
+
+ $results[] = $this->renderStamp($this->getKey(), $mail_name);
+ }
+
+ return $results;
+ }
+
+}
diff --git a/src/applications/metamta/stamp/PhabricatorStringMailStamp.php b/src/applications/metamta/stamp/PhabricatorStringMailStamp.php
new file mode 100644
index 000000000..b6210afb4
--- /dev/null
+++ b/src/applications/metamta/stamp/PhabricatorStringMailStamp.php
@@ -0,0 +1,26 @@
+<?php
+
+final class PhabricatorStringMailStamp
+ extends PhabricatorMailStamp {
+
+ const STAMPTYPE = 'string';
+
+ public function renderStamps($value) {
+ if ($value === null || $value === '') {
+ return null;
+ }
+
+ $value = (array)$value;
+ if (!$value) {
+ return null;
+ }
+
+ $results = array();
+ foreach ($value as $v) {
+ $results[] = $this->renderStamp($this->getKey(), $v);
+ }
+
+ return $results;
+ }
+
+}
diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAAttachment.php b/src/applications/metamta/storage/PhabricatorMetaMTAAttachment.php
index b26bb0b8a..256bee46e 100644
--- a/src/applications/metamta/storage/PhabricatorMetaMTAAttachment.php
+++ b/src/applications/metamta/storage/PhabricatorMetaMTAAttachment.php
@@ -1,56 +1,90 @@
<?php
final class PhabricatorMetaMTAAttachment extends Phobject {
- protected $data;
- protected $filename;
- protected $mimetype;
+
+ private $data;
+ private $filename;
+ private $mimetype;
+ private $file;
+ private $filePHID;
public function __construct($data, $filename, $mimetype) {
$this->setData($data);
$this->setFilename($filename);
$this->setMimeType($mimetype);
}
public function getData() {
return $this->data;
}
public function setData($data) {
$this->data = $data;
return $this;
}
public function getFilename() {
return $this->filename;
}
public function setFilename($filename) {
$this->filename = $filename;
return $this;
}
public function getMimeType() {
return $this->mimetype;
}
public function setMimeType($mimetype) {
$this->mimetype = $mimetype;
return $this;
}
public function toDictionary() {
+ if (!$this->file) {
+ $iterator = new ArrayIterator(array($this->getData()));
+
+ $source = id(new PhabricatorIteratorFileUploadSource())
+ ->setName($this->getFilename())
+ ->setViewPolicy(PhabricatorPolicies::POLICY_NOONE)
+ ->setMIMEType($this->getMimeType())
+ ->setIterator($iterator);
+
+ $this->file = $source->uploadFile();
+ }
+
return array(
'filename' => $this->getFilename(),
'mimetype' => $this->getMimeType(),
- 'data' => $this->getData(),
+ 'filePHID' => $this->file->getPHID(),
);
}
public static function newFromDictionary(array $dict) {
- return new PhabricatorMetaMTAAttachment(
+ $file = null;
+
+ $file_phid = idx($dict, 'filePHID');
+ if ($file_phid) {
+ $file = id(new PhabricatorFileQuery())
+ ->setViewer(PhabricatorUser::getOmnipotentUser())
+ ->withPHIDs(array($file_phid))
+ ->executeOne();
+ if ($file) {
+ $dict['data'] = $file->loadFileData();
+ }
+ }
+
+ $attachment = new self(
idx($dict, 'data'),
idx($dict, 'filename'),
idx($dict, 'mimetype'));
+
+ if ($file) {
+ $attachment->file = $file;
+ }
+
+ return $attachment;
}
}
diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php
index 20b148203..5326e1063 100644
--- a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php
+++ b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php
@@ -1,1184 +1,1549 @@
<?php
/**
* @task recipients Managing Recipients
*/
final class PhabricatorMetaMTAMail
extends PhabricatorMetaMTADAO
- implements PhabricatorPolicyInterface {
+ 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);
+ $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;
+ }
+
+ public function getMustEncrypt() {
+ return $this->getParam('mustEncrypt', false);
+ }
+
+ public function setMustEncryptReasons(array $reasons) {
+ $this->setParam('mustEncryptReasons', $reasons);
+ return $this;
+ }
+
+ 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;
}
- public function buildDefaultMailer() {
- return PhabricatorEnv::newObjectFromConfig('metamta.mail-adapter');
- }
-
-
/**
* Attempt to deliver an email immediately, in this process.
*
- * @param bool Try to deliver this email even if it has already been
- * delivered or is in backoff after a failed delivery attempt.
- * @param PhabricatorMailImplementationAdapter Use a specific mail adapter,
- * instead of the default.
- *
* @return void
*/
- public function sendNow(
- $force_send = false,
- PhabricatorMailImplementationAdapter $mailer = null) {
+ public function sendNow() {
+ if ($this->getStatus() != PhabricatorMailOutboundStatus::STATUS_QUEUE) {
+ throw new Exception(pht('Trying to send an already-sent mail!'));
+ }
+
+ $mailers = self::newMailers();
- if ($mailer === null) {
- $mailer = $this->buildDefaultMailer();
+ $try_mailers = $this->getParam('mailers.try');
+ if ($try_mailers) {
+ $mailers = mpull($mailers, null, 'getKey');
+ $mailers = array_select_keys($mailers, $try_mailers);
}
- if (!$force_send) {
- if ($this->getStatus() != PhabricatorMailOutboundStatus::STATUS_QUEUE) {
- throw new Exception(pht('Trying to send an already-sent mail!'));
+ 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]);
}
}
- try {
- $headers = $this->generateHeaders();
+ return array_values($mailers);
+ }
+
+ 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;
- $params = $this->parameters;
+ 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);
- $actors = $this->loadAllActors();
- $deliverable_actors = $this->filterDeliverableActors($actors);
+ $defaults = $mailer->newDefaultOptions();
+ $options = idx($spec, 'options', array()) + $defaults;
+ $mailer->setOptions($options);
- $default_from = PhabricatorEnv::getEnvConfig('metamta.default-address');
- if (empty($params['from'])) {
- $mailer->setFrom($default_from);
+ $mailers[] = $mailer;
}
+ }
- $is_first = idx($params, 'is-first-message');
- unset($params['is-first-message']);
+ $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;
+ }
+ }
- $is_threaded = (bool)idx($params, 'thread-id');
+ foreach ($sorted as $mailer) {
+ $mailer->prepareForSend();
+ }
- $reply_to_name = idx($params, 'reply-to-name', '');
- unset($params['reply-to-name']);
+ return $sorted;
+ }
- $add_cc = array();
- $add_to = array();
+ public function sendWithMailers(array $mailers) {
+ if (!$mailers) {
+ return $this
+ ->setStatus(PhabricatorMailOutboundStatus::STATUS_VOID)
+ ->setMessage(pht('No mailers are configured.'))
+ ->save();
+ }
+
+ $exceptions = array();
+ foreach ($mailers as $template_mailer) {
+ $mailer = null;
+
+ try {
+ $mailer = $this->buildMailer($template_mailer);
+ } catch (Exception $ex) {
+ $exceptions[] = $ex;
+ continue;
+ }
- // If multiplexing is enabled, 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()));
+ 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();
}
- $preferences = $this->loadPreferences($target_phid);
+ 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;
+ }
- 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':
- $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;
- }
+ // 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);
+ }
- $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'));
+ 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;
- case 'attachments':
- $value = $this->getAttachments();
- foreach ($value as $attachment) {
- $mailer->addAttachment(
- $attachment->getData(),
- $attachment->getFilename(),
- $attachment->getMimeType());
+ }
+
+ $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;
- case 'subject':
- $subject = array();
+ }
- if ($is_threaded) {
- if ($this->shouldAddRePrefix($preferences)) {
- $subject[] = 'Re:';
- }
+ $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'));
+ $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');
+ } 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);
+ $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;
}
- $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;
- }
+ $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;
}
+ }
- $body = idx($params, 'body', '');
- $max = PhabricatorEnv::getEnvConfig('metamta.email-body-limit');
- if (strlen($body) > $max) {
- $body = id(new PhutilUTF8StringTruncator())
- ->setMaximumBytes($max)
- ->truncateString($body);
- $body .= "\n";
- $body .= pht('(This email was truncated at %d bytes.)', $max);
- }
- $mailer->setBody($body);
+ $stamps = $this->getMailStamps();
+ if ($stamps) {
+ $headers[] = array('X-Phabricator-Stamps', implode(' ', $stamps));
+ }
- $html_emails = $this->shouldSendHTML($preferences);
- if ($html_emails && isset($params['html-body'])) {
- $mailer->setHTMLBody($params['html-body']);
- }
+ $raw_body = idx($params, 'body', '');
+ $body = $raw_body;
+ if ($must_encrypt) {
+ $parts = array();
+ $parts[] = pht(
+ 'The content for this message can only be transmitted over a '.
+ 'secure channel. To view the message content, follow this '.
+ 'link:');
- // Pass the headers to the mailer, then save the state so we can show
- // them in the web UI.
- foreach ($headers as $header) {
- list($header_key, $header_value) = $header;
- $mailer->addHeader($header_key, $header_value);
- }
- $this->setParam('headers.sent', $headers);
+ $parts[] = PhabricatorEnv::getProductionURI($this->getURI());
- // 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);
+ $body = implode("\n\n", $parts);
+ } else {
+ $body = $raw_body;
+ }
- $this->setParam('routing.sent', $this->getParam('routing'));
- $this->setParam('routingmap.sent', $this->getRoutingRuleMap());
+ $max = PhabricatorEnv::getEnvConfig('metamta.email-body-limit');
+ if (strlen($body) > $max) {
+ $body = id(new PhutilUTF8StringTruncator())
+ ->setMaximumBytes($max)
+ ->truncateString($body);
+ $body .= "\n";
+ $body .= pht('(This email was truncated at %d bytes.)', $max);
+ }
+ $mailer->setBody($body);
- if (!$add_to && !$add_cc) {
- $this->setStatus(PhabricatorMailOutboundStatus::STATUS_VOID);
- $this->setMessage(
- pht(
- 'Message has no valid recipients: all To/Cc are disabled, '.
- 'invalid, or configured not to receive this mail.'));
- return $this->save();
- }
+ // 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 ($this->getIsErrorEmail()) {
- $all_recipients = array_merge($add_to, $add_cc);
- if ($this->shouldRateLimitMail($all_recipients)) {
- $this->setStatus(PhabricatorMailOutboundStatus::STATUS_VOID);
- $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 $this->save();
- }
- }
+ if ($must_encrypt) {
+ $send_html = false;
+ } else {
+ $send_html = $this->shouldSendHTML($preferences);
+ }
- if (PhabricatorEnv::getEnvConfig('phabricator.silent')) {
- $this->setStatus(PhabricatorMailOutboundStatus::STATUS_VOID);
- $this->setMessage(
- pht(
- 'Phabricator is running in silent mode. See `%s` '.
- 'in the configuration to change this setting.',
- 'phabricator.silent'));
- return $this->save();
- }
+ if ($send_html && isset($params['html-body'])) {
+ $mailer->setHTMLBody($params['html-body']);
+ }
- // 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();
- }
- }
+ // 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);
- $add_to = array_unique($add_to);
- $add_cc = array_diff(array_unique($add_cc), $add_to);
+ $this->setParam('routing.sent', $this->getParam('routing'));
+ $this->setParam('routingmap.sent', $this->getRoutingRuleMap());
- $mailer->addTos($add_to);
- if ($add_cc) {
- $mailer->addCCs($add_cc);
- }
- } catch (Exception $ex) {
- $this
- ->setStatus(PhabricatorMailOutboundStatus::STATUS_FAIL)
- ->setMessage($ex->getMessage())
- ->save();
+ 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.'));
- throw $ex;
+ return null;
}
- 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.'));
+ 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;
}
+ }
- $this->setStatus(PhabricatorMailOutboundStatus::STATUS_SENT);
- $this->save();
+ 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 $this;
- } catch (PhabricatorMetaMTAPermanentFailureException $ex) {
- $this
- ->setStatus(PhabricatorMailOutboundStatus::STATUS_FAIL)
- ->setMessage($ex->getMessage())
- ->save();
+ return null;
+ }
- throw $ex;
- } catch (Exception $ex) {
- $this
- ->setMessage($ex->getMessage()."\n".$ex->getTraceAsString())
- ->save();
+ // 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);
- throw $ex;
+ $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 shouldMultiplexAllMail() {
+ 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 delete() {
- $this->openTransaction();
- queryfx(
- $this->establishConnection('w'),
- 'DELETE FROM %T WHERE src = %s AND type = %d',
- PhabricatorEdgeConfig::TABLE_NAME_EDGE,
- $this->getPHID(),
- PhabricatorMetaMTAMailHasRecipientEdgeType::EDGECONST);
- $ret = parent::delete();
- $this->saveTransaction();
-
- return $ret;
- }
-
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');
- // If the message has mailtags, filter out any recipients who don't want
- // to receive this type of mail.
$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::shouldMultiplexAllMail()) {
+ 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/PhabricatorMetaMTAReceivedMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php
index 18fa7dd2b..fc98d1701 100644
--- a/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php
+++ b/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php
@@ -1,382 +1,387 @@
<?php
final class PhabricatorMetaMTAReceivedMail extends PhabricatorMetaMTADAO {
protected $headers = array();
protected $bodies = array();
protected $attachments = array();
protected $status = '';
protected $relatedPHID;
protected $authorPHID;
protected $message;
protected $messageIDHash = '';
protected function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'headers' => self::SERIALIZATION_JSON,
'bodies' => self::SERIALIZATION_JSON,
'attachments' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'relatedPHID' => 'phid?',
'authorPHID' => 'phid?',
'message' => 'text?',
'messageIDHash' => 'bytes12',
'status' => 'text32',
),
self::CONFIG_KEY_SCHEMA => array(
'relatedPHID' => array(
'columns' => array('relatedPHID'),
),
'authorPHID' => array(
'columns' => array('authorPHID'),
),
'key_messageIDHash' => array(
'columns' => array('messageIDHash'),
),
'key_created' => array(
'columns' => array('dateCreated'),
),
),
) + parent::getConfiguration();
}
public function setHeaders(array $headers) {
// Normalize headers to lowercase.
$normalized = array();
foreach ($headers as $name => $value) {
$name = $this->normalizeMailHeaderName($name);
if ($name == 'message-id') {
$this->setMessageIDHash(PhabricatorHash::digestForIndex($value));
}
$normalized[$name] = $value;
}
$this->headers = $normalized;
return $this;
}
public function getHeader($key, $default = null) {
$key = $this->normalizeMailHeaderName($key);
return idx($this->headers, $key, $default);
}
private function normalizeMailHeaderName($name) {
return strtolower($name);
}
public function getMessageID() {
return $this->getHeader('Message-ID');
}
public function getSubject() {
return $this->getHeader('Subject');
}
public function getCCAddresses() {
return $this->getRawEmailAddresses(idx($this->headers, 'cc'));
}
public function getToAddresses() {
return $this->getRawEmailAddresses(idx($this->headers, 'to'));
}
public function loadAllRecipientPHIDs() {
$addresses = array_merge(
$this->getToAddresses(),
$this->getCCAddresses());
return $this->loadPHIDsFromAddresses($addresses);
}
public function loadCCPHIDs() {
return $this->loadPHIDsFromAddresses($this->getCCAddresses());
}
private function loadPHIDsFromAddresses(array $addresses) {
if (empty($addresses)) {
return array();
}
$users = id(new PhabricatorUserEmail())
->loadAllWhere('address IN (%Ls)', $addresses);
return mpull($users, 'getUserPHID');
}
public function processReceivedMail() {
+ $sender = null;
try {
$this->dropMailFromPhabricator();
$this->dropMailAlreadyReceived();
$receiver = $this->loadReceiver();
$sender = $receiver->loadSender($this);
$receiver->validateSender($this, $sender);
$this->setAuthorPHID($sender->getPHID());
// Now that we've identified the sender, mark them as the author of
// any attached files.
$attachments = $this->getAttachments();
if ($attachments) {
$files = id(new PhabricatorFileQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs($attachments)
->execute();
foreach ($files as $file) {
$file->setAuthorPHID($sender->getPHID())->save();
}
}
$receiver->receiveMail($this, $sender);
} catch (PhabricatorMetaMTAReceivedMailProcessingException $ex) {
switch ($ex->getStatusCode()) {
case MetaMTAReceivedMailStatus::STATUS_DUPLICATE:
case MetaMTAReceivedMailStatus::STATUS_FROM_PHABRICATOR:
// Don't send an error email back in these cases, since they're
// very unlikely to be the sender's fault.
break;
case MetaMTAReceivedMailStatus::STATUS_EMPTY_IGNORED:
// This error is explicitly ignored.
break;
default:
- $this->sendExceptionMail($ex);
+ $this->sendExceptionMail($ex, $sender);
break;
}
$this
->setStatus($ex->getStatusCode())
->setMessage($ex->getMessage())
->save();
return $this;
} catch (Exception $ex) {
- $this->sendExceptionMail($ex);
+ $this->sendExceptionMail($ex, $sender);
$this
->setStatus(MetaMTAReceivedMailStatus::STATUS_UNHANDLED_EXCEPTION)
->setMessage(pht('Unhandled Exception: %s', $ex->getMessage()))
->save();
throw $ex;
}
return $this->setMessage('OK')->save();
}
public function getCleanTextBody() {
$body = $this->getRawTextBody();
$parser = new PhabricatorMetaMTAEmailBodyParser();
return $parser->stripTextBody($body);
}
public function parseBody() {
$body = $this->getRawTextBody();
$parser = new PhabricatorMetaMTAEmailBodyParser();
return $parser->parseBody($body);
}
public function getRawTextBody() {
return idx($this->bodies, 'text');
}
/**
* Strip an email address down to the actual user@domain.tld part if
* necessary, since sometimes it will have formatting like
* '"Abraham Lincoln" <alincoln@logcab.in>'.
*/
private function getRawEmailAddress($address) {
$matches = null;
$ok = preg_match('/<(.*)>/', $address, $matches);
if ($ok) {
$address = $matches[1];
}
return $address;
}
private function getRawEmailAddresses($addresses) {
$raw_addresses = array();
foreach (explode(',', $addresses) as $address) {
$raw_addresses[] = $this->getRawEmailAddress($address);
}
return array_filter($raw_addresses);
}
/**
* If Phabricator sent the mail, always drop it immediately. This prevents
* loops where, e.g., the public bug address is also a user email address
* and creating a bug sends them an email, which loops.
*/
private function dropMailFromPhabricator() {
if (!$this->getHeader('x-phabricator-sent-this-message')) {
return;
}
throw new PhabricatorMetaMTAReceivedMailProcessingException(
MetaMTAReceivedMailStatus::STATUS_FROM_PHABRICATOR,
pht(
"Ignoring email with '%s' header to avoid loops.",
'X-Phabricator-Sent-This-Message'));
}
/**
* If this mail has the same message ID as some other mail, and isn't the
* first mail we we received with that message ID, we drop it as a duplicate.
*/
private function dropMailAlreadyReceived() {
$message_id_hash = $this->getMessageIDHash();
if (!$message_id_hash) {
// No message ID hash, so we can't detect duplicates. This should only
// happen with very old messages.
return;
}
$messages = $this->loadAllWhere(
'messageIDHash = %s ORDER BY id ASC LIMIT 2',
$message_id_hash);
$messages_count = count($messages);
if ($messages_count <= 1) {
// If we only have one copy of this message, we're good to process it.
return;
}
$first_message = reset($messages);
if ($first_message->getID() == $this->getID()) {
// If this is the first copy of the message, it is okay to process it.
// We may not have been able to to process it immediately when we received
// it, and could may have received several copies without processing any
// yet.
return;
}
$message = pht(
'Ignoring email with "Message-ID" hash "%s" that has been seen %d '.
'times, including this message.',
$message_id_hash,
$messages_count);
throw new PhabricatorMetaMTAReceivedMailProcessingException(
MetaMTAReceivedMailStatus::STATUS_DUPLICATE,
$message);
}
/**
* Load a concrete instance of the @{class:PhabricatorMailReceiver} which
* accepts this mail, if one exists.
*/
private function loadReceiver() {
$receivers = id(new PhutilClassMapQuery())
->setAncestorClass('PhabricatorMailReceiver')
->setFilterMethod('isEnabled')
->execute();
$accept = array();
foreach ($receivers as $key => $receiver) {
if ($receiver->canAcceptMail($this)) {
$accept[$key] = $receiver;
}
}
if (!$accept) {
throw new PhabricatorMetaMTAReceivedMailProcessingException(
MetaMTAReceivedMailStatus::STATUS_NO_RECEIVERS,
pht(
'Phabricator can not process this mail because no application '.
'knows how to handle it. Check that the address you sent it to is '.
'correct.'.
"\n\n".
'(No concrete, enabled subclass of PhabricatorMailReceiver can '.
'accept this mail.)'));
}
if (count($accept) > 1) {
$names = implode(', ', array_keys($accept));
throw new PhabricatorMetaMTAReceivedMailProcessingException(
MetaMTAReceivedMailStatus::STATUS_ABUNDANT_RECEIVERS,
pht(
'Phabricator is not able to process this mail because more than '.
'one application is willing to accept it, creating ambiguity. '.
'Mail needs to be accepted by exactly one receiving application.'.
"\n\n".
'Accepting receivers: %s.',
$names));
}
return head($accept);
}
- private function sendExceptionMail(Exception $ex) {
- $from = $this->getHeader('from');
- if (!strlen($from)) {
+ private function sendExceptionMail(
+ Exception $ex,
+ PhabricatorUser $viewer = null) {
+
+ // If we've failed to identify a legitimate sender, we don't send them
+ // an error message back. We want to avoid sending mail to unverified
+ // addresses. See T12491.
+ if (!$viewer) {
return;
}
if ($ex instanceof PhabricatorMetaMTAReceivedMailProcessingException) {
$status_code = $ex->getStatusCode();
$status_name = MetaMTAReceivedMailStatus::getHumanReadableName(
$status_code);
$title = pht('Error Processing Mail (%s)', $status_name);
$description = $ex->getMessage();
} else {
$title = pht('Error Processing Mail (%s)', get_class($ex));
$description = pht('%s: %s', get_class($ex), $ex->getMessage());
}
// TODO: Since headers don't necessarily have unique names, this may not
// really be all the headers. It would be nice to pass the raw headers
// through from the upper layers where possible.
// On the MimeMailParser pathway, we arrive here with a list value for
// headers that appeared multiple times in the original mail. Be
// accommodating until header handling gets straightened out.
$headers = array();
foreach ($this->headers as $key => $values) {
if (!is_array($values)) {
$values = array($values);
}
foreach ($values as $value) {
$headers[] = pht('%s: %s', $key, $value);
}
}
$headers = implode("\n", $headers);
$body = pht(<<<EOBODY
Your email to Phabricator was not processed, because an error occurred while
trying to handle it:
%s
-- Original Message Body -----------------------------------------------------
%s
-- Original Message Headers --------------------------------------------------
%s
EOBODY
,
wordwrap($description, 78),
$this->getRawTextBody(),
$headers);
$mail = id(new PhabricatorMetaMTAMail())
->setIsErrorEmail(true)
- ->setForceDelivery(true)
->setSubject($title)
- ->addRawTos(array($from))
+ ->addTos(array($viewer->getPHID()))
->setBody($body)
->saveAndSend();
}
public function newContentSource() {
return PhabricatorContentSource::newForSource(
PhabricatorEmailContentSource::SOURCECONST,
array(
'id' => $this->getID(),
));
}
}
diff --git a/src/applications/metamta/storage/__tests__/PhabricatorMailConfigTestCase.php b/src/applications/metamta/storage/__tests__/PhabricatorMailConfigTestCase.php
new file mode 100644
index 000000000..25984a2c1
--- /dev/null
+++ b/src/applications/metamta/storage/__tests__/PhabricatorMailConfigTestCase.php
@@ -0,0 +1,131 @@
+<?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) {
+ $env = PhabricatorEnv::beginScopedEnv();
+ $env->overrideEnvConfig('cluster.mailers', $config);
+
+ $mailers = PhabricatorMetaMTAMail::newMailers();
+
+ unset($env);
+ return $mailers;
+ }
+
+}
diff --git a/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php b/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php
index 635913439..c0045301f 100644
--- a/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php
+++ b/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php
@@ -1,254 +1,334 @@
<?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->sendNow($force = true, $mailer);
+ $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->sendNow($force = true, $mailer);
+ $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->sendNow($force = true, $mailer);
+ $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 = 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->sendNow($force = true, $mailer);
+ $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());
+ }
+
}
diff --git a/src/applications/metamta/view/PhabricatorMetaMTAMailBody.php b/src/applications/metamta/view/PhabricatorMetaMTAMailBody.php
index 8a21a75cf..6aaf4fb05 100644
--- a/src/applications/metamta/view/PhabricatorMetaMTAMailBody.php
+++ b/src/applications/metamta/view/PhabricatorMetaMTAMailBody.php
@@ -1,218 +1,241 @@
<?php
/**
* Render the body of an application email by building it up section-by-section.
*
* @task compose Composition
* @task render Rendering
*/
final class PhabricatorMetaMTAMailBody extends Phobject {
private $sections = array();
private $htmlSections = array();
private $attachments = array();
private $viewer;
+ private $contextObject;
public function getViewer() {
return $this->viewer;
}
public function setViewer($viewer) {
$this->viewer = $viewer;
return $this;
}
+ public function setContextObject($context_object) {
+ $this->contextObject = $context_object;
+ return $this;
+ }
+
+ public function getContextObject() {
+ return $this->contextObject;
+ }
+
+
/* -( Composition )-------------------------------------------------------- */
/**
* Add a raw block of text to the email. This will be rendered as-is.
*
* @param string Block of text.
* @return this
* @task compose
*/
public function addRawSection($text) {
if (strlen($text)) {
$text = rtrim($text);
$this->sections[] = $text;
$this->htmlSections[] = phutil_escape_html_newlines(
phutil_tag('div', array(), $text));
}
return $this;
}
public function addRemarkupSection($header, $text) {
try {
- $engine = PhabricatorMarkupEngine::newMarkupEngine(array());
- $engine->setConfig('viewer', $this->getViewer());
- $engine->setMode(PhutilRemarkupEngine::MODE_TEXT);
+ $engine = $this->newMarkupEngine()
+ ->setMode(PhutilRemarkupEngine::MODE_TEXT);
+
$styled_text = $engine->markupText($text);
$this->addPlaintextSection($header, $styled_text);
} catch (Exception $ex) {
phlog($ex);
$this->addTextSection($header, $text);
}
try {
- $mail_engine = PhabricatorMarkupEngine::newMarkupEngine(array());
- $mail_engine->setConfig('viewer', $this->getViewer());
- $mail_engine->setMode(PhutilRemarkupEngine::MODE_HTML_MAIL);
- $mail_engine->setConfig(
- 'uri.base',
- PhabricatorEnv::getProductionURI('/'));
+ $mail_engine = $this->newMarkupEngine()
+ ->setMode(PhutilRemarkupEngine::MODE_HTML_MAIL);
+
$html = $mail_engine->markupText($text);
$this->addHTMLSection($header, $html);
} catch (Exception $ex) {
phlog($ex);
$this->addHTMLSection($header, $text);
}
return $this;
}
public function addRawPlaintextSection($text) {
if (strlen($text)) {
$text = rtrim($text);
$this->sections[] = $text;
}
return $this;
}
public function addRawHTMLSection($html) {
$this->htmlSections[] = phutil_safe_html($html);
return $this;
}
/**
* Add a block of text with a section header. This is rendered like this:
*
* HEADER
* Text is indented.
*
* @param string Header text.
* @param string Section text.
* @return this
* @task compose
*/
public function addTextSection($header, $section) {
if ($section instanceof PhabricatorMetaMTAMailSection) {
$plaintext = $section->getPlaintext();
$html = $section->getHTML();
} else {
$plaintext = $section;
$html = phutil_escape_html_newlines(phutil_tag('div', array(), $section));
}
$this->addPlaintextSection($header, $plaintext);
$this->addHTMLSection($header, $html);
return $this;
}
public function addPlaintextSection($header, $text, $indent = true) {
if ($indent) {
$text = $this->indent($text);
}
$this->sections[] = $header."\n".$text;
return $this;
}
public function addHTMLSection($header, $html_fragment) {
if ($header !== null) {
$header = phutil_tag('strong', array(), $header);
}
$this->htmlSections[] = array(
phutil_tag(
'div',
array(),
array(
$header,
phutil_tag('div', array(), $html_fragment),
)),
);
return $this;
}
public function addLinkSection($header, $link) {
$html = phutil_tag('a', array('href' => $link), $link);
$this->addPlaintextSection($header, $link);
$this->addHTMLSection($header, $html);
return $this;
}
/**
* Add a Herald section with a rule management URI and a transcript URI.
*
* @param string URI to rule transcripts.
* @return this
* @task compose
*/
public function addHeraldSection($xscript_uri) {
if (!PhabricatorEnv::getEnvConfig('metamta.herald.show-hints')) {
return $this;
}
$this->addLinkSection(
pht('WHY DID I GET THIS EMAIL?'),
PhabricatorEnv::getProductionURI($xscript_uri));
return $this;
}
/**
* Add an attachment.
*
* @param PhabricatorMetaMTAAttachment Attachment.
* @return this
* @task compose
*/
public function addAttachment(PhabricatorMetaMTAAttachment $attachment) {
$this->attachments[] = $attachment;
return $this;
}
/* -( Rendering )---------------------------------------------------------- */
/**
* Render the email body.
*
* @return string Rendered body.
* @task render
*/
public function render() {
return implode("\n\n", $this->sections)."\n";
}
public function renderHTML() {
$br = phutil_tag('br');
$body = phutil_implode_html($br, $this->htmlSections);
return (string)hsprintf('%s', array($body, $br));
}
/**
* Retrieve attachments.
*
* @return list<PhabricatorMetaMTAAttachment> Attachments.
* @task render
*/
public function getAttachments() {
return $this->attachments;
}
/**
* Indent a block of text for rendering under a section heading.
*
* @param string Text to indent.
* @return string Indented text.
* @task render
*/
private function indent($text) {
return rtrim(" ".str_replace("\n", "\n ", $text));
}
+
+
+ private function newMarkupEngine() {
+ $engine = PhabricatorMarkupEngine::newMarkupEngine(array())
+ ->setConfig('viewer', $this->getViewer())
+ ->setConfig('uri.base', PhabricatorEnv::getProductionURI('/'));
+
+ $context = $this->getContextObject();
+ if ($context) {
+ $engine->setConfig('contextObject', $context);
+ }
+
+ return $engine;
+ }
+
}
diff --git a/src/applications/nuance/item/NuanceGitHubEventItemType.php b/src/applications/nuance/item/NuanceGitHubEventItemType.php
index b8bfb3cca..1f6249f22 100644
--- a/src/applications/nuance/item/NuanceGitHubEventItemType.php
+++ b/src/applications/nuance/item/NuanceGitHubEventItemType.php
@@ -1,450 +1,452 @@
<?php
final class NuanceGitHubEventItemType
extends NuanceItemType {
const ITEMTYPE = 'github.event';
private $externalObject;
private $externalActor;
public function getItemTypeDisplayName() {
return pht('GitHub Event');
}
public function getItemTypeDisplayIcon() {
return 'fa-github';
}
public function getItemDisplayName(NuanceItem $item) {
return $this->newRawEvent($item)->getEventFullTitle();
}
public function canUpdateItems() {
return true;
}
protected function updateItemFromSource(NuanceItem $item) {
$viewer = $this->getViewer();
$is_dirty = false;
$xobj = $this->reloadExternalObject($item);
if ($xobj) {
$item->setItemProperty('doorkeeper.xobj.phid', $xobj->getPHID());
$is_dirty = true;
}
$actor = $this->reloadExternalActor($item);
if ($actor) {
$item->setRequestorPHID($actor->getPHID());
$is_dirty = true;
}
if ($item->getStatus() == NuanceItem::STATUS_IMPORTING) {
$item->setStatus(NuanceItem::STATUS_ROUTING);
$is_dirty = true;
}
if ($is_dirty) {
$item->save();
}
}
private function getDoorkeeperActorRef(NuanceItem $item) {
$raw = $this->newRawEvent($item);
$user_id = $raw->getActorGitHubUserID();
if (!$user_id) {
return null;
}
$ref_type = DoorkeeperBridgeGitHubUser::OBJTYPE_GITHUB_USER;
return $this->newDoorkeeperRef()
->setObjectType($ref_type)
->setObjectID($user_id);
}
private function getDoorkeeperRef(NuanceItem $item) {
$raw = $this->newRawEvent($item);
$full_repository = $raw->getRepositoryFullName();
if (!strlen($full_repository)) {
return null;
}
if ($raw->isIssueEvent()) {
$ref_type = DoorkeeperBridgeGitHubIssue::OBJTYPE_GITHUB_ISSUE;
$issue_number = $raw->getIssueNumber();
$full_ref = "{$full_repository}#{$issue_number}";
} else {
return null;
}
return $this->newDoorkeeperRef()
->setObjectType($ref_type)
->setObjectID($full_ref);
}
private function newDoorkeeperRef() {
return id(new DoorkeeperObjectRef())
->setApplicationType(DoorkeeperBridgeGitHub::APPTYPE_GITHUB)
->setApplicationDomain(DoorkeeperBridgeGitHub::APPDOMAIN_GITHUB);
}
private function reloadExternalObject(NuanceItem $item, $local = false) {
$ref = $this->getDoorkeeperRef($item);
if (!$ref) {
return null;
}
$xobj = $this->reloadExternalRef($item, $ref, $local);
if ($xobj) {
$this->externalObject = $xobj;
}
return $xobj;
}
private function reloadExternalActor(NuanceItem $item, $local = false) {
$ref = $this->getDoorkeeperActorRef($item);
if (!$ref) {
return null;
}
$xobj = $this->reloadExternalRef($item, $ref, $local);
if ($xobj) {
$this->externalActor = $xobj;
}
return $xobj;
}
private function reloadExternalRef(
NuanceItem $item,
DoorkeeperObjectRef $ref,
$local) {
$source = $item->getSource();
$token = $source->getSourceProperty('github.token');
$token = new PhutilOpaqueEnvelope($token);
$viewer = $this->getViewer();
$ref = id(new DoorkeeperImportEngine())
->setViewer($viewer)
->setRefs(array($ref))
->setThrowOnMissingLink(true)
->setContextProperty('github.token', $token)
->needLocalOnly($local)
->executeOne();
if ($ref->getSyncFailed()) {
$xobj = null;
} else {
$xobj = $ref->getExternalObject();
}
return $xobj;
}
private function getExternalObject(NuanceItem $item) {
if ($this->externalObject === null) {
$xobj = $this->reloadExternalObject($item, $local = true);
if ($xobj) {
$this->externalObject = $xobj;
} else {
$this->externalObject = false;
}
}
if ($this->externalObject) {
return $this->externalObject;
}
return null;
}
private function getExternalActor(NuanceItem $item) {
if ($this->externalActor === null) {
$xobj = $this->reloadExternalActor($item, $local = true);
if ($xobj) {
$this->externalActor = $xobj;
} else {
$this->externalActor = false;
}
}
if ($this->externalActor) {
return $this->externalActor;
}
return null;
}
private function newRawEvent(NuanceItem $item) {
$type = $item->getItemProperty('api.type');
$raw = $item->getItemProperty('api.raw', array());
return NuanceGitHubRawEvent::newEvent($type, $raw);
}
public function getItemActions(NuanceItem $item) {
$actions = array();
$xobj = $this->getExternalObject($item);
if ($xobj) {
$actions[] = $this->newItemAction($item, 'reload')
->setName(pht('Reload from GitHub'))
->setIcon('fa-refresh')
->setWorkflow(true)
->setRenderAsForm(true);
}
$actions[] = $this->newItemAction($item, 'sync')
->setName(pht('Import to Maniphest'))
->setIcon('fa-anchor')
->setWorkflow(true)
->setRenderAsForm(true);
$actions[] = $this->newItemAction($item, 'raw')
->setName(pht('View Raw Event'))
->setWorkflow(true)
->setIcon('fa-code');
return $actions;
}
public function getItemCurtainPanels(NuanceItem $item) {
$viewer = $this->getViewer();
$panels = array();
$xobj = $this->getExternalObject($item);
if ($xobj) {
$xobj_phid = $xobj->getPHID();
$task = id(new ManiphestTaskQuery())
->setViewer($viewer)
->withBridgedObjectPHIDs(array($xobj_phid))
->executeOne();
if ($task) {
$panels[] = $this->newCurtainPanel($item)
->setHeaderText(pht('Imported As'))
->appendChild($viewer->renderHandle($task->getPHID()));
}
}
$xactor = $this->getExternalActor($item);
if ($xactor) {
$panels[] = $this->newCurtainPanel($item)
->setHeaderText(pht('GitHub Actor'))
->appendChild($viewer->renderHandle($xactor->getPHID()));
}
return $panels;
}
protected function handleAction(NuanceItem $item, $action) {
$viewer = $this->getViewer();
$controller = $this->getController();
switch ($action) {
case 'raw':
$raw = array(
'api.type' => $item->getItemProperty('api.type'),
'api.raw' => $item->getItemProperty('api.raw'),
);
$raw_output = id(new PhutilJSON())->encodeFormatted($raw);
$raw_box = id(new AphrontFormTextAreaControl())
->setCustomClass('PhabricatorMonospaced')
->setLabel(pht('Raw Event'))
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL)
->setValue($raw_output);
$form = id(new AphrontFormView())
->appendChild($raw_box);
return $controller->newDialog()
->setWidth(AphrontDialogView::WIDTH_FULL)
->setTitle(pht('GitHub Raw Event'))
->appendForm($form)
->addCancelButton($item->getURI(), pht('Done'));
case 'sync':
case 'reload':
$item->issueCommand($viewer->getPHID(), $action);
return id(new AphrontRedirectResponse())->setURI($item->getURI());
}
return null;
}
protected function newItemView(NuanceItem $item) {
$content = array();
$content[] = $this->newGitHubEventItemPropertyBox($item);
return $content;
}
private function newGitHubEventItemPropertyBox($item) {
$viewer = $this->getViewer();
$property_list = id(new PHUIPropertyListView())
->setViewer($viewer);
$event = $this->newRawEvent($item);
$property_list->addProperty(
pht('GitHub Event ID'),
$event->getID());
$event_uri = $event->getURI();
if ($event_uri && PhabricatorEnv::isValidRemoteURIForLink($event_uri)) {
$event_uri = phutil_tag(
'a',
array(
'href' => $event_uri,
+ 'target' => '_blank',
+ 'rel' => 'noreferrer',
),
$event_uri);
}
if ($event_uri) {
$property_list->addProperty(
pht('GitHub Event URI'),
$event_uri);
}
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Event Properties'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($property_list);
}
protected function handleCommand(
NuanceItem $item,
NuanceItemCommand $command) {
// TODO: This code is no longer reachable, and has moved to
// CommandImplementation subclasses.
$action = $command->getCommand();
switch ($action) {
case 'sync':
return $this->syncItem($item, $command);
case 'reload':
$this->reloadExternalObject($item);
return true;
}
return null;
}
private function syncItem(
NuanceItem $item,
NuanceItemCommand $command) {
$xobj_phid = $item->getItemProperty('doorkeeper.xobj.phid');
if (!$xobj_phid) {
throw new Exception(
pht(
'Unable to sync: no external object PHID.'));
}
// TODO: Write some kind of marker to prevent double-synchronization.
$viewer = $this->getViewer();
$xobj = id(new DoorkeeperExternalObjectQuery())
->setViewer($viewer)
->withPHIDs(array($xobj_phid))
->executeOne();
if (!$xobj) {
throw new Exception(
pht(
'Unable to sync: failed to load object "%s".',
$xobj_phid));
}
$acting_as_phid = $this->getActingAsPHID($item);
$xactions = array();
$task = id(new ManiphestTaskQuery())
->setViewer($viewer)
->withBridgedObjectPHIDs(array($xobj_phid))
->executeOne();
if (!$task) {
$task = ManiphestTask::initializeNewTask($viewer)
->setAuthorPHID($acting_as_phid)
->setBridgedObjectPHID($xobj_phid);
$title = $xobj->getProperty('task.title');
if (!strlen($title)) {
$title = pht('Nuance Item %d Task', $item->getID());
}
$description = $xobj->getProperty('task.description');
$created = $xobj->getProperty('task.created');
$state = $xobj->getProperty('task.state');
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(
ManiphestTaskTitleTransaction::TRANSACTIONTYPE)
->setNewValue($title)
->setDateCreated($created);
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(
ManiphestTaskDescriptionTransaction::TRANSACTIONTYPE)
->setNewValue($description)
->setDateCreated($created);
$task->setDateCreated($created);
// TODO: Synchronize state.
}
$event = $this->newRawEvent($item);
$comment = $event->getComment();
if (strlen($comment)) {
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment(
id(new ManiphestTransactionComment())
->setContent($comment));
}
$agent_phid = $command->getAuthorPHID();
$source = $this->newContentSource($item, $agent_phid);
$editor = id(new ManiphestTransactionEditor())
->setActor($viewer)
->setActingAsPHID($acting_as_phid)
->setContentSource($source)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
$xactions = $editor->applyTransactions($task, $xactions);
return array(
'objectPHID' => $task->getPHID(),
'xactionPHIDs' => mpull($xactions, 'getPHID'),
);
}
protected function getActingAsPHID(NuanceItem $item) {
$actor_phid = $item->getRequestorPHID();
if ($actor_phid) {
return $actor_phid;
}
return parent::getActingAsPHID($item);
}
}
diff --git a/src/applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php b/src/applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php
index b9d916e82..745be3e82 100644
--- a/src/applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php
+++ b/src/applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php
@@ -1,293 +1,316 @@
<?php
final class PhabricatorOAuthServerAuthController
extends PhabricatorOAuthServerController {
protected function buildApplicationCrumbs() {
// We're specifically not putting an "OAuth Server" application crumb
// on the auth pages because it doesn't make sense to send users there.
return new PHUICrumbsView();
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$server = new PhabricatorOAuthServer();
$client_phid = $request->getStr('client_id');
$redirect_uri = $request->getStr('redirect_uri');
$response_type = $request->getStr('response_type');
// state is an opaque value the client sent us for their own purposes
// we just need to send it right back to them in the response!
$state = $request->getStr('state');
if (!$client_phid) {
return $this->buildErrorResponse(
'invalid_request',
pht('Malformed Request'),
pht(
'Required parameter %s was not present in the request.',
phutil_tag('strong', array(), 'client_id')));
}
// We require that users must be able to see an OAuth application
// in order to authorize it. This allows an application's visibility
// policy to be used to restrict authorized users.
try {
$client = id(new PhabricatorOAuthServerClientQuery())
->setViewer($viewer)
->withPHIDs(array($client_phid))
->executeOne();
} catch (PhabricatorPolicyException $ex) {
$ex->setContext(self::CONTEXT_AUTHORIZE);
throw $ex;
}
$server->setUser($viewer);
$is_authorized = false;
$authorization = null;
$uri = null;
$name = null;
// one giant try / catch around all the exciting database stuff so we
// can return a 'server_error' response if something goes wrong!
try {
if (!$client) {
return $this->buildErrorResponse(
'invalid_request',
pht('Invalid Client Application'),
pht(
'Request parameter %s does not specify a valid client application.',
phutil_tag('strong', array(), 'client_id')));
}
if ($client->getIsDisabled()) {
return $this->buildErrorResponse(
'invalid_request',
pht('Application Disabled'),
pht(
'The %s OAuth application has been disabled.',
phutil_tag('strong', array(), 'client_id')));
}
$name = $client->getName();
$server->setClient($client);
if ($redirect_uri) {
$client_uri = new PhutilURI($client->getRedirectURI());
$redirect_uri = new PhutilURI($redirect_uri);
if (!($server->validateSecondaryRedirectURI($redirect_uri,
$client_uri))) {
return $this->buildErrorResponse(
'invalid_request',
pht('Invalid Redirect URI'),
pht(
'Request parameter %s specifies an invalid redirect URI. '.
'The redirect URI must be a fully-qualified domain with no '.
'fragments, and must have the same domain and at least '.
'the same query parameters as the redirect URI the client '.
'registered.',
phutil_tag('strong', array(), 'redirect_uri')));
}
$uri = $redirect_uri;
} else {
$uri = new PhutilURI($client->getRedirectURI());
}
if (empty($response_type)) {
return $this->buildErrorResponse(
'invalid_request',
pht('Invalid Response Type'),
pht(
'Required request parameter %s is missing.',
phutil_tag('strong', array(), 'response_type')));
}
if ($response_type != 'code') {
return $this->buildErrorResponse(
'unsupported_response_type',
pht('Unsupported Response Type'),
pht(
'Request parameter %s specifies an unsupported response type. '.
'Valid response types are: %s.',
phutil_tag('strong', array(), 'response_type'),
implode(', ', array('code'))));
}
$requested_scope = $request->getStrList('scope');
$requested_scope = array_fuse($requested_scope);
$scope = PhabricatorOAuthServerScope::filterScope($requested_scope);
// NOTE: We're always requiring a confirmation dialog to redirect.
// Partly this is a general defense against redirect attacks, and
// partly this shakes off anchors in the URI (which are not shaken
// by 302'ing).
$auth_info = $server->userHasAuthorizedClient($scope);
list($is_authorized, $authorization) = $auth_info;
if ($request->isFormPost()) {
if ($authorization) {
$authorization->setScope($scope)->save();
} else {
$authorization = $server->authorizeClient($scope);
}
$is_authorized = true;
}
} catch (Exception $e) {
return $this->buildErrorResponse(
'server_error',
pht('Server Error'),
pht(
'The authorization server encountered an unexpected condition '.
'which prevented it from fulfilling the request.'));
}
// When we reach this part of the controller, we can be in two states:
//
// 1. The user has not authorized the application yet. We want to
// give them an "Authorize this application?" dialog.
// 2. The user has authorized the application. We want to give them
// a "Confirm Login" dialog.
if ($is_authorized) {
// The second case is simpler, so handle it first. The user either
// authorized the application previously, or has just authorized the
// application. Show them a confirm dialog with a normal link back to
// the application. This shakes anchors from the URI.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$auth_code = $server->generateAuthorizationCode($uri);
unset($unguarded);
$full_uri = $this->addQueryParams(
$uri,
array(
'code' => $auth_code->getCode(),
'scope' => $authorization->getScopeString(),
'state' => $state,
));
if ($client->getIsTrusted()) {
- return id(new AphrontRedirectResponse())
- ->setIsExternal(true)
- ->setURI((string)$full_uri);
+ // NOTE: See T13099. We currently emit a "Content-Security-Policy"
+ // which includes a narrow "form-action". At the time of writing,
+ // Chrome applies "form-action" to redirects following form submission.
+
+ // This can lead to a situation where a user enters the OAuth workflow
+ // and is prompted for MFA. When they submit an MFA response, the form
+ // can redirect here, and Chrome will block the "Location" redirect.
+
+ // To avoid this, render an interstitial. We only actually need to do
+ // this in Chrome (but do it everywhere for consistency) and only need
+ // to do it if the request is a redirect after a form submission (but
+ // we can't tell if it is or not).
+
+ Javelin::initBehavior(
+ 'redirect',
+ array(
+ 'uri' => (string)$full_uri,
+ ));
+
+ return $this->newDialog()
+ ->setTitle(pht('Authenticate: %s', $name))
+ ->appendParagraph(
+ pht(
+ 'Authorization for "%s" confirmed, redirecting...',
+ phutil_tag('strong', array(), $name)))
+ ->addCancelButton((string)$full_uri, pht('Continue'));
}
// TODO: It would be nice to give the user more options here, like
// reviewing permissions, canceling the authorization, or aborting
// the workflow.
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setTitle(pht('Authenticate: %s', $name))
->appendParagraph(
pht(
'This application ("%s") is authorized to use your Phabricator '.
'credentials. Continue to complete the authentication workflow.',
phutil_tag('strong', array(), $name)))
->addCancelButton((string)$full_uri, pht('Continue to Application'));
return id(new AphrontDialogResponse())->setDialog($dialog);
}
// Here, we're confirming authorization for the application.
if ($authorization) {
$missing_scope = array_diff_key($scope, $authorization->getScope());
} else {
$missing_scope = $scope;
}
$form = id(new AphrontFormView())
->addHiddenInput('client_id', $client_phid)
->addHiddenInput('redirect_uri', $redirect_uri)
->addHiddenInput('response_type', $response_type)
->addHiddenInput('state', $state)
->addHiddenInput('scope', $request->getStr('scope'))
->setUser($viewer);
$cancel_msg = pht('The user declined to authorize this application.');
$cancel_uri = $this->addQueryParams(
$uri,
array(
'error' => 'access_denied',
'error_description' => $cancel_msg,
));
$dialog = $this->newDialog()
->setShortTitle(pht('Authorize Access'))
->setTitle(pht('Authorize "%s"?', $name))
->setSubmitURI($request->getRequestURI()->getPath())
->setWidth(AphrontDialogView::WIDTH_FORM)
->appendParagraph(
pht(
'Do you want to authorize the external application "%s" to '.
'access your Phabricator account data, including your primary '.
'email address?',
phutil_tag('strong', array(), $name)))
->appendForm($form)
->addSubmitButton(pht('Authorize Access'))
->addCancelButton((string)$cancel_uri, pht('Do Not Authorize'));
if ($missing_scope) {
$dialog->appendParagraph(
pht(
'This application has requested these additional permissions. '.
'Authorizing it will grant it the permissions it requests:'));
foreach ($missing_scope as $scope_key => $ignored) {
// TODO: Once we introduce more scopes, explain them here.
}
}
$unknown_scope = array_diff_key($requested_scope, $scope);
if ($unknown_scope) {
$dialog->appendParagraph(
pht(
'This application also requested additional unrecognized '.
'permissions. These permissions may have existed in an older '.
'version of Phabricator, or may be from a future version of '.
'Phabricator. They will not be granted.'));
$unknown_form = id(new AphrontFormView())
->setViewer($viewer)
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Unknown Scope'))
->setValue(implode(', ', array_keys($unknown_scope)))
->setDisabled(true));
$dialog->appendForm($unknown_form);
}
return $dialog;
}
private function buildErrorResponse($code, $title, $message) {
$viewer = $this->getRequest()->getUser();
return $this->newDialog()
->setTitle(pht('OAuth: %s', $title))
->appendParagraph($message)
->appendParagraph(
pht('OAuth Error Code: %s', phutil_tag('tt', array(), $code)))
->addCancelButton('/', pht('Alas!'));
}
private function addQueryParams(PhutilURI $uri, array $params) {
$full_uri = clone $uri;
foreach ($params as $key => $value) {
if (strlen($value)) {
$full_uri->setQueryParam($key, $value);
}
}
return $full_uri;
}
}
diff --git a/src/applications/owners/controller/PhabricatorOwnersDetailController.php b/src/applications/owners/controller/PhabricatorOwnersDetailController.php
index 3c6ae3836..c7e96594f 100644
--- a/src/applications/owners/controller/PhabricatorOwnersDetailController.php
+++ b/src/applications/owners/controller/PhabricatorOwnersDetailController.php
@@ -1,342 +1,342 @@
<?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);
$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->getPath(),
+ 'path' => $path->getPathDisplay(),
'action' => 'browse',
));
$path_link = phutil_tag(
'a',
array(
'href' => (string)$href,
),
- $path->getPath());
+ $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/controller/PhabricatorOwnersPathsController.php b/src/applications/owners/controller/PhabricatorOwnersPathsController.php
index e5463b4cb..d4d97290f 100644
--- a/src/applications/owners/controller/PhabricatorOwnersPathsController.php
+++ b/src/applications/owners/controller/PhabricatorOwnersPathsController.php
@@ -1,177 +1,196 @@
<?php
final class PhabricatorOwnersPathsController
extends PhabricatorOwnersController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$package = id(new PhabricatorOwnersPackageQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->needPaths(true)
->executeOne();
if (!$package) {
return new Aphront404Response();
}
if ($request->isFormPost()) {
$paths = $request->getArr('path');
$repos = $request->getArr('repo');
$excludes = $request->getArr('exclude');
$path_refs = array();
foreach ($paths as $key => $path) {
- if (!isset($repos[$key])) {
+ if (!isset($repos[$key]) || !strlen($repos[$key])) {
throw new Exception(
pht(
'No repository PHID for path "%s"!',
$key));
}
if (!isset($excludes[$key])) {
throw new Exception(
pht(
'No exclusion value for path "%s"!',
$key));
}
$path_refs[] = array(
'repositoryPHID' => $repos[$key],
'path' => $path,
'excluded' => (int)$excludes[$key],
);
}
$type_paths = PhabricatorOwnersPackagePathsTransaction::TRANSACTIONTYPE;
$xactions = array();
$xactions[] = id(new PhabricatorOwnersPackageTransaction())
->setTransactionType($type_paths)
->setNewValue($path_refs);
$editor = id(new PhabricatorOwnersPackageTransactionEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
$editor->applyTransactions($package, $xactions);
return id(new AphrontRedirectResponse())
->setURI($package->getURI());
} else {
$paths = $package->getPaths();
$path_refs = mpull($paths, 'getRef');
}
- $repos = id(new PhabricatorRepositoryQuery())
- ->setViewer($viewer)
- ->execute();
+ $template = new AphrontTokenizerTemplateView();
- $default_paths = array();
- foreach ($repos as $repo) {
- $default_path = $repo->getDetail('default-owners-path');
- if ($default_path) {
- $default_paths[$repo->getPHID()] = $default_path;
- }
- }
+ $datasource = id(new DiffusionRepositoryDatasource())
+ ->setViewer($viewer);
+ $tokenizer_spec = array(
+ 'markup' => $template->render(),
+ 'config' => array(
+ 'src' => $datasource->getDatasourceURI(),
+ 'browseURI' => $datasource->getBrowseURI(),
+ 'placeholder' => $datasource->getPlaceholderText(),
+ 'limit' => 1,
+ ),
+ );
- $repo_map = array();
- foreach ($repos as $key => $repo) {
- $monogram = $repo->getMonogram();
- $name = $repo->getName();
- $repo_map[$repo->getPHID()] = "{$monogram} {$name}";
+ foreach ($path_refs as $key => $path_ref) {
+ $path_refs[$key]['repositoryValue'] = $datasource->getWireTokens(
+ array(
+ $path_ref['repositoryPHID'],
+ ));
}
- asort($repos);
+
+ $icon_test = id(new PHUIIconView())
+ ->setIcon('fa-spinner grey')
+ ->setTooltip(pht('Validating...'));
+
+ $icon_okay = id(new PHUIIconView())
+ ->setIcon('fa-check-circle green')
+ ->setTooltip(pht('Path Exists in Repository'));
+
+ $icon_fail = id(new PHUIIconView())
+ ->setIcon('fa-question-circle-o red')
+ ->setTooltip(pht('Path Not Found On Default Branch'));
$template = new AphrontTypeaheadTemplateView();
$template = $template->render();
Javelin::initBehavior(
'owners-path-editor',
array(
- 'root' => 'path-editor',
- 'table' => 'paths',
- 'add_button' => 'addpath',
- 'repositories' => $repo_map,
- 'input_template' => $template,
- 'pathRefs' => $path_refs,
-
- 'completeURI' => '/diffusion/services/path/complete/',
- 'validateURI' => '/diffusion/services/path/validate/',
-
- 'repositoryDefaultPaths' => $default_paths,
+ 'root' => 'path-editor',
+ 'table' => 'paths',
+ 'add_button' => 'addpath',
+ 'input_template' => $template,
+ 'pathRefs' => $path_refs,
+ 'completeURI' => '/diffusion/services/path/complete/',
+ 'validateURI' => '/diffusion/services/path/validate/',
+ 'repositoryTokenizerSpec' => $tokenizer_spec,
+ 'icons' => array(
+ 'test' => hsprintf('%s', $icon_test),
+ 'okay' => hsprintf('%s', $icon_okay),
+ 'fail' => hsprintf('%s', $icon_fail),
+ ),
+ 'modeOptions' => array(
+ 0 => pht('Include'),
+ 1 => pht('Exclude'),
+ ),
));
require_celerity_resource('owners-path-editor-css');
$cancel_uri = $package->getURI();
$form = id(new AphrontFormView())
->setUser($viewer)
->appendChild(
id(new PHUIFormInsetView())
->setTitle(pht('Paths'))
->addDivAttributes(array('id' => 'path-editor'))
->setRightButton(javelin_tag(
'a',
array(
'href' => '#',
'class' => 'button button-green',
'sigil' => 'addpath',
'mustcapture' => true,
),
pht('Add New Path')))
->setDescription(
pht(
'Specify the files and directories which comprise '.
'this package.'))
->setContent(javelin_tag(
'table',
array(
'class' => 'owners-path-editor-table',
'sigil' => 'paths',
),
'')))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($cancel_uri)
->setValue(pht('Save Paths')));
$box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Paths'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setForm($form);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(
$package->getName(),
$this->getApplicationURI('package/'.$package->getID().'/'));
$crumbs->addTextCrumb(pht('Edit Paths'));
$crumbs->setBorder(true);
$header = id(new PHUIHeaderView())
->setHeader(pht('Edit Paths: %s', $package->getName()))
->setHeaderIcon('fa-pencil');
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter($box);
$title = array($package->getName(), pht('Edit Paths'));
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
}
}
diff --git a/src/applications/owners/editor/PhabricatorOwnersPackageTransactionEditor.php b/src/applications/owners/editor/PhabricatorOwnersPackageTransactionEditor.php
index 40657abd5..c6aad6e2b 100644
--- a/src/applications/owners/editor/PhabricatorOwnersPackageTransactionEditor.php
+++ b/src/applications/owners/editor/PhabricatorOwnersPackageTransactionEditor.php
@@ -1,76 +1,74 @@
<?php
final class PhabricatorOwnersPackageTransactionEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorOwnersApplication';
}
public function getEditorObjectsDescription() {
return pht('Owners Packages');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
return $types;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function getMailSubjectPrefix() {
return PhabricatorEnv::getEnvConfig('metamta.package.subject-prefix');
}
protected function getMailTo(PhabricatorLiskDAO $object) {
return array(
$this->requireActor()->getPHID(),
);
}
protected function getMailCC(PhabricatorLiskDAO $object) {
return mpull($object->getOwners(), 'getUserPHID');
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new OwnersPackageReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
- $id = $object->getID();
$name = $object->getName();
return id(new PhabricatorMetaMTAMail())
- ->setSubject($name)
- ->addHeader('Thread-Topic', $object->getPHID());
+ ->setSubject($name);
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
$detail_uri = PhabricatorEnv::getProductionURI($object->getURI());
$body->addLinkSection(
pht('PACKAGE DETAIL'),
$detail_uri);
return $body;
}
protected function supportsSearch() {
return true;
}
}
diff --git a/src/applications/owners/engineextension/PhabricatorOwnersPathsSearchEngineAttachment.php b/src/applications/owners/engineextension/PhabricatorOwnersPathsSearchEngineAttachment.php
index ad5415ef6..19e699e8f 100644
--- a/src/applications/owners/engineextension/PhabricatorOwnersPathsSearchEngineAttachment.php
+++ b/src/applications/owners/engineextension/PhabricatorOwnersPathsSearchEngineAttachment.php
@@ -1,35 +1,35 @@
<?php
final class PhabricatorOwnersPathsSearchEngineAttachment
extends PhabricatorSearchEngineAttachment {
public function getAttachmentName() {
return pht('Included Paths');
}
public function getAttachmentDescription() {
return pht('Get the paths for each package.');
}
public function willLoadAttachmentData($query, $spec) {
$query->needPaths(true);
}
public function getAttachmentForObject($object, $data, $spec) {
$paths = $object->getPaths();
$list = array();
foreach ($paths as $path) {
$list[] = array(
'repositoryPHID' => $path->getRepositoryPHID(),
- 'path' => $path->getPath(),
+ 'path' => $path->getPathDisplay(),
'excluded' => (bool)$path->getExcluded(),
);
}
return array(
'paths' => $list,
);
}
}
diff --git a/src/applications/owners/phid/PhabricatorOwnersPackagePHIDType.php b/src/applications/owners/phid/PhabricatorOwnersPackagePHIDType.php
index cfbaf6eeb..fbff6a210 100644
--- a/src/applications/owners/phid/PhabricatorOwnersPackagePHIDType.php
+++ b/src/applications/owners/phid/PhabricatorOwnersPackagePHIDType.php
@@ -1,85 +1,86 @@
<?php
final class PhabricatorOwnersPackagePHIDType extends PhabricatorPHIDType {
const TYPECONST = 'OPKG';
public function getTypeName() {
return pht('Owners Package');
}
public function getTypeIcon() {
return 'fa-shopping-bag';
}
public function newObject() {
return new PhabricatorOwnersPackage();
}
public function getPHIDTypeApplicationClass() {
return 'PhabricatorOwnersApplication';
}
protected function buildQueryForObjects(
PhabricatorObjectQuery $query,
array $phids) {
return id(new PhabricatorOwnersPackageQuery())
->withPHIDs($phids);
}
public function loadHandles(
PhabricatorHandleQuery $query,
array $handles,
array $objects) {
foreach ($handles as $phid => $handle) {
$package = $objects[$phid];
$monogram = $package->getMonogram();
$name = $package->getName();
$id = $package->getID();
$uri = $package->getURI();
$handle
->setName($monogram)
->setFullName("{$monogram}: {$name}")
->setCommandLineObjectName("{$monogram} {$name}")
+ ->setMailStampName($monogram)
->setURI($uri);
if ($package->isArchived()) {
$handle->setStatus(PhabricatorObjectHandle::STATUS_CLOSED);
}
}
}
public function canLoadNamedObject($name) {
return preg_match('/^O\d*[1-9]\d*$/i', $name);
}
public function loadNamedObjects(
PhabricatorObjectQuery $query,
array $names) {
$id_map = array();
foreach ($names as $name) {
$id = (int)substr($name, 1);
$id_map[$id][] = $name;
}
$objects = id(new PhabricatorOwnersPackageQuery())
->setViewer($query->getViewer())
->withIDs(array_keys($id_map))
->execute();
$results = array();
foreach ($objects as $id => $object) {
foreach (idx($id_map, $id, array()) as $name) {
$results[$name] = $object;
}
}
return $results;
}
}
diff --git a/src/applications/owners/query/PhabricatorOwnersPackageQuery.php b/src/applications/owners/query/PhabricatorOwnersPackageQuery.php
index ce411aad3..ff0665b34 100644
--- a/src/applications/owners/query/PhabricatorOwnersPackageQuery.php
+++ b/src/applications/owners/query/PhabricatorOwnersPackageQuery.php
@@ -1,442 +1,452 @@
<?php
final class PhabricatorOwnersPackageQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $ownerPHIDs;
private $authorityPHIDs;
private $repositoryPHIDs;
private $paths;
private $statuses;
private $controlMap = array();
private $controlResults;
private $needPaths;
/**
* Query owner PHIDs exactly. This does not expand authorities, so a user
* PHID will not match projects the user is a member of.
*/
public function withOwnerPHIDs(array $phids) {
$this->ownerPHIDs = $phids;
return $this;
}
/**
* Query owner authority. This will expand authorities, so a user PHID will
* match both packages they own directly and packages owned by a project they
* are a member of.
*/
public function withAuthorityPHIDs(array $phids) {
$this->authorityPHIDs = $phids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withRepositoryPHIDs(array $phids) {
$this->repositoryPHIDs = $phids;
return $this;
}
public function withPaths(array $paths) {
$this->paths = $paths;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
public function withControl($repository_phid, array $paths) {
if (empty($this->controlMap[$repository_phid])) {
$this->controlMap[$repository_phid] = array();
}
foreach ($paths as $path) {
$path = (string)$path;
$this->controlMap[$repository_phid][$path] = $path;
}
// We need to load paths to execute control queries.
$this->needPaths = true;
return $this;
}
public function withNameNgrams($ngrams) {
return $this->withNgramsConstraint(
new PhabricatorOwnersPackageNameNgrams(),
$ngrams);
}
public function needPaths($need_paths) {
$this->needPaths = $need_paths;
return $this;
}
public function newResultObject() {
return new PhabricatorOwnersPackage();
}
protected function willExecute() {
$this->controlResults = array();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function willFilterPage(array $packages) {
$package_ids = mpull($packages, 'getID');
$owners = id(new PhabricatorOwnersOwner())->loadAllWhere(
'packageID IN (%Ld)',
$package_ids);
$owners = mgroup($owners, 'getPackageID');
foreach ($packages as $package) {
$package->attachOwners(idx($owners, $package->getID(), array()));
}
return $packages;
}
protected function didFilterPage(array $packages) {
$package_ids = mpull($packages, 'getID');
if ($this->needPaths) {
$paths = id(new PhabricatorOwnersPath())->loadAllWhere(
'packageID IN (%Ld)',
$package_ids);
$paths = mgroup($paths, 'getPackageID');
foreach ($packages as $package) {
$package->attachPaths(idx($paths, $package->getID(), array()));
}
}
if ($this->controlMap) {
foreach ($packages as $package) {
// If this package is archived, it's no longer a controlling package
// for any path. In particular, it can not force active packages with
// weak dominion to give up control.
if ($package->isArchived()) {
continue;
}
$this->controlResults[$package->getID()] = $package;
}
}
return $packages;
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = parent::buildJoinClauseParts($conn);
if ($this->shouldJoinOwnersTable()) {
$joins[] = qsprintf(
$conn,
'JOIN %T o ON o.packageID = p.id',
id(new PhabricatorOwnersOwner())->getTableName());
}
if ($this->shouldJoinPathTable()) {
$joins[] = qsprintf(
$conn,
'JOIN %T rpath ON rpath.packageID = p.id',
id(new PhabricatorOwnersPath())->getTableName());
}
return $joins;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'p.phid IN (%Ls)',
$this->phids);
}
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'p.id IN (%Ld)',
$this->ids);
}
if ($this->repositoryPHIDs !== null) {
$where[] = qsprintf(
$conn,
'rpath.repositoryPHID IN (%Ls)',
$this->repositoryPHIDs);
}
if ($this->authorityPHIDs !== null) {
$authority_phids = $this->expandAuthority($this->authorityPHIDs);
$where[] = qsprintf(
$conn,
'o.userPHID IN (%Ls)',
$authority_phids);
}
if ($this->ownerPHIDs !== null) {
$where[] = qsprintf(
$conn,
'o.userPHID IN (%Ls)',
$this->ownerPHIDs);
}
if ($this->paths !== null) {
$where[] = qsprintf(
$conn,
- 'rpath.path IN (%Ls)',
- $this->getFragmentsForPaths($this->paths));
+ 'rpath.pathIndex IN (%Ls)',
+ $this->getFragmentIndexesForPaths($this->paths));
}
if ($this->statuses !== null) {
$where[] = qsprintf(
$conn,
'p.status IN (%Ls)',
$this->statuses);
}
if ($this->controlMap) {
$clauses = array();
foreach ($this->controlMap as $repository_phid => $paths) {
- $fragments = $this->getFragmentsForPaths($paths);
+ $indexes = $this->getFragmentIndexesForPaths($paths);
$clauses[] = qsprintf(
$conn,
- '(rpath.repositoryPHID = %s AND rpath.path IN (%Ls))',
+ '(rpath.repositoryPHID = %s AND rpath.pathIndex IN (%Ls))',
$repository_phid,
- $fragments);
+ $indexes);
}
$where[] = implode(' OR ', $clauses);
}
return $where;
}
protected function shouldGroupQueryResultRows() {
if ($this->shouldJoinOwnersTable()) {
return true;
}
if ($this->shouldJoinPathTable()) {
return true;
}
return parent::shouldGroupQueryResultRows();
}
public function getBuiltinOrders() {
return array(
'name' => array(
'vector' => array('name'),
'name' => pht('Name'),
),
) + parent::getBuiltinOrders();
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'name' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'name',
'type' => 'string',
'unique' => true,
'reverse' => true,
),
);
}
protected function getPagingValueMap($cursor, array $keys) {
$package = $this->loadCursorObject($cursor);
return array(
'id' => $package->getID(),
'name' => $package->getName(),
);
}
public function getQueryApplicationClass() {
return 'PhabricatorOwnersApplication';
}
protected function getPrimaryTableAlias() {
return 'p';
}
private function shouldJoinOwnersTable() {
if ($this->ownerPHIDs !== null) {
return true;
}
if ($this->authorityPHIDs !== null) {
return true;
}
return false;
}
private function shouldJoinPathTable() {
if ($this->repositoryPHIDs !== null) {
return true;
}
if ($this->paths !== null) {
return true;
}
if ($this->controlMap) {
return true;
}
return false;
}
private function expandAuthority(array $phids) {
$projects = id(new PhabricatorProjectQuery())
->setViewer($this->getViewer())
->withMemberPHIDs($phids)
->execute();
$project_phids = mpull($projects, 'getPHID');
return array_fuse($phids) + array_fuse($project_phids);
}
private function getFragmentsForPaths(array $paths) {
$fragments = array();
foreach ($paths as $path) {
foreach (PhabricatorOwnersPackage::splitPath($path) as $fragment) {
$fragments[$fragment] = $fragment;
}
}
return $fragments;
}
+ private function getFragmentIndexesForPaths(array $paths) {
+ $indexes = array();
+
+ foreach ($this->getFragmentsForPaths($paths) as $fragment) {
+ $indexes[] = PhabricatorHash::digestForIndex($fragment);
+ }
+
+ return $indexes;
+ }
+
/* -( Path Control )------------------------------------------------------- */
/**
* Get a list of all packages which control a path or its parent directories,
* ordered from weakest to strongest.
*
* The first package has the most specific claim on the path; the last
* package has the most general claim. Multiple packages may have claims of
* equal strength, so this ordering is primarily one of usability and
* convenience.
*
* @return list<PhabricatorOwnersPackage> List of controlling packages.
*/
public function getControllingPackagesForPath(
$repository_phid,
$path,
$ignore_dominion = false) {
$path = (string)$path;
if (!isset($this->controlMap[$repository_phid][$path])) {
throw new PhutilInvalidStateException('withControl');
}
if ($this->controlResults === null) {
throw new PhutilInvalidStateException('execute');
}
$packages = $this->controlResults;
$weak_dominion = PhabricatorOwnersPackage::DOMINION_WEAK;
$path_fragments = PhabricatorOwnersPackage::splitPath($path);
$fragment_count = count($path_fragments);
$matches = array();
foreach ($packages as $package_id => $package) {
$best_match = null;
$include = false;
$repository_paths = $package->getPathsForRepository($repository_phid);
foreach ($repository_paths as $package_path) {
$strength = $package_path->getPathMatchStrength(
$path_fragments,
$fragment_count);
if ($strength > $best_match) {
$best_match = $strength;
$include = !$package_path->getExcluded();
}
}
if ($best_match && $include) {
if ($ignore_dominion) {
$is_weak = false;
} else {
$is_weak = ($package->getDominion() == $weak_dominion);
}
$matches[$package_id] = array(
'strength' => $best_match,
'weak' => $is_weak,
'package' => $package,
);
}
}
// At each strength level, drop weak packages if there are also strong
// packages of the same strength.
$strength_map = igroup($matches, 'strength');
foreach ($strength_map as $strength => $package_list) {
$any_strong = false;
foreach ($package_list as $package_id => $package) {
if (!$package['weak']) {
$any_strong = true;
break;
}
}
if ($any_strong) {
foreach ($package_list as $package_id => $package) {
if ($package['weak']) {
unset($matches[$package_id]);
}
}
}
}
$matches = isort($matches, 'strength');
$matches = array_reverse($matches);
$strongest = null;
foreach ($matches as $package_id => $match) {
if ($strongest === null) {
$strongest = $match['strength'];
}
if ($match['strength'] === $strongest) {
continue;
}
if ($match['weak']) {
unset($matches[$package_id]);
}
}
return array_values(ipull($matches, 'package'));
}
}
diff --git a/src/applications/owners/storage/PhabricatorOwnersPackage.php b/src/applications/owners/storage/PhabricatorOwnersPackage.php
index a09137c4d..c76864702 100644
--- a/src/applications/owners/storage/PhabricatorOwnersPackage.php
+++ b/src/applications/owners/storage/PhabricatorOwnersPackage.php
@@ -1,637 +1,715 @@
<?php
final class PhabricatorOwnersPackage
extends PhabricatorOwnersDAO
implements
PhabricatorPolicyInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorCustomFieldInterface,
PhabricatorDestructibleInterface,
PhabricatorConduitResultInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface,
PhabricatorNgramsInterface {
protected $name;
protected $auditingEnabled;
protected $autoReview;
protected $description;
- protected $primaryOwnerPHID;
protected $mailKey;
protected $status;
protected $viewPolicy;
protected $editPolicy;
protected $dominion;
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';
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_SUBSCRIBE => array(
- 'name' => pht('Subscribe to Changes'),
- ),
self::AUTOREVIEW_REVIEW => array(
- 'name' => pht('Review Changes'),
+ 'name' => pht('Review Changes With Non-Owner Author'),
+ 'authority' => true,
),
self::AUTOREVIEW_BLOCK => array(
- 'name' => pht('Review Changes (Blocking)'),
+ '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_COLUMN_SCHEMA => array(
'name' => 'sort',
'description' => 'text',
- 'primaryOwnerPHID' => 'phid?',
'auditingEnabled' => 'bool',
'mailKey' => 'bytes20',
'status' => 'text32',
'autoReview' => 'text32',
'dominion' => 'text32',
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorOwnersPackagePHIDType::TYPECONST);
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
public function isArchived() {
return ($this->getStatus() == self::STATUS_ARCHIVED);
}
public function setName($name) {
$this->name = $name;
return $this;
}
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 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), 128) as $chunk) {
+ 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.path IN (%Ls) AND pkg.status IN (%Ls) %Q',
+ WHERE p.pathIndex IN (%Ls) AND pkg.status IN (%Ls) %Q',
$package->getTableName(),
$path->getTableName(),
- $chunk,
+ $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) {
- $trailing_slash = preg_match('@/$@', $path) ? '/' : '';
- $path = trim($path, '/');
+ $result = array(
+ '/',
+ );
+
$parts = explode('/', $path);
+ $buffer = '/';
+ foreach ($parts as $part) {
+ if (!strlen($part)) {
+ continue;
+ }
- $result = array();
- while (count($parts)) {
- $result[] = '/'.implode('/', $parts).$trailing_slash;
- $trailing_slash = '/';
- array_pop($parts);
+ $buffer = $buffer.$part.'/';
+ $result[] = $buffer;
}
- $result[] = '/';
- return array_reverse($result);
+ 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.')),
);
}
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,
+ );
+
return array(
'name' => $this->getName(),
'description' => $this->getDescription(),
'status' => $this->getStatus(),
'owners' => $owner_list,
+ 'review' => $review,
+ 'audit' => $audit,
+ 'dominion' => $dominion,
);
}
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/storage/PhabricatorOwnersPath.php b/src/applications/owners/storage/PhabricatorOwnersPath.php
index f240db285..7022cb887 100644
--- a/src/applications/owners/storage/PhabricatorOwnersPath.php
+++ b/src/applications/owners/storage/PhabricatorOwnersPath.php
@@ -1,110 +1,125 @@
<?php
final class PhabricatorOwnersPath extends PhabricatorOwnersDAO {
protected $packageID;
protected $repositoryPHID;
+ protected $pathIndex;
protected $path;
+ protected $pathDisplay;
protected $excluded;
private $fragments;
private $fragmentCount;
protected function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_COLUMN_SCHEMA => array(
- 'path' => 'text255',
+ 'path' => 'text',
+ 'pathDisplay' => 'text',
+ 'pathIndex' => 'bytes12',
'excluded' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
- 'packageID' => array(
- 'columns' => array('packageID'),
+ 'key_path' => array(
+ 'columns' => array('packageID', 'repositoryPHID', 'pathIndex'),
+ 'unique' => true,
+ ),
+ 'key_repository' => array(
+ 'columns' => array('repositoryPHID', 'pathIndex'),
),
),
) + parent::getConfiguration();
}
-
public static function newFromRef(array $ref) {
$path = new PhabricatorOwnersPath();
$path->repositoryPHID = $ref['repositoryPHID'];
- $path->path = $ref['path'];
+
+ $raw_path = $ref['path'];
+
+ $path->pathIndex = PhabricatorHash::digestForIndex($raw_path);
+ $path->path = $raw_path;
+ $path->pathDisplay = $raw_path;
+
$path->excluded = $ref['excluded'];
+
return $path;
}
public function getRef() {
return array(
'repositoryPHID' => $this->getRepositoryPHID(),
'path' => $this->getPath(),
+ 'display' => $this->getPathDisplay(),
'excluded' => (int)$this->getExcluded(),
);
}
public static function getTransactionValueChanges(array $old, array $new) {
return array(
self::getTransactionValueDiff($old, $new),
self::getTransactionValueDiff($new, $old),
);
}
private static function getTransactionValueDiff(array $u, array $v) {
$set = self::getSetFromTransactionValue($v);
foreach ($u as $key => $ref) {
if (self::isRefInSet($ref, $set)) {
unset($u[$key]);
}
}
return $u;
}
public static function getSetFromTransactionValue(array $v) {
$set = array();
foreach ($v as $ref) {
$set[$ref['repositoryPHID']][$ref['path']][$ref['excluded']] = true;
}
return $set;
}
public static function isRefInSet(array $ref, array $set) {
return isset($set[$ref['repositoryPHID']][$ref['path']][$ref['excluded']]);
}
/**
* Get the number of directory matches between this path specification and
* some real path.
*/
public function getPathMatchStrength($path_fragments, $path_count) {
$this_path = $this->path;
if ($this_path === '/') {
// The root path "/" just matches everything with strength 1.
return 1;
}
if ($this->fragments === null) {
$this->fragments = PhabricatorOwnersPackage::splitPath($this_path);
$this->fragmentCount = count($this->fragments);
}
$self_fragments = $this->fragments;
$self_count = $this->fragmentCount;
if ($self_count > $path_count) {
// If this path is longer (and therefore more specific) than the target
// path, we don't match it at all.
return 0;
}
for ($ii = 0; $ii < $self_count; $ii++) {
if ($self_fragments[$ii] != $path_fragments[$ii]) {
return 0;
}
}
return $self_count;
}
}
diff --git a/src/applications/owners/xaction/PhabricatorOwnersPackagePathsTransaction.php b/src/applications/owners/xaction/PhabricatorOwnersPackagePathsTransaction.php
index 5952b78ae..984b26b8a 100644
--- a/src/applications/owners/xaction/PhabricatorOwnersPackagePathsTransaction.php
+++ b/src/applications/owners/xaction/PhabricatorOwnersPackagePathsTransaction.php
@@ -1,189 +1,221 @@
<?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();
+ 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])) {
+ unset($new[$key]);
+ continue;
+ }
+
+ $new[$key]['path'] = $raw_path;
+ $display_map[$raw_path] = $display_path;
+ }
+
$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/controller/PhabricatorPasteViewController.php b/src/applications/paste/controller/PhabricatorPasteViewController.php
index f29c93a16..9b4079fd4 100644
--- a/src/applications/paste/controller/PhabricatorPasteViewController.php
+++ b/src/applications/paste/controller/PhabricatorPasteViewController.php
@@ -1,186 +1,173 @@
<?php
final class PhabricatorPasteViewController extends PhabricatorPasteController {
- private $highlightMap;
-
public function shouldAllowPublic() {
return true;
}
- public function willProcessRequest(array $data) {
- $raw_lines = idx($data, 'lines');
- $map = array();
- if ($raw_lines) {
- $lines = explode('-', $raw_lines);
- $first = idx($lines, 0, 0);
- $last = idx($lines, 1);
- if ($last) {
- $min = min($first, $last);
- $max = max($first, $last);
- $map = array_fuse(range($min, $max));
- } else {
- $map[$first] = $first;
- }
- }
- $this->highlightMap = $map;
- }
-
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$paste = id(new PhabricatorPasteQuery())
->setViewer($viewer)
->withIDs(array($id))
->needContent(true)
->needRawContent(true)
->executeOne();
if (!$paste) {
return new Aphront404Response();
}
+ $lines = $request->getURILineRange('lines', 1000);
+ if ($lines) {
+ $map = range($lines[0], $lines[1]);
+ } else {
+ $map = array();
+ }
+
$header = $this->buildHeaderView($paste);
$curtain = $this->buildCurtain($paste);
$subheader = $this->buildSubheaderView($paste);
- $source_code = $this->buildSourceCodeView($paste, $this->highlightMap);
+ $source_code = $this->buildSourceCodeView($paste, $map);
require_celerity_resource('paste-css');
$monogram = $paste->getMonogram();
$crumbs = $this->buildApplicationCrumbs()
->addTextCrumb($monogram)
->setBorder(true);
$timeline = $this->buildTransactionTimeline(
$paste,
new PhabricatorPasteTransactionQuery());
$comment_view = id(new PhabricatorPasteEditEngine())
->setViewer($viewer)
->buildEditEngineCommentView($paste);
$timeline->setQuoteRef($monogram);
$comment_view->setTransactionTimeline($timeline);
$paste_view = id(new PHUITwoColumnView())
->setHeader($header)
->setSubheader($subheader)
->setMainColumn(array(
$source_code,
$timeline,
$comment_view,
))
->setCurtain($curtain)
->addClass('ponder-question-view');
return $this->newPage()
->setTitle($paste->getFullName())
->setCrumbs($crumbs)
->setPageObjectPHIDs(
array(
$paste->getPHID(),
))
->appendChild($paste_view);
}
private function buildHeaderView(PhabricatorPaste $paste) {
$title = (nonempty($paste->getTitle())) ?
$paste->getTitle() : pht('(An Untitled Masterwork)');
if ($paste->isArchived()) {
$header_icon = 'fa-ban';
$header_name = pht('Archived');
$header_color = 'dark';
} else {
$header_icon = 'fa-check';
$header_name = pht('Active');
$header_color = 'bluegrey';
}
$header = id(new PHUIHeaderView())
->setHeader($title)
->setUser($this->getRequest()->getUser())
->setStatus($header_icon, $header_color, $header_name)
->setPolicyObject($paste)
->setHeaderIcon('fa-clipboard');
return $header;
}
private function buildCurtain(PhabricatorPaste $paste) {
$viewer = $this->getViewer();
$curtain = $this->newCurtainView($paste);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$paste,
PhabricatorPolicyCapability::CAN_EDIT);
$id = $paste->getID();
$edit_uri = $this->getApplicationURI("edit/{$id}/");
$archive_uri = $this->getApplicationURI("archive/{$id}/");
$raw_uri = $this->getApplicationURI("raw/{$id}/");
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Paste'))
->setIcon('fa-pencil')
->setDisabled(!$can_edit)
->setHref($edit_uri));
if ($paste->isArchived()) {
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Activate Paste'))
->setIcon('fa-check')
->setDisabled(!$can_edit)
->setWorkflow($can_edit)
->setHref($archive_uri));
} else {
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Archive Paste'))
->setIcon('fa-ban')
->setDisabled(!$can_edit)
->setWorkflow($can_edit)
->setHref($archive_uri));
}
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('View Raw File'))
->setIcon('fa-file-text-o')
->setHref($raw_uri));
return $curtain;
}
private function buildSubheaderView(
PhabricatorPaste $paste) {
$viewer = $this->getViewer();
$author = $viewer->renderHandle($paste->getAuthorPHID())->render();
$date = phabricator_datetime($paste->getDateCreated(), $viewer);
$author = phutil_tag('strong', array(), $author);
$author_info = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withPHIDs(array($paste->getAuthorPHID()))
->needProfileImage(true)
->executeOne();
$image_uri = $author_info->getProfileImageURI();
$image_href = '/p/'.$author_info->getUsername();
$content = pht('Authored by %s on %s.', $author, $date);
return id(new PHUIHeadThingView())
->setImage($image_uri)
->setImageHref($image_href)
->setContent($content);
}
}
diff --git a/src/applications/paste/editor/PhabricatorPasteEditor.php b/src/applications/paste/editor/PhabricatorPasteEditor.php
index 063b72cfc..c31291572 100644
--- a/src/applications/paste/editor/PhabricatorPasteEditor.php
+++ b/src/applications/paste/editor/PhabricatorPasteEditor.php
@@ -1,98 +1,97 @@
<?php
final class PhabricatorPasteEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorPasteApplication';
}
public function getEditorObjectsDescription() {
return pht('Pastes');
}
public function getCreateObjectTitle($author, $object) {
return pht('%s created this paste.', $author);
}
public function getCreateObjectTitleForFeed($author, $object) {
return pht('%s created %s.', $author, $object);
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
$types[] = PhabricatorTransactions::TYPE_COMMENT;
return $types;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
if ($this->getIsNewObject()) {
return false;
}
return true;
}
protected function getMailSubjectPrefix() {
return PhabricatorEnv::getEnvConfig('metamta.paste.subject-prefix');
}
protected function getMailTo(PhabricatorLiskDAO $object) {
return array(
$object->getAuthorPHID(),
$this->getActingAsPHID(),
);
}
public function getMailTagsMap() {
return array(
PhabricatorPasteTransaction::MAILTAG_CONTENT =>
pht('Paste title, language or text changes.'),
PhabricatorPasteTransaction::MAILTAG_COMMENT =>
pht('Someone comments on a paste.'),
PhabricatorPasteTransaction::MAILTAG_OTHER =>
pht('Other paste activity not listed above occurs.'),
);
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new PasteReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
$name = $object->getTitle();
return id(new PhabricatorMetaMTAMail())
- ->setSubject("P{$id}: {$name}")
- ->addHeader('Thread-Topic', "P{$id}");
+ ->setSubject("P{$id}: {$name}");
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
$body->addLinkSection(
pht('PASTE DETAIL'),
PhabricatorEnv::getProductionURI('/P'.$object->getID()));
return $body;
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
}
diff --git a/src/applications/people/application/PhabricatorPeopleApplication.php b/src/applications/people/application/PhabricatorPeopleApplication.php
index 28405ca92..6322b29b2 100644
--- a/src/applications/people/application/PhabricatorPeopleApplication.php
+++ b/src/applications/people/application/PhabricatorPeopleApplication.php
@@ -1,109 +1,110 @@
<?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/(?:query/(?P<queryKey>[^/]+)/)?'
- => 'PhabricatorPeopleLogsController',
+ '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,
),
PeopleBrowseUserDirectoryCapability::CAPABILITY => array(),
);
}
public function getApplicationSearchDocumentTypes() {
return array(
PhabricatorPeopleUserPHIDType::TYPECONST,
);
}
}
diff --git a/src/applications/people/cache/PhabricatorUserProfileImageCacheType.php b/src/applications/people/cache/PhabricatorUserProfileImageCacheType.php
index e2a535cdd..8babff859 100644
--- a/src/applications/people/cache/PhabricatorUserProfileImageCacheType.php
+++ b/src/applications/people/cache/PhabricatorUserProfileImageCacheType.php
@@ -1,120 +1,109 @@
<?php
final class PhabricatorUserProfileImageCacheType
extends PhabricatorUserCacheType {
const CACHETYPE = 'user.profile';
const KEY_URI = 'user.profile.image.uri.v1';
public function getAutoloadKeys() {
return array(
self::KEY_URI,
);
}
public function canManageKey($key) {
return ($key === self::KEY_URI);
}
public function getDefaultValue() {
return PhabricatorUser::getDefaultProfileImageURI();
}
public function newValueForUsers($key, array $users) {
$viewer = $this->getViewer();
$file_phids = array();
$generate_users = array();
foreach ($users as $user) {
$user_phid = $user->getPHID();
$custom_phid = $user->getProfileImagePHID();
$default_phid = $user->getDefaultProfileImagePHID();
$version = $user->getDefaultProfileImageVersion();
if ($custom_phid) {
$file_phids[$user_phid] = $custom_phid;
continue;
}
if ($default_phid) {
if ($version == PhabricatorFilesComposeAvatarBuiltinFile::VERSION) {
$file_phids[$user_phid] = $default_phid;
continue;
}
}
$generate_users[] = $user;
}
- // Generate Files for anyone without a default
- foreach ($generate_users as $generate_user) {
- $generate_user_phid = $generate_user->getPHID();
- $generate_username = $generate_user->getUsername();
- $generate_version = PhabricatorFilesComposeAvatarBuiltinFile::VERSION;
- $generate_file = id(new PhabricatorFilesComposeAvatarBuiltinFile())
- ->getUserProfileImageFile($generate_username);
-
- $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
- $generate_user->setDefaultProfileImagePHID($generate_file->getPHID());
- $generate_user->setDefaultProfileImageVersion($generate_version);
- $generate_user->save();
- unset($unguarded);
-
- $file_phids[$generate_user_phid] = $generate_file->getPHID();
+ $generator = new PhabricatorFilesComposeAvatarBuiltinFile();
+ foreach ($generate_users as $user) {
+ $file = $generator->updateUser($user);
+ $file_phids[$user->getPHID()] = $file->getPHID();
}
if ($file_phids) {
$files = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs($file_phids)
->execute();
$files = mpull($files, null, 'getPHID');
} else {
$files = array();
}
$results = array();
foreach ($users as $user) {
$image_phid = $user->getProfileImagePHID();
$default_phid = $user->getDefaultProfileImagePHID();
if (isset($files[$image_phid])) {
$image_uri = $files[$image_phid]->getBestURI();
} else if (isset($files[$default_phid])) {
$image_uri = $files[$default_phid]->getBestURI();
} else {
$image_uri = PhabricatorUser::getDefaultProfileImageURI();
}
$user_phid = $user->getPHID();
$version = $this->getCacheVersion($user);
$results[$user_phid] = "{$version},{$image_uri}";
}
return $results;
}
public function getValueFromStorage($value) {
$parts = explode(',', $value, 2);
return end($parts);
}
public function shouldValidateRawCacheData() {
return true;
}
public function isRawCacheDataValid(PhabricatorUser $user, $key, $data) {
$parts = explode(',', $data, 2);
$version = reset($parts);
return ($version === $this->getCacheVersion($user));
}
private function getCacheVersion(PhabricatorUser $user) {
$parts = array(
PhabricatorEnv::getCDNURI('/'),
PhabricatorEnv::getEnvConfig('cluster.instance'),
$user->getProfileImagePHID(),
);
$parts = serialize($parts);
return PhabricatorHash::digestForIndex($parts);
}
}
diff --git a/src/applications/people/controller/PhabricatorPeopleProfileRevisionsController.php b/src/applications/people/controller/PhabricatorPeopleProfileRevisionsController.php
index adb2a60e5..55baf0140 100644
--- a/src/applications/people/controller/PhabricatorPeopleProfileRevisionsController.php
+++ b/src/applications/people/controller/PhabricatorPeopleProfileRevisionsController.php
@@ -1,82 +1,78 @@
<?php
final class PhabricatorPeopleProfileRevisionsController
extends PhabricatorPeopleProfileController {
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();
}
$class = 'PhabricatorDifferentialApplication';
if (!PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) {
return new Aphront404Response();
}
$this->setUser($user);
$title = array(pht('Recent Revisions'), $user->getUsername());
$header = $this->buildProfileHeader();
$commits = $this->buildRevisionsView($user);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Recent Revisions'));
$crumbs->setBorder(true);
$nav = $this->getProfileMenu();
$nav->selectFilter(PhabricatorPeopleProfileMenuEngine::ITEM_REVISIONS);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->addClass('project-view-home')
->addClass('project-view-people-home')
->setFooter(array(
$commits,
));
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->setNavigation($nav)
->appendChild($view);
}
private function buildRevisionsView(PhabricatorUser $user) {
$viewer = $this->getViewer();
$revisions = id(new DifferentialRevisionQuery())
->setViewer($viewer)
->withAuthors(array($user->getPHID()))
->needFlags(true)
->needDrafts(true)
->needReviewers(true)
->setLimit(100)
->execute();
$list = id(new DifferentialRevisionListView())
- ->setUser($viewer)
+ ->setViewer($viewer)
->setNoBox(true)
->setRevisions($revisions)
->setNoDataString(pht('No recent revisions.'));
- $object_phids = $list->getRequiredHandlePHIDs();
- $handles = $this->loadViewerHandles($object_phids);
- $list->setHandles($handles);
-
$view = id(new PHUIObjectBoxView())
->setHeaderText(pht('Recent Revisions'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($list);
return $view;
}
}
diff --git a/src/applications/people/engineextension/PeopleMainMenuBarExtension.php b/src/applications/people/engineextension/PeopleMainMenuBarExtension.php
index bed9dde44..152fb2bec 100644
--- a/src/applications/people/engineextension/PeopleMainMenuBarExtension.php
+++ b/src/applications/people/engineextension/PeopleMainMenuBarExtension.php
@@ -1,122 +1,121 @@
<?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);
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/')
- ->setColor(PhabricatorActionView::RED)
->setWorkflow(true));
return $view;
}
}
diff --git a/src/applications/people/engineextension/PhabricatorPeopleDatasourceEngineExtension.php b/src/applications/people/engineextension/PhabricatorPeopleDatasourceEngineExtension.php
new file mode 100644
index 000000000..dc8599554
--- /dev/null
+++ b/src/applications/people/engineextension/PhabricatorPeopleDatasourceEngineExtension.php
@@ -0,0 +1,36 @@
+<?php
+
+final class PhabricatorPeopleDatasourceEngineExtension
+ extends PhabricatorDatasourceEngineExtension {
+
+ public function newQuickSearchDatasources() {
+ return array(
+ new PhabricatorPeopleDatasource(),
+ );
+ }
+
+ public function newJumpURI($query) {
+ $viewer = $this->getViewer();
+
+ // Send "u" to the user directory.
+ if (preg_match('/^u\z/i', $query)) {
+ return '/people/';
+ }
+
+ // Send "u <string>" to the user's profile page.
+ $matches = null;
+ if (preg_match('/^u\s+(.+)\z/i', $query, $matches)) {
+ $raw_query = $matches[1];
+
+ // TODO: We could test that this is a valid username and jump to
+ // a search in the user directory if it isn't.
+
+ return urisprintf(
+ '/p/%s/',
+ $raw_query);
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/applications/people/engineextension/PhabricatorPeopleQuickSearchEngineExtension.php b/src/applications/people/engineextension/PhabricatorPeopleQuickSearchEngineExtension.php
deleted file mode 100644
index 565838de7..000000000
--- a/src/applications/people/engineextension/PhabricatorPeopleQuickSearchEngineExtension.php
+++ /dev/null
@@ -1,11 +0,0 @@
-<?php
-
-final class PhabricatorPeopleQuickSearchEngineExtension
- extends PhabricatorQuickSearchEngineExtension {
-
- public function newQuickSearchDatasources() {
- return array(
- new PhabricatorPeopleDatasource(),
- );
- }
-}
diff --git a/src/applications/people/management/PhabricatorPeopleProfileImageWorkflow.php b/src/applications/people/management/PhabricatorPeopleProfileImageWorkflow.php
index 358756cec..8bf3c8e11 100644
--- a/src/applications/people/management/PhabricatorPeopleProfileImageWorkflow.php
+++ b/src/applications/people/management/PhabricatorPeopleProfileImageWorkflow.php
@@ -1,87 +1,85 @@
<?php
final class PhabricatorPeopleProfileImageWorkflow
extends PhabricatorPeopleManagementWorkflow {
protected function didConstruct() {
$this
->setName('profileimage')
->setExamples('**profileimage** --users __username__')
->setSynopsis(pht('Generate default profile images.'))
->setArguments(
array(
array(
'name' => 'all',
'help' => pht(
'Generate default profile images for all users.'),
),
array(
'name' => 'force',
'short' => 'f',
'help' => pht(
'Force a default profile image to be replaced.'),
),
array(
'name' => 'users',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$is_force = $args->getArg('force');
$is_all = $args->getArg('all');
$gd = function_exists('imagecreatefromstring');
if (!$gd) {
throw new PhutilArgumentUsageException(
pht(
'GD is not installed for php-cli. Aborting.'));
}
$iterator = $this->buildIterator($args);
if (!$iterator) {
throw new PhutilArgumentUsageException(
pht(
'Either specify a list of users to update, or use `%s` '.
'to update all users.',
'--all'));
}
$version = PhabricatorFilesComposeAvatarBuiltinFile::VERSION;
+ $generator = new PhabricatorFilesComposeAvatarBuiltinFile();
foreach ($iterator as $user) {
$username = $user->getUsername();
$default_phid = $user->getDefaultProfileImagePHID();
$gen_version = $user->getDefaultProfileImageVersion();
$generate = false;
if ($gen_version != $version) {
$generate = true;
}
if ($default_phid == null || $is_force || $generate) {
- $file = id(new PhabricatorFilesComposeAvatarBuiltinFile())
- ->getUserProfileImageFile($username);
- $user->setDefaultProfileImagePHID($file->getPHID());
- $user->setDefaultProfileImageVersion($version);
- $user->save();
$console->writeOut(
"%s\n",
pht(
'Generating profile image for "%s".',
$username));
+
+ $generator->updateUser($user);
} else {
$console->writeOut(
"%s\n",
pht(
'Default profile image "%s" already set for "%s".',
$version,
$username));
}
}
}
}
diff --git a/src/applications/people/phid/PhabricatorPeopleUserPHIDType.php b/src/applications/people/phid/PhabricatorPeopleUserPHIDType.php
index f0512e91f..7867f098f 100644
--- a/src/applications/people/phid/PhabricatorPeopleUserPHIDType.php
+++ b/src/applications/people/phid/PhabricatorPeopleUserPHIDType.php
@@ -1,118 +1,121 @@
<?php
final class PhabricatorPeopleUserPHIDType extends PhabricatorPHIDType {
const TYPECONST = 'USER';
public function getTypeName() {
return pht('User');
}
public function getTypeIcon() {
return 'fa-user bluegrey';
}
public function newObject() {
return new PhabricatorUser();
}
public function getPHIDTypeApplicationClass() {
return 'PhabricatorPeopleApplication';
}
protected function buildQueryForObjects(
PhabricatorObjectQuery $query,
array $phids) {
return id(new PhabricatorPeopleQuery())
->withPHIDs($phids)
->needProfile(true)
->needProfileImage(true)
->needAvailability(true);
}
public function loadHandles(
PhabricatorHandleQuery $query,
array $handles,
array $objects) {
foreach ($handles as $phid => $handle) {
$user = $objects[$phid];
$realname = $user->getRealName();
-
- $handle->setName($user->getUsername());
- $handle->setURI('/p/'.$user->getUsername().'/');
- $handle->setFullName($user->getFullName());
- $handle->setImageURI($user->getProfileImageURI());
+ $username = $user->getUsername();
+
+ $handle
+ ->setName($username)
+ ->setURI('/p/'.$username.'/')
+ ->setFullName($user->getFullName())
+ ->setImageURI($user->getProfileImageURI())
+ ->setMailStampName('@'.$username);
if ($user->getIsMailingList()) {
$handle->setIcon('fa-envelope-o');
$handle->setSubtitle(pht('Mailing List'));
} else {
$profile = $user->getUserProfile();
$icon_key = $profile->getIcon();
$icon_icon = PhabricatorPeopleIconSet::getIconIcon($icon_key);
$subtitle = $profile->getDisplayTitle();
$handle
->setIcon($icon_icon)
->setSubtitle($subtitle)
->setTokenIcon('fa-user');
}
$availability = null;
if ($user->getIsDisabled()) {
$availability = PhabricatorObjectHandle::AVAILABILITY_DISABLED;
} else if (!$user->isResponsive()) {
$availability = PhabricatorObjectHandle::AVAILABILITY_NOEMAIL;
} else {
$until = $user->getAwayUntil();
if ($until) {
$away = PhabricatorCalendarEventInvitee::AVAILABILITY_AWAY;
if ($user->getDisplayAvailability() == $away) {
$availability = PhabricatorObjectHandle::AVAILABILITY_NONE;
} else {
$availability = PhabricatorObjectHandle::AVAILABILITY_PARTIAL;
}
}
}
if ($availability) {
$handle->setAvailability($availability);
}
}
}
public function canLoadNamedObject($name) {
return preg_match('/^@.+/', $name);
}
public function loadNamedObjects(
PhabricatorObjectQuery $query,
array $names) {
$id_map = array();
foreach ($names as $name) {
$id = substr($name, 1);
$id = phutil_utf8_strtolower($id);
$id_map[$id][] = $name;
}
$objects = id(new PhabricatorPeopleQuery())
->setViewer($query->getViewer())
->withUsernames(array_keys($id_map))
->execute();
$results = array();
foreach ($objects as $id => $object) {
$user_key = $object->getUsername();
$user_key = phutil_utf8_strtolower($user_key);
foreach (idx($id_map, $user_key, array()) as $name) {
$results[$name] = $object;
}
}
return $results;
}
}
diff --git a/src/applications/people/query/PhabricatorPeopleLogQuery.php b/src/applications/people/query/PhabricatorPeopleLogQuery.php
index 9bcdc53f4..fc6a87b33 100644
--- a/src/applications/people/query/PhabricatorPeopleLogQuery.php
+++ b/src/applications/people/query/PhabricatorPeopleLogQuery.php
@@ -1,113 +1,126 @@
<?php
final class PhabricatorPeopleLogQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $actorPHIDs;
private $userPHIDs;
private $relatedPHIDs;
private $sessionKeys;
private $actions;
private $remoteAddressPrefix;
+ private $dateCreatedMin;
+ private $dateCreatedMax;
public function withActorPHIDs(array $actor_phids) {
$this->actorPHIDs = $actor_phids;
return $this;
}
public function withUserPHIDs(array $user_phids) {
$this->userPHIDs = $user_phids;
return $this;
}
public function withRelatedPHIDs(array $related_phids) {
$this->relatedPHIDs = $related_phids;
return $this;
}
public function withSessionKeys(array $session_keys) {
$this->sessionKeys = $session_keys;
return $this;
}
public function withActions(array $actions) {
$this->actions = $actions;
return $this;
}
public function withRemoteAddressPrefix($remote_address_prefix) {
$this->remoteAddressPrefix = $remote_address_prefix;
return $this;
}
+ public function withDateCreatedBetween($min, $max) {
+ $this->dateCreatedMin = $min;
+ $this->dateCreatedMax = $max;
+ return $this;
+ }
+
+ public function newResultObject() {
+ return new PhabricatorUserLog();
+ }
+
protected function loadPage() {
- $table = new PhabricatorUserLog();
- $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->actorPHIDs !== null) {
$where[] = qsprintf(
- $conn_r,
+ $conn,
'actorPHID IN (%Ls)',
$this->actorPHIDs);
}
if ($this->userPHIDs !== null) {
$where[] = qsprintf(
- $conn_r,
+ $conn,
'userPHID IN (%Ls)',
$this->userPHIDs);
}
if ($this->relatedPHIDs !== null) {
$where[] = qsprintf(
- $conn_r,
- 'actorPHID IN (%Ls) OR userPHID IN (%Ls)',
+ $conn,
+ '(actorPHID IN (%Ls) OR userPHID IN (%Ls))',
$this->relatedPHIDs,
$this->relatedPHIDs);
}
if ($this->sessionKeys !== null) {
$where[] = qsprintf(
- $conn_r,
+ $conn,
'session IN (%Ls)',
$this->sessionKeys);
}
if ($this->actions !== null) {
$where[] = qsprintf(
- $conn_r,
+ $conn,
'action IN (%Ls)',
$this->actions);
}
if ($this->remoteAddressPrefix !== null) {
$where[] = qsprintf(
- $conn_r,
+ $conn,
'remoteAddr LIKE %>',
$this->remoteAddressPrefix);
}
- $where[] = $this->buildPagingClause($conn_r);
+ if ($this->dateCreatedMin !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'dateCreated >= %d',
+ $this->dateCreatedMin);
+ }
+
+ if ($this->dateCreatedMax !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'dateCreated <= %d',
+ $this->dateCreatedMax);
+ }
- return $this->formatWhereClause($where);
+ return $where;
}
public function getQueryApplicationClass() {
return 'PhabricatorPeopleApplication';
}
}
diff --git a/src/applications/people/query/PhabricatorPeopleLogSearchEngine.php b/src/applications/people/query/PhabricatorPeopleLogSearchEngine.php
index 851ef113d..b052456cd 100644
--- a/src/applications/people/query/PhabricatorPeopleLogSearchEngine.php
+++ b/src/applications/people/query/PhabricatorPeopleLogSearchEngine.php
@@ -1,194 +1,241 @@
<?php
final class PhabricatorPeopleLogSearchEngine
extends PhabricatorApplicationSearchEngine {
public function getResultTypeDescription() {
return pht('Account Activity');
}
public function getApplicationClassName() {
return 'PhabricatorPeopleApplication';
}
public function getPageSize(PhabricatorSavedQuery $saved) {
return 500;
}
- public function buildSavedQueryFromRequest(AphrontRequest $request) {
- $saved = new PhabricatorSavedQuery();
-
- $saved->setParameter(
- 'userPHIDs',
- $this->readUsersFromRequest($request, 'users'));
-
- $saved->setParameter(
- 'actorPHIDs',
- $this->readUsersFromRequest($request, 'actors'));
-
- $saved->setParameter(
- 'actions',
- $this->readListFromRequest($request, 'actions'));
-
- $saved->setParameter(
- 'ip',
- $request->getStr('ip'));
-
- $saved->setParameter(
- 'sessions',
- $this->readListFromRequest($request, 'sessions'));
-
- return $saved;
- }
-
- public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
- $query = id(new PhabricatorPeopleLogQuery());
+ public function newQuery() {
+ $query = new PhabricatorPeopleLogQuery();
// NOTE: If the viewer isn't an administrator, always restrict the query to
// related records. This echoes the policy logic of these logs. This is
// mostly a performance optimization, to prevent us from having to pull
// large numbers of logs that the user will not be able to see and filter
// them in-process.
$viewer = $this->requireViewer();
if (!$viewer->getIsAdmin()) {
$query->withRelatedPHIDs(array($viewer->getPHID()));
}
- $actor_phids = $saved->getParameter('actorPHIDs', array());
- if ($actor_phids) {
- $query->withActorPHIDs($actor_phids);
- }
+ return $query;
+ }
+
+ protected function buildQueryFromParameters(array $map) {
+ $query = $this->newQuery();
- $user_phids = $saved->getParameter('userPHIDs', array());
- if ($user_phids) {
- $query->withUserPHIDs($user_phids);
+ if ($map['userPHIDs']) {
+ $query->withUserPHIDs($map['userPHIDs']);
}
- $actions = $saved->getParameter('actions', array());
- if ($actions) {
- $query->withActions($actions);
+ if ($map['actorPHIDs']) {
+ $query->withActorPHIDs($map['actorPHIDs']);
}
- $remote_prefix = $saved->getParameter('ip');
- if (strlen($remote_prefix)) {
- $query->withRemoteAddressprefix($remote_prefix);
+ if ($map['actions']) {
+ $query->withActions($map['actions']);
}
- $sessions = $saved->getParameter('sessions', array());
- if ($sessions) {
- $query->withSessionKeys($sessions);
+ if (strlen($map['ip'])) {
+ $query->withRemoteAddressPrefix($map['ip']);
}
- return $query;
- }
+ if ($map['sessions']) {
+ $query->withSessionKeys($map['sessions']);
+ }
- public function buildSearchForm(
- AphrontFormView $form,
- PhabricatorSavedQuery $saved) {
-
- $actor_phids = $saved->getParameter('actorPHIDs', array());
- $user_phids = $saved->getParameter('userPHIDs', array());
-
- $actions = $saved->getParameter('actions', array());
- $remote_prefix = $saved->getParameter('ip');
- $sessions = $saved->getParameter('sessions', array());
-
- $actions = array_fuse($actions);
- $action_control = id(new AphrontFormCheckboxControl())
- ->setLabel(pht('Actions'));
- $action_types = PhabricatorUserLog::getActionTypeMap();
- foreach ($action_types as $type => $label) {
- $action_control->addCheckbox(
- 'actions[]',
- $type,
- $label,
- isset($actions[$label]));
+ if ($map['createdStart'] || $map['createdEnd']) {
+ $query->withDateCreatedBetween(
+ $map['createdStart'],
+ $map['createdEnd']);
}
- $form
- ->appendControl(
- id(new AphrontFormTokenizerControl())
- ->setDatasource(new PhabricatorPeopleDatasource())
- ->setName('actors')
- ->setLabel(pht('Actors'))
- ->setValue($actor_phids))
- ->appendControl(
- id(new AphrontFormTokenizerControl())
- ->setDatasource(new PhabricatorPeopleDatasource())
- ->setName('users')
- ->setLabel(pht('Users'))
- ->setValue($user_phids))
- ->appendChild($action_control)
- ->appendChild(
- id(new AphrontFormTextControl())
- ->setLabel(pht('Filter IP'))
- ->setName('ip')
- ->setValue($remote_prefix))
- ->appendChild(
- id(new AphrontFormTextControl())
- ->setLabel(pht('Sessions'))
- ->setName('sessions')
- ->setValue(implode(', ', $sessions)));
+ return $query;
+ }
+ protected function buildCustomSearchFields() {
+ return array(
+ id(new PhabricatorUsersSearchField())
+ ->setKey('userPHIDs')
+ ->setAliases(array('users', 'user', 'userPHID'))
+ ->setLabel(pht('Users'))
+ ->setDescription(pht('Search for activity affecting specific users.')),
+ id(new PhabricatorUsersSearchField())
+ ->setKey('actorPHIDs')
+ ->setAliases(array('actors', 'actor', 'actorPHID'))
+ ->setLabel(pht('Actors'))
+ ->setDescription(pht('Search for activity by specific users.')),
+ id(new PhabricatorSearchCheckboxesField())
+ ->setKey('actions')
+ ->setLabel(pht('Actions'))
+ ->setDescription(pht('Search for particular types of activity.'))
+ ->setOptions(PhabricatorUserLog::getActionTypeMap()),
+ id(new PhabricatorSearchTextField())
+ ->setKey('ip')
+ ->setLabel(pht('Filter IP'))
+ ->setDescription(pht('Search for actions by remote address.')),
+ id(new PhabricatorSearchStringListField())
+ ->setKey('sessions')
+ ->setLabel(pht('Sessions'))
+ ->setDescription(pht('Search for activity in particular sessions.')),
+ id(new PhabricatorSearchDateField())
+ ->setLabel(pht('Created After'))
+ ->setKey('createdStart'),
+ id(new PhabricatorSearchDateField())
+ ->setLabel(pht('Created Before'))
+ ->setKey('createdEnd'),
+ );
}
protected function getURI($path) {
return '/people/logs/'.$path;
}
protected function getBuiltinQueryNames() {
$names = array(
'all' => pht('All'),
);
return $names;
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
switch ($query_key) {
case 'all':
return $query;
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
- protected function getRequiredHandlePHIDsForResultList(
- array $logs,
- PhabricatorSavedQuery $query) {
-
- $phids = array();
- foreach ($logs as $log) {
- $phids[$log->getActorPHID()] = true;
- $phids[$log->getUserPHID()] = true;
- }
-
- return array_keys($phids);
- }
-
protected function renderResultList(
array $logs,
PhabricatorSavedQuery $query,
array $handles) {
assert_instances_of($logs, 'PhabricatorUserLog');
$viewer = $this->requireViewer();
$table = id(new PhabricatorUserLogView())
->setUser($viewer)
- ->setLogs($logs)
- ->setHandles($handles);
+ ->setLogs($logs);
if ($viewer->getIsAdmin()) {
$table->setSearchBaseURI($this->getApplicationURI('logs/'));
}
- $result = new PhabricatorApplicationSearchResultView();
- $result->setTable($table);
+ return id(new PhabricatorApplicationSearchResultView())
+ ->setTable($table);
+ }
+
+ protected function newExportFields() {
+ $viewer = $this->requireViewer();
+
+ $fields = array(
+ $fields[] = id(new PhabricatorPHIDExportField())
+ ->setKey('actorPHID')
+ ->setLabel(pht('Actor PHID')),
+ $fields[] = id(new PhabricatorStringExportField())
+ ->setKey('actor')
+ ->setLabel(pht('Actor')),
+ $fields[] = id(new PhabricatorPHIDExportField())
+ ->setKey('userPHID')
+ ->setLabel(pht('User PHID')),
+ $fields[] = id(new PhabricatorStringExportField())
+ ->setKey('user')
+ ->setLabel(pht('User')),
+ $fields[] = id(new PhabricatorStringExportField())
+ ->setKey('action')
+ ->setLabel(pht('Action')),
+ $fields[] = id(new PhabricatorStringExportField())
+ ->setKey('actionName')
+ ->setLabel(pht('Action Name')),
+ $fields[] = id(new PhabricatorStringExportField())
+ ->setKey('session')
+ ->setLabel(pht('Session')),
+ $fields[] = id(new PhabricatorStringExportField())
+ ->setKey('old')
+ ->setLabel(pht('Old Value')),
+ $fields[] = id(new PhabricatorStringExportField())
+ ->setKey('new')
+ ->setLabel(pht('New Value')),
+ );
+
+ 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->getUserPHID();
+ $phids[] = $log->getActorPHID();
+ }
+ $handles = $viewer->loadHandles($phids);
+
+ $action_map = PhabricatorUserLog::getActionTypeMap();
+
+ $export = array();
+ foreach ($logs as $log) {
+
+ $user_phid = $log->getUserPHID();
+ if ($user_phid) {
+ $user_name = $handles[$user_phid]->getName();
+ } else {
+ $user_name = null;
+ }
+
+ $actor_phid = $log->getActorPHID();
+ if ($actor_phid) {
+ $actor_name = $handles[$actor_phid]->getName();
+ } else {
+ $actor_name = null;
+ }
+
+ $action = $log->getAction();
+ $action_name = idx($action_map, $action, pht('Unknown ("%s")', $action));
+
+ $map = array(
+ 'actorPHID' => $actor_phid,
+ 'actor' => $actor_name,
+ 'userPHID' => $user_phid,
+ 'user' => $user_name,
+ 'action' => $action,
+ 'actionName' => $action_name,
+ 'session' => substr($log->getSession(), 0, 6),
+ 'old' => $log->getOldValue(),
+ 'new' => $log->getNewValue(),
+ );
+
+ if ($viewer->getIsAdmin()) {
+ $map['remoteAddress'] = $log->getRemoteAddr();
+ }
+
+ $export[] = $map;
+ }
- return $result;
+ return $export;
}
+
}
diff --git a/src/applications/people/query/PhabricatorPeopleSearchEngine.php b/src/applications/people/query/PhabricatorPeopleSearchEngine.php
index 5192b3f07..9969c6c59 100644
--- a/src/applications/people/query/PhabricatorPeopleSearchEngine.php
+++ b/src/applications/people/query/PhabricatorPeopleSearchEngine.php
@@ -1,370 +1,358 @@
<?php
final class PhabricatorPeopleSearchEngine
extends PhabricatorApplicationSearchEngine {
public function getResultTypeDescription() {
return pht('Users');
}
public function getApplicationClassName() {
return 'PhabricatorPeopleApplication';
}
public function newQuery() {
return id(new PhabricatorPeopleQuery())
->needPrimaryEmail(true)
->needProfileImage(true);
}
protected function buildCustomSearchFields() {
$fields = array(
id(new PhabricatorSearchStringListField())
->setLabel(pht('Usernames'))
->setKey('usernames')
->setAliases(array('username'))
->setDescription(pht('Find users by exact username.')),
id(new PhabricatorSearchTextField())
->setLabel(pht('Name Contains'))
->setKey('nameLike')
->setDescription(
pht('Find users whose usernames contain a substring.')),
id(new PhabricatorSearchThreeStateField())
->setLabel(pht('Administrators'))
->setKey('isAdmin')
->setOptions(
pht('(Show All)'),
pht('Show Only Administrators'),
pht('Hide Administrators'))
->setDescription(
pht(
'Pass true to find only administrators, or false to omit '.
'administrators.')),
id(new PhabricatorSearchThreeStateField())
->setLabel(pht('Disabled'))
->setKey('isDisabled')
->setOptions(
pht('(Show All)'),
pht('Show Only Disabled Users'),
pht('Hide Disabled Users'))
->setDescription(
pht(
'Pass true to find only disabled users, or false to omit '.
'disabled users.')),
id(new PhabricatorSearchThreeStateField())
->setLabel(pht('Bots'))
->setKey('isBot')
->setAliases(array('isSystemAgent'))
->setOptions(
pht('(Show All)'),
pht('Show Only Bots'),
pht('Hide Bots'))
->setDescription(
pht(
'Pass true to find only bots, or false to omit bots.')),
id(new PhabricatorSearchThreeStateField())
->setLabel(pht('Mailing Lists'))
->setKey('isMailingList')
->setOptions(
pht('(Show All)'),
pht('Show Only Mailing Lists'),
pht('Hide Mailing Lists'))
->setDescription(
pht(
'Pass true to find only mailing lists, or false to omit '.
'mailing lists.')),
id(new PhabricatorSearchThreeStateField())
->setLabel(pht('Needs Approval'))
->setKey('needsApproval')
->setOptions(
pht('(Show All)'),
pht('Show Only Unapproved Users'),
pht('Hide Unapproved Users'))
->setDescription(
pht(
'Pass true to find only users awaiting administrative approval, '.
'or false to omit these users.')),
// c4science customization
id(new PhabricatorSearchDatasourceField())
->setDatasource(new PhabricatorProjectDatasource())
->setLabel(pht('Member of Projects'))
->setKey('memberOf'),
);
$viewer = $this->requireViewer();
if ($viewer->getIsAdmin()) {
$fields[] = id(new PhabricatorSearchThreeStateField())
->setLabel(pht('Has MFA'))
->setKey('mfa')
->setOptions(
pht('(Show All)'),
pht('Show Only Users With MFA'),
pht('Hide Users With MFA'))
->setDescription(
pht(
'Pass true to find only users who are enrolled in MFA, or false '.
'to omit these users.'));
}
$fields[] = id(new PhabricatorSearchDateField())
->setKey('createdStart')
->setLabel(pht('Joined After'))
->setDescription(
pht('Find user accounts created after a given time.'));
$fields[] = id(new PhabricatorSearchDateField())
->setKey('createdEnd')
->setLabel(pht('Joined Before'))
->setDescription(
pht('Find user accounts created before a given time.'));
return $fields;
}
protected function getDefaultFieldOrder() {
return array(
'...',
'createdStart',
'createdEnd',
);
}
protected function buildQueryFromParameters(array $map) {
$query = $this->newQuery();
$viewer = $this->requireViewer();
// If the viewer can't browse the user directory, restrict the query to
// just the user's own profile. This is a little bit silly, but serves to
// restrict users from creating a dashboard panel which essentially just
// contains a user directory anyway.
$can_browse = PhabricatorPolicyFilter::hasCapability(
$viewer,
$this->getApplication(),
PeopleBrowseUserDirectoryCapability::CAPABILITY);
if (!$can_browse) {
$query->withPHIDs(array($viewer->getPHID()));
}
if ($map['usernames']) {
$query->withUsernames($map['usernames']);
}
if ($map['nameLike']) {
$query->withNameLike($map['nameLike']);
}
if ($map['isAdmin'] !== null) {
$query->withIsAdmin($map['isAdmin']);
}
if ($map['isDisabled'] !== null) {
$query->withIsDisabled($map['isDisabled']);
}
if ($map['isMailingList'] !== null) {
$query->withIsMailingList($map['isMailingList']);
}
if ($map['isBot'] !== null) {
$query->withIsSystemAgent($map['isBot']);
}
if ($map['needsApproval'] !== null) {
$query->withIsApproved(!$map['needsApproval']);
}
if (idx($map, 'mfa') !== null) {
$viewer = $this->requireViewer();
if (!$viewer->getIsAdmin()) {
throw new PhabricatorSearchConstraintException(
pht(
'The "Has MFA" query constraint may only be used by '.
'administrators, to prevent attackers from using it to target '.
'weak accounts.'));
}
$query->withIsEnrolledInMultiFactor($map['mfa']);
}
if ($map['createdStart']) {
$query->withDateCreatedAfter($map['createdStart']);
}
if ($map['createdEnd']) {
$query->withDateCreatedBefore($map['createdEnd']);
}
// c4science customization
if ($map['memberOf']) {
$query->withMemberOf($map['memberOf']);
}
return $query;
}
protected function getURI($path) {
return '/people/'.$path;
}
protected function getBuiltinQueryNames() {
$names = array(
'active' => pht('Active'),
'all' => pht('All'),
);
$viewer = $this->requireViewer();
if ($viewer->getIsAdmin()) {
$names['approval'] = pht('Approval Queue');
}
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('isDisabled', false);
case 'approval':
return $query
->setParameter('needsApproval', true)
->setParameter('isDisabled', false);
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
protected function renderResultList(
array $users,
PhabricatorSavedQuery $query,
array $handles) {
assert_instances_of($users, 'PhabricatorUser');
$request = $this->getRequest();
$viewer = $this->requireViewer();
$list = new PHUIObjectItemListView();
$is_approval = ($query->getQueryKey() == 'approval');
foreach ($users as $user) {
$primary_email = $user->loadPrimaryEmail();
if ($primary_email && $primary_email->getIsVerified()) {
$email = pht('Verified');
} else {
$email = pht('Unverified');
}
$item = new PHUIObjectItemView();
$item->setHeader($user->getFullName())
->setHref('/p/'.$user->getUsername().'/')
->addAttribute(phabricator_datetime($user->getDateCreated(), $viewer))
->addAttribute($email)
->setImageURI($user->getProfileImageURI());
if ($is_approval && $primary_email) {
$item->addAttribute($primary_email->getAddress());
}
if ($user->getIsDisabled()) {
$item->addIcon('fa-ban', pht('Disabled'));
$item->setDisabled(true);
}
if (!$is_approval) {
if (!$user->getIsApproved()) {
$item->addIcon('fa-clock-o', pht('Needs Approval'));
}
}
if ($user->getIsAdmin()) {
$item->addIcon('fa-star', pht('Admin'));
}
if ($user->getIsSystemAgent()) {
$item->addIcon('fa-desktop', pht('Bot'));
}
if ($user->getIsMailingList()) {
$item->addIcon('fa-envelope-o', pht('Mailing List'));
}
if ($viewer->getIsAdmin()) {
if ($user->getIsEnrolledInMultiFactor()) {
$item->addIcon('fa-lock', pht('Has MFA'));
}
}
if ($viewer->getIsAdmin()) {
$user_id = $user->getID();
if ($is_approval) {
$item->addAction(
id(new PHUIListItemView())
->setIcon('fa-ban')
->setName(pht('Disable'))
->setWorkflow(true)
->setHref($this->getApplicationURI('disapprove/'.$user_id.'/')));
$item->addAction(
id(new PHUIListItemView())
->setIcon('fa-thumbs-o-up')
->setName(pht('Approve'))
->setWorkflow(true)
->setHref($this->getApplicationURI('approve/'.$user_id.'/')));
}
}
$list->addItem($item);
}
$result = new PhabricatorApplicationSearchResultView();
$result->setObjectList($list);
$result->setNoDataString(pht('No accounts found.'));
return $result;
}
protected function newExportFields() {
return array(
- id(new PhabricatorIDExportField())
- ->setKey('id')
- ->setLabel(pht('ID')),
- id(new PhabricatorPHIDExportField())
- ->setKey('phid')
- ->setLabel(pht('PHID')),
id(new PhabricatorStringExportField())
->setKey('username')
->setLabel(pht('Username')),
id(new PhabricatorStringExportField())
->setKey('realName')
->setLabel(pht('Real Name')),
- id(new PhabricatorEpochExportField())
- ->setKey('created')
- ->setLabel(pht('Date Created')),
);
}
- public function newExport(array $users) {
+ protected function newExportData(array $users) {
$viewer = $this->requireViewer();
$export = array();
foreach ($users as $user) {
$export[] = array(
- 'id' => $user->getID(),
- 'phid' => $user->getPHID(),
'username' => $user->getUsername(),
'realName' => $user->getRealName(),
- 'created' => $user->getDateCreated(),
);
}
return $export;
}
}
diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php
index e64fe2409..4ce944d90 100644
--- a/src/applications/people/storage/PhabricatorUser.php
+++ b/src/applications/people/storage/PhabricatorUser.php
@@ -1,1665 +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 = parent::save();
+ $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();
}
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/view/PhabricatorUserLogView.php b/src/applications/people/view/PhabricatorUserLogView.php
index d648b248f..72a9378a1 100644
--- a/src/applications/people/view/PhabricatorUserLogView.php
+++ b/src/applications/people/view/PhabricatorUserLogView.php
@@ -1,88 +1,121 @@
<?php
final class PhabricatorUserLogView extends AphrontView {
private $logs;
- private $handles;
private $searchBaseURI;
public function setSearchBaseURI($search_base_uri) {
$this->searchBaseURI = $search_base_uri;
return $this;
}
public function setLogs(array $logs) {
assert_instances_of($logs, 'PhabricatorUserLog');
$this->logs = $logs;
return $this;
}
- public function setHandles(array $handles) {
- assert_instances_of($handles, 'PhabricatorObjectHandle');
- $this->handles = $handles;
- return $this;
- }
-
public function render() {
$logs = $this->logs;
- $handles = $this->handles;
$viewer = $this->getUser();
+ $phids = array();
+ foreach ($logs as $log) {
+ $phids[] = $log->getActorPHID();
+ $phids[] = $log->getUserPHID();
+ }
+ $handles = $viewer->loadHandles($phids);
+
$action_map = PhabricatorUserLog::getActionTypeMap();
$base_uri = $this->searchBaseURI;
+ $viewer_phid = $viewer->getPHID();
+
$rows = array();
foreach ($logs as $log) {
- $ip = $log->getRemoteAddr();
$session = substr($log->getSession(), 0, 6);
- if ($base_uri) {
- $ip = phutil_tag(
- 'a',
- array(
- 'href' => $base_uri.'?ip='.$ip.'#R',
- ),
- $ip);
+ $actor_phid = $log->getActorPHID();
+ $user_phid = $log->getUserPHID();
+
+ if ($viewer->getIsAdmin()) {
+ $can_see_ip = true;
+ } else if ($viewer_phid == $actor_phid) {
+ // You can see the address if you took the action.
+ $can_see_ip = true;
+ } else if (!$actor_phid && ($viewer_phid == $user_phid)) {
+ // You can see the address if it wasn't authenticated and applied
+ // to you (partial login).
+ $can_see_ip = true;
+ } else {
+ // You can't see the address when an administrator disables your
+ // account, since it's their address.
+ $can_see_ip = false;
+ }
+
+ if ($can_see_ip) {
+ $ip = $log->getRemoteAddr();
+ if ($base_uri) {
+ $ip = phutil_tag(
+ 'a',
+ array(
+ 'href' => $base_uri.'?ip='.$ip.'#R',
+ ),
+ $ip);
+ }
+ } else {
+ $ip = null;
}
$action = $log->getAction();
$action_name = idx($action_map, $action, $action);
+ if ($actor_phid) {
+ $actor_name = $handles[$actor_phid]->renderLink();
+ } else {
+ $actor_name = null;
+ }
+
+ if ($user_phid) {
+ $user_name = $handles[$user_phid]->renderLink();
+ } else {
+ $user_name = null;
+ }
+
$rows[] = array(
phabricator_date($log->getDateCreated(), $viewer),
phabricator_time($log->getDateCreated(), $viewer),
$action_name,
- $log->getActorPHID()
- ? $handles[$log->getActorPHID()]->getName()
- : null,
- $username = $handles[$log->getUserPHID()]->renderLink(),
+ $actor_name,
+ $user_name,
$ip,
$session,
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
pht('Date'),
pht('Time'),
pht('Action'),
pht('Actor'),
pht('User'),
pht('IP'),
pht('Session'),
));
$table->setColumnClasses(
array(
'',
'right',
'wide',
'',
'',
'',
'n',
));
return $table;
}
}
diff --git a/src/applications/phame/editor/PhameBlogEditor.php b/src/applications/phame/editor/PhameBlogEditor.php
index f30a74065..c122d8fa3 100644
--- a/src/applications/phame/editor/PhameBlogEditor.php
+++ b/src/applications/phame/editor/PhameBlogEditor.php
@@ -1,111 +1,109 @@
<?php
final class PhameBlogEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorPhameApplication';
}
public function getEditorObjectsDescription() {
return pht('Phame Blogs');
}
public function getCreateObjectTitle($author, $object) {
return pht('%s created this blog.', $author);
}
public function getCreateObjectTitleForFeed($author, $object) {
return pht('%s created %s.', $author, $object);
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
return $types;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function getMailTo(PhabricatorLiskDAO $object) {
$phids = array();
$phids[] = $this->requireActor()->getPHID();
$phids[] = $object->getCreatorPHID();
return $phids;
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
- $phid = $object->getPHID();
$name = $object->getName();
return id(new PhabricatorMetaMTAMail())
- ->setSubject($name)
- ->addHeader('Thread-Topic', $phid);
+ ->setSubject($name);
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new PhameBlogReplyHandler())
->setMailReceiver($object);
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
$body->addLinkSection(
pht('BLOG DETAIL'),
PhabricatorEnv::getProductionURI($object->getViewURI()));
return $body;
}
public function getMailTagsMap() {
return array(
PhameBlogTransaction::MAILTAG_DETAILS =>
pht("A blog's details change."),
PhameBlogTransaction::MAILTAG_SUBSCRIBERS =>
pht("A blog's subscribers change."),
PhameBlogTransaction::MAILTAG_OTHER =>
pht('Other blog activity not listed above occurs.'),
);
}
protected function getMailSubjectPrefix() {
return '[Phame]';
}
protected function supportsSearch() {
return true;
}
protected function shouldApplyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
return id(new HeraldPhameBlogAdapter())
->setBlog($object);
}
}
diff --git a/src/applications/phame/editor/PhamePostEditor.php b/src/applications/phame/editor/PhamePostEditor.php
index 488d7a493..d95389e54 100644
--- a/src/applications/phame/editor/PhamePostEditor.php
+++ b/src/applications/phame/editor/PhamePostEditor.php
@@ -1,142 +1,140 @@
<?php
final class PhamePostEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorPhameApplication';
}
public function getEditorObjectsDescription() {
return pht('Phame Posts');
}
public function getCreateObjectTitle($author, $object) {
return pht('%s created this post.', $author);
}
public function getCreateObjectTitleForFeed($author, $object) {
return pht('%s created %s.', $author, $object);
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
return $types;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
if ($object->isDraft() || ($object->isArchived())) {
return false;
}
return true;
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
if ($object->isDraft() || $object->isArchived()) {
return false;
}
return true;
}
protected function getMailTo(PhabricatorLiskDAO $object) {
$phids = array();
$phids[] = $object->getBloggerPHID();
$phids[] = $this->requireActor()->getPHID();
$blog_phid = $object->getBlogPHID();
if ($blog_phid) {
$cc_phids = PhabricatorSubscribersQuery::loadSubscribersForPHID(
$blog_phid);
foreach ($cc_phids as $cc) {
$phids[] = $cc;
}
}
return $phids;
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
- $phid = $object->getPHID();
$title = $object->getTitle();
return id(new PhabricatorMetaMTAMail())
- ->setSubject($title)
- ->addHeader('Thread-Topic', $phid);
+ ->setSubject($title);
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new PhamePostReplyHandler())
->setMailReceiver($object);
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
// We don't send mail if the object is a draft, and we only want
// to include the full body of the post on the either the
// first creation or if it was created as a draft, once it goes live.
if ($this->getIsNewObject()) {
$body->addRemarkupSection(null, $object->getBody());
} else {
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhamePostVisibilityTransaction::TRANSACTIONTYPE:
if (!$object->isDraft() && !$object->isArchived()) {
$body->addRemarkupSection(null, $object->getBody());
}
break;
}
}
}
$body->addLinkSection(
pht('POST DETAIL'),
PhabricatorEnv::getProductionURI($object->getViewURI()));
return $body;
}
public function getMailTagsMap() {
return array(
PhamePostTransaction::MAILTAG_CONTENT =>
pht("A post's content changes."),
PhamePostTransaction::MAILTAG_SUBSCRIBERS =>
pht("A post's subscribers change."),
PhamePostTransaction::MAILTAG_COMMENT =>
pht('Someone comments on a post.'),
PhamePostTransaction::MAILTAG_OTHER =>
pht('Other post activity not listed above occurs.'),
);
}
protected function getMailSubjectPrefix() {
return '[Phame]';
}
protected function supportsSearch() {
return true;
}
protected function shouldApplyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
return id(new HeraldPhamePostAdapter())
->setPost($object);
}
}
diff --git a/src/applications/phid/PhabricatorObjectHandle.php b/src/applications/phid/PhabricatorObjectHandle.php
index 1e6812b53..ba93dbcea 100644
--- a/src/applications/phid/PhabricatorObjectHandle.php
+++ b/src/applications/phid/PhabricatorObjectHandle.php
@@ -1,458 +1,468 @@
<?php
final class PhabricatorObjectHandle
extends Phobject
implements PhabricatorPolicyInterface {
const AVAILABILITY_FULL = 'full';
const AVAILABILITY_NONE = 'none';
const AVAILABILITY_NOEMAIL = 'no-email';
const AVAILABILITY_PARTIAL = 'partial';
const AVAILABILITY_DISABLED = 'disabled';
const STATUS_OPEN = 'open';
const STATUS_CLOSED = 'closed';
private $uri;
private $phid;
private $type;
private $name;
private $fullName;
private $title;
private $imageURI;
private $icon;
private $tagColor;
private $timestamp;
private $status = self::STATUS_OPEN;
private $availability = self::AVAILABILITY_FULL;
private $complete;
private $objectName;
private $policyFiltered;
private $subtitle;
private $tokenIcon;
private $commandLineObjectName;
+ private $mailStampName;
private $stateIcon;
private $stateColor;
private $stateName;
public function setIcon($icon) {
$this->icon = $icon;
return $this;
}
public function getIcon() {
if ($this->getPolicyFiltered()) {
return 'fa-lock';
}
if ($this->icon) {
return $this->icon;
}
return $this->getTypeIcon();
}
public function setSubtitle($subtitle) {
$this->subtitle = $subtitle;
return $this;
}
public function getSubtitle() {
return $this->subtitle;
}
public function setTagColor($color) {
static $colors;
if (!$colors) {
$colors = array_fuse(array_keys(PHUITagView::getShadeMap()));
}
if (isset($colors[$color])) {
$this->tagColor = $color;
}
return $this;
}
public function getTagColor() {
if ($this->getPolicyFiltered()) {
return 'disabled';
}
if ($this->tagColor) {
return $this->tagColor;
}
return 'blue';
}
public function getIconColor() {
if ($this->tagColor) {
return $this->tagColor;
}
return null;
}
public function setTokenIcon($icon) {
$this->tokenIcon = $icon;
return $this;
}
public function getTokenIcon() {
if ($this->tokenIcon !== null) {
return $this->tokenIcon;
}
return $this->getIcon();
}
public function getTypeIcon() {
if ($this->getPHIDType()) {
return $this->getPHIDType()->getTypeIcon();
}
return null;
}
public function setPolicyFiltered($policy_filered) {
$this->policyFiltered = $policy_filered;
return $this;
}
public function getPolicyFiltered() {
return $this->policyFiltered;
}
public function setObjectName($object_name) {
$this->objectName = $object_name;
return $this;
}
public function getObjectName() {
if (!$this->objectName) {
return $this->getName();
}
return $this->objectName;
}
+ public function setMailStampName($mail_stamp_name) {
+ $this->mailStampName = $mail_stamp_name;
+ return $this;
+ }
+
+ public function getMailStampName() {
+ return $this->mailStampName;
+ }
+
public function setURI($uri) {
$this->uri = $uri;
return $this;
}
public function getURI() {
return $this->uri;
}
public function setPHID($phid) {
$this->phid = $phid;
return $this;
}
public function getPHID() {
return $this->phid;
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
if ($this->name === null) {
if ($this->getPolicyFiltered()) {
return pht('Restricted %s', $this->getTypeName());
} else {
return pht('Unknown Object (%s)', $this->getTypeName());
}
}
return $this->name;
}
public function setAvailability($availability) {
$this->availability = $availability;
return $this;
}
public function getAvailability() {
return $this->availability;
}
public function isDisabled() {
return ($this->getAvailability() == self::AVAILABILITY_DISABLED);
}
public function setStatus($status) {
$this->status = $status;
return $this;
}
public function getStatus() {
return $this->status;
}
public function setFullName($full_name) {
$this->fullName = $full_name;
return $this;
}
public function getFullName() {
if ($this->fullName !== null) {
return $this->fullName;
}
return $this->getName();
}
public function setCommandLineObjectName($command_line_object_name) {
$this->commandLineObjectName = $command_line_object_name;
return $this;
}
public function getCommandLineObjectName() {
if ($this->commandLineObjectName !== null) {
return $this->commandLineObjectName;
}
return $this->getObjectName();
}
public function setTitle($title) {
$this->title = $title;
return $this;
}
public function getTitle() {
return $this->title;
}
public function setType($type) {
$this->type = $type;
return $this;
}
public function getType() {
return $this->type;
}
public function setImageURI($uri) {
$this->imageURI = $uri;
return $this;
}
public function getImageURI() {
return $this->imageURI;
}
public function setTimestamp($timestamp) {
$this->timestamp = $timestamp;
return $this;
}
public function getTimestamp() {
return $this->timestamp;
}
public function getTypeName() {
if ($this->getPHIDType()) {
return $this->getPHIDType()->getTypeName();
}
return $this->getType();
}
/**
* Set whether or not the underlying object is complete. See
* @{method:isComplete} for an explanation of what it means to be complete.
*
* @param bool True if the handle represents a complete object.
* @return this
*/
public function setComplete($complete) {
$this->complete = $complete;
return $this;
}
/**
* Determine if the handle represents an object which was completely loaded
* (i.e., the underlying object exists) vs an object which could not be
* completely loaded (e.g., the type or data for the PHID could not be
* identified or located).
*
* Basically, @{class:PhabricatorHandleQuery} gives you back a handle for
* any PHID you give it, but it gives you a complete handle only for valid
* PHIDs.
*
* @return bool True if the handle represents a complete object.
*/
public function isComplete() {
return $this->complete;
}
public function setStateIcon($state_icon) {
$this->stateIcon = $state_icon;
return $this;
}
public function getStateIcon() {
return $this->stateIcon;
}
public function setStateColor($state_color) {
$this->stateColor = $state_color;
return $this;
}
public function getStateColor() {
return $this->stateColor;
}
public function setStateName($state_name) {
$this->stateName = $state_name;
return $this;
}
public function getStateName() {
return $this->stateName;
}
public function renderStateIcon() {
$icon = $this->getStateIcon();
if ($icon === null) {
$icon = 'fa-question-circle-o';
}
$color = $this->getStateColor();
$name = $this->getStateName();
if ($name === null) {
$name = pht('Unknown');
}
return id(new PHUIIconView())
->setIcon($icon, $color)
->addSigil('has-tooltip')
->setMetadata(
array(
'tip' => $name,
));
}
public function renderLink($name = null) {
return $this->renderLinkWithAttributes($name, array());
}
public function renderHovercardLink($name = null) {
Javelin::initBehavior('phui-hovercards');
$attributes = array(
'sigil' => 'hovercard',
'meta' => array(
'hoverPHID' => $this->getPHID(),
),
);
return $this->renderLinkWithAttributes($name, $attributes);
}
private function renderLinkWithAttributes($name, array $attributes) {
if ($name === null) {
$name = $this->getLinkName();
}
$classes = array();
$classes[] = 'phui-handle';
$title = $this->title;
if ($this->status != self::STATUS_OPEN) {
$classes[] = 'handle-status-'.$this->status;
}
$circle = null;
if ($this->availability != self::AVAILABILITY_FULL) {
$classes[] = 'handle-availability-'.$this->availability;
$circle = array(
phutil_tag(
'span',
array(
'class' => 'perfect-circle',
),
"\xE2\x80\xA2"),
' ',
);
}
if ($this->getType() == PhabricatorPeopleUserPHIDType::TYPECONST) {
$classes[] = 'phui-link-person';
}
$uri = $this->getURI();
$icon = null;
if ($this->getPolicyFiltered()) {
$icon = id(new PHUIIconView())
->setIcon('fa-lock lightgreytext');
}
$attributes = $attributes + array(
'href' => $uri,
'class' => implode(' ', $classes),
'title' => $title,
);
return javelin_tag(
$uri ? 'a' : 'span',
$attributes,
array($circle, $icon, $name));
}
public function renderTag() {
return id(new PHUITagView())
->setType(PHUITagView::TYPE_SHADE)
->setColor($this->getTagColor())
->setIcon($this->getIcon())
->setHref($this->getURI())
->setName($this->getLinkName());
}
public function getLinkName() {
switch ($this->getType()) {
case PhabricatorPeopleUserPHIDType::TYPECONST:
$name = $this->getName();
break;
default:
$name = $this->getFullName();
break;
}
return $name;
}
protected function getPHIDType() {
$types = PhabricatorPHIDType::getAllTypes();
return idx($types, $this->getType());
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
return PhabricatorPolicies::POLICY_PUBLIC;
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
// NOTE: Handles are always visible, they just don't get populated with
// data if the user can't see the underlying object.
return true;
}
public function describeAutomaticCapability($capability) {
return null;
}
}
diff --git a/src/applications/pholio/editor/PholioMockEditor.php b/src/applications/pholio/editor/PholioMockEditor.php
index c0fcf31f8..6bd49d2e7 100644
--- a/src/applications/pholio/editor/PholioMockEditor.php
+++ b/src/applications/pholio/editor/PholioMockEditor.php
@@ -1,256 +1,254 @@
<?php
final class PholioMockEditor extends PhabricatorApplicationTransactionEditor {
private $newImages = array();
public function getEditorApplicationClass() {
return 'PhabricatorPholioApplication';
}
public function getEditorObjectsDescription() {
return pht('Pholio Mocks');
}
private function setNewImages(array $new_images) {
assert_instances_of($new_images, 'PholioImage');
$this->newImages = $new_images;
return $this;
}
public function getNewImages() {
return $this->newImages;
}
public function getCreateObjectTitle($author, $object) {
return pht('%s created this mock.', $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_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 PholioImageFileTransaction::TRANSACTIONTYPE:
case PholioImageReplaceTransaction::TRANSACTIONTYPE:
return true;
break;
}
}
return false;
}
protected function applyInitialEffects(
PhabricatorLiskDAO $object,
array $xactions) {
$new_images = array();
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PholioImageFileTransaction::TRANSACTIONTYPE:
$new_value = $xaction->getNewValue();
foreach ($new_value as $key => $txn_images) {
if ($key != '+') {
continue;
}
foreach ($txn_images as $image) {
$image->save();
$new_images[] = $image;
}
}
break;
case PholioImageReplaceTransaction::TRANSACTIONTYPE:
$image = $xaction->getNewValue();
$image->save();
$new_images[] = $image;
break;
}
}
$this->setNewImages($new_images);
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
$images = $this->getNewImages();
foreach ($images as $image) {
$image->setMockID($object->getID());
$image->save();
}
return $xactions;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new PholioReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
$name = $object->getName();
- $original_name = $object->getOriginalName();
return id(new PhabricatorMetaMTAMail())
- ->setSubject("M{$id}: {$name}")
- ->addHeader('Thread-Topic', "M{$id}: {$original_name}");
+ ->setSubject("M{$id}: {$name}");
}
protected function getMailTo(PhabricatorLiskDAO $object) {
return array(
$object->getAuthorPHID(),
$this->requireActor()->getPHID(),
);
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = new PhabricatorMetaMTAMailBody();
$headers = array();
$comments = array();
$inline_comments = array();
foreach ($xactions as $xaction) {
if ($xaction->shouldHide()) {
continue;
}
$comment = $xaction->getComment();
switch ($xaction->getTransactionType()) {
case PholioMockInlineTransaction::TRANSACTIONTYPE:
if ($comment && strlen($comment->getContent())) {
$inline_comments[] = $comment;
}
break;
case PhabricatorTransactions::TYPE_COMMENT:
if ($comment && strlen($comment->getContent())) {
$comments[] = $comment->getContent();
}
// fallthrough
default:
$headers[] = id(clone $xaction)
->setRenderingTarget('text')
->getTitle();
break;
}
}
$body->addRawSection(implode("\n", $headers));
foreach ($comments as $comment) {
$body->addRawSection($comment);
}
if ($inline_comments) {
$body->addRawSection(pht('INLINE COMMENTS'));
foreach ($inline_comments as $comment) {
$text = pht(
'Image %d: %s',
$comment->getImageID(),
$comment->getContent());
$body->addRawSection($text);
}
}
$body->addLinkSection(
pht('MOCK DETAIL'),
PhabricatorEnv::getProductionURI('/M'.$object->getID()));
return $body;
}
protected function getMailSubjectPrefix() {
return PhabricatorEnv::getEnvConfig('metamta.pholio.subject-prefix');
}
public function getMailTagsMap() {
return array(
PholioTransaction::MAILTAG_STATUS =>
pht("A mock's status changes."),
PholioTransaction::MAILTAG_COMMENT =>
pht('Someone comments on a mock.'),
PholioTransaction::MAILTAG_UPDATED =>
pht('Mock images or descriptions change.'),
PholioTransaction::MAILTAG_OTHER =>
pht('Other mock activity not listed above occurs.'),
);
}
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 HeraldPholioMockAdapter())
->setMock($object);
}
protected function sortTransactions(array $xactions) {
$head = array();
$tail = array();
// Move inline comments to the end, so the comments precede them.
foreach ($xactions as $xaction) {
$type = $xaction->getTransactionType();
if ($type == PholioMockInlineTransaction::TRANSACTIONTYPE) {
$tail[] = $xaction;
} else {
$head[] = $xaction;
}
}
return array_values(array_merge($head, $tail));
}
protected function shouldImplyCC(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PholioMockInlineTransaction::TRANSACTIONTYPE:
return true;
}
return parent::shouldImplyCC($object, $xaction);
}
}
diff --git a/src/applications/pholio/storage/PholioMock.php b/src/applications/pholio/storage/PholioMock.php
index 4aa9ef405..523733b3d 100644
--- a/src/applications/pholio/storage/PholioMock.php
+++ b/src/applications/pholio/storage/PholioMock.php
@@ -1,331 +1,329 @@
<?php
final class PholioMock extends PholioDAO
implements
PhabricatorMarkupInterface,
PhabricatorPolicyInterface,
PhabricatorSubscribableInterface,
PhabricatorTokenReceiverInterface,
PhabricatorFlaggableInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorProjectInterface,
PhabricatorDestructibleInterface,
PhabricatorSpacesInterface,
PhabricatorMentionableInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface {
const MARKUP_FIELD_DESCRIPTION = 'markup:description';
const STATUS_OPEN = 'open';
const STATUS_CLOSED = 'closed';
protected $authorPHID;
protected $viewPolicy;
protected $editPolicy;
protected $name;
- protected $originalName;
protected $description;
protected $coverPHID;
protected $mailKey;
protected $status;
protected $spacePHID;
private $images = self::ATTACHABLE;
private $allImages = self::ATTACHABLE;
private $coverFile = self::ATTACHABLE;
private $tokenCount = self::ATTACHABLE;
public static function initializeNewMock(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorPholioApplication'))
->executeOne();
$view_policy = $app->getPolicy(PholioDefaultViewCapability::CAPABILITY);
$edit_policy = $app->getPolicy(PholioDefaultEditCapability::CAPABILITY);
return id(new PholioMock())
->setAuthorPHID($actor->getPHID())
->attachImages(array())
->setStatus(self::STATUS_OPEN)
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy)
->setSpacePHID($actor->getDefaultSpacePHID());
}
public function getMonogram() {
return 'M'.$this->getID();
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text128',
'description' => 'text',
- 'originalName' => 'text128',
'mailKey' => 'bytes20',
'status' => 'text12',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'authorPHID' => array(
'columns' => array('authorPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID('MOCK');
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
/**
* These should be the images currently associated with the Mock.
*/
public function attachImages(array $images) {
assert_instances_of($images, 'PholioImage');
$this->images = $images;
return $this;
}
public function getImages() {
$this->assertAttached($this->images);
return $this->images;
}
/**
* These should be *all* images associated with the Mock. This includes
* images which have been removed and / or replaced from the Mock.
*/
public function attachAllImages(array $images) {
assert_instances_of($images, 'PholioImage');
$this->allImages = $images;
return $this;
}
public function getAllImages() {
$this->assertAttached($this->images);
return $this->allImages;
}
public function attachCoverFile(PhabricatorFile $file) {
$this->coverFile = $file;
return $this;
}
public function getCoverFile() {
$this->assertAttached($this->coverFile);
return $this->coverFile;
}
public function getTokenCount() {
$this->assertAttached($this->tokenCount);
return $this->tokenCount;
}
public function attachTokenCount($count) {
$this->tokenCount = $count;
return $this;
}
public function getImageHistorySet($image_id) {
$images = $this->getAllImages();
$images = mpull($images, null, 'getID');
$selected_image = $images[$image_id];
$replace_map = mpull($images, null, 'getReplacesImagePHID');
$phid_map = mpull($images, null, 'getPHID');
// find the earliest image
$image = $selected_image;
while (isset($phid_map[$image->getReplacesImagePHID()])) {
$image = $phid_map[$image->getReplacesImagePHID()];
}
// now build history moving forward
$history = array($image->getID() => $image);
while (isset($replace_map[$image->getPHID()])) {
$image = $replace_map[$image->getPHID()];
$history[$image->getID()] = $image;
}
return $history;
}
public function getStatuses() {
$options = array();
$options[self::STATUS_OPEN] = pht('Open');
$options[self::STATUS_CLOSED] = pht('Closed');
return $options;
}
public function isClosed() {
return ($this->getStatus() == 'closed');
}
/* -( PhabricatorSubscribableInterface Implementation )-------------------- */
public function isAutomaticallySubscribed($phid) {
return ($this->authorPHID == $phid);
}
/* -( PhabricatorPolicyInterface Implementation )-------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return ($viewer->getPHID() == $this->getAuthorPHID());
}
public function describeAutomaticCapability($capability) {
return pht("A mock's owner can always view and edit it.");
}
/* -( PhabricatorMarkupInterface )----------------------------------------- */
public function getMarkupFieldKey($field) {
$content = $this->getMarkupText($field);
return PhabricatorMarkupEngine::digestRemarkupContent($this, $content);
}
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newMarkupEngine(array());
}
public function getMarkupText($field) {
if ($this->getDescription()) {
return $this->getDescription();
}
return null;
}
public function didMarkupText($field, $output, PhutilMarkupEngine $engine) {
require_celerity_resource('phabricator-remarkup-css');
return phutil_tag(
'div',
array(
'class' => 'phabricator-remarkup',
),
$output);
}
public function shouldUseMarkupCache($field) {
return (bool)$this->getID();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PholioMockEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PholioTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
PholioMockQuery::loadImages(
$request->getUser(),
array($this),
$need_inline_comments = true);
$timeline->setMock($this);
return $timeline;
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array(
$this->getAuthorPHID(),
);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$images = id(new PholioImage())->loadAllWhere(
'mockID = %d',
$this->getID());
foreach ($images as $image) {
$image->delete();
}
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorSpacesInterface )----------------------------------------- */
public function getSpacePHID() {
return $this->spacePHID;
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new PholioMockFulltextEngine();
}
/* -( PhabricatorFerretInterface )----------------------------------------- */
public function newFerretEngine() {
return new PholioMockFerretEngine();
}
}
diff --git a/src/applications/pholio/xaction/PholioMockNameTransaction.php b/src/applications/pholio/xaction/PholioMockNameTransaction.php
index d1231636a..82fb92fe4 100644
--- a/src/applications/pholio/xaction/PholioMockNameTransaction.php
+++ b/src/applications/pholio/xaction/PholioMockNameTransaction.php
@@ -1,92 +1,89 @@
<?php
final class PholioMockNameTransaction
extends PholioMockTransactionType {
const TRANSACTIONTYPE = 'name';
public function generateOldValue($object) {
return $object->getName();
}
public function getActionStrength() {
return 1.4;
}
public function applyInternalEffects($object, $value) {
$object->setName($value);
- if ($object->getOriginalName() === null) {
- $object->setOriginalName($this->getNewValue());
- }
}
public function getTitle() {
$old = $this->getOldValue();
$new = $this->getNewValue();
if ($old === null) {
return pht(
'%s created %s.',
$this->renderAuthor(),
$this->renderValue($new));
} else {
return pht(
'%s renamed this mock from %s to %s.',
$this->renderAuthor(),
$this->renderValue($old),
$this->renderValue($new));
}
}
public function getTitleForFeed() {
$old = $this->getOldValue();
$new = $this->getNewValue();
if ($old === null) {
return pht(
'%s created %s.',
$this->renderAuthor(),
$this->renderObject());
} else {
return pht(
'%s renamed %s from %s to %s.',
$this->renderAuthor(),
$this->renderObject(),
$this->renderValue($old),
$this->renderValue($new));
}
}
public function getColor() {
$old = $this->getOldValue();
if ($old === null) {
return PhabricatorTransactions::COLOR_GREEN;
}
return parent::getColor();
}
public function validateTransactions($object, array $xactions) {
$errors = array();
if ($this->isEmptyTextTransaction($object->getName(), $xactions)) {
$errors[] = $this->newRequiredError(pht('Mocks must have a name.'));
}
$max_length = $object->getColumnMaximumByteLength('name');
foreach ($xactions as $xaction) {
$new_value = $xaction->getNewValue();
$new_length = strlen($new_value);
if ($new_length > $max_length) {
$errors[] = $this->newInvalidError(
pht(
'Mock names must not be longer than %s character(s).',
new PhutilNumber($max_length)));
}
}
return $errors;
}
}
diff --git a/src/applications/phortune/editor/PhortuneCartEditor.php b/src/applications/phortune/editor/PhortuneCartEditor.php
index 5196e1242..dcf1f2d0e 100644
--- a/src/applications/phortune/editor/PhortuneCartEditor.php
+++ b/src/applications/phortune/editor/PhortuneCartEditor.php
@@ -1,243 +1,242 @@
<?php
final class PhortuneCartEditor
extends PhabricatorApplicationTransactionEditor {
private $invoiceIssues;
public function setInvoiceIssues(array $invoice_issues) {
$this->invoiceIssues = $invoice_issues;
return $this;
}
public function getInvoiceIssues() {
return $this->invoiceIssues;
}
public function isInvoice() {
return (bool)$this->invoiceIssues;
}
public function getEditorApplicationClass() {
return 'PhabricatorPhortuneApplication';
}
public function getEditorObjectsDescription() {
return pht('Phortune Carts');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhortuneCartTransaction::TYPE_CREATED;
$types[] = PhortuneCartTransaction::TYPE_PURCHASED;
$types[] = PhortuneCartTransaction::TYPE_HOLD;
$types[] = PhortuneCartTransaction::TYPE_REVIEW;
$types[] = PhortuneCartTransaction::TYPE_CANCEL;
$types[] = PhortuneCartTransaction::TYPE_REFUND;
$types[] = PhortuneCartTransaction::TYPE_INVOICED;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhortuneCartTransaction::TYPE_CREATED:
case PhortuneCartTransaction::TYPE_PURCHASED:
case PhortuneCartTransaction::TYPE_HOLD:
case PhortuneCartTransaction::TYPE_REVIEW:
case PhortuneCartTransaction::TYPE_CANCEL:
case PhortuneCartTransaction::TYPE_REFUND:
case PhortuneCartTransaction::TYPE_INVOICED:
return null;
}
return parent::getCustomTransactionOldValue($object, $xaction);
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhortuneCartTransaction::TYPE_CREATED:
case PhortuneCartTransaction::TYPE_PURCHASED:
case PhortuneCartTransaction::TYPE_HOLD:
case PhortuneCartTransaction::TYPE_REVIEW:
case PhortuneCartTransaction::TYPE_CANCEL:
case PhortuneCartTransaction::TYPE_REFUND:
case PhortuneCartTransaction::TYPE_INVOICED:
return $xaction->getNewValue();
}
return parent::getCustomTransactionNewValue($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhortuneCartTransaction::TYPE_CREATED:
case PhortuneCartTransaction::TYPE_PURCHASED:
case PhortuneCartTransaction::TYPE_HOLD:
case PhortuneCartTransaction::TYPE_REVIEW:
case PhortuneCartTransaction::TYPE_CANCEL:
case PhortuneCartTransaction::TYPE_REFUND:
case PhortuneCartTransaction::TYPE_INVOICED:
return;
}
return parent::applyCustomInternalTransaction($object, $xaction);
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhortuneCartTransaction::TYPE_CREATED:
case PhortuneCartTransaction::TYPE_PURCHASED:
case PhortuneCartTransaction::TYPE_HOLD:
case PhortuneCartTransaction::TYPE_REVIEW:
case PhortuneCartTransaction::TYPE_CANCEL:
case PhortuneCartTransaction::TYPE_REFUND:
case PhortuneCartTransaction::TYPE_INVOICED:
return;
}
return parent::applyCustomExternalTransaction($object, $xaction);
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
$name = $object->getName();
return id(new PhabricatorMetaMTAMail())
- ->setSubject(pht('Order %d: %s', $id, $name))
- ->addHeader('Thread-Topic', pht('Order %s', $id));
+ ->setSubject(pht('Order %d: %s', $id, $name));
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
if ($this->isInvoice()) {
$issues = $this->getInvoiceIssues();
foreach ($issues as $key => $issue) {
$issues[$key] = ' - '.$issue;
}
$issues = implode("\n", $issues);
$overview = pht(
"Payment for this invoice could not be processed automatically:\n\n".
"%s",
$issues);
$body->addRemarkupSection(null, $overview);
$body->addLinkSection(
pht('PAY NOW'),
PhabricatorEnv::getProductionURI($object->getCheckoutURI()));
}
$items = array();
foreach ($object->getPurchases() as $purchase) {
$name = $purchase->getFullDisplayName();
$price = $purchase->getTotalPriceAsCurrency()->formatForDisplay();
$items[] = "{$name} {$price}";
}
$body->addTextSection(pht('ORDER CONTENTS'), implode("\n", $items));
if ($this->isInvoice()) {
$subscription = id(new PhortuneSubscriptionQuery())
->setViewer($this->requireActor())
->withPHIDs(array($object->getSubscriptionPHID()))
->executeOne();
if ($subscription) {
$body->addLinkSection(
pht('SUBSCRIPTION'),
PhabricatorEnv::getProductionURI($subscription->getURI()));
}
} else {
$body->addLinkSection(
pht('ORDER DETAIL'),
PhabricatorEnv::getProductionURI($object->getDetailURI()));
}
$account_uri = '/phortune/'.$object->getAccount()->getID().'/';
$body->addLinkSection(
pht('ACCOUNT OVERVIEW'),
PhabricatorEnv::getProductionURI($account_uri));
return $body;
}
protected function getMailTo(PhabricatorLiskDAO $object) {
$phids = array();
// Reload the cart to pull merchant and account information, in case we
// just created the object.
$cart = id(new PhortuneCartQuery())
->setViewer($this->requireActor())
->withPHIDs(array($object->getPHID()))
->executeOne();
foreach ($cart->getAccount()->getMemberPHIDs() as $account_member) {
$phids[] = $account_member;
}
foreach ($cart->getMerchant()->getMemberPHIDs() as $merchant_member) {
$phids[] = $merchant_member;
}
return $phids;
}
protected function getMailCC(PhabricatorLiskDAO $object) {
return array();
}
protected function getMailSubjectPrefix() {
return '[Phortune]';
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new PhortuneCartReplyHandler())
->setMailReceiver($object);
}
protected function willPublish(PhabricatorLiskDAO $object, array $xactions) {
// We need the purchases in order to build mail.
return id(new PhortuneCartQuery())
->setViewer($this->getActor())
->withIDs(array($object->getID()))
->needPurchases(true)
->executeOne();
}
protected function getCustomWorkerState() {
return array(
'invoiceIssues' => $this->invoiceIssues,
);
}
protected function loadCustomWorkerState(array $state) {
$this->invoiceIssues = idx($state, 'invoiceIssues');
return $this;
}
}
diff --git a/src/applications/phortune/provider/PhortuneStripePaymentProvider.php b/src/applications/phortune/provider/PhortuneStripePaymentProvider.php
index 5adfc62e1..bdaa4294b 100644
--- a/src/applications/phortune/provider/PhortuneStripePaymentProvider.php
+++ b/src/applications/phortune/provider/PhortuneStripePaymentProvider.php
@@ -1,380 +1,386 @@
<?php
final class PhortuneStripePaymentProvider extends PhortunePaymentProvider {
const STRIPE_PUBLISHABLE_KEY = 'stripe.publishable-key';
const STRIPE_SECRET_KEY = 'stripe.secret-key';
public function isAcceptingLivePayments() {
return preg_match('/_live_/', $this->getPublishableKey());
}
public function getName() {
return pht('Stripe');
}
public function getConfigureName() {
return pht('Add Stripe Payments Account');
}
public function getConfigureDescription() {
return pht(
'Allows you to accept credit or debit card payments with a '.
'stripe.com account.');
}
public function getConfigureProvidesDescription() {
return pht('This merchant accepts credit and debit cards via Stripe.');
}
public function getPaymentMethodDescription() {
return pht('Add Credit or Debit Card (US and Canada)');
}
public function getPaymentMethodIcon() {
return 'Stripe';
}
public function getPaymentMethodProviderDescription() {
return pht('Processed by Stripe');
}
public function getDefaultPaymentMethodDisplayName(
PhortunePaymentMethod $method) {
return pht('Credit/Debit Card');
}
public function getAllConfigurableProperties() {
return array(
self::STRIPE_PUBLISHABLE_KEY,
self::STRIPE_SECRET_KEY,
);
}
public function getAllConfigurableSecretProperties() {
return array(
self::STRIPE_SECRET_KEY,
);
}
public function processEditForm(
AphrontRequest $request,
array $values) {
$errors = array();
$issues = array();
if (!strlen($values[self::STRIPE_SECRET_KEY])) {
$errors[] = pht('Stripe Secret Key is required.');
$issues[self::STRIPE_SECRET_KEY] = pht('Required');
}
if (!strlen($values[self::STRIPE_PUBLISHABLE_KEY])) {
$errors[] = pht('Stripe Publishable Key is required.');
$issues[self::STRIPE_PUBLISHABLE_KEY] = pht('Required');
}
return array($errors, $issues, $values);
}
public function extendEditForm(
AphrontRequest $request,
AphrontFormView $form,
array $values,
array $issues) {
$form
->appendChild(
id(new AphrontFormTextControl())
->setName(self::STRIPE_SECRET_KEY)
->setValue($values[self::STRIPE_SECRET_KEY])
->setError(idx($issues, self::STRIPE_SECRET_KEY, true))
->setLabel(pht('Stripe Secret Key')))
->appendChild(
id(new AphrontFormTextControl())
->setName(self::STRIPE_PUBLISHABLE_KEY)
->setValue($values[self::STRIPE_PUBLISHABLE_KEY])
->setError(idx($issues, self::STRIPE_PUBLISHABLE_KEY, true))
->setLabel(pht('Stripe Publishable Key')));
}
public function getConfigureInstructions() {
return pht(
"To configure Stripe, register or log in to an existing account on ".
"[[https://stripe.com | stripe.com]]. Once logged in:\n\n".
" - Go to {nav icon=user, name=Your Account > Account Settings ".
"> API Keys}\n".
" - Copy the **Secret Key** and **Publishable Key** into the fields ".
"above.\n\n".
"You can either use the test keys to add this provider in test mode, ".
"or the live keys to accept live payments.");
}
public function canRunConfigurationTest() {
return true;
}
public function runConfigurationTest() {
$this->loadStripeAPILibraries();
$secret_key = $this->getSecretKey();
$account = Stripe_Account::retrieve($secret_key);
}
/**
* @phutil-external-symbol class Stripe_Charge
* @phutil-external-symbol class Stripe_CardError
* @phutil-external-symbol class Stripe_Account
*/
protected function executeCharge(
PhortunePaymentMethod $method,
PhortuneCharge $charge) {
$this->loadStripeAPILibraries();
$price = $charge->getAmountAsCurrency();
$secret_key = $this->getSecretKey();
$params = array(
'amount' => $price->getValueInUSDCents(),
'currency' => $price->getCurrency(),
'customer' => $method->getMetadataValue('stripe.customerID'),
'description' => $charge->getPHID(),
'capture' => true,
);
$stripe_charge = Stripe_Charge::create($params, $secret_key);
$id = $stripe_charge->id;
if (!$id) {
throw new Exception(pht('Stripe charge call did not return an ID!'));
}
$charge->setMetadataValue('stripe.chargeID', $id);
$charge->save();
}
protected function executeRefund(
PhortuneCharge $charge,
PhortuneCharge $refund) {
$this->loadStripeAPILibraries();
$charge_id = $charge->getMetadataValue('stripe.chargeID');
if (!$charge_id) {
throw new Exception(
pht('Unable to refund charge; no Stripe chargeID!'));
}
$refund_cents = $refund
->getAmountAsCurrency()
->negate()
->getValueInUSDCents();
$secret_key = $this->getSecretKey();
$params = array(
'amount' => $refund_cents,
);
$stripe_charge = Stripe_Charge::retrieve($charge_id, $secret_key);
$stripe_refund = $stripe_charge->refunds->create($params);
$id = $stripe_refund->id;
if (!$id) {
throw new Exception(pht('Stripe refund call did not return an ID!'));
}
$charge->setMetadataValue('stripe.refundID', $id);
$charge->save();
}
public function updateCharge(PhortuneCharge $charge) {
$this->loadStripeAPILibraries();
$charge_id = $charge->getMetadataValue('stripe.chargeID');
if (!$charge_id) {
throw new Exception(
pht('Unable to update charge; no Stripe chargeID!'));
}
$secret_key = $this->getSecretKey();
$stripe_charge = Stripe_Charge::retrieve($charge_id, $secret_key);
// TODO: Deal with disputes / chargebacks / surprising refunds.
}
private function getPublishableKey() {
return $this
->getProviderConfig()
->getMetadataValue(self::STRIPE_PUBLISHABLE_KEY);
}
private function getSecretKey() {
return $this
->getProviderConfig()
->getMetadataValue(self::STRIPE_SECRET_KEY);
}
/* -( Adding Payment Methods )--------------------------------------------- */
public function canCreatePaymentMethods() {
return true;
}
/**
* @phutil-external-symbol class Stripe_Token
* @phutil-external-symbol class Stripe_Customer
*/
public function createPaymentMethodFromRequest(
AphrontRequest $request,
PhortunePaymentMethod $method,
array $token) {
$this->loadStripeAPILibraries();
$errors = array();
$secret_key = $this->getSecretKey();
$stripe_token = $token['stripeCardToken'];
// First, make sure the token is valid.
$info = id(new Stripe_Token())->retrieve($stripe_token, $secret_key);
$account_phid = $method->getAccountPHID();
$author_phid = $method->getAuthorPHID();
$params = array(
'card' => $stripe_token,
'description' => $account_phid.':'.$author_phid,
);
// Then, we need to create a Customer in order to be able to charge
// the card more than once. We create one Customer for each card;
// they do not map to PhortuneAccounts because we allow an account to
// have more than one active card.
$customer = Stripe_Customer::create($params, $secret_key);
$card = $info->card;
$method
->setBrand($card->brand)
->setLastFourDigits($card->last4)
->setExpires($card->exp_year, $card->exp_month)
->setMetadata(
array(
'type' => 'stripe.customer',
'stripe.customerID' => $customer->id,
'stripe.cardToken' => $stripe_token,
));
return $errors;
}
public function renderCreatePaymentMethodForm(
AphrontRequest $request,
array $errors) {
+ $src = 'https://js.stripe.com/v2/';
+
$ccform = id(new PhortuneCreditCardForm())
->setSecurityAssurance(
pht('Payments are processed securely by Stripe.'))
->setUser($request->getUser())
->setErrors($errors)
- ->addScript('https://js.stripe.com/v2/');
+ ->addScript($src);
+
+ CelerityAPI::getStaticResourceResponse()
+ ->addContentSecurityPolicyURI('script-src', $src)
+ ->addContentSecurityPolicyURI('frame-src', $src);
Javelin::initBehavior(
'stripe-payment-form',
array(
'stripePublishableKey' => $this->getPublishableKey(),
'formID' => $ccform->getFormID(),
));
return $ccform->buildForm();
}
private function getStripeShortErrorCode($error_code) {
$prefix = 'cc:stripe:';
if (strncmp($error_code, $prefix, strlen($prefix))) {
return null;
}
return substr($error_code, strlen($prefix));
}
public function validateCreatePaymentMethodToken(array $token) {
return isset($token['stripeCardToken']);
}
public function translateCreatePaymentMethodErrorCode($error_code) {
$short_code = $this->getStripeShortErrorCode($error_code);
if ($short_code) {
static $map = array(
'error:invalid_number' => PhortuneErrCode::ERR_CC_INVALID_NUMBER,
'error:invalid_cvc' => PhortuneErrCode::ERR_CC_INVALID_CVC,
'error:invalid_expiry_month' => PhortuneErrCode::ERR_CC_INVALID_EXPIRY,
'error:invalid_expiry_year' => PhortuneErrCode::ERR_CC_INVALID_EXPIRY,
);
if (isset($map[$short_code])) {
return $map[$short_code];
}
}
return $error_code;
}
/**
* See https://stripe.com/docs/api#errors for more information on possible
* errors.
*/
public function getCreatePaymentMethodErrorMessage($error_code) {
$short_code = $this->getStripeShortErrorCode($error_code);
if (!$short_code) {
return null;
}
switch ($short_code) {
case 'error:incorrect_number':
$error_key = 'number';
$message = pht('Invalid or incorrect credit card number.');
break;
case 'error:incorrect_cvc':
$error_key = 'cvc';
$message = pht('Card CVC is invalid or incorrect.');
break;
$error_key = 'exp';
$message = pht('Card expiration date is invalid or incorrect.');
break;
case 'error:invalid_expiry_month':
case 'error:invalid_expiry_year':
case 'error:invalid_cvc':
case 'error:invalid_number':
// NOTE: These should be translated into Phortune error codes earlier,
// so we don't expect to receive them here. They are listed for clarity
// and completeness. If we encounter one, we treat it as an unknown
// error.
break;
case 'error:invalid_amount':
case 'error:missing':
case 'error:card_declined':
case 'error:expired_card':
case 'error:duplicate_transaction':
case 'error:processing_error':
default:
// NOTE: These errors currently don't receive a detailed message.
// NOTE: We can also end up here with "http:nnn" messages.
// TODO: At least some of these should have a better message, or be
// translated into common errors above.
break;
}
return null;
}
private function loadStripeAPILibraries() {
$root = dirname(phutil_get_library_root('phabricator'));
require_once $root.'/externals/stripe-php/lib/Stripe.php';
}
}
diff --git a/src/applications/phortune/query/PhortunePaymentMethodQuery.php b/src/applications/phortune/query/PhortunePaymentMethodQuery.php
index 07d49a590..42d54805e 100644
--- a/src/applications/phortune/query/PhortunePaymentMethodQuery.php
+++ b/src/applications/phortune/query/PhortunePaymentMethodQuery.php
@@ -1,156 +1,147 @@
<?php
final class PhortunePaymentMethodQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $accountPHIDs;
private $merchantPHIDs;
private $statuses;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withAccountPHIDs(array $phids) {
$this->accountPHIDs = $phids;
return $this;
}
public function withMerchantPHIDs(array $phids) {
$this->merchantPHIDs = $phids;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
+ public function newResultObject() {
+ return new PhortunePaymentMethod();
+ }
+
protected function loadPage() {
- $table = new PhortunePaymentMethod();
- $conn = $table->establishConnection('r');
-
- $rows = queryfx_all(
- $conn,
- 'SELECT * FROM %T %Q %Q %Q',
- $table->getTableName(),
- $this->buildWhereClause($conn),
- $this->buildOrderClause($conn),
- $this->buildLimitClause($conn));
-
- return $table->loadAllFromArray($rows);
+ return $this->loadStandardPage($this->newResultObject());
}
protected function willFilterPage(array $methods) {
$accounts = id(new PhortuneAccountQuery())
->setViewer($this->getViewer())
->withPHIDs(mpull($methods, 'getAccountPHID'))
->execute();
$accounts = mpull($accounts, null, 'getPHID');
foreach ($methods as $key => $method) {
$account = idx($accounts, $method->getAccountPHID());
if (!$account) {
unset($methods[$key]);
continue;
}
$method->attachAccount($account);
}
if (!$methods) {
return $methods;
}
$merchants = id(new PhortuneMerchantQuery())
->setViewer($this->getViewer())
->withPHIDs(mpull($methods, 'getMerchantPHID'))
->execute();
$merchants = mpull($merchants, null, 'getPHID');
foreach ($methods as $key => $method) {
$merchant = idx($merchants, $method->getMerchantPHID());
if (!$merchant) {
unset($methods[$key]);
continue;
}
$method->attachMerchant($merchant);
}
if (!$methods) {
return $methods;
}
$provider_configs = id(new PhortunePaymentProviderConfigQuery())
->setViewer($this->getViewer())
->withPHIDs(mpull($methods, 'getProviderPHID'))
->execute();
$provider_configs = mpull($provider_configs, null, 'getPHID');
foreach ($methods as $key => $method) {
$provider_config = idx($provider_configs, $method->getProviderPHID());
if (!$provider_config) {
unset($methods[$key]);
continue;
}
$method->attachProviderConfig($provider_config);
}
return $methods;
}
- protected function buildWhereClause(AphrontDatabaseConnection $conn) {
- $where = array();
+ protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
+ $where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
if ($this->accountPHIDs !== null) {
$where[] = qsprintf(
$conn,
'accountPHID IN (%Ls)',
$this->accountPHIDs);
}
if ($this->merchantPHIDs !== null) {
$where[] = qsprintf(
$conn,
'merchantPHID IN (%Ls)',
$this->merchantPHIDs);
}
if ($this->statuses !== null) {
$where[] = qsprintf(
$conn,
'status IN (%Ls)',
$this->statuses);
}
- $where[] = $this->buildPagingClause($conn);
-
- return $this->formatWhereClause($where);
+ return $where;
}
public function getQueryApplicationClass() {
return 'PhabricatorPhortuneApplication';
}
}
diff --git a/src/applications/phortune/storage/PhortuneCart.php b/src/applications/phortune/storage/PhortuneCart.php
index af2b386df..07554ccb8 100644
--- a/src/applications/phortune/storage/PhortuneCart.php
+++ b/src/applications/phortune/storage/PhortuneCart.php
@@ -1,693 +1,700 @@
<?php
final class PhortuneCart extends PhortuneDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface {
const STATUS_BUILDING = 'cart:building';
const STATUS_READY = 'cart:ready';
const STATUS_PURCHASING = 'cart:purchasing';
const STATUS_CHARGED = 'cart:charged';
const STATUS_HOLD = 'cart:hold';
const STATUS_REVIEW = 'cart:review';
const STATUS_PURCHASED = 'cart:purchased';
protected $accountPHID;
protected $authorPHID;
protected $merchantPHID;
protected $subscriptionPHID;
protected $cartClass;
protected $status;
protected $metadata = array();
protected $mailKey;
protected $isInvoice;
private $account = self::ATTACHABLE;
private $purchases = self::ATTACHABLE;
private $implementation = self::ATTACHABLE;
private $merchant = self::ATTACHABLE;
public static function initializeNewCart(
PhabricatorUser $actor,
PhortuneAccount $account,
PhortuneMerchant $merchant) {
$cart = id(new PhortuneCart())
->setAuthorPHID($actor->getPHID())
->setStatus(self::STATUS_BUILDING)
->setAccountPHID($account->getPHID())
->setIsInvoice(0)
->attachAccount($account)
->setMerchantPHID($merchant->getPHID())
->attachMerchant($merchant);
$cart->account = $account;
$cart->purchases = array();
return $cart;
}
public function newPurchase(
PhabricatorUser $actor,
PhortuneProduct $product) {
$purchase = PhortunePurchase::initializeNewPurchase($actor, $product)
->setAccountPHID($this->getAccount()->getPHID())
->setCartPHID($this->getPHID())
->save();
$this->purchases[] = $purchase;
return $purchase;
}
public static function getStatusNameMap() {
return array(
self::STATUS_BUILDING => pht('Building'),
self::STATUS_READY => pht('Ready'),
self::STATUS_PURCHASING => pht('Purchasing'),
self::STATUS_CHARGED => pht('Charged'),
self::STATUS_HOLD => pht('Hold'),
self::STATUS_REVIEW => pht('Review'),
self::STATUS_PURCHASED => pht('Purchased'),
);
}
public static function getNameForStatus($status) {
return idx(self::getStatusNameMap(), $status, $status);
}
public function activateCart() {
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if ($copy->getStatus() !== self::STATUS_BUILDING) {
throw new Exception(
pht(
'Cart has wrong status ("%s") to call %s.',
$copy->getStatus(),
'willApplyCharge()'));
}
$this->setStatus(self::STATUS_READY)->save();
$this->endReadLocking();
$this->saveTransaction();
$this->recordCartTransaction(PhortuneCartTransaction::TYPE_CREATED);
return $this;
}
public function willApplyCharge(
PhabricatorUser $actor,
PhortunePaymentProvider $provider,
PhortunePaymentMethod $method = null) {
$account = $this->getAccount();
$charge = PhortuneCharge::initializeNewCharge()
->setAccountPHID($account->getPHID())
->setCartPHID($this->getPHID())
->setAuthorPHID($actor->getPHID())
->setMerchantPHID($this->getMerchant()->getPHID())
->setProviderPHID($provider->getProviderConfig()->getPHID())
->setAmountAsCurrency($this->getTotalPriceAsCurrency());
if ($method) {
+ if (!$method->isActive()) {
+ throw new Exception(
+ pht(
+ 'Attempting to apply a charge using an inactive '.
+ 'payment method ("%s")!',
+ $method->getPHID()));
+ }
$charge->setPaymentMethodPHID($method->getPHID());
}
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if ($copy->getStatus() !== self::STATUS_READY) {
throw new Exception(
pht(
'Cart has wrong status ("%s") to call %s, expected "%s".',
$copy->getStatus(),
'willApplyCharge()',
self::STATUS_READY));
}
$charge->save();
$this->setStatus(self::STATUS_PURCHASING)->save();
$this->endReadLocking();
$this->saveTransaction();
return $charge;
}
public function didHoldCharge(PhortuneCharge $charge) {
$charge->setStatus(PhortuneCharge::STATUS_HOLD);
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if ($copy->getStatus() !== self::STATUS_PURCHASING) {
throw new Exception(
pht(
'Cart has wrong status ("%s") to call %s, expected "%s".',
$copy->getStatus(),
'didHoldCharge()',
self::STATUS_PURCHASING));
}
$charge->save();
$this->setStatus(self::STATUS_HOLD)->save();
$this->endReadLocking();
$this->saveTransaction();
$this->recordCartTransaction(PhortuneCartTransaction::TYPE_HOLD);
}
public function didApplyCharge(PhortuneCharge $charge) {
$charge->setStatus(PhortuneCharge::STATUS_CHARGED);
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if (($copy->getStatus() !== self::STATUS_PURCHASING) &&
($copy->getStatus() !== self::STATUS_HOLD)) {
throw new Exception(
pht(
'Cart has wrong status ("%s") to call %s.',
$copy->getStatus(),
'didApplyCharge()'));
}
$charge->save();
$this->setStatus(self::STATUS_CHARGED)->save();
$this->endReadLocking();
$this->saveTransaction();
// TODO: Perform purchase review. Here, we would apply rules to determine
// whether the charge needs manual review (maybe making the decision via
// Herald, configuration, or by examining provider fraud data). For now,
// don't require review.
$needs_review = false;
if ($needs_review) {
$this->willReviewCart();
} else {
$this->didReviewCart();
}
return $this;
}
public function willReviewCart() {
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if (($copy->getStatus() !== self::STATUS_CHARGED)) {
throw new Exception(
pht(
'Cart has wrong status ("%s") to call %s!',
$copy->getStatus(),
'willReviewCart()'));
}
$this->setStatus(self::STATUS_REVIEW)->save();
$this->endReadLocking();
$this->saveTransaction();
$this->recordCartTransaction(PhortuneCartTransaction::TYPE_REVIEW);
return $this;
}
public function didReviewCart() {
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if (($copy->getStatus() !== self::STATUS_CHARGED) &&
($copy->getStatus() !== self::STATUS_REVIEW)) {
throw new Exception(
pht(
'Cart has wrong status ("%s") to call %s!',
$copy->getStatus(),
'didReviewCart()'));
}
foreach ($this->purchases as $purchase) {
$purchase->getProduct()->didPurchaseProduct($purchase);
}
$this->setStatus(self::STATUS_PURCHASED)->save();
$this->endReadLocking();
$this->saveTransaction();
$this->recordCartTransaction(PhortuneCartTransaction::TYPE_PURCHASED);
return $this;
}
public function didFailCharge(PhortuneCharge $charge) {
$charge->setStatus(PhortuneCharge::STATUS_FAILED);
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if (($copy->getStatus() !== self::STATUS_PURCHASING) &&
($copy->getStatus() !== self::STATUS_HOLD)) {
throw new Exception(
pht(
'Cart has wrong status ("%s") to call %s.',
$copy->getStatus(),
'didFailCharge()'));
}
$charge->save();
// Move the cart back into STATUS_READY so the user can try
// making the purchase again.
$this->setStatus(self::STATUS_READY)->save();
$this->endReadLocking();
$this->saveTransaction();
return $this;
}
public function willRefundCharge(
PhabricatorUser $actor,
PhortunePaymentProvider $provider,
PhortuneCharge $charge,
PhortuneCurrency $amount) {
if (!$amount->isPositive()) {
throw new Exception(
pht('Trying to refund non-positive amount of money!'));
}
if ($amount->isGreaterThan($charge->getAmountRefundableAsCurrency())) {
throw new Exception(
pht('Trying to refund more money than remaining on charge!'));
}
if ($charge->getRefundedChargePHID()) {
throw new Exception(
pht('Trying to refund a refund!'));
}
if (($charge->getStatus() !== PhortuneCharge::STATUS_CHARGED) &&
($charge->getStatus() !== PhortuneCharge::STATUS_HOLD)) {
throw new Exception(
pht('Trying to refund an uncharged charge!'));
}
$refund_charge = PhortuneCharge::initializeNewCharge()
->setAccountPHID($this->getAccount()->getPHID())
->setCartPHID($this->getPHID())
->setAuthorPHID($actor->getPHID())
->setMerchantPHID($this->getMerchant()->getPHID())
->setProviderPHID($provider->getProviderConfig()->getPHID())
->setPaymentMethodPHID($charge->getPaymentMethodPHID())
->setRefundedChargePHID($charge->getPHID())
->setAmountAsCurrency($amount->negate());
$charge->openTransaction();
$charge->beginReadLocking();
$copy = clone $charge;
$copy->reload();
if ($copy->getRefundingPHID() !== null) {
throw new Exception(
pht('Trying to refund a charge which is already refunding!'));
}
$refund_charge->save();
$charge->setRefundingPHID($refund_charge->getPHID());
$charge->save();
$charge->endReadLocking();
$charge->saveTransaction();
return $refund_charge;
}
public function didRefundCharge(
PhortuneCharge $charge,
PhortuneCharge $refund) {
$refund->setStatus(PhortuneCharge::STATUS_CHARGED);
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $charge;
$copy->reload();
if ($charge->getRefundingPHID() !== $refund->getPHID()) {
throw new Exception(
pht('Charge is in the wrong refunding state!'));
}
$charge->setRefundingPHID(null);
// NOTE: There's some trickiness here to get the signs right. Both
// these values are positive but the refund has a negative value.
$total_refunded = $charge
->getAmountRefundedAsCurrency()
->add($refund->getAmountAsCurrency()->negate());
$charge->setAmountRefundedAsCurrency($total_refunded);
$charge->save();
$refund->save();
$this->endReadLocking();
$this->saveTransaction();
$amount = $refund->getAmountAsCurrency()->negate();
foreach ($this->purchases as $purchase) {
$purchase->getProduct()->didRefundProduct($purchase, $amount);
}
return $this;
}
public function didFailRefund(
PhortuneCharge $charge,
PhortuneCharge $refund) {
$refund->setStatus(PhortuneCharge::STATUS_FAILED);
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $charge;
$copy->reload();
if ($charge->getRefundingPHID() !== $refund->getPHID()) {
throw new Exception(
pht('Charge is in the wrong refunding state!'));
}
$charge->setRefundingPHID(null);
$charge->save();
$refund->save();
$this->endReadLocking();
$this->saveTransaction();
}
private function recordCartTransaction($type) {
$omnipotent_user = PhabricatorUser::getOmnipotentUser();
$phortune_phid = id(new PhabricatorPhortuneApplication())->getPHID();
$xactions = array();
$xactions[] = id(new PhortuneCartTransaction())
->setTransactionType($type)
->setNewValue(true);
$content_source = PhabricatorContentSource::newForSource(
PhabricatorPhortuneContentSource::SOURCECONST);
$editor = id(new PhortuneCartEditor())
->setActor($omnipotent_user)
->setActingAsPHID($phortune_phid)
->setContentSource($content_source)
->setContinueOnMissingFields(true)
->setContinueOnNoEffect(true);
$editor->applyTransactions($this, $xactions);
}
public function getName() {
return $this->getImplementation()->getName($this);
}
public function getDoneURI() {
return $this->getImplementation()->getDoneURI($this);
}
public function getDoneActionName() {
return $this->getImplementation()->getDoneActionName($this);
}
public function getCancelURI() {
return $this->getImplementation()->getCancelURI($this);
}
public function getDescription() {
return $this->getImplementation()->getDescription($this);
}
public function getDetailURI(PhortuneMerchant $authority = null) {
if ($authority) {
$prefix = 'merchant/'.$authority->getID().'/';
} else {
$prefix = '';
}
return '/phortune/'.$prefix.'cart/'.$this->getID().'/';
}
public function getCheckoutURI() {
return '/phortune/cart/'.$this->getID().'/checkout/';
}
public function canCancelOrder() {
try {
$this->assertCanCancelOrder();
return true;
} catch (Exception $ex) {
return false;
}
}
public function canRefundOrder() {
try {
$this->assertCanRefundOrder();
return true;
} catch (Exception $ex) {
return false;
}
}
public function assertCanCancelOrder() {
switch ($this->getStatus()) {
case self::STATUS_BUILDING:
throw new Exception(
pht(
'This order can not be cancelled because the application has not '.
'finished building it yet.'));
case self::STATUS_READY:
throw new Exception(
pht(
'This order can not be cancelled because it has not been placed.'));
}
return $this->getImplementation()->assertCanCancelOrder($this);
}
public function assertCanRefundOrder() {
switch ($this->getStatus()) {
case self::STATUS_BUILDING:
throw new Exception(
pht(
'This order can not be refunded because the application has not '.
'finished building it yet.'));
case self::STATUS_READY:
throw new Exception(
pht(
'This order can not be refunded because it has not been placed.'));
}
return $this->getImplementation()->assertCanRefundOrder($this);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'metadata' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'status' => 'text32',
'cartClass' => 'text128',
'mailKey' => 'bytes20',
'subscriptionPHID' => 'phid?',
'isInvoice' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_account' => array(
'columns' => array('accountPHID'),
),
'key_merchant' => array(
'columns' => array('merchantPHID'),
),
'key_subscription' => array(
'columns' => array('subscriptionPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhortuneCartPHIDType::TYPECONST);
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
public function attachPurchases(array $purchases) {
assert_instances_of($purchases, 'PhortunePurchase');
$this->purchases = $purchases;
return $this;
}
public function getPurchases() {
return $this->assertAttached($this->purchases);
}
public function attachAccount(PhortuneAccount $account) {
$this->account = $account;
return $this;
}
public function getAccount() {
return $this->assertAttached($this->account);
}
public function attachMerchant(PhortuneMerchant $merchant) {
$this->merchant = $merchant;
return $this;
}
public function getMerchant() {
return $this->assertAttached($this->merchant);
}
public function attachImplementation(
PhortuneCartImplementation $implementation) {
$this->implementation = $implementation;
return $this;
}
public function getImplementation() {
return $this->assertAttached($this->implementation);
}
public function getTotalPriceAsCurrency() {
$prices = array();
foreach ($this->getPurchases() as $purchase) {
$prices[] = $purchase->getTotalPriceAsCurrency();
}
return PhortuneCurrency::newFromList($prices);
}
public function setMetadataValue($key, $value) {
$this->metadata[$key] = $value;
return $this;
}
public function getMetadataValue($key, $default = null) {
return idx($this->metadata, $key, $default);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhortuneCartEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhortuneCartTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
// NOTE: Both view and edit use the account's edit policy. We punch a hole
// through this for merchants, below.
return $this
->getAccount()
->getPolicy(PhabricatorPolicyCapability::CAN_EDIT);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if ($this->getAccount()->hasAutomaticCapability($capability, $viewer)) {
return true;
}
// If the viewer controls the merchant this order was placed with, they
// can view the order.
if ($capability == PhabricatorPolicyCapability::CAN_VIEW) {
$can_admin = PhabricatorPolicyFilter::hasCapability(
$viewer,
$this->getMerchant(),
PhabricatorPolicyCapability::CAN_EDIT);
if ($can_admin) {
return true;
}
}
return false;
}
public function describeAutomaticCapability($capability) {
return array(
pht('Orders inherit the policies of the associated account.'),
pht('The merchant you placed an order with can review and manage it.'),
);
}
}
diff --git a/src/applications/phortune/storage/PhortunePaymentMethod.php b/src/applications/phortune/storage/PhortunePaymentMethod.php
index 8044168ba..1712d3f97 100644
--- a/src/applications/phortune/storage/PhortunePaymentMethod.php
+++ b/src/applications/phortune/storage/PhortunePaymentMethod.php
@@ -1,157 +1,161 @@
<?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();
}
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/phortune/worker/PhortuneSubscriptionWorker.php b/src/applications/phortune/worker/PhortuneSubscriptionWorker.php
index 097fb54dd..d05aacbb7 100644
--- a/src/applications/phortune/worker/PhortuneSubscriptionWorker.php
+++ b/src/applications/phortune/worker/PhortuneSubscriptionWorker.php
@@ -1,237 +1,241 @@
<?php
final class PhortuneSubscriptionWorker extends PhabricatorWorker {
protected function doWork() {
$subscription = $this->loadSubscription();
$range = $this->getBillingPeriodRange($subscription);
list($last_epoch, $next_epoch) = $range;
$should_invoice = $subscription->shouldInvoiceForBillingPeriod(
$last_epoch,
$next_epoch);
if (!$should_invoice) {
return;
}
$currency = $subscription->getCostForBillingPeriodAsCurrency(
$last_epoch,
$next_epoch);
if (!$currency->isPositive()) {
return;
}
$account = $subscription->getAccount();
$merchant = $subscription->getMerchant();
$viewer = PhabricatorUser::getOmnipotentUser();
$product = id(new PhortuneProductQuery())
->setViewer($viewer)
->withClassAndRef('PhortuneSubscriptionProduct', $subscription->getPHID())
->executeOne();
$cart_implementation = id(new PhortuneSubscriptionCart())
->setSubscription($subscription);
// TODO: This isn't really ideal. It would be better to use an application
// actor than a fairly arbitrary account member.
// However, for now, some of the stuff later in the pipeline requires a
// valid actor with a real PHID. The subscription should eventually be
// able to create these invoices "as" the application it is acting on
// behalf of.
$members = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withPHIDs($account->getMemberPHIDs())
->execute();
$actor = null;
foreach ($members as $member) {
// Don't act as a disabled user. If all of the users on the account are
// disabled this means we won't charge the subscription, but that's
// probably correct since it means no one can cancel or pay it anyway.
if ($member->getIsDisabled()) {
continue;
}
// For now, just pick the first valid user we encounter as the actor.
$actor = $member;
break;
}
if (!$actor) {
throw new Exception(pht('Failed to load actor to bill subscription!'));
}
$cart = $account->newCart($actor, $cart_implementation, $merchant);
$purchase = $cart->newPurchase($actor, $product);
$purchase
->setBasePriceAsCurrency($currency)
->setMetadataValue('subscriptionPHID', $subscription->getPHID())
->setMetadataValue('epoch.start', $last_epoch)
->setMetadataValue('epoch.end', $next_epoch)
->save();
$cart
->setSubscriptionPHID($subscription->getPHID())
->setIsInvoice(1)
->save();
$cart->activateCart();
try {
$issues = $this->chargeSubscription($actor, $subscription, $cart);
} catch (Exception $ex) {
$issues = array(
pht(
'There was a technical error while trying to automatically bill '.
'this subscription: %s',
$ex),
);
}
if (!$issues) {
// We're all done; charging the cart sends a billing email as a side
// effect.
return;
}
// We're shoving this through the CartEditor because it has all the logic
// for sending mail about carts. This doesn't really affect the state of
// the cart, but reduces the amount of code duplication.
$xactions = array();
$xactions[] = id(new PhortuneCartTransaction())
->setTransactionType(PhortuneCartTransaction::TYPE_INVOICED)
->setNewValue(true);
$content_source = PhabricatorContentSource::newForSource(
PhabricatorPhortuneContentSource::SOURCECONST);
$acting_phid = id(new PhabricatorPhortuneApplication())->getPHID();
$editor = id(new PhortuneCartEditor())
->setActor($viewer)
->setActingAsPHID($acting_phid)
->setContentSource($content_source)
->setContinueOnMissingFields(true)
->setInvoiceIssues($issues)
->applyTransactions($cart, $xactions);
}
private function chargeSubscription(
PhabricatorUser $viewer,
PhortuneSubscription $subscription,
PhortuneCart $cart) {
$issues = array();
if (!$subscription->getDefaultPaymentMethodPHID()) {
$issues[] = pht(
'There is no payment method associated with this subscription, so '.
'it could not be billed automatically. Add a default payment method '.
'to enable automatic billing.');
return $issues;
}
$method = id(new PhortunePaymentMethodQuery())
->setViewer($viewer)
->withPHIDs(array($subscription->getDefaultPaymentMethodPHID()))
+ ->withStatuses(
+ array(
+ PhortunePaymentMethod::STATUS_ACTIVE,
+ ))
->executeOne();
if (!$method) {
$issues[] = pht(
'The payment method associated with this subscription is invalid '.
'or out of date, so it could not be automatically billed. Update '.
'the default payment method to enable automatic billing.');
return $issues;
}
$provider = $method->buildPaymentProvider();
$charge = $cart->willApplyCharge($viewer, $provider, $method);
try {
$provider->applyCharge($method, $charge);
} catch (Exception $ex) {
$cart->didFailCharge($charge);
$issues[] = pht(
'Automatic billing failed: %s',
$ex->getMessage());
return $issues;
}
$cart->didApplyCharge($charge);
}
/**
* Load the subscription to generate an invoice for.
*
* @return PhortuneSubscription The subscription to invoice.
*/
private function loadSubscription() {
$viewer = PhabricatorUser::getOmnipotentUser();
$data = $this->getTaskData();
$subscription_phid = idx($data, 'subscriptionPHID');
$subscription = id(new PhortuneSubscriptionQuery())
->setViewer($viewer)
->withPHIDs(array($subscription_phid))
->executeOne();
if (!$subscription) {
throw new PhabricatorWorkerPermanentFailureException(
pht(
'Failed to load subscription with PHID "%s".',
$subscription_phid));
}
return $subscription;
}
/**
* Get the start and end epoch timestamps for this billing period.
*
* @param PhortuneSubscription The subscription being billed.
* @return pair<int, int> Beginning and end of the billing range.
*/
private function getBillingPeriodRange(PhortuneSubscription $subscription) {
$data = $this->getTaskData();
$last_epoch = idx($data, 'trigger.last-epoch');
if (!$last_epoch) {
// If this is the first time the subscription is firing, use the
// creation date as the start of the billing period.
$last_epoch = $subscription->getDateCreated();
}
$this_epoch = idx($data, 'trigger.this-epoch');
if (!$last_epoch || !$this_epoch) {
throw new PhabricatorWorkerPermanentFailureException(
pht('Subscription is missing billing period information.'));
}
$period_length = ($this_epoch - $last_epoch);
if ($period_length <= 0) {
throw new PhabricatorWorkerPermanentFailureException(
pht(
'Subscription has invalid billing period.'));
}
if (empty($data['manual'])) {
if (PhabricatorTime::getNow() < $this_epoch) {
throw new Exception(
pht(
'Refusing to generate a subscription invoice for a billing period '.
'which ends in the future.'));
}
}
return array($last_epoch, $this_epoch);
}
}
diff --git a/src/applications/phriction/conduit/PhrictionContentSearchConduitAPIMethod.php b/src/applications/phriction/conduit/PhrictionContentSearchConduitAPIMethod.php
new file mode 100644
index 000000000..2f4013078
--- /dev/null
+++ b/src/applications/phriction/conduit/PhrictionContentSearchConduitAPIMethod.php
@@ -0,0 +1,18 @@
+<?php
+
+final class PhrictionContentSearchConduitAPIMethod
+ extends PhabricatorSearchEngineAPIMethod {
+
+ public function getAPIMethodName() {
+ return 'phriction.content.search';
+ }
+
+ public function newSearchEngine() {
+ return new PhrictionContentSearchEngine();
+ }
+
+ public function getMethodSummary() {
+ return pht('Read information about Phriction document history.');
+ }
+
+}
diff --git a/src/applications/phriction/conduit/PhrictionCreateConduitAPIMethod.php b/src/applications/phriction/conduit/PhrictionCreateConduitAPIMethod.php
index fc28b53b6..300dbbfa8 100644
--- a/src/applications/phriction/conduit/PhrictionCreateConduitAPIMethod.php
+++ b/src/applications/phriction/conduit/PhrictionCreateConduitAPIMethod.php
@@ -1,71 +1,71 @@
<?php
final class PhrictionCreateConduitAPIMethod extends PhrictionConduitAPIMethod {
public function getAPIMethodName() {
return 'phriction.create';
}
public function getMethodDescription() {
return pht('Create a Phriction document.');
}
protected function defineParamTypes() {
return array(
'slug' => 'required string',
'title' => 'required string',
'content' => 'required string',
'description' => 'optional string',
);
}
protected function defineReturnType() {
return 'nonempty dict';
}
protected function execute(ConduitAPIRequest $request) {
$slug = $request->getValue('slug');
if (!strlen($slug)) {
throw new Exception(pht('No such document.'));
}
$doc = id(new PhrictionDocumentQuery())
->setViewer($request->getUser())
->withSlugs(array(PhabricatorSlug::normalize($slug)))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if ($doc) {
throw new Exception(pht('Document already exists!'));
}
$doc = PhrictionDocument::initializeNewDocument(
$request->getUser(),
$slug);
$xactions = array();
$xactions[] = id(new PhrictionTransaction())
->setTransactionType(PhrictionDocumentTitleTransaction::TRANSACTIONTYPE)
->setNewValue($request->getValue('title'));
$xactions[] = id(new PhrictionTransaction())
->setTransactionType(PhrictionDocumentContentTransaction::TRANSACTIONTYPE)
->setNewValue($request->getValue('content'));
$editor = id(new PhrictionTransactionEditor())
->setActor($request->getUser())
->setContentSource($request->newContentSource())
->setContinueOnNoEffect(true)
- ->setDescription($request->getValue('description'));
+ ->setDescription((string)$request->getValue('description'));
try {
$editor->applyTransactions($doc, $xactions);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
// TODO - some magical hotness via T5873
throw $ex;
}
return $this->buildDocumentInfoDictionary($doc);
}
}
diff --git a/src/applications/phriction/conduit/PhrictionDocumentSearchConduitAPIMethod.php b/src/applications/phriction/conduit/PhrictionDocumentSearchConduitAPIMethod.php
new file mode 100644
index 000000000..f51faede5
--- /dev/null
+++ b/src/applications/phriction/conduit/PhrictionDocumentSearchConduitAPIMethod.php
@@ -0,0 +1,18 @@
+<?php
+
+final class PhrictionDocumentSearchConduitAPIMethod
+ extends PhabricatorSearchEngineAPIMethod {
+
+ public function getAPIMethodName() {
+ return 'phriction.document.search';
+ }
+
+ public function newSearchEngine() {
+ return new PhrictionDocumentSearchEngine();
+ }
+
+ public function getMethodSummary() {
+ return pht('Read information about Phriction documents.');
+ }
+
+}
diff --git a/src/applications/phriction/conduit/PhrictionEditConduitAPIMethod.php b/src/applications/phriction/conduit/PhrictionEditConduitAPIMethod.php
index 70c02d376..9d97ed7f8 100644
--- a/src/applications/phriction/conduit/PhrictionEditConduitAPIMethod.php
+++ b/src/applications/phriction/conduit/PhrictionEditConduitAPIMethod.php
@@ -1,67 +1,67 @@
<?php
final class PhrictionEditConduitAPIMethod extends PhrictionConduitAPIMethod {
public function getAPIMethodName() {
return 'phriction.edit';
}
public function getMethodDescription() {
return pht('Update a Phriction document.');
}
protected function defineParamTypes() {
return array(
'slug' => 'required string',
'title' => 'optional string',
'content' => 'optional string',
'description' => 'optional string',
);
}
protected function defineReturnType() {
return 'nonempty dict';
}
protected function execute(ConduitAPIRequest $request) {
$slug = $request->getValue('slug');
$doc = id(new PhrictionDocumentQuery())
->setViewer($request->getUser())
->withSlugs(array(PhabricatorSlug::normalize($slug)))
->needContent(true)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$doc) {
throw new Exception(pht('No such document.'));
}
$xactions = array();
$xactions[] = id(new PhrictionTransaction())
->setTransactionType(PhrictionDocumentTitleTransaction::TRANSACTIONTYPE)
->setNewValue($request->getValue('title'));
$xactions[] = id(new PhrictionTransaction())
->setTransactionType(PhrictionDocumentContentTransaction::TRANSACTIONTYPE)
->setNewValue($request->getValue('content'));
$editor = id(new PhrictionTransactionEditor())
->setActor($request->getUser())
->setContentSource($request->newContentSource())
->setContinueOnNoEffect(true)
- ->setDescription($request->getValue('description'));
+ ->setDescription((string)$request->getValue('description'));
try {
$editor->applyTransactions($doc, $xactions);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
// TODO - some magical hotness via T5873
throw $ex;
}
return $this->buildDocumentInfoDictionary($doc);
}
}
diff --git a/src/applications/phriction/conduit/PhrictionHistoryConduitAPIMethod.php b/src/applications/phriction/conduit/PhrictionHistoryConduitAPIMethod.php
index 454af2934..f03042059 100644
--- a/src/applications/phriction/conduit/PhrictionHistoryConduitAPIMethod.php
+++ b/src/applications/phriction/conduit/PhrictionHistoryConduitAPIMethod.php
@@ -1,53 +1,63 @@
<?php
final class PhrictionHistoryConduitAPIMethod extends PhrictionConduitAPIMethod {
public function getAPIMethodName() {
return 'phriction.history';
}
public function getMethodDescription() {
return pht('Retrieve history about a Phriction document.');
}
+ 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 "phriction.content.search" instead.');
+ }
+
protected function defineParamTypes() {
return array(
'slug' => 'required string',
);
}
protected function defineReturnType() {
return 'nonempty list';
}
protected function defineErrorTypes() {
return array(
'ERR-BAD-DOCUMENT' => pht('No such document exists.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$slug = $request->getValue('slug');
$doc = id(new PhrictionDocumentQuery())
->setViewer($request->getUser())
->withSlugs(array(PhabricatorSlug::normalize($slug)))
->executeOne();
if (!$doc) {
throw new ConduitException('ERR-BAD-DOCUMENT');
}
$content = id(new PhrictionContent())->loadAllWhere(
'documentID = %d ORDER BY version DESC',
$doc->getID());
$results = array();
foreach ($content as $version) {
$results[] = $this->buildDocumentContentDictionary(
$doc,
$version);
}
return $results;
}
}
diff --git a/src/applications/phriction/conduit/PhrictionInfoConduitAPIMethod.php b/src/applications/phriction/conduit/PhrictionInfoConduitAPIMethod.php
index 360b3e7b9..d6da3d6ae 100644
--- a/src/applications/phriction/conduit/PhrictionInfoConduitAPIMethod.php
+++ b/src/applications/phriction/conduit/PhrictionInfoConduitAPIMethod.php
@@ -1,44 +1,54 @@
<?php
final class PhrictionInfoConduitAPIMethod extends PhrictionConduitAPIMethod {
public function getAPIMethodName() {
return 'phriction.info';
}
public function getMethodDescription() {
return pht('Retrieve information about a Phriction document.');
}
+ 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 "phriction.document.search" instead.');
+ }
+
protected function defineParamTypes() {
return array(
'slug' => 'required string',
);
}
protected function defineReturnType() {
return 'nonempty dict';
}
protected function defineErrorTypes() {
return array(
'ERR-BAD-DOCUMENT' => pht('No such document exists.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$slug = $request->getValue('slug');
$document = id(new PhrictionDocumentQuery())
->setViewer($request->getUser())
->withSlugs(array(PhabricatorSlug::normalize($slug)))
->needContent(true)
->executeOne();
if (!$document) {
throw new ConduitException('ERR-BAD-DOCUMENT');
}
return $this->buildDocumentInfoDictionary($document);
}
}
diff --git a/src/applications/phriction/constants/PhrictionDocumentStatus.php b/src/applications/phriction/constants/PhrictionDocumentStatus.php
index 11e75ec8a..1830f3ec9 100644
--- a/src/applications/phriction/constants/PhrictionDocumentStatus.php
+++ b/src/applications/phriction/constants/PhrictionDocumentStatus.php
@@ -1,21 +1,63 @@
<?php
-final class PhrictionDocumentStatus extends PhrictionConstants {
+final class PhrictionDocumentStatus
+ extends PhabricatorObjectStatus {
- const STATUS_EXISTS = 0;
- const STATUS_DELETED = 1;
- const STATUS_MOVED = 2;
- const STATUS_STUB = 3;
+ const STATUS_EXISTS = 'active';
+ const STATUS_DELETED = 'deleted';
+ const STATUS_MOVED = 'moved';
+ const STATUS_STUB = 'stub';
public static function getConduitConstant($const) {
static $map = array(
self::STATUS_EXISTS => 'exists',
self::STATUS_DELETED => 'deleted',
self::STATUS_MOVED => 'moved',
self::STATUS_STUB => 'stubbed',
);
return idx($map, $const, 'unknown');
}
+ public static function newStatusObject($key) {
+ return new self($key, id(new self())->getStatusSpecification($key));
+ }
+
+ public static function getStatusMap() {
+ $map = id(new self())->getStatusSpecifications();
+ return ipull($map, 'name', 'key');
+ }
+
+ public function isActive() {
+ return ($this->getKey() == self::STATUS_EXISTS);
+ }
+
+ protected function newStatusSpecifications() {
+ return array(
+ array(
+ 'key' => self::STATUS_EXISTS,
+ 'name' => pht('Active'),
+ 'icon' => 'fa-file-text-o',
+ 'color' => 'bluegrey',
+ ),
+ array(
+ 'key' => self::STATUS_DELETED,
+ 'name' => pht('Deleted'),
+ 'icon' => 'fa-file-text-o',
+ 'color' => 'grey',
+ ),
+ array(
+ 'key' => self::STATUS_MOVED,
+ 'name' => pht('Moved'),
+ 'icon' => 'fa-arrow-right',
+ 'color' => 'grey',
+ ),
+ array(
+ 'key' => self::STATUS_STUB,
+ 'name' => pht('Stub'),
+ 'icon' => 'fa-file-text-o',
+ 'color' => 'grey',
+ ),
+ );
+ }
}
diff --git a/src/applications/phriction/controller/PhrictionController.php b/src/applications/phriction/controller/PhrictionController.php
index 277692334..51ffbcab1 100644
--- a/src/applications/phriction/controller/PhrictionController.php
+++ b/src/applications/phriction/controller/PhrictionController.php
@@ -1,94 +1,94 @@
<?php
abstract class PhrictionController extends PhabricatorController {
public function buildSideNavView($for_app = false) {
$user = $this->getRequest()->getUser();
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI($this->getApplicationURI()));
if ($for_app) {
$nav->addFilter('create', pht('New Document'));
$nav->addFilter('/phriction/', pht('Index'));
}
- id(new PhrictionSearchEngine())
+ id(new PhrictionDocumentSearchEngine())
->setViewer($user)
->addNavigationItems($nav->getMenu());
$nav->selectFilter(null);
return $nav;
}
public function buildApplicationMenu() {
return $this->buildSideNavView(true)->getMenu();
}
protected function buildApplicationCrumbs() {
$crumbs = parent::buildApplicationCrumbs();
if (get_class($this) != 'PhrictionListController') {
$crumbs->addAction(
id(new PHUIListItemView())
->setName(pht('Index'))
->setHref('/phriction/')
->setIcon('fa-home'));
}
$crumbs->addAction(
id(new PHUIListItemView())
->setName(pht('New Document'))
->setHref('/phriction/new/?slug='.$this->getDocumentSlug())
->setWorkflow(true)
->setIcon('fa-plus-square'));
return $crumbs;
}
public function renderBreadcrumbs($slug) {
$ancestor_handles = array();
$ancestral_slugs = PhabricatorSlug::getAncestry($slug);
$ancestral_slugs[] = $slug;
if ($ancestral_slugs) {
$empty_slugs = array_fill_keys($ancestral_slugs, null);
$ancestors = id(new PhrictionDocumentQuery())
->setViewer($this->getRequest()->getUser())
->withSlugs($ancestral_slugs)
->execute();
$ancestors = mpull($ancestors, null, 'getSlug');
$ancestor_phids = mpull($ancestors, 'getPHID');
$handles = array();
if ($ancestor_phids) {
$handles = $this->loadViewerHandles($ancestor_phids);
}
$ancestor_handles = array();
foreach ($ancestral_slugs as $slug) {
if (isset($ancestors[$slug])) {
$ancestor_handles[] = $handles[$ancestors[$slug]->getPHID()];
} else {
$handle = new PhabricatorObjectHandle();
$handle->setName(PhabricatorSlug::getDefaultTitle($slug));
$handle->setURI(PhrictionDocument::getSlugURI($slug));
$ancestor_handles[] = $handle;
}
}
}
$breadcrumbs = array();
foreach ($ancestor_handles as $ancestor_handle) {
$breadcrumbs[] = id(new PHUICrumbView())
->setName($ancestor_handle->getName())
->setHref($ancestor_handle->getUri());
}
return $breadcrumbs;
}
protected function getDocumentSlug() {
return '';
}
}
diff --git a/src/applications/phriction/controller/PhrictionDiffController.php b/src/applications/phriction/controller/PhrictionDiffController.php
index b66a3b309..f443ad657 100644
--- a/src/applications/phriction/controller/PhrictionDiffController.php
+++ b/src/applications/phriction/controller/PhrictionDiffController.php
@@ -1,257 +1,258 @@
<?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 PhrictionContent())->loadAllWhere(
- 'documentID = %d AND version IN (%Ld)',
- $document->getID(),
- array($l, $r));
+ $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);
$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(
PhrictionContent $content,
PhrictionContent $current) {
$document_id = $content->getDocumentID();
$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 e455bd86d..8377cf7d2 100644
--- a/src/applications/phriction/controller/PhrictionDocumentController.php
+++ b/src/applications/phriction/controller/PhrictionDocumentController.php
@@ -1,490 +1,498 @@
<?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');
- $document = id(new PhrictionDocumentQuery())
- ->setViewer($viewer)
- ->withSlugs(array($slug))
- ->executeOne();
-
$version_note = null;
$core_content = '';
$move_notice = '';
$properties = null;
$content = null;
$toc = null;
+ $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');
if ($version) {
- $content = id(new PhrictionContent())->loadOneWhere(
- 'documentID = %d AND version = %d',
- $document->getID(),
- $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()) {
- $vdate = phabricator_datetime($content->getDateCreated(), $viewer);
- $version_note = new PHUIInfoView();
- $version_note->setSeverity(PHUIInfoView::SEVERITY_NOTICE);
- $version_note->appendChild(
- pht('You are viewing an older version of this document, as it '.
- 'appeared on %s.', $vdate));
+ $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)));
}
} else {
- $content = id(new PhrictionContent())->load($document->getContentID());
+ $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) {
- $core_content = $content->renderContent($viewer);
- $toc = $this->getToc($content);
+ $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);
$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);
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())
->setHeader($header)
->setToc($toc)
->appendChild(
array(
$version_note,
$move_notice,
$core_content,
));
return $this->newPage()
->setTitle($page_title)
->setCrumbs($crumbs)
->setPageObjectPHIDs(array($document->getPHID()))
->appendChild(array(
$page_content,
$prop_list,
$children,
));
}
private function buildPropertyListView(
PhrictionDocument $document,
PhrictionContent $content,
$slug) {
$viewer = $this->getRequest()->getUser();
$view = id(new PHUIPropertyListView())
->setUser($viewer)
->setObject($document);
$view->addProperty(
pht('Last Author'),
$viewer->renderHandle($content->getAuthorPHID()));
return $view;
}
private function buildActionView(
PhabricatorUser $viewer,
PhrictionDocument $document) {
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$document,
PhabricatorPolicyCapability::CAN_EDIT);
$slug = PhabricatorSlug::normalize($this->slug);
$action_view = id(new PhabricatorActionListView())
->setUser($viewer)
->setObject($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(
id(new PhabricatorActionView())
->setName(pht('Edit Document'))
->setDisabled(!$can_edit)
->setIcon('fa-pencil')
->setHref('/phriction/edit/'.$document->getID().'/'));
if ($document->getStatus() == PhrictionDocumentStatus::STATUS_EXISTS) {
$action_view->addAction(
id(new PhabricatorActionView())
->setName(pht('Move Document'))
->setDisabled(!$can_edit)
->setIcon('fa-arrows')
->setHref('/phriction/move/'.$document->getID().'/')
->setWorkflow(true));
$action_view->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(
id(new PhabricatorActionView())
->setName(pht('Printable Page'))
->setIcon('fa-print')
->setOpenInNewWindow(true)
->setHref($print_uri));
return $action_view;
}
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(PhrictionContent $content) {
- $toc = $content->getRenderedTableOfContents();
+ 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 0ac3fc35b..e7e2b40d8 100644
--- a/src/applications/phriction/controller/PhrictionEditController.php
+++ b/src/applications/phriction/controller/PhrictionEditController.php
@@ -1,324 +1,325 @@
<?php
final class PhrictionEditController
extends PhrictionController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$current_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();
$revert = $request->getInt('revert');
if ($revert) {
- $content = id(new PhrictionContent())->loadOneWhere(
- 'documentID = %d AND version = %d',
- $document->getID(),
- $revert);
+ $content = id(new PhrictionContentQuery())
+ ->setViewer($viewer)
+ ->withDocumentPHIDs(array($document->getPHID()))
+ ->withVersions(array($revert))
+ ->executeOne();
if (!$content) {
return new Aphront404Response();
}
} else {
$content = $document->getContent();
}
} 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();
$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);
}
if ($request->isFormPost()) {
$title = $request->getStr('title');
$content_text = $request->getStr('content');
$notes = $request->getStr('description');
$current_version = $request->getInt('contentVersion');
$v_view = $request->getStr('viewPolicy');
$v_edit = $request->getStr('editPolicy');
$v_cc = $request->getArr('cc');
$v_projects = $request->getArr('projects');
$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_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);
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);
}
}
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('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())
->setName('viewPolicy')
->setPolicyObject($document)
->setCapability($view_capability)
->setPolicies($policies)
->setCaption(
$document->describeAutomaticCapability($view_capability)))
->appendChild(
id(new AphrontFormPolicyControl())
->setName('editPolicy')
->setPolicyObject($document)
->setCapability($edit_capability)
->setPolicies($policies)
->setCaption(
$document->describeAutomaticCapability($edit_capability)))
->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,
));
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 9b0d3188a..432b1dfde 100644
--- a/src/applications/phriction/controller/PhrictionHistoryController.php
+++ b/src/applications/phriction/controller/PhrictionHistoryController.php
@@ -1,179 +1,176 @@
<?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 = new PHUIPagerView();
- $pager->setOffset($request->getInt('page'));
- $pager->setURI($request->getRequestURI(), 'page');
-
- $history = id(new PhrictionContent())->loadAllWhere(
- 'documentID = %d ORDER BY version DESC LIMIT %d, %d',
- $document->getID(),
- $pager->getOffset(),
- $pager->getPageSize() + 1);
- $history = $pager->sliceResults($history);
+ $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);
$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());
}
$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';
break;
case PhrictionChangeType::CHANGE_MOVE_AWAY:
$color = 'orange';
break;
case PhrictionChangeType::CHANGE_STUB:
$color = 'green';
break;
default:
throw new Exception(pht('Unknown change type!'));
break;
}
$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());
}
if ($vs_previous) {
$item->addIcon(
'fa-reply',
pht('Show Change'),
array(
'href' => $vs_previous,
));
} else {
$item->addIcon(
'fa-reply grey',
phutil_tag('em', array(), pht('No previous change')));
}
if ($vs_head) {
$item->addIcon(
'fa-reply-all',
pht('Show Later Changes'),
array(
'href' => $vs_head,
));
} else {
$item->addIcon(
'fa-reply-all grey',
phutil_tag('em', array(), pht('No later changes')));
}
$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/PhrictionListController.php b/src/applications/phriction/controller/PhrictionListController.php
index 44ef95f17..7da29b297 100644
--- a/src/applications/phriction/controller/PhrictionListController.php
+++ b/src/applications/phriction/controller/PhrictionListController.php
@@ -1,21 +1,21 @@
<?php
final class PhrictionListController
extends PhrictionController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$querykey = $request->getURIData('queryKey');
$controller = id(new PhabricatorApplicationSearchController())
->setQueryKey($querykey)
- ->setSearchEngine(new PhrictionSearchEngine())
+ ->setSearchEngine(new PhrictionDocumentSearchEngine())
->setNavigation($this->buildSideNavView());
return $this->delegateToController($controller);
}
}
diff --git a/src/applications/phriction/editor/PhrictionTransactionEditor.php b/src/applications/phriction/editor/PhrictionTransactionEditor.php
index fcc9fe047..e6bf46c15 100644
--- a/src/applications/phriction/editor/PhrictionTransactionEditor.php
+++ b/src/applications/phriction/editor/PhrictionTransactionEditor.php
@@ -1,621 +1,621 @@
<?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(
PhabricatorLiskDAO $object,
array $xactions) {
$this->setOldContent($object->getContent());
$this->setNewContent($this->buildNewContentTemplate($object));
}
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();
$object->setContentID($content->getID());
$object->save();
$object->attachContent($content);
}
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(
- $object->getContent()->getAuthorPHID(),
$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) {
- $id = $object->getID();
$title = $object->getContent()->getTitle();
return id(new PhabricatorMetaMTAMail())
- ->setSubject($title)
- ->addHeader('Thread-Topic', $object->getPHID());
+ ->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())) {
$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) {
$new_content = id(new PhrictionContent())
->setSlug($document->getSlug())
->setAuthorPHID($this->getActor()->getPHID())
->setChangeType(PhrictionChangeType::CHANGE_EDIT)
->setTitle($this->getOldContent()->getTitle())
- ->setContent($this->getOldContent()->getContent());
+ ->setContent($this->getOldContent()->getContent())
+ ->setDescription('');
+
if (strlen($this->getDescription())) {
$new_content->setDescription($this->getDescription());
}
+
$new_content->setVersion($this->getOldContent()->getVersion() + 1);
return $new_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/engineextension/PhrictionContentSearchEngineAttachment.php b/src/applications/phriction/engineextension/PhrictionContentSearchEngineAttachment.php
new file mode 100644
index 000000000..3321f23eb
--- /dev/null
+++ b/src/applications/phriction/engineextension/PhrictionContentSearchEngineAttachment.php
@@ -0,0 +1,31 @@
+<?php
+
+final class PhrictionContentSearchEngineAttachment
+ extends PhabricatorSearchEngineAttachment {
+
+ public function getAttachmentName() {
+ return pht('Document Content');
+ }
+
+ public function getAttachmentDescription() {
+ return pht('Get the content of documents or document histories.');
+ }
+
+ public function getAttachmentForObject($object, $data, $spec) {
+ if ($object instanceof PhrictionDocument) {
+ $content = $object->getContent();
+ } else {
+ $content = $object;
+ }
+
+ return array(
+ 'title' => $content->getTitle(),
+ 'path' => $content->getSlug(),
+ 'authorPHID' => $content->getAuthorPHID(),
+ 'content' => array(
+ 'raw' => $content->getContent(),
+ ),
+ );
+ }
+
+}
diff --git a/src/applications/phriction/engineextension/PhrictionDatasourceEngineExtension.php b/src/applications/phriction/engineextension/PhrictionDatasourceEngineExtension.php
new file mode 100644
index 000000000..af262d541
--- /dev/null
+++ b/src/applications/phriction/engineextension/PhrictionDatasourceEngineExtension.php
@@ -0,0 +1,56 @@
+<?php
+
+final class PhrictionDatasourceEngineExtension
+ extends PhabricatorDatasourceEngineExtension {
+
+ public function newQuickSearchDatasources() {
+ return array(
+ new PhrictionDocumentDatasource(),
+ );
+ }
+
+ public function newJumpURI($query) {
+ $viewer = $this->getViewer();
+
+ // Send "w" to Phriction.
+ if (preg_match('/^w\z/i', $query)) {
+ return '/w/';
+ }
+
+ // Send "w <string>" to a search for similar wiki documents.
+ $matches = null;
+ if (preg_match('/^w\s+(.+)\z/i', $query, $matches)) {
+ $raw_query = $matches[1];
+
+ $engine = id(new PhrictionDocument())
+ ->newFerretEngine();
+
+ $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;
+ }
+
+ $documents = id(new PhrictionDocumentQuery())
+ ->setViewer($viewer)
+ ->withFerretConstraint($engine, $fulltext_tokens)
+ ->execute();
+ if (count($documents) == 1) {
+ return head($documents)->getURI();
+ } else {
+ // More than one match, jump to search.
+ return urisprintf(
+ '/phriction/?order=relevance&query=%s#R',
+ $raw_query);
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/src/applications/phriction/markup/PhrictionRemarkupRule.php b/src/applications/phriction/markup/PhrictionRemarkupRule.php
index 9c5649bbb..d17de331e 100644
--- a/src/applications/phriction/markup/PhrictionRemarkupRule.php
+++ b/src/applications/phriction/markup/PhrictionRemarkupRule.php
@@ -1,198 +1,285 @@
<?php
final class PhrictionRemarkupRule extends PhutilRemarkupRule {
const KEY_RULE_PHRICTION_LINK = 'phriction.link';
public function getPriority() {
return 175.0;
}
public function apply($text) {
return preg_replace_callback(
'@\B\\[\\[([^|\\]]+)(?:\\|([^\\]]+))?\\]\\]\B@U',
array($this, 'markupDocumentLink'),
$text);
}
public function markupDocumentLink(array $matches) {
// If the link contains an anchor, separate that off first.
$parts = explode('#', trim($matches[1]), 2);
if (count($parts) == 2) {
$link = $parts[0];
$anchor = $parts[1];
} else {
$link = $parts[0];
$anchor = null;
}
// Handle relative links.
if ((substr($link, 0, 2) === './') || (substr($link, 0, 3) === '../')) {
- $base = null;
- $context = $this->getEngine()->getConfig('contextObject');
- if ($context !== null && $context instanceof PhrictionContent) {
- // Handle content when it's being rendered in document view.
- $base = $context->getSlug();
- }
- if ($context !== null && is_array($context) &&
- idx($context, 'phriction.isPreview')) {
- // Handle content when it's a preview for the Phriction editor.
- $base = idx($context, 'phriction.slug');
- }
+ $base = $this->getRelativeBaseURI();
if ($base !== null) {
$base_parts = explode('/', rtrim($base, '/'));
$rel_parts = explode('/', rtrim($link, '/'));
foreach ($rel_parts as $part) {
if ($part === '.') {
// Consume standalone dots in a relative path, and do
// nothing with them.
} else if ($part === '..') {
if (count($base_parts) > 0) {
array_pop($base_parts);
}
} else {
array_push($base_parts, $part);
}
}
$link = implode('/', $base_parts).'/';
}
}
$name = trim(idx($matches, 2, ''));
if (empty($matches[2])) {
$name = null;
}
// Link is now used for slug detection, so append a slash if one
// is needed.
$link = rtrim($link, '/').'/';
$engine = $this->getEngine();
$token = $engine->storeText('x');
$metadata = $engine->getTextMetadata(
self::KEY_RULE_PHRICTION_LINK,
array());
$metadata[] = array(
'token' => $token,
'link' => $link,
'anchor' => $anchor,
'explicitName' => $name,
);
$engine->setTextMetadata(self::KEY_RULE_PHRICTION_LINK, $metadata);
return $token;
}
public function didMarkupText() {
$engine = $this->getEngine();
$metadata = $engine->getTextMetadata(
self::KEY_RULE_PHRICTION_LINK,
array());
if (!$metadata) {
return;
}
+ $viewer = $engine->getConfig('viewer');
+
$slugs = ipull($metadata, 'link');
- foreach ($slugs as $key => $slug) {
- $slugs[$key] = PhabricatorSlug::normalize($slug);
+
+ $load_map = array();
+ foreach ($slugs as $key => $raw_slug) {
+ $lookup = PhabricatorSlug::normalize($raw_slug);
+ $load_map[$lookup][] = $key;
+
+ // Also try to lookup the slug with URL decoding applied. The right
+ // way to link to a page titled "$cash" is to write "[[ $cash ]]" (and
+ // not the URL encoded form "[[ %24cash ]]"), but users may reasonably
+ // have copied URL encoded variations out of their browser location
+ // bar or be skeptical that "[[ $cash ]]" will actually work.
+
+ $lookup = phutil_unescape_uri_path_component($raw_slug);
+ $lookup = phutil_utf8ize($lookup);
+ $lookup = PhabricatorSlug::normalize($lookup);
+ $load_map[$lookup][] = $key;
}
- // We have to make two queries here to distinguish between
- // documents the user can't see, and documents that don't
- // exist.
$visible_documents = id(new PhrictionDocumentQuery())
- ->setViewer($engine->getConfig('viewer'))
- ->withSlugs($slugs)
+ ->setViewer($viewer)
+ ->withSlugs(array_keys($load_map))
->needContent(true)
->execute();
- $existant_documents = id(new PhrictionDocumentQuery())
- ->setViewer(PhabricatorUser::getOmnipotentUser())
- ->withSlugs($slugs)
- ->execute();
-
$visible_documents = mpull($visible_documents, null, 'getSlug');
- $existant_documents = mpull($existant_documents, null, 'getSlug');
+ $document_map = array();
+ foreach ($load_map as $lookup => $keys) {
+ $visible = idx($visible_documents, $lookup);
+ if (!$visible) {
+ continue;
+ }
+
+ foreach ($keys as $key) {
+ $document_map[$key] = array(
+ 'visible' => true,
+ 'document' => $visible,
+ );
+ }
+
+ unset($load_map[$lookup]);
+ }
+
+ // For each document we found, remove all remaining requests for it from
+ // the load map. If we remove all requests for a slug, remove the slug.
+ // This stops us from doing unnecessary lookups on alternate names for
+ // documents we already found.
+ foreach ($load_map as $lookup => $keys) {
+ foreach ($keys as $lookup_key => $key) {
+ if (isset($document_map[$key])) {
+ unset($keys[$lookup_key]);
+ }
+ }
+
+ if (!$keys) {
+ unset($load_map[$lookup]);
+ continue;
+ }
+
+ $load_map[$lookup] = $keys;
+ }
+
+
+ // If we still have links we haven't found a document for, do another
+ // query with the omnipotent viewer so we can distinguish between pages
+ // which do not exist and pages which exist but which the viewer does not
+ // have permission to see.
+ if ($load_map) {
+ $existent_documents = id(new PhrictionDocumentQuery())
+ ->setViewer(PhabricatorUser::getOmnipotentUser())
+ ->withSlugs(array_keys($load_map))
+ ->execute();
+ $existent_documents = mpull($existent_documents, null, 'getSlug');
- foreach ($metadata as $spec) {
+ foreach ($load_map as $lookup => $keys) {
+ $existent = idx($existent_documents, $lookup);
+ if (!$existent) {
+ continue;
+ }
+
+ foreach ($keys as $key) {
+ $document_map[$key] = array(
+ 'visible' => false,
+ 'document' => null,
+ );
+ }
+ }
+ }
+
+ foreach ($metadata as $key => $spec) {
$link = $spec['link'];
$slug = PhabricatorSlug::normalize($link);
$name = $spec['explicitName'];
$class = 'phriction-link';
// If the name is something meaningful to humans, we'll render this
// in text as: "Title" <link>. Otherwise, we'll just render: <link>.
$is_interesting_name = (bool)strlen($name);
- if (idx($existant_documents, $slug) === null) {
+ $target = idx($document_map, $key, null);
+
+ if ($target === null) {
// The target document doesn't exist.
if ($name === null) {
$name = explode('/', trim($link, '/'));
$name = end($name);
}
$class = 'phriction-link-missing';
- } else if (idx($visible_documents, $slug) === null) {
+ } else if (!$target['visible']) {
// The document exists, but the user can't see it.
if ($name === null) {
$name = explode('/', trim($link, '/'));
$name = end($name);
}
$class = 'phriction-link-lock';
} else {
if ($name === null) {
// Use the title of the document if no name is set.
- $name = $visible_documents[$slug]
+ $name = $target['document']
->getContent()
->getTitle();
$is_interesting_name = true;
}
}
$uri = new PhutilURI($link);
$slug = $uri->getPath();
$slug = PhabricatorSlug::normalize($slug);
$slug = PhrictionDocument::getSlugURI($slug);
$anchor = idx($spec, 'anchor');
$href = (string)id(new PhutilURI($slug))->setFragment($anchor);
$text_mode = $this->getEngine()->isTextMode();
$mail_mode = $this->getEngine()->isHTMLMailMode();
if ($this->getEngine()->getState('toc')) {
$text = $name;
} else if ($text_mode || $mail_mode) {
$href = PhabricatorEnv::getProductionURI($href);
if ($is_interesting_name) {
$text = pht('"%s" <%s>', $name, $href);
} else {
$text = pht('<%s>', $href);
}
} else {
if ($class === 'phriction-link-lock') {
$name = array(
$this->newTag(
'i',
array(
'class' => 'phui-icon-view phui-font-fa fa-lock',
),
''),
' ',
$name,
);
}
$text = $this->newTag(
'a',
array(
'href' => $href,
'class' => $class,
),
$name);
}
$this->getEngine()->overwriteStoredText($spec['token'], $text);
}
}
+ private function getRelativeBaseURI() {
+ $context = $this->getEngine()->getConfig('contextObject');
+
+ if (!$context) {
+ return null;
+ }
+
+ // Handle content when it's a preview for the Phriction editor.
+ if (is_array($context)) {
+ if (idx($context, 'phriction.isPreview')) {
+ return idx($context, 'phriction.slug');
+ }
+ }
+
+ if ($context instanceof PhrictionContent) {
+ return $context->getSlug();
+ }
+
+ if ($context instanceof PhrictionDocument) {
+ return $context->getContent()->getSlug();
+ }
+
+ return null;
+ }
+
+
}
diff --git a/src/applications/harbormaster/phid/HarbormasterBuildLogPHIDType.php b/src/applications/phriction/phid/PhrictionContentPHIDType.php
similarity index 59%
copy from src/applications/harbormaster/phid/HarbormasterBuildLogPHIDType.php
copy to src/applications/phriction/phid/PhrictionContentPHIDType.php
index c0fba81c4..b8f39c0ef 100644
--- a/src/applications/harbormaster/phid/HarbormasterBuildLogPHIDType.php
+++ b/src/applications/phriction/phid/PhrictionContentPHIDType.php
@@ -1,37 +1,38 @@
<?php
-final class HarbormasterBuildLogPHIDType extends PhabricatorPHIDType {
+final class PhrictionContentPHIDType
+ extends PhabricatorPHIDType {
- const TYPECONST = 'HMCL';
+ const TYPECONST = 'WRDS';
public function getTypeName() {
- return pht('Build Log');
+ return pht('Phriction Content');
}
public function newObject() {
- return new HarbormasterBuildLog();
+ return new PhrictionContent();
}
public function getPHIDTypeApplicationClass() {
- return 'PhabricatorHarbormasterApplication';
+ return 'PhabricatorPhrictionApplication';
}
protected function buildQueryForObjects(
PhabricatorObjectQuery $query,
array $phids) {
- return id(new HarbormasterBuildLogQuery())
+ return id(new PhrictionContentQuery())
->withPHIDs($phids);
}
public function loadHandles(
PhabricatorHandleQuery $query,
array $handles,
array $objects) {
foreach ($handles as $phid => $handle) {
- $build_log = $objects[$phid];
+ $content = $objects[$phid];
}
}
}
diff --git a/src/applications/phriction/query/PhrictionContentQuery.php b/src/applications/phriction/query/PhrictionContentQuery.php
new file mode 100644
index 000000000..053f40cf9
--- /dev/null
+++ b/src/applications/phriction/query/PhrictionContentQuery.php
@@ -0,0 +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',
+ id(new PhrictionDocument())->getTableName());
+ }
+
+ return $joins;
+ }
+
+ protected function willFilterPage(array $contents) {
+ $document_ids = mpull($contents, 'getDocumentID');
+
+ $documents = id(new PhrictionDocumentQuery())
+ ->setViewer($this->getViewer())
+ ->setParentQuery($this)
+ ->withIDs($document_ids)
+ ->execute();
+ $documents = mpull($documents, null, 'getID');
+
+ foreach ($contents as $key => $content) {
+ $document_id = $content->getDocumentID();
+
+ $document = idx($documents, $document_id);
+ 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/PhrictionContentSearchEngine.php b/src/applications/phriction/query/PhrictionContentSearchEngine.php
new file mode 100644
index 000000000..f7cd9be2b
--- /dev/null
+++ b/src/applications/phriction/query/PhrictionContentSearchEngine.php
@@ -0,0 +1,76 @@
+<?php
+
+final class PhrictionContentSearchEngine
+ extends PhabricatorApplicationSearchEngine {
+
+ public function getResultTypeDescription() {
+ return pht('Phriction Document Content');
+ }
+
+ public function getApplicationClassName() {
+ return 'PhabricatorPhrictionApplication';
+ }
+
+ public function newQuery() {
+ return new PhrictionContentQuery();
+ }
+
+ protected function buildQueryFromParameters(array $map) {
+ $query = $this->newQuery();
+
+ if ($map['documentPHIDs']) {
+ $query->withDocumentPHIDs($map['documentPHIDs']);
+ }
+
+ if ($map['versions']) {
+ $query->withVersions($map['versions']);
+ }
+
+ return $query;
+ }
+
+ protected function buildCustomSearchFields() {
+ return array(
+ id(new PhabricatorPHIDsSearchField())
+ ->setKey('documentPHIDs')
+ ->setAliases(array('document', 'documents', 'documentPHID'))
+ ->setLabel(pht('Documents')),
+ id(new PhabricatorIDsSearchField())
+ ->setKey('versions')
+ ->setAliases(array('version')),
+ );
+ }
+
+ protected function getURI($path) {
+ // There's currently no web UI for this search interface, it exists purely
+ // to power the Conduit API.
+ throw new PhutilMethodNotImplementedException();
+ }
+
+ protected function getBuiltinQueryNames() {
+ return array(
+ 'all' => pht('All Content'),
+ );
+ }
+
+ 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 $contents,
+ PhabricatorSavedQuery $query,
+ array $handles) {
+ assert_instances_of($contents, 'PhrictionContent');
+ throw new PhutilMethodNotImplementedException();
+ }
+
+}
diff --git a/src/applications/phriction/query/PhrictionDocumentQuery.php b/src/applications/phriction/query/PhrictionDocumentQuery.php
index a38073974..d4d0f88f7 100644
--- a/src/applications/phriction/query/PhrictionDocumentQuery.php
+++ b/src/applications/phriction/query/PhrictionDocumentQuery.php
@@ -1,339 +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 $needContent;
+ private $parentPaths;
+ private $ancestorPaths;
- private $status = 'status-any';
- const STATUS_ANY = 'status-any';
- const STATUS_OPEN = 'status-open';
- const STATUS_NONSTUB = 'status-nonstub';
+ private $needContent;
- const ORDER_HIERARCHY = 'order-hierarchy';
+ 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) {
+ 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 withStatus($status) {
- $this->status = $status;
+ 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 PhrictionContent())->loadAllWhere(
- 'id IN (%Ld)',
- mpull($documents, 'getContentID'));
+ $contents = id(new PhrictionContentQuery())
+ ->setViewer($this->getViewer())
+ ->setParentQuery($this)
+ ->withIDs(mpull($documents, 'getContentID'))
+ ->execute();
+ $contents = mpull($contents, null, 'getID');
foreach ($documents as $key => $document) {
$content_id = $document->getContentID();
if (empty($contents[$content_id])) {
unset($documents[$key]);
continue;
}
$document->attachContent($contents[$content_id]);
}
}
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',
$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 (%Ld)',
+ '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);
}
- switch ($this->status) {
- case self::STATUS_OPEN:
- $where[] = qsprintf(
- $conn,
- 'd.status NOT IN (%Ld)',
- array(
- PhrictionDocumentStatus::STATUS_DELETED,
- PhrictionDocumentStatus::STATUS_MOVED,
- PhrictionDocumentStatus::STATUS_STUB,
- ));
- break;
- case self::STATUS_NONSTUB:
- $where[] = qsprintf(
- $conn,
- 'd.status NOT IN (%Ld)',
- array(
- PhrictionDocumentStatus::STATUS_MOVED,
- PhrictionDocumentStatus::STATUS_STUB,
- ));
- break;
- case self::STATUS_ANY:
- break;
- default:
- throw new Exception(pht("Unknown status '%s'!", $this->status));
+ 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 array(
+ return parent::getBuiltinOrders() + array(
self::ORDER_HIERARCHY => array(
'vector' => array('depth', 'title', 'updated'),
'name' => pht('Hierarchy'),
),
- ) + parent::getBuiltinOrders();
+ );
}
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',
'type' => 'int',
'unique' => true,
),
);
}
protected function getPagingValueMap($cursor, array $keys) {
$document = $this->loadCursorObject($cursor);
$map = array(
'id' => $document->getID(),
'depth' => $document->getDepth(),
'updated' => $document->getContentID(),
);
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/PhrictionSearchEngine.php b/src/applications/phriction/query/PhrictionDocumentSearchEngine.php
similarity index 65%
rename from src/applications/phriction/query/PhrictionSearchEngine.php
rename to src/applications/phriction/query/PhrictionDocumentSearchEngine.php
index 245500e0f..e3d962146 100644
--- a/src/applications/phriction/query/PhrictionSearchEngine.php
+++ b/src/applications/phriction/query/PhrictionDocumentSearchEngine.php
@@ -1,142 +1,160 @@
<?php
-final class PhrictionSearchEngine
+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)
- ->withStatus(PhrictionDocumentQuery::STATUS_NONSTUB);
+ ->needContent(true);
}
protected function buildQueryFromParameters(array $map) {
$query = $this->newQuery();
- if ($map['status']) {
- $query->withStatus($map['status']);
+ 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 PhabricatorSearchSelectField())
- ->setKey('status')
+ id(new PhabricatorSearchCheckboxesField())
+ ->setKey('statuses')
->setLabel(pht('Status'))
- ->setOptions($this->getStatusOptions()),
+ ->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(
- 'status', PhrictionDocumentQuery::STATUS_OPEN);
+ 'statuses',
+ array(
+ PhrictionDocumentStatus::STATUS_EXISTS,
+ ));
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
- private function getStatusOptions() {
- return array(
- PhrictionDocumentQuery::STATUS_OPEN => pht('Show Active Documents'),
- PhrictionDocumentQuery::STATUS_NONSTUB => pht('Show All Documents'),
- );
- }
-
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())
->setHref($slug_uri)
->addByline($byline)
->addIcon('none', $updated);
$item->addAttribute($slug_uri);
- switch ($document->getStatus()) {
- case PhrictionDocumentStatus::STATUS_DELETED:
- $item->setDisabled(true);
- $item->addIcon('delete', pht('Deleted'));
- break;
- case PhrictionDocumentStatus::STATUS_MOVED:
- $item->setDisabled(true);
- $item->addIcon('arrow-right', pht('Moved Away'));
- break;
+ $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/search/PhrictionDocumentFerretEngine.php b/src/applications/phriction/search/PhrictionDocumentFerretEngine.php
index 76802391e..498b9274f 100644
--- a/src/applications/phriction/search/PhrictionDocumentFerretEngine.php
+++ b/src/applications/phriction/search/PhrictionDocumentFerretEngine.php
@@ -1,18 +1,18 @@
<?php
final class PhrictionDocumentFerretEngine
extends PhabricatorFerretEngine {
public function getApplicationName() {
return 'phriction';
}
public function getScopeName() {
return 'document';
}
public function newSearchEngine() {
- return new PhrictionSearchEngine();
+ return new PhrictionDocumentSearchEngine();
}
}
diff --git a/src/applications/phriction/storage/PhrictionContent.php b/src/applications/phriction/storage/PhrictionContent.php
index 049253479..515fc6b7d 100644
--- a/src/applications/phriction/storage/PhrictionContent.php
+++ b/src/applications/phriction/storage/PhrictionContent.php
@@ -1,127 +1,139 @@
<?php
-/**
- * @task markup Markup Interface
- */
-final class PhrictionContent extends PhrictionDAO
- implements PhabricatorMarkupInterface {
+final class PhrictionContent
+ extends PhrictionDAO
+ implements
+ PhabricatorPolicyInterface,
+ PhabricatorDestructibleInterface,
+ PhabricatorConduitResultInterface {
- const MARKUP_FIELD_BODY = 'markup:body';
-
- protected $id;
protected $documentID;
protected $version;
protected $authorPHID;
protected $title;
protected $slug;
protected $content;
protected $description;
protected $changeType;
protected $changeRef;
- private $renderedTableOfContents;
-
- public function renderContent(PhabricatorUser $viewer) {
- return PhabricatorMarkupEngine::renderOneObject(
- $this,
- self::MARKUP_FIELD_BODY,
- $viewer,
- $this);
- }
+ 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?',
-
- // T6203/NULLABILITY
- // This should just be empty if not provided?
- 'description' => 'text?',
+ 'description' => 'text',
),
self::CONFIG_KEY_SCHEMA => array(
'documentID' => array(
'columns' => array('documentID', 'version'),
'unique' => true,
),
'authorPHID' => array(
'columns' => array('authorPHID'),
),
'slug' => array(
'columns' => array('slug'),
),
),
) + parent::getConfiguration();
}
+ public function getPHIDType() {
+ return PhrictionContentPHIDType::TYPECONST;
+ }
-/* -( Markup Interface )--------------------------------------------------- */
+ 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;
+ }
- /**
- * @task markup
- */
- public function getMarkupFieldKey($field) {
- $content = $this->getMarkupText($field);
- return PhabricatorMarkupEngine::digestRemarkupContent($this, $content);
+ public function getDocument() {
+ return $this->assertAttached($this->document);
}
- /**
- * @task markup
- */
- public function getMarkupText($field) {
- return $this->getContent();
+/* -( PhabricatorPolicyInterface )----------------------------------------- */
+
+
+ public function getCapabilities() {
+ return array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ );
}
+ public function getPolicy($capability) {
+ return PhabricatorPolicies::getMostOpenPolicy();
+ }
- /**
- * @task markup
- */
- public function newMarkupEngine($field) {
- return PhabricatorMarkupEngine::newPhrictionMarkupEngine();
+ public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
+ return false;
}
- /**
- * @task markup
- */
- public function didMarkupText(
- $field,
- $output,
- PhutilMarkupEngine $engine) {
+/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
- $this->renderedTableOfContents =
- PhutilRemarkupHeaderBlockRule::renderTableOfContents($engine);
- return phutil_tag(
- 'div',
- array(
- 'class' => 'phabricator-remarkup',
- ),
- $output);
+ public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
+ return array(
+ array($this->getDocument(), PhabricatorPolicyCapability::CAN_VIEW),
+ );
}
- /**
- * @task markup
- */
- public function getRenderedTableOfContents() {
- return $this->renderedTableOfContents;
+
+/* -( PhabricatorDestructibleInterface )----------------------------------- */
+
+
+ public function destroyObjectPermanently(
+ PhabricatorDestructionEngine $engine) {
+ $this->delete();
}
- /**
- * @task markup
- */
- public function shouldUseMarkupCache($field) {
- return (bool)$this->getID();
+/* -( 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 7a3e17888..729d3e4ca 100644
--- a/src/applications/phriction/storage/PhrictionDocument.php
+++ b/src/applications/phriction/storage/PhrictionDocument.php
@@ -1,267 +1,331 @@
<?php
final class PhrictionDocument extends PhrictionDAO
implements
PhabricatorPolicyInterface,
PhabricatorSubscribableInterface,
PhabricatorFlaggableInterface,
PhabricatorTokenReceiverInterface,
PhabricatorDestructibleInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface,
PhabricatorProjectInterface,
- PhabricatorApplicationTransactionInterface {
+ PhabricatorApplicationTransactionInterface,
+ PhabricatorConduitResultInterface {
protected $slug;
protected $depth;
protected $contentID;
protected $status;
protected $mailKey;
protected $viewPolicy;
protected $editPolicy;
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' => 'uint32',
+ 'status' => 'text32',
'mailKey' => 'bytes20',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'slug' => array(
'columns' => array('slug'),
'unique' => true,
),
'depth' => array(
'columns' => array('depth', 'slug'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhrictionDocumentPHIDType::TYPECONST);
}
public static function initializeNewDocument(PhabricatorUser $actor, $slug) {
$document = new PhrictionDocument();
$document->setSlug($slug);
- $content = new PhrictionContent();
+ $content = new PhrictionContent();
$content->setSlug($slug);
$default_title = PhabricatorSlug::getDefaultTitle($slug);
$content->setTitle($default_title);
$document->attachContent($content);
$parent_doc = null;
$ancestral_slugs = PhabricatorSlug::getAncestry($slug);
if ($ancestral_slugs) {
$parent = end($ancestral_slugs);
$parent_doc = id(new PhrictionDocumentQuery())
->setViewer($actor)
->withSlugs(array($parent))
->executeOne();
}
if ($parent_doc) {
$document->setViewPolicy($parent_doc->getViewPolicy());
$document->setEditPolicy($parent_doc->getEditPolicy());
} else {
$default_view_policy = PhabricatorPolicies::getMostOpenPolicy();
$document->setViewPolicy($default_view_policy);
$document->setEditPolicy(PhabricatorPolicies::POLICY_USER);
}
return $document;
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
public static function getSlugURI($slug, $type = 'document') {
static $types = array(
'document' => '/w/',
'history' => '/phriction/history/',
);
if (empty($types[$type])) {
throw new Exception(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.');
}
return null;
}
/* -( 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();
- $this->delete();
-
- $contents = id(new PhrictionContent())->loadAllWhere(
- 'documentID = %d',
- $this->getID());
+ $contents = id(new PhrictionContentQuery())
+ ->setViewer($engine->getViewer())
+ ->withDocumentPHIDs(array($this->getPHID()))
+ ->execute();
foreach ($contents as $content) {
- $content->delete();
+ $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'),
+ );
+ }
}
diff --git a/src/applications/phriction/storage/PhrictionTransaction.php b/src/applications/phriction/storage/PhrictionTransaction.php
index 2ce9d38a0..860a86be3 100644
--- a/src/applications/phriction/storage/PhrictionTransaction.php
+++ b/src/applications/phriction/storage/PhrictionTransaction.php
@@ -1,91 +1,79 @@
<?php
final class PhrictionTransaction
extends PhabricatorModularTransaction {
const MAILTAG_TITLE = 'phriction-title';
const MAILTAG_CONTENT = 'phriction-content';
const MAILTAG_DELETE = 'phriction-delete';
const MAILTAG_SUBSCRIBERS = 'phriction-subscribers';
const MAILTAG_OTHER = 'phriction-other';
public function getApplicationName() {
return 'phriction';
}
public function getApplicationTransactionType() {
return PhrictionDocumentPHIDType::TYPECONST;
}
public function getApplicationTransactionCommentObject() {
return new PhrictionTransactionComment();
}
public function getBaseTransactionClass() {
return 'PhrictionDocumentTransactionType';
}
public function getRequiredHandlePHIDs() {
$phids = parent::getRequiredHandlePHIDs();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case PhrictionDocumentMoveToTransaction::TRANSACTIONTYPE:
case PhrictionDocumentMoveAwayTransaction::TRANSACTIONTYPE:
$phids[] = $new['phid'];
break;
case PhrictionDocumentTitleTransaction::TRANSACTIONTYPE:
if ($this->getMetadataValue('stub:create:phid')) {
$phids[] = $this->getMetadataValue('stub:create:phid');
}
break;
}
return $phids;
}
public function shouldHideForMail(array $xactions) {
switch ($this->getTransactionType()) {
case PhrictionDocumentMoveToTransaction::TRANSACTIONTYPE:
case PhrictionDocumentMoveAwayTransaction::TRANSACTIONTYPE:
return true;
case PhrictionDocumentTitleTransaction::TRANSACTIONTYPE:
return $this->getMetadataValue('stub:create:phid', false);
}
return parent::shouldHideForMail($xactions);
}
- public function shouldHideForFeed() {
- switch ($this->getTransactionType()) {
- case PhrictionDocumentMoveToTransaction::TRANSACTIONTYPE:
- case PhrictionDocumentMoveAwayTransaction::TRANSACTIONTYPE:
- return true;
- case PhrictionDocumentTitleTransaction::TRANSACTIONTYPE:
- return $this->getMetadataValue('stub:create:phid', false);
- }
- return parent::shouldHideForFeed();
- }
-
-
public function getMailTags() {
$tags = array();
switch ($this->getTransactionType()) {
case PhrictionDocumentTitleTransaction::TRANSACTIONTYPE:
$tags[] = self::MAILTAG_TITLE;
break;
case PhrictionDocumentContentTransaction::TRANSACTIONTYPE:
$tags[] = self::MAILTAG_CONTENT;
break;
case PhrictionDocumentDeleteTransaction::TRANSACTIONTYPE:
$tags[] = self::MAILTAG_DELETE;
break;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$tags[] = self::MAILTAG_SUBSCRIBERS;
break;
default:
$tags[] = self::MAILTAG_OTHER;
break;
}
return $tags;
}
}
diff --git a/src/applications/phriction/typeahead/PhrictionDocumentDatasource.php b/src/applications/phriction/typeahead/PhrictionDocumentDatasource.php
new file mode 100644
index 000000000..bb9608edb
--- /dev/null
+++ b/src/applications/phriction/typeahead/PhrictionDocumentDatasource.php
@@ -0,0 +1,90 @@
+<?php
+
+final class PhrictionDocumentDatasource
+ extends PhabricatorTypeaheadDatasource {
+
+ public function getBrowseTitle() {
+ return pht('Browse Documents');
+ }
+
+ public function getPlaceholderText() {
+ return pht('Type a document name...');
+ }
+
+ public function getDatasourceApplicationClass() {
+ return 'PhabricatorPhrictionApplication';
+ }
+
+ public function loadResults() {
+ $viewer = $this->getViewer();
+
+ $raw_query = $this->getRawQuery();
+
+ $engine = id(new PhrictionDocument())
+ ->newFerretEngine();
+
+ $compiler = id(new PhutilSearchQueryCompiler())
+ ->setEnableFunctions(true);
+
+ $raw_tokens = $compiler->newTokens($raw_query);
+
+ $fulltext_tokens = array();
+ foreach ($raw_tokens as $raw_token) {
+
+ // This is a little hacky and could maybe be cleaner. We're treating
+ // every search term as though the user had entered "title:dog" insead
+ // of "dog".
+
+ $alternate_token = PhutilSearchQueryToken::newFromDictionary(
+ array(
+ 'quoted' => $raw_token->isQuoted(),
+ 'value' => $raw_token->getValue(),
+ 'operator' => PhutilSearchQueryCompiler::OPERATOR_SUBSTRING,
+ 'function' => 'title',
+ ));
+
+ $fulltext_token = id(new PhabricatorFulltextToken())
+ ->setToken($alternate_token);
+ $fulltext_tokens[] = $fulltext_token;
+ }
+
+ $documents = id(new PhrictionDocumentQuery())
+ ->setViewer($viewer)
+ ->withFerretConstraint($engine, $fulltext_tokens)
+ ->needContent(true)
+ ->execute();
+
+ $results = array();
+ foreach ($documents as $document) {
+ $content = $document->getContent();
+
+ if (!$document->isActive()) {
+ $closed = $document->getStatusDisplayName();
+ } else {
+ $closed = null;
+ }
+
+ $slug = $document->getSlug();
+ $title = $content->getTitle();
+
+ $sprite = 'phabricator-search-icon phui-font-fa phui-icon-view fa-book';
+ $autocomplete = '[[ '.$slug.' ]]';
+
+ $result = id(new PhabricatorTypeaheadResult())
+ ->setName($title)
+ ->setDisplayName($title)
+ ->setURI($document->getURI())
+ ->setPHID($document->getPHID())
+ ->setDisplayType($slug)
+ ->setPriorityType('wiki')
+ ->setImageSprite($sprite)
+ ->setAutocomplete($autocomplete)
+ ->setClosed($closed);
+
+ $results[] = $result;
+ }
+
+ return $results;
+ }
+
+}
diff --git a/src/applications/phriction/xaction/PhrictionDocumentMoveAwayTransaction.php b/src/applications/phriction/xaction/PhrictionDocumentMoveAwayTransaction.php
index c827f7337..3cd45f73b 100644
--- a/src/applications/phriction/xaction/PhrictionDocumentMoveAwayTransaction.php
+++ b/src/applications/phriction/xaction/PhrictionDocumentMoveAwayTransaction.php
@@ -1,62 +1,66 @@
<?php
final class PhrictionDocumentMoveAwayTransaction
extends PhrictionDocumentTransactionType {
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']);
}
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 a95e70ebd..bdf2c5d8f 100644
--- a/src/applications/phriction/xaction/PhrictionDocumentMoveToTransaction.php
+++ b/src/applications/phriction/xaction/PhrictionDocumentMoveToTransaction.php
@@ -1,105 +1,109 @@
<?php
final class PhrictionDocumentMoveToTransaction
extends PhrictionDocumentTransactionType {
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']);
}
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/phurl/editor/PhabricatorPhurlURLEditor.php b/src/applications/phurl/editor/PhabricatorPhurlURLEditor.php
index 439d62f84..49f290c34 100644
--- a/src/applications/phurl/editor/PhabricatorPhurlURLEditor.php
+++ b/src/applications/phurl/editor/PhabricatorPhurlURLEditor.php
@@ -1,116 +1,115 @@
<?php
final class PhabricatorPhurlURLEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorPhurlApplication';
}
public function getEditorObjectsDescription() {
return pht('Phurl');
}
public function getCreateObjectTitle($author, $object) {
return pht('%s created this URL.', $author);
}
public function getCreateObjectTitleForFeed($author, $object) {
return pht('%s created %s.', $author, $object);
}
protected function supportsSearch() {
return true;
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
return $types;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
public function getMailTagsMap() {
return array(
PhabricatorPhurlURLTransaction::MAILTAG_DETAILS =>
pht(
"A URL's details change."),
);
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function getMailSubjectPrefix() {
return pht('[Phurl]');
}
protected function getMailTo(PhabricatorLiskDAO $object) {
$phids = array();
$phids[] = $this->getActingAsPHID();
return $phids;
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
$name = $object->getName();
return id(new PhabricatorMetaMTAMail())
- ->setSubject("U{$id}: {$name}")
- ->addHeader('Thread-Topic', "U{$id}: ".$object->getName());
+ ->setSubject("U{$id}: {$name}");
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$description = $object->getDescription();
$body = parent::buildMailBody($object, $xactions);
if (strlen($description)) {
$body->addRemarkupSection(
pht('URL DESCRIPTION'),
$object->getDescription());
}
$body->addLinkSection(
pht('URL DETAIL'),
PhabricatorEnv::getProductionURI('/U'.$object->getID()));
return $body;
}
protected function didCatchDuplicateKeyException(
PhabricatorLiskDAO $object,
array $xactions,
Exception $ex) {
$errors = array();
$errors[] = new PhabricatorApplicationTransactionValidationError(
PhabricatorPhurlURLAliasTransaction::TRANSACTIONTYPE,
pht('Duplicate'),
pht('This alias is already in use.'),
null);
throw new PhabricatorApplicationTransactionValidationException($errors);
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new PhabricatorPhurlURLReplyHandler())
->setMailReceiver($object);
}
}
diff --git a/src/applications/phurl/remarkup/PhabricatorPhurlLinkRemarkupRule.php b/src/applications/phurl/remarkup/PhabricatorPhurlLinkRemarkupRule.php
index 4ef59b300..c4ffc366d 100644
--- a/src/applications/phurl/remarkup/PhabricatorPhurlLinkRemarkupRule.php
+++ b/src/applications/phurl/remarkup/PhabricatorPhurlLinkRemarkupRule.php
@@ -1,74 +1,75 @@
<?php
final class PhabricatorPhurlLinkRemarkupRule extends PhutilRemarkupRule {
public function getPriority() {
return 200.0;
}
public function apply($text) {
// `((123))` remarkup link to `/u/123`
// `((alias))` remarkup link to `/u/alias`
return preg_replace_callback(
'/\(\(([^ )]+)\)\)/',
array($this, 'markupLink'),
$text);
}
public function markupLink(array $matches) {
$engine = $this->getEngine();
$viewer = $engine->getConfig('viewer');
$text_mode = $engine->isTextMode();
$html_mode = $engine->isHTMLMailMode();
if (!$this->isFlatText($matches[0])) {
return $matches[0];
}
$ref = $matches[1];
$monogram = null;
$is_monogram = '/^U(?P<id>[1-9]\d*)/';
$query = id(new PhabricatorPhurlURLQuery())
->setViewer($viewer);
if (preg_match($is_monogram, $ref, $monogram)) {
$query->withIDs(array($monogram[1]));
} else if (ctype_digit($ref)) {
$query->withIDs(array($ref));
} else {
$query->withAliases(array($ref));
}
$phurl = $query->executeOne();
if (!$phurl) {
return $matches[0];
}
$uri = $phurl->getRedirectURI();
$name = $phurl->getDisplayName();
if ($text_mode || $html_mode) {
$uri = PhabricatorEnv::getProductionURI($uri);
}
if ($text_mode) {
return pht(
'%s <%s>',
$name,
$uri);
} else {
$link = phutil_tag(
'a',
array(
'href' => $uri,
'target' => '_blank',
+ 'rel' => 'noreferrer',
),
$name);
}
return $this->getEngine()->storeText($link);
}
}
diff --git a/src/applications/phurl/typeahead/PhabricatorPhurlURLDatasource.php b/src/applications/phurl/typeahead/PhabricatorPhurlURLDatasource.php
index cd01c265c..f8d3cda0c 100644
--- a/src/applications/phurl/typeahead/PhabricatorPhurlURLDatasource.php
+++ b/src/applications/phurl/typeahead/PhabricatorPhurlURLDatasource.php
@@ -1,35 +1,36 @@
<?php
final class PhabricatorPhurlURLDatasource
extends PhabricatorTypeaheadDatasource {
public function getBrowseTitle() {
return pht('Browse Phurl URLs');
}
public function getPlaceholderText() {
return pht('Select a phurl...');
}
public function getDatasourceApplicationClass() {
return 'PhabricatorPhurlApplication';
}
public function loadResults() {
$query = id(new PhabricatorPhurlURLQuery());
$urls = $this->executeQuery($query);
$results = array();
foreach ($urls as $url) {
$result = id(new PhabricatorTypeaheadResult())
->setDisplayName($url->getName())
->setName($url->getName()." ".$url->getAlias())
->setPHID($url->getPHID())
+ ->setAutocomplete('(('.$url->getAlias().'))')
->addAttribute($url->getLongURL());
$results[] = $result;
}
return $this->filterResultsAgainstTokens($results);
}
}
diff --git a/src/applications/ponder/application/PhabricatorPonderApplication.php b/src/applications/ponder/application/PhabricatorPonderApplication.php
index ed37c7ef6..56973447f 100644
--- a/src/applications/ponder/application/PhabricatorPonderApplication.php
+++ b/src/applications/ponder/application/PhabricatorPonderApplication.php
@@ -1,124 +1,118 @@
<?php
final class PhabricatorPonderApplication extends PhabricatorApplication {
public function getBaseURI() {
return '/ponder/';
}
public function getName() {
return pht('Ponder');
}
public function getShortDescription() {
return pht('Questions and Answers');
}
public function getIcon() {
return 'fa-university';
}
- public function getFactObjectsForAnalysis() {
- return array(
- new PonderQuestion(),
- );
- }
-
public function getTitleGlyph() {
return "\xE2\x97\xB3";
}
public function getRemarkupRules() {
return array(
new PonderRemarkupRule(),
);
}
public function getApplicationGroup() {
return self::GROUP_UTILITIES;
}
public function supportsEmailIntegration() {
return true;
}
public function getAppEmailBlurb() {
return pht(
'Send email to these addresses to create questions. %s',
phutil_tag(
'a',
array(
'href' => $this->getInboundEmailSupportLink(),
),
pht('Learn More')));
}
public function getRoutes() {
return array(
'/Q(?P<id>[1-9]\d*)'
=> 'PonderQuestionViewController',
'/ponder/' => array(
'(?:query/(?P<queryKey>[^/]+)/)?'
=> 'PonderQuestionListController',
'answer/' => array(
'add/'
=> 'PonderAnswerSaveController',
'edit/(?P<id>\d+)/'
=> 'PonderAnswerEditController',
'comment/(?P<id>\d+)/'
=> 'PonderAnswerCommentController',
'history/(?P<id>\d+)/'
=> 'PonderAnswerHistoryController',
),
'question/' => array(
$this->getEditRoutePattern('edit/')
=> 'PonderQuestionEditController',
'create/'
=> 'PonderQuestionEditController',
'comment/(?P<id>\d+)/'
=> 'PonderQuestionCommentController',
'history/(?P<id>\d+)/'
=> 'PonderQuestionHistoryController',
),
'preview/'
=> 'PhabricatorMarkupPreviewController',
'question/status/(?P<id>[1-9]\d*)/'
=> 'PonderQuestionStatusController',
),
);
}
public function getMailCommandObjects() {
return array(
'question' => array(
'name' => pht('Email Commands: Questions'),
'header' => pht('Interacting with Ponder Questions'),
'object' => new PonderQuestion(),
'summary' => pht(
'This page documents the commands you can use to interact with '.
'questions in Ponder.'),
),
);
}
protected function getCustomCapabilities() {
return array(
PonderDefaultViewCapability::CAPABILITY => array(
'template' => PonderQuestionPHIDType::TYPECONST,
'capability' => PhabricatorPolicyCapability::CAN_VIEW,
),
PonderModerateCapability::CAPABILITY => array(
'default' => PhabricatorPolicies::POLICY_ADMIN,
'template' => PonderQuestionPHIDType::TYPECONST,
'capability' => PhabricatorPolicyCapability::CAN_EDIT,
),
);
}
public function getApplicationSearchDocumentTypes() {
return array(
PonderQuestionPHIDType::TYPECONST,
);
}
}
diff --git a/src/applications/ponder/editor/PonderAnswerEditor.php b/src/applications/ponder/editor/PonderAnswerEditor.php
index bab0e1f72..37b2fe2cd 100644
--- a/src/applications/ponder/editor/PonderAnswerEditor.php
+++ b/src/applications/ponder/editor/PonderAnswerEditor.php
@@ -1,86 +1,85 @@
<?php
final class PonderAnswerEditor extends PonderEditor {
public function getEditorObjectsDescription() {
return pht('Ponder Answers');
}
public function getCreateObjectTitle($author, $object) {
return pht('%s added this answer.', $author);
}
public function getCreateObjectTitleForFeed($author, $object) {
return pht('%s added %s.', $author, $object);
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
return $types;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function getMailTo(PhabricatorLiskDAO $object) {
$phids = array();
$phids[] = $object->getAuthorPHID();
$phids[] = $this->requireActor()->getPHID();
$question = id(new PonderQuestionQuery())
->setViewer($this->requireActor())
->withIDs(array($object->getQuestionID()))
->executeOne();
$phids[] = $question->getAuthorPHID();
return $phids;
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new PonderAnswerReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
return id(new PhabricatorMetaMTAMail())
- ->setSubject("ANSR{$id}")
- ->addHeader('Thread-Topic', "ANSR{$id}");
+ ->setSubject("ANSR{$id}");
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
// If the user just gave the answer, add the answer text.
foreach ($xactions as $xaction) {
$type = $xaction->getTransactionType();
$new = $xaction->getNewValue();
if ($type == PonderAnswerContentTransaction::TRANSACTIONTYPE) {
$body->addRawSection($new);
}
}
$body->addLinkSection(
pht('ANSWER DETAIL'),
PhabricatorEnv::getProductionURI($object->getURI()));
return $body;
}
}
diff --git a/src/applications/ponder/editor/PonderQuestionEditor.php b/src/applications/ponder/editor/PonderQuestionEditor.php
index 0720f436b..ba9687bd0 100644
--- a/src/applications/ponder/editor/PonderQuestionEditor.php
+++ b/src/applications/ponder/editor/PonderQuestionEditor.php
@@ -1,197 +1,195 @@
<?php
final class PonderQuestionEditor
extends PonderEditor {
private $answer;
public function getEditorObjectsDescription() {
return pht('Ponder Questions');
}
public function getCreateObjectTitle($author, $object) {
return pht('%s asked this question.', $author);
}
public function getCreateObjectTitleForFeed($author, $object) {
return pht('%s asked %s.', $author, $object);
}
/**
* This is used internally on @{method:applyInitialEffects} if a transaction
* of type PonderQuestionTransaction::TYPE_ANSWERS is in the mix. The value
* is set to the //last// answer in the transactions. Practically, one
* answer is given at a time in the application, though theoretically
* this is buggy.
*
* The answer is used in emails to generate proper links.
*/
private function setAnswer(PonderAnswer $answer) {
$this->answer = $answer;
return $this;
}
private function getAnswer() {
return $this->answer;
}
protected function shouldApplyInitialEffects(
PhabricatorLiskDAO $object,
array $xactions) {
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PonderQuestionAnswerTransaction::TRANSACTIONTYPE:
return true;
}
}
return false;
}
protected function applyInitialEffects(
PhabricatorLiskDAO $object,
array $xactions) {
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PonderQuestionAnswerTransaction::TRANSACTIONTYPE:
$new_value = $xaction->getNewValue();
$new = idx($new_value, '+', array());
foreach ($new as $new_answer) {
$answer = idx($new_answer, 'answer');
if (!$answer) {
continue;
}
$answer->save();
$this->setAnswer($answer);
}
break;
}
}
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
return $types;
}
protected function supportsSearch() {
return true;
}
protected function shouldImplyCC(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PonderQuestionAnswerTransaction::TRANSACTIONTYPE:
return false;
}
return parent::shouldImplyCC($object, $xaction);
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PonderQuestionAnswerTransaction::TRANSACTIONTYPE:
return false;
}
}
return true;
}
protected function getMailTo(PhabricatorLiskDAO $object) {
return array(
$object->getAuthorPHID(),
$this->requireActor()->getPHID(),
);
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PonderQuestionAnswerTransaction::TRANSACTIONTYPE:
return false;
}
}
return true;
}
public function getMailTagsMap() {
return array(
PonderQuestionTransaction::MAILTAG_DETAILS =>
pht('Someone changes the questions details.'),
PonderQuestionTransaction::MAILTAG_ANSWERS =>
pht('Someone adds a new answer.'),
PonderQuestionTransaction::MAILTAG_COMMENT =>
pht('Someone comments on the question.'),
PonderQuestionTransaction::MAILTAG_OTHER =>
pht('Other question activity not listed above occurs.'),
);
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new PonderQuestionReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
$title = $object->getTitle();
- $original_title = $object->getOriginalTitle();
return id(new PhabricatorMetaMTAMail())
- ->setSubject("Q{$id}: {$title}")
- ->addHeader('Thread-Topic', "Q{$id}: {$original_title}");
+ ->setSubject("Q{$id}: {$title}");
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
$header = pht('QUESTION DETAIL');
$uri = '/Q'.$object->getID();
foreach ($xactions as $xaction) {
$type = $xaction->getTransactionType();
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
// If the user just asked the question, add the question text.
if ($type == PonderQuestionContentTransaction::TRANSACTIONTYPE) {
if ($old === null) {
$body->addRawSection($new);
}
}
}
$body->addLinkSection(
$header,
PhabricatorEnv::getProductionURI($uri));
return $body;
}
protected function shouldApplyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
return id(new HeraldPonderQuestionAdapter())
->setQuestion($object);
}
}
diff --git a/src/applications/ponder/storage/PonderQuestion.php b/src/applications/ponder/storage/PonderQuestion.php
index eefcdba9b..17f7ee3fd 100644
--- a/src/applications/ponder/storage/PonderQuestion.php
+++ b/src/applications/ponder/storage/PonderQuestion.php
@@ -1,313 +1,308 @@
<?php
final class PonderQuestion extends PonderDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorMarkupInterface,
PhabricatorSubscribableInterface,
PhabricatorFlaggableInterface,
PhabricatorPolicyInterface,
PhabricatorTokenReceiverInterface,
PhabricatorProjectInterface,
PhabricatorDestructibleInterface,
PhabricatorSpacesInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface {
const MARKUP_FIELD_CONTENT = 'markup:content';
protected $title;
protected $phid;
protected $authorPHID;
protected $status;
protected $content;
protected $answerWiki;
protected $contentSource;
protected $viewPolicy;
protected $spacePHID;
protected $answerCount;
protected $mailKey;
private $answers;
private $comments;
private $projectPHIDs = self::ATTACHABLE;
public static function initializeNewQuestion(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorPonderApplication'))
->executeOne();
$view_policy = $app->getPolicy(
PonderDefaultViewCapability::CAPABILITY);
return id(new PonderQuestion())
->setAuthorPHID($actor->getPHID())
->setViewPolicy($view_policy)
->setStatus(PonderQuestionStatus::STATUS_OPEN)
->setAnswerCount(0)
->setAnswerWiki('')
->setSpacePHID($actor->getDefaultSpacePHID());
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'title' => 'text255',
'status' => 'text32',
'content' => 'text',
'answerWiki' => 'text',
'answerCount' => 'uint32',
'mailKey' => 'bytes20',
// T6203/NULLABILITY
// This should always exist.
'contentSource' => 'text?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'authorPHID' => array(
'columns' => array('authorPHID'),
),
'status' => array(
'columns' => array('status'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(PonderQuestionPHIDType::TYPECONST);
}
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source->serialize();
return $this;
}
public function getContentSource() {
return PhabricatorContentSource::newFromSerialized($this->contentSource);
}
public function setComments($comments) {
$this->comments = $comments;
return $this;
}
public function getComments() {
return $this->comments;
}
public function getMonogram() {
return 'Q'.$this->getID();
}
public function getViewURI() {
return '/'.$this->getMonogram();
}
public function attachAnswers(array $answers) {
assert_instances_of($answers, 'PonderAnswer');
$this->answers = $answers;
return $this;
}
public function getAnswers() {
return $this->answers;
}
public function getProjectPHIDs() {
return $this->assertAttached($this->projectPHIDs);
}
public function attachProjectPHIDs(array $phids) {
$this->projectPHIDs = $phids;
return $this;
}
public function getMarkupField() {
return self::MARKUP_FIELD_CONTENT;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PonderQuestionEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PonderQuestionTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
// Markup interface
public function getMarkupFieldKey($field) {
$content = $this->getMarkupText($field);
return PhabricatorMarkupEngine::digestRemarkupContent($this, $content);
}
public function getMarkupText($field) {
return $this->getContent();
}
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::getEngine();
}
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
return $output;
}
public function shouldUseMarkupCache($field) {
return (bool)$this->getID();
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
- public function getOriginalTitle() {
- // TODO: Make this actually save/return the original title.
- return $this->getTitle();
- }
-
public function getFullTitle() {
$id = $this->getID();
$title = $this->getTitle();
return "Q{$id}: {$title}";
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
$app = PhabricatorApplication::getByClass(
'PhabricatorPonderApplication');
return $app->getPolicy(PonderModerateCapability::CAPABILITY);
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if ($capability == PhabricatorPolicyCapability::CAN_VIEW) {
if (PhabricatorPolicyFilter::hasCapability(
$viewer, $this, PhabricatorPolicyCapability::CAN_EDIT)) {
return true;
}
}
return ($viewer->getPHID() == $this->getAuthorPHID());
}
public function describeAutomaticCapability($capability) {
$out = array();
$out[] = pht('The user who asked a question can always view and edit it.');
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
$out[] = pht(
'A moderator can always view the question.');
break;
}
return $out;
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return ($phid == $this->getAuthorPHID());
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array(
$this->getAuthorPHID(),
);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$answers = id(new PonderAnswer())->loadAllWhere(
'questionID = %d',
$this->getID());
foreach ($answers as $answer) {
$engine->destroyObject($answer);
}
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorSpacesInterface )----------------------------------------- */
public function getSpacePHID() {
return $this->spacePHID;
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new PonderQuestionFulltextEngine();
}
/* -( PhabricatorFerretInterface )----------------------------------------- */
public function newFerretEngine() {
return new PonderQuestionFerretEngine();
}
}
diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php
index 3f2b49c9d..027004327 100644
--- a/src/applications/project/controller/PhabricatorProjectProfileController.php
+++ b/src/applications/project/controller/PhabricatorProjectProfileController.php
@@ -1,504 +1,504 @@
<?php
final class PhabricatorProjectProfileController
extends PhabricatorProjectController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$response = $this->loadProject();
if ($response) {
return $response;
}
$viewer = $request->getUser();
$project = $this->getProject();
$id = $project->getID();
$picture = $project->getProfileImageURI();
$icon = $project->getDisplayIconIcon();
$icon_name = $project->getDisplayIconName();
$tag = id(new PHUITagView())
->setIcon($icon)
->setName($icon_name)
->addClass('project-view-header-tag')
->setType(PHUITagView::TYPE_SHADE);
$header = id(new PHUIHeaderView())
->setHeader(array($project->getDisplayName(), $tag))
->setUser($viewer)
->setPolicyObject($project)
->setProfileHeader(true);
if ($project->getStatus() == PhabricatorProjectStatus::STATUS_ACTIVE) {
$header->setStatus('fa-check', 'bluegrey', pht('Active'));
} else {
$header->setStatus('fa-ban', 'red', pht('Archived'));
}
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$project,
PhabricatorPolicyCapability::CAN_EDIT);
if ($can_edit) {
$header->setImageEditURL($this->getApplicationURI("picture/{$id}/"));
}
$properties = $this->buildPropertyListView($project);
$watch_action = $this->renderWatchAction($project);
$header->addActionLink($watch_action);
// c4science custo
// Create and list Phriction pages, repositories and
// Jenkins Job linked to this project
if($can_edit){
$wiki_action = $this->renderWikiAction($project);
$header->addActionLink($wiki_action);
$have_jenkins = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorJenkinsJobApplication', $viewer);
if ($have_jenkins) {
$job_action = $this->renderJobAction($project);
$header->addActionLink($job_action);
}
}
$repo_list = $this->buildRepoList($project);
$jobs_list = $this->buildJobsList($project);
$wiki_list = $this->buildWikiList($project);
// end of c4science custo
$milestone_list = $this->buildMilestoneList($project);
$subproject_list = $this->buildSubprojectList($project);
$member_list = id(new PhabricatorProjectMemberListView())
->setUser($viewer)
->setProject($project)
->setLimit(5)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setUserPHIDs($project->getMemberPHIDs());
// c4science custo
// $watcher_list = id(new PhabricatorProjectWatcherListView())
- // ->setUser($viewer)
- // ->setProject($project)
- // ->setLimit(5)
- // ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
- // ->setUserPHIDs($project->getWatcherPHIDs());
+ // ->setUser($viewer)
+ // ->setProject($project)
+ // ->setLimit(5)
+ // ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+ // ->setUserPHIDs($project->getWatcherPHIDs());
$nav = $this->getProfileMenu();
$nav->selectFilter(PhabricatorProject::ITEM_PROFILE);
$stories = id(new PhabricatorFeedQuery())
->setViewer($viewer)
->withFilterPHIDs(
array(
$project->getPHID(),
))
->setLimit(50)
->execute();
$view_all = id(new PHUIButtonView())
->setTag('a')
->setIcon(
id(new PHUIIconView())
->setIcon('fa-list-ul'))
->setText(pht('View All'))
->setHref('/feed/?projectPHIDs='.$project->getPHID());
$feed_header = id(new PHUIHeaderView())
->setHeader(pht('Recent Activity'))
->addActionLink($view_all);
$feed = $this->renderStories($stories);
$feed = id(new PHUIObjectBoxView())
->setHeader($feed_header)
->addClass('project-view-feed')
->appendChild($feed);
require_celerity_resource('project-view-css');
$home = id(new PHUITwoColumnView())
->setHeader($header)
->addClass('project-view-home')
->addClass('project-view-people-home')
->setMainColumn(
array(
$properties,
$feed,
))
->setSideColumn(
array(
$repo_list, // c4s custo
$jobs_list, // c4s custo
$wiki_list, // c4s custo
$milestone_list,
$subproject_list,
$member_list,
// $watcher_list, // c4s custo
));
$crumbs = $this->buildApplicationCrumbs();
$crumbs->setBorder(true);
return $this->newPage()
->setNavigation($nav)
->setCrumbs($crumbs)
->setTitle($project->getDisplayName())
->setPageObjectPHIDs(array($project->getPHID()))
->appendChild($home);
}
private function buildPropertyListView(
PhabricatorProject $project) {
$request = $this->getRequest();
$viewer = $request->getUser();
$view = id(new PHUIPropertyListView())
->setUser($viewer)
->setObject($project);
$field_list = PhabricatorCustomField::getObjectFields(
$project,
PhabricatorCustomField::ROLE_VIEW);
$field_list->appendFieldsToPropertyList($project, $viewer, $view);
if (!$view->hasAnyProperties()) {
return null;
}
$header = id(new PHUIHeaderView())
->setHeader(pht('Details'));
$view = id(new PHUIObjectBoxView())
->setHeader($header)
->appendChild($view)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->addClass('project-view-properties');
return $view;
}
private function renderStories(array $stories) {
assert_instances_of($stories, 'PhabricatorFeedStory');
$builder = new PhabricatorFeedBuilder($stories);
$builder->setUser($this->getRequest()->getUser());
$builder->setShowHovercards(true);
$view = $builder->buildView();
return $view;
}
private function renderWatchAction(PhabricatorProject $project) {
$viewer = $this->getViewer();
$id = $project->getID();
if (!$viewer->isLoggedIn()) {
$is_watcher = false;
$is_ancestor = false;
} else {
$viewer_phid = $viewer->getPHID();
$is_watcher = $project->isUserWatcher($viewer_phid);
$is_ancestor = $project->isUserAncestorWatcher($viewer_phid);
}
if ($is_ancestor && !$is_watcher) {
$watch_icon = 'fa-eye';
$watch_text = pht('Watching Ancestor');
$watch_href = "/project/watch/{$id}/?via=profile";
$watch_disabled = true;
} else if (!$is_watcher) {
$watch_icon = 'fa-eye';
$watch_text = pht('Watch Project');
$watch_href = "/project/watch/{$id}/?via=profile";
$watch_disabled = false;
} else {
$watch_icon = 'fa-eye-slash';
$watch_text = pht('Unwatch Project');
$watch_href = "/project/unwatch/{$id}/?via=profile";
$watch_disabled = false;
}
$watch_icon = id(new PHUIIconView())
->setIcon($watch_icon);
return id(new PHUIButtonView())
->setTag('a')
->setWorkflow(true)
->setIcon($watch_icon)
->setText($watch_text)
->setHref($watch_href)
->setDisabled($watch_disabled);
}
// c4science custo
private function renderWikiAction(PhabricatorProject $project) {
$id = $project->getID();
$wiki_icon = id(new PHUIIconView())->setIcon('fa-book');
$wiki_text = pht('Create wiki page');
$wiki_href = "/project/wiki/create/{$id}/?via=profile";
return id(new PHUIButtonView())
->setTag('a')
->setWorkflow(true)
->setIcon($wiki_icon)
->setText($wiki_text)
->setHref($wiki_href);
}
// c4science custo
private function renderJobAction(PhabricatorProject $project) {
$phid = $project->getPHID();
$job_icon = id(new PHUIIconView())->setIcon('fa-tasks');
$job_text = pht('Create Jenkins Job');
$job_href = "/jobs/create/?with_project={$phid}";
return id(new PHUIButtonView())
->setTag('a')
->setWorkflow(true)
->setIcon($job_icon)
->setText($job_text)
->setHref($job_href);
}
// c4science custo
private function getHasWiki(PhabricatorProject $project) {
$viewer = $this->getViewer();
$slug = PhabricatorProjectWikiCreate::getAllSlugs($project);
return id(new PhrictionDocumentQuery())
->setViewer($viewer)
->withStatus(PhrictionDocumentQuery::STATUS_OPEN)
->withSlugPrefix($slug)
->setLimit(1)
->execute();
}
// c4science custo
private function buildWikiList(PhabricatorProject $project) {
if (!$this->getHasWiki($project)) {
return null;
}
$viewer = $this->getViewer();
$id = $project->getID();
$slug = PhabricatorProjectWikiCreate::getAllSlugs($project);
$query = id(new PhrictionDocumentQuery())
->setViewer($viewer)
->needContent(true)
->withStatus(PhrictionDocumentQuery::STATUS_OPEN)
->withSlugPrefix($slug)
->setLimit(5)
->execute();
$wiki_list = new PHUIObjectItemListView();
foreach($query as $w){
$wiki_list->addItem(
id(new PHUIObjectItemView())
->setHeader($w->getContent()->getTitle())
->addAttribute($w->getSlug())
->setHref($w->getSlugURI($w->getSlug())));
}
$view_all = id(new PHUIButtonView())
->setTag('a')
->setIcon(
id(new PHUIIconView())
->setIcon('fa-list-ul'))
->setText(pht('View All'))
->setHref("/project/wiki/view/{$id}/");
$header = id(new PHUIHeaderView())
->setHeader(pht('Wiki pages'))
->addActionLink($view_all);
return id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setObjectList($wiki_list);
}
// c4science custo
private function buildJobsList(PhabricatorProject $project) {
$viewer = $this->getViewer();
$have_jenkins = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorJenkinsJobApplication', $viewer);
if (!$have_jenkins) {
return;
}
$query = id(new PhabricatorJenkinsJobQuery())
->setViewer($viewer)
->withProjects(array($project->getPHID()))
->execute();
if (!$query) {
return null;
}
$jobs_list = PhabricatorJenkinsJobController::createObjectList(
$viewer, $project->getPHID(), null);
$header = id(new PHUIHeaderView())
->setHeader(pht('Jobs'));
return id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setObjectList($jobs_list);
}
// c4science custo
private function buildRepoList(PhabricatorProject $project) {
$viewer = $this->getViewer();
$datasource = id(new PhabricatorProjectLogicalDatasource())
->setViewer($viewer);
$constraints = $datasource->evaluateTokens(array($project->getPHID()));
$query = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->needProjectPHIDs(true)
->withStatus(PhabricatorRepositoryQuery::STATUS_OPEN)
->setOrder('committed')
->setLimit(5)
->withEdgeLogicConstraints(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
$constraints)
->execute();
if(!$query){
return null;
}
$repo_list = new PHUIObjectItemListView();
foreach($query as $r){
$repo_list->addItem(
id(new PHUIObjectItemView())
->setHeader($r->getName())
->addAttribute($r->getDisplayName())
->setHref($r->getURI())
);
}
$view_all = id(new PHUIButtonView())
->setTag('a')
->setIcon(
id(new PHUIIconView())
->setIcon('fa-list-ul'))
->setText(pht('View All'))
->setHref("/diffusion/?status=open&projectPHIDs=" . $project->getPHID() . '#R');
$header = id(new PHUIHeaderView())
->setHeader(pht('Repositories'))
->addActionLink($view_all);
return id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setObjectList($repo_list);
}
private function buildMilestoneList(PhabricatorProject $project) {
if (!$project->getHasMilestones()) {
return null;
}
$viewer = $this->getViewer();
$id = $project->getID();
$milestones = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withParentProjectPHIDs(array($project->getPHID()))
->needImages(true)
->withIsMilestone(true)
->withStatuses(
array(
PhabricatorProjectStatus::STATUS_ACTIVE,
))
->setOrderVector(array('milestoneNumber', 'id'))
->execute();
if (!$milestones) {
return null;
}
$milestone_list = id(new PhabricatorProjectListView())
->setUser($viewer)
->setProjects($milestones)
->renderList();
$view_all = id(new PHUIButtonView())
->setTag('a')
->setIcon(
id(new PHUIIconView())
->setIcon('fa-list-ul'))
->setText(pht('View All'))
->setHref("/project/subprojects/{$id}/");
$header = id(new PHUIHeaderView())
->setHeader(pht('Milestones'))
->addActionLink($view_all);
return id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setObjectList($milestone_list);
}
private function buildSubprojectList(PhabricatorProject $project) {
if (!$project->getHasSubprojects()) {
return null;
}
$viewer = $this->getViewer();
$id = $project->getID();
$limit = 25;
$subprojects = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withParentProjectPHIDs(array($project->getPHID()))
->needImages(true)
->withStatuses(
array(
PhabricatorProjectStatus::STATUS_ACTIVE,
))
->withIsMilestone(false)
->setLimit($limit)
->execute();
if (!$subprojects) {
return null;
}
$subproject_list = id(new PhabricatorProjectListView())
->setUser($viewer)
->setProjects($subprojects)
->renderList();
$view_all = id(new PHUIButtonView())
->setTag('a')
->setIcon(
id(new PHUIIconView())
->setIcon('fa-list-ul'))
->setText(pht('View All'))
->setHref("/project/subprojects/{$id}/");
$header = id(new PHUIHeaderView())
->setHeader(pht('Subprojects'))
->addActionLink($view_all);
return id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setObjectList($subproject_list);
}
}
diff --git a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php
index 2764ce632..2a90d6ce2 100644
--- a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php
+++ b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php
@@ -1,476 +1,477 @@
<?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 {
- // You need CAN_EDIT to change members other than yourself.
- PhabricatorPolicyFilter::requireCapability(
- $this->requireActor(),
- $object,
- PhabricatorPolicyCapability::CAN_EDIT);
+ 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) {
- $id = $object->getID();
$name = $object->getName();
return id(new PhabricatorMetaMTAMail())
- ->setSubject("{$name}")
- ->addHeader('Thread-Topic', "Project {$id}");
+ ->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/engineextension/PhabricatorProjectsMailEngineExtension.php b/src/applications/project/engineextension/PhabricatorProjectsMailEngineExtension.php
new file mode 100644
index 000000000..6f92f87b1
--- /dev/null
+++ b/src/applications/project/engineextension/PhabricatorProjectsMailEngineExtension.php
@@ -0,0 +1,32 @@
+<?php
+
+final class PhabricatorProjectsMailEngineExtension
+ extends PhabricatorMailEngineExtension {
+
+ const EXTENSIONKEY = 'projects';
+
+ public function supportsObject($object) {
+ return ($object instanceof PhabricatorProjectInterface);
+ }
+
+ public function newMailStampTemplates($object) {
+ return array(
+ id(new PhabricatorPHIDMailStamp())
+ ->setKey('tag')
+ ->setLabel(pht('Tagged with Project')),
+ );
+ }
+
+ public function newMailStamps($object, array $xactions) {
+ $editor = $this->getEditor();
+ $viewer = $this->getViewer();
+
+ $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
+ $object->getPHID(),
+ PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
+
+ $this->getMailStamp('tag')
+ ->setValue($project_phids);
+ }
+
+}
diff --git a/src/applications/project/engineextension/ProjectDatasourceEngineExtension.php b/src/applications/project/engineextension/ProjectDatasourceEngineExtension.php
new file mode 100644
index 000000000..b566d58f9
--- /dev/null
+++ b/src/applications/project/engineextension/ProjectDatasourceEngineExtension.php
@@ -0,0 +1,57 @@
+<?php
+
+final class ProjectDatasourceEngineExtension
+ extends PhabricatorDatasourceEngineExtension {
+
+ public function newQuickSearchDatasources() {
+ return array(
+ new PhabricatorProjectDatasource(),
+ );
+ }
+
+ public function newJumpURI($query) {
+ $viewer = $this->getViewer();
+
+ // Send "p" to Projects.
+ if (preg_match('/^p\z/i', $query)) {
+ return '/diffusion/';
+ }
+
+ // Send "p <string>" to a search for similar projects.
+ $matches = null;
+ if (preg_match('/^p\s+(.+)\z/i', $query, $matches)) {
+ $raw_query = $matches[1];
+
+ $engine = id(new PhabricatorProject())
+ ->newFerretEngine();
+
+ $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;
+ }
+
+ $projects = id(new PhabricatorProjectQuery())
+ ->setViewer($viewer)
+ ->withFerretConstraint($engine, $fulltext_tokens)
+ ->execute();
+ if (count($projects) == 1) {
+ // Just one match, jump to project.
+ return head($projects)->getURI();
+ } else {
+ // More than one match, jump to search.
+ return urisprintf(
+ '/project/?order=relevance&query=%s#R',
+ $raw_query);
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/src/applications/project/engineextension/ProjectQuickSearchEngineExtension.php b/src/applications/project/engineextension/ProjectQuickSearchEngineExtension.php
deleted file mode 100644
index 6bb50e021..000000000
--- a/src/applications/project/engineextension/ProjectQuickSearchEngineExtension.php
+++ /dev/null
@@ -1,11 +0,0 @@
-<?php
-
-final class ProjectQuickSearchEngineExtension
- extends PhabricatorQuickSearchEngineExtension {
-
- public function newQuickSearchDatasources() {
- return array(
- new PhabricatorProjectDatasource(),
- );
- }
-}
diff --git a/src/applications/project/herald/HeraldProjectsField.php b/src/applications/project/herald/HeraldProjectsField.php
index 1e2594868..7f66a00cf 100644
--- a/src/applications/project/herald/HeraldProjectsField.php
+++ b/src/applications/project/herald/HeraldProjectsField.php
@@ -1,33 +1,33 @@
<?php
final class HeraldProjectsField extends HeraldField {
const FIELDCONST = 'projects';
public function getHeraldFieldName() {
- return pht('Projects');
+ return pht('Project tags');
}
public function getFieldGroupKey() {
return HeraldSupportFieldGroup::FIELDGROUPKEY;
}
public function supportsObject($object) {
return ($object instanceof PhabricatorProjectInterface);
}
public function getHeraldFieldValue($object) {
return PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
}
protected function getHeraldFieldStandardType() {
return self::STANDARD_PHID_LIST;
}
protected function getDatasource() {
return new PhabricatorProjectDatasource();
}
}
diff --git a/src/applications/project/phid/PhabricatorProjectProjectPHIDType.php b/src/applications/project/phid/PhabricatorProjectProjectPHIDType.php
index 3aa608878..9247966d7 100644
--- a/src/applications/project/phid/PhabricatorProjectProjectPHIDType.php
+++ b/src/applications/project/phid/PhabricatorProjectProjectPHIDType.php
@@ -1,120 +1,121 @@
<?php
final class PhabricatorProjectProjectPHIDType extends PhabricatorPHIDType {
const TYPECONST = 'PROJ';
public function getTypeName() {
return pht('Project');
}
public function getTypeIcon() {
return 'fa-briefcase bluegrey';
}
public function newObject() {
return new PhabricatorProject();
}
public function getPHIDTypeApplicationClass() {
return 'PhabricatorProjectApplication';
}
protected function buildQueryForObjects(
PhabricatorObjectQuery $query,
array $phids) {
return id(new PhabricatorProjectQuery())
->withPHIDs($phids)
->needImages(true);
}
public function loadHandles(
PhabricatorHandleQuery $query,
array $handles,
array $objects) {
foreach ($handles as $phid => $handle) {
$project = $objects[$phid];
$name = $project->getDisplayName();
$id = $project->getID();
$slug = $project->getPrimarySlug();
$handle->setName($name);
if (strlen($slug)) {
$handle->setObjectName('#'.$slug);
+ $handle->setMailStampName('#'.$slug);
$handle->setURI("/tag/{$slug}/");
} else {
// We set the name to the project's PHID to avoid a parse error when a
// project has no hashtag (as is the case with milestones by default).
- // See T12659 for more details
+ // See T12659 for more details.
$handle->setCommandLineObjectName($project->getPHID());
$handle->setURI("/project/view/{$id}/");
}
$handle->setImageURI($project->getProfileImageURI());
$handle->setIcon($project->getDisplayIconIcon());
$handle->setTagColor($project->getDisplayColor());
if ($project->isArchived()) {
$handle->setStatus(PhabricatorObjectHandle::STATUS_CLOSED);
}
}
}
public static function getProjectMonogramPatternFragment() {
// NOTE: See some discussion in ProjectRemarkupRule.
return '[^\s,#]+';
}
public function canLoadNamedObject($name) {
$fragment = self::getProjectMonogramPatternFragment();
return preg_match('/^#'.$fragment.'$/i', $name);
}
public function loadNamedObjects(
PhabricatorObjectQuery $query,
array $names) {
// If the user types "#YoloSwag", we still want to match "#yoloswag", so
// we normalize, query, and then map back to the original inputs.
$map = array();
foreach ($names as $key => $slug) {
$map[$this->normalizeSlug(substr($slug, 1))][] = $slug;
}
$projects = id(new PhabricatorProjectQuery())
->setViewer($query->getViewer())
->withSlugs(array_keys($map))
->needSlugs(true)
->execute();
$result = array();
foreach ($projects as $project) {
$slugs = $project->getSlugs();
$slug_strs = mpull($slugs, 'getSlug');
foreach ($slug_strs as $slug) {
$slug_map = idx($map, $slug, array());
foreach ($slug_map as $original) {
$result[$original] = $project;
}
}
}
return $result;
}
private function normalizeSlug($slug) {
// NOTE: We're using phutil_utf8_strtolower() (and not PhabricatorSlug's
// normalize() method) because this normalization should be only somewhat
// liberal. We want "#YOLO" to match against "#yolo", but "#\\yo!!lo"
// should not. normalize() strips out most punctuation and leads to
// excessively aggressive matches.
return phutil_utf8_strtolower($slug);
}
}
diff --git a/src/applications/project/query/PhabricatorProjectQuery.php b/src/applications/project/query/PhabricatorProjectQuery.php
index 955eba997..98713078f 100644
--- a/src/applications/project/query/PhabricatorProjectQuery.php
+++ b/src/applications/project/query/PhabricatorProjectQuery.php
@@ -1,819 +1,859 @@
<?php
final class PhabricatorProjectQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $memberPHIDs;
private $watcherPHIDs;
private $slugs;
private $slugNormals;
private $slugMap;
private $allSlugs;
private $names;
private $namePrefixes;
private $nameTokens;
private $icons;
private $colors;
private $ancestorPHIDs;
private $parentPHIDs;
private $isMilestone;
private $hasSubprojects;
private $minDepth;
private $maxDepth;
private $minMilestoneNumber;
private $maxMilestoneNumber;
private $status = 'status-any';
const STATUS_ANY = 'status-any';
const STATUS_OPEN = 'status-open';
const STATUS_CLOSED = 'status-closed';
const STATUS_ACTIVE = 'status-active';
const STATUS_ARCHIVED = 'status-archived';
private $statuses;
private $needSlugs;
private $needMembers;
private $needAncestorMembers;
private $needWatchers;
private $needImages;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withStatus($status) {
$this->status = $status;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
public function withMemberPHIDs(array $member_phids) {
$this->memberPHIDs = $member_phids;
return $this;
}
public function withWatcherPHIDs(array $watcher_phids) {
$this->watcherPHIDs = $watcher_phids;
return $this;
}
public function withSlugs(array $slugs) {
$this->slugs = $slugs;
return $this;
}
public function withNames(array $names) {
$this->names = $names;
return $this;
}
public function withNamePrefixes(array $prefixes) {
$this->namePrefixes = $prefixes;
return $this;
}
public function withNameTokens(array $tokens) {
$this->nameTokens = array_values($tokens);
return $this;
}
public function withIcons(array $icons) {
$this->icons = $icons;
return $this;
}
public function withColors(array $colors) {
$this->colors = $colors;
return $this;
}
public function withParentProjectPHIDs($parent_phids) {
$this->parentPHIDs = $parent_phids;
return $this;
}
public function withAncestorProjectPHIDs($ancestor_phids) {
$this->ancestorPHIDs = $ancestor_phids;
return $this;
}
public function withIsMilestone($is_milestone) {
$this->isMilestone = $is_milestone;
return $this;
}
public function withHasSubprojects($has_subprojects) {
$this->hasSubprojects = $has_subprojects;
return $this;
}
public function withDepthBetween($min, $max) {
$this->minDepth = $min;
$this->maxDepth = $max;
return $this;
}
public function withMilestoneNumberBetween($min, $max) {
$this->minMilestoneNumber = $min;
$this->maxMilestoneNumber = $max;
return $this;
}
public function needMembers($need_members) {
$this->needMembers = $need_members;
return $this;
}
public function needAncestorMembers($need_ancestor_members) {
$this->needAncestorMembers = $need_ancestor_members;
return $this;
}
public function needWatchers($need_watchers) {
$this->needWatchers = $need_watchers;
return $this;
}
public function needImages($need_images) {
$this->needImages = $need_images;
return $this;
}
public function needSlugs($need_slugs) {
$this->needSlugs = $need_slugs;
return $this;
}
public function newResultObject() {
return new PhabricatorProject();
}
protected function getDefaultOrderVector() {
return array('name');
}
public function getBuiltinOrders() {
return array(
'name' => array(
'vector' => array('name'),
'name' => pht('Name'),
),
) + parent::getBuiltinOrders();
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'name' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'name',
'reverse' => true,
'type' => 'string',
'unique' => true,
),
'milestoneNumber' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'milestoneNumber',
'type' => 'int',
),
);
}
protected function getPagingValueMap($cursor, array $keys) {
$project = $this->loadCursorObject($cursor);
return array(
'id' => $project->getID(),
'name' => $project->getName(),
);
}
public function getSlugMap() {
if ($this->slugMap === null) {
throw new PhutilInvalidStateException('execute');
}
return $this->slugMap;
}
protected function willExecute() {
$this->slugMap = array();
$this->slugNormals = array();
$this->allSlugs = array();
if ($this->slugs) {
foreach ($this->slugs as $slug) {
if (PhabricatorSlug::isValidProjectSlug($slug)) {
$normal = PhabricatorSlug::normalizeProjectSlug($slug);
$this->slugNormals[$slug] = $normal;
$this->allSlugs[$normal] = $normal;
}
// NOTE: At least for now, we query for the normalized slugs but also
// for the slugs exactly as entered. This allows older projects with
// slugs that are no longer valid to continue to work.
$this->allSlugs[$slug] = $slug;
}
}
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function willFilterPage(array $projects) {
$ancestor_paths = array();
foreach ($projects as $project) {
foreach ($project->getAncestorProjectPaths() as $path) {
$ancestor_paths[$path] = $path;
}
}
if ($ancestor_paths) {
$ancestors = id(new PhabricatorProject())->loadAllWhere(
'projectPath IN (%Ls)',
$ancestor_paths);
} else {
$ancestors = array();
}
$projects = $this->linkProjectGraph($projects, $ancestors);
$viewer_phid = $this->getViewer()->getPHID();
$material_type = PhabricatorProjectMaterializedMemberEdgeType::EDGECONST;
$watcher_type = PhabricatorObjectHasWatcherEdgeType::EDGECONST;
$types = array();
$types[] = $material_type;
if ($this->needWatchers) {
$types[] = $watcher_type;
}
$all_graph = $this->getAllReachableAncestors($projects);
// NOTE: Although we may not need much information about ancestors, we
// always need to test if the viewer is a member, because we will return
// ancestor projects to the policy filter via ExtendedPolicy calls. If
// we skip populating membership data on a parent, the policy framework
// will think the user is not a member of the parent project.
$all_sources = array();
foreach ($all_graph as $project) {
// For milestones, we need parent members.
if ($project->isMilestone()) {
$parent_phid = $project->getParentProjectPHID();
$all_sources[$parent_phid] = $parent_phid;
}
$phid = $project->getPHID();
$all_sources[$phid] = $phid;
}
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($all_sources)
->withEdgeTypes($types);
$need_all_edges =
$this->needMembers ||
$this->needWatchers ||
$this->needAncestorMembers;
// If we only need to know if the viewer is a member, we can restrict
// the query to just their PHID.
$any_edges = true;
if (!$need_all_edges) {
if ($viewer_phid) {
$edge_query->withDestinationPHIDs(array($viewer_phid));
} else {
// If we don't need members or watchers and don't have a viewer PHID
// (viewer is logged-out or omnipotent), they'll never be a member
// so we don't need to issue this query at all.
$any_edges = false;
}
}
if ($any_edges) {
$edge_query->execute();
}
$membership_projects = array();
foreach ($all_graph as $project) {
$project_phid = $project->getPHID();
if ($project->isMilestone()) {
$source_phids = array($project->getParentProjectPHID());
} else {
$source_phids = array($project_phid);
}
if ($any_edges) {
$member_phids = $edge_query->getDestinationPHIDs(
$source_phids,
array($material_type));
} else {
$member_phids = array();
}
if (in_array($viewer_phid, $member_phids)) {
$membership_projects[$project_phid] = $project;
}
if ($this->needMembers || $this->needAncestorMembers) {
$project->attachMemberPHIDs($member_phids);
}
if ($this->needWatchers) {
$watcher_phids = $edge_query->getDestinationPHIDs(
array($project_phid),
array($watcher_type));
$project->attachWatcherPHIDs($watcher_phids);
$project->setIsUserWatcher(
$viewer_phid,
in_array($viewer_phid, $watcher_phids));
}
}
// If we loaded ancestor members, we've already populated membership
// lists above, so we can skip this step.
if (!$this->needAncestorMembers) {
$member_graph = $this->getAllReachableAncestors($membership_projects);
foreach ($all_graph as $phid => $project) {
$is_member = isset($member_graph[$phid]);
$project->setIsUserMember($viewer_phid, $is_member);
}
}
return $projects;
}
protected function didFilterPage(array $projects) {
+ $viewer = $this->getViewer();
+
if ($this->needImages) {
- $file_phids = mpull($projects, 'getProfileImagePHID');
- $file_phids = array_filter($file_phids);
+ $need_images = $projects;
+
+ // First, try to load custom profile images for any projects with custom
+ // images.
+ $file_phids = array();
+ foreach ($need_images as $key => $project) {
+ $image_phid = $project->getProfileImagePHID();
+ if ($image_phid) {
+ $file_phids[$key] = $image_phid;
+ }
+ }
+
if ($file_phids) {
$files = id(new PhabricatorFileQuery())
->setParentQuery($this)
- ->setViewer($this->getViewer())
+ ->setViewer($viewer)
->withPHIDs($file_phids)
->execute();
$files = mpull($files, null, 'getPHID');
- } else {
- $files = array();
+
+ foreach ($file_phids as $key => $image_phid) {
+ $file = idx($files, $image_phid);
+ if (!$file) {
+ continue;
+ }
+
+ $need_images[$key]->attachProfileImageFile($file);
+ unset($need_images[$key]);
+ }
}
- foreach ($projects as $project) {
- $file = idx($files, $project->getProfileImagePHID());
- if (!$file) {
- $builtin = PhabricatorProjectIconSet::getIconImage(
- $project->getIcon());
- $file = PhabricatorFile::loadBuiltin($this->getViewer(),
- 'projects/'.$builtin);
+ // For projects with default images, or projects where the custom image
+ // failed to load, load a builtin image.
+ if ($need_images) {
+ $builtin_map = array();
+ $builtins = array();
+ foreach ($need_images as $key => $project) {
+ $icon = $project->getIcon();
+
+ $builtin_name = PhabricatorProjectIconSet::getIconImage($icon);
+ $builtin_name = 'projects/'.$builtin_name;
+
+ $builtin = id(new PhabricatorFilesOnDiskBuiltinFile())
+ ->setName($builtin_name);
+
+ $builtin_key = $builtin->getBuiltinFileKey();
+
+ $builtins[] = $builtin;
+ $builtin_map[$key] = $builtin_key;
+ }
+
+ $builtin_files = PhabricatorFile::loadBuiltins(
+ $viewer,
+ $builtins);
+
+ foreach ($need_images as $key => $project) {
+ $builtin_key = $builtin_map[$key];
+ $builtin_file = $builtin_files[$builtin_key];
+ $project->attachProfileImageFile($builtin_file);
}
- $project->attachProfileImageFile($file);
}
}
$this->loadSlugs($projects);
return $projects;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->status != self::STATUS_ANY) {
switch ($this->status) {
case self::STATUS_OPEN:
case self::STATUS_ACTIVE:
$filter = array(
PhabricatorProjectStatus::STATUS_ACTIVE,
);
break;
case self::STATUS_CLOSED:
case self::STATUS_ARCHIVED:
$filter = array(
PhabricatorProjectStatus::STATUS_ARCHIVED,
);
break;
default:
throw new Exception(
pht(
"Unknown project status '%s'!",
$this->status));
}
$where[] = qsprintf(
$conn,
'status IN (%Ld)',
$filter);
}
if ($this->statuses !== null) {
$where[] = qsprintf(
$conn,
'status IN (%Ls)',
$this->statuses);
}
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
if ($this->memberPHIDs !== null) {
$where[] = qsprintf(
$conn,
'e.dst IN (%Ls)',
$this->memberPHIDs);
}
if ($this->watcherPHIDs !== null) {
$where[] = qsprintf(
$conn,
'w.dst IN (%Ls)',
$this->watcherPHIDs);
}
if ($this->slugs !== null) {
$where[] = qsprintf(
$conn,
'slug.slug IN (%Ls)',
$this->allSlugs);
}
if ($this->names !== null) {
$where[] = qsprintf(
$conn,
'name IN (%Ls)',
$this->names);
}
if ($this->namePrefixes) {
$parts = array();
foreach ($this->namePrefixes as $name_prefix) {
$parts[] = qsprintf(
$conn,
'name LIKE %>',
$name_prefix);
}
$where[] = '('.implode(' OR ', $parts).')';
}
if ($this->icons !== null) {
$where[] = qsprintf(
$conn,
'icon IN (%Ls)',
$this->icons);
}
if ($this->colors !== null) {
$where[] = qsprintf(
$conn,
'color IN (%Ls)',
$this->colors);
}
if ($this->parentPHIDs !== null) {
$where[] = qsprintf(
$conn,
'parentProjectPHID IN (%Ls)',
$this->parentPHIDs);
}
if ($this->ancestorPHIDs !== null) {
$ancestor_paths = queryfx_all(
$conn,
'SELECT projectPath, projectDepth FROM %T WHERE phid IN (%Ls)',
id(new PhabricatorProject())->getTableName(),
$this->ancestorPHIDs);
if (!$ancestor_paths) {
throw new PhabricatorEmptyQueryException();
}
$sql = array();
foreach ($ancestor_paths as $ancestor_path) {
$sql[] = qsprintf(
$conn,
'(projectPath LIKE %> AND projectDepth > %d)',
$ancestor_path['projectPath'],
$ancestor_path['projectDepth']);
}
$where[] = '('.implode(' OR ', $sql).')';
$where[] = qsprintf(
$conn,
'parentProjectPHID IS NOT NULL');
}
if ($this->isMilestone !== null) {
if ($this->isMilestone) {
$where[] = qsprintf(
$conn,
'milestoneNumber IS NOT NULL');
} else {
$where[] = qsprintf(
$conn,
'milestoneNumber IS NULL');
}
}
if ($this->hasSubprojects !== null) {
$where[] = qsprintf(
$conn,
'hasSubprojects = %d',
(int)$this->hasSubprojects);
}
if ($this->minDepth !== null) {
$where[] = qsprintf(
$conn,
'projectDepth >= %d',
$this->minDepth);
}
if ($this->maxDepth !== null) {
$where[] = qsprintf(
$conn,
'projectDepth <= %d',
$this->maxDepth);
}
if ($this->minMilestoneNumber !== null) {
$where[] = qsprintf(
$conn,
'milestoneNumber >= %d',
$this->minMilestoneNumber);
}
if ($this->maxMilestoneNumber !== null) {
$where[] = qsprintf(
$conn,
'milestoneNumber <= %d',
$this->maxMilestoneNumber);
}
return $where;
}
protected function shouldGroupQueryResultRows() {
if ($this->memberPHIDs || $this->watcherPHIDs || $this->nameTokens) {
return true;
}
return parent::shouldGroupQueryResultRows();
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = parent::buildJoinClauseParts($conn);
if ($this->memberPHIDs !== null) {
$joins[] = qsprintf(
$conn,
'JOIN %T e ON e.src = p.phid AND e.type = %d',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorProjectMaterializedMemberEdgeType::EDGECONST);
}
if ($this->watcherPHIDs !== null) {
$joins[] = qsprintf(
$conn,
'JOIN %T w ON w.src = p.phid AND w.type = %d',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorObjectHasWatcherEdgeType::EDGECONST);
}
if ($this->slugs !== null) {
$joins[] = qsprintf(
$conn,
'JOIN %T slug on slug.projectPHID = p.phid',
id(new PhabricatorProjectSlug())->getTableName());
}
if ($this->nameTokens !== null) {
$name_tokens = $this->getNameTokensForQuery($this->nameTokens);
foreach ($name_tokens as $key => $token) {
$token_table = 'token_'.$key;
$joins[] = qsprintf(
$conn,
'JOIN %T %T ON %T.projectID = p.id AND %T.token LIKE %>',
PhabricatorProject::TABLE_DATASOURCE_TOKEN,
$token_table,
$token_table,
$token_table,
$token);
}
}
return $joins;
}
public function getQueryApplicationClass() {
return 'PhabricatorProjectApplication';
}
protected function getPrimaryTableAlias() {
return 'p';
}
private function linkProjectGraph(array $projects, array $ancestors) {
$ancestor_map = mpull($ancestors, null, 'getPHID');
$projects_map = mpull($projects, null, 'getPHID');
$all_map = $projects_map + $ancestor_map;
$done = array();
foreach ($projects as $key => $project) {
$seen = array($project->getPHID() => true);
if (!$this->linkProject($project, $all_map, $done, $seen)) {
$this->didRejectResult($project);
unset($projects[$key]);
continue;
}
foreach ($project->getAncestorProjects() as $ancestor) {
$seen[$ancestor->getPHID()] = true;
}
}
return $projects;
}
private function linkProject($project, array $all, array $done, array $seen) {
$parent_phid = $project->getParentProjectPHID();
// This project has no parent, so just attach `null` and return.
if (!$parent_phid) {
$project->attachParentProject(null);
return true;
}
// This project has a parent, but it failed to load.
if (empty($all[$parent_phid])) {
return false;
}
// Test for graph cycles. If we encounter one, we're going to hide the
// entire cycle since we can't meaningfully resolve it.
if (isset($seen[$parent_phid])) {
return false;
}
$seen[$parent_phid] = true;
$parent = $all[$parent_phid];
$project->attachParentProject($parent);
if (!empty($done[$parent_phid])) {
return true;
}
return $this->linkProject($parent, $all, $done, $seen);
}
private function getAllReachableAncestors(array $projects) {
$ancestors = array();
$seen = mpull($projects, null, 'getPHID');
$stack = $projects;
while ($stack) {
$project = array_pop($stack);
$phid = $project->getPHID();
$ancestors[$phid] = $project;
$parent_phid = $project->getParentProjectPHID();
if (!$parent_phid) {
continue;
}
if (isset($seen[$parent_phid])) {
continue;
}
$seen[$parent_phid] = true;
$stack[] = $project->getParentProject();
}
return $ancestors;
}
private function loadSlugs(array $projects) {
// Build a map from primary slugs to projects.
$primary_map = array();
foreach ($projects as $project) {
$primary_slug = $project->getPrimarySlug();
if ($primary_slug === null) {
continue;
}
$primary_map[$primary_slug] = $project;
}
// Link up all of the queried slugs which correspond to primary
// slugs. If we can link up everything from this (no slugs were queried,
// or only primary slugs were queried) we don't need to load anything
// else.
$unknown = $this->slugNormals;
foreach ($unknown as $input => $normal) {
if (isset($primary_map[$input])) {
$match = $input;
} else if (isset($primary_map[$normal])) {
$match = $normal;
} else {
continue;
}
$this->slugMap[$input] = array(
'slug' => $match,
'projectPHID' => $primary_map[$match]->getPHID(),
);
unset($unknown[$input]);
}
// If we need slugs, we have to load everything.
// If we still have some queried slugs which we haven't mapped, we only
// need to look for them.
// If we've mapped everything, we don't have to do any work.
$project_phids = mpull($projects, 'getPHID');
if ($this->needSlugs) {
$slugs = id(new PhabricatorProjectSlug())->loadAllWhere(
'projectPHID IN (%Ls)',
$project_phids);
} else if ($unknown) {
$slugs = id(new PhabricatorProjectSlug())->loadAllWhere(
'projectPHID IN (%Ls) AND slug IN (%Ls)',
$project_phids,
$unknown);
} else {
$slugs = array();
}
// Link up any slugs we were not able to link up earlier.
$extra_map = mpull($slugs, 'getProjectPHID', 'getSlug');
foreach ($unknown as $input => $normal) {
if (isset($extra_map[$input])) {
$match = $input;
} else if (isset($extra_map[$normal])) {
$match = $normal;
} else {
continue;
}
$this->slugMap[$input] = array(
'slug' => $match,
'projectPHID' => $extra_map[$match],
);
unset($unknown[$input]);
}
if ($this->needSlugs) {
$slug_groups = mgroup($slugs, 'getProjectPHID');
foreach ($projects as $project) {
$project_slugs = idx($slug_groups, $project->getPHID(), array());
$project->attachSlugs($project_slugs);
}
}
}
private function getNameTokensForQuery(array $tokens) {
// When querying for projects by name, only actually search for the five
// longest tokens. MySQL can get grumpy with a large number of JOINs
// with LIKEs and queries for more than 5 tokens are essentially never
// legitimate searches for projects, but users copy/pasting nonsense.
// See also PHI47.
$length_map = array();
foreach ($tokens as $token) {
$length_map[$token] = strlen($token);
}
arsort($length_map);
$length_map = array_slice($length_map, 0, 5, true);
return array_keys($length_map);
}
}
diff --git a/src/applications/project/query/PhabricatorProjectSearchEngine.php b/src/applications/project/query/PhabricatorProjectSearchEngine.php
index 452085fa9..e38532c41 100644
--- a/src/applications/project/query/PhabricatorProjectSearchEngine.php
+++ b/src/applications/project/query/PhabricatorProjectSearchEngine.php
@@ -1,261 +1,273 @@
<?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'),
+ 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/releeph/editor/ReleephRequestTransactionalEditor.php b/src/applications/releeph/editor/ReleephRequestTransactionalEditor.php
index 471055704..da488d9c7 100644
--- a/src/applications/releeph/editor/ReleephRequestTransactionalEditor.php
+++ b/src/applications/releeph/editor/ReleephRequestTransactionalEditor.php
@@ -1,309 +1,307 @@
<?php
final class ReleephRequestTransactionalEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorReleephApplication';
}
public function getEditorObjectsDescription() {
return pht('Releeph Requests');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = ReleephRequestTransaction::TYPE_COMMIT;
$types[] = ReleephRequestTransaction::TYPE_DISCOVERY;
$types[] = ReleephRequestTransaction::TYPE_EDIT_FIELD;
$types[] = ReleephRequestTransaction::TYPE_MANUAL_IN_BRANCH;
$types[] = ReleephRequestTransaction::TYPE_PICK_STATUS;
$types[] = ReleephRequestTransaction::TYPE_REQUEST;
$types[] = ReleephRequestTransaction::TYPE_USER_INTENT;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case ReleephRequestTransaction::TYPE_REQUEST:
return $object->getRequestCommitPHID();
case ReleephRequestTransaction::TYPE_EDIT_FIELD:
$field = newv($xaction->getMetadataValue('fieldClass'), array());
$value = $field->setReleephRequest($object)->getValue();
return $value;
case ReleephRequestTransaction::TYPE_USER_INTENT:
$user_phid = $xaction->getAuthorPHID();
return idx($object->getUserIntents(), $user_phid);
case ReleephRequestTransaction::TYPE_PICK_STATUS:
return (int)$object->getPickStatus();
break;
case ReleephRequestTransaction::TYPE_COMMIT:
return $object->getCommitIdentifier();
case ReleephRequestTransaction::TYPE_DISCOVERY:
return $object->getCommitPHID();
case ReleephRequestTransaction::TYPE_MANUAL_IN_BRANCH:
return $object->getInBranch();
}
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case ReleephRequestTransaction::TYPE_REQUEST:
case ReleephRequestTransaction::TYPE_USER_INTENT:
case ReleephRequestTransaction::TYPE_EDIT_FIELD:
case ReleephRequestTransaction::TYPE_PICK_STATUS:
case ReleephRequestTransaction::TYPE_COMMIT:
case ReleephRequestTransaction::TYPE_DISCOVERY:
case ReleephRequestTransaction::TYPE_MANUAL_IN_BRANCH:
return $xaction->getNewValue();
}
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$new = $xaction->getNewValue();
switch ($xaction->getTransactionType()) {
case ReleephRequestTransaction::TYPE_REQUEST:
$object->setRequestCommitPHID($new);
break;
case ReleephRequestTransaction::TYPE_USER_INTENT:
$user_phid = $xaction->getAuthorPHID();
$intents = $object->getUserIntents();
$intents[$user_phid] = $new;
$object->setUserIntents($intents);
break;
case ReleephRequestTransaction::TYPE_EDIT_FIELD:
$field = newv($xaction->getMetadataValue('fieldClass'), array());
$field
->setReleephRequest($object)
->setValue($new);
break;
case ReleephRequestTransaction::TYPE_PICK_STATUS:
$object->setPickStatus($new);
break;
case ReleephRequestTransaction::TYPE_COMMIT:
$this->setInBranchFromAction($object, $xaction);
$object->setCommitIdentifier($new);
break;
case ReleephRequestTransaction::TYPE_DISCOVERY:
$this->setInBranchFromAction($object, $xaction);
$object->setCommitPHID($new);
break;
case ReleephRequestTransaction::TYPE_MANUAL_IN_BRANCH:
$object->setInBranch((int)$new);
break;
}
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
return;
}
protected function filterTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
// Remove TYPE_DISCOVERY xactions that are the result of a reparse.
$previously_discovered_commits = array();
$discovery_xactions = idx(
mgroup($xactions, 'getTransactionType'),
ReleephRequestTransaction::TYPE_DISCOVERY);
if ($discovery_xactions) {
$previous_xactions = id(new ReleephRequestTransactionQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withObjectPHIDs(array($object->getPHID()))
->execute();
foreach ($previous_xactions as $xaction) {
if ($xaction->getTransactionType() ===
ReleephRequestTransaction::TYPE_DISCOVERY) {
$commit_phid = $xaction->getNewValue();
$previously_discovered_commits[$commit_phid] = true;
}
}
}
foreach ($xactions as $key => $xaction) {
if ($xaction->getTransactionType() ===
ReleephRequestTransaction::TYPE_DISCOVERY &&
idx($previously_discovered_commits, $xaction->getNewValue())) {
unset($xactions[$key]);
}
}
return parent::filterTransactions($object, $xactions);
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
// Avoid sending emails that only talk about commit discovery.
$types = array_unique(mpull($xactions, 'getTransactionType'));
if ($types === array(ReleephRequestTransaction::TYPE_DISCOVERY)) {
return false;
}
// Don't email people when we discover that something picks or reverts OK.
if ($types === array(ReleephRequestTransaction::TYPE_PICK_STATUS)) {
if (!mfilter($xactions, 'isBoringPickStatus', true /* negate */)) {
// If we effectively call "isInterestingPickStatus" and get nothing...
return false;
}
}
return true;
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new ReleephRequestReplyHandler())
->setActor($this->getActor())
->setMailReceiver($object);
}
protected function getMailSubjectPrefix() {
return '[Releeph]';
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
- $phid = $object->getPHID();
$title = $object->getSummaryForDisplay();
return id(new PhabricatorMetaMTAMail())
- ->setSubject("RQ{$id}: {$title}")
- ->addHeader('Thread-Topic', "RQ{$id}: {$phid}");
+ ->setSubject("RQ{$id}: {$title}");
}
protected function getMailTo(PhabricatorLiskDAO $object) {
$to_phids = array();
$product = $object->getBranch()->getProduct();
foreach ($product->getPushers() as $phid) {
$to_phids[] = $phid;
}
foreach ($object->getUserIntents() as $phid => $intent) {
$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);
$rq = $object;
$releeph_branch = $rq->getBranch();
$releeph_project = $releeph_branch->getProduct();
/**
* If any of the events we are emailing about were about a pick failure
* (and/or a revert failure?), include pick failure instructions.
*/
$has_pick_failure = false;
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() ===
ReleephRequestTransaction::TYPE_PICK_STATUS &&
$xaction->getNewValue() === ReleephRequest::PICK_FAILED) {
$has_pick_failure = true;
break;
}
}
if ($has_pick_failure) {
$instructions = $releeph_project->getDetail('pick_failure_instructions');
if ($instructions) {
$body->addRemarkupSection(
pht('PICK FAILURE INSTRUCTIONS'),
$instructions);
}
}
$name = sprintf('RQ%s: %s', $rq->getID(), $rq->getSummaryForDisplay());
$body->addTextSection(
pht('RELEEPH REQUEST'),
$name."\n".
PhabricatorEnv::getProductionURI('/RQ'.$rq->getID()));
$project_and_branch = sprintf(
'%s - %s',
$releeph_project->getName(),
$releeph_branch->getDisplayNameWithDetail());
$body->addTextSection(
pht('RELEEPH BRANCH'),
$project_and_branch."\n".
PhabricatorEnv::getProductionURI($releeph_branch->getURI()));
return $body;
}
private function setInBranchFromAction(
ReleephRequest $rq,
ReleephRequestTransaction $xaction) {
$action = $xaction->getMetadataValue('action');
switch ($action) {
case 'pick':
$rq->setInBranch(1);
break;
case 'revert':
$rq->setInBranch(0);
break;
default:
$id = $rq->getID();
$type = $xaction->getTransactionType();
$new = $xaction->getNewValue();
phlog(
pht(
"Unknown discovery action '%s' for xaction of type %s ".
"with new value %s mentioning %s!",
$action,
$type,
$new,
'RQ'.$id));
break;
}
return $this;
}
}
diff --git a/src/applications/releeph/field/specification/ReleephDiffChurnFieldSpecification.php b/src/applications/releeph/field/specification/ReleephDiffChurnFieldSpecification.php
index 112ef0988..3b472bbae 100644
--- a/src/applications/releeph/field/specification/ReleephDiffChurnFieldSpecification.php
+++ b/src/applications/releeph/field/specification/ReleephDiffChurnFieldSpecification.php
@@ -1,90 +1,90 @@
<?php
final class ReleephDiffChurnFieldSpecification
extends ReleephFieldSpecification {
const REJECTIONS_WEIGHT = 30;
const COMMENTS_WEIGHT = 7;
const UPDATES_WEIGHT = 10;
const MAX_POINTS = 100;
public function getFieldKey() {
return 'churn';
}
public function getName() {
return pht('Churn');
}
public function renderPropertyViewValue(array $handles) {
$requested_object = $this->getObject()->getRequestedObject();
if (!($requested_object instanceof DifferentialRevision)) {
return null;
}
$diff_rev = $requested_object;
$xactions = id(new DifferentialTransactionQuery())
->setViewer($this->getViewer())
->withObjectPHIDs(array($diff_rev->getPHID()))
->execute();
$rejections = 0;
$comments = 0;
$updates = 0;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
$comments++;
break;
- case DifferentialTransaction::TYPE_UPDATE:
+ case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE:
$updates++;
break;
case DifferentialTransaction::TYPE_ACTION:
switch ($xaction->getNewValue()) {
case DifferentialAction::ACTION_REJECT:
$rejections++;
break;
}
break;
}
}
$points =
self::REJECTIONS_WEIGHT * $rejections +
self::COMMENTS_WEIGHT * $comments +
self::UPDATES_WEIGHT * $updates;
if ($points === 0) {
$points = 0.15 * self::MAX_POINTS;
$blurb = pht('Silent diff');
} else {
$parts = array();
if ($rejections) {
$parts[] = pht('%s rejection(s)', new PhutilNumber($rejections));
}
if ($comments) {
$parts[] = pht('%s comment(s)', new PhutilNumber($comments));
}
if ($updates) {
$parts[] = pht('%s update(s)', new PhutilNumber($updates));
}
if (count($parts) === 0) {
$blurb = '';
} else if (count($parts) === 1) {
$blurb = head($parts);
} else {
$last = array_pop($parts);
$blurb = pht('%s and %s', implode(', ', $parts), $last);
}
}
return id(new AphrontProgressBarView())
->setValue($points)
->setMax(self::MAX_POINTS)
->setCaption($blurb)
->render();
}
}
diff --git a/src/applications/repository/engine/PhabricatorRepositoryEngine.php b/src/applications/repository/engine/PhabricatorRepositoryEngine.php
index 244ed0cd4..fed5b0952 100644
--- a/src/applications/repository/engine/PhabricatorRepositoryEngine.php
+++ b/src/applications/repository/engine/PhabricatorRepositoryEngine.php
@@ -1,204 +1,203 @@
<?php
/**
* @task config Configuring Repository Engines
* @task internal Internals
*/
abstract class PhabricatorRepositoryEngine extends Phobject {
private $repository;
private $verbose;
/**
* @task config
*/
public function setRepository(PhabricatorRepository $repository) {
$this->repository = $repository;
return $this;
}
/**
* @task config
*/
protected function getRepository() {
if ($this->repository === null) {
throw new PhutilInvalidStateException('setRepository');
}
return $this->repository;
}
/**
* @task config
*/
public function setVerbose($verbose) {
$this->verbose = $verbose;
return $this;
}
/**
* @task config
*/
public function getVerbose() {
return $this->verbose;
}
public function getViewer() {
return PhabricatorUser::getOmnipotentUser();
}
protected function newRepositoryLock(
PhabricatorRepository $repository,
$lock_key,
$lock_device_only) {
- $lock_parts = array();
- $lock_parts[] = $lock_key;
- $lock_parts[] = $repository->getID();
+ $lock_parts = array(
+ 'repositoryPHID' => $repository->getPHID(),
+ );
if ($lock_device_only) {
$device = AlmanacKeys::getLiveDevice();
if ($device) {
- $lock_parts[] = $device->getID();
+ $lock_parts['devicePHID'] = $device->getPHID();
}
}
- $lock_name = implode(':', $lock_parts);
- return PhabricatorGlobalLock::newLock($lock_name);
+ return PhabricatorGlobalLock::newLock($lock_key, $lock_parts);
}
/**
* Verify that the "origin" remote exists, and points at the correct URI.
*
* This catches or corrects some types of misconfiguration, and also repairs
* an issue where Git 1.7.1 does not create an "origin" for `--bare` clones.
* See T4041.
*
* @param PhabricatorRepository Repository to verify.
* @return void
*/
protected function verifyGitOrigin(PhabricatorRepository $repository) {
try {
list($remotes) = $repository->execxLocalCommand(
'remote show -n origin');
} catch (CommandException $ex) {
throw new PhutilProxyException(
pht(
'Expected to find a Git working copy at path "%s", but the '.
'path exists and is not a valid working copy. If you remove '.
'this directory, the daemons will automatically recreate it '.
'correctly. Phabricator will not destroy the directory for you '.
'because it can not be sure that it does not contain important '.
'data.',
$repository->getLocalPath()),
$ex);
}
$matches = null;
if (!preg_match('/^\s*Fetch URL:\s*(.*?)\s*$/m', $remotes, $matches)) {
throw new Exception(
pht(
"Expected '%s' in '%s'.",
'Fetch URL',
'git remote show -n origin'));
}
$remote_uri = $matches[1];
$expect_remote = $repository->getRemoteURI();
if ($remote_uri == 'origin') {
// If a remote does not exist, git pretends it does and prints out a
// made up remote where the URI is the same as the remote name. This is
// definitely not correct.
// Possibly, we should use `git remote --verbose` instead, which does not
// suffer from this problem (but is a little more complicated to parse).
$valid = false;
$exists = false;
} else {
$normal_type_git = PhabricatorRepositoryURINormalizer::TYPE_GIT;
$remote_normal = id(new PhabricatorRepositoryURINormalizer(
$normal_type_git,
$remote_uri))->getNormalizedPath();
$expect_normal = id(new PhabricatorRepositoryURINormalizer(
$normal_type_git,
$expect_remote))->getNormalizedPath();
$valid = ($remote_normal == $expect_normal);
$exists = true;
}
// These URIs may have plaintext HTTP credentials. If they do, censor
// them for display. See T12945.
$display_remote = phutil_censor_credentials($remote_uri);
$display_expect = phutil_censor_credentials($expect_remote);
if (!$valid) {
if (!$exists) {
// If there's no "origin" remote, just create it regardless of how
// strongly we own the working copy. There is almost no conceivable
// scenario in which this could do damage.
$this->log(
pht(
'Remote "origin" does not exist. Creating "origin", with '.
'URI "%s".',
$expect_remote));
$repository->execxLocalCommand(
'remote add origin %P',
$repository->getRemoteURIEnvelope());
// NOTE: This doesn't fetch the origin (it just creates it), so we won't
// know about origin branches until the next "pull" happens. That's fine
// for our purposes, but might impact things in the future.
} else {
if ($repository->canDestroyWorkingCopy()) {
// Bad remote, but we can try to repair it.
$this->log(
pht(
'Remote "origin" exists, but is pointed at the wrong URI, "%s". '.
'Resetting origin URI to "%s.',
$remote_uri,
$expect_remote));
$repository->execxLocalCommand(
'remote set-url origin %P',
$repository->getRemoteURIEnvelope());
} else {
// Bad remote and we aren't comfortable repairing it.
$message = pht(
'Working copy at "%s" has a mismatched origin URI, "%s". '.
'The expected origin URI is "%s". Fix your configuration, or '.
'set the remote URI correctly. To avoid breaking anything, '.
'Phabricator will not automatically fix this.',
$repository->getLocalPath(),
$display_remote,
$display_expect);
throw new Exception($message);
}
}
}
}
/**
* @task internal
*/
protected function log($pattern /* ... */) {
if ($this->getVerbose()) {
$console = PhutilConsole::getConsole();
$argv = func_get_args();
array_unshift($argv, "%s\n");
call_user_func_array(array($console, 'writeOut'), $argv);
}
return $this;
}
}
diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementUnpublishWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementUnpublishWorkflow.php
new file mode 100644
index 000000000..5a6afb8c6
--- /dev/null
+++ b/src/applications/repository/management/PhabricatorRepositoryManagementUnpublishWorkflow.php
@@ -0,0 +1,273 @@
+<?php
+
+final class PhabricatorRepositoryManagementUnpublishWorkflow
+ extends PhabricatorRepositoryManagementWorkflow {
+
+ protected function didConstruct() {
+ $this
+ ->setName('unpublish')
+ ->setExamples(
+ '**unpublish** [__options__] __repository__')
+ ->setSynopsis(
+ pht(
+ 'Unpublish all feed stories and notifications that a repository '.
+ 'has generated. Keep expectations low; can not rewind time.'))
+ ->setArguments(
+ array(
+ array(
+ 'name' => 'force',
+ 'help' => pht('Do not prompt for confirmation.'),
+ ),
+ array(
+ 'name' => 'dry-run',
+ 'help' => pht('Do not perform any writes.'),
+ ),
+ array(
+ 'name' => 'repositories',
+ 'wildcard' => true,
+ ),
+ ));
+ }
+
+ public function execute(PhutilArgumentParser $args) {
+ $viewer = $this->getViewer();
+ $is_force = $args->getArg('force');
+ $is_dry_run = $args->getArg('dry-run');
+
+ $repositories = $this->loadLocalRepositories($args, 'repositories');
+ if (count($repositories) !== 1) {
+ throw new PhutilArgumentUsageException(
+ pht('Specify exactly one repository to unpublish.'));
+ }
+ $repository = head($repositories);
+
+ if (!$is_force) {
+ echo tsprintf(
+ "%s\n",
+ pht(
+ 'This script will unpublish all feed stories and notifications '.
+ 'which a repository generated during import. This action can not '.
+ 'be undone.'));
+
+ $prompt = pht(
+ 'Permanently unpublish "%s"?',
+ $repository->getDisplayName());
+ if (!phutil_console_confirm($prompt)) {
+ throw new PhutilArgumentUsageException(
+ pht('User aborted workflow.'));
+ }
+ }
+
+ $commits = id(new DiffusionCommitQuery())
+ ->setViewer($viewer)
+ ->withRepositoryPHIDs(array($repository->getPHID()))
+ ->execute();
+
+ echo pht("Will unpublish %s commits.\n", count($commits));
+
+ foreach ($commits as $commit) {
+ $this->unpublishCommit($commit, $is_dry_run);
+ }
+
+ return 0;
+ }
+
+ private function unpublishCommit(
+ PhabricatorRepositoryCommit $commit,
+ $is_dry_run) {
+ $viewer = $this->getViewer();
+
+ echo tsprintf(
+ "%s\n",
+ pht(
+ 'Unpublishing commit "%s".',
+ $commit->getMonogram()));
+
+ $stories = id(new PhabricatorFeedQuery())
+ ->setViewer($viewer)
+ ->withFilterPHIDs(array($commit->getPHID()))
+ ->execute();
+
+ if ($stories) {
+ echo tsprintf(
+ "%s\n",
+ pht(
+ 'Found %s feed storie(s).',
+ count($stories)));
+
+ if (!$is_dry_run) {
+ $engine = new PhabricatorDestructionEngine();
+ foreach ($stories as $story) {
+ $story_data = $story->getStoryData();
+ $engine->destroyObject($story_data);
+ }
+
+ echo tsprintf(
+ "%s\n",
+ pht(
+ 'Destroyed %s feed storie(s).',
+ count($stories)));
+ }
+ }
+
+ $edge_types = array(
+ PhabricatorObjectMentionsObjectEdgeType::EDGECONST => true,
+ DiffusionCommitHasTaskEdgeType::EDGECONST => true,
+ DiffusionCommitHasRevisionEdgeType::EDGECONST => true,
+ DiffusionCommitRevertsCommitEdgeType::EDGECONST => true,
+ );
+
+ $query = id(new PhabricatorEdgeQuery())
+ ->withSourcePHIDs(array($commit->getPHID()))
+ ->withEdgeTypes(array_keys($edge_types));
+ $edges = $query->execute();
+
+ foreach ($edges[$commit->getPHID()] as $type => $edge_list) {
+ foreach ($edge_list as $edge) {
+ $dst = $edge['dst'];
+
+ echo tsprintf(
+ "%s\n",
+ pht(
+ 'Commit "%s" has edge of type "%s" to object "%s".',
+ $commit->getMonogram(),
+ $type,
+ $dst));
+
+ $object = id(new PhabricatorObjectQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(array($dst))
+ ->executeOne();
+ if ($object) {
+ if ($object instanceof PhabricatorApplicationTransactionInterface) {
+ $this->unpublishEdgeTransaction(
+ $commit,
+ $type,
+ $object,
+ $is_dry_run);
+ }
+ }
+ }
+ }
+ }
+
+ private function unpublishEdgeTransaction(
+ $src,
+ $type,
+ PhabricatorApplicationTransactionInterface $dst,
+ $is_dry_run) {
+ $viewer = $this->getViewer();
+
+ $query = PhabricatorApplicationTransactionQuery::newQueryForObject($dst)
+ ->setViewer($viewer)
+ ->withObjectPHIDs(array($dst->getPHID()));
+
+ $xactions = id(clone $query)
+ ->withTransactionTypes(
+ array(
+ PhabricatorTransactions::TYPE_EDGE,
+ ))
+ ->execute();
+
+ $type_obj = PhabricatorEdgeType::getByConstant($type);
+ $inverse_type = $type_obj->getInverseEdgeConstant();
+
+ $engine = new PhabricatorDestructionEngine();
+ foreach ($xactions as $xaction) {
+ $edge_type = $xaction->getMetadataValue('edge:type');
+ if ($edge_type != $inverse_type) {
+ // Some other type of edge was edited.
+ continue;
+ }
+
+ $record = PhabricatorEdgeChangeRecord::newFromTransaction($xaction);
+ $changed = $record->getChangedPHIDs();
+ if ($changed !== array($src->getPHID())) {
+ // Affected objects were not just the object we're unpublishing.
+ continue;
+ }
+
+ echo tsprintf(
+ "%s\n",
+ pht(
+ 'Found edge transaction "%s" on object "%s" for type "%s".',
+ $xaction->getPHID(),
+ $dst->getPHID(),
+ $type));
+
+ if (!$is_dry_run) {
+ $engine->destroyObject($xaction);
+
+ echo tsprintf(
+ "%s\n",
+ pht(
+ 'Destroyed transaction "%s" on object "%s".',
+ $xaction->getPHID(),
+ $dst->getPHID()));
+ }
+ }
+
+ if ($type === DiffusionCommitHasTaskEdgeType::EDGECONST) {
+ $xactions = id(clone $query)
+ ->withTransactionTypes(
+ array(
+ ManiphestTaskStatusTransaction::TRANSACTIONTYPE,
+ ))
+ ->execute();
+
+ if ($xactions) {
+ foreach ($xactions as $xaction) {
+ $metadata = $xaction->getMetadata();
+ if (idx($metadata, 'commitPHID') === $src->getPHID()) {
+ echo tsprintf(
+ "%s\n",
+ pht(
+ 'MANUAL Task "%s" was likely closed improperly by "%s".',
+ $dst->getMonogram(),
+ $src->getMonogram()));
+ }
+ }
+ }
+ }
+
+ if ($type === DiffusionCommitHasRevisionEdgeType::EDGECONST) {
+ $xactions = id(clone $query)
+ ->withTransactionTypes(
+ array(
+ DifferentialRevisionCloseTransaction::TRANSACTIONTYPE,
+ ))
+ ->execute();
+
+ if ($xactions) {
+ foreach ($xactions as $xaction) {
+ $metadata = $xaction->getMetadata();
+ if (idx($metadata, 'isCommitClose')) {
+ if (idx($metadata, 'commitPHID') === $src->getPHID()) {
+ echo tsprintf(
+ "%s\n",
+ pht(
+ 'MANUAL Revision "%s" was likely closed improperly by "%s".',
+ $dst->getMonogram(),
+ $src->getMonogram()));
+ }
+ }
+ }
+ }
+ }
+
+ if (!$is_dry_run) {
+ id(new PhabricatorEdgeEditor())
+ ->removeEdge($src->getPHID(), $type, $dst->getPHID())
+ ->save();
+ echo tsprintf(
+ "%s\n",
+ pht(
+ 'Destroyed edge of type "%s" between "%s" and "%s".',
+ $type,
+ $src->getPHID(),
+ $dst->getPHID()));
+ }
+ }
+
+
+}
diff --git a/src/applications/repository/phid/PhabricatorRepositoryRepositoryPHIDType.php b/src/applications/repository/phid/PhabricatorRepositoryRepositoryPHIDType.php
index c938b31a6..ba78b0fe7 100644
--- a/src/applications/repository/phid/PhabricatorRepositoryRepositoryPHIDType.php
+++ b/src/applications/repository/phid/PhabricatorRepositoryRepositoryPHIDType.php
@@ -1,83 +1,85 @@
<?php
final class PhabricatorRepositoryRepositoryPHIDType
extends PhabricatorPHIDType {
const TYPECONST = 'REPO';
public function getTypeName() {
return pht('Repository');
}
public function getTypeIcon() {
return 'fa-code';
}
public function newObject() {
return new PhabricatorRepository();
}
public function getPHIDTypeApplicationClass() {
return 'PhabricatorDiffusionApplication';
}
protected function buildQueryForObjects(
PhabricatorObjectQuery $query,
array $phids) {
return id(new PhabricatorRepositoryQuery())
->withPHIDs($phids);
}
public function loadHandles(
PhabricatorHandleQuery $query,
array $handles,
array $objects) {
foreach ($handles as $phid => $handle) {
$repository = $objects[$phid];
$monogram = $repository->getMonogram();
$name = $repository->getName();
$uri = $repository->getURI();
- $handle->setName($monogram);
- $handle->setFullName("{$monogram} {$name}");
- $handle->setURI($uri);
+ $handle
+ ->setName($monogram)
+ ->setFullName("{$monogram} {$name}")
+ ->setURI($uri)
+ ->setMailStampName($monogram);
}
}
public function canLoadNamedObject($name) {
return preg_match('/^(r[A-Z]+|R[1-9]\d*)\z/', $name);
}
public function loadNamedObjects(
PhabricatorObjectQuery $query,
array $names) {
$results = array();
$id_map = array();
foreach ($names as $key => $name) {
$id = substr($name, 1);
$id_map[$id][] = $name;
$names[$key] = substr($name, 1);
}
$query = id(new PhabricatorRepositoryQuery())
->setViewer($query->getViewer())
->withIdentifiers($names);
if ($query->execute()) {
$objects = $query->getIdentifierMap();
foreach ($objects as $key => $object) {
foreach (idx($id_map, $key, array()) as $name) {
$results[$name] = $object;
}
}
return $results;
} else {
return array();
}
}
}
diff --git a/src/applications/repository/query/PhabricatorRepositoryPullEventQuery.php b/src/applications/repository/query/PhabricatorRepositoryPullEventQuery.php
index af60ee038..ce14a6f83 100644
--- a/src/applications/repository/query/PhabricatorRepositoryPullEventQuery.php
+++ b/src/applications/repository/query/PhabricatorRepositoryPullEventQuery.php
@@ -1,113 +1,135 @@
<?php
final class PhabricatorRepositoryPullEventQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $repositoryPHIDs;
private $pullerPHIDs;
+ private $epochMin;
+ private $epochMax;
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 withPullerPHIDs(array $puller_phids) {
$this->pullerPHIDs = $puller_phids;
return $this;
}
+ public function withEpochBetween($min, $max) {
+ $this->epochMin = $min;
+ $this->epochMax = $max;
+ return $this;
+ }
+
public function newResultObject() {
return new PhabricatorRepositoryPullEvent();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function willFilterPage(array $events) {
// If a pull targets an invalid repository or fails before authenticating,
// it may not have an associated repository.
$repository_phids = mpull($events, 'getRepositoryPHID');
$repository_phids = array_filter($repository_phids);
if ($repository_phids) {
$repositories = id(new PhabricatorRepositoryQuery())
->setViewer($this->getViewer())
->withPHIDs($repository_phids)
->execute();
$repositories = mpull($repositories, null, 'getPHID');
} else {
$repositories = array();
}
foreach ($events as $key => $event) {
$phid = $event->getRepositoryPHID();
if (!$phid) {
$event->attachRepository(null);
continue;
}
if (empty($repositories[$phid])) {
unset($events[$key]);
$this->didRejectResult($event);
continue;
}
$event->attachRepository($repositories[$phid]);
}
return $events;
}
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->repositoryPHIDs !== null) {
$where[] = qsprintf(
$conn,
'repositoryPHID IN (%Ls)',
$this->repositoryPHIDs);
}
if ($this->pullerPHIDs !== null) {
$where[] = qsprintf(
$conn,
'pullerPHID in (%Ls)',
$this->pullerPHIDs);
}
+ if ($this->epochMin !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'epoch >= %d',
+ $this->epochMin);
+ }
+
+ if ($this->epochMax !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'epoch <= %d',
+ $this->epochMax);
+ }
+
return $where;
}
public function getQueryApplicationClass() {
return 'PhabricatorDiffusionApplication';
}
}
diff --git a/src/applications/repository/query/PhabricatorRepositoryPushLogQuery.php b/src/applications/repository/query/PhabricatorRepositoryPushLogQuery.php
index 94a0b6922..cb097fae2 100644
--- a/src/applications/repository/query/PhabricatorRepositoryPushLogQuery.php
+++ b/src/applications/repository/query/PhabricatorRepositoryPushLogQuery.php
@@ -1,146 +1,159 @@
<?php
final class PhabricatorRepositoryPushLogQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $repositoryPHIDs;
private $pusherPHIDs;
private $refTypes;
private $newRefs;
private $pushEventPHIDs;
+ private $epochMin;
+ private $epochMax;
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 newResultObject() {
+ return new PhabricatorRepositoryPushLog();
+ }
+
protected function loadPage() {
- $table = new PhabricatorRepositoryPushLog();
- $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 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 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->repositoryPHIDs) {
+ if ($this->repositoryPHIDs !== null) {
$where[] = qsprintf(
- $conn_r,
+ $conn,
'repositoryPHID IN (%Ls)',
$this->repositoryPHIDs);
}
- if ($this->pusherPHIDs) {
+ if ($this->pusherPHIDs !== null) {
$where[] = qsprintf(
- $conn_r,
+ $conn,
'pusherPHID in (%Ls)',
$this->pusherPHIDs);
}
- if ($this->pushEventPHIDs) {
+ if ($this->pushEventPHIDs !== null) {
$where[] = qsprintf(
- $conn_r,
+ $conn,
'pushEventPHID in (%Ls)',
$this->pushEventPHIDs);
}
- if ($this->refTypes) {
+ if ($this->refTypes !== null) {
$where[] = qsprintf(
- $conn_r,
+ $conn,
'refType IN (%Ls)',
$this->refTypes);
}
- if ($this->newRefs) {
+ if ($this->newRefs !== null) {
$where[] = qsprintf(
- $conn_r,
+ $conn,
'refNew IN (%Ls)',
$this->newRefs);
}
- $where[] = $this->buildPagingClause($conn_r);
+ if ($this->epochMin !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'epoch >= %d',
+ $this->epochMin);
+ }
+
+ if ($this->epochMax !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'epoch <= %d',
+ $this->epochMax);
+ }
- return $this->formatWhereClause($where);
+ return $where;
}
public function getQueryApplicationClass() {
return 'PhabricatorDiffusionApplication';
}
}
diff --git a/src/applications/repository/query/PhabricatorRepositoryPushLogSearchEngine.php b/src/applications/repository/query/PhabricatorRepositoryPushLogSearchEngine.php
index d171b8099..7b626fff3 100644
--- a/src/applications/repository/query/PhabricatorRepositoryPushLogSearchEngine.php
+++ b/src/applications/repository/query/PhabricatorRepositoryPushLogSearchEngine.php
@@ -1,85 +1,255 @@
<?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']);
+ }
+
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.')),
id(new PhabricatorUsersSearchField())
->setKey('pusherPHIDs')
->setAliases(array('pusher', 'pushers', 'pusherPHID'))
->setLabel(pht('Pushers'))
->setDescription(
pht('Search for pull logs by specific users.')),
+ 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 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,
+ '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/query/PhabricatorRepositorySearchEngine.php b/src/applications/repository/query/PhabricatorRepositorySearchEngine.php
index 9155e8ce6..a04e63696 100644
--- a/src/applications/repository/query/PhabricatorRepositorySearchEngine.php
+++ b/src/applications/repository/query/PhabricatorRepositorySearchEngine.php
@@ -1,322 +1,329 @@
<?php
final class PhabricatorRepositorySearchEngine
extends PhabricatorApplicationSearchEngine {
public function getResultTypeDescription() {
return pht('Repositories');
}
public function getApplicationClassName() {
return 'PhabricatorDiffusionApplication';
}
public function newQuery() {
return id(new PhabricatorRepositoryQuery())
->needProjectPHIDs(true)
->needCommitCounts(true)
->needMostRecentCommits(true)
->needProfileImage(true);
}
protected function buildCustomSearchFields() {
return array(
id(new PhabricatorSearchStringListField())
->setLabel(pht('Callsigns'))
->setKey('callsigns'),
+ id(new PhabricatorSearchStringListField())
+ ->setLabel(pht('Short Names'))
+ ->setKey('shortNames'),
id(new PhabricatorSearchSelectField())
->setLabel(pht('Status'))
->setKey('status')
->setOptions($this->getStatusOptions()),
id(new PhabricatorSearchSelectField())
->setLabel(pht('Hosted'))
->setKey('hosted')
->setOptions($this->getHostedOptions()),
id(new PhabricatorSearchCheckboxesField())
->setLabel(pht('Types'))
->setKey('types')
->setOptions(PhabricatorRepositoryType::getAllRepositoryTypes()),
id(new PhabricatorSearchStringListField())
->setLabel(pht('URIs'))
->setKey('uris')
->setDescription(
pht('Search for repositories by clone/checkout URI.')),
// c4science custo
// Search repo by author
id(new PhabricatorUsersSearchField())
->setLabel(pht('Author'))
->setKey('authorPHIDs')
->setAliases(array('author', 'authors')),
);
}
protected function buildQueryFromParameters(array $map) {
$query = $this->newQuery();
// c4science custo
// Search repo by author
if ($map['authorPHIDs']) {
$viewer = $this->requireViewer();
$repo_transaction = id(new PhabricatorRepositoryTransactionQuery())
->setViewer($viewer)
->withAuthorPHIDs($map['authorPHIDs'])
->withTransactionTypes(array(PhabricatorTransactions::TYPE_CREATE))
->execute();
if(!empty($repo_transaction)) {
$repo_phids = mpull($repo_transaction, 'getObjectPHID');
}
if(!empty($repo_phids)) {
$query->withPHIDs($repo_phids);
} else {
// Force the query to have no result
$query->withPHIDs(array(NULL));
// If there's no result here, no point of continuing to filter
return $query;
}
}
if ($map['callsigns']) {
$query->withCallsigns($map['callsigns']);
}
+ if ($map['shortNames']) {
+ $query->withSlugs($map['shortNames']);
+ }
+
if ($map['status']) {
$status = idx($this->getStatusValues(), $map['status']);
if ($status) {
$query->withStatus($status);
}
}
if ($map['hosted']) {
$hosted = idx($this->getHostedValues(), $map['hosted']);
if ($hosted) {
$query->withHosted($hosted);
}
}
if ($map['types']) {
$query->withTypes($map['types']);
}
if ($map['uris']) {
$query->withURIs($map['uris']);
}
return $query;
}
protected function getURI($path) {
return '/diffusion/'.$path;
}
protected function getBuiltinQueryNames() {
// C4science customization
$names = array();
$viewer = $this->requireViewer();
if($viewer->getUsername()) {
$names += array(
'user' => pht('My Repositories'),
'project' => pht('Project Repositories'),
);
} else {
$names += array(
'active' => pht('Active Repositories'),
);
}
$names += array(
'all' => pht('All Repositories'),
);
// end of c4science customization
return $names;
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
switch ($query_key) {
case 'user': // c4science custo
return $query
->setParameter('status', 'open')
->setParameter('authorPHIDs', array('viewer()'));
case 'project': // c4science custo
return $query
->setParameter('status', 'open')
->setParameter('projectPHIDs', array('viewerprojects()'));
case 'active':
return $query->setParameter('status', 'open');
case 'all':
return $query;
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
private function getStatusOptions() {
return array(
'' => pht('Active and Inactive Repositories'),
'open' => pht('Active Repositories'),
'closed' => pht('Inactive Repositories'),
);
}
private function getStatusValues() {
return array(
'' => PhabricatorRepositoryQuery::STATUS_ALL,
'open' => PhabricatorRepositoryQuery::STATUS_OPEN,
'closed' => PhabricatorRepositoryQuery::STATUS_CLOSED,
);
}
private function getHostedOptions() {
return array(
'' => pht('Hosted and Remote Repositories'),
'phabricator' => pht('Hosted Repositories'),
'remote' => pht('Remote Repositories'),
);
}
private function getHostedValues() {
return array(
'' => PhabricatorRepositoryQuery::HOSTED_ALL,
'phabricator' => PhabricatorRepositoryQuery::HOSTED_PHABRICATOR,
'remote' => PhabricatorRepositoryQuery::HOSTED_REMOTE,
);
}
protected function getRequiredHandlePHIDsForResultList(
array $repositories,
PhabricatorSavedQuery $query) {
return array_mergev(mpull($repositories, 'getProjectPHIDs'));
}
protected function renderResultList(
array $repositories,
PhabricatorSavedQuery $query,
array $handles) {
assert_instances_of($repositories, 'PhabricatorRepository');
$viewer = $this->requireViewer();
$list = new PHUIObjectItemListView();
foreach ($repositories as $repository) {
$id = $repository->getID();
$item = id(new PHUIObjectItemView())
->setUser($viewer)
->setObject($repository)
->setHeader($repository->getName())
->setObjectName($repository->getMonogram())
->setHref($repository->getURI())
->setImageURI($repository->getProfileImageURI());
$commit = $repository->getMostRecentCommit();
if ($commit) {
$commit_link = phutil_tag(
'a',
array(
'href' => $commit->getURI(),
),
pht(
'%s: %s',
$commit->getLocalName(),
$commit->getSummary()));
$item->setSubhead($commit_link);
$item->setEpoch($commit->getEpoch());
}
$item->addIcon(
'none',
PhabricatorRepositoryType::getNameForRepositoryType(
$repository->getVersionControlSystem()));
$size = $repository->getCommitCount();
if ($size) {
$history_uri = $repository->generateURI(
array(
'action' => 'history',
));
$item->addAttribute(
phutil_tag(
'a',
array(
'href' => $history_uri,
),
pht('%s Commit(s)', new PhutilNumber($size))));
} else {
$item->addAttribute(pht('No Commits'));
}
$project_handles = array_select_keys(
$handles,
$repository->getProjectPHIDs());
if ($project_handles) {
$item->addAttribute(
id(new PHUIHandleTagListView())
->setSlim(true)
->setHandles($project_handles));
}
if (!$repository->isTracked()) {
$item->setDisabled(true);
$item->addIcon('disable-grey', pht('Inactive'));
} else if ($repository->isImporting()) {
$item->addIcon('fa-clock-o indigo', pht('Importing...'));
}
$list->addItem($item);
}
$result = new PhabricatorApplicationSearchResultView();
$result->setObjectList($list);
$result->setNoDataString(pht('No repositories found for this query.'));
return $result;
}
protected function willUseSavedQuery(PhabricatorSavedQuery $saved) {
$project_phids = $saved->getParameter('projectPHIDs', array());
$old = $saved->getParameter('projects', array());
foreach ($old as $phid) {
$project_phids[] = $phid;
}
$any = $saved->getParameter('anyProjectPHIDs', array());
foreach ($any as $project) {
$project_phids[] = 'any('.$project.')';
}
$saved->setParameter('projectPHIDs', $project_phids);
}
protected function getNewUserBody() {
$new_button = id(new PHUIButtonView())
->setTag('a')
->setText(pht('New Repository'))
->setHref('/diffusion/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('Import, create, or just browse repositories in Diffusion.'))
->addAction($new_button);
return $view;
}
}
diff --git a/src/applications/repository/storage/PhabricatorRepositoryAuditRequest.php b/src/applications/repository/storage/PhabricatorRepositoryAuditRequest.php
index ea86594d9..e05820c82 100644
--- a/src/applications/repository/storage/PhabricatorRepositoryAuditRequest.php
+++ b/src/applications/repository/storage/PhabricatorRepositoryAuditRequest.php
@@ -1,97 +1,106 @@
<?php
final class PhabricatorRepositoryAuditRequest
extends PhabricatorRepositoryDAO
implements PhabricatorPolicyInterface {
protected $auditorPHID;
protected $commitPHID;
protected $auditReasons = array();
protected $auditStatus;
private $commit = self::ATTACHABLE;
protected function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_SERIALIZATION => array(
'auditReasons' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'auditStatus' => 'text64',
),
self::CONFIG_KEY_SCHEMA => array(
'commitPHID' => array(
'columns' => array('commitPHID'),
),
'auditorPHID' => array(
'columns' => array('auditorPHID', 'auditStatus'),
),
'key_unique' => array(
'columns' => array('commitPHID', 'auditorPHID'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function isUser() {
$user_type = PhabricatorPeopleUserPHIDType::TYPECONST;
return (phid_get_type($this->getAuditorPHID()) == $user_type);
}
public function attachCommit(PhabricatorRepositoryCommit $commit) {
$this->commit = $commit;
return $this;
}
public function getCommit() {
return $this->assertAttached($this->commit);
}
public function isActiveAudit() {
switch ($this->getAuditStatus()) {
case PhabricatorAuditStatusConstants::NONE:
case PhabricatorAuditStatusConstants::AUDIT_NOT_REQUIRED:
case PhabricatorAuditStatusConstants::RESIGNED:
case PhabricatorAuditStatusConstants::CLOSED:
case PhabricatorAuditStatusConstants::CC:
return false;
}
return true;
}
public function isInteresting() {
switch ($this->getAuditStatus()) {
case PhabricatorAuditStatusConstants::NONE:
case PhabricatorAuditStatusConstants::AUDIT_NOT_REQUIRED:
return false;
}
return true;
}
+ public function isResigned() {
+ switch ($this->getAuditStatus()) {
+ case PhabricatorAuditStatusConstants::RESIGNED:
+ return true;
+ }
+
+ return false;
+ }
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
return $this->getCommit()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getCommit()->hasAutomaticCapability($capability, $viewer);
}
public function describeAutomaticCapability($capability) {
return pht(
'This audit is attached to a commit, and inherits its policies.');
}
}
diff --git a/src/applications/repository/storage/PhabricatorRepositoryCommit.php b/src/applications/repository/storage/PhabricatorRepositoryCommit.php
index 31a06dcbd..f6e6e22c3 100644
--- a/src/applications/repository/storage/PhabricatorRepositoryCommit.php
+++ b/src/applications/repository/storage/PhabricatorRepositoryCommit.php
@@ -1,761 +1,766 @@
<?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 $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 $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?',
'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 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);
}
public function renderAuthorLink($handles) {
$author_phid = $this->getAuthorPHID();
if ($author_phid && isset($handles[$author_phid])) {
return $handles[$author_phid]->renderLink();
}
return $this->renderAuthorShortName($handles);
}
public function renderAuthorShortName($handles) {
$author_phid = $this->getAuthorPHID();
if ($author_phid && isset($handles[$author_phid])) {
return $handles[$author_phid]->getName();
}
$data = $this->getCommitData();
$name = $data->getAuthorName();
$parsed = new PhutilEmailAddress($name);
return nonempty($parsed->getDisplayName(), $parsed->getAddress());
}
/* -( 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 getHarbormasterPublishablePHID() {
- return $this->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.
+ // 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() {
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/PhabricatorRepositoryPushEvent.php b/src/applications/repository/storage/PhabricatorRepositoryPushEvent.php
index 2bc751ffc..e44f99df4 100644
--- a/src/applications/repository/storage/PhabricatorRepositoryPushEvent.php
+++ b/src/applications/repository/storage/PhabricatorRepositoryPushEvent.php
@@ -1,92 +1,103 @@
<?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'),
+ ),
),
) + 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/storage/PhabricatorRepositoryPushLog.php b/src/applications/repository/storage/PhabricatorRepositoryPushLog.php
index 4e099209c..c2d3456da 100644
--- a/src/applications/repository/storage/PhabricatorRepositoryPushLog.php
+++ b/src/applications/repository/storage/PhabricatorRepositoryPushLog.php
@@ -1,206 +1,231 @@
<?php
/**
* Records a push to a hosted repository. This allows us to store metadata
* about who pushed commits, when, and from where. We can also record the
* history of branches and tags, which is not normally persisted outside of
* the reflog.
*
* This log is written by commit hooks installed into hosted repositories.
* See @{class:DiffusionCommitHookEngine}.
*/
final class PhabricatorRepositoryPushLog
extends PhabricatorRepositoryDAO
implements PhabricatorPolicyInterface {
const REFTYPE_BRANCH = 'branch';
const REFTYPE_TAG = 'tag';
const REFTYPE_BOOKMARK = 'bookmark';
const REFTYPE_COMMIT = 'commit';
const CHANGEFLAG_ADD = 1;
const CHANGEFLAG_DELETE = 2;
const CHANGEFLAG_APPEND = 4;
const CHANGEFLAG_REWRITE = 8;
const CHANGEFLAG_DANGEROUS = 16;
const CHANGEFLAG_ENORMOUS = 32;
const REJECT_ACCEPT = 0;
const REJECT_DANGEROUS = 1;
const REJECT_HERALD = 2;
const REJECT_EXTERNAL = 3;
const REJECT_BROKEN = 4;
const REJECT_ENORMOUS = 5;
protected $repositoryPHID;
protected $epoch;
protected $pusherPHID;
protected $pushEventPHID;
protected $devicePHID;
protected $refType;
protected $refNameHash;
protected $refNameRaw;
protected $refNameEncoding;
protected $refOld;
protected $refNew;
protected $mergeBase;
protected $changeFlags;
private $dangerousChangeDescription = self::ATTACHABLE;
private $pushEvent = self::ATTACHABLE;
private $repository = self::ATTACHABLE;
public static function initializeNewLog(PhabricatorUser $viewer) {
return id(new PhabricatorRepositoryPushLog())
->setPusherPHID($viewer->getPHID());
}
+ public static function getFlagDisplayNames() {
+ return array(
+ self::CHANGEFLAG_ADD => pht('Create'),
+ self::CHANGEFLAG_DELETE => pht('Delete'),
+ self::CHANGEFLAG_APPEND => pht('Append'),
+ self::CHANGEFLAG_REWRITE => pht('Rewrite'),
+ self::CHANGEFLAG_DANGEROUS => pht('Dangerous'),
+ self::CHANGEFLAG_ENORMOUS => pht('Enormous'),
+ );
+ }
+
+ public static function getRejectCodeDisplayNames() {
+ return array(
+ self::REJECT_ACCEPT => pht('Accepted'),
+ self::REJECT_DANGEROUS => pht('Rejected: Dangerous'),
+ self::REJECT_HERALD => pht('Rejected: Herald'),
+ self::REJECT_EXTERNAL => pht('Rejected: External Hook'),
+ self::REJECT_BROKEN => pht('Rejected: Broken'),
+ self::REJECT_ENORMOUS => pht('Rejected: Enormous'),
+ );
+ }
+
public static function getHeraldChangeFlagConditionOptions() {
return array(
self::CHANGEFLAG_ADD =>
pht('change creates ref'),
self::CHANGEFLAG_DELETE =>
pht('change deletes ref'),
self::CHANGEFLAG_REWRITE =>
pht('change rewrites ref'),
self::CHANGEFLAG_DANGEROUS =>
pht('dangerous change'),
);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_BINARY => array(
'refNameRaw' => true,
),
self::CONFIG_COLUMN_SCHEMA => array(
'refType' => 'text12',
'refNameHash' => 'bytes12?',
'refNameRaw' => 'bytes?',
'refNameEncoding' => 'text16?',
'refOld' => 'text40?',
'refNew' => 'text40',
'mergeBase' => 'text40?',
'changeFlags' => 'uint32',
'devicePHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_repository' => array(
'columns' => array('repositoryPHID'),
),
'key_ref' => array(
'columns' => array('repositoryPHID', 'refNew'),
),
'key_name' => array(
'columns' => array('repositoryPHID', 'refNameHash'),
),
'key_event' => array(
'columns' => array('pushEventPHID'),
),
'key_pusher' => array(
'columns' => array('pusherPHID'),
),
+ 'key_epoch' => array(
+ 'columns' => array('epoch'),
+ ),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorRepositoryPushLogPHIDType::TYPECONST);
}
public function attachPushEvent(PhabricatorRepositoryPushEvent $push_event) {
$this->pushEvent = $push_event;
return $this;
}
public function getPushEvent() {
return $this->assertAttached($this->pushEvent);
}
public function getRefName() {
return $this->getUTF8StringFromStorage(
$this->getRefNameRaw(),
$this->getRefNameEncoding());
}
public function setRefName($ref_raw) {
$this->setRefNameRaw($ref_raw);
$this->setRefNameHash(PhabricatorHash::digestForIndex($ref_raw));
$this->setRefNameEncoding($this->detectEncodingForStorage($ref_raw));
return $this;
}
public function getRefOldShort() {
if ($this->getRepository()->isSVN()) {
return $this->getRefOld();
}
return substr($this->getRefOld(), 0, 12);
}
public function getRefNewShort() {
if ($this->getRepository()->isSVN()) {
return $this->getRefNew();
}
return substr($this->getRefNew(), 0, 12);
}
public function hasChangeFlags($mask) {
return ($this->changeFlags & $mask);
}
public function attachDangerousChangeDescription($description) {
$this->dangerousChangeDescription = $description;
return $this;
}
public function getDangerousChangeDescription() {
return $this->assertAttached($this->dangerousChangeDescription);
}
public function attachRepository(PhabricatorRepository $repository) {
// NOTE: Some gymnastics around this because of object construction order
// in the hook engine. Particularly, web build the logs before we build
// their push event.
$this->repository = $repository;
return $this;
}
public function getRepository() {
if ($this->repository == self::ATTACHABLE) {
return $this->getPushEvent()->getRepository();
}
return $this->assertAttached($this->repository);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
// NOTE: We're passing through the repository rather than the push event
// mostly because we need to do policy checks in Herald before we create
// the event. The two approaches are equivalent in practice.
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 logs are visible to users who can see the ".
"repository.");
}
}
diff --git a/src/applications/repository/storage/PhabricatorRepositoryWorkingCopyVersion.php b/src/applications/repository/storage/PhabricatorRepositoryWorkingCopyVersion.php
index 750cc91c4..da5d54b57 100644
--- a/src/applications/repository/storage/PhabricatorRepositoryWorkingCopyVersion.php
+++ b/src/applications/repository/storage/PhabricatorRepositoryWorkingCopyVersion.php
@@ -1,186 +1,218 @@
<?php
final class PhabricatorRepositoryWorkingCopyVersion
extends PhabricatorRepositoryDAO {
protected $repositoryPHID;
protected $devicePHID;
protected $repositoryVersion;
protected $isWriting;
protected $lockOwner;
protected $writeProperties;
protected function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_COLUMN_SCHEMA => array(
'repositoryVersion' => 'uint32',
'isWriting' => 'bool',
'writeProperties' => 'text?',
'lockOwner' => 'text255?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_workingcopy' => array(
'columns' => array('repositoryPHID', 'devicePHID'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
+ public function getWriteProperty($key, $default = null) {
+ // The "writeProperties" don't currently get automatically serialized or
+ // deserialized. Perhaps they should.
+ try {
+ $properties = phutil_json_decode($this->writeProperties);
+ return idx($properties, $key, $default);
+ } catch (Exception $ex) {
+ return null;
+ }
+ }
+
public static function loadVersions($repository_phid) {
$version = new self();
$conn_w = $version->establishConnection('w');
$table = $version->getTableName();
// This is a normal read, but force it to come from the master.
$rows = queryfx_all(
$conn_w,
'SELECT * FROM %T WHERE repositoryPHID = %s',
$table,
$repository_phid);
return $version->loadAllFromArray($rows);
}
+ public static function loadWriter($repository_phid) {
+ $version = new self();
+ $conn_w = $version->establishConnection('w');
+ $table = $version->getTableName();
+
+ // We're forcing this read to go to the master.
+ $row = queryfx_one(
+ $conn_w,
+ 'SELECT * FROM %T WHERE repositoryPHID = %s AND isWriting = 1
+ LIMIT 1',
+ $table,
+ $repository_phid);
+
+ if (!$row) {
+ return null;
+ }
+
+ return $version->loadFromArray($row);
+ }
+
+
public static function getReadLock($repository_phid, $device_phid) {
$repository_hash = PhabricatorHash::digestForIndex($repository_phid);
$device_hash = PhabricatorHash::digestForIndex($device_phid);
$lock_key = "repo.read({$repository_hash}, {$device_hash})";
return PhabricatorGlobalLock::newLock($lock_key);
}
public static function getWriteLock($repository_phid) {
$repository_hash = PhabricatorHash::digestForIndex($repository_phid);
$lock_key = "repo.write({$repository_hash})";
return PhabricatorGlobalLock::newLock($lock_key);
}
/**
* Before a write, set the "isWriting" flag.
*
* This allows us to detect when we lose a node partway through a write and
* may have committed and acknowledged a write on a node that lost the lock
* partway through the write and is no longer reachable.
*
* In particular, if a node loses its connection to the database the global
* lock is released by default. This is a durable lock which stays locked
* by default.
*/
public static function willWrite(
AphrontDatabaseConnection $locked_connection,
$repository_phid,
$device_phid,
array $write_properties,
$lock_owner) {
$version = new self();
$table = $version->getTableName();
queryfx(
$locked_connection,
'INSERT INTO %T
(repositoryPHID, devicePHID, repositoryVersion, isWriting,
writeProperties, lockOwner)
VALUES
(%s, %s, %d, %d, %s, %s)
ON DUPLICATE KEY UPDATE
isWriting = VALUES(isWriting),
writeProperties = VALUES(writeProperties),
lockOwner = VALUES(lockOwner)',
$table,
$repository_phid,
$device_phid,
0,
1,
phutil_json_encode($write_properties),
$lock_owner);
}
/**
* After a write, update the version and release the "isWriting" lock.
*/
public static function didWrite(
$repository_phid,
$device_phid,
$old_version,
$new_version,
$lock_owner) {
$version = new self();
$conn_w = $version->establishConnection('w');
$table = $version->getTableName();
queryfx(
$conn_w,
'UPDATE %T SET
repositoryVersion = %d,
isWriting = 0,
lockOwner = NULL
WHERE
repositoryPHID = %s AND
devicePHID = %s AND
repositoryVersion = %d AND
isWriting = 1 AND
lockOwner = %s',
$table,
$new_version,
$repository_phid,
$device_phid,
$old_version,
$lock_owner);
}
/**
* After a fetch, set the local version to the fetched version.
*/
public static function updateVersion(
$repository_phid,
$device_phid,
$new_version) {
$version = new self();
$conn_w = $version->establishConnection('w');
$table = $version->getTableName();
queryfx(
$conn_w,
'INSERT INTO %T
(repositoryPHID, devicePHID, repositoryVersion, isWriting)
VALUES
(%s, %s, %d, %d)
ON DUPLICATE KEY UPDATE
repositoryVersion = VALUES(repositoryVersion)',
$table,
$repository_phid,
$device_phid,
$new_version,
0);
}
/**
* Explicitly demote a device.
*/
public static function demoteDevice(
$repository_phid,
$device_phid) {
$version = new self();
$conn_w = $version->establishConnection('w');
$table = $version->getTableName();
queryfx(
$conn_w,
'DELETE FROM %T WHERE repositoryPHID = %s AND devicePHID = %s',
$table,
$repository_phid,
$device_phid);
}
}
diff --git a/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php b/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php
index 9fb666792..818d1e878 100644
--- a/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php
+++ b/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php
@@ -1,168 +1,146 @@
<?php
final class PhabricatorRepositoryCommitHeraldWorker
extends PhabricatorRepositoryCommitParserWorker {
protected function getImportStepFlag() {
return PhabricatorRepositoryCommit::IMPORTED_HERALD;
}
public function getRequiredLeaseTime() {
// Herald rules may take a long time to process.
return phutil_units('4 hours in seconds');
}
protected function parseCommit(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit) {
+ $viewer = PhabricatorUser::getOmnipotentUser();
if ($this->shouldSkipImportStep()) {
// This worker has no followup tasks, so we can just bail out
// right away without queueing anything.
return;
}
// Reload the commit to pull commit data and audit requests.
$commit = id(new DiffusionCommitQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withIDs(array($commit->getID()))
->needCommitData(true)
->needAuditRequests(true)
->executeOne();
$data = $commit->getCommitData();
if (!$data) {
throw new PhabricatorWorkerPermanentFailureException(
pht(
'Unable to load commit data. The data for this task is invalid '.
'or no longer exists.'));
}
$commit->attachRepository($repository);
$content_source = $this->newContentSource();
$committer_phid = $data->getCommitDetail('committerPHID');
$author_phid = $data->getCommitDetail('authorPHID');
$acting_as_phid = nonempty(
$committer_phid,
$author_phid,
id(new PhabricatorDiffusionApplication())->getPHID());
$editor = id(new PhabricatorAuditEditor())
- ->setActor(PhabricatorUser::getOmnipotentUser())
+ ->setActor($viewer)
->setActingAsPHID($acting_as_phid)
->setContinueOnMissingFields(true)
->setContinueOnNoEffect(true)
->setContentSource($content_source);
$xactions = array();
$xactions[] = id(new PhabricatorAuditTransaction())
->setTransactionType(PhabricatorAuditTransaction::TYPE_COMMIT)
->setDateCreated($commit->getEpoch())
->setNewValue(array(
'description' => $data->getCommitMessage(),
'summary' => $data->getSummary(),
'authorName' => $data->getAuthorName(),
'authorPHID' => $commit->getAuthorPHID(),
'committerName' => $data->getCommitDetail('committer'),
'committerPHID' => $data->getCommitDetail('committerPHID'),
));
- $reverts_refs = id(new DifferentialCustomFieldRevertsParser())
- ->parseCorpus($data->getCommitMessage());
- $reverts = array_mergev(ipull($reverts_refs, 'monograms'));
-
- if ($reverts) {
- $reverted_commits = id(new DiffusionCommitQuery())
- ->setViewer(PhabricatorUser::getOmnipotentUser())
- ->withRepository($repository)
- ->withIdentifiers($reverts)
- ->execute();
- $reverted_commit_phids = mpull($reverted_commits, 'getPHID', 'getPHID');
-
- // NOTE: Skip any write attempts if a user cleverly implies a commit
- // reverts itself.
- unset($reverted_commit_phids[$commit->getPHID()]);
-
- $reverts_edge = DiffusionCommitRevertsCommitEdgeType::EDGECONST;
- $xactions[] = id(new PhabricatorAuditTransaction())
- ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
- ->setMetadataValue('edge:type', $reverts_edge)
- ->setNewValue(array('+' => array_fuse($reverted_commit_phids)));
- }
-
try {
$raw_patch = $this->loadRawPatchText($repository, $commit);
} catch (Exception $ex) {
$raw_patch = pht('Unable to generate patch: %s', $ex->getMessage());
}
$editor->setRawPatch($raw_patch);
return $editor->applyTransactions($commit, $xactions);
}
private function loadRawPatchText(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit) {
$viewer = PhabricatorUser::getOmnipotentUser();
$identifier = $commit->getCommitIdentifier();
$drequest = DiffusionRequest::newFromDictionary(
array(
'user' => $viewer,
'repository' => $repository,
));
$time_key = 'metamta.diffusion.time-limit';
$byte_key = 'metamta.diffusion.byte-limit';
$time_limit = PhabricatorEnv::getEnvConfig($time_key);
$byte_limit = PhabricatorEnv::getEnvConfig($byte_key);
$diff_info = DiffusionQuery::callConduitWithDiffusionRequest(
$viewer,
$drequest,
'diffusion.rawdiffquery',
array(
'commit' => $identifier,
'linesOfContext' => 3,
'timeout' => $time_limit,
'byteLimit' => $byte_limit,
));
if ($diff_info['tooSlow']) {
throw new Exception(
pht(
'Patch generation took longer than configured limit ("%s") of '.
'%s second(s).',
$time_key,
new PhutilNumber($time_limit)));
}
if ($diff_info['tooHuge']) {
$pretty_limit = phutil_format_bytes($byte_limit);
throw new Exception(
pht(
'Patch size exceeds configured byte size limit ("%s") of %s.',
$byte_key,
$pretty_limit));
}
$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'));
}
return $file->loadFileData();
}
}
diff --git a/src/applications/repository/worker/PhabricatorRepositoryPushMailWorker.php b/src/applications/repository/worker/PhabricatorRepositoryPushMailWorker.php
index 17226a137..554c2cf77 100644
--- a/src/applications/repository/worker/PhabricatorRepositoryPushMailWorker.php
+++ b/src/applications/repository/worker/PhabricatorRepositoryPushMailWorker.php
@@ -1,226 +1,226 @@
<?php
final class PhabricatorRepositoryPushMailWorker
extends PhabricatorWorker {
protected function doWork() {
$viewer = PhabricatorUser::getOmnipotentUser();
$task_data = $this->getTaskData();
$email_phids = idx($task_data, 'emailPHIDs');
if (!$email_phids) {
// If we don't have any email targets, don't send any email.
return;
}
$event_phid = idx($task_data, 'eventPHID');
$event = id(new PhabricatorRepositoryPushEventQuery())
->setViewer($viewer)
->withPHIDs(array($event_phid))
->needLogs(true)
->executeOne();
$repository = $event->getRepository();
if (!$repository->shouldPublish()) {
// If the repository is still importing, don't send email.
return;
}
$targets = id(new PhabricatorRepositoryPushReplyHandler())
->setMailReceiver($repository)
->getMailTargets($email_phids, array());
$messages = array();
foreach ($targets as $target) {
$messages[] = $this->sendMail($target, $repository, $event);
}
foreach ($messages as $message) {
$message->save();
}
}
private function sendMail(
PhabricatorMailTarget $target,
PhabricatorRepository $repository,
PhabricatorRepositoryPushEvent $event) {
$task_data = $this->getTaskData();
$viewer = $target->getViewer();
$locale = PhabricatorEnv::beginScopedLocale($viewer->getTranslation());
$logs = $event->getLogs();
list($ref_lines, $ref_list) = $this->renderRefs($logs);
list($commit_lines, $subject_line) = $this->renderCommits(
$repository,
$logs,
idx($task_data, 'info', array()));
$ref_count = count($ref_lines);
$commit_count = count($commit_lines);
$handles = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs(array($event->getPusherPHID()))
->execute();
$pusher_name = $handles[$event->getPusherPHID()]->getName();
$repo_name = $repository->getMonogram();
if ($commit_count) {
$overview = pht(
'%s pushed %d commit(s) to %s.',
$pusher_name,
$commit_count,
$repo_name);
} else {
$overview = pht(
'%s pushed to %s.',
$pusher_name,
$repo_name);
}
$details_uri = PhabricatorEnv::getProductionURI(
'/diffusion/pushlog/view/'.$event->getID().'/');
$body = new PhabricatorMetaMTAMailBody();
$body->addRawSection($overview);
$body->addLinkSection(pht('DETAILS'), $details_uri);
if ($commit_lines) {
$body->addTextSection(pht('COMMITS'), implode("\n", $commit_lines));
}
if ($ref_lines) {
$body->addTextSection(pht('REFERENCES'), implode("\n", $ref_lines));
}
$prefix = PhabricatorEnv::getEnvConfig('metamta.diffusion.subject-prefix');
$parts = array();
if ($commit_count) {
$parts[] = pht('%s commit(s)', $commit_count);
}
if ($ref_count) {
$parts[] = implode(', ', $ref_list);
}
$parts = implode(', ', $parts);
if ($subject_line) {
$subject = pht('(%s) %s', $parts, $subject_line);
} else {
$subject = pht('(%s)', $parts);
}
$mail = id(new PhabricatorMetaMTAMail())
->setRelatedPHID($event->getPHID())
->setSubjectPrefix($prefix)
->setVarySubjectPrefix(pht('[Push]'))
->setSubject($subject)
->setFrom($event->getPusherPHID())
->setBody($body->render())
+ ->setHTMLBody($body->renderHTML())
->setThreadID($event->getPHID(), $is_new = true)
- ->addHeader('Thread-Topic', $subject)
->setIsBulk(true);
return $target->willSendMail($mail);
}
private function renderRefs(array $logs) {
$ref_lines = array();
$ref_list = array();
foreach ($logs as $log) {
$type_name = null;
$type_prefix = null;
switch ($log->getRefType()) {
case PhabricatorRepositoryPushLog::REFTYPE_BRANCH:
$type_name = pht('branch');
break;
case PhabricatorRepositoryPushLog::REFTYPE_TAG:
$type_name = pht('tag');
$type_prefix = pht('tag:');
break;
case PhabricatorRepositoryPushLog::REFTYPE_BOOKMARK:
$type_name = pht('bookmark');
$type_prefix = pht('bookmark:');
break;
case PhabricatorRepositoryPushLog::REFTYPE_COMMIT:
default:
break;
}
if ($type_name === null) {
continue;
}
$flags = $log->getChangeFlags();
if ($flags & PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS) {
$action = '!';
} else if ($flags & PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE) {
$action = '-';
} else if ($flags & PhabricatorRepositoryPushLog::CHANGEFLAG_REWRITE) {
$action = '~';
} else if ($flags & PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND) {
$action = ' ';
} else if ($flags & PhabricatorRepositoryPushLog::CHANGEFLAG_ADD) {
$action = '+';
} else {
$action = '?';
}
$old = nonempty($log->getRefOldShort(), pht('<null>'));
$new = nonempty($log->getRefNewShort(), pht('<null>'));
$name = $log->getRefName();
$ref_lines[] = "{$action} {$type_name} {$name} {$old} > {$new}";
$ref_list[] = $type_prefix.$name;
}
return array(
$ref_lines,
array_unique($ref_list),
);
}
private function renderCommits(
PhabricatorRepository $repository,
array $logs,
array $info) {
$commit_lines = array();
$subject_line = null;
foreach ($logs as $log) {
if ($log->getRefType() != PhabricatorRepositoryPushLog::REFTYPE_COMMIT) {
continue;
}
$commit_info = idx($info, $log->getRefNew(), array());
$name = $repository->formatCommitName($log->getRefNew());
$branches = null;
if (idx($commit_info, 'branches')) {
$branches = ' ('.implode(', ', $commit_info['branches']).')';
}
$summary = null;
if (strlen(idx($commit_info, 'summary'))) {
$summary = ' '.$commit_info['summary'];
}
$commit_lines[] = "{$name}{$branches}{$summary}";
if ($subject_line === null) {
$subject_line = "{$name}{$summary}";
}
}
return array($commit_lines, $subject_line);
}
}
diff --git a/src/applications/search/controller/PhabricatorApplicationSearchController.php b/src/applications/search/controller/PhabricatorApplicationSearchController.php
index d3b05d1e3..cf4f95c16 100644
--- a/src/applications/search/controller/PhabricatorApplicationSearchController.php
+++ b/src/applications/search/controller/PhabricatorApplicationSearchController.php
@@ -1,917 +1,975 @@
<?php
final class PhabricatorApplicationSearchController
extends PhabricatorSearchBaseController {
private $searchEngine;
private $navigation;
private $queryKey;
private $preface;
+ private $activeQuery;
public function setPreface($preface) {
$this->preface = $preface;
return $this;
}
public function getPreface() {
return $this->preface;
}
public function setQueryKey($query_key) {
$this->queryKey = $query_key;
return $this;
}
protected function getQueryKey() {
return $this->queryKey;
}
public function setNavigation(AphrontSideNavFilterView $navigation) {
$this->navigation = $navigation;
return $this;
}
protected function getNavigation() {
return $this->navigation;
}
public function setSearchEngine(
PhabricatorApplicationSearchEngine $search_engine) {
$this->searchEngine = $search_engine;
return $this;
}
protected function getSearchEngine() {
return $this->searchEngine;
}
+ protected function getActiveQuery() {
+ if (!$this->activeQuery) {
+ throw new Exception(pht('There is no active query yet.'));
+ }
+
+ return $this->activeQuery;
+ }
+
protected function validateDelegatingController() {
$parent = $this->getDelegatingController();
if (!$parent) {
throw new Exception(
pht('You must delegate to this controller, not invoke it directly.'));
}
$engine = $this->getSearchEngine();
if (!$engine) {
throw new PhutilInvalidStateException('setEngine');
}
$engine->setViewer($this->getRequest()->getUser());
$parent = $this->getDelegatingController();
}
public function processRequest() {
$this->validateDelegatingController();
$query_action = $this->getRequest()->getURIData('queryAction');
if ($query_action == 'export') {
return $this->processExportRequest();
}
$key = $this->getQueryKey();
if ($key == 'edit') {
return $this->processEditRequest();
} else {
return $this->processSearchRequest();
}
}
private function processSearchRequest() {
$parent = $this->getDelegatingController();
$request = $this->getRequest();
$user = $request->getUser();
$engine = $this->getSearchEngine();
$nav = $this->getNavigation();
if (!$nav) {
$nav = $this->buildNavigation();
}
if ($request->isFormPost()) {
$saved_query = $engine->buildSavedQueryFromRequest($request);
$engine->saveQuery($saved_query);
return id(new AphrontRedirectResponse())->setURI(
$engine->getQueryResultsPageURI($saved_query->getQueryKey()).'#R');
}
$named_query = null;
$run_query = true;
$query_key = $this->queryKey;
if ($this->queryKey == 'advanced') {
$run_query = false;
$query_key = $request->getStr('query');
} else if (!strlen($this->queryKey)) {
$found_query_data = false;
if ($request->isHTTPGet() || $request->isQuicksand()) {
// If this is a GET request and it has some query data, don't
// do anything unless it's only before= or after=. We'll build and
// execute a query from it below. This allows external tools to build
// URIs like "/query/?users=a,b".
$pt_data = $request->getPassthroughRequestData();
$exempt = array(
'before' => true,
'after' => true,
'nux' => true,
'overheated' => true,
);
foreach ($pt_data as $pt_key => $pt_value) {
if (isset($exempt[$pt_key])) {
continue;
}
$found_query_data = true;
break;
}
}
if (!$found_query_data) {
// Otherwise, there's no query data so just run the user's default
// query for this application.
$query_key = $engine->getDefaultQueryKey();
}
}
if ($engine->isBuiltinQuery($query_key)) {
$saved_query = $engine->buildSavedQueryFromBuiltin($query_key);
$named_query = idx($engine->loadEnabledNamedQueries(), $query_key);
} else if ($query_key) {
$saved_query = id(new PhabricatorSavedQueryQuery())
->setViewer($user)
->withQueryKeys(array($query_key))
->executeOne();
if (!$saved_query) {
return new Aphront404Response();
}
$named_query = idx($engine->loadEnabledNamedQueries(), $query_key);
} else {
$saved_query = $engine->buildSavedQueryFromRequest($request);
// Save the query to generate a query key, so "Save Custom Query..." and
- // other features like Maniphest's "Export..." work correctly.
+ // other features like "Bulk Edit" and "Export Data" work correctly.
$engine->saveQuery($saved_query);
}
+ $this->activeQuery = $saved_query;
+
$nav->selectFilter(
'query/'.$saved_query->getQueryKey(),
'query/advanced');
$form = id(new AphrontFormView())
->setUser($user)
->setAction($request->getPath());
$engine->buildSearchForm($form, $saved_query);
$errors = $engine->getErrors();
if ($errors) {
$run_query = false;
}
$submit = id(new AphrontFormSubmitControl())
->setValue(pht('Search'));
if ($run_query && !$named_query && $user->isLoggedIn()) {
$save_button = id(new PHUIButtonView())
->setTag('a')
->setHref('/search/edit/key/'.$saved_query->getQueryKey().'/')
->setText(pht('Save Query'))
->setIcon('fa-floppy-o');
$submit->addButton($save_button);
}
// TODO: A "Create Dashboard Panel" action goes here somewhere once
// we sort out T5307.
$form->appendChild($submit);
$body = array();
if ($this->getPreface()) {
$body[] = $this->getPreface();
}
if ($named_query) {
$title = $named_query->getQueryName();
} else {
$title = pht('Advanced Search');
}
$header = id(new PHUIHeaderView())
->setHeader($title)
->setProfileHeader(true);
$box = id(new PHUIObjectBoxView())
->setHeader($header)
->addClass('application-search-results');
if ($run_query || $named_query) {
$box->setShowHide(
pht('Edit Query'),
pht('Hide Query'),
$form,
$this->getApplicationURI('query/advanced/?query='.$query_key),
(!$named_query ? true : false));
} else {
$box->setForm($form);
}
$body[] = $box;
$more_crumbs = null;
if ($run_query) {
$exec_errors = array();
$box->setAnchor(
id(new PhabricatorAnchorView())
->setAnchorName('R'));
try {
$engine->setRequest($request);
$query = $engine->buildQueryFromSavedQuery($saved_query);
$pager = $engine->newPagerForSavedQuery($saved_query);
$pager->readFromRequest($request);
$objects = $engine->executeQuery($query, $pager);
$force_nux = $request->getBool('nux');
if (!$objects || $force_nux) {
$nux_view = $this->renderNewUserView($engine, $force_nux);
} else {
$nux_view = null;
}
$is_overflowing =
$pager->willShowPagingControls() &&
$engine->getResultBucket($saved_query);
$force_overheated = $request->getBool('overheated');
$is_overheated = $query->getIsOverheated() || $force_overheated;
if ($nux_view) {
$box->appendChild($nux_view);
} else {
$list = $engine->renderResults($objects, $saved_query);
if (!($list instanceof PhabricatorApplicationSearchResultView)) {
throw new Exception(
pht(
'SearchEngines must render a "%s" object, but this engine '.
'(of class "%s") rendered something else.',
'PhabricatorApplicationSearchResultView',
get_class($engine)));
}
if ($list->getObjectList()) {
$box->setObjectList($list->getObjectList());
}
if ($list->getTable()) {
$box->setTable($list->getTable());
}
if ($list->getInfoView()) {
$box->setInfoView($list->getInfoView());
}
if ($is_overflowing) {
$box->appendChild($this->newOverflowingView());
}
if ($list->getContent()) {
$box->appendChild($list->getContent());
}
if ($is_overheated) {
$box->appendChild($this->newOverheatedView($objects));
}
$result_header = $list->getHeader();
if ($result_header) {
$box->setHeader($result_header);
$header = $result_header;
}
$actions = $list->getActions();
if ($actions) {
foreach ($actions as $action) {
$header->addActionLink($action);
}
}
$use_actions = $engine->newUseResultsActions($saved_query);
// TODO: Eventually, modularize all this stuff.
$builtin_use_actions = $this->newBuiltinUseActions();
if ($builtin_use_actions) {
foreach ($builtin_use_actions as $builtin_use_action) {
$use_actions[] = $builtin_use_action;
}
}
if ($use_actions) {
$use_dropdown = $this->newUseResultsDropdown(
$saved_query,
$use_actions);
$header->addActionLink($use_dropdown);
}
$more_crumbs = $list->getCrumbs();
if ($pager->willShowPagingControls()) {
$pager_box = id(new PHUIBoxView())
->setColor(PHUIBoxView::GREY)
->addClass('application-search-pager')
->appendChild($pager);
$body[] = $pager_box;
}
}
} catch (PhabricatorTypeaheadInvalidTokenException $ex) {
$exec_errors[] = pht(
'This query specifies an invalid parameter. Review the '.
'query parameters and correct errors.');
} catch (PhutilSearchQueryCompilerSyntaxException $ex) {
$exec_errors[] = $ex->getMessage();
} catch (PhabricatorSearchConstraintException $ex) {
$exec_errors[] = $ex->getMessage();
}
// The engine may have encountered additional errors during rendering;
// merge them in and show everything.
foreach ($engine->getErrors() as $error) {
$exec_errors[] = $error;
}
$errors = $exec_errors;
}
if ($errors) {
$box->setFormErrors($errors, pht('Query Errors'));
}
$crumbs = $parent
->buildApplicationCrumbs()
->setBorder(true);
if ($more_crumbs) {
$query_uri = $engine->getQueryResultsPageURI($saved_query->getQueryKey());
$crumbs->addTextCrumb($title, $query_uri);
foreach ($more_crumbs as $crumb) {
$crumbs->addCrumb($crumb);
}
} else {
$crumbs->addTextCrumb($title);
}
require_celerity_resource('application-search-view-css');
return $this->newPage()
->setApplicationMenu($this->buildApplicationMenu())
->setTitle(pht('Query: %s', $title))
->setCrumbs($crumbs)
->setNavigation($nav)
->addClass('application-search-view')
->appendChild($body);
}
private function processExportRequest() {
$viewer = $this->getViewer();
$engine = $this->getSearchEngine();
$request = $this->getRequest();
if (!$this->canExport()) {
return new Aphront404Response();
}
$query_key = $this->getQueryKey();
if ($engine->isBuiltinQuery($query_key)) {
$saved_query = $engine->buildSavedQueryFromBuiltin($query_key);
} else if ($query_key) {
$saved_query = id(new PhabricatorSavedQueryQuery())
->setViewer($viewer)
->withQueryKeys(array($query_key))
->executeOne();
} else {
$saved_query = null;
}
if (!$saved_query) {
return new Aphront404Response();
}
$cancel_uri = $engine->getQueryResultsPageURI($query_key);
$named_query = idx($engine->loadEnabledNamedQueries(), $query_key);
if ($named_query) {
$filename = $named_query->getQueryName();
+ $sheet_title = $named_query->getQueryName();
} else {
$filename = $engine->getResultTypeDescription();
+ $sheet_title = $engine->getResultTypeDescription();
}
$filename = phutil_utf8_strtolower($filename);
$filename = PhabricatorFile::normalizeFileName($filename);
- $formats = PhabricatorExportFormat::getAllEnabledExportFormats();
- $format_options = mpull($formats, 'getExportFormatName');
+ $all_formats = PhabricatorExportFormat::getAllExportFormats();
+
+ $available_options = array();
+ $unavailable_options = array();
+ $formats = array();
+ $unavailable_formats = array();
+ foreach ($all_formats as $key => $format) {
+ if ($format->isExportFormatEnabled()) {
+ $available_options[$key] = $format->getExportFormatName();
+ $formats[$key] = $format;
+ } else {
+ $unavailable_options[$key] = pht(
+ '%s (Not Available)',
+ $format->getExportFormatName());
+ $unavailable_formats[$key] = $format;
+ }
+ }
+ $format_options = $available_options + $unavailable_options;
+
+ // Try to default to the format the user used last time. If you just
+ // exported to Excel, you probably want to export to Excel again.
+ $format_key = $this->readExportFormatPreference();
+ if (!isset($formats[$format_key])) {
+ $format_key = head_key($format_options);
+ }
+
+ // Check if this is a large result set or not. If we're exporting a
+ // large amount of data, we'll build the actual export file in the daemons.
+
+ $threshold = 1000;
+ $query = $engine->buildQueryFromSavedQuery($saved_query);
+ $pager = $engine->newPagerForSavedQuery($saved_query);
+ $pager->setPageSize($threshold + 1);
+ $objects = $engine->executeQuery($query, $pager);
+ $object_count = count($objects);
+ $is_large_export = ($object_count > $threshold);
$errors = array();
$e_format = null;
if ($request->isFormPost()) {
$format_key = $request->getStr('format');
+
+ if (isset($unavailable_formats[$format_key])) {
+ $unavailable = $unavailable_formats[$format_key];
+ $instructions = $unavailable->getInstallInstructions();
+
+ $markup = id(new PHUIRemarkupView($viewer, $instructions))
+ ->setRemarkupOption(
+ PHUIRemarkupView::OPTION_PRESERVE_LINEBREAKS,
+ false);
+
+ return $this->newDialog()
+ ->setTitle(pht('Export Format Not Available'))
+ ->appendChild($markup)
+ ->addCancelButton($cancel_uri, pht('Done'));
+ }
+
$format = idx($formats, $format_key);
if (!$format) {
$e_format = pht('Invalid');
$errors[] = pht('Choose a valid export format.');
}
if (!$errors) {
- $query = $engine->buildQueryFromSavedQuery($saved_query);
-
- // NOTE: We aren't reading the pager from the request. Exports always
- // affect the entire result set.
- $pager = $engine->newPagerForSavedQuery($saved_query);
- $pager->setPageSize(0x7FFFFFFF);
-
- $objects = $engine->executeQuery($query, $pager);
-
- $extension = $format->getFileExtension();
- $mime_type = $format->getMIMEContentType();
- $filename = $filename.'.'.$extension;
-
- $format = clone $format;
- $format->setViewer($viewer);
+ $this->writeExportFormatPreference($format_key);
- $export_data = $engine->newExport($objects);
+ $export_engine = id(new PhabricatorExportEngine())
+ ->setViewer($viewer)
+ ->setSearchEngine($engine)
+ ->setSavedQuery($saved_query)
+ ->setTitle($sheet_title)
+ ->setFilename($filename)
+ ->setExportFormat($format);
- if (count($export_data) !== count($objects)) {
- throw new Exception(
- pht(
- 'Search engine exported the wrong number of objects, expected '.
- '%s but got %s.',
- phutil_count($objects),
- phutil_count($export_data)));
- }
-
- $objects = array_values($objects);
- $export_data = array_values($export_data);
+ if ($is_large_export) {
+ $job = $export_engine->newBulkJob($request);
- $field_list = $engine->newExportFieldList();
- $field_list = mpull($field_list, null, 'getKey');
-
- for ($ii = 0; $ii < count($objects); $ii++) {
- $format->addObject($objects[$ii], $field_list, $export_data[$ii]);
+ return id(new AphrontRedirectResponse())
+ ->setURI($job->getMonitorURI());
+ } else {
+ $file = $export_engine->exportFile();
+ return $file->newDownloadResponse();
}
-
- $export_result = $format->newFileData();
-
- $file = PhabricatorFile::newFromFileData(
- $export_result,
- array(
- 'name' => $filename,
- 'authorPHID' => $viewer->getPHID(),
- 'ttl.relative' => phutil_units('15 minutes in seconds'),
- 'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
- 'mime-type' => $mime_type,
- ));
-
- return $this->newDialog()
- ->setTitle(pht('Download Results'))
- ->appendParagraph(
- pht('Click the download button to download the exported data.'))
- ->addCancelButton($cancel_uri, pht('Done'))
- ->setSubmitURI($file->getDownloadURI())
- ->setDisableWorkflowOnSubmit(true)
- ->addSubmitButton(pht('Download Data'));
}
}
$export_form = id(new AphrontFormView())
->setViewer($viewer)
->appendControl(
id(new AphrontFormSelectControl())
->setName('format')
->setLabel(pht('Format'))
->setError($e_format)
+ ->setValue($format_key)
->setOptions($format_options));
+ if ($is_large_export) {
+ $submit_button = pht('Continue');
+ } else {
+ $submit_button = pht('Download Data');
+ }
+
return $this->newDialog()
->setTitle(pht('Export Results'))
->setErrors($errors)
->appendForm($export_form)
->addCancelButton($cancel_uri)
- ->addSubmitButton(pht('Continue'));
+ ->addSubmitButton($submit_button);
}
private function processEditRequest() {
$parent = $this->getDelegatingController();
$request = $this->getRequest();
$viewer = $request->getUser();
$engine = $this->getSearchEngine();
$nav = $this->getNavigation();
if (!$nav) {
$nav = $this->buildNavigation();
}
$named_queries = $engine->loadAllNamedQueries();
$can_global = $viewer->getIsAdmin();
$groups = array(
'personal' => array(
'name' => pht('Personal Saved Queries'),
'items' => array(),
'edit' => true,
),
'global' => array(
'name' => pht('Global Saved Queries'),
'items' => array(),
'edit' => $can_global,
),
);
foreach ($named_queries as $named_query) {
if ($named_query->isGlobal()) {
$group = 'global';
} else {
$group = 'personal';
}
$groups[$group]['items'][] = $named_query;
}
$default_key = $engine->getDefaultQueryKey();
$lists = array();
foreach ($groups as $group) {
$lists[] = $this->newQueryListView(
$group['name'],
$group['items'],
$default_key,
$group['edit']);
}
$crumbs = $parent
->buildApplicationCrumbs()
->addTextCrumb(pht('Saved Queries'), $engine->getQueryManagementURI())
->setBorder(true);
$nav->selectFilter('query/edit');
$header = id(new PHUIHeaderView())
->setHeader(pht('Saved Queries'))
->setProfileHeader(true);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter($lists);
return $this->newPage()
->setApplicationMenu($this->buildApplicationMenu())
->setTitle(pht('Saved Queries'))
->setCrumbs($crumbs)
->setNavigation($nav)
->appendChild($view);
}
private function newQueryListView(
$list_name,
array $named_queries,
$default_key,
$can_edit) {
$engine = $this->getSearchEngine();
$viewer = $this->getViewer();
$list = id(new PHUIObjectItemListView())
->setViewer($viewer);
if ($can_edit) {
$list_id = celerity_generate_unique_node_id();
$list->setID($list_id);
Javelin::initBehavior(
'search-reorder-queries',
array(
'listID' => $list_id,
'orderURI' => '/search/order/'.get_class($engine).'/',
));
}
foreach ($named_queries as $named_query) {
$class = get_class($engine);
$key = $named_query->getQueryKey();
$item = id(new PHUIObjectItemView())
->setHeader($named_query->getQueryName())
->setHref($engine->getQueryResultsPageURI($key));
if ($named_query->getIsDisabled()) {
if ($can_edit) {
$item->setDisabled(true);
} else {
// If an item is disabled and you don't have permission to edit it,
// just skip it.
continue;
}
}
if ($can_edit) {
if ($named_query->getIsBuiltin() && $named_query->getIsDisabled()) {
$icon = 'fa-plus';
$disable_name = pht('Enable');
} else {
$icon = 'fa-times';
if ($named_query->getIsBuiltin()) {
$disable_name = pht('Disable');
} else {
$disable_name = pht('Delete');
}
}
if ($named_query->getID()) {
$disable_href = '/search/delete/id/'.$named_query->getID().'/';
} else {
$disable_href = '/search/delete/key/'.$key.'/'.$class.'/';
}
$item->addAction(
id(new PHUIListItemView())
->setIcon($icon)
->setHref($disable_href)
->setRenderNameAsTooltip(true)
->setName($disable_name)
->setWorkflow(true));
}
$default_disabled = $named_query->getIsDisabled();
$default_icon = 'fa-thumb-tack';
if ($default_key === $key) {
$default_color = 'green';
} else {
$default_color = null;
}
$item->addAction(
id(new PHUIListItemView())
->setIcon("{$default_icon} {$default_color}")
->setHref('/search/default/'.$key.'/'.$class.'/')
->setRenderNameAsTooltip(true)
->setName(pht('Make Default'))
->setWorkflow(true)
->setDisabled($default_disabled));
if ($can_edit) {
if ($named_query->getIsBuiltin()) {
$edit_icon = 'fa-lock lightgreytext';
$edit_disabled = true;
$edit_name = pht('Builtin');
$edit_href = null;
} else {
$edit_icon = 'fa-pencil';
$edit_disabled = false;
$edit_name = pht('Edit');
$edit_href = '/search/edit/id/'.$named_query->getID().'/';
}
$item->addAction(
id(new PHUIListItemView())
->setIcon($edit_icon)
->setHref($edit_href)
->setRenderNameAsTooltip(true)
->setName($edit_name)
->setDisabled($edit_disabled));
}
$item->setGrippable($can_edit);
$item->addSigil('named-query');
$item->setMetadata(
array(
'queryKey' => $named_query->getQueryKey(),
));
$list->addItem($item);
}
$list->setNoDataString(pht('No saved queries.'));
return id(new PHUIObjectBoxView())
->setHeaderText($list_name)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setObjectList($list);
}
public function buildApplicationMenu() {
$menu = $this->getDelegatingController()
->buildApplicationMenu();
if ($menu instanceof PHUIApplicationMenuView) {
$menu->setSearchEngine($this->getSearchEngine());
}
return $menu;
}
private function buildNavigation() {
$viewer = $this->getViewer();
$engine = $this->getSearchEngine();
$nav = id(new AphrontSideNavFilterView())
->setUser($viewer)
->setBaseURI(new PhutilURI($this->getApplicationURI()));
$engine->addNavigationItems($nav->getMenu());
return $nav;
}
private function renderNewUserView(
PhabricatorApplicationSearchEngine $engine,
$force_nux) {
// Don't render NUX if the user has clicked away from the default page.
if (strlen($this->getQueryKey())) {
return null;
}
// Don't put NUX in panels because it would be weird.
if ($engine->isPanelContext()) {
return null;
}
// Try to render the view itself first, since this should be very cheap
// (just returning some text).
$nux_view = $engine->renderNewUserView();
if (!$nux_view) {
return null;
}
$query = $engine->newQuery();
if (!$query) {
return null;
}
// Try to load any object at all. If we can, the application has seen some
// use so we just render the normal view.
if (!$force_nux) {
$object = $query
->setViewer(PhabricatorUser::getOmnipotentUser())
->setLimit(1)
->execute();
if ($object) {
return null;
}
}
return $nux_view;
}
private function newUseResultsDropdown(
PhabricatorSavedQuery $query,
array $dropdown_items) {
$viewer = $this->getViewer();
$action_list = id(new PhabricatorActionListView())
->setViewer($viewer);
foreach ($dropdown_items as $dropdown_item) {
$action_list->addAction($dropdown_item);
}
return id(new PHUIButtonView())
->setTag('a')
->setHref('#')
->setText(pht('Use Results'))
->setIcon('fa-bars')
->setDropdownMenu($action_list)
->addClass('dropdown');
}
private function newOverflowingView() {
$message = pht(
'The query matched more than one page of results. Results are '.
'paginated before bucketing, so later pages may contain additional '.
'results in any bucket.');
return id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setFlush(true)
->setTitle(pht('Buckets Overflowing'))
->setErrors(
array(
$message,
));
}
private function newOverheatedView(array $results) {
if ($results) {
$message = pht(
'Most objects matching your query are not visible to you, so '.
'filtering results is taking a long time. Only some results are '.
'shown. Refine your query to find results more quickly.');
} else {
$message = pht(
'Most objects matching your query are not visible to you, so '.
'filtering results is taking a long time. Refine your query to '.
'find results more quickly.');
}
return id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setFlush(true)
->setTitle(pht('Query Overheated'))
->setErrors(
array(
$message,
));
}
private function newBuiltinUseActions() {
$actions = array();
$request = $this->getRequest();
$viewer = $request->getUser();
$is_dev = PhabricatorEnv::getEnvConfig('phabricator.developer-mode');
$engine = $this->getSearchEngine();
$engine_class = get_class($engine);
- $query_key = $this->getQueryKey();
- if (!$query_key) {
- $query_key = $engine->getDefaultQueryKey();
- }
+
+ $query_key = $this->getActiveQuery()->getQueryKey();
$can_use = $engine->canUseInPanelContext();
$is_installed = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorDashboardApplication',
$viewer);
if ($can_use && $is_installed) {
$actions[] = id(new PhabricatorActionView())
->setIcon('fa-dashboard')
->setName(pht('Add to Dashboard'))
->setWorkflow(true)
->setHref("/dashboard/panel/install/{$engine_class}/{$query_key}/");
}
if ($this->canExport()) {
$export_uri = $engine->getExportURI($query_key);
$actions[] = id(new PhabricatorActionView())
->setIcon('fa-download')
->setName(pht('Export Data'))
->setWorkflow(true)
->setHref($export_uri);
}
if ($is_dev) {
$engine = $this->getSearchEngine();
$nux_uri = $engine->getQueryBaseURI();
$nux_uri = id(new PhutilURI($nux_uri))
->setQueryParam('nux', true);
$actions[] = id(new PhabricatorActionView())
->setIcon('fa-user-plus')
->setName(pht('DEV: New User State'))
->setHref($nux_uri);
}
if ($is_dev) {
$overheated_uri = $this->getRequest()->getRequestURI()
->setQueryParam('overheated', true);
$actions[] = id(new PhabricatorActionView())
->setIcon('fa-fire')
->setName(pht('DEV: Overheated State'))
->setHref($overheated_uri);
}
return $actions;
}
private function canExport() {
$engine = $this->getSearchEngine();
if (!$engine->canExport()) {
return false;
}
// Don't allow logged-out users to perform exports. There's no technical
// or policy reason they can't, but we don't normally give them access
// to write files or jobs. For now, just err on the side of caution.
$viewer = $this->getViewer();
if (!$viewer->getPHID()) {
return false;
}
return true;
}
+ private function readExportFormatPreference() {
+ $viewer = $this->getViewer();
+ $export_key = PhabricatorPolicyFavoritesSetting::SETTINGKEY;
+ return $viewer->getUserSetting($export_key);
+ }
+
+ private function writeExportFormatPreference($value) {
+ $viewer = $this->getViewer();
+ $request = $this->getRequest();
+
+ if (!$viewer->isLoggedIn()) {
+ return;
+ }
+
+ $export_key = PhabricatorPolicyFavoritesSetting::SETTINGKEY;
+ $preferences = PhabricatorUserPreferences::loadUserPreferences($viewer);
+
+ $editor = id(new PhabricatorUserPreferencesEditor())
+ ->setActor($viewer)
+ ->setContentSourceFromRequest($request)
+ ->setContinueOnNoEffect(true)
+ ->setContinueOnMissingFields(true);
+
+ $xactions = array();
+ $xactions[] = $preferences->newTransaction($export_key, $value);
+ $editor->applyTransactions($preferences, $xactions);
+ }
+
}
diff --git a/src/applications/search/controller/PhabricatorSearchController.php b/src/applications/search/controller/PhabricatorSearchController.php
index a1f6fd68b..b3d3ab18f 100644
--- a/src/applications/search/controller/PhabricatorSearchController.php
+++ b/src/applications/search/controller/PhabricatorSearchController.php
@@ -1,112 +1,114 @@
<?php
final class PhabricatorSearchController
extends PhabricatorSearchBaseController {
const SCOPE_CURRENT_APPLICATION = 'application';
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
+ $query = $request->getStr('query');
- if ($request->getStr('jump') != 'no') {
- $response = PhabricatorJumpNavHandler::getJumpResponse(
- $viewer,
- $request->getStr('query'));
- if ($response) {
- return $response;
+ if ($request->getStr('jump') != 'no' && strlen($query)) {
+ $jump_uri = id(new PhabricatorDatasourceEngine())
+ ->setViewer($viewer)
+ ->newJumpURI($query);
+
+ if ($jump_uri !== null) {
+ return id(new AphrontRedirectResponse())->setURI($jump_uri);
}
}
$engine = new PhabricatorSearchApplicationSearchEngine();
$engine->setViewer($viewer);
// If we're coming from primary search, do some special handling to
// interpret the scope selector and query.
if ($request->getBool('search:primary')) {
// If there's no query, just take the user to advanced search.
- if (!strlen($request->getStr('query'))) {
+ if (!strlen($query)) {
$advanced_uri = '/search/query/advanced/';
return id(new AphrontRedirectResponse())->setURI($advanced_uri);
}
// First, load or construct a template for the search by examining
// the current search scope.
$scope = $request->getStr('search:scope');
$saved = null;
if ($scope == self::SCOPE_CURRENT_APPLICATION) {
$application = id(new PhabricatorApplicationQuery())
->setViewer($viewer)
->withClasses(array($request->getStr('search:application')))
->executeOne();
if ($application) {
$types = $application->getApplicationSearchDocumentTypes();
if ($types) {
$saved = id(new PhabricatorSavedQuery())
->setEngineClassName(get_class($engine))
->setParameter('types', $types)
->setParameter('statuses', array('open'));
}
}
}
if (!$saved && !$engine->isBuiltinQuery($scope)) {
$saved = id(new PhabricatorSavedQueryQuery())
->setViewer($viewer)
->withQueryKeys(array($scope))
->executeOne();
}
if (!$saved) {
if (!$engine->isBuiltinQuery($scope)) {
$scope = 'all';
}
$saved = $engine->buildSavedQueryFromBuiltin($scope);
}
// Add the user's query, then save this as a new saved query and send
// the user to the results page.
- $saved->setParameter('query', $request->getStr('query'));
+ $saved->setParameter('query', $query);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
try {
$saved->setID(null)->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
// Ignore, this is just a repeated search.
}
unset($unguarded);
$query_key = $saved->getQueryKey();
$results_uri = $engine->getQueryResultsPageURI($query_key).'#R';
return id(new AphrontRedirectResponse())->setURI($results_uri);
}
$controller = id(new PhabricatorApplicationSearchController())
->setQueryKey($request->getURIData('queryKey'))
->setSearchEngine($engine)
->setNavigation($this->buildSideNavView());
return $this->delegateToController($controller);
}
public function buildSideNavView($for_app = false) {
$viewer = $this->getViewer();
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI($this->getApplicationURI()));
id(new PhabricatorSearchApplicationSearchEngine())
->setViewer($viewer)
->addNavigationItems($nav->getMenu());
$nav->selectFilter(null);
return $nav;
}
}
diff --git a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php
index 33efd890b..b808291a5 100644
--- a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php
+++ b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php
@@ -1,1465 +1,1599 @@
<?php
/**
* Represents an abstract search engine for an application. It supports
* creating and storing saved queries.
*
* @task construct Constructing Engines
* @task app Applications
* @task builtin Builtin Queries
* @task uri Query URIs
* @task dates Date Filters
* @task order Result Ordering
* @task read Reading Utilities
* @task exec Paging and Executing Queries
* @task render Rendering Results
*/
abstract class PhabricatorApplicationSearchEngine extends Phobject {
private $application;
private $viewer;
private $errors = array();
private $request;
private $context;
private $controller;
private $namedQueries;
private $navigationItems = array();
const CONTEXT_LIST = 'list';
const CONTEXT_PANEL = 'panel';
const BUCKET_NONE = 'none';
public function setController(PhabricatorController $controller) {
$this->controller = $controller;
return $this;
}
public function getController() {
return $this->controller;
}
public function buildResponse() {
$controller = $this->getController();
$request = $controller->getRequest();
$search = id(new PhabricatorApplicationSearchController())
->setQueryKey($request->getURIData('queryKey'))
->setSearchEngine($this);
return $controller->delegateToController($search);
}
public function newResultObject() {
// We may be able to get this automatically if newQuery() is implemented.
$query = $this->newQuery();
if ($query) {
$object = $query->newResultObject();
if ($object) {
return $object;
}
}
return null;
}
public function newQuery() {
return null;
}
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
protected function requireViewer() {
if (!$this->viewer) {
throw new PhutilInvalidStateException('setViewer');
}
return $this->viewer;
}
public function setContext($context) {
$this->context = $context;
return $this;
}
public function isPanelContext() {
return ($this->context == self::CONTEXT_PANEL);
}
public function setNavigationItems(array $navigation_items) {
assert_instances_of($navigation_items, 'PHUIListItemView');
$this->navigationItems = $navigation_items;
return $this;
}
public function getNavigationItems() {
return $this->navigationItems;
}
public function canUseInPanelContext() {
return true;
}
public function saveQuery(PhabricatorSavedQuery $query) {
$query->setEngineClassName(get_class($this));
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
try {
$query->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
// Ignore, this is just a repeated search.
}
unset($unguarded);
}
/**
* Create a saved query object from the request.
*
* @param AphrontRequest The search request.
* @return PhabricatorSavedQuery
*/
public function buildSavedQueryFromRequest(AphrontRequest $request) {
$fields = $this->buildSearchFields();
$viewer = $this->requireViewer();
$saved = new PhabricatorSavedQuery();
foreach ($fields as $field) {
$field->setViewer($viewer);
$value = $field->readValueFromRequest($request);
$saved->setParameter($field->getKey(), $value);
}
return $saved;
}
/**
* Executes the saved query.
*
* @param PhabricatorSavedQuery The saved query to operate on.
* @return PhabricatorQuery The result of the query.
*/
public function buildQueryFromSavedQuery(PhabricatorSavedQuery $original) {
$saved = clone $original;
$this->willUseSavedQuery($saved);
$fields = $this->buildSearchFields();
$viewer = $this->requireViewer();
$map = array();
foreach ($fields as $field) {
$field->setViewer($viewer);
$field->readValueFromSavedQuery($saved);
$value = $field->getValueForQuery($field->getValue());
$map[$field->getKey()] = $value;
}
$original->attachParameterMap($map);
$query = $this->buildQueryFromParameters($map);
$object = $this->newResultObject();
if (!$object) {
return $query;
}
$extensions = $this->getEngineExtensions();
foreach ($extensions as $extension) {
$extension->applyConstraintsToQuery($object, $query, $saved, $map);
}
$order = $saved->getParameter('order');
$builtin = $query->getBuiltinOrderAliasMap();
if (strlen($order) && isset($builtin[$order])) {
$query->setOrder($order);
} else {
// If the order is invalid or not available, we choose the first
// builtin order. This isn't always the default order for the query,
// but is the first value in the "Order" dropdown, and makes the query
// behavior more consistent with the UI. In queries where the two
// orders differ, this order is the preferred order for humans.
$query->setOrder(head_key($builtin));
}
return $query;
}
/**
* Hook for subclasses to adjust saved queries prior to use.
*
* If an application changes how queries are saved, it can implement this
* hook to keep old queries working the way users expect, by reading,
* adjusting, and overwriting parameters.
*
* @param PhabricatorSavedQuery Saved query which will be executed.
* @return void
*/
protected function willUseSavedQuery(PhabricatorSavedQuery $saved) {
return;
}
protected function buildQueryFromParameters(array $parameters) {
throw new PhutilMethodNotImplementedException();
}
/**
* Builds the search form using the request.
*
* @param AphrontFormView Form to populate.
* @param PhabricatorSavedQuery The query from which to build the form.
* @return void
*/
public function buildSearchForm(
AphrontFormView $form,
PhabricatorSavedQuery $saved) {
$saved = clone $saved;
$this->willUseSavedQuery($saved);
$fields = $this->buildSearchFields();
$fields = $this->adjustFieldsForDisplay($fields);
$viewer = $this->requireViewer();
foreach ($fields as $field) {
$field->setViewer($viewer);
$field->readValueFromSavedQuery($saved);
}
foreach ($fields as $field) {
foreach ($field->getErrors() as $error) {
$this->addError(last($error));
}
}
foreach ($fields as $field) {
$field->appendToForm($form);
}
}
protected function buildSearchFields() {
$fields = array();
foreach ($this->buildCustomSearchFields() as $field) {
$fields[] = $field;
}
$object = $this->newResultObject();
if ($object) {
$extensions = $this->getEngineExtensions();
foreach ($extensions as $extension) {
$extension_fields = $extension->getSearchFields($object);
foreach ($extension_fields as $extension_field) {
$fields[] = $extension_field;
}
}
}
$query = $this->newQuery();
if ($query && $this->shouldShowOrderField()) {
$orders = $query->getBuiltinOrders();
$orders = ipull($orders, 'name');
$fields[] = id(new PhabricatorSearchOrderField())
->setLabel(pht('Order By'))
->setKey('order')
->setOrderAliases($query->getBuiltinOrderAliasMap())
->setOptions($orders);
}
$buckets = $this->newResultBuckets();
if ($query && $buckets) {
$bucket_options = array(
self::BUCKET_NONE => pht('No Bucketing'),
) + mpull($buckets, 'getResultBucketName');
$fields[] = id(new PhabricatorSearchSelectField())
->setLabel(pht('Bucket'))
->setKey('bucket')
->setOptions($bucket_options);
}
$field_map = array();
foreach ($fields as $field) {
$key = $field->getKey();
if (isset($field_map[$key])) {
throw new Exception(
pht(
'Two fields in this SearchEngine use the same key ("%s"), but '.
'each field must use a unique key.',
$key));
}
$field_map[$key] = $field;
}
return $field_map;
}
protected function shouldShowOrderField() {
return true;
}
private function adjustFieldsForDisplay(array $field_map) {
$order = $this->getDefaultFieldOrder();
$head_keys = array();
$tail_keys = array();
$seen_tail = false;
foreach ($order as $order_key) {
if ($order_key === '...') {
$seen_tail = true;
continue;
}
if (!$seen_tail) {
$head_keys[] = $order_key;
} else {
$tail_keys[] = $order_key;
}
}
$head = array_select_keys($field_map, $head_keys);
$body = array_diff_key($field_map, array_fuse($tail_keys));
$tail = array_select_keys($field_map, $tail_keys);
$result = $head + $body + $tail;
// Force the fulltext "query" field to the top unconditionally.
$result = array_select_keys($result, array('query')) + $result;
foreach ($this->getHiddenFields() as $hidden_key) {
unset($result[$hidden_key]);
}
return $result;
}
protected function buildCustomSearchFields() {
throw new PhutilMethodNotImplementedException();
}
/**
* Define the default display order for fields by returning a list of
* field keys.
*
* You can use the special key `...` to mean "all unspecified fields go
* here". This lets you easily put important fields at the top of the form,
* standard fields in the middle of the form, and less important fields at
* the bottom.
*
* For example, you might return a list like this:
*
* return array(
* 'authorPHIDs',
* 'reviewerPHIDs',
* '...',
* 'createdAfter',
* 'createdBefore',
* );
*
* Any unspecified fields (including custom fields and fields added
* automatically by infrastructure) will be put in the middle.
*
* @return list<string> Default ordering for field keys.
*/
protected function getDefaultFieldOrder() {
return array();
}
/**
* Return a list of field keys which should be hidden from the viewer.
*
* @return list<string> Fields to hide.
*/
protected function getHiddenFields() {
return array();
}
public function getErrors() {
return $this->errors;
}
public function addError($error) {
$this->errors[] = $error;
return $this;
}
/**
* Return an application URI corresponding to the results page of a query.
* Normally, this is something like `/application/query/QUERYKEY/`.
*
* @param string The query key to build a URI for.
* @return string URI where the query can be executed.
* @task uri
*/
public function getQueryResultsPageURI($query_key) {
return $this->getURI('query/'.$query_key.'/');
}
/**
* Return an application URI for query management. This is used when, e.g.,
* a query deletion operation is cancelled.
*
* @return string URI where queries can be managed.
* @task uri
*/
public function getQueryManagementURI() {
return $this->getURI('query/edit/');
}
public function getQueryBaseURI() {
return $this->getURI('');
}
public function getExportURI($query_key) {
return $this->getURI('query/'.$query_key.'/export/');
}
/**
* Return the URI to a path within the application. Used to construct default
* URIs for management and results.
*
* @return string URI to path.
* @task uri
*/
abstract protected function getURI($path);
/**
* Return a human readable description of the type of objects this query
* searches for.
*
* For example, "Tasks" or "Commits".
*
* @return string Human-readable description of what this engine is used to
* find.
*/
abstract public function getResultTypeDescription();
public function newSavedQuery() {
return id(new PhabricatorSavedQuery())
->setEngineClassName(get_class($this));
}
public function addNavigationItems(PHUIListView $menu) {
$viewer = $this->requireViewer();
$menu->newLabel(pht('Queries'));
$named_queries = $this->loadEnabledNamedQueries();
foreach ($named_queries as $query) {
$key = $query->getQueryKey();
$uri = $this->getQueryResultsPageURI($key);
$menu->newLink($query->getQueryName(), $uri, 'query/'.$key);
}
if ($viewer->isLoggedIn()) {
$manage_uri = $this->getQueryManagementURI();
$menu->newLink(pht('Edit Queries...'), $manage_uri, 'query/edit');
}
$menu->newLabel(pht('Search'));
$advanced_uri = $this->getQueryResultsPageURI('advanced');
$menu->newLink(pht('Advanced Search'), $advanced_uri, 'query/advanced');
foreach ($this->navigationItems as $extra_item) {
$menu->addMenuItem($extra_item);
}
return $this;
}
public function loadAllNamedQueries() {
$viewer = $this->requireViewer();
$builtin = $this->getBuiltinQueries();
if ($this->namedQueries === null) {
$named_queries = id(new PhabricatorNamedQueryQuery())
->setViewer($viewer)
->withEngineClassNames(array(get_class($this)))
->withUserPHIDs(
array(
$viewer->getPHID(),
PhabricatorNamedQuery::SCOPE_GLOBAL,
))
->execute();
$named_queries = mpull($named_queries, null, 'getQueryKey');
$builtin = mpull($builtin, null, 'getQueryKey');
foreach ($named_queries as $key => $named_query) {
if ($named_query->getIsBuiltin()) {
if (isset($builtin[$key])) {
$named_queries[$key]->setQueryName($builtin[$key]->getQueryName());
unset($builtin[$key]);
} else {
unset($named_queries[$key]);
}
}
unset($builtin[$key]);
}
$named_queries = msortv($named_queries, 'getNamedQuerySortVector');
$this->namedQueries = $named_queries;
}
return $this->namedQueries + $builtin;
}
public function loadEnabledNamedQueries() {
$named_queries = $this->loadAllNamedQueries();
foreach ($named_queries as $key => $named_query) {
if ($named_query->getIsBuiltin() && $named_query->getIsDisabled()) {
unset($named_queries[$key]);
}
}
return $named_queries;
}
public function getDefaultQueryKey() {
$viewer = $this->requireViewer();
$configs = id(new PhabricatorNamedQueryConfigQuery())
->setViewer($viewer)
->withEngineClassNames(array(get_class($this)))
->withScopePHIDs(
array(
$viewer->getPHID(),
PhabricatorNamedQueryConfig::SCOPE_GLOBAL,
))
->execute();
$configs = msortv($configs, 'getStrengthSortVector');
$key_pinned = PhabricatorNamedQueryConfig::PROPERTY_PINNED;
$map = $this->loadEnabledNamedQueries();
foreach ($configs as $config) {
$pinned = $config->getConfigProperty($key_pinned);
if (!isset($map[$pinned])) {
continue;
}
return $pinned;
}
return head_key($map);
}
protected function setQueryProjects(
PhabricatorCursorPagedPolicyAwareQuery $query,
PhabricatorSavedQuery $saved) {
$datasource = id(new PhabricatorProjectLogicalDatasource())
->setViewer($this->requireViewer());
$projects = $saved->getParameter('projects', array());
$constraints = $datasource->evaluateTokens($projects);
if ($constraints) {
$query->withEdgeLogicConstraints(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
$constraints);
}
return $this;
}
/* -( Applications )------------------------------------------------------- */
protected function getApplicationURI($path = '') {
return $this->getApplication()->getApplicationURI($path);
}
protected function getApplication() {
if (!$this->application) {
$class = $this->getApplicationClassName();
$this->application = id(new PhabricatorApplicationQuery())
->setViewer($this->requireViewer())
->withClasses(array($class))
->withInstalled(true)
->executeOne();
if (!$this->application) {
throw new Exception(
pht(
'Application "%s" is not installed!',
$class));
}
}
return $this->application;
}
abstract public function getApplicationClassName();
/* -( Constructing Engines )----------------------------------------------- */
/**
* Load all available application search engines.
*
* @return list<PhabricatorApplicationSearchEngine> All available engines.
* @task construct
*/
public static function getAllEngines() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->execute();
}
/**
* Get an engine by class name, if it exists.
*
* @return PhabricatorApplicationSearchEngine|null Engine, or null if it does
* not exist.
* @task construct
*/
public static function getEngineByClassName($class_name) {
return idx(self::getAllEngines(), $class_name);
}
/* -( Builtin Queries )---------------------------------------------------- */
/**
* @task builtin
*/
public function getBuiltinQueries() {
$names = $this->getBuiltinQueryNames();
$queries = array();
$sequence = 0;
foreach ($names as $key => $name) {
$queries[$key] = id(new PhabricatorNamedQuery())
->setUserPHID(PhabricatorNamedQuery::SCOPE_GLOBAL)
->setEngineClassName(get_class($this))
->setQueryName($name)
->setQueryKey($key)
->setSequence((1 << 24) + $sequence++)
->setIsBuiltin(true);
}
return $queries;
}
/**
* @task builtin
*/
public function getBuiltinQuery($query_key) {
if (!$this->isBuiltinQuery($query_key)) {
throw new Exception(pht("'%s' is not a builtin!", $query_key));
}
return idx($this->getBuiltinQueries(), $query_key);
}
/**
* @task builtin
*/
protected function getBuiltinQueryNames() {
return array();
}
/**
* @task builtin
*/
public function isBuiltinQuery($query_key) {
$builtins = $this->getBuiltinQueries();
return isset($builtins[$query_key]);
}
/**
* @task builtin
*/
public function buildSavedQueryFromBuiltin($query_key) {
throw new Exception(pht("Builtin '%s' is not supported!", $query_key));
}
/* -( Reading Utilities )--------------------------------------------------- */
/**
* Read a list of user PHIDs from a request in a flexible way. This method
* supports either of these forms:
*
* users[]=alincoln&users[]=htaft
* users=alincoln,htaft
*
* Additionally, users can be specified either by PHID or by name.
*
* The main goal of this flexibility is to allow external programs to generate
* links to pages (like "alincoln's open revisions") without needing to make
* API calls.
*
* @param AphrontRequest Request to read user PHIDs from.
* @param string Key to read in the request.
* @param list<const> Other permitted PHID types.
* @return list<phid> List of user PHIDs and selector functions.
* @task read
*/
protected function readUsersFromRequest(
AphrontRequest $request,
$key,
array $allow_types = array()) {
$list = $this->readListFromRequest($request, $key);
$phids = array();
$names = array();
$allow_types = array_fuse($allow_types);
$user_type = PhabricatorPeopleUserPHIDType::TYPECONST;
foreach ($list as $item) {
$type = phid_get_type($item);
if ($type == $user_type) {
$phids[] = $item;
} else if (isset($allow_types[$type])) {
$phids[] = $item;
} else {
if (PhabricatorTypeaheadDatasource::isFunctionToken($item)) {
// If this is a function, pass it through unchanged; we'll evaluate
// it later.
$phids[] = $item;
} else {
$names[] = $item;
}
}
}
if ($names) {
$users = id(new PhabricatorPeopleQuery())
->setViewer($this->requireViewer())
->withUsernames($names)
->execute();
foreach ($users as $user) {
$phids[] = $user->getPHID();
}
$phids = array_unique($phids);
}
return $phids;
}
/**
* Read a list of subscribers from a request in a flexible way.
*
* @param AphrontRequest Request to read PHIDs from.
* @param string Key to read in the request.
* @return list<phid> List of object PHIDs.
* @task read
*/
protected function readSubscribersFromRequest(
AphrontRequest $request,
$key) {
return $this->readUsersFromRequest(
$request,
$key,
array(
PhabricatorProjectProjectPHIDType::TYPECONST,
));
}
/**
* Read a list of generic PHIDs from a request in a flexible way. Like
* @{method:readUsersFromRequest}, this method supports either array or
* comma-delimited forms. Objects can be specified either by PHID or by
* object name.
*
* @param AphrontRequest Request to read PHIDs from.
* @param string Key to read in the request.
* @param list<const> Optional, list of permitted PHID types.
* @return list<phid> List of object PHIDs.
*
* @task read
*/
protected function readPHIDsFromRequest(
AphrontRequest $request,
$key,
array $allow_types = array()) {
$list = $this->readListFromRequest($request, $key);
$objects = id(new PhabricatorObjectQuery())
->setViewer($this->requireViewer())
->withNames($list)
->execute();
$list = mpull($objects, 'getPHID');
if (!$list) {
return array();
}
// If only certain PHID types are allowed, filter out all the others.
if ($allow_types) {
$allow_types = array_fuse($allow_types);
foreach ($list as $key => $phid) {
if (empty($allow_types[phid_get_type($phid)])) {
unset($list[$key]);
}
}
}
return $list;
}
/**
* 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.
*/
protected function readListFromRequest(
AphrontRequest $request,
$key) {
$list = $request->getArr($key, null);
if ($list === null) {
$list = $request->getStrList($key);
}
if (!$list) {
return array();
}
return $list;
}
protected function readBoolFromRequest(
AphrontRequest $request,
$key) {
if (!strlen($request->getStr($key))) {
return null;
}
return $request->getBool($key);
}
protected function getBoolFromQuery(PhabricatorSavedQuery $query, $key) {
$value = $query->getParameter($key);
if ($value === null) {
return $value;
}
return $value ? 'true' : 'false';
}
/* -( Dates )-------------------------------------------------------------- */
/**
* @task dates
*/
protected function parseDateTime($date_time) {
if (!strlen($date_time)) {
return null;
}
return PhabricatorTime::parseLocalTime($date_time, $this->requireViewer());
}
/**
* @task dates
*/
protected function buildDateRange(
AphrontFormView $form,
PhabricatorSavedQuery $saved_query,
$start_key,
$start_name,
$end_key,
$end_name) {
$start_str = $saved_query->getParameter($start_key);
$start = null;
if (strlen($start_str)) {
$start = $this->parseDateTime($start_str);
if (!$start) {
$this->addError(
pht(
'"%s" date can not be parsed.',
$start_name));
}
}
$end_str = $saved_query->getParameter($end_key);
$end = null;
if (strlen($end_str)) {
$end = $this->parseDateTime($end_str);
if (!$end) {
$this->addError(
pht(
'"%s" date can not be parsed.',
$end_name));
}
}
if ($start && $end && ($start >= $end)) {
$this->addError(
pht(
'"%s" must be a date before "%s".',
$start_name,
$end_name));
}
$form
->appendChild(
id(new PHUIFormFreeformDateControl())
->setName($start_key)
->setLabel($start_name)
->setValue($start_str))
->appendChild(
id(new AphrontFormTextControl())
->setName($end_key)
->setLabel($end_name)
->setValue($end_str));
}
/* -( Paging and Executing Queries )--------------------------------------- */
protected function newResultBuckets() {
return array();
}
public function getResultBucket(PhabricatorSavedQuery $saved) {
$key = $saved->getParameter('bucket');
if ($key == self::BUCKET_NONE) {
return null;
}
$buckets = $this->newResultBuckets();
return idx($buckets, $key);
}
public function getPageSize(PhabricatorSavedQuery $saved) {
$bucket = $this->getResultBucket($saved);
$limit = (int)$saved->getParameter('limit');
if ($limit > 0) {
if ($bucket) {
$bucket->setPageSize($limit);
}
return $limit;
}
if ($bucket) {
return $bucket->getPageSize();
}
return 100;
}
public function shouldUseOffsetPaging() {
return false;
}
public function newPagerForSavedQuery(PhabricatorSavedQuery $saved) {
if ($this->shouldUseOffsetPaging()) {
$pager = new PHUIPagerView();
} else {
$pager = new AphrontCursorPagerView();
}
$page_size = $this->getPageSize($saved);
if (is_finite($page_size)) {
$pager->setPageSize($page_size);
} else {
// Consider an INF pagesize to mean a large finite pagesize.
// TODO: It would be nice to handle this more gracefully, but math
// with INF seems to vary across PHP versions, systems, and runtimes.
$pager->setPageSize(0xFFFF);
}
return $pager;
}
public function executeQuery(
PhabricatorPolicyAwareQuery $query,
AphrontView $pager) {
$query->setViewer($this->requireViewer());
if ($this->shouldUseOffsetPaging()) {
$objects = $query->executeWithOffsetPager($pager);
} else {
$objects = $query->executeWithCursorPager($pager);
}
$this->didExecuteQuery($query);
return $objects;
}
protected function didExecuteQuery(PhabricatorPolicyAwareQuery $query) {
return;
}
/* -( Rendering )---------------------------------------------------------- */
public function setRequest(AphrontRequest $request) {
$this->request = $request;
return $this;
}
public function getRequest() {
return $this->request;
}
public function renderResults(
array $objects,
PhabricatorSavedQuery $query) {
$phids = $this->getRequiredHandlePHIDsForResultList($objects, $query);
if ($phids) {
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->requireViewer())
->witHPHIDs($phids)
->execute();
} else {
$handles = array();
}
return $this->renderResultList($objects, $query, $handles);
}
protected function getRequiredHandlePHIDsForResultList(
array $objects,
PhabricatorSavedQuery $query) {
return array();
}
abstract protected function renderResultList(
array $objects,
PhabricatorSavedQuery $query,
array $handles);
/* -( Application Search )------------------------------------------------- */
public function getSearchFieldsForConduit() {
$standard_fields = $this->buildSearchFields();
$fields = array();
foreach ($standard_fields as $field_key => $field) {
$conduit_key = $field->getConduitKey();
if (isset($fields[$conduit_key])) {
$other = $fields[$conduit_key];
$other_key = $other->getKey();
throw new Exception(
pht(
'SearchFields "%s" (of class "%s") and "%s" (of class "%s") both '.
'define the same Conduit key ("%s"). Keys must be unique.',
$field_key,
get_class($field),
$other_key,
get_class($other),
$conduit_key));
}
$fields[$conduit_key] = $field;
}
// These are handled separately for Conduit, so don't show them as
// supported.
unset($fields['order']);
unset($fields['limit']);
$viewer = $this->requireViewer();
foreach ($fields as $key => $field) {
$field->setViewer($viewer);
}
return $fields;
}
public function buildConduitResponse(
ConduitAPIRequest $request,
ConduitAPIMethod $method) {
$viewer = $this->requireViewer();
$query_key = $request->getValue('queryKey');
if (!strlen($query_key)) {
$saved_query = new PhabricatorSavedQuery();
} else if ($this->isBuiltinQuery($query_key)) {
$saved_query = $this->buildSavedQueryFromBuiltin($query_key);
} else {
$saved_query = id(new PhabricatorSavedQueryQuery())
->setViewer($viewer)
->withQueryKeys(array($query_key))
->executeOne();
if (!$saved_query) {
throw new Exception(
pht(
'Query key "%s" does not correspond to a valid query.',
$query_key));
}
}
$constraints = $request->getValue('constraints', array());
$fields = $this->getSearchFieldsForConduit();
foreach ($fields as $key => $field) {
if (!$field->getConduitParameterType()) {
unset($fields[$key]);
}
}
$valid_constraints = array();
foreach ($fields as $field) {
foreach ($field->getValidConstraintKeys() as $key) {
$valid_constraints[$key] = true;
}
}
foreach ($constraints as $key => $constraint) {
if (empty($valid_constraints[$key])) {
throw new Exception(
pht(
'Constraint "%s" is not a valid constraint for this query.',
$key));
}
}
foreach ($fields as $field) {
if (!$field->getValueExistsInConduitRequest($constraints)) {
continue;
}
$value = $field->readValueFromConduitRequest(
$constraints,
$request->getIsStrictlyTyped());
$saved_query->setParameter($field->getKey(), $value);
}
// NOTE: Currently, when running an ad-hoc query we never persist it into
// a saved query. We might want to add an option to do this in the future
// (for example, to enable a CLI-to-Web workflow where user can view more
// details about results by following a link), but have no use cases for
// it today. If we do identify a use case, we could save the query here.
$query = $this->buildQueryFromSavedQuery($saved_query);
$pager = $this->newPagerForSavedQuery($saved_query);
$attachments = $this->getConduitSearchAttachments();
// TODO: Validate this better.
$attachment_specs = $request->getValue('attachments', array());
$attachments = array_select_keys(
$attachments,
array_keys($attachment_specs));
foreach ($attachments as $key => $attachment) {
$attachment->setViewer($viewer);
}
foreach ($attachments as $key => $attachment) {
$attachment->willLoadAttachmentData($query, $attachment_specs[$key]);
}
$this->setQueryOrderForConduit($query, $request);
$this->setPagerLimitForConduit($pager, $request);
$this->setPagerOffsetsForConduit($pager, $request);
$objects = $this->executeQuery($query, $pager);
$data = array();
if ($objects) {
$field_extensions = $this->getConduitFieldExtensions();
$extension_data = array();
foreach ($field_extensions as $key => $extension) {
$extension_data[$key] = $extension->loadExtensionConduitData($objects);
}
$attachment_data = array();
foreach ($attachments as $key => $attachment) {
$attachment_data[$key] = $attachment->loadAttachmentData(
$objects,
$attachment_specs[$key]);
}
foreach ($objects as $object) {
$field_map = $this->getObjectWireFieldsForConduit(
$object,
$field_extensions,
$extension_data);
$attachment_map = array();
foreach ($attachments as $key => $attachment) {
$attachment_map[$key] = $attachment->getAttachmentForObject(
$object,
$attachment_data[$key],
$attachment_specs[$key]);
}
// If this is empty, we still want to emit a JSON object, not a
// JSON list.
if (!$attachment_map) {
$attachment_map = (object)$attachment_map;
}
$id = (int)$object->getID();
$phid = $object->getPHID();
$data[] = array(
'id' => $id,
'type' => phid_get_type($phid),
'phid' => $phid,
'fields' => $field_map,
'attachments' => $attachment_map,
);
}
}
return array(
'data' => $data,
'maps' => $method->getQueryMaps($query),
'query' => array(
// This may be `null` if we have not saved the query.
'queryKey' => $saved_query->getQueryKey(),
),
'cursor' => array(
'limit' => $pager->getPageSize(),
'after' => $pager->getNextPageID(),
'before' => $pager->getPrevPageID(),
'order' => $request->getValue('order'),
),
);
}
public function getAllConduitFieldSpecifications() {
$extensions = $this->getConduitFieldExtensions();
$object = $this->newQuery()->newResultObject();
$map = array();
foreach ($extensions as $extension) {
$specifications = $extension->getFieldSpecificationsForConduit($object);
foreach ($specifications as $specification) {
$key = $specification->getKey();
if (isset($map[$key])) {
throw new Exception(
pht(
'Two field specifications share the same key ("%s"). Each '.
'specification must have a unique key.',
$key));
}
$map[$key] = $specification;
}
}
return $map;
}
private function getEngineExtensions() {
$extensions = PhabricatorSearchEngineExtension::getAllEnabledExtensions();
foreach ($extensions as $key => $extension) {
$extension
->setViewer($this->requireViewer())
->setSearchEngine($this);
}
$object = $this->newResultObject();
foreach ($extensions as $key => $extension) {
if (!$extension->supportsObject($object)) {
unset($extensions[$key]);
}
}
return $extensions;
}
private function getConduitFieldExtensions() {
$extensions = $this->getEngineExtensions();
$object = $this->newResultObject();
foreach ($extensions as $key => $extension) {
if (!$extension->getFieldSpecificationsForConduit($object)) {
unset($extensions[$key]);
}
}
return $extensions;
}
private function setQueryOrderForConduit($query, ConduitAPIRequest $request) {
$order = $request->getValue('order');
if ($order === null) {
return;
}
if (is_scalar($order)) {
$query->setOrder($order);
} else {
$query->setOrderVector($order);
}
}
private function setPagerLimitForConduit($pager, ConduitAPIRequest $request) {
$limit = $request->getValue('limit');
// If there's no limit specified and the query uses a weird huge page
// size, just leave it at the default gigantic page size. Otherwise,
// make sure it's between 1 and 100, inclusive.
if ($limit === null) {
if ($pager->getPageSize() >= 0xFFFF) {
return;
} else {
$limit = 100;
}
}
if ($limit > 100) {
throw new Exception(
pht(
'Maximum page size for Conduit API method calls is 100, but '.
'this call specified %s.',
$limit));
}
if ($limit < 1) {
throw new Exception(
pht(
'Minimum page size for API searches is 1, but this call '.
'specified %s.',
$limit));
}
$pager->setPageSize($limit);
}
private function setPagerOffsetsForConduit(
$pager,
ConduitAPIRequest $request) {
$before_id = $request->getValue('before');
if ($before_id !== null) {
$pager->setBeforeID($before_id);
}
$after_id = $request->getValue('after');
if ($after_id !== null) {
$pager->setAfterID($after_id);
}
}
protected function getObjectWireFieldsForConduit(
$object,
array $field_extensions,
array $extension_data) {
$fields = array();
foreach ($field_extensions as $key => $extension) {
$data = idx($extension_data, $key, array());
$fields += $extension->getFieldValuesForConduit($object, $data);
}
return $fields;
}
public function getConduitSearchAttachments() {
$extensions = $this->getEngineExtensions();
$object = $this->newResultObject();
$attachments = array();
foreach ($extensions as $extension) {
$extension_attachments = $extension->getSearchAttachments($object);
foreach ($extension_attachments as $attachment) {
$attachment_key = $attachment->getAttachmentKey();
if (isset($attachments[$attachment_key])) {
$other = $attachments[$attachment_key];
throw new Exception(
pht(
'Two search engine attachments (of classes "%s" and "%s") '.
'specify the same attachment key ("%s"); keys must be unique.',
get_class($attachment),
get_class($other),
$attachment_key));
}
$attachments[$attachment_key] = $attachment;
}
}
return $attachments;
}
final public function renderNewUserView() {
$body = $this->getNewUserBody();
if (!$body) {
return null;
}
return $body;
}
protected function getNewUserHeader() {
return null;
}
protected function getNewUserBody() {
return null;
}
public function newUseResultsActions(PhabricatorSavedQuery $saved) {
return array();
}
/* -( Export )------------------------------------------------------------- */
public function canExport() {
$fields = $this->newExportFields();
return (bool)$fields;
}
final public function newExportFieldList() {
- return $this->newExportFields();
+ $object = $this->newResultObject();
+
+ $builtin_fields = array(
+ id(new PhabricatorIDExportField())
+ ->setKey('id')
+ ->setLabel(pht('ID')),
+ );
+
+ if ($object->getConfigOption(LiskDAO::CONFIG_AUX_PHID)) {
+ $builtin_fields[] = id(new PhabricatorPHIDExportField())
+ ->setKey('phid')
+ ->setLabel(pht('PHID'));
+ }
+
+ $fields = mpull($builtin_fields, null, 'getKey');
+
+ $export_fields = $this->newExportFields();
+ foreach ($export_fields as $export_field) {
+ $key = $export_field->getKey();
+
+ if (isset($fields[$key])) {
+ throw new Exception(
+ pht(
+ 'Search engine ("%s") defines an export field with a key ("%s") '.
+ 'that collides with another field. Each field must have a '.
+ 'unique key.',
+ get_class($this),
+ $key));
+ }
+
+ $fields[$key] = $export_field;
+ }
+
+ $extensions = $this->newExportExtensions();
+ foreach ($extensions as $extension) {
+ $extension_fields = $extension->newExportFields();
+ foreach ($extension_fields as $extension_field) {
+ $key = $extension_field->getKey();
+
+ if (isset($fields[$key])) {
+ throw new Exception(
+ pht(
+ 'Export engine extension ("%s") defines an export field with '.
+ 'a key ("%s") that collides with another field. Each field '.
+ 'must have a unique key.',
+ get_class($extension_field),
+ $key));
+ }
+
+ $fields[$key] = $extension_field;
+ }
+ }
+
+ return $fields;
+ }
+
+ final public function newExport(array $objects) {
+ $object = $this->newResultObject();
+ $has_phid = $object->getConfigOption(LiskDAO::CONFIG_AUX_PHID);
+
+ $objects = array_values($objects);
+ $n = count($objects);
+
+ $maps = array();
+ foreach ($objects as $object) {
+ $map = array(
+ 'id' => $object->getID(),
+ );
+
+ if ($has_phid) {
+ $map['phid'] = $object->getPHID();
+ }
+
+ $maps[] = $map;
+ }
+
+ $export_data = $this->newExportData($objects);
+ $export_data = array_values($export_data);
+ if (count($export_data) !== count($objects)) {
+ throw new Exception(
+ pht(
+ 'Search engine ("%s") exported the wrong number of objects, '.
+ 'expected %s but got %s.',
+ get_class($this),
+ phutil_count($objects),
+ phutil_count($export_data)));
+ }
+
+ for ($ii = 0; $ii < $n; $ii++) {
+ $maps[$ii] += $export_data[$ii];
+ }
+
+ $extensions = $this->newExportExtensions();
+ foreach ($extensions as $extension) {
+ $extension_data = $extension->newExportData($objects);
+ $extension_data = array_values($extension_data);
+ if (count($export_data) !== count($objects)) {
+ throw new Exception(
+ pht(
+ 'Export engine extension ("%s") exported the wrong number of '.
+ 'objects, expected %s but got %s.',
+ get_class($extension),
+ phutil_count($objects),
+ phutil_count($export_data)));
+ }
+
+ for ($ii = 0; $ii < $n; $ii++) {
+ $maps[$ii] += $extension_data[$ii];
+ }
+ }
+
+ return $maps;
}
protected function newExportFields() {
return array();
}
+ protected function newExportData(array $objects) {
+ throw new PhutilMethodNotImplementedException();
+ }
+
+ private function newExportExtensions() {
+ $object = $this->newResultObject();
+ $viewer = $this->requireViewer();
+
+ $extensions = PhabricatorExportEngineExtension::getAllExtensions();
+
+ $supported = array();
+ foreach ($extensions as $extension) {
+ $extension = clone $extension;
+ $extension->setViewer($viewer);
+
+ if ($extension->supportsObject($object)) {
+ $supported[] = $extension;
+ }
+ }
+
+ return $supported;
+ }
+
}
diff --git a/src/applications/search/engine/PhabricatorDatasourceEngine.php b/src/applications/search/engine/PhabricatorDatasourceEngine.php
new file mode 100644
index 000000000..a9cb76624
--- /dev/null
+++ b/src/applications/search/engine/PhabricatorDatasourceEngine.php
@@ -0,0 +1,55 @@
+<?php
+
+final class PhabricatorDatasourceEngine extends Phobject {
+
+ private $viewer;
+
+ public function setViewer(PhabricatorUser $viewer) {
+ $this->viewer = $viewer;
+ return $this;
+ }
+
+ public function getViewer() {
+ return $this->viewer;
+ }
+
+ public function getAllQuickSearchDatasources() {
+ return PhabricatorDatasourceEngineExtension::getAllQuickSearchDatasources();
+ }
+
+ public function newJumpURI($query) {
+ $viewer = $this->getViewer();
+ $extensions = PhabricatorDatasourceEngineExtension::getAllExtensions();
+
+ foreach ($extensions as $extension) {
+ $jump_uri = id(clone $extension)
+ ->setViewer($viewer)
+ ->newJumpURI($query);
+
+ if ($jump_uri !== null) {
+ return $jump_uri;
+ }
+ }
+
+ return null;
+ }
+
+ public function newDatasourcesForCompositeDatasource(
+ PhabricatorTypeaheadCompositeDatasource $datasource) {
+ $viewer = $this->getViewer();
+ $extensions = PhabricatorDatasourceEngineExtension::getAllExtensions();
+
+ $sources = array();
+ foreach ($extensions as $extension) {
+ $extension_sources = id(clone $extension)
+ ->setViewer($viewer)
+ ->newDatasourcesForCompositeDatasource($datasource);
+ foreach ($extension_sources as $extension_source) {
+ $sources[] = $extension_source;
+ }
+ }
+
+ return $sources;
+ }
+
+}
diff --git a/src/applications/search/engine/PhabricatorJumpNavHandler.php b/src/applications/search/engine/PhabricatorJumpNavHandler.php
deleted file mode 100644
index ddade36d8..000000000
--- a/src/applications/search/engine/PhabricatorJumpNavHandler.php
+++ /dev/null
@@ -1,135 +0,0 @@
-<?php
-
-final class PhabricatorJumpNavHandler extends Phobject {
-
- public static function getJumpResponse(PhabricatorUser $viewer, $jump) {
- $jump = trim($jump);
-
- $patterns = array(
- '/^a$/i' => 'uri:/diffusion/commit/',
- '/^f$/i' => 'uri:/feed/',
- '/^d$/i' => 'uri:/differential/',
- '/^r$/i' => 'uri:/diffusion/',
- '/^t$/i' => 'uri:/maniphest/',
- '/^p$/i' => 'uri:/project/',
- '/^u$/i' => 'uri:/people/',
- '/^p\s+(.+)$/i' => 'project',
- '/^u\s+(\S+)$/i' => 'user',
- '/^(?:s)\s+(\S+)/i' => 'find-symbol',
- '/^r\s+(.+)$/i' => 'find-repository',
- );
-
- foreach ($patterns as $pattern => $effect) {
- $matches = null;
- if (preg_match($pattern, $jump, $matches)) {
- if (!strncmp($effect, 'uri:', 4)) {
- return id(new AphrontRedirectResponse())
- ->setURI(substr($effect, 4));
- } else {
- switch ($effect) {
- case 'user':
- return id(new AphrontRedirectResponse())
- ->setURI('/p/'.$matches[1].'/');
- case 'project':
- $project = self::findCloselyNamedProject($matches[1]);
- if ($project) {
- return id(new AphrontRedirectResponse())
- ->setURI('/project/view/'.$project->getID().'/');
- } else {
- $jump = $matches[1];
- }
- break;
- case 'find-symbol':
- $context = '';
- $symbol = $matches[1];
- $parts = array();
- if (preg_match('/(.*)(?:\\.|::|->)(.*)/', $symbol, $parts)) {
- $context = '&context='.phutil_escape_uri($parts[1]);
- $symbol = $parts[2];
- }
- return id(new AphrontRedirectResponse())
- ->setURI("/diffusion/symbol/$symbol/?jump=true$context");
- case 'find-repository':
- $raw_query = $matches[1];
-
- $engine = id(new PhabricatorRepository())
- ->newFerretEngine();
-
- $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;
- }
-
- $repositories = id(new PhabricatorRepositoryQuery())
- ->setViewer($viewer)
- ->withFerretConstraint($engine, $fulltext_tokens)
- ->execute();
- if (count($repositories) == 1) {
- // Just one match, jump to repository.
- $uri = head($repositories)->getURI();
- } else {
- // More than one match, jump to search.
- $uri = urisprintf(
- '/diffusion/?order=name&query=%s',
- $raw_query);
- }
- return id(new AphrontRedirectResponse())->setURI($uri);
- default:
- throw new Exception(pht("Unknown jump effect '%s'!", $effect));
- }
- }
- }
- }
-
- // If none of the patterns matched, look for an object by name.
- $objects = id(new PhabricatorObjectQuery())
- ->setViewer($viewer)
- ->withNames(array($jump))
- ->execute();
-
- if (count($objects) == 1) {
- $handle = id(new PhabricatorHandleQuery())
- ->setViewer($viewer)
- ->withPHIDs(mpull($objects, 'getPHID'))
- ->executeOne();
-
- return id(new AphrontRedirectResponse())->setURI($handle->getURI());
- }
-
- return null;
- }
-
- private static function findCloselyNamedProject($name) {
- $project = id(new PhabricatorProject())->loadOneWhere(
- 'name = %s',
- $name);
- if ($project) {
- return $project;
- } else { // no exact match, try a fuzzy match
- $projects = id(new PhabricatorProject())->loadAllWhere(
- 'name LIKE %~',
- $name);
- if ($projects) {
- $min_name_length = PHP_INT_MAX;
- $best_project = null;
- foreach ($projects as $project) {
- $name_length = strlen($project->getName());
- if ($name_length <= $min_name_length) {
- $min_name_length = $name_length;
- $best_project = $project;
- }
- }
- return $best_project;
- } else {
- return null;
- }
- }
- }
-}
diff --git a/src/applications/search/engine/PhabricatorQuickSearchEngine.php b/src/applications/search/engine/PhabricatorQuickSearchEngine.php
deleted file mode 100644
index 4b67dca65..000000000
--- a/src/applications/search/engine/PhabricatorQuickSearchEngine.php
+++ /dev/null
@@ -1,8 +0,0 @@
-<?php
-
-final class PhabricatorQuickSearchEngine extends Phobject {
-
- public function getAllDatasources() {
- return PhabricatorQuickSearchEngineExtension::getAllDatasources();
- }
-}
diff --git a/src/applications/search/engineextension/PhabricatorDatasourceEngineExtension.php b/src/applications/search/engineextension/PhabricatorDatasourceEngineExtension.php
new file mode 100644
index 000000000..c568e9e14
--- /dev/null
+++ b/src/applications/search/engineextension/PhabricatorDatasourceEngineExtension.php
@@ -0,0 +1,45 @@
+<?php
+
+abstract class PhabricatorDatasourceEngineExtension extends Phobject {
+
+ private $viewer;
+
+ final public function setViewer(PhabricatorUser $viewer) {
+ $this->viewer = $viewer;
+ return $this;
+ }
+
+ final public function getViewer() {
+ return $this->viewer;
+ }
+
+ public function newQuickSearchDatasources() {
+ return array();
+ }
+
+ public function newJumpURI($query) {
+ return null;
+ }
+
+ public function newDatasourcesForCompositeDatasource(
+ PhabricatorTypeaheadCompositeDatasource $datasource) {
+ return array();
+ }
+
+ final public static function getAllExtensions() {
+ return id(new PhutilClassMapQuery())
+ ->setAncestorClass(__CLASS__)
+ ->execute();
+ }
+
+ final public static function getAllQuickSearchDatasources() {
+ $extensions = self::getAllExtensions();
+
+ $datasources = array();
+ foreach ($extensions as $extension) {
+ $datasources[] = $extension->newQuickSearchDatasources();
+ }
+
+ return array_mergev($datasources);
+ }
+}
diff --git a/src/applications/search/engineextension/PhabricatorQuickSearchEngineExtension.php b/src/applications/search/engineextension/PhabricatorQuickSearchEngineExtension.php
index 43f0b4c3c..082eb77e7 100644
--- a/src/applications/search/engineextension/PhabricatorQuickSearchEngineExtension.php
+++ b/src/applications/search/engineextension/PhabricatorQuickSearchEngineExtension.php
@@ -1,18 +1,6 @@
<?php
-abstract class PhabricatorQuickSearchEngineExtension extends Phobject {
-
- abstract public function newQuickSearchDatasources();
-
- final public static function getAllDatasources() {
- $extensions = id(new PhutilClassMapQuery())
- ->setAncestorClass(__CLASS__)
- ->execute();
-
- $datasources = array();
- foreach ($extensions as $extension) {
- $datasources[] = $extension->newQuickSearchDatasources();
- }
- return array_mergev($datasources);
- }
-}
+// TODO: This is an older name "PhabricatorDatasourceEngineExtension" purely
+// to preserve compatibility that should be removed soon.
+abstract class PhabricatorQuickSearchEngineExtension
+ extends PhabricatorDatasourceEngineExtension {}
diff --git a/src/applications/search/fulltextstorage/PhabricatorFerretFulltextStorageEngine.php b/src/applications/search/fulltextstorage/PhabricatorFerretFulltextStorageEngine.php
index a7d9570c5..fa03bcafd 100644
--- a/src/applications/search/fulltextstorage/PhabricatorFerretFulltextStorageEngine.php
+++ b/src/applications/search/fulltextstorage/PhabricatorFerretFulltextStorageEngine.php
@@ -1,127 +1,127 @@
<?php
final class PhabricatorFerretFulltextStorageEngine
extends PhabricatorFulltextStorageEngine {
private $fulltextTokens = array();
private $engineLimits;
public function getEngineIdentifier() {
return 'mysql';
}
public function getHostType() {
return new PhabricatorMySQLSearchHost($this);
}
public function reindexAbstractDocument(
PhabricatorSearchAbstractDocument $doc) {
// NOTE: The Ferret engine indexes are rebuilt by an extension rather than
// by the main fulltext engine, and are always built regardless of
// configuration.
return;
}
public function executeSearch(PhabricatorSavedQuery $query) {
$all_objects = id(new PhutilClassMapQuery())
->setAncestorClass('PhabricatorFerretInterface')
->execute();
$type_map = array();
foreach ($all_objects as $object) {
$phid_type = phid_get_type($object->generatePHID());
$type_map[$phid_type] = array(
'object' => $object,
'engine' => $object->newFerretEngine(),
);
}
$types = $query->getParameter('types');
if ($types) {
$type_map = array_select_keys($type_map, $types);
}
$offset = (int)$query->getParameter('offset', 0);
$limit = (int)$query->getParameter('limit', 25);
// NOTE: For now, it's okay to query with the omnipotent viewer here
// because we're just returning PHIDs which we'll filter later.
$viewer = PhabricatorUser::getOmnipotentUser();
$type_results = array();
$metadata = array();
foreach ($type_map as $type => $spec) {
$engine = $spec['engine'];
$object = $spec['object'];
$local_query = new PhabricatorSavedQuery();
$local_query->setParameter('query', $query->getParameter('query'));
$project_phids = $query->getParameter('projectPHIDs');
if ($project_phids) {
$local_query->setParameter('projectPHIDs', $project_phids);
}
$subscriber_phids = $query->getParameter('subscriberPHIDs');
if ($subscriber_phids) {
$local_query->setParameter('subscriberPHIDs', $subscriber_phids);
}
$search_engine = $engine->newSearchEngine()
->setViewer($viewer);
$engine_query = $search_engine->buildQueryFromSavedQuery($local_query)
->setViewer($viewer);
$engine_query
->withFerretQuery($engine, $query)
->setOrder('relevance')
->setLimit($offset + $limit);
$results = $engine_query->execute();
$results = mpull($results, null, 'getPHID');
$type_results[$type] = $results;
$metadata += $engine_query->getFerretMetadata();
if (!$this->fulltextTokens) {
$this->fulltextTokens = $engine_query->getFerretTokens();
}
}
$list = array();
foreach ($type_results as $type => $results) {
$list += $results;
}
// Currently, the list is grouped by object type. For example, all the
// tasks might be first, then all the revisions, and so on. In each group,
// the results are ordered properly.
// Reorder the results so that the highest-ranking results come first,
// no matter which object types they belong to.
- $metadata = msort($metadata, 'getRelevanceSortVector');
+ $metadata = msortv($metadata, 'getRelevanceSortVector');
$list = array_select_keys($list, array_keys($metadata)) + $list;
$result_slice = array_slice($list, $offset, $limit, true);
return array_keys($result_slice);
}
public function indexExists() {
return true;
}
public function getIndexStats() {
return false;
}
public function getFulltextTokens() {
return $this->fulltextTokens;
}
}
diff --git a/src/applications/search/menuitem/PhabricatorLinkProfileMenuItem.php b/src/applications/search/menuitem/PhabricatorLinkProfileMenuItem.php
index f790816c8..0b6a2f330 100644
--- a/src/applications/search/menuitem/PhabricatorLinkProfileMenuItem.php
+++ b/src/applications/search/menuitem/PhabricatorLinkProfileMenuItem.php
@@ -1,158 +1,159 @@
<?php
final class PhabricatorLinkProfileMenuItem
extends PhabricatorProfileMenuItem {
const MENUITEMKEY = 'link';
const FIELD_URI = 'uri';
const FIELD_NAME = 'name';
const FIELD_TOOLTIP = 'tooltip';
public function getMenuItemTypeIcon() {
return 'fa-link';
}
public function getMenuItemTypeName() {
return pht('Link');
}
public function canAddToObject($object) {
return true;
}
public function getDisplayName(
PhabricatorProfileMenuItemConfiguration $config) {
return $this->getLinkName($config);
}
public function buildEditEngineFields(
PhabricatorProfileMenuItemConfiguration $config) {
return array(
id(new PhabricatorTextEditField())
->setKey(self::FIELD_NAME)
->setLabel(pht('Name'))
->setIsRequired(true)
->setValue($this->getLinkName($config)),
id(new PhabricatorTextEditField())
->setKey(self::FIELD_URI)
->setLabel(pht('URI'))
->setIsRequired(true)
->setValue($this->getLinkURI($config)),
id(new PhabricatorTextEditField())
->setKey(self::FIELD_TOOLTIP)
->setLabel(pht('Tooltip'))
->setValue($this->getLinkTooltip($config)),
id(new PhabricatorIconSetEditField())
->setKey('icon')
->setLabel(pht('Icon'))
->setIconSet(new PhabricatorProfileMenuItemIconSet())
->setValue($this->getLinkIcon($config)),
);
}
private function getLinkName(
PhabricatorProfileMenuItemConfiguration $config) {
return $config->getMenuItemProperty('name');
}
private function getLinkIcon(
PhabricatorProfileMenuItemConfiguration $config) {
return $config->getMenuItemProperty('icon', 'link');
}
private function getLinkURI(
PhabricatorProfileMenuItemConfiguration $config) {
return $config->getMenuItemProperty('uri');
}
private function getLinkTooltip(
PhabricatorProfileMenuItemConfiguration $config) {
return $config->getMenuItemProperty('tooltip');
}
private function isValidLinkURI($uri) {
return PhabricatorEnv::isValidURIForLink($uri);
}
protected function newNavigationMenuItems(
PhabricatorProfileMenuItemConfiguration $config) {
$icon = $this->getLinkIcon($config);
$name = $this->getLinkName($config);
$href = $this->getLinkURI($config);
$tooltip = $this->getLinkTooltip($config);
if (!$this->isValidLinkURI($href)) {
$href = '#';
}
$icon_object = id(new PhabricatorProfileMenuItemIconSet())
->getIcon($icon);
if ($icon_object) {
$icon_class = $icon_object->getIcon();
} else {
$icon_class = 'fa-link';
}
$item = $this->newItem()
->setHref($href)
->setName($name)
->setIcon($icon_class)
- ->setTooltip($tooltip);
+ ->setTooltip($tooltip)
+ ->setRel('noreferrer');
return array(
$item,
);
}
public function validateTransactions(
PhabricatorProfileMenuItemConfiguration $config,
$field_key,
$value,
array $xactions) {
$viewer = $this->getViewer();
$errors = array();
if ($field_key == self::FIELD_NAME) {
if ($this->isEmptyTransaction($value, $xactions)) {
$errors[] = $this->newRequiredError(
pht('You must choose a link name.'),
$field_key);
}
}
if ($field_key == self::FIELD_URI) {
if ($this->isEmptyTransaction($value, $xactions)) {
$errors[] = $this->newRequiredError(
pht('You must choose a URI to link to.'),
$field_key);
}
foreach ($xactions as $xaction) {
$new = $xaction['new'];
if (!$new) {
continue;
}
if ($new === $value) {
continue;
}
if (!$this->isValidLinkURI($new)) {
$errors[] = $this->newInvalidError(
pht(
'URI "%s" is not a valid link URI. It should be a full, valid '.
'URI beginning with a protocol like "%s".',
$new,
'https://'),
$xaction['xaction']);
}
}
}
return $errors;
}
}
diff --git a/src/applications/search/typeahead/PhabricatorSearchDatasource.php b/src/applications/search/typeahead/PhabricatorSearchDatasource.php
index ed17a3118..2d8182a2a 100644
--- a/src/applications/search/typeahead/PhabricatorSearchDatasource.php
+++ b/src/applications/search/typeahead/PhabricatorSearchDatasource.php
@@ -1,31 +1,31 @@
<?php
final class PhabricatorSearchDatasource
extends PhabricatorTypeaheadCompositeDatasource {
public function getBrowseTitle() {
return pht('Browse Results');
}
public function getPlaceholderText() {
return pht('Type an object name...');
}
public function getDatasourceApplicationClass() {
return 'PhabricatorSearchApplication';
}
public function getComponentDatasources() {
- $sources = id(new PhabricatorQuickSearchEngine())
- ->getAllDatasources();
+ $sources = id(new PhabricatorDatasourceEngine())
+ ->getAllQuickSearchDatasources();
// These results are always rendered in the full browse display mode, so
// set the browse flag on all component sources.
foreach ($sources as $source) {
$source->setIsBrowse(true);
}
return $sources;
}
}
diff --git a/src/applications/settings/panel/PhabricatorActivitySettingsPanel.php b/src/applications/settings/panel/PhabricatorActivitySettingsPanel.php
index 50f951d66..2759f3a26 100644
--- a/src/applications/settings/panel/PhabricatorActivitySettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorActivitySettingsPanel.php
@@ -1,62 +1,46 @@
<?php
final class PhabricatorActivitySettingsPanel extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'activity';
}
public function getPanelName() {
return pht('Activity Logs');
}
public function getPanelGroupKey() {
return PhabricatorSettingsLogsPanelGroup::PANELGROUPKEY;
}
public function processRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$user = $this->getUser();
$pager = id(new AphrontCursorPagerView())
->readFromRequest($request);
$logs = id(new PhabricatorPeopleLogQuery())
->setViewer($viewer)
->withRelatedPHIDs(array($user->getPHID()))
->executeWithCursorPager($pager);
- $phids = array();
- foreach ($logs as $log) {
- $phids[] = $log->getUserPHID();
- $phids[] = $log->getActorPHID();
- }
-
- if ($phids) {
- $handles = id(new PhabricatorHandleQuery())
- ->setViewer($viewer)
- ->withPHIDs($phids)
- ->execute();
- } else {
- $handles = array();
- }
-
$table = id(new PhabricatorUserLogView())
->setUser($viewer)
- ->setLogs($logs)
- ->setHandles($handles);
+ ->setLogs($logs);
$panel = $this->newBox(pht('Account Activity Logs'), $table);
$pager_box = id(new PHUIBoxView())
->addMargin(PHUI::MARGIN_LARGE)
->appendChild($pager);
return array($panel, $pager_box);
}
public function isManagementPanel() {
return true;
}
}
diff --git a/src/applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php b/src/applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php
index 107816f2e..51ff40ed9 100644
--- a/src/applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php
@@ -1,36 +1,39 @@
<?php
final class PhabricatorEmailFormatSettingsPanel
extends PhabricatorEditEngineSettingsPanel {
const PANELKEY = 'emailformat';
public function getPanelName() {
return pht('Email Format');
}
public function getPanelGroupKey() {
return PhabricatorSettingsEmailPanelGroup::PANELGROUPKEY;
}
public function isUserPanel() {
- return PhabricatorMetaMTAMail::shouldMultiplexAllMail();
+ return PhabricatorMetaMTAMail::shouldMailEachRecipient();
}
public function isManagementPanel() {
- if (!$this->isUserPanel()) {
+ return false;
+/*
+ if (!$this->isUserPanel()) {
return false;
}
if ($this->getUser()->getIsMailingList()) {
return true;
}
return false;
+*/
}
public function isTemplatePanel() {
return true;
}
}
diff --git a/src/applications/settings/setting/PhabricatorEmailStampsSetting.php b/src/applications/settings/setting/PhabricatorEmailStampsSetting.php
new file mode 100644
index 000000000..39403f40a
--- /dev/null
+++ b/src/applications/settings/setting/PhabricatorEmailStampsSetting.php
@@ -0,0 +1,47 @@
+<?php
+
+final class PhabricatorEmailStampsSetting
+ extends PhabricatorSelectSetting {
+
+ const SETTINGKEY = 'stamps';
+
+ const VALUE_BODY_STAMPS = 'body';
+ const VALUE_HEADER_STAMPS = 'header';
+
+ public function getSettingName() {
+ return pht('Send Stamps');
+ }
+
+ public function getSettingPanelKey() {
+ return PhabricatorEmailFormatSettingsPanel::PANELKEY;
+ }
+
+ protected function getSettingOrder() {
+ return 400;
+ }
+
+ protected function getControlInstructions() {
+ return pht(<<<EOREMARKUP
+Phabricator stamps mail with labels like `actor(alice)` which can be used to
+write client mail rules to organize mail. By default, these stamps are sent
+in an `X-Phabricator-Stamps` header.
+
+If you use a client which can not use headers to route mail (like Gmail),
+you can also include the stamps in the message body so mail rules based on
+body content can route messages.
+EOREMARKUP
+ );
+ }
+
+ public function getSettingDefaultValue() {
+ return self::VALUE_HEADER_STAMPS;
+ }
+
+ protected function getSelectOptions() {
+ return array(
+ self::VALUE_HEADER_STAMPS => pht('Mail Headers'),
+ self::VALUE_BODY_STAMPS => pht('Mail Headers and Body'),
+ );
+ }
+
+}
diff --git a/src/applications/settings/setting/PhabricatorFiletreeWidthSetting.php b/src/applications/settings/setting/PhabricatorFiletreeWidthSetting.php
new file mode 100644
index 000000000..5cfc9f3fe
--- /dev/null
+++ b/src/applications/settings/setting/PhabricatorFiletreeWidthSetting.php
@@ -0,0 +1,12 @@
+<?php
+
+final class PhabricatorFiletreeWidthSetting
+ extends PhabricatorInternalSetting {
+
+ const SETTINGKEY = 'filetree.width';
+
+ public function getSettingName() {
+ return pht('Filetree Width');
+ }
+
+}
diff --git a/src/applications/slowvote/editor/PhabricatorSlowvoteEditor.php b/src/applications/slowvote/editor/PhabricatorSlowvoteEditor.php
index 38dbfb12d..cf088f37d 100644
--- a/src/applications/slowvote/editor/PhabricatorSlowvoteEditor.php
+++ b/src/applications/slowvote/editor/PhabricatorSlowvoteEditor.php
@@ -1,96 +1,95 @@
<?php
final class PhabricatorSlowvoteEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorSlowvoteApplication';
}
public function getEditorObjectsDescription() {
return pht('Slowvote');
}
public function getCreateObjectTitle($author, $object) {
return pht('%s created this poll.', $author);
}
public function getCreateObjectTitleForFeed($author, $object) {
return pht('%s created %s.', $author, $object);
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
return $types;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
public function getMailTagsMap() {
return array(
PhabricatorSlowvoteTransaction::MAILTAG_DETAILS =>
pht('Someone changes the poll details.'),
PhabricatorSlowvoteTransaction::MAILTAG_RESPONSES =>
pht('Someone votes on a poll.'),
PhabricatorSlowvoteTransaction::MAILTAG_OTHER =>
pht('Other poll activity not listed above occurs.'),
);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$monogram = $object->getMonogram();
$name = $object->getQuestion();
return id(new PhabricatorMetaMTAMail())
- ->setSubject("{$monogram}: {$name}")
- ->addHeader('Thread-Topic', $monogram);
+ ->setSubject("{$monogram}: {$name}");
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
$description = $object->getDescription();
if (strlen($description)) {
$body->addRemarkupSection(
pht('SLOWVOTE DESCRIPTION'),
$object->getDescription());
}
$body->addLinkSection(
pht('SLOWVOTE DETAIL'),
PhabricatorEnv::getProductionURI('/'.$object->getMonogram()));
return $body;
}
protected function getMailTo(PhabricatorLiskDAO $object) {
return array(
$object->getAuthorPHID(),
$this->requireActor()->getPHID(),
);
}
protected function getMailSubjectPrefix() {
return '[Slowvote]';
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new PhabricatorSlowvoteReplyHandler())
->setMailReceiver($object);
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
}
diff --git a/src/applications/spaces/engineextension/PhabricatorSpacesMailEngineExtension.php b/src/applications/spaces/engineextension/PhabricatorSpacesMailEngineExtension.php
new file mode 100644
index 000000000..7ddbda05f
--- /dev/null
+++ b/src/applications/spaces/engineextension/PhabricatorSpacesMailEngineExtension.php
@@ -0,0 +1,35 @@
+<?php
+
+final class PhabricatorSpacesMailEngineExtension
+ extends PhabricatorMailEngineExtension {
+
+ const EXTENSIONKEY = 'spaces';
+
+ public function supportsObject($object) {
+ return ($object instanceof PhabricatorSpacesInterface);
+ }
+
+ public function newMailStampTemplates($object) {
+ return array(
+ id(new PhabricatorPHIDMailStamp())
+ ->setKey('space')
+ ->setLabel(pht('Space')),
+ );
+ }
+
+ public function newMailStamps($object, array $xactions) {
+ $editor = $this->getEditor();
+ $viewer = $this->getViewer();
+
+ if (!PhabricatorSpacesNamespaceQuery::getSpacesExist()) {
+ return;
+ }
+
+ $space_phid = PhabricatorSpacesNamespaceQuery::getObjectSpacePHID(
+ $object);
+
+ $this->getMailStamp('space')
+ ->setValue($space_phid);
+ }
+
+}
diff --git a/src/applications/spaces/phid/PhabricatorSpacesNamespacePHIDType.php b/src/applications/spaces/phid/PhabricatorSpacesNamespacePHIDType.php
index 86371d642..1399e71c8 100644
--- a/src/applications/spaces/phid/PhabricatorSpacesNamespacePHIDType.php
+++ b/src/applications/spaces/phid/PhabricatorSpacesNamespacePHIDType.php
@@ -1,78 +1,80 @@
<?php
final class PhabricatorSpacesNamespacePHIDType
extends PhabricatorPHIDType {
const TYPECONST = 'SPCE';
public function getTypeName() {
return pht('Space');
}
public function newObject() {
return new PhabricatorSpacesNamespace();
}
public function getPHIDTypeApplicationClass() {
return 'PhabricatorSpacesApplication';
}
protected function buildQueryForObjects(
PhabricatorObjectQuery $query,
array $phids) {
return id(new PhabricatorSpacesNamespaceQuery())
->withPHIDs($phids);
}
public function loadHandles(
PhabricatorHandleQuery $query,
array $handles,
array $objects) {
foreach ($handles as $phid => $handle) {
$namespace = $objects[$phid];
$monogram = $namespace->getMonogram();
$name = $namespace->getNamespaceName();
- $handle->setName($name);
- $handle->setFullName(pht('%s %s', $monogram, $name));
- $handle->setURI('/'.$monogram);
+ $handle
+ ->setName($name)
+ ->setFullName(pht('%s %s', $monogram, $name))
+ ->setURI('/'.$monogram)
+ ->setMailStampName($monogram);
if ($namespace->getIsArchived()) {
$handle->setStatus(PhabricatorObjectHandle::STATUS_CLOSED);
}
}
}
public function canLoadNamedObject($name) {
return preg_match('/^S[1-9]\d*$/i', $name);
}
public function loadNamedObjects(
PhabricatorObjectQuery $query,
array $names) {
$id_map = array();
foreach ($names as $name) {
$id = (int)substr($name, 1);
$id_map[$id][] = $name;
}
$objects = id(new PhabricatorSpacesNamespaceQuery())
->setViewer($query->getViewer())
->withIDs(array_keys($id_map))
->execute();
$results = array();
foreach ($objects as $id => $object) {
foreach (idx($id_map, $id, array()) as $name) {
$results[$name] = $object;
}
}
return $results;
}
}
diff --git a/src/applications/subscriptions/application/PhabricatorSubscriptionsApplication.php b/src/applications/subscriptions/application/PhabricatorSubscriptionsApplication.php
index 56759f5dc..2de2994a9 100644
--- a/src/applications/subscriptions/application/PhabricatorSubscriptionsApplication.php
+++ b/src/applications/subscriptions/application/PhabricatorSubscriptionsApplication.php
@@ -1,35 +1,38 @@
<?php
final class PhabricatorSubscriptionsApplication extends PhabricatorApplication {
public function getName() {
return pht('Subscriptions');
}
public function isLaunchable() {
return false;
}
public function canUninstall() {
return false;
}
public function getEventListeners() {
return array(
new PhabricatorSubscriptionsUIEventListener(),
);
}
public function getRoutes() {
return array(
'/subscriptions/' => array(
'(?P<action>add|delete)/'.
- '(?P<phid>[^/]+)/' => 'PhabricatorSubscriptionsEditController',
+ '(?P<phid>[^/]+)/' => 'PhabricatorSubscriptionsEditController',
+ 'mute/' => array(
+ '(?P<phid>[^/]+)/' => 'PhabricatorSubscriptionsMuteController',
+ ),
'list/(?P<phid>[^/]+)/' => 'PhabricatorSubscriptionsListController',
'transaction/(?P<type>add|rem)/(?<phid>[^/]+)/'
=> 'PhabricatorSubscriptionsTransactionController',
),
);
}
}
diff --git a/src/applications/subscriptions/controller/PhabricatorSubscriptionsMuteController.php b/src/applications/subscriptions/controller/PhabricatorSubscriptionsMuteController.php
new file mode 100644
index 000000000..1369643ff
--- /dev/null
+++ b/src/applications/subscriptions/controller/PhabricatorSubscriptionsMuteController.php
@@ -0,0 +1,92 @@
+<?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/engineextension/PhabricatorSubscriptionsMailEngineExtension.php b/src/applications/subscriptions/engineextension/PhabricatorSubscriptionsMailEngineExtension.php
new file mode 100644
index 000000000..122fad4b0
--- /dev/null
+++ b/src/applications/subscriptions/engineextension/PhabricatorSubscriptionsMailEngineExtension.php
@@ -0,0 +1,32 @@
+<?php
+
+final class PhabricatorSubscriptionsMailEngineExtension
+ extends PhabricatorMailEngineExtension {
+
+ const EXTENSIONKEY = 'subscriptions';
+
+ public function supportsObject($object) {
+ return ($object instanceof PhabricatorSubscribableInterface);
+ }
+
+ public function newMailStampTemplates($object) {
+ return array(
+ id(new PhabricatorPHIDMailStamp())
+ ->setKey('subscriber')
+ ->setLabel(pht('Subscriber')),
+ );
+ }
+
+ public function newMailStamps($object, array $xactions) {
+ $editor = $this->getEditor();
+ $viewer = $this->getViewer();
+
+ $subscriber_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
+ $object->getPHID(),
+ PhabricatorObjectHasSubscriberEdgeType::EDGECONST);
+
+ $this->getMailStamp('subscriber')
+ ->setValue($subscriber_phids);
+ }
+
+}
diff --git a/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php b/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php
index 5f371d69f..caf860117 100644
--- a/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php
+++ b/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php
@@ -1,130 +1,157 @@
<?php
final class PhabricatorSubscriptionsUIEventListener
extends PhabricatorEventListener {
public function register() {
$this->listen(PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS);
$this->listen(PhabricatorEventType::TYPE_UI_WILLRENDERPROPERTIES);
}
public function handleEvent(PhutilEvent $event) {
$object = $event->getValue('object');
switch ($event->getType()) {
case PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS:
$this->handleActionEvent($event);
break;
case PhabricatorEventType::TYPE_UI_WILLRENDERPROPERTIES:
// Hacky solution so that property list view on Diffusion
// commits shows build status, but not Projects, Subscriptions,
// or Tokens.
if ($object instanceof PhabricatorRepositoryCommit) {
return;
}
$this->handlePropertyEvent($event);
break;
}
}
private function handleActionEvent($event) {
$user = $event->getUser();
$user_phid = $user->getPHID();
$object = $event->getValue('object');
if (!$object || !$object->getPHID()) {
// No object, or the object has no PHID yet. No way to subscribe.
return;
}
if (!($object instanceof PhabricatorSubscribableInterface)) {
// This object isn't subscribable.
return;
}
+ $src_phid = $object->getPHID();
+ $subscribed_type = PhabricatorObjectHasSubscriberEdgeType::EDGECONST;
+ $muted_type = PhabricatorMutedByEdgeType::EDGECONST;
+
+ $edges = id(new PhabricatorEdgeQuery())
+ ->withSourcePHIDs(array($src_phid))
+ ->withEdgeTypes(
+ array(
+ $subscribed_type,
+ $muted_type,
+ ))
+ ->withDestinationPHIDs(array($user_phid))
+ ->execute();
+
+ if ($user_phid) {
+ $is_subscribed = isset($edges[$src_phid][$subscribed_type][$user_phid]);
+ $is_muted = isset($edges[$src_phid][$muted_type][$user_phid]);
+ } else {
+ $is_subscribed = false;
+ $is_muted = false;
+ }
+
if ($user_phid && $object->isAutomaticallySubscribed($user_phid)) {
$sub_action = id(new PhabricatorActionView())
->setWorkflow(true)
->setDisabled(true)
->setRenderAsForm(true)
->setHref('/subscriptions/add/'.$object->getPHID().'/')
->setName(pht('Automatically Subscribed'))
->setIcon('fa-check-circle lightgreytext');
} else {
- $subscribed = false;
- if ($user->isLoggedIn()) {
- $src_phid = $object->getPHID();
- $edge_type = PhabricatorObjectHasSubscriberEdgeType::EDGECONST;
-
- $edges = id(new PhabricatorEdgeQuery())
- ->withSourcePHIDs(array($src_phid))
- ->withEdgeTypes(array($edge_type))
- ->withDestinationPHIDs(array($user_phid))
- ->execute();
- $subscribed = isset($edges[$src_phid][$edge_type][$user_phid]);
- }
-
$can_interact = PhabricatorPolicyFilter::canInteract($user, $object);
- if ($subscribed) {
+ if ($is_subscribed) {
$sub_action = id(new PhabricatorActionView())
->setWorkflow(true)
->setRenderAsForm(true)
->setHref('/subscriptions/delete/'.$object->getPHID().'/')
->setName(pht('Unsubscribe'))
->setIcon('fa-minus-circle')
->setDisabled(!$can_interact);
} else {
$sub_action = id(new PhabricatorActionView())
->setWorkflow(true)
->setRenderAsForm(true)
->setHref('/subscriptions/add/'.$object->getPHID().'/')
->setName(pht('Subscribe'))
->setIcon('fa-plus-circle')
->setDisabled(!$can_interact);
}
if (!$user->isLoggedIn()) {
$sub_action->setDisabled(true);
}
}
+ $mute_action = id(new PhabricatorActionView())
+ ->setWorkflow(true)
+ ->setHref('/subscriptions/mute/'.$object->getPHID().'/')
+ ->setDisabled(!$user_phid);
+
+ if (!$is_muted) {
+ $mute_action
+ ->setName(pht('Mute Notifications'))
+ ->setIcon('fa-volume-up');
+ } else {
+ $mute_action
+ ->setName(pht('Unmute Notifications'))
+ ->setIcon('fa-volume-off')
+ ->setColor(PhabricatorActionView::RED);
+ }
+
+
$actions = $event->getValue('actions');
$actions[] = $sub_action;
+ $actions[] = $mute_action;
$event->setValue('actions', $actions);
}
private function handlePropertyEvent($event) {
$user = $event->getUser();
$object = $event->getValue('object');
if (!$object || !$object->getPHID()) {
// No object, or the object has no PHID yet..
return;
}
if (!($object instanceof PhabricatorSubscribableInterface)) {
// This object isn't subscribable.
return;
}
$subscribers = PhabricatorSubscribersQuery::loadSubscribersForPHID(
$object->getPHID());
if ($subscribers) {
$handles = id(new PhabricatorHandleQuery())
->setViewer($user)
->withPHIDs($subscribers)
->execute();
} else {
$handles = array();
}
$sub_view = id(new SubscriptionListStringBuilder())
->setObjectPHID($object->getPHID())
->setHandles($handles)
->buildPropertyString();
$view = $event->getValue('view');
$view->addProperty(pht('Subscribers'), $sub_view);
}
}
diff --git a/src/applications/subscriptions/policyrule/PhabricatorSubscriptionsSubscribersPolicyRule.php b/src/applications/subscriptions/policyrule/PhabricatorSubscriptionsSubscribersPolicyRule.php
index 7da35f370..922f8c965 100644
--- a/src/applications/subscriptions/policyrule/PhabricatorSubscriptionsSubscribersPolicyRule.php
+++ b/src/applications/subscriptions/policyrule/PhabricatorSubscriptionsSubscribersPolicyRule.php
@@ -1,121 +1,128 @@
<?php
final class PhabricatorSubscriptionsSubscribersPolicyRule
extends PhabricatorPolicyRule {
private $subscribed = array();
private $sourcePHIDs = array();
public function getObjectPolicyKey() {
return 'subscriptions.subscribers';
}
public function getObjectPolicyName() {
return pht('Subscribers');
}
public function getPolicyExplanation() {
return pht('Subscribers can take this action.');
}
public function getRuleDescription() {
return pht('subscribers');
}
public function canApplyToObject(PhabricatorPolicyInterface $object) {
return ($object instanceof PhabricatorSubscribableInterface);
}
public function willApplyRules(
PhabricatorUser $viewer,
array $values,
array $objects) {
// We want to let the user see the object if they're a subscriber or
// a member of any project which is a subscriber. Additionally, because
// subscriber state is complex, we need to read hints passed from
// the TransactionEditor to predict policy state after transactions apply.
$viewer_phid = $viewer->getPHID();
if (!$viewer_phid) {
return;
}
if (empty($this->subscribed[$viewer_phid])) {
$this->subscribed[$viewer_phid] = array();
}
- // Load the project PHIDs the user is a member of.
+ // Load the project PHIDs the user is a member of. We use the omnipotent
+ // user here because projects may themselves have "Subscribers" visibility
+ // policies and we don't want to get stuck in an infinite stack of
+ // recursive policy checks. See T13106.
if (!isset($this->sourcePHIDs[$viewer_phid])) {
- $source_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
- $viewer_phid,
- PhabricatorProjectMemberOfProjectEdgeType::EDGECONST);
+ $projects = id(new PhabricatorProjectQuery())
+ ->setViewer(PhabricatorUser::getOmnipotentUser())
+ ->withMemberPHIDs(array($viewer_phid))
+ ->execute();
+
+ $source_phids = mpull($projects, 'getPHID');
$source_phids[] = $viewer_phid;
+
$this->sourcePHIDs[$viewer_phid] = $source_phids;
}
// Look for transaction hints.
foreach ($objects as $key => $object) {
$cache = $this->getTransactionHint($object);
if ($cache === null) {
// We don't have a hint for this object, so we'll deal with it below.
continue;
}
// We have a hint, so use that as the source of truth.
unset($objects[$key]);
foreach ($this->sourcePHIDs[$viewer_phid] as $source_phid) {
if (isset($cache[$source_phid])) {
$this->subscribed[$viewer_phid][$object->getPHID()] = true;
break;
}
}
}
$phids = mpull($objects, 'getPHID');
if (!$phids) {
return;
}
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($this->sourcePHIDs[$viewer_phid])
->withEdgeTypes(
array(
PhabricatorSubscribedToObjectEdgeType::EDGECONST,
))
->withDestinationPHIDs($phids);
$edge_query->execute();
$subscribed = $edge_query->getDestinationPHIDs();
if (!$subscribed) {
return;
}
$this->subscribed[$viewer_phid] += array_fill_keys($subscribed, true);
}
public function applyRule(
PhabricatorUser $viewer,
$value,
PhabricatorPolicyInterface $object) {
$viewer_phid = $viewer->getPHID();
if (!$viewer_phid) {
return false;
}
if ($object->isAutomaticallySubscribed($viewer_phid)) {
return true;
}
$subscribed = idx($this->subscribed, $viewer_phid);
return isset($subscribed[$object->getPHID()]);
}
public function getValueControlType() {
return self::CONTROL_TYPE_NONE;
}
}
diff --git a/src/applications/system/application/PhabricatorSystemApplication.php b/src/applications/system/application/PhabricatorSystemApplication.php
index 7ce2b4dbe..0ec8f6a7a 100644
--- a/src/applications/system/application/PhabricatorSystemApplication.php
+++ b/src/applications/system/application/PhabricatorSystemApplication.php
@@ -1,33 +1,32 @@
<?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',
),
- '/favicon.ico' => 'PhabricatorSystemFaviconController',
);
}
}
diff --git a/src/applications/system/controller/PhabricatorSystemFaviconController.php b/src/applications/system/controller/PhabricatorSystemFaviconController.php
deleted file mode 100644
index d9a31cbcb..000000000
--- a/src/applications/system/controller/PhabricatorSystemFaviconController.php
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-final class PhabricatorSystemFaviconController extends PhabricatorController {
-
- public function shouldRequireLogin() {
- return false;
- }
-
- public function processRequest() {
- $webroot = dirname(phutil_get_library_root('phabricator')).'/webroot/';
- $content = Filesystem::readFile($webroot.'/rsrc/favicons/favicon.ico');
-
- return id(new AphrontFileResponse())
- ->setContent($content)
- ->setMimeType('image/x-icon')
- ->setCacheDurationInSeconds(phutil_units('24 hours in seconds'))
- ->setCanCDN(true);
- }
-}
diff --git a/src/applications/transactions/bulk/PhabricatorBulkEngine.php b/src/applications/transactions/bulk/PhabricatorBulkEngine.php
index 534390b51..009132124 100644
--- a/src/applications/transactions/bulk/PhabricatorBulkEngine.php
+++ b/src/applications/transactions/bulk/PhabricatorBulkEngine.php
@@ -1,464 +1,464 @@
<?php
abstract class PhabricatorBulkEngine extends Phobject {
private $viewer;
private $controller;
private $context = array();
private $objectList;
private $savedQuery;
private $editableList;
private $targetList;
private $rootFormID;
abstract public function newSearchEngine();
abstract public function newEditEngine();
public function getCancelURI() {
$saved_query = $this->savedQuery;
if ($saved_query) {
$path = '/query/'.$saved_query->getQueryKey().'/';
} else {
$path = '/';
}
return $this->getQueryURI($path);
}
public function getDoneURI() {
if ($this->objectList !== null) {
$ids = mpull($this->objectList, 'getID');
$path = '/?ids='.implode(',', $ids);
} else {
$path = '/';
}
return $this->getQueryURI($path);
}
protected function getQueryURI($path = '/') {
$viewer = $this->getViewer();
$engine = id($this->newSearchEngine())
->setViewer($viewer);
return $engine->getQueryBaseURI().ltrim($path, '/');
}
protected function getBulkURI() {
$saved_query = $this->savedQuery;
if ($saved_query) {
$path = '/query/'.$saved_query->getQueryKey().'/';
} else {
$path = '/';
}
return $this->getBulkBaseURI($path);
}
protected function getBulkBaseURI($path) {
return $this->getQueryURI('bulk/'.ltrim($path, '/'));
}
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;
return $this;
}
final public function getController() {
return $this->controller;
}
final public function addContextParameter($key) {
$this->context[$key] = true;
return $this;
}
final public function buildResponse() {
$viewer = $this->getViewer();
$controller = $this->getController();
$request = $controller->getRequest();
$response = $this->loadObjectList();
if ($response) {
return $response;
}
if ($request->isFormPost() && $request->getBool('bulkEngine')) {
return $this->buildEditResponse();
}
$list_view = $this->newBulkObjectList();
$header = id(new PHUIHeaderView())
->setHeader(pht('Bulk Editor'))
->setHeaderIcon('fa-pencil-square-o');
$list_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Working Set'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setObjectList($list_view);
$form_view = $this->newBulkActionForm();
$form_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Actions'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setForm($form_view);
$complete_form = phabricator_form(
$viewer,
array(
'action' => $this->getBulkURI(),
'method' => 'POST',
'id' => $this->getRootFormID(),
),
array(
$this->newContextInputs(),
$list_box,
$form_box,
));
$column_view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter($complete_form);
// TODO: This is a bit hacky and inflexible.
$crumbs = $controller->buildApplicationCrumbsForEditEngine();
$crumbs->addTextCrumb(pht('Query'), $this->getCancelURI());
$crumbs->addTextCrumb(pht('Bulk Editor'));
return $controller->newPage()
->setTitle(pht('Bulk Edit'))
->setCrumbs($crumbs)
->appendChild($column_view);
}
private function loadObjectList() {
$viewer = $this->getViewer();
$controller = $this->getController();
$request = $controller->getRequest();
$search_engine = id($this->newSearchEngine())
->setViewer($viewer);
$query_key = $request->getURIData('queryKey');
if (strlen($query_key)) {
if ($search_engine->isBuiltinQuery($query_key)) {
$saved = $search_engine->buildSavedQueryFromBuiltin($query_key);
} else {
$saved = id(new PhabricatorSavedQueryQuery())
->setViewer($viewer)
->withQueryKeys(array($query_key))
->executeOne();
if (!$saved) {
return new Aphront404Response();
}
}
} else {
// TODO: For now, since we don't deal gracefully with queries which
// match a huge result set, just bail if we don't have any query
// parameters instead of querying for a trillion tasks and timing out.
$request_data = $request->getPassthroughRequestData();
if (!$request_data) {
throw new Exception(
pht(
'Expected a query key or a set of query constraints.'));
}
$saved = $search_engine->buildSavedQueryFromRequest($request);
$search_engine->saveQuery($saved);
}
$object_query = $search_engine->buildQueryFromSavedQuery($saved)
->setViewer($viewer);
$object_list = $object_query->execute();
$object_list = mpull($object_list, null, 'getPHID');
// If the user has submitted the bulk edit form, select only the objects
// they checked.
if ($request->getBool('bulkEngine')) {
$target_phids = $request->getArr('bulkTargetPHIDs');
// NOTE: It's possible that the underlying query result set has changed
// between the time we ran the query initially and now: for example, the
// query was for "Open Tasks" and some tasks were closed while the user
// was making action selections.
// This could result in some objects getting dropped from the working set
// here: we'll have target PHIDs for them, but they will no longer be
// part of the object list. For now, just go with this since it doesn't
// seem like a big problem and may even be desirable.
$this->targetList = array_select_keys($object_list, $target_phids);
} else {
$this->targetList = $object_list;
}
$this->objectList = $object_list;
$this->savedQuery = $saved;
// Filter just the editable objects. We show all the objects which the
// query matches whether they're editable or not, but indicate which ones
// can not be edited to the user.
$editable_list = id(new PhabricatorPolicyFilter())
->setViewer($viewer)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->apply($object_list);
$this->editableList = mpull($editable_list, null, 'getPHID');
return null;
}
private function newBulkObjectList() {
$viewer = $this->getViewer();
$objects = $this->objectList;
$objects = mpull($objects, null, 'getPHID');
$handles = $viewer->loadHandles(array_keys($objects));
$status_closed = PhabricatorObjectHandle::STATUS_CLOSED;
$list = id(new PHUIObjectItemListView())
->setViewer($viewer)
->setFlush(true);
foreach ($objects as $phid => $object) {
$handle = $handles[$phid];
$is_closed = ($handle->getStatus() === $status_closed);
$can_edit = isset($this->editableList[$phid]);
$is_disabled = ($is_closed || !$can_edit);
$is_selected = isset($this->targetList[$phid]);
$item = id(new PHUIObjectItemView())
->setHeader($handle->getFullName())
->setHref($handle->getURI())
->setDisabled($is_disabled)
->setSelectable('bulkTargetPHIDs[]', $phid, $is_selected, !$can_edit);
if (!$can_edit) {
$item->addIcon('fa-pencil red', pht('Not Editable'));
}
$list->addItem($item);
}
return $list;
}
private function newContextInputs() {
$viewer = $this->getViewer();
$controller = $this->getController();
$request = $controller->getRequest();
$parameters = array();
foreach ($this->context as $key => $value) {
$parameters[$key] = $request->getStr($key);
}
$parameters = array(
'bulkEngine' => 1,
) + $parameters;
$result = array();
foreach ($parameters as $key => $value) {
$result[] = phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => $key,
'value' => $value,
));
}
return $result;
}
private function newBulkActionForm() {
$viewer = $this->getViewer();
$input_id = celerity_generate_unique_node_id();
$edit_engine = id($this->newEditEngine())
->setViewer($viewer);
$edit_map = $edit_engine->newBulkEditMap();
$groups = $edit_engine->newBulkEditGroupMap();
$spec = array();
$option_groups = igroup($edit_map, 'group');
$default_value = null;
foreach ($groups as $group_key => $group) {
$options = idx($option_groups, $group_key, array());
if (!$options) {
continue;
}
$option_map = array();
foreach ($options as $option) {
$option_map[] = array(
'key' => $option['xaction'],
'label' => $option['label'],
);
if ($default_value === null) {
$default_value = $option['xaction'];
}
}
$spec[] = array(
'label' => $group->getLabel(),
'options' => $option_map,
);
}
require_celerity_resource('phui-bulk-editor-css');
Javelin::initBehavior(
'bulk-editor',
array(
'rootNodeID' => $this->getRootFormID(),
'inputNodeID' => $input_id,
'edits' => $edit_map,
'optgroups' => array(
'value' => $default_value,
'groups' => $spec,
),
));
$cancel_uri = $this->getCancelURI();
return id(new PHUIFormLayoutView())
->setViewer($viewer)
->appendChild(
phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => 'xactions',
'id' => $input_id,
)))
->appendChild(
id(new PHUIFormInsetView())
->setTitle(pht('Bulk Edit Actions'))
->setRightButton(
javelin_tag(
'a',
array(
'href' => '#',
'class' => 'button button-green',
'sigil' => 'add-action',
'mustcapture' => true,
),
pht('Add Another Action')))
->setContent(
javelin_tag(
'table',
array(
'sigil' => 'bulk-actions',
'class' => 'bulk-edit-table',
),
'')))
->appendChild(
id(new AphrontFormSubmitControl())
- ->setValue(pht('Apply Bulk Edit'))
+ ->setValue(pht('Continue'))
->addCancelButton($cancel_uri));
}
private function buildEditResponse() {
$viewer = $this->getViewer();
$controller = $this->getController();
$request = $controller->getRequest();
if (!$this->objectList) {
throw new Exception(pht('Query does not match any objects.'));
}
if (!$this->editableList) {
throw new Exception(
pht(
'Query does not match any objects you have permission to edit.'));
}
// Restrict the selection set to objects the user can actually edit.
$objects = array_intersect_key($this->editableList, $this->targetList);
if (!$objects) {
throw new Exception(
pht(
'You have not selected any objects to edit.'));
}
$raw_xactions = $request->getStr('xactions');
if ($raw_xactions) {
$raw_xactions = phutil_json_decode($raw_xactions);
} else {
$raw_xactions = array();
}
if (!$raw_xactions) {
throw new Exception(
pht(
'You have not chosen any edits to apply.'));
}
$edit_engine = id($this->newEditEngine())
->setViewer($viewer);
$xactions = $edit_engine->newRawBulkTransactions($raw_xactions);
$cancel_uri = $this->getCancelURI();
$done_uri = $this->getDoneURI();
$job = PhabricatorWorkerBulkJob::initializeNewJob(
$viewer,
new PhabricatorEditEngineBulkJobType(),
array(
'objectPHIDs' => mpull($objects, 'getPHID'),
'xactions' => $xactions,
'cancelURI' => $cancel_uri,
'doneURI' => $done_uri,
));
$type_status = PhabricatorWorkerBulkJobTransaction::TYPE_STATUS;
$xactions = array();
$xactions[] = id(new PhabricatorWorkerBulkJobTransaction())
->setTransactionType($type_status)
->setNewValue(PhabricatorWorkerBulkJob::STATUS_CONFIRM);
$editor = id(new PhabricatorWorkerBulkJobEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnMissingFields(true)
->applyTransactions($job, $xactions);
return id(new AphrontRedirectResponse())
->setURI($job->getMonitorURI());
}
private function getRootFormID() {
if (!$this->rootFormID) {
$this->rootFormID = celerity_generate_unique_node_id();
}
return $this->rootFormID;
}
}
diff --git a/src/applications/transactions/bulk/management/PhabricatorBulkManagementExportWorkflow.php b/src/applications/transactions/bulk/management/PhabricatorBulkManagementExportWorkflow.php
new file mode 100644
index 000000000..cd0ed346f
--- /dev/null
+++ b/src/applications/transactions/bulk/management/PhabricatorBulkManagementExportWorkflow.php
@@ -0,0 +1,168 @@
+<?php
+
+final class PhabricatorBulkManagementExportWorkflow
+ extends PhabricatorBulkManagementWorkflow {
+
+ protected function didConstruct() {
+ $this
+ ->setName('export')
+ ->setExamples('**export** [options]')
+ ->setSynopsis(
+ pht('Export data to a flat file (JSON, CSV, Excel, etc).'))
+ ->setArguments(
+ array(
+ array(
+ 'name' => 'class',
+ 'param' => 'class',
+ 'help' => pht(
+ 'SearchEngine class to export data from.'),
+ ),
+ array(
+ 'name' => 'format',
+ 'param' => 'format',
+ 'help' => pht('Export format.'),
+ ),
+ array(
+ 'name' => 'query',
+ 'param' => 'key',
+ 'help' => pht(
+ 'Export the data selected by this query.'),
+ ),
+ array(
+ 'name' => 'output',
+ 'param' => 'path',
+ 'help' => pht(
+ 'Write output to a file. If omitted, output will be sent to '.
+ 'stdout.'),
+ ),
+ array(
+ 'name' => 'overwrite',
+ 'help' => pht(
+ 'If the output file already exists, overwrite it instead of '.
+ 'raising an error.'),
+ ),
+ ));
+ }
+
+ public function execute(PhutilArgumentParser $args) {
+ $viewer = $this->getViewer();
+
+ $class = $args->getArg('class');
+
+ if (!strlen($class)) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Specify a search engine class to export data from with '.
+ '"--class".'));
+ }
+
+ if (!is_subclass_of($class, 'PhabricatorApplicationSearchEngine')) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'SearchEngine class ("%s") is unknown.',
+ $class));
+ }
+
+ $engine = newv($class, array())
+ ->setViewer($viewer);
+
+ if (!$engine->canExport()) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'SearchEngine class ("%s") does not support data export.',
+ $class));
+ }
+
+ $query_key = $args->getArg('query');
+ if (!strlen($query_key)) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Specify a query to export with "--query".'));
+ }
+
+ if ($engine->isBuiltinQuery($query_key)) {
+ $saved_query = $engine->buildSavedQueryFromBuiltin($query_key);
+ } else if ($query_key) {
+ $saved_query = id(new PhabricatorSavedQueryQuery())
+ ->setViewer($viewer)
+ ->withQueryKeys(array($query_key))
+ ->executeOne();
+ } else {
+ $saved_query = null;
+ }
+
+ if (!$saved_query) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Failed to load saved query ("%s").',
+ $query_key));
+ }
+
+ $format_key = $args->getArg('format');
+ if (!strlen($format_key)) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Specify an export format with "--format".'));
+ }
+
+ $all_formats = PhabricatorExportFormat::getAllExportFormats();
+ $format = idx($all_formats, $format_key);
+ if (!$format) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Unknown export format ("%s"). Known formats are: %s.',
+ $format_key,
+ implode(', ', array_keys($all_formats))));
+ }
+
+ if (!$format->isExportFormatEnabled()) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Export format ("%s") is not enabled.',
+ $format_key));
+ }
+
+ $is_overwrite = $args->getArg('overwrite');
+ $output_path = $args->getArg('output');
+
+ if (!strlen($output_path) && $is_overwrite) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Flag "--overwrite" has no effect without "--output".'));
+ }
+
+ if (!$is_overwrite) {
+ if (Filesystem::pathExists($output_path)) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Output path already exists. Use "--overwrite" to overwrite '.
+ 'it.'));
+ }
+ }
+
+ $export_engine = id(new PhabricatorExportEngine())
+ ->setViewer($viewer)
+ ->setTitle(pht('Export'))
+ ->setFilename(pht('export'))
+ ->setSearchEngine($engine)
+ ->setSavedQuery($saved_query)
+ ->setExportFormat($format);
+
+ $file = $export_engine->exportFile();
+
+ $iterator = $file->getFileDataIterator();
+
+ if (strlen($output_path)) {
+ foreach ($iterator as $chunk) {
+ Filesystem::appendFile($output_path, $chunk);
+ }
+ } else {
+ foreach ($iterator as $chunk) {
+ echo $chunk;
+ }
+ }
+
+ return 0;
+ }
+
+}
diff --git a/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php b/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php
index 43b94874b..66285547b 100644
--- a/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php
+++ b/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php
@@ -1,216 +1,230 @@
<?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);
- $xactions = $xaction_query
+ $xaction_query
->withObjectPHIDs(array($object->getPHID()))
- ->setViewer($viewer)
- ->executeWithCursorPager($pager);
+ ->setViewer($viewer);
+
+ $constraints = $request->getValue('constraints', array());
+ PhutilTypeSpec::checkMap(
+ $constraints,
+ array(
+ 'phids' => 'optional list<string>',
+ ));
+
+ $with_phids = idx($constraints, 'phids');
+ if ($with_phids) {
+ $xaction_query->withPHIDs($with_phids);
+ }
+
+ $xactions = $xaction_query->executeWithCursorPager($pager);
if ($xactions) {
$template = head($xactions)->getApplicationTransactionCommentObject();
$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();
}
$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;
}
}
$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 fa3941a61..4089187ca 100644
--- a/src/applications/transactions/constants/PhabricatorTransactions.php
+++ b/src/applications/transactions/constants/PhabricatorTransactions.php
@@ -1,41 +1,40 @@
<?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_BUILDABLE = 'harbormaster:buildable';
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 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/edges/PhabricatorMutedByEdgeType.php b/src/applications/transactions/edges/PhabricatorMutedByEdgeType.php
new file mode 100644
index 000000000..1f592239b
--- /dev/null
+++ b/src/applications/transactions/edges/PhabricatorMutedByEdgeType.php
@@ -0,0 +1,16 @@
+<?php
+
+final class PhabricatorMutedByEdgeType
+ extends PhabricatorEdgeType {
+
+ const EDGECONST = 68;
+
+ public function getInverseEdgeConstant() {
+ return PhabricatorMutedEdgeType::EDGECONST;
+ }
+
+ public function shouldWriteInverseTransactions() {
+ return true;
+ }
+
+}
diff --git a/src/applications/transactions/edges/PhabricatorMutedEdgeType.php b/src/applications/transactions/edges/PhabricatorMutedEdgeType.php
new file mode 100644
index 000000000..f473e0c61
--- /dev/null
+++ b/src/applications/transactions/edges/PhabricatorMutedEdgeType.php
@@ -0,0 +1,16 @@
+<?php
+
+final class PhabricatorMutedEdgeType
+ extends PhabricatorEdgeType {
+
+ const EDGECONST = 67;
+
+ public function getInverseEdgeConstant() {
+ return PhabricatorMutedByEdgeType::EDGECONST;
+ }
+
+ public function shouldWriteInverseTransactions() {
+ return true;
+ }
+
+}
diff --git a/src/applications/transactions/editengine/PhabricatorEditEngine.php b/src/applications/transactions/editengine/PhabricatorEditEngine.php
index 77f0894d0..331bfbf6c 100644
--- a/src/applications/transactions/editengine/PhabricatorEditEngine.php
+++ b/src/applications/transactions/editengine/PhabricatorEditEngine.php
@@ -1,2659 +1,2610 @@
<?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();
if ($this->supportsEditEngineConfiguration()) {
$engine_key = $this->getEngineKey();
$config = $this->getEditEngineConfiguration();
$can_manage = PhabricatorPolicyFilter::hasCapability(
$this->getViewer(),
$config,
PhabricatorPolicyCapability::CAN_EDIT);
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;
}
- /**
- * Test if the viewer could apply a certain type of change by using the
- * normal "Edit" form.
- *
- * This method returns `true` if the user has access to an edit form and
- * that edit form has a field which applied the specified transaction type,
- * and that field is visible and editable for the user.
- *
- * For example, you can use it to test if a user is able to reassign tasks
- * or not, prior to rendering dedicated UI for task reassignment.
- *
- * Note that this method does NOT test if the user can actually edit the
- * current object, just if they have access to the related field.
- *
- * @param const Transaction type to test for.
- * @return bool True if the user could "Edit" to apply the transaction type.
- */
- final public function hasEditAccessToTransaction($xaction_type) {
- $viewer = $this->getViewer();
-
- $object = $this->getTargetObject();
- if (!$object) {
- $object = $this->newEditableObject();
- }
-
- $config = $this->loadDefaultEditConfiguration($object);
- if (!$config) {
- return false;
- }
-
- $fields = $this->buildEditFields($object);
-
- $field = null;
- foreach ($fields as $form_field) {
- $field_xaction_type = $form_field->getTransactionType();
- if ($field_xaction_type === $xaction_type) {
- $field = $form_field;
- break;
- }
- }
-
- if (!$field) {
- return false;
- }
-
- if (!$field->shouldReadValueFromSubmit()) {
- return false;
- }
-
- return true;
- }
-
-
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())
->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(),
- $this->loadDraftVersion($object));
+ $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);
} 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' => $object->getID(),
+ '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/PhabricatorPHIDListEditField.php b/src/applications/transactions/editfield/PhabricatorPHIDListEditField.php
index b084c142e..d2b647b9a 100644
--- a/src/applications/transactions/editfield/PhabricatorPHIDListEditField.php
+++ b/src/applications/transactions/editfield/PhabricatorPHIDListEditField.php
@@ -1,148 +1,160 @@
<?php
abstract class PhabricatorPHIDListEditField
extends PhabricatorEditField {
private $useEdgeTransactions;
private $isSingleValue;
+ private $isNullable;
public function setUseEdgeTransactions($use_edge_transactions) {
$this->useEdgeTransactions = $use_edge_transactions;
return $this;
}
public function getUseEdgeTransactions() {
return $this->useEdgeTransactions;
}
public function setSingleValue($value) {
if ($value === null) {
$value = array();
} else {
$value = array($value);
}
$this->isSingleValue = true;
return $this->setValue($value);
}
public function getIsSingleValue() {
return $this->isSingleValue;
}
+ public function setIsNullable($is_nullable) {
+ $this->isNullable = $is_nullable;
+ return $this;
+ }
+
+ public function getIsNullable() {
+ return $this->isNullable;
+ }
+
protected function newHTTPParameterType() {
return new AphrontPHIDListHTTPParameterType();
}
protected function newConduitParameterType() {
if ($this->getIsSingleValue()) {
- return new ConduitPHIDParameterType();
+ return id(new ConduitPHIDParameterType())
+ ->setIsNullable($this->getIsNullable());
} else {
return new ConduitPHIDListParameterType();
}
}
protected function getValueFromRequest(AphrontRequest $request, $key) {
$value = parent::getValueFromRequest($request, $key);
if ($this->getIsSingleValue()) {
$value = array_slice($value, 0, 1);
}
return $value;
}
public function getValueForTransaction() {
$new = parent::getValueForTransaction();
if ($this->getIsSingleValue()) {
if ($new) {
return head($new);
} else {
return null;
}
}
if (!$this->getUseEdgeTransactions()) {
return $new;
}
$old = $this->getInitialValue();
if ($old === null) {
return array(
'=' => array_fuse($new),
);
}
// If we're building an edge transaction and the request has data about the
// original value the user saw when they loaded the form, interpret the
// edit as a mixture of "+" and "-" operations instead of a single "="
// operation. This limits our exposure to race conditions by making most
// concurrent edits merge correctly.
$add = array_diff($new, $old);
$rem = array_diff($old, $new);
$value = array();
if ($add) {
$value['+'] = array_fuse($add);
}
if ($rem) {
$value['-'] = array_fuse($rem);
}
return $value;
}
protected function newEditType() {
if ($this->getUseEdgeTransactions()) {
return new PhabricatorEdgeEditType();
}
return id(new PhabricatorDatasourceEditType())
- ->setIsSingleValue($this->getIsSingleValue());
+ ->setIsSingleValue($this->getIsSingleValue())
+ ->setIsNullable($this->getIsNullable());
}
protected function newBulkEditTypes() {
return $this->newConduitEditTypes();
}
protected function newConduitEditTypes() {
if (!$this->getUseEdgeTransactions()) {
return parent::newConduitEditTypes();
}
$transaction_type = $this->getTransactionType();
if ($transaction_type === null) {
return array();
}
$type_key = $this->getEditTypeKey();
$base = $this->getEditType();
$add = id(clone $base)
->setEditType($type_key.'.add')
->setEdgeOperation('+')
->setConduitTypeDescription(pht('List of PHIDs to add.'))
->setConduitParameterType($this->getConduitParameterType());
$rem = id(clone $base)
->setEditType($type_key.'.remove')
->setEdgeOperation('-')
->setConduitTypeDescription(pht('List of PHIDs to remove.'))
->setConduitParameterType($this->getConduitParameterType());
$set = id(clone $base)
->setEditType($type_key.'.set')
->setEdgeOperation('=')
->setConduitTypeDescription(pht('List of PHIDs to set.'))
->setConduitParameterType($this->getConduitParameterType());
return array(
$add,
$rem,
$set,
);
}
}
diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
index 155592fc4..70644b197 100644
--- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
+++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
@@ -1,3953 +1,4331 @@
<?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();
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;
}
- protected function getMentionedPHIDs() {
+ 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;
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 HarbormasterBuildableInterface) {
- $types[] = PhabricatorTransactions::TYPE_BUILDABLE;
- }
-
if ($this->object instanceof PhabricatorTokenReceiverInterface) {
$types[] = PhabricatorTransactions::TYPE_TOKEN;
}
if ($this->object instanceof PhabricatorProjectInterface ||
$this->object instanceof PhabricatorMentionableInterface) {
$types[] = PhabricatorTransactions::TYPE_EDGE;
}
if ($this->object instanceof PhabricatorSpacesInterface) {
$types[] = PhabricatorTransactions::TYPE_SPACE;
}
$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:
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_BUILDABLE:
case PhabricatorTransactions::TYPE_TOKEN:
case PhabricatorTransactions::TYPE_INLINESTATE:
case PhabricatorTransactions::TYPE_SUBTYPE:
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:
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_SUBTYPE:
- case PhabricatorTransactions::TYPE_BUILDABLE:
case PhabricatorTransactions::TYPE_TOKEN:
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_JOIN_POLICY:
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
case PhabricatorTransactions::TYPE_INLINESTATE:
case PhabricatorTransactions::TYPE_EDGE:
case PhabricatorTransactions::TYPE_SPACE:
case PhabricatorTransactions::TYPE_COMMENT:
return $this->applyBuiltinInternalTransaction($object, $xaction);
}
return $this->applyCustomInternalTransaction($object, $xaction);
}
private function applyExternalEffects(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$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_SUBTYPE:
case PhabricatorTransactions::TYPE_EDGE:
- case PhabricatorTransactions::TYPE_BUILDABLE:
case PhabricatorTransactions::TYPE_TOKEN:
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_JOIN_POLICY:
case PhabricatorTransactions::TYPE_INLINESTATE:
case PhabricatorTransactions::TYPE_SPACE:
case PhabricatorTransactions::TYPE_COMMENT:
return $this->applyBuiltinExternalTransaction($object, $xaction);
}
return $this->applyCustomExternalTransaction($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
throw new Exception(
pht(
"Transaction type '%s' is missing an internal apply implementation!",
$type));
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
throw new Exception(
pht(
"Transaction type '%s' is missing an external apply implementation!",
$type));
}
/**
* @{class:PhabricatorTransactions} provides many built-in transactions
* which should not require much - if any - code in specific applications.
*
* This method is a hook for the exceedingly-rare cases where you may need
* to do **additional** work for built-in transactions. Developers should
* extend this method, making sure to return the parent implementation
* regardless of handling any transactions.
*
* See also @{method:applyBuiltinExternalTransaction}.
*/
protected function applyBuiltinInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_VIEW_POLICY:
$object->setViewPolicy($xaction->getNewValue());
break;
case PhabricatorTransactions::TYPE_EDIT_POLICY:
$object->setEditPolicy($xaction->getNewValue());
break;
case PhabricatorTransactions::TYPE_JOIN_POLICY:
$object->setJoinPolicy($xaction->getNewValue());
break;
case PhabricatorTransactions::TYPE_SPACE:
$object->setSpacePHID($xaction->getNewValue());
break;
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;
}
}
/**
* 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()) {
- foreach ($xactions as $xaction) {
+ $this->buildOldRecipientLists($object, $xactions);
- // If any of the transactions require a read lock, hold one and
- // reload the object. We need to do this fairly early so that the
- // call to `adjustTransactionValues()` (which populates old values)
- // is based on the synchronized state of the object, which may differ
- // from the state when it was originally loaded.
-
- if ($this->shouldReadLock($object, $xaction)) {
- $object->openTransaction();
- $object->beginReadLocking();
- $transaction_open = true;
- $read_locking = true;
- $object->reload();
- break;
- }
- }
+ $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;
}
}
}
- if ($this->shouldApplyInitialEffects($object, $xactions)) {
- $this->applyInitialEffects($object, $xactions);
- }
+ try {
+ if ($this->shouldApplyInitialEffects($object, $xactions)) {
+ $this->applyInitialEffects($object, $xactions);
+ }
- foreach ($xactions as $xaction) {
- $this->adjustTransactionValues($object, $xaction);
- }
+ foreach ($xactions as $xaction) {
+ $this->adjustTransactionValues($object, $xaction);
+ }
- try {
$xactions = $this->filterTransactions($object, $xactions);
- } catch (Exception $ex) {
- if ($read_locking) {
- $object->endReadLocking();
- }
- if ($transaction_open) {
- $object->killTransaction();
+
+ // 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;
+ }
}
- throw $ex;
- }
- // 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);
+ }
}
- }
- if ($mark_as_create) {
+ // Now that we've merged, filtered, and combined transactions, check for
+ // required capabilities.
foreach ($xactions as $xaction) {
- $xaction->setIsCreateTransaction(true);
+ $this->requireCapabilities($object, $xaction);
}
- }
-
- // 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);
+ $xactions = $this->sortTransactions($xactions);
+ $file_phids = $this->extractFilePHIDs($object, $xactions);
- if ($is_preview) {
- $this->loadHandles($xactions);
- return $xactions;
- }
+ if ($is_preview) {
+ $this->loadHandles($xactions);
+ return $xactions;
+ }
- $comment_editor = id(new PhabricatorApplicationTransactionCommentEditor())
- ->setActor($actor)
- ->setActingAsPHID($this->getActingAsPHID())
- ->setContentSource($this->getContentSource());
+ $comment_editor = id(new PhabricatorApplicationTransactionCommentEditor())
+ ->setActor($actor)
+ ->setActingAsPHID($this->getActingAsPHID())
+ ->setContentSource($this->getContentSource());
- if (!$transaction_open) {
- $object->openTransaction();
- }
+ if (!$transaction_open) {
+ $object->openTransaction();
+ $transaction_open = true;
+ }
- try {
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;
}
- $object->saveTransaction();
+ if ($transaction_open) {
+ $object->saveTransaction();
+ $transaction_open = false;
+ }
} catch (Exception $ex) {
- $object->killTransaction();
+ 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()) {
- // If we're applying inverse edge transactions, don't trigger Herald.
- // From a product perspective, the current set of inverse edges (most
- // often, mentions) aren't things users would expect to trigger Herald.
- // From a technical perspective, objects loaded by the inverse editor may
- // not have enough data to execute rules. At least for now, just stop
- // Herald from executing when applying inverse edges.
+ // 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);
}
// 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;
}
-
- /**
- * Determine if the editor should hold a read lock on the object while
- * applying a transaction.
- *
- * If the editor does not hold a lock, two editors may read an object at the
- * same time, then apply their changes without any synchronization. For most
- * transactions, this does not matter much. However, it is important for some
- * transactions. For example, if an object has a transaction count on it, both
- * editors may read the object with `count = 23`, then independently update it
- * and save the object with `count = 24` twice. This will produce the wrong
- * state: the object really has 25 transactions, but the count is only 24.
- *
- * Generally, transactions fall into one of four buckets:
- *
- * - Append operations: Actions like adding a comment to an object purely
- * add information to its state, and do not depend on the current object
- * state in any way. These transactions never need to hold locks.
- * - Overwrite operations: Actions like changing the title or description
- * of an object replace the current value with a new value, so the end
- * state is consistent without a lock. We currently do not lock these
- * transactions, although we may in the future.
- * - Edge operations: Edge and subscription operations have internal
- * synchronization which limits the damage race conditions can cause.
- * We do not currently lock these transactions, although we may in the
- * future.
- * - Update operations: Actions like incrementing a count on an object.
- * These operations generally should use locks, unless it is not
- * important that the state remain consistent in the presence of races.
- *
- * @param PhabricatorLiskDAO Object being updated.
- * @param PhabricatorApplicationTransaction Transaction being applied.
- * @return bool True to synchronize the edit with a lock.
- */
- protected function shouldReadLock(
- PhabricatorLiskDAO $object,
- PhabricatorApplicationTransaction $xaction) {
- return false;
- }
-
private function loadHandles(array $xactions) {
$phids = array();
foreach ($xactions as $key => $xaction) {
$phids[$key] = $xaction->getRequiredHandlePHIDs();
}
$handles = array();
$merged = array_mergev($phids);
if ($merged) {
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->requireActor())
->withPHIDs($merged)
->execute();
}
foreach ($xactions as $key => $xaction) {
$xaction->setHandles(array_select_keys($handles, $phids[$key]));
}
}
private function loadSubscribers(PhabricatorLiskDAO $object) {
if ($object->getPHID() &&
($object instanceof PhabricatorSubscribableInterface)) {
$subs = PhabricatorSubscribersQuery::loadSubscribersForPHID(
$object->getPHID());
$this->subscribers = array_fuse($subs);
} else {
$this->subscribers = array();
}
}
private function validateEditParameters(
PhabricatorLiskDAO $object,
array $xactions) {
if (!$this->getContentSource()) {
throw new PhutilInvalidStateException('setContentSource');
}
// Do a bunch of sanity checks that the incoming transactions are fresh.
// They should be unsaved and have only "transactionType" and "newValue"
// set.
$types = array_fill_keys($this->getTransactionTypes(), true);
assert_instances_of($xactions, 'PhabricatorApplicationTransaction');
foreach ($xactions as $xaction) {
if ($xaction->getPHID() || $xaction->getID()) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht('You can not apply transactions which already have IDs/PHIDs!'));
}
if ($xaction->getObjectPHID()) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
'You can not apply transactions which already have %s!',
'objectPHIDs'));
}
if ($xaction->getCommentPHID()) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
'You can not apply transactions which already have %s!',
'commentPHIDs'));
}
if ($xaction->getCommentVersion() !== 0) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
'You can not apply transactions which already have '.
'commentVersions!'));
}
$expect_value = !$xaction->shouldGenerateOldValue();
$has_value = $xaction->hasOldValue();
if ($expect_value && !$has_value) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
'This transaction is supposed to have an %s set, but it does not!',
'oldValue'));
}
if ($has_value && !$expect_value) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
'This transaction should generate its %s automatically, '.
'but has already had one set!',
'oldValue'));
}
$type = $xaction->getTransactionType();
if (empty($types[$type])) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
'Transaction has type "%s", but that transaction type is not '.
'supported by this editor (%s).',
$type,
get_class($this)));
}
}
}
protected function requireCapabilities(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
if ($this->getIsNewObject()) {
return;
}
$actor = $this->requireActor();
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
PhabricatorPolicyFilter::requireCapability(
$actor,
$object,
PhabricatorPolicyCapability::CAN_VIEW);
break;
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_JOIN_POLICY:
case PhabricatorTransactions::TYPE_SPACE:
PhabricatorPolicyFilter::requireCapability(
$actor,
$object,
PhabricatorPolicyCapability::CAN_EDIT);
break;
}
}
private function buildSubscribeTransaction(
PhabricatorLiskDAO $object,
array $xactions,
array $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();
+ }
+
$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 = new PhabricatorMetaMTAMailBody();
- $body->setViewer($this->requireActor());
+ $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) {
$headers = array();
$headers_html = array();
$comments = array();
$details = array();
foreach ($xactions as $xaction) {
if ($xaction->shouldHideForMail($xactions)) {
continue;
}
$header = $xaction->getTitleForMail();
if ($header !== null) {
$headers[] = $header;
}
$header_html = $xaction->getTitleForHTMLMail();
if ($header_html !== null) {
$headers_html[] = $header_html;
}
$comment = $xaction->getBodyForMail();
if ($comment !== null) {
$comments[] = $comment;
}
if ($xaction->hasChangeDetailsForMail()) {
$details[] = $xaction;
}
}
$headers_text = implode("\n", $headers);
$body->addRawPlaintextSection($headers_text);
$headers_html = phutil_implode_html(phutil_tag('br'), $headers_html);
$header_button = null;
if ($object_label !== null) {
$button_style = array(
'text-decoration: none;',
'padding: 4px 8px;',
'margin: 0 8px 8px;',
'float: right;',
'color: #464C5C;',
'font-weight: bold;',
'border-radius: 3px;',
'background-color: #F7F7F9;',
'background-image: linear-gradient(to bottom,#fff,#f1f0f1);',
'display: inline-block;',
'border: 1px solid rgba(71,87,120,.2);',
);
$header_button = phutil_tag(
'a',
array(
'style' => implode(' ', $button_style),
'href' => $object_href,
),
$object_label);
}
$xactions_style = array(
);
$header_action = phutil_tag(
'td',
array(),
$header_button);
$header_action = phutil_tag(
'td',
array(
'style' => implode(' ', $xactions_style),
),
array(
$headers_html,
// Add an extra newline to prevent the "View Object" button from
// running into the transaction text in Mail.app text snippet
// previews.
"\n",
));
$headers_html = phutil_tag(
'table',
array(),
phutil_tag('tr', array(), array($header_action, $header_button)));
$body->addRawHTMLSection($headers_html);
foreach ($comments as $comment) {
$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(
- $adapter->getHarbormasterBuildablePHID(),
+ $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)
->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',
);
}
/**
* 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;
+ }
+
}
diff --git a/src/applications/transactions/edittype/PhabricatorPHIDListEditType.php b/src/applications/transactions/edittype/PhabricatorPHIDListEditType.php
index eb728a680..763f6ff00 100644
--- a/src/applications/transactions/edittype/PhabricatorPHIDListEditType.php
+++ b/src/applications/transactions/edittype/PhabricatorPHIDListEditType.php
@@ -1,58 +1,73 @@
<?php
abstract class PhabricatorPHIDListEditType
extends PhabricatorEditType {
private $datasource;
private $isSingleValue;
private $defaultValue;
+ private $isNullable;
public function setDatasource(PhabricatorTypeaheadDatasource $datasource) {
$this->datasource = $datasource;
return $this;
}
public function getDatasource() {
return $this->datasource;
}
public function setIsSingleValue($is_single_value) {
$this->isSingleValue = $is_single_value;
return $this;
}
public function getIsSingleValue() {
return $this->isSingleValue;
}
public function setDefaultValue(array $default_value) {
$this->defaultValue = $default_value;
return $this;
}
- public function getDefaultValue() {
- return $this->defaultValue;
+ public function setIsNullable($is_nullable) {
+ $this->isNullable = $is_nullable;
+ return $this;
}
- public function getValueType() {
- if ($this->getIsSingleValue()) {
- return 'phid';
- } else {
- return 'list<phid>';
- }
+ public function getIsNullable() {
+ return $this->isNullable;
+ }
+
+ public function getDefaultValue() {
+ return $this->defaultValue;
}
protected function newConduitParameterType() {
$default = parent::newConduitParameterType();
if ($default) {
return $default;
}
if ($this->getIsSingleValue()) {
- return new ConduitPHIDParameterType();
+ return id(new ConduitPHIDParameterType())
+ ->setIsNullable($this->getIsNullable());
} else {
return new ConduitPHIDListParameterType();
}
}
+ public function getTransactionValueFromBulkEdit($value) {
+ if (!$this->getIsSingleValue()) {
+ return $value;
+ }
+
+ if ($value) {
+ return head($value);
+ }
+
+ return null;
+ }
+
}
diff --git a/src/applications/transactions/engineextension/PhabricatorApplicationObjectMailEngineExtension.php b/src/applications/transactions/engineextension/PhabricatorApplicationObjectMailEngineExtension.php
new file mode 100644
index 000000000..bf441df71
--- /dev/null
+++ b/src/applications/transactions/engineextension/PhabricatorApplicationObjectMailEngineExtension.php
@@ -0,0 +1,92 @@
+<?php
+
+final class PhabricatorApplicationObjectMailEngineExtension
+ extends PhabricatorMailEngineExtension {
+
+ const EXTENSIONKEY = 'application/object';
+
+ public function supportsObject($object) {
+ return true;
+ }
+
+ public function newMailStampTemplates($object) {
+ $templates = array(
+ id(new PhabricatorStringMailStamp())
+ ->setKey('application')
+ ->setLabel(pht('Application')),
+ );
+
+ if ($this->hasMonogram($object)) {
+ $templates[] = id(new PhabricatorStringMailStamp())
+ ->setKey('monogram')
+ ->setLabel(pht('Object Monogram'));
+ }
+
+ if ($this->hasPHID($object)) {
+ // This is a PHID, but we always want to render it as a raw string, so
+ // use a string mail stamp.
+ $templates[] = id(new PhabricatorStringMailStamp())
+ ->setKey('phid')
+ ->setLabel(pht('Object PHID'));
+
+ $templates[] = id(new PhabricatorStringMailStamp())
+ ->setKey('object-type')
+ ->setLabel(pht('Object Type'));
+ }
+
+ return $templates;
+ }
+
+ public function newMailStamps($object, array $xactions) {
+ $editor = $this->getEditor();
+ $viewer = $this->getViewer();
+
+ $application = null;
+ $class = $editor->getEditorApplicationClass();
+ if (PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) {
+ $application = newv($class, array());
+ }
+
+ if ($application) {
+ $application_name = $application->getName();
+ $this->getMailStamp('application')
+ ->setValue($application_name);
+ }
+
+ if ($this->hasMonogram($object)) {
+ $monogram = $object->getMonogram();
+ $this->getMailStamp('monogram')
+ ->setValue($monogram);
+ }
+
+ if ($this->hasPHID($object)) {
+ $object_phid = $object->getPHID();
+
+ $this->getMailStamp('phid')
+ ->setValue($object_phid);
+
+ $phid_type = phid_get_type($object_phid);
+ if ($phid_type != PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
+ $this->getMailStamp('object-type')
+ ->setValue($phid_type);
+ }
+ }
+ }
+
+ private function hasPHID($object) {
+ if (!($object instanceof LiskDAO)) {
+ return false;
+ }
+
+ if (!$object->getConfigOption(LiskDAO::CONFIG_AUX_PHID)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private function hasMonogram($object) {
+ return method_exists($object, 'getMonogram');
+ }
+
+}
diff --git a/src/applications/transactions/engineextension/PhabricatorEditorMailEngineExtension.php b/src/applications/transactions/engineextension/PhabricatorEditorMailEngineExtension.php
new file mode 100644
index 000000000..536589442
--- /dev/null
+++ b/src/applications/transactions/engineextension/PhabricatorEditorMailEngineExtension.php
@@ -0,0 +1,81 @@
+<?php
+
+final class PhabricatorEditorMailEngineExtension
+ extends PhabricatorMailEngineExtension {
+
+ const EXTENSIONKEY = 'editor';
+
+ public function supportsObject($object) {
+ return true;
+ }
+
+ public function newMailStampTemplates($object) {
+ $templates = array();
+
+ $templates[] = id(new PhabricatorPHIDMailStamp())
+ ->setKey('actor')
+ ->setLabel(pht('Acting User'));
+
+ $templates[] = id(new PhabricatorStringMailStamp())
+ ->setKey('via')
+ ->setLabel(pht('Via Content Source'));
+
+ $templates[] = id(new PhabricatorBoolMailStamp())
+ ->setKey('silent')
+ ->setLabel(pht('Silent Edit'));
+
+ $templates[] = id(new PhabricatorBoolMailStamp())
+ ->setKey('encrypted')
+ ->setLabel(pht('Encryption Required'));
+
+ $templates[] = id(new PhabricatorBoolMailStamp())
+ ->setKey('new')
+ ->setLabel(pht('New Object'));
+
+ $templates[] = id(new PhabricatorPHIDMailStamp())
+ ->setKey('mention')
+ ->setLabel(pht('Mentioned User'));
+
+ $templates[] = id(new PhabricatorStringMailStamp())
+ ->setKey('herald')
+ ->setLabel(pht('Herald Rule'));
+
+ $templates[] = id(new PhabricatorPHIDMailStamp())
+ ->setKey('removed')
+ ->setLabel(pht('Recipient Removed'));
+
+ return $templates;
+ }
+
+ public function newMailStamps($object, array $xactions) {
+ $editor = $this->getEditor();
+ $viewer = $this->getViewer();
+
+ $this->getMailStamp('actor')
+ ->setValue($editor->getActingAsPHID());
+
+ $content_source = $editor->getContentSource();
+ $this->getMailStamp('via')
+ ->setValue($content_source->getSourceTypeConstant());
+
+ $this->getMailStamp('silent')
+ ->setValue($editor->getIsSilent());
+
+ $this->getMailStamp('encrypted')
+ ->setValue($editor->getMustEncrypt());
+
+ $this->getMailStamp('new')
+ ->setValue($editor->getIsNewObject());
+
+ $mentioned_phids = $editor->getMentionedPHIDs();
+ $this->getMailStamp('mention')
+ ->setValue($mentioned_phids);
+
+ $this->getMailStamp('herald')
+ ->setValue($editor->getHeraldRuleMonograms());
+
+ $this->getMailStamp('removed')
+ ->setValue($editor->getRemovedRecipientPHIDs());
+ }
+
+}
diff --git a/src/applications/transactions/exception/PhabricatorApplicationTransactionWarningException.php b/src/applications/transactions/exception/PhabricatorApplicationTransactionWarningException.php
new file mode 100644
index 000000000..e57707154
--- /dev/null
+++ b/src/applications/transactions/exception/PhabricatorApplicationTransactionWarningException.php
@@ -0,0 +1,13 @@
+<?php
+
+final class PhabricatorApplicationTransactionWarningException
+ extends Exception {
+
+ private $xactions;
+
+ public function __construct(array $xactions) {
+ $this->xactions = $xactions;
+ parent::__construct();
+ }
+
+}
diff --git a/src/applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php b/src/applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php
index 6cc18d518..305e38ab0 100644
--- a/src/applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php
+++ b/src/applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php
@@ -1,171 +1,202 @@
<?php
/**
* @concrete-extensible
*/
class PhabricatorApplicationTransactionFeedStory
extends PhabricatorFeedStory {
+ private $primaryTransactionPHID;
+
public function getPrimaryObjectPHID() {
return $this->getValue('objectPHID');
}
public function getRequiredObjectPHIDs() {
return $this->getValue('transactionPHIDs');
}
public function getRequiredHandlePHIDs() {
$phids = array();
$phids[] = $this->getValue('objectPHID');
foreach ($this->getValue('transactionPHIDs') as $xaction_phid) {
$xaction = $this->getObject($xaction_phid);
foreach ($xaction->getRequiredHandlePHIDs() as $handle_phid) {
$phids[] = $handle_phid;
}
}
return $phids;
}
protected function getPrimaryTransactionPHID() {
- return head($this->getValue('transactionPHIDs'));
+ if ($this->primaryTransactionPHID === null) {
+ // Transactions are filtered and sorted before they're stored, but the
+ // rendering logic can change between the time an edit occurs and when
+ // we actually render the story. Recalculate the filtering at display
+ // time because it's cheap and gets us better results when things change
+ // by letting the changes apply retroactively.
+
+ $xaction_phids = $this->getValue('transactionPHIDs');
+
+ $xactions = array();
+ foreach ($xaction_phids as $xaction_phid) {
+ $xactions[] = $this->getObject($xaction_phid);
+ }
+
+ foreach ($xactions as $key => $xaction) {
+ if ($xaction->shouldHideForFeed()) {
+ unset($xactions[$key]);
+ }
+ }
+
+ if ($xactions) {
+ $primary_phid = head($xactions)->getPHID();
+ } else {
+ $primary_phid = head($xaction_phids);
+ }
+
+ $this->primaryTransactionPHID = $primary_phid;
+ }
+
+ return $this->primaryTransactionPHID;
}
public function getPrimaryTransaction() {
return $this->getObject($this->getPrimaryTransactionPHID());
}
public function getFieldStoryMarkupFields() {
$xaction_phids = $this->getValue('transactionPHIDs');
$fields = array();
foreach ($xaction_phids as $xaction_phid) {
$xaction = $this->getObject($xaction_phid);
foreach ($xaction->getMarkupFieldsForFeed($this) as $field) {
$fields[] = $field;
}
}
return $fields;
}
public function getMarkupText($field) {
$xaction_phids = $this->getValue('transactionPHIDs');
foreach ($xaction_phids as $xaction_phid) {
$xaction = $this->getObject($xaction_phid);
foreach ($xaction->getMarkupFieldsForFeed($this) as $xaction_field) {
if ($xaction_field == $field) {
return $xaction->getMarkupTextForFeed($this, $field);
}
}
}
return null;
}
public function renderView() {
$view = $this->newStoryView();
$handle = $this->getHandle($this->getPrimaryObjectPHID());
$view->setHref($handle->getURI());
$type = phid_get_type($handle->getPHID());
$phid_types = PhabricatorPHIDType::getAllTypes();
$icon = null;
if (!empty($phid_types[$type])) {
$phid_type = $phid_types[$type];
$class = $phid_type->getPHIDTypeApplicationClass();
if ($class) {
$application = PhabricatorApplication::getByClass($class);
$icon = $application->getIcon();
}
}
$view->setAppIcon($icon);
$xaction_phids = $this->getValue('transactionPHIDs');
$xaction = $this->getPrimaryTransaction();
$xaction->setHandles($this->getHandles());
$view->setTitle($xaction->getTitleForFeed());
foreach ($xaction_phids as $xaction_phid) {
$secondary_xaction = $this->getObject($xaction_phid);
$secondary_xaction->setHandles($this->getHandles());
$body = $secondary_xaction->getBodyForFeed($this);
if (nonempty($body)) {
$view->appendChild($body);
}
}
$author_phid = $xaction->getAuthorPHID();
$author_handle = $this->getHandle($author_phid);
$author_image = $author_handle->getImageURI();
if ($author_image) {
$view->setImage($author_image);
$view->setImageHref($author_handle->getURI());
} else {
$view->setAuthorIcon($author_handle->getIcon());
}
return $view;
}
public function renderText() {
$xaction = $this->getPrimaryTransaction();
$old_target = $xaction->getRenderingTarget();
$new_target = PhabricatorApplicationTransaction::TARGET_TEXT;
$xaction->setRenderingTarget($new_target);
$xaction->setHandles($this->getHandles());
$text = $xaction->getTitleForFeed();
$xaction->setRenderingTarget($old_target);
return $text;
}
public function renderTextBody() {
$all_bodies = '';
$new_target = PhabricatorApplicationTransaction::TARGET_TEXT;
$xaction_phids = $this->getValue('transactionPHIDs');
foreach ($xaction_phids as $xaction_phid) {
$secondary_xaction = $this->getObject($xaction_phid);
$old_target = $secondary_xaction->getRenderingTarget();
$secondary_xaction->setRenderingTarget($new_target);
$secondary_xaction->setHandles($this->getHandles());
$body = $secondary_xaction->getBodyForMail();
if (nonempty($body)) {
$all_bodies .= $body."\n";
}
$secondary_xaction->setRenderingTarget($old_target);
}
return trim($all_bodies);
}
public function getImageURI() {
$author_phid = $this->getPrimaryTransaction()->getAuthorPHID();
return $this->getHandle($author_phid)->getImageURI();
}
public function getURI() {
$handle = $this->getHandle($this->getPrimaryObjectPHID());
return PhabricatorEnv::getProductionURI($handle->getURI());
}
public function renderAsTextForDoorkeeper(
DoorkeeperFeedStoryPublisher $publisher) {
$xactions = array();
$xaction_phids = $this->getValue('transactionPHIDs');
foreach ($xaction_phids as $xaction_phid) {
$xaction = $this->getObject($xaction_phid);
$xaction->setHandles($this->getHandles());
$xactions[] = $xaction;
}
$primary = $this->getPrimaryTransaction();
return $primary->renderAsTextForDoorkeeper($publisher, $this, $xactions);
}
}
diff --git a/src/applications/transactions/response/PhabricatorApplicationTransactionWarningResponse.php b/src/applications/transactions/response/PhabricatorApplicationTransactionWarningResponse.php
new file mode 100644
index 000000000..21b3ee61c
--- /dev/null
+++ b/src/applications/transactions/response/PhabricatorApplicationTransactionWarningResponse.php
@@ -0,0 +1,58 @@
+<?php
+
+final class PhabricatorApplicationTransactionWarningResponse
+ extends AphrontProxyResponse {
+
+ private $viewer;
+ private $exception;
+ private $cancelURI;
+
+ public function setCancelURI($cancel_uri) {
+ $this->cancelURI = $cancel_uri;
+ return $this;
+ }
+
+ public function setException(
+ PhabricatorApplicationTransactionWarningException $exception) {
+ $this->exception = $exception;
+ return $this;
+ }
+
+ protected function buildProxy() {
+ return new AphrontDialogResponse();
+ }
+
+ public function reduceProxyResponse() {
+ $request = $this->getRequest();
+
+ $title = pht('Warning: Unexpected Effects');
+
+ $head = pht(
+ 'This is a draft revision that will not publish any notifications '.
+ 'until the author requests review.');
+ $tail = pht(
+ 'Mentioned or subscribed users will not be notified.');
+
+ $continue = pht('Tell No One');
+
+ $dialog = id(new AphrontDialogView())
+ ->setViewer($request->getViewer())
+ ->setTitle($title);
+
+ $dialog->appendParagraph($head);
+ $dialog->appendParagraph($tail);
+
+ $passthrough = $request->getPassthroughRequestParameters();
+ foreach ($passthrough as $key => $value) {
+ $dialog->addHiddenInput($key, $value);
+ }
+
+ $dialog
+ ->addHiddenInput('editEngine.warnings', 1)
+ ->addSubmitButton($continue)
+ ->addCancelButton($this->cancelURI);
+
+ return $this->getProxy()->setDialog($dialog);
+ }
+
+}
diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php
index 97b57009e..69d11fe75 100644
--- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php
+++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php
@@ -1,1702 +1,1628 @@
<?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_BUILDABLE:
case PhabricatorTransactions::TYPE_TOKEN:
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
case PhabricatorTransactions::TYPE_INLINESTATE:
return false;
}
return true;
}
abstract public function getApplicationTransactionType();
private function getApplicationObjectTypeName() {
$types = PhabricatorPHIDType::getAllTypes();
$type = idx($types, $this->getApplicationTransactionType());
if ($type) {
return $type->getTypeName();
}
return pht('Object');
}
public function getApplicationTransactionCommentObject() {
throw new PhutilMethodNotImplementedException();
}
public function getApplicationTransactionViewObject() {
return new PhabricatorApplicationTransactionView();
}
public function getMetadataValue($key, $default = null) {
return idx($this->metadata, $key, $default);
}
public function setMetadataValue($key, $value) {
$this->metadata[$key] = $value;
return $this;
}
public function generatePHID() {
$type = PhabricatorApplicationTransactionTransactionPHIDType::TYPECONST;
$subtype = $this->getApplicationTransactionType();
return PhabricatorPHID::generateNewPHID($type, $subtype);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'oldValue' => self::SERIALIZATION_JSON,
'newValue' => self::SERIALIZATION_JSON,
'metadata' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'commentPHID' => 'phid?',
'commentVersion' => 'uint32',
'contentSource' => 'text',
'transactionType' => 'text32',
),
self::CONFIG_KEY_SCHEMA => array(
'key_object' => array(
'columns' => array('objectPHID'),
),
),
) + parent::getConfiguration();
}
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source->serialize();
return $this;
}
public function getContentSource() {
return PhabricatorContentSource::newFromSerialized($this->contentSource);
}
public function hasComment() {
return $this->getComment() && strlen($this->getComment()->getContent());
}
public function getComment() {
if ($this->commentNotLoaded) {
throw new Exception(pht('Comment for this transaction was not loaded.'));
}
return $this->comment;
}
public function 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;
- case PhabricatorTransactions::TYPE_BUILDABLE:
- $phid = $this->getMetadataValue('harbormaster:buildablePHID');
- if ($phid) {
- $phids[] = array($phid);
- }
- break;
}
if ($this->getComment()) {
$phids[] = array($this->getComment()->getAuthorPHID());
}
return array_mergev($phids);
}
public function setHandles(array $handles) {
$this->handles = $handles;
return $this;
}
public function getHandle($phid) {
if (empty($this->handles[$phid])) {
throw new Exception(
pht(
'Transaction ("%s", of type "%s") requires a handle ("%s") that it '.
'did not load.',
$this->getPHID(),
$this->getTransactionType(),
$phid));
}
return $this->handles[$phid];
}
public function getHandleIfExists($phid) {
return idx($this->handles, $phid);
}
public function getHandles() {
if ($this->handles === null) {
throw new Exception(
pht('Transaction requires handles and it did not load them.'));
}
return $this->handles;
}
public function renderHandleLink($phid) {
if ($this->renderingTarget == self::TARGET_HTML) {
return $this->getHandle($phid)->renderLink();
} else {
return $this->getHandle($phid)->getLinkName();
}
}
public function renderHandleList(array $phids) {
$links = array();
foreach ($phids as $phid) {
$links[] = $this->renderHandleLink($phid);
}
if ($this->renderingTarget == self::TARGET_HTML) {
return phutil_implode_html(', ', $links);
} else {
return implode(', ', $links);
}
}
private function renderSubscriberList(array $phids, $change_type) {
if ($this->getRenderingTarget() == self::TARGET_TEXT) {
return $this->renderHandleList($phids);
} else {
$handles = array_select_keys($this->getHandles(), $phids);
return id(new SubscriptionListStringBuilder())
->setHandles($handles)
->setObjectPHID($this->getPHID())
->buildTransactionString($change_type);
}
}
protected function renderPolicyName($phid, $state = 'old') {
$policy = PhabricatorPolicy::newFromPolicyAndHandle(
$phid,
$this->getHandleIfExists($phid));
if ($this->renderingTarget == self::TARGET_HTML) {
switch ($policy->getType()) {
case PhabricatorPolicyType::TYPE_CUSTOM:
$policy->setHref('/transactions/'.$state.'/'.$this->getPHID().'/');
$policy->setWorkflow(true);
break;
default:
break;
}
$output = $policy->renderDescription();
} else {
$output = hsprintf('%s', $policy->getFullName());
}
return $output;
}
public function getIcon() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
$comment = $this->getComment();
if ($comment && $comment->getIsRemoved()) {
return 'fa-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_BUILDABLE:
- return 'fa-wrench';
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_BUILDABLE:
- switch ($this->getNewValue()) {
- case HarbormasterBuildable::STATUS_PASSED:
- return 'green';
- case HarbormasterBuildable::STATUS_FAILED:
- return 'red';
+ 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;
}
// Hide creation transactions if the old value is empty. These are
// transactions like "alice set the task tile to: ...", which are
// essentially never interesting.
if ($this->getIsCreateTransaction()) {
switch ($this->getTransactionType()) {
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_BUILDABLE:
- switch ($this->getNewValue()) {
- case HarbormasterBuildable::STATUS_FAILED:
- // For now, only ever send mail when builds fail. We might let
- // you customize this later, but in most cases this is probably
- // completely uninteresting.
- return false;
- }
- return true;
case PhabricatorTransactions::TYPE_EDGE:
$edge_type = $this->getMetadataValue('edge:type');
switch ($edge_type) {
case PhabricatorObjectMentionsObjectEdgeType::EDGECONST:
case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST:
return true;
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_BUILDABLE:
- switch ($this->getNewValue()) {
- case HarbormasterBuildable::STATUS_FAILED:
- // For now, don't notify on build passes either. These are pretty
- // high volume and annoying, with very little present value. We
- // might want to turn them back on in the specific case of
- // build successes on the current document?
- return false;
- }
- return true;
case PhabricatorTransactions::TYPE_EDGE:
$edge_type = $this->getMetadataValue('edge:type');
switch ($edge_type) {
case PhabricatorObjectMentionsObjectEdgeType::EDGECONST:
case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST:
return true;
break;
default:
break;
}
break;
case PhabricatorTransactions::TYPE_INLINESTATE:
return true;
}
return $this->shouldHide();
}
public function getTitleForMail() {
return id(clone $this)->setRenderingTarget('text')->getTitle();
}
public function 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_BUILDABLE:
- switch ($this->getNewValue()) {
- case HarbormasterBuildable::STATUS_BUILDING:
- return pht(
- '%s started building %s.',
- $this->renderHandleLink($author_phid),
- $this->renderHandleLink(
- $this->getMetadataValue('harbormaster:buildablePHID')));
- case HarbormasterBuildable::STATUS_PASSED:
- return pht(
- '%s completed building %s.',
- $this->renderHandleLink($author_phid),
- $this->renderHandleLink(
- $this->getMetadataValue('harbormaster:buildablePHID')));
- case HarbormasterBuildable::STATUS_FAILED:
- return pht(
- '%s failed to build %s!',
- $this->renderHandleLink($author_phid),
- $this->renderHandleLink(
- $this->getMetadataValue('harbormaster:buildablePHID')));
- default:
- return null;
- }
-
case PhabricatorTransactions::TYPE_INLINESTATE:
$done = 0;
$undone = 0;
foreach ($new as $phid => $state) {
if ($state == PhabricatorInlineCommentInterface::STATE_DONE) {
$done++;
} else {
$undone++;
}
}
if ($done && $undone) {
return pht(
'%s marked %s inline comment(s) as done and %s inline comment(s) '.
'as not done.',
$this->renderHandleLink($author_phid),
new PhutilNumber($done),
new PhutilNumber($undone));
} else if ($done) {
return pht(
'%s marked %s inline comment(s) as done.',
$this->renderHandleLink($author_phid),
new PhutilNumber($done));
} else {
return pht(
'%s marked %s inline comment(s) as not done.',
$this->renderHandleLink($author_phid),
new PhutilNumber($undone));
}
break;
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_BUILDABLE:
- switch ($this->getNewValue()) {
- case HarbormasterBuildable::STATUS_BUILDING:
- return pht(
- '%s started building %s for %s.',
- $this->renderHandleLink($author_phid),
- $this->renderHandleLink(
- $this->getMetadataValue('harbormaster:buildablePHID')),
- $this->renderHandleLink($object_phid));
- case HarbormasterBuildable::STATUS_PASSED:
- return pht(
- '%s completed building %s for %s.',
- $this->renderHandleLink($author_phid),
- $this->renderHandleLink(
- $this->getMetadataValue('harbormaster:buildablePHID')),
- $this->renderHandleLink($object_phid));
- case HarbormasterBuildable::STATUS_FAILED:
- return pht(
- '%s failed to build %s for %s.',
- $this->renderHandleLink($author_phid),
- $this->renderHandleLink(
- $this->getMetadataValue('harbormaster:buildablePHID')),
- $this->renderHandleLink($object_phid));
- default:
- return null;
- }
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');
- case PhabricatorTransactions::TYPE_BUILDABLE:
- switch ($this->getNewValue()) {
- case HarbormasterBuildable::STATUS_PASSED:
- return pht('Build Passed');
- case HarbormasterBuildable::STATUS_FAILED:
- return pht('Build Failed');
- default:
- return pht('Build Status');
- }
default:
return pht('Updated');
}
}
public function getMailTags() {
return array();
}
public function hasChangeDetails() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getTransactionCustomField();
if ($field) {
return $field->getApplicationTransactionHasChangeDetails($this);
}
break;
}
return false;
}
public function 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/PhabricatorModularTransaction.php b/src/applications/transactions/storage/PhabricatorModularTransaction.php
index 9f3c24b94..eef4b8e5d 100644
--- a/src/applications/transactions/storage/PhabricatorModularTransaction.php
+++ b/src/applications/transactions/storage/PhabricatorModularTransaction.php
@@ -1,186 +1,202 @@
<?php
// TODO: Some "final" modifiers have been VERY TEMPORARILY moved aside to
// allow DifferentialTransaction to extend this class without converting
// fully to ModularTransactions.
abstract class PhabricatorModularTransaction
extends PhabricatorApplicationTransaction {
private $implementation;
abstract public function getBaseTransactionClass();
public function getModularType() {
return $this->getTransactionImplementation();
}
final protected function getTransactionImplementation() {
if (!$this->implementation) {
$this->implementation = $this->newTransactionImplementation();
}
return $this->implementation;
}
public function newModularTransactionTypes() {
$base_class = $this->getBaseTransactionClass();
$types = id(new PhutilClassMapQuery())
->setAncestorClass($base_class)
->setUniqueMethod('getTransactionTypeConstant')
->execute();
// Add core transaction types.
$types += id(new PhutilClassMapQuery())
->setAncestorClass('PhabricatorCoreTransactionType')
->setUniqueMethod('getTransactionTypeConstant')
->execute();
return $types;
}
private function newTransactionImplementation() {
$types = $this->newModularTransactionTypes();
$key = $this->getTransactionType();
if (empty($types[$key])) {
$type = $this->newFallbackModularTransactionType();
} else {
$type = clone $types[$key];
}
$type->setStorage($this);
return $type;
}
protected function newFallbackModularTransactionType() {
return new PhabricatorCoreVoidTransaction();
}
final public function generateOldValue($object) {
return $this->getTransactionImplementation()->generateOldValue($object);
}
final public function generateNewValue($object) {
return $this->getTransactionImplementation()
->generateNewValue($object, $this->getNewValue());
}
final public function willApplyTransactions($object, array $xactions) {
return $this->getTransactionImplementation()
->willApplyTransactions($object, $xactions);
}
final public function applyInternalEffects($object) {
return $this->getTransactionImplementation()
->applyInternalEffects($object);
}
final public function applyExternalEffects($object) {
return $this->getTransactionImplementation()
->applyExternalEffects($object);
}
/* final */ public function shouldHide() {
if ($this->getTransactionImplementation()->shouldHide()) {
return true;
}
return parent::shouldHide();
}
+ final public function shouldHideForFeed() {
+ if ($this->getTransactionImplementation()->shouldHideForFeed()) {
+ return true;
+ }
+
+ return parent::shouldHideForFeed();
+ }
+
+ /* final */ public function shouldHideForMail(array $xactions) {
+ if ($this->getTransactionImplementation()->shouldHideForMail()) {
+ return true;
+ }
+
+ return parent::shouldHideForMail($xactions);
+ }
+
/* final */ public function getIcon() {
$icon = $this->getTransactionImplementation()->getIcon();
if ($icon !== null) {
return $icon;
}
return parent::getIcon();
}
/* final */ public function getTitle() {
$title = $this->getTransactionImplementation()->getTitle();
if ($title !== null) {
return $title;
}
return parent::getTitle();
}
/* final */ public function getActionName() {
$action = $this->getTransactionImplementation()->getActionName();
if ($action !== null) {
return $action;
}
return parent::getActionName();
}
/* final */ public function getActionStrength() {
$strength = $this->getTransactionImplementation()->getActionStrength();
if ($strength !== null) {
return $strength;
}
return parent::getActionStrength();
}
public function getTitleForMail() {
$old_target = $this->getRenderingTarget();
$new_target = self::TARGET_TEXT;
$this->setRenderingTarget($new_target);
$title = $this->getTitle();
$this->setRenderingTarget($old_target);
return $title;
}
/* final */ public function getTitleForFeed() {
$title = $this->getTransactionImplementation()->getTitleForFeed();
if ($title !== null) {
return $title;
}
return parent::getTitleForFeed();
}
/* final */ public function getColor() {
$color = $this->getTransactionImplementation()->getColor();
if ($color !== null) {
return $color;
}
return parent::getColor();
}
public function attachViewer(PhabricatorUser $viewer) {
$this->getTransactionImplementation()->setViewer($viewer);
return parent::attachViewer($viewer);
}
- /* final */ public function hasChangeDetails() {
+ final public function hasChangeDetails() {
if ($this->getTransactionImplementation()->hasChangeDetailView()) {
return true;
}
return parent::hasChangeDetails();
}
- /* final */ public function renderChangeDetails(PhabricatorUser $viewer) {
+ final public function renderChangeDetails(PhabricatorUser $viewer) {
$impl = $this->getTransactionImplementation();
$impl->setViewer($viewer);
$view = $impl->newChangeDetailView();
if ($view !== null) {
return $view;
}
return parent::renderChangeDetails($viewer);
}
final protected function newRemarkupChanges() {
return $this->getTransactionImplementation()->newRemarkupChanges();
}
}
diff --git a/src/applications/transactions/storage/PhabricatorModularTransactionType.php b/src/applications/transactions/storage/PhabricatorModularTransactionType.php
index f2b59c8a2..eaf7d029c 100644
--- a/src/applications/transactions/storage/PhabricatorModularTransactionType.php
+++ b/src/applications/transactions/storage/PhabricatorModularTransactionType.php
@@ -1,351 +1,359 @@
<?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();
}
}
diff --git a/src/applications/typeahead/controller/PhabricatorTypeaheadFunctionHelpController.php b/src/applications/typeahead/controller/PhabricatorTypeaheadFunctionHelpController.php
index 3084b2d43..27b30a627 100644
--- a/src/applications/typeahead/controller/PhabricatorTypeaheadFunctionHelpController.php
+++ b/src/applications/typeahead/controller/PhabricatorTypeaheadFunctionHelpController.php
@@ -1,143 +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();
}
- $source = $sources[$class];
+ $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();
+ }
- $application_class = $source->getDatasourceApplicationClass();
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())
->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/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php b/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php
index 2f55c2638..e0ba9a763 100644
--- a/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php
+++ b/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php
@@ -1,418 +1,424 @@
<?php
final class PhabricatorTypeaheadModularDatasourceController
extends PhabricatorTypeaheadDatasourceController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$request = $this->getRequest();
$viewer = $request->getUser();
$query = $request->getStr('q');
$offset = $request->getInt('offset');
$select_phid = null;
$is_browse = ($request->getURIData('action') == 'browse');
$select = $request->getStr('select');
if ($select) {
$select = phutil_json_decode($select);
$query = idx($select, 'q');
$offset = idx($select, 'offset');
$select_phid = idx($select, 'phid');
}
// Default this to the query string to make debugging a little bit easier.
$raw_query = nonempty($request->getStr('raw'), $query);
// This makes form submission easier in the debug view.
$class = nonempty($request->getURIData('class'), $request->getStr('class'));
$sources = id(new PhutilClassMapQuery())
->setAncestorClass('PhabricatorTypeaheadDatasource')
->execute();
if (isset($sources[$class])) {
$source = $sources[$class];
$source->setParameters($request->getRequestData());
$source->setViewer($viewer);
// NOTE: Wrapping the source in a Composite datasource ensures we perform
// application visibility checks for the viewer, so we do not need to do
// those separately.
$composite = new PhabricatorTypeaheadRuntimeCompositeDatasource();
$composite->addDatasource($source);
$hard_limit = 1000;
$limit = 100;
$composite
->setViewer($viewer)
->setQuery($query)
->setRawQuery($raw_query)
->setLimit($limit + 1);
if ($is_browse) {
if (!$composite->isBrowsable()) {
return new Aphront404Response();
}
if (($offset + $limit) >= $hard_limit) {
// Offset-based paging is intrinsically slow; hard-cap how far we're
// willing to go with it.
return new Aphront404Response();
}
$composite
->setOffset($offset)
->setIsBrowse(true);
}
$results = $composite->loadResults();
if ($is_browse) {
// If this is a request for a specific token after the user clicks
// "Select", return the token in wire format so it can be added to
// the tokenizer.
if ($select_phid !== null) {
$map = mpull($results, null, 'getPHID');
$token = idx($map, $select_phid);
if (!$token) {
return new Aphront404Response();
}
$payload = array(
'key' => $token->getPHID(),
'token' => $token->getWireFormat(),
);
return id(new AphrontAjaxResponse())->setContent($payload);
}
$format = $request->getStr('format');
switch ($format) {
case 'html':
case 'dialog':
// These are the acceptable response formats.
break;
default:
// Return a dialog if format information is missing or invalid.
$format = 'dialog';
break;
}
$next_link = null;
if (count($results) > $limit) {
$results = array_slice($results, 0, $limit, $preserve_keys = true);
if (($offset + (2 * $limit)) < $hard_limit) {
$next_uri = id(new PhutilURI($request->getRequestURI()))
->setQueryParam('offset', $offset + $limit)
->setQueryParam('q', $query)
->setQueryParam('raw', $raw_query)
->setQueryParam('format', 'html');
$next_link = javelin_tag(
'a',
array(
'href' => $next_uri,
'class' => 'typeahead-browse-more',
'sigil' => 'typeahead-browse-more',
'mustcapture' => true,
),
pht('More Results'));
} else {
// If the user has paged through more than 1K results, don't
// offer to page any further.
$next_link = javelin_tag(
'div',
array(
'class' => 'typeahead-browse-hard-limit',
),
pht('You reach the edge of the abyss.'));
}
}
$exclude = $request->getStrList('exclude');
$exclude = array_fuse($exclude);
$select = array(
'offset' => $offset,
'q' => $query,
);
$items = array();
foreach ($results as $result) {
// Disable already-selected tokens.
$disabled = isset($exclude[$result->getPHID()]);
$value = $select + array('phid' => $result->getPHID());
$value = json_encode($value);
$button = phutil_tag(
'button',
array(
'class' => 'small grey',
'name' => 'select',
'value' => $value,
'disabled' => $disabled ? 'disabled' : null,
),
pht('Select'));
$information = $this->renderBrowseResult($result, $button);
$items[] = phutil_tag(
'div',
array(
'class' => 'typeahead-browse-item grouped',
),
$information);
}
$markup = array(
$items,
$next_link,
);
if ($format == 'html') {
$content = array(
'markup' => hsprintf('%s', $markup),
);
return id(new AphrontAjaxResponse())->setContent($content);
}
$this->requireResource('typeahead-browse-css');
$this->initBehavior('typeahead-browse');
$input_id = celerity_generate_unique_node_id();
$frame_id = celerity_generate_unique_node_id();
$config = array(
'inputID' => $input_id,
'frameID' => $frame_id,
'uri' => (string)$request->getRequestURI(),
);
$this->initBehavior('typeahead-search', $config);
$search = javelin_tag(
'input',
array(
'type' => 'text',
'id' => $input_id,
'class' => 'typeahead-browse-input',
'autocomplete' => 'off',
'placeholder' => $source->getPlaceholderText(),
));
$frame = phutil_tag(
'div',
array(
'class' => 'typeahead-browse-frame',
'id' => $frame_id,
),
$markup);
$browser = array(
phutil_tag(
'div',
array(
'class' => 'typeahead-browse-header',
),
$search),
$frame,
);
$function_help = null;
if ($source->getAllDatasourceFunctions()) {
$reference_uri = '/typeahead/help/'.get_class($source).'/';
+ $parameters = $source->getParameters();
+ if ($parameters) {
+ $reference_uri = (string)id(new PhutilURI($reference_uri))
+ ->setQueryParam('parameters', phutil_json_encode($parameters));
+ }
+
$reference_link = phutil_tag(
'a',
array(
'href' => $reference_uri,
'target' => '_blank',
),
pht('Reference: Advanced Functions'));
$function_help = array(
id(new PHUIIconView())
->setIcon('fa-book'),
' ',
$reference_link,
);
}
return $this->newDialog()
->setWidth(AphrontDialogView::WIDTH_FORM)
->setRenderDialogAsDiv(true)
->setTitle($source->getBrowseTitle())
->appendChild($browser)
->setResizeX(true)
->setResizeY($frame_id)
->addFooter($function_help)
->addCancelButton('/', pht('Close'));
}
} else if ($is_browse) {
return new Aphront404Response();
} else {
$results = array();
}
$content = mpull($results, 'getWireFormat');
$content = array_values($content);
if ($request->isAjax()) {
return id(new AphrontAjaxResponse())->setContent($content);
}
// If there's a non-Ajax request to this endpoint, show results in a tabular
// format to make it easier to debug typeahead output.
foreach ($sources as $key => $source) {
// This can happen with composite or generic sources.
if (!$source->getDatasourceApplicationClass()) {
continue;
}
if (!PhabricatorApplication::isClassInstalledForViewer(
$source->getDatasourceApplicationClass(),
$viewer)) {
unset($sources[$key]);
}
}
$options = array_fuse(array_keys($sources));
asort($options);
$form = id(new AphrontFormView())
->setUser($viewer)
->setAction('/typeahead/class/')
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Source Class'))
->setName('class')
->setValue($class)
->setOptions($options))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Query'))
->setName('q')
->setValue($request->getStr('q')))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Raw Query'))
->setName('raw')
->setValue($request->getStr('raw')))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Query')));
$form_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Token Query'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setForm($form);
// Make "\n" delimiters more visible.
foreach ($content as $key => $row) {
$content[$key][0] = str_replace("\n", '<\n>', $row[0]);
}
$table = new AphrontTableView($content);
$table->setHeaders(
array(
pht('Name'),
pht('URI'),
pht('PHID'),
pht('Priority'),
pht('Display Name'),
pht('Display Type'),
pht('Image URI'),
pht('Priority Type'),
pht('Icon'),
pht('Closed'),
pht('Sprite'),
pht('Color'),
pht('Type'),
pht('Unique'),
pht('Auto'),
pht('Phase'),
));
$result_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Token Results (%s)', $class))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($table);
$title = pht('Typeahead Results');
$header = id(new PHUIHeaderView())
->setHeader($title);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter(array(
$form_box,
$result_box,
));
return $this->newPage()
->setTitle($title)
->appendChild($view);
}
private function renderBrowseResult(
PhabricatorTypeaheadResult $result,
$button) {
$class = array();
$style = array();
$separator = " \xC2\xB7 ";
$class[] = 'phabricator-main-search-typeahead-result';
$name = phutil_tag(
'div',
array(
'class' => 'result-name',
),
$result->getDisplayName());
$icon = $result->getIcon();
$icon = id(new PHUIIconView())->setIcon($icon);
$attributes = $result->getAttributes();
$attributes = phutil_implode_html($separator, $attributes);
$attributes = array($icon, ' ', $attributes);
$closed = $result->getClosed();
if ($closed) {
$class[] = 'result-closed';
$attributes = array($closed, $separator, $attributes);
}
$attributes = phutil_tag(
'div',
array(
'class' => 'result-type',
),
$attributes);
$image = $result->getImageURI();
if ($image) {
$style[] = 'background-image: url('.$image.');';
$class[] = 'has-image';
}
return phutil_tag(
'div',
array(
'class' => implode(' ', $class),
'style' => implode(' ', $style),
),
array(
$button,
$name,
$attributes,
));
}
}
diff --git a/src/applications/typeahead/datasource/PhabricatorTypeaheadCompositeDatasource.php b/src/applications/typeahead/datasource/PhabricatorTypeaheadCompositeDatasource.php
index 33f06e4ae..5594044c4 100644
--- a/src/applications/typeahead/datasource/PhabricatorTypeaheadCompositeDatasource.php
+++ b/src/applications/typeahead/datasource/PhabricatorTypeaheadCompositeDatasource.php
@@ -1,320 +1,329 @@
<?php
abstract class PhabricatorTypeaheadCompositeDatasource
extends PhabricatorTypeaheadDatasource {
private $usable;
private $prefixString;
private $prefixLength;
abstract public function getComponentDatasources();
public function isBrowsable() {
foreach ($this->getUsableDatasources() as $datasource) {
if (!$datasource->isBrowsable()) {
return false;
}
}
return parent::isBrowsable();
}
public function getDatasourceApplicationClass() {
return null;
}
public function loadResults() {
$phases = array();
// We only need to do a prefix phase query if there's an actual query
// string. If the user didn't type anything, nothing can possibly match it.
if (strlen($this->getRawQuery())) {
$phases[] = self::PHASE_PREFIX;
}
$phases[] = self::PHASE_CONTENT;
$offset = $this->getOffset();
$limit = $this->getLimit();
$results = array();
foreach ($phases as $phase) {
if ($limit) {
$phase_limit = ($offset + $limit) - count($results);
} else {
$phase_limit = 0;
}
$phase_results = $this->loadResultsForPhase(
$phase,
$phase_limit);
foreach ($phase_results as $result) {
$results[] = $result;
}
if ($limit) {
if (count($results) >= $offset + $limit) {
break;
}
}
}
return $results;
}
protected function loadResultsForPhase($phase, $limit) {
if ($phase == self::PHASE_PREFIX) {
$this->prefixString = $this->getPrefixQuery();
$this->prefixLength = strlen($this->prefixString);
}
// If the input query is a function like `members(platy`, and we can
// parse the function, we strip the function off and hand the stripped
// query to child sources. This makes it easier to implement function
// sources in terms of real object sources.
$raw_query = $this->getRawQuery();
$is_function = false;
if (self::isFunctionToken($raw_query)) {
$is_function = true;
}
$stack = $this->getFunctionStack();
$is_browse = $this->getIsBrowse();
$results = array();
foreach ($this->getUsableDatasources() as $source) {
$source_stack = $stack;
$source_query = $raw_query;
if ($is_function) {
// If this source can't handle the function, skip it.
$function = $source->parseFunction($raw_query, $allow_partial = true);
if (!$function) {
continue;
}
// If this source handles the function directly, strip the function.
// Otherwise, this is something like a composite source which has
// some internal source which can evaluate the function, but will
// perform stripping later.
if ($source->shouldStripFunction($function['name'])) {
$source_query = head($function['argv']);
$source_stack[] = $function['name'];
}
}
$source
->setPhase($phase)
->setFunctionStack($source_stack)
->setRawQuery($source_query)
->setQuery($this->getQuery())
->setViewer($this->getViewer());
if ($is_browse) {
$source->setIsBrowse(true);
}
if ($limit) {
// If we are loading results from a source with a limit, it may return
// some results which belong to the wrong phase. We need an entire page
// of valid results in the correct phase AFTER any results for the
// wrong phase are filtered for pagination to work correctly.
// To make sure we can get there, we fetch more and more results until
// enough of them survive filtering to generate a full page.
// We start by fetching 150% of the results than we think we need, and
// double the amount we overfetch by each time.
$factor = 1.5;
while (true) {
$query_source = clone $source;
$total = (int)ceil($limit * $factor) + 1;
$query_source->setLimit($total);
$source_results = $query_source->loadResultsForPhase(
$phase,
$limit);
// If there are fewer unfiltered results than we asked for, we know
// this is the entire result set and we don't need to keep going.
if (count($source_results) < $total) {
$source_results = $query_source->didLoadResults($source_results);
$source_results = $this->filterPhaseResults(
$phase,
$source_results);
break;
}
// Otherwise, this result set have everything we need, or may not.
// Filter the results that are part of the wrong phase out first...
$source_results = $query_source->didLoadResults($source_results);
$source_results = $this->filterPhaseResults($phase, $source_results);
// Now check if we have enough results left. If we do, we're all set.
if (count($source_results) >= $total) {
break;
}
// We filtered out too many results to have a full page left, so we
// need to run the query again, asking for even more results. We'll
// keep doing this until we get a full page or get all of the
// results.
$factor = $factor * 2;
}
} else {
$source_results = $source->loadResults();
$source_results = $source->didLoadResults($source_results);
$source_results = $this->filterPhaseResults($phase, $source_results);
}
$results[] = $source_results;
}
$results = array_mergev($results);
$results = msort($results, 'getSortKey');
$results = $this->sliceResults($results);
return $results;
}
private function filterPhaseResults($phase, $source_results) {
foreach ($source_results as $key => $source_result) {
$result_phase = $this->getResultPhase($source_result);
if ($result_phase != $phase) {
unset($source_results[$key]);
continue;
}
$source_result->setPhase($result_phase);
}
return $source_results;
}
private function getResultPhase(PhabricatorTypeaheadResult $result) {
if ($this->prefixLength) {
$result_name = phutil_utf8_strtolower($result->getName());
if (!strncmp($result_name, $this->prefixString, $this->prefixLength)) {
return self::PHASE_PREFIX;
}
}
return self::PHASE_CONTENT;
}
protected function sliceResults(array $results) {
$offset = $this->getOffset();
$limit = $this->getLimit();
if ($offset || $limit) {
if (!$limit) {
$limit = count($results);
}
$results = array_slice($results, $offset, $limit, $preserve_keys = true);
}
return $results;
}
private function getUsableDatasources() {
if ($this->usable === null) {
+ $viewer = $this->getViewer();
+
$sources = $this->getComponentDatasources();
+ $extension_sources = id(new PhabricatorDatasourceEngine())
+ ->setViewer($viewer)
+ ->newDatasourcesForCompositeDatasource($this);
+ foreach ($extension_sources as $extension_source) {
+ $sources[] = $extension_source;
+ }
+
$usable = array();
foreach ($sources as $source) {
$application_class = $source->getDatasourceApplicationClass();
if ($application_class) {
$result = id(new PhabricatorApplicationQuery())
->setViewer($this->getViewer())
->withClasses(array($application_class))
->execute();
if (!$result) {
continue;
}
}
- $source->setViewer($this->getViewer());
+ $source->setViewer($viewer);
$usable[] = $source;
}
$this->usable = $usable;
}
return $this->usable;
}
public function getAllDatasourceFunctions() {
$results = parent::getAllDatasourceFunctions();
foreach ($this->getUsableDatasources() as $source) {
$results += $source->getAllDatasourceFunctions();
}
return $results;
}
protected function didEvaluateTokens(array $results) {
foreach ($this->getUsableDatasources() as $source) {
$results = $source->didEvaluateTokens($results);
}
return $results;
}
protected function canEvaluateFunction($function) {
foreach ($this->getUsableDatasources() as $source) {
if ($source->canEvaluateFunction($function)) {
return true;
}
}
return parent::canEvaluateFunction($function);
}
protected function evaluateValues(array $values) {
foreach ($this->getUsableDatasources() as $source) {
$values = $source->evaluateValues($values);
}
return parent::evaluateValues($values);
}
protected function evaluateFunction($function, array $argv) {
foreach ($this->getUsableDatasources() as $source) {
if ($source->canEvaluateFunction($function)) {
return $source->evaluateFunction($function, $argv);
}
}
return parent::evaluateFunction($function, $argv);
}
public function renderFunctionTokens($function, array $argv_list) {
foreach ($this->getUsableDatasources() as $source) {
if ($source->canEvaluateFunction($function)) {
return $source->renderFunctionTokens($function, $argv_list);
}
}
return parent::renderFunctionTokens($function, $argv_list);
}
protected function renderSpecialTokens(array $values) {
$result = array();
foreach ($this->getUsableDatasources() as $source) {
$special = $source->renderSpecialTokens($values);
foreach ($special as $key => $token) {
$result[$key] = $token;
unset($values[$key]);
}
if (!$values) {
break;
}
}
return $result;
}
}
diff --git a/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php b/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php
index 8b747281b..2e369a3f6 100644
--- a/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php
+++ b/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php
@@ -1,554 +1,595 @@
<?php
/**
* @task functions Token Functions
*/
abstract class PhabricatorTypeaheadDatasource extends Phobject {
private $viewer;
private $query;
private $rawQuery;
private $offset;
private $limit;
private $parameters = array();
private $functionStack = array();
private $isBrowse;
private $phase = self::PHASE_CONTENT;
const PHASE_PREFIX = 'prefix';
const PHASE_CONTENT = 'content';
public function setLimit($limit) {
$this->limit = $limit;
return $this;
}
public function getLimit() {
return $this->limit;
}
public function setOffset($offset) {
$this->offset = $offset;
return $this;
}
public function getOffset() {
return $this->offset;
}
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setRawQuery($raw_query) {
$this->rawQuery = $raw_query;
return $this;
}
public function getPrefixQuery() {
return phutil_utf8_strtolower($this->getRawQuery());
}
public function getRawQuery() {
return $this->rawQuery;
}
public function setQuery($query) {
$this->query = $query;
return $this;
}
public function getQuery() {
return $this->query;
}
public function setParameters(array $params) {
$this->parameters = $params;
return $this;
}
public function getParameters() {
return $this->parameters;
}
public function getParameter($name, $default = null) {
return idx($this->parameters, $name, $default);
}
public function setIsBrowse($is_browse) {
$this->isBrowse = $is_browse;
return $this;
}
public function getIsBrowse() {
return $this->isBrowse;
}
public function setPhase($phase) {
$this->phase = $phase;
return $this;
}
public function getPhase() {
return $this->phase;
}
public function getDatasourceURI() {
$uri = new PhutilURI('/typeahead/class/'.get_class($this).'/');
$uri->setQueryParams($this->parameters);
return (string)$uri;
}
public function getBrowseURI() {
if (!$this->isBrowsable()) {
return null;
}
$uri = new PhutilURI('/typeahead/browse/'.get_class($this).'/');
$uri->setQueryParams($this->parameters);
return (string)$uri;
}
abstract public function getPlaceholderText();
public function getBrowseTitle() {
return get_class($this);
}
abstract public function getDatasourceApplicationClass();
abstract public function loadResults();
protected function loadResultsForPhase($phase, $limit) {
// By default, sources just load all of their results in every phase and
// rely on filtering at a higher level to sequence phases correctly.
$this->setLimit($limit);
return $this->loadResults();
}
protected function didLoadResults(array $results) {
return $results;
}
public static function tokenizeString($string) {
$string = phutil_utf8_strtolower($string);
$string = trim($string);
if (!strlen($string)) {
return array();
}
// NOTE: Splitting on "(" and ")" is important for milestones.
$tokens = preg_split('/[\s\[\]\(\)-]+/u', $string);
$tokens = array_unique($tokens);
// Make sure we don't return the empty token, as this will boil down to a
// JOIN against every token.
foreach ($tokens as $key => $value) {
if (!strlen($value)) {
unset($tokens[$key]);
}
}
return array_values($tokens);
}
public function getTokens() {
return self::tokenizeString($this->getRawQuery());
}
protected function executeQuery(
PhabricatorCursorPagedPolicyAwareQuery $query) {
return $query
->setViewer($this->getViewer())
->setOffset($this->getOffset())
->setLimit($this->getLimit())
->execute();
}
/**
* Can the user browse through results from this datasource?
*
* Browsable datasources allow the user to switch from typeahead mode to
* a browse mode where they can scroll through all results.
*
* By default, datasources are browsable, but some datasources can not
* generate a meaningful result set or can't filter results on the server.
*
* @return bool
*/
public function isBrowsable() {
return true;
}
/**
* Filter a list of results, removing items which don't match the query
* tokens.
*
* This is useful for datasources which return a static list of hard-coded
* or configured results and can't easily do query filtering in a real
* query class. Instead, they can just build the entire result set and use
* this method to filter it.
*
* For datasources backed by database objects, this is often much less
* efficient than filtering at the query level.
*
* @param list<PhabricatorTypeaheadResult> List of typeahead results.
* @return list<PhabricatorTypeaheadResult> Filtered results.
*/
protected function filterResultsAgainstTokens(array $results) {
$tokens = $this->getTokens();
if (!$tokens) {
return $results;
}
$map = array();
foreach ($tokens as $token) {
$map[$token] = strlen($token);
}
foreach ($results as $key => $result) {
$rtokens = self::tokenizeString($result->getName());
// For each token in the query, we need to find a match somewhere
// in the result name.
foreach ($map as $token => $length) {
// Look for a match.
$match = false;
foreach ($rtokens as $rtoken) {
if (!strncmp($rtoken, $token, $length)) {
// This part of the result name has the query token as a prefix.
$match = true;
break;
}
}
if (!$match) {
// We didn't find a match for this query token, so throw the result
// away. Try with the next result.
unset($results[$key]);
break;
}
}
}
return $results;
}
protected function newFunctionResult() {
return id(new PhabricatorTypeaheadResult())
->setTokenType(PhabricatorTypeaheadTokenView::TYPE_FUNCTION)
->setIcon('fa-asterisk')
->addAttribute(pht('Function'));
}
public function newInvalidToken($name) {
return id(new PhabricatorTypeaheadTokenView())
->setValue($name)
->setIcon('fa-exclamation-circle')
->setTokenType(PhabricatorTypeaheadTokenView::TYPE_INVALID);
}
public function renderTokens(array $values) {
$phids = array();
$setup = array();
$tokens = array();
foreach ($values as $key => $value) {
if (!self::isFunctionToken($value)) {
$phids[$key] = $value;
} else {
$function = $this->parseFunction($value);
if ($function) {
$setup[$function['name']][$key] = $function;
} else {
$name = pht('Invalid Function: %s', $value);
$tokens[$key] = $this->newInvalidToken($name)
->setKey($value);
}
}
}
// Give special non-function tokens which are also not PHIDs (like statuses
// and priorities) an opportunity to render.
$type_unknown = PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN;
$special = array();
foreach ($values as $key => $value) {
if (phid_get_type($value) == $type_unknown) {
$special[$key] = $value;
}
}
if ($special) {
$special_tokens = $this->renderSpecialTokens($special);
foreach ($special_tokens as $key => $token) {
$tokens[$key] = $token;
unset($phids[$key]);
}
}
if ($phids) {
$handles = $this->getViewer()->loadHandles($phids);
foreach ($phids as $key => $phid) {
$handle = $handles[$phid];
$tokens[$key] = PhabricatorTypeaheadTokenView::newFromHandle($handle);
}
}
if ($setup) {
foreach ($setup as $function_name => $argv_list) {
// Render the function tokens.
$function_tokens = $this->renderFunctionTokens(
$function_name,
ipull($argv_list, 'argv'));
// Rekey the function tokens using the original array keys.
$function_tokens = array_combine(
array_keys($argv_list),
$function_tokens);
// For any functions which were invalid, set their value to the
// original input value before it was parsed.
foreach ($function_tokens as $key => $token) {
$type = $token->getTokenType();
if ($type == PhabricatorTypeaheadTokenView::TYPE_INVALID) {
$token->setKey($values[$key]);
}
}
$tokens += $function_tokens;
}
}
return array_select_keys($tokens, array_keys($values));
}
protected function renderSpecialTokens(array $values) {
return array();
}
/* -( Token Functions )---------------------------------------------------- */
/**
* @task functions
*/
public function getDatasourceFunctions() {
return array();
}
/**
* @task functions
*/
public function getAllDatasourceFunctions() {
return $this->getDatasourceFunctions();
}
/**
* @task functions
*/
protected function canEvaluateFunction($function) {
return $this->shouldStripFunction($function);
}
/**
* @task functions
*/
protected function shouldStripFunction($function) {
$functions = $this->getDatasourceFunctions();
return isset($functions[$function]);
}
/**
* @task functions
*/
protected function evaluateFunction($function, array $argv_list) {
throw new PhutilMethodNotImplementedException();
}
/**
* @task functions
*/
protected function evaluateValues(array $values) {
return $values;
}
/**
* @task functions
*/
public function evaluateTokens(array $tokens) {
$results = array();
$evaluate = array();
foreach ($tokens as $token) {
if (!self::isFunctionToken($token)) {
$results[] = $token;
} else {
// Put a placeholder in the result list so that we retain token order
// when possible. We'll overwrite this below.
$results[] = null;
$evaluate[last_key($results)] = $token;
}
}
$results = $this->evaluateValues($results);
foreach ($evaluate as $result_key => $function) {
- $function = self::parseFunction($function);
+ $function = $this->parseFunction($function);
if (!$function) {
throw new PhabricatorTypeaheadInvalidTokenException();
}
$name = $function['name'];
$argv = $function['argv'];
$evaluated_tokens = $this->evaluateFunction($name, array($argv));
if (!$evaluated_tokens) {
unset($results[$result_key]);
} else {
$is_first = true;
foreach ($evaluated_tokens as $phid) {
if ($is_first) {
$results[$result_key] = $phid;
$is_first = false;
} else {
$results[] = $phid;
}
}
}
}
$results = array_values($results);
$results = $this->didEvaluateTokens($results);
return $results;
}
/**
* @task functions
*/
protected function didEvaluateTokens(array $results) {
return $results;
}
/**
* @task functions
*/
public static function isFunctionToken($token) {
// We're looking for a "(" so that a string like "members(q" is identified
// and parsed as a function call. This allows us to start generating
// results immediately, before the user fully types out "members(quack)".
return (strpos($token, '(') !== false);
}
/**
* @task functions
*/
- public function parseFunction($token, $allow_partial = false) {
+ protected function parseFunction($token, $allow_partial = false) {
$matches = null;
if ($allow_partial) {
- $ok = preg_match('/^([^(]+)\((.*?)\)?$/', $token, $matches);
+ $ok = preg_match('/^([^(]+)\((.*?)\)?\z/', $token, $matches);
} else {
- $ok = preg_match('/^([^(]+)\((.*)\)$/', $token, $matches);
+ $ok = preg_match('/^([^(]+)\((.*)\)\z/', $token, $matches);
}
if (!$ok) {
+ if (!$allow_partial) {
+ throw new PhabricatorTypeaheadInvalidTokenException(
+ pht(
+ 'Unable to parse function and arguments for token "%s".',
+ $token));
+ }
return null;
}
$function = trim($matches[1]);
if (!$this->canEvaluateFunction($function)) {
+ if (!$allow_partial) {
+ throw new PhabricatorTypeaheadInvalidTokenException(
+ pht(
+ 'This datasource ("%s") can not evaluate the function "%s(...)".',
+ get_class($this),
+ $function));
+ }
+
return null;
}
+ // TODO: There is currently no way to quote characters in arguments, so
+ // some characters can't be argument characters. Replace this with a real
+ // parser once we get use cases.
+
+ $argv = $matches[2];
+ $argv = trim($argv);
+ if (!strlen($argv)) {
+ $argv = array();
+ } else {
+ $argv = preg_split('/,/', $matches[2]);
+ foreach ($argv as $key => $arg) {
+ $argv[$key] = trim($arg);
+ }
+ }
+
+ foreach ($argv as $key => $arg) {
+ if (self::isFunctionToken($arg)) {
+ $subfunction = $this->parseFunction($arg);
+
+ $results = $this->evaluateFunction(
+ $subfunction['name'],
+ array($subfunction['argv']));
+
+ $argv[$key] = head($results);
+ }
+ }
+
return array(
'name' => $function,
- 'argv' => array(trim($matches[2])),
+ 'argv' => $argv,
);
}
/**
* @task functions
*/
public function renderFunctionTokens($function, array $argv_list) {
throw new PhutilMethodNotImplementedException();
}
/**
* @task functions
*/
public function setFunctionStack(array $function_stack) {
$this->functionStack = $function_stack;
return $this;
}
/**
* @task functions
*/
public function getFunctionStack() {
return $this->functionStack;
}
/**
* @task functions
*/
protected function getCurrentFunction() {
return nonempty(last($this->functionStack), null);
}
protected function renderTokensFromResults(array $results, array $values) {
$tokens = array();
foreach ($values as $key => $value) {
if (empty($results[$value])) {
continue;
}
$tokens[$key] = PhabricatorTypeaheadTokenView::newFromTypeaheadResult(
$results[$value]);
}
return $tokens;
}
public function getWireTokens(array $values) {
// TODO: This is a bit hacky for now: we're sort of generating wire
// results, rendering them, then reverting them back to wire results. This
// is pretty silly. It would probably be much cleaner to make
// renderTokens() call this method instead, then render from the result
// structure.
$rendered = $this->renderTokens($values);
$tokens = array();
foreach ($rendered as $key => $render) {
$tokens[$key] = id(new PhabricatorTypeaheadResult())
->setPHID($render->getKey())
->setIcon($render->getIcon())
->setColor($render->getColor())
->setDisplayName($render->getValue())
->setTokenType($render->getTokenType());
}
return mpull($tokens, 'getWireFormat', 'getPHID');
}
}
diff --git a/src/applications/typeahead/datasource/PhabricatorTypeaheadProxyDatasource.php b/src/applications/typeahead/datasource/PhabricatorTypeaheadProxyDatasource.php
new file mode 100644
index 000000000..3feb2d054
--- /dev/null
+++ b/src/applications/typeahead/datasource/PhabricatorTypeaheadProxyDatasource.php
@@ -0,0 +1,58 @@
+<?php
+
+abstract class PhabricatorTypeaheadProxyDatasource
+ extends PhabricatorTypeaheadCompositeDatasource {
+
+ private $datasource;
+
+ public function setDatasource(PhabricatorTypeaheadDatasource $datasource) {
+ $this->datasource = $datasource;
+ $this->setParameters(
+ array(
+ 'class' => get_class($datasource),
+ 'parameters' => $datasource->getParameters(),
+ ));
+ return $this;
+ }
+
+ public function getDatasource() {
+ if (!$this->datasource) {
+ $class = $this->getParameter('class');
+
+ $parent = 'PhabricatorTypeaheadDatasource';
+ if (!is_subclass_of($class, $parent)) {
+ throw new Exception(
+ pht(
+ 'Configured datasource class "%s" must be a valid subclass of '.
+ '"%s".',
+ $class,
+ $parent));
+ }
+
+ $datasource = newv($class, array());
+ $datasource->setParameters($this->getParameter('parameters', array()));
+ $this->datasource = $datasource;
+ }
+
+ return $this->datasource;
+ }
+
+ public function getComponentDatasources() {
+ return array(
+ $this->getDatasource(),
+ );
+ }
+
+ public function getDatasourceApplicationClass() {
+ return $this->getDatasource()->getDatasourceApplicationClass();
+ }
+
+ public function getBrowseTitle() {
+ return $this->getDatasource()->getBrowseTitle();
+ }
+
+ public function getPlaceholderText() {
+ return $this->getDatasource()->getPlaceholderText();
+ }
+
+}
diff --git a/src/applications/typeahead/datasource/__tests__/PhabricatorTypeaheadDatasourceTestCase.php b/src/applications/typeahead/datasource/__tests__/PhabricatorTypeaheadDatasourceTestCase.php
index 150275a33..c58596f02 100644
--- a/src/applications/typeahead/datasource/__tests__/PhabricatorTypeaheadDatasourceTestCase.php
+++ b/src/applications/typeahead/datasource/__tests__/PhabricatorTypeaheadDatasourceTestCase.php
@@ -1,51 +1,93 @@
<?php
final class PhabricatorTypeaheadDatasourceTestCase
extends PhabricatorTestCase {
public function testTypeaheadTokenization() {
$this->assertTokenization(
'The quick brown fox',
array('the', 'quick', 'brown', 'fox'));
$this->assertTokenization(
'Quack quack QUACK',
array('quack'));
$this->assertTokenization(
'',
array());
$this->assertTokenization(
' [ - ] ',
array());
$this->assertTokenization(
'jury-rigged',
array('jury', 'rigged'));
$this->assertTokenization(
'[[ brackets ]] [-] ]-[ tie-fighters',
array('brackets', 'tie', 'fighters'));
$this->assertTokenization(
'viewer()',
array('viewer'));
$this->assertTokenization(
'Work (Done)',
array('work', 'done'));
$this->assertTokenization(
'A (B C D)',
array('a', 'b', 'c', 'd'));
}
private function assertTokenization($input, $expect) {
$this->assertEqual(
$expect,
PhabricatorTypeaheadDatasource::tokenizeString($input),
pht('Tokenization of "%s"', $input));
}
+ public function testFunctionEvaluation() {
+ $viewer = PhabricatorUser::getOmnipotentUser();
+
+ $datasource = id(new PhabricatorTypeaheadTestNumbersDatasource())
+ ->setViewer($viewer);
+
+ $constraints = $datasource->evaluateTokens(
+ array(
+ 9,
+ 'seven()',
+ 12,
+ 3,
+ ));
+
+ $this->assertEqual(
+ array(9, 7, 12, 3),
+ $constraints);
+
+ $map = array(
+ 'inc(3)' => 4,
+ 'sum(3, 4)' => 7,
+ 'inc(seven())' => 8,
+ 'inc(inc(3))' => 5,
+ 'inc(inc(seven()))' => 9,
+ 'sum(seven(), seven())' => 14,
+ 'sum(inc(seven()), inc(inc(9)))' => 19,
+ );
+
+ foreach ($map as $input => $expect) {
+ $constraints = $datasource->evaluateTokens(
+ array(
+ $input,
+ ));
+
+ $this->assertEqual(
+ array($expect),
+ $constraints,
+ pht('Constraints for input "%s".', $input));
+ }
+ }
+
+
}
diff --git a/src/applications/typeahead/datasource/__tests__/PhabricatorTypeaheadTestNumbersDatasource.php b/src/applications/typeahead/datasource/__tests__/PhabricatorTypeaheadTestNumbersDatasource.php
new file mode 100644
index 000000000..f16a64bb9
--- /dev/null
+++ b/src/applications/typeahead/datasource/__tests__/PhabricatorTypeaheadTestNumbersDatasource.php
@@ -0,0 +1,67 @@
+<?php
+
+final class PhabricatorTypeaheadTestNumbersDatasource
+ extends PhabricatorTypeaheadDatasource {
+
+ public function getBrowseTitle() {
+ return pht('Browse Numbers');
+ }
+
+ public function getPlaceholderText() {
+ return null;
+ }
+
+ public function getDatasourceApplicationClass() {
+ return 'PhabricatorPeopleApplication';
+ }
+
+ public function getDatasourceFunctions() {
+ return array(
+ 'seven' => array(),
+ 'inc' => array(),
+ 'sum' => array(),
+ );
+ }
+
+ public function loadResults() {
+ return array();
+ }
+
+ public function renderFunctionTokens($function, array $argv_list) {
+ return array();
+ }
+
+ protected function evaluateFunction($function, array $argv_list) {
+ $results = array();
+
+ foreach ($argv_list as $argv) {
+ foreach ($argv as $k => $arg) {
+ if (!is_scalar($arg) || !preg_match('/^\d+\z/', $arg)) {
+ throw new PhabricatorTypeaheadInvalidTokenException(
+ pht(
+ 'All arguments to "%s(...)" must be integers, found '.
+ '"%s" in position %d.',
+ $function,
+ (is_scalar($arg) ? $arg : gettype($arg)),
+ $k + 1));
+ }
+ $argv[$k] = (int)$arg;
+ }
+
+ switch ($function) {
+ case 'seven':
+ $results[] = 7;
+ break;
+ case 'inc':
+ $results[] = $argv[0] + 1;
+ break;
+ case 'sum':
+ $results[] = array_sum($argv);
+ break;
+ }
+ }
+
+ return $results;
+ }
+
+}
diff --git a/src/applications/typeahead/engineextension/PhabricatorMonogramDatasourceEngineExtension.php b/src/applications/typeahead/engineextension/PhabricatorMonogramDatasourceEngineExtension.php
new file mode 100644
index 000000000..ec34538dd
--- /dev/null
+++ b/src/applications/typeahead/engineextension/PhabricatorMonogramDatasourceEngineExtension.php
@@ -0,0 +1,53 @@
+<?php
+
+final class PhabricatorMonogramDatasourceEngineExtension
+ extends PhabricatorDatasourceEngineExtension {
+
+ public function newQuickSearchDatasources() {
+ return array(
+ new PhabricatorTypeaheadMonogramDatasource(),
+ );
+ }
+
+ public function newJumpURI($query) {
+ $viewer = $this->getViewer();
+
+ // These first few rules are sort of random but don't fit anywhere else
+ // today and don't feel worth adding separate extensions for.
+
+ // Send "f" to Feed.
+ if (preg_match('/^f\z/i', $query)) {
+ return '/feed/';
+ }
+
+ // Send "d" to Differential.
+ if (preg_match('/^d\z/i', $query)) {
+ return '/differential/';
+ }
+
+ // Send "t" to Maniphest.
+ if (preg_match('/^t\z/i', $query)) {
+ return '/maniphest/';
+ }
+
+ // Otherwise, if the user entered an object name, jump to that object.
+ $objects = id(new PhabricatorObjectQuery())
+ ->setViewer($viewer)
+ ->withNames(array($query))
+ ->execute();
+ if (count($objects) == 1) {
+ $object = head($objects);
+ $object_phid = $object->getPHID();
+
+ $handles = $viewer->loadHandles(array($object_phid));
+ $handle = $handles[$object_phid];
+
+ if ($handle->isComplete()) {
+ return $handle->getURI();
+ }
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/applications/typeahead/engineextension/PhabricatorMonogramQuickSearchEngineExtension.php b/src/applications/typeahead/engineextension/PhabricatorMonogramQuickSearchEngineExtension.php
deleted file mode 100644
index 078cb4468..000000000
--- a/src/applications/typeahead/engineextension/PhabricatorMonogramQuickSearchEngineExtension.php
+++ /dev/null
@@ -1,11 +0,0 @@
-<?php
-
-final class PhabricatorMonogramQuickSearchEngineExtension
- extends PhabricatorQuickSearchEngineExtension {
-
- public function newQuickSearchDatasources() {
- return array(
- new PhabricatorTypeaheadMonogramDatasource(),
- );
- }
-}
diff --git a/src/applications/uiexample/examples/PhabricatorFilesComposeAvatarExample.php b/src/applications/uiexample/examples/PhabricatorFilesComposeAvatarExample.php
deleted file mode 100644
index 57a65863d..000000000
--- a/src/applications/uiexample/examples/PhabricatorFilesComposeAvatarExample.php
+++ /dev/null
@@ -1,95 +0,0 @@
-<?php
-
-final class PhabricatorFilesComposeAvatarExample extends PhabricatorUIExample {
-
- public function getName() {
- return pht('Avatars');
- }
-
- public function getDescription() {
- return pht('Tests various color palettes and sizes.');
- }
-
- public function getCategory() {
- return pht('Technical');
- }
-
- public function renderExample() {
- $request = $this->getRequest();
- $viewer = $request->getUser();
-
- $colors = PhabricatorFilesComposeAvatarBuiltinFile::getColorMap();
- $packs = PhabricatorFilesComposeAvatarBuiltinFile::getImagePackMap();
- $builtins = PhabricatorFilesComposeAvatarBuiltinFile::getImageMap();
- $borders = PhabricatorFilesComposeAvatarBuiltinFile::getBorderMap();
-
- $images = array();
- foreach ($builtins as $builtin => $raw_file) {
- $file = PhabricatorFile::loadBuiltin($viewer, $builtin);
- $images[] = $file->getBestURI();
- }
-
- $content = array();
- shuffle($colors);
- foreach ($colors as $color) {
- shuffle($borders);
- $color_const = hexdec(trim($color, '#'));
- $border = head($borders);
- $border_color = implode(', ', $border);
-
- $styles = array();
- $styles[] = 'background-color: '.$color.';';
- $styles[] = 'display: inline-block;';
- $styles[] = 'height: 42px;';
- $styles[] = 'width: 42px;';
- $styles[] = 'border-radius: 3px;';
- $styles[] = 'border: 4px solid rgba('.$border_color.');';
-
- shuffle($images);
- $png = head($images);
-
- $image = phutil_tag(
- 'img',
- array(
- 'src' => $png,
- 'height' => 42,
- 'width' => 42,
- ));
-
- $tag = phutil_tag(
- 'div',
- array(
- 'style' => implode(' ', $styles),
- ),
- $image);
-
- $content[] = phutil_tag(
- 'div',
- array(
- 'class' => 'mlr mlb',
- 'style' => 'float: left;',
- ),
- $tag);
- }
-
- $count = new PhutilNumber(
- count($colors) * count($builtins) * count($borders));
-
- $infoview = id(new PHUIInfoView())
- ->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
- ->appendChild(pht('This installation can generate %s unique '.
- 'avatars. You can add additional image packs in '.
- 'resources/builtins/alphanumeric/.', $count));
-
- $info = phutil_tag_div('pmb', $infoview);
- $view = phutil_tag_div('ml', $content);
-
- return phutil_tag(
- 'div',
- array(),
- array(
- $info,
- $view,
- ));
- }
-}
diff --git a/src/docs/flavor/writing_reviewable_code.diviner b/src/docs/flavor/writing_reviewable_code.diviner
index 955fe6590..edff09f7d 100644
--- a/src/docs/flavor/writing_reviewable_code.diviner
+++ b/src/docs/flavor/writing_reviewable_code.diviner
@@ -1,163 +1,160 @@
@title Writing Reviewable Code
@group review
Project recommendations on how to structure changes.
This document is purely advisory. Phabricator works with a variety of revision
control strategies, and diverging from the recommendations in this document
will not impact your ability to use it for code review and source management.
= Overview =
This document describes a strategy for structuring changes used successfully at
Facebook and in Phabricator. In essence:
- Each commit should be as small as possible, but no smaller.
- The smallest a commit can be is a single cohesive idea: don't make commits
so small that they are meaningless on their own.
- There should be a one-to-one mapping between ideas and commits:
each commit should build one idea, and each idea should be implemented by
one commit.
- Turn large commits into small commits by dividing large problems into
smaller problems and solving the small problems one at a time.
- Write sensible commit messages.
= Many Small Commits =
Small, simple commits are generally better than large, complex commits. They are
easier to understand, easier to test, and easier to review. The complexity of
understanding, testing and reviewing a change often increases faster than its
size: ten 200-line changes each doing one thing are often far easier to
understand than one 2,000 line change doing ten things. Splitting a change which
does many things into smaller changes which each do only one thing can decrease
the total complexity associated with accomplishing the same goal.
Each commit should do one thing. Generally, this means that you should separate
distinct changes into different commits when developing. For example, if you're
developing a feature and run into a preexisting bug, stash or checkpoint your
change, check out a clean HEAD/tip, fix the bug in one change, and then
merge/rebase your new feature on top of your bugfix so that you have two
changes, each with one idea ("add feature x", "fix a bug in y"), not one change
with two ideas ("add feature x and fix a bug in y").
(In Git, you can do this easily with local feature branches and commands like
`git rebase`, `git rebase -i`, and `git stash`, or with merges. In Mercurial,
you can use bookmarks or the queues extension. In SVN, there are few builtin
tools, but you can use multiple working copies or treat Differential like a
stash you access with `arc patch`.)
Even changes like fixing style problems should ideally be separated: they're
accomplishing a different goal. And it is far easier to review one 300-line
change which "converts tabs to spaces" plus one 30-line change which "implements
feature z" than one 330-line change which "implements feature z and also
converts a bunch of tabs to spaces".
Similarly, break related but complex changes into smaller, simpler components.
Here's a ridiculous analogy: if you're adding a new house, don't make one
5,000-line change which adds the whole house in one fell sweep. Split it apart
into smaller steps which are each easy to understand: start with the foundation,
then build the frame, etc. If you decided to dig the foundation with a shovel or
build the frame out of cardboard, it's both easier to miss and harder to correct
if the decisions are buried in 5,000 lines of interior design and landscaping.
Do it one piece at a time, providing enough context that the larger problem
can be understood but accomplishing no more with each step than you need to in
order for it to stand on its own.
The minimum size of a change should be a complete implementation of the simplest
subproblem which works on its own and expresses an entire idea, not just part
of an idea. You could mechanically split a 1,000-line change into ten 100-line
changes by choosing lines at random, but none of the individual changes would
make any sense and you would increase the collective complexity. The real goal
is for each change to have minimal complexity, line size is just a proxy that is
often well-correlated with complexity.
We generally follow these practices in Phabricator. The median change size for
Phabricator is 35 lines.
-See @{article:Differential User Guide: Large Changes} for information about
-reviewing big checkins.
-
= Write Sensible Commit Messages =
There are lots of resources for this on the internet. All of them say pretty
much the same thing; this one does too.
The single most important thing is: **commit messages should explain //why// you
are making the change**.
Differential attempts to encourage the construction of sensible commit messages,
but can only enforce structure, not content. Structurally, commit messages
should probably:
- Have a title, briefly describing the change in one line.
- Have a summary, describing the change in more detail.
- Maybe have some other fields.
The content is far more important than the structure. In particular, the summary
should explain //why// you're making the change and //why// you're choosing the
implementation you're choosing. The //what// of the change is generally
well-explained by the change itself. For example, this is obviously an awful
commit message:
COUNTEREXAMPLE
fix a bug
But this one is almost as bad:
COUNTEREXAMPLE
Allow dots in usernames
Change the regexps so usernames can have dots in them.
This is better than nothing but just summarizes information which can be
inferred from the text of the diff. Instead, you should provide context and
explain why you're making the change you're making, and why it's the right one:
lang=txt
Allow dots in usernames to support Google and LDAP auth
To prevent nonsense, usernames are currently restricted to A-Z0-9. Now that
we have Google and LDAP auth, a couple of installs want to allow "." too
since they have schemes like "abraham.lincoln@mycompany.com" (see Tnnn). There
are no technical reasons not to do this, so I opened up the regexps a bit.
We could mostly open this up more but I figured I'd wait until someone asks
before allowing "ke$ha", etc., because I personally find such names
distasteful and offensive.
This information can not be extracted from the change itself, and is much more
useful for the reviewer and for anyone trying to understand the change after the
fact.
An easy way to explain //why// is to reference other objects
(bugs/issues/revisions) which motivate the change.
Differential also includes a "Test Plan" field which is required by default.
There is a detailed description of this field in @{article:Differential User
Guide: Test Plans}. You can make it optional or disable it in the configuration,
but consider adopting it. Having this information can be particularly helpful
for reviewers.
Some things that people sometimes feel strongly about but which are probably not
really all that important in commit messages include:
- If/where text is wrapped.
- Maximum length of the title.
- Whether there should be a period or not in the title.
- Use of voice/tense, e.g. "fix"/"add" vs "fixes"/"adds".
- Other sorts of pedantry not related to getting the context and
reasons //why// a change is happening into the commit message.
- Although maybe the spelling and grammar shouldn't be egregiously bad?
Phabricator does not have guidelines for this stuff. You can obviously set
guidelines at your organization if you prefer, but getting the //why// into the
message is the most important part.
= Next Steps =
Continue by:
- reading recommendations on structuring revision control with
@{article:Recommendations on Revision Control}; or
- reading recommendations on structuring branches with
@{article:Recommendations on Branching}.
diff --git a/src/docs/user/configuration/configuration_locked.diviner b/src/docs/user/configuration/configuration_locked.diviner
index 040b83817..fff0da9bd 100644
--- a/src/docs/user/configuration/configuration_locked.diviner
+++ b/src/docs/user/configuration/configuration_locked.diviner
@@ -1,101 +1,121 @@
@title Configuration Guide: Locked and Hidden Configuration
@group config
Details about locked and hidden configuration.
Overview
========
Some configuration options are **Locked** or **Hidden**. If an option has one
of these attributes, it means:
- **Locked Configuration**: This setting can not be written from the web UI.
- **Hidden Configuration**: This setting can not be read or written from
the web UI.
This document explains these attributes in more detail.
Locked Configuration
====================
**Locked Configuration** can not be edited from the web UI. In general, you
can edit it from the CLI instead, with `bin/config`:
```
phabricator/ $ ./bin/config set <key> <value>
```
+Some configuration options take complicated values which can be difficult
+to escape properly for the shell. The easiest way to set these options is
+to use the `--stdin` flag. First, put your desired value in a `config.json`
+file:
+
+```name=config.json, lang=json
+{
+ "duck": "quack",
+ "cow": "moo"
+}
+```
+
+Then, set it with `--stdin` like this:
+
+```
+phabricator/ $ ./bin/config set <key> --stdin < config.json
+```
+
A few settings have alternate CLI tools. Refer to the setting page for
details.
Note that these settings can not be written to the database, even from the
CLI.
Locked values can not be unlocked: they are locked because of what the setting
does or how the setting operates. Some of the reasons configuration options are
locked include:
**Required for bootstrapping**: Some options, like `mysql.host`, must be
available before Phabricator can read configuration from the database.
If you stored `mysql.host` only in the database, Phabricator would not know how
to connect to the database in order to read the value in the first place.
These options must be provided in a configuration source which is read earlier
in the bootstrapping process, before Phabricator connects to the database.
**Errors could not be fixed from the web UI**: Some options, like
`phabricator.base-uri`, can effectively disable the web UI if they are
configured incorrectly.
If these options could be configured from the web UI, you could not fix them if
you made a mistake (because the web UI would no longer work, so you could not
load the page to change the value).
We require these options to be edited from the CLI to make sure the editor has
access to fix any mistakes.
**Attackers could gain greater access**: Some options could be modified by an
attacker who has gained access to an administrator account in order to gain
greater access.
For example, an attacker who could modify `metamta.mail-adapter` (and other
similar options), could potentially reconfigure Phabricator to send mail
through an evil server they controlled, then trigger password resets on other
user accounts to compromise them.
We require these options to be edited from the CLI to make sure the editor
has full access to the install.
Hidden Configuration
====================
**Hidden Configuration** is similar to locked configuration, but also can not
be //read// from the web UI.
In almost all cases, configuration is hidden because it is some sort of secret
key or access token for an external service. These values are hidden from the
web UI to prevent administrators (or attackers who have compromised
administrator accounts) from reading them.
You can review (and edit) hidden configuration from the CLI:
```
phabricator/ $ ./bin/config get <key>
phabricator/ $ ./bin/config set <key> <value>
```
Next Steps
==========
Continue by:
+ - learning more about advanced options with
+ @{Configuration User Guide: Advanced Configuration}; or
- returning to the @{article: Configuration Guide}.
diff --git a/src/docs/user/configuration/configuring_inbound_email.diviner b/src/docs/user/configuration/configuring_inbound_email.diviner
index 5b47a1783..f4f367d57 100644
--- a/src/docs/user/configuration/configuring_inbound_email.diviner
+++ b/src/docs/user/configuration/configuring_inbound_email.diviner
@@ -1,218 +1,234 @@
@title Configuring Inbound Email
@group config
This document contains instructions for configuring inbound email, so users
may interact with some Phabricator applications via email.
= Preamble =
This can be extremely difficult to configure correctly. This is doubly true if
you use a local MTA.
There are a few approaches available:
| Receive Mail With | Setup | Cost | Notes |
|--------|-------|------|-------|
| Mailgun | Easy | Cheap | Recommended |
+| Postmark | Easy | Cheap | Recommended |
| SendGrid | Easy | Cheap | |
| Local MTA | Extremely Difficult | Free | Strongly discouraged! |
The remainder of this document walks through configuring Phabricator to
receive mail, and then configuring your chosen transport to deliver mail
to Phabricator.
= Configuring Phabricator =
By default, Phabricator uses a `noreply@phabricator.example.com` email address
as the 'From' (configurable with `metamta.default-address`) and sets
'Reply-To' to the user generating the email (e.g., by making a comment), if the
mail was generated by a user action. This means that users can reply (or
reply-all) to email to discuss changes, but the conversation won't be recorded
in Phabricator and users will not be able to take actions like claiming tasks or
requesting changes to revisions.
To change this behavior so that users can interact with objects in Phabricator
over email, change the configuration key `metamta.reply-handler-domain` to some
domain you configure according to the instructions below, e.g.
`phabricator.example.com`. Once you set this key, emails will use a
'Reply-To' like `T123+273+af310f9220ad@phabricator.example.com`, which -- when
configured correctly, according to the instructions below -- will parse incoming
email and allow users to interact with Differential revisions, Maniphest tasks,
etc. over email.
If you don't want Phabricator to take up an entire domain (or subdomain) you
can configure a general prefix so you can use a single mailbox to receive mail
on. To make use of this set `metamta.single-reply-handler-prefix` to the
prefix of your choice, and Phabricator will prepend this to the 'Reply-To'
mail address. This works because everything up to the first (optional) '+'
character in an email-address is considered the receiver, and everything
after is essentially ignored.
You can also set up application email addresses to allow users to create
application objects via email. For example, you could configure
`bugs@phabricator.example.com` to create a Maniphest task out of any email
which is sent to it. To do this, see application settings for a given
application at
{nav icon=home, name=Home >
name=Applications >
icon=cog, name=Settings}
= Security =
The email reply channel is "somewhat" authenticated. Each reply-to address is
unique to the recipient and includes a hash of user information and a unique
object ID, so it can only be used to update that object and only be used to act
on behalf of the recipient.
However, if an address is leaked (which is fairly easy -- for instance,
forwarding an email will leak a live reply address, or a user might take a
screenshot), //anyone// who can send mail to your reply-to domain may interact
with the object the email relates to as the user who leaked the mail. Because
the authentication around email has this weakness, some actions (like accepting
revisions) are not permitted over email.
This implementation is an attempt to balance utility and security, but makes
some sacrifices on both sides to achieve it because of the difficulty of
authenticating senders in the general case (e.g., where you are an open source
project and need to interact with users whose email accounts you have no control
over).
If you leak a bunch of reply-to addresses by accident, you can change
`phabricator.mail-key` in your configuration to invalidate all the old hashes.
You can also set `metamta.public-replies`, which will change how Phabricator
delivers email. Instead of sending each recipient a unique mail with a personal
reply-to address, it will send a single email to everyone with a public reply-to
address. This decreases security because anyone who can spoof a "From" address
can act as another user, but increases convenience if you use mailing lists and,
practically, is a reasonable setting for many installs. The reply-to address
will still contain a hash unique to the object it represents, so users who have
not received an email about an object can not blindly interact with it.
If you enable application email addresses, those addresses also use the weaker
"From" authentication mechanism.
NOTE: Phabricator does not currently attempt to verify "From" addresses because
this is technically complex, seems unreasonably difficult in the general case,
and no installs have had a need for it yet. If you have a specific case where a
reasonable mechanism exists to provide sender verification (e.g., DKIM
signatures are sufficient to authenticate the sender under your configuration,
or you are willing to require all users to sign their email), file a feature
request.
= Testing and Debugging Inbound Email =
You can use the `bin/mail` utility to test and review inbound mail. This can
help you determine if mail is being delivered to Phabricator or not:
phabricator/ $ ./bin/mail list-inbound # List inbound messages.
phabricator/ $ ./bin/mail show-inbound # Show details about a message.
You can also test receiving mail, but note that this just simulates receiving
the mail and doesn't send any information over the network. It is
primarily aimed at developing email handlers: it will still work properly
if your inbound email configuration is incorrect or even disabled.
phabricator/ $ ./bin/mail receive-test # Receive test message.
Run `bin/mail help <command>` for detailed help on using these commands.
= Mailgun Setup =
To use Mailgun, you need a Mailgun account. You can sign up at
<http://www.mailgun.com>. Provided you have such an account, configure it
like this:
- Configure a mail domain according to Mailgun's instructions.
- Add a Mailgun route with a `catch_all()` rule which takes the action
`forward("https://phabricator.example.com/mail/mailgun/")`. Replace the
example domain with your actual domain.
- Set the `mailgun.api-key` config key to your Mailgun API key.
+Postmark Setup
+==============
+
+To process inbound mail from Postmark, configure this URI as your inbound
+webhook URI in the Postmark control panel:
+
+```
+https://<phabricator.yourdomain.com>/mail/postmark/
+```
+
+See also the Postmark section in @{article:Configuring Outbound Email} for
+discussion of the remote address whitelist used to verify that requests this
+endpoint receives are authentic requests originating from Postmark.
+
+
= SendGrid Setup =
To use SendGrid, you need a SendGrid account with access to the "Parse API" for
inbound email. Provided you have such an account, configure it like this:
- Configure an MX record according to SendGrid's instructions, i.e. add
`phabricator.example.com MX 10 mx.sendgrid.net.` or similar.
- Go to the "Parse Incoming Emails" page on SendGrid
(<http://sendgrid.com/developer/reply>) and add the domain as the
"Hostname".
- Add the URL `https://phabricator.example.com/mail/sendgrid/` as the "Url",
using your domain (and HTTP instead of HTTPS if you are not configured with
SSL).
- If you get an error that the hostname "can't be located or verified", it
means your MX record is either incorrectly configured or hasn't propagated
yet.
- Set `metamta.reply-handler-domain` to `phabricator.example.com`"
(whatever you configured the MX record for).
That's it! If everything is working properly you should be able to send email
to `anything@phabricator.example.com` and it should appear in
`bin/mail list-inbound` within a few seconds.
= Local MTA: Installing Mailparse =
If you're going to run your own MTA, you need to install the PECL mailparse
extension. In theory, you can do that with:
$ sudo pecl install mailparse
You may run into an error like "needs mbstring". If so, try:
$ sudo yum install php-mbstring # or equivalent
$ sudo pecl install -n mailparse
If you get a linker error like this:
COUNTEREXAMPLE
PHP Warning: PHP Startup: Unable to load dynamic library
'/usr/lib64/php/modules/mailparse.so' - /usr/lib64/php/modules/mailparse.so:
undefined symbol: mbfl_name2no_encoding in Unknown on line 0
...you need to edit your php.ini file so that mbstring.so is loaded **before**
mailparse.so. This is not the default if you have individual files in
`php.d/`.
= Local MTA: Configuring Sendmail =
Before you can configure Sendmail, you need to install Mailparse. See the
section "Installing Mailparse" above.
Sendmail is very difficult to configure. First, you need to configure it for
your domain so that mail can be delivered correctly. In broad strokes, this
probably means something like this:
- add an MX record;
- make sendmail listen on external interfaces;
- open up port 25 if necessary (e.g., in your EC2 security policy);
- add your host to /etc/mail/local-host-names; and
- restart sendmail.
Now, you can actually configure sendmail to deliver to Phabricator. In
`/etc/aliases`, add an entry like this:
phabricator: "| /path/to/phabricator/scripts/mail/mail_handler.php"
If you use the `PHABRICATOR_ENV` environmental variable to select a
configuration, you can pass the value to the script as an argument:
.../path/to/mail_handler.php <ENV>
This is an advanced feature which is rarely used. Most installs should run
without an argument.
After making this change, run `sudo newaliases`. Now you likely need to symlink
this script into `/etc/smrsh/`:
sudo ln -s /path/to/phabricator/scripts/mail/mail_handler.php /etc/smrsh/
Finally, edit `/etc/mail/virtusertable` and add an entry like this:
@yourdomain.com phabricator@localhost
That will forward all mail to @yourdomain.com to the Phabricator processing
script. Run `sudo /etc/mail/make` or similar and then restart sendmail with
`sudo /etc/init.d/sendmail restart`.
diff --git a/src/docs/user/configuration/configuring_outbound_email.diviner b/src/docs/user/configuration/configuring_outbound_email.diviner
index 2a95f49bc..5de8429c1 100644
--- a/src/docs/user/configuration/configuring_outbound_email.diviner
+++ b/src/docs/user/configuration/configuring_outbound_email.diviner
@@ -1,201 +1,330 @@
@title Configuring Outbound Email
@group config
Instructions for configuring Phabricator to send mail.
-= Overview =
+Overview
+========
-Phabricator can send outbound email via several different providers, called
-"Adapters".
+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 (See Note) |
+| SendGrid | Medium | Cheap | Yes | Discouraged |
| External SMTP | Medium | Varies | No | Gmail, etc. |
-| Local SMTP | Hard | Free | No | (Default) sendmail, postfix, etc |
-| Custom | Hard | Free | No | Write an adapter for some other service. |
+| 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. |
-Of these options, sending mail via local SMTP is the default, but usually
-requires some configuration to get working. See below for details on how to
-select and configure a delivery method.
+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 adapter, but this requires digging into the code.
+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}.
-**Note on SendGrid**: 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. If you send to SendGrid via SMTP, you may need
-to adjust `phpmailer.smtp-encoding`.
-= Basics =
+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 Mail Adapters =
-
-To choose how mail will be sent, change the `metamta.mail-adapter` key in
-your configuration. Possible values are listed in the UI:
-
- - `PhabricatorMailImplementationAmazonMailgunAdapter`: use Mailgun, see
- "Adapter: Mailgun".
- - `PhabricatorMailImplementationAmazonSESAdapter`: use Amazon SES, see
- "Adapter: Amazon SES".
- - `PhabricatorMailImplementationPHPMailerLiteAdapter`: default, uses
- "sendmail", see "Adapter: Sendmail".
- - `PhabricatorMailImplementationPHPMailerAdapter`: uses SMTP, see
- "Adapter: SMTP".
- - `PhabricatorMailImplementationSendGridAdapter`: use SendGrid, see
- "Adapter: SendGrid".
- - `Some Custom Class You Write`: use a custom adapter you write, see
- "Adapter: Custom".
- - `PhabricatorMailImplementationTestAdapter`: this will
- **completely disable** outbound mail. You can use this if you don't want to
- send outbound mail, or want to skip this step for now and configure it
- later.
-
-= Adapter: Sendmail =
-
-This is the default, and selected by choosing
-`PhabricatorMailImplementationPHPMailerLiteAdapter` as the value for
-**metamta.mail-adapter**. 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.
+Configuring Mailers
+===================
-If you experience issues with mail getting mangled (for example, arriving with
-too many or too few newlines) you may try adjusting `phpmailer.smtp-encoding`.
+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.
-= Adapter: SMTP =
+A valid `cluster.mailers` configuration looks something like this:
-You can use this adapter to send mail via an external SMTP server, like Gmail.
-To do this, set these configuration keys:
-
- - **metamta.mail-adapter**: set to
- `PhabricatorMailImplementationPHPMailerAdapter`.
- - **phpmailer.mailer**: set to `smtp`.
- - **phpmailer.smtp-host**: set to hostname of your SMTP server.
- - **phpmailer.smtp-port**: set to port of your SMTP server.
- - **phpmailer.smtp-user**: set to your username used for authentication.
- - **phpmailer.smtp-password**: set to your password used for authentication.
- - **phpmailer.smtp-protocol**: set to `tls` or `ssl` if necessary. Use
- `ssl` for Gmail.
- - **phpmailer.smtp-encoding**: Normally safe to leave as the default, but
- adjusting it may help resolve mail mangling issues (for example, mail
- arriving with too many or too few newlines).
+```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.
+
+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.
-= Adapter: Mailgun =
+To use this mailer, set `type` to `mailgun`, then configure these `options`:
-Mailgun is an email delivery service. You can learn more at
-<http://www.mailgun.com>. Mailgun isn't free, but is very easy to configure
-and works well.
+ - `api-key`: Required string. Your Mailgun API key.
+ - `domain`: Required string. Your Mailgun domain.
-To use Mailgun, sign up for an account, then set these configuration keys:
- - **metamta.mail-adapter**: set to
- `PhabricatorMailImplementationMailgunAdapter`.
- - **mailgun.api-key**: set to your Mailgun API key.
- - **mailgun.domain**: set to your Mailgun domain.
+Mailer: Postmark
+================
-= Adapter: Amazon SES =
+Postmark is a third-party email delivery serivice. You can learn more at
+<https://www.postmarkapp.com/>.
-Amazon SES is Amazon's cloud email service. It is not free, but is easier to
-configure than sendmail and can simplify outbound email configuration. To use
-Amazon SES, you need to sign up for an account with Amazon at
+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 configure Phabricator to use Amazon SES, set these configuration keys:
+To use this mailer, set `type` to `ses`, then configure these `options`:
- - **metamta.mail-adapter**: set to
- "PhabricatorMailImplementationAmazonSESAdapter".
- - **amazon-ses.access-key**: set to your Amazon SES access key.
- - **amazon-ses.secret-key**: set to your Amazon SES secret key.
- - **amazon-ses.endpoint**: Set to your Amazon SES endpoint.
+ - `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!
+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!
-= Adapter: SendGrid =
-SendGrid is an email delivery service like Amazon SES. You can learn more at
-<http://sendgrid.com/>. It is easy to configure, but not free.
+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, just configure `sendmail` and leave Phabricator's setup
-with defaults. To use the REST API, follow the instructions in this section.
+API. To use SMTP, configure Phabricator to use an `smtp` mailer.
-To configure Phabricator to use SendGrid, set these configuration keys:
+To use the REST API mailer, set `type` to `sendgrid`, then configure
+these `options`:
- - **metamta.mail-adapter**: set to
- "PhabricatorMailImplementationSendGridAdapter".
- - **sendgrid.api-user**: set to your SendGrid login name.
- - **sendgrid.api-key**: set to your SendGrid password.
+ - `api-user`: Required string. Your SendGrid login name.
+ - `api-key`: Required string. Your SendGrid API key.
-If you're logged into your SendGrid account, you may be able to find this
-information easily by visiting <http://sendgrid.com/developer>.
+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.
-= Adapter: Custom =
-You can provide a custom adapter by writing a concrete subclass of
-@{class:PhabricatorMailImplementationAdapter} and setting it as the
-`metamta.mail-adapter`.
+Mailer: Sendmail
+================
-TODO: This should be better documented once extending Phabricator is better
-documented.
+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.
-= Adapter: Disable Outbound Mail =
+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.
-You can use the @{class:PhabricatorMailImplementationTestAdapter} to completely
-disable outbound mail, if you don't want to send mail or don't want to configure
-it yet. Just set **metamta.mail-adapter** to
-`PhabricatorMailImplementationTestAdapter`.
+To use this mailer, set `type` to `smtp`, then configure these `options`:
-= Testing and Debugging Outbound Email =
+ - `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).
-= Next Steps =
+
+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/installation_guide.diviner b/src/docs/user/installation_guide.diviner
index 8feb8ae1d..29895eb2a 100644
--- a/src/docs/user/installation_guide.diviner
+++ b/src/docs/user/installation_guide.diviner
@@ -1,179 +1,179 @@
@title Installation Guide
@group intro
This document contains basic install instructions to get Phabricator up and
running.
Overview
========
Phabricator is a LAMP (Linux, Apache, MySQL, PHP) application. To install
Phabricator, you will need:
- a normal computer to install it on (shared hosts and unusual environments
are not supported) running some flavor of Linux or a similar OS;
- a domain name (like `phabricator.mycompany.com`);
- basic sysadmin skills;
- Apache, nginx, or another webserver;
- PHP, MySQL, and Git.
The remainder of this document details these requirements.
Installation Requirements
=========================
You will need **a computer**. Options include:
- **A Normal Computer**: This is strongly recommended. Many installs use a VM
in EC2. Phabricator installs properly and works well on a normal computer.
- **A Shared Host**: This may work, but is not recommended. Many shared
hosting environments have restrictions which prevent some of Phabricator's
features from working. Consider using a normal computer instead. We do not
support shared hosts.
- **A SAN Appliance, Network Router, Gaming Console, Raspberry Pi, etc.**:
Although you may be able to install Phabricator on specialized hardware, it
is unlikely to work well and will be difficult for us to support. Strongly
consider using a normal computer instead. We do not support specialized
hardware.
- **A Toaster, Car, Firearm, Thermostat, etc.**: Yes, many modern devices now
have embedded computing capability. We live in interesting times. However,
you should not install Phabricator on these devices. Instead, install it on
a normal computer. We do not support installing on noncomputing devices.
To install the Phabricator server software, you will need an **operating
system** on your normal computer which is **not Windows**. Note that the
command line interface //does// work on Windows, and you can //use//
Phabricator from any operating system with a web browser. However, the server
software does not run on Windows. It does run on most other operating systems,
so choose one of these instead:
- **Linux**: Most installs use Linux.
- **Mac OS X**: Mac OS X is an acceptable flavor of Linux.
- **FreeBSD**: While FreeBSD is certainly not a flavor of Linux, it is a fine
operating system possessed of many desirable qualities, and Phabricator will
install and run properly on FreeBSD.
- **Solaris, etc.**: Other systems which look like Linux and quack like Linux
will generally work fine, although we may suffer a reduced ability to
support and resolve issues on unusual operating systems.
Beyond an operating system, you will need **a webserver**.
- **Apache**: Many installs use Apache + `mod_php`.
- **nginx**: Many installs use nginx + `php-fpm`.
- **lighttpd**: `lighttpd` is less popular than Apache or nginx, but it
works fine.
- **Other**: Other webservers which can run PHP are also likely to work fine,
although these installation instructions will not cover how to set them up.
- **PHP Builtin Server**: You can use the builtin PHP webserver for
development or testing, although it should not be used in production.
You will also need:
- **MySQL**: You need MySQL. We strongly recommend MySQL 5.5 or newer.
- **PHP**: You need PHP 5.2 or newer.
You'll probably also need a **domain name**. In particular, you should read this
note:
NOTE: Phabricator must be installed on an entire domain. You can not install it
to a path on an existing domain, like `example.com/phabricator/`. Instead,
install it to an entire domain or subdomain, like `phabricator.example.com`.
Level Requirements
==================
To install and administrate Phabricator, you'll need to be comfortable with
common system administration skills. For example, you should be familiar with
using the command line, installing software on your operating system of choice,
working with the filesystem, managing processes, dealing with permissions,
editing configuration files, and setting environment variables.
If you aren't comfortable with these skills, you can still try to perform an
install. The install documentation will attempt to guide you through what you
need to know. However, if you aren't very familiar or comfortable with using
this set of skills to troubleshoot and resolve problems, you may encounter
issues which you have substantial difficulty working through.
We assume users installing and administrating Phabricator are comfortable with
common system administration skills and concepts. If you aren't, proceed at
your own risk and expect that your skills may be tested.
Installing Required Components
==============================
If you are installing on Ubuntu or an RedHat derivative, there are install
scripts available which should handle most of the things discussed in this
document for you:
- **RedHat Derivatives**:
[[ https://secure.phabricator.com/diffusion/P/browse/master/scripts/install/install_rhel-derivs.sh
| install_rhel-derivs.sh ]]
- **Ubuntu**:
[[ https://secure.phabricator.com/diffusion/P/browse/master/scripts/install/install_ubuntu.sh
| install_ubuntu.sh ]]
If those work for you, you can skip directly to the
@{article:Configuration Guide}. These scripts are also available in the
`scripts/install` directory in the project itself.
Otherwise, here's a general description of what you need to install:
- git (usually called "git" in package management systems)
- Apache (usually "httpd" or "apache2") (or nginx)
- MySQL Server (usually "mysqld" or "mysql-server")
- PHP (usually "php")
- Required PHP extensions: mbstring, iconv, mysql (or mysqli), curl, pcntl
- (these might be something like "php-mysql" or "php5-mysql")
+ (these might be something like "php-mysql" or "php5-mysqlnd")
- Optional PHP extensions: gd, apc (special instructions for APC are available
below if you have difficulty installing it), xhprof (instructions below,
you only need this if you are developing Phabricator)
If you already have LAMP setup, you've probably already got everything you need.
It may also be helpful to refer to the install scripts above, even if they don't
work for your system.
Now that you have all that stuff installed, grab Phabricator and its
dependencies:
$ cd somewhere/ # pick some install directory
somewhere/ $ git clone https://github.com/phacility/libphutil.git
somewhere/ $ git clone https://github.com/phacility/arcanist.git
somewhere/ $ git clone https://github.com/phacility/phabricator.git
= Installing APC (Optional) =
Like everything else written in PHP, Phabricator will run much faster with APC
installed. You likely need to install "pcre-devel" first:
sudo yum install pcre-devel
Then you have two options. Either install via PECL (try this first):
sudo yum install php-pear
sudo pecl install apc
**If that doesn't work**, grab the package from PECL directly and follow the
build instructions there:
http://pecl.php.net/package/APC
Installing APC is optional but **strongly recommended**, especially on
production hosts.
Once APC is installed, test that it is available by running:
php -i | grep apc
If it doesn't show up, add:
extension=apc.so
..to "/etc/php.d/apc.ini" or the "php.ini" file indicated by "php -i".
= Next Steps =
Continue by:
- configuring Phabricator with the @{article:Configuration Guide}; or
- learning how to keep Phabricator up to date with
@{article:Upgrading Phabricator}.
diff --git a/src/docs/user/support.diviner b/src/docs/user/support.diviner
index 9dc06b15a..c99b272de 100644
--- a/src/docs/user/support.diviner
+++ b/src/docs/user/support.diviner
@@ -1,87 +1,92 @@
@title Support Resources
@short Support
@group intro
Resources for reporting bugs, requesting features, and getting support.
Overview
========
This document describes available support resources.
-The upstream provides active, free support for a narrow range of problems
-(primarily, security issues and reproducible bugs).
+The upstream provides free support for a narrow range of problems (primarily,
+security issues and reproducible bugs) and paid support for virtually anything.
The upstream does not provide free support for general problems with installing
or configuring Phabricator. You may be able to get some help with these
kinds of issues from the community.
+Paid Support
+============
+
+If you'd like upstream support, see ((pacts)).
+
+This is the only way to request features and the only way to get guaranteed
+answers from experts quickly.
+
+
Reporting Security Vulnerabilities
==================================
The upstream accepts, fixes, and awards bounties for reports of material
security issues with the software.
To report security issues, see @{article:Reporting Security Vulnerabilities}.
Reporting Bugs
==============
The upstream will accept **reproducible** bug reports in modern, first-party
production code running in reasonable environments. Before submitting a bug
-report you **must update** to the latest version of Phabricator. This reduces
-support costs on the upstream, please be mindful.
+report you **must update** to the latest version of Phabricator.
To report bugs, see @{article:Contributing Bug Reports}.
Contributing
============
Phabricator is a very difficult project to contribute to. New contributors
will face a high barrier to entry.
If you'd like to contribute to Phabricator, start with
@{article:Contributor Introduction}.
Installation and Setup Help
===========================
-Installation and setup help is available from the upstream at consulting rates.
-See [[ https://secure.phabricator.com/w/consulting/ | Consulting ]] for details.
-
-Helping individual installs navigate unique setup problems takes our time
-away from developing Phabricator, so we can not offer this service for free.
-
You may be able to get free help with these issues from the
-[[ https://phurl.io/u/discourse | community ]]. See below for details.
+[[ https://phurl.io/u/discourse | community ]].
+
+You can also pay us for support. See ((pacts)).
Hosting
=========
The upstream offers Phabricator as a hosted service at
[[ https://phacility.com | Phacility ]]. This simplifies setting up and
-operating a Phabricator instance, and gives you access to a broader range
-of upstream support services.
+operating a Phabricator instance, and automatically gives you access to a
+broader range of upstream support services.
Running this service gives us a strong financial incentive to make installing
and operating Phabricator as difficult as possible. Blinded by greed, we toil
endlessly to make installation a perplexing nightmare that none other than
ourselves can hope to navigate.
Phabricator Community
=====================
-We provide hosting for a
-[[ https://phurl.io/u/discourse | Discussion Forum ]]
-where admins and users help and answer questions from other community members.
-Upstream developers may occasionally participate, but this is mostly
-a user to user community. If you run into general problems, but are not
-interested in paid support, this is the main place to find help.
+We provide hosting for a [[ https://phurl.io/u/discourse | Discussion Forum ]]
+where admininstrators and users help and answer questions from other community
+members.
+
+Upstream developers occasionally participate, but this is mostly a user to user
+community. If you run into general problems, but are not interested in paid
+support, this is the main place to find help.
diff --git a/src/docs/user/userguide/differential.diviner b/src/docs/user/userguide/differential.diviner
index 414ad87a4..496fba638 100644
--- a/src/docs/user/userguide/differential.diviner
+++ b/src/docs/user/userguide/differential.diviner
@@ -1,73 +1,71 @@
@title Differential User Guide
@group userguide
Guide to the Differential (pre-push code review) tool and workflow.
= Overview =
Phabricator supports two code review workflows, "review" (pre-push) and
"audit" (post-push). To understand the differences between the two, see
@{article:User Guide: Review vs Audit}.
This document summarizes the pre-push "review" workflow implemented by the tool
//Differential//.
= How Review Works =
Code review in Phabricator is a lightweight, asynchronous web-based process. If
you are familiar with GitHub, it is similar to how pull requests work:
- An author prepares a change to a codebase, then sends it for review. They
specify who they want to review it (additional users may be notified as
well, see below). The change itself is called a "Differential Revision".
- The reviewers receive an email asking them to review the change.
- The reviewers inspect the change and either discuss it, approve it, or
request changes (e.g., if they identify problems or bugs).
- In response to feedback, the author may update the change (e.g., fixing
the bugs or addressing the problems).
- Once everything is satisfied, some reviewer accepts the change and the
author pushes it to the upstream.
The Differential home screen shows two sets of revisions:
- **Action Required** is revisions you are the author of or a reviewer for,
which you need to review, revise, or push.
- **Waiting on Others** is revisions you are the author of or a reviewer for,
which someone else needs to review, revise, or push.
= Creating Revisions =
The preferred way to create revisions in Differential is with `arc`
(see @{article:Arcanist User Guide}). You can also create revisions from the
web interface, by navigating to Differential, pressing the "Create Revision"
button, and pasting a diff in.
= Herald Rules =
If you're interested in keeping track of changes to certain parts of a codebase
(e.g., maybe changes to a feature or changes in a certain language, or there's
just some intern who you don't trust) you can write a Herald rule to
automatically CC you on any revisions which match rules (like content, author,
files affected, etc.)
Inline Comments
===============
You can leave inline comments by clicking the line number next to a line. For
an in-depth look at inline comments, see
@{article:Differential User Guide: Inline Comments}.
Next Steps
==========
Continue by:
- diving into the details of inline comments in
@{article:Differential User Guide: Inline Comments}; or
- reading the FAQ at @{article:Differential User Guide: FAQ}; or
- - learning about handling large changesets in
- @{article:Differential User Guide: Large Changes}; or
- learning about test plans in
@{article:Differential User Guide: Test Plans}; or
- learning more about Herald in @{article:Herald User Guide}.
diff --git a/src/docs/user/userguide/differential_large_changes.diviner b/src/docs/user/userguide/differential_large_changes.diviner
deleted file mode 100644
index 5d9f47829..000000000
--- a/src/docs/user/userguide/differential_large_changes.diviner
+++ /dev/null
@@ -1,54 +0,0 @@
-@title Differential User Guide: Large Changes
-@group userguide
-
-Dealing with huge changesets, and when **not** to use Differential.
-
-= Overview =
-
-When you want code review for a given changeset, Differential is not always the
-right tool to use. The rule of thumb is that you should only send changes to
-Differential if you expect humans to review the actual differences in the source
-code from the web interface. This should cover the vast majority of changes but,
-for example, you usually should //not// submit changes like these through
-Differential:
-
- - Committing an entire open source project to a private repo somewhere so
- you can fork it or link against it.
- - Committing an enormous text datafile, like a list of every English word or a
- dump of a database.
- - Making a trivial (e.g., find/replace or codemod) edit to 10,000 files.
-
-You can still try submitting these kinds of changes, but you may encounter
-problems getting them to work (database or connection timeouts, for example).
-Differential is pretty fast and scalable, but at some point either it or the
-browser will break down: you simply can't show nine million files on a webpage.
-
-More importantly, in all these cases, the text of the changes won't be reviewed
-by a human. The metadata associated with the change is what needs review (e.g.,
-what are you checking in, where are you putting it, and why? Does the change
-make sense? In the case of automated transformations, what script did you use?).
-To get review for these types of changes, one of these strategies will usually
-work better than trying to get the entire change into Differential:
-
- - Send an email/AIM/IRC to your reviewer(s) like "Hey, I'm going to check in
- the source for MySQL 9.3.1 to /full/path/to/whatever. The change is staged
- in /home/whatever/path/somewhere if you want to take a look. Can I put your
- name on the review?". This is best for straightforward changes. The reviewer
- is not going to review MySQL's source itself, instead they are reviewing the
- change metadata: which version are you checking in, why are you checking it
- in, and where are you putting it? You won't be able to use "arc commit" or
- "arc amend" to actually push the change. Just use "svn" or "git" and
- manually edit the commit message instead. (It is normally sufficient to add
- a "Reviewed By: <username>" field.)
- - Create a Differential revision with only the metadata, like the script you
- used to make automated changes or a text file explaining what you're doing,
- and maybe a sample of some of the changes if they were automated. Include a
- link to where the changes are staged so reviewers can look at the actual
- changeset if they want to. This is best for more complicated changes, since
- Differential can still be used for discussion and provide a permanent record
- others can refer to. Once the revision is accepted, amend your local commit
- (e.g. by `git commit --amend`) with the real change and push as usual.
-
-These kinds of changes are generally rare and don't have much in common, which
-is why there's no explicit support for them in Differential. If you frequently
-run into cases which Differential doesn't handle, let us know what they are.
diff --git a/src/docs/user/userguide/mail_rules.diviner b/src/docs/user/userguide/mail_rules.diviner
index 3640f5e5a..cdbbf1919 100644
--- a/src/docs/user/userguide/mail_rules.diviner
+++ b/src/docs/user/userguide/mail_rules.diviner
@@ -1,81 +1,69 @@
@title User Guide: Managing Phabricator Email
@group userguide
How to effectively manage Phabricator email notifications.
-= Overview =
+Overview
+========
Phabricator uses email as a major notification channel, but the amount of email
it sends can seem overwhelming if you're working on an active team. This
document discusses some strategies for managing email.
By far the best approach to managing mail is to **write mail rules** to
categorize mail. Essentially all modern mail clients allow you to quickly
write sophisticated rules to route, categorize, or delete email.
-= Reducing Email =
+Reducing Email
+==============
You can reduce the amount of email you receive by turning off some types of
email in {nav Settings > Email Preferences}. For example, you can turn off email
produced by your own actions (like when you comment on a revision), and some
types of less-important notifications about events.
-= Mail Rules =
+Mail Rules
+==========
The best approach to managing mail is to write mail rules. Simply writing rules
to move mail from Differential, Maniphest and Herald to separate folders will
vastly simplify mail management.
-Phabricator also sets a large number of headers (see below) which can allow you
-to write more sophisticated mail rules.
-
-= Mail Headers =
-
-Phabricator sends a variety of mail headers that can be useful in crafting rules
-to route and manage mail.
-
-Headers in plural contain lists. A list containing two items, `1` and
-`15` will generally be formatted like this:
-
- X-Header: <1>, <15>
-
-The intent is to allow you to write a rule which matches against "<1>". If you
-just match against "1", you'll incorrectly match "15", but matching "<1>" will
-correctly match only "<1>".
-
-Some other headers use a single value but can be presented multiple times.
-It is to support e-mail clients which are not able to create rules using regular
-expressions or wildcards (namely Outlook).
-
-The headers Phabricator adds to mail are:
-
- - `X-Phabricator-Sent-This-Message`: this is attached to all mail
- Phabricator sends. You can use it to differentiate between email from
- Phabricator and replies/forwards of Phabricator mail from human beings.
- - `X-Phabricator-To`: this is attached to all mail Phabricator sends.
- It shows the PHIDs of the original "To" line, before any mutation
- by the mailer configuration.
- - `X-Phabricator-Cc`: this is attached to all mail Phabricator sends.
- It shows the PHIDs of the original "Cc" line, before any mutation by the
- mailer configuration.
- - `X-Differential-Author`: this is attached to Differential mail and shows
- the revision's author. You can use it to filter mail about your revisions
- (or other users' revisions).
- - `X-Differential-Reviewer`: this is attached to Differential mail and
- shows the reviewers. You can use it to filter mail about revisions you
- are reviewing, versus revisions you are explicitly CC'd on or CC'd as
- a result of Herald rules.
- - `X-Differential-Reviewers`: list version of the previous.
- - `X-Differential-CC`: this is attached to Differential mail and shows
- the CCs on the revision.
- - `X-Differential-CCs`: list version of the previous.
- - `X-Differential-Explicit-CC`: this is attached to Differential mail and
- shows the explicit CCs on the revision (those that were added by humans,
- not by Herald).
- - `X-Differential-Explicit-CCs`: list version of the previous.
- - `X-Phabricator-Mail-Tags`: this is attached to some mail and has
- a list of descriptors about the mail. (This is fairly new and subject
- to some change.)
- - `X-Herald-Rules`: this is attached to some mail and shows Herald rule
- IDs which have triggered for the object. You can use this to sort or
- categorize mail that has triggered specific rules.
+Phabricator also adds mail headers (see below) which can allow you to write
+more sophisticated mail rules.
+
+Mail Headers
+============
+
+Phabricator sends various information in mail headers that can be useful in
+crafting rules to route and manage mail. To see a full list of headers, use
+the "View Raw Message" feature in your mail client.
+
+The most useful header for routing is generally `X-Phabricator-Stamps`. This
+is a list of attributes which describe the object the mail is about and the
+actions which the mail informs you about.
+
+Stamps and Gmail
+================
+
+If you use a client which can not perform header matching (like Gmail), you can
+change the {nav Settings > Email Format > Send Stamps} setting to include the
+stamps in the mail body and then match them with body rules.
+
+When writing filter rules against mail stamps in Gmail, you should quote any
+filters you want to apply. For example, specify rules like this, with quotes:
+
+> "author(@alice)"
+
+Note that Gmail will ignore some symbols when matching mail against filtering
+rules, so you can get false positives if the body of the message includes text
+like `author alice` (the same words in the same order, without the special
+symbols).
+
+You'll also get false positives if the message body includes the text of a
+mail stamp explicitly in a normal text field like a summary, description, or
+comment.
+
+There's no way to avoid these false positives other than using a different
+client with support for more powerful filtering rules, but these false
+positives should normally be uncommon.
diff --git a/src/docs/user/userguide/owners.diviner b/src/docs/user/userguide/owners.diviner
index f30fe5023..35260b08a 100644
--- a/src/docs/user/userguide/owners.diviner
+++ b/src/docs/user/userguide/owners.diviner
@@ -1,154 +1,159 @@
@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 are:
+The available settings allow you to take these actions:
- - **No Autoreview**: This package will not be added to new reviews.
- - **Subscribe to Changes**: This package will be added to reviews as a
- subscriber. Owners will be notified of changes, but not required to act.
- **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
+ - **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.
-NOTE: These rules **do not trigger** if the change author is a package owner.
-They only apply to changes made by users who aren't already owners.
+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 also do not trigger if the package has been archived.
+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.
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/remarkup.diviner b/src/docs/user/userguide/remarkup.diviner
index 28a1a31fc..d052a9a24 100644
--- a/src/docs/user/userguide/remarkup.diviner
+++ b/src/docs/user/userguide/remarkup.diviner
@@ -1,722 +1,722 @@
@title Remarkup Reference
@group userguide
Explains how to make bold text; this makes your words louder so you can win
arguments.
= Overview =
Phabricator uses a lightweight markup language called "Remarkup", similar to
other lightweight markup languages like Markdown and Wiki markup.
This document describes how to format text using Remarkup.
= Quick Reference =
All the syntax is explained in more detail below, but this is a quick guide to
formatting text in Remarkup.
These are inline styles, and can be applied to most text:
**bold** //italic// `monospaced` ##monospaced## ~~deleted~~ __underlined__
!!highlighted!!
D123 T123 rX123 # Link to Objects
{D123} {T123} # Link to Objects (Full Name)
{F123} # Embed Images
{M123} # Embed Pholio Mock
@username # Mention a User
#project # Mention a Project
[[wiki page]] # Link to Phriction
[[wiki page | name]] # Named link to Phriction
http://xyz/ # Link to web
[[http://xyz/ | name]] # Named link to web
[name](http://xyz/) # Alternate Link
These are block styles, and must be separated from surrounding text by
empty lines:
= Large Header =
== Smaller Header ==
## This is a Header As Well
Also a Large Header
===================
Also a Smaller Header
---------------------
> Quoted Text
Use `- ` or `* ` for bulleted lists, and `# ` for numbered lists.
Use ``` or indent two spaces for code.
Use %%% for a literal block.
Use | ... | ... for tables.
= Basic Styling =
Format **basic text styles** like this:
**bold text**
//italic text//
`monospaced text`
##monospaced text##
~~deleted text~~
__underlined text__
!!highlighted text!!
Those produce **bold text**, //italic text//, `monospaced text`, ##monospaced
text##, ~~deleted text~~, __underlined text__, and !!highlighted text!!
respectively.
= Layout =
Make **headers** like this:
= Large Header =
== Smaller Header ==
===== Very Small Header =====
Alternate Large Header
======================
Alternate Smaller Header
------------------------
You can optionally omit the trailing `=` signs -- that is, these are the same:
== Smaller Header ==
== Smaller Header
This produces headers like the ones in this document. Make sure you have an
empty line before and after the header.
Lists
=====
Make **lists** by beginning each item with a `-` or a `*`:
lang=text
- milk
- eggs
- bread
* duck
* duck
* goose
This produces a list like this:
- milk
- eggs
- bread
(Note that you need to put a space after the `-` or `*`.)
You can make numbered lists with a `#` instead of `-` or `*`:
# Articuno
# Zapdos
# Moltres
Numbered lists can also be started with `1.` or `1)`. If you use a number other
than `1`, the list will start at that number instead. For example, this:
```
200) OK
201) Created
202) Accepted
```
...produces this:
200) OK
201) Created
202) Accepted
You can also nest lists:
```- Body
- Head
- Arm
- Elbow
- Hand
# Thumb
# Index
# Middle
# Ring
# Pinkie
- Leg
- Knee
- Foot```
...which produces:
- Body
- Head
- Arm
- Elbow
- Hand
# Thumb
# Index
# Middle
# Ring
# Pinkie
- Leg
- Knee
- Foot
If you prefer, you can indent lists using multiple characters to show indent
depth, like this:
```- Tree
-- Branch
--- Twig```
As expected, this produces:
- Tree
-- Branch
--- Twig
You can add checkboxes to items by prefacing them with `[ ]` or `[X]`, like
this:
```
- [X] Preheat oven to 450 degrees.
- [ ] Zest 35 lemons.
```
When rendered, this produces:
- [X] Preheat oven to 450 degrees.
- [ ] Zest 35 lemons.
Make **code blocks** by indenting two spaces:
f(x, y);
You can also use three backticks to enclose the code block:
```f(x, y);
g(f);```
You can specify a language for syntax highlighting with `lang=xxx`:
lang=text
lang=html
<a href="#">...</a>
This will highlight the block using a highlighter for that language, if one is
available (in most cases, this means you need to configure Pygments):
lang=html
<a href="#">...</a>
You can also use a `COUNTEREXAMPLE` header to show that a block of code is
bad and shouldn't be copied:
lang=text
COUNTEREXAMPLE
function f() {
global $$variable_variable;
}
This produces a block like this:
COUNTEREXAMPLE
function f() {
global $$variable_variable;
}
You can use `lines=N` to limit the vertical size of a chunk of code, and
`name=some_name.ext` to give it a name. For example, this:
lang=text
lang=html, name=example.html, lines=12, counterexample
...
...produces this:
lang=html, name=example.html, lines=12, counterexample
<p>Apple</p>
<p>Apricot</p>
<p>Avocado</p>
<p>Banana</p>
<p>Bilberry</p>
<p>Blackberry</p>
<p>Blackcurrant</p>
<p>Blueberry</p>
<p>Currant</p>
<p>Cherry</p>
<p>Cherimoya</p>
<p>Clementine</p>
<p>Date</p>
<p>Damson</p>
<p>Durian</p>
<p>Eggplant</p>
<p>Elderberry</p>
<p>Feijoa</p>
<p>Gooseberry</p>
<p>Grape</p>
<p>Grapefruit</p>
<p>Guava</p>
<p>Huckleberry</p>
<p>Jackfruit</p>
<p>Jambul</p>
<p>Kiwi fruit</p>
<p>Kumquat</p>
<p>Legume</p>
<p>Lemon</p>
<p>Lime</p>
<p>Lychee</p>
<p>Mandarine</p>
<p>Mango</p>
<p>Mangostine</p>
<p>Melon</p>
You can use the `NOTE:`, `WARNING:` or `IMPORTANT:` elements to call attention
to an important idea.
For example, write this:
```
NOTE: Best practices in proton pack operation include not crossing the streams.
```
...to produce this:
NOTE: Best practices in proton pack operation include not crossing the streams.
Using `WARNING:` or `IMPORTANT:` at the beginning of the line changes the
color of the callout:
WARNING: Crossing the streams can result in total protonic reversal!
IMPORTANT: Don't cross the streams!
In addition, you can use `(NOTE)`, `(WARNING)`, or `(IMPORTANT)` to get the
same effect but without `(NOTE)`, `(WARNING)`, or `(IMPORTANT)` appearing in
the rendered result. For example, this callout uses `(NOTE)`:
(NOTE) Dr. Egon Spengler is the best resource for additional proton pack
questions.
Dividers
========
You can divide sections by putting three or more dashes on a line by
themselves. This creates a divider or horizontal rule similar to an `<hr />`
tag, like this one:
---
The dashes need to appear on their own line and be separated from other
content. For example, like this:
```
This section will be visually separated.
---
On an entirely different topic, ...
```
= Linking URIs =
URIs are automatically linked: http://phabricator.org/
If you have a URI with problematic characters in it, like
"`http://comma.org/,`", you can surround it with angle brackets:
<http://comma.org/,>
This will force the parser to consume the whole URI: <http://comma.org/,>
You can also use create named links, where you choose the displayed text. These
work within Phabricator or on the internet at large:
[[/herald/transcript/ | Herald Transcripts]]
[[http://www.boring-legal-documents.com/ | exciting legal documents]]
Markdown-style links are also supported:
[Toil](http://www.trouble.com)
= Linking to Objects =
You can link to Phabricator objects, such as Differential revisions, Diffusion
commits and Maniphest tasks, by mentioning the name of an object:
D123 # Link to Differential revision D123
rX123 # Link to SVN commit 123 from the "X" repository
rXaf3192cd5 # Link to Git commit "af3192cd5..." from the "X" repository.
# You must specify at least 7 characters of the hash.
T123 # Link to Maniphest task T123
You can also link directly to a comment in Maniphest and Differential (these
can be found on the date stamp of any transaction/comment):
T123#412 # Link to comment id #412 of task T123
See the Phabricator configuration setting `remarkup.ignored-object-names` to
modify this behavior.
= Embedding Objects
You can also generate full-name references to some objects by using braces:
{D123} # Link to Differential revision D123 with the full name
{T123} # Link to Maniphest task T123 with the full name
These references will also show when an object changes state (for instance, a
task or revision is closed). Some types of objects support rich embedding.
== Linking to Project Tags
Projects can be linked to with the use of a hashtag `#`. This works by default
using the name of the Project (lowercase, underscored). Additionally you
can set multiple additional hashtags by editing the Project details.
#qa, #quality_assurance
== Embedding Mocks (Pholio)
You can embed a Pholio mock by using braces to refer to it:
{M123}
By default the first four images from the mock set are displayed. This behavior
can be overridden with the **image** option. With the **image** option you can
provide one or more image IDs to display.
You can set the image (or images) to display like this:
{M123, image=12345}
{M123, image=12345 & 6789}
== Embedding Pastes
You can embed a Paste using braces:
{P123}
You can adjust the embed height with the `lines` option:
{P123, lines=15}
You can highlight specific lines with the `highlight` option:
{P123, highlight=15}
{P123, highlight="23-25, 31"}
== Embedding Images
You can embed an image or other file by using braces to refer to it:
{F123}
In most interfaces, you can drag-and-drop an image from your computer into the
text area to upload and reference it.
Some browsers (e.g. Chrome) support uploading an image data just by pasting them
from clipboard into the text area.
You can set file display options like this:
{F123, layout=left, float, size=full, alt="a duckling"}
Valid options for all files are:
- **layout** left (default), center, right, inline, link (render a link
instead of a thumbnail for images)
- **name** with `layout=link` or for non-images, use this name for the link
text
- **alt** Provide alternate text for assistive technologies.
Image files support these options:
- **float** If layout is set to left or right, the image will be floated so
text wraps around it.
- **size** thumb (default), full
- **width** Scale image to a specific width.
- **height** Scale image to a specific height.
Audio and video files support these options:
- **media**: Specify the media type as `audio` or `video`. This allows you
to disambiguate how file format which may contain either audio or video
should be rendered.
- **loop**: Loop this media.
- **autoplay**: Automatically begin playing this media.
== Embedding Countdowns
You can embed a countdown by using braces:
{C123}
= Quoting Text =
To quote text, preface it with an `>`:
> This is quoted text.
This appears like this:
> This is quoted text.
= Embedding Media =
If you set a configuration flag, you can embed media directly in text:
- **remarkup.enable-embedded-youtube**: allows you to paste in YouTube videos
and have them render inline.
This option is disabled by default because it has security and/or
silliness implications. Carefully read the description before enabling it.
= Image Macros =
You can upload image macros (More Stuff -> Macro) which will replace text
strings with the image you specify. For instance, you could upload an image of a
dancing banana to create a macro named "peanutbutterjellytime", and then any
time you type that string on a separate line it will be replaced with the image
of a dancing banana.
= Memes =
You can also use image macros in the context of memes. For example, if you
have an image macro named `grumpy`, you can create a meme by doing the
following:
{meme, src = grumpy, above = toptextgoeshere, below = bottomtextgoeshere}
By default, the font used to create the text for the meme is `tuffy.ttf`. For
the more authentic feel of `impact.ttf`, you simply have to place the Impact
TrueType font in the Phabricator subfolder `/resources/font/`. If Remarkup
detects the presence of `impact.ttf`, it will automatically use it.
= Mentioning Users =
In Differential and Maniphest, you can mention another user by writing:
@username
When you submit your comment, this will add them as a CC on the revision or task
if they aren't already CC'd.
Icons
=====
You can add icons to comments using the `{icon ...}` syntax. For example:
{icon camera}
This renders: {icon camera}
You can select a color for icons:
{icon camera color=blue}
This renders: {icon camera color=blue}
For a list of available icons and colors, check the UIExamples application.
(The icons are sourced from
-[[ http://fortawesome.github.io/Font-Awesome/ | FontAwesome ]], so you can also
+[[ https://fontawesome.com/v4.7.0/icons/ | FontAwesome ]], so you can also
browse the collection there.)
You can add `spin` to make the icon spin:
{icon cog spin}
This renders: {icon cog spin}
= Phriction Documents =
You can link to Phriction documents with a name or path:
Make sure you sign and date your [[legal/Letter of Marque and Reprisal]]!
By default, the link will render with the document title as the link name.
With a pipe (`|`), you can retitle the link. Use this to mislead your
opponents:
Check out these [[legal/boring_documents/ | exciting legal documents]]!
Links to pages which do not exist are shown in red. Links to pages which exist
but which the viewer does not have permission to see are shown with a lock
icon, and the link will not disclose the page title.
If you begin a link path with `./` or `../`, the remainder of the path will be
evaluated relative to the current wiki page. For example, if you are writing
content for the document `fruit/` a link to `[[./guava]]` is the same as a link
to `[[fruit/guava]]` from elsewhere.
Relative links may use `../` to transverse up the document tree. From the
`produce/vegetables/` page, you can use `[[../fruit/guava]]` to link to the
`produce/fruit/guava` page.
Relative links do not work when used outside of wiki pages. For example,
you can't use a relative link in a comment on a task, because there is no
reasonable place for the link to start resolving from.
When documents are moved, relative links are not automatically updated: they
are preserved as currently written. After moving a document, you may need to
review and adjust any relative links it contains.
= Literal Blocks =
To place text in a literal block use `%%%`:
%%%Text that won't be processed by remarkup
[[http://www.example.com | example]]
%%%
Remarkup will not process the text inside of literal blocks (other than to
escape HTML and preserve line breaks).
= Tables =
Remarkup supports simple table syntax. For example, this:
```
| Fruit | Color | Price | Peel?
| ----- | ----- | ----- | -----
| Apple | red | `$0.93` | no
| Banana | yellow | `$0.19` | **YES**
```
...produces this:
| Fruit | Color | Price | Peel?
| ----- | ----- | ----- | -----
| Apple | red | `$0.93` | no
| Banana | yellow | `$0.19` | **YES**
Remarkup also supports a simplified HTML table syntax. For example, this:
```
<table>
<tr>
<th>Fruit</th>
<th>Color</th>
<th>Price</th>
<th>Peel?</th>
</tr>
<tr>
<td>Apple</td>
<td>red</td>
<td>`$0.93`</td>
<td>no</td>
</tr>
<tr>
<td>Banana</td>
<td>yellow</td>
<td>`$0.19`</td>
<td>**YES**</td>
</tr>
</table>
```
...produces this:
<table>
<tr>
<th>Fruit</th>
<th>Color</th>
<th>Price</th>
<th>Peel?</th>
</tr>
<tr>
<td>Apple</td>
<td>red</td>
<td>`$0.93`</td>
<td>no</td>
</tr>
<tr>
<td>Banana</td>
<td>yellow</td>
<td>`$0.19`</td>
<td>**YES**</td>
</tr>
</table>
Some general notes about this syntax:
- your tags must all be properly balanced;
- your tags must NOT include attributes (`<td>` is OK, `<td style="...">` is
not);
- you can use other Remarkup rules (like **bold**, //italics//, etc.) inside
table cells.
Navigation Sequences
====================
You can use `{nav ...}` to render a stylized navigation sequence when helping
someone to locate something. This can be useful when writing documentation.
For example, you could give someone directions to purchase lemons:
{nav icon=home, name=Home >
Grocery Store >
Produce Section >
icon=lemon-o, name=Lemons}
To render this example, use this markup:
```
{nav icon=home, name=Home >
Grocery Store >
Produce Section >
icon=lemon-o, name=Lemons}
```
In general:
- Separate sections with `>`.
- Each section can just have a name to add an element to the navigation
sequence, or a list of key-value pairs.
- Supported keys are `icon`, `name`, `type` and `href`.
- The `type` option can be set to `instructions` to indicate that an element
is asking the user to make a choice or follow specific instructions.
Keystrokes
==========
You can use `{key ...}` to render a stylized keystroke. For example, this:
```
Press {key M} to view the starmap.
```
...renders this:
> Press {key M} to view the starmap.
You can also render sequences with modifier keys. This:
```
Use {key command option shift 3} to take a screenshot.
Press {key down down-right right LP} to activate the hadoken technique.
```
...renders this:
> Use {key command option shift 3} to take a screenshot.
> Press {key down down-right right LP} to activate the hadoken technique.
= Fullscreen Mode =
Remarkup editors provide a fullscreen composition mode. This can make it easier
to edit large blocks of text, or improve focus by removing distractions. You can
exit **Fullscreen** mode by clicking the button again or by pressing escape.
diff --git a/src/docs/user/userguide/webhooks.diviner b/src/docs/user/userguide/webhooks.diviner
new file mode 100644
index 000000000..51521b462
--- /dev/null
+++ b/src/docs/user/userguide/webhooks.diviner
@@ -0,0 +1,212 @@
+@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.
+
+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/config/PhabricatorClusterMailersConfigType.php b/src/infrastructure/cluster/config/PhabricatorClusterMailersConfigType.php
new file mode 100644
index 000000000..03f30506b
--- /dev/null
+++ b/src/infrastructure/cluster/config/PhabricatorClusterMailersConfigType.php
@@ -0,0 +1,102 @@
+<?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',
+ ));
+ } 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/customfield/datasource/PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource.php b/src/infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource.php
new file mode 100644
index 000000000..31f189988
--- /dev/null
+++ b/src/infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource.php
@@ -0,0 +1,70 @@
+<?php
+
+final class PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource
+ extends PhabricatorTypeaheadDatasource {
+
+ public function getBrowseTitle() {
+ return pht('Browse Any');
+ }
+
+ public function getPlaceholderText() {
+ return pht('Type "any()"...');
+ }
+
+ public function getDatasourceApplicationClass() {
+ return null;
+ }
+
+ public function getDatasourceFunctions() {
+ return array(
+ 'any' => array(
+ 'name' => pht('Any Value'),
+ 'summary' => pht('Find results with any value.'),
+ 'description' => pht(
+ "This function includes results which have any value. Use a query ".
+ "like this to find results with any value:\n\n%s",
+ '> any()'),
+ ),
+ );
+ }
+
+ public function loadResults() {
+ $results = array(
+ $this->newAnyFunction(),
+ );
+ return $this->filterResultsAgainstTokens($results);
+ }
+
+ protected function evaluateFunction($function, array $argv_list) {
+ $results = array();
+
+ foreach ($argv_list as $argv) {
+ $results[] = new PhabricatorQueryConstraint(
+ PhabricatorQueryConstraint::OPERATOR_ANY,
+ null);
+ }
+
+ return $results;
+ }
+
+ public function renderFunctionTokens($function, array $argv_list) {
+ $results = array();
+ foreach ($argv_list as $argv) {
+ $results[] = PhabricatorTypeaheadTokenView::newFromTypeaheadResult(
+ $this->newAnyFunction());
+ }
+ return $results;
+ }
+
+ private function newAnyFunction() {
+ $name = pht('Any Value');
+ return $this->newFunctionResult()
+ ->setName($name.' any')
+ ->setDisplayName($name)
+ ->setIcon('fa-circle-o')
+ ->setPHID('any()')
+ ->setUnique(true)
+ ->addAttribute(pht('Select results with any value.'));
+ }
+
+}
diff --git a/src/infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchDatasource.php b/src/infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchDatasource.php
new file mode 100644
index 000000000..7ba10e84f
--- /dev/null
+++ b/src/infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchDatasource.php
@@ -0,0 +1,17 @@
+<?php
+
+final class PhabricatorCustomFieldApplicationSearchDatasource
+ extends PhabricatorTypeaheadProxyDatasource {
+
+ public function getComponentDatasources() {
+ $datasources = parent::getComponentDatasources();
+
+ $datasources[] =
+ new PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource();
+ $datasources[] =
+ new PhabricatorCustomFieldApplicationSearchNoneFunctionDatasource();
+
+ return $datasources;
+ }
+
+}
diff --git a/src/infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchNoneFunctionDatasource.php b/src/infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchNoneFunctionDatasource.php
new file mode 100644
index 000000000..591e4fda0
--- /dev/null
+++ b/src/infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchNoneFunctionDatasource.php
@@ -0,0 +1,72 @@
+<?php
+
+final class PhabricatorCustomFieldApplicationSearchNoneFunctionDatasource
+ extends PhabricatorTypeaheadDatasource {
+
+ public function getBrowseTitle() {
+ return pht('Browse No Value');
+ }
+
+ public function getPlaceholderText() {
+ return pht('Type "none()"...');
+ }
+
+ public function getDatasourceApplicationClass() {
+ return null;
+ }
+
+ public function getDatasourceFunctions() {
+ return array(
+ 'none' => array(
+ 'name' => pht('No Value'),
+ 'summary' => pht('Find results with no value.'),
+ 'description' => pht(
+ "This function includes results which have no value. Use a query ".
+ "like this to find results with no value:\n\n%s\n\n",
+ 'If you combine this function with other constraints, results '.
+ 'which have no value or the specified values will be returned.',
+ '> any()'),
+ ),
+ );
+ }
+
+ public function loadResults() {
+ $results = array(
+ $this->newNoneFunction(),
+ );
+ return $this->filterResultsAgainstTokens($results);
+ }
+
+ protected function evaluateFunction($function, array $argv_list) {
+ $results = array();
+
+ foreach ($argv_list as $argv) {
+ $results[] = new PhabricatorQueryConstraint(
+ PhabricatorQueryConstraint::OPERATOR_NULL,
+ null);
+ }
+
+ return $results;
+ }
+
+ public function renderFunctionTokens($function, array $argv_list) {
+ $results = array();
+ foreach ($argv_list as $argv) {
+ $results[] = PhabricatorTypeaheadTokenView::newFromTypeaheadResult(
+ $this->newNoneFunction());
+ }
+ return $results;
+ }
+
+ private function newNoneFunction() {
+ $name = pht('No Value');
+ return $this->newFunctionResult()
+ ->setName($name.' none')
+ ->setDisplayName($name)
+ ->setIcon('fa-ban')
+ ->setPHID('none()')
+ ->setUnique(true)
+ ->addAttribute(pht('Select results with no value.'));
+ }
+
+}
diff --git a/src/infrastructure/customfield/editor/PhabricatorCustomFieldEditField.php b/src/infrastructure/customfield/editor/PhabricatorCustomFieldEditField.php
index 1153f82a3..27b8276c8 100644
--- a/src/infrastructure/customfield/editor/PhabricatorCustomFieldEditField.php
+++ b/src/infrastructure/customfield/editor/PhabricatorCustomFieldEditField.php
@@ -1,151 +1,185 @@
<?php
final class PhabricatorCustomFieldEditField
extends PhabricatorEditField {
private $customField;
private $httpParameterType;
private $conduitParameterType;
private $bulkParameterType;
+ private $commentAction;
public function setCustomField(PhabricatorCustomField $custom_field) {
$this->customField = $custom_field;
return $this;
}
public function getCustomField() {
return $this->customField;
}
public function setCustomFieldHTTPParameterType(
AphrontHTTPParameterType $type) {
$this->httpParameterType = $type;
return $this;
}
public function getCustomFieldHTTPParameterType() {
return $this->httpParameterType;
}
public function setCustomFieldConduitParameterType(
ConduitParameterType $type) {
$this->conduitParameterType = $type;
return $this;
}
public function getCustomFieldConduitParameterType() {
return $this->conduitParameterType;
}
public function setCustomFieldBulkParameterType(
BulkParameterType $type) {
$this->bulkParameterType = $type;
return $this;
}
public function getCustomFieldBulkParameterType() {
return $this->bulkParameterType;
}
+ public function setCustomFieldCommentAction(
+ PhabricatorEditEngineCommentAction $comment_action) {
+ $this->commentAction = $comment_action;
+ return $this;
+ }
+
+ public function getCustomFieldCommentAction() {
+ return $this->commentAction;
+ }
+
protected function buildControl() {
if ($this->getIsConduitOnly()) {
return null;
}
$field = $this->getCustomField();
$clone = clone $field;
$value = $this->getValue();
$clone->setValueFromApplicationTransactions($value);
return $clone->renderEditControl(array());
}
protected function newEditType() {
return id(new PhabricatorCustomFieldEditType())
->setCustomField($this->getCustomField());
}
public function getValueForTransaction() {
$value = $this->getValue();
$field = $this->getCustomField();
// Avoid changing the value of the field itself, since later calls would
// incorrectly reflect the new value.
$clone = clone $field;
$clone->setValueFromApplicationTransactions($value);
return $clone->getNewValueForApplicationTransactions();
}
+ protected function getValueForCommentAction($value) {
+ $field = $this->getCustomField();
+ $clone = clone $field;
+ $clone->setValueFromApplicationTransactions($value);
+
+ // TODO: This is somewhat bogus because only StandardCustomFields
+ // implement a getFieldValue() method -- not all CustomFields. Today,
+ // only StandardCustomFields can ever actually generate a comment action
+ // so we never reach this method with other field types.
+
+ return $clone->getFieldValue();
+ }
+
protected function getValueExistsInSubmit(AphrontRequest $request, $key) {
return true;
}
protected function getValueFromSubmit(AphrontRequest $request, $key) {
$field = $this->getCustomField();
$clone = clone $field;
$clone->readValueFromRequest($request);
return $clone->getNewValueForApplicationTransactions();
}
protected function newConduitEditTypes() {
$field = $this->getCustomField();
if (!$field->shouldAppearInConduitTransactions()) {
return array();
}
return parent::newConduitEditTypes();
}
protected function newHTTPParameterType() {
$type = $this->getCustomFieldHTTPParameterType();
if ($type) {
return clone $type;
}
return null;
}
+ protected function newCommentAction() {
+ $action = $this->getCustomFieldCommentAction();
+
+ if ($action) {
+ return clone $action;
+ }
+
+ return null;
+ }
+
protected function newConduitParameterType() {
$type = $this->getCustomFieldConduitParameterType();
if ($type) {
return clone $type;
}
return null;
}
protected function newBulkParameterType() {
$type = $this->getCustomFieldBulkParameterType();
if ($type) {
return clone $type;
}
return null;
}
public function getAllReadValueFromRequestKeys() {
$keys = array();
// NOTE: This piece of complexity is so we can expose a reasonable key in
// the UI ("custom.x") instead of a crufty internal key ("std:app:x").
// Perhaps we can simplify this some day.
// In the parent, this is just getKey(), but that returns a cumbersome
// key in EditFields. Use the simpler edit type key instead.
$keys[] = $this->getEditTypeKey();
foreach ($this->getAliases() as $alias) {
$keys[] = $alias;
}
return $keys;
}
}
diff --git a/src/infrastructure/customfield/field/PhabricatorCustomField.php b/src/infrastructure/customfield/field/PhabricatorCustomField.php
index c96f09f36..36db8f239 100644
--- a/src/infrastructure/customfield/field/PhabricatorCustomField.php
+++ b/src/infrastructure/customfield/field/PhabricatorCustomField.php
@@ -1,1561 +1,1625 @@
<?php
/**
* @task apps Building Applications with Custom Fields
* @task core Core Properties and Field Identity
* @task proxy Field Proxies
* @task context Contextual Data
* @task render Rendering Utilities
* @task storage Field Storage
* @task edit Integration with Edit Views
* @task view Integration with Property Views
* @task list Integration with List views
* @task appsearch Integration with ApplicationSearch
* @task appxaction Integration with ApplicationTransactions
* @task xactionmail Integration with Transaction Mail
* @task globalsearch Integration with Global Search
* @task herald Integration with Herald
*/
abstract class PhabricatorCustomField extends Phobject {
private $viewer;
private $object;
private $proxy;
const ROLE_APPLICATIONTRANSACTIONS = 'ApplicationTransactions';
const ROLE_TRANSACTIONMAIL = 'ApplicationTransactions.mail';
const ROLE_APPLICATIONSEARCH = 'ApplicationSearch';
const ROLE_STORAGE = 'storage';
const ROLE_DEFAULT = 'default';
const ROLE_EDIT = 'edit';
const ROLE_VIEW = 'view';
const ROLE_LIST = 'list';
const ROLE_GLOBALSEARCH = 'GlobalSearch';
const ROLE_CONDUIT = 'conduit';
const ROLE_HERALD = 'herald';
const ROLE_EDITENGINE = 'EditEngine';
const ROLE_HERALDACTION = 'herald.action';
+ const ROLE_EXPORT = 'export';
/* -( Building Applications with Custom Fields )--------------------------- */
/**
* @task apps
*/
public static function getObjectFields(
PhabricatorCustomFieldInterface $object,
$role) {
try {
$attachment = $object->getCustomFields();
} catch (PhabricatorDataNotAttachedException $ex) {
$attachment = new PhabricatorCustomFieldAttachment();
$object->attachCustomFields($attachment);
}
try {
$field_list = $attachment->getCustomFieldList($role);
} catch (PhabricatorCustomFieldNotAttachedException $ex) {
$base_class = $object->getCustomFieldBaseClass();
$spec = $object->getCustomFieldSpecificationForRole($role);
if (!is_array($spec)) {
throw new Exception(
pht(
"Expected an array from %s for object of class '%s'.",
'getCustomFieldSpecificationForRole()',
get_class($object)));
}
$fields = self::buildFieldList(
$base_class,
$spec,
$object);
foreach ($fields as $key => $field) {
if (!$field->shouldEnableForRole($role)) {
unset($fields[$key]);
}
}
foreach ($fields as $field) {
$field->setObject($object);
}
$field_list = new PhabricatorCustomFieldList($fields);
$attachment->addCustomFieldList($role, $field_list);
}
return $field_list;
}
/**
* @task apps
*/
public static function getObjectField(
PhabricatorCustomFieldInterface $object,
$role,
$field_key) {
$fields = self::getObjectFields($object, $role)->getFields();
return idx($fields, $field_key);
}
/**
* @task apps
*/
public static function buildFieldList(
$base_class,
array $spec,
$object,
array $options = array()) {
$field_objects = id(new PhutilClassMapQuery())
->setAncestorClass($base_class)
->execute();
$fields = array();
foreach ($field_objects as $field_object) {
$field_object = clone $field_object;
foreach ($field_object->createFields($object) as $field) {
$key = $field->getFieldKey();
if (isset($fields[$key])) {
throw new Exception(
pht(
"Both '%s' and '%s' define a custom field with ".
"field key '%s'. Field keys must be unique.",
get_class($fields[$key]),
get_class($field),
$key));
}
$fields[$key] = $field;
}
}
foreach ($fields as $key => $field) {
if (!$field->isFieldEnabled()) {
unset($fields[$key]);
}
}
$fields = array_select_keys($fields, array_keys($spec)) + $fields;
if (empty($options['withDisabled'])) {
foreach ($fields as $key => $field) {
if (isset($spec[$key]['disabled'])) {
$is_disabled = $spec[$key]['disabled'];
} else {
$is_disabled = $field->shouldDisableByDefault();
}
if ($is_disabled) {
if ($field->canDisableField()) {
unset($fields[$key]);
}
}
}
}
return $fields;
}
/* -( Core Properties and Field Identity )--------------------------------- */
/**
* Return a key which uniquely identifies this field, like
* "mycompany:dinosaur:count". Normally you should provide some level of
* namespacing to prevent collisions.
*
* @return string String which uniquely identifies this field.
* @task core
*/
public function getFieldKey() {
if ($this->proxy) {
return $this->proxy->getFieldKey();
}
throw new PhabricatorCustomFieldImplementationIncompleteException(
$this,
$field_key_is_incomplete = true);
}
public function getModernFieldKey() {
if ($this->proxy) {
return $this->proxy->getModernFieldKey();
}
return $this->getFieldKey();
}
/**
* Return a human-readable field name.
*
* @return string Human readable field name.
* @task core
*/
public function getFieldName() {
if ($this->proxy) {
return $this->proxy->getFieldName();
}
return $this->getModernFieldKey();
}
/**
* Return a short, human-readable description of the field's behavior. This
* provides more context to administrators when they are customizing fields.
*
* @return string|null Optional human-readable description.
* @task core
*/
public function getFieldDescription() {
if ($this->proxy) {
return $this->proxy->getFieldDescription();
}
return null;
}
/**
* Most field implementations are unique, in that one class corresponds to
* one field. However, some field implementations are general and a single
* implementation may drive several fields.
*
* For general implementations, the general field implementation can return
* multiple field instances here.
*
* @param object The object to create fields for.
* @return list<PhabricatorCustomField> List of fields.
* @task core
*/
public function createFields($object) {
return array($this);
}
/**
* You can return `false` here if the field should not be enabled for any
* role. For example, it might depend on something (like an application or
* library) which isn't installed, or might have some global configuration
* which allows it to be disabled.
*
* @return bool False to completely disable this field for all roles.
* @task core
*/
public function isFieldEnabled() {
if ($this->proxy) {
return $this->proxy->isFieldEnabled();
}
return true;
}
/**
* Low level selector for field availability. Fields can appear in different
* roles (like an edit view, a list view, etc.), but not every field needs
* to appear everywhere. Fields that are disabled in a role won't appear in
* that context within applications.
*
* Normally, you do not need to override this method. Instead, override the
* methods specific to roles you want to enable. For example, implement
* @{method:shouldUseStorage()} to activate the `'storage'` role.
*
* @return bool True to enable the field for the given role.
* @task core
*/
public function shouldEnableForRole($role) {
// NOTE: All of these calls proxy individually, so we don't need to
// proxy this call as a whole.
switch ($role) {
case self::ROLE_APPLICATIONTRANSACTIONS:
return $this->shouldAppearInApplicationTransactions();
case self::ROLE_APPLICATIONSEARCH:
return $this->shouldAppearInApplicationSearch();
case self::ROLE_STORAGE:
return $this->shouldUseStorage();
case self::ROLE_EDIT:
return $this->shouldAppearInEditView();
case self::ROLE_VIEW:
return $this->shouldAppearInPropertyView();
case self::ROLE_LIST:
return $this->shouldAppearInListView();
case self::ROLE_GLOBALSEARCH:
return $this->shouldAppearInGlobalSearch();
case self::ROLE_CONDUIT:
return $this->shouldAppearInConduitDictionary();
case self::ROLE_TRANSACTIONMAIL:
return $this->shouldAppearInTransactionMail();
case self::ROLE_HERALD:
return $this->shouldAppearInHerald();
case self::ROLE_HERALDACTION:
return $this->shouldAppearInHeraldActions();
case self::ROLE_EDITENGINE:
return $this->shouldAppearInEditView() ||
$this->shouldAppearInEditEngine();
+ case self::ROLE_EXPORT:
+ return $this->shouldAppearInDataExport();
case self::ROLE_DEFAULT:
return true;
default:
throw new Exception(pht("Unknown field role '%s'!", $role));
}
}
/**
* Allow administrators to disable this field. Most fields should allow this,
* but some are fundamental to the behavior of the application and can be
* locked down to avoid chaos, disorder, and the decline of civilization.
*
* @return bool False to prevent this field from being disabled through
* configuration.
* @task core
*/
public function canDisableField() {
return true;
}
public function shouldDisableByDefault() {
return false;
}
/**
* Return an index string which uniquely identifies this field.
*
* @return string Index string which uniquely identifies this field.
* @task core
*/
final public function getFieldIndex() {
return PhabricatorHash::digestForIndex($this->getFieldKey());
}
/* -( Field Proxies )------------------------------------------------------ */
/**
* Proxies allow a field to use some other field's implementation for most
* of their behavior while still subclassing an application field. When a
* proxy is set for a field with @{method:setProxy}, all of its methods will
* call through to the proxy by default.
*
* This is most commonly used to implement configuration-driven custom fields
* using @{class:PhabricatorStandardCustomField}.
*
* This method must be overridden to return `true` before a field can accept
* proxies.
*
* @return bool True if you can @{method:setProxy} this field.
* @task proxy
*/
public function canSetProxy() {
if ($this instanceof PhabricatorStandardCustomFieldInterface) {
return true;
}
return false;
}
/**
* Set the proxy implementation for this field. See @{method:canSetProxy} for
* discussion of field proxies.
*
* @param PhabricatorCustomField Field implementation.
* @return this
*/
final public function setProxy(PhabricatorCustomField $proxy) {
if (!$this->canSetProxy()) {
throw new PhabricatorCustomFieldNotProxyException($this);
}
$this->proxy = $proxy;
return $this;
}
/**
* Get the field's proxy implementation, if any. For discussion, see
* @{method:canSetProxy}.
*
* @return PhabricatorCustomField|null Proxy field, if one is set.
*/
final public function getProxy() {
return $this->proxy;
}
/* -( Contextual Data )---------------------------------------------------- */
/**
* Sets the object this field belongs to.
*
* @param PhabricatorCustomFieldInterface The object this field belongs to.
* @return this
* @task context
*/
final public function setObject(PhabricatorCustomFieldInterface $object) {
if ($this->proxy) {
$this->proxy->setObject($object);
return $this;
}
$this->object = $object;
$this->didSetObject($object);
return $this;
}
/**
* Read object data into local field storage, if applicable.
*
* @param PhabricatorCustomFieldInterface The object this field belongs to.
* @return this
* @task context
*/
public function readValueFromObject(PhabricatorCustomFieldInterface $object) {
if ($this->proxy) {
$this->proxy->readValueFromObject($object);
}
return $this;
}
/**
* Get the object this field belongs to.
*
* @return PhabricatorCustomFieldInterface The object this field belongs to.
* @task context
*/
final public function getObject() {
if ($this->proxy) {
return $this->proxy->getObject();
}
return $this->object;
}
/**
* This is a hook, primarily for subclasses to load object data.
*
* @return PhabricatorCustomFieldInterface The object this field belongs to.
* @return void
*/
protected function didSetObject(PhabricatorCustomFieldInterface $object) {
return;
}
/**
* @task context
*/
final public function setViewer(PhabricatorUser $viewer) {
if ($this->proxy) {
$this->proxy->setViewer($viewer);
return $this;
}
$this->viewer = $viewer;
return $this;
}
/**
* @task context
*/
final public function getViewer() {
if ($this->proxy) {
return $this->proxy->getViewer();
}
return $this->viewer;
}
/**
* @task context
*/
final protected function requireViewer() {
if ($this->proxy) {
return $this->proxy->requireViewer();
}
if (!$this->viewer) {
throw new PhabricatorCustomFieldDataNotAvailableException($this);
}
return $this->viewer;
}
/* -( Rendering Utilities )------------------------------------------------ */
/**
* @task render
*/
protected function renderHandleList(array $handles) {
if (!$handles) {
return null;
}
$out = array();
foreach ($handles as $handle) {
$out[] = $handle->renderHovercardLink();
}
return phutil_implode_html(phutil_tag('br'), $out);
}
/* -( Storage )------------------------------------------------------------ */
/**
* Return true to use field storage.
*
* Fields which can be edited by the user will most commonly use storage,
* while some other types of fields (for instance, those which just display
* information in some stylized way) may not. Many builtin fields do not use
* storage because their data is available on the object itself.
*
* If you implement this, you must also implement @{method:getValueForStorage}
* and @{method:setValueFromStorage}.
*
* @return bool True to use storage.
* @task storage
*/
public function shouldUseStorage() {
if ($this->proxy) {
return $this->proxy->shouldUseStorage();
}
return false;
}
/**
* Return a new, empty storage object. This should be a subclass of
* @{class:PhabricatorCustomFieldStorage} which is bound to the application's
* database.
*
* @return PhabricatorCustomFieldStorage New empty storage object.
* @task storage
*/
public function newStorageObject() {
// NOTE: This intentionally isn't proxied, to avoid call cycles.
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* Return a serialized representation of the field value, appropriate for
* storing in auxiliary field storage. You must implement this method if
* you implement @{method:shouldUseStorage}.
*
* If the field value is a scalar, it can be returned unmodiifed. If not,
* it should be serialized (for example, using JSON).
*
* @return string Serialized field value.
* @task storage
*/
public function getValueForStorage() {
if ($this->proxy) {
return $this->proxy->getValueForStorage();
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* Set the field's value given a serialized storage value. This is called
* when the field is loaded; if no data is available, the value will be
* null. You must implement this method if you implement
* @{method:shouldUseStorage}.
*
* Usually, the value can be loaded directly. If it isn't a scalar, you'll
* need to undo whatever serialization you applied in
* @{method:getValueForStorage}.
*
* @param string|null Serialized field representation (from
* @{method:getValueForStorage}) or null if no value has
* ever been stored.
* @return this
* @task storage
*/
public function setValueFromStorage($value) {
if ($this->proxy) {
return $this->proxy->setValueFromStorage($value);
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
public function didSetValueFromStorage() {
if ($this->proxy) {
return $this->proxy->didSetValueFromStorage();
}
return $this;
}
/* -( ApplicationSearch )-------------------------------------------------- */
/**
* Appearing in ApplicationSearch allows a field to be indexed and searched
* for.
*
* @return bool True to appear in ApplicationSearch.
* @task appsearch
*/
public function shouldAppearInApplicationSearch() {
if ($this->proxy) {
return $this->proxy->shouldAppearInApplicationSearch();
}
return false;
}
/**
* Return one or more indexes which this field can meaningfully query against
* to implement ApplicationSearch.
*
* Normally, you should build these using @{method:newStringIndex} and
* @{method:newNumericIndex}. For example, if a field holds a numeric value
* it might return a single numeric index:
*
* return array($this->newNumericIndex($this->getValue()));
*
* If a field holds a more complex value (like a list of users), it might
* return several string indexes:
*
* $indexes = array();
* foreach ($this->getValue() as $phid) {
* $indexes[] = $this->newStringIndex($phid);
* }
* return $indexes;
*
* @return list<PhabricatorCustomFieldIndexStorage> List of indexes.
* @task appsearch
*/
public function buildFieldIndexes() {
if ($this->proxy) {
return $this->proxy->buildFieldIndexes();
}
return array();
}
/**
* Return an index against which this field can be meaningfully ordered
* against to implement ApplicationSearch.
*
* This should be a single index, normally built using
* @{method:newStringIndex} and @{method:newNumericIndex}.
*
* The value of the index is not used.
*
* Return null from this method if the field can not be ordered.
*
* @return PhabricatorCustomFieldIndexStorage A single index to order by.
* @task appsearch
*/
public function buildOrderIndex() {
if ($this->proxy) {
return $this->proxy->buildOrderIndex();
}
return null;
}
/**
* Build a new empty storage object for storing string indexes. Normally,
* this should be a concrete subclass of
* @{class:PhabricatorCustomFieldStringIndexStorage}.
*
* @return PhabricatorCustomFieldStringIndexStorage Storage object.
* @task appsearch
*/
protected function newStringIndexStorage() {
// NOTE: This intentionally isn't proxied, to avoid call cycles.
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* Build a new empty storage object for storing string indexes. Normally,
* this should be a concrete subclass of
* @{class:PhabricatorCustomFieldStringIndexStorage}.
*
* @return PhabricatorCustomFieldStringIndexStorage Storage object.
* @task appsearch
*/
protected function newNumericIndexStorage() {
// NOTE: This intentionally isn't proxied, to avoid call cycles.
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* Build and populate storage for a string index.
*
* @param string String to index.
* @return PhabricatorCustomFieldStringIndexStorage Populated storage.
* @task appsearch
*/
protected function newStringIndex($value) {
if ($this->proxy) {
return $this->proxy->newStringIndex();
}
$key = $this->getFieldIndex();
return $this->newStringIndexStorage()
->setIndexKey($key)
->setIndexValue($value);
}
/**
* Build and populate storage for a numeric index.
*
* @param string Numeric value to index.
* @return PhabricatorCustomFieldNumericIndexStorage Populated storage.
* @task appsearch
*/
protected function newNumericIndex($value) {
if ($this->proxy) {
return $this->proxy->newNumericIndex();
}
$key = $this->getFieldIndex();
return $this->newNumericIndexStorage()
->setIndexKey($key)
->setIndexValue($value);
}
/**
* Read a query value from a request, for storage in a saved query. Normally,
* this method should, e.g., read a string out of the request.
*
* @param PhabricatorApplicationSearchEngine Engine building the query.
* @param AphrontRequest Request to read from.
* @return wild
* @task appsearch
*/
public function readApplicationSearchValueFromRequest(
PhabricatorApplicationSearchEngine $engine,
AphrontRequest $request) {
if ($this->proxy) {
return $this->proxy->readApplicationSearchValueFromRequest(
$engine,
$request);
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* Constrain a query, given a field value. Generally, this method should
* use `with...()` methods to apply filters or other constraints to the
* query.
*
* @param PhabricatorApplicationSearchEngine Engine executing the query.
* @param PhabricatorCursorPagedPolicyAwareQuery Query to constrain.
* @param wild Constraint provided by the user.
* @return void
* @task appsearch
*/
public function applyApplicationSearchConstraintToQuery(
PhabricatorApplicationSearchEngine $engine,
PhabricatorCursorPagedPolicyAwareQuery $query,
$value) {
if ($this->proxy) {
return $this->proxy->applyApplicationSearchConstraintToQuery(
$engine,
$query,
$value);
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* Append search controls to the interface.
*
* @param PhabricatorApplicationSearchEngine Engine constructing the form.
* @param AphrontFormView The form to update.
* @param wild Value from the saved query.
* @return void
* @task appsearch
*/
public function appendToApplicationSearchForm(
PhabricatorApplicationSearchEngine $engine,
AphrontFormView $form,
$value) {
if ($this->proxy) {
return $this->proxy->appendToApplicationSearchForm(
$engine,
$form,
$value);
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/* -( ApplicationTransactions )-------------------------------------------- */
/**
* Appearing in ApplicationTrasactions allows a field to be edited using
* standard workflows.
*
* @return bool True to appear in ApplicationTransactions.
* @task appxaction
*/
public function shouldAppearInApplicationTransactions() {
if ($this->proxy) {
return $this->proxy->shouldAppearInApplicationTransactions();
}
return false;
}
/**
* @task appxaction
*/
public function getApplicationTransactionType() {
if ($this->proxy) {
return $this->proxy->getApplicationTransactionType();
}
return PhabricatorTransactions::TYPE_CUSTOMFIELD;
}
/**
* @task appxaction
*/
public function getApplicationTransactionMetadata() {
if ($this->proxy) {
return $this->proxy->getApplicationTransactionMetadata();
}
return array();
}
/**
* @task appxaction
*/
public function getOldValueForApplicationTransactions() {
if ($this->proxy) {
return $this->proxy->getOldValueForApplicationTransactions();
}
return $this->getValueForStorage();
}
/**
* @task appxaction
*/
public function getNewValueForApplicationTransactions() {
if ($this->proxy) {
return $this->proxy->getNewValueForApplicationTransactions();
}
return $this->getValueForStorage();
}
/**
* @task appxaction
*/
public function setValueFromApplicationTransactions($value) {
if ($this->proxy) {
return $this->proxy->setValueFromApplicationTransactions($value);
}
return $this->setValueFromStorage($value);
}
/**
* @task appxaction
*/
public function getNewValueFromApplicationTransactions(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->getNewValueFromApplicationTransactions($xaction);
}
return $xaction->getNewValue();
}
/**
* @task appxaction
*/
public function getApplicationTransactionHasEffect(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->getApplicationTransactionHasEffect($xaction);
}
return ($xaction->getOldValue() !== $xaction->getNewValue());
}
/**
* @task appxaction
*/
public function applyApplicationTransactionInternalEffects(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->applyApplicationTransactionInternalEffects($xaction);
}
return;
}
/**
* @task appxaction
*/
public function getApplicationTransactionRemarkupBlocks(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->getApplicationTransactionRemarkupBlocks($xaction);
}
return array();
}
/**
* @task appxaction
*/
public function applyApplicationTransactionExternalEffects(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->applyApplicationTransactionExternalEffects($xaction);
}
if (!$this->shouldEnableForRole(self::ROLE_STORAGE)) {
return;
}
$this->setValueFromApplicationTransactions($xaction->getNewValue());
$value = $this->getValueForStorage();
$table = $this->newStorageObject();
$conn_w = $table->establishConnection('w');
if ($value === null) {
queryfx(
$conn_w,
'DELETE FROM %T WHERE objectPHID = %s AND fieldIndex = %s',
$table->getTableName(),
$this->getObject()->getPHID(),
$this->getFieldIndex());
} else {
queryfx(
$conn_w,
'INSERT INTO %T (objectPHID, fieldIndex, fieldValue)
VALUES (%s, %s, %s)
ON DUPLICATE KEY UPDATE fieldValue = VALUES(fieldValue)',
$table->getTableName(),
$this->getObject()->getPHID(),
$this->getFieldIndex(),
$value);
}
return;
}
/**
* Validate transactions for an object. This allows you to raise an error
* when a transaction would set a field to an invalid value, or when a field
* is required but no transactions provide value.
*
* @param PhabricatorLiskDAO Editor applying the transactions.
* @param string Transaction type. This type is always
* `PhabricatorTransactions::TYPE_CUSTOMFIELD`, it is provided for
* convenience when constructing exceptions.
* @param list<PhabricatorApplicationTransaction> Transactions being applied,
* which may be empty if this field is not being edited.
* @return list<PhabricatorApplicationTransactionValidationError> Validation
* errors.
*
* @task appxaction
*/
public function validateApplicationTransactions(
PhabricatorApplicationTransactionEditor $editor,
$type,
array $xactions) {
if ($this->proxy) {
return $this->proxy->validateApplicationTransactions(
$editor,
$type,
$xactions);
}
return array();
}
public function getApplicationTransactionTitle(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->getApplicationTransactionTitle(
$xaction);
}
$author_phid = $xaction->getAuthorPHID();
return pht(
'%s updated this object.',
$xaction->renderHandleLink($author_phid));
}
public function getApplicationTransactionTitleForFeed(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->getApplicationTransactionTitleForFeed(
$xaction);
}
$author_phid = $xaction->getAuthorPHID();
$object_phid = $xaction->getObjectPHID();
return pht(
'%s updated %s.',
$xaction->renderHandleLink($author_phid),
$xaction->renderHandleLink($object_phid));
}
public function getApplicationTransactionHasChangeDetails(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->getApplicationTransactionHasChangeDetails(
$xaction);
}
return false;
}
public function getApplicationTransactionChangeDetails(
PhabricatorApplicationTransaction $xaction,
PhabricatorUser $viewer) {
if ($this->proxy) {
return $this->proxy->getApplicationTransactionChangeDetails(
$xaction,
$viewer);
}
return null;
}
public function getApplicationTransactionRequiredHandlePHIDs(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->getApplicationTransactionRequiredHandlePHIDs(
$xaction);
}
return array();
}
public function shouldHideInApplicationTransactions(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->shouldHideInApplicationTransactions($xaction);
}
return false;
}
/* -( Transaction Mail )--------------------------------------------------- */
/**
* @task xactionmail
*/
public function shouldAppearInTransactionMail() {
if ($this->proxy) {
return $this->proxy->shouldAppearInTransactionMail();
}
return false;
}
/**
* @task xactionmail
*/
public function updateTransactionMailBody(
PhabricatorMetaMTAMailBody $body,
PhabricatorApplicationTransactionEditor $editor,
array $xactions) {
if ($this->proxy) {
return $this->proxy->updateTransactionMailBody($body, $editor, $xactions);
}
return;
}
/* -( Edit View )---------------------------------------------------------- */
public function getEditEngineFields(PhabricatorEditEngine $engine) {
$field = $this->newStandardEditField();
return array(
$field,
);
}
protected function newEditField() {
$field = id(new PhabricatorCustomFieldEditField())
->setCustomField($this);
$http_type = $this->getHTTPParameterType();
if ($http_type) {
$field->setCustomFieldHTTPParameterType($http_type);
}
$conduit_type = $this->getConduitEditParameterType();
if ($conduit_type) {
$field->setCustomFieldConduitParameterType($conduit_type);
}
$bulk_type = $this->getBulkParameterType();
if ($bulk_type) {
$field->setCustomFieldBulkParameterType($bulk_type);
}
+ $comment_action = $this->getCommentAction();
+ if ($comment_action) {
+ $field
+ ->setCustomFieldCommentAction($comment_action)
+ ->setCommentActionLabel(
+ pht(
+ 'Change %s',
+ $this->getFieldName()));
+ }
+
return $field;
}
protected function newStandardEditField() {
if ($this->proxy) {
return $this->proxy->newStandardEditField();
}
if (!$this->shouldAppearInEditView()) {
$conduit_only = true;
} else {
$conduit_only = false;
}
$bulk_label = $this->getBulkEditLabel();
return $this->newEditField()
->setKey($this->getFieldKey())
->setEditTypeKey($this->getModernFieldKey())
->setLabel($this->getFieldName())
->setBulkEditLabel($bulk_label)
->setDescription($this->getFieldDescription())
->setTransactionType($this->getApplicationTransactionType())
->setIsConduitOnly($conduit_only)
->setValue($this->getNewValueForApplicationTransactions());
}
protected function getBulkEditLabel() {
if ($this->proxy) {
return $this->proxy->getBulkEditLabel();
}
return pht('Set "%s" to', $this->getFieldName());
}
public function getBulkParameterType() {
return $this->newBulkParameterType();
}
protected function newBulkParameterType() {
if ($this->proxy) {
return $this->proxy->newBulkParameterType();
}
return null;
}
protected function getHTTPParameterType() {
if ($this->proxy) {
return $this->proxy->getHTTPParameterType();
}
return null;
}
/**
* @task edit
*/
public function shouldAppearInEditView() {
if ($this->proxy) {
return $this->proxy->shouldAppearInEditView();
}
return false;
}
/**
* @task edit
*/
public function shouldAppearInEditEngine() {
if ($this->proxy) {
return $this->proxy->shouldAppearInEditEngine();
}
return false;
}
/**
* @task edit
*/
public function readValueFromRequest(AphrontRequest $request) {
if ($this->proxy) {
return $this->proxy->readValueFromRequest($request);
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* @task edit
*/
public function getRequiredHandlePHIDsForEdit() {
if ($this->proxy) {
return $this->proxy->getRequiredHandlePHIDsForEdit();
}
return array();
}
/**
* @task edit
*/
public function getInstructionsForEdit() {
if ($this->proxy) {
return $this->proxy->getInstructionsForEdit();
}
return null;
}
/**
* @task edit
*/
public function renderEditControl(array $handles) {
if ($this->proxy) {
return $this->proxy->renderEditControl($handles);
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/* -( Property View )------------------------------------------------------ */
/**
* @task view
*/
public function shouldAppearInPropertyView() {
if ($this->proxy) {
return $this->proxy->shouldAppearInPropertyView();
}
return false;
}
/**
* @task view
*/
public function renderPropertyViewLabel() {
if ($this->proxy) {
return $this->proxy->renderPropertyViewLabel();
}
return $this->getFieldName();
}
/**
* @task view
*/
public function renderPropertyViewValue(array $handles) {
if ($this->proxy) {
return $this->proxy->renderPropertyViewValue($handles);
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* @task view
*/
public function getStyleForPropertyView() {
if ($this->proxy) {
return $this->proxy->getStyleForPropertyView();
}
return 'property';
}
/**
* @task view
*/
public function getIconForPropertyView() {
if ($this->proxy) {
return $this->proxy->getIconForPropertyView();
}
return null;
}
/**
* @task view
*/
public function getRequiredHandlePHIDsForPropertyView() {
if ($this->proxy) {
return $this->proxy->getRequiredHandlePHIDsForPropertyView();
}
return array();
}
/* -( List View )---------------------------------------------------------- */
/**
* @task list
*/
public function shouldAppearInListView() {
if ($this->proxy) {
return $this->proxy->shouldAppearInListView();
}
return false;
}
/**
* @task list
*/
public function renderOnListItem(PHUIObjectItemView $view) {
if ($this->proxy) {
return $this->proxy->renderOnListItem($view);
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/* -( Global Search )------------------------------------------------------ */
/**
* @task globalsearch
*/
public function shouldAppearInGlobalSearch() {
if ($this->proxy) {
return $this->proxy->shouldAppearInGlobalSearch();
}
return false;
}
/**
* @task globalsearch
*/
public function updateAbstractDocument(
PhabricatorSearchAbstractDocument $document) {
if ($this->proxy) {
return $this->proxy->updateAbstractDocument($document);
}
return $document;
}
+/* -( Data Export )-------------------------------------------------------- */
+
+
+ public function shouldAppearInDataExport() {
+ if ($this->proxy) {
+ return $this->proxy->shouldAppearInDataExport();
+ }
+
+ try {
+ $this->newExportFieldType();
+ return true;
+ } catch (PhabricatorCustomFieldImplementationIncompleteException $ex) {
+ return false;
+ }
+ }
+
+ public function newExportField() {
+ if ($this->proxy) {
+ return $this->proxy->newExportField();
+ }
+
+ return $this->newExportFieldType()
+ ->setLabel($this->getFieldName());
+ }
+
+ public function newExportData() {
+ if ($this->proxy) {
+ return $this->proxy->newExportData();
+ }
+ throw new PhabricatorCustomFieldImplementationIncompleteException($this);
+ }
+
+ protected function newExportFieldType() {
+ if ($this->proxy) {
+ return $this->proxy->newExportFieldType();
+ }
+ throw new PhabricatorCustomFieldImplementationIncompleteException($this);
+ }
+
+
/* -( Conduit )------------------------------------------------------------ */
/**
* @task conduit
*/
public function shouldAppearInConduitDictionary() {
if ($this->proxy) {
return $this->proxy->shouldAppearInConduitDictionary();
}
return false;
}
/**
* @task conduit
*/
public function getConduitDictionaryValue() {
if ($this->proxy) {
return $this->proxy->getConduitDictionaryValue();
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
public function shouldAppearInConduitTransactions() {
if ($this->proxy) {
return $this->proxy->shouldAppearInConduitDictionary();
}
return false;
}
public function getConduitSearchParameterType() {
return $this->newConduitSearchParameterType();
}
protected function newConduitSearchParameterType() {
if ($this->proxy) {
return $this->proxy->newConduitSearchParameterType();
}
return null;
}
public function getConduitEditParameterType() {
return $this->newConduitEditParameterType();
}
protected function newConduitEditParameterType() {
if ($this->proxy) {
return $this->proxy->newConduitEditParameterType();
}
return null;
}
+ public function getCommentAction() {
+ return $this->newCommentAction();
+ }
+
+ protected function newCommentAction() {
+ if ($this->proxy) {
+ return $this->proxy->newCommentAction();
+ }
+ return null;
+ }
+
/* -( Herald )------------------------------------------------------------- */
/**
* Return `true` to make this field available in Herald.
*
* @return bool True to expose the field in Herald.
* @task herald
*/
public function shouldAppearInHerald() {
if ($this->proxy) {
return $this->proxy->shouldAppearInHerald();
}
return false;
}
/**
* Get the name of the field in Herald. By default, this uses the
* normal field name.
*
* @return string Herald field name.
* @task herald
*/
public function getHeraldFieldName() {
if ($this->proxy) {
return $this->proxy->getHeraldFieldName();
}
return $this->getFieldName();
}
/**
* Get the field value for evaluation by Herald.
*
* @return wild Field value.
* @task herald
*/
public function getHeraldFieldValue() {
if ($this->proxy) {
return $this->proxy->getHeraldFieldValue();
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* Get the available conditions for this field in Herald.
*
* @return list<const> List of Herald condition constants.
* @task herald
*/
public function getHeraldFieldConditions() {
if ($this->proxy) {
return $this->proxy->getHeraldFieldConditions();
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* Get the Herald value type for the given condition.
*
* @param const Herald condition constant.
* @return const|null Herald value type, or null to use the default.
* @task herald
*/
public function getHeraldFieldValueType($condition) {
if ($this->proxy) {
return $this->proxy->getHeraldFieldValueType($condition);
}
return null;
}
public function getHeraldFieldStandardType() {
if ($this->proxy) {
return $this->proxy->getHeraldFieldStandardType();
}
return null;
}
public function getHeraldDatasource() {
if ($this->proxy) {
return $this->proxy->getHeraldDatasource();
}
return null;
}
public function shouldAppearInHeraldActions() {
if ($this->proxy) {
return $this->proxy->shouldAppearInHeraldActions();
}
return false;
}
public function getHeraldActionName() {
if ($this->proxy) {
return $this->proxy->getHeraldActionName();
}
return null;
}
public function getHeraldActionStandardType() {
if ($this->proxy) {
return $this->proxy->getHeraldActionStandardType();
}
return null;
}
public function getHeraldActionDescription($value) {
if ($this->proxy) {
return $this->proxy->getHeraldActionDescription($value);
}
return null;
}
public function getHeraldActionEffectDescription($value) {
if ($this->proxy) {
return $this->proxy->getHeraldActionEffectDescription($value);
}
return null;
}
public function getHeraldActionDatasource() {
if ($this->proxy) {
return $this->proxy->getHeraldActionDatasource();
}
return null;
}
}
diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php
index f617d3a84..5bd6256b7 100644
--- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php
+++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php
@@ -1,500 +1,503 @@
<?php
abstract class PhabricatorStandardCustomField
extends PhabricatorCustomField {
private $rawKey;
private $fieldKey;
private $fieldName;
private $fieldValue;
private $fieldDescription;
private $fieldConfig;
private $applicationField;
private $strings = array();
private $caption;
private $fieldError;
private $required;
private $default;
private $isCopyable;
private $hasStorageValue;
private $isBuiltin;
abstract public function getFieldType();
public static function buildStandardFields(
PhabricatorCustomField $template,
array $config,
$builtin = false) {
$types = id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getFieldType')
->execute();
$fields = array();
foreach ($config as $key => $value) {
$type = idx($value, 'type', 'text');
if (empty($types[$type])) {
// TODO: We should have better typechecking somewhere, and then make
// this more serious.
continue;
}
$namespace = $template->getStandardCustomFieldNamespace();
$full_key = "std:{$namespace}:{$key}";
$template = clone $template;
$standard = id(clone $types[$type])
->setRawStandardFieldKey($key)
->setFieldKey($full_key)
->setFieldConfig($value)
->setApplicationField($template);
if ($builtin) {
$standard->setIsBuiltin(true);
}
$field = $template->setProxy($standard);
$fields[] = $field;
}
return $fields;
}
public function setApplicationField(
PhabricatorStandardCustomFieldInterface $application_field) {
$this->applicationField = $application_field;
return $this;
}
public function getApplicationField() {
return $this->applicationField;
}
public function setFieldName($name) {
$this->fieldName = $name;
return $this;
}
public function getFieldValue() {
return $this->fieldValue;
}
public function setFieldValue($value) {
$this->fieldValue = $value;
return $this;
}
public function setCaption($caption) {
$this->caption = $caption;
return $this;
}
public function getCaption() {
return $this->caption;
}
public function setFieldDescription($description) {
$this->fieldDescription = $description;
return $this;
}
public function setIsBuiltin($is_builtin) {
$this->isBuiltin = $is_builtin;
return $this;
}
public function getIsBuiltin() {
return $this->isBuiltin;
}
public function setFieldConfig(array $config) {
foreach ($config as $key => $value) {
switch ($key) {
case 'name':
$this->setFieldName($value);
break;
case 'description':
$this->setFieldDescription($value);
break;
case 'strings':
$this->setStrings($value);
break;
case 'caption':
$this->setCaption($value);
break;
case 'required':
if ($value) {
$this->setRequired($value);
$this->setFieldError(true);
}
break;
case 'default':
$this->setFieldValue($value);
break;
case 'copy':
$this->setIsCopyable($value);
break;
case 'type':
// We set this earlier on.
break;
}
}
$this->fieldConfig = $config;
return $this;
}
public function getFieldConfigValue($key, $default = null) {
return idx($this->fieldConfig, $key, $default);
}
public function setFieldError($field_error) {
$this->fieldError = $field_error;
return $this;
}
public function getFieldError() {
return $this->fieldError;
}
public function setRequired($required) {
$this->required = $required;
return $this;
}
public function getRequired() {
return $this->required;
}
public function setRawStandardFieldKey($raw_key) {
$this->rawKey = $raw_key;
return $this;
}
public function getRawStandardFieldKey() {
return $this->rawKey;
}
/* -( PhabricatorCustomField )--------------------------------------------- */
public function setFieldKey($field_key) {
$this->fieldKey = $field_key;
return $this;
}
public function getFieldKey() {
return $this->fieldKey;
}
public function getFieldName() {
return coalesce($this->fieldName, parent::getFieldName());
}
public function getFieldDescription() {
return coalesce($this->fieldDescription, parent::getFieldDescription());
}
public function setStrings(array $strings) {
$this->strings = $strings;
return;
}
public function getString($key, $default = null) {
return idx($this->strings, $key, $default);
}
public function setIsCopyable($is_copyable) {
$this->isCopyable = $is_copyable;
return $this;
}
public function getIsCopyable() {
return $this->isCopyable;
}
public function shouldUseStorage() {
try {
$object = $this->newStorageObject();
return true;
} catch (PhabricatorCustomFieldImplementationIncompleteException $ex) {
return false;
}
}
public function getValueForStorage() {
return $this->getFieldValue();
}
public function setValueFromStorage($value) {
return $this->setFieldValue($value);
}
public function didSetValueFromStorage() {
$this->hasStorageValue = true;
return $this;
}
public function getOldValueForApplicationTransactions() {
if ($this->hasStorageValue) {
return $this->getValueForStorage();
} else {
return null;
}
}
public function shouldAppearInApplicationTransactions() {
return true;
}
public function shouldAppearInEditView() {
return $this->getFieldConfigValue('edit', true);
}
public function readValueFromRequest(AphrontRequest $request) {
$value = $request->getStr($this->getFieldKey());
if (!strlen($value)) {
$value = null;
}
$this->setFieldValue($value);
}
public function getInstructionsForEdit() {
return $this->getFieldConfigValue('instructions');
}
public function getPlaceholder() {
return $this->getFieldConfigValue('placeholder', null);
}
public function renderEditControl(array $handles) {
return id(new AphrontFormTextControl())
->setName($this->getFieldKey())
->setCaption($this->getCaption())
->setValue($this->getFieldValue())
->setError($this->getFieldError())
->setLabel($this->getFieldName())
->setPlaceholder($this->getPlaceholder());
}
public function newStorageObject() {
return $this->getApplicationField()->newStorageObject();
}
public function shouldAppearInPropertyView() {
return $this->getFieldConfigValue('view', true);
}
public function renderPropertyViewValue(array $handles) {
if (!strlen($this->getFieldValue())) {
return null;
}
return $this->getFieldValue();
}
public function shouldAppearInApplicationSearch() {
return $this->getFieldConfigValue('search', false);
}
protected function newStringIndexStorage() {
return $this->getApplicationField()->newStringIndexStorage();
}
protected function newNumericIndexStorage() {
return $this->getApplicationField()->newNumericIndexStorage();
}
public function buildFieldIndexes() {
return array();
}
public function buildOrderIndex() {
return null;
}
public function readApplicationSearchValueFromRequest(
PhabricatorApplicationSearchEngine $engine,
AphrontRequest $request) {
return;
}
public function applyApplicationSearchConstraintToQuery(
PhabricatorApplicationSearchEngine $engine,
PhabricatorCursorPagedPolicyAwareQuery $query,
$value) {
return;
}
public function appendToApplicationSearchForm(
PhabricatorApplicationSearchEngine $engine,
AphrontFormView $form,
$value) {
return;
}
public function validateApplicationTransactions(
PhabricatorApplicationTransactionEditor $editor,
$type,
array $xactions) {
$this->setFieldError(null);
$errors = parent::validateApplicationTransactions(
$editor,
$type,
$xactions);
if ($this->getRequired()) {
$value = $this->getOldValueForApplicationTransactions();
$transaction = null;
foreach ($xactions as $xaction) {
$value = $xaction->getNewValue();
if (!$this->isValueEmpty($value)) {
$transaction = $xaction;
break;
}
}
if ($this->isValueEmpty($value)) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Required'),
pht('%s is required.', $this->getFieldName()),
$transaction);
$error->setIsMissingFieldError(true);
$errors[] = $error;
$this->setFieldError(pht('Required'));
}
}
return $errors;
}
protected function isValueEmpty($value) {
if (is_array($value)) {
return empty($value);
}
return !strlen($value);
}
public function getApplicationTransactionTitle(
PhabricatorApplicationTransaction $xaction) {
$author_phid = $xaction->getAuthorPHID();
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
if (!$old) {
return pht(
'%s set %s to %s.',
$xaction->renderHandleLink($author_phid),
$this->getFieldName(),
$new);
} else if (!$new) {
return pht(
'%s removed %s.',
$xaction->renderHandleLink($author_phid),
$this->getFieldName());
} else {
return pht(
'%s changed %s from %s to %s.',
$xaction->renderHandleLink($author_phid),
$this->getFieldName(),
$old,
$new);
}
}
public function getApplicationTransactionTitleForFeed(
PhabricatorApplicationTransaction $xaction) {
$author_phid = $xaction->getAuthorPHID();
$object_phid = $xaction->getObjectPHID();
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
if (!$old) {
return pht(
'%s set %s to %s on %s.',
$xaction->renderHandleLink($author_phid),
$this->getFieldName(),
$new,
$xaction->renderHandleLink($object_phid));
} else if (!$new) {
return pht(
'%s removed %s on %s.',
$xaction->renderHandleLink($author_phid),
$this->getFieldName(),
$xaction->renderHandleLink($object_phid));
} else {
return pht(
'%s changed %s from %s to %s on %s.',
$xaction->renderHandleLink($author_phid),
$this->getFieldName(),
$old,
$new,
$xaction->renderHandleLink($object_phid));
}
}
public function getHeraldFieldValue() {
return $this->getFieldValue();
}
public function getFieldControlID($key = null) {
$key = coalesce($key, $this->getRawStandardFieldKey());
return 'std:control:'.$key;
}
public function shouldAppearInGlobalSearch() {
return $this->getFieldConfigValue('fulltext', false);
}
public function updateAbstractDocument(
PhabricatorSearchAbstractDocument $document) {
$field_key = $this->getFieldConfigValue('fulltext');
// If the caller or configuration didn't specify a valid field key,
// generate one automatically from the field index.
if (!is_string($field_key) || (strlen($field_key) != 4)) {
$field_key = '!'.substr($this->getFieldIndex(), 0, 3);
}
$field_value = $this->getFieldValue();
if (strlen($field_value)) {
$document->addField($field_key, $field_value);
}
}
protected function newStandardEditField() {
$short = $this->getModernFieldKey();
return parent::newStandardEditField()
->setEditTypeKey($short)
->setIsCopyable($this->getIsCopyable());
}
public function shouldAppearInConduitTransactions() {
return true;
}
public function shouldAppearInConduitDictionary() {
return true;
}
public function getModernFieldKey() {
if ($this->getIsBuiltin()) {
return $this->getRawStandardFieldKey();
} else {
return 'custom.'.$this->getRawStandardFieldKey();
}
}
public function getConduitDictionaryValue() {
return $this->getFieldValue();
}
+ public function newExportData() {
+ return $this->getFieldValue();
+ }
}
diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldInt.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldInt.php
index 4a3e45d75..f06c30d48 100644
--- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldInt.php
+++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldInt.php
@@ -1,127 +1,131 @@
<?php
final class PhabricatorStandardCustomFieldInt
extends PhabricatorStandardCustomField {
public function getFieldType() {
return 'int';
}
public function buildFieldIndexes() {
$indexes = array();
$value = $this->getFieldValue();
if (strlen($value)) {
$indexes[] = $this->newNumericIndex((int)$value);
}
return $indexes;
}
public function buildOrderIndex() {
return $this->newNumericIndex(0);
}
public function getValueForStorage() {
$value = $this->getFieldValue();
if (strlen($value)) {
return $value;
} else {
return null;
}
}
public function setValueFromStorage($value) {
if (strlen($value)) {
$value = (int)$value;
} else {
$value = null;
}
return $this->setFieldValue($value);
}
public function readApplicationSearchValueFromRequest(
PhabricatorApplicationSearchEngine $engine,
AphrontRequest $request) {
return $request->getStr($this->getFieldKey());
}
public function applyApplicationSearchConstraintToQuery(
PhabricatorApplicationSearchEngine $engine,
PhabricatorCursorPagedPolicyAwareQuery $query,
$value) {
if (strlen($value)) {
$query->withApplicationSearchContainsConstraint(
$this->newNumericIndex(null),
$value);
}
}
public function appendToApplicationSearchForm(
PhabricatorApplicationSearchEngine $engine,
AphrontFormView $form,
$value) {
$form->appendChild(
id(new AphrontFormTextControl())
->setLabel($this->getFieldName())
->setName($this->getFieldKey())
->setValue($value));
}
public function validateApplicationTransactions(
PhabricatorApplicationTransactionEditor $editor,
$type,
array $xactions) {
$errors = parent::validateApplicationTransactions(
$editor,
$type,
$xactions);
foreach ($xactions as $xaction) {
$value = $xaction->getNewValue();
if (strlen($value)) {
if (!preg_match('/^-?\d+/', $value)) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht('%s must be an integer.', $this->getFieldName()),
$xaction);
$this->setFieldError(pht('Invalid'));
}
}
}
return $errors;
}
public function getApplicationTransactionHasEffect(
PhabricatorApplicationTransaction $xaction) {
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
if (!strlen($old) && strlen($new)) {
return true;
} else if (strlen($old) && !strlen($new)) {
return true;
} else {
return ((int)$old !== (int)$new);
}
}
protected function getHTTPParameterType() {
return new AphrontIntHTTPParameterType();
}
protected function newConduitSearchParameterType() {
return new ConduitIntParameterType();
}
protected function newConduitEditParameterType() {
return new ConduitIntParameterType();
}
+ protected function newExportFieldType() {
+ return new PhabricatorIntExportField();
+ }
+
}
diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldLink.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldLink.php
index c2b9d6543..146f34ab0 100644
--- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldLink.php
+++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldLink.php
@@ -1,105 +1,109 @@
<?php
final class PhabricatorStandardCustomFieldLink
extends PhabricatorStandardCustomField {
public function getFieldType() {
return 'link';
}
public function buildFieldIndexes() {
$indexes = array();
$value = $this->getFieldValue();
if (strlen($value)) {
$indexes[] = $this->newStringIndex($value);
}
return $indexes;
}
public function renderPropertyViewValue(array $handles) {
$value = $this->getFieldValue();
if (!strlen($value)) {
return null;
}
if (!PhabricatorEnv::isValidRemoteURIForLink($value)) {
return $value;
}
return phutil_tag(
'a',
- array('href' => $value, 'target' => '_blank'),
+ array(
+ 'href' => $value,
+ 'target' => '_blank',
+ 'rel' => 'noreferrer',
+ ),
$value);
}
public function readApplicationSearchValueFromRequest(
PhabricatorApplicationSearchEngine $engine,
AphrontRequest $request) {
return $request->getStr($this->getFieldKey());
}
public function applyApplicationSearchConstraintToQuery(
PhabricatorApplicationSearchEngine $engine,
PhabricatorCursorPagedPolicyAwareQuery $query,
$value) {
if (is_string($value) && !strlen($value)) {
return;
}
$value = (array)$value;
if ($value) {
$query->withApplicationSearchContainsConstraint(
$this->newStringIndex(null),
$value);
}
}
public function appendToApplicationSearchForm(
PhabricatorApplicationSearchEngine $engine,
AphrontFormView $form,
$value) {
$form->appendChild(
id(new AphrontFormTextControl())
->setLabel($this->getFieldName())
->setName($this->getFieldKey())
->setValue($value));
}
public function shouldAppearInHerald() {
return true;
}
public function getHeraldFieldConditions() {
return array(
HeraldAdapter::CONDITION_CONTAINS,
HeraldAdapter::CONDITION_NOT_CONTAINS,
HeraldAdapter::CONDITION_IS,
HeraldAdapter::CONDITION_IS_NOT,
HeraldAdapter::CONDITION_REGEXP,
HeraldAdapter::CONDITION_NOT_REGEXP,
);
}
public function getHeraldFieldStandardType() {
return HeraldField::STANDARD_TEXT;
}
protected function getHTTPParameterType() {
return new AphrontStringHTTPParameterType();
}
protected function newConduitSearchParameterType() {
return new ConduitStringListParameterType();
}
protected function newConduitEditParameterType() {
return new ConduitStringParameterType();
}
}
diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php
index 8242c477f..56164bb7b 100644
--- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php
+++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php
@@ -1,79 +1,83 @@
<?php
final class PhabricatorStandardCustomFieldText
extends PhabricatorStandardCustomField {
public function getFieldType() {
return 'text';
}
public function buildFieldIndexes() {
$indexes = array();
$value = $this->getFieldValue();
if (strlen($value)) {
$indexes[] = $this->newStringIndex($value);
}
return $indexes;
}
public function readApplicationSearchValueFromRequest(
PhabricatorApplicationSearchEngine $engine,
AphrontRequest $request) {
return $request->getStr($this->getFieldKey());
}
public function applyApplicationSearchConstraintToQuery(
PhabricatorApplicationSearchEngine $engine,
PhabricatorCursorPagedPolicyAwareQuery $query,
$value) {
if (strlen($value)) {
$query->withApplicationSearchContainsConstraint(
$this->newStringIndex(null),
$value);
}
}
public function appendToApplicationSearchForm(
PhabricatorApplicationSearchEngine $engine,
AphrontFormView $form,
$value) {
$form->appendChild(
id(new AphrontFormTextControl())
->setLabel($this->getFieldName())
->setName($this->getFieldKey())
->setValue($value));
}
public function shouldAppearInHerald() {
return true;
}
public function getHeraldFieldConditions() {
return array(
HeraldAdapter::CONDITION_CONTAINS,
HeraldAdapter::CONDITION_NOT_CONTAINS,
HeraldAdapter::CONDITION_IS,
HeraldAdapter::CONDITION_IS_NOT,
HeraldAdapter::CONDITION_REGEXP,
HeraldAdapter::CONDITION_NOT_REGEXP,
);
}
public function getHeraldFieldStandardType() {
return HeraldField::STANDARD_TEXT;
}
protected function getHTTPParameterType() {
return new AphrontStringHTTPParameterType();
}
public function getConduitEditParameterType() {
return new ConduitStringParameterType();
}
+ protected function newExportFieldType() {
+ return new PhabricatorStringExportField();
+ }
+
}
diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php
index cdb54fb7e..3c5268d65 100644
--- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php
+++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php
@@ -1,123 +1,168 @@
<?php
abstract class PhabricatorStandardCustomFieldTokenizer
extends PhabricatorStandardCustomFieldPHIDs {
abstract public function getDatasource();
public function renderEditControl(array $handles) {
$value = $this->getFieldValue();
$control = id(new AphrontFormTokenizerControl())
->setUser($this->getViewer())
->setLabel($this->getFieldName())
->setName($this->getFieldKey())
->setDatasource($this->getDatasource())
->setCaption($this->getCaption())
->setError($this->getFieldError())
->setValue(nonempty($value, array()));
$limit = $this->getFieldConfigValue('limit');
if ($limit) {
$control->setLimit($limit);
}
return $control;
}
public function appendToApplicationSearchForm(
PhabricatorApplicationSearchEngine $engine,
AphrontFormView $form,
$value) {
$control = id(new AphrontFormTokenizerControl())
->setLabel($this->getFieldName())
->setName($this->getFieldKey())
- ->setDatasource($this->getDatasource())
+ ->setDatasource($this->newApplicationSearchDatasource())
->setValue(nonempty($value, array()));
$form->appendControl($control);
}
+ public function applyApplicationSearchConstraintToQuery(
+ PhabricatorApplicationSearchEngine $engine,
+ PhabricatorCursorPagedPolicyAwareQuery $query,
+ $value) {
+ if ($value) {
+
+ $datasource = $this->newApplicationSearchDatasource()
+ ->setViewer($this->getViewer());
+ $value = $datasource->evaluateTokens($value);
+
+ $query->withApplicationSearchContainsConstraint(
+ $this->newStringIndex(null),
+ $value);
+ }
+ }
+
public function getHeraldFieldValueType($condition) {
return id(new HeraldTokenizerFieldValue())
->setKey('custom.'.$this->getFieldKey())
->setDatasource($this->getDatasource());
}
public function getHeraldFieldStandardType() {
return HeraldField::STANDARD_PHID_LIST;
}
public function getHeraldDatasource() {
return $this->getDatasource();
}
protected function getHTTPParameterType() {
return new AphrontPHIDListHTTPParameterType();
}
protected function newConduitSearchParameterType() {
return new ConduitPHIDListParameterType();
}
protected function newConduitEditParameterType() {
return new ConduitPHIDListParameterType();
}
protected function newBulkParameterType() {
$datasource = $this->getDatasource();
$limit = $this->getFieldConfigValue('limit');
if ($limit) {
$datasource->setLimit($limit);
}
return id(new BulkTokenizerParameterType())
->setDatasource($datasource);
}
public function shouldAppearInHeraldActions() {
return true;
}
public function getHeraldActionName() {
return pht('Set "%s" to', $this->getFieldName());
}
public function getHeraldActionDescription($value) {
$list = $this->renderHeraldHandleList($value);
return pht('Set "%s" to: %s.', $this->getFieldName(), $list);
}
public function getHeraldActionEffectDescription($value) {
return $this->renderHeraldHandleList($value);
}
public function getHeraldActionStandardType() {
return HeraldAction::STANDARD_PHID_LIST;
}
public function getHeraldActionDatasource() {
$datasource = $this->getDatasource();
$limit = $this->getFieldConfigValue('limit');
if ($limit) {
$datasource->setLimit($limit);
}
return $datasource;
}
private function renderHeraldHandleList($value) {
if (!is_array($value)) {
return pht('(Invalid List)');
} else {
return $this->getViewer()
->renderHandleList($value)
->setAsInline(true)
->render();
}
}
+ protected function newApplicationSearchDatasource() {
+ $datasource = $this->getDatasource();
+
+ return id(new PhabricatorCustomFieldApplicationSearchDatasource())
+ ->setDatasource($datasource);
+ }
+
+ protected function newCommentAction() {
+ $viewer = $this->getViewer();
+
+ $datasource = $this->getDatasource()
+ ->setViewer($viewer);
+
+ $action = id(new PhabricatorEditEngineTokenizerCommentAction())
+ ->setDatasource($datasource);
+
+ $limit = $this->getFieldConfigValue('limit');
+ if ($limit) {
+ $action->setLimit($limit);
+ }
+
+ $value = $this->getFieldValue();
+ if ($value !== null) {
+ $action->setInitialValue($value);
+ }
+
+ return $action;
+ }
+
}
diff --git a/src/infrastructure/daemon/garbagecollector/PhabricatorGarbageCollector.php b/src/infrastructure/daemon/garbagecollector/PhabricatorGarbageCollector.php
index f5960e950..9cf1c2252 100644
--- a/src/infrastructure/daemon/garbagecollector/PhabricatorGarbageCollector.php
+++ b/src/infrastructure/daemon/garbagecollector/PhabricatorGarbageCollector.php
@@ -1,175 +1,177 @@
<?php
/**
* @task info Getting Collector Information
* @task collect Collecting Garbage
*/
abstract class PhabricatorGarbageCollector extends Phobject {
/* -( Getting Collector Information )-------------------------------------- */
/**
* Get a human readable name for what this collector cleans up, like
* "User Activity Logs".
*
* @return string Human-readable collector name.
* @task info
*/
abstract public function getCollectorName();
/**
* Specify that the collector has an automatic retention policy and
* is not configurable.
*
* @return bool True if the collector has an automatic retention policy.
* @task info
*/
public function hasAutomaticPolicy() {
return false;
}
/**
* Get the default retention policy for this collector.
*
* Return the age (in seconds) when resources start getting collected, or
* `null` to retain resources indefinitely.
*
* @return int|null Lifetime, or `null` for indefinite retention.
* @task info
*/
public function getDefaultRetentionPolicy() {
throw new PhutilMethodNotImplementedException();
}
/**
* Get the effective retention policy.
*
* @return int|null Lifetime, or `null` for indefinite retention.
* @task info
*/
public function getRetentionPolicy() {
if ($this->hasAutomaticPolicy()) {
throw new Exception(
pht(
'Can not get retention policy of collector with automatic '.
'policy.'));
}
$config = PhabricatorEnv::getEnvConfig('phd.garbage-collection');
$const = $this->getCollectorConstant();
return idx($config, $const, $this->getDefaultRetentionPolicy());
}
/**
* Get a unique string constant identifying this collector.
*
* @return string Collector constant.
* @task info
*/
final public function getCollectorConstant() {
return $this->getPhobjectClassConstant('COLLECTORCONST', 64);
}
/* -( Collecting Garbage )------------------------------------------------- */
/**
* Run the collector.
*
* @return bool True if there is more garbage to collect.
* @task collect
*/
final public function runCollector() {
// Don't do anything if this collector is configured with an indefinite
// retention policy.
if (!$this->hasAutomaticPolicy()) {
$policy = $this->getRetentionPolicy();
if (!$policy) {
return false;
}
}
// Hold a lock while performing collection to avoid racing other daemons
// running the same collectors.
- $lock_name = 'gc:'.$this->getCollectorConstant();
- $lock = PhabricatorGlobalLock::newLock($lock_name);
+ $params = array(
+ 'collector' => $this->getCollectorConstant(),
+ );
+ $lock = PhabricatorGlobalLock::newLock('gc', $params);
try {
$lock->lock(5);
} catch (PhutilLockException $ex) {
return false;
}
try {
$result = $this->collectGarbage();
} catch (Exception $ex) {
$lock->unlock();
throw $ex;
}
$lock->unlock();
return $result;
}
/**
* Collect garbage from whatever source this GC handles.
*
* @return bool True if there is more garbage to collect.
* @task collect
*/
abstract protected function collectGarbage();
/**
* Get the most recent epoch timestamp that is considered garbage.
*
* Records older than this should be collected.
*
* @return int Most recent garbage timestamp.
* @task collect
*/
final protected function getGarbageEpoch() {
if ($this->hasAutomaticPolicy()) {
throw new Exception(
pht(
'Can not get garbage epoch for a collector with an automatic '.
'collection policy.'));
}
$ttl = $this->getRetentionPolicy();
if (!$ttl) {
throw new Exception(
pht(
'Can not get garbage epoch for a collector with an indefinite '.
'retention policy.'));
}
return (PhabricatorTime::getNow() - $ttl);
}
/**
* Load all of the available garbage collectors.
*
* @return list<PhabricatorGarbageCollector> Garbage collectors.
* @task collect
*/
final public static function getAllCollectors() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getCollectorConstant')
->execute();
}
}
diff --git a/src/infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobType.php b/src/infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobType.php
index a5e29bc10..7854730be 100644
--- a/src/infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobType.php
+++ b/src/infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobType.php
@@ -1,28 +1,48 @@
<?php
abstract class PhabricatorWorkerBulkJobType extends Phobject {
abstract public function getJobName(PhabricatorWorkerBulkJob $job);
abstract public function getBulkJobTypeKey();
abstract public function getJobSize(PhabricatorWorkerBulkJob $job);
abstract public function getDescriptionForConfirm(
PhabricatorWorkerBulkJob $job);
abstract public function createTasks(PhabricatorWorkerBulkJob $job);
abstract public function runTask(
PhabricatorUser $actor,
PhabricatorWorkerBulkJob $job,
PhabricatorWorkerBulkTask $task);
public function getDoneURI(PhabricatorWorkerBulkJob $job) {
return $job->getManageURI();
}
final public static function getAllJobTypes() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getBulkJobTypeKey')
->execute();
}
+ public function getCurtainActions(
+ PhabricatorUser $viewer,
+ PhabricatorWorkerBulkJob $job) {
+
+ if ($job->isConfirming()) {
+ $continue_uri = $job->getMonitorURI();
+ } else {
+ $continue_uri = $job->getDoneURI();
+ }
+
+ $continue = id(new PhabricatorActionView())
+ ->setHref($continue_uri)
+ ->setIcon('fa-arrow-circle-o-right')
+ ->setName(pht('Continue'));
+
+ return array(
+ $continue,
+ );
+ }
+
}
diff --git a/src/infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobWorker.php b/src/infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobWorker.php
index 9117060da..d11f7e81a 100644
--- a/src/infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobWorker.php
+++ b/src/infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobWorker.php
@@ -1,136 +1,140 @@
<?php
abstract class PhabricatorWorkerBulkJobWorker
extends PhabricatorWorker {
final protected function acquireJobLock() {
return PhabricatorGlobalLock::newLock('bulkjob.'.$this->getJobID())
->lock(15);
}
final protected function acquireTaskLock() {
return PhabricatorGlobalLock::newLock('bulktask.'.$this->getTaskID())
->lock(15);
}
final protected function getJobID() {
$data = $this->getTaskData();
$id = idx($data, 'jobID');
if (!$id) {
throw new PhabricatorWorkerPermanentFailureException(
pht('Worker has no job ID.'));
}
return $id;
}
final protected function getTaskID() {
$data = $this->getTaskData();
$id = idx($data, 'taskID');
if (!$id) {
throw new PhabricatorWorkerPermanentFailureException(
pht('Worker has no task ID.'));
}
return $id;
}
final protected function loadJob() {
$id = $this->getJobID();
$job = id(new PhabricatorWorkerBulkJobQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withIDs(array($id))
->executeOne();
if (!$job) {
throw new PhabricatorWorkerPermanentFailureException(
pht('Worker has invalid job ID ("%s").', $id));
}
return $job;
}
final protected function loadTask() {
$id = $this->getTaskID();
$task = id(new PhabricatorWorkerBulkTask())->load($id);
if (!$task) {
throw new PhabricatorWorkerPermanentFailureException(
pht('Worker has invalid task ID ("%s").', $id));
}
return $task;
}
final protected function loadActor(PhabricatorWorkerBulkJob $job) {
$actor_phid = $job->getAuthorPHID();
$actor = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($actor_phid))
->executeOne();
if (!$actor) {
throw new PhabricatorWorkerPermanentFailureException(
pht('Worker has invalid actor PHID ("%s").', $actor_phid));
}
$can_edit = PhabricatorPolicyFilter::hasCapability(
$actor,
$job,
PhabricatorPolicyCapability::CAN_EDIT);
if (!$can_edit) {
throw new PhabricatorWorkerPermanentFailureException(
pht('Job actor does not have permission to edit job.'));
}
+ // Allow the worker to fill user caches inline; bulk jobs occasionally
+ // need to access user preferences.
+ $actor->setAllowInlineCacheGeneration(true);
+
return $actor;
}
final protected function updateJob(PhabricatorWorkerBulkJob $job) {
$has_work = $this->hasRemainingWork($job);
if ($has_work) {
return;
}
$lock = $this->acquireJobLock();
$job = $this->loadJob();
if ($job->getStatus() == PhabricatorWorkerBulkJob::STATUS_RUNNING) {
if (!$this->hasRemainingWork($job)) {
$this->updateJobStatus(
$job,
PhabricatorWorkerBulkJob::STATUS_COMPLETE);
}
}
$lock->unlock();
}
private function hasRemainingWork(PhabricatorWorkerBulkJob $job) {
return (bool)queryfx_one(
$job->establishConnection('r'),
'SELECT * FROM %T WHERE bulkJobPHID = %s
AND status NOT IN (%Ls) LIMIT 1',
id(new PhabricatorWorkerBulkTask())->getTableName(),
$job->getPHID(),
array(
PhabricatorWorkerBulkTask::STATUS_DONE,
PhabricatorWorkerBulkTask::STATUS_FAIL,
));
}
protected function updateJobStatus(PhabricatorWorkerBulkJob $job, $status) {
$type_status = PhabricatorWorkerBulkJobTransaction::TYPE_STATUS;
$xactions = array();
$xactions[] = id(new PhabricatorWorkerBulkJobTransaction())
->setTransactionType($type_status)
->setNewValue($status);
$daemon_source = $this->newContentSource();
$app_phid = id(new PhabricatorDaemonsApplication())->getPHID();
$editor = id(new PhabricatorWorkerBulkJobEditor())
->setActor(PhabricatorUser::getOmnipotentUser())
->setActingAsPHID($app_phid)
->setContentSource($daemon_source)
->setContinueOnMissingFields(true)
->applyTransactions($job, $xactions);
}
}
diff --git a/src/infrastructure/daemon/workers/bulk/PhabricatorWorkerSingleBulkJobType.php b/src/infrastructure/daemon/workers/bulk/PhabricatorWorkerSingleBulkJobType.php
new file mode 100644
index 000000000..f80411348
--- /dev/null
+++ b/src/infrastructure/daemon/workers/bulk/PhabricatorWorkerSingleBulkJobType.php
@@ -0,0 +1,27 @@
+<?php
+
+/**
+ * An bulk job which can not be parallelized and executes only one task.
+ */
+abstract class PhabricatorWorkerSingleBulkJobType
+ extends PhabricatorWorkerBulkJobType {
+
+ public function getDescriptionForConfirm(PhabricatorWorkerBulkJob $job) {
+ return null;
+ }
+
+ public function getJobSize(PhabricatorWorkerBulkJob $job) {
+ return 1;
+ }
+
+ public function createTasks(PhabricatorWorkerBulkJob $job) {
+ $tasks = array();
+
+ $tasks[] = PhabricatorWorkerBulkTask::initializeNewTask(
+ $job,
+ $job->getPHID());
+
+ return $tasks;
+ }
+
+}
diff --git a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php
index 40e43c2ec..312281617 100644
--- a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php
+++ b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php
@@ -1,271 +1,275 @@
<?php
/**
* @task implementation Job Implementation
*/
final class PhabricatorWorkerBulkJob
extends PhabricatorWorkerDAO
implements
PhabricatorPolicyInterface,
PhabricatorSubscribableInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorDestructibleInterface {
const STATUS_CONFIRM = 'confirm';
const STATUS_WAITING = 'waiting';
const STATUS_RUNNING = 'running';
const STATUS_COMPLETE = 'complete';
protected $authorPHID;
protected $jobTypeKey;
protected $status;
protected $parameters = array();
protected $size;
protected $isSilent;
private $jobImplementation = self::ATTACHABLE;
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'parameters' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'jobTypeKey' => 'text32',
'status' => 'text32',
'size' => 'uint32',
'isSilent' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_type' => array(
'columns' => array('jobTypeKey'),
),
'key_author' => array(
'columns' => array('authorPHID'),
),
'key_status' => array(
'columns' => array('status'),
),
),
) + parent::getConfiguration();
}
public static function initializeNewJob(
PhabricatorUser $actor,
PhabricatorWorkerBulkJobType $type,
array $parameters) {
$job = id(new PhabricatorWorkerBulkJob())
->setAuthorPHID($actor->getPHID())
->setJobTypeKey($type->getBulkJobTypeKey())
->setParameters($parameters)
->attachJobImplementation($type)
->setIsSilent(0);
$job->setSize($job->computeSize());
return $job;
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorWorkerBulkJobPHIDType::TYPECONST);
}
public function getMonitorURI() {
return '/daemon/bulk/monitor/'.$this->getID().'/';
}
public function getManageURI() {
return '/daemon/bulk/view/'.$this->getID().'/';
}
public function getParameter($key, $default = null) {
return idx($this->parameters, $key, $default);
}
public function setParameter($key, $value) {
$this->parameters[$key] = $value;
return $this;
}
public function loadTaskStatusCounts() {
$table = new PhabricatorWorkerBulkTask();
$conn_r = $table->establishConnection('r');
$rows = queryfx_all(
$conn_r,
'SELECT status, COUNT(*) N FROM %T WHERE bulkJobPHID = %s
GROUP BY status',
$table->getTableName(),
$this->getPHID());
return ipull($rows, 'N', 'status');
}
public function newContentSource() {
return PhabricatorContentSource::newForSource(
PhabricatorBulkContentSource::SOURCECONST,
array(
'jobID' => $this->getID(),
));
}
public function getStatusIcon() {
$map = array(
self::STATUS_CONFIRM => 'fa-question',
self::STATUS_WAITING => 'fa-clock-o',
self::STATUS_RUNNING => 'fa-clock-o',
self::STATUS_COMPLETE => 'fa-check grey',
);
return idx($map, $this->getStatus(), 'none');
}
public function getStatusName() {
$map = array(
self::STATUS_CONFIRM => pht('Confirming'),
self::STATUS_WAITING => pht('Waiting'),
self::STATUS_RUNNING => pht('Running'),
self::STATUS_COMPLETE => pht('Complete'),
);
return idx($map, $this->getStatus(), $this->getStatus());
}
public function isConfirming() {
return ($this->getStatus() == self::STATUS_CONFIRM);
}
/* -( Job Implementation )------------------------------------------------- */
protected function getJobImplementation() {
return $this->assertAttached($this->jobImplementation);
}
public function attachJobImplementation(PhabricatorWorkerBulkJobType $type) {
$this->jobImplementation = $type;
return $this;
}
private function computeSize() {
return $this->getJobImplementation()->getJobSize($this);
}
public function getCancelURI() {
return $this->getJobImplementation()->getCancelURI($this);
}
public function getDoneURI() {
return $this->getJobImplementation()->getDoneURI($this);
}
public function getDescriptionForConfirm() {
return $this->getJobImplementation()->getDescriptionForConfirm($this);
}
public function createTasks() {
return $this->getJobImplementation()->createTasks($this);
}
public function runTask(
PhabricatorUser $actor,
PhabricatorWorkerBulkTask $task) {
return $this->getJobImplementation()->runTask($actor, $this, $task);
}
public function getJobName() {
return $this->getJobImplementation()->getJobName($this);
}
+ public function getCurtainActions(PhabricatorUser $viewer) {
+ return $this->getJobImplementation()->getCurtainActions($viewer, $this);
+ }
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::getMostOpenPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getAuthorPHID();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
public function describeAutomaticCapability($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_EDIT:
return pht('Only the owner of a bulk job can edit it.');
default:
return null;
}
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return false;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorWorkerBulkJobEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorWorkerBulkJobTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
// We're only removing the actual task objects. This may leave stranded
// workers in the queue itself, but they'll just flush out automatically
// when they can't load bulk job data.
$task_table = new PhabricatorWorkerBulkTask();
$conn_w = $task_table->establishConnection('w');
queryfx(
$conn_w,
'DELETE FROM %T WHERE bulkJobPHID = %s',
$task_table->getPHID(),
$this->getPHID());
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/infrastructure/export/PhabricatorEpochExportField.php b/src/infrastructure/export/PhabricatorEpochExportField.php
deleted file mode 100644
index a19e60b50..000000000
--- a/src/infrastructure/export/PhabricatorEpochExportField.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-final class PhabricatorEpochExportField
- extends PhabricatorExportField {
-
- private $zone;
-
- public function getTextValue($value) {
- if (!isset($this->zone)) {
- $this->zone = new DateTimeZone('UTC');
- }
-
- try {
- $date = new DateTime('@'.$value);
- } catch (Exception $ex) {
- return null;
- }
-
- $date->setTimezone($this->zone);
- return $date->format('c');
- }
-
- public function getNaturalValue($value) {
- return (int)$value;
- }
-
-}
diff --git a/src/infrastructure/export/PhabricatorIntExportField.php b/src/infrastructure/export/PhabricatorIntExportField.php
deleted file mode 100644
index 3363f9b5d..000000000
--- a/src/infrastructure/export/PhabricatorIntExportField.php
+++ /dev/null
@@ -1,10 +0,0 @@
-<?php
-
-final class PhabricatorIntExportField
- extends PhabricatorExportField {
-
- public function getNaturalValue($value) {
- return (int)$value;
- }
-
-}
diff --git a/src/infrastructure/export/PhabricatorStringExportField.php b/src/infrastructure/export/PhabricatorStringExportField.php
deleted file mode 100644
index e8d30c84d..000000000
--- a/src/infrastructure/export/PhabricatorStringExportField.php
+++ /dev/null
@@ -1,4 +0,0 @@
-<?php
-
-final class PhabricatorStringExportField
- extends PhabricatorExportField {}
diff --git a/src/infrastructure/export/engine/PhabricatorCustomFieldExportEngineExtension.php b/src/infrastructure/export/engine/PhabricatorCustomFieldExportEngineExtension.php
new file mode 100644
index 000000000..c2a9eddae
--- /dev/null
+++ b/src/infrastructure/export/engine/PhabricatorCustomFieldExportEngineExtension.php
@@ -0,0 +1,86 @@
+<?php
+
+final class PhabricatorCustomFieldExportEngineExtension
+ extends PhabricatorExportEngineExtension {
+
+ const EXTENSIONKEY = 'custom-field';
+
+ private $object;
+
+ public function supportsObject($object) {
+ $this->object = $object;
+ return ($object instanceof PhabricatorCustomFieldInterface);
+ }
+
+ public function newExportFields() {
+ $prototype = $this->object;
+
+ $fields = $this->newCustomFields($prototype);
+
+ $results = array();
+ foreach ($fields as $field) {
+ $field_key = $field->getModernFieldKey();
+
+ $results[] = $field->newExportField()
+ ->setKey($field_key);
+ }
+
+ return $results;
+ }
+
+ public function newExportData(array $objects) {
+ $viewer = $this->getViewer();
+
+ $field_map = array();
+ foreach ($objects as $object) {
+ $object_phid = $object->getPHID();
+
+ $fields = PhabricatorCustomField::getObjectFields(
+ $object,
+ PhabricatorCustomField::ROLE_EXPORT);
+
+ $fields
+ ->setViewer($viewer)
+ ->readFieldsFromObject($object);
+
+ $field_map[$object_phid] = $fields;
+ }
+
+ $all_fields = array();
+ foreach ($field_map as $field_list) {
+ foreach ($field_list->getFields() as $field) {
+ $all_fields[] = $field;
+ }
+ }
+
+ id(new PhabricatorCustomFieldStorageQuery())
+ ->addFields($all_fields)
+ ->execute();
+
+ $results = array();
+ foreach ($objects as $object) {
+ $object_phid = $object->getPHID();
+ $object_fields = $field_map[$object_phid];
+
+ $map = array();
+ foreach ($object_fields->getFields() as $field) {
+ $key = $field->getModernFieldKey();
+ $map[$key] = $field->newExportData();
+ }
+
+ $results[] = $map;
+ }
+
+ return $results;
+ }
+
+ private function newCustomFields($object) {
+ $fields = PhabricatorCustomField::getObjectFields(
+ $object,
+ PhabricatorCustomField::ROLE_EXPORT);
+ $fields->setViewer($this->getViewer());
+
+ return $fields->getFields();
+ }
+
+}
diff --git a/src/infrastructure/export/engine/PhabricatorExportEngine.php b/src/infrastructure/export/engine/PhabricatorExportEngine.php
new file mode 100644
index 000000000..f2c6a1270
--- /dev/null
+++ b/src/infrastructure/export/engine/PhabricatorExportEngine.php
@@ -0,0 +1,168 @@
+<?php
+
+final class PhabricatorExportEngine
+ extends Phobject {
+
+ private $viewer;
+ private $searchEngine;
+ private $savedQuery;
+ private $exportFormat;
+ private $filename;
+ private $title;
+
+ public function setViewer(PhabricatorUser $viewer) {
+ $this->viewer = $viewer;
+ return $this;
+ }
+
+ public function getViewer() {
+ return $this->viewer;
+ }
+
+ public function setSearchEngine(
+ PhabricatorApplicationSearchEngine $search_engine) {
+ $this->searchEngine = $search_engine;
+ return $this;
+ }
+
+ public function getSearchEngine() {
+ return $this->searchEngine;
+ }
+
+ public function setSavedQuery(PhabricatorSavedQuery $saved_query) {
+ $this->savedQuery = $saved_query;
+ return $this;
+ }
+
+ public function getSavedQuery() {
+ return $this->savedQuery;
+ }
+
+ public function setExportFormat(
+ PhabricatorExportFormat $export_format) {
+ $this->exportFormat = $export_format;
+ return $this;
+ }
+
+ public function getExportFormat() {
+ return $this->exportFormat;
+ }
+
+ public function setFilename($filename) {
+ $this->filename = $filename;
+ return $this;
+ }
+
+ public function getFilename() {
+ return $this->filename;
+ }
+
+ public function setTitle($title) {
+ $this->title = $title;
+ return $this;
+ }
+
+ public function getTitle() {
+ return $this->title;
+ }
+
+ public function newBulkJob(AphrontRequest $request) {
+ $viewer = $this->getViewer();
+ $engine = $this->getSearchEngine();
+ $saved_query = $this->getSavedQuery();
+ $format = $this->getExportFormat();
+
+ $params = array(
+ 'engineClass' => get_class($engine),
+ 'queryKey' => $saved_query->getQueryKey(),
+ 'formatKey' => $format->getExportFormatKey(),
+ 'title' => $this->getTitle(),
+ 'filename' => $this->getFilename(),
+ );
+
+ $job = PhabricatorWorkerBulkJob::initializeNewJob(
+ $viewer,
+ new PhabricatorExportEngineBulkJobType(),
+ $params);
+
+ // We queue these jobs directly into STATUS_WAITING without requiring
+ // a confirmation from the user.
+
+ $xactions = array();
+
+ $xactions[] = id(new PhabricatorWorkerBulkJobTransaction())
+ ->setTransactionType(PhabricatorWorkerBulkJobTransaction::TYPE_STATUS)
+ ->setNewValue(PhabricatorWorkerBulkJob::STATUS_WAITING);
+
+ $editor = id(new PhabricatorWorkerBulkJobEditor())
+ ->setActor($viewer)
+ ->setContentSourceFromRequest($request)
+ ->setContinueOnMissingFields(true)
+ ->applyTransactions($job, $xactions);
+
+ return $job;
+ }
+
+ public function exportFile() {
+ $viewer = $this->getViewer();
+ $engine = $this->getSearchEngine();
+ $saved_query = $this->getSavedQuery();
+ $format = $this->getExportFormat();
+ $title = $this->getTitle();
+ $filename = $this->getFilename();
+
+ $query = $engine->buildQueryFromSavedQuery($saved_query);
+
+ $extension = $format->getFileExtension();
+ $mime_type = $format->getMIMEContentType();
+ $filename = $filename.'.'.$extension;
+
+ $format = id(clone $format)
+ ->setViewer($viewer)
+ ->setTitle($title);
+
+ $field_list = $engine->newExportFieldList();
+ $field_list = mpull($field_list, null, 'getKey');
+ $format->addHeaders($field_list);
+
+ // Iterate over the query results in large page so we don't have to hold
+ // too much stuff in memory.
+ $page_size = 1000;
+ $page_cursor = null;
+ do {
+ $pager = $engine->newPagerForSavedQuery($saved_query);
+ $pager->setPageSize($page_size);
+
+ if ($page_cursor !== null) {
+ $pager->setAfterID($page_cursor);
+ }
+
+ $objects = $engine->executeQuery($query, $pager);
+ $objects = array_values($objects);
+ $page_cursor = $pager->getNextPageID();
+
+ $export_data = $engine->newExport($objects);
+ for ($ii = 0; $ii < count($objects); $ii++) {
+ $format->addObject($objects[$ii], $field_list, $export_data[$ii]);
+ }
+ } while ($pager->getHasMoreResults());
+
+ $export_result = $format->newFileData();
+
+ // We have all the data in one big string and aren't actually
+ // streaming it, but pretending that we are allows us to actviate
+ // the chunk engine and store large files.
+ $iterator = new ArrayIterator(array($export_result));
+
+ $source = id(new PhabricatorIteratorFileUploadSource())
+ ->setName($filename)
+ ->setViewPolicy(PhabricatorPolicies::POLICY_NOONE)
+ ->setMIMEType($mime_type)
+ ->setRelativeTTL(phutil_units('60 minutes in seconds'))
+ ->setAuthorPHID($viewer->getPHID())
+ ->setIterator($iterator);
+
+ return $source->uploadFile();
+ }
+
+}
diff --git a/src/infrastructure/export/engine/PhabricatorExportEngineBulkJobType.php b/src/infrastructure/export/engine/PhabricatorExportEngineBulkJobType.php
new file mode 100644
index 000000000..cf54bc799
--- /dev/null
+++ b/src/infrastructure/export/engine/PhabricatorExportEngineBulkJobType.php
@@ -0,0 +1,117 @@
+<?php
+
+final class PhabricatorExportEngineBulkJobType
+ extends PhabricatorWorkerSingleBulkJobType {
+
+ public function getBulkJobTypeKey() {
+ return 'export';
+ }
+
+ public function getJobName(PhabricatorWorkerBulkJob $job) {
+ return pht('Data Export');
+ }
+
+ public function getCurtainActions(
+ PhabricatorUser $viewer,
+ PhabricatorWorkerBulkJob $job) {
+ $actions = array();
+
+ $file_phid = $job->getParameter('filePHID');
+ if (!$file_phid) {
+ $actions[] = id(new PhabricatorActionView())
+ ->setHref('#')
+ ->setIcon('fa-download')
+ ->setDisabled(true)
+ ->setName(pht('Exporting Data...'));
+ } else {
+ $file = id(new PhabricatorFileQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(array($file_phid))
+ ->executeOne();
+ if (!$file) {
+ $actions[] = id(new PhabricatorActionView())
+ ->setHref('#')
+ ->setIcon('fa-download')
+ ->setDisabled(true)
+ ->setName(pht('Temporary File Expired'));
+ } else {
+ $actions[] = id(new PhabricatorActionView())
+ ->setHref($file->getDownloadURI())
+ ->setIcon('fa-download')
+ ->setName(pht('Download Data Export'));
+ }
+ }
+
+ return $actions;
+ }
+
+
+ public function runTask(
+ PhabricatorUser $actor,
+ PhabricatorWorkerBulkJob $job,
+ PhabricatorWorkerBulkTask $task) {
+
+ $engine_class = $job->getParameter('engineClass');
+ if (!is_subclass_of($engine_class, 'PhabricatorApplicationSearchEngine')) {
+ throw new Exception(
+ pht(
+ 'Unknown search engine class "%s".',
+ $engine_class));
+ }
+
+ $engine = newv($engine_class, array())
+ ->setViewer($actor);
+
+ $query_key = $job->getParameter('queryKey');
+ if ($engine->isBuiltinQuery($query_key)) {
+ $saved_query = $engine->buildSavedQueryFromBuiltin($query_key);
+ } else if ($query_key) {
+ $saved_query = id(new PhabricatorSavedQueryQuery())
+ ->setViewer($actor)
+ ->withQueryKeys(array($query_key))
+ ->executeOne();
+ } else {
+ $saved_query = null;
+ }
+
+ if (!$saved_query) {
+ throw new Exception(
+ pht(
+ 'Failed to load saved query ("%s").',
+ $query_key));
+ }
+
+ $format_key = $job->getParameter('formatKey');
+
+ $all_formats = PhabricatorExportFormat::getAllExportFormats();
+ $format = idx($all_formats, $format_key);
+ if (!$format) {
+ throw new Exception(
+ pht(
+ 'Unknown export format ("%s").',
+ $format_key));
+ }
+
+ if (!$format->isExportFormatEnabled()) {
+ throw new Exception(
+ pht(
+ 'Export format ("%s") is not enabled.',
+ $format_key));
+ }
+
+ $export_engine = id(new PhabricatorExportEngine())
+ ->setViewer($actor)
+ ->setTitle($job->getParameter('title'))
+ ->setFilename($job->getParameter('filename'))
+ ->setSearchEngine($engine)
+ ->setSavedQuery($saved_query)
+ ->setExportFormat($format);
+
+ $file = $export_engine->exportFile();
+
+ $job
+ ->setParameter('filePHID', $file->getPHID())
+ ->save();
+ }
+
+}
diff --git a/src/infrastructure/export/engine/PhabricatorExportEngineExtension.php b/src/infrastructure/export/engine/PhabricatorExportEngineExtension.php
new file mode 100644
index 000000000..01d4471ef
--- /dev/null
+++ b/src/infrastructure/export/engine/PhabricatorExportEngineExtension.php
@@ -0,0 +1,31 @@
+<?php
+
+abstract class PhabricatorExportEngineExtension extends Phobject {
+
+ private $viewer;
+
+ final public function getExtensionKey() {
+ return $this->getPhobjectClassConstant('EXTENSIONKEY');
+ }
+
+ final public function setViewer($viewer) {
+ $this->viewer = $viewer;
+ return $this;
+ }
+
+ final public function getViewer() {
+ return $this->viewer;
+ }
+
+ abstract public function supportsObject($object);
+ abstract public function newExportFields();
+ abstract public function newExportData(array $objects);
+
+ final public static function getAllExtensions() {
+ return id(new PhutilClassMapQuery())
+ ->setAncestorClass(__CLASS__)
+ ->setUniqueMethod('getExtensionKey')
+ ->execute();
+ }
+
+}
diff --git a/src/infrastructure/export/engine/PhabricatorExportFormatSetting.php b/src/infrastructure/export/engine/PhabricatorExportFormatSetting.php
new file mode 100644
index 000000000..625a90719
--- /dev/null
+++ b/src/infrastructure/export/engine/PhabricatorExportFormatSetting.php
@@ -0,0 +1,16 @@
+<?php
+
+final class PhabricatorExportFormatSetting
+ extends PhabricatorInternalSetting {
+
+ const SETTINGKEY = 'export.format';
+
+ public function getSettingName() {
+ return pht('Export Format');
+ }
+
+ public function getSettingDefaultValue() {
+ return null;
+ }
+
+}
diff --git a/src/infrastructure/export/engine/PhabricatorLiskExportEngineExtension.php b/src/infrastructure/export/engine/PhabricatorLiskExportEngineExtension.php
new file mode 100644
index 000000000..516298605
--- /dev/null
+++ b/src/infrastructure/export/engine/PhabricatorLiskExportEngineExtension.php
@@ -0,0 +1,42 @@
+<?php
+
+final class PhabricatorLiskExportEngineExtension
+ extends PhabricatorExportEngineExtension {
+
+ const EXTENSIONKEY = 'lisk';
+
+ public function supportsObject($object) {
+ if (!($object instanceof LiskDAO)) {
+ return false;
+ }
+
+ if (!$object->getConfigOption(LiskDAO::CONFIG_TIMESTAMPS)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public function newExportFields() {
+ return array(
+ id(new PhabricatorEpochExportField())
+ ->setKey('dateCreated')
+ ->setLabel(pht('Created')),
+ id(new PhabricatorEpochExportField())
+ ->setKey('dateModified')
+ ->setLabel(pht('Modified')),
+ );
+ }
+
+ public function newExportData(array $objects) {
+ $map = array();
+ foreach ($objects as $object) {
+ $map[] = array(
+ 'dateCreated' => $object->getDateCreated(),
+ 'dateModified' => $object->getDateModified(),
+ );
+ }
+ return $map;
+ }
+
+}
diff --git a/src/infrastructure/export/engine/PhabricatorProjectsExportEngineExtension.php b/src/infrastructure/export/engine/PhabricatorProjectsExportEngineExtension.php
new file mode 100644
index 000000000..eb3bca3a4
--- /dev/null
+++ b/src/infrastructure/export/engine/PhabricatorProjectsExportEngineExtension.php
@@ -0,0 +1,60 @@
+<?php
+
+final class PhabricatorProjectsExportEngineExtension
+ extends PhabricatorExportEngineExtension {
+
+ const EXTENSIONKEY = 'projects';
+
+ public function supportsObject($object) {
+ return ($object instanceof PhabricatorProjectInterface);
+ }
+
+ public function newExportFields() {
+ return array(
+ id(new PhabricatorPHIDListExportField())
+ ->setKey('tagPHIDs')
+ ->setLabel(pht('Tag PHIDs')),
+ id(new PhabricatorStringListExportField())
+ ->setKey('tags')
+ ->setLabel(pht('Tags')),
+ );
+ }
+
+ public function newExportData(array $objects) {
+ $viewer = $this->getViewer();
+
+ $object_phids = mpull($objects, 'getPHID');
+
+ $projects_query = id(new PhabricatorEdgeQuery())
+ ->withSourcePHIDs($object_phids)
+ ->withEdgeTypes(
+ array(
+ PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
+ ));
+ $projects_query->execute();
+
+ $handles = $viewer->loadHandles($projects_query->getDestinationPHIDs());
+
+ $map = array();
+ foreach ($objects as $object) {
+ $object_phid = $object->getPHID();
+
+ $project_phids = $projects_query->getDestinationPHIDs(
+ array($object_phid),
+ array(PhabricatorProjectObjectHasProjectEdgeType::EDGECONST));
+
+ $handle_list = $handles->newSublist($project_phids);
+ $handle_list = iterator_to_array($handle_list);
+ $handle_names = mpull($handle_list, 'getName');
+ $handle_names = array_values($handle_names);
+
+ $map[] = array(
+ 'tagPHIDs' => $project_phids,
+ 'tags' => $handle_names,
+ );
+ }
+
+ return $map;
+ }
+
+}
diff --git a/src/infrastructure/export/engine/PhabricatorSpacesExportEngineExtension.php b/src/infrastructure/export/engine/PhabricatorSpacesExportEngineExtension.php
new file mode 100644
index 000000000..3e187bc8c
--- /dev/null
+++ b/src/infrastructure/export/engine/PhabricatorSpacesExportEngineExtension.php
@@ -0,0 +1,53 @@
+<?php
+
+final class PhabricatorSpacesExportEngineExtension
+ extends PhabricatorExportEngineExtension {
+
+ const EXTENSIONKEY = 'spaces';
+
+ public function supportsObject($object) {
+ $viewer = $this->getViewer();
+
+ if (!PhabricatorSpacesNamespaceQuery::getViewerSpacesExist($viewer)) {
+ return false;
+ }
+
+ return ($object instanceof PhabricatorSpacesInterface);
+ }
+
+ public function newExportFields() {
+ return array(
+ id(new PhabricatorPHIDExportField())
+ ->setKey('spacePHID')
+ ->setLabel(pht('Space PHID')),
+ id(new PhabricatorStringExportField())
+ ->setKey('space')
+ ->setLabel(pht('Space')),
+ );
+ }
+
+ public function newExportData(array $objects) {
+ $viewer = $this->getViewer();
+
+ $space_phids = array();
+ foreach ($objects as $object) {
+ $space_phids[] = PhabricatorSpacesNamespaceQuery::getObjectSpacePHID(
+ $object);
+ }
+ $handles = $viewer->loadHandles($space_phids);
+
+ $map = array();
+ foreach ($objects as $object) {
+ $space_phid = PhabricatorSpacesNamespaceQuery::getObjectSpacePHID(
+ $object);
+
+ $map[] = array(
+ 'spacePHID' => $space_phid,
+ 'space' => $handles[$space_phid]->getName(),
+ );
+ }
+
+ return $map;
+ }
+
+}
diff --git a/src/infrastructure/export/engine/PhabricatorSubscriptionsExportEngineExtension.php b/src/infrastructure/export/engine/PhabricatorSubscriptionsExportEngineExtension.php
new file mode 100644
index 000000000..8aedb38fa
--- /dev/null
+++ b/src/infrastructure/export/engine/PhabricatorSubscriptionsExportEngineExtension.php
@@ -0,0 +1,60 @@
+<?php
+
+final class PhabricatorSubscriptionsExportEngineExtension
+ extends PhabricatorExportEngineExtension {
+
+ const EXTENSIONKEY = 'subscriptions';
+
+ public function supportsObject($object) {
+ return ($object instanceof PhabricatorSubscribableInterface);
+ }
+
+ public function newExportFields() {
+ return array(
+ id(new PhabricatorPHIDListExportField())
+ ->setKey('subscriberPHIDs')
+ ->setLabel(pht('Subscriber PHIDs')),
+ id(new PhabricatorStringListExportField())
+ ->setKey('subscribers')
+ ->setLabel(pht('Subscribers')),
+ );
+ }
+
+ public function newExportData(array $objects) {
+ $viewer = $this->getViewer();
+
+ $object_phids = mpull($objects, 'getPHID');
+
+ $projects_query = id(new PhabricatorEdgeQuery())
+ ->withSourcePHIDs($object_phids)
+ ->withEdgeTypes(
+ array(
+ PhabricatorObjectHasSubscriberEdgeType::EDGECONST,
+ ));
+ $projects_query->execute();
+
+ $handles = $viewer->loadHandles($projects_query->getDestinationPHIDs());
+
+ $map = array();
+ foreach ($objects as $object) {
+ $object_phid = $object->getPHID();
+
+ $project_phids = $projects_query->getDestinationPHIDs(
+ array($object_phid),
+ array(PhabricatorObjectHasSubscriberEdgeType::EDGECONST));
+
+ $handle_list = $handles->newSublist($project_phids);
+ $handle_list = iterator_to_array($handle_list);
+ $handle_names = mpull($handle_list, 'getName');
+ $handle_names = array_values($handle_names);
+
+ $map[] = array(
+ 'subscriberPHIDs' => $project_phids,
+ 'subscribers' => $handle_names,
+ );
+ }
+
+ return $map;
+ }
+
+}
diff --git a/src/infrastructure/export/field/PhabricatorEpochExportField.php b/src/infrastructure/export/field/PhabricatorEpochExportField.php
new file mode 100644
index 000000000..4dffde5aa
--- /dev/null
+++ b/src/infrastructure/export/field/PhabricatorEpochExportField.php
@@ -0,0 +1,47 @@
+<?php
+
+final class PhabricatorEpochExportField
+ extends PhabricatorExportField {
+
+ private $zone;
+
+ public function getTextValue($value) {
+ if (!isset($this->zone)) {
+ $this->zone = new DateTimeZone('UTC');
+ }
+
+ try {
+ $date = new DateTime('@'.$value);
+ } catch (Exception $ex) {
+ return null;
+ }
+
+ $date->setTimezone($this->zone);
+ return $date->format('c');
+ }
+
+ public function getNaturalValue($value) {
+ return (int)$value;
+ }
+
+ public function getPHPExcelValue($value) {
+ $epoch = $this->getNaturalValue($value);
+
+ $seconds_per_day = phutil_units('1 day in seconds');
+ $offset = ($seconds_per_day * 25569);
+
+ return ($epoch + $offset) / $seconds_per_day;
+ }
+
+ /**
+ * @phutil-external-symbol class PHPExcel_Style_NumberFormat
+ */
+ public function formatPHPExcelCell($cell, $style) {
+ $code = PHPExcel_Style_NumberFormat::FORMAT_DATE_YYYYMMDD2;
+
+ $style
+ ->getNumberFormat()
+ ->setFormatCode($code);
+ }
+
+}
diff --git a/src/infrastructure/export/PhabricatorExportField.php b/src/infrastructure/export/field/PhabricatorExportField.php
similarity index 50%
rename from src/infrastructure/export/PhabricatorExportField.php
rename to src/infrastructure/export/field/PhabricatorExportField.php
index 3efb7a8b9..7ee091859 100644
--- a/src/infrastructure/export/PhabricatorExportField.php
+++ b/src/infrastructure/export/field/PhabricatorExportField.php
@@ -1,35 +1,56 @@
<?php
abstract class PhabricatorExportField
extends Phobject {
private $key;
private $label;
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 getTextValue($value) {
- return (string)$this->getNaturalValue($value);
+ $natural_value = $this->getNaturalValue($value);
+
+ if ($natural_value === null) {
+ return null;
+ }
+
+ return (string)$natural_value;
}
public function getNaturalValue($value) {
return $value;
}
+ public function getPHPExcelValue($value) {
+ return $this->getTextValue($value);
+ }
+
+ /**
+ * @phutil-external-symbol class PHPExcel_Cell_DataType
+ */
+ public function formatPHPExcelCell($cell, $style) {
+ $cell->setDataType(PHPExcel_Cell_DataType::TYPE_STRING);
+ }
+
+ public function getCharacterWidth() {
+ return 24;
+ }
+
}
diff --git a/src/infrastructure/export/PhabricatorIDExportField.php b/src/infrastructure/export/field/PhabricatorIDExportField.php
similarity index 72%
rename from src/infrastructure/export/PhabricatorIDExportField.php
rename to src/infrastructure/export/field/PhabricatorIDExportField.php
index 5b29fdb21..1ef3d5337 100644
--- a/src/infrastructure/export/PhabricatorIDExportField.php
+++ b/src/infrastructure/export/field/PhabricatorIDExportField.php
@@ -1,10 +1,14 @@
<?php
final class PhabricatorIDExportField
extends PhabricatorExportField {
public function getNaturalValue($value) {
return (int)$value;
}
+ public function getCharacterWidth() {
+ return 12;
+ }
+
}
diff --git a/src/infrastructure/export/field/PhabricatorIntExportField.php b/src/infrastructure/export/field/PhabricatorIntExportField.php
new file mode 100644
index 000000000..57f7e0ab2
--- /dev/null
+++ b/src/infrastructure/export/field/PhabricatorIntExportField.php
@@ -0,0 +1,25 @@
+<?php
+
+final class PhabricatorIntExportField
+ extends PhabricatorExportField {
+
+ public function getNaturalValue($value) {
+ if ($value === null) {
+ return $value;
+ }
+
+ return (int)$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/field/PhabricatorListExportField.php b/src/infrastructure/export/field/PhabricatorListExportField.php
new file mode 100644
index 000000000..67c3e06ff
--- /dev/null
+++ b/src/infrastructure/export/field/PhabricatorListExportField.php
@@ -0,0 +1,10 @@
+<?php
+
+abstract class PhabricatorListExportField
+ extends PhabricatorExportField {
+
+ public function getTextValue($value) {
+ return implode("\n", $value);
+ }
+
+}
diff --git a/src/infrastructure/export/field/PhabricatorPHIDExportField.php b/src/infrastructure/export/field/PhabricatorPHIDExportField.php
new file mode 100644
index 000000000..052c73fbd
--- /dev/null
+++ b/src/infrastructure/export/field/PhabricatorPHIDExportField.php
@@ -0,0 +1,10 @@
+<?php
+
+final class PhabricatorPHIDExportField
+ extends PhabricatorExportField {
+
+ public function getCharacterWidth() {
+ return 32;
+ }
+
+}
diff --git a/src/infrastructure/export/field/PhabricatorPHIDListExportField.php b/src/infrastructure/export/field/PhabricatorPHIDListExportField.php
new file mode 100644
index 000000000..e75a891a5
--- /dev/null
+++ b/src/infrastructure/export/field/PhabricatorPHIDListExportField.php
@@ -0,0 +1,10 @@
+<?php
+
+final class PhabricatorPHIDListExportField
+ extends PhabricatorListExportField {
+
+ public function getCharacterWidth() {
+ return 32;
+ }
+
+}
diff --git a/src/infrastructure/export/field/PhabricatorStringExportField.php b/src/infrastructure/export/field/PhabricatorStringExportField.php
new file mode 100644
index 000000000..f41548f55
--- /dev/null
+++ b/src/infrastructure/export/field/PhabricatorStringExportField.php
@@ -0,0 +1,18 @@
+<?php
+
+final class PhabricatorStringExportField
+ extends PhabricatorExportField {
+
+ public function getNaturalValue($value) {
+ if ($value === null) {
+ return $value;
+ }
+
+ if (!strlen($value)) {
+ return null;
+ }
+
+ return (string)$value;
+ }
+
+}
diff --git a/src/infrastructure/export/field/PhabricatorStringListExportField.php b/src/infrastructure/export/field/PhabricatorStringListExportField.php
new file mode 100644
index 000000000..3548e5189
--- /dev/null
+++ b/src/infrastructure/export/field/PhabricatorStringListExportField.php
@@ -0,0 +1,4 @@
+<?php
+
+final class PhabricatorStringListExportField
+ extends PhabricatorListExportField {}
diff --git a/src/infrastructure/export/PhabricatorPHIDExportField.php b/src/infrastructure/export/field/PhabricatorURIExportField.php
similarity index 52%
rename from src/infrastructure/export/PhabricatorPHIDExportField.php
rename to src/infrastructure/export/field/PhabricatorURIExportField.php
index 7c08ae022..9ba2cd4b7 100644
--- a/src/infrastructure/export/PhabricatorPHIDExportField.php
+++ b/src/infrastructure/export/field/PhabricatorURIExportField.php
@@ -1,4 +1,4 @@
<?php
-final class PhabricatorPHIDExportField
+final class PhabricatorURIExportField
extends PhabricatorExportField {}
diff --git a/src/infrastructure/export/PhabricatorCSVExportFormat.php b/src/infrastructure/export/format/PhabricatorCSVExportFormat.php
similarity index 55%
rename from src/infrastructure/export/PhabricatorCSVExportFormat.php
rename to src/infrastructure/export/format/PhabricatorCSVExportFormat.php
index d9662467e..8f3879d5f 100644
--- a/src/infrastructure/export/PhabricatorCSVExportFormat.php
+++ b/src/infrastructure/export/format/PhabricatorCSVExportFormat.php
@@ -1,47 +1,70 @@
<?php
final class PhabricatorCSVExportFormat
extends PhabricatorExportFormat {
const EXPORTKEY = 'csv';
private $rows = array();
public function getExportFormatName() {
return pht('Comma-Separated Values (.csv)');
}
public function isExportFormatEnabled() {
return true;
}
public function getFileExtension() {
return 'csv';
}
public function getMIMEContentType() {
return 'text/csv';
}
+ public function addHeaders(array $fields) {
+ $headers = mpull($fields, 'getLabel');
+ $this->addRow($headers);
+ }
+
public function addObject($object, array $fields, array $map) {
$values = array();
foreach ($fields as $key => $field) {
$value = $map[$key];
$value = $field->getTextValue($value);
+ $values[] = $value;
+ }
+
+ $this->addRow($values);
+ }
+
+ private function addRow(array $values) {
+ $row = array();
+ foreach ($values as $value) {
+
+ // Excel is extremely interested in executing arbitrary code it finds in
+ // untrusted CSV files downloaded from the internet. When a cell looks
+ // like it might be too tempting for Excel to ignore, mangle the value
+ // to dissuade remote code execution. See T12800.
+
+ if (preg_match('/^\s*[+=@-]/', $value)) {
+ $value = '(!) '.$value;
+ }
if (preg_match('/\s|,|\"/', $value)) {
$value = str_replace('"', '""', $value);
$value = '"'.$value.'"';
}
- $values[] = $value;
+ $row[] = $value;
}
- $this->rows[] = implode(',', $values);
+ $this->rows[] = implode(',', $row);
}
public function newFileData() {
return implode("\n", $this->rows);
}
}
diff --git a/src/infrastructure/export/format/PhabricatorExcelExportFormat.php b/src/infrastructure/export/format/PhabricatorExcelExportFormat.php
new file mode 100644
index 000000000..2b0c78788
--- /dev/null
+++ b/src/infrastructure/export/format/PhabricatorExcelExportFormat.php
@@ -0,0 +1,168 @@
+<?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;
+ }
+
+ private function getCellName($col, $row = null) {
+ $col_name = chr(ord('A') + $col);
+
+ if ($row === null) {
+ return $col_name;
+ }
+
+ return $col_name.$row;
+ }
+
+}
diff --git a/src/infrastructure/export/PhabricatorExportFormat.php b/src/infrastructure/export/format/PhabricatorExportFormat.php
similarity index 77%
rename from src/infrastructure/export/PhabricatorExportFormat.php
rename to src/infrastructure/export/format/PhabricatorExportFormat.php
index a1da4e90d..4566814b9 100644
--- a/src/infrastructure/export/PhabricatorExportFormat.php
+++ b/src/infrastructure/export/format/PhabricatorExportFormat.php
@@ -1,51 +1,53 @@
<?php
abstract class PhabricatorExportFormat
extends Phobject {
private $viewer;
+ private $title;
final public function getExportFormatKey() {
return $this->getPhobjectClassConstant('EXPORTKEY');
}
final public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
final public function getViewer() {
return $this->viewer;
}
+ final public function setTitle($title) {
+ $this->title = $title;
+ return $this;
+ }
+
+ final public function getTitle() {
+ return $this->title;
+ }
+
abstract public function getExportFormatName();
abstract public function getMIMEContentType();
abstract public function getFileExtension();
+ public function addHeaders(array $fields) {
+ return;
+ }
+
abstract public function addObject($object, array $fields, array $map);
abstract public function newFileData();
public function isExportFormatEnabled() {
return true;
}
final public static function getAllExportFormats() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getExportFormatKey')
->execute();
}
- final public static function getAllEnabledExportFormats() {
- $formats = self::getAllExportFormats();
-
- foreach ($formats as $key => $format) {
- if (!$format->isExportFormatEnabled()) {
- unset($formats[$key]);
- }
- }
-
- return $formats;
- }
-
}
diff --git a/src/infrastructure/export/PhabricatorJSONExportFormat.php b/src/infrastructure/export/format/PhabricatorJSONExportFormat.php
similarity index 100%
rename from src/infrastructure/export/PhabricatorJSONExportFormat.php
rename to src/infrastructure/export/format/PhabricatorJSONExportFormat.php
diff --git a/src/infrastructure/export/PhabricatorTextExportFormat.php b/src/infrastructure/export/format/PhabricatorTextExportFormat.php
similarity index 67%
rename from src/infrastructure/export/PhabricatorTextExportFormat.php
rename to src/infrastructure/export/format/PhabricatorTextExportFormat.php
index ec308f2eb..d51e199f9 100644
--- a/src/infrastructure/export/PhabricatorTextExportFormat.php
+++ b/src/infrastructure/export/format/PhabricatorTextExportFormat.php
@@ -1,43 +1,55 @@
<?php
final class PhabricatorTextExportFormat
extends PhabricatorExportFormat {
const EXPORTKEY = 'text';
private $rows = array();
public function getExportFormatName() {
return 'Tab-Separated Text (.txt)';
}
public function isExportFormatEnabled() {
return true;
}
public function getFileExtension() {
return 'txt';
}
public function getMIMEContentType() {
return 'text/plain';
}
+ public function addHeaders(array $fields) {
+ $headers = mpull($fields, 'getLabel');
+ $this->addRow($headers);
+ }
+
public function addObject($object, array $fields, array $map) {
$values = array();
foreach ($fields as $key => $field) {
$value = $map[$key];
$value = $field->getTextValue($value);
- $value = addcslashes($value, "\0..\37\\\177..\377");
-
$values[] = $value;
}
- $this->rows[] = implode("\t", $values);
+ $this->addRow($values);
+ }
+
+ private function addRow(array $values) {
+ $row = array();
+ foreach ($values as $value) {
+ $row[] = addcslashes($value, "\0..\37\\\177..\377");
+ }
+
+ $this->rows[] = implode("\t", $row);
}
public function newFileData() {
return implode("\n", $this->rows)."\n";
}
}
diff --git a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php
index 40cedacf0..eb74f8deb 100644
--- a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php
+++ b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php
@@ -1,1657 +1,1657 @@
<?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(
array(
'%s added a dependency: %3$s.',
'%s added dependencies: %3$s.',
),
),
'%s added %s dependencie(s) for %s: %s.' => array(
array(
'%s added a dependency for %3$s: %4$s.',
'%s added dependencies for %3$s: %4$s.',
),
),
'%s removed %s dependencie(s): %s.' => array(
array(
'%s removed a dependency: %3$s.',
'%s removed dependencies: %3$s.',
),
),
'%s removed %s dependencie(s) for %s: %s.' => array(
array(
'%s removed a dependency for %3$s: %4$s.',
'%s removed dependencies for %3$s: %4$s.',
),
),
'%s edited dependencie(s), added %s: %s; removed %s: %s.' => array(
'%s edited dependencies, 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 added %s dependent revision(s): %s.' => array(
array(
'%s added a dependent revision: %3$s.',
'%s added dependent revisions: %3$s.',
),
),
'%s added %s dependent revision(s) for %s: %s.' => array(
array(
'%s added a dependent revision for %3$s: %4$s.',
'%s added dependent revisions for %3$s: %4$s.',
),
),
'%s removed %s dependent revision(s): %s.' => array(
array(
'%s removed a dependent revision: %3$s.',
'%s removed dependent revisions: %3$s.',
),
),
'%s removed %s dependent revision(s) for %s: %s.' => array(
array(
'%s removed a dependent revision for %3$s: %4$s.',
'%s removed dependent revisions for %3$s: %4$s.',
),
),
'%s added %s commit(s): %s.' => array(
array(
'%s added a commit: %3$s.',
'%s added commits: %3$s.',
),
),
'%s removed %s commit(s): %s.' => array(
array(
'%s removed a commit: %3$s.',
'%s removed commits: %3$s.',
),
),
'%s edited commit(s), added %s: %s; removed %s: %s.' =>
'%s edited commits, added %3$s; removed %5$s.',
- '%s added %s reverted commit(s): %s.' => array(
+ '%s added %s reverted change(s): %s.' => array(
array(
- '%s added a reverted commit: %3$s.',
- '%s added reverted commits: %3$s.',
+ '%s added a reverted change: %3$s.',
+ '%s added reverted changes: %3$s.',
),
),
- '%s removed %s reverted commit(s): %s.' => array(
+ '%s removed %s reverted change(s): %s.' => array(
array(
- '%s removed a reverted commit: %3$s.',
- '%s removed reverted commits: %3$s.',
+ '%s removed a reverted change: %3$s.',
+ '%s removed reverted changes: %3$s.',
),
),
- '%s edited reverted commit(s), added %s: %s; removed %s: %s.' =>
- '%s edited reverted commits, added %3$s; removed %5$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 commit(s) for %s: %s.' => array(
+ '%s added %s reverted change(s) for %s: %s.' => array(
array(
- '%s added a reverted commit for %3$s: %4$s.',
- '%s added reverted commits for %3$s: %4$s.',
+ '%s added a reverted change for %3$s: %4$s.',
+ '%s added reverted changes for %3$s: %4$s.',
),
),
- '%s removed %s reverted commit(s) for %s: %s.' => array(
+ '%s removed %s reverted change(s) for %s: %s.' => array(
array(
- '%s removed a reverted commit for %3$s: %4$s.',
- '%s removed reverted commits for %3$s: %4$s.',
+ '%s removed a reverted change for %3$s: %4$s.',
+ '%s removed reverted changes for %3$s: %4$s.',
),
),
- '%s edited reverted commit(s) for %s, added %s: %s; removed %s: %s.' =>
- '%s edited reverted commits for %2$s, added %4$s; removed %6$s.',
+ '%s 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 commit(s): %s.' => array(
+ '%s added %s reverting change(s): %s.' => array(
array(
- '%s added a reverting commit: %3$s.',
- '%s added reverting commits: %3$s.',
+ '%s added a reverting change: %3$s.',
+ '%s added reverting changes: %3$s.',
),
),
- '%s removed %s reverting commit(s): %s.' => array(
+ '%s removed %s reverting change(s): %s.' => array(
array(
- '%s removed a reverting commit: %3$s.',
- '%s removed reverting commits: %3$s.',
+ '%s removed a reverting change: %3$s.',
+ '%s removed reverting changes: %3$s.',
),
),
- '%s edited reverting commit(s), added %s: %s; removed %s: %s.' =>
- '%s edited reverting commits, added %3$s; removed %5$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 commit(s) for %s: %s.' => array(
+ '%s added %s reverting change(s) for %s: %s.' => array(
array(
- '%s added a reverting commit for %3$s: %4$s.',
- '%s added reverting commits for %3$s: %4$s.',
+ '%s added a reverting change for %3$s: %4$s.',
+ '%s added reverting changes for %3$s: %4$s.',
),
),
- '%s removed %s reverting commit(s) for %s: %s.' => array(
+ '%s removed %s reverting change(s) for %s: %s.' => array(
array(
- '%s removed a reverting commit for %3$s: %4$s.',
- '%s removed reverting commits for %3$s: %4$s.',
+ '%s removed a reverting change for %3$s: %4$s.',
+ '%s removed reverting changes for %3$s: %4$s.',
),
),
- '%s edited reverting commit(s) for %s, added %s: %s; removed %s: %s.' =>
- '%s edited reverting commits for %s, added %4$s; removed %6$s.',
+ '%s 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".',
),
);
}
}
diff --git a/src/infrastructure/markup/PhabricatorMarkupOneOff.php b/src/infrastructure/markup/PhabricatorMarkupOneOff.php
index 6bffcf7e2..cc57adc64 100644
--- a/src/infrastructure/markup/PhabricatorMarkupOneOff.php
+++ b/src/infrastructure/markup/PhabricatorMarkupOneOff.php
@@ -1,101 +1,138 @@
<?php
/**
* DEPRECATED. Use @{class:PHUIRemarkupView}.
*/
final class PhabricatorMarkupOneOff
extends Phobject
implements PhabricatorMarkupInterface {
private $content;
private $preserveLinebreaks;
private $engineRuleset;
private $engine;
private $disableCache;
+ private $contentCacheFragment;
+
+ private $generateTableOfContents;
+ private $tableOfContents;
public function setEngineRuleset($engine_ruleset) {
$this->engineRuleset = $engine_ruleset;
return $this;
}
public function getEngineRuleset() {
return $this->engineRuleset;
}
public function setPreserveLinebreaks($preserve_linebreaks) {
$this->preserveLinebreaks = $preserve_linebreaks;
return $this;
}
public function setContent($content) {
$this->content = $content;
return $this;
}
public function getContent() {
return $this->content;
}
public function setEngine(PhutilMarkupEngine $engine) {
$this->engine = $engine;
return $this;
}
public function getEngine() {
return $this->engine;
}
public function setDisableCache($disable_cache) {
$this->disableCache = $disable_cache;
return $this;
}
public function getDisableCache() {
return $this->disableCache;
}
+ public function setGenerateTableOfContents($generate) {
+ $this->generateTableOfContents = $generate;
+ return $this;
+ }
+
+ public function getGenerateTableOfContents() {
+ return $this->generateTableOfContents;
+ }
+
+ public function getTableOfContents() {
+ return $this->tableOfContents;
+ }
+
+ public function setContentCacheFragment($fragment) {
+ $this->contentCacheFragment = $fragment;
+ return $this;
+ }
+
+ public function getContentCacheFragment() {
+ return $this->contentCacheFragment;
+ }
+
public function getMarkupFieldKey($field) {
+ $fragment = $this->getContentCacheFragment();
+ if ($fragment !== null) {
+ return $fragment;
+ }
+
return PhabricatorHash::digestForIndex($this->getContent()).':oneoff';
}
public function newMarkupEngine($field) {
if ($this->engine) {
return $this->engine;
}
if ($this->engineRuleset) {
return PhabricatorMarkupEngine::getEngine($this->engineRuleset);
} else if ($this->preserveLinebreaks) {
return PhabricatorMarkupEngine::getEngine();
} else {
return PhabricatorMarkupEngine::getEngine('nolinebreaks');
}
}
public function getMarkupText($field) {
return $this->getContent();
}
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
+ if ($this->getGenerateTableOfContents()) {
+ $toc = PhutilRemarkupHeaderBlockRule::renderTableOfContents($engine);
+ $this->tableOfContents = $toc;
+ }
+
require_celerity_resource('phabricator-remarkup-css');
+
return phutil_tag(
'div',
array(
'class' => 'phabricator-remarkup',
),
$output);
}
public function shouldUseMarkupCache($field) {
if ($this->getDisableCache()) {
return false;
}
return true;
}
}
diff --git a/src/infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php b/src/infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php
index 35c0ecfad..31b4e5225 100644
--- a/src/infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php
+++ b/src/infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php
@@ -1,390 +1,405 @@
<?php
abstract class PhabricatorObjectRemarkupRule extends PhutilRemarkupRule {
const KEY_RULE_OBJECT = 'rule.object';
const KEY_MENTIONED_OBJECTS = 'rule.object.mentioned';
abstract protected function getObjectNamePrefix();
abstract protected function loadObjects(array $ids);
public function getPriority() {
return 450.0;
}
protected function getObjectNamePrefixBeginsWithWordCharacter() {
$prefix = $this->getObjectNamePrefix();
return preg_match('/^\w/', $prefix);
}
protected function getObjectIDPattern() {
return '[1-9]\d*';
}
protected function shouldMarkupObject(array $params) {
return true;
}
protected function getObjectNameText(
$object,
PhabricatorObjectHandle $handle,
$id) {
return $this->getObjectNamePrefix().$id;
}
protected function loadHandles(array $objects) {
$phids = mpull($objects, 'getPHID');
$viewer = $this->getEngine()->getConfig('viewer');
$handles = $viewer->loadHandles($phids);
$handles = iterator_to_array($handles);
$result = array();
foreach ($objects as $id => $object) {
$result[$id] = $handles[$object->getPHID()];
}
return $result;
}
protected function getObjectHref(
$object,
PhabricatorObjectHandle $handle,
$id) {
$uri = $handle->getURI();
if ($this->getEngine()->getConfig('uri.full')) {
$uri = PhabricatorEnv::getURI($uri);
}
return $uri;
}
protected function renderObjectRefForAnyMedia(
$object,
PhabricatorObjectHandle $handle,
$anchor,
$id) {
$href = $this->getObjectHref($object, $handle, $id);
$text = $this->getObjectNameText($object, $handle, $id);
if ($anchor) {
$href = $href.'#'.$anchor;
$text = $text.'#'.$anchor;
}
if ($this->getEngine()->isTextMode()) {
- return PhabricatorEnv::getProductionURI($href);
+ return $text.' <'.PhabricatorEnv::getProductionURI($href).'>';
} else if ($this->getEngine()->isHTMLMailMode()) {
$href = PhabricatorEnv::getProductionURI($href);
return $this->renderObjectTagForMail($text, $href, $handle);
}
return $this->renderObjectRef($object, $handle, $anchor, $id);
}
protected function renderObjectRef(
$object,
PhabricatorObjectHandle $handle,
$anchor,
$id) {
$href = $this->getObjectHref($object, $handle, $id);
$text = $this->getObjectNameText($object, $handle, $id);
$status_closed = PhabricatorObjectHandle::STATUS_CLOSED;
if ($anchor) {
$href = $href.'#'.$anchor;
$text = $text.'#'.$anchor;
}
$attr = array(
'phid' => $handle->getPHID(),
'closed' => ($handle->getStatus() == $status_closed),
);
return $this->renderHovertag($text, $href, $attr);
}
protected function renderObjectEmbedForAnyMedia(
$object,
PhabricatorObjectHandle $handle,
$options) {
$name = $handle->getFullName();
$href = $handle->getURI();
if ($this->getEngine()->isTextMode()) {
return $name.' <'.PhabricatorEnv::getProductionURI($href).'>';
} else if ($this->getEngine()->isHTMLMailMode()) {
$href = PhabricatorEnv::getProductionURI($href);
return $this->renderObjectTagForMail($name, $href, $handle);
}
return $this->renderObjectEmbed($object, $handle, $options);
}
protected function renderObjectEmbed(
$object,
PhabricatorObjectHandle $handle,
$options) {
$name = $handle->getFullName();
$href = $handle->getURI();
$status_closed = PhabricatorObjectHandle::STATUS_CLOSED;
$attr = array(
'phid' => $handle->getPHID(),
'closed' => ($handle->getStatus() == $status_closed),
);
return $this->renderHovertag($name, $href, $attr);
}
protected function renderObjectTagForMail(
$text,
$href,
PhabricatorObjectHandle $handle) {
$status_closed = PhabricatorObjectHandle::STATUS_CLOSED;
$strikethrough = $handle->getStatus() == $status_closed ?
'text-decoration: line-through;' :
'text-decoration: none;';
return phutil_tag(
'a',
array(
'href' => $href,
'style' => 'background-color: #e7e7e7;
border-color: #e7e7e7;
border-radius: 3px;
padding: 0 4px;
font-weight: bold;
color: black;'
.$strikethrough,
),
$text);
}
protected function renderHovertag($name, $href, array $attr = array()) {
return id(new PHUITagView())
->setName($name)
->setHref($href)
->setType(PHUITagView::TYPE_OBJECT)
->setPHID(idx($attr, 'phid'))
->setClosed(idx($attr, 'closed'))
->render();
}
public function apply($text) {
$text = preg_replace_callback(
$this->getObjectEmbedPattern(),
array($this, 'markupObjectEmbed'),
$text);
$text = preg_replace_callback(
$this->getObjectReferencePattern(),
array($this, 'markupObjectReference'),
$text);
return $text;
}
private function getObjectEmbedPattern() {
$prefix = $this->getObjectNamePrefix();
$prefix = preg_quote($prefix);
$id = $this->getObjectIDPattern();
return '(\B{'.$prefix.'('.$id.')([,\s](?:[^}\\\\]|\\\\.)*)?}\B)u';
}
private function getObjectReferencePattern() {
$prefix = $this->getObjectNamePrefix();
$prefix = preg_quote($prefix);
$id = $this->getObjectIDPattern();
// If the prefix starts with a word character (like "D"), we want to
// require a word boundary so that we don't match "XD1" as "D1". If the
// prefix does not start with a word character, we want to require no word
// boundary for the same reasons. Test if the prefix starts with a word
// character.
if ($this->getObjectNamePrefixBeginsWithWordCharacter()) {
$boundary = '\\b';
} else {
$boundary = '\\B';
}
// The "(?<![#@-])" prevents us from linking "#abcdef" or similar, and
// "ABC-T1" (see T5714), and from matching "@T1" as a task (it is a user)
// (see T9479).
// The "\b" allows us to link "(abcdef)" or similar without linking things
// in the middle of words.
return '((?<![#@-])'.$boundary.$prefix.'('.$id.')(?:#([-\w\d]+))?(?!\w))u';
}
/**
* Extract matched object references from a block of text.
*
* This is intended to make it easy to write unit tests for object remarkup
* rules. Production code is not normally expected to call this method.
*
* @param string Text to match rules against.
* @return wild Matches, suitable for writing unit tests against.
*/
public function extractReferences($text) {
$embed_matches = null;
preg_match_all(
$this->getObjectEmbedPattern(),
$text,
$embed_matches,
PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
$ref_matches = null;
preg_match_all(
$this->getObjectReferencePattern(),
$text,
$ref_matches,
PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
$results = array();
$sets = array(
'embed' => $embed_matches,
'ref' => $ref_matches,
);
foreach ($sets as $type => $matches) {
$formatted = array();
foreach ($matches as $match) {
$format = array(
'offset' => $match[1][1],
'id' => $match[1][0],
);
if (isset($match[2][0])) {
$format['tail'] = $match[2][0];
}
$formatted[] = $format;
}
$results[$type] = $formatted;
}
return $results;
}
public function markupObjectEmbed(array $matches) {
if (!$this->isFlatText($matches[0])) {
return $matches[0];
}
+ // If we're rendering a table of contents, just render the raw input.
+ // This could perhaps be handled more gracefully but it seems unusual to
+ // put something like "{P123}" in a header and it's not obvious what users
+ // expect? See T8845.
+ $engine = $this->getEngine();
+ if ($engine->getState('toc')) {
+ return $matches[0];
+ }
+
return $this->markupObject(array(
'type' => 'embed',
'id' => $matches[1],
'options' => idx($matches, 2),
'original' => $matches[0],
));
}
public function markupObjectReference(array $matches) {
if (!$this->isFlatText($matches[0])) {
return $matches[0];
}
+ // If we're rendering a table of contents, just render the monogram.
+ $engine = $this->getEngine();
+ if ($engine->getState('toc')) {
+ return $matches[0];
+ }
+
return $this->markupObject(array(
'type' => 'ref',
'id' => $matches[1],
'anchor' => idx($matches, 2),
'original' => $matches[0],
));
}
private function markupObject(array $params) {
if (!$this->shouldMarkupObject($params)) {
return $params['original'];
}
$regex = trim(
PhabricatorEnv::getEnvConfig('remarkup.ignored-object-names'));
if ($regex && preg_match($regex, $params['original'])) {
return $params['original'];
}
$engine = $this->getEngine();
$token = $engine->storeText('x');
$metadata_key = self::KEY_RULE_OBJECT.'.'.$this->getObjectNamePrefix();
$metadata = $engine->getTextMetadata($metadata_key, array());
$metadata[] = array(
'token' => $token,
) + $params;
$engine->setTextMetadata($metadata_key, $metadata);
return $token;
}
public function didMarkupText() {
$engine = $this->getEngine();
$metadata_key = self::KEY_RULE_OBJECT.'.'.$this->getObjectNamePrefix();
$metadata = $engine->getTextMetadata($metadata_key, array());
if (!$metadata) {
return;
}
$ids = ipull($metadata, 'id');
$objects = $this->loadObjects($ids);
// For objects that are invalid or which the user can't see, just render
// the original text.
// TODO: We should probably distinguish between these cases and render a
// "you can't see this" state for nonvisible objects.
foreach ($metadata as $key => $spec) {
if (empty($objects[$spec['id']])) {
$engine->overwriteStoredText(
$spec['token'],
$spec['original']);
unset($metadata[$key]);
}
}
$phids = $engine->getTextMetadata(self::KEY_MENTIONED_OBJECTS, array());
foreach ($objects as $object) {
$phids[$object->getPHID()] = $object->getPHID();
}
$engine->setTextMetadata(self::KEY_MENTIONED_OBJECTS, $phids);
$handles = $this->loadHandles($objects);
foreach ($metadata as $key => $spec) {
$handle = $handles[$spec['id']];
$object = $objects[$spec['id']];
switch ($spec['type']) {
case 'ref':
$view = $this->renderObjectRefForAnyMedia(
$object,
$handle,
$spec['anchor'],
$spec['id']);
break;
case 'embed':
$spec['options'] = $this->assertFlatText($spec['options']);
$view = $this->renderObjectEmbedForAnyMedia(
$object,
$handle,
$spec['options']);
break;
}
$engine->overwriteStoredText($spec['token'], $view);
}
$engine->setTextMetadata($metadata_key, array());
}
}
diff --git a/src/infrastructure/markup/rule/PhabricatorYoutubeRemarkupRule.php b/src/infrastructure/markup/rule/PhabricatorYoutubeRemarkupRule.php
index 25422b970..cbf322b2d 100644
--- a/src/infrastructure/markup/rule/PhabricatorYoutubeRemarkupRule.php
+++ b/src/infrastructure/markup/rule/PhabricatorYoutubeRemarkupRule.php
@@ -1,55 +1,60 @@
<?php
final class PhabricatorYoutubeRemarkupRule extends PhutilRemarkupRule {
public function getPriority() {
return 350.0;
}
public function apply($text) {
try {
$uri = new PhutilURI($text);
} catch (Exception $ex) {
return $text;
}
$domain = $uri->getDomain();
if (!preg_match('/(^|\.)youtube\.com\z/', $domain)) {
return $text;
}
$params = $uri->getQueryParams();
$v_param = idx($params, 'v');
if (!strlen($v_param)) {
return $text;
}
$text_mode = $this->getEngine()->isTextMode();
$mail_mode = $this->getEngine()->isHTMLMailMode();
if ($text_mode || $mail_mode) {
return $text;
}
$youtube_src = 'https://www.youtube.com/embed/'.$v_param;
$iframe = $this->newTag(
'div',
array(
'class' => 'embedded-youtube-video',
),
$this->newTag(
'iframe',
array(
'width' => '650',
'height' => '400',
'style' => 'margin: 1em auto; border: 0px;',
'src' => $youtube_src,
'frameborder' => 0,
),
''));
return $this->getEngine()->storeText($iframe);
}
+ public function didMarkupText() {
+ CelerityAPI::getStaticResourceResponse()
+ ->addContentSecurityPolicyURI('frame-src', 'https://www.youtube.com/');
+ }
+
}
diff --git a/src/infrastructure/markup/view/PHUIRemarkupImageView.php b/src/infrastructure/markup/view/PHUIRemarkupImageView.php
new file mode 100644
index 000000000..0402f000a
--- /dev/null
+++ b/src/infrastructure/markup/view/PHUIRemarkupImageView.php
@@ -0,0 +1,79 @@
+<?php
+
+final class PHUIRemarkupImageView
+ extends AphrontView {
+
+ private $uri;
+ private $width;
+ private $height;
+ private $alt;
+ private $classes = array();
+
+ public function setURI($uri) {
+ $this->uri = $uri;
+ return $this;
+ }
+
+ public function getURI() {
+ return $this->uri;
+ }
+
+ 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 setAlt($alt) {
+ $this->alt = $alt;
+ return $this;
+ }
+
+ public function getAlt() {
+ return $this->alt;
+ }
+
+ public function addClass($class) {
+ $this->classes[] = $class;
+ return $this;
+ }
+
+ public function render() {
+ $id = celerity_generate_unique_node_id();
+
+ Javelin::initBehavior(
+ 'remarkup-load-image',
+ array(
+ 'uri' => (string)$this->uri,
+ 'imageID' => $id,
+ ));
+
+ $classes = null;
+ if ($this->classes) {
+ $classes = implode(' ', $this->classes);
+ }
+
+ return phutil_tag(
+ 'img',
+ array(
+ 'id' => $id,
+ 'width' => $this->getWidth(),
+ 'height' => $this->getHeight(),
+ 'alt' => $this->getAlt(),
+ 'class' => $classes,
+ ));
+ }
+
+}
diff --git a/src/infrastructure/markup/view/PHUIRemarkupView.php b/src/infrastructure/markup/view/PHUIRemarkupView.php
index e5772e9d8..d60d28b5d 100644
--- a/src/infrastructure/markup/view/PHUIRemarkupView.php
+++ b/src/infrastructure/markup/view/PHUIRemarkupView.php
@@ -1,96 +1,138 @@
<?php
/**
* Simple API for rendering blocks of Remarkup.
*
* Example usage:
*
* $fancy_text = new PHUIRemarkupView($viewer, $raw_remarkup);
* $view->appendChild($fancy_text);
*
*/
final class PHUIRemarkupView extends AphrontView {
private $corpus;
private $contextObject;
private $options;
+ private $oneoff;
+ private $generateTableOfContents;
// TODO: In the long run, rules themselves should define available options.
// For now, just define constants here so we can more easily replace things
// later once this is cleaned up.
const OPTION_PRESERVE_LINEBREAKS = 'preserve-linebreaks';
+ const OPTION_GENERATE_TOC = 'header.generate-toc';
public function __construct(PhabricatorUser $viewer, $corpus) {
$this->setUser($viewer);
$this->corpus = $corpus;
}
public function setContextObject($context_object) {
$this->contextObject = $context_object;
return $this;
}
public function getContextObject() {
return $this->contextObject;
}
public function setRemarkupOption($key, $value) {
$this->options[$key] = $value;
return $this;
}
public function setRemarkupOptions(array $options) {
foreach ($options as $key => $value) {
$this->setRemarkupOption($key, $value);
}
return $this;
}
+ public function setGenerateTableOfContents($generate) {
+ $this->generateTableOfContents = $generate;
+ return $this;
+ }
+
+ public function getGenerateTableOfContents() {
+ return $this->generateTableOfContents;
+ }
+
+ public function getTableOfContents() {
+ return $this->oneoff->getTableOfContents();
+ }
+
public function render() {
$viewer = $this->getViewer();
$corpus = $this->corpus;
$context = $this->getContextObject();
$options = $this->options;
$oneoff = id(new PhabricatorMarkupOneOff())
- ->setContent($corpus);
+ ->setContent($corpus)
+ ->setContentCacheFragment($this->getContentCacheFragment());
if ($options) {
$oneoff->setEngine($this->getEngine());
} else {
$oneoff->setPreserveLinebreaks(true);
}
+ $generate_toc = $this->getGenerateTableOfContents();
+ $oneoff->setGenerateTableOfContents($generate_toc);
+ $this->oneoff = $oneoff;
+
$content = PhabricatorMarkupEngine::renderOneObject(
$oneoff,
'default',
$viewer,
$context);
return $content;
}
private function getEngine() {
$options = $this->options;
$viewer = $this->getViewer();
$viewer_key = $viewer->getCacheFragment();
-
- ksort($options);
- $engine_key = serialize($options);
- $engine_key = PhabricatorHash::digestForIndex($engine_key);
+ $engine_key = $this->getEngineCacheFragment();
$cache = PhabricatorCaches::getRequestCache();
$cache_key = "remarkup.engine({$viewer_key}, {$engine_key})";
$engine = $cache->getKey($cache_key);
if (!$engine) {
$engine = PhabricatorMarkupEngine::newMarkupEngine($options);
$cache->setKey($cache_key, $engine);
}
return $engine;
}
+ private function getEngineCacheFragment() {
+ $options = $this->options;
+
+ ksort($options);
+
+ $engine_key = serialize($options);
+ $engine_key = PhabricatorHash::digestForIndex($engine_key);
+
+ return $engine_key;
+ }
+
+ private function getContentCacheFragment() {
+ $corpus = $this->corpus;
+
+ $content_fragment = PhabricatorHash::digestForIndex($corpus);
+ $options_fragment = array(
+ 'toc' => $this->getGenerateTableOfContents(),
+ );
+ $options_fragment = serialize($options_fragment);
+ $options_fragment = PhabricatorHash::digestForIndex($options_fragment);
+
+ return "remarkup({$content_fragment}, {$options_fragment})";
+ }
+
}
diff --git a/src/infrastructure/query/constraint/PhabricatorQueryConstraint.php b/src/infrastructure/query/constraint/PhabricatorQueryConstraint.php
index 54cd7ae51..874d8756e 100644
--- a/src/infrastructure/query/constraint/PhabricatorQueryConstraint.php
+++ b/src/infrastructure/query/constraint/PhabricatorQueryConstraint.php
@@ -1,39 +1,40 @@
<?php
final class PhabricatorQueryConstraint extends Phobject {
const OPERATOR_AND = 'and';
const OPERATOR_OR = 'or';
const OPERATOR_NOT = 'not';
const OPERATOR_NULL = 'null';
const OPERATOR_ANCESTOR = 'ancestor';
const OPERATOR_EMPTY = 'empty';
const OPERATOR_ONLY = 'only';
+ const OPERATOR_ANY = 'any';
private $operator;
private $value;
public function __construct($operator, $value) {
$this->operator = $operator;
$this->value = $value;
}
public function setOperator($operator) {
$this->operator = $operator;
return $this;
}
public function getOperator() {
return $this->operator;
}
public function setValue($value) {
$this->value = $value;
return $this;
}
public function getValue() {
return $this->value;
}
}
diff --git a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
index 63daa6df7..5193b1497 100644
--- a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
+++ b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
@@ -1,2827 +1,2920 @@
<?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(),
- 'value' => $value,
+ '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((array)$value) > 1) {
+ 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_r) {
+ AphrontDatabaseConnection $conn) {
$joins = array();
foreach ($this->applicationSearchConstraints as $key => $constraint) {
$table = $constraint['table'];
- $alias = 'appsearch_'.$key;
+ $alias = $constraint['alias'];
$index = $constraint['index'];
$cond = $constraint['cond'];
$phid_column = $this->getApplicationSearchObjectPHIDColumn();
switch ($cond) {
case '=':
- $type = $constraint['type'];
- switch ($type) {
- case 'string':
- $constraint_clause = qsprintf(
- $conn_r,
- '%T.indexValue IN (%Ls)',
- $alias,
- (array)$constraint['value']);
- break;
- case 'int':
- $constraint_clause = qsprintf(
- $conn_r,
- '%T.indexValue IN (%Ld)',
- $alias,
- (array)$constraint['value']);
+ // 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;
- default:
- throw new Exception(pht('Unknown index type "%s"!', $type));
+ }
}
$joins[] = qsprintf(
- $conn_r,
- 'JOIN %T %T ON %T.objectPHID = %Q
- AND %T.indexKey = %s
- AND (%Q)',
+ $conn,
+ '%Q %T %T ON %T.objectPHID = %Q
+ AND %T.indexKey = %s',
+ $join_type,
$table,
$alias,
$alias,
$phid_column,
$alias,
- $index,
- $constraint_clause);
+ $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_r,
+ $conn,
'%T.indexValue <= %d',
$alias,
$max);
} else if ($max === null) {
$constraint_clause = qsprintf(
- $conn_r,
+ $conn,
'%T.indexValue >= %d',
$alias,
$min);
} else {
$constraint_clause = qsprintf(
- $conn_r,
+ $conn,
'%T.indexValue BETWEEN %d AND %d',
$alias,
$min,
$max);
}
$joins[] = qsprintf(
- $conn_r,
+ $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_r,
+ $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;
$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 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/ssh/PhabricatorSSHWorkflow.php b/src/infrastructure/ssh/PhabricatorSSHWorkflow.php
index 6b6222304..f7739f133 100644
--- a/src/infrastructure/ssh/PhabricatorSSHWorkflow.php
+++ b/src/infrastructure/ssh/PhabricatorSSHWorkflow.php
@@ -1,113 +1,123 @@
<?php
abstract class PhabricatorSSHWorkflow
extends PhutilArgumentWorkflow {
// NOTE: We are explicitly extending "PhutilArgumentWorkflow", not
// "PhabricatorManagementWorkflow". We want to avoid inheriting "getViewer()"
// and other methods which assume workflows are administrative commands
// like `bin/storage`.
private $sshUser;
private $iochannel;
private $errorChannel;
private $isClusterRequest;
private $originalArguments;
+ private $requestIdentifier;
public function isExecutable() {
return false;
}
public function setErrorChannel(PhutilChannel $error_channel) {
$this->errorChannel = $error_channel;
return $this;
}
public function getErrorChannel() {
return $this->errorChannel;
}
public function setSSHUser(PhabricatorUser $ssh_user) {
$this->sshUser = $ssh_user;
return $this;
}
public function getSSHUser() {
return $this->sshUser;
}
public function setIOChannel(PhutilChannel $channel) {
$this->iochannel = $channel;
return $this;
}
public function getIOChannel() {
return $this->iochannel;
}
public function readAllInput() {
$channel = $this->getIOChannel();
while ($channel->update()) {
PhutilChannel::waitForAny(array($channel));
if (!$channel->isOpenForReading()) {
break;
}
}
return $channel->read();
}
public function writeIO($data) {
$this->getIOChannel()->write($data);
return $this;
}
public function writeErrorIO($data) {
$this->getErrorChannel()->write($data);
return $this;
}
protected function newPassthruCommand() {
return id(new PhabricatorSSHPassthruCommand())
->setErrorChannel($this->getErrorChannel());
}
public function setIsClusterRequest($is_cluster_request) {
$this->isClusterRequest = $is_cluster_request;
return $this;
}
public function getIsClusterRequest() {
return $this->isClusterRequest;
}
public function setOriginalArguments(array $original_arguments) {
$this->originalArguments = $original_arguments;
return $this;
}
public function getOriginalArguments() {
return $this->originalArguments;
}
+ public function setRequestIdentifier($request_identifier) {
+ $this->requestIdentifier = $request_identifier;
+ return $this;
+ }
+
+ public function getRequestIdentifier() {
+ return $this->requestIdentifier;
+ }
+
public function getSSHRemoteAddress() {
$ssh_client = getenv('SSH_CLIENT');
if (!strlen($ssh_client)) {
return null;
}
// TODO: When commands are proxied, the original remote address should
// also be proxied.
// This has the format "<ip> <remote-port> <local-port>". Grab the IP.
$remote_address = head(explode(' ', $ssh_client));
try {
$address = PhutilIPAddress::newAddress($remote_address);
} catch (Exception $ex) {
return null;
}
return $address->getAddress();
}
}
diff --git a/src/infrastructure/status/PhabricatorObjectStatus.php b/src/infrastructure/status/PhabricatorObjectStatus.php
new file mode 100644
index 000000000..27dec48dc
--- /dev/null
+++ b/src/infrastructure/status/PhabricatorObjectStatus.php
@@ -0,0 +1,84 @@
+<?php
+
+abstract class PhabricatorObjectStatus
+ extends Phobject {
+
+ private $key;
+ private $properties;
+
+ protected function __construct($key = null, array $properties = array()) {
+ $this->key = $key;
+ $this->properties = $properties;
+ }
+
+ protected function getStatusProperty($key) {
+ if (!array_key_exists($key, $this->properties)) {
+ throw new Exception(
+ pht(
+ 'Attempting to access unknown status property ("%s").',
+ $key));
+ }
+
+ return $this->properties[$key];
+ }
+
+ public function getKey() {
+ return $this->key;
+ }
+
+ public function getIcon() {
+ return $this->getStatusProperty('icon');
+ }
+
+ public function getDisplayName() {
+ return $this->getStatusProperty('name');
+ }
+
+ public function getColor() {
+ return $this->getStatusProperty('color');
+ }
+
+ protected function getStatusSpecification($status) {
+ $map = self::getStatusSpecifications();
+ if (isset($map[$status])) {
+ return $map[$status];
+ }
+
+ return array(
+ 'key' => $status,
+ 'name' => pht('Unknown ("%s")', $status),
+ 'icon' => 'fa-question-circle',
+ 'color' => 'indigo',
+ ) + $this->newUnknownStatusSpecification($status);
+ }
+
+ protected function getStatusSpecifications() {
+ $map = $this->newStatusSpecifications();
+
+ $result = array();
+ foreach ($map as $item) {
+ if (!array_key_exists('key', $item)) {
+ throw new Exception(pht('Status specification has no "key".'));
+ }
+
+ $key = $item['key'];
+ if (isset($result[$key])) {
+ throw new Exception(
+ pht(
+ 'Multiple status definitions share the same key ("%s").',
+ $key));
+ }
+
+ $result[$key] = $item;
+ }
+
+ return $result;
+ }
+
+ abstract protected function newStatusSpecifications();
+
+ protected function newUnknownStatusSpecification($status) {
+ return array();
+ }
+
+}
diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php
index f6b120ba8..5b9459cfb 100644
--- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php
+++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php
@@ -1,1225 +1,1229 @@
<?php
abstract class PhabricatorStorageManagementWorkflow
extends PhabricatorManagementWorkflow {
private $apis = array();
private $dryRun;
private $force;
private $patches;
private $didInitialize;
final public function setAPIs(array $apis) {
$this->apis = $apis;
return $this;
}
final public function getAnyAPI() {
return head($this->getAPIs());
}
final public function getMasterAPIs() {
$apis = $this->getAPIs();
$results = array();
foreach ($apis as $api) {
if ($api->getRef()->getIsMaster()) {
$results[] = $api;
}
}
if (!$results) {
throw new PhutilArgumentUsageException(
pht(
'This command only operates on database masters, but the selected '.
'database hosts do not include any masters.'));
}
return $results;
}
final public function getSingleAPI() {
$apis = $this->getAPIs();
if (count($apis) == 1) {
return head($apis);
}
throw new PhutilArgumentUsageException(
pht(
'Phabricator is configured in cluster mode, with multiple database '.
'hosts. Use "--host" to specify which host you want to operate on.'));
}
final public function getAPIs() {
return $this->apis;
}
final protected function isDryRun() {
return $this->dryRun;
}
final protected function setDryRun($dry_run) {
$this->dryRun = $dry_run;
return $this;
}
final protected function isForce() {
return $this->force;
}
final protected function setForce($force) {
$this->force = $force;
return $this;
}
public function getPatches() {
return $this->patches;
}
public function setPatches(array $patches) {
assert_instances_of($patches, 'PhabricatorStoragePatch');
$this->patches = $patches;
return $this;
}
protected function isReadOnlyWorkflow() {
return false;
}
public function execute(PhutilArgumentParser $args) {
$this->setDryRun($args->getArg('dryrun'));
$this->setForce($args->getArg('force'));
if (!$this->isReadOnlyWorkflow()) {
if (PhabricatorEnv::isReadOnly()) {
if ($this->isForce()) {
PhabricatorEnv::setReadOnly(false, null);
} else {
throw new PhutilArgumentUsageException(
pht(
'Phabricator is currently in read-only mode. Use --force to '.
'override this mode.'));
}
}
}
return $this->didExecute($args);
}
public function didExecute(PhutilArgumentParser $args) {}
private function loadSchemata(PhabricatorStorageManagementAPI $api) {
$query = id(new PhabricatorConfigSchemaQuery());
$ref = $api->getRef();
$ref_key = $ref->getRefKey();
$query->setAPIs(array($api));
$query->setRefs(array($ref));
$actual = $query->loadActualSchemata();
$expect = $query->loadExpectedSchemata();
$comp = $query->buildComparisonSchemata($expect, $actual);
return array(
$comp[$ref_key],
$expect[$ref_key],
$actual[$ref_key],
);
}
final protected function adjustSchemata(
PhabricatorStorageManagementAPI $api,
$unsafe) {
$lock = $this->lock($api);
try {
$err = $this->doAdjustSchemata($api, $unsafe);
// Analyze tables if we're not doing a dry run and adjustments are either
// all clear or have minor errors like surplus tables.
if (!$this->dryRun) {
$should_analyze = (($err == 0) || ($err == 2));
if ($should_analyze) {
$this->analyzeTables($api);
}
}
} catch (Exception $ex) {
$lock->unlock();
throw $ex;
}
$lock->unlock();
return $err;
}
final private function doAdjustSchemata(
PhabricatorStorageManagementAPI $api,
$unsafe) {
$console = PhutilConsole::getConsole();
$console->writeOut(
"%s\n",
pht(
'Verifying database schemata on "%s"...',
$api->getRef()->getRefKey()));
list($adjustments, $errors) = $this->findAdjustments($api);
if (!$adjustments) {
$console->writeOut(
"%s\n",
pht('Found no adjustments for schemata.'));
return $this->printErrors($errors, 0);
}
if (!$this->force && !$api->isCharacterSetAvailable('utf8mb4')) {
$message = pht(
"You have an old version of MySQL (older than 5.5) which does not ".
"support the utf8mb4 character set. We strongly recommend upgrading ".
"to 5.5 or newer.\n\n".
"If you apply adjustments now and later update MySQL to 5.5 or newer, ".
"you'll need to apply adjustments again (and they will take a long ".
"time).\n\n".
"You can exit this workflow, update MySQL now, and then run this ".
"workflow again. This is recommended, but may cause a lot of downtime ".
"right now.\n\n".
"You can exit this workflow, continue using Phabricator without ".
"applying adjustments, update MySQL at a later date, and then run ".
"this workflow again. This is also a good approach, and will let you ".
"delay downtime until later.\n\n".
"You can proceed with this workflow, and then optionally update ".
"MySQL at a later date. After you do, you'll need to apply ".
"adjustments again.\n\n".
"For more information, see \"Managing Storage Adjustments\" in ".
"the documentation.");
$console->writeOut(
"\n**<bg:yellow> %s </bg>**\n\n%s\n",
pht('OLD MySQL VERSION'),
phutil_console_wrap($message));
$prompt = pht('Continue with old MySQL version?');
if (!phutil_console_confirm($prompt, $default_no = true)) {
return;
}
}
$table = id(new PhutilConsoleTable())
->addColumn('database', array('title' => pht('Database')))
->addColumn('table', array('title' => pht('Table')))
->addColumn('name', array('title' => pht('Name')))
->addColumn('info', array('title' => pht('Issues')));
foreach ($adjustments as $adjust) {
$info = array();
foreach ($adjust['issues'] as $issue) {
$info[] = PhabricatorConfigStorageSchema::getIssueName($issue);
}
$table->addRow(array(
'database' => $adjust['database'],
'table' => idx($adjust, 'table'),
'name' => idx($adjust, 'name'),
'info' => implode(', ', $info),
));
}
$console->writeOut("\n\n");
$table->draw();
if ($this->dryRun) {
$console->writeOut(
"%s\n",
pht('DRYRUN: Would apply adjustments.'));
return 0;
} else if ($this->didInitialize) {
// If we just initialized the database, continue without prompting. This
// is nicer for first-time setup and there's no reasonable reason any
// user would ever answer "no" to the prompt against an empty schema.
} else if (!$this->force) {
$console->writeOut(
"\n%s\n",
pht(
"Found %s adjustment(s) to apply, detailed above.\n\n".
"You can review adjustments in more detail from the web interface, ".
"in Config > Database Status. To better understand the adjustment ".
"workflow, see \"Managing Storage Adjustments\" in the ".
"documentation.\n\n".
"MySQL needs to copy table data to make some adjustments, so these ".
"migrations may take some time.",
phutil_count($adjustments)));
$prompt = pht('Apply these schema adjustments?');
if (!phutil_console_confirm($prompt, $default_no = true)) {
return 1;
}
}
$console->writeOut(
"%s\n",
pht('Applying schema adjustments...'));
$conn = $api->getConn(null);
if ($unsafe) {
queryfx($conn, 'SET SESSION sql_mode = %s', '');
} else {
queryfx($conn, 'SET SESSION sql_mode = %s', 'STRICT_ALL_TABLES');
}
$failed = array();
// We make changes in several phases.
$phases = array(
// Drop surplus autoincrements. This allows us to drop primary keys on
// autoincrement columns.
'drop_auto',
// Drop all keys we're going to adjust. This prevents them from
// interfering with column changes.
'drop_keys',
// Apply all database, table, and column changes.
'main',
// Restore adjusted keys.
'add_keys',
// Add missing autoincrements.
'add_auto',
);
$bar = id(new PhutilConsoleProgressBar())
->setTotal(count($adjustments) * count($phases));
foreach ($phases as $phase) {
foreach ($adjustments as $adjust) {
try {
switch ($adjust['kind']) {
case 'database':
if ($phase == 'main') {
queryfx(
$conn,
'ALTER DATABASE %T CHARACTER SET = %s COLLATE = %s',
$adjust['database'],
$adjust['charset'],
$adjust['collation']);
}
break;
case 'table':
if ($phase == 'main') {
queryfx(
$conn,
'ALTER TABLE %T.%T COLLATE = %s, ENGINE = %s',
$adjust['database'],
$adjust['table'],
$adjust['collation'],
$adjust['engine']);
}
break;
case 'column':
$apply = false;
$auto = false;
$new_auto = idx($adjust, 'auto');
if ($phase == 'drop_auto') {
if ($new_auto === false) {
$apply = true;
$auto = false;
}
} else if ($phase == 'main') {
$apply = true;
if ($new_auto === false) {
$auto = false;
} else {
$auto = $adjust['is_auto'];
}
} else if ($phase == 'add_auto') {
if ($new_auto === true) {
$apply = true;
$auto = true;
}
}
if ($apply) {
$parts = array();
if ($auto) {
$parts[] = qsprintf(
$conn,
'AUTO_INCREMENT');
}
if ($adjust['charset']) {
$parts[] = qsprintf(
$conn,
'CHARACTER SET %Q COLLATE %Q',
$adjust['charset'],
$adjust['collation']);
}
queryfx(
$conn,
'ALTER TABLE %T.%T MODIFY %T %Q %Q %Q',
$adjust['database'],
$adjust['table'],
$adjust['name'],
$adjust['type'],
implode(' ', $parts),
$adjust['nullable'] ? 'NULL' : 'NOT NULL');
}
break;
case 'key':
if (($phase == 'drop_keys') && $adjust['exists']) {
if ($adjust['name'] == 'PRIMARY') {
$key_name = 'PRIMARY KEY';
} else {
$key_name = qsprintf($conn, 'KEY %T', $adjust['name']);
}
queryfx(
$conn,
'ALTER TABLE %T.%T DROP %Q',
$adjust['database'],
$adjust['table'],
$key_name);
}
if (($phase == 'add_keys') && $adjust['keep']) {
// Different keys need different creation syntax. Notable
// special cases are primary keys and fulltext keys.
if ($adjust['name'] == 'PRIMARY') {
$key_name = 'PRIMARY KEY';
} else if ($adjust['indexType'] == 'FULLTEXT') {
$key_name = qsprintf($conn, 'FULLTEXT %T', $adjust['name']);
} else {
if ($adjust['unique']) {
$key_name = qsprintf(
$conn,
'UNIQUE KEY %T',
$adjust['name']);
} else {
$key_name = qsprintf(
$conn,
'/* NONUNIQUE */ KEY %T',
$adjust['name']);
}
}
queryfx(
$conn,
'ALTER TABLE %T.%T ADD %Q (%Q)',
$adjust['database'],
$adjust['table'],
$key_name,
implode(', ', $adjust['columns']));
}
break;
default:
throw new Exception(
pht('Unknown schema adjustment kind "%s"!', $adjust['kind']));
}
} catch (AphrontQueryException $ex) {
$failed[] = array($adjust, $ex);
}
$bar->update(1);
}
}
$bar->done();
if (!$failed) {
$console->writeOut(
"%s\n",
pht('Completed applying all schema adjustments.'));
$err = 0;
} else {
$table = id(new PhutilConsoleTable())
->addColumn('target', array('title' => pht('Target')))
->addColumn('error', array('title' => pht('Error')));
foreach ($failed as $failure) {
list($adjust, $ex) = $failure;
$pieces = array_select_keys(
$adjust,
array('database', 'table', 'name'));
$pieces = array_filter($pieces);
$target = implode('.', $pieces);
$table->addRow(
array(
'target' => $target,
'error' => $ex->getMessage(),
));
}
$console->writeOut("\n");
$table->draw();
$console->writeOut(
"\n%s\n",
pht('Failed to make some schema adjustments, detailed above.'));
$console->writeOut(
"%s\n",
pht(
'For help troubleshooting adjustments, see "Managing Storage '.
'Adjustments" in the documentation.'));
$err = 1;
}
return $this->printErrors($errors, $err);
}
private function findAdjustments(
PhabricatorStorageManagementAPI $api) {
list($comp, $expect, $actual) = $this->loadSchemata($api);
$issue_charset = PhabricatorConfigStorageSchema::ISSUE_CHARSET;
$issue_collation = PhabricatorConfigStorageSchema::ISSUE_COLLATION;
$issue_columntype = PhabricatorConfigStorageSchema::ISSUE_COLUMNTYPE;
$issue_surpluskey = PhabricatorConfigStorageSchema::ISSUE_SURPLUSKEY;
$issue_missingkey = PhabricatorConfigStorageSchema::ISSUE_MISSINGKEY;
$issue_columns = PhabricatorConfigStorageSchema::ISSUE_KEYCOLUMNS;
$issue_unique = PhabricatorConfigStorageSchema::ISSUE_UNIQUE;
$issue_longkey = PhabricatorConfigStorageSchema::ISSUE_LONGKEY;
$issue_auto = PhabricatorConfigStorageSchema::ISSUE_AUTOINCREMENT;
$issue_engine = PhabricatorConfigStorageSchema::ISSUE_ENGINE;
$adjustments = array();
$errors = array();
foreach ($comp->getDatabases() as $database_name => $database) {
foreach ($this->findErrors($database) as $issue) {
$errors[] = array(
'database' => $database_name,
'issue' => $issue,
);
}
$expect_database = $expect->getDatabase($database_name);
$actual_database = $actual->getDatabase($database_name);
if (!$expect_database || !$actual_database) {
// If there's a real issue here, skip this stuff.
continue;
}
if ($actual_database->getAccessDenied()) {
// If we can't access the database, we can't access the tables either.
continue;
}
$issues = array();
if ($database->hasIssue($issue_charset)) {
$issues[] = $issue_charset;
}
if ($database->hasIssue($issue_collation)) {
$issues[] = $issue_collation;
}
if ($issues) {
$adjustments[] = array(
'kind' => 'database',
'database' => $database_name,
'issues' => $issues,
'charset' => $expect_database->getCharacterSet(),
'collation' => $expect_database->getCollation(),
);
}
foreach ($database->getTables() as $table_name => $table) {
foreach ($this->findErrors($table) as $issue) {
$errors[] = array(
'database' => $database_name,
'table' => $table_name,
'issue' => $issue,
);
}
$expect_table = $expect_database->getTable($table_name);
$actual_table = $actual_database->getTable($table_name);
if (!$expect_table || !$actual_table) {
continue;
}
$issues = array();
if ($table->hasIssue($issue_collation)) {
$issues[] = $issue_collation;
}
if ($table->hasIssue($issue_engine)) {
$issues[] = $issue_engine;
}
if ($issues) {
$adjustments[] = array(
'kind' => 'table',
'database' => $database_name,
'table' => $table_name,
'issues' => $issues,
'collation' => $expect_table->getCollation(),
'engine' => $expect_table->getEngine(),
);
}
foreach ($table->getColumns() as $column_name => $column) {
foreach ($this->findErrors($column) as $issue) {
$errors[] = array(
'database' => $database_name,
'table' => $table_name,
'name' => $column_name,
'issue' => $issue,
);
}
$expect_column = $expect_table->getColumn($column_name);
$actual_column = $actual_table->getColumn($column_name);
if (!$expect_column || !$actual_column) {
continue;
}
$issues = array();
if ($column->hasIssue($issue_collation)) {
$issues[] = $issue_collation;
}
if ($column->hasIssue($issue_charset)) {
$issues[] = $issue_charset;
}
if ($column->hasIssue($issue_columntype)) {
$issues[] = $issue_columntype;
}
if ($column->hasIssue($issue_auto)) {
$issues[] = $issue_auto;
}
if ($issues) {
if ($expect_column->getCharacterSet() === null) {
// For non-text columns, we won't be specifying a collation or
// character set.
$charset = null;
$collation = null;
} else {
$charset = $expect_column->getCharacterSet();
$collation = $expect_column->getCollation();
}
$adjustment = array(
'kind' => 'column',
'database' => $database_name,
'table' => $table_name,
'name' => $column_name,
'issues' => $issues,
'collation' => $collation,
'charset' => $charset,
'type' => $expect_column->getColumnType(),
// NOTE: We don't adjust column nullability because it is
// dangerous, so always use the current nullability.
'nullable' => $actual_column->getNullable(),
// NOTE: This always stores the current value, because we have
// to make these updates separately.
'is_auto' => $actual_column->getAutoIncrement(),
);
if ($column->hasIssue($issue_auto)) {
$adjustment['auto'] = $expect_column->getAutoIncrement();
}
$adjustments[] = $adjustment;
}
}
foreach ($table->getKeys() as $key_name => $key) {
foreach ($this->findErrors($key) as $issue) {
$errors[] = array(
'database' => $database_name,
'table' => $table_name,
'name' => $key_name,
'issue' => $issue,
);
}
$expect_key = $expect_table->getKey($key_name);
$actual_key = $actual_table->getKey($key_name);
$issues = array();
$keep_key = true;
if ($key->hasIssue($issue_surpluskey)) {
$issues[] = $issue_surpluskey;
$keep_key = false;
}
if ($key->hasIssue($issue_missingkey)) {
$issues[] = $issue_missingkey;
}
if ($key->hasIssue($issue_columns)) {
$issues[] = $issue_columns;
}
if ($key->hasIssue($issue_unique)) {
$issues[] = $issue_unique;
}
// NOTE: We can't really fix this, per se, but we may need to remove
// the key to change the column type. In the best case, the new
// column type won't be overlong and recreating the key really will
// fix the issue. In the worst case, we get the right column type and
// lose the key, which is still better than retaining the key having
// the wrong column type.
if ($key->hasIssue($issue_longkey)) {
$issues[] = $issue_longkey;
}
if ($issues) {
$adjustment = array(
'kind' => 'key',
'database' => $database_name,
'table' => $table_name,
'name' => $key_name,
'issues' => $issues,
'exists' => (bool)$actual_key,
'keep' => $keep_key,
);
if ($keep_key) {
$adjustment += array(
'columns' => $expect_key->getColumnNames(),
'unique' => $expect_key->getUnique(),
'indexType' => $expect_key->getIndexType(),
);
}
$adjustments[] = $adjustment;
}
}
}
}
return array($adjustments, $errors);
}
private function findErrors(PhabricatorConfigStorageSchema $schema) {
$result = array();
foreach ($schema->getLocalIssues() as $issue) {
$status = PhabricatorConfigStorageSchema::getIssueStatus($issue);
if ($status == PhabricatorConfigStorageSchema::STATUS_FAIL) {
$result[] = $issue;
}
}
return $result;
}
private function printErrors(array $errors, $default_return) {
if (!$errors) {
return $default_return;
}
$console = PhutilConsole::getConsole();
$table = id(new PhutilConsoleTable())
->addColumn('target', array('title' => pht('Target')))
->addColumn('error', array('title' => pht('Error')));
$any_surplus = false;
$all_surplus = true;
$any_access = false;
$all_access = true;
foreach ($errors as $error) {
$pieces = array_select_keys(
$error,
array('database', 'table', 'name'));
$pieces = array_filter($pieces);
$target = implode('.', $pieces);
$name = PhabricatorConfigStorageSchema::getIssueName($error['issue']);
$issue = $error['issue'];
if ($issue === PhabricatorConfigStorageSchema::ISSUE_SURPLUS) {
$any_surplus = true;
} else {
$all_surplus = false;
}
if ($issue === PhabricatorConfigStorageSchema::ISSUE_ACCESSDENIED) {
$any_access = true;
} else {
$all_access = false;
}
$table->addRow(
array(
'target' => $target,
'error' => $name,
));
}
$console->writeOut("\n");
$table->draw();
$console->writeOut("\n");
$message = array();
if ($all_surplus) {
$message[] = pht(
'You have surplus schemata (extra tables or columns which Phabricator '.
'does not expect). For information on resolving these '.
'issues, see the "Surplus Schemata" section in the "Managing Storage '.
'Adjustments" article in the documentation.');
} else if ($all_access) {
$message[] = pht(
'The user you are connecting to MySQL with does not have the correct '.
'permissions, and can not access some databases or tables that it '.
'needs to be able to access. GRANT the user additional permissions.');
} else {
$message[] = pht(
'The schemata have errors (detailed above) which the adjustment '.
'workflow can not fix.');
if ($any_access) {
$message[] = pht(
'Some of these errors are caused by access control problems. '.
'The user you are connecting with does not have permission to see '.
'all of the database or tables that Phabricator uses. You need to '.
'GRANT the user more permission, or use a different user.');
}
if ($any_surplus) {
$message[] = pht(
'Some of these errors are caused by surplus schemata (extra '.
'tables or columns which Phabricator does not expect). These are '.
'not serious. For information on resolving these issues, see the '.
'"Surplus Schemata" section in the "Managing Storage Adjustments" '.
'article in the documentation.');
}
$message[] = pht(
'If you are not developing Phabricator itself, report this issue to '.
'the upstream.');
$message[] = pht(
'If you are developing Phabricator, these errors usually indicate '.
'that your schema specifications do not agree with the schemata your '.
'code actually builds.');
}
$message = implode("\n\n", $message);
if ($all_surplus) {
$console->writeOut(
"**<bg:yellow> %s </bg>**\n\n%s\n",
pht('SURPLUS SCHEMATA'),
phutil_console_wrap($message));
} else if ($all_access) {
$console->writeOut(
"**<bg:yellow> %s </bg>**\n\n%s\n",
pht('ACCESS DENIED'),
phutil_console_wrap($message));
} else {
$console->writeOut(
"**<bg:red> %s </bg>**\n\n%s\n",
pht('SCHEMATA ERRORS'),
phutil_console_wrap($message));
}
return 2;
}
final protected function upgradeSchemata(
array $apis,
$apply_only = null,
$no_quickstart = false,
$init_only = false) {
$locks = array();
foreach ($apis as $api) {
$locks[] = $this->lock($api);
}
try {
$this->doUpgradeSchemata($apis, $apply_only, $no_quickstart, $init_only);
} catch (Exception $ex) {
foreach ($locks as $lock) {
$lock->unlock();
}
throw $ex;
}
foreach ($locks as $lock) {
$lock->unlock();
}
}
final private function doUpgradeSchemata(
array $apis,
$apply_only,
$no_quickstart,
$init_only) {
$patches = $this->patches;
$is_dryrun = $this->dryRun;
$api_map = array();
foreach ($apis as $api) {
$api_map[$api->getRef()->getRefKey()] = $api;
}
foreach ($api_map as $ref_key => $api) {
$applied = $api->getAppliedPatches();
$needs_init = ($applied === null);
if (!$needs_init) {
continue;
}
if ($is_dryrun) {
echo tsprintf(
"%s\n",
pht(
'DRYRUN: Storage on host "%s" does not exist yet, so it '.
'would be created.',
$ref_key));
continue;
}
if ($apply_only) {
throw new PhutilArgumentUsageException(
pht(
'Storage on host "%s" has not been initialized yet. You must '.
'initialize storage before selectively applying patches.',
$ref_key));
}
// If we're initializing storage for the first time on any host, track
// it so that we can give the user a nicer experience during the
// subsequent adjustment phase.
$this->didInitialize = true;
$legacy = $api->getLegacyPatches($patches);
if ($legacy || $no_quickstart || $init_only) {
// If we have legacy patches, we can't quickstart.
$api->createDatabase('meta_data');
$api->createTable(
'meta_data',
'patch_status',
array(
'patch VARCHAR(255) NOT NULL PRIMARY KEY COLLATE utf8_general_ci',
'applied INT UNSIGNED NOT NULL',
));
foreach ($legacy as $patch) {
$api->markPatchApplied($patch);
}
} else {
echo tsprintf(
"%s\n",
pht(
'Loading quickstart template onto "%s"...',
$ref_key));
$root = dirname(phutil_get_library_root('phabricator'));
$sql = $root.'/resources/sql/quickstart.sql';
$api->applyPatchSQL($sql);
}
}
if ($init_only) {
echo pht('Storage initialized.')."\n";
return 0;
}
$applied_map = array();
$state_map = array();
foreach ($api_map as $ref_key => $api) {
$applied = $api->getAppliedPatches();
// If we still have nothing applied, this is a dry run and we didn't
// actually initialize storage. Here, just do nothing.
if ($applied === null) {
if ($is_dryrun) {
continue;
} else {
throw new Exception(
pht(
'Database initialization on host "%s" applied no patches!',
$ref_key));
}
}
$applied = array_fuse($applied);
$state_map[$ref_key] = $applied;
if ($apply_only) {
if (isset($applied[$apply_only])) {
if (!$this->force && !$is_dryrun) {
echo phutil_console_wrap(
pht(
'Patch "%s" has already been applied on host "%s". Are you '.
'sure you want to apply it again? This may put your storage '.
'in a state that the upgrade scripts can not automatically '.
'manage.',
$apply_only,
$ref_key));
if (!phutil_console_confirm(pht('Apply patch again?'))) {
echo pht('Cancelled.')."\n";
return 1;
}
}
// Mark this patch as not yet applied on this host.
unset($applied[$apply_only]);
}
}
$applied_map[$ref_key] = $applied;
}
// If we're applying only a specific patch, select just that patch.
if ($apply_only) {
$patches = array_select_keys($patches, array($apply_only));
}
// Apply each patch to each database. We apply patches patch-by-patch,
// not database-by-database: for each patch we apply it to every database,
// then move to the next patch.
// We must do this because ".php" patches may depend on ".sql" patches
// being up to date on all masters, and that will work fine if we put each
// patch on every host before moving on. If we try to bring database hosts
// up to date one at a time we can end up in a big mess.
$duration_map = array();
// First, find any global patches which have been applied to ANY database.
// We are just going to mark these as applied without actually running
// them. Otherwise, adding new empty masters to an existing cluster will
// try to apply them against invalid states.
foreach ($patches as $key => $patch) {
if ($patch->getIsGlobalPatch()) {
foreach ($applied_map as $ref_key => $applied) {
if (isset($applied[$key])) {
$duration_map[$key] = 1;
}
}
}
}
while (true) {
$applied_something = false;
foreach ($patches as $key => $patch) {
// First, check if any databases need this patch. We can just skip it
// if it has already been applied everywhere.
$need_patch = array();
foreach ($applied_map as $ref_key => $applied) {
if (isset($applied[$key])) {
continue;
}
$need_patch[] = $ref_key;
}
if (!$need_patch) {
unset($patches[$key]);
continue;
}
// Check if we can apply this patch yet. Before we can apply a patch,
// all of the dependencies for the patch must have been applied on all
// databases. Requiring that all databases stay in sync prevents one
// database from racing ahead if it happens to get a patch that nothing
// else has yet.
$missing_patch = null;
foreach ($patch->getAfter() as $after) {
foreach ($applied_map as $ref_key => $applied) {
if (isset($applied[$after])) {
// This database already has the patch. We can apply it to
// other databases but don't need to apply it here.
continue;
}
$missing_patch = $after;
break 2;
}
}
if ($missing_patch) {
if ($apply_only) {
echo tsprintf(
"%s\n",
pht(
'Unable to apply patch "%s" because it depends on patch '.
'"%s", which has not been applied on some hosts: %s.',
$apply_only,
$missing_patch,
implode(', ', $need_patch)));
return 1;
} else {
// Some databases are missing the dependencies, so keep trying
// other patches instead. If everything goes right, we'll apply the
// dependencies and then come back and apply this patch later.
continue;
}
}
$is_global = $patch->getIsGlobalPatch();
$patch_apis = array_select_keys($api_map, $need_patch);
foreach ($patch_apis as $ref_key => $api) {
if ($is_global) {
// If this is a global patch which we previously applied, just
// read the duration from the map without actually applying
// the patch.
$duration = idx($duration_map, $key);
} else {
$duration = null;
}
if ($duration === null) {
if ($is_dryrun) {
echo tsprintf(
"%s\n",
pht(
'DRYRUN: Would apply patch "%s" to host "%s".',
$key,
$ref_key));
} else {
echo tsprintf(
"%s\n",
pht(
'Applying patch "%s" to host "%s"...',
$key,
$ref_key));
}
$t_begin = microtime(true);
if (!$is_dryrun) {
$api->applyPatch($patch);
}
$t_end = microtime(true);
$duration = ($t_end - $t_begin);
$duration_map[$key] = $duration;
}
// If we're explicitly reapplying this patch, we don't need to
// mark it as applied.
if (!isset($state_map[$ref_key][$key])) {
if (!$is_dryrun) {
$api->markPatchApplied($key, ($t_end - $t_begin));
}
$applied_map[$ref_key][$key] = true;
}
}
// We applied this everywhere, so we're done with the patch.
unset($patches[$key]);
$applied_something = true;
}
if (!$applied_something) {
if ($patches) {
throw new Exception(
pht(
'Some patches could not be applied: %s',
implode(', ', array_keys($patches))));
} else if (!$is_dryrun && !$apply_only) {
echo pht(
'Storage is up to date. Use "%s" for details.',
'storage status')."\n";
}
break;
}
}
}
final protected function getBareHostAndPort($host) {
// Split out port information, since the command-line client requires a
// separate flag for the port.
$uri = new PhutilURI('mysql://'.$host);
if ($uri->getPort()) {
$port = $uri->getPort();
$bare_hostname = $uri->getDomain();
} else {
$port = null;
$bare_hostname = $host;
}
return array($bare_hostname, $port);
}
/**
* Acquires a @{class:PhabricatorGlobalLock}.
*
* @return PhabricatorGlobalLock
*/
final protected function lock(PhabricatorStorageManagementAPI $api) {
// Although we're holding this lock on different databases so it could
// have the same name on each as far as the database is concerned, the
// locks would be the same within this process.
- $ref_key = $api->getRef()->getRefKey();
- $ref_hash = PhabricatorHash::digestForIndex($ref_key);
- $lock_name = 'adjust('.$ref_hash.')';
+ $parameters = array(
+ 'refKey' => $api->getRef()->getRefKey(),
+ );
+
+ // We disable logging for this lock because we may not have created the
+ // log table yet, or may need to adjust it.
- return PhabricatorGlobalLock::newLock($lock_name)
+ return PhabricatorGlobalLock::newLock('adjust', $parameters)
->useSpecificConnection($api->getConn(null))
+ ->setDisableLogging(true)
->lock();
}
final protected function analyzeTables(
PhabricatorStorageManagementAPI $api) {
// Analyzing tables can sometimes have a significant effect on query
// performance, particularly for the fulltext ngrams tables. See T12819
// for some specific examples.
$conn = $api->getConn(null);
$patches = $this->getPatches();
$databases = $api->getDatabaseList($patches, true);
$this->logInfo(
pht('ANALYZE'),
pht('Analyzing tables...'));
$targets = array();
foreach ($databases as $database) {
queryfx($conn, 'USE %C', $database);
$tables = queryfx_all($conn, 'SHOW TABLE STATUS');
foreach ($tables as $table) {
$table_name = $table['Name'];
$targets[] = array(
'database' => $database,
'table' => $table_name,
);
}
}
$bar = id(new PhutilConsoleProgressBar())
->setTotal(count($targets));
foreach ($targets as $target) {
queryfx(
$conn,
'ANALYZE TABLE %T.%T',
$target['database'],
$target['table']);
$bar->update(1);
}
$bar->done();
$this->logOkay(
pht('ANALYZED'),
pht(
'Analyzed %d table(s).',
count($targets)));
}
}
diff --git a/src/infrastructure/util/PhabricatorGlobalLock.php b/src/infrastructure/util/PhabricatorGlobalLock.php
index 26e11d189..827d7e0e3 100644
--- a/src/infrastructure/util/PhabricatorGlobalLock.php
+++ b/src/infrastructure/util/PhabricatorGlobalLock.php
@@ -1,164 +1,231 @@
<?php
/**
* Global, MySQL-backed lock. This is a high-reliability, low-performance
* global lock.
*
* The lock is maintained by using GET_LOCK() in MySQL, and automatically
* released when the connection terminates. Thus, this lock can safely be used
* to control access to shared resources without implementing any sort of
* timeout or override logic: the lock can't normally be stuck in a locked state
* with no process actually holding the lock.
*
* However, acquiring the lock is moderately expensive (several network
* roundtrips). This makes it unsuitable for tasks where lock performance is
* important.
*
* $lock = PhabricatorGlobalLock::newLock('example');
* $lock->lock();
* do_contentious_things();
* $lock->unlock();
*
* NOTE: This lock is not completely global; it is namespaced to the active
* storage namespace so that unit tests running in separate table namespaces
* are isolated from one another.
*
* @task construct Constructing Locks
* @task impl Implementation
*/
final class PhabricatorGlobalLock extends PhutilLock {
+ private $parameters;
private $conn;
private $isExternalConnection = false;
+ private $log;
+ private $disableLogging;
private static $pool = array();
/* -( Constructing Locks )------------------------------------------------- */
- public static function newLock($name) {
+ public static function newLock($name, $parameters = array()) {
$namespace = PhabricatorLiskDAO::getStorageNamespace();
$namespace = PhabricatorHash::digestToLength($namespace, 20);
- $full_name = 'ph:'.$namespace.':'.$name;
-
- $length_limit = 64;
- if (strlen($full_name) > $length_limit) {
- throw new Exception(
- pht(
- 'Lock name "%s" is too long (full lock name is "%s"). The '.
- 'full lock name must not be longer than %s bytes.',
- $name,
- $full_name,
- new PhutilNumber($length_limit)));
+ $parts = array();
+ ksort($parameters);
+ foreach ($parameters as $key => $parameter) {
+ if (!preg_match('/^[a-zA-Z0-9]+\z/', $key)) {
+ throw new Exception(
+ pht(
+ 'Lock parameter key "%s" must be alphanumeric.',
+ $key));
+ }
+
+ if (!is_scalar($parameter) && !is_null($parameter)) {
+ throw new Exception(
+ pht(
+ 'Lock parameter for key "%s" must be a scalar.',
+ $key));
+ }
+
+ $value = phutil_json_encode($parameter);
+ $parts[] = "{$key}={$value}";
}
+ $parts = implode(', ', $parts);
+ $local = "{$name}({$parts})";
+ $local = PhabricatorHash::digestToLength($local, 20);
+
+ $full_name = "ph:{$namespace}:{$local}";
$lock = self::getLock($full_name);
if (!$lock) {
$lock = new PhabricatorGlobalLock($full_name);
self::registerLock($lock);
+
+ $lock->parameters = $parameters;
}
return $lock;
}
/**
* Use a specific database connection for locking.
*
* By default, `PhabricatorGlobalLock` will lock on the "repository" database
* (somewhat arbitrarily). In most cases this is fine, but this method can
* be used to lock on a specific connection.
*
* @param AphrontDatabaseConnection
* @return this
*/
public function useSpecificConnection(AphrontDatabaseConnection $conn) {
$this->conn = $conn;
$this->isExternalConnection = true;
return $this;
}
+ public function setDisableLogging($disable) {
+ $this->disableLogging = $disable;
+ return $this;
+ }
+
/* -( Implementation )----------------------------------------------------- */
protected function doLock($wait) {
$conn = $this->conn;
if (!$conn) {
// Try to reuse a connection from the connection pool.
$conn = array_pop(self::$pool);
}
if (!$conn) {
// NOTE: Using the 'repository' database somewhat arbitrarily, mostly
// because the first client of locks is the repository daemons. We must
// always use the same database for all locks, but don't access any
// tables so we could use any valid database. We could build a
// database-free connection instead, but that's kind of messy and we
// might forget about it in the future if we vertically partition the
// application.
$dao = new PhabricatorRepository();
// NOTE: Using "force_new" to make sure each lock is on its own
// connection.
$conn = $dao->establishConnection('w', $force_new = true);
}
// NOTE: Since MySQL will disconnect us if we're idle for too long, we set
// the wait_timeout to an enormous value, to allow us to hold the
// connection open indefinitely (or, at least, for 24 days).
$max_allowed_timeout = 2147483;
queryfx($conn, 'SET wait_timeout = %d', $max_allowed_timeout);
$lock_name = $this->getName();
$result = queryfx_one(
$conn,
'SELECT GET_LOCK(%s, %f)',
$lock_name,
$wait);
$ok = head($result);
if (!$ok) {
throw new PhutilLockException($lock_name);
}
$conn->rememberLock($lock_name);
$this->conn = $conn;
+
+ if ($this->shouldLogLock()) {
+ global $argv;
+
+ $lock_context = array(
+ 'pid' => getmypid(),
+ 'host' => php_uname('n'),
+ 'argv' => $argv,
+ );
+
+ $log = id(new PhabricatorDaemonLockLog())
+ ->setLockName($lock_name)
+ ->setLockParameters($this->parameters)
+ ->setLockContext($lock_context)
+ ->save();
+
+ $this->log = $log;
+ }
}
protected function doUnlock() {
$lock_name = $this->getName();
$conn = $this->conn;
try {
$result = queryfx_one(
$conn,
'SELECT RELEASE_LOCK(%s)',
$lock_name);
$conn->forgetLock($lock_name);
} catch (Exception $ex) {
$result = array(null);
}
$ok = head($result);
if (!$ok) {
// TODO: We could throw here, but then this lock doesn't get marked
// unlocked and we throw again later when exiting. It also doesn't
// particularly matter for any current applications. For now, just
// swallow the error.
}
$this->conn = null;
$this->isExternalConnection = false;
if (!$this->isExternalConnection) {
$conn->close();
self::$pool[] = $conn;
}
+
+ if ($this->log) {
+ $log = $this->log;
+ $this->log = null;
+
+ $conn = $log->establishConnection('w');
+ queryfx(
+ $conn,
+ 'UPDATE %T SET lockReleased = UNIX_TIMESTAMP() WHERE id = %d',
+ $log->getTableName(),
+ $log->getID());
+ }
+ }
+
+ private function shouldLogLock() {
+ if ($this->disableLogging) {
+ return false;
+ }
+
+ $policy = id(new PhabricatorDaemonLockLogGarbageCollector())
+ ->getRetentionPolicy();
+ if (!$policy) {
+ return false;
+ }
+
+ return true;
}
}
diff --git a/src/view/control/AphrontTableView.php b/src/view/control/AphrontTableView.php
index 176d24671..cae9dabec 100644
--- a/src/view/control/AphrontTableView.php
+++ b/src/view/control/AphrontTableView.php
@@ -1,389 +1,389 @@
<?php
final class AphrontTableView extends AphrontView {
protected $data;
protected $headers;
- protected $shortHeaders;
+ protected $shortHeaders = array();
protected $rowClasses = array();
protected $columnClasses = array();
protected $cellClasses = array();
protected $zebraStripes = true;
protected $noDataString;
protected $className;
protected $notice;
protected $columnVisibility = array();
private $deviceVisibility = array();
private $columnWidths = array();
protected $sortURI;
protected $sortParam;
protected $sortSelected;
protected $sortReverse;
- protected $sortValues;
+ protected $sortValues = array();
private $deviceReadyTable;
public function __construct(array $data) {
$this->data = $data;
}
public function setHeaders(array $headers) {
$this->headers = $headers;
return $this;
}
public function setColumnClasses(array $column_classes) {
$this->columnClasses = $column_classes;
return $this;
}
public function setRowClasses(array $row_classes) {
$this->rowClasses = $row_classes;
return $this;
}
public function setCellClasses(array $cell_classes) {
$this->cellClasses = $cell_classes;
return $this;
}
public function setColumnWidths(array $widths) {
$this->columnWidths = $widths;
return $this;
}
public function setNoDataString($no_data_string) {
$this->noDataString = $no_data_string;
return $this;
}
public function setClassName($class_name) {
$this->className = $class_name;
return $this;
}
public function setNotice($notice) {
$this->notice = $notice;
return $this;
}
public function setZebraStripes($zebra_stripes) {
$this->zebraStripes = $zebra_stripes;
return $this;
}
public function setColumnVisibility(array $visibility) {
$this->columnVisibility = $visibility;
return $this;
}
public function setDeviceVisibility(array $device_visibility) {
$this->deviceVisibility = $device_visibility;
return $this;
}
public function setDeviceReadyTable($ready) {
$this->deviceReadyTable = $ready;
return $this;
}
public function setShortHeaders(array $short_headers) {
$this->shortHeaders = $short_headers;
return $this;
}
/**
* Parse a sorting parameter:
*
* list($sort, $reverse) = AphrontTableView::parseSortParam($sort_param);
*
* @param string Sort request parameter.
* @return pair Sort value, sort direction.
*/
public static function parseSort($sort) {
return array(ltrim($sort, '-'), preg_match('/^-/', $sort));
}
public function makeSortable(
PhutilURI $base_uri,
$param,
$selected,
$reverse,
array $sort_values) {
$this->sortURI = $base_uri;
$this->sortParam = $param;
$this->sortSelected = $selected;
$this->sortReverse = $reverse;
$this->sortValues = array_values($sort_values);
return $this;
}
public function render() {
require_celerity_resource('aphront-table-view-css');
$table = array();
$col_classes = array();
foreach ($this->columnClasses as $key => $class) {
if (strlen($class)) {
$col_classes[] = $class;
} else {
$col_classes[] = null;
}
}
$visibility = array_values($this->columnVisibility);
$device_visibility = array_values($this->deviceVisibility);
$column_widths = $this->columnWidths;
$headers = $this->headers;
$short_headers = $this->shortHeaders;
$sort_values = $this->sortValues;
if ($headers) {
while (count($headers) > count($visibility)) {
$visibility[] = true;
}
while (count($headers) > count($device_visibility)) {
$device_visibility[] = true;
}
while (count($headers) > count($short_headers)) {
$short_headers[] = null;
}
while (count($headers) > count($sort_values)) {
$sort_values[] = null;
}
$tr = array();
foreach ($headers as $col_num => $header) {
if (!$visibility[$col_num]) {
continue;
}
$classes = array();
if (!empty($col_classes[$col_num])) {
$classes[] = $col_classes[$col_num];
}
if (empty($device_visibility[$col_num])) {
$classes[] = 'aphront-table-view-nodevice';
}
if ($sort_values[$col_num] !== null) {
$classes[] = 'aphront-table-view-sortable';
$sort_value = $sort_values[$col_num];
$sort_glyph_class = 'aphront-table-down-sort';
if ($sort_value == $this->sortSelected) {
if ($this->sortReverse) {
$sort_glyph_class = 'aphront-table-up-sort';
} else if (!$this->sortReverse) {
$sort_value = '-'.$sort_value;
}
$classes[] = 'aphront-table-view-sortable-selected';
}
$sort_glyph = phutil_tag(
'span',
array(
'class' => $sort_glyph_class,
),
'');
$header = phutil_tag(
'a',
array(
'href' => $this->sortURI->alter($this->sortParam, $sort_value),
'class' => 'aphront-table-view-sort-link',
),
array(
$header,
' ',
$sort_glyph,
));
}
if ($classes) {
$class = implode(' ', $classes);
} else {
$class = null;
}
if ($short_headers[$col_num] !== null) {
$header_nodevice = phutil_tag(
'span',
array(
'class' => 'aphront-table-view-nodevice',
),
$header);
$header_device = phutil_tag(
'span',
array(
'class' => 'aphront-table-view-device',
),
$short_headers[$col_num]);
$header = hsprintf('%s %s', $header_nodevice, $header_device);
}
$style = null;
if (isset($column_widths[$col_num])) {
$style = 'width: '.$column_widths[$col_num].';';
}
$tr[] = phutil_tag(
'th',
array(
'class' => $class,
'style' => $style,
),
$header);
}
$table[] = phutil_tag('tr', array(), $tr);
}
foreach ($col_classes as $key => $value) {
- if (($sort_values[$key] !== null) &&
+ if (isset($sort_values[$key]) &&
($sort_values[$key] == $this->sortSelected)) {
$value = trim($value.' sorted-column');
}
if ($value !== null) {
$col_classes[$key] = $value;
}
}
$data = $this->data;
if ($data) {
$row_num = 0;
foreach ($data as $row) {
$row_size = count($row);
while (count($row) > count($col_classes)) {
$col_classes[] = null;
}
while (count($row) > count($visibility)) {
$visibility[] = true;
}
while (count($row) > count($device_visibility)) {
$device_visibility[] = true;
}
$tr = array();
// NOTE: Use of a separate column counter is to allow this to work
// correctly if the row data has string or non-sequential keys.
$col_num = 0;
foreach ($row as $value) {
if (!$visibility[$col_num]) {
++$col_num;
continue;
}
$class = $col_classes[$col_num];
if (empty($device_visibility[$col_num])) {
$class = trim($class.' aphront-table-view-nodevice');
}
if (!empty($this->cellClasses[$row_num][$col_num])) {
$class = trim($class.' '.$this->cellClasses[$row_num][$col_num]);
}
$tr[] = phutil_tag(
'td',
array(
'class' => $class,
),
$value);
++$col_num;
}
$class = idx($this->rowClasses, $row_num);
if ($this->zebraStripes && ($row_num % 2)) {
if ($class !== null) {
$class = 'alt alt-'.$class;
} else {
$class = 'alt';
}
}
$table[] = phutil_tag('tr', array('class' => $class), $tr);
++$row_num;
}
} else {
$colspan = max(count(array_filter($visibility)), 1);
$table[] = phutil_tag(
'tr',
array('class' => 'no-data'),
phutil_tag(
'td',
array('colspan' => $colspan),
coalesce($this->noDataString, pht('No data available.'))));
}
$classes = array();
$classes[] = 'aphront-table-view';
if ($this->className !== null) {
$classes[] = $this->className;
}
if ($this->deviceReadyTable) {
$classes[] = 'aphront-table-view-device-ready';
}
if ($this->columnWidths) {
$classes[] = 'aphront-table-view-fixed';
}
$notice = null;
if ($this->notice) {
$notice = phutil_tag(
'div',
array(
'class' => 'aphront-table-notice',
),
$this->notice);
}
$html = phutil_tag(
'table',
array(
'class' => implode(' ', $classes),
),
$table);
return phutil_tag_div(
'aphront-table-wrap',
array(
$notice,
$html,
));
}
public static function renderSingleDisplayLine($line) {
// TODO: Is there a cleaner way to do this? We use a relative div with
// overflow hidden to provide the bounds, and an absolute span with
// white-space: pre to prevent wrapping. We need to append a character
// (&nbsp; -- nonbreaking space) afterward to give the bounds div height
// (alternatively, we could hard-code the line height). This is gross but
// it's not clear that there's a better appraoch.
return phutil_tag(
'div',
array(
'class' => 'single-display-line-bounds',
),
array(
phutil_tag(
'span',
array(
'class' => 'single-display-line-content',
),
$line),
"\xC2\xA0",
));
}
}
diff --git a/src/view/form/control/AphrontFormRecaptchaControl.php b/src/view/form/control/AphrontFormRecaptchaControl.php
index 8c056b5bf..4152f7d8b 100644
--- a/src/view/form/control/AphrontFormRecaptchaControl.php
+++ b/src/view/form/control/AphrontFormRecaptchaControl.php
@@ -1,57 +1,65 @@
<?php
final class AphrontFormRecaptchaControl extends AphrontFormControl {
protected function getCustomControlClass() {
return 'aphront-form-control-recaptcha';
}
protected function shouldRender() {
return self::isRecaptchaEnabled();
}
public static function isRecaptchaEnabled() {
return PhabricatorEnv::getEnvConfig('recaptcha.enabled');
}
public static function hasCaptchaResponse(AphrontRequest $request) {
return $request->getBool('g-recaptcha-response');
}
public static function processCaptcha(AphrontRequest $request) {
if (!self::isRecaptchaEnabled()) {
return true;
}
$uri = 'https://www.google.com/recaptcha/api/siteverify';
$params = array(
'secret' => PhabricatorEnv::getEnvConfig('recaptcha.private-key'),
'response' => $request->getStr('g-recaptcha-response'),
'remoteip' => $request->getRemoteAddress(),
);
list($body) = id(new HTTPSFuture($uri, $params))
->setMethod('POST')
->resolvex();
$json = phutil_json_decode($body);
return (bool)idx($json, 'success');
}
protected function renderInput() {
$js = 'https://www.google.com/recaptcha/api.js';
$pubkey = PhabricatorEnv::getEnvConfig('recaptcha.public-key');
+ CelerityAPI::getStaticResourceResponse()
+ ->addContentSecurityPolicyURI('script-src', $js)
+ ->addContentSecurityPolicyURI('script-src', 'https://www.gstatic.com/')
+ ->addContentSecurityPolicyURI('frame-src', 'https://www.google.com/');
+
return array(
- phutil_tag('div', array(
- 'class' => 'g-recaptcha',
- 'data-sitekey' => $pubkey,
- )),
-
- phutil_tag('script', array(
- 'type' => 'text/javascript',
- 'src' => $js,
- )),
+ phutil_tag(
+ 'div',
+ array(
+ 'class' => 'g-recaptcha',
+ 'data-sitekey' => $pubkey,
+ )),
+ phutil_tag(
+ 'script',
+ array(
+ 'type' => 'text/javascript',
+ 'src' => $js,
+ )),
);
}
}
diff --git a/src/view/form/control/PhabricatorRemarkupControl.php b/src/view/form/control/PhabricatorRemarkupControl.php
index 5d0cc079e..dba0ef054 100644
--- a/src/view/form/control/PhabricatorRemarkupControl.php
+++ b/src/view/form/control/PhabricatorRemarkupControl.php
@@ -1,334 +1,359 @@
<?php
final class PhabricatorRemarkupControl extends AphrontFormTextAreaControl {
private $disableMacro = false;
private $disableFullScreen = false;
private $canPin;
private $sendOnEnter = false;
public function setDisableMacros($disable) {
$this->disableMacro = $disable;
return $this;
}
public function setDisableFullScreen($disable) {
$this->disableFullScreen = $disable;
return $this;
}
public function setCanPin($can_pin) {
$this->canPin = $can_pin;
return $this;
}
public function getCanPin() {
return $this->canPin;
}
public function setSendOnEnter($soe) {
$this->sendOnEnter = $soe;
return $this;
}
public function getSendOnEnter() {
return $this->sendOnEnter;
}
protected function renderInput() {
$id = $this->getID();
if (!$id) {
$id = celerity_generate_unique_node_id();
$this->setID($id);
}
$viewer = $this->getUser();
if (!$viewer) {
throw new PhutilInvalidStateException('setUser');
}
// We need to have this if previews render images, since Ajax can not
// currently ship JS or CSS.
require_celerity_resource('phui-lightbox-css');
if (!$this->getDisabled()) {
Javelin::initBehavior(
'aphront-drag-and-drop-textarea',
array(
'target' => $id,
'activatedClass' => 'aphront-textarea-drag-and-drop',
'uri' => '/file/dropupload/',
'chunkThreshold' => PhabricatorFileStorageEngine::getChunkThreshold(),
));
}
$root_id = celerity_generate_unique_node_id();
$user_datasource = new PhabricatorPeopleDatasource();
$emoji_datasource = new PhabricatorEmojiDatasource();
$proj_datasource = id(new PhabricatorProjectDatasource())
->setParameters(
array(
'autocomplete' => 1,
));
+ $phriction_datasource = new PhrictionDocumentDatasource();
+ $phurl_datasource = new PhabricatorPhurlURLDatasource();
+
Javelin::initBehavior(
'phabricator-remarkup-assist',
array(
'pht' => array(
'bold text' => pht('bold text'),
'italic text' => pht('italic text'),
'monospaced text' => pht('monospaced text'),
'List Item' => pht('List Item'),
'Quoted Text' => pht('Quoted Text'),
'data' => pht('data'),
'name' => pht('name'),
'URL' => pht('URL'),
'key-help' => pht('Pin or unpin the comment form.'),
),
'canPin' => $this->getCanPin(),
'disabled' => $this->getDisabled(),
'sendOnEnter' => $this->getSendOnEnter(),
'rootID' => $root_id,
'autocompleteMap' => (object)array(
64 => array( // "@"
'datasourceURI' => $user_datasource->getDatasourceURI(),
'headerIcon' => 'fa-user',
'headerText' => pht('Find User:'),
'hintText' => $user_datasource->getPlaceholderText(),
),
35 => array( // "#"
'datasourceURI' => $proj_datasource->getDatasourceURI(),
'headerIcon' => 'fa-briefcase',
'headerText' => pht('Find Project:'),
'hintText' => $proj_datasource->getPlaceholderText(),
),
58 => array( // ":"
'datasourceURI' => $emoji_datasource->getDatasourceURI(),
'headerIcon' => 'fa-smile-o',
'headerText' => pht('Find Emoji:'),
'hintText' => $emoji_datasource->getPlaceholderText(),
// Cancel on emoticons like ":3".
'ignore' => array(
'3',
')',
'(',
'-',
'/',
),
),
+ 91 => array( // "["
+ 'datasourceURI' => $phriction_datasource->getDatasourceURI(),
+ 'headerIcon' => 'fa-book',
+ 'headerText' => pht('Find Document:'),
+ 'hintText' => $phriction_datasource->getPlaceholderText(),
+ 'cancel' => array(
+ ':', // Cancel on "http:" and similar.
+ '|',
+ ']',
+ ),
+ 'prefix' => '^\\[',
+ ),
+ 40 => array( // "("
+ 'datasourceURI' => $phurl_datasource->getDatasourceURI(),
+ 'headerIcon' => 'fa-compress',
+ 'headerText' => pht('Find Phurl:'),
+ 'hintText' => $phurl_datasource->getPlaceholderText(),
+ 'cancel' => array(
+ ')',
+ ),
+ 'prefix' => '^\\(',
+ ),
),
));
Javelin::initBehavior('phabricator-tooltips', array());
$actions = array(
'fa-bold' => array(
'tip' => pht('Bold'),
'nodevice' => true,
),
'fa-italic' => array(
'tip' => pht('Italics'),
'nodevice' => true,
),
'fa-text-width' => array(
'tip' => pht('Monospaced'),
'nodevice' => true,
),
'fa-link' => array(
'tip' => pht('Link'),
'nodevice' => true,
),
array(
'spacer' => true,
'nodevice' => true,
),
'fa-list-ul' => array(
'tip' => pht('Bulleted List'),
'nodevice' => true,
),
'fa-list-ol' => array(
'tip' => pht('Numbered List'),
'nodevice' => true,
),
'fa-code' => array(
'tip' => pht('Code Block'),
'nodevice' => true,
),
'fa-quote-right' => array(
'tip' => pht('Quote'),
'nodevice' => true,
),
'fa-table' => array(
'tip' => pht('Table'),
'nodevice' => true,
),
'fa-cloud-upload' => array(
'tip' => pht('Upload File'),
),
);
$can_use_macros =
(!$this->disableMacro) &&
(function_exists('imagettftext'));
if ($can_use_macros) {
$can_use_macros = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorMacroApplication',
$viewer);
}
if ($can_use_macros) {
$actions[] = array(
'spacer' => true,
);
$actions['fa-meh-o'] = array(
'tip' => pht('Meme'),
);
}
$actions['fa-eye'] = array(
'tip' => pht('Preview'),
'align' => 'right',
);
$actions['fa-book'] = array(
'tip' => pht('Help'),
'align' => 'right',
'href' => PhabricatorEnv::getDoclink('Remarkup Reference'),
);
$mode_actions = array();
if (!$this->disableFullScreen) {
$mode_actions['fa-arrows-alt'] = array(
'tip' => pht('Fullscreen Mode'),
'align' => 'right',
);
}
if ($this->getCanPin()) {
$mode_actions['fa-thumb-tack'] = array(
'tip' => pht('Pin Form On Screen'),
'align' => 'right',
);
}
if ($mode_actions) {
$actions += $mode_actions;
}
$buttons = array();
foreach ($actions as $action => $spec) {
$classes = array();
if (idx($spec, 'align') == 'right') {
$classes[] = 'remarkup-assist-right';
}
if (idx($spec, 'nodevice')) {
$classes[] = 'remarkup-assist-nodevice';
}
if (idx($spec, 'spacer')) {
$classes[] = 'remarkup-assist-separator';
$buttons[] = phutil_tag(
'span',
array(
'class' => implode(' ', $classes),
),
'');
continue;
} else {
$classes[] = 'remarkup-assist-button';
}
if ($action == 'fa-cloud-upload') {
$classes[] = 'remarkup-assist-upload';
}
$href = idx($spec, 'href', '#');
if ($href == '#') {
$meta = array('action' => $action);
$mustcapture = true;
$target = null;
} else {
$meta = array();
$mustcapture = null;
$target = '_blank';
}
$content = null;
$tip = idx($spec, 'tip');
if ($tip) {
$meta['tip'] = $tip;
$content = javelin_tag(
'span',
array(
'aural' => true,
),
$tip);
}
$sigils = array();
$sigils[] = 'remarkup-assist';
if (!$this->getDisabled()) {
$sigils[] = 'has-tooltip';
}
$buttons[] = javelin_tag(
'a',
array(
'class' => implode(' ', $classes),
'href' => $href,
'sigil' => implode(' ', $sigils),
'meta' => $meta,
'mustcapture' => $mustcapture,
'target' => $target,
'tabindex' => -1,
),
phutil_tag(
'div',
array(
'class' =>
'remarkup-assist phui-icon-view phui-font-fa bluegrey '.$action,
),
$content));
}
$buttons = phutil_tag(
'div',
array(
'class' => 'remarkup-assist-bar',
),
$buttons);
$use_monospaced = $viewer->compareUserSetting(
PhabricatorMonospacedTextareasSetting::SETTINGKEY,
PhabricatorMonospacedTextareasSetting::VALUE_TEXT_MONOSPACED);
if ($use_monospaced) {
$monospaced_textareas_class = 'PhabricatorMonospaced';
} else {
$monospaced_textareas_class = null;
}
$this->setCustomClass(
'remarkup-assist-textarea '.$monospaced_textareas_class);
return javelin_tag(
'div',
array(
'sigil' => 'remarkup-assist-control',
'class' => $this->getDisabled() ? 'disabled-control' : null,
'id' => $root_id,
),
array(
$buttons,
parent::renderInput(),
));
}
}
diff --git a/src/view/layout/AphrontSideNavFilterView.php b/src/view/layout/AphrontSideNavFilterView.php
index 6564fab19..9abcea5a0 100644
--- a/src/view/layout/AphrontSideNavFilterView.php
+++ b/src/view/layout/AphrontSideNavFilterView.php
@@ -1,329 +1,356 @@
<?php
/**
* Provides a navigation sidebar. For example:
*
* $nav = new AphrontSideNavFilterView();
* $nav
* ->setBaseURI($some_uri)
* ->addLabel('Cats')
* ->addFilter('meow', 'Meow')
* ->addFilter('purr', 'Purr')
* ->addLabel('Dogs')
* ->addFilter('woof', 'Woof')
* ->addFilter('bark', 'Bark');
* $valid_filter = $nav->selectFilter($user_selection, $default = 'meow');
*
*/
final class AphrontSideNavFilterView extends AphrontView {
private $items = array();
private $baseURI;
private $selectedFilter = false;
private $flexible;
private $collapsed = false;
private $active;
private $menu;
private $crumbs;
private $classes = array();
private $menuID;
private $mainID;
private $isProfileMenu;
private $footer = array();
+ private $width;
public function setMenuID($menu_id) {
$this->menuID = $menu_id;
return $this;
}
public function getMenuID() {
return $this->menuID;
}
public function __construct() {
$this->menu = new PHUIListView();
}
public function addClass($class) {
$this->classes[] = $class;
return $this;
}
public function setCrumbs(PHUICrumbsView $crumbs) {
$this->crumbs = $crumbs;
return $this;
}
public function getCrumbs() {
return $this->crumbs;
}
public function setIsProfileMenu($is_profile) {
$this->isProfileMenu = $is_profile;
return $this;
}
public function getIsProfileMenu() {
return $this->isProfileMenu;
}
public function setActive($active) {
$this->active = $active;
return $this;
}
public function setFlexible($flexible) {
$this->flexible = $flexible;
return $this;
}
public function setCollapsed($collapsed) {
$this->collapsed = $collapsed;
return $this;
}
+ public function setWidth($width) {
+ $this->width = $width;
+ return $this;
+ }
+
public function getMenuView() {
return $this->menu;
}
public function addMenuItem(PHUIListItemView $item) {
$this->menu->addMenuItem($item);
return $this;
}
public function getMenu() {
return $this->menu;
}
public function addFilter($key, $name, $uri = null, $icon = null) {
return $this->addThing(
$key, $name, $uri, PHUIListItemView::TYPE_LINK, $icon);
}
public function addButton($key, $name, $uri = null) {
return $this->addThing(
$key, $name, $uri, PHUIListItemView::TYPE_BUTTON);
}
private function addThing($key, $name, $uri, $type, $icon = null) {
$item = id(new PHUIListItemView())
->setName($name)
->setType($type);
if (strlen($icon)) {
$item->setIcon($icon);
}
if (strlen($key)) {
$item->setKey($key);
}
if ($uri) {
$item->setHref($uri);
} else {
$href = clone $this->baseURI;
$href->setPath(rtrim($href->getPath().$key, '/').'/');
$href = (string)$href;
$item->setHref($href);
}
return $this->addMenuItem($item);
}
public function addCustomBlock($block) {
$this->menu->addMenuItem(
id(new PHUIListItemView())
->setType(PHUIListItemView::TYPE_CUSTOM)
->appendChild($block));
return $this;
}
public function addLabel($name) {
return $this->addMenuItem(
id(new PHUIListItemView())
->setType(PHUIListItemView::TYPE_LABEL)
->setName($name));
}
public function setBaseURI(PhutilURI $uri) {
$this->baseURI = $uri;
return $this;
}
public function getBaseURI() {
return $this->baseURI;
}
public function selectFilter($key, $default = null) {
$this->selectedFilter = $default;
if ($this->menu->getItem($key) && strlen($key)) {
$this->selectedFilter = $key;
}
return $this->selectedFilter;
}
public function getSelectedFilter() {
return $this->selectedFilter;
}
public function appendFooter($footer) {
$this->footer[] = $footer;
return $this;
}
public function getMainID() {
if (!$this->mainID) {
$this->mainID = celerity_generate_unique_node_id();
}
return $this->mainID;
}
public function render() {
if ($this->menu->getItems()) {
if (!$this->baseURI) {
throw new PhutilInvalidStateException('setBaseURI');
}
if ($this->selectedFilter === false) {
throw new PhutilInvalidStateException('selectFilter');
}
}
if ($this->selectedFilter !== null) {
$selected_item = $this->menu->getItem($this->selectedFilter);
if ($selected_item) {
$selected_item->addClass('phui-list-item-selected');
}
}
require_celerity_resource('phui-basic-nav-view-css');
return $this->renderFlexNav();
}
private function renderFlexNav() {
require_celerity_resource('phabricator-nav-view-css');
$nav_classes = array();
$nav_classes[] = 'phabricator-nav';
$nav_id = null;
$drag_id = null;
$content_id = celerity_generate_unique_node_id();
$local_id = null;
$background_id = null;
$local_menu = null;
$main_id = $this->getMainID();
+ $width = $this->width;
+ if ($width) {
+ $width = min($width, 600);
+ $width = max($width, 150);
+ } else {
+ $width = null;
+ }
+
+ if ($width && !$this->collapsed) {
+ $width_drag_style = 'left: '.$width.'px';
+ $width_panel_style = 'width: '.$width.'px';
+ $width_margin_style = 'margin-left: '.($width + 7).'px';
+ } else {
+ $width_drag_style = null;
+ $width_panel_style = null;
+ $width_margin_style = null;
+ }
+
if ($this->flexible) {
$drag_id = celerity_generate_unique_node_id();
$flex_bar = phutil_tag(
'div',
array(
'class' => 'phabricator-nav-drag',
'id' => $drag_id,
+ 'style' => $width_drag_style,
),
'');
} else {
$flex_bar = null;
}
$nav_menu = null;
if ($this->menu->getItems()) {
$local_id = celerity_generate_unique_node_id();
$background_id = celerity_generate_unique_node_id();
if (!$this->collapsed) {
$nav_classes[] = 'has-local-nav';
}
- $local_menu =
- phutil_tag(
- 'div',
- array(
- 'class' => 'phabricator-nav-local phabricator-side-menu',
- 'id' => $local_id,
- ),
- $this->menu->setID($this->getMenuID()));
+ $local_menu = phutil_tag(
+ 'div',
+ array(
+ 'class' => 'phabricator-nav-local phabricator-side-menu',
+ 'id' => $local_id,
+ 'style' => $width_panel_style,
+ ),
+ $this->menu->setID($this->getMenuID()));
}
$crumbs = null;
if ($this->crumbs) {
$crumbs = $this->crumbs->render();
$nav_classes[] = 'has-crumbs';
}
if ($this->flexible) {
if (!$this->collapsed) {
$nav_classes[] = 'has-drag-nav';
} else {
$nav_classes[] = 'has-closed-nav';
}
Javelin::initBehavior(
'phabricator-nav',
array(
- 'mainID' => $main_id,
- 'localID' => $local_id,
- 'dragID' => $drag_id,
- 'contentID' => $content_id,
- 'backgroundID' => $background_id,
- 'collapsed' => $this->collapsed,
+ 'mainID' => $main_id,
+ 'localID' => $local_id,
+ 'dragID' => $drag_id,
+ 'contentID' => $content_id,
+ 'backgroundID' => $background_id,
+ 'collapsed' => $this->collapsed,
+ 'width' => $width,
));
if ($this->active) {
Javelin::initBehavior(
'phabricator-active-nav',
array(
'localID' => $local_id,
));
}
}
$nav_classes = array_merge($nav_classes, $this->classes);
$menu = phutil_tag(
'div',
array(
'class' => implode(' ', $nav_classes),
'id' => $main_id,
),
array(
$local_menu,
$flex_bar,
phutil_tag(
'div',
array(
'class' => 'phabricator-nav-content plb',
'id' => $content_id,
+ 'style' => $width_margin_style,
),
array(
$crumbs,
$this->renderChildren(),
$this->footer,
)),
));
$classes = array();
$classes[] = 'phui-navigation-shell';
if ($this->getIsProfileMenu()) {
$classes[] = 'phui-profile-menu phui-basic-nav';
} else {
$classes[] = 'phui-basic-nav';
}
$shell = phutil_tag(
'div',
array(
'class' => implode(' ', $classes),
),
array(
$menu,
));
return $shell;
}
}
diff --git a/src/view/layout/PhabricatorActionView.php b/src/view/layout/PhabricatorActionView.php
index f6de8eca5..a1d8fe266 100644
--- a/src/view/layout/PhabricatorActionView.php
+++ b/src/view/layout/PhabricatorActionView.php
@@ -1,371 +1,374 @@
<?php
final class PhabricatorActionView extends AphrontView {
private $name;
private $icon;
private $href;
private $disabled;
private $label;
private $workflow;
private $renderAsForm;
private $download;
private $sigils = array();
private $metadata;
private $selected;
private $openInNewWindow;
private $submenu = array();
private $hidden;
private $depth;
private $id;
private $order;
private $color;
private $type;
const TYPE_DIVIDER = 'type-divider';
const TYPE_LABEL = 'label';
const RED = 'action-item-red';
public function setSelected($selected) {
$this->selected = $selected;
return $this;
}
public function getSelected() {
return $this->selected;
}
public function setMetadata($metadata) {
$this->metadata = $metadata;
return $this;
}
public function getMetadata() {
return $this->metadata;
}
public function setDownload($download) {
$this->download = $download;
return $this;
}
public function getDownload() {
return $this->download;
}
public function setHref($href) {
$this->href = $href;
return $this;
}
public function setColor($color) {
$this->color = $color;
return $this;
}
public function addSigil($sigil) {
$this->sigils[] = $sigil;
return $this;
}
public function getHref() {
return $this->href;
}
public function setIcon($icon) {
$this->icon = $icon;
return $this;
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
public function setLabel($label) {
$this->label = $label;
return $this;
}
public function setDisabled($disabled) {
$this->disabled = $disabled;
return $this;
}
public function getDisabled() {
return $this->disabled;
}
public function setWorkflow($workflow) {
$this->workflow = $workflow;
return $this;
}
public function setRenderAsForm($form) {
$this->renderAsForm = $form;
return $this;
}
public function setOpenInNewWindow($open_in_new_window) {
$this->openInNewWindow = $open_in_new_window;
return $this;
}
public function getOpenInNewWindow() {
return $this->openInNewWindow;
}
public function setID($id) {
$this->id = $id;
return $this;
}
public function getID() {
if (!$this->id) {
$this->id = celerity_generate_unique_node_id();
}
return $this->id;
}
public function setOrder($order) {
$this->order = $order;
return $this;
}
public function getOrder() {
return $this->order;
}
public function setType($type) {
$this->type = $type;
return $this;
}
public function getType() {
return $this->type;
}
public function setSubmenu(array $submenu) {
$this->submenu = $submenu;
if (!$this->getHref()) {
$this->setHref('#');
}
return $this;
}
public function getItems($depth = 0) {
$items = array();
$items[] = $this;
foreach ($this->submenu as $action) {
foreach ($action->getItems($depth + 1) as $item) {
$item
->setHidden(true)
->setDepth($depth + 1);
$items[] = $item;
}
}
return $items;
}
public function setHidden($hidden) {
$this->hidden = $hidden;
return $this;
}
public function getHidden() {
return $this->hidden;
}
public function setDepth($depth) {
$this->depth = $depth;
return $this;
}
public function getDepth() {
return $this->depth;
}
public function render() {
$caret_id = celerity_generate_unique_node_id();
$icon = null;
if ($this->icon) {
$color = '';
if ($this->disabled) {
$color = ' grey';
}
$icon = id(new PHUIIconView())
->addClass('phabricator-action-view-icon')
->setIcon($this->icon.$color);
}
$sigils = array();
if ($this->workflow) {
$sigils[] = 'workflow';
}
if ($this->download) {
$sigils[] = 'download';
}
if ($this->submenu) {
$sigils[] = 'keep-open';
}
if ($this->sigils) {
$sigils = array_merge($sigils, $this->sigils);
}
$sigils = $sigils ? implode(' ', $sigils) : null;
if ($this->href) {
if ($this->renderAsForm) {
if (!$this->hasViewer()) {
throw new Exception(
pht(
'Call %s when rendering an action as a form.',
'setViewer()'));
}
$item = javelin_tag(
'button',
array(
'class' => 'phabricator-action-view-item',
),
array($icon, $this->name));
$item = phabricator_form(
$this->getViewer(),
array(
'action' => $this->getHref(),
'method' => 'POST',
'sigil' => $sigils,
'meta' => $this->metadata,
),
$item);
} else {
if ($this->getOpenInNewWindow()) {
$target = '_blank';
+ $rel = 'noreferrer';
} else {
$target = null;
+ $rel = null;
}
if ($this->submenu) {
$caret = javelin_tag(
'span',
array(
'class' => 'caret-right',
'id' => $caret_id,
),
'');
} else {
$caret = null;
}
$item = javelin_tag(
'a',
array(
'href' => $this->getHref(),
'class' => 'phabricator-action-view-item',
'target' => $target,
+ 'rel' => $rel,
'sigil' => $sigils,
'meta' => $this->metadata,
),
array($icon, $this->name, $caret));
}
} else {
$item = javelin_tag(
'span',
array(
'class' => 'phabricator-action-view-item',
'sigil' => $sigils,
),
array($icon, $this->name, $this->renderChildren()));
}
$classes = array();
$classes[] = 'phabricator-action-view';
if ($this->disabled) {
$classes[] = 'phabricator-action-view-disabled';
}
if ($this->label) {
$classes[] = 'phabricator-action-view-label';
}
if ($this->selected) {
$classes[] = 'phabricator-action-view-selected';
}
if ($this->submenu) {
$classes[] = 'phabricator-action-view-submenu';
}
if ($this->getHref()) {
$classes[] = 'phabricator-action-view-href';
}
if ($this->icon) {
$classes[] = 'action-has-icon';
}
if ($this->color) {
$classes[] = $this->color;
}
if ($this->type) {
$classes[] = 'phabricator-action-view-'.$this->type;
}
$style = array();
if ($this->hidden) {
$style[] = 'display: none;';
}
if ($this->depth) {
$indent = ($this->depth * 16);
$style[] = "margin-left: {$indent}px;";
}
$sigil = null;
$meta = null;
if ($this->submenu) {
Javelin::initBehavior('phui-submenu');
$sigil = 'phui-submenu';
$item_ids = array();
foreach ($this->submenu as $subitem) {
$item_ids[] = $subitem->getID();
}
$meta = array(
'itemIDs' => $item_ids,
'caretID' => $caret_id,
);
}
return javelin_tag(
'li',
array(
'id' => $this->getID(),
'class' => implode(' ', $classes),
'style' => implode(' ', $style),
'sigil' => $sigil,
'meta' => $meta,
),
$item);
}
}
diff --git a/src/view/layout/PhabricatorFileLinkView.php b/src/view/layout/PhabricatorFileLinkView.php
index b0649381d..49320c4b8 100644
--- a/src/view/layout/PhabricatorFileLinkView.php
+++ b/src/view/layout/PhabricatorFileLinkView.php
@@ -1,183 +1,193 @@
<?php
final class PhabricatorFileLinkView extends AphrontTagView {
private $fileName;
private $fileDownloadURI;
private $fileViewURI;
private $fileViewable;
private $filePHID;
private $fileMonogram;
private $fileSize;
private $customClass;
public function setCustomClass($custom_class) {
$this->customClass = $custom_class;
return $this;
}
public function getCustomClass() {
return $this->customClass;
}
public function setFilePHID($file_phid) {
$this->filePHID = $file_phid;
return $this;
}
private function getFilePHID() {
return $this->filePHID;
}
public function setFileMonogram($monogram) {
$this->fileMonogram = $monogram;
return $this;
}
private function getFileMonogram() {
return $this->fileMonogram;
}
public function setFileViewable($file_viewable) {
$this->fileViewable = $file_viewable;
return $this;
}
private function getFileViewable() {
return $this->fileViewable;
}
public function setFileViewURI($file_view_uri) {
$this->fileViewURI = $file_view_uri;
return $this;
}
private function getFileViewURI() {
return $this->fileViewURI;
}
public function setFileDownloadURI($file_download_uri) {
$this->fileDownloadURI = $file_download_uri;
return $this;
}
private function getFileDownloadURI() {
return $this->fileDownloadURI;
}
public function setFileName($file_name) {
$this->fileName = $file_name;
return $this;
}
private function getFileName() {
return $this->fileName;
}
public function setFileSize($file_size) {
$this->fileSize = $file_size;
return $this;
}
private function getFileSize() {
return $this->fileSize;
}
private function getFileIcon() {
return FileTypeIcon::getFileIcon($this->getFileName());
}
public function getMeta() {
return array(
'phid' => $this->getFilePHID(),
'viewable' => $this->getFileViewable(),
'uri' => $this->getFileViewURI(),
'dUri' => $this->getFileDownloadURI(),
'name' => $this->getFileName(),
'monogram' => $this->getFileMonogram(),
'icon' => $this->getFileIcon(),
'size' => $this->getFileSize(),
);
}
protected function getTagName() {
- return 'div';
+ if ($this->getFileDownloadURI()) {
+ return 'div';
+ } else {
+ return 'a';
+ }
}
protected function getTagAttributes() {
- $mustcapture = true;
- $sigil = 'lightboxable';
- $meta = $this->getMeta();
-
$class = 'phabricator-remarkup-embed-layout-link';
if ($this->getCustomClass()) {
$class = $this->getCustomClass();
}
- return array(
- 'href' => $this->getFileViewURI(),
- 'class' => $class,
- 'sigil' => $sigil,
- 'meta' => $meta,
- 'mustcapture' => $mustcapture,
+ $attributes = array(
+ 'href' => $this->getFileViewURI(),
+ 'target' => '_blank',
+ 'rel' => 'noreferrer',
+ 'class' => $class,
);
+
+ if ($this->getFilePHID()) {
+ $mustcapture = true;
+ $sigil = 'lightboxable';
+ $meta = $this->getMeta();
+
+ $attributes += array(
+ 'sigil' => $sigil,
+ 'meta' => $meta,
+ 'mustcapture' => $mustcapture,
+ );
+ }
+
+ return $attributes;
}
protected function getTagContent() {
require_celerity_resource('phabricator-remarkup-css');
require_celerity_resource('phui-lightbox-css');
$icon = id(new PHUIIconView())
->setIcon($this->getFileIcon())
->addClass('phabricator-remarkup-embed-layout-icon');
- $dl_icon = id(new PHUIIconView())
- ->setIcon('fa-download');
+ $download_link = null;
- $download_form = phabricator_form(
- $this->getViewer(),
- array(
- 'action' => $this->getFileDownloadURI(),
- 'method' => 'POST',
- 'class' => 'embed-download-form',
- 'sigil' => 'embed-download-form download',
- ),
- phutil_tag(
- 'button',
+ $download_uri = $this->getFileDownloadURI();
+ if ($download_uri) {
+ $dl_icon = id(new PHUIIconView())
+ ->setIcon('fa-download');
+
+ $download_link = phutil_tag(
+ 'a',
array(
'class' => 'phabricator-remarkup-embed-layout-download',
- 'type' => 'submit',
+ 'href' => $download_uri,
),
- pht('Download')));
+ pht('Download'));
+ }
$info = phutil_tag(
'span',
array(
'class' => 'phabricator-remarkup-embed-layout-info',
),
$this->getFileSize());
$name = phutil_tag(
'span',
array(
'class' => 'phabricator-remarkup-embed-layout-name',
),
$this->getFileName());
$inner = phutil_tag(
'span',
array(
'class' => 'phabricator-remarkup-embed-layout-info-block',
),
array(
$name,
$info,
));
return array(
$icon,
$inner,
- $download_form,
+ $download_link,
);
}
}
diff --git a/src/view/layout/PhabricatorSourceCodeView.php b/src/view/layout/PhabricatorSourceCodeView.php
index eb34925b3..ce26fa3df 100644
--- a/src/view/layout/PhabricatorSourceCodeView.php
+++ b/src/view/layout/PhabricatorSourceCodeView.php
@@ -1,141 +1,147 @@
<?php
final class PhabricatorSourceCodeView extends AphrontView {
private $lines;
private $uri;
private $highlights = array();
private $canClickHighlight = true;
private $truncatedFirstBytes = false;
private $truncatedFirstLines = false;
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 render() {
require_celerity_resource('phabricator-source-code-view-css');
require_celerity_resource('syntax-highlighting-css');
Javelin::initBehavior('phabricator-oncopy', array());
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;
foreach ($lines as $line) {
// NOTE: See phabricator-oncopy behavior.
$content_line = hsprintf("\xE2\x80\x8B%s", $line);
$row_attributes = array();
if (isset($this->highlights[$line_number])) {
$row_attributes['class'] = 'phabricator-source-highlight';
}
if ($this->canClickHighlight) {
- $line_uri = $this->uri.'$'.$line_number;
- $line_href = (string)new PhutilURI($line_uri);
+ if ($base_uri) {
+ $line_href = $base_uri.'$'.$line_number;
+ } else {
+ $line_href = null;
+ }
- $tag_number = javelin_tag(
+ $tag_number = phutil_tag(
'a',
array(
'href' => $line_href,
),
$line_number);
} else {
- $tag_number = javelin_tag(
+ $tag_number = phutil_tag(
'span',
array(),
$line_number);
}
$rows[] = phutil_tag(
'tr',
$row_attributes,
array(
- javelin_tag(
+ phutil_tag(
'th',
array(
'class' => 'phabricator-source-line',
- 'sigil' => 'phabricator-source-line',
),
$tag_number),
phutil_tag(
'td',
array(
'class' => 'phabricator-source-code',
),
$content_line),
));
$line_number++;
}
$classes = array();
$classes[] = 'phabricator-source-code-view';
$classes[] = 'remarkup-code';
$classes[] = 'PhabricatorMonospaced';
return phutil_tag_div(
'phabricator-source-code-container',
javelin_tag(
'table',
array(
'class' => implode(' ', $classes),
'sigil' => 'phabricator-source',
+ 'meta' => array(
+ 'uri' => (string)$this->uri,
+ ),
),
phutil_implode_html('', $rows)));
}
}
diff --git a/src/view/page/AphrontPageView.php b/src/view/page/AphrontPageView.php
index 8f3704dca..d64c3b8e9 100644
--- a/src/view/page/AphrontPageView.php
+++ b/src/view/page/AphrontPageView.php
@@ -1,82 +1,89 @@
<?php
abstract class AphrontPageView extends AphrontView {
private $title;
public function setTitle($title) {
$this->title = $title;
return $this;
}
public function getTitle() {
$title = $this->title;
if (is_array($title)) {
$title = implode(" \xC2\xB7 ", $title);
}
return $title;
}
protected function getHead() {
return '';
}
protected function getBody() {
return phutil_implode_html('', $this->renderChildren());
}
protected function getTail() {
return '';
}
protected function willRenderPage() {
return;
}
protected function willSendResponse($response) {
return $response;
}
protected function getBodyClasses() {
return null;
}
public function render() {
$this->willRenderPage();
$title = $this->getTitle();
$head = $this->getHead();
$body = $this->getBody();
$tail = $this->getTail();
$body_classes = $this->getBodyClasses();
$body = phutil_tag(
'body',
array(
'class' => nonempty($body_classes, null),
),
array($body, $tail));
+ if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {
+ $data_fragment = phutil_safe_html(' data-developer-mode="1"');
+ } else {
+ $data_fragment = null;
+ }
+
$response = hsprintf(
'<!DOCTYPE html>'.
- '<html>'.
+ '<html%s>'.
'<head>'.
'<meta charset="UTF-8" />'.
'<title>%s</title>'.
'%s'.
'</head>'.
'%s'.
'</html>',
+ $data_fragment,
$title,
$head,
$body);
$response = $this->willSendResponse($response);
return $response;
}
}
diff --git a/src/view/page/PhabricatorBarePageView.php b/src/view/page/PhabricatorBarePageView.php
index c9a3c5c5c..de421d9e7 100644
--- a/src/view/page/PhabricatorBarePageView.php
+++ b/src/view/page/PhabricatorBarePageView.php
@@ -1,167 +1,181 @@
<?php
/**
* This is a bare HTML page view which has access to Phabricator page
* infrastructure like Celerity, but no content or builtin static resources.
* You basically get a valid HMTL5 document and an empty body tag.
*
* @concrete-extensible
*/
class PhabricatorBarePageView extends AphrontPageView {
private $request;
private $controller;
private $frameable;
private $deviceReady;
private $bodyContent;
public function setController(AphrontController $controller) {
$this->controller = $controller;
return $this;
}
public function getController() {
return $this->controller;
}
public function setRequest(AphrontRequest $request) {
$this->request = $request;
return $this;
}
public function getRequest() {
return $this->request;
}
public function setFrameable($frameable) {
$this->frameable = $frameable;
return $this;
}
public function getFrameable() {
return $this->frameable;
}
public function setDeviceReady($device_ready) {
$this->deviceReady = $device_ready;
return $this;
}
public function getDeviceReady() {
return $this->deviceReady;
}
protected function willRenderPage() {
// We render this now to resolve static resources so they can appear in the
// document head.
$this->bodyContent = phutil_implode_html('', $this->renderChildren());
}
protected function getHead() {
- $framebust = null;
- if (!$this->getFrameable()) {
- $framebust = '(top == self) || top.location.replace(self.location.href);';
- }
-
$viewport_tag = null;
if ($this->getDeviceReady()) {
$viewport_tag = phutil_tag(
'meta',
array(
'name' => 'viewport',
'content' => 'width=device-width, '.
'initial-scale=1, '.
'user-scalable=no',
));
}
- $mask_icon = phutil_tag(
- 'link',
- array(
- 'rel' => 'mask-icon',
- 'color' => '#3D4B67',
- 'href' => celerity_get_resource_uri(
- '/rsrc/favicons/mask-icon.svg'),
- ));
-
- $icon_tag_76 = phutil_tag(
- 'link',
- array(
- 'rel' => 'apple-touch-icon',
- 'href' => celerity_get_resource_uri(
- '/rsrc/favicons/apple-touch-icon-76x76.png'),
- ));
-
- $icon_tag_120 = phutil_tag(
- 'link',
+ $referrer_tag = phutil_tag(
+ 'meta',
array(
- 'rel' => 'apple-touch-icon',
- 'sizes' => '120x120',
- 'href' => celerity_get_resource_uri(
- '/rsrc/favicons/apple-touch-icon-120x120.png'),
+ 'name' => 'referrer',
+ 'content' => 'no-referrer',
));
- $icon_tag_152 = phutil_tag(
- 'link',
- array(
- 'rel' => 'apple-touch-icon',
- 'sizes' => '152x152',
- 'href' => celerity_get_resource_uri(
- '/rsrc/favicons/apple-touch-icon-152x152.png'),
- ));
- $favicon_tag = phutil_tag(
+ $mask_icon = phutil_tag(
'link',
array(
- 'id' => 'favicon',
- 'rel' => 'shortcut icon',
+ 'rel' => 'mask-icon',
+ 'color' => '#3D4B67',
'href' => celerity_get_resource_uri(
- '/rsrc/favicons/favicon.ico'),
+ '/rsrc/favicons/mask-icon.svg'),
));
- $referrer_tag = phutil_tag(
- 'meta',
- array(
- 'name' => 'referrer',
- 'content' => 'never',
- ));
+ $favicon_links = $this->newFavicons();
$response = CelerityAPI::getStaticResourceResponse();
if ($this->getRequest()) {
$viewer = $this->getRequest()->getViewer();
if ($viewer) {
$postprocessor_key = $viewer->getUserSetting(
PhabricatorAccessibilitySetting::SETTINGKEY);
if (strlen($postprocessor_key)) {
$response->setPostProcessorKey($postprocessor_key);
}
}
}
- $developer = PhabricatorEnv::getEnvConfig('phabricator.developer-mode');
return hsprintf(
- '%s%s%s%s%s%s%s%s%s',
+ '%s%s%s%s%s',
$viewport_tag,
$mask_icon,
- $icon_tag_76,
- $icon_tag_120,
- $icon_tag_152,
- $favicon_tag,
+ $favicon_links,
$referrer_tag,
- CelerityStaticResourceResponse::renderInlineScript(
- $framebust.jsprintf('window.__DEV__=%d;', ($developer ? 1 : 0))),
$response->renderResourcesOfType('css'));
}
protected function getBody() {
return $this->bodyContent;
}
protected function getTail() {
$response = CelerityAPI::getStaticResourceResponse();
return $response->renderResourcesOfType('js');
}
+ private function newFavicons() {
+ $favicon_refs = array(
+ array(
+ 'rel' => 'apple-touch-icon',
+ 'sizes' => '76x76',
+ 'width' => 76,
+ 'height' => 76,
+ ),
+ array(
+ 'rel' => 'apple-touch-icon',
+ 'sizes' => '120x120',
+ 'width' => 120,
+ 'height' => 120,
+ ),
+ array(
+ 'rel' => 'apple-touch-icon',
+ 'sizes' => '152x152',
+ 'width' => 152,
+ 'height' => 152,
+ ),
+ array(
+ 'rel' => 'icon',
+ 'id' => 'favicon',
+ 'width' => 64,
+ 'height' => 64,
+ ),
+ );
+
+ $fetch_refs = array();
+ foreach ($favicon_refs as $key => $spec) {
+ $ref = id(new PhabricatorFaviconRef())
+ ->setWidth($spec['width'])
+ ->setHeight($spec['height']);
+
+ $favicon_refs[$key]['ref'] = $ref;
+ $fetch_refs[] = $ref;
+ }
+
+ id(new PhabricatorFaviconRefQuery())
+ ->withRefs($fetch_refs)
+ ->execute();
+
+ $favicon_links = array();
+ foreach ($favicon_refs as $spec) {
+ $favicon_links[] = phutil_tag(
+ 'link',
+ array(
+ 'rel' => $spec['rel'],
+ 'sizes' => idx($spec, 'sizes'),
+ 'id' => idx($spec, 'id'),
+ 'href' => $spec['ref']->getURI(),
+ ));
+ }
+
+ return $favicon_links;
+ }
+
}
diff --git a/src/view/page/PhabricatorStandardPageView.php b/src/view/page/PhabricatorStandardPageView.php
index 78ff716a4..99143add5 100644
--- a/src/view/page/PhabricatorStandardPageView.php
+++ b/src/view/page/PhabricatorStandardPageView.php
@@ -1,914 +1,907 @@
<?php
/**
* This is a standard Phabricator page with menus, Javelin, DarkConsole, and
* basic styles.
*/
final class PhabricatorStandardPageView extends PhabricatorBarePageView
implements AphrontResponseProducerInterface {
private $baseURI;
private $applicationName;
private $glyph;
private $menuContent;
private $showChrome = true;
private $classes = array();
private $disableConsole;
private $pageObjects = array();
private $applicationMenu;
private $showFooter = true;
private $showDurableColumn = true;
private $quicksandConfig = array();
private $tabs;
private $crumbs;
private $navigation;
public function setShowFooter($show_footer) {
$this->showFooter = $show_footer;
return $this;
}
public function getShowFooter() {
return $this->showFooter;
}
public function setApplicationMenu($application_menu) {
// NOTE: For now, this can either be a PHUIListView or a
// PHUIApplicationMenuView.
$this->applicationMenu = $application_menu;
return $this;
}
public function getApplicationMenu() {
return $this->applicationMenu;
}
public function setApplicationName($application_name) {
$this->applicationName = $application_name;
return $this;
}
public function setDisableConsole($disable) {
$this->disableConsole = $disable;
return $this;
}
public function getApplicationName() {
return $this->applicationName;
}
public function setBaseURI($base_uri) {
$this->baseURI = $base_uri;
return $this;
}
public function getBaseURI() {
return $this->baseURI;
}
public function setShowChrome($show_chrome) {
$this->showChrome = $show_chrome;
return $this;
}
public function getShowChrome() {
return $this->showChrome;
}
public function addClass($class) {
$this->classes[] = $class;
return $this;
}
public function setPageObjectPHIDs(array $phids) {
$this->pageObjects = $phids;
return $this;
}
public function setShowDurableColumn($show) {
$this->showDurableColumn = $show;
return $this;
}
public function getShowDurableColumn() {
$request = $this->getRequest();
if (!$request) {
return false;
}
$viewer = $request->getUser();
if (!$viewer->isLoggedIn()) {
return false;
}
$conpherence_installed = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorConpherenceApplication',
$viewer);
if (!$conpherence_installed) {
return false;
}
if ($this->isQuicksandBlacklistURI()) {
return false;
}
return true;
}
private function isQuicksandBlacklistURI() {
$request = $this->getRequest();
if (!$request) {
return false;
}
$patterns = $this->getQuicksandURIPatternBlacklist();
$path = $request->getRequestURI()->getPath();
foreach ($patterns as $pattern) {
if (preg_match('(^'.$pattern.'$)', $path)) {
return true;
}
}
return false;
}
public function getDurableColumnVisible() {
$column_key = PhabricatorConpherenceColumnVisibleSetting::SETTINGKEY;
return (bool)$this->getUserPreference($column_key, false);
}
public function getDurableColumnMinimize() {
$column_key = PhabricatorConpherenceColumnMinimizeSetting::SETTINGKEY;
return (bool)$this->getUserPreference($column_key, false);
}
public function addQuicksandConfig(array $config) {
$this->quicksandConfig = $config + $this->quicksandConfig;
return $this;
}
public function getQuicksandConfig() {
return $this->quicksandConfig;
}
public function setCrumbs(PHUICrumbsView $crumbs) {
$this->crumbs = $crumbs;
return $this;
}
public function getCrumbs() {
return $this->crumbs;
}
public function setTabs(PHUIListView $tabs) {
$tabs->setType(PHUIListView::TABBAR_LIST);
$tabs->addClass('phabricator-standard-page-tabs');
$this->tabs = $tabs;
return $this;
}
public function getTabs() {
return $this->tabs;
}
public function setNavigation(AphrontSideNavFilterView $navigation) {
$this->navigation = $navigation;
return $this;
}
public function getNavigation() {
return $this->navigation;
}
public function getTitle() {
$glyph_key = PhabricatorTitleGlyphsSetting::SETTINGKEY;
$glyph_on = PhabricatorTitleGlyphsSetting::VALUE_TITLE_GLYPHS;
$glyph_setting = $this->getUserPreference($glyph_key, $glyph_on);
$use_glyph = ($glyph_setting == $glyph_on);
$title = parent::getTitle();
$prefix = null;
if ($use_glyph) {
$prefix = $this->getGlyph();
} else {
$application_name = $this->getApplicationName();
if (strlen($application_name)) {
$prefix = '['.$application_name.']';
}
}
if (strlen($prefix)) {
$title = $prefix.' '.$title;
}
return $title;
}
protected function willRenderPage() {
parent::willRenderPage();
if (!$this->getRequest()) {
throw new Exception(
pht(
'You must set the %s to render a %s.',
'Request',
__CLASS__));
}
$console = $this->getConsole();
require_celerity_resource('phabricator-core-css');
require_celerity_resource('phabricator-zindex-css');
require_celerity_resource('phui-button-css');
require_celerity_resource('phui-spacing-css');
require_celerity_resource('phui-form-css');
require_celerity_resource('phabricator-standard-page-view');
require_celerity_resource('conpherence-durable-column-view');
require_celerity_resource('font-lato');
Javelin::initBehavior('workflow', array());
$request = $this->getRequest();
$user = null;
if ($request) {
$user = $request->getUser();
}
if ($user) {
if ($user->isUserActivated()) {
$offset = $user->getTimeZoneOffset();
$ignore_key = PhabricatorTimezoneIgnoreOffsetSetting::SETTINGKEY;
$ignore = $user->getUserSetting($ignore_key);
Javelin::initBehavior(
'detect-timezone',
array(
'offset' => $offset,
'uri' => '/settings/timezone/',
'message' => pht(
'Your browser timezone setting differs from the timezone '.
'setting in your profile, click to reconcile.'),
'ignoreKey' => $ignore_key,
'ignore' => $ignore,
));
if ($user->getIsAdmin()) {
$server_https = $request->isHTTPS();
$server_protocol = $server_https ? 'HTTPS' : 'HTTP';
$client_protocol = $server_https ? 'HTTP' : 'HTTPS';
$doc_name = 'Configuring a Preamble Script';
$doc_href = PhabricatorEnv::getDoclink($doc_name);
Javelin::initBehavior(
'setup-check-https',
array(
'server_https' => $server_https,
'doc_name' => pht('See Documentation'),
'doc_href' => $doc_href,
'message' => pht(
'Phabricator thinks you are using %s, but your '.
'client is convinced that it is using %s. This is a serious '.
'misconfiguration with subtle, but significant, consequences.',
$server_protocol, $client_protocol),
));
}
}
- $icon = id(new PHUIIconView())
- ->setIcon('fa-download')
- ->addClass('phui-icon-circle-icon');
- $lightbox_id = celerity_generate_unique_node_id();
- $download_form = phabricator_form(
- $user,
- array(
- 'action' => '#',
- 'method' => 'POST',
- 'class' => 'lightbox-download-form',
- 'sigil' => 'download lightbox-download-submit',
- 'id' => 'lightbox-download-form',
- ),
- phutil_tag(
- 'a',
- array(
- 'class' => 'lightbox-download phui-icon-circle hover-green',
- 'href' => '#',
- ),
- array(
- $icon,
- )));
-
- Javelin::initBehavior(
- 'lightbox-attachments',
- array(
- 'lightbox_id' => $lightbox_id,
- 'downloadForm' => $download_form,
- ));
+ Javelin::initBehavior('lightbox-attachments');
}
Javelin::initBehavior('aphront-form-disable-on-submit');
Javelin::initBehavior('toggle-class', array());
Javelin::initBehavior('history-install');
Javelin::initBehavior('phabricator-gesture');
$current_token = null;
if ($user) {
$current_token = $user->getCSRFToken();
}
Javelin::initBehavior(
'refresh-csrf',
array(
'tokenName' => AphrontRequest::getCSRFTokenName(),
'header' => AphrontRequest::getCSRFHeaderName(),
'viaHeader' => AphrontRequest::getViaHeaderName(),
'current' => $current_token,
));
Javelin::initBehavior('device');
Javelin::initBehavior(
'high-security-warning',
$this->getHighSecurityWarningConfig());
if (PhabricatorEnv::isReadOnly()) {
Javelin::initBehavior(
'read-only-warning',
array(
'message' => PhabricatorEnv::getReadOnlyMessage(),
'uri' => PhabricatorEnv::getReadOnlyURI(),
));
}
if ($console) {
require_celerity_resource('aphront-dark-console-css');
$headers = array();
if (DarkConsoleXHProfPluginAPI::isProfilerStarted()) {
$headers[DarkConsoleXHProfPluginAPI::getProfilerHeader()] = 'page';
}
if (DarkConsoleServicesPlugin::isQueryAnalyzerRequested()) {
$headers[DarkConsoleServicesPlugin::getQueryAnalyzerHeader()] = true;
}
Javelin::initBehavior(
'dark-console',
$this->getConsoleConfig());
-
- // Change this to initBehavior when there is some behavior to initialize
- require_celerity_resource('javelin-behavior-error-log');
}
if ($user) {
$viewer = $user;
} else {
$viewer = new PhabricatorUser();
}
$menu = id(new PhabricatorMainMenuView())
->setUser($viewer);
if ($this->getController()) {
$menu->setController($this->getController());
}
$application_menu = $this->getApplicationMenu();
if ($application_menu) {
if ($application_menu instanceof PHUIApplicationMenuView) {
$crumbs = $this->getCrumbs();
if ($crumbs) {
$application_menu->setCrumbs($crumbs);
}
$application_menu = $application_menu->buildListView();
}
$menu->setApplicationMenu($application_menu);
}
$this->menuContent = $menu->render();
}
protected function getHead() {
$monospaced = null;
$request = $this->getRequest();
if ($request) {
$user = $request->getUser();
if ($user) {
$monospaced = $user->getUserSetting(
PhabricatorMonospacedFontSetting::SETTINGKEY);
}
}
$response = CelerityAPI::getStaticResourceResponse();
$font_css = null;
if (!empty($monospaced)) {
// We can't print this normally because escaping quotation marks will
// break the CSS. Instead, filter it strictly and then mark it as safe.
$monospaced = new PhutilSafeHTML(
PhabricatorMonospacedFontSetting::filterMonospacedCSSRule(
$monospaced));
$font_css = hsprintf(
'<style type="text/css">'.
'.PhabricatorMonospaced, '.
'.phabricator-remarkup .remarkup-code-block '.
'.remarkup-code { font: %s !important; } '.
'</style>',
$monospaced);
}
return hsprintf(
'%s%s%s',
parent::getHead(),
$font_css,
$response->renderSingleResource('javelin-magical-init', 'phabricator'));
}
public function setGlyph($glyph) {
$this->glyph = $glyph;
return $this;
}
public function getGlyph() {
return $this->glyph;
}
protected function willSendResponse($response) {
$request = $this->getRequest();
$response = parent::willSendResponse($response);
$console = $request->getApplicationConfiguration()->getConsole();
if ($console) {
$response = PhutilSafeHTML::applyFunction(
'str_replace',
hsprintf('<darkconsole />'),
$console->render($request),
$response);
}
return $response;
}
protected function getBody() {
$user = null;
$request = $this->getRequest();
if ($request) {
$user = $request->getUser();
}
$header_chrome = null;
if ($this->getShowChrome()) {
$header_chrome = $this->menuContent;
}
$classes = array();
$classes[] = 'main-page-frame';
$developer_warning = null;
if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode') &&
DarkConsoleErrorLogPluginAPI::getErrors()) {
$developer_warning = phutil_tag_div(
'aphront-developer-error-callout',
pht(
'This page raised PHP errors. Find them in DarkConsole '.
'or the error log.'));
}
$main_page = phutil_tag(
'div',
array(
'id' => 'phabricator-standard-page',
'class' => 'phabricator-standard-page',
),
array(
$developer_warning,
$header_chrome,
phutil_tag(
'div',
array(
'id' => 'phabricator-standard-page-body',
'class' => 'phabricator-standard-page-body',
),
$this->renderPageBodyContent()),
));
$durable_column = null;
if ($this->getShowDurableColumn()) {
$is_visible = $this->getDurableColumnVisible();
$is_minimize = $this->getDurableColumnMinimize();
$durable_column = id(new ConpherenceDurableColumnView())
->setSelectedConpherence(null)
->setUser($user)
->setQuicksandConfig($this->buildQuicksandConfig())
->setVisible($is_visible)
->setMinimize($is_minimize)
->setInitialLoad(true);
if ($is_minimize) {
$this->classes[] = 'minimize-column';
}
}
Javelin::initBehavior('quicksand-blacklist', array(
'patterns' => $this->getQuicksandURIPatternBlacklist(),
));
return phutil_tag(
'div',
array(
'class' => implode(' ', $classes),
'id' => 'main-page-frame',
),
array(
$main_page,
$durable_column,
));
}
private function renderPageBodyContent() {
$console = $this->getConsole();
$body = parent::getBody();
$footer = $this->renderFooter();
$nav = $this->getNavigation();
$tabs = $this->getTabs();
if ($nav) {
$crumbs = $this->getCrumbs();
if ($crumbs) {
$nav->setCrumbs($crumbs);
}
$nav->appendChild($body);
$nav->appendFooter($footer);
$content = phutil_implode_html('', array($nav->render()));
} else {
$content = array();
$crumbs = $this->getCrumbs();
if ($crumbs) {
if ($this->getTabs()) {
$crumbs->setBorder(true);
}
$content[] = $crumbs;
}
$tabs = $this->getTabs();
if ($tabs) {
$content[] = $tabs;
}
$content[] = $body;
$content[] = $footer;
$content = phutil_implode_html('', $content);
}
return array(
($console ? hsprintf('<darkconsole />') : null),
$content,
);
}
protected function getTail() {
$request = $this->getRequest();
$user = $request->getUser();
$tail = array(
parent::getTail(),
);
$response = CelerityAPI::getStaticResourceResponse();
if ($request->isHTTPS()) {
$with_protocol = 'https';
} else {
$with_protocol = 'http';
}
$servers = PhabricatorNotificationServerRef::getEnabledClientServers(
$with_protocol);
if ($servers) {
if ($user && $user->isLoggedIn()) {
// TODO: We could tell the browser about all the servers and let it
// do random reconnects to improve reliability.
shuffle($servers);
$server = head($servers);
$client_uri = $server->getWebsocketURI();
Javelin::initBehavior(
'aphlict-listen',
array(
- 'websocketURI' => (string)$client_uri,
+ 'websocketURI' => (string)$client_uri,
) + $this->buildAphlictListenConfigData());
+
+ CelerityAPI::getStaticResourceResponse()
+ ->addContentSecurityPolicyURI('connect-src', $client_uri);
}
}
- $tail[] = $response->renderHTMLFooter();
+ $tail[] = $response->renderHTMLFooter($this->getFrameable());
return $tail;
}
protected function getBodyClasses() {
$classes = array();
if (!$this->getShowChrome()) {
$classes[] = 'phabricator-chromeless-page';
}
$agent = AphrontRequest::getHTTPHeader('User-Agent');
// Try to guess the device resolution based on UA strings to avoid a flash
// of incorrectly-styled content.
$device_guess = 'device-desktop';
if (preg_match('@iPhone|iPod|(Android.*Chrome/[.0-9]* Mobile)@', $agent)) {
$device_guess = 'device-phone device';
} else if (preg_match('@iPad|(Android.*Chrome/)@', $agent)) {
$device_guess = 'device-tablet device';
}
$classes[] = $device_guess;
if (preg_match('@Windows@', $agent)) {
$classes[] = 'platform-windows';
} else if (preg_match('@Macintosh@', $agent)) {
$classes[] = 'platform-mac';
} else if (preg_match('@X11@', $agent)) {
$classes[] = 'platform-linux';
}
if ($this->getRequest()->getStr('__print__')) {
$classes[] = 'printable';
}
if ($this->getRequest()->getStr('__aural__')) {
$classes[] = 'audible';
}
$classes[] = 'phui-theme-'.PhabricatorEnv::getEnvConfig('ui.header-color');
foreach ($this->classes as $class) {
$classes[] = $class;
}
return implode(' ', $classes);
}
private function getConsole() {
if ($this->disableConsole) {
return null;
}
return $this->getRequest()->getApplicationConfiguration()->getConsole();
}
private function getConsoleConfig() {
$user = $this->getRequest()->getUser();
$headers = array();
if (DarkConsoleXHProfPluginAPI::isProfilerStarted()) {
$headers[DarkConsoleXHProfPluginAPI::getProfilerHeader()] = 'page';
}
if (DarkConsoleServicesPlugin::isQueryAnalyzerRequested()) {
$headers[DarkConsoleServicesPlugin::getQueryAnalyzerHeader()] = true;
}
if ($user) {
$setting_tab = PhabricatorDarkConsoleTabSetting::SETTINGKEY;
$setting_visible = PhabricatorDarkConsoleVisibleSetting::SETTINGKEY;
$tab = $user->getUserSetting($setting_tab);
$visible = $user->getUserSetting($setting_visible);
} else {
$tab = null;
$visible = true;
}
return array(
// NOTE: We use a generic label here to prevent input reflection
// and mitigate compression attacks like BREACH. See discussion in
// T3684.
'uri' => pht('Main Request'),
'selected' => $tab,
'visible' => $visible,
'headers' => $headers,
);
}
private function getHighSecurityWarningConfig() {
$user = $this->getRequest()->getUser();
$show = false;
if ($user->hasSession()) {
$hisec = ($user->getSession()->getHighSecurityUntil() - time());
if ($hisec > 0) {
$show = true;
}
}
return array(
'show' => $show,
'uri' => '/auth/session/downgrade/',
'message' => pht(
'Your session is in high security mode. When you '.
'finish using it, click here to leave.'),
);
}
private function renderFooter() {
if (!$this->getShowChrome()) {
return null;
}
if (!$this->getShowFooter()) {
return null;
}
$items = PhabricatorEnv::getEnvConfig('ui.footer-items');
if (!$items) {
return null;
}
$foot = array();
foreach ($items as $item) {
$name = idx($item, 'name', pht('Unnamed Footer Item'));
$href = idx($item, 'href');
if (!PhabricatorEnv::isValidURIForLink($href)) {
$href = null;
}
if ($href !== null) {
$tag = 'a';
} else {
$tag = 'span';
}
$foot[] = phutil_tag(
$tag,
array(
'href' => $href,
),
$name);
}
$foot = phutil_implode_html(" \xC2\xB7 ", $foot);
return phutil_tag(
'div',
array(
'class' => 'phabricator-standard-page-footer grouped',
),
$foot);
}
public function renderForQuicksand() {
parent::willRenderPage();
$response = $this->renderPageBodyContent();
$response = $this->willSendResponse($response);
$extra_config = $this->getQuicksandConfig();
return array(
'content' => hsprintf('%s', $response),
) + $this->buildQuicksandConfig()
+ $extra_config;
}
private function buildQuicksandConfig() {
$viewer = $this->getRequest()->getUser();
$controller = $this->getController();
$dropdown_query = id(new AphlictDropdownDataQuery())
->setViewer($viewer);
$dropdown_query->execute();
$hisec_warning_config = $this->getHighSecurityWarningConfig();
$console_config = null;
$console = $this->getConsole();
if ($console) {
$console_config = $this->getConsoleConfig();
}
$upload_enabled = false;
if ($controller) {
$upload_enabled = $controller->isGlobalDragAndDropUploadEnabled();
}
$application_class = null;
$application_search_icon = null;
$application_help = null;
$controller = $this->getController();
if ($controller) {
$application = $controller->getCurrentApplication();
if ($application) {
$application_class = get_class($application);
if ($application->getApplicationSearchDocumentTypes()) {
$application_search_icon = $application->getIcon();
}
$help_items = $application->getHelpMenuItems($viewer);
if ($help_items) {
$help_list = id(new PhabricatorActionListView())
->setViewer($viewer);
foreach ($help_items as $help_item) {
$help_list->addAction($help_item);
}
$application_help = $help_list->getDropdownMenuMetadata();
}
}
}
return array(
'title' => $this->getTitle(),
'bodyClasses' => $this->getBodyClasses(),
'aphlictDropdownData' => array(
$dropdown_query->getNotificationData(),
$dropdown_query->getConpherenceData(),
),
'globalDragAndDrop' => $upload_enabled,
'hisecWarningConfig' => $hisec_warning_config,
'consoleConfig' => $console_config,
'applicationClass' => $application_class,
'applicationSearchIcon' => $application_search_icon,
'helpItems' => $application_help,
) + $this->buildAphlictListenConfigData();
}
private function buildAphlictListenConfigData() {
$user = $this->getRequest()->getUser();
$subscriptions = $this->pageObjects;
$subscriptions[] = $user->getPHID();
return array(
'pageObjects' => array_fill_keys($this->pageObjects, true),
'subscriptions' => $subscriptions,
);
}
private function getQuicksandURIPatternBlacklist() {
$applications = PhabricatorApplication::getAllApplications();
$blacklist = array();
foreach ($applications as $application) {
$blacklist[] = $application->getQuicksandURIPatternBlacklist();
}
+ // See T4340. Currently, Phortune and Auth both require pulling in external
+ // Javascript (for Stripe card management and Recaptcha, respectively).
+ // This can put us in a position where the user loads a page with a
+ // restrictive Content-Security-Policy, then uses Quicksand to navigate to
+ // a page which needs to load external scripts. For now, just blacklist
+ // these entire applications since we aren't giving up anything
+ // significant by doing so.
+
+ $blacklist[] = array(
+ '/phortune/.*',
+ '/auth/.*',
+ );
+
return array_mergev($blacklist);
}
private function getUserPreference($key, $default = null) {
$request = $this->getRequest();
if (!$request) {
return $default;
}
$user = $request->getUser();
if (!$user) {
return $default;
}
return $user->getUserSetting($key);
}
public function produceAphrontResponse() {
$controller = $this->getController();
if (!$this->getApplicationMenu()) {
$application_menu = $controller->buildApplicationMenu();
if ($application_menu) {
$this->setApplicationMenu($application_menu);
}
}
$viewer = $this->getUser();
if ($viewer && $viewer->getPHID()) {
$object_phids = $this->pageObjects;
foreach ($object_phids as $object_phid) {
PhabricatorFeedStoryNotification::updateObjectNotificationViews(
$viewer,
$object_phid);
}
}
if ($this->getRequest()->isQuicksand()) {
$content = $this->renderForQuicksand();
$response = id(new AphrontAjaxResponse())
->setContent($content);
} else {
$content = $this->render();
+
$response = id(new AphrontWebpageResponse())
->setContent($content)
->setFrameable($this->getFrameable());
+
+ $static = CelerityAPI::getStaticResourceResponse();
+ foreach ($static->getContentSecurityPolicyURIMap() as $kind => $uris) {
+ foreach ($uris as $uri) {
+ $response->addContentSecurityPolicyURI($kind, $uri);
+ }
+ }
}
return $response;
}
}
diff --git a/src/view/page/menu/PhabricatorMainMenuView.php b/src/view/page/menu/PhabricatorMainMenuView.php
index ad9510f34..583952b0a 100644
--- a/src/view/page/menu/PhabricatorMainMenuView.php
+++ b/src/view/page/menu/PhabricatorMainMenuView.php
@@ -1,730 +1,735 @@
<?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 function getFaviconURI($type = null) {
- switch ($type) {
- case 'message':
- return celerity_get_resource_uri('/rsrc/favicons/favicon-message.ico');
- case 'mention':
- return celerity_get_resource_uri('/rsrc/favicons/favicon-mention.ico');
- default:
- return celerity_get_resource_uri('/rsrc/favicons/favicon.ico');
- }
+ 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);
}
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',
- 'favicon' => $this->getFaviconURI('default'),
- 'message_favicon' => $this->getFaviconURI('message'),
- 'mention_favicon' => $this->getFaviconURI('mention'),
- ));
+ ) + 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',
- 'favicon' => $this->getFaviconURI('default'),
- 'message_favicon' => $this->getFaviconURI('message'),
- 'mention_favicon' => $this->getFaviconURI('mention'),
- ));
+ ) + 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',
- 'favicon' => $this->getFaviconURI('default'),
- 'message_favicon' => $this->getFaviconURI('message'),
- 'mention_favicon' => $this->getFaviconURI('mention'),
- ));
+ ) + 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/PHUIHeaderView.php b/src/view/phui/PHUIHeaderView.php
index 53ec09626..4ac4b2de5 100644
--- a/src/view/phui/PHUIHeaderView.php
+++ b/src/view/phui/PHUIHeaderView.php
@@ -1,549 +1,554 @@
<?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) {
- $icon = id(new PHUIIconView())
- ->setIcon($this->headerIcon)
- ->addClass('phui-header-icon');
+ 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';
}
}
}
}
$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/PHUIInfoView.php b/src/view/phui/PHUIInfoView.php
index 161e49108..69d054929 100644
--- a/src/view/phui/PHUIInfoView.php
+++ b/src/view/phui/PHUIInfoView.php
@@ -1,200 +1,200 @@
<?php
final class PHUIInfoView extends AphrontTagView {
const SEVERITY_ERROR = 'error';
const SEVERITY_WARNING = 'warning';
const SEVERITY_NOTICE = 'notice';
const SEVERITY_NODATA = 'nodata';
const SEVERITY_SUCCESS = 'success';
const SEVERITY_PLAIN = 'plain';
private $title;
- private $errors;
+ private $errors = array();
private $severity = null;
private $id;
private $buttons = array();
private $isHidden;
private $flush;
private $icon;
public function setTitle($title) {
$this->title = $title;
return $this;
}
public function setSeverity($severity) {
$this->severity = $severity;
return $this;
}
private function getSeverity() {
$severity = $this->severity ? $this->severity : self::SEVERITY_ERROR;
return $severity;
}
public function setErrors(array $errors) {
$this->errors = $errors;
return $this;
}
public function setID($id) {
$this->id = $id;
return $this;
}
public function setIsHidden($bool) {
$this->isHidden = $bool;
return $this;
}
public function setFlush($flush) {
$this->flush = $flush;
return $this;
}
public function setIcon($icon) {
if ($icon instanceof PHUIIconView) {
$this->icon = $icon;
} else {
$icon = id(new PHUIIconView())
->setIcon($icon);
$this->icon = $icon;
}
return $this;
}
private function getIcon() {
if ($this->icon) {
return $this->icon;
}
switch ($this->getSeverity()) {
case self::SEVERITY_ERROR:
$icon = 'fa-exclamation-circle';
break;
case self::SEVERITY_WARNING:
$icon = 'fa-exclamation-triangle';
break;
case self::SEVERITY_NOTICE:
$icon = 'fa-info-circle';
break;
case self::SEVERITY_PLAIN:
case self::SEVERITY_NODATA:
return null;
break;
case self::SEVERITY_SUCCESS:
$icon = 'fa-check-circle';
break;
}
$icon = id(new PHUIIconView())
->setIcon($icon)
->addClass('phui-info-icon');
return $icon;
}
public function addButton(PHUIButtonView $button) {
$this->buttons[] = $button;
return $this;
}
protected function getTagAttributes() {
$classes = array();
$classes[] = 'phui-info-view';
$classes[] = 'phui-info-severity-'.$this->getSeverity();
$classes[] = 'grouped';
if ($this->flush) {
$classes[] = 'phui-info-view-flush';
}
if ($this->getIcon()) {
$classes[] = 'phui-info-has-icon';
}
return array(
'id' => $this->id,
'class' => implode(' ', $classes),
'style' => $this->isHidden ? 'display: none;' : null,
);
}
protected function getTagContent() {
require_celerity_resource('phui-info-view-css');
$errors = $this->errors;
if (count($errors) > 1) {
$list = array();
foreach ($errors as $error) {
$list[] = phutil_tag(
'li',
array(),
$error);
}
$list = phutil_tag(
'ul',
array(
'class' => 'phui-info-view-list',
),
$list);
} else if (count($errors) == 1) {
$list = head($this->errors);
} else {
$list = null;
}
$title = $this->title;
if (strlen($title)) {
$title = phutil_tag(
'h1',
array(
'class' => 'phui-info-view-head',
),
$title);
} else {
$title = null;
}
$children = $this->renderChildren();
if ($list) {
$children[] = $list;
}
$body = null;
if (!empty($children)) {
$body = phutil_tag(
'div',
array(
'class' => 'phui-info-view-body',
),
$children);
}
$buttons = null;
if (!empty($this->buttons)) {
$buttons = phutil_tag(
'div',
array(
'class' => 'phui-info-view-actions',
),
$this->buttons);
}
$icon = null;
if ($this->getIcon()) {
$icon = phutil_tag(
'div',
array(
'class' => 'phui-info-view-icon',
),
$this->getIcon());
}
return array(
$icon,
$buttons,
$title,
$body,
);
}
}
diff --git a/src/view/phui/PHUIListItemView.php b/src/view/phui/PHUIListItemView.php
index e8a194073..8e5302482 100644
--- a/src/view/phui/PHUIListItemView.php
+++ b/src/view/phui/PHUIListItemView.php
@@ -1,379 +1,390 @@
<?php
final class PHUIListItemView extends AphrontTagView {
const TYPE_LINK = 'type-link';
const TYPE_SPACER = 'type-spacer';
const TYPE_LABEL = 'type-label';
const TYPE_BUTTON = 'type-button';
const TYPE_CUSTOM = 'type-custom';
const TYPE_DIVIDER = 'type-divider';
const TYPE_ICON = 'type-icon';
const STATUS_WARN = 'phui-list-item-warn';
const STATUS_FAIL = 'phui-list-item-fail';
private $name;
private $href;
private $type = self::TYPE_LINK;
private $isExternal;
private $key;
private $icon;
private $selected;
private $disabled;
private $renderNameAsTooltip;
private $statusColor;
private $order;
private $aural;
private $profileImage;
private $indented;
private $hideInApplicationMenu;
private $icons = array();
private $openInNewWindow = false;
private $tooltip;
private $actionIcon;
private $actionIconHref;
private $count;
+ private $rel;
public function setOpenInNewWindow($open_in_new_window) {
$this->openInNewWindow = $open_in_new_window;
return $this;
}
public function getOpenInNewWindow() {
return $this->openInNewWindow;
}
- public function setHideInApplicationMenu($hide) {
+ public function setRel($rel) {
+ $this->rel = $rel;
+ return $this;
+ }
+
+ public function getRel() {
+ return $this->rel;
+ }
+
+ public function setHideInApplicationMenu($hide) {
$this->hideInApplicationMenu = $hide;
return $this;
}
public function getHideInApplicationMenu() {
return $this->hideInApplicationMenu;
}
public function setDropdownMenu(PhabricatorActionListView $actions) {
Javelin::initBehavior('phui-dropdown-menu');
$this->addSigil('phui-dropdown-menu');
$this->setMetadata($actions->getDropdownMenuMetadata());
return $this;
}
public function setAural($aural) {
$this->aural = $aural;
return $this;
}
public function getAural() {
return $this->aural;
}
public function setOrder($order) {
$this->order = $order;
return $this;
}
public function getOrder() {
return $this->order;
}
public function setRenderNameAsTooltip($render_name_as_tooltip) {
$this->renderNameAsTooltip = $render_name_as_tooltip;
return $this;
}
public function getRenderNameAsTooltip() {
return $this->renderNameAsTooltip;
}
public function setSelected($selected) {
$this->selected = $selected;
return $this;
}
public function getSelected() {
return $this->selected;
}
public function setIcon($icon) {
$this->icon = $icon;
return $this;
}
public function setProfileImage($image) {
$this->profileImage = $image;
return $this;
}
public function getIcon() {
return $this->icon;
}
public function setCount($count) {
$this->count = $count;
return $this;
}
public function setIndented($indented) {
$this->indented = $indented;
return $this;
}
public function getIndented() {
return $this->indented;
}
public function setKey($key) {
$this->key = (string)$key;
return $this;
}
public function getKey() {
return $this->key;
}
public function setType($type) {
$this->type = $type;
return $this;
}
public function getType() {
return $this->type;
}
public function setHref($href) {
$this->href = $href;
return $this;
}
public function getHref() {
return $this->href;
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
public function setActionIcon($icon, $href) {
$this->actionIcon = $icon;
$this->actionIconHref = $href;
return $this;
}
public function setIsExternal($is_external) {
$this->isExternal = $is_external;
return $this;
}
public function getIsExternal() {
return $this->isExternal;
}
public function setStatusColor($color) {
$this->statusColor = $color;
return $this;
}
public function addIcon($icon) {
$this->icons[] = $icon;
return $this;
}
public function getIcons() {
return $this->icons;
}
public function setTooltip($tooltip) {
$this->tooltip = $tooltip;
return $this;
}
protected function getTagName() {
return 'li';
}
protected function getTagAttributes() {
$classes = array();
$classes[] = 'phui-list-item-view';
$classes[] = 'phui-list-item-'.$this->type;
if ($this->icon || $this->profileImage) {
$classes[] = 'phui-list-item-has-icon';
}
if ($this->selected) {
$classes[] = 'phui-list-item-selected';
}
if ($this->disabled) {
$classes[] = 'phui-list-item-disabled';
}
if ($this->statusColor) {
$classes[] = $this->statusColor;
}
if ($this->actionIcon) {
$classes[] = 'phui-list-item-has-action-icon';
}
return array(
'class' => implode(' ', $classes),
);
}
public function setDisabled($disabled) {
$this->disabled = $disabled;
return $this;
}
public function getDisabled() {
return $this->disabled;
}
protected function getTagContent() {
$name = null;
$icon = null;
$meta = null;
$sigil = null;
if ($this->name) {
if ($this->getRenderNameAsTooltip()) {
Javelin::initBehavior('phabricator-tooltips');
$sigil = 'has-tooltip';
$meta = array(
'tip' => $this->name,
'align' => 'E',
);
} else {
if ($this->tooltip) {
Javelin::initBehavior('phabricator-tooltips');
$sigil = 'has-tooltip';
$meta = array(
'tip' => $this->tooltip,
'align' => 'E',
'size' => 300,
);
}
$external = null;
if ($this->isExternal) {
$external = " \xE2\x86\x97";
}
// If this element has an aural representation, make any name visual
// only. This is primarily dealing with the links in the main menu like
// "Profile" and "Logout". If we don't hide the name, the mobile
// version of these elements will have two redundant names.
$classes = array();
$classes[] = 'phui-list-item-name';
if ($this->aural !== null) {
$classes[] = 'visual-only';
}
$name = phutil_tag(
'span',
array(
'class' => implode(' ', $classes),
),
array(
$this->name,
$external,
));
}
}
$aural = null;
if ($this->aural !== null) {
$aural = javelin_tag(
'span',
array(
'aural' => true,
),
$this->aural);
}
if ($this->icon) {
$icon_name = $this->icon;
if ($this->getDisabled()) {
$icon_name .= ' grey';
}
$icon = id(new PHUIIconView())
->addClass('phui-list-item-icon')
->setIcon($icon_name);
}
if ($this->profileImage) {
$icon = id(new PHUIIconView())
->setHeadSize(PHUIIconView::HEAD_SMALL)
->addClass('phui-list-item-icon')
->setImage($this->profileImage);
}
$classes = array();
if ($this->href) {
$classes[] = 'phui-list-item-href';
}
if ($this->indented) {
$classes[] = 'phui-list-item-indented';
}
$action_link = null;
if ($this->actionIcon) {
$action_icon = id(new PHUIIconView())
->setIcon($this->actionIcon)
->addClass('phui-list-item-action-icon');
$action_link = phutil_tag(
'a',
array(
'href' => $this->actionIconHref,
'class' => 'phui-list-item-action-href',
),
$action_icon);
}
$count = null;
if ($this->count) {
$count = phutil_tag(
'span',
array(
'class' => 'phui-list-item-count',
),
$this->count);
}
$icons = $this->getIcons();
$list_item = javelin_tag(
$this->href ? 'a' : 'div',
array(
'href' => $this->href,
'class' => implode(' ', $classes),
'meta' => $meta,
'sigil' => $sigil,
'target' => $this->getOpenInNewWindow() ? '_blank' : null,
+ 'rel' => $this->rel,
),
array(
$aural,
$icon,
$icons,
$this->renderChildren(),
$name,
$count,
));
return array($list_item, $action_link);
}
}
diff --git a/src/view/phui/PHUITagView.php b/src/view/phui/PHUITagView.php
index 292246c4a..aa309a4bb 100644
--- a/src/view/phui/PHUITagView.php
+++ b/src/view/phui/PHUITagView.php
@@ -1,312 +1,317 @@
<?php
final class PHUITagView extends AphrontTagView {
const TYPE_PERSON = 'person';
const TYPE_OBJECT = 'object';
const TYPE_STATE = 'state';
const TYPE_SHADE = 'shade';
const TYPE_OUTLINE = 'outline';
const COLOR_RED = 'red';
const COLOR_ORANGE = 'orange';
const COLOR_YELLOW = 'yellow';
const COLOR_BLUE = 'blue';
const COLOR_INDIGO = 'indigo';
const COLOR_VIOLET = 'violet';
const COLOR_GREEN = 'green';
const COLOR_BLACK = 'black';
const COLOR_SKY = 'sky';
const COLOR_FIRE = 'fire';
const COLOR_GREY = 'grey';
const COLOR_WHITE = 'white';
const COLOR_PINK = 'pink';
const COLOR_BLUEGREY = 'bluegrey';
const COLOR_CHECKERED = 'checkered';
const COLOR_DISABLED = 'disabled';
const COLOR_OBJECT = 'object';
const COLOR_PERSON = 'person';
const BORDER_NONE = 'border-none';
private $type;
private $href;
private $name;
private $phid;
private $color;
private $backgroundColor;
private $dotColor;
private $closed;
private $external;
private $icon;
private $shade;
private $slimShady;
private $border;
public function setType($type) {
$this->type = $type;
switch ($type) {
case self::TYPE_SHADE:
case self::TYPE_OUTLINE:
break;
case self::TYPE_OBJECT:
$this->setBackgroundColor(self::COLOR_OBJECT);
break;
case self::TYPE_PERSON:
$this->setBackgroundColor(self::COLOR_PERSON);
break;
}
return $this;
}
/**
* This method has been deprecated, use @{method:setColor} instead.
*
* @deprecated
*/
public function setShade($shade) {
phlog(
pht('Deprecated call to setShade(), use setColor() instead.'));
$this->color = $shade;
return $this;
}
public function setColor($color) {
$this->color = $color;
return $this;
}
public function setDotColor($dot_color) {
$this->dotColor = $dot_color;
return $this;
}
public function setBackgroundColor($background_color) {
$this->backgroundColor = $background_color;
return $this;
}
public function setPHID($phid) {
$this->phid = $phid;
return $this;
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function setHref($href) {
$this->href = $href;
return $this;
}
public function setClosed($closed) {
$this->closed = $closed;
return $this;
}
public function setBorder($border) {
$this->border = $border;
return $this;
}
public function setIcon($icon) {
$this->icon = $icon;
return $this;
}
- public function setSlimShady($mm) {
- $this->slimShady = $mm;
+ public function setSlimShady($is_eminem) {
+ $this->slimShady = $is_eminem;
return $this;
}
protected function getTagName() {
return strlen($this->href) ? 'a' : 'span';
}
protected function getTagAttributes() {
require_celerity_resource('phui-tag-view-css');
$classes = array(
'phui-tag-view',
'phui-tag-type-'.$this->type,
);
if ($this->color) {
$classes[] = 'phui-tag-'.$this->color;
}
if ($this->slimShady) {
$classes[] = 'phui-tag-slim';
}
if ($this->type == self::TYPE_SHADE) {
$classes[] = 'phui-tag-shade';
}
if ($this->icon) {
$classes[] = 'phui-tag-icon-view';
}
if ($this->border) {
$classes[] = 'phui-tag-'.$this->border;
}
+ $attributes = array(
+ 'href' => $this->href,
+ 'class' => $classes,
+ );
+
+ if ($this->external) {
+ $attributes += array(
+ 'target' => '_blank',
+ 'rel' => 'noreferrer',
+ );
+ }
+
if ($this->phid) {
Javelin::initBehavior('phui-hovercards');
- $attributes = array(
- 'href' => $this->href,
+ $attributes += array(
'sigil' => 'hovercard',
- 'meta' => array(
+ 'meta' => array(
'hoverPHID' => $this->phid,
),
- 'target' => $this->external ? '_blank' : null,
- );
- } else {
- $attributes = array(
- 'href' => $this->href,
- 'target' => $this->external ? '_blank' : null,
);
}
- return $attributes + array('class' => $classes);
+ return $attributes;
}
protected function getTagContent() {
if (!$this->type) {
throw new PhutilInvalidStateException('setType', 'render');
}
$color = null;
if (!$this->shade && $this->backgroundColor) {
$color = 'phui-tag-color-'.$this->backgroundColor;
}
if ($this->dotColor) {
$dotcolor = 'phui-tag-color-'.$this->dotColor;
$dot = phutil_tag(
'span',
array(
'class' => 'phui-tag-dot '.$dotcolor,
),
'');
} else {
$dot = null;
}
if ($this->icon) {
$icon = id(new PHUIIconView())
->setIcon($this->icon);
} else {
$icon = null;
}
$content = phutil_tag(
'span',
array(
'class' => 'phui-tag-core '.$color,
),
array($dot, $icon, $this->name));
if ($this->closed) {
$content = phutil_tag(
'span',
array(
'class' => 'phui-tag-core-closed',
),
array($icon, $content));
}
return $content;
}
public static function getTagTypes() {
return array(
self::TYPE_PERSON,
self::TYPE_OBJECT,
self::TYPE_STATE,
);
}
public static function getColors() {
return array(
self::COLOR_RED,
self::COLOR_ORANGE,
self::COLOR_YELLOW,
self::COLOR_BLUE,
self::COLOR_INDIGO,
self::COLOR_VIOLET,
self::COLOR_GREEN,
self::COLOR_BLACK,
self::COLOR_GREY,
self::COLOR_WHITE,
self::COLOR_OBJECT,
self::COLOR_PERSON,
);
}
public static function getShades() {
return array_keys(self::getShadeMap());
}
public static function getShadeMap() {
return array(
self::COLOR_RED => pht('Red'),
self::COLOR_ORANGE => pht('Orange'),
self::COLOR_YELLOW => pht('Yellow'),
self::COLOR_BLUE => pht('Blue'),
self::COLOR_INDIGO => pht('Indigo'),
self::COLOR_VIOLET => pht('Violet'),
self::COLOR_GREEN => pht('Green'),
self::COLOR_GREY => pht('Grey'),
self::COLOR_PINK => pht('Pink'),
self::COLOR_CHECKERED => pht('Checkered'),
self::COLOR_DISABLED => pht('Disabled'),
);
}
public static function getShadeName($shade) {
return idx(self::getShadeMap(), $shade, $shade);
}
public static function getOutlines() {
return array_keys(self::getOutlineMap());
}
public static function getOutlineMap() {
return array(
self::COLOR_RED => pht('Red'),
self::COLOR_ORANGE => pht('Orange'),
self::COLOR_YELLOW => pht('Yellow'),
self::COLOR_BLUE => pht('Blue'),
self::COLOR_INDIGO => pht('Indigo'),
self::COLOR_VIOLET => pht('Violet'),
self::COLOR_GREEN => pht('Green'),
self::COLOR_GREY => pht('Grey'),
self::COLOR_PINK => pht('Pink'),
self::COLOR_SKY => pht('Sky'),
self::COLOR_FIRE => pht('Fire'),
self::COLOR_BLACK => pht('Black'),
self::COLOR_DISABLED => pht('Disabled'),
);
}
public static function getOutlineName($outline) {
return idx(self::getOutlineMap(), $outline, $outline);
}
public function setExternal($external) {
$this->external = $external;
return $this;
}
public function getExternal() {
return $this->external;
}
}
diff --git a/src/view/phui/PHUITimelineEventView.php b/src/view/phui/PHUITimelineEventView.php
index 161dd0c94..e64b9b06b 100644
--- a/src/view/phui/PHUITimelineEventView.php
+++ b/src/view/phui/PHUITimelineEventView.php
@@ -1,710 +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();
- $has_menu = false;
if (!$this->getIsPreview() && !$this->getHideCommentOptions()) {
foreach ($this->getEventGroup() as $event) {
$items[] = $event->getMenuItems($this->anchor);
- if ($event->hasChildren()) {
- $has_menu = true;
- }
}
$items = array_mergev($items);
}
- if ($items || $has_menu) {
+ 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',
'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/startup/PhabricatorStartup.php b/support/startup/PhabricatorStartup.php
index 1911a46b8..212b05737 100644
--- a/support/startup/PhabricatorStartup.php
+++ b/support/startup/PhabricatorStartup.php
@@ -1,821 +1,826 @@
<?php
/**
* Handle request startup, before loading the environment or libraries. This
* class bootstraps the request state up to the point where we can enter
* Phabricator code.
*
* NOTE: This class MUST NOT have any dependencies. It runs before libraries
* load.
*
* Rate Limiting
* =============
*
* Phabricator limits the rate at which clients can request pages, and issues
* HTTP 429 "Too Many Requests" responses if clients request too many pages too
* quickly. Although this is not a complete defense against high-volume attacks,
* it can protect an install against aggressive crawlers, security scanners,
* and some types of malicious activity.
*
* To perform rate limiting, each page increments a score counter for the
* requesting user's IP. The page can give the IP more points for an expensive
* request, or fewer for an authetnicated request.
*
* Score counters are kept in buckets, and writes move to a new bucket every
* minute. After a few minutes (defined by @{method:getRateLimitBucketCount}),
* the oldest bucket is discarded. This provides a simple mechanism for keeping
* track of scores without needing to store, access, or read very much data.
*
* Users are allowed to accumulate up to 1000 points per minute, averaged across
* all of the tracked buckets.
*
* @task info Accessing Request Information
* @task hook Startup Hooks
* @task apocalypse In Case Of Apocalypse
* @task validation Validation
* @task ratelimit Rate Limiting
* @task phases Startup Phase Timers
*/
final class PhabricatorStartup {
private static $startTime;
private static $debugTimeLimit;
private static $accessLog;
private static $capturingOutput;
private static $rawInput;
private static $oldMemoryLimit;
private static $phases;
private static $limits = array();
/* -( Accessing Request Information )-------------------------------------- */
/**
* @task info
*/
public static function getStartTime() {
return self::$startTime;
}
/**
* @task info
*/
public static function getMicrosecondsSinceStart() {
return (int)(1000000 * (microtime(true) - self::getStartTime()));
}
/**
* @task info
*/
public static function setAccessLog($access_log) {
self::$accessLog = $access_log;
}
/**
* @task info
*/
public static function getRawInput() {
if (self::$rawInput === null) {
$stream = new AphrontRequestStream();
if (isset($_SERVER['HTTP_CONTENT_ENCODING'])) {
$encoding = trim($_SERVER['HTTP_CONTENT_ENCODING']);
$stream->setEncoding($encoding);
}
$input = '';
do {
$bytes = $stream->readData();
if ($bytes === null) {
break;
}
$input .= $bytes;
} while (true);
self::$rawInput = $input;
}
return self::$rawInput;
}
/* -( Startup Hooks )------------------------------------------------------ */
/**
* @param float Request start time, from `microtime(true)`.
* @task hook
*/
public static function didStartup($start_time) {
self::$startTime = $start_time;
self::$phases = array();
self::$accessLog = null;
static $registered;
if (!$registered) {
// NOTE: This protects us against multiple calls to didStartup() in the
// same request, but also against repeated requests to the same
// interpreter state, which we may implement in the future.
register_shutdown_function(array(__CLASS__, 'didShutdown'));
$registered = true;
}
self::setupPHP();
self::verifyPHP();
// If we've made it this far, the environment isn't completely broken so
// we can switch over to relying on our own exception recovery mechanisms.
ini_set('display_errors', 0);
self::connectRateLimits();
self::normalizeInput();
self::verifyRewriteRules();
self::detectPostMaxSizeTriggered();
self::beginOutputCapture();
}
/**
* @task hook
*/
public static function didShutdown() {
// Disconnect any active rate limits before we shut down. If we don't do
// this, requests which exit early will lock a slot in any active
// connection limits, and won't count for rate limits.
self::disconnectRateLimits(array());
$event = error_get_last();
if (!$event) {
return;
}
switch ($event['type']) {
case E_ERROR:
case E_PARSE:
case E_COMPILE_ERROR:
break;
default:
return;
}
$msg = ">>> UNRECOVERABLE FATAL ERROR <<<\n\n";
if ($event) {
// Even though we should be emitting this as text-plain, escape things
// just to be sure since we can't really be sure what the program state
// is when we get here.
$msg .= htmlspecialchars(
$event['message']."\n\n".$event['file'].':'.$event['line'],
ENT_QUOTES,
'UTF-8');
}
// flip dem tables
$msg .= "\n\n\n";
$msg .= "\xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb\x20\xef\xb8\xb5\x20\xc2\xaf".
"\x5c\x5f\x28\xe3\x83\x84\x29\x5f\x2f\xc2\xaf\x20\xef\xb8\xb5\x20".
"\xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb";
self::didFatal($msg);
}
public static function loadCoreLibraries() {
$phabricator_root = dirname(dirname(dirname(__FILE__)));
$libraries_root = dirname($phabricator_root);
$root = null;
if (!empty($_SERVER['PHUTIL_LIBRARY_ROOT'])) {
$root = $_SERVER['PHUTIL_LIBRARY_ROOT'];
}
ini_set(
'include_path',
$libraries_root.PATH_SEPARATOR.ini_get('include_path'));
@include_once $root.'libphutil/src/__phutil_library_init__.php';
if (!@constant('__LIBPHUTIL__')) {
self::didFatal(
"Unable to load libphutil. Put libphutil/ next to phabricator/, or ".
"update your PHP 'include_path' to include the parent directory of ".
"libphutil/.");
}
phutil_load_library('arcanist/src');
// Load Phabricator itself using the absolute path, so we never end up doing
// anything surprising (loading index.php and libraries from different
// directories).
phutil_load_library($phabricator_root.'/src');
}
/* -( Output Capture )----------------------------------------------------- */
public static function beginOutputCapture() {
if (self::$capturingOutput) {
self::didFatal('Already capturing output!');
}
self::$capturingOutput = true;
ob_start();
}
public static function endOutputCapture() {
if (!self::$capturingOutput) {
return null;
}
self::$capturingOutput = false;
return ob_get_clean();
}
/* -( Debug Time Limit )--------------------------------------------------- */
/**
* Set a time limit (in seconds) for the current script. After time expires,
* the script fatals.
*
* This works like `max_execution_time`, but prints out a useful stack trace
* when the time limit expires. This is primarily intended to make it easier
* to debug pages which hang by allowing extraction of a stack trace: set a
* short debug limit, then use the trace to figure out what's happening.
*
* The limit is implemented with a tick function, so enabling it implies
* some accounting overhead.
*
* @param int Time limit in seconds.
* @return void
*/
public static function setDebugTimeLimit($limit) {
self::$debugTimeLimit = $limit;
static $initialized;
if (!$initialized) {
declare(ticks=1);
register_tick_function(array(__CLASS__, 'onDebugTick'));
}
}
/**
* Callback tick function used by @{method:setDebugTimeLimit}.
*
* Fatals with a useful stack trace after the time limit expires.
*
* @return void
*/
public static function onDebugTick() {
$limit = self::$debugTimeLimit;
if (!$limit) {
return;
}
$elapsed = (microtime(true) - self::getStartTime());
if ($elapsed > $limit) {
$frames = array();
foreach (debug_backtrace() as $frame) {
$file = isset($frame['file']) ? $frame['file'] : '-';
$file = basename($file);
$line = isset($frame['line']) ? $frame['line'] : '-';
$class = isset($frame['class']) ? $frame['class'].'->' : null;
$func = isset($frame['function']) ? $frame['function'].'()' : '?';
$frames[] = "{$file}:{$line} {$class}{$func}";
}
self::didFatal(
"Request aborted by debug time limit after {$limit} seconds.\n\n".
"STACK TRACE\n".
implode("\n", $frames));
}
}
/* -( In Case of Apocalypse )---------------------------------------------- */
/**
* Fatal the request completely in response to an exception, sending a plain
* text message to the client. Calls @{method:didFatal} internally.
*
* @param string Brief description of the exception context, like
* `"Rendering Exception"`.
* @param Exception The exception itself.
* @param bool True if it's okay to show the exception's stack trace
* to the user. The trace will always be logged.
* @return exit This method **does not return**.
*
* @task apocalypse
*/
public static function didEncounterFatalException(
$note,
Exception $ex,
$show_trace) {
$message = '['.$note.'/'.get_class($ex).'] '.$ex->getMessage();
$full_message = $message;
$full_message .= "\n\n";
$full_message .= $ex->getTraceAsString();
if ($show_trace) {
$message = $full_message;
}
self::didFatal($message, $full_message);
}
/**
* Fatal the request completely, sending a plain text message to the client.
*
* @param string Plain text message to send to the client.
* @param string Plain text message to send to the error log. If not
* provided, the client message is used. You can pass a more
* detailed message here (e.g., with stack traces) to avoid
* showing it to users.
* @return exit This method **does not return**.
*
* @task apocalypse
*/
public static function didFatal($message, $log_message = null) {
if ($log_message === null) {
$log_message = $message;
}
self::endOutputCapture();
$access_log = self::$accessLog;
if ($access_log) {
// We may end up here before the access log is initialized, e.g. from
// verifyPHP().
$access_log->setData(
array(
'c' => 500,
));
$access_log->write();
}
header(
'Content-Type: text/plain; charset=utf-8',
$replace = true,
$http_error = 500);
error_log($log_message);
echo $message."\n";
exit(1);
}
/* -( Validation )--------------------------------------------------------- */
/**
* @task validation
*/
private static function setupPHP() {
error_reporting(E_ALL | E_STRICT);
self::$oldMemoryLimit = ini_get('memory_limit');
ini_set('memory_limit', -1);
// If we have libxml, disable the incredibly dangerous entity loader.
if (function_exists('libxml_disable_entity_loader')) {
libxml_disable_entity_loader(true);
}
+
+ // See T13060. If the locale for this process (the parent process) is not
+ // a UTF-8 locale we can encounter problems when launching subprocesses
+ // which receive UTF-8 parameters in their command line argument list.
+ @setlocale(LC_ALL, 'en_US.UTF-8');
}
/**
* @task validation
*/
public static function getOldMemoryLimit() {
return self::$oldMemoryLimit;
}
/**
* @task validation
*/
private static function normalizeInput() {
// Replace superglobals with unfiltered versions, disrespect php.ini (we
// filter ourselves).
// NOTE: We don't filter INPUT_SERVER because we don't want to overwrite
// changes made in "preamble.php".
// NOTE: WE don't filter INPUT_POST because we may be constructing it
// lazily if "enable_post_data_reading" is disabled.
$filter = array(
INPUT_GET,
INPUT_ENV,
INPUT_COOKIE,
);
foreach ($filter as $type) {
$filtered = filter_input_array($type, FILTER_UNSAFE_RAW);
if (!is_array($filtered)) {
continue;
}
switch ($type) {
case INPUT_GET:
$_GET = array_merge($_GET, $filtered);
break;
case INPUT_COOKIE:
$_COOKIE = array_merge($_COOKIE, $filtered);
break;
case INPUT_ENV;
$env = array_merge($_ENV, $filtered);
$_ENV = self::filterEnvSuperglobal($env);
break;
}
}
self::rebuildRequest();
}
/**
* @task validation
*/
public static function rebuildRequest() {
// Rebuild $_REQUEST, respecting order declared in ".ini" files.
$order = ini_get('request_order');
if (!$order) {
$order = ini_get('variables_order');
}
if (!$order) {
// $_REQUEST will be empty, so leave it alone.
return;
}
$_REQUEST = array();
for ($ii = 0; $ii < strlen($order); $ii++) {
switch ($order[$ii]) {
case 'G':
$_REQUEST = array_merge($_REQUEST, $_GET);
break;
case 'P':
$_REQUEST = array_merge($_REQUEST, $_POST);
break;
case 'C':
$_REQUEST = array_merge($_REQUEST, $_COOKIE);
break;
default:
// $_ENV and $_SERVER never go into $_REQUEST.
break;
}
}
}
/**
* Adjust `$_ENV` before execution.
*
* Adjustments here primarily impact the environment as seen by subprocesses.
* The environment is forwarded explicitly by @{class:ExecFuture}.
*
* @param map<string, wild> Input `$_ENV`.
* @return map<string, string> Suitable `$_ENV`.
* @task validation
*/
private static function filterEnvSuperglobal(array $env) {
// In some configurations, we may get "argc" and "argv" set in $_ENV.
// These are not real environmental variables, and "argv" may have an array
// value which can not be forwarded to subprocesses. Remove these from the
// environment if they are present.
unset($env['argc']);
unset($env['argv']);
return $env;
}
/**
* @task validation
*/
private static function verifyPHP() {
$required_version = '5.2.3';
if (version_compare(PHP_VERSION, $required_version) < 0) {
self::didFatal(
"You are running PHP version '".PHP_VERSION."', which is older than ".
"the minimum version, '{$required_version}'. Update to at least ".
"'{$required_version}'.");
}
if (get_magic_quotes_gpc()) {
self::didFatal(
"Your server is configured with PHP 'magic_quotes_gpc' enabled. This ".
"feature is 'highly discouraged' by PHP's developers and you must ".
"disable it to run Phabricator. Consult the PHP manual for ".
"instructions.");
}
if (extension_loaded('apc')) {
$apc_version = phpversion('apc');
$known_bad = array(
'3.1.14' => true,
'3.1.15' => true,
'3.1.15-dev' => true,
);
if (isset($known_bad[$apc_version])) {
self::didFatal(
"You have APC {$apc_version} installed. This version of APC is ".
"known to be bad, and does not work with Phabricator (it will ".
"cause Phabricator to fatal unrecoverably with nonsense errors). ".
"Downgrade to version 3.1.13.");
}
}
if (isset($_SERVER['HTTP_PROXY'])) {
self::didFatal(
'This HTTP request included a "Proxy:" header, poisoning the '.
'environment (CVE-2016-5385 / httpoxy). Declining to process this '.
'request. For details, see: https://phurl.io/u/httpoxy');
}
}
/**
* @task validation
*/
private static function verifyRewriteRules() {
if (isset($_REQUEST['__path__']) && strlen($_REQUEST['__path__'])) {
return;
}
if (php_sapi_name() == 'cli-server') {
// Compatibility with PHP 5.4+ built-in web server.
$url = parse_url($_SERVER['REQUEST_URI']);
$_REQUEST['__path__'] = $url['path'];
return;
}
if (!isset($_REQUEST['__path__'])) {
self::didFatal(
"Request parameter '__path__' is not set. Your rewrite rules ".
"are not configured correctly.");
}
if (!strlen($_REQUEST['__path__'])) {
self::didFatal(
"Request parameter '__path__' is set, but empty. Your rewrite rules ".
"are not configured correctly. The '__path__' should always ".
"begin with a '/'.");
}
}
/**
* Detect if this request has had its POST data stripped by exceeding the
* 'post_max_size' PHP configuration limit.
*
* PHP has a setting called 'post_max_size'. If a POST request arrives with
* a body larger than the limit, PHP doesn't generate $_POST but processes
* the request anyway, and provides no formal way to detect that this
* happened.
*
* We can still read the entire body out of `php://input`. However according
* to the documentation the stream isn't available for "multipart/form-data"
* (on nginx + php-fpm it appears that it is available, though, at least) so
* any attempt to generate $_POST would be fragile.
*
* @task validation
*/
private static function detectPostMaxSizeTriggered() {
// If this wasn't a POST, we're fine.
if ($_SERVER['REQUEST_METHOD'] != 'POST') {
return;
}
// If "enable_post_data_reading" is off, we won't have $_POST and this
// condition is effectively impossible.
if (!ini_get('enable_post_data_reading')) {
return;
}
// If there's POST data, clearly we're in good shape.
if ($_POST) {
return;
}
// For HTML5 drag-and-drop file uploads, Safari submits the data as
// "application/x-www-form-urlencoded". For most files this generates
// something in POST because most files decode to some nonempty (albeit
// meaningless) value. However, some files (particularly small images)
// don't decode to anything. If we know this is a drag-and-drop upload,
// we can skip this check.
if (isset($_REQUEST['__upload__'])) {
return;
}
// PHP generates $_POST only for two content types. This routing happens
// in `main/php_content_types.c` in PHP. Normally, all forms use one of
// these content types, but some requests may not -- for example, Firefox
// submits files sent over HTML5 XMLHTTPRequest APIs with the Content-Type
// of the file itself. If we don't have a recognized content type, we
// don't need $_POST.
//
// NOTE: We use strncmp() because the actual content type may be something
// like "multipart/form-data; boundary=...".
//
// NOTE: Chrome sometimes omits this header, see some discussion in T1762
// and http://code.google.com/p/chromium/issues/detail?id=6800
$content_type = isset($_SERVER['CONTENT_TYPE'])
? $_SERVER['CONTENT_TYPE']
: '';
$parsed_types = array(
'application/x-www-form-urlencoded',
'multipart/form-data',
);
$is_parsed_type = false;
foreach ($parsed_types as $parsed_type) {
if (strncmp($content_type, $parsed_type, strlen($parsed_type)) === 0) {
$is_parsed_type = true;
break;
}
}
if (!$is_parsed_type) {
return;
}
// Check for 'Content-Length'. If there's no data, we don't expect $_POST
// to exist.
$length = (int)$_SERVER['CONTENT_LENGTH'];
if (!$length) {
return;
}
// Time to fatal: we know this was a POST with data that should have been
// populated into $_POST, but it wasn't.
$config = ini_get('post_max_size');
self::didFatal(
"As received by the server, this request had a nonzero content length ".
"but no POST data.\n\n".
"Normally, this indicates that it exceeds the 'post_max_size' setting ".
"in the PHP configuration on the server. Increase the 'post_max_size' ".
"setting or reduce the size of the request.\n\n".
"Request size according to 'Content-Length' was '{$length}', ".
"'post_max_size' is set to '{$config}'.");
}
/* -( Rate Limiting )------------------------------------------------------ */
/**
* Add a new client limits.
*
* @param PhabricatorClientLimit New limit.
* @return PhabricatorClientLimit The limit.
*/
public static function addRateLimit(PhabricatorClientLimit $limit) {
self::$limits[] = $limit;
return $limit;
}
/**
* Apply configured rate limits.
*
* If any limit is exceeded, this method terminates the request.
*
* @return void
* @task ratelimit
*/
private static function connectRateLimits() {
$limits = self::$limits;
$reason = null;
$connected = array();
foreach ($limits as $limit) {
$reason = $limit->didConnect();
$connected[] = $limit;
if ($reason !== null) {
break;
}
}
// If we're killing the request here, disconnect any limits that we
// connected to try to keep the accounting straight.
if ($reason !== null) {
foreach ($connected as $limit) {
$limit->didDisconnect(array());
}
self::didRateLimit($reason);
}
}
/**
* Tear down rate limiting and allow limits to score the request.
*
* @param map<string, wild> Additional, freeform request state.
* @return void
* @task ratelimit
*/
public static function disconnectRateLimits(array $request_state) {
$limits = self::$limits;
// Remove all limits before disconnecting them so this works properly if
// it runs twice. (We run this automatically as a shutdown handler.)
self::$limits = array();
foreach ($limits as $limit) {
$limit->didDisconnect($request_state);
}
}
/**
* Emit an HTTP 429 "Too Many Requests" response (indicating that the user
* has exceeded application rate limits) and exit.
*
* @return exit This method **does not return**.
* @task ratelimit
*/
private static function didRateLimit($reason) {
header(
'Content-Type: text/plain; charset=utf-8',
$replace = true,
$http_error = 429);
echo $reason;
exit(1);
}
/* -( Startup Timers )----------------------------------------------------- */
/**
* Record the beginning of a new startup phase.
*
* For phases which occur before @{class:PhabricatorStartup} loads, save the
* time and record it with @{method:recordStartupPhase} after the class is
* available.
*
* @param string Phase name.
* @task phases
*/
public static function beginStartupPhase($phase) {
self::recordStartupPhase($phase, microtime(true));
}
/**
* Record the start time of a previously executed startup phase.
*
* For startup phases which occur after @{class:PhabricatorStartup} loads,
* use @{method:beginStartupPhase} instead. This method can be used to
* record a time before the class loads, then hand it over once the class
* becomes available.
*
* @param string Phase name.
* @param float Phase start time, from `microtime(true)`.
* @task phases
*/
public static function recordStartupPhase($phase, $time) {
self::$phases[$phase] = $time;
}
/**
* Get information about startup phase timings.
*
* Sometimes, performance problems can occur before we start the profiler.
* Since the profiler can't examine these phases, it isn't useful in
* understanding their performance costs.
*
* Instead, the startup process marks when it enters various phases using
* @{method:beginStartupPhase}. A later call to this method can retrieve this
* information, which can be examined to gain greater insight into where
* time was spent. The output is still crude, but better than nothing.
*
* @task phases
*/
public static function getPhases() {
return self::$phases;
}
}
diff --git a/webroot/favicon.ico b/webroot/favicon.ico
deleted file mode 100644
index f07770aa8..000000000
Binary files a/webroot/favicon.ico and /dev/null differ
diff --git a/webroot/rsrc/css/aphront/dark-console.css b/webroot/rsrc/css/aphront/dark-console.css
index 81e888ba4..c3833e75b 100644
--- a/webroot/rsrc/css/aphront/dark-console.css
+++ b/webroot/rsrc/css/aphront/dark-console.css
@@ -1,244 +1,245 @@
/**
* @provides aphront-dark-console-css
*/
.dark-console {
background: #444444;
color: #eeeeee;
width: 100%;
font-family: "Verdana";
font-size: 11px;
position: relative;
}
.dark-console a {
color: #cccccc;
}
.dark-console-requests,
.dark-console-tabs {
position: absolute;
overflow-y: auto;
top: 0;
left: 0;
bottom: 0;
width: 15%;
padding: 8px 0;
}
.dark-console-requests,
.dark-console-tabs,
.dark-console-panel,
.dark-console-load {
border-left: 1px solid #111111;
box-shadow: -2px 0px 2px rgba({$alphablack}, 0.25);
}
.dark-console-requests {
background: #222222;
}
.dark-console-tabs {
background: #333333;
left: 15%;
}
.dark-console-panel,
.dark-console-load {
position: relative;
min-height: 320px;
}
.dark-console-panel {
margin-left: 30%;
background: #444444;
}
.dark-console-requests a.dark-console-request,
.dark-console-tabs a.dark-console-tab {
display: block;
padding: 6px;
overflow: hidden;
background: #444444;
margin: 3px 0;
border-color: {$greytext};
border-width: 1px 0;
border-style: solid;
}
.dark-console-requests a.dark-selected,
.dark-console-tabs a.dark-selected {
background: #0066aa;
}
.dark-console-requests a.dark-console-request:hover,
.dark-console-tabs a.dark-console-tab:hover {
background: #1188cc;
text-decoration: none;
}
.dark-console-tabs a.dark-console-tab {
text-align: right;
}
.dark-console-load {
background-image: url(/rsrc/image/darkload.gif);
background-position: center center;
background-repeat: no-repeat;
background-color: #000;
margin-left: 15%;
}
.dark-console .aphront-table-view {
font-size: 11px;
background: {$lightgreytext};
color: #eeeeee;
width: 100%;
border-color: #333333;
margin: 8px 0;
}
.dark-console .aphront-table-view th {
text-shadow: none;
font-family: "Verdana";
font-size: 11px;
background: #333333;
color: #ffffff;
+ border-bottom: 0px;
}
.dark-console .aphront-table-view td {
font-size: 11px;
color: #eee;
}
.dark-console .aphront-table-view tr:hover,
.dark-console .aphront-table-view tr.alt:hover {
background: #333;
}
.dark-console .aphront-table-view td.header {
background: #444444;
color: #ffffff;
min-width: 200px;
}
.dark-console .aphront-table-view tr.alt {
background: {$greytext};
}
.dark-console .aphront-table-view tr.highlight {
background: #001A66;
}
.dark-console .aphront-table-view tr.no-data td {
color: #dddddd;
}
.dark-console-panel-core {
padding: 12px;
}
.explain-sev-1 {
color: #33ff33;
}
.explain-sev-2 {
color: #99ff33;
}
.explain-sev-3 {
color: #ccff33;
}
.explain-sev-4 {
color: #ffff33;
}
.explain-sev-5 {
color: #ffcc33;
}
.explain-sev-6 {
color: #ffffff;
font-weight: bold;
background: #aa0000;
padding: 0 1em;
border: 2px solid #ffff00;
}
.explain-sev-7 {
color: #aaaaaa;
}
.dark-console-panel-header {
padding: 8px 4px 0;
}
.dark-console-panel-header h1 {
font-size: 15px;
}
.dark-console-panel-header .button {
margin-top: .5em;
float: right;
}
.dark-console-panel a.bright-link {
color: #00cfff;
font-weight: bold;
}
.dark-console iframe {
width: 98%;
margin: .5em 1%;
height: 450px;
border: 0;
}
.dark-console-no-content {
padding: 1.5em 2em;
font-style: italic;
}
.dark-console-request-log {
border-bottom: 2px solid #000000;
}
.dark-console-panel-RequestLog {
max-height: 10em;
overflow: auto;
}
.dark-console-panel-request-log-separator {
- background-color: #e8e8e8;
- border-bottom: 1px solid #b7b7b7;
- border-top: 1px solid #b7b7b7;
- height: 2px;
+ background-color: #e8e8e8;
+ border-bottom: 1px solid #b7b7b7;
+ border-top: 1px solid #b7b7b7;
+ height: 2px;
}
.dark-console-panel-ErrorLog {
max-height: 500px;
overflow: auto;
}
.dark-console-panel-error-details {
display: none;
}
.dark-console-log-frame {
height: 500px;
overflow: auto;
background: #303030;
border: 1px solid #202020;
margin: 8px 0;
}
.dark-console-log-message {
background: #404040;
padding: 8px;
margin: 2px;
}
.dark-console-realtime .button {
margin-right: 8px;
}
diff --git a/webroot/rsrc/css/aphront/phabricator-nav-view.css b/webroot/rsrc/css/aphront/phabricator-nav-view.css
index e8081a55e..70a110d1f 100644
--- a/webroot/rsrc/css/aphront/phabricator-nav-view.css
+++ b/webroot/rsrc/css/aphront/phabricator-nav-view.css
@@ -1,97 +1,102 @@
/**
* @provides phabricator-nav-view-css
*/
.jx-drag-col {
cursor: col-resize;
}
.device-desktop .has-closed-nav div.phabricator-nav-local,
.device-desktop .has-closed-nav div.phabricator-nav-drag,
.device .phui-navigation-shell div.phabricator-nav-local,
.device .phui-navigation-shell div.phabricator-nav-drag {
display: none;
}
.device-desktop .has-local-nav .phabricator-nav-local,
.device-desktop .has-local-nav .phabricator-nav-drag {
display: block;
}
.device-phone .phabricator-side-menu-home .phabricator-nav-local {
display: block;
}
/* Home Sidenav */
.phui-basic-nav.phui-navigation-shell
.phabricator-side-menu-home .phabricator-nav-local {
padding-top: 16px;
padding-right: 0;
background-color: transparent;
width: 205px;
max-width: 205px;
}
.device-phone .phui-basic-nav.phui-navigation-shell
.phabricator-side-menu-home .phabricator-nav-local {
padding-top: 0;
padding-right: 0;
background-color: transparent;
width: auto;
}
.phabricator-nav-drag {
position: fixed;
top: 0;
bottom: 0;
- left: 205px;
+ left: 310px;
width: 7px;
cursor: col-resize;
background: #f5f5f5;
border-style: solid;
border-width: 0 1px 0 1px;
border-color: #fff #999c9e #fff #999c9e;
box-shadow: inset -1px 0px 1px rgba({$alphablack}, 0.15);
background-image: url(/rsrc/image/divot.png);
background-position: center;
background-repeat: no-repeat;
}
.phabricator-nav-content {
overflow: hidden;
}
.device-desktop .phabricator-standard-page-body .has-drag-nav
.phabricator-nav-content {
- margin-left: 212px;
+ margin-left: 317px;
+}
+
+.device-desktop .phabricator-standard-page-body .has-drag-nav
+ .phabricator-nav-local {
+ max-width: none;
}
.device-desktop .phabricator-standard-page-body .has-drag-nav
.phabricator-nav-local {
max-width: none;
}
.has-drag-nav ul.phui-list-view {
height: 100%;
overflow-y: auto;
overflow-x: hidden;
}
.device-desktop .phui-navigation-shell .has-drag-nav .phabricator-nav-local {
- width: 205px;
+ width: 310px;
padding: 0;
background: transparent;
}
.device-phone .phabricator-side-menu-home .phabricator-nav-content {
display: none;
}
.device-phone .phabricator-side-menu-home .phabricator-nav-local {
width: 100%;
padding-top: 0;
margin-top: 0;
}
diff --git a/webroot/rsrc/css/application/config/setup-issue.css b/webroot/rsrc/css/application/config/setup-issue.css
index 7ba033d9c..a80b02815 100644
--- a/webroot/rsrc/css/application/config/setup-issue.css
+++ b/webroot/rsrc/css/application/config/setup-issue.css
@@ -1,149 +1,149 @@
/**
* @provides setup-issue-css
*/
.setup-issue-background {
padding: 12px 0;
}
.setup-issue-shell {
max-width: 880px;
margin: 16px auto;
}
.setup-issue {
background: #fff;
border: 1px solid #e4e5e6;
padding: 0;
border-radius: 5px;
}
.setup-issue p {
margin: 12px 0;
}
.setup-issue table {
width: 100%;
border-collapse: collapse;
border: 1px solid #e2e2e2;
}
.setup-issue table th {
text-align: right;
width: 30%;
background: #F8F9FC;
border: 1px solid #e2e2e2;
padding: 8px;
}
.setup-issue table td {
border: 1px solid #e2e2e2;
padding: 8px;
word-break: break-word;
}
.setup-issue pre {
width: auto;
border: 1px solid #e2e2e2;
padding: 12px;
background: #F8F9FC;
overflow-x: auto;
border-radius: 3px;
}
.setup-issue tt {
color: black;
background: #EBECEE;
padding: 2px 4px;
}
.setup-issue em {
font-weight: bold;
}
.setup-issue-instructions {
font-size: 13px;
padding: 16px;
line-height: 20px;
background: rgba(71,87,120,0.08);
border-radius: 3px;
}
.setup-fatal .setup-issue-instructions {
color: #af1404;
background: #f4dddb;
}
.setup-issue-name {
font-size: 15px;
font-weight: bold;
color: #6B748C;
border-bottom: 1px solid #e2e2e2;
overflow: hidden;
padding: 16px;
line-height: 24px;
}
.setup-issue-body {
- padding: 16px 16px 0 16px;
+ padding: 16px;
}
.setup-issue-status {
margin: 0 0 16px 0;
padding: 12px;
background: #FDF5D4;
color: #ab8206;
border-radius: 3px;
}
.setup-issue-actions {
padding: 12px;
background-color: #f5f5f5;
border-bottom-right-radius: 5px;
border-bottom-left-radius: 5px;
overflow: hidden;
text-align: right;
}
.setup-issue-actions .button {
margin-left: 8px;
}
.setup-issue-next {
padding: 12px;
border: 1px solid #BFCFDA;
background: #daeaf3;
text-align: center;
margin: 12px 0;
color: #2980b9;
border-radius: 3px;
}
.setup-issue-config {
padding: 12px 0;
}
.setup-issue-config + .setup-issue-config {
padding-top: 0;
}
.setup-issue ul {
width: 90%;
margin: 1em auto;
list-style: circle;
}
.setup-issue-debug {
margin-top: 8px;
padding: 4px;
text-align: right;
color: #74777D;
}
.phui-two-column-view .setup-issue-background {
padding: 0;
}
.phui-two-column-view .setup-issue-shell {
width: auto;
margin: 0;
}
diff --git a/webroot/rsrc/css/application/harbormaster/harbormaster.css b/webroot/rsrc/css/application/harbormaster/harbormaster.css
index 4bfd2d6cb..f3b75a397 100644
--- a/webroot/rsrc/css/application/harbormaster/harbormaster.css
+++ b/webroot/rsrc/css/application/harbormaster/harbormaster.css
@@ -1,32 +1,169 @@
/**
* @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-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/application/owners/owners-path-editor.css b/webroot/rsrc/css/application/owners/owners-path-editor.css
index 026420123..b70365636 100644
--- a/webroot/rsrc/css/application/owners/owners-path-editor.css
+++ b/webroot/rsrc/css/application/owners/owners-path-editor.css
@@ -1,37 +1,49 @@
/**
* @provides owners-path-editor-css
*/
.owners-path-editor-table {
- margin: 10px;
+ margin: 10px 0;
+ width: 100%;
}
.owners-path-editor-table td {
padding: 2px 4px;
vertical-align: middle;
}
-.owners-path-editor-table select.owners-repo {
- width: 150px;
+.owners-path-editor-table td.owners-path-mode-control {
+ width: 180px;
}
-.owners-path-editor-table input {
- width: 400px;
+.owners-path-editor-table td.owners-path-mode-control select {
+ width: 100%;
}
-.owners-path-editor-table div.error-display {
- padding: 4px 12px 0;
+.owners-path-editor-table td.owners-path-repo-control {
+ width: 280px;
}
-.owners-path-editor-table div.validating {
- color: {$greytext};
+.owners-path-editor-table td.owners-path-path-control {
+ width: auto;
}
-.owners-path-editor-table div.invalid {
- color: #aa0000;
+.owners-path-editor-table td.owners-path-path-control input {
+ width: 100%;
}
-.owners-path-editor-table div.valid {
- color: #00aa00;
- font-weight: bold;
+.owners-path-editor-table td.owners-path-path-control .jx-typeahead-results a {
+ padding: 4px;
+}
+
+.owners-path-editor-table td.owners-path-icon-control {
+ width: 18px;
+}
+
+.owners-path-editor-table td.remove-column {
+ width: 100px;
+}
+
+.owners-path-editor-table td.remove-column a {
+ display: block;
}
diff --git a/webroot/rsrc/css/core/remarkup.css b/webroot/rsrc/css/core/remarkup.css
index 3da8d0b9a..86f00afcb 100644
--- a/webroot/rsrc/css/core/remarkup.css
+++ b/webroot/rsrc/css/core/remarkup.css
@@ -1,905 +1,921 @@
/**
* @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: inline-block;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
.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;
- /*height: 22px;*/
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;
}
.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-filetree-view.css b/webroot/rsrc/css/layout/phabricator-filetree-view.css
index b247f5e4f..6497c3705 100644
--- a/webroot/rsrc/css/layout/phabricator-filetree-view.css
+++ b/webroot/rsrc/css/layout/phabricator-filetree-view.css
@@ -1,56 +1,91 @@
/**
* @provides phabricator-filetree-view-css
*/
.phabricator-filetree {
padding: 4px 0;
}
/* NOTE: Until the whole side nav situation gets cleaned up, we need to be
highly specific in specifying selectors here, to override side nav styles.
*/
.phabricator-filetree .phabricator-filetree-item {
margin: 0;
padding: 0;
display: block;
border-left: 4px solid transparent;
}
.phabricator-filetree span.phabricator-filetree-icon {
background-repeat: no-repeat;
background-position: 0 2px;
width: 16px;
height: 20px;
padding: 0;
float: left;
}
.phabricator-filetree span.phabricator-filetree-name {
padding: 0;
margin-left: 4px;
font-size: 12px;
font-weight: normal;
line-height: 20px;
white-space: nowrap;
}
.phabricator-filetree span.phabricator-filetree-item
.phabricator-filetree-name {
color: {$darkbluetext};
}
.phabricator-filetree a.phabricator-filetree-item
.phabricator-filetree-name {
color: {$darkbluetext};
}
.phabricator-filetree a.phabricator-filetree-item:hover {
text-decoration: none;
background-color: {$hovergrey};
}
+.phabricator-filetree .filetree-added {
+ background: {$sh-greenbackground};
+}
+
+.phabricator-filetree .filetree-deleted {
+ background: {$sh-redbackground};
+}
+
+.phabricator-filetree .filetree-movecopy {
+ background: {$sh-orangebackground};
+}
+
.phabricator-filetree .phabricator-active-nav-focus {
background-color: {$hovergrey};
border-left: 4px solid {$sky};
}
+
+.phabricator-filetree .filetree-progress-hint {
+ width: 24px;
+ margin-right: 6px;
+ display: inline-block;
+ padding: 0 4px;
+ border-radius: 4px;
+ font-size: smaller;
+ background: {$greybackground};
+ text-align: center;
+ opacity: 0.5;
+}
+
+.phabricator-filetree .filetree-comments-visible {
+ background: {$lightblue};
+ opacity: 0.75;
+ color: {$darkgreytext};
+}
+
+.phabricator-filetree .filetree-comments-completed {
+ background: {$darkgreybackground};
+ color: {$greytext};
+}
diff --git a/webroot/rsrc/css/layout/phabricator-source-code-view.css b/webroot/rsrc/css/layout/phabricator-source-code-view.css
index a855e0144..1836d321a 100644
--- a/webroot/rsrc/css/layout/phabricator-source-code-view.css
+++ b/webroot/rsrc/css/layout/phabricator-source-code-view.css
@@ -1,74 +1,71 @@
/**
* @provides phabricator-source-code-view-css
*/
.phabricator-source-code-container {
overflow-x: auto;
overflow-y: hidden;
border: 1px solid {$paste.border};
border-radius: 3px;
background-color: {$paste.content};
}
.phui-oi .phabricator-source-code-container {
margin-left: 8px;
}
-.phabricator-source-code-view tr:first-child * {
- padding-top: 8px;
-}
-
-.phabricator-source-code-view tr:last-child * {
- padding-bottom: 8px;
-}
-
.phabricator-source-code {
white-space: pre-wrap;
padding: 2px 8px 1px;
width: 100%;
}
.phabricator-source-line {
background-color: {$paste.highlight};
text-align: right;
- padding: 2px 6px 1px 12px;
border-right: 1px solid {$paste.border};
color: {$sh-yellowtext};
/* When the user selects rows of source, don't visibly select the line
numbers beside them. We use JS to strip the line numbers out when the user
copies the text. */
-moz-user-select: -moz-none;
-khtml-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
white-space: nowrap;
}
+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:hover {
+th.phabricator-source-line a:hover {
background: {$paste.border};
- cursor: pointer;
+ text-decoration: none;
}
.phabricator-source-highlight {
background: {$paste.highlight};
}
.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;
}
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 ff79a8d70..9436d171d 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,689 +1,718 @@
/**
* @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};
+}
+
+.differential-revision-large {
+ background: {$sh-redbackground};
+}
+
+.differential-revision-large .phui-icon-view {
+ color: {$red};
+}
+
+.differential-revision-small {
+ background: {$sh-greenbackground};
+}
+
+.differential-revision-small .phui-icon-view {
+ color: {$green};
+}
diff --git a/webroot/rsrc/css/phui/phui-action-list.css b/webroot/rsrc/css/phui/phui-action-list.css
index 5e32a1ea0..e7ee38a8b 100644
--- a/webroot/rsrc/css/phui/phui-action-list.css
+++ b/webroot/rsrc/css/phui/phui-action-list.css
@@ -1,193 +1,198 @@
/**
* @provides phabricator-action-list-view-css
*/
.device .phabricator-action-list-view {
padding: 4px 0;
display: none;
}
!print .phabricator-action-list-view {
padding: 4px 0;
display: none;
}
.device .phuix-dropdown-menu .phabricator-action-list-view {
/* When an action list view appears inside a dropdown menu, don't hide it
by default. */
display: block;
padding: 0;
}
.device .phabricator-action-list-view.phabricator-action-list-toggle,
.device-desktop .phui-document-content
.phabricator-action-list-view.phabricator-action-list-toggle {
display: block;
width: 200px;
border: 1px solid {$lightgreyborder};
border-radius: 3px;
position: absolute;
right: 16px;
top: 42px;
background: #fff;
box-shadow: {$dropshadow};
padding: 4px 0;
}
.device-phone .phabricator-action-list-view.phabricator-action-list-toggle {
right: 8px;
top: 38px;
}
.phabricator-action-view {
position: relative;
}
.phabricator-action-view button.phabricator-action-view-item {
border: none;
background: transparent;
box-shadow: none;
outline: 0;
padding: 0;
margin: 0;
font-weight: normal;
width: 100%;
text-align: left;
text-shadow: none;
border-radius: 0;
color: {$anchor};
font: inherit;
display: inline;
min-width: 0;
}
.phabricator-action-view button.phabricator-action-view-item .phui-icon-view {
color: {$darkbluetext};
}
.phabricator-action-view button.phabricator-action-view-item,
.phabricator-action-view-item {
padding: 4px 8px 6px 8px;
display: block;
text-decoration: none;
color: {$darkbluetext};
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.action-has-icon button.phabricator-action-view-item,
.action-has-icon .phabricator-action-view-item {
padding: 4px 4px 4px 28px;
}
.device-desktop .phabricator-action-view-href:hover
.phabricator-action-view-item {
text-decoration: none;
background: rgba({$alphablue}, .08);
color: {$sky};
border-radius: 3px;
}
.device-desktop .phabricator-action-view-href:hover
.phabricator-action-view-icon {
color: {$sky};
}
-.device-desktop .phabricator-action-view-href.action-item-red:hover
- .phabricator-action-view-item {
- background-color: {$sh-redbackground};
- color: {$sh-redtext};
+.phabricator-action-view.action-item-red {
+ background-color: {$sh-redbackground};
+}
+
+.phabricator-action-view.action-item-red .phabricator-action-view-item,
+.phabricator-action-view.action-item-red .phabricator-action-view-icon {
+ color: {$sh-redtext};
}
-.device-desktop .phabricator-action-view-href.action-item-red:hover
+.device-desktop .phabricator-action-view.action-item-red:hover
+ .phabricator-action-view-item,
+.device-desktop .phabricator-action-view.action-item-red:hover
.phabricator-action-view-icon {
- color: {$red};
+ color: {$red};
}
.phabricator-action-view-label .phabricator-action-view-item,
.phabricator-action-view-type-label .phabricator-action-view-item {
font-size: {$smallerfontsize};
font-weight: bold;
color: {$bluetext};
padding: 4px 8px 6px 8px;
display: block;
text-transform: uppercase;
-webkit-font-smoothing: antialiased;
}
.phabricator-action-view + .phabricator-action-view-label {
padding-top: 8px;
}
.phabricator-action-view-icon {
width: 14px;
height: 14px;
position: absolute;
top: 7px;
left: 8px;
text-align: center;
}
.phabricator-action-view-disabled .phabricator-action-view-item,
.phabricator-action-view-disabled button.phabricator-action-view-item {
color: {$lightgreytext};
}
.phabricator-action-view-selected {
background: {$sh-violetbackground};
border-radius: 3px;
}
.phabricator-action-view-selected:hover a {
text-decoration: none;
}
.phabricator-action-view button[disabled] {
opacity: 1.0;
}
.device-desktop .phabricator-action-view-disabled:hover
.phabricator-action-view-item,
.device-desktop .phabricator-action-view-disabled:hover
button.phabricator-action-view-item,
.device-desktop .phabricator-action-view-disabled:hover
.phabricator-action-view-icon,
.device-desktop .phabricator-action-view-disabled:hover
button.phabricator-action-view-icon {
color: {$lightgreytext};
}
.phabricator-action-view-type-divider {
margin-top: 8px;
border-top: 1px solid {$thinblueborder};
}
/******* Sub Menu *************************************************************/
.phabricator-action-view-submenu .caret-right {
position: absolute;
top: 8px;
right: 8px;
border-left-color: {$alphablue};
}
.phabricator-action-view-submenu .caret {
position: absolute;
top: 10px;
right: 8px;
border-top: 7px solid {$lightgreytext};
}
.phabricator-action-list-view .phabricator-action-view-submenu.phui-submenu-open
.phabricator-action-view-item {
background-color: rgba({$alphablue}, 0.07);
color: {$sky};
border-radius: 3px;
}
.phabricator-action-list-view .phabricator-action-view-submenu.phui-submenu-open
.phabricator-action-view-item .phui-icon-view {
color: {$sky};
}
diff --git a/webroot/rsrc/css/phui/phui-icon.css b/webroot/rsrc/css/phui/phui-icon.css
index acc781876..4108074b0 100644
--- a/webroot/rsrc/css/phui/phui-icon.css
+++ b/webroot/rsrc/css/phui/phui-icon.css
@@ -1,181 +1,185 @@
/**
* @provides phui-icon-view-css
*/
.phui-icon-example .phui-icon-view {
display: inline-block;
vertical-align: top;
}
.phui-icon-view.sprite-tokens {
height: 18px;
width: 18px;
display: inline-block;
vertical-align: top;
}
.phui-icon-view.sprite-login {
height: 28px;
width: 28px;
}
.phui-icon-view.phuihead-medium {
height: 50px;
width: 50px;
}
.phui-icon-view.phuihead-small {
height: 35px;
width: 35px;
background-size: 35px;
}
.phui-icon-has-text:before {
margin-right: 6px;
}
a.phui-icon-view:hover {
text-decoration: none;
color: {$sky};
}
img.phui-image-disabled {
opacity: .8;
-webkit-filter: grayscale(100%);
filter: grayscale(100%);
}
.phui-icon-view.bluetext {
color: {$bluetext};
}
+.phui-icon-view.invisible {
+ visibility: hidden;
+}
+
/* - Icon in a Circle ------------------------------------------------------- */
.phui-icon-circle {
border: 1px solid {$lightblueborder};
border-radius: 24px;
height: 24px;
width: 24px;
text-align: center;
display: inline-block;
cursor: pointer;
background: transparent;
padding: 0;
position: relative;
}
.phui-icon-circle.circle-medium {
height: 36px;
width: 36px;
border-radius: 36px;
}
.phui-icon-circle.phui-icon-circle-state {
border-color: transparent;
background-color: {$bluebackground};
}
.phui-icon-circle.phui-icon-circle-state .phui-icon-circle-icon {
color: {$bluetext};
font-size: 16px;
}
a.phui-icon-circle.phui-icon-circle-state:hover {
border-color: transparent !important;
}
.phui-icon-circle .phui-icon-circle-icon {
height: 24px;
width: 24px;
font-size: 11px;
line-height: 24px;
color: {$lightblueborder};
cursor: pointer;
}
.phui-icon-circle.circle-medium .phui-icon-circle-icon {
font-size: 18px;
line-height: 36px;
}
a.phui-icon-circle.hover-sky:hover {
text-decoration: none;
border-color: {$sky};
cursor: pointer;
}
a.phui-icon-circle.hover-sky:hover .phui-icon-view {
color: {$sky};
}
a.phui-icon-circle.hover-violet:hover {
text-decoration: none;
border-color: {$violet};
cursor: pointer;
}
a.phui-icon-circle.hover-violet:hover .phui-icon-view {
color: {$violet};
}
a.phui-icon-circle.hover-pink:hover {
text-decoration: none;
border-color: {$pink};
cursor: pointer;
}
a.phui-icon-circle.hover-pink:hover .phui-icon-view {
color: {$pink};
}
a.phui-icon-circle.hover-green:hover {
text-decoration: none;
border-color: {$green};
cursor: pointer;
}
a.phui-icon-circle.hover-green:hover .phui-icon-view {
color: {$green};
}
a.phui-icon-circle.hover-red:hover {
text-decoration: none;
border-color: {$red};
cursor: pointer;
}
a.phui-icon-circle.hover-red:hover .phui-icon-view {
color: {$red};
}
.phui-icon-circle .phui-icon-view.phui-icon-circle-state-icon {
position: absolute;
width: 14px;
height: 14px;
display: inline-block;
font-size: 12px;
right: -3px;
top: -4px;
text-shadow:
-1px -1px 0 #fff,
1px -1px 0 #fff,
-1px 1px 0 #fff,
1px 1px 0 #fff;
}
/* - Icon in a Square ------------------------------------------------------- */
.phui-icon-view.phui-icon-square {
height: 40px;
width: 40px;
color: #fff;
font-size: 26px;
text-align: center;
line-height: 38px;
border-radius: 3px;
}
a.phui-icon-view.phui-icon-square:hover {
text-decoration: none;
color: #fff;
}
diff --git a/webroot/rsrc/css/phui/phui-property-list-view.css b/webroot/rsrc/css/phui/phui-property-list-view.css
index 16a78e219..15632c902 100644
--- a/webroot/rsrc/css/phui/phui-property-list-view.css
+++ b/webroot/rsrc/css/phui/phui-property-list-view.css
@@ -1,222 +1,327 @@
/**
* @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;
}
-.phui-property-list-image {
- margin: auto;
- max-width: 95%;
-}
-
-.phui-property-list-audio {
- display: block;
- margin: 16px auto;
- width: 50%;
- min-width: 240px;
-}
-
-.phui-property-list-video {
- display: block;
- margin: 0 auto;
- max-width: 95%;
-}
-
/* 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');
+}
+
+.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/css/phui/phui-timeline-view.css b/webroot/rsrc/css/phui/phui-timeline-view.css
index b0ae9c004..6fae6802b 100644
--- a/webroot/rsrc/css/phui/phui-timeline-view.css
+++ b/webroot/rsrc/css/phui/phui-timeline-view.css
@@ -1,441 +1,437 @@
/**
* @provides phui-timeline-view-css
*/
.phui-timeline-view {
padding: 0 16px;
background-image: url('/rsrc/image/d5d8e1.png');
background-repeat: repeat-y;
background-position: 96px;
}
.device .phui-timeline-view {
padding: 0 8px;
}
.device-tablet .phui-timeline-view {
background-position: 24px;
}
.device-phone .phui-timeline-view {
padding: 0;
background-position: 24px;
}
.phui-timeline-major-event .phui-timeline-group {
border-left: 1px solid {$timeline};
border-right: 1px solid {$timeline};
border-radius: 3px;
}
.device-desktop .phui-timeline-event-view {
margin-left: 62px;
position: relative;
}
.device-desktop .phui-timeline-event-view.phui-timeline-minor-event {
margin-left: 67px;
}
.device-desktop .phui-timeline-spacer {
min-height: 16px;
}
.device-desktop .phui-timeline-event-view.the-worlds-end {
background: {$lightblueborder};
width: 9px;
height: 9px;
border-radius: 2px;
margin-left: 76px;
margin-bottom: 20px;
}
.device-desktop .phui-timeline-wedge {
border-bottom: 1px solid {$timeline};
position: absolute;
width: 12px;
}
.device-phone .phui-timeline-minor-event,
.device-tablet .phui-timeline-minor-event {
padding-left: 3px;
}
.phui-timeline-major-event .phui-timeline-content {
border-top: 1px solid {$timeline};
border-bottom: 1px solid {$timeline};
border-radius: 3px;
}
.phui-timeline-title {
line-height: 24px;
min-height: 19px;
position: relative;
color: {$greytext};
}
.phui-timeline-minor-event .phui-timeline-title {
padding: 1px 8px 4px 33px;
}
.phui-timeline-title a {
font-weight: bold;
color: {$darkgreytext};
}
.device-desktop .phui-timeline-wedge {
left: -12px;
}
.device-desktop .phui-timeline-major-event .phui-timeline-wedge {
top: 26px;
}
.device-desktop .phui-timeline-minor-event .phui-timeline-wedge {
top: 12px;
left: -18px;
width: 20px;
}
.phui-timeline-image {
background-repeat: no-repeat;
position: absolute;
border-radius: 3px;
background-size: 100%;
display: block;
}
.device-desktop .phui-timeline-major-event .phui-timeline-image {
width: 50px;
height: 50px;
top: 0px;
left: -62px;
}
.device-desktop .phui-timeline-minor-event .phui-timeline-image {
width: 26px;
height: 26px;
background-size: 26px auto;
left: -41px;
}
.phui-timeline-major-event .phui-timeline-title {
background: {$lightgreybackground};
min-height: 22px;
border-top-right-radius: 3px;
border-top-left-radius: 3px;
}
.phui-timeline-major-event .phui-timeline-title + .phui-timeline-title {
border-radius: 0;
padding-top: 0;
}
.phui-timeline-major-event .phui-timeline-title + .phui-timeline-title
.phui-timeline-icon-fill {
margin-top: 0;
}
.phui-timeline-title {
padding: 5px 8px;
overflow-x: auto;
overflow-y: hidden;
}
.phui-timeline-title-with-icon {
padding-left: 36px;
}
.phui-timeline-title-with-menu {
padding-right: 36px;
}
.phui-timeline-view .phui-icon-view.phui-timeline-token {
vertical-align: middle;
margin-right: 4px;
}
.phui-timeline-token.strikethrough {
position: relative;
}
.phui-timeline-token.strikethrough:before {
position: absolute;
content: "";
left: 0;
top: 50%;
right: 0;
border-top: 3px solid;
border-color: {$darkbluetext};
-webkit-transform:rotate(-40deg);
-moz-transform:rotate(-40deg);
-ms-transform:rotate(-40deg);
-o-transform:rotate(-40deg);
transform:rotate(-40deg);
}
.phui-timeline-major-event .phui-timeline-content
.phui-timeline-core-content {
padding: 16px;
line-height: 18px;
background: {$page.content};
border-top: 1px solid rgba({$alphablue},.1);
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
}
.phui-timeline-core-content {
overflow-x: auto;
}
.phui-timeline-core-content .comment-deleted {
font-style: italic;
}
.device .phui-timeline-event-view {
min-height: 23px;
position: relative;
}
.device-phone .phui-timeline-event-view {
margin: 0 8px;
}
.device .phui-timeline-image {
display: none;
}
.device .phui-timeline-spacer {
min-height: 8px;
border-width: 0;
}
.phui-timeline-icon-fill {
position: absolute;
width: 34px;
height: 34px;
top: 0;
left: 0;
text-align: center;
}
.phui-timeline-icon {
color: {$bluetext};
}
.phui-icon-view.phui-timeline-icon {
font-size: 14px;
}
.phui-timeline-icon-fill {
height: 26px;
width: 26px;
border-radius: 3px;
background-color: {$timeline.icon.background};
}
.phui-timeline-major-event .phui-timeline-icon-fill {
margin: 4px;
}
.phui-timeline-icon-fill .phui-timeline-icon {
margin-top: 6px;
}
.phui-timeline-extra,
.phui-timeline-extra .phabricator-content-source-view {
font-size: {$smallerfontsize};
font-weight: normal;
color: {$lightgreytext};
margin-left: 8px;
}
.phui-timeline-title .phui-timeline-extra a {
font-weight: normal;
color: {$lightgreytext};
}
.phui-timeline-title .phui-timeline-value,
.conpherence-transaction-content .phui-timeline-value,
.phui-feed-story-head .phui-timeline-value {
font-style: italic;
color: {$blacktext};
}
.device-desktop .phui-timeline-extra {
float: right;
}
.device .phui-timeline-extra {
display: inline-block;
line-height: 16px;
margin-left: 8px;
white-space: nowrap;
}
.device-phone .phui-timeline-extra {
display: block;
margin: 0;
}
.phui-timeline-icon-fill.fill-has-color .phui-icon-view {
color: {$page.content};
}
.phui-timeline-icon-fill-red {
background-color: {$red};
}
.phui-timeline-icon-fill-orange {
background-color: {$orange};
}
.phui-timeline-icon-fill-yellow {
background-color: {$yellow};
}
.phui-timeline-icon-fill-green {
background-color: {$green};
}
.phui-timeline-icon-fill-sky {
background-color: {$sky};
}
.phui-timeline-icon-fill-blue {
background-color: {$blue};
}
.phui-timeline-icon-fill-indigo {
background-color: {$indigo};
}
.phui-timeline-icon-fill-violet {
background-color: {$violet};
}
.phui-timeline-icon-fill-pink {
background-color: {$pink};
}
.phui-timeline-icon-fill-grey {
background-color: #888;
}
.phui-timeline-icon-fill-black {
background-color: #000;
}
.phui-timeline-shell.anchor-target {
background: {$lightyellow};
padding: 4px;
margin: -4px;
}
.phui-timeline-preview-header {
background: #e0e3ec;
color: {$darkgreytext};
padding: 4px 1.25%;
border: solid {$blueborder} 1px 0;
}
.phui-timeline-change-details {
padding: 10px 0;
border-style: solid;
border-color: #efefef;
border-width: 1px 0;
}
.phui-timeline-older-transactions-are-hidden {
background: {$gentle.highlight};
border: 1px solid {$gentle.highlight.border};
text-align: center;
padding: 12px;
color: {$darkgreytext};
cursor: pointer;
border-radius: 3px;
}
.device-phone .phui-timeline-older-transactions-are-hidden {
margin: 0 8px;
}
.phui-timeline-title .phui-timeline-extra-information a {
font-weight: normal;
color: {$greytext};
}
.phui-timeline-comment-actions .phui-icon-view {
width: 16px;
height: 16px;
font-size: 16px;
text-align: center;
overflow: hidden;
}
.phui-timeline-menu {
position: absolute;
right: 3px;
top: 6px;
width: 28px;
height: 24px;
text-align: center;
line-height: 22px;
font-size: 16px;
border-left: 1px solid {$thinblueborder};
}
.phui-timeline-menu:focus {
outline: none;
}
-.phui-timeline-menu .phui-icon-view {
- color: {$lightgreytext};
-}
-
a.phui-timeline-menu .phui-icon-view {
color: {$bluetext};
}
.device-desktop a.phui-timeline-menu:hover .phui-icon-view {
color: {$sky};
}
.phui-timeline-menu.phuix-dropdown-open {
background: rgba({$alphablue},0.1);
border: none;
border-radius: 3px;
}
.phui-timeline-view + .phui-object-box {
margin-top: 0;
}
.phui-timeline-badges {
position: absolute;
left: -64px;
top: 52px;
width: 54px;
text-align: center;
}
.phui-timeline-badges .phui-badge-mini {
height: 18px;
width: 18px;
line-height: 16px;
}
.phui-timeline-badges .phui-badge-mini .phui-icon-view {
font-size: 10px;
}
.phui-comment-preview-view {
margin-bottom: 20px;
}
.phui-timeline-view .phui-pinboard-view {
margin: 8px 0 0 0;
padding: 0;
text-align: left;
}
diff --git a/webroot/rsrc/externals/javelin/core/Stratcom.js b/webroot/rsrc/externals/javelin/core/Stratcom.js
index 4bfb11d4f..9ad2b8bbd 100644
--- a/webroot/rsrc/externals/javelin/core/Stratcom.js
+++ b/webroot/rsrc/externals/javelin/core/Stratcom.js
@@ -1,687 +1,716 @@
/**
* @requires javelin-install javelin-event javelin-util javelin-magical-init
* @provides javelin-stratcom
* @javelin
*/
/**
* Javelin strategic command, the master event delegation core. This class is
* a sort of hybrid between Arbiter and traditional event delegation, and
* serves to route event information to handlers in a general way.
*
* Each Javelin :JX.Event has a 'type', which may be a normal Javascript type
* (for instance, a click or a keypress) or an application-defined type. It
* also has a "path", based on the path in the DOM from the root node to the
* event target. Note that, while the type is required, the path may be empty
* (it often will be for application-defined events which do not originate
* from the DOM).
*
* The path is determined by walking down the tree to the event target and
* looking for nodes that have been tagged with metadata. These names are used
* to build the event path, and unnamed nodes are ignored. Each named node may
* also have data attached to it.
*
* Listeners specify one or more event types they are interested in handling,
* and, optionally, one or more paths. A listener will only receive events
* which occurred on paths it is listening to. See listen() for more details.
*
* @task invoke Invoking Events
* @task listen Listening to Events
* @task handle Responding to Events
* @task sigil Managing Sigils
* @task meta Managing Metadata
* @task internal Internals
*/
JX.install('Stratcom', {
statics : {
ready : false,
_targets : {},
_handlers : [],
_need : {},
_auto : '*',
_data : {},
_execContext : [],
_touchState: null,
/**
* Node metadata is stored in a series of blocks to prevent collisions
* between indexes that are generated on the server side (and potentially
* concurrently). Block 0 is for metadata on the initial page load, block 1
* is for metadata added at runtime with JX.Stratcom.siglize(), and blocks
* 2 and up are for metadata generated from other sources (e.g. JX.Request).
* Use allocateMetadataBlock() to reserve a block, and mergeData() to fill
* a block with data.
*
* When a JX.Request is sent, a block is allocated for it and any metadata
* it returns is filled into that block.
*/
_dataBlock : 2,
/**
* Within each datablock, data is identified by a unique index. The data
* pointer (data-meta attribute) on a node looks like this:
*
* 1_2
*
* ...where 1 is the block, and 2 is the index within that block. Normally,
* blocks are filled on the server side, so index allocation takes place
* there. However, when data is provided with JX.Stratcom.addData(), we
* need to allocate indexes on the client.
*/
_dataIndex : 0,
/**
* Dispatch a simple event that does not have a corresponding native event
* object. It is unusual to call this directly. Generally, you will instead
* dispatch events from an object using the invoke() method present on all
* objects. See @{JX.Base.invoke()} for documentation.
*
* @param string Event type.
* @param string|list? Optionally, a sigil path to attach to the event.
* This is rarely meaningful for simple events.
* @param object? Optionally, arbitrary data to send with the event.
* @return @{JX.Event} The event object which was dispatched to listeners.
* The main use of this is to test whether any
* listeners prevented the event.
* @task invoke
*/
invoke : function(type, path, data) {
if (__DEV__) {
if (path && typeof path !== 'string' && !JX.isArray(path)) {
throw new Error(
'JX.Stratcom.invoke(...): path must be a string or an array.');
}
}
path = JX.$AX(path);
return this._dispatchProxy(
new JX.Event()
.setType(type)
.setData(data || {})
.setPath(path || [])
);
},
/**
* Listen for events on given paths. Specify one or more event types, and
* zero or more paths to filter on. If you don't specify a path, you will
* receive all events of the given type:
*
* // Listen to all clicks.
* JX.Stratcom.listen('click', null, handler);
*
* This will notify you of all clicks anywhere in the document (unless
* they are intercepted and killed by a higher priority handler before they
* get to you).
*
* Often, you may be interested in only clicks on certain elements. You
* can specify the paths you're interested in to filter out events which
* you do not want to be notified of.
*
* // Listen to all clicks inside elements annotated "news-feed".
* JX.Stratcom.listen('click', 'news-feed', handler);
*
* By adding more elements to the path, you can create a finer-tuned
* filter:
*
* // Listen to only "like" clicks inside "news-feed".
* JX.Stratcom.listen('click', ['news-feed', 'like'], handler);
*
*
* TODO: Further explain these shenanigans.
*
* @param string|list<string> Event type (or list of event names) to
* listen for. For example, ##'click'## or
* ##['keydown', 'keyup']##.
*
* @param wild Sigil paths to listen for this event on. See discussion
* in method documentation.
*
* @param function Callback to invoke when this event is triggered. It
* should have the signature ##f(:JX.Event e)##.
*
* @return object A reference to the installed listener. You can later
* remove the listener by calling this object's remove()
* method.
* @task listen
*/
listen : function(types, paths, func) {
if (__DEV__) {
if (arguments.length != 3) {
JX.$E(
'JX.Stratcom.listen(...): '+
'requires exactly 3 arguments. Did you mean JX.DOM.listen?');
}
if (typeof func != 'function') {
JX.$E(
'JX.Stratcom.listen(...): '+
'callback is not a function.');
}
}
var ids = [];
types = JX.$AX(types);
if (!paths) {
paths = this._auto;
}
if (!JX.isArray(paths)) {
paths = [[paths]];
} else if (!JX.isArray(paths[0])) {
paths = [paths];
}
var listener = { _callback : func };
// To listen to multiple event types on multiple paths, we just install
// the same listener a whole bunch of times: if we install for two
// event types on three paths, we'll end up with six references to the
// listener.
//
// TODO: we'll call your listener twice if you install on two paths where
// one path is a subset of another. The solution is "don't do that", but
// it would be nice to verify that the caller isn't doing so, in __DEV__.
for (var ii = 0; ii < types.length; ++ii) {
var type = types[ii];
if (('onpagehide' in window) && type == 'unload') {
// If we use "unload", we break the bfcache ("Back-Forward Cache") in
// Safari and Firefox. The BFCache makes using the back/forward
// buttons really fast since the pages can come out of magical
// fairyland instead of over the network, so use "pagehide" as a proxy
// for "unload" in these browsers.
type = 'pagehide';
}
if (!(type in this._targets)) {
this._targets[type] = {};
}
var type_target = this._targets[type];
for (var jj = 0; jj < paths.length; ++jj) {
var path = paths[jj];
var id = this._handlers.length;
this._handlers.push(listener);
this._need[id] = path.length;
ids.push(id);
for (var kk = 0; kk < path.length; ++kk) {
if (__DEV__) {
if (path[kk] == 'tag:#document') {
JX.$E(
'JX.Stratcom.listen(..., "tag:#document", ...): ' +
'listen for all events using null, not "tag:#document"');
}
if (path[kk] == 'tag:window') {
JX.$E(
'JX.Stratcom.listen(..., "tag:window", ...): ' +
'listen for window events using null, not "tag:window"');
}
}
(type_target[path[kk]] || (type_target[path[kk]] = [])).push(id);
}
}
}
// Add a remove function to the listener
listener.remove = function() {
if (listener._callback) {
delete listener._callback;
for (var ii = 0; ii < ids.length; ii++) {
delete JX.Stratcom._handlers[ids[ii]];
}
}
};
return listener;
},
/**
* Sometimes you may be interested in removing a listener directly from it's
* handler. This is possible by calling JX.Stratcom.removeCurrentListener()
*
* // Listen to only the first click on the page
* JX.Stratcom.listen('click', null, function() {
* // do interesting things
* JX.Stratcom.removeCurrentListener();
* });
*
* @task remove
*/
removeCurrentListener : function() {
var context = this._execContext[this._execContext.length - 1];
var listeners = context.listeners;
// JX.Stratcom.pass will have incremented cursor by now
var cursor = context.cursor - 1;
if (listeners[cursor]) {
listeners[cursor].handler.remove();
}
},
/**
* Dispatch a native Javascript event through the Stratcom control flow.
* Generally, this is automatically called for you by the master dispatcher
* installed by ##init.js##. When you want to dispatch an application event,
* you should instead call invoke().
*
* @param Event Native event for dispatch.
* @return :JX.Event Dispatched :JX.Event.
* @task internal
*/
dispatch : function(event) {
var path = [];
var nodes = {};
var distances = {};
var push = function(key, node, distance) {
// we explicitly only store the first occurrence of each key
if (!nodes.hasOwnProperty(key)) {
nodes[key] = node;
distances[key] = distance;
path.push(key);
}
};
var target = event.srcElement || event.target;
// Touch events may originate from text nodes, but we want to start our
// traversal from the nearest Element, so we grab the parentNode instead.
if (target && target.nodeType === 3) {
target = target.parentNode;
}
// Since you can only listen by tag, id, or sigil we unset the target if
// it isn't an Element. Document and window are Nodes but not Elements.
if (!target || !target.getAttribute) {
target = null;
}
var distance = 1;
var cursor = target;
while (cursor && cursor.getAttribute) {
push('tag:' + cursor.nodeName.toLowerCase(), cursor, distance);
var id = cursor.id;
if (id) {
push('id:' + id, cursor, distance);
}
var sigils = cursor.getAttribute('data-sigil');
if (sigils) {
sigils = sigils.split(' ');
for (var ii = 0; ii < sigils.length; ii++) {
push(sigils[ii], cursor, distance);
}
}
var auto_id = cursor.getAttribute('data-autoid');
if (auto_id) {
push('autoid:' + auto_id, cursor, distance);
}
++distance;
cursor = cursor.parentNode;
}
var etype = event.type;
if (etype == 'focusin') {
etype = 'focus';
} else if (etype == 'focusout') {
etype = 'blur';
}
// Map of touch events and whether they are unique per touch.
var touch_map = {
touchstart: true,
touchend: true,
mousedown: true,
mouseup: true,
click: true,
// These can conceivably fire multiple times per touch, so if we see
// them more than once that doesn't tell us that we're handling a new
// event.
mouseover: false,
mouseout: false,
mousemove: false,
touchmove: false
};
// If this is a 'touchstart', we're handling touch events.
if (etype == 'touchstart') {
this._touchState = {};
}
// If this is a unique touch event that we haven't seen before, remember
// it as part of the current touch state. On the other hand, if we've
// already seen it, we can deduce that we must be handling a new cursor
// event that is unrelated to the last touch we saw, and conclude that
// we are no longer processing a touch event.
if (touch_map[etype] && this._touchState) {
if (!this._touchState[etype]) {
this._touchState[etype] = true;
} else {
this._touchState = null;
}
}
// This event is a touch event if we're carrying some touch state.
var is_touch = (etype in touch_map) &&
(this._touchState !== null);
var proxy = new JX.Event()
.setRawEvent(event)
.setData(event.customData)
.setType(etype)
.setTarget(target)
.setNodes(nodes)
.setNodeDistances(distances)
.setIsTouchEvent(is_touch)
.setPath(path.reverse());
// You can uncomment this to print out all events flowing through
// Stratcom, which tends to make debugging easier.
//JX.log('~> '+proxy.toString());
return this._dispatchProxy(proxy);
},
/**
* Dispatch a previously constructed proxy :JX.Event.
*
* @param :JX.Event Event to dispatch.
* @return :JX.Event Returns the event argument.
* @task internal
*/
_dispatchProxy : function(proxy) {
var scope = this._targets[proxy.getType()];
if (!scope) {
return proxy;
}
var path = proxy.getPath();
var distances = proxy.getNodeDistances();
var len = path.length;
var hits = {};
var hit_distances = {};
var matches;
// A large number (larger than any distance we will ever encounter), but
// we need to do math on it in the sort function so we can't use
// Number.POSITIVE_INFINITY.
var far_away = 1000000;
for (var root = -1; root < len; ++root) {
matches = scope[(root == -1) ? this._auto : path[root]];
if (matches) {
var distance = distances[path[root]] || far_away;
for (var ii = 0; ii < matches.length; ++ii) {
var match = matches[ii];
hits[match] = (hits[match] || 0) + 1;
hit_distances[match] = Math.min(
hit_distances[match] || distance,
distance
);
}
}
}
var listeners = [];
for (var k in hits) {
if (hits[k] == this._need[k]) {
var handler = this._handlers[k];
if (handler) {
listeners.push({
distance: hit_distances[k],
handler: handler
});
}
}
}
// Sort listeners by matched sigil closest to the target node
// Listeners with the same closest sigil are called in an undefined order
listeners.sort(function(a, b) {
if (__DEV__) {
// Make sure people play by the rules. >:)
return (a.distance - b.distance) || (Math.random() - 0.5);
}
return a.distance - b.distance;
});
this._execContext.push({
listeners: listeners,
event: proxy,
cursor: 0
});
this.pass();
this._execContext.pop();
return proxy;
},
/**
* Pass on an event, allowing other handlers to process it. The use case
* here is generally something like:
*
* if (JX.Stratcom.pass()) {
* // something else handled the event
* return;
* }
* // handle the event
* event.prevent();
*
* This allows you to install event handlers that operate at a lower
* effective priority, and provide a default behavior which is overridable
* by listeners.
*
* @return bool True if the event was stopped or prevented by another
* handler.
* @task handle
*/
pass : function() {
var context = this._execContext[this._execContext.length - 1];
var event = context.event;
var listeners = context.listeners;
while (context.cursor < listeners.length) {
var cursor = context.cursor++;
if (listeners[cursor]) {
var handler = listeners[cursor].handler;
handler._callback && handler._callback(event);
}
if (event.getStopped()) {
break;
}
}
return event.getStopped() || event.getPrevented();
},
/**
* Retrieve the event (if any) which is currently being dispatched.
*
* @return :JX.Event|null Event which is currently being dispatched, or
* null if there is no active dispatch.
* @task handle
*/
context : function() {
var len = this._execContext.length;
return len ? this._execContext[len - 1].event : null;
},
+ initialize: function(initializers) {
+ var frameable = false;
+
+ for (var ii = 0; ii < initializers.length; ii++) {
+ var kind = initializers[ii].kind;
+ var data = initializers[ii].data;
+ switch (kind) {
+ case 'behaviors':
+ JX.initBehaviors(data);
+ break;
+ case 'merge':
+ JX.Stratcom.mergeData(data.block, data.data);
+ JX.Stratcom.ready = true;
+ break;
+ case 'frameable':
+ frameable = !!data;
+ break;
+ }
+ }
+
+ // If the initializer tags did not explicitly allow framing, framebust.
+ // This protects us from clickjacking attacks on older versions of IE.
+ // The "X-Frame-Options" and "Content-Security-Policy" headers provide
+ // more modern variations of this protection.
+ if (!frameable) {
+ if (window.top != window.self) {
+ window.top.location.replace(window.self.location.href);
+ }
+ }
+ },
/**
* Merge metadata. You must call this (even if you have no metadata) to
* start the Stratcom queue.
*
* @param int The datablock to merge data into.
* @param dict Dictionary of metadata.
* @return void
* @task internal
*/
mergeData : function(block, data) {
if (this._data[block]) {
if (__DEV__) {
for (var key in data) {
if (key in this._data[block]) {
JX.$E(
'JX.Stratcom.mergeData(' + block + ', ...); is overwriting ' +
'existing data.');
}
}
}
JX.copy(this._data[block], data);
} else {
this._data[block] = data;
if (block === 0) {
- JX.Stratcom.ready = true;
JX.flushHoldingQueue('install-init', function(fn) {
fn();
});
JX.__rawEventQueue({type: 'start-queue'});
}
}
},
/**
* Determine if a node has a specific sigil.
*
* @param Node Node to test.
* @param string Sigil to check for.
* @return bool True if the node has the sigil.
*
* @task sigil
*/
hasSigil : function(node, sigil) {
if (__DEV__) {
if (!node || !node.getAttribute) {
JX.$E(
'JX.Stratcom.hasSigil(<non-element>, ...): ' +
'node is not an element. Most likely, you\'re passing window or ' +
'document, which are not elements and can\'t have sigils.');
}
}
var sigils = node.getAttribute('data-sigil') || false;
return sigils && (' ' + sigils + ' ').indexOf(' ' + sigil + ' ') > -1;
},
/**
* Add a sigil to a node.
*
* @param Node Node to add the sigil to.
* @param string Sigil to name the node with.
* @return void
* @task sigil
*/
addSigil: function(node, sigil) {
if (__DEV__) {
if (!node || !node.getAttribute) {
JX.$E(
'JX.Stratcom.addSigil(<non-element>, ...): ' +
'node is not an element. Most likely, you\'re passing window or ' +
'document, which are not elements and can\'t have sigils.');
}
}
var sigils = node.getAttribute('data-sigil') || '';
if (!JX.Stratcom.hasSigil(node, sigil)) {
sigils += ' ' + sigil;
}
node.setAttribute('data-sigil', sigils);
},
/**
* Retrieve a node's metadata.
*
* @param Node Node from which to retrieve data.
* @return object Data attached to the node. If no data has been attached
* to the node yet, an empty object will be returned, but
* subsequent calls to this method will always retrieve the
* same object.
* @task meta
*/
getData : function(node) {
if (__DEV__) {
if (!node || !node.getAttribute) {
JX.$E(
'JX.Stratcom.getData(<non-element>): ' +
'node is not an element. Most likely, you\'re passing window or ' +
'document, which are not elements and can\'t have data.');
}
}
var meta_id = (node.getAttribute('data-meta') || '').split('_');
if (meta_id[0] && meta_id[1]) {
var block = this._data[meta_id[0]];
var index = meta_id[1];
if (block && (index in block)) {
return block[index];
} else if (__DEV__) {
JX.$E(
'JX.Stratcom.getData(<node>): Tried to access data (block ' +
meta_id[0] + ', index ' + index + ') that was not present. This ' +
'probably means you are calling getData() before the block ' +
'is provided by mergeData().');
}
}
var data = {};
if (!this._data[1]) { // data block 1 is reserved for JavaScript
this._data[1] = {};
}
this._data[1][this._dataIndex] = data;
node.setAttribute('data-meta', '1_' + (this._dataIndex++));
return data;
},
/**
* Add data to a node's metadata.
*
* @param Node Node which data should be attached to.
* @param object Data to add to the node's metadata.
* @return object Data attached to the node that is returned by
* JX.Stratcom.getData().
* @task meta
*/
addData : function(node, data) {
if (__DEV__) {
if (!node || !node.getAttribute) {
JX.$E(
'JX.Stratcom.addData(<non-element>, ...): ' +
'node is not an element. Most likely, you\'re passing window or ' +
'document, which are not elements and can\'t have sigils.');
}
if (!data || typeof data != 'object') {
JX.$E(
'JX.Stratcom.addData(..., <nonobject>): ' +
'data to attach to node is not an object. You must use ' +
'objects, not primitives, for metadata.');
}
}
return JX.copy(JX.Stratcom.getData(node), data);
},
/**
* @task internal
*/
allocateMetadataBlock : function() {
return this._dataBlock++;
}
}
});
diff --git a/webroot/rsrc/externals/javelin/core/init.js b/webroot/rsrc/externals/javelin/core/init.js
index 88a9cf137..fb10b0f4c 100644
--- a/webroot/rsrc/externals/javelin/core/init.js
+++ b/webroot/rsrc/externals/javelin/core/init.js
@@ -1,237 +1,262 @@
/**
* Javelin core; installs Javelin and Stratcom event delegation.
*
* @provides javelin-magical-init
*
* @javelin-installs JX.__rawEventQueue
* @javelin-installs JX.__simulate
* @javelin-installs JX.__allowedEvents
* @javelin-installs JX.enableDispatch
* @javelin-installs JX.onload
* @javelin-installs JX.flushHoldingQueue
*
* @javelin
*/
(function() {
if (window.JX) {
return;
}
window.JX = {};
// The holding queues hold calls to functions (JX.install() and JX.behavior())
// before they load, so if you're async-loading them later in the document
// the page will execute correctly regardless of the order resources arrive
// in.
var holding_queues = {};
function makeHoldingQueue(name) {
if (JX[name]) {
return;
}
holding_queues[name] = [];
JX[name] = function() { holding_queues[name].push(arguments); };
}
JX.flushHoldingQueue = function(name, fn) {
for (var ii = 0; ii < holding_queues[name].length; ii++) {
fn.apply(null, holding_queues[name][ii]);
}
holding_queues[name] = {};
};
makeHoldingQueue('install');
makeHoldingQueue('behavior');
makeHoldingQueue('install-init');
- window.__DEV__ = window.__DEV__ || 0;
-
var loaded = false;
var onload = [];
var master_event_queue = [];
var root = document.documentElement;
var has_add_event_listener = !!root.addEventListener;
+ window.__DEV__ = !!root.getAttribute('data-developer-mode');
+
JX.__rawEventQueue = function(what) {
master_event_queue.push(what);
- // Evade static analysis - JX.Stratcom
+ var ii;
var Stratcom = JX['Stratcom'];
- if (Stratcom && Stratcom.ready) {
- // Empty the queue now so that exceptions don't cause us to repeatedly
- // try to handle events.
+
+ if (!loaded && what.type == 'domready') {
+ var initializers = [];
+
+ var tags = JX.DOM.scry(document.body, 'data');
+ for (ii = 0; ii < tags.length; ii++) {
+
+ // Ignore tags which are not immediate children of the document
+ // body. If an attacker somehow injects arbitrary tags into the
+ // content of the document, that should not give them access to
+ // modify initialization behaviors.
+ if (tags[ii].parentNode !== document.body) {
+ continue;
+ }
+
+ var tag_kind = tags[ii].getAttribute('data-javelin-init-kind');
+ var tag_data = tags[ii].getAttribute('data-javelin-init-data');
+ tag_data = JX.JSON.parse(tag_data);
+
+ initializers.push({kind: tag_kind, data: tag_data});
+ }
+
+ Stratcom.initialize(initializers);
+ loaded = true;
+ }
+
+ if (loaded) {
+ // Empty the queue now so that exceptions don't cause us to repeatedly
+ // try to handle events.
var local_queue = master_event_queue;
master_event_queue = [];
- for (var ii = 0; ii < local_queue.length; ++ii) {
+ for (ii = 0; ii < local_queue.length; ++ii) {
var evt = local_queue[ii];
// Sometimes IE gives us events which throw when ".type" is accessed;
// just ignore them since we can't meaningfully dispatch them. TODO:
// figure out where these are coming from.
try { var test = evt.type; } catch (x) { continue; }
- if (!loaded && evt.type == 'domready') {
+ if (evt.type == 'domready') {
// NOTE: Firefox interprets "document.body.id = null" as the string
// literal "null".
document.body && (document.body.id = '');
- loaded = true;
for (var jj = 0; jj < onload.length; jj++) {
onload[jj]();
}
}
Stratcom.dispatch(evt);
}
} else {
var target = what.srcElement || what.target;
if (target &&
(what.type in {click: 1, submit: 1}) &&
target.getAttribute &&
target.getAttribute('data-mustcapture') === '1') {
what.returnValue = false;
what.preventDefault && what.preventDefault();
document.body.id = 'event_capture';
// For versions of IE that use attachEvent, the event object is somehow
// stored globally by reference, and all the references we push to the
// master_event_queue will always refer to the most recent event. We
// work around this by popping the useless global event off the queue,
// and pushing a clone of the event that was just fired using the IE's
// proprietary createEventObject function.
// see: http://msdn.microsoft.com/en-us/library/ms536390(v=vs.85).aspx
if (!add_event_listener && document.createEventObject) {
master_event_queue.pop();
master_event_queue.push(document.createEventObject(what));
}
return false;
}
}
};
JX.enableDispatch = function(target, type) {
if (__DEV__) {
JX.__allowedEvents[type] = true;
}
if (target.addEventListener) {
target.addEventListener(type, JX.__rawEventQueue, true);
} else if (target.attachEvent) {
target.attachEvent('on' + type, JX.__rawEventQueue);
}
};
var document_events = [
'click',
'dblclick',
'change',
'submit',
'keypress',
'mousedown',
'mouseover',
'mouseout',
'keyup',
'keydown',
'input',
'drop',
'dragenter',
'dragleave',
'dragover',
'paste',
'touchstart',
'touchmove',
'touchend',
'touchcancel',
'load'
];
// Simulate focus and blur in old versions of IE using focusin and focusout
// TODO: Document the gigantic IE mess here with focus/blur.
// TODO: beforeactivate/beforedeactivate?
// http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html
if (!has_add_event_listener) {
document_events.push('focusin', 'focusout');
}
// Opera is multilol: it propagates focus / blur oddly
if (window.opera) {
document_events.push('focus', 'blur');
}
if (__DEV__) {
JX.__allowedEvents = {};
if ('onpagehide' in window) {
JX.__allowedEvents.unload = true;
}
}
var ii;
for (ii = 0; ii < document_events.length; ++ii) {
JX.enableDispatch(root, document_events[ii]);
}
// In particular, we're interested in capturing window focus/blur here so
// long polls can abort when the window is not focused.
var window_events = [
('onpagehide' in window) ? 'pagehide' : 'unload',
'resize',
'scroll',
'focus',
'blur',
'popstate',
'hashchange',
// In Firefox, if the user clicks in the window then drags the cursor
// outside of the window and releases the mouse button, we don't get this
// event unless we listen for it as a window event.
'mouseup'
];
if (window.localStorage) {
window_events.push('storage');
}
for (ii = 0; ii < window_events.length; ++ii) {
JX.enableDispatch(window, window_events[ii]);
}
JX.__simulate = function(node, event) {
if (!has_add_event_listener) {
var e = {target: node, type: event};
JX.__rawEventQueue(e);
if (e.returnValue === false) {
return false;
}
}
};
if (has_add_event_listener) {
document.addEventListener('DOMContentLoaded', function() {
JX.__rawEventQueue({type: 'domready'});
}, true);
} else {
var ready =
'if (this.readyState == "complete") {' +
'JX.__rawEventQueue({type: "domready"});' +
'}';
// NOTE: Don't write a 'src' attribute, because "javascript:void(0)" causes
// a mixed content warning in IE8 if the page is served over SSL.
document.write(
'<script' +
' defer="defer"' +
' onreadystatechange="' + ready + '"' +
'><\/sc' + 'ript' + '>');
}
JX.onload = function(func) {
if (loaded) {
func();
} else {
onload.push(func);
}
};
})();
diff --git a/webroot/rsrc/externals/javelin/lib/Workflow.js b/webroot/rsrc/externals/javelin/lib/Workflow.js
index 7f0818d4e..d767a5878 100644
--- a/webroot/rsrc/externals/javelin/lib/Workflow.js
+++ b/webroot/rsrc/externals/javelin/lib/Workflow.js
@@ -1,501 +1,520 @@
/**
* @requires javelin-stratcom
* javelin-request
* javelin-dom
* javelin-vector
* javelin-install
* javelin-util
* javelin-mask
* javelin-uri
* javelin-routable
* @provides javelin-workflow
* @javelin
*/
JX.install('Workflow', {
construct : function(uri, data) {
if (__DEV__) {
if (!uri || uri == '#') {
JX.$E(
'new JX.Workflow(<?>, ...): '+
'bogus URI provided when creating workflow.');
}
}
this.setURI(uri);
this.setData(data || {});
},
events : ['error', 'finally', 'submit', 'start'],
statics : {
_stack : [],
newFromForm : function(form, data, keep_enabled) {
var pairs = JX.DOM.convertFormToListOfPairs(form);
for (var k in data) {
pairs.push([k, data[k]]);
}
var inputs;
if (keep_enabled) {
inputs = [];
} else {
// Disable form elements during the request
inputs = [].concat(
JX.DOM.scry(form, 'input'),
JX.DOM.scry(form, 'button'),
JX.DOM.scry(form, 'textarea'));
for (var ii = 0; ii < inputs.length; ii++) {
if (inputs[ii].disabled) {
delete inputs[ii];
} else {
inputs[ii].disabled = true;
}
}
}
var workflow = new JX.Workflow(form.getAttribute('action'), {});
workflow._form = form;
workflow.setDataWithListOfPairs(pairs);
workflow.setMethod(form.getAttribute('method'));
- workflow.listen('finally', function() {
- // Re-enable form elements
- for (var ii = 0; ii < inputs.length; ii++) {
- inputs[ii] && (inputs[ii].disabled = false);
+
+ var onfinally = JX.bind(workflow, function() {
+ if (!this._keepControlsDisabled) {
+ for (var ii = 0; ii < inputs.length; ii++) {
+ inputs[ii] && (inputs[ii].disabled = false);
+ }
}
});
+ workflow.listen('finally', onfinally);
return workflow;
},
newFromLink : function(link) {
var workflow = new JX.Workflow(link.href);
return workflow;
},
_push : function(workflow) {
JX.Mask.show();
JX.Workflow._stack.push(workflow);
},
_pop : function() {
var dialog = JX.Workflow._stack.pop();
(dialog.getCloseHandler() || JX.bag)();
dialog._destroy();
JX.Mask.hide();
},
disable : function() {
JX.Workflow._disabled = true;
},
_onbutton : function(event) {
if (JX.Stratcom.pass()) {
return;
}
if (JX.Workflow._disabled) {
return;
}
// Get the button (which is sometimes actually another tag, like an <a />)
// which triggered the event. In particular, this makes sure we get the
// right node if there is a <button> with an <img /> inside it or
// or something similar.
var t = event.getNode('jx-workflow-button') ||
event.getNode('tag:button');
// If this button disables workflow (normally, because it is a file
// download button) let the event through without modification.
if (JX.Stratcom.getData(t).disableWorkflow) {
return;
}
event.prevent();
if (t.name == '__cancel__' || t.name == '__close__') {
JX.Workflow._pop();
} else {
var form = event.getNode('jx-dialog');
JX.Workflow._dosubmit(form, t);
}
},
_onsyntheticsubmit : function(e) {
if (JX.Stratcom.pass()) {
return;
}
if (JX.Workflow._disabled) {
return;
}
e.prevent();
var form = e.getNode('jx-dialog');
var button = JX.DOM.find(form, 'button', '__default__');
JX.Workflow._dosubmit(form, button);
},
_dosubmit : function(form, button) {
// Issue a DOM event first, so form-oriented handlers can act.
var dom_event = JX.DOM.invoke(form, 'didWorkflowSubmit');
if (dom_event.getPrevented()) {
return;
}
var data = JX.DOM.convertFormToListOfPairs(form);
data.push([button.name, button.value || true]);
var active = JX.Workflow._getActiveWorkflow();
active._form = form;
var e = active.invoke('submit', {form: form, data: data});
if (!e.getStopped()) {
// NOTE: Don't remove the current dialog yet because additional
// handlers may still want to access the nodes.
+ // Disable whatever button the user clicked to prevent duplicate
+ // submission mistakes when you accidentally click a button multiple
+ // times. See T11145.
+ button.disabled = true;
+
active
.setURI(form.getAttribute('action') || active.getURI())
.setDataWithListOfPairs(data)
.start();
}
},
_getActiveWorkflow : function() {
var stack = JX.Workflow._stack;
return stack[stack.length - 1];
},
_onresizestart: function(e) {
var self = JX.Workflow;
if (self._resizing) {
return;
}
var workflow = self._getActiveWorkflow();
if (!workflow) {
return;
}
e.kill();
var form = JX.DOM.find(workflow._root, 'div', 'jx-dialog');
var resize = e.getNodeData('jx-dialog-resize');
var node_y = JX.$(resize.resizeY);
var dim = JX.Vector.getDim(form);
dim.y = JX.Vector.getDim(node_y).y;
if (!form._minimumSize) {
form._minimumSize = dim;
}
self._resizing = {
min: form._minimumSize,
form: form,
startPos: JX.$V(e),
startDim: dim,
resizeY: node_y,
resizeX: resize.resizeX
};
},
_onmousemove: function(e) {
var self = JX.Workflow;
if (!self._resizing) {
return;
}
var spec = self._resizing;
var form = spec.form;
var min = spec.min;
var delta = JX.$V(e).add(-spec.startPos.x, -spec.startPos.y);
var src_dim = spec.startDim;
var dst_dim = JX.$V(src_dim.x + delta.x, src_dim.y + delta.y);
if (dst_dim.x < min.x) {
dst_dim.x = min.x;
}
if (dst_dim.y < min.y) {
dst_dim.y = min.y;
}
if (spec.resizeX) {
JX.$V(dst_dim.x, null).setDim(form);
}
if (spec.resizeY) {
JX.$V(null, dst_dim.y).setDim(spec.resizeY);
}
},
_onmouseup: function() {
var self = JX.Workflow;
if (!self._resizing) {
return;
}
self._resizing = false;
}
},
members : {
_root : null,
_pushed : false,
_data : null,
_form: null,
_paused: 0,
_nextCallback: null,
+ _keepControlsDisabled: false,
getSourceForm: function() {
return this._form;
},
pause: function() {
this._paused++;
return this;
},
resume: function() {
if (!this._paused) {
JX.$E('Resuming a workflow which is not paused!');
}
this._paused--;
if (!this._paused) {
var next = this._nextCallback;
this._nextCallback = null;
if (next) {
next();
}
}
return this;
},
_onload : function(r) {
this._destroy();
// It is permissible to send back a falsey redirect to force a page
// reload, so we need to take this branch if the key is present.
if (r && (typeof r.redirect != 'undefined')) {
+ // Before we redirect to file downloads, we close the dialog. These
+ // redirects aren't real navigation events so we end up stuck in the
+ // dialog otherwise.
+ if (r.close) {
+ this._pop();
+ }
+
+ // If we're redirecting, don't re-enable for controls.
+ this._keepControlsDisabled = true;
+
JX.$U(r.redirect).go();
} else if (r && r.dialog) {
this._push();
this._root = JX.$N(
'div',
{className: 'jx-client-dialog'},
JX.$H(r.dialog));
JX.DOM.listen(
this._root,
'click',
[['jx-workflow-button'], ['tag:button']],
JX.Workflow._onbutton);
JX.DOM.listen(
this._root,
'didSyntheticSubmit',
[],
JX.Workflow._onsyntheticsubmit);
JX.DOM.listen(
this._root,
'mousedown',
'jx-dialog-resize',
JX.Workflow._onresizestart);
// Note that even in the presence of a content frame, we're doing
// everything here at top level: dialogs are fully modal and cover
// the entire window.
document.body.appendChild(this._root);
var d = JX.Vector.getDim(this._root);
var v = JX.Vector.getViewport();
var s = JX.Vector.getScroll();
// Normally, we position dialogs 100px from the top of the screen.
// Use more space if the dialog is large (at least roughly the size
// of the viewport).
var offset = Math.min(Math.max(20, (v.y - d.y) / 2), 100);
JX.$V(0, s.y + offset).setPos(this._root);
try {
JX.DOM.focus(JX.DOM.find(this._root, 'button', '__default__'));
var inputs = JX.DOM.scry(this._root, 'input')
.concat(JX.DOM.scry(this._root, 'textarea'));
var miny = Number.POSITIVE_INFINITY;
var target = null;
for (var ii = 0; ii < inputs.length; ++ii) {
if (inputs[ii].type != 'hidden') {
// Find the topleft-most displayed element.
var p = JX.$V(inputs[ii]);
if (p.y < miny) {
miny = p.y;
target = inputs[ii];
}
}
}
target && JX.DOM.focus(target);
} catch (_ignored) {}
// The `focus()` call may have scrolled the window. Scroll it back to
// where it was before -- we want to focus the control, but not adjust
// the scroll position.
// Dialogs are window-level, so scroll the window explicitly.
window.scrollTo(s.x, s.y);
} else if (this.getHandler()) {
this.getHandler()(r);
this._pop();
} else if (r) {
if (__DEV__) {
JX.$E('Response to workflow request went unhandled.');
}
}
},
_push : function() {
if (!this._pushed) {
this._pushed = true;
JX.Workflow._push(this);
}
},
_pop : function() {
if (this._pushed) {
this._pushed = false;
JX.Workflow._pop();
}
},
_destroy : function() {
if (this._root) {
JX.DOM.remove(this._root);
this._root = null;
}
},
start : function() {
var next = JX.bind(this, this._send);
this.pause();
this._nextCallback = next;
this.invoke('start', this);
this.resume();
},
_send: function() {
var uri = this.getURI();
var method = this.getMethod();
var r = new JX.Request(uri, JX.bind(this, this._onload));
var list_of_pairs = this._data;
list_of_pairs.push(['__wflow__', true]);
r.setDataWithListOfPairs(list_of_pairs);
r.setDataSerializer(this.getDataSerializer());
if (method) {
r.setMethod(method);
}
r.listen('finally', JX.bind(this, this.invoke, 'finally'));
r.listen('error', JX.bind(this, function(error) {
var e = this.invoke('error', error);
if (e.getStopped()) {
return;
}
// TODO: Default error behavior? On Facebook Lite, we just shipped the
// user to "/error/". We could emit a blanket 'workflow-failed' type
// event instead.
}));
r.send();
},
getRoutable: function() {
var routable = new JX.Routable();
routable.listen('start', JX.bind(this, function() {
// Pass the event to allow other listeners to "start" to configure this
// workflow before it fires.
JX.Stratcom.pass(JX.Stratcom.context());
this.start();
}));
this.listen('finally', JX.bind(routable, routable.done));
return routable;
},
setData : function(dictionary) {
this._data = [];
for (var k in dictionary) {
this._data.push([k, dictionary[k]]);
}
return this;
},
addData: function(key, value) {
this._data.push([key, value]);
return this;
},
setDataWithListOfPairs : function(list_of_pairs) {
this._data = list_of_pairs;
return this;
}
},
properties : {
handler : null,
closeHandler : null,
dataSerializer : null,
method : null,
URI : null
},
initialize : function() {
function close_dialog_when_user_presses_escape(e) {
if (e.getSpecialKey() != 'esc') {
// Some key other than escape.
return;
}
if (JX.Workflow._disabled) {
// Workflows are disabled on this page.
return;
}
if (JX.Stratcom.pass()) {
// Something else swallowed the event.
return;
}
var active = JX.Workflow._getActiveWorkflow();
if (!active) {
// No active workflow.
return;
}
// Note: the cancel button is actually an <a /> tag.
var buttons = JX.DOM.scry(active._root, 'a', 'jx-workflow-button');
if (!buttons.length) {
// No buttons in the dialog.
return;
}
var cancel = null;
for (var ii = 0; ii < buttons.length; ii++) {
if (buttons[ii].name == '__cancel__') {
cancel = buttons[ii];
break;
}
}
if (!cancel) {
// No 'Cancel' button.
return;
}
JX.Workflow._pop();
e.prevent();
}
JX.Stratcom.listen('keydown', null, close_dialog_when_user_presses_escape);
JX.Stratcom.listen('mousemove', null, JX.Workflow._onmousemove);
JX.Stratcom.listen('mouseup', null, JX.Workflow._onmouseup);
}
});
diff --git a/webroot/rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js b/webroot/rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js
index 91eec515f..0dedc0866 100644
--- a/webroot/rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js
+++ b/webroot/rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js
@@ -1,371 +1,376 @@
/**
* @requires javelin-install
* javelin-util
* javelin-dom
* javelin-typeahead-normalizer
* @provides javelin-typeahead-source
* @javelin
*/
JX.install('TypeaheadSource', {
construct : function() {
- this._raw = {};
- this._lookup = {};
+ this.resetResults();
this.setNormalizer(JX.TypeaheadNormalizer.normalize);
this._excludeIDs = {};
},
events : ['waiting', 'resultsready', 'complete'],
properties : {
/**
* Allows you to specify a function which will be used to normalize strings.
* Strings are normalized before being tokenized, and before being sent to
* the server. The purpose of normalization is to strip out irrelevant data,
* like uppercase/lowercase, extra spaces, or punctuation. By default,
* the @{JX.TypeaheadNormalizer} is used to normalize strings, but you may
* want to provide a different normalizer, particularly if there are
* special characters with semantic meaning in your object names.
*
* @param function
*/
normalizer : null,
/**
* If a typeahead query should be processed before being normalized and
* tokenized, specify a queryExtractor.
*
* @param function
*/
queryExtractor : null,
/**
* Transformers convert data from a wire format to a runtime format. The
* transformation mechanism allows you to choose an efficient wire format
* and then expand it on the client side, rather than duplicating data
* over the wire. The transformation is applied to objects passed to
* addResult(). It should accept whatever sort of object you ship over the
* wire, and produce a dictionary with these keys:
*
* - **id**: a unique id for each object.
* - **name**: the string used for matching against user input.
* - **uri**: the URI corresponding with the object (must be present
* but need not be meaningful)
*
* You can also give:
* - **display**: the text or nodes to show in the DOM. Usually just the
* same as ##name##.
* - **tokenizable**: if you want to tokenize something other than the
* ##name##, for the typeahead to complete on, specify it here. A
* selected entry from the typeahead will still insert the ##name##
* into the input, but the ##tokenizable## field lets you complete on
* non-name things.
*
* The default transformer expects a three element list with elements
* [name, uri, id]. It assigns the first element to both ##name## and
* ##display##.
*
* @param function
*/
transformer : null,
/**
* Configures the maximum number of suggestions shown in the typeahead
* dropdown.
*
* @param int
*/
maximumResultCount : 5,
/**
* Optional function which is used to sort results. Inputs are the input
* string, the list of matches, and a default comparator. The function
* should sort the list for display. This is the minimum useful
* implementation:
*
* function(value, list, comparator) {
* list.sort(comparator);
* }
*
* Alternatively, you may pursue more creative implementations.
*
* The `value` is a raw string; you can bind the datasource into the
* function and use normalize() or tokenize() to parse it.
*
* The `list` is a list of objects returned from the transformer function,
* see the `transformer` property. These are the objects in the list which
* match the value.
*
* The `comparator` is a sort callback which implements sensible default
* sorting rules (e.g., alphabetic order), which you can use as a fallback
* if you just want to tweak the results (e.g., put some items at the top).
*
* The function is called after the user types some text, immediately before
* the possible completion results are displayed to the user.
*
* @param function
*/
sortHandler : null,
/**
* Optional function which is used to filter results before display. Inputs
* are the input string and a list of matches. The function should
* return a list of matches to display. This is the minimum useful
* implementation:
*
* function(value, list) {
* return list;
* }
*
* @param function
*/
filterHandler : null
},
members : {
_raw : null,
_lookup : null,
_excludeIDs : null,
_changeListener : null,
_startListener : null,
bindToTypeahead : function(typeahead) {
this._changeListener = typeahead.listen(
'change',
JX.bind(this, this.didChange)
);
this._startListener = typeahead.listen(
'start',
JX.bind(this, this.didStart)
);
},
unbindFromTypeahead : function() {
this._changeListener.remove();
this._startListener.remove();
},
didChange : function() {
return;
},
didStart : function() {
return;
},
clearCache : function() {
this._raw = {};
this._lookup = {};
},
addExcludeID : function(id) {
if (id) {
this._excludeIDs[id] = true;
}
},
removeExcludeID : function (id) {
if (id) {
delete this._excludeIDs[id];
}
},
addResult : function(obj) {
obj = (this.getTransformer() || this._defaultTransformer)(obj);
if (obj.id in this._raw) {
// We're already aware of this result. This will happen if someone
// searches for "zeb" and then for "zebra" with a
// TypeaheadRequestSource, for example, or the datasource just doesn't
// dedupe things properly. Whatever the case, just ignore it.
return;
}
if (__DEV__) {
for (var k in {name : 1, id : 1, display : 1, uri : 1}) {
if (!(k in obj)) {
throw new Error(
'JX.TypeaheadSource.addResult(): result must have ' +
'properties \'name\', \'id\', \'uri\' and \'display\'.');
}
}
}
this._raw[obj.id] = obj;
var t = this.tokenize(obj.tokenizable || obj.name);
for (var jj = 0; jj < t.length; ++jj) {
if (!this._lookup.hasOwnProperty(t[jj])) {
this._lookup[t[jj]] = [];
}
this._lookup[t[jj]].push(obj.id);
}
},
waitForResults : function() {
this.invoke('waiting');
return this;
},
/**
* Get the raw state of a result by its ID. A number of other events and
* mechanisms give a list of result IDs and limited additional data; if you
* need to act on the full result data you can look it up here.
*
* @param scalar Result ID.
* @return dict Corresponding raw result.
*/
getResult : function(id) {
return this._raw[id];
},
matchResults : function(value, partial) {
// This table keeps track of the number of tokens each potential match
// has actually matched. When we're done, the real matches are those
// which have matched every token (so the value is equal to the token
// list length).
var match_count = {};
// This keeps track of distinct matches. If the user searches for
// something like "Chris C" against "Chris Cox", the "C" will match
// both fragments. We need to make sure we only count distinct matches.
var match_fragments = {};
var matched = {};
var seen = {};
var query_extractor = this.getQueryExtractor();
if (query_extractor) {
value = query_extractor(value);
}
var t = this.tokenize(value);
// Sort tokens by longest-first. We match each name fragment with at
// most one token.
t.sort(function(u, v) { return v.length - u.length; });
for (var ii = 0; ii < t.length; ++ii) {
// Do something reasonable if the user types the same token twice; this
// is sort of stupid so maybe kill it?
if (t[ii] in seen) {
t.splice(ii--, 1);
continue;
}
seen[t[ii]] = true;
var fragment = t[ii];
for (var name_fragment in this._lookup) {
if (name_fragment.substr(0, fragment.length) === fragment) {
if (!(name_fragment in matched)) {
matched[name_fragment] = true;
} else {
continue;
}
var l = this._lookup[name_fragment];
for (var jj = 0; jj < l.length; ++jj) {
var match_id = l[jj];
if (!match_fragments[match_id]) {
match_fragments[match_id] = {};
}
if (!(fragment in match_fragments[match_id])) {
match_fragments[match_id][fragment] = true;
match_count[match_id] = (match_count[match_id] || 0) + 1;
}
}
}
}
}
var hits = [];
for (var k in match_count) {
if (match_count[k] == t.length && !this._excludeIDs[k]) {
hits.push(k);
}
}
this.filterAndSortHits(value, hits);
var nodes = this.renderNodes(value, hits);
this.invoke('resultsready', nodes, value, partial);
if (!partial) {
this.invoke('complete');
}
},
filterAndSortHits : function(value, hits) {
var objs = [];
var ii;
for (ii = 0; ii < hits.length; ii++) {
objs.push(this._raw[hits[ii]]);
}
var default_comparator = function(u, v) {
var key_u = u.sort || u.name;
var key_v = v.sort || v.name;
return key_u.localeCompare(key_v);
};
var filter_handler = this.getFilterHandler() || function(value, list) {
return list;
};
objs = filter_handler(value, objs);
var sort_handler = this.getSortHandler() || function(value, list, cmp) {
list.sort(cmp);
};
sort_handler(value, objs, default_comparator);
hits.splice(0, hits.length);
for (ii = 0; ii < objs.length; ii++) {
hits.push(objs[ii].id);
}
},
renderNodes : function(value, hits) {
var n = Math.min(this.getMaximumResultCount(), hits.length);
var nodes = [];
for (var kk = 0; kk < n; kk++) {
nodes.push(this.createNode(this._raw[hits[kk]]));
}
return nodes;
},
createNode : function(data) {
return JX.$N(
'a',
{
sigil: 'typeahead-result',
href: data.uri,
name: data.name,
rel: data.id,
className: 'jx-result'
},
data.display
);
},
normalize : function(str) {
return this.getNormalizer()(str);
},
tokenize : function(str) {
str = this.normalize(str);
if (!str.length) {
return [];
}
return str.split(/\s+/g);
},
+
+ resetResults: function() {
+ this._raw = {};
+ this._lookup = {};
+ },
+
_defaultTransformer : function(object) {
return {
name : object[0],
display : object[0],
uri : object[1],
id : object[2]
};
}
}
});
diff --git a/webroot/rsrc/favicons/apple-touch-icon-114x114.png b/webroot/rsrc/favicons/apple-touch-icon-114x114.png
deleted file mode 100644
index cb4d8cd93..000000000
Binary files a/webroot/rsrc/favicons/apple-touch-icon-114x114.png and /dev/null differ
diff --git a/webroot/rsrc/favicons/apple-touch-icon-144x144.png b/webroot/rsrc/favicons/apple-touch-icon-144x144.png
deleted file mode 100644
index 92f2114b2..000000000
Binary files a/webroot/rsrc/favicons/apple-touch-icon-144x144.png and /dev/null differ
diff --git a/webroot/rsrc/favicons/apple-touch-icon-57x57.png b/webroot/rsrc/favicons/apple-touch-icon-57x57.png
deleted file mode 100644
index 55df0a48e..000000000
Binary files a/webroot/rsrc/favicons/apple-touch-icon-57x57.png and /dev/null differ
diff --git a/webroot/rsrc/favicons/apple-touch-icon-60x60.png b/webroot/rsrc/favicons/apple-touch-icon-60x60.png
deleted file mode 100644
index 97dfdbc36..000000000
Binary files a/webroot/rsrc/favicons/apple-touch-icon-60x60.png and /dev/null differ
diff --git a/webroot/rsrc/favicons/apple-touch-icon-72x72.png b/webroot/rsrc/favicons/apple-touch-icon-72x72.png
deleted file mode 100644
index cb1f066ad..000000000
Binary files a/webroot/rsrc/favicons/apple-touch-icon-72x72.png and /dev/null differ
diff --git a/webroot/rsrc/favicons/favicon-196x196.png b/webroot/rsrc/favicons/favicon-196x196.png
deleted file mode 100644
index bf30eb1cf..000000000
Binary files a/webroot/rsrc/favicons/favicon-196x196.png and /dev/null differ
diff --git a/webroot/rsrc/favicons/favicon-32x32.png b/webroot/rsrc/favicons/favicon-32x32.png
deleted file mode 100644
index c6f2a7556..000000000
Binary files a/webroot/rsrc/favicons/favicon-32x32.png and /dev/null differ
diff --git a/webroot/rsrc/favicons/favicon-96x96.png b/webroot/rsrc/favicons/favicon-96x96.png
deleted file mode 100644
index 5f0522be6..000000000
Binary files a/webroot/rsrc/favicons/favicon-96x96.png and /dev/null differ
diff --git a/webroot/rsrc/favicons/favicon-mention.ico b/webroot/rsrc/favicons/favicon-mention.ico
deleted file mode 100644
index 6e27ce833..000000000
Binary files a/webroot/rsrc/favicons/favicon-mention.ico and /dev/null differ
diff --git a/webroot/rsrc/favicons/favicon-message.ico b/webroot/rsrc/favicons/favicon-message.ico
deleted file mode 100644
index 07408f318..000000000
Binary files a/webroot/rsrc/favicons/favicon-message.ico and /dev/null differ
diff --git a/webroot/rsrc/favicons/favicon.ico b/webroot/rsrc/favicons/favicon.ico
deleted file mode 100644
index f07770aa8..000000000
Binary files a/webroot/rsrc/favicons/favicon.ico and /dev/null differ
diff --git a/webroot/rsrc/favicons/mstile-144x144.png b/webroot/rsrc/favicons/mstile-144x144.png
deleted file mode 100644
index 92f2114b2..000000000
Binary files a/webroot/rsrc/favicons/mstile-144x144.png and /dev/null differ
diff --git a/webroot/rsrc/favicons/mstile-150x150.png b/webroot/rsrc/favicons/mstile-150x150.png
deleted file mode 100644
index df1804953..000000000
Binary files a/webroot/rsrc/favicons/mstile-150x150.png and /dev/null differ
diff --git a/webroot/rsrc/favicons/mstile-310x150.png b/webroot/rsrc/favicons/mstile-310x150.png
deleted file mode 100644
index 56c3ed198..000000000
Binary files a/webroot/rsrc/favicons/mstile-310x150.png and /dev/null differ
diff --git a/webroot/rsrc/favicons/mstile-310x310.png b/webroot/rsrc/favicons/mstile-310x310.png
deleted file mode 100644
index ef911d803..000000000
Binary files a/webroot/rsrc/favicons/mstile-310x310.png and /dev/null differ
diff --git a/webroot/rsrc/favicons/mstile-70x70.png b/webroot/rsrc/favicons/mstile-70x70.png
deleted file mode 100644
index 3b51e6052..000000000
Binary files a/webroot/rsrc/favicons/mstile-70x70.png and /dev/null differ
diff --git a/webroot/rsrc/js/application/aphlict/behavior-aphlict-listen.js b/webroot/rsrc/js/application/aphlict/behavior-aphlict-listen.js
index 24571b598..ac058bde4 100644
--- a/webroot/rsrc/js/application/aphlict/behavior-aphlict-listen.js
+++ b/webroot/rsrc/js/application/aphlict/behavior-aphlict-listen.js
@@ -1,170 +1,171 @@
/**
* @provides javelin-behavior-aphlict-listen
* @requires javelin-behavior
* javelin-aphlict
* javelin-stratcom
* javelin-request
* javelin-uri
* javelin-dom
* javelin-json
* javelin-router
* javelin-util
* javelin-leader
* javelin-sound
* phabricator-notification
*/
JX.behavior('aphlict-listen', function(config) {
var page_objects = config.pageObjects;
var reload_notification = null;
JX.Stratcom.listen('aphlict-server-message', null, function(e) {
var message = e.getData();
if (message.type != 'notification') {
return;
}
JX.Leader.callIfLeader(function() {
var request = new JX.Request(
'/notification/individual/',
onNotification);
var routable = request
.addData({key: message.key})
.getRoutable();
routable
.setType('notification')
.setPriority(250);
JX.Router.getInstance().queue(routable);
});
});
// Respond to a notification from the Aphlict notification server. We send
// a request to Phabricator to get notification details.
function onAphlictMessage(message) {
switch (message.type) {
case 'aphlict.server':
JX.Stratcom.invoke('aphlict-server-message', null, message.data);
break;
case 'notification.individual':
JX.Stratcom.invoke('aphlict-notification-message', null, message.data);
break;
case 'aphlict.reconnect':
JX.Stratcom.invoke('aphlict-reconnect', null, message.data);
break;
}
}
// Respond to a response from Phabricator about a specific notification.
function onNotification(response) {
if (!response.pertinent) {
return;
}
JX.Leader.broadcast(
response.uniqueID,
{
type: 'notification.individual',
data: response
});
}
JX.Stratcom.listen('aphlict-notification-message', null, function(e) {
JX.Stratcom.invoke('notification-panel-update', null, {});
var response = e.getData();
- if (!response.showAnyNotification) {
+ if (!response.showAnyNotification && !response.showDesktopNotification) {
return;
}
// Show the notification itself.
new JX.Notification()
.setContent(JX.$H(response.content))
.setKey(response.primaryObjectPHID)
+ .setShowAsWebNotification(response.showAnyNotification)
.setShowAsDesktopNotification(response.showDesktopNotification)
.setTitle(response.title)
.setBody(response.body)
.setHref(response.href)
.setIcon(response.icon)
.show();
// If the notification affected an object on this page, show a
// permanent reload notification if we aren't already.
if ((response.primaryObjectPHID in page_objects) &&
reload_notification === null) {
var reload = new JX.Notification()
.setContent('Page updated, click to reload.')
.alterClassName('jx-notification-alert', true)
.setDuration(0);
reload.listen(
'activate',
function() {
// double check we are still on the page where re-loading makes
// sense...!
if (response.primaryObjectPHID in page_objects) {
JX.$U().go();
}
});
reload.show();
reload_notification = {
dialog: reload,
phid: response.primaryObjectPHID
};
}
});
var client = new JX.Aphlict(
config.websocketURI,
config.subscriptions);
var start_client = function() {
client
.setHandler(onAphlictMessage)
.start();
};
// Don't start the client until other behaviors have had a chance to
// initialize. In particular, we want to capture events into the log for
// the DarkConsole "Realtime" panel.
setTimeout(start_client, 0);
JX.Stratcom.listen(
'quicksand-redraw',
null,
function (e) {
var old_data = e.getData().oldResponse;
var new_data = e.getData().newResponse;
client.clearSubscriptions(old_data.subscriptions);
client.setSubscriptions(new_data.subscriptions);
page_objects = new_data.pageObjects;
if (reload_notification) {
if (reload_notification.phid in page_objects) {
return;
}
reload_notification.dialog.hide();
reload_notification = null;
}
});
JX.Leader.listen('onReceiveBroadcast', function(message, is_leader) {
if (message.type !== 'sound') {
return;
}
if (!is_leader) {
return;
}
JX.Sound.play(message.data);
});
});
diff --git a/webroot/rsrc/js/application/diff/DiffChangeset.js b/webroot/rsrc/js/application/diff/DiffChangeset.js
index 72eeae294..24d734573 100644
--- a/webroot/rsrc/js/application/diff/DiffChangeset.js
+++ b/webroot/rsrc/js/application/diff/DiffChangeset.js
@@ -1,819 +1,891 @@
/**
* @provides phabricator-diff-changeset
* @requires javelin-dom
* javelin-util
* javelin-stratcom
* javelin-install
* javelin-workflow
* javelin-router
* javelin-behavior-device
* javelin-vector
* phabricator-diff-inline
* @javelin
*/
JX.install('DiffChangeset', {
construct : function(node) {
this._node = node;
var data = this._getNodeData();
this._renderURI = data.renderURI;
this._ref = data.ref;
this._whitespace = data.whitespace;
this._renderer = data.renderer;
this._highlight = data.highlight;
this._encoding = data.encoding;
this._loaded = data.loaded;
+ this._treeNodeID = data.treeNodeID;
this._leftID = data.left;
this._rightID = data.right;
this._displayPath = JX.$H(data.displayPath);
this._icon = data.icon;
this._inlines = [];
},
members: {
_node: null,
_loaded: false,
_sequence: 0,
_stabilize: false,
_renderURI: null,
_ref: null,
_whitespace: null,
_renderer: null,
_highlight: null,
_encoding: null,
_undoTemplates: null,
_leftID: null,
_rightID: null,
_inlines: null,
_visible: true,
_undoNode: null,
_displayPath: null,
_changesetList: null,
_icon: null,
+ _treeNodeID: null,
getLeftChangesetID: function() {
return this._leftID;
},
getRightChangesetID: function() {
return this._rightID;
},
setChangesetList: function(list) {
this._changesetList = list;
return this;
},
getIcon: function() {
if (!this._visible) {
return 'fa-file-o';
}
return this._icon;
},
getColor: function() {
if (!this._visible) {
return 'grey';
}
return 'blue';
},
getChangesetList: function() {
return this._changesetList;
},
/**
* Has the content of this changeset been loaded?
*
* This method returns `true` if a request has been fired, even if the
* response has not returned yet.
*
* @return bool True if the content has been loaded.
*/
isLoaded: function() {
return this._loaded;
},
/**
* Configure stabilization of the document position on content load.
*
* When we dump the changeset into the document, we can try to stabilize
* the document scroll position so that the user doesn't feel like they
* are jumping around as things load in. This is generally useful when
* populating initial changes.
*
* However, if a user explicitly requests a content load by clicking a
* "Load" link or using the dropdown menu, this stabilization generally
* feels unnatural, so we don't use it in response to explicit user action.
*
* @param bool True to stabilize the next content fill.
* @return this
*/
setStabilize: function(stabilize) {
this._stabilize = stabilize;
return this;
},
/**
* Should this changeset load immediately when the page loads?
*
* Normally, changes load immediately, but if a diff or commit is very
* large we stop doing this and have the user load files explicitly, or
* choose to load everything.
*
* @return bool True if the changeset should load automatically when the
* page loads.
*/
shouldAutoload: function() {
return this._getNodeData().autoload;
},
/**
* Load this changeset, if it isn't already loading.
*
* This fires a request to fill the content of this changeset, provided
* there isn't already a request in flight. To force a reload, use
* @{method:reload}.
*
* @return this
*/
load: function() {
if (this._loaded) {
return this;
}
return this.reload();
},
/**
* Reload the changeset content.
*
* This method always issues a request, even if the content is already
* loading. To load conditionally, use @{method:load}.
*
* @return this
*/
reload: function() {
this._loaded = true;
this._sequence++;
var params = this._getViewParameters();
var pht = this.getChangesetList().getTranslations();
var workflow = new JX.Workflow(this._renderURI, params)
.setHandler(JX.bind(this, this._onresponse, this._sequence));
this._startContentWorkflow(workflow);
JX.DOM.setContent(
this._getContentFrame(),
JX.$N(
'div',
{className: 'differential-loading'},
pht('Loading...')));
return this;
},
/**
* Load missing context in a changeset.
*
* We do this when the user clicks "Show X Lines". We also expand all of
* the missing context when they "Show All Context".
*
* @param string Line range specification, like "0-40/0-20".
* @param node Row where the context should be rendered after loading.
* @param bool True if this is a bulk load of multiple context blocks.
* @return this
*/
loadContext: function(range, target, bulk) {
var params = this._getViewParameters();
params.range = range;
var pht = this.getChangesetList().getTranslations();
var container = JX.DOM.scry(target, 'td')[0];
JX.DOM.setContent(container, pht('Loading...'));
JX.DOM.alterClass(target, 'differential-show-more-loading', true);
var workflow = new JX.Workflow(this._renderURI, params)
.setHandler(JX.bind(this, this._oncontext, target));
if (bulk) {
// If we're loading a bunch of these because the viewer clicked
// "Show All Context" or similar, use lower-priority requests
// and draw a progress bar.
this._startContentWorkflow(workflow);
} else {
// If this is a single click on a context link, use a higher priority
// load without a chrome change.
workflow.start();
}
return this;
},
loadAllContext: function() {
var nodes = JX.DOM.scry(this._node, 'tr', 'context-target');
for (var ii = 0; ii < nodes.length; ii++) {
var show = JX.DOM.scry(nodes[ii], 'a', 'show-more');
for (var jj = 0; jj < show.length; jj++) {
var data = JX.Stratcom.getData(show[jj]);
if (data.type != 'all') {
continue;
}
this.loadContext(data.range, nodes[ii], true);
}
}
},
_startContentWorkflow: function(workflow) {
var routable = workflow.getRoutable();
routable
.setPriority(500)
.setType('content')
.setKey(this._getRoutableKey());
JX.Router.getInstance().queue(routable);
},
getDisplayPath: function() {
return this._displayPath;
},
/**
* Receive a response to a context request.
*/
_oncontext: function(target, response) {
// TODO: This should be better structured.
// If the response comes back with several top-level nodes, the last one
// is the actual context; the others are headers. Add any headers first,
// then copy the new rows into the document.
var markup = JX.$H(response.changeset).getFragment();
var len = markup.childNodes.length;
var diff = JX.DOM.findAbove(target, 'table', 'differential-diff');
for (var ii = 0; ii < len - 1; ii++) {
diff.parentNode.insertBefore(markup.firstChild, diff);
}
var table = markup.firstChild;
var root = target.parentNode;
this._moveRows(table, root, target);
root.removeChild(target);
this._onchangesetresponse(response);
},
_moveRows: function(src, dst, before) {
var rows = JX.DOM.scry(src, 'tr');
for (var ii = 0; ii < rows.length; ii++) {
// Find the table this <tr /> belongs to. If it's a sub-table, like a
// table in an inline comment, don't copy it.
if (JX.DOM.findAbove(rows[ii], 'table') !== src) {
continue;
}
if (before) {
dst.insertBefore(rows[ii], before);
} else {
dst.appendChild(rows[ii]);
}
}
},
/**
* Get parameters which define the current rendering options.
*/
_getViewParameters: function() {
return {
ref: this._ref,
whitespace: this._whitespace || '',
renderer: this.getRenderer() || '',
highlight: this._highlight || '',
encoding: this._encoding || ''
};
},
/**
* Get the active @{class:JX.Routable} for this changeset.
*
* After issuing a request with @{method:load} or @{method:reload}, you
* can adjust routable settings (like priority) by querying the routable
* with this method. Note that there may not be a current routable.
*
* @return JX.Routable|null Active routable, if one exists.
*/
getRoutable: function() {
return JX.Router.getInstance().getRoutableByKey(this._getRoutableKey());
},
setRenderer: function(renderer) {
this._renderer = renderer;
return this;
},
getRenderer: function() {
if (this._renderer !== null) {
return this._renderer;
}
// NOTE: If you load the page at one device resolution and then resize to
// a different one we don't re-render the diffs, because it's a
// complicated mess and you could lose inline comments, cursor positions,
// etc.
return (JX.Device.getDevice() == 'desktop') ? '2up' : '1up';
},
getUndoTemplates: function() {
return this._undoTemplates;
},
setEncoding: function(encoding) {
this._encoding = encoding;
return this;
},
getEncoding: function() {
return this._encoding;
},
setHighlight: function(highlight) {
this._highlight = highlight;
return this;
},
getHighlight: function() {
return this._highlight;
},
getSelectableItems: function() {
var items = [];
items.push({
type: 'file',
changeset: this,
target: this,
nodes: {
begin: this._node,
end: null
}
});
if (!this._visible) {
return items;
}
var rows = JX.DOM.scry(this._node, 'tr');
var blocks = [];
var block;
var ii;
for (ii = 0; ii < rows.length; ii++) {
var type = this._getRowType(rows[ii]);
if (!block || (block.type !== type)) {
block = {
type: type,
items: []
};
blocks.push(block);
}
block.items.push(rows[ii]);
}
var last_inline = null;
var last_inline_item = null;
for (ii = 0; ii < blocks.length; ii++) {
block = blocks[ii];
if (block.type == 'change') {
items.push({
type: block.type,
changeset: this,
target: block.items[0],
nodes: {
begin: block.items[0],
end: block.items[block.items.length - 1]
}
});
}
if (block.type == 'comment') {
for (var jj = 0; jj < block.items.length; jj++) {
var inline = this.getInlineForRow(block.items[jj]);
// When comments are being edited, they have a hidden row with
// the actual comment and then a visible row with the editor.
// In this case, we only want to generate one item, but it should
// use the editor as a scroll target. To accomplish this, check if
// this row has the same inline as the previous row. If so, update
// the last item to use this row's nodes.
if (inline === last_inline) {
last_inline_item.nodes.begin = block.items[jj];
last_inline_item.nodes.end = block.items[jj];
continue;
} else {
last_inline = inline;
}
var is_saved = (!inline.isDraft() && !inline.isEditing());
last_inline_item = {
type: block.type,
changeset: this,
target: inline,
hidden: inline.isHidden(),
collapsed: inline.isCollapsed(),
deleted: !inline.getID() && !inline.isEditing(),
nodes: {
begin: block.items[jj],
end: block.items[jj]
},
attributes: {
unsaved: inline.isEditing(),
anyDraft: inline.isDraft() || inline.isDraftDone(),
undone: (is_saved && !inline.isDone()),
done: (is_saved && inline.isDone())
}
};
items.push(last_inline_item);
}
}
}
return items;
},
_getRowType: function(row) {
// NOTE: Don't do "className.indexOf()" elsewhere. This is evil legacy
// magic.
if (row.className.indexOf('inline') !== -1) {
return 'comment';
}
var cells = JX.DOM.scry(row, 'td');
for (var ii = 0; ii < cells.length; ii++) {
if (cells[ii].className.indexOf('old') !== -1 ||
cells[ii].className.indexOf('new') !== -1) {
return 'change';
}
}
},
_getNodeData: function() {
return JX.Stratcom.getData(this._node);
},
getVectors: function() {
return {
pos: JX.$V(this._node),
dim: JX.Vector.getDim(this._node)
};
},
_onresponse: function(sequence, response) {
if (sequence != this._sequence) {
// If this isn't the most recent request, ignore it. This normally
// means the user changed view settings between the time the page loaded
// and the content filled.
return;
}
// As we populate the changeset list, we try to hold the document scroll
// position steady, so that, e.g., users who want to leave a comment on a
// diff with a large number of changes don't constantly have the text
// area scrolled off the bottom of the screen until the entire diff loads.
//
// There are several major cases here:
//
// - If we're near the top of the document, never scroll.
// - If we're near the bottom of the document, always scroll, unless
// we have an anchor.
// - Otherwise, scroll if the changes were above (or, at least,
// almost entirely above) the viewport.
//
// We don't scroll if the changes were just near the top of the viewport
// because this makes us scroll incorrectly when an anchored change is
// visible. See T12779.
var target = this._node;
var old_pos = JX.Vector.getScroll();
var old_view = JX.Vector.getViewport();
var old_dim = JX.Vector.getDocument();
// Number of pixels away from the top or bottom of the document which
// count as "nearby".
var sticky = 480;
var near_top = (old_pos.y <= sticky);
var near_bot = ((old_pos.y + old_view.y) >= (old_dim.y - sticky));
// If we have an anchor in the URL, never stick to the bottom of the
// page. See T11784 for discussion.
if (window.location.hash) {
near_bot = false;
}
var target_pos = JX.Vector.getPos(target);
var target_dim = JX.Vector.getDim(target);
var target_bot = (target_pos.y + target_dim.y);
// Detect if the changeset is entirely (or, at least, almost entirely)
// above us. The height here is roughly the height of the persistent
// banner.
var above_screen = (target_bot < old_pos.y + 64);
// If we have a URL anchor and are currently nearby, stick to it
// no matter what.
var on_target = null;
if (window.location.hash) {
try {
var anchor = JX.$(window.location.hash.replace('#', ''));
if (anchor) {
var anchor_pos = JX.$V(anchor);
if ((anchor_pos.y > old_pos.y) &&
(anchor_pos.y < old_pos.y + 96)) {
on_target = anchor;
}
}
} catch (ignored) {
// If we have a bogus anchor, just ignore it.
}
}
var frame = this._getContentFrame();
JX.DOM.setContent(frame, JX.$H(response.changeset));
if (this._stabilize) {
if (on_target) {
JX.DOM.scrollToPosition(old_pos.x, JX.$V(on_target).y - 60);
} else if (!near_top) {
if (near_bot || above_screen) {
// Figure out how much taller the document got.
var delta = (JX.Vector.getDocument().y - old_dim.y);
JX.DOM.scrollToPosition(old_pos.x, old_pos.y + delta);
}
}
this._stabilize = false;
}
this._onchangesetresponse(response);
},
_onchangesetresponse: function(response) {
// Code shared by autoload and context responses.
if (response.coverage) {
for (var k in response.coverage) {
try {
JX.DOM.replace(JX.$(k), JX.$H(response.coverage[k]));
} catch (ignored) {
// Not terribly important.
}
}
}
if (response.undoTemplates) {
this._undoTemplates = response.undoTemplates;
}
JX.Stratcom.invoke('differential-inline-comment-refresh');
this._rebuildAllInlines();
JX.Stratcom.invoke('resize');
},
_getContentFrame: function() {
return JX.DOM.find(this._node, 'div', 'changeset-view-content');
},
_getRoutableKey: function() {
return 'changeset-view.' + this._ref + '.' + this._sequence;
},
getInlineForRow: function(node) {
var data = JX.Stratcom.getData(node);
if (!data.inline) {
var inline = new JX.DiffInline()
.setChangeset(this)
.bindToRow(node);
this._inlines.push(inline);
}
return data.inline;
},
newInlineForRange: function(origin, target) {
var list = this.getChangesetList();
var src = list.getLineNumberFromHeader(origin);
var dst = list.getLineNumberFromHeader(target);
var changeset_id = null;
var side = list.getDisplaySideFromHeader(origin);
if (side == 'right') {
changeset_id = this.getRightChangesetID();
} else {
changeset_id = this.getLeftChangesetID();
}
var is_new = false;
if (side == 'right') {
is_new = true;
} else if (this.getRightChangesetID() != this.getLeftChangesetID()) {
is_new = true;
}
var data = {
origin: origin,
target: target,
number: src,
length: dst - src,
changesetID: changeset_id,
displaySide: side,
isNewFile: is_new
};
var inline = new JX.DiffInline()
.setChangeset(this)
.bindToRange(data);
this._inlines.push(inline);
inline.create();
return inline;
},
newInlineReply: function(original, text) {
var inline = new JX.DiffInline()
.setChangeset(this)
.bindToReply(original);
this._inlines.push(inline);
inline.create(text);
return inline;
},
getInlineByID: function(id) {
return this._queryInline('id', id);
},
getInlineByPHID: function(phid) {
return this._queryInline('phid', phid);
},
_queryInline: function(field, value) {
// First, look for the inline in the objects we've already built.
var inline = this._findInline(field, value);
if (inline) {
return inline;
}
// If we haven't found a matching inline yet, rebuild all the inlines
// present in the document, then look again.
this._rebuildAllInlines();
return this._findInline(field, value);
},
_findInline: function(field, value) {
for (var ii = 0; ii < this._inlines.length; ii++) {
var inline = this._inlines[ii];
var target;
switch (field) {
case 'id':
target = inline.getID();
break;
case 'phid':
target = inline.getPHID();
break;
}
if (target == value) {
return inline;
}
}
return null;
},
getInlines: function() {
this._rebuildAllInlines();
return this._inlines;
},
_rebuildAllInlines: function() {
var rows = JX.DOM.scry(this._node, 'tr');
- for (var ii = 0; ii < rows.length; ii++) {
+ var ii;
+ for (ii = 0; ii < rows.length; ii++) {
var row = rows[ii];
if (this._getRowType(row) != 'comment') {
continue;
}
// As a side effect, this builds any missing inline objects and adds
// them to this Changeset's list of inlines.
this.getInlineForRow(row);
}
},
+ redrawFileTree: function() {
+ var tree;
+ try {
+ tree = JX.$(this._treeNodeID);
+ } catch (e) {
+ return;
+ }
+
+ var inlines = this._inlines;
+ var done = [];
+ var undone = [];
+ var inline;
+
+ for (var ii = 0; ii < inlines.length; ii++) {
+ inline = inlines[ii];
+
+ if (inline.isDeleted()) {
+ continue;
+ }
+
+ if (inline.isSynthetic()) {
+ continue;
+ }
+
+ if (inline.isEditing()) {
+ continue;
+ }
+
+ if (!inline.getID()) {
+ // These are new comments which have been cancelled, and do not
+ // count as anything.
+ continue;
+ }
+
+ if (inline.isDraft()) {
+ continue;
+ }
+
+ if (!inline.isDone()) {
+ undone.push(inline);
+ } else {
+ done.push(inline);
+ }
+ }
+
+ var total = done.length + undone.length;
+
+ var hint;
+ var is_visible;
+ var is_completed;
+ if (total) {
+ if (done.length) {
+ hint = [done.length, '/', total];
+ } else {
+ hint = total;
+ }
+ is_visible = true;
+ is_completed = (done.length == total);
+ } else {
+ hint = '-';
+ is_visible = false;
+ is_completed = false;
+ }
+
+ JX.DOM.setContent(tree, hint);
+ JX.DOM.alterClass(tree, 'filetree-comments-visible', is_visible);
+ JX.DOM.alterClass(tree, 'filetree-comments-completed', is_completed);
+ },
+
toggleVisibility: function() {
this._visible = !this._visible;
var diff = JX.DOM.find(this._node, 'table', 'differential-diff');
var undo = this._getUndoNode();
if (this._visible) {
JX.DOM.show(diff);
JX.DOM.remove(undo);
} else {
JX.DOM.hide(diff);
JX.DOM.appendContent(diff.parentNode, undo);
}
JX.Stratcom.invoke('resize');
},
isVisible: function() {
return this._visible;
},
_getUndoNode: function() {
if (!this._undoNode) {
var pht = this.getChangesetList().getTranslations();
var link_attributes = {
href: '#'
};
var undo_link = JX.$N('a', link_attributes, pht('Show Content'));
var onundo = JX.bind(this, this._onundo);
JX.DOM.listen(undo_link, 'click', null, onundo);
var node_attributes = {
className: 'differential-collapse-undo'
};
var node_content = [
pht('This file content has been collapsed.'),
' ',
undo_link
];
var undo_node = JX.$N('div', node_attributes, node_content);
this._undoNode = undo_node;
}
return this._undoNode;
},
_onundo: function(e) {
e.kill();
this.toggleVisibility();
}
},
statics: {
getForNode: function(node) {
var data = JX.Stratcom.getData(node);
if (!data.changesetViewManager) {
data.changesetViewManager = new JX.DiffChangeset(node);
}
return data.changesetViewManager;
}
}
});
diff --git a/webroot/rsrc/js/application/diff/DiffChangesetList.js b/webroot/rsrc/js/application/diff/DiffChangesetList.js
index ec0270ac1..5eba051d9 100644
--- a/webroot/rsrc/js/application/diff/DiffChangesetList.js
+++ b/webroot/rsrc/js/application/diff/DiffChangesetList.js
@@ -1,1861 +1,1872 @@
/**
* @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
},
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();
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');
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);
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) {
+ 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;
- if (!changeset) {
- JX.DOM.remove(node);
- return;
- }
-
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 button = new JX.PHUIXButtonView()
.setIcon('fa-bars')
.setButtonType(JX.PHUIXButtonView.BUTTONTYPE_SIMPLE);
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/doorkeeper/behavior-doorkeeper-tag.js b/webroot/rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js
index c7b850f44..f725ef608 100644
--- a/webroot/rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js
+++ b/webroot/rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js
@@ -1,64 +1,78 @@
/**
* @provides javelin-behavior-doorkeeper-tag
* @requires javelin-behavior
* javelin-dom
* javelin-json
* javelin-workflow
* javelin-magical-init
*/
JX.behavior('doorkeeper-tag', function(config, statics) {
statics.tags = (statics.tags || []).concat(config.tags);
statics.cache = statics.cache || {};
// NOTE: We keep a cache in the browser of external objects that we've already
// looked up. This is mostly to keep previews from being flickery messes.
var load = function() {
var tags = statics.tags;
statics.tags = [];
if (!tags.length) {
return;
}
var have = [];
var need = [];
var keys = {};
var draw = function(tags) {
for (var ii = 0; ii < tags.length; ii++) {
try {
JX.DOM.replace(JX.$(tags[ii].id), JX.$H(tags[ii].markup));
} catch (ignored) {
// The tag may have been wiped out of the body by the time the
// response returns, for whatever reason. That's fine, just don't
// bother drawing it.
}
statics.cache[keys[tags[ii].id]] = tags[ii].markup;
}
};
for (var ii = 0; ii < tags.length; ii++) {
- var tag_key = tags[ii].ref.join('@');
+ var key_parts = [];
+
+ key_parts = key_parts.concat(tags[ii].ref);
+ key_parts.push(tags[ii].view);
+
+ var tag_key = key_parts.join(' ');
+
if (tag_key in statics.cache) {
- have.push({id: tags[ii].id, markup: statics.cache[tag_key]});
+ have.push(
+ {
+ id: tags[ii].id,
+ markup: statics.cache[tag_key]
+ });
} else {
need.push(tags[ii]);
keys[tags[ii].id] = tag_key;
}
}
if (have.length) {
draw(have);
}
if (need.length) {
- new JX.Workflow('/doorkeeper/tags/', {tags: JX.JSON.stringify(need)})
+ var data = {
+ tags: JX.JSON.stringify(need)
+ };
+
+ new JX.Workflow('/doorkeeper/tags/', data)
.setHandler(function(r) { draw(r.tags); })
.start();
}
};
JX.onload(load);
});
diff --git a/webroot/rsrc/js/application/files/behavior-document-engine.js b/webroot/rsrc/js/application/files/behavior-document-engine.js
new file mode 100644
index 000000000..ccbd41ca9
--- /dev/null
+++ b/webroot/rsrc/js/application/files/behavior-document-engine.js
@@ -0,0 +1,227 @@
+/**
+ * @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);
+
+ 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);
+ }
+ }
+ });
+
+ 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);
+ }
+
+ 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);
+
+ 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, 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 (!statics.initialized) {
+ JX.Stratcom.listen('click', 'document-engine-view-dropdown', onmenu);
+ statics.initialized = true;
+ }
+
+ if (config && config.renderControlID) {
+ var control = JX.$(config.renderControlID);
+ var data = JX.Stratcom.getData(control);
+ onview(data, null, true);
+ }
+
+});
diff --git a/webroot/rsrc/js/application/harbormaster/behavior-harbormaster-log.js b/webroot/rsrc/js/application/harbormaster/behavior-harbormaster-log.js
new file mode 100644
index 000000000..5bff2c1bb
--- /dev/null
+++ b/webroot/rsrc/js/application/harbormaster/behavior-harbormaster-log.js
@@ -0,0 +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);
+ });
+
+ 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 988d728ce..0ce9823d1 100644
--- a/webroot/rsrc/js/application/herald/PathTypeahead.js
+++ b/webroot/rsrc/js/application/herald/PathTypeahead.js
@@ -1,205 +1,216 @@
/**
* @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._repositorySelect = config.repo_select;
+ 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 = {};
- /*
- * Default values to preload the typeahead with, for extremely common
- * cases.
- */
- this._textInputValues = config.repositoryDefaultPaths;
+ this._icons = config.icons;
this._initializeDatasource();
this._initializeTypeahead(this._input);
},
members : {
- /*
- * DOM <select> elem for choosing the repository of a path.
- */
- _repositorySelect : null,
+ _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()) {
- this._textInputValues[this._repositorySelect.value] =
- this._typeahead.getValue();
+ var phid = this._getRepositoryPHID();
+ if (phid) {
+ this._textInputValues[phid] = this._typeahead.getValue();
+ }
}
this._typeahead.listen(
'change',
JX.bind(this, function(value) {
- this._textInputValues[this._repositorySelect.value] = value;
- this._validate();
- }));
+ var phid = this._getRepositoryPHID();
+ if (phid) {
+ this._textInputValues[phid] = value;
+ }
- this._typeahead.listen(
- 'choose',
- JX.bind(this, function() {
- setTimeout(JX.bind(this._typeahead, this._typeahead.refresh), 0);
+ this._validate();
}));
var repo_set_input = JX.bind(this, this._onrepochange);
this._typeahead.listen('start', repo_set_input);
- JX.DOM.listen(
- this._repositorySelect,
- 'change',
- null,
- 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._repositorySelect.value}
- );
+ {
+ repositoryPHID: this._getRepositoryPHID()
+ });
+
+ // Since we've changed the repository, reset the results.
+ this._datasource.resetResults();
},
_setPathInputBasedOnRepository : function(typeahead, lookup) {
- if (lookup[this._repositorySelect.value]) {
- typeahead.setValue(lookup[this._repositorySelect.value]);
+ 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 repo_id = this._repositorySelect.value;
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,
- function(payload) {
+ 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) {
- if (payload.valid) {
- JX.DOM.alterClass(error_display, 'invalid', false);
- JX.DOM.alterClass(error_display, 'valid', true);
- } else {
- JX.DOM.alterClass(error_display, 'invalid', true);
- JX.DOM.alterClass(error_display, 'valid', false);
- }
- JX.DOM.setContent(error_display, payload.message);
+ 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() {
- JX.DOM.alterClass(error_display, 'validating', false);
this._validationInflight = null;
});
validation_request.setData(
{
- repositoryPHID : repo_id,
+ 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/owners/OwnersPathEditor.js b/webroot/rsrc/js/application/owners/OwnersPathEditor.js
index 3199ac4e2..51780d8a9 100644
--- a/webroot/rsrc/js/application/owners/OwnersPathEditor.js
+++ b/webroot/rsrc/js/application/owners/OwnersPathEditor.js
@@ -1,180 +1,231 @@
/**
* @requires multirow-row-manager
* javelin-install
* path-typeahead
* javelin-dom
* javelin-util
* phabricator-prefab
+ * phuix-form-control-view
* @provides owners-path-editor
* @javelin
*/
JX.install('OwnersPathEditor', {
construct : function(config) {
var root = JX.$(config.root);
this._rowManager = new JX.MultirowRowManager(
JX.DOM.find(root, 'table', config.table));
JX.DOM.listen(
JX.DOM.find(root, 'a', config.add_button),
'click',
null,
JX.bind(this, this._onaddpath));
this._count = 0;
- this._repositories = config.repositories;
this._inputTemplate = config.input_template;
+ this._repositoryTokenizerSpec = config.repositoryTokenizerSpec;
this._completeURI = config.completeURI;
this._validateURI = config.validateURI;
- this._repositoryDefaultPaths = config.repositoryDefaultPaths;
+ this._icons = config.icons;
+ this._modeOptions = config.modeOptions;
this._initializePaths(config.pathRefs);
},
members : {
/*
* MultirowRowManager for controlling add/remove behavior
*/
_rowManager : null,
- /*
- * Array of objects with 'name' and 'repo_id' keys for
- * selecting the repository of a path.
- */
- _repositories : null,
-
/*
* How many rows have been created, for form name generation.
*/
_count : 0,
/*
* URL for the typeahead datasource.
*/
_completeURI : null,
/*
* URL for path validation requests.
*/
_validateURI : null,
/*
* Template typeahead markup to be copied per row.
*/
_inputTemplate : null,
/*
* Most packages will be in one repository, so remember whenever
* the user chooses a repository, and use that repository as the
* default for future rows.
*/
_lastRepositoryChoice : null,
-
- _repositoryDefaultPaths : null,
+ _icons: null,
+ _modeOptions: null,
/*
* Initialize with 0 or more rows.
* Adds one initial row if none are given.
*/
_initializePaths : function(path_refs) {
for (var k in path_refs) {
this.addPath(path_refs[k]);
}
if (!JX.keys(path_refs).length) {
this.addPath();
}
},
/*
* Build a row.
*/
addPath : function(path_ref) {
// Smart default repository. See _lastRepositoryChoice.
if (path_ref) {
- this._lastRepositoryChoice = path_ref.repositoryPHID;
+ this._lastRepositoryChoice = path_ref.repositoryValue;
+ } else {
+ path_ref = {
+ repositoryValue: this._lastRepositoryChoice || {}
+ };
}
- path_ref = path_ref || {};
-
- var selected_repository = path_ref.repositoryPHID ||
- this._lastRepositoryChoice;
- var options = this._buildRepositoryOptions(selected_repository);
- var attrs = {
- name : 'repo[' + this._count + ']',
- className : 'owners-repo'
- };
- var repo_select = JX.$N('select', attrs, options);
- JX.DOM.listen(repo_select, 'change', null, JX.bind(this, function() {
- this._lastRepositoryChoice = repo_select.value;
- }));
+ var repo = this._newRepoCell(path_ref.repositoryValue);
+ var path = this._newPathCell(path_ref.display);
+ var icon = this._newIconCell();
+ var mode_cell = this._newModeCell(path_ref.excluded);
- var repo_cell = JX.$N('td', {}, repo_select);
- var typeahead_cell = JX.$N(
+ var row = this._rowManager.addRow(
+ [
+ mode_cell,
+ repo.cell,
+ path.cell,
+ icon.cell
+ ]);
+
+ new JX.PathTypeahead({
+ repositoryTokenizer: repo.tokenizer,
+ path_input : path.input,
+ hardpoint : path.hardpoint,
+ error_display : icon.cell,
+ completeURI : this._completeURI,
+ validateURI : this._validateURI,
+ icons: this._icons
+ }).start();
+
+ this._count++;
+ return row;
+ },
+
+ _onaddpath : function(e) {
+ e.kill();
+ this.addPath();
+ },
+
+ _newModeCell: function(value) {
+ var options = this._modeOptions;
+
+ var name = 'exclude[' + this._count + ']';
+
+ var control = JX.Prefab.renderSelect(options, value, {name: name});
+
+ return JX.$N(
'td',
- JX.$H(this._inputTemplate));
+ {
+ className: 'owners-path-mode-control'
+ },
+ control);
+ },
- // Text input for path.
- var path_input = JX.DOM.find(typeahead_cell, 'input');
- JX.copy(
- path_input,
+ _newRepoCell: function(value) {
+ var repo_control = new JX.PHUIXFormControl()
+ .setControl('tokenizer', this._repositoryTokenizerSpec)
+ .setValue(value);
+
+ var repo_tokenizer = repo_control.getTokenizer();
+ var name = 'repo[' + this._count + ']';
+
+ function get_phid() {
+ var phids = repo_control.getValue();
+ if (!phids.length) {
+ return null;
+ }
+
+ return phids[0];
+ }
+
+ var input = JX.$N(
+ 'input',
{
- value : path_ref.path || '',
- name : 'path[' + this._count + ']'
+ type: 'hidden',
+ name: name,
+ value: get_phid()
});
- // The Typeahead requires a display div called hardpoint.
- var hardpoint = JX.DOM.find(
- typeahead_cell,
- 'div',
- 'typeahead-hardpoint');
+ repo_tokenizer.listen('change', JX.bind(this, function() {
+ this._lastRepositoryChoice = repo_tokenizer.getTokens();
- var error_display = JX.$N(
- 'div',
+ input.value = get_phid();
+ }));
+
+ var cell = JX.$N(
+ 'td',
{
- className : 'error-display validating'
+ className: 'owners-path-repo-control'
},
- 'Validating...');
+ [
+ repo_control.getRawInputNode(),
+ input
+ ]);
+
+ return {
+ cell: cell,
+ tokenizer: repo_tokenizer
+ };
+ },
- var error_display_cell = JX.$N('td', {}, error_display);
+ _newPathCell: function(value) {
+ var path_cell = JX.$N(
+ 'td',
+ {
+ className: 'owners-path-path-control'
+ },
+ JX.$H(this._inputTemplate));
- var exclude = JX.Prefab.renderSelect(
- {'0' : 'Include', '1' : 'Exclude'},
- path_ref.excluded,
- {name : 'exclude[' + this._count + ']'});
- var exclude_cell = JX.$N('td', {}, exclude);
+ var path_input = JX.DOM.find(path_cell, 'input');
- var row = this._rowManager.addRow(
- [exclude_cell, repo_cell, typeahead_cell, error_display_cell]);
+ JX.copy(
+ path_input,
+ {
+ value: value || '',
+ name: 'path[' + this._count + ']'
+ });
- new JX.PathTypeahead({
- repositoryDefaultPaths : this._repositoryDefaultPaths,
- repo_select : repo_select,
- path_input : path_input,
- hardpoint : hardpoint,
- error_display : error_display,
- completeURI : this._completeURI,
- validateURI : this._validateURI}).start();
+ var hardpoint = JX.DOM.find(
+ path_cell,
+ 'div',
+ 'typeahead-hardpoint');
- this._count++;
- return row;
+ return {
+ cell: path_cell,
+ input: path_input,
+ hardpoint: hardpoint
+ };
},
- _onaddpath : function(e) {
- e.kill();
- this.addPath();
- },
+ _newIconCell: function() {
+ var cell = JX.$N(
+ 'td',
+ {
+ className: 'owners-path-icon-control'
+ });
- /**
- * Helper to build the options for the repository choice dropdown.
- */
- _buildRepositoryOptions : function(selected) {
- var repos = this._repositories;
- var result = [];
- for (var k in repos) {
- var attr = {
- value : k,
- selected : (selected == k)
- };
- result.push(JX.$N('option', attr, repos[k]));
- }
- return result;
+ return {
+ cell: cell
+ };
}
+
}
+
});
diff --git a/webroot/rsrc/js/core/Notification.js b/webroot/rsrc/js/core/Notification.js
index 13e6cc47a..67e1e5c91 100644
--- a/webroot/rsrc/js/core/Notification.js
+++ b/webroot/rsrc/js/core/Notification.js
@@ -1,255 +1,268 @@
/**
* @requires javelin-install
* javelin-dom
* javelin-stratcom
* javelin-util
* phabricator-notification-css
* @provides phabricator-notification
* @javelin
*/
/**
* Show a notification popup on screen. Usage:
*
* var n = new JX.Notification()
* .setContent('click me!');
* n.listen('activate', function(e) { alert('you clicked!'); });
* n.show();
*
*/
JX.install('Notification', {
events : ['activate', 'close'],
members : {
_container : null,
_visible : false,
_hideTimer : null,
_duration : 12000,
_asDesktop : false,
+ _asWeb : true,
_key : null,
_title : null,
_body : null,
_href : null,
_icon : null,
show : function() {
var self = JX.Notification;
if (!this._visible) {
this._visible = true;
self._show(this);
this._updateTimer();
}
if (self.supportsDesktopNotifications() &&
self.desktopNotificationsEnabled() &&
this._asDesktop) {
// Note: specifying "tag" means that notifications with matching
// keys will aggregate.
var n = new window.Notification(this._title, {
icon: this._icon,
body: this._body,
tag: this._key,
});
n.onclick = JX.bind(n, function (href) {
this.close();
window.focus();
if (href) {
JX.$U(href).go();
}
}, this._href);
// Note: some OS / browsers do this automagically; make the behavior
// happen everywhere.
setTimeout(n.close.bind(n), this._duration);
}
return this;
},
hide : function() {
if (this._visible) {
this._visible = false;
var self = JX.Notification;
self._hide(this);
this._updateTimer();
}
return this;
},
alterClassName : function(name, enable) {
JX.DOM.alterClass(this._getContainer(), name, enable);
return this;
},
setContent : function(content) {
JX.DOM.setContent(this._getContainer(), content);
return this;
},
+ setShowAsWebNotification: function(mode) {
+ this._asWeb = mode;
+ return this;
+ },
+
setShowAsDesktopNotification : function(mode) {
this._asDesktop = mode;
return this;
},
setTitle : function(title) {
this._title = title;
return this;
},
setBody : function(body) {
this._body = body;
return this;
},
setHref : function(href) {
this._href = href;
return this;
},
setKey : function(key) {
this._key = key;
return this;
},
setIcon : function(icon) {
this._icon = icon;
return this;
},
/**
* Set duration before the notification fades away, in milliseconds. If set
* to 0, the notification persists until dismissed.
*
* @param int Notification duration, in milliseconds.
* @return this
*/
setDuration : function(milliseconds) {
this._duration = milliseconds;
this._updateTimer(false);
return this;
},
_updateTimer : function() {
if (this._hideTimer) {
clearTimeout(this._hideTimer);
this._hideTimer = null;
}
if (this._visible && this._duration) {
this._hideTimer = setTimeout(JX.bind(this, this.hide), this._duration);
}
},
_getContainer : function() {
if (!this._container) {
this._container = JX.$N(
'div',
{
className: 'jx-notification',
sigil: 'jx-notification'
});
}
return this._container;
}
},
statics : {
supportsDesktopNotifications : function () {
return 'Notification' in window;
},
desktopNotificationsEnabled : function () {
return window.Notification.permission === 'granted';
},
_container : null,
_listening : false,
_active : [],
_show : function(notification) {
var self = JX.Notification;
self._installListener();
self._active.push(notification);
self._redraw();
},
_hide : function(notification) {
var self = JX.Notification;
for (var ii = 0; ii < self._active.length; ii++) {
if (self._active[ii] === notification) {
notification.invoke('close');
self._active.splice(ii, 1);
break;
}
}
self._redraw();
},
_installListener : function() {
var self = JX.Notification;
if (self._listening) {
return;
} else {
self._listening = true;
}
JX.Stratcom.listen(
'click',
'jx-notification',
function(e) {
// NOTE: Don't kill the event since the user might have clicked a
// link, and we want to follow the link if they did. Instead, invoke
// the activate event for the active notification and dismiss it if it
// isn't handled.
var target = e.getNode('jx-notification');
for (var ii = 0; ii < self._active.length; ii++) {
var n = self._active[ii];
if (n._getContainer() === target) {
var activation = n.invoke('activate');
if (!activation.getPrevented()) {
n.hide();
}
return;
}
}
});
},
_redraw : function() {
var self = JX.Notification;
if (!self._active.length) {
if (self._container) {
JX.DOM.remove(self._container);
self._container = null;
}
return;
}
if (!self._container) {
self._container = JX.$N(
'div',
{
className: 'jx-notification-container'
});
document.body.appendChild(self._container);
}
// Show only a limited number of notifications at once.
var limit = 5;
var notifications = [];
for (var ii = 0; ii < self._active.length; ii++) {
+
+ // Don't render this notification if it's not configured to show as
+ // a web notification.
+ if (!self._active[ii]._asWeb) {
+ continue;
+ }
+
notifications.push(self._active[ii]._getContainer());
if (!(--limit)) {
break;
}
}
JX.DOM.setContent(self._container, notifications);
}
}
});
diff --git a/webroot/rsrc/js/core/behavior-error-log.js b/webroot/rsrc/js/core/behavior-error-log.js
deleted file mode 100644
index 24f3f6114..000000000
--- a/webroot/rsrc/js/core/behavior-error-log.js
+++ /dev/null
@@ -1,19 +0,0 @@
-/**
- * @provides javelin-behavior-error-log
- * @requires javelin-dom
- */
-
-/* exported show_details */
-
-var current_details = null;
-
-function show_details(row) {
- var node = JX.$('row-details-' + row);
-
- if (current_details !== null) {
- JX.$('row-details-' + current_details).style.display = 'none';
- }
-
- node.style.display = 'block';
- current_details = row;
-}
diff --git a/webroot/rsrc/js/core/behavior-lightbox-attachments.js b/webroot/rsrc/js/core/behavior-lightbox-attachments.js
index b9ea0dd8d..af134f9b0 100644
--- a/webroot/rsrc/js/core/behavior-lightbox-attachments.js
+++ b/webroot/rsrc/js/core/behavior-lightbox-attachments.js
@@ -1,390 +1,392 @@
/**
* @provides javelin-behavior-lightbox-attachments
* @requires javelin-behavior
* javelin-stratcom
* javelin-dom
* javelin-mask
* javelin-util
* phuix-icon-view
* phabricator-busy
*/
-JX.behavior('lightbox-attachments', function (config) {
+JX.behavior('lightbox-attachments', function() {
+
+ var lightbox = null;
- var lightbox = null;
var prev = null;
var next = null;
var shown = false;
- var downloadForm = JX.$H(config.downloadForm).getFragment().firstChild;
- var lightbox_id = config.lightbox_id;
function _toggleComment(e) {
e.kill();
shown = !shown;
- JX.DOM.alterClass(JX.$(lightbox_id), 'comment-panel-open', shown);
+ JX.DOM.alterClass(lightbox, 'comment-panel-open', shown);
}
function markCommentsLoading(loading) {
var frame = JX.$('lightbox-comment-frame');
JX.DOM.alterClass(frame, 'loading', loading);
}
function onLoadCommentsResponse(r) {
var frame = JX.$('lightbox-comment-frame');
JX.DOM.setContent(frame, JX.$H(r));
markCommentsLoading(false);
}
function loadComments(phid) {
markCommentsLoading(true);
var uri = '/file/thread/' + phid + '/';
new JX.Workflow(uri)
.setHandler(onLoadCommentsResponse)
.start();
}
function loadLightBox(e) {
if (!e.isNormalClick()) {
return;
}
+ // If you click the "Download" link inside an embedded file element,
+ // don't lightbox the file. But do lightbox when the user clicks an
+ // "<img />" inside an "<a />".
+ if (e.getNode('tag:a') && !e.getNode('tag:img')) {
+ return;
+ }
+
e.kill();
+ activateLightbox(e.getNode('lightboxable'));
+ }
+
+ function activateLightbox(target) {
var mainFrame = JX.$('main-page-frame');
var links = JX.DOM.scry(mainFrame, '*', 'lightboxable');
var phids = {};
var data;
for (var i = 0; i < links.length; i++) {
data = JX.Stratcom.getData(links[i]);
phids[data.phid] = links[i];
}
// Now that we have the big picture phid situation sorted out, figure
// out how the actual node the user clicks fits into that big picture
// and build some pretty UI to show the attachment.
- var target = e.getNode('lightboxable');
var target_data = JX.Stratcom.getData(target);
var total = JX.keys(phids).length;
var current = 1;
var past_target = false;
for (var phid in phids) {
if (past_target) {
next = phids[phid];
break;
} else if (phid == target_data.phid) {
past_target = true;
} else {
prev = phids[phid];
current++;
}
}
var img_uri = '';
var img = '';
var extra_status = '';
- // for now, this conditional is always true
- // revisit if / when we decide to add non-images to lightbox view
+
if (target_data.viewable) {
img_uri = target_data.uri;
var alt_name = '';
if (typeof target_data.name != 'undefined') {
alt_name = target_data.name;
}
img =
JX.$N('img',
{
className : 'loading',
alt : alt_name
}
);
} else {
var imgIcon = new JX.PHUIXIconView()
.setIcon(target_data.icon + ' phui-lightbox-file-icon')
.getNode();
var nameElement =
JX.$N('div',
{
className : 'attachment-name'
},
target_data.name
);
img =
JX.$N('a',
{
className : 'lightbox-icon-frame',
sigil : 'lightbox-download-submit',
- href : '#',
+ href : target_data.dUri,
},
[ imgIcon, nameElement ]
);
}
var imgFrame =
JX.$N('div',
{
className : 'lightbox-image-frame',
sigil : 'lightbox-image-frame',
},
img
);
var commentFrame =
JX.$N('div',
{
className : 'lightbox-comment-frame',
id : 'lightbox-comment-frame'
}
);
var commentClass = (shown) ? 'comment-panel-open' : '';
+
lightbox =
JX.$N('div',
{
className : 'lightbox-attachment ' + commentClass,
- sigil : 'lightbox-attachment',
- id : lightbox_id
+ sigil : 'lightbox-attachment'
},
[imgFrame, commentFrame]
);
var monogram = JX.$N('strong', {}, target_data.monogram);
var m_url = JX.$N('a', { href : '/' + target_data.monogram }, monogram);
var statusSpan =
JX.$N('span',
{
className: 'lightbox-status-txt'
},
[
m_url,
current + ' / ' + total
]
);
- var downloadSpan =
- JX.$N('span',
- {
- className : 'lightbox-download'
- }
- );
+ var download_icon = new JX.PHUIXIconView()
+ .setIcon('fa-download phui-icon-circle-icon')
+ .getNode();
+
+ var download_button = JX.$N(
+ 'a',
+ {
+ className: 'lightbox-download phui-icon-circle hover-sky',
+ href: target_data.dUri
+ },
+ download_icon);
var commentIcon = new JX.PHUIXIconView()
.setIcon('fa-comments phui-icon-circle-icon')
.getNode();
var commentButton =
JX.$N('a',
{
className : 'lightbox-comment phui-icon-circle hover-sky',
href : '#',
sigil : 'lightbox-comment'
},
commentIcon
);
+
var closeIcon = new JX.PHUIXIconView()
.setIcon('fa-times phui-icon-circle-icon')
.getNode();
var closeButton =
JX.$N('a',
{
className : 'lightbox-close phui-icon-circle hover-red',
href : '#'
},
closeIcon);
+
var statusHTML =
JX.$N('div',
{
className : 'lightbox-status'
},
- [statusSpan, closeButton, commentButton, downloadSpan]
+ [statusSpan, closeButton, commentButton, download_button]
);
JX.DOM.appendContent(lightbox, statusHTML);
JX.DOM.listen(closeButton, 'click', null, closeLightBox);
var leftIcon = '';
if (next) {
var r_icon = new JX.PHUIXIconView()
.setIcon('fa-angle-right')
.setColor('lightgreytext')
.getNode();
leftIcon =
JX.$N('a',
{
className : 'lightbox-right',
href : '#'
},
r_icon
);
JX.DOM.listen(leftIcon,
'click',
null,
JX.bind(null, loadAnotherLightBox, next)
);
}
JX.DOM.appendContent(lightbox, leftIcon);
var rightIcon = '';
if (prev) {
var l_icon = new JX.PHUIXIconView()
.setIcon('fa-angle-left')
.setColor('lightgreytext')
.getNode();
rightIcon =
JX.$N('a',
{
className : 'lightbox-left',
href : '#'
},
l_icon
);
JX.DOM.listen(rightIcon,
'click',
null,
JX.bind(null, loadAnotherLightBox, prev)
);
}
JX.DOM.appendContent(lightbox, rightIcon);
JX.DOM.alterClass(document.body, 'lightbox-attached', true);
JX.Mask.show('jx-dark-mask');
- downloadForm.action = target_data.dUri;
- downloadSpan.appendChild(downloadForm);
-
document.body.appendChild(lightbox);
if (img_uri) {
JX.Busy.start();
img.onload = function() {
JX.DOM.alterClass(img, 'loading', false);
JX.Busy.done();
};
img.src = img_uri;
}
loadComments(target_data.phid);
}
// TODO - make this work with KeyboardShortcut, which means
// making an uninstall / de-register for KeyboardShortcut
function lightBoxHandleKeyDown(e) {
if (!lightbox) {
return;
}
var raw = e.getRawEvent();
if (raw.altKey || raw.ctrlKey || raw.metaKey) {
return;
}
if (JX.Stratcom.pass()) {
return;
}
var handler = JX.bag;
switch (e.getSpecialKey()) {
case 'esc':
handler = closeLightBox;
break;
case 'right':
if (next) {
handler = JX.bind(null, loadAnotherLightBox, next);
}
break;
case 'left':
if (prev) {
handler = JX.bind(null, loadAnotherLightBox, prev);
}
break;
}
return handler(e);
}
function closeLightBox(e) {
if (!lightbox) {
return;
}
e.prevent();
JX.DOM.remove(lightbox);
JX.Mask.hide();
JX.DOM.alterClass(document.body, 'lightbox-attached', false);
lightbox = null;
prev = null;
next = null;
}
function loadAnotherLightBox(el, e) {
if (!el) {
return;
}
e.prevent();
closeLightBox(e);
- el.click();
+
+ activateLightbox(el);
}
// Only look for lightboxable inside the main page, not other lightboxes.
JX.DOM.listen(
JX.$('main-page-frame'),
'click',
['lightboxable'],
loadLightBox);
JX.Stratcom.listen(
'keydown',
null,
lightBoxHandleKeyDown);
// When the user clicks the background, close the lightbox.
JX.Stratcom.listen(
'click',
'lightbox-image-frame',
function (e) {
if (!lightbox) {
return;
}
if (e.getTarget() != e.getNode('lightbox-image-frame')) {
// Don't close if they clicked some other element, like the image
// itself or the next/previous arrows.
return;
}
closeLightBox(e);
e.kill();
});
JX.Stratcom.listen(
'click',
'lightbox-comment',
_toggleComment);
var _sendMessage = function(e) {
e.kill();
var form = e.getNode('tag:form');
JX.Workflow.newFromForm(form)
.setHandler(onLoadCommentsResponse)
.start();
};
JX.Stratcom.listen(
['submit', 'didSyntheticSubmit'],
'lightbox-comment-form',
_sendMessage);
- var _startDownload = function(e) {
- e.kill();
- var form = JX.$('lightbox-download-form');
- form.submit();
- };
-
var _startPageDownload = function(e) {
e.kill();
var form = e.getNode('tag:form');
form.submit();
};
- JX.Stratcom.listen(
- 'click',
- 'lightbox-download-submit',
- _startDownload);
-
JX.Stratcom.listen(
'click',
'embed-download-form',
_startPageDownload);
});
diff --git a/webroot/rsrc/js/core/behavior-line-linker.js b/webroot/rsrc/js/core/behavior-line-linker.js
index 0a651d6ca..8a68cec84 100644
--- a/webroot/rsrc/js/core/behavior-line-linker.js
+++ b/webroot/rsrc/js/core/behavior-line-linker.js
@@ -1,95 +1,169 @@
/**
* @provides javelin-behavior-phabricator-line-linker
* @requires javelin-behavior
* javelin-stratcom
* javelin-dom
* javelin-history
*/
JX.behavior('phabricator-line-linker', function() {
var origin = null;
var target = null;
var root = null;
+ var highlighted = null;
var editor_link = null;
try {
editor_link = JX.$('editor_link');
} catch (ex) {
// Ignore.
}
function getRowNumber(tr) {
- var th = JX.DOM.find(tr, 'th', 'phabricator-source-line');
+ var th = tr.firstChild;
+
+ // If the "<th />" tag contains an "<a />" with "data-n" that we're using
+ // to prevent copy/paste of line numbers, use that.
+ if (th.firstChild) {
+ var line = th.firstChild.getAttribute('data-n');
+ if (line) {
+ return line;
+ }
+ }
+
return +(th.textContent || th.innerText);
}
JX.Stratcom.listen(
- 'mousedown',
- 'phabricator-source-line',
+ ['click', 'mousedown'],
+ ['phabricator-source', 'tag:tr', 'tag:th', 'tag:a'],
function(e) {
if (!e.isNormalMouseEvent()) {
return;
}
- origin = e.getNode('tag:tr');
- target = origin;
- root = e.getNode('phabricator-source');
- e.kill();
- });
- JX.Stratcom.listen(
- 'click',
- 'phabricator-source-line',
- function(e) {
+ // Make sure the link we clicked is actually a line number in a source
+ // table, not some kind of link in some element embedded inside the
+ // table. The row's immediate ancestor table needs to be the table with
+ // the "phabricator-source" sigil.
+
+ var row = e.getNode('tag:tr');
+ var table = e.getNode('phabricator-source');
+ if (JX.DOM.findAbove(row, 'table') !== table) {
+ return;
+ }
+
+ var number = getRowNumber(row);
+ if (!number) {
+ return;
+ }
+
e.kill();
+
+ // If this is a click event, kill it. We handle mousedown and mouseup
+ // instead.
+ if (e.getType() === 'click') {
+ return;
+ }
+
+ origin = row;
+ target = origin;
+
+ root = table;
});
var highlight = function(e) {
- if (!origin || e.getNode('phabricator-source') !== root) {
+ if (!origin) {
+ return;
+ }
+
+ if (e.getNode('phabricator-source') !== root) {
return;
}
target = e.getNode('tag:tr');
- var highlighting = false;
- var source = null;
- var trs = JX.DOM.scry(root, 'tr');
- for (var i = 0; i < trs.length; i++) {
- if (!highlighting && (trs[i] === origin || trs[i] === target)) {
- highlighting = true;
- source = trs[i];
+ var min;
+ var max;
+
+ // NOTE: We're using position to figure out which order these rows are in,
+ // not row numbers. We do this because Harbormaster build logs may have
+ // multiple rows with the same row number.
+
+ if (JX.$V(origin).y <= JX.$V(target).y) {
+ min = origin;
+ max = target;
+ } else {
+ min = target;
+ max = origin;
+ }
+
+ // If we haven't changed highlighting, we don't have a list of highlighted
+ // nodes yet. Assume every row is highlighted.
+ var ii;
+ if (highlighted === null) {
+ highlighted = [];
+ var rows = JX.DOM.scry(root, 'tr');
+ for (ii = 0; ii < rows.length; ii++) {
+ highlighted.push(rows[ii]);
}
- JX.DOM.alterClass(trs[i], 'phabricator-source-highlight', highlighting);
- if (trs[i] === (source === origin ? target : origin)) {
- highlighting = false;
+ }
+
+ // Unhighlight any existing highlighted rows.
+ for (ii = 0; ii < highlighted.length; ii++) {
+ JX.DOM.alterClass(highlighted[ii], 'phabricator-source-highlight', false);
+ }
+ highlighted = [];
+
+ // Highlight the newly selected rows.
+ var cursor = min;
+ while (true) {
+ JX.DOM.alterClass(cursor, 'phabricator-source-highlight', true);
+ highlighted.push(cursor);
+
+ if (cursor === max) {
+ break;
}
+
+ cursor = cursor.nextSibling;
}
};
JX.Stratcom.listen('mouseover', 'phabricator-source', highlight);
JX.Stratcom.listen(
'mouseup',
null,
function(e) {
if (!origin) {
return;
}
highlight(e);
+ e.kill();
var o = getRowNumber(origin);
var t = getRowNumber(target);
- var lines = (o == t ? o : Math.min(o, t) + '-' + Math.max(o, t));
- var th = JX.DOM.find(origin, 'th', 'phabricator-source-line');
- var uri = JX.DOM.find(th, 'a').href;
- uri = uri.replace(/(.*\$)\d+/, '$1' + lines);
+ var uri = JX.Stratcom.getData(root).uri;
+
+ if (!uri) {
+ uri = ('' + window.location).split('$')[0];
+ }
+
origin = null;
target = null;
- e.kill();
+ root = null;
+
+ var lines = (o == t ? o : Math.min(o, t) + '-' + Math.max(o, t));
+ uri = uri + '$' + lines;
+
JX.History.replace(uri);
- if (editor_link.href) {
- var editdata = JX.Stratcom.getData(editor_link);
- editor_link.href = editdata.link_template.replace('%25l', o);
+
+ if (editor_link) {
+ if (editor_link.href) {
+ var editdata = JX.Stratcom.getData(editor_link);
+ editor_link.href = editdata.link_template.replace('%25l', o);
+ }
}
});
});
diff --git a/webroot/rsrc/js/core/behavior-phabricator-nav.js b/webroot/rsrc/js/core/behavior-phabricator-nav.js
index e37680abf..50d8e1c82 100644
--- a/webroot/rsrc/js/core/behavior-phabricator-nav.js
+++ b/webroot/rsrc/js/core/behavior-phabricator-nav.js
@@ -1,164 +1,197 @@
/**
* @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;
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: JX.$V(drag).x
},
{
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: JX.$V(drag).x
+ })
+ .send();
});
- function resetdrag() {
+ var saved_width = config.width;
+ function savedrag() {
+ saved_width = JX.$V(drag).x;
+
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);
- resetdrag();
+
+ if (!collapsed) {
+ restoredrag();
+ }
+
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') {
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();
// - Navigation Reset ----------------------------------------------------------
JX.Stratcom.listen('phabricator-device-change', null, function() {
resetdrag();
onresize();
});
});
diff --git a/webroot/rsrc/js/core/behavior-redirect.js b/webroot/rsrc/js/core/behavior-redirect.js
new file mode 100644
index 000000000..4f6e234c0
--- /dev/null
+++ b/webroot/rsrc/js/core/behavior-redirect.js
@@ -0,0 +1,9 @@
+/**
+ * @provides javelin-behavior-redirect
+ * @requires javelin-behavior
+ * javelin-uri
+ */
+
+JX.behavior('redirect', function(config) {
+ JX.$U(config.uri).go();
+});
diff --git a/webroot/rsrc/js/core/behavior-remarkup-load-image.js b/webroot/rsrc/js/core/behavior-remarkup-load-image.js
new file mode 100644
index 000000000..a13f3b0d7
--- /dev/null
+++ b/webroot/rsrc/js/core/behavior-remarkup-load-image.js
@@ -0,0 +1,45 @@
+/**
+ * @provides javelin-behavior-remarkup-load-image
+ * @requires javelin-behavior
+ * javelin-request
+ */
+
+JX.behavior('remarkup-load-image', function(config) {
+
+ function get_node() {
+ try {
+ return JX.$(config.imageID);
+ } catch (ex) {
+ return null;
+ }
+ }
+
+ function onload(r) {
+ var node = get_node();
+ if (!node) {
+ return;
+ }
+
+ node.src = r.imageURI;
+ }
+
+ function onerror(r) {
+ var node = get_node();
+ if (!node) {
+ return;
+ }
+
+ var error = JX.$N(
+ 'div',
+ {
+ className: 'phabricator-remarkup-image-error'
+ },
+ r.info);
+
+ JX.DOM.replace(node, error);
+ }
+
+ var request = new JX.Request(config.uri, onload);
+ request.listen('error', onerror);
+ request.send();
+});
diff --git a/webroot/rsrc/js/core/behavior-search-typeahead.js b/webroot/rsrc/js/core/behavior-search-typeahead.js
index ddcc2e6aa..0f5b4d163 100644
--- a/webroot/rsrc/js/core/behavior-search-typeahead.js
+++ b/webroot/rsrc/js/core/behavior-search-typeahead.js
@@ -1,265 +1,266 @@
/**
* @provides javelin-behavior-phabricator-search-typeahead
* @requires javelin-behavior
* javelin-typeahead-ondemand-source
* javelin-typeahead
* javelin-dom
* javelin-uri
* javelin-util
* javelin-stratcom
* phabricator-prefab
* phuix-icon-view
*/
JX.behavior('phabricator-search-typeahead', function(config) {
var datasource = new JX.TypeaheadOnDemandSource(config.src);
function transform(object) {
object = JX.Prefab.transformDatasourceResults(object);
var attr = {
className: 'phabricator-main-search-typeahead-result'
};
if (object.imageURI) {
attr.style = {backgroundImage: 'url('+object.imageURI+')'};
}
var icon = null;
if (object.icon) {
icon = new JX.PHUIXIconView()
.setIcon(object.icon)
.setColor('lightgreytext')
.getNode();
icon = [icon, ' '];
}
var render = JX.$N(
'span',
attr,
[
JX.$N('span', {className: object.sprite}),
JX.$N('span', {className: 'result-name'}, object.displayName),
icon,
JX.$N('span', {className: 'result-type'}, object.type)
]);
if (object.closed) {
JX.DOM.alterClass(render, 'result-closed', true);
}
object.display = render;
return object;
}
datasource.setTransformer(transform);
var sort_handler = function(value, list, cmp) {
// First, sort all the results normally.
JX.bind(this, JX.Prefab.sortHandler, {}, value, list, cmp)();
// Now we're going to apply some special rules to order results by type,
// so applications always appear near the top, then users, etc.
var ii;
var type_order = [
'jump',
'apps',
'proj',
'user',
'repo',
+ 'wiki',
'symb',
'misc'
];
var type_map = {};
for (ii = 0; ii < type_order.length; ii++) {
type_map[type_order[ii]] = true;
}
var buckets = {};
for (ii = 0; ii < list.length; ii++) {
var item = list[ii];
var type = item.priorityType;
if (!type_map.hasOwnProperty(type)) {
type = 'misc';
}
if (!buckets.hasOwnProperty(type)) {
buckets[type] = [];
}
buckets[type].push(item);
}
// If we have more results than fit, limit each type of result to 3, so
// we show 3 applications, then 3 users, etc. For jump items, we show only
// one result.
var jj;
var results = [];
for (ii = 0; ii < type_order.length; ii++) {
var current_type = type_order[ii];
var type_list = buckets[current_type] || [];
for (jj = 0; jj < type_list.length; jj++) {
// Skip this item if:
// - it's a jump nav item, and we already have at least one jump
// nav item; or
// - we have more items than will fit in the typeahead, and this
// is the 4..Nth result of its type.
var skip = ((current_type == 'jump') && (jj >= 1)) ||
((list.length > config.limit) && (jj >= 3));
if (skip) {
continue;
}
results.push(type_list[jj]);
}
}
// Replace the list in place with the results.
list.splice.apply(list, [0, list.length].concat(results));
};
datasource.setSortHandler(JX.bind(datasource, sort_handler));
datasource.setMaximumResultCount(config.limit);
var typeahead = new JX.Typeahead(JX.$(config.id), JX.$(config.input));
typeahead.setDatasource(datasource);
typeahead.setPlaceholder(config.placeholder);
typeahead.listen('choose', function(r) {
JX.$U(r.href).go();
JX.Stratcom.context().kill();
});
typeahead.start();
JX.DOM.listen(JX.$(config.button), 'click', null, function () {
typeahead.setPlaceholder('');
typeahead.updatePlaceHolder();
});
// When the user navigates between applications, we need to update the
// input in the document, the icon on the button, and the icon in the
// menu.
JX.Stratcom.listen(
'quicksand-redraw',
null,
function(e) {
var r = e.getData().newResponse;
updateCurrentApplication(r.applicationClass, r.applicationSearchIcon);
});
var current_app_icon;
function updateCurrentApplication(app_class, app_icon) {
current_app_icon = app_icon || config.defaultApplicationIcon;
// Update the icon on the button.
var button = JX.$(config.selectorID);
var data = JX.Stratcom.getData(button);
if (data.value == config.appScope) {
updateIcon(button, data, current_app_icon);
}
// Set the hidden input to the new value.
JX.$(config.applicationID).value = app_class;
}
function updateIcon(button, data, new_icon) {
var icon = JX.DOM.find(button, 'span', 'global-search-dropdown-icon');
JX.DOM.alterClass(icon, data.icon, false);
data.icon = new_icon;
JX.DOM.alterClass(icon, data.icon, true);
}
// Implement the scope selector menu for the global search.
JX.Stratcom.listen('click', 'global-search-dropdown', function(e) {
var data = e.getNodeData('global-search-dropdown');
var button = e.getNode('global-search-dropdown');
if (data.menu) {
return;
}
e.kill();
function updateValue(spec) {
if (data.value == spec.value) {
return;
}
// Swap out the icon.
updateIcon(button, data, spec.icon);
// Update the value.
data.value = spec.value;
// Update the form input.
var frame = button.parentNode;
var input = JX.DOM.find(frame, 'input', 'global-search-dropdown-input');
input.value = data.value;
new JX.Request(config.scopeUpdateURI)
.setData({value: data.value})
.send();
}
var menu = new JX.PHUIXDropdownMenu(button)
.setAlign('left');
data.menu = menu;
menu.listen('open', function() {
var list = new JX.PHUIXActionListView();
for (var ii = 0; ii < data.items.length; ii++) {
var spec = data.items[ii];
// If this is the "Search Current Application" item and we've
// navigated to a page which sent us new information about the
// icon, update the icon so the menu reflects the icon for the
// current application.
if (spec.value == config.appScope) {
if (current_app_icon !== undefined) {
spec.icon = current_app_icon;
}
}
var item = new JX.PHUIXActionView()
.setName(spec.name)
.setIcon(spec.icon);
if (spec.value) {
if (spec.value == data.value) {
item.setSelected(true);
}
var handler = function(spec, e) {
e.prevent();
menu.close();
updateValue(spec);
};
item.setHandler(JX.bind(null, handler, spec));
} else if (spec.href) {
item.setHref(spec.href);
item.setHandler(function() { menu.close(); });
} else {
item.setLabel(true);
}
list.addItem(item);
}
menu.setContent(list.getNode());
});
menu.open();
});
});
diff --git a/webroot/rsrc/js/core/darkconsole/behavior-dark-console.js b/webroot/rsrc/js/core/darkconsole/behavior-dark-console.js
index 13949432f..a9f33d459 100644
--- a/webroot/rsrc/js/core/darkconsole/behavior-dark-console.js
+++ b/webroot/rsrc/js/core/darkconsole/behavior-dark-console.js
@@ -1,407 +1,427 @@
/**
* @provides javelin-behavior-dark-console
* @requires javelin-behavior
* javelin-stratcom
* javelin-util
* javelin-dom
* javelin-request
* phabricator-keyboard-shortcut
* phabricator-darklog
* phabricator-darkmessage
*/
JX.behavior('dark-console', function(config, statics) {
// Do first-time setup.
function setup_console() {
init_console(config.visible);
statics.selected = config.selected;
install_shortcut();
if (config.headers) {
// If the main page had profiling enabled, also enable it for any Ajax
// requests.
JX.Request.listen('open', function(r) {
for (var k in config.headers) {
r.getTransport().setRequestHeader(k, config.headers[k]);
}
});
}
// When the user clicks a tab, select it.
JX.Stratcom.listen('click', 'dark-console-tab', function(e) {
e.kill();
select_tab(e.getNodeData('dark-console-tab')['class']);
});
JX.Stratcom.listen(
'quicksand-redraw',
null,
function (e) {
var data = e.getData();
var new_console;
if (data.fromServer) {
new_console = JX.$('darkconsole');
// The correct key has to be pulled from the rendered console
statics.quicksand_key = new_console.getAttribute('data-console-key');
statics.quicksand_color =
new_console.getAttribute('data-console-color');
} else {
// we need to add a console holder back in since we blew it away
new_console = JX.$N(
'div',
{ id : 'darkconsole', class : 'dark-console' });
JX.DOM.prependContent(
JX.$('phabricator-standard-page-body'),
new_console);
}
JX.DOM.replace(new_console, statics.root);
});
return statics.root;
}
function init_console(visible) {
statics.root = JX.$('darkconsole');
statics.req = {all: {}, current: null};
statics.tab = {all: {}, current: null};
statics.el = {};
statics.el.reqs = JX.$N('div', {className: 'dark-console-requests'});
statics.root.appendChild(statics.el.reqs);
statics.el.tabs = JX.$N('div', {className: 'dark-console-tabs'});
statics.root.appendChild(statics.el.tabs);
statics.el.panel = JX.$N('div', {className: 'dark-console-panel'});
statics.root.appendChild(statics.el.panel);
statics.el.load = JX.$N('div', {className: 'dark-console-load'});
statics.root.appendChild(statics.el.load);
statics.cache = {};
statics.visible = visible;
return statics.root;
}
// Add a new request to the console (initial page load, or new Ajax response).
function add_request(config) {
// Ignore DarkConsole data requests.
if (config.uri.match(new RegExp('^/~/data/'))) {
return;
}
var attr = {
className: 'dark-console-request',
sigil: 'dark-console-request',
title: config.uri,
meta: config,
href: '#'
};
var link = JX.$N('a', attr, [get_bullet(config.color), ' ', config.uri]);
statics.el.reqs.appendChild(link);
statics.req.all[config.key] = link;
// When the user clicks a request, select it.
JX.DOM.listen(
link,
'click',
'dark-console-request',
function(e) {
e.kill();
select_request(e.getNodeData('dark-console-request').key);
});
if (!statics.req.current) {
select_request(config.key);
}
}
function get_bullet(color) {
if (!color) {
return null;
}
return JX.$N('span', {style: {color: color}}, '\u2022');
}
// Select a request (on load, or when the user clicks one).
function select_request(key) {
if (statics.req.current) {
JX.DOM.alterClass(
statics.req.all[statics.req.current],
'dark-selected',
false);
}
statics.req.current = key;
JX.DOM.alterClass(
statics.req.all[statics.req.current],
'dark-selected',
true);
if (statics.visible) {
draw_request(key);
}
}
// After the user selects a request, draw its tabs.
function draw_request(key) {
var cache = statics.cache;
if (cache[key]) {
render_request(key);
return;
}
new JX.Request(
'/~/data/' + key + '/',
function(r) {
cache[key] = r;
if (statics.req.current == key) {
render_request(key);
}
})
.send();
show_loading();
}
// Show the loading indicator.
function show_loading() {
JX.DOM.hide(statics.el.tabs);
JX.DOM.hide(statics.el.panel);
JX.DOM.show(statics.el.load);
}
// Hide the loading indicator.
function hide_loading() {
JX.DOM.show(statics.el.tabs);
JX.DOM.show(statics.el.panel);
JX.DOM.hide(statics.el.load);
}
function render_request(key) {
var data = statics.cache[key];
statics.tab.all = {};
var links = [];
var first = null;
for (var ii = 0; ii < data.tabs.length; ii++) {
var tab = data.tabs[ii];
var attr = {
className: 'dark-console-tab',
sigil: 'dark-console-tab',
meta: tab,
href: '#'
};
var link = JX.$N('a', attr, [get_bullet(tab.color), ' ', tab.name]);
links.push(link);
statics.tab.all[tab['class']] = link;
first = first || tab['class'];
}
JX.DOM.setContent(statics.el.tabs, links);
if (statics.tab.current in statics.tab.all) {
select_tab(statics.tab.current);
} else if (statics.selected in statics.tab.all) {
select_tab(statics.selected);
} else {
select_tab(first);
}
hide_loading();
}
function select_tab(tclass) {
var tabs = statics.tab;
if (tabs.current) {
JX.DOM.alterClass(tabs.current, 'dark-selected', false);
}
tabs.current = tabs.all[tclass];
JX.DOM.alterClass(tabs.current, 'dark-selected', true);
if (tclass != statics.selected) {
// Save user preference.
new JX.Request('/~/', JX.bag)
.setData({ tab : tclass })
.send();
statics.selected = tclass;
}
draw_panel();
}
function draw_panel() {
var data = statics.cache[statics.req.current];
var tclass = JX.Stratcom.getData(statics.tab.current)['class'];
var html = data.panel[tclass];
var div = JX.$N('div', {className: 'dark-console-panel-core'}, JX.$H(html));
JX.DOM.setContent(statics.el.panel, div);
var params = {
panel: tclass
};
JX.Stratcom.invoke('darkconsole.draw', null, params);
}
function install_shortcut() {
var desc = 'Toggle visibility of DarkConsole.';
new JX.KeyboardShortcut('`', desc)
.setHandler(function() {
statics.visible = !statics.visible;
if (statics.visible) {
JX.DOM.show(statics.root);
if (statics.req.current) {
draw_request(statics.req.current);
}
} else {
JX.DOM.hide(statics.root);
}
// Save user preference.
new JX.Request('/~/', JX.bag)
.setData({visible: statics.visible ? 1 : 0})
.send();
// Force resize listeners to take effect.
JX.Stratcom.invoke('resize');
})
.register();
}
statics.root = statics.root || setup_console();
if (config.quicksand && statics.quicksand_key) {
config.key = statics.quicksand_key;
config.color = statics.quicksand_color;
statics.quicksand_key = null;
statics.quicksand_color = null;
}
config.key = config.key || statics.root.getAttribute('data-console-key');
if (!('color' in config)) {
config.color = statics.root.getAttribute('data-console-color');
}
add_request(config);
/* -( Realtime Panel )----------------------------------------------------- */
if (!statics.realtime) {
statics.realtime = true;
var realtime_log = new JX.DarkLog();
var realtime_id = 'dark-console-realtime-log';
JX.Stratcom.listen('darkconsole.draw', null, function(e) {
var data = e.getData();
if (data.panel != 'DarkConsoleRealtimePlugin') {
return;
}
var node = JX.$(realtime_id);
realtime_log.setNode(node);
});
// If the panel is initially visible, try rendering.
try {
var node = JX.$(realtime_id);
realtime_log.setNode(node);
} catch (exception) {
// Ignore.
}
var leader_log = function(event_name, type, is_leader, details) {
var parts = [];
if (is_leader === true) {
parts.push('+');
} else if (is_leader === false) {
parts.push('-');
} else {
parts.push('~');
}
parts.push('[Leader/' + event_name + ']');
if (type) {
parts.push('(' + type + ')');
}
if (details) {
parts.push(details);
}
parts = parts.join(' ');
var message = new JX.DarkMessage()
.setMessage(parts);
realtime_log.addMessage(message);
};
JX.Leader.listen('onReceiveBroadcast', function(message, is_leader) {
var json = JX.JSON.stringify(message.data);
if (message.type == 'aphlict.status') {
if (message.data == 'closed') {
var ws = JX.Aphlict.getInstance().getWebsocket();
if (ws) {
var delay = ws.getReconnectDelay();
json += ' [Reconnect: ' + delay + 'ms]';
}
}
}
leader_log('onReceiveBroadcast', message.type, is_leader, json);
});
JX.Leader.listen('onBecomeLeader', function() {
leader_log('onBecomeLeader');
});
var action_log = function(action) {
var message = new JX.DarkMessage()
.setMessage('> ' + action);
realtime_log.addMessage(message);
};
JX.Stratcom.listen('click', 'dark-console-realtime-action', function(e) {
var node = e.getNode('dark-console-realtime-action');
var data = JX.Stratcom.getData(node);
action_log(data.label);
var action = data.action;
switch (action) {
case 'reconnect':
var ws = JX.Aphlict.getInstance().getWebsocket();
if (ws) {
ws.reconnect();
}
break;
case 'replay':
JX.Aphlict.getInstance().replay();
break;
case 'repaint':
JX.Aphlict.getInstance().reconnect();
break;
}
});
}
+ if (!statics.expand) {
+ statics.expand = true;
+
+ var current_details = null;
+ JX.Stratcom.listen('click', 'darkconsole-expand', function(e) {
+ e.kill();
+
+ if (current_details) {
+ current_details.style.display = 'none';
+ current_details = null;
+ }
+
+ var id = e.getNodeData('darkconsole-expand').expandID;
+ var node = JX.$(id);
+
+ node.style.display = 'block';
+ current_details = node;
+ });
+ }
+
});
diff --git a/webroot/rsrc/js/phuix/PHUIXActionView.js b/webroot/rsrc/js/phuix/PHUIXActionView.js
index 2db58dbdc..97e746c2c 100644
--- a/webroot/rsrc/js/phuix/PHUIXActionView.js
+++ b/webroot/rsrc/js/phuix/PHUIXActionView.js
@@ -1,180 +1,195 @@
/**
* @provides phuix-action-view
* @requires javelin-install
* javelin-dom
* javelin-util
* @javelin
*/
JX.install('PHUIXActionView', {
members: {
_node: null,
_name: null,
_icon: 'none',
+ _iconColor: null,
_disabled: false,
_label: false,
_handler: null,
_selected: false,
_divider: false,
_iconNode: null,
_nameNode: null,
setDisabled: function(disabled) {
this._disabled = disabled;
JX.DOM.alterClass(
this.getNode(),
'phabricator-action-view-disabled',
disabled);
this._buildIconNode(true);
return this;
},
+ getDisabled: function() {
+ return this._disabled;
+ },
+
setLabel: function(label) {
this._label = label;
JX.DOM.alterClass(
this.getNode(),
'phabricator-action-view-label',
label);
return this;
},
setDivider: function(divider) {
this._divider = divider;
JX.DOM.alterClass(
this.getNode(),
'phabricator-action-view-type-divider',
divider);
return this;
},
setSelected: function(selected) {
this._selected = selected;
JX.DOM.alterClass(
this.getNode(),
'phabricator-action-view-selected',
selected);
return this;
},
setName: function(name) {
this._name = name;
this._buildNameNode(true);
return this;
},
setHandler: function(handler) {
this._handler = handler;
this._buildNameNode(true);
return this;
},
setIcon: function(icon) {
this._icon = icon;
this._buildIconNode(true);
return this;
},
+ setIconColor: function(color) {
+ this._iconColor = color;
+ this._buildIconNode(true);
+ return this;
+ },
+
setHref: function(href) {
this._href = href;
this._buildNameNode(true);
return this;
},
getNode: function() {
if (!this._node) {
var classes = ['phabricator-action-view'];
if (this._href || this._handler) {
classes.push('phabricator-action-view-href');
}
if (this._icon) {
classes.push('action-has-icon');
}
var content = [
this._buildIconNode(),
this._buildNameNode()
];
var attr = {
className: classes.join(' ')
};
this._node = JX.$N('li', attr, content);
JX.Stratcom.addSigil(this._node, 'phuix-action-view');
}
return this._node;
},
_buildIconNode: function(dirty) {
if (!this._iconNode || dirty) {
var attr = {
className: [
'phui-icon-view',
'phabricator-action-view-icon',
'phui-font-fa'
].join(' ')
};
var node = JX.$N('span', attr);
var icon_class = this._icon;
if (this._disabled) {
icon_class = icon_class + ' grey';
}
+ if (this._iconColor) {
+ icon_class = icon_class + ' ' + this._iconColor;
+ }
+
JX.DOM.alterClass(node, icon_class, true);
if (this._iconNode && this._iconNode.parentNode) {
JX.DOM.replace(this._iconNode, node);
}
this._iconNode = node;
}
return this._iconNode;
},
_buildNameNode: function(dirty) {
if (!this._nameNode || dirty) {
var attr = {
className: 'phabricator-action-view-item'
};
var href = this._href;
if (!href && this._handler) {
href = '#';
}
if (href) {
attr.href = href;
}
var tag = href ? 'a' : 'span';
var node = JX.$N(tag, attr, this._name);
JX.DOM.listen(node, 'click', null, JX.bind(this, this._onclick));
if (this._nameNode && this._nameNode.parentNode) {
JX.DOM.replace(this._nameNode, node);
}
this._nameNode = node;
}
return this._nameNode;
},
_onclick: function(e) {
if (this._handler) {
this._handler(e);
}
}
}
});
diff --git a/webroot/rsrc/js/phuix/PHUIXAutocomplete.js b/webroot/rsrc/js/phuix/PHUIXAutocomplete.js
index a7f719dab..f46e7666e 100644
--- a/webroot/rsrc/js/phuix/PHUIXAutocomplete.js
+++ b/webroot/rsrc/js/phuix/PHUIXAutocomplete.js
@@ -1,739 +1,766 @@
/**
* @provides phuix-autocomplete
* @requires javelin-install
* javelin-dom
* phuix-icon-view
* phabricator-prefab
*/
JX.install('PHUIXAutocomplete', {
construct: function() {
this._map = {};
this._datasources = {};
this._listNodes = [];
this._resultMap = {};
},
members: {
_area: null,
_active: false,
_cursorHead: null,
_cursorTail: null,
_pixelHead: null,
_pixelTail: null,
_map: null,
_datasource: null,
_datasources: null,
_value: null,
_node: null,
_echoNode: null,
_listNode: null,
_promptNode: null,
_focus: null,
_focusRef: null,
_listNodes: null,
_x: null,
_y: null,
_visible: false,
_resultMap: null,
setArea: function(area) {
this._area = area;
return this;
},
addAutocomplete: function(code, spec) {
this._map[code] = spec;
return this;
},
start: function() {
var area = this._area;
JX.DOM.listen(area, 'keypress', null, JX.bind(this, this._onkeypress));
JX.DOM.listen(
area,
['click', 'keyup', 'keydown', 'keypress'],
null,
JX.bind(this, this._update));
var select = JX.bind(this, this._onselect);
JX.DOM.listen(this._getNode(), 'mousedown', 'typeahead-result', select);
var device = JX.bind(this, this._ondevice);
JX.Stratcom.listen('phabricator-device-change', null, device);
// When the user clicks away from the textarea, deactivate.
var deactivate = JX.bind(this, this._deactivate);
JX.DOM.listen(area, 'blur', null, deactivate);
},
_getSpec: function() {
return this._map[this._active];
},
_ondevice: function() {
if (JX.Device.getDevice() != 'desktop') {
this._deactivate();
}
},
_activate: function(code) {
if (JX.Device.getDevice() != 'desktop') {
return;
}
if (!this._map[code]) {
return;
}
var area = this._area;
var range = JX.TextAreaUtils.getSelectionRange(area);
// Check the character immediately before the trigger character. We'll
// only activate the typeahead if it's something that we think a user
// might reasonably want to autocomplete after, like a space, newline,
// or open parenthesis. For example, if a user types "alincoln@",
// the prior letter will be the last "n" in "alincoln". They are probably
// typing an email address, not a username, so we don't activate the
// autocomplete.
var head = range.start;
var prior;
if (head > 1) {
prior = area.value.substring(head - 2, head - 1);
} else {
prior = '<start>';
}
+ // If this is a repeating sequence and the previous character is the
+ // same as the one the user just typed, like "((", don't reactivate.
+ if (prior === String.fromCharCode(code)) {
+ return;
+ }
+
switch (prior) {
case '<start>':
case ' ':
case '\n':
case '\t':
case '(': // Might be "(@username, what do you think?)".
case '-': // Might be an unnumbered list.
case '.': // Might be a numbered list.
case '|': // Might be a table cell.
case '>': // Might be a blockquote.
case '!': // Might be a blockquote attribution line.
// We'll let these autocomplete.
break;
default:
// We bail out on anything else, since the user is probably not
// typing a username or project tag.
return;
}
// Get all the text on the current line. If the line only contains
// whitespace, don't activate: the user is probably typing code or a
// numbered list.
var line = area.value.substring(0, head - 1);
line = line.split('\n');
line = line[line.length - 1];
if (line.match(/^\s+$/)) {
return;
}
this._cursorHead = head;
this._cursorTail = range.end;
this._pixelHead = JX.TextAreaUtils.getPixelDimensions(
area,
range.start,
range.end);
var spec = this._map[code];
if (!this._datasources[code]) {
var datasource = new JX.TypeaheadOnDemandSource(spec.datasourceURI);
datasource.listen(
'resultsready',
JX.bind(this, this._onresults, code));
datasource.setTransformer(JX.bind(this, this._transformresult));
datasource.setSortHandler(
JX.bind(datasource, JX.Prefab.sortHandler, {}));
this._datasources[code] = datasource;
}
this._datasource = this._datasources[code];
this._active = code;
var head_icon = new JX.PHUIXIconView()
.setIcon(spec.headerIcon)
.getNode();
var head_text = spec.headerText;
var node = this._getPromptNode();
JX.DOM.setContent(node, [head_icon, head_text]);
},
_transformresult: function(fields) {
var map = JX.Prefab.transformDatasourceResults(fields);
var icon;
if (map.icon) {
icon = new JX.PHUIXIconView()
.setIcon(map.icon)
.getNode();
}
- map.display = [icon, map.displayName];
+ var display = JX.$N('span', {}, [icon, map.displayName]);
+ JX.DOM.alterClass(display, 'tokenizer-result-closed', !!map.closed);
+
+ map.display = display;
return map;
},
_deactivate: function() {
var node = this._getNode();
JX.DOM.hide(node);
this._active = false;
this._visible = false;
},
_onkeypress: function(e) {
var r = e.getRawEvent();
// NOTE: We allow events to continue with "altKey", because you need
// to press Alt to type characters like "@" on a German keyboard layout.
// The cost of misfiring autocompleters is very small since we do not
// eat the keystroke. See T10252.
if (r.metaKey || (r.ctrlKey && !r.altKey)) {
return;
}
var code = r.charCode;
if (this._map[code]) {
setTimeout(JX.bind(this, this._activate, code), 0);
}
},
_onresults: function(code, nodes, value, partial) {
// Even if these results are out of date, we still want to fill in the
// result map so we can terminate things later.
if (!partial) {
if (!this._resultMap[code]) {
this._resultMap[code] = {};
}
var hits = [];
for (var ii = 0; ii < nodes.length; ii++) {
var result = this._datasources[code].getResult(nodes[ii].rel);
if (!result) {
hits = null;
break;
}
if (!result.autocomplete || !result.autocomplete.length) {
hits = null;
break;
}
hits.push(result.autocomplete);
}
if (hits !== null) {
this._resultMap[code][value] = hits;
}
}
if (code !== this._active) {
return;
}
if (value !== this._value) {
return;
}
if (this._isTerminatedString(value)) {
if (this._hasUnrefinableResults(value)) {
this._deactivate();
return;
}
}
var list = this._getListNode();
JX.DOM.setContent(list, nodes);
this._listNodes = nodes;
var old_ref = this._focusRef;
this._clearFocus();
for (var ii = 0; ii < nodes.length; ii++) {
if (nodes[ii].rel == old_ref) {
this._setFocus(ii);
break;
}
}
if (this._focus === null && nodes.length) {
this._setFocus(0);
}
this._redraw();
},
_setFocus: function(idx) {
if (!this._listNodes[idx]) {
this._clearFocus();
return false;
}
if (this._focus !== null) {
JX.DOM.alterClass(this._listNodes[this._focus], 'focused', false);
}
this._focus = idx;
this._focusRef = this._listNodes[idx].rel;
JX.DOM.alterClass(this._listNodes[idx], 'focused', true);
return true;
},
_changeFocus: function(delta) {
if (this._focus === null) {
return false;
}
return this._setFocus(this._focus + delta);
},
_clearFocus: function() {
this._focus = null;
this._focusRef = null;
},
_onselect: function (e) {
if (!e.isNormalMouseEvent()) {
// Eat right clicks, control clicks, etc., on the results. These can
// not do anything meaningful and if we let them through they'll blur
// the field and dismiss the results.
e.kill();
return;
}
var target = e.getNode('typeahead-result');
for (var ii = 0; ii < this._listNodes.length; ii++) {
if (this._listNodes[ii] === target) {
this._setFocus(ii);
this._autocomplete();
break;
}
}
this._deactivate();
e.kill();
},
_getSuffixes: function() {
return [' ', ':', ',', ')'];
},
_getCancelCharacters: function() {
// The "." character does not cancel because of projects named
// "node.js" or "blog.mycompany.com".
- return ['#', '@', ',', '!', '?', '{', '}'];
+ var defaults = ['#', '@', ',', '!', '?', '{', '}'];
+
+ return this._map[this._active].cancel || defaults;
},
_getTerminators: function() {
return [' ', ':', ',', '.', '!', '?'];
},
_getIgnoreList: function() {
return this._map[this._active].ignore || [];
},
_isTerminatedString: function(string) {
var terminators = this._getTerminators();
for (var ii = 0; ii < terminators.length; ii++) {
var term = terminators[ii];
if (string.substring(string.length - term.length) == term) {
return true;
}
}
return false;
},
_hasUnrefinableResults: function(query) {
if (!this._resultMap[this._active]) {
return false;
}
var map = this._resultMap[this._active];
for (var ii = 1; ii < query.length; ii++) {
var prefix = query.substring(0, ii);
if (map.hasOwnProperty(prefix)) {
var results = map[prefix];
// If any prefix of the query has no results, the full query also
// has no results so we can not refine them.
if (!results.length) {
return true;
}
// If there is exactly one match and the it is a prefix of the query,
// we can safely assume the user just typed out the right result
// from memory and doesn't need to refine it.
if (results.length == 1) {
// Strip the first character off, like a "#" or "@".
var result = results[0].substring(1);
if (query.length >= result.length) {
if (query.substring(0, result.length) === result) {
return true;
}
}
}
}
}
return false;
},
_trim: function(str) {
var suffixes = this._getSuffixes();
for (var ii = 0; ii < suffixes.length; ii++) {
if (str.substring(str.length - suffixes[ii].length) == suffixes[ii]) {
str = str.substring(0, str.length - suffixes[ii].length);
}
}
return str;
},
_update: function(e) {
if (!this._active) {
return;
}
var special = e.getSpecialKey();
// Deactivate if the user types escape.
if (special == 'esc') {
this._deactivate();
e.kill();
return;
}
var area = this._area;
if (e.getType() == 'keydown') {
if (special == 'up' || special == 'down') {
var delta = (special == 'up') ? -1 : +1;
if (!this._changeFocus(delta)) {
this._deactivate();
}
e.kill();
return;
}
}
// Deactivate if the user moves the cursor to the left of the assist
// range. For example, they might press the "left" arrow to move the
// cursor to the left, or click in the textarea prior to the active
// range.
var range = JX.TextAreaUtils.getSelectionRange(area);
if (range.start < this._cursorHead) {
this._deactivate();
return;
}
if (special == 'tab' || special == 'return') {
var r = e.getRawEvent();
if (r.shiftKey && special == 'tab') {
// Don't treat "Shift + Tab" as an autocomplete action. Instead,
// let it through normally so the focus shifts to the previous
// control.
this._deactivate();
return;
}
// If the user hasn't typed any text yet after typing the character
// which can summon the autocomplete, deactivate and let the keystroke
// through. For example, we hit this when a line ends with an
// autocomplete character and the user is trying to type a newline.
if (range.start == this._cursorHead) {
this._deactivate();
return;
}
// If we autocomplete, we're done. Otherwise, just eat the event. This
// happens if you type too fast and try to tab complete before results
// load.
if (this._autocomplete()) {
this._deactivate();
}
e.kill();
return;
}
// Deactivate if the user moves the cursor to the right of the assist
// range. For example, they might click later in the document. If the user
// is pressing the "right" arrow key, they are not allowed to move the
// cursor beyond the existing end of the text range. If they are pressing
// other keys, assume they're typing and allow the tail to move forward
// one character.
var margin;
if (special == 'right') {
margin = 0;
} else {
margin = 1;
}
var tail = this._cursorTail;
if ((range.start > tail + margin) || (range.end > tail + margin)) {
this._deactivate();
return;
}
this._cursorTail = Math.max(this._cursorTail, range.end);
var text = area.value.substring(
this._cursorHead,
this._cursorTail);
- this._value = text;
-
var pixels = JX.TextAreaUtils.getPixelDimensions(
area,
range.start,
range.end);
var x = this._pixelHead.start.x;
var y = Math.max(this._pixelHead.end.y, pixels.end.y) + 24;
// If the first character after the trigger is a space, just deactivate
// immediately. This occurs if a user types a numbered list using "#".
if (text.length && text[0] == ' ') {
this._deactivate();
return;
}
- var trim = this._trim(text);
-
// Deactivate immediately if a user types a character that we are
// reasonably sure means they don't want to use the autocomplete. For
// example, "##" is almost certainly a header or monospaced text, not
// a project autocompletion.
var cancels = this._getCancelCharacters();
for (var ii = 0; ii < cancels.length; ii++) {
- if (trim.indexOf(cancels[ii]) !== -1) {
+ if (text.indexOf(cancels[ii]) !== -1) {
this._deactivate();
return;
}
}
+ var trim = this._trim(text);
+
+ // If this rule has a prefix pattern, like the "[[ document ]]" rule,
+ // require it match and throw it away before we begin suggesting
+ // results. The autocomplete remains active, it's just dormant until
+ // the user gives us more to work with.
+ var prefix = this._map[this._active].prefix;
+ if (prefix) {
+ var pattern = new RegExp(prefix);
+ if (!trim.match(pattern)) {
+ return;
+ }
+ trim = trim.replace(pattern, '');
+ trim = trim.trim();
+ }
+
+ // Store the current value now that we've finished mutating the text.
+ // This needs to match what we pass to the typeahead datasource.
+ this._value = trim;
+
// Deactivate immediately if the user types an ignored token like ":)",
// the smiley face emoticon. Note that we test against "text", not
// "trim", because the ignore list and suffix list can otherwise
// interact destructively.
var ignore = this._getIgnoreList();
for (ii = 0; ii < ignore.length; ii++) {
if (text.indexOf(ignore[ii]) === 0) {
this._deactivate();
return;
}
}
// If the input is terminated by a space or another word-terminating
// punctuation mark, we're going to deactivate if the results can not
// be refined by adding more words.
// The idea is that if you type "@alan ab", you're allowed to keep
// editing "ab" until you type a space, period, or other terminator,
// since you might not be sure how to spell someone's last name or the
// second word of a project.
// Once you do terminate a word, if the words you have have entered match
// nothing or match only one exact match, we can safely deactivate and
// assume you're just typing text because further words could never
// refine the result set.
var force;
if (this._isTerminatedString(text)) {
if (this._hasUnrefinableResults(text)) {
this._deactivate();
return;
}
force = true;
} else {
force = false;
}
this._datasource.didChange(trim, force);
this._x = x;
this._y = y;
var hint = trim;
if (hint.length) {
// We only show the autocompleter after the user types at least one
// character. For example, "@" does not trigger it, but "@d" does.
this._visible = true;
} else {
hint = this._getSpec().hintText;
}
var echo = this._getEchoNode();
JX.DOM.setContent(echo, hint);
this._redraw();
},
_redraw: function() {
if (!this._visible) {
return;
}
var node = this._getNode();
JX.DOM.show(node);
var p = new JX.Vector(this._x, this._y);
var s = JX.Vector.getScroll();
var v = JX.Vector.getViewport();
// If the menu would run off the bottom of the screen when showing the
// maximum number of possible choices, put it above instead. We're doing
// this based on the maximum size so the menu doesn't jump up and down
// as results arrive.
var option_height = 30;
var extra_margin = 24;
if ((s.y + v.y) < (p.y + (5 * option_height) + extra_margin)) {
var d = JX.Vector.getDim(node);
p.y = p.y - d.y - 36;
}
p.setPos(node);
},
_autocomplete: function() {
if (this._focus === null) {
return false;
}
var area = this._area;
var head = this._cursorHead;
var tail = this._cursorTail;
var text = area.value;
var ref = this._focusRef;
var result = this._datasource.getResult(ref);
if (!result) {
return false;
}
ref = result.autocomplete;
if (!ref || !ref.length) {
return false;
}
// If the user types a string like "@username:" (with a trailing colon),
// then presses tab or return to pick the completion, don't destroy the
// trailing character.
var suffixes = this._getSuffixes();
var value = this._value;
var found_suffix = false;
for (var ii = 0; ii < suffixes.length; ii++) {
var last = value.substring(value.length - suffixes[ii].length);
if (last == suffixes[ii]) {
ref += suffixes[ii];
found_suffix = true;
break;
}
}
// If we didn't find an existing suffix, add a space.
if (!found_suffix) {
ref = ref + ' ';
}
area.value = text.substring(0, head - 1) + ref + text.substring(tail);
var end = head + ref.length;
JX.TextAreaUtils.setSelectionRange(area, end, end);
return true;
},
_getNode: function() {
if (!this._node) {
var head = this._getHeadNode();
var list = this._getListNode();
this._node = JX.$N(
'div',
{
className: 'phuix-autocomplete',
style: {
display: 'none'
}
},
[head, list]);
JX.DOM.hide(this._node);
document.body.appendChild(this._node);
}
return this._node;
},
_getHeadNode: function() {
if (!this._headNode) {
this._headNode = JX.$N(
'div',
{
className: 'phuix-autocomplete-head'
},
[
this._getPromptNode(),
this._getEchoNode()
]);
}
return this._headNode;
},
_getPromptNode: function() {
if (!this._promptNode) {
this._promptNode = JX.$N(
'span',
{
className: 'phuix-autocomplete-prompt',
});
}
return this._promptNode;
},
_getEchoNode: function() {
if (!this._echoNode) {
this._echoNode = JX.$N(
'span',
{
className: 'phuix-autocomplete-echo'
});
}
return this._echoNode;
},
_getListNode: function() {
if (!this._listNode) {
this._listNode = JX.$N(
'div',
{
className: 'phuix-autocomplete-list'
});
}
return this._listNode;
}
}
});
diff --git a/webroot/rsrc/js/phuix/PHUIXFormControl.js b/webroot/rsrc/js/phuix/PHUIXFormControl.js
index cd67e8969..88a1cd3f3 100644
--- a/webroot/rsrc/js/phuix/PHUIXFormControl.js
+++ b/webroot/rsrc/js/phuix/PHUIXFormControl.js
@@ -1,374 +1,381 @@
/**
* @provides phuix-form-control-view
* @requires javelin-install
* javelin-dom
*/
JX.install('PHUIXFormControl', {
members: {
_node: null,
_labelNode: null,
_errorNode: null,
_inputNode: null,
_className: null,
_valueSetCallback: null,
_valueGetCallback: null,
_rawInputNode: null,
+ _tokenizer: null,
setLabel: function(label) {
JX.DOM.setContent(this._getLabelNode(), label);
return this;
},
setError: function(error) {
JX.DOM.setContent(this._getErrorNode(), error);
return this;
},
setClass: function(className) {
this._className = className;
return this;
},
setControl: function(type, spec) {
var node = this._getInputNode();
var input;
switch (type) {
case 'tokenizer':
input = this._newTokenizer(spec);
break;
case 'select':
input = this._newSelect(spec);
break;
case 'points':
input = this._newPoints(spec);
break;
case 'optgroups':
input = this._newOptgroups(spec);
break;
case 'static':
input = this._newStatic(spec);
break;
case 'checkboxes':
input = this._newCheckboxes(spec);
break;
case 'text':
input = this._newText(spec);
break;
case 'remarkup':
input = this._newRemarkup(spec);
break;
default:
// TODO: Default or better error?
JX.$E('Bad Input Type');
return;
}
JX.DOM.setContent(node, input.node);
this._valueGetCallback = input.get;
this._valueSetCallback = input.set;
this._rawInputNode = input.node;
+ this._tokenizer = input.tokenizer || null;
return this;
},
setValue: function(value) {
this._valueSetCallback(value);
return this;
},
getValue: function() {
return this._valueGetCallback();
},
getRawInputNode: function() {
return this._rawInputNode;
},
+ getTokenizer: function() {
+ return this._tokenizer;
+ },
+
getNode: function() {
if (!this._node) {
var attrs = {
className: 'aphront-form-control ' + this._className + ' grouped'
};
var content = [
this._getLabelNode(),
this._getErrorNode(),
this._getInputNode()
];
this._node = JX.$N('div', attrs, content);
}
return this._node;
},
_getLabelNode: function() {
if (!this._labelNode) {
var attrs = {
className: 'aphront-form-label'
};
this._labelNode = JX.$N('label', attrs);
}
return this._labelNode;
},
_getErrorNode: function() {
if (!this._errorNode) {
var attrs = {
className: 'aphront-form-error'
};
this._errorNode = JX.$N('span', attrs);
}
return this._errorNode;
},
_getInputNode: function() {
if (!this._inputNode) {
var attrs = {
className: 'aphront-form-input'
};
this._inputNode = JX.$N('div', attrs);
}
return this._inputNode;
},
_newTokenizer: function(spec) {
var build = JX.Prefab.newTokenizerFromTemplate(
spec.markup,
spec.config);
build.tokenizer.start();
function get_value() {
return JX.keys(build.tokenizer.getTokens());
}
function set_value(map) {
var tokens = get_value();
for (var ii = 0; ii < tokens.length; ii++) {
build.tokenizer.removeToken(tokens[ii]);
}
for (var k in map) {
var v = JX.Prefab.transformDatasourceResults(map[k]);
build.tokenizer.addToken(k, v);
}
}
set_value(spec.value || {});
return {
node: build.node,
get: get_value,
- set: set_value
+ set: set_value,
+ tokenizer: build.tokenizer
};
},
_newSelect: function(spec) {
var node = JX.Prefab.renderSelect(
spec.options,
spec.value,
{},
spec.order);
return {
node: node,
get: function() {
return node.value;
},
set: function(value) {
node.value = value;
}
};
},
_newStatic: function(spec) {
var node = JX.$N(
'div',
{
className: 'phui-form-static-action'
},
spec.description || '');
return {
node: node,
get: function() {
return true;
},
set: function() {
return;
}
};
},
_newCheckboxes: function(spec) {
var checkboxes = [];
var checkbox_list = [];
for (var ii = 0; ii < spec.keys.length; ii++) {
var key = spec.keys[ii];
var checkbox_id = 'checkbox-' + Math.floor(Math.random() * 1000000);
var checkbox = JX.$N(
'input',
{
type: 'checkbox',
value: key,
id: checkbox_id
});
checkboxes.push(checkbox);
var label = JX.$N(
'label',
{
className: 'phuix-form-checkbox-label',
htmlFor: checkbox_id
},
JX.$H(spec.labels[key] || ''));
var display = JX.$N(
'div',
{
className: 'phuix-form-checkbox-item'
},
[checkbox, label]);
checkbox_list.push(display);
}
var node = JX.$N(
'div',
{
className: 'phuix-form-checkbox-action'
},
checkbox_list);
var get_value = function() {
var list = [];
for (var ii = 0; ii < checkboxes.length; ii++) {
if (checkboxes[ii].checked) {
list.push(checkboxes[ii].value);
}
}
return list;
};
var set_value = function(value) {
value = value || [];
if (!value.length) {
value = [];
}
var map = {};
var ii;
for (ii = 0; ii < value.length; ii++) {
map[value[ii]] = true;
}
for (ii = 0; ii < checkboxes.length; ii++) {
if (map.hasOwnProperty(checkboxes[ii].value)) {
checkboxes[ii].checked = 'checked';
} else {
checkboxes[ii].checked = false;
}
}
};
set_value(spec.value);
return {
node: node,
get: get_value,
set: set_value
};
},
_newPoints: function(spec) {
- return this._newText();
+ return this._newText(spec);
},
_newText: function(spec) {
var attrs = {
type: 'text',
value: spec.value
};
var node = JX.$N('input', attrs);
return {
node: node,
get: function() {
return node.value;
},
set: function(value) {
node.value = value;
}
};
},
_newRemarkup: function(spec) {
var attrs = {};
// We could imagine a world where this renders a full remarkup control
// with all the hint buttons and client behaviors, but today much of that
// behavior is defined server-side and thus this isn't a world we
// currently live in.
var node = JX.$N('textarea', attrs);
node.value = spec.value || '';
return {
node: node,
get: function() {
return node.value;
},
set: function(value) {
node.value = value;
}
};
},
_newOptgroups: function(spec) {
var value = spec.value || null;
var optgroups = [];
for (var ii = 0; ii < spec.groups.length; ii++) {
var group = spec.groups[ii];
var options = [];
for (var jj = 0; jj < group.options.length; jj++) {
var option = group.options[jj];
options.push(JX.$N('option', {value: option.key}, option.label));
if (option.selected && (value === null)) {
value = option.key;
}
}
optgroups.push(JX.$N('optgroup', {label: group.label}, options));
}
var node = JX.$N('select', {}, optgroups);
node.value = value;
return {
node: node,
get: function() {
return node.value;
},
set: function(value) {
node.value = value;
}
};
}
}
});

Event Timeline