diff --git a/resources/builtin/image-100x100.png b/resources/builtin/image-100x100.png new file mode 100644 index 0000000000..c56a9aa083 Binary files /dev/null and b/resources/builtin/image-100x100.png differ diff --git a/resources/builtin/image-220x220.png b/resources/builtin/image-220x220.png new file mode 100644 index 0000000000..928b5b05eb Binary files /dev/null and b/resources/builtin/image-220x220.png differ diff --git a/resources/builtin/image-280x210.png b/resources/builtin/image-280x210.png new file mode 100644 index 0000000000..48237b045e Binary files /dev/null and b/resources/builtin/image-280x210.png differ diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 0a6ce7d67e..2fdb459dcc 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,11 +7,11 @@ */ return array( 'names' => array( - 'core.pkg.css' => '8aed144e', - 'core.pkg.js' => '60924527', + 'core.pkg.css' => '50250d4f', + 'core.pkg.js' => 'f3e08b38', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => 'bb338e4b', - 'differential.pkg.js' => '3cfa26f9', + 'differential.pkg.js' => '895b8d62', 'diffusion.pkg.css' => '591664fa', 'diffusion.pkg.js' => '0115b37c', 'maniphest.pkg.css' => '68d4dd3d', @@ -33,22 +33,23 @@ return array( 'rsrc/css/aphront/typeahead-browse.css' => 'd8581d2c', 'rsrc/css/aphront/typeahead.css' => '0e403212', 'rsrc/css/application/almanac/almanac.css' => 'dbb9b3af', - 'rsrc/css/application/auth/auth.css' => '1e655982', + 'rsrc/css/application/auth/auth.css' => '44975d4b', 'rsrc/css/application/base/main-menu-view.css' => '1766b04d', 'rsrc/css/application/base/notification-menu.css' => '3c9d8aa1', 'rsrc/css/application/base/phabricator-application-launch-view.css' => '132f9d14', - 'rsrc/css/application/base/standard-page-view.css' => 'dc14c671', + 'rsrc/css/application/base/standard-page-view.css' => '062f0f54', 'rsrc/css/application/chatlog/chatlog.css' => '852140ff', + 'rsrc/css/application/conduit/conduit-api.css' => '7bc725c4', 'rsrc/css/application/config/config-options.css' => '7fedf08b', 'rsrc/css/application/config/config-template.css' => '8e6c6fcd', 'rsrc/css/application/config/config-welcome.css' => '6abd79be', 'rsrc/css/application/config/setup-issue.css' => '22270af2', 'rsrc/css/application/config/unhandled-exception.css' => '37d4f9a2', - 'rsrc/css/application/conpherence/durable-column.css' => '2e68a92f', + 'rsrc/css/application/conpherence/durable-column.css' => '8c43d6ac', 'rsrc/css/application/conpherence/menu.css' => 'f9f1d143', - 'rsrc/css/application/conpherence/message-pane.css' => '73631823', - 'rsrc/css/application/conpherence/notification.css' => 'd208f806', - 'rsrc/css/application/conpherence/transaction.css' => '25138b7f', + 'rsrc/css/application/conpherence/message-pane.css' => '7cbf4cbb', + 'rsrc/css/application/conpherence/notification.css' => '919974b6', + 'rsrc/css/application/conpherence/transaction.css' => '42a457f6', 'rsrc/css/application/conpherence/update.css' => '1099a660', 'rsrc/css/application/conpherence/widget-pane.css' => '2af42ebe', 'rsrc/css/application/contentsource/content-source-view.css' => '4b8b05d4', @@ -106,10 +107,10 @@ return array( 'rsrc/css/application/slowvote/slowvote.css' => '266df6a1', 'rsrc/css/application/tokens/tokens.css' => '3d0f239e', 'rsrc/css/application/uiexample/example.css' => '528b19de', - 'rsrc/css/core/core.css' => 'cee2aadb', + 'rsrc/css/core/core.css' => '6230ff55', 'rsrc/css/core/remarkup.css' => '0037bdbf', 'rsrc/css/core/syntax.css' => '6b7b24d9', - 'rsrc/css/core/z-index.css' => 'ef044fae', + 'rsrc/css/core/z-index.css' => '8c8c40aa', 'rsrc/css/diviner/diviner-shared.css' => '38813222', 'rsrc/css/font/font-awesome.css' => 'e2e712fe', 'rsrc/css/font/font-source-sans-pro.css' => '8906c07b', @@ -118,9 +119,9 @@ return array( 'rsrc/css/layout/phabricator-hovercard-view.css' => '44394670', 'rsrc/css/layout/phabricator-side-menu-view.css' => 'a440478a', 'rsrc/css/layout/phabricator-source-code-view.css' => '2ceee894', - 'rsrc/css/phui/calendar/phui-calendar-day.css' => '75b8cc4a', + 'rsrc/css/phui/calendar/phui-calendar-day.css' => '38891735', 'rsrc/css/phui/calendar/phui-calendar-list.css' => 'c1d0ca59', - 'rsrc/css/phui/calendar/phui-calendar-month.css' => 'a92e47d2', + 'rsrc/css/phui/calendar/phui-calendar-month.css' => '75e6a2ee', 'rsrc/css/phui/calendar/phui-calendar.css' => '8675968e', 'rsrc/css/phui/phui-action-header-view.css' => 'e4471f43', 'rsrc/css/phui/phui-action-list.css' => '4f4d09f2', @@ -131,16 +132,16 @@ return array( 'rsrc/css/phui/phui-document.css' => '7b564cf6', 'rsrc/css/phui/phui-feed-story.css' => 'c9f3a0b5', 'rsrc/css/phui/phui-fontkit.css' => '1e71371a', - 'rsrc/css/phui/phui-form-view.css' => 'ddec8479', + 'rsrc/css/phui/phui-form-view.css' => 'e1abbe8e', 'rsrc/css/phui/phui-form.css' => 'f535f938', - 'rsrc/css/phui/phui-header-view.css' => 'a1d2905a', + 'rsrc/css/phui/phui-header-view.css' => '5c0c1c39', 'rsrc/css/phui/phui-icon.css' => 'bc766998', 'rsrc/css/phui/phui-image-mask.css' => '5a8b09c8', 'rsrc/css/phui/phui-info-panel.css' => '27ea50a1', 'rsrc/css/phui/phui-info-view.css' => '33595731', 'rsrc/css/phui/phui-list.css' => '2e25ebfb', 'rsrc/css/phui/phui-object-box.css' => '3a601bc5', - 'rsrc/css/phui/phui-object-item-list-view.css' => '172ea456', + 'rsrc/css/phui/phui-object-item-list-view.css' => 'c259c94f', 'rsrc/css/phui/phui-pinboard-view.css' => 'eaab2b1b', 'rsrc/css/phui/phui-property-list-view.css' => 'd2d143ea', 'rsrc/css/phui/phui-remarkup-preview.css' => '19ad512b', @@ -148,7 +149,7 @@ return array( 'rsrc/css/phui/phui-status.css' => '888cedb8', 'rsrc/css/phui/phui-tag-view.css' => '402691cc', 'rsrc/css/phui/phui-text.css' => 'cf019f54', - 'rsrc/css/phui/phui-timeline-view.css' => 'b0fbc4d7', + 'rsrc/css/phui/phui-timeline-view.css' => 'a85542c8', 'rsrc/css/phui/phui-workboard-view.css' => '3279cbbf', 'rsrc/css/phui/phui-workpanel-view.css' => 'e495a5cc', 'rsrc/css/sprite-gradient.css' => '4bdb98a7', @@ -209,7 +210,7 @@ return array( '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' => 'eaa5b321', + 'rsrc/externals/javelin/lib/Scrollbar.js' => '087e919c', 'rsrc/externals/javelin/lib/Sound.js' => '949c0fe5', 'rsrc/externals/javelin/lib/URI.js' => '6eff08aa', 'rsrc/externals/javelin/lib/Vector.js' => '2caa8fb8', @@ -279,22 +280,6 @@ return array( 'rsrc/image/icon/fatcow/source/mobile.png' => 'f1321264', 'rsrc/image/icon/fatcow/source/tablet.png' => '49396799', 'rsrc/image/icon/fatcow/source/web.png' => '136ccb5d', - 'rsrc/image/icon/fatcow/thumbnails/default.p100.png' => '7d490b01', - 'rsrc/image/icon/fatcow/thumbnails/default160x120.png' => 'f2e8a2eb', - 'rsrc/image/icon/fatcow/thumbnails/default280x210.png' => '43e8926a', - 'rsrc/image/icon/fatcow/thumbnails/default60x45.png' => '0118abed', - 'rsrc/image/icon/fatcow/thumbnails/image.p100.png' => 'da23cf97', - 'rsrc/image/icon/fatcow/thumbnails/image160x120.png' => '79bb556a', - 'rsrc/image/icon/fatcow/thumbnails/image280x210.png' => '91ae054a', - 'rsrc/image/icon/fatcow/thumbnails/image60x45.png' => 'c5e1685e', - 'rsrc/image/icon/fatcow/thumbnails/pdf.p100.png' => '87d5e065', - 'rsrc/image/icon/fatcow/thumbnails/pdf160x120.png' => 'ac9edbf5', - 'rsrc/image/icon/fatcow/thumbnails/pdf280x210.png' => '1c585653', - 'rsrc/image/icon/fatcow/thumbnails/pdf60x45.png' => 'c0db4143', - 'rsrc/image/icon/fatcow/thumbnails/zip.p100.png' => '6ea5aae4', - 'rsrc/image/icon/fatcow/thumbnails/zip160x120.png' => '75f9cd0f', - 'rsrc/image/icon/fatcow/thumbnails/zip280x210.png' => 'dfda5b8e', - 'rsrc/image/icon/fatcow/thumbnails/zip60x45.png' => 'af11bf3e', 'rsrc/image/icon/lightbox/close-2.png' => 'cc40e7c8', 'rsrc/image/icon/lightbox/close-hover-2.png' => 'fb5d6d9e', 'rsrc/image/icon/lightbox/left-arrow-2.png' => '8426133b', @@ -339,16 +324,17 @@ return array( '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' => '30a6303c', - 'rsrc/js/application/aphlict/behavior-aphlict-dropdown.js' => '6e1f0cba', + 'rsrc/js/application/aphlict/Aphlict.js' => '5359e785', + 'rsrc/js/application/aphlict/behavior-aphlict-dropdown.js' => 'e09f6208', 'rsrc/js/application/aphlict/behavior-aphlict-listen.js' => 'b1a59974', 'rsrc/js/application/aphlict/behavior-aphlict-status.js' => 'ea681761', 'rsrc/js/application/auth/behavior-persona-login.js' => '9414ff18', + 'rsrc/js/application/calendar/event-all-day.js' => 'ca5fa62a', 'rsrc/js/application/config/behavior-reorder-fields.js' => '14a827de', - 'rsrc/js/application/conpherence/ConpherenceThreadManager.js' => '6709c934', + 'rsrc/js/application/conpherence/ConpherenceThreadManager.js' => 'b7342ddb', 'rsrc/js/application/conpherence/behavior-drag-and-drop-photo.js' => 'cf86d16a', - 'rsrc/js/application/conpherence/behavior-durable-column.js' => '657c2b50', - 'rsrc/js/application/conpherence/behavior-menu.js' => '804b0773', + 'rsrc/js/application/conpherence/behavior-durable-column.js' => '16c695bf', + 'rsrc/js/application/conpherence/behavior-menu.js' => '4351c4a0', 'rsrc/js/application/conpherence/behavior-pontificate.js' => '21ba5861', 'rsrc/js/application/conpherence/behavior-quicksand-blacklist.js' => '7927a7d3', 'rsrc/js/application/conpherence/behavior-widget-pane.js' => '93568464', @@ -358,7 +344,7 @@ return array( 'rsrc/js/application/dashboard/behavior-dashboard-query-panel-select.js' => '453c5375', 'rsrc/js/application/dashboard/behavior-dashboard-tab-panel.js' => 'd4eecc63', 'rsrc/js/application/differential/ChangesetViewManager.js' => '58562350', - 'rsrc/js/application/differential/DifferentialInlineCommentEditor.js' => '2529c82d', + 'rsrc/js/application/differential/DifferentialInlineCommentEditor.js' => 'd4c87bf4', 'rsrc/js/application/differential/behavior-add-reviewers-and-ccs.js' => 'e10f8e18', 'rsrc/js/application/differential/behavior-comment-jump.js' => '4fdb476d', 'rsrc/js/application/differential/behavior-comment-preview.js' => '8e1389b5', @@ -404,7 +390,7 @@ return array( 'rsrc/js/application/policy/behavior-policy-control.js' => '9a340b3d', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '5e9f347c', 'rsrc/js/application/ponder/behavior-votebox.js' => '4e9b766b', - 'rsrc/js/application/projects/behavior-project-boards.js' => '60292820', + 'rsrc/js/application/projects/behavior-project-boards.js' => 'ba4fa35c', 'rsrc/js/application/projects/behavior-project-create.js' => '065227cc', 'rsrc/js/application/projects/behavior-reorder-columns.js' => 'e1d25dfb', 'rsrc/js/application/releeph/releeph-preview-branch.js' => 'b2b4fbaf', @@ -435,7 +421,7 @@ return array( 'rsrc/js/core/DragAndDropFileUpload.js' => '07de8873', 'rsrc/js/core/DraggableList.js' => 'a16ec1c6', 'rsrc/js/core/FileUpload.js' => '477359c8', - 'rsrc/js/core/Hovercard.js' => '7e8468ae', + 'rsrc/js/core/Hovercard.js' => '14ac66f5', 'rsrc/js/core/KeyboardShortcut.js' => '1ae869f2', 'rsrc/js/core/KeyboardShortcutManager.js' => 'c1700f6f', 'rsrc/js/core/MultirowRowManager.js' => 'b5d57730', @@ -504,21 +490,22 @@ return array( 'aphront-tooltip-css' => '7672b60f', 'aphront-two-column-view-css' => '16ab3ad2', 'aphront-typeahead-control-css' => '0e403212', - 'auth-css' => '1e655982', + 'auth-css' => '44975d4b', 'changeset-view-manager' => '58562350', + 'conduit-api-css' => '7bc725c4', 'config-options-css' => '7fedf08b', 'config-welcome-css' => '6abd79be', - 'conpherence-durable-column-view' => '2e68a92f', + 'conpherence-durable-column-view' => '8c43d6ac', 'conpherence-menu-css' => 'f9f1d143', - 'conpherence-message-pane-css' => '73631823', - 'conpherence-notification-css' => 'd208f806', - 'conpherence-thread-manager' => '6709c934', - 'conpherence-transaction-css' => '25138b7f', + 'conpherence-message-pane-css' => '7cbf4cbb', + 'conpherence-notification-css' => '919974b6', + 'conpherence-thread-manager' => 'b7342ddb', + 'conpherence-transaction-css' => '42a457f6', 'conpherence-update-css' => '1099a660', 'conpherence-widget-pane-css' => '2af42ebe', 'differential-changeset-view-css' => 'e19cfd6e', 'differential-core-view-css' => '7ac3cabc', - 'differential-inline-comment-editor' => '2529c82d', + 'differential-inline-comment-editor' => 'd4c87bf4', 'differential-results-table-css' => '181aa9d9', 'differential-revision-add-comment-css' => 'c47f8c40', 'differential-revision-comment-css' => '14b8565a', @@ -537,9 +524,9 @@ return array( 'herald-rule-editor' => '9229e764', 'herald-test-css' => '778b008e', 'inline-comment-summary-css' => 'eb5f8e8c', - 'javelin-aphlict' => '30a6303c', + 'javelin-aphlict' => '5359e785', 'javelin-behavior' => '61cbc29a', - 'javelin-behavior-aphlict-dropdown' => '6e1f0cba', + 'javelin-behavior-aphlict-dropdown' => 'e09f6208', 'javelin-behavior-aphlict-listen' => 'b1a59974', 'javelin-behavior-aphlict-status' => 'ea681761', 'javelin-behavior-aphront-basic-tokenizer' => 'b3a4b884', @@ -552,7 +539,7 @@ return array( 'javelin-behavior-choose-control' => '6153c708', 'javelin-behavior-config-reorder-fields' => '14a827de', 'javelin-behavior-conpherence-drag-and-drop-photo' => 'cf86d16a', - 'javelin-behavior-conpherence-menu' => '804b0773', + 'javelin-behavior-conpherence-menu' => '4351c4a0', 'javelin-behavior-conpherence-pontificate' => '21ba5861', 'javelin-behavior-conpherence-widget-pane' => '93568464', 'javelin-behavior-countdown-timer' => 'e4cc26b3', @@ -579,8 +566,9 @@ return array( 'javelin-behavior-diffusion-locate-file' => '6d3e1947', 'javelin-behavior-diffusion-pull-lastmodified' => '2b228192', 'javelin-behavior-doorkeeper-tag' => 'e5822781', - 'javelin-behavior-durable-column' => '657c2b50', + 'javelin-behavior-durable-column' => '16c695bf', 'javelin-behavior-error-log' => '6882e80a', + 'javelin-behavior-event-all-day' => 'ca5fa62a', 'javelin-behavior-fancy-datepicker' => '5c0f680f', 'javelin-behavior-global-drag-and-drop' => 'c8e57404', 'javelin-behavior-herald-rule-editor' => '7ebaeed3', @@ -631,7 +619,7 @@ return array( 'javelin-behavior-policy-control' => '9a340b3d', 'javelin-behavior-policy-rule-editor' => '5e9f347c', 'javelin-behavior-ponder-votebox' => '4e9b766b', - 'javelin-behavior-project-boards' => '60292820', + 'javelin-behavior-project-boards' => 'ba4fa35c', 'javelin-behavior-project-create' => '065227cc', 'javelin-behavior-quicksand-blacklist' => '7927a7d3', 'javelin-behavior-refresh-csrf' => '7814b593', @@ -675,7 +663,7 @@ return array( 'javelin-resource' => '44959b73', 'javelin-routable' => 'b3e7d692', 'javelin-router' => '29274e2b', - 'javelin-scrollbar' => 'eaa5b321', + 'javelin-scrollbar' => '087e919c', 'javelin-sound' => '949c0fe5', 'javelin-stratcom' => '6c53634d', 'javelin-tokenizer' => 'ab5f468d', @@ -712,7 +700,7 @@ return array( 'phabricator-busy' => '59a7976a', 'phabricator-chatlog-css' => '852140ff', 'phabricator-content-source-view-css' => '4b8b05d4', - 'phabricator-core-css' => 'cee2aadb', + 'phabricator-core-css' => '6230ff55', 'phabricator-countdown-css' => '86b7b0a0', 'phabricator-dashboard-css' => 'db1d30b0', 'phabricator-drag-and-drop-file-upload' => '07de8873', @@ -722,7 +710,7 @@ return array( 'phabricator-file-upload' => '477359c8', 'phabricator-filetree-view-css' => 'fccf9f82', 'phabricator-flag-css' => '5337623f', - 'phabricator-hovercard' => '7e8468ae', + 'phabricator-hovercard' => '14ac66f5', 'phabricator-hovercard-view-css' => '44394670', 'phabricator-keyboard-shortcut' => '1ae869f2', 'phabricator-keyboard-shortcut-manager' => 'c1700f6f', @@ -741,7 +729,7 @@ return array( 'phabricator-side-menu-view-css' => 'a440478a', 'phabricator-slowvote-css' => '266df6a1', 'phabricator-source-code-view-css' => '2ceee894', - 'phabricator-standard-page-view' => 'dc14c671', + 'phabricator-standard-page-view' => '062f0f54', 'phabricator-textareautils' => '5c93c52c', 'phabricator-title' => 'df5e11d2', 'phabricator-tooltip' => '1d298e3a', @@ -756,7 +744,7 @@ return array( 'phabricator-uiexample-reactor-select' => 'a155550f', 'phabricator-uiexample-reactor-sendclass' => '1def2711', 'phabricator-uiexample-reactor-sendproperties' => 'b1f0ccee', - 'phabricator-zindex-css' => 'ef044fae', + 'phabricator-zindex-css' => '8c8c40aa', 'phame-css' => '88bd4705', 'pholio-css' => '95174bdd', 'pholio-edit-css' => '3ad9d1ee', @@ -771,17 +759,17 @@ return array( 'phui-box-css' => 'a5bb366d', 'phui-button-css' => 'de610129', 'phui-calendar-css' => '8675968e', - 'phui-calendar-day-css' => '75b8cc4a', + 'phui-calendar-day-css' => '38891735', 'phui-calendar-list-css' => 'c1d0ca59', - 'phui-calendar-month-css' => 'a92e47d2', + 'phui-calendar-month-css' => '75e6a2ee', 'phui-crumbs-view-css' => 'aeff7a21', 'phui-document-view-css' => '7b564cf6', 'phui-feed-story-css' => 'c9f3a0b5', 'phui-font-icon-base-css' => '3dad2ae3', 'phui-fontkit-css' => '1e71371a', 'phui-form-css' => 'f535f938', - 'phui-form-view-css' => 'ddec8479', - 'phui-header-view-css' => 'a1d2905a', + 'phui-form-view-css' => 'e1abbe8e', + 'phui-header-view-css' => '5c0c1c39', 'phui-icon-view-css' => 'bc766998', 'phui-image-mask-css' => '5a8b09c8', 'phui-info-panel-css' => '27ea50a1', @@ -789,7 +777,7 @@ return array( 'phui-inline-comment-view-css' => '2174771a', 'phui-list-view-css' => '2e25ebfb', 'phui-object-box-css' => '3a601bc5', - 'phui-object-item-list-view-css' => '172ea456', + 'phui-object-item-list-view-css' => 'c259c94f', 'phui-pinboard-view-css' => 'eaab2b1b', 'phui-property-list-view-css' => 'd2d143ea', 'phui-remarkup-preview-css' => '19ad512b', @@ -797,7 +785,7 @@ return array( 'phui-status-list-view-css' => '888cedb8', 'phui-tag-view-css' => '402691cc', 'phui-text-css' => 'cf019f54', - 'phui-timeline-view-css' => 'b0fbc4d7', + 'phui-timeline-view-css' => 'a85542c8', 'phui-workboard-view-css' => '3279cbbf', 'phui-workpanel-view-css' => 'e495a5cc', 'phuix-action-list-view' => 'b5c256b8', @@ -862,6 +850,12 @@ return array( 'javelin-uri', 'phabricator-file-upload', ), + '087e919c' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-vector', + ), '0a3f3021' => array( 'javelin-behavior', 'javelin-stratcom', @@ -901,6 +895,13 @@ return array( 'javelin-json', 'phabricator-draggable-list', ), + '14ac66f5' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-vector', + 'javelin-request', + 'javelin-uri', + ), '14d7a8b8' => array( 'javelin-behavior', 'javelin-behavior-device', @@ -911,6 +912,16 @@ return array( 'javelin-request', 'javelin-util', ), + '16c695bf' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-behavior-device', + 'javelin-scrollbar', + 'javelin-quicksand', + 'phabricator-keyboard-shortcut', + 'conpherence-thread-manager', + ), '1ad0a787' => array( 'javelin-install', 'javelin-reactor', @@ -970,14 +981,6 @@ return array( 'phabricator-drag-and-drop-file-upload', 'phabricator-draggable-list', ), - '2529c82d' => array( - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-install', - 'javelin-request', - 'javelin-workflow', - ), '2818f5ce' => array( 'javelin-install', 'javelin-util', @@ -1018,13 +1021,6 @@ return array( 'javelin-install', 'javelin-event', ), - '30a6303c' => array( - 'javelin-install', - 'javelin-util', - 'javelin-websocket', - 'javelin-leader', - 'javelin-json', - ), '316b8fa1' => array( 'javelin-install', 'javelin-typeahead-source', @@ -1069,6 +1065,20 @@ return array( 'javelin-dom', 'javelin-request', ), + '4351c4a0' => 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', + ), '44168bad' => array( 'javelin-behavior', 'javelin-dom', @@ -1151,6 +1161,13 @@ return array( 'javelin-dom', 'javelin-reactor-dom', ), + '5359e785' => array( + 'javelin-install', + 'javelin-util', + 'javelin-websocket', + 'javelin-leader', + 'javelin-json', + ), '54b612ba' => array( 'javelin-color', 'javelin-install', @@ -1236,15 +1253,6 @@ return array( 'javelin-workflow', 'javelin-stratcom', ), - 60292820 => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - ), 60479091 => array( 'phabricator-busy', 'javelin-behavior', @@ -1274,26 +1282,6 @@ return array( 'javelin-workflow', 'javelin-dom', ), - '657c2b50' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-behavior-device', - 'javelin-scrollbar', - 'javelin-quicksand', - 'phabricator-keyboard-shortcut', - 'conpherence-thread-manager', - ), - '6709c934' => array( - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-install', - 'javelin-workflow', - 'javelin-router', - 'javelin-behavior-device', - 'javelin-vector', - ), '6882e80a' => array( 'javelin-dom', ), @@ -1335,16 +1323,6 @@ return array( 'phabricator-drag-and-drop-file-upload', 'phabricator-textareautils', ), - '6e1f0cba' => array( - 'javelin-behavior', - 'javelin-request', - 'javelin-stratcom', - 'javelin-vector', - 'javelin-dom', - 'javelin-uri', - 'javelin-behavior-device', - 'phabricator-title', - ), '6eff08aa' => array( 'javelin-install', 'javelin-util', @@ -1415,13 +1393,6 @@ return array( '7e41274a' => array( 'javelin-install', ), - '7e8468ae' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-vector', - 'javelin-request', - 'javelin-uri', - ), '7ebaeed3' => array( 'herald-rule-editor', 'javelin-behavior', @@ -1430,20 +1401,6 @@ return array( 'javelin-behavior', 'javelin-history', ), - '804b0773' => 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', - ), 82439934 => array( 'javelin-behavior', 'javelin-dom', @@ -1734,6 +1691,26 @@ return array( 'javelin-dom', 'javelin-util', ), + 'b7342ddb' => array( + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-install', + 'javelin-aphlict', + 'javelin-workflow', + 'javelin-router', + 'javelin-behavior-device', + 'javelin-vector', + ), + 'ba4fa35c' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-vector', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + ), 'bba9eedf' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1813,6 +1790,14 @@ return array( 'javelin-dom', 'javelin-view', ), + 'd4c87bf4' => array( + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-install', + 'javelin-request', + 'javelin-workflow', + ), 'd4eecc63' => array( 'javelin-behavior', 'javelin-dom', @@ -1847,6 +1832,16 @@ return array( 'df5e11d2' => array( 'javelin-install', ), + 'e09f6208' => array( + 'javelin-behavior', + 'javelin-request', + 'javelin-stratcom', + 'javelin-vector', + 'javelin-dom', + 'javelin-uri', + 'javelin-behavior-device', + 'phabricator-title', + ), 'e10f8e18' => array( 'javelin-behavior', 'javelin-dom', @@ -1921,12 +1916,6 @@ return array( 'phabricator-phtize', 'javelin-dom', ), - 'eaa5b321' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-vector', - ), 'efe49472' => array( 'javelin-install', 'javelin-util', diff --git a/resources/sql/autopatches/20150507.calendar.1.isallday.sql b/resources/sql/autopatches/20150507.calendar.1.isallday.sql new file mode 100644 index 0000000000..172015b1be --- /dev/null +++ b/resources/sql/autopatches/20150507.calendar.1.isallday.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_calendar.calendar_event + ADD isAllDay BOOL NOT NULL; diff --git a/resources/sql/autopatches/20150513.user.cache.1.sql b/resources/sql/autopatches/20150513.user.cache.1.sql new file mode 100644 index 0000000000..f6bf6e1e6a --- /dev/null +++ b/resources/sql/autopatches/20150513.user.cache.1.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_user.user + ADD profileImageCache VARCHAR(255) COLLATE {$COLLATE_TEXT}; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 8e19b001f3..0fa277df27 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -293,6 +293,7 @@ phutil_register_library_map(array( 'DifferentialActionEmailCommand' => 'applications/differential/command/DifferentialActionEmailCommand.php', 'DifferentialActionMenuEventListener' => 'applications/differential/event/DifferentialActionMenuEventListener.php', 'DifferentialAddCommentView' => 'applications/differential/view/DifferentialAddCommentView.php', + 'DifferentialAdjustmentMapTestCase' => 'applications/differential/storage/__tests__/DifferentialAdjustmentMapTestCase.php', 'DifferentialAffectedPath' => 'applications/differential/storage/DifferentialAffectedPath.php', 'DifferentialApplyPatchField' => 'applications/differential/customfield/DifferentialApplyPatchField.php', 'DifferentialArcanistProjectField' => 'applications/differential/customfield/DifferentialArcanistProjectField.php', @@ -391,6 +392,7 @@ phutil_register_library_map(array( 'DifferentialLandingActionMenuEventListener' => 'applications/differential/landing/DifferentialLandingActionMenuEventListener.php', 'DifferentialLandingStrategy' => 'applications/differential/landing/DifferentialLandingStrategy.php', 'DifferentialLegacyHunk' => 'applications/differential/storage/DifferentialLegacyHunk.php', + 'DifferentialLineAdjustmentMap' => 'applications/differential/parser/DifferentialLineAdjustmentMap.php', 'DifferentialLintField' => 'applications/differential/customfield/DifferentialLintField.php', 'DifferentialLintStatus' => 'applications/differential/constants/DifferentialLintStatus.php', 'DifferentialLocalCommitsView' => 'applications/differential/view/DifferentialLocalCommitsView.php', @@ -1493,7 +1495,6 @@ phutil_register_library_map(array( 'PhabricatorCacheTTLGarbageCollector' => 'applications/cache/garbagecollector/PhabricatorCacheTTLGarbageCollector.php', 'PhabricatorCaches' => 'applications/cache/PhabricatorCaches.php', 'PhabricatorCalendarApplication' => 'applications/calendar/application/PhabricatorCalendarApplication.php', - 'PhabricatorCalendarBrowseController' => 'applications/calendar/controller/PhabricatorCalendarBrowseController.php', 'PhabricatorCalendarController' => 'applications/calendar/controller/PhabricatorCalendarController.php', 'PhabricatorCalendarDAO' => 'applications/calendar/storage/PhabricatorCalendarDAO.php', 'PhabricatorCalendarEvent' => 'applications/calendar/storage/PhabricatorCalendarEvent.php', @@ -1859,6 +1860,7 @@ phutil_register_library_map(array( 'PhabricatorFileFilePHIDType' => 'applications/files/phid/PhabricatorFileFilePHIDType.php', 'PhabricatorFileHasObjectEdgeType' => 'applications/files/edge/PhabricatorFileHasObjectEdgeType.php', 'PhabricatorFileImageMacro' => 'applications/macro/storage/PhabricatorFileImageMacro.php', + 'PhabricatorFileImageTransform' => 'applications/files/transform/PhabricatorFileImageTransform.php', 'PhabricatorFileInfoController' => 'applications/files/controller/PhabricatorFileInfoController.php', 'PhabricatorFileLinkListView' => 'view/layout/PhabricatorFileLinkListView.php', 'PhabricatorFileLinkView' => 'view/layout/PhabricatorFileLinkView.php', @@ -1872,10 +1874,13 @@ phutil_register_library_map(array( 'PhabricatorFileTemporaryGarbageCollector' => 'applications/files/garbagecollector/PhabricatorFileTemporaryGarbageCollector.php', 'PhabricatorFileTestCase' => 'applications/files/storage/__tests__/PhabricatorFileTestCase.php', 'PhabricatorFileTestDataGenerator' => 'applications/files/lipsum/PhabricatorFileTestDataGenerator.php', + 'PhabricatorFileThumbnailTransform' => 'applications/files/transform/PhabricatorFileThumbnailTransform.php', 'PhabricatorFileTransaction' => 'applications/files/storage/PhabricatorFileTransaction.php', 'PhabricatorFileTransactionComment' => 'applications/files/storage/PhabricatorFileTransactionComment.php', 'PhabricatorFileTransactionQuery' => 'applications/files/query/PhabricatorFileTransactionQuery.php', + 'PhabricatorFileTransform' => 'applications/files/transform/PhabricatorFileTransform.php', 'PhabricatorFileTransformController' => 'applications/files/controller/PhabricatorFileTransformController.php', + 'PhabricatorFileTransformListController' => 'applications/files/controller/PhabricatorFileTransformListController.php', 'PhabricatorFileUploadController' => 'applications/files/controller/PhabricatorFileUploadController.php', 'PhabricatorFileUploadDialogController' => 'applications/files/controller/PhabricatorFileUploadDialogController.php', 'PhabricatorFileUploadException' => 'applications/files/exception/PhabricatorFileUploadException.php', @@ -2823,7 +2828,6 @@ phutil_register_library_map(array( 'PholioImageUploadController' => 'applications/pholio/controller/PholioImageUploadController.php', 'PholioInlineController' => 'applications/pholio/controller/PholioInlineController.php', 'PholioInlineListController' => 'applications/pholio/controller/PholioInlineListController.php', - 'PholioInlineThumbController' => 'applications/pholio/controller/PholioInlineThumbController.php', 'PholioMock' => 'applications/pholio/storage/PholioMock.php', 'PholioMockCommentController' => 'applications/pholio/controller/PholioMockCommentController.php', 'PholioMockEditController' => 'applications/pholio/controller/PholioMockEditController.php', @@ -3530,6 +3534,7 @@ phutil_register_library_map(array( 'DifferentialActionEmailCommand' => 'MetaMTAEmailTransactionCommand', 'DifferentialActionMenuEventListener' => 'PhabricatorEventListener', 'DifferentialAddCommentView' => 'AphrontView', + 'DifferentialAdjustmentMapTestCase' => 'ArcanistPhutilTestCase', 'DifferentialAffectedPath' => 'DifferentialDAO', 'DifferentialApplyPatchField' => 'DifferentialCustomField', 'DifferentialArcanistProjectField' => 'DifferentialCustomField', @@ -3632,6 +3637,7 @@ phutil_register_library_map(array( 'DifferentialJIRAIssuesField' => 'DifferentialStoredCustomField', 'DifferentialLandingActionMenuEventListener' => 'PhabricatorEventListener', 'DifferentialLegacyHunk' => 'DifferentialHunk', + 'DifferentialLineAdjustmentMap' => 'Phobject', 'DifferentialLintField' => 'DifferentialCustomField', 'DifferentialLocalCommitsView' => 'AphrontView', 'DifferentialManiphestTasksField' => 'DifferentialCoreCustomField', @@ -4516,7 +4522,7 @@ phutil_register_library_map(array( 'PHUIHandleListView' => 'AphrontTagView', 'PHUIHandleTagListView' => 'AphrontTagView', 'PHUIHandleView' => 'AphrontView', - 'PHUIHeaderView' => 'AphrontView', + 'PHUIHeaderView' => 'AphrontTagView', 'PHUIIconExample' => 'PhabricatorUIExample', 'PHUIIconView' => 'AphrontTagView', 'PHUIImageMaskExample' => 'PhabricatorUIExample', @@ -4835,7 +4841,6 @@ phutil_register_library_map(array( 'PhabricatorCacheSpec' => 'Phobject', 'PhabricatorCacheTTLGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorCalendarApplication' => 'PhabricatorApplication', - 'PhabricatorCalendarBrowseController' => 'PhabricatorCalendarController', 'PhabricatorCalendarController' => 'PhabricatorController', 'PhabricatorCalendarDAO' => 'PhabricatorLiskDAO', 'PhabricatorCalendarEvent' => array( @@ -5261,6 +5266,7 @@ phutil_register_library_map(array( 'PhabricatorFlaggableInterface', 'PhabricatorPolicyInterface', ), + 'PhabricatorFileImageTransform' => 'PhabricatorFileTransform', 'PhabricatorFileInfoController' => 'PhabricatorFileController', 'PhabricatorFileLinkListView' => 'AphrontView', 'PhabricatorFileLinkView' => 'AphrontView', @@ -5273,10 +5279,13 @@ phutil_register_library_map(array( 'PhabricatorFileTemporaryGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorFileTestCase' => 'PhabricatorTestCase', 'PhabricatorFileTestDataGenerator' => 'PhabricatorTestDataGenerator', + 'PhabricatorFileThumbnailTransform' => 'PhabricatorFileImageTransform', 'PhabricatorFileTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorFileTransactionComment' => 'PhabricatorApplicationTransactionComment', 'PhabricatorFileTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhabricatorFileTransform' => 'Phobject', 'PhabricatorFileTransformController' => 'PhabricatorFileController', + 'PhabricatorFileTransformListController' => 'PhabricatorFileController', 'PhabricatorFileUploadController' => 'PhabricatorFileController', 'PhabricatorFileUploadDialogController' => 'PhabricatorFileController', 'PhabricatorFileUploadException' => 'Exception', @@ -6309,7 +6318,6 @@ phutil_register_library_map(array( 'PholioImageUploadController' => 'PholioController', 'PholioInlineController' => 'PholioController', 'PholioInlineListController' => 'PholioController', - 'PholioInlineThumbController' => 'PholioController', 'PholioMock' => array( 'PholioDAO', 'PhabricatorMarkupInterface', diff --git a/src/aphront/AphrontRequest.php b/src/aphront/AphrontRequest.php index 197e5a437c..ab6e0d9820 100644 --- a/src/aphront/AphrontRequest.php +++ b/src/aphront/AphrontRequest.php @@ -650,7 +650,7 @@ final class AphrontRequest { * safe. */ public function isProxiedClusterRequest() { - return (bool)AphrontRequest::getHTTPHeader('X-Phabricator-Cluster'); + return (bool)self::getHTTPHeader('X-Phabricator-Cluster'); } diff --git a/src/aphront/response/AphrontProxyResponse.php b/src/aphront/response/AphrontProxyResponse.php index 7e27ea2b1b..175f2d2c53 100644 --- a/src/aphront/response/AphrontProxyResponse.php +++ b/src/aphront/response/AphrontProxyResponse.php @@ -65,7 +65,10 @@ abstract class AphrontProxyResponse extends AphrontResponse { final public function buildResponseString() { throw new Exception( - 'AphrontProxyResponse must implement reduceProxyResponse().'); + pht( + '%s must implement %s.', + __CLASS__, + 'reduceProxyResponse()')); } } diff --git a/src/aphront/response/AphrontResponse.php b/src/aphront/response/AphrontResponse.php index 0bbe22b5ec..b1e72f5529 100644 --- a/src/aphront/response/AphrontResponse.php +++ b/src/aphront/response/AphrontResponse.php @@ -154,7 +154,7 @@ abstract class AphrontResponse { array_walk_recursive( $object, - array('AphrontResponse', 'processValueForJSONEncoding')); + array(__CLASS__, 'processValueForJSONEncoding')); $response = json_encode($object); diff --git a/src/applications/audit/constants/PhabricatorAuditStatusConstants.php b/src/applications/audit/constants/PhabricatorAuditStatusConstants.php index e1a4378ab3..78924905b9 100644 --- a/src/applications/audit/constants/PhabricatorAuditStatusConstants.php +++ b/src/applications/audit/constants/PhabricatorAuditStatusConstants.php @@ -60,19 +60,19 @@ final class PhabricatorAuditStatusConstants { public static function getStatusIcon($code) { switch ($code) { - case PhabricatorAuditStatusConstants::AUDIT_NOT_REQUIRED: - case PhabricatorAuditStatusConstants::RESIGNED: + case self::AUDIT_NOT_REQUIRED: + case self::RESIGNED: $icon = PHUIStatusItemView::ICON_OPEN; break; - case PhabricatorAuditStatusConstants::AUDIT_REQUIRED: - case PhabricatorAuditStatusConstants::AUDIT_REQUESTED: + case self::AUDIT_REQUIRED: + case self::AUDIT_REQUESTED: $icon = PHUIStatusItemView::ICON_WARNING; break; - case PhabricatorAuditStatusConstants::CONCERNED: + case self::CONCERNED: $icon = PHUIStatusItemView::ICON_REJECT; break; - case PhabricatorAuditStatusConstants::ACCEPTED: - case PhabricatorAuditStatusConstants::CLOSED: + case self::ACCEPTED: + case self::CLOSED: $icon = PHUIStatusItemView::ICON_ACCEPT; break; default: diff --git a/src/applications/audit/storage/PhabricatorAuditInlineComment.php b/src/applications/audit/storage/PhabricatorAuditInlineComment.php index 8c0aa78d5d..292e0274a5 100644 --- a/src/applications/audit/storage/PhabricatorAuditInlineComment.php +++ b/src/applications/audit/storage/PhabricatorAuditInlineComment.php @@ -115,7 +115,7 @@ final class PhabricatorAuditInlineComment private static function buildProxies(array $inlines) { $results = array(); foreach ($inlines as $key => $inline) { - $results[$key] = PhabricatorAuditInlineComment::newFromModernComment( + $results[$key] = self::newFromModernComment( $inline); } return $results; diff --git a/src/applications/auth/controller/PhabricatorAuthRegisterController.php b/src/applications/auth/controller/PhabricatorAuthRegisterController.php index 6c40375227..d27a644480 100644 --- a/src/applications/auth/controller/PhabricatorAuthRegisterController.php +++ b/src/applications/auth/controller/PhabricatorAuthRegisterController.php @@ -604,17 +604,9 @@ final class PhabricatorAuthRegisterController return null; } - try { - $xformer = new PhabricatorImageTransformer(); - return $xformer->executeProfileTransform( - $file, - $width = 50, - $min_height = 50, - $max_height = 50); - } catch (Exception $ex) { - phlog($ex); - return null; - } + $xform = PhabricatorFileTransform::getTransformByKey( + PhabricatorFileThumbnailTransform::TRANSFORM_PROFILE); + return $xform->executeTransform($file); } protected function renderError($message) { diff --git a/src/applications/auth/provider/PhabricatorAuthProvider.php b/src/applications/auth/provider/PhabricatorAuthProvider.php index b1b064c363..5ffb18b41a 100644 --- a/src/applications/auth/provider/PhabricatorAuthProvider.php +++ b/src/applications/auth/provider/PhabricatorAuthProvider.php @@ -15,8 +15,7 @@ abstract class PhabricatorAuthProvider { public function getProviderConfig() { if ($this->providerConfig === null) { - throw new Exception( - 'Call attachProviderConfig() before getProviderConfig()!'); + throw new PhutilInvalidStateException('attachProviderConfig'); } return $this->providerConfig; } diff --git a/src/applications/auth/view/PhabricatorAuthAccountView.php b/src/applications/auth/view/PhabricatorAuthAccountView.php index a68cdfff07..8eb73144aa 100644 --- a/src/applications/auth/view/PhabricatorAuthAccountView.php +++ b/src/applications/auth/view/PhabricatorAuthAccountView.php @@ -89,15 +89,28 @@ final class PhabricatorAuthAccountView extends AphrontView { $account_uri); } - $image_uri = $account->getProfileImageFile()->getProfileThumbURI(); + $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', - 'style' => 'background-image: url('.$image_uri.')', ), - $content); + array( + $profile_image, + $content, + )); } } diff --git a/src/applications/base/PhabricatorApplication.php b/src/applications/base/PhabricatorApplication.php index d5237adc11..295d4db180 100644 --- a/src/applications/base/PhabricatorApplication.php +++ b/src/applications/base/PhabricatorApplication.php @@ -361,7 +361,7 @@ abstract class PhabricatorApplication implements PhabricatorPolicyInterface { public static function getByClass($class_name) { $selected = null; - $applications = PhabricatorApplication::getAllApplications(); + $applications = self::getAllApplications(); foreach ($applications as $application) { if (get_class($application) == $class_name) { diff --git a/src/applications/cache/PhabricatorCaches.php b/src/applications/cache/PhabricatorCaches.php index 37708457b7..4e3af7a6a4 100644 --- a/src/applications/cache/PhabricatorCaches.php +++ b/src/applications/cache/PhabricatorCaches.php @@ -270,7 +270,7 @@ final class PhabricatorCaches { } private static function addNamespaceToCaches(array $caches) { - $namespace = PhabricatorCaches::getNamespace(); + $namespace = self::getNamespace(); if (!$namespace) { return $caches; } diff --git a/src/applications/calendar/controller/PhabricatorCalendarBrowseController.php b/src/applications/calendar/controller/PhabricatorCalendarBrowseController.php deleted file mode 100644 index ba2017cba9..0000000000 --- a/src/applications/calendar/controller/PhabricatorCalendarBrowseController.php +++ /dev/null @@ -1,97 +0,0 @@ -getViewer(); - - $now = time(); - $year_d = phabricator_format_local_time($now, $viewer, 'Y'); - $year = $request->getInt('year', $year_d); - $month_d = phabricator_format_local_time($now, $viewer, 'm'); - $month = $request->getInt('month', $month_d); - $day = phabricator_format_local_time($now, $viewer, 'j'); - - $holidays = id(new PhabricatorCalendarHoliday())->loadAllWhere( - 'day BETWEEN %s AND %s', - "{$year}-{$month}-01", - "{$year}-{$month}-31"); - - $statuses = id(new PhabricatorCalendarEventQuery()) - ->setViewer($viewer) - ->withDateRange( - strtotime("{$year}-{$month}-01"), - strtotime("{$year}-{$month}-01 next month")) - ->execute(); - - if ($month == $month_d && $year == $year_d) { - $month_view = new PHUICalendarMonthView($month, $year, $day); - } else { - $month_view = new PHUICalendarMonthView($month, $year); - } - - $month_view->setBrowseURI($request->getRequestURI()); - $month_view->setUser($viewer); - $month_view->setHolidays($holidays); - - $phids = mpull($statuses, 'getUserPHID'); - $handles = $viewer->loadHandles($phids); - - /* Assign Colors */ - $unique = array_unique($phids); - $allblue = false; - $calcolors = CalendarColors::getColors(); - if (count($unique) > count($calcolors)) { - $allblue = true; - } - $i = 0; - $eventcolor = array(); - foreach ($unique as $phid) { - if ($allblue) { - $eventcolor[$phid] = CalendarColors::COLOR_SKY; - } else { - $eventcolor[$phid] = $calcolors[$i]; - } - $i++; - } - - foreach ($statuses as $status) { - $event = new AphrontCalendarEventView(); - $event->setEpochRange($status->getDateFrom(), $status->getDateTo()); - - $name_text = $handles[$status->getUserPHID()]->getName(); - $status_text = $status->getHumanStatus(); - $event->setUserPHID($status->getUserPHID()); - $event->setDescription(pht('%s (%s)', $name_text, $status_text)); - $event->setName($status_text); - $event->setEventID($status->getID()); - $event->setColor($eventcolor[$status->getUserPHID()]); - $month_view->addEvent($event); - } - - $date = new DateTime("{$year}-{$month}-01"); - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('All Events')); - $crumbs->addTextCrumb($date->format('F Y')); - - $nav = $this->buildSideNavView(); - $nav->selectFilter('all/'); - $nav->appendChild( - array( - $crumbs, - $month_view, - )); - - return $this->buildApplicationPage( - $nav, - array( - 'title' => pht('Calendar'), - )); - } - -} diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php b/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php index 38d12b099b..e31b9bdf0e 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php @@ -74,6 +74,7 @@ final class PhabricatorCalendarEventEditController $name = $event->getName(); $description = $event->getDescription(); $type = $event->getStatus(); + $is_all_day = $event->getIsAllDay(); $current_policies = id(new PhabricatorPolicyQuery()) ->setViewer($user) @@ -95,6 +96,7 @@ final class PhabricatorCalendarEventEditController $subscribers = $request->getArr('subscribers'); $edit_policy = $request->getStr('editPolicy'); $view_policy = $request->getStr('viewPolicy'); + $is_all_day = $request->getStr('isAllDay'); $invitees = $request->getArr('invitees'); $new_invitees = $this->getNewInviteeList($invitees, $event); @@ -111,6 +113,11 @@ final class PhabricatorCalendarEventEditController PhabricatorCalendarEventTransaction::TYPE_NAME) ->setNewValue($name); + $xactions[] = id(new PhabricatorCalendarEventTransaction()) + ->setTransactionType( + PhabricatorCalendarEventTransaction::TYPE_ALL_DAY) + ->setNewValue($is_all_day); + $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_START_DATE) @@ -172,6 +179,16 @@ final class PhabricatorCalendarEventEditController } } + $all_day_id = celerity_generate_unique_node_id(); + $start_date_id = celerity_generate_unique_node_id(); + $end_date_id = celerity_generate_unique_node_id(); + + Javelin::initBehavior('event-all-day', array( + 'allDayID' => $all_day_id, + 'startDateID' => $start_date_id, + 'endDateID' => $end_date_id, + )); + $name = id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setName('name') @@ -184,19 +201,31 @@ final class PhabricatorCalendarEventEditController ->setValue($type) ->setOptions($event->getStatusOptions()); + $all_day_checkbox = id(new AphrontFormCheckboxControl()) + ->addCheckbox( + 'isAllDay', + 1, + pht('All Day Event'), + $is_all_day, + $all_day_id); + $start_control = id(new AphrontFormDateControl()) ->setUser($user) ->setName('start') ->setLabel(pht('Start')) ->setError($error_start_date) - ->setValue($start_value); + ->setValue($start_value) + ->setID($start_date_id) + ->setIsTimeDisabled($is_all_day); $end_control = id(new AphrontFormDateControl()) ->setUser($user) ->setName('end') ->setLabel(pht('End')) ->setError($error_end_date) - ->setValue($end_value); + ->setValue($end_value) + ->setID($end_date_id) + ->setIsTimeDisabled($is_all_day); $description = id(new AphrontFormTextAreaControl()) ->setLabel(pht('Description')) @@ -234,6 +263,7 @@ final class PhabricatorCalendarEventEditController ->setUser($user) ->appendChild($name) ->appendChild($status_select) + ->appendChild($all_day_checkbox) ->appendChild($start_control) ->appendChild($end_control) ->appendControl($view_policies) diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php b/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php index 6f6fa66e6e..742ec95e3f 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php @@ -179,13 +179,31 @@ final class PhabricatorCalendarEventViewController ->setUser($viewer) ->setObject($event); - $properties->addProperty( - pht('Starts'), - phabricator_datetime($event->getDateFrom(), $viewer)); + if ($event->getIsAllDay()) { + $date_start = phabricator_date($event->getDateFrom(), $viewer); + $date_end = phabricator_date($event->getDateTo(), $viewer); - $properties->addProperty( - pht('Ends'), - phabricator_datetime($event->getDateTo(), $viewer)); + if ($date_start == $date_end) { + $properties->addProperty( + pht('Time'), + phabricator_date($event->getDateFrom(), $viewer)); + } else { + $properties->addProperty( + pht('Starts'), + phabricator_date($event->getDateFrom(), $viewer)); + $properties->addProperty( + pht('Ends'), + phabricator_date($event->getDateTo(), $viewer)); + } + } else { + $properties->addProperty( + pht('Starts'), + phabricator_datetime($event->getDateFrom(), $viewer)); + + $properties->addProperty( + pht('Ends'), + phabricator_datetime($event->getDateTo(), $viewer)); + } $properties->addProperty( pht('Host'), diff --git a/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php b/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php index 09ab1309eb..6e80d19236 100644 --- a/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php +++ b/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php @@ -21,6 +21,7 @@ final class PhabricatorCalendarEventEditor $types[] = PhabricatorCalendarEventTransaction::TYPE_DESCRIPTION; $types[] = PhabricatorCalendarEventTransaction::TYPE_CANCEL; $types[] = PhabricatorCalendarEventTransaction::TYPE_INVITE; + $types[] = PhabricatorCalendarEventTransaction::TYPE_ALL_DAY; $types[] = PhabricatorTransactions::TYPE_COMMENT; $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; @@ -49,6 +50,8 @@ final class PhabricatorCalendarEventEditor return $object->getDescription(); case PhabricatorCalendarEventTransaction::TYPE_CANCEL: return $object->getIsCancelled(); + case PhabricatorCalendarEventTransaction::TYPE_ALL_DAY: + return (int)$object->getIsAllDay(); case PhabricatorCalendarEventTransaction::TYPE_INVITE: $map = $xaction->getNewValue(); $phids = array_keys($map); @@ -87,6 +90,8 @@ final class PhabricatorCalendarEventEditor case PhabricatorCalendarEventTransaction::TYPE_CANCEL: case PhabricatorCalendarEventTransaction::TYPE_INVITE: return $xaction->getNewValue(); + case PhabricatorCalendarEventTransaction::TYPE_ALL_DAY: + return (int)$xaction->getNewValue(); case PhabricatorCalendarEventTransaction::TYPE_STATUS: return (int)$xaction->getNewValue(); case PhabricatorCalendarEventTransaction::TYPE_START_DATE: @@ -120,6 +125,9 @@ final class PhabricatorCalendarEventEditor case PhabricatorCalendarEventTransaction::TYPE_CANCEL: $object->setIsCancelled((int)$xaction->getNewValue()); return; + case PhabricatorCalendarEventTransaction::TYPE_ALL_DAY: + $object->setIsAllDay((int)$xaction->getNewValue()); + return; case PhabricatorCalendarEventTransaction::TYPE_INVITE: case PhabricatorTransactions::TYPE_COMMENT: case PhabricatorTransactions::TYPE_VIEW_POLICY: @@ -143,6 +151,7 @@ final class PhabricatorCalendarEventEditor case PhabricatorCalendarEventTransaction::TYPE_STATUS: case PhabricatorCalendarEventTransaction::TYPE_DESCRIPTION: case PhabricatorCalendarEventTransaction::TYPE_CANCEL: + case PhabricatorCalendarEventTransaction::TYPE_ALL_DAY: return; case PhabricatorCalendarEventTransaction::TYPE_INVITE: $map = $xaction->getNewValue(); @@ -175,6 +184,16 @@ final class PhabricatorCalendarEventEditor return parent::applyCustomExternalTransaction($object, $xaction); } + protected function didApplyInternalEffects( + PhabricatorLiskDAO $object, + array $xactions) { + + $object->removeViewerTimezone($this->requireActor()); + + return $xactions; + } + + protected function validateAllTransactions( PhabricatorLiskDAO $object, array $xactions) { diff --git a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php index 1fd294781f..c66e367793 100644 --- a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php +++ b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php @@ -56,7 +56,13 @@ final class PhabricatorCalendarEventQuery $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); - return $table->loadAllFromArray($data); + $events = $table->loadAllFromArray($data); + + foreach ($events as $event) { + $event->applyViewerTimezone($this->getViewer()); + } + + return $events; } protected function buildJoinClauseParts(AphrontDatabaseConnection $conn_r) { diff --git a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php index b525420ca3..c8a788adb8 100644 --- a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php +++ b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php @@ -57,8 +57,8 @@ final class PhabricatorCalendarEventSearchEngine $min_range = $this->getDateFrom($saved)->getEpoch(); $max_range = $this->getDateTo($saved)->getEpoch(); - if ($saved->getParameter('display') == 'month' || - $saved->getParameter('display') == 'day') { + if ($this->isMonthView($saved) || + $this->isDayView($saved)) { list($start_year, $start_month, $start_day) = $this->getDisplayYearAndMonthAndDay($saved); @@ -67,20 +67,42 @@ final class PhabricatorCalendarEventSearchEngine $timezone); $next = clone $start_day; - if ($saved->getParameter('display') == 'month') { + if ($this->isMonthView($saved)) { $next->modify('+1 month'); - } else if ($saved->getParameter('display') == 'day') { + } else if ($this->isDayView($saved)) { $next->modify('+6 day'); } $display_start = $start_day->format('U'); $display_end = $next->format('U'); + // 0 = Sunday is always the start of the week, for now + $start_of_week = 0; + $end_of_week = 6 - $start_of_week; + + $first_of_month = $start_day->format('w'); + $last_of_month = id(clone $next)->modify('-1 day')->format('w'); + if (!$min_range || ($min_range < $display_start)) { $min_range = $display_start; + + if ($this->isMonthView($saved) && + $first_of_month > $start_of_week) { + $min_range = id(clone $start_day) + ->modify('-'.$first_of_month.' days') + ->format('U'); + } } if (!$max_range || ($max_range > $display_end)) { $max_range = $display_end; + + if ($this->isMonthView($saved) && + $last_of_month < $end_of_week) { + $max_range = id(clone $next) + ->modify('+'.(6 - $first_of_month).' days') + ->format('U'); + } + } } @@ -220,6 +242,7 @@ final class PhabricatorCalendarEventSearchEngine protected function getBuiltinQueryNames() { $names = array( 'month' => pht('Month View'), + 'day' => pht('Day View'), 'upcoming' => pht('Upcoming Events'), 'all' => pht('All Events'), ); @@ -242,6 +265,8 @@ final class PhabricatorCalendarEventSearchEngine switch ($query_key) { case 'month': return $query->setParameter('display', 'month'); + case 'day': + return $query->setParameter('display', 'day'); case 'upcoming': return $query->setParameter('upcoming', true); case 'all': @@ -266,9 +291,9 @@ final class PhabricatorCalendarEventSearchEngine PhabricatorSavedQuery $query, array $handles) { - if ($query->getParameter('display') == 'month') { + if ($this->isMonthView($query)) { return $this->buildCalendarView($events, $query, $handles); - } else if ($query->getParameter('display') == 'day') { + } else if ($this->isDayView($query)) { return $this->buildCalendarDayView($events, $query, $handles); } @@ -281,15 +306,12 @@ final class PhabricatorCalendarEventSearchEngine $to = phabricator_datetime($event->getDateTo(), $viewer); $creator_handle = $handles[$event->getUserPHID()]; - $name = (strlen($event->getName())) ? - $event->getName() : $event->getTerseSummary($viewer); - $color = ($event->getStatus() == PhabricatorCalendarEvent::STATUS_AWAY) ? 'red' : 'yellow'; $item = id(new PHUIObjectItemView()) - ->setHeader($name) + ->setHeader($event->getName()) ->setHref($href) ->setBarColor($color) ->addByline(pht('Creator: %s', $creator_handle->renderLink())) @@ -320,11 +342,15 @@ final class PhabricatorCalendarEventSearchEngine if ($start_month == $now_month && $start_year == $now_year) { $month_view = new PHUICalendarMonthView( + $this->getDateFrom($query), + $this->getDateTo($query), $start_month, $start_year, $now_day); } else { $month_view = new PHUICalendarMonthView( + $this->getDateFrom($query), + $this->getDateTo($query), $start_month, $start_year); } @@ -354,9 +380,10 @@ final class PhabricatorCalendarEventSearchEngine foreach ($statuses as $status) { $event = new AphrontCalendarEventView(); $event->setEpochRange($status->getDateFrom(), $status->getDateTo()); + $event->setIsAllDay($status->getIsAllDay()); $name_text = $handles[$status->getUserPHID()]->getName(); - $status_text = $status->getHumanStatus(); + $status_text = $status->getName(); $event->setUserPHID($status->getUserPHID()); $event->setDescription(pht('%s (%s)', $name_text, $status_text)); $event->setName($status_text); @@ -380,6 +407,8 @@ final class PhabricatorCalendarEventSearchEngine $this->getDisplayYearAndMonthAndDay($query); $day_view = new PHUICalendarDayView( + $this->getDateFrom($query), + $this->getDateTo($query), $start_year, $start_month, $start_day); @@ -389,9 +418,14 @@ final class PhabricatorCalendarEventSearchEngine $phids = mpull($statuses, 'getUserPHID'); foreach ($statuses as $status) { + if ($status->getIsCancelled()) { + continue; + } + $event = new AphrontCalendarEventView(); $event->setEventID($status->getID()); $event->setEpochRange($status->getDateFrom(), $status->getDateTo()); + $event->setIsAllDay($status->getIsAllDay()); $event->setName($status->getName()); $event->setURI('/'.$status->getMonogram()); @@ -419,9 +453,14 @@ final class PhabricatorCalendarEventSearchEngine $epoch = time(); } } + if ($this->isMonthView($query)) { + $day = 1; + } else { + $day = phabricator_format_local_time($epoch, $viewer, 'd'); + } $start_year = phabricator_format_local_time($epoch, $viewer, 'Y'); $start_month = phabricator_format_local_time($epoch, $viewer, 'm'); - $start_day = phabricator_format_local_time($epoch, $viewer, 'd'); + $start_day = $day; } return array($start_year, $start_month, $start_day); } @@ -456,4 +495,23 @@ final class PhabricatorCalendarEventSearchEngine return $value; } + private function isMonthView(PhabricatorSavedQuery $query) { + if ($this->isDayView($query)) { + return false; + } + if ($query->getParameter('display') == 'month') { + return true; + } + } + + private function isDayView(PhabricatorSavedQuery $query) { + if ($query->getParameter('display') == 'day') { + return true; + } + if ($this->calendarDay) { + return true; + } + + return false; + } } diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php index 03da6851a5..3dd984ef7d 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php @@ -17,12 +17,14 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO protected $status; protected $description; protected $isCancelled; + protected $isAllDay; protected $mailKey; protected $viewPolicy; protected $editPolicy; private $invitees = self::ATTACHABLE; + private $appliedViewer; const STATUS_AWAY = 1; const STATUS_SPORADIC = 2; @@ -36,15 +38,112 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO return id(new PhabricatorCalendarEvent()) ->setUserPHID($actor->getPHID()) ->setIsCancelled(0) + ->setIsAllDay(0) ->setViewPolicy($actor->getPHID()) ->setEditPolicy($actor->getPHID()) - ->attachInvitees(array()); + ->attachInvitees(array()) + ->applyViewerTimezone($actor); + } + + public function applyViewerTimezone(PhabricatorUser $viewer) { + if ($this->appliedViewer) { + throw new Exception(pht('Viewer timezone is already applied!')); + } + + $this->appliedViewer = $viewer; + + if (!$this->getIsAllDay()) { + return $this; + } + + $zone = $viewer->getTimeZone(); + + + $this->setDateFrom( + $this->getDateEpochForTimeZone( + $this->getDateFrom(), + new DateTimeZone('Pacific/Kiritimati'), + 'Y-m-d', + null, + $zone)); + + $this->setDateTo( + $this->getDateEpochForTimeZone( + $this->getDateTo(), + new DateTimeZone('Pacific/Midway'), + 'Y-m-d 23:59:00', + '-1 day', + $zone)); + + return $this; + } + + + public function removeViewerTimezone(PhabricatorUser $viewer) { + if (!$this->appliedViewer) { + throw new Exception(pht('Viewer timezone is not applied!')); + } + + if ($viewer->getPHID() != $this->appliedViewer->getPHID()) { + throw new Exception(pht('Removed viewer must match applied viewer!')); + } + + $this->appliedViewer = null; + + if (!$this->getIsAllDay()) { + return $this; + } + + $zone = $viewer->getTimeZone(); + + $this->setDateFrom( + $this->getDateEpochForTimeZone( + $this->getDateFrom(), + $zone, + 'Y-m-d', + null, + new DateTimeZone('Pacific/Kiritimati'))); + + $this->setDateTo( + $this->getDateEpochForTimeZone( + $this->getDateTo(), + $zone, + 'Y-m-d', + '+1 day', + new DateTimeZone('Pacific/Midway'))); + + return $this; + } + + private function getDateEpochForTimeZone( + $epoch, + $src_zone, + $format, + $adjust, + $dst_zone) { + + $src = new DateTime('@'.$epoch); + $src->setTimeZone($src_zone); + + if (strlen($adjust)) { + $adjust = ' '.$adjust; + } + + $dst = new DateTime($src->format($format).$adjust, $dst_zone); + return $dst->format('U'); } public function save() { + if ($this->appliedViewer) { + throw new Exception( + pht( + 'Can not save event with viewer timezone still applied!')); + } + if (!$this->mailKey) { $this->mailKey = Filesystem::readRandomCharacters(20); } + return parent::save(); } @@ -69,11 +168,6 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO ); } - public function getHumanStatus() { - $options = $this->getStatusOptions(); - return $options[$this->status]; - } - protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, @@ -84,6 +178,7 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO 'status' => 'uint32', 'description' => 'text', 'isCancelled' => 'bool', + 'isAllDay' => 'bool', 'mailKey' => 'bytes20', ), self::CONFIG_KEY_SCHEMA => array( @@ -105,7 +200,7 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO public function getTerseSummary(PhabricatorUser $viewer) { $until = phabricator_date($this->dateTo, $viewer); - if ($this->status == PhabricatorCalendarEvent::STATUS_SPORADIC) { + if ($this->status == self::STATUS_SPORADIC) { return pht('Sporadic until %s', $until); } else { return pht('Away until %s', $until); diff --git a/src/applications/calendar/storage/PhabricatorCalendarEventTransaction.php b/src/applications/calendar/storage/PhabricatorCalendarEventTransaction.php index 7e4e7ff512..d3e7a25efd 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarEventTransaction.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEventTransaction.php @@ -9,6 +9,7 @@ final class PhabricatorCalendarEventTransaction const TYPE_STATUS = 'calendar.status'; const TYPE_DESCRIPTION = 'calendar.description'; const TYPE_CANCEL = 'calendar.cancel'; + const TYPE_ALL_DAY = 'calendar.allday'; const TYPE_INVITE = 'calendar.invite'; const MAILTAG_RESCHEDULE = 'calendar-reschedule'; @@ -37,6 +38,7 @@ final class PhabricatorCalendarEventTransaction case self::TYPE_STATUS: case self::TYPE_DESCRIPTION: case self::TYPE_CANCEL: + case self::TYPE_ALL_DAY: $phids[] = $this->getObjectPHID(); break; case self::TYPE_INVITE: @@ -58,6 +60,7 @@ final class PhabricatorCalendarEventTransaction case self::TYPE_STATUS: case self::TYPE_DESCRIPTION: case self::TYPE_CANCEL: + case self::TYPE_ALL_DAY: case self::TYPE_INVITE: return ($old === null); } @@ -71,6 +74,7 @@ final class PhabricatorCalendarEventTransaction case self::TYPE_END_DATE: case self::TYPE_STATUS: case self::TYPE_DESCRIPTION: + case self::TYPE_ALL_DAY: case self::TYPE_CANCEL: return 'fa-pencil'; break; @@ -128,6 +132,16 @@ final class PhabricatorCalendarEventTransaction return pht( "%s updated the event's description.", $this->renderHandleLink($author_phid)); + case self::TYPE_ALL_DAY: + if ($new) { + return pht( + '%s made this an all day event.', + $this->renderHandleLink($author_phid)); + } else { + return pht( + '%s converted this from an all day event.', + $this->renderHandleLink($author_phid)); + } case self::TYPE_CANCEL: if ($new) { return pht( @@ -287,6 +301,18 @@ final class PhabricatorCalendarEventTransaction '%s updated the description of %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); + case self::TYPE_ALL_DAY: + if ($new) { + return pht( + '%s made %s an all day event.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid)); + } else { + return pht( + '%s converted %s from an all day event.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid)); + } case self::TYPE_CANCEL: if ($new) { return pht( diff --git a/src/applications/calendar/view/AphrontCalendarEventView.php b/src/applications/calendar/view/AphrontCalendarEventView.php index aebfaa9a50..11dbff3847 100644 --- a/src/applications/calendar/view/AphrontCalendarEventView.php +++ b/src/applications/calendar/view/AphrontCalendarEventView.php @@ -10,6 +10,7 @@ final class AphrontCalendarEventView extends AphrontView { private $eventID; private $color; private $uri; + private $isAllDay; public function setURI($uri) { $this->uri = $uri; @@ -81,14 +82,16 @@ final class AphrontCalendarEventView extends AphrontView { } } - public function getAllDay() { - $time = (60 * 60 * 22); - if (($this->getEpochEnd() - $this->getEpochStart()) >= $time) { - return true; - } - return false; + public function setIsAllDay($is_all_day) { + $this->isAllDay = $is_all_day; + return $this; } + public function getIsAllDay() { + return $this->isAllDay; + } + + public function getMultiDay() { $nextday = strtotime('12:00 AM Tomorrow', $this->getEpochStart()); if ($this->getEpochEnd() > $nextday) { diff --git a/src/applications/celerity/CelerityResourceGraph.php b/src/applications/celerity/CelerityResourceGraph.php index a85c3ffbd8..715a64c35c 100644 --- a/src/applications/celerity/CelerityResourceGraph.php +++ b/src/applications/celerity/CelerityResourceGraph.php @@ -7,9 +7,7 @@ final class CelerityResourceGraph extends AbstractDirectedGraph { protected function loadEdges(array $nodes) { if (!$this->graphSet) { - throw new Exception( - 'Call setResourceGraph before loading the graph!' - ); + throw new PhutilInvalidStateException('setResourceGraph'); } $graph = $this->getResourceGraph(); diff --git a/src/applications/celerity/resources/CelerityPhysicalResources.php b/src/applications/celerity/resources/CelerityPhysicalResources.php index 995b9f1a55..41665d15f2 100644 --- a/src/applications/celerity/resources/CelerityPhysicalResources.php +++ b/src/applications/celerity/resources/CelerityPhysicalResources.php @@ -25,7 +25,7 @@ abstract class CelerityPhysicalResources extends CelerityResources { $resources_map = array(); $resources_list = id(new PhutilSymbolLoader()) - ->setAncestorClass('CelerityPhysicalResources') + ->setAncestorClass(__CLASS__) ->loadObjects(); foreach ($resources_list as $resources) { diff --git a/src/applications/conduit/call/ConduitCall.php b/src/applications/conduit/call/ConduitCall.php index aa55d1b895..7d767cd8c1 100644 --- a/src/applications/conduit/call/ConduitCall.php +++ b/src/applications/conduit/call/ConduitCall.php @@ -150,5 +150,9 @@ final class ConduitCall { return $method; } + public function getMethodImplementation() { + return $this->handler; + } + } diff --git a/src/applications/conduit/controller/PhabricatorConduitAPIController.php b/src/applications/conduit/controller/PhabricatorConduitAPIController.php index e8e69f18e3..cdc14da27d 100644 --- a/src/applications/conduit/controller/PhabricatorConduitAPIController.php +++ b/src/applications/conduit/controller/PhabricatorConduitAPIController.php @@ -21,6 +21,7 @@ final class PhabricatorConduitAPIController $method = $this->method; $api_request = null; + $method_implementation = null; $log = new PhabricatorConduitMethodCallLog(); $log->setMethod($method); @@ -36,6 +37,7 @@ final class PhabricatorConduitAPIController list($metadata, $params) = $this->decodeConduitParams($request, $method); $call = new ConduitCall($method, $params); + $method_implementation = $call->getMethodImplementation(); $result = null; @@ -151,7 +153,8 @@ final class PhabricatorConduitAPIController return $this->buildHumanReadableResponse( $method, $api_request, - $response->toDictionary()); + $response->toDictionary(), + $method_implementation); case 'json': default: return id(new AphrontJSONResponse()) @@ -525,7 +528,8 @@ final class PhabricatorConduitAPIController private function buildHumanReadableResponse( $method, ConduitAPIRequest $request = null, - $result = null) { + $result = null, + ConduitAPIMethod $method_implementation = null) { $param_rows = array(); $param_rows[] = array('Method', $this->renderAPIValue($method)); @@ -574,11 +578,20 @@ final class PhabricatorConduitAPIController ->addTextCrumb($method, $method_uri) ->addTextCrumb(pht('Call')); + $example_panel = null; + if ($request && $method_implementation) { + $params = $request->getAllParameters(); + $example_panel = $this->renderExampleBox( + $method_implementation, + $params); + } + return $this->buildApplicationPage( array( $crumbs, $param_panel, $result_panel, + $example_panel, ), array( 'title' => pht('Method Call Result'), diff --git a/src/applications/conduit/controller/PhabricatorConduitConsoleController.php b/src/applications/conduit/controller/PhabricatorConduitConsoleController.php index 62cdb7ba77..5c0ccfe6fb 100644 --- a/src/applications/conduit/controller/PhabricatorConduitConsoleController.php +++ b/src/applications/conduit/controller/PhabricatorConduitConsoleController.php @@ -3,31 +3,23 @@ final class PhabricatorConduitConsoleController extends PhabricatorConduitController { - private $method; - public function shouldAllowPublic() { return true; } - public function willProcessRequest(array $data) { - $this->method = $data['method']; - } - - public function processRequest() { - - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); + $method_name = $request->getURIData('method'); $method = id(new PhabricatorConduitMethodQuery()) ->setViewer($viewer) - ->withMethods(array($this->method)) + ->withMethods(array($method_name)) ->executeOne(); - if (!$method) { return new Aphront404Response(); } - $can_call_method = false; + $call_uri = '/api/'.$method->getAPIMethodName(); $status = $method->getMethodStatus(); $reason = $method->getMethodStatusDescription(); @@ -48,37 +40,13 @@ final class PhabricatorConduitConsoleController break; } - $error_types = $method->getErrorTypes(); - $error_types['ERR-CONDUIT-CORE'] = pht('See error message for details.'); - $error_description = array(); - foreach ($error_types as $error => $meaning) { - $error_description[] = hsprintf( - '
  • %s: %s
  • ', - $error, - $meaning); - } - $error_description = phutil_tag('ul', array(), $error_description); - - $form = new AphrontFormView(); - $form + $form = id(new AphrontFormView()) + ->setAction($call_uri) ->setUser($request->getUser()) - ->setAction('/api/'.$this->method) - ->appendChild( - id(new AphrontFormStaticControl()) - ->setLabel('Description') - ->setValue($method->getMethodDescription())) - ->appendChild( - id(new AphrontFormStaticControl()) - ->setLabel('Returns') - ->setValue($method->getReturnType())) - ->appendChild( - id(new AphrontFormMarkupControl()) - ->setLabel('Errors') - ->setValue($error_description)) - ->appendChild(hsprintf( - '

    Enter parameters using '. - 'JSON. For instance, to enter a list, type: '. - '["apple", "banana", "cherry"]')); + ->appendRemarkupInstructions( + pht( + 'Enter parameters using **JSON**. For instance, to enter a '. + 'list, type: `["apple", "banana", "cherry"]`')); $params = $method->getParamTypes(); foreach ($params as $param => $desc) { @@ -117,12 +85,22 @@ final class PhabricatorConduitConsoleController ->setHeader($method->getAPIMethodName()); $form_box = id(new PHUIObjectBoxView()) - ->setHeader($header) - ->setFormErrors($errors) + ->setHeaderText(pht('Call Method')) ->appendChild($form); $content = array(); + $properties = $this->buildMethodProperties($method); + + $info_box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('API Method: %s', $method->getAPIMethodName())) + ->setFormErrors($errors) + ->appendChild($properties); + + $content[] = $info_box; + $content[] = $form_box; + $content[] = $this->renderExampleBox($method, null); + $query = $method->newQueryObject(); if ($query) { $orders = $query->getBuiltinOrders(); @@ -185,7 +163,6 @@ final class PhabricatorConduitConsoleController return $this->buildApplicationPage( array( $crumbs, - $form_box, $content, ), array( @@ -193,4 +170,41 @@ final class PhabricatorConduitConsoleController )); } + private function buildMethodProperties(ConduitAPIMethod $method) { + $viewer = $this->getViewer(); + + $view = id(new PHUIPropertyListView()); + + $view->addProperty( + pht('Returns'), + $method->getReturnType()); + + $error_types = $method->getErrorTypes(); + $error_types['ERR-CONDUIT-CORE'] = pht('See error message for details.'); + $error_description = array(); + foreach ($error_types as $error => $meaning) { + $error_description[] = hsprintf( + '

  • %s: %s
  • ', + $error, + $meaning); + } + $error_description = phutil_tag('ul', array(), $error_description); + + $view->addProperty( + pht('Errors'), + $error_description); + + + $description = $method->getMethodDescription(); + $description = PhabricatorMarkupEngine::renderOneObject( + id(new PhabricatorMarkupOneOff())->setContent($description), + 'default', + $viewer); + $view->addSectionHeader(pht('Description')); + $view->addTextContent($description); + + return $view; + } + + } diff --git a/src/applications/conduit/controller/PhabricatorConduitController.php b/src/applications/conduit/controller/PhabricatorConduitController.php index 4643511952..4fa11dbfad 100644 --- a/src/applications/conduit/controller/PhabricatorConduitController.php +++ b/src/applications/conduit/controller/PhabricatorConduitController.php @@ -24,4 +24,250 @@ abstract class PhabricatorConduitController extends PhabricatorController { return $this->buildSideNavView()->getMenu(); } + protected function renderExampleBox(ConduitAPIMethod $method, $params) { + $arc_example = id(new PHUIPropertyListView()) + ->addRawContent($this->renderExample($method, 'arc', $params)); + + $curl_example = id(new PHUIPropertyListView()) + ->addRawContent($this->renderExample($method, 'curl', $params)); + + $php_example = id(new PHUIPropertyListView()) + ->addRawContent($this->renderExample($method, 'php', $params)); + + $panel_link = phutil_tag( + 'a', + array( + 'href' => '/settings/panel/apitokens/', + ), + pht('Conduit API Tokens')); + + $panel_link = phutil_tag('strong', array(), $panel_link); + + $messages = array( + pht( + 'Use the %s panel in Settings to generate or manage API tokens.', + $panel_link), + ); + + $info_view = id(new PHUIInfoView()) + ->setErrors($messages) + ->setSeverity(PHUIInfoView::SEVERITY_NOTICE); + + return id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Examples')) + ->setInfoView($info_view) + ->addPropertyList($arc_example, pht('arc call-conduit')) + ->addPropertyList($curl_example, pht('cURL')) + ->addPropertyList($php_example, pht('PHP')); + } + + private function renderExample( + ConduitAPIMethod $method, + $kind, + $params) { + + switch ($kind) { + case 'arc': + $example = $this->buildArcanistExample($method, $params); + break; + case 'php': + $example = $this->buildPHPExample($method, $params); + break; + case 'curl': + $example = $this->buildCURLExample($method, $params); + break; + default: + throw new Exception(pht('Conduit client "%s" is not known.', $kind)); + } + + return $example; + } + + private function buildArcanistExample( + ConduitAPIMethod $method, + $params) { + + $parts = array(); + + $parts[] = '$ echo '; + if ($params === null) { + $parts[] = phutil_tag('strong', array(), ''); + } else { + $params = $this->simplifyParams($params); + $params = id(new PhutilJSON())->encodeFormatted($params); + $params = trim($params); + $params = csprintf('%s', $params); + $parts[] = phutil_tag('strong', array('class' => 'real'), $params); + } + + $parts[] = ' | '; + $parts[] = 'arc call-conduit '; + + $parts[] = '--conduit-uri '; + $parts[] = phutil_tag( + 'strong', + array('class' => 'real'), + PhabricatorEnv::getURI('/')); + $parts[] = ' '; + + $parts[] = '--conduit-token '; + $parts[] = phutil_tag('strong', array(), ''); + $parts[] = ' '; + + $parts[] = $method->getAPIMethodName(); + + return $this->renderExampleCode($parts); + } + + private function buildPHPExample( + ConduitAPIMethod $method, + $params) { + + $parts = array(); + + $libphutil_path = 'path/to/libphutil/src/__phutil_library_init__.php'; + + $parts[] = '')); + $parts[] = "\";\n"; + + $parts[] = '$api_parameters = '; + if ($params === null) { + $parts[] = 'array('; + $parts[] = phutil_tag('strong', array(), pht('')); + $parts[] = ');'; + } else { + $params = $this->simplifyParams($params); + $params = phutil_var_export($params, true); + $parts[] = phutil_tag('strong', array('class' => 'real'), $params); + $parts[] = ';'; + } + $parts[] = "\n\n"; + + $parts[] = '$client = new ConduitClient('; + $parts[] = phutil_tag( + 'strong', + array('class' => 'real'), + phutil_var_export(PhabricatorEnv::getURI('/'), true)); + $parts[] = ");\n"; + + $parts[] = '$client->setConduitToken($api_token);'; + $parts[] = "\n\n"; + + $parts[] = '$result = $client->callMethodSynchronous('; + $parts[] = phutil_tag( + 'strong', + array('class' => 'real'), + phutil_var_export($method->getAPIMethodName(), true)); + $parts[] = ', '; + $parts[] = '$api_parameters'; + $parts[] = ");\n"; + + $parts[] = 'print_r($result);'; + + return $this->renderExampleCode($parts); + } + + private function buildCURLExample( + ConduitAPIMethod $method, + $params) { + + $call_uri = '/api/'.$method->getAPIMethodName(); + + $parts = array(); + + $linebreak = array('\\', phutil_tag('br'), ' '); + + $parts[] = '$ curl '; + $parts[] = phutil_tag( + 'strong', + array('class' => 'real'), + csprintf('%R', PhabricatorEnv::getURI($call_uri))); + $parts[] = ' '; + $parts[] = $linebreak; + + $parts[] = '-d api.token='; + $parts[] = phutil_tag('strong', array(), 'api-token'); + $parts[] = ' '; + $parts[] = $linebreak; + + if ($params === null) { + $parts[] = '-d '; + $parts[] = phutil_tag('strong', array(), 'param'); + $parts[] = '='; + $parts[] = phutil_tag('strong', array(), 'value'); + $parts[] = ' '; + $parts[] = $linebreak; + $parts[] = phutil_tag('strong', array(), '...'); + } else { + $lines = array(); + $params = $this->simplifyParams($params); + + foreach ($params as $key => $value) { + $pieces = $this->getQueryStringParts(null, $key, $value); + foreach ($pieces as $piece) { + $lines[] = array( + '-d ', + phutil_tag('strong', array('class' => 'real'), $piece), + ); + } + } + + $parts[] = phutil_implode_html(array(' ', $linebreak), $lines); + } + + return $this->renderExampleCode($parts); + } + + private function renderExampleCode($example) { + require_celerity_resource('conduit-api-css'); + + return phutil_tag( + 'div', + array( + 'class' => 'PhabricatorMonospaced conduit-api-example-code', + ), + $example); + } + + private function simplifyParams(array $params) { + foreach ($params as $key => $value) { + if ($value === null) { + unset($params[$key]); + } + } + return $params; + } + + private function getQueryStringParts($prefix, $key, $value) { + if ($prefix === null) { + $head = phutil_escape_uri($key); + } else { + $head = $prefix.'['.phutil_escape_uri($key).']'; + } + + if (!is_array($value)) { + return array( + $head.'='.phutil_escape_uri($value), + ); + } + + $results = array(); + foreach ($value as $subkey => $subvalue) { + $subparts = $this->getQueryStringParts($head, $subkey, $subvalue); + foreach ($subparts as $subpart) { + $results[] = $subpart; + } + } + + return $results; + } + } diff --git a/src/applications/conduit/method/ConduitAPIMethod.php b/src/applications/conduit/method/ConduitAPIMethod.php index a048e3e0f0..f6d464cfc5 100644 --- a/src/applications/conduit/method/ConduitAPIMethod.php +++ b/src/applications/conduit/method/ConduitAPIMethod.php @@ -100,9 +100,9 @@ abstract class ConduitAPIMethod $name = $this->getAPIMethodName(); $map = array( - ConduitAPIMethod::METHOD_STATUS_STABLE => 0, - ConduitAPIMethod::METHOD_STATUS_UNSTABLE => 1, - ConduitAPIMethod::METHOD_STATUS_DEPRECATED => 2, + self::METHOD_STATUS_STABLE => 0, + self::METHOD_STATUS_UNSTABLE => 1, + self::METHOD_STATUS_DEPRECATED => 2, ); $ord = idx($map, $this->getMethodStatus(), 0); diff --git a/src/applications/conduit/query/PhabricatorConduitLogQuery.php b/src/applications/conduit/query/PhabricatorConduitLogQuery.php index 4d66cccae5..a08fd25a68 100644 --- a/src/applications/conduit/query/PhabricatorConduitLogQuery.php +++ b/src/applications/conduit/query/PhabricatorConduitLogQuery.php @@ -22,7 +22,7 @@ final class PhabricatorConduitLogQuery $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); - return $table->loadAllFromArray($data);; + return $table->loadAllFromArray($data); } protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { diff --git a/src/applications/conduit/query/PhabricatorConduitTokenQuery.php b/src/applications/conduit/query/PhabricatorConduitTokenQuery.php index 870043cac8..44586f1815 100644 --- a/src/applications/conduit/query/PhabricatorConduitTokenQuery.php +++ b/src/applications/conduit/query/PhabricatorConduitTokenQuery.php @@ -46,7 +46,7 @@ final class PhabricatorConduitTokenQuery $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); - return $table->loadAllFromArray($data);; + return $table->loadAllFromArray($data); } protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { diff --git a/src/applications/conduit/storage/PhabricatorConduitToken.php b/src/applications/conduit/storage/PhabricatorConduitToken.php index ab4d88335e..f5673fdab3 100644 --- a/src/applications/conduit/storage/PhabricatorConduitToken.php +++ b/src/applications/conduit/storage/PhabricatorConduitToken.php @@ -65,7 +65,7 @@ final class PhabricatorConduitToken // to expire) so generate a new token. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); - $token = PhabricatorConduitToken::initializeNewToken( + $token = self::initializeNewToken( $user->getPHID(), self::TYPE_CLUSTER); $token->save(); diff --git a/src/applications/config/check/PhabricatorSetupCheck.php b/src/applications/config/check/PhabricatorSetupCheck.php index b40dd4c8cc..a689d14943 100644 --- a/src/applications/config/check/PhabricatorSetupCheck.php +++ b/src/applications/config/check/PhabricatorSetupCheck.php @@ -113,7 +113,7 @@ abstract class PhabricatorSetupCheck { final public static function runAllChecks() { $symbols = id(new PhutilSymbolLoader()) - ->setAncestorClass('PhabricatorSetupCheck') + ->setAncestorClass(__CLASS__) ->setConcreteOnly(true) ->selectAndLoadSymbols(); diff --git a/src/applications/config/option/PhabricatorApplicationConfigOptions.php b/src/applications/config/option/PhabricatorApplicationConfigOptions.php index 6982ef513f..7ac1df3e16 100644 --- a/src/applications/config/option/PhabricatorApplicationConfigOptions.php +++ b/src/applications/config/option/PhabricatorApplicationConfigOptions.php @@ -187,7 +187,7 @@ abstract class PhabricatorApplicationConfigOptions extends Phobject { final public static function loadAll($external_only = false) { $symbols = id(new PhutilSymbolLoader()) - ->setAncestorClass('PhabricatorApplicationConfigOptions') + ->setAncestorClass(__CLASS__) ->setConcreteOnly(true) ->selectAndLoadSymbols(); @@ -204,8 +204,12 @@ abstract class PhabricatorApplicationConfigOptions extends Phobject { $nclass = $symbol['name']; throw new Exception( - "Multiple PhabricatorApplicationConfigOptions subclasses have the ". - "same key ('{$key}'): {$pclass}, {$nclass}."); + pht( + "Multiple %s subclasses have the same key ('%s'): %s, %s.", + __CLASS__, + $key, + $pclass, + $nclass)); } $groups[$key] = $obj; } @@ -222,8 +226,10 @@ abstract class PhabricatorApplicationConfigOptions extends Phobject { $key = $option->getKey(); if (isset($options[$key])) { throw new Exception( - "Mulitple PhabricatorApplicationConfigOptions subclasses contain ". - "an option named '{$key}'!"); + pht( + "Mulitple % subclasses contain an option named '%s'!", + __CLASS__, + $key)); } $options[$key] = $option; } diff --git a/src/applications/config/option/PhabricatorConfigOption.php b/src/applications/config/option/PhabricatorConfigOption.php index f11c436402..e5c9773611 100644 --- a/src/applications/config/option/PhabricatorConfigOption.php +++ b/src/applications/config/option/PhabricatorConfigOption.php @@ -122,8 +122,7 @@ final class PhabricatorConfigOption return $this->enumOptions; } - throw new Exception( - 'Call setEnumOptions() before trying to access them!'); + throw new PhutilInvalidStateException('setEnumOptions'); } public function setKey($key) { diff --git a/src/applications/config/schema/PhabricatorConfigSchemaQuery.php b/src/applications/config/schema/PhabricatorConfigSchemaQuery.php index 7a11d64684..57db889956 100644 --- a/src/applications/config/schema/PhabricatorConfigSchemaQuery.php +++ b/src/applications/config/schema/PhabricatorConfigSchemaQuery.php @@ -11,7 +11,7 @@ final class PhabricatorConfigSchemaQuery extends Phobject { protected function getAPI() { if (!$this->api) { - throw new Exception(pht('Call setAPI() before issuing a query!')); + throw new PhutilInvalidStateException('setAPI'); } return $this->api; } diff --git a/src/applications/conpherence/ConpherenceTransactionRenderer.php b/src/applications/conpherence/ConpherenceTransactionRenderer.php index d6be83fb65..1b14f5f0d6 100644 --- a/src/applications/conpherence/ConpherenceTransactionRenderer.php +++ b/src/applications/conpherence/ConpherenceTransactionRenderer.php @@ -90,6 +90,7 @@ final class ConpherenceTransactionRenderer { if ($previous_day != $current_day) { $date_marker_transaction->setDateCreated( $transaction->getDateCreated()); + $date_marker_transaction->setID($previous_transaction->getID()); $rendered_transactions[] = $date_marker_transaction_view->render(); } } @@ -144,6 +145,15 @@ final class ConpherenceTransactionRenderer { ), ), pht('Show Older Messages')); + $oldscrollbutton = javelin_tag( + 'div', + array( + 'sigil' => 'conpherence-transaction-view', + 'meta' => array( + 'id' => $oldest_transaction_id - 0.5, + ), + ), + $oldscrollbutton); } $newscrollbutton = ''; @@ -160,6 +170,15 @@ final class ConpherenceTransactionRenderer { ), ), pht('Show Newer Messages')); + $newscrollbutton = javelin_tag( + 'div', + array( + 'sigil' => 'conpherence-transaction-view', + 'meta' => array( + 'id' => $newest_transaction_id + 0.5, + ), + ), + $newscrollbutton); } return hsprintf( diff --git a/src/applications/conpherence/controller/ConpherenceColumnViewController.php b/src/applications/conpherence/controller/ConpherenceColumnViewController.php index 0155784015..9a146b4b23 100644 --- a/src/applications/conpherence/controller/ConpherenceColumnViewController.php +++ b/src/applications/conpherence/controller/ConpherenceColumnViewController.php @@ -88,12 +88,19 @@ final class ConpherenceColumnViewController extends PhabricatorPolicyCapability::CAN_EDIT); } + $dropdown_query = id(new AphlictDropdownDataQuery()) + ->setViewer($user); + $dropdown_query->execute(); $response = array( 'content' => hsprintf('%s', $durable_column), 'threadID' => $conpherence_id, 'threadPHID' => $conpherence_phid, 'latestTransactionID' => $latest_transaction_id, 'canEdit' => $can_edit, + 'aphlictDropdownData' => array( + $dropdown_query->getNotificationData(), + $dropdown_query->getConpherenceData(), + ), ); return id(new AphrontAjaxResponse())->setContent($response); diff --git a/src/applications/conpherence/controller/ConpherenceListController.php b/src/applications/conpherence/controller/ConpherenceListController.php index f8be8f7de6..ad672ff6d8 100644 --- a/src/applications/conpherence/controller/ConpherenceListController.php +++ b/src/applications/conpherence/controller/ConpherenceListController.php @@ -25,6 +25,10 @@ final class ConpherenceListController extends ConpherenceController { return $mode; } + public function shouldAllowPublic() { + return true; + } + public function handleRequest(AphrontRequest $request) { $user = $request->getUser(); $title = pht('Conpherence'); diff --git a/src/applications/conpherence/controller/ConpherenceNewController.php b/src/applications/conpherence/controller/ConpherenceNewController.php index a887404c7d..102cf0e5ea 100644 --- a/src/applications/conpherence/controller/ConpherenceNewController.php +++ b/src/applications/conpherence/controller/ConpherenceNewController.php @@ -42,9 +42,8 @@ final class ConpherenceNewController extends ConpherenceController { } } } else { - $uri = $this->getApplicationURI($conpherence->getID()); return id(new AphrontRedirectResponse()) - ->setURI($uri); + ->setURI('/'.$conpherence->getMonogram()); } } else { $participant_prefill = $request->getStr('participant'); diff --git a/src/applications/conpherence/controller/ConpherenceNewRoomController.php b/src/applications/conpherence/controller/ConpherenceNewRoomController.php index ce9afcb195..a401da0a19 100644 --- a/src/applications/conpherence/controller/ConpherenceNewRoomController.php +++ b/src/applications/conpherence/controller/ConpherenceNewRoomController.php @@ -36,9 +36,8 @@ final class ConpherenceNewRoomController extends ConpherenceController { ->setActor($user) ->applyTransactions($conpherence, $xactions); - $uri = $this->getApplicationURI($conpherence->getID()); return id(new AphrontRedirectResponse()) - ->setURI($uri); + ->setURI('/'.$conpherence->getMonogram()); } catch (PhabricatorApplicationTransactionValidationException $ex) { $validation_exception = $ex; diff --git a/src/applications/conpherence/controller/ConpherenceRoomListController.php b/src/applications/conpherence/controller/ConpherenceRoomListController.php index 299ca6b8d0..43c36d20d1 100644 --- a/src/applications/conpherence/controller/ConpherenceRoomListController.php +++ b/src/applications/conpherence/controller/ConpherenceRoomListController.php @@ -2,6 +2,10 @@ final class ConpherenceRoomListController extends ConpherenceController { + public function shouldAllowPublic() { + return true; + } + public function handleRequest(AphrontRequest $request) { $user = $request->getUser(); diff --git a/src/applications/conpherence/controller/ConpherenceUpdateController.php b/src/applications/conpherence/controller/ConpherenceUpdateController.php index 269b87f0e6..eea1d1dc8a 100644 --- a/src/applications/conpherence/controller/ConpherenceUpdateController.php +++ b/src/applications/conpherence/controller/ConpherenceUpdateController.php @@ -251,7 +251,7 @@ final class ConpherenceUpdateController case 'redirect': default: return id(new AphrontRedirectResponse()) - ->setURI($this->getApplicationURI($conpherence->getID().'/')); + ->setURI('/'.$conpherence->getMonogram()); break; } } @@ -453,6 +453,7 @@ final class ConpherenceUpdateController $conpherence_id, $latest_transaction_id) { + $minimal_display = $this->getRequest()->getExists('minimal_display'); $need_widget_data = false; $need_transactions = false; $need_participant_cache = true; @@ -464,7 +465,7 @@ final class ConpherenceUpdateController case ConpherenceUpdateActions::MESSAGE: case ConpherenceUpdateActions::ADD_PERSON: $need_transactions = true; - $need_widget_data = true; + $need_widget_data = !$minimal_display; break; case ConpherenceUpdateActions::REMOVE_PERSON: case ConpherenceUpdateActions::NOTIFICATIONS: @@ -488,7 +489,7 @@ final class ConpherenceUpdateController $data = ConpherenceTransactionRenderer::renderTransactions( $user, $conpherence, - !$this->getRequest()->getExists('minimal_display')); + !$minimal_display); $participant_obj = $conpherence->getParticipant($user->getPHID()); $participant_obj->markUpToDate($conpherence, $data['latest_transaction']); } else if ($need_transactions) { @@ -505,51 +506,61 @@ final class ConpherenceUpdateController $header = null; $people_widget = null; $file_widget = null; - switch ($action) { - case ConpherenceUpdateActions::METADATA: - $policy_objects = id(new PhabricatorPolicyQuery()) - ->setViewer($user) - ->setObject($conpherence) - ->execute(); - $header = $this->buildHeaderPaneContent($conpherence, $policy_objects); - $nav_item = id(new ConpherenceThreadListView()) - ->setUser($user) - ->setBaseURI($this->getApplicationURI()) - ->renderSingleThread($conpherence); - break; - case ConpherenceUpdateActions::MESSAGE: - $file_widget = id(new ConpherenceFileWidgetView()) - ->setUser($this->getRequest()->getUser()) - ->setConpherence($conpherence) - ->setUpdateURI($widget_uri); - break; - case ConpherenceUpdateActions::ADD_PERSON: - $people_widget = id(new ConpherencePeopleWidgetView()) - ->setUser($user) - ->setConpherence($conpherence) - ->setUpdateURI($widget_uri); - break; - case ConpherenceUpdateActions::REMOVE_PERSON: - case ConpherenceUpdateActions::NOTIFICATIONS: - default: - break; - } - - $people_html = null; - if ($people_widget) { - $people_html = hsprintf('%s', $people_widget->render()); + if (!$minimal_display) { + switch ($action) { + case ConpherenceUpdateActions::METADATA: + $policy_objects = id(new PhabricatorPolicyQuery()) + ->setViewer($user) + ->setObject($conpherence) + ->execute(); + $header = $this->buildHeaderPaneContent( + $conpherence, + $policy_objects); + $header = hsprintf('%s', $header); + $nav_item = id(new ConpherenceThreadListView()) + ->setUser($user) + ->setBaseURI($this->getApplicationURI()) + ->renderSingleThread($conpherence); + $nav_item = hsprintf('%s', $nav_item); + break; + case ConpherenceUpdateActions::MESSAGE: + $file_widget = id(new ConpherenceFileWidgetView()) + ->setUser($this->getRequest()->getUser()) + ->setConpherence($conpherence) + ->setUpdateURI($widget_uri); + $file_widget = $file_widget->render(); + break; + case ConpherenceUpdateActions::ADD_PERSON: + $people_widget = id(new ConpherencePeopleWidgetView()) + ->setUser($user) + ->setConpherence($conpherence) + ->setUpdateURI($widget_uri); + $people_widget = $people_widget->render(); + break; + case ConpherenceUpdateActions::REMOVE_PERSON: + case ConpherenceUpdateActions::NOTIFICATIONS: + default: + break; + } } $data = $conpherence->getDisplayData($user); + $dropdown_query = id(new AphlictDropdownDataQuery()) + ->setViewer($user); + $dropdown_query->execute(); $content = array( 'non_update' => $non_update, 'transactions' => hsprintf('%s', $rendered_transactions), 'conpherence_title' => (string) $data['title'], 'latest_transaction_id' => $new_latest_transaction_id, - 'nav_item' => hsprintf('%s', $nav_item), + 'nav_item' => $nav_item, 'conpherence_phid' => $conpherence->getPHID(), - 'header' => hsprintf('%s', $header), - 'file_widget' => $file_widget ? $file_widget->render() : null, - 'people_widget' => $people_html, + 'header' => $header, + 'file_widget' => $file_widget, + 'people_widget' => $people_widget, + 'aphlictDropdownData' => array( + $dropdown_query->getNotificationData(), + $dropdown_query->getConpherenceData(), + ), ); return $content; diff --git a/src/applications/conpherence/controller/ConpherenceViewController.php b/src/applications/conpherence/controller/ConpherenceViewController.php index 7a3027dc53..6293c1bbdd 100644 --- a/src/applications/conpherence/controller/ConpherenceViewController.php +++ b/src/applications/conpherence/controller/ConpherenceViewController.php @@ -5,6 +5,10 @@ final class ConpherenceViewController extends const OLDER_FETCH_LIMIT = 5; + public function shouldAllowPublic() { + return true; + } + public function handleRequest(AphrontRequest $request) { $user = $request->getUser(); @@ -75,7 +79,7 @@ final class ConpherenceViewController extends if ($before_transaction_id || $after_transaction_id) { $header = null; $form = null; - $content = array('messages' => $messages); + $content = array('transactions' => $messages); } else { $policy_objects = id(new PhabricatorPolicyQuery()) ->setViewer($user) @@ -85,7 +89,7 @@ final class ConpherenceViewController extends $form = $this->renderFormContent(); $content = array( 'header' => $header, - 'messages' => $messages, + 'transactions' => $messages, 'form' => $form, ); } @@ -94,6 +98,9 @@ final class ConpherenceViewController extends $content['title'] = $title = $d_data['title']; if ($request->isAjax()) { + $dropdown_query = id(new AphlictDropdownDataQuery()) + ->setViewer($user); + $dropdown_query->execute(); $content['threadID'] = $conpherence->getID(); $content['threadPHID'] = $conpherence->getPHID(); $content['latestTransactionID'] = $data['latest_transaction_id']; @@ -101,6 +108,10 @@ final class ConpherenceViewController extends $user, $conpherence, PhabricatorPolicyCapability::CAN_EDIT); + $content['aphlictDropdownData'] = array( + $dropdown_query->getNotificationData(), + $dropdown_query->getConpherenceData(), + ); return id(new AphrontAjaxResponse())->setContent($content); } @@ -131,7 +142,7 @@ final class ConpherenceViewController extends $conpherence, PhabricatorPolicyCapability::CAN_JOIN); $participating = $conpherence->getParticipantIfExists($user->getPHID()); - if (!$can_join && !$participating) { + if (!$can_join && !$participating && $user->isLoggedIn()) { return null; } $draft = PhabricatorDraft::newFromUserAndKey( @@ -140,9 +151,20 @@ final class ConpherenceViewController extends if ($participating) { $action = ConpherenceUpdateActions::MESSAGE; $button_text = pht('Send'); - } else { + } else if ($user->isLoggedIn()) { $action = ConpherenceUpdateActions::JOIN_ROOM; $button_text = pht('Join'); + } else { + // user not logged in so give them a login button. + $login_href = id(new PhutilURI('/auth/start/')) + ->setQueryParam('next', '/'.$conpherence->getMonogram()); + return id(new PHUIFormLayoutView()) + ->addClass('login-to-participate') + ->appendChild( + id(new PHUIButtonView()) + ->setTag('a') + ->setText(pht('Login to Participate')) + ->setHref((string)$login_href)); } $update_uri = $this->getApplicationURI('update/'.$conpherence->getID().'/'); @@ -150,10 +172,10 @@ final class ConpherenceViewController extends $form = id(new AphrontFormView()) + ->setUser($user) ->setAction($update_uri) ->addSigil('conpherence-pontificate') ->setWorkflow(true) - ->setUser($user) ->addHiddenInput('action', $action) ->appendChild( id(new PhabricatorRemarkupControl()) diff --git a/src/applications/conpherence/controller/ConpherenceWidgetController.php b/src/applications/conpherence/controller/ConpherenceWidgetController.php index f7775dcc32..9a06f0dcf9 100644 --- a/src/applications/conpherence/controller/ConpherenceWidgetController.php +++ b/src/applications/conpherence/controller/ConpherenceWidgetController.php @@ -13,6 +13,10 @@ final class ConpherenceWidgetController extends ConpherenceController { return $this->userPreferences; } + public function shouldAllowPublic() { + return true; + } + public function handleRequest(AphrontRequest $request) { $request = $this->getRequest(); $user = $request->getUser(); @@ -26,6 +30,9 @@ final class ConpherenceWidgetController extends ConpherenceController { ->withIDs(array($conpherence_id)) ->needWidgetData(true) ->executeOne(); + if (!$conpherence) { + return new Aphront404Response(); + } $this->setConpherence($conpherence); $this->setUserPreferences($user->loadPreferences()); @@ -138,8 +145,11 @@ final class ConpherenceWidgetController extends ConpherenceController { PhabricatorPolicyCapability::CAN_JOIN); if ($can_join) { $text = pht('Settings are available after joining the room.'); - } else { + } else if ($viewer->isLoggedIn()) { $text = pht('Settings not applicable to rooms you can not join.'); + } else { + $text = pht( + 'Settings are available after logging in and joining the room.'); } return phutil_tag( 'div', diff --git a/src/applications/conpherence/query/ConpherenceThreadQuery.php b/src/applications/conpherence/query/ConpherenceThreadQuery.php index 03c341f090..b17044ead0 100644 --- a/src/applications/conpherence/query/ConpherenceThreadQuery.php +++ b/src/applications/conpherence/query/ConpherenceThreadQuery.php @@ -285,10 +285,9 @@ final class ConpherenceThreadQuery $conpherence->$method(); } $flat_phids = array_mergev($handle_phids); - $handles = id(new PhabricatorHandleQuery()) - ->setViewer($this->getViewer()) - ->withPHIDs($flat_phids) - ->execute(); + $viewer = $this->getViewer(); + $handles = $viewer->loadHandles($flat_phids); + $handles = iterator_to_array($handles); foreach ($handle_phids as $conpherence_phid => $phids) { $conpherence = $conpherences[$conpherence_phid]; $conpherence->attachHandles( diff --git a/src/applications/conpherence/query/ConpherenceThreadSearchEngine.php b/src/applications/conpherence/query/ConpherenceThreadSearchEngine.php index 2a23d6c073..f2a2d920bd 100644 --- a/src/applications/conpherence/query/ConpherenceThreadSearchEngine.php +++ b/src/applications/conpherence/query/ConpherenceThreadSearchEngine.php @@ -149,32 +149,48 @@ final class ConpherenceThreadSearchEngine $viewer, $conpherences); + $engines = array(); + $fulltext = $query->getParameter('fulltext'); if (strlen($fulltext) && $conpherences) { $context = $this->loadContextMessages($conpherences, $fulltext); $author_phids = array(); - foreach ($context as $messages) { + foreach ($context as $phid => $messages) { + $conpherence = $conpherences[$phid]; + + $engine = id(new PhabricatorMarkupEngine()) + ->setViewer($viewer) + ->setContextObject($conpherence); + foreach ($messages as $group) { foreach ($group as $message) { $xaction = $message['xaction']; if ($xaction) { $author_phids[] = $xaction->getAuthorPHID(); + $engine->addObject( + $xaction->getComment(), + PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT); } } } + $engine->process(); + + $engines[$phid] = $engine; } $handles = $viewer->loadHandles($author_phids); + $handles = iterator_to_array($handles); } else { $context = array(); } $list = new PHUIObjectItemListView(); $list->setUser($viewer); - foreach ($conpherences as $conpherence) { + foreach ($conpherences as $conpherence_phid => $conpherence) { $created = phabricator_date($conpherence->getDateCreated(), $viewer); $title = $conpherence->getDisplayTitle($viewer); + $monogram = $conpherence->getMonogram(); if ($conpherence->getIsRoom()) { $icon_name = $conpherence->getPolicyIconName($policy_objects); @@ -186,7 +202,7 @@ final class ConpherenceThreadSearchEngine $item = id(new PHUIObjectItemView()) ->setObjectName($conpherence->getMonogram()) ->setHeader($title) - ->setHref('/conpherence/'.$conpherence->getID().'/') + ->setHref('/'.$conpherence->getMonogram()) ->setObject($conpherence) ->addIcon('none', $created) ->addIcon( @@ -201,43 +217,37 @@ final class ConpherenceThreadSearchEngine phabricator_datetime($conpherence->getDateModified(), $viewer)), )); - $messages = idx($context, $conpherence->getPHID()); + $messages = idx($context, $conpherence_phid); if ($messages) { - - // TODO: This is egregiously under-designed. - foreach ($messages as $group) { $rows = array(); - $rowc = array(); foreach ($group as $message) { $xaction = $message['xaction']; if (!$xaction) { continue; } - $rowc[] = ($message['match'] ? 'highlighted' : null); - $rows[] = array( - $handles->renderHandle($xaction->getAuthorPHID()), - $xaction->getComment()->getContent(), - phabricator_datetime($xaction->getDateCreated(), $viewer), - ); + $history_href = '/'.$monogram.'#'.$xaction->getID(); + + $view = id(new ConpherenceTransactionView()) + ->setUser($viewer) + ->setHandles($handles) + ->setMarkupEngine($engines[$conpherence_phid]) + ->setConpherenceThread($conpherence) + ->setConpherenceTransaction($xaction) + ->setEpoch($xaction->getDateCreated(), $history_href) + ->addClass('conpherence-fulltext-result'); + + if ($message['match']) { + $view->addClass('conpherence-fulltext-match'); + } + + $rows[] = $view; } - $table = id(new AphrontTableView($rows)) - ->setHeaders( - array( - pht('User'), - pht('Message'), - pht('At'), - )) - ->setRowClasses($rowc) - ->setColumnClasses( - array( - '', - 'wide', - )); + $box = id(new PHUIBoxView()) - ->appendChild($table) - ->addMargin(PHUI::MARGIN_SMALL); + ->appendChild($rows) + ->addClass('conpherence-fulltext-results'); $item->appendChild($box); } } @@ -362,8 +372,12 @@ final class ConpherenceThreadSearchEngine $groups = array(); foreach ($hits as $thread_phid => $rows) { $rows = ipull($rows, null, 'transactionPHID'); + $done = array(); foreach ($rows as $phid => $row) { - unset($rows[$phid]); + if (isset($done[$phid])) { + continue; + } + $done[$phid] = true; $group = array(); @@ -381,7 +395,7 @@ final class ConpherenceThreadSearchEngine if (isset($rows[$prev])) { $match = true; - unset($rows[$prev]); + $done[$prev] = true; } else { $match = false; } @@ -411,7 +425,7 @@ final class ConpherenceThreadSearchEngine if (isset($rows[$next])) { $match = true; - unset($rows[$next]); + $done[$next] = true; } else { $match = false; } @@ -451,6 +465,9 @@ final class ConpherenceThreadSearchEngine foreach ($group as $key => $list) { foreach ($list as $lkey => $item) { $xaction = idx($xactions, $item['phid']); + if ($xaction->shouldHide()) { + continue; + } $groups[$thread_phid][$key][$lkey]['xaction'] = $xaction; } } diff --git a/src/applications/conpherence/storage/ConpherenceThread.php b/src/applications/conpherence/storage/ConpherenceThread.php index 03f8f6997e..8ed0e4fd18 100644 --- a/src/applications/conpherence/storage/ConpherenceThread.php +++ b/src/applications/conpherence/storage/ConpherenceThread.php @@ -407,7 +407,7 @@ final class ConpherenceThread extends ConpherenceDAO PhabricatorUser $viewer, array $conpherences) { - assert_instances_of($conpherences, 'ConpherenceThread'); + assert_instances_of($conpherences, __CLASS__); $grouped = mgroup($conpherences, 'getIsRoom'); $rooms = idx($grouped, 1, array()); diff --git a/src/applications/conpherence/view/ConpherenceTransactionView.php b/src/applications/conpherence/view/ConpherenceTransactionView.php index 3ed5fa14eb..2dba770e54 100644 --- a/src/applications/conpherence/view/ConpherenceTransactionView.php +++ b/src/applications/conpherence/view/ConpherenceTransactionView.php @@ -103,10 +103,14 @@ final class ConpherenceTransactionView extends AphrontView { $transaction = $this->getConpherenceTransaction(); switch ($transaction->getTransactionType()) { case ConpherenceTransactionType::TYPE_DATE_MARKER: - return phutil_tag( + return javelin_tag( 'div', array( 'class' => 'conpherence-transaction-view date-marker', + 'sigil' => 'conpherence-transaction-view', + 'meta' => array( + 'id' => $transaction->getID() + 0.5, + ), ), array( phutil_tag( @@ -134,11 +138,15 @@ final class ConpherenceTransactionView extends AphrontView { 'conpherence-transaction-header grouped', array($actions, $info)); - return phutil_tag( + return javelin_tag( 'div', array( 'class' => 'conpherence-transaction-view '.$classes, 'id' => $transaction_id, + 'sigil' => 'conpherence-transaction-view', + 'meta' => array( + 'id' => $transaction->getID(), + ), ), array( $image, diff --git a/src/applications/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php b/src/applications/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php index 0708c98eec..5d9723cc4b 100644 --- a/src/applications/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php +++ b/src/applications/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php @@ -12,7 +12,7 @@ final class DarkConsoleErrorLogPluginAPI { // reenter autoloaders). PhutilReadableSerializer::printableValue(null); PhutilErrorHandler::setErrorListener( - array('DarkConsoleErrorLogPluginAPI', 'handleErrors')); + array(__CLASS__, 'handleErrors')); } public static function enableDiscardMode() { diff --git a/src/applications/daemon/query/PhabricatorDaemonLogQuery.php b/src/applications/daemon/query/PhabricatorDaemonLogQuery.php index 5822194d06..961c1cfc61 100644 --- a/src/applications/daemon/query/PhabricatorDaemonLogQuery.php +++ b/src/applications/daemon/query/PhabricatorDaemonLogQuery.php @@ -68,8 +68,8 @@ final class PhabricatorDaemonLogQuery } protected function willFilterPage(array $daemons) { - $unknown_delay = PhabricatorDaemonLogQuery::getTimeUntilUnknown(); - $dead_delay = PhabricatorDaemonLogQuery::getTimeUntilDead(); + $unknown_delay = self::getTimeUntilUnknown(); + $dead_delay = self::getTimeUntilDead(); $status_running = PhabricatorDaemonLog::STATUS_RUNNING; $status_unknown = PhabricatorDaemonLog::STATUS_UNKNOWN; diff --git a/src/applications/daemon/view/PhabricatorDaemonLogEventsView.php b/src/applications/daemon/view/PhabricatorDaemonLogEventsView.php index a3598bfe25..b06ebdd7d2 100644 --- a/src/applications/daemon/view/PhabricatorDaemonLogEventsView.php +++ b/src/applications/daemon/view/PhabricatorDaemonLogEventsView.php @@ -27,7 +27,7 @@ final class PhabricatorDaemonLogEventsView extends AphrontView { $rows = array(); if (!$this->user) { - throw new Exception('Call setUser() before rendering!'); + throw new PhutilInvalidStateException('setUser'); } foreach ($this->events as $event) { diff --git a/src/applications/daemon/view/PhabricatorDaemonLogListView.php b/src/applications/daemon/view/PhabricatorDaemonLogListView.php index 88a92ced98..f45606e9e7 100644 --- a/src/applications/daemon/view/PhabricatorDaemonLogListView.php +++ b/src/applications/daemon/view/PhabricatorDaemonLogListView.php @@ -14,7 +14,7 @@ final class PhabricatorDaemonLogListView extends AphrontView { $rows = array(); if (!$this->user) { - throw new Exception('Call setUser() before rendering!'); + throw new PhutilInvalidStateException('setUser'); } $env_hash = PhabricatorEnv::calculateEnvironmentHash(); diff --git a/src/applications/differential/constants/DifferentialAction.php b/src/applications/differential/constants/DifferentialAction.php index 9238990161..48a263c45a 100644 --- a/src/applications/differential/constants/DifferentialAction.php +++ b/src/applications/differential/constants/DifferentialAction.php @@ -22,71 +22,71 @@ final class DifferentialAction { public static function getBasicStoryText($action, $author_name) { switch ($action) { - case DifferentialAction::ACTION_COMMENT: + case self::ACTION_COMMENT: $title = pht('%s commented on this revision.', $author_name); break; - case DifferentialAction::ACTION_ACCEPT: + case self::ACTION_ACCEPT: $title = pht('%s accepted this revision.', $author_name); break; - case DifferentialAction::ACTION_REJECT: + case self::ACTION_REJECT: $title = pht('%s requested changes to this revision.', $author_name); break; - case DifferentialAction::ACTION_RETHINK: + case self::ACTION_RETHINK: $title = pht('%s planned changes to this revision.', $author_name); break; - case DifferentialAction::ACTION_ABANDON: + case self::ACTION_ABANDON: $title = pht('%s abandoned this revision.', $author_name); break; - case DifferentialAction::ACTION_CLOSE: + case self::ACTION_CLOSE: $title = pht('%s closed this revision.', $author_name); break; - case DifferentialAction::ACTION_REQUEST: + case self::ACTION_REQUEST: $title = pht('%s requested a review of this revision.', $author_name); break; - case DifferentialAction::ACTION_RECLAIM: + case self::ACTION_RECLAIM: $title = pht('%s reclaimed this revision.', $author_name); break; - case DifferentialAction::ACTION_UPDATE: + case self::ACTION_UPDATE: $title = pht('%s updated this revision.', $author_name); break; - case DifferentialAction::ACTION_RESIGN: + case self::ACTION_RESIGN: $title = pht('%s resigned from this revision.', $author_name); break; - case DifferentialAction::ACTION_SUMMARIZE: + case self::ACTION_SUMMARIZE: $title = pht('%s summarized this revision.', $author_name); break; - case DifferentialAction::ACTION_TESTPLAN: + case self::ACTION_TESTPLAN: $title = pht('%s explained the test plan for this revision.', $author_name); break; - case DifferentialAction::ACTION_CREATE: + case self::ACTION_CREATE: $title = pht('%s created this revision.', $author_name); break; - case DifferentialAction::ACTION_ADDREVIEWERS: + case self::ACTION_ADDREVIEWERS: $title = pht('%s added reviewers to this revision.', $author_name); break; - case DifferentialAction::ACTION_ADDCCS: + case self::ACTION_ADDCCS: $title = pht('%s added CCs to this revision.', $author_name); break; - case DifferentialAction::ACTION_CLAIM: + case self::ACTION_CLAIM: $title = pht('%s commandeered this revision.', $author_name); break; - case DifferentialAction::ACTION_REOPEN: + case self::ACTION_REOPEN: $title = pht('%s reopened this revision.', $author_name); break; @@ -127,9 +127,9 @@ final class DifferentialAction { } public static function allowReviewers($action) { - if ($action == DifferentialAction::ACTION_ADDREVIEWERS || - $action == DifferentialAction::ACTION_REQUEST || - $action == DifferentialAction::ACTION_RESIGN) { + if ($action == self::ACTION_ADDREVIEWERS || + $action == self::ACTION_REQUEST || + $action == self::ACTION_RESIGN) { return true; } return false; diff --git a/src/applications/differential/constants/DifferentialChangeType.php b/src/applications/differential/constants/DifferentialChangeType.php index 0a4f7861cc..2ce5392165 100644 --- a/src/applications/differential/constants/DifferentialChangeType.php +++ b/src/applications/differential/constants/DifferentialChangeType.php @@ -52,42 +52,42 @@ final class DifferentialChangeType { public static function isOldLocationChangeType($type) { static $types = array( - DifferentialChangeType::TYPE_MOVE_AWAY => true, - DifferentialChangeType::TYPE_COPY_AWAY => true, - DifferentialChangeType::TYPE_MULTICOPY => true, + self::TYPE_MOVE_AWAY => true, + self::TYPE_COPY_AWAY => true, + self::TYPE_MULTICOPY => true, ); return isset($types[$type]); } public static function isNewLocationChangeType($type) { static $types = array( - DifferentialChangeType::TYPE_MOVE_HERE => true, - DifferentialChangeType::TYPE_COPY_HERE => true, + self::TYPE_MOVE_HERE => true, + self::TYPE_COPY_HERE => true, ); return isset($types[$type]); } public static function isDeleteChangeType($type) { static $types = array( - DifferentialChangeType::TYPE_DELETE => true, - DifferentialChangeType::TYPE_MOVE_AWAY => true, - DifferentialChangeType::TYPE_MULTICOPY => true, + self::TYPE_DELETE => true, + self::TYPE_MOVE_AWAY => true, + self::TYPE_MULTICOPY => true, ); return isset($types[$type]); } public static function isCreateChangeType($type) { static $types = array( - DifferentialChangeType::TYPE_ADD => true, - DifferentialChangeType::TYPE_COPY_HERE => true, - DifferentialChangeType::TYPE_MOVE_HERE => true, + self::TYPE_ADD => true, + self::TYPE_COPY_HERE => true, + self::TYPE_MOVE_HERE => true, ); return isset($types[$type]); } public static function isModifyChangeType($type) { static $types = array( - DifferentialChangeType::TYPE_CHANGE => true, + self::TYPE_CHANGE => true, ); return isset($types[$type]); } diff --git a/src/applications/differential/customfield/DifferentialRevisionIDField.php b/src/applications/differential/customfield/DifferentialRevisionIDField.php index 3741c0126f..bb54951dc4 100644 --- a/src/applications/differential/customfield/DifferentialRevisionIDField.php +++ b/src/applications/differential/customfield/DifferentialRevisionIDField.php @@ -32,6 +32,14 @@ final class DifferentialRevisionIDField } public function parseValueFromCommitMessage($value) { + // If the value is just "D123" or similar, parse the ID from it directly. + $value = trim($value); + $matches = null; + if (preg_match('/^[dD]([1-9]\d*)\z/', $value, $matches)) { + return (int)$matches[1]; + } + + // Otherwise, try to extract a URI value. return self::parseRevisionIDFromURI($value); } diff --git a/src/applications/differential/parser/DifferentialLineAdjustmentMap.php b/src/applications/differential/parser/DifferentialLineAdjustmentMap.php new file mode 100644 index 0000000000..fde8f61f7d --- /dev/null +++ b/src/applications/differential/parser/DifferentialLineAdjustmentMap.php @@ -0,0 +1,376 @@ +map; + } + + public function getNearestMap() { + if ($this->nearestMap === null) { + $this->buildNearestMap(); + } + + return $this->nearestMap; + } + + public function getFinalOffset() { + // Make sure we've built this map already. + $this->getNearestMap(); + return $this->finalOffset; + } + + + /** + * Add a map to the end of the chain. + * + * When a line is mapped with @{method:mapLine}, it is mapped through all + * maps in the chain. + */ + public function addMapToChain(DifferentialLineAdjustmentMap $map) { + if ($this->nextMapInChain) { + $this->nextMapInChain->addMapToChain($map); + } else { + $this->nextMapInChain = $map; + } + return $this; + } + + + /** + * Map a line across a change, or a series of changes. + * + * @param int Line to map + * @param bool True to map it as the end of a range. + * @return wild Spooky magic. + */ + public function mapLine($line, $is_end) { + $nmap = $this->getNearestMap(); + + $deleted = false; + $offset = false; + if (isset($nmap[$line])) { + $line_range = $nmap[$line]; + if ($is_end) { + $to_line = end($line_range); + } else { + $to_line = reset($line_range); + } + if ($to_line <= 0) { + // If we're tracing the first line and this block is collapsing, + // compute the offset from the top of the block. + if (!$is_end && $this->isInverse) { + $offset = 0; + $cursor = $line - 1; + while (isset($nmap[$cursor])) { + $prev = $nmap[$cursor]; + $prev = reset($prev); + if ($prev == $to_line) { + $offset++; + } else { + break; + } + $cursor--; + } + } + + $to_line = -$to_line; + if (!$this->isInverse) { + $deleted = true; + } + } + $line = $to_line; + } else { + $line = $line + $this->finalOffset; + } + + if ($this->nextMapInChain) { + $chain = $this->nextMapInChain->mapLine($line, $is_end); + list($chain_deleted, $chain_offset, $line) = $chain; + $deleted = ($deleted || $chain_deleted); + if ($chain_offset !== false) { + if ($offset === false) { + $offset = 0; + } + $offset += $chain_offset; + } + } + + return array($deleted, $offset, $line); + } + + + /** + * Build a derived map which maps deleted lines to the nearest valid line. + * + * This computes a "nearest line" map and a final-line offset. These + * derived maps allow us to map deleted code to the previous (or next) line + * which actually exists. + */ + private function buildNearestMap() { + $map = $this->map; + $nmap = array(); + + $nearest = 0; + foreach ($map as $key => $value) { + if ($value) { + $nmap[$key] = $value; + $nearest = end($value); + } else { + $nmap[$key][0] = -$nearest; + } + } + + if (isset($key)) { + $this->finalOffset = ($nearest - $key); + } else { + $this->finalOffset = 0; + } + + foreach (array_reverse($map, true) as $key => $value) { + if ($value) { + $nearest = reset($value); + } else { + $nmap[$key][1] = -$nearest; + } + } + + $this->nearestMap = $nmap; + + return $this; + } + + public static function newFromHunks(array $hunks) { + assert_instances_of($hunks, 'DifferentialHunk'); + + $map = array(); + $o = 0; + $n = 0; + + $hunks = msort($hunks, 'getOldOffset'); + foreach ($hunks as $hunk) { + + // If the hunks are disjoint, add the implied missing lines where + // nothing changed. + $min = ($hunk->getOldOffset() - 1); + while ($o < $min) { + $o++; + $n++; + $map[$o][] = $n; + } + + $lines = $hunk->getStructuredLines(); + foreach ($lines as $line) { + switch ($line['type']) { + case '-': + $o++; + $map[$o] = array(); + break; + case '+': + $n++; + $map[$o][] = $n; + break; + case ' ': + $o++; + $n++; + $map[$o][] = $n; + break; + default: + break; + } + } + } + + $map = self::reduceMapRanges($map); + + return self::newFromMap($map); + } + + public static function newFromMap(array $map) { + $obj = new DifferentialLineAdjustmentMap(); + $obj->map = $map; + return $obj; + } + + public static function newInverseMap(DifferentialLineAdjustmentMap $map) { + $old = $map->getMap(); + $inv = array(); + $last = 0; + foreach ($old as $k => $v) { + if (count($v) > 1) { + $v = range(reset($v), end($v)); + } + if ($k == 0) { + foreach ($v as $line) { + $inv[$line] = array(); + $last = $line; + } + } else if ($v) { + $first = true; + foreach ($v as $line) { + if ($first) { + $first = false; + $inv[$line][] = $k; + $last = $line; + } else { + $inv[$line] = array(); + } + } + } else { + $inv[$last][] = $k; + } + } + + $inv = self::reduceMapRanges($inv); + + $obj = new DifferentialLineAdjustmentMap(); + $obj->map = $inv; + $obj->isInverse = !$map->isInverse; + return $obj; + } + + private static function reduceMapRanges(array $map) { + foreach ($map as $key => $values) { + if (count($values) > 2) { + $map[$key] = array(reset($values), end($values)); + } + } + return $map; + } + + + public static function loadMaps(array $maps) { + $keys = array(); + foreach ($maps as $map) { + list($u, $v) = $map; + $keys[self::getCacheKey($u, $v)] = $map; + } + + $cache = new PhabricatorKeyValueDatabaseCache(); + $cache = new PhutilKeyValueCacheProfiler($cache); + $cache->setProfiler(PhutilServiceProfiler::getInstance()); + + $results = array(); + + if ($keys) { + $caches = $cache->getKeys(array_keys($keys)); + foreach ($caches as $key => $value) { + list($u, $v) = $keys[$key]; + try { + $results[$u][$v] = self::newFromMap( + phutil_json_decode($value)); + } catch (Exception $ex) { + // Ignore, rebuild below. + } + unset($keys[$key]); + } + } + + if ($keys) { + $built = self::buildMaps($maps); + + $write = array(); + foreach ($built as $u => $list) { + foreach ($list as $v => $map) { + $write[self::getCacheKey($u, $v)] = json_encode($map->getMap()); + $results[$u][$v] = $map; + } + } + + $cache->setKeys($write); + } + + return $results; + } + + private static function buildMaps(array $maps) { + $need = array(); + foreach ($maps as $map) { + list($u, $v) = $map; + $need[$u] = $u; + $need[$v] = $v; + } + + if ($need) { + $changesets = id(new DifferentialChangesetQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withIDs($need) + ->needHunks(true) + ->execute(); + $changesets = mpull($changesets, null, 'getID'); + } + + $results = array(); + foreach ($maps as $map) { + list($u, $v) = $map; + $u_set = idx($changesets, $u); + $v_set = idx($changesets, $v); + + if (!$u_set || !$v_set) { + continue; + } + + // This is the simple case. + if ($u == $v) { + $results[$u][$v] = self::newFromHunks( + $u_set->getHunks()); + continue; + } + + $u_old = $u_set->makeOldFile(); + $v_old = $v_set->makeOldFile(); + + // No difference between the two left sides. + if ($u_old == $v_old) { + $results[$u][$v] = self::newFromMap( + array()); + continue; + } + + // If we're missing context, this won't currently work. We can + // make this case work, but it's fairly rare. + $u_hunks = $u_set->getHunks(); + $v_hunks = $v_set->getHunks(); + if (count($u_hunks) != 1 || + count($v_hunks) != 1 || + head($u_hunks)->getOldOffset() != 1 || + head($u_hunks)->getNewOffset() != 1 || + head($v_hunks)->getOldOffset() != 1 || + head($v_hunks)->getNewOffset() != 1) { + continue; + } + + $changeset = id(new PhabricatorDifferenceEngine()) + ->setIgnoreWhitespace(true) + ->generateChangesetFromFileContent($u_old, $v_old); + + $results[$u][$v] = self::newFromHunks( + $changeset->getHunks()); + } + + return $results; + } + + private static function getCacheKey($u, $v) { + return 'diffadjust.v1('.$u.','.$v.')'; + } + +} diff --git a/src/applications/differential/query/DifferentialInlineCommentQuery.php b/src/applications/differential/query/DifferentialInlineCommentQuery.php index 711c638bf1..3fb26739c0 100644 --- a/src/applications/differential/query/DifferentialInlineCommentQuery.php +++ b/src/applications/differential/query/DifferentialInlineCommentQuery.php @@ -323,6 +323,7 @@ final class DifferentialInlineCommentQuery 'new' => $is_new, 'reason' => $reason, 'href' => $href, + 'originalID' => $changeset->getID(), )); $results[] = $inline; @@ -348,6 +349,107 @@ final class DifferentialInlineCommentQuery } } + // Adjust inline line numbers to account for content changes across + // updates and rebases. + $plan = array(); + $need = array(); + foreach ($results as $inline) { + $ghost = $inline->getIsGhost(); + if (!$ghost) { + // If this isn't a "ghost" inline, ignore it. + continue; + } + + $src_id = $ghost['originalID']; + $dst_id = $inline->getChangesetID(); + + $xforms = array(); + + // If the comment is on the right, transform it through the inverse map + // back to the left. + if ($inline->getIsNewFile()) { + $xforms[] = array($src_id, $src_id, true); + } + + // Transform it across rebases. + $xforms[] = array($src_id, $dst_id, false); + + // If the comment is on the right, transform it back onto the right. + if ($inline->getIsNewFile()) { + $xforms[] = array($dst_id, $dst_id, false); + } + + $key = array(); + foreach ($xforms as $xform) { + list($u, $v, $inverse) = $xform; + + $short = $u.'/'.$v; + $need[$short] = array($u, $v); + + $part = $u.($inverse ? '<' : '>').$v; + $key[] = $part; + } + $key = implode(',', $key); + + if (empty($plan[$key])) { + $plan[$key] = array( + 'xforms' => $xforms, + 'inlines' => array(), + ); + } + + $plan[$key]['inlines'][] = $inline; + } + + if ($need) { + $maps = DifferentialLineAdjustmentMap::loadMaps($need); + } else { + $maps = array(); + } + + foreach ($plan as $step) { + $xforms = $step['xforms']; + + $chain = null; + foreach ($xforms as $xform) { + list($u, $v, $inverse) = $xform; + $map = idx(idx($maps, $u, array()), $v); + if (!$map) { + continue 2; + } + + if ($inverse) { + $map = DifferentialLineAdjustmentMap::newInverseMap($map); + } else { + $map = clone $map; + } + + if ($chain) { + $chain->addMapToChain($map); + } else { + $chain = $map; + } + } + + foreach ($step['inlines'] as $inline) { + $head_line = $inline->getLineNumber(); + $tail_line = ($head_line + $inline->getLineLength()); + + $head_info = $chain->mapLine($head_line, false); + $tail_info = $chain->mapLine($tail_line, true); + + list($head_deleted, $head_offset, $head_line) = $head_info; + list($tail_deleted, $tail_offset, $tail_line) = $tail_info; + + if ($head_offset !== false) { + $inline->setLineNumber($head_line + 1 + $head_offset); + } else { + $inline->setLineNumber($head_line); + $inline->setLineLength($tail_line - $head_line); + } + } + } + return $results; } diff --git a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php index a9eef41ac8..3eb0190664 100644 --- a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php @@ -278,6 +278,7 @@ final class DifferentialChangesetTwoUpRenderer $scaffold->addInlineView($companion); unset($new_comments[$n_num][$key]); + break; } } } diff --git a/src/applications/differential/storage/DifferentialHunk.php b/src/applications/differential/storage/DifferentialHunk.php index f22ba59d2b..bd13cadfec 100644 --- a/src/applications/differential/storage/DifferentialHunk.php +++ b/src/applications/differential/storage/DifferentialHunk.php @@ -117,7 +117,7 @@ abstract class DifferentialHunk extends DifferentialDAO return $this->splitLines; } - private function getStructuredLines() { + public function getStructuredLines() { if ($this->structuredLines === null) { $lines = $this->getSplitLines(); diff --git a/src/applications/differential/storage/DifferentialTransaction.php b/src/applications/differential/storage/DifferentialTransaction.php index 5cb6fd7e87..2857d24272 100644 --- a/src/applications/differential/storage/DifferentialTransaction.php +++ b/src/applications/differential/storage/DifferentialTransaction.php @@ -568,7 +568,7 @@ final class DifferentialTransaction extends PhabricatorApplicationTransaction { 'this revision.'); } break; - case DifferentialTransaction::TYPE_ACTION: + case self::TYPE_ACTION: switch ($this->getNewValue()) { case DifferentialAction::ACTION_CLOSE: return pht('This revision is already closed.'); diff --git a/src/applications/differential/storage/__tests__/DifferentialAdjustmentMapTestCase.php b/src/applications/differential/storage/__tests__/DifferentialAdjustmentMapTestCase.php new file mode 100644 index 0000000000..8fc28953fc --- /dev/null +++ b/src/applications/differential/storage/__tests__/DifferentialAdjustmentMapTestCase.php @@ -0,0 +1,294 @@ + array(1), + 2 => array(2), + 3 => array(3), + 4 => array(), + 5 => array(), + 6 => array(), + 7 => array(4), + 8 => array(5), + 9 => array(6), + 10 => array(7), + 11 => array(8), + 12 => array(9), + 13 => array(10), + 14 => array(11), + 15 => array(12), + 16 => array(13), + 17 => array(14), + 18 => array(15), + 19 => array(16), + 20 => array(17, 20), + 21 => array(21), + 22 => array(22), + 23 => array(23), + 24 => array(24), + 25 => array(25), + 26 => array(26), + ); + + $hunks = $this->loadHunks('add.diff'); + $this->assertEqual( + array( + 0 => array(1, 26), + ), + DifferentialLineAdjustmentMap::newFromHunks($hunks)->getMap()); + + $hunks = $this->loadHunks('change.diff'); + $this->assertEqual( + $change_map, + DifferentialLineAdjustmentMap::newFromHunks($hunks)->getMap()); + + $hunks = $this->loadHunks('remove.diff'); + $this->assertEqual( + array_fill_keys(range(1, 26), array()), + DifferentialLineAdjustmentMap::newFromHunks($hunks)->getMap()); + + // With the contextless diff, we don't get the last few similar lines + // in the map. + $reduced_map = $change_map; + unset($reduced_map[24]); + unset($reduced_map[25]); + unset($reduced_map[26]); + + $hunks = $this->loadHunks('context.diff'); + $this->assertEqual( + $reduced_map, + DifferentialLineAdjustmentMap::newFromHunks($hunks)->getMap()); + } + + + public function testInverseMaps() { + $change_map = array( + 1 => array(1), + 2 => array(2), + 3 => array(3, 6), + 4 => array(7), + 5 => array(8), + 6 => array(9), + 7 => array(10), + 8 => array(11), + 9 => array(12), + 10 => array(13), + 11 => array(14), + 12 => array(15), + 13 => array(16), + 14 => array(17), + 15 => array(18), + 16 => array(19), + 17 => array(20), + 18 => array(), + 19 => array(), + 20 => array(), + 21 => array(21), + 22 => array(22), + 23 => array(23), + 24 => array(24), + 25 => array(25), + 26 => array(26), + ); + + $hunks = $this->loadHunks('add.diff'); + $this->assertEqual( + array_fill_keys(range(1, 26), array()), + DifferentialLineAdjustmentMap::newInverseMap( + DifferentialLineAdjustmentMap::newFromHunks($hunks))->getMap()); + + $hunks = $this->loadHunks('change.diff'); + $this->assertEqual( + $change_map, + DifferentialLineAdjustmentMap::newInverseMap( + DifferentialLineAdjustmentMap::newFromHunks($hunks))->getMap()); + + $hunks = $this->loadHunks('remove.diff'); + $this->assertEqual( + array( + 0 => array(1, 26), + ), + DifferentialLineAdjustmentMap::newInverseMap( + DifferentialLineAdjustmentMap::newFromHunks($hunks))->getMap()); + + // With the contextless diff, we don't get the last few similar lines + // in the map. + $reduced_map = $change_map; + unset($reduced_map[24]); + unset($reduced_map[25]); + unset($reduced_map[26]); + + $hunks = $this->loadHunks('context.diff'); + $this->assertEqual( + $reduced_map, + DifferentialLineAdjustmentMap::newInverseMap( + DifferentialLineAdjustmentMap::newFromHunks($hunks))->getMap()); + } + + + public function testNearestMaps() { + $change_map = array( + 1 => array(1), + 2 => array(2), + 3 => array(3), + 4 => array(-3, -4), + 5 => array(-3, -4), + 6 => array(-3, -4), + 7 => array(4), + 8 => array(5), + 9 => array(6), + 10 => array(7), + 11 => array(8), + 12 => array(9), + 13 => array(10), + 14 => array(11), + 15 => array(12), + 16 => array(13), + 17 => array(14), + 18 => array(15), + 19 => array(16), + 20 => array(17, 20), + 21 => array(21), + 22 => array(22), + 23 => array(23), + 24 => array(24), + 25 => array(25), + 26 => array(26), + ); + + $hunks = $this->loadHunks('add.diff'); + $map = DifferentialLineAdjustmentMap::newFromHunks($hunks); + $this->assertEqual( + array( + 0 => array(1, 26), + ), + $map->getNearestMap()); + $this->assertEqual(26, $map->getFinalOffset()); + + + $hunks = $this->loadHunks('change.diff'); + $map = DifferentialLineAdjustmentMap::newFromHunks($hunks); + $this->assertEqual( + $change_map, + $map->getNearestMap()); + $this->assertEqual(0, $map->getFinalOffset()); + + + $hunks = $this->loadHunks('remove.diff'); + $map = DifferentialLineAdjustmentMap::newFromHunks($hunks); + $this->assertEqual( + array_fill_keys( + range(1, 26), + array(0, 0)), + $map->getNearestMap()); + $this->assertEqual(-26, $map->getFinalOffset()); + + + $reduced_map = $change_map; + unset($reduced_map[24]); + unset($reduced_map[25]); + unset($reduced_map[26]); + + $hunks = $this->loadHunks('context.diff'); + $map = DifferentialLineAdjustmentMap::newFromHunks($hunks); + $this->assertEqual( + $reduced_map, + $map->getNearestMap()); + $this->assertEqual(0, $map->getFinalOffset()); + + + $hunks = $this->loadHunks('insert.diff'); + $map = DifferentialLineAdjustmentMap::newFromHunks($hunks); + $this->assertEqual( + array( + 1 => array(1), + 2 => array(2), + 3 => array(3), + 4 => array(4), + 5 => array(5), + 6 => array(6), + 7 => array(7), + 8 => array(8), + 9 => array(9), + 10 => array(10, 13), + 11 => array(14), + 12 => array(15), + 13 => array(16), + ), + $map->getNearestMap()); + $this->assertEqual(3, $map->getFinalOffset()); + } + + + public function testChainMaps() { + // This test simulates porting inlines forward across a rebase. + // Part 1 is the original diff. + // Part 2 is the rebase, which we would normally compute synthetically. + // Part 3 is the updated diff against the rebased changes. + + $diff1 = $this->loadHunks('chain.adjust.1.diff'); + $diff2 = $this->loadHunks('chain.adjust.2.diff'); + $diff3 = $this->loadHunks('chain.adjust.3.diff'); + + $map = DifferentialLineAdjustmentMap::newInverseMap( + DifferentialLineAdjustmentMap::newFromHunks($diff1)); + + $map->addMapToChain( + DifferentialLineAdjustmentMap::newFromHunks($diff2)); + + $map->addMapToChain( + DifferentialLineAdjustmentMap::newFromHunks($diff3)); + + $actual = array(); + for ($ii = 1; $ii <= 13; $ii++) { + $actual[$ii] = array( + $map->mapLine($ii, false), + $map->mapLine($ii, true), + ); + } + + $this->assertEqual( + array( + 1 => array(array(false, false, 1), array(false, false, 1)), + 2 => array(array(true, false, 1), array(true, false, 2)), + 3 => array(array(true, false, 1), array(true, false, 2)), + 4 => array(array(false, false, 2), array(false, false, 2)), + 5 => array(array(false, false, 3), array(false, false, 3)), + 6 => array(array(false, false, 4), array(false, false, 4)), + 7 => array(array(false, false, 5), array(false, false, 8)), + 8 => array(array(false, 0, 5), array(false, false, 9)), + 9 => array(array(false, 1, 5), array(false, false, 9)), + 10 => array(array(false, 2, 5), array(false, false, 9)), + 11 => array(array(false, false, 9), array(false, false, 9)), + 12 => array(array(false, false, 10), array(false, false, 10)), + 13 => array(array(false, false, 11), array(false, false, 11)), + ), + $actual); + } + + + private function loadHunks($name) { + $root = dirname(__FILE__).'/map/'; + $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); + + return $changeset->getHunks(); + } + +} diff --git a/src/applications/differential/storage/__tests__/map/add.diff b/src/applications/differential/storage/__tests__/map/add.diff new file mode 100644 index 0000000000..97e60a8b7c --- /dev/null +++ b/src/applications/differential/storage/__tests__/map/add.diff @@ -0,0 +1,32 @@ +diff --git a/alphabet b/alphabet +new file mode 100644 +index 0000000..0edb856 +--- /dev/null ++++ b/alphabet +@@ -0,0 +1,26 @@ ++a ++b ++c ++d ++e ++f ++g ++h ++i ++j ++k ++l ++m ++n ++o ++p ++q ++r ++s ++t ++u ++v ++w ++x ++y ++z diff --git a/src/applications/differential/storage/__tests__/map/chain.adjust.1.diff b/src/applications/differential/storage/__tests__/map/chain.adjust.1.diff new file mode 100644 index 0000000000..8370a66e1a --- /dev/null +++ b/src/applications/differential/storage/__tests__/map/chain.adjust.1.diff @@ -0,0 +1,14 @@ +diff --git a/alphabet b/alphabet +index 92dfa21..292798b 100644 +--- a/alphabet ++++ b/alphabet +@@ -5,6 +5,9 @@ d + e + f + g ++G1 ++G2 ++G3 + h + i + j diff --git a/src/applications/differential/storage/__tests__/map/chain.adjust.2.diff b/src/applications/differential/storage/__tests__/map/chain.adjust.2.diff new file mode 100644 index 0000000000..ac6f8c854a --- /dev/null +++ b/src/applications/differential/storage/__tests__/map/chain.adjust.2.diff @@ -0,0 +1,11 @@ +diff --git a/alphabet b/alphabet +index 92dfa21..e3344af 100644 +--- a/alphabet ++++ b/alphabet +@@ -1,6 +1,4 @@ + a +-b +-c + d + e + f diff --git a/src/applications/differential/storage/__tests__/map/chain.adjust.3.diff b/src/applications/differential/storage/__tests__/map/chain.adjust.3.diff new file mode 100644 index 0000000000..4d23f185fd --- /dev/null +++ b/src/applications/differential/storage/__tests__/map/chain.adjust.3.diff @@ -0,0 +1,14 @@ +diff --git a/alphabet b/alphabet +index e3344af..febfe3e 100644 +--- a/alphabet ++++ b/alphabet +@@ -3,6 +3,9 @@ d + e + f + g ++G1x ++G2x ++G3x + h + i + j diff --git a/src/applications/differential/storage/__tests__/map/change.diff b/src/applications/differential/storage/__tests__/map/change.diff new file mode 100644 index 0000000000..7ef945267f --- /dev/null +++ b/src/applications/differential/storage/__tests__/map/change.diff @@ -0,0 +1,34 @@ +diff --git a/alphabet b/alphabet +index 0edb856..2449de2 100644 +--- a/alphabet ++++ b/alphabet +@@ -1,26 +1,26 @@ + a + b + c +-d +-e +-f + g + h + i + j + k + l + m + n + o + p + q + r + s + t ++tx ++ty ++tz + u + v + w + x + y + z diff --git a/src/applications/differential/storage/__tests__/map/context.diff b/src/applications/differential/storage/__tests__/map/context.diff new file mode 100644 index 0000000000..ab77e4a9ba --- /dev/null +++ b/src/applications/differential/storage/__tests__/map/context.diff @@ -0,0 +1,24 @@ +diff --git a/alphabet b/alphabet +index 0edb856..2449de2 100644 +--- a/alphabet ++++ b/alphabet +@@ -1,9 +1,6 @@ + a + b + c +-d +-e +-f + g + h + i +@@ -18,6 +15,9 @@ q + r + s + t ++tx ++ty ++tz + u + v + w diff --git a/src/applications/differential/storage/__tests__/map/insert.diff b/src/applications/differential/storage/__tests__/map/insert.diff new file mode 100644 index 0000000000..9a726955e7 --- /dev/null +++ b/src/applications/differential/storage/__tests__/map/insert.diff @@ -0,0 +1,14 @@ +diff --git a/alphabet b/alphabet +index f2b41ef..755b349 100644 +--- a/alphabet ++++ b/alphabet +@@ -8,6 +8,9 @@ g + h + i + j ++j1 ++j2 ++j3 + k + l + n diff --git a/src/applications/differential/storage/__tests__/map/remove.diff b/src/applications/differential/storage/__tests__/map/remove.diff new file mode 100644 index 0000000000..0feafbbfc3 --- /dev/null +++ b/src/applications/differential/storage/__tests__/map/remove.diff @@ -0,0 +1,32 @@ +diff --git a/alphabet b/alphabet +deleted file mode 100644 +index 2449de2..0000000 +--- a/alphabet ++++ /dev/null +@@ -1,26 +0,0 @@ +-a +-b +-c +-g +-h +-i +-j +-k +-l +-m +-n +-o +-p +-q +-r +-s +-t +-tx +-ty +-tz +-u +-v +-w +-x +-y +-z diff --git a/src/applications/differential/view/DifferentialLocalCommitsView.php b/src/applications/differential/view/DifferentialLocalCommitsView.php index 489674b0ab..ab83016e2a 100644 --- a/src/applications/differential/view/DifferentialLocalCommitsView.php +++ b/src/applications/differential/view/DifferentialLocalCommitsView.php @@ -19,7 +19,7 @@ final class DifferentialLocalCommitsView extends AphrontView { public function render() { $user = $this->user; if (!$user) { - throw new Exception('Call setUser() before render()-ing this view.'); + throw new PhutilInvalidStateException('setUser'); } $local = $this->localCommits; diff --git a/src/applications/differential/view/DifferentialRevisionListView.php b/src/applications/differential/view/DifferentialRevisionListView.php index 1dd5a5cc57..121792585d 100644 --- a/src/applications/differential/view/DifferentialRevisionListView.php +++ b/src/applications/differential/view/DifferentialRevisionListView.php @@ -57,10 +57,9 @@ final class DifferentialRevisionListView extends AphrontView { } public function render() { - $user = $this->user; if (!$user) { - throw new Exception('Call setUser() before render()!'); + throw new PhutilInvalidStateException('setUser'); } $fresh = PhabricatorEnv::getEnvConfig('differential.days-fresh'); diff --git a/src/applications/diffusion/data/DiffusionPathChange.php b/src/applications/diffusion/data/DiffusionPathChange.php index b1d7286fdd..6f96057014 100644 --- a/src/applications/diffusion/data/DiffusionPathChange.php +++ b/src/applications/diffusion/data/DiffusionPathChange.php @@ -119,7 +119,7 @@ final class DiffusionPathChange { } final public static function convertToArcanistChanges(array $changes) { - assert_instances_of($changes, 'DiffusionPathChange'); + assert_instances_of($changes, __CLASS__); $direct = array(); $result = array(); foreach ($changes as $path) { @@ -145,7 +145,7 @@ final class DiffusionPathChange { final public static function convertToDifferentialChangesets( PhabricatorUser $user, array $changes) { - assert_instances_of($changes, 'DiffusionPathChange'); + assert_instances_of($changes, __CLASS__); $arcanist_changes = self::convertToArcanistChanges($changes); $diff = DifferentialDiff::newEphemeralFromRawChanges( $arcanist_changes); diff --git a/src/applications/diffusion/query/DiffusionLintCountQuery.php b/src/applications/diffusion/query/DiffusionLintCountQuery.php index f85505af76..4441ccf3ef 100644 --- a/src/applications/diffusion/query/DiffusionLintCountQuery.php +++ b/src/applications/diffusion/query/DiffusionLintCountQuery.php @@ -23,11 +23,11 @@ final class DiffusionLintCountQuery extends PhabricatorQuery { public function execute() { if (!$this->paths) { - throw new Exception(pht('Call withPaths() before execute()!')); + throw new PhutilInvalidStateException('withPaths'); } if (!$this->branchIDs) { - throw new Exception(pht('Call withBranchIDs() before execute()!')); + throw new PhutilInvalidStateException('withBranchIDs'); } $conn_r = id(new PhabricatorRepositoryCommit())->establishConnection('r'); diff --git a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelQuery.php b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelQuery.php index 0e8472c24d..e14198371c 100644 --- a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelQuery.php +++ b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelQuery.php @@ -17,7 +17,7 @@ abstract class DiffusionLowLevelQuery extends Phobject { public function execute() { if (!$this->getRepository()) { - throw new Exception('Call setRepository() before execute()!'); + throw new PhutilInvalidStateException('setRepository'); } return $this->executeQuery(); diff --git a/src/applications/diviner/atom/DivinerAtom.php b/src/applications/diviner/atom/DivinerAtom.php index 46c1c70a9e..ef1a079a41 100644 --- a/src/applications/diviner/atom/DivinerAtom.php +++ b/src/applications/diviner/atom/DivinerAtom.php @@ -95,16 +95,14 @@ final class DivinerAtom { public function getDocblockText() { if ($this->docblockText === null) { - throw new Exception( - pht('Call %s before %s!', 'setDocblockRaw()', 'getDocblockText()')); + throw new PhutilInvalidStateException('setDocblockRaw'); } return $this->docblockText; } public function getDocblockMeta() { if ($this->docblockMeta === null) { - throw new Exception( - pht('Call %s before %s!', 'setDocblockRaw()', 'getDocblockMeta()')); + throw new PhutilInvalidStateException('setDocblockRaw'); } return $this->docblockMeta; } diff --git a/src/applications/diviner/storage/DivinerLiveSymbol.php b/src/applications/diviner/storage/DivinerLiveSymbol.php index b6a7cde226..ba35eed255 100644 --- a/src/applications/diviner/storage/DivinerLiveSymbol.php +++ b/src/applications/diviner/storage/DivinerLiveSymbol.php @@ -174,7 +174,7 @@ final class DivinerLiveSymbol extends DivinerDAO } public function attachExtends(array $extends) { - assert_instances_of($extends, 'DivinerLiveSymbol'); + assert_instances_of($extends, __CLASS__); $this->extends = $extends; return $this; } @@ -184,7 +184,7 @@ final class DivinerLiveSymbol extends DivinerDAO } public function attachChildren(array $children) { - assert_instances_of($children, 'DivinerLiveSymbol'); + assert_instances_of($children, __CLASS__); $this->children = $children; return $this; } diff --git a/src/applications/doorkeeper/view/DoorkeeperTagView.php b/src/applications/doorkeeper/view/DoorkeeperTagView.php index 66f759f43c..8dee79d72b 100644 --- a/src/applications/doorkeeper/view/DoorkeeperTagView.php +++ b/src/applications/doorkeeper/view/DoorkeeperTagView.php @@ -12,7 +12,7 @@ final class DoorkeeperTagView extends AphrontView { public function render() { $xobj = $this->xobj; if (!$xobj) { - throw new Exception('Call setExternalObject() before render()!'); + throw new PhutilInvalidStateException('setExternalObject'); } $tag_id = celerity_generate_unique_node_id(); diff --git a/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php index 18ca597a90..43891d0d07 100644 --- a/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php +++ b/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php @@ -377,7 +377,7 @@ abstract class DrydockBlueprintImplementation { if ($list === null) { $blueprints = id(new PhutilSymbolLoader()) ->setType('class') - ->setAncestorClass('DrydockBlueprintImplementation') + ->setAncestorClass(__CLASS__) ->setConcreteOnly(true) ->selectAndLoadSymbols(); $list = ipull($blueprints, 'name', 'name'); diff --git a/src/applications/drydock/storage/DrydockLease.php b/src/applications/drydock/storage/DrydockLease.php index cb61e54490..252c922065 100644 --- a/src/applications/drydock/storage/DrydockLease.php +++ b/src/applications/drydock/storage/DrydockLease.php @@ -149,7 +149,7 @@ final class DrydockLease extends DrydockDAO } public static function waitForLeases(array $leases) { - assert_instances_of($leases, 'DrydockLease'); + assert_instances_of($leases, __CLASS__); $task_ids = array_filter(mpull($leases, 'getTaskID')); diff --git a/src/applications/feed/story/PhabricatorFeedStory.php b/src/applications/feed/story/PhabricatorFeedStory.php index a115985704..f94c2601e8 100644 --- a/src/applications/feed/story/PhabricatorFeedStory.php +++ b/src/applications/feed/story/PhabricatorFeedStory.php @@ -48,7 +48,7 @@ abstract class PhabricatorFeedStory try { $ok = class_exists($class) && - is_subclass_of($class, 'PhabricatorFeedStory'); + is_subclass_of($class, __CLASS__); } catch (PhutilMissingSymbolException $ex) { $ok = false; } @@ -453,15 +453,9 @@ abstract class PhabricatorFeedStory * @task policy */ public function getPolicy($capability) { - // If this story's primary object is a policy-aware object, use its policy - // to control story visiblity. - - $primary_phid = $this->getPrimaryObjectPHID(); - if (isset($this->objects[$primary_phid])) { - $object = $this->objects[$primary_phid]; - if ($object instanceof PhabricatorPolicyInterface) { - return $object->getPolicy($capability); - } + $policy_object = $this->getPrimaryPolicyObject(); + if ($policy_object) { + return $policy_object->getPolicy($capability); } // TODO: Remove this once all objects are policy-aware. For now, keep @@ -476,6 +470,11 @@ abstract class PhabricatorFeedStory * @task policy */ public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + $policy_object = $this->getPrimaryPolicyObject(); + if ($policy_object) { + return $policy_object->hasAutomaticCapability($capability, $viewer); + } + return false; } @@ -484,6 +483,26 @@ abstract class PhabricatorFeedStory } + /** + * Get the policy object this story is about, if such a policy object + * exists. + * + * @return PhabricatorPolicyInterface|null Policy object, if available. + * @task policy + */ + private function getPrimaryPolicyObject() { + $primary_phid = $this->getPrimaryObjectPHID(); + if (empty($this->objects[$primary_phid])) { + $object = $this->objects[$primary_phid]; + if ($object instanceof PhabricatorPolicyInterface) { + return $object; + } + } + + return null; + } + + /* -( PhabricatorMarkupInterface Implementation )--------------------------- */ diff --git a/src/applications/files/PhabricatorImageTransformer.php b/src/applications/files/PhabricatorImageTransformer.php index 95cf1fac7a..829f5d9f2d 100644 --- a/src/applications/files/PhabricatorImageTransformer.php +++ b/src/applications/files/PhabricatorImageTransformer.php @@ -20,51 +20,6 @@ final class PhabricatorImageTransformer { )); } - public function executeThumbTransform( - PhabricatorFile $file, - $x, - $y) { - - $image = $this->crudelyScaleTo($file, $x, $y); - - return PhabricatorFile::newFromFileData( - $image, - array( - 'name' => 'thumb-'.$file->getName(), - 'canCDN' => true, - )); - } - - public function executeProfileTransform( - PhabricatorFile $file, - $x, - $min_y, - $max_y) { - - $image = $this->crudelyCropTo($file, $x, $min_y, $max_y); - - return PhabricatorFile::newFromFileData( - $image, - array( - 'name' => 'profile-'.$file->getName(), - 'canCDN' => true, - )); - } - - public function executePreviewTransform( - PhabricatorFile $file, - $size) { - - $image = $this->generatePreview($file, $size); - - return PhabricatorFile::newFromFileData( - $image, - array( - 'name' => 'preview-'.$file->getName(), - 'canCDN' => true, - )); - } - public function executeConpherenceTransform( PhabricatorFile $file, $top, @@ -83,38 +38,11 @@ final class PhabricatorImageTransformer { $image, array( 'name' => 'conpherence-'.$file->getName(), + 'profile' => true, 'canCDN' => true, )); } - private function crudelyCropTo(PhabricatorFile $file, $x, $min_y, $max_y) { - $data = $file->loadFileData(); - $img = imagecreatefromstring($data); - $sx = imagesx($img); - $sy = imagesy($img); - - $scaled_y = ($x / $sx) * $sy; - if ($scaled_y > $max_y) { - // This image is very tall and thin. - $scaled_y = $max_y; - } else if ($scaled_y < $min_y) { - // This image is very short and wide. - $scaled_y = $min_y; - } - - $cropped = $this->applyScaleWithImagemagick($file, $x, $scaled_y); - if ($cropped != null) { - return $cropped; - } - - $img = $this->applyScaleTo( - $file, - $x, - $scaled_y); - - return self::saveImageDataInAnyFormat($img, $file->getMimeType()); - } - private function crasslyCropTo(PhabricatorFile $file, $top, $left, $w, $h) { $data = $file->loadFileData(); $src = imagecreatefromstring($data); @@ -137,21 +65,6 @@ final class PhabricatorImageTransformer { return self::saveImageDataInAnyFormat($dst, $file->getMimeType()); } - - /** - * Very crudely scale an image up or down to an exact size. - */ - private function crudelyScaleTo(PhabricatorFile $file, $dx, $dy) { - $scaled = $this->applyScaleWithImagemagick($file, $dx, $dy); - - if ($scaled != null) { - return $scaled; - } - - $dst = $this->applyScaleTo($file, $dx, $dy); - return self::saveImageDataInAnyFormat($dst, $file->getMimeType()); - } - private function getBlankDestinationFile($dx, $dy) { $dst = imagecreatetruecolor($dx, $dy); imagesavealpha($dst, true); @@ -160,65 +73,6 @@ final class PhabricatorImageTransformer { return $dst; } - private function applyScaleTo(PhabricatorFile $file, $dx, $dy) { - $data = $file->loadFileData(); - $src = imagecreatefromstring($data); - - $x = imagesx($src); - $y = imagesy($src); - - $scale = min(($dx / $x), ($dy / $y), 1); - - $sdx = $scale * $x; - $sdy = $scale * $y; - - $dst = $this->getBlankDestinationFile($dx, $dy); - imagesavealpha($dst, true); - imagefill($dst, 0, 0, imagecolorallocatealpha($dst, 255, 255, 255, 127)); - - imagecopyresampled( - $dst, - $src, - ($dx - $sdx) / 2, ($dy - $sdy) / 2, - 0, 0, - $sdx, $sdy, - $x, $y); - - return $dst; - - } - - public static function getPreviewDimensions(PhabricatorFile $file, $size) { - $metadata = $file->getMetadata(); - $x = idx($metadata, PhabricatorFile::METADATA_IMAGE_WIDTH); - $y = idx($metadata, PhabricatorFile::METADATA_IMAGE_HEIGHT); - - if (!$x || !$y) { - $data = $file->loadFileData(); - $src = imagecreatefromstring($data); - - $x = imagesx($src); - $y = imagesy($src); - } - - $scale = min($size / $x, $size / $y, 1); - - $dx = max($size / 4, $scale * $x); - $dy = max($size / 4, $scale * $y); - - $sdx = $scale * $x; - $sdy = $scale * $y; - - return array( - 'x' => $x, - 'y' => $y, - 'dx' => $dx, - 'dy' => $dy, - 'sdx' => $sdx, - 'sdy' => $sdy, - ); - } - public static function getScaleForCrop( PhabricatorFile $file, $des_width, @@ -241,31 +95,6 @@ final class PhabricatorImageTransformer { return $scale; } - private function generatePreview(PhabricatorFile $file, $size) { - $data = $file->loadFileData(); - $src = imagecreatefromstring($data); - - $dimensions = self::getPreviewDimensions($file, $size); - $x = $dimensions['x']; - $y = $dimensions['y']; - $dx = $dimensions['dx']; - $dy = $dimensions['dy']; - $sdx = $dimensions['sdx']; - $sdy = $dimensions['sdy']; - - $dst = $this->getBlankDestinationFile($dx, $dy); - - imagecopyresampled( - $dst, - $src, - ($dx - $sdx) / 2, ($dy - $sdy) / 2, - 0, 0, - $sdx, $sdy, - $x, $y); - - return self::saveImageDataInAnyFormat($dst, $file->getMimeType()); - } - private function applyMemeToFile( PhabricatorFile $file, $upper_text, @@ -402,49 +231,6 @@ final class PhabricatorImageTransformer { ); } - private function applyScaleWithImagemagick(PhabricatorFile $file, $dx, $dy) { - $img_type = $file->getMimeType(); - $imagemagick = PhabricatorEnv::getEnvConfig('files.enable-imagemagick'); - - if ($img_type != 'image/gif' || $imagemagick == false) { - return null; - } - - $data = $file->loadFileData(); - $src = imagecreatefromstring($data); - - $x = imagesx($src); - $y = imagesy($src); - - if (self::isEnormousGIF($x, $y)) { - return null; - } - - $scale = min(($dx / $x), ($dy / $y), 1); - - $sdx = $scale * $x; - $sdy = $scale * $y; - - $input = new TempFile(); - Filesystem::writeFile($input, $data); - - $resized = new TempFile(); - - $future = new ExecFuture( - 'convert %s -coalesce -resize %sX%s%s %s', - $input, - $sdx, - $sdy, - '!', - $resized); - - // Don't spend more than 10 seconds resizing; just fail if it takes longer - // than that. - $future->setTimeout(10)->resolvex(); - - return Filesystem::readFile($resized); - } - private function applyMemeWithImagemagick( $input, $above, @@ -482,57 +268,6 @@ final class PhabricatorImageTransformer { return Filesystem::readFile($output); } -/* -( Detecting Enormous Files )------------------------------------------- */ - - - /** - * Determine if an image is enormous (too large to transform). - * - * Attackers can perform a denial of service attack by uploading highly - * compressible images with enormous dimensions but a very small filesize. - * Transforming them (e.g., into thumbnails) may consume huge quantities of - * memory and CPU relative to the resources required to transmit the file. - * - * In general, we respond to these images by declining to transform them, and - * using a default thumbnail instead. - * - * @param int Width of the image, in pixels. - * @param int Height of the image, in pixels. - * @return bool True if this image is enormous (too large to transform). - * @task enormous - */ - public static function isEnormousImage($x, $y) { - // This is just a sanity check, but if we don't have valid dimensions we - // shouldn't be trying to transform the file. - if (($x <= 0) || ($y <= 0)) { - return true; - } - - return ($x * $y) > (4096 * 4096); - } - - - /** - * Determine if a GIF is enormous (too large to transform). - * - * For discussion, see @{method:isEnormousImage}. We need to be more - * careful about GIFs, because they can also have a large number of frames - * despite having a very small filesize. We're more conservative about - * calling GIFs enormous than about calling images in general enormous. - * - * @param int Width of the GIF, in pixels. - * @param int Height of the GIF, in pixels. - * @return bool True if this image is enormous (too large to transform). - * @task enormous - */ - public static function isEnormousGIF($x, $y) { - if (self::isEnormousImage($x, $y)) { - return true; - } - - return ($x * $y) > (800 * 800); - } - /* -( Saving Image Data )-------------------------------------------------- */ diff --git a/src/applications/files/application/PhabricatorFilesApplication.php b/src/applications/files/application/PhabricatorFilesApplication.php index 1187b0545b..ed4801ace6 100644 --- a/src/applications/files/application/PhabricatorFilesApplication.php +++ b/src/applications/files/application/PhabricatorFilesApplication.php @@ -91,6 +91,8 @@ final class PhabricatorFilesApplication extends PhabricatorApplication { '(?P[^/]+)/'. '(?P[^/]+)/' => 'PhabricatorFileTransformController', + 'transforms/(?P[1-9]\d*)/' => + 'PhabricatorFileTransformListController', 'uploaddialog/' => 'PhabricatorFileUploadDialogController', 'download/(?P[^/]+)/' => 'PhabricatorFileDialogController', ), diff --git a/src/applications/files/controller/PhabricatorFileComposeController.php b/src/applications/files/controller/PhabricatorFileComposeController.php index 6903cff5d3..d1e6041131 100644 --- a/src/applications/files/controller/PhabricatorFileComposeController.php +++ b/src/applications/files/controller/PhabricatorFileComposeController.php @@ -58,7 +58,7 @@ final class PhabricatorFileComposeController } $root = dirname(phutil_get_library_root('phabricator')); - $icon_file = $root.'/resources/sprite/projects_1x/'.$icon.'.png'; + $icon_file = $root.'/resources/sprite/projects_2x/'.$icon.'.png'; $icon_data = Filesystem::readFile($icon_file); @@ -68,6 +68,7 @@ final class PhabricatorFileComposeController $data, array( 'name' => 'project.png', + 'profile' => true, 'canCDN' => true, )); @@ -241,7 +242,7 @@ final class PhabricatorFileComposeController } $dialog_id = celerity_generate_unique_node_id(); - $color_input_id = celerity_generate_unique_node_id();; + $color_input_id = celerity_generate_unique_node_id(); $icon_input_id = celerity_generate_unique_node_id(); $preview_id = celerity_generate_unique_node_id(); @@ -325,10 +326,10 @@ final class PhabricatorFileComposeController $color_string = idx($map, $color, '#ff00ff'); $color_const = hexdec(trim($color_string, '#')); - $canvas = imagecreatetruecolor(50, 50); + $canvas = imagecreatetruecolor(100, 100); imagefill($canvas, 0, 0, $color_const); - imagecopy($canvas, $icon_img, 0, 0, 0, 0, 50, 50); + imagecopy($canvas, $icon_img, 0, 0, 0, 0, 100, 100); return PhabricatorImageTransformer::saveImageDataInAnyFormat( $canvas, diff --git a/src/applications/files/controller/PhabricatorFileDataController.php b/src/applications/files/controller/PhabricatorFileDataController.php index 2a66a3807e..e528eb15f2 100644 --- a/src/applications/files/controller/PhabricatorFileDataController.php +++ b/src/applications/files/controller/PhabricatorFileDataController.php @@ -227,7 +227,7 @@ final class PhabricatorFileDataController extends PhabricatorFileController { private function getFile() { if (!$this->file) { - throw new Exception(pht('Call loadFile() before getFile()!')); + throw new PhutilInvalidStateException('loadFile'); } return $this->file; } diff --git a/src/applications/files/controller/PhabricatorFileInfoController.php b/src/applications/files/controller/PhabricatorFileInfoController.php index 3d9731f6b4..0e3d041eac 100644 --- a/src/applications/files/controller/PhabricatorFileInfoController.php +++ b/src/applications/files/controller/PhabricatorFileInfoController.php @@ -170,6 +170,12 @@ final class PhabricatorFileInfoController extends PhabricatorFileController { ->setWorkflow(true) ->setDisabled(!$can_edit)); + $view->addAction( + id(new PhabricatorActionView()) + ->setName(pht('View Transforms')) + ->setIcon('fa-crop') + ->setHref($this->getApplicationURI("/transforms/{$id}/"))); + return $view; } @@ -241,6 +247,12 @@ final class PhabricatorFileInfoController extends PhabricatorFileController { $finfo->addProperty(pht('Builtin'), $builtin_string); + $is_profile = $file->getIsProfileImage() + ? pht('Yes') + : pht('No'); + + $finfo->addProperty(pht('Profile'), $is_profile); + $storage_properties = new PHUIPropertyListView(); $box->addPropertyList($storage_properties, pht('Storage')); @@ -267,7 +279,6 @@ final class PhabricatorFileInfoController extends PhabricatorFileController { $user->renderHandleList($phids)); } - if ($file->isViewableImage()) { $image = phutil_tag( 'img', diff --git a/src/applications/files/controller/PhabricatorFileTransformController.php b/src/applications/files/controller/PhabricatorFileTransformController.php index 248a6dddd2..5062fe76fc 100644 --- a/src/applications/files/controller/PhabricatorFileTransformController.php +++ b/src/applications/files/controller/PhabricatorFileTransformController.php @@ -3,138 +3,89 @@ final class PhabricatorFileTransformController extends PhabricatorFileController { - private $transform; - private $phid; - private $key; - public function shouldRequireLogin() { return false; } - public function willProcessRequest(array $data) { - $this->transform = $data['transform']; - $this->phid = $data['phid']; - $this->key = $data['key']; - } - - public function processRequest() { - $viewer = $this->getRequest()->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); // NOTE: This is a public/CDN endpoint, and permission to see files is // controlled by knowing the secret key, not by authentication. + $is_regenerate = $request->getBool('regenerate'); + + $source_phid = $request->getURIData('phid'); $file = id(new PhabricatorFileQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withPHIDs(array($this->phid)) + ->withPHIDs(array($source_phid)) ->executeOne(); if (!$file) { return new Aphront404Response(); } - if (!$file->validateSecretKey($this->key)) { + $secret_key = $request->getURIData('key'); + if (!$file->validateSecretKey($secret_key)) { return new Aphront403Response(); } + $transform = $request->getURIData('transform'); $xform = id(new PhabricatorTransformedFile()) ->loadOneWhere( 'originalPHID = %s AND transform = %s', - $this->phid, - $this->transform); + $source_phid, + $transform); if ($xform) { - return $this->buildTransformedFileResponse($xform); + if ($is_regenerate) { + $this->destroyTransform($xform); + } else { + return $this->buildTransformedFileResponse($xform); + } } - $type = $file->getMimeType(); - - if (!$file->isViewableInBrowser() || !$file->isTransformableImage()) { - return $this->buildDefaultTransformation($file); + $xforms = PhabricatorFileTransform::getAllTransforms(); + if (!isset($xforms[$transform])) { + return new Aphront404Response(); } + $xform = $xforms[$transform]; + // We're essentially just building a cache here and don't need CSRF // protection. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); - switch ($this->transform) { - case 'thumb-profile': - $xformed_file = $this->executeThumbTransform($file, 50, 50); - break; - case 'thumb-280x210': - $xformed_file = $this->executeThumbTransform($file, 280, 210); - break; - case 'thumb-220x165': - $xformed_file = $this->executeThumbTransform($file, 220, 165); - break; - case 'preview-100': - $xformed_file = $this->executePreviewTransform($file, 100); - break; - case 'preview-220': - $xformed_file = $this->executePreviewTransform($file, 220); - break; - case 'thumb-160x120': - $xformed_file = $this->executeThumbTransform($file, 160, 120); - break; - case 'thumb-60x45': - $xformed_file = $this->executeThumbTransform($file, 60, 45); - break; - default: - return new Aphront400Response(); + $xformed_file = null; + if ($xform->canApplyTransform($file)) { + try { + $xformed_file = $xforms[$transform]->applyTransform($file); + } catch (Exception $ex) { + // In normal transform mode, we ignore failures and generate a + // default transform below. If we're explicitly regenerating the + // thumbnail, rethrow the exception. + if ($is_regenerate) { + throw $ex; + } + } + } + + if (!$xformed_file) { + $xformed_file = $xform->getDefaultTransform($file); } if (!$xformed_file) { return new Aphront400Response(); } - $xform = new PhabricatorTransformedFile(); - $xform->setOriginalPHID($this->phid); - $xform->setTransform($this->transform); - $xform->setTransformedPHID($xformed_file->getPHID()); - $xform->save(); + $xform = id(new PhabricatorTransformedFile()) + ->setOriginalPHID($source_phid) + ->setTransform($transform) + ->setTransformedPHID($xformed_file->getPHID()) + ->save(); return $this->buildTransformedFileResponse($xform); } - private function buildDefaultTransformation(PhabricatorFile $file) { - static $regexps = array( - '@application/zip@' => 'zip', - '@image/@' => 'image', - '@application/pdf@' => 'pdf', - '@.*@' => 'default', - ); - - $type = $file->getMimeType(); - $prefix = 'default'; - foreach ($regexps as $regexp => $implied_prefix) { - if (preg_match($regexp, $type)) { - $prefix = $implied_prefix; - break; - } - } - - switch ($this->transform) { - case 'thumb-280x210': - $suffix = '280x210'; - break; - case 'thumb-160x120': - $suffix = '160x120'; - break; - case 'thumb-60x45': - $suffix = '60x45'; - break; - case 'preview-100': - $suffix = '.p100'; - break; - default: - throw new Exception('Unsupported transformation type!'); - } - - $path = celerity_get_resource_uri( - "rsrc/image/icon/fatcow/thumbnails/{$prefix}{$suffix}.png"); - - return id(new AphrontRedirectResponse()) - ->setURI($path); - } - private function buildTransformedFileResponse( PhabricatorTransformedFile $xform) { @@ -152,14 +103,22 @@ final class PhabricatorFileTransformController return $file->getRedirectResponse(); } - private function executePreviewTransform(PhabricatorFile $file, $size) { - $xformer = new PhabricatorImageTransformer(); - return $xformer->executePreviewTransform($file, $size); - } + private function destroyTransform(PhabricatorTransformedFile $xform) { + $file = id(new PhabricatorFileQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs(array($xform->getTransformedPHID())) + ->executeOne(); - private function executeThumbTransform(PhabricatorFile $file, $x, $y) { - $xformer = new PhabricatorImageTransformer(); - return $xformer->executeThumbTransform($file, $x, $y); + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + + if (!$file) { + $xform->delete(); + } else { + $engine = new PhabricatorDestructionEngine(); + $engine->destroyObject($file); + } + + unset($unguarded); } } diff --git a/src/applications/files/controller/PhabricatorFileTransformListController.php b/src/applications/files/controller/PhabricatorFileTransformListController.php new file mode 100644 index 0000000000..6ef1818962 --- /dev/null +++ b/src/applications/files/controller/PhabricatorFileTransformListController.php @@ -0,0 +1,138 @@ +getViewer(); + + $file = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withIDs(array($request->getURIData('id'))) + ->executeOne(); + if (!$file) { + return new Aphront404Response(); + } + + $monogram = $file->getMonogram(); + + $xdst = id(new PhabricatorTransformedFile())->loadAllWhere( + 'transformedPHID = %s', + $file->getPHID()); + + $dst_rows = array(); + foreach ($xdst as $source) { + $dst_rows[] = array( + $source->getTransform(), + $viewer->renderHandle($source->getOriginalPHID()), + ); + } + $dst_table = id(new AphrontTableView($dst_rows)) + ->setHeaders( + array( + pht('Key'), + pht('Source'), + )) + ->setColumnClasses( + array( + '', + 'wide', + )) + ->setNoDataString( + pht( + 'This file was not created by transforming another file.')); + + $xsrc = id(new PhabricatorTransformedFile())->loadAllWhere( + 'originalPHID = %s', + $file->getPHID()); + $xsrc = mpull($xsrc, 'getTransformedPHID', 'getTransform'); + + $src_rows = array(); + $xforms = PhabricatorFileTransform::getAllTransforms(); + foreach ($xforms as $xform) { + $dst_phid = idx($xsrc, $xform->getTransformKey()); + + if ($xform->canApplyTransform($file)) { + $can_apply = pht('Yes'); + + $view_href = $file->getURIForTransform($xform); + $view_href = new PhutilURI($view_href); + $view_href->setQueryParam('regenerate', 'true'); + + $view_text = pht('Regenerate'); + + $view_link = phutil_tag( + 'a', + array( + 'class' => 'small grey button', + 'href' => $view_href, + ), + $view_text); + } else { + $can_apply = phutil_tag('em', array(), pht('No')); + $view_link = phutil_tag('em', array(), pht('None')); + } + + if ($dst_phid) { + $dst_link = $viewer->renderHandle($dst_phid); + } else { + $dst_link = phutil_tag('em', array(), pht('None')); + } + + $src_rows[] = array( + $xform->getTransformName(), + $xform->getTransformKey(), + $can_apply, + $dst_link, + $view_link, + ); + } + + $src_table = id(new AphrontTableView($src_rows)) + ->setHeaders( + array( + pht('Name'), + pht('Key'), + pht('Supported'), + pht('Transform'), + pht('View'), + )) + ->setColumnClasses( + array( + 'wide', + '', + '', + '', + 'action', + )); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb($monogram, '/'.$monogram); + $crumbs->addTextCrumb(pht('Transforms')); + + $dst_box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('File Sources')) + ->appendChild($dst_table); + + $src_box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Available Transforms')) + ->appendChild($src_table); + + return $this->buildApplicationPage( + array( + $crumbs, + $dst_box, + $src_box, + ), + array( + 'title' => array( + pht('%s %s', $monogram, $file->getName()), + pht('Tranforms'), + ), + )); + } +} diff --git a/src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php b/src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php index fe0bb1da7a..5ffb70be7f 100644 --- a/src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php +++ b/src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php @@ -107,11 +107,17 @@ final class PhabricatorEmbedFileRemarkupRule break; case 'thumb': default: - $attrs['src'] = $file->getPreview220URI(); - $dimensions = - PhabricatorImageTransformer::getPreviewDimensions($file, 220); - $attrs['width'] = $dimensions['sdx']; - $attrs['height'] = $dimensions['sdy']; + $preview_key = PhabricatorFileThumbnailTransform::TRANSFORM_PREVIEW; + $xform = PhabricatorFileTransform::getTransformByKey($preview_key); + $attrs['src'] = $file->getURIForTransform($xform); + + $dimensions = $xform->getTransformedDimensions($file); + if ($dimensions) { + list($x, $y) = $dimensions; + $attrs['width'] = $x; + $attrs['height'] = $y; + } + $image_class = 'phabricator-remarkup-embed-image'; break; } diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php index 5bc0eef0e6..f8e99bc965 100644 --- a/src/applications/files/storage/PhabricatorFile.php +++ b/src/applications/files/storage/PhabricatorFile.php @@ -34,6 +34,7 @@ final class PhabricatorFile extends PhabricatorFileDAO const METADATA_CAN_CDN = 'canCDN'; const METADATA_BUILTIN = 'builtin'; const METADATA_PARTIAL = 'partial'; + const METADATA_PROFILE = 'profile'; protected $name; protected $mimeType; @@ -214,7 +215,7 @@ final class PhabricatorFile extends PhabricatorFileDAO if (!$file) { $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); - $file = PhabricatorFile::newFromFileData($data, $params); + $file = self::newFromFileData($data, $params); unset($unguarded); } @@ -235,7 +236,7 @@ final class PhabricatorFile extends PhabricatorFileDAO $copy_of_byte_size = $file->getByteSize(); $copy_of_mime_type = $file->getMimeType(); - $new_file = PhabricatorFile::initializeNewFile(); + $new_file = self::initializeNewFile(); $new_file->setByteSize($copy_of_byte_size); @@ -261,7 +262,7 @@ final class PhabricatorFile extends PhabricatorFileDAO $length, array $params) { - $file = PhabricatorFile::initializeNewFile(); + $file = self::initializeNewFile(); $file->setByteSize($length); @@ -315,7 +316,7 @@ final class PhabricatorFile extends PhabricatorFileDAO throw new Exception(pht('No valid storage engines are available!')); } - $file = PhabricatorFile::initializeNewFile(); + $file = self::initializeNewFile(); $data_handle = null; $engine_identifier = null; @@ -760,6 +761,10 @@ final class PhabricatorFile extends PhabricatorFileDAO return (string) $uri; } + public function getURIForTransform(PhabricatorFileTransform $transform) { + return $this->getTransformedURI($transform->getTransformKey()); + } + private function getTransformedURI($transform) { $parts = array(); $parts[] = 'file'; @@ -780,34 +785,6 @@ final class PhabricatorFile extends PhabricatorFileDAO return PhabricatorEnv::getCDNURI($path); } - public function getProfileThumbURI() { - return $this->getTransformedURI('thumb-profile'); - } - - public function getThumb60x45URI() { - return $this->getTransformedURI('thumb-60x45'); - } - - public function getThumb160x120URI() { - return $this->getTransformedURI('thumb-160x120'); - } - - public function getPreview100URI() { - return $this->getTransformedURI('preview-100'); - } - - public function getPreview220URI() { - return $this->getTransformedURI('preview-220'); - } - - public function getThumb220x165URI() { - return $this->getTransfomredURI('thumb-220x165'); - } - - public function getThumb280x210URI() { - return $this->getTransformedURI('thumb-280x210'); - } - public function isViewableInBrowser() { return ($this->getViewableMimeType() !== null); } @@ -1040,7 +1017,7 @@ final class PhabricatorFile extends PhabricatorFileDAO ); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); - $file = PhabricatorFile::newFromFileData($data, $params); + $file = self::newFromFileData($data, $params); $xform = id(new PhabricatorTransformedFile()) ->setOriginalPHID(PhabricatorPHIDConstants::PHID_VOID) ->setTransform('builtin:'.$name) @@ -1136,6 +1113,15 @@ final class PhabricatorFile extends PhabricatorFileDAO return $this; } + public function getIsProfileImage() { + return idx($this->metadata, self::METADATA_PROFILE); + } + + public function setIsProfileImage($value) { + $this->metadata[self::METADATA_PROFILE] = $value; + return $this; + } + protected function generateOneTimeToken() { $key = Filesystem::readRandomCharacters(16); @@ -1237,6 +1223,11 @@ final class PhabricatorFile extends PhabricatorFileDAO $this->setBuiltinName($builtin); } + $profile = idx($params, 'profile'); + if ($profile) { + $this->setIsProfileImage(true); + } + $mime_type = idx($params, 'mime-type'); if ($mime_type) { $this->setMimeType($mime_type); @@ -1304,6 +1295,9 @@ final class PhabricatorFile extends PhabricatorFileDAO if ($this->isBuiltin()) { return PhabricatorPolicies::getMostOpenPolicy(); } + if ($this->getIsProfileImage()) { + return PhabricatorPolicies::getMostOpenPolicy(); + } return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return PhabricatorPolicies::POLICY_NOONE; diff --git a/src/applications/files/transform/PhabricatorFileImageTransform.php b/src/applications/files/transform/PhabricatorFileImageTransform.php new file mode 100644 index 0000000000..ff8bd14683 --- /dev/null +++ b/src/applications/files/transform/PhabricatorFileImageTransform.php @@ -0,0 +1,382 @@ +|null Width and height, if available. + */ + public function getTransformedDimensions(PhabricatorFile $file) { + return null; + } + + public function canApplyTransform(PhabricatorFile $file) { + if (!$file->isViewableImage()) { + return false; + } + + if (!$file->isTransformableImage()) { + return false; + } + + return true; + } + + protected function willTransformFile(PhabricatorFile $file) { + $this->file = $file; + $this->data = null; + $this->image = null; + $this->imageX = null; + $this->imageY = null; + } + + protected function getFileProperties() { + return array(); + } + + protected function applyCropAndScale( + $dst_w, $dst_h, + $src_x, $src_y, + $src_w, $src_h, + $use_w, $use_h, + $scale_up) { + + // Figure out the effective destination width, height, and offsets. + $cpy_w = min($dst_w, $use_w); + $cpy_h = min($dst_h, $use_h); + + // If we aren't scaling up, and are copying a very small source image, + // we're just going to center it in the destination image. + if (!$scale_up) { + $cpy_w = min($cpy_w, $src_w); + $cpy_h = min($cpy_h, $src_h); + } + + $off_x = ($dst_w - $cpy_w) / 2; + $off_y = ($dst_h - $cpy_h) / 2; + + if ($this->shouldUseImagemagick()) { + $argv = array(); + $argv[] = '-coalesce'; + $argv[] = '-shave'; + $argv[] = $src_x.'x'.$src_y; + $argv[] = '-resize'; + + if ($scale_up) { + $argv[] = $dst_w.'x'.$dst_h; + } else { + $argv[] = $dst_w.'x'.$dst_h.'>'; + } + + $argv[] = '-bordercolor'; + $argv[] = 'rgba(255, 255, 255, 0)'; + $argv[] = '-border'; + $argv[] = $off_x.'x'.$off_y; + + return $this->applyImagemagick($argv); + } + + $src = $this->getImage(); + $dst = $this->newEmptyImage($dst_w, $dst_h); + + $trap = new PhutilErrorTrap(); + $ok = @imagecopyresampled( + $dst, + $src, + $off_x, $off_y, + $src_x, $src_y, + $cpy_w, $cpy_h, + $src_w, $src_h); + $errors = $trap->getErrorsAsString(); + $trap->destroy(); + + if ($ok === false) { + throw new Exception( + pht( + 'Failed to imagecopyresampled() image: %s', + $errors)); + } + + $data = PhabricatorImageTransformer::saveImageDataInAnyFormat( + $dst, + $this->file->getMimeType()); + + return $this->newFileFromData($data); + } + + protected function applyImagemagick(array $argv) { + $tmp = new TempFile(); + Filesystem::writeFile($tmp, $this->getData()); + + $out = new TempFile(); + + $future = new ExecFuture('convert %s %Ls %s', $tmp, $argv, $out); + // Don't spend more than 10 seconds resizing; just fail if it takes longer + // than that. + $future->setTimeout(10)->resolvex(); + + $data = Filesystem::readFile($out); + + return $this->newFileFromData($data); + } + + + /** + * Create a new @{class:PhabricatorFile} from raw data. + * + * @param string Raw file data. + */ + protected function newFileFromData($data) { + if ($this->file) { + $name = $this->file->getName(); + } else { + $name = 'default.png'; + } + + $defaults = array( + 'canCDN' => true, + 'name' => $this->getTransformKey().'-'.$name, + ); + + $properties = $this->getFileProperties() + $defaults; + + return PhabricatorFile::newFromFileData($data, $properties); + } + + + /** + * Create a new image filled with transparent pixels. + * + * @param int Desired image width. + * @param int Desired image height. + * @return resource New image resource. + */ + protected function newEmptyImage($w, $h) { + $w = (int)$w; + $h = (int)$h; + + if (($w <= 0) || ($h <= 0)) { + throw new Exception( + pht('Can not create an image with nonpositive dimensions.')); + } + + $trap = new PhutilErrorTrap(); + $img = @imagecreatetruecolor($w, $h); + $errors = $trap->getErrorsAsString(); + $trap->destroy(); + if ($img === false) { + throw new Exception( + pht( + 'Unable to imagecreatetruecolor() a new empty image: %s', + $errors)); + } + + $trap = new PhutilErrorTrap(); + $ok = @imagesavealpha($img, true); + $errors = $trap->getErrorsAsString(); + $trap->destroy(); + if ($ok === false) { + throw new Exception( + pht( + 'Unable to imagesavealpha() a new empty image: %s', + $errors)); + } + + $trap = new PhutilErrorTrap(); + $color = @imagecolorallocatealpha($img, 255, 255, 255, 127); + $errors = $trap->getErrorsAsString(); + $trap->destroy(); + if ($color === false) { + throw new Exception( + pht( + 'Unable to imagecolorallocatealpha() a new empty image: %s', + $errors)); + } + + $trap = new PhutilErrorTrap(); + $ok = @imagefill($img, 0, 0, $color); + $errors = $trap->getErrorsAsString(); + $trap->destroy(); + if ($ok === false) { + throw new Exception( + pht( + 'Unable to imagefill() a new empty image: %s', + $errors)); + } + + return $img; + } + + + /** + * Get the pixel dimensions of the image being transformed. + * + * @return list Width and height of the image. + */ + protected function getImageDimensions() { + if ($this->imageX === null) { + $image = $this->getImage(); + + $trap = new PhutilErrorTrap(); + $x = @imagesx($image); + $y = @imagesy($image); + $errors = $trap->getErrorsAsString(); + $trap->destroy(); + + if (($x === false) || ($y === false) || ($x <= 0) || ($y <= 0)) { + throw new Exception( + pht( + 'Unable to determine image dimensions with '. + 'imagesx()/imagesy(): %s', + $errors)); + } + + $this->imageX = $x; + $this->imageY = $y; + } + + return array($this->imageX, $this->imageY); + } + + + /** + * Get the raw file data for the image being transformed. + * + * @return string Raw file data. + */ + protected function getData() { + if ($this->data !== null) { + return $this->data; + } + + $file = $this->file; + + $max_size = (1024 * 1024 * 4); + $img_size = $file->getByteSize(); + if ($img_size > $max_size) { + throw new Exception( + pht( + 'This image is too large to transform. The transform limit is %s '. + 'bytes, but the image size is %s bytes.', + new PhutilNumber($max_size), + new PhutilNumber($img_size))); + } + + $data = $file->loadFileData(); + $this->data = $data; + return $this->data; + } + + + /** + * Get the GD image resource for the image being transformed. + * + * @return resource GD image resource. + */ + protected function getImage() { + if ($this->image !== null) { + return $this->image; + } + + if (!function_exists('imagecreatefromstring')) { + throw new Exception( + pht( + 'Unable to transform image: the imagecreatefromstring() function '. + 'is not available. Install or enable the "gd" extension for PHP.')); + } + + $data = $this->getData(); + $data = (string)$data; + + // First, we're going to write the file to disk and use getimagesize() + // to determine its dimensions without actually loading the pixel data + // into memory. For very large images, we'll bail out. + + // In particular, this defuses a resource exhaustion attack where the + // attacker uploads a 40,000 x 40,000 pixel PNGs of solid white. These + // kinds of files compress extremely well, but require a huge amount + // of memory and CPU to process. + + $tmp = new TempFile(); + Filesystem::writeFile($tmp, $data); + $tmp_path = (string)$tmp; + + $trap = new PhutilErrorTrap(); + $info = @getimagesize($tmp_path); + $errors = $trap->getErrorsAsString(); + $trap->destroy(); + + unset($tmp); + + if ($info === false) { + throw new Exception( + pht( + 'Unable to get image information with getimagesize(): %s', + $errors)); + } + + list($width, $height) = $info; + if (($width <= 0) || ($height <= 0)) { + throw new Exception( + pht( + 'Unable to determine image width and height with getimagesize().')); + } + + $max_pixels = (4096 * 4096); + $img_pixels = ($width * $height); + + if ($img_pixels > $max_pixels) { + throw new Exception( + pht( + 'This image (with dimensions %spx x %spx) is too large to '. + 'transform. The image has %s pixels, but transforms are limited '. + 'to images with %s or fewer pixels.', + new PhutilNumber($width), + new PhutilNumber($height), + new PhutilNumber($img_pixels), + new PhutilNumber($max_pixels))); + } + + $trap = new PhutilErrorTrap(); + $image = @imagecreatefromstring($data); + $errors = $trap->getErrorsAsString(); + $trap->destroy(); + + if ($image === false) { + throw new Exception( + pht( + 'Unable to load image data with imagecreatefromstring(): %s', + $errors)); + } + + $this->image = $image; + return $this->image; + } + + private function shouldUseImagemagick() { + if (!PhabricatorEnv::getEnvConfig('files.enable-imagemagick')) { + return false; + } + + if ($this->file->getMimeType() != 'image/gif') { + return false; + } + + // Don't try to preserve the animation in huge GIFs. + list($x, $y) = $this->getImageDimensions(); + if (($x * $y) > (512 * 512)) { + return false; + } + + return true; + } + +} diff --git a/src/applications/files/transform/PhabricatorFileThumbnailTransform.php b/src/applications/files/transform/PhabricatorFileThumbnailTransform.php new file mode 100644 index 0000000000..af97f8a9fa --- /dev/null +++ b/src/applications/files/transform/PhabricatorFileThumbnailTransform.php @@ -0,0 +1,225 @@ +name = $name; + return $this; + } + + public function setKey($key) { + $this->key = $key; + return $this; + } + + public function setDimensions($x, $y) { + $this->dstX = $x; + $this->dstY = $y; + return $this; + } + + public function setScaleUp($scale) { + $this->scaleUp = $scale; + return $this; + } + + public function getTransformName() { + return $this->name; + } + + public function getTransformKey() { + return $this->key; + } + + protected function getFileProperties() { + $properties = array(); + switch ($this->key) { + case self::TRANSFORM_PROFILE: + $properties['profile'] = true; + $properties['name'] = 'profile'; + break; + } + return $properties; + } + + public function generateTransforms() { + return array( + id(new PhabricatorFileThumbnailTransform()) + ->setName(pht("Profile (100px \xC3\x97 100px)")) + ->setKey(self::TRANSFORM_PROFILE) + ->setDimensions(100, 100) + ->setScaleUp(true), + id(new PhabricatorFileThumbnailTransform()) + ->setName(pht("Pinboard (280px \xC3\x97 210px)")) + ->setKey(self::TRANSFORM_PINBOARD) + ->setDimensions(280, 210), + id(new PhabricatorFileThumbnailTransform()) + ->setName(pht('Thumbgrid (100px)')) + ->setKey(self::TRANSFORM_THUMBGRID) + ->setDimensions(100, null), + id(new PhabricatorFileThumbnailTransform()) + ->setName(pht('Preview (220px)')) + ->setKey(self::TRANSFORM_PREVIEW) + ->setDimensions(220, null), + ); + } + + public function applyTransform(PhabricatorFile $file) { + $this->willTransformFile($file); + + list($src_x, $src_y) = $this->getImageDimensions(); + $dst_x = $this->dstX; + $dst_y = $this->dstY; + + $dimensions = $this->computeDimensions( + $src_x, + $src_y, + $dst_x, + $dst_y); + + $copy_x = $dimensions['copy_x']; + $copy_y = $dimensions['copy_y']; + $use_x = $dimensions['use_x']; + $use_y = $dimensions['use_y']; + $dst_x = $dimensions['dst_x']; + $dst_y = $dimensions['dst_y']; + + return $this->applyCropAndScale( + $dst_x, + $dst_y, + ($src_x - $copy_x) / 2, + ($src_y - $copy_y) / 2, + $copy_x, + $copy_y, + $use_x, + $use_y, + $this->scaleUp); + } + + + public function getTransformedDimensions(PhabricatorFile $file) { + $dst_x = $this->dstX; + $dst_y = $this->dstY; + + // If this is transform has fixed dimensions, we can trivially predict + // the dimensions of the transformed file. + if ($dst_y !== null) { + return array($dst_x, $dst_y); + } + + $src_x = $file->getImageWidth(); + $src_y = $file->getImageHeight(); + + if (!$src_x || !$src_y) { + return null; + } + + $dimensions = $this->computeDimensions( + $src_x, + $src_y, + $dst_x, + $dst_y); + + return array($dimensions['dst_x'], $dimensions['dst_y']); + } + + + private function computeDimensions($src_x, $src_y, $dst_x, $dst_y) { + if ($dst_y === null) { + // If we only have one dimension, it represents a maximum dimension. + // The other dimension of the transform is scaled appropriately, except + // that we never generate images with crazily extreme aspect ratios. + if ($src_x < $src_y) { + // This is a tall, narrow image. Use the maximum dimension for the + // height and scale the width. + $use_y = $dst_x; + $dst_y = $dst_x; + + $use_x = $dst_y * ($src_x / $src_y); + $dst_x = max($dst_y / 4, $use_x); + } else { + // This is a short, wide image. Use the maximum dimension for the width + // and scale the height. + $use_x = $dst_x; + + $use_y = $dst_x * ($src_y / $src_x); + $dst_y = max($dst_x / 4, $use_y); + } + + // In this mode, we always copy the entire source image. We may generate + // margins in the output. + $copy_x = $src_x; + $copy_y = $src_y; + } else { + $scale_up = $this->scaleUp; + + // Otherwise, both dimensions are fixed. Figure out how much we'd have to + // scale the image down along each dimension to get the entire thing to + // fit. + $scale_x = ($dst_x / $src_x); + $scale_y = ($dst_y / $src_y); + + if (!$scale_up) { + $scale_x = min($scale_x, 1); + $scale_y = min($scale_y, 1); + } + + if ($scale_x > $scale_y) { + // This image is relatively tall and narrow. We're going to crop off the + // top and bottom. + $scale = $scale_x; + } else { + // This image is relatively short and wide. We're going to crop off the + // left and right. + $scale = $scale_y; + } + + $copy_x = $dst_x / $scale; + $copy_y = $dst_y / $scale; + + if (!$scale_up) { + $copy_x = min($src_x, $copy_x); + $copy_y = min($src_y, $copy_y); + } + + // In this mode, we always use the entire destination image. We may + // crop the source input. + $use_x = $dst_x; + $use_y = $dst_y; + } + + return array( + 'copy_x' => $copy_x, + 'copy_y' => $copy_y, + 'use_x' => $use_x, + 'use_y' => $use_y, + 'dst_x' => $dst_x, + 'dst_y' => $dst_y, + ); + } + + + public function getDefaultTransform(PhabricatorFile $file) { + $x = (int)$this->dstX; + $y = (int)$this->dstY; + $name = 'image-'.$x.'x'.nonempty($y, $x).'.png'; + + $root = dirname(phutil_get_library_root('phabricator')); + $data = Filesystem::readFile($root.'/resources/builtin/'.$name); + + return $this->newFileFromData($data); + } + +} diff --git a/src/applications/files/transform/PhabricatorFileTransform.php b/src/applications/files/transform/PhabricatorFileTransform.php new file mode 100644 index 0000000000..caaf46920d --- /dev/null +++ b/src/applications/files/transform/PhabricatorFileTransform.php @@ -0,0 +1,74 @@ +canApplyTransform($file)) { + try { + return $this->applyTransform($file); + } catch (Exception $ex) { + // Ignore. + } + } + + return $this->getDefaultTransform($file); + } + + public static function getAllTransforms() { + static $map; + + if ($map === null) { + $xforms = id(new PhutilSymbolLoader()) + ->setAncestorClass(__CLASS__) + ->loadObjects(); + + $result = array(); + foreach ($xforms as $xform_template) { + foreach ($xform_template->generateTransforms() as $xform) { + $key = $xform->getTransformKey(); + if (isset($result[$key])) { + throw new Exception( + pht( + 'Two %s objects define the same transform key ("%s"), but '. + 'each transform must have a unique key.', + __CLASS__, + $key)); + } + $result[$key] = $xform; + } + } + + $map = $result; + } + + return $map; + } + + public static function getTransformByKey($key) { + $all = self::getAllTransforms(); + + $xform = idx($all, $key); + if (!$xform) { + throw new Exception( + pht( + 'No file transform with key "%s" exists.', + $key)); + } + + return $xform; + } + +} diff --git a/src/applications/fund/phortune/FundBackerCart.php b/src/applications/fund/phortune/FundBackerCart.php index 3dc25d4bbf..9cf530f23e 100644 --- a/src/applications/fund/phortune/FundBackerCart.php +++ b/src/applications/fund/phortune/FundBackerCart.php @@ -33,8 +33,7 @@ final class FundBackerCart extends PhortuneCartImplementation { $initiative = $this->getInitiative(); if (!$initiative) { - throw new Exception( - pht('Call setInitiative() before building a cart!')); + throw new PhutilInvalidStateException('setInitiative'); } $cart->setMetadataValue('initiativePHID', $initiative->getPHID()); diff --git a/src/applications/fund/storage/FundInitiativeTransaction.php b/src/applications/fund/storage/FundInitiativeTransaction.php index 3531ff3e5e..c06d62e229 100644 --- a/src/applications/fund/storage/FundInitiativeTransaction.php +++ b/src/applications/fund/storage/FundInitiativeTransaction.php @@ -38,7 +38,7 @@ final class FundInitiativeTransaction $type = $this->getTransactionType(); switch ($type) { - case FundInitiativeTransaction::TYPE_MERCHANT: + case self::TYPE_MERCHANT: if ($old) { $phids[] = $old; } @@ -46,7 +46,7 @@ final class FundInitiativeTransaction $phids[] = $new; } break; - case FundInitiativeTransaction::TYPE_REFUND: + case self::TYPE_REFUND: $phids[] = $this->getMetadataValue(self::PROPERTY_BACKER); break; } @@ -63,7 +63,7 @@ final class FundInitiativeTransaction $type = $this->getTransactionType(); switch ($type) { - case FundInitiativeTransaction::TYPE_NAME: + case self::TYPE_NAME: if ($old === null) { return pht( '%s created this initiative.', @@ -76,15 +76,15 @@ final class FundInitiativeTransaction $new); } break; - case FundInitiativeTransaction::TYPE_RISKS: + case self::TYPE_RISKS: return pht( '%s edited the risks for this initiative.', $this->renderHandleLink($author_phid)); - case FundInitiativeTransaction::TYPE_DESCRIPTION: + case self::TYPE_DESCRIPTION: return pht( '%s edited the description of this initiative.', $this->renderHandleLink($author_phid)); - case FundInitiativeTransaction::TYPE_STATUS: + case self::TYPE_STATUS: switch ($new) { case FundInitiative::STATUS_OPEN: return pht( @@ -96,14 +96,14 @@ final class FundInitiativeTransaction $this->renderHandleLink($author_phid)); } break; - case FundInitiativeTransaction::TYPE_BACKER: + case self::TYPE_BACKER: $amount = $this->getMetadataValue(self::PROPERTY_AMOUNT); $amount = PhortuneCurrency::newFromString($amount); return pht( '%s backed this initiative with %s.', $this->renderHandleLink($author_phid), $amount->formatForDisplay()); - case FundInitiativeTransaction::TYPE_REFUND: + case self::TYPE_REFUND: $amount = $this->getMetadataValue(self::PROPERTY_AMOUNT); $amount = PhortuneCurrency::newFromString($amount); @@ -114,7 +114,7 @@ final class FundInitiativeTransaction $this->renderHandleLink($author_phid), $amount->formatForDisplay(), $this->renderHandleLink($backer_phid)); - case FundInitiativeTransaction::TYPE_MERCHANT: + case self::TYPE_MERCHANT: if ($old === null) { return pht( '%s set this initiative to pay to %s.', @@ -142,7 +142,7 @@ final class FundInitiativeTransaction $type = $this->getTransactionType(); switch ($type) { - case FundInitiativeTransaction::TYPE_NAME: + case self::TYPE_NAME: if ($old === null) { return pht( '%s created %s.', @@ -156,12 +156,12 @@ final class FundInitiativeTransaction $this->renderHandleLink($object_phid)); } break; - case FundInitiativeTransaction::TYPE_DESCRIPTION: + case self::TYPE_DESCRIPTION: return pht( '%s updated the description for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); - case FundInitiativeTransaction::TYPE_STATUS: + case self::TYPE_STATUS: switch ($new) { case FundInitiative::STATUS_OPEN: return pht( @@ -175,7 +175,7 @@ final class FundInitiativeTransaction $this->renderHandleLink($object_phid)); } break; - case FundInitiativeTransaction::TYPE_BACKER: + case self::TYPE_BACKER: $amount = $this->getMetadataValue(self::PROPERTY_AMOUNT); $amount = PhortuneCurrency::newFromString($amount); return pht( @@ -183,7 +183,7 @@ final class FundInitiativeTransaction $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $amount->formatForDisplay()); - case FundInitiativeTransaction::TYPE_REFUND: + case self::TYPE_REFUND: $amount = $this->getMetadataValue(self::PROPERTY_AMOUNT); $amount = PhortuneCurrency::newFromString($amount); @@ -223,8 +223,8 @@ final class FundInitiativeTransaction public function shouldHide() { $old = $this->getOldValue(); switch ($this->getTransactionType()) { - case FundInitiativeTransaction::TYPE_DESCRIPTION: - case FundInitiativeTransaction::TYPE_RISKS: + case self::TYPE_DESCRIPTION: + case self::TYPE_RISKS: return ($old === null); } return parent::shouldHide(); @@ -232,8 +232,8 @@ final class FundInitiativeTransaction public function hasChangeDetails() { switch ($this->getTransactionType()) { - case FundInitiativeTransaction::TYPE_DESCRIPTION: - case FundInitiativeTransaction::TYPE_RISKS: + case self::TYPE_DESCRIPTION: + case self::TYPE_RISKS: return ($this->getOldValue() !== null); } diff --git a/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php index 5057097fac..4fe1128ac0 100644 --- a/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php +++ b/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php @@ -4,7 +4,7 @@ abstract class HarbormasterBuildStepImplementation { public static function getImplementations() { return id(new PhutilSymbolLoader()) - ->setAncestorClass('HarbormasterBuildStepImplementation') + ->setAncestorClass(__CLASS__) ->loadObjects(); } diff --git a/src/applications/harbormaster/storage/HarbormasterBuildable.php b/src/applications/harbormaster/storage/HarbormasterBuildable.php index b53e2bc2d3..81f8e66d07 100644 --- a/src/applications/harbormaster/storage/HarbormasterBuildable.php +++ b/src/applications/harbormaster/storage/HarbormasterBuildable.php @@ -88,7 +88,7 @@ final class HarbormasterBuildable extends HarbormasterDAO if ($buildable) { return $buildable; } - $buildable = HarbormasterBuildable::initializeNewBuildable($actor) + $buildable = self::initializeNewBuildable($actor) ->setBuildablePHID($buildable_object_phid) ->setContainerPHID($container_object_phid); $buildable->save(); @@ -116,7 +116,7 @@ final class HarbormasterBuildable extends HarbormasterDAO return; } - $buildable = HarbormasterBuildable::createOrLoadExisting( + $buildable = self::createOrLoadExisting( PhabricatorUser::getOmnipotentUser(), $phid, $container_phid); diff --git a/src/applications/help/application/PhabricatorHelpApplication.php b/src/applications/help/application/PhabricatorHelpApplication.php index c86d5cd0d0..b1f66b02cd 100644 --- a/src/applications/help/application/PhabricatorHelpApplication.php +++ b/src/applications/help/application/PhabricatorHelpApplication.php @@ -43,7 +43,7 @@ final class PhabricatorHelpApplication extends PhabricatorApplication { array( 'bubbleID' => $help_id, 'dropdownID' => 'phabricator-help-menu', - 'applicationClass' => 'PhabricatorHelpApplication', + 'applicationClass' => __CLASS__, 'local' => true, 'desktop' => true, 'right' => true, diff --git a/src/applications/herald/adapter/HeraldAdapter.php b/src/applications/herald/adapter/HeraldAdapter.php index 6a51ade55f..f56572a398 100644 --- a/src/applications/herald/adapter/HeraldAdapter.php +++ b/src/applications/herald/adapter/HeraldAdapter.php @@ -1083,7 +1083,7 @@ abstract class HeraldAdapter { public static function getEnabledAdapterMap(PhabricatorUser $viewer) { $map = array(); - $adapters = HeraldAdapter::getAllAdapters(); + $adapters = self::getAllAdapters(); foreach ($adapters as $adapter) { if (!$adapter->isAvailableToUser($viewer)) { continue; diff --git a/src/applications/macro/controller/PhabricatorMacroMemeController.php b/src/applications/macro/controller/PhabricatorMacroMemeController.php index badb1759ad..ff25a8a8a4 100644 --- a/src/applications/macro/controller/PhabricatorMacroMemeController.php +++ b/src/applications/macro/controller/PhabricatorMacroMemeController.php @@ -14,7 +14,7 @@ final class PhabricatorMacroMemeController $lower_text = $request->getStr('lowertext'); $user = $request->getUser(); - $uri = PhabricatorMacroMemeController::generateMacro($user, $macro_name, + $uri = self::generateMacro($user, $macro_name, $upper_text, $lower_text); if ($uri === false) { return new Aphront404Response(); diff --git a/src/applications/macro/markup/PhabricatorMemeRemarkupRule.php b/src/applications/macro/markup/PhabricatorMemeRemarkupRule.php index 0c949e130c..004e415450 100644 --- a/src/applications/macro/markup/PhabricatorMemeRemarkupRule.php +++ b/src/applications/macro/markup/PhabricatorMemeRemarkupRule.php @@ -10,7 +10,7 @@ final class PhabricatorMemeRemarkupRule extends PhutilRemarkupRule { public function apply($text) { return preg_replace_callback( - '@{meme,((?:[^}\\\\]+|\\\\.)+)}$@m', + '@{meme,((?:[^}\\\\]+|\\\\.)+)}@m', array($this, 'markupMeme'), $text); } diff --git a/src/applications/macro/query/PhabricatorMacroSearchEngine.php b/src/applications/macro/query/PhabricatorMacroSearchEngine.php index 8a810c886f..8d632e818f 100644 --- a/src/applications/macro/query/PhabricatorMacroSearchEngine.php +++ b/src/applications/macro/query/PhabricatorMacroSearchEngine.php @@ -179,14 +179,18 @@ final class PhabricatorMacroSearchEngine assert_instances_of($macros, 'PhabricatorFileImageMacro'); $viewer = $this->requireViewer(); + $xform = PhabricatorFileTransform::getTransformByKey( + PhabricatorFileThumbnailTransform::TRANSFORM_PINBOARD); + $pinboard = new PHUIPinboardView(); foreach ($macros as $macro) { $file = $macro->getFile(); $item = new PHUIPinboardItemView(); if ($file) { - $item->setImageURI($file->getThumb280x210URI()); - $item->setImageSize(280, 210); + $item->setImageURI($file->getURIForTransform($xform)); + list($x, $y) = $xform->getTransformedDimensions($file); + $item->setImageSize($x, $y); } if ($macro->getDateCreated()) { diff --git a/src/applications/maniphest/query/ManiphestTaskQuery.php b/src/applications/maniphest/query/ManiphestTaskQuery.php index 60e3425af6..4a6381581b 100644 --- a/src/applications/maniphest/query/ManiphestTaskQuery.php +++ b/src/applications/maniphest/query/ManiphestTaskQuery.php @@ -754,7 +754,7 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery { $id = $result->getID(); if ($this->groupBy == self::GROUP_PROJECT) { - return rtrim($id.'.'.$result->getGroupByProjectPHID(), '.');; + return rtrim($id.'.'.$result->getGroupByProjectPHID(), '.'); } return $id; diff --git a/src/applications/metamta/command/MetaMTAEmailTransactionCommand.php b/src/applications/metamta/command/MetaMTAEmailTransactionCommand.php index e6011768df..cc78df3492 100644 --- a/src/applications/metamta/command/MetaMTAEmailTransactionCommand.php +++ b/src/applications/metamta/command/MetaMTAEmailTransactionCommand.php @@ -91,7 +91,7 @@ abstract class MetaMTAEmailTransactionCommand extends Phobject { } public static function getCommandMap(array $commands) { - assert_instances_of($commands, 'MetaMTAEmailTransactionCommand'); + assert_instances_of($commands, __CLASS__); $map = array(); foreach ($commands as $command) { diff --git a/src/applications/metamta/contentsource/PhabricatorContentSource.php b/src/applications/metamta/contentsource/PhabricatorContentSource.php index a1cbd061f2..e5a29572c1 100644 --- a/src/applications/metamta/contentsource/PhabricatorContentSource.php +++ b/src/applications/metamta/contentsource/PhabricatorContentSource.php @@ -46,13 +46,13 @@ final class PhabricatorContentSource { public static function newConsoleSource() { return self::newForSource( - PhabricatorContentSource::SOURCE_CONSOLE, + self::SOURCE_CONSOLE, array()); } public static function newFromRequest(AphrontRequest $request) { return self::newForSource( - PhabricatorContentSource::SOURCE_WEB, + self::SOURCE_WEB, array( 'ip' => $request->getRemoteAddr(), )); @@ -60,7 +60,7 @@ final class PhabricatorContentSource { public static function newFromConduitRequest(ConduitAPIRequest $request) { return self::newForSource( - PhabricatorContentSource::SOURCE_CONDUIT, + self::SOURCE_CONDUIT, array()); } diff --git a/src/applications/multimeter/data/MultimeterControl.php b/src/applications/multimeter/data/MultimeterControl.php index 2bc849c185..892e9e56cd 100644 --- a/src/applications/multimeter/data/MultimeterControl.php +++ b/src/applications/multimeter/data/MultimeterControl.php @@ -88,7 +88,7 @@ final class MultimeterControl { } if ($this->sampleRate === null) { - throw new Exception(pht('Call setSampleRate() before saving events!')); + throw new PhutilInvalidStateException('setSampleRate'); } $this->addServiceEvents(); diff --git a/src/applications/nuance/storage/NuanceItem.php b/src/applications/nuance/storage/NuanceItem.php index 2db403cd2e..50d7ee72ef 100644 --- a/src/applications/nuance/storage/NuanceItem.php +++ b/src/applications/nuance/storage/NuanceItem.php @@ -20,7 +20,7 @@ final class NuanceItem public static function initializeNewItem(PhabricatorUser $user) { return id(new NuanceItem()) ->setDateNuanced(time()) - ->setStatus(NuanceItem::STATUS_OPEN); + ->setStatus(self::STATUS_OPEN); } protected function getConfiguration() { diff --git a/src/applications/paste/storage/PhabricatorPasteTransaction.php b/src/applications/paste/storage/PhabricatorPasteTransaction.php index cc4e36d2ff..34cc6bd92d 100644 --- a/src/applications/paste/storage/PhabricatorPasteTransaction.php +++ b/src/applications/paste/storage/PhabricatorPasteTransaction.php @@ -67,7 +67,7 @@ final class PhabricatorPasteTransaction $type = $this->getTransactionType(); switch ($type) { - case PhabricatorPasteTransaction::TYPE_CONTENT: + case self::TYPE_CONTENT: if ($old === null) { return pht( '%s created this paste.', @@ -78,13 +78,13 @@ final class PhabricatorPasteTransaction $this->renderHandleLink($author_phid)); } break; - case PhabricatorPasteTransaction::TYPE_TITLE: + case self::TYPE_TITLE: return pht( '%s updated the paste\'s title to "%s".', $this->renderHandleLink($author_phid), $new); break; - case PhabricatorPasteTransaction::TYPE_LANGUAGE: + case self::TYPE_LANGUAGE: return pht( "%s updated the paste's language.", $this->renderHandleLink($author_phid)); @@ -103,7 +103,7 @@ final class PhabricatorPasteTransaction $type = $this->getTransactionType(); switch ($type) { - case PhabricatorPasteTransaction::TYPE_CONTENT: + case self::TYPE_CONTENT: if ($old === null) { return pht( '%s created %s.', @@ -116,13 +116,13 @@ final class PhabricatorPasteTransaction $this->renderHandleLink($object_phid)); } break; - case PhabricatorPasteTransaction::TYPE_TITLE: + case self::TYPE_TITLE: return pht( '%s updated the title for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); break; - case PhabricatorPasteTransaction::TYPE_LANGUAGE: + case self::TYPE_LANGUAGE: return pht( '%s updated the language for %s.', $this->renderHandleLink($author_phid), diff --git a/src/applications/paste/view/PasteEmbedView.php b/src/applications/paste/view/PasteEmbedView.php index 46301e5a31..650905c46c 100644 --- a/src/applications/paste/view/PasteEmbedView.php +++ b/src/applications/paste/view/PasteEmbedView.php @@ -28,7 +28,7 @@ final class PasteEmbedView extends AphrontView { public function render() { if (!$this->paste) { - throw new Exception('Call setPaste() before render()!'); + throw new PhutilInvalidStateException('setPaste'); } $lines = phutil_split_lines($this->paste->getContent()); diff --git a/src/applications/people/controller/PhabricatorPeopleCalendarController.php b/src/applications/people/controller/PhabricatorPeopleCalendarController.php index 06c7033fd7..168c1def70 100644 --- a/src/applications/people/controller/PhabricatorPeopleCalendarController.php +++ b/src/applications/people/controller/PhabricatorPeopleCalendarController.php @@ -35,29 +35,41 @@ final class PhabricatorPeopleCalendarController $month = $request->getInt('month', $month_d); $day = phabricator_format_local_time($now, $user, 'j'); - - $holidays = id(new PhabricatorCalendarHoliday())->loadAllWhere( - 'day BETWEEN %s AND %s', - "{$year}-{$month}-01", - "{$year}-{$month}-31"); + $start_epoch = strtotime("{$year}-{$month}-01"); + $end_epoch = strtotime("{$year}-{$month}-01 next month"); $statuses = id(new PhabricatorCalendarEventQuery()) ->setViewer($user) ->withInvitedPHIDs(array($user->getPHID())) ->withDateRange( - strtotime("{$year}-{$month}-01"), - strtotime("{$year}-{$month}-01 next month")) + $start_epoch, + $end_epoch) ->execute(); + $start_range_value = AphrontFormDateControlValue::newFromEpoch( + $user, + $start_epoch); + $end_range_value = AphrontFormDateControlValue::newFromEpoch( + $user, + $end_epoch); + if ($month == $month_d && $year == $year_d) { - $month_view = new PHUICalendarMonthView($month, $year, $day); + $month_view = new PHUICalendarMonthView( + $start_range_value, + $end_range_value, + $month, + $year, + $day); } else { - $month_view = new PHUICalendarMonthView($month, $year); + $month_view = new PHUICalendarMonthView( + $start_range_value, + $end_range_value, + $month, + $year); } $month_view->setBrowseURI($request->getRequestURI()); $month_view->setUser($user); - $month_view->setHolidays($holidays); $month_view->setImage($picture); $phids = mpull($statuses, 'getUserPHID'); @@ -67,7 +79,7 @@ final class PhabricatorPeopleCalendarController $event = new AphrontCalendarEventView(); $event->setEpochRange($status->getDateFrom(), $status->getDateTo()); $event->setUserPHID($status->getUserPHID()); - $event->setName($status->getHumanStatus()); + $event->setName($status->getName()); $event->setDescription($status->getDescription()); $event->setEventID($status->getID()); $month_view->addEvent($event); diff --git a/src/applications/people/controller/PhabricatorPeopleProfilePictureController.php b/src/applications/people/controller/PhabricatorPeopleProfilePictureController.php index 231181614d..0f59e23286 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfilePictureController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfilePictureController.php @@ -70,12 +70,9 @@ final class PhabricatorPeopleProfilePictureController 'This server only supports these image formats: %s.', implode(', ', $supported_formats)); } else { - $xformer = new PhabricatorImageTransformer(); - $xformed = $xformer->executeProfileTransform( - $file, - $width = 50, - $min_height = 50, - $max_height = 50); + $xform = PhabricatorFileTransform::getTransformByKey( + PhabricatorFileThumbnailTransform::TRANSFORM_PROFILE); + $xformed = $xform->executeTransform($file); } } diff --git a/src/applications/people/editor/__tests__/PhabricatorUserEditorTestCase.php b/src/applications/people/editor/__tests__/PhabricatorUserEditorTestCase.php index f5c3c73a9a..6cc6abf37b 100644 --- a/src/applications/people/editor/__tests__/PhabricatorUserEditorTestCase.php +++ b/src/applications/people/editor/__tests__/PhabricatorUserEditorTestCase.php @@ -14,7 +14,7 @@ final class PhabricatorUserEditorTestCase extends PhabricatorTestCase { $this->registerUser( 'PhabricatorUserEditorTestCaseOK', - 'PhabricatorUserEditorTestCase@example.com'); + 'PhabricatorUserEditorTest@example.com'); $this->assertTrue(true); } @@ -45,7 +45,7 @@ final class PhabricatorUserEditorTestCase extends PhabricatorTestCase { try { $this->registerUser( 'PhabricatorUserEditorTestCaseDomain', - 'PhabricatorUserEditorTestCase@whitehouse.gov'); + 'PhabricatorUserEditorTest@whitehouse.gov'); } catch (Exception $ex) { $caught = $ex; } diff --git a/src/applications/people/query/PhabricatorPeopleQuery.php b/src/applications/people/query/PhabricatorPeopleQuery.php index ac9d8b0200..b8b7ad1e29 100644 --- a/src/applications/people/query/PhabricatorPeopleQuery.php +++ b/src/applications/people/query/PhabricatorPeopleQuery.php @@ -148,26 +148,55 @@ final class PhabricatorPeopleQuery } if ($this->needProfileImage) { - $user_profile_file_phids = mpull($users, 'getProfileImagePHID'); - $user_profile_file_phids = array_filter($user_profile_file_phids); - if ($user_profile_file_phids) { - $files = id(new PhabricatorFileQuery()) - ->setParentQuery($this) - ->setViewer($this->getViewer()) - ->withPHIDs($user_profile_file_phids) - ->execute(); - $files = mpull($files, null, 'getPHID'); - } else { - $files = array(); - } + $rebuild = array(); foreach ($users as $user) { - $image_phid = $user->getProfileImagePHID(); - if (isset($files[$image_phid])) { - $profile_image_uri = $files[$image_phid]->getBestURI(); - } else { - $profile_image_uri = PhabricatorUser::getDefaultProfileImageURI(); + $image_uri = $user->getProfileImageCache(); + if ($image_uri) { + // This user has a valid cache, so we don't need to fetch any + // data or rebuild anything. + + $user->attachProfileImageURI($image_uri); + continue; + } + + // This user's cache is invalid or missing, so we're going to rebuild + // it. + $rebuild[] = $user; + } + + if ($rebuild) { + $file_phids = mpull($rebuild, 'getProfileImagePHID'); + $file_phids = array_filter($file_phids); + + if ($file_phids) { + // NOTE: We're using the omnipotent user here because older profile + // images do not have the 'profile' flag, so they may not be visible + // to the executing viewer. At some point, we could migrate to add + // this flag and then use the real viewer, or just use the real + // viewer after enough time has passed to limit the impact of old + // data. The consequence of missing here is that we cache a default + // image when a real image exists. + $files = id(new PhabricatorFileQuery()) + ->setParentQuery($this) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs($file_phids) + ->execute(); + $files = mpull($files, null, 'getPHID'); + } else { + $files = array(); + } + + foreach ($rebuild as $user) { + $image_phid = $user->getProfileImagePHID(); + if (isset($files[$image_phid])) { + $image_uri = $files[$image_phid]->getBestURI(); + } else { + $image_uri = PhabricatorUser::getDefaultProfileImageURI(); + } + + $user->writeProfileImageCache($image_uri); + $user->attachProfileImageURI($image_uri); } - $user->attachProfileImageURI($profile_image_uri); } } diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php index ea138042b2..3fbfac062f 100644 --- a/src/applications/people/storage/PhabricatorUser.php +++ b/src/applications/people/storage/PhabricatorUser.php @@ -1,6 +1,7 @@ 'uint32', 'accountSecret' => 'bytes64', 'isEnrolledInMultiFactor' => 'bool', + 'profileImageCache' => 'text255?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, @@ -160,6 +163,9 @@ final class PhabricatorUser 'columns' => array('isApproved'), ), ), + self::CONFIG_NO_MUTATE => array( + 'profileImageCache' => true, + ), ) + parent::getConfiguration(); } @@ -682,6 +688,10 @@ EOBODY; } } + public function getTimeZone() { + return new DateTimeZone($this->getTimezoneIdentifier()); + } + public function __toString() { return $this->getUsername(); } @@ -717,6 +727,72 @@ EOBODY; } +/* -( Profile Image Cache )------------------------------------------------ */ + + + /** + * Get this user's cached profile image URI. + * + * @return string|null Cached URI, if a URI is cached. + * @task image-cache + */ + public function getProfileImageCache() { + $version = $this->getProfileImageVersion(); + + $parts = explode(',', $this->profileImageCache, 2); + if (count($parts) !== 2) { + return null; + } + + if ($parts[0] !== $version) { + return null; + } + + return $parts[1]; + } + + + /** + * Generate a new cache value for this user's profile image. + * + * @return string New cache value. + * @task image-cache + */ + public function writeProfileImageCache($uri) { + $version = $this->getProfileImageVersion(); + $cache = "{$version},{$uri}"; + + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + queryfx( + $this->establishConnection('w'), + 'UPDATE %T SET profileImageCache = %s WHERE id = %d', + $this->getTableName(), + $cache, + $this->getID()); + unset($unguarded); + } + + + /** + * Get a version identifier for a user's profile image. + * + * This version will change if the image changes, or if any of the + * environment configuration which goes into generating a URI changes. + * + * @return string Cache version. + * @task image-cache + */ + private function getProfileImageVersion() { + $parts = array( + PhabricatorEnv::getCDNURI('/'), + PhabricatorEnv::getEnvConfig('cluster.instance'), + $this->getProfileImagePHID(), + ); + $parts = serialize($parts); + return PhabricatorHash::digestForIndex($parts); + } + + /* -( Multi-Factor Authentication )---------------------------------------- */ diff --git a/src/applications/phame/application/PhabricatorPhameApplication.php b/src/applications/phame/application/PhabricatorPhameApplication.php index b466ad5191..4d8cebffa0 100644 --- a/src/applications/phame/application/PhabricatorPhameApplication.php +++ b/src/applications/phame/application/PhabricatorPhameApplication.php @@ -69,4 +69,10 @@ final class PhabricatorPhameApplication extends PhabricatorApplication { ); } + public function getQuicksandURIPatternBlacklist() { + return array( + '/phame/live/.*', + ); + } + } diff --git a/src/applications/phid/handle/pool/PhabricatorHandleList.php b/src/applications/phid/handle/pool/PhabricatorHandleList.php index d21e461871..7a8fed529f 100644 --- a/src/applications/phid/handle/pool/PhabricatorHandleList.php +++ b/src/applications/phid/handle/pool/PhabricatorHandleList.php @@ -24,6 +24,7 @@ final class PhabricatorHandleList private $handlePool; private $phids; + private $count; private $handles; private $cursor; private $map; @@ -35,6 +36,7 @@ final class PhabricatorHandleList public function setPHIDs(array $phids) { $this->phids = $phids; + $this->count = count($phids); return $this; } @@ -119,7 +121,7 @@ final class PhabricatorHandleList } public function valid() { - return isset($this->phids[$this->cursor]); + return ($this->cursor < $this->count); } @@ -156,8 +158,9 @@ final class PhabricatorHandleList private function raiseImmutableException() { throw new Exception( pht( - 'Trying to mutate a PhabricatorHandleList, but this is not permitted; '. - 'handle lists are immutable.')); + 'Trying to mutate a %s, but this is not permitted; '. + 'handle lists are immutable.', + __CLASS__)); } @@ -165,7 +168,7 @@ final class PhabricatorHandleList public function count() { - return count($this->phids); + return $this->count; } } diff --git a/src/applications/phid/handle/pool/PhabricatorHandlePool.php b/src/applications/phid/handle/pool/PhabricatorHandlePool.php index a1195225bb..eaa48828a4 100644 --- a/src/applications/phid/handle/pool/PhabricatorHandlePool.php +++ b/src/applications/phid/handle/pool/PhabricatorHandlePool.php @@ -61,12 +61,17 @@ final class PhabricatorHandlePool extends Phobject { // If we need any handles, bulk load everything in the queue. if ($need) { + // Clear the list of PHIDs that need to be loaded before performing the + // actual fetch. This prevents us from looping if we need to reenter the + // HandlePool while loading handles. + $fetch_phids = array_keys($this->unloadedPHIDs); + $this->unloadedPHIDs = array(); + $handles = id(new PhabricatorHandleQuery()) ->setViewer($this->getViewer()) - ->withPHIDs(array_keys($this->unloadedPHIDs)) + ->withPHIDs($fetch_phids) ->execute(); $this->handles += $handles; - $this->unloadedPHIDs = array(); } return array_select_keys($this->handles, $phids); diff --git a/src/applications/phid/query/PhabricatorObjectQuery.php b/src/applications/phid/query/PhabricatorObjectQuery.php index 49d238c6a9..26d0668cc2 100644 --- a/src/applications/phid/query/PhabricatorObjectQuery.php +++ b/src/applications/phid/query/PhabricatorObjectQuery.php @@ -78,7 +78,7 @@ final class PhabricatorObjectQuery public function getNamedResults() { if ($this->namedResults === null) { - throw new Exception('Call execute() before getNamedResults()!'); + throw new PhutilInvalidStateException('execute'); } return $this->namedResults; } @@ -125,8 +125,19 @@ final class PhabricatorObjectQuery $groups[$type][] = $phid; } + $in_flight = $this->getPHIDsInFlight(); foreach ($groups as $type => $group) { - if (isset($types[$type])) { + // Don't try to load PHIDs which are already "in flight"; this prevents + // us from recursing indefinitely if policy checks or edges form a loop. + // We will decline to load the corresponding objects. + foreach ($group as $key => $phid) { + if (isset($in_flight[$phid])) { + unset($group[$key]); + } + } + + if ($group && isset($types[$type])) { + $this->putPHIDsInFlight($group); $objects = $types[$type]->loadObjects($this, $group); $results += mpull($objects, null, 'getPHID'); } diff --git a/src/applications/phid/type/PhabricatorPHIDType.php b/src/applications/phid/type/PhabricatorPHIDType.php index 4445ab863d..fd64ab7bd1 100644 --- a/src/applications/phid/type/PhabricatorPHIDType.php +++ b/src/applications/phid/type/PhabricatorPHIDType.php @@ -169,9 +169,13 @@ abstract class PhabricatorPHIDType { $that_class = $original[$type]; $this_class = get_class($object); throw new Exception( - "Two PhabricatorPHIDType classes ({$that_class}, {$this_class}) ". - "both handle PHID type '{$type}'. A type may be handled by only ". - "one class."); + pht( + "Two %s classes (%s, %s) both handle PHID type '%s'. ". + "A type may be handled by only one class.", + __CLASS__, + $that_class, + $this_class, + $type)); } $original[$type] = get_class($object); diff --git a/src/applications/pholio/application/PhabricatorPholioApplication.php b/src/applications/pholio/application/PhabricatorPholioApplication.php index e56f989630..801a2de4e8 100644 --- a/src/applications/pholio/application/PhabricatorPholioApplication.php +++ b/src/applications/pholio/application/PhabricatorPholioApplication.php @@ -49,7 +49,6 @@ final class PhabricatorPholioApplication extends PhabricatorApplication { 'inline/' => array( '(?:(?P\d+)/)?' => 'PholioInlineController', 'list/(?P\d+)/' => 'PholioInlineListController', - 'thumb/(?P\d+)/' => 'PholioInlineThumbController', ), 'image/' => array( 'upload/' => 'PholioImageUploadController', diff --git a/src/applications/pholio/controller/PholioInlineThumbController.php b/src/applications/pholio/controller/PholioInlineThumbController.php deleted file mode 100644 index 624ce3d3ea..0000000000 --- a/src/applications/pholio/controller/PholioInlineThumbController.php +++ /dev/null @@ -1,46 +0,0 @@ -imageid = idx($data, 'imageid'); - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); - - $image = id(new PholioImage())->load($this->imageid); - - if ($image == null) { - return new Aphront404Response(); - } - - $mock = id(new PholioMockQuery()) - ->setViewer($user) - ->withIDs(array($image->getMockID())) - ->executeOne(); - - if (!$mock) { - return new Aphront404Response(); - } - - $file = id(new PhabricatorFileQuery()) - ->setViewer($user) - ->witHPHIDs(array($image->getFilePHID())) - ->executeOne(); - - if (!$file) { - return new Aphront404Response(); - } - - return id(new AphrontRedirectResponse())->setURI($file->getThumb60x45URI()); - } - -} diff --git a/src/applications/pholio/query/PholioMockSearchEngine.php b/src/applications/pholio/query/PholioMockSearchEngine.php index 226f225e11..057a370649 100644 --- a/src/applications/pholio/query/PholioMockSearchEngine.php +++ b/src/applications/pholio/query/PholioMockSearchEngine.php @@ -141,15 +141,22 @@ final class PholioMockSearchEngine extends PhabricatorApplicationSearchEngine { $viewer = $this->requireViewer(); + $xform = PhabricatorFileTransform::getTransformByKey( + PhabricatorFileThumbnailTransform::TRANSFORM_PINBOARD); + $board = new PHUIPinboardView(); foreach ($mocks as $mock) { + $image = $mock->getCoverFile(); + $image_uri = $image->getURIForTransform($xform); + list($x, $y) = $xform->getTransformedDimensions($image); + $header = 'M'.$mock->getID().' '.$mock->getName(); $item = id(new PHUIPinboardItemView()) ->setHeader($header) ->setURI('/M'.$mock->getID()) - ->setImageURI($mock->getCoverFile()->getThumb280x210URI()) - ->setImageSize(280, 210) + ->setImageURI($image_uri) + ->setImageSize($x, $y) ->setDisabled($mock->isClosed()) ->addIconCount('fa-picture-o', count($mock->getImages())) ->addIconCount('fa-trophy', $mock->getTokenCount()); diff --git a/src/applications/pholio/remarkup/PholioRemarkupRule.php b/src/applications/pholio/remarkup/PholioRemarkupRule.php index e8c8f00a17..2b1ca0f876 100644 --- a/src/applications/pholio/remarkup/PholioRemarkupRule.php +++ b/src/applications/pholio/remarkup/PholioRemarkupRule.php @@ -25,6 +25,10 @@ final class PholioRemarkupRule extends PhabricatorObjectRemarkupRule { $href = $href.'/'.$id[1].'/'; } + if ($this->getEngine()->getConfig('uri.full')) { + $href = PhabricatorEnv::getURI($href); + } + return $href; } diff --git a/src/applications/pholio/view/PholioMockEmbedView.php b/src/applications/pholio/view/PholioMockEmbedView.php index 81dfa670a3..3429cfd569 100644 --- a/src/applications/pholio/view/PholioMockEmbedView.php +++ b/src/applications/pholio/view/PholioMockEmbedView.php @@ -17,7 +17,7 @@ final class PholioMockEmbedView extends AphrontView { public function render() { if (!$this->mock) { - throw new Exception('Call setMock() before render()!'); + throw new PhutilInvalidStateException('setMock'); } $mock = $this->mock; @@ -28,25 +28,29 @@ final class PholioMockEmbedView extends AphrontView { $this->mock->getImages(), array_flip($this->images)); } + $xform = PhabricatorFileTransform::getTransformByKey( + PhabricatorFileThumbnailTransform::TRANSFORM_PINBOARD); + if ($images_to_show) { - foreach ($images_to_show as $image) { - $thumbfile = $image->getFile(); - $thumbnail = $thumbfile->getThumb280x210URI(); - } + $image = head($images_to_show); + $thumbfile = $image->getFile(); $header = 'M'.$mock->getID().' '.$mock->getName(). ' (#'.$image->getID().')'; $uri = '/M'.$this->mock->getID().'/'.$image->getID().'/'; } else { - $thumbnail = $mock->getCoverFile()->getThumb280x210URI(); + $thumbfile = $mock->getCoverFile(); $header = 'M'.$mock->getID().' '.$mock->getName(); $uri = '/M'.$this->mock->getID(); } + $thumbnail = $thumbfile->getURIForTransform($xform); + list($x, $y) = $xform->getTransformedDimensions($thumbfile); + $item = id(new PHUIPinboardItemView()) ->setHeader($header) ->setURI($uri) ->setImageURI($thumbnail) - ->setImageSize(280, 210) + ->setImageSize($x, $y) ->setDisabled($mock->isClosed()) ->addIconCount('fa-picture-o', count($mock->getImages())) ->addIconCount('fa-trophy', $mock->getTokenCount()); diff --git a/src/applications/pholio/view/PholioMockImagesView.php b/src/applications/pholio/view/PholioMockImagesView.php index f894c698f2..d59e701579 100644 --- a/src/applications/pholio/view/PholioMockImagesView.php +++ b/src/applications/pholio/view/PholioMockImagesView.php @@ -68,8 +68,11 @@ final class PholioMockImagesView extends AphrontView { // TODO: We could maybe do a better job with tailoring this, which is the // image shown on the review stage. - $nonimage_uri = celerity_get_resource_uri( - 'rsrc/image/icon/fatcow/thumbnails/default.p100.png'); + $default_name = 'image-100x100.png'; + $builtins = PhabricatorFile::loadBuiltins( + $this->getUser(), + array($default_name)); + $default = $builtins[$default_name]; $engine = id(new PhabricatorMarkupEngine()) ->setViewer($this->getUser()); @@ -97,7 +100,7 @@ final class PholioMockImagesView extends AphrontView { 'fullURI' => $file->getBestURI(), 'stageURI' => ($file->isViewableImage() ? $file->getBestURI() - : $nonimage_uri), + : $default->getBestURI()), 'pageURI' => $this->getImagePageURI($image, $mock), 'downloadURI' => $file->getDownloadURI(), 'historyURI' => $history_uri, diff --git a/src/applications/pholio/view/PholioMockThumbGridView.php b/src/applications/pholio/view/PholioMockThumbGridView.php index df9fe1aa06..8e9d3007c5 100644 --- a/src/applications/pholio/view/PholioMockThumbGridView.php +++ b/src/applications/pholio/view/PholioMockThumbGridView.php @@ -114,28 +114,34 @@ final class PholioMockThumbGridView extends AphrontView { private function renderThumbnail(PholioImage $image) { $thumbfile = $image->getFile(); + $preview_key = PhabricatorFileThumbnailTransform::TRANSFORM_THUMBGRID; + $xform = PhabricatorFileTransform::getTransformByKey($preview_key); + + $attributes = array( + 'class' => 'pholio-mock-thumb-grid-image', + 'src' => $thumbfile->getURIForTransform($xform), + ); + if ($image->getFile()->isViewableImage()) { - $dimensions = PhabricatorImageTransformer::getPreviewDimensions( - $thumbfile, - 100); + $dimensions = $xform->getTransformedDimensions($thumbfile); + if ($dimensions) { + list($x, $y) = $dimensions; + $attributes += array( + 'width' => $x, + 'height' => $y, + 'style' => 'top: '.floor((100 - $y) / 2).'px', + ); + } } else { // If this is a PDF or a text file or something, we'll end up using a // generic thumbnail which is always sized correctly. - $dimensions = array( - 'sdx' => 100, - 'sdy' => 100, + $attributes += array( + 'width' => 100, + 'height' => 100, ); } - $tag = phutil_tag( - 'img', - array( - 'width' => $dimensions['sdx'], - 'height' => $dimensions['sdy'], - 'src' => $thumbfile->getPreview100URI(), - 'class' => 'pholio-mock-thumb-grid-image', - 'style' => 'top: '.floor((100 - $dimensions['sdy'] ) / 2).'px', - )); + $tag = phutil_tag('img', $attributes); $classes = array('pholio-mock-thumb-grid-item'); if ($image->getIsObsolete()) { diff --git a/src/applications/pholio/view/PholioUploadedImageView.php b/src/applications/pholio/view/PholioUploadedImageView.php index fb8a82431e..2ff3ba0390 100644 --- a/src/applications/pholio/view/PholioUploadedImageView.php +++ b/src/applications/pholio/view/PholioUploadedImageView.php @@ -38,11 +38,15 @@ final class PholioUploadedImageView extends AphrontView { ->setSigil('image-description') ->setLabel(pht('Description')); + $xform = PhabricatorFileTransform::getTransformByKey( + PhabricatorFileThumbnailTransform::TRANSFORM_PINBOARD); + $thumbnail_uri = $file->getURIForTransform($xform); + $thumb_frame = phutil_tag( 'div', array( 'class' => 'pholio-thumb-frame', - 'style' => 'background-image: url('.$file->getThumb280x210URI().');', + 'style' => 'background-image: url('.$thumbnail_uri.');', )); $handle = javelin_tag( diff --git a/src/applications/phortune/cart/PhortuneSubscriptionCart.php b/src/applications/phortune/cart/PhortuneSubscriptionCart.php index 6c17e00331..ff71106932 100644 --- a/src/applications/phortune/cart/PhortuneSubscriptionCart.php +++ b/src/applications/phortune/cart/PhortuneSubscriptionCart.php @@ -34,8 +34,7 @@ final class PhortuneSubscriptionCart $subscription = $this->getSubscription(); if (!$subscription) { - throw new Exception( - pht('Call setSubscription() before building a cart!')); + throw new PhutilInvalidStateException('setSubscription'); } $cart->setMetadataValue('subscriptionPHID', $subscription->getPHID()); diff --git a/src/applications/phortune/currency/PhortuneCurrency.php b/src/applications/phortune/currency/PhortuneCurrency.php index a473738ed6..b59d9b144a 100644 --- a/src/applications/phortune/currency/PhortuneCurrency.php +++ b/src/applications/phortune/currency/PhortuneCurrency.php @@ -68,10 +68,10 @@ final class PhortuneCurrency extends Phobject { } public static function newFromList(array $list) { - assert_instances_of($list, 'PhortuneCurrency'); + assert_instances_of($list, __CLASS__); if (!$list) { - return PhortuneCurrency::newEmptyCurrency(); + return self::newEmptyCurrency(); } $total = null; @@ -201,8 +201,8 @@ final class PhortuneCurrency extends Phobject { */ public function assertInRange($minimum, $maximum) { if ($minimum !== null && $maximum !== null) { - $min = PhortuneCurrency::newFromString($minimum); - $max = PhortuneCurrency::newFromString($maximum); + $min = self::newFromString($minimum); + $max = self::newFromString($maximum); if ($min->value > $max->value) { throw new Exception( pht( @@ -213,7 +213,7 @@ final class PhortuneCurrency extends Phobject { } if ($minimum !== null) { - $min = PhortuneCurrency::newFromString($minimum); + $min = self::newFromString($minimum); if ($min->value > $this->value) { throw new Exception( pht( @@ -223,7 +223,7 @@ final class PhortuneCurrency extends Phobject { } if ($maximum !== null) { - $max = PhortuneCurrency::newFromString($maximum); + $max = self::newFromString($maximum); if ($max->value < $this->value) { throw new Exception( pht( diff --git a/src/applications/phortune/provider/PhortunePaymentProvider.php b/src/applications/phortune/provider/PhortunePaymentProvider.php index 49c80dcd72..da36779d06 100644 --- a/src/applications/phortune/provider/PhortunePaymentProvider.php +++ b/src/applications/phortune/provider/PhortunePaymentProvider.php @@ -118,7 +118,7 @@ abstract class PhortunePaymentProvider { public static function getAllProviders() { return id(new PhutilSymbolLoader()) - ->setAncestorClass('PhortunePaymentProvider') + ->setAncestorClass(__CLASS__) ->loadObjects(); } diff --git a/src/applications/phortune/storage/PhortuneAccount.php b/src/applications/phortune/storage/PhortuneAccount.php index facb9d5089..e86fd53df2 100644 --- a/src/applications/phortune/storage/PhortuneAccount.php +++ b/src/applications/phortune/storage/PhortuneAccount.php @@ -27,7 +27,7 @@ final class PhortuneAccount extends PhortuneDAO PhabricatorUser $actor, PhabricatorContentSource $content_source) { - $account = PhortuneAccount::initializeNewAccount($actor); + $account = self::initializeNewAccount($actor); $xactions = array(); $xactions[] = id(new PhortuneAccountTransaction()) diff --git a/src/applications/phortune/storage/PhortuneCart.php b/src/applications/phortune/storage/PhortuneCart.php index 46d474cc93..70ecbb21c9 100644 --- a/src/applications/phortune/storage/PhortuneCart.php +++ b/src/applications/phortune/storage/PhortuneCart.php @@ -136,7 +136,7 @@ final class PhortuneCart extends PhortuneDAO } $charge->save(); - $this->setStatus(PhortuneCart::STATUS_PURCHASING)->save(); + $this->setStatus(self::STATUS_PURCHASING)->save(); $this->endReadLocking(); $this->saveTransaction(); diff --git a/src/applications/phragment/storage/PhragmentFragment.php b/src/applications/phragment/storage/PhragmentFragment.php index 3f5719178b..574283d7a7 100644 --- a/src/applications/phragment/storage/PhragmentFragment.php +++ b/src/applications/phragment/storage/PhragmentFragment.php @@ -256,7 +256,7 @@ final class PhragmentFragment extends PhragmentDAO $mappings[$path], array('name' => basename($path))); } - PhragmentFragment::createFromFile( + self::createFromFile( $viewer, $file, $base_path.'/'.$path, diff --git a/src/applications/policy/constants/PhabricatorPolicies.php b/src/applications/policy/constants/PhabricatorPolicies.php index 859010eca2..c33b9bf909 100644 --- a/src/applications/policy/constants/PhabricatorPolicies.php +++ b/src/applications/policy/constants/PhabricatorPolicies.php @@ -15,9 +15,9 @@ final class PhabricatorPolicies extends PhabricatorPolicyConstants { */ public static function getMostOpenPolicy() { if (PhabricatorEnv::getEnvConfig('policy.allow-public')) { - return PhabricatorPolicies::POLICY_PUBLIC; + return self::POLICY_PUBLIC; } else { - return PhabricatorPolicies::POLICY_USER; + return self::POLICY_USER; } } diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 031427d058..b9c366fecc 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -383,6 +383,7 @@ final class PhabricatorProjectBoardViewController array( 'title' => pht('%s Board', $project->getName()), 'showFooter' => false, + 'pageObjects' => array($project->getPHID()), )); } diff --git a/src/applications/project/controller/PhabricatorProjectEditPictureController.php b/src/applications/project/controller/PhabricatorProjectEditPictureController.php index 58e345930e..ca99159718 100644 --- a/src/applications/project/controller/PhabricatorProjectEditPictureController.php +++ b/src/applications/project/controller/PhabricatorProjectEditPictureController.php @@ -68,12 +68,9 @@ final class PhabricatorProjectEditPictureController 'This server only supports these image formats: %s.', implode(', ', $supported_formats)); } else { - $xformer = new PhabricatorImageTransformer(); - $xformed = $xformer->executeProfileTransform( - $file, - $width = 50, - $min_height = 50, - $max_height = 50); + $xform = PhabricatorFileTransform::getTransformByKey( + PhabricatorFileThumbnailTransform::TRANSFORM_PROFILE); + $xformed = $xform->executeTransform($file); } } diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php index 9679ddab5c..cb8fe9e87f 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileController.php @@ -67,6 +67,7 @@ final class PhabricatorProjectProfileController $nav, array( 'title' => $project->getName(), + 'pageObjects' => array($project->getPHID()), )); } diff --git a/src/applications/project/icon/PhabricatorProjectIcon.php b/src/applications/project/icon/PhabricatorProjectIcon.php index ae6efa9b6e..9411baa6a9 100644 --- a/src/applications/project/icon/PhabricatorProjectIcon.php +++ b/src/applications/project/icon/PhabricatorProjectIcon.php @@ -44,7 +44,7 @@ final class PhabricatorProjectIcon extends Phobject { } public static function renderIconForChooser($icon) { - $project_icons = PhabricatorProjectIcon::getIconMap(); + $project_icons = self::getIconMap(); return phutil_tag( 'span', diff --git a/src/applications/project/storage/PhabricatorProjectColumn.php b/src/applications/project/storage/PhabricatorProjectColumn.php index 8fcb6ade9c..63b8a8748f 100644 --- a/src/applications/project/storage/PhabricatorProjectColumn.php +++ b/src/applications/project/storage/PhabricatorProjectColumn.php @@ -111,7 +111,7 @@ final class PhabricatorProjectColumn ->setMetadata( array( 'tip' => $text, - ));; + )); } return null; diff --git a/src/applications/project/storage/PhabricatorProjectColumnTransaction.php b/src/applications/project/storage/PhabricatorProjectColumnTransaction.php index dd4f95d5f6..ed4bfed8a6 100644 --- a/src/applications/project/storage/PhabricatorProjectColumnTransaction.php +++ b/src/applications/project/storage/PhabricatorProjectColumnTransaction.php @@ -21,7 +21,7 @@ final class PhabricatorProjectColumnTransaction $author_handle = $this->renderHandleLink($this->getAuthorPHID()); switch ($this->getTransactionType()) { - case PhabricatorProjectColumnTransaction::TYPE_NAME: + case self::TYPE_NAME: if ($old === null) { return pht( '%s created this column.', @@ -44,7 +44,7 @@ final class PhabricatorProjectColumnTransaction $author_handle); } } - case PhabricatorProjectColumnTransaction::TYPE_LIMIT: + case self::TYPE_LIMIT: if (!$old) { return pht( '%s set the point limit for this column to %s.', @@ -62,7 +62,7 @@ final class PhabricatorProjectColumnTransaction $new); } - case PhabricatorProjectColumnTransaction::TYPE_STATUS: + case self::TYPE_STATUS: switch ($new) { case PhabricatorProjectColumn::STATUS_ACTIVE: return pht( diff --git a/src/applications/project/storage/PhabricatorProjectTransaction.php b/src/applications/project/storage/PhabricatorProjectTransaction.php index 9b0ff0c340..3dceb0934c 100644 --- a/src/applications/project/storage/PhabricatorProjectTransaction.php +++ b/src/applications/project/storage/PhabricatorProjectTransaction.php @@ -28,12 +28,12 @@ final class PhabricatorProjectTransaction $req_phids = array(); switch ($this->getTransactionType()) { - case PhabricatorProjectTransaction::TYPE_MEMBERS: + case self::TYPE_MEMBERS: $add = array_diff($new, $old); $rem = array_diff($old, $new); $req_phids = array_merge($add, $rem); break; - case PhabricatorProjectTransaction::TYPE_IMAGE: + case self::TYPE_IMAGE: $req_phids[] = $old; $req_phids[] = $new; break; @@ -48,7 +48,7 @@ final class PhabricatorProjectTransaction $new = $this->getNewValue(); switch ($this->getTransactionType()) { - case PhabricatorProjectTransaction::TYPE_STATUS: + case self::TYPE_STATUS: if ($old == 0) { return 'red'; } else { @@ -64,25 +64,25 @@ final class PhabricatorProjectTransaction $new = $this->getNewValue(); switch ($this->getTransactionType()) { - case PhabricatorProjectTransaction::TYPE_STATUS: + case self::TYPE_STATUS: if ($old == 0) { return 'fa-ban'; } else { return 'fa-check'; } - case PhabricatorProjectTransaction::TYPE_LOCKED: + case self::TYPE_LOCKED: if ($new) { return 'fa-lock'; } else { return 'fa-unlock'; } - case PhabricatorProjectTransaction::TYPE_ICON: + case self::TYPE_ICON: return $new; - case PhabricatorProjectTransaction::TYPE_IMAGE: + case self::TYPE_IMAGE: return 'fa-photo'; - case PhabricatorProjectTransaction::TYPE_MEMBERS: + case self::TYPE_MEMBERS: return 'fa-user'; - case PhabricatorProjectTransaction::TYPE_SLUGS: + case self::TYPE_SLUGS: return 'fa-tag'; } return parent::getIcon(); @@ -94,7 +94,7 @@ final class PhabricatorProjectTransaction $author_handle = $this->renderHandleLink($this->getAuthorPHID()); switch ($this->getTransactionType()) { - case PhabricatorProjectTransaction::TYPE_NAME: + case self::TYPE_NAME: if ($old === null) { return pht( '%s created this project.', @@ -106,7 +106,7 @@ final class PhabricatorProjectTransaction $old, $new); } - case PhabricatorProjectTransaction::TYPE_STATUS: + case self::TYPE_STATUS: if ($old == 0) { return pht( '%s archived this project.', @@ -116,7 +116,7 @@ final class PhabricatorProjectTransaction '%s activated this project.', $author_handle); } - case PhabricatorProjectTransaction::TYPE_IMAGE: + case self::TYPE_IMAGE: // TODO: Some day, it would be nice to show the images. if (!$old) { return pht( @@ -135,19 +135,19 @@ final class PhabricatorProjectTransaction $this->renderHandleLink($new)); } - case PhabricatorProjectTransaction::TYPE_ICON: + case self::TYPE_ICON: return pht( '%s set this project\'s icon to %s.', $author_handle, PhabricatorProjectIcon::getLabel($new)); - case PhabricatorProjectTransaction::TYPE_COLOR: + case self::TYPE_COLOR: return pht( '%s set this project\'s color to %s.', $author_handle, PHUITagView::getShadeName($new)); - case PhabricatorProjectTransaction::TYPE_LOCKED: + case self::TYPE_LOCKED: if ($new) { return pht( '%s locked this project\'s membership.', @@ -158,7 +158,7 @@ final class PhabricatorProjectTransaction $author_handle); } - case PhabricatorProjectTransaction::TYPE_SLUGS: + case self::TYPE_SLUGS: $add = array_diff($new, $old); $rem = array_diff($old, $new); @@ -184,7 +184,7 @@ final class PhabricatorProjectTransaction $this->renderSlugList($rem)); } - case PhabricatorProjectTransaction::TYPE_MEMBERS: + case self::TYPE_MEMBERS: $add = array_diff($new, $old); $rem = array_diff($old, $new); diff --git a/src/applications/releeph/storage/ReleephRequest.php b/src/applications/releeph/storage/ReleephRequest.php index 7f3f190a6d..7f2487107a 100644 --- a/src/applications/releeph/storage/ReleephRequest.php +++ b/src/applications/releeph/storage/ReleephRequest.php @@ -127,8 +127,8 @@ final class ReleephRequest extends ReleephDAO if ($this->getInBranch()) { return ReleephRequestStatus::STATUS_NEEDS_REVERT; } else { - $intent_pass = ReleephRequest::INTENT_PASS; - $intent_want = ReleephRequest::INTENT_WANT; + $intent_pass = self::INTENT_PASS; + $intent_want = self::INTENT_WANT; $has_been_in_branch = $this->getCommitIdentifier(); // Regardless of why we reverted something, always say reverted if it diff --git a/src/applications/releeph/storage/ReleephRequestTransaction.php b/src/applications/releeph/storage/ReleephRequestTransaction.php index bd17ad43c9..f4a4720c78 100644 --- a/src/applications/releeph/storage/ReleephRequestTransaction.php +++ b/src/applications/releeph/storage/ReleephRequestTransaction.php @@ -38,12 +38,12 @@ final class ReleephRequestTransaction $new = $this->getNewValue(); switch ($this->getTransactionType()) { - case ReleephRequestTransaction::TYPE_REQUEST: - case ReleephRequestTransaction::TYPE_DISCOVERY: + case self::TYPE_REQUEST: + case self::TYPE_DISCOVERY: $phids[] = $new; break; - case ReleephRequestTransaction::TYPE_EDIT_FIELD: + case self::TYPE_EDIT_FIELD: self::searchForPHIDs($this->getOldValue(), $phids); self::searchForPHIDs($this->getNewValue(), $phids); break; @@ -60,18 +60,18 @@ final class ReleephRequestTransaction $new = $this->getNewValue(); switch ($this->getTransactionType()) { - case ReleephRequestTransaction::TYPE_REQUEST: + case self::TYPE_REQUEST: return pht( '%s requested %s', $this->renderHandleLink($author_phid), $this->renderHandleLink($new)); break; - case ReleephRequestTransaction::TYPE_USER_INTENT: + case self::TYPE_USER_INTENT: return $this->getIntentTitle(); break; - case ReleephRequestTransaction::TYPE_EDIT_FIELD: + case self::TYPE_EDIT_FIELD: $field = newv($this->getMetadataValue('fieldClass'), array()); $name = $field->getName(); @@ -89,7 +89,7 @@ final class ReleephRequestTransaction $field->normalizeForTransactionView($this, $new)); break; - case ReleephRequestTransaction::TYPE_PICK_STATUS: + case self::TYPE_PICK_STATUS: switch ($new) { case ReleephRequest::PICK_OK: return pht('%s found this request picks without error', @@ -109,7 +109,7 @@ final class ReleephRequestTransaction } break; - case ReleephRequestTransaction::TYPE_COMMIT: + case self::TYPE_COMMIT: $action_type = $this->getMetadataValue('action'); switch ($action_type) { case 'pick': @@ -126,7 +126,7 @@ final class ReleephRequestTransaction } break; - case ReleephRequestTransaction::TYPE_MANUAL_IN_BRANCH: + case self::TYPE_MANUAL_IN_BRANCH: $action = $new ? pht('picked') : pht('reverted'); return pht( '%s marked this request as manually %s', @@ -134,7 +134,7 @@ final class ReleephRequestTransaction $action); break; - case ReleephRequestTransaction::TYPE_DISCOVERY: + case self::TYPE_DISCOVERY: return pht('%s discovered this commit as %s', $this->renderHandleLink($author_phid), $this->renderHandleLink($new)); @@ -173,7 +173,7 @@ final class ReleephRequestTransaction $new = $this->getNewValue(); switch ($this->getTransactionType()) { - case ReleephRequestTransaction::TYPE_USER_INTENT: + case self::TYPE_USER_INTENT: switch ($new) { case ReleephRequest::INTENT_WANT: return PhabricatorTransactions::COLOR_GREEN; @@ -243,7 +243,7 @@ final class ReleephRequestTransaction public function shouldHide() { $type = $this->getTransactionType(); - if ($type === ReleephRequestTransaction::TYPE_USER_INTENT && + if ($type === self::TYPE_USER_INTENT && $this->getMetadataValue('isRQCreate')) { return true; @@ -255,7 +255,7 @@ final class ReleephRequestTransaction // ReleephSummaryFieldSpecification is usually blank when an RQ is created, // creating a transaction change from null to "". Hide these! - if ($type === ReleephRequestTransaction::TYPE_EDIT_FIELD) { + if ($type === self::TYPE_EDIT_FIELD) { if ($this->getOldValue() === null && $this->getNewValue() === '') { return true; } @@ -265,7 +265,7 @@ final class ReleephRequestTransaction public function isBoringPickStatus() { $type = $this->getTransactionType(); - if ($type === ReleephRequestTransaction::TYPE_PICK_STATUS) { + if ($type === self::TYPE_PICK_STATUS) { $new = $this->getNewValue(); if ($new === ReleephRequest::PICK_OK || $new === ReleephRequest::REVERT_OK) { diff --git a/src/applications/repository/query/PhabricatorRepositorySearchEngine.php b/src/applications/repository/query/PhabricatorRepositorySearchEngine.php index 3131f9b9e6..21c7fe38ab 100644 --- a/src/applications/repository/query/PhabricatorRepositorySearchEngine.php +++ b/src/applications/repository/query/PhabricatorRepositorySearchEngine.php @@ -201,7 +201,7 @@ final class PhabricatorRepositorySearchEngine array $handles) { assert_instances_of($repositories, 'PhabricatorRepository'); - $viewer = $this->requireViewer();; + $viewer = $this->requireViewer(); $list = new PHUIObjectItemListView(); foreach ($repositories as $repository) { diff --git a/src/applications/repository/storage/PhabricatorRepositoryPushLog.php b/src/applications/repository/storage/PhabricatorRepositoryPushLog.php index d5fb87fa8d..f16784bbf1 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryPushLog.php +++ b/src/applications/repository/storage/PhabricatorRepositoryPushLog.php @@ -54,13 +54,13 @@ final class PhabricatorRepositoryPushLog public static function getHeraldChangeFlagConditionOptions() { return array( - PhabricatorRepositoryPushLog::CHANGEFLAG_ADD => + self::CHANGEFLAG_ADD => pht('change creates ref'), - PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE => + self::CHANGEFLAG_DELETE => pht('change deletes ref'), - PhabricatorRepositoryPushLog::CHANGEFLAG_REWRITE => + self::CHANGEFLAG_REWRITE => pht('change rewrites ref'), - PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS => + self::CHANGEFLAG_DANGEROUS => pht('dangerous change'), ); } diff --git a/src/applications/search/controller/PhabricatorApplicationSearchController.php b/src/applications/search/controller/PhabricatorApplicationSearchController.php index d3ba1cf9ae..ac67585268 100644 --- a/src/applications/search/controller/PhabricatorApplicationSearchController.php +++ b/src/applications/search/controller/PhabricatorApplicationSearchController.php @@ -55,18 +55,12 @@ final class PhabricatorApplicationSearchController $engine = $this->getSearchEngine(); if (!$engine) { - throw new Exception( - pht( - 'Call %s before delegating to this controller!', - 'setEngine()')); + throw new PhutilInvalidStateException('setEngine'); } $nav = $this->getNavigation(); if (!$nav) { - throw new Exception( - pht( - 'Call %s before delegating to this controller!', - 'setNavigation()')); + throw new PhutilInvalidStateException('setNavigation'); } $engine->setViewer($this->getRequest()->getUser()); diff --git a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php index a0e7245a76..cd0a9655fa 100644 --- a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php +++ b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php @@ -33,7 +33,7 @@ abstract class PhabricatorApplicationSearchEngine { protected function requireViewer() { if (!$this->viewer) { - throw new Exception('Call setViewer() before using an engine!'); + throw new PhutilInvalidStateException('setViewer'); } return $this->viewer; } diff --git a/src/applications/settings/storage/PhabricatorUserPreferences.php b/src/applications/settings/storage/PhabricatorUserPreferences.php index e07a045dc2..3248dc8b2a 100644 --- a/src/applications/settings/storage/PhabricatorUserPreferences.php +++ b/src/applications/settings/storage/PhabricatorUserPreferences.php @@ -74,14 +74,14 @@ final class PhabricatorUserPreferences extends PhabricatorUserDAO { } public function getPinnedApplications(array $apps, PhabricatorUser $viewer) { - $pref_pinned = PhabricatorUserPreferences::PREFERENCE_APP_PINNED; + $pref_pinned = self::PREFERENCE_APP_PINNED; $pinned = $this->getPreference($pref_pinned); if ($pinned) { return $pinned; } - $pref_tiles = PhabricatorUserPreferences::PREFERENCE_APP_TILES; + $pref_tiles = self::PREFERENCE_APP_TILES; $tiles = $this->getPreference($pref_tiles, array()); $full_tile = 'full'; diff --git a/src/applications/slowvote/storage/PhabricatorSlowvoteTransaction.php b/src/applications/slowvote/storage/PhabricatorSlowvoteTransaction.php index abb73fd588..9abf1f5801 100644 --- a/src/applications/slowvote/storage/PhabricatorSlowvoteTransaction.php +++ b/src/applications/slowvote/storage/PhabricatorSlowvoteTransaction.php @@ -26,10 +26,10 @@ final class PhabricatorSlowvoteTransaction $new = $this->getNewValue(); switch ($this->getTransactionType()) { - case PhabricatorSlowvoteTransaction::TYPE_DESCRIPTION: - case PhabricatorSlowvoteTransaction::TYPE_RESPONSES: - case PhabricatorSlowvoteTransaction::TYPE_SHUFFLE: - case PhabricatorSlowvoteTransaction::TYPE_CLOSE: + case self::TYPE_DESCRIPTION: + case self::TYPE_RESPONSES: + case self::TYPE_SHUFFLE: + case self::TYPE_CLOSE: return ($old === null); } @@ -43,7 +43,7 @@ final class PhabricatorSlowvoteTransaction $new = $this->getNewValue(); switch ($this->getTransactionType()) { - case PhabricatorSlowvoteTransaction::TYPE_QUESTION: + case self::TYPE_QUESTION: if ($old === null) { return pht( '%s created this poll.', @@ -56,16 +56,16 @@ final class PhabricatorSlowvoteTransaction $new); } break; - case PhabricatorSlowvoteTransaction::TYPE_DESCRIPTION: + case self::TYPE_DESCRIPTION: return pht( '%s updated the description for this poll.', $this->renderHandleLink($author_phid)); - case PhabricatorSlowvoteTransaction::TYPE_RESPONSES: + case self::TYPE_RESPONSES: // TODO: This could be more detailed return pht( '%s changed who can see the responses.', $this->renderHandleLink($author_phid)); - case PhabricatorSlowvoteTransaction::TYPE_SHUFFLE: + case self::TYPE_SHUFFLE: if ($new) { return pht( '%s made poll responses appear in a random order.', @@ -76,7 +76,7 @@ final class PhabricatorSlowvoteTransaction $this->renderHandleLink($author_phid)); } break; - case PhabricatorSlowvoteTransaction::TYPE_CLOSE: + case self::TYPE_CLOSE: if ($new) { return pht( '%s closed this poll.', @@ -98,18 +98,18 @@ final class PhabricatorSlowvoteTransaction $new = $this->getNewValue(); switch ($this->getTransactionType()) { - case PhabricatorSlowvoteTransaction::TYPE_QUESTION: + case self::TYPE_QUESTION: if ($old === null) { return 'fa-plus'; } else { return 'fa-pencil'; } - case PhabricatorSlowvoteTransaction::TYPE_DESCRIPTION: - case PhabricatorSlowvoteTransaction::TYPE_RESPONSES: + case self::TYPE_DESCRIPTION: + case self::TYPE_RESPONSES: return 'fa-pencil'; - case PhabricatorSlowvoteTransaction::TYPE_SHUFFLE: + case self::TYPE_SHUFFLE: return 'fa-refresh'; - case PhabricatorSlowvoteTransaction::TYPE_CLOSE: + case self::TYPE_CLOSE: if ($new) { return 'fa-ban'; } else { @@ -126,11 +126,11 @@ final class PhabricatorSlowvoteTransaction $new = $this->getNewValue(); switch ($this->getTransactionType()) { - case PhabricatorSlowvoteTransaction::TYPE_QUESTION: - case PhabricatorSlowvoteTransaction::TYPE_DESCRIPTION: - case PhabricatorSlowvoteTransaction::TYPE_RESPONSES: - case PhabricatorSlowvoteTransaction::TYPE_SHUFFLE: - case PhabricatorSlowvoteTransaction::TYPE_CLOSE: + case self::TYPE_QUESTION: + case self::TYPE_DESCRIPTION: + case self::TYPE_RESPONSES: + case self::TYPE_SHUFFLE: + case self::TYPE_CLOSE: return PhabricatorTransactions::COLOR_BLUE; } @@ -139,7 +139,7 @@ final class PhabricatorSlowvoteTransaction public function hasChangeDetails() { switch ($this->getTransactionType()) { - case PhabricatorSlowvoteTransaction::TYPE_DESCRIPTION: + case self::TYPE_DESCRIPTION: return true; } return parent::hasChangeDetails(); diff --git a/src/applications/slowvote/view/SlowvoteEmbedView.php b/src/applications/slowvote/view/SlowvoteEmbedView.php index 79e4c0bb1d..4ff920bb7d 100644 --- a/src/applications/slowvote/view/SlowvoteEmbedView.php +++ b/src/applications/slowvote/view/SlowvoteEmbedView.php @@ -22,7 +22,7 @@ final class SlowvoteEmbedView extends AphrontView { public function render() { if (!$this->poll) { - throw new Exception('Call setPoll() before render()!'); + throw new PhutilInvalidStateException('setPoll'); } $poll = $this->poll; diff --git a/src/applications/subscriptions/editor/PhabricatorSubscriptionsEditor.php b/src/applications/subscriptions/editor/PhabricatorSubscriptionsEditor.php index 498b01a04f..3ee4b5f6e8 100644 --- a/src/applications/subscriptions/editor/PhabricatorSubscriptionsEditor.php +++ b/src/applications/subscriptions/editor/PhabricatorSubscriptionsEditor.php @@ -56,7 +56,7 @@ final class PhabricatorSubscriptionsEditor extends PhabricatorEditor { public function save() { if (!$this->object) { - throw new Exception('Call setObject() before save()!'); + throw new PhutilInvalidStateException('setObject'); } $actor = $this->requireActor(); diff --git a/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php b/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php index 380c8380ce..fb7c32892f 100644 --- a/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php +++ b/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php @@ -21,6 +21,7 @@ final class PhabricatorSubscriptionsUIEventListener private function handleActionEvent($event) { $user = $event->getUser(); + $user_phid = $user->getPHID(); $object = $event->getValue('object'); if (!$object || !$object->getPHID()) { @@ -33,12 +34,12 @@ final class PhabricatorSubscriptionsUIEventListener return; } - if (!$object->shouldAllowSubscription($user->getPHID())) { + if (!$object->shouldAllowSubscription($user_phid)) { // This object doesn't allow the viewer to subscribe. return; } - if ($object->isAutomaticallySubscribed($user->getPHID())) { + if ($user_phid && $object->isAutomaticallySubscribed($user_phid)) { $sub_action = id(new PhabricatorActionView()) ->setWorkflow(true) ->setDisabled(true) @@ -50,15 +51,14 @@ final class PhabricatorSubscriptionsUIEventListener $subscribed = false; if ($user->isLoggedIn()) { $src_phid = $object->getPHID(); - $dst_phid = $user->getPHID(); $edge_type = PhabricatorObjectHasSubscriberEdgeType::EDGECONST; $edges = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array($src_phid)) ->withEdgeTypes(array($edge_type)) - ->withDestinationPHIDs(array($user->getPHID())) + ->withDestinationPHIDs(array($user_phid)) ->execute(); - $subscribed = isset($edges[$src_phid][$edge_type][$dst_phid]); + $subscribed = isset($edges[$src_phid][$edge_type][$user_phid]); } if ($subscribed) { diff --git a/src/applications/system/engine/PhabricatorDestructionEngine.php b/src/applications/system/engine/PhabricatorDestructionEngine.php index e1f1045132..b55b336052 100644 --- a/src/applications/system/engine/PhabricatorDestructionEngine.php +++ b/src/applications/system/engine/PhabricatorDestructionEngine.php @@ -46,6 +46,9 @@ final class PhabricatorDestructionEngine extends Phobject { $template = $object->getApplicationTransactionTemplate(); $this->destroyTransactions($template, $object_phid); } + + $this->destroyWorkerTasks($object_phid); + $this->destroyNotifications($object_phid); } // Nuke any Herald transcripts of the object, because they may contain @@ -94,7 +97,28 @@ final class PhabricatorDestructionEngine extends Phobject { foreach ($xactions as $xaction) { $this->destroyObject($xaction); } + } + private function destroyWorkerTasks($object_phid) { + $tasks = id(new PhabricatorWorkerActiveTask())->loadAllWhere( + 'objectPHID = %s', + $object_phid); + + foreach ($tasks as $task) { + $task->archiveTask( + PhabricatorWorkerArchiveTask::RESULT_CANCELLED, + 0); + } + } + + private function destroyNotifications($object_phid) { + $notifications = id(new PhabricatorFeedStoryNotification())->loadAllWhere( + 'primaryObjectPHID = %s', + $object_phid); + + foreach ($notifications as $notification) { + $notification->delete(); + } } } diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php index ee09a76eda..23b77fbd1b 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php @@ -133,8 +133,7 @@ final class PhabricatorApplicationTransactionCommentEditor } if (!$this->getContentSource()) { - throw new Exception( - 'Call setContentSource() before applyEdit()!'); + throw new PhutilInvalidStateException('applyEdit'); } $actor = $this->requireActor(); diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index c28fb135ee..dfd4bba286 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -569,6 +569,11 @@ abstract class PhabricatorApplicationTransactionEditor return $xaction; } + protected function didApplyInternalEffects( + PhabricatorLiskDAO $object, + array $xactions) { + return $xactions; + } protected function applyFinalEffects( PhabricatorLiskDAO $object, @@ -731,6 +736,8 @@ abstract class PhabricatorApplicationTransactionEditor $this->applyInternalEffects($object, $xaction); } + $xactions = $this->didApplyInternalEffects($object, $xactions); + $object->save(); foreach ($xactions as $xaction) { @@ -963,8 +970,7 @@ abstract class PhabricatorApplicationTransactionEditor array $xactions) { if (!$this->getContentSource()) { - throw new Exception( - 'Call setContentSource() before applyTransactions()!'); + throw new PhutilInvalidStateException('setContentSource'); } // Do a bunch of sanity checks that the incoming transactions are fresh. diff --git a/src/applications/transactions/query/PhabricatorApplicationTransactionQuery.php b/src/applications/transactions/query/PhabricatorApplicationTransactionQuery.php index ac6f713eeb..4c1e3ccd17 100644 --- a/src/applications/transactions/query/PhabricatorApplicationTransactionQuery.php +++ b/src/applications/transactions/query/PhabricatorApplicationTransactionQuery.php @@ -126,10 +126,8 @@ abstract class PhabricatorApplicationTransactionQuery $handles = array(); $merged = array_mergev($phids); if ($merged) { - $handles = id(new PhabricatorHandleQuery()) - ->setViewer($this->getViewer()) - ->withPHIDs($merged) - ->execute(); + $handles = $this->getViewer()->loadHandles($merged); + $handles = iterator_to_array($handles); } foreach ($xactions as $xaction) { $xaction->setHandles( diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index c6755ccfcc..355f15c2a7 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -1073,7 +1073,7 @@ abstract class PhabricatorApplicationTransaction } public function attachTransactionGroup(array $group) { - assert_instances_of($group, 'PhabricatorApplicationTransaction'); + assert_instances_of($group, __CLASS__); $this->transactionGroup = $group; return $this; } @@ -1165,7 +1165,7 @@ abstract class PhabricatorApplicationTransaction } $old_target = $xaction->getRenderingTarget(); - $new_target = PhabricatorApplicationTransaction::TARGET_TEXT; + $new_target = self::TARGET_TEXT; $xaction->setRenderingTarget($new_target); if ($publisher->getRenderWithImpliedContext()) { diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php index 77cc6a6371..311c7e8b9f 100644 --- a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php +++ b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php @@ -149,7 +149,7 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView { } if (!$this->getObjectPHID()) { - throw new Exception('Call setObjectPHID() before render()!'); + throw new PhutilInvalidStateException('setObjectPHID', 'render'); } return id(new AphrontFormView()) diff --git a/src/infrastructure/customfield/field/PhabricatorCustomField.php b/src/infrastructure/customfield/field/PhabricatorCustomField.php index ec5a02337e..28110e7334 100644 --- a/src/infrastructure/customfield/field/PhabricatorCustomField.php +++ b/src/infrastructure/customfield/field/PhabricatorCustomField.php @@ -65,7 +65,7 @@ abstract class PhabricatorCustomField { "object of class '{$obj_class}'."); } - $fields = PhabricatorCustomField::buildFieldList( + $fields = self::buildFieldList( $base_class, $spec, $object); diff --git a/src/infrastructure/daemon/bot/PhabricatorBot.php b/src/infrastructure/daemon/bot/PhabricatorBot.php index 12ab9e663a..ef949cb9ff 100644 --- a/src/infrastructure/daemon/bot/PhabricatorBot.php +++ b/src/infrastructure/daemon/bot/PhabricatorBot.php @@ -19,7 +19,11 @@ final class PhabricatorBot extends PhabricatorDaemon { protected function run() { $argv = $this->getArgv(); if (count($argv) !== 1) { - throw new Exception('usage: PhabricatorBot '); + throw new Exception( + pht( + 'Usage: %s %s', + __CLASS__, + '')); } $json_raw = Filesystem::readFile($argv[0]); @@ -53,8 +57,7 @@ final class PhabricatorBot extends PhabricatorDaemon { $conduit_uri = idx($config, 'conduit.uri'); if ($conduit_uri) { - $conduit_user = idx($config, 'conduit.user'); - $conduit_cert = idx($config, 'conduit.cert'); + $conduit_token = idx($config, 'conduit.token'); // Normalize the path component of the URI so users can enter the // domain without the "/api/" part. @@ -64,16 +67,23 @@ final class PhabricatorBot extends PhabricatorDaemon { $conduit_uri = (string)$conduit_uri->setPath('/api/'); $conduit = new ConduitClient($conduit_uri); - $response = $conduit->callMethodSynchronous( - 'conduit.connect', - array( - 'client' => 'PhabricatorBot', - 'clientVersion' => '1.0', - 'clientDescription' => php_uname('n').':'.$nick, - 'host' => $conduit_host, - 'user' => $conduit_user, - 'certificate' => $conduit_cert, - )); + if ($conduit_token) { + $conduit->setConduitToken($conduit_token); + } else { + $conduit_user = idx($config, 'conduit.user'); + $conduit_cert = idx($config, 'conduit.cert'); + + $response = $conduit->callMethodSynchronous( + 'conduit.connect', + array( + 'client' => __CLASS__, + 'clientVersion' => '1.0', + 'clientDescription' => php_uname('n').':'.$nick, + 'host' => $conduit_host, + 'user' => $conduit_user, + 'certificate' => $conduit_cert, + )); + } $this->conduit = $conduit; } diff --git a/src/infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php index cffa6faf35..f226d0ad76 100644 --- a/src/infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php +++ b/src/infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php @@ -26,7 +26,7 @@ final class PhabricatorBotObjectNameHandler extends PhabricatorBotHandler { $pattern = '@'. - '(?getConduit()->callMethodSynchronous( - 'diffusion.getcommits', + 'diffusion.querycommits', array( - 'commits' => $commit_names, + 'names' => $commit_names, )); - foreach ($commits as $commit) { - if (isset($commit['error'])) { - continue; - } - $output[$commit['commitPHID']] = $commit['uri']; + foreach ($commits['data'] as $commit) { + $output[$commit['phid']] = $commit['uri']; } } diff --git a/src/infrastructure/daemon/workers/query/PhabricatorWorkerTriggerQuery.php b/src/infrastructure/daemon/workers/query/PhabricatorWorkerTriggerQuery.php index 34cab99afa..c569f0c695 100644 --- a/src/infrastructure/daemon/workers/query/PhabricatorWorkerTriggerQuery.php +++ b/src/infrastructure/daemon/workers/query/PhabricatorWorkerTriggerQuery.php @@ -150,7 +150,7 @@ final class PhabricatorWorkerTriggerQuery if (($this->nextEpochMin !== null) || ($this->nextEpochMax !== null) || - ($this->order == PhabricatorWorkerTriggerQuery::ORDER_EXECUTION)) { + ($this->order == self::ORDER_EXECUTION)) { $joins[] = qsprintf( $conn_r, 'JOIN %T e ON e.triggerID = t.id', diff --git a/src/infrastructure/diff/PhabricatorInlineCommentController.php b/src/infrastructure/diff/PhabricatorInlineCommentController.php index bc4358ba55..4608ebeffa 100644 --- a/src/infrastructure/diff/PhabricatorInlineCommentController.php +++ b/src/infrastructure/diff/PhabricatorInlineCommentController.php @@ -266,6 +266,8 @@ abstract class PhabricatorInlineCommentController // comment appears on. This is expected in the case of ghost comments. // We currently put the new comment on the visible changeset, not the // original comment's changeset. + + $this->isNewFile = $reply_comment->getIsNewFile(); } } diff --git a/src/infrastructure/diff/view/PHUIDiffInlineCommentEditView.php b/src/infrastructure/diff/view/PHUIDiffInlineCommentEditView.php index 6634c397f9..3a9bc616d9 100644 --- a/src/infrastructure/diff/view/PHUIDiffInlineCommentEditView.php +++ b/src/infrastructure/diff/view/PHUIDiffInlineCommentEditView.php @@ -76,10 +76,10 @@ final class PHUIDiffInlineCommentEditView public function render() { if (!$this->uri) { - throw new Exception('Call setSubmitURI() before render()!'); + throw new PhutilInvalidStateException('setSubmitURI'); } if (!$this->user) { - throw new Exception('Call setUser() before render()!'); + throw new PhutilInvalidStateException('setUser'); } $content = phabricator_form( diff --git a/src/infrastructure/env/PhabricatorEnv.php b/src/infrastructure/env/PhabricatorEnv.php index 05ab7c677d..39f911e3c5 100644 --- a/src/infrastructure/env/PhabricatorEnv.php +++ b/src/infrastructure/env/PhabricatorEnv.php @@ -88,7 +88,7 @@ final class PhabricatorEnv { // Force a valid timezone. If both PHP and Phabricator configuration are // invalid, use UTC. - $tz = PhabricatorEnv::getEnvConfig('phabricator.timezone'); + $tz = self::getEnvConfig('phabricator.timezone'); if ($tz) { @date_default_timezone_set($tz); } @@ -102,7 +102,7 @@ final class PhabricatorEnv { $phabricator_path = dirname(phutil_get_library_root('phabricator')); $support_path = $phabricator_path.'/support/bin'; $env_path = $support_path.PATH_SEPARATOR.$env_path; - $append_dirs = PhabricatorEnv::getEnvConfig('environment.append-paths'); + $append_dirs = self::getEnvConfig('environment.append-paths'); if (!empty($append_dirs)) { $append_path = implode(PATH_SEPARATOR, $append_dirs); $env_path = $env_path.PATH_SEPARATOR.$append_path; @@ -116,7 +116,7 @@ final class PhabricatorEnv { // If an instance identifier is defined, write it into the environment so // it's available to subprocesses. - $instance = PhabricatorEnv::getEnvConfig('cluster.instance'); + $instance = self::getEnvConfig('cluster.instance'); if (strlen($instance)) { putenv('PHABRICATOR_INSTANCE='.$instance); $_ENV['PHABRICATOR_INSTANCE'] = $instance; @@ -139,7 +139,7 @@ final class PhabricatorEnv { $translations = PhutilTranslation::getTranslationMapForLocale( $locale_code); - $override = PhabricatorEnv::getEnvConfig('translation.override'); + $override = self::getEnvConfig('translation.override'); if (!is_array($override)) { $override = array(); } @@ -178,7 +178,7 @@ final class PhabricatorEnv { // If the install overrides the database adapter, we might need to load // the database adapter class before we can push on the database config. // This config is locked and can't be edited from the web UI anyway. - foreach (PhabricatorEnv::getEnvConfig('load-libraries') as $library) { + foreach (self::getEnvConfig('load-libraries') as $library) { phutil_load_library($library); } @@ -809,7 +809,7 @@ final class PhabricatorEnv { } public static function isClusterAddress($address) { - $cluster_addresses = PhabricatorEnv::getEnvConfig('cluster.addresses'); + $cluster_addresses = self::getEnvConfig('cluster.addresses'); if (!$cluster_addresses) { throw new Exception( pht( diff --git a/src/infrastructure/events/PhabricatorExampleEventListener.php b/src/infrastructure/events/PhabricatorExampleEventListener.php index 0a7bd699df..fb85678da1 100644 --- a/src/infrastructure/events/PhabricatorExampleEventListener.php +++ b/src/infrastructure/events/PhabricatorExampleEventListener.php @@ -21,8 +21,11 @@ final class PhabricatorExampleEventListener extends PhabricatorEventListener { $console = PhutilConsole::getConsole(); $console->writeOut( - "PhabricatorExampleEventListener got test event at %d\n", - $event->getValue('time')); + "%s\n", + pht( + '% got test event at %d', + __CLASS__, + $event->getValue('time'))); } } diff --git a/src/infrastructure/markup/PhabricatorMarkupEngine.php b/src/infrastructure/markup/PhabricatorMarkupEngine.php index 491940d23f..c29dddd821 100644 --- a/src/infrastructure/markup/PhabricatorMarkupEngine.php +++ b/src/infrastructure/markup/PhabricatorMarkupEngine.php @@ -42,7 +42,7 @@ final class PhabricatorMarkupEngine { private $objects = array(); private $viewer; private $contextObject; - private $version = 14; + private $version = 15; /* -( Markup Pipeline )---------------------------------------------------- */ @@ -337,6 +337,7 @@ final class PhabricatorMarkupEngine { public static function newPhameMarkupEngine() { return self::newMarkupEngine(array( 'macros' => false, + 'uri.full' => true, )); } @@ -349,7 +350,6 @@ final class PhabricatorMarkupEngine { array( 'macros' => false, 'youtube' => false, - )); } @@ -437,6 +437,7 @@ final class PhabricatorMarkupEngine { 'macros' => true, 'uri.allowed-protocols' => PhabricatorEnv::getEnvConfig( 'uri.allowed-protocols'), + 'uri.full' => false, 'syntax-highlighter.engine' => PhabricatorEnv::getEnvConfig( 'syntax-highlighter.engine'), 'preserve-linebreaks' => true, @@ -464,6 +465,8 @@ final class PhabricatorMarkupEngine { 'syntax-highlighter.engine', $options['syntax-highlighter.engine']); + $engine->setConfig('uri.full', $options['uri.full']); + $rules = array(); $rules[] = new PhutilRemarkupEscapeRemarkupRule(); $rules[] = new PhutilRemarkupMonospaceRule(); @@ -586,7 +589,7 @@ final class PhabricatorMarkupEngine { // - Hopefully don't return too much text. We don't explicitly limit // this right now. - $blocks = preg_split("/\n *\n\s*/", trim($corpus)); + $blocks = preg_split("/\n *\n\s*/", $corpus); $best = null; foreach ($blocks as $block) { diff --git a/src/infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php b/src/infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php index 0b4471d811..8f776716ea 100644 --- a/src/infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php +++ b/src/infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php @@ -28,10 +28,9 @@ abstract class PhabricatorObjectRemarkupRule extends PhutilRemarkupRule { protected function loadHandles(array $objects) { $phids = mpull($objects, 'getPHID'); - $handles = id(new PhabricatorHandleQuery($phids)) - ->withPHIDs($phids) - ->setViewer($this->getEngine()->getConfig('viewer')) - ->execute(); + $viewer = $this->getEngine()->getConfig('viewer'); + $handles = $viewer->loadHandles($phids); + $handles = iterator_to_array($handles); $result = array(); foreach ($objects as $id => $object) { @@ -45,7 +44,13 @@ abstract class PhabricatorObjectRemarkupRule extends PhutilRemarkupRule { PhabricatorObjectHandle $handle, $id) { - return $handle->getURI(); + $uri = $handle->getURI(); + + if ($this->getEngine()->getConfig('uri.full')) { + $uri = PhabricatorEnv::getURI($uri); + } + + return $uri; } protected function renderObjectRefForAnyMedia ( diff --git a/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php index 1f353d7dfb..e1b2028a19 100644 --- a/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php +++ b/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php @@ -33,6 +33,7 @@ abstract class PhabricatorPolicyAwareQuery extends PhabricatorOffsetPagedQuery { private $rawResultLimit; private $capabilities; private $workspace = array(); + private $inFlightPHIDs = array(); private $policyFilteredPHIDs = array(); private $canUseApplication; @@ -468,6 +469,39 @@ abstract class PhabricatorPolicyAwareQuery extends PhabricatorOffsetPagedQuery { } + /** + * Mark PHIDs as in flight. + * + * PHIDs which are "in flight" are actively being queried for. Using this + * list can prevent infinite query loops by aborting queries which cycle. + * + * @param list List of PHIDs which are now in flight. + * @return this + */ + public function putPHIDsInFlight(array $phids) { + foreach ($phids as $phid) { + $this->inFlightPHIDs[$phid] = $phid; + } + return $this; + } + + + /** + * Get PHIDs which are currently in flight. + * + * PHIDs which are "in flight" are actively being queried for. + * + * @return map PHIDs currently in flight. + */ + public function getPHIDsInFlight() { + $results = $this->inFlightPHIDs; + if ($this->getParentQuery()) { + $results += $this->getParentQuery()->getPHIDsInFlight(); + } + return $results; + } + + /* -( Policy Query Implementation )---------------------------------------- */ diff --git a/src/infrastructure/sms/storage/PhabricatorSMS.php b/src/infrastructure/sms/storage/PhabricatorSMS.php index c17d8f16da..7b6c85f159 100644 --- a/src/infrastructure/sms/storage/PhabricatorSMS.php +++ b/src/infrastructure/sms/storage/PhabricatorSMS.php @@ -38,8 +38,8 @@ final class PhabricatorSMS // and ProviderSMSID are totally garbage data before a send it attempted. return id(new PhabricatorSMS()) ->setBody($body) - ->setSendStatus(PhabricatorSMS::STATUS_UNSENT) - ->setProviderShortName(PhabricatorSMS::SHORTNAME_PLACEHOLDER) + ->setSendStatus(self::STATUS_UNSENT) + ->setProviderShortName(self::SHORTNAME_PLACEHOLDER) ->setProviderSMSID(Filesystem::readRandomCharacters(40)); } @@ -70,6 +70,6 @@ final class PhabricatorSMS public function hasBeenSentAtLeastOnce() { return ($this->getProviderShortName() != - PhabricatorSMS::SHORTNAME_PLACEHOLDER); + self::SHORTNAME_PLACEHOLDER); } } diff --git a/src/infrastructure/storage/patch/PhabricatorSQLPatchList.php b/src/infrastructure/storage/patch/PhabricatorSQLPatchList.php index 0eefe68069..390d591a07 100644 --- a/src/infrastructure/storage/patch/PhabricatorSQLPatchList.php +++ b/src/infrastructure/storage/patch/PhabricatorSQLPatchList.php @@ -38,7 +38,7 @@ abstract class PhabricatorSQLPatchList { final public static function buildAllPatches() { $patch_lists = id(new PhutilSymbolLoader()) - ->setAncestorClass('PhabricatorSQLPatchList') + ->setAncestorClass(__CLASS__) ->setConcreteOnly(true) ->selectAndLoadSymbols(); diff --git a/src/infrastructure/time/PhabricatorTime.php b/src/infrastructure/time/PhabricatorTime.php index d2866d99c2..03495d8412 100644 --- a/src/infrastructure/time/PhabricatorTime.php +++ b/src/infrastructure/time/PhabricatorTime.php @@ -25,7 +25,10 @@ final class PhabricatorTime { public static function popTime($key) { if ($key !== last_key(self::$stack)) { - throw new Exception('PhabricatorTime::popTime with bad key.'); + throw new Exception( + pht( + '%s with bad key.', + __METHOD__)); } array_pop(self::$stack); @@ -49,7 +52,7 @@ final class PhabricatorTime { $old_zone = date_default_timezone_get(); date_default_timezone_set($user->getTimezoneIdentifier()); - $timestamp = (int)strtotime($time, PhabricatorTime::getNow()); + $timestamp = (int)strtotime($time, self::getNow()); if ($timestamp <= 0) { $timestamp = null; } diff --git a/src/infrastructure/util/PhabricatorHash.php b/src/infrastructure/util/PhabricatorHash.php index dd158d1168..df1fbfa08b 100644 --- a/src/infrastructure/util/PhabricatorHash.php +++ b/src/infrastructure/util/PhabricatorHash.php @@ -35,7 +35,7 @@ final class PhabricatorHash extends Phobject { } for ($ii = 0; $ii < 1000; $ii++) { - $result = PhabricatorHash::digest($result, $salt); + $result = self::digest($result, $salt); } return $result; @@ -113,7 +113,7 @@ final class PhabricatorHash extends Phobject { // who can control the inputs from intentionally using the hashed form // of a string to cause a collision. - $hash = PhabricatorHash::digestForIndex($string); + $hash = self::digestForIndex($string); $prefix = substr($string, 0, ($length - ($min_length - 1))); diff --git a/src/infrastructure/util/password/PhabricatorPasswordHasher.php b/src/infrastructure/util/password/PhabricatorPasswordHasher.php index 4409163509..3b136b4ec3 100644 --- a/src/infrastructure/util/password/PhabricatorPasswordHasher.php +++ b/src/infrastructure/util/password/PhabricatorPasswordHasher.php @@ -213,7 +213,7 @@ abstract class PhabricatorPasswordHasher extends Phobject { */ public static function getAllHashers() { $objects = id(new PhutilSymbolLoader()) - ->setAncestorClass('PhabricatorPasswordHasher') + ->setAncestorClass(__CLASS__) ->loadObjects(); $map = array(); @@ -404,7 +404,7 @@ abstract class PhabricatorPasswordHasher extends Phobject { } try { - $current_hasher = PhabricatorPasswordHasher::getHasherForHash($hash); + $current_hasher = self::getHasherForHash($hash); return $current_hasher->getHumanReadableName(); } catch (Exception $ex) { $info = self::parseHashFromStorage($hash); @@ -421,7 +421,7 @@ abstract class PhabricatorPasswordHasher extends Phobject { */ public static function getBestAlgorithmName() { try { - $best_hasher = PhabricatorPasswordHasher::getBestHasher(); + $best_hasher = self::getBestHasher(); return $best_hasher->getHumanReadableName(); } catch (Exception $ex) { return pht('Unknown'); diff --git a/src/view/AphrontDialogView.php b/src/view/AphrontDialogView.php index ac946969d5..408a92d94d 100644 --- a/src/view/AphrontDialogView.php +++ b/src/view/AphrontDialogView.php @@ -215,7 +215,10 @@ final class AphrontDialogView extends AphrontView { if (!$this->user) { throw new Exception( - pht('You must call setUser() when rendering an AphrontDialogView.')); + pht( + 'You must call %s when rendering an %s.', + 'setUser()', + __CLASS__)); } $more = $this->class; diff --git a/src/view/control/AphrontCursorPagerView.php b/src/view/control/AphrontCursorPagerView.php index a683eb3d6d..81ab33df81 100644 --- a/src/view/control/AphrontCursorPagerView.php +++ b/src/view/control/AphrontCursorPagerView.php @@ -92,8 +92,7 @@ final class AphrontCursorPagerView extends AphrontView { public function getFirstPageURI() { if (!$this->uri) { - throw new Exception( - pht('You must call setURI() before you can call getFirstPageURI().')); + throw new PhutilInvalidStateException('setURI'); } if (!$this->afterID && !($this->beforeID && $this->moreResults)) { @@ -107,8 +106,7 @@ final class AphrontCursorPagerView extends AphrontView { public function getPrevPageURI() { if (!$this->uri) { - throw new Exception( - pht('You must call setURI() before you can call getPrevPageURI().')); + throw new PhutilInvalidStateException('getPrevPageURI'); } if (!$this->prevPageID) { @@ -122,8 +120,7 @@ final class AphrontCursorPagerView extends AphrontView { public function getNextPageURI() { if (!$this->uri) { - throw new Exception( - pht('You must call setURI() before you can call getNextPageURI().')); + throw new PhutilInvalidStateException('setURI'); } if (!$this->nextPageID) { @@ -137,8 +134,7 @@ final class AphrontCursorPagerView extends AphrontView { public function render() { if (!$this->uri) { - throw new Exception( - pht('You must call setURI() before you can call render().')); + throw new PhutilInvalidStateException('setURI'); } $links = array(); diff --git a/src/view/control/AphrontPagerView.php b/src/view/control/AphrontPagerView.php index ba6a98736c..8b08b15c5a 100644 --- a/src/view/control/AphrontPagerView.php +++ b/src/view/control/AphrontPagerView.php @@ -109,8 +109,7 @@ final class AphrontPagerView extends AphrontView { public function render() { if (!$this->uri) { - throw new Exception( - pht('You must call setURI() before you can call render().')); + throw new PhutilInvalidStateException('setURI'); } require_celerity_resource('aphront-pager-view-css'); diff --git a/src/view/control/AphrontTableView.php b/src/view/control/AphrontTableView.php index 9edd851c19..a6814a030e 100644 --- a/src/view/control/AphrontTableView.php +++ b/src/view/control/AphrontTableView.php @@ -124,6 +124,7 @@ final class AphrontTableView extends AphrontView { $visibility = array_values($this->columnVisibility); $device_visibility = array_values($this->deviceVisibility); + $headers = $this->headers; $short_headers = $this->shortHeaders; $sort_values = $this->sortValues; @@ -235,12 +236,16 @@ final class AphrontTableView extends AphrontView { 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. diff --git a/src/view/form/AphrontFormView.php b/src/view/form/AphrontFormView.php index 6b30656131..f9f281ff20 100644 --- a/src/view/form/AphrontFormView.php +++ b/src/view/form/AphrontFormView.php @@ -126,7 +126,10 @@ final class AphrontFormView extends AphrontView { $layout = $this->buildLayoutView(); if (!$this->user) { - throw new Exception(pht('You must pass the user to AphrontFormView.')); + throw new Exception( + pht( + 'You must pass the user to %s.', + __CLASS__)); } $sigils = $this->sigils; diff --git a/src/view/form/PHUIFormLayoutView.php b/src/view/form/PHUIFormLayoutView.php index fa05a72a59..1c5ccbb7f2 100644 --- a/src/view/form/PHUIFormLayoutView.php +++ b/src/view/form/PHUIFormLayoutView.php @@ -7,6 +7,7 @@ */ final class PHUIFormLayoutView extends AphrontView { + private $classes = array(); private $fullWidth; public function setFullWidth($width) { @@ -14,6 +15,11 @@ final class PHUIFormLayoutView extends AphrontView { return $this; } + public function addClass($class) { + $this->classes[] = $class; + return $this; + } + public function appendInstructions($text) { return $this->appendChild( phutil_tag( @@ -26,8 +32,7 @@ final class PHUIFormLayoutView extends AphrontView { public function appendRemarkupInstructions($remarkup) { if ($this->getUser() === null) { - throw new Exception( - 'Call `setUser` before appending Remarkup to PHUIFormLayoutView.'); + throw new PhutilInvalidStateException('setUser'); } return $this->appendInstructions( @@ -38,7 +43,8 @@ final class PHUIFormLayoutView extends AphrontView { } public function render() { - $classes = array('phui-form-view'); + $classes = $this->classes; + $classes[] = 'phui-form-view'; if ($this->fullWidth) { $classes[] = 'phui-form-full-width'; diff --git a/src/view/form/PHUIInfoView.php b/src/view/form/PHUIInfoView.php index a079e3f6ca..497fcf3f78 100644 --- a/src/view/form/PHUIInfoView.php +++ b/src/view/form/PHUIInfoView.php @@ -41,7 +41,6 @@ final class PHUIInfoView extends AphrontView { } final public function render() { - require_celerity_resource('phui-info-view-css'); $errors = $this->errors; diff --git a/src/view/form/PHUIPagedFormView.php b/src/view/form/PHUIPagedFormView.php index 82e58c7f58..ff72f67ca0 100644 --- a/src/view/form/PHUIPagedFormView.php +++ b/src/view/form/PHUIPagedFormView.php @@ -152,7 +152,7 @@ final class PHUIPagedFormView extends AphrontView { $is_attempt_complete = false; if ($this->prevPage) { - $prev_index = $this->getPageIndex($selected->getKey()) - 1;; + $prev_index = $this->getPageIndex($selected->getKey()) - 1; $index = max(0, $prev_index); $selected = $this->getPageByIndex($index); } else if ($this->nextPage) { diff --git a/src/view/form/control/AphrontFormCheckboxControl.php b/src/view/form/control/AphrontFormCheckboxControl.php index 673c1c6d82..0d89e0a6ad 100644 --- a/src/view/form/control/AphrontFormCheckboxControl.php +++ b/src/view/form/control/AphrontFormCheckboxControl.php @@ -4,12 +4,18 @@ final class AphrontFormCheckboxControl extends AphrontFormControl { private $boxes = array(); - public function addCheckbox($name, $value, $label, $checked = false) { + public function addCheckbox( + $name, + $value, + $label, + $checked = false, + $id = null) { $this->boxes[] = array( 'name' => $name, 'value' => $value, 'label' => $label, 'checked' => $checked, + 'id' => $id, ); return $this; } @@ -21,7 +27,10 @@ final class AphrontFormCheckboxControl extends AphrontFormControl { protected function renderInput() { $rows = array(); foreach ($this->boxes as $box) { - $id = celerity_generate_unique_node_id(); + $id = idx($box, 'id'); + if ($id === null) { + $id = celerity_generate_unique_node_id(); + } $checkbox = phutil_tag( 'input', array( diff --git a/src/view/form/control/AphrontFormDateControl.php b/src/view/form/control/AphrontFormDateControl.php index ed79392fbb..84de3b3215 100644 --- a/src/view/form/control/AphrontFormDateControl.php +++ b/src/view/form/control/AphrontFormDateControl.php @@ -11,6 +11,7 @@ final class AphrontFormDateControl extends AphrontFormControl { private $valueTime; private $allowNull; private $continueOnInvalidDate = false; + private $isTimeDisabled; private $isDisabled; public function setAllowNull($allow_null) { @@ -18,6 +19,11 @@ final class AphrontFormDateControl extends AphrontFormControl { return $this; } + public function setIsTimeDisabled($is_disabled) { + $this->isTimeDisabled = $is_disabled; + return $this; + } + const TIME_START_OF_DAY = 'start-of-day'; const TIME_END_OF_DAY = 'end-of-day'; const TIME_START_OF_BUSINESS = 'start-of-business'; @@ -282,6 +288,9 @@ final class AphrontFormDateControl extends AphrontFormControl { if ($disabled) { $classes[] = 'datepicker-disabled'; } + if ($this->isTimeDisabled) { + $classes[] = 'no-time'; + } return javelin_tag( 'div', @@ -291,6 +300,7 @@ final class AphrontFormDateControl extends AphrontFormControl { 'meta' => array( 'disabled' => (bool)$disabled, ), + 'id' => $this->getID(), ), array( $checkbox, diff --git a/src/view/form/control/PhabricatorRemarkupControl.php b/src/view/form/control/PhabricatorRemarkupControl.php index 7d5451a1b2..f384dd3ba3 100644 --- a/src/view/form/control/PhabricatorRemarkupControl.php +++ b/src/view/form/control/PhabricatorRemarkupControl.php @@ -24,8 +24,7 @@ final class PhabricatorRemarkupControl extends AphrontFormTextAreaControl { $viewer = $this->getUser(); if (!$viewer) { - throw new Exception( - pht('Call setUser() before rendering a PhabricatorRemarkupControl!')); + throw new PhutilInvalidStateException('setUser'); } // We need to have this if previews render images, since Ajax can not diff --git a/src/view/page/PhabricatorStandardPageView.php b/src/view/page/PhabricatorStandardPageView.php index b0f8b5d0be..7c5dcaf1cb 100644 --- a/src/view/page/PhabricatorStandardPageView.php +++ b/src/view/page/PhabricatorStandardPageView.php @@ -3,7 +3,6 @@ /** * This is a standard Phabricator page with menus, Javelin, DarkConsole, and * basic styles. - * */ final class PhabricatorStandardPageView extends PhabricatorBarePageView { @@ -160,7 +159,8 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView { if (!$this->getRequest()) { throw new Exception( pht( - 'You must set the Request to render a PhabricatorStandardPageView.')); + 'You must set the Request to render a %s.', + __CLASS__)); } $console = $this->getConsole(); @@ -377,15 +377,6 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView { } } - if (!$this->isQuicksandBlacklistURI()) { - Javelin::initBehavior( - 'scrollbar', - array( - 'nodeID' => 'phabricator-standard-page', - 'isMainContent' => true, - )); - } - $main_page = phutil_tag( 'div', array( diff --git a/src/view/phui/PHUIDocumentView.php b/src/view/phui/PHUIDocumentView.php index 5140d0aa7b..2d4689b8bf 100644 --- a/src/view/phui/PHUIDocumentView.php +++ b/src/view/phui/PHUIDocumentView.php @@ -74,7 +74,7 @@ final class PHUIDocumentView extends AphrontTagView { if ($this->offset) { $classes[] = 'phui-document-offset'; - }; + } if ($this->fluid) { $classes[] = 'phui-document-fluid'; diff --git a/src/view/phui/PHUIHeaderView.php b/src/view/phui/PHUIHeaderView.php index ad3561d930..14159e500c 100644 --- a/src/view/phui/PHUIHeaderView.php +++ b/src/view/phui/PHUIHeaderView.php @@ -1,6 +1,6 @@ image) { + $classes[] = 'phui-header-has-image'; + } + + return array( + 'class' => $classes, + ); + } + + protected function getTagContent() { $image = null; if ($this->image) { $image = phutil_tag( @@ -156,7 +170,6 @@ final class PHUIHeaderView extends AphrontView { 'style' => 'background-image: url('.$this->image.')', ), ' '); - $classes[] = 'phui-header-has-image'; } $header = array(); @@ -243,20 +256,15 @@ final class PHUIHeaderView extends AphrontView { $property_list); } - return phutil_tag( - 'div', - array( - 'class' => implode(' ', $classes), - ), - array( - $image, - phutil_tag( - 'h1', - array( - 'class' => 'phui-header-view grouped', - ), - $header), - )); + return array( + $image, + phutil_tag( + 'h1', + array( + 'class' => 'phui-header-view grouped', + ), + $header), + ); } private function renderPolicyProperty(PhabricatorPolicyInterface $object) { diff --git a/src/view/phui/PHUIRemarkupPreviewPanel.php b/src/view/phui/PHUIRemarkupPreviewPanel.php index 8fa02f983c..9ca2f620f9 100644 --- a/src/view/phui/PHUIRemarkupPreviewPanel.php +++ b/src/view/phui/PHUIRemarkupPreviewPanel.php @@ -42,8 +42,11 @@ final class PHUIRemarkupPreviewPanel extends AphrontTagView { ); if (empty($skins[$skin])) { - $valid = implode(', ', array_keys($skins)); - throw new Exception("Invalid skin '{$skin}'. Valid skins are: {$valid}."); + throw new Exception( + pht( + "Invalid skin '%s'. Valid skins are: %s.", + $skin, + implode(', ', array_keys($skins)))); } $this->skin = $skin; @@ -69,10 +72,10 @@ final class PHUIRemarkupPreviewPanel extends AphrontTagView { protected function getTagContent() { if ($this->previewURI === null) { - throw new Exception('Call setPreviewURI() before rendering!'); + throw new PhutilInvalidStateException('setPreviewURI'); } if ($this->controlID === null) { - throw new Exception('Call setControlID() before rendering!'); + throw new PhutilInvalidStateException('setControlID'); } $preview_id = celerity_generate_unique_node_id(); diff --git a/src/view/phui/calendar/PHUICalendarDayView.php b/src/view/phui/calendar/PHUICalendarDayView.php index 6d42fdcec3..21c893125d 100644 --- a/src/view/phui/calendar/PHUICalendarDayView.php +++ b/src/view/phui/calendar/PHUICalendarDayView.php @@ -1,12 +1,17 @@ events[] = $event; @@ -21,7 +26,16 @@ final class PHUICalendarDayView extends AphrontView { return $this->browseURI; } - public function __construct($year, $month, $day = null) { + public function __construct( + $range_start, + $range_end, + $year, + $month, + $day = null) { + + $this->rangeStart = $range_start; + $this->rangeEnd = $range_end; + $this->day = $day; $this->month = $month; $this->year = $year; @@ -32,53 +46,91 @@ final class PHUICalendarDayView extends AphrontView { $hours = $this->getHoursOfDay(); $hourly_events = array(); - $rows = array(); - // sort events into buckets by their start time - // pretend no events overlap + $first_event_hour = null; + + $all_day_events = $this->getAllDayEvents(); + $today_all_day_events = array(); + + $day_start = $this->getDateTime(); + $day_end = id(clone $day_start)->modify('+1 day'); + + $day_start_epoch = $day_start->format('U'); + $day_end_epoch = $day_end->format('U') - 1; + + foreach ($all_day_events as $all_day_event) { + $all_day_start = $all_day_event->getEpochStart(); + $all_day_end = $all_day_event->getEpochEnd(); + + if ($all_day_start < $day_end_epoch && $all_day_end > $day_start_epoch) { + $today_all_day_events[] = $all_day_event; + } + } + foreach ($hours as $hour) { - $events = array(); + $current_hour_events = array(); $hour_start = $hour->format('U'); $hour_end = id(clone $hour)->modify('+1 hour')->format('U'); + foreach ($this->events as $event) { - if ($event->getEpochStart() >= $hour_start - && $event->getEpochStart() < $hour_end) { - $events[] = $event; + if ($event->getIsAllDay()) { + continue; + } + if (($hour == $day_start && + $event->getEpochStart() <= $hour_start && + $event->getEpochEnd() > $day_start_epoch) || + ($event->getEpochStart() >= $hour_start + && $event->getEpochStart() < $hour_end)) { + $current_hour_events[] = $event; + $this->todayEvents[] = $event; } } - $count_events = count($events); - $n = 0; - foreach ($events as $event) { - $event_start = $event->getEpochStart(); - $event_end = $event->getEpochEnd(); + foreach ($current_hour_events as $event) { + $day_start_epoch = $this->getDateTime()->format('U'); + $event_start = max($event->getEpochStart(), $day_start_epoch); + $event_end = min($event->getEpochEnd(), $day_end_epoch); - $top = ((($event_start - $hour_start) / ($hour_end - $hour_start)) - * 100).'%'; - $height = ((($event_end - $event_start) / ($hour_end - $hour_start)) - * 100).'%'; + $top = (($event_start - $hour_start) / ($hour_end - $hour_start)) + * 100; + $top = max(0, $top); + + $height = (($event_end - $event_start) / ($hour_end - $hour_start)) + * 100; + $height = min(2400, $height); + + if ($first_event_hour === null) { + $first_event_hour = $hour; + } $hourly_events[$event->getEventID()] = array( 'hour' => $hour, 'event' => $event, 'offset' => '0', 'width' => '100%', - 'top' => $top, - 'height' => $height, + 'top' => $top.'%', + 'height' => $height.'%', ); - - $n++; } } - $clusters = $this->findClusters(); + $clusters = $this->findTodayClusters(); foreach ($clusters as $cluster) { $hourly_events = $this->updateEventsFromCluster( $cluster, $hourly_events); } - // actually construct table + $rows = array(); + foreach ($hours as $hour) { + $early_hours = array(8); + if ($first_event_hour) { + $early_hours[] = $first_event_hour->format('G'); + } + if ($hour->format('G') < min($early_hours)) { + continue; + } + $drawn_hourly_events = array(); $cell_time = phutil_tag( 'td', @@ -87,6 +139,7 @@ final class PHUICalendarDayView extends AphrontView { foreach ($hourly_events as $hourly_event) { if ($hourly_event['hour'] == $hour) { + $drawn_hourly_events[] = $this->drawEvent( $hourly_event['event'], $hourly_event['offset'], @@ -111,17 +164,23 @@ final class PHUICalendarDayView extends AphrontView { $table = phutil_tag( 'table', array('class' => 'phui-calendar-day-view'), - array( - '', - $rows, - )); + $rows); + + $all_day_event_box = new PHUIBoxView(); + foreach ($today_all_day_events as $all_day_event) { + $all_day_event_box->appendChild( + $this->drawAllDayEvent($all_day_event)); + } $header = $this->renderDayViewHeader(); $sidebar = $this->renderSidebar(); + $warnings = $this->getQueryRangeWarning(); $table_box = id(new PHUIObjectBoxView()) ->setHeader($header) + ->appendChild($all_day_event_box) ->appendChild($table) + ->setFormErrors($warnings) ->setFlush(true); $layout = id(new AphrontMultiColumnView()) @@ -138,33 +197,83 @@ final class PHUICalendarDayView extends AphrontView { $layout); } + private function getAllDayEvents() { + $all_day_events = array(); + + foreach ($this->events as $event) { + if ($event->getIsAllDay()) { + $all_day_events[] = $event; + } + } + + $all_day_events = array_values(msort($all_day_events, 'getEpochStart')); + return $all_day_events; + } + + private function getQueryRangeWarning() { + $errors = array(); + + $range_start_epoch = $this->rangeStart->getEpoch(); + $range_end_epoch = $this->rangeEnd->getEpoch(); + + $day_start = $this->getDateTime(); + $day_end = id(clone $day_start)->modify('+1 day'); + + $day_start = $day_start->format('U'); + $day_end = $day_end->format('U') - 1; + + if (($range_start_epoch != null && + $range_start_epoch < $day_end && + $range_start_epoch > $day_start) || + ($range_end_epoch != null && + $range_end_epoch < $day_end && + $range_end_epoch > $day_start)) { + $errors[] = pht('Part of the day is out of range'); + } + + if (($this->rangeEnd->getEpoch() != null && + $this->rangeEnd->getEpoch() < $day_start) || + ($this->rangeStart->getEpoch() != null && + $this->rangeStart->getEpoch() > $day_end)) { + $errors[] = pht('Day is out of query range'); + } + return $errors; + } + private function renderSidebar() { $this->events = msort($this->events, 'getEpochStart'); $week_of_boxes = $this->getWeekOfBoxes(); $filled_boxes = array(); - foreach ($week_of_boxes as $weekly_box) { - $start = $weekly_box['start']; - $end = id(clone $start)->modify('+1 day'); + foreach ($week_of_boxes as $day_box) { + $box_start = $day_box['start']; + $box_end = id(clone $box_start)->modify('+1 day'); + + $box_start = $box_start->format('U'); + $box_end = $box_end->format('U'); $box_events = array(); foreach ($this->events as $event) { - if ($event->getEpochStart() >= $start->format('U') && - $event->getEpochStart() < $end->format('U')) { + $event_start = $event->getEpochStart(); + $event_end = $event->getEpochEnd(); + + if ($event_start < $box_end && $event_end > $box_start) { $box_events[] = $event; } } + $filled_boxes[] = $this->renderSidebarBox( $box_events, - $weekly_box['title']); + $day_box['title']); } return $filled_boxes; } private function renderSidebarBox($events, $title) { - $widget = new PHUICalendarWidgetView(); + $widget = id(new PHUICalendarWidgetView()) + ->addClass('calendar-day-view-sidebar'); $list = id(new PHUICalendarListView()) ->setUser($this->user); @@ -172,7 +281,8 @@ final class PHUICalendarDayView extends AphrontView { if (count($events) == 0) { $list->showBlankState(true); } else { - foreach ($events as $event) { + $sorted_events = msort($events, 'getEpochStart'); + foreach ($sorted_events as $event) { $list->addEvent($event); } } @@ -216,8 +326,6 @@ final class PHUICalendarDayView extends AphrontView { private function renderDayViewHeader() { $button_bar = null; - - // check for a browseURI, which means we need "fancy" prev / next UI $uri = $this->getBrowseURI(); if ($uri) { list($prev_year, $prev_month, $prev_day) = $this->getPrevDay(); @@ -251,9 +359,8 @@ final class PHUICalendarDayView extends AphrontView { } - $day_of_week = $this->getDayOfWeek(); - $header_text = $this->getDateTime()->format('F j, Y'); - $header_text = $day_of_week.', '.$header_text; + $display_day = $this->getDateTime(); + $header_text = $display_day->format('l, F j, Y'); $header = id(new PHUIHeaderView()) ->setHeader($header_text); @@ -267,7 +374,6 @@ final class PHUICalendarDayView extends AphrontView { private function updateEventsFromCluster($cluster, $hourly_events) { $cluster_size = count($cluster); - $n = 0; foreach ($cluster as $cluster_member) { $event_id = $cluster_member->getEventID(); @@ -284,6 +390,35 @@ final class PHUICalendarDayView extends AphrontView { return $hourly_events; } + private function drawAllDayEvent(AphrontCalendarEventView $event) { + $name = phutil_tag( + 'a', + array( + 'class' => 'day-view-all-day', + 'href' => $event->getURI(), + ), + $event->getName()); + + $all_day_label = phutil_tag( + 'span', + array( + 'class' => 'phui-calendar-all-day-label', + ), + pht('All Day')); + + $div = phutil_tag( + 'div', + array( + 'class' => 'phui-calendar-day-event', + ), + array( + $all_day_label, + $name, + )); + + return $div; + } + private function drawEvent( AphrontCalendarEventView $event, $offset, @@ -313,12 +448,6 @@ final class PHUICalendarDayView extends AphrontView { return $div; } - private function getDayOfWeek() { - $date = $this->getDateTime(); - $day_of_week = $date->format('l'); - return $day_of_week; - } - // returns DateTime of each hour in the day private function getHoursOfDay() { $included_datetimes = array(); @@ -375,14 +504,14 @@ final class PHUICalendarDayView extends AphrontView { return $date; } - private function findClusters() { - $events = msort($this->events, 'getEpochStart'); + private function findTodayClusters() { + $events = msort($this->todayEvents, 'getEpochStart'); $clusters = array(); foreach ($events as $event) { $destination_cluster_key = null; - $event_start = $event->getEpochStart(); - $event_end = $event->getEpochEnd(); + $event_start = $event->getEpochStart() - (30 * 60); + $event_end = $event->getEpochEnd() + (30 * 60); foreach ($clusters as $key => $cluster) { foreach ($cluster as $clustered_event) { diff --git a/src/view/phui/calendar/PHUICalendarListView.php b/src/view/phui/calendar/PHUICalendarListView.php index fd6a7a0e73..fc40d87745 100644 --- a/src/view/phui/calendar/PHUICalendarListView.php +++ b/src/view/phui/calendar/PHUICalendarListView.php @@ -22,7 +22,7 @@ final class PHUICalendarListView extends AphrontTagView { protected function getTagAttributes() { require_celerity_resource('phui-calendar-css'); require_celerity_resource('phui-calendar-list-css'); - return array('class' => 'phui-calendar-day-list'); + return array('class' => 'phui-calendar-event-list'); } protected function getTagContent() { @@ -30,27 +30,28 @@ final class PHUICalendarListView extends AphrontTagView { return ''; } - $events = msort($this->events, 'getEpochStart'); - $singletons = array(); $allday = false; - foreach ($events as $event) { + foreach ($this->events as $event) { $color = $event->getColor(); + $start_epoch = $event->getEpochStart(); - if ($event->getAllDay()) { + if ($event->getIsAllDay()) { $timelabel = pht('All Day'); + $dot = null; } else { $timelabel = phabricator_time( $event->getEpochStart(), $this->getUser()); + + $dot = phutil_tag( + 'span', + array( + 'class' => 'phui-calendar-list-dot', + ), + ''); } - $dot = phutil_tag( - 'span', - array( - 'class' => 'phui-calendar-list-dot', - ), - ''); $title = phutil_tag( 'span', array( @@ -64,10 +65,15 @@ final class PHUICalendarListView extends AphrontTagView { ), $timelabel); + $class = 'phui-calendar-list-item phui-calendar-'.$color; + if ($event->getIsAllDay()) { + $class = $class.' all-day'; + } + $singletons[] = phutil_tag( 'li', array( - 'class' => 'phui-calendar-list-item phui-calendar-'.$color, + 'class' => $class, ), array( $dot, @@ -112,11 +118,13 @@ final class PHUICalendarListView extends AphrontTagView { $description = pht('(%s)', $event->getName()); } + $class = 'phui-calendar-item-link'; + $anchor = javelin_tag( 'a', array( 'sigil' => 'has-tooltip', - 'class' => 'phui-calendar-item-link', + 'class' => $class, 'href' => '/E'.$event->getEventID(), 'meta' => array( 'tip' => $tip, diff --git a/src/view/phui/calendar/PHUICalendarMonthView.php b/src/view/phui/calendar/PHUICalendarMonthView.php index 089d98144c..7eb35f76a5 100644 --- a/src/view/phui/calendar/PHUICalendarMonthView.php +++ b/src/view/phui/calendar/PHUICalendarMonthView.php @@ -1,11 +1,12 @@ holidays = mpull($holidays, null, 'getDay'); - return $this; - } + public function __construct( + $range_start, + $range_end, + $month, + $year, + $day = null) { + + $this->rangeStart = $range_start; + $this->rangeEnd = $range_end; - public function __construct($month, $year, $day = null) { $this->day = $day; $this->month = $month; $this->year = $year; @@ -48,126 +52,237 @@ final class PHUICalendarMonthView extends AphrontView { public function render() { if (empty($this->user)) { - throw new Exception('Call setUser() before render()!'); + throw new PhutilInvalidStateException('setUser'); } $events = msort($this->events, 'getEpochStart'); - $days = $this->getDatesInMonth(); + $cell_lists = array(); + $empty_cell = array( + 'list' => null, + 'date' => null, + 'uri' => null, + 'count' => 0, + 'class' => null, + ); + require_celerity_resource('phui-calendar-month-css'); $first = reset($days); + $start_of_week = 0; + $empty = $first->format('w'); - $markup = array(); - - $empty_box = phutil_tag( - 'div', - array('class' => 'phui-calendar-day phui-calendar-empty'), - ''); - for ($ii = 0; $ii < $empty; $ii++) { - $markup[] = $empty_box; + $cell_lists[] = $empty_cell; } - $show_events = array(); - foreach ($days as $day) { $day_number = $day->format('j'); - $holiday = idx($this->holidays, $day->format('Y-m-d')); - $class = 'phui-calendar-day'; + $class = 'phui-calendar-month-day'; $weekday = $day->format('w'); - if ($day_number == $this->day) { - $class .= ' phui-calendar-today'; - } - - if ($holiday || $weekday == 0 || $weekday == 6) { - $class .= ' phui-calendar-not-work-day'; - } - $day->setTime(0, 0, 0); - $epoch_start = $day->format('U'); - - $day->modify('+1 day'); - $epoch_end = $day->format('U'); - - if ($weekday == 0) { - $show_events = array(); - } else { - $show_events = array_fill_keys( - array_keys($show_events), - phutil_tag_div( - 'phui-calendar-event phui-calendar-event-empty', - "\xC2\xA0")); //   - } + $day_start_epoch = $day->format('U'); + $day_end_epoch = id(clone $day)->modify('+1 day')->format('U'); $list_events = array(); + $all_day_events = array(); + foreach ($events as $event) { - if ($event->getEpochStart() >= $epoch_end) { - // This list is sorted, so we can stop looking. + if ($event->getEpochStart() >= $day_end_epoch) { break; } - if ($event->getEpochStart() < $epoch_end && - $event->getEpochEnd() > $epoch_start) { - $list_events[] = $event; + if ($event->getEpochStart() < $day_end_epoch && + $event->getEpochEnd() > $day_start_epoch) { + if ($event->getIsAllDay()) { + $all_day_events[] = $event; + } else { + $list_events[] = $event; + } } } $list = new PHUICalendarListView(); $list->setUser($this->user); + foreach ($all_day_events as $item) { + $list->addEvent($item); + } foreach ($list_events as $item) { $list->addEvent($item); } - $holiday_markup = null; - if ($holiday) { - $name = $holiday->getName(); - $holiday_markup = phutil_tag( - 'div', - array( - 'class' => 'phui-calendar-holiday', - 'title' => $name, - ), - $name); - } + $uri = $this->getBrowseURI(); + $uri = $uri.$day->format('Y').'/'. + $day->format('m').'/'. + $day->format('d').'/'; - $markup[] = phutil_tag_div( - $class, - array( - phutil_tag_div('phui-calendar-date-number', $day_number), - $holiday_markup, - $list, - )); + $cell_lists[] = array( + 'list' => $list, + 'date' => $day, + 'uri' => $uri, + 'count' => count($all_day_events) + count($list_events), + 'class' => $class, + ); } - $table = array(); - $rows = array_chunk($markup, 7); - foreach ($rows as $row) { + $rows = array(); + $cell_lists_by_week = array_chunk($cell_lists, 7); + + foreach ($cell_lists_by_week as $week_of_cell_lists) { $cells = array(); - while (count($row) < 7) { - $row[] = $empty_box; + while (count($week_of_cell_lists) < 7) { + $week_of_cell_lists[] = $empty_cell; } - $j = 0; - foreach ($row as $cell) { - if ($j == 0) { - $cells[] = phutil_tag( - 'td', - array( - 'class' => 'phui-calendar-month-weekstart', - ), - $cell); - } else { - $cells[] = phutil_tag('td', array(), $cell); - } - $j++; + foreach ($week_of_cell_lists as $cell_list) { + $cells[] = $this->getEventListCell($cell_list); } - $table[] = phutil_tag('tr', array(), $cells); + $rows[] = phutil_tag('tr', array(), $cells); + + $cells = array(); + foreach ($week_of_cell_lists as $cell_list) { + $cells[] = $this->getDayNumberCell($cell_list); + } + $rows[] = phutil_tag('tr', array(), $cells); } - $header = phutil_tag( + $header = $this->getDayNamesHeader(); + + $table = phutil_tag( + 'table', + array('class' => 'phui-calendar-view'), + array( + $header, + $rows, + )); + + $warnings = $this->getQueryRangeWarning(); + + $box = id(new PHUIObjectBoxView()) + ->setHeader($this->renderCalendarHeader($first)) + ->appendChild($table) + ->setFormErrors($warnings); + if ($this->error) { + $box->setInfoView($this->error); + + } + + return $box; + } + + private function getEventListCell($event_list) { + $list = $event_list['list']; + $class = $event_list['class']; + $uri = $event_list['uri']; + $count = $event_list['count']; + + $event_count_badge = $this->getEventCountBadge($count); + $cell_day_secret_link = $this->getHiddenDayLink($uri); + + $cell_data_div = phutil_tag( + 'div', + array( + 'class' => 'phui-calendar-month-cell-div', + ), + array( + $cell_day_secret_link, + $event_count_badge, + $list, + )); + + return phutil_tag( + 'td', + array( + 'class' => 'phui-calendar-month-event-list '.$class, + ), + $cell_data_div); + } + + private function getDayNumberCell($event_list) { + $class = $event_list['class']; + $date = $event_list['date']; + $cell_day_secret_link = null; + + if ($date) { + $uri = $event_list['uri']; + $cell_day_secret_link = $this->getHiddenDayLink($uri); + + $cell_day = phutil_tag( + 'a', + array( + 'class' => 'phui-calendar-date-number', + 'href' => $uri, + ), + $date->format('j')); + } else { + $cell_day = null; + } + + if ($date && $date->format('j') == $this->day) { + $today_class = 'phui-calendar-today-slot phui-calendar-today'; + } else { + $today_class = 'phui-calendar-today-slot'; + } + + $today_slot = phutil_tag ( + 'div', + array( + 'class' => $today_class, + ), + null); + + $cell_div = phutil_tag( + 'div', + array( + 'class' => 'phui-calendar-month-cell-div', + ), + array( + $cell_day_secret_link, + $cell_day, + $today_slot, + )); + + return phutil_tag( + 'td', + array( + 'class' => 'phui-calendar-date-number-container '.$class, + ), + $cell_div); + } + + private function getEventCountBadge($count) { + $event_count = null; + if ($count > 0) { + $event_count = phutil_tag( + 'div', + array( + 'class' => 'phui-calendar-month-count-badge', + ), + $count); + } + + return phutil_tag( + 'div', + array( + 'class' => 'phui-calendar-month-event-count', + ), + $event_count); + } + + private function getHiddenDayLink($uri) { + return phutil_tag( + 'a', + array( + 'class' => 'phui-calendar-month-secret-link', + 'href' => $uri, + ), + null); + } + + private function getDayNamesHeader() { + return phutil_tag( 'tr', array('class' => 'phui-calendar-day-of-week-header'), array( @@ -179,24 +294,6 @@ final class PHUICalendarMonthView extends AphrontView { phutil_tag('th', array(), pht('Fri')), phutil_tag('th', array(), pht('Sat')), )); - - $table = phutil_tag( - 'table', - array('class' => 'phui-calendar-view'), - array( - $header, - phutil_implode_html("\n", $table), - )); - - $box = id(new PHUIObjectBoxView()) - ->setHeader($this->renderCalendarHeader($first)) - ->appendChild($table); - if ($this->error) { - $box->setInfoView($this->error); - - } - - return $box; } private function renderCalendarHeader(DateTime $date) { @@ -250,6 +347,37 @@ final class PHUICalendarMonthView extends AphrontView { return $header; } + private function getQueryRangeWarning() { + $errors = array(); + + $range_start_epoch = $this->rangeStart->getEpoch(); + $range_end_epoch = $this->rangeEnd->getEpoch(); + + $month_start = $this->getDateTime(); + $month_end = id(clone $month_start)->modify('+1 month'); + + $month_start = $month_start->format('U'); + $month_end = $month_end->format('U') - 1; + + if (($range_start_epoch != null && + $range_start_epoch < $month_end && + $range_start_epoch > $month_start) || + ($range_end_epoch != null && + $range_end_epoch < $month_end && + $range_end_epoch > $month_start)) { + $errors[] = pht('Part of the month is out of range'); + } + + if (($this->rangeEnd->getEpoch() != null && + $this->rangeEnd->getEpoch() < $month_start) || + ($this->rangeStart->getEpoch() != null && + $this->rangeStart->getEpoch() > $month_end)) { + $errors[] = pht('Month is out of query range'); + } + + return $errors; + } + private function getNextYearAndMonth() { $next = $this->getDateTime(); $next->modify('+1 month'); @@ -282,22 +410,41 @@ final class PHUICalendarMonthView extends AphrontView { $month = $this->month; $year = $this->year; - // Get the year and month numbers of the following month, so we can - // determine when this month ends. list($next_year, $next_month) = $this->getNextYearAndMonth(); - $end_date = new DateTime("{$next_year}-{$next_month}-01", $timezone); - $end_epoch = $end_date->format('U'); + + $start_of_week = 0; + $end_of_week = 6 - $start_of_week; + $days_in_month = id(clone $end_date)->modify('-1 day')->format('d'); + + $first_month_day_date = new DateTime("{$year}-{$month}-01", $timezone); + $last_month_day_date = id(clone $end_date)->modify('-1 day'); + + $first_weekday_of_month = $first_month_day_date->format('w'); + $last_weekday_of_month = $last_month_day_date->format('w'); + + $num_days_display = $days_in_month; + if ($start_of_week < $first_weekday_of_month) { + $num_days_display += $first_weekday_of_month; + } + if ($end_of_week > $last_weekday_of_month) { + $num_days_display += (6 - $last_weekday_of_month); + $end_date->modify('+'.(6 - $last_weekday_of_month).' days'); + } $days = array(); - for ($day = 1; $day <= 31; $day++) { - $day_date = new DateTime("{$year}-{$month}-{$day}", $timezone); + $day_date = id(clone $first_month_day_date) + ->modify('-'.$first_weekday_of_month.' days'); + + for ($day = 1; $day <= $num_days_display; $day++) { $day_epoch = $day_date->format('U'); + $end_epoch = $end_date->format('U'); if ($day_epoch >= $end_epoch) { break; } else { - $days[] = $day_date; + $days[] = clone $day_date; } + $day_date->modify('+1 day'); } return $days; diff --git a/src/view/widget/hovercard/PhabricatorHovercardView.php b/src/view/widget/hovercard/PhabricatorHovercardView.php index 40cb6fa30e..58131a6a6b 100644 --- a/src/view/widget/hovercard/PhabricatorHovercardView.php +++ b/src/view/widget/hovercard/PhabricatorHovercardView.php @@ -62,7 +62,7 @@ final class PhabricatorHovercardView extends AphrontView { public function render() { if (!$this->handle) { - throw new Exception('Call setObjectHandle() before calling render()!'); + throw new PhutilInvalidStateException('setObjectHandle'); } $handle = $this->handle; diff --git a/support/PhabricatorStartup.php b/support/PhabricatorStartup.php index 3945049ffb..30e8f3c937 100644 --- a/support/PhabricatorStartup.php +++ b/support/PhabricatorStartup.php @@ -255,7 +255,7 @@ final class PhabricatorStartup { static $initialized; if (!$initialized) { declare(ticks=1); - register_tick_function(array('PhabricatorStartup', 'onDebugTick')); + register_tick_function(array(__CLASS__, 'onDebugTick')); } } @@ -647,7 +647,7 @@ final class PhabricatorStartup { // populated into $_POST, but it wasn't. $config = ini_get('post_max_size'); - PhabricatorStartup::didFatal( + 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 ". diff --git a/webroot/rsrc/css/application/auth/auth.css b/webroot/rsrc/css/application/auth/auth.css index 219c8795a3..07eaccd46c 100644 --- a/webroot/rsrc/css/application/auth/auth.css +++ b/webroot/rsrc/css/application/auth/auth.css @@ -26,11 +26,21 @@ .auth-account-view { background-color: #fff; border: 1px solid {$lightblueborder}; - background-repeat: no-repeat; - background-position: 4px 4px; - padding: 4px 4px 4px 62px; - min-height: 50px; border-radius: 2px; + min-height: 50px; + position: relative; + padding: 4px 4px 4px 64px; +} + +.auth-account-view-profile-image { + width: 50px; + height: 50px; + top: 4px; + left: 4px; + + background-repeat: no-repeat; + background-size: 100%; + position: absolute; } .auth-account-view-name { diff --git a/webroot/rsrc/css/application/base/standard-page-view.css b/webroot/rsrc/css/application/base/standard-page-view.css index d31594852c..93a9364412 100644 --- a/webroot/rsrc/css/application/base/standard-page-view.css +++ b/webroot/rsrc/css/application/base/standard-page-view.css @@ -114,27 +114,6 @@ a.handle-disabled { margin: 2px 2px -2px 0; } -.main-page-frame { - position: absolute; - top: 0; - bottom: 0; - right: 0; - left: 0; - overflow: hidden; -} - -.phabricator-standard-page { - /* If we don't activate JX.Scrollbar because the default scrollbars are - satisfactory, make sure the page still has sensible behavior. These - settings will be overwritten by .jx-scrollbar-frame if JX.Scrollbar - activates. */ - position: relative; - height: 100%; - overflow-y: scroll; - - -webkit-overflow-scrolling: touch; -} - .jx-scrollbar-frame { position: relative; overflow: hidden; diff --git a/webroot/rsrc/css/application/conduit/conduit-api.css b/webroot/rsrc/css/application/conduit/conduit-api.css new file mode 100644 index 0000000000..fbe577e908 --- /dev/null +++ b/webroot/rsrc/css/application/conduit/conduit-api.css @@ -0,0 +1,16 @@ +/** + * @provides conduit-api-css + */ +.conduit-api-example-code { + margin: 16px; + white-space: pre; + color: {$darkgreytext}; +} + +.conduit-api-example-code strong { + color: {$red}; +} + +.conduit-api-example-code strong.real { + color: {$blue}; +} diff --git a/webroot/rsrc/css/application/conpherence/durable-column.css b/webroot/rsrc/css/application/conpherence/durable-column.css index 324d76c27c..1dbb829565 100644 --- a/webroot/rsrc/css/application/conpherence/durable-column.css +++ b/webroot/rsrc/css/application/conpherence/durable-column.css @@ -2,10 +2,22 @@ * @provides conpherence-durable-column-view */ -.with-durable-column .phabricator-standard-page { +.with-durable-column .phabricator-standard-page-body { margin-right: 300px; } +.with-durable-margin .phabricator-standard-page-body { + margin-right: 312px; +} + +.with-durable-column .phabricator-main-menu { + padding-right: 304px; +} + +.with-durable-margin .phabricator-main-menu { + padding-right: 316px; +} + .with-durable-column .phabricator-global-upload-instructions { font-size: 28px; @@ -20,8 +32,12 @@ right: 300px; } +.with-durable-margin .global-upload-mask { + right: 312px; +} + .conpherence-durable-column { - position: absolute; + position: fixed; top: 0; bottom: 0; right: 0; @@ -29,6 +45,11 @@ background: #fff; } +.with-durable-margin .conpherence-durable-column { + right: 12px; + box-shadow: 1px 0px 2px rgba(0, 0, 0, 0.10); +} + .conpherence-durable-column .loading-mask { position: absolute; top: 90px; @@ -126,6 +147,10 @@ border-left: 1px solid {$lightblueborder}; } +.with-durable-margin .conpherence-durable-column-body { + border-right: 1px solid {$lightblueborder}; +} + .conpherence-durable-column-main { position: absolute; top: 46px; diff --git a/webroot/rsrc/css/application/conpherence/message-pane.css b/webroot/rsrc/css/application/conpherence/message-pane.css index b450703d96..41815d086f 100644 --- a/webroot/rsrc/css/application/conpherence/message-pane.css +++ b/webroot/rsrc/css/application/conpherence/message-pane.css @@ -113,6 +113,14 @@ right: 241px; } +.conpherence-message-pane .phui-form-view.login-to-participate { + height: 28px; +} + +.conpherence-message-pane .login-to-participate a.button { + float: right; +} + .conpherence-message-pane .aphront-form-control-submit button, .conpherence-message-pane .aphront-form-control-submit a.button { margin-top: 6px; @@ -154,7 +162,7 @@ .conpherence-message-pane .conpherence-transaction-view { padding: 2px 0px; margin: 4px 12px; - background-size: 35px; + background-size: 100%; min-height: auto; } diff --git a/webroot/rsrc/css/application/conpherence/notification.css b/webroot/rsrc/css/application/conpherence/notification.css index 75403f991d..e5bd40274a 100644 --- a/webroot/rsrc/css/application/conpherence/notification.css +++ b/webroot/rsrc/css/application/conpherence/notification.css @@ -26,7 +26,7 @@ position: absolute; width: 30px; height: 30px; - background-size: 30px; + background-size: 100%; } .phabricator-notification .conpherence-menu-item-view diff --git a/webroot/rsrc/css/application/conpherence/transaction.css b/webroot/rsrc/css/application/conpherence/transaction.css index d67d31692e..6ebd53b2ee 100644 --- a/webroot/rsrc/css/application/conpherence/transaction.css +++ b/webroot/rsrc/css/application/conpherence/transaction.css @@ -25,3 +25,22 @@ color: {$darkbluetext}; font-weight: bold; } + +.conpherence-fulltext-results { + margin: 0 8px 8px; + background: {$lightgreybackground}; + border: 1px solid {$lightgreyborder}; +} + +.conpherence-fulltext-result { + margin: 0 0 1px; + padding: 8px; +} + +.conpherence-fulltext-match { + background: {$lightyellow}; +} + +.conpherence-fulltext-results .epoch-link { + float: right; +} diff --git a/webroot/rsrc/css/core/core.css b/webroot/rsrc/css/core/core.css index 7333d87994..4bb08ea3df 100644 --- a/webroot/rsrc/css/core/core.css +++ b/webroot/rsrc/css/core/core.css @@ -54,6 +54,10 @@ body { breaks lots of things and prevents you from using landscape to see more columns in source code views. */ -webkit-text-size-adjust: none; + + /* Prevent content from resizing abruptly when shifting between scrollable + and unscrollable pages. */ + overflow-y: scroll; } textarea { diff --git a/webroot/rsrc/css/core/z-index.css b/webroot/rsrc/css/core/z-index.css index 4c2c5d457a..c69638eede 100644 --- a/webroot/rsrc/css/core/z-index.css +++ b/webroot/rsrc/css/core/z-index.css @@ -81,7 +81,6 @@ z-index: 5; } -.conpherence-durable-column-header, .phabricator-main-menu { z-index: 6; } @@ -90,10 +89,14 @@ z-index: 6; } -.jx-scrollbar-bar { +.conpherence-durable-column { z-index: 7; } +.jx-scrollbar-bar { + z-index: 8; +} + .differential-haunt-mode-1 .differential-add-comment-panel, .differential-haunt-mode-2 .differential-add-comment-panel { z-index: 8; diff --git a/webroot/rsrc/css/phui/calendar/phui-calendar-day.css b/webroot/rsrc/css/phui/calendar/phui-calendar-day.css index bb07f8b8f8..11f4045fe1 100644 --- a/webroot/rsrc/css/phui/calendar/phui-calendar-day.css +++ b/webroot/rsrc/css/phui/calendar/phui-calendar-day.css @@ -48,3 +48,25 @@ text-decoration: none; color: {$greytext}; } + +.day-view-all-day { + border: 1px solid {$blueborder}; + height: 12px; + margin: 0; + display: block; + padding: 8px; + background-color: {$bluebackground}; + text-decoration: none; + color: {$greytext}; +} + +.phui-calendar-day-event + .phui-calendar-day-event .day-view-all-day { + border-top-style: none; + border-top-width: 0; +} + +.phui-calendar-all-day-label { + color: {$greytext}; + float: right; + margin: 8px 8px 0 0; +} diff --git a/webroot/rsrc/css/phui/calendar/phui-calendar-month.css b/webroot/rsrc/css/phui/calendar/phui-calendar-month.css index c3abbb131c..3f53e89b25 100644 --- a/webroot/rsrc/css/phui/calendar/phui-calendar-month.css +++ b/webroot/rsrc/css/phui/calendar/phui-calendar-month.css @@ -17,52 +17,91 @@ tr.phui-calendar-day-of-week-header th { } table.phui-calendar-view td { - border: 1px solid #dfdfdf; + border: solid #dfdfdf; + border-width: 1px 1px 0 1px; width: 14.2857%; /* This is one seventh, approximately. */ } -table.phui-calendar-view td div.phui-calendar-day { - min-height: 125px; +.phui-calendar-month-cell-div { position: relative; } -.phui-calendar-holiday { - color: {$greytext}; - padding: .5em; - max-height: 1em; - overflow: hidden; +.phui-calendar-month-event-list .phui-calendar-month-cell-div { + min-height: 125px; } -table.phui-calendar-view td.phui-calendar-month-weekstart { - border-left: none; +.device .phui-calendar-month-event-list .phui-calendar-month-cell-div { + min-height: 60px; } -.phui-calendar-date-number { +a.phui-calendar-month-secret-link { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + outline: 0; +} + +table.phui-calendar-view tr td:first-child { + border-left-width: 0px; +} + +.device table.phui-calendar-view .phui-calendar-event-list { + display: none; +} + +.phui-calendar-month-event-count { + display: none; +} + +.device .phui-calendar-month-event-count { + display: block; + text-align: center; + padding-top: 12px; +} + +.phui-calendar-month-event-count .phui-calendar-month-count-badge { + border: 1px solid {$lightgreyborder}; + color: {$lightgreytext}; + width: 20px; + height: 20px; + border-radius: 50%; + text-align: center; + vertical-align: middle; + padding: 5px 3px 0px 3px; + margin: 0 auto; +} + +table.phui-calendar-view a.phui-calendar-date-number { + color: {$lightgreytext}; + padding: 0 4px; + display: inline-block; + min-width: 16px; + text-align: center; +} + +table.phui-calendar-view td.phui-calendar-date-number-container { font-weight: normal; color: {$lightgreytext}; - padding: 4px; - border-color: {$thinblueborder}; - border-style: solid; - border-width: 0 0 1px 1px; - position: absolute; - background: #ffffff; - width: 16px; - height: 16px; - text-align: center; - top: 0; - right: 0; + border-width: 0 1px 0 1px; + text-align: right; } .phui-calendar-not-work-day { background-color: {$lightgreybackground}; } -.phui-calendar-today { - background-color: {$lightgreen}; +.phui-calendar-today-slot { + display: block; + width: 100%; + height: 4px; + padding: 0; + margin: 0; } -.phui-calendar-empty { - background-color: {$greybackground}; +.phui-calendar-today-slot.phui-calendar-today { + background-color: {$lightblueborder}; } .phui-calendar-event-empty { @@ -71,11 +110,35 @@ table.phui-calendar-view td.phui-calendar-month-weekstart { } .phui-calendar-view .phui-calendar-list { - padding: 8px; + padding: 1px; +} + +.phui-calendar-list-item.all-day span { + padding: 0; + margin: 0; +} + +.phui-calendar-view .phui-calendar-list li.phui-calendar-list-item.all-day { + margin: 0; + padding: 4px 8px; + background-color: {$lightpink}; + display: block; + float: none; +} + +li.phui-calendar-list-item.all-day:first-child { + margin-top: 0; +} + +.phui-calendar-view .phui-calendar-list li { + margin: 0 8px; + display: inline-block; + float: left; + clear: both; } .phui-calendar-view .phui-calendar-list li:first-child { - margin-right: 16px; + margin-top: 8px; } .phui-calendar-view .phui-calendar-list-dot { @@ -95,6 +158,10 @@ table.phui-calendar-view td.phui-calendar-month-weekstart { word-break: break-word; } +li.phui-calendar-list-item.all-day .phui-calendar-list-title a{ + color: {$pink}; +} + .phui-calendar-view .phui-calendar-list-time { display: none; } diff --git a/webroot/rsrc/css/phui/phui-form-view.css b/webroot/rsrc/css/phui/phui-form-view.css index c9026c3980..2f7462cf11 100644 --- a/webroot/rsrc/css/phui/phui-form-view.css +++ b/webroot/rsrc/css/phui/phui-form-view.css @@ -453,6 +453,10 @@ properly, and submit values. */ opacity: 0.5; } +.aphront-form-date-container.no-time .aphront-form-date-time-input{ + display: none; +} + .login-to-comment { margin: 12px; diff --git a/webroot/rsrc/css/phui/phui-header-view.css b/webroot/rsrc/css/phui/phui-header-view.css index 6352681156..a270536711 100644 --- a/webroot/rsrc/css/phui/phui-header-view.css +++ b/webroot/rsrc/css/phui/phui-header-view.css @@ -93,6 +93,7 @@ body.device-phone .phui-header-view { .phui-header-image { display: inline-block; background-repeat: no-repeat; + background-size: 100%; border: 2px solid white; width: 50px; height: 50px; diff --git a/webroot/rsrc/css/phui/phui-object-item-list-view.css b/webroot/rsrc/css/phui/phui-object-item-list-view.css index a8ba8d5c01..a6251754ef 100644 --- a/webroot/rsrc/css/phui/phui-object-item-list-view.css +++ b/webroot/rsrc/css/phui/phui-object-item-list-view.css @@ -545,7 +545,7 @@ ul.phui-object-item-icons { .phui-object-item-image { width: 40px; height: 40px; - background-size: 40px; + background-size: 100%; margin: 6px; position: absolute; background-color: {$lightbluebackground}; diff --git a/webroot/rsrc/css/phui/phui-timeline-view.css b/webroot/rsrc/css/phui/phui-timeline-view.css index b82c9385a7..d35a5458ee 100644 --- a/webroot/rsrc/css/phui/phui-timeline-view.css +++ b/webroot/rsrc/css/phui/phui-timeline-view.css @@ -92,6 +92,7 @@ .phui-timeline-image { background-repeat: no-repeat; + background-size: 100%; position: absolute; border-radius: 3px; } diff --git a/webroot/rsrc/externals/javelin/lib/Scrollbar.js b/webroot/rsrc/externals/javelin/lib/Scrollbar.js index 684fb4ce08..7596939581 100644 --- a/webroot/rsrc/externals/javelin/lib/Scrollbar.js +++ b/webroot/rsrc/externals/javelin/lib/Scrollbar.js @@ -100,6 +100,7 @@ JX.install('Scrollbar', { statics: { _controlWidth: null, + /** * Compute the width of the browser's scrollbar control, in pixels. */ @@ -118,8 +119,35 @@ JX.install('Scrollbar', { } return self._controlWidth; + }, + + + /** + * Get the margin width required to avoid double scrollbars. + * + * For most browsers which render a real scrollbar control, this is 0. + * Adjacent elements may touch the edge of the content directly without + * overlapping. + * + * On OSX with a trackpad, scrollbars are only drawn when content is + * scrolled. Content panes with internal scrollbars may overlap adjacent + * scrollbars if they are not laid out with a margin. + * + * @return int Control margin width in pixels. + */ + getScrollbarControlMargin: function() { + var self = JX.Scrollbar; + + // If this browser and OS don't render a real scrollbar control, we + // need to leave a margin. Generally, this is OSX with no mouse attached. + if (self._getScrollbarControlWidth() === 0) { + return 12; + } + + return 0; } + }, members: { diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/default.p100.png b/webroot/rsrc/image/icon/fatcow/thumbnails/default.p100.png deleted file mode 100644 index f713c2398b..0000000000 Binary files a/webroot/rsrc/image/icon/fatcow/thumbnails/default.p100.png and /dev/null differ diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/default160x120.png b/webroot/rsrc/image/icon/fatcow/thumbnails/default160x120.png deleted file mode 100644 index 16d6fd4f90..0000000000 Binary files a/webroot/rsrc/image/icon/fatcow/thumbnails/default160x120.png and /dev/null differ diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/default280x210.png b/webroot/rsrc/image/icon/fatcow/thumbnails/default280x210.png deleted file mode 100644 index 7288c81954..0000000000 Binary files a/webroot/rsrc/image/icon/fatcow/thumbnails/default280x210.png and /dev/null differ diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/default60x45.png b/webroot/rsrc/image/icon/fatcow/thumbnails/default60x45.png deleted file mode 100644 index 145ea1eb63..0000000000 Binary files a/webroot/rsrc/image/icon/fatcow/thumbnails/default60x45.png and /dev/null differ diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/image.p100.png b/webroot/rsrc/image/icon/fatcow/thumbnails/image.p100.png deleted file mode 100644 index f5fa35ab08..0000000000 Binary files a/webroot/rsrc/image/icon/fatcow/thumbnails/image.p100.png and /dev/null differ diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/image160x120.png b/webroot/rsrc/image/icon/fatcow/thumbnails/image160x120.png deleted file mode 100644 index 90cc11c02d..0000000000 Binary files a/webroot/rsrc/image/icon/fatcow/thumbnails/image160x120.png and /dev/null differ diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/image280x210.png b/webroot/rsrc/image/icon/fatcow/thumbnails/image280x210.png deleted file mode 100644 index efdf733f8e..0000000000 Binary files a/webroot/rsrc/image/icon/fatcow/thumbnails/image280x210.png and /dev/null differ diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/image60x45.png b/webroot/rsrc/image/icon/fatcow/thumbnails/image60x45.png deleted file mode 100644 index 9077e69586..0000000000 Binary files a/webroot/rsrc/image/icon/fatcow/thumbnails/image60x45.png and /dev/null differ diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/pdf.p100.png b/webroot/rsrc/image/icon/fatcow/thumbnails/pdf.p100.png deleted file mode 100644 index ad3a39b490..0000000000 Binary files a/webroot/rsrc/image/icon/fatcow/thumbnails/pdf.p100.png and /dev/null differ diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/pdf160x120.png b/webroot/rsrc/image/icon/fatcow/thumbnails/pdf160x120.png deleted file mode 100644 index 20f08f955b..0000000000 Binary files a/webroot/rsrc/image/icon/fatcow/thumbnails/pdf160x120.png and /dev/null differ diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/pdf280x210.png b/webroot/rsrc/image/icon/fatcow/thumbnails/pdf280x210.png deleted file mode 100644 index 8036981aca..0000000000 Binary files a/webroot/rsrc/image/icon/fatcow/thumbnails/pdf280x210.png and /dev/null differ diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/pdf60x45.png b/webroot/rsrc/image/icon/fatcow/thumbnails/pdf60x45.png deleted file mode 100644 index 8a16eaf488..0000000000 Binary files a/webroot/rsrc/image/icon/fatcow/thumbnails/pdf60x45.png and /dev/null differ diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/zip.p100.png b/webroot/rsrc/image/icon/fatcow/thumbnails/zip.p100.png deleted file mode 100644 index 86fa739b3b..0000000000 Binary files a/webroot/rsrc/image/icon/fatcow/thumbnails/zip.p100.png and /dev/null differ diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/zip160x120.png b/webroot/rsrc/image/icon/fatcow/thumbnails/zip160x120.png deleted file mode 100644 index fbe19e59f6..0000000000 Binary files a/webroot/rsrc/image/icon/fatcow/thumbnails/zip160x120.png and /dev/null differ diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/zip280x210.png b/webroot/rsrc/image/icon/fatcow/thumbnails/zip280x210.png deleted file mode 100644 index 8db127b282..0000000000 Binary files a/webroot/rsrc/image/icon/fatcow/thumbnails/zip280x210.png and /dev/null differ diff --git a/webroot/rsrc/image/icon/fatcow/thumbnails/zip60x45.png b/webroot/rsrc/image/icon/fatcow/thumbnails/zip60x45.png deleted file mode 100644 index c66416c318..0000000000 Binary files a/webroot/rsrc/image/icon/fatcow/thumbnails/zip60x45.png and /dev/null differ diff --git a/webroot/rsrc/js/application/aphlict/Aphlict.js b/webroot/rsrc/js/application/aphlict/Aphlict.js index d4886a12a0..78d0958c81 100644 --- a/webroot/rsrc/js/application/aphlict/Aphlict.js +++ b/webroot/rsrc/js/application/aphlict/Aphlict.js @@ -49,6 +49,10 @@ JX.install('Aphlict', { JX.Leader.call(JX.bind(this, this._begin)); }, + getSubscriptions: function() { + return this._subscriptions; + }, + setSubscriptions: function(subscriptions) { this._subscriptions = subscriptions; JX.Leader.broadcast( diff --git a/webroot/rsrc/js/application/aphlict/behavior-aphlict-dropdown.js b/webroot/rsrc/js/application/aphlict/behavior-aphlict-dropdown.js index 042855ee4e..d1ebc33d9f 100644 --- a/webroot/rsrc/js/application/aphlict/behavior-aphlict-dropdown.js +++ b/webroot/rsrc/js/application/aphlict/behavior-aphlict-dropdown.js @@ -93,23 +93,34 @@ JX.behavior('aphlict-dropdown', function(config, statics) { if (!data.fromServer) { return; } - var updated = false; var new_data = data.newResponse.aphlictDropdownData; - for (var ii = 0; ii < new_data.length; ii++) { - if (new_data[ii].countType != config.countType) { - continue; - } - if (!new_data[ii].isInstalled) { - continue; - } - updated = true; - _updateCount(parseInt(new_data[ii].count)); - } - if (updated) { - dirty = true; - } + update_counts(new_data); }); + JX.Stratcom.listen( + 'conpherence-redraw-aphlict', + null, + function (e) { + update_counts(e.getData()); + }); + + function update_counts(new_data) { + var updated = false; + for (var ii = 0; ii < new_data.length; ii++) { + if (new_data[ii].countType != config.countType) { + continue; + } + if (!new_data[ii].isInstalled) { + continue; + } + updated = true; + _updateCount(parseInt(new_data[ii].count)); + } + if (updated) { + dirty = true; + } + } + function set_visible(menu, icon) { if (menu) { statics.visible = {menu: menu, icon: icon}; diff --git a/webroot/rsrc/js/application/calendar/event-all-day.js b/webroot/rsrc/js/application/calendar/event-all-day.js new file mode 100644 index 0000000000..a8bd7da7a8 --- /dev/null +++ b/webroot/rsrc/js/application/calendar/event-all-day.js @@ -0,0 +1,16 @@ +/** + * @provides javelin-behavior-event-all-day + */ + + +JX.behavior('event-all-day', function(config) { + var checkbox = JX.$(config.allDayID); + JX.DOM.listen(checkbox, 'change', null, function() { + var start = JX.$(config.startDateID); + var end = JX.$(config.endDateID); + + JX.DOM.alterClass(start, 'no-time', checkbox.checked); + JX.DOM.alterClass(end, 'no-time', checkbox.checked); + }); + +}); diff --git a/webroot/rsrc/js/application/conpherence/ConpherenceThreadManager.js b/webroot/rsrc/js/application/conpherence/ConpherenceThreadManager.js index b1694e19ca..c1c47c5c78 100644 --- a/webroot/rsrc/js/application/conpherence/ConpherenceThreadManager.js +++ b/webroot/rsrc/js/application/conpherence/ConpherenceThreadManager.js @@ -4,6 +4,7 @@ * javelin-util * javelin-stratcom * javelin-install + * javelin-aphlict * javelin-workflow * javelin-router * javelin-behavior-device @@ -26,9 +27,12 @@ JX.install('ConpherenceThreadManager', { _loadedThreadID: null, _loadedThreadPHID: null, _latestTransactionID: null, + _transactionIDMap: null, + _transactionCache: null, _canEditLoadedThread: null, _updating: null, _minimalDisplay: false, + _messagesRootCallback: JX.bag, _willLoadThreadCallback: JX.bag, _didLoadThreadCallback: JX.bag, _didUpdateThreadCallback: JX.bag, @@ -81,6 +85,59 @@ JX.install('ConpherenceThreadManager', { return this; }, + _updateTransactionIDMap: function(transactions) { + var loaded_id = this.getLoadedThreadID(); + if (!this._transactionIDMap[loaded_id]) { + this._transactionIDMap[this._loadedThreadID] = {}; + } + var loaded_transaction_ids = this._transactionIDMap[loaded_id]; + var transaction; + for (var ii = 0; ii < transactions.length; ii++) { + transaction = transactions[ii]; + loaded_transaction_ids[JX.Stratcom.getData(transaction).id] = 1; + } + this._transactionIDMap[this._loadedThreadID] = loaded_transaction_ids; + return this; + }, + + _updateTransactionCache: function(transactions) { + var transaction; + for (var ii = 0; ii < transactions.length; ii++) { + transaction = transactions[ii]; + this._transactionCache[JX.Stratcom.getData(transaction).id] = + transaction; + } + return this; + }, + + _getLoadedTransactions: function() { + var loaded_id = this.getLoadedThreadID(); + var loaded_tx_ids = JX.keys(this._transactionIDMap[loaded_id]); + loaded_tx_ids.sort(function (a, b) { + var x = parseFloat(a); + var y = parseFloat(b); + if (x > y) { + return 1; + } + if (x < y) { + return -1; + } + return 0; + }); + var transactions = []; + for (var ii = 0; ii < loaded_tx_ids.length; ii++) { + transactions.push(this._transactionCache[loaded_tx_ids[ii]]); + } + return transactions; + }, + + _deleteTransactionCaches: function(id) { + delete this._transactionCache[id]; + delete this._transactionIDMap[this._loadedThreadID][id]; + + return this; + }, + setCanEditLoadedThread: function(bool) { this._canEditLoadedThread = bool; return this; @@ -98,6 +155,11 @@ JX.install('ConpherenceThreadManager', { return this; }, + setMessagesRootCallback: function(callback) { + this._messagesRootCallback = callback; + return this; + }, + setWillLoadThreadCallback: function(callback) { this._willLoadThreadCallback = callback; return this; @@ -144,6 +206,10 @@ JX.install('ConpherenceThreadManager', { }, start: function() { + + this._transactionIDMap = {}; + this._transactionCache = {}; + JX.Stratcom.listen( 'aphlict-server-message', null, @@ -164,20 +230,66 @@ JX.install('ConpherenceThreadManager', { // Message event for something we already know about. return; } - // If we're currently updating, wait for the update to complete. + // If this notification tells us about a message which is newer than - // the newest one we know to exist, keep track of it so we can - // update once the in-flight update finishes. + // the newest one we know to exist, update our latest knownID so we + // can properly update later. if (this._updating && this._updating.threadPHID == this._loadedThreadPHID) { if (message.messageID > this._updating.knownID) { this._updating.knownID = message.messageID; - return; + // We're currently updating, so wait for the update to complete. + // this.syncWorkflow has us covered in this case. + if (this._updating.active) { + return; + } } } this._updateThread(); })); + + JX.Stratcom.listen( + 'click', + 'show-older-messages', + JX.bind(this, function(e) { + e.kill(); + var data = e.getNodeData('show-older-messages'); + + var node = e.getNode('show-older-messages'); + JX.DOM.setContent(node, 'Loading...'); + JX.DOM.alterClass( + node, + 'conpherence-show-more-messages-loading', + true); + + new JX.Workflow(this._getMoreMessagesURI(), data) + .setHandler(JX.bind(this, function(r) { + this._deleteTransactionCaches(JX.Stratcom.getData(node).id); + JX.DOM.remove(node); + this._updateTransactions(r); + })).start(); + })); + JX.Stratcom.listen( + 'click', + 'show-newer-messages', + JX.bind(this, function(e) { + e.kill(); + var data = e.getNodeData('show-newer-messages'); + var node = e.getNode('show-newer-messages'); + JX.DOM.setContent(node, 'Loading...'); + JX.DOM.alterClass( + node, + 'conpherence-show-more-messages-loading', + true); + + new JX.Workflow(this._getMoreMessagesURI(), data) + .setHandler(JX.bind(this, function(r) { + this._deleteTransactionCaches(JX.Stratcom.getData(node).id); + JX.DOM.remove(node); + this._updateTransactions(r); + })).start(); + })); }, _shouldUpdateDOM: function(r) { @@ -197,10 +309,25 @@ JX.install('ConpherenceThreadManager', { return true; }, - _markUpdated: function(r) { + _updateDOM: function(r) { + this._updateTransactions(r); + this._updating.knownID = r.latest_transaction_id; this._latestTransactionID = r.latest_transaction_id; - JX.Stratcom.invoke('notification-panel-update', null, {}); + JX.Stratcom.invoke( + 'conpherence-redraw-aphlict', + null, + r.aphlictDropdownData); + }, + + _updateTransactions: function(r) { + var new_transactions = JX.$H(r.transactions).getFragment().childNodes; + this._updateTransactionIDMap(new_transactions); + this._updateTransactionCache(new_transactions); + + var transactions = this._getLoadedTransactions(); + + JX.DOM.setContent(this._messagesRootCallback(), transactions); }, _updateThread: function() { @@ -208,14 +335,11 @@ JX.install('ConpherenceThreadManager', { action: 'load', }); - var uri = '/conpherence/update/' + this._loadedThreadID + '/'; - - var workflow = new JX.Workflow(uri) + var workflow = new JX.Workflow(this._getUpdateURI()) .setData(params) .setHandler(JX.bind(this, function(r) { if (this._shouldUpdateDOM(r)) { - this._markUpdated(r); - + this._updateDOM(r); this._didUpdateThreadCallback(r); } })); @@ -226,7 +350,8 @@ JX.install('ConpherenceThreadManager', { syncWorkflow: function(workflow, stage) { this._updating = { threadPHID: this._loadedThreadPHID, - knownID: this._latestTransactionID + knownID: this._latestTransactionID, + active: true }; workflow.listen(stage, JX.bind(this, function() { // TODO - do we need to handle if we switch threads somehow? @@ -235,6 +360,7 @@ JX.install('ConpherenceThreadManager', { if (need_sync) { return this._updateThread(); } + this._updating.active = false; })); workflow.start(); }, @@ -246,8 +372,7 @@ JX.install('ConpherenceThreadManager', { .setData(params) .setHandler(JX.bind(this, function(r) { if (this._shouldUpdateDOM(r)) { - this._markUpdated(r); - + this._updateDOM(r); this._didUpdateWorkflowCallback(r); } })); @@ -271,13 +396,39 @@ JX.install('ConpherenceThreadManager', { params = this._getParams(params); var handler = JX.bind(this, function(r) { + var client = JX.Aphlict.getInstance(); + if (client) { + var old_subs = client.getSubscriptions(); + var new_subs = []; + for (var ii = 0; ii < old_subs.length; ii++) { + if (old_subs[ii] == this._loadedThreadPHID) { + continue; + } else { + new_subs.push(old_subs[ii]); + } + } + new_subs.push(r.threadPHID); + client.clearSubscriptions(client.getSubscriptions()); + client.setSubscriptions(new_subs); + } this._loadedThreadID = r.threadID; this._loadedThreadPHID = r.threadPHID; this._latestTransactionID = r.latestTransactionID; this._canEditLoadedThread = r.canEdit; - JX.Stratcom.invoke('notification-panel-update', null, {}); + + JX.Stratcom.invoke( + 'conpherence-redraw-aphlict', + null, + r.aphlictDropdownData); this._didLoadThreadCallback(r); + var messages_root = this._messagesRootCallback(); + var messages = JX.DOM.scry( + messages_root, + 'div', + 'conpherence-transaction-view'); + this._updateTransactionIDMap(messages); + this._updateTransactionCache(messages); if (force_reload) { JX.Stratcom.invoke('hashchange'); @@ -304,8 +455,7 @@ JX.install('ConpherenceThreadManager', { var workflow = JX.Workflow.newFromForm(form, params, keep_enabled) .setHandler(JX.bind(this, function(r) { if (this._shouldUpdateDOM(r)) { - this._markUpdated(r); - + this._updateDOM(r); this._didSendMessageCallback(r); } else if (r.non_update) { this._didSendMessageCallback(r, true); @@ -321,9 +471,8 @@ JX.install('ConpherenceThreadManager', { var data = e.getNodeData('tag:form'); if (!data.preview) { - var uri = '/conpherence/update/' + this._loadedThreadID + '/'; data.preview = new JX.PhabricatorShapedRequest( - uri, + this._getUpdateURI(), JX.bag, JX.bind(this, function () { var data = JX.DOM.convertFormToDictionary(form); @@ -333,6 +482,14 @@ JX.install('ConpherenceThreadManager', { })); } data.preview.trigger(); + }, + + _getUpdateURI: function() { + return '/conpherence/update/' + this._loadedThreadID + '/'; + }, + + _getMoreMessagesURI: function() { + return '/conpherence/' + this._loadedThreadID + '/'; } }, diff --git a/webroot/rsrc/js/application/conpherence/behavior-durable-column.js b/webroot/rsrc/js/application/conpherence/behavior-durable-column.js index adcaa382a3..4a9eac1eaf 100644 --- a/webroot/rsrc/js/application/conpherence/behavior-durable-column.js +++ b/webroot/rsrc/js/application/conpherence/behavior-durable-column.js @@ -29,9 +29,11 @@ JX.behavior('durable-column', function(config, statics) { var loadThreadID = null; var scrollbar = null; - var columnWidth = 300; + var margin = JX.Scrollbar.getScrollbarControlMargin(); + + var columnWidth = (300 + margin); // This is the smallest window size where we'll enable the column. - var minimumViewportWidth = 768; + var minimumViewportWidth = (768 - margin); var quick = JX.$('phabricator-standard-page-body'); @@ -71,7 +73,15 @@ JX.behavior('durable-column', function(config, statics) { } function _drawColumn(visible) { - JX.DOM.alterClass(document.body, 'with-durable-column', visible); + JX.DOM.alterClass( + document.body, + 'with-durable-column', + visible); + JX.DOM.alterClass( + document.body, + 'with-durable-margin', + visible && !!margin); + var column = _getColumnNode(); if (visible) { JX.DOM.show(column); @@ -109,6 +119,9 @@ JX.behavior('durable-column', function(config, statics) { var threadManager = new JX.ConpherenceThreadManager(); threadManager.setMinimalDisplay(true); + threadManager.setMessagesRootCallback(function() { + return _getColumnMessagesNode(); + }); threadManager.setLoadThreadURI('/conpherence/columnview/'); threadManager.setWillLoadThreadCallback(function() { _markLoading(true); @@ -146,7 +159,6 @@ JX.behavior('durable-column', function(config, statics) { return; } var messages = _getColumnMessagesNode(); - JX.DOM.appendContent(messages, JX.$H(r.transactions)); scrollbar.scrollTo(messages.scrollHeight); }); @@ -155,7 +167,6 @@ JX.behavior('durable-column', function(config, statics) { }); threadManager.setDidUpdateWorkflowCallback(function(r) { var messages = _getColumnMessagesNode(); - JX.DOM.appendContent(messages, JX.$H(r.transactions)); scrollbar.scrollTo(messages.scrollHeight); JX.DOM.setContent(_getColumnTitleNode(), r.conpherence_title); }); diff --git a/webroot/rsrc/js/application/conpherence/behavior-menu.js b/webroot/rsrc/js/application/conpherence/behavior-menu.js index 26e184813e..2233b3ff28 100644 --- a/webroot/rsrc/js/application/conpherence/behavior-menu.js +++ b/webroot/rsrc/js/application/conpherence/behavior-menu.js @@ -28,12 +28,15 @@ JX.behavior('conpherence-menu', function(config) { // TODO - move more logic into the ThreadManager var threadManager = new JX.ConpherenceThreadManager(); + threadManager.setMessagesRootCallback(function() { + return scrollbar.getContentNode(); + }); threadManager.setWillLoadThreadCallback(function() { markThreadLoading(true); }); threadManager.setDidLoadThreadCallback(function(r) { var header = JX.$H(r.header); - var messages = JX.$H(r.messages); + var messages = JX.$H(r.transactions); var form = JX.$H(r.form); var root = JX.DOM.find(document, 'div', 'conpherence-layout'); var header_root = JX.DOM.find(root, 'div', 'conpherence-header-pane'); @@ -48,7 +51,6 @@ JX.behavior('conpherence-menu', function(config) { }); threadManager.setDidUpdateThreadCallback(function(r) { - JX.DOM.appendContent(scrollbar.getContentNode(), JX.$H(r.transactions)); _scrollMessageWindow(); }); @@ -70,14 +72,13 @@ JX.behavior('conpherence-menu', function(config) { } catch (ex) { // Ignore; maybe no files widget } - JX.DOM.appendContent(scrollbar.getContentNode(), JX.$H(r.transactions)); - _scrollMessageWindow(); - if (fileWidget) { JX.DOM.setContent( fileWidget, JX.$H(r.file_widget)); } + + _scrollMessageWindow(); textarea.value = ''; } markThreadLoading(false); @@ -415,56 +416,6 @@ JX.behavior('conpherence-menu', function(config) { .start(); }); - var _oldLoadingTransactionID = null; - JX.Stratcom.listen('click', 'show-older-messages', function(e) { - e.kill(); - var data = e.getNodeData('show-older-messages'); - if (data.oldest_transaction_id == _oldLoadingTransactionID) { - return; - } - _oldLoadingTransactionID = data.oldest_transaction_id; - - var node = e.getNode('show-older-messages'); - JX.DOM.setContent(node, 'Loading...'); - JX.DOM.alterClass(node, 'conpherence-show-more-messages-loading', true); - - var conf_id = _thread.selected; - var messages_root = scrollbar.getContentNode(); - new JX.Workflow(config.baseURI + conf_id + '/', data) - .setHandler(function(r) { - JX.DOM.remove(node); - var messages = JX.$H(r.messages); - JX.DOM.prependContent( - messages_root, - JX.$H(messages)); - }).start(); - }); - - var _newLoadingTransactionID = null; - JX.Stratcom.listen('click', 'show-newer-messages', function(e) { - e.kill(); - var data = e.getNodeData('show-newer-messages'); - if (data.newest_transaction_id == _newLoadingTransactionID) { - return; - } - _newLoadingTransactionID = data.newest_transaction_id; - - var node = e.getNode('show-newer-messages'); - JX.DOM.setContent(node, 'Loading...'); - JX.DOM.alterClass(node, 'conpherence-show-more-messages-loading', true); - - var conf_id = _thread.selected; - var messages_root = scrollbar.getContentNode(); - new JX.Workflow(config.baseURI + conf_id + '/', data) - .setHandler(function(r) { - JX.DOM.remove(node); - var messages = JX.$H(r.messages); - JX.DOM.appendContent( - messages_root, - JX.$H(messages)); - }).start(); - }); - /** * On devices, we just show a thread list, so we don't want to automatically * select or load any threads. On desktop, we automatically select the first diff --git a/webroot/rsrc/js/application/differential/DifferentialInlineCommentEditor.js b/webroot/rsrc/js/application/differential/DifferentialInlineCommentEditor.js index 049e3f1db1..9842da11f5 100644 --- a/webroot/rsrc/js/application/differential/DifferentialInlineCommentEditor.js +++ b/webroot/rsrc/js/application/differential/DifferentialInlineCommentEditor.js @@ -33,7 +33,7 @@ JX.install('DifferentialInlineCommentEditor', { on_right : this.getOnRight(), id : this.getID(), number : this.getLineNumber(), - is_new : this.getIsNew(), + is_new : (this.getIsNew() ? 1 : 0), length : this.getLength(), changesetID : this.getChangesetID(), text : this.getText() || '', diff --git a/webroot/rsrc/js/application/projects/behavior-project-boards.js b/webroot/rsrc/js/application/projects/behavior-project-boards.js index 20a78d0963..04e99fde28 100644 --- a/webroot/rsrc/js/application/projects/behavior-project-boards.js +++ b/webroot/rsrc/js/application/projects/behavior-project-boards.js @@ -249,6 +249,9 @@ JX.behavior('project-boards', function(config, statics) { for (ii = 0; ii < lists.length; ii++) { lists[ii].setGroup(lists); } + } + + function setup() { JX.Stratcom.listen( 'click', @@ -336,6 +339,9 @@ JX.behavior('project-boards', function(config, statics) { statics.boardID = new_config.boardID; } update_statics(new_config); + if (data.fromServer) { + init_board(); + } }); return true; } @@ -345,7 +351,8 @@ JX.behavior('project-boards', function(config, statics) { var current_page_id = JX.Quicksand.getCurrentPageID(); statics.boardConfigCache = {}; statics.boardConfigCache[current_page_id] = config; - statics.setup = init_board(); + init_board(); + statics.setup = setup(); } }); diff --git a/webroot/rsrc/js/core/Hovercard.js b/webroot/rsrc/js/core/Hovercard.js index e83b0fcba0..15149eceeb 100644 --- a/webroot/rsrc/js/core/Hovercard.js +++ b/webroot/rsrc/js/core/Hovercard.js @@ -80,6 +80,7 @@ JX.install('Hovercard', { var p = JX.$V(root); var d = JX.Vector.getDim(root); var n = JX.Vector.getDim(child); + var v = JX.Vector.getViewport(); // Move the tip so it's nicely aligned. // I'm just doing north/south alignment for now @@ -89,8 +90,12 @@ JX.install('Hovercard', { var x = parseInt(p.x, 10) - margin / 2; var y = parseInt(p.y - n.y, 10) - margin; + // If running off the edge of the viewport, make it margin / 2 away + // from the far right edge of the viewport instead + if ((x + n.x) > (v.x)) { + x = x - parseInt(x + n.x - v.x + margin / 2, 10); // If more in the center, we can safely center - if (x > (n.x / 2) + margin) { + } else if (x > (n.x / 2) + margin) { x = parseInt(p.x - (n.x / 2) + d.x, 10); }