diff --git a/bin/worker b/bin/worker new file mode 120000 index 0000000000..d0284581e5 --- /dev/null +++ b/bin/worker @@ -0,0 +1 @@ +../scripts/setup/manage_worker.php \ No newline at end of file diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 9bc63c10b1..f88091eb2e 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,11 +7,11 @@ */ return array( 'names' => array( - 'core.pkg.css' => '61fe1c6e', - 'core.pkg.js' => 'cbdbd552', + 'core.pkg.css' => 'dab8bb9c', + 'core.pkg.js' => 'e64447dc', 'darkconsole.pkg.js' => 'df001cab', 'differential.pkg.css' => '8af45893', - 'differential.pkg.js' => '85cb2027', + 'differential.pkg.js' => '42c10e78', 'diffusion.pkg.css' => '591664fa', 'diffusion.pkg.js' => 'bfc0737b', 'maniphest.pkg.css' => 'e34dfbec', @@ -30,7 +30,7 @@ return array( 'rsrc/css/aphront/phabricator-nav-view.css' => '9283c2df', 'rsrc/css/aphront/table-view.css' => 'b22b7216', 'rsrc/css/aphront/tokenizer.css' => '82ce2142', - 'rsrc/css/aphront/tooltip.css' => '9c90229d', + 'rsrc/css/aphront/tooltip.css' => '4099b97e', 'rsrc/css/aphront/transaction.css' => '5d0cae25', 'rsrc/css/aphront/two-column.css' => '16ab3ad2', 'rsrc/css/aphront/typeahead.css' => 'a989b5b3', @@ -68,7 +68,7 @@ return array( 'rsrc/css/application/flag/flag.css' => '5337623f', 'rsrc/css/application/harbormaster/harbormaster.css' => '49d64eb4', 'rsrc/css/application/herald/herald-test.css' => '778b008e', - 'rsrc/css/application/herald/herald.css' => 'c544dd1c', + 'rsrc/css/application/herald/herald.css' => '826075fa', 'rsrc/css/application/maniphest/batch-editor.css' => '8f380ebc', 'rsrc/css/application/maniphest/report.css' => '6fc16517', 'rsrc/css/application/maniphest/task-edit.css' => '8e23031b', @@ -85,7 +85,7 @@ return array( 'rsrc/css/application/phortune/phortune.css' => '9149f103', 'rsrc/css/application/phrequent/phrequent.css' => 'ffc185ad', 'rsrc/css/application/phriction/phriction-document-css.css' => '7d7f0071', - 'rsrc/css/application/policy/policy-edit.css' => '05cca26a', + 'rsrc/css/application/policy/policy-edit.css' => '815c66f7', 'rsrc/css/application/policy/policy-transaction-detail.css' => '82100a43', 'rsrc/css/application/policy/policy.css' => '957ea14c', 'rsrc/css/application/ponder/comments.css' => '6cdccea7', @@ -103,7 +103,7 @@ return array( 'rsrc/css/application/tokens/tokens.css' => '3d0f239e', 'rsrc/css/application/uiexample/example.css' => '528b19de', 'rsrc/css/core/core.css' => '40151074', - 'rsrc/css/core/remarkup.css' => '45313445', + 'rsrc/css/core/remarkup.css' => '63c9a0a7', 'rsrc/css/core/syntax.css' => '56c1ba38', 'rsrc/css/core/z-index.css' => '44e1d311', 'rsrc/css/diviner/diviner-shared.css' => '38813222', @@ -122,11 +122,11 @@ return array( 'rsrc/css/phui/phui-action-header-view.css' => '89c497e7', 'rsrc/css/phui/phui-action-list.css' => '9ee9910a', 'rsrc/css/phui/phui-box.css' => '7b3a2eed', - 'rsrc/css/phui/phui-button.css' => 'c7412aa1', + 'rsrc/css/phui/phui-button.css' => '008ba5e2', 'rsrc/css/phui/phui-document.css' => 'a5615198', - 'rsrc/css/phui/phui-feed-story.css' => '55dc7732', + 'rsrc/css/phui/phui-feed-story.css' => 'dd3c5ff5', 'rsrc/css/phui/phui-fontkit.css' => '9c3d2dce', - 'rsrc/css/phui/phui-form-view.css' => '26e6016f', + 'rsrc/css/phui/phui-form-view.css' => '4ac1192d', 'rsrc/css/phui/phui-form.css' => 'b78ec020', 'rsrc/css/phui/phui-header-view.css' => '39594ac0', 'rsrc/css/phui/phui-icon.css' => 'b4963a4f', @@ -140,9 +140,9 @@ return array( 'rsrc/css/phui/phui-remarkup-preview.css' => '19ad512b', 'rsrc/css/phui/phui-spacing.css' => '042804d6', 'rsrc/css/phui/phui-status.css' => '888cedb8', - 'rsrc/css/phui/phui-tag-view.css' => 'b0c282e0', + 'rsrc/css/phui/phui-tag-view.css' => '6b74282b', 'rsrc/css/phui/phui-text.css' => 'cf019f54', - 'rsrc/css/phui/phui-timeline-view.css' => '8c6fefe7', + 'rsrc/css/phui/phui-timeline-view.css' => '26bb3fd4', 'rsrc/css/phui/phui-workboard-view.css' => '2bf82d00', 'rsrc/css/phui/phui-workpanel-view.css' => '198c7e6c', 'rsrc/css/sprite-apps-large.css' => '20ec0cc0', @@ -371,7 +371,7 @@ return array( 'rsrc/js/application/differential/behavior-diff-radios.js' => 'e1ff79b1', 'rsrc/js/application/differential/behavior-dropdown-menus.js' => '710f209e', 'rsrc/js/application/differential/behavior-edit-inline-comments.js' => '00861799', - 'rsrc/js/application/differential/behavior-keyboard-nav.js' => '8d199d97', + 'rsrc/js/application/differential/behavior-keyboard-nav.js' => '2c426492', 'rsrc/js/application/differential/behavior-populate.js' => 'bdb3e4d0', 'rsrc/js/application/differential/behavior-show-all-comments.js' => '7c273581', 'rsrc/js/application/differential/behavior-show-field-details.js' => 'bba9eedf', @@ -389,7 +389,7 @@ return array( 'rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js' => 'e5822781', 'rsrc/js/application/files/behavior-icon-composer.js' => '8ef9ab58', 'rsrc/js/application/files/behavior-launch-icon-composer.js' => '48086888', - 'rsrc/js/application/herald/HeraldRuleEditor.js' => '3fc2c8f2', + 'rsrc/js/application/herald/HeraldRuleEditor.js' => '335fd41f', 'rsrc/js/application/herald/PathTypeahead.js' => 'f7fc67ec', 'rsrc/js/application/herald/herald-rule-editor.js' => '7ebaeed3', 'rsrc/js/application/maniphest/behavior-batch-editor.js' => 'f588412e', @@ -445,13 +445,13 @@ return array( 'rsrc/js/core/Hovercard.js' => '7e8468ae', 'rsrc/js/core/KeyboardShortcut.js' => '1ae869f2', 'rsrc/js/core/KeyboardShortcutManager.js' => 'ad7a69ca', - 'rsrc/js/core/MultirowRowManager.js' => '41e47dea', + 'rsrc/js/core/MultirowRowManager.js' => 'b5d57730', 'rsrc/js/core/Notification.js' => '0c6946e7', 'rsrc/js/core/Prefab.js' => 'bbae734c', 'rsrc/js/core/ShapedRequest.js' => '7cbe244b', 'rsrc/js/core/TextAreaUtils.js' => '5c93c52c', 'rsrc/js/core/Title.js' => '5c1c758c', - 'rsrc/js/core/ToolTip.js' => '3915d490', + 'rsrc/js/core/ToolTip.js' => '031d4411', 'rsrc/js/core/behavior-active-nav.js' => 'e379b58e', 'rsrc/js/core/behavior-audio-source.js' => '59b251eb', 'rsrc/js/core/behavior-autofocus.js' => '7319e029', @@ -483,7 +483,7 @@ return array( 'rsrc/js/core/behavior-remarkup-preview.js' => 'f7379f45', 'rsrc/js/core/behavior-reorder-applications.js' => '76b9fc3e', 'rsrc/js/core/behavior-reveal-content.js' => '60821bc7', - 'rsrc/js/core/behavior-search-typeahead.js' => 'd712ac5f', + 'rsrc/js/core/behavior-search-typeahead.js' => '724b1247', 'rsrc/js/core/behavior-select-on-click.js' => '4e3e79a6', 'rsrc/js/core/behavior-toggle-class.js' => 'e566f52c', 'rsrc/js/core/behavior-tokenizer.js' => 'b3a4b884', @@ -510,7 +510,7 @@ return array( 'aphront-panel-view-css' => '5846dfa2', 'aphront-table-view-css' => 'b22b7216', 'aphront-tokenizer-control-css' => '82ce2142', - 'aphront-tooltip-css' => '9c90229d', + 'aphront-tooltip-css' => '4099b97e', 'aphront-two-column-view-css' => '16ab3ad2', 'aphront-typeahead-control-css' => 'a989b5b3', 'auth-css' => '1e655982', @@ -538,8 +538,8 @@ return array( 'font-source-sans-pro' => '91d53463', 'global-drag-and-drop-css' => '697324ad', 'harbormaster-css' => '49d64eb4', - 'herald-css' => 'c544dd1c', - 'herald-rule-editor' => '3fc2c8f2', + 'herald-css' => '826075fa', + 'herald-rule-editor' => '335fd41f', 'herald-test-css' => '778b008e', 'inline-comment-summary-css' => '8cfd34e8', 'javelin-aphlict' => '4a07e8e3', @@ -574,7 +574,7 @@ return array( 'javelin-behavior-differential-dropdown-menus' => '710f209e', 'javelin-behavior-differential-edit-inline-comments' => '00861799', 'javelin-behavior-differential-feedback-preview' => '6932def3', - 'javelin-behavior-differential-keyboard-navigation' => '8d199d97', + 'javelin-behavior-differential-keyboard-navigation' => '2c426492', 'javelin-behavior-differential-populate' => 'bdb3e4d0', 'javelin-behavior-differential-show-field-details' => 'bba9eedf', 'javelin-behavior-differential-show-more' => 'dd7e8ef5', @@ -624,7 +624,7 @@ return array( 'javelin-behavior-phabricator-oncopy' => '2926fff2', 'javelin-behavior-phabricator-remarkup-assist' => 'e32d14ab', 'javelin-behavior-phabricator-reveal-content' => '60821bc7', - 'javelin-behavior-phabricator-search-typeahead' => 'd712ac5f', + 'javelin-behavior-phabricator-search-typeahead' => '724b1247', 'javelin-behavior-phabricator-show-all-transactions' => '7c273581', 'javelin-behavior-phabricator-tooltips' => '3ee3408b', 'javelin-behavior-phabricator-transaction-comment-form' => '9f7309fb', @@ -699,7 +699,7 @@ return array( 'maniphest-report-css' => '6fc16517', 'maniphest-task-edit-css' => '8e23031b', 'maniphest-task-summary-css' => '13ed8360', - 'multirow-row-manager' => '41e47dea', + 'multirow-row-manager' => 'b5d57730', 'owners-path-editor' => 'aa1733d0', 'owners-path-editor-css' => '2f00933b', 'paste-css' => 'aa1767d1', @@ -734,7 +734,7 @@ return array( 'phabricator-phtize' => 'd254d646', 'phabricator-prefab' => 'bbae734c', 'phabricator-profile-css' => '28f433ef', - 'phabricator-remarkup-css' => '45313445', + 'phabricator-remarkup-css' => '63c9a0a7', 'phabricator-search-results-css' => 'f240504c', 'phabricator-shaped-request' => '7cbe244b', 'phabricator-side-menu-view-css' => 'a2ccd7bd', @@ -743,7 +743,7 @@ return array( 'phabricator-standard-page-view' => '3f5b9311', 'phabricator-textareautils' => '5c93c52c', 'phabricator-title' => '5c1c758c', - 'phabricator-tooltip' => '3915d490', + 'phabricator-tooltip' => '031d4411', 'phabricator-transaction-view-css' => '5d0cae25', 'phabricator-ui-example-css' => '528b19de', 'phabricator-uiexample-javelin-view' => 'd4a14807', @@ -769,17 +769,17 @@ return array( 'phriction-document-css' => '7d7f0071', 'phui-action-header-view-css' => '89c497e7', 'phui-box-css' => '7b3a2eed', - 'phui-button-css' => 'c7412aa1', + 'phui-button-css' => '008ba5e2', 'phui-calendar-css' => '8675968e', 'phui-calendar-day-css' => 'de035c8a', 'phui-calendar-list-css' => 'c1d0ca59', 'phui-calendar-month-css' => 'a92e47d2', 'phui-document-view-css' => 'a5615198', - 'phui-feed-story-css' => '55dc7732', + 'phui-feed-story-css' => 'dd3c5ff5', 'phui-font-icon-base-css' => '3dad2ae3', 'phui-fontkit-css' => '9c3d2dce', 'phui-form-css' => 'b78ec020', - 'phui-form-view-css' => '26e6016f', + 'phui-form-view-css' => '4ac1192d', 'phui-header-view-css' => '39594ac0', 'phui-icon-view-css' => 'b4963a4f', 'phui-image-mask-css' => '5a8b09c8', @@ -792,16 +792,16 @@ return array( 'phui-remarkup-preview-css' => '19ad512b', 'phui-spacing-css' => '042804d6', 'phui-status-list-view-css' => '888cedb8', - 'phui-tag-view-css' => 'b0c282e0', + 'phui-tag-view-css' => '6b74282b', 'phui-text-css' => 'cf019f54', - 'phui-timeline-view-css' => '8c6fefe7', + 'phui-timeline-view-css' => '26bb3fd4', 'phui-workboard-view-css' => '2bf82d00', 'phui-workpanel-view-css' => '198c7e6c', 'phuix-action-list-view' => 'b5c256b8', 'phuix-action-view' => '6e8cefa4', 'phuix-dropdown-menu' => 'bd4c8dca', 'policy-css' => '957ea14c', - 'policy-edit-css' => '05cca26a', + 'policy-edit-css' => '815c66f7', 'policy-transaction-detail-css' => '82100a43', 'ponder-comment-table-css' => '6cdccea7', 'ponder-feed-view-css' => 'e62615b6', @@ -842,6 +842,12 @@ return array( '029a133d' => array( 'aphront-dialog-view-css', ), + '031d4411' => array( + 'javelin-install', + 'javelin-util', + 'javelin-dom', + 'javelin-vector', + ), '03d6ed07' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1012,6 +1018,12 @@ return array( 'javelin-stratcom', 'javelin-dom', ), + '2c426492' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-stratcom', + 'phabricator-keyboard-shortcut', + ), '2fa810fc' => array( 'javelin-behavior', 'javelin-dom', @@ -1022,6 +1034,15 @@ return array( 'javelin-install', 'javelin-typeahead-source', ), + '335fd41f' => array( + 'multirow-row-manager', + 'javelin-install', + 'javelin-util', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-json', + 'phabricator-prefab', + ), '357b6e9b' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1034,12 +1055,6 @@ return array( 'javelin-behavior', 'javelin-dom', ), - '3915d490' => array( - 'javelin-install', - 'javelin-util', - 'javelin-dom', - 'javelin-vector', - ), '3ab51e2c' => array( 'javelin-behavior', 'javelin-behavior-device', @@ -1067,15 +1082,6 @@ return array( 'javelin-stratcom', 'phabricator-tooltip', ), - '3fc2c8f2' => array( - 'multirow-row-manager', - 'javelin-install', - 'javelin-util', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-json', - 'phabricator-prefab', - ), '40a6a403' => array( 'javelin-install', 'javelin-dom', @@ -1093,12 +1099,6 @@ return array( 'phuix-action-list-view', 'phuix-action-view', ), - '41e47dea' => array( - 'javelin-install', - 'javelin-stratcom', - 'javelin-dom', - 'javelin-util', - ), '44168bad' => array( 'javelin-behavior', 'javelin-dom', @@ -1281,6 +1281,16 @@ return array( 'phabricator-phtize', 'changeset-view-manager', ), + '724b1247' => array( + 'javelin-behavior', + 'javelin-typeahead-ondemand-source', + 'javelin-typeahead', + 'javelin-dom', + 'javelin-uri', + 'javelin-util', + 'javelin-stratcom', + 'phabricator-prefab', + ), '7319e029' => array( 'javelin-behavior', 'javelin-dom', @@ -1419,12 +1429,6 @@ return array( 'javelin-uri', 'phabricator-file-upload', ), - '8d199d97' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-stratcom', - 'phabricator-keyboard-shortcut', - ), '8ef9ab58' => array( 'javelin-behavior', 'javelin-dom', @@ -1608,6 +1612,12 @@ return array( 'javelin-install', 'javelin-dom', ), + 'b5d57730' => array( + 'javelin-install', + 'javelin-stratcom', + 'javelin-dom', + 'javelin-util', + ), 'b6d401d6' => array( 'javelin-dom', 'javelin-dynval', @@ -1736,16 +1746,6 @@ return array( 'javelin-stratcom', 'javelin-dom', ), - 'd712ac5f' => array( - 'javelin-behavior', - 'javelin-typeahead-ondemand-source', - 'javelin-typeahead', - 'javelin-dom', - 'javelin-uri', - 'javelin-util', - 'javelin-stratcom', - 'phabricator-prefab', - ), 'd75709e6' => array( 'javelin-behavior', 'javelin-workflow', diff --git a/resources/sql/autopatches/20141107.phriction.policy.2.php b/resources/sql/autopatches/20141107.phriction.policy.2.php index c310b3c3ff..9391173c6a 100644 --- a/resources/sql/autopatches/20141107.phriction.policy.2.php +++ b/resources/sql/autopatches/20141107.phriction.policy.2.php @@ -16,11 +16,16 @@ foreach (new LiskMigrationIterator($table) as $doc) { continue; } - // project documents get the project policy - if (PhrictionDocument::isProjectSlug($doc->getSlug())) { + // If this was previously a magical project wiki page (under projects/, but + // not projects/ itself) we need to apply the project policies. Otherwise, + // apply the default policies. + $slug = $doc->getSlug(); + $slug = PhabricatorSlug::normalize($slug); + $prefix = 'projects/'; + if (($slug != $prefix) && (strncmp($slug, $prefix, strlen($prefix)) === 0)) { + $parts = explode('/', $slug); + $project_slug = $parts[1].'/'; - $project_slug = - PhrictionDocument::getProjectSlugIdentifier($doc->getSlug()); $project_slugs = array($project_slug); $project = id(new PhabricatorProjectQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) diff --git a/resources/sql/autopatches/20141113.auditdupes.php b/resources/sql/autopatches/20141113.auditdupes.php new file mode 100644 index 0000000000..2cc7b9bab4 --- /dev/null +++ b/resources/sql/autopatches/20141113.auditdupes.php @@ -0,0 +1,22 @@ +establishConnection('w'); + +echo "Removing duplicate Audit requests...\n"; +$seen_audit_map = array(); +foreach (new LiskMigrationIterator($table) as $request) { + $commit_phid = $request->getCommitPHID(); + $auditor_phid = $request->getAuditorPHID(); + if (isset($seen_audit_map[$commit_phid][$auditor_phid])) { + $request->delete(); + } + + if (!isset($seen_audit_map[$commit_phid])) { + $seen_audit_map[$commit_phid] = array(); + } + + $seen_audit_map[$commit_phid][$auditor_phid] = 1; +} + +echo "Done.\n"; diff --git a/resources/sql/autopatches/20141118.diffxaction.sql b/resources/sql/autopatches/20141118.diffxaction.sql new file mode 100644 index 0000000000..4ee0554131 --- /dev/null +++ b/resources/sql/autopatches/20141118.diffxaction.sql @@ -0,0 +1,19 @@ +CREATE TABLE {$NAMESPACE}_differential.differential_difftransaction ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + authorPHID VARBINARY(64) NOT NULL, + objectPHID VARBINARY(64) NOT NULL, + viewPolicy VARBINARY(64) NOT NULL, + editPolicy VARBINARY(64) NOT NULL, + commentPHID VARBINARY(64), + commentVersion INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) COLLATE {$COLLATE_TEXT} NOT NULL, + oldValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + newValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + contentSource LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + metadata LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20141119.commitpedge.sql b/resources/sql/autopatches/20141119.commitpedge.sql new file mode 100644 index 0000000000..bb5c1ec56c --- /dev/null +++ b/resources/sql/autopatches/20141119.commitpedge.sql @@ -0,0 +1,11 @@ +INSERT IGNORE INTO {$NAMESPACE}_repository.edge + (src, type, dst, dateCreated, seq) + SELECT src, 41, dst, dateCreated, seq + FROM {$NAMESPACE}_repository.edge + WHERE type = 15; + +INSERT IGNORE INTO {$NAMESPACE}_project.edge + (src, type, dst, dateCreated, seq) + SELECT src, 42, dst, dateCreated, seq + FROM {$NAMESPACE}_project.edge + WHERE type = 16; diff --git a/resources/sql/autopatches/20141119.differential.diff.policy.sql b/resources/sql/autopatches/20141119.differential.diff.policy.sql new file mode 100644 index 0000000000..b6c19c0dea --- /dev/null +++ b/resources/sql/autopatches/20141119.differential.diff.policy.sql @@ -0,0 +1,5 @@ +ALTER TABLE {$NAMESPACE}_differential.differential_diff + ADD viewPolicy VARBINARY(64) NOT NULL; + +UPDATE {$NAMESPACE}_differential.differential_diff + SET viewPolicy = 'users' WHERE viewPolicy = ''; diff --git a/resources/sql/autopatches/20141119.sshtrust.sql b/resources/sql/autopatches/20141119.sshtrust.sql new file mode 100644 index 0000000000..a75255b1c8 --- /dev/null +++ b/resources/sql/autopatches/20141119.sshtrust.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_auth.auth_sshkey + ADD isTrusted BOOL NOT NULL; diff --git a/resources/sql/autopatches/20141123.taskpriority.1.sql b/resources/sql/autopatches/20141123.taskpriority.1.sql new file mode 100644 index 0000000000..b7790e711e --- /dev/null +++ b/resources/sql/autopatches/20141123.taskpriority.1.sql @@ -0,0 +1,2 @@ +UPDATE {$NAMESPACE}_worker.worker_activetask + SET priority = 5000 - priority; diff --git a/resources/sql/autopatches/20141123.taskpriority.2.sql b/resources/sql/autopatches/20141123.taskpriority.2.sql new file mode 100644 index 0000000000..91b8979112 --- /dev/null +++ b/resources/sql/autopatches/20141123.taskpriority.2.sql @@ -0,0 +1,2 @@ +UPDATE {$NAMESPACE}_worker.worker_archivetask + SET priority = 5000 - priority; diff --git a/scripts/setup/manage_worker.php b/scripts/setup/manage_worker.php new file mode 100755 index 0000000000..05dcf085b8 --- /dev/null +++ b/scripts/setup/manage_worker.php @@ -0,0 +1,21 @@ +#!/usr/bin/env php +setTagline('manage task queue'); +$args->setSynopsis(<<parseStandardArguments(); + +$workflows = id(new PhutilSymbolLoader()) + ->setAncestorClass('PhabricatorWorkerManagementWorkflow') + ->loadObjects(); +$workflows[] = new PhutilHelpArgumentWorkflow(); +$args->parseWorkflows($workflows); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 507411ea7d..933184f256 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -19,7 +19,7 @@ phutil_register_library_map(array( 'AlmanacBindingTransaction' => 'applications/almanac/storage/AlmanacBindingTransaction.php', 'AlmanacBindingTransactionQuery' => 'applications/almanac/query/AlmanacBindingTransactionQuery.php', 'AlmanacBindingViewController' => 'applications/almanac/controller/AlmanacBindingViewController.php', - 'AlmanacConduitUtil' => 'applications/almanac/util/AlmanacConduitUtil.php', + 'AlmanacConduitAPIMethod' => 'applications/almanac/conduit/AlmanacConduitAPIMethod.php', 'AlmanacConsoleController' => 'applications/almanac/controller/AlmanacConsoleController.php', 'AlmanacController' => 'applications/almanac/controller/AlmanacController.php', 'AlmanacCoreCustomField' => 'applications/almanac/customfield/AlmanacCoreCustomField.php', @@ -45,7 +45,8 @@ phutil_register_library_map(array( 'AlmanacInterfacePHIDType' => 'applications/almanac/phid/AlmanacInterfacePHIDType.php', 'AlmanacInterfaceQuery' => 'applications/almanac/query/AlmanacInterfaceQuery.php', 'AlmanacInterfaceTableView' => 'applications/almanac/view/AlmanacInterfaceTableView.php', - 'AlmanacManagementRegisterWorkflow' => 'applications/almanac/management/AlmanacManagementRegisterWorkflow.php', + 'AlmanacManagementTrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementTrustKeyWorkflow.php', + 'AlmanacManagementUntrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementUntrustKeyWorkflow.php', 'AlmanacManagementWorkflow' => 'applications/almanac/management/AlmanacManagementWorkflow.php', 'AlmanacNames' => 'applications/almanac/util/AlmanacNames.php', 'AlmanacNamesTestCase' => 'applications/almanac/util/__tests__/AlmanacNamesTestCase.php', @@ -66,6 +67,7 @@ phutil_register_library_map(array( 'AlmanacPropertyInterface' => 'applications/almanac/property/AlmanacPropertyInterface.php', 'AlmanacPropertyQuery' => 'applications/almanac/query/AlmanacPropertyQuery.php', 'AlmanacQuery' => 'applications/almanac/query/AlmanacQuery.php', + 'AlmanacQueryServicesConduitAPIMethod' => 'applications/almanac/conduit/AlmanacQueryServicesConduitAPIMethod.php', 'AlmanacSchemaSpec' => 'applications/almanac/storage/AlmanacSchemaSpec.php', 'AlmanacService' => 'applications/almanac/storage/AlmanacService.php', 'AlmanacServiceController' => 'applications/almanac/controller/AlmanacServiceController.php', @@ -323,6 +325,7 @@ phutil_register_library_map(array( 'DifferentialDiffQuery' => 'applications/differential/query/DifferentialDiffQuery.php', 'DifferentialDiffTableOfContentsView' => 'applications/differential/view/DifferentialDiffTableOfContentsView.php', 'DifferentialDiffTestCase' => 'applications/differential/storage/__tests__/DifferentialDiffTestCase.php', + 'DifferentialDiffTransaction' => 'applications/differential/storage/DifferentialDiffTransaction.php', 'DifferentialDiffViewController' => 'applications/differential/controller/DifferentialDiffViewController.php', 'DifferentialDoorkeeperRevisionFeedStoryPublisher' => 'applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php', 'DifferentialDraft' => 'applications/differential/storage/DifferentialDraft.php', @@ -722,6 +725,7 @@ phutil_register_library_map(array( 'FileReplyHandler' => 'applications/files/mail/FileReplyHandler.php', 'FileUploadConduitAPIMethod' => 'applications/files/conduit/FileUploadConduitAPIMethod.php', 'FileUploadHashConduitAPIMethod' => 'applications/files/conduit/FileUploadHashConduitAPIMethod.php', + 'FilesDefaultViewCapability' => 'applications/files/capability/FilesDefaultViewCapability.php', 'FlagConduitAPIMethod' => 'applications/flag/conduit/FlagConduitAPIMethod.php', 'FlagDeleteConduitAPIMethod' => 'applications/flag/conduit/FlagDeleteConduitAPIMethod.php', 'FlagEditConduitAPIMethod' => 'applications/flag/conduit/FlagEditConduitAPIMethod.php', @@ -1015,6 +1019,7 @@ phutil_register_library_map(array( 'ManiphestTaskResultListView' => 'applications/maniphest/view/ManiphestTaskResultListView.php', 'ManiphestTaskSearchEngine' => 'applications/maniphest/query/ManiphestTaskSearchEngine.php', 'ManiphestTaskStatus' => 'applications/maniphest/constants/ManiphestTaskStatus.php', + 'ManiphestTaskStatusDatasource' => 'applications/maniphest/typeahead/ManiphestTaskStatusDatasource.php', 'ManiphestTaskStatusTestCase' => 'applications/maniphest/constants/__tests__/ManiphestTaskStatusTestCase.php', 'ManiphestTaskSubscriber' => 'applications/maniphest/storage/ManiphestTaskSubscriber.php', 'ManiphestTransaction' => 'applications/maniphest/storage/ManiphestTransaction.php', @@ -1321,8 +1326,13 @@ phutil_register_library_map(array( 'PhabricatorAuthRegisterController' => 'applications/auth/controller/PhabricatorAuthRegisterController.php', 'PhabricatorAuthRevokeTokenController' => 'applications/auth/controller/PhabricatorAuthRevokeTokenController.php', 'PhabricatorAuthSSHKey' => 'applications/auth/storage/PhabricatorAuthSSHKey.php', + 'PhabricatorAuthSSHKeyController' => 'applications/auth/controller/PhabricatorAuthSSHKeyController.php', + 'PhabricatorAuthSSHKeyDeleteController' => 'applications/auth/controller/PhabricatorAuthSSHKeyDeleteController.php', + 'PhabricatorAuthSSHKeyEditController' => 'applications/auth/controller/PhabricatorAuthSSHKeyEditController.php', + 'PhabricatorAuthSSHKeyGenerateController' => 'applications/auth/controller/PhabricatorAuthSSHKeyGenerateController.php', 'PhabricatorAuthSSHKeyQuery' => 'applications/auth/query/PhabricatorAuthSSHKeyQuery.php', - 'PhabricatorAuthSSHPublicKey' => 'applications/auth/storage/PhabricatorAuthSSHPublicKey.php', + 'PhabricatorAuthSSHKeyTableView' => 'applications/auth/view/PhabricatorAuthSSHKeyTableView.php', + 'PhabricatorAuthSSHPublicKey' => 'applications/auth/sshkey/PhabricatorAuthSSHPublicKey.php', 'PhabricatorAuthSession' => 'applications/auth/storage/PhabricatorAuthSession.php', 'PhabricatorAuthSessionEngine' => 'applications/auth/engine/PhabricatorAuthSessionEngine.php', 'PhabricatorAuthSessionGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthSessionGarbageCollector.php', @@ -2140,6 +2150,7 @@ phutil_register_library_map(array( 'PhabricatorProjectUIEventListener' => 'applications/project/events/PhabricatorProjectUIEventListener.php', 'PhabricatorProjectUpdateController' => 'applications/project/controller/PhabricatorProjectUpdateController.php', 'PhabricatorProjectWatchController' => 'applications/project/controller/PhabricatorProjectWatchController.php', + 'PhabricatorProjectWikiExplainController' => 'applications/project/controller/PhabricatorProjectWikiExplainController.php', 'PhabricatorQuery' => 'infrastructure/query/PhabricatorQuery.php', 'PhabricatorRecaptchaConfigOptions' => 'applications/config/option/PhabricatorRecaptchaConfigOptions.php', 'PhabricatorRedirectController' => 'applications/base/controller/PhabricatorRedirectController.php', @@ -2253,6 +2264,7 @@ phutil_register_library_map(array( 'PhabricatorSSHKeyGenerator' => 'infrastructure/util/PhabricatorSSHKeyGenerator.php', 'PhabricatorSSHLog' => 'infrastructure/log/PhabricatorSSHLog.php', 'PhabricatorSSHPassthruCommand' => 'infrastructure/ssh/PhabricatorSSHPassthruCommand.php', + 'PhabricatorSSHPublicKeyInterface' => 'applications/auth/sshkey/PhabricatorSSHPublicKeyInterface.php', 'PhabricatorSSHWorkflow' => 'infrastructure/ssh/PhabricatorSSHWorkflow.php', 'PhabricatorSavedQuery' => 'applications/search/storage/PhabricatorSavedQuery.php', 'PhabricatorSavedQueryQuery' => 'applications/search/query/PhabricatorSavedQueryQuery.php', @@ -2508,6 +2520,8 @@ phutil_register_library_map(array( 'PhabricatorWorkerArchiveTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerArchiveTask.php', 'PhabricatorWorkerDAO' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerDAO.php', 'PhabricatorWorkerLeaseQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerLeaseQuery.php', + 'PhabricatorWorkerManagementFloodWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementFloodWorkflow.php', + 'PhabricatorWorkerManagementWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementWorkflow.php', 'PhabricatorWorkerPermanentFailureException' => 'infrastructure/daemon/workers/exception/PhabricatorWorkerPermanentFailureException.php', 'PhabricatorWorkerTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTask.php', 'PhabricatorWorkerTaskData' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTaskData.php', @@ -2772,11 +2786,11 @@ phutil_register_library_map(array( 'PhrictionDiffController' => 'applications/phriction/controller/PhrictionDiffController.php', 'PhrictionDocument' => 'applications/phriction/storage/PhrictionDocument.php', 'PhrictionDocumentController' => 'applications/phriction/controller/PhrictionDocumentController.php', + 'PhrictionDocumentHeraldAdapter' => 'applications/phriction/herald/PhrictionDocumentHeraldAdapter.php', 'PhrictionDocumentPHIDType' => 'applications/phriction/phid/PhrictionDocumentPHIDType.php', 'PhrictionDocumentPreviewController' => 'applications/phriction/controller/PhrictionDocumentPreviewController.php', 'PhrictionDocumentQuery' => 'applications/phriction/query/PhrictionDocumentQuery.php', 'PhrictionDocumentStatus' => 'applications/phriction/constants/PhrictionDocumentStatus.php', - 'PhrictionDocumentTestCase' => 'applications/phriction/storage/__tests__/PhrictionDocumentTestCase.php', 'PhrictionEditConduitAPIMethod' => 'applications/phriction/conduit/PhrictionEditConduitAPIMethod.php', 'PhrictionEditController' => 'applications/phriction/controller/PhrictionEditController.php', 'PhrictionHistoryConduitAPIMethod' => 'applications/phriction/conduit/PhrictionHistoryConduitAPIMethod.php', @@ -2841,6 +2855,9 @@ phutil_register_library_map(array( 'ProjectConduitAPIMethod' => 'applications/project/conduit/ProjectConduitAPIMethod.php', 'ProjectCreateConduitAPIMethod' => 'applications/project/conduit/ProjectCreateConduitAPIMethod.php', 'ProjectCreateProjectsCapability' => 'applications/project/capability/ProjectCreateProjectsCapability.php', + 'ProjectDefaultEditCapability' => 'applications/project/capability/ProjectDefaultEditCapability.php', + 'ProjectDefaultJoinCapability' => 'applications/project/capability/ProjectDefaultJoinCapability.php', + 'ProjectDefaultViewCapability' => 'applications/project/capability/ProjectDefaultViewCapability.php', 'ProjectQueryConduitAPIMethod' => 'applications/project/conduit/ProjectQueryConduitAPIMethod.php', 'ProjectRemarkupRule' => 'applications/project/remarkup/ProjectRemarkupRule.php', 'ProjectRemarkupRuleTestCase' => 'applications/project/remarkup/__tests__/ProjectRemarkupRuleTestCase.php', @@ -2993,7 +3010,7 @@ phutil_register_library_map(array( 'AlmanacBindingTransaction' => 'PhabricatorApplicationTransaction', 'AlmanacBindingTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'AlmanacBindingViewController' => 'AlmanacServiceController', - 'AlmanacConduitUtil' => 'Phobject', + 'AlmanacConduitAPIMethod' => 'ConduitAPIMethod', 'AlmanacConsoleController' => 'AlmanacController', 'AlmanacController' => 'PhabricatorController', 'AlmanacCoreCustomField' => array( @@ -3011,6 +3028,7 @@ phutil_register_library_map(array( 'PhabricatorCustomFieldInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorProjectInterface', + 'PhabricatorSSHPublicKeyInterface', 'AlmanacPropertyInterface', ), 'AlmanacDeviceController' => 'AlmanacController', @@ -3032,7 +3050,8 @@ phutil_register_library_map(array( 'AlmanacInterfacePHIDType' => 'PhabricatorPHIDType', 'AlmanacInterfaceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'AlmanacInterfaceTableView' => 'AphrontView', - 'AlmanacManagementRegisterWorkflow' => 'AlmanacManagementWorkflow', + 'AlmanacManagementTrustKeyWorkflow' => 'AlmanacManagementWorkflow', + 'AlmanacManagementUntrustKeyWorkflow' => 'AlmanacManagementWorkflow', 'AlmanacManagementWorkflow' => 'PhabricatorManagementWorkflow', 'AlmanacNames' => 'Phobject', 'AlmanacNamesTestCase' => 'PhabricatorTestCase', @@ -3058,6 +3077,7 @@ phutil_register_library_map(array( 'AlmanacPropertyEditController' => 'AlmanacDeviceController', 'AlmanacPropertyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'AlmanacQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'AlmanacQueryServicesConduitAPIMethod' => 'AlmanacConduitAPIMethod', 'AlmanacSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'AlmanacService' => array( 'AlmanacDAO', @@ -3305,12 +3325,13 @@ phutil_register_library_map(array( ), 'DifferentialDiffCreateController' => 'DifferentialController', 'DifferentialDiffCreationRejectException' => 'Exception', - 'DifferentialDiffEditor' => 'PhabricatorEditor', + 'DifferentialDiffEditor' => 'PhabricatorApplicationTransactionEditor', 'DifferentialDiffPHIDType' => 'PhabricatorPHIDType', 'DifferentialDiffProperty' => 'DifferentialDAO', 'DifferentialDiffQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'DifferentialDiffTableOfContentsView' => 'AphrontView', 'DifferentialDiffTestCase' => 'ArcanistPhutilTestCase', + 'DifferentialDiffTransaction' => 'PhabricatorApplicationTransaction', 'DifferentialDiffViewController' => 'DifferentialController', 'DifferentialDoorkeeperRevisionFeedStoryPublisher' => 'DoorkeeperFeedStoryPublisher', 'DifferentialDraft' => 'DifferentialDAO', @@ -3716,6 +3737,7 @@ phutil_register_library_map(array( 'FileReplyHandler' => 'PhabricatorMailReplyHandler', 'FileUploadConduitAPIMethod' => 'FileConduitAPIMethod', 'FileUploadHashConduitAPIMethod' => 'FileConduitAPIMethod', + 'FilesDefaultViewCapability' => 'PhabricatorPolicyCapability', 'FlagConduitAPIMethod' => 'ConduitAPIMethod', 'FlagDeleteConduitAPIMethod' => 'FlagConduitAPIMethod', 'FlagEditConduitAPIMethod' => 'FlagConduitAPIMethod', @@ -4072,6 +4094,7 @@ phutil_register_library_map(array( 'ManiphestTaskResultListView' => 'ManiphestView', 'ManiphestTaskSearchEngine' => 'PhabricatorApplicationSearchEngine', 'ManiphestTaskStatus' => 'ManiphestConstants', + 'ManiphestTaskStatusDatasource' => 'PhabricatorTypeaheadDatasource', 'ManiphestTaskStatusTestCase' => 'PhabricatorTestCase', 'ManiphestTaskSubscriber' => 'ManiphestDAO', 'ManiphestTransaction' => 'PhabricatorApplicationTransaction', @@ -4394,7 +4417,12 @@ phutil_register_library_map(array( 'PhabricatorAuthDAO', 'PhabricatorPolicyInterface', ), + 'PhabricatorAuthSSHKeyController' => 'PhabricatorAuthController', + 'PhabricatorAuthSSHKeyDeleteController' => 'PhabricatorAuthSSHKeyController', + 'PhabricatorAuthSSHKeyEditController' => 'PhabricatorAuthSSHKeyController', + 'PhabricatorAuthSSHKeyGenerateController' => 'PhabricatorAuthSSHKeyController', 'PhabricatorAuthSSHKeyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorAuthSSHKeyTableView' => 'AphrontView', 'PhabricatorAuthSSHPublicKey' => 'Phobject', 'PhabricatorAuthSession' => array( 'PhabricatorAuthDAO', @@ -5259,6 +5287,7 @@ phutil_register_library_map(array( 'PhabricatorProjectUIEventListener' => 'PhabricatorEventListener', 'PhabricatorProjectUpdateController' => 'PhabricatorProjectController', 'PhabricatorProjectWatchController' => 'PhabricatorProjectController', + 'PhabricatorProjectWikiExplainController' => 'PhabricatorProjectController', 'PhabricatorRecaptchaConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorRedirectController' => 'PhabricatorController', 'PhabricatorRefreshCSRFController' => 'PhabricatorAuthController', @@ -5299,6 +5328,7 @@ phutil_register_library_map(array( 'PhabricatorRepositoryDAO', 'PhabricatorPolicyInterface', 'PhabricatorFlaggableInterface', + 'PhabricatorProjectInterface', 'PhabricatorTokenReceiverInterface', 'PhabricatorSubscribableInterface', 'PhabricatorMentionableInterface', @@ -5618,6 +5648,7 @@ phutil_register_library_map(array( 'PhabricatorPolicyInterface', 'PhabricatorCustomFieldInterface', 'PhabricatorDestructibleInterface', + 'PhabricatorSSHPublicKeyInterface', ), 'PhabricatorUserBlurbField' => 'PhabricatorUserCustomField', 'PhabricatorUserConfigOptions' => 'PhabricatorApplicationConfigOptions', @@ -5657,6 +5688,8 @@ phutil_register_library_map(array( 'PhabricatorWorkerArchiveTask' => 'PhabricatorWorkerTask', 'PhabricatorWorkerDAO' => 'PhabricatorLiskDAO', 'PhabricatorWorkerLeaseQuery' => 'PhabricatorQuery', + 'PhabricatorWorkerManagementFloodWorkflow' => 'PhabricatorWorkerManagementWorkflow', + 'PhabricatorWorkerManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorWorkerPermanentFailureException' => 'Exception', 'PhabricatorWorkerTask' => 'PhabricatorWorkerDAO', 'PhabricatorWorkerTaskData' => 'PhabricatorWorkerDAO', @@ -5986,13 +6019,14 @@ phutil_register_library_map(array( 'PhabricatorFlaggableInterface', 'PhabricatorTokenReceiverInterface', 'PhabricatorDestructibleInterface', + 'PhabricatorApplicationTransactionInterface', ), 'PhrictionDocumentController' => 'PhrictionController', + 'PhrictionDocumentHeraldAdapter' => 'HeraldAdapter', 'PhrictionDocumentPHIDType' => 'PhabricatorPHIDType', 'PhrictionDocumentPreviewController' => 'PhrictionController', 'PhrictionDocumentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhrictionDocumentStatus' => 'PhrictionConstants', - 'PhrictionDocumentTestCase' => 'PhabricatorTestCase', 'PhrictionEditConduitAPIMethod' => 'PhrictionConduitAPIMethod', 'PhrictionEditController' => 'PhrictionController', 'PhrictionHistoryConduitAPIMethod' => 'PhrictionConduitAPIMethod', @@ -6073,6 +6107,9 @@ phutil_register_library_map(array( 'ProjectConduitAPIMethod' => 'ConduitAPIMethod', 'ProjectCreateConduitAPIMethod' => 'ProjectConduitAPIMethod', 'ProjectCreateProjectsCapability' => 'PhabricatorPolicyCapability', + 'ProjectDefaultEditCapability' => 'PhabricatorPolicyCapability', + 'ProjectDefaultJoinCapability' => 'PhabricatorPolicyCapability', + 'ProjectDefaultViewCapability' => 'PhabricatorPolicyCapability', 'ProjectQueryConduitAPIMethod' => 'ProjectConduitAPIMethod', 'ProjectRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'ProjectRemarkupRuleTestCase' => 'PhabricatorTestCase', diff --git a/src/applications/almanac/conduit/AlmanacConduitAPIMethod.php b/src/applications/almanac/conduit/AlmanacConduitAPIMethod.php new file mode 100644 index 0000000000..fc3e8811c4 --- /dev/null +++ b/src/applications/almanac/conduit/AlmanacConduitAPIMethod.php @@ -0,0 +1,20 @@ + 'optional list', + 'phids' => 'optional list', + 'names' => 'optional list', + ) + self::getPagerParamTypes(); + } + + public function defineReturnType() { + return 'list'; + } + + public function defineErrorTypes() { + return array(); + } + + protected function execute(ConduitAPIRequest $request) { + $viewer = $request->getUser(); + + $query = id(new AlmanacServiceQuery()) + ->setViewer($viewer); + + $ids = $request->getValue('ids'); + if ($ids !== null) { + $query->withIDs($ids); + } + + $phids = $request->getValue('phids'); + if ($phids !== null) { + $query->withPHIDs($phids); + } + + $names = $request->getValue('names'); + if ($names !== null) { + $query->withNames($names); + } + + $pager = $this->newPager($request); + + $services = $query->executeWithCursorPager($pager); + + if ($services) { + $bindings = id(new AlmanacBindingQuery()) + ->setViewer($viewer) + ->withServicePHIDs(mpull($services, 'getPHID')) + ->execute(); + $bindings = mgroup($bindings, 'getServicePHID'); + } else { + $bindings = array(); + } + + $data = array(); + foreach ($services as $service) { + $phid = $service->getPHID(); + + $properties = $service->getAlmanacProperties(); + $properties = mpull($properties, 'getFieldValue', 'getFieldName'); + + $service_bindings = idx($bindings, $phid, array()); + $service_bindings = array_values($service_bindings); + foreach ($service_bindings as $key => $service_binding) { + $service_bindings[$key] = $this->getBindingDictionary($service_binding); + } + + $data[] = $this->getServiceDictionary($service) + array( + 'bindings' => $service_bindings, + ); + } + + $results = array( + 'data' => $data, + ); + + return $this->addPagerResults($results, $pager); + } + + private function getServiceDictionary(AlmanacService $service) { + return array( + 'id' => (int)$service->getID(), + 'phid' => $service->getPHID(), + 'name' => $service->getName(), + 'uri' => PhabricatorEnv::getProductionURI($service->getURI()), + 'properties' => $this->getPropertiesDictionary($service), + ); + } + + private function getBindingDictionary(AlmanacBinding $binding) { + return array( + 'id' => (int)$binding->getID(), + 'phid' => $binding->getPHID(), + 'properties' => $this->getPropertiesDictionary($binding), + 'interface' => $this->getInterfaceDictionary($binding->getInterface()), + ); + } + + private function getPropertiesDictionary(AlmanacPropertyInterface $obj) { + $properties = $obj->getAlmanacProperties(); + return (object)mpull($properties, 'getFieldValue', 'getFieldName'); + } + + private function getInterfaceDictionary(AlmanacInterface $interface) { + return array( + 'id' => (int)$interface->getID(), + 'phid' => $interface->getPHID(), + 'address' => $interface->getAddress(), + 'port' => (int)$interface->getPort(), + 'device' => $this->getDeviceDictionary($interface->getDevice()), + 'network' => $this->getNetworkDictionary($interface->getNetwork()), + ); + } + + private function getDeviceDictionary(AlmanacDevice $device) { + return array( + 'id' => (int)$device->getID(), + 'phid' => $device->getPHID(), + 'name' => $device->getName(), + 'properties' => $this->getPropertiesDictionary($device), + ); + } + + private function getNetworkDictionary(AlmanacNetwork $network) { + return array( + 'id' => (int)$network->getID(), + 'phid' => $network->getPHID(), + 'name' => $network->getName(), + ); + } + +} diff --git a/src/applications/almanac/controller/AlmanacDeviceViewController.php b/src/applications/almanac/controller/AlmanacDeviceViewController.php index 799494963a..d6ebfa1260 100644 --- a/src/applications/almanac/controller/AlmanacDeviceViewController.php +++ b/src/applications/almanac/controller/AlmanacDeviceViewController.php @@ -57,6 +57,7 @@ final class AlmanacDeviceViewController $box, $interfaces, $this->buildAlmanacPropertiesTable($device), + $this->buildSSHKeysTable($device), $xaction_view, ), array( @@ -141,4 +142,67 @@ final class AlmanacDeviceViewController ->appendChild($table); } + private function buildSSHKeysTable(AlmanacDevice $device) { + $viewer = $this->getViewer(); + $id = $device->getID(); + $device_phid = $device->getPHID(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $device, + PhabricatorPolicyCapability::CAN_EDIT); + + $keys = id(new PhabricatorAuthSSHKeyQuery()) + ->setViewer($viewer) + ->withObjectPHIDs(array($device_phid)) + ->execute(); + + $table = id(new PhabricatorAuthSSHKeyTableView()) + ->setUser($viewer) + ->setKeys($keys) + ->setCanEdit($can_edit) + ->setShowID(true) + ->setShowTrusted(true) + ->setNoDataString(pht('This device has no associated SSH public keys.')); + + try { + PhabricatorSSHKeyGenerator::assertCanGenerateKeypair(); + $can_generate = true; + } catch (Exception $ex) { + $can_generate = false; + } + + $generate_uri = '/auth/sshkey/generate/?objectPHID='.$device_phid; + $upload_uri = '/auth/sshkey/upload/?objectPHID='.$device_phid; + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('SSH Public Keys')) + ->addActionLink( + id(new PHUIButtonView()) + ->setTag('a') + ->setHref($generate_uri) + ->setWorkflow(true) + ->setDisabled(!$can_edit || !$can_generate) + ->setText(pht('Generate Keypair')) + ->setIcon( + id(new PHUIIconView()) + ->setIconFont('fa-lock'))) + ->addActionLink( + id(new PHUIButtonView()) + ->setTag('a') + ->setHref($upload_uri) + ->setWorkflow(true) + ->setDisabled(!$can_edit) + ->setText(pht('Upload Public Key')) + ->setIcon( + id(new PHUIIconView()) + ->setIconFont('fa-upload'))); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->appendChild($table); + + + } + } diff --git a/src/applications/almanac/management/AlmanacManagementRegisterWorkflow.php b/src/applications/almanac/management/AlmanacManagementRegisterWorkflow.php deleted file mode 100644 index f48f3e8325..0000000000 --- a/src/applications/almanac/management/AlmanacManagementRegisterWorkflow.php +++ /dev/null @@ -1,64 +0,0 @@ -setName('register') - ->setSynopsis(pht('Register this host for authorized Conduit access.')) - ->setArguments(array()); - } - - public function execute(PhutilArgumentParser $args) { - $console = PhutilConsole::getConsole(); - - if (Filesystem::pathExists(AlmanacConduitUtil::getHostPrivateKeyPath())) { - throw new Exception( - 'This host already has a private key for Conduit access.'); - } - - $pair = PhabricatorSSHKeyGenerator::generateKeypair(); - list($public_key, $private_key) = $pair; - - $host = id(new AlmanacDevice()) - ->setName(php_uname('n')) - ->save(); - - id(new AlmanacProperty()) - ->setObjectPHID($host->getPHID()) - ->setName('conduitPublicOpenSSHKey') - ->setValue($public_key) - ->save(); - - id(new AlmanacProperty()) - ->setObjectPHID($host->getPHID()) - ->setName('conduitPublicOpenSSLKey') - ->setValue($this->convertToOpenSSLPublicKey($public_key)) - ->save(); - - Filesystem::writeFile( - AlmanacConduitUtil::getHostPrivateKeyPath(), - $private_key); - - Filesystem::writeFile( - AlmanacConduitUtil::getHostIDPath(), - $host->getID()); - - $console->writeOut("Registered as device %d.\n", $host->getID()); - } - - private function convertToOpenSSLPublicKey($openssh_public_key) { - $ssh_public_key_file = new TempFile(); - Filesystem::writeFile($ssh_public_key_file, $openssh_public_key); - - list($public_key, $stderr) = id(new ExecFuture( - 'ssh-keygen -e -f %s -m pkcs8', - $ssh_public_key_file))->resolvex(); - - unset($ssh_public_key_file); - - return $public_key; - } - -} diff --git a/src/applications/almanac/management/AlmanacManagementTrustKeyWorkflow.php b/src/applications/almanac/management/AlmanacManagementTrustKeyWorkflow.php new file mode 100644 index 0000000000..952d9c71e5 --- /dev/null +++ b/src/applications/almanac/management/AlmanacManagementTrustKeyWorkflow.php @@ -0,0 +1,85 @@ +setName('trust-key') + ->setSynopsis(pht('Mark a public key as trusted.')) + ->setArguments( + array( + array( + 'name' => 'id', + 'param' => 'id', + 'help' => pht('ID of the key to trust.'), + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $console = PhutilConsole::getConsole(); + + $id = $args->getArg('id'); + if (!$id) { + throw new PhutilArgumentUsageException( + pht('Specify a public key to trust with --id.')); + } + + $key = id(new PhabricatorAuthSSHKeyQuery()) + ->setViewer($this->getViewer()) + ->withIDs(array($id)) + ->executeOne(); + if (!$key) { + throw new PhutilArgumentUsageException( + pht('No public key exists with ID "%s".', $id)); + } + + if ($key->getIsTrusted()) { + throw new PhutilArgumentUsageException( + pht('Public key with ID %s is already trusted.', $id)); + } + + if (!($key->getObject() instanceof AlmanacDevice)) { + throw new PhutilArgumentUsageException( + pht('You can only trust keys associated with Almanac devices.')); + } + + $handle = id(new PhabricatorHandleQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs(array($key->getObject()->getPHID())) + ->executeOne(); + + $console->writeOut( + "** %s **\n\n%s\n\n%s\n\n%s", + pht('IMPORTANT!'), + phutil_console_wrap( + pht( + 'Trusting a public key gives anyone holding the corresponding '. + 'private key complete, unrestricted access to all data in '. + 'Phabricator. The private key will be able to sign requests that '. + 'skip policy and security checks.')), + phutil_console_wrap( + pht( + 'This is an advanced feature which should normally be used only '. + 'when building a Phabricator cluster. This feature is very '. + 'dangerous if misused.')), + pht('This key is associated with device "%s".', $handle->getName())); + + $prompt = pht( + 'Really trust this key?'); + if (!phutil_console_confirm($prompt)) { + throw new PhutilArgumentUsageException( + pht('User aborted workflow.')); + } + + $key->setIsTrusted(1); + $key->save(); + + $console->writeOut( + "** %s ** %s\n", + pht('TRUSTED'), + pht('Key %s has been marked as trusted.', $id)); + } + +} diff --git a/src/applications/almanac/management/AlmanacManagementUntrustKeyWorkflow.php b/src/applications/almanac/management/AlmanacManagementUntrustKeyWorkflow.php new file mode 100644 index 0000000000..0c489605f0 --- /dev/null +++ b/src/applications/almanac/management/AlmanacManagementUntrustKeyWorkflow.php @@ -0,0 +1,52 @@ +setName('untrust-key') + ->setSynopsis(pht('Revoke trust of a public key.')) + ->setArguments( + array( + array( + 'name' => 'id', + 'param' => 'id', + 'help' => pht('ID of the key to revoke trust for.'), + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $console = PhutilConsole::getConsole(); + + $id = $args->getArg('id'); + if (!$id) { + throw new PhutilArgumentUsageException( + pht('Specify a public key to revoke trust for with --id.')); + } + + $key = id(new PhabricatorAuthSSHKeyQuery()) + ->setViewer($this->getViewer()) + ->withIDs(array($id)) + ->executeOne(); + if (!$key) { + throw new PhutilArgumentUsageException( + pht('No public key exists with ID "%s".', $id)); + } + + if (!$key->getIsTrusted()) { + throw new PhutilArgumentUsageException( + pht('Public key with ID %s is not trusted.', $id)); + } + + $key->setIsTrusted(0); + $key->save(); + + $console->writeOut( + "** %s ** %s\n", + pht('TRUST REVOKED'), + pht('Trust has been revoked for public key %s.', $id)); + } + +} diff --git a/src/applications/almanac/storage/AlmanacBinding.php b/src/applications/almanac/storage/AlmanacBinding.php index 8294817da9..75d78a24aa 100644 --- a/src/applications/almanac/storage/AlmanacBinding.php +++ b/src/applications/almanac/storage/AlmanacBinding.php @@ -21,7 +21,8 @@ final class AlmanacBinding public static function initializeNewBinding(AlmanacService $service) { return id(new AlmanacBinding()) - ->setServicePHID($service->getPHID()); + ->setServicePHID($service->getPHID()) + ->attachAlmanacProperties(array()); } public function getConfiguration() { diff --git a/src/applications/almanac/storage/AlmanacDevice.php b/src/applications/almanac/storage/AlmanacDevice.php index b7163389ba..9075c1fd7e 100644 --- a/src/applications/almanac/storage/AlmanacDevice.php +++ b/src/applications/almanac/storage/AlmanacDevice.php @@ -7,6 +7,7 @@ final class AlmanacDevice PhabricatorCustomFieldInterface, PhabricatorApplicationTransactionInterface, PhabricatorProjectInterface, + PhabricatorSSHPublicKeyInterface, AlmanacPropertyInterface { protected $name; @@ -21,7 +22,8 @@ final class AlmanacDevice public static function initializeNewDevice() { return id(new AlmanacDevice()) ->setViewPolicy(PhabricatorPolicies::POLICY_USER) - ->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN); + ->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN) + ->attachAlmanacProperties(array()); } public function getConfiguration() { @@ -160,4 +162,17 @@ final class AlmanacDevice return new AlmanacDeviceTransaction(); } + +/* -( PhabricatorSSHPublicKeyInterface )----------------------------------- */ + + + public function getSSHPublicKeyManagementURI(PhabricatorUser $viewer) { + return $this->getURI(); + } + + public function getSSHKeyDefaultName() { + return $this->getName(); + } + + } diff --git a/src/applications/almanac/storage/AlmanacService.php b/src/applications/almanac/storage/AlmanacService.php index ecb062809f..0d15afb15e 100644 --- a/src/applications/almanac/storage/AlmanacService.php +++ b/src/applications/almanac/storage/AlmanacService.php @@ -21,7 +21,8 @@ final class AlmanacService public static function initializeNewService() { return id(new AlmanacService()) ->setViewPolicy(PhabricatorPolicies::POLICY_USER) - ->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN); + ->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN) + ->attachAlmanacProperties(array()); } public function getConfiguration() { diff --git a/src/applications/almanac/util/AlmanacConduitUtil.php b/src/applications/almanac/util/AlmanacConduitUtil.php deleted file mode 100644 index 5b55f5fa26..0000000000 --- a/src/applications/almanac/util/AlmanacConduitUtil.php +++ /dev/null @@ -1,17 +0,0 @@ - 'PhabricatorAuthDowngradeSessionController', 'multifactor/' => 'PhabricatorAuthNeedsMultiFactorController', + 'sshkey/' => array( + 'generate/' => 'PhabricatorAuthSSHKeyGenerateController', + 'upload/' => 'PhabricatorAuthSSHKeyEditController', + 'edit/(?P\d+)/' => 'PhabricatorAuthSSHKeyEditController', + 'delete/(?P\d+)/' => 'PhabricatorAuthSSHKeyDeleteController', + ), ), '/oauth/(?P\w+)/login/' diff --git a/src/applications/auth/controller/PhabricatorAuthSSHKeyController.php b/src/applications/auth/controller/PhabricatorAuthSSHKeyController.php new file mode 100644 index 0000000000..86cf81778f --- /dev/null +++ b/src/applications/auth/controller/PhabricatorAuthSSHKeyController.php @@ -0,0 +1,33 @@ +getViewer(); + + $object = id(new PhabricatorObjectQuery()) + ->setViewer($viewer) + ->withPHIDs(array($object_phid)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$object) { + return null; + } + + // If this kind of object can't have SSH keys, don't let the viewer + // add them. + if (!($object instanceof PhabricatorSSHPublicKeyInterface)) { + return null; + } + + return id(new PhabricatorAuthSSHKey()) + ->setObjectPHID($object_phid) + ->attachObject($object); + } + +} diff --git a/src/applications/auth/controller/PhabricatorAuthSSHKeyDeleteController.php b/src/applications/auth/controller/PhabricatorAuthSSHKeyDeleteController.php new file mode 100644 index 0000000000..6c18e211cc --- /dev/null +++ b/src/applications/auth/controller/PhabricatorAuthSSHKeyDeleteController.php @@ -0,0 +1,48 @@ +getViewer(); + + $key = id(new PhabricatorAuthSSHKeyQuery()) + ->setViewer($viewer) + ->withIDs(array($request->getURIData('id'))) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$key) { + return new Aphront404Response(); + } + + $cancel_uri = $key->getObject()->getSSHPublicKeyManagementURI($viewer); + + $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( + $viewer, + $request, + $cancel_uri); + + if ($request->isFormPost()) { + // TODO: It would be nice to write an edge transaction here or something. + $key->delete(); + return id(new AphrontRedirectResponse())->setURI($cancel_uri); + } + + $name = phutil_tag('strong', array(), $key->getName()); + + return $this->newDialog() + ->setTitle(pht('Really delete SSH Public Key?')) + ->appendParagraph( + pht( + 'The key "%s" will be permanently deleted, and you will not longer '. + 'be able to use the corresponding private key to authenticate.', + $name)) + ->addSubmitButton(pht('Delete Public Key')) + ->addCancelButton($cancel_uri); + } + +} diff --git a/src/applications/auth/controller/PhabricatorAuthSSHKeyEditController.php b/src/applications/auth/controller/PhabricatorAuthSSHKeyEditController.php new file mode 100644 index 0000000000..bb4acd0b4d --- /dev/null +++ b/src/applications/auth/controller/PhabricatorAuthSSHKeyEditController.php @@ -0,0 +1,143 @@ +getViewer(); + + $id = $request->getURIData('id'); + if ($id) { + $key = id(new PhabricatorAuthSSHKeyQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$key) { + return new Aphront404Response(); + } + + $is_new = false; + } else { + $key = $this->newKeyForObjectPHID($request->getStr('objectPHID')); + if (!$key) { + return new Aphront404Response(); + } + $is_new = true; + } + + $cancel_uri = $key->getObject()->getSSHPublicKeyManagementURI($viewer); + + if ($key->getIsTrusted()) { + $id = $key->getID(); + + return $this->newDialog() + ->setTitle(pht('Can Not Edit Trusted Key')) + ->appendParagraph( + pht( + 'This key is trusted. Trusted keys can not be edited. '. + 'Use %s to revoke trust before editing the key.', + phutil_tag( + 'tt', + array(), + "bin/almanac untrust-key --id {$id}"))) + ->addCancelButton($cancel_uri, pht('Okay')); + } + + $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( + $viewer, + $request, + $cancel_uri); + + $v_name = $key->getName(); + $e_name = strlen($v_name) ? null : true; + + $v_key = $key->getEntireKey(); + $e_key = strlen($v_key) ? null : true; + + $errors = array(); + if ($request->isFormPost()) { + $v_name = $request->getStr('name'); + $v_key = $request->getStr('key'); + + if (!strlen($v_name)) { + $errors[] = pht('You must provide a name for this public key.'); + $e_name = pht('Required'); + } else { + $key->setName($v_name); + } + + if (!strlen($v_key)) { + $errors[] = pht('You must provide a public key.'); + $e_key = pht('Required'); + } else { + try { + $public_key = PhabricatorAuthSSHPublicKey::newFromRawKey($v_key); + + $type = $public_key->getType(); + $body = $public_key->getBody(); + $comment = $public_key->getComment(); + + $key->setKeyType($type); + $key->setKeyBody($body); + $key->setKeyComment($comment); + + $e_key = null; + } catch (Exception $ex) { + $e_key = pht('Invalid'); + $errors[] = $ex->getMessage(); + } + } + + if (!$errors) { + try { + $key->save(); + return id(new AphrontRedirectResponse())->setURI($cancel_uri); + } catch (Exception $ex) { + $e_key = pht('Duplicate'); + $errors[] = pht( + 'This public key is already associated with another user or '. + 'device. Each key must unambiguously identify a single unique '. + 'owner.'); + } + } + } + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel(pht('Name')) + ->setName('name') + ->setError($e_name) + ->setValue($v_name)) + ->appendChild( + id(new AphrontFormTextAreaControl()) + ->setLabel(pht('Public Key')) + ->setName('key') + ->setValue($v_key) + ->setError($e_key)); + + if ($is_new) { + $title = pht('Upload SSH Public Key'); + $save_button = pht('Upload Public Key'); + $form->addHiddenInput('objectPHID', $key->getObject()->getPHID()); + } else { + $title = pht('Edit SSH Public Key'); + $save_button = pht('Save Changes'); + } + + return $this->newDialog() + ->setTitle($title) + ->setWidth(AphrontDialogView::WIDTH_FORM) + ->setErrors($errors) + ->appendForm($form) + ->addSubmitButton($save_button) + ->addCancelButton($cancel_uri); + } + +} diff --git a/src/applications/auth/controller/PhabricatorAuthSSHKeyGenerateController.php b/src/applications/auth/controller/PhabricatorAuthSSHKeyGenerateController.php new file mode 100644 index 0000000000..d055db514a --- /dev/null +++ b/src/applications/auth/controller/PhabricatorAuthSSHKeyGenerateController.php @@ -0,0 +1,92 @@ +getViewer(); + + $key = $this->newKeyForObjectPHID($request->getStr('objectPHID')); + if (!$key) { + return new Aphront404Response(); + } + + $cancel_uri = $key->getObject()->getSSHPublicKeyManagementURI($viewer); + + $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( + $viewer, + $request, + $cancel_uri); + + if ($request->isFormPost()) { + $default_name = $key->getObject()->getSSHKeyDefaultName(); + + $keys = PhabricatorSSHKeyGenerator::generateKeypair(); + list($public_key, $private_key) = $keys; + + $file = PhabricatorFile::buildFromFileDataOrHash( + $private_key, + array( + 'name' => $default_name.'.key', + 'ttl' => time() + (60 * 10), + 'viewPolicy' => $viewer->getPHID(), + )); + + $public_key = PhabricatorAuthSSHPublicKey::newFromRawKey($public_key); + + $type = $public_key->getType(); + $body = $public_key->getBody(); + + $key + ->setName($default_name) + ->setKeyType($type) + ->setKeyBody($body) + ->setKeyComment(pht('Generated')) + ->save(); + + // NOTE: We're disabling workflow on submit so the download works. We're + // disabling workflow on cancel so the page reloads, showing the new + // key. + + return $this->newDialog() + ->setTitle(pht('Download Private Key')) + ->setDisableWorkflowOnCancel(true) + ->setDisableWorkflowOnSubmit(true) + ->setSubmitURI($file->getDownloadURI()) + ->appendParagraph( + pht( + 'A keypair has been generated, and the public key has been '. + 'added as a recognized key. Use the button below to download '. + 'the private key.')) + ->appendParagraph( + pht( + 'After you download the private key, it will be destroyed. '. + 'You will not be able to retrieve it if you lose your copy.')) + ->addSubmitButton(pht('Download Private Key')) + ->addCancelButton($cancel_uri, pht('Done')); + } + + try { + PhabricatorSSHKeyGenerator::assertCanGenerateKeypair(); + + return $this->newDialog() + ->setTitle(pht('Generate New Keypair')) + ->addHiddenInput('objectPHID', $key->getObject()->getPHID()) + ->appendParagraph( + pht( + 'This workflow will generate a new SSH keypair, add the public '. + 'key, and let you download the private key.')) + ->appendParagraph( + pht( + 'Phabricator will not retain a copy of the private key.')) + ->addSubmitButton(pht('Generate New Keypair')) + ->addCancelButton($cancel_uri); + } catch (Exception $ex) { + return $this->newDialog() + ->setTitle(pht('Unable to Generate Keys')) + ->appendParagraph($ex->getMessage()) + ->addCancelButton($cancel_uri); + } + } + +} diff --git a/src/applications/auth/query/PhabricatorAuthSSHKeyQuery.php b/src/applications/auth/query/PhabricatorAuthSSHKeyQuery.php index d2963234f7..4a14456f35 100644 --- a/src/applications/auth/query/PhabricatorAuthSSHKeyQuery.php +++ b/src/applications/auth/query/PhabricatorAuthSSHKeyQuery.php @@ -50,10 +50,14 @@ final class PhabricatorAuthSSHKeyQuery foreach ($keys as $key => $ssh_key) { $object = idx($objects, $ssh_key->getObjectPHID()); - if (!$object) { + + // We must have an object, and that object must be a valid object for + // SSH keys. + if (!$object || !($object instanceof PhabricatorSSHPublicKeyInterface)) { unset($keys[$key]); continue; } + $ssh_key->attachObject($object); } diff --git a/src/applications/auth/storage/PhabricatorAuthSSHPublicKey.php b/src/applications/auth/sshkey/PhabricatorAuthSSHPublicKey.php similarity index 84% rename from src/applications/auth/storage/PhabricatorAuthSSHPublicKey.php rename to src/applications/auth/sshkey/PhabricatorAuthSSHPublicKey.php index 6ed5ff20df..8d14a14c6e 100644 --- a/src/applications/auth/storage/PhabricatorAuthSSHPublicKey.php +++ b/src/applications/auth/sshkey/PhabricatorAuthSSHPublicKey.php @@ -99,4 +99,26 @@ final class PhabricatorAuthSSHPublicKey extends Phobject { return PhabricatorHash::digestForIndex($body); } + public function getEntireKey() { + $key = $this->type.' '.$this->body; + if (strlen($this->comment)) { + $key = $key.' '.$this->comment; + } + return $key; + } + + public function toPKCS8() { + + // TODO: Put a cache in front of this. + + $tmp = new TempFile(); + Filesystem::writeFile($tmp, $this->getEntireKey()); + list($pem_key) = execx( + 'ssh-keygen -e -m PKCS8 -f %s', + $tmp); + unset($tmp); + + return $pem_key; + } + } diff --git a/src/applications/auth/sshkey/PhabricatorSSHPublicKeyInterface.php b/src/applications/auth/sshkey/PhabricatorSSHPublicKeyInterface.php new file mode 100644 index 0000000000..87d862bd62 --- /dev/null +++ b/src/applications/auth/sshkey/PhabricatorSSHPublicKeyInterface.php @@ -0,0 +1,20 @@ + 'bytes12', 'keyBody' => 'text', 'keyComment' => 'text255', + 'isTrusted' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_object' => array( @@ -56,7 +58,7 @@ final class PhabricatorAuthSSHKey return $this->assertAttached($this->object); } - public function attachObject($object) { + public function attachObject(PhabricatorSSHPublicKeyInterface $object) { $this->object = $object; return $this; } diff --git a/src/applications/auth/view/PhabricatorAuthSSHKeyTableView.php b/src/applications/auth/view/PhabricatorAuthSSHKeyTableView.php new file mode 100644 index 0000000000..ee7f5756ae --- /dev/null +++ b/src/applications/auth/view/PhabricatorAuthSSHKeyTableView.php @@ -0,0 +1,110 @@ +noDataString = $no_data_string; + return $this; + } + + public function setCanEdit($can_edit) { + $this->canEdit = $can_edit; + return $this; + } + + public function setShowTrusted($show_trusted) { + $this->showTrusted = $show_trusted; + return $this; + } + + public function setShowID($show_id) { + $this->showID = $show_id; + return $this; + } + + public function setKeys(array $keys) { + assert_instances_of($keys, 'PhabricatorAuthSSHKey'); + $this->keys = $keys; + return $this; + } + + public function render() { + $keys = $this->keys; + $viewer = $this->getUser(); + + if ($this->canEdit) { + $delete_class = 'small grey button'; + } else { + $delete_class = 'small grey button disabled'; + } + + $trusted_icon = id(new PHUIIconView()) + ->setIconFont('fa-star blue'); + $untrusted_icon = id(new PHUIIconView()) + ->setIconFont('fa-times grey'); + + $rows = array(); + foreach ($keys as $key) { + $rows[] = array( + $key->getID(), + javelin_tag( + 'a', + array( + 'href' => '/auth/sshkey/edit/'.$key->getID().'/', + 'sigil' => 'workflow', + ), + $key->getName()), + $key->getIsTrusted() ? $trusted_icon : $untrusted_icon, + $key->getKeyComment(), + $key->getKeyType(), + phabricator_datetime($key->getDateCreated(), $viewer), + javelin_tag( + 'a', + array( + 'href' => '/auth/sshkey/delete/'.$key->getID().'/', + 'class' => $delete_class, + 'sigil' => 'workflow', + ), + pht('Delete')), + ); + } + + $table = id(new AphrontTableView($rows)) + ->setNoDataString($this->noDataString) + ->setHeaders( + array( + pht('ID'), + pht('Name'), + pht('Trusted'), + pht('Comment'), + pht('Type'), + pht('Added'), + null, + )) + ->setColumnVisibility( + array( + $this->showID, + true, + $this->showTrusted, + )) + ->setColumnClasses( + array( + '', + 'wide pri', + 'center', + '', + '', + 'right', + 'action', + )); + + return $table; + } + +} diff --git a/src/applications/base/controller/PhabricatorController.php b/src/applications/base/controller/PhabricatorController.php index a8d8f32cb8..24ca453f44 100644 --- a/src/applications/base/controller/PhabricatorController.php +++ b/src/applications/base/controller/PhabricatorController.php @@ -526,4 +526,39 @@ abstract class PhabricatorController extends AphrontController { ->setSubmitURI($submit_uri); } + protected function buildTransactionTimeline( + PhabricatorApplicationTransactionInterface $object, + PhabricatorApplicationTransactionQuery $query, + PhabricatorMarkupEngine $engine = null) { + + $viewer = $this->getRequest()->getUser(); + $xaction = $object->getApplicationTransactionTemplate(); + $view = $xaction->getApplicationTransactionViewObject(); + + $xactions = $query + ->setViewer($viewer) + ->withObjectPHIDs(array($object->getPHID())) + ->needComments(true) + ->execute(); + + if ($engine) { + foreach ($xactions as $xaction) { + if ($xaction->getComment()) { + $engine->addObject( + $xaction->getComment(), + PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT); + } + } + $engine->process(); + $view->setMarkupEngine($engine); + } + + $timeline = $view + ->setUser($viewer) + ->setObjectPHID($object->getPHID()) + ->setTransactions($xactions); + + return $timeline; + } + } diff --git a/src/applications/conduit/controller/PhabricatorConduitAPIController.php b/src/applications/conduit/controller/PhabricatorConduitAPIController.php index 1a6080b0fe..dfacbb8058 100644 --- a/src/applications/conduit/controller/PhabricatorConduitAPIController.php +++ b/src/applications/conduit/controller/PhabricatorConduitAPIController.php @@ -209,6 +209,93 @@ final class PhabricatorConduitAPIController $request->getUser()); } + $auth_type = idx($metadata, 'auth.type'); + if ($auth_type === ConduitClient::AUTH_ASYMMETRIC) { + $host = idx($metadata, 'auth.host'); + if (!$host) { + return array( + 'ERR-INVALID-AUTH', + pht( + 'Request is missing required "auth.host" parameter.'), + ); + } + + // TODO: Validate that we are the host! + + $raw_key = idx($metadata, 'auth.key'); + $public_key = PhabricatorAuthSSHPublicKey::newFromRawKey($raw_key); + $ssl_public_key = $public_key->toPKCS8(); + + // First, verify the signature. + try { + $protocol_data = $metadata; + + // TODO: We should stop writing this into the protocol data when + // processing a request. + unset($protocol_data['scope']); + + ConduitClient::verifySignature( + $this->method, + $api_request->getAllParameters(), + $protocol_data, + $ssl_public_key); + } catch (Exception $ex) { + return array( + 'ERR-INVALID-AUTH', + pht( + 'Signature verification failure. %s', + $ex->getMessage()), + ); + } + + // If the signature is valid, find the user or device which is + // associated with this public key. + + $stored_key = id(new PhabricatorAuthSSHKeyQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withKeys(array($public_key)) + ->executeOne(); + if (!$stored_key) { + return array( + 'ERR-INVALID-AUTH', + pht( + 'No user or device is associated with that public key.'), + ); + } + + $object = $stored_key->getObject(); + + if ($object instanceof PhabricatorUser) { + $user = $object; + } else { + if (!$stored_key->getIsTrusted()) { + return array( + 'ERR-INVALID-AUTH', + pht( + 'The key which signed this request is not trusted. Only '. + 'trusted keys can be used to sign API calls.'), + ); + } + + throw new Exception( + pht('Not Implemented: Would authenticate Almanac device.')); + } + + return $this->validateAuthenticatedUser( + $api_request, + $user); + } else if ($auth_type === null) { + // No specified authentication type, continue with other authentication + // methods below. + } else { + return array( + 'ERR-INVALID-AUTH', + pht( + 'Provided "auth.type" ("%s") is not recognized.', + $auth_type), + ); + } + // handle oauth $access_token = $request->getStr('access_token'); $method_scope = $metadata['scope']; diff --git a/src/applications/config/check/PhabricatorSetupCheckBinaries.php b/src/applications/config/check/PhabricatorSetupCheckBinaries.php index b5bc79d808..cea3576e19 100644 --- a/src/applications/config/check/PhabricatorSetupCheckBinaries.php +++ b/src/applications/config/check/PhabricatorSetupCheckBinaries.php @@ -92,6 +92,53 @@ final class PhabricatorSetupCheckBinaries extends PhabricatorSetupCheck { 'version control system. It will not work without the VCS binary.'); $this->raiseWarning($binary, $message); } + + switch ($binary) { + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: + $minimum_version = null; + $bad_versions = array(); + list($err, $stdout, $stderr) = exec_manual('git --version'); + $version = trim(substr($stdout, strlen('git version '))); + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: + $minimum_version = null; + $bad_versions = array( + '1.7.1' => pht('This version of Subversion has a bug where '. + '"svn diff -c N" does not work for files added '. + 'in rN (Subverison issue #2873), fixed in 1.7.2.'),); + list($err, $stdout, $stderr) = exec_manual('svn --version --quiet'); + $version = trim($stdout); + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: + $minimum_version = '1.9'; + $bad_versions = array( + '2.1' => pht('This version of Mercurial returns a bad exit code '. + 'after a successful pull.'), + '2.2' => pht('This version of Mercurial has a significant memory '. + 'leak, fixed in 2.2.1. Pushing fails with this '. + 'version as well; see T3046#54922.'),); + list($err, $stdout, $stderr) = exec_manual('hg --version --quiet'); + $version = rtrim( + substr($stdout, strlen('Mercurial Distributed SCM (version ')), + ")\n"); + break; + } + + if ($minimum_version && + version_compare($version, $minimum_version, '<')) { + $this->raiseMinimumVersionWarning( + $binary, + $minimum_version, + $version); + } + + foreach ($bad_versions as $bad_version => $details) { + if ($bad_version === $version) { + $this->raiseBadVersionWarning( + $binary, + $bad_version); + } + } } } @@ -126,4 +173,61 @@ final class PhabricatorSetupCheckBinaries extends PhabricatorSetupCheck { ->addPhabricatorConfig('environment.append-paths'); } + private function raiseMinimumVersionWarning( + $binary, + $minimum_version, + $version) { + + switch ($binary) { + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: + $summary = pht( + "The '%s' binary is version %s and Phabricator requires version ". + "%s or higher.", + $binary, + $version, + $minimum_version); + $message = pht( + "Please upgrade the '%s' binary to a more modern version.", + $binary); + $this->newIssue('bin.'.$binary) + ->setShortName(pht("Unsupported '%s' Version", $binary)) + ->setName(pht("Unsupported '%s' Version", $binary)) + ->setSummary($summary) + ->setMessage($summary.' '.$message); + break; + } + + + } + + private function raiseBadVersionWarning($binary, $bad_version) { + + switch ($binary) { + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: + case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: + $summary = pht( + "The '%s' binary is version %s which has bugs that break ". + "Phabricator.", + $binary, + $bad_version); + $message = pht( + "Please upgrade the '%s' binary to a more modern version.", + $binary); + $this->newIssue('bin.'.$binary) + ->setShortName(pht("Unsupported '%s' Version", $binary)) + ->setName(pht("Unsupported '%s' Version", $binary)) + ->setSummary($summary) + ->setMessage($summary.' '.$message); + break; + } + + + } + } diff --git a/src/applications/config/check/PhabricatorSetupCheckMySQL.php b/src/applications/config/check/PhabricatorSetupCheckMySQL.php index aec1b6ab23..9ea3ca6f6d 100644 --- a/src/applications/config/check/PhabricatorSetupCheckMySQL.php +++ b/src/applications/config/check/PhabricatorSetupCheckMySQL.php @@ -34,6 +34,7 @@ final class PhabricatorSetupCheckMySQL extends PhabricatorSetupCheck { $modes = self::loadRawConfigValue('sql_mode'); $modes = explode(',', $modes); + if (!in_array('STRICT_ALL_TABLES', $modes)) { $summary = pht( 'MySQL is not in strict mode, but using strict mode is strongly '. @@ -67,6 +68,45 @@ final class PhabricatorSetupCheckMySQL extends PhabricatorSetupCheck { ->setMessage($message) ->addMySQLConfig('sql_mode'); } + if (in_array('ONLY_FULL_GROUP_BY', $modes)) { + $summary = pht( + 'MySQL is in ONLY_FULL_GROUP_BY mode, but using this mode is strongly '. + 'discouraged.'); + + $message = pht( + "On your MySQL instance, the global %s is set to %s. ". + "It is strongly encouraged that you disable this mode when running ". + "Phabricator.\n\n". + "With %s enabled, MySQL rejects queries for which the select list ". + "or (as of MySQL 5.0.23) %s list refer to nonaggregated columns ". + "that are not named in the %s clause. More importantly, Phabricator ". + "does not work properly with this mode enabled.\n\n". + "You can find more information about this mode (and how to configure ". + "it) in the MySQL manual. Usually, it is sufficient to change the %s ". + "in your %s file (in the %s section) and then restart %s:\n\n". + "%s\n". + "(Note that if you run other applications against the same database, ". + "they may not work with %s. Be careful about enabling ". + "it in these cases and consider migrating Phabricator to a different ". + "database.)", + phutil_tag('tt', array(), 'sql_mode'), + phutil_tag('tt', array(), 'ONLY_FULL_GROUP_BY'), + phutil_tag('tt', array(), 'ONLY_FULL_GROUP_BY'), + phutil_tag('tt', array(), 'HAVING'), + phutil_tag('tt', array(), 'GROUP BY'), + phutil_tag('tt', array(), 'sql_mode'), + phutil_tag('tt', array(), 'my.cnf'), + phutil_tag('tt', array(), '[mysqld]'), + phutil_tag('tt', array(), 'mysqld'), + phutil_tag('pre', array(), 'sql_mode=STRICT_ALL_TABLES'), + phutil_tag('tt', array(), 'ONLY_FULL_GROUP_BY')); + + $this->newIssue('mysql.mode') + ->setName(pht('MySQL ONLY_FULL_GROUP_BY Mode Set')) + ->setSummary($summary) + ->setMessage($message) + ->addMySQLConfig('sql_mode'); + } $stopword_file = self::loadRawConfigValue('ft_stopword_file'); if (!PhabricatorDefaultSearchEngineSelector::shouldUseElasticSearch()) { diff --git a/src/applications/config/controller/PhabricatorConfigEditController.php b/src/applications/config/controller/PhabricatorConfigEditController.php index 5983d4cb44..41d6ccc391 100644 --- a/src/applications/config/controller/PhabricatorConfigEditController.php +++ b/src/applications/config/controller/PhabricatorConfigEditController.php @@ -120,10 +120,8 @@ final class PhabricatorConfigEditController ->setSeverity(AphrontErrorView::SEVERITY_WARNING) ->appendChild(phutil_tag('p', array(), $msg)); } else if ($option->getLocked()) { - $msg = pht( - 'This configuration is locked and can not be edited from the web '. - 'interface. Use `./bin/config` in `phabricator/` to edit it.'); + $msg = $option->getLockedMessage(); $error_view = id(new AphrontErrorView()) ->setTitle(pht('Configuration Locked')) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) diff --git a/src/applications/config/management/PhabricatorConfigManagementGetWorkflow.php b/src/applications/config/management/PhabricatorConfigManagementGetWorkflow.php index 9ccb26af0d..1924cc4792 100644 --- a/src/applications/config/management/PhabricatorConfigManagementGetWorkflow.php +++ b/src/applications/config/management/PhabricatorConfigManagementGetWorkflow.php @@ -59,8 +59,8 @@ final class PhabricatorConfigManagementGetWorkflow ); } - $database_config = new PhabricatorConfigDatabaseSource('default'); try { + $database_config = new PhabricatorConfigDatabaseSource('default'); $database_value = $database_config->getKeys(array($key)); if (empty($database_value)) { $values['database'] = array( diff --git a/src/applications/config/management/PhabricatorConfigManagementMigrateWorkflow.php b/src/applications/config/management/PhabricatorConfigManagementMigrateWorkflow.php index 950c12beaa..4b3caa09ce 100644 --- a/src/applications/config/management/PhabricatorConfigManagementMigrateWorkflow.php +++ b/src/applications/config/management/PhabricatorConfigManagementMigrateWorkflow.php @@ -55,13 +55,14 @@ final class PhabricatorConfigManagementMigrateWorkflow 'Skipping option "%s"; already in database config.', $key)."\n"); continue; } else { - PhabricatorConfigEditor::deleteConfig( + PhabricatorConfigEditor::storeNewValue( $this->getViewer(), - $option, + id(new PhabricatorConfigEntry()) + ->loadOneWhere('namespace = %s AND key = %s', 'default', $key), PhabricatorContentSource::newConsoleSource()); $key_count++; $console->writeOut(pht( - 'Migrated option "%s" from file to local config.', $key)."\n"); + 'Migrated option "%s" from file to database config.', $key)."\n"); } } } diff --git a/src/applications/config/option/PhabricatorConfigOption.php b/src/applications/config/option/PhabricatorConfigOption.php index 1c0c090b08..b2e6a0e85d 100644 --- a/src/applications/config/option/PhabricatorConfigOption.php +++ b/src/applications/config/option/PhabricatorConfigOption.php @@ -14,6 +14,7 @@ final class PhabricatorConfigOption private $group; private $examples; private $locked; + private $lockedMessage; private $hidden; private $masked; private $baseClass; @@ -85,6 +86,20 @@ final class PhabricatorConfigOption false); } + public function setLockedMessage($message) { + $this->lockedMessage = $message; + return $this; + } + + public function getLockedMessage() { + if ($this->lockedMessage !== null) { + return $this->lockedMessage; + } + return pht( + 'This configuration is locked and can not be edited from the web '. + 'interface. Use `./bin/config` in `phabricator/` to edit it.'); + } + public function addExample($value, $description) { $this->examples[] = array($value, $description); return $this; diff --git a/src/applications/config/option/PhabricatorCoreConfigOptions.php b/src/applications/config/option/PhabricatorCoreConfigOptions.php index 1227f700b2..66af99566e 100644 --- a/src/applications/config/option/PhabricatorCoreConfigOptions.php +++ b/src/applications/config/option/PhabricatorCoreConfigOptions.php @@ -27,6 +27,7 @@ final class PhabricatorCoreConfigOptions $proto_doc_href = PhabricatorEnv::getDoclink( 'User Guide: Prototype Applications'); $proto_doc_name = pht('User Guide: Prototype Applications'); + $applications_app_href = '/applications/'; return array( $this->newOption('phabricator.base-uri', 'string', null) @@ -183,6 +184,14 @@ final class PhabricatorCoreConfigOptions ->setDescription(pht('Unit test value.')), $this->newOption('phabricator.uninstalled-applications', 'set', array()) ->setLocked(true) + ->setLockedMessage(pht( + 'Use the %s to manage installed applications.', + phutil_tag( + 'a', + array( + 'href' => $applications_app_href, + ), + pht('Applications application')))) ->setDescription( pht('Array containing list of Uninstalled applications.')), $this->newOption('phabricator.application-settings', 'wild', array()) diff --git a/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php b/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php index eceb8aaac0..850ff9c533 100644 --- a/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php +++ b/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php @@ -98,6 +98,12 @@ from replying. However, it may also cause deliverability issues -- notably, you currently can not send this header via Amazon SES, and enabling this option with SES will prevent delivery of any affected mail. EODOC +)); + + $email_preferences_description = $this->deformat(pht(<<deformat(pht(<<setSummary(pht('Show "To:" and "Cc:" footer hints in email.')) ->setDescription($recipient_hints_description), + $this->newOption('metamta.email-preferences', 'bool', true) + ->setBoolOptions( + array( + pht('Show Email Preferences Link'), + pht('No Email Preferences Link'), + )) + ->setSummary(pht('Show email preferences link in email.')) + ->setDescription($email_preferences_description), $this->newOption('metamta.precedence-bulk', 'bool', false) ->setBoolOptions( array( diff --git a/src/applications/conpherence/controller/ConpherenceNewController.php b/src/applications/conpherence/controller/ConpherenceNewController.php index dad9a95608..1a600a7fee 100644 --- a/src/applications/conpherence/controller/ConpherenceNewController.php +++ b/src/applications/conpherence/controller/ConpherenceNewController.php @@ -87,10 +87,11 @@ final class ConpherenceNewController extends ConpherenceController { ->setError($e_participants)) ->appendChild( id(new PhabricatorRemarkupControl()) - ->setName('message') - ->setValue($message) - ->setLabel(pht('Message')) - ->setError($e_message)); + ->setUser($user) + ->setName('message') + ->setValue($message) + ->setLabel(pht('Message')) + ->setError($e_message)); $dialog->appendChild($form); diff --git a/src/applications/differential/__tests__/DifferentialParseRenderTestCase.php b/src/applications/differential/__tests__/DifferentialParseRenderTestCase.php index 41165bf23e..9d9f47315d 100644 --- a/src/applications/differential/__tests__/DifferentialParseRenderTestCase.php +++ b/src/applications/differential/__tests__/DifferentialParseRenderTestCase.php @@ -35,7 +35,9 @@ final class DifferentialParseRenderTestCase extends PhabricatorTestCase { $parser = new ArcanistDiffParser(); $changes = $parser->parseDiff($data); - $diff = DifferentialDiff::newFromRawChanges($changes); + $diff = DifferentialDiff::newFromRawChanges( + PhabricatorUser::getOmnipotentUser(), + $changes); if (count($diff->getChangesets()) !== 1) { throw new Exception("Expected one changeset: {$file}"); } diff --git a/src/applications/differential/conduit/DifferentialCreateDiffConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialCreateDiffConduitAPIMethod.php index 8da95dc19a..26b1c04ddd 100644 --- a/src/applications/differential/conduit/DifferentialCreateDiffConduitAPIMethod.php +++ b/src/applications/differential/conduit/DifferentialCreateDiffConduitAPIMethod.php @@ -69,20 +69,14 @@ final class DifferentialCreateDiffConduitAPIMethod $changes[] = ArcanistDiffChange::newFromDictionary($dict); } - $diff = DifferentialDiff::newFromRawChanges($changes); - $diff->setSourcePath($request->getValue('sourcePath')); - $diff->setSourceMachine($request->getValue('sourceMachine')); + $diff = DifferentialDiff::newFromRawChanges($viewer, $changes); - $diff->setBranch($request->getValue('branch')); - $diff->setCreationMethod($request->getValue('creationMethod')); - $diff->setAuthorPHID($viewer->getPHID()); - $diff->setBookmark($request->getValue('bookmark')); - - // TODO: Remove this eventually; for now continue writing the UUID. Note - // that we'll overwrite it below if we identify a repository, and `arc` - // no longer sends it. This stuff is retained for backward compatibility. - $diff->setRepositoryUUID($request->getValue('repositoryUUID')); + // TODO: Remove repository UUID eventually; for now continue writing + // the UUID. Note that we'll overwrite it below if we identify a + // repository, and `arc` no longer sends it. This stuff is retained for + // backward compatibility. + $repository_uuid = $request->getValue('repositoryUUID'); $repository_phid = $request->getValue('repositoryPHID'); if ($repository_phid) { $repository = id(new PhabricatorRepositoryQuery()) @@ -90,17 +84,11 @@ final class DifferentialCreateDiffConduitAPIMethod ->withPHIDs(array($repository_phid)) ->executeOne(); if ($repository) { - $diff->setRepositoryPHID($repository->getPHID()); - $diff->setRepositoryUUID($repository->getUUID()); + $repository_phid = $repository->getPHID(); + $repository_uuid = $repository->getUUID(); } } - $system = $request->getValue('sourceControlSystem'); - $diff->setSourceControlSystem($system); - $diff->setSourceControlPath($request->getValue('sourceControlPath')); - $diff->setSourceControlBaseRevision( - $request->getValue('sourceControlBaseRevision')); - $project_name = $request->getValue('arcanistProject'); $project_phid = null; if ($project_name) { @@ -116,73 +104,75 @@ final class DifferentialCreateDiffConduitAPIMethod $project_phid = $arcanist_project->getPHID(); } - $diff->setArcanistProjectPHID($project_phid); - switch ($request->getValue('lintStatus')) { case 'skip': - $diff->setLintStatus(DifferentialLintStatus::LINT_SKIP); + $lint_status = DifferentialLintStatus::LINT_SKIP; break; case 'okay': - $diff->setLintStatus(DifferentialLintStatus::LINT_OKAY); + $lint_status = DifferentialLintStatus::LINT_OKAY; break; case 'warn': - $diff->setLintStatus(DifferentialLintStatus::LINT_WARN); + $lint_status = DifferentialLintStatus::LINT_WARN; break; case 'fail': - $diff->setLintStatus(DifferentialLintStatus::LINT_FAIL); + $lint_status = DifferentialLintStatus::LINT_FAIL; break; case 'postponed': - $diff->setLintStatus(DifferentialLintStatus::LINT_POSTPONED); + $lint_status = DifferentialLintStatus::LINT_POSTPONED; break; case 'none': default: - $diff->setLintStatus(DifferentialLintStatus::LINT_NONE); + $lint_status = DifferentialLintStatus::LINT_NONE; break; } switch ($request->getValue('unitStatus')) { case 'skip': - $diff->setUnitStatus(DifferentialUnitStatus::UNIT_SKIP); + $unit_status = DifferentialUnitStatus::UNIT_SKIP; break; case 'okay': - $diff->setUnitStatus(DifferentialUnitStatus::UNIT_OKAY); + $unit_status = DifferentialUnitStatus::UNIT_OKAY; break; case 'warn': - $diff->setUnitStatus(DifferentialUnitStatus::UNIT_WARN); + $unit_status = DifferentialUnitStatus::UNIT_WARN; break; case 'fail': - $diff->setUnitStatus(DifferentialUnitStatus::UNIT_FAIL); + $unit_status = DifferentialUnitStatus::UNIT_FAIL; break; case 'postponed': - $diff->setUnitStatus(DifferentialUnitStatus::UNIT_POSTPONED); + $unit_status = DifferentialUnitStatus::UNIT_POSTPONED; break; case 'none': default: - $diff->setUnitStatus(DifferentialUnitStatus::UNIT_NONE); + $unit_status = DifferentialUnitStatus::UNIT_NONE; break; } + $diff_data_dict = array( + 'sourcePath' => $request->getValue('sourcePath'), + 'sourceMachine' => $request->getValue('sourceMachine'), + 'branch' => $request->getValue('branch'), + 'creationMethod' => $request->getValue('creationMethod'), + 'authorPHID' => $viewer->getPHID(), + 'bookmark' => $request->getValue('bookmark'), + 'repositoryUUID' => $repository_uuid, + 'repositoryPHID' => $repository_phid, + 'sourceControlSystem' => $request->getValue('sourceControlSystem'), + 'sourceControlPath' => $request->getValue('sourceControlPath'), + 'sourceControlBaseRevision' => + $request->getValue('sourceControlBaseRevision'), + 'arcanistProjectPHID' => $project_phid, + 'lintStatus' => $lint_status, + 'unitStatus' => $unit_status,); + + $xactions = array(id(new DifferentialTransaction()) + ->setTransactionType(DifferentialDiffTransaction::TYPE_DIFF_CREATE) + ->setNewValue($diff_data_dict),); + id(new DifferentialDiffEditor()) ->setActor($viewer) - ->setContentSource( - PhabricatorContentSource::newFromConduitRequest($request)) - ->saveDiff($diff); - - // If we didn't get an explicit `repositoryPHID` (which means the client is - // old, or couldn't figure out which repository the working copy belongs - // to), apply heuristics to try to figure it out. - - if (!$repository_phid) { - $repository = id(new DifferentialRepositoryLookup()) - ->setDiff($diff) - ->setViewer($viewer) - ->lookupRepository(); - if ($repository) { - $diff->setRepositoryPHID($repository->getPHID()); - $diff->setRepositoryUUID($repository->getUUID()); - $diff->save(); - } - } + ->setContentSourceFromConduitRequest($request) + ->applyTransactions($diff, $xactions); $path = '/differential/diff/'.$diff->getID().'/'; $uri = PhabricatorEnv::getURI($path); diff --git a/src/applications/differential/conduit/DifferentialCreateRawDiffConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialCreateRawDiffConduitAPIMethod.php index f87dcb1a73..29638b5396 100644 --- a/src/applications/differential/conduit/DifferentialCreateRawDiffConduitAPIMethod.php +++ b/src/applications/differential/conduit/DifferentialCreateRawDiffConduitAPIMethod.php @@ -15,6 +15,7 @@ final class DifferentialCreateRawDiffConduitAPIMethod return array( 'diff' => 'required string', 'repositoryPHID' => 'optional string', + 'viewPolicy' => 'optional string', ); } @@ -41,29 +42,34 @@ final class DifferentialCreateRawDiffConduitAPIMethod throw new Exception( pht('No such repository "%s"!', $repository_phid)); } - } else { - $repository = null; } $parser = new ArcanistDiffParser(); $changes = $parser->parseDiff($raw_diff); - $diff = DifferentialDiff::newFromRawChanges($changes); + $diff = DifferentialDiff::newFromRawChanges($viewer, $changes); - $diff->setLintStatus(DifferentialLintStatus::LINT_SKIP); - $diff->setUnitStatus(DifferentialUnitStatus::UNIT_SKIP); + $diff_data_dict = array( + 'creationMethod' => 'web', + 'authorPHID' => $viewer->getPHID(), + 'repositoryPHID' => $repository_phid, + 'lintStatus' => DifferentialLintStatus::LINT_SKIP, + 'unitStatus' => DifferentialUnitStatus::UNIT_SKIP,); - $diff->setAuthorPHID($viewer->getPHID()); - $diff->setCreationMethod('web'); + $xactions = array(id(new DifferentialTransaction()) + ->setTransactionType(DifferentialDiffTransaction::TYPE_DIFF_CREATE) + ->setNewValue($diff_data_dict),); - if ($repository) { - $diff->setRepositoryPHID($repository->getPHID()); + if ($request->getValue('viewPolicy')) { + $xactions[] = id(new DifferentialTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) + ->setNewValue($request->getValue('viewPolicy')); } id(new DifferentialDiffEditor()) ->setActor($viewer) - ->setContentSource( - PhabricatorContentSource::newFromConduitRequest($request)) - ->saveDiff($diff); + ->setContentSourceFromConduitRequest($request) + ->setLookupRepository(false) // respect user choice + ->applyTransactions($diff, $xactions); return $this->buildDiffInfoDictionary($diff); } diff --git a/src/applications/differential/controller/DifferentialDiffCreateController.php b/src/applications/differential/controller/DifferentialDiffCreateController.php index 65b5590f04..d3c8112276 100644 --- a/src/applications/differential/controller/DifferentialDiffCreateController.php +++ b/src/applications/differential/controller/DifferentialDiffCreateController.php @@ -5,12 +5,24 @@ final class DifferentialDiffCreateController extends DifferentialController { public function processRequest() { $request = $this->getRequest(); + $viewer = $request->getUser(); + $diff = null; + // This object is just for policy stuff + $diff_object = DifferentialDiff::initializeNewDiff($viewer); + $repository_phid = null; + $repository_value = array(); $errors = array(); $e_diff = null; $e_file = null; + $validation_exception = null; if ($request->isFormPost()) { - $diff = null; + + $repository_tokenizer = $request->getArr( + id(new DifferentialRepositoryField())->getFieldKey()); + if ($repository_tokenizer) { + $repository_phid = reset($repository_tokenizer); + } if ($request->getFileExists('diff-file')) { $diff = PhabricatorFile::readUploadedFileData($_FILES['diff-file']); @@ -27,16 +39,20 @@ final class DifferentialDiffCreateController extends DifferentialController { } if (!$errors) { - $call = new ConduitCall( - 'differential.createrawdiff', - array( - 'diff' => $diff, - )); - $call->setUser($request->getUser()); - $result = $call->execute(); - - $path = id(new PhutilURI($result['uri']))->getPath(); - return id(new AphrontRedirectResponse())->setURI($path); + try { + $call = new ConduitCall( + 'differential.createrawdiff', + array( + 'diff' => $diff, + 'repositoryPHID' => $repository_phid, + 'viewPolicy' => $request->getStr('viewPolicy'),)); + $call->setUser($viewer); + $result = $call->execute(); + $path = id(new PhutilURI($result['uri']))->getPath(); + return id(new AphrontRedirectResponse())->setURI($path); + } catch (PhabricatorApplicationTransactionValidationException $ex) { + $validation_exception = $ex; + } } } @@ -68,15 +84,25 @@ final class DifferentialDiffCreateController extends DifferentialController { phutil_tag('tt', array(), 'hg diff --git')); } + if ($repository_phid) { + $repository_value = $this->loadViewerHandles(array($repository_phid)); + } + + $policies = id(new PhabricatorPolicyQuery()) + ->setViewer($viewer) + ->setObject($diff_object) + ->execute(); + $form ->setAction('/differential/diff/create/') ->setEncType('multipart/form-data') - ->setUser($request->getUser()) + ->setUser($viewer) ->appendInstructions($instructions) ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel(pht('Raw Diff')) ->setName('diff') + ->setValue($diff) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) ->setError($e_diff)) ->appendChild( @@ -84,6 +110,13 @@ final class DifferentialDiffCreateController extends DifferentialController { ->setLabel(pht('Raw Diff From File')) ->setName('diff-file') ->setError($e_file)) + ->appendChild( + id(new AphrontFormTokenizerControl()) + ->setName(id(new DifferentialRepositoryField())->getFieldKey()) + ->setLabel(pht('Repository')) + ->setDatasource(new DiffusionRepositoryDatasource()) + ->setValue($repository_value) + ->setLimit(1)) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri) @@ -91,6 +124,7 @@ final class DifferentialDiffCreateController extends DifferentialController { $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Create New Diff')) + ->setValidationException($validation_exception) ->setForm($form) ->setFormErrors($errors); diff --git a/src/applications/differential/controller/DifferentialDiffViewController.php b/src/applications/differential/controller/DifferentialDiffViewController.php index 9bb61cff3b..39f2d18b15 100644 --- a/src/applications/differential/controller/DifferentialDiffViewController.php +++ b/src/applications/differential/controller/DifferentialDiffViewController.php @@ -80,6 +80,9 @@ final class DifferentialDiffViewController extends DifferentialController { ->setAction('/differential/revision/edit/') ->addHiddenInput('diffID', $diff->getID()) ->addHiddenInput('viaDiffView', 1) + ->addHiddenInput( + id(new DifferentialRepositoryField())->getFieldKey(), + $diff->getRepositoryPHID()) ->appendRemarkupInstructions( pht( 'Review the diff for correctness. When you are satisfied, either '. diff --git a/src/applications/differential/controller/DifferentialRevisionEditController.php b/src/applications/differential/controller/DifferentialRevisionEditController.php index 12dac2ca1d..ea26693454 100644 --- a/src/applications/differential/controller/DifferentialRevisionEditController.php +++ b/src/applications/differential/controller/DifferentialRevisionEditController.php @@ -71,16 +71,48 @@ final class DifferentialRevisionEditController ->setViewer($viewer) ->readFieldsFromStorage($revision); + if ($request->getStr('viaDiffView') && $diff) { + $repo_key = id(new DifferentialRepositoryField())->getFieldKey(); + $repository_field = idx( + $field_list->getFields(), + $repo_key); + if ($repository_field) { + $repository_field->setValue($request->getStr($repo_key)); + } + $view_policy_key = id(new DifferentialViewPolicyField())->getFieldKey(); + $view_policy_field = idx( + $field_list->getFields(), + $view_policy_key); + if ($view_policy_field) { + $view_policy_field->setValue($diff->getViewPolicy()); + } + } + $validation_exception = null; if ($request->isFormPost() && !$request->getStr('viaDiffView')) { + + $editor = id(new DifferentialTransactionEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true); + $xactions = $field_list->buildFieldTransactionsFromRequest( new DifferentialTransaction(), $request); if ($diff) { + $repository_phid = null; + $repository_tokenizer = $request->getArr( + id(new DifferentialRepositoryField())->getFieldKey()); + if ($repository_tokenizer) { + $repository_phid = reset($repository_tokenizer); + } + $xactions[] = id(new DifferentialTransaction()) ->setTransactionType(DifferentialTransaction::TYPE_UPDATE) ->setNewValue($diff->getPHID()); + + $editor->setRepositoryPHIDOverride($repository_phid); } $comments = $request->getStr('comments'); @@ -92,11 +124,6 @@ final class DifferentialRevisionEditController ->setContent($comments)); } - $editor = id(new DifferentialTransactionEditor()) - ->setActor($viewer) - ->setContentSourceFromRequest($request) - ->setContinueOnNoEffect(true); - try { $editor->applyTransactions($revision, $xactions); $revision_uri = '/D'.$revision->getID(); diff --git a/src/applications/differential/customfield/DifferentialRevertPlanField.php b/src/applications/differential/customfield/DifferentialRevertPlanField.php index 8934bc01cb..6b159d9d86 100644 --- a/src/applications/differential/customfield/DifferentialRevertPlanField.php +++ b/src/applications/differential/customfield/DifferentialRevertPlanField.php @@ -85,6 +85,7 @@ final class DifferentialRevertPlanField public function renderEditControl(array $handles) { return id(new PhabricatorRemarkupControl()) + ->setUser($this->getViewer()) ->setName($this->getFieldKey()) ->setValue($this->getValue()) ->setLabel($this->getFieldName()); diff --git a/src/applications/differential/customfield/DifferentialSummaryField.php b/src/applications/differential/customfield/DifferentialSummaryField.php index 8498ae5234..f0f06341fc 100644 --- a/src/applications/differential/customfield/DifferentialSummaryField.php +++ b/src/applications/differential/customfield/DifferentialSummaryField.php @@ -39,6 +39,7 @@ final class DifferentialSummaryField public function renderEditControl(array $handles) { return id(new PhabricatorRemarkupControl()) + ->setUser($this->getViewer()) ->setName($this->getFieldKey()) ->setValue($this->getValue()) ->setError($this->getFieldError()) diff --git a/src/applications/differential/customfield/DifferentialTestPlanField.php b/src/applications/differential/customfield/DifferentialTestPlanField.php index efc3bfc1a0..aef7937330 100644 --- a/src/applications/differential/customfield/DifferentialTestPlanField.php +++ b/src/applications/differential/customfield/DifferentialTestPlanField.php @@ -53,6 +53,7 @@ final class DifferentialTestPlanField public function renderEditControl(array $handles) { return id(new PhabricatorRemarkupControl()) + ->setUser($this->getViewer()) ->setName($this->getFieldKey()) ->setValue($this->getValue()) ->setError($this->getFieldError()) diff --git a/src/applications/differential/editor/DifferentialDiffEditor.php b/src/applications/differential/editor/DifferentialDiffEditor.php index 738dd7afeb..69125da1e1 100644 --- a/src/applications/differential/editor/DifferentialDiffEditor.php +++ b/src/applications/differential/editor/DifferentialDiffEditor.php @@ -1,80 +1,247 @@ contentSource = $content_source; + public function setLookupRepository($bool) { + $this->lookupRepository = $bool; return $this; } - public function getContentSource() { - return $this->contentSource; + public function getEditorApplicationClass() { + return 'PhabricatorDifferentialApplication'; } - public function saveDiff(DifferentialDiff $diff) { - $actor = $this->requireActor(); + public function getEditorObjectsDescription() { + return pht('Differential Diffs'); + } - // Generate a PHID first, so the transcript will point at the object if - // we deicde to preserve it. - $phid = $diff->generatePHID(); - $diff->setPHID($phid); + public function getTransactionTypes() { + $types = parent::getTransactionTypes(); + + $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; + $types[] = DifferentialDiffTransaction::TYPE_DIFF_CREATE; + + return $types; + } + + protected function getCustomTransactionOldValue( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case DifferentialDiffTransaction::TYPE_DIFF_CREATE: + return null; + } + + return parent::getCustomTransactionOldValue($object, $xaction); + } + + protected function getCustomTransactionNewValue( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case DifferentialDiffTransaction::TYPE_DIFF_CREATE: + $this->diffDataDict = $xaction->getNewValue(); + return true; + } + + return parent::getCustomTransactionNewValue($object, $xaction); + } + + protected function applyCustomInternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case DifferentialDiffTransaction::TYPE_DIFF_CREATE: + $dict = $this->diffDataDict; + $this->updateDiffFromDict($object, $dict); + return; + case PhabricatorTransactions::TYPE_VIEW_POLICY: + $object->setViewPolicy($xaction->getNewValue()); + return; + } + + return parent::applyCustomInternalTransaction($object, $xaction); + } + + protected function applyCustomExternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case DifferentialDiffTransaction::TYPE_DIFF_CREATE: + case PhabricatorTransactions::TYPE_VIEW_POLICY: + return; + } + + return parent::applyCustomExternalTransaction($object, $xaction); + } + + protected function applyFinalEffects( + PhabricatorLiskDAO $object, + array $xactions) { + + // If we didn't get an explicit `repositoryPHID` (which means the client + // is old, or couldn't figure out which repository the working copy + // belongs to), apply heuristics to try to figure it out. + + if ($this->lookupRepository && !$object->getRepositoryPHID()) { + $repository = id(new DifferentialRepositoryLookup()) + ->setDiff($object) + ->setViewer($this->getActor()) + ->lookupRepository(); + if ($repository) { + $object->setRepositoryPHID($repository->getPHID()); + $object->setRepositoryUUID($repository->getUUID()); + $object->save(); + } + } + + return $xactions; + } + + /** + * We run Herald as part of transaction validation because Herald can + * block diff creation for Differential diffs. Its important to do this + * separately so no Herald logs are saved; these logs could expose + * information the Herald rules are inteneded to block. + */ + protected function validateTransaction( + PhabricatorLiskDAO $object, + $type, + array $xactions) { + + $errors = parent::validateTransaction($object, $type, $xactions); + + foreach ($xactions as $xaction) { + switch ($type) { + case DifferentialDiffTransaction::TYPE_DIFF_CREATE: + $diff = clone $object; + $diff = $this->updateDiffFromDict($diff, $xaction->getNewValue()); + + $adapter = $this->buildHeraldAdapter($diff, $xactions); + $adapter->setContentSource($this->getContentSource()); + $adapter->setIsNewObject($this->getIsNewObject()); + + $engine = new HeraldEngine(); + + $rules = $engine->loadRulesForAdapter($adapter); + $rules = mpull($rules, null, 'getID'); + + $effects = $engine->applyRules($rules, $adapter); + + $blocking_effect = null; + foreach ($effects as $effect) { + if ($effect->getAction() == HeraldAdapter::ACTION_BLOCK) { + $blocking_effect = $effect; + break; + } + } + + if ($blocking_effect) { + $rule = idx($rules, $effect->getRuleID()); + if ($rule && strlen($rule->getName())) { + $rule_name = $rule->getName(); + } else { + $rule_name = pht('Unnamed Herald Rule'); + } + + $message = $effect->getTarget(); + if (!strlen($message)) { + $message = pht('(None.)'); + } + + $errors[] = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Rejected by Herald'), + pht( + "Creation of this diff was rejected by Herald rule %s.\n". + " Rule: %s\n". + "Reason: %s", + 'H'.$effect->getRuleID(), + $rule_name, + $message)); + } + break; + } + } + + return $errors; + } + + + protected function shouldPublishFeedStory( + PhabricatorLiskDAO $object, + array $xactions) { + return false; + } + + protected function shouldSendMail( + PhabricatorLiskDAO $object, + array $xactions) { + return false; + } + + protected function supportsSearch() { + return false; + } + +/* -( Herald Integration )------------------------------------------------- */ + + /** + * See @{method:validateTransaction}. The only Herald action is to block + * the creation of Diffs. We thus have to be careful not to save any + * data and do this validation very early. + */ + protected function shouldApplyHeraldRules( + PhabricatorLiskDAO $object, + array $xactions) { + + return false; + } + + protected function buildHeraldAdapter( + PhabricatorLiskDAO $object, + array $xactions) { $adapter = id(new HeraldDifferentialDiffAdapter()) - ->setDiff($diff); - - $adapter->setContentSource($this->getContentSource()); - $adapter->setIsNewObject(true); - - $engine = new HeraldEngine(); - - $rules = $engine->loadRulesForAdapter($adapter); - $rules = mpull($rules, null, 'getID'); - - $effects = $engine->applyRules($rules, $adapter); - - $blocking_effect = null; - foreach ($effects as $effect) { - if ($effect->getAction() == HeraldAdapter::ACTION_BLOCK) { - $blocking_effect = $effect; - break; - } - } - - if ($blocking_effect) { - $rule = idx($rules, $effect->getRuleID()); - if ($rule && strlen($rule->getName())) { - $rule_name = $rule->getName(); - } else { - $rule_name = pht('Unnamed Herald Rule'); - } - - $message = $effect->getTarget(); - if (!strlen($message)) { - $message = pht('(None.)'); - } - - throw new DifferentialDiffCreationRejectException( - pht( - "Creation of this diff was rejected by Herald rule %s.\n". - " Rule: %s\n". - "Reason: %s", - 'H'.$effect->getRuleID(), - $rule_name, - $message)); - } - - $diff->save(); - - // NOTE: We only save the transcript if we didn't block the diff. - // Otherwise, we might save some of the diff's content in the transcript - // table, which would defeat the purpose of allowing rules to block - // storage of key material. - - $engine->applyEffects($effects, $adapter, $rules); - $xscript = $engine->getTranscript(); + ->setDiff($object); + return $adapter; } + protected function didApplyHeraldRules( + PhabricatorLiskDAO $object, + HeraldAdapter $adapter, + HeraldTranscript $transcript) { + + $xactions = array(); + return $xactions; + } + + private function updateDiffFromDict(DifferentialDiff $diff, $dict) { + $diff + ->setSourcePath(idx($dict, 'sourcePath')) + ->setSourceMachine(idx($dict, 'sourceMachine')) + ->setBranch(idx($dict, 'branch')) + ->setCreationMethod(idx($dict, 'creationMethod')) + ->setAuthorPHID(idx($dict, 'authorPHID', $this->getActor())) + ->setBookmark(idx($dict, 'bookmark')) + ->setRepositoryPHID(idx($dict, 'repositoryPHID')) + ->setRepositoryUUID(idx($dict, 'repositoryUUID')) + ->setSourceControlSystem(idx($dict, 'sourceControlSystem')) + ->setSourceControlPath(idx($dict, 'sourceControlPath')) + ->setSourceControlBaseRevision(idx($dict, 'sourceControlBaseRevision')) + ->setLintStatus(idx($dict, 'lintStatus')) + ->setUnitStatus(idx($dict, 'unitStatus')) + ->setArcanistProjectPHID(idx($dict, 'arcanistProjectPHID')); + + return $diff; + } } diff --git a/src/applications/differential/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php index dc0058b0f3..f2adcfc78f 100644 --- a/src/applications/differential/editor/DifferentialTransactionEditor.php +++ b/src/applications/differential/editor/DifferentialTransactionEditor.php @@ -6,6 +6,7 @@ final class DifferentialTransactionEditor private $heraldEmailPHIDs; private $changedPriorToCommitURI; private $isCloseByCommit; + private $repositoryPHIDOverride = false; public function getEditorApplicationClass() { return 'PhabricatorDifferentialApplication'; @@ -45,6 +46,11 @@ final class DifferentialTransactionEditor return $this->changedPriorToCommitURI; } + public function setRepositoryPHIDOverride($phid_or_null) { + $this->repositoryPHIDOverride = $phid_or_null; + return $this; + } + public function getTransactionTypes() { $types = parent::getTransactionTypes(); @@ -205,7 +211,11 @@ final class DifferentialTransactionEditor $diff = $this->requireDiff($xaction->getNewValue()); $object->setLineCount($diff->getLineCount()); - $object->setRepositoryPHID($diff->getRepositoryPHID()); + if ($this->repositoryPHIDOverride !== false) { + $object->setRepositoryPHID($this->repositoryPHIDOverride); + } else { + $object->setRepositoryPHID($diff->getRepositoryPHID()); + } $object->setArcanistProjectPHID($diff->getArcanistProjectPHID()); $object->attachActiveDiff($diff); @@ -1523,8 +1533,6 @@ final class DifferentialTransactionEditor $adapter->setExplicitReviewers($reviewer_phids); $adapter->setForbiddenCCs($unsubscribed_phids); - $adapter->setIsNewObject($this->getIsNewObject()); - return $adapter; } diff --git a/src/applications/differential/parser/DifferentialChangesetParser.php b/src/applications/differential/parser/DifferentialChangesetParser.php index 2de2ee9140..9db301ab35 100644 --- a/src/applications/differential/parser/DifferentialChangesetParser.php +++ b/src/applications/differential/parser/DifferentialChangesetParser.php @@ -90,6 +90,7 @@ final class DifferentialChangesetParser { const ATTR_DELETED = 'attr:deleted'; const ATTR_UNCHANGED = 'attr:unchanged'; const ATTR_WHITELINES = 'attr:white'; + const ATTR_MOVEAWAY = 'attr:moveaway'; const LINES_CONTEXT = 8; @@ -438,6 +439,10 @@ final class DifferentialChangesetParser { return idx($this->specialAttributes, self::ATTR_WHITELINES, false); } + public function isMoveAway() { + return idx($this->specialAttributes, self::ATTR_MOVEAWAY, false); + } + private function applyIntraline(&$render, $intra, $corpus) { foreach ($render as $key => $text) { @@ -594,6 +599,7 @@ final class DifferentialChangesetParser { } } + $moveaway = false; $changetype = $this->changeset->getChangeType(); if ($changetype == DifferentialChangeType::TYPE_MOVE_AWAY) { // sometimes we show moved files as unchanged, sometimes deleted, @@ -601,12 +607,14 @@ final class DifferentialChangesetParser { // destination of the move. Rather than make a false claim, // omit the 'not changed' notice if this is the source of a move $unchanged = false; + $moveaway = true; } $this->setSpecialAttributes(array( self::ATTR_UNCHANGED => $unchanged, self::ATTR_DELETED => $hunk_parser->getIsDeleted(), self::ATTR_WHITELINES => !$hunk_parser->getHasTextChanges(), + self::ATTR_MOVEAWAY => $moveaway, )); $hunk_parser->generateIntraLineDiffs(); @@ -775,6 +783,8 @@ final class DifferentialChangesetParser { $shield = $renderer->renderShield( pht('The contents of this file were not changed.'), $type); + } else if ($this->isMoveAway()) { + $shield = null; } else if ($this->isWhitespaceOnly()) { $shield = $renderer->renderShield( pht('This file was changed only by adding or removing whitespace.'), diff --git a/src/applications/differential/parser/__tests__/DifferentialHunkParserTestCase.php b/src/applications/differential/parser/__tests__/DifferentialHunkParserTestCase.php index dc4acb383b..111d6ba91f 100644 --- a/src/applications/differential/parser/__tests__/DifferentialHunkParserTestCase.php +++ b/src/applications/differential/parser/__tests__/DifferentialHunkParserTestCase.php @@ -40,7 +40,9 @@ final class DifferentialHunkParserTestCase extends PhabricatorTestCase { throw new Exception("Expected 1 changeset for '{$name}'!"); } - $diff = DifferentialDiff::newFromRawChanges($changes); + $diff = DifferentialDiff::newFromRawChanges( + PhabricatorUser::getOmnipotentUser(), + $changes); return head($diff->getChangesets())->getHunks(); } diff --git a/src/applications/differential/query/DifferentialDiffQuery.php b/src/applications/differential/query/DifferentialDiffQuery.php index f9666400f1..4fc3ffcdfe 100644 --- a/src/applications/differential/query/DifferentialDiffQuery.php +++ b/src/applications/differential/query/DifferentialDiffQuery.php @@ -62,7 +62,6 @@ final class DifferentialDiffQuery foreach ($diffs as $key => $diff) { if (!$diff->getRevisionID()) { - $diff->attachRevision(null); continue; } diff --git a/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php b/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php index ae94b74026..bddbe46d74 100644 --- a/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php @@ -409,16 +409,8 @@ abstract class DifferentialChangesetHTMLRenderer $content))); } - private function renderColgroup() { - return phutil_tag('colgroup', array(), array( - phutil_tag('col', array('class' => 'num')), - phutil_tag('col', array('class' => 'left')), - phutil_tag('col', array('class' => 'num')), - phutil_tag('col', array('class' => 'copy')), - phutil_tag('col', array('class' => 'right')), - phutil_tag('col', array('class' => 'cov')), - )); - } + abstract protected function renderColgroup(); + protected function wrapChangeInTable($content) { if (!$content) { diff --git a/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php b/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php index b214f74c5f..cf5be02f5c 100644 --- a/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php @@ -7,6 +7,14 @@ final class DifferentialChangesetOneUpRenderer return true; } + protected function renderColgroup() { + return phutil_tag('colgroup', array(), array( + phutil_tag('col', array('class' => 'num')), + phutil_tag('col', array('class' => 'num')), + phutil_tag('col', array('class' => 'unified')), + )); + } + public function renderTextChange( $range_start, $range_len, diff --git a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php index 4608c2453d..06c841284d 100644 --- a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php @@ -7,6 +7,17 @@ final class DifferentialChangesetTwoUpRenderer return false; } + protected function renderColgroup() { + return phutil_tag('colgroup', array(), array( + phutil_tag('col', array('class' => 'num')), + phutil_tag('col', array('class' => 'left')), + phutil_tag('col', array('class' => 'num')), + phutil_tag('col', array('class' => 'copy')), + phutil_tag('col', array('class' => 'right')), + phutil_tag('col', array('class' => 'cov')), + )); + } + public function renderTextChange( $range_start, $range_len, diff --git a/src/applications/differential/storage/DifferentialDiff.php b/src/applications/differential/storage/DifferentialDiff.php index d52df5c2ec..c16a9f85fd 100644 --- a/src/applications/differential/storage/DifferentialDiff.php +++ b/src/applications/differential/storage/DifferentialDiff.php @@ -33,6 +33,8 @@ final class DifferentialDiff protected $description; + protected $viewPolicy; + private $unsavedChangesets = array(); private $changesets = self::ATTACHABLE; private $arcanistProject = self::ATTACHABLE; @@ -136,10 +138,27 @@ final class DifferentialDiff return $ret; } - public static function newFromRawChanges(array $changes) { - assert_instances_of($changes, 'ArcanistDiffChange'); - $diff = new DifferentialDiff(); + public static function initializeNewDiff(PhabricatorUser $actor) { + $app = id(new PhabricatorApplicationQuery()) + ->setViewer($actor) + ->withClasses(array('PhabricatorDifferentialApplication')) + ->executeOne(); + $view_policy = $app->getPolicy( + DifferentialDefaultViewCapability::CAPABILITY); + $diff = id(new DifferentialDiff()) + ->setViewPolicy($view_policy); + + return $diff; + } + + public static function newFromRawChanges( + PhabricatorUser $actor, + array $changes) { + + assert_instances_of($changes, 'ArcanistDiffChange'); + + $diff = self::initializeNewDiff($actor); // There may not be any changes; initialize the changesets list so that // we don't throw later when accessing it. $diff->attachChangesets(array()); @@ -289,6 +308,10 @@ final class DifferentialDiff return $changes; } + public function hasRevision() { + return $this->revision !== self::ATTACHABLE; + } + public function getRevision() { return $this->assertAttached($this->revision); } @@ -318,27 +341,27 @@ final class DifferentialDiff } public function getPolicy($capability) { - if ($this->getRevision()) { + if ($this->hasRevision()) { return $this->getRevision()->getPolicy($capability); } - return PhabricatorPolicies::POLICY_USER; + return $this->viewPolicy; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { - if ($this->getRevision()) { + if ($this->hasRevision()) { return $this->getRevision()->hasAutomaticCapability($capability, $viewer); } - return false; + return ($this->getAuthorPHID() == $viewer->getPhid()); } public function describeAutomaticCapability($capability) { - if ($this->getRevision()) { + if ($this->hasRevision()) { return pht( 'This diff is attached to a revision, and inherits its policies.'); } - return null; + return pht('The author of a diff can see it.'); } diff --git a/src/applications/differential/storage/DifferentialDiffTransaction.php b/src/applications/differential/storage/DifferentialDiffTransaction.php new file mode 100644 index 0000000000..430638c3c2 --- /dev/null +++ b/src/applications/differential/storage/DifferentialDiffTransaction.php @@ -0,0 +1,64 @@ +getTransactionType()) { + case self::TYPE_DIFF_CREATE; + return pht('Created'); + } + + return parent::getActionName(); + } + + public function getTitle() { + $author_phid = $this->getAuthorPHID(); + $author_handle = $this->renderHandleLink($author_phid); + + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + switch ($this->getTransactionType()) { + case self::TYPE_DIFF_CREATE; + return pht( + '%s created this diff.', + $author_handle); + } + + return parent::getTitle(); + } + + public function getIcon() { + switch ($this->getTransactionType()) { + case self::TYPE_DIFF_CREATE: + return 'fa-refresh'; + } + + return parent::getIcon(); + } + + public function getColor() { + switch ($this->getTransactionType()) { + case self::TYPE_DIFF_CREATE: + return PhabricatorTransactions::COLOR_SKY; + } + + return parent::getColor(); + } + +} diff --git a/src/applications/differential/storage/__tests__/DifferentialDiffTestCase.php b/src/applications/differential/storage/__tests__/DifferentialDiffTestCase.php index e9e21784e7..3f851d767c 100644 --- a/src/applications/differential/storage/__tests__/DifferentialDiffTestCase.php +++ b/src/applications/differential/storage/__tests__/DifferentialDiffTestCase.php @@ -7,6 +7,7 @@ final class DifferentialDiffTestCase extends ArcanistPhutilTestCase { $parser = new ArcanistDiffParser(); $diff = DifferentialDiff::newFromRawChanges( + PhabricatorUser::getOmnipotentUser(), $parser->parseDiff(Filesystem::readFile($root.'lint_engine.diff'))); $copies = idx(head($diff->getChangesets())->getMetadata(), 'copy:lines'); @@ -46,7 +47,9 @@ index 123457..0000000 {$oblock} EODIFF; - $diff = DifferentialDiff::newFromRawChanges($parser->parseDiff($raw_diff)); + $diff = DifferentialDiff::newFromRawChanges( + PhabricatorUser::getOmnipotentUser(), + $parser->parseDiff($raw_diff)); $this->assertTrue(true); } diff --git a/src/applications/diffusion/controller/DiffusionChangeController.php b/src/applications/diffusion/controller/DiffusionChangeController.php index 37e1dfb02d..c510b403cd 100644 --- a/src/applications/diffusion/controller/DiffusionChangeController.php +++ b/src/applications/diffusion/controller/DiffusionChangeController.php @@ -22,7 +22,9 @@ final class DiffusionChangeController extends DiffusionController { $drequest->updateSymbolicCommit($data['effectiveCommit']); $raw_changes = ArcanistDiffChange::newFromConduit($data['changes']); - $diff = DifferentialDiff::newFromRawChanges($raw_changes); + $diff = DifferentialDiff::newFromRawChanges( + $viewer, + $raw_changes); $changesets = $diff->getChangesets(); $changeset = reset($changesets); diff --git a/src/applications/diffusion/controller/DiffusionCommitController.php b/src/applications/diffusion/controller/DiffusionCommitController.php index 3e7308a56a..7b92ae062e 100644 --- a/src/applications/diffusion/controller/DiffusionCommitController.php +++ b/src/applications/diffusion/controller/DiffusionCommitController.php @@ -276,6 +276,7 @@ final class DiffusionCommitController extends DiffusionController { $content[] = $change_panel; $changesets = DiffusionPathChange::convertToDifferentialChangesets( + $user, $changes); $vcs = $repository->getVersionControlSystem(); @@ -419,7 +420,6 @@ final class DiffusionCommitController extends DiffusionController { ->withSourcePHIDs(array($commit_phid)) ->withEdgeTypes(array( DiffusionCommitHasTaskEdgeType::EDGECONST, - PhabricatorEdgeConfig::TYPE_COMMIT_HAS_PROJECT, PhabricatorEdgeConfig::TYPE_COMMIT_HAS_DREV, )); @@ -427,8 +427,6 @@ final class DiffusionCommitController extends DiffusionController { $task_phids = array_keys( $edges[$commit_phid][DiffusionCommitHasTaskEdgeType::EDGECONST]); - $proj_phids = array_keys( - $edges[$commit_phid][PhabricatorEdgeConfig::TYPE_COMMIT_HAS_PROJECT]); $revision_phid = key( $edges[$commit_phid][PhabricatorEdgeConfig::TYPE_COMMIT_HAS_DREV]); @@ -628,15 +626,6 @@ final class DiffusionCommitController extends DiffusionController { $props['Tasks'] = $task_list; } - if ($proj_phids) { - $proj_list = array(); - foreach ($proj_phids as $phid) { - $proj_list[] = $handles[$phid]->renderLink(); - } - $proj_list = phutil_implode_html(phutil_tag('br'), $proj_list); - $props['Projects'] = $proj_list; - } - return $props; } diff --git a/src/applications/diffusion/controller/DiffusionCommitEditController.php b/src/applications/diffusion/controller/DiffusionCommitEditController.php index 5515f3e322..75b4a31ea4 100644 --- a/src/applications/diffusion/controller/DiffusionCommitEditController.php +++ b/src/applications/diffusion/controller/DiffusionCommitEditController.php @@ -22,7 +22,7 @@ final class DiffusionCommitEditController extends DiffusionController { } $commit_phid = $commit->getPHID(); - $edge_type = PhabricatorEdgeConfig::TYPE_COMMIT_HAS_PROJECT; + $edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; $current_proj_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $commit_phid, $edge_type); @@ -30,23 +30,17 @@ final class DiffusionCommitEditController extends DiffusionController { $proj_t_values = $handles; if ($request->isFormPost()) { + $xactions = array(); $proj_phids = $request->getArr('projects'); - $new_proj_phids = array_values($proj_phids); - $rem_proj_phids = array_diff($current_proj_phids, - $new_proj_phids); - - $editor = id(new PhabricatorEdgeEditor()); - foreach ($rem_proj_phids as $phid) { - $editor->removeEdge($commit_phid, $edge_type, $phid); - } - foreach ($new_proj_phids as $phid) { - $editor->addEdge($commit_phid, $edge_type, $phid); - } - $editor->save(); - - id(new PhabricatorSearchIndexer()) - ->queueDocumentForIndexing($commit->getPHID()); - + $xactions[] = id(new PhabricatorAuditTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue('edge:type', $edge_type) + ->setNewValue(array('=' => array_fuse($proj_phids))); + $editor = id(new PhabricatorAuditEditor()) + ->setActor($user) + ->setContinueOnNoEffect(true) + ->setContentSourceFromRequest($request); + $xactions = $editor->applyTransactions($commit, $xactions); return id(new AphrontRedirectResponse()) ->setURI('/r'.$callsign.$commit->getCommitIdentifier()); } diff --git a/src/applications/diffusion/controller/DiffusionDiffController.php b/src/applications/diffusion/controller/DiffusionDiffController.php index 92b7b64cd4..81460ea67a 100644 --- a/src/applications/diffusion/controller/DiffusionDiffController.php +++ b/src/applications/diffusion/controller/DiffusionDiffController.php @@ -54,7 +54,9 @@ final class DiffusionDiffController extends DiffusionController { )); $drequest->updateSymbolicCommit($data['effectiveCommit']); $raw_changes = ArcanistDiffChange::newFromConduit($data['changes']); - $diff = DifferentialDiff::newFromRawChanges($raw_changes); + $diff = DifferentialDiff::newFromRawChanges( + $user, + $raw_changes); $changesets = $diff->getChangesets(); $changeset = reset($changesets); diff --git a/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php b/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php index e14259a9fc..2717cc9ad1 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php @@ -104,6 +104,8 @@ final class DiffusionRepositoryCreateController $type_local_path = PhabricatorRepositoryTransaction::TYPE_LOCAL_PATH; $type_remote_uri = PhabricatorRepositoryTransaction::TYPE_REMOTE_URI; $type_hosting = PhabricatorRepositoryTransaction::TYPE_HOSTING; + $type_http = PhabricatorRepositoryTransaction::TYPE_PROTOCOL_HTTP; + $type_ssh = PhabricatorRepositoryTransaction::TYPE_PROTOCOL_SSH; $type_credential = PhabricatorRepositoryTransaction::TYPE_CREDENTIAL; $type_view = PhabricatorTransactions::TYPE_VIEW_POLICY; $type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY; @@ -157,6 +159,26 @@ final class DiffusionRepositoryCreateController $xactions[] = id(clone $template) ->setTransactionType($type_hosting) ->setNewValue(true); + $vcs = $form->getPage('vcs')->getControl('vcs')->getValue(); + if ($vcs != PhabricatorRepositoryType::REPOSITORY_TYPE_SVN) { + if (PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth')) { + $v_http_mode = PhabricatorRepository::SERVE_READWRITE; + } else { + $v_http_mode = PhabricatorRepository::SERVE_OFF; + } + $xactions[] = id(clone $template) + ->setTransactionType($type_http) + ->setNewValue($v_http_mode); + } + + if (PhabricatorEnv::getEnvConfig('diffusion.ssh-user')) { + $v_ssh_mode = PhabricatorRepository::SERVE_READWRITE; + } else { + $v_ssh_mode = PhabricatorRepository::SERVE_OFF; + } + $xactions[] = id(clone $template) + ->setTransactionType($type_ssh) + ->setNewValue($v_ssh_mode); } if ($is_auth) { diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditBasicController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditBasicController.php index 4457df8d23..d29aeb0be6 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryEditBasicController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryEditBasicController.php @@ -120,6 +120,7 @@ final class DiffusionRepositoryEditBasicController $form ->appendChild( id(new PhabricatorRemarkupControl()) + ->setUser($user) ->setName('description') ->setLabel(pht('Description')) ->setValue($v_desc)) diff --git a/src/applications/diffusion/data/DiffusionPathChange.php b/src/applications/diffusion/data/DiffusionPathChange.php index 188669126e..e4f6fa182a 100644 --- a/src/applications/diffusion/data/DiffusionPathChange.php +++ b/src/applications/diffusion/data/DiffusionPathChange.php @@ -142,10 +142,12 @@ final class DiffusionPathChange { return array_select_keys($result, $direct); } - final public static function convertToDifferentialChangesets(array $changes) { + final public static function convertToDifferentialChangesets( + PhabricatorUser $user, + array $changes) { assert_instances_of($changes, 'DiffusionPathChange'); $arcanist_changes = self::convertToArcanistChanges($changes); - $diff = DifferentialDiff::newFromRawChanges($arcanist_changes); + $diff = DifferentialDiff::newFromRawChanges($user, $arcanist_changes); return $diff->getChangesets(); } diff --git a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php index 09a65f3c3d..9e7d5ccef9 100644 --- a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php +++ b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php @@ -1115,7 +1115,9 @@ final class DiffusionCommitHookEngine extends Phobject { $parser = new ArcanistDiffParser(); $changes = $parser->parseDiff($raw_diff); - $diff = DifferentialDiff::newFromRawChanges($changes); + $diff = DifferentialDiff::newFromRawChanges( + $this->getViewer(), + $changes); return $diff->getChangesets(); } diff --git a/src/applications/diviner/markup/DivinerSymbolRemarkupRule.php b/src/applications/diviner/markup/DivinerSymbolRemarkupRule.php index 422209fca0..bb4fafd11e 100644 --- a/src/applications/diviner/markup/DivinerSymbolRemarkupRule.php +++ b/src/applications/diviner/markup/DivinerSymbolRemarkupRule.php @@ -140,6 +140,10 @@ final class DivinerSymbolRemarkupRule extends PhutilRemarkupRule { $link = $title; } } else if ($href) { + if ($this->getEngine()->isHTMLMailMode()) { + $href = PhabricatorEnv::getProductionURI($href); + } + $link = $this->newTag( 'a', array( diff --git a/src/applications/files/application/PhabricatorFilesApplication.php b/src/applications/files/application/PhabricatorFilesApplication.php index eb1a5a4db8..c3d434bf2e 100644 --- a/src/applications/files/application/PhabricatorFilesApplication.php +++ b/src/applications/files/application/PhabricatorFilesApplication.php @@ -11,7 +11,7 @@ final class PhabricatorFilesApplication extends PhabricatorApplication { } public function getShortDescription() { - return 'Store and Share Files'; + return pht('Store and Share Files'); } public function getIconName() { @@ -40,6 +40,15 @@ final class PhabricatorFilesApplication extends PhabricatorApplication { ); } + protected function getCustomCapabilities() { + return array( + FilesDefaultViewCapability::CAPABILITY => array( + 'caption' => pht( + 'Default view policy for newly created files.'), + ), + ); + } + public function getRoutes() { return array( '/F(?P[1-9]\d*)' => 'PhabricatorFileInfoController', diff --git a/src/applications/files/capability/FilesDefaultViewCapability.php b/src/applications/files/capability/FilesDefaultViewCapability.php new file mode 100644 index 0000000000..7a0aae8e0c --- /dev/null +++ b/src/applications/files/capability/FilesDefaultViewCapability.php @@ -0,0 +1,16 @@ +getRequest(); $viewer = $request->getUser(); - $file = new PhabricatorFile(); + $file = PhabricatorFile::initializeNewFile(); $e_file = true; $errors = array(); diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php index ff4395d460..581cac46e9 100644 --- a/src/applications/files/storage/PhabricatorFile.php +++ b/src/applications/files/storage/PhabricatorFile.php @@ -53,7 +53,16 @@ final class PhabricatorFile extends PhabricatorFileDAO private $originalFile = self::ATTACHABLE; public static function initializeNewFile() { + $app = id(new PhabricatorApplicationQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withClasses(array('PhabricatorFilesApplication')) + ->executeOne(); + + $view_policy = $app->getPolicy( + FilesDefaultViewCapability::CAPABILITY); + return id(new PhabricatorFile()) + ->setViewPolicy($view_policy) ->attachOriginalFile(null) ->attachObjects(array()) ->attachObjectPHIDs(array()); diff --git a/src/applications/fund/controller/FundInitiativeEditController.php b/src/applications/fund/controller/FundInitiativeEditController.php index b6b69945b6..5b3adf1679 100644 --- a/src/applications/fund/controller/FundInitiativeEditController.php +++ b/src/applications/fund/controller/FundInitiativeEditController.php @@ -200,11 +200,13 @@ final class FundInitiativeEditController ->setOptions($merchant_options)) ->appendChild( id(new PhabricatorRemarkupControl()) + ->setUser($viewer) ->setName('description') ->setLabel(pht('Description')) ->setValue($v_desc)) ->appendChild( id(new PhabricatorRemarkupControl()) + ->setUser($viewer) ->setName('risks') ->setLabel(pht('Risks/Challenges')) ->setValue($v_risk)) diff --git a/src/applications/harbormaster/controller/HarbormasterStepEditController.php b/src/applications/harbormaster/controller/HarbormasterStepEditController.php index 62a8ac0e68..12631db71a 100644 --- a/src/applications/harbormaster/controller/HarbormasterStepEditController.php +++ b/src/applications/harbormaster/controller/HarbormasterStepEditController.php @@ -155,6 +155,7 @@ final class HarbormasterStepEditController extends HarbormasterController { $form ->appendChild( id(new PhabricatorRemarkupControl()) + ->setUser($viewer) ->setName('description') ->setLabel(pht('Description')) ->setError($e_description) diff --git a/src/applications/herald/adapter/HeraldAdapter.php b/src/applications/herald/adapter/HeraldAdapter.php index 147ebaee11..9c56a6001f 100644 --- a/src/applications/herald/adapter/HeraldAdapter.php +++ b/src/applications/herald/adapter/HeraldAdapter.php @@ -40,8 +40,10 @@ abstract class HeraldAdapter { const FIELD_COMMITTER_RAW = 'committer-raw'; const FIELD_IS_NEW_OBJECT = 'new-object'; const FIELD_TASK_PRIORITY = 'taskpriority'; + const FIELD_TASK_STATUS = 'taskstatus'; const FIELD_ARCANIST_PROJECT = 'arcanist-project'; const FIELD_PUSHER_IS_COMMITTER = 'pusher-is-committer'; + const FIELD_PATH = 'path'; const CONDITION_CONTAINS = 'contains'; const CONDITION_NOT_CONTAINS = '!contains'; @@ -95,6 +97,7 @@ abstract class HeraldAdapter { const VALUE_USER_OR_PROJECT = 'userorproject'; const VALUE_BUILD_PLAN = 'buildplan'; const VALUE_TASK_PRIORITY = 'taskpriority'; + const VALUE_TASK_STATUS = 'taskstatus'; const VALUE_ARCANIST_PROJECT = 'arcanistprojects'; const VALUE_LEGAL_DOCUMENTS = 'legaldocuments'; @@ -310,8 +313,10 @@ abstract class HeraldAdapter { self::FIELD_COMMITTER_RAW => pht('Raw committer name'), self::FIELD_IS_NEW_OBJECT => pht('Is newly created?'), self::FIELD_TASK_PRIORITY => pht('Task priority'), + self::FIELD_TASK_STATUS => pht('Task status'), self::FIELD_ARCANIST_PROJECT => pht('Arcanist Project'), self::FIELD_PUSHER_IS_COMMITTER => pht('Pusher same as committer'), + self::FIELD_PATH => pht('Path'), ) + $this->getCustomFieldNameMap(); } @@ -353,6 +358,7 @@ abstract class HeraldAdapter { case self::FIELD_BODY: case self::FIELD_COMMITTER_RAW: case self::FIELD_AUTHOR_RAW: + case self::FIELD_PATH: return array( self::CONDITION_CONTAINS, self::CONDITION_NOT_CONTAINS, @@ -363,6 +369,7 @@ abstract class HeraldAdapter { case self::FIELD_REVIEWER: case self::FIELD_PUSHER: case self::FIELD_TASK_PRIORITY: + case self::FIELD_TASK_STATUS: case self::FIELD_ARCANIST_PROJECT: return array( self::CONDITION_IS_ANY, @@ -840,6 +847,8 @@ abstract class HeraldAdapter { return self::VALUE_REPOSITORY; case self::FIELD_TASK_PRIORITY: return self::VALUE_TASK_PRIORITY; + case self::FIELD_TASK_STATUS: + return self::VALUE_TASK_STATUS; case self::FIELD_ARCANIST_PROJECT: return self::VALUE_ARCANIST_PROJECT; default: @@ -1159,6 +1168,15 @@ abstract class HeraldAdapter { } } break; + case self::FIELD_TASK_STATUS: + $status_map = ManiphestTaskStatus::getTaskStatusMap(); + foreach ($value as $index => $val) { + $name = idx($status_map, $val); + if ($name) { + $value[$index] = $name; + } + } + break; case HeraldPreCommitRefAdapter::FIELD_REF_CHANGE: $change_map = PhabricatorRepositoryPushLog::getHeraldChangeFlagConditionOptions(); diff --git a/src/applications/herald/adapter/HeraldCommitAdapter.php b/src/applications/herald/adapter/HeraldCommitAdapter.php index e52ad18fa6..35e93f5916 100644 --- a/src/applications/herald/adapter/HeraldCommitAdapter.php +++ b/src/applications/herald/adapter/HeraldCommitAdapter.php @@ -346,7 +346,9 @@ final class HeraldCommitAdapter extends HeraldAdapter { $parser = new ArcanistDiffParser(); $changes = $parser->parseDiff($raw); - $diff = DifferentialDiff::newFromRawChanges($changes); + $diff = DifferentialDiff::newFromRawChanges( + PhabricatorUser::getOmnipotentUser(), + $changes); return $diff; } diff --git a/src/applications/herald/adapter/HeraldManiphestTaskAdapter.php b/src/applications/herald/adapter/HeraldManiphestTaskAdapter.php index ac0bbcad93..5174ca34a9 100644 --- a/src/applications/herald/adapter/HeraldManiphestTaskAdapter.php +++ b/src/applications/herald/adapter/HeraldManiphestTaskAdapter.php @@ -89,6 +89,7 @@ final class HeraldManiphestTaskAdapter extends HeraldAdapter { self::FIELD_CONTENT_SOURCE, self::FIELD_PROJECTS, self::FIELD_TASK_PRIORITY, + self::FIELD_TASK_STATUS, self::FIELD_IS_NEW_OBJECT, ), parent::getFields()); @@ -145,6 +146,8 @@ final class HeraldManiphestTaskAdapter extends HeraldAdapter { PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); case self::FIELD_TASK_PRIORITY: return $this->getTask()->getPriority(); + case self::FIELD_TASK_STATUS: + return $this->getTask()->getStatus(); } return parent::getHeraldField($field); diff --git a/src/applications/herald/controller/HeraldRuleController.php b/src/applications/herald/controller/HeraldRuleController.php index beed090203..11bfd7fc8a 100644 --- a/src/applications/herald/controller/HeraldRuleController.php +++ b/src/applications/herald/controller/HeraldRuleController.php @@ -358,6 +358,14 @@ final class HeraldRuleController extends HeraldController { } $value = $value_map; break; + case HeraldAdapter::FIELD_TASK_STATUS: + $value_map = array(); + $status_map = ManiphestTaskStatus::getTaskStatusMap(); + foreach ($value as $status) { + $value_map[$status] = idx($status_map, $status); + } + $value = $value_map; + break; default: if (is_array($value)) { $value_map = array(); @@ -586,6 +594,7 @@ final class HeraldRuleController extends HeraldController { 'repository' => new DiffusionRepositoryDatasource(), 'legaldocuments' => new LegalpadDocumentDatasource(), 'taskpriority' => new ManiphestTaskPriorityDatasource(), + 'taskstatus' => new ManiphestTaskStatusDatasource(), 'buildplan' => new HarbormasterBuildPlanDatasource(), 'arcanistprojects' => new DiffusionArcanistProjectDatasource(), 'package' => new PhabricatorOwnersPackageDatasource(), diff --git a/src/applications/herald/controller/HeraldTestConsoleController.php b/src/applications/herald/controller/HeraldTestConsoleController.php index 6e4488465f..0af51cf672 100644 --- a/src/applications/herald/controller/HeraldTestConsoleController.php +++ b/src/applications/herald/controller/HeraldTestConsoleController.php @@ -47,6 +47,9 @@ final class HeraldTestConsoleController extends HeraldController { } else if ($object instanceof PholioMock) { $adapter = id(new HeraldPholioMockAdapter()) ->setMock($object); + } else if ($object instanceof PhrictionDocument) { + $adapter = id(new PhrictionDocumentHeraldAdapter()) + ->setDocument($object); } else { throw new Exception('Can not build adapter for object!'); } diff --git a/src/applications/herald/storage/HeraldRule.php b/src/applications/herald/storage/HeraldRule.php index bc85bdd203..997dd25217 100644 --- a/src/applications/herald/storage/HeraldRule.php +++ b/src/applications/herald/storage/HeraldRule.php @@ -18,7 +18,7 @@ final class HeraldRule extends HeraldDAO protected $isDisabled = 0; protected $triggerObjectPHID; - protected $configVersion = 37; + protected $configVersion = 38; // PHIDs for which this rule has been applied private $ruleApplied = self::ATTACHABLE; diff --git a/src/applications/legalpad/controller/LegalpadDocumentEditController.php b/src/applications/legalpad/controller/LegalpadDocumentEditController.php index 20002c4c77..d8b7d7c31b 100644 --- a/src/applications/legalpad/controller/LegalpadDocumentEditController.php +++ b/src/applications/legalpad/controller/LegalpadDocumentEditController.php @@ -143,20 +143,22 @@ final class LegalpadDocumentEditController extends LegalpadController { $form ->appendChild( id(new PhabricatorRemarkupControl()) - ->setID('preamble') - ->setLabel(pht('Preamble')) - ->setValue($v_preamble) - ->setName('preamble') - ->setCaption( - pht('Optional help text for users signing this document.'))) + ->setUser($user) + ->setID('preamble') + ->setLabel(pht('Preamble')) + ->setValue($v_preamble) + ->setName('preamble') + ->setCaption( + pht('Optional help text for users signing this document.'))) ->appendChild( id(new PhabricatorRemarkupControl()) - ->setID('document-text') - ->setLabel(pht('Document Body')) - ->setError($e_text) - ->setValue($text) - ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) - ->setName('text')); + ->setUser($user) + ->setID('document-text') + ->setLabel(pht('Document Body')) + ->setError($e_text) + ->setValue($text) + ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) + ->setName('text')); $policies = id(new PhabricatorPolicyQuery()) ->setViewer($user) diff --git a/src/applications/macro/markup/PhabricatorIconRemarkupRule.php b/src/applications/macro/markup/PhabricatorIconRemarkupRule.php index 7fd9f6b6be..9daf9b6f0c 100644 --- a/src/applications/macro/markup/PhabricatorIconRemarkupRule.php +++ b/src/applications/macro/markup/PhabricatorIconRemarkupRule.php @@ -14,7 +14,11 @@ final class PhabricatorIconRemarkupRule extends PhutilRemarkupRule { } public function markupIcon($matches) { - if (!$this->isFlatText($matches[0])) { + $engine = $this->getEngine(); + $text_mode = $engine->isTextMode(); + $mail_mode = $engine->isHTMLMailMode(); + + if (!$this->isFlatText($matches[0]) || $text_mode || $mail_mode) { return $matches[0]; } @@ -69,6 +73,7 @@ final class PhabricatorIconRemarkupRule extends PhutilRemarkupRule { $icon_view = id(new PHUIIconView()) ->setIconFont('fa-'.$icon, $color); + return $this->getEngine()->storeText($icon_view); } diff --git a/src/applications/macro/markup/PhabricatorImageMacroRemarkupRule.php b/src/applications/macro/markup/PhabricatorImageMacroRemarkupRule.php index 081f2bd5c5..965064b4a4 100644 --- a/src/applications/macro/markup/PhabricatorImageMacroRemarkupRule.php +++ b/src/applications/macro/markup/PhabricatorImageMacroRemarkupRule.php @@ -109,6 +109,8 @@ final class PhabricatorImageMacroRemarkupRule extends PhutilRemarkupRule { $result = $spec['original'].' <'.$src_uri.'>'; $engine->overwriteStoredText($spec['token'], $result); continue; + } else if ($this->getEngine()->isHTMLMailMode()) { + $src_uri = PhabricatorEnv::getProductionURI($src_uri); } $file_data = $file->getMetadata(); diff --git a/src/applications/macro/markup/PhabricatorMemeRemarkupRule.php b/src/applications/macro/markup/PhabricatorMemeRemarkupRule.php index 8e6e0f5639..0b65226e09 100644 --- a/src/applications/macro/markup/PhabricatorMemeRemarkupRule.php +++ b/src/applications/macro/markup/PhabricatorMemeRemarkupRule.php @@ -34,6 +34,10 @@ final class PhabricatorMemeRemarkupRule extends PhutilRemarkupRule { ->alter('uppertext', $options['above']) ->alter('lowertext', $options['below']); + if ($this->getEngine()->isHTMLMailMode()) { + $uri = PhabricatorEnv::getProductionURI($uri); + } + if ($this->getEngine()->isTextMode()) { $img = ($options['above'] != '' ? "\"{$options['above']}\"\n" : ''). diff --git a/src/applications/maniphest/application/PhabricatorManiphestApplication.php b/src/applications/maniphest/application/PhabricatorManiphestApplication.php index 2bf35fe7a8..b1022083ea 100644 --- a/src/applications/maniphest/application/PhabricatorManiphestApplication.php +++ b/src/applications/maniphest/application/PhabricatorManiphestApplication.php @@ -75,6 +75,10 @@ final class PhabricatorManiphestApplication extends PhabricatorApplication { public function loadStatus(PhabricatorUser $user) { $status = array(); + if (!$user->isLoggedIn()) { + return $status; + } + $query = id(new ManiphestTaskQuery()) ->setViewer($user) ->withStatuses(ManiphestTaskStatus::getOpenStatusConstants()) diff --git a/src/applications/maniphest/conduit/ManiphestGetTaskTransactionsConduitAPIMethod.php b/src/applications/maniphest/conduit/ManiphestGetTaskTransactionsConduitAPIMethod.php index 062523beee..cf3f755314 100644 --- a/src/applications/maniphest/conduit/ManiphestGetTaskTransactionsConduitAPIMethod.php +++ b/src/applications/maniphest/conduit/ManiphestGetTaskTransactionsConduitAPIMethod.php @@ -64,6 +64,7 @@ final class ManiphestGetTaskTransactionsConduitAPIMethod $results[$task_id][] = array( 'taskID' => $task_id, + 'transactionPHID' => $transaction->getPHID(), 'transactionType' => $transaction->getTransactionType(), 'oldValue' => $transaction->getOldValue(), 'newValue' => $transaction->getNewValue(), diff --git a/src/applications/maniphest/controller/ManiphestBatchEditController.php b/src/applications/maniphest/controller/ManiphestBatchEditController.php index a93b9be0d7..12f31c00d3 100644 --- a/src/applications/maniphest/controller/ManiphestBatchEditController.php +++ b/src/applications/maniphest/controller/ManiphestBatchEditController.php @@ -114,12 +114,9 @@ final class ManiphestBatchEditController extends ManiphestController { 'name' => 'actions', 'id' => 'batch-form-actions', ))); - $form->appendChild( - phutil_tag('p', array(), pht('These tasks will be edited:'))); - $form->appendChild($list); $form->appendChild( id(new PHUIFormInsetView()) - ->setTitle('Actions') + ->setTitle(pht('Actions')) ->setRightButton(javelin_tag( 'a', array( @@ -146,18 +143,22 @@ final class ManiphestBatchEditController extends ManiphestController { $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($title); + $task_box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Selected Tasks')) + ->appendChild($list); + $form_box = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Batch Edit Tasks')) + ->setHeaderText(pht('Batch Editor')) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, + $task_box, $form_box, ), array( 'title' => $title, - 'device' => false, )); } diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php index 20e5c84579..cfd00b4740 100644 --- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php +++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php @@ -38,12 +38,6 @@ final class ManiphestTaskDetailController extends ManiphestController { ->executeOne(); } - $transactions = id(new ManiphestTransactionQuery()) - ->setViewer($user) - ->withObjectPHIDs(array($task->getPHID())) - ->needComments(true) - ->execute(); - $field_list = PhabricatorCustomField::getObjectFields( $task, PhabricatorCustomField::ROLE_VIEW); @@ -137,15 +131,11 @@ final class ManiphestTaskDetailController extends ManiphestController { $engine = new PhabricatorMarkupEngine(); $engine->setViewer($user); $engine->addObject($task, ManiphestTask::MARKUP_FIELD_DESCRIPTION); - foreach ($transactions as $modern_xaction) { - if ($modern_xaction->getComment()) { - $engine->addObject( - $modern_xaction->getComment(), - PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT); - } - } - $engine->process(); + $timeline = $this->buildTransactionTimeline( + $task, + new ManiphestTransactionQuery(), + $engine); $resolution_types = ManiphestTaskStatus::getTaskStatusMap(); @@ -265,6 +255,7 @@ final class ManiphestTaskDetailController extends ManiphestController { ->setControlStyle('display: none')) ->appendChild( id(new PhabricatorRemarkupControl()) + ->setUser($user) ->setLabel(pht('Comments')) ->setName('comments') ->setValue($draft_text) @@ -338,12 +329,6 @@ final class ManiphestTaskDetailController extends ManiphestController { 'aphront-panel-preview-loading-text', pht('Loading preview...')))); - $timeline = id(new PhabricatorApplicationTransactionView()) - ->setUser($user) - ->setObjectPHID($task->getPHID()) - ->setTransactions($transactions) - ->setMarkupEngine($engine); - $object_name = 'T'.$task->getID(); $actions = $this->buildActionView($task); diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php index 1fb7d2ec52..70ac6f7df8 100644 --- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php +++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php @@ -501,6 +501,34 @@ final class ManiphestTransactionEditor pht('TASK DETAIL'), PhabricatorEnv::getProductionURI('/T'.$object->getID())); + + $board_phids = array(); + $type_column = ManiphestTransaction::TYPE_PROJECT_COLUMN; + foreach ($xactions as $xaction) { + if ($xaction->getTransactionType() == $type_column) { + $new = $xaction->getNewValue(); + $project_phid = idx($new, 'projectPHID'); + if ($project_phid) { + $board_phids[] = $project_phid; + } + } + } + + if ($board_phids) { + $projects = id(new PhabricatorProjectQuery()) + ->setViewer($this->requireActor()) + ->withPHIDs($board_phids) + ->execute(); + + foreach ($projects as $project) { + $body->addLinkSection( + pht('WORKBOARD'), + PhabricatorEnv::getProductionURI( + '/project/board/'.$project->getID().'/')); + } + } + + return $body; } diff --git a/src/applications/maniphest/search/ManiphestSearchIndexer.php b/src/applications/maniphest/search/ManiphestSearchIndexer.php index 290d7c92da..3b2ffe48a6 100644 --- a/src/applications/maniphest/search/ManiphestSearchIndexer.php +++ b/src/applications/maniphest/search/ManiphestSearchIndexer.php @@ -39,14 +39,6 @@ final class ManiphestSearchIndexer extends PhabricatorSearchDocumentIndexer { new ManiphestTransactionQuery(), array($phid)); - foreach ($task->getProjectPHIDs() as $phid) { - $doc->addRelationship( - PhabricatorSearchRelationship::RELATIONSHIP_PROJECT, - $phid, - PhabricatorProjectProjectPHIDType::TYPECONST, - $task->getDateModified()); // Bogus. - } - $owner = $task->getOwnerPHID(); if ($owner) { $doc->addRelationship( diff --git a/src/applications/maniphest/storage/ManiphestTransaction.php b/src/applications/maniphest/storage/ManiphestTransaction.php index 69ba720d29..4b386ed112 100644 --- a/src/applications/maniphest/storage/ManiphestTransaction.php +++ b/src/applications/maniphest/storage/ManiphestTransaction.php @@ -213,6 +213,11 @@ final class ManiphestTransaction return 'yellow'; } + case self::TYPE_MERGED_FROM: + return 'orange'; + + case self::TYPE_MERGED_INTO: + return 'black'; } return parent::getColor(); @@ -347,6 +352,7 @@ final class ManiphestTransaction return 'fa-columns'; case self::TYPE_MERGED_INTO: + return 'fa-check'; case self::TYPE_MERGED_FROM: return 'fa-compress'; @@ -591,7 +597,7 @@ final class ManiphestTransaction case self::TYPE_MERGED_INTO: return pht( - '%s merged this task into %s.', + '%s closed this task as a duplicate of %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($new)); break; diff --git a/src/applications/maniphest/typeahead/ManiphestTaskStatusDatasource.php b/src/applications/maniphest/typeahead/ManiphestTaskStatusDatasource.php new file mode 100644 index 0000000000..e7eba2923c --- /dev/null +++ b/src/applications/maniphest/typeahead/ManiphestTaskStatusDatasource.php @@ -0,0 +1,30 @@ +getViewer(); + $raw_query = $this->getRawQuery(); + + $results = array(); + + $status_map = ManiphestTaskStatus::getTaskStatusMap(); + foreach ($status_map as $value => $name) { + // NOTE: $value is not a PHID but is unique. This'll work. + $results[] = id(new PhabricatorTypeaheadResult()) + ->setPHID($value) + ->setName($name); + } + + return $results; + } +} diff --git a/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php b/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php index 8ac1c2c9bf..582e48d7f6 100644 --- a/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php +++ b/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php @@ -124,6 +124,31 @@ abstract class PhabricatorMailReplyHandler { return $body; } + final public function getRecipientsSummaryHTML( + array $to_handles, + array $cc_handles) { + assert_instances_of($to_handles, 'PhabricatorObjectHandle'); + assert_instances_of($cc_handles, 'PhabricatorObjectHandle'); + + if (PhabricatorEnv::getEnvConfig('metamta.recipients.show-hints')) { + $body = array(); + if ($to_handles) { + $body[] = phutil_tag('strong', array(), 'To: '); + $body[] = phutil_implode_html(', ', mpull($to_handles, 'getName')); + $body[] = phutil_tag('br'); + } + if ($cc_handles) { + $body[] = phutil_tag('strong', array(), 'Cc: '); + $body[] = phutil_implode_html(', ', mpull($cc_handles, 'getName')); + $body[] = phutil_tag('br'); + } + return phutil_tag('div', array(), $body); + } else { + return ''; + } + + } + final public function multiplexMail( PhabricatorMetaMTAMail $mail_template, array $to_handles, @@ -184,8 +209,13 @@ abstract class PhabricatorMailReplyHandler { $body .= "\n"; $body .= $this->getRecipientsSummary($to_handles, $cc_handles); - foreach ($recipients as $phid => $recipient) { + $html_body = $mail_template->getHTMLBody(); + if (strlen($html_body)) { + $html_body .= hsprintf('%s', + $this->getRecipientsSummaryHTML($to_handles, $cc_handles)); + } + foreach ($recipients as $phid => $recipient) { $mail = clone $mail_template; if (isset($to_handles[$phid])) { @@ -198,6 +228,7 @@ abstract class PhabricatorMailReplyHandler { } $mail->setBody($body); + $mail->setHTMLBody($html_body); $reply_to = null; if (!$reply_to && $this->supportsPrivateReplies()) { diff --git a/src/applications/metamta/view/PhabricatorMetaMTAMailBody.php b/src/applications/metamta/view/PhabricatorMetaMTAMailBody.php index e1ef4d6373..8d6c82cfc2 100644 --- a/src/applications/metamta/view/PhabricatorMetaMTAMailBody.php +++ b/src/applications/metamta/view/PhabricatorMetaMTAMailBody.php @@ -12,6 +12,15 @@ final class PhabricatorMetaMTAMailBody { private $htmlSections = array(); private $attachments = array(); + private $viewer; + + public function getViewer() { + return $this->viewer; + } + + public function setViewer($viewer) { + $this->viewer = $viewer; + } /* -( Composition )-------------------------------------------------------- */ @@ -33,6 +42,39 @@ final class PhabricatorMetaMTAMailBody { return $this; } + public function addRemarkupSection($text) { + try { + $engine = PhabricatorMarkupEngine::newMarkupEngine(array()); + $engine->setConfig('viewer', $this->getViewer()); + $engine->setMode(PhutilRemarkupEngine::MODE_TEXT); + $styled_text = $engine->markupText($text); + $this->sections[] = $styled_text; + } catch (Exception $ex) { + phlog($ex); + $this->sections[] = $text; + } + + try { + $mail_engine = PhabricatorMarkupEngine::newMarkupEngine(array()); + $mail_engine->setConfig('viewer', $this->getViewer()); + $mail_engine->setMode(PhutilRemarkupEngine::MODE_HTML_MAIL); + $mail_engine->setConfig( + 'uri.base', + PhabricatorEnv::getProductionURI('/')); + $html = $mail_engine->markupText($text); + $this->htmlSections[] = $html; + } catch (Exception $ex) { + phlog($ex); + $this->htmlSections[] = phutil_escape_html_newlines( + phutil_tag( + 'div', + array(), + $text)); + } + + return $this; + } + public function addRawPlaintextSection($text) { if (strlen($text)) { $text = rtrim($text); @@ -137,6 +179,23 @@ final class PhabricatorMetaMTAMailBody { return $this; } + /** + * Add a section with a link to email preferences. + * + * @return this + * @task compose + */ + public function addEmailPreferenceSection() { + if (!PhabricatorEnv::getEnvConfig('metamta.email-preferences')) { + return $this; + } + + $href = PhabricatorEnv::getProductionURI( + '/settings/panel/emailpreferences/'); + $this->addLinkSection(pht('EMAIL PREFERENCES'), $href); + + return $this; + } /** * Add an attachment. diff --git a/src/applications/passphrase/conduit/PassphraseQueryConduitAPIMethod.php b/src/applications/passphrase/conduit/PassphraseQueryConduitAPIMethod.php index cf0d6a1b37..c0ec34acf5 100644 --- a/src/applications/passphrase/conduit/PassphraseQueryConduitAPIMethod.php +++ b/src/applications/passphrase/conduit/PassphraseQueryConduitAPIMethod.php @@ -79,6 +79,7 @@ final class PassphraseQueryConduitAPIMethod $material['publicKey'] = $public_key; } break; + case PassphraseCredentialTypeSSHGeneratedKey::CREDENTIAL_TYPE: case PassphraseCredentialTypeSSHPrivateKeyText::CREDENTIAL_TYPE: if ($secret) { $material['privateKey'] = $secret; diff --git a/src/applications/people/customfield/PhabricatorUserBlurbField.php b/src/applications/people/customfield/PhabricatorUserBlurbField.php index 8bd903e57c..3221a33202 100644 --- a/src/applications/people/customfield/PhabricatorUserBlurbField.php +++ b/src/applications/people/customfield/PhabricatorUserBlurbField.php @@ -52,6 +52,7 @@ final class PhabricatorUserBlurbField public function renderEditControl(array $handles) { return id(new PhabricatorRemarkupControl()) + ->setUser($this->getViewer()) ->setName($this->getFieldKey()) ->setValue($this->value) ->setLabel($this->getFieldName()); diff --git a/src/applications/people/markup/PhabricatorMentionRemarkupRule.php b/src/applications/people/markup/PhabricatorMentionRemarkupRule.php index 49f56251f0..622b28998e 100644 --- a/src/applications/people/markup/PhabricatorMentionRemarkupRule.php +++ b/src/applications/people/markup/PhabricatorMentionRemarkupRule.php @@ -100,22 +100,41 @@ final class PhabricatorMentionRemarkupRule extends PhutilRemarkupRule { $user = $actual_users[$username]; Javelin::initBehavior('phabricator-hovercards'); - $tag = id(new PHUITagView()) - ->setType(PHUITagView::TYPE_PERSON) - ->setPHID($user->getPHID()) - ->setName('@'.$user->getUserName()) - ->setHref('/p/'.$user->getUserName().'/'); + $user_href = '/p/'.$user->getUserName().'/'; - if (!$user->isUserActivated()) { - $tag->setDotColor(PHUITagView::COLOR_GREY); + if ($engine->isHTMLMailMode()) { + $user_href = PhabricatorEnv::getProductionURI($user_href); + $tag = phutil_tag( + 'a', + array( + 'href' => $user_href, + 'style' => 'background-color: #f1f7ff; + border-color: #f1f7ff; + border: 1px solid transparent; + border-radius: 3px; + color: #19558d; + font-weight: bold; + padding: 0 4px;', + ), + '@'.$user->getUserName()); } else { - $status = idx($user_statuses, $user->getPHID()); - if ($status) { - $status = $status->getStatus(); - if ($status == PhabricatorCalendarEvent::STATUS_AWAY) { - $tag->setDotColor(PHUITagView::COLOR_RED); - } else if ($status == PhabricatorCalendarEvent::STATUS_AWAY) { - $tag->setDotColor(PHUITagView::COLOR_ORANGE); + $tag = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_PERSON) + ->setPHID($user->getPHID()) + ->setName('@'.$user->getUserName()) + ->setHref($user_href); + + if (!$user->isUserActivated()) { + $tag->setDotColor(PHUITagView::COLOR_GREY); + } else { + $status = idx($user_statuses, $user->getPHID()); + if ($status) { + $status = $status->getStatus(); + if ($status == PhabricatorCalendarEvent::STATUS_AWAY) { + $tag->setDotColor(PHUITagView::COLOR_RED); + } else if ($status == PhabricatorCalendarEvent::STATUS_AWAY) { + $tag->setDotColor(PHUITagView::COLOR_ORANGE); + } } } } diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php index 52c06886ea..33f184f36a 100644 --- a/src/applications/people/storage/PhabricatorUser.php +++ b/src/applications/people/storage/PhabricatorUser.php @@ -9,7 +9,8 @@ final class PhabricatorUser PhutilPerson, PhabricatorPolicyInterface, PhabricatorCustomFieldInterface, - PhabricatorDestructibleInterface { + PhabricatorDestructibleInterface, + PhabricatorSSHPublicKeyInterface { const SESSION_TABLE = 'phabricator_session'; const NAMETOKEN_TABLE = 'user_nametoken'; @@ -1000,4 +1001,22 @@ EOBODY; } +/* -( PhabricatorSSHPublicKeyInterface )----------------------------------- */ + + + public function getSSHPublicKeyManagementURI(PhabricatorUser $viewer) { + if ($viewer->getPHID() == $this->getPHID()) { + // If the viewer is managing their own keys, take them to the normal + // panel. + return '/settings/panel/ssh/'; + } else { + // Otherwise, take them to the administrative panel for this user. + return '/settings/'.$this->getID().'/panel/ssh/'; + } + } + + public function getSSHKeyDefaultName() { + return 'id_rsa_phabricator'; + } + } diff --git a/src/applications/phame/controller/blog/PhameBlogEditController.php b/src/applications/phame/controller/blog/PhameBlogEditController.php index e8ec34d19c..1363cf8343 100644 --- a/src/applications/phame/controller/blog/PhameBlogEditController.php +++ b/src/applications/phame/controller/blog/PhameBlogEditController.php @@ -122,12 +122,13 @@ final class PhameBlogEditController ->setError($e_name)) ->appendChild( id(new PhabricatorRemarkupControl()) - ->setLabel(pht('Description')) - ->setName('description') - ->setValue($blog->getDescription()) - ->setID('blog-description') - ->setUser($user) - ->setDisableMacros(true)) + ->setUser($user) + ->setLabel(pht('Description')) + ->setName('description') + ->setValue($blog->getDescription()) + ->setID('blog-description') + ->setUser($user) + ->setDisableMacros(true)) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) diff --git a/src/applications/pholio/controller/PholioInlineController.php b/src/applications/pholio/controller/PholioInlineController.php index b17679c758..782383cdb8 100644 --- a/src/applications/pholio/controller/PholioInlineController.php +++ b/src/applications/pholio/controller/PholioInlineController.php @@ -157,6 +157,7 @@ final class PholioInlineController extends PholioController { $form ->appendChild( id(new PhabricatorRemarkupControl()) + ->setUser($viewer) ->setName('content') ->setLabel(pht('Comment')) ->setValue($v_content)); diff --git a/src/applications/pholio/view/PholioUploadedImageView.php b/src/applications/pholio/view/PholioUploadedImageView.php index 766264ee9b..fb8a82431e 100644 --- a/src/applications/pholio/view/PholioUploadedImageView.php +++ b/src/applications/pholio/view/PholioUploadedImageView.php @@ -32,6 +32,7 @@ final class PholioUploadedImageView extends AphrontView { ->setLabel(pht('Title')); $description = id(new PhabricatorRemarkupControl()) + ->setUser($this->getUser()) ->setName('description_'.$phid) ->setValue($image->getDescription()) ->setSigil('image-description') diff --git a/src/applications/phortune/controller/PhortuneMerchantEditController.php b/src/applications/phortune/controller/PhortuneMerchantEditController.php index fa7b5391d1..2b66209761 100644 --- a/src/applications/phortune/controller/PhortuneMerchantEditController.php +++ b/src/applications/phortune/controller/PhortuneMerchantEditController.php @@ -131,6 +131,7 @@ final class PhortuneMerchantEditController ->setError($e_name)) ->appendChild( id(new PhabricatorRemarkupControl()) + ->setUser($viewer) ->setName('desc') ->setLabel(pht('Description')) ->setValue($v_desc)) diff --git a/src/applications/phriction/application/PhabricatorPhrictionApplication.php b/src/applications/phriction/application/PhabricatorPhrictionApplication.php index b2cc472341..d8a7923b04 100644 --- a/src/applications/phriction/application/PhabricatorPhrictionApplication.php +++ b/src/applications/phriction/application/PhabricatorPhrictionApplication.php @@ -52,7 +52,7 @@ final class PhabricatorPhrictionApplication extends PhabricatorApplication { 'edit/(?:(?P[1-9]\d*)/)?' => 'PhrictionEditController', 'delete/(?P[1-9]\d*)/' => 'PhrictionDeleteController', 'new/' => 'PhrictionNewController', - 'move/(?:(?P[1-9]\d*)/)?' => 'PhrictionMoveController', + 'move/(?P[1-9]\d*)/' => 'PhrictionMoveController', 'preview/' => 'PhabricatorMarkupPreviewController', 'diff/(?P[1-9]\d*)/' => 'PhrictionDiffController', diff --git a/src/applications/phriction/controller/PhrictionDocumentController.php b/src/applications/phriction/controller/PhrictionDocumentController.php index 2f1cc2f6e5..570cc8014b 100644 --- a/src/applications/phriction/controller/PhrictionDocumentController.php +++ b/src/applications/phriction/controller/PhrictionDocumentController.php @@ -38,19 +38,8 @@ final class PhrictionDocumentController if (!$document) { - $document = new PhrictionDocument(); + $document = PhrictionDocument::initializeNewDocument($user, $slug); - if (PhrictionDocument::isProjectSlug($slug)) { - $project = id(new PhabricatorProjectQuery()) - ->setViewer($user) - ->withPhrictionSlugs(array( - PhrictionDocument::getProjectSlugIdentifier($slug), - )) - ->executeOne(); - if (!$project) { - return new Aphront404Response(); - } - } $create_uri = '/phriction/edit/?slug='.$slug; $notice = new AphrontErrorView(); @@ -258,34 +247,10 @@ final class PhrictionDocumentController ->setUser($viewer) ->setObject($document); - $project_phid = null; - if (PhrictionDocument::isProjectSlug($slug)) { - $project = id(new PhabricatorProjectQuery()) - ->setViewer($viewer) - ->withPhrictionSlugs(array( - PhrictionDocument::getProjectSlugIdentifier($slug), - )) - ->executeOne(); - if ($project) { - $project_phid = $project->getPHID(); - } - } - - $phids = array_filter( - array( - $content->getAuthorPHID(), - $project_phid, - )); + $phids = array($content->getAuthorPHID()); $this->loadHandles($phids); - $project_info = null; - if ($project_phid) { - $view->addProperty( - pht('Project Info'), - $this->getHandle($project_phid)->renderLink()); - } - $view->addProperty( pht('Last Author'), $this->getHandle($content->getAuthorPHID())->renderLink()); @@ -358,33 +323,24 @@ final class PhrictionDocumentController } private function renderDocumentChildren($slug) { - $document_dao = new PhrictionDocument(); - $content_dao = new PhrictionContent(); - $conn = $document_dao->establishConnection('r'); - $limit = 250; $d_child = PhabricatorSlug::getDepth($slug) + 1; $d_grandchild = PhabricatorSlug::getDepth($slug) + 2; + $limit = 250; - // Select children and grandchildren. - $children = queryfx_all( - $conn, - 'SELECT d.slug, d.depth, c.title FROM %T d JOIN %T c - ON d.contentID = c.id - WHERE d.slug LIKE %> AND d.depth IN (%d, %d) - AND d.status IN (%Ld) - ORDER BY d.depth, c.title LIMIT %d', - $document_dao->getTableName(), - $content_dao->getTableName(), - ($slug == '/' ? '' : $slug), - $d_child, - $d_grandchild, - array( + $query = id(new PhrictionDocumentQuery()) + ->setViewer($this->getRequest()->getUser()) + ->withDepths(array($d_child, $d_grandchild)) + ->withSlugPrefix($slug == '/' ? '' : $slug) + ->withStatuses(array( PhrictionDocumentStatus::STATUS_EXISTS, PhrictionDocumentStatus::STATUS_STUB, - ), - $limit); + )) + ->setLimit($limit) + ->setOrder(PhrictionDocumentQuery::ORDER_HIERARCHY) + ->needContent(true); + $children = $query->execute(); if (!$children) { return; } @@ -405,7 +361,7 @@ final class PhrictionDocumentController if (count($children) == $limit) { $more_children = true; foreach ($children as $child) { - if ($child['depth'] == $d_grandchild) { + if ($child->getDepth() == $d_grandchild) { $more_children = false; } } @@ -415,24 +371,30 @@ final class PhrictionDocumentController $more_children = false; } - $grandchildren = array(); + $children_dicts = array(); + $grandchildren_dicts = array(); foreach ($children as $key => $child) { - if ($child['depth'] == $d_child) { + $child_dict = array( + 'slug' => $child->getSlug(), + 'depth' => $child->getDepth(), + 'title' => $child->getContent()->getTitle(),); + if ($child->getDepth() == $d_child) { + $children_dicts[] = $child_dict; continue; } else { unset($children[$key]); if ($show_grandchildren) { - $ancestors = PhabricatorSlug::getAncestry($child['slug']); - $grandchildren[end($ancestors)][] = $child; + $ancestors = PhabricatorSlug::getAncestry($child->getSlug()); + $grandchildren_dicts[end($ancestors)][] = $child_dict; } } } // Fill in any missing children. - $known_slugs = ipull($children, null, 'slug'); - foreach ($grandchildren as $slug => $ignored) { + $known_slugs = mpull($children, null, 'getSlug'); + foreach ($grandchildren_dicts as $slug => $ignored) { if (empty($known_slugs[$slug])) { - $children[] = array( + $children_dicts[] = array( 'slug' => $slug, 'depth' => $d_child, 'title' => PhabricatorSlug::getDefaultTitle($slug), @@ -441,13 +403,13 @@ final class PhrictionDocumentController } } - $children = isort($children, 'title'); + $children_dicts = isort($children_dicts, 'title'); $list = array(); - foreach ($children as $child) { + foreach ($children_dicts as $child) { $list[] = hsprintf('
  • '); $list[] = $this->renderChildDocumentLink($child); - $grand = idx($grandchildren, $child['slug'], array()); + $grand = idx($grandchildren_dicts, $child['slug'], array()); if ($grand) { $list[] = hsprintf('
      '); foreach ($grand as $grandchild) { diff --git a/src/applications/phriction/controller/PhrictionEditController.php b/src/applications/phriction/controller/PhrictionEditController.php index 3892ab9578..8433640040 100644 --- a/src/applications/phriction/controller/PhrictionEditController.php +++ b/src/applications/phriction/controller/PhrictionEditController.php @@ -62,17 +62,6 @@ final class PhrictionEditController $content = $document->getContent(); $current_version = $content->getVersion(); } else { - if (PhrictionDocument::isProjectSlug($slug)) { - $project = id(new PhabricatorProjectQuery()) - ->setViewer($user) - ->withPhrictionSlugs(array( - PhrictionDocument::getProjectSlugIdentifier($slug), - )) - ->executeOne(); - if (!$project) { - return new Aphront404Response(); - } - } $document = PhrictionDocument::initializeNewDocument($user, $slug); $content = $document->getContent(); } diff --git a/src/applications/phriction/controller/PhrictionMoveController.php b/src/applications/phriction/controller/PhrictionMoveController.php index ce4dbe240c..21c65f0495 100644 --- a/src/applications/phriction/controller/PhrictionMoveController.php +++ b/src/applications/phriction/controller/PhrictionMoveController.php @@ -2,79 +2,72 @@ final class PhrictionMoveController extends PhrictionController { - private $id; - - public function willProcessRequest(array $data) { - $this->id = idx($data, 'id'); - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); - - if ($this->id) { - $document = id(new PhrictionDocumentQuery()) - ->setViewer($user) - ->withIDs(array($this->id)) - ->needContent(true) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - } else { - $slug = PhabricatorSlug::normalize( - $request->getStr('slug')); - if (!$slug) { - return new Aphront404Response(); - } - - $document = id(new PhrictionDocumentQuery()) - ->setViewer($user) - ->withSlugs(array($slug)) - ->needContent(true) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - } + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); + $document = id(new PhrictionDocumentQuery()) + ->setViewer($viewer) + ->withIDs(array($request->getURIData('id'))) + ->needContent(true) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); if (!$document) { return new Aphront404Response(); } - if (!isset($slug)) { - $slug = $document->getSlug(); - } - - $target_slug = PhabricatorSlug::normalize( - $request->getStr('new-slug', $slug)); - - $submit_uri = $request->getRequestURI()->getPath(); + $slug = $document->getSlug(); $cancel_uri = PhrictionDocument::getSlugURI($slug); - $e_url = true; - $validation_exception = null; - $content = $document->getContent(); + $v_slug = $slug; + $e_slug = null; + $v_note = ''; + + $validation_exception = null; if ($request->isFormPost()) { + $v_note = $request->getStr('description'); + $v_slug = $request->getStr('slug'); + + // If what the user typed isn't what we're actually using, warn them + // about it. + if (strlen($v_slug)) { + $normal_slug = PhabricatorSlug::normalize($v_slug); + if ($normal_slug !== $v_slug) { + return $this->newDialog() + ->setTitle(pht('Adjust Path')) + ->appendParagraph( + pht( + 'The path you entered (%s) is not a valid wiki document '. + 'path. Paths may not contain special characters.', + phutil_tag('strong', array(), $v_slug))) + ->appendParagraph( + pht( + 'Would you like to use the path %s instead?', + phutil_tag('strong', array(), $normal_slug))) + ->addHiddenInput('slug', $normal_slug) + ->addHiddenInput('description', $v_note) + ->addCancelButton($cancel_uri) + ->addSubmitButton(pht('Accept Path')); + } + } $editor = id(new PhrictionTransactionEditor()) - ->setActor($user) + ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) - ->setDescription($request->getStr('description')); + ->setDescription($v_note); $xactions = array(); $xactions[] = id(new PhrictionTransaction()) ->setTransactionType(PhrictionTransaction::TYPE_MOVE_TO) ->setNewValue($document); $target_document = PhrictionDocument::initializeNewDocument( - $user, - $target_slug); + $viewer, + $v_slug); try { $editor->applyTransactions($target_document, $xactions); $redir_uri = PhrictionDocument::getSlugURI( @@ -82,40 +75,40 @@ final class PhrictionMoveController extends PhrictionController { return id(new AphrontRedirectResponse())->setURI($redir_uri); } catch (PhabricatorApplicationTransactionValidationException $ex) { $validation_exception = $ex; - $e_url = $ex->getShortMessage(PhrictionTransaction::TYPE_MOVE_TO); + $e_slug = $ex->getShortMessage(PhrictionTransaction::TYPE_MOVE_TO); } } - $form = id(new PHUIFormLayoutView()) - ->setUser($user) + + $form = id(new AphrontFormView()) + ->setUser($viewer) ->appendChild( id(new AphrontFormStaticControl()) - ->setLabel(pht('Title')) - ->setValue($content->getTitle())) + ->setLabel(pht('Title')) + ->setValue($document->getContent()->getTitle())) ->appendChild( id(new AphrontFormTextControl()) - ->setLabel(pht('New URI')) - ->setValue($target_slug) - ->setError($e_url) - ->setName('new-slug') - ->setCaption(pht('The new location of the document.'))) + ->setLabel(pht('Current Path')) + ->setDisabled(true) + ->setValue($slug)) ->appendChild( id(new AphrontFormTextControl()) - ->setLabel(pht('Edit Notes')) - ->setValue(pht('Moving document to a new location.')) - ->setError(null) - ->setName('description')); + ->setLabel(pht('New Path')) + ->setValue($v_slug) + ->setError($e_slug) + ->setName('slug')) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel(pht('Edit Notes')) + ->setValue($v_note) + ->setName('description')); - $dialog = id(new AphrontDialogView()) - ->setUser($user) - ->setValidationException($validation_exception) + return $this->newDialog() ->setTitle(pht('Move Document')) - ->appendChild($form) - ->setSubmitURI($submit_uri) + ->setValidationException($validation_exception) + ->appendForm($form) ->addSubmitButton(pht('Move Document')) ->addCancelButton($cancel_uri); - - return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/phriction/controller/PhrictionNewController.php b/src/applications/phriction/controller/PhrictionNewController.php index 333600287c..48cd5fc8f0 100644 --- a/src/applications/phriction/controller/PhrictionNewController.php +++ b/src/applications/phriction/controller/PhrictionNewController.php @@ -30,25 +30,6 @@ final class PhrictionNewController extends PhrictionController { ->addSubmitButton(pht('Edit Document')); return id(new AphrontDialogResponse())->setDialog($dialog); - } else if (PhrictionDocument::isProjectSlug($slug)) { - $project = id(new PhabricatorProjectQuery()) - ->setViewer($user) - ->withPhrictionSlugs(array( - PhrictionDocument::getProjectSlugIdentifier($slug), - )) - ->executeOne(); - if (!$project) { - $dialog = new AphrontDialogView(); - $dialog->setSubmitURI('/w/') - ->setTitle(pht('Oops!')) - ->setUser($user) - ->appendChild(pht( - 'You cannot create wiki pages under "projects/", - because they are reserved as project pages. - Create a new project with this name first.')) - ->addCancelButton('/w/', 'Okay'); - return id(new AphrontDialogResponse())->setDialog($dialog); - } } $uri = '/phriction/edit/?slug='.$slug; diff --git a/src/applications/phriction/editor/PhrictionTransactionEditor.php b/src/applications/phriction/editor/PhrictionTransactionEditor.php index af40cf117e..c5c6498b85 100644 --- a/src/applications/phriction/editor/PhrictionTransactionEditor.php +++ b/src/applications/phriction/editor/PhrictionTransactionEditor.php @@ -10,6 +10,7 @@ final class PhrictionTransactionEditor private $skipAncestorCheck; private $contentVersion; private $processContentVersionError = true; + private $heraldEmailPHIDs = array(); public function setDescription($description) { $this->description = $description; @@ -359,6 +360,16 @@ final class PhrictionTransactionEditor ); } + protected function getMailCC(PhabricatorLiskDAO $object) { + $phids = array(); + + foreach ($this->heraldEmailPHIDs as $phid) { + $phids[] = $phid; + } + + return $phids; + } + public function getMailTagsMap() { return array( PhrictionTransaction::MAILTAG_TITLE => @@ -394,6 +405,24 @@ final class PhrictionTransactionEditor $body->addTextSection( pht('DOCUMENT CONTENT'), $object->getContent()->getContent()); + } else { + + foreach ($xactions as $xaction) { + switch ($xaction->getTransactionType()) { + case PhrictionTransaction::TYPE_CONTENT: + $diff_uri = id(new PhutilURI( + '/phriction/diff/'.$object->getID().'/')) + ->alter('l', $this->getOldContent()->getVersion()) + ->alter('r', $this->getNewContent()->getVersion()); + $body->addLinkSection( + pht('DOCUMENT DIFF'), + PhabricatorEnv::getProductionURI($diff_uri)); + break 2; + default: + break; + } + } + } $body->addLinkSection( @@ -526,13 +555,24 @@ final class PhrictionTransactionEditor ->needContent(true) ->executeOne(); - // Considering to overwrite existing docs? Nuke this! + // Prevent overwrites and no-op moves. $exists = PhrictionDocumentStatus::STATUS_EXISTS; - if ($target_document && $target_document->getStatus() == $exists) { + if ($target_document) { + if ($target_document->getSlug() == $source_document->getSlug()) { + $message = pht( + 'You can not move a document to its existing location. '. + 'Choose a different location to move the document to.'); + } else if ($target_document->getStatus() == $exists) { + $message = pht( + 'You can not move this document there, because it would '. + 'overwrite an existing document which is already at that '. + 'location. Move or delete the existing document first.'); + } + $error = new PhabricatorApplicationTransactionValidationError( $type, - pht('Can not move document.'), - pht('Can not overwrite existing target document.'), + pht('Invalid'), + $message, $xaction); $errors[] = $error; } @@ -587,6 +627,58 @@ final class PhrictionTransactionEditor } return $error; } + protected function requireCapabilities( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + /* + * New objects have a special case. If a user can't see + * x/y + * then definitely don't let them make some + * x/y/z + * We need to load the direct parent to handle this case. + */ + if ($this->getIsNewObject()) { + $actor = $this->requireActor(); + $parent_doc = null; + $ancestral_slugs = PhabricatorSlug::getAncestry($object->getSlug()); + // No ancestral slugs is "/"; the first person gets to play with "/". + if ($ancestral_slugs) { + $parent = end($ancestral_slugs); + $parent_doc = id(new PhrictionDocumentQuery()) + ->setViewer($actor) + ->withSlugs(array($parent)) + ->executeOne(); + // If the $actor can't see the $parent_doc then they can't create + // the child $object; throw a policy exception. + if (!$parent_doc) { + id(new PhabricatorPolicyFilter()) + ->setViewer($actor) + ->raisePolicyExceptions(true) + ->rejectObject( + $object, + $object->getEditPolicy(), + PhabricatorPolicyCapability::CAN_EDIT); + } + + // If the $actor can't edit the $parent_doc then they can't create + // the child $object; throw a policy exception. + if (!PhabricatorPolicyFilter::hasCapability( + $actor, + $parent_doc, + PhabricatorPolicyCapability::CAN_EDIT)) { + id(new PhabricatorPolicyFilter()) + ->setViewer($actor) + ->raisePolicyExceptions(true) + ->rejectObject( + $object, + $object->getEditPolicy(), + PhabricatorPolicyCapability::CAN_EDIT); + } + } + } + return parent::requireCapabilities($object, $xaction); + } protected function supportsSearch() { return true; @@ -595,7 +687,35 @@ final class PhrictionTransactionEditor protected function shouldApplyHeraldRules( PhabricatorLiskDAO $object, array $xactions) { - return false; + return true; + } + + protected function buildHeraldAdapter( + PhabricatorLiskDAO $object, + array $xactions) { + + return id(new PhrictionDocumentHeraldAdapter()) + ->setDocument($object); + } + + protected function didApplyHeraldRules( + PhabricatorLiskDAO $object, + HeraldAdapter $adapter, + HeraldTranscript $transcript) { + + $xactions = array(); + + $cc_phids = $adapter->getCcPHIDs(); + if ($cc_phids) { + $value = array_fuse($cc_phids); + $xactions[] = id(new PhrictionTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) + ->setNewValue(array('+' => $value)); + } + + $this->heraldEmailPHIDs = $adapter->getEmailPHIDs(); + + return $xactions; } private function buildNewContentTemplate( diff --git a/src/applications/phriction/herald/PhrictionDocumentHeraldAdapter.php b/src/applications/phriction/herald/PhrictionDocumentHeraldAdapter.php new file mode 100644 index 0000000000..5e01d75258 --- /dev/null +++ b/src/applications/phriction/herald/PhrictionDocumentHeraldAdapter.php @@ -0,0 +1,169 @@ +document; + } + + public function setDocument(PhrictionDocument $document) { + $this->document = $document; + return $this; + } + public function getDocument() { + return $this->document; + } + + private function setCcPHIDs(array $cc_phids) { + $this->ccPHIDs = $cc_phids; + return $this; + } + public function getCcPHIDs() { + return $this->ccPHIDs; + } + + public function getEmailPHIDs() { + return $this->emailPHIDs; + } + + + public function getAdapterContentName() { + return pht('Phriction Documents'); + } + + public function supportsRuleType($rule_type) { + switch ($rule_type) { + case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: + case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: + return true; + case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: + default: + return false; + } + } + + public function getFields() { + return array_merge( + array( + self::FIELD_TITLE, + self::FIELD_BODY, + self::FIELD_AUTHOR, + self::FIELD_IS_NEW_OBJECT, + self::FIELD_CC, + self::FIELD_PATH, + ), + parent::getFields()); + } + + public function getActions($rule_type) { + switch ($rule_type) { + case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: + return array_merge( + array( + self::ACTION_ADD_CC, + self::ACTION_EMAIL, + self::ACTION_NOTHING, + ), + parent::getActions($rule_type)); + case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: + return array_merge( + array( + self::ACTION_ADD_CC, + self::ACTION_EMAIL, + self::ACTION_FLAG, + self::ACTION_NOTHING, + ), + parent::getActions($rule_type)); + } + } + + public function getPHID() { + return $this->getDocument()->getPHID(); + } + + public function getHeraldName() { + return pht('Wiki Document %d', $this->getDocument()->getID()); + } + + public function getHeraldField($field) { + switch ($field) { + case self::FIELD_TITLE: + return $this->getDocument()->getContent()->getTitle(); + case self::FIELD_BODY: + return $this->getDocument()->getContent()->getContent(); + case self::FIELD_AUTHOR: + return $this->getDocument()->getContent()->getAuthorPHID(); + case self::FIELD_CC: + return PhabricatorSubscribersQuery::loadSubscribersForPHID( + $this->getDocument()->getPHID()); + case self::FIELD_PATH: + return $this->getDocument()->getContent()->getSlug(); + } + + return parent::getHeraldField($field); + } + + public function applyHeraldEffects(array $effects) { + assert_instances_of($effects, 'HeraldEffect'); + + $result = array(); + foreach ($effects as $effect) { + $action = $effect->getAction(); + switch ($action) { + case self::ACTION_NOTHING: + $result[] = new HeraldApplyTranscript( + $effect, + true, + pht('Great success at doing nothing.')); + break; + case self::ACTION_ADD_CC: + foreach ($effect->getTarget() as $phid) { + $this->ccPHIDs[] = $phid; + } + $result[] = new HeraldApplyTranscript( + $effect, + true, + pht('Added address to cc list.')); + break; + case self::ACTION_FLAG: + $result[] = parent::applyFlagEffect( + $effect, + $this->getDocument()->getPHID()); + break; + case self::ACTION_EMAIL: + foreach ($effect->getTarget() as $phid) { + $this->emailPHIDs[] = $phid; + } + $result[] = new HeraldApplyTranscript( + $effect, + true, + pht('Added addresses to email list.')); + break; + default: + $custom_result = parent::handleCustomHeraldEffect($effect); + if ($custom_result === null) { + throw new Exception(pht( + "No rules to handle action '%s'.", + $action)); + } + + $result[] = $custom_result; + break; + } + } + return $result; + } + +} diff --git a/src/applications/phriction/markup/PhrictionRemarkupRule.php b/src/applications/phriction/markup/PhrictionRemarkupRule.php index 48eb9cc3ee..ba740e5f7b 100644 --- a/src/applications/phriction/markup/PhrictionRemarkupRule.php +++ b/src/applications/phriction/markup/PhrictionRemarkupRule.php @@ -29,9 +29,12 @@ final class PhrictionRemarkupRule extends PhutilRemarkupRule { $slug = PhrictionDocument::getSlugURI($slug); $href = (string)id(new PhutilURI($slug))->setFragment($fragment); + $text_mode = $this->getEngine()->isTextMode(); + $mail_mode = $this->getEngine()->isHTMLMailMode(); + if ($this->getEngine()->getState('toc')) { $text = $name; - } else if ($this->getEngine()->isTextMode()) { + } else if ($text_mode || $mail_mode) { return PhabricatorEnv::getProductionURI($href); } else { $text = $this->newTag( diff --git a/src/applications/phriction/query/PhrictionDocumentQuery.php b/src/applications/phriction/query/PhrictionDocumentQuery.php index 35b4ee1675..b4e5cbbaeb 100644 --- a/src/applications/phriction/query/PhrictionDocumentQuery.php +++ b/src/applications/phriction/query/PhrictionDocumentQuery.php @@ -6,6 +6,9 @@ final class PhrictionDocumentQuery private $ids; private $phids; private $slugs; + private $depths; + private $slugPrefix; + private $statuses; private $needContent; @@ -17,6 +20,7 @@ final class PhrictionDocumentQuery private $order = 'order-created'; const ORDER_CREATED = 'order-created'; const ORDER_UPDATED = 'order-updated'; + const ORDER_HIERARCHY = 'order-hierarchy'; public function withIDs(array $ids) { $this->ids = $ids; @@ -33,6 +37,21 @@ final class PhrictionDocumentQuery return $this; } + public function withDepths(array $depths) { + $this->depths = $depths; + return $this; + } + + public function withSlugPrefix($slug_prefix) { + $this->slugPrefix = $slug_prefix; + return $this; + } + + public function withStatuses(array $statuses) { + $this->statuses = $statuses; + return $this; + } + public function withStatus($status) { $this->status = $status; return $this; @@ -52,12 +71,19 @@ final class PhrictionDocumentQuery $table = new PhrictionDocument(); $conn_r = $table->establishConnection('r'); + if ($this->order == self::ORDER_HIERARCHY) { + $order_clause = $this->buildHierarchicalOrderClause($conn_r); + } else { + $order_clause = $this->buildOrderClause($conn_r); + } + $rows = queryfx_all( $conn_r, - 'SELECT * FROM %T %Q %Q %Q', + 'SELECT d.* FROM %T d %Q %Q %Q %Q', $table->getTableName(), + $this->buildJoinClause($conn_r), $this->buildWhereClause($conn_r), - $this->buildOrderClause($conn_r), + $order_clause, $this->buildLimitClause($conn_r)); $documents = $table->loadAllFromArray($rows); @@ -158,35 +184,67 @@ final class PhrictionDocumentQuery return $documents; } + private function buildJoinClause(AphrontDatabaseConnection $conn) { + $join = ''; + if ($this->order == self::ORDER_HIERARCHY) { + $content_dao = new PhrictionContent(); + $join = qsprintf( + $conn, + 'JOIN %T c ON d.contentID = c.id', + $content_dao->getTableName()); + } + return $join; + } protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->ids) { $where[] = qsprintf( $conn, - 'id IN (%Ld)', + 'd.id IN (%Ld)', $this->ids); } if ($this->phids) { $where[] = qsprintf( $conn, - 'phid IN (%Ls)', + 'd.phid IN (%Ls)', $this->phids); } if ($this->slugs) { $where[] = qsprintf( $conn, - 'slug IN (%Ls)', + 'd.slug IN (%Ls)', $this->slugs); } + if ($this->statuses) { + $where[] = qsprintf( + $conn, + 'd.status IN (%Ld)', + $this->statuses); + } + + if ($this->slugPrefix) { + $where[] = qsprintf( + $conn, + 'd.slug LIKE %>', + $this->slugPrefix); + } + + if ($this->depths) { + $where[] = qsprintf( + $conn, + 'd.depth IN (%Ld)', + $this->depths); + } + switch ($this->status) { case self::STATUS_OPEN: $where[] = qsprintf( $conn, - 'status NOT IN (%Ld)', + 'd.status NOT IN (%Ld)', array( PhrictionDocumentStatus::STATUS_DELETED, PhrictionDocumentStatus::STATUS_MOVED, @@ -196,7 +254,7 @@ final class PhrictionDocumentQuery case self::STATUS_NONSTUB: $where[] = qsprintf( $conn, - 'status NOT IN (%Ld)', + 'd.status NOT IN (%Ld)', array( PhrictionDocumentStatus::STATUS_MOVED, PhrictionDocumentStatus::STATUS_STUB, @@ -213,12 +271,31 @@ final class PhrictionDocumentQuery return $this->formatWhereClause($where); } + private function buildHierarchicalOrderClause( + AphrontDatabaseConnection $conn_r) { + + if ($this->getBeforeID()) { + return qsprintf( + $conn_r, + 'ORDER BY d.depth, c.title, %Q %Q', + $this->getPagingColumn(), + $this->getReversePaging() ? 'DESC' : 'ASC'); + } else { + return qsprintf( + $conn_r, + 'ORDER BY d.depth, c.title, %Q %Q', + $this->getPagingColumn(), + $this->getReversePaging() ? 'ASC' : 'DESC'); + } + } + protected function getPagingColumn() { switch ($this->order) { case self::ORDER_CREATED: - return 'id'; + case self::ORDER_HIERARCHY: + return 'd.id'; case self::ORDER_UPDATED: - return 'contentID'; + return 'd.contentID'; default: throw new Exception("Unknown order '{$this->order}'!"); } @@ -227,6 +304,7 @@ final class PhrictionDocumentQuery protected function getPagingValue($result) { switch ($this->order) { case self::ORDER_CREATED: + case self::ORDER_HIERARCHY: return $result->getID(); case self::ORDER_UPDATED: return $result->getContentID(); diff --git a/src/applications/phriction/storage/PhrictionDocument.php b/src/applications/phriction/storage/PhrictionDocument.php index 40606fc768..fe1ed78a48 100644 --- a/src/applications/phriction/storage/PhrictionDocument.php +++ b/src/applications/phriction/storage/PhrictionDocument.php @@ -6,7 +6,8 @@ final class PhrictionDocument extends PhrictionDAO PhabricatorSubscribableInterface, PhabricatorFlaggableInterface, PhabricatorTokenReceiverInterface, - PhabricatorDestructibleInterface { + PhabricatorDestructibleInterface, + PhabricatorApplicationTransactionInterface { protected $slug; protected $depth; @@ -144,26 +145,6 @@ final class PhrictionDocument extends PhrictionDAO return $this; } - public static function isProjectSlug($slug) { - $slug = PhabricatorSlug::normalize($slug); - $prefix = 'projects/'; - if ($slug == $prefix) { - // The 'projects/' document is not itself a project slug. - return false; - } - return !strncmp($slug, $prefix, strlen($prefix)); - } - - public static function getProjectSlugIdentifier($slug) { - if (!self::isProjectSlug($slug)) { - throw new Exception("Slug '{$slug}' is not a project slug!"); - } - - $slug = PhabricatorSlug::normalize($slug); - $parts = explode('/', $slug); - return $parts[1].'/'; - } - /* -( PhabricatorPolicyInterface )----------------------------------------- */ @@ -220,6 +201,22 @@ final class PhrictionDocument extends PhrictionDAO return true; } +/* -( PhabricatorApplicationTransactionInterface )------------------------- */ + + + public function getApplicationTransactionEditor() { + return new PhrictionTransactionEditor(); + } + + public function getApplicationTransactionObject() { + return $this; + } + + public function getApplicationTransactionTemplate() { + return new PhrictionTransaction(); + } + + /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ diff --git a/src/applications/phriction/storage/__tests__/PhrictionDocumentTestCase.php b/src/applications/phriction/storage/__tests__/PhrictionDocumentTestCase.php deleted file mode 100644 index ede8ec3429..0000000000 --- a/src/applications/phriction/storage/__tests__/PhrictionDocumentTestCase.php +++ /dev/null @@ -1,48 +0,0 @@ - false, - 'zebra/' => false, - 'projects/' => false, - 'projects/a/' => true, - 'projects/a/b/' => true, - 'stuff/projects/a/' => false, - ); - - foreach ($slugs as $slug => $expect) { - $this->assertEqual( - $expect, - PhrictionDocument::isProjectSlug($slug), - "Is '{$slug}' a project slug?"); - } - } - - public function testProjectSlugIdentifiers() { - $slugs = array( - 'projects/' => null, - 'derp/' => null, - 'projects/a/' => 'a/', - 'projects/a/b/' => 'a/', - ); - - foreach ($slugs as $slug => $expect) { - $ex = null; - $result = null; - try { - $result = PhrictionDocument::getProjectSlugIdentifier($slug); - } catch (Exception $e) { - $ex = $e; - } - - if ($expect === null) { - $this->assertTrue((bool)$ex, "Slug '{$slug}' is invalid."); - } else { - $this->assertEqual($expect, $result, "Slug '{$slug}' identifier."); - } - } - } - -} diff --git a/src/applications/ponder/controller/PonderAnswerEditController.php b/src/applications/ponder/controller/PonderAnswerEditController.php index 30f97e1177..80273bb053 100644 --- a/src/applications/ponder/controller/PonderAnswerEditController.php +++ b/src/applications/ponder/controller/PonderAnswerEditController.php @@ -71,6 +71,7 @@ final class PonderAnswerEditController extends PonderController { ->setValue($question->getTitle())) ->appendChild( id(new PhabricatorRemarkupControl()) + ->setUser($viewer) ->setLabel(pht('Answer')) ->setName('content') ->setID($answer_content_id) diff --git a/src/applications/ponder/controller/PonderQuestionEditController.php b/src/applications/ponder/controller/PonderQuestionEditController.php index 557c2c8910..117eefa461 100644 --- a/src/applications/ponder/controller/PonderQuestionEditController.php +++ b/src/applications/ponder/controller/PonderQuestionEditController.php @@ -98,6 +98,7 @@ final class PonderQuestionEditController extends PonderController { ->setError($e_title)) ->appendChild( id(new PhabricatorRemarkupControl()) + ->setUser($user) ->setName('content') ->setID('content') ->setValue($v_content) diff --git a/src/applications/project/application/PhabricatorProjectApplication.php b/src/applications/project/application/PhabricatorProjectApplication.php index 9c8a5e48d3..568d25d944 100644 --- a/src/applications/project/application/PhabricatorProjectApplication.php +++ b/src/applications/project/application/PhabricatorProjectApplication.php @@ -83,7 +83,7 @@ final class PhabricatorProjectApplication extends PhabricatorApplication { 'history/(?P[1-9]\d*)/' => 'PhabricatorProjectHistoryController', '(?Pwatch|unwatch)/(?P[1-9]\d*)/' => 'PhabricatorProjectWatchController', - + 'wiki/' => 'PhabricatorProjectWikiExplainController', ), '/tag/' => array( '(?P[^/]+)/' => 'PhabricatorProjectProfileController', @@ -92,12 +92,42 @@ final class PhabricatorProjectApplication extends PhabricatorApplication { ); } + public function getQuickCreateItems(PhabricatorUser $viewer) { + $can_create = PhabricatorPolicyFilter::hasCapability( + $viewer, + $this, + ProjectCreateProjectsCapability::CAPABILITY); + + $items = array(); + if ($can_create) { + $item = id(new PHUIListItemView()) + ->setName(pht('Project')) + ->setIcon('fa-briefcase') + ->setHref($this->getBaseURI().'create/'); + $items[] = $item; + } + + return $items; + } + protected function getCustomCapabilities() { return array( ProjectCreateProjectsCapability::CAPABILITY => array(), ProjectCanLockProjectsCapability::CAPABILITY => array( 'default' => PhabricatorPolicies::POLICY_ADMIN, ), + ProjectDefaultViewCapability::CAPABILITY => array( + 'caption' => pht( + 'Default view policy for newly created projects.'), + ), + ProjectDefaultEditCapability::CAPABILITY => array( + 'caption' => pht( + 'Default edit policy for newly created projects.'), + ), + ProjectDefaultJoinCapability::CAPABILITY => array( + 'caption' => pht( + 'Default join policy for newly created projects.'), + ), ); } diff --git a/src/applications/project/capability/ProjectDefaultEditCapability.php b/src/applications/project/capability/ProjectDefaultEditCapability.php new file mode 100644 index 0000000000..ef631b1798 --- /dev/null +++ b/src/applications/project/capability/ProjectDefaultEditCapability.php @@ -0,0 +1,12 @@ +getSlugs(); $project_slugs = array_values(mpull($project_slugs, 'getSlug')); + $project_icon = PhabricatorProjectIcon::getAPIName($project->getIcon()); + $result[$project->getPHID()] = array( - 'id' => $project->getID(), - 'phid' => $project->getPHID(), - 'name' => $project->getName(), - 'members' => $member_phids, - 'slugs' => $project_slugs, - 'dateCreated' => $project->getDateCreated(), - 'dateModified' => $project->getDateModified(), + 'id' => $project->getID(), + 'phid' => $project->getPHID(), + 'name' => $project->getName(), + 'profileImagePHID' => $project->getProfileImagePHID(), + 'icon' => $project_icon, + 'color' => $project->getColor(), + 'members' => $member_phids, + 'slugs' => $project_slugs, + 'dateCreated' => $project->getDateCreated(), + 'dateModified' => $project->getDateModified(), ); } diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php index 9feb64ebbc..49c18af813 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileController.php @@ -261,6 +261,17 @@ final class PhabricatorProjectProfileController } } + $have_phriction = PhabricatorApplication::isClassInstalledForViewer( + 'PhabricatorPhrictionApplication', + $viewer); + if ($have_phriction) { + $view->addAction( + id(new PhabricatorActionView()) + ->setIcon('fa-book grey') + ->setName(pht('View Wiki')) + ->setWorkflow(true) + ->setHref('/project/wiki/')); + } return $view; } diff --git a/src/applications/project/controller/PhabricatorProjectWikiExplainController.php b/src/applications/project/controller/PhabricatorProjectWikiExplainController.php new file mode 100644 index 0000000000..83ab9ba8c5 --- /dev/null +++ b/src/applications/project/controller/PhabricatorProjectWikiExplainController.php @@ -0,0 +1,31 @@ +newDialog() + ->setTitle(pht('Wikis Have Changed')) + ->appendParagraph( + pht( + 'Wiki pages in Phriction have been upgraded to have more powerful '. + 'support for policies and access control. Each page can now have '. + 'its own policies.')) + ->appendParagraph( + pht( + 'This change obsoletes dedicated project wiki pages and '. + 'resolves a number of issues they had: you can now have '. + 'multiple wiki pages for a project, put them anywhere, give '. + 'them custom access controls, and rename them (or the project) '. + 'more easily and with fewer issues.')) + ->appendParagraph( + pht( + 'If you want to point users of this project to specific wiki '. + 'pages with relevant documentation or information, edit the project '. + 'description and add links. You can use the %s syntax to link to a '. + 'wiki page.', + phutil_tag('tt', array(), '[[ example/page/ ]]'))) + ->addCancelButton('/', pht('Okay')); + } + +} diff --git a/src/applications/project/icon/PhabricatorProjectIcon.php b/src/applications/project/icon/PhabricatorProjectIcon.php index 7ed9d1c535..ae6efa9b6e 100644 --- a/src/applications/project/icon/PhabricatorProjectIcon.php +++ b/src/applications/project/icon/PhabricatorProjectIcon.php @@ -39,6 +39,10 @@ final class PhabricatorProjectIcon extends Phobject { return $map[$key]; } + public static function getAPIName($key) { + return substr($key, 3); + } + public static function renderIconForChooser($icon) { $project_icons = PhabricatorProjectIcon::getIconMap(); diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php index e5698d744e..9f85a8f42f 100644 --- a/src/applications/project/storage/PhabricatorProject.php +++ b/src/applications/project/storage/PhabricatorProject.php @@ -36,14 +36,26 @@ final class PhabricatorProject extends PhabricatorProjectDAO const TABLE_DATASOURCE_TOKEN = 'project_datasourcetoken'; public static function initializeNewProject(PhabricatorUser $actor) { + $app = id(new PhabricatorApplicationQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withClasses(array('PhabricatorProjectApplication')) + ->executeOne(); + + $view_policy = $app->getPolicy( + ProjectDefaultViewCapability::CAPABILITY); + $edit_policy = $app->getPolicy( + ProjectDefaultEditCapability::CAPABILITY); + $join_policy = $app->getPolicy( + ProjectDefaultJoinCapability::CAPABILITY); + return id(new PhabricatorProject()) ->setName('') ->setAuthorPHID($actor->getPHID()) ->setIcon(self::DEFAULT_ICON) ->setColor(self::DEFAULT_COLOR) - ->setViewPolicy(PhabricatorPolicies::POLICY_USER) - ->setEditPolicy(PhabricatorPolicies::POLICY_USER) - ->setJoinPolicy(PhabricatorPolicies::POLICY_USER) + ->setViewPolicy($view_policy) + ->setEditPolicy($edit_policy) + ->setJoinPolicy($join_policy) ->setIsMembershipLocked(0) ->attachMemberPHIDs(array()) ->attachSlugs(array()); diff --git a/src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php b/src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php index 15dbb0f757..f404f9c8a4 100644 --- a/src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php +++ b/src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php @@ -135,99 +135,6 @@ final class PhabricatorRepositoryDiscoveryEngine } - /** - * Verify that the "origin" remote exists, and points at the correct URI. - * - * This catches or corrects some types of misconfiguration, and also repairs - * an issue where Git 1.7.1 does not create an "origin" for `--bare` clones. - * See T4041. - * - * @param PhabricatorRepository Repository to verify. - * @return void - */ - private function verifyGitOrigin(PhabricatorRepository $repository) { - list($remotes) = $repository->execxLocalCommand( - 'remote show -n origin'); - - $matches = null; - if (!preg_match('/^\s*Fetch URL:\s*(.*?)\s*$/m', $remotes, $matches)) { - throw new Exception( - "Expected 'Fetch URL' in 'git remote show -n origin'."); - } - - $remote_uri = $matches[1]; - $expect_remote = $repository->getRemoteURI(); - - if ($remote_uri == 'origin') { - // If a remote does not exist, git pretends it does and prints out a - // made up remote where the URI is the same as the remote name. This is - // definitely not correct. - - // Possibly, we should use `git remote --verbose` instead, which does not - // suffer from this problem (but is a little more complicated to parse). - $valid = false; - $exists = false; - } else { - $normal_type_git = PhabricatorRepositoryURINormalizer::TYPE_GIT; - - $remote_normal = id(new PhabricatorRepositoryURINormalizer( - $normal_type_git, - $remote_uri))->getNormalizedPath(); - - $expect_normal = id(new PhabricatorRepositoryURINormalizer( - $normal_type_git, - $expect_remote))->getNormalizedPath(); - - $valid = ($remote_normal == $expect_normal); - $exists = true; - } - - if (!$valid) { - if (!$exists) { - // If there's no "origin" remote, just create it regardless of how - // strongly we own the working copy. There is almost no conceivable - // scenario in which this could do damage. - $this->log( - pht( - 'Remote "origin" does not exist. Creating "origin", with '. - 'URI "%s".', - $expect_remote)); - $repository->execxLocalCommand( - 'remote add origin %P', - $repository->getRemoteURIEnvelope()); - - // NOTE: This doesn't fetch the origin (it just creates it), so we won't - // know about origin branches until the next "pull" happens. That's fine - // for our purposes, but might impact things in the future. - } else { - if ($repository->canDestroyWorkingCopy()) { - // Bad remote, but we can try to repair it. - $this->log( - pht( - 'Remote "origin" exists, but is pointed at the wrong URI, "%s". '. - 'Resetting origin URI to "%s.', - $remote_uri, - $expect_remote)); - $repository->execxLocalCommand( - 'remote set-url origin %P', - $repository->getRemoteURIEnvelope()); - } else { - // Bad remote and we aren't comfortable repairing it. - $message = pht( - 'Working copy at "%s" has a mismatched origin URI, "%s". '. - 'The expected origin URI is "%s". Fix your configuration, or '. - 'set the remote URI correctly. To avoid breaking anything, '. - 'Phabricator will not automatically fix this.', - $repository->getLocalPath(), - $remote_uri, - $expect_remote); - throw new Exception($message); - } - } - } - } - - /* -( Discovering Subversion Repositories )-------------------------------- */ diff --git a/src/applications/repository/engine/PhabricatorRepositoryEngine.php b/src/applications/repository/engine/PhabricatorRepositoryEngine.php index 307738ef98..300fde13e0 100644 --- a/src/applications/repository/engine/PhabricatorRepositoryEngine.php +++ b/src/applications/repository/engine/PhabricatorRepositoryEngine.php @@ -51,6 +51,100 @@ abstract class PhabricatorRepositoryEngine { return PhabricatorUser::getOmnipotentUser(); } + /** + * Verify that the "origin" remote exists, and points at the correct URI. + * + * This catches or corrects some types of misconfiguration, and also repairs + * an issue where Git 1.7.1 does not create an "origin" for `--bare` clones. + * See T4041. + * + * @param PhabricatorRepository Repository to verify. + * @return void + */ + protected function verifyGitOrigin(PhabricatorRepository $repository) { + list($remotes) = $repository->execxLocalCommand( + 'remote show -n origin'); + + $matches = null; + if (!preg_match('/^\s*Fetch URL:\s*(.*?)\s*$/m', $remotes, $matches)) { + throw new Exception( + "Expected 'Fetch URL' in 'git remote show -n origin'."); + } + + $remote_uri = $matches[1]; + $expect_remote = $repository->getRemoteURI(); + + if ($remote_uri == 'origin') { + // If a remote does not exist, git pretends it does and prints out a + // made up remote where the URI is the same as the remote name. This is + // definitely not correct. + + // Possibly, we should use `git remote --verbose` instead, which does not + // suffer from this problem (but is a little more complicated to parse). + $valid = false; + $exists = false; + } else { + $normal_type_git = PhabricatorRepositoryURINormalizer::TYPE_GIT; + + $remote_normal = id(new PhabricatorRepositoryURINormalizer( + $normal_type_git, + $remote_uri))->getNormalizedPath(); + + $expect_normal = id(new PhabricatorRepositoryURINormalizer( + $normal_type_git, + $expect_remote))->getNormalizedPath(); + + $valid = ($remote_normal == $expect_normal); + $exists = true; + } + + if (!$valid) { + if (!$exists) { + // If there's no "origin" remote, just create it regardless of how + // strongly we own the working copy. There is almost no conceivable + // scenario in which this could do damage. + $this->log( + pht( + 'Remote "origin" does not exist. Creating "origin", with '. + 'URI "%s".', + $expect_remote)); + $repository->execxLocalCommand( + 'remote add origin %P', + $repository->getRemoteURIEnvelope()); + + // NOTE: This doesn't fetch the origin (it just creates it), so we won't + // know about origin branches until the next "pull" happens. That's fine + // for our purposes, but might impact things in the future. + } else { + if ($repository->canDestroyWorkingCopy()) { + // Bad remote, but we can try to repair it. + $this->log( + pht( + 'Remote "origin" exists, but is pointed at the wrong URI, "%s". '. + 'Resetting origin URI to "%s.', + $remote_uri, + $expect_remote)); + $repository->execxLocalCommand( + 'remote set-url origin %P', + $repository->getRemoteURIEnvelope()); + } else { + // Bad remote and we aren't comfortable repairing it. + $message = pht( + 'Working copy at "%s" has a mismatched origin URI, "%s". '. + 'The expected origin URI is "%s". Fix your configuration, or '. + 'set the remote URI correctly. To avoid breaking anything, '. + 'Phabricator will not automatically fix this.', + $repository->getLocalPath(), + $remote_uri, + $expect_remote); + throw new Exception($message); + } + } + } + } + + + /** * @task internal diff --git a/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php b/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php index 21caf30ada..4165723c90 100644 --- a/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php +++ b/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php @@ -88,6 +88,7 @@ final class PhabricatorRepositoryPullEngine "Updating the working copy for repository '%s'.", $callsign)); if ($is_git) { + $this->verifyGitOrigin($repository); $this->executeGitUpdate(); } else if ($is_hg) { $this->executeMercurialUpdate(); diff --git a/src/applications/repository/search/PhabricatorRepositoryCommitSearchIndexer.php b/src/applications/repository/search/PhabricatorRepositoryCommitSearchIndexer.php index f7ecf57ba6..1c17e39af9 100644 --- a/src/applications/repository/search/PhabricatorRepositoryCommitSearchIndexer.php +++ b/src/applications/repository/search/PhabricatorRepositoryCommitSearchIndexer.php @@ -47,19 +47,6 @@ final class PhabricatorRepositoryCommitSearchIndexer $date_created); } - $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( - $commit->getPHID(), - PhabricatorEdgeConfig::TYPE_COMMIT_HAS_PROJECT); - if ($project_phids) { - foreach ($project_phids as $project_phid) { - $doc->addRelationship( - PhabricatorSearchRelationship::RELATIONSHIP_PROJECT, - $project_phid, - PhabricatorProjectProjectPHIDType::TYPECONST, - $date_created); - } - } - $doc->addRelationship( PhabricatorSearchRelationship::RELATIONSHIP_REPOSITORY, $repository->getPHID(), diff --git a/src/applications/repository/storage/PhabricatorRepositoryAuditRequest.php b/src/applications/repository/storage/PhabricatorRepositoryAuditRequest.php index 9752529253..56613ac494 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryAuditRequest.php +++ b/src/applications/repository/storage/PhabricatorRepositoryAuditRequest.php @@ -27,6 +27,10 @@ final class PhabricatorRepositoryAuditRequest 'auditorPHID' => array( 'columns' => array('auditorPHID', 'auditStatus'), ), + 'key_unique' => array( + 'columns' => array('commitPHID', 'auditorPHID'), + 'unique' => true, + ), ), ) + parent::getConfiguration(); } diff --git a/src/applications/repository/storage/PhabricatorRepositoryCommit.php b/src/applications/repository/storage/PhabricatorRepositoryCommit.php index 782bc018d3..2a3c4551bb 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryCommit.php +++ b/src/applications/repository/storage/PhabricatorRepositoryCommit.php @@ -5,6 +5,7 @@ final class PhabricatorRepositoryCommit implements PhabricatorPolicyInterface, PhabricatorFlaggableInterface, + PhabricatorProjectInterface, PhabricatorTokenReceiverInterface, PhabricatorSubscribableInterface, PhabricatorMentionableInterface, diff --git a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php index 7c56251dea..f97fefc4c6 100644 --- a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php +++ b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php @@ -265,7 +265,7 @@ abstract class PhabricatorRepositoryCommitMessageParserWorker $changes = array(); } - $diff = DifferentialDiff::newFromRawChanges($changes) + $diff = DifferentialDiff::newFromRawChanges($viewer, $changes) ->setRepositoryPHID($this->repository->getPHID()) ->setAuthorPHID($actor_phid) ->setCreationMethod('commit') diff --git a/src/applications/search/index/PhabricatorSearchDocumentIndexer.php b/src/applications/search/index/PhabricatorSearchDocumentIndexer.php index fbecdc8213..83b450cd0e 100644 --- a/src/applications/search/index/PhabricatorSearchDocumentIndexer.php +++ b/src/applications/search/index/PhabricatorSearchDocumentIndexer.php @@ -47,6 +47,11 @@ abstract class PhabricatorSearchDocumentIndexer { $this->indexSubscribers($document); } + // Automatically build project relationships + if ($object instanceof PhabricatorProjectInterface) { + $this->indexProjects($document, $object); + } + $engine = PhabricatorSearchEngineSelector::newSelector()->newEngine(); try { $engine->reindexAbstractDocument($document); @@ -93,6 +98,24 @@ abstract class PhabricatorSearchDocumentIndexer { } } + protected function indexProjects( + PhabricatorSearchAbstractDocument $doc, + PhabricatorProjectInterface $object) { + + $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( + $object->getPHID(), + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); + if ($project_phids) { + foreach ($project_phids as $project_phid) { + $doc->addRelationship( + PhabricatorSearchRelationship::RELATIONSHIP_PROJECT, + $project_phid, + PhabricatorProjectProjectPHIDType::TYPECONST, + $doc->getDocumentModified()); // Bogus timestamp. + } + } + } + protected function indexTransactions( PhabricatorSearchAbstractDocument $doc, PhabricatorApplicationTransactionQuery $query, diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php b/src/applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php index de300477ed..1d59071d87 100644 --- a/src/applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php @@ -24,138 +24,6 @@ final class PhabricatorSettingsPanelSSHKeys } public function processRequest(AphrontRequest $request) { - $viewer = $request->getUser(); - $user = $this->getUser(); - - $generate = $request->getStr('generate'); - if ($generate) { - return $this->processGenerate($request); - } - - $edit = $request->getStr('edit'); - $delete = $request->getStr('delete'); - if (!$edit && !$delete) { - return $this->renderKeyListView($request); - } - - $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( - $viewer, - $request, - $this->getPanelURI()); - - $id = nonempty($edit, $delete); - if ($id && (int)$id) { - $key = id(new PhabricatorAuthSSHKeyQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$key) { - return new Aphront404Response(); - } - } else { - $key = id(new PhabricatorAuthSSHKey()) - ->setObjectPHID($user->getPHID()); - } - - if ($delete) { - return $this->processDelete($request, $key); - } - - $e_name = true; - $e_key = true; - $errors = array(); - $entire_key = $key->getEntireKey(); - if ($request->isFormPost()) { - $key->setName($request->getStr('name')); - $entire_key = $request->getStr('key'); - - if (!strlen($entire_key)) { - $errors[] = pht('You must provide an SSH Public Key.'); - $e_key = pht('Required'); - } else { - - try { - $public_key = PhabricatorAuthSSHPublicKey::newFromRawKey($entire_key); - - $type = $public_key->getType(); - $body = $public_key->getBody(); - $comment = $public_key->getComment(); - - $key->setKeyType($type); - $key->setKeyBody($body); - $key->setKeyComment($comment); - - $e_key = null; - } catch (Exception $ex) { - $e_key = pht('Invalid'); - $errors[] = $ex->getMessage(); - } - } - - if (!strlen($key->getName())) { - $errors[] = pht('You must name this public key.'); - $e_name = pht('Required'); - } else { - $e_name = null; - } - - if (!$errors) { - try { - $key->save(); - return id(new AphrontRedirectResponse()) - ->setURI($this->getPanelURI()); - } catch (AphrontDuplicateKeyQueryException $ex) { - $e_key = pht('Duplicate'); - $errors[] = pht('This public key is already associated with a user '. - 'account.'); - } - } - } - - $is_new = !$key->getID(); - - if ($is_new) { - $header = pht('Add New SSH Public Key'); - $save = pht('Add Key'); - } else { - $header = pht('Edit SSH Public Key'); - $save = pht('Save Changes'); - } - - $form = id(new AphrontFormView()) - ->setUser($viewer) - ->addHiddenInput('edit', $is_new ? 'true' : $key->getID()) - ->appendChild( - id(new AphrontFormTextControl()) - ->setLabel(pht('Name')) - ->setName('name') - ->setValue($key->getName()) - ->setError($e_name)) - ->appendChild( - id(new AphrontFormTextAreaControl()) - ->setLabel(pht('Public Key')) - ->setName('key') - ->setValue($entire_key) - ->setError($e_key)) - ->appendChild( - id(new AphrontFormSubmitControl()) - ->addCancelButton($this->getPanelURI()) - ->setValue($save)); - - $form_box = id(new PHUIObjectBoxView()) - ->setHeaderText($header) - ->setFormErrors($errors) - ->setForm($form); - - return $form_box; - } - - private function renderKeyListView(AphrontRequest $request) { $user = $this->getUser(); $viewer = $request->getUser(); @@ -164,50 +32,11 @@ final class PhabricatorSettingsPanelSSHKeys ->withObjectPHIDs(array($user->getPHID())) ->execute(); - $rows = array(); - foreach ($keys as $key) { - $rows[] = array( - phutil_tag( - 'a', - array( - 'href' => $this->getPanelURI('?edit='.$key->getID()), - ), - $key->getName()), - $key->getKeyComment(), - $key->getKeyType(), - phabricator_date($key->getDateCreated(), $viewer), - phabricator_time($key->getDateCreated(), $viewer), - javelin_tag( - 'a', - array( - 'href' => $this->getPanelURI('?delete='.$key->getID()), - 'class' => 'small grey button', - 'sigil' => 'workflow', - ), - pht('Delete')), - ); - } - - $table = new AphrontTableView($rows); - $table->setNoDataString(pht("You haven't added any SSH Public Keys.")); - $table->setHeaders( - array( - pht('Name'), - pht('Comment'), - pht('Type'), - pht('Created'), - pht('Time'), - '', - )); - $table->setColumnClasses( - array( - 'wide pri', - '', - '', - '', - 'right', - 'action', - )); + $table = id(new PhabricatorAuthSSHKeyTableView()) + ->setUser($viewer) + ->setKeys($keys) + ->setCanEdit(true) + ->setNoDataString("You haven't added any SSH Public Keys."); $panel = new PHUIObjectBoxView(); $header = new PHUIHeaderView(); @@ -216,7 +45,8 @@ final class PhabricatorSettingsPanelSSHKeys ->setIconFont('fa-upload'); $upload_button = id(new PHUIButtonView()) ->setText(pht('Upload Public Key')) - ->setHref($this->getPanelURI('?edit=true')) + ->setHref('/auth/sshkey/upload/?objectPHID='.$user->getPHID()) + ->setWorkflow(true) ->setTag('a') ->setIcon($upload_icon); @@ -231,7 +61,7 @@ final class PhabricatorSettingsPanelSSHKeys ->setIconFont('fa-lock'); $generate_button = id(new PHUIButtonView()) ->setText(pht('Generate Keypair')) - ->setHref($this->getPanelURI('?generate=true')) + ->setHref('/auth/sshkey/generate/?objectPHID='.$user->getPHID()) ->setTag('a') ->setWorkflow(true) ->setDisabled(!$can_generate) @@ -247,143 +77,4 @@ final class PhabricatorSettingsPanelSSHKeys return $panel; } - private function processDelete( - AphrontRequest $request, - PhabricatorAuthSSHKey $key) { - - $viewer = $request->getUser(); - $user = $this->getUser(); - - $name = phutil_tag('strong', array(), $key->getName()); - - if ($request->isDialogFormPost()) { - $key->delete(); - return id(new AphrontReloadResponse()) - ->setURI($this->getPanelURI()); - } - - $dialog = id(new AphrontDialogView()) - ->setUser($viewer) - ->addHiddenInput('delete', $key->getID()) - ->setTitle(pht('Really delete SSH Public Key?')) - ->appendChild(phutil_tag('p', array(), pht( - 'The key "%s" will be permanently deleted, and you will not longer be '. - 'able to use the corresponding private key to authenticate.', - $name))) - ->addSubmitButton(pht('Delete Public Key')) - ->addCancelButton($this->getPanelURI()); - - return id(new AphrontDialogResponse()) - ->setDialog($dialog); - } - - private function processGenerate(AphrontRequest $request) { - $user = $this->getUser(); - $viewer = $request->getUser(); - - $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( - $viewer, - $request, - $this->getPanelURI()); - - - $is_self = ($user->getPHID() == $viewer->getPHID()); - - if ($request->isFormPost()) { - $keys = PhabricatorSSHKeyGenerator::generateKeypair(); - list($public_key, $private_key) = $keys; - - $file = PhabricatorFile::buildFromFileDataOrHash( - $private_key, - array( - 'name' => 'id_rsa_phabricator.key', - 'ttl' => time() + (60 * 10), - 'viewPolicy' => $viewer->getPHID(), - )); - - $public_key = PhabricatorAuthSSHPublicKey::newFromRawKey($public_key); - - $type = $public_key->getType(); - $body = $public_key->getBody(); - - $key = id(new PhabricatorAuthSSHKey()) - ->setObjectPHID($user->getPHID()) - ->setName('id_rsa_phabricator') - ->setKeyType($type) - ->setKeyBody($body) - ->setKeyComment(pht('Generated')) - ->save(); - - // NOTE: We're disabling workflow on submit so the download works. We're - // disabling workflow on cancel so the page reloads, showing the new - // key. - - if ($is_self) { - $what_happened = pht( - 'The public key has been associated with your Phabricator '. - 'account. Use the button below to download the private key.'); - } else { - $what_happened = pht( - 'The public key has been associated with the %s account. '. - 'Use the button below to download the private key.', - phutil_tag('strong', array(), $user->getUsername())); - } - - $dialog = id(new AphrontDialogView()) - ->setTitle(pht('Download Private Key')) - ->setUser($viewer) - ->setDisableWorkflowOnCancel(true) - ->setDisableWorkflowOnSubmit(true) - ->setSubmitURI($file->getDownloadURI()) - ->appendParagraph( - pht( - 'Successfully generated a new keypair.')) - ->appendParagraph($what_happened) - ->appendParagraph( - pht( - 'After you download the private key, it will be destroyed. '. - 'You will not be able to retrieve it if you lose your copy.')) - ->addSubmitButton(pht('Download Private Key')) - ->addCancelButton($this->getPanelURI(), pht('Done')); - - return id(new AphrontDialogResponse()) - ->setDialog($dialog); - } - - $dialog = id(new AphrontDialogView()) - ->setUser($viewer) - ->addCancelButton($this->getPanelURI()); - - try { - PhabricatorSSHKeyGenerator::assertCanGenerateKeypair(); - - if ($is_self) { - $explain = pht( - 'This will generate an SSH keypair, associate the public key '. - 'with your account, and let you download the private key.'); - } else { - $explain = pht( - 'This will generate an SSH keypair, associate the public key with '. - 'the %s account, and let you download the private key.', - phutil_tag('strong', array(), $user->getUsername())); - } - - $dialog - ->addHiddenInput('generate', true) - ->setTitle(pht('Generate New Keypair')) - ->appendParagraph($explain) - ->appendParagraph( - pht( - 'Phabricator will not retain a copy of the private key.')) - ->addSubmitButton(pht('Generate Keypair')); - } catch (Exception $ex) { - $dialog - ->setTitle(pht('Unable to Generate Keys')) - ->appendParagraph($ex->getMessage()); - } - - return id(new AphrontDialogResponse()) - ->setDialog($dialog); - } - } diff --git a/src/applications/slowvote/controller/PhabricatorSlowvoteEditController.php b/src/applications/slowvote/controller/PhabricatorSlowvoteEditController.php index 57e18d760f..d84b255086 100644 --- a/src/applications/slowvote/controller/PhabricatorSlowvoteEditController.php +++ b/src/applications/slowvote/controller/PhabricatorSlowvoteEditController.php @@ -163,6 +163,7 @@ final class PhabricatorSlowvoteEditController ->setError($e_question)) ->appendChild( id(new PhabricatorRemarkupControl()) + ->setUser($user) ->setLabel(pht('Description')) ->setName('description') ->setValue($v_description)) diff --git a/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php b/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php index 0b4d0b615e..1cb38b1768 100644 --- a/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php +++ b/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php @@ -73,6 +73,7 @@ final class PhabricatorApplicationTransactionCommentEditController ->setFullWidth(true) ->appendChild( id(new PhabricatorRemarkupControl()) + ->setUser($user) ->setName('text') ->setValue($xaction->getComment()->getContent()))); diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index e6f6a8f2ff..3213cddfa0 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -1902,6 +1902,8 @@ abstract class PhabricatorApplicationTransactionEditor $body->addReplySection($reply_section); } + $body->addEmailPreferenceSection(); + $template ->setFrom($this->getActingAsPHID()) ->setSubjectPrefix($this->getMailSubjectPrefix()) @@ -2016,7 +2018,6 @@ abstract class PhabricatorApplicationTransactionEditor throw new Exception('Capability not supported.'); } - /** * @task mail */ @@ -2162,10 +2163,11 @@ abstract class PhabricatorApplicationTransactionEditor } $body = new PhabricatorMetaMTAMailBody(); + $body->setViewer($this->requireActor()); $body->addRawSection(implode("\n", $headers)); foreach ($comments as $comment) { - $body->addRawSection($comment); + $body->addRemarkupSection($comment); } if ($object instanceof PhabricatorCustomFieldInterface) { diff --git a/src/applications/uiexample/examples/PHUITimelineExample.php b/src/applications/uiexample/examples/PHUITimelineExample.php index ff7ac1a938..0ed81b01a3 100644 --- a/src/applications/uiexample/examples/PHUITimelineExample.php +++ b/src/applications/uiexample/examples/PHUITimelineExample.php @@ -79,21 +79,22 @@ final class PHUITimelineExample extends PhabricatorUIExample { $events[] = id(new PHUITimelineEventView()) ->setUserHandle($handle) - ->setIcon('fa-random') - ->setTitle('Minor Not-Red Event') - ->setColor(PhabricatorTransactions::COLOR_BLACK); + ->setIcon('fa-check') + ->setTitle('Historically Important Action') + ->setColor(PhabricatorTransactions::COLOR_BLACK) + ->setReallyMajorEvent(true); $events[] = id(new PHUITimelineEventView()) ->setUserHandle($handle) - ->setIcon('tag') - ->setTitle('Major Green Event') + ->setIcon('fa-circle-o') + ->setTitle('Major Green Disagreement Action') ->appendChild('This event is green!') ->setColor(PhabricatorTransactions::COLOR_GREEN); $events[] = id(new PHUITimelineEventView()) ->setUserHandle($handle) - ->setIcon('tag') + ->setIcon('fa-tag') ->setTitle(str_repeat('Long Text Title ', 64)) ->appendChild(str_repeat('Long Text Body ', 64)) ->setColor(PhabricatorTransactions::COLOR_ORANGE); @@ -120,13 +121,13 @@ final class PHUITimelineExample extends PhabricatorUIExample { $events[] = id(new PHUITimelineEventView()) ->setUserHandle($handle) ->setTitle('Colorless') - ->setIcon('lock'); + ->setIcon('fa-lock'); foreach ($colors as $color) { $events[] = id(new PHUITimelineEventView()) ->setUserHandle($handle) ->setTitle("Color '{$color}'") - ->setIcon('lock') + ->setIcon('fa-paw') ->setColor($color); } @@ -141,21 +142,21 @@ final class PHUITimelineExample extends PhabricatorUIExample { ->setUserHandle($handle) ->setTitle(pht('%s bought an apple.', $vhandle)) ->setColor('green') - ->setIcon('check')); + ->setIcon('fa-apple')); $group_event->addEventToGroup( id(new PHUITimelineEventView()) ->setUserHandle($handle) ->setTitle(pht('%s bought a banana.', $vhandle)) ->setColor('yellow') - ->setIcon('check')); + ->setIcon('fa-check')); $group_event->addEventToGroup( id(new PHUITimelineEventView()) ->setUserHandle($handle) ->setTitle(pht('%s bought a cherry.', $vhandle)) ->setColor('red') - ->setIcon('check')); + ->setIcon('fa-check')); $group_event->addEventToGroup( id(new PHUITimelineEventView()) @@ -166,7 +167,7 @@ final class PHUITimelineExample extends PhabricatorUIExample { id(new PHUITimelineEventView()) ->setUserHandle($handle) ->setTitle(pht('%s returned home.', $vhandle)) - ->setIcon('home') + ->setIcon('fa-home') ->setColor('blue')); $group_event->addEventToGroup( diff --git a/src/applications/uiexample/examples/PhabricatorUITooltipExample.php b/src/applications/uiexample/examples/PhabricatorUITooltipExample.php index 178b16beb9..ae45873890 100644 --- a/src/applications/uiexample/examples/PhabricatorUITooltipExample.php +++ b/src/applications/uiexample/examples/PhabricatorUITooltipExample.php @@ -16,18 +16,14 @@ final class PhabricatorUITooltipExample extends PhabricatorUIExample { require_celerity_resource('aphront-tooltip-css'); $style = 'width: 200px; '. - 'height: 200px '. 'text-align: center; '. 'margin: 20px; '. 'background: #dfdfdf; '. - 'padding: 30px 10px; '. + 'padding: 20px 10px; '. 'border: 1px solid black; '; $lorem = <<setUser($this->getViewer()) ->setLabel($this->getFieldName()) ->setName($this->getFieldKey()) ->setCaption($this->getCaption()) diff --git a/src/infrastructure/daemon/workers/PhabricatorWorker.php b/src/infrastructure/daemon/workers/PhabricatorWorker.php index 699afdfc50..6d3fcb52e9 100644 --- a/src/infrastructure/daemon/workers/PhabricatorWorker.php +++ b/src/infrastructure/daemon/workers/PhabricatorWorker.php @@ -9,10 +9,14 @@ abstract class PhabricatorWorker { private static $runAllTasksInProcess = false; private $queuedTasks = array(); - const PRIORITY_ALERTS = 4000; - const PRIORITY_DEFAULT = 3000; - const PRIORITY_BULK = 2000; - const PRIORITY_IMPORT = 1000; + // NOTE: Lower priority numbers execute first. The priority numbers have to + // have the same ordering that IDs do (lowest first) so MySQL can use a + // multipart key across both of them efficiently. + + const PRIORITY_ALERTS = 1000; + const PRIORITY_DEFAULT = 2000; + const PRIORITY_BULK = 3000; + const PRIORITY_IMPORT = 4000; /* -( Configuring Retries and Failures )----------------------------------- */ diff --git a/src/infrastructure/daemon/workers/__tests__/PhabricatorWorkerTestCase.php b/src/infrastructure/daemon/workers/__tests__/PhabricatorWorkerTestCase.php index 659a83821e..c5c9b0959f 100644 --- a/src/infrastructure/daemon/workers/__tests__/PhabricatorWorkerTestCase.php +++ b/src/infrastructure/daemon/workers/__tests__/PhabricatorWorkerTestCase.php @@ -160,14 +160,14 @@ final class PhabricatorWorkerTestCase extends PhabricatorTestCase { $this->expectNextLease($task1); } - public function testLeasedIsHighestPriority() { - $task1 = $this->scheduleTask(array(), 1); - $task2 = $this->scheduleTask(array(), 1); - $task3 = $this->scheduleTask(array(), 2); + public function testLeasedIsLowestPriority() { + $task1 = $this->scheduleTask(array(), 2); + $task2 = $this->scheduleTask(array(), 2); + $task3 = $this->scheduleTask(array(), 1); $this->expectNextLease( $task3, - 'Tasks with a higher priority should be scheduled first.'); + 'Tasks with a lower priority should be scheduled first.'); $this->expectNextLease( $task1, 'Tasks with the same priority should be FIFO.'); diff --git a/src/infrastructure/daemon/workers/management/PhabricatorWorkerManagementFloodWorkflow.php b/src/infrastructure/daemon/workers/management/PhabricatorWorkerManagementFloodWorkflow.php new file mode 100644 index 0000000000..379ae2f5c4 --- /dev/null +++ b/src/infrastructure/daemon/workers/management/PhabricatorWorkerManagementFloodWorkflow.php @@ -0,0 +1,36 @@ +setName('flood') + ->setExamples('**flood**') + ->setSynopsis( + pht( + 'Flood the queue with test tasks. This command is intended for '. + 'use when developing and debugging Phabricator.')) + ->setArguments(array()); + } + + public function execute(PhutilArgumentParser $args) { + $console = PhutilConsole::getConsole(); + + $console->writeOut( + "%s\n", + pht('Adding many test tasks to worker queue. Use ^C to exit.')); + + $n = 0; + while (true) { + PhabricatorWorker::scheduleTask( + 'PhabricatorTestWorker', + array()); + + if (($n++ % 100) === 0) { + $console->writeOut('.'); + } + } + } + +} diff --git a/src/infrastructure/daemon/workers/management/PhabricatorWorkerManagementWorkflow.php b/src/infrastructure/daemon/workers/management/PhabricatorWorkerManagementWorkflow.php new file mode 100644 index 0000000000..58e3bf4adf --- /dev/null +++ b/src/infrastructure/daemon/workers/management/PhabricatorWorkerManagementWorkflow.php @@ -0,0 +1,4 @@ +generateRawDiffFromFileContent($old, $new); $changes = id(new ArcanistDiffParser())->parseDiff($diff); - $diff = DifferentialDiff::newFromRawChanges($changes); + $diff = DifferentialDiff::newFromRawChanges( + PhabricatorUser::getOmnipotentUser(), + $changes); return head($diff->getChangesets()); } diff --git a/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php b/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php index db725776ab..9738dec2a8 100644 --- a/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php +++ b/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php @@ -19,9 +19,6 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants { const TYPE_PROJ_MEMBER = 13; const TYPE_MEMBER_OF_PROJ = 14; - const TYPE_COMMIT_HAS_PROJECT = 15; - const TYPE_PROJECT_HAS_COMMIT = 16; - const TYPE_QUESTION_HAS_VOTING_USER = 17; const TYPE_VOTING_USER_HAS_QUESTION = 18; const TYPE_ANSWER_HAS_VOTING_USER = 19; @@ -100,6 +97,9 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants { array(9000), range(80000, 80005)); + $exclude[] = 15; // Was TYPE_COMMIT_HAS_PROJECT + $exclude[] = 16; // Was TYPE_PROJECT_HAS_COMMIT + $exclude[] = 27; // Was TYPE_ACCOUNT_HAS_MEMBER $exclude[] = 28; // Was TYPE_MEMBER_HAS_ACCOUNT @@ -145,9 +145,6 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants { self::TYPE_PROJ_MEMBER => self::TYPE_MEMBER_OF_PROJ, self::TYPE_MEMBER_OF_PROJ => self::TYPE_PROJ_MEMBER, - self::TYPE_COMMIT_HAS_PROJECT => self::TYPE_PROJECT_HAS_COMMIT, - self::TYPE_PROJECT_HAS_COMMIT => self::TYPE_COMMIT_HAS_PROJECT, - self::TYPE_QUESTION_HAS_VOTING_USER => self::TYPE_VOTING_USER_HAS_QUESTION, self::TYPE_VOTING_USER_HAS_QUESTION => @@ -239,7 +236,6 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants { public static function getEditStringForEdgeType($type) { switch ($type) { - case self::TYPE_PROJECT_HAS_COMMIT: case self::TYPE_DREV_HAS_COMMIT: return '%s edited commit(s), added %d: %s; removed %d: %s.'; case self::TYPE_TASK_DEPENDS_ON_TASK: @@ -261,7 +257,6 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants { case self::TYPE_PROJ_MEMBER: return '%s edited member(s), added %d: %s; removed %d: %s.'; case self::TYPE_MEMBER_OF_PROJ: - case self::TYPE_COMMIT_HAS_PROJECT: return '%s edited project(s), added %d: %s; removed %d: %s.'; case self::TYPE_QUESTION_HAS_VOTING_USER: case self::TYPE_ANSWER_HAS_VOTING_USER: @@ -307,7 +302,6 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants { public static function getAddStringForEdgeType($type) { switch ($type) { - case self::TYPE_PROJECT_HAS_COMMIT: case self::TYPE_DREV_HAS_COMMIT: return '%s added %d commit(s): %s.'; case self::TYPE_TASK_DEPENDS_ON_TASK: @@ -332,7 +326,6 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants { case self::TYPE_PROJ_MEMBER: return '%s added %d member(s): %s.'; case self::TYPE_MEMBER_OF_PROJ: - case self::TYPE_COMMIT_HAS_PROJECT: return '%s added %d project(s): %s.'; case self::TYPE_QUESTION_HAS_VOTING_USER: case self::TYPE_ANSWER_HAS_VOTING_USER: @@ -377,7 +370,6 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants { public static function getRemoveStringForEdgeType($type) { switch ($type) { - case self::TYPE_PROJECT_HAS_COMMIT: case self::TYPE_DREV_HAS_COMMIT: return '%s removed %d commit(s): %s.'; case self::TYPE_TASK_DEPENDS_ON_TASK: @@ -401,7 +393,6 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants { case self::TYPE_PROJ_MEMBER: return '%s removed %d member(s): %s.'; case self::TYPE_MEMBER_OF_PROJ: - case self::TYPE_COMMIT_HAS_PROJECT: return '%s removed %d project(s): %s.'; case self::TYPE_QUESTION_HAS_VOTING_USER: case self::TYPE_ANSWER_HAS_VOTING_USER: @@ -444,7 +435,6 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants { public static function getFeedStringForEdgeType($type) { switch ($type) { - case self::TYPE_PROJECT_HAS_COMMIT: case self::TYPE_DREV_HAS_COMMIT: return '%s updated commits of %s.'; case self::TYPE_TASK_DEPENDS_ON_TASK: @@ -466,7 +456,6 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants { case self::TYPE_PROJ_MEMBER: return '%s updated members of %s.'; case self::TYPE_MEMBER_OF_PROJ: - case self::TYPE_COMMIT_HAS_PROJECT: return '%s updated projects of %s.'; case self::TYPE_QUESTION_HAS_VOTING_USER: case self::TYPE_ANSWER_HAS_VOTING_USER: diff --git a/src/infrastructure/internationalization/translation/PhabricatorBaseEnglishTranslation.php b/src/infrastructure/internationalization/translation/PhabricatorBaseEnglishTranslation.php index 855cf62130..40735a6651 100644 --- a/src/infrastructure/internationalization/translation/PhabricatorBaseEnglishTranslation.php +++ b/src/infrastructure/internationalization/translation/PhabricatorBaseEnglishTranslation.php @@ -554,7 +554,7 @@ abstract class PhabricatorBaseEnglishTranslation ), '%s edited reviewer(s), added %d: %s; removed %d: %s.' => - '%s edited reviewers, added: %3$s; removed: %5$s', + '%s edited reviewers, added: %4$s; removed: %6$s', '%s added %d reviewer(s): %s.' => array( array( diff --git a/src/infrastructure/markup/rule/PhabricatorNavigationRemarkupRule.php b/src/infrastructure/markup/rule/PhabricatorNavigationRemarkupRule.php index d0bf3a69b1..2f32950f4e 100644 --- a/src/infrastructure/markup/rule/PhabricatorNavigationRemarkupRule.php +++ b/src/infrastructure/markup/rule/PhabricatorNavigationRemarkupRule.php @@ -80,20 +80,30 @@ final class PhabricatorNavigationRemarkupRule extends PhutilRemarkupRule { $out[] = $tag; } + if ($this->getEngine()->isHTMLMailMode()) { + $arrow_attr = array( + 'style' => 'color: #92969D;', + ); + $nav_attr = array(); + } else { + $arrow_attr = array( + 'class' => 'remarkup-nav-sequence-arrow', + ); + $nav_attr = array( + 'class' => 'remarkup-nav-sequence', + ); + } + $joiner = phutil_tag( 'span', - array( - 'class' => 'remarkup-nav-sequence-arrow', - ), + $arrow_attr, " \xE2\x86\x92 "); $out = phutil_implode_html($joiner, $out); $out = phutil_tag( 'span', - array( - 'class' => 'remarkup-nav-sequence', - ), + $nav_attr, $out); return $this->getEngine()->storeText($out); diff --git a/src/infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php b/src/infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php index 1e60bde7dc..3f8688897e 100644 --- a/src/infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php +++ b/src/infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php @@ -44,7 +44,11 @@ abstract class PhabricatorObjectRemarkupRule extends PhutilRemarkupRule { return $handle->getURI(); } - protected function renderObjectRef($object, $handle, $anchor, $id) { + protected function renderObjectRefForAnyMedia ( + $object, + $handle, + $anchor, + $id) { $href = $this->getObjectHref($object, $handle, $id); $text = $this->getObjectNamePrefix().$id; @@ -55,10 +59,25 @@ abstract class PhabricatorObjectRemarkupRule extends PhutilRemarkupRule { if ($this->getEngine()->isTextMode()) { return PhabricatorEnv::getProductionURI($href); + } else if ($this->getEngine()->isHTMLMailMode()) { + $href = PhabricatorEnv::getProductionURI($href); + return $this->renderObjectTagForMail($text, $href, $handle); } + return $this->renderObjectRef($object, $handle, $anchor, $id); + + } + + protected function renderObjectRef($object, $handle, $anchor, $id) { + $href = $this->getObjectHref($object, $handle, $id); + $text = $this->getObjectNamePrefix().$id; $status_closed = PhabricatorObjectHandleStatus::STATUS_CLOSED; + if ($anchor) { + $href = $href.'#'.$anchor; + $text = $text.'#'.$anchor; + } + $attr = array( 'phid' => $handle->getPHID(), 'closed' => ($handle->getStatus() == $status_closed), @@ -67,15 +86,24 @@ abstract class PhabricatorObjectRemarkupRule extends PhutilRemarkupRule { return $this->renderHovertag($text, $href, $attr); } + protected function renderObjectEmbedForAnyMedia($object, $handle, $options) { + $name = $handle->getFullName(); + $href = $handle->getURI(); + + if ($this->getEngine()->isTextMode()) { + return $name.' <'.PhabricatorEnv::getProductionURI($href).'>'; + } else if ($this->getEngine()->isHTMLMailMode()) { + $href = PhabricatorEnv::getProductionURI($href); + return $this->renderObjectTagForMail($name, $href, $handle); + } + + return $this->renderObjectEmbed($object, $handle, $options); + } + protected function renderObjectEmbed($object, $handle, $options) { $name = $handle->getFullName(); $href = $handle->getURI(); $status_closed = PhabricatorObjectHandleStatus::STATUS_CLOSED; - - if ($this->getEngine()->isTextMode()) { - return $name.' <'.PhabricatorEnv::getProductionURI($href).'>'; - } - $attr = array( 'phid' => $handle->getPHID(), 'closed' => ($handle->getStatus() == $status_closed), @@ -84,6 +112,31 @@ abstract class PhabricatorObjectRemarkupRule extends PhutilRemarkupRule { return $this->renderHovertag($name, $href, $attr); } + protected function renderObjectTagForMail( + $text, + $href, + $handle) { + + $status_closed = PhabricatorObjectHandleStatus::STATUS_CLOSED; + $strikethrough = $handle->getStatus() == $status_closed ? + 'text-decoration: line-through;' : + 'text-decoration: none;'; + + return phutil_tag( + 'a', + array( + 'href' => $href, + 'style' => 'background-color: #e7e7e7; + border-color: #e7e7e7; + border-radius: 3px; + padding: 0 4px; + font-weight: bold; + color: black;' + .$strikethrough, + ), + $text); + } + protected function renderHovertag($name, $href, array $attr = array()) { return id(new PHUITagView()) ->setName($name) @@ -282,7 +335,8 @@ abstract class PhabricatorObjectRemarkupRule extends PhutilRemarkupRule { $object = $objects[$spec['id']]; switch ($spec['type']) { case 'ref': - $view = $this->renderObjectRef( + + $view = $this->renderObjectRefForAnyMedia( $object, $handle, $spec['anchor'], @@ -290,7 +344,10 @@ abstract class PhabricatorObjectRemarkupRule extends PhutilRemarkupRule { break; case 'embed': $spec['options'] = $this->assertFlatText($spec['options']); - $view = $this->renderObjectEmbed($object, $handle, $spec['options']); + $view = $this->renderObjectEmbedForAnyMedia( + $object, + $handle, + $spec['options']); break; } $engine->overwriteStoredText($spec['token'], $view); diff --git a/src/infrastructure/markup/rule/PhabricatorYoutubeRemarkupRule.php b/src/infrastructure/markup/rule/PhabricatorYoutubeRemarkupRule.php index 6528783aed..43af8d3f01 100644 --- a/src/infrastructure/markup/rule/PhabricatorYoutubeRemarkupRule.php +++ b/src/infrastructure/markup/rule/PhabricatorYoutubeRemarkupRule.php @@ -20,8 +20,10 @@ final class PhabricatorYoutubeRemarkupRule extends PhutilRemarkupRule { public function markupYoutubeLink() { $v = idx($this->uri->getQueryParams(), 'v'); + $text_mode = $this->getEngine()->isTextMode(); + $mail_mode = $this->getEngine()->isHTMLMailMode(); - if ($this->getEngine()->isTextMode()) { + if ($text_mode || $mail_mode) { return $this->getEngine()->storeText('http://youtu.be/'.$v); } diff --git a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php index 71a1ece904..c07a139344 100644 --- a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php +++ b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php @@ -39,24 +39,7 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList { $root = dirname(phutil_get_library_root('phabricator')); $auto_root = $root.'/resources/sql/autopatches/'; - $auto_list = Filesystem::listDirectory($auto_root, $include_hidden = false); - sort($auto_list); - - foreach ($auto_list as $auto_patch) { - $matches = null; - if (!preg_match('/\.(sql|php)$/', $auto_patch, $matches)) { - throw new Exception( - pht( - 'Unknown patch "%s" in "%s", expected ".php" or ".sql" suffix.', - $auto_patch, - $auto_root)); - } - - $patches[$auto_patch] = array( - 'type' => $matches[1], - 'name' => $auto_root.$auto_patch, - ); - } + $patches += $this->buildPatchesFromDirectory($auto_root); return $patches; } diff --git a/src/infrastructure/storage/patch/PhabricatorSQLPatchList.php b/src/infrastructure/storage/patch/PhabricatorSQLPatchList.php index 65070e67e6..282cbbd822 100644 --- a/src/infrastructure/storage/patch/PhabricatorSQLPatchList.php +++ b/src/infrastructure/storage/patch/PhabricatorSQLPatchList.php @@ -5,6 +5,37 @@ abstract class PhabricatorSQLPatchList { abstract function getNamespace(); abstract function getPatches(); + /** + * Examine a directory for `.php` and `.sql` files and build patch + * specifications for them. + */ + protected function buildPatchesFromDirectory($directory) { + $patch_list = Filesystem::listDirectory( + $directory, + $include_hidden = false); + + sort($patch_list); + $patches = array(); + + foreach ($patch_list as $patch) { + $matches = null; + if (!preg_match('/\.(sql|php)$/', $patch, $matches)) { + throw new Exception( + pht( + 'Unknown patch "%s" in "%s", expected ".php" or ".sql" suffix.', + $patch, + $directory)); + } + + $patches[$patch] = array( + 'type' => $matches[1], + 'name' => rtrim($directory, '/').'/'.$patch, + ); + } + + return $patches; + } + final public static function buildAllPatches() { $patch_lists = id(new PhutilSymbolLoader()) ->setAncestorClass('PhabricatorSQLPatchList') diff --git a/src/infrastructure/util/PhabricatorSlug.php b/src/infrastructure/util/PhabricatorSlug.php index 17e961ddbd..4e6d761dc2 100644 --- a/src/infrastructure/util/PhabricatorSlug.php +++ b/src/infrastructure/util/PhabricatorSlug.php @@ -8,7 +8,17 @@ final class PhabricatorSlug { $slug = phutil_utf8_strtolower($slug); $slug = preg_replace("@[\\x00-\\x19#%&+=\\\\?<> ]+@", '_', $slug); $slug = preg_replace('@_+@', '_', $slug); - $slug = trim($slug, '_'); + + // Remove leading and trailing underscores from each component, if the + // component has not been reduced to a single underscore. For example, "a?" + // converts to "a", but "??" converts to "_". + $parts = explode('/', $slug); + foreach ($parts as $key => $part) { + if ($part != '_') { + $parts[$key] = trim($part, '_'); + } + } + $slug = implode('/', $parts); // Specifically rewrite these slugs. It's OK to have a slug like "a..b", // but not a slug which is only "..". diff --git a/src/infrastructure/util/__tests__/PhabricatorSlugTestCase.php b/src/infrastructure/util/__tests__/PhabricatorSlugTestCase.php index faefb51382..3c820a085e 100644 --- a/src/infrastructure/util/__tests__/PhabricatorSlugTestCase.php +++ b/src/infrastructure/util/__tests__/PhabricatorSlugTestCase.php @@ -7,7 +7,7 @@ final class PhabricatorSlugTestCase extends PhabricatorTestCase { '' => '/', '/' => '/', '//' => '/', - '&&&' => '/', + '&&&' => '_/', '/derp/' => 'derp/', 'derp' => 'derp/', 'derp//derp' => 'derp/derp/', @@ -27,6 +27,13 @@ final class PhabricatorSlugTestCase extends PhabricatorTestCase { '../a' => 'dotdot/a/', 'a/..' => 'a/dotdot/', 'a/../' => 'a/dotdot/', + 'a?' => 'a/', + '??' => '_/', + 'a/?' => 'a/_/', + '??/a/??' => '_/a/_/', + 'a/??/c' => 'a/_/c/', + 'a/?b/c' => 'a/b/c/', + 'a/b?/c' => 'a/b/c/', ); foreach ($slugs as $slug => $normal) { diff --git a/src/view/form/PHUIFormInsetView.php b/src/view/form/PHUIFormInsetView.php index 89a23feb2b..1d66df6d8a 100644 --- a/src/view/form/PHUIFormInsetView.php +++ b/src/view/form/PHUIFormInsetView.php @@ -69,6 +69,7 @@ final class PHUIFormInsetView extends AphrontView { 'style' => 'float: right;', ), $this->rightButton); + $right_button = phutil_tag_div('grouped', $right_button); } if ($this->description) { @@ -76,10 +77,6 @@ final class PHUIFormInsetView extends AphrontView { 'p', array(), $this->description); - - if ($right_button) { - $desc = hsprintf('%s
      ', $desc); - } } $div_attributes = $this->divAttributes; diff --git a/src/view/form/control/PhabricatorRemarkupControl.php b/src/view/form/control/PhabricatorRemarkupControl.php index 3d37c7d671..e1041c409e 100644 --- a/src/view/form/control/PhabricatorRemarkupControl.php +++ b/src/view/form/control/PhabricatorRemarkupControl.php @@ -22,6 +22,12 @@ final class PhabricatorRemarkupControl extends AphrontFormTextAreaControl { $this->setID($id); } + $viewer = $this->getUser(); + if (!$viewer) { + throw new Exception( + pht('Call setUser() before rendering a PhabricatorRemarkupControl!')); + } + // We need to have this if previews render images, since Ajax can not // currently ship JS or CSS. require_celerity_resource('lightbox-attachment-css'); @@ -82,7 +88,17 @@ final class PhabricatorRemarkupControl extends AphrontFormTextAreaControl { ), ); - if (!$this->disableMacro and function_exists('imagettftext')) { + $can_use_macros = + (!$this->disableMacro) && + (function_exists('imagettftext')); + + if ($can_use_macros) { + $can_use_macros = PhabricatorApplication::isClassInstalledForViewer( + 'PhabricatorMacroApplication', + $viewer); + } + + if ($can_use_macros) { $actions[] = array( 'spacer' => true, ); @@ -184,16 +200,13 @@ final class PhabricatorRemarkupControl extends AphrontFormTextAreaControl { $monospaced_textareas = null; $monospaced_textareas_class = null; - $user = $this->getUser(); - if ($user) { - $monospaced_textareas = $user - ->loadPreferences() - ->getPreference( - PhabricatorUserPreferences::PREFERENCE_MONOSPACED_TEXTAREAS); - if ($monospaced_textareas == 'enabled') { - $monospaced_textareas_class = 'PhabricatorMonospaced'; - } + $monospaced_textareas = $viewer + ->loadPreferences() + ->getPreference( + PhabricatorUserPreferences::PREFERENCE_MONOSPACED_TEXTAREAS); + if ($monospaced_textareas == 'enabled') { + $monospaced_textareas_class = 'PhabricatorMonospaced'; } $this->setCustomClass( diff --git a/src/view/page/PhabricatorStandardPageView.php b/src/view/page/PhabricatorStandardPageView.php index 9af3affb75..08edde8015 100644 --- a/src/view/page/PhabricatorStandardPageView.php +++ b/src/view/page/PhabricatorStandardPageView.php @@ -262,10 +262,11 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView { return hsprintf( '%s%s', parent::getHead(), phutil_safe_html($monospaced), diff --git a/src/view/phui/PHUITimelineEventView.php b/src/view/phui/PHUITimelineEventView.php index 9f308a0a4c..ff53875bac 100644 --- a/src/view/phui/PHUITimelineEventView.php +++ b/src/view/phui/PHUITimelineEventView.php @@ -23,6 +23,7 @@ final class PHUITimelineEventView extends AphrontView { private $tokenRemoved; private $quoteTargetID; private $quoteRef; + private $reallyMajorEvent; public function setQuoteRef($quote_ref) { $this->quoteRef = $quote_ref; @@ -148,6 +149,11 @@ final class PHUITimelineEventView extends AphrontView { return $this; } + public function setReallyMajorEvent($me) { + $this->reallyMajorEvent = $me; + return $this; + } + public function setToken($token, $removed = false) { $this->token = $token; $this->tokenRemoved = $removed; @@ -401,20 +407,33 @@ final class PHUITimelineEventView extends AphrontView { ); } - return javelin_tag( - 'div', - array( - 'class' => implode(' ', $outer_classes), - 'id' => $this->anchor ? 'anchor-'.$this->anchor : null, - 'sigil' => $sigil, - 'meta' => $meta, - ), - phutil_tag( + $major_event = null; + if ($this->reallyMajorEvent) { + $major_event = phutil_tag( 'div', array( - 'class' => implode(' ', $classes), + 'class' => 'phui-timeline-event-view '. + 'phui-timeline-spacer '. + 'phui-timeline-spacer-bold', + '',)); + } + + return array( + javelin_tag( + 'div', + array( + 'class' => implode(' ', $outer_classes), + 'id' => $this->anchor ? 'anchor-'.$this->anchor : null, + 'sigil' => $sigil, + 'meta' => $meta, ), - $content)); + phutil_tag( + 'div', + array( + 'class' => implode(' ', $classes), + ), + $content)), + $major_event,); } private function renderExtra(array $events) { diff --git a/webroot/rsrc/css/aphront/tooltip.css b/webroot/rsrc/css/aphront/tooltip.css index e9d5d961ed..e20d69f79b 100644 --- a/webroot/rsrc/css/aphront/tooltip.css +++ b/webroot/rsrc/css/aphront/tooltip.css @@ -6,17 +6,18 @@ position: absolute; } +.jx-tooltip-inner { + position: relative; + background: rgba(0,0,0, .9); + border-radius: 3px; +} + .jx-tooltip { - background: #000; color: #f9f9f9; - font-size: 12px; - padding: 4px 8px; + font-size: 13px; + padding: 6px 8px; overflow: hidden; white-space: pre-wrap; - border-radius: 3px; - opacity: 0.9; - - box-shadow: 0 0 2px rgba(255, 255, 255, 0.5); } .jx-tooltip:after { @@ -41,6 +42,10 @@ top: 50%; } +.jx-tooltip-align-E { + margin-right: 5px; +} + .jx-tooltip-align-W .jx-tooltip:after { margin-top: -5px; border-left-color: #000; @@ -49,7 +54,7 @@ } .jx-tooltip-align-N { - margin-top: -5px; + margin-bottom: 5px; } .jx-tooltip-align-N .jx-tooltip:after { @@ -59,6 +64,10 @@ left: 50%; } +.jx-tooltip-align-N { + margin-top: 5px; +} + .jx-tooltip-align-S .jx-tooltip:after { margin-left: -5px; border-bottom-color: #000; diff --git a/webroot/rsrc/css/application/herald/herald.css b/webroot/rsrc/css/application/herald/herald.css index 99078335a2..8feebd122e 100644 --- a/webroot/rsrc/css/application/herald/herald.css +++ b/webroot/rsrc/css/application/herald/herald.css @@ -17,10 +17,15 @@ .herald-action-table td, .herald-condition-table td { - padding: 2px 4px; + padding: 2px 8px 2px 0; vertical-align: middle; } +.herald-action-table td.remove-column, +.herald-condition-table td.remove-column { + padding: 2px 0 2px 4px; +} + .herald-condition-table td.value { width: 100%; } diff --git a/webroot/rsrc/css/application/policy/policy-edit.css b/webroot/rsrc/css/application/policy/policy-edit.css index 08446d96ae..ba02691b51 100644 --- a/webroot/rsrc/css/application/policy/policy-edit.css +++ b/webroot/rsrc/css/application/policy/policy-edit.css @@ -7,11 +7,15 @@ } .policy-rules-table td { - padding: 4px; + padding: 4px 8px 4px 0; width: 32px; vertical-align: middle; } +.policy-rules-table td.remove-column { + padding-right: 0; +} + .policy-rules-table td.action-cell { width: 120px; } diff --git a/webroot/rsrc/css/core/remarkup.css b/webroot/rsrc/css/core/remarkup.css index 98db1ed179..c790ea9e8c 100644 --- a/webroot/rsrc/css/core/remarkup.css +++ b/webroot/rsrc/css/core/remarkup.css @@ -159,6 +159,10 @@ background-color: #F8F9FC; } +.phabricator-remarkup blockquote blockquote { + background-color: rgba(175,175,175, .1); +} + .phabricator-remarkup blockquote em { font-style: normal; } diff --git a/webroot/rsrc/css/phui/phui-button.css b/webroot/rsrc/css/phui/phui-button.css index f7998dd3bb..014b867703 100644 --- a/webroot/rsrc/css/phui/phui-button.css +++ b/webroot/rsrc/css/phui/phui-button.css @@ -6,6 +6,16 @@ button, a.button { font: 13px/1.231 'Helvetica Neue', Helvetica, Arial, sans-serif; + -webkit-font-smoothing: antialiased; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +button::-moz-focus-inner { + padding: 0; + border: 0; } button, @@ -14,13 +24,12 @@ a.button:visited, input[type="submit"] { background-color: #3477ad; color: white; - text-shadow: 0 -1px rgba(0,0,0,0.75); border: 1px solid #19558D; cursor: pointer; font-weight: bold; font-size: 13px; display: inline-block; - padding: 3px 10px 4px; + padding: 3px 12px 4px; text-align: center; white-space: nowrap; border-radius: 3px; @@ -67,7 +76,6 @@ a.grey:visited { border-color: {$lightgreyborder}; color: {$darkgreytext}; border-bottom-color: {$greyborder}; - text-shadow: none; } button.simple, @@ -79,7 +87,6 @@ a.simple:visited { border: 1px solid transparent; color: {$bluetext}; border: 1px solid {$lightblueborder}; - text-shadow: 0 1px #fff; } a.disabled, @@ -120,7 +127,6 @@ button.simple:hover { border: 1px solid {$lightgreyborder}; background-image: none; border-bottom: 1px solid {$greyborder}; - text-shadow: none; box-shadow: none; } @@ -145,7 +151,7 @@ body button.disabled:active { button.small, a.small, a.small:visited { - padding: 2px 7px; + padding: 2px 8px; height: auto; font-size: 11px; line-height: 16px; @@ -161,7 +167,6 @@ button.link { font-size: inherit; border-bottom: none; text-decoration: none; - text-shadow: none; color: #19558D; -webkit-box-shadow: none; -moz-box-shadow: none; @@ -200,13 +205,6 @@ a.policy-control .caret { float: right; } -a.policy-control span.phui-icon-view { - /* NOTE: Nudge these icons a little bit. Should this be for all - dropdown buttons? */ - top: 4px; - left: 7px; -} - a.toggle { display: inline-block; padding: 4px 8px; @@ -293,11 +291,11 @@ a.toggle-fixed { display: inline-block; position: absolute; top: 5px; - left: 8px; + left: 10px; } .phui-button-bar .button .phui-icon-view { - left: 11px; + left: 12px; } .button.has-icon .phui-button-text { @@ -313,7 +311,7 @@ a.toggle-fixed { } .button.big.has-icon .phui-button-text { - margin-left: 36px; + margin-left: 40px; font-size: 14px; display: block; } diff --git a/webroot/rsrc/css/phui/phui-feed-story.css b/webroot/rsrc/css/phui/phui-feed-story.css index f53a2dcbf9..11cdde9de8 100644 --- a/webroot/rsrc/css/phui/phui-feed-story.css +++ b/webroot/rsrc/css/phui/phui-feed-story.css @@ -35,6 +35,10 @@ overflow: hidden; } +.phui-feed-story-body.phabricator-remarkup { + line-height: inherit; +} + .phui-feed-story-foot { font-size: 11px; background: {$lightgreybackground}; @@ -47,8 +51,7 @@ color: {$greytext}; } -.phui-feed-story-foot .phui-icon-view, -.phui-feed-story-oneline-foot .phui-icon-view { +.phui-feed-story-foot .phui-icon-view { float: left; display: inline-block; margin-right: 5px; @@ -94,20 +97,3 @@ .phui-feed-story-action-list .phui-icon-view { display: block; } - -.phui-feed-story-oneline .phui-feed-story-head { - padding: 8px; -} - -.phui-feed-story-oneline .phui-feed-story-body { - padding: 0; -} - -.phui-feed-story-oneline-foot, -.phui-feed-story-oneline-foot a { - font-size: 11px; - color: {$lightgreytext}; - margin-top: 2px; - line-height: 14px; - font-weight: normal; -} diff --git a/webroot/rsrc/css/phui/phui-form-view.css b/webroot/rsrc/css/phui/phui-form-view.css index f62474ed23..766e5ff38a 100644 --- a/webroot/rsrc/css/phui/phui-form-view.css +++ b/webroot/rsrc/css/phui/phui-form-view.css @@ -144,8 +144,7 @@ .phui-form-view .aphront-form-caption { font-size: 12px; color: {$bluetext}; - padding: 8px 4px; - text-align: right; + padding: 8px 0; margin-right: 20%; margin-left: 20%; -webkit-font-smoothing: antialiased; @@ -154,7 +153,7 @@ .device-phone .phui-form-view .aphront-form-caption, .phui-form-full-width .phui-form-view .aphront-form-caption { - margin-right: 0%; + margin: 0; } /* override for when inside an aphront-panel-view */ @@ -252,16 +251,20 @@ table.aphront-form-control-checkbox-layout th { } .phui-form-inset { - margin: 0 0 8px; + margin: 4px 0 8px; padding: 8px; - background: #fff; - border: 1px solid #d4dae0; + background: #f7f9fd; + border: 1px solid {$lightblueborder}; + border-bottom: 1px solid {$blueborder}; + border-radius: 3px; } .phui-form-inset h1 { - color: {$greytext}; - font-weight: normal; + color: {$bluetext}; padding-bottom: 8px; + margin-bottom: 8px; + font-size: 14px; + border-bottom: 1px solid {$thinblueborder}; } .aphront-form-drag-and-drop-file-list { diff --git a/webroot/rsrc/css/phui/phui-tag-view.css b/webroot/rsrc/css/phui/phui-tag-view.css index 9162ca88c0..39a0c29223 100644 --- a/webroot/rsrc/css/phui/phui-tag-view.css +++ b/webroot/rsrc/css/phui/phui-tag-view.css @@ -334,10 +334,10 @@ a.phui-tag-view:hover.phui-tag-shade-disabled .phui-tag-core { } .phui-object-item .phabricator-handle-tag-list { - display: inline-block; + display: inline; } .phui-object-item .phabricator-handle-tag-list-item { - display: inline-block; + display: inline; margin: 0 4px 2px 0; } diff --git a/webroot/rsrc/css/phui/phui-timeline-view.css b/webroot/rsrc/css/phui/phui-timeline-view.css index 1303dbcca9..5953d22b7c 100644 --- a/webroot/rsrc/css/phui/phui-timeline-view.css +++ b/webroot/rsrc/css/phui/phui-timeline-view.css @@ -187,6 +187,15 @@ border-width: 0; } +.phui-timeline-spacer.phui-timeline-spacer-bold { + border-bottom: 4px solid {$lightblueborder}; + margin: 0; +} + +.phui-timeline-spacer-bold + .phui-timeline-spacer { + background-color: #ebecee; +} + .phui-timeline-icon-fill { position: absolute; width: 30px; diff --git a/webroot/rsrc/js/application/differential/behavior-keyboard-nav.js b/webroot/rsrc/js/application/differential/behavior-keyboard-nav.js index 3acca2b6a7..da97d908b2 100644 --- a/webroot/rsrc/js/application/differential/behavior-keyboard-nav.js +++ b/webroot/rsrc/js/application/differential/behavior-keyboard-nav.js @@ -242,6 +242,10 @@ JX.behavior('differential-keyboard-navigation', function(config) { function inline_op(node, op) { + // nothing selected + if (!node) { + return; + } if (!JX.DOM.scry(node, 'a', 'differential-inline-' + op)) { // No link for this operation, e.g. editing a comment you can't edit. return; diff --git a/webroot/rsrc/js/application/herald/HeraldRuleEditor.js b/webroot/rsrc/js/application/herald/HeraldRuleEditor.js index f9da34e0f6..30f34d27b3 100644 --- a/webroot/rsrc/js/application/herald/HeraldRuleEditor.js +++ b/webroot/rsrc/js/application/herald/HeraldRuleEditor.js @@ -219,6 +219,7 @@ JX.install('HeraldRuleEditor', { case 'userorproject': case 'buildplan': case 'taskpriority': + case 'taskstatus': case 'arcanistprojects': case 'legaldocuments': var tokenizer = this._newTokenizer(type); diff --git a/webroot/rsrc/js/core/MultirowRowManager.js b/webroot/rsrc/js/core/MultirowRowManager.js index f8854568c6..938add8075 100644 --- a/webroot/rsrc/js/core/MultirowRowManager.js +++ b/webroot/rsrc/js/core/MultirowRowManager.js @@ -57,13 +57,13 @@ JX.install('MultirowRowManager', { var removeButton = JX.$N( 'td', - {}, + { className: 'remove-column' }, JX.$N( 'a', - { className: 'button', + { className: 'button simple', sigil: JX.MultirowRowManager._removeSigil }, - '-')); + 'Remove')); JX.DOM.appendContent(row, removeButton); return row; diff --git a/webroot/rsrc/js/core/ToolTip.js b/webroot/rsrc/js/core/ToolTip.js index 4793a31c0e..e1a696a65b 100644 --- a/webroot/rsrc/js/core/ToolTip.js +++ b/webroot/rsrc/js/core/ToolTip.js @@ -29,14 +29,19 @@ JX.install('Tooltip', { } } - var node = JX.$N( + var node_inner = JX.$N( 'div', - { className: 'jx-tooltip-container' }, + { className: 'jx-tooltip-inner' }, [ JX.$N('div', { className: 'jx-tooltip' }, content), JX.$N('div', { className: 'jx-tooltip-anchor' }) ]); + var node = JX.$N( + 'div', + { className: 'jx-tooltip-container' }, + node_inner); + node.style.maxWidth = scale + 'px'; JX.Tooltip.hide(); diff --git a/webroot/rsrc/js/core/behavior-search-typeahead.js b/webroot/rsrc/js/core/behavior-search-typeahead.js index 151f938195..644969d107 100644 --- a/webroot/rsrc/js/core/behavior-search-typeahead.js +++ b/webroot/rsrc/js/core/behavior-search-typeahead.js @@ -62,12 +62,19 @@ JX.behavior('phabricator-search-typeahead', function(config) { var ii; for (ii = 0; ii < list.length; ii++) { var item = list[ii]; + + for (var jj = 0; jj < tokens.length; jj++) { + if (item.name.indexOf(tokens[jj]) === 0) { + priority_hits[item.id] = true; + } + } + if (!item.priority) { continue; } - for (var jj = 0; jj < tokens.length; jj++) { - if (item.priority.substr(0, tokens[jj].length) == tokens[jj]) { + for (var hh = 0; hh < tokens.length; hh++) { + if (item.priority.substr(0, tokens[hh].length) == tokens[hh]) { priority_hits[item.id] = true; } }