diff --git a/bin/conduit b/bin/conduit new file mode 120000 index 0000000000..9221340a93 --- /dev/null +++ b/bin/conduit @@ -0,0 +1 @@ +../scripts/setup/manage_conduit.php \ No newline at end of file diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 58239423a9..eb25ac0261 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,11 +9,11 @@ return array( 'names' => array( 'conpherence.pkg.css' => 'e68cf1fa', 'conpherence.pkg.js' => '15191c65', - 'core.pkg.css' => '144d9932', - 'core.pkg.js' => '4c79d74f', + 'core.pkg.css' => '07cdfee8', + 'core.pkg.js' => '3ac6e174', 'darkconsole.pkg.js' => '1f9a31bc', - 'differential.pkg.css' => '45951e9e', - 'differential.pkg.js' => '19ee9979', + 'differential.pkg.css' => '113e692c', + 'differential.pkg.js' => '5d53d5ce', 'diffusion.pkg.css' => 'a2d17c7d', 'diffusion.pkg.js' => '6134c5a1', 'favicon.ico' => '4d48ee79', @@ -31,7 +31,7 @@ return array( 'rsrc/css/aphront/multi-column.css' => '84cc6640', 'rsrc/css/aphront/notification.css' => '457861ec', 'rsrc/css/aphront/panel-view.css' => '8427b78d', - 'rsrc/css/aphront/phabricator-nav-view.css' => 'faf6a6fc', + 'rsrc/css/aphront/phabricator-nav-view.css' => '028126f6', 'rsrc/css/aphront/table-view.css' => '8c9bbafe', 'rsrc/css/aphront/tokenizer.css' => '15d5ff71', 'rsrc/css/aphront/tooltip.css' => '173b9431', @@ -121,7 +121,7 @@ return array( 'rsrc/css/font/font-awesome.css' => 'e838e088', 'rsrc/css/font/font-lato.css' => 'c7ccd872', 'rsrc/css/font/phui-font-icon-base.css' => '870a7360', - 'rsrc/css/layout/phabricator-filetree-view.css' => 'fccf9f82', + 'rsrc/css/layout/phabricator-filetree-view.css' => 'b912ad97', 'rsrc/css/layout/phabricator-source-code-view.css' => 'aea41829', 'rsrc/css/phui/button/phui-button-bar.css' => 'f1ff5494', 'rsrc/css/phui/button/phui-button-simple.css' => '8e1baf68', @@ -136,7 +136,7 @@ return array( 'rsrc/css/phui/object-item/phui-oi-flush-ui.css' => '9d9685d6', 'rsrc/css/phui/object-item/phui-oi-list-view.css' => '6ae18df0', 'rsrc/css/phui/object-item/phui-oi-simple-ui.css' => 'a8beebea', - 'rsrc/css/phui/phui-action-list.css' => 'f7f61a34', + 'rsrc/css/phui/phui-action-list.css' => '0bcd9a45', 'rsrc/css/phui/phui-action-panel.css' => 'b4798122', 'rsrc/css/phui/phui-badge.css' => '22c0cf4f', 'rsrc/css/phui/phui-basic-nav-view.css' => '98c11ab3', @@ -176,7 +176,7 @@ return array( 'rsrc/css/phui/phui-spacing.css' => '042804d6', 'rsrc/css/phui/phui-status.css' => 'd5263e49', 'rsrc/css/phui/phui-tag-view.css' => 'b4719c50', - 'rsrc/css/phui/phui-timeline-view.css' => 'e2ef62b1', + 'rsrc/css/phui/phui-timeline-view.css' => '6ddf8126', 'rsrc/css/phui/phui-two-column-view.css' => '44ec4951', 'rsrc/css/phui/workboards/phui-workboard-color.css' => '783cdff5', 'rsrc/css/phui/workboards/phui-workboard.css' => '3bc85455', @@ -397,8 +397,8 @@ return array( 'rsrc/js/application/dashboard/behavior-dashboard-move-panels.js' => '408bf173', 'rsrc/js/application/dashboard/behavior-dashboard-query-panel-select.js' => '453c5375', 'rsrc/js/application/dashboard/behavior-dashboard-tab-panel.js' => 'd4eecc63', - 'rsrc/js/application/diff/DiffChangeset.js' => '99abf4cd', - 'rsrc/js/application/diff/DiffChangesetList.js' => '3b77efdd', + 'rsrc/js/application/diff/DiffChangeset.js' => 'b49b59d6', + 'rsrc/js/application/diff/DiffChangesetList.js' => '1f2e5265', 'rsrc/js/application/diff/DiffInline.js' => 'e83d28f3', 'rsrc/js/application/diff/behavior-preview-link.js' => '051c7832', 'rsrc/js/application/differential/behavior-comment-preview.js' => '51c5ad07', @@ -500,7 +500,7 @@ return array( 'rsrc/js/core/behavior-more.js' => 'a80d0378', 'rsrc/js/core/behavior-object-selector.js' => '77c1f0b0', 'rsrc/js/core/behavior-oncopy.js' => '2926fff2', - 'rsrc/js/core/behavior-phabricator-nav.js' => '947753e0', + 'rsrc/js/core/behavior-phabricator-nav.js' => '81144dfa', 'rsrc/js/core/behavior-phabricator-remarkup-assist.js' => 'acd29eee', 'rsrc/js/core/behavior-read-only-warning.js' => 'ba158207', 'rsrc/js/core/behavior-refresh-csrf.js' => 'ab2f381b', @@ -534,7 +534,7 @@ return array( 'rsrc/js/phuix/PHUIXButtonView.js' => '8a91e1ac', 'rsrc/js/phuix/PHUIXDropdownMenu.js' => '04b2ae03', 'rsrc/js/phuix/PHUIXExample.js' => '68af71ca', - 'rsrc/js/phuix/PHUIXFormControl.js' => '1dd0870c', + 'rsrc/js/phuix/PHUIXFormControl.js' => '16ad6224', 'rsrc/js/phuix/PHUIXIconView.js' => 'bff6884b', ), 'symbols' => array( @@ -659,7 +659,7 @@ return array( 'javelin-behavior-phabricator-keyboard-pager' => 'a8da01f0', 'javelin-behavior-phabricator-keyboard-shortcuts' => '01fca1f0', 'javelin-behavior-phabricator-line-linker' => '1499a8cb', - 'javelin-behavior-phabricator-nav' => '947753e0', + 'javelin-behavior-phabricator-nav' => '81144dfa', 'javelin-behavior-phabricator-notification-example' => '8ce821c5', 'javelin-behavior-phabricator-object-selector' => '77c1f0b0', 'javelin-behavior-phabricator-oncopy' => '2926fff2', @@ -768,7 +768,7 @@ return array( 'path-typeahead' => 'f7fc67ec', 'people-picture-menu-item-css' => 'a06f7f34', 'people-profile-css' => '4df76faf', - 'phabricator-action-list-view-css' => 'f7f61a34', + 'phabricator-action-list-view-css' => '0bcd9a45', 'phabricator-busy' => '59a7976a', 'phabricator-chatlog-css' => 'd295b020', 'phabricator-content-source-view-css' => '4b8b05d4', @@ -777,8 +777,8 @@ return array( 'phabricator-darklog' => 'c8e1ffe3', 'phabricator-darkmessage' => 'c48cccdd', 'phabricator-dashboard-css' => 'fe5b1869', - 'phabricator-diff-changeset' => '99abf4cd', - 'phabricator-diff-changeset-list' => '3b77efdd', + 'phabricator-diff-changeset' => 'b49b59d6', + 'phabricator-diff-changeset-list' => '1f2e5265', 'phabricator-diff-inline' => 'e83d28f3', 'phabricator-drag-and-drop-file-upload' => '58dea2fa', 'phabricator-draggable-list' => 'bea6e7f4', @@ -786,12 +786,12 @@ return array( 'phabricator-favicon' => '1fe2510c', 'phabricator-feed-css' => 'ecd4ec57', 'phabricator-file-upload' => '680ea2c8', - 'phabricator-filetree-view-css' => 'fccf9f82', + 'phabricator-filetree-view-css' => 'b912ad97', 'phabricator-flag-css' => 'bba8f811', 'phabricator-keyboard-shortcut' => '1ae869f2', 'phabricator-keyboard-shortcut-manager' => 'c19dd9b9', 'phabricator-main-menu-view' => '7821ca89', - 'phabricator-nav-view-css' => 'faf6a6fc', + 'phabricator-nav-view-css' => '028126f6', 'phabricator-notification' => '008faf9c', 'phabricator-notification-css' => '457861ec', 'phabricator-notification-menu-css' => '10685bd4', @@ -876,7 +876,7 @@ return array( 'phui-status-list-view-css' => 'd5263e49', 'phui-tag-view-css' => 'b4719c50', 'phui-theme-css' => '9f261c6b', - 'phui-timeline-view-css' => 'e2ef62b1', + 'phui-timeline-view-css' => '6ddf8126', 'phui-two-column-view-css' => '44ec4951', 'phui-workboard-color-css' => '783cdff5', 'phui-workboard-view-css' => '3bc85455', @@ -887,7 +887,7 @@ return array( 'phuix-autocomplete' => 'e0731603', 'phuix-button-view' => '8a91e1ac', 'phuix-dropdown-menu' => '04b2ae03', - 'phuix-form-control-view' => '1dd0870c', + 'phuix-form-control-view' => '16ad6224', 'phuix-icon-view' => 'bff6884b', 'policy-css' => '957ea14c', 'policy-edit-css' => '815c66f7', @@ -998,6 +998,10 @@ return array( 'aphront-typeahead-control-css', 'phui-tag-view-css', ), + '16ad6224' => array( + 'javelin-install', + 'javelin-dom', + ), '17bb8539' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1029,10 +1033,6 @@ return array( 'javelin-request', 'javelin-uri', ), - '1dd0870c' => array( - 'javelin-install', - 'javelin-dom', - ), '1e911d0f' => array( 'javelin-stratcom', 'javelin-request', @@ -1044,6 +1044,10 @@ return array( 'javelin-uri', 'javelin-routable', ), + '1f2e5265' => array( + 'javelin-install', + 'phuix-button-view', + ), '1f6794f6' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1143,10 +1147,6 @@ return array( 'javelin-dom', 'javelin-magical-init', ), - '3b77efdd' => array( - 'javelin-install', - 'phuix-button-view', - ), '3cb0b2fc' => array( 'javelin-behavior', 'javelin-dom', @@ -1564,6 +1564,16 @@ return array( '7f243deb' => array( 'javelin-install', ), + '81144dfa' => array( + 'javelin-behavior', + 'javelin-behavior-device', + 'javelin-stratcom', + 'javelin-dom', + 'javelin-magical-init', + 'javelin-vector', + 'javelin-request', + 'javelin-util', + ), '834a1173' => array( 'javelin-behavior', 'javelin-scrollbar', @@ -1651,16 +1661,6 @@ return array( 'javelin-workflow', 'javelin-dom', ), - '947753e0' => array( - 'javelin-behavior', - 'javelin-behavior-device', - 'javelin-stratcom', - 'javelin-dom', - 'javelin-magical-init', - 'javelin-vector', - 'javelin-request', - 'javelin-util', - ), '949c0fe5' => array( 'javelin-install', ), @@ -1681,17 +1681,6 @@ return array( 'javelin-mask', 'phabricator-drag-and-drop-file-upload', ), - '99abf4cd' => array( - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-install', - 'javelin-workflow', - 'javelin-router', - 'javelin-behavior-device', - 'javelin-vector', - 'phabricator-diff-inline', - ), '9a6dd75c' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1840,6 +1829,17 @@ return array( 'b3e7d692' => array( 'javelin-install', ), + 'b49b59d6' => array( + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-install', + 'javelin-workflow', + 'javelin-router', + 'javelin-behavior-device', + 'javelin-vector', + 'phabricator-diff-inline', + ), 'b59e1e96' => array( 'javelin-behavior', 'javelin-stratcom', diff --git a/resources/emoji/manifest.json b/resources/emoji/manifest.json index 2d8388d277..47568fb43d 100644 --- a/resources/emoji/manifest.json +++ b/resources/emoji/manifest.json @@ -1622,5 +1622,9 @@ "zipper_mouth": "\ud83e\udd10", "zzz": "\ud83d\udca4", "100": "\ud83d\udcaf", - "1234": "\ud83d\udd22" + "1234": "\ud83d\udd22", + + "party": "\ud83c\udf89", + "celebration": "\ud83c\udf89", + "confetti": "\ud83c\udf89" } diff --git a/resources/sql/autopatches/20180207.mail.01.task.sql b/resources/sql/autopatches/20180207.mail.01.task.sql new file mode 100644 index 0000000000..f04b90c809 --- /dev/null +++ b/resources/sql/autopatches/20180207.mail.01.task.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_maniphest.maniphest_task + DROP originalTitle; diff --git a/resources/sql/autopatches/20180207.mail.02.revision.sql b/resources/sql/autopatches/20180207.mail.02.revision.sql new file mode 100644 index 0000000000..881efbcc94 --- /dev/null +++ b/resources/sql/autopatches/20180207.mail.02.revision.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_differential.differential_revision + DROP originalTitle; diff --git a/resources/sql/autopatches/20180207.mail.03.mock.sql b/resources/sql/autopatches/20180207.mail.03.mock.sql new file mode 100644 index 0000000000..360d7cf9a7 --- /dev/null +++ b/resources/sql/autopatches/20180207.mail.03.mock.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_pholio.pholio_mock + DROP originalName; diff --git a/resources/sql/autopatches/20180208.maniphest.01.close.sql b/resources/sql/autopatches/20180208.maniphest.01.close.sql new file mode 100644 index 0000000000..856300e9ba --- /dev/null +++ b/resources/sql/autopatches/20180208.maniphest.01.close.sql @@ -0,0 +1,5 @@ +ALTER TABLE {$NAMESPACE}_maniphest.maniphest_task + ADD closedEpoch INT UNSIGNED; + +ALTER TABLE {$NAMESPACE}_maniphest.maniphest_task + ADD closerPHID VARBINARY(64); diff --git a/resources/sql/autopatches/20180208.maniphest.02.populate.php b/resources/sql/autopatches/20180208.maniphest.02.populate.php new file mode 100644 index 0000000000..16aa2bf57b --- /dev/null +++ b/resources/sql/autopatches/20180208.maniphest.02.populate.php @@ -0,0 +1,65 @@ +establishConnection('w'); +$viewer = PhabricatorUser::getOmnipotentUser(); + +foreach (new LiskMigrationIterator($table) as $task) { + if ($task->getClosedEpoch()) { + // Task already has a closed date. + continue; + } + + $status = $task->getStatus(); + if (!ManiphestTaskStatus::isClosedStatus($status)) { + // Task isn't closed. + continue; + } + + // Look through the transactions from newest to oldest until we find one + // where the task was closed. A merge also counts as a close, even though + // it doesn't currently produce a separate transaction. + + $type_merge = ManiphestTaskStatusTransaction::TRANSACTIONTYPE; + $type_status = ManiphestTaskMergedIntoTransaction::TRANSACTIONTYPE; + + $xactions = id(new ManiphestTransactionQuery()) + ->setViewer($viewer) + ->withObjectPHIDs(array($task->getPHID())) + ->withTransactionTypes( + array( + $type_merge, + $type_status, + )) + ->execute(); + foreach ($xactions as $xaction) { + $old = $xaction->getOldValue(); + $new = $xaction->getNewValue(); + + $type = $xaction->getTransactionType(); + + // If this is a status change, but is not a close, don't use it. + // (We always use merges, even though it's possible to merge a task which + // was previously closed: we can't tell when this happens very easily.) + if ($type === $type_status) { + if (!ManiphestTaskStatus::isClosedStatus($new)) { + continue; + } + + if ($old && ManiphestTaskStatus::isClosedStatus($old)) { + continue; + } + } + + queryfx( + $conn, + 'UPDATE %T SET closedEpoch = %d, closerPHID = %ns + WHERE id = %d', + $table->getTableName(), + $xaction->getDateCreated(), + $xaction->getAuthorPHID(), + $task->getID()); + + break; + } +} diff --git a/scripts/setup/manage_conduit.php b/scripts/setup/manage_conduit.php new file mode 100755 index 0000000000..07384e7ed8 --- /dev/null +++ b/scripts/setup/manage_conduit.php @@ -0,0 +1,21 @@ +#!/usr/bin/env php +setTagline(pht('manage Conduit')); +$args->setSynopsis(<<parseStandardArguments(); + +$workflows = id(new PhutilClassMapQuery()) + ->setAncestorClass('PhabricatorConduitManagementWorkflow') + ->execute(); +$workflows[] = new PhutilHelpArgumentWorkflow(); +$args->parseWorkflows($workflows); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index f0b3114924..722eebbaae 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -487,6 +487,7 @@ phutil_register_library_map(array( 'DifferentialLintField' => 'applications/differential/customfield/DifferentialLintField.php', 'DifferentialLintStatus' => 'applications/differential/constants/DifferentialLintStatus.php', 'DifferentialLocalCommitsView' => 'applications/differential/view/DifferentialLocalCommitsView.php', + 'DifferentialMailEngineExtension' => 'applications/differential/engineextension/DifferentialMailEngineExtension.php', 'DifferentialMailView' => 'applications/differential/mail/DifferentialMailView.php', 'DifferentialManiphestTasksField' => 'applications/differential/customfield/DifferentialManiphestTasksField.php', 'DifferentialModernHunk' => 'applications/differential/storage/DifferentialModernHunk.php', @@ -1345,6 +1346,7 @@ phutil_register_library_map(array( 'HarbormasterWaitForPreviousBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterWaitForPreviousBuildStepImplementation.php', 'HarbormasterWorker' => 'applications/harbormaster/worker/HarbormasterWorker.php', 'HarbormasterWorkingCopyArtifact' => 'applications/harbormaster/artifact/HarbormasterWorkingCopyArtifact.php', + 'HeraldActingUserField' => 'applications/herald/field/HeraldActingUserField.php', 'HeraldAction' => 'applications/herald/action/HeraldAction.php', 'HeraldActionGroup' => 'applications/herald/action/HeraldActionGroup.php', 'HeraldActionRecord' => 'applications/herald/storage/HeraldActionRecord.php', @@ -1525,13 +1527,10 @@ phutil_register_library_map(array( 'ManiphestEditProjectsCapability' => 'applications/maniphest/capability/ManiphestEditProjectsCapability.php', 'ManiphestEditStatusCapability' => 'applications/maniphest/capability/ManiphestEditStatusCapability.php', 'ManiphestEmailCommand' => 'applications/maniphest/command/ManiphestEmailCommand.php', - 'ManiphestExcelDefaultFormat' => 'applications/maniphest/export/ManiphestExcelDefaultFormat.php', - 'ManiphestExcelFormat' => 'applications/maniphest/export/ManiphestExcelFormat.php', - 'ManiphestExcelFormatTestCase' => 'applications/maniphest/export/__tests__/ManiphestExcelFormatTestCase.php', - 'ManiphestExportController' => 'applications/maniphest/controller/ManiphestExportController.php', 'ManiphestGetTaskTransactionsConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestGetTaskTransactionsConduitAPIMethod.php', 'ManiphestHovercardEngineExtension' => 'applications/maniphest/engineextension/ManiphestHovercardEngineExtension.php', 'ManiphestInfoConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestInfoConduitAPIMethod.php', + 'ManiphestMailEngineExtension' => 'applications/maniphest/engineextension/ManiphestMailEngineExtension.php', 'ManiphestNameIndex' => 'applications/maniphest/storage/ManiphestNameIndex.php', 'ManiphestPointsConfigType' => 'applications/maniphest/config/ManiphestPointsConfigType.php', 'ManiphestPrioritiesConfigType' => 'applications/maniphest/config/ManiphestPrioritiesConfigType.php', @@ -1965,6 +1964,7 @@ phutil_register_library_map(array( 'PhabricatorApplicationEditHTTPParameterHelpView' => 'applications/transactions/view/PhabricatorApplicationEditHTTPParameterHelpView.php', 'PhabricatorApplicationEditor' => 'applications/meta/editor/PhabricatorApplicationEditor.php', 'PhabricatorApplicationEmailCommandsController' => 'applications/meta/controller/PhabricatorApplicationEmailCommandsController.php', + 'PhabricatorApplicationObjectMailEngineExtension' => 'applications/transactions/engineextension/PhabricatorApplicationObjectMailEngineExtension.php', 'PhabricatorApplicationPanelController' => 'applications/meta/controller/PhabricatorApplicationPanelController.php', 'PhabricatorApplicationPolicyChangeTransaction' => 'applications/meta/xactions/PhabricatorApplicationPolicyChangeTransaction.php', 'PhabricatorApplicationProfileMenuItem' => 'applications/search/menuitem/PhabricatorApplicationProfileMenuItem.php', @@ -2226,6 +2226,7 @@ phutil_register_library_map(array( 'PhabricatorBoardResponseEngine' => 'applications/project/engine/PhabricatorBoardResponseEngine.php', 'PhabricatorBoolConfigType' => 'applications/config/type/PhabricatorBoolConfigType.php', 'PhabricatorBoolEditField' => 'applications/transactions/editfield/PhabricatorBoolEditField.php', + 'PhabricatorBoolMailStamp' => 'applications/metamta/stamp/PhabricatorBoolMailStamp.php', 'PhabricatorBritishEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorBritishEnglishTranslation.php', 'PhabricatorBuildbotController' => 'extensions/buildbot/controller/PhabricatorBuildbotController.php', 'PhabricatorBuiltinDraftEngine' => 'applications/transactions/draft/PhabricatorBuiltinDraftEngine.php', @@ -2234,9 +2235,10 @@ phutil_register_library_map(array( 'PhabricatorBulkContentSource' => 'infrastructure/daemon/contentsource/PhabricatorBulkContentSource.php', 'PhabricatorBulkEditGroup' => 'applications/transactions/bulk/PhabricatorBulkEditGroup.php', 'PhabricatorBulkEngine' => 'applications/transactions/bulk/PhabricatorBulkEngine.php', + 'PhabricatorBulkManagementExportWorkflow' => 'applications/transactions/bulk/management/PhabricatorBulkManagementExportWorkflow.php', 'PhabricatorBulkManagementMakeSilentWorkflow' => 'applications/transactions/bulk/management/PhabricatorBulkManagementMakeSilentWorkflow.php', 'PhabricatorBulkManagementWorkflow' => 'applications/transactions/bulk/management/PhabricatorBulkManagementWorkflow.php', - 'PhabricatorCSVExportFormat' => 'infrastructure/export/PhabricatorCSVExportFormat.php', + 'PhabricatorCSVExportFormat' => 'infrastructure/export/format/PhabricatorCSVExportFormat.php', 'PhabricatorCacheDAO' => 'applications/cache/storage/PhabricatorCacheDAO.php', 'PhabricatorCacheEngine' => 'applications/system/engine/PhabricatorCacheEngine.php', 'PhabricatorCacheEngineExtension' => 'applications/system/engine/PhabricatorCacheEngineExtension.php', @@ -2414,6 +2416,7 @@ phutil_register_library_map(array( 'PhabricatorClusterExceptionHandler' => 'infrastructure/cluster/exception/PhabricatorClusterExceptionHandler.php', 'PhabricatorClusterImpossibleWriteException' => 'infrastructure/cluster/exception/PhabricatorClusterImpossibleWriteException.php', 'PhabricatorClusterImproperWriteException' => 'infrastructure/cluster/exception/PhabricatorClusterImproperWriteException.php', + 'PhabricatorClusterMailersConfigType' => 'infrastructure/cluster/config/PhabricatorClusterMailersConfigType.php', 'PhabricatorClusterNoHostForRoleException' => 'infrastructure/cluster/exception/PhabricatorClusterNoHostForRoleException.php', 'PhabricatorClusterSearchConfigType' => 'infrastructure/cluster/config/PhabricatorClusterSearchConfigType.php', 'PhabricatorClusterServiceHealthRecord' => 'infrastructure/cluster/PhabricatorClusterServiceHealthRecord.php', @@ -2432,6 +2435,7 @@ phutil_register_library_map(array( 'PhabricatorCommonPasswords' => 'applications/auth/constants/PhabricatorCommonPasswords.php', 'PhabricatorConduitAPIController' => 'applications/conduit/controller/PhabricatorConduitAPIController.php', 'PhabricatorConduitApplication' => 'applications/conduit/application/PhabricatorConduitApplication.php', + 'PhabricatorConduitCallManagementWorkflow' => 'applications/conduit/management/PhabricatorConduitCallManagementWorkflow.php', 'PhabricatorConduitCertificateToken' => 'applications/conduit/storage/PhabricatorConduitCertificateToken.php', 'PhabricatorConduitConsoleController' => 'applications/conduit/controller/PhabricatorConduitConsoleController.php', 'PhabricatorConduitContentSource' => 'infrastructure/contentsource/PhabricatorConduitContentSource.php', @@ -2442,6 +2446,7 @@ phutil_register_library_map(array( 'PhabricatorConduitLogController' => 'applications/conduit/controller/PhabricatorConduitLogController.php', 'PhabricatorConduitLogQuery' => 'applications/conduit/query/PhabricatorConduitLogQuery.php', 'PhabricatorConduitLogSearchEngine' => 'applications/conduit/query/PhabricatorConduitLogSearchEngine.php', + 'PhabricatorConduitManagementWorkflow' => 'applications/conduit/management/PhabricatorConduitManagementWorkflow.php', 'PhabricatorConduitMethodCallLog' => 'applications/conduit/storage/PhabricatorConduitMethodCallLog.php', 'PhabricatorConduitMethodQuery' => 'applications/conduit/query/PhabricatorConduitMethodQuery.php', 'PhabricatorConduitRequestExceptionHandler' => 'aphront/handler/PhabricatorConduitRequestExceptionHandler.php', @@ -2587,6 +2592,7 @@ phutil_register_library_map(array( 'PhabricatorCustomFieldEditEngineExtension' => 'infrastructure/customfield/engineextension/PhabricatorCustomFieldEditEngineExtension.php', 'PhabricatorCustomFieldEditField' => 'infrastructure/customfield/editor/PhabricatorCustomFieldEditField.php', 'PhabricatorCustomFieldEditType' => 'infrastructure/customfield/editor/PhabricatorCustomFieldEditType.php', + 'PhabricatorCustomFieldExportEngineExtension' => 'infrastructure/export/engine/PhabricatorCustomFieldExportEngineExtension.php', 'PhabricatorCustomFieldFulltextEngineExtension' => 'infrastructure/customfield/engineextension/PhabricatorCustomFieldFulltextEngineExtension.php', 'PhabricatorCustomFieldHeraldAction' => 'infrastructure/customfield/herald/PhabricatorCustomFieldHeraldAction.php', 'PhabricatorCustomFieldHeraldActionGroup' => 'infrastructure/customfield/herald/PhabricatorCustomFieldHeraldActionGroup.php', @@ -2751,6 +2757,8 @@ phutil_register_library_map(array( 'PhabricatorDraftEngine' => 'applications/transactions/draft/PhabricatorDraftEngine.php', 'PhabricatorDraftInterface' => 'applications/transactions/draft/PhabricatorDraftInterface.php', 'PhabricatorDrydockApplication' => 'applications/drydock/application/PhabricatorDrydockApplication.php', + 'PhabricatorEdgeChangeRecord' => 'infrastructure/edges/util/PhabricatorEdgeChangeRecord.php', + 'PhabricatorEdgeChangeRecordTestCase' => 'infrastructure/edges/__tests__/PhabricatorEdgeChangeRecordTestCase.php', 'PhabricatorEdgeConfig' => 'infrastructure/edges/constants/PhabricatorEdgeConfig.php', 'PhabricatorEdgeConstants' => 'infrastructure/edges/constants/PhabricatorEdgeConstants.php', 'PhabricatorEdgeCycleException' => 'infrastructure/edges/exception/PhabricatorEdgeCycleException.php', @@ -2814,6 +2822,7 @@ phutil_register_library_map(array( 'PhabricatorEditPage' => 'applications/transactions/editengine/PhabricatorEditPage.php', 'PhabricatorEditType' => 'applications/transactions/edittype/PhabricatorEditType.php', 'PhabricatorEditor' => 'infrastructure/PhabricatorEditor.php', + 'PhabricatorEditorMailEngineExtension' => 'applications/transactions/engineextension/PhabricatorEditorMailEngineExtension.php', 'PhabricatorEditorMultipleSetting' => 'applications/settings/setting/PhabricatorEditorMultipleSetting.php', 'PhabricatorEditorSetting' => 'applications/settings/setting/PhabricatorEditorSetting.php', 'PhabricatorElasticFulltextStorageEngine' => 'applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php', @@ -2830,6 +2839,7 @@ phutil_register_library_map(array( 'PhabricatorEmailPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorEmailPreferencesSettingsPanel.php', 'PhabricatorEmailRePrefixSetting' => 'applications/settings/setting/PhabricatorEmailRePrefixSetting.php', 'PhabricatorEmailSelfActionsSetting' => 'applications/settings/setting/PhabricatorEmailSelfActionsSetting.php', + 'PhabricatorEmailStampsSetting' => 'applications/settings/setting/PhabricatorEmailStampsSetting.php', 'PhabricatorEmailTagsSetting' => 'applications/settings/setting/PhabricatorEmailTagsSetting.php', 'PhabricatorEmailVarySubjectsSetting' => 'applications/settings/setting/PhabricatorEmailVarySubjectsSetting.php', 'PhabricatorEmailVerificationController' => 'applications/auth/controller/PhabricatorEmailVerificationController.php', @@ -2842,15 +2852,20 @@ phutil_register_library_map(array( 'PhabricatorEnv' => 'infrastructure/env/PhabricatorEnv.php', 'PhabricatorEnvTestCase' => 'infrastructure/env/__tests__/PhabricatorEnvTestCase.php', 'PhabricatorEpochEditField' => 'applications/transactions/editfield/PhabricatorEpochEditField.php', - 'PhabricatorEpochExportField' => 'infrastructure/export/PhabricatorEpochExportField.php', + 'PhabricatorEpochExportField' => 'infrastructure/export/field/PhabricatorEpochExportField.php', 'PhabricatorEvent' => 'infrastructure/events/PhabricatorEvent.php', 'PhabricatorEventEngine' => 'infrastructure/events/PhabricatorEventEngine.php', 'PhabricatorEventListener' => 'infrastructure/events/PhabricatorEventListener.php', 'PhabricatorEventType' => 'infrastructure/events/constant/PhabricatorEventType.php', 'PhabricatorExampleEventListener' => 'infrastructure/events/PhabricatorExampleEventListener.php', + 'PhabricatorExcelExportFormat' => 'infrastructure/export/format/PhabricatorExcelExportFormat.php', 'PhabricatorExecFutureFileUploadSource' => 'applications/files/uploadsource/PhabricatorExecFutureFileUploadSource.php', - 'PhabricatorExportField' => 'infrastructure/export/PhabricatorExportField.php', - 'PhabricatorExportFormat' => 'infrastructure/export/PhabricatorExportFormat.php', + 'PhabricatorExportEngine' => 'infrastructure/export/engine/PhabricatorExportEngine.php', + 'PhabricatorExportEngineBulkJobType' => 'infrastructure/export/engine/PhabricatorExportEngineBulkJobType.php', + 'PhabricatorExportEngineExtension' => 'infrastructure/export/engine/PhabricatorExportEngineExtension.php', + 'PhabricatorExportField' => 'infrastructure/export/field/PhabricatorExportField.php', + 'PhabricatorExportFormat' => 'infrastructure/export/format/PhabricatorExportFormat.php', + 'PhabricatorExportFormatSetting' => 'infrastructure/export/engine/PhabricatorExportFormatSetting.php', 'PhabricatorExtendedPolicyInterface' => 'applications/policy/interface/PhabricatorExtendedPolicyInterface.php', 'PhabricatorExtendingPhabricatorConfigOptions' => 'applications/config/option/PhabricatorExtendingPhabricatorConfigOptions.php', 'PhabricatorExtensionsSetupCheck' => 'applications/config/check/PhabricatorExtensionsSetupCheck.php', @@ -3020,6 +3035,7 @@ phutil_register_library_map(array( 'PhabricatorGDSetupCheck' => 'applications/config/check/PhabricatorGDSetupCheck.php', 'PhabricatorGarbageCollector' => 'infrastructure/daemon/garbagecollector/PhabricatorGarbageCollector.php', 'PhabricatorGarbageCollectorManagementCollectWorkflow' => 'infrastructure/daemon/garbagecollector/management/PhabricatorGarbageCollectorManagementCollectWorkflow.php', + 'PhabricatorGarbageCollectorManagementCompactEdgesWorkflow' => 'infrastructure/daemon/garbagecollector/management/PhabricatorGarbageCollectorManagementCompactEdgesWorkflow.php', 'PhabricatorGarbageCollectorManagementSetPolicyWorkflow' => 'infrastructure/daemon/garbagecollector/management/PhabricatorGarbageCollectorManagementSetPolicyWorkflow.php', 'PhabricatorGarbageCollectorManagementWorkflow' => 'infrastructure/daemon/garbagecollector/management/PhabricatorGarbageCollectorManagementWorkflow.php', 'PhabricatorGeneralCachePurger' => 'applications/cache/purger/PhabricatorGeneralCachePurger.php', @@ -3070,7 +3086,7 @@ phutil_register_library_map(array( 'PhabricatorHomeProfileMenuItem' => 'applications/home/menuitem/PhabricatorHomeProfileMenuItem.php', 'PhabricatorHovercardEngineExtension' => 'applications/search/engineextension/PhabricatorHovercardEngineExtension.php', 'PhabricatorHovercardEngineExtensionModule' => 'applications/search/engineextension/PhabricatorHovercardEngineExtensionModule.php', - 'PhabricatorIDExportField' => 'infrastructure/export/PhabricatorIDExportField.php', + 'PhabricatorIDExportField' => 'infrastructure/export/field/PhabricatorIDExportField.php', 'PhabricatorIDsSearchEngineExtension' => 'applications/search/engineextension/PhabricatorIDsSearchEngineExtension.php', 'PhabricatorIDsSearchField' => 'applications/search/field/PhabricatorIDsSearchField.php', 'PhabricatorIconDatasource' => 'applications/files/typeahead/PhabricatorIconDatasource.php', @@ -3094,7 +3110,7 @@ phutil_register_library_map(array( 'PhabricatorInlineSummaryView' => 'infrastructure/diff/view/PhabricatorInlineSummaryView.php', 'PhabricatorInstructionsEditField' => 'applications/transactions/editfield/PhabricatorInstructionsEditField.php', 'PhabricatorIntConfigType' => 'applications/config/type/PhabricatorIntConfigType.php', - 'PhabricatorIntExportField' => 'infrastructure/export/PhabricatorIntExportField.php', + 'PhabricatorIntExportField' => 'infrastructure/export/field/PhabricatorIntExportField.php', 'PhabricatorInternalSetting' => 'applications/settings/setting/PhabricatorInternalSetting.php', 'PhabricatorInternationalizationManagementExtractWorkflow' => 'infrastructure/internationalization/management/PhabricatorInternationalizationManagementExtractWorkflow.php', 'PhabricatorInternationalizationManagementWorkflow' => 'infrastructure/internationalization/management/PhabricatorInternationalizationManagementWorkflow.php', @@ -3104,7 +3120,7 @@ phutil_register_library_map(array( 'PhabricatorIteratorFileUploadSource' => 'applications/files/uploadsource/PhabricatorIteratorFileUploadSource.php', 'PhabricatorJIRAAuthProvider' => 'applications/auth/provider/PhabricatorJIRAAuthProvider.php', 'PhabricatorJSONConfigType' => 'applications/config/type/PhabricatorJSONConfigType.php', - 'PhabricatorJSONExportFormat' => 'infrastructure/export/PhabricatorJSONExportFormat.php', + 'PhabricatorJSONExportFormat' => 'infrastructure/export/format/PhabricatorJSONExportFormat.php', 'PhabricatorJavelinLinter' => 'infrastructure/lint/linter/PhabricatorJavelinLinter.php', 'PhabricatorJiraIssueHasObjectEdgeType' => 'applications/doorkeeper/edge/PhabricatorJiraIssueHasObjectEdgeType.php', 'PhabricatorJumpNavHandler' => 'applications/search/engine/PhabricatorJumpNavHandler.php', @@ -3127,9 +3143,11 @@ phutil_register_library_map(array( 'PhabricatorLipsumManagementWorkflow' => 'applications/lipsum/management/PhabricatorLipsumManagementWorkflow.php', 'PhabricatorLipsumMondrianArtist' => 'applications/lipsum/image/PhabricatorLipsumMondrianArtist.php', 'PhabricatorLiskDAO' => 'infrastructure/storage/lisk/PhabricatorLiskDAO.php', + 'PhabricatorLiskExportEngineExtension' => 'infrastructure/export/engine/PhabricatorLiskExportEngineExtension.php', 'PhabricatorLiskFulltextEngineExtension' => 'applications/search/engineextension/PhabricatorLiskFulltextEngineExtension.php', 'PhabricatorLiskSearchEngineExtension' => 'applications/search/engineextension/PhabricatorLiskSearchEngineExtension.php', 'PhabricatorLiskSerializer' => 'infrastructure/storage/lisk/PhabricatorLiskSerializer.php', + 'PhabricatorListExportField' => 'infrastructure/export/field/PhabricatorListExportField.php', 'PhabricatorLocalDiskFileStorageEngine' => 'applications/files/engine/PhabricatorLocalDiskFileStorageEngine.php', 'PhabricatorLocalTimeTestCase' => 'view/__tests__/PhabricatorLocalTimeTestCase.php', 'PhabricatorLocaleScopeGuard' => 'infrastructure/internationalization/scope/PhabricatorLocaleScopeGuard.php', @@ -3165,14 +3183,17 @@ phutil_register_library_map(array( 'PhabricatorMacroTransactionQuery' => 'applications/macro/query/PhabricatorMacroTransactionQuery.php', 'PhabricatorMacroTransactionType' => 'applications/macro/xaction/PhabricatorMacroTransactionType.php', 'PhabricatorMacroViewController' => 'applications/macro/controller/PhabricatorMacroViewController.php', + 'PhabricatorMailConfigTestCase' => 'applications/metamta/storage/__tests__/PhabricatorMailConfigTestCase.php', 'PhabricatorMailEmailHeraldField' => 'applications/metamta/herald/PhabricatorMailEmailHeraldField.php', 'PhabricatorMailEmailHeraldFieldGroup' => 'applications/metamta/herald/PhabricatorMailEmailHeraldFieldGroup.php', 'PhabricatorMailEmailSubjectHeraldField' => 'applications/metamta/herald/PhabricatorMailEmailSubjectHeraldField.php', + 'PhabricatorMailEngineExtension' => 'applications/metamta/engine/PhabricatorMailEngineExtension.php', 'PhabricatorMailImplementationAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationAdapter.php', 'PhabricatorMailImplementationAmazonSESAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationAmazonSESAdapter.php', 'PhabricatorMailImplementationMailgunAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php', 'PhabricatorMailImplementationPHPMailerAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationPHPMailerAdapter.php', 'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php', + 'PhabricatorMailImplementationPostmarkAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationPostmarkAdapter.php', 'PhabricatorMailImplementationSendGridAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php', 'PhabricatorMailImplementationTestAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php', 'PhabricatorMailManagementListInboundWorkflow' => 'applications/metamta/management/PhabricatorMailManagementListInboundWorkflow.php', @@ -3185,6 +3206,7 @@ phutil_register_library_map(array( 'PhabricatorMailManagementUnverifyWorkflow' => 'applications/metamta/management/PhabricatorMailManagementUnverifyWorkflow.php', 'PhabricatorMailManagementVolumeWorkflow' => 'applications/metamta/management/PhabricatorMailManagementVolumeWorkflow.php', 'PhabricatorMailManagementWorkflow' => 'applications/metamta/management/PhabricatorMailManagementWorkflow.php', + 'PhabricatorMailMustEncryptHeraldAction' => 'applications/metamta/herald/PhabricatorMailMustEncryptHeraldAction.php', 'PhabricatorMailOutboundMailHeraldAdapter' => 'applications/metamta/herald/PhabricatorMailOutboundMailHeraldAdapter.php', 'PhabricatorMailOutboundRoutingHeraldAction' => 'applications/metamta/herald/PhabricatorMailOutboundRoutingHeraldAction.php', 'PhabricatorMailOutboundRoutingSelfEmailHeraldAction' => 'applications/metamta/herald/PhabricatorMailOutboundRoutingSelfEmailHeraldAction.php', @@ -3195,6 +3217,7 @@ phutil_register_library_map(array( 'PhabricatorMailReplyHandler' => 'applications/metamta/replyhandler/PhabricatorMailReplyHandler.php', 'PhabricatorMailRoutingRule' => 'applications/metamta/constants/PhabricatorMailRoutingRule.php', 'PhabricatorMailSetupCheck' => 'applications/config/check/PhabricatorMailSetupCheck.php', + 'PhabricatorMailStamp' => 'applications/metamta/stamp/PhabricatorMailStamp.php', 'PhabricatorMailTarget' => 'applications/metamta/replyhandler/PhabricatorMailTarget.php', 'PhabricatorMailgunConfigOptions' => 'applications/config/option/PhabricatorMailgunConfigOptions.php', 'PhabricatorMainMenuBarExtension' => 'view/page/menu/PhabricatorMainMenuBarExtension.php', @@ -3254,6 +3277,7 @@ phutil_register_library_map(array( 'PhabricatorMetaMTAMailgunReceiveController' => 'applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php', 'PhabricatorMetaMTAMemberQuery' => 'applications/metamta/query/PhabricatorMetaMTAMemberQuery.php', 'PhabricatorMetaMTAPermanentFailureException' => 'applications/metamta/exception/PhabricatorMetaMTAPermanentFailureException.php', + 'PhabricatorMetaMTAPostmarkReceiveController' => 'applications/metamta/controller/PhabricatorMetaMTAPostmarkReceiveController.php', 'PhabricatorMetaMTAReceivedMail' => 'applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php', 'PhabricatorMetaMTAReceivedMailProcessingException' => 'applications/metamta/exception/PhabricatorMetaMTAReceivedMailProcessingException.php', 'PhabricatorMetaMTAReceivedMailTestCase' => 'applications/metamta/storage/__tests__/PhabricatorMetaMTAReceivedMailTestCase.php', @@ -3271,6 +3295,8 @@ phutil_register_library_map(array( 'PhabricatorMultiFactorSettingsPanel' => 'applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php', 'PhabricatorMultimeterApplication' => 'applications/multimeter/application/PhabricatorMultimeterApplication.php', 'PhabricatorMustVerifyEmailController' => 'applications/auth/controller/PhabricatorMustVerifyEmailController.php', + 'PhabricatorMutedByEdgeType' => 'applications/transactions/edges/PhabricatorMutedByEdgeType.php', + 'PhabricatorMutedEdgeType' => 'applications/transactions/edges/PhabricatorMutedEdgeType.php', 'PhabricatorMySQLConfigOptions' => 'applications/config/option/PhabricatorMySQLConfigOptions.php', 'PhabricatorMySQLFileStorageEngine' => 'applications/files/engine/PhabricatorMySQLFileStorageEngine.php', 'PhabricatorMySQLSearchHost' => 'infrastructure/cluster/search/PhabricatorMySQLSearchHost.php', @@ -3424,10 +3450,12 @@ phutil_register_library_map(array( 'PhabricatorPHDConfigOptions' => 'applications/config/option/PhabricatorPHDConfigOptions.php', 'PhabricatorPHID' => 'applications/phid/storage/PhabricatorPHID.php', 'PhabricatorPHIDConstants' => 'applications/phid/PhabricatorPHIDConstants.php', - 'PhabricatorPHIDExportField' => 'infrastructure/export/PhabricatorPHIDExportField.php', + 'PhabricatorPHIDExportField' => 'infrastructure/export/field/PhabricatorPHIDExportField.php', 'PhabricatorPHIDInterface' => 'applications/phid/interface/PhabricatorPHIDInterface.php', 'PhabricatorPHIDListEditField' => 'applications/transactions/editfield/PhabricatorPHIDListEditField.php', 'PhabricatorPHIDListEditType' => 'applications/transactions/edittype/PhabricatorPHIDListEditType.php', + 'PhabricatorPHIDListExportField' => 'infrastructure/export/field/PhabricatorPHIDListExportField.php', + 'PhabricatorPHIDMailStamp' => 'applications/metamta/stamp/PhabricatorPHIDMailStamp.php', 'PhabricatorPHIDResolver' => 'applications/phid/resolver/PhabricatorPHIDResolver.php', 'PhabricatorPHIDType' => 'applications/phid/type/PhabricatorPHIDType.php', 'PhabricatorPHIDTypeTestCase' => 'applications/phid/type/__tests__/PhabricatorPHIDTypeTestCase.php', @@ -3836,7 +3864,9 @@ phutil_register_library_map(array( 'PhabricatorProjectsCurtainExtension' => 'applications/project/engineextension/PhabricatorProjectsCurtainExtension.php', 'PhabricatorProjectsEditEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsEditEngineExtension.php', 'PhabricatorProjectsEditField' => 'applications/transactions/editfield/PhabricatorProjectsEditField.php', + 'PhabricatorProjectsExportEngineExtension' => 'infrastructure/export/engine/PhabricatorProjectsExportEngineExtension.php', 'PhabricatorProjectsFulltextEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsFulltextEngineExtension.php', + 'PhabricatorProjectsMailEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsMailEngineExtension.php', 'PhabricatorProjectsMembersSearchEngineAttachment' => 'applications/project/engineextension/PhabricatorProjectsMembersSearchEngineAttachment.php', 'PhabricatorProjectsMembershipIndexEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsMembershipIndexEngineExtension.php', 'PhabricatorProjectsPolicyRule' => 'applications/project/policyrule/PhabricatorProjectsPolicyRule.php', @@ -4128,8 +4158,10 @@ phutil_register_library_map(array( 'PhabricatorSpacesController' => 'applications/spaces/controller/PhabricatorSpacesController.php', 'PhabricatorSpacesDAO' => 'applications/spaces/storage/PhabricatorSpacesDAO.php', 'PhabricatorSpacesEditController' => 'applications/spaces/controller/PhabricatorSpacesEditController.php', + 'PhabricatorSpacesExportEngineExtension' => 'infrastructure/export/engine/PhabricatorSpacesExportEngineExtension.php', 'PhabricatorSpacesInterface' => 'applications/spaces/interface/PhabricatorSpacesInterface.php', 'PhabricatorSpacesListController' => 'applications/spaces/controller/PhabricatorSpacesListController.php', + 'PhabricatorSpacesMailEngineExtension' => 'applications/spaces/engineextension/PhabricatorSpacesMailEngineExtension.php', 'PhabricatorSpacesNamespace' => 'applications/spaces/storage/PhabricatorSpacesNamespace.php', 'PhabricatorSpacesNamespaceArchiveTransaction' => 'applications/spaces/xaction/PhabricatorSpacesNamespaceArchiveTransaction.php', 'PhabricatorSpacesNamespaceDatasource' => 'applications/spaces/typeahead/PhabricatorSpacesNamespaceDatasource.php', @@ -4191,9 +4223,11 @@ phutil_register_library_map(array( 'PhabricatorStorageSchemaSpec' => 'infrastructure/storage/schema/PhabricatorStorageSchemaSpec.php', 'PhabricatorStorageSetupCheck' => 'applications/config/check/PhabricatorStorageSetupCheck.php', 'PhabricatorStringConfigType' => 'applications/config/type/PhabricatorStringConfigType.php', - 'PhabricatorStringExportField' => 'infrastructure/export/PhabricatorStringExportField.php', + 'PhabricatorStringExportField' => 'infrastructure/export/field/PhabricatorStringExportField.php', 'PhabricatorStringListConfigType' => 'applications/config/type/PhabricatorStringListConfigType.php', 'PhabricatorStringListEditField' => 'applications/transactions/editfield/PhabricatorStringListEditField.php', + 'PhabricatorStringListExportField' => 'infrastructure/export/field/PhabricatorStringListExportField.php', + 'PhabricatorStringMailStamp' => 'applications/metamta/stamp/PhabricatorStringMailStamp.php', 'PhabricatorStringSetting' => 'applications/settings/setting/PhabricatorStringSetting.php', 'PhabricatorSubmitEditField' => 'applications/transactions/editfield/PhabricatorSubmitEditField.php', 'PhabricatorSubscribableInterface' => 'applications/subscriptions/interface/PhabricatorSubscribableInterface.php', @@ -4208,9 +4242,12 @@ phutil_register_library_map(array( 'PhabricatorSubscriptionsEditController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php', 'PhabricatorSubscriptionsEditEngineExtension' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsEditEngineExtension.php', 'PhabricatorSubscriptionsEditor' => 'applications/subscriptions/editor/PhabricatorSubscriptionsEditor.php', + 'PhabricatorSubscriptionsExportEngineExtension' => 'infrastructure/export/engine/PhabricatorSubscriptionsExportEngineExtension.php', 'PhabricatorSubscriptionsFulltextEngineExtension' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsFulltextEngineExtension.php', 'PhabricatorSubscriptionsHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsHeraldAction.php', 'PhabricatorSubscriptionsListController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsListController.php', + 'PhabricatorSubscriptionsMailEngineExtension' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsMailEngineExtension.php', + 'PhabricatorSubscriptionsMuteController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsMuteController.php', 'PhabricatorSubscriptionsRemoveSelfHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsRemoveSelfHeraldAction.php', 'PhabricatorSubscriptionsRemoveSubscribersHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsRemoveSubscribersHeraldAction.php', 'PhabricatorSubscriptionsSearchEngineAttachment' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsSearchEngineAttachment.php', @@ -4255,7 +4292,7 @@ phutil_register_library_map(array( 'PhabricatorTextAreaEditField' => 'applications/transactions/editfield/PhabricatorTextAreaEditField.php', 'PhabricatorTextConfigType' => 'applications/config/type/PhabricatorTextConfigType.php', 'PhabricatorTextEditField' => 'applications/transactions/editfield/PhabricatorTextEditField.php', - 'PhabricatorTextExportFormat' => 'infrastructure/export/PhabricatorTextExportFormat.php', + 'PhabricatorTextExportFormat' => 'infrastructure/export/format/PhabricatorTextExportFormat.php', 'PhabricatorTextListConfigType' => 'applications/config/type/PhabricatorTextListConfigType.php', 'PhabricatorTime' => 'infrastructure/time/PhabricatorTime.php', 'PhabricatorTimeFormatSetting' => 'applications/settings/setting/PhabricatorTimeFormatSetting.php', @@ -4320,6 +4357,7 @@ phutil_register_library_map(array( 'PhabricatorUIExample' => 'applications/uiexample/examples/PhabricatorUIExample.php', 'PhabricatorUIExampleRenderController' => 'applications/uiexample/controller/PhabricatorUIExampleRenderController.php', 'PhabricatorUIExamplesApplication' => 'applications/uiexample/application/PhabricatorUIExamplesApplication.php', + 'PhabricatorURIExportField' => 'infrastructure/export/field/PhabricatorURIExportField.php', 'PhabricatorUSEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php', 'PhabricatorUnifiedDiffsSetting' => 'applications/settings/setting/PhabricatorUnifiedDiffsSetting.php', 'PhabricatorUnitTestContentSource' => 'infrastructure/contentsource/PhabricatorUnitTestContentSource.php', @@ -4413,6 +4451,7 @@ phutil_register_library_map(array( 'PhabricatorWorkerManagementWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementWorkflow.php', 'PhabricatorWorkerPermanentFailureException' => 'infrastructure/daemon/workers/exception/PhabricatorWorkerPermanentFailureException.php', 'PhabricatorWorkerSchemaSpec' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerSchemaSpec.php', + 'PhabricatorWorkerSingleBulkJobType' => 'infrastructure/daemon/workers/bulk/PhabricatorWorkerSingleBulkJobType.php', 'PhabricatorWorkerTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTask.php', 'PhabricatorWorkerTaskData' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTaskData.php', 'PhabricatorWorkerTaskDetailController' => 'applications/daemon/controller/PhabricatorWorkerTaskDetailController.php', @@ -5588,6 +5627,7 @@ phutil_register_library_map(array( 'DifferentialLintField' => 'DifferentialHarbormasterField', 'DifferentialLintStatus' => 'Phobject', 'DifferentialLocalCommitsView' => 'AphrontView', + 'DifferentialMailEngineExtension' => 'PhabricatorMailEngineExtension', 'DifferentialMailView' => 'Phobject', 'DifferentialManiphestTasksField' => 'DifferentialCoreCustomField', 'DifferentialModernHunk' => 'DifferentialHunk', @@ -6560,6 +6600,7 @@ phutil_register_library_map(array( 'HarbormasterWaitForPreviousBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterWorker' => 'PhabricatorWorker', 'HarbormasterWorkingCopyArtifact' => 'HarbormasterDrydockLeaseArtifact', + 'HeraldActingUserField' => 'HeraldField', 'HeraldAction' => 'Phobject', 'HeraldActionGroup' => 'HeraldGroup', 'HeraldActionRecord' => 'HeraldDAO', @@ -6772,13 +6813,10 @@ phutil_register_library_map(array( 'ManiphestEditProjectsCapability' => 'PhabricatorPolicyCapability', 'ManiphestEditStatusCapability' => 'PhabricatorPolicyCapability', 'ManiphestEmailCommand' => 'MetaMTAEmailTransactionCommand', - 'ManiphestExcelDefaultFormat' => 'ManiphestExcelFormat', - 'ManiphestExcelFormat' => 'Phobject', - 'ManiphestExcelFormatTestCase' => 'PhabricatorTestCase', - 'ManiphestExportController' => 'ManiphestController', 'ManiphestGetTaskTransactionsConduitAPIMethod' => 'ManiphestConduitAPIMethod', 'ManiphestHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension', 'ManiphestInfoConduitAPIMethod' => 'ManiphestConduitAPIMethod', + 'ManiphestMailEngineExtension' => 'PhabricatorMailEngineExtension', 'ManiphestNameIndex' => 'ManiphestDAO', 'ManiphestPointsConfigType' => 'PhabricatorJSONConfigType', 'ManiphestPrioritiesConfigType' => 'PhabricatorJSONConfigType', @@ -7265,6 +7303,7 @@ phutil_register_library_map(array( 'PhabricatorApplicationEditHTTPParameterHelpView' => 'AphrontView', 'PhabricatorApplicationEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorApplicationEmailCommandsController' => 'PhabricatorApplicationsController', + 'PhabricatorApplicationObjectMailEngineExtension' => 'PhabricatorMailEngineExtension', 'PhabricatorApplicationPanelController' => 'PhabricatorApplicationsController', 'PhabricatorApplicationPolicyChangeTransaction' => 'PhabricatorApplicationTransactionType', 'PhabricatorApplicationProfileMenuItem' => 'PhabricatorProfileMenuItem', @@ -7572,6 +7611,7 @@ phutil_register_library_map(array( 'PhabricatorBoardResponseEngine' => 'Phobject', 'PhabricatorBoolConfigType' => 'PhabricatorTextConfigType', 'PhabricatorBoolEditField' => 'PhabricatorEditField', + 'PhabricatorBoolMailStamp' => 'PhabricatorMailStamp', 'PhabricatorBritishEnglishTranslation' => 'PhutilTranslation', 'PhabricatorBuildbotController' => 'PhabricatorController', 'PhabricatorBuiltinDraftEngine' => 'PhabricatorDraftEngine', @@ -7580,6 +7620,7 @@ phutil_register_library_map(array( 'PhabricatorBulkContentSource' => 'PhabricatorContentSource', 'PhabricatorBulkEditGroup' => 'Phobject', 'PhabricatorBulkEngine' => 'Phobject', + 'PhabricatorBulkManagementExportWorkflow' => 'PhabricatorBulkManagementWorkflow', 'PhabricatorBulkManagementMakeSilentWorkflow' => 'PhabricatorBulkManagementWorkflow', 'PhabricatorBulkManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorCSVExportFormat' => 'PhabricatorExportFormat', @@ -7803,6 +7844,7 @@ phutil_register_library_map(array( 'PhabricatorClusterExceptionHandler' => 'PhabricatorRequestExceptionHandler', 'PhabricatorClusterImpossibleWriteException' => 'PhabricatorClusterException', 'PhabricatorClusterImproperWriteException' => 'PhabricatorClusterException', + 'PhabricatorClusterMailersConfigType' => 'PhabricatorJSONConfigType', 'PhabricatorClusterNoHostForRoleException' => 'Exception', 'PhabricatorClusterSearchConfigType' => 'PhabricatorJSONConfigType', 'PhabricatorClusterServiceHealthRecord' => 'Phobject', @@ -7820,6 +7862,7 @@ phutil_register_library_map(array( 'PhabricatorCommonPasswords' => 'Phobject', 'PhabricatorConduitAPIController' => 'PhabricatorConduitController', 'PhabricatorConduitApplication' => 'PhabricatorApplication', + 'PhabricatorConduitCallManagementWorkflow' => 'PhabricatorConduitManagementWorkflow', 'PhabricatorConduitCertificateToken' => 'PhabricatorConduitDAO', 'PhabricatorConduitConsoleController' => 'PhabricatorConduitController', 'PhabricatorConduitContentSource' => 'PhabricatorContentSource', @@ -7830,6 +7873,7 @@ phutil_register_library_map(array( 'PhabricatorConduitLogController' => 'PhabricatorConduitController', 'PhabricatorConduitLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorConduitLogSearchEngine' => 'PhabricatorApplicationSearchEngine', + 'PhabricatorConduitManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorConduitMethodCallLog' => array( 'PhabricatorConduitDAO', 'PhabricatorPolicyInterface', @@ -7999,6 +8043,7 @@ phutil_register_library_map(array( 'PhabricatorCustomFieldEditEngineExtension' => 'PhabricatorEditEngineExtension', 'PhabricatorCustomFieldEditField' => 'PhabricatorEditField', 'PhabricatorCustomFieldEditType' => 'PhabricatorEditType', + 'PhabricatorCustomFieldExportEngineExtension' => 'PhabricatorExportEngineExtension', 'PhabricatorCustomFieldFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension', 'PhabricatorCustomFieldHeraldAction' => 'HeraldAction', 'PhabricatorCustomFieldHeraldActionGroup' => 'HeraldActionGroup', @@ -8181,6 +8226,8 @@ phutil_register_library_map(array( 'PhabricatorDraftDAO' => 'PhabricatorLiskDAO', 'PhabricatorDraftEngine' => 'Phobject', 'PhabricatorDrydockApplication' => 'PhabricatorApplication', + 'PhabricatorEdgeChangeRecord' => 'Phobject', + 'PhabricatorEdgeChangeRecordTestCase' => 'PhabricatorTestCase', 'PhabricatorEdgeConfig' => 'PhabricatorEdgeConstants', 'PhabricatorEdgeConstants' => 'Phobject', 'PhabricatorEdgeCycleException' => 'Exception', @@ -8252,6 +8299,7 @@ phutil_register_library_map(array( 'PhabricatorEditPage' => 'Phobject', 'PhabricatorEditType' => 'Phobject', 'PhabricatorEditor' => 'Phobject', + 'PhabricatorEditorMailEngineExtension' => 'PhabricatorMailEngineExtension', 'PhabricatorEditorMultipleSetting' => 'PhabricatorSelectSetting', 'PhabricatorEditorSetting' => 'PhabricatorStringSetting', 'PhabricatorElasticFulltextStorageEngine' => 'PhabricatorFulltextStorageEngine', @@ -8267,6 +8315,7 @@ phutil_register_library_map(array( 'PhabricatorEmailPreferencesSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorEmailRePrefixSetting' => 'PhabricatorSelectSetting', 'PhabricatorEmailSelfActionsSetting' => 'PhabricatorSelectSetting', + 'PhabricatorEmailStampsSetting' => 'PhabricatorSelectSetting', 'PhabricatorEmailTagsSetting' => 'PhabricatorInternalSetting', 'PhabricatorEmailVarySubjectsSetting' => 'PhabricatorSelectSetting', 'PhabricatorEmailVerificationController' => 'PhabricatorAuthController', @@ -8285,9 +8334,14 @@ phutil_register_library_map(array( 'PhabricatorEventListener' => 'PhutilEventListener', 'PhabricatorEventType' => 'PhutilEventType', 'PhabricatorExampleEventListener' => 'PhabricatorEventListener', + 'PhabricatorExcelExportFormat' => 'PhabricatorExportFormat', 'PhabricatorExecFutureFileUploadSource' => 'PhabricatorFileUploadSource', + 'PhabricatorExportEngine' => 'Phobject', + 'PhabricatorExportEngineBulkJobType' => 'PhabricatorWorkerSingleBulkJobType', + 'PhabricatorExportEngineExtension' => 'Phobject', 'PhabricatorExportField' => 'Phobject', 'PhabricatorExportFormat' => 'Phobject', + 'PhabricatorExportFormatSetting' => 'PhabricatorInternalSetting', 'PhabricatorExtendingPhabricatorConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorExtensionsSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorExternalAccount' => array( @@ -8493,6 +8547,7 @@ phutil_register_library_map(array( 'PhabricatorGDSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorGarbageCollector' => 'Phobject', 'PhabricatorGarbageCollectorManagementCollectWorkflow' => 'PhabricatorGarbageCollectorManagementWorkflow', + 'PhabricatorGarbageCollectorManagementCompactEdgesWorkflow' => 'PhabricatorGarbageCollectorManagementWorkflow', 'PhabricatorGarbageCollectorManagementSetPolicyWorkflow' => 'PhabricatorGarbageCollectorManagementWorkflow', 'PhabricatorGarbageCollectorManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorGeneralCachePurger' => 'PhabricatorCachePurger', @@ -8604,9 +8659,11 @@ phutil_register_library_map(array( 'PhabricatorLipsumManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorLipsumMondrianArtist' => 'PhabricatorLipsumArtist', 'PhabricatorLiskDAO' => 'LiskDAO', + 'PhabricatorLiskExportEngineExtension' => 'PhabricatorExportEngineExtension', 'PhabricatorLiskFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension', 'PhabricatorLiskSearchEngineExtension' => 'PhabricatorSearchEngineExtension', 'PhabricatorLiskSerializer' => 'Phobject', + 'PhabricatorListExportField' => 'PhabricatorExportField', 'PhabricatorLocalDiskFileStorageEngine' => 'PhabricatorFileStorageEngine', 'PhabricatorLocalTimeTestCase' => 'PhabricatorTestCase', 'PhabricatorLocaleScopeGuard' => 'Phobject', @@ -8642,14 +8699,17 @@ phutil_register_library_map(array( 'PhabricatorMacroTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorMacroTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorMacroViewController' => 'PhabricatorMacroController', + 'PhabricatorMailConfigTestCase' => 'PhabricatorTestCase', 'PhabricatorMailEmailHeraldField' => 'HeraldField', 'PhabricatorMailEmailHeraldFieldGroup' => 'HeraldFieldGroup', 'PhabricatorMailEmailSubjectHeraldField' => 'PhabricatorMailEmailHeraldField', + 'PhabricatorMailEngineExtension' => 'Phobject', 'PhabricatorMailImplementationAdapter' => 'Phobject', 'PhabricatorMailImplementationAmazonSESAdapter' => 'PhabricatorMailImplementationPHPMailerLiteAdapter', 'PhabricatorMailImplementationMailgunAdapter' => 'PhabricatorMailImplementationAdapter', 'PhabricatorMailImplementationPHPMailerAdapter' => 'PhabricatorMailImplementationAdapter', 'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'PhabricatorMailImplementationAdapter', + 'PhabricatorMailImplementationPostmarkAdapter' => 'PhabricatorMailImplementationAdapter', 'PhabricatorMailImplementationSendGridAdapter' => 'PhabricatorMailImplementationAdapter', 'PhabricatorMailImplementationTestAdapter' => 'PhabricatorMailImplementationAdapter', 'PhabricatorMailManagementListInboundWorkflow' => 'PhabricatorMailManagementWorkflow', @@ -8662,6 +8722,7 @@ phutil_register_library_map(array( 'PhabricatorMailManagementUnverifyWorkflow' => 'PhabricatorMailManagementWorkflow', 'PhabricatorMailManagementVolumeWorkflow' => 'PhabricatorMailManagementWorkflow', 'PhabricatorMailManagementWorkflow' => 'PhabricatorManagementWorkflow', + 'PhabricatorMailMustEncryptHeraldAction' => 'HeraldAction', 'PhabricatorMailOutboundMailHeraldAdapter' => 'HeraldAdapter', 'PhabricatorMailOutboundRoutingHeraldAction' => 'HeraldAction', 'PhabricatorMailOutboundRoutingSelfEmailHeraldAction' => 'PhabricatorMailOutboundRoutingHeraldAction', @@ -8672,6 +8733,7 @@ phutil_register_library_map(array( 'PhabricatorMailReplyHandler' => 'Phobject', 'PhabricatorMailRoutingRule' => 'Phobject', 'PhabricatorMailSetupCheck' => 'PhabricatorSetupCheck', + 'PhabricatorMailStamp' => 'Phobject', 'PhabricatorMailTarget' => 'Phobject', 'PhabricatorMailgunConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorMainMenuBarExtension' => 'Phobject', @@ -8725,6 +8787,7 @@ phutil_register_library_map(array( 'PhabricatorMetaMTAMail' => array( 'PhabricatorMetaMTADAO', 'PhabricatorPolicyInterface', + 'PhabricatorDestructibleInterface', ), 'PhabricatorMetaMTAMailBody' => 'Phobject', 'PhabricatorMetaMTAMailBodyTestCase' => 'PhabricatorTestCase', @@ -8741,6 +8804,7 @@ phutil_register_library_map(array( 'PhabricatorMetaMTAMailgunReceiveController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTAMemberQuery' => 'PhabricatorQuery', 'PhabricatorMetaMTAPermanentFailureException' => 'Exception', + 'PhabricatorMetaMTAPostmarkReceiveController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTAReceivedMail' => 'PhabricatorMetaMTADAO', 'PhabricatorMetaMTAReceivedMailProcessingException' => 'Exception', 'PhabricatorMetaMTAReceivedMailTestCase' => 'PhabricatorTestCase', @@ -8758,6 +8822,8 @@ phutil_register_library_map(array( 'PhabricatorMultiFactorSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorMultimeterApplication' => 'PhabricatorApplication', 'PhabricatorMustVerifyEmailController' => 'PhabricatorAuthController', + 'PhabricatorMutedByEdgeType' => 'PhabricatorEdgeType', + 'PhabricatorMutedEdgeType' => 'PhabricatorEdgeType', 'PhabricatorMySQLConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorMySQLFileStorageEngine' => 'PhabricatorFileStorageEngine', 'PhabricatorMySQLSearchHost' => 'PhabricatorSearchHost', @@ -8944,6 +9010,8 @@ phutil_register_library_map(array( 'PhabricatorPHIDExportField' => 'PhabricatorExportField', 'PhabricatorPHIDListEditField' => 'PhabricatorEditField', 'PhabricatorPHIDListEditType' => 'PhabricatorEditType', + 'PhabricatorPHIDListExportField' => 'PhabricatorListExportField', + 'PhabricatorPHIDMailStamp' => 'PhabricatorMailStamp', 'PhabricatorPHIDResolver' => 'Phobject', 'PhabricatorPHIDType' => 'Phobject', 'PhabricatorPHIDTypeTestCase' => 'PhutilTestCase', @@ -9444,7 +9512,9 @@ phutil_register_library_map(array( 'PhabricatorProjectsCurtainExtension' => 'PHUICurtainExtension', 'PhabricatorProjectsEditEngineExtension' => 'PhabricatorEditEngineExtension', 'PhabricatorProjectsEditField' => 'PhabricatorTokenizerEditField', + 'PhabricatorProjectsExportEngineExtension' => 'PhabricatorExportEngineExtension', 'PhabricatorProjectsFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension', + 'PhabricatorProjectsMailEngineExtension' => 'PhabricatorMailEngineExtension', 'PhabricatorProjectsMembersSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', 'PhabricatorProjectsMembershipIndexEngineExtension' => 'PhabricatorIndexEngineExtension', 'PhabricatorProjectsPolicyRule' => 'PhabricatorPolicyRule', @@ -9812,8 +9882,10 @@ phutil_register_library_map(array( 'PhabricatorSpacesController' => 'PhabricatorController', 'PhabricatorSpacesDAO' => 'PhabricatorLiskDAO', 'PhabricatorSpacesEditController' => 'PhabricatorSpacesController', + 'PhabricatorSpacesExportEngineExtension' => 'PhabricatorExportEngineExtension', 'PhabricatorSpacesInterface' => 'PhabricatorPHIDInterface', 'PhabricatorSpacesListController' => 'PhabricatorSpacesController', + 'PhabricatorSpacesMailEngineExtension' => 'PhabricatorMailEngineExtension', 'PhabricatorSpacesNamespace' => array( 'PhabricatorSpacesDAO', 'PhabricatorPolicyInterface', @@ -9885,6 +9957,8 @@ phutil_register_library_map(array( 'PhabricatorStringExportField' => 'PhabricatorExportField', 'PhabricatorStringListConfigType' => 'PhabricatorTextListConfigType', 'PhabricatorStringListEditField' => 'PhabricatorEditField', + 'PhabricatorStringListExportField' => 'PhabricatorListExportField', + 'PhabricatorStringMailStamp' => 'PhabricatorMailStamp', 'PhabricatorStringSetting' => 'PhabricatorSetting', 'PhabricatorSubmitEditField' => 'PhabricatorEditField', 'PhabricatorSubscribedToObjectEdgeType' => 'PhabricatorEdgeType', @@ -9898,9 +9972,12 @@ phutil_register_library_map(array( 'PhabricatorSubscriptionsEditController' => 'PhabricatorController', 'PhabricatorSubscriptionsEditEngineExtension' => 'PhabricatorEditEngineExtension', 'PhabricatorSubscriptionsEditor' => 'PhabricatorEditor', + 'PhabricatorSubscriptionsExportEngineExtension' => 'PhabricatorExportEngineExtension', 'PhabricatorSubscriptionsFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension', 'PhabricatorSubscriptionsHeraldAction' => 'HeraldAction', 'PhabricatorSubscriptionsListController' => 'PhabricatorController', + 'PhabricatorSubscriptionsMailEngineExtension' => 'PhabricatorMailEngineExtension', + 'PhabricatorSubscriptionsMuteController' => 'PhabricatorController', 'PhabricatorSubscriptionsRemoveSelfHeraldAction' => 'PhabricatorSubscriptionsHeraldAction', 'PhabricatorSubscriptionsRemoveSubscribersHeraldAction' => 'PhabricatorSubscriptionsHeraldAction', 'PhabricatorSubscriptionsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', @@ -10021,6 +10098,7 @@ phutil_register_library_map(array( 'PhabricatorUIExample' => 'Phobject', 'PhabricatorUIExampleRenderController' => 'PhabricatorController', 'PhabricatorUIExamplesApplication' => 'PhabricatorApplication', + 'PhabricatorURIExportField' => 'PhabricatorExportField', 'PhabricatorUSEnglishTranslation' => 'PhutilTranslation', 'PhabricatorUnifiedDiffsSetting' => 'PhabricatorSelectSetting', 'PhabricatorUnitTestContentSource' => 'PhabricatorContentSource', @@ -10144,6 +10222,7 @@ phutil_register_library_map(array( 'PhabricatorWorkerManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorWorkerPermanentFailureException' => 'Exception', 'PhabricatorWorkerSchemaSpec' => 'PhabricatorConfigSchemaSpec', + 'PhabricatorWorkerSingleBulkJobType' => 'PhabricatorWorkerBulkJobType', 'PhabricatorWorkerTask' => 'PhabricatorWorkerDAO', 'PhabricatorWorkerTaskData' => 'PhabricatorWorkerDAO', 'PhabricatorWorkerTaskDetailController' => 'PhabricatorDaemonController', diff --git a/src/applications/audit/editor/PhabricatorAuditEditor.php b/src/applications/audit/editor/PhabricatorAuditEditor.php index 0cf6339239..d142bd60cd 100644 --- a/src/applications/audit/editor/PhabricatorAuditEditor.php +++ b/src/applications/audit/editor/PhabricatorAuditEditor.php @@ -358,9 +358,17 @@ final class PhabricatorAuditEditor array $changes, PhutilMarkupEngine $engine) { - // we are only really trying to find unmentionable phids here... - // don't bother with this outside initial commit (i.e. create) - // transaction + $actor = $this->getActor(); + $result = array(); + + // Some interactions (like "Fixes Txxx" interacting with Maniphest) have + // already been processed, so we're only re-parsing them here to avoid + // generating an extra redundant mention. Other interactions are being + // processed for the first time. + + // We're only recognizing magic in the commit message itself, not in + // audit comments. + $is_commit = false; foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { @@ -370,8 +378,6 @@ final class PhabricatorAuditEditor } } - // "result" is always an array.... - $result = array(); if (!$is_commit) { return $result; } @@ -403,6 +409,46 @@ final class PhabricatorAuditEditor ->withNames($monograms) ->execute(); $phid_map[] = mpull($objects, 'getPHID', 'getPHID'); + + + $reverts_refs = id(new DifferentialCustomFieldRevertsParser()) + ->parseCorpus($huge_block); + $reverts = array_mergev(ipull($reverts_refs, 'monograms')); + if ($reverts) { + // Only allow commits to revert other commits in the same repository. + $reverted_commits = id(new DiffusionCommitQuery()) + ->setViewer($actor) + ->withRepository($object->getRepository()) + ->withIdentifiers($reverts) + ->execute(); + + $reverted_revisions = id(new PhabricatorObjectQuery()) + ->setViewer($actor) + ->withNames($reverts) + ->withTypes( + array( + DifferentialRevisionPHIDType::TYPECONST, + )) + ->execute(); + + $reverted_phids = + mpull($reverted_commits, 'getPHID', 'getPHID') + + mpull($reverted_revisions, 'getPHID', 'getPHID'); + + // NOTE: Skip any write attempts if a user cleverly implies a commit + // reverts itself, although this would be exceptionally clever in Git + // or Mercurial. + unset($reverted_phids[$object->getPHID()]); + + $reverts_edge = DiffusionCommitRevertsCommitEdgeType::EDGECONST; + $result[] = id(new PhabricatorAuditTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue('edge:type', $reverts_edge) + ->setNewValue(array('+' => $reverted_phids)); + + $phid_map[] = $reverted_phids; + } + $phid_map = array_mergev($phid_map); $this->setUnmentionablePHIDMap($phid_map); @@ -427,17 +473,14 @@ final class PhabricatorAuditEditor protected function buildMailTemplate(PhabricatorLiskDAO $object) { $identifier = $object->getCommitIdentifier(); $repository = $object->getRepository(); - $monogram = $repository->getMonogram(); $summary = $object->getSummary(); $name = $repository->formatCommitName($identifier); $subject = "{$name}: {$summary}"; - $thread_topic = "Commit {$monogram}{$identifier}"; $template = id(new PhabricatorMetaMTAMail()) - ->setSubject($subject) - ->addHeader('Thread-Topic', $thread_topic); + ->setSubject($subject); $this->attachPatch( $template, @@ -453,7 +496,6 @@ final class PhabricatorAuditEditor $phids[] = $object->getAuthorPHID(); } - $status_resigned = PhabricatorAuditStatusConstants::RESIGNED; foreach ($object->getAudits() as $audit) { if (!$audit->isInteresting()) { // Don't send mail to uninteresting auditors, like packages which @@ -461,7 +503,7 @@ final class PhabricatorAuditEditor continue; } - if ($audit->getAuditStatus() != $status_resigned) { + if (!$audit->isResigned()) { $phids[] = $audit->getAuditorPHID(); } } @@ -471,6 +513,18 @@ final class PhabricatorAuditEditor return $phids; } + protected function newMailUnexpandablePHIDs(PhabricatorLiskDAO $object) { + $phids = array(); + + foreach ($object->getAudits() as $auditor) { + if ($auditor->isResigned()) { + $phids[] = $auditor->getAuditorPHID(); + } + } + + return $phids; + } + protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { diff --git a/src/applications/auth/editor/PhabricatorAuthSSHKeyEditor.php b/src/applications/auth/editor/PhabricatorAuthSSHKeyEditor.php index 569c37403b..3f178c9855 100644 --- a/src/applications/auth/editor/PhabricatorAuthSSHKeyEditor.php +++ b/src/applications/auth/editor/PhabricatorAuthSSHKeyEditor.php @@ -255,11 +255,9 @@ final class PhabricatorAuthSSHKeyEditor protected function buildMailTemplate(PhabricatorLiskDAO $object) { $id = $object->getID(); $name = $object->getName(); - $phid = $object->getPHID(); $mail = id(new PhabricatorMetaMTAMail()) - ->setSubject(pht('SSH Key %d: %s', $id, $name)) - ->addHeader('Thread-Topic', $phid); + ->setSubject(pht('SSH Key %d: %s', $id, $name)); // The primary value of this mail is alerting users to account compromises, // so force delivery. In particular, this mail should still be delivered diff --git a/src/applications/badges/editor/PhabricatorBadgesEditor.php b/src/applications/badges/editor/PhabricatorBadgesEditor.php index fddc55747c..785d8c989b 100644 --- a/src/applications/badges/editor/PhabricatorBadgesEditor.php +++ b/src/applications/badges/editor/PhabricatorBadgesEditor.php @@ -87,12 +87,10 @@ final class PhabricatorBadgesEditor protected function buildMailTemplate(PhabricatorLiskDAO $object) { $name = $object->getName(); $id = $object->getID(); - $topic = pht('Badge %d', $id); $subject = pht('Badge %d: %s', $id, $name); return id(new PhabricatorMetaMTAMail()) - ->setSubject($subject) - ->addHeader('Thread-Topic', $topic); + ->setSubject($subject); } protected function getMailTo(PhabricatorLiskDAO $object) { diff --git a/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php b/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php index 4ab13fd360..f1b72dc0ea 100644 --- a/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php +++ b/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php @@ -309,13 +309,11 @@ final class PhabricatorCalendarEventEditor } protected function buildMailTemplate(PhabricatorLiskDAO $object) { - $id = $object->getID(); $name = $object->getName(); $monogram = $object->getMonogram(); return id(new PhabricatorMetaMTAMail()) - ->setSubject("{$monogram}: {$name}") - ->addHeader('Thread-Topic', $monogram); + ->setSubject("{$monogram}: {$name}"); } protected function buildMailBody( diff --git a/src/applications/conduit/management/PhabricatorConduitCallManagementWorkflow.php b/src/applications/conduit/management/PhabricatorConduitCallManagementWorkflow.php new file mode 100644 index 0000000000..6cb3bd2409 --- /dev/null +++ b/src/applications/conduit/management/PhabricatorConduitCallManagementWorkflow.php @@ -0,0 +1,66 @@ +setName('call') + ->setSynopsis(pht('Call a Conduit method..')) + ->setArguments( + array( + array( + 'name' => 'method', + 'param' => 'method', + 'help' => pht('Method to call.'), + ), + array( + 'name' => 'input', + 'param' => 'input', + 'help' => pht( + 'File to read parameters from, or "-" to read from '. + 'stdin.'), + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $viewer = $this->getViewer(); + + $method = $args->getArg('method'); + if (!strlen($method)) { + throw new PhutilArgumentUsageException( + pht('Specify a method to call with "--method".')); + } + + $input = $args->getArg('input'); + if (!strlen($input)) { + throw new PhutilArgumentUsageException( + pht('Specify a file to read parameters from with "--input".')); + } + + if ($input === '-') { + fprintf(STDERR, tsprintf("%s\n", pht('Reading input from stdin...'))); + $input_json = file_get_contents('php://stdin'); + } else { + $input_json = Filesystem::readFile($input); + } + + $params = phutil_json_decode($input_json); + + $result = id(new ConduitCall($method, $params)) + ->setUser($viewer) + ->execute(); + + $output = array( + 'result' => $result, + ); + + echo tsprintf( + "%B\n", + id(new PhutilJSON())->encodeFormatted($output)); + + return 0; + } + +} diff --git a/src/applications/conduit/management/PhabricatorConduitManagementWorkflow.php b/src/applications/conduit/management/PhabricatorConduitManagementWorkflow.php new file mode 100644 index 0000000000..4abb250fc4 --- /dev/null +++ b/src/applications/conduit/management/PhabricatorConduitManagementWorkflow.php @@ -0,0 +1,4 @@ +isNullable = $is_nullable; + return $this; + } + + public function getIsNullable() { + return $this->isNullable; + } + protected function getParameterValue(array $request, $key, $strict) { $value = parent::getParameterValue($request, $key, $strict); + if ($this->getIsNullable()) { + if ($value === null) { + return $value; + } + } + if (!is_string($value)) { $this->raiseValidationException( $request, @@ -17,7 +34,11 @@ final class ConduitPHIDParameterType } protected function getParameterTypeName() { - return 'phid'; + if ($this->getIsNullable()) { + return 'phid|null'; + } else { + return 'phid'; + } } protected function getParameterFormatDescriptions() { @@ -27,9 +48,15 @@ final class ConduitPHIDParameterType } protected function getParameterExamples() { - return array( + $examples = array( '"PHID-WXYZ-1111222233334444"', ); + + if ($this->getIsNullable()) { + $examples[] = 'null'; + } + + return $examples; } } diff --git a/src/applications/config/check/PhabricatorMailSetupCheck.php b/src/applications/config/check/PhabricatorMailSetupCheck.php index 2b8e4e12d5..b3b6143ad0 100644 --- a/src/applications/config/check/PhabricatorMailSetupCheck.php +++ b/src/applications/config/check/PhabricatorMailSetupCheck.php @@ -7,6 +7,10 @@ final class PhabricatorMailSetupCheck extends PhabricatorSetupCheck { } protected function executeChecks() { + if (PhabricatorEnv::getEnvConfig('cluster.mailers')) { + return; + } + $adapter = PhabricatorEnv::getEnvConfig('metamta.mail-adapter'); switch ($adapter) { diff --git a/src/applications/config/management/PhabricatorConfigManagementSetWorkflow.php b/src/applications/config/management/PhabricatorConfigManagementSetWorkflow.php index 9f50d2eeed..22b760872e 100644 --- a/src/applications/config/management/PhabricatorConfigManagementSetWorkflow.php +++ b/src/applications/config/management/PhabricatorConfigManagementSetWorkflow.php @@ -6,7 +6,9 @@ final class PhabricatorConfigManagementSetWorkflow protected function didConstruct() { $this ->setName('set') - ->setExamples('**set** __key__ __value__') + ->setExamples( + "**set** __key__ __value__\n". + "**set** __key__ --stdin < value.json") ->setSynopsis(pht('Set a local configuration value.')) ->setArguments( array( @@ -16,6 +18,10 @@ final class PhabricatorConfigManagementSetWorkflow 'Update configuration in the database instead of '. 'in local configuration.'), ), + array( + 'name' => 'stdin', + 'help' => pht('Read option value from stdin.'), + ), array( 'name' => 'args', 'wildcard' => true, @@ -31,22 +37,36 @@ final class PhabricatorConfigManagementSetWorkflow pht('Specify a configuration key and a value to set it to.')); } + $is_stdin = $args->getArg('stdin'); + $key = $argv[0]; - if (count($argv) == 1) { - throw new PhutilArgumentUsageException( - pht( - "Specify a value to set the key '%s' to.", - $key)); + if ($is_stdin) { + if (count($argv) > 1) { + throw new PhutilArgumentUsageException( + pht( + 'Too many arguments: expected only a key when using "--stdin".')); + } + + fprintf(STDERR, tsprintf("%s\n", pht('Reading value from stdin...'))); + $value = file_get_contents('php://stdin'); + } else { + if (count($argv) == 1) { + throw new PhutilArgumentUsageException( + pht( + "Specify a value to set the key '%s' to.", + $key)); + } + + if (count($argv) > 2) { + throw new PhutilArgumentUsageException( + pht( + 'Too many arguments: expected one key and one value.')); + } + + $value = $argv[1]; } - $value = $argv[1]; - - if (count($argv) > 2) { - throw new PhutilArgumentUsageException( - pht( - 'Too many arguments: expected one key and one value.')); - } $options = PhabricatorApplicationConfigOptions::loadAllOptions(); if (empty($options[$key])) { diff --git a/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php b/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php index 6c846daa57..8a236a883b 100644 --- a/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php +++ b/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php @@ -66,7 +66,9 @@ of each approach are: received a similar message, but can not prevent all stray email arising from "Reply All". - Not supported with a private reply-to address. - - Mails are sent in the server default translation. + - Mail messages are sent in the server default translation. + - Mail that must be delivered over secure channels will leak the recipient + list in the "To" and "Cc" headers. - One mail to each user: - Policy controls work correctly and are enforced per-user. - Recipients need to look in the mail body to see To/Cc. @@ -77,7 +79,7 @@ of each approach are: - "Reply All" will never send extra mail to other users involved in the thread. - Required if private reply-to addresses are configured. - - Mails are sent in the language of user preference. + - Mail messages are sent in the language of user preference. EODOC )); @@ -138,24 +140,19 @@ EODOC , 'metamta.public-replies')); - $adapter_doc_href = PhabricatorEnv::getDoclink( - 'Configuring Outbound Email'); - $adapter_doc_name = pht('Configuring Outbound Email'); $adapter_description = $this->deformat(pht(<<deformat(pht(<<deformat(pht(<<newOption('cluster.mailers', 'cluster.mailers', null) + ->setHidden(true) + ->setDescription($mailers_description), $this->newOption( 'metamta.default-address', 'string', diff --git a/src/applications/conpherence/editor/ConpherenceEditor.php b/src/applications/conpherence/editor/ConpherenceEditor.php index 29ffc22251..7896055f64 100644 --- a/src/applications/conpherence/editor/ConpherenceEditor.php +++ b/src/applications/conpherence/editor/ConpherenceEditor.php @@ -227,11 +227,9 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor { '%s sent you a message.', $this->getActor()->getUserName()); } - $phid = $object->getPHID(); return id(new PhabricatorMetaMTAMail()) - ->setSubject("Z{$id}: {$title}") - ->addHeader('Thread-Topic', "Z{$id}: {$phid}"); + ->setSubject("Z{$id}: {$title}"); } protected function getMailTo(PhabricatorLiskDAO $object) { diff --git a/src/applications/countdown/editor/PhabricatorCountdownEditor.php b/src/applications/countdown/editor/PhabricatorCountdownEditor.php index 322b2ee2c3..2102b3785b 100644 --- a/src/applications/countdown/editor/PhabricatorCountdownEditor.php +++ b/src/applications/countdown/editor/PhabricatorCountdownEditor.php @@ -45,8 +45,7 @@ final class PhabricatorCountdownEditor $name = $object->getTitle(); return id(new PhabricatorMetaMTAMail()) - ->setSubject("{$monogram}: {$name}") - ->addHeader('Thread-Topic', $monogram); + ->setSubject("{$monogram}: {$name}"); } protected function buildMailBody( diff --git a/src/applications/daemon/controller/PhabricatorDaemonBulkJobViewController.php b/src/applications/daemon/controller/PhabricatorDaemonBulkJobViewController.php index f7aa396e94..00fb297fe0 100644 --- a/src/applications/daemon/controller/PhabricatorDaemonBulkJobViewController.php +++ b/src/applications/daemon/controller/PhabricatorDaemonBulkJobViewController.php @@ -71,18 +71,10 @@ final class PhabricatorDaemonBulkJobViewController $viewer = $this->getViewer(); $curtain = $this->newCurtainView($job); - if ($job->isConfirming()) { - $continue_uri = $job->getMonitorURI(); - } else { - $continue_uri = $job->getDoneURI(); + foreach ($job->getCurtainActions($viewer) as $action) { + $curtain->addAction($action); } - $curtain->addAction( - id(new PhabricatorActionView()) - ->setHref($continue_uri) - ->setIcon('fa-arrow-circle-o-right') - ->setName(pht('Continue'))); - return $curtain; } diff --git a/src/applications/differential/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php index ecd1e6c95c..063df6c602 100644 --- a/src/applications/differential/editor/DifferentialTransactionEditor.php +++ b/src/applications/differential/editor/DifferentialTransactionEditor.php @@ -632,6 +632,8 @@ final class DifferentialTransactionEditor } protected function getMailTo(PhabricatorLiskDAO $object) { + $this->requireReviewers($object); + $phids = array(); $phids[] = $object->getAuthorPHID(); foreach ($object->getReviewers() as $reviewer) { @@ -644,6 +646,20 @@ final class DifferentialTransactionEditor return $phids; } + protected function newMailUnexpandablePHIDs(PhabricatorLiskDAO $object) { + $this->requireReviewers($object); + + $phids = array(); + + foreach ($object->getReviewers() as $reviewer) { + if ($reviewer->isResigned()) { + $phids[] = $reviewer->getReviewerPHID(); + } + } + + return $phids; + } + protected function getMailAction( PhabricatorLiskDAO $object, array $xactions) { @@ -689,15 +705,10 @@ final class DifferentialTransactionEditor protected function buildMailTemplate(PhabricatorLiskDAO $object) { $id = $object->getID(); $title = $object->getTitle(); - - $original_title = $object->getOriginalTitle(); - $subject = "D{$id}: {$title}"; - $thread_topic = "D{$id}: {$original_title}"; return id(new PhabricatorMetaMTAMail()) - ->setSubject($subject) - ->addHeader('Thread-Topic', $thread_topic); + ->setSubject($subject); } protected function getTransactionsForMail( @@ -919,7 +930,44 @@ final class DifferentialTransactionEditor } } - $this->setUnmentionablePHIDMap(array_merge($task_phids, $rev_phids)); + $revert_refs = id(new DifferentialCustomFieldRevertsParser()) + ->parseCorpus($content_block); + + $revert_monograms = array(); + foreach ($revert_refs as $match) { + foreach ($match['monograms'] as $monogram) { + $revert_monograms[] = $monogram; + } + } + + if ($revert_monograms) { + $revert_objects = id(new PhabricatorObjectQuery()) + ->setViewer($this->getActor()) + ->withNames($revert_monograms) + ->withTypes( + array( + DifferentialRevisionPHIDType::TYPECONST, + PhabricatorRepositoryCommitPHIDType::TYPECONST, + )) + ->execute(); + + $revert_phids = mpull($revert_objects, 'getPHID', 'getPHID'); + + // Don't let an object revert itself, although other silly stuff like + // cycles of objects reverting each other is not prevented. + unset($revert_phids[$object->getPHID()]); + + $revert_type = DiffusionCommitRevertsCommitEdgeType::EDGECONST; + $edges[$revert_type] = $revert_phids; + } else { + $revert_phids = array(); + } + + $this->setUnmentionablePHIDMap( + array_merge( + $task_phids, + $rev_phids, + $revert_phids)); $result = array(); foreach ($edges as $type => $specs) { @@ -1693,4 +1741,25 @@ final class DifferentialTransactionEditor } } + private function requireReviewers(DifferentialRevision $revision) { + if ($revision->hasAttachedReviewers()) { + return; + } + + $with_reviewers = id(new DifferentialRevisionQuery()) + ->setViewer($this->getActor()) + ->needReviewers(true) + ->withPHIDs(array($revision->getPHID())) + ->executeOne(); + if (!$with_reviewers) { + throw new Exception( + pht( + 'Failed to reload revision ("%s").', + $revision->getPHID())); + } + + $revision->attachReviewers($with_reviewers->getReviewers()); + } + + } diff --git a/src/applications/differential/engineextension/DifferentialMailEngineExtension.php b/src/applications/differential/engineextension/DifferentialMailEngineExtension.php new file mode 100644 index 0000000000..24aa9fb329 --- /dev/null +++ b/src/applications/differential/engineextension/DifferentialMailEngineExtension.php @@ -0,0 +1,80 @@ +setKey('author') + ->setLabel(pht('Author')), + id(new PhabricatorPHIDMailStamp()) + ->setKey('reviewer') + ->setLabel(pht('Reviewer')), + id(new PhabricatorPHIDMailStamp()) + ->setKey('blocking-reviewer') + ->setLabel(pht('Reviewer')), + id(new PhabricatorPHIDMailStamp()) + ->setKey('resigned-reviewer') + ->setLabel(pht('Reviewer')), + id(new PhabricatorPHIDMailStamp()) + ->setKey('revision-repository') + ->setLabel(pht('Revision Repository')), + id(new PhabricatorPHIDMailStamp()) + ->setKey('revision-status') + ->setLabel(pht('Revision Status')), + ); + } + + public function newMailStamps($object, array $xactions) { + $editor = $this->getEditor(); + $viewer = $this->getViewer(); + + $revision = id(new DifferentialRevisionQuery()) + ->setViewer($viewer) + ->needReviewers(true) + ->withPHIDs(array($object->getPHID())) + ->executeOne(); + + $reviewers = array(); + $blocking = array(); + $resigned = array(); + foreach ($revision->getReviewers() as $reviewer) { + $reviewer_phid = $reviewer->getReviewerPHID(); + + if ($reviewer->isResigned()) { + $resigned[] = $reviewer_phid; + } else { + $reviewers[] = $reviewer_phid; + if ($reviewer->isBlocking()) { + $reviewers[] = $blocking; + } + } + } + + $this->getMailStamp('author') + ->setValue($revision->getAuthorPHID()); + + $this->getMailStamp('reviewer') + ->setValue($reviewers); + + $this->getMailStamp('blocking-reviewer') + ->setValue($blocking); + + $this->getMailStamp('resigned-reviewer') + ->setValue($resigned); + + $this->getMailStamp('revision-repository') + ->setValue($revision->getRepositoryPHID()); + + $this->getMailStamp('revision-status') + ->setValue($revision->getModernRevisionStatus()); + } + +} diff --git a/src/applications/differential/storage/DifferentialChangeset.php b/src/applications/differential/storage/DifferentialChangeset.php index ebdaeacd0a..dbb06fe72b 100644 --- a/src/applications/differential/storage/DifferentialChangeset.php +++ b/src/applications/differential/storage/DifferentialChangeset.php @@ -221,6 +221,51 @@ final class DifferentialChangeset return $this->assertAttached($this->diff); } + public function newFileTreeIcon() { + $file_type = $this->getFileType(); + $change_type = $this->getChangeType(); + + $change_icons = array( + DifferentialChangeType::TYPE_DELETE => 'fa-file-o', + ); + + if (isset($change_icons[$change_type])) { + $icon = $change_icons[$change_type]; + } else { + $icon = DifferentialChangeType::getIconForFileType($file_type); + } + + $change_colors = array( + DifferentialChangeType::TYPE_ADD => 'green', + DifferentialChangeType::TYPE_DELETE => 'red', + DifferentialChangeType::TYPE_MOVE_AWAY => 'orange', + DifferentialChangeType::TYPE_MOVE_HERE => 'orange', + DifferentialChangeType::TYPE_COPY_HERE => 'orange', + DifferentialChangeType::TYPE_MULTICOPY => 'orange', + ); + + $color = idx($change_colors, $change_type, 'bluetext'); + + return id(new PHUIIconView()) + ->setIcon($icon.' '.$color); + } + + public function getFileTreeClass() { + switch ($this->getChangeType()) { + case DifferentialChangeType::TYPE_ADD: + return 'filetree-added'; + case DifferentialChangeType::TYPE_DELETE: + return 'filetree-deleted'; + case DifferentialChangeType::TYPE_MOVE_AWAY: + case DifferentialChangeType::TYPE_MOVE_HERE: + case DifferentialChangeType::TYPE_COPY_HERE: + case DifferentialChangeType::TYPE_MULTICOPY: + return 'filetree-movecopy'; + } + + return null; + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/differential/storage/DifferentialReviewer.php b/src/applications/differential/storage/DifferentialReviewer.php index 9df149e788..e3f9bdaf8d 100644 --- a/src/applications/differential/storage/DifferentialReviewer.php +++ b/src/applications/differential/storage/DifferentialReviewer.php @@ -69,6 +69,11 @@ final class DifferentialReviewer return ($this->getReviewerStatus() == $status_resigned); } + public function isBlocking() { + $status_blocking = DifferentialReviewerStatus::STATUS_BLOCKING; + return ($this->getReviewerStatus() == $status_blocking); + } + public function isRejected($diff_phid) { $status_rejected = DifferentialReviewerStatus::STATUS_REJECTED; diff --git a/src/applications/differential/storage/DifferentialRevision.php b/src/applications/differential/storage/DifferentialRevision.php index 2c82de164a..e8fdf7e514 100644 --- a/src/applications/differential/storage/DifferentialRevision.php +++ b/src/applications/differential/storage/DifferentialRevision.php @@ -20,7 +20,6 @@ final class DifferentialRevision extends DifferentialDAO PhabricatorDraftInterface { protected $title = ''; - protected $originalTitle; protected $status; protected $summary = ''; @@ -98,7 +97,6 @@ final class DifferentialRevision extends DifferentialDAO ), self::CONFIG_COLUMN_SCHEMA => array( 'title' => 'text255', - 'originalTitle' => 'text255', 'status' => 'text32', 'summary' => 'text', 'testPlan' => 'text', @@ -155,14 +153,6 @@ final class DifferentialRevision extends DifferentialDAO return '/'.$this->getMonogram(); } - public function setTitle($title) { - $this->title = $title; - if (!$this->getID()) { - $this->originalTitle = $title; - } - return $this; - } - public function loadIDsByCommitPHIDs($phids) { if (!$phids) { return array(); @@ -593,6 +583,10 @@ final class DifferentialRevision extends DifferentialDAO return $this; } + public function hasAttachedReviewers() { + return ($this->reviewerStatus !== self::ATTACHABLE); + } + public function getReviewerPHIDs() { $reviewers = $this->getReviewers(); return mpull($reviewers, 'getReviewerPHID'); @@ -830,9 +824,15 @@ final class DifferentialRevision extends DifferentialDAO } foreach ($reviewers as $reviewer) { - if ($reviewer->getReviewerPHID() == $phid) { - return true; + if ($reviewer->getReviewerPHID() !== $phid) { + continue; } + + if ($reviewer->isResigned()) { + continue; + } + + return true; } return false; diff --git a/src/applications/differential/storage/DifferentialTransaction.php b/src/applications/differential/storage/DifferentialTransaction.php index 667341cef4..f7dcf29860 100644 --- a/src/applications/differential/storage/DifferentialTransaction.php +++ b/src/applications/differential/storage/DifferentialTransaction.php @@ -87,19 +87,6 @@ final class DifferentialTransaction } break; - case PhabricatorTransactions::TYPE_EDGE: - $add = array_diff_key($new, $old); - $rem = array_diff_key($old, $new); - - // Hide metadata-only edge transactions. These correspond to users - // accepting or rejecting revisions, but the change is always explicit - // because of the TYPE_ACTION transaction. Rendering these transactions - // just creates clutter. - - if (!$add && !$rem) { - return true; - } - break; case DifferentialRevisionRequestReviewTransaction::TRANSACTIONTYPE: // Don't hide the initial "X requested review: ..." transaction from // mail or feed even when it occurs during creation. We need this diff --git a/src/applications/differential/view/DifferentialChangesetDetailView.php b/src/applications/differential/view/DifferentialChangesetDetailView.php index d4a13745dc..cb697c2e9d 100644 --- a/src/applications/differential/view/DifferentialChangesetDetailView.php +++ b/src/applications/differential/view/DifferentialChangesetDetailView.php @@ -206,6 +206,7 @@ final class DifferentialChangesetDetailView extends AphrontView { 'displayPath' => hsprintf('%s', $display_parts), 'path' => $display_filename, 'icon' => $display_icon, + 'treeNodeID' => 'tree-node-'.$changeset->getAnchorName(), ), 'class' => $class, 'id' => $id, diff --git a/src/applications/differential/view/DifferentialChangesetFileTreeSideNavBuilder.php b/src/applications/differential/view/DifferentialChangesetFileTreeSideNavBuilder.php index 14050e942c..1f699be8eb 100644 --- a/src/applications/differential/view/DifferentialChangesetFileTreeSideNavBuilder.php +++ b/src/applications/differential/view/DifferentialChangesetFileTreeSideNavBuilder.php @@ -83,6 +83,9 @@ final class DifferentialChangesetFileTreeSideNavBuilder extends Phobject { while (($path = $path->getNextNode())) { $data = $path->getData(); + $classes = array(); + $classes[] = 'phabricator-filetree-item'; + $name = $path->getName(); $style = 'padding-left: '.(2 + (3 * $path->getDepth())).'px'; @@ -90,13 +93,23 @@ final class DifferentialChangesetFileTreeSideNavBuilder extends Phobject { if ($data) { $href = '#'.$data->getAnchorName(); $title = $name; - $icon = id(new PHUIIconView()) - ->setIcon('fa-file-text-o bluetext'); + + $icon = $data->newFileTreeIcon(); + $classes[] = $data->getFileTreeClass(); + + $count = phutil_tag( + 'span', + array( + 'class' => 'filetree-progress-hint', + 'id' => 'tree-node-'.$data->getAnchorName(), + )); } else { $name .= '/'; $title = $path->getFullPath().'/'; $icon = id(new PHUIIconView()) ->setIcon('fa-folder-open blue'); + + $count = null; } $name_element = phutil_tag( @@ -106,15 +119,16 @@ final class DifferentialChangesetFileTreeSideNavBuilder extends Phobject { ), $name); + $filetree[] = javelin_tag( $href ? 'a' : 'span', array( 'href' => $href, 'style' => $style, 'title' => $title, - 'class' => 'phabricator-filetree-item', + 'class' => implode(' ', $classes), ), - array($icon, $name_element)); + array($count, $icon, $name_element)); } $tree->destroy(); diff --git a/src/applications/diffusion/application/PhabricatorDiffusionApplication.php b/src/applications/diffusion/application/PhabricatorDiffusionApplication.php index d932149a75..e619ecb1ad 100644 --- a/src/applications/diffusion/application/PhabricatorDiffusionApplication.php +++ b/src/applications/diffusion/application/PhabricatorDiffusionApplication.php @@ -121,7 +121,7 @@ final class PhabricatorDiffusionApplication extends PhabricatorApplication { $this->getEditRoutePattern('edit/') => 'DiffusionRepositoryEditController', 'pushlog/' => array( - '(?:query/(?P[^/]+)/)?' => 'DiffusionPushLogListController', + $this->getQueryRoutePattern() => 'DiffusionPushLogListController', 'view/(?P\d+)/' => 'DiffusionPushEventViewController', ), 'pulllog/' => array( diff --git a/src/applications/diffusion/conduit/DiffusionQueryPathsConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionQueryPathsConduitAPIMethod.php index be2f07f2c6..09c07ec28f 100644 --- a/src/applications/diffusion/conduit/DiffusionQueryPathsConduitAPIMethod.php +++ b/src/applications/diffusion/conduit/DiffusionQueryPathsConduitAPIMethod.php @@ -37,7 +37,11 @@ final class DiffusionQueryPathsConduitAPIMethod $commit = $request->getValue('commit'); $repository = $drequest->getRepository(); - // http://comments.gmane.org/gmane.comp.version-control.git/197735 + // Recent versions of Git don't work if you pass the empty string, and + // require "." to list everything. + if (!strlen($path)) { + $path = '.'; + } $future = $repository->getLocalCommandFuture( 'ls-tree --name-only -r -z %s -- %s', diff --git a/src/applications/diffusion/controller/DiffusionPullLogListController.php b/src/applications/diffusion/controller/DiffusionPullLogListController.php index 29712be2a4..f3b570c815 100644 --- a/src/applications/diffusion/controller/DiffusionPullLogListController.php +++ b/src/applications/diffusion/controller/DiffusionPullLogListController.php @@ -9,4 +9,9 @@ final class DiffusionPullLogListController ->buildResponse(); } + protected function buildApplicationCrumbs() { + return parent::buildApplicationCrumbs() + ->addTextCrumb(pht('Pull Logs'), $this->getApplicationURI('pulllog/')); + } + } diff --git a/src/applications/diffusion/controller/DiffusionPushLogListController.php b/src/applications/diffusion/controller/DiffusionPushLogListController.php index 658e21674d..a6c612eebe 100644 --- a/src/applications/diffusion/controller/DiffusionPushLogListController.php +++ b/src/applications/diffusion/controller/DiffusionPushLogListController.php @@ -9,4 +9,9 @@ final class DiffusionPushLogListController ->buildResponse(); } + protected function buildApplicationCrumbs() { + return parent::buildApplicationCrumbs() + ->addTextCrumb(pht('Push Logs'), $this->getApplicationURI('pushlog/')); + } + } diff --git a/src/applications/diffusion/controller/DiffusionRepositoryURIViewController.php b/src/applications/diffusion/controller/DiffusionRepositoryURIViewController.php index 91ffbb473f..e923ebfc20 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryURIViewController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryURIViewController.php @@ -23,14 +23,10 @@ final class DiffusionRepositoryURIViewController return new Aphront404Response(); } - // For display, reload the URI by loading it through the repository. This + // For display, access the URI by loading it through the repository. This // may adjust builtin URIs for repository configuration, so we may end up // with a different view of builtin URIs than we'd see if we loaded them // directly from the database. See T12884. - $repository_with_uris = id(new PhabricatorRepositoryQuery()) - ->setViewer($viewer) - ->needURIs(true) - ->execute(); $repository_uris = $repository->getURIs(); $repository_uris = mpull($repository_uris, null, 'getID'); diff --git a/src/applications/diffusion/edge/DiffusionCommitRevertedByCommitEdgeType.php b/src/applications/diffusion/edge/DiffusionCommitRevertedByCommitEdgeType.php index 53c39e54c9..ae59a3b1e6 100644 --- a/src/applications/diffusion/edge/DiffusionCommitRevertedByCommitEdgeType.php +++ b/src/applications/diffusion/edge/DiffusionCommitRevertedByCommitEdgeType.php @@ -19,7 +19,7 @@ final class DiffusionCommitRevertedByCommitEdgeType $add_edges) { return pht( - '%s added %s reverting commit(s): %s.', + '%s added %s reverting change(s): %s.', $actor, $add_count, $add_edges); @@ -31,7 +31,7 @@ final class DiffusionCommitRevertedByCommitEdgeType $rem_edges) { return pht( - '%s removed %s reverting commit(s): %s.', + '%s removed %s reverting change(s): %s.', $actor, $rem_count, $rem_edges); @@ -46,7 +46,7 @@ final class DiffusionCommitRevertedByCommitEdgeType $rem_edges) { return pht( - '%s edited reverting commit(s), added %s: %s; removed %s: %s.', + '%s edited reverting change(s), added %s: %s; removed %s: %s.', $actor, $add_count, $add_edges, @@ -61,7 +61,7 @@ final class DiffusionCommitRevertedByCommitEdgeType $add_edges) { return pht( - '%s added %s reverting commit(s) for %s: %s.', + '%s added %s reverting change(s) for %s: %s.', $actor, $add_count, $object, @@ -75,7 +75,7 @@ final class DiffusionCommitRevertedByCommitEdgeType $rem_edges) { return pht( - '%s removed %s reverting commit(s) for %s: %s.', + '%s removed %s reverting change(s) for %s: %s.', $actor, $rem_count, $object, @@ -92,7 +92,7 @@ final class DiffusionCommitRevertedByCommitEdgeType $rem_edges) { return pht( - '%s edited reverting commit(s) for %s, added %s: %s; removed %s: %s.', + '%s edited reverting change(s) for %s, added %s: %s; removed %s: %s.', $actor, $object, $add_count, diff --git a/src/applications/diffusion/edge/DiffusionCommitRevertsCommitEdgeType.php b/src/applications/diffusion/edge/DiffusionCommitRevertsCommitEdgeType.php index 8f32797173..ee0223c966 100644 --- a/src/applications/diffusion/edge/DiffusionCommitRevertsCommitEdgeType.php +++ b/src/applications/diffusion/edge/DiffusionCommitRevertsCommitEdgeType.php @@ -22,7 +22,7 @@ final class DiffusionCommitRevertsCommitEdgeType extends PhabricatorEdgeType { $add_edges) { return pht( - '%s added %s reverted commit(s): %s.', + '%s added %s reverted change(s): %s.', $actor, $add_count, $add_edges); @@ -34,7 +34,7 @@ final class DiffusionCommitRevertsCommitEdgeType extends PhabricatorEdgeType { $rem_edges) { return pht( - '%s removed %s reverted commit(s): %s.', + '%s removed %s reverted change(s): %s.', $actor, $rem_count, $rem_edges); @@ -49,7 +49,7 @@ final class DiffusionCommitRevertsCommitEdgeType extends PhabricatorEdgeType { $rem_edges) { return pht( - '%s edited reverted commit(s), added %s: %s; removed %s: %s.', + '%s edited reverted change(s), added %s: %s; removed %s: %s.', $actor, $add_count, $add_edges, @@ -64,7 +64,7 @@ final class DiffusionCommitRevertsCommitEdgeType extends PhabricatorEdgeType { $add_edges) { return pht( - '%s added %s reverted commit(s) for %s: %s.', + '%s added %s reverted change(s) for %s: %s.', $actor, $add_count, $object, @@ -78,7 +78,7 @@ final class DiffusionCommitRevertsCommitEdgeType extends PhabricatorEdgeType { $rem_edges) { return pht( - '%s removed %s reverted commit(s) for %s: %s.', + '%s removed %s reverted change(s) for %s: %s.', $actor, $rem_count, $object, @@ -95,7 +95,7 @@ final class DiffusionCommitRevertsCommitEdgeType extends PhabricatorEdgeType { $rem_edges) { return pht( - '%s edited reverted commit(s) for %s, added %s: %s; removed %s: %s.', + '%s edited reverted change(s) for %s, added %s: %s; removed %s: %s.', $actor, $object, $add_count, diff --git a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php index 99df0e54af..a0769a51f0 100644 --- a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php +++ b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php @@ -297,7 +297,11 @@ final class DiffusionCommitHookEngine extends Phobject { return; } - $adapter_template->setHookEngine($this); + $viewer = $this->getViewer(); + + $adapter_template + ->setHookEngine($this) + ->setActingAsPHID($viewer->getPHID()); $engine = new HeraldEngine(); $rules = null; diff --git a/src/applications/diffusion/gitlfs/DiffusionGitLFSAuthenticateWorkflow.php b/src/applications/diffusion/gitlfs/DiffusionGitLFSAuthenticateWorkflow.php index 41c009455d..56297f4d8b 100644 --- a/src/applications/diffusion/gitlfs/DiffusionGitLFSAuthenticateWorkflow.php +++ b/src/applications/diffusion/gitlfs/DiffusionGitLFSAuthenticateWorkflow.php @@ -84,7 +84,7 @@ final class DiffusionGitLFSAuthenticateWorkflow // This works even if normal HTTP repository operations are not available // on this host, and does not require the user to have a VCS password. - $user = $this->getUser(); + $user = $this->getSSHUser(); $authorization = DiffusionGitLFSTemporaryTokenType::newHTTPAuthorization( $repository, diff --git a/src/applications/diffusion/herald/HeraldCommitAdapter.php b/src/applications/diffusion/herald/HeraldCommitAdapter.php index e0e15352b6..fc3c8c4f5b 100644 --- a/src/applications/diffusion/herald/HeraldCommitAdapter.php +++ b/src/applications/diffusion/herald/HeraldCommitAdapter.php @@ -135,13 +135,16 @@ final class HeraldCommitAdapter } public function loadAffectedPaths() { + $viewer = $this->getViewer(); + if ($this->affectedPaths === null) { $result = PhabricatorOwnerPathQuery::loadAffectedPaths( $this->getRepository(), $this->commit, - PhabricatorUser::getOmnipotentUser()); + $viewer); $this->affectedPaths = $result; } + return $this->affectedPaths; } @@ -172,6 +175,8 @@ final class HeraldCommitAdapter } public function loadDifferentialRevision() { + $viewer = $this->getViewer(); + if ($this->affectedRevision === null) { $this->affectedRevision = false; @@ -189,7 +194,7 @@ final class HeraldCommitAdapter $revision = id(new DifferentialRevisionQuery()) ->withIDs(array($revision_id)) - ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->setViewer($viewer) ->needReviewers(true) ->executeOne(); if ($revision) { @@ -197,6 +202,7 @@ final class HeraldCommitAdapter } } } + return $this->affectedRevision; } @@ -323,7 +329,7 @@ final class HeraldCommitAdapter } private function callConduit($method, array $params) { - $viewer = PhabricatorUser::getOmnipotentUser(); + $viewer = $this->getViewer(); $drequest = DiffusionRequest::newFromDictionary( array( diff --git a/src/applications/diffusion/query/DiffusionPullLogSearchEngine.php b/src/applications/diffusion/query/DiffusionPullLogSearchEngine.php index 8d6102d4eb..dfdfceb519 100644 --- a/src/applications/diffusion/query/DiffusionPullLogSearchEngine.php +++ b/src/applications/diffusion/query/DiffusionPullLogSearchEngine.php @@ -26,6 +26,12 @@ final class DiffusionPullLogSearchEngine $query->withPullerPHIDs($map['pullerPHIDs']); } + if ($map['createdStart'] || $map['createdEnd']) { + $query->withEpochBetween( + $map['createdStart'], + $map['createdEnd']); + } + return $query; } @@ -44,17 +50,19 @@ final class DiffusionPullLogSearchEngine ->setLabel(pht('Pullers')) ->setDescription( pht('Search for pull logs by specific users.')), + id(new PhabricatorSearchDateField()) + ->setLabel(pht('Created After')) + ->setKey('createdStart'), + id(new PhabricatorSearchDateField()) + ->setLabel(pht('Created Before')) + ->setKey('createdEnd'), ); } protected function newExportFields() { - return array( - id(new PhabricatorIDExportField()) - ->setKey('id') - ->setLabel(pht('ID')), - id(new PhabricatorPHIDExportField()) - ->setKey('phid') - ->setLabel(pht('PHID')), + $viewer = $this->requireViewer(); + + $fields = array( id(new PhabricatorPHIDExportField()) ->setKey('repositoryPHID') ->setLabel(pht('Repository PHID')), @@ -80,9 +88,17 @@ final class DiffusionPullLogSearchEngine ->setKey('date') ->setLabel(pht('Date')), ); + + if ($viewer->getIsAdmin()) { + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('remoteAddress') + ->setLabel(pht('Remote Address')); + } + + return $fields; } - public function newExport(array $events) { + protected function newExportData(array $events) { $viewer = $this->requireViewer(); $phids = array(); @@ -111,9 +127,7 @@ final class DiffusionPullLogSearchEngine $puller_name = null; } - $export[] = array( - 'id' => $event->getID(), - 'phid' => $event->getPHID(), + $map = array( 'repositoryPHID' => $repository_phid, 'repository' => $repository_name, 'pullerPHID' => $puller_phid, @@ -123,6 +137,12 @@ final class DiffusionPullLogSearchEngine 'code' => $event->getResultCode(), 'date' => $event->getEpoch(), ); + + if ($viewer->getIsAdmin()) { + $map['remoteAddress'] = $event->getRemoteAddress(); + } + + $export[] = $map; } return $export; diff --git a/src/applications/diffusion/view/DiffusionPullLogListView.php b/src/applications/diffusion/view/DiffusionPullLogListView.php index f2e3280eba..8df35e2922 100644 --- a/src/applications/diffusion/view/DiffusionPullLogListView.php +++ b/src/applications/diffusion/view/DiffusionPullLogListView.php @@ -22,24 +22,10 @@ final class DiffusionPullLogListView extends AphrontView { } $handles = $viewer->loadHandles($handle_phids); - // Figure out which repositories are editable. We only let you see remote - // IPs if you have edit capability on a repository. - $editable_repos = array(); - if ($events) { - $editable_repos = id(new PhabricatorRepositoryQuery()) - ->setViewer($viewer) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->withPHIDs(mpull($events, 'getRepositoryPHID')) - ->execute(); - $editable_repos = mpull($editable_repos, null, 'getPHID'); - } + // Only administrators can view remote addresses. + $remotes_visible = $viewer->getIsAdmin(); $rows = array(); - $any_host = false; foreach ($events as $event) { if ($event->getRepositoryPHID()) { $repository = $event->getRepository(); @@ -47,13 +33,10 @@ final class DiffusionPullLogListView extends AphrontView { $repository = null; } - // Reveal this if it's valid and the user can edit the repository. For - // invalid requests you currently have to go fishing in the database. - $remote_address = '-'; - if ($repository) { - if (isset($editable_repos[$event->getRepositoryPHID()])) { - $remote_address = $event->getRemoteAddress(); - } + if ($remotes_visible) { + $remote_address = $event->getRemoteAddress(); + } else { + $remote_address = null; } $event_id = $event->getID(); @@ -107,6 +90,13 @@ final class DiffusionPullLogListView extends AphrontView { '', 'n', 'right', + )) + ->setColumnVisibility( + array( + true, + true, + true, + $remotes_visible, )); return $table; diff --git a/src/applications/diffusion/view/DiffusionPushLogListView.php b/src/applications/diffusion/view/DiffusionPushLogListView.php index 303f0519f6..e101625752 100644 --- a/src/applications/diffusion/view/DiffusionPushLogListView.php +++ b/src/applications/diffusion/view/DiffusionPushLogListView.php @@ -25,31 +25,21 @@ final class DiffusionPushLogListView extends AphrontView { $handles = $viewer->loadHandles($handle_phids); - // Figure out which repositories are editable. We only let you see remote - // IPs if you have edit capability on a repository. - $editable_repos = array(); - if ($logs) { - $editable_repos = id(new PhabricatorRepositoryQuery()) - ->setViewer($viewer) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->withPHIDs(mpull($logs, 'getRepositoryPHID')) - ->execute(); - $editable_repos = mpull($editable_repos, null, 'getPHID'); - } + // Only administrators can view remote addresses. + $remotes_visible = $viewer->getIsAdmin(); + + $flag_map = PhabricatorRepositoryPushLog::getFlagDisplayNames(); + $reject_map = PhabricatorRepositoryPushLog::getRejectCodeDisplayNames(); $rows = array(); $any_host = false; foreach ($logs as $log) { $repository = $log->getRepository(); - // Reveal this if it's valid and the user can edit the repository. - $remote_address = '-'; - if (isset($editable_repos[$log->getRepositoryPHID()])) { + if ($remotes_visible) { $remote_address = $log->getPushEvent()->getRemoteAddress(); + } else { + $remote_address = null; } $event_id = $log->getPushEvent()->getID(); @@ -72,6 +62,23 @@ final class DiffusionPushLogListView extends AphrontView { $device = null; } + $flags = $log->getChangeFlags(); + $flag_names = array(); + foreach ($flag_map as $flag_key => $flag_name) { + if (($flags & $flag_key) === $flag_key) { + $flag_names[] = $flag_name; + } + } + $flag_names = phutil_implode_html( + phutil_tag('br'), + $flag_names); + + $reject_code = $log->getPushEvent()->getRejectCode(); + $reject_label = idx( + $reject_map, + $reject_code, + pht('Unknown ("%s")', $reject_code)); + $rows[] = array( phutil_tag( 'a', @@ -98,10 +105,8 @@ final class DiffusionPushLogListView extends AphrontView { 'href' => $repository->getCommitURI($log->getRefNew()), ), $log->getRefNewShort()), - - // TODO: Make these human-readable. - $log->getChangeFlags(), - $log->getPushEvent()->getRejectCode(), + $flag_names, + $reject_label, $viewer->formatShortDateTime($log->getEpoch()), ); } @@ -120,7 +125,7 @@ final class DiffusionPushLogListView extends AphrontView { pht('Old'), pht('New'), pht('Flags'), - pht('Code'), + pht('Result'), pht('Date'), )) ->setColumnClasses( @@ -135,6 +140,8 @@ final class DiffusionPushLogListView extends AphrontView { 'wide', 'n', 'n', + '', + '', 'right', )) ->setColumnVisibility( @@ -142,7 +149,7 @@ final class DiffusionPushLogListView extends AphrontView { true, true, true, - true, + $remotes_visible, true, $any_host, )); diff --git a/src/applications/files/editor/PhabricatorFileEditor.php b/src/applications/files/editor/PhabricatorFileEditor.php index 6a2b797b40..db974cec65 100644 --- a/src/applications/files/editor/PhabricatorFileEditor.php +++ b/src/applications/files/editor/PhabricatorFileEditor.php @@ -47,8 +47,7 @@ final class PhabricatorFileEditor $name = $object->getName(); return id(new PhabricatorMetaMTAMail()) - ->setSubject("F{$id}: {$name}") - ->addHeader('Thread-Topic', "F{$id}"); + ->setSubject("F{$id}: {$name}"); } protected function buildMailBody( diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php index 4ed5b8ab8d..7e3aaf84c4 100644 --- a/src/applications/files/storage/PhabricatorFile.php +++ b/src/applications/files/storage/PhabricatorFile.php @@ -272,8 +272,12 @@ final class PhabricatorFile extends PhabricatorFileDAO $file->setByteSize($length); // NOTE: Once we receive the first chunk, we'll detect its MIME type and - // update the parent file. This matters for large media files like video. - $file->setMimeType('application/octet-stream'); + // update the parent file if a MIME type hasn't been provided. This matters + // for large media files like video. + $mime_type = idx($params, 'mime-type'); + if (!strlen($mime_type)) { + $file->setMimeType('application/octet-stream'); + } $chunked_hash = idx($params, 'chunkedHash'); diff --git a/src/applications/files/uploadsource/PhabricatorFileUploadSource.php b/src/applications/files/uploadsource/PhabricatorFileUploadSource.php index bf213a417e..87a4486869 100644 --- a/src/applications/files/uploadsource/PhabricatorFileUploadSource.php +++ b/src/applications/files/uploadsource/PhabricatorFileUploadSource.php @@ -6,6 +6,8 @@ abstract class PhabricatorFileUploadSource private $name; private $relativeTTL; private $viewPolicy; + private $mimeType; + private $authorPHID; private $rope; private $data; @@ -51,6 +53,24 @@ abstract class PhabricatorFileUploadSource return $this->byteLimit; } + public function setMIMEType($mime_type) { + $this->mimeType = $mime_type; + return $this; + } + + public function getMIMEType() { + return $this->mimeType; + } + + public function setAuthorPHID($author_phid) { + $this->authorPHID = $author_phid; + return $this; + } + + public function getAuthorPHID() { + return $this->authorPHID; + } + public function uploadFile() { if (!$this->shouldChunkFile()) { return $this->writeSingleFile(); @@ -245,6 +265,16 @@ abstract class PhabricatorFileUploadSource $parameters['ttl.relative'] = $ttl; } + $mime_type = $this->getMimeType(); + if ($mime_type !== null) { + $parameters['mime-type'] = $mime_type; + } + + $author_phid = $this->getAuthorPHID(); + if ($author_phid !== null) { + $parameters['authorPHID'] = $author_phid; + } + return $parameters; } diff --git a/src/applications/fund/editor/FundInitiativeEditor.php b/src/applications/fund/editor/FundInitiativeEditor.php index e5c372fd12..9175156ffd 100644 --- a/src/applications/fund/editor/FundInitiativeEditor.php +++ b/src/applications/fund/editor/FundInitiativeEditor.php @@ -50,8 +50,7 @@ final class FundInitiativeEditor $name = $object->getName(); return id(new PhabricatorMetaMTAMail()) - ->setSubject("{$monogram}: {$name}") - ->addHeader('Thread-Topic', $monogram); + ->setSubject("{$monogram}: {$name}"); } protected function buildMailBody( diff --git a/src/applications/herald/adapter/HeraldAdapter.php b/src/applications/herald/adapter/HeraldAdapter.php index 9d56f474ff..940d604019 100644 --- a/src/applications/herald/adapter/HeraldAdapter.php +++ b/src/applications/herald/adapter/HeraldAdapter.php @@ -39,6 +39,8 @@ abstract class HeraldAdapter extends Phobject { private $edgeCache = array(); private $forbiddenActions = array(); private $viewer; + private $mustEncryptReasons = array(); + private $actingAsPHID; public function getEmailPHIDs() { return array_values($this->emailPHIDs); @@ -48,6 +50,15 @@ abstract class HeraldAdapter extends Phobject { return array_values($this->forcedEmailPHIDs); } + final public function setActingAsPHID($acting_as_phid) { + $this->actingAsPHID = $acting_as_phid; + return $this; + } + + final public function getActingAsPHID() { + return $this->actingAsPHID; + } + public function addEmailPHID($phid, $force) { $this->emailPHIDs[$phid] = $phid; if ($force) { @@ -1182,4 +1193,17 @@ abstract class HeraldAdapter extends Phobject { return $this->forbiddenActions[$action]; } + +/* -( Must Encrypt )------------------------------------------------------- */ + + + final public function addMustEncryptReason($reason) { + $this->mustEncryptReasons[] = $reason; + return $this; + } + + final public function getMustEncryptReasons() { + return $this->mustEncryptReasons; + } + } diff --git a/src/applications/herald/controller/HeraldRuleController.php b/src/applications/herald/controller/HeraldRuleController.php index c61c29e90e..d400f8ae90 100644 --- a/src/applications/herald/controller/HeraldRuleController.php +++ b/src/applications/herald/controller/HeraldRuleController.php @@ -265,7 +265,15 @@ final class HeraldRuleController extends HeraldController { $new_name = $request->getStr('name'); $match_all = ($request->getStr('must_match') == 'all'); - $repetition_policy_param = $request->getStr('repetition_policy'); + $repetition_policy = $request->getStr('repetition_policy'); + + // If the user selected an invalid policy, or there's only one possible + // value so we didn't render a control, adjust the value to the first + // valid policy value. + $repetition_options = $this->getRepetitionOptionMap($adapter); + if (!isset($repetition_options[$repetition_policy])) { + $repetition_policy = head_key($repetition_options); + } $e_name = true; $errors = array(); @@ -348,7 +356,7 @@ final class HeraldRuleController extends HeraldController { $match_all, $conditions, $actions, - $repetition_policy_param); + $repetition_policy); $xactions = array(); $xactions[] = id(new HeraldRuleTransaction()) @@ -373,7 +381,7 @@ final class HeraldRuleController extends HeraldController { // mutate current rule, so it would be sent to the client in the right state $rule->setMustMatchAll((int)$match_all); $rule->setName($new_name); - $rule->setRepetitionPolicyStringConstant($repetition_policy_param); + $rule->setRepetitionPolicyStringConstant($repetition_policy); $rule->attachConditions($conditions); $rule->attachActions($actions); @@ -594,13 +602,9 @@ final class HeraldRuleController extends HeraldController { */ private function renderRepetitionSelector($rule, HeraldAdapter $adapter) { $repetition_policy = $rule->getRepetitionPolicyStringConstant(); - - $repetition_options = $adapter->getRepetitionOptions(); - $repetition_names = HeraldRule::getRepetitionPolicySelectOptionMap(); - $repetition_map = array_select_keys($repetition_names, $repetition_options); - + $repetition_map = $this->getRepetitionOptionMap($adapter); if (count($repetition_map) < 2) { - return head($repetition_names); + return head($repetition_map); } else { return AphrontFormSelectControl::renderSelectTag( $repetition_policy, @@ -611,6 +615,11 @@ final class HeraldRuleController extends HeraldController { } } + private function getRepetitionOptionMap(HeraldAdapter $adapter) { + $repetition_options = $adapter->getRepetitionOptions(); + $repetition_names = HeraldRule::getRepetitionPolicySelectOptionMap(); + return array_select_keys($repetition_names, $repetition_options); + } protected function buildTokenizerTemplates() { $template = new AphrontTokenizerTemplateView(); diff --git a/src/applications/herald/controller/HeraldTestConsoleController.php b/src/applications/herald/controller/HeraldTestConsoleController.php index 8a7a94963d..4ddab2669b 100644 --- a/src/applications/herald/controller/HeraldTestConsoleController.php +++ b/src/applications/herald/controller/HeraldTestConsoleController.php @@ -41,6 +41,7 @@ final class HeraldTestConsoleController extends HeraldController { $adapter ->setIsNewObject(false) + ->setActingAsPHID($viewer->getPHID()) ->setViewer($viewer); $rules = id(new HeraldRuleQuery()) diff --git a/src/applications/herald/field/HeraldActingUserField.php b/src/applications/herald/field/HeraldActingUserField.php new file mode 100644 index 0000000000..2245c7e9f7 --- /dev/null +++ b/src/applications/herald/field/HeraldActingUserField.php @@ -0,0 +1,32 @@ +getAdapter()->getActingAsPHID(); + } + + protected function getHeraldFieldStandardType() { + return self::STANDARD_PHID; + } + + protected function getDatasource() { + return new PhabricatorPeopleDatasource(); + } + + public function supportsObject($object) { + return true; + } + + public function getFieldGroupKey() { + return HeraldEditFieldGroup::FIELDGROUPKEY; + } + +} diff --git a/src/applications/legalpad/editor/LegalpadDocumentEditor.php b/src/applications/legalpad/editor/LegalpadDocumentEditor.php index 35f2487a81..e4b43186ee 100644 --- a/src/applications/legalpad/editor/LegalpadDocumentEditor.php +++ b/src/applications/legalpad/editor/LegalpadDocumentEditor.php @@ -124,12 +124,10 @@ final class LegalpadDocumentEditor protected function buildMailTemplate(PhabricatorLiskDAO $object) { $id = $object->getID(); - $phid = $object->getPHID(); $title = $object->getDocumentBody()->getTitle(); return id(new PhabricatorMetaMTAMail()) - ->setSubject("L{$id}: {$title}") - ->addHeader('Thread-Topic', "L{$id}: {$phid}"); + ->setSubject("L{$id}: {$title}"); } protected function getMailTo(PhabricatorLiskDAO $object) { diff --git a/src/applications/legalpad/query/LegalpadDocumentSearchEngine.php b/src/applications/legalpad/query/LegalpadDocumentSearchEngine.php index 1b608b02e3..a245417483 100644 --- a/src/applications/legalpad/query/LegalpadDocumentSearchEngine.php +++ b/src/applications/legalpad/query/LegalpadDocumentSearchEngine.php @@ -176,7 +176,7 @@ final class LegalpadDocumentSearchEngine $create_button = id(new PHUIButtonView()) ->setTag('a') ->setText(pht('Create a Document')) - ->setHref('/legalpad/create/') + ->setHref('/legalpad/edit/') ->setColor(PHUIButtonView::GREEN); $icon = $this->getApplication()->getIcon(); diff --git a/src/applications/macro/editor/PhabricatorMacroEditor.php b/src/applications/macro/editor/PhabricatorMacroEditor.php index 5d28b78f5f..f59c29b426 100644 --- a/src/applications/macro/editor/PhabricatorMacroEditor.php +++ b/src/applications/macro/editor/PhabricatorMacroEditor.php @@ -35,8 +35,7 @@ final class PhabricatorMacroEditor $name = 'Image Macro "'.$name.'"'; return id(new PhabricatorMetaMTAMail()) - ->setSubject($name) - ->addHeader('Thread-Topic', $name); + ->setSubject($name); } protected function getMailTo(PhabricatorLiskDAO $object) { diff --git a/src/applications/maniphest/application/PhabricatorManiphestApplication.php b/src/applications/maniphest/application/PhabricatorManiphestApplication.php index 1d0988b365..4e5f613696 100644 --- a/src/applications/maniphest/application/PhabricatorManiphestApplication.php +++ b/src/applications/maniphest/application/PhabricatorManiphestApplication.php @@ -51,13 +51,13 @@ final class PhabricatorManiphestApplication extends PhabricatorApplication { '/T(?P[1-9]\d*)' => 'ManiphestTaskDetailController', '/maniphest/' => array( '(?:project/(?P[^/]+)/)?(?:type/(?P[^/]+)/)?(?:query/(?P[^/]+)/)?' => 'ManiphestTaskListController', + $this->getQueryRoutePattern() => 'ManiphestTaskListController', 'report/(?:(?P\w+)/)?' => 'ManiphestReportController', $this->getBulkRoutePattern('bulk/') => 'ManiphestBulkEditController', 'task/' => array( $this->getEditRoutePattern('edit/') => 'ManiphestTaskEditController', ), - 'export/(?P[^/]+)/' => 'ManiphestExportController', 'subpriority/' => 'ManiphestSubpriorityController', ), ); diff --git a/src/applications/maniphest/controller/ManiphestExportController.php b/src/applications/maniphest/controller/ManiphestExportController.php deleted file mode 100644 index 2b96bb96ed..0000000000 --- a/src/applications/maniphest/controller/ManiphestExportController.php +++ /dev/null @@ -1,137 +0,0 @@ -getViewer(); - $key = $request->getURIData('key'); - - $ok = @include_once 'PHPExcel.php'; - if (!$ok) { - $dialog = $this->newDialog(); - - $inst1 = pht( - 'This system does not have PHPExcel installed. This software '. - 'component is required to export tasks to Excel. Have your system '. - 'administrator install it from:'); - - $inst2 = pht( - 'Your PHP "%s" needs to be updated to include the '. - 'PHPExcel Classes directory.', - 'include_path'); - - $dialog->setTitle(pht('Excel Export Not Configured')); - $dialog->appendChild(hsprintf( - '

%s

'. - '
'. - '

'. - ''. - 'https://github.com/PHPOffice/PHPExcel'. - ''. - '

'. - '
'. - '

%s

', - $inst1, - $inst2)); - - $dialog->addCancelButton('/maniphest/'); - return id(new AphrontDialogResponse())->setDialog($dialog); - } - - // TODO: PHPExcel has a dependency on the PHP zip extension. We should test - // for that here, since it fatals if we don't have the ZipArchive class. - - $saved = id(new PhabricatorSavedQueryQuery()) - ->setViewer($viewer) - ->withQueryKeys(array($key)) - ->executeOne(); - if (!$saved) { - $engine = id(new ManiphestTaskSearchEngine()) - ->setViewer($viewer) - ->setProjectKey($this->projectKey) - ->setTaskTypeKey($this->taskTypeKey); - if ($engine->isBuiltinQuery($key)) { - $saved = $engine->buildSavedQueryFromBuiltin($key); - } - if (!$saved) { - return new Aphront404Response(); - } - } - - $formats = ManiphestExcelFormat::loadAllFormats(); - $export_formats = array(); - foreach ($formats as $format_class => $format_object) { - $export_formats[$format_class] = $format_object->getName(); - } - - if (!$request->isDialogFormPost()) { - $dialog = new AphrontDialogView(); - $dialog->setUser($viewer); - - $dialog->setTitle(pht('Export Tasks to Excel')); - $dialog->appendChild( - phutil_tag( - 'p', - array(), - pht('Do you want to export the query results to Excel?'))); - - $form = id(new PHUIFormLayoutView()) - ->appendChild( - id(new AphrontFormSelectControl()) - ->setLabel(pht('Format:')) - ->setName('excel-format') - ->setOptions($export_formats)); - - $dialog->appendChild($form); - - $dialog->addCancelButton('/maniphest/'); - $dialog->addSubmitButton(pht('Export to Excel')); - return id(new AphrontDialogResponse())->setDialog($dialog); - } - - $format = idx($formats, $request->getStr('excel-format')); - if ($format === null) { - throw new Exception(pht('Excel format object not found.')); - } - - $saved->makeEphemeral(); - $saved->setParameter('limit', PHP_INT_MAX); - - $engine = id(new ManiphestTaskSearchEngine()) - ->setViewer($viewer); - - $query = $engine->buildQueryFromSavedQuery($saved); - $query->setViewer($viewer); - $tasks = $query->execute(); - - $all_projects = array_mergev(mpull($tasks, 'getProjectPHIDs')); - $all_assigned = mpull($tasks, 'getOwnerPHID'); - - $handles = id(new PhabricatorHandleQuery()) - ->setViewer($viewer) - ->withPHIDs(array_merge($all_projects, $all_assigned)) - ->execute(); - - $workbook = new PHPExcel(); - $format->buildWorkbook($workbook, $tasks, $handles, $viewer); - $writer = PHPExcel_IOFactory::createWriter($workbook, 'Excel2007'); - - ob_start(); - $writer->save('php://output'); - $data = ob_get_clean(); - - $mime = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; - - return id(new AphrontFileResponse()) - ->setMimeType($mime) - ->setDownload($format->getFileName().'.xlsx') - ->setContent($data); - } - -} diff --git a/src/applications/maniphest/editor/ManiphestEditEngine.php b/src/applications/maniphest/editor/ManiphestEditEngine.php index b0e19d0a3f..41722a3124 100644 --- a/src/applications/maniphest/editor/ManiphestEditEngine.php +++ b/src/applications/maniphest/editor/ManiphestEditEngine.php @@ -196,6 +196,7 @@ EODOCS pht('New task owner, or `null` to unassign.')) ->setTransactionType(ManiphestTaskOwnerTransaction::TRANSACTIONTYPE) ->setIsCopyable(true) + ->setIsNullable(true) ->setSingleValue($object->getOwnerPHID()) ->setCommentActionLabel(pht('Assign / Claim')) ->setCommentActionValue($owner_value) diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php index 72d6069129..822cc14314 100644 --- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php +++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php @@ -206,8 +206,7 @@ final class ManiphestTransactionEditor $title = $object->getTitle(); return id(new PhabricatorMetaMTAMail()) - ->setSubject("T{$id}: {$title}") - ->addHeader('Thread-Topic', "T{$id}: ".$object->getOriginalTitle()); + ->setSubject("T{$id}: {$title}"); } protected function buildMailBody( @@ -534,7 +533,6 @@ final class ManiphestTransactionEditor 'status' => '""', 'priority' => 0, 'title' => '""', - 'originalTitle' => '""', 'description' => '""', 'dateCreated' => 0, 'dateModified' => 0, diff --git a/src/applications/maniphest/engineextension/ManiphestMailEngineExtension.php b/src/applications/maniphest/engineextension/ManiphestMailEngineExtension.php new file mode 100644 index 0000000000..ee38bdf604 --- /dev/null +++ b/src/applications/maniphest/engineextension/ManiphestMailEngineExtension.php @@ -0,0 +1,58 @@ +setKey('author') + ->setLabel(pht('Author')), + id(new PhabricatorPHIDMailStamp()) + ->setKey('task-owner') + ->setLabel(pht('Task Owner')), + id(new PhabricatorBoolMailStamp()) + ->setKey('task-unassigned') + ->setLabel(pht('Task Unassigned')), + id(new PhabricatorStringMailStamp()) + ->setKey('task-priority') + ->setLabel(pht('Task Priority')), + id(new PhabricatorStringMailStamp()) + ->setKey('task-status') + ->setLabel(pht('Task Status')), + id(new PhabricatorStringMailStamp()) + ->setKey('subtype') + ->setLabel(pht('Subtype')), + ); + } + + public function newMailStamps($object, array $xactions) { + $editor = $this->getEditor(); + $viewer = $this->getViewer(); + + $this->getMailStamp('author') + ->setValue($object->getAuthorPHID()); + + $this->getMailStamp('task-owner') + ->setValue($object->getOwnerPHID()); + + $this->getMailStamp('task-unassigned') + ->setValue(!$object->getOwnerPHID()); + + $this->getMailStamp('task-priority') + ->setValue($object->getPriority()); + + $this->getMailStamp('task-status') + ->setValue($object->getStatus()); + + $this->getMailStamp('subtype') + ->setValue($object->getSubtype()); + } + +} diff --git a/src/applications/maniphest/export/ManiphestExcelDefaultFormat.php b/src/applications/maniphest/export/ManiphestExcelDefaultFormat.php deleted file mode 100644 index 1451ae504e..0000000000 --- a/src/applications/maniphest/export/ManiphestExcelDefaultFormat.php +++ /dev/null @@ -1,140 +0,0 @@ -setActiveSheetIndex(0); - $sheet->setTitle(pht('Tasks')); - - $widths = array( - null, - 15, - null, - 10, - 15, - 15, - 60, - 30, - 20, - 100, - ); - - foreach ($widths as $col => $width) { - if ($width !== null) { - $sheet->getColumnDimension($this->col($col))->setWidth($width); - } - } - - $status_map = ManiphestTaskStatus::getTaskStatusMap(); - $pri_map = ManiphestTaskPriority::getTaskPriorityMap(); - - $date_format = null; - - $rows = array(); - $rows[] = array( - pht('ID'), - pht('Owner'), - pht('Status'), - pht('Priority'), - pht('Date Created'), - pht('Date Updated'), - pht('Title'), - pht('Tags'), - pht('URI'), - pht('Description'), - ); - - $is_date = array( - false, - false, - false, - false, - true, - true, - false, - false, - false, - false, - ); - - $header_format = array( - 'font' => array( - 'bold' => true, - ), - ); - - foreach ($tasks as $task) { - $task_owner = null; - if ($task->getOwnerPHID()) { - $task_owner = $handles[$task->getOwnerPHID()]->getName(); - } - - $projects = array(); - foreach ($task->getProjectPHIDs() as $phid) { - $projects[] = $handles[$phid]->getName(); - } - $projects = implode(', ', $projects); - - $rows[] = array( - 'T'.$task->getID(), - $task_owner, - idx($status_map, $task->getStatus(), '?'), - idx($pri_map, $task->getPriority(), '?'), - $this->computeExcelDate($task->getDateCreated()), - $this->computeExcelDate($task->getDateModified()), - $task->getTitle(), - $projects, - PhabricatorEnv::getProductionURI('/T'.$task->getID()), - id(new PhutilUTF8StringTruncator()) - ->setMaximumBytes(512) - ->truncateString($task->getDescription()), - ); - } - - foreach ($rows as $row => $cols) { - foreach ($cols as $col => $spec) { - $cell_name = $this->col($col).($row + 1); - $cell = $sheet - ->setCellValue($cell_name, $spec, $return_cell = true); - - if ($row == 0) { - $sheet->getStyle($cell_name)->applyFromArray($header_format); - } - - if ($is_date[$col]) { - $code = PHPExcel_Style_NumberFormat::FORMAT_DATE_YYYYMMDD2; - $sheet - ->getStyle($cell_name) - ->getNumberFormat() - ->setFormatCode($code); - } else { - $cell->setDataType(PHPExcel_Cell_DataType::TYPE_STRING); - } - } - } - } - - private function col($n) { - return chr(ord('A') + $n); - } - -} diff --git a/src/applications/maniphest/export/ManiphestExcelFormat.php b/src/applications/maniphest/export/ManiphestExcelFormat.php deleted file mode 100644 index e455a63656..0000000000 --- a/src/applications/maniphest/export/ManiphestExcelFormat.php +++ /dev/null @@ -1,35 +0,0 @@ -setAncestorClass(__CLASS__) - ->setSortMethod('getOrder') - ->execute(); - } - - abstract public function getName(); - abstract public function getFileName(); - - public function getOrder() { - return 0; - } - - protected function computeExcelDate($epoch) { - $seconds_per_day = (60 * 60 * 24); - $offset = ($seconds_per_day * 25569); - - return ($epoch + $offset) / $seconds_per_day; - } - - /** - * @phutil-external-symbol class PHPExcel - */ - abstract public function buildWorkbook( - PHPExcel $workbook, - array $tasks, - array $handles, - PhabricatorUser $user); - -} diff --git a/src/applications/maniphest/export/__tests__/ManiphestExcelFormatTestCase.php b/src/applications/maniphest/export/__tests__/ManiphestExcelFormatTestCase.php deleted file mode 100644 index a3c312fcd2..0000000000 --- a/src/applications/maniphest/export/__tests__/ManiphestExcelFormatTestCase.php +++ /dev/null @@ -1,10 +0,0 @@ -assertTrue(true); - } - -} diff --git a/src/applications/maniphest/query/ManiphestTaskQuery.php b/src/applications/maniphest/query/ManiphestTaskQuery.php index fdbd3640e1..93a4d72a21 100644 --- a/src/applications/maniphest/query/ManiphestTaskQuery.php +++ b/src/applications/maniphest/query/ManiphestTaskQuery.php @@ -23,6 +23,9 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery { private $parentTaskIDs; private $subtaskIDs; private $subtypes; + private $closedEpochMin; + private $closedEpochMax; + private $closerPHIDs; private $status = 'status-any'; const STATUS_ANY = 'status-any'; @@ -179,6 +182,17 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery { return $this; } + public function withClosedEpochBetween($min, $max) { + $this->closedEpochMin = $min; + $this->closedEpochMax = $max; + return $this; + } + + public function withCloserPHIDs(array $phids) { + $this->closerPHIDs = $phids; + return $this; + } + public function needSubscriberPHIDs($bool) { $this->needSubscriberPHIDs = $bool; return $this; @@ -411,6 +425,27 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery { $this->dateModifiedBefore); } + if ($this->closedEpochMin !== null) { + $where[] = qsprintf( + $conn, + 'task.closedEpoch >= %d', + $this->closedEpochMin); + } + + if ($this->closedEpochMax !== null) { + $where[] = qsprintf( + $conn, + 'task.closedEpoch <= %d', + $this->closedEpochMax); + } + + if ($this->closerPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'task.closerPHID IN (%Ls)', + $this->closerPHIDs); + } + if ($this->priorities !== null) { $where[] = qsprintf( $conn, @@ -754,7 +789,11 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery { 'outdated' => array( 'vector' => array('-updated', '-id'), 'name' => pht('Date Updated (Oldest First)'), - ), + ), + 'closed' => array( + 'vector' => array('closed', 'id'), + 'name' => pht('Date Closed (Latest First)'), + ), 'title' => array( 'vector' => array('title', 'id'), 'name' => pht('Title'), @@ -773,6 +812,7 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery { 'outdated', 'newest', 'oldest', + 'closed', 'title', )) + $orders; @@ -822,6 +862,12 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery { 'column' => 'dateModified', 'type' => 'int', ), + 'closed' => array( + 'table' => 'task', + 'column' => 'closedEpoch', + 'type' => 'int', + 'null' => 'tail', + ), ); } @@ -840,6 +886,7 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery { 'status' => $task->getStatus(), 'title' => $task->getTitle(), 'updated' => $task->getDateModified(), + 'closed' => $task->getClosedEpoch(), ); foreach ($keys as $key) { diff --git a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php index b910f5c9b3..6e92da1b15 100644 --- a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php +++ b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php @@ -128,6 +128,17 @@ final class ManiphestTaskSearchEngine id(new PhabricatorSearchDateField()) ->setLabel(pht('Updated Before')) ->setKey('modifiedEnd'), + id(new PhabricatorSearchDateField()) + ->setLabel(pht('Closed After')) + ->setKey('closedStart'), + id(new PhabricatorSearchDateField()) + ->setLabel(pht('Closed Before')) + ->setKey('closedEnd'), + id(new PhabricatorUsersSearchField()) + ->setLabel(pht('Closed By')) + ->setKey('closerPHIDs') + ->setAliases(array('closer', 'closerPHID', 'closers')) + ->setDescription(pht('Search for tasks closed by certain users.')), id(new PhabricatorSearchTextField()) ->setLabel(pht('Page Size')) ->setKey('limit'), @@ -155,6 +166,9 @@ final class ManiphestTaskSearchEngine 'createdEnd', 'modifiedStart', 'modifiedEnd', + 'closedStart', + 'closedEnd', + 'closerPHIDs', 'limit', ); } @@ -210,6 +224,14 @@ final class ManiphestTaskSearchEngine $query->withDateModifiedBefore($map['modifiedEnd']); } + if ($map['closedStart'] || $map['closedEnd']) { + $query->withClosedEpochBetween($map['closedStart'], $map['closedEnd']); + } + + if ($map['closerPHIDs']) { + $query->withCloserPHIDs($map['closerPHIDs']); + } + if ($map['hasParents'] !== null) { $query->withOpenParents($map['hasParents']); } @@ -485,4 +507,131 @@ final class ManiphestTaskSearchEngine return $view; } + + protected function newExportFields() { + $fields = array( + id(new PhabricatorStringExportField()) + ->setKey('monogram') + ->setLabel(pht('Monogram')), + id(new PhabricatorPHIDExportField()) + ->setKey('authorPHID') + ->setLabel(pht('Author PHID')), + id(new PhabricatorStringExportField()) + ->setKey('author') + ->setLabel(pht('Author')), + id(new PhabricatorPHIDExportField()) + ->setKey('ownerPHID') + ->setLabel(pht('Owner PHID')), + id(new PhabricatorStringExportField()) + ->setKey('owner') + ->setLabel(pht('Owner')), + id(new PhabricatorStringExportField()) + ->setKey('status') + ->setLabel(pht('Status')), + id(new PhabricatorStringExportField()) + ->setKey('statusName') + ->setLabel(pht('Status Name')), + id(new PhabricatorEpochExportField()) + ->setKey('dateClosed') + ->setLabel(pht('Date Closed')), + id(new PhabricatorPHIDExportField()) + ->setKey('closerPHID') + ->setLabel(pht('Closer PHID')), + id(new PhabricatorStringExportField()) + ->setKey('closer') + ->setLabel(pht('Closer')), + id(new PhabricatorStringExportField()) + ->setKey('priority') + ->setLabel(pht('Priority')), + id(new PhabricatorStringExportField()) + ->setKey('priorityName') + ->setLabel(pht('Priority Name')), + id(new PhabricatorStringExportField()) + ->setKey('subtype') + ->setLabel('Subtype'), + id(new PhabricatorURIExportField()) + ->setKey('uri') + ->setLabel(pht('URI')), + id(new PhabricatorStringExportField()) + ->setKey('title') + ->setLabel(pht('Title')), + id(new PhabricatorStringExportField()) + ->setKey('description') + ->setLabel(pht('Description')), + ); + + if (ManiphestTaskPoints::getIsEnabled()) { + $fields[] = id(new PhabricatorIntExportField()) + ->setKey('points') + ->setLabel('Points'); + } + + return $fields; + } + + protected function newExportData(array $tasks) { + $viewer = $this->requireViewer(); + + $phids = array(); + foreach ($tasks as $task) { + $phids[] = $task->getAuthorPHID(); + $phids[] = $task->getOwnerPHID(); + $phids[] = $task->getCloserPHID(); + } + $handles = $viewer->loadHandles($phids); + + $export = array(); + foreach ($tasks as $task) { + + $author_phid = $task->getAuthorPHID(); + if ($author_phid) { + $author_name = $handles[$author_phid]->getName(); + } else { + $author_name = null; + } + + $owner_phid = $task->getOwnerPHID(); + if ($owner_phid) { + $owner_name = $handles[$owner_phid]->getName(); + } else { + $owner_name = null; + } + + $closer_phid = $task->getCloserPHID(); + if ($closer_phid) { + $closer_name = $handles[$closer_phid]->getName(); + } else { + $closer_name = null; + } + + $status_value = $task->getStatus(); + $status_name = ManiphestTaskStatus::getTaskStatusName($status_value); + + $priority_value = $task->getPriority(); + $priority_name = ManiphestTaskPriority::getTaskPriorityName( + $priority_value); + + $export[] = array( + 'monogram' => $task->getMonogram(), + 'authorPHID' => $author_phid, + 'author' => $author_name, + 'ownerPHID' => $owner_phid, + 'owner' => $owner_name, + 'status' => $status_value, + 'statusName' => $status_name, + 'priority' => $priority_value, + 'priorityName' => $priority_name, + 'points' => $task->getPoints(), + 'subtype' => $task->getSubtype(), + 'title' => $task->getTitle(), + 'uri' => PhabricatorEnv::getProductionURI($task->getURI()), + 'description' => $task->getDescription(), + 'dateClosed' => $task->getClosedEpoch(), + 'closerPHID' => $closer_phid, + 'closer' => $closer_name, + ); + } + + return $export; + } } diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php index f72977c5b2..a93fe58c3f 100644 --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -31,7 +31,6 @@ final class ManiphestTask extends ManiphestDAO protected $subpriority = 0; protected $title = ''; - protected $originalTitle = ''; protected $description = ''; protected $originalEmailSource; protected $mailKey; @@ -45,6 +44,9 @@ final class ManiphestTask extends ManiphestDAO protected $points; protected $subtype; + protected $closedEpoch; + protected $closerPHID; + private $subscriberPHIDs = self::ATTACHABLE; private $groupByProjectPHID = self::ATTACHABLE; private $customFields = self::ATTACHABLE; @@ -83,7 +85,6 @@ final class ManiphestTask extends ManiphestDAO 'status' => 'text64', 'priority' => 'uint32', 'title' => 'sort', - 'originalTitle' => 'text', 'description' => 'text', 'mailKey' => 'bytes20', 'ownerOrdering' => 'text64?', @@ -92,6 +93,8 @@ final class ManiphestTask extends ManiphestDAO 'points' => 'double?', 'bridgedObjectPHID' => 'phid?', 'subtype' => 'text64', + 'closedEpoch' => 'epoch?', + 'closerPHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, @@ -133,6 +136,12 @@ final class ManiphestTask extends ManiphestDAO 'key_subtype' => array( 'columns' => array('subtype'), ), + 'key_closed' => array( + 'columns' => array('closedEpoch'), + ), + 'key_closer' => array( + 'columns' => array('closerPHID', 'closedEpoch'), + ), ), ) + parent::getConfiguration(); } @@ -176,14 +185,6 @@ final class ManiphestTask extends ManiphestDAO return $this; } - public function setTitle($title) { - $this->title = $title; - if (!$this->getID()) { - $this->originalTitle = $title; - } - return $this; - } - public function getMonogram() { return 'T'.$this->getID(); } @@ -512,6 +513,16 @@ final class ManiphestTask extends ManiphestDAO ->setKey('subtype') ->setType('string') ->setDescription(pht('Subtype of the task.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('closerPHID') + ->setType('phid?') + ->setDescription( + pht('User who closed the task, if the task is closed.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('dateClosed') + ->setType('int?') + ->setDescription( + pht('Epoch timestamp when the task was closed.')), ); } @@ -531,6 +542,11 @@ final class ManiphestTask extends ManiphestDAO 'color' => ManiphestTaskPriority::getTaskPriorityColor($priority_value), ); + $closed_epoch = $this->getClosedEpoch(); + if ($closed_epoch !== null) { + $closed_epoch = (int)$closed_epoch; + } + return array( 'name' => $this->getTitle(), 'description' => array( @@ -542,6 +558,8 @@ final class ManiphestTask extends ManiphestDAO 'priority' => $priority_info, 'points' => $this->getPoints(), 'subtype' => $this->getSubtype(), + 'closerPHID' => $this->getCloserPHID(), + 'dateClosed' => $closed_epoch, ); } diff --git a/src/applications/maniphest/view/ManiphestTaskListView.php b/src/applications/maniphest/view/ManiphestTaskListView.php index 5899be0c07..08f7b05f70 100644 --- a/src/applications/maniphest/view/ManiphestTaskListView.php +++ b/src/applications/maniphest/view/ManiphestTaskListView.php @@ -68,6 +68,11 @@ final class ManiphestTaskListView extends ManiphestView { ->setHeader($task->getTitle()) ->setHref('/T'.$task->getID()); + if ($task->getAuthorPHID()) { + $author = $handles[$task->getAuthorPHID()]; + $item->addByline(pht('By: %s', $author->renderLink())); + } + if ($task->getOwnerPHID()) { $owner = $handles[$task->getOwnerPHID()]; $item->addByline(pht('Assigned: %s', $owner->renderLink())); @@ -87,19 +92,24 @@ final class ManiphestTaskListView extends ManiphestView { $item->setStatusIcon($icon.' '.$color, $tooltip); - $date = phabricator_datetime($task->getDateModified(), $this->getUser()); + if ($task->isClosed()) { + $closed_epoch = $task->getClosedEpoch(); - if ($task->getAuthorPHID()) { - $author = idx($handles, $task->getAuthorPHID()); - // TODO: This should be guaranteed, see T3817. - if ($author) { - $date = array($date, " by ", $author->renderLink()); + // We don't expect a task to be closed without a closed epoch, but + // recover if we find one. This can happen with older objects or with + // lipsum test data. + if (!$closed_epoch) { + $closed_epoch = $task->getDateModified(); } - } - $item->addIcon( - 'none', - $date); + $item->addIcon( + 'fa-check-square-o grey', + phabricator_datetime($closed_epoch, $this->getUser())); + } else { + $item->addIcon( + 'none', + phabricator_datetime($task->getDateModified(), $this->getUser())); + } if ($this->showSubpriorityControls) { $item->setGrippable(true); diff --git a/src/applications/maniphest/view/ManiphestTaskResultListView.php b/src/applications/maniphest/view/ManiphestTaskResultListView.php index b4cf9a544c..6aafcbdccb 100644 --- a/src/applications/maniphest/view/ManiphestTaskResultListView.php +++ b/src/applications/maniphest/view/ManiphestTaskResultListView.php @@ -175,8 +175,7 @@ final class ManiphestTaskResultListView extends ManiphestView { } if (!$user->isLoggedIn()) { - // Don't show the batch editor or excel export for logged-out users. - // Technically we //could// let them export, but ehh. + // Don't show the batch editor for logged-out users. return null; } @@ -220,14 +219,6 @@ final class ManiphestTaskResultListView extends ManiphestView { ), pht("Bulk Edit Selected \xC2\xBB")); - $export = javelin_tag( - 'a', - array( - 'href' => '/maniphest/export/'.$saved_query->getQueryKey().'/', - 'class' => 'button button-grey', - ), - pht('Export to Excel')); - $hidden = phutil_tag( 'div', array( @@ -239,14 +230,12 @@ final class ManiphestTaskResultListView extends ManiphestView { ''. ''. ''. - ''. ''. ''. ''. '
%s%s%s%s%s%s
', $select_all, $select_none, - $export, '', $submit, $hidden); diff --git a/src/applications/maniphest/xaction/ManiphestTaskMergedIntoTransaction.php b/src/applications/maniphest/xaction/ManiphestTaskMergedIntoTransaction.php index cd0cad6a39..630f5190ce 100644 --- a/src/applications/maniphest/xaction/ManiphestTaskMergedIntoTransaction.php +++ b/src/applications/maniphest/xaction/ManiphestTaskMergedIntoTransaction.php @@ -10,7 +10,7 @@ final class ManiphestTaskMergedIntoTransaction } public function applyInternalEffects($object, $value) { - $object->setStatus(ManiphestTaskStatus::getDuplicateStatus()); + $this->updateStatus($object, ManiphestTaskStatus::getDuplicateStatus()); } public function getActionName() { diff --git a/src/applications/maniphest/xaction/ManiphestTaskStatusTransaction.php b/src/applications/maniphest/xaction/ManiphestTaskStatusTransaction.php index dd51a63799..6f4b558e05 100644 --- a/src/applications/maniphest/xaction/ManiphestTaskStatusTransaction.php +++ b/src/applications/maniphest/xaction/ManiphestTaskStatusTransaction.php @@ -10,7 +10,7 @@ final class ManiphestTaskStatusTransaction } public function applyInternalEffects($object, $value) { - $object->setStatus($value); + $this->updateStatus($object, $value); } public function shouldHide() { diff --git a/src/applications/maniphest/xaction/ManiphestTaskTransactionType.php b/src/applications/maniphest/xaction/ManiphestTaskTransactionType.php index c59de163c6..836e7765b8 100644 --- a/src/applications/maniphest/xaction/ManiphestTaskTransactionType.php +++ b/src/applications/maniphest/xaction/ManiphestTaskTransactionType.php @@ -3,4 +3,27 @@ abstract class ManiphestTaskTransactionType extends PhabricatorModularTransactionType { + protected function updateStatus($object, $new_value) { + $old_value = $object->getStatus(); + $object->setStatus($new_value); + + // If this status change closes or opens the task, update the closed + // date and actor PHID. + $old_closed = ManiphestTaskStatus::isClosedStatus($old_value); + $new_closed = ManiphestTaskStatus::isClosedStatus($new_value); + + $is_close = ($new_closed && !$old_closed); + $is_open = (!$new_closed && $old_closed); + + if ($is_close) { + $object + ->setClosedEpoch(PhabricatorTime::getNow()) + ->setCloserPHID($this->getActingAsPHID()); + } else if ($is_open) { + $object + ->setClosedEpoch(null) + ->setCloserPHID(null); + } + } + } diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationAdapter.php index 3363301909..dfbe891651 100644 --- a/src/applications/metamta/adapter/PhabricatorMailImplementationAdapter.php +++ b/src/applications/metamta/adapter/PhabricatorMailImplementationAdapter.php @@ -2,6 +2,22 @@ abstract class PhabricatorMailImplementationAdapter extends Phobject { + private $key; + private $priority; + private $options = array(); + + final public function getAdapterType() { + return $this->getPhobjectClassConstant('ADAPTERTYPE'); + } + + final public static function getAllAdapters() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getAdapterType') + ->execute(); + } + + abstract public function setFrom($email, $name = ''); abstract public function addReplyTo($email, $name = ''); abstract public function addTos(array $emails); @@ -12,6 +28,7 @@ abstract class PhabricatorMailImplementationAdapter extends Phobject { abstract public function setHTMLBody($html_body); abstract public function setSubject($subject); + /** * Some mailers, notably Amazon SES, do not support us setting a specific * Message-ID header. @@ -32,4 +49,59 @@ abstract class PhabricatorMailImplementationAdapter extends Phobject { */ abstract public function send(); + final public function setKey($key) { + $this->key = $key; + return $this; + } + + final public function getKey() { + return $this->key; + } + + final public function setPriority($priority) { + $this->priority = $priority; + return $this; + } + + final public function getPriority() { + return $this->priority; + } + + final public function getOption($key) { + if (!array_key_exists($key, $this->options)) { + throw new Exception( + pht( + 'Mailer ("%s") is attempting to access unknown option ("%s").', + get_class($this), + $key)); + } + + return $this->options[$key]; + } + + final public function setOptions(array $options) { + $this->validateOptions($options); + $this->options = $options; + return $this; + } + + abstract protected function validateOptions(array $options); + + abstract public function newDefaultOptions(); + abstract public function newLegacyOptions(); + + public function prepareForSend() { + return; + } + + protected function renderAddress($email, $name = null) { + if (strlen($name)) { + return (string)id(new PhutilEmailAddress()) + ->setDisplayName($name) + ->setAddress($email); + } else { + return $email; + } + } + } diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationAmazonSESAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationAmazonSESAdapter.php index 5b03cd86ac..22cc102262 100644 --- a/src/applications/metamta/adapter/PhabricatorMailImplementationAmazonSESAdapter.php +++ b/src/applications/metamta/adapter/PhabricatorMailImplementationAmazonSESAdapter.php @@ -3,11 +3,13 @@ final class PhabricatorMailImplementationAmazonSESAdapter extends PhabricatorMailImplementationPHPMailerLiteAdapter { + const ADAPTERTYPE = 'ses'; + private $message; private $isHTML; - public function __construct() { - parent::__construct(); + public function prepareForSend() { + parent::prepareForSend(); $this->mailer->Mailer = 'amazon-ses'; $this->mailer->customMailer = $this; } @@ -17,13 +19,39 @@ final class PhabricatorMailImplementationAmazonSESAdapter return false; } + protected function validateOptions(array $options) { + PhutilTypeSpec::checkMap( + $options, + array( + 'access-key' => 'string', + 'secret-key' => 'string', + 'endpoint' => 'string', + )); + } + + public function newDefaultOptions() { + return array( + 'access-key' => null, + 'secret-key' => null, + 'endpoint' => null, + ); + } + + public function newLegacyOptions() { + return array( + 'access-key' => PhabricatorEnv::getEnvConfig('amazon-ses.access-key'), + 'secret-key' => PhabricatorEnv::getEnvConfig('amazon-ses.secret-key'), + 'endpoint' => PhabricatorEnv::getEnvConfig('amazon-ses.endpoint'), + ); + } + /** * @phutil-external-symbol class SimpleEmailService */ public function executeSend($body) { - $key = PhabricatorEnv::getEnvConfig('amazon-ses.access-key'); - $secret = PhabricatorEnv::getEnvConfig('amazon-ses.secret-key'); - $endpoint = PhabricatorEnv::getEnvConfig('amazon-ses.endpoint'); + $key = $this->getOption('access-key'); + $secret = $this->getOption('secret-key'); + $endpoint = $this->getOption('endpoint'); $root = phutil_get_library_root('phabricator'); $root = dirname($root); diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php index cfe6491fe0..349dae2d27 100644 --- a/src/applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php +++ b/src/applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php @@ -6,6 +6,8 @@ final class PhabricatorMailImplementationMailgunAdapter extends PhabricatorMailImplementationAdapter { + const ADAPTERTYPE = 'mailgun'; + private $params = array(); private $attachments = array(); @@ -19,7 +21,7 @@ final class PhabricatorMailImplementationMailgunAdapter if (empty($this->params['reply-to'])) { $this->params['reply-to'] = array(); } - $this->params['reply-to'][] = "{$name} <{$email}>"; + $this->params['reply-to'][] = $this->renderAddress($email, $name); return $this; } @@ -71,9 +73,32 @@ final class PhabricatorMailImplementationMailgunAdapter return true; } + protected function validateOptions(array $options) { + PhutilTypeSpec::checkMap( + $options, + array( + 'api-key' => 'string', + 'domain' => 'string', + )); + } + + public function newDefaultOptions() { + return array( + 'api-key' => null, + 'domain' => null, + ); + } + + public function newLegacyOptions() { + return array( + 'api-key' => PhabricatorEnv::getEnvConfig('mailgun.api-key'), + 'domain' => PhabricatorEnv::getEnvConfig('mailgun.domain'), + ); + } + public function send() { - $key = PhabricatorEnv::getEnvConfig('mailgun.api-key'); - $domain = PhabricatorEnv::getEnvConfig('mailgun.domain'); + $key = $this->getOption('api-key'); + $domain = $this->getOption('domain'); $params = array(); $params['to'] = implode(', ', idx($this->params, 'tos', array())); @@ -85,11 +110,8 @@ final class PhabricatorMailImplementationMailgunAdapter } $from = idx($this->params, 'from'); - if (idx($this->params, 'from-name')) { - $params['from'] = "\"{$this->params['from-name']}\" <{$from}>"; - } else { - $params['from'] = $from; - } + $from_name = idx($this->params, 'from-name'); + $params['from'] = $this->renderAddress($from, $from_name); if (idx($this->params, 'reply-to')) { $replyto = $this->params['reply-to']; diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerAdapter.php index 2fadd6491f..3ca6366730 100644 --- a/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerAdapter.php +++ b/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerAdapter.php @@ -3,40 +3,79 @@ final class PhabricatorMailImplementationPHPMailerAdapter extends PhabricatorMailImplementationAdapter { + const ADAPTERTYPE = 'smtp'; + private $mailer; + protected function validateOptions(array $options) { + PhutilTypeSpec::checkMap( + $options, + array( + 'host' => 'string|null', + 'port' => 'int', + 'user' => 'string|null', + 'password' => 'string|null', + 'protocol' => 'string|null', + 'encoding' => 'string', + 'mailer' => 'string', + )); + } + + public function newDefaultOptions() { + return array( + 'host' => null, + 'port' => 25, + 'user' => null, + 'password' => null, + 'protocol' => null, + 'encoding' => 'base64', + 'mailer' => 'smtp', + ); + } + + public function newLegacyOptions() { + return array( + 'host' => PhabricatorEnv::getEnvConfig('phpmailer.smtp-host'), + 'port' => PhabricatorEnv::getEnvConfig('phpmailer.smtp-port'), + 'user' => PhabricatorEnv::getEnvConfig('phpmailer.smtp-user'), + 'password' => PhabricatorEnv::getEnvConfig('phpmailer.smtp-passsword'), + 'protocol' => PhabricatorEnv::getEnvConfig('phpmailer.smtp-protocol'), + 'encoding' => PhabricatorEnv::getEnvConfig('phpmailer.smtp-encoding'), + 'mailer' => PhabricatorEnv::getEnvConfig('phpmailer.mailer'), + ); + } + /** * @phutil-external-symbol class PHPMailer */ - public function __construct() { + public function prepareForSend() { $root = phutil_get_library_root('phabricator'); $root = dirname($root); require_once $root.'/externals/phpmailer/class.phpmailer.php'; $this->mailer = new PHPMailer($use_exceptions = true); $this->mailer->CharSet = 'utf-8'; - $encoding = PhabricatorEnv::getEnvConfig('phpmailer.smtp-encoding'); + $encoding = $this->getOption('encoding'); $this->mailer->Encoding = $encoding; // By default, PHPMailer sends one mail per recipient. We handle - // multiplexing higher in the stack, so tell it to send mail exactly - // like we ask. + // combining or separating To and Cc higher in the stack, so tell it to + // send mail exactly like we ask. $this->mailer->SingleTo = false; - $mailer = PhabricatorEnv::getEnvConfig('phpmailer.mailer'); + $mailer = $this->getOption('mailer'); if ($mailer == 'smtp') { $this->mailer->IsSMTP(); - $this->mailer->Host = PhabricatorEnv::getEnvConfig('phpmailer.smtp-host'); - $this->mailer->Port = PhabricatorEnv::getEnvConfig('phpmailer.smtp-port'); - $user = PhabricatorEnv::getEnvConfig('phpmailer.smtp-user'); + $this->mailer->Host = $this->getOption('host'); + $this->mailer->Port = $this->getOption('port'); + $user = $this->getOption('user'); if ($user) { $this->mailer->SMTPAuth = true; $this->mailer->Username = $user; - $this->mailer->Password = - PhabricatorEnv::getEnvConfig('phpmailer.smtp-password'); + $this->mailer->Password = $this->getOption('password'); } - $protocol = PhabricatorEnv::getEnvConfig('phpmailer.smtp-protocol'); + $protocol = $this->getOption('protocol'); if ($protocol) { $protocol = phutil_utf8_strtolower($protocol); $this->mailer->SMTPSecure = $protocol; diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php index f072e769c3..1f21a993c9 100644 --- a/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php +++ b/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php @@ -6,24 +6,46 @@ class PhabricatorMailImplementationPHPMailerLiteAdapter extends PhabricatorMailImplementationAdapter { + const ADAPTERTYPE = 'sendmail'; + protected $mailer; + protected function validateOptions(array $options) { + PhutilTypeSpec::checkMap( + $options, + array( + 'encoding' => 'string', + )); + } + + public function newDefaultOptions() { + return array( + 'encoding' => 'base64', + ); + } + + public function newLegacyOptions() { + return array( + 'encoding' => PhabricatorEnv::getEnvConfig('phpmailer.smtp-encoding'), + ); + } + /** * @phutil-external-symbol class PHPMailerLite */ - public function __construct() { + public function prepareForSend() { $root = phutil_get_library_root('phabricator'); $root = dirname($root); require_once $root.'/externals/phpmailer/class.phpmailer-lite.php'; $this->mailer = new PHPMailerLite($use_exceptions = true); $this->mailer->CharSet = 'utf-8'; - $encoding = PhabricatorEnv::getEnvConfig('phpmailer.smtp-encoding'); + $encoding = $this->getOption('encoding'); $this->mailer->Encoding = $encoding; // By default, PHPMailerLite sends one mail per recipient. We handle - // multiplexing higher in the stack, so tell it to send mail exactly - // like we ask. + // combining or separating To and Cc higher in the stack, so tell it to + // send mail exactly like we ask. $this->mailer->SingleTo = false; } diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationPostmarkAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationPostmarkAdapter.php new file mode 100644 index 0000000000..5792ba08f8 --- /dev/null +++ b/src/applications/metamta/adapter/PhabricatorMailImplementationPostmarkAdapter.php @@ -0,0 +1,124 @@ +parameters['From'] = $this->renderAddress($email, $name); + return $this; + } + + public function addReplyTo($email, $name = '') { + $this->parameters['ReplyTo'] = $this->renderAddress($email, $name); + return $this; + } + + public function addTos(array $emails) { + foreach ($emails as $email) { + $this->parameters['To'][] = $email; + } + return $this; + } + + public function addCCs(array $emails) { + foreach ($emails as $email) { + $this->parameters['Cc'][] = $email; + } + return $this; + } + + public function addAttachment($data, $filename, $mimetype) { + $this->parameters['Attachments'][] = array( + 'Name' => $filename, + 'ContentType' => $mimetype, + 'Content' => base64_encode($data), + ); + + return $this; + } + + public function addHeader($header_name, $header_value) { + $this->parameters['Headers'][] = array( + 'Name' => $header_name, + 'Value' => $header_value, + ); + return $this; + } + + public function setBody($body) { + $this->parameters['TextBody'] = $body; + return $this; + } + + public function setHTMLBody($html_body) { + $this->parameters['HtmlBody'] = $html_body; + return $this; + } + + public function setSubject($subject) { + $this->parameters['Subject'] = $subject; + return $this; + } + + public function supportsMessageIDHeader() { + return true; + } + + protected function validateOptions(array $options) { + PhutilTypeSpec::checkMap( + $options, + array( + 'access-token' => 'string', + 'inbound-addresses' => 'list', + )); + + // Make sure this is properly formatted. + PhutilCIDRList::newList($options['inbound-addresses']); + } + + public function newDefaultOptions() { + return array( + 'access-token' => null, + 'inbound-addresses' => array( + // Via Postmark support circa February 2018, see: + // + // https://postmarkapp.com/support/article/800-ips-for-firewalls + // + // "Configuring Outbound Email" should be updated if this changes. + '50.31.156.6/32', + ), + ); + } + + public function newLegacyOptions() { + return array(); + } + + public function send() { + $access_token = $this->getOption('access-token'); + + $parameters = $this->parameters; + $flatten = array( + 'To', + 'Cc', + ); + + foreach ($flatten as $key) { + if (isset($parameters[$key])) { + $parameters[$key] = implode(', ', $parameters[$key]); + } + } + + id(new PhutilPostmarkFuture()) + ->setAccessToken($access_token) + ->setMethod('email', $parameters) + ->resolve(); + + return true; + } + +} diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php index 566d33fd14..be2a837053 100644 --- a/src/applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php +++ b/src/applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php @@ -6,8 +6,33 @@ final class PhabricatorMailImplementationSendGridAdapter extends PhabricatorMailImplementationAdapter { + const ADAPTERTYPE = 'sendgrid'; + private $params = array(); + protected function validateOptions(array $options) { + PhutilTypeSpec::checkMap( + $options, + array( + 'api-user' => 'string', + 'api-key' => 'string', + )); + } + + public function newDefaultOptions() { + return array( + 'api-user' => null, + 'api-key' => null, + ); + } + + public function newLegacyOptions() { + return array( + 'api-user' => PhabricatorEnv::getEnvConfig('sendgrid.api-user'), + 'api-key' => PhabricatorEnv::getEnvConfig('sendgrid.api-key'), + ); + } + public function setFrom($email, $name = '') { $this->params['from'] = $email; $this->params['from-name'] = $name; @@ -73,8 +98,8 @@ final class PhabricatorMailImplementationSendGridAdapter public function send() { - $user = PhabricatorEnv::getEnvConfig('sendgrid.api-user'); - $key = PhabricatorEnv::getEnvConfig('sendgrid.api-key'); + $user = $this->getOption('api-user'); + $key = $this->getOption('api-key'); if (!$user || !$key) { throw new Exception( diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php index 0ea2af916f..8a8d0de0c2 100644 --- a/src/applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php +++ b/src/applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php @@ -7,10 +7,26 @@ final class PhabricatorMailImplementationTestAdapter extends PhabricatorMailImplementationAdapter { - private $guts = array(); - private $config; + const ADAPTERTYPE = 'test'; - public function __construct(array $config = array()) { + private $guts = array(); + private $config = array(); + + protected function validateOptions(array $options) { + PhutilTypeSpec::checkMap( + $options, + array()); + } + + public function newDefaultOptions() { + return array(); + } + + public function newLegacyOptions() { + return array(); + } + + public function prepareForSend(array $config = array()) { $this->config = $config; } diff --git a/src/applications/metamta/application/PhabricatorMetaMTAApplication.php b/src/applications/metamta/application/PhabricatorMetaMTAApplication.php index adb08aaa24..f53af55035 100644 --- a/src/applications/metamta/application/PhabricatorMetaMTAApplication.php +++ b/src/applications/metamta/application/PhabricatorMetaMTAApplication.php @@ -42,6 +42,7 @@ final class PhabricatorMetaMTAApplication extends PhabricatorApplication { 'detail/(?P[1-9]\d*)/' => 'PhabricatorMetaMTAMailViewController', 'sendgrid/' => 'PhabricatorMetaMTASendGridReceiveController', 'mailgun/' => 'PhabricatorMetaMTAMailgunReceiveController', + 'postmark/' => 'PhabricatorMetaMTAPostmarkReceiveController', ), ); } diff --git a/src/applications/metamta/controller/PhabricatorMetaMTAMailViewController.php b/src/applications/metamta/controller/PhabricatorMetaMTAMailViewController.php index 80da535a9e..9b33397831 100644 --- a/src/applications/metamta/controller/PhabricatorMetaMTAMailViewController.php +++ b/src/applications/metamta/controller/PhabricatorMetaMTAMailViewController.php @@ -32,6 +32,23 @@ final class PhabricatorMetaMTAMailViewController $color = PhabricatorMailOutboundStatus::getStatusColor($status); $header->setStatus($icon, $color, $name); + if ($mail->getMustEncrypt()) { + Javelin::initBehavior('phabricator-tooltips'); + $header->addTag( + id(new PHUITagView()) + ->setType(PHUITagView::TYPE_SHADE) + ->setColor('blue') + ->setName(pht('Must Encrypt')) + ->setIcon('fa-shield blue') + ->addSigil('has-tooltip') + ->setMetadata( + array( + 'tip' => pht( + 'Message content can only be transmitted over secure '. + 'channels.'), + ))); + } + $crumbs = $this->buildApplicationCrumbs() ->addTextCrumb(pht('Mail %d', $mail->getID())) ->setBorder(true); @@ -58,8 +75,26 @@ final class PhabricatorMetaMTAMailViewController ->setKey('metadata') ->appendChild($this->buildMetadataProperties($mail))); + $header_view = id(new PHUIHeaderView()) + ->setHeader(pht('Mail')); + + $object_phid = $mail->getRelatedPHID(); + if ($object_phid) { + $handles = $viewer->loadHandles(array($object_phid)); + $handle = $handles[$object_phid]; + if ($handle->isComplete() && $handle->getURI()) { + $view_button = id(new PHUIButtonView()) + ->setTag('a') + ->setText(pht('View Object')) + ->setIcon('fa-chevron-right') + ->setHref($handle->getURI()); + + $header_view->addActionLink($view_button); + } + } + $object_box = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Mail')) + ->setHeader($header_view) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->addTabGroup($tab_group); @@ -134,6 +169,12 @@ final class PhabricatorMetaMTAMailViewController $properties->addTextContent($body); + $file_phids = $mail->getAttachmentFilePHIDs(); + if ($file_phids) { + $properties->addProperty( + pht('Attached Files'), + $viewer->loadHandles($file_phids)->renderList()); + } return $properties; } @@ -158,6 +199,15 @@ final class PhabricatorMetaMTAMailViewController $properties->addProperty($key, $value); } + $encrypt_phids = $mail->getMustEncryptReasons(); + if ($encrypt_phids) { + $properties->addProperty( + pht('Must Encrypt'), + $viewer->loadHandles($encrypt_phids) + ->renderList()); + } + + return $properties; } diff --git a/src/applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php b/src/applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php index 467995a186..3ca2711dcf 100644 --- a/src/applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php +++ b/src/applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php @@ -8,14 +8,28 @@ final class PhabricatorMetaMTAMailgunReceiveController } private function verifyMessage() { - $api_key = PhabricatorEnv::getEnvConfig('mailgun.api-key'); $request = $this->getRequest(); $timestamp = $request->getStr('timestamp'); $token = $request->getStr('token'); $sig = $request->getStr('signature'); - $hash = hash_hmac('sha256', $timestamp.$token, $api_key); - return phutil_hashes_are_identical($sig, $hash); + // An install may configure multiple Mailgun mailers, and we might receive + // inbound mail from any of them. Test the signature to see if it matches + // any configured Mailgun mailer. + + $mailers = PhabricatorMetaMTAMail::newMailersWithTypes( + array( + PhabricatorMailImplementationMailgunAdapter::ADAPTERTYPE, + )); + foreach ($mailers as $mailer) { + $api_key = $mailer->getOption('api-key'); + $hash = hash_hmac('sha256', $timestamp.$token, $api_key); + if (phutil_hashes_are_identical($sig, $hash)) { + return true; + } + } + + return false; } public function handleRequest(AphrontRequest $request) { diff --git a/src/applications/metamta/controller/PhabricatorMetaMTAPostmarkReceiveController.php b/src/applications/metamta/controller/PhabricatorMetaMTAPostmarkReceiveController.php new file mode 100644 index 0000000000..345cd93fe1 --- /dev/null +++ b/src/applications/metamta/controller/PhabricatorMetaMTAPostmarkReceiveController.php @@ -0,0 +1,102 @@ +getRemoteAddress(); + $any_remote_match = false; + foreach ($mailers as $mailer) { + $inbound_addresses = $mailer->getOption('inbound-addresses'); + $cidr_list = PhutilCIDRList::newList($inbound_addresses); + if ($cidr_list->containsAddress($remote_address)) { + $any_remote_match = true; + break; + } + } + + if (!$any_remote_match) { + return new Aphront400Response(); + } + + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + $raw_input = PhabricatorStartup::getRawInput(); + + try { + $data = phutil_json_decode($raw_input); + } catch (Exception $ex) { + return new Aphront400Response(); + } + + $raw_headers = array(); + $header_items = idx($data, 'Headers', array()); + foreach ($header_items as $header_item) { + $name = idx($header_item, 'Name'); + $value = idx($header_item, 'Value'); + $raw_headers[$name] = $value; + } + + $headers = array( + 'to' => idx($data, 'To'), + 'from' => idx($data, 'From'), + 'cc' => idx($data, 'Cc'), + 'subject' => idx($data, 'Subject'), + ) + $raw_headers; + + + $received = id(new PhabricatorMetaMTAReceivedMail()) + ->setHeaders($headers) + ->setBodies( + array( + 'text' => idx($data, 'TextBody'), + 'html' => idx($data, 'HtmlBody'), + )); + + $file_phids = array(); + $attachments = idx($data, 'Attachments', array()); + foreach ($attachments as $attachment) { + $file_data = idx($attachment, 'Content'); + $file_data = base64_decode($file_data); + + try { + $file = PhabricatorFile::newFromFileData( + $file_data, + array( + 'name' => idx($attachment, 'Name'), + 'viewPolicy' => PhabricatorPolicies::POLICY_NOONE, + )); + $file_phids[] = $file->getPHID(); + } catch (Exception $ex) { + phlog($ex); + } + } + $received->setAttachments($file_phids); + + try { + $received->save(); + $received->processReceivedMail(); + } catch (Exception $ex) { + phlog($ex); + } + + return id(new AphrontWebpageResponse()) + ->setContent(pht("Got it! Thanks, Postmark!\n")); + } + +} diff --git a/src/applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php b/src/applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php index 0a5e28fcee..6651f85d6c 100644 --- a/src/applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php +++ b/src/applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php @@ -8,6 +8,16 @@ final class PhabricatorMetaMTASendGridReceiveController } public function handleRequest(AphrontRequest $request) { + // SendGrid doesn't sign payloads so we can't be sure that SendGrid + // actually sent this request, but require a configured SendGrid mailer + // before we activate this endpoint. + $mailers = PhabricatorMetaMTAMail::newMailersWithTypes( + array( + PhabricatorMailImplementationSendGridAdapter::ADAPTERTYPE, + )); + if (!$mailers) { + return new Aphront404Response(); + } // No CSRF for SendGrid. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); diff --git a/src/applications/metamta/engine/PhabricatorMailEngineExtension.php b/src/applications/metamta/engine/PhabricatorMailEngineExtension.php new file mode 100644 index 0000000000..36675dda4a --- /dev/null +++ b/src/applications/metamta/engine/PhabricatorMailEngineExtension.php @@ -0,0 +1,47 @@ +getPhobjectClassConstant('EXTENSIONKEY'); + } + + final public function setViewer($viewer) { + $this->viewer = $viewer; + return $this; + } + + final public function getViewer() { + return $this->viewer; + } + + final public function setEditor( + PhabricatorApplicationTransactionEditor $editor) { + $this->editor = $editor; + return $this; + } + + final public function getEditor() { + return $this->editor; + } + + abstract public function supportsObject($object); + abstract public function newMailStampTemplates($object); + abstract public function newMailStamps($object, array $xactions); + + final public static function getAllExtensions() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getExtensionKey') + ->execute(); + } + + final protected function getMailStamp($key) { + return $this->getEditor()->getMailStamp($key); + } + +} diff --git a/src/applications/metamta/garbagecollector/MetaMTAMailSentGarbageCollector.php b/src/applications/metamta/garbagecollector/MetaMTAMailSentGarbageCollector.php index c9ca274436..dacd46d187 100644 --- a/src/applications/metamta/garbagecollector/MetaMTAMailSentGarbageCollector.php +++ b/src/applications/metamta/garbagecollector/MetaMTAMailSentGarbageCollector.php @@ -18,8 +18,9 @@ final class MetaMTAMailSentGarbageCollector 'dateCreated < %d LIMIT 100', $this->getGarbageEpoch()); + $engine = new PhabricatorDestructionEngine(); foreach ($mails as $mail) { - $mail->delete(); + $engine->destroyObject($mail); } return (count($mails) == 100); diff --git a/src/applications/metamta/herald/PhabricatorMailMustEncryptHeraldAction.php b/src/applications/metamta/herald/PhabricatorMailMustEncryptHeraldAction.php new file mode 100644 index 0000000000..027e1bb733 --- /dev/null +++ b/src/applications/metamta/herald/PhabricatorMailMustEncryptHeraldAction.php @@ -0,0 +1,62 @@ +getRule()->getPHID(); + + $adapter = $this->getAdapter(); + $adapter->addMustEncryptReason($rule_phid); + + $this->logEffect(self::DO_MUST_ENCRYPT, array($rule_phid)); + } + + protected function getActionEffectMap() { + return array( + self::DO_MUST_ENCRYPT => array( + 'icon' => 'fa-shield', + 'color' => 'blue', + 'name' => pht('Must Encrypt'), + ), + ); + } + + protected function renderActionEffectDescription($type, $data) { + switch ($type) { + case self::DO_MUST_ENCRYPT: + return pht( + 'Made it a requirement that mail content be transmitted only '. + 'over secure channels.'); + } + } + +} diff --git a/src/applications/metamta/herald/PhabricatorMetaMTAEmailHeraldAction.php b/src/applications/metamta/herald/PhabricatorMetaMTAEmailHeraldAction.php index 74fb879fe7..383b8ebd36 100644 --- a/src/applications/metamta/herald/PhabricatorMetaMTAEmailHeraldAction.php +++ b/src/applications/metamta/herald/PhabricatorMetaMTAEmailHeraldAction.php @@ -13,6 +13,10 @@ abstract class PhabricatorMetaMTAEmailHeraldAction } public function supportsObject($object) { + return self::isMailGeneratingObject($object); + } + + public static function isMailGeneratingObject($object) { // NOTE: This implementation lacks generality, but there's no great way to // figure out if something generates email right now. diff --git a/src/applications/metamta/management/PhabricatorMailManagementListOutboundWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementListOutboundWorkflow.php index b4aa76379e..a83dafb0a8 100644 --- a/src/applications/metamta/management/PhabricatorMailManagementListOutboundWorkflow.php +++ b/src/applications/metamta/management/PhabricatorMailManagementListOutboundWorkflow.php @@ -37,6 +37,7 @@ final class PhabricatorMailManagementListOutboundWorkflow $table = id(new PhutilConsoleTable()) ->setShowHeader(false) ->addColumn('id', array('title' => pht('ID'))) + ->addColumn('encrypt', array('title' => pht('#'))) ->addColumn('status', array('title' => pht('Status'))) ->addColumn('subject', array('title' => pht('Subject'))); @@ -45,6 +46,7 @@ final class PhabricatorMailManagementListOutboundWorkflow $table->addRow(array( 'id' => $mail->getID(), + 'encrypt' => ($mail->getMustEncrypt() ? '#' : ' '), 'status' => PhabricatorMailOutboundStatus::getStatusName($status), 'subject' => $mail->getSubject(), )); diff --git a/src/applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php index 9f4e91ca22..152b62fd3f 100644 --- a/src/applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php +++ b/src/applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php @@ -47,6 +47,11 @@ final class PhabricatorMailManagementSendTestWorkflow 'help' => pht('Attach a file.'), 'repeat' => true, ), + array( + 'name' => 'mailer', + 'param' => 'key', + 'help' => pht('Send with a specific configured mailer.'), + ), array( 'name' => 'html', 'help' => pht('Send as HTML mail.'), @@ -161,6 +166,21 @@ final class PhabricatorMailManagementSendTestWorkflow $mail->setFrom($from->getPHID()); } + $mailer_key = $args->getArg('mailer'); + if ($mailer_key !== null) { + $mailers = PhabricatorMetaMTAMail::newMailers(); + $mailers = mpull($mailers, null, 'getKey'); + if (!isset($mailers[$mailer_key])) { + throw new PhutilArgumentUsageException( + pht( + 'Mailer key ("%s") is not configured. Available keys are: %s.', + $mailer_key, + implode(', ', array_keys($mailers)))); + } + + $mail->setTryMailers(array($mailer_key)); + } + foreach ($attach as $attachment) { $data = Filesystem::readFile($attachment); $name = basename($attachment); diff --git a/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php index 54a91861ae..0fc7dd14b9 100644 --- a/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php +++ b/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php @@ -79,7 +79,7 @@ final class PhabricatorMailManagementShowOutboundWorkflow $info = array(); - $info[] = pht('PROPERTIES'); + $info[] = $this->newSectionHeader(pht('PROPERTIES')); $info[] = pht('ID: %d', $message->getID()); $info[] = pht('Status: %s', $message->getStatus()); $info[] = pht('Related PHID: %s', $message->getRelatedPHID()); @@ -87,15 +87,17 @@ final class PhabricatorMailManagementShowOutboundWorkflow $ignore = array( 'body' => true, + 'body.sent' => true, 'html-body' => true, 'headers' => true, 'attachments' => true, 'headers.sent' => true, + 'headers.unfiltered' => true, 'authors.sent' => true, ); $info[] = null; - $info[] = pht('PARAMETERS'); + $info[] = $this->newSectionHeader(pht('PARAMETERS')); $parameters = $message->getParameters(); foreach ($parameters as $key => $value) { if (isset($ignore[$key])) { @@ -110,22 +112,40 @@ final class PhabricatorMailManagementShowOutboundWorkflow } $info[] = null; - $info[] = pht('HEADERS'); + $info[] = $this->newSectionHeader(pht('HEADERS')); $headers = $message->getDeliveredHeaders(); - if (!$headers) { + $unfiltered = $message->getUnfilteredHeaders(); + if (!$unfiltered) { $headers = $message->generateHeaders(); + $unfiltered = $headers; } + $header_map = array(); foreach ($headers as $header) { list($name, $value) = $header; - $info[] = "{$name}: {$value}"; + $header_map[$name.':'.$value] = true; + } + + foreach ($unfiltered as $header) { + list($name, $value) = $header; + $was_sent = isset($header_map[$name.':'.$value]); + + if ($was_sent) { + $marker = ' '; + } else { + $marker = '#'; + } + + $info[] = "{$marker} {$name}: {$value}"; } $attachments = idx($parameters, 'attachments'); if ($attachments) { $info[] = null; - $info[] = pht('ATTACHMENTS'); + + $info[] = $this->newSectionHeader(pht('ATTACHMENTS')); + foreach ($attachments as $attachment) { $info[] = idx($attachment, 'filename', pht('Unnamed File')); } @@ -136,7 +156,9 @@ final class PhabricatorMailManagementShowOutboundWorkflow $actors = $message->getDeliveredActors(); if ($actors) { $info[] = null; - $info[] = pht('RECIPIENTS'); + + $info[] = $this->newSectionHeader(pht('RECIPIENTS')); + foreach ($actors as $actor_phid => $actor_info) { $actor = idx($all_actors, $actor_phid); if ($actor) { @@ -162,15 +184,22 @@ final class PhabricatorMailManagementShowOutboundWorkflow } $info[] = null; - $info[] = pht('TEXT BODY'); + $info[] = $this->newSectionHeader(pht('TEXT BODY')); if (strlen($message->getBody())) { - $info[] = $message->getBody(); + $info[] = tsprintf('%B', $message->getBody()); } else { $info[] = pht('(This message has no text body.)'); } + $delivered_body = $message->getDeliveredBody(); + if ($delivered_body !== null) { + $info[] = null; + $info[] = $this->newSectionHeader(pht('BODY AS DELIVERED'), true); + $info[] = tsprintf('%B', $delivered_body); + } + $info[] = null; - $info[] = pht('HTML BODY'); + $info[] = $this->newSectionHeader(pht('HTML BODY')); if (strlen($message->getHTMLBody())) { $info[] = $message->getHTMLBody(); $info[] = null; @@ -186,4 +215,12 @@ final class PhabricatorMailManagementShowOutboundWorkflow } } + private function newSectionHeader($label, $emphasize = false) { + if ($emphasize) { + return tsprintf('** %s **', $label); + } else { + return tsprintf('** %s **', $label); + } + } + } diff --git a/src/applications/metamta/query/PhabricatorMetaMTAActor.php b/src/applications/metamta/query/PhabricatorMetaMTAActor.php index 1f4cc7da12..cf2060a8f7 100644 --- a/src/applications/metamta/query/PhabricatorMetaMTAActor.php +++ b/src/applications/metamta/query/PhabricatorMetaMTAActor.php @@ -21,6 +21,7 @@ final class PhabricatorMetaMTAActor extends Phobject { const REASON_ROUTE_AS_NOTIFICATION = 'route-as-notification'; const REASON_ROUTE_AS_MAIL = 'route-as-mail'; const REASON_UNVERIFIED = 'unverified'; + const REASON_MUTED = 'muted'; private $phid; private $emailAddress; @@ -116,6 +117,7 @@ final class PhabricatorMetaMTAActor extends Phobject { self::REASON_ROUTE_AS_NOTIFICATION => pht('Route as Notification'), self::REASON_ROUTE_AS_MAIL => pht('Route as Mail'), self::REASON_UNVERIFIED => pht('Address Not Verified'), + self::REASON_MUTED => pht('Muted'), ); return idx($names, $reason, pht('Unknown ("%s")', $reason)); @@ -172,6 +174,8 @@ final class PhabricatorMetaMTAActor extends Phobject { 'in Herald.'), self::REASON_UNVERIFIED => pht( 'This recipient does not have a verified primary email address.'), + self::REASON_MUTED => pht( + 'This recipient has muted notifications for this object.'), ); return idx($descriptions, $reason, pht('Unknown Reason ("%s")', $reason)); diff --git a/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php b/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php index b0ae2de494..f8dd784e3b 100644 --- a/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php +++ b/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php @@ -6,6 +6,7 @@ abstract class PhabricatorMailReplyHandler extends Phobject { private $applicationEmail; private $actor; private $excludePHIDs = array(); + private $unexpandablePHIDs = array(); final public function setMailReceiver($mail_receiver) { $this->validateMailReceiver($mail_receiver); @@ -45,6 +46,15 @@ abstract class PhabricatorMailReplyHandler extends Phobject { return $this->excludePHIDs; } + public function setUnexpandablePHIDs(array $phids) { + $this->unexpandablePHIDs = $phids; + return $this; + } + + public function getUnexpandablePHIDs() { + return $this->unexpandablePHIDs; + } + abstract public function validateMailReceiver($mail_receiver); abstract public function getPrivateReplyHandlerEmailAddress( PhabricatorUser $user); @@ -297,6 +307,16 @@ abstract class PhabricatorMailReplyHandler extends Phobject { $to_result = array(); $cc_result = array(); + // "Unexpandable" users have disengaged from an object (for example, + // by resigning from a revision). + + // If such a user is still a direct recipient (for example, they're still + // on the Subscribers list) they're fair game, but group targets (like + // projects) will no longer include them when expanded. + + $unexpandable = $this->getUnexpandablePHIDs(); + $unexpandable = array_fuse($unexpandable); + $all_phids = array_merge($to, $cc); if ($all_phids) { $map = id(new PhabricatorMetaMTAMemberQuery()) @@ -305,11 +325,21 @@ abstract class PhabricatorMailReplyHandler extends Phobject { ->execute(); foreach ($to as $phid) { foreach ($map[$phid] as $expanded) { + if ($expanded !== $phid) { + if (isset($unexpandable[$expanded])) { + continue; + } + } $to_result[$expanded] = $expanded; } } foreach ($cc as $phid) { foreach ($map[$phid] as $expanded) { + if ($expanded !== $phid) { + if (isset($unexpandable[$expanded])) { + continue; + } + } $cc_result[$expanded] = $expanded; } } diff --git a/src/applications/metamta/replyhandler/PhabricatorMailTarget.php b/src/applications/metamta/replyhandler/PhabricatorMailTarget.php index 49e45a19d0..fd80533d00 100644 --- a/src/applications/metamta/replyhandler/PhabricatorMailTarget.php +++ b/src/applications/metamta/replyhandler/PhabricatorMailTarget.php @@ -58,23 +58,55 @@ final class PhabricatorMailTarget extends Phobject { public function willSendMail(PhabricatorMetaMTAMail $mail) { $viewer = $this->getViewer(); + $show_stamps = $mail->shouldRenderMailStampsInBody($viewer); + + $body = $mail->getBody(); + $html_body = $mail->getHTMLBody(); + $has_html = (strlen($html_body) > 0); + + if ($show_stamps) { + $stamps = $mail->getMailStamps(); + if ($stamps) { + $body .= "\n"; + $body .= pht('STAMPS'); + $body .= "\n"; + $body .= implode(' ', $stamps); + $body .= "\n"; + + if ($has_html) { + $html = array(); + $html[] = phutil_tag('strong', array(), pht('STAMPS')); + $html[] = phutil_tag('br'); + $html[] = phutil_tag( + 'span', + array( + 'style' => 'font-size: smaller; color: #92969D', + ), + phutil_implode_html(' ', $stamps)); + $html[] = phutil_tag('br'); + $html[] = phutil_tag('br'); + $html = phutil_tag('div', array(), $html); + $html_body .= hsprintf('%s', $html); + } + } + } + $mail->addPHIDHeaders('X-Phabricator-To', $this->rawToPHIDs); $mail->addPHIDHeaders('X-Phabricator-Cc', $this->rawCCPHIDs); $to_handles = $viewer->loadHandles($this->rawToPHIDs); $cc_handles = $viewer->loadHandles($this->rawCCPHIDs); - $body = $mail->getBody(); $body .= "\n"; $body .= $this->getRecipientsSummary($to_handles, $cc_handles); - $mail->setBody($body); - $html_body = $mail->getHTMLBody(); - if (strlen($html_body)) { + if ($has_html) { $html_body .= hsprintf( '%s', $this->getRecipientsSummaryHTML($to_handles, $cc_handles)); } + + $mail->setBody($body); $mail->setHTMLBody($html_body); $reply_to = $this->getReplyTo(); diff --git a/src/applications/metamta/stamp/PhabricatorBoolMailStamp.php b/src/applications/metamta/stamp/PhabricatorBoolMailStamp.php new file mode 100644 index 0000000000..d274df67fe --- /dev/null +++ b/src/applications/metamta/stamp/PhabricatorBoolMailStamp.php @@ -0,0 +1,16 @@ +renderStamp($this->getKey()); + } + +} diff --git a/src/applications/metamta/stamp/PhabricatorMailStamp.php b/src/applications/metamta/stamp/PhabricatorMailStamp.php new file mode 100644 index 0000000000..9b425a4bdf --- /dev/null +++ b/src/applications/metamta/stamp/PhabricatorMailStamp.php @@ -0,0 +1,88 @@ +getPhobjectClassConstant('STAMPTYPE'); + } + + final public function setKey($key) { + $this->key = $key; + return $this; + } + + final public function getKey() { + return $this->key; + } + + final protected function setRawValue($value) { + $this->value = $value; + return $this; + } + + final protected function getRawValue() { + return $this->value; + } + + final public function setViewer(PhabricatorUser $viewer) { + $this->viewer = $viewer; + return $this; + } + + final public function getViewer() { + return $this->viewer; + } + + final public function setLabel($label) { + $this->label = $label; + return $this; + } + + final public function getLabel() { + return $this->label; + } + + public function setValue($value) { + return $this->setRawValue($value); + } + + final public function toDictionary() { + return array( + 'type' => $this->getStampType(), + 'key' => $this->getKey(), + 'value' => $this->getValueForDictionary(), + ); + } + + final public static function getAllStamps() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getStampType') + ->execute(); + } + + protected function getValueForDictionary() { + return $this->getRawValue(); + } + + public function setValueFromDictionary($value) { + return $this->setRawValue($value); + } + + public function getValueForRendering() { + return $this->getRawValue(); + } + + abstract public function renderStamps($value); + + final protected function renderStamp($key, $value = null) { + return $key.'('.$value.')'; + } + +} diff --git a/src/applications/metamta/stamp/PhabricatorPHIDMailStamp.php b/src/applications/metamta/stamp/PhabricatorPHIDMailStamp.php new file mode 100644 index 0000000000..575ad16f6a --- /dev/null +++ b/src/applications/metamta/stamp/PhabricatorPHIDMailStamp.php @@ -0,0 +1,36 @@ +getViewer(); + $handles = $viewer->loadHandles($value); + + $results = array(); + foreach ($value as $phid) { + $handle = $handles[$phid]; + + $mail_name = $handle->getMailStampName(); + if ($mail_name === null) { + $mail_name = $handle->getPHID(); + } + + $results[] = $this->renderStamp($this->getKey(), $mail_name); + } + + return $results; + } + +} diff --git a/src/applications/metamta/stamp/PhabricatorStringMailStamp.php b/src/applications/metamta/stamp/PhabricatorStringMailStamp.php new file mode 100644 index 0000000000..b6210afb4e --- /dev/null +++ b/src/applications/metamta/stamp/PhabricatorStringMailStamp.php @@ -0,0 +1,26 @@ +renderStamp($this->getKey(), $v); + } + + return $results; + } + +} diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAAttachment.php b/src/applications/metamta/storage/PhabricatorMetaMTAAttachment.php index b26bb0b8a7..256bee46e8 100644 --- a/src/applications/metamta/storage/PhabricatorMetaMTAAttachment.php +++ b/src/applications/metamta/storage/PhabricatorMetaMTAAttachment.php @@ -1,9 +1,12 @@ setData($data); @@ -39,18 +42,49 @@ final class PhabricatorMetaMTAAttachment extends Phobject { } public function toDictionary() { + if (!$this->file) { + $iterator = new ArrayIterator(array($this->getData())); + + $source = id(new PhabricatorIteratorFileUploadSource()) + ->setName($this->getFilename()) + ->setViewPolicy(PhabricatorPolicies::POLICY_NOONE) + ->setMIMEType($this->getMimeType()) + ->setIterator($iterator); + + $this->file = $source->uploadFile(); + } + return array( 'filename' => $this->getFilename(), 'mimetype' => $this->getMimeType(), - 'data' => $this->getData(), + 'filePHID' => $this->file->getPHID(), ); } public static function newFromDictionary(array $dict) { - return new PhabricatorMetaMTAAttachment( + $file = null; + + $file_phid = idx($dict, 'filePHID'); + if ($file_phid) { + $file = id(new PhabricatorFileQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs(array($file_phid)) + ->executeOne(); + if ($file) { + $dict['data'] = $file->loadFileData(); + } + } + + $attachment = new self( idx($dict, 'data'), idx($dict, 'filename'), idx($dict, 'mimetype')); + + if ($file) { + $attachment->file = $file; + } + + return $attachment; } } diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php index 65ffe888d9..4735ff61d7 100644 --- a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php +++ b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php @@ -5,7 +5,9 @@ */ final class PhabricatorMetaMTAMail extends PhabricatorMetaMTADAO - implements PhabricatorPolicyInterface { + implements + PhabricatorPolicyInterface, + PhabricatorDestructibleInterface { const RETRY_DELAY = 5; @@ -21,7 +23,10 @@ final class PhabricatorMetaMTAMail public function __construct() { $this->status = PhabricatorMailOutboundStatus::STATUS_QUEUE; - $this->parameters = array('sensitive' => true); + $this->parameters = array( + 'sensitive' => true, + 'mustEncrypt' => false, + ); parent::__construct(); } @@ -155,6 +160,15 @@ final class PhabricatorMetaMTAMail return $this->getParam('exclude', array()); } + public function setMutedPHIDs(array $muted) { + $this->setParam('muted', $muted); + return $this; + } + + private function getMutedPHIDs() { + return $this->getParam('muted', array()); + } + public function setForceHeraldMailRecipientPHIDs(array $force) { $this->setParam('herald-force-recipients', $force); return $this; @@ -192,6 +206,35 @@ final class PhabricatorMetaMTAMail return $result; } + public function getAttachmentFilePHIDs() { + $file_phids = array(); + + $dictionaries = $this->getParam('attachments'); + if ($dictionaries) { + foreach ($dictionaries as $dictionary) { + $file_phid = idx($dictionary, 'filePHID'); + if ($file_phid) { + $file_phids[] = $file_phid; + } + } + } + + return $file_phids; + } + + public function loadAttachedFiles(PhabricatorUser $viewer) { + $file_phids = $this->getAttachmentFilePHIDs(); + + if (!$file_phids) { + return array(); + } + + return id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs($file_phids) + ->execute(); + } + public function setAttachments(array $attachments) { assert_instances_of($attachments, 'PhabricatorMetaMTAAttachment'); $this->setParam('attachments', mpull($attachments, 'toDictionary')); @@ -247,6 +290,48 @@ final class PhabricatorMetaMTAMail return $this->getParam('sensitive', true); } + public function setMustEncrypt($bool) { + $this->setParam('mustEncrypt', $bool); + return $this; + } + + public function getMustEncrypt() { + return $this->getParam('mustEncrypt', false); + } + + public function setMustEncryptReasons(array $reasons) { + $this->setParam('mustEncryptReasons', $reasons); + return $this; + } + + public function getMustEncryptReasons() { + return $this->getParam('mustEncryptReasons', array()); + } + + public function setMailStamps(array $stamps) { + return $this->setParam('stamps', $stamps); + } + + public function getMailStamps() { + return $this->getParam('stamps', array()); + } + + public function setMailStampMetadata($metadata) { + return $this->setParam('stampMetadata', $metadata); + } + + public function getMailStampMetadata() { + return $this->getParam('stampMetadata', array()); + } + + public function getMailerKey() { + return $this->getParam('mailer.key'); + } + + public function setTryMailers(array $mailers) { + return $this->setParam('mailers.try', $mailers); + } + public function setHTMLBody($html) { $this->setParam('html-body', $html); return $this; @@ -385,142 +470,325 @@ final class PhabricatorMetaMTAMail return $result; } - public function buildDefaultMailer() { - return PhabricatorEnv::newObjectFromConfig('metamta.mail-adapter'); - } - - /** * Attempt to deliver an email immediately, in this process. * - * @param bool Try to deliver this email even if it has already been - * delivered or is in backoff after a failed delivery attempt. - * @param PhabricatorMailImplementationAdapter Use a specific mail adapter, - * instead of the default. - * * @return void */ - public function sendNow( - $force_send = false, - PhabricatorMailImplementationAdapter $mailer = null) { - - if ($mailer === null) { - $mailer = $this->buildDefaultMailer(); + public function sendNow() { + if ($this->getStatus() != PhabricatorMailOutboundStatus::STATUS_QUEUE) { + throw new Exception(pht('Trying to send an already-sent mail!')); } - if (!$force_send) { - if ($this->getStatus() != PhabricatorMailOutboundStatus::STATUS_QUEUE) { - throw new Exception(pht('Trying to send an already-sent mail!')); + $mailers = self::newMailers(); + + $try_mailers = $this->getParam('mailers.try'); + if ($try_mailers) { + $mailers = mpull($mailers, null, 'getKey'); + $mailers = array_select_keys($mailers, $try_mailers); + } + + return $this->sendWithMailers($mailers); + } + + public static function newMailersWithTypes(array $types) { + $mailers = self::newMailers(); + $types = array_fuse($types); + + foreach ($mailers as $key => $mailer) { + $mailer_type = $mailer->getAdapterType(); + if (!isset($types[$mailer_type])) { + unset($mailers[$key]); } } - try { - $headers = $this->generateHeaders(); + return array_values($mailers); + } - $params = $this->parameters; + public static function newMailers() { + $mailers = array(); - $actors = $this->loadAllActors(); - $deliverable_actors = $this->filterDeliverableActors($actors); + $config = PhabricatorEnv::getEnvConfig('cluster.mailers'); + if ($config === null) { + $mailer = PhabricatorEnv::newObjectFromConfig('metamta.mail-adapter'); - $default_from = PhabricatorEnv::getEnvConfig('metamta.default-address'); - if (empty($params['from'])) { - $mailer->setFrom($default_from); + $defaults = $mailer->newDefaultOptions(); + $options = $mailer->newLegacyOptions(); + + $options = $options + $defaults; + + $mailer + ->setKey('default') + ->setPriority(-1) + ->setOptions($options); + + $mailers[] = $mailer; + } else { + $adapters = PhabricatorMailImplementationAdapter::getAllAdapters(); + $next_priority = -1; + + foreach ($config as $spec) { + $type = $spec['type']; + if (!isset($adapters[$type])) { + throw new Exception( + pht( + 'Unknown mailer ("%s")!', + $type)); + } + + $key = $spec['key']; + $mailer = id(clone $adapters[$type]) + ->setKey($key); + + $priority = idx($spec, 'priority'); + if (!$priority) { + $priority = $next_priority; + $next_priority--; + } + $mailer->setPriority($priority); + + $defaults = $mailer->newDefaultOptions(); + $options = idx($spec, 'options', array()) + $defaults; + $mailer->setOptions($options); + + $mailers[] = $mailer; + } + } + + $sorted = array(); + $groups = mgroup($mailers, 'getPriority'); + krsort($groups); + foreach ($groups as $group) { + // Reorder services within the same priority group randomly. + shuffle($group); + foreach ($group as $mailer) { + $sorted[] = $mailer; + } + } + + foreach ($sorted as $mailer) { + $mailer->prepareForSend(); + } + + return $sorted; + } + + public function sendWithMailers(array $mailers) { + if (!$mailers) { + return $this + ->setStatus(PhabricatorMailOutboundStatus::STATUS_VOID) + ->setMessage(pht('No mailers are configured.')) + ->save(); + } + + $exceptions = array(); + foreach ($mailers as $template_mailer) { + $mailer = null; + + try { + $mailer = $this->buildMailer($template_mailer); + } catch (Exception $ex) { + $exceptions[] = $ex; + continue; } - $is_first = idx($params, 'is-first-message'); - unset($params['is-first-message']); - - $is_threaded = (bool)idx($params, 'thread-id'); - - $reply_to_name = idx($params, 'reply-to-name', ''); - unset($params['reply-to-name']); - - $add_cc = array(); - $add_to = array(); - - // If multiplexing is enabled, some recipients will be in "Cc" - // rather than "To". We'll move them to "To" later (or supply a - // dummy "To") but need to look for the recipient in either the - // "To" or "Cc" fields here. - $target_phid = head(idx($params, 'to', array())); - if (!$target_phid) { - $target_phid = head(idx($params, 'cc', array())); + if (!$mailer) { + // If we don't get a mailer back, that means the mail doesn't + // actually need to be sent (for example, because recipients have + // declined to receive the mail). Void it and return. + return $this + ->setStatus(PhabricatorMailOutboundStatus::STATUS_VOID) + ->save(); } - $preferences = $this->loadPreferences($target_phid); + try { + $ok = $mailer->send(); + if (!$ok) { + // TODO: At some point, we should clean this up and make all mailers + // throw. + throw new Exception( + pht( + 'Mail adapter encountered an unexpected, unspecified '. + 'failure.')); + } + } catch (PhabricatorMetaMTAPermanentFailureException $ex) { + // If any mailer raises a permanent failure, stop trying to send the + // mail with other mailers. + $this + ->setStatus(PhabricatorMailOutboundStatus::STATUS_FAIL) + ->setMessage($ex->getMessage()) + ->save(); - foreach ($params as $key => $value) { - switch ($key) { - case 'raw-from': - list($from_email, $from_name) = $value; - $mailer->setFrom($from_email, $from_name); - break; - case 'from': - $from = $value; - $actor_email = null; - $actor_name = null; - $actor = idx($actors, $from); - if ($actor) { - $actor_email = $actor->getEmailAddress(); - $actor_name = $actor->getName(); - } - $can_send_as_user = $actor_email && - PhabricatorEnv::getEnvConfig('metamta.can-send-as-user'); + throw $ex; + } catch (Exception $ex) { + $exceptions[] = $ex; + continue; + } - if ($can_send_as_user) { - $mailer->setFrom($actor_email, $actor_name); - } else { - $from_email = coalesce($actor_email, $default_from); - $from_name = coalesce($actor_name, pht('Blender Foundation')); + // Keep track of which mailer actually ended up accepting the message. + $mailer_key = $mailer->getKey(); + if ($mailer_key !== null) { + $this->setParam('mailer.key', $mailer_key); + } - if (empty($params['reply-to'])) { - $params['reply-to'] = $from_email; - $params['reply-to-name'] = $from_name; - } + return $this + ->setStatus(PhabricatorMailOutboundStatus::STATUS_SENT) + ->save(); + } - $mailer->setFrom($default_from, $from_name); - } - break; - case 'reply-to': - $mailer->addReplyTo($value, $reply_to_name); - break; - case 'to': - $to_phids = $this->expandRecipients($value); - $to_actors = array_select_keys($deliverable_actors, $to_phids); - $add_to = array_merge( - $add_to, - mpull($to_actors, 'getEmailAddress')); - break; - case 'raw-to': - $add_to = array_merge($add_to, $value); - break; - case 'cc': - $cc_phids = $this->expandRecipients($value); - $cc_actors = array_select_keys($deliverable_actors, $cc_phids); - $add_cc = array_merge( - $add_cc, - mpull($cc_actors, 'getEmailAddress')); - break; - case 'attachments': - $value = $this->getAttachments(); - foreach ($value as $attachment) { - $mailer->addAttachment( - $attachment->getData(), - $attachment->getFilename(), - $attachment->getMimeType()); - } - break; - case 'subject': - $subject = array(); + // If we make it here, no mailer could send the mail but no mailer failed + // permanently either. We update the error message for the mail, but leave + // it in the current status (usually, STATUS_QUEUE) and try again later. - if ($is_threaded) { - if ($this->shouldAddRePrefix($preferences)) { - $subject[] = 'Re:'; - } + $messages = array(); + foreach ($exceptions as $ex) { + $messages[] = $ex->getMessage(); + } + $messages = implode("\n\n", $messages); + + $this + ->setMessage($messages) + ->save(); + + if (count($exceptions) === 1) { + throw head($exceptions); + } + + throw new PhutilAggregateException( + pht('Encountered multiple exceptions while transmitting mail.'), + $exceptions); + } + + private function buildMailer(PhabricatorMailImplementationAdapter $mailer) { + $headers = $this->generateHeaders(); + + $params = $this->parameters; + + $actors = $this->loadAllActors(); + $deliverable_actors = $this->filterDeliverableActors($actors); + + $default_from = PhabricatorEnv::getEnvConfig('metamta.default-address'); + if (empty($params['from'])) { + $mailer->setFrom($default_from); + } + + $is_first = idx($params, 'is-first-message'); + unset($params['is-first-message']); + + $is_threaded = (bool)idx($params, 'thread-id'); + $must_encrypt = $this->getMustEncrypt(); + + $reply_to_name = idx($params, 'reply-to-name', ''); + unset($params['reply-to-name']); + + $add_cc = array(); + $add_to = array(); + + // If we're sending one mail to everyone, some recipients will be in + // "Cc" rather than "To". We'll move them to "To" later (or supply a + // dummy "To") but need to look for the recipient in either the + // "To" or "Cc" fields here. + $target_phid = head(idx($params, 'to', array())); + if (!$target_phid) { + $target_phid = head(idx($params, 'cc', array())); + } + + $preferences = $this->loadPreferences($target_phid); + + foreach ($params as $key => $value) { + switch ($key) { + case 'raw-from': + list($from_email, $from_name) = $value; + $mailer->setFrom($from_email, $from_name); + break; + case 'from': + // If the mail content must be encrypted, disguise the sender. + if ($must_encrypt) { + $mailer->setFrom($default_from, pht('Phabricator')); + break; + } + + $from = $value; + $actor_email = null; + $actor_name = null; + $actor = idx($actors, $from); + if ($actor) { + $actor_email = $actor->getEmailAddress(); + $actor_name = $actor->getName(); + } + $can_send_as_user = $actor_email && + PhabricatorEnv::getEnvConfig('metamta.can-send-as-user'); + + if ($can_send_as_user) { + $mailer->setFrom($actor_email, $actor_name); + } else { + $from_email = coalesce($actor_email, $default_from); + $from_name = coalesce($actor_name, pht('Blender Foundation')); + + if (empty($params['reply-to'])) { + $params['reply-to'] = $from_email; + $params['reply-to-name'] = $from_name; } - $subject[] = trim(idx($params, 'subject-prefix')); + $mailer->setFrom($default_from, $from_name); + } + break; + case 'reply-to': + $mailer->addReplyTo($value, $reply_to_name); + break; + case 'to': + $to_phids = $this->expandRecipients($value); + $to_actors = array_select_keys($deliverable_actors, $to_phids); + $add_to = array_merge( + $add_to, + mpull($to_actors, 'getEmailAddress')); + break; + case 'raw-to': + $add_to = array_merge($add_to, $value); + break; + case 'cc': + $cc_phids = $this->expandRecipients($value); + $cc_actors = array_select_keys($deliverable_actors, $cc_phids); + $add_cc = array_merge( + $add_cc, + mpull($cc_actors, 'getEmailAddress')); + break; + case 'attachments': + $attached_viewer = PhabricatorUser::getOmnipotentUser(); + $files = $this->loadAttachedFiles($attached_viewer); + foreach ($files as $file) { + $file->attachToObject($this->getPHID()); + } + // If the mail content must be encrypted, don't add attachments. + if ($must_encrypt) { + break; + } + + $value = $this->getAttachments(); + foreach ($value as $attachment) { + $mailer->addAttachment( + $attachment->getData(), + $attachment->getFilename(), + $attachment->getMimeType()); + } + break; + case 'subject': + $subject = array(); + + if ($is_threaded) { + if ($this->shouldAddRePrefix($preferences)) { + $subject[] = 'Re:'; + } + } + + $subject[] = trim(idx($params, 'subject-prefix')); + + // If mail content must be encrypted, we replace the subject with + // a generic one. + if ($must_encrypt) { + $subject[] = pht('Object Updated'); + } else { $vary_prefix = idx($params, 'vary-subject-prefix'); if ($vary_prefix != '') { if ($this->shouldVarySubject($preferences)) { @@ -529,171 +797,172 @@ final class PhabricatorMetaMTAMail } $subject[] = $value; + } - $mailer->setSubject(implode(' ', array_filter($subject))); - break; - case 'thread-id': + $mailer->setSubject(implode(' ', array_filter($subject))); + break; + case 'thread-id': - // NOTE: Gmail freaks out about In-Reply-To and References which - // aren't in the form ""; this is also required - // by RFC 2822, although some clients are more liberal in what they - // accept. - $domain = PhabricatorEnv::getEnvConfig('metamta.domain'); - $value = '<'.$value.'@'.$domain.'>'; + // NOTE: Gmail freaks out about In-Reply-To and References which + // aren't in the form ""; this is also required + // by RFC 2822, although some clients are more liberal in what they + // accept. + $domain = PhabricatorEnv::getEnvConfig('metamta.domain'); + $value = '<'.$value.'@'.$domain.'>'; - if ($is_first && $mailer->supportsMessageIDHeader()) { - $headers[] = array('Message-ID', $value); - } else { - $in_reply_to = $value; - $references = array($value); - $parent_id = $this->getParentMessageID(); - if ($parent_id) { - $in_reply_to = $parent_id; - // By RFC 2822, the most immediate parent should appear last - // in the "References" header, so this order is intentional. - $references[] = $parent_id; - } - $references = implode(' ', $references); - $headers[] = array('In-Reply-To', $in_reply_to); - $headers[] = array('References', $references); + if ($is_first && $mailer->supportsMessageIDHeader()) { + $headers[] = array('Message-ID', $value); + } else { + $in_reply_to = $value; + $references = array($value); + $parent_id = $this->getParentMessageID(); + if ($parent_id) { + $in_reply_to = $parent_id; + // By RFC 2822, the most immediate parent should appear last + // in the "References" header, so this order is intentional. + $references[] = $parent_id; } - $thread_index = $this->generateThreadIndex($value, $is_first); - $headers[] = array('Thread-Index', $thread_index); - break; - default: - // Other parameters are handled elsewhere or are not relevant to - // constructing the message. - break; - } + $references = implode(' ', $references); + $headers[] = array('In-Reply-To', $in_reply_to); + $headers[] = array('References', $references); + } + $thread_index = $this->generateThreadIndex($value, $is_first); + $headers[] = array('Thread-Index', $thread_index); + break; + default: + // Other parameters are handled elsewhere or are not relevant to + // constructing the message. + break; } - - $body = idx($params, 'body', ''); - $max = PhabricatorEnv::getEnvConfig('metamta.email-body-limit'); - if (strlen($body) > $max) { - $body = id(new PhutilUTF8StringTruncator()) - ->setMaximumBytes($max) - ->truncateString($body); - $body .= "\n"; - $body .= pht('(This email was truncated at %d bytes.)', $max); - } - $mailer->setBody($body); - - $html_emails = $this->shouldSendHTML($preferences); - if ($html_emails && isset($params['html-body'])) { - $mailer->setHTMLBody($params['html-body']); - } - - // Pass the headers to the mailer, then save the state so we can show - // them in the web UI. - foreach ($headers as $header) { - list($header_key, $header_value) = $header; - $mailer->addHeader($header_key, $header_value); - } - $this->setParam('headers.sent', $headers); - - // Save the final deliverability outcomes and reasoning so we can - // explain why things happened the way they did. - $actor_list = array(); - foreach ($actors as $actor) { - $actor_list[$actor->getPHID()] = array( - 'deliverable' => $actor->isDeliverable(), - 'reasons' => $actor->getDeliverabilityReasons(), - ); - } - $this->setParam('actors.sent', $actor_list); - - $this->setParam('routing.sent', $this->getParam('routing')); - $this->setParam('routingmap.sent', $this->getRoutingRuleMap()); - - if (!$add_to && !$add_cc) { - $this->setStatus(PhabricatorMailOutboundStatus::STATUS_VOID); - $this->setMessage( - pht( - 'Message has no valid recipients: all To/Cc are disabled, '. - 'invalid, or configured not to receive this mail.')); - return $this->save(); - } - - if ($this->getIsErrorEmail()) { - $all_recipients = array_merge($add_to, $add_cc); - if ($this->shouldRateLimitMail($all_recipients)) { - $this->setStatus(PhabricatorMailOutboundStatus::STATUS_VOID); - $this->setMessage( - pht( - 'This is an error email, but one or more recipients have '. - 'exceeded the error email rate limit. Declining to deliver '. - 'message.')); - return $this->save(); - } - } - - if (PhabricatorEnv::getEnvConfig('phabricator.silent')) { - $this->setStatus(PhabricatorMailOutboundStatus::STATUS_VOID); - $this->setMessage( - pht( - 'Phabricator is running in silent mode. See `%s` '. - 'in the configuration to change this setting.', - 'phabricator.silent')); - return $this->save(); - } - - // Some mailers require a valid "To:" in order to deliver mail. If we - // don't have any "To:", try to fill it in with a placeholder "To:". - // If that also fails, move the "Cc:" line to "To:". - if (!$add_to) { - $placeholder_key = 'metamta.placeholder-to-recipient'; - $placeholder = PhabricatorEnv::getEnvConfig($placeholder_key); - if ($placeholder !== null) { - $add_to = array($placeholder); - } else { - $add_to = $add_cc; - $add_cc = array(); - } - } - - $add_to = array_unique($add_to); - $add_cc = array_diff(array_unique($add_cc), $add_to); - - $mailer->addTos($add_to); - if ($add_cc) { - $mailer->addCCs($add_cc); - } - } catch (Exception $ex) { - $this - ->setStatus(PhabricatorMailOutboundStatus::STATUS_FAIL) - ->setMessage($ex->getMessage()) - ->save(); - - throw $ex; } - try { - $ok = $mailer->send(); - if (!$ok) { - // TODO: At some point, we should clean this up and make all mailers - // throw. - throw new Exception( - pht('Mail adapter encountered an unexpected, unspecified failure.')); - } - - $this->setStatus(PhabricatorMailOutboundStatus::STATUS_SENT); - $this->save(); - - return $this; - } catch (PhabricatorMetaMTAPermanentFailureException $ex) { - $this - ->setStatus(PhabricatorMailOutboundStatus::STATUS_FAIL) - ->setMessage($ex->getMessage()) - ->save(); - - throw $ex; - } catch (Exception $ex) { - $this - ->setMessage($ex->getMessage()."\n".$ex->getTraceAsString()) - ->save(); - - throw $ex; + $stamps = $this->getMailStamps(); + if ($stamps) { + $headers[] = array('X-Phabricator-Stamps', implode(' ', $stamps)); } + + $raw_body = idx($params, 'body', ''); + $body = $raw_body; + if ($must_encrypt) { + $parts = array(); + $parts[] = pht( + 'The content for this message can only be transmitted over a '. + 'secure channel. To view the message content, follow this '. + 'link:'); + + $parts[] = PhabricatorEnv::getProductionURI($this->getURI()); + + $body = implode("\n\n", $parts); + } else { + $body = $raw_body; + } + + $max = PhabricatorEnv::getEnvConfig('metamta.email-body-limit'); + if (strlen($body) > $max) { + $body = id(new PhutilUTF8StringTruncator()) + ->setMaximumBytes($max) + ->truncateString($body); + $body .= "\n"; + $body .= pht('(This email was truncated at %d bytes.)', $max); + } + $mailer->setBody($body); + + // If we sent a different message body than we were asked to, record + // what we actually sent to make debugging and diagnostics easier. + if ($body !== $raw_body) { + $this->setParam('body.sent', $body); + } + + if ($must_encrypt) { + $send_html = false; + } else { + $send_html = $this->shouldSendHTML($preferences); + } + + if ($send_html && isset($params['html-body'])) { + $mailer->setHTMLBody($params['html-body']); + } + + // Pass the headers to the mailer, then save the state so we can show + // them in the web UI. If the mail must be encrypted, we remove headers + // which are not on a strict whitelist to avoid disclosing information. + $filtered_headers = $this->filterHeaders($headers, $must_encrypt); + foreach ($filtered_headers as $header) { + list($header_key, $header_value) = $header; + $mailer->addHeader($header_key, $header_value); + } + $this->setParam('headers.unfiltered', $headers); + $this->setParam('headers.sent', $filtered_headers); + + // Save the final deliverability outcomes and reasoning so we can + // explain why things happened the way they did. + $actor_list = array(); + foreach ($actors as $actor) { + $actor_list[$actor->getPHID()] = array( + 'deliverable' => $actor->isDeliverable(), + 'reasons' => $actor->getDeliverabilityReasons(), + ); + } + $this->setParam('actors.sent', $actor_list); + + $this->setParam('routing.sent', $this->getParam('routing')); + $this->setParam('routingmap.sent', $this->getRoutingRuleMap()); + + if (!$add_to && !$add_cc) { + $this->setMessage( + pht( + 'Message has no valid recipients: all To/Cc are disabled, '. + 'invalid, or configured not to receive this mail.')); + + return null; + } + + if ($this->getIsErrorEmail()) { + $all_recipients = array_merge($add_to, $add_cc); + if ($this->shouldRateLimitMail($all_recipients)) { + $this->setMessage( + pht( + 'This is an error email, but one or more recipients have '. + 'exceeded the error email rate limit. Declining to deliver '. + 'message.')); + + return null; + } + } + + if (PhabricatorEnv::getEnvConfig('phabricator.silent')) { + $this->setMessage( + pht( + 'Phabricator is running in silent mode. See `%s` '. + 'in the configuration to change this setting.', + 'phabricator.silent')); + + return null; + } + + // Some mailers require a valid "To:" in order to deliver mail. If we + // don't have any "To:", try to fill it in with a placeholder "To:". + // If that also fails, move the "Cc:" line to "To:". + if (!$add_to) { + $placeholder_key = 'metamta.placeholder-to-recipient'; + $placeholder = PhabricatorEnv::getEnvConfig($placeholder_key); + if ($placeholder !== null) { + $add_to = array($placeholder); + } else { + $add_to = $add_cc; + $add_cc = array(); + } + } + + $add_to = array_unique($add_to); + $add_cc = array_diff(array_unique($add_cc), $add_to); + + $mailer->addTos($add_to); + if ($add_cc) { + $mailer->addCCs($add_cc); + } + + return $mailer; } private function generateThreadIndex($seed, $is_first_mail) { @@ -727,7 +996,7 @@ final class PhabricatorMetaMTAMail return base64_encode($base); } - public static function shouldMultiplexAllMail() { + public static function shouldMailEachRecipient() { return PhabricatorEnv::getEnvConfig('metamta.one-mail-per-recipient'); } @@ -853,6 +1122,18 @@ final class PhabricatorMetaMTAMail } } + // Exclude muted recipients. We're doing this after saving deliverability + // so that Herald "Send me an email" actions can still punch through a + // mute. + + foreach ($this->getMutedPHIDs() as $muted_phid) { + $muted_actor = idx($actors, $muted_phid); + if (!$muted_actor) { + continue; + } + $muted_actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_MUTED); + } + // For the rest of the rules, order matters. We're going to run all the // possible rules in order from weakest to strongest, and let the strongest // matching rule win. The weaker rules leave annotations behind which help @@ -979,20 +1260,6 @@ final class PhabricatorMetaMTAMail } } - public function delete() { - $this->openTransaction(); - queryfx( - $this->establishConnection('w'), - 'DELETE FROM %T WHERE src = %s AND type = %d', - PhabricatorEdgeConfig::TABLE_NAME_EDGE, - $this->getPHID(), - PhabricatorMetaMTAMailHasRecipientEdgeType::EDGECONST); - $ret = parent::delete(); - $this->saveTransaction(); - - return $ret; - } - public function generateHeaders() { $headers = array(); @@ -1002,8 +1269,6 @@ final class PhabricatorMetaMTAMail // Some clients respect this to suppress OOF and other auto-responses. $headers[] = array('X-Auto-Response-Suppress', 'All'); - // If the message has mailtags, filter out any recipients who don't want - // to receive this type of mail. $mailtags = $this->getParam('mailtags'); if ($mailtags) { $tag_header = array(); @@ -1028,6 +1293,15 @@ final class PhabricatorMetaMTAMail $headers[] = array('Precedence', 'bulk'); } + if ($this->getMustEncrypt()) { + $headers[] = array('X-Phabricator-Must-Encrypt', 'Yes'); + } + + $related_phid = $this->getRelatedPHID(); + if ($related_phid) { + $headers[] = array('Thread-Topic', $related_phid); + } + return $headers; } @@ -1035,6 +1309,19 @@ final class PhabricatorMetaMTAMail return $this->getParam('headers.sent'); } + public function getUnfilteredHeaders() { + $unfiltered = $this->getParam('headers.unfiltered'); + + if ($unfiltered === null) { + // Older versions of Phabricator did not filter headers, and thus did + // not record unfiltered headers. If we don't have unfiltered header + // data just return the delivered headers for compatibility. + return $this->getDeliveredHeaders(); + } + + return $unfiltered; + } + public function getDeliveredActors() { return $this->getParam('actors.sent'); } @@ -1047,6 +1334,55 @@ final class PhabricatorMetaMTAMail return $this->getParam('routingmap.sent'); } + public function getDeliveredBody() { + return $this->getParam('body.sent'); + } + + private function filterHeaders(array $headers, $must_encrypt) { + if (!$must_encrypt) { + return $headers; + } + + $whitelist = array( + 'In-Reply-To', + 'Message-ID', + 'Precedence', + 'References', + 'Thread-Index', + 'Thread-Topic', + + 'X-Mail-Transport-Agent', + 'X-Auto-Response-Suppress', + + 'X-Phabricator-Sent-This-Message', + 'X-Phabricator-Must-Encrypt', + ); + + // NOTE: The major header we want to drop is "X-Phabricator-Mail-Tags". + // This header contains a significant amount of meaningful information + // about the object. + + $whitelist_map = array(); + foreach ($whitelist as $term) { + $whitelist_map[phutil_utf8_strtolower($term)] = true; + } + + foreach ($headers as $key => $header) { + list($name, $value) = $header; + $name = phutil_utf8_strtolower($name); + + if (!isset($whitelist_map[$name])) { + unset($headers[$key]); + } + } + + return $headers; + } + + public function getURI() { + return '/mail/detail/'.$this->getID().'/'; + } + /* -( Routing )------------------------------------------------------------ */ @@ -1121,7 +1457,7 @@ final class PhabricatorMetaMTAMail private function loadPreferences($target_phid) { $viewer = PhabricatorUser::getOmnipotentUser(); - if (self::shouldMultiplexAllMail()) { + if (self::shouldMailEachRecipient()) { $preferences = id(new PhabricatorUserPreferencesQuery()) ->setViewer($viewer) ->withUserPHIDs(array($target_phid)) @@ -1156,6 +1492,14 @@ final class PhabricatorMetaMTAMail return ($value == PhabricatorEmailFormatSetting::VALUE_HTML_EMAIL); } + public function shouldRenderMailStampsInBody($viewer) { + $preferences = $this->loadPreferences($viewer->getPHID()); + $value = $preferences->getSettingValue( + PhabricatorEmailStampsSetting::SETTINGKEY); + + return ($value == PhabricatorEmailStampsSetting::VALUE_BODY_STAMPS); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ @@ -1181,4 +1525,18 @@ final class PhabricatorMetaMTAMail } +/* -( PhabricatorDestructibleInterface )----------------------------------- */ + + + public function destroyObjectPermanently( + PhabricatorDestructionEngine $engine) { + + $files = $this->loadAttachedFiles($engine->getViewer()); + foreach ($files as $file) { + $engine->destroyObject($file); + } + + $this->delete(); + } + } diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php index 18fa7dd2ba..fc98d17010 100644 --- a/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php +++ b/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php @@ -105,6 +105,7 @@ final class PhabricatorMetaMTAReceivedMail extends PhabricatorMetaMTADAO { public function processReceivedMail() { + $sender = null; try { $this->dropMailFromPhabricator(); $this->dropMailAlreadyReceived(); @@ -140,7 +141,7 @@ final class PhabricatorMetaMTAReceivedMail extends PhabricatorMetaMTADAO { // This error is explicitly ignored. break; default: - $this->sendExceptionMail($ex); + $this->sendExceptionMail($ex, $sender); break; } @@ -150,7 +151,7 @@ final class PhabricatorMetaMTAReceivedMail extends PhabricatorMetaMTADAO { ->save(); return $this; } catch (Exception $ex) { - $this->sendExceptionMail($ex); + $this->sendExceptionMail($ex, $sender); $this ->setStatus(MetaMTAReceivedMailStatus::STATUS_UNHANDLED_EXCEPTION) @@ -305,9 +306,14 @@ final class PhabricatorMetaMTAReceivedMail extends PhabricatorMetaMTADAO { return head($accept); } - private function sendExceptionMail(Exception $ex) { - $from = $this->getHeader('from'); - if (!strlen($from)) { + private function sendExceptionMail( + Exception $ex, + PhabricatorUser $viewer = null) { + + // If we've failed to identify a legitimate sender, we don't send them + // an error message back. We want to avoid sending mail to unverified + // addresses. See T12491. + if (!$viewer) { return; } @@ -364,9 +370,8 @@ EOBODY $mail = id(new PhabricatorMetaMTAMail()) ->setIsErrorEmail(true) - ->setForceDelivery(true) ->setSubject($title) - ->addRawTos(array($from)) + ->addTos(array($viewer->getPHID())) ->setBody($body) ->saveAndSend(); } diff --git a/src/applications/metamta/storage/__tests__/PhabricatorMailConfigTestCase.php b/src/applications/metamta/storage/__tests__/PhabricatorMailConfigTestCase.php new file mode 100644 index 0000000000..25984a2c1d --- /dev/null +++ b/src/applications/metamta/storage/__tests__/PhabricatorMailConfigTestCase.php @@ -0,0 +1,131 @@ +newMailersWithConfig( + array( + array( + 'key' => 'A', + 'type' => 'test', + ), + array( + 'key' => 'B', + 'type' => 'test', + ), + )); + + $this->assertEqual( + array('A', 'B'), + mpull($mailers, 'getKey')); + + $mailers = $this->newMailersWithConfig( + array( + array( + 'key' => 'A', + 'priority' => 1, + 'type' => 'test', + ), + array( + 'key' => 'B', + 'priority' => 2, + 'type' => 'test', + ), + )); + + $this->assertEqual( + array('B', 'A'), + mpull($mailers, 'getKey')); + + $mailers = $this->newMailersWithConfig( + array( + array( + 'key' => 'A1', + 'priority' => 300, + 'type' => 'test', + ), + array( + 'key' => 'A2', + 'priority' => 300, + 'type' => 'test', + ), + array( + 'key' => 'B', + 'type' => 'test', + ), + array( + 'key' => 'C', + 'priority' => 400, + 'type' => 'test', + ), + array( + 'key' => 'D', + 'type' => 'test', + ), + )); + + // The "A" servers should be shuffled randomly, so either outcome is + // acceptable. + $option_1 = array('C', 'A1', 'A2', 'B', 'D'); + $option_2 = array('C', 'A2', 'A1', 'B', 'D'); + $actual = mpull($mailers, 'getKey'); + + $this->assertTrue(($actual === $option_1) || ($actual === $option_2)); + + // Make sure that when we're load balancing we actually send traffic to + // both servers reasonably often. + $saw_a1 = false; + $saw_a2 = false; + $attempts = 0; + while (true) { + $mailers = $this->newMailersWithConfig( + array( + array( + 'key' => 'A1', + 'priority' => 300, + 'type' => 'test', + ), + array( + 'key' => 'A2', + 'priority' => 300, + 'type' => 'test', + ), + )); + + $first_key = head($mailers)->getKey(); + + if ($first_key == 'A1') { + $saw_a1 = true; + } + + if ($first_key == 'A2') { + $saw_a2 = true; + } + + if ($saw_a1 && $saw_a2) { + break; + } + + if ($attempts++ > 1024) { + throw new Exception( + pht( + 'Load balancing between two mail servers did not select both '. + 'servers after an absurd number of attempts.')); + } + } + + $this->assertTrue($saw_a1 && $saw_a2); + } + + private function newMailersWithConfig(array $config) { + $env = PhabricatorEnv::beginScopedEnv(); + $env->overrideEnvConfig('cluster.mailers', $config); + + $mailers = PhabricatorMetaMTAMail::newMailers(); + + unset($env); + return $mailers; + } + +} diff --git a/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php b/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php index 635913439d..c0045301fd 100644 --- a/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php +++ b/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php @@ -18,7 +18,7 @@ final class PhabricatorMetaMTAMailTestCase extends PhabricatorTestCase { $mail->addTos(array($phid)); $mailer = new PhabricatorMailImplementationTestAdapter(); - $mail->sendNow($force = true, $mailer); + $mail->sendWithMailers(array($mailer)); $this->assertEqual( PhabricatorMailOutboundStatus::STATUS_SENT, $mail->getStatus()); @@ -31,7 +31,7 @@ final class PhabricatorMetaMTAMailTestCase extends PhabricatorTestCase { $mailer = new PhabricatorMailImplementationTestAdapter(); $mailer->setFailTemporarily(true); try { - $mail->sendNow($force = true, $mailer); + $mail->sendWithMailers(array($mailer)); } catch (Exception $ex) { // Ignore. } @@ -47,7 +47,7 @@ final class PhabricatorMetaMTAMailTestCase extends PhabricatorTestCase { $mailer = new PhabricatorMailImplementationTestAdapter(); $mailer->setFailPermanently(true); try { - $mail->sendNow($force = true, $mailer); + $mail->sendWithMailers(array($mailer)); } catch (Exception $ex) { // Ignore. } @@ -182,7 +182,9 @@ final class PhabricatorMetaMTAMailTestCase extends PhabricatorTestCase { $supports_message_id, $is_first_mail) { - $mailer = new PhabricatorMailImplementationTestAdapter( + $mailer = new PhabricatorMailImplementationTestAdapter(); + + $mailer->prepareForSend( array( 'supportsMessageIDHeader' => $supports_message_id, )); @@ -191,7 +193,7 @@ final class PhabricatorMetaMTAMailTestCase extends PhabricatorTestCase { $mail = new PhabricatorMetaMTAMail(); $mail->setThreadID($thread_id, $is_first_mail); - $mail->sendNow($force = true, $mailer); + $mail->sendWithMailers(array($mailer)); $guts = $mailer->getGuts(); $dict = ipull($guts['headers'], 1, 0); @@ -251,4 +253,82 @@ final class PhabricatorMetaMTAMailTestCase extends PhabricatorTestCase { ->executeOne(); } + public function testMailerFailover() { + $user = $this->generateNewTestUser(); + $phid = $user->getPHID(); + + $status_sent = PhabricatorMailOutboundStatus::STATUS_SENT; + $status_queue = PhabricatorMailOutboundStatus::STATUS_QUEUE; + $status_fail = PhabricatorMailOutboundStatus::STATUS_FAIL; + + $mailer1 = id(new PhabricatorMailImplementationTestAdapter()) + ->setKey('mailer1'); + + $mailer2 = id(new PhabricatorMailImplementationTestAdapter()) + ->setKey('mailer2'); + + $mailers = array( + $mailer1, + $mailer2, + ); + + // Send mail with both mailers active. The first mailer should be used. + $mail = id(new PhabricatorMetaMTAMail()) + ->addTos(array($phid)) + ->sendWithMailers($mailers); + $this->assertEqual($status_sent, $mail->getStatus()); + $this->assertEqual('mailer1', $mail->getMailerKey()); + + + // If the first mailer fails, the mail should be sent with the second + // mailer. Since we transmitted the mail, this doesn't raise an exception. + $mailer1->setFailTemporarily(true); + + $mail = id(new PhabricatorMetaMTAMail()) + ->addTos(array($phid)) + ->sendWithMailers($mailers); + $this->assertEqual($status_sent, $mail->getStatus()); + $this->assertEqual('mailer2', $mail->getMailerKey()); + + + // If both mailers fail, the mail should remain in queue. + $mailer2->setFailTemporarily(true); + + $mail = id(new PhabricatorMetaMTAMail()) + ->addTos(array($phid)); + + $caught = null; + try { + $mail->sendWithMailers($mailers); + } catch (Exception $ex) { + $caught = $ex; + } + + $this->assertTrue($caught instanceof Exception); + $this->assertEqual($status_queue, $mail->getStatus()); + $this->assertEqual(null, $mail->getMailerKey()); + + $mailer1->setFailTemporarily(false); + $mailer2->setFailTemporarily(false); + + + // If the first mailer fails permanently, the mail should fail even though + // the second mailer isn't configured to fail. + $mailer1->setFailPermanently(true); + + $mail = id(new PhabricatorMetaMTAMail()) + ->addTos(array($phid)); + + $caught = null; + try { + $mail->sendWithMailers($mailers); + } catch (Exception $ex) { + $caught = $ex; + } + + $this->assertTrue($caught instanceof Exception); + $this->assertEqual($status_fail, $mail->getStatus()); + $this->assertEqual(null, $mail->getMailerKey()); + } + } diff --git a/src/applications/owners/editor/PhabricatorOwnersPackageTransactionEditor.php b/src/applications/owners/editor/PhabricatorOwnersPackageTransactionEditor.php index 40657abd57..c6aad6e2bd 100644 --- a/src/applications/owners/editor/PhabricatorOwnersPackageTransactionEditor.php +++ b/src/applications/owners/editor/PhabricatorOwnersPackageTransactionEditor.php @@ -46,12 +46,10 @@ final class PhabricatorOwnersPackageTransactionEditor } protected function buildMailTemplate(PhabricatorLiskDAO $object) { - $id = $object->getID(); $name = $object->getName(); return id(new PhabricatorMetaMTAMail()) - ->setSubject($name) - ->addHeader('Thread-Topic', $object->getPHID()); + ->setSubject($name); } protected function buildMailBody( diff --git a/src/applications/owners/phid/PhabricatorOwnersPackagePHIDType.php b/src/applications/owners/phid/PhabricatorOwnersPackagePHIDType.php index cfbaf6eeb2..fbff6a2103 100644 --- a/src/applications/owners/phid/PhabricatorOwnersPackagePHIDType.php +++ b/src/applications/owners/phid/PhabricatorOwnersPackagePHIDType.php @@ -45,6 +45,7 @@ final class PhabricatorOwnersPackagePHIDType extends PhabricatorPHIDType { ->setName($monogram) ->setFullName("{$monogram}: {$name}") ->setCommandLineObjectName("{$monogram} {$name}") + ->setMailStampName($monogram) ->setURI($uri); if ($package->isArchived()) { diff --git a/src/applications/paste/editor/PhabricatorPasteEditor.php b/src/applications/paste/editor/PhabricatorPasteEditor.php index 063b72cfc0..c312915727 100644 --- a/src/applications/paste/editor/PhabricatorPasteEditor.php +++ b/src/applications/paste/editor/PhabricatorPasteEditor.php @@ -72,8 +72,7 @@ final class PhabricatorPasteEditor $name = $object->getTitle(); return id(new PhabricatorMetaMTAMail()) - ->setSubject("P{$id}: {$name}") - ->addHeader('Thread-Topic', "P{$id}"); + ->setSubject("P{$id}: {$name}"); } protected function buildMailBody( diff --git a/src/applications/people/application/PhabricatorPeopleApplication.php b/src/applications/people/application/PhabricatorPeopleApplication.php index 28405ca92c..6322b29b24 100644 --- a/src/applications/people/application/PhabricatorPeopleApplication.php +++ b/src/applications/people/application/PhabricatorPeopleApplication.php @@ -42,8 +42,9 @@ final class PhabricatorPeopleApplication extends PhabricatorApplication { return array( '/people/' => array( $this->getQueryRoutePattern() => 'PhabricatorPeopleListController', - 'logs/(?:query/(?P[^/]+)/)?' - => 'PhabricatorPeopleLogsController', + 'logs/' => array( + $this->getQueryRoutePattern() => 'PhabricatorPeopleLogsController', + ), 'invite/' => array( '(?:query/(?P[^/]+)/)?' => 'PhabricatorPeopleInviteListController', diff --git a/src/applications/people/phid/PhabricatorPeopleUserPHIDType.php b/src/applications/people/phid/PhabricatorPeopleUserPHIDType.php index f0512e91f1..7867f098f1 100644 --- a/src/applications/people/phid/PhabricatorPeopleUserPHIDType.php +++ b/src/applications/people/phid/PhabricatorPeopleUserPHIDType.php @@ -39,11 +39,14 @@ final class PhabricatorPeopleUserPHIDType extends PhabricatorPHIDType { foreach ($handles as $phid => $handle) { $user = $objects[$phid]; $realname = $user->getRealName(); + $username = $user->getUsername(); - $handle->setName($user->getUsername()); - $handle->setURI('/p/'.$user->getUsername().'/'); - $handle->setFullName($user->getFullName()); - $handle->setImageURI($user->getProfileImageURI()); + $handle + ->setName($username) + ->setURI('/p/'.$username.'/') + ->setFullName($user->getFullName()) + ->setImageURI($user->getProfileImageURI()) + ->setMailStampName('@'.$username); if ($user->getIsMailingList()) { $handle->setIcon('fa-envelope-o'); diff --git a/src/applications/people/query/PhabricatorPeopleLogQuery.php b/src/applications/people/query/PhabricatorPeopleLogQuery.php index 9bcdc53f49..fc6a87b335 100644 --- a/src/applications/people/query/PhabricatorPeopleLogQuery.php +++ b/src/applications/people/query/PhabricatorPeopleLogQuery.php @@ -9,6 +9,8 @@ final class PhabricatorPeopleLogQuery private $sessionKeys; private $actions; private $remoteAddressPrefix; + private $dateCreatedMin; + private $dateCreatedMax; public function withActorPHIDs(array $actor_phids) { $this->actorPHIDs = $actor_phids; @@ -40,70 +42,81 @@ final class PhabricatorPeopleLogQuery return $this; } - protected function loadPage() { - $table = new PhabricatorUserLog(); - $conn_r = $table->establishConnection('r'); - - $data = queryfx_all( - $conn_r, - 'SELECT * FROM %T %Q %Q %Q', - $table->getTableName(), - $this->buildWhereClause($conn_r), - $this->buildOrderClause($conn_r), - $this->buildLimitClause($conn_r)); - - return $table->loadAllFromArray($data); + public function withDateCreatedBetween($min, $max) { + $this->dateCreatedMin = $min; + $this->dateCreatedMax = $max; + return $this; } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { - $where = array(); + public function newResultObject() { + return new PhabricatorUserLog(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); if ($this->actorPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'actorPHID IN (%Ls)', $this->actorPHIDs); } if ($this->userPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'userPHID IN (%Ls)', $this->userPHIDs); } if ($this->relatedPHIDs !== null) { $where[] = qsprintf( - $conn_r, - 'actorPHID IN (%Ls) OR userPHID IN (%Ls)', + $conn, + '(actorPHID IN (%Ls) OR userPHID IN (%Ls))', $this->relatedPHIDs, $this->relatedPHIDs); } if ($this->sessionKeys !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'session IN (%Ls)', $this->sessionKeys); } if ($this->actions !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'action IN (%Ls)', $this->actions); } if ($this->remoteAddressPrefix !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'remoteAddr LIKE %>', $this->remoteAddressPrefix); } - $where[] = $this->buildPagingClause($conn_r); + if ($this->dateCreatedMin !== null) { + $where[] = qsprintf( + $conn, + 'dateCreated >= %d', + $this->dateCreatedMin); + } - return $this->formatWhereClause($where); + if ($this->dateCreatedMax !== null) { + $where[] = qsprintf( + $conn, + 'dateCreated <= %d', + $this->dateCreatedMax); + } + + return $where; } public function getQueryApplicationClass() { diff --git a/src/applications/people/query/PhabricatorPeopleLogSearchEngine.php b/src/applications/people/query/PhabricatorPeopleLogSearchEngine.php index 851ef113d0..b052456cd3 100644 --- a/src/applications/people/query/PhabricatorPeopleLogSearchEngine.php +++ b/src/applications/people/query/PhabricatorPeopleLogSearchEngine.php @@ -15,34 +15,8 @@ final class PhabricatorPeopleLogSearchEngine return 500; } - public function buildSavedQueryFromRequest(AphrontRequest $request) { - $saved = new PhabricatorSavedQuery(); - - $saved->setParameter( - 'userPHIDs', - $this->readUsersFromRequest($request, 'users')); - - $saved->setParameter( - 'actorPHIDs', - $this->readUsersFromRequest($request, 'actors')); - - $saved->setParameter( - 'actions', - $this->readListFromRequest($request, 'actions')); - - $saved->setParameter( - 'ip', - $request->getStr('ip')); - - $saved->setParameter( - 'sessions', - $this->readListFromRequest($request, 'sessions')); - - return $saved; - } - - public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { - $query = id(new PhabricatorPeopleLogQuery()); + public function newQuery() { + $query = new PhabricatorPeopleLogQuery(); // NOTE: If the viewer isn't an administrator, always restrict the query to // related records. This echoes the policy logic of these logs. This is @@ -54,82 +28,73 @@ final class PhabricatorPeopleLogSearchEngine $query->withRelatedPHIDs(array($viewer->getPHID())); } - $actor_phids = $saved->getParameter('actorPHIDs', array()); - if ($actor_phids) { - $query->withActorPHIDs($actor_phids); + return $query; + } + + protected function buildQueryFromParameters(array $map) { + $query = $this->newQuery(); + + if ($map['userPHIDs']) { + $query->withUserPHIDs($map['userPHIDs']); } - $user_phids = $saved->getParameter('userPHIDs', array()); - if ($user_phids) { - $query->withUserPHIDs($user_phids); + if ($map['actorPHIDs']) { + $query->withActorPHIDs($map['actorPHIDs']); } - $actions = $saved->getParameter('actions', array()); - if ($actions) { - $query->withActions($actions); + if ($map['actions']) { + $query->withActions($map['actions']); } - $remote_prefix = $saved->getParameter('ip'); - if (strlen($remote_prefix)) { - $query->withRemoteAddressprefix($remote_prefix); + if (strlen($map['ip'])) { + $query->withRemoteAddressPrefix($map['ip']); } - $sessions = $saved->getParameter('sessions', array()); - if ($sessions) { - $query->withSessionKeys($sessions); + if ($map['sessions']) { + $query->withSessionKeys($map['sessions']); + } + + if ($map['createdStart'] || $map['createdEnd']) { + $query->withDateCreatedBetween( + $map['createdStart'], + $map['createdEnd']); } return $query; } - public function buildSearchForm( - AphrontFormView $form, - PhabricatorSavedQuery $saved) { - - $actor_phids = $saved->getParameter('actorPHIDs', array()); - $user_phids = $saved->getParameter('userPHIDs', array()); - - $actions = $saved->getParameter('actions', array()); - $remote_prefix = $saved->getParameter('ip'); - $sessions = $saved->getParameter('sessions', array()); - - $actions = array_fuse($actions); - $action_control = id(new AphrontFormCheckboxControl()) - ->setLabel(pht('Actions')); - $action_types = PhabricatorUserLog::getActionTypeMap(); - foreach ($action_types as $type => $label) { - $action_control->addCheckbox( - 'actions[]', - $type, - $label, - isset($actions[$label])); - } - - $form - ->appendControl( - id(new AphrontFormTokenizerControl()) - ->setDatasource(new PhabricatorPeopleDatasource()) - ->setName('actors') - ->setLabel(pht('Actors')) - ->setValue($actor_phids)) - ->appendControl( - id(new AphrontFormTokenizerControl()) - ->setDatasource(new PhabricatorPeopleDatasource()) - ->setName('users') - ->setLabel(pht('Users')) - ->setValue($user_phids)) - ->appendChild($action_control) - ->appendChild( - id(new AphrontFormTextControl()) - ->setLabel(pht('Filter IP')) - ->setName('ip') - ->setValue($remote_prefix)) - ->appendChild( - id(new AphrontFormTextControl()) - ->setLabel(pht('Sessions')) - ->setName('sessions') - ->setValue(implode(', ', $sessions))); - + protected function buildCustomSearchFields() { + return array( + id(new PhabricatorUsersSearchField()) + ->setKey('userPHIDs') + ->setAliases(array('users', 'user', 'userPHID')) + ->setLabel(pht('Users')) + ->setDescription(pht('Search for activity affecting specific users.')), + id(new PhabricatorUsersSearchField()) + ->setKey('actorPHIDs') + ->setAliases(array('actors', 'actor', 'actorPHID')) + ->setLabel(pht('Actors')) + ->setDescription(pht('Search for activity by specific users.')), + id(new PhabricatorSearchCheckboxesField()) + ->setKey('actions') + ->setLabel(pht('Actions')) + ->setDescription(pht('Search for particular types of activity.')) + ->setOptions(PhabricatorUserLog::getActionTypeMap()), + id(new PhabricatorSearchTextField()) + ->setKey('ip') + ->setLabel(pht('Filter IP')) + ->setDescription(pht('Search for actions by remote address.')), + id(new PhabricatorSearchStringListField()) + ->setKey('sessions') + ->setLabel(pht('Sessions')) + ->setDescription(pht('Search for activity in particular sessions.')), + id(new PhabricatorSearchDateField()) + ->setLabel(pht('Created After')) + ->setKey('createdStart'), + id(new PhabricatorSearchDateField()) + ->setLabel(pht('Created Before')) + ->setKey('createdEnd'), + ); } protected function getURI($path) { @@ -156,19 +121,6 @@ final class PhabricatorPeopleLogSearchEngine return parent::buildSavedQueryFromBuiltin($query_key); } - protected function getRequiredHandlePHIDsForResultList( - array $logs, - PhabricatorSavedQuery $query) { - - $phids = array(); - foreach ($logs as $log) { - $phids[$log->getActorPHID()] = true; - $phids[$log->getUserPHID()] = true; - } - - return array_keys($phids); - } - protected function renderResultList( array $logs, PhabricatorSavedQuery $query, @@ -179,16 +131,111 @@ final class PhabricatorPeopleLogSearchEngine $table = id(new PhabricatorUserLogView()) ->setUser($viewer) - ->setLogs($logs) - ->setHandles($handles); + ->setLogs($logs); if ($viewer->getIsAdmin()) { $table->setSearchBaseURI($this->getApplicationURI('logs/')); } - $result = new PhabricatorApplicationSearchResultView(); - $result->setTable($table); - - return $result; + return id(new PhabricatorApplicationSearchResultView()) + ->setTable($table); } + + protected function newExportFields() { + $viewer = $this->requireViewer(); + + $fields = array( + $fields[] = id(new PhabricatorPHIDExportField()) + ->setKey('actorPHID') + ->setLabel(pht('Actor PHID')), + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('actor') + ->setLabel(pht('Actor')), + $fields[] = id(new PhabricatorPHIDExportField()) + ->setKey('userPHID') + ->setLabel(pht('User PHID')), + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('user') + ->setLabel(pht('User')), + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('action') + ->setLabel(pht('Action')), + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('actionName') + ->setLabel(pht('Action Name')), + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('session') + ->setLabel(pht('Session')), + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('old') + ->setLabel(pht('Old Value')), + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('new') + ->setLabel(pht('New Value')), + ); + + if ($viewer->getIsAdmin()) { + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('remoteAddress') + ->setLabel(pht('Remote Address')); + } + + return $fields; + } + + protected function newExportData(array $logs) { + $viewer = $this->requireViewer(); + + + $phids = array(); + foreach ($logs as $log) { + $phids[] = $log->getUserPHID(); + $phids[] = $log->getActorPHID(); + } + $handles = $viewer->loadHandles($phids); + + $action_map = PhabricatorUserLog::getActionTypeMap(); + + $export = array(); + foreach ($logs as $log) { + + $user_phid = $log->getUserPHID(); + if ($user_phid) { + $user_name = $handles[$user_phid]->getName(); + } else { + $user_name = null; + } + + $actor_phid = $log->getActorPHID(); + if ($actor_phid) { + $actor_name = $handles[$actor_phid]->getName(); + } else { + $actor_name = null; + } + + $action = $log->getAction(); + $action_name = idx($action_map, $action, pht('Unknown ("%s")', $action)); + + $map = array( + 'actorPHID' => $actor_phid, + 'actor' => $actor_name, + 'userPHID' => $user_phid, + 'user' => $user_name, + 'action' => $action, + 'actionName' => $action_name, + 'session' => substr($log->getSession(), 0, 6), + 'old' => $log->getOldValue(), + 'new' => $log->getNewValue(), + ); + + if ($viewer->getIsAdmin()) { + $map['remoteAddress'] = $log->getRemoteAddr(); + } + + $export[] = $map; + } + + return $export; + } + } diff --git a/src/applications/people/query/PhabricatorPeopleSearchEngine.php b/src/applications/people/query/PhabricatorPeopleSearchEngine.php index db2256a8b8..57ed133df4 100644 --- a/src/applications/people/query/PhabricatorPeopleSearchEngine.php +++ b/src/applications/people/query/PhabricatorPeopleSearchEngine.php @@ -322,35 +322,23 @@ final class PhabricatorPeopleSearchEngine protected function newExportFields() { return array( - id(new PhabricatorIDExportField()) - ->setKey('id') - ->setLabel(pht('ID')), - id(new PhabricatorPHIDExportField()) - ->setKey('phid') - ->setLabel(pht('PHID')), id(new PhabricatorStringExportField()) ->setKey('username') ->setLabel(pht('Username')), id(new PhabricatorStringExportField()) ->setKey('realName') ->setLabel(pht('Real Name')), - id(new PhabricatorEpochExportField()) - ->setKey('created') - ->setLabel(pht('Date Created')), ); } - public function newExport(array $users) { + protected function newExportData(array $users) { $viewer = $this->requireViewer(); $export = array(); foreach ($users as $user) { $export[] = array( - 'id' => $user->getID(), - 'phid' => $user->getPHID(), 'username' => $user->getUsername(), 'realName' => $user->getRealName(), - 'created' => $user->getDateCreated(), ); } diff --git a/src/applications/people/view/PhabricatorUserLogView.php b/src/applications/people/view/PhabricatorUserLogView.php index d648b248f7..72a9378a1a 100644 --- a/src/applications/people/view/PhabricatorUserLogView.php +++ b/src/applications/people/view/PhabricatorUserLogView.php @@ -3,7 +3,6 @@ final class PhabricatorUserLogView extends AphrontView { private $logs; - private $handles; private $searchBaseURI; public function setSearchBaseURI($search_base_uri) { @@ -17,45 +16,79 @@ final class PhabricatorUserLogView extends AphrontView { return $this; } - public function setHandles(array $handles) { - assert_instances_of($handles, 'PhabricatorObjectHandle'); - $this->handles = $handles; - return $this; - } - public function render() { $logs = $this->logs; - $handles = $this->handles; $viewer = $this->getUser(); + $phids = array(); + foreach ($logs as $log) { + $phids[] = $log->getActorPHID(); + $phids[] = $log->getUserPHID(); + } + $handles = $viewer->loadHandles($phids); + $action_map = PhabricatorUserLog::getActionTypeMap(); $base_uri = $this->searchBaseURI; + $viewer_phid = $viewer->getPHID(); + $rows = array(); foreach ($logs as $log) { - $ip = $log->getRemoteAddr(); $session = substr($log->getSession(), 0, 6); - if ($base_uri) { - $ip = phutil_tag( - 'a', - array( - 'href' => $base_uri.'?ip='.$ip.'#R', - ), - $ip); + $actor_phid = $log->getActorPHID(); + $user_phid = $log->getUserPHID(); + + if ($viewer->getIsAdmin()) { + $can_see_ip = true; + } else if ($viewer_phid == $actor_phid) { + // You can see the address if you took the action. + $can_see_ip = true; + } else if (!$actor_phid && ($viewer_phid == $user_phid)) { + // You can see the address if it wasn't authenticated and applied + // to you (partial login). + $can_see_ip = true; + } else { + // You can't see the address when an administrator disables your + // account, since it's their address. + $can_see_ip = false; + } + + if ($can_see_ip) { + $ip = $log->getRemoteAddr(); + if ($base_uri) { + $ip = phutil_tag( + 'a', + array( + 'href' => $base_uri.'?ip='.$ip.'#R', + ), + $ip); + } + } else { + $ip = null; } $action = $log->getAction(); $action_name = idx($action_map, $action, $action); + if ($actor_phid) { + $actor_name = $handles[$actor_phid]->renderLink(); + } else { + $actor_name = null; + } + + if ($user_phid) { + $user_name = $handles[$user_phid]->renderLink(); + } else { + $user_name = null; + } + $rows[] = array( phabricator_date($log->getDateCreated(), $viewer), phabricator_time($log->getDateCreated(), $viewer), $action_name, - $log->getActorPHID() - ? $handles[$log->getActorPHID()]->getName() - : null, - $username = $handles[$log->getUserPHID()]->renderLink(), + $actor_name, + $user_name, $ip, $session, ); diff --git a/src/applications/phame/editor/PhameBlogEditor.php b/src/applications/phame/editor/PhameBlogEditor.php index f30a74065e..c122d8fa3b 100644 --- a/src/applications/phame/editor/PhameBlogEditor.php +++ b/src/applications/phame/editor/PhameBlogEditor.php @@ -48,12 +48,10 @@ final class PhameBlogEditor } protected function buildMailTemplate(PhabricatorLiskDAO $object) { - $phid = $object->getPHID(); $name = $object->getName(); return id(new PhabricatorMetaMTAMail()) - ->setSubject($name) - ->addHeader('Thread-Topic', $phid); + ->setSubject($name); } protected function buildReplyHandler(PhabricatorLiskDAO $object) { diff --git a/src/applications/phame/editor/PhamePostEditor.php b/src/applications/phame/editor/PhamePostEditor.php index 488d7a4938..d95389e549 100644 --- a/src/applications/phame/editor/PhamePostEditor.php +++ b/src/applications/phame/editor/PhamePostEditor.php @@ -61,12 +61,10 @@ final class PhamePostEditor } protected function buildMailTemplate(PhabricatorLiskDAO $object) { - $phid = $object->getPHID(); $title = $object->getTitle(); return id(new PhabricatorMetaMTAMail()) - ->setSubject($title) - ->addHeader('Thread-Topic', $phid); + ->setSubject($title); } protected function buildReplyHandler(PhabricatorLiskDAO $object) { diff --git a/src/applications/phid/PhabricatorObjectHandle.php b/src/applications/phid/PhabricatorObjectHandle.php index 6379915225..a7ec1f5ddf 100644 --- a/src/applications/phid/PhabricatorObjectHandle.php +++ b/src/applications/phid/PhabricatorObjectHandle.php @@ -31,6 +31,7 @@ final class PhabricatorObjectHandle private $subtitle; private $tokenIcon; private $commandLineObjectName; + private $mailStampName; private $stateIcon; private $stateColor; @@ -134,6 +135,15 @@ final class PhabricatorObjectHandle return $this->objectName; } + public function setMailStampName($mail_stamp_name) { + $this->mailStampName = $mail_stamp_name; + return $this; + } + + public function getMailStampName() { + return $this->mailStampName; + } + public function setURI($uri) { $this->uri = $uri; return $this; diff --git a/src/applications/pholio/editor/PholioMockEditor.php b/src/applications/pholio/editor/PholioMockEditor.php index c0fcf31f83..6bd49d2e7b 100644 --- a/src/applications/pholio/editor/PholioMockEditor.php +++ b/src/applications/pholio/editor/PholioMockEditor.php @@ -112,11 +112,9 @@ final class PholioMockEditor extends PhabricatorApplicationTransactionEditor { protected function buildMailTemplate(PhabricatorLiskDAO $object) { $id = $object->getID(); $name = $object->getName(); - $original_name = $object->getOriginalName(); return id(new PhabricatorMetaMTAMail()) - ->setSubject("M{$id}: {$name}") - ->addHeader('Thread-Topic', "M{$id}: {$original_name}"); + ->setSubject("M{$id}: {$name}"); } protected function getMailTo(PhabricatorLiskDAO $object) { diff --git a/src/applications/pholio/storage/PholioMock.php b/src/applications/pholio/storage/PholioMock.php index 4aa9ef4055..523733b3df 100644 --- a/src/applications/pholio/storage/PholioMock.php +++ b/src/applications/pholio/storage/PholioMock.php @@ -25,7 +25,6 @@ final class PholioMock extends PholioDAO protected $editPolicy; protected $name; - protected $originalName; protected $description; protected $coverPHID; protected $mailKey; @@ -65,7 +64,6 @@ final class PholioMock extends PholioDAO self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text128', 'description' => 'text', - 'originalName' => 'text128', 'mailKey' => 'bytes20', 'status' => 'text12', ), diff --git a/src/applications/pholio/xaction/PholioMockNameTransaction.php b/src/applications/pholio/xaction/PholioMockNameTransaction.php index d1231636af..82fb92fe40 100644 --- a/src/applications/pholio/xaction/PholioMockNameTransaction.php +++ b/src/applications/pholio/xaction/PholioMockNameTransaction.php @@ -15,9 +15,6 @@ final class PholioMockNameTransaction public function applyInternalEffects($object, $value) { $object->setName($value); - if ($object->getOriginalName() === null) { - $object->setOriginalName($this->getNewValue()); - } } public function getTitle() { diff --git a/src/applications/phortune/editor/PhortuneCartEditor.php b/src/applications/phortune/editor/PhortuneCartEditor.php index 5196e12429..dcf1f2d0e0 100644 --- a/src/applications/phortune/editor/PhortuneCartEditor.php +++ b/src/applications/phortune/editor/PhortuneCartEditor.php @@ -123,8 +123,7 @@ final class PhortuneCartEditor $name = $object->getName(); return id(new PhabricatorMetaMTAMail()) - ->setSubject(pht('Order %d: %s', $id, $name)) - ->addHeader('Thread-Topic', pht('Order %s', $id)); + ->setSubject(pht('Order %d: %s', $id, $name)); } protected function buildMailBody( diff --git a/src/applications/phortune/query/PhortunePaymentMethodQuery.php b/src/applications/phortune/query/PhortunePaymentMethodQuery.php index 07d49a5909..42d54805e6 100644 --- a/src/applications/phortune/query/PhortunePaymentMethodQuery.php +++ b/src/applications/phortune/query/PhortunePaymentMethodQuery.php @@ -34,19 +34,12 @@ final class PhortunePaymentMethodQuery return $this; } + public function newResultObject() { + return new PhortunePaymentMethod(); + } + protected function loadPage() { - $table = new PhortunePaymentMethod(); - $conn = $table->establishConnection('r'); - - $rows = queryfx_all( - $conn, - 'SELECT * FROM %T %Q %Q %Q', - $table->getTableName(), - $this->buildWhereClause($conn), - $this->buildOrderClause($conn), - $this->buildLimitClause($conn)); - - return $table->loadAllFromArray($rows); + return $this->loadStandardPage($this->newResultObject()); } protected function willFilterPage(array $methods) { @@ -106,8 +99,8 @@ final class PhortunePaymentMethodQuery return $methods; } - protected function buildWhereClause(AphrontDatabaseConnection $conn) { - $where = array(); + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( @@ -144,9 +137,7 @@ final class PhortunePaymentMethodQuery $this->statuses); } - $where[] = $this->buildPagingClause($conn); - - return $this->formatWhereClause($where); + return $where; } public function getQueryApplicationClass() { diff --git a/src/applications/phortune/storage/PhortuneCart.php b/src/applications/phortune/storage/PhortuneCart.php index af2b386dfe..07554ccb87 100644 --- a/src/applications/phortune/storage/PhortuneCart.php +++ b/src/applications/phortune/storage/PhortuneCart.php @@ -118,6 +118,13 @@ final class PhortuneCart extends PhortuneDAO ->setAmountAsCurrency($this->getTotalPriceAsCurrency()); if ($method) { + if (!$method->isActive()) { + throw new Exception( + pht( + 'Attempting to apply a charge using an inactive '. + 'payment method ("%s")!', + $method->getPHID())); + } $charge->setPaymentMethodPHID($method->getPHID()); } diff --git a/src/applications/phortune/storage/PhortunePaymentMethod.php b/src/applications/phortune/storage/PhortunePaymentMethod.php index 8044168ba4..1712d3f973 100644 --- a/src/applications/phortune/storage/PhortunePaymentMethod.php +++ b/src/applications/phortune/storage/PhortunePaymentMethod.php @@ -128,6 +128,10 @@ final class PhortunePaymentMethod extends PhortuneDAO return $month.'/'.$year; } + public function isActive() { + return ($this->getStatus() === self::STATUS_ACTIVE); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/phortune/worker/PhortuneSubscriptionWorker.php b/src/applications/phortune/worker/PhortuneSubscriptionWorker.php index 097fb54dd9..d05aacbb7c 100644 --- a/src/applications/phortune/worker/PhortuneSubscriptionWorker.php +++ b/src/applications/phortune/worker/PhortuneSubscriptionWorker.php @@ -141,6 +141,10 @@ final class PhortuneSubscriptionWorker extends PhabricatorWorker { $method = id(new PhortunePaymentMethodQuery()) ->setViewer($viewer) ->withPHIDs(array($subscription->getDefaultPaymentMethodPHID())) + ->withStatuses( + array( + PhortunePaymentMethod::STATUS_ACTIVE, + )) ->executeOne(); if (!$method) { $issues[] = pht( diff --git a/src/applications/phriction/editor/PhrictionTransactionEditor.php b/src/applications/phriction/editor/PhrictionTransactionEditor.php index fcc9fe0474..73aee3fd4c 100644 --- a/src/applications/phriction/editor/PhrictionTransactionEditor.php +++ b/src/applications/phriction/editor/PhrictionTransactionEditor.php @@ -299,12 +299,10 @@ final class PhrictionTransactionEditor } protected function buildMailTemplate(PhabricatorLiskDAO $object) { - $id = $object->getID(); $title = $object->getContent()->getTitle(); return id(new PhabricatorMetaMTAMail()) - ->setSubject($title) - ->addHeader('Thread-Topic', $object->getPHID()); + ->setSubject($title); } protected function buildMailBody( diff --git a/src/applications/phurl/editor/PhabricatorPhurlURLEditor.php b/src/applications/phurl/editor/PhabricatorPhurlURLEditor.php index 439d62f84a..49f290c343 100644 --- a/src/applications/phurl/editor/PhabricatorPhurlURLEditor.php +++ b/src/applications/phurl/editor/PhabricatorPhurlURLEditor.php @@ -68,8 +68,7 @@ final class PhabricatorPhurlURLEditor $name = $object->getName(); return id(new PhabricatorMetaMTAMail()) - ->setSubject("U{$id}: {$name}") - ->addHeader('Thread-Topic', "U{$id}: ".$object->getName()); + ->setSubject("U{$id}: {$name}"); } protected function buildMailBody( diff --git a/src/applications/ponder/editor/PonderAnswerEditor.php b/src/applications/ponder/editor/PonderAnswerEditor.php index bab0e1f72d..37b2fe2cd0 100644 --- a/src/applications/ponder/editor/PonderAnswerEditor.php +++ b/src/applications/ponder/editor/PonderAnswerEditor.php @@ -57,8 +57,7 @@ final class PonderAnswerEditor extends PonderEditor { $id = $object->getID(); return id(new PhabricatorMetaMTAMail()) - ->setSubject("ANSR{$id}") - ->addHeader('Thread-Topic', "ANSR{$id}"); + ->setSubject("ANSR{$id}"); } protected function buildMailBody( diff --git a/src/applications/ponder/editor/PonderQuestionEditor.php b/src/applications/ponder/editor/PonderQuestionEditor.php index 0720f436b9..ba9687bd0d 100644 --- a/src/applications/ponder/editor/PonderQuestionEditor.php +++ b/src/applications/ponder/editor/PonderQuestionEditor.php @@ -146,11 +146,9 @@ final class PonderQuestionEditor protected function buildMailTemplate(PhabricatorLiskDAO $object) { $id = $object->getID(); $title = $object->getTitle(); - $original_title = $object->getOriginalTitle(); return id(new PhabricatorMetaMTAMail()) - ->setSubject("Q{$id}: {$title}") - ->addHeader('Thread-Topic', "Q{$id}: {$original_title}"); + ->setSubject("Q{$id}: {$title}"); } protected function buildMailBody( diff --git a/src/applications/ponder/storage/PonderQuestion.php b/src/applications/ponder/storage/PonderQuestion.php index eefcdba9be..17f7ee3fdc 100644 --- a/src/applications/ponder/storage/PonderQuestion.php +++ b/src/applications/ponder/storage/PonderQuestion.php @@ -194,11 +194,6 @@ final class PonderQuestion extends PonderDAO return parent::save(); } - public function getOriginalTitle() { - // TODO: Make this actually save/return the original title. - return $this->getTitle(); - } - public function getFullTitle() { $id = $this->getID(); $title = $this->getTitle(); diff --git a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php index 2764ce6322..de61c2a09b 100644 --- a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php +++ b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php @@ -219,12 +219,10 @@ final class PhabricatorProjectTransactionEditor } protected function buildMailTemplate(PhabricatorLiskDAO $object) { - $id = $object->getID(); $name = $object->getName(); return id(new PhabricatorMetaMTAMail()) - ->setSubject("{$name}") - ->addHeader('Thread-Topic', "Project {$id}"); + ->setSubject("{$name}"); } protected function buildMailBody( diff --git a/src/applications/project/engineextension/PhabricatorProjectsMailEngineExtension.php b/src/applications/project/engineextension/PhabricatorProjectsMailEngineExtension.php new file mode 100644 index 0000000000..6f92f87b11 --- /dev/null +++ b/src/applications/project/engineextension/PhabricatorProjectsMailEngineExtension.php @@ -0,0 +1,32 @@ +setKey('tag') + ->setLabel(pht('Tagged with Project')), + ); + } + + public function newMailStamps($object, array $xactions) { + $editor = $this->getEditor(); + $viewer = $this->getViewer(); + + $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( + $object->getPHID(), + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); + + $this->getMailStamp('tag') + ->setValue($project_phids); + } + +} diff --git a/src/applications/project/phid/PhabricatorProjectProjectPHIDType.php b/src/applications/project/phid/PhabricatorProjectProjectPHIDType.php index 3aa6088780..9247966d75 100644 --- a/src/applications/project/phid/PhabricatorProjectProjectPHIDType.php +++ b/src/applications/project/phid/PhabricatorProjectProjectPHIDType.php @@ -45,11 +45,12 @@ final class PhabricatorProjectProjectPHIDType extends PhabricatorPHIDType { if (strlen($slug)) { $handle->setObjectName('#'.$slug); + $handle->setMailStampName('#'.$slug); $handle->setURI("/tag/{$slug}/"); } else { // We set the name to the project's PHID to avoid a parse error when a // project has no hashtag (as is the case with milestones by default). - // See T12659 for more details + // See T12659 for more details. $handle->setCommandLineObjectName($project->getPHID()); $handle->setURI("/project/view/{$id}/"); } diff --git a/src/applications/releeph/editor/ReleephRequestTransactionalEditor.php b/src/applications/releeph/editor/ReleephRequestTransactionalEditor.php index 4710557043..da488d9c72 100644 --- a/src/applications/releeph/editor/ReleephRequestTransactionalEditor.php +++ b/src/applications/releeph/editor/ReleephRequestTransactionalEditor.php @@ -196,11 +196,9 @@ final class ReleephRequestTransactionalEditor protected function buildMailTemplate(PhabricatorLiskDAO $object) { $id = $object->getID(); - $phid = $object->getPHID(); $title = $object->getSummaryForDisplay(); return id(new PhabricatorMetaMTAMail()) - ->setSubject("RQ{$id}: {$title}") - ->addHeader('Thread-Topic', "RQ{$id}: {$phid}"); + ->setSubject("RQ{$id}: {$title}"); } protected function getMailTo(PhabricatorLiskDAO $object) { diff --git a/src/applications/repository/phid/PhabricatorRepositoryRepositoryPHIDType.php b/src/applications/repository/phid/PhabricatorRepositoryRepositoryPHIDType.php index c938b31a65..ba78b0fe7a 100644 --- a/src/applications/repository/phid/PhabricatorRepositoryRepositoryPHIDType.php +++ b/src/applications/repository/phid/PhabricatorRepositoryRepositoryPHIDType.php @@ -41,9 +41,11 @@ final class PhabricatorRepositoryRepositoryPHIDType $name = $repository->getName(); $uri = $repository->getURI(); - $handle->setName($monogram); - $handle->setFullName("{$monogram} {$name}"); - $handle->setURI($uri); + $handle + ->setName($monogram) + ->setFullName("{$monogram} {$name}") + ->setURI($uri) + ->setMailStampName($monogram); } } diff --git a/src/applications/repository/query/PhabricatorRepositoryPullEventQuery.php b/src/applications/repository/query/PhabricatorRepositoryPullEventQuery.php index af60ee0383..ce14a6f831 100644 --- a/src/applications/repository/query/PhabricatorRepositoryPullEventQuery.php +++ b/src/applications/repository/query/PhabricatorRepositoryPullEventQuery.php @@ -7,6 +7,8 @@ final class PhabricatorRepositoryPullEventQuery private $phids; private $repositoryPHIDs; private $pullerPHIDs; + private $epochMin; + private $epochMax; public function withIDs(array $ids) { $this->ids = $ids; @@ -28,6 +30,12 @@ final class PhabricatorRepositoryPullEventQuery return $this; } + public function withEpochBetween($min, $max) { + $this->epochMin = $min; + $this->epochMax = $max; + return $this; + } + public function newResultObject() { return new PhabricatorRepositoryPullEvent(); } @@ -103,6 +111,20 @@ final class PhabricatorRepositoryPullEventQuery $this->pullerPHIDs); } + if ($this->epochMin !== null) { + $where[] = qsprintf( + $conn, + 'epoch >= %d', + $this->epochMin); + } + + if ($this->epochMax !== null) { + $where[] = qsprintf( + $conn, + 'epoch <= %d', + $this->epochMax); + } + return $where; } diff --git a/src/applications/repository/query/PhabricatorRepositoryPushLogQuery.php b/src/applications/repository/query/PhabricatorRepositoryPushLogQuery.php index 94a0b6922e..cb097fae2f 100644 --- a/src/applications/repository/query/PhabricatorRepositoryPushLogQuery.php +++ b/src/applications/repository/query/PhabricatorRepositoryPushLogQuery.php @@ -10,6 +10,8 @@ final class PhabricatorRepositoryPushLogQuery private $refTypes; private $newRefs; private $pushEventPHIDs; + private $epochMin; + private $epochMax; public function withIDs(array $ids) { $this->ids = $ids; @@ -46,19 +48,18 @@ final class PhabricatorRepositoryPushLogQuery return $this; } + public function withEpochBetween($min, $max) { + $this->epochMin = $min; + $this->epochMax = $max; + return $this; + } + + public function newResultObject() { + return new PhabricatorRepositoryPushLog(); + } + protected function loadPage() { - $table = new PhabricatorRepositoryPushLog(); - $conn_r = $table->establishConnection('r'); - - $data = queryfx_all( - $conn_r, - 'SELECT * FROM %T %Q %Q %Q', - $table->getTableName(), - $this->buildWhereClause($conn_r), - $this->buildOrderClause($conn_r), - $this->buildLimitClause($conn_r)); - - return $table->loadAllFromArray($data); + return $this->loadStandardPage($this->newResultObject()); } protected function willFilterPage(array $logs) { @@ -82,61 +83,73 @@ final class PhabricatorRepositoryPushLogQuery return $logs; } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { - $where = array(); + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); - if ($this->ids) { + if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->ids); } - if ($this->phids) { + if ($this->phids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'phid IN (%Ls)', $this->phids); } - if ($this->repositoryPHIDs) { + if ($this->repositoryPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'repositoryPHID IN (%Ls)', $this->repositoryPHIDs); } - if ($this->pusherPHIDs) { + if ($this->pusherPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'pusherPHID in (%Ls)', $this->pusherPHIDs); } - if ($this->pushEventPHIDs) { + if ($this->pushEventPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'pushEventPHID in (%Ls)', $this->pushEventPHIDs); } - if ($this->refTypes) { + if ($this->refTypes !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'refType IN (%Ls)', $this->refTypes); } - if ($this->newRefs) { + if ($this->newRefs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'refNew IN (%Ls)', $this->newRefs); } - $where[] = $this->buildPagingClause($conn_r); + if ($this->epochMin !== null) { + $where[] = qsprintf( + $conn, + 'epoch >= %d', + $this->epochMin); + } - return $this->formatWhereClause($where); + if ($this->epochMax !== null) { + $where[] = qsprintf( + $conn, + 'epoch <= %d', + $this->epochMax); + } + + return $where; } public function getQueryApplicationClass() { diff --git a/src/applications/repository/query/PhabricatorRepositoryPushLogSearchEngine.php b/src/applications/repository/query/PhabricatorRepositoryPushLogSearchEngine.php index d171b80999..8ad76987f9 100644 --- a/src/applications/repository/query/PhabricatorRepositoryPushLogSearchEngine.php +++ b/src/applications/repository/query/PhabricatorRepositoryPushLogSearchEngine.php @@ -26,6 +26,12 @@ final class PhabricatorRepositoryPushLogSearchEngine $query->withPusherPHIDs($map['pusherPHIDs']); } + if ($map['createdStart'] || $map['createdEnd']) { + $query->withEpochBetween( + $map['createdStart'], + $map['createdEnd']); + } + return $query; } @@ -44,6 +50,12 @@ final class PhabricatorRepositoryPushLogSearchEngine ->setLabel(pht('Pushers')) ->setDescription( pht('Search for pull logs by specific users.')), + id(new PhabricatorSearchDateField()) + ->setLabel(pht('Created After')) + ->setKey('createdStart'), + id(new PhabricatorSearchDateField()) + ->setLabel(pht('Created Before')) + ->setKey('createdEnd'), ); } @@ -82,4 +94,146 @@ final class PhabricatorRepositoryPushLogSearchEngine ->setTable($table); } + protected function newExportFields() { + $viewer = $this->requireViewer(); + + $fields = array( + $fields[] = id(new PhabricatorIDExportField()) + ->setKey('pushID') + ->setLabel(pht('Push ID')), + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('protocol') + ->setLabel(pht('Protocol')), + $fields[] = id(new PhabricatorPHIDExportField()) + ->setKey('repositoryPHID') + ->setLabel(pht('Repository PHID')), + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('repository') + ->setLabel(pht('Repository')), + $fields[] = id(new PhabricatorPHIDExportField()) + ->setKey('pusherPHID') + ->setLabel(pht('Pusher PHID')), + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('pusher') + ->setLabel(pht('Pusher')), + $fields[] = id(new PhabricatorPHIDExportField()) + ->setKey('devicePHID') + ->setLabel(pht('Device PHID')), + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('device') + ->setLabel(pht('Device')), + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('type') + ->setLabel(pht('Ref Type')), + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('name') + ->setLabel(pht('Ref Name')), + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('old') + ->setLabel(pht('Ref Old')), + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('new') + ->setLabel(pht('Ref New')), + $fields[] = id(new PhabricatorIntExportField()) + ->setKey('flags') + ->setLabel(pht('Flags')), + $fields[] = id(new PhabricatorStringListExportField()) + ->setKey('flagNames') + ->setLabel(pht('Flag Names')), + $fields[] = id(new PhabricatorIntExportField()) + ->setKey('result') + ->setLabel(pht('Result')), + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('resultName') + ->setLabel(pht('Result Name')), + ); + + if ($viewer->getIsAdmin()) { + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('remoteAddress') + ->setLabel(pht('Remote Address')); + } + + return $fields; + } + + protected function newExportData(array $logs) { + $viewer = $this->requireViewer(); + + $phids = array(); + foreach ($logs as $log) { + $phids[] = $log->getPusherPHID(); + $phids[] = $log->getDevicePHID(); + $phids[] = $log->getPushEvent()->getRepositoryPHID(); + } + $handles = $viewer->loadHandles($phids); + + $flag_map = PhabricatorRepositoryPushLog::getFlagDisplayNames(); + $reject_map = PhabricatorRepositoryPushLog::getRejectCodeDisplayNames(); + + $export = array(); + foreach ($logs as $log) { + $event = $log->getPushEvent(); + + $repository_phid = $event->getRepositoryPHID(); + if ($repository_phid) { + $repository_name = $handles[$repository_phid]->getName(); + } else { + $repository_name = null; + } + + $pusher_phid = $log->getPusherPHID(); + if ($pusher_phid) { + $pusher_name = $handles[$pusher_phid]->getName(); + } else { + $pusher_name = null; + } + + $device_phid = $log->getDevicePHID(); + if ($device_phid) { + $device_name = $handles[$device_phid]->getName(); + } else { + $device_name = null; + } + + $flags = $log->getChangeFlags(); + $flag_names = array(); + foreach ($flag_map as $flag_key => $flag_name) { + if (($flags & $flag_key) === $flag_key) { + $flag_names[] = $flag_name; + } + } + + $result = $event->getRejectCode(); + $result_name = idx($reject_map, $result, pht('Unknown ("%s")', $result)); + + $map = array( + 'pushID' => $event->getID(), + 'protocol' => $event->getRemoteProtocol(), + 'repositoryPHID' => $repository_phid, + 'repository' => $repository_name, + 'pusherPHID' => $pusher_phid, + 'pusher' => $pusher_name, + 'devicePHID' => $device_phid, + 'device' => $device_name, + 'type' => $log->getRefType(), + 'name' => $log->getRefName(), + 'old' => $log->getRefOld(), + 'new' => $log->getRefNew(), + 'flags' => $flags, + 'flagNames' => $flag_names, + 'result' => $result, + 'resultName' => $result_name, + ); + + if ($viewer->getIsAdmin()) { + $map['remoteAddress'] = $event->getRemoteAddress(); + } + + $export[] = $map; + } + + return $export; + } + } diff --git a/src/applications/repository/storage/PhabricatorRepositoryAuditRequest.php b/src/applications/repository/storage/PhabricatorRepositoryAuditRequest.php index ea86594d9e..e05820c825 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryAuditRequest.php +++ b/src/applications/repository/storage/PhabricatorRepositoryAuditRequest.php @@ -72,6 +72,15 @@ final class PhabricatorRepositoryAuditRequest return true; } + public function isResigned() { + switch ($this->getAuditStatus()) { + case PhabricatorAuditStatusConstants::RESIGNED: + return true; + } + + return false; + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/repository/storage/PhabricatorRepositoryCommit.php b/src/applications/repository/storage/PhabricatorRepositoryCommit.php index 31a06dcbd4..1c5998d583 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryCommit.php +++ b/src/applications/repository/storage/PhabricatorRepositoryCommit.php @@ -657,7 +657,8 @@ final class PhabricatorRepositoryCommit public function isAutomaticallySubscribed($phid) { // TODO: This should also list auditors, but handling that is a bit messy - // right now because we are not guaranteed to have the data. + // right now because we are not guaranteed to have the data. (It should not + // include resigned auditors.) return ($phid == $this->getAuthorPHID()); } diff --git a/src/applications/repository/storage/PhabricatorRepositoryPushLog.php b/src/applications/repository/storage/PhabricatorRepositoryPushLog.php index 4e099209c6..c2d3456da6 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryPushLog.php +++ b/src/applications/repository/storage/PhabricatorRepositoryPushLog.php @@ -55,6 +55,28 @@ final class PhabricatorRepositoryPushLog ->setPusherPHID($viewer->getPHID()); } + public static function getFlagDisplayNames() { + return array( + self::CHANGEFLAG_ADD => pht('Create'), + self::CHANGEFLAG_DELETE => pht('Delete'), + self::CHANGEFLAG_APPEND => pht('Append'), + self::CHANGEFLAG_REWRITE => pht('Rewrite'), + self::CHANGEFLAG_DANGEROUS => pht('Dangerous'), + self::CHANGEFLAG_ENORMOUS => pht('Enormous'), + ); + } + + public static function getRejectCodeDisplayNames() { + return array( + self::REJECT_ACCEPT => pht('Accepted'), + self::REJECT_DANGEROUS => pht('Rejected: Dangerous'), + self::REJECT_HERALD => pht('Rejected: Herald'), + self::REJECT_EXTERNAL => pht('Rejected: External Hook'), + self::REJECT_BROKEN => pht('Rejected: Broken'), + self::REJECT_ENORMOUS => pht('Rejected: Enormous'), + ); + } + public static function getHeraldChangeFlagConditionOptions() { return array( self::CHANGEFLAG_ADD => @@ -102,6 +124,9 @@ final class PhabricatorRepositoryPushLog 'key_pusher' => array( 'columns' => array('pusherPHID'), ), + 'key_epoch' => array( + 'columns' => array('epoch'), + ), ), ) + parent::getConfiguration(); } diff --git a/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php b/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php index 9fb6667924..818d1e8781 100644 --- a/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php +++ b/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php @@ -15,6 +15,7 @@ final class PhabricatorRepositoryCommitHeraldWorker protected function parseCommit( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { + $viewer = PhabricatorUser::getOmnipotentUser(); if ($this->shouldSkipImportStep()) { // This worker has no followup tasks, so we can just bail out @@ -50,7 +51,7 @@ final class PhabricatorRepositoryCommitHeraldWorker id(new PhabricatorDiffusionApplication())->getPHID()); $editor = id(new PhabricatorAuditEditor()) - ->setActor(PhabricatorUser::getOmnipotentUser()) + ->setActor($viewer) ->setActingAsPHID($acting_as_phid) ->setContinueOnMissingFields(true) ->setContinueOnNoEffect(true) @@ -69,29 +70,6 @@ final class PhabricatorRepositoryCommitHeraldWorker 'committerPHID' => $data->getCommitDetail('committerPHID'), )); - $reverts_refs = id(new DifferentialCustomFieldRevertsParser()) - ->parseCorpus($data->getCommitMessage()); - $reverts = array_mergev(ipull($reverts_refs, 'monograms')); - - if ($reverts) { - $reverted_commits = id(new DiffusionCommitQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withRepository($repository) - ->withIdentifiers($reverts) - ->execute(); - $reverted_commit_phids = mpull($reverted_commits, 'getPHID', 'getPHID'); - - // NOTE: Skip any write attempts if a user cleverly implies a commit - // reverts itself. - unset($reverted_commit_phids[$commit->getPHID()]); - - $reverts_edge = DiffusionCommitRevertsCommitEdgeType::EDGECONST; - $xactions[] = id(new PhabricatorAuditTransaction()) - ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) - ->setMetadataValue('edge:type', $reverts_edge) - ->setNewValue(array('+' => array_fuse($reverted_commit_phids))); - } - try { $raw_patch = $this->loadRawPatchText($repository, $commit); } catch (Exception $ex) { diff --git a/src/applications/repository/worker/PhabricatorRepositoryPushMailWorker.php b/src/applications/repository/worker/PhabricatorRepositoryPushMailWorker.php index 17226a1377..554c2cf772 100644 --- a/src/applications/repository/worker/PhabricatorRepositoryPushMailWorker.php +++ b/src/applications/repository/worker/PhabricatorRepositoryPushMailWorker.php @@ -123,8 +123,8 @@ final class PhabricatorRepositoryPushMailWorker ->setSubject($subject) ->setFrom($event->getPusherPHID()) ->setBody($body->render()) + ->setHTMLBody($body->renderHTML()) ->setThreadID($event->getPHID(), $is_new = true) - ->addHeader('Thread-Topic', $subject) ->setIsBulk(true); return $target->willSendMail($mail); diff --git a/src/applications/search/controller/PhabricatorApplicationSearchController.php b/src/applications/search/controller/PhabricatorApplicationSearchController.php index 3839ff3942..f2e6895f3d 100644 --- a/src/applications/search/controller/PhabricatorApplicationSearchController.php +++ b/src/applications/search/controller/PhabricatorApplicationSearchController.php @@ -7,6 +7,7 @@ private $navigation; private $queryKey; private $preface; + private $activeQuery; public function setPreface($preface) { $this->preface = $preface; @@ -45,6 +46,14 @@ return $this->searchEngine; } + protected function getActiveQuery() { + if (!$this->activeQuery) { + throw new Exception(pht('There is no active query yet.')); + } + + return $this->activeQuery; + } + protected function validateDelegatingController() { $parent = $this->getDelegatingController(); @@ -158,10 +167,12 @@ $saved_query = $engine->buildSavedQueryFromRequest($request); // Save the query to generate a query key, so "Save Custom Query..." and - // other features like Maniphest's "Export..." work correctly. + // other features like "Bulk Edit" and "Export Data" work correctly. $engine->saveQuery($saved_query); } + $this->activeQuery = $saved_query; + $nav->selectFilter( 'query/'.$saved_query->getQueryKey(), 'query/advanced'); @@ -420,20 +431,72 @@ if ($named_query) { $filename = $named_query->getQueryName(); + $sheet_title = $named_query->getQueryName(); } else { $filename = $engine->getResultTypeDescription(); + $sheet_title = $engine->getResultTypeDescription(); } $filename = phutil_utf8_strtolower($filename); $filename = PhabricatorFile::normalizeFileName($filename); - $formats = PhabricatorExportFormat::getAllEnabledExportFormats(); - $format_options = mpull($formats, 'getExportFormatName'); + $all_formats = PhabricatorExportFormat::getAllExportFormats(); + + $available_options = array(); + $unavailable_options = array(); + $formats = array(); + $unavailable_formats = array(); + foreach ($all_formats as $key => $format) { + if ($format->isExportFormatEnabled()) { + $available_options[$key] = $format->getExportFormatName(); + $formats[$key] = $format; + } else { + $unavailable_options[$key] = pht( + '%s (Not Available)', + $format->getExportFormatName()); + $unavailable_formats[$key] = $format; + } + } + $format_options = $available_options + $unavailable_options; + + // Try to default to the format the user used last time. If you just + // exported to Excel, you probably want to export to Excel again. + $format_key = $this->readExportFormatPreference(); + if (!isset($formats[$format_key])) { + $format_key = head_key($format_options); + } + + // Check if this is a large result set or not. If we're exporting a + // large amount of data, we'll build the actual export file in the daemons. + + $threshold = 1000; + $query = $engine->buildQueryFromSavedQuery($saved_query); + $pager = $engine->newPagerForSavedQuery($saved_query); + $pager->setPageSize($threshold + 1); + $objects = $engine->executeQuery($query, $pager); + $object_count = count($objects); + $is_large_export = ($object_count > $threshold); $errors = array(); $e_format = null; if ($request->isFormPost()) { $format_key = $request->getStr('format'); + + if (isset($unavailable_formats[$format_key])) { + $unavailable = $unavailable_formats[$format_key]; + $instructions = $unavailable->getInstallInstructions(); + + $markup = id(new PHUIRemarkupView($viewer, $instructions)) + ->setRemarkupOption( + PHUIRemarkupView::OPTION_PRESERVE_LINEBREAKS, + false); + + return $this->newDialog() + ->setTitle(pht('Export Format Not Available')) + ->appendChild($markup) + ->addCancelButton($cancel_uri, pht('Done')); + } + $format = idx($formats, $format_key); if (!$format) { @@ -442,63 +505,33 @@ } if (!$errors) { - $query = $engine->buildQueryFromSavedQuery($saved_query); + $this->writeExportFormatPreference($format_key); - // NOTE: We aren't reading the pager from the request. Exports always - // affect the entire result set. - $pager = $engine->newPagerForSavedQuery($saved_query); - $pager->setPageSize(0x7FFFFFFF); + $export_engine = id(new PhabricatorExportEngine()) + ->setViewer($viewer) + ->setSearchEngine($engine) + ->setSavedQuery($saved_query) + ->setTitle($sheet_title) + ->setFilename($filename) + ->setExportFormat($format); - $objects = $engine->executeQuery($query, $pager); + if ($is_large_export) { + $job = $export_engine->newBulkJob($request); - $extension = $format->getFileExtension(); - $mime_type = $format->getMIMEContentType(); - $filename = $filename.'.'.$extension; + return id(new AphrontRedirectResponse()) + ->setURI($job->getMonitorURI()); + } else { + $file = $export_engine->exportFile(); - $format = clone $format; - $format->setViewer($viewer); - - $export_data = $engine->newExport($objects); - - if (count($export_data) !== count($objects)) { - throw new Exception( - pht( - 'Search engine exported the wrong number of objects, expected '. - '%s but got %s.', - phutil_count($objects), - phutil_count($export_data))); + return $this->newDialog() + ->setTitle(pht('Download Results')) + ->appendParagraph( + pht('Click the download button to download the exported data.')) + ->addCancelButton($cancel_uri, pht('Done')) + ->setSubmitURI($file->getDownloadURI()) + ->setDisableWorkflowOnSubmit(true) + ->addSubmitButton(pht('Download Data')); } - - $objects = array_values($objects); - $export_data = array_values($export_data); - - $field_list = $engine->newExportFieldList(); - $field_list = mpull($field_list, null, 'getKey'); - - for ($ii = 0; $ii < count($objects); $ii++) { - $format->addObject($objects[$ii], $field_list, $export_data[$ii]); - } - - $export_result = $format->newFileData(); - - $file = PhabricatorFile::newFromFileData( - $export_result, - array( - 'name' => $filename, - 'authorPHID' => $viewer->getPHID(), - 'ttl.relative' => phutil_units('15 minutes in seconds'), - 'viewPolicy' => PhabricatorPolicies::POLICY_NOONE, - 'mime-type' => $mime_type, - )); - - return $this->newDialog() - ->setTitle(pht('Download Results')) - ->appendParagraph( - pht('Click the download button to download the exported data.')) - ->addCancelButton($cancel_uri, pht('Done')) - ->setSubmitURI($file->getDownloadURI()) - ->setDisableWorkflowOnSubmit(true) - ->addSubmitButton(pht('Download Data')); } } @@ -509,6 +542,7 @@ ->setName('format') ->setLabel(pht('Format')) ->setError($e_format) + ->setValue($format_key) ->setOptions($format_options)); return $this->newDialog() @@ -854,10 +888,8 @@ $engine = $this->getSearchEngine(); $engine_class = get_class($engine); - $query_key = $this->getQueryKey(); - if (!$query_key) { - $query_key = $engine->getDefaultQueryKey(); - } + + $query_key = $this->getActiveQuery()->getQueryKey(); $can_use = $engine->canUseInPanelContext(); $is_installed = PhabricatorApplication::isClassInstalledForViewer( @@ -924,4 +956,32 @@ return true; } + private function readExportFormatPreference() { + $viewer = $this->getViewer(); + $export_key = PhabricatorPolicyFavoritesSetting::SETTINGKEY; + return $viewer->getUserSetting($export_key); + } + + private function writeExportFormatPreference($value) { + $viewer = $this->getViewer(); + $request = $this->getRequest(); + + if (!$viewer->isLoggedIn()) { + return; + } + + $export_key = PhabricatorPolicyFavoritesSetting::SETTINGKEY; + $preferences = PhabricatorUserPreferences::loadUserPreferences($viewer); + + $editor = id(new PhabricatorUserPreferencesEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true); + + $xactions = array(); + $xactions[] = $preferences->newTransaction($export_key, $value); + $editor->applyTransactions($preferences, $xactions); + } + } diff --git a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php index 33efd890bb..b808291a52 100644 --- a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php +++ b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php @@ -1455,11 +1455,145 @@ abstract class PhabricatorApplicationSearchEngine extends Phobject { } final public function newExportFieldList() { - return $this->newExportFields(); + $object = $this->newResultObject(); + + $builtin_fields = array( + id(new PhabricatorIDExportField()) + ->setKey('id') + ->setLabel(pht('ID')), + ); + + if ($object->getConfigOption(LiskDAO::CONFIG_AUX_PHID)) { + $builtin_fields[] = id(new PhabricatorPHIDExportField()) + ->setKey('phid') + ->setLabel(pht('PHID')); + } + + $fields = mpull($builtin_fields, null, 'getKey'); + + $export_fields = $this->newExportFields(); + foreach ($export_fields as $export_field) { + $key = $export_field->getKey(); + + if (isset($fields[$key])) { + throw new Exception( + pht( + 'Search engine ("%s") defines an export field with a key ("%s") '. + 'that collides with another field. Each field must have a '. + 'unique key.', + get_class($this), + $key)); + } + + $fields[$key] = $export_field; + } + + $extensions = $this->newExportExtensions(); + foreach ($extensions as $extension) { + $extension_fields = $extension->newExportFields(); + foreach ($extension_fields as $extension_field) { + $key = $extension_field->getKey(); + + if (isset($fields[$key])) { + throw new Exception( + pht( + 'Export engine extension ("%s") defines an export field with '. + 'a key ("%s") that collides with another field. Each field '. + 'must have a unique key.', + get_class($extension_field), + $key)); + } + + $fields[$key] = $extension_field; + } + } + + return $fields; + } + + final public function newExport(array $objects) { + $object = $this->newResultObject(); + $has_phid = $object->getConfigOption(LiskDAO::CONFIG_AUX_PHID); + + $objects = array_values($objects); + $n = count($objects); + + $maps = array(); + foreach ($objects as $object) { + $map = array( + 'id' => $object->getID(), + ); + + if ($has_phid) { + $map['phid'] = $object->getPHID(); + } + + $maps[] = $map; + } + + $export_data = $this->newExportData($objects); + $export_data = array_values($export_data); + if (count($export_data) !== count($objects)) { + throw new Exception( + pht( + 'Search engine ("%s") exported the wrong number of objects, '. + 'expected %s but got %s.', + get_class($this), + phutil_count($objects), + phutil_count($export_data))); + } + + for ($ii = 0; $ii < $n; $ii++) { + $maps[$ii] += $export_data[$ii]; + } + + $extensions = $this->newExportExtensions(); + foreach ($extensions as $extension) { + $extension_data = $extension->newExportData($objects); + $extension_data = array_values($extension_data); + if (count($export_data) !== count($objects)) { + throw new Exception( + pht( + 'Export engine extension ("%s") exported the wrong number of '. + 'objects, expected %s but got %s.', + get_class($extension), + phutil_count($objects), + phutil_count($export_data))); + } + + for ($ii = 0; $ii < $n; $ii++) { + $maps[$ii] += $extension_data[$ii]; + } + } + + return $maps; } protected function newExportFields() { return array(); } + protected function newExportData(array $objects) { + throw new PhutilMethodNotImplementedException(); + } + + private function newExportExtensions() { + $object = $this->newResultObject(); + $viewer = $this->requireViewer(); + + $extensions = PhabricatorExportEngineExtension::getAllExtensions(); + + $supported = array(); + foreach ($extensions as $extension) { + $extension = clone $extension; + $extension->setViewer($viewer); + + if ($extension->supportsObject($object)) { + $supported[] = $extension; + } + } + + return $supported; + } + } diff --git a/src/applications/settings/panel/PhabricatorActivitySettingsPanel.php b/src/applications/settings/panel/PhabricatorActivitySettingsPanel.php index 50f951d661..2759f3a26c 100644 --- a/src/applications/settings/panel/PhabricatorActivitySettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorActivitySettingsPanel.php @@ -26,25 +26,9 @@ final class PhabricatorActivitySettingsPanel extends PhabricatorSettingsPanel { ->withRelatedPHIDs(array($user->getPHID())) ->executeWithCursorPager($pager); - $phids = array(); - foreach ($logs as $log) { - $phids[] = $log->getUserPHID(); - $phids[] = $log->getActorPHID(); - } - - if ($phids) { - $handles = id(new PhabricatorHandleQuery()) - ->setViewer($viewer) - ->withPHIDs($phids) - ->execute(); - } else { - $handles = array(); - } - $table = id(new PhabricatorUserLogView()) ->setUser($viewer) - ->setLogs($logs) - ->setHandles($handles); + ->setLogs($logs); $panel = $this->newBox(pht('Account Activity Logs'), $table); diff --git a/src/applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php b/src/applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php index 107816f2eb..bdc3c994b3 100644 --- a/src/applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php @@ -14,7 +14,7 @@ final class PhabricatorEmailFormatSettingsPanel } public function isUserPanel() { - return PhabricatorMetaMTAMail::shouldMultiplexAllMail(); + return PhabricatorMetaMTAMail::shouldMailEachRecipient(); } public function isManagementPanel() { diff --git a/src/applications/settings/setting/PhabricatorEmailStampsSetting.php b/src/applications/settings/setting/PhabricatorEmailStampsSetting.php new file mode 100644 index 0000000000..39403f40a0 --- /dev/null +++ b/src/applications/settings/setting/PhabricatorEmailStampsSetting.php @@ -0,0 +1,47 @@ + pht('Mail Headers'), + self::VALUE_BODY_STAMPS => pht('Mail Headers and Body'), + ); + } + +} diff --git a/src/applications/slowvote/editor/PhabricatorSlowvoteEditor.php b/src/applications/slowvote/editor/PhabricatorSlowvoteEditor.php index 38dbfb12d6..cf088f37d4 100644 --- a/src/applications/slowvote/editor/PhabricatorSlowvoteEditor.php +++ b/src/applications/slowvote/editor/PhabricatorSlowvoteEditor.php @@ -48,8 +48,7 @@ final class PhabricatorSlowvoteEditor $name = $object->getQuestion(); return id(new PhabricatorMetaMTAMail()) - ->setSubject("{$monogram}: {$name}") - ->addHeader('Thread-Topic', $monogram); + ->setSubject("{$monogram}: {$name}"); } protected function buildMailBody( diff --git a/src/applications/spaces/engineextension/PhabricatorSpacesMailEngineExtension.php b/src/applications/spaces/engineextension/PhabricatorSpacesMailEngineExtension.php new file mode 100644 index 0000000000..7ddbda05fe --- /dev/null +++ b/src/applications/spaces/engineextension/PhabricatorSpacesMailEngineExtension.php @@ -0,0 +1,35 @@ +setKey('space') + ->setLabel(pht('Space')), + ); + } + + public function newMailStamps($object, array $xactions) { + $editor = $this->getEditor(); + $viewer = $this->getViewer(); + + if (!PhabricatorSpacesNamespaceQuery::getSpacesExist()) { + return; + } + + $space_phid = PhabricatorSpacesNamespaceQuery::getObjectSpacePHID( + $object); + + $this->getMailStamp('space') + ->setValue($space_phid); + } + +} diff --git a/src/applications/spaces/phid/PhabricatorSpacesNamespacePHIDType.php b/src/applications/spaces/phid/PhabricatorSpacesNamespacePHIDType.php index 86371d6420..1399e71c8e 100644 --- a/src/applications/spaces/phid/PhabricatorSpacesNamespacePHIDType.php +++ b/src/applications/spaces/phid/PhabricatorSpacesNamespacePHIDType.php @@ -36,9 +36,11 @@ final class PhabricatorSpacesNamespacePHIDType $monogram = $namespace->getMonogram(); $name = $namespace->getNamespaceName(); - $handle->setName($name); - $handle->setFullName(pht('%s %s', $monogram, $name)); - $handle->setURI('/'.$monogram); + $handle + ->setName($name) + ->setFullName(pht('%s %s', $monogram, $name)) + ->setURI('/'.$monogram) + ->setMailStampName($monogram); if ($namespace->getIsArchived()) { $handle->setStatus(PhabricatorObjectHandle::STATUS_CLOSED); diff --git a/src/applications/subscriptions/application/PhabricatorSubscriptionsApplication.php b/src/applications/subscriptions/application/PhabricatorSubscriptionsApplication.php index 56759f5dc3..2de2994a92 100644 --- a/src/applications/subscriptions/application/PhabricatorSubscriptionsApplication.php +++ b/src/applications/subscriptions/application/PhabricatorSubscriptionsApplication.php @@ -24,7 +24,10 @@ final class PhabricatorSubscriptionsApplication extends PhabricatorApplication { return array( '/subscriptions/' => array( '(?Padd|delete)/'. - '(?P[^/]+)/' => 'PhabricatorSubscriptionsEditController', + '(?P[^/]+)/' => 'PhabricatorSubscriptionsEditController', + 'mute/' => array( + '(?P[^/]+)/' => 'PhabricatorSubscriptionsMuteController', + ), 'list/(?P[^/]+)/' => 'PhabricatorSubscriptionsListController', 'transaction/(?Padd|rem)/(?[^/]+)/' => 'PhabricatorSubscriptionsTransactionController', diff --git a/src/applications/subscriptions/controller/PhabricatorSubscriptionsMuteController.php b/src/applications/subscriptions/controller/PhabricatorSubscriptionsMuteController.php new file mode 100644 index 0000000000..1369643ffc --- /dev/null +++ b/src/applications/subscriptions/controller/PhabricatorSubscriptionsMuteController.php @@ -0,0 +1,92 @@ +getViewer(); + $phid = $request->getURIData('phid'); + + $handle = id(new PhabricatorHandleQuery()) + ->setViewer($viewer) + ->withPHIDs(array($phid)) + ->executeOne(); + + $object = id(new PhabricatorObjectQuery()) + ->setViewer($viewer) + ->withPHIDs(array($phid)) + ->executeOne(); + + if (!($object instanceof PhabricatorSubscribableInterface)) { + return new Aphront400Response(); + } + + $muted_type = PhabricatorMutedByEdgeType::EDGECONST; + + $edge_query = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs(array($object->getPHID())) + ->withEdgeTypes(array($muted_type)) + ->withDestinationPHIDs(array($viewer->getPHID())); + + $edge_query->execute(); + + $is_mute = !$edge_query->getDestinationPHIDs(); + $object_uri = $handle->getURI(); + + if ($request->isFormPost()) { + if ($is_mute) { + $xaction_value = array( + '+' => array_fuse(array($viewer->getPHID())), + ); + } else { + $xaction_value = array( + '-' => array_fuse(array($viewer->getPHID())), + ); + } + + $muted_type = PhabricatorMutedByEdgeType::EDGECONST; + + $xaction = id($object->getApplicationTransactionTemplate()) + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue('edge:type', $muted_type) + ->setNewValue($xaction_value); + + $editor = id($object->getApplicationTransactionEditor()) + ->setActor($viewer) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->setContentSourceFromRequest($request); + + $editor->applyTransactions( + $object->getApplicationTransactionObject(), + array($xaction)); + + return id(new AphrontReloadResponse())->setURI($object_uri); + } + + $dialog = $this->newDialog() + ->addCancelButton($object_uri); + + if ($is_mute) { + $dialog + ->setTitle(pht('Mute Notifications')) + ->appendParagraph( + pht( + 'Mute this object? You will no longer receive notifications or '. + 'email about it.')) + ->addSubmitButton(pht('Mute')); + } else { + $dialog + ->setTitle(pht('Unmute Notifications')) + ->appendParagraph( + pht( + 'Unmute this object? You will receive notifications and email '. + 'again.')) + ->addSubmitButton(pht('Unmute')); + } + + return $dialog; + } + + +} diff --git a/src/applications/subscriptions/engineextension/PhabricatorSubscriptionsMailEngineExtension.php b/src/applications/subscriptions/engineextension/PhabricatorSubscriptionsMailEngineExtension.php new file mode 100644 index 0000000000..122fad4b0d --- /dev/null +++ b/src/applications/subscriptions/engineextension/PhabricatorSubscriptionsMailEngineExtension.php @@ -0,0 +1,32 @@ +setKey('subscriber') + ->setLabel(pht('Subscriber')), + ); + } + + public function newMailStamps($object, array $xactions) { + $editor = $this->getEditor(); + $viewer = $this->getViewer(); + + $subscriber_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( + $object->getPHID(), + PhabricatorObjectHasSubscriberEdgeType::EDGECONST); + + $this->getMailStamp('subscriber') + ->setValue($subscriber_phids); + } + +} diff --git a/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php b/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php index 5f371d69f3..caf860117e 100644 --- a/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php +++ b/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php @@ -42,6 +42,28 @@ final class PhabricatorSubscriptionsUIEventListener return; } + $src_phid = $object->getPHID(); + $subscribed_type = PhabricatorObjectHasSubscriberEdgeType::EDGECONST; + $muted_type = PhabricatorMutedByEdgeType::EDGECONST; + + $edges = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs(array($src_phid)) + ->withEdgeTypes( + array( + $subscribed_type, + $muted_type, + )) + ->withDestinationPHIDs(array($user_phid)) + ->execute(); + + if ($user_phid) { + $is_subscribed = isset($edges[$src_phid][$subscribed_type][$user_phid]); + $is_muted = isset($edges[$src_phid][$muted_type][$user_phid]); + } else { + $is_subscribed = false; + $is_muted = false; + } + if ($user_phid && $object->isAutomaticallySubscribed($user_phid)) { $sub_action = id(new PhabricatorActionView()) ->setWorkflow(true) @@ -51,22 +73,9 @@ final class PhabricatorSubscriptionsUIEventListener ->setName(pht('Automatically Subscribed')) ->setIcon('fa-check-circle lightgreytext'); } else { - $subscribed = false; - if ($user->isLoggedIn()) { - $src_phid = $object->getPHID(); - $edge_type = PhabricatorObjectHasSubscriberEdgeType::EDGECONST; - - $edges = id(new PhabricatorEdgeQuery()) - ->withSourcePHIDs(array($src_phid)) - ->withEdgeTypes(array($edge_type)) - ->withDestinationPHIDs(array($user_phid)) - ->execute(); - $subscribed = isset($edges[$src_phid][$edge_type][$user_phid]); - } - $can_interact = PhabricatorPolicyFilter::canInteract($user, $object); - if ($subscribed) { + if ($is_subscribed) { $sub_action = id(new PhabricatorActionView()) ->setWorkflow(true) ->setRenderAsForm(true) @@ -89,8 +98,26 @@ final class PhabricatorSubscriptionsUIEventListener } } + $mute_action = id(new PhabricatorActionView()) + ->setWorkflow(true) + ->setHref('/subscriptions/mute/'.$object->getPHID().'/') + ->setDisabled(!$user_phid); + + if (!$is_muted) { + $mute_action + ->setName(pht('Mute Notifications')) + ->setIcon('fa-volume-up'); + } else { + $mute_action + ->setName(pht('Unmute Notifications')) + ->setIcon('fa-volume-off') + ->setColor(PhabricatorActionView::RED); + } + + $actions = $event->getValue('actions'); $actions[] = $sub_action; + $actions[] = $mute_action; $event->setValue('actions', $actions); } diff --git a/src/applications/transactions/bulk/management/PhabricatorBulkManagementExportWorkflow.php b/src/applications/transactions/bulk/management/PhabricatorBulkManagementExportWorkflow.php new file mode 100644 index 0000000000..cd0ed346fc --- /dev/null +++ b/src/applications/transactions/bulk/management/PhabricatorBulkManagementExportWorkflow.php @@ -0,0 +1,168 @@ +setName('export') + ->setExamples('**export** [options]') + ->setSynopsis( + pht('Export data to a flat file (JSON, CSV, Excel, etc).')) + ->setArguments( + array( + array( + 'name' => 'class', + 'param' => 'class', + 'help' => pht( + 'SearchEngine class to export data from.'), + ), + array( + 'name' => 'format', + 'param' => 'format', + 'help' => pht('Export format.'), + ), + array( + 'name' => 'query', + 'param' => 'key', + 'help' => pht( + 'Export the data selected by this query.'), + ), + array( + 'name' => 'output', + 'param' => 'path', + 'help' => pht( + 'Write output to a file. If omitted, output will be sent to '. + 'stdout.'), + ), + array( + 'name' => 'overwrite', + 'help' => pht( + 'If the output file already exists, overwrite it instead of '. + 'raising an error.'), + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $viewer = $this->getViewer(); + + $class = $args->getArg('class'); + + if (!strlen($class)) { + throw new PhutilArgumentUsageException( + pht( + 'Specify a search engine class to export data from with '. + '"--class".')); + } + + if (!is_subclass_of($class, 'PhabricatorApplicationSearchEngine')) { + throw new PhutilArgumentUsageException( + pht( + 'SearchEngine class ("%s") is unknown.', + $class)); + } + + $engine = newv($class, array()) + ->setViewer($viewer); + + if (!$engine->canExport()) { + throw new PhutilArgumentUsageException( + pht( + 'SearchEngine class ("%s") does not support data export.', + $class)); + } + + $query_key = $args->getArg('query'); + if (!strlen($query_key)) { + throw new PhutilArgumentUsageException( + pht( + 'Specify a query to export with "--query".')); + } + + if ($engine->isBuiltinQuery($query_key)) { + $saved_query = $engine->buildSavedQueryFromBuiltin($query_key); + } else if ($query_key) { + $saved_query = id(new PhabricatorSavedQueryQuery()) + ->setViewer($viewer) + ->withQueryKeys(array($query_key)) + ->executeOne(); + } else { + $saved_query = null; + } + + if (!$saved_query) { + throw new PhutilArgumentUsageException( + pht( + 'Failed to load saved query ("%s").', + $query_key)); + } + + $format_key = $args->getArg('format'); + if (!strlen($format_key)) { + throw new PhutilArgumentUsageException( + pht( + 'Specify an export format with "--format".')); + } + + $all_formats = PhabricatorExportFormat::getAllExportFormats(); + $format = idx($all_formats, $format_key); + if (!$format) { + throw new PhutilArgumentUsageException( + pht( + 'Unknown export format ("%s"). Known formats are: %s.', + $format_key, + implode(', ', array_keys($all_formats)))); + } + + if (!$format->isExportFormatEnabled()) { + throw new PhutilArgumentUsageException( + pht( + 'Export format ("%s") is not enabled.', + $format_key)); + } + + $is_overwrite = $args->getArg('overwrite'); + $output_path = $args->getArg('output'); + + if (!strlen($output_path) && $is_overwrite) { + throw new PhutilArgumentUsageException( + pht( + 'Flag "--overwrite" has no effect without "--output".')); + } + + if (!$is_overwrite) { + if (Filesystem::pathExists($output_path)) { + throw new PhutilArgumentUsageException( + pht( + 'Output path already exists. Use "--overwrite" to overwrite '. + 'it.')); + } + } + + $export_engine = id(new PhabricatorExportEngine()) + ->setViewer($viewer) + ->setTitle(pht('Export')) + ->setFilename(pht('export')) + ->setSearchEngine($engine) + ->setSavedQuery($saved_query) + ->setExportFormat($format); + + $file = $export_engine->exportFile(); + + $iterator = $file->getFileDataIterator(); + + if (strlen($output_path)) { + foreach ($iterator as $chunk) { + Filesystem::appendFile($output_path, $chunk); + } + } else { + foreach ($iterator as $chunk) { + echo $chunk; + } + } + + return 0; + } + +} diff --git a/src/applications/transactions/edges/PhabricatorMutedByEdgeType.php b/src/applications/transactions/edges/PhabricatorMutedByEdgeType.php new file mode 100644 index 0000000000..1f592239ba --- /dev/null +++ b/src/applications/transactions/edges/PhabricatorMutedByEdgeType.php @@ -0,0 +1,16 @@ +useEdgeTransactions = $use_edge_transactions; @@ -30,13 +31,23 @@ abstract class PhabricatorPHIDListEditField return $this->isSingleValue; } + public function setIsNullable($is_nullable) { + $this->isNullable = $is_nullable; + return $this; + } + + public function getIsNullable() { + return $this->isNullable; + } + protected function newHTTPParameterType() { return new AphrontPHIDListHTTPParameterType(); } protected function newConduitParameterType() { if ($this->getIsSingleValue()) { - return new ConduitPHIDParameterType(); + return id(new ConduitPHIDParameterType()) + ->setIsNullable($this->getIsNullable()); } else { return new ConduitPHIDListParameterType(); } @@ -99,7 +110,8 @@ abstract class PhabricatorPHIDListEditField } return id(new PhabricatorDatasourceEditType()) - ->setIsSingleValue($this->getIsSingleValue()); + ->setIsSingleValue($this->getIsSingleValue()) + ->setIsNullable($this->getIsNullable()); } protected function newBulkEditTypes() { diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index 2b86dcabd9..fad7ca1d91 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -71,6 +71,14 @@ abstract class PhabricatorApplicationTransactionEditor private $mailShouldSend = false; private $modularTypes; private $silent; + private $mustEncrypt; + private $stampTemplates = array(); + private $mailStamps = array(); + private $oldTo = array(); + private $oldCC = array(); + private $mailRemovedPHIDs = array(); + private $mailUnexpandablePHIDs = array(); + private $mailMutedPHIDs = array(); private $transactionQueue = array(); @@ -176,7 +184,7 @@ abstract class PhabricatorApplicationTransactionEditor return $this->isNewObject; } - protected function getMentionedPHIDs() { + public function getMentionedPHIDs() { return $this->mentionedPHIDs; } @@ -198,6 +206,29 @@ abstract class PhabricatorApplicationTransactionEditor return $this->silent; } + public function getMustEncrypt() { + return $this->mustEncrypt; + } + + public function getHeraldRuleMonograms() { + // Convert the stored "<123>, <456>" string into a list: "H123", "H456". + $list = $this->heraldHeader; + $list = preg_split('/[, ]+/', $list); + + foreach ($list as $key => $item) { + $item = trim($item, '<>'); + + if (!is_numeric($item)) { + unset($list[$key]); + continue; + } + + $list[$key] = 'H'.$item; + } + + return $list; + } + public function setIsInverseEdgeEditor($is_inverse_edge_editor) { $this->isInverseEdgeEditor = $is_inverse_edge_editor; return $this; @@ -890,6 +921,8 @@ abstract class PhabricatorApplicationTransactionEditor $this->willApplyTransactions($object, $xactions); if ($object->getID()) { + $this->buildOldRecipientLists($object, $xactions); + foreach ($xactions as $xaction) { // If any of the transactions require a read lock, hold one and @@ -999,7 +1032,31 @@ abstract class PhabricatorApplicationTransactionEditor $xaction->setPHID($xaction->generatePHID()); $comment_editor->applyEdit($xaction, $xaction->getComment()); } else { - $xaction->save(); + + // TODO: This is a transitional hack to let us migrate edge + // transactions to a more efficient storage format. For now, we're + // going to write a new slim format to the database but keep the old + // bulky format on the objects so we don't have to upgrade all the + // edit logic to the new format yet. See T13051. + + $edge_type = PhabricatorTransactions::TYPE_EDGE; + if ($xaction->getTransactionType() == $edge_type) { + $bulky_old = $xaction->getOldValue(); + $bulky_new = $xaction->getNewValue(); + + $record = PhabricatorEdgeChangeRecord::newFromTransaction($xaction); + $slim_old = $record->getModernOldEdgeTransactionData(); + $slim_new = $record->getModernNewEdgeTransactionData(); + + $xaction->setOldValue($slim_old); + $xaction->setNewValue($slim_new); + $xaction->save(); + + $xaction->setOldValue($bulky_old); + $xaction->setNewValue($bulky_new); + } else { + $xaction->save(); + } } } @@ -1045,13 +1102,6 @@ abstract class PhabricatorApplicationTransactionEditor // We are the Herald editor, so stop work here and return the updated // transactions. return $xactions; - } else if ($this->getIsInverseEdgeEditor()) { - // If we're applying inverse edge transactions, don't trigger Herald. - // From a product perspective, the current set of inverse edges (most - // often, mentions) aren't things users would expect to trigger Herald. - // From a technical perspective, objects loaded by the inverse editor may - // not have enough data to execute rules. At least for now, just stop - // Herald from executing when applying inverse edges. } else if ($this->shouldApplyHeraldRules($object, $xactions)) { // We are not the Herald editor, so try to apply Herald rules. $herald_xactions = $this->applyHeraldRules($object, $xactions); @@ -1156,6 +1206,25 @@ abstract class PhabricatorApplicationTransactionEditor $this->mailShouldSend = true; $this->mailToPHIDs = $this->getMailTo($object); $this->mailCCPHIDs = $this->getMailCC($object); + $this->mailUnexpandablePHIDs = $this->newMailUnexpandablePHIDs($object); + + // Add any recipients who were previously on the notification list + // but were removed by this change. + $this->applyOldRecipientLists(); + + if ($object instanceof PhabricatorSubscribableInterface) { + $this->mailMutedPHIDs = PhabricatorEdgeQuery::loadDestinationPHIDs( + $object->getPHID(), + PhabricatorMutedByEdgeType::EDGECONST); + } else { + $this->mailMutedPHIDs = array(); + } + + $mail_xactions = $this->getTransactionsForMail($object, $xactions); + $stamps = $this->newMailStamps($object, $xactions); + foreach ($stamps as $stamp) { + $this->mailStamps[] = $stamp->toDictionary(); + } } if ($this->shouldPublishFeedStory($object, $xactions)) { @@ -2507,7 +2576,13 @@ abstract class PhabricatorApplicationTransactionEditor $email_cc = $this->mailCCPHIDs; $email_cc = array_merge($email_cc, $this->heraldEmailPHIDs); + $unexpandable = $this->mailUnexpandablePHIDs; + if (!is_array($unexpandable)) { + $unexpandable = array(); + } + $targets = $this->buildReplyHandler($object) + ->setUnexpandablePHIDs($unexpandable) ->getMailTargets($email_to, $email_cc); // Set this explicitly before we start swapping out the effective actor. @@ -2528,6 +2603,14 @@ abstract class PhabricatorApplicationTransactionEditor $this->loadHandles($xactions); $mail = $this->buildMailForTarget($object, $xactions, $target); + + if ($mail) { + if ($this->mustEncrypt) { + $mail + ->setMustEncrypt(true) + ->setMustEncryptReasons($this->mustEncrypt); + } + } } catch (Exception $ex) { $caught = $ex; } @@ -2582,6 +2665,7 @@ abstract class PhabricatorApplicationTransactionEditor $mail_tags = $this->getMailTags($object, $mail_xactions); $action = $this->getMailAction($object, $mail_xactions); + $stamps = $this->generateMailStamps($object, $this->mailStamps); if (PhabricatorEnv::getEnvConfig('metamta.email-preferences')) { $this->addEmailPreferenceSectionToMailBody( @@ -2590,6 +2674,11 @@ abstract class PhabricatorApplicationTransactionEditor $mail_xactions); } + $muted_phids = $this->mailMutedPHIDs; + if (!is_array($muted_phids)) { + $muted_phids = array(); + } + $mail ->setSensitiveContent(false) ->setFrom($this->getActingAsPHID()) @@ -2598,6 +2687,7 @@ abstract class PhabricatorApplicationTransactionEditor ->setThreadID($this->getMailThreadID($object), $this->getIsNewObject()) ->setRelatedPHID($object->getPHID()) ->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs()) + ->setMutedPHIDs($muted_phids) ->setForceHeraldMailRecipientPHIDs($this->heraldForcedEmailPHIDs) ->setMailTags($mail_tags) ->setIsBulk(true) @@ -2620,6 +2710,18 @@ abstract class PhabricatorApplicationTransactionEditor $mail->setParentMessageID($this->getParentMessageID()); } + // If we have stamps, attach the raw dictionary version (not the actual + // objects) to the mail so that debugging tools can see what we used to + // render the final list. + if ($this->mailStamps) { + $mail->setMailStampMetadata($this->mailStamps); + } + + // If we have rendered stamps, attach them to the mail. + if ($stamps) { + $mail->setMailStamps($stamps); + } + return $target->willSendMail($mail); } @@ -2741,6 +2843,11 @@ abstract class PhabricatorApplicationTransactionEditor } + protected function newMailUnexpandablePHIDs(PhabricatorLiskDAO $object) { + return array(); + } + + /** * @task mail */ @@ -3097,6 +3204,18 @@ abstract class PhabricatorApplicationTransactionEditor $related_phids = $this->feedRelatedPHIDs; $subscribed_phids = $this->feedNotifyPHIDs; + // Remove muted users from the subscription list so they don't get + // notifications, either. + $muted_phids = $this->mailMutedPHIDs; + if (!is_array($muted_phids)) { + $muted_phids = array(); + } + $subscribed_phids = array_fuse($subscribed_phids); + foreach ($muted_phids as $muted_phid) { + unset($subscribed_phids[$muted_phid]); + } + $subscribed_phids = array_values($subscribed_phids); + $story_type = $this->getFeedStoryType(); $story_data = $this->getFeedStoryData($object, $xactions); @@ -3165,6 +3284,7 @@ abstract class PhabricatorApplicationTransactionEditor $adapter = $this->buildHeraldAdapter($object, $xactions) ->setContentSource($this->getContentSource()) ->setIsNewObject($this->getIsNewObject()) + ->setActingAsPHID($this->getActingAsPHID()) ->setAppliedTransactions($xactions); if ($this->getApplicationEmail()) { @@ -3193,6 +3313,8 @@ abstract class PhabricatorApplicationTransactionEditor $adapter->getQueuedHarbormasterBuildRequests()); } + $this->mustEncrypt = $adapter->getMustEncryptReasons(); + return array_merge( $this->didApplyHeraldRules($object, $adapter, $xscript), $adapter->getQueuedTransactions()); @@ -3537,6 +3659,10 @@ abstract class PhabricatorApplicationTransactionEditor 'feedRelatedPHIDs', 'feedShouldPublish', 'mailShouldSend', + 'mustEncrypt', + 'mailStamps', + 'mailUnexpandablePHIDs', + 'mailMutedPHIDs', ); } @@ -3929,4 +4055,192 @@ abstract class PhabricatorApplicationTransactionEditor return $editor; } + +/* -( Stamps )------------------------------------------------------------- */ + + + public function newMailStampTemplates($object) { + $actor = $this->getActor(); + + $templates = array(); + + $extensions = $this->newMailExtensions($object); + foreach ($extensions as $extension) { + $stamps = $extension->newMailStampTemplates($object); + foreach ($stamps as $stamp) { + $key = $stamp->getKey(); + if (isset($templates[$key])) { + throw new Exception( + pht( + 'Mail extension ("%s") defines a stamp template with the '. + 'same key ("%s") as another template. Each stamp template '. + 'must have a unique key.', + get_class($extension), + $key)); + } + + $stamp->setViewer($actor); + + $templates[$key] = $stamp; + } + } + + return $templates; + } + + final public function getMailStamp($key) { + if (!isset($this->stampTemplates)) { + throw new PhutilInvalidStateException('newMailStampTemplates'); + } + + if (!isset($this->stampTemplates[$key])) { + throw new Exception( + pht( + 'Editor ("%s") has no mail stamp template with provided key ("%s").', + get_class($this), + $key)); + } + + return $this->stampTemplates[$key]; + } + + private function newMailStamps($object, array $xactions) { + $actor = $this->getActor(); + + $this->stampTemplates = $this->newMailStampTemplates($object); + + $extensions = $this->newMailExtensions($object); + $stamps = array(); + foreach ($extensions as $extension) { + $extension->newMailStamps($object, $xactions); + } + + return $this->stampTemplates; + } + + private function newMailExtensions($object) { + $actor = $this->getActor(); + + $all_extensions = PhabricatorMailEngineExtension::getAllExtensions(); + + $extensions = array(); + foreach ($all_extensions as $key => $template) { + $extension = id(clone $template) + ->setViewer($actor) + ->setEditor($this); + + if ($extension->supportsObject($object)) { + $extensions[$key] = $extension; + } + } + + return $extensions; + } + + private function generateMailStamps($object, $data) { + if (!$data || !is_array($data)) { + return null; + } + + $templates = $this->newMailStampTemplates($object); + foreach ($data as $spec) { + if (!is_array($spec)) { + continue; + } + + $key = idx($spec, 'key'); + if (!isset($templates[$key])) { + continue; + } + + $type = idx($spec, 'type'); + if ($templates[$key]->getStampType() !== $type) { + continue; + } + + $value = idx($spec, 'value'); + $templates[$key]->setValueFromDictionary($value); + } + + $results = array(); + foreach ($templates as $template) { + $value = $template->getValueForRendering(); + + $rendered = $template->renderStamps($value); + if ($rendered === null) { + continue; + } + + $rendered = (array)$rendered; + foreach ($rendered as $stamp) { + $results[] = $stamp; + } + } + + natcasesort($results); + + return $results; + } + + public function getRemovedRecipientPHIDs() { + return $this->mailRemovedPHIDs; + } + + private function buildOldRecipientLists($object, $xactions) { + // See T4776. Before we start making any changes, build a list of the old + // recipients. If a change removes a user from the recipient list for an + // object we still want to notify the user about that change. This allows + // them to respond if they didn't want to be removed. + + if (!$this->shouldSendMail($object, $xactions)) { + return; + } + + $this->oldTo = $this->getMailTo($object); + $this->oldCC = $this->getMailCC($object); + + return $this; + } + + private function applyOldRecipientLists() { + $actor_phid = $this->getActingAsPHID(); + + // If you took yourself off the recipient list (for example, by + // unsubscribing or resigning) assume that you know what you did and + // don't need to be notified. + + // If you just moved from "To" to "Cc" (or vice versa), you're still a + // recipient so we don't need to add you back in. + + $map = array_fuse($this->mailToPHIDs) + array_fuse($this->mailCCPHIDs); + + foreach ($this->oldTo as $phid) { + if ($phid === $actor_phid) { + continue; + } + + if (isset($map[$phid])) { + continue; + } + + $this->mailToPHIDs[] = $phid; + $this->mailRemovedPHIDs[] = $phid; + } + + foreach ($this->oldCC as $phid) { + if ($phid === $actor_phid) { + continue; + } + + if (isset($map[$phid])) { + continue; + } + + $this->mailCCPHIDs[] = $phid; + $this->mailRemovedPHIDs[] = $phid; + } + + return $this; + } + } diff --git a/src/applications/transactions/edittype/PhabricatorPHIDListEditType.php b/src/applications/transactions/edittype/PhabricatorPHIDListEditType.php index eb728a6808..763f6ff001 100644 --- a/src/applications/transactions/edittype/PhabricatorPHIDListEditType.php +++ b/src/applications/transactions/edittype/PhabricatorPHIDListEditType.php @@ -6,6 +6,7 @@ abstract class PhabricatorPHIDListEditType private $datasource; private $isSingleValue; private $defaultValue; + private $isNullable; public function setDatasource(PhabricatorTypeaheadDatasource $datasource) { $this->datasource = $datasource; @@ -30,16 +31,17 @@ abstract class PhabricatorPHIDListEditType return $this; } - public function getDefaultValue() { - return $this->defaultValue; + public function setIsNullable($is_nullable) { + $this->isNullable = $is_nullable; + return $this; } - public function getValueType() { - if ($this->getIsSingleValue()) { - return 'phid'; - } else { - return 'list'; - } + public function getIsNullable() { + return $this->isNullable; + } + + public function getDefaultValue() { + return $this->defaultValue; } protected function newConduitParameterType() { @@ -49,10 +51,23 @@ abstract class PhabricatorPHIDListEditType } if ($this->getIsSingleValue()) { - return new ConduitPHIDParameterType(); + return id(new ConduitPHIDParameterType()) + ->setIsNullable($this->getIsNullable()); } else { return new ConduitPHIDListParameterType(); } } + public function getTransactionValueFromBulkEdit($value) { + if (!$this->getIsSingleValue()) { + return $value; + } + + if ($value) { + return head($value); + } + + return null; + } + } diff --git a/src/applications/transactions/engineextension/PhabricatorApplicationObjectMailEngineExtension.php b/src/applications/transactions/engineextension/PhabricatorApplicationObjectMailEngineExtension.php new file mode 100644 index 0000000000..bf441df71c --- /dev/null +++ b/src/applications/transactions/engineextension/PhabricatorApplicationObjectMailEngineExtension.php @@ -0,0 +1,92 @@ +setKey('application') + ->setLabel(pht('Application')), + ); + + if ($this->hasMonogram($object)) { + $templates[] = id(new PhabricatorStringMailStamp()) + ->setKey('monogram') + ->setLabel(pht('Object Monogram')); + } + + if ($this->hasPHID($object)) { + // This is a PHID, but we always want to render it as a raw string, so + // use a string mail stamp. + $templates[] = id(new PhabricatorStringMailStamp()) + ->setKey('phid') + ->setLabel(pht('Object PHID')); + + $templates[] = id(new PhabricatorStringMailStamp()) + ->setKey('object-type') + ->setLabel(pht('Object Type')); + } + + return $templates; + } + + public function newMailStamps($object, array $xactions) { + $editor = $this->getEditor(); + $viewer = $this->getViewer(); + + $application = null; + $class = $editor->getEditorApplicationClass(); + if (PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) { + $application = newv($class, array()); + } + + if ($application) { + $application_name = $application->getName(); + $this->getMailStamp('application') + ->setValue($application_name); + } + + if ($this->hasMonogram($object)) { + $monogram = $object->getMonogram(); + $this->getMailStamp('monogram') + ->setValue($monogram); + } + + if ($this->hasPHID($object)) { + $object_phid = $object->getPHID(); + + $this->getMailStamp('phid') + ->setValue($object_phid); + + $phid_type = phid_get_type($object_phid); + if ($phid_type != PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) { + $this->getMailStamp('object-type') + ->setValue($phid_type); + } + } + } + + private function hasPHID($object) { + if (!($object instanceof LiskDAO)) { + return false; + } + + if (!$object->getConfigOption(LiskDAO::CONFIG_AUX_PHID)) { + return false; + } + + return true; + } + + private function hasMonogram($object) { + return method_exists($object, 'getMonogram'); + } + +} diff --git a/src/applications/transactions/engineextension/PhabricatorEditorMailEngineExtension.php b/src/applications/transactions/engineextension/PhabricatorEditorMailEngineExtension.php new file mode 100644 index 0000000000..5365894429 --- /dev/null +++ b/src/applications/transactions/engineextension/PhabricatorEditorMailEngineExtension.php @@ -0,0 +1,81 @@ +setKey('actor') + ->setLabel(pht('Acting User')); + + $templates[] = id(new PhabricatorStringMailStamp()) + ->setKey('via') + ->setLabel(pht('Via Content Source')); + + $templates[] = id(new PhabricatorBoolMailStamp()) + ->setKey('silent') + ->setLabel(pht('Silent Edit')); + + $templates[] = id(new PhabricatorBoolMailStamp()) + ->setKey('encrypted') + ->setLabel(pht('Encryption Required')); + + $templates[] = id(new PhabricatorBoolMailStamp()) + ->setKey('new') + ->setLabel(pht('New Object')); + + $templates[] = id(new PhabricatorPHIDMailStamp()) + ->setKey('mention') + ->setLabel(pht('Mentioned User')); + + $templates[] = id(new PhabricatorStringMailStamp()) + ->setKey('herald') + ->setLabel(pht('Herald Rule')); + + $templates[] = id(new PhabricatorPHIDMailStamp()) + ->setKey('removed') + ->setLabel(pht('Recipient Removed')); + + return $templates; + } + + public function newMailStamps($object, array $xactions) { + $editor = $this->getEditor(); + $viewer = $this->getViewer(); + + $this->getMailStamp('actor') + ->setValue($editor->getActingAsPHID()); + + $content_source = $editor->getContentSource(); + $this->getMailStamp('via') + ->setValue($content_source->getSourceTypeConstant()); + + $this->getMailStamp('silent') + ->setValue($editor->getIsSilent()); + + $this->getMailStamp('encrypted') + ->setValue($editor->getMustEncrypt()); + + $this->getMailStamp('new') + ->setValue($editor->getIsNewObject()); + + $mentioned_phids = $editor->getMentionedPHIDs(); + $this->getMailStamp('mention') + ->setValue($mentioned_phids); + + $this->getMailStamp('herald') + ->setValue($editor->getHeraldRuleMonograms()); + + $this->getMailStamp('removed') + ->setValue($editor->getRemovedRecipientPHIDs()); + } + +} diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index d49c3bf675..d27370cd44 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -302,8 +302,8 @@ abstract class PhabricatorApplicationTransaction $phids[] = $new; break; case PhabricatorTransactions::TYPE_EDGE: - $phids[] = ipull($old, 'dst'); - $phids[] = ipull($new, 'dst'); + $record = PhabricatorEdgeChangeRecord::newFromTransaction($this); + $phids[] = $record->getChangedPHIDs(); break; case PhabricatorTransactions::TYPE_COLUMNS: foreach ($new as $move) { @@ -458,6 +458,12 @@ abstract class PhabricatorApplicationTransaction case PhabricatorTransactions::TYPE_JOIN_POLICY: return 'fa-lock'; case PhabricatorTransactions::TYPE_EDGE: + switch ($this->getMetadataValue('edge:type')) { + case DiffusionCommitRevertedByCommitEdgeType::EDGECONST: + return 'fa-undo'; + case DiffusionCommitRevertsCommitEdgeType::EDGECONST: + return 'fa-ambulance'; + } return 'fa-link'; case PhabricatorTransactions::TYPE_BUILDABLE: return 'fa-wrench'; @@ -496,6 +502,14 @@ abstract class PhabricatorApplicationTransaction return 'black'; } break; + case PhabricatorTransactions::TYPE_EDGE: + switch ($this->getMetadataValue('edge:type')) { + case DiffusionCommitRevertedByCommitEdgeType::EDGECONST: + return 'pink'; + case DiffusionCommitRevertsCommitEdgeType::EDGECONST: + return 'sky'; + } + break; case PhabricatorTransactions::TYPE_BUILDABLE: switch ($this->getNewValue()) { case HarbormasterBuildable::STATUS_PASSED: @@ -629,12 +643,13 @@ abstract class PhabricatorApplicationTransaction case PhabricatorObjectMentionsObjectEdgeType::EDGECONST: case ManiphestTaskHasDuplicateTaskEdgeType::EDGECONST: case ManiphestTaskIsDuplicateOfTaskEdgeType::EDGECONST: + case PhabricatorMutedEdgeType::EDGECONST: + case PhabricatorMutedByEdgeType::EDGECONST: return true; break; case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST: - $new = ipull($this->getNewValue(), 'dst'); - $old = ipull($this->getOldValue(), 'dst'); - $add = array_diff($new, $old); + $record = PhabricatorEdgeChangeRecord::newFromTransaction($this); + $add = $record->getAddedPHIDs(); $add_value = reset($add); $add_handle = $this->getHandle($add_value); if ($add_handle->getPolicyFiltered()) { @@ -933,10 +948,10 @@ abstract class PhabricatorApplicationTransaction } break; case PhabricatorTransactions::TYPE_EDGE: - $new = ipull($new, 'dst'); - $old = ipull($old, 'dst'); - $add = array_diff($new, $old); - $rem = array_diff($old, $new); + $record = PhabricatorEdgeChangeRecord::newFromTransaction($this); + $add = $record->getAddedPHIDs(); + $rem = $record->getRemovedPHIDs(); + $type = $this->getMetadata('edge:type'); $type = head($type); @@ -1172,10 +1187,10 @@ abstract class PhabricatorApplicationTransaction $this->renderHandleLink($new)); } case PhabricatorTransactions::TYPE_EDGE: - $new = ipull($new, 'dst'); - $old = ipull($old, 'dst'); - $add = array_diff($new, $old); - $rem = array_diff($old, $new); + $record = PhabricatorEdgeChangeRecord::newFromTransaction($this); + $add = $record->getAddedPHIDs(); + $rem = $record->getRemovedPHIDs(); + $type = $this->getMetadata('edge:type'); $type = head($type); diff --git a/src/docs/user/configuration/configuration_locked.diviner b/src/docs/user/configuration/configuration_locked.diviner index 040b838177..fff0da9bdc 100644 --- a/src/docs/user/configuration/configuration_locked.diviner +++ b/src/docs/user/configuration/configuration_locked.diviner @@ -27,6 +27,24 @@ can edit it from the CLI instead, with `bin/config`: phabricator/ $ ./bin/config set ``` +Some configuration options take complicated values which can be difficult +to escape properly for the shell. The easiest way to set these options is +to use the `--stdin` flag. First, put your desired value in a `config.json` +file: + +```name=config.json, lang=json +{ + "duck": "quack", + "cow": "moo" +} +``` + +Then, set it with `--stdin` like this: + +``` +phabricator/ $ ./bin/config set --stdin < config.json +``` + A few settings have alternate CLI tools. Refer to the setting page for details. @@ -98,4 +116,6 @@ Next Steps Continue by: + - learning more about advanced options with + @{Configuration User Guide: Advanced Configuration}; or - returning to the @{article: Configuration Guide}. diff --git a/src/docs/user/configuration/configuring_inbound_email.diviner b/src/docs/user/configuration/configuring_inbound_email.diviner index 5b47a17831..f4f367d57e 100644 --- a/src/docs/user/configuration/configuring_inbound_email.diviner +++ b/src/docs/user/configuration/configuring_inbound_email.diviner @@ -14,6 +14,7 @@ There are a few approaches available: | Receive Mail With | Setup | Cost | Notes | |--------|-------|------|-------| | Mailgun | Easy | Cheap | Recommended | +| Postmark | Easy | Cheap | Recommended | | SendGrid | Easy | Cheap | | | Local MTA | Extremely Difficult | Free | Strongly discouraged! | @@ -130,6 +131,21 @@ like this: example domain with your actual domain. - Set the `mailgun.api-key` config key to your Mailgun API key. +Postmark Setup +============== + +To process inbound mail from Postmark, configure this URI as your inbound +webhook URI in the Postmark control panel: + +``` +https:///mail/postmark/ +``` + +See also the Postmark section in @{article:Configuring Outbound Email} for +discussion of the remote address whitelist used to verify that requests this +endpoint receives are authentic requests originating from Postmark. + + = SendGrid Setup = To use SendGrid, you need a SendGrid account with access to the "Parse API" for diff --git a/src/docs/user/configuration/configuring_outbound_email.diviner b/src/docs/user/configuration/configuring_outbound_email.diviner index 2a95f49bc3..37a344c275 100644 --- a/src/docs/user/configuration/configuring_outbound_email.diviner +++ b/src/docs/user/configuration/configuring_outbound_email.diviner @@ -3,43 +3,41 @@ Instructions for configuring Phabricator to send mail. -= Overview = +Overview +======== -Phabricator can send outbound email via several different providers, called -"Adapters". +Phabricator can send outbound email through several different mail services, +including a local mailer or various third-party services. Options include: | Send Mail With | Setup | Cost | Inbound | Notes | |---------|-------|------|---------|-------| | Mailgun | Easy | Cheap | Yes | Recommended | +| Postmark | Easy | Cheap | Yes | Recommended | | Amazon SES | Easy | Cheap | No | Recommended | -| SendGrid | Medium | Cheap | Yes | Discouraged (See Note) | +| SendGrid | Medium | Cheap | Yes | Discouraged | | External SMTP | Medium | Varies | No | Gmail, etc. | -| Local SMTP | Hard | Free | No | (Default) sendmail, postfix, etc | -| Custom | Hard | Free | No | Write an adapter for some other service. | +| Local SMTP | Hard | Free | No | sendmail, postfix, etc | +| Custom | Hard | Free | No | Write a custom mailer for some other service. | | Drop in a Hole | Easy | Free | No | Drops mail in a deep, dark hole. | -Of these options, sending mail via local SMTP is the default, but usually -requires some configuration to get working. See below for details on how to -select and configure a delivery method. +See below for details on how to select and configure mail delivery for each +mailer. Overall, Mailgun and SES are much easier to set up, and using one of them is recommended. In particular, Mailgun will also let you set up inbound email easily. If you have some internal mail service you'd like to use you can also -write a custom adapter, but this requires digging into the code. +write a custom mailer, but this requires digging into the code. Phabricator sends mail in the background, so the daemons need to be running for it to be able to deliver mail. You should receive setup warnings if they are not. For more information on using daemons, see @{article:Managing Daemons with phd}. -**Note on SendGrid**: Users have experienced a number of odd issues with -SendGrid, compared to fewer issues with other mailers. We discourage SendGrid -unless you're already using it. If you send to SendGrid via SMTP, you may need -to adjust `phpmailer.smtp-encoding`. -= Basics = +Basics +====== Regardless of how outbound email is delivered, you should configure these keys in your configuration: @@ -51,33 +49,175 @@ in your configuration: - **metamta.can-send-as-user** should be left as `false` in most cases, but see the documentation for details. -= Configuring Mail Adapters = -To choose how mail will be sent, change the `metamta.mail-adapter` key in -your configuration. Possible values are listed in the UI: +Configuring Mailers +=================== - - `PhabricatorMailImplementationAmazonMailgunAdapter`: use Mailgun, see - "Adapter: Mailgun". - - `PhabricatorMailImplementationAmazonSESAdapter`: use Amazon SES, see - "Adapter: Amazon SES". - - `PhabricatorMailImplementationPHPMailerLiteAdapter`: default, uses - "sendmail", see "Adapter: Sendmail". - - `PhabricatorMailImplementationPHPMailerAdapter`: uses SMTP, see - "Adapter: SMTP". - - `PhabricatorMailImplementationSendGridAdapter`: use SendGrid, see - "Adapter: SendGrid". - - `Some Custom Class You Write`: use a custom adapter you write, see - "Adapter: Custom". - - `PhabricatorMailImplementationTestAdapter`: this will - **completely disable** outbound mail. You can use this if you don't want to - send outbound mail, or want to skip this step for now and configure it - later. +Configure one or more mailers by listing them in the the `cluster.mailers` +configuration option. Most installs only need to configure one mailer, but you +can configure multiple mailers to provide greater availability in the event of +a service disruption. -= Adapter: Sendmail = +A valid `cluster.mailers` configuration looks something like this: -This is the default, and selected by choosing -`PhabricatorMailImplementationPHPMailerLiteAdapter` as the value for -**metamta.mail-adapter**. This requires a `sendmail` binary to be installed on +```lang=json +[ + { + "key": "mycompany-mailgun", + "type": "mailgun", + "options": { + "domain": "mycompany.com", + "api-key": "..." + } + }, + ... +] +``` + +The supported keys for each mailer are: + + - `key`: Required string. A unique name for this mailer. + - `type`: Required string. Identifies the type of mailer. See below for + options. + - `priority`: Optional string. Advanced option which controls load balancing + and failover behavior. See below for details. + - `options`: Optional map. Additional options for the mailer type. + +The `type` field can be used to select these third-party mailers: + + - `mailgun`: Use Mailgun. + - `ses`: Use Amazon SES. + - `sendgrid`: Use Sendgrid. + +It also supports these local mailers: + + - `sendmail`: Use the local `sendmail` binary. + - `smtp`: Connect directly to an SMTP server. + - `test`: Internal mailer for testing. Does not send mail. + +You can also write your own mailer by extending +`PhabricatorMailImplementationAdapter`. + +Once you've selected a mailer, find the corresponding section below for +instructions on configuring it. + + +Setting Complex Configuration +============================= + +Mailers can not be edited from the web UI. If mailers could be edited from +the web UI, it would give an attacker who compromised an administrator account +a lot of power: they could redirect mail to a server they control and then +intercept mail for any other account, including password reset mail. + +For more information about locked configuration options, see +@{article:Configuration Guide: Locked and Hidden Configuration}. + +Setting `cluster.mailers` from the command line using `bin/config set` can be +tricky because of shell escaping. The easiest way to do it is to use the +`--stdin` flag. First, put your desired configuration in a file like this: + +```lang=json, name=mailers.json +[ + { + "key": "test-mailer", + "type": "test" + } +] +``` + +Then set the value like this: + +``` +phabricator/ $ ./bin/config set --stdin < mailers.json +``` + +For alternatives and more information on configuration, see +@{article:Configuration User Guide: Advanced Configuration} + + +Mailer: Mailgun +=============== + +Mailgun is a third-party email delivery service. You can learn more at +. Mailgun is easy to configure and works well. + +To use this mailer, set `type` to `mailgun`, then configure these `options`: + + - `api-key`: Required string. Your Mailgun API key. + - `domain`: Required string. Your Mailgun domain. + + +Mailer: Postmark +================ + +Postmark is a third-party email delivery serivice. You can learn more at +. + +To use this mailer, set `type` to `postmark`, then configure these `options`: + + - `access-token`: Required string. Your Postmark access token. + - `inbound-addresses`: Optional list. Address ranges which you + will accept inbound Postmark HTTP webook requests from. + +The default address list is preconfigured with Postmark's address range, so +you generally will not need to set or adjust it. + +The option accepts a list of CIDR ranges, like `1.2.3.4/16` (IPv4) or +`::ffff:0:0/96` (IPv6). The default ranges are: + +```lang=json +[ + "50.31.156.6/32" +] +``` + +The default address ranges were last updated in February 2018, and were +documented at: + + +Mailer: Amazon SES +================== + +Amazon SES is Amazon's cloud email service. You can learn more at +. + +To use this mailer, set `type` to `ses`, then configure these `options`: + + - `access-key`: Required string. Your Amazon SES access key. + - `secret-key`: Required string. Your Amazon SES secret key. + - `endpoint`: Required string. Your Amazon SES endpoint. + +NOTE: Amazon SES **requires you to verify your "From" address**. Configure +which "From" address to use by setting "`metamta.default-address`" in your +config, then follow the Amazon SES verification process to verify it. You +won't be able to send email until you do this! + + +Mailer: SendGrid +================ + +SendGrid is a third-party email delivery service. You can learn more at +. + +You can configure SendGrid in two ways: you can send via SMTP or via the REST +API. To use SMTP, configure Phabricator to use an `smtp` mailer. + +To use the REST API mailer, set `type` to `sendgrid`, then configure +these `options`: + + - `api-user`: Required string. Your SendGrid login name. + - `api-key`: Required string. Your SendGrid API key. + +NOTE: Users have experienced a number of odd issues with SendGrid, compared to +fewer issues with other mailers. We discourage SendGrid unless you're already +using it. + + +Mailer: Sendmail +================ + +This requires a `sendmail` binary to be installed on the system. Most MTAs (e.g., sendmail, qmail, postfix) should do this, but your machine may not have one installed by default. For install instructions, consult the documentation for your favorite MTA. @@ -88,96 +228,32 @@ document. If you can already send outbound email from the command line or know how to configure it, this option is straightforward. If you have no idea how to do any of this, strongly consider using Mailgun or Amazon SES instead. -If you experience issues with mail getting mangled (for example, arriving with -too many or too few newlines) you may try adjusting `phpmailer.smtp-encoding`. +To use this mailer, set `type` to `sendmail`. There are no `options` to +configure. -= Adapter: SMTP = + +Mailer: STMP +============ You can use this adapter to send mail via an external SMTP server, like Gmail. -To do this, set these configuration keys: - - **metamta.mail-adapter**: set to - `PhabricatorMailImplementationPHPMailerAdapter`. - - **phpmailer.mailer**: set to `smtp`. - - **phpmailer.smtp-host**: set to hostname of your SMTP server. - - **phpmailer.smtp-port**: set to port of your SMTP server. - - **phpmailer.smtp-user**: set to your username used for authentication. - - **phpmailer.smtp-password**: set to your password used for authentication. - - **phpmailer.smtp-protocol**: set to `tls` or `ssl` if necessary. Use +To use this mailer, set `type` to `smtp`, then configure these `options`: + + - `host`: Required string. The hostname of your SMTP server. + - `user`: Optional string. Username used for authentication. + - `password`: Optional string. Password for authentication. + - `protocol`: Optional string. Set to `tls` or `ssl` if necessary. Use `ssl` for Gmail. - - **phpmailer.smtp-encoding**: Normally safe to leave as the default, but - adjusting it may help resolve mail mangling issues (for example, mail - arriving with too many or too few newlines). -= Adapter: Mailgun = -Mailgun is an email delivery service. You can learn more at -. Mailgun isn't free, but is very easy to configure -and works well. +Disable Mail +============ -To use Mailgun, sign up for an account, then set these configuration keys: +To disable mail, just don't configure any mailers. - - **metamta.mail-adapter**: set to - `PhabricatorMailImplementationMailgunAdapter`. - - **mailgun.api-key**: set to your Mailgun API key. - - **mailgun.domain**: set to your Mailgun domain. -= Adapter: Amazon SES = - -Amazon SES is Amazon's cloud email service. It is not free, but is easier to -configure than sendmail and can simplify outbound email configuration. To use -Amazon SES, you need to sign up for an account with Amazon at -. - -To configure Phabricator to use Amazon SES, set these configuration keys: - - - **metamta.mail-adapter**: set to - "PhabricatorMailImplementationAmazonSESAdapter". - - **amazon-ses.access-key**: set to your Amazon SES access key. - - **amazon-ses.secret-key**: set to your Amazon SES secret key. - - **amazon-ses.endpoint**: Set to your Amazon SES endpoint. - -NOTE: Amazon SES **requires you to verify your "From" address**. Configure which -"From" address to use by setting "`metamta.default-address`" in your config, -then follow the Amazon SES verification process to verify it. You won't be able -to send email until you do this! - -= Adapter: SendGrid = - -SendGrid is an email delivery service like Amazon SES. You can learn more at -. It is easy to configure, but not free. - -You can configure SendGrid in two ways: you can send via SMTP or via the REST -API. To use SMTP, just configure `sendmail` and leave Phabricator's setup -with defaults. To use the REST API, follow the instructions in this section. - -To configure Phabricator to use SendGrid, set these configuration keys: - - - **metamta.mail-adapter**: set to - "PhabricatorMailImplementationSendGridAdapter". - - **sendgrid.api-user**: set to your SendGrid login name. - - **sendgrid.api-key**: set to your SendGrid password. - -If you're logged into your SendGrid account, you may be able to find this -information easily by visiting . - -= Adapter: Custom = - -You can provide a custom adapter by writing a concrete subclass of -@{class:PhabricatorMailImplementationAdapter} and setting it as the -`metamta.mail-adapter`. - -TODO: This should be better documented once extending Phabricator is better -documented. - -= Adapter: Disable Outbound Mail = - -You can use the @{class:PhabricatorMailImplementationTestAdapter} to completely -disable outbound mail, if you don't want to send mail or don't want to configure -it yet. Just set **metamta.mail-adapter** to -`PhabricatorMailImplementationTestAdapter`. - -= Testing and Debugging Outbound Email = +Testing and Debugging Outbound Email +==================================== You can use the `bin/mail` utility to test, debug, and examine outbound mail. In particular: @@ -191,7 +267,59 @@ Run `bin/mail help ` for more help on using these commands. You can monitor daemons using the Daemon Console (`/daemon/`, or click **Daemon Console** from the homepage). -= Next Steps = + +Priorities +========== + +By default, Phabricator will try each mailer in order: it will try the first +mailer first. If that fails (for example, because the service is not available +at the moment) it will try the second mailer, and so on. + +If you want to load balance between multiple mailers instead of using one as +a primary, you can set `priority`. Phabricator will start with mailers in the +highest priority group and go through them randomly, then fall back to the +next group. + +For example, if you have two SMTP servers and you want to balance requests +between them and then fall back to Mailgun if both fail, configure priorities +like this: + +```lang=json +[ + { + "key": "smtp-uswest", + "type": "smtp", + "priority": 300, + "options": "..." + }, + { + "key": "smtp-useast", + "type": "smtp", + "priority": 300, + "options": "..." + }, + { + "key": "mailgun-fallback", + "type": "mailgun", + "options": "..." + } +} +``` + +Phabricator will start with servers in the highest priority group (the group +with the **largest** `priority` number). In this example, the highest group is +`300`, which has the two SMTP servers. They'll be tried in random order first. + +If both fail, Phabricator will move on to the next priority group. In this +example, there are no other priority groups. + +If it still hasn't sent the mail, Phabricator will try servers which are not +in any priority group, in the configured order. In this example there is +only one such server, so it will try to send via Mailgun. + + +Next Steps +========== Continue by: diff --git a/src/docs/user/userguide/mail_rules.diviner b/src/docs/user/userguide/mail_rules.diviner index 3640f5e5a5..61bc3210e9 100644 --- a/src/docs/user/userguide/mail_rules.diviner +++ b/src/docs/user/userguide/mail_rules.diviner @@ -3,7 +3,8 @@ How to effectively manage Phabricator email notifications. -= Overview = +Overview +======== Phabricator uses email as a major notification channel, but the amount of email it sends can seem overwhelming if you're working on an active team. This @@ -13,69 +14,35 @@ By far the best approach to managing mail is to **write mail rules** to categorize mail. Essentially all modern mail clients allow you to quickly write sophisticated rules to route, categorize, or delete email. -= Reducing Email = +Reducing Email +============== You can reduce the amount of email you receive by turning off some types of email in {nav Settings > Email Preferences}. For example, you can turn off email produced by your own actions (like when you comment on a revision), and some types of less-important notifications about events. -= Mail Rules = +Mail Rules +========== The best approach to managing mail is to write mail rules. Simply writing rules to move mail from Differential, Maniphest and Herald to separate folders will vastly simplify mail management. -Phabricator also sets a large number of headers (see below) which can allow you -to write more sophisticated mail rules. +Phabricator also adds mail headers (see below) which can allow you to write +more sophisticated mail rules. -= Mail Headers = +Mail Headers +============ -Phabricator sends a variety of mail headers that can be useful in crafting rules -to route and manage mail. +Phabricator sends various information in mail headers that can be useful in +crafting rules to route and manage mail. To see a full list of headers, use +the "View Raw Message" feature in your mail client. -Headers in plural contain lists. A list containing two items, `1` and -`15` will generally be formatted like this: +The most useful header for routing is generally `X-Phabricator-Stamps`. This +is a list of attributes which describe the object the mail is about and the +actions which the mail informs you about. - X-Header: <1>, <15> - -The intent is to allow you to write a rule which matches against "<1>". If you -just match against "1", you'll incorrectly match "15", but matching "<1>" will -correctly match only "<1>". - -Some other headers use a single value but can be presented multiple times. -It is to support e-mail clients which are not able to create rules using regular -expressions or wildcards (namely Outlook). - -The headers Phabricator adds to mail are: - - - `X-Phabricator-Sent-This-Message`: this is attached to all mail - Phabricator sends. You can use it to differentiate between email from - Phabricator and replies/forwards of Phabricator mail from human beings. - - `X-Phabricator-To`: this is attached to all mail Phabricator sends. - It shows the PHIDs of the original "To" line, before any mutation - by the mailer configuration. - - `X-Phabricator-Cc`: this is attached to all mail Phabricator sends. - It shows the PHIDs of the original "Cc" line, before any mutation by the - mailer configuration. - - `X-Differential-Author`: this is attached to Differential mail and shows - the revision's author. You can use it to filter mail about your revisions - (or other users' revisions). - - `X-Differential-Reviewer`: this is attached to Differential mail and - shows the reviewers. You can use it to filter mail about revisions you - are reviewing, versus revisions you are explicitly CC'd on or CC'd as - a result of Herald rules. - - `X-Differential-Reviewers`: list version of the previous. - - `X-Differential-CC`: this is attached to Differential mail and shows - the CCs on the revision. - - `X-Differential-CCs`: list version of the previous. - - `X-Differential-Explicit-CC`: this is attached to Differential mail and - shows the explicit CCs on the revision (those that were added by humans, - not by Herald). - - `X-Differential-Explicit-CCs`: list version of the previous. - - `X-Phabricator-Mail-Tags`: this is attached to some mail and has - a list of descriptors about the mail. (This is fairly new and subject - to some change.) - - `X-Herald-Rules`: this is attached to some mail and shows Herald rule - IDs which have triggered for the object. You can use this to sort or - categorize mail that has triggered specific rules. +If you use a client which can not perform header matching (like Gmail), you can +change the {nav Settings > Email Format > Send Stamps} setting to include the +stamps in the mail body and then match them with body rules. diff --git a/src/infrastructure/cluster/config/PhabricatorClusterMailersConfigType.php b/src/infrastructure/cluster/config/PhabricatorClusterMailersConfigType.php new file mode 100644 index 0000000000..03f30506bd --- /dev/null +++ b/src/infrastructure/cluster/config/PhabricatorClusterMailersConfigType.php @@ -0,0 +1,102 @@ +newException( + pht( + 'Mailer cluster configuration is not valid: it should be a list '. + 'of mailer configurations.')); + } + + foreach ($value as $index => $spec) { + if (!is_array($spec)) { + throw $this->newException( + pht( + 'Mailer cluster configuration is not valid: each entry in the '. + 'list must be a dictionary describing a mailer, but the value '. + 'with index "%s" is not a dictionary.', + $index)); + } + } + + $adapters = PhabricatorMailImplementationAdapter::getAllAdapters(); + + $map = array(); + foreach ($value as $index => $spec) { + try { + PhutilTypeSpec::checkMap( + $spec, + array( + 'key' => 'string', + 'type' => 'string', + 'priority' => 'optional int', + 'options' => 'optional wild', + )); + } catch (Exception $ex) { + throw $this->newException( + pht( + 'Mailer configuration has an invalid mailer specification '. + '(at index "%s"): %s.', + $index, + $ex->getMessage())); + } + + $key = $spec['key']; + if (isset($map[$key])) { + throw $this->newException( + pht( + 'Mailer configuration is invalid: multiple mailers have the same '. + 'key ("%s"). Each mailer must have a unique key.', + $key)); + } + $map[$key] = true; + + $priority = idx($spec, 'priority'); + if ($priority !== null && $priority <= 0) { + throw $this->newException( + pht( + 'Mailer configuration ("%s") is invalid: priority must be '. + 'greater than 0.', + $key)); + } + + $type = $spec['type']; + if (!isset($adapters[$type])) { + throw $this->newException( + pht( + 'Mailer configuration ("%s") is invalid: mailer type ("%s") is '. + 'unknown. Supported mailer types are: %s.', + $key, + $type, + implode(', ', array_keys($adapters)))); + } + + $options = idx($spec, 'options', array()); + try { + $defaults = $adapters[$type]->newDefaultOptions(); + $options = $options + $defaults; + id(clone $adapters[$type])->setOptions($options); + } catch (Exception $ex) { + throw $this->newException( + pht( + 'Mailer configuration ("%s") specifies invalid options for '. + 'mailer: %s', + $key, + $ex->getMessage())); + } + } + } + +} diff --git a/src/infrastructure/customfield/field/PhabricatorCustomField.php b/src/infrastructure/customfield/field/PhabricatorCustomField.php index c96f09f369..818bf119ff 100644 --- a/src/infrastructure/customfield/field/PhabricatorCustomField.php +++ b/src/infrastructure/customfield/field/PhabricatorCustomField.php @@ -35,6 +35,7 @@ abstract class PhabricatorCustomField extends Phobject { const ROLE_HERALD = 'herald'; const ROLE_EDITENGINE = 'EditEngine'; const ROLE_HERALDACTION = 'herald.action'; + const ROLE_EXPORT = 'export'; /* -( Building Applications with Custom Fields )--------------------------- */ @@ -299,6 +300,8 @@ abstract class PhabricatorCustomField extends Phobject { case self::ROLE_EDITENGINE: return $this->shouldAppearInEditView() || $this->shouldAppearInEditEngine(); + case self::ROLE_EXPORT: + return $this->shouldAppearInDataExport(); case self::ROLE_DEFAULT: return true; default: @@ -1362,6 +1365,46 @@ abstract class PhabricatorCustomField extends Phobject { } +/* -( Data Export )-------------------------------------------------------- */ + + + public function shouldAppearInDataExport() { + if ($this->proxy) { + return $this->proxy->shouldAppearInDataExport(); + } + + try { + $this->newExportFieldType(); + return true; + } catch (PhabricatorCustomFieldImplementationIncompleteException $ex) { + return false; + } + } + + public function newExportField() { + if ($this->proxy) { + return $this->proxy->newExportField(); + } + + return $this->newExportFieldType() + ->setLabel($this->getFieldName()); + } + + public function newExportData() { + if ($this->proxy) { + return $this->proxy->newExportData(); + } + throw new PhabricatorCustomFieldImplementationIncompleteException($this); + } + + protected function newExportFieldType() { + if ($this->proxy) { + return $this->proxy->newExportFieldType(); + } + throw new PhabricatorCustomFieldImplementationIncompleteException($this); + } + + /* -( Conduit )------------------------------------------------------------ */ diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php index c42cf71c23..acb7f59a32 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php @@ -530,5 +530,8 @@ abstract class PhabricatorStandardCustomField return $this->getFieldValue(); } + public function newExportData() { + return $this->getFieldValue(); + } } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldInt.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldInt.php index 4a3e45d757..f06c30d482 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldInt.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldInt.php @@ -124,4 +124,8 @@ final class PhabricatorStandardCustomFieldInt return new ConduitIntParameterType(); } + protected function newExportFieldType() { + return new PhabricatorIntExportField(); + } + } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php index 8242c477fd..56164bb7b5 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php @@ -76,4 +76,8 @@ final class PhabricatorStandardCustomFieldText return new ConduitStringParameterType(); } + protected function newExportFieldType() { + return new PhabricatorStringExportField(); + } + } diff --git a/src/infrastructure/daemon/garbagecollector/management/PhabricatorGarbageCollectorManagementCompactEdgesWorkflow.php b/src/infrastructure/daemon/garbagecollector/management/PhabricatorGarbageCollectorManagementCompactEdgesWorkflow.php new file mode 100644 index 0000000000..c47d159850 --- /dev/null +++ b/src/infrastructure/daemon/garbagecollector/management/PhabricatorGarbageCollectorManagementCompactEdgesWorkflow.php @@ -0,0 +1,103 @@ +setName('compact-edges') + ->setExamples('**compact-edges**') + ->setSynopsis( + pht( + 'Rebuild old edge transactions storage to use a more compact '. + 'format.')) + ->setArguments(array()); + } + + public function execute(PhutilArgumentParser $args) { + $tables = id(new PhutilClassMapQuery()) + ->setAncestorClass('PhabricatorApplicationTransaction') + ->execute(); + + foreach ($tables as $table) { + $this->compactEdges($table); + } + + return 0; + } + + private function compactEdges(PhabricatorApplicationTransaction $table) { + $conn = $table->establishConnection('w'); + $class = get_class($table); + + echo tsprintf( + "%s\n", + pht( + 'Rebuilding transactions for "%s"...', + $class)); + + $cursor = 0; + $updated = 0; + while (true) { + $rows = $table->loadAllWhere( + 'transactionType = %s + AND id > %d + AND (oldValue LIKE %> OR newValue LIKE %>) + ORDER BY id ASC LIMIT 100', + PhabricatorTransactions::TYPE_EDGE, + $cursor, + // We're looking for transactions with JSON objects in their value + // fields: the new style transactions have JSON lists instead and + // start with "[" rather than "{". + '{', + '{'); + + if (!$rows) { + break; + } + + foreach ($rows as $row) { + $id = $row->getID(); + + $old = $row->getOldValue(); + $new = $row->getNewValue(); + + if (!is_array($old) || !is_array($new)) { + echo tsprintf( + "%s\n", + pht( + 'Transaction %s (of type %s) has unexpected data, skipping.', + $id, + $class)); + } + + $record = PhabricatorEdgeChangeRecord::newFromTransaction($row); + + $old_data = $record->getModernOldEdgeTransactionData(); + $old_json = phutil_json_encode($old_data); + + $new_data = $record->getModernNewEdgeTransactionData(); + $new_json = phutil_json_encode($new_data); + + queryfx( + $conn, + 'UPDATE %T SET oldValue = %s, newValue = %s WHERE id = %d', + $table->getTableName(), + $old_json, + $new_json, + $id); + + $updated++; + + $cursor = $row->getID(); + } + } + + echo tsprintf( + "%s\n", + pht( + 'Done, compacted %s edge transactions.', + new PhutilNumber($updated))); + } + +} diff --git a/src/infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobType.php b/src/infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobType.php index a5e29bc101..7854730bea 100644 --- a/src/infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobType.php +++ b/src/infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobType.php @@ -25,4 +25,24 @@ abstract class PhabricatorWorkerBulkJobType extends Phobject { ->execute(); } + public function getCurtainActions( + PhabricatorUser $viewer, + PhabricatorWorkerBulkJob $job) { + + if ($job->isConfirming()) { + $continue_uri = $job->getMonitorURI(); + } else { + $continue_uri = $job->getDoneURI(); + } + + $continue = id(new PhabricatorActionView()) + ->setHref($continue_uri) + ->setIcon('fa-arrow-circle-o-right') + ->setName(pht('Continue')); + + return array( + $continue, + ); + } + } diff --git a/src/infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobWorker.php b/src/infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobWorker.php index 9117060da6..d11f7e81af 100644 --- a/src/infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobWorker.php +++ b/src/infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobWorker.php @@ -77,6 +77,10 @@ abstract class PhabricatorWorkerBulkJobWorker pht('Job actor does not have permission to edit job.')); } + // Allow the worker to fill user caches inline; bulk jobs occasionally + // need to access user preferences. + $actor->setAllowInlineCacheGeneration(true); + return $actor; } diff --git a/src/infrastructure/daemon/workers/bulk/PhabricatorWorkerSingleBulkJobType.php b/src/infrastructure/daemon/workers/bulk/PhabricatorWorkerSingleBulkJobType.php new file mode 100644 index 0000000000..f80411348c --- /dev/null +++ b/src/infrastructure/daemon/workers/bulk/PhabricatorWorkerSingleBulkJobType.php @@ -0,0 +1,27 @@ +getPHID()); + + return $tasks; + } + +} diff --git a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php index 40e43c2ec7..312281617d 100644 --- a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php +++ b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php @@ -180,6 +180,10 @@ final class PhabricatorWorkerBulkJob return $this->getJobImplementation()->getJobName($this); } + public function getCurtainActions(PhabricatorUser $viewer) { + return $this->getJobImplementation()->getCurtainActions($viewer, $this); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/infrastructure/edges/__tests__/PhabricatorEdgeChangeRecordTestCase.php b/src/infrastructure/edges/__tests__/PhabricatorEdgeChangeRecordTestCase.php new file mode 100644 index 0000000000..cf46c00d4d --- /dev/null +++ b/src/infrastructure/edges/__tests__/PhabricatorEdgeChangeRecordTestCase.php @@ -0,0 +1,158 @@ +setOldValue($old_bulky); + $bulky_xaction->setNewValue($new_bulky); + + $slim_xaction = new ManiphestTransaction(); + $slim_xaction->setOldValue($old_slim); + $slim_xaction->setNewValue($new_slim); + + $bulky_record = PhabricatorEdgeChangeRecord::newFromTransaction( + $bulky_xaction); + + $slim_record = PhabricatorEdgeChangeRecord::newFromTransaction( + $slim_xaction); + + $this->assertEqual( + $bulky_record->getAddedPHIDs(), + $slim_record->getAddedPHIDs()); + + $this->assertEqual( + $bulky_record->getRemovedPHIDs(), + $slim_record->getRemovedPHIDs()); + } + +} diff --git a/src/infrastructure/edges/util/PhabricatorEdgeChangeRecord.php b/src/infrastructure/edges/util/PhabricatorEdgeChangeRecord.php new file mode 100644 index 0000000000..38557dc09f --- /dev/null +++ b/src/infrastructure/edges/util/PhabricatorEdgeChangeRecord.php @@ -0,0 +1,91 @@ +xaction = $xaction; + return $record; + } + + public function getChangedPHIDs() { + $add = $this->getAddedPHIDs(); + $rem = $this->getRemovedPHIDs(); + + $add = array_fuse($add); + $rem = array_fuse($rem); + + return array_keys($add + $rem); + } + + public function getAddedPHIDs() { + $old = $this->getOldDestinationPHIDs(); + $new = $this->getNewDestinationPHIDs(); + + $old = array_fuse($old); + $new = array_fuse($new); + + $add = array_diff_key($new, $old); + return array_keys($add); + } + + public function getRemovedPHIDs() { + $old = $this->getOldDestinationPHIDs(); + $new = $this->getNewDestinationPHIDs(); + + $old = array_fuse($old); + $new = array_fuse($new); + + $rem = array_diff_key($old, $new); + return array_keys($rem); + } + + public function getModernOldEdgeTransactionData() { + return $this->getRemovedPHIDs(); + } + + public function getModernNewEdgeTransactionData() { + return $this->getAddedPHIDs(); + } + + private function getOldDestinationPHIDs() { + if ($this->xaction) { + $old = $this->xaction->getOldValue(); + return $this->getPHIDsFromTransactionValue($old); + } + + throw new Exception( + pht('Edge change record is not configured with any change data.')); + } + + private function getNewDestinationPHIDs() { + if ($this->xaction) { + $new = $this->xaction->getNewValue(); + return $this->getPHIDsFromTransactionValue($new); + } + + throw new Exception( + pht('Edge change record is not configured with any change data.')); + } + + private function getPHIDsFromTransactionValue($value) { + if (!$value) { + return array(); + } + + // If the list items are arrays, this is an older-style map of + // dictionaries. + $head = head($value); + if (is_array($head)) { + return ipull($value, 'dst'); + } + + // If the list items are not arrays, this is a newer-style list of PHIDs. + return $value; + } + +} diff --git a/src/infrastructure/export/PhabricatorEpochExportField.php b/src/infrastructure/export/PhabricatorEpochExportField.php deleted file mode 100644 index a19e60b50e..0000000000 --- a/src/infrastructure/export/PhabricatorEpochExportField.php +++ /dev/null @@ -1,27 +0,0 @@ -zone)) { - $this->zone = new DateTimeZone('UTC'); - } - - try { - $date = new DateTime('@'.$value); - } catch (Exception $ex) { - return null; - } - - $date->setTimezone($this->zone); - return $date->format('c'); - } - - public function getNaturalValue($value) { - return (int)$value; - } - -} diff --git a/src/infrastructure/export/PhabricatorIntExportField.php b/src/infrastructure/export/PhabricatorIntExportField.php deleted file mode 100644 index 3363f9b5d5..0000000000 --- a/src/infrastructure/export/PhabricatorIntExportField.php +++ /dev/null @@ -1,10 +0,0 @@ -object = $object; + return ($object instanceof PhabricatorCustomFieldInterface); + } + + public function newExportFields() { + $prototype = $this->object; + + $fields = $this->newCustomFields($prototype); + + $results = array(); + foreach ($fields as $field) { + $field_key = $field->getModernFieldKey(); + + $results[] = $field->newExportField() + ->setKey($field_key); + } + + return $results; + } + + public function newExportData(array $objects) { + $viewer = $this->getViewer(); + + $field_map = array(); + foreach ($objects as $object) { + $object_phid = $object->getPHID(); + + $fields = PhabricatorCustomField::getObjectFields( + $object, + PhabricatorCustomField::ROLE_EXPORT); + + $fields + ->setViewer($viewer) + ->readFieldsFromObject($object); + + $field_map[$object_phid] = $fields; + } + + $all_fields = array(); + foreach ($field_map as $field_list) { + foreach ($field_list->getFields() as $field) { + $all_fields[] = $field; + } + } + + id(new PhabricatorCustomFieldStorageQuery()) + ->addFields($all_fields) + ->execute(); + + $results = array(); + foreach ($objects as $object) { + $object_phid = $object->getPHID(); + $object_fields = $field_map[$object_phid]; + + $map = array(); + foreach ($object_fields->getFields() as $field) { + $key = $field->getModernFieldKey(); + $map[$key] = $field->newExportData(); + } + + $results[] = $map; + } + + return $results; + } + + private function newCustomFields($object) { + $fields = PhabricatorCustomField::getObjectFields( + $object, + PhabricatorCustomField::ROLE_EXPORT); + $fields->setViewer($this->getViewer()); + + return $fields->getFields(); + } + +} diff --git a/src/infrastructure/export/engine/PhabricatorExportEngine.php b/src/infrastructure/export/engine/PhabricatorExportEngine.php new file mode 100644 index 0000000000..f2c6a1270b --- /dev/null +++ b/src/infrastructure/export/engine/PhabricatorExportEngine.php @@ -0,0 +1,168 @@ +viewer = $viewer; + return $this; + } + + public function getViewer() { + return $this->viewer; + } + + public function setSearchEngine( + PhabricatorApplicationSearchEngine $search_engine) { + $this->searchEngine = $search_engine; + return $this; + } + + public function getSearchEngine() { + return $this->searchEngine; + } + + public function setSavedQuery(PhabricatorSavedQuery $saved_query) { + $this->savedQuery = $saved_query; + return $this; + } + + public function getSavedQuery() { + return $this->savedQuery; + } + + public function setExportFormat( + PhabricatorExportFormat $export_format) { + $this->exportFormat = $export_format; + return $this; + } + + public function getExportFormat() { + return $this->exportFormat; + } + + public function setFilename($filename) { + $this->filename = $filename; + return $this; + } + + public function getFilename() { + return $this->filename; + } + + public function setTitle($title) { + $this->title = $title; + return $this; + } + + public function getTitle() { + return $this->title; + } + + public function newBulkJob(AphrontRequest $request) { + $viewer = $this->getViewer(); + $engine = $this->getSearchEngine(); + $saved_query = $this->getSavedQuery(); + $format = $this->getExportFormat(); + + $params = array( + 'engineClass' => get_class($engine), + 'queryKey' => $saved_query->getQueryKey(), + 'formatKey' => $format->getExportFormatKey(), + 'title' => $this->getTitle(), + 'filename' => $this->getFilename(), + ); + + $job = PhabricatorWorkerBulkJob::initializeNewJob( + $viewer, + new PhabricatorExportEngineBulkJobType(), + $params); + + // We queue these jobs directly into STATUS_WAITING without requiring + // a confirmation from the user. + + $xactions = array(); + + $xactions[] = id(new PhabricatorWorkerBulkJobTransaction()) + ->setTransactionType(PhabricatorWorkerBulkJobTransaction::TYPE_STATUS) + ->setNewValue(PhabricatorWorkerBulkJob::STATUS_WAITING); + + $editor = id(new PhabricatorWorkerBulkJobEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnMissingFields(true) + ->applyTransactions($job, $xactions); + + return $job; + } + + public function exportFile() { + $viewer = $this->getViewer(); + $engine = $this->getSearchEngine(); + $saved_query = $this->getSavedQuery(); + $format = $this->getExportFormat(); + $title = $this->getTitle(); + $filename = $this->getFilename(); + + $query = $engine->buildQueryFromSavedQuery($saved_query); + + $extension = $format->getFileExtension(); + $mime_type = $format->getMIMEContentType(); + $filename = $filename.'.'.$extension; + + $format = id(clone $format) + ->setViewer($viewer) + ->setTitle($title); + + $field_list = $engine->newExportFieldList(); + $field_list = mpull($field_list, null, 'getKey'); + $format->addHeaders($field_list); + + // Iterate over the query results in large page so we don't have to hold + // too much stuff in memory. + $page_size = 1000; + $page_cursor = null; + do { + $pager = $engine->newPagerForSavedQuery($saved_query); + $pager->setPageSize($page_size); + + if ($page_cursor !== null) { + $pager->setAfterID($page_cursor); + } + + $objects = $engine->executeQuery($query, $pager); + $objects = array_values($objects); + $page_cursor = $pager->getNextPageID(); + + $export_data = $engine->newExport($objects); + for ($ii = 0; $ii < count($objects); $ii++) { + $format->addObject($objects[$ii], $field_list, $export_data[$ii]); + } + } while ($pager->getHasMoreResults()); + + $export_result = $format->newFileData(); + + // We have all the data in one big string and aren't actually + // streaming it, but pretending that we are allows us to actviate + // the chunk engine and store large files. + $iterator = new ArrayIterator(array($export_result)); + + $source = id(new PhabricatorIteratorFileUploadSource()) + ->setName($filename) + ->setViewPolicy(PhabricatorPolicies::POLICY_NOONE) + ->setMIMEType($mime_type) + ->setRelativeTTL(phutil_units('60 minutes in seconds')) + ->setAuthorPHID($viewer->getPHID()) + ->setIterator($iterator); + + return $source->uploadFile(); + } + +} diff --git a/src/infrastructure/export/engine/PhabricatorExportEngineBulkJobType.php b/src/infrastructure/export/engine/PhabricatorExportEngineBulkJobType.php new file mode 100644 index 0000000000..712127f479 --- /dev/null +++ b/src/infrastructure/export/engine/PhabricatorExportEngineBulkJobType.php @@ -0,0 +1,118 @@ +getParameter('filePHID'); + if (!$file_phid) { + $actions[] = id(new PhabricatorActionView()) + ->setHref('#') + ->setIcon('fa-download') + ->setDisabled(true) + ->setName(pht('Exporting Data...')); + } else { + $file = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs(array($file_phid)) + ->executeOne(); + if (!$file) { + $actions[] = id(new PhabricatorActionView()) + ->setHref('#') + ->setIcon('fa-download') + ->setDisabled(true) + ->setName(pht('Temporary File Expired')); + } else { + $actions[] = id(new PhabricatorActionView()) + ->setRenderAsForm(true) + ->setHref($file->getDownloadURI()) + ->setIcon('fa-download') + ->setName(pht('Download Data Export')); + } + } + + return $actions; + } + + + public function runTask( + PhabricatorUser $actor, + PhabricatorWorkerBulkJob $job, + PhabricatorWorkerBulkTask $task) { + + $engine_class = $job->getParameter('engineClass'); + if (!is_subclass_of($engine_class, 'PhabricatorApplicationSearchEngine')) { + throw new Exception( + pht( + 'Unknown search engine class "%s".', + $engine_class)); + } + + $engine = newv($engine_class, array()) + ->setViewer($actor); + + $query_key = $job->getParameter('queryKey'); + if ($engine->isBuiltinQuery($query_key)) { + $saved_query = $engine->buildSavedQueryFromBuiltin($query_key); + } else if ($query_key) { + $saved_query = id(new PhabricatorSavedQueryQuery()) + ->setViewer($actor) + ->withQueryKeys(array($query_key)) + ->executeOne(); + } else { + $saved_query = null; + } + + if (!$saved_query) { + throw new Exception( + pht( + 'Failed to load saved query ("%s").', + $query_key)); + } + + $format_key = $job->getParameter('formatKey'); + + $all_formats = PhabricatorExportFormat::getAllExportFormats(); + $format = idx($all_formats, $format_key); + if (!$format) { + throw new Exception( + pht( + 'Unknown export format ("%s").', + $format_key)); + } + + if (!$format->isExportFormatEnabled()) { + throw new Exception( + pht( + 'Export format ("%s") is not enabled.', + $format_key)); + } + + $export_engine = id(new PhabricatorExportEngine()) + ->setViewer($actor) + ->setTitle($job->getParameter('title')) + ->setFilename($job->getParameter('filename')) + ->setSearchEngine($engine) + ->setSavedQuery($saved_query) + ->setExportFormat($format); + + $file = $export_engine->exportFile(); + + $job + ->setParameter('filePHID', $file->getPHID()) + ->save(); + } + +} diff --git a/src/infrastructure/export/engine/PhabricatorExportEngineExtension.php b/src/infrastructure/export/engine/PhabricatorExportEngineExtension.php new file mode 100644 index 0000000000..01d4471ef2 --- /dev/null +++ b/src/infrastructure/export/engine/PhabricatorExportEngineExtension.php @@ -0,0 +1,31 @@ +getPhobjectClassConstant('EXTENSIONKEY'); + } + + final public function setViewer($viewer) { + $this->viewer = $viewer; + return $this; + } + + final public function getViewer() { + return $this->viewer; + } + + abstract public function supportsObject($object); + abstract public function newExportFields(); + abstract public function newExportData(array $objects); + + final public static function getAllExtensions() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getExtensionKey') + ->execute(); + } + +} diff --git a/src/infrastructure/export/engine/PhabricatorExportFormatSetting.php b/src/infrastructure/export/engine/PhabricatorExportFormatSetting.php new file mode 100644 index 0000000000..625a90719b --- /dev/null +++ b/src/infrastructure/export/engine/PhabricatorExportFormatSetting.php @@ -0,0 +1,16 @@ +getConfigOption(LiskDAO::CONFIG_TIMESTAMPS)) { + return false; + } + + return true; + } + + public function newExportFields() { + return array( + id(new PhabricatorEpochExportField()) + ->setKey('dateCreated') + ->setLabel(pht('Created')), + id(new PhabricatorEpochExportField()) + ->setKey('dateModified') + ->setLabel(pht('Modified')), + ); + } + + public function newExportData(array $objects) { + $map = array(); + foreach ($objects as $object) { + $map[] = array( + 'dateCreated' => $object->getDateCreated(), + 'dateModified' => $object->getDateModified(), + ); + } + return $map; + } + +} diff --git a/src/infrastructure/export/engine/PhabricatorProjectsExportEngineExtension.php b/src/infrastructure/export/engine/PhabricatorProjectsExportEngineExtension.php new file mode 100644 index 0000000000..eb3bca3a49 --- /dev/null +++ b/src/infrastructure/export/engine/PhabricatorProjectsExportEngineExtension.php @@ -0,0 +1,60 @@ +setKey('tagPHIDs') + ->setLabel(pht('Tag PHIDs')), + id(new PhabricatorStringListExportField()) + ->setKey('tags') + ->setLabel(pht('Tags')), + ); + } + + public function newExportData(array $objects) { + $viewer = $this->getViewer(); + + $object_phids = mpull($objects, 'getPHID'); + + $projects_query = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs($object_phids) + ->withEdgeTypes( + array( + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, + )); + $projects_query->execute(); + + $handles = $viewer->loadHandles($projects_query->getDestinationPHIDs()); + + $map = array(); + foreach ($objects as $object) { + $object_phid = $object->getPHID(); + + $project_phids = $projects_query->getDestinationPHIDs( + array($object_phid), + array(PhabricatorProjectObjectHasProjectEdgeType::EDGECONST)); + + $handle_list = $handles->newSublist($project_phids); + $handle_list = iterator_to_array($handle_list); + $handle_names = mpull($handle_list, 'getName'); + $handle_names = array_values($handle_names); + + $map[] = array( + 'tagPHIDs' => $project_phids, + 'tags' => $handle_names, + ); + } + + return $map; + } + +} diff --git a/src/infrastructure/export/engine/PhabricatorSpacesExportEngineExtension.php b/src/infrastructure/export/engine/PhabricatorSpacesExportEngineExtension.php new file mode 100644 index 0000000000..3e187bc8cd --- /dev/null +++ b/src/infrastructure/export/engine/PhabricatorSpacesExportEngineExtension.php @@ -0,0 +1,53 @@ +getViewer(); + + if (!PhabricatorSpacesNamespaceQuery::getViewerSpacesExist($viewer)) { + return false; + } + + return ($object instanceof PhabricatorSpacesInterface); + } + + public function newExportFields() { + return array( + id(new PhabricatorPHIDExportField()) + ->setKey('spacePHID') + ->setLabel(pht('Space PHID')), + id(new PhabricatorStringExportField()) + ->setKey('space') + ->setLabel(pht('Space')), + ); + } + + public function newExportData(array $objects) { + $viewer = $this->getViewer(); + + $space_phids = array(); + foreach ($objects as $object) { + $space_phids[] = PhabricatorSpacesNamespaceQuery::getObjectSpacePHID( + $object); + } + $handles = $viewer->loadHandles($space_phids); + + $map = array(); + foreach ($objects as $object) { + $space_phid = PhabricatorSpacesNamespaceQuery::getObjectSpacePHID( + $object); + + $map[] = array( + 'spacePHID' => $space_phid, + 'space' => $handles[$space_phid]->getName(), + ); + } + + return $map; + } + +} diff --git a/src/infrastructure/export/engine/PhabricatorSubscriptionsExportEngineExtension.php b/src/infrastructure/export/engine/PhabricatorSubscriptionsExportEngineExtension.php new file mode 100644 index 0000000000..8aedb38fa8 --- /dev/null +++ b/src/infrastructure/export/engine/PhabricatorSubscriptionsExportEngineExtension.php @@ -0,0 +1,60 @@ +setKey('subscriberPHIDs') + ->setLabel(pht('Subscriber PHIDs')), + id(new PhabricatorStringListExportField()) + ->setKey('subscribers') + ->setLabel(pht('Subscribers')), + ); + } + + public function newExportData(array $objects) { + $viewer = $this->getViewer(); + + $object_phids = mpull($objects, 'getPHID'); + + $projects_query = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs($object_phids) + ->withEdgeTypes( + array( + PhabricatorObjectHasSubscriberEdgeType::EDGECONST, + )); + $projects_query->execute(); + + $handles = $viewer->loadHandles($projects_query->getDestinationPHIDs()); + + $map = array(); + foreach ($objects as $object) { + $object_phid = $object->getPHID(); + + $project_phids = $projects_query->getDestinationPHIDs( + array($object_phid), + array(PhabricatorObjectHasSubscriberEdgeType::EDGECONST)); + + $handle_list = $handles->newSublist($project_phids); + $handle_list = iterator_to_array($handle_list); + $handle_names = mpull($handle_list, 'getName'); + $handle_names = array_values($handle_names); + + $map[] = array( + 'subscriberPHIDs' => $project_phids, + 'subscribers' => $handle_names, + ); + } + + return $map; + } + +} diff --git a/src/infrastructure/export/field/PhabricatorEpochExportField.php b/src/infrastructure/export/field/PhabricatorEpochExportField.php new file mode 100644 index 0000000000..4dffde5aa8 --- /dev/null +++ b/src/infrastructure/export/field/PhabricatorEpochExportField.php @@ -0,0 +1,47 @@ +zone)) { + $this->zone = new DateTimeZone('UTC'); + } + + try { + $date = new DateTime('@'.$value); + } catch (Exception $ex) { + return null; + } + + $date->setTimezone($this->zone); + return $date->format('c'); + } + + public function getNaturalValue($value) { + return (int)$value; + } + + public function getPHPExcelValue($value) { + $epoch = $this->getNaturalValue($value); + + $seconds_per_day = phutil_units('1 day in seconds'); + $offset = ($seconds_per_day * 25569); + + return ($epoch + $offset) / $seconds_per_day; + } + + /** + * @phutil-external-symbol class PHPExcel_Style_NumberFormat + */ + public function formatPHPExcelCell($cell, $style) { + $code = PHPExcel_Style_NumberFormat::FORMAT_DATE_YYYYMMDD2; + + $style + ->getNumberFormat() + ->setFormatCode($code); + } + +} diff --git a/src/infrastructure/export/PhabricatorExportField.php b/src/infrastructure/export/field/PhabricatorExportField.php similarity index 50% rename from src/infrastructure/export/PhabricatorExportField.php rename to src/infrastructure/export/field/PhabricatorExportField.php index 3efb7a8b9a..7ee0918595 100644 --- a/src/infrastructure/export/PhabricatorExportField.php +++ b/src/infrastructure/export/field/PhabricatorExportField.php @@ -25,11 +25,32 @@ abstract class PhabricatorExportField } public function getTextValue($value) { - return (string)$this->getNaturalValue($value); + $natural_value = $this->getNaturalValue($value); + + if ($natural_value === null) { + return null; + } + + return (string)$natural_value; } public function getNaturalValue($value) { return $value; } + public function getPHPExcelValue($value) { + return $this->getTextValue($value); + } + + /** + * @phutil-external-symbol class PHPExcel_Cell_DataType + */ + public function formatPHPExcelCell($cell, $style) { + $cell->setDataType(PHPExcel_Cell_DataType::TYPE_STRING); + } + + public function getCharacterWidth() { + return 24; + } + } diff --git a/src/infrastructure/export/PhabricatorIDExportField.php b/src/infrastructure/export/field/PhabricatorIDExportField.php similarity index 72% rename from src/infrastructure/export/PhabricatorIDExportField.php rename to src/infrastructure/export/field/PhabricatorIDExportField.php index 5b29fdb21d..1ef3d53370 100644 --- a/src/infrastructure/export/PhabricatorIDExportField.php +++ b/src/infrastructure/export/field/PhabricatorIDExportField.php @@ -7,4 +7,8 @@ final class PhabricatorIDExportField return (int)$value; } + public function getCharacterWidth() { + return 12; + } + } diff --git a/src/infrastructure/export/field/PhabricatorIntExportField.php b/src/infrastructure/export/field/PhabricatorIntExportField.php new file mode 100644 index 0000000000..57f7e0ab29 --- /dev/null +++ b/src/infrastructure/export/field/PhabricatorIntExportField.php @@ -0,0 +1,25 @@ +setDataType(PHPExcel_Cell_DataType::TYPE_NUMERIC); + } + + public function getCharacterWidth() { + return 8; + } + +} diff --git a/src/infrastructure/export/field/PhabricatorListExportField.php b/src/infrastructure/export/field/PhabricatorListExportField.php new file mode 100644 index 0000000000..67c3e06ff5 --- /dev/null +++ b/src/infrastructure/export/field/PhabricatorListExportField.php @@ -0,0 +1,10 @@ +addRow($headers); + } + public function addObject($object, array $fields, array $map) { $values = array(); foreach ($fields as $key => $field) { $value = $map[$key]; $value = $field->getTextValue($value); + $values[] = $value; + } + + $this->addRow($values); + } + + private function addRow(array $values) { + $row = array(); + foreach ($values as $value) { + + // Excel is extremely interested in executing arbitrary code it finds in + // untrusted CSV files downloaded from the internet. When a cell looks + // like it might be too tempting for Excel to ignore, mangle the value + // to dissuade remote code execution. See T12800. + + if (preg_match('/^\s*[+=@-]/', $value)) { + $value = '(!) '.$value; + } if (preg_match('/\s|,|\"/', $value)) { $value = str_replace('"', '""', $value); $value = '"'.$value.'"'; } - $values[] = $value; + $row[] = $value; } - $this->rows[] = implode(',', $values); + $this->rows[] = implode(',', $row); } public function newFileData() { diff --git a/src/infrastructure/export/format/PhabricatorExcelExportFormat.php b/src/infrastructure/export/format/PhabricatorExcelExportFormat.php new file mode 100644 index 0000000000..2b0c787884 --- /dev/null +++ b/src/infrastructure/export/format/PhabricatorExcelExportFormat.php @@ -0,0 +1,168 @@ + https://github.com/PHPOffice/PHPExcel + +Briefly: + + - Clone that repository somewhere on the sever + (like `/path/to/example/PHPExcel`). + - Update your PHP `%s` setting (in `php.ini`) to include the PHPExcel + `Classes` directory (like `/path/to/example/PHPExcel/Classes`). +EOHELP + , + 'include_path'); + } + + public function getFileExtension() { + return 'xlsx'; + } + + public function getMIMEContentType() { + return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; + } + + /** + * @phutil-external-symbol class PHPExcel_Cell_DataType + */ + public function addHeaders(array $fields) { + $sheet = $this->getSheet(); + + $header_format = array( + 'font' => array( + 'bold' => true, + ), + ); + + $row = 1; + $col = 0; + foreach ($fields as $field) { + $cell_value = $field->getLabel(); + + $cell_name = $this->getCellName($col, $row); + + $cell = $sheet->setCellValue( + $cell_name, + $cell_value, + $return_cell = true); + + $sheet->getStyle($cell_name)->applyFromArray($header_format); + $cell->setDataType(PHPExcel_Cell_DataType::TYPE_STRING); + + $width = $field->getCharacterWidth(); + if ($width !== null) { + $col_name = $this->getCellName($col); + $sheet->getColumnDimension($col_name) + ->setWidth($width); + } + + $col++; + } + } + + public function addObject($object, array $fields, array $map) { + $sheet = $this->getSheet(); + + $col = 0; + foreach ($fields as $key => $field) { + $cell_value = $map[$key]; + $cell_value = $field->getPHPExcelValue($cell_value); + + $cell_name = $this->getCellName($col, $this->rowCursor); + + $cell = $sheet->setCellValue( + $cell_name, + $cell_value, + $return_cell = true); + + $style = $sheet->getStyle($cell_name); + $field->formatPHPExcelCell($cell, $style); + + $col++; + } + + $this->rowCursor++; + } + + /** + * @phutil-external-symbol class PHPExcel_IOFactory + */ + public function newFileData() { + $workbook = $this->getWorkbook(); + $writer = PHPExcel_IOFactory::createWriter($workbook, 'Excel2007'); + + ob_start(); + $writer->save('php://output'); + $data = ob_get_clean(); + + return $data; + } + + private function getWorkbook() { + if (!$this->workbook) { + $this->workbook = $this->newWorkbook(); + } + return $this->workbook; + } + + /** + * @phutil-external-symbol class PHPExcel + */ + private function newWorkbook() { + include_once 'PHPExcel.php'; + return new PHPExcel(); + } + + private function getSheet() { + if (!$this->sheet) { + $workbook = $this->getWorkbook(); + + $sheet = $workbook->setActiveSheetIndex(0); + $sheet->setTitle($this->getTitle()); + + $this->sheet = $sheet; + + // The row cursor starts on the second row, after the header row. + $this->rowCursor = 2; + } + + return $this->sheet; + } + + private function getCellName($col, $row = null) { + $col_name = chr(ord('A') + $col); + + if ($row === null) { + return $col_name; + } + + return $col_name.$row; + } + +} diff --git a/src/infrastructure/export/PhabricatorExportFormat.php b/src/infrastructure/export/format/PhabricatorExportFormat.php similarity index 77% rename from src/infrastructure/export/PhabricatorExportFormat.php rename to src/infrastructure/export/format/PhabricatorExportFormat.php index a1da4e90d8..4566814b9d 100644 --- a/src/infrastructure/export/PhabricatorExportFormat.php +++ b/src/infrastructure/export/format/PhabricatorExportFormat.php @@ -4,6 +4,7 @@ abstract class PhabricatorExportFormat extends Phobject { private $viewer; + private $title; final public function getExportFormatKey() { return $this->getPhobjectClassConstant('EXPORTKEY'); @@ -18,10 +19,23 @@ abstract class PhabricatorExportFormat return $this->viewer; } + final public function setTitle($title) { + $this->title = $title; + return $this; + } + + final public function getTitle() { + return $this->title; + } + abstract public function getExportFormatName(); abstract public function getMIMEContentType(); abstract public function getFileExtension(); + public function addHeaders(array $fields) { + return; + } + abstract public function addObject($object, array $fields, array $map); abstract public function newFileData(); @@ -36,16 +50,4 @@ abstract class PhabricatorExportFormat ->execute(); } - final public static function getAllEnabledExportFormats() { - $formats = self::getAllExportFormats(); - - foreach ($formats as $key => $format) { - if (!$format->isExportFormatEnabled()) { - unset($formats[$key]); - } - } - - return $formats; - } - } diff --git a/src/infrastructure/export/PhabricatorJSONExportFormat.php b/src/infrastructure/export/format/PhabricatorJSONExportFormat.php similarity index 100% rename from src/infrastructure/export/PhabricatorJSONExportFormat.php rename to src/infrastructure/export/format/PhabricatorJSONExportFormat.php diff --git a/src/infrastructure/export/PhabricatorTextExportFormat.php b/src/infrastructure/export/format/PhabricatorTextExportFormat.php similarity index 67% rename from src/infrastructure/export/PhabricatorTextExportFormat.php rename to src/infrastructure/export/format/PhabricatorTextExportFormat.php index ec308f2eb5..d51e199f91 100644 --- a/src/infrastructure/export/PhabricatorTextExportFormat.php +++ b/src/infrastructure/export/format/PhabricatorTextExportFormat.php @@ -23,17 +23,29 @@ final class PhabricatorTextExportFormat return 'text/plain'; } + public function addHeaders(array $fields) { + $headers = mpull($fields, 'getLabel'); + $this->addRow($headers); + } + public function addObject($object, array $fields, array $map) { $values = array(); foreach ($fields as $key => $field) { $value = $map[$key]; $value = $field->getTextValue($value); - $value = addcslashes($value, "\0..\37\\\177..\377"); - $values[] = $value; } - $this->rows[] = implode("\t", $values); + $this->addRow($values); + } + + private function addRow(array $values) { + $row = array(); + foreach ($values as $value) { + $row[] = addcslashes($value, "\0..\37\\\177..\377"); + } + + $this->rows[] = implode("\t", $row); } public function newFileData() { diff --git a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php index 40cedacf06..eb74f8debf 100644 --- a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php +++ b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php @@ -676,73 +676,73 @@ final class PhabricatorUSEnglishTranslation '%s edited commit(s), added %s: %s; removed %s: %s.' => '%s edited commits, added %3$s; removed %5$s.', - '%s added %s reverted commit(s): %s.' => array( + '%s added %s reverted change(s): %s.' => array( array( - '%s added a reverted commit: %3$s.', - '%s added reverted commits: %3$s.', + '%s added a reverted change: %3$s.', + '%s added reverted changes: %3$s.', ), ), - '%s removed %s reverted commit(s): %s.' => array( + '%s removed %s reverted change(s): %s.' => array( array( - '%s removed a reverted commit: %3$s.', - '%s removed reverted commits: %3$s.', + '%s removed a reverted change: %3$s.', + '%s removed reverted changes: %3$s.', ), ), - '%s edited reverted commit(s), added %s: %s; removed %s: %s.' => - '%s edited reverted commits, added %3$s; removed %5$s.', + '%s edited reverted change(s), added %s: %s; removed %s: %s.' => + '%s edited reverted changes, added %3$s; removed %5$s.', - '%s added %s reverted commit(s) for %s: %s.' => array( + '%s added %s reverted change(s) for %s: %s.' => array( array( - '%s added a reverted commit for %3$s: %4$s.', - '%s added reverted commits for %3$s: %4$s.', + '%s added a reverted change for %3$s: %4$s.', + '%s added reverted changes for %3$s: %4$s.', ), ), - '%s removed %s reverted commit(s) for %s: %s.' => array( + '%s removed %s reverted change(s) for %s: %s.' => array( array( - '%s removed a reverted commit for %3$s: %4$s.', - '%s removed reverted commits for %3$s: %4$s.', + '%s removed a reverted change for %3$s: %4$s.', + '%s removed reverted changes for %3$s: %4$s.', ), ), - '%s edited reverted commit(s) for %s, added %s: %s; removed %s: %s.' => - '%s edited reverted commits for %2$s, added %4$s; removed %6$s.', + '%s edited reverted change(s) for %s, added %s: %s; removed %s: %s.' => + '%s edited reverted changes for %2$s, added %4$s; removed %6$s.', - '%s added %s reverting commit(s): %s.' => array( + '%s added %s reverting change(s): %s.' => array( array( - '%s added a reverting commit: %3$s.', - '%s added reverting commits: %3$s.', + '%s added a reverting change: %3$s.', + '%s added reverting changes: %3$s.', ), ), - '%s removed %s reverting commit(s): %s.' => array( + '%s removed %s reverting change(s): %s.' => array( array( - '%s removed a reverting commit: %3$s.', - '%s removed reverting commits: %3$s.', + '%s removed a reverting change: %3$s.', + '%s removed reverting changes: %3$s.', ), ), - '%s edited reverting commit(s), added %s: %s; removed %s: %s.' => - '%s edited reverting commits, added %3$s; removed %5$s.', + '%s edited reverting change(s), added %s: %s; removed %s: %s.' => + '%s edited reverting changes, added %3$s; removed %5$s.', - '%s added %s reverting commit(s) for %s: %s.' => array( + '%s added %s reverting change(s) for %s: %s.' => array( array( - '%s added a reverting commit for %3$s: %4$s.', - '%s added reverting commits for %3$s: %4$s.', + '%s added a reverting change for %3$s: %4$s.', + '%s added reverting changes for %3$s: %4$s.', ), ), - '%s removed %s reverting commit(s) for %s: %s.' => array( + '%s removed %s reverting change(s) for %s: %s.' => array( array( - '%s removed a reverting commit for %3$s: %4$s.', - '%s removed reverting commits for %3$s: %4$s.', + '%s removed a reverting change for %3$s: %4$s.', + '%s removed reverting changes for %3$s: %4$s.', ), ), - '%s edited reverting commit(s) for %s, added %s: %s; removed %s: %s.' => - '%s edited reverting commits for %s, added %4$s; removed %6$s.', + '%s edited reverting change(s) for %s, added %s: %s; removed %s: %s.' => + '%s edited reverting changes for %s, added %4$s; removed %6$s.', '%s changed project member(s), added %d: %s; removed %d: %s.' => '%s changed project members, added %3$s; removed %5$s.', diff --git a/src/infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php b/src/infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php index 35c0ecfad0..fbbfa36805 100644 --- a/src/infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php +++ b/src/infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php @@ -75,7 +75,7 @@ abstract class PhabricatorObjectRemarkupRule extends PhutilRemarkupRule { } if ($this->getEngine()->isTextMode()) { - return PhabricatorEnv::getProductionURI($href); + return $text.' <'.PhabricatorEnv::getProductionURI($href).'>'; } else if ($this->getEngine()->isHTMLMailMode()) { $href = PhabricatorEnv::getProductionURI($href); return $this->renderObjectTagForMail($text, $href, $handle); diff --git a/src/view/phui/PHUIInfoView.php b/src/view/phui/PHUIInfoView.php index 161e49108b..69d0549299 100644 --- a/src/view/phui/PHUIInfoView.php +++ b/src/view/phui/PHUIInfoView.php @@ -10,7 +10,7 @@ final class PHUIInfoView extends AphrontTagView { const SEVERITY_PLAIN = 'plain'; private $title; - private $errors; + private $errors = array(); private $severity = null; private $id; private $buttons = array(); diff --git a/src/view/phui/PHUITimelineEventView.php b/src/view/phui/PHUITimelineEventView.php index 161dd0c944..e64b9b06b2 100644 --- a/src/view/phui/PHUITimelineEventView.php +++ b/src/view/phui/PHUITimelineEventView.php @@ -301,18 +301,14 @@ final class PHUITimelineEventView extends AphrontView { $menu = null; $items = array(); - $has_menu = false; if (!$this->getIsPreview() && !$this->getHideCommentOptions()) { foreach ($this->getEventGroup() as $event) { $items[] = $event->getMenuItems($this->anchor); - if ($event->hasChildren()) { - $has_menu = true; - } } $items = array_mergev($items); } - if ($items || $has_menu) { + if ($items) { $icon = id(new PHUIIconView()) ->setIcon('fa-caret-down'); $aural = javelin_tag( @@ -351,6 +347,8 @@ final class PHUITimelineEventView extends AphrontView { )); $has_menu = true; + } else { + $has_menu = false; } // Render "extra" information (timestamp, etc). diff --git a/support/startup/PhabricatorStartup.php b/support/startup/PhabricatorStartup.php index 1911a46b8a..212b057376 100644 --- a/support/startup/PhabricatorStartup.php +++ b/support/startup/PhabricatorStartup.php @@ -395,6 +395,11 @@ final class PhabricatorStartup { if (function_exists('libxml_disable_entity_loader')) { libxml_disable_entity_loader(true); } + + // See T13060. If the locale for this process (the parent process) is not + // a UTF-8 locale we can encounter problems when launching subprocesses + // which receive UTF-8 parameters in their command line argument list. + @setlocale(LC_ALL, 'en_US.UTF-8'); } diff --git a/webroot/rsrc/css/aphront/phabricator-nav-view.css b/webroot/rsrc/css/aphront/phabricator-nav-view.css index e8081a55e6..f3320e3eae 100644 --- a/webroot/rsrc/css/aphront/phabricator-nav-view.css +++ b/webroot/rsrc/css/aphront/phabricator-nav-view.css @@ -44,7 +44,7 @@ position: fixed; top: 0; bottom: 0; - left: 205px; + left: 410px; width: 7px; cursor: col-resize; @@ -66,7 +66,7 @@ .device-desktop .phabricator-standard-page-body .has-drag-nav .phabricator-nav-content { - margin-left: 212px; + margin-left: 417px; } .device-desktop .phabricator-standard-page-body .has-drag-nav @@ -81,7 +81,7 @@ } .device-desktop .phui-navigation-shell .has-drag-nav .phabricator-nav-local { - width: 205px; + width: 410px; padding: 0; background: transparent; } diff --git a/webroot/rsrc/css/layout/phabricator-filetree-view.css b/webroot/rsrc/css/layout/phabricator-filetree-view.css index b247f5e4f9..6497c37056 100644 --- a/webroot/rsrc/css/layout/phabricator-filetree-view.css +++ b/webroot/rsrc/css/layout/phabricator-filetree-view.css @@ -50,7 +50,42 @@ background-color: {$hovergrey}; } +.phabricator-filetree .filetree-added { + background: {$sh-greenbackground}; +} + +.phabricator-filetree .filetree-deleted { + background: {$sh-redbackground}; +} + +.phabricator-filetree .filetree-movecopy { + background: {$sh-orangebackground}; +} + .phabricator-filetree .phabricator-active-nav-focus { background-color: {$hovergrey}; border-left: 4px solid {$sky}; } + +.phabricator-filetree .filetree-progress-hint { + width: 24px; + margin-right: 6px; + display: inline-block; + padding: 0 4px; + border-radius: 4px; + font-size: smaller; + background: {$greybackground}; + text-align: center; + opacity: 0.5; +} + +.phabricator-filetree .filetree-comments-visible { + background: {$lightblue}; + opacity: 0.75; + color: {$darkgreytext}; +} + +.phabricator-filetree .filetree-comments-completed { + background: {$darkgreybackground}; + color: {$greytext}; +} diff --git a/webroot/rsrc/css/phui/phui-action-list.css b/webroot/rsrc/css/phui/phui-action-list.css index 5e32a1ea0a..e7ee38a8bf 100644 --- a/webroot/rsrc/css/phui/phui-action-list.css +++ b/webroot/rsrc/css/phui/phui-action-list.css @@ -95,15 +95,20 @@ color: {$sky}; } -.device-desktop .phabricator-action-view-href.action-item-red:hover - .phabricator-action-view-item { - background-color: {$sh-redbackground}; - color: {$sh-redtext}; +.phabricator-action-view.action-item-red { + background-color: {$sh-redbackground}; } -.device-desktop .phabricator-action-view-href.action-item-red:hover +.phabricator-action-view.action-item-red .phabricator-action-view-item, +.phabricator-action-view.action-item-red .phabricator-action-view-icon { + color: {$sh-redtext}; +} + +.device-desktop .phabricator-action-view.action-item-red:hover + .phabricator-action-view-item, +.device-desktop .phabricator-action-view.action-item-red:hover .phabricator-action-view-icon { - color: {$red}; + color: {$red}; } .phabricator-action-view-label .phabricator-action-view-item, diff --git a/webroot/rsrc/css/phui/phui-timeline-view.css b/webroot/rsrc/css/phui/phui-timeline-view.css index b0ae9c0044..6fae6802be 100644 --- a/webroot/rsrc/css/phui/phui-timeline-view.css +++ b/webroot/rsrc/css/phui/phui-timeline-view.css @@ -390,10 +390,6 @@ outline: none; } -.phui-timeline-menu .phui-icon-view { - color: {$lightgreytext}; -} - a.phui-timeline-menu .phui-icon-view { color: {$bluetext}; } diff --git a/webroot/rsrc/js/application/diff/DiffChangeset.js b/webroot/rsrc/js/application/diff/DiffChangeset.js index 72eeae294a..24d734573d 100644 --- a/webroot/rsrc/js/application/diff/DiffChangeset.js +++ b/webroot/rsrc/js/application/diff/DiffChangeset.js @@ -27,6 +27,7 @@ JX.install('DiffChangeset', { this._highlight = data.highlight; this._encoding = data.encoding; this._loaded = data.loaded; + this._treeNodeID = data.treeNodeID; this._leftID = data.left; this._rightID = data.right; @@ -62,6 +63,7 @@ JX.install('DiffChangeset', { _changesetList: null, _icon: null, + _treeNodeID: null, getLeftChangesetID: function() { return this._leftID; @@ -737,7 +739,8 @@ JX.install('DiffChangeset', { _rebuildAllInlines: function() { var rows = JX.DOM.scry(this._node, 'tr'); - for (var ii = 0; ii < rows.length; ii++) { + var ii; + for (ii = 0; ii < rows.length; ii++) { var row = rows[ii]; if (this._getRowType(row) != 'comment') { continue; @@ -749,6 +752,75 @@ JX.install('DiffChangeset', { } }, + redrawFileTree: function() { + var tree; + try { + tree = JX.$(this._treeNodeID); + } catch (e) { + return; + } + + var inlines = this._inlines; + var done = []; + var undone = []; + var inline; + + for (var ii = 0; ii < inlines.length; ii++) { + inline = inlines[ii]; + + if (inline.isDeleted()) { + continue; + } + + if (inline.isSynthetic()) { + continue; + } + + if (inline.isEditing()) { + continue; + } + + if (!inline.getID()) { + // These are new comments which have been cancelled, and do not + // count as anything. + continue; + } + + if (inline.isDraft()) { + continue; + } + + if (!inline.isDone()) { + undone.push(inline); + } else { + done.push(inline); + } + } + + var total = done.length + undone.length; + + var hint; + var is_visible; + var is_completed; + if (total) { + if (done.length) { + hint = [done.length, '/', total]; + } else { + hint = total; + } + is_visible = true; + is_completed = (done.length == total); + } else { + hint = '-'; + is_visible = false; + is_completed = false; + } + + JX.DOM.setContent(tree, hint); + JX.DOM.alterClass(tree, 'filetree-comments-visible', is_visible); + JX.DOM.alterClass(tree, 'filetree-comments-completed', is_completed); + }, + toggleVisibility: function() { this._visible = !this._visible; diff --git a/webroot/rsrc/js/application/diff/DiffChangesetList.js b/webroot/rsrc/js/application/diff/DiffChangesetList.js index ec0270ac12..e62d2f51dd 100644 --- a/webroot/rsrc/js/application/diff/DiffChangesetList.js +++ b/webroot/rsrc/js/application/diff/DiffChangesetList.js @@ -915,6 +915,11 @@ JX.install('DiffChangesetList', { this._bannerChangeset = null; this._redrawBanner(); + + var changesets = this._changesets; + for (var ii = 0; ii < changesets.length; ii++) { + changesets[ii].redrawFileTree(); + } }, _onscroll: function() { diff --git a/webroot/rsrc/js/core/behavior-phabricator-nav.js b/webroot/rsrc/js/core/behavior-phabricator-nav.js index e37680abf0..74909e447d 100644 --- a/webroot/rsrc/js/core/behavior-phabricator-nav.js +++ b/webroot/rsrc/js/core/behavior-phabricator-nav.js @@ -28,6 +28,10 @@ JX.behavior('phabricator-nav', function(config) { JX.enableDispatch(document.body, 'mousemove'); JX.DOM.listen(drag, 'mousedown', null, function(e) { + if (!e.isNormalMouseEvent()) { + return; + } + dragging = JX.$V(e); // Show the "col-resize" cursor on the whole document while we're diff --git a/webroot/rsrc/js/phuix/PHUIXFormControl.js b/webroot/rsrc/js/phuix/PHUIXFormControl.js index cd67e89691..eb5f32963d 100644 --- a/webroot/rsrc/js/phuix/PHUIXFormControl.js +++ b/webroot/rsrc/js/phuix/PHUIXFormControl.js @@ -293,7 +293,7 @@ JX.install('PHUIXFormControl', { }, _newPoints: function(spec) { - return this._newText(); + return this._newText(spec); }, _newText: function(spec) {