diff --git a/bin/bulk b/bin/bulk new file mode 120000 index 0000000000..04d0550497 --- /dev/null +++ b/bin/bulk @@ -0,0 +1 @@ +../scripts/setup/manage_bulk.php \ No newline at end of file diff --git a/resources/celerity/map.php b/resources/celerity/map.php index d937b91e4f..fece1e10ee 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,16 +9,16 @@ return array( 'names' => array( 'conpherence.pkg.css' => 'e68cf1fa', 'conpherence.pkg.js' => '15191c65', - 'core.pkg.css' => 'fdb27ef9', + 'core.pkg.css' => '075f9867', 'core.pkg.js' => '4c79d74f', 'darkconsole.pkg.js' => '1f9a31bc', 'differential.pkg.css' => '45951e9e', - 'differential.pkg.js' => '500a75c5', + 'differential.pkg.js' => '19ee9979', 'diffusion.pkg.css' => 'a2d17c7d', 'diffusion.pkg.js' => '6134c5a1', 'favicon.ico' => '30672e08', 'maniphest.pkg.css' => '4845691a', - 'maniphest.pkg.js' => '5ab2753f', + 'maniphest.pkg.js' => '4d7e79c8', 'rsrc/audio/basic/alert.mp3' => '98461568', 'rsrc/audio/basic/bing.mp3' => 'ab8603a5', 'rsrc/audio/basic/pock.mp3' => '0cc772f5', @@ -81,7 +81,6 @@ return array( 'rsrc/css/application/harbormaster/harbormaster.css' => 'f491c9f4', 'rsrc/css/application/herald/herald-test.css' => 'a52e323e', 'rsrc/css/application/herald/herald.css' => 'cd8d0134', - 'rsrc/css/application/maniphest/batch-editor.css' => 'b0f0b6d5', 'rsrc/css/application/maniphest/report.css' => '9b9580b7', 'rsrc/css/application/maniphest/task-edit.css' => 'fda62a9b', 'rsrc/css/application/maniphest/task-summary.css' => '11cc5344', @@ -135,14 +134,15 @@ return array( 'rsrc/css/phui/object-item/phui-oi-color.css' => 'cd2b9b77', 'rsrc/css/phui/object-item/phui-oi-drag-ui.css' => '08f4ccc3', 'rsrc/css/phui/object-item/phui-oi-flush-ui.css' => '9d9685d6', - 'rsrc/css/phui/object-item/phui-oi-list-view.css' => 'bf094950', + 'rsrc/css/phui/object-item/phui-oi-list-view.css' => '6ae18df0', 'rsrc/css/phui/object-item/phui-oi-simple-ui.css' => 'a8beebea', 'rsrc/css/phui/phui-action-list.css' => 'f7f61a34', 'rsrc/css/phui/phui-action-panel.css' => 'b4798122', 'rsrc/css/phui/phui-badge.css' => '22c0cf4f', 'rsrc/css/phui/phui-basic-nav-view.css' => '98c11ab3', 'rsrc/css/phui/phui-big-info-view.css' => 'acc3492c', - 'rsrc/css/phui/phui-box.css' => '9f3745fb', + 'rsrc/css/phui/phui-box.css' => '4bd6cdb9', + 'rsrc/css/phui/phui-bulk-editor.css' => '9a81e5d5', 'rsrc/css/phui/phui-chart.css' => '6bf6f78e', 'rsrc/css/phui/phui-cms.css' => '504b4b23', 'rsrc/css/phui/phui-comment-form.css' => 'ac68149f', @@ -416,11 +416,10 @@ return array( 'rsrc/js/application/drydock/drydock-live-operation-status.js' => '901935ef', 'rsrc/js/application/files/behavior-icon-composer.js' => '8499b6ab', 'rsrc/js/application/files/behavior-launch-icon-composer.js' => '48086888', - 'rsrc/js/application/herald/HeraldRuleEditor.js' => '2dff5579', + 'rsrc/js/application/herald/HeraldRuleEditor.js' => 'dca75c0e', 'rsrc/js/application/herald/PathTypeahead.js' => 'f7fc67ec', 'rsrc/js/application/herald/herald-rule-editor.js' => '7ebaeed3', - 'rsrc/js/application/maniphest/behavior-batch-editor.js' => '782ab6e7', - 'rsrc/js/application/maniphest/behavior-batch-selector.js' => '0825c27a', + 'rsrc/js/application/maniphest/behavior-batch-selector.js' => 'ad54037e', 'rsrc/js/application/maniphest/behavior-line-chart.js' => 'e4232876', 'rsrc/js/application/maniphest/behavior-list-edit.js' => 'a9f88de2', 'rsrc/js/application/maniphest/behavior-subpriorityeditor.js' => '71237763', @@ -444,7 +443,7 @@ return array( 'rsrc/js/application/releeph/releeph-preview-branch.js' => 'b2b4fbaf', 'rsrc/js/application/releeph/releeph-request-state-change.js' => 'a0b57eb8', 'rsrc/js/application/releeph/releeph-request-typeahead.js' => 'de2e896f', - 'rsrc/js/application/repository/repository-crossreference.js' => '7fe9bc12', + 'rsrc/js/application/repository/repository-crossreference.js' => '2ab10a76', 'rsrc/js/application/search/behavior-reorder-profile-menu-items.js' => 'e2e0a072', 'rsrc/js/application/search/behavior-reorder-queries.js' => 'e9581f08', 'rsrc/js/application/slowvote/behavior-slowvote-embed.js' => '887ad43f', @@ -477,6 +476,7 @@ return array( 'rsrc/js/core/behavior-audio-source.js' => '59b251eb', 'rsrc/js/core/behavior-autofocus.js' => '7319e029', 'rsrc/js/core/behavior-badge-view.js' => '8ff5e24c', + 'rsrc/js/core/behavior-bulk-editor.js' => '66a6def1', 'rsrc/js/core/behavior-choose-control.js' => '327a00d1', 'rsrc/js/core/behavior-copy.js' => 'b0b8f86d', 'rsrc/js/core/behavior-detect-timezone.js' => '4c193c96', @@ -523,6 +523,7 @@ return array( 'rsrc/js/core/phtize.js' => 'd254d646', 'rsrc/js/phui/behavior-phui-dropdown-menu.js' => 'b95d6f7d', 'rsrc/js/phui/behavior-phui-file-upload.js' => 'b003d4fb', + 'rsrc/js/phui/behavior-phui-selectable-list.js' => '464259a2', 'rsrc/js/phui/behavior-phui-submenu.js' => 'a6f7a73b', 'rsrc/js/phui/behavior-phui-tab-group.js' => '0a0b10e9', 'rsrc/js/phuix/PHUIXActionListView.js' => 'b5c256b8', @@ -531,7 +532,7 @@ return array( 'rsrc/js/phuix/PHUIXButtonView.js' => '8a91e1ac', 'rsrc/js/phuix/PHUIXDropdownMenu.js' => '04b2ae03', 'rsrc/js/phuix/PHUIXExample.js' => '68af71ca', - 'rsrc/js/phuix/PHUIXFormControl.js' => '83e03671', + 'rsrc/js/phuix/PHUIXFormControl.js' => '1dd0870c', 'rsrc/js/phuix/PHUIXIconView.js' => 'bff6884b', ), 'symbols' => array( @@ -579,7 +580,7 @@ return array( 'global-drag-and-drop-css' => 'b556a948', 'harbormaster-css' => 'f491c9f4', 'herald-css' => 'cd8d0134', - 'herald-rule-editor' => '2dff5579', + 'herald-rule-editor' => 'dca75c0e', 'herald-test-css' => 'a52e323e', 'inline-comment-summary-css' => 'f23d4e8f', 'javelin-aphlict' => 'e1d4b11a', @@ -594,6 +595,7 @@ return array( 'javelin-behavior-audio-source' => '59b251eb', 'javelin-behavior-audit-preview' => 'd835b03a', 'javelin-behavior-badge-view' => '8ff5e24c', + 'javelin-behavior-bulk-editor' => '66a6def1', 'javelin-behavior-bulk-job-reload' => 'edf8a145', 'javelin-behavior-calendar-month-view' => 'fe33e256', 'javelin-behavior-choose-control' => '327a00d1', @@ -641,8 +643,7 @@ return array( 'javelin-behavior-lightbox-attachments' => '560f41da', 'javelin-behavior-line-chart' => 'e4232876', 'javelin-behavior-load-blame' => '42126667', - 'javelin-behavior-maniphest-batch-editor' => '782ab6e7', - 'javelin-behavior-maniphest-batch-selector' => '0825c27a', + 'javelin-behavior-maniphest-batch-selector' => 'ad54037e', 'javelin-behavior-maniphest-list-editor' => 'a9f88de2', 'javelin-behavior-maniphest-subpriority-editor' => '71237763', 'javelin-behavior-owners-path-editor' => '7a68dda3', @@ -673,6 +674,7 @@ return array( 'javelin-behavior-phui-dropdown-menu' => 'b95d6f7d', 'javelin-behavior-phui-file-upload' => 'b003d4fb', 'javelin-behavior-phui-hovercards' => 'bcaccd64', + 'javelin-behavior-phui-selectable-list' => '464259a2', 'javelin-behavior-phui-submenu' => 'a6f7a73b', 'javelin-behavior-phui-tab-group' => '0a0b10e9', 'javelin-behavior-phuix-example' => '68af71ca', @@ -690,7 +692,7 @@ return array( 'javelin-behavior-reorder-applications' => '76b9fc3e', 'javelin-behavior-reorder-columns' => 'e1d25dfb', 'javelin-behavior-reorder-profile-menu-items' => 'e2e0a072', - 'javelin-behavior-repository-crossreference' => '7fe9bc12', + 'javelin-behavior-repository-crossreference' => '2ab10a76', 'javelin-behavior-scrollbar' => '834a1173', 'javelin-behavior-search-reorder-queries' => 'e9581f08', 'javelin-behavior-select-content' => 'bf5374ef', @@ -754,7 +756,6 @@ return array( 'javelin-workboard-column' => '758b4758', 'javelin-workboard-controller' => '26167537', 'javelin-workflow' => '1e911d0f', - 'maniphest-batch-editor' => 'b0f0b6d5', 'maniphest-report-css' => '9b9580b7', 'maniphest-task-edit-css' => 'fda62a9b', 'maniphest-task-summary-css' => '11cc5344', @@ -820,7 +821,8 @@ return array( 'phui-badge-view-css' => '22c0cf4f', 'phui-basic-nav-view-css' => '98c11ab3', 'phui-big-info-view-css' => 'acc3492c', - 'phui-box-css' => '9f3745fb', + 'phui-box-css' => '4bd6cdb9', + 'phui-bulk-editor-css' => '9a81e5d5', 'phui-button-bar-css' => 'f1ff5494', 'phui-button-css' => '1863cc6e', 'phui-button-simple-css' => '8e1baf68', @@ -860,7 +862,7 @@ return array( 'phui-oi-color-css' => 'cd2b9b77', 'phui-oi-drag-ui-css' => '08f4ccc3', 'phui-oi-flush-ui-css' => '9d9685d6', - 'phui-oi-list-view-css' => 'bf094950', + 'phui-oi-list-view-css' => '6ae18df0', 'phui-oi-simple-ui-css' => 'a8beebea', 'phui-pager-css' => 'edcbc226', 'phui-pinboard-view-css' => '2495140e', @@ -882,7 +884,7 @@ return array( 'phuix-autocomplete' => 'e0731603', 'phuix-button-view' => '8a91e1ac', 'phuix-dropdown-menu' => '04b2ae03', - 'phuix-form-control-view' => '83e03671', + 'phuix-form-control-view' => '1dd0870c', 'phuix-icon-view' => 'bff6884b', 'policy-css' => '957ea14c', 'policy-edit-css' => '815c66f7', @@ -958,12 +960,6 @@ return array( 'javelin-stratcom', 'javelin-workflow', ), - '0825c27a' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-util', - ), '08f4ccc3' => array( 'phui-oi-list-view-css', ), @@ -1033,6 +1029,10 @@ return array( 'javelin-request', 'javelin-uri', ), + '1dd0870c' => array( + 'javelin-install', + 'javelin-dom', + ), '1e911d0f' => array( 'javelin-stratcom', 'javelin-request', @@ -1088,6 +1088,12 @@ return array( 'javelin-install', 'javelin-util', ), + '2ab10a76' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-uri', + ), '2ae077e1' => array( 'javelin-behavior', 'javelin-dom', @@ -1106,15 +1112,6 @@ return array( 'javelin-install', 'javelin-event', ), - '2dff5579' => array( - 'multirow-row-manager', - 'javelin-install', - 'javelin-util', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-json', - 'phabricator-prefab', - ), '2ee659ce' => array( 'javelin-install', ), @@ -1226,6 +1223,11 @@ return array( 'javelin-behavior', 'javelin-dom', ), + '464259a2' => array( + 'javelin-behavior', + 'javelin-stratcom', + 'javelin-dom', + ), '469c0d9e' => array( 'javelin-behavior', 'javelin-dom', @@ -1422,6 +1424,14 @@ return array( 'javelin-workflow', 'javelin-dom', ), + '66a6def1' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'multirow-row-manager', + 'javelin-json', + 'phuix-form-control-view', + ), '680ea2c8' => array( 'javelin-install', 'javelin-dom', @@ -1523,14 +1533,6 @@ return array( 'javelin-request', 'javelin-util', ), - '782ab6e7' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'phabricator-prefab', - 'multirow-row-manager', - 'javelin-json', - ), '7927a7d3' => array( 'javelin-behavior', 'javelin-quicksand', @@ -1559,20 +1561,10 @@ return array( '7f243deb' => array( 'javelin-install', ), - '7fe9bc12' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-uri', - ), '834a1173' => array( 'javelin-behavior', 'javelin-scrollbar', ), - '83e03671' => array( - 'javelin-install', - 'javelin-dom', - ), '8499b6ab' => array( 'javelin-behavior', 'javelin-dom', @@ -1808,6 +1800,12 @@ return array( 'phuix-autocomplete', 'javelin-mask', ), + 'ad54037e' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-util', + ), 'b003d4fb' => array( 'javelin-behavior', 'javelin-stratcom', @@ -2023,6 +2021,15 @@ return array( 'javelin-util', 'phabricator-shaped-request', ), + 'dca75c0e' => array( + 'multirow-row-manager', + 'javelin-install', + 'javelin-util', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-json', + 'phabricator-prefab', + ), 'de2e896f' => array( 'javelin-behavior', 'javelin-dom', diff --git a/resources/sql/autopatches/20140218.passwords.4.vcs.php b/resources/sql/autopatches/20140218.passwords.4.vcs.php index 1030775326..c811844c27 100644 --- a/resources/sql/autopatches/20140218.passwords.4.vcs.php +++ b/resources/sql/autopatches/20140218.passwords.4.vcs.php @@ -1,27 +1,13 @@ establishConnection('w'); +// This migration once upgraded VCS password hashing, but the table was +// later removed in 2018 (see T13043). -echo pht('Upgrading password hashing for VCS passwords.')."\n"; +// Since almost four years have passed since this migration, the cost of +// losing this data is very small (users just need to reset their passwords), +// and a version of this migration against the modern schema isn't easy to +// implement or test, just skip the migration. -$best_hasher = PhabricatorPasswordHasher::getBestHasher(); -foreach (new LiskMigrationIterator($table) as $password) { - $id = $password->getID(); - - echo pht('Migrating VCS password %d...', $id)."\n"; - - $input_hash = $password->getPasswordHash(); - $input_envelope = new PhutilOpaqueEnvelope($input_hash); - - $storage_hash = $best_hasher->getPasswordHashForStorage($input_envelope); - - queryfx( - $conn_w, - 'UPDATE %T SET passwordHash = %s WHERE id = %d', - $table->getTableName(), - $storage_hash->openEnvelope(), - $id); -} - -echo pht('Done.')."\n"; +// This means that installs which upgrade from a version of Phabricator +// released prior to Feb 2014 to a version of Phabricator relased after +// Jan 2018 will need to have users reset VCS passwords. diff --git a/resources/sql/autopatches/20180119.bulk.01.silent.sql b/resources/sql/autopatches/20180119.bulk.01.silent.sql new file mode 100644 index 0000000000..b426de953d --- /dev/null +++ b/resources/sql/autopatches/20180119.bulk.01.silent.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_worker.worker_bulkjob + ADD isSilent BOOL NOT NULL; diff --git a/resources/sql/autopatches/20180120.auth.01.password.sql b/resources/sql/autopatches/20180120.auth.01.password.sql new file mode 100644 index 0000000000..679d50c5b1 --- /dev/null +++ b/resources/sql/autopatches/20180120.auth.01.password.sql @@ -0,0 +1,10 @@ +CREATE TABLE {$NAMESPACE}_auth.auth_password ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + objectPHID VARBINARY(64) NOT NULL, + passwordType VARCHAR(64) NOT NULL COLLATE {$COLLATE_TEXT}, + passwordHash VARCHAR(128) NOT NULL COLLATE {$COLLATE_TEXT}, + isRevoked BOOL NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20180120.auth.02.passwordxaction.sql b/resources/sql/autopatches/20180120.auth.02.passwordxaction.sql new file mode 100644 index 0000000000..1d4f075b87 --- /dev/null +++ b/resources/sql/autopatches/20180120.auth.02.passwordxaction.sql @@ -0,0 +1,19 @@ +CREATE TABLE {$NAMESPACE}_auth.auth_passwordtransaction ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + authorPHID VARBINARY(64) NOT NULL, + objectPHID VARBINARY(64) NOT NULL, + viewPolicy VARBINARY(64) NOT NULL, + editPolicy VARBINARY(64) NOT NULL, + commentPHID VARBINARY(64) DEFAULT NULL, + commentVersion INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) COLLATE {$COLLATE_TEXT} NOT NULL, + oldValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + newValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + contentSource LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + metadata LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20180120.auth.03.vcsdata.sql b/resources/sql/autopatches/20180120.auth.03.vcsdata.sql new file mode 100644 index 0000000000..0cf73f84d9 --- /dev/null +++ b/resources/sql/autopatches/20180120.auth.03.vcsdata.sql @@ -0,0 +1,6 @@ +INSERT INTO {$NAMESPACE}_auth.auth_password + (objectPHID, phid, passwordType, passwordHash, isRevoked, + dateCreated, dateModified) + SELECT userPHID, CONCAT('XVCS', id), 'vcs', passwordHash, 0, + dateCreated, dateModified + FROM {$NAMESPACE}_repository.repository_vcspassword; diff --git a/resources/sql/autopatches/20180120.auth.04.vcsphid.php b/resources/sql/autopatches/20180120.auth.04.vcsphid.php new file mode 100644 index 0000000000..0a5dd0f067 --- /dev/null +++ b/resources/sql/autopatches/20180120.auth.04.vcsphid.php @@ -0,0 +1,24 @@ +establishConnection('w'); + +$password_type = PhabricatorAuthPasswordPHIDType::TYPECONST; + +foreach (new LiskMigrationIterator($table) as $row) { + if (phid_get_type($row->getPHID()) == $password_type) { + continue; + } + + $new_phid = $row->generatePHID(); + + queryfx( + $conn, + 'UPDATE %T SET phid = %s WHERE id = %d', + $table->getTableName(), + $new_phid, + $row->getID()); +} diff --git a/resources/sql/autopatches/20180121.auth.01.vcsnuke.sql b/resources/sql/autopatches/20180121.auth.01.vcsnuke.sql new file mode 100644 index 0000000000..b106a2ddd7 --- /dev/null +++ b/resources/sql/autopatches/20180121.auth.01.vcsnuke.sql @@ -0,0 +1 @@ +DROP TABLE {$NAMESPACE}_repository.repository_vcspassword; diff --git a/resources/sql/autopatches/20180121.auth.02.passsalt.sql b/resources/sql/autopatches/20180121.auth.02.passsalt.sql new file mode 100644 index 0000000000..78ee953ea4 --- /dev/null +++ b/resources/sql/autopatches/20180121.auth.02.passsalt.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_auth.auth_password + ADD passwordSalt VARCHAR(64) NOT NULL COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20180121.auth.03.accountdata.sql b/resources/sql/autopatches/20180121.auth.03.accountdata.sql new file mode 100644 index 0000000000..cbb4dc2a50 --- /dev/null +++ b/resources/sql/autopatches/20180121.auth.03.accountdata.sql @@ -0,0 +1,7 @@ +INSERT INTO {$NAMESPACE}_auth.auth_password + (objectPHID, phid, passwordType, passwordHash, passwordSalt, isRevoked, + dateCreated, dateModified) + SELECT phid, CONCAT('XACCOUNT', id), 'account', passwordHash, passwordSalt, 0, + dateCreated, dateModified + FROM {$NAMESPACE}_user.user + WHERE passwordHash != ''; diff --git a/resources/sql/autopatches/20180121.auth.04.accountphid.php b/resources/sql/autopatches/20180121.auth.04.accountphid.php new file mode 100644 index 0000000000..e0ebc04f4b --- /dev/null +++ b/resources/sql/autopatches/20180121.auth.04.accountphid.php @@ -0,0 +1,24 @@ +establishConnection('w'); + +$password_type = PhabricatorAuthPasswordPHIDType::TYPECONST; + +foreach (new LiskMigrationIterator($table) as $row) { + if (phid_get_type($row->getPHID()) == $password_type) { + continue; + } + + $new_phid = $row->generatePHID(); + + queryfx( + $conn, + 'UPDATE %T SET phid = %s WHERE id = %d', + $table->getTableName(), + $new_phid, + $row->getID()); +} diff --git a/resources/sql/autopatches/20180121.auth.05.accountnuke.sql b/resources/sql/autopatches/20180121.auth.05.accountnuke.sql new file mode 100644 index 0000000000..c8dc50bcf2 --- /dev/null +++ b/resources/sql/autopatches/20180121.auth.05.accountnuke.sql @@ -0,0 +1,5 @@ +ALTER TABLE {$NAMESPACE}_user.user + DROP passwordSalt; + +ALTER TABLE {$NAMESPACE}_user.user + DROP passwordHash; diff --git a/resources/sql/autopatches/20180121.auth.06.legacydigest.sql b/resources/sql/autopatches/20180121.auth.06.legacydigest.sql new file mode 100644 index 0000000000..af9c7990d0 --- /dev/null +++ b/resources/sql/autopatches/20180121.auth.06.legacydigest.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_auth.auth_password + ADD legacyDigestFormat VARCHAR(32) COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20180121.auth.07.marklegacy.sql b/resources/sql/autopatches/20180121.auth.07.marklegacy.sql new file mode 100644 index 0000000000..798757d348 --- /dev/null +++ b/resources/sql/autopatches/20180121.auth.07.marklegacy.sql @@ -0,0 +1,4 @@ +UPDATE {$NAMESPACE}_auth.auth_password + SET legacyDigestFormat = 'v1' + WHERE passwordType IN ('vcs', 'account') + AND legacyDigestFormat IS NULL; diff --git a/resources/sql/autopatches/20180124.herald.01.repetition.sql b/resources/sql/autopatches/20180124.herald.01.repetition.sql new file mode 100644 index 0000000000..31f1477e01 --- /dev/null +++ b/resources/sql/autopatches/20180124.herald.01.repetition.sql @@ -0,0 +1,26 @@ +/* This column was previously "uint32?" with these values: + + 1: run every time + 0: run only the first time + +*/ + +UPDATE {$NAMESPACE}_herald.herald_rule + SET repetitionPolicy = '1' + WHERE repetitionPolicy IS NULL; + +ALTER TABLE {$NAMESPACE}_herald.herald_rule + CHANGE repetitionPolicy + repetitionPolicy VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT}; + +/* If the old value was "0", the new value is "first". */ + +UPDATE {$NAMESPACE}_herald.herald_rule + SET repetitionPolicy = 'first' + WHERE repetitionPolicy = '0'; + +/* If the old value was anything else, the new value is "every". */ + +UPDATE {$NAMESPACE}_herald.herald_rule + SET repetitionPolicy = 'every' + WHERE repetitionPolicy NOT IN ('first', '0'); diff --git a/resources/sql/patches/102.heraldcleanup.php b/resources/sql/patches/102.heraldcleanup.php index 5b885bd670..f7f58131ea 100644 --- a/resources/sql/patches/102.heraldcleanup.php +++ b/resources/sql/patches/102.heraldcleanup.php @@ -1,39 +1,8 @@ openTransaction(); -$table->beginReadLocking(); +// Once, this migration deleted some unnecessary rows written by Herald before +// January 2012. These rows don't hurt anything, they just cluttered up the +// database a bit. -$rules = $table->loadAll(); -foreach ($rules as $key => $rule) { - $first_policy = HeraldRepetitionPolicyConfig::toInt( - HeraldRepetitionPolicyConfig::FIRST); - if ($rule->getRepetitionPolicy() != $first_policy) { - unset($rules[$key]); - } -} - -$conn_w = $table->establishConnection('w'); - -$clause = ''; -if ($rules) { - $clause = qsprintf( - $conn_w, - 'WHERE ruleID NOT IN (%Ld)', - mpull($rules, 'getID')); -} - -echo pht('This may take a moment')."\n"; -do { - queryfx( - $conn_w, - 'DELETE FROM %T %Q LIMIT 1000', - HeraldRule::TABLE_RULE_APPLIED, - $clause); - echo '.'; -} while ($conn_w->getAffectedRows()); - -$table->endReadLocking(); -$table->saveTransaction(); -echo "\n".pht('Done.')."\n"; +// The migration was removed in January 2018 to make maintenance on rule +// repetition policies easier. diff --git a/scripts/manage_bulk.php b/scripts/manage_bulk.php new file mode 120000 index 0000000000..04d0550497 --- /dev/null +++ b/scripts/manage_bulk.php @@ -0,0 +1 @@ +../scripts/setup/manage_bulk.php \ No newline at end of file diff --git a/scripts/setup/manage_bulk.php b/scripts/setup/manage_bulk.php new file mode 100755 index 0000000000..9786f9b078 --- /dev/null +++ b/scripts/setup/manage_bulk.php @@ -0,0 +1,21 @@ +#!/usr/bin/env php +setTagline(pht('manage bulk jobs')); +$args->setSynopsis(<<parseStandardArguments(); + +$workflows = id(new PhutilClassMapQuery()) + ->setAncestorClass('PhabricatorBulkManagementWorkflow') + ->execute(); +$workflows[] = new PhutilHelpArgumentWorkflow(); +$args->parseWorkflows($workflows); diff --git a/scripts/ssh/ssh-exec.php b/scripts/ssh/ssh-exec.php index 5748e371cf..2ff1bdc198 100755 --- a/scripts/ssh/ssh-exec.php +++ b/scripts/ssh/ssh-exec.php @@ -245,7 +245,7 @@ try { } $workflow = $parsed_args->parseWorkflows($workflows); - $workflow->setUser($user); + $workflow->setSSHUser($user); $workflow->setOriginalArguments($original_argv); $workflow->setIsClusterRequest($is_cluster_request); diff --git a/scripts/user/account_admin.php b/scripts/user/account_admin.php index 2fa5446648..9e01896637 100755 --- a/scripts/user/account_admin.php +++ b/scripts/user/account_admin.php @@ -112,17 +112,6 @@ if ($is_new) { $create_email = $email; } -$changed_pass = false; -// This disables local echo, so the user's password is not shown as they type -// it. -phutil_passthru('stty -echo'); -$password = phutil_console_prompt( - pht('Enter a password for this user [blank to leave unchanged]:')); -phutil_passthru('stty echo'); -if (strlen($password)) { - $changed_pass = $password; -} - $is_system_agent = $user->getIsSystemAgent(); $set_system_agent = phutil_console_confirm( pht('Is this user a bot?'), @@ -158,10 +147,6 @@ printf($tpl, pht('Real Name'), $original->getRealName(), $user->getRealName()); if ($is_new) { printf($tpl, pht('Email'), '', $create_email); } -printf($tpl, pht('Password'), null, - ($changed_pass !== false) - ? pht('Updated') - : pht('Unchanged')); printf( $tpl, @@ -218,11 +203,6 @@ $user->openTransaction(); $editor->makeAdminUser($user, $set_admin); $editor->makeSystemAgentUser($user, $set_system_agent); - if ($changed_pass !== false) { - $envelope = new PhutilOpaqueEnvelope($changed_pass); - $editor->changePassword($user, $envelope); - } - $user->saveTransaction(); echo pht('Saved changes.')."\n"; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index f4a410ad2e..44d67c6259 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -222,6 +222,12 @@ phutil_register_library_map(array( 'AuditConduitAPIMethod' => 'applications/audit/conduit/AuditConduitAPIMethod.php', 'AuditQueryConduitAPIMethod' => 'applications/audit/conduit/AuditQueryConduitAPIMethod.php', 'AuthManageProvidersCapability' => 'applications/auth/capability/AuthManageProvidersCapability.php', + 'BulkParameterType' => 'applications/transactions/bulk/type/BulkParameterType.php', + 'BulkPointsParameterType' => 'applications/transactions/bulk/type/BulkPointsParameterType.php', + 'BulkRemarkupParameterType' => 'applications/transactions/bulk/type/BulkRemarkupParameterType.php', + 'BulkSelectParameterType' => 'applications/transactions/bulk/type/BulkSelectParameterType.php', + 'BulkStringParameterType' => 'applications/transactions/bulk/type/BulkStringParameterType.php', + 'BulkTokenizerParameterType' => 'applications/transactions/bulk/type/BulkTokenizerParameterType.php', 'CalendarTimeUtil' => 'applications/calendar/util/CalendarTimeUtil.php', 'CalendarTimeUtilTestCase' => 'applications/calendar/__tests__/CalendarTimeUtilTestCase.php', 'CelerityAPI' => 'applications/celerity/CelerityAPI.php', @@ -584,6 +590,7 @@ phutil_register_library_map(array( 'DifferentialRevisionStatus' => 'applications/differential/constants/DifferentialRevisionStatus.php', 'DifferentialRevisionStatusDatasource' => 'applications/differential/typeahead/DifferentialRevisionStatusDatasource.php', 'DifferentialRevisionStatusFunctionDatasource' => 'applications/differential/typeahead/DifferentialRevisionStatusFunctionDatasource.php', + 'DifferentialRevisionStatusHeraldField' => 'applications/differential/herald/DifferentialRevisionStatusHeraldField.php', 'DifferentialRevisionStatusTransaction' => 'applications/differential/xaction/DifferentialRevisionStatusTransaction.php', 'DifferentialRevisionSummaryHeraldField' => 'applications/differential/herald/DifferentialRevisionSummaryHeraldField.php', 'DifferentialRevisionSummaryTransaction' => 'applications/differential/xaction/DifferentialRevisionSummaryTransaction.php', @@ -758,6 +765,7 @@ phutil_register_library_map(array( 'DiffusionLintCountQuery' => 'applications/diffusion/query/DiffusionLintCountQuery.php', 'DiffusionLintSaveRunner' => 'applications/diffusion/DiffusionLintSaveRunner.php', 'DiffusionLocalRepositoryFilter' => 'applications/diffusion/data/DiffusionLocalRepositoryFilter.php', + 'DiffusionLogController' => 'applications/diffusion/controller/DiffusionLogController.php', 'DiffusionLookSoonConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionLookSoonConduitAPIMethod.php', 'DiffusionLowLevelCommitFieldsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelCommitFieldsQuery.php', 'DiffusionLowLevelCommitQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelCommitQuery.php', @@ -825,9 +833,11 @@ phutil_register_library_map(array( 'DiffusionPreCommitRefTypeHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitRefTypeHeraldField.php', 'DiffusionPreCommitUsesGitLFSHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitUsesGitLFSHeraldField.php', 'DiffusionPullEventGarbageCollector' => 'applications/diffusion/garbagecollector/DiffusionPullEventGarbageCollector.php', + 'DiffusionPullLogListController' => 'applications/diffusion/controller/DiffusionPullLogListController.php', + 'DiffusionPullLogListView' => 'applications/diffusion/view/DiffusionPullLogListView.php', + 'DiffusionPullLogSearchEngine' => 'applications/diffusion/query/DiffusionPullLogSearchEngine.php', 'DiffusionPushCapability' => 'applications/diffusion/capability/DiffusionPushCapability.php', 'DiffusionPushEventViewController' => 'applications/diffusion/controller/DiffusionPushEventViewController.php', - 'DiffusionPushLogController' => 'applications/diffusion/controller/DiffusionPushLogController.php', 'DiffusionPushLogListController' => 'applications/diffusion/controller/DiffusionPushLogListController.php', 'DiffusionPushLogListView' => 'applications/diffusion/view/DiffusionPushLogListView.php', 'DiffusionPythonExternalSymbolsSource' => 'applications/diffusion/symbol/DiffusionPythonExternalSymbolsSource.php', @@ -1351,7 +1361,9 @@ phutil_register_library_map(array( 'HeraldConditionTranscript' => 'applications/herald/storage/transcript/HeraldConditionTranscript.php', 'HeraldContentSourceField' => 'applications/herald/field/HeraldContentSourceField.php', 'HeraldController' => 'applications/herald/controller/HeraldController.php', + 'HeraldCoreStateReasons' => 'applications/herald/state/HeraldCoreStateReasons.php', 'HeraldDAO' => 'applications/herald/storage/HeraldDAO.php', + 'HeraldDeprecatedFieldGroup' => 'applications/herald/field/HeraldDeprecatedFieldGroup.php', 'HeraldDifferentialAdapter' => 'applications/differential/herald/HeraldDifferentialAdapter.php', 'HeraldDifferentialDiffAdapter' => 'applications/differential/herald/HeraldDifferentialDiffAdapter.php', 'HeraldDifferentialRevisionAdapter' => 'applications/differential/herald/HeraldDifferentialRevisionAdapter.php', @@ -1389,7 +1401,6 @@ phutil_register_library_map(array( 'HeraldRelatedFieldGroup' => 'applications/herald/field/HeraldRelatedFieldGroup.php', 'HeraldRemarkupFieldValue' => 'applications/herald/value/HeraldRemarkupFieldValue.php', 'HeraldRemarkupRule' => 'applications/herald/remarkup/HeraldRemarkupRule.php', - 'HeraldRepetitionPolicyConfig' => 'applications/herald/config/HeraldRepetitionPolicyConfig.php', 'HeraldRule' => 'applications/herald/storage/HeraldRule.php', 'HeraldRuleController' => 'applications/herald/controller/HeraldRuleController.php', 'HeraldRuleDatasource' => 'applications/herald/typeahead/HeraldRuleDatasource.php', @@ -1487,8 +1498,8 @@ phutil_register_library_map(array( 'MacroQueryConduitAPIMethod' => 'applications/macro/conduit/MacroQueryConduitAPIMethod.php', 'ManiphestAssignEmailCommand' => 'applications/maniphest/command/ManiphestAssignEmailCommand.php', 'ManiphestAssigneeDatasource' => 'applications/maniphest/typeahead/ManiphestAssigneeDatasource.php', - 'ManiphestBatchEditController' => 'applications/maniphest/controller/ManiphestBatchEditController.php', 'ManiphestBulkEditCapability' => 'applications/maniphest/capability/ManiphestBulkEditCapability.php', + 'ManiphestBulkEditController' => 'applications/maniphest/controller/ManiphestBulkEditController.php', 'ManiphestClaimEmailCommand' => 'applications/maniphest/command/ManiphestClaimEmailCommand.php', 'ManiphestCloseEmailCommand' => 'applications/maniphest/command/ManiphestCloseEmailCommand.php', 'ManiphestConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestConduitAPIMethod.php', @@ -1547,6 +1558,7 @@ phutil_register_library_map(array( 'ManiphestTaskAttachTransaction' => 'applications/maniphest/xaction/ManiphestTaskAttachTransaction.php', 'ManiphestTaskAuthorHeraldField' => 'applications/maniphest/herald/ManiphestTaskAuthorHeraldField.php', 'ManiphestTaskAuthorPolicyRule' => 'applications/maniphest/policyrule/ManiphestTaskAuthorPolicyRule.php', + 'ManiphestTaskBulkEngine' => 'applications/maniphest/bulk/ManiphestTaskBulkEngine.php', 'ManiphestTaskCloseAsDuplicateRelationship' => 'applications/maniphest/relationship/ManiphestTaskCloseAsDuplicateRelationship.php', 'ManiphestTaskClosedStatusDatasource' => 'applications/maniphest/typeahead/ManiphestTaskClosedStatusDatasource.php', 'ManiphestTaskCoverImageTransaction' => 'applications/maniphest/xaction/ManiphestTaskCoverImageTransaction.php', @@ -1556,7 +1568,6 @@ phutil_register_library_map(array( 'ManiphestTaskDescriptionTransaction' => 'applications/maniphest/xaction/ManiphestTaskDescriptionTransaction.php', 'ManiphestTaskDetailController' => 'applications/maniphest/controller/ManiphestTaskDetailController.php', 'ManiphestTaskEdgeTransaction' => 'applications/maniphest/xaction/ManiphestTaskEdgeTransaction.php', - 'ManiphestTaskEditBulkJobType' => 'applications/maniphest/bulk/ManiphestTaskEditBulkJobType.php', 'ManiphestTaskEditController' => 'applications/maniphest/controller/ManiphestTaskEditController.php', 'ManiphestTaskEditEngineLock' => 'applications/maniphest/editor/ManiphestTaskEditEngineLock.php', 'ManiphestTaskFerretEngine' => 'applications/maniphest/search/ManiphestTaskFerretEngine.php', @@ -2026,6 +2037,7 @@ phutil_register_library_map(array( 'PhabricatorAuthApplication' => 'applications/auth/application/PhabricatorAuthApplication.php', 'PhabricatorAuthAuthFactorPHIDType' => 'applications/auth/phid/PhabricatorAuthAuthFactorPHIDType.php', 'PhabricatorAuthAuthProviderPHIDType' => 'applications/auth/phid/PhabricatorAuthAuthProviderPHIDType.php', + 'PhabricatorAuthChangePasswordAction' => 'applications/auth/action/PhabricatorAuthChangePasswordAction.php', 'PhabricatorAuthConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthConduitAPIMethod.php', 'PhabricatorAuthConduitTokenRevoker' => 'applications/auth/revoker/PhabricatorAuthConduitTokenRevoker.php', 'PhabricatorAuthConfirmLinkController' => 'applications/auth/controller/PhabricatorAuthConfirmLinkController.php', @@ -2082,7 +2094,21 @@ phutil_register_library_map(array( 'PhabricatorAuthOldOAuthRedirectController' => 'applications/auth/controller/PhabricatorAuthOldOAuthRedirectController.php', 'PhabricatorAuthOneTimeLoginController' => 'applications/auth/controller/PhabricatorAuthOneTimeLoginController.php', 'PhabricatorAuthOneTimeLoginTemporaryTokenType' => 'applications/auth/tokentype/PhabricatorAuthOneTimeLoginTemporaryTokenType.php', + 'PhabricatorAuthPassword' => 'applications/auth/storage/PhabricatorAuthPassword.php', + 'PhabricatorAuthPasswordEditor' => 'applications/auth/editor/PhabricatorAuthPasswordEditor.php', + 'PhabricatorAuthPasswordEngine' => 'applications/auth/engine/PhabricatorAuthPasswordEngine.php', + 'PhabricatorAuthPasswordException' => 'applications/auth/password/PhabricatorAuthPasswordException.php', + 'PhabricatorAuthPasswordHashInterface' => 'applications/auth/password/PhabricatorAuthPasswordHashInterface.php', + 'PhabricatorAuthPasswordPHIDType' => 'applications/auth/phid/PhabricatorAuthPasswordPHIDType.php', + 'PhabricatorAuthPasswordQuery' => 'applications/auth/query/PhabricatorAuthPasswordQuery.php', 'PhabricatorAuthPasswordResetTemporaryTokenType' => 'applications/auth/tokentype/PhabricatorAuthPasswordResetTemporaryTokenType.php', + 'PhabricatorAuthPasswordRevokeTransaction' => 'applications/auth/xaction/PhabricatorAuthPasswordRevokeTransaction.php', + 'PhabricatorAuthPasswordRevoker' => 'applications/auth/revoker/PhabricatorAuthPasswordRevoker.php', + 'PhabricatorAuthPasswordTestCase' => 'applications/auth/__tests__/PhabricatorAuthPasswordTestCase.php', + 'PhabricatorAuthPasswordTransaction' => 'applications/auth/storage/PhabricatorAuthPasswordTransaction.php', + 'PhabricatorAuthPasswordTransactionQuery' => 'applications/auth/query/PhabricatorAuthPasswordTransactionQuery.php', + 'PhabricatorAuthPasswordTransactionType' => 'applications/auth/xaction/PhabricatorAuthPasswordTransactionType.php', + 'PhabricatorAuthPasswordUpgradeTransaction' => 'applications/auth/xaction/PhabricatorAuthPasswordUpgradeTransaction.php', 'PhabricatorAuthProvider' => 'applications/auth/provider/PhabricatorAuthProvider.php', 'PhabricatorAuthProviderConfig' => 'applications/auth/storage/PhabricatorAuthProviderConfig.php', 'PhabricatorAuthProviderConfigController' => 'applications/auth/controller/config/PhabricatorAuthProviderConfigController.php', @@ -2098,7 +2124,6 @@ phutil_register_library_map(array( 'PhabricatorAuthRevoker' => 'applications/auth/revoker/PhabricatorAuthRevoker.php', 'PhabricatorAuthSSHKey' => 'applications/auth/storage/PhabricatorAuthSSHKey.php', 'PhabricatorAuthSSHKeyController' => 'applications/auth/controller/PhabricatorAuthSSHKeyController.php', - 'PhabricatorAuthSSHKeyDeactivateController' => 'applications/auth/controller/PhabricatorAuthSSHKeyDeactivateController.php', 'PhabricatorAuthSSHKeyEditController' => 'applications/auth/controller/PhabricatorAuthSSHKeyEditController.php', 'PhabricatorAuthSSHKeyEditor' => 'applications/auth/editor/PhabricatorAuthSSHKeyEditor.php', 'PhabricatorAuthSSHKeyGenerateController' => 'applications/auth/controller/PhabricatorAuthSSHKeyGenerateController.php', @@ -2106,12 +2131,15 @@ phutil_register_library_map(array( 'PhabricatorAuthSSHKeyPHIDType' => 'applications/auth/phid/PhabricatorAuthSSHKeyPHIDType.php', 'PhabricatorAuthSSHKeyQuery' => 'applications/auth/query/PhabricatorAuthSSHKeyQuery.php', 'PhabricatorAuthSSHKeyReplyHandler' => 'applications/auth/mail/PhabricatorAuthSSHKeyReplyHandler.php', + 'PhabricatorAuthSSHKeyRevokeController' => 'applications/auth/controller/PhabricatorAuthSSHKeyRevokeController.php', 'PhabricatorAuthSSHKeySearchEngine' => 'applications/auth/query/PhabricatorAuthSSHKeySearchEngine.php', 'PhabricatorAuthSSHKeyTableView' => 'applications/auth/view/PhabricatorAuthSSHKeyTableView.php', + 'PhabricatorAuthSSHKeyTestCase' => 'applications/auth/__tests__/PhabricatorAuthSSHKeyTestCase.php', 'PhabricatorAuthSSHKeyTransaction' => 'applications/auth/storage/PhabricatorAuthSSHKeyTransaction.php', 'PhabricatorAuthSSHKeyTransactionQuery' => 'applications/auth/query/PhabricatorAuthSSHKeyTransactionQuery.php', 'PhabricatorAuthSSHKeyViewController' => 'applications/auth/controller/PhabricatorAuthSSHKeyViewController.php', 'PhabricatorAuthSSHPublicKey' => 'applications/auth/sshkey/PhabricatorAuthSSHPublicKey.php', + 'PhabricatorAuthSSHRevoker' => 'applications/auth/revoker/PhabricatorAuthSSHRevoker.php', 'PhabricatorAuthSession' => 'applications/auth/storage/PhabricatorAuthSession.php', 'PhabricatorAuthSessionEngine' => 'applications/auth/engine/PhabricatorAuthSessionEngine.php', 'PhabricatorAuthSessionEngineExtension' => 'applications/auth/engine/PhabricatorAuthSessionEngineExtension.php', @@ -2119,6 +2147,7 @@ phutil_register_library_map(array( 'PhabricatorAuthSessionGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthSessionGarbageCollector.php', 'PhabricatorAuthSessionInfo' => 'applications/auth/data/PhabricatorAuthSessionInfo.php', 'PhabricatorAuthSessionQuery' => 'applications/auth/query/PhabricatorAuthSessionQuery.php', + 'PhabricatorAuthSessionRevoker' => 'applications/auth/revoker/PhabricatorAuthSessionRevoker.php', 'PhabricatorAuthSetPasswordController' => 'applications/auth/controller/PhabricatorAuthSetPasswordController.php', 'PhabricatorAuthSetupCheck' => 'applications/config/check/PhabricatorAuthSetupCheck.php', 'PhabricatorAuthStartController' => 'applications/auth/controller/PhabricatorAuthStartController.php', @@ -2126,6 +2155,7 @@ phutil_register_library_map(array( 'PhabricatorAuthTemporaryToken' => 'applications/auth/storage/PhabricatorAuthTemporaryToken.php', 'PhabricatorAuthTemporaryTokenGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthTemporaryTokenGarbageCollector.php', 'PhabricatorAuthTemporaryTokenQuery' => 'applications/auth/query/PhabricatorAuthTemporaryTokenQuery.php', + 'PhabricatorAuthTemporaryTokenRevoker' => 'applications/auth/revoker/PhabricatorAuthTemporaryTokenRevoker.php', 'PhabricatorAuthTemporaryTokenType' => 'applications/auth/tokentype/PhabricatorAuthTemporaryTokenType.php', 'PhabricatorAuthTemporaryTokenTypeModule' => 'applications/auth/tokentype/PhabricatorAuthTemporaryTokenTypeModule.php', 'PhabricatorAuthTerminateSessionController' => 'applications/auth/controller/PhabricatorAuthTerminateSessionController.php', @@ -2198,6 +2228,11 @@ phutil_register_library_map(array( 'PhabricatorBuiltinFileCachePurger' => 'applications/cache/purger/PhabricatorBuiltinFileCachePurger.php', 'PhabricatorBuiltinPatchList' => 'infrastructure/storage/patch/PhabricatorBuiltinPatchList.php', 'PhabricatorBulkContentSource' => 'infrastructure/daemon/contentsource/PhabricatorBulkContentSource.php', + 'PhabricatorBulkEditGroup' => 'applications/transactions/bulk/PhabricatorBulkEditGroup.php', + 'PhabricatorBulkEngine' => 'applications/transactions/bulk/PhabricatorBulkEngine.php', + 'PhabricatorBulkManagementMakeSilentWorkflow' => 'applications/transactions/bulk/management/PhabricatorBulkManagementMakeSilentWorkflow.php', + 'PhabricatorBulkManagementWorkflow' => 'applications/transactions/bulk/management/PhabricatorBulkManagementWorkflow.php', + 'PhabricatorCSVExportFormat' => 'infrastructure/export/PhabricatorCSVExportFormat.php', 'PhabricatorCacheDAO' => 'applications/cache/storage/PhabricatorCacheDAO.php', 'PhabricatorCacheEngine' => 'applications/system/engine/PhabricatorCacheEngine.php', 'PhabricatorCacheEngineExtension' => 'applications/system/engine/PhabricatorCacheEngineExtension.php', @@ -2727,6 +2762,7 @@ phutil_register_library_map(array( 'PhabricatorEdgesDestructionEngineExtension' => 'infrastructure/edges/engineextension/PhabricatorEdgesDestructionEngineExtension.php', 'PhabricatorEditEngine' => 'applications/transactions/editengine/PhabricatorEditEngine.php', 'PhabricatorEditEngineAPIMethod' => 'applications/transactions/editengine/PhabricatorEditEngineAPIMethod.php', + 'PhabricatorEditEngineBulkJobType' => 'applications/transactions/bulk/PhabricatorEditEngineBulkJobType.php', 'PhabricatorEditEngineCheckboxesCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineCheckboxesCommentAction.php', 'PhabricatorEditEngineColumnsCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineColumnsCommentAction.php', 'PhabricatorEditEngineCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineCommentAction.php', @@ -2802,12 +2838,15 @@ phutil_register_library_map(array( 'PhabricatorEnv' => 'infrastructure/env/PhabricatorEnv.php', 'PhabricatorEnvTestCase' => 'infrastructure/env/__tests__/PhabricatorEnvTestCase.php', 'PhabricatorEpochEditField' => 'applications/transactions/editfield/PhabricatorEpochEditField.php', + 'PhabricatorEpochExportField' => 'infrastructure/export/PhabricatorEpochExportField.php', 'PhabricatorEvent' => 'infrastructure/events/PhabricatorEvent.php', 'PhabricatorEventEngine' => 'infrastructure/events/PhabricatorEventEngine.php', 'PhabricatorEventListener' => 'infrastructure/events/PhabricatorEventListener.php', 'PhabricatorEventType' => 'infrastructure/events/constant/PhabricatorEventType.php', 'PhabricatorExampleEventListener' => 'infrastructure/events/PhabricatorExampleEventListener.php', 'PhabricatorExecFutureFileUploadSource' => 'applications/files/uploadsource/PhabricatorExecFutureFileUploadSource.php', + 'PhabricatorExportField' => 'infrastructure/export/PhabricatorExportField.php', + 'PhabricatorExportFormat' => 'infrastructure/export/PhabricatorExportFormat.php', 'PhabricatorExtendedPolicyInterface' => 'applications/policy/interface/PhabricatorExtendedPolicyInterface.php', 'PhabricatorExtendingPhabricatorConfigOptions' => 'applications/config/option/PhabricatorExtendingPhabricatorConfigOptions.php', 'PhabricatorExtensionsSetupCheck' => 'applications/config/check/PhabricatorExtensionsSetupCheck.php', @@ -2927,6 +2966,7 @@ phutil_register_library_map(array( 'PhabricatorFileUploadDialogController' => 'applications/files/controller/PhabricatorFileUploadDialogController.php', 'PhabricatorFileUploadException' => 'applications/files/exception/PhabricatorFileUploadException.php', 'PhabricatorFileUploadSource' => 'applications/files/uploadsource/PhabricatorFileUploadSource.php', + 'PhabricatorFileUploadSourceByteLimitException' => 'applications/files/uploadsource/PhabricatorFileUploadSourceByteLimitException.php', 'PhabricatorFileinfoSetupCheck' => 'applications/config/check/PhabricatorFileinfoSetupCheck.php', 'PhabricatorFilesApplication' => 'applications/files/application/PhabricatorFilesApplication.php', 'PhabricatorFilesApplicationStorageEnginePanel' => 'applications/files/applicationpanel/PhabricatorFilesApplicationStorageEnginePanel.php', @@ -3026,6 +3066,7 @@ phutil_register_library_map(array( 'PhabricatorHomeProfileMenuItem' => 'applications/home/menuitem/PhabricatorHomeProfileMenuItem.php', 'PhabricatorHovercardEngineExtension' => 'applications/search/engineextension/PhabricatorHovercardEngineExtension.php', 'PhabricatorHovercardEngineExtensionModule' => 'applications/search/engineextension/PhabricatorHovercardEngineExtensionModule.php', + 'PhabricatorIDExportField' => 'infrastructure/export/PhabricatorIDExportField.php', 'PhabricatorIDsSearchEngineExtension' => 'applications/search/engineextension/PhabricatorIDsSearchEngineExtension.php', 'PhabricatorIDsSearchField' => 'applications/search/field/PhabricatorIDsSearchField.php', 'PhabricatorIconDatasource' => 'applications/files/typeahead/PhabricatorIconDatasource.php', @@ -3049,6 +3090,7 @@ phutil_register_library_map(array( 'PhabricatorInlineSummaryView' => 'infrastructure/diff/view/PhabricatorInlineSummaryView.php', 'PhabricatorInstructionsEditField' => 'applications/transactions/editfield/PhabricatorInstructionsEditField.php', 'PhabricatorIntConfigType' => 'applications/config/type/PhabricatorIntConfigType.php', + 'PhabricatorIntExportField' => 'infrastructure/export/PhabricatorIntExportField.php', 'PhabricatorInternalSetting' => 'applications/settings/setting/PhabricatorInternalSetting.php', 'PhabricatorInternationalizationManagementExtractWorkflow' => 'infrastructure/internationalization/management/PhabricatorInternationalizationManagementExtractWorkflow.php', 'PhabricatorInternationalizationManagementWorkflow' => 'infrastructure/internationalization/management/PhabricatorInternationalizationManagementWorkflow.php', @@ -3058,6 +3100,7 @@ phutil_register_library_map(array( 'PhabricatorIteratorFileUploadSource' => 'applications/files/uploadsource/PhabricatorIteratorFileUploadSource.php', 'PhabricatorJIRAAuthProvider' => 'applications/auth/provider/PhabricatorJIRAAuthProvider.php', 'PhabricatorJSONConfigType' => 'applications/config/type/PhabricatorJSONConfigType.php', + 'PhabricatorJSONExportFormat' => 'infrastructure/export/PhabricatorJSONExportFormat.php', 'PhabricatorJavelinLinter' => 'infrastructure/lint/linter/PhabricatorJavelinLinter.php', 'PhabricatorJiraIssueHasObjectEdgeType' => 'applications/doorkeeper/edge/PhabricatorJiraIssueHasObjectEdgeType.php', 'PhabricatorJumpNavHandler' => 'applications/search/engine/PhabricatorJumpNavHandler.php', @@ -3377,6 +3420,7 @@ phutil_register_library_map(array( 'PhabricatorPHDConfigOptions' => 'applications/config/option/PhabricatorPHDConfigOptions.php', 'PhabricatorPHID' => 'applications/phid/storage/PhabricatorPHID.php', 'PhabricatorPHIDConstants' => 'applications/phid/PhabricatorPHIDConstants.php', + 'PhabricatorPHIDExportField' => 'infrastructure/export/PhabricatorPHIDExportField.php', 'PhabricatorPHIDInterface' => 'applications/phid/interface/PhabricatorPHIDInterface.php', 'PhabricatorPHIDListEditField' => 'applications/transactions/editfield/PhabricatorPHIDListEditField.php', 'PhabricatorPHIDListEditType' => 'applications/transactions/edittype/PhabricatorPHIDListEditType.php', @@ -3465,6 +3509,7 @@ phutil_register_library_map(array( 'PhabricatorPagerUIExample' => 'applications/uiexample/examples/PhabricatorPagerUIExample.php', 'PhabricatorPassphraseApplication' => 'applications/passphrase/application/PhabricatorPassphraseApplication.php', 'PhabricatorPasswordAuthProvider' => 'applications/auth/provider/PhabricatorPasswordAuthProvider.php', + 'PhabricatorPasswordDestructionEngineExtension' => 'applications/auth/extension/PhabricatorPasswordDestructionEngineExtension.php', 'PhabricatorPasswordHasher' => 'infrastructure/util/password/PhabricatorPasswordHasher.php', 'PhabricatorPasswordHasherTestCase' => 'infrastructure/util/password/__tests__/PhabricatorPasswordHasherTestCase.php', 'PhabricatorPasswordHasherUnavailableException' => 'infrastructure/util/password/PhabricatorPasswordHasherUnavailableException.php', @@ -3915,7 +3960,6 @@ phutil_register_library_map(array( 'PhabricatorRepositoryURITestCase' => 'applications/repository/storage/__tests__/PhabricatorRepositoryURITestCase.php', 'PhabricatorRepositoryURITransaction' => 'applications/repository/storage/PhabricatorRepositoryURITransaction.php', 'PhabricatorRepositoryURITransactionQuery' => 'applications/repository/query/PhabricatorRepositoryURITransactionQuery.php', - 'PhabricatorRepositoryVCSPassword' => 'applications/repository/storage/PhabricatorRepositoryVCSPassword.php', 'PhabricatorRepositoryWorkingCopyVersion' => 'applications/repository/storage/PhabricatorRepositoryWorkingCopyVersion.php', 'PhabricatorRequestExceptionHandler' => 'aphront/handler/PhabricatorRequestExceptionHandler.php', 'PhabricatorResourceSite' => 'aphront/site/PhabricatorResourceSite.php', @@ -4142,6 +4186,7 @@ phutil_register_library_map(array( 'PhabricatorStorageSchemaSpec' => 'infrastructure/storage/schema/PhabricatorStorageSchemaSpec.php', 'PhabricatorStorageSetupCheck' => 'applications/config/check/PhabricatorStorageSetupCheck.php', 'PhabricatorStringConfigType' => 'applications/config/type/PhabricatorStringConfigType.php', + 'PhabricatorStringExportField' => 'infrastructure/export/PhabricatorStringExportField.php', 'PhabricatorStringListConfigType' => 'applications/config/type/PhabricatorStringListConfigType.php', 'PhabricatorStringListEditField' => 'applications/transactions/editfield/PhabricatorStringListEditField.php', 'PhabricatorStringSetting' => 'applications/settings/setting/PhabricatorStringSetting.php', @@ -4205,6 +4250,7 @@ phutil_register_library_map(array( 'PhabricatorTextAreaEditField' => 'applications/transactions/editfield/PhabricatorTextAreaEditField.php', 'PhabricatorTextConfigType' => 'applications/config/type/PhabricatorTextConfigType.php', 'PhabricatorTextEditField' => 'applications/transactions/editfield/PhabricatorTextEditField.php', + 'PhabricatorTextExportFormat' => 'infrastructure/export/PhabricatorTextExportFormat.php', 'PhabricatorTextListConfigType' => 'applications/config/type/PhabricatorTextListConfigType.php', 'PhabricatorTime' => 'infrastructure/time/PhabricatorTime.php', 'PhabricatorTimeFormatSetting' => 'applications/settings/setting/PhabricatorTimeFormatSetting.php', @@ -5239,6 +5285,12 @@ phutil_register_library_map(array( 'AuditConduitAPIMethod' => 'ConduitAPIMethod', 'AuditQueryConduitAPIMethod' => 'AuditConduitAPIMethod', 'AuthManageProvidersCapability' => 'PhabricatorPolicyCapability', + 'BulkParameterType' => 'Phobject', + 'BulkPointsParameterType' => 'BulkParameterType', + 'BulkRemarkupParameterType' => 'BulkParameterType', + 'BulkSelectParameterType' => 'BulkParameterType', + 'BulkStringParameterType' => 'BulkParameterType', + 'BulkTokenizerParameterType' => 'BulkParameterType', 'CalendarTimeUtil' => 'Phobject', 'CalendarTimeUtilTestCase' => 'PhabricatorTestCase', 'CelerityAPI' => 'Phobject', @@ -5650,6 +5702,7 @@ phutil_register_library_map(array( 'DifferentialRevisionStatus' => 'Phobject', 'DifferentialRevisionStatusDatasource' => 'PhabricatorTypeaheadDatasource', 'DifferentialRevisionStatusFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', + 'DifferentialRevisionStatusHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionStatusTransaction' => 'DifferentialRevisionTransactionType', 'DifferentialRevisionSummaryHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionSummaryTransaction' => 'DifferentialRevisionTransactionType', @@ -5827,6 +5880,7 @@ phutil_register_library_map(array( 'DiffusionLintCountQuery' => 'PhabricatorQuery', 'DiffusionLintSaveRunner' => 'Phobject', 'DiffusionLocalRepositoryFilter' => 'Phobject', + 'DiffusionLogController' => 'DiffusionController', 'DiffusionLookSoonConduitAPIMethod' => 'DiffusionConduitAPIMethod', 'DiffusionLowLevelCommitFieldsQuery' => 'DiffusionLowLevelQuery', 'DiffusionLowLevelCommitQuery' => 'DiffusionLowLevelQuery', @@ -5894,10 +5948,12 @@ phutil_register_library_map(array( 'DiffusionPreCommitRefTypeHeraldField' => 'DiffusionPreCommitRefHeraldField', 'DiffusionPreCommitUsesGitLFSHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPullEventGarbageCollector' => 'PhabricatorGarbageCollector', + 'DiffusionPullLogListController' => 'DiffusionLogController', + 'DiffusionPullLogListView' => 'AphrontView', + 'DiffusionPullLogSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DiffusionPushCapability' => 'PhabricatorPolicyCapability', - 'DiffusionPushEventViewController' => 'DiffusionPushLogController', - 'DiffusionPushLogController' => 'DiffusionController', - 'DiffusionPushLogListController' => 'DiffusionPushLogController', + 'DiffusionPushEventViewController' => 'DiffusionLogController', + 'DiffusionPushLogListController' => 'DiffusionLogController', 'DiffusionPushLogListView' => 'AphrontView', 'DiffusionPythonExternalSymbolsSource' => 'DiffusionExternalSymbolsSource', 'DiffusionQuery' => 'PhabricatorQuery', @@ -6516,7 +6572,9 @@ phutil_register_library_map(array( 'HeraldConditionTranscript' => 'Phobject', 'HeraldContentSourceField' => 'HeraldField', 'HeraldController' => 'PhabricatorController', + 'HeraldCoreStateReasons' => 'HeraldStateReasons', 'HeraldDAO' => 'PhabricatorLiskDAO', + 'HeraldDeprecatedFieldGroup' => 'HeraldFieldGroup', 'HeraldDifferentialAdapter' => 'HeraldAdapter', 'HeraldDifferentialDiffAdapter' => 'HeraldDifferentialAdapter', 'HeraldDifferentialRevisionAdapter' => array( @@ -6557,7 +6615,6 @@ phutil_register_library_map(array( 'HeraldRelatedFieldGroup' => 'HeraldFieldGroup', 'HeraldRemarkupFieldValue' => 'HeraldFieldValue', 'HeraldRemarkupRule' => 'PhabricatorObjectRemarkupRule', - 'HeraldRepetitionPolicyConfig' => 'Phobject', 'HeraldRule' => array( 'HeraldDAO', 'PhabricatorApplicationTransactionInterface', @@ -6678,8 +6735,8 @@ phutil_register_library_map(array( 'MacroQueryConduitAPIMethod' => 'MacroConduitAPIMethod', 'ManiphestAssignEmailCommand' => 'ManiphestEmailCommand', 'ManiphestAssigneeDatasource' => 'PhabricatorTypeaheadCompositeDatasource', - 'ManiphestBatchEditController' => 'ManiphestController', 'ManiphestBulkEditCapability' => 'PhabricatorPolicyCapability', + 'ManiphestBulkEditController' => 'ManiphestController', 'ManiphestClaimEmailCommand' => 'ManiphestEmailCommand', 'ManiphestCloseEmailCommand' => 'ManiphestEmailCommand', 'ManiphestConduitAPIMethod' => 'ConduitAPIMethod', @@ -6761,6 +6818,7 @@ phutil_register_library_map(array( 'ManiphestTaskAttachTransaction' => 'ManiphestTaskTransactionType', 'ManiphestTaskAuthorHeraldField' => 'ManiphestTaskHeraldField', 'ManiphestTaskAuthorPolicyRule' => 'PhabricatorPolicyRule', + 'ManiphestTaskBulkEngine' => 'PhabricatorBulkEngine', 'ManiphestTaskCloseAsDuplicateRelationship' => 'ManiphestTaskRelationship', 'ManiphestTaskClosedStatusDatasource' => 'PhabricatorTypeaheadDatasource', 'ManiphestTaskCoverImageTransaction' => 'ManiphestTaskTransactionType', @@ -6770,7 +6828,6 @@ phutil_register_library_map(array( 'ManiphestTaskDescriptionTransaction' => 'ManiphestTaskTransactionType', 'ManiphestTaskDetailController' => 'ManiphestController', 'ManiphestTaskEdgeTransaction' => 'ManiphestTaskTransactionType', - 'ManiphestTaskEditBulkJobType' => 'PhabricatorWorkerBulkJobType', 'ManiphestTaskEditController' => 'ManiphestController', 'ManiphestTaskEditEngineLock' => 'PhabricatorEditEngineLock', 'ManiphestTaskFerretEngine' => 'PhabricatorFerretEngine', @@ -7284,6 +7341,7 @@ phutil_register_library_map(array( 'PhabricatorAuthApplication' => 'PhabricatorApplication', 'PhabricatorAuthAuthFactorPHIDType' => 'PhabricatorPHIDType', 'PhabricatorAuthAuthProviderPHIDType' => 'PhabricatorPHIDType', + 'PhabricatorAuthChangePasswordAction' => 'PhabricatorSystemAction', 'PhabricatorAuthConduitAPIMethod' => 'ConduitAPIMethod', 'PhabricatorAuthConduitTokenRevoker' => 'PhabricatorAuthRevoker', 'PhabricatorAuthConfirmLinkController' => 'PhabricatorAuthController', @@ -7343,7 +7401,25 @@ phutil_register_library_map(array( 'PhabricatorAuthOldOAuthRedirectController' => 'PhabricatorAuthController', 'PhabricatorAuthOneTimeLoginController' => 'PhabricatorAuthController', 'PhabricatorAuthOneTimeLoginTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType', + 'PhabricatorAuthPassword' => array( + 'PhabricatorAuthDAO', + 'PhabricatorPolicyInterface', + 'PhabricatorDestructibleInterface', + 'PhabricatorApplicationTransactionInterface', + ), + 'PhabricatorAuthPasswordEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhabricatorAuthPasswordEngine' => 'Phobject', + 'PhabricatorAuthPasswordException' => 'Exception', + 'PhabricatorAuthPasswordPHIDType' => 'PhabricatorPHIDType', + 'PhabricatorAuthPasswordQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorAuthPasswordResetTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType', + 'PhabricatorAuthPasswordRevokeTransaction' => 'PhabricatorAuthPasswordTransactionType', + 'PhabricatorAuthPasswordRevoker' => 'PhabricatorAuthRevoker', + 'PhabricatorAuthPasswordTestCase' => 'PhabricatorTestCase', + 'PhabricatorAuthPasswordTransaction' => 'PhabricatorModularTransaction', + 'PhabricatorAuthPasswordTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhabricatorAuthPasswordTransactionType' => 'PhabricatorModularTransactionType', + 'PhabricatorAuthPasswordUpgradeTransaction' => 'PhabricatorAuthPasswordTransactionType', 'PhabricatorAuthProvider' => 'Phobject', 'PhabricatorAuthProviderConfig' => array( 'PhabricatorAuthDAO', @@ -7368,7 +7444,6 @@ phutil_register_library_map(array( 'PhabricatorApplicationTransactionInterface', ), 'PhabricatorAuthSSHKeyController' => 'PhabricatorAuthController', - 'PhabricatorAuthSSHKeyDeactivateController' => 'PhabricatorAuthSSHKeyController', 'PhabricatorAuthSSHKeyEditController' => 'PhabricatorAuthSSHKeyController', 'PhabricatorAuthSSHKeyEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorAuthSSHKeyGenerateController' => 'PhabricatorAuthSSHKeyController', @@ -7376,12 +7451,15 @@ phutil_register_library_map(array( 'PhabricatorAuthSSHKeyPHIDType' => 'PhabricatorPHIDType', 'PhabricatorAuthSSHKeyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorAuthSSHKeyReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', + 'PhabricatorAuthSSHKeyRevokeController' => 'PhabricatorAuthSSHKeyController', 'PhabricatorAuthSSHKeySearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorAuthSSHKeyTableView' => 'AphrontView', + 'PhabricatorAuthSSHKeyTestCase' => 'PhabricatorTestCase', 'PhabricatorAuthSSHKeyTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorAuthSSHKeyTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorAuthSSHKeyViewController' => 'PhabricatorAuthSSHKeyController', 'PhabricatorAuthSSHPublicKey' => 'Phobject', + 'PhabricatorAuthSSHRevoker' => 'PhabricatorAuthRevoker', 'PhabricatorAuthSession' => array( 'PhabricatorAuthDAO', 'PhabricatorPolicyInterface', @@ -7392,6 +7470,7 @@ phutil_register_library_map(array( 'PhabricatorAuthSessionGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorAuthSessionInfo' => 'Phobject', 'PhabricatorAuthSessionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorAuthSessionRevoker' => 'PhabricatorAuthRevoker', 'PhabricatorAuthSetPasswordController' => 'PhabricatorAuthController', 'PhabricatorAuthSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorAuthStartController' => 'PhabricatorAuthController', @@ -7402,6 +7481,7 @@ phutil_register_library_map(array( ), 'PhabricatorAuthTemporaryTokenGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorAuthTemporaryTokenQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorAuthTemporaryTokenRevoker' => 'PhabricatorAuthRevoker', 'PhabricatorAuthTemporaryTokenType' => 'Phobject', 'PhabricatorAuthTemporaryTokenTypeModule' => 'PhabricatorConfigModule', 'PhabricatorAuthTerminateSessionController' => 'PhabricatorAuthController', @@ -7487,6 +7567,11 @@ phutil_register_library_map(array( 'PhabricatorBuiltinFileCachePurger' => 'PhabricatorCachePurger', 'PhabricatorBuiltinPatchList' => 'PhabricatorSQLPatchList', 'PhabricatorBulkContentSource' => 'PhabricatorContentSource', + 'PhabricatorBulkEditGroup' => 'Phobject', + 'PhabricatorBulkEngine' => 'Phobject', + 'PhabricatorBulkManagementMakeSilentWorkflow' => 'PhabricatorBulkManagementWorkflow', + 'PhabricatorBulkManagementWorkflow' => 'PhabricatorManagementWorkflow', + 'PhabricatorCSVExportFormat' => 'PhabricatorExportFormat', 'PhabricatorCacheDAO' => 'PhabricatorLiskDAO', 'PhabricatorCacheEngine' => 'Phobject', 'PhabricatorCacheEngineExtension' => 'Phobject', @@ -8106,6 +8191,7 @@ phutil_register_library_map(array( 'PhabricatorPolicyInterface', ), 'PhabricatorEditEngineAPIMethod' => 'ConduitAPIMethod', + 'PhabricatorEditEngineBulkJobType' => 'PhabricatorWorkerBulkJobType', 'PhabricatorEditEngineCheckboxesCommentAction' => 'PhabricatorEditEngineCommentAction', 'PhabricatorEditEngineColumnsCommentAction' => 'PhabricatorEditEngineCommentAction', 'PhabricatorEditEngineCommentAction' => 'Phobject', @@ -8182,12 +8268,15 @@ phutil_register_library_map(array( 'PhabricatorEnv' => 'Phobject', 'PhabricatorEnvTestCase' => 'PhabricatorTestCase', 'PhabricatorEpochEditField' => 'PhabricatorEditField', + 'PhabricatorEpochExportField' => 'PhabricatorExportField', 'PhabricatorEvent' => 'PhutilEvent', 'PhabricatorEventEngine' => 'Phobject', 'PhabricatorEventListener' => 'PhutilEventListener', 'PhabricatorEventType' => 'PhutilEventType', 'PhabricatorExampleEventListener' => 'PhabricatorEventListener', 'PhabricatorExecFutureFileUploadSource' => 'PhabricatorFileUploadSource', + 'PhabricatorExportField' => 'Phobject', + 'PhabricatorExportFormat' => 'Phobject', 'PhabricatorExtendingPhabricatorConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorExtensionsSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorExternalAccount' => array( @@ -8340,6 +8429,7 @@ phutil_register_library_map(array( 'PhabricatorFileUploadDialogController' => 'PhabricatorFileController', 'PhabricatorFileUploadException' => 'Exception', 'PhabricatorFileUploadSource' => 'Phobject', + 'PhabricatorFileUploadSourceByteLimitException' => 'Exception', 'PhabricatorFileinfoSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorFilesApplication' => 'PhabricatorApplication', 'PhabricatorFilesApplicationStorageEnginePanel' => 'PhabricatorApplicationConfigurationPanel', @@ -8447,6 +8537,7 @@ phutil_register_library_map(array( 'PhabricatorHomeProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorHovercardEngineExtension' => 'Phobject', 'PhabricatorHovercardEngineExtensionModule' => 'PhabricatorConfigModule', + 'PhabricatorIDExportField' => 'PhabricatorExportField', 'PhabricatorIDsSearchEngineExtension' => 'PhabricatorSearchEngineExtension', 'PhabricatorIDsSearchField' => 'PhabricatorSearchField', 'PhabricatorIconDatasource' => 'PhabricatorTypeaheadDatasource', @@ -8469,6 +8560,7 @@ phutil_register_library_map(array( 'PhabricatorInlineSummaryView' => 'AphrontView', 'PhabricatorInstructionsEditField' => 'PhabricatorEditField', 'PhabricatorIntConfigType' => 'PhabricatorTextConfigType', + 'PhabricatorIntExportField' => 'PhabricatorExportField', 'PhabricatorInternalSetting' => 'PhabricatorSetting', 'PhabricatorInternationalizationManagementExtractWorkflow' => 'PhabricatorInternationalizationManagementWorkflow', 'PhabricatorInternationalizationManagementWorkflow' => 'PhabricatorManagementWorkflow', @@ -8478,6 +8570,7 @@ phutil_register_library_map(array( 'PhabricatorIteratorFileUploadSource' => 'PhabricatorFileUploadSource', 'PhabricatorJIRAAuthProvider' => 'PhabricatorOAuth1AuthProvider', 'PhabricatorJSONConfigType' => 'PhabricatorTextConfigType', + 'PhabricatorJSONExportFormat' => 'PhabricatorExportFormat', 'PhabricatorJavelinLinter' => 'ArcanistLinter', 'PhabricatorJiraIssueHasObjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorJumpNavHandler' => 'Phobject', @@ -8837,6 +8930,7 @@ phutil_register_library_map(array( 'PhabricatorPHDConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPHID' => 'Phobject', 'PhabricatorPHIDConstants' => 'Phobject', + 'PhabricatorPHIDExportField' => 'PhabricatorExportField', 'PhabricatorPHIDListEditField' => 'PhabricatorEditField', 'PhabricatorPHIDListEditType' => 'PhabricatorEditType', 'PhabricatorPHIDResolver' => 'Phobject', @@ -8952,6 +9046,7 @@ phutil_register_library_map(array( 'PhabricatorPagerUIExample' => 'PhabricatorUIExample', 'PhabricatorPassphraseApplication' => 'PhabricatorApplication', 'PhabricatorPasswordAuthProvider' => 'PhabricatorAuthProvider', + 'PhabricatorPasswordDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension', 'PhabricatorPasswordHasher' => 'Phobject', 'PhabricatorPasswordHasherTestCase' => 'PhabricatorTestCase', 'PhabricatorPasswordHasherUnavailableException' => 'Exception', @@ -9530,7 +9625,6 @@ phutil_register_library_map(array( 'PhabricatorRepositoryURITestCase' => 'PhabricatorTestCase', 'PhabricatorRepositoryURITransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorRepositoryURITransactionQuery' => 'PhabricatorApplicationTransactionQuery', - 'PhabricatorRepositoryVCSPassword' => 'PhabricatorRepositoryDAO', 'PhabricatorRepositoryWorkingCopyVersion' => 'PhabricatorRepositoryDAO', 'PhabricatorRequestExceptionHandler' => 'AphrontRequestExceptionHandler', 'PhabricatorResourceSite' => 'PhabricatorSite', @@ -9554,7 +9648,7 @@ phutil_register_library_map(array( 'PhabricatorSSHKeysSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorSSHLog' => 'Phobject', 'PhabricatorSSHPassthruCommand' => 'Phobject', - 'PhabricatorSSHWorkflow' => 'PhabricatorManagementWorkflow', + 'PhabricatorSSHWorkflow' => 'PhutilArgumentWorkflow', 'PhabricatorSavedQuery' => array( 'PhabricatorSearchDAO', 'PhabricatorPolicyInterface', @@ -9776,6 +9870,7 @@ phutil_register_library_map(array( 'PhabricatorStorageSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorStorageSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorStringConfigType' => 'PhabricatorTextConfigType', + 'PhabricatorStringExportField' => 'PhabricatorExportField', 'PhabricatorStringListConfigType' => 'PhabricatorTextListConfigType', 'PhabricatorStringListEditField' => 'PhabricatorEditField', 'PhabricatorStringSetting' => 'PhabricatorSetting', @@ -9838,6 +9933,7 @@ phutil_register_library_map(array( 'PhabricatorTextAreaEditField' => 'PhabricatorEditField', 'PhabricatorTextConfigType' => 'PhabricatorConfigType', 'PhabricatorTextEditField' => 'PhabricatorEditField', + 'PhabricatorTextExportFormat' => 'PhabricatorExportFormat', 'PhabricatorTextListConfigType' => 'PhabricatorTextConfigType', 'PhabricatorTime' => 'Phobject', 'PhabricatorTimeFormatSetting' => 'PhabricatorSelectSetting', @@ -9931,6 +10027,7 @@ phutil_register_library_map(array( 'PhabricatorFulltextInterface', 'PhabricatorFerretInterface', 'PhabricatorConduitResultInterface', + 'PhabricatorAuthPasswordHashInterface', ), 'PhabricatorUserBadgesCacheType' => 'PhabricatorUserCacheType', 'PhabricatorUserBlurbField' => 'PhabricatorUserCustomField', diff --git a/src/applications/auth/__tests__/PhabricatorAuthPasswordTestCase.php b/src/applications/auth/__tests__/PhabricatorAuthPasswordTestCase.php new file mode 100644 index 0000000000..6ac616b2a1 --- /dev/null +++ b/src/applications/auth/__tests__/PhabricatorAuthPasswordTestCase.php @@ -0,0 +1,208 @@ + true, + ); + } + + public function testCompare() { + $password1 = new PhutilOpaqueEnvelope('hunter2'); + $password2 = new PhutilOpaqueEnvelope('hunter3'); + + $user = $this->generateNewTestUser(); + $type = PhabricatorAuthPassword::PASSWORD_TYPE_TEST; + + $pass = PhabricatorAuthPassword::initializeNewPassword($user, $type) + ->setPassword($password1, $user) + ->save(); + + $this->assertTrue( + $pass->comparePassword($password1, $user), + pht('Good password should match.')); + + $this->assertFalse( + $pass->comparePassword($password2, $user), + pht('Bad password should not match.')); + } + + public function testPasswordEngine() { + $password1 = new PhutilOpaqueEnvelope('the quick'); + $password2 = new PhutilOpaqueEnvelope('brown fox'); + + $user = $this->generateNewTestUser(); + $test_type = PhabricatorAuthPassword::PASSWORD_TYPE_TEST; + $account_type = PhabricatorAuthPassword::PASSWORD_TYPE_ACCOUNT; + $content_source = $this->newContentSource(); + + $engine = id(new PhabricatorAuthPasswordEngine()) + ->setViewer($user) + ->setContentSource($content_source) + ->setPasswordType($test_type) + ->setObject($user); + + $account_engine = id(new PhabricatorAuthPasswordEngine()) + ->setViewer($user) + ->setContentSource($content_source) + ->setPasswordType($account_type) + ->setObject($user); + + // We haven't set any passwords yet, so both passwords should be + // invalid. + $this->assertFalse($engine->isValidPassword($password1)); + $this->assertFalse($engine->isValidPassword($password2)); + + $pass = PhabricatorAuthPassword::initializeNewPassword($user, $test_type) + ->setPassword($password1, $user) + ->save(); + + // The password should now be valid. + $this->assertTrue($engine->isValidPassword($password1)); + $this->assertFalse($engine->isValidPassword($password2)); + + // But, since the password is a "test" password, it should not be a valid + // "account" password. + $this->assertFalse($account_engine->isValidPassword($password1)); + $this->assertFalse($account_engine->isValidPassword($password2)); + + // Both passwords are unique for the "test" engine, since an active + // password of a given type doesn't collide with itself. + $this->assertTrue($engine->isUniquePassword($password1)); + $this->assertTrue($engine->isUniquePassword($password2)); + + // The "test" password is no longer unique for the "account" engine. + $this->assertFalse($account_engine->isUniquePassword($password1)); + $this->assertTrue($account_engine->isUniquePassword($password2)); + + $this->revokePassword($user, $pass); + + // Now that we've revoked the password, it should no longer be valid. + $this->assertFalse($engine->isValidPassword($password1)); + $this->assertFalse($engine->isValidPassword($password2)); + + // But it should be a revoked password. + $this->assertTrue($engine->isRevokedPassword($password1)); + $this->assertFalse($engine->isRevokedPassword($password2)); + + // It should be revoked for both roles: revoking a "test" password also + // prevents you from choosing it as a new "account" password. + $this->assertTrue($account_engine->isRevokedPassword($password1)); + $this->assertFalse($account_engine->isValidPassword($password2)); + + // The revoked password makes this password non-unique for all account + // types. + $this->assertFalse($engine->isUniquePassword($password1)); + $this->assertTrue($engine->isUniquePassword($password2)); + $this->assertFalse($account_engine->isUniquePassword($password1)); + $this->assertTrue($account_engine->isUniquePassword($password2)); + } + + public function testPasswordUpgrade() { + $weak_hasher = new PhabricatorIteratedMD5PasswordHasher(); + + // Make sure we have two different hashers, and that the second one is + // stronger than iterated MD5. The most common reason this would fail is + // if an install does not have bcrypt available. + $strong_hasher = PhabricatorPasswordHasher::getBestHasher(); + if ($strong_hasher->getStrength() <= $weak_hasher->getStrength()) { + $this->assertSkipped( + pht( + 'Multiple password hashers of different strengths are not '. + 'available, so hash upgrading can not be tested.')); + } + + $envelope = new PhutilOpaqueEnvelope('lunar1997'); + + $user = $this->generateNewTestUser(); + $type = PhabricatorAuthPassword::PASSWORD_TYPE_TEST; + $content_source = $this->newContentSource(); + + $engine = id(new PhabricatorAuthPasswordEngine()) + ->setViewer($user) + ->setContentSource($content_source) + ->setPasswordType($type) + ->setObject($user); + + $password = PhabricatorAuthPassword::initializeNewPassword($user, $type) + ->setPasswordWithHasher($envelope, $user, $weak_hasher) + ->save(); + + $weak_name = $weak_hasher->getHashName(); + $strong_name = $strong_hasher->getHashName(); + + // Since we explicitly used the weak hasher, the password should have + // been hashed with it. + $actual_hasher = $password->getHasher(); + $this->assertEqual($weak_name, $actual_hasher->getHashName()); + + $is_valid = $engine + ->setUpgradeHashers(false) + ->isValidPassword($envelope, $user); + $password->reload(); + + // Since we disabled hasher upgrading, the password should not have been + // rehashed. + $this->assertTrue($is_valid); + $actual_hasher = $password->getHasher(); + $this->assertEqual($weak_name, $actual_hasher->getHashName()); + + $is_valid = $engine + ->setUpgradeHashers(true) + ->isValidPassword($envelope, $user); + $password->reload(); + + // Now that we enabled hasher upgrading, the password should have been + // automatically rehashed into the stronger format. + $this->assertTrue($is_valid); + $actual_hasher = $password->getHasher(); + $this->assertEqual($strong_name, $actual_hasher->getHashName()); + + // We should also have an "upgrade" transaction in the transaction record + // now which records the two hasher names. + $xactions = id(new PhabricatorAuthPasswordTransactionQuery()) + ->setViewer($user) + ->withObjectPHIDs(array($password->getPHID())) + ->withTransactionTypes( + array( + PhabricatorAuthPasswordUpgradeTransaction::TRANSACTIONTYPE, + )) + ->execute(); + + $this->assertEqual(1, count($xactions)); + $xaction = head($xactions); + + $this->assertEqual($weak_name, $xaction->getOldValue()); + $this->assertEqual($strong_name, $xaction->getNewValue()); + + $is_valid = $engine + ->isValidPassword($envelope, $user); + + // Finally, the password should still be valid after all the dust has + // settled. + $this->assertTrue($is_valid); + } + + private function revokePassword( + PhabricatorUser $actor, + PhabricatorAuthPassword $password) { + + $content_source = $this->newContentSource(); + $revoke_type = PhabricatorAuthPasswordRevokeTransaction::TRANSACTIONTYPE; + + $xactions = array(); + + $xactions[] = $password->getApplicationTransactionTemplate() + ->setTransactionType($revoke_type) + ->setNewValue(true); + + $editor = $password->getApplicationTransactionEditor() + ->setActor($actor) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->setContentSource($content_source) + ->applyTransactions($password, $xactions); + } + +} diff --git a/src/applications/auth/__tests__/PhabricatorAuthSSHKeyTestCase.php b/src/applications/auth/__tests__/PhabricatorAuthSSHKeyTestCase.php new file mode 100644 index 0000000000..4b1e1b4c02 --- /dev/null +++ b/src/applications/auth/__tests__/PhabricatorAuthSSHKeyTestCase.php @@ -0,0 +1,78 @@ + true, + ); + } + + public function testRevokeSSHKey() { + $user = $this->generateNewTestUser(); + $raw_key = 'ssh-rsa hunter2'; + + $ssh_key = PhabricatorAuthSSHKey::initializeNewSSHKey($user, $user); + + // Add the key to the user's account. + $xactions = array(); + $xactions[] = $ssh_key->getApplicationTransactionTemplate() + ->setTransactionType(PhabricatorAuthSSHKeyTransaction::TYPE_NAME) + ->setNewValue('key1'); + $xactions[] = $ssh_key->getApplicationTransactionTemplate() + ->setTransactionType(PhabricatorAuthSSHKeyTransaction::TYPE_KEY) + ->setNewValue($raw_key); + $this->applyTransactions($user, $ssh_key, $xactions); + + $ssh_key->reload(); + $this->assertTrue((bool)$ssh_key->getIsActive()); + + // Revoke it. + $xactions = array(); + $xactions[] = $ssh_key->getApplicationTransactionTemplate() + ->setTransactionType(PhabricatorAuthSSHKeyTransaction::TYPE_DEACTIVATE) + ->setNewValue(true); + $this->applyTransactions($user, $ssh_key, $xactions); + + $ssh_key->reload(); + $this->assertFalse((bool)$ssh_key->getIsActive()); + + // Try to add the revoked key back. This should fail with a validation + // error because the key was previously revoked by the user. + $revoked_key = PhabricatorAuthSSHKey::initializeNewSSHKey($user, $user); + $xactions = array(); + $xactions[] = $ssh_key->getApplicationTransactionTemplate() + ->setTransactionType(PhabricatorAuthSSHKeyTransaction::TYPE_NAME) + ->setNewValue('key2'); + $xactions[] = $ssh_key->getApplicationTransactionTemplate() + ->setTransactionType(PhabricatorAuthSSHKeyTransaction::TYPE_KEY) + ->setNewValue($raw_key); + + $caught = null; + try { + $this->applyTransactions($user, $ssh_key, $xactions); + } catch (PhabricatorApplicationTransactionValidationException $ex) { + $errors = $ex->getErrors(); + $this->assertEqual(1, count($errors)); + $caught = head($errors)->getType(); + } + + $this->assertEqual(PhabricatorAuthSSHKeyTransaction::TYPE_KEY, $caught); + } + + private function applyTransactions( + PhabricatorUser $actor, + PhabricatorAuthSSHKey $key, + array $xactions) { + + $content_source = $this->newContentSource(); + + $editor = $key->getApplicationTransactionEditor() + ->setActor($actor) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->setContentSource($content_source) + ->applyTransactions($key, $xactions); + } + +} diff --git a/src/applications/auth/action/PhabricatorAuthChangePasswordAction.php b/src/applications/auth/action/PhabricatorAuthChangePasswordAction.php new file mode 100644 index 0000000000..323c3e65b6 --- /dev/null +++ b/src/applications/auth/action/PhabricatorAuthChangePasswordAction.php @@ -0,0 +1,22 @@ + 'PhabricatorAuthSSHKeyGenerateController', 'upload/' => 'PhabricatorAuthSSHKeyEditController', 'edit/(?P\d+)/' => 'PhabricatorAuthSSHKeyEditController', - 'deactivate/(?P\d+)/' - => 'PhabricatorAuthSSHKeyDeactivateController', + 'revoke/(?P\d+)/' + => 'PhabricatorAuthSSHKeyRevokeController', 'view/(?P\d+)/' => 'PhabricatorAuthSSHKeyViewController', ), 'password/' => 'PhabricatorAuthSetPasswordController', diff --git a/src/applications/auth/controller/PhabricatorAuthRegisterController.php b/src/applications/auth/controller/PhabricatorAuthRegisterController.php index 5a792dc864..1138d52b33 100644 --- a/src/applications/auth/controller/PhabricatorAuthRegisterController.php +++ b/src/applications/auth/controller/PhabricatorAuthRegisterController.php @@ -61,6 +61,9 @@ final class PhabricatorAuthRegisterController $default_username = $account->getUsername(); $default_realname = $account->getRealName(); + $account_type = PhabricatorAuthPassword::PASSWORD_TYPE_ACCOUNT; + $content_source = PhabricatorContentSource::newFromRequest($request); + $default_email = $account->getEmail(); if ($invite) { @@ -285,27 +288,22 @@ final class PhabricatorAuthRegisterController if ($must_set_password) { $value_password = $request->getStr('password'); $value_confirm = $request->getStr('confirm'); - if (!strlen($value_password)) { - $e_password = pht('Required'); - $errors[] = pht('You must choose a password.'); - } else if ($value_password !== $value_confirm) { - $e_password = pht('No Match'); - $errors[] = pht('Password and confirmation must match.'); - } else if (strlen($value_password) < $min_len) { - $e_password = pht('Too Short'); - $errors[] = pht( - 'Password is too short (must be at least %d characters long).', - $min_len); - } else if ( - PhabricatorCommonPasswords::isCommonPassword($value_password)) { - $e_password = pht('Very Weak'); - $errors[] = pht( - 'Password is pathologically weak. This password is one of the '. - 'most common passwords in use, and is extremely easy for '. - 'attackers to guess. You must choose a stronger password.'); - } else { + $password_envelope = new PhutilOpaqueEnvelope($value_password); + $confirm_envelope = new PhutilOpaqueEnvelope($value_confirm); + + $engine = id(new PhabricatorAuthPasswordEngine()) + ->setViewer($user) + ->setContentSource($content_source) + ->setPasswordType($account_type) + ->setObject($user); + + try { + $engine->checkNewPassword($password_envelope, $confirm_envelope); $e_password = null; + } catch (PhabricatorAuthPasswordException $ex) { + $errors[] = $ex->getMessage(); + $e_password = $ex->getPasswordError(); } } @@ -408,8 +406,13 @@ final class PhabricatorAuthRegisterController $editor->createNewUser($user, $email_obj, $allow_reassign_email); if ($must_set_password) { - $envelope = new PhutilOpaqueEnvelope($value_password); - $editor->changePassword($user, $envelope); + $password_object = PhabricatorAuthPassword::initializeNewPassword( + $user, + $account_type); + + $password_object + ->setPassword($password_envelope, $user) + ->save(); } if ($is_setup) { diff --git a/src/applications/auth/controller/PhabricatorAuthSSHKeyDeactivateController.php b/src/applications/auth/controller/PhabricatorAuthSSHKeyRevokeController.php similarity index 86% rename from src/applications/auth/controller/PhabricatorAuthSSHKeyDeactivateController.php rename to src/applications/auth/controller/PhabricatorAuthSSHKeyRevokeController.php index 8eca02340d..0547c2175d 100644 --- a/src/applications/auth/controller/PhabricatorAuthSSHKeyDeactivateController.php +++ b/src/applications/auth/controller/PhabricatorAuthSSHKeyRevokeController.php @@ -1,6 +1,6 @@ getName()); return $this->newDialog() - ->setTitle(pht('Deactivate SSH Public Key')) + ->setTitle(pht('Revoke SSH Public Key')) ->appendParagraph( pht( - 'The key "%s" will be permanently deactivated, and you will no '. + 'The key "%s" will be permanently revoked, and you will no '. 'longer be able to use the corresponding private key to '. 'authenticate.', $name)) - ->addSubmitButton(pht('Deactivate Public Key')) + ->addSubmitButton(pht('Revoke Public Key')) ->addCancelButton($cancel_uri); } diff --git a/src/applications/auth/controller/PhabricatorAuthSSHKeyViewController.php b/src/applications/auth/controller/PhabricatorAuthSSHKeyViewController.php index 66c3a5f09b..54c6e3861b 100644 --- a/src/applications/auth/controller/PhabricatorAuthSSHKeyViewController.php +++ b/src/applications/auth/controller/PhabricatorAuthSSHKeyViewController.php @@ -35,7 +35,7 @@ final class PhabricatorAuthSSHKeyViewController if ($ssh_key->getIsActive()) { $header->setStatus('fa-check', 'bluegrey', pht('Active')); } else { - $header->setStatus('fa-ban', 'dark', pht('Deactivated')); + $header->setStatus('fa-ban', 'dark', pht('Revoked')); } $header->addActionLink( @@ -80,7 +80,7 @@ final class PhabricatorAuthSSHKeyViewController $id = $ssh_key->getID(); $edit_uri = $this->getApplicationURI("sshkey/edit/{$id}/"); - $deactivate_uri = $this->getApplicationURI("sshkey/deactivate/{$id}/"); + $revoke_uri = $this->getApplicationURI("sshkey/revoke/{$id}/"); $curtain = $this->newCurtainView($ssh_key); @@ -95,8 +95,8 @@ final class PhabricatorAuthSSHKeyViewController $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-times') - ->setName(pht('Deactivate SSH Key')) - ->setHref($deactivate_uri) + ->setName(pht('Revoke SSH Key')) + ->setHref($revoke_uri) ->setWorkflow(true) ->setDisabled(!$can_edit)); diff --git a/src/applications/auth/controller/PhabricatorAuthSetPasswordController.php b/src/applications/auth/controller/PhabricatorAuthSetPasswordController.php index 0db23e52a7..1218276043 100644 --- a/src/applications/auth/controller/PhabricatorAuthSetPasswordController.php +++ b/src/applications/auth/controller/PhabricatorAuthSetPasswordController.php @@ -40,8 +40,30 @@ final class PhabricatorAuthSetPasswordController return new Aphront404Response(); } - $min_len = PhabricatorEnv::getEnvConfig('account.minimum-password-length'); - $min_len = (int)$min_len; + $content_source = PhabricatorContentSource::newFromRequest($request); + $account_type = PhabricatorAuthPassword::PASSWORD_TYPE_ACCOUNT; + + $password_objects = id(new PhabricatorAuthPasswordQuery()) + ->setViewer($viewer) + ->withObjectPHIDs(array($viewer->getPHID())) + ->withPasswordTypes(array($account_type)) + ->withIsRevoked(false) + ->execute(); + if ($password_objects) { + $password_object = head($password_objects); + $has_password = true; + } else { + $password_object = PhabricatorAuthPassword::initializeNewPassword( + $viewer, + $account_type); + $has_password = false; + } + + $engine = id(new PhabricatorAuthPasswordEngine()) + ->setViewer($viewer) + ->setContentSource($content_source) + ->setPasswordType($account_type) + ->setObject($viewer); $e_password = true; $e_confirm = true; @@ -50,46 +72,23 @@ final class PhabricatorAuthSetPasswordController $password = $request->getStr('password'); $confirm = $request->getStr('confirm'); - $e_password = null; - $e_confirm = null; + $password_envelope = new PhutilOpaqueEnvelope($password); + $confirm_envelope = new PhutilOpaqueEnvelope($confirm); - if (!strlen($password)) { - $errors[] = pht('You must choose a password or skip this step.'); - $e_password = pht('Required'); - } else if (strlen($password) < $min_len) { - $errors[] = pht( - 'The selected password is too short. Passwords must be a minimum '. - 'of %s characters.', - new PhutilNumber($min_len)); - $e_password = pht('Too Short'); - } else if (!strlen($confirm)) { - $errors[] = pht('You must confirm the selecetd password.'); - $e_confirm = pht('Required'); - } else if ($password !== $confirm) { - $errors[] = pht('The password and confirmation do not match.'); - $e_password = pht('Invalid'); - $e_confirm = pht('Invalid'); - } else if (PhabricatorCommonPasswords::isCommonPassword($password)) { - $e_password = pht('Very Weak'); - $errors[] = pht( - 'The selected password is very weak: it is one of the most common '. - 'passwords in use. Choose a stronger password.'); + try { + $engine->checkNewPassword($password_envelope, $confirm_envelope, true); + $e_password = null; + $e_confirm = null; + } catch (PhabricatorAuthPasswordException $ex) { + $errors[] = $ex->getMessage(); + $e_password = $ex->getPasswordError(); + $e_confirm = $ex->getConfirmError(); } if (!$errors) { - $envelope = new PhutilOpaqueEnvelope($password); - - // This write is unguarded because the CSRF token has already - // been checked in the call to $request->isFormPost() and - // the CSRF token depends on the password hash, so when it - // is changed here the CSRF token check will fail. - $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); - - id(new PhabricatorUserEditor()) - ->setActor($viewer) - ->changePassword($viewer, $envelope); - - unset($unguarded); + $password_object + ->setPassword($password_envelope, $viewer) + ->save(); // Destroy the token. $auth_token->delete(); @@ -98,12 +97,15 @@ final class PhabricatorAuthSetPasswordController } } + $min_len = PhabricatorEnv::getEnvConfig('account.minimum-password-length'); + $min_len = (int)$min_len; + $len_caption = null; if ($min_len) { $len_caption = pht('Minimum password length: %d characters.', $min_len); } - if ($viewer->hasPassword()) { + if ($has_password) { $title = pht('Reset Password'); $crumb = pht('Reset Password'); $submit = pht('Reset Password'); diff --git a/src/applications/auth/editor/PhabricatorAuthPasswordEditor.php b/src/applications/auth/editor/PhabricatorAuthPasswordEditor.php new file mode 100644 index 0000000000..0867d22b87 --- /dev/null +++ b/src/applications/auth/editor/PhabricatorAuthPasswordEditor.php @@ -0,0 +1,33 @@ +oldHasher = $old_hasher; + return $this; + } + + public function getOldHasher() { + return $this->oldHasher; + } + + public function getEditorApplicationClass() { + return 'PhabricatorAuthApplication'; + } + + public function getEditorObjectsDescription() { + return pht('Passwords'); + } + + public function getCreateObjectTitle($author, $object) { + return pht('%s created this password.', $author); + } + + public function getCreateObjectTitleForFeed($author, $object) { + return pht('%s created %s.', $author, $object); + } + +} diff --git a/src/applications/auth/editor/PhabricatorAuthSSHKeyEditor.php b/src/applications/auth/editor/PhabricatorAuthSSHKeyEditor.php index 1c40af5795..569c37403b 100644 --- a/src/applications/auth/editor/PhabricatorAuthSSHKeyEditor.php +++ b/src/applications/auth/editor/PhabricatorAuthSSHKeyEditor.php @@ -3,6 +3,17 @@ final class PhabricatorAuthSSHKeyEditor extends PhabricatorApplicationTransactionEditor { + private $isAdministrativeEdit; + + public function setIsAdministrativeEdit($is_administrative_edit) { + $this->isAdministrativeEdit = $is_administrative_edit; + return $this; + } + + public function getIsAdministrativeEdit() { + return $this->isAdministrativeEdit; + } + public function getEditorApplicationClass() { return 'PhabricatorAuthApplication'; } @@ -93,6 +104,7 @@ final class PhabricatorAuthSSHKeyEditor array $xactions) { $errors = parent::validateTransaction($object, $type, $xactions); + $viewer = $this->requireActor(); switch ($type) { case PhabricatorAuthSSHKeyTransaction::TYPE_NAME: @@ -138,6 +150,30 @@ final class PhabricatorAuthSSHKeyEditor pht('Invalid'), $ex->getMessage(), $xaction); + continue; + } + + // The database does not have a unique key on just the + // column because we allow multiple accounts to revoke the same + // key, so we can't rely on database constraints to prevent users + // from adding keys that are on the revocation list back to their + // accounts. Explicitly check for a revoked copy of the key. + + $revoked_keys = id(new PhabricatorAuthSSHKeyQuery()) + ->setViewer($viewer) + ->withObjectPHIDs(array($object->getObjectPHID())) + ->withIsActive(0) + ->withKeys(array($public_key)) + ->execute(); + if ($revoked_keys) { + $errors[] = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Revoked'), + pht( + 'This key has been revoked. Choose or generate a new, '. + 'unique key.'), + $xaction); + continue; } } } @@ -239,11 +275,13 @@ final class PhabricatorAuthSSHKeyEditor $body = parent::buildMailBody($object, $xactions); - $body->addTextSection( - pht('SECURITY WARNING'), - pht( - 'If you do not recognize this change, it may indicate your account '. - 'has been compromised.')); + if (!$this->getIsAdministrativeEdit()) { + $body->addTextSection( + pht('SECURITY WARNING'), + pht( + 'If you do not recognize this change, it may indicate your account '. + 'has been compromised.')); + } $detail_uri = $object->getURI(); $detail_uri = PhabricatorEnv::getProductionURI($detail_uri); @@ -253,4 +291,17 @@ final class PhabricatorAuthSSHKeyEditor return $body; } + + protected function getCustomWorkerState() { + return array( + 'isAdministrativeEdit' => $this->isAdministrativeEdit, + ); + } + + protected function loadCustomWorkerState(array $state) { + $this->isAdministrativeEdit = idx($state, 'isAdministrativeEdit'); + return $this; + } + + } diff --git a/src/applications/auth/engine/PhabricatorAuthPasswordEngine.php b/src/applications/auth/engine/PhabricatorAuthPasswordEngine.php new file mode 100644 index 0000000000..c09aa88308 --- /dev/null +++ b/src/applications/auth/engine/PhabricatorAuthPasswordEngine.php @@ -0,0 +1,320 @@ +viewer = $viewer; + return $this; + } + + public function getViewer() { + return $this->viewer; + } + + public function setContentSource(PhabricatorContentSource $content_source) { + $this->contentSource = $content_source; + return $this; + } + + public function getContentSource() { + return $this->contentSource; + } + + public function setObject(PhabricatorAuthPasswordHashInterface $object) { + $this->object = $object; + return $this; + } + + public function getObject() { + return $this->object; + } + + public function setPasswordType($password_type) { + $this->passwordType = $password_type; + return $this; + } + + public function getPasswordType() { + return $this->passwordType; + } + + public function setUpgradeHashers($upgrade_hashers) { + $this->upgradeHashers = $upgrade_hashers; + return $this; + } + + public function getUpgradeHashers() { + return $this->upgradeHashers; + } + + public function checkNewPassword( + PhutilOpaqueEnvelope $password, + PhutilOpaqueEnvelope $confirm, + $can_skip = false) { + + $raw_password = $password->openEnvelope(); + + if (!strlen($raw_password)) { + if ($can_skip) { + throw new PhabricatorAuthPasswordException( + pht('You must choose a password or skip this step.'), + pht('Required')); + } else { + throw new PhabricatorAuthPasswordException( + pht('You must choose a password.'), + pht('Required')); + } + } + + $min_len = PhabricatorEnv::getEnvConfig('account.minimum-password-length'); + $min_len = (int)$min_len; + if ($min_len) { + if (strlen($raw_password) < $min_len) { + throw new PhabricatorAuthPasswordException( + pht( + 'The selected password is too short. Passwords must be a minimum '. + 'of %s characters long.', + new PhutilNumber($min_len)), + pht('Too Short')); + } + } + + $raw_confirm = $confirm->openEnvelope(); + + if (!strlen($raw_confirm)) { + throw new PhabricatorAuthPasswordException( + pht('You must confirm the selected password.'), + null, + pht('Required')); + } + + if ($raw_password !== $raw_confirm) { + throw new PhabricatorAuthPasswordException( + pht('The password and confirmation do not match.'), + pht('Invalid'), + pht('Invalid')); + } + + if (PhabricatorCommonPasswords::isCommonPassword($raw_password)) { + throw new PhabricatorAuthPasswordException( + pht( + 'The selected password is very weak: it is one of the most common '. + 'passwords in use. Choose a stronger password.'), + pht('Very Weak')); + } + + // If we're creating a brand new object (like registering a new user) + // and it does not have a PHID yet, it isn't possible for it to have any + // revoked passwords or colliding passwords either, so we can skip these + // checks. + + if ($this->getObject()->getPHID()) { + if ($this->isRevokedPassword($password)) { + throw new PhabricatorAuthPasswordException( + pht( + 'The password you entered has been revoked. You can not reuse '. + 'a password which has been revoked. Choose a new password.'), + pht('Revoked')); + } + + if (!$this->isUniquePassword($password)) { + throw new PhabricatorAuthPasswordException( + pht( + 'The password you entered is the same as another password '. + 'associated with your account. Each password must be unique.'), + pht('Not Unique')); + } + } + } + + public function isValidPassword(PhutilOpaqueEnvelope $envelope) { + $this->requireSetup(); + + $password_type = $this->getPasswordType(); + + $passwords = $this->newQuery() + ->withPasswordTypes(array($password_type)) + ->withIsRevoked(false) + ->execute(); + + $matches = $this->getMatches($envelope, $passwords); + if (!$matches) { + return false; + } + + if ($this->shouldUpgradeHashers()) { + $this->upgradeHashers($envelope, $matches); + } + + return true; + } + + public function isUniquePassword(PhutilOpaqueEnvelope $envelope) { + $this->requireSetup(); + + $password_type = $this->getPasswordType(); + + // To test that the password is unique, we're loading all active and + // revoked passwords for all roles for the given user, then throwing out + // the active passwords for the current role (so a password can't + // collide with itself). + + // Note that two different objects can have the same password (say, + // users @alice and @bailey). We're only preventing @alice from using + // the same password for everything. + + $passwords = $this->newQuery() + ->execute(); + + foreach ($passwords as $key => $password) { + $same_type = ($password->getPasswordType() === $password_type); + $is_active = !$password->getIsRevoked(); + + if ($same_type && $is_active) { + unset($passwords[$key]); + } + } + + $matches = $this->getMatches($envelope, $passwords); + + return !$matches; + } + + public function isRevokedPassword(PhutilOpaqueEnvelope $envelope) { + $this->requireSetup(); + + // To test if a password is revoked, we're loading all revoked passwords + // across all roles for the given user. If a password was revoked in one + // role, you can't reuse it in a different role. + + $passwords = $this->newQuery() + ->withIsRevoked(true) + ->execute(); + + $matches = $this->getMatches($envelope, $passwords); + + return (bool)$matches; + } + + private function requireSetup() { + if (!$this->getObject()) { + throw new PhutilInvalidStateException('setObject'); + } + + if (!$this->getPasswordType()) { + throw new PhutilInvalidStateException('setPasswordType'); + } + + if (!$this->getViewer()) { + throw new PhutilInvalidStateException('setViewer'); + } + + if ($this->shouldUpgradeHashers()) { + if (!$this->getContentSource()) { + throw new PhutilInvalidStateException('setContentSource'); + } + } + } + + private function shouldUpgradeHashers() { + if (!$this->getUpgradeHashers()) { + return false; + } + + if (PhabricatorEnv::isReadOnly()) { + // Don't try to upgrade hashers if we're in read-only mode, since we + // won't be able to write the new hash to the database. + return false; + } + + return true; + } + + private function newQuery() { + $viewer = $this->getViewer(); + $object = $this->getObject(); + $password_type = $this->getPasswordType(); + + return id(new PhabricatorAuthPasswordQuery()) + ->setViewer($viewer) + ->withObjectPHIDs(array($object->getPHID())); + } + + private function getMatches( + PhutilOpaqueEnvelope $envelope, + array $passwords) { + + $object = $this->getObject(); + + $matches = array(); + foreach ($passwords as $password) { + try { + $is_match = $password->comparePassword($envelope, $object); + } catch (PhabricatorPasswordHasherUnavailableException $ex) { + $is_match = false; + } + + if ($is_match) { + $matches[] = $password; + } + } + + return $matches; + } + + private function upgradeHashers( + PhutilOpaqueEnvelope $envelope, + array $passwords) { + + assert_instances_of($passwords, 'PhabricatorAuthPassword'); + + $need_upgrade = array(); + foreach ($passwords as $password) { + if (!$password->canUpgrade()) { + continue; + } + $need_upgrade[] = $password; + } + + if (!$need_upgrade) { + return; + } + + $upgrade_type = PhabricatorAuthPasswordUpgradeTransaction::TRANSACTIONTYPE; + $viewer = $this->getViewer(); + $content_source = $this->getContentSource(); + + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + foreach ($need_upgrade as $password) { + + // This does the actual upgrade. We then apply a transaction to make + // the upgrade more visible and auditable. + $old_hasher = $password->getHasher(); + $password->upgradePasswordHasher($envelope, $this->getObject()); + $new_hasher = $password->getHasher(); + + $xactions = array(); + + $xactions[] = $password->getApplicationTransactionTemplate() + ->setTransactionType($upgrade_type) + ->setNewValue($new_hasher->getHashName()); + + $editor = $password->getApplicationTransactionEditor() + ->setActor($viewer) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->setContentSource($content_source) + ->setOldHasher($old_hasher) + ->applyTransactions($password, $xactions); + } + unset($unguarded); + } + +} diff --git a/src/applications/auth/extension/PhabricatorPasswordDestructionEngineExtension.php b/src/applications/auth/extension/PhabricatorPasswordDestructionEngineExtension.php new file mode 100644 index 0000000000..7b28044b0b --- /dev/null +++ b/src/applications/auth/extension/PhabricatorPasswordDestructionEngineExtension.php @@ -0,0 +1,29 @@ +getViewer(); + $object_phid = $object->getPHID(); + + $passwords = id(new PhabricatorAuthPasswordQuery()) + ->setViewer($viewer) + ->withObjectPHIDs(array($object_phid)) + ->execute(); + + foreach ($passwords as $password) { + $engine->destroyObject($password); + } + } + +} diff --git a/src/applications/auth/management/PhabricatorAuthManagementRecoverWorkflow.php b/src/applications/auth/management/PhabricatorAuthManagementRecoverWorkflow.php index a31b108f08..9efd07571a 100644 --- a/src/applications/auth/management/PhabricatorAuthManagementRecoverWorkflow.php +++ b/src/applications/auth/management/PhabricatorAuthManagementRecoverWorkflow.php @@ -9,8 +9,8 @@ final class PhabricatorAuthManagementRecoverWorkflow ->setExamples('**recover** __username__') ->setSynopsis( pht( - 'Recover access to an administrative account if you have locked '. - 'yourself out of Phabricator.')) + 'Recover access to an account if you have locked yourself out '. + 'of Phabricator.')) ->setArguments( array( 'username' => array( @@ -21,23 +21,6 @@ final class PhabricatorAuthManagementRecoverWorkflow } public function execute(PhutilArgumentParser $args) { - - $can_recover = id(new PhabricatorPeopleQuery()) - ->setViewer($this->getViewer()) - ->withIsAdmin(true) - ->execute(); - if (!$can_recover) { - throw new PhutilArgumentUsageException( - pht( - 'This Phabricator installation has no recoverable administrator '. - 'accounts. You can use `%s` to create a new administrator '. - 'account or make an existing user an administrator.', - 'bin/accountadmin')); - } - $can_recover = mpull($can_recover, 'getUsername'); - sort($can_recover); - $can_recover = implode(', ', $can_recover); - $usernames = $args->getArg('username'); if (!$usernames) { throw new PhutilArgumentUsageException( @@ -57,18 +40,8 @@ final class PhabricatorAuthManagementRecoverWorkflow if (!$user) { throw new PhutilArgumentUsageException( pht( - 'No such user "%s". Recoverable administrator accounts are: %s.', - $username, - $can_recover)); - } - - if (!$user->getIsAdmin()) { - throw new PhutilArgumentUsageException( - pht( - 'You can only recover administrator accounts, but %s is not an '. - 'administrator. Recoverable administrator accounts are: %s.', - $username, - $can_recover)); + 'No such user "%s" to recover.', + $username)); } if (!$user->canEstablishWebSessions()) { diff --git a/src/applications/auth/management/PhabricatorAuthManagementRevokeWorkflow.php b/src/applications/auth/management/PhabricatorAuthManagementRevokeWorkflow.php index 758761b2ca..6d907e67d6 100644 --- a/src/applications/auth/management/PhabricatorAuthManagementRevokeWorkflow.php +++ b/src/applications/auth/management/PhabricatorAuthManagementRevokeWorkflow.php @@ -7,7 +7,8 @@ final class PhabricatorAuthManagementRevokeWorkflow $this ->setName('revoke') ->setExamples( - "**revoke** --type __type__ --from __user__\n". + "**revoke** --list\n". + "**revoke** --type __type__ --from __@user__\n". "**revoke** --everything --everywhere") ->setSynopsis( pht( @@ -16,15 +17,20 @@ final class PhabricatorAuthManagementRevokeWorkflow array( array( 'name' => 'from', - 'param' => 'user', + 'param' => 'object', 'help' => pht( - 'Revoke credentials for the specified user.'), + 'Revoke credentials for the specified object. To revoke '. + 'credentials for a user, use "@username".'), ), array( 'name' => 'type', 'param' => 'type', + 'help' => pht('Revoke credentials of the given type.'), + ), + array( + 'name' => 'list', 'help' => pht( - 'Revoke credentials of the given type.'), + 'List information about available credential revokers.'), ), array( 'name' => 'everything', @@ -34,21 +40,37 @@ final class PhabricatorAuthManagementRevokeWorkflow 'name' => 'everywhere', 'help' => pht('Revoke from all credential owners.'), ), + array( + 'name' => 'force', + 'help' => pht('Revoke credentials without prompting.'), + ), )); } public function execute(PhutilArgumentParser $args) { - $viewer = PhabricatorUser::getOmnipotentUser(); + $viewer = $this->getViewer(); $all_types = PhabricatorAuthRevoker::getAllRevokers(); + $is_force = $args->getArg('force'); + + // The "--list" flag is compatible with revoker selection flags like + // "--type" to filter the list, but not compatible with target selection + // flags like "--from". + $is_list = $args->getArg('list'); $type = $args->getArg('type'); $is_everything = $args->getArg('everything'); if (!strlen($type) && !$is_everything) { - throw new PhutilArgumentUsageException( - pht( - 'Specify the credential type to revoke with "--type" or specify '. - '"--everything".')); + if ($is_list) { + // By default, "bin/revoke --list" implies "--everything". + $types = $all_types; + } else { + throw new PhutilArgumentUsageException( + pht( + 'Specify the credential type to revoke with "--type" or specify '. + '"--everything". Use "--list" to list available credential '. + 'types.')); + } } else if (strlen($type) && $is_everything) { throw new PhutilArgumentUsageException( pht( @@ -70,6 +92,32 @@ final class PhabricatorAuthManagementRevokeWorkflow $is_everywhere = $args->getArg('everywhere'); $from = $args->getArg('from'); + + if ($is_list) { + if (strlen($from) || $is_everywhere) { + throw new PhutilArgumentUsageException( + pht( + 'You can not "--list" and revoke credentials (with "--from" or '. + '"--everywhere") in the same operation.')); + } + } + + if ($is_list) { + $last_key = last_key($types); + foreach ($types as $key => $type) { + echo tsprintf( + "**%s** (%s)\n\n", + $type->getRevokerKey(), + $type->getRevokerName()); + + id(new PhutilConsoleBlock()) + ->addParagraph(tsprintf('%B', $type->getRevokerDescription())) + ->draw(); + } + + return 0; + } + $target = null; if (!strlen($from) && !$is_everywhere) { throw new PhutilArgumentUsageException( @@ -97,7 +145,7 @@ final class PhabricatorAuthManagementRevokeWorkflow } } - if ($is_everywhere) { + if ($is_everywhere && !$is_force) { echo id(new PhutilConsoleBlock()) ->addParagraph( pht( @@ -128,6 +176,13 @@ final class PhabricatorAuthManagementRevokeWorkflow 'Destroyed %s credential(s) of type "%s".', new PhutilNumber($count), $type->getRevokerKey())); + + $guidance = $type->getRevokerNextSteps(); + if ($guidance !== null) { + echo tsprintf( + "%s\n", + $guidance); + } } echo tsprintf( diff --git a/src/applications/auth/password/PhabricatorAuthPasswordException.php b/src/applications/auth/password/PhabricatorAuthPasswordException.php new file mode 100644 index 0000000000..d4b13bb10c --- /dev/null +++ b/src/applications/auth/password/PhabricatorAuthPasswordException.php @@ -0,0 +1,28 @@ +passwordError = $password_error; + $this->confirmError = $confirm_error; + + parent::__construct($message); + } + + public function getPasswordError() { + return $this->passwordError; + } + + public function getConfirmError() { + return $this->confirmError; + } + +} diff --git a/src/applications/auth/password/PhabricatorAuthPasswordHashInterface.php b/src/applications/auth/password/PhabricatorAuthPasswordHashInterface.php new file mode 100644 index 0000000000..36a296b209 --- /dev/null +++ b/src/applications/auth/password/PhabricatorAuthPasswordHashInterface.php @@ -0,0 +1,9 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $password = $objects[$phid]; + } + } + +} diff --git a/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php b/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php index 206eba4578..75deb9dde5 100644 --- a/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php +++ b/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php @@ -253,6 +253,7 @@ final class PhabricatorPasswordAuthProvider extends PhabricatorAuthProvider { $request = $controller->getRequest(); $viewer = $request->getUser(); + $content_source = PhabricatorContentSource::newFromRequest($request); $require_captcha = false; $captcha_valid = false; @@ -285,22 +286,16 @@ final class PhabricatorPasswordAuthProvider extends PhabricatorAuthProvider { if ($user) { $envelope = new PhutilOpaqueEnvelope($request->getStr('password')); - if ($user->comparePassword($envelope)) { + + $engine = id(new PhabricatorAuthPasswordEngine()) + ->setViewer($user) + ->setContentSource($content_source) + ->setPasswordType(PhabricatorAuthPassword::PASSWORD_TYPE_ACCOUNT) + ->setObject($user); + + if ($engine->isValidPassword($envelope)) { $account = $this->loadOrCreateAccount($user->getPHID()); $log_user = $user; - - // If the user's password is stored using a less-than-optimal - // hash, upgrade them to the strongest available hash. - - $hash_envelope = new PhutilOpaqueEnvelope( - $user->getPasswordHash()); - if (PhabricatorPasswordHasher::canUpgradeHash($hash_envelope)) { - $user->setPassword($envelope); - - $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); - $user->save(); - unset($unguarded); - } } } } diff --git a/src/applications/auth/query/PhabricatorAuthPasswordQuery.php b/src/applications/auth/query/PhabricatorAuthPasswordQuery.php new file mode 100644 index 0000000000..483936fb30 --- /dev/null +++ b/src/applications/auth/query/PhabricatorAuthPasswordQuery.php @@ -0,0 +1,114 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withObjectPHIDs(array $object_phids) { + $this->objectPHIDs = $object_phids; + return $this; + } + + public function withPasswordTypes(array $types) { + $this->passwordTypes = $types; + return $this; + } + + public function withIsRevoked($is_revoked) { + $this->isRevoked = $is_revoked; + return $this; + } + + public function newResultObject() { + return new PhabricatorAuthPassword(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'phid IN (%Ls)', + $this->phids); + } + + if ($this->objectPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'objectPHID IN (%Ls)', + $this->objectPHIDs); + } + + if ($this->passwordTypes !== null) { + $where[] = qsprintf( + $conn, + 'passwordType IN (%Ls)', + $this->passwordTypes); + } + + if ($this->isRevoked !== null) { + $where[] = qsprintf( + $conn, + 'isRevoked = %d', + (int)$this->isRevoked); + } + + return $where; + } + + protected function willFilterPage(array $passwords) { + $object_phids = mpull($passwords, 'getObjectPHID'); + + $objects = id(new PhabricatorObjectQuery()) + ->setViewer($this->getViewer()) + ->setParentQuery($this) + ->withPHIDs($object_phids) + ->execute(); + $objects = mpull($objects, null, 'getPHID'); + + foreach ($passwords as $key => $password) { + $object = idx($objects, $password->getObjectPHID()); + if (!$object) { + unset($passwords[$key]); + $this->didRejectResult($password); + continue; + } + + $password->attachObject($object); + } + + return $passwords; + } + + public function getQueryApplicationClass() { + return 'PhabricatorAuthApplication'; + } + +} diff --git a/src/applications/auth/query/PhabricatorAuthPasswordTransactionQuery.php b/src/applications/auth/query/PhabricatorAuthPasswordTransactionQuery.php new file mode 100644 index 0000000000..519b6aa77b --- /dev/null +++ b/src/applications/auth/query/PhabricatorAuthPasswordTransactionQuery.php @@ -0,0 +1,10 @@ +establishConnection('w'); diff --git a/src/applications/auth/revoker/PhabricatorAuthPasswordRevoker.php b/src/applications/auth/revoker/PhabricatorAuthPasswordRevoker.php new file mode 100644 index 0000000000..646d266b1e --- /dev/null +++ b/src/applications/auth/revoker/PhabricatorAuthPasswordRevoker.php @@ -0,0 +1,81 @@ +revokeWithQuery($query); + } + + public function revokeCredentialsFrom($object) { + $query = id(new PhabricatorAuthPasswordQuery()) + ->withObjectPHIDs(array($object->getPHID())); + return $this->revokeWithQuery($query); + } + + private function revokeWithQuery(PhabricatorAuthPasswordQuery $query) { + $viewer = $this->getViewer(); + + $passwords = $query + ->setViewer($viewer) + ->withIsRevoked(false) + ->execute(); + + $content_source = PhabricatorContentSource::newForSource( + PhabricatorDaemonContentSource::SOURCECONST); + + $revoke_type = PhabricatorAuthPasswordRevokeTransaction::TRANSACTIONTYPE; + + $auth_phid = id(new PhabricatorAuthApplication())->getPHID(); + foreach ($passwords as $password) { + $xactions = array(); + + $xactions[] = $password->getApplicationTransactionTemplate() + ->setTransactionType($revoke_type) + ->setNewValue(true); + + $editor = $password->getApplicationTransactionEditor() + ->setActor($viewer) + ->setActingAsPHID($auth_phid) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->setContentSource($content_source) + ->applyTransactions($password, $xactions); + } + + return count($passwords); + } + +} diff --git a/src/applications/auth/revoker/PhabricatorAuthRevoker.php b/src/applications/auth/revoker/PhabricatorAuthRevoker.php index f4467036b9..9bf44b05bc 100644 --- a/src/applications/auth/revoker/PhabricatorAuthRevoker.php +++ b/src/applications/auth/revoker/PhabricatorAuthRevoker.php @@ -5,9 +5,16 @@ abstract class PhabricatorAuthRevoker private $viewer; - abstract public function revokeAlLCredentials(); + abstract public function revokeAllCredentials(); abstract public function revokeCredentialsFrom($object); + abstract public function getRevokerName(); + abstract public function getRevokerDescription(); + + public function getRevokerNextSteps() { + return null; + } + public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; diff --git a/src/applications/auth/revoker/PhabricatorAuthSSHRevoker.php b/src/applications/auth/revoker/PhabricatorAuthSSHRevoker.php new file mode 100644 index 0000000000..3e6b862370 --- /dev/null +++ b/src/applications/auth/revoker/PhabricatorAuthSSHRevoker.php @@ -0,0 +1,65 @@ +revokeWithQuery($query); + } + + public function revokeCredentialsFrom($object) { + $query = id(new PhabricatorAuthSSHKeyQuery()) + ->withObjectPHIDs(array($object->getPHID())); + + return $this->revokeWithQuery($query); + } + + private function revokeWithQuery(PhabricatorAuthSSHKeyQuery $query) { + $viewer = $this->getViewer(); + + // We're only going to revoke keys which have not already been revoked. + + $ssh_keys = $query + ->setViewer($viewer) + ->withIsActive(true) + ->execute(); + + $content_source = PhabricatorContentSource::newForSource( + PhabricatorDaemonContentSource::SOURCECONST); + + $auth_phid = id(new PhabricatorAuthApplication())->getPHID(); + foreach ($ssh_keys as $ssh_key) { + $xactions = array(); + $xactions[] = $ssh_key->getApplicationTransactionTemplate() + ->setTransactionType(PhabricatorAuthSSHKeyTransaction::TYPE_DEACTIVATE) + ->setNewValue(1); + + $editor = $ssh_key->getApplicationTransactionEditor() + ->setActor($viewer) + ->setActingAsPHID($auth_phid) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->setContentSource($content_source) + ->setIsAdministrativeEdit(true) + ->applyTransactions($ssh_key, $xactions); + } + + return count($ssh_keys); + } + +} diff --git a/src/applications/auth/revoker/PhabricatorAuthSessionRevoker.php b/src/applications/auth/revoker/PhabricatorAuthSessionRevoker.php new file mode 100644 index 0000000000..c3362b0331 --- /dev/null +++ b/src/applications/auth/revoker/PhabricatorAuthSessionRevoker.php @@ -0,0 +1,43 @@ +establishConnection('w'); + + queryfx( + $conn, + 'DELETE FROM %T', + $table->getTableName()); + + return $conn->getAffectedRows(); + } + + public function revokeCredentialsFrom($object) { + $table = new PhabricatorAuthSession(); + $conn = $table->establishConnection('w'); + + queryfx( + $conn, + 'DELETE FROM %T WHERE userPHID = %s', + $table->getTableName(), + $object->getPHID()); + + return $conn->getAffectedRows(); + } + +} diff --git a/src/applications/auth/revoker/PhabricatorAuthTemporaryTokenRevoker.php b/src/applications/auth/revoker/PhabricatorAuthTemporaryTokenRevoker.php new file mode 100644 index 0000000000..f274e13a18 --- /dev/null +++ b/src/applications/auth/revoker/PhabricatorAuthTemporaryTokenRevoker.php @@ -0,0 +1,46 @@ +establishConnection('w'); + + queryfx( + $conn, + 'DELETE FROM %T', + $table->getTableName()); + + return $conn->getAffectedRows(); + } + + public function revokeCredentialsFrom($object) { + $table = new PhabricatorAuthTemporaryToken(); + $conn = $table->establishConnection('w'); + + queryfx( + $conn, + 'DELETE FROM %T WHERE tokenResource = %s', + $table->getTableName(), + $object->getPHID()); + + return $conn->getAffectedRows(); + } + +} diff --git a/src/applications/auth/storage/PhabricatorAuthPassword.php b/src/applications/auth/storage/PhabricatorAuthPassword.php new file mode 100644 index 0000000000..5343e622fd --- /dev/null +++ b/src/applications/auth/storage/PhabricatorAuthPassword.php @@ -0,0 +1,235 @@ +setObjectPHID($object->getPHID()) + ->attachObject($object) + ->setPasswordType($type) + ->setIsRevoked(0); + } + + protected function getConfiguration() { + return array( + self::CONFIG_AUX_PHID => true, + self::CONFIG_COLUMN_SCHEMA => array( + 'passwordType' => 'text64', + 'passwordHash' => 'text128', + 'passwordSalt' => 'text64', + 'isRevoked' => 'bool', + 'legacyDigestFormat' => 'text32?', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_role' => array( + 'columns' => array('objectPHID', 'passwordType'), + ), + ), + ) + parent::getConfiguration(); + } + + public function getPHIDType() { + return PhabricatorAuthPasswordPHIDType::TYPECONST; + } + + public function getObject() { + return $this->assertAttached($this->object); + } + + public function attachObject($object) { + $this->object = $object; + return $this; + } + + public function getHasher() { + $hash = $this->newPasswordEnvelope(); + return PhabricatorPasswordHasher::getHasherForHash($hash); + } + + public function canUpgrade() { + // If this password uses a legacy digest format, we can upgrade it to the + // new digest format even if a better hasher isn't available. + if ($this->getLegacyDigestFormat() !== null) { + return true; + } + + $hash = $this->newPasswordEnvelope(); + return PhabricatorPasswordHasher::canUpgradeHash($hash); + } + + public function upgradePasswordHasher( + PhutilOpaqueEnvelope $envelope, + PhabricatorAuthPasswordHashInterface $object) { + + // Before we make changes, double check that this is really the correct + // password. It could be really bad if we "upgraded" a password and changed + // the secret! + + if (!$this->comparePassword($envelope, $object)) { + throw new Exception( + pht( + 'Attempting to upgrade password hasher, but the password for the '. + 'upgrade is not the stored credential!')); + } + + return $this->setPassword($envelope, $object); + } + + public function setPassword( + PhutilOpaqueEnvelope $password, + PhabricatorAuthPasswordHashInterface $object) { + + $hasher = PhabricatorPasswordHasher::getBestHasher(); + return $this->setPasswordWithHasher($password, $object, $hasher); + } + + public function setPasswordWithHasher( + PhutilOpaqueEnvelope $password, + PhabricatorAuthPasswordHashInterface $object, + PhabricatorPasswordHasher $hasher) { + + if (!strlen($password->openEnvelope())) { + throw new Exception( + pht('Attempting to set an empty password!')); + } + + // Generate (or regenerate) the salt first. + $new_salt = Filesystem::readRandomCharacters(64); + $this->setPasswordSalt($new_salt); + + // Clear any legacy digest format to force a modern digest. + $this->setLegacyDigestFormat(null); + + $digest = $this->digestPassword($password, $object); + $hash = $hasher->getPasswordHashForStorage($digest); + $raw_hash = $hash->openEnvelope(); + + return $this->setPasswordHash($raw_hash); + } + + public function comparePassword( + PhutilOpaqueEnvelope $password, + PhabricatorAuthPasswordHashInterface $object) { + + $digest = $this->digestPassword($password, $object); + $hash = $this->newPasswordEnvelope(); + + return PhabricatorPasswordHasher::comparePassword($digest, $hash); + } + + public function newPasswordEnvelope() { + return new PhutilOpaqueEnvelope($this->getPasswordHash()); + } + + private function digestPassword( + PhutilOpaqueEnvelope $password, + PhabricatorAuthPasswordHashInterface $object) { + + $object_phid = $object->getPHID(); + + if ($this->getObjectPHID() !== $object->getPHID()) { + throw new Exception( + pht( + 'This password is associated with an object PHID ("%s") for '. + 'a different object than the provided one ("%s").', + $this->getObjectPHID(), + $object->getPHID())); + } + + $digest = $object->newPasswordDigest($password, $this); + + if (!($digest instanceof PhutilOpaqueEnvelope)) { + throw new Exception( + pht( + 'Failed to digest password: object ("%s") did not return an '. + 'opaque envelope with a password digest.', + $object->getPHID())); + } + + return $digest; + } + + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + ); + } + + public function getPolicy($capability) { + return PhabricatorPolicies::getMostOpenPolicy(); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } + + +/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ + + + public function getExtendedPolicy($capability, PhabricatorUser $viewer) { + return array( + array($this->getObject(), PhabricatorPolicyCapability::CAN_VIEW), + ); + } + + +/* -( PhabricatorDestructibleInterface )----------------------------------- */ + + + public function destroyObjectPermanently( + PhabricatorDestructionEngine $engine) { + $this->delete(); + } + + +/* -( PhabricatorApplicationTransactionInterface )------------------------- */ + + + public function getApplicationTransactionEditor() { + return new PhabricatorAuthPasswordEditor(); + } + + public function getApplicationTransactionObject() { + return $this; + } + + public function getApplicationTransactionTemplate() { + return new PhabricatorAuthPasswordTransaction(); + } + + public function willRenderTimeline( + PhabricatorApplicationTransactionView $timeline, + AphrontRequest $request) { + + return $timeline; + } + + +} diff --git a/src/applications/auth/storage/PhabricatorAuthPasswordTransaction.php b/src/applications/auth/storage/PhabricatorAuthPasswordTransaction.php new file mode 100644 index 0000000000..9d02112dff --- /dev/null +++ b/src/applications/auth/storage/PhabricatorAuthPasswordTransaction.php @@ -0,0 +1,21 @@ +getIsACtive()) { return pht( - 'Deactivated SSH keys can not be edited or reactivated.'); + 'Revoked SSH keys can not be edited or reinstated.'); } return pht( diff --git a/src/applications/auth/storage/PhabricatorAuthSSHKeyTransaction.php b/src/applications/auth/storage/PhabricatorAuthSSHKeyTransaction.php index 37edb7384d..bb08310cf3 100644 --- a/src/applications/auth/storage/PhabricatorAuthSSHKeyTransaction.php +++ b/src/applications/auth/storage/PhabricatorAuthSSHKeyTransaction.php @@ -43,11 +43,11 @@ final class PhabricatorAuthSSHKeyTransaction case self::TYPE_DEACTIVATE: if ($new) { return pht( - '%s deactivated this key.', + '%s revoked this key.', $this->renderHandleLink($author_phid)); } else { return pht( - '%s activated this key.', + '%s reinstated this key.', $this->renderHandleLink($author_phid)); } diff --git a/src/applications/auth/xaction/PhabricatorAuthPasswordRevokeTransaction.php b/src/applications/auth/xaction/PhabricatorAuthPasswordRevokeTransaction.php new file mode 100644 index 0000000000..e5f9489f6e --- /dev/null +++ b/src/applications/auth/xaction/PhabricatorAuthPasswordRevokeTransaction.php @@ -0,0 +1,32 @@ +getIsRevoked(); + } + + public function generateNewValue($object, $value) { + return (bool)$value; + } + + public function applyInternalEffects($object, $value) { + $object->setIsRevoked((int)$value); + } + + public function getTitle() { + if ($this->getNewValue()) { + return pht( + '%s revoked this password.', + $this->renderAuthor()); + } else { + return pht( + '%s removed this password from the revocation list.', + $this->renderAuthor()); + } + } + +} diff --git a/src/applications/auth/xaction/PhabricatorAuthPasswordTransactionType.php b/src/applications/auth/xaction/PhabricatorAuthPasswordTransactionType.php new file mode 100644 index 0000000000..c557ffa10f --- /dev/null +++ b/src/applications/auth/xaction/PhabricatorAuthPasswordTransactionType.php @@ -0,0 +1,4 @@ +getEditor()->getOldHasher(); + + if (!$old_hasher) { + throw new PhutilInvalidStateException('setOldHasher'); + } + + return $old_hasher->getHashName(); + } + + public function generateNewValue($object, $value) { + return $value; + } + + public function getTitle() { + return pht( + '%s upgraded the hash algorithm for this password from "%s" to "%s".', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } + +} diff --git a/src/applications/base/PhabricatorApplication.php b/src/applications/base/PhabricatorApplication.php index c25612408c..420d126507 100644 --- a/src/applications/base/PhabricatorApplication.php +++ b/src/applications/base/PhabricatorApplication.php @@ -618,10 +618,14 @@ abstract class PhabricatorApplication ')?'; } - protected function getQueryRoutePattern($base = null) { + protected function getBulkRoutePattern($base = null) { return $base.'(?:query/(?P[^/]+)/)?'; } + protected function getQueryRoutePattern($base = null) { + return $base.'(?:query/(?P[^/]+)/(?:(?P[^/]+)/)?)?'; + } + protected function getProfileMenuRouting($controller) { $edit_route = $this->getEditRoutePattern(); diff --git a/src/applications/calendar/herald/PhabricatorCalendarEventHeraldAdapter.php b/src/applications/calendar/herald/PhabricatorCalendarEventHeraldAdapter.php index 1fc3a56317..49d5f38959 100644 --- a/src/applications/calendar/herald/PhabricatorCalendarEventHeraldAdapter.php +++ b/src/applications/calendar/herald/PhabricatorCalendarEventHeraldAdapter.php @@ -49,13 +49,6 @@ final class PhabricatorCalendarEventHeraldAdapter extends HeraldAdapter { } } - public function getRepetitionOptions() { - return array( - HeraldRepetitionPolicyConfig::EVERY, - HeraldRepetitionPolicyConfig::FIRST, - ); - } - public function getHeraldName() { return $this->getObject()->getMonogram(); } diff --git a/src/applications/conduit/ssh/ConduitSSHWorkflow.php b/src/applications/conduit/ssh/ConduitSSHWorkflow.php index 6589fac324..603a479ea0 100644 --- a/src/applications/conduit/ssh/ConduitSSHWorkflow.php +++ b/src/applications/conduit/ssh/ConduitSSHWorkflow.php @@ -46,7 +46,7 @@ final class ConduitSSHWorkflow extends PhabricatorSSHWorkflow { try { $call = new ConduitCall($method, $params); - $call->setUser($this->getUser()); + $call->setUser($this->getSSHUser()); $result = $call->execute(); } catch (ConduitException $ex) { @@ -77,7 +77,7 @@ final class ConduitSSHWorkflow extends PhabricatorSSHWorkflow { $connection_id = idx($metadata, 'connectionID'); $log = id(new PhabricatorConduitMethodCallLog()) - ->setCallerPHID($this->getUser()->getPHID()) + ->setCallerPHID($this->getSSHUser()->getPHID()) ->setConnectionID($connection_id) ->setMethod($method) ->setError((string)$error_code) diff --git a/src/applications/config/check/PhabricatorGDSetupCheck.php b/src/applications/config/check/PhabricatorGDSetupCheck.php index 750702c706..7ada204801 100644 --- a/src/applications/config/check/PhabricatorGDSetupCheck.php +++ b/src/applications/config/check/PhabricatorGDSetupCheck.php @@ -18,7 +18,8 @@ final class PhabricatorGDSetupCheck extends PhabricatorSetupCheck { $this->newIssue('extension.gd') ->setName(pht("Missing '%s' Extension", 'gd')) - ->setMessage($message); + ->setMessage($message) + ->addPHPExtension('gd'); } else { $image_type_map = array( 'imagecreatefrompng' => 'PNG', diff --git a/src/applications/daemon/controller/PhabricatorDaemonBulkJobMonitorController.php b/src/applications/daemon/controller/PhabricatorDaemonBulkJobMonitorController.php index 79d509f1ea..fd3e61d869 100644 --- a/src/applications/daemon/controller/PhabricatorDaemonBulkJobMonitorController.php +++ b/src/applications/daemon/controller/PhabricatorDaemonBulkJobMonitorController.php @@ -49,13 +49,22 @@ final class PhabricatorDaemonBulkJobMonitorController return id(new AphrontRedirectResponse()) ->setURI($job->getMonitorURI()); } else { - return $this->newDialog() - ->setTitle(pht('Confirm Bulk Job')) - ->appendParagraph($job->getDescriptionForConfirm()) + $dialog = $this->newDialog() + ->setTitle(pht('Confirm Bulk Job')); + + $confirm = $job->getDescriptionForConfirm(); + $confirm = (array)$confirm; + foreach ($confirm as $paragraph) { + $dialog->appendParagraph($paragraph); + } + + $dialog ->appendParagraph( pht('Start work on this bulk job?')) ->addCancelButton($job->getManageURI(), pht('Details')) ->addSubmitButton(pht('Start Work')); + + return $dialog; } } else { return $this->newDialog() diff --git a/src/applications/daemon/controller/PhabricatorWorkerTaskDetailController.php b/src/applications/daemon/controller/PhabricatorWorkerTaskDetailController.php index 85c2f2f87e..e18c2d725e 100644 --- a/src/applications/daemon/controller/PhabricatorWorkerTaskDetailController.php +++ b/src/applications/daemon/controller/PhabricatorWorkerTaskDetailController.php @@ -79,13 +79,20 @@ final class PhabricatorWorkerTaskDetailController ->appendChild($view); } - private function buildPropertyListView( - PhabricatorWorkerTask $task) { - - $viewer = $this->getRequest()->getUser(); + private function buildPropertyListView(PhabricatorWorkerTask $task) { + $viewer = $this->getViewer(); $view = new PHUIPropertyListView(); + $object_phid = $task->getObjectPHID(); + if ($object_phid) { + $handles = $viewer->loadHandles(array($object_phid)); + $handle = $handles[$object_phid]; + if ($handle->isComplete()) { + $view->addProperty(pht('Object'), $handle->renderLink()); + } + } + if ($task->isArchived()) { switch ($task->getResult()) { case PhabricatorWorkerArchiveTask::RESULT_SUCCESS: diff --git a/src/applications/differential/conduit/DifferentialCreateCommentConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialCreateCommentConduitAPIMethod.php index 21b055a1e4..657237c718 100644 --- a/src/applications/differential/conduit/DifferentialCreateCommentConduitAPIMethod.php +++ b/src/applications/differential/conduit/DifferentialCreateCommentConduitAPIMethod.php @@ -106,9 +106,11 @@ final class DifferentialCreateCommentConduitAPIMethod } } + // NOTE: The legacy "silent" flag is now ignored and has no effect. See + // T13042. + $editor = id(new DifferentialTransactionEditor()) ->setActor($viewer) - ->setDisableEmail($request->getValue('silent')) ->setContentSource($request->newContentSource()) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true); diff --git a/src/applications/differential/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php index e6dfed88a7..ecd1e6c95c 100644 --- a/src/applications/differential/editor/DifferentialTransactionEditor.php +++ b/src/applications/differential/editor/DifferentialTransactionEditor.php @@ -700,20 +700,26 @@ final class DifferentialTransactionEditor ->addHeader('Thread-Topic', $thread_topic); } - protected function buildMailBody( + protected function getTransactionsForMail( PhabricatorLiskDAO $object, array $xactions) { - - $viewer = $this->requireActor(); - // If this is the first time we're sending mail about this revision, we // generate mail for all prior transactions, not just whatever is being // applied now. This gets the "added reviewers" lines and other relevant // information into the mail. if ($this->isFirstBroadcast()) { - $xactions = $this->loadUnbroadcastTransactions($object); + return $this->loadUnbroadcastTransactions($object); } + return $xactions; + } + + protected function buildMailBody( + PhabricatorLiskDAO $object, + array $xactions) { + + $viewer = $this->requireActor(); + $body = new PhabricatorMetaMTAMailBody(); $body->setViewer($this->requireActor()); @@ -1565,9 +1571,29 @@ final class DifferentialTransactionEditor protected function didApplyTransactions($object, array $xactions) { + // In a moment, we're going to try to publish draft revisions which have + // completed all their builds. However, we only want to do that if the + // actor is either the revision author or an omnipotent user (generally, + // the Harbormaster application). + + // If we let any actor publish the revision as a side effect of other + // changes then an unlucky third party who innocently comments on the draft + // can end up racing Harbormaster and promoting the revision. At best, this + // is confusing. It can also run into validation problems with the "Request + // Review" transaction. See PHI309 for some discussion. + $author_phid = $object->getAuthorPHID(); + $viewer = $this->requireActor(); + $can_undraft = + ($this->getActingAsPHID() === $author_phid) || + ($viewer->isOmnipotent()); + // If a draft revision has no outstanding builds and we're automatically // making drafts public after builds finish, make the revision public. - $auto_undraft = !$object->getHoldAsDraft(); + if ($can_undraft) { + $auto_undraft = !$object->getHoldAsDraft(); + } else { + $auto_undraft = false; + } if ($object->isDraft() && $auto_undraft) { $active_builds = $this->hasActiveBuilds($object); @@ -1575,7 +1601,6 @@ final class DifferentialTransactionEditor // When Harbormaster moves a revision out of the draft state, we // attribute the action to the revision author since this is more // natural and more useful. - $author_phid = $object->getAuthorPHID(); // Additionally, we change the acting PHID for the transaction set // to the author if it isn't already a user so that mail comes from diff --git a/src/applications/differential/herald/DifferentialRevisionStatusHeraldField.php b/src/applications/differential/herald/DifferentialRevisionStatusHeraldField.php new file mode 100644 index 0000000000..1f343eeb90 --- /dev/null +++ b/src/applications/differential/herald/DifferentialRevisionStatusHeraldField.php @@ -0,0 +1,29 @@ +getStatus(); + } + + protected function getHeraldFieldStandardType() { + return self::STANDARD_PHID; + } + + protected function getDatasource() { + return new DifferentialRevisionStatusDatasource(); + } + + protected function getDatasourceValueMap() { + $map = DifferentialRevisionStatus::getAll(); + return mpull($map, 'getDisplayName', 'getKey'); + } + +} diff --git a/src/applications/differential/herald/HeraldDifferentialDiffAdapter.php b/src/applications/differential/herald/HeraldDifferentialDiffAdapter.php index 9528978050..a6b2e7c36e 100644 --- a/src/applications/differential/herald/HeraldDifferentialDiffAdapter.php +++ b/src/applications/differential/herald/HeraldDifferentialDiffAdapter.php @@ -53,12 +53,6 @@ final class HeraldDifferentialDiffAdapter extends HeraldDifferentialAdapter { } } - public function getRepetitionOptions() { - return array( - HeraldRepetitionPolicyConfig::FIRST, - ); - } - public function getHeraldName() { return pht('New Diff'); } diff --git a/src/applications/differential/herald/HeraldDifferentialRevisionAdapter.php b/src/applications/differential/herald/HeraldDifferentialRevisionAdapter.php index e20a4fd37d..9f7f8fe3f2 100644 --- a/src/applications/differential/herald/HeraldDifferentialRevisionAdapter.php +++ b/src/applications/differential/herald/HeraldDifferentialRevisionAdapter.php @@ -69,13 +69,6 @@ final class HeraldDifferentialRevisionAdapter } } - public function getRepetitionOptions() { - return array( - HeraldRepetitionPolicyConfig::EVERY, - HeraldRepetitionPolicyConfig::FIRST, - ); - } - public static function newLegacyAdapter( DifferentialRevision $revision, DifferentialDiff $diff) { diff --git a/src/applications/differential/storage/DifferentialChangeset.php b/src/applications/differential/storage/DifferentialChangeset.php index 3656fe6507..ebdaeacd0a 100644 --- a/src/applications/differential/storage/DifferentialChangeset.php +++ b/src/applications/differential/storage/DifferentialChangeset.php @@ -173,7 +173,7 @@ final class DifferentialChangeset } public function getAnchorName() { - return 'change-'.PhabricatorHash::digestForIndex($this->getFilename()); + return 'change-'.PhabricatorHash::digestForAnchor($this->getFilename()); } public function getAbsoluteRepositoryPath( diff --git a/src/applications/differential/storage/DifferentialTransaction.php b/src/applications/differential/storage/DifferentialTransaction.php index 18edd3c625..667341cef4 100644 --- a/src/applications/differential/storage/DifferentialTransaction.php +++ b/src/applications/differential/storage/DifferentialTransaction.php @@ -100,6 +100,13 @@ final class DifferentialTransaction return true; } break; + case DifferentialRevisionRequestReviewTransaction::TRANSACTIONTYPE: + // Don't hide the initial "X requested review: ..." transaction from + // mail or feed even when it occurs during creation. We need this + // transaction to survive so we'll generate mail and feed stories when + // revisions immediately leave the draft state. See T13035 for + // discussion. + return false; } return parent::shouldHide(); @@ -111,12 +118,6 @@ final class DifferentialTransaction // Don't hide the initial "X added reviewers: ..." transaction during // object creation from mail. See T12118 and PHI54. return false; - case DifferentialRevisionRequestReviewTransaction::TRANSACTIONTYPE: - // Don't hide the initial "X requested review: ..." transaction from - // mail even when it occurs during creation. We need this transaction - // to survive so we'll generate mail when revisions immediately leave - // the draft state. See T13035 for discussion. - return false; } return parent::shouldHideForMail($xactions); diff --git a/src/applications/differential/view/DifferentialChangesetDetailView.php b/src/applications/differential/view/DifferentialChangesetDetailView.php index a1a3de26bb..d4a13745dc 100644 --- a/src/applications/differential/view/DifferentialChangesetDetailView.php +++ b/src/applications/differential/view/DifferentialChangesetDetailView.php @@ -204,6 +204,7 @@ final class DifferentialChangesetDetailView extends AphrontView { 'loaded' => $this->getLoaded(), 'undoTemplates' => hsprintf('%s', $renderer->renderUndoTemplates()), 'displayPath' => hsprintf('%s', $display_parts), + 'path' => $display_filename, 'icon' => $display_icon, ), 'class' => $class, diff --git a/src/applications/diffusion/application/PhabricatorDiffusionApplication.php b/src/applications/diffusion/application/PhabricatorDiffusionApplication.php index 5797b8ba7c..d932149a75 100644 --- a/src/applications/diffusion/application/PhabricatorDiffusionApplication.php +++ b/src/applications/diffusion/application/PhabricatorDiffusionApplication.php @@ -124,6 +124,9 @@ final class PhabricatorDiffusionApplication extends PhabricatorApplication { '(?:query/(?P[^/]+)/)?' => 'DiffusionPushLogListController', 'view/(?P\d+)/' => 'DiffusionPushEventViewController', ), + 'pulllog/' => array( + $this->getQueryRoutePattern() => 'DiffusionPullLogListController', + ), '(?P[A-Z]+)' => $repository_routes, '(?P[1-9]\d*)' => $repository_routes, diff --git a/src/applications/diffusion/conduit/DiffusionSearchQueryConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionSearchQueryConduitAPIMethod.php index af973f102d..e2c56bb0f8 100644 --- a/src/applications/diffusion/conduit/DiffusionSearchQueryConduitAPIMethod.php +++ b/src/applications/diffusion/conduit/DiffusionSearchQueryConduitAPIMethod.php @@ -54,6 +54,12 @@ final class DiffusionSearchQueryConduitAPIMethod $limit = $request->getValue('limit'); $offset = $request->getValue('offset'); + // Starting with Git 2.16.0, Git assumes passing an empty argument is + // an error and recommends you pass "." instead. + if (!strlen($path)) { + $path = '.'; + } + $results = array(); $future = $repository->getLocalCommandFuture( // NOTE: --perl-regexp is available only with libpcre compiled in. diff --git a/src/applications/diffusion/controller/DiffusionBrowseController.php b/src/applications/diffusion/controller/DiffusionBrowseController.php index 92181ff551..8df5579a62 100644 --- a/src/applications/diffusion/controller/DiffusionBrowseController.php +++ b/src/applications/diffusion/controller/DiffusionBrowseController.php @@ -658,6 +658,11 @@ final class DiffusionBrowseController extends DiffusionController { ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($corpus) ->addClass('diffusion-mobile-view') + ->addSigil('diffusion-file-content-view') + ->setMetadata( + array( + 'path' => $this->getDiffusionRequest()->getPath(), + )) ->setCollapsed(true); $messages = array(); @@ -947,6 +952,10 @@ final class DiffusionBrowseController extends DiffusionController { } foreach ($revision_ids as $commit_phid => $revision_id) { + // If the viewer can't actually see this revision, skip it. + if (!isset($revisions[$revision_id])) { + continue; + } $revision_map[$commit_map[$commit_phid]] = $revision_id; } } diff --git a/src/applications/diffusion/controller/DiffusionPushLogController.php b/src/applications/diffusion/controller/DiffusionLogController.php similarity index 54% rename from src/applications/diffusion/controller/DiffusionPushLogController.php rename to src/applications/diffusion/controller/DiffusionLogController.php index f974cc91cd..9e5a4eaa3d 100644 --- a/src/applications/diffusion/controller/DiffusionPushLogController.php +++ b/src/applications/diffusion/controller/DiffusionLogController.php @@ -1,6 +1,6 @@ setController($this) + ->buildResponse(); + } + +} diff --git a/src/applications/diffusion/controller/DiffusionPushEventViewController.php b/src/applications/diffusion/controller/DiffusionPushEventViewController.php index c5eb6368b4..21718c256d 100644 --- a/src/applications/diffusion/controller/DiffusionPushEventViewController.php +++ b/src/applications/diffusion/controller/DiffusionPushEventViewController.php @@ -1,11 +1,7 @@ getViewer(); diff --git a/src/applications/diffusion/controller/DiffusionPushLogListController.php b/src/applications/diffusion/controller/DiffusionPushLogListController.php index 5b58881470..658e21674d 100644 --- a/src/applications/diffusion/controller/DiffusionPushLogListController.php +++ b/src/applications/diffusion/controller/DiffusionPushLogListController.php @@ -1,10 +1,7 @@ isHosted()) { $push_uri = $this->getApplicationURI( - 'pushlog/?repositories='.$repository->getMonogram()); + 'pushlog/?repositories='.$repository->getPHID()); $action_view->addAction( id(new PhabricatorActionView()) @@ -374,6 +374,15 @@ final class DiffusionRepositoryController extends DiffusionController { ->setHref($push_uri)); } + $pull_uri = $this->getApplicationURI( + 'pulllog/?repositories='.$repository->getPHID()); + + $action_view->addAction( + id(new PhabricatorActionView()) + ->setName(pht('View Pull Logs')) + ->setIcon('fa-list-alt') + ->setHref($pull_uri)); + return $action_view; } diff --git a/src/applications/diffusion/controller/DiffusionServeController.php b/src/applications/diffusion/controller/DiffusionServeController.php index 475f688a4c..c0f72ae404 100644 --- a/src/applications/diffusion/controller/DiffusionServeController.php +++ b/src/applications/diffusion/controller/DiffusionServeController.php @@ -104,15 +104,29 @@ final class DiffusionServeController extends DiffusionController { try { $remote_addr = $request->getRemoteAddress(); + if ($request->isHTTPS()) { + $remote_protocol = PhabricatorRepositoryPullEvent::PROTOCOL_HTTPS; + } else { + $remote_protocol = PhabricatorRepositoryPullEvent::PROTOCOL_HTTP; + } + $pull_event = id(new PhabricatorRepositoryPullEvent()) ->setEpoch(PhabricatorTime::getNow()) ->setRemoteAddress($remote_addr) - ->setRemoteProtocol('http'); + ->setRemoteProtocol($remote_protocol); if ($response) { - $pull_event - ->setResultType('wild') - ->setResultCode($response->getHTTPResponseCode()); + $response_code = $response->getHTTPResponseCode(); + + if ($response_code == 200) { + $pull_event + ->setResultType(PhabricatorRepositoryPullEvent::RESULT_PULL) + ->setResultCode($response_code); + } else { + $pull_event + ->setResultType(PhabricatorRepositoryPullEvent::RESULT_ERROR) + ->setResultCode($response_code); + } if ($response instanceof PhabricatorVCSResponse) { $pull_event->setProperties( @@ -122,7 +136,7 @@ final class DiffusionServeController extends DiffusionController { } } else { $pull_event - ->setResultType('exception') + ->setResultType(PhabricatorRepositoryPullEvent::RESULT_EXCEPTION) ->setResultCode(500) ->setProperties( array( @@ -715,30 +729,19 @@ final class DiffusionServeController extends DiffusionController { return null; } - $password_entry = id(new PhabricatorRepositoryVCSPassword()) - ->loadOneWhere('userPHID = %s', $user->getPHID()); - if (!$password_entry) { - // User doesn't have a password set. + $request = $this->getRequest(); + $content_source = PhabricatorContentSource::newFromRequest($request); + + $engine = id(new PhabricatorAuthPasswordEngine()) + ->setViewer($user) + ->setContentSource($content_source) + ->setPasswordType(PhabricatorAuthPassword::PASSWORD_TYPE_VCS) + ->setObject($user); + + if (!$engine->isValidPassword($password)) { return null; } - if (!$password_entry->comparePassword($password, $user)) { - // Password doesn't match. - return null; - } - - // If the user's password is stored using a less-than-optimal hash, upgrade - // them to the strongest available hash. - - $hash_envelope = new PhutilOpaqueEnvelope( - $password_entry->getPasswordHash()); - if (PhabricatorPasswordHasher::canUpgradeHash($hash_envelope)) { - $password_entry->setPassword($password, $user); - $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); - $password_entry->save(); - unset($unguarded); - } - return $user; } diff --git a/src/applications/diffusion/controller/DiffusionSymbolController.php b/src/applications/diffusion/controller/DiffusionSymbolController.php index 3aaccffd22..b855f967db 100644 --- a/src/applications/diffusion/controller/DiffusionSymbolController.php +++ b/src/applications/diffusion/controller/DiffusionSymbolController.php @@ -22,6 +22,7 @@ final class DiffusionSymbolController extends DiffusionController { $query->setLanguage($request->getStr('lang')); } + $repos = array(); if ($request->getStr('repositories')) { $phids = $request->getStr('repositories'); $phids = explode(',', $phids); @@ -33,9 +34,9 @@ final class DiffusionSymbolController extends DiffusionController { ->withPHIDs($phids) ->execute(); - $repos = mpull($repos, 'getPHID'); - if ($repos) { - $query->withRepositoryPHIDs($repos); + $repo_phids = mpull($repos, 'getPHID'); + if ($repo_phids) { + $query->withRepositoryPHIDs($repo_phids); } } } @@ -45,7 +46,6 @@ final class DiffusionSymbolController extends DiffusionController { $symbols = $query->execute(); - $external_query = id(new DiffusionExternalSymbolQuery()) ->withNames(array($name)); @@ -61,13 +61,51 @@ final class DiffusionSymbolController extends DiffusionController { $external_query->withLanguages(array($request->getStr('lang'))); } + if ($request->getStr('path')) { + $external_query->withPaths(array($request->getStr('path'))); + } + + if ($request->getInt('line')) { + $external_query->withLines(array($request->getInt('line'))); + } + + if ($request->getInt('char')) { + $external_query->withCharacterPositions( + array( + $request->getInt('char'), + )); + } + + if ($repos) { + $external_query->withRepositories($repos); + } + $external_sources = id(new PhutilClassMapQuery()) ->setAncestorClass('DiffusionExternalSymbolsSource') ->execute(); $results = array($symbols); foreach ($external_sources as $source) { - $results[] = $source->executeQuery($external_query); + $source_results = $source->executeQuery($external_query); + + if (!is_array($source_results)) { + throw new Exception( + pht( + 'Expected a list of results from external symbol source "%s".', + get_class($source))); + } + + try { + assert_instances_of($source_results, 'PhabricatorRepositorySymbol'); + } catch (InvalidArgumentException $ex) { + throw new Exception( + pht( + 'Expected a list of PhabricatorRepositorySymbol objects '. + 'from external symbol source "%s".', + get_class($source))); + } + + $results[] = $source_results; } $symbols = array_mergev($results); diff --git a/src/applications/diffusion/herald/DiffusionCommitReviewerHeraldField.php b/src/applications/diffusion/herald/DiffusionCommitReviewerHeraldField.php index 2a09eff83e..420c2576ec 100644 --- a/src/applications/diffusion/herald/DiffusionCommitReviewerHeraldField.php +++ b/src/applications/diffusion/herald/DiffusionCommitReviewerHeraldField.php @@ -6,7 +6,11 @@ final class DiffusionCommitReviewerHeraldField const FIELDCONST = 'diffusion.commit.reviewer'; public function getHeraldFieldName() { - return pht('Reviewer'); + return pht('Reviewer (Deprecated)'); + } + + public function getFieldGroupKey() { + return HeraldDeprecatedFieldGroup::FIELDGROUPKEY; } public function getHeraldFieldValue($object) { diff --git a/src/applications/diffusion/herald/HeraldCommitAdapter.php b/src/applications/diffusion/herald/HeraldCommitAdapter.php index bfcd8e3ccb..e0e15352b6 100644 --- a/src/applications/diffusion/herald/HeraldCommitAdapter.php +++ b/src/applications/diffusion/herald/HeraldCommitAdapter.php @@ -209,7 +209,7 @@ final class HeraldCommitAdapter } private function loadCommitDiff() { - $viewer = PhabricatorUser::getOmnipotentUser(); + $viewer = $this->getViewer(); $byte_limit = self::getEnormousByteLimit(); $time_limit = self::getEnormousTimeLimit(); diff --git a/src/applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php b/src/applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php index ccfa0df4a4..1899302223 100644 --- a/src/applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php +++ b/src/applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php @@ -35,13 +35,20 @@ final class DiffusionSetPasswordSettingsPanel extends PhabricatorSettingsPanel { $request, '/settings/'); - $vcspassword = id(new PhabricatorRepositoryVCSPassword()) - ->loadOneWhere( - 'userPHID = %s', - $user->getPHID()); - if (!$vcspassword) { - $vcspassword = id(new PhabricatorRepositoryVCSPassword()); - $vcspassword->setUserPHID($user->getPHID()); + $vcs_type = PhabricatorAuthPassword::PASSWORD_TYPE_VCS; + + $vcspasswords = id(new PhabricatorAuthPasswordQuery()) + ->setViewer($viewer) + ->withObjectPHIDs(array($user->getPHID())) + ->withPasswordTypes(array($vcs_type)) + ->withIsRevoked(false) + ->execute(); + if ($vcspasswords) { + $vcspassword = head($vcspasswords); + } else { + $vcspassword = PhabricatorAuthPassword::initializeNewPassword( + $user, + $vcs_type); } $panel_uri = $this->getPanelURI('?saved=true'); @@ -51,6 +58,19 @@ final class DiffusionSetPasswordSettingsPanel extends PhabricatorSettingsPanel { $e_password = true; $e_confirm = true; + $content_source = PhabricatorContentSource::newFromRequest($request); + + // NOTE: This test is against $viewer (not $user), so that the error + // message below makes sense in the case that the two are different, + // and because an admin reusing their own password is bad, while + // system agents generally do not have passwords anyway. + + $engine = id(new PhabricatorAuthPasswordEngine()) + ->setViewer($viewer) + ->setContentSource($content_source) + ->setObject($viewer) + ->setPasswordType($vcs_type); + if ($request->isFormPost()) { if ($request->getBool('remove')) { if ($vcspassword->getID()) { @@ -61,62 +81,26 @@ final class DiffusionSetPasswordSettingsPanel extends PhabricatorSettingsPanel { $new_password = $request->getStr('password'); $confirm = $request->getStr('confirm'); - if (!strlen($new_password)) { - $e_password = pht('Required'); - $errors[] = pht('Password is required.'); - } else { - $e_password = null; - } - if (!strlen($confirm)) { - $e_confirm = pht('Required'); - $errors[] = pht('You must confirm the new password.'); - } else { + $envelope = new PhutilOpaqueEnvelope($new_password); + $confirm_envelope = new PhutilOpaqueEnvelope($confirm); + + try { + $engine->checkNewPassword($envelope, $confirm_envelope); + $e_password = null; $e_confirm = null; + } catch (PhabricatorAuthPasswordException $ex) { + $errors[] = $ex->getMessage(); + $e_password = $ex->getPasswordError(); + $e_confirm = $ex->getConfirmError(); } if (!$errors) { - $envelope = new PhutilOpaqueEnvelope($new_password); + $vcspassword + ->setPassword($envelope, $user) + ->save(); - try { - // NOTE: This test is against $viewer (not $user), so that the error - // message below makes sense in the case that the two are different, - // and because an admin reusing their own password is bad, while - // system agents generally do not have passwords anyway. - - $same_password = $viewer->comparePassword($envelope); - } catch (PhabricatorPasswordHasherUnavailableException $ex) { - // If we're missing the hasher, just let the user continue. - $same_password = false; - } - - if ($new_password !== $confirm) { - $e_password = pht('Does Not Match'); - $e_confirm = pht('Does Not Match'); - $errors[] = pht('Password and confirmation do not match.'); - } else if ($same_password) { - $e_password = pht('Not Unique'); - $e_confirm = pht('Not Unique'); - $errors[] = pht( - 'This password is the same as another password associated '. - 'with your account. You must use a unique password for '. - 'VCS access.'); - } else if ( - PhabricatorCommonPasswords::isCommonPassword($new_password)) { - $e_password = pht('Very Weak'); - $e_confirm = pht('Very Weak'); - $errors[] = pht( - 'This password is extremely weak: it is one of the most common '. - 'passwords in use. Choose a stronger password.'); - } - - - if (!$errors) { - $vcspassword->setPassword($envelope, $user); - $vcspassword->save(); - - return id(new AphrontRedirectResponse())->setURI($panel_uri); - } + return id(new AphrontRedirectResponse())->setURI($panel_uri); } } diff --git a/src/applications/diffusion/protocol/DiffusionGitCommandEngine.php b/src/applications/diffusion/protocol/DiffusionGitCommandEngine.php index 168b18caa5..995e156d8e 100644 --- a/src/applications/diffusion/protocol/DiffusionGitCommandEngine.php +++ b/src/applications/diffusion/protocol/DiffusionGitCommandEngine.php @@ -27,6 +27,7 @@ final class DiffusionGitCommandEngine $env['HOME'] = PhabricatorEnv::getEmptyCWD(); $env['GIT_SSH'] = $this->getSSHWrapper(); + $env['GIT_SSH_VARIANT'] = 'ssh'; if ($this->isAnyHTTPProtocol()) { $uri = $this->getURI(); diff --git a/src/applications/diffusion/query/DiffusionFileFutureQuery.php b/src/applications/diffusion/query/DiffusionFileFutureQuery.php index 250e4962da..18779e5fb8 100644 --- a/src/applications/diffusion/query/DiffusionFileFutureQuery.php +++ b/src/applications/diffusion/query/DiffusionFileFutureQuery.php @@ -88,7 +88,7 @@ abstract class DiffusionFileFutureQuery } final protected function executeQuery() { - $future = $this->newQueryFuture(); + $future = $this->newConfiguredQueryFuture(); $drequest = $this->getRequest(); @@ -105,6 +105,11 @@ abstract class DiffusionFileFutureQuery ->setViewPolicy(PhabricatorPolicies::POLICY_NOONE) ->setExecFuture($future); + $byte_limit = $this->getByteLimit(); + if ($byte_limit) { + $source->setByteLimit($byte_limit); + } + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $file = $source->uploadFile(); unset($unguarded); @@ -116,18 +121,8 @@ abstract class DiffusionFileFutureQuery $this->didHitTimeLimit = true; $file = null; - } - - $byte_limit = $this->getByteLimit(); - - if ($byte_limit && ($file->getByteSize() > $byte_limit)) { + } catch (PhabricatorFileUploadSourceByteLimitException $ex) { $this->didHitByteLimit = true; - - $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); - id(new PhabricatorDestructionEngine()) - ->destroyObject($file); - unset($unguarded); - $file = null; } @@ -141,11 +136,6 @@ abstract class DiffusionFileFutureQuery $future->setTimeout($this->getTimeout()); } - $byte_limit = $this->getByteLimit(); - if ($byte_limit) { - $future->setStdoutSizeLimit($byte_limit + 1); - } - return $future; } diff --git a/src/applications/diffusion/query/DiffusionPullLogSearchEngine.php b/src/applications/diffusion/query/DiffusionPullLogSearchEngine.php new file mode 100644 index 0000000000..8d6102d4eb --- /dev/null +++ b/src/applications/diffusion/query/DiffusionPullLogSearchEngine.php @@ -0,0 +1,166 @@ +newQuery(); + + if ($map['repositoryPHIDs']) { + $query->withRepositoryPHIDs($map['repositoryPHIDs']); + } + + if ($map['pullerPHIDs']) { + $query->withPullerPHIDs($map['pullerPHIDs']); + } + + return $query; + } + + protected function buildCustomSearchFields() { + return array( + id(new PhabricatorSearchDatasourceField()) + ->setDatasource(new DiffusionRepositoryDatasource()) + ->setKey('repositoryPHIDs') + ->setAliases(array('repository', 'repositories', 'repositoryPHID')) + ->setLabel(pht('Repositories')) + ->setDescription( + pht('Search for pull logs for specific repositories.')), + id(new PhabricatorUsersSearchField()) + ->setKey('pullerPHIDs') + ->setAliases(array('puller', 'pullers', 'pullerPHID')) + ->setLabel(pht('Pullers')) + ->setDescription( + pht('Search for pull logs by specific users.')), + ); + } + + protected function newExportFields() { + return array( + id(new PhabricatorIDExportField()) + ->setKey('id') + ->setLabel(pht('ID')), + id(new PhabricatorPHIDExportField()) + ->setKey('phid') + ->setLabel(pht('PHID')), + id(new PhabricatorPHIDExportField()) + ->setKey('repositoryPHID') + ->setLabel(pht('Repository PHID')), + id(new PhabricatorStringExportField()) + ->setKey('repository') + ->setLabel(pht('Repository')), + id(new PhabricatorPHIDExportField()) + ->setKey('pullerPHID') + ->setLabel(pht('Puller PHID')), + id(new PhabricatorStringExportField()) + ->setKey('puller') + ->setLabel(pht('Puller')), + id(new PhabricatorStringExportField()) + ->setKey('protocol') + ->setLabel(pht('Protocol')), + id(new PhabricatorStringExportField()) + ->setKey('result') + ->setLabel(pht('Result')), + id(new PhabricatorIntExportField()) + ->setKey('code') + ->setLabel(pht('Code')), + id(new PhabricatorEpochExportField()) + ->setKey('date') + ->setLabel(pht('Date')), + ); + } + + public function newExport(array $events) { + $viewer = $this->requireViewer(); + + $phids = array(); + foreach ($events as $event) { + if ($event->getPullerPHID()) { + $phids[] = $event->getPullerPHID(); + } + } + $handles = $viewer->loadHandles($phids); + + $export = array(); + foreach ($events as $event) { + $repository = $event->getRepository(); + if ($repository) { + $repository_phid = $repository->getPHID(); + $repository_name = $repository->getDisplayName(); + } else { + $repository_phid = null; + $repository_name = null; + } + + $puller_phid = $event->getPullerPHID(); + if ($puller_phid) { + $puller_name = $handles[$puller_phid]->getName(); + } else { + $puller_name = null; + } + + $export[] = array( + 'id' => $event->getID(), + 'phid' => $event->getPHID(), + 'repositoryPHID' => $repository_phid, + 'repository' => $repository_name, + 'pullerPHID' => $puller_phid, + 'puller' => $puller_name, + 'protocol' => $event->getRemoteProtocol(), + 'result' => $event->getResultType(), + 'code' => $event->getResultCode(), + 'date' => $event->getEpoch(), + ); + } + + return $export; + } + + protected function getURI($path) { + return '/diffusion/pulllog/'.$path; + } + + protected function getBuiltinQueryNames() { + return array( + 'all' => pht('All Pull Logs'), + ); + } + + public function buildSavedQueryFromBuiltin($query_key) { + $query = $this->newSavedQuery(); + $query->setQueryKey($query_key); + + switch ($query_key) { + case 'all': + return $query; + } + + return parent::buildSavedQueryFromBuiltin($query_key); + } + + protected function renderResultList( + array $logs, + PhabricatorSavedQuery $query, + array $handles) { + + $table = id(new DiffusionPullLogListView()) + ->setViewer($this->requireViewer()) + ->setLogs($logs); + + return id(new PhabricatorApplicationSearchResultView()) + ->setTable($table); + } + +} diff --git a/src/applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php index 76f8d3c837..91c8238718 100644 --- a/src/applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php +++ b/src/applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php @@ -15,7 +15,7 @@ final class DiffusionGitReceivePackSSHWorkflow extends DiffusionGitSSHWorkflow { protected function executeRepositoryOperations() { $repository = $this->getRepository(); - $viewer = $this->getUser(); + $viewer = $this->getSSHUser(); $device = AlmanacKeys::getLiveDevice(); // This is a write, and must have write access. diff --git a/src/applications/diffusion/ssh/DiffusionGitUploadPackSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionGitUploadPackSSHWorkflow.php index 754bafd850..5ed42ba79d 100644 --- a/src/applications/diffusion/ssh/DiffusionGitUploadPackSSHWorkflow.php +++ b/src/applications/diffusion/ssh/DiffusionGitUploadPackSSHWorkflow.php @@ -15,7 +15,7 @@ final class DiffusionGitUploadPackSSHWorkflow extends DiffusionGitSSHWorkflow { protected function executeRepositoryOperations() { $repository = $this->getRepository(); - $viewer = $this->getUser(); + $viewer = $this->getSSHUser(); $device = AlmanacKeys::getLiveDevice(); $skip_sync = $this->shouldSkipReadSynchronization(); @@ -61,11 +61,11 @@ final class DiffusionGitUploadPackSSHWorkflow extends DiffusionGitSSHWorkflow { if ($err) { $pull_event - ->setResultType('error') + ->setResultType(PhabricatorRepositoryPullEvent::RESULT_ERROR) ->setResultCode($err); } else { $pull_event - ->setResultType('pull') + ->setResultType(PhabricatorRepositoryPullEvent::RESULT_PULL) ->setResultCode(0); } diff --git a/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php index 0e5ad7bbe1..e40d8e1f51 100644 --- a/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php +++ b/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php @@ -26,7 +26,7 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow { public function getEnvironment() { $env = array( - DiffusionCommitHookEngine::ENV_USER => $this->getUser()->getUsername(), + DiffusionCommitHookEngine::ENV_USER => $this->getSSHUser()->getUsername(), DiffusionCommitHookEngine::ENV_REMOTE_PROTOCOL => 'ssh', ); @@ -122,14 +122,14 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow { $key_path, $port, $host, - '@'.$this->getUser()->getUsername(), + '@'.$this->getSSHUser()->getUsername(), $this->getOriginalArguments()); } final public function execute(PhutilArgumentParser $args) { $this->args = $args; - $viewer = $this->getUser(); + $viewer = $this->getSSHUser(); $have_diffusion = PhabricatorApplication::isClassInstalledForViewer( 'PhabricatorDiffusionApplication', $viewer); @@ -164,7 +164,7 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow { } protected function loadRepositoryWithPath($path, $vcs) { - $viewer = $this->getUser(); + $viewer = $this->getSSHUser(); $info = PhabricatorRepository::parseRepositoryServicePath($path, $vcs); if ($info === null) { @@ -214,7 +214,7 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow { } $repository = $this->getRepository(); - $viewer = $this->getUser(); + $viewer = $this->getSSHUser(); if ($viewer->isOmnipotent()) { throw new Exception( @@ -252,7 +252,7 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow { } protected function shouldSkipReadSynchronization() { - $viewer = $this->getUser(); + $viewer = $this->getSSHUser(); // Currently, the only case where devices interact over SSH without // assuming user credentials is when synchronizing before a read. These @@ -265,14 +265,14 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow { } protected function newPullEvent() { - $viewer = $this->getViewer(); + $viewer = $this->getSSHUser(); $repository = $this->getRepository(); $remote_address = $this->getSSHRemoteAddress(); return id(new PhabricatorRepositoryPullEvent()) ->setEpoch(PhabricatorTime::getNow()) ->setRemoteAddress($remote_address) - ->setRemoteProtocol('ssh') + ->setRemoteProtocol(PhabricatorRepositoryPullEvent::PROTOCOL_SSH) ->setPullerPHID($viewer->getPHID()) ->setRepositoryPHID($repository->getPHID()); } diff --git a/src/applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php index f2a932f2bb..0df4aa17fe 100644 --- a/src/applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php +++ b/src/applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php @@ -154,7 +154,7 @@ final class DiffusionSubversionServeSSHWorkflow } else { $command = csprintf( 'svnserve -t --tunnel-user=%s', - $this->getUser()->getUsername()); + $this->getSSHUser()->getUsername()); $cwd = PhabricatorEnv::getEmptyCWD(); } diff --git a/src/applications/diffusion/symbol/DiffusionExternalSymbolQuery.php b/src/applications/diffusion/symbol/DiffusionExternalSymbolQuery.php index d32f1b3f64..719496dcdf 100644 --- a/src/applications/diffusion/symbol/DiffusionExternalSymbolQuery.php +++ b/src/applications/diffusion/symbol/DiffusionExternalSymbolQuery.php @@ -1,45 +1,93 @@ languages = $languages; return $this; } + public function withTypes(array $types) { $this->types = $types; return $this; } + public function withNames(array $names) { $this->names = $names; return $this; } + public function withContexts(array $contexts) { $this->contexts = $contexts; return $this; } + public function withPaths(array $paths) { + $this->paths = $paths; + return $this; + } + + public function withLines(array $lines) { + $this->lines = $lines; + return $this; + } + + public function withCharacterPositions(array $positions) { + $this->characterPositions = $positions; + return $this; + } + + public function withRepositories(array $repositories) { + assert_instances_of($repositories, 'PhabricatorRepository'); + $this->repositories = $repositories; + return $this; + } public function getLanguages() { return $this->languages; } + public function getTypes() { return $this->types; } + public function getNames() { return $this->names; } + public function getContexts() { return $this->contexts; } + public function getPaths() { + return $this->paths; + } + + public function getLines() { + return $this->lines; + } + + public function getRepositories() { + return $this->repositories; + } + + public function getCharacterPositions() { + return $this->characterPositions; + } + public function matchesAnyLanguage(array $languages) { return (!$this->languages) || array_intersect($languages, $this->languages); } + public function matchesAnyType(array $types) { return (!$this->types) || array_intersect($types, $this->types); } diff --git a/src/applications/diffusion/view/DiffusionPullLogListView.php b/src/applications/diffusion/view/DiffusionPullLogListView.php new file mode 100644 index 0000000000..f2e3280eba --- /dev/null +++ b/src/applications/diffusion/view/DiffusionPullLogListView.php @@ -0,0 +1,115 @@ +logs = $logs; + return $this; + } + + public function render() { + $events = $this->logs; + $viewer = $this->getViewer(); + + $handle_phids = array(); + foreach ($events as $event) { + if ($event->getPullerPHID()) { + $handle_phids[] = $event->getPullerPHID(); + } + } + $handles = $viewer->loadHandles($handle_phids); + + // Figure out which repositories are editable. We only let you see remote + // IPs if you have edit capability on a repository. + $editable_repos = array(); + if ($events) { + $editable_repos = id(new PhabricatorRepositoryQuery()) + ->setViewer($viewer) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->withPHIDs(mpull($events, 'getRepositoryPHID')) + ->execute(); + $editable_repos = mpull($editable_repos, null, 'getPHID'); + } + + $rows = array(); + $any_host = false; + foreach ($events as $event) { + if ($event->getRepositoryPHID()) { + $repository = $event->getRepository(); + } else { + $repository = null; + } + + // Reveal this if it's valid and the user can edit the repository. For + // invalid requests you currently have to go fishing in the database. + $remote_address = '-'; + if ($repository) { + if (isset($editable_repos[$event->getRepositoryPHID()])) { + $remote_address = $event->getRemoteAddress(); + } + } + + $event_id = $event->getID(); + + $repository_link = null; + if ($repository) { + $repository_link = phutil_tag( + 'a', + array( + 'href' => $repository->getURI(), + ), + $repository->getDisplayName()); + } + + $puller_link = null; + if ($event->getPullerPHID()) { + $puller_link = $viewer->renderHandle($event->getPullerPHID()); + } + + $rows[] = array( + $event_id, + $repository_link, + $puller_link, + $remote_address, + $event->getRemoteProtocolDisplayName(), + $event->newResultIcon(), + $event->getResultCode(), + phabricator_datetime($event->getEpoch(), $viewer), + ); + } + + $table = id(new AphrontTableView($rows)) + ->setHeaders( + array( + pht('Pull'), + pht('Repository'), + pht('Puller'), + pht('From'), + pht('Via'), + null, + pht('Code'), + pht('Date'), + )) + ->setColumnClasses( + array( + 'n', + '', + '', + 'n', + 'wide', + '', + 'n', + 'right', + )); + + return $table; + } + +} diff --git a/src/applications/files/uploadsource/PhabricatorFileUploadSource.php b/src/applications/files/uploadsource/PhabricatorFileUploadSource.php index bbd93a455a..bf213a417e 100644 --- a/src/applications/files/uploadsource/PhabricatorFileUploadSource.php +++ b/src/applications/files/uploadsource/PhabricatorFileUploadSource.php @@ -12,6 +12,8 @@ abstract class PhabricatorFileUploadSource private $shouldChunk; private $didRewind; private $totalBytesWritten = 0; + private $totalBytesRead = 0; + private $byteLimit = 0; public function setName($name) { $this->name = $name; @@ -40,6 +42,15 @@ abstract class PhabricatorFileUploadSource return $this->viewPolicy; } + public function setByteLimit($byte_limit) { + $this->byteLimit = $byte_limit; + return $this; + } + + public function getByteLimit() { + return $this->byteLimit; + } + public function uploadFile() { if (!$this->shouldChunkFile()) { return $this->writeSingleFile(); @@ -81,8 +92,15 @@ abstract class PhabricatorFileUploadSource return false; } + $read_bytes = $data->current(); + $this->totalBytesRead += strlen($read_bytes); + + if ($this->byteLimit && ($this->totalBytesRead > $this->byteLimit)) { + throw new PhabricatorFileUploadSourceByteLimitException(); + } + $rope = $this->getRope(); - $rope->append($data->current()); + $rope->append($read_bytes); return true; } @@ -160,8 +178,10 @@ abstract class PhabricatorFileUploadSource } } - // If we have extra bytes at the end, write them. - if ($rope->getByteLength()) { + // If we have extra bytes at the end, write them. Note that it's possible + // that we have more than one chunk of bytes left if the read was very + // fast. + while ($rope->getByteLength()) { $this->writeChunk($file, $engine); } diff --git a/src/applications/files/uploadsource/PhabricatorFileUploadSourceByteLimitException.php b/src/applications/files/uploadsource/PhabricatorFileUploadSourceByteLimitException.php new file mode 100644 index 0000000000..d78c2189b6 --- /dev/null +++ b/src/applications/files/uploadsource/PhabricatorFileUploadSourceByteLimitException.php @@ -0,0 +1,4 @@ +emailPHIDs); @@ -55,10 +56,29 @@ abstract class HeraldAdapter extends Phobject { return $this; } + public function setViewer(PhabricatorUser $viewer) { + $this->viewer = $viewer; + return $this; + } + + public function getViewer() { + // See PHI276. Normally, Herald runs without regard for policy checks. + // However, we use a real viewer during test console runs: this makes + // intracluster calls to Diffusion APIs work even if web nodes don't + // have privileged credentials. + + if ($this->viewer) { + return $this->viewer; + } + + return PhabricatorUser::getOmnipotentUser(); + } + public function setContentSource(PhabricatorContentSource $content_source) { $this->contentSource = $content_source; return $this; } + public function getContentSource() { return $this->contentSource; } @@ -764,9 +784,20 @@ abstract class HeraldAdapter extends Phobject { public function getRepetitionOptions() { - return array( - HeraldRepetitionPolicyConfig::EVERY, - ); + $options = array(); + + $options[] = HeraldRule::REPEAT_EVERY; + + // Some rules, like pre-commit rules, only ever fire once. It doesn't + // make sense to use state-based repetition policies like "only the first + // time" for these rules. + + if (!$this->isSingleEventAdapter()) { + $options[] = HeraldRule::REPEAT_FIRST; + $options[] = HeraldRule::REPEAT_CHANGE; + } + + return $options; } protected function initializeNewAdapter() { @@ -887,15 +918,15 @@ abstract class HeraldAdapter extends Phobject { )); } - $integer_code_for_every = HeraldRepetitionPolicyConfig::toInt( - HeraldRepetitionPolicyConfig::EVERY); - - if ($rule->getRepetitionPolicy() == $integer_code_for_every) { - $action_text = - pht('Take these actions every time this rule matches:'); + if ($rule->isRepeatFirst()) { + $action_text = pht( + 'Take these actions the first time this rule matches:'); + } else if ($rule->isRepeatOnChange()) { + $action_text = pht( + 'Take these actions if this rule did not match the last time:'); } else { - $action_text = - pht('Take these actions the first time this rule matches:'); + $action_text = pht( + 'Take these actions every time this rule matches:'); } $action_title = phutil_tag( diff --git a/src/applications/herald/config/HeraldRepetitionPolicyConfig.php b/src/applications/herald/config/HeraldRepetitionPolicyConfig.php deleted file mode 100644 index 40e84f0ae4..0000000000 --- a/src/applications/herald/config/HeraldRepetitionPolicyConfig.php +++ /dev/null @@ -1,28 +0,0 @@ - 0, - self::EVERY => 1, - ); - - public static function getMap() { - return array( - self::EVERY => pht('every time'), - self::FIRST => pht('only the first time'), - ); - } - - public static function toInt($str) { - return idx(self::$policyIntMap, $str, self::$policyIntMap[self::EVERY]); - } - - public static function toString($int) { - return idx(array_flip(self::$policyIntMap), $int, self::EVERY); - } - -} diff --git a/src/applications/herald/controller/HeraldRuleController.php b/src/applications/herald/controller/HeraldRuleController.php index eaf352a0a7..c61c29e90e 100644 --- a/src/applications/herald/controller/HeraldRuleController.php +++ b/src/applications/herald/controller/HeraldRuleController.php @@ -218,7 +218,7 @@ final class HeraldRuleController extends HeraldController { ), pht('New Action'))) ->setDescription(pht( - 'Take these actions %s this rule matches:', + 'Take these actions %s', $repetition_selector)) ->setContent(javelin_tag( 'table', @@ -373,8 +373,7 @@ final class HeraldRuleController extends HeraldController { // mutate current rule, so it would be sent to the client in the right state $rule->setMustMatchAll((int)$match_all); $rule->setName($new_name); - $rule->setRepetitionPolicy( - HeraldRepetitionPolicyConfig::toInt($repetition_policy_param)); + $rule->setRepetitionPolicyStringConstant($repetition_policy_param); $rule->attachConditions($conditions); $rule->attachActions($actions); @@ -594,11 +593,10 @@ final class HeraldRuleController extends HeraldController { * time) this rule matches..." element. */ private function renderRepetitionSelector($rule, HeraldAdapter $adapter) { - $repetition_policy = HeraldRepetitionPolicyConfig::toString( - $rule->getRepetitionPolicy()); + $repetition_policy = $rule->getRepetitionPolicyStringConstant(); $repetition_options = $adapter->getRepetitionOptions(); - $repetition_names = HeraldRepetitionPolicyConfig::getMap(); + $repetition_names = HeraldRule::getRepetitionPolicySelectOptionMap(); $repetition_map = array_select_keys($repetition_names, $repetition_options); if (count($repetition_map) < 2) { diff --git a/src/applications/herald/controller/HeraldTestConsoleController.php b/src/applications/herald/controller/HeraldTestConsoleController.php index 21bedcd848..8a7a94963d 100644 --- a/src/applications/herald/controller/HeraldTestConsoleController.php +++ b/src/applications/herald/controller/HeraldTestConsoleController.php @@ -39,7 +39,9 @@ final class HeraldTestConsoleController extends HeraldController { $object = $this->getTestObject(); $adapter = $this->getTestAdapter(); - $adapter->setIsNewObject(false); + $adapter + ->setIsNewObject(false) + ->setViewer($viewer); $rules = id(new HeraldRuleQuery()) ->setViewer($viewer) diff --git a/src/applications/herald/editor/HeraldRuleEditor.php b/src/applications/herald/editor/HeraldRuleEditor.php index de9bb01ef2..30480108f4 100644 --- a/src/applications/herald/editor/HeraldRuleEditor.php +++ b/src/applications/herald/editor/HeraldRuleEditor.php @@ -66,8 +66,10 @@ final class HeraldRuleEditor $object->setMustMatchAll((int)$new_state['match_all']); $object->attachConditions($new_state['conditions']); $object->attachActions($new_state['actions']); - $object->setRepetitionPolicy( - HeraldRepetitionPolicyConfig::toInt($new_state['repetition_policy'])); + + $new_repetition = $new_state['repetition_policy']; + $object->setRepetitionPolicyStringConstant($new_repetition); + return $object; } diff --git a/src/applications/herald/editor/HeraldRuleSerializer.php b/src/applications/herald/editor/HeraldRuleSerializer.php index d1b2527aaa..cf045fc9bf 100644 --- a/src/applications/herald/editor/HeraldRuleSerializer.php +++ b/src/applications/herald/editor/HeraldRuleSerializer.php @@ -9,7 +9,7 @@ final class HeraldRuleSerializer extends Phobject { (bool)$rule->getMustMatchAll(), $rule->getConditions(), $rule->getActions(), - HeraldRepetitionPolicyConfig::toString($rule->getRepetitionPolicy())); + $rule->getRepetitionPolicyStringConstant()); } public function serializeRuleComponents( diff --git a/src/applications/herald/engine/HeraldEngine.php b/src/applications/herald/engine/HeraldEngine.php index b8e1db5626..739e83e4e8 100644 --- a/src/applications/herald/engine/HeraldEngine.php +++ b/src/applications/herald/engine/HeraldEngine.php @@ -14,6 +14,7 @@ final class HeraldEngine extends Phobject { private $forbiddenFields = array(); private $forbiddenActions = array(); + private $skipEffects = array(); public function setDryRun($dry_run) { $this->dryRun = $dry_run; @@ -68,9 +69,7 @@ final class HeraldEngine extends Phobject { foreach ($rules as $phid => $rule) { $this->stack = array(); - $policy_first = HeraldRepetitionPolicyConfig::FIRST; - $policy_first_int = HeraldRepetitionPolicyConfig::toInt($policy_first); - $is_first_only = ($rule->getRepetitionPolicy() == $policy_first_int); + $is_first_only = $rule->isRepeatFirst(); try { if (!$this->getDryRun() && @@ -173,15 +172,31 @@ final class HeraldEngine extends Phobject { return; } - $rules = mpull($rules, null, 'getID'); - $applied_ids = array(); - $first_policy = HeraldRepetitionPolicyConfig::toInt( - HeraldRepetitionPolicyConfig::FIRST); + // Update the "applied" state table. How this table works depends on the + // repetition policy for the rule. + // + // REPEAT_EVERY: We delete existing rows for the rule, then write nothing. + // This policy doesn't use any state. + // + // REPEAT_FIRST: We keep existing rows, then write additional rows for + // rules which fired. This policy accumulates state over the life of the + // object. + // + // REPEAT_CHANGE: We delete existing rows, then write all the rows which + // matched. This policy only uses the state from the previous run. - // Mark all the rules that have had their effects applied as having been - // executed for the current object. + $rules = mpull($rules, null, 'getID'); $rule_ids = mpull($xscripts, 'getRuleID'); + $delete_ids = array(); + foreach ($rules as $rule_id => $rule) { + if ($rule->isRepeatFirst()) { + continue; + } + $delete_ids[] = $rule_id; + } + + $applied_ids = array(); foreach ($rule_ids as $rule_id) { if (!$rule_id) { // Some apply transcripts are purely informational and not associated @@ -194,26 +209,44 @@ final class HeraldEngine extends Phobject { continue; } - if ($rule->getRepetitionPolicy() == $first_policy) { + if ($rule->isRepeatFirst() || $rule->isRepeatOnChange()) { $applied_ids[] = $rule_id; } } - if ($applied_ids) { + // Also include "only if this rule did not match the last time" rules + // which matched but were skipped in the "applied" list. + foreach ($this->skipEffects as $rule_id => $ignored) { + $applied_ids[] = $rule_id; + } + + if ($delete_ids || $applied_ids) { $conn_w = id(new HeraldRule())->establishConnection('w'); - $sql = array(); - foreach ($applied_ids as $id) { - $sql[] = qsprintf( + + if ($delete_ids) { + queryfx( $conn_w, - '(%s, %d)', + 'DELETE FROM %T WHERE phid = %s AND ruleID IN (%Ld)', + HeraldRule::TABLE_RULE_APPLIED, $adapter->getPHID(), - $id); + $delete_ids); + } + + if ($applied_ids) { + $sql = array(); + foreach ($applied_ids as $id) { + $sql[] = qsprintf( + $conn_w, + '(%s, %d)', + $adapter->getPHID(), + $id); + } + queryfx( + $conn_w, + 'INSERT IGNORE INTO %T (phid, ruleID) VALUES %Q', + HeraldRule::TABLE_RULE_APPLIED, + implode(', ', $sql)); } - queryfx( - $conn_w, - 'INSERT IGNORE INTO %T (phid, ruleID) VALUES %Q', - HeraldRule::TABLE_RULE_APPLIED, - implode(', ', $sql)); } } @@ -315,6 +348,30 @@ final class HeraldEngine extends Phobject { } } + // If this rule matched, and is set to run "if it did not match the last + // time", and we matched the last time, we're going to return a match in + // the transcript but set a flag so we don't actually apply any effects. + + // We need the rule to match so that storage gets updated properly. If we + // just pretend the rule didn't match it won't cause any effects (which + // is correct), but it also won't set the "it matched" flag in storage, + // so the next run after this one would incorrectly trigger again. + + $is_dry_run = $this->getDryRun(); + if ($result && !$is_dry_run) { + $is_on_change = $rule->isRepeatOnChange(); + if ($is_on_change) { + $did_apply = $rule->getRuleApplied($object->getPHID()); + if ($did_apply) { + $reason = pht( + 'This rule matched, but did not take any actions because it '. + 'is configured to act only if it did not match the last time.'); + + $this->skipEffects[$rule->getID()] = true; + } + } + } + $this->newRuleTranscript($rule) ->setResult($result) ->setReason($reason); @@ -367,6 +424,11 @@ final class HeraldEngine extends Phobject { HeraldRule $rule, HeraldAdapter $object) { + $rule_id = $rule->getID(); + if (isset($this->skipEffects[$rule_id])) { + return array(); + } + $effects = array(); foreach ($rule->getActions() as $action) { $effect = id(new HeraldEffect()) diff --git a/src/applications/herald/field/HeraldDeprecatedFieldGroup.php b/src/applications/herald/field/HeraldDeprecatedFieldGroup.php new file mode 100644 index 0000000000..2b3bd5835c --- /dev/null +++ b/src/applications/herald/field/HeraldDeprecatedFieldGroup.php @@ -0,0 +1,15 @@ + pht( + 'This change applied silently, so mail and other notifications '. + 'will not be sent.'), + ); + + return idx($reasons, $reason); + } + +} diff --git a/src/applications/herald/storage/HeraldRule.php b/src/applications/herald/storage/HeraldRule.php index cf00e046b7..44ef68ac03 100644 --- a/src/applications/herald/storage/HeraldRule.php +++ b/src/applications/herald/storage/HeraldRule.php @@ -30,6 +30,10 @@ final class HeraldRule extends HeraldDAO private $actions; private $triggerObject = self::ATTACHABLE; + const REPEAT_EVERY = 'every'; + const REPEAT_FIRST = 'first'; + const REPEAT_CHANGE = 'change'; + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, @@ -38,13 +42,10 @@ final class HeraldRule extends HeraldDAO 'contentType' => 'text255', 'mustMatchAll' => 'bool', 'configVersion' => 'uint32', + 'repetitionPolicy' => 'text32', 'ruleType' => 'text32', 'isDisabled' => 'uint32', 'triggerObjectPHID' => 'phid?', - - // T6203/NULLABILITY - // This should not be nullable. - 'repetitionPolicy' => 'uint32?', ), self::CONFIG_KEY_SCHEMA => array( 'key_name' => array( @@ -254,6 +255,58 @@ final class HeraldRule extends HeraldDAO } +/* -( Repetition Policies )------------------------------------------------ */ + + + public function getRepetitionPolicyStringConstant() { + return $this->getRepetitionPolicy(); + } + + public function setRepetitionPolicyStringConstant($value) { + $map = self::getRepetitionPolicyMap(); + + if (!isset($map[$value])) { + throw new Exception( + pht( + 'Rule repetition string constant "%s" is unknown.', + $value)); + } + + return $this->setRepetitionPolicy($value); + } + + public function isRepeatEvery() { + return ($this->getRepetitionPolicyStringConstant() === self::REPEAT_EVERY); + } + + public function isRepeatFirst() { + return ($this->getRepetitionPolicyStringConstant() === self::REPEAT_FIRST); + } + + public function isRepeatOnChange() { + return ($this->getRepetitionPolicyStringConstant() === self::REPEAT_CHANGE); + } + + public static function getRepetitionPolicySelectOptionMap() { + $map = self::getRepetitionPolicyMap(); + return ipull($map, 'select'); + } + + private static function getRepetitionPolicyMap() { + return array( + self::REPEAT_EVERY => array( + 'select' => pht('every time this rule matches:'), + ), + self::REPEAT_FIRST => array( + 'select' => pht('only the first time this rule matches:'), + ), + self::REPEAT_CHANGE => array( + 'select' => pht('if this rule did not match the last time:'), + ), + ); + } + + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/applications/herald/value/HeraldTokenizerFieldValue.php b/src/applications/herald/value/HeraldTokenizerFieldValue.php index bb3ceef1e7..215df29693 100644 --- a/src/applications/herald/value/HeraldTokenizerFieldValue.php +++ b/src/applications/herald/value/HeraldTokenizerFieldValue.php @@ -58,6 +58,7 @@ final class HeraldTokenizerFieldValue 'datasourceURI' => $datasource->getDatasourceURI(), 'browseURI' => $datasource->getBrowseURI(), 'placeholder' => $datasource->getPlaceholderText(), + 'limit' => $datasource->getLimit(), ), ); } diff --git a/src/applications/maniphest/application/PhabricatorManiphestApplication.php b/src/applications/maniphest/application/PhabricatorManiphestApplication.php index 3076354599..6e4ac0a8f6 100644 --- a/src/applications/maniphest/application/PhabricatorManiphestApplication.php +++ b/src/applications/maniphest/application/PhabricatorManiphestApplication.php @@ -52,7 +52,7 @@ final class PhabricatorManiphestApplication extends PhabricatorApplication { '/maniphest/' => array( '(?:query/(?P[^/]+)/)?' => 'ManiphestTaskListController', 'report/(?:(?P\w+)/)?' => 'ManiphestReportController', - 'batch/' => 'ManiphestBatchEditController', + $this->getBulkRoutePattern('bulk/') => 'ManiphestBulkEditController', 'task/' => array( $this->getEditRoutePattern('edit/') => 'ManiphestTaskEditController', diff --git a/src/applications/maniphest/bulk/ManiphestTaskBulkEngine.php b/src/applications/maniphest/bulk/ManiphestTaskBulkEngine.php new file mode 100644 index 0000000000..44f575eefe --- /dev/null +++ b/src/applications/maniphest/bulk/ManiphestTaskBulkEngine.php @@ -0,0 +1,54 @@ +workboard = $workboard; + return $this; + } + + public function getWorkboard() { + return $this->workboard; + } + + public function newSearchEngine() { + return new ManiphestTaskSearchEngine(); + } + + public function newEditEngine() { + return new ManiphestEditEngine(); + } + + public function getDoneURI() { + $board_uri = $this->getBoardURI(); + if ($board_uri) { + return $board_uri; + } + + return parent::getDoneURI(); + } + + public function getCancelURI() { + $board_uri = $this->getBoardURI(); + if ($board_uri) { + return $board_uri; + } + + return parent::getCancelURI(); + } + + private function getBoardURI() { + $workboard = $this->getWorkboard(); + + if ($workboard) { + $project_id = $workboard->getID(); + return "/project/board/{$project_id}/"; + } + + return null; + } + +} diff --git a/src/applications/maniphest/bulk/ManiphestTaskEditBulkJobType.php b/src/applications/maniphest/bulk/ManiphestTaskEditBulkJobType.php deleted file mode 100644 index 1b083d88f9..0000000000 --- a/src/applications/maniphest/bulk/ManiphestTaskEditBulkJobType.php +++ /dev/null @@ -1,303 +0,0 @@ -getSize())); - } - - public function getJobSize(PhabricatorWorkerBulkJob $job) { - return count($job->getParameter('taskPHIDs', array())); - } - - public function getDoneURI(PhabricatorWorkerBulkJob $job) { - return $job->getParameter('doneURI'); - } - - public function createTasks(PhabricatorWorkerBulkJob $job) { - $tasks = array(); - - foreach ($job->getParameter('taskPHIDs', array()) as $phid) { - $tasks[] = PhabricatorWorkerBulkTask::initializeNewTask($job, $phid); - } - - return $tasks; - } - - public function runTask( - PhabricatorUser $actor, - PhabricatorWorkerBulkJob $job, - PhabricatorWorkerBulkTask $task) { - - $object = id(new ManiphestTaskQuery()) - ->setViewer($actor) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->withPHIDs(array($task->getObjectPHID())) - ->needProjectPHIDs(true) - ->needSubscriberPHIDs(true) - ->executeOne(); - if (!$object) { - return; - } - - $field_list = PhabricatorCustomField::getObjectFields( - $object, - PhabricatorCustomField::ROLE_EDIT); - $field_list->readFieldsFromStorage($object); - - $actions = $job->getParameter('actions'); - $xactions = $this->buildTransactions($actions, $object); - - $editor = id(new ManiphestTransactionEditor()) - ->setActor($actor) - ->setContentSource($job->newContentSource()) - ->setContinueOnNoEffect(true) - ->setContinueOnMissingFields(true) - ->applyTransactions($object, $xactions); - } - - private function buildTransactions($actions, ManiphestTask $task) { - $value_map = array(); - $type_map = array( - 'add_comment' => PhabricatorTransactions::TYPE_COMMENT, - 'assign' => ManiphestTaskOwnerTransaction::TRANSACTIONTYPE, - 'status' => ManiphestTaskStatusTransaction::TRANSACTIONTYPE, - 'priority' => ManiphestTaskPriorityTransaction::TRANSACTIONTYPE, - 'add_project' => PhabricatorTransactions::TYPE_EDGE, - 'remove_project' => PhabricatorTransactions::TYPE_EDGE, - 'add_ccs' => PhabricatorTransactions::TYPE_SUBSCRIBERS, - 'remove_ccs' => PhabricatorTransactions::TYPE_SUBSCRIBERS, - 'space' => PhabricatorTransactions::TYPE_SPACE, - ); - - $edge_edit_types = array( - 'add_project' => true, - 'remove_project' => true, - 'add_ccs' => true, - 'remove_ccs' => true, - ); - - $xactions = array(); - foreach ($actions as $action) { - if (empty($type_map[$action['action']])) { - throw new Exception(pht("Unknown batch edit action '%s'!", $action)); - } - - $type = $type_map[$action['action']]; - - // Figure out the current value, possibly after modifications by other - // batch actions of the same type. For example, if the user chooses to - // "Add Comment" twice, we should add both comments. More notably, if the - // user chooses "Remove Project..." and also "Add Project...", we should - // avoid restoring the removed project in the second transaction. - - if (array_key_exists($type, $value_map)) { - $current = $value_map[$type]; - } else { - switch ($type) { - case PhabricatorTransactions::TYPE_COMMENT: - $current = null; - break; - case ManiphestTaskOwnerTransaction::TRANSACTIONTYPE: - $current = $task->getOwnerPHID(); - break; - case ManiphestTaskStatusTransaction::TRANSACTIONTYPE: - $current = $task->getStatus(); - break; - case ManiphestTaskPriorityTransaction::TRANSACTIONTYPE: - $current = $task->getPriority(); - break; - case PhabricatorTransactions::TYPE_EDGE: - $current = $task->getProjectPHIDs(); - break; - case PhabricatorTransactions::TYPE_SUBSCRIBERS: - $current = $task->getSubscriberPHIDs(); - break; - case PhabricatorTransactions::TYPE_SPACE: - $current = PhabricatorSpacesNamespaceQuery::getObjectSpacePHID( - $task); - break; - } - } - - // Check if the value is meaningful / provided, and normalize it if - // necessary. This discards, e.g., empty comments and empty owner - // changes. - - $value = $action['value']; - switch ($type) { - case PhabricatorTransactions::TYPE_COMMENT: - if (!strlen($value)) { - continue 2; - } - break; - case PhabricatorTransactions::TYPE_SPACE: - if (empty($value)) { - continue 2; - } - $value = head($value); - break; - case ManiphestTaskOwnerTransaction::TRANSACTIONTYPE: - if (empty($value)) { - continue 2; - } - $value = head($value); - $no_owner = PhabricatorPeopleNoOwnerDatasource::FUNCTION_TOKEN; - if ($value === $no_owner) { - $value = null; - } - break; - case PhabricatorTransactions::TYPE_EDGE: - if (empty($value)) { - continue 2; - } - break; - case PhabricatorTransactions::TYPE_SUBSCRIBERS: - if (empty($value)) { - continue 2; - } - break; - } - - // If the edit doesn't change anything, go to the next action. This - // check is only valid for changes like "owner", "status", etc, not - // for edge edits, because we should still apply an edit like - // "Remove Projects: A, B" to a task with projects "A, B". - - if (empty($edge_edit_types[$action['action']])) { - if ($value == $current) { - continue; - } - } - - // Apply the value change; for most edits this is just replacement, but - // some need to merge the current and edited values (add/remove project). - - switch ($type) { - case PhabricatorTransactions::TYPE_COMMENT: - if (strlen($current)) { - $value = $current."\n\n".$value; - } - break; - case PhabricatorTransactions::TYPE_EDGE: - $is_remove = $action['action'] == 'remove_project'; - - $current = array_fill_keys($current, true); - $value = array_fill_keys($value, true); - - $new = $current; - $did_something = false; - - if ($is_remove) { - foreach ($value as $phid => $ignored) { - if (isset($new[$phid])) { - unset($new[$phid]); - $did_something = true; - } - } - } else { - foreach ($value as $phid => $ignored) { - if (empty($new[$phid])) { - $new[$phid] = true; - $did_something = true; - } - } - } - - if (!$did_something) { - continue 2; - } - - $value = array_keys($new); - break; - case PhabricatorTransactions::TYPE_SUBSCRIBERS: - $is_remove = $action['action'] == 'remove_ccs'; - - $current = array_fill_keys($current, true); - - $new = array(); - $did_something = false; - - if ($is_remove) { - foreach ($value as $phid) { - if (isset($current[$phid])) { - $new[$phid] = true; - $did_something = true; - } - } - if ($new) { - $value = array('-' => array_keys($new)); - } - } else { - $new = array(); - foreach ($value as $phid) { - $new[$phid] = true; - $did_something = true; - } - if ($new) { - $value = array('+' => array_keys($new)); - } - } - if (!$did_something) { - continue 2; - } - - break; - } - - $value_map[$type] = $value; - } - - $template = new ManiphestTransaction(); - - foreach ($value_map as $type => $value) { - $xaction = clone $template; - $xaction->setTransactionType($type); - - switch ($type) { - case PhabricatorTransactions::TYPE_COMMENT: - $xaction->attachComment( - id(new ManiphestTransactionComment()) - ->setContent($value)); - break; - case PhabricatorTransactions::TYPE_EDGE: - $project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; - $xaction - ->setMetadataValue('edge:type', $project_type) - ->setNewValue( - array( - '=' => array_fuse($value), - )); - break; - case ManiphestTaskPriorityTransaction::TRANSACTIONTYPE: - $keyword_map = ManiphestTaskPriority::getTaskPriorityKeywordsMap(); - $keyword = head(idx($keyword_map, $value)); - $xaction->setNewValue($keyword); - break; - default: - $xaction->setNewValue($value); - break; - } - - $xactions[] = $xaction; - } - - return $xactions; - } -} diff --git a/src/applications/maniphest/controller/ManiphestBatchEditController.php b/src/applications/maniphest/controller/ManiphestBatchEditController.php deleted file mode 100644 index f2ec4b433f..0000000000 --- a/src/applications/maniphest/controller/ManiphestBatchEditController.php +++ /dev/null @@ -1,226 +0,0 @@ -getViewer(); - - $this->requireApplicationCapability( - ManiphestBulkEditCapability::CAPABILITY); - - $project = null; - $board_id = $request->getInt('board'); - if ($board_id) { - $project = id(new PhabricatorProjectQuery()) - ->setViewer($viewer) - ->withIDs(array($board_id)) - ->executeOne(); - if (!$project) { - return new Aphront404Response(); - } - } - - $task_ids = $request->getArr('batch'); - if (!$task_ids) { - $task_ids = $request->getStrList('batch'); - } - - if (!$task_ids) { - throw new Exception( - pht( - 'No tasks are selected.')); - } - - $tasks = id(new ManiphestTaskQuery()) - ->setViewer($viewer) - ->withIDs($task_ids) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->needSubscriberPHIDs(true) - ->needProjectPHIDs(true) - ->execute(); - - if (!$tasks) { - throw new Exception( - pht("You don't have permission to edit any of the selected tasks.")); - } - - if ($project) { - $cancel_uri = '/project/board/'.$project->getID().'/'; - $redirect_uri = $cancel_uri; - } else { - $cancel_uri = '/maniphest/'; - $redirect_uri = '/maniphest/?ids='.implode(',', mpull($tasks, 'getID')); - } - - $actions = $request->getStr('actions'); - if ($actions) { - $actions = phutil_json_decode($actions); - } - - if ($request->isFormPost() && $actions) { - $job = PhabricatorWorkerBulkJob::initializeNewJob( - $viewer, - new ManiphestTaskEditBulkJobType(), - array( - 'taskPHIDs' => mpull($tasks, 'getPHID'), - 'actions' => $actions, - 'cancelURI' => $cancel_uri, - 'doneURI' => $redirect_uri, - )); - - $type_status = PhabricatorWorkerBulkJobTransaction::TYPE_STATUS; - - $xactions = array(); - $xactions[] = id(new PhabricatorWorkerBulkJobTransaction()) - ->setTransactionType($type_status) - ->setNewValue(PhabricatorWorkerBulkJob::STATUS_CONFIRM); - - $editor = id(new PhabricatorWorkerBulkJobEditor()) - ->setActor($viewer) - ->setContentSourceFromRequest($request) - ->setContinueOnMissingFields(true) - ->applyTransactions($job, $xactions); - - return id(new AphrontRedirectResponse()) - ->setURI($job->getMonitorURI()); - } - - $handles = ManiphestTaskListView::loadTaskHandles($viewer, $tasks); - - $list = new ManiphestTaskListView(); - $list->setTasks($tasks); - $list->setUser($viewer); - $list->setHandles($handles); - - $template = new AphrontTokenizerTemplateView(); - $template = $template->render(); - - $projects_source = new PhabricatorProjectDatasource(); - $mailable_source = new PhabricatorMetaMTAMailableDatasource(); - $mailable_source->setViewer($viewer); - $owner_source = new ManiphestAssigneeDatasource(); - $owner_source->setViewer($viewer); - $spaces_source = id(new PhabricatorSpacesNamespaceDatasource()) - ->setViewer($viewer); - - require_celerity_resource('maniphest-batch-editor'); - Javelin::initBehavior( - 'maniphest-batch-editor', - array( - 'root' => 'maniphest-batch-edit-form', - 'tokenizerTemplate' => $template, - 'sources' => array( - 'project' => array( - 'src' => $projects_source->getDatasourceURI(), - 'placeholder' => $projects_source->getPlaceholderText(), - 'browseURI' => $projects_source->getBrowseURI(), - ), - 'owner' => array( - 'src' => $owner_source->getDatasourceURI(), - 'placeholder' => $owner_source->getPlaceholderText(), - 'browseURI' => $owner_source->getBrowseURI(), - 'limit' => 1, - ), - 'cc' => array( - 'src' => $mailable_source->getDatasourceURI(), - 'placeholder' => $mailable_source->getPlaceholderText(), - 'browseURI' => $mailable_source->getBrowseURI(), - ), - 'spaces' => array( - 'src' => $spaces_source->getDatasourceURI(), - 'placeholder' => $spaces_source->getPlaceholderText(), - 'browseURI' => $spaces_source->getBrowseURI(), - 'limit' => 1, - ), - ), - 'input' => 'batch-form-actions', - 'priorityMap' => ManiphestTaskPriority::getTaskPriorityMap(), - 'statusMap' => ManiphestTaskStatus::getTaskStatusMap(), - )); - - $form = id(new AphrontFormView()) - ->setUser($viewer) - ->addHiddenInput('board', $board_id) - ->setID('maniphest-batch-edit-form'); - - foreach ($tasks as $task) { - $form->appendChild( - phutil_tag( - 'input', - array( - 'type' => 'hidden', - 'name' => 'batch[]', - 'value' => $task->getID(), - ))); - } - - $form->appendChild( - phutil_tag( - 'input', - array( - 'type' => 'hidden', - 'name' => 'actions', - 'id' => 'batch-form-actions', - ))); - $form->appendChild( - id(new PHUIFormInsetView()) - ->setTitle(pht('Actions')) - ->setRightButton(javelin_tag( - 'a', - array( - 'href' => '#', - 'class' => 'button button-green', - 'sigil' => 'add-action', - 'mustcapture' => true, - ), - pht('Add Another Action'))) - ->setContent(javelin_tag( - 'table', - array( - 'sigil' => 'maniphest-batch-actions', - 'class' => 'maniphest-batch-actions-table', - ), - ''))) - ->appendChild( - id(new AphrontFormSubmitControl()) - ->setValue(pht('Update Tasks')) - ->addCancelButton($cancel_uri)); - - $title = pht('Batch Editor'); - - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb($title); - $crumbs->setBorder(true); - - $header = id(new PHUIHeaderView()) - ->setHeader(pht('Batch Editor')) - ->setHeaderIcon('fa-pencil-square-o'); - - $task_box = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Selected Tasks')) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->setObjectList($list); - - $form_box = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Actions')) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->setForm($form); - - $view = id(new PHUITwoColumnView()) - ->setHeader($header) - ->setFooter(array( - $task_box, - $form_box, - )); - - return $this->newPage() - ->setTitle($title) - ->setCrumbs($crumbs) - ->appendChild($view); - } - -} diff --git a/src/applications/maniphest/controller/ManiphestBulkEditController.php b/src/applications/maniphest/controller/ManiphestBulkEditController.php new file mode 100644 index 0000000000..b698e54848 --- /dev/null +++ b/src/applications/maniphest/controller/ManiphestBulkEditController.php @@ -0,0 +1,32 @@ +getViewer(); + + $this->requireApplicationCapability( + ManiphestBulkEditCapability::CAPABILITY); + + $bulk_engine = id(new ManiphestTaskBulkEngine()) + ->setViewer($viewer) + ->setController($this) + ->addContextParameter('board'); + + $board_id = $request->getInt('board'); + if ($board_id) { + $project = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withIDs(array($board_id)) + ->executeOne(); + if (!$project) { + return new Aphront404Response(); + } + + $bulk_engine->setWorkboard($project); + } + + return $bulk_engine->buildResponse(); + } + +} diff --git a/src/applications/maniphest/editor/ManiphestEditEngine.php b/src/applications/maniphest/editor/ManiphestEditEngine.php index b4ca86a442..359ba493d4 100644 --- a/src/applications/maniphest/editor/ManiphestEditEngine.php +++ b/src/applications/maniphest/editor/ManiphestEditEngine.php @@ -178,6 +178,7 @@ EODOCS id(new PhabricatorTextEditField()) ->setKey('title') ->setLabel(pht('Title')) + ->setBulkEditLabel(pht('Set title to')) ->setDescription(pht('Name of the task.')) ->setConduitDescription(pht('Rename the task.')) ->setConduitTypeDescription(pht('New task name.')) @@ -188,6 +189,7 @@ EODOCS ->setKey('owner') ->setAliases(array('ownerPHID', 'assign', 'assigned')) ->setLabel(pht('Assigned To')) + ->setBulkEditLabel(pht('Assign to')) ->setDescription(pht('User who is responsible for the task.')) ->setConduitDescription(pht('Reassign the task.')) ->setConduitTypeDescription( @@ -200,6 +202,7 @@ EODOCS id(new PhabricatorSelectEditField()) ->setKey('status') ->setLabel(pht('Status')) + ->setBulkEditLabel(pht('Set status to')) ->setDescription(pht('Status of the task.')) ->setConduitDescription(pht('Change the task status.')) ->setConduitTypeDescription(pht('New task status constant.')) @@ -212,6 +215,7 @@ EODOCS id(new PhabricatorSelectEditField()) ->setKey('priority') ->setLabel(pht('Priority')) + ->setBulkEditLabel(pht('Set priority to')) ->setDescription(pht('Priority of the task.')) ->setConduitDescription(pht('Change the priority of the task.')) ->setConduitTypeDescription(pht('New task priority constant.')) @@ -230,6 +234,7 @@ EODOCS $fields[] = id(new PhabricatorPointsEditField()) ->setKey('points') ->setLabel($points_label) + ->setBulkEditLabel($action_label) ->setDescription(pht('Point value of the task.')) ->setConduitDescription(pht('Change the task point value.')) ->setConduitTypeDescription(pht('New task point value.')) @@ -242,6 +247,7 @@ EODOCS $fields[] = id(new PhabricatorRemarkupEditField()) ->setKey('description') ->setLabel(pht('Description')) + ->setBulkEditLabel(pht('Set description to')) ->setDescription(pht('Task description.')) ->setConduitDescription(pht('Update the task description.')) ->setConduitTypeDescription(pht('New task description.')) diff --git a/src/applications/maniphest/herald/HeraldManiphestTaskAdapter.php b/src/applications/maniphest/herald/HeraldManiphestTaskAdapter.php index 8503839e3e..1aa544de57 100644 --- a/src/applications/maniphest/herald/HeraldManiphestTaskAdapter.php +++ b/src/applications/maniphest/herald/HeraldManiphestTaskAdapter.php @@ -33,13 +33,6 @@ final class HeraldManiphestTaskAdapter extends HeraldAdapter { return true; } - public function getRepetitionOptions() { - return array( - HeraldRepetitionPolicyConfig::EVERY, - HeraldRepetitionPolicyConfig::FIRST, - ); - } - public function supportsRuleType($rule_type) { switch ($rule_type) { case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: diff --git a/src/applications/maniphest/herald/ManiphestTaskAssignOtherHeraldAction.php b/src/applications/maniphest/herald/ManiphestTaskAssignOtherHeraldAction.php index 5a2591058e..27a5477623 100644 --- a/src/applications/maniphest/herald/ManiphestTaskAssignOtherHeraldAction.php +++ b/src/applications/maniphest/herald/ManiphestTaskAssignOtherHeraldAction.php @@ -22,9 +22,8 @@ final class ManiphestTaskAssignOtherHeraldAction } protected function getDatasource() { - // TODO: Eventually, it would be nice to get "limit = 1" exported from here - // up to the UI. - return new ManiphestAssigneeDatasource(); + return id(new ManiphestAssigneeDatasource()) + ->setLimit(1); } public function renderActionDescription($value) { diff --git a/src/applications/maniphest/herald/ManiphestTaskPriorityHeraldAction.php b/src/applications/maniphest/herald/ManiphestTaskPriorityHeraldAction.php index 2e2db4b62d..8fe5e07122 100644 --- a/src/applications/maniphest/herald/ManiphestTaskPriorityHeraldAction.php +++ b/src/applications/maniphest/herald/ManiphestTaskPriorityHeraldAction.php @@ -61,7 +61,8 @@ final class ManiphestTaskPriorityHeraldAction } protected function getDatasource() { - return new ManiphestTaskPriorityDatasource(); + return id(new ManiphestTaskPriorityDatasource()) + ->setLimit(1); } protected function getDatasourceValueMap() { diff --git a/src/applications/maniphest/herald/ManiphestTaskStatusHeraldAction.php b/src/applications/maniphest/herald/ManiphestTaskStatusHeraldAction.php index c1ff3bcdc7..de84b3c245 100644 --- a/src/applications/maniphest/herald/ManiphestTaskStatusHeraldAction.php +++ b/src/applications/maniphest/herald/ManiphestTaskStatusHeraldAction.php @@ -58,7 +58,8 @@ final class ManiphestTaskStatusHeraldAction } protected function getDatasource() { - return new ManiphestTaskStatusDatasource(); + return id(new ManiphestTaskStatusDatasource()) + ->setLimit(1); } protected function getDatasourceValueMap() { diff --git a/src/applications/maniphest/view/ManiphestTaskResultListView.php b/src/applications/maniphest/view/ManiphestTaskResultListView.php index 0144df0e33..b4cf9a544c 100644 --- a/src/applications/maniphest/view/ManiphestTaskResultListView.php +++ b/src/applications/maniphest/view/ManiphestTaskResultListView.php @@ -218,7 +218,7 @@ final class ManiphestTaskResultListView extends ManiphestView { 'disabled' => 'disabled', 'class' => 'disabled', ), - pht("Batch Edit Selected \xC2\xBB")); + pht("Bulk Edit Selected \xC2\xBB")); $export = javelin_tag( 'a', @@ -255,7 +255,7 @@ final class ManiphestTaskResultListView extends ManiphestView { $user, array( 'method' => 'POST', - 'action' => '/maniphest/batch/', + 'action' => '/maniphest/bulk/', 'id' => 'batch-select-form', ), $editor); diff --git a/src/applications/metamta/herald/PhabricatorMailOutboundMailHeraldAdapter.php b/src/applications/metamta/herald/PhabricatorMailOutboundMailHeraldAdapter.php index 6ca797833e..f96a6f86bd 100644 --- a/src/applications/metamta/herald/PhabricatorMailOutboundMailHeraldAdapter.php +++ b/src/applications/metamta/herald/PhabricatorMailOutboundMailHeraldAdapter.php @@ -49,12 +49,6 @@ final class PhabricatorMailOutboundMailHeraldAdapter return true; } - public function getRepetitionOptions() { - return array( - HeraldRepetitionPolicyConfig::FIRST, - ); - } - public function supportsRuleType($rule_type) { switch ($rule_type) { case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: diff --git a/src/applications/people/application/PhabricatorPeopleApplication.php b/src/applications/people/application/PhabricatorPeopleApplication.php index dde82f1d3a..28405ca92c 100644 --- a/src/applications/people/application/PhabricatorPeopleApplication.php +++ b/src/applications/people/application/PhabricatorPeopleApplication.php @@ -41,7 +41,7 @@ final class PhabricatorPeopleApplication extends PhabricatorApplication { public function getRoutes() { return array( '/people/' => array( - '(query/(?P[^/]+)/)?' => 'PhabricatorPeopleListController', + $this->getQueryRoutePattern() => 'PhabricatorPeopleListController', 'logs/(?:query/(?P[^/]+)/)?' => 'PhabricatorPeopleLogsController', 'invite/' => array( @@ -76,7 +76,7 @@ final class PhabricatorPeopleApplication extends PhabricatorApplication { 'PhabricatorPeopleProfilePictureController', 'manage/(?P[1-9]\d*)/' => 'PhabricatorPeopleProfileManageController', - ), + ), '/p/(?P[\w._-]+)/' => array( '' => 'PhabricatorPeopleProfileViewController', 'item/' => $this->getProfileMenuRouting( diff --git a/src/applications/people/controller/PhabricatorPeopleListController.php b/src/applications/people/controller/PhabricatorPeopleListController.php index edcfc7ba0f..511899070c 100644 --- a/src/applications/people/controller/PhabricatorPeopleListController.php +++ b/src/applications/people/controller/PhabricatorPeopleListController.php @@ -16,7 +16,7 @@ final class PhabricatorPeopleListController PeopleBrowseUserDirectoryCapability::CAPABILITY); $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($request->getURIData('key')) + ->setQueryKey($request->getURIData('queryKey')) ->setSearchEngine(new PhabricatorPeopleSearchEngine()) ->setNavigation($this->buildSideNavView()); diff --git a/src/applications/people/editor/PhabricatorUserEditor.php b/src/applications/people/editor/PhabricatorUserEditor.php index 20c9c01370..cda9c1e41b 100644 --- a/src/applications/people/editor/PhabricatorUserEditor.php +++ b/src/applications/people/editor/PhabricatorUserEditor.php @@ -129,33 +129,6 @@ final class PhabricatorUserEditor extends PhabricatorEditor { } - /** - * @task edit - */ - public function changePassword( - PhabricatorUser $user, - PhutilOpaqueEnvelope $envelope) { - - if (!$user->getID()) { - throw new Exception(pht('User has not been created yet!')); - } - - $user->openTransaction(); - $user->reload(); - - $user->setPassword($envelope); - $user->save(); - - $log = PhabricatorUserLog::initializeNewLog( - $this->requireActor(), - $user->getPHID(), - PhabricatorUserLog::ACTION_CHANGE_PASSWORD); - $log->save(); - - $user->saveTransaction(); - } - - /** * @task edit */ diff --git a/src/applications/people/query/PhabricatorPeopleSearchEngine.php b/src/applications/people/query/PhabricatorPeopleSearchEngine.php index 0a4367d367..db2256a8b8 100644 --- a/src/applications/people/query/PhabricatorPeopleSearchEngine.php +++ b/src/applications/people/query/PhabricatorPeopleSearchEngine.php @@ -320,4 +320,41 @@ final class PhabricatorPeopleSearchEngine return $result; } + protected function newExportFields() { + return array( + id(new PhabricatorIDExportField()) + ->setKey('id') + ->setLabel(pht('ID')), + id(new PhabricatorPHIDExportField()) + ->setKey('phid') + ->setLabel(pht('PHID')), + id(new PhabricatorStringExportField()) + ->setKey('username') + ->setLabel(pht('Username')), + id(new PhabricatorStringExportField()) + ->setKey('realName') + ->setLabel(pht('Real Name')), + id(new PhabricatorEpochExportField()) + ->setKey('created') + ->setLabel(pht('Date Created')), + ); + } + + public function newExport(array $users) { + $viewer = $this->requireViewer(); + + $export = array(); + foreach ($users as $user) { + $export[] = array( + 'id' => $user->getID(), + 'phid' => $user->getPHID(), + 'username' => $user->getUsername(), + 'realName' => $user->getRealName(), + 'created' => $user->getDateCreated(), + ); + } + + return $export; + } + } diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php index 30aa3d81ef..6c8b7ac6b5 100644 --- a/src/applications/people/storage/PhabricatorUser.php +++ b/src/applications/people/storage/PhabricatorUser.php @@ -20,7 +20,8 @@ final class PhabricatorUser PhabricatorApplicationTransactionInterface, PhabricatorFulltextInterface, PhabricatorFerretInterface, - PhabricatorConduitResultInterface { + PhabricatorConduitResultInterface, + PhabricatorAuthPasswordHashInterface { const SESSION_TABLE = 'phabricator_session'; const NAMETOKEN_TABLE = 'user_nametoken'; @@ -28,8 +29,6 @@ final class PhabricatorUser protected $userName; protected $realName; - protected $passwordSalt; - protected $passwordHash; protected $profileImagePHID; protected $defaultProfileImagePHID; protected $defaultProfileImageVersion; @@ -216,8 +215,6 @@ final class PhabricatorUser self::CONFIG_COLUMN_SCHEMA => array( 'userName' => 'sort64', 'realName' => 'text128', - 'passwordSalt' => 'text32?', - 'passwordHash' => 'text128?', 'profileImagePHID' => 'phid?', 'conduitCertificate' => 'text255', 'isSystemAgent' => 'bool', @@ -262,28 +259,6 @@ final class PhabricatorUser PhabricatorPeopleUserPHIDType::TYPECONST); } - public function hasPassword() { - return (bool)strlen($this->passwordHash); - } - - public function setPassword(PhutilOpaqueEnvelope $envelope) { - if (!$this->getPHID()) { - throw new Exception( - pht( - 'You can not set a password for an unsaved user because their PHID '. - 'is a salt component in the password hash.')); - } - - if (!strlen($envelope->openEnvelope())) { - $this->setPasswordHash(''); - } else { - $this->setPasswordSalt(md5(Filesystem::readRandomBytes(32))); - $hash = $this->hashPassword($envelope); - $this->setPasswordHash($hash->openEnvelope()); - } - return $this; - } - public function getMonogram() { return '@'.$this->getUsername(); } @@ -331,36 +306,6 @@ final class PhabricatorUser return Filesystem::readRandomCharacters(255); } - public function comparePassword(PhutilOpaqueEnvelope $envelope) { - if (!strlen($envelope->openEnvelope())) { - return false; - } - if (!strlen($this->getPasswordHash())) { - return false; - } - - return PhabricatorPasswordHasher::comparePassword( - $this->getPasswordHashInput($envelope), - new PhutilOpaqueEnvelope($this->getPasswordHash())); - } - - private function getPasswordHashInput(PhutilOpaqueEnvelope $password) { - $input = - $this->getUsername(). - $password->openEnvelope(). - $this->getPHID(). - $this->getPasswordSalt(); - - return new PhutilOpaqueEnvelope($input); - } - - private function hashPassword(PhutilOpaqueEnvelope $password) { - $hasher = PhabricatorPasswordHasher::getBestHasher(); - - $input_envelope = $this->getPasswordHashInput($password); - return $hasher->getPasswordHashForStorage($input_envelope); - } - const CSRF_CYCLE_FREQUENCY = 3600; const CSRF_SALT_LENGTH = 8; const CSRF_TOKEN_LENGTH = 16; @@ -1620,4 +1565,93 @@ final class PhabricatorUser return $variables[$variable_key]; } +/* -( PhabricatorAuthPasswordHashInterface )------------------------------- */ + + + public function newPasswordDigest( + PhutilOpaqueEnvelope $envelope, + PhabricatorAuthPassword $password) { + + // Before passwords are hashed, they are digested. The goal of digestion + // is twofold: to reduce the length of very long passwords to something + // reasonable; and to salt the password in case the best available hasher + // does not include salt automatically. + + // Users may choose arbitrarily long passwords, and attackers may try to + // attack the system by probing it with very long passwords. When large + // inputs are passed to hashers -- which are intentionally slow -- it + // can result in unacceptably long runtimes. The classic attack here is + // to try to log in with a 64MB password and see if that locks up the + // machine for the next century. By digesting passwords to a standard + // length first, the length of the raw input does not impact the runtime + // of the hashing algorithm. + + // Some hashers like bcrypt are self-salting, while other hashers are not. + // Applying salt while digesting passwords ensures that hashes are salted + // whether we ultimately select a self-salting hasher or not. + + // For legacy compatibility reasons, old VCS and Account password digest + // algorithms are significantly more complicated than necessary to achieve + // these goals. This is because they once used a different hashing and + // salting process. When we upgraded to the modern modular hasher + // infrastructure, we just bolted it onto the end of the existing pipelines + // so that upgrading didn't break all users' credentials. + + // New implementations can (and, generally, should) safely select the + // simple HMAC SHA256 digest at the bottom of the function, which does + // everything that a digest callback should without any needless legacy + // baggage on top. + + if ($password->getLegacyDigestFormat() == 'v1') { + switch ($password->getPasswordType()) { + case PhabricatorAuthPassword::PASSWORD_TYPE_VCS: + // Old VCS passwords use an iterated HMAC SHA1 as a digest algorithm. + // They originally used this as a hasher, but it became a digest + // algorithm once hashing was upgraded to include bcrypt. + $digest = $envelope->openEnvelope(); + $salt = $this->getPHID(); + for ($ii = 0; $ii < 1000; $ii++) { + $digest = PhabricatorHash::weakDigest($digest, $salt); + } + return new PhutilOpaqueEnvelope($digest); + case PhabricatorAuthPassword::PASSWORD_TYPE_ACCOUNT: + // Account passwords previously used this weird mess of salt and did + // not digest the input to a standard length. + + // Beyond this being a weird special case, there are two actual + // problems with this, although neither are particularly severe: + + // First, because we do not normalize the length of passwords, this + // algorithm may make us vulnerable to DOS attacks where an attacker + // attempts to use a very long input to slow down hashers. + + // Second, because the username is part of the hash algorithm, + // renaming a user breaks their password. This isn't a huge deal but + // it's pretty silly. There's no security justification for this + // behavior, I just didn't think about the implication when I wrote + // it originally. + + $parts = array( + $this->getUsername(), + $envelope->openEnvelope(), + $this->getPHID(), + $password->getPasswordSalt(), + ); + + return new PhutilOpaqueEnvelope(implode('', $parts)); + } + } + + // For passwords which do not have some crazy legacy reason to use some + // other digest algorithm, HMAC SHA256 is an excellent choice. It satisfies + // the digest requirements and is simple. + + $digest = PhabricatorHash::digestHMACSHA256( + $envelope->openEnvelope(), + $password->getPasswordSalt()); + + return new PhutilOpaqueEnvelope($digest); + } + + } diff --git a/src/applications/phame/herald/HeraldPhameBlogAdapter.php b/src/applications/phame/herald/HeraldPhameBlogAdapter.php index d8956ba4a6..d9368a97d2 100644 --- a/src/applications/phame/herald/HeraldPhameBlogAdapter.php +++ b/src/applications/phame/herald/HeraldPhameBlogAdapter.php @@ -24,13 +24,6 @@ final class HeraldPhameBlogAdapter extends HeraldAdapter { return true; } - public function getRepetitionOptions() { - return array( - HeraldRepetitionPolicyConfig::EVERY, - HeraldRepetitionPolicyConfig::FIRST, - ); - } - public function supportsRuleType($rule_type) { switch ($rule_type) { case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: diff --git a/src/applications/phame/herald/HeraldPhamePostAdapter.php b/src/applications/phame/herald/HeraldPhamePostAdapter.php index 4776bbbdbc..72e7124271 100644 --- a/src/applications/phame/herald/HeraldPhamePostAdapter.php +++ b/src/applications/phame/herald/HeraldPhamePostAdapter.php @@ -24,13 +24,6 @@ final class HeraldPhamePostAdapter extends HeraldAdapter { return true; } - public function getRepetitionOptions() { - return array( - HeraldRepetitionPolicyConfig::EVERY, - HeraldRepetitionPolicyConfig::FIRST, - ); - } - public function supportsRuleType($rule_type) { switch ($rule_type) { case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: diff --git a/src/applications/ponder/herald/HeraldPonderQuestionAdapter.php b/src/applications/ponder/herald/HeraldPonderQuestionAdapter.php index f3f47681bb..f9434c7f78 100644 --- a/src/applications/ponder/herald/HeraldPonderQuestionAdapter.php +++ b/src/applications/ponder/herald/HeraldPonderQuestionAdapter.php @@ -39,13 +39,6 @@ final class HeraldPonderQuestionAdapter extends HeraldAdapter { return true; } - public function getRepetitionOptions() { - return array( - HeraldRepetitionPolicyConfig::EVERY, - HeraldRepetitionPolicyConfig::FIRST, - ); - } - public function supportsRuleType($rule_type) { switch ($rule_type) { case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 793fe5603d..9396d1873e 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -230,14 +230,23 @@ final class PhabricatorProjectBoardViewController ->addCancelButton($board_uri); } - $batch_ids = mpull($batch_tasks, 'getID'); - $batch_ids = implode(',', $batch_ids); + // Create a saved query to hold the working set. This allows us to get + // around URI length limitations with a long "?ids=..." query string. + // For details, see T10268. + $search_engine = id(new ManiphestTaskSearchEngine()) + ->setViewer($viewer); + + $saved_query = $search_engine->newSavedQuery(); + $saved_query->setParameter('ids', mpull($batch_tasks, 'getID')); + $search_engine->saveQuery($saved_query); + + $query_key = $saved_query->getQueryKey(); + + $bulk_uri = new PhutilURI("/maniphest/bulk/query/{$query_key}/"); + $bulk_uri->setQueryParam('board', $this->id); - $batch_uri = new PhutilURI('/maniphest/batch/'); - $batch_uri->setQueryParam('board', $this->id); - $batch_uri->setQueryParam('batch', $batch_ids); return id(new AphrontRedirectResponse()) - ->setURI($batch_uri); + ->setURI($bulk_uri); } $move_id = $request->getStr('move'); @@ -1048,7 +1057,7 @@ final class PhabricatorProjectBoardViewController $column_items[] = id(new PhabricatorActionView()) ->setIcon('fa-list-ul') - ->setName(pht('Batch Edit Tasks...')) + ->setName(pht('Bulk Edit Tasks...')) ->setHref($batch_edit_uri) ->setDisabled(!$can_batch_edit); diff --git a/src/applications/project/engineextension/PhabricatorProjectsEditEngineExtension.php b/src/applications/project/engineextension/PhabricatorProjectsEditEngineExtension.php index 5daacd315c..a388a39a97 100644 --- a/src/applications/project/engineextension/PhabricatorProjectsEditEngineExtension.php +++ b/src/applications/project/engineextension/PhabricatorProjectsEditEngineExtension.php @@ -46,6 +46,8 @@ final class PhabricatorProjectsEditEngineExtension $project_phids = array(); } + $viewer = $engine->getViewer(); + $projects_field = id(new PhabricatorProjectsEditField()) ->setKey('projectPHIDs') ->setLabel(pht('Tags')) @@ -58,9 +60,11 @@ final class PhabricatorProjectsEditEngineExtension ->setDescription(pht('Select project tags for the object.')) ->setTransactionType($edge_type) ->setMetadataValue('edge:type', $project_edge_type) - ->setValue($project_phids); + ->setValue($project_phids) + ->setViewer($viewer); - $projects_field->setViewer($engine->getViewer()); + $projects_datasource = id(new PhabricatorProjectDatasource()) + ->setViewer($viewer); $edit_add = $projects_field->getConduitEditType(self::EDITKEY_ADD) ->setConduitDescription(pht('Add project tags.')); @@ -72,6 +76,18 @@ final class PhabricatorProjectsEditEngineExtension $edit_rem = $projects_field->getConduitEditType(self::EDITKEY_REMOVE) ->setConduitDescription(pht('Remove project tags.')); + $projects_field->getBulkEditType(self::EDITKEY_ADD) + ->setBulkEditLabel(pht('Add project tags')) + ->setDatasource($projects_datasource); + + $projects_field->getBulkEditType(self::EDITKEY_SET) + ->setBulkEditLabel(pht('Set project tags to')) + ->setDatasource($projects_datasource); + + $projects_field->getBulkEditType(self::EDITKEY_REMOVE) + ->setBulkEditLabel(pht('Remove project tags')) + ->setDatasource($projects_datasource); + return array( $projects_field, ); diff --git a/src/applications/project/herald/PhabricatorProjectHeraldAdapter.php b/src/applications/project/herald/PhabricatorProjectHeraldAdapter.php index f6d0984cb6..72f064a5c6 100644 --- a/src/applications/project/herald/PhabricatorProjectHeraldAdapter.php +++ b/src/applications/project/herald/PhabricatorProjectHeraldAdapter.php @@ -24,13 +24,6 @@ final class PhabricatorProjectHeraldAdapter extends HeraldAdapter { return true; } - public function getRepetitionOptions() { - return array( - HeraldRepetitionPolicyConfig::EVERY, - HeraldRepetitionPolicyConfig::FIRST, - ); - } - public function supportsRuleType($rule_type) { switch ($rule_type) { case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: diff --git a/src/applications/repository/engine/PhabricatorRepositoryMirrorEngine.php b/src/applications/repository/engine/PhabricatorRepositoryMirrorEngine.php index b18f6b1bf8..ed856957f0 100644 --- a/src/applications/repository/engine/PhabricatorRepositoryMirrorEngine.php +++ b/src/applications/repository/engine/PhabricatorRepositoryMirrorEngine.php @@ -76,6 +76,20 @@ final class PhabricatorRepositoryMirrorEngine PhabricatorRepository $repository, PhabricatorRepositoryURI $mirror_uri) { + // See T5965. Test if we have any refs to mirror. If we have nothing, git + // will exit with an error ("No refs in common and none specified; ...") + // when we run "git push --mirror". + + // If we don't have any refs, we just bail out. (This is arguably sort of + // the wrong behavior: to mirror an empty repository faithfully we should + // delete everything in the remote.) + + list($stdout) = $repository->execxLocalCommand( + 'for-each-ref --count 1 --'); + if (!strlen($stdout)) { + return; + } + $argv = array( 'push --verbose --mirror -- %P', $mirror_uri->getURIEnvelope(), diff --git a/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php b/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php index 10eddd38d4..208a3792b3 100644 --- a/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php +++ b/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php @@ -399,6 +399,11 @@ final class PhabricatorRepositoryPullEngine 'ls-remote %P', $remote_envelope); + // Empty repositories don't have any refs. + if (!strlen(rtrim($stdout))) { + return array(); + } + $map = array(); $lines = phutil_split_lines($stdout, false); foreach ($lines as $line) { diff --git a/src/applications/repository/query/PhabricatorRepositoryPullEventQuery.php b/src/applications/repository/query/PhabricatorRepositoryPullEventQuery.php index 1063cdb48c..af60ee0383 100644 --- a/src/applications/repository/query/PhabricatorRepositoryPullEventQuery.php +++ b/src/applications/repository/query/PhabricatorRepositoryPullEventQuery.php @@ -37,19 +37,35 @@ final class PhabricatorRepositoryPullEventQuery } protected function willFilterPage(array $events) { + // If a pull targets an invalid repository or fails before authenticating, + // it may not have an associated repository. + $repository_phids = mpull($events, 'getRepositoryPHID'); - $repositories = id(new PhabricatorRepositoryQuery()) - ->setViewer($this->getViewer()) - ->withPHIDs($repository_phids) - ->execute(); - $repositories = mpull($repositories, null, 'getPHID'); + $repository_phids = array_filter($repository_phids); + + if ($repository_phids) { + $repositories = id(new PhabricatorRepositoryQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs($repository_phids) + ->execute(); + $repositories = mpull($repositories, null, 'getPHID'); + } else { + $repositories = array(); + } foreach ($events as $key => $event) { $phid = $event->getRepositoryPHID(); - if (empty($repositories[$phid])) { - unset($events[$key]); + if (!$phid) { + $event->attachRepository(null); continue; } + + if (empty($repositories[$phid])) { + unset($events[$key]); + $this->didRejectResult($event); + continue; + } + $event->attachRepository($repositories[$phid]); } diff --git a/src/applications/repository/query/PhabricatorRepositoryPushEventQuery.php b/src/applications/repository/query/PhabricatorRepositoryPushEventQuery.php index a3bed7abd3..f3e5fc62b4 100644 --- a/src/applications/repository/query/PhabricatorRepositoryPushEventQuery.php +++ b/src/applications/repository/query/PhabricatorRepositoryPushEventQuery.php @@ -34,19 +34,12 @@ final class PhabricatorRepositoryPushEventQuery return $this; } + public function newResultObject() { + return new PhabricatorRepositoryPushEvent(); + } + protected function loadPage() { - $table = new PhabricatorRepositoryPushEvent(); - $conn_r = $table->establishConnection('r'); - - $data = queryfx_all( - $conn_r, - 'SELECT * FROM %T %Q %Q %Q', - $table->getTableName(), - $this->buildWhereClause($conn_r), - $this->buildOrderClause($conn_r), - $this->buildLimitClause($conn_r)); - - return $table->loadAllFromArray($data); + return $this->loadStandardPage($this->newResultObject()); } protected function willFilterPage(array $events) { @@ -88,40 +81,38 @@ final class PhabricatorRepositoryPushEventQuery return $events; } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { - $where = array(); + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); - if ($this->ids) { + if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->ids); } - if ($this->phids) { + if ($this->phids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'phid IN (%Ls)', $this->phids); } - if ($this->repositoryPHIDs) { + if ($this->repositoryPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'repositoryPHID IN (%Ls)', $this->repositoryPHIDs); } - if ($this->pusherPHIDs) { + if ($this->pusherPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'pusherPHID in (%Ls)', $this->pusherPHIDs); } - $where[] = $this->buildPagingClause($conn_r); - - return $this->formatWhereClause($where); + return $where; } public function getQueryApplicationClass() { diff --git a/src/applications/repository/query/PhabricatorRepositoryPushLogSearchEngine.php b/src/applications/repository/query/PhabricatorRepositoryPushLogSearchEngine.php index 8fd3baeb54..d171b80999 100644 --- a/src/applications/repository/query/PhabricatorRepositoryPushLogSearchEngine.php +++ b/src/applications/repository/query/PhabricatorRepositoryPushLogSearchEngine.php @@ -11,63 +11,40 @@ final class PhabricatorRepositoryPushLogSearchEngine return 'PhabricatorDiffusionApplication'; } - public function buildSavedQueryFromRequest(AphrontRequest $request) { - $saved = new PhabricatorSavedQuery(); - - $saved->setParameter( - 'repositoryPHIDs', - $this->readPHIDsFromRequest( - $request, - 'repositories', - array( - PhabricatorRepositoryRepositoryPHIDType::TYPECONST, - ))); - - $saved->setParameter( - 'pusherPHIDs', - $this->readUsersFromRequest( - $request, - 'pushers')); - - return $saved; + public function newQuery() { + return new PhabricatorRepositoryPushLogQuery(); } - public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { - $query = id(new PhabricatorRepositoryPushLogQuery()); + protected function buildQueryFromParameters(array $map) { + $query = $this->newQuery(); - $repository_phids = $saved->getParameter('repositoryPHIDs'); - if ($repository_phids) { - $query->withRepositoryPHIDs($repository_phids); + if ($map['repositoryPHIDs']) { + $query->withRepositoryPHIDs($map['repositoryPHIDs']); } - $pusher_phids = $saved->getParameter('pusherPHIDs'); - if ($pusher_phids) { - $query->withPusherPHIDs($pusher_phids); + if ($map['pusherPHIDs']) { + $query->withPusherPHIDs($map['pusherPHIDs']); } return $query; } - public function buildSearchForm( - AphrontFormView $form, - PhabricatorSavedQuery $saved_query) { - - $repository_phids = $saved_query->getParameter('repositoryPHIDs', array()); - $pusher_phids = $saved_query->getParameter('pusherPHIDs', array()); - - $form - ->appendControl( - id(new AphrontFormTokenizerControl()) - ->setDatasource(new DiffusionRepositoryDatasource()) - ->setName('repositories') - ->setLabel(pht('Repositories')) - ->setValue($repository_phids)) - ->appendControl( - id(new AphrontFormTokenizerControl()) - ->setDatasource(new PhabricatorPeopleDatasource()) - ->setName('pushers') - ->setLabel(pht('Pushers')) - ->setValue($pusher_phids)); + protected function buildCustomSearchFields() { + return array( + id(new PhabricatorSearchDatasourceField()) + ->setDatasource(new DiffusionRepositoryDatasource()) + ->setKey('repositoryPHIDs') + ->setAliases(array('repository', 'repositories', 'repositoryPHID')) + ->setLabel(pht('Repositories')) + ->setDescription( + pht('Search for pull logs for specific repositories.')), + id(new PhabricatorUsersSearchField()) + ->setKey('pusherPHIDs') + ->setAliases(array('pusher', 'pushers', 'pusherPHID')) + ->setLabel(pht('Pushers')) + ->setDescription( + pht('Search for pull logs by specific users.')), + ); } protected function getURI($path) { diff --git a/src/applications/repository/storage/PhabricatorRepositoryPullEvent.php b/src/applications/repository/storage/PhabricatorRepositoryPullEvent.php index c1227402d7..85ea27062b 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryPullEvent.php +++ b/src/applications/repository/storage/PhabricatorRepositoryPullEvent.php @@ -15,6 +15,14 @@ final class PhabricatorRepositoryPullEvent private $repository = self::ATTACHABLE; + const RESULT_PULL = 'pull'; + const RESULT_ERROR = 'error'; + const RESULT_EXCEPTION = 'exception'; + + const PROTOCOL_HTTP = 'http'; + const PROTOCOL_HTTPS = 'https'; + const PROTOCOL_SSH = 'ssh'; + public static function initializeNewEvent(PhabricatorUser $viewer) { return id(new PhabricatorRepositoryPushEvent()) ->setPusherPHID($viewer->getPHID()); @@ -51,7 +59,7 @@ final class PhabricatorRepositoryPullEvent PhabricatorRepositoryPullEventPHIDType::TYPECONST); } - public function attachRepository(PhabricatorRepository $repository) { + public function attachRepository(PhabricatorRepository $repository = null) { $this->repository = $repository; return $this; } @@ -60,6 +68,67 @@ final class PhabricatorRepositoryPullEvent return $this->assertAttached($this->repository); } + public function getRemoteProtocolDisplayName() { + $map = array( + self::PROTOCOL_SSH => pht('SSH'), + self::PROTOCOL_HTTP => pht('HTTP'), + self::PROTOCOL_HTTPS => pht('HTTPS'), + ); + + $protocol = $this->getRemoteProtocol(); + + return idx($map, $protocol, $protocol); + } + + public function newResultIcon() { + $icon = new PHUIIconView(); + $type = $this->getResultType(); + $code = $this->getResultCode(); + + $protocol = $this->getRemoteProtocol(); + + $is_any_http = + ($protocol === self::PROTOCOL_HTTP) || + ($protocol === self::PROTOCOL_HTTPS); + + // If this was an HTTP request and we responded with a 401, that means + // the user didn't provide credentials. This is technically an error, but + // it's routine and just causes the client to prompt them. Show a more + // comforting icon and description in the UI. + if ($is_any_http) { + if ($code == 401) { + return $icon + ->setIcon('fa-key blue') + ->setTooltip(pht('Authentication Required')); + } + } + + switch ($type) { + case self::RESULT_ERROR: + $icon + ->setIcon('fa-times red') + ->setTooltip(pht('Error')); + break; + case self::RESULT_EXCEPTION: + $icon + ->setIcon('fa-exclamation-triangle red') + ->setTooltip(pht('Exception')); + break; + case self::RESULT_PULL: + $icon + ->setIcon('fa-download green') + ->setTooltip(pht('Pull')); + break; + default: + $icon + ->setIcon('fa-question indigo') + ->setTooltip(pht('Unknown ("%s")', $type)); + break; + } + + return $icon; + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ @@ -71,10 +140,18 @@ final class PhabricatorRepositoryPullEvent } public function getPolicy($capability) { - return $this->getRepository()->getPolicy($capability); + if ($this->getRepository()) { + return $this->getRepository()->getPolicy($capability); + } + + return PhabricatorPolicies::POLICY_ADMIN; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + if (!$this->getRepository()) { + return false; + } + return $this->getRepository()->hasAutomaticCapability($capability, $viewer); } diff --git a/src/applications/repository/storage/PhabricatorRepositoryVCSPassword.php b/src/applications/repository/storage/PhabricatorRepositoryVCSPassword.php deleted file mode 100644 index 2f5c20cf7e..0000000000 --- a/src/applications/repository/storage/PhabricatorRepositoryVCSPassword.php +++ /dev/null @@ -1,60 +0,0 @@ - array( - 'passwordHash' => 'text128', - ), - self::CONFIG_KEY_SCHEMA => array( - 'key_phid' => array( - 'columns' => array('userPHID'), - 'unique' => true, - ), - ), - ) + parent::getConfiguration(); - } - - public function setPassword( - PhutilOpaqueEnvelope $password, - PhabricatorUser $user) { - $hash_envelope = $this->hashPassword($password, $user); - return $this->setPasswordHash($hash_envelope->openEnvelope()); - } - - public function comparePassword( - PhutilOpaqueEnvelope $password, - PhabricatorUser $user) { - - return PhabricatorPasswordHasher::comparePassword( - $this->getPasswordHashInput($password, $user), - new PhutilOpaqueEnvelope($this->getPasswordHash())); - } - - private function getPasswordHashInput( - PhutilOpaqueEnvelope $password, - PhabricatorUser $user) { - if ($user->getPHID() != $this->getUserPHID()) { - throw new Exception(pht('User does not match password user PHID!')); - } - - $raw_input = PhabricatorHash::digestPassword($password, $user->getPHID()); - return new PhutilOpaqueEnvelope($raw_input); - } - - private function hashPassword( - PhutilOpaqueEnvelope $password, - PhabricatorUser $user) { - - $input_envelope = $this->getPasswordHashInput($password, $user); - - $best_hasher = PhabricatorPasswordHasher::getBestHasher(); - return $best_hasher->getPasswordHashForStorage($input_envelope); - } - -} diff --git a/src/applications/search/controller/PhabricatorApplicationSearchController.php b/src/applications/search/controller/PhabricatorApplicationSearchController.php index f781e20380..d3b05d1e3e 100644 --- a/src/applications/search/controller/PhabricatorApplicationSearchController.php +++ b/src/applications/search/controller/PhabricatorApplicationSearchController.php @@ -66,6 +66,11 @@ final class PhabricatorApplicationSearchController public function processRequest() { $this->validateDelegatingController(); + $query_action = $this->getRequest()->getURIData('queryAction'); + if ($query_action == 'export') { + return $this->processExportRequest(); + } + $key = $this->getQueryKey(); if ($key == 'edit') { return $this->processEditRequest(); @@ -374,6 +379,136 @@ final class PhabricatorApplicationSearchController ->appendChild($body); } + private function processExportRequest() { + $viewer = $this->getViewer(); + $engine = $this->getSearchEngine(); + $request = $this->getRequest(); + + if (!$this->canExport()) { + return new Aphront404Response(); + } + + $query_key = $this->getQueryKey(); + if ($engine->isBuiltinQuery($query_key)) { + $saved_query = $engine->buildSavedQueryFromBuiltin($query_key); + } else if ($query_key) { + $saved_query = id(new PhabricatorSavedQueryQuery()) + ->setViewer($viewer) + ->withQueryKeys(array($query_key)) + ->executeOne(); + } else { + $saved_query = null; + } + + if (!$saved_query) { + return new Aphront404Response(); + } + + $cancel_uri = $engine->getQueryResultsPageURI($query_key); + + $named_query = idx($engine->loadEnabledNamedQueries(), $query_key); + + if ($named_query) { + $filename = $named_query->getQueryName(); + } else { + $filename = $engine->getResultTypeDescription(); + } + $filename = phutil_utf8_strtolower($filename); + $filename = PhabricatorFile::normalizeFileName($filename); + + $formats = PhabricatorExportFormat::getAllEnabledExportFormats(); + $format_options = mpull($formats, 'getExportFormatName'); + + $errors = array(); + + $e_format = null; + if ($request->isFormPost()) { + $format_key = $request->getStr('format'); + $format = idx($formats, $format_key); + + if (!$format) { + $e_format = pht('Invalid'); + $errors[] = pht('Choose a valid export format.'); + } + + if (!$errors) { + $query = $engine->buildQueryFromSavedQuery($saved_query); + + // NOTE: We aren't reading the pager from the request. Exports always + // affect the entire result set. + $pager = $engine->newPagerForSavedQuery($saved_query); + $pager->setPageSize(0x7FFFFFFF); + + $objects = $engine->executeQuery($query, $pager); + + $extension = $format->getFileExtension(); + $mime_type = $format->getMIMEContentType(); + $filename = $filename.'.'.$extension; + + $format = clone $format; + $format->setViewer($viewer); + + $export_data = $engine->newExport($objects); + + if (count($export_data) !== count($objects)) { + throw new Exception( + pht( + 'Search engine exported the wrong number of objects, expected '. + '%s but got %s.', + phutil_count($objects), + phutil_count($export_data))); + } + + $objects = array_values($objects); + $export_data = array_values($export_data); + + $field_list = $engine->newExportFieldList(); + $field_list = mpull($field_list, null, 'getKey'); + + for ($ii = 0; $ii < count($objects); $ii++) { + $format->addObject($objects[$ii], $field_list, $export_data[$ii]); + } + + $export_result = $format->newFileData(); + + $file = PhabricatorFile::newFromFileData( + $export_result, + array( + 'name' => $filename, + 'authorPHID' => $viewer->getPHID(), + 'ttl.relative' => phutil_units('15 minutes in seconds'), + 'viewPolicy' => PhabricatorPolicies::POLICY_NOONE, + 'mime-type' => $mime_type, + )); + + return $this->newDialog() + ->setTitle(pht('Download Results')) + ->appendParagraph( + pht('Click the download button to download the exported data.')) + ->addCancelButton($cancel_uri, pht('Done')) + ->setSubmitURI($file->getDownloadURI()) + ->setDisableWorkflowOnSubmit(true) + ->addSubmitButton(pht('Download Data')); + } + } + + $export_form = id(new AphrontFormView()) + ->setViewer($viewer) + ->appendControl( + id(new AphrontFormSelectControl()) + ->setName('format') + ->setLabel(pht('Format')) + ->setError($e_format) + ->setOptions($format_options)); + + return $this->newDialog() + ->setTitle(pht('Export Results')) + ->setErrors($errors) + ->appendForm($export_form) + ->addCancelButton($cancel_uri) + ->addSubmitButton(pht('Continue')); + } + private function processEditRequest() { $parent = $this->getDelegatingController(); $request = $this->getRequest(); @@ -720,7 +855,6 @@ final class PhabricatorApplicationSearchController $viewer); if ($can_use && $is_installed) { - $dashboard_uri = '/dashboard/install/'; $actions[] = id(new PhabricatorActionView()) ->setIcon('fa-dashboard') ->setName(pht('Add to Dashboard')) @@ -728,6 +862,15 @@ final class PhabricatorApplicationSearchController ->setHref("/dashboard/panel/install/{$engine_class}/{$query_key}/"); } + if ($this->canExport()) { + $export_uri = $engine->getExportURI($query_key); + $actions[] = id(new PhabricatorActionView()) + ->setIcon('fa-download') + ->setName(pht('Export Data')) + ->setWorkflow(true) + ->setHref($export_uri); + } + if ($is_dev) { $engine = $this->getSearchEngine(); $nux_uri = $engine->getQueryBaseURI(); @@ -753,4 +896,22 @@ final class PhabricatorApplicationSearchController return $actions; } + private function canExport() { + $engine = $this->getSearchEngine(); + if (!$engine->canExport()) { + return false; + } + + // Don't allow logged-out users to perform exports. There's no technical + // or policy reason they can't, but we don't normally give them access + // to write files or jobs. For now, just err on the side of caution. + + $viewer = $this->getViewer(); + if (!$viewer->getPHID()) { + return false; + } + + return true; + } + } diff --git a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php index c0497a0c03..33efd890bb 100644 --- a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php +++ b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php @@ -413,6 +413,10 @@ abstract class PhabricatorApplicationSearchEngine extends Phobject { return $this->getURI(''); } + public function getExportURI($query_key) { + return $this->getURI('query/'.$query_key.'/export/'); + } + /** * Return the URI to a path within the application. Used to construct default @@ -1441,4 +1445,21 @@ abstract class PhabricatorApplicationSearchEngine extends Phobject { return array(); } + +/* -( Export )------------------------------------------------------------- */ + + + public function canExport() { + $fields = $this->newExportFields(); + return (bool)$fields; + } + + final public function newExportFieldList() { + return $this->newExportFields(); + } + + protected function newExportFields() { + return array(); + } + } diff --git a/src/applications/search/field/PhabricatorSearchDateField.php b/src/applications/search/field/PhabricatorSearchDateField.php index 21b1627de7..41decd9503 100644 --- a/src/applications/search/field/PhabricatorSearchDateField.php +++ b/src/applications/search/field/PhabricatorSearchDateField.php @@ -4,7 +4,8 @@ final class PhabricatorSearchDateField extends PhabricatorSearchField { protected function newControl() { - return new AphrontFormTextControl(); + return id(new AphrontFormTextControl()) + ->setPlaceholder(pht('"2022-12-25" or "7 days ago"...')); } protected function getValueFromRequest(AphrontRequest $request, $key) { diff --git a/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php b/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php index 3807139fe3..9fb8838cf7 100644 --- a/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php @@ -25,10 +25,13 @@ final class PhabricatorPasswordSettingsPanel extends PhabricatorSettingsPanel { } public function processRequest(AphrontRequest $request) { - $user = $request->getUser(); + $viewer = $request->getUser(); + $user = $this->getUser(); + + $content_source = PhabricatorContentSource::newFromRequest($request); $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( - $user, + $viewer, $request, '/settings/'); @@ -40,48 +43,79 @@ final class PhabricatorPasswordSettingsPanel extends PhabricatorSettingsPanel { // registration or password reset. If this flow changes, that flow may // also need to change. + $account_type = PhabricatorAuthPassword::PASSWORD_TYPE_ACCOUNT; + + $password_objects = id(new PhabricatorAuthPasswordQuery()) + ->setViewer($viewer) + ->withObjectPHIDs(array($user->getPHID())) + ->withPasswordTypes(array($account_type)) + ->withIsRevoked(false) + ->execute(); + if ($password_objects) { + $password_object = head($password_objects); + } else { + $password_object = PhabricatorAuthPassword::initializeNewPassword( + $user, + $account_type); + } + $e_old = true; $e_new = true; $e_conf = true; $errors = array(); if ($request->isFormPost()) { + // Rate limit guesses about the old password. This page requires MFA and + // session compromise already, so this is mostly just to stop researchers + // from reporting this as a vulnerability. + PhabricatorSystemActionEngine::willTakeAction( + array($viewer->getPHID()), + new PhabricatorAuthChangePasswordAction(), + 1); + $envelope = new PhutilOpaqueEnvelope($request->getStr('old_pw')); - if (!$user->comparePassword($envelope)) { + + $engine = id(new PhabricatorAuthPasswordEngine()) + ->setViewer($viewer) + ->setContentSource($content_source) + ->setPasswordType($account_type) + ->setObject($user); + + if (!strlen($envelope->openEnvelope())) { + $errors[] = pht('You must enter your current password.'); + $e_old = pht('Required'); + } else if (!$engine->isValidPassword($envelope)) { $errors[] = pht('The old password you entered is incorrect.'); $e_old = pht('Invalid'); + } else { + $e_old = null; + + // Refund the user an action credit for getting the password right. + PhabricatorSystemActionEngine::willTakeAction( + array($viewer->getPHID()), + new PhabricatorAuthChangePasswordAction(), + -1); } $pass = $request->getStr('new_pw'); $conf = $request->getStr('conf_pw'); + $password_envelope = new PhutilOpaqueEnvelope($pass); + $confirm_envelope = new PhutilOpaqueEnvelope($conf); - if (strlen($pass) < $min_len) { - $errors[] = pht('Your new password is too short.'); - $e_new = pht('Too Short'); - } else if ($pass !== $conf) { - $errors[] = pht('New password and confirmation do not match.'); - $e_conf = pht('Invalid'); - } else if (PhabricatorCommonPasswords::isCommonPassword($pass)) { - $e_new = pht('Very Weak'); - $e_conf = pht('Very Weak'); - $errors[] = pht( - 'Your new password is very weak: it is one of the most common '. - 'passwords in use. Choose a stronger password.'); + try { + $engine->checkNewPassword($password_envelope, $confirm_envelope); + $e_new = null; + $e_conf = null; + } catch (PhabricatorAuthPasswordException $ex) { + $errors[] = $ex->getMessage(); + $e_new = $ex->getPasswordError(); + $e_conf = $ex->getConfirmError(); } if (!$errors) { - // This write is unguarded because the CSRF token has already - // been checked in the call to $request->isFormPost() and - // the CSRF token depends on the password hash, so when it - // is changed here the CSRF token check will fail. - $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); - - $envelope = new PhutilOpaqueEnvelope($pass); - id(new PhabricatorUserEditor()) - ->setActor($user) - ->changePassword($user, $envelope); - - unset($unguarded); + $password_object + ->setPassword($password_envelope, $user) + ->save(); $next = $this->getPanelURI('?saved=true'); @@ -93,11 +127,9 @@ final class PhabricatorPasswordSettingsPanel extends PhabricatorSettingsPanel { } } - $hash_envelope = new PhutilOpaqueEnvelope($user->getPasswordHash()); - if (strlen($hash_envelope->openEnvelope())) { + if ($password_object->getID()) { try { - $can_upgrade = PhabricatorPasswordHasher::canUpgradeHash( - $hash_envelope); + $can_upgrade = $password_object->canUpgrade(); } catch (PhabricatorPasswordHasherUnavailableException $ex) { $can_upgrade = false; @@ -126,7 +158,7 @@ final class PhabricatorPasswordSettingsPanel extends PhabricatorSettingsPanel { } $form = id(new AphrontFormView()) - ->setViewer($user) + ->setViewer($viewer) ->appendChild( id(new AphrontFormPasswordControl()) ->setLabel(pht('Old Password')) @@ -154,7 +186,7 @@ final class PhabricatorPasswordSettingsPanel extends PhabricatorSettingsPanel { $properties->addProperty( pht('Current Algorithm'), PhabricatorPasswordHasher::getCurrentAlgorithmName( - new PhutilOpaqueEnvelope($user->getPasswordHash()))); + $password_object->newPasswordEnvelope())); $properties->addProperty( pht('Best Available Algorithm'), diff --git a/src/applications/subscriptions/engineextension/PhabricatorSubscriptionsEditEngineExtension.php b/src/applications/subscriptions/engineextension/PhabricatorSubscriptionsEditEngineExtension.php index c37933a938..67efd31f63 100644 --- a/src/applications/subscriptions/engineextension/PhabricatorSubscriptionsEditEngineExtension.php +++ b/src/applications/subscriptions/engineextension/PhabricatorSubscriptionsEditEngineExtension.php @@ -42,6 +42,8 @@ final class PhabricatorSubscriptionsEditEngineExtension $sub_phids = array(); } + $viewer = $engine->getViewer(); + $subscribers_field = id(new PhabricatorSubscribersEditField()) ->setKey(self::FIELDKEY) ->setLabel(pht('Subscribers')) @@ -53,9 +55,11 @@ final class PhabricatorSubscriptionsEditEngineExtension ->setCommentActionOrder(9000) ->setDescription(pht('Choose subscribers.')) ->setTransactionType($subscribers_type) - ->setValue($sub_phids); + ->setValue($sub_phids) + ->setViewer($viewer); - $subscribers_field->setViewer($engine->getViewer()); + $subscriber_datasource = id(new PhabricatorMetaMTAMailableDatasource()) + ->setViewer($viewer); $edit_add = $subscribers_field->getConduitEditType(self::EDITKEY_ADD) ->setConduitDescription(pht('Add subscribers.')); @@ -67,6 +71,18 @@ final class PhabricatorSubscriptionsEditEngineExtension $edit_rem = $subscribers_field->getConduitEditType(self::EDITKEY_REMOVE) ->setConduitDescription(pht('Remove subscribers.')); + $subscribers_field->getBulkEditType(self::EDITKEY_ADD) + ->setBulkEditLabel(pht('Add subscribers')) + ->setDatasource($subscriber_datasource); + + $subscribers_field->getBulkEditType(self::EDITKEY_SET) + ->setBulkEditLabel(pht('Set subscribers to')) + ->setDatasource($subscriber_datasource); + + $subscribers_field->getBulkEditType(self::EDITKEY_REMOVE) + ->setBulkEditLabel(pht('Remove subscribers')) + ->setDatasource($subscriber_datasource); + return array( $subscribers_field, ); diff --git a/src/applications/transactions/bulk/PhabricatorBulkEditGroup.php b/src/applications/transactions/bulk/PhabricatorBulkEditGroup.php new file mode 100644 index 0000000000..c650ec222e --- /dev/null +++ b/src/applications/transactions/bulk/PhabricatorBulkEditGroup.php @@ -0,0 +1,27 @@ +key = $key; + return $this; + } + + public function getKey() { + return $this->key; + } + + public function setLabel($label) { + $this->label = $label; + return $this; + } + + public function getLabel() { + return $this->label; + } + +} diff --git a/src/applications/transactions/bulk/PhabricatorBulkEngine.php b/src/applications/transactions/bulk/PhabricatorBulkEngine.php new file mode 100644 index 0000000000..534390b518 --- /dev/null +++ b/src/applications/transactions/bulk/PhabricatorBulkEngine.php @@ -0,0 +1,464 @@ +savedQuery; + if ($saved_query) { + $path = '/query/'.$saved_query->getQueryKey().'/'; + } else { + $path = '/'; + } + + return $this->getQueryURI($path); + } + + public function getDoneURI() { + if ($this->objectList !== null) { + $ids = mpull($this->objectList, 'getID'); + $path = '/?ids='.implode(',', $ids); + } else { + $path = '/'; + } + + return $this->getQueryURI($path); + } + + protected function getQueryURI($path = '/') { + $viewer = $this->getViewer(); + + $engine = id($this->newSearchEngine()) + ->setViewer($viewer); + + return $engine->getQueryBaseURI().ltrim($path, '/'); + } + + protected function getBulkURI() { + $saved_query = $this->savedQuery; + if ($saved_query) { + $path = '/query/'.$saved_query->getQueryKey().'/'; + } else { + $path = '/'; + } + + return $this->getBulkBaseURI($path); + } + + protected function getBulkBaseURI($path) { + return $this->getQueryURI('bulk/'.ltrim($path, '/')); + } + + final public function setViewer(PhabricatorUser $viewer) { + $this->viewer = $viewer; + return $this; + } + + final public function getViewer() { + return $this->viewer; + } + + final public function setController(PhabricatorController $controller) { + $this->controller = $controller; + return $this; + } + + final public function getController() { + return $this->controller; + } + + final public function addContextParameter($key) { + $this->context[$key] = true; + return $this; + } + + final public function buildResponse() { + $viewer = $this->getViewer(); + $controller = $this->getController(); + $request = $controller->getRequest(); + + $response = $this->loadObjectList(); + if ($response) { + return $response; + } + + if ($request->isFormPost() && $request->getBool('bulkEngine')) { + return $this->buildEditResponse(); + } + + $list_view = $this->newBulkObjectList(); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Bulk Editor')) + ->setHeaderIcon('fa-pencil-square-o'); + + $list_box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Working Set')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setObjectList($list_view); + + $form_view = $this->newBulkActionForm(); + + $form_box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Actions')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setForm($form_view); + + $complete_form = phabricator_form( + $viewer, + array( + 'action' => $this->getBulkURI(), + 'method' => 'POST', + 'id' => $this->getRootFormID(), + ), + array( + $this->newContextInputs(), + $list_box, + $form_box, + )); + + $column_view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter($complete_form); + + // TODO: This is a bit hacky and inflexible. + $crumbs = $controller->buildApplicationCrumbsForEditEngine(); + $crumbs->addTextCrumb(pht('Query'), $this->getCancelURI()); + $crumbs->addTextCrumb(pht('Bulk Editor')); + + return $controller->newPage() + ->setTitle(pht('Bulk Edit')) + ->setCrumbs($crumbs) + ->appendChild($column_view); + } + + private function loadObjectList() { + $viewer = $this->getViewer(); + $controller = $this->getController(); + $request = $controller->getRequest(); + + $search_engine = id($this->newSearchEngine()) + ->setViewer($viewer); + + $query_key = $request->getURIData('queryKey'); + if (strlen($query_key)) { + if ($search_engine->isBuiltinQuery($query_key)) { + $saved = $search_engine->buildSavedQueryFromBuiltin($query_key); + } else { + $saved = id(new PhabricatorSavedQueryQuery()) + ->setViewer($viewer) + ->withQueryKeys(array($query_key)) + ->executeOne(); + if (!$saved) { + return new Aphront404Response(); + } + } + } else { + // TODO: For now, since we don't deal gracefully with queries which + // match a huge result set, just bail if we don't have any query + // parameters instead of querying for a trillion tasks and timing out. + $request_data = $request->getPassthroughRequestData(); + if (!$request_data) { + throw new Exception( + pht( + 'Expected a query key or a set of query constraints.')); + } + + $saved = $search_engine->buildSavedQueryFromRequest($request); + $search_engine->saveQuery($saved); + } + + $object_query = $search_engine->buildQueryFromSavedQuery($saved) + ->setViewer($viewer); + $object_list = $object_query->execute(); + $object_list = mpull($object_list, null, 'getPHID'); + + // If the user has submitted the bulk edit form, select only the objects + // they checked. + if ($request->getBool('bulkEngine')) { + $target_phids = $request->getArr('bulkTargetPHIDs'); + + // NOTE: It's possible that the underlying query result set has changed + // between the time we ran the query initially and now: for example, the + // query was for "Open Tasks" and some tasks were closed while the user + // was making action selections. + + // This could result in some objects getting dropped from the working set + // here: we'll have target PHIDs for them, but they will no longer be + // part of the object list. For now, just go with this since it doesn't + // seem like a big problem and may even be desirable. + + $this->targetList = array_select_keys($object_list, $target_phids); + } else { + $this->targetList = $object_list; + } + + $this->objectList = $object_list; + $this->savedQuery = $saved; + + // Filter just the editable objects. We show all the objects which the + // query matches whether they're editable or not, but indicate which ones + // can not be edited to the user. + + $editable_list = id(new PhabricatorPolicyFilter()) + ->setViewer($viewer) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->apply($object_list); + $this->editableList = mpull($editable_list, null, 'getPHID'); + + return null; + } + + private function newBulkObjectList() { + $viewer = $this->getViewer(); + + $objects = $this->objectList; + $objects = mpull($objects, null, 'getPHID'); + + $handles = $viewer->loadHandles(array_keys($objects)); + + $status_closed = PhabricatorObjectHandle::STATUS_CLOSED; + + $list = id(new PHUIObjectItemListView()) + ->setViewer($viewer) + ->setFlush(true); + + foreach ($objects as $phid => $object) { + $handle = $handles[$phid]; + + $is_closed = ($handle->getStatus() === $status_closed); + $can_edit = isset($this->editableList[$phid]); + $is_disabled = ($is_closed || !$can_edit); + $is_selected = isset($this->targetList[$phid]); + + $item = id(new PHUIObjectItemView()) + ->setHeader($handle->getFullName()) + ->setHref($handle->getURI()) + ->setDisabled($is_disabled) + ->setSelectable('bulkTargetPHIDs[]', $phid, $is_selected, !$can_edit); + + if (!$can_edit) { + $item->addIcon('fa-pencil red', pht('Not Editable')); + } + + $list->addItem($item); + } + + return $list; + } + + private function newContextInputs() { + $viewer = $this->getViewer(); + $controller = $this->getController(); + $request = $controller->getRequest(); + + $parameters = array(); + foreach ($this->context as $key => $value) { + $parameters[$key] = $request->getStr($key); + } + + $parameters = array( + 'bulkEngine' => 1, + ) + $parameters; + + $result = array(); + foreach ($parameters as $key => $value) { + $result[] = phutil_tag( + 'input', + array( + 'type' => 'hidden', + 'name' => $key, + 'value' => $value, + )); + } + + return $result; + } + + private function newBulkActionForm() { + $viewer = $this->getViewer(); + $input_id = celerity_generate_unique_node_id(); + + $edit_engine = id($this->newEditEngine()) + ->setViewer($viewer); + + $edit_map = $edit_engine->newBulkEditMap(); + $groups = $edit_engine->newBulkEditGroupMap(); + + $spec = array(); + $option_groups = igroup($edit_map, 'group'); + $default_value = null; + foreach ($groups as $group_key => $group) { + $options = idx($option_groups, $group_key, array()); + if (!$options) { + continue; + } + + $option_map = array(); + foreach ($options as $option) { + $option_map[] = array( + 'key' => $option['xaction'], + 'label' => $option['label'], + ); + + if ($default_value === null) { + $default_value = $option['xaction']; + } + } + + $spec[] = array( + 'label' => $group->getLabel(), + 'options' => $option_map, + ); + } + + require_celerity_resource('phui-bulk-editor-css'); + + Javelin::initBehavior( + 'bulk-editor', + array( + 'rootNodeID' => $this->getRootFormID(), + 'inputNodeID' => $input_id, + 'edits' => $edit_map, + 'optgroups' => array( + 'value' => $default_value, + 'groups' => $spec, + ), + )); + + $cancel_uri = $this->getCancelURI(); + + return id(new PHUIFormLayoutView()) + ->setViewer($viewer) + ->appendChild( + phutil_tag( + 'input', + array( + 'type' => 'hidden', + 'name' => 'xactions', + 'id' => $input_id, + ))) + ->appendChild( + id(new PHUIFormInsetView()) + ->setTitle(pht('Bulk Edit Actions')) + ->setRightButton( + javelin_tag( + 'a', + array( + 'href' => '#', + 'class' => 'button button-green', + 'sigil' => 'add-action', + 'mustcapture' => true, + ), + pht('Add Another Action'))) + ->setContent( + javelin_tag( + 'table', + array( + 'sigil' => 'bulk-actions', + 'class' => 'bulk-edit-table', + ), + ''))) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue(pht('Apply Bulk Edit')) + ->addCancelButton($cancel_uri)); + } + + private function buildEditResponse() { + $viewer = $this->getViewer(); + $controller = $this->getController(); + $request = $controller->getRequest(); + + if (!$this->objectList) { + throw new Exception(pht('Query does not match any objects.')); + } + + if (!$this->editableList) { + throw new Exception( + pht( + 'Query does not match any objects you have permission to edit.')); + } + + // Restrict the selection set to objects the user can actually edit. + $objects = array_intersect_key($this->editableList, $this->targetList); + + if (!$objects) { + throw new Exception( + pht( + 'You have not selected any objects to edit.')); + } + + $raw_xactions = $request->getStr('xactions'); + if ($raw_xactions) { + $raw_xactions = phutil_json_decode($raw_xactions); + } else { + $raw_xactions = array(); + } + + if (!$raw_xactions) { + throw new Exception( + pht( + 'You have not chosen any edits to apply.')); + } + + $edit_engine = id($this->newEditEngine()) + ->setViewer($viewer); + + $xactions = $edit_engine->newRawBulkTransactions($raw_xactions); + + $cancel_uri = $this->getCancelURI(); + $done_uri = $this->getDoneURI(); + + $job = PhabricatorWorkerBulkJob::initializeNewJob( + $viewer, + new PhabricatorEditEngineBulkJobType(), + array( + 'objectPHIDs' => mpull($objects, 'getPHID'), + 'xactions' => $xactions, + 'cancelURI' => $cancel_uri, + 'doneURI' => $done_uri, + )); + + $type_status = PhabricatorWorkerBulkJobTransaction::TYPE_STATUS; + + $xactions = array(); + $xactions[] = id(new PhabricatorWorkerBulkJobTransaction()) + ->setTransactionType($type_status) + ->setNewValue(PhabricatorWorkerBulkJob::STATUS_CONFIRM); + + $editor = id(new PhabricatorWorkerBulkJobEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnMissingFields(true) + ->applyTransactions($job, $xactions); + + return id(new AphrontRedirectResponse()) + ->setURI($job->getMonitorURI()); + } + + private function getRootFormID() { + if (!$this->rootFormID) { + $this->rootFormID = celerity_generate_unique_node_id(); + } + + return $this->rootFormID; + } + +} diff --git a/src/applications/transactions/bulk/PhabricatorEditEngineBulkJobType.php b/src/applications/transactions/bulk/PhabricatorEditEngineBulkJobType.php new file mode 100644 index 0000000000..758e7f3439 --- /dev/null +++ b/src/applications/transactions/bulk/PhabricatorEditEngineBulkJobType.php @@ -0,0 +1,129 @@ +getSize())); + + if ($job->getIsSilent()) { + $parts[] = pht( + 'If you start work now, this edit will be applied silently: it will '. + 'not send mail or publish notifications.'); + } else { + $parts[] = pht( + 'If you start work now, this edit will send mail and publish '. + 'notifications normally.'); + + $parts[] = pht('To silence this edit, run this command:'); + + $command = csprintf( + 'phabricator/ $ ./bin/bulk make-silent --id %R', + $job->getID()); + $command = (string)$command; + + $parts[] = phutil_tag('tt', array(), $command); + + $parts[] = pht( + 'After running this command, reload this page to see the new setting.'); + } + + return $parts; + } + + public function getJobSize(PhabricatorWorkerBulkJob $job) { + return count($job->getParameter('objectPHIDs', array())); + } + + public function getDoneURI(PhabricatorWorkerBulkJob $job) { + return $job->getParameter('doneURI'); + } + + public function createTasks(PhabricatorWorkerBulkJob $job) { + $tasks = array(); + + foreach ($job->getParameter('objectPHIDs', array()) as $phid) { + $tasks[] = PhabricatorWorkerBulkTask::initializeNewTask($job, $phid); + } + + return $tasks; + } + + public function runTask( + PhabricatorUser $actor, + PhabricatorWorkerBulkJob $job, + PhabricatorWorkerBulkTask $task) { + + $object = id(new PhabricatorObjectQuery()) + ->setViewer($actor) + ->withPHIDs(array($task->getObjectPHID())) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$object) { + return; + } + + $raw_xactions = $job->getParameter('xactions'); + $xactions = $this->buildTransactions($object, $raw_xactions); + $is_silent = $job->getIsSilent(); + + $editor = $object->getApplicationTransactionEditor() + ->setActor($actor) + ->setContentSource($job->newContentSource()) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->setIsSilent($is_silent) + ->applyTransactions($object, $xactions); + } + + private function buildTransactions($object, array $raw_xactions) { + $xactions = array(); + + foreach ($raw_xactions as $raw_xaction) { + $xaction = $object->getApplicationTransactionTemplate() + ->setTransactionType($raw_xaction['type']); + + if (isset($raw_xaction['new'])) { + $xaction->setNewValue($raw_xaction['new']); + } + + if (isset($raw_xaction['comment'])) { + $comment = $xaction->getApplicationTransactionCommentObject() + ->setContent($raw_xaction['comment']); + $xaction->attachComment($comment); + } + + if (isset($raw_xaction['metadata'])) { + foreach ($raw_xaction['metadata'] as $meta_key => $meta_value) { + $xaction->setMetadataValue($meta_key, $meta_value); + } + } + + if (array_key_exists('old', $raw_xaction)) { + $xaction->setOldValue($raw_xaction['old']); + } + + $xactions[] = $xaction; + } + + return $xactions; + } + +} diff --git a/src/applications/transactions/bulk/management/PhabricatorBulkManagementMakeSilentWorkflow.php b/src/applications/transactions/bulk/management/PhabricatorBulkManagementMakeSilentWorkflow.php new file mode 100644 index 0000000000..ca13a8f0ea --- /dev/null +++ b/src/applications/transactions/bulk/management/PhabricatorBulkManagementMakeSilentWorkflow.php @@ -0,0 +1,72 @@ +setName('make-silent') + ->setExamples('**make-silent** [options]') + ->setSynopsis( + pht('Configure a bulk job to execute silently.')) + ->setArguments( + array( + array( + 'name' => 'id', + 'param' => 'id', + 'help' => pht( + 'Configure bulk job __id__ to run silently (without sending '. + 'mail or publishing notifications).'), + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $viewer = $this->getViewer(); + + $id = $args->getArg('id'); + if (!$id) { + throw new PhutilArgumentUsageException( + pht('Use "--id" to choose a bulk job to make silent.')); + } + + $job = id(new PhabricatorWorkerBulkJobQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + if (!$job) { + throw new PhutilArgumentUsageException( + pht( + 'Unable to load bulk job with ID "%s".', + $id)); + } + + if ($job->getIsSilent()) { + echo tsprintf( + "%s\n", + pht('This job is already configured to run silently.')); + return 0; + } + + if ($job->getStatus() !== PhabricatorWorkerBulkJob::STATUS_CONFIRM) { + throw new PhutilArgumentUsageException( + pht( + 'Work has already started on job "%s". Jobs can not be '. + 'reconfigured after they have been started.', + $id)); + } + + $job + ->setIsSilent(true) + ->save(); + + echo tsprintf( + "%s\n", + pht( + 'Configured job "%s" to run silently.', + $id)); + + return 0; + } + +} diff --git a/src/applications/transactions/bulk/management/PhabricatorBulkManagementWorkflow.php b/src/applications/transactions/bulk/management/PhabricatorBulkManagementWorkflow.php new file mode 100644 index 0000000000..6533fedd80 --- /dev/null +++ b/src/applications/transactions/bulk/management/PhabricatorBulkManagementWorkflow.php @@ -0,0 +1,4 @@ +viewer = $viewer; + return $this; + } + + final public function getViewer() { + return $this->viewer; + } + + final public function setField(PhabricatorEditField $field) { + $this->field = $field; + return $this; + } + + final public function getField() { + return $this->field; + } + + abstract public function getPHUIXControlType(); + + public function getPHUIXControlSpecification() { + return array( + 'value' => null, + ); + } + +} diff --git a/src/applications/transactions/bulk/type/BulkPointsParameterType.php b/src/applications/transactions/bulk/type/BulkPointsParameterType.php new file mode 100644 index 0000000000..fe01db976a --- /dev/null +++ b/src/applications/transactions/bulk/type/BulkPointsParameterType.php @@ -0,0 +1,10 @@ +options = $options; + return $this; + } + + public function getOptions() { + return $this->options; + } + + public function getPHUIXControlType() { + return 'select'; + } + + public function getPHUIXControlSpecification() { + return array( + 'options' => $this->getOptions(), + 'order' => array_keys($this->getOptions()), + 'value' => null, + ); + } + +} diff --git a/src/applications/transactions/bulk/type/BulkStringParameterType.php b/src/applications/transactions/bulk/type/BulkStringParameterType.php new file mode 100644 index 0000000000..906ff9e2de --- /dev/null +++ b/src/applications/transactions/bulk/type/BulkStringParameterType.php @@ -0,0 +1,10 @@ +datasource = $datasource; + return $this; + } + + public function getDatasource() { + return $this->datasource; + } + + public function getPHUIXControlType() { + return 'tokenizer'; + } + + public function getPHUIXControlSpecification() { + $template = new AphrontTokenizerTemplateView(); + $template_markup = $template->render(); + + $datasource = $this->getDatasource(); + + return array( + 'markup' => (string)hsprintf('%s', $template_markup), + 'config' => array( + 'src' => $datasource->getDatasourceURI(), + 'browseURI' => $datasource->getBrowseURI(), + 'placeholder' => $datasource->getPlaceholderText(), + 'limit' => $datasource->getLimit(), + ), + 'value' => null, + ); + } + +} diff --git a/src/applications/transactions/editengine/PhabricatorEditEngine.php b/src/applications/transactions/editengine/PhabricatorEditEngine.php index 1e9d936929..ea3b987568 100644 --- a/src/applications/transactions/editengine/PhabricatorEditEngine.php +++ b/src/applications/transactions/editengine/PhabricatorEditEngine.php @@ -181,6 +181,11 @@ abstract class PhabricatorEditEngine $field ->setViewer($viewer) ->setObject($object); + + $group_key = $field->getBulkEditGroupKey(); + if ($group_key === null) { + $field->setBulkEditGroupKey('extension'); + } } $extension_fields = mpull($extension_fields, null, 'getKey'); @@ -2169,6 +2174,8 @@ abstract class PhabricatorEditEngine ->setTransactionType(PhabricatorTransactions::TYPE_CREATE); } + $is_strict = $request->getIsStrictlyTyped(); + foreach ($xactions as $xaction) { $type = $types[$xaction['type']]; @@ -2179,10 +2186,10 @@ abstract class PhabricatorEditEngine $parameter_type->setViewer($viewer); try { - $xaction['value'] = $parameter_type->getValue( - $xaction, - 'value', - $request->getIsStrictlyTyped()); + $value = $xaction['value']; + $value = $parameter_type->getValue($xaction, 'value', $is_strict); + $value = $type->getTransactionValueFromConduit($value); + $xaction['value'] = $value; } catch (Exception $ex) { throw new PhutilProxyException( pht( @@ -2219,7 +2226,6 @@ abstract class PhabricatorEditEngine } foreach ($field_types as $field_type) { - $field_type->setField($field); $types[$field_type->getEditType()] = $field_type; } } @@ -2421,6 +2427,204 @@ abstract class PhabricatorEditEngine } +/* -( Bulk Edits )--------------------------------------------------------- */ + + final public function newBulkEditGroupMap() { + $groups = $this->newBulkEditGroups(); + + $map = array(); + foreach ($groups as $group) { + $key = $group->getKey(); + + if (isset($map[$key])) { + throw new Exception( + pht( + 'Two bulk edit groups have the same key ("%s"). Each bulk edit '. + 'group must have a unique key.', + $key)); + } + + $map[$key] = $group; + } + + if ($this->isEngineExtensible()) { + $extensions = PhabricatorEditEngineExtension::getAllEnabledExtensions(); + } else { + $extensions = array(); + } + + foreach ($extensions as $extension) { + $extension_groups = $extension->newBulkEditGroups($this); + foreach ($extension_groups as $group) { + $key = $group->getKey(); + + if (isset($map[$key])) { + throw new Exception( + pht( + 'Extension "%s" defines a bulk edit group with the same key '. + '("%s") as the main editor or another extension. Each bulk '. + 'edit group must have a unique key.')); + } + + $map[$key] = $group; + } + } + + return $map; + } + + protected function newBulkEditGroups() { + return array( + id(new PhabricatorBulkEditGroup()) + ->setKey('default') + ->setLabel(pht('Primary Fields')), + id(new PhabricatorBulkEditGroup()) + ->setKey('extension') + ->setLabel(pht('Support Applications')), + ); + } + + final public function newBulkEditMap() { + $config = $this->loadDefaultConfiguration(); + if (!$config) { + throw new Exception( + pht('No default edit engine configuration for bulk edit.')); + } + + $object = $this->newEditableObject(); + $fields = $this->buildEditFields($object); + $groups = $this->newBulkEditGroupMap(); + + $edit_types = $this->getBulkEditTypesFromFields($fields); + + $map = array(); + foreach ($edit_types as $key => $type) { + $bulk_type = $type->getBulkParameterType(); + if ($bulk_type === null) { + continue; + } + + $bulk_label = $type->getBulkEditLabel(); + if ($bulk_label === null) { + continue; + } + + $group_key = $type->getBulkEditGroupKey(); + if (!$group_key) { + $group_key = 'default'; + } + + if (!isset($groups[$group_key])) { + throw new Exception( + pht( + 'Field "%s" has a bulk edit group key ("%s") with no '. + 'corresponding bulk edit group.', + $key, + $group_key)); + } + + $map[] = array( + 'label' => $bulk_label, + 'xaction' => $key, + 'group' => $group_key, + 'control' => array( + 'type' => $bulk_type->getPHUIXControlType(), + 'spec' => (object)$bulk_type->getPHUIXControlSpecification(), + ), + ); + } + + return $map; + } + + + final public function newRawBulkTransactions(array $xactions) { + $config = $this->loadDefaultConfiguration(); + if (!$config) { + throw new Exception( + pht('No default edit engine configuration for bulk edit.')); + } + + $object = $this->newEditableObject(); + $fields = $this->buildEditFields($object); + + $edit_types = $this->getBulkEditTypesFromFields($fields); + $template = $object->getApplicationTransactionTemplate(); + + $raw_xactions = array(); + foreach ($xactions as $key => $xaction) { + PhutilTypeSpec::checkMap( + $xaction, + array( + 'type' => 'string', + 'value' => 'optional wild', + )); + + $type = $xaction['type']; + if (!isset($edit_types[$type])) { + throw new Exception( + pht( + 'Unsupported bulk edit type "%s".', + $type)); + } + + $edit_type = $edit_types[$type]; + + // Replace the edit type with the underlying transaction type. Usually + // these are 1:1 and the transaction type just has more internal noise, + // but it's possible that this isn't the case. + $xaction['type'] = $edit_type->getTransactionType(); + + $value = $xaction['value']; + $value = $edit_type->getTransactionValueFromBulkEdit($value); + $xaction['value'] = $value; + + $xaction_objects = $edit_type->generateTransactions( + clone $template, + $xaction); + + foreach ($xaction_objects as $xaction_object) { + $raw_xaction = array( + 'type' => $xaction_object->getTransactionType(), + 'metadata' => $xaction_object->getMetadata(), + 'new' => $xaction_object->getNewValue(), + ); + + if ($xaction_object->hasOldValue()) { + $raw_xaction['old'] = $xaction_object->getOldValue(); + } + + if ($xaction_object->hasComment()) { + $comment = $xaction_object->getComment(); + $raw_xaction['comment'] = $comment->getContent(); + } + + $raw_xactions[] = $raw_xaction; + } + } + + return $raw_xactions; + } + + private function getBulkEditTypesFromFields(array $fields) { + $types = array(); + + foreach ($fields as $field) { + $field_types = $field->getBulkEditTypes(); + + if ($field_types === null) { + continue; + } + + foreach ($field_types as $field_type) { + $types[$field_type->getEditType()] = $field_type; + } + } + + return $types; + } + + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/transactions/editfield/PhabricatorEditField.php b/src/applications/transactions/editfield/PhabricatorEditField.php index 3cc6fe2fff..94c95a5052 100644 --- a/src/applications/transactions/editfield/PhabricatorEditField.php +++ b/src/applications/transactions/editfield/PhabricatorEditField.php @@ -17,6 +17,8 @@ abstract class PhabricatorEditField extends Phobject { private $previewPanel; private $controlID; private $controlInstructions; + private $bulkEditLabel; + private $bulkEditGroupKey; private $description; private $conduitDescription; @@ -45,6 +47,7 @@ abstract class PhabricatorEditField extends Phobject { private $isConduitOnly = false; private $conduitEditTypes; + private $bulkEditTypes; public function setKey($key) { $this->key = $key; @@ -64,6 +67,24 @@ abstract class PhabricatorEditField extends Phobject { return $this->label; } + public function setBulkEditLabel($bulk_edit_label) { + $this->bulkEditLabel = $bulk_edit_label; + return $this; + } + + public function getBulkEditLabel() { + return $this->bulkEditLabel; + } + + public function setBulkEditGroupKey($key) { + $this->bulkEditGroupKey = $key; + return $this; + } + + public function getBulkEditGroupKey() { + return $this->bulkEditGroupKey; + } + public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; @@ -625,6 +646,24 @@ abstract class PhabricatorEditField extends Phobject { return new AphrontStringHTTPParameterType(); } + protected function getBulkParameterType() { + $type = $this->newBulkParameterType(); + + if (!$type) { + return null; + } + + $type + ->setField($this) + ->setViewer($this->getViewer()); + + return $type; + } + + protected function newBulkParameterType() { + return null; + } + public function getConduitParameterType() { $type = $this->newConduitParameterType(); @@ -652,43 +691,49 @@ abstract class PhabricatorEditField extends Phobject { } protected function newEditType() { - $parameter_type = $this->getConduitParameterType(); - if (!$parameter_type) { - return null; - } - - return id(new PhabricatorSimpleEditType()) - ->setConduitParameterType($parameter_type); + return new PhabricatorSimpleEditType(); } protected function getEditType() { $transaction_type = $this->getTransactionType(); - if ($transaction_type === null) { return null; } - $type_key = $this->getEditTypeKey(); $edit_type = $this->newEditType(); if (!$edit_type) { return null; } - return $edit_type - ->setEditType($type_key) + $type_key = $this->getEditTypeKey(); + + $edit_type + ->setEditField($this) ->setTransactionType($transaction_type) + ->setEditType($type_key) ->setMetadata($this->getMetadata()); + + if (!$edit_type->getConduitParameterType()) { + $conduit_parameter = $this->getConduitParameterType(); + if ($conduit_parameter) { + $edit_type->setConduitParameterType($conduit_parameter); + } + } + + if (!$edit_type->getBulkParameterType()) { + $bulk_parameter = $this->getBulkParameterType(); + if ($bulk_parameter) { + $edit_type->setBulkParameterType($bulk_parameter); + } + } + + return $edit_type; } final public function getConduitEditTypes() { if ($this->conduitEditTypes === null) { $edit_types = $this->newConduitEditTypes(); $edit_types = mpull($edit_types, null, 'getEditType'); - - foreach ($edit_types as $edit_type) { - $edit_type->setEditField($this); - } - $this->conduitEditTypes = $edit_types; } @@ -718,6 +763,39 @@ abstract class PhabricatorEditField extends Phobject { return array($edit_type); } + final public function getBulkEditTypes() { + if ($this->bulkEditTypes === null) { + $edit_types = $this->newBulkEditTypes(); + $edit_types = mpull($edit_types, null, 'getEditType'); + $this->bulkEditTypes = $edit_types; + } + + return $this->bulkEditTypes; + } + + final public function getBulkEditType($key) { + $edit_types = $this->getBulkEditTypes(); + + if (empty($edit_types[$key])) { + throw new Exception( + pht( + 'This EditField does not provide a Bulk EditType with key "%s".', + $key)); + } + + return $edit_types[$key]; + } + + protected function newBulkEditTypes() { + $edit_type = $this->getEditType(); + + if (!$edit_type) { + return array(); + } + + return array($edit_type); + } + public function getCommentAction() { $label = $this->getCommentActionLabel(); if ($label === null) { diff --git a/src/applications/transactions/editfield/PhabricatorPHIDListEditField.php b/src/applications/transactions/editfield/PhabricatorPHIDListEditField.php index 7195741c43..b084c142e5 100644 --- a/src/applications/transactions/editfield/PhabricatorPHIDListEditField.php +++ b/src/applications/transactions/editfield/PhabricatorPHIDListEditField.php @@ -98,10 +98,12 @@ abstract class PhabricatorPHIDListEditField return new PhabricatorEdgeEditType(); } - $type = new PhabricatorDatasourceEditType(); - $type->setIsSingleValue($this->getIsSingleValue()); - $type->setConduitParameterType($this->newConduitParameterType()); - return $type; + return id(new PhabricatorDatasourceEditType()) + ->setIsSingleValue($this->getIsSingleValue()); + } + + protected function newBulkEditTypes() { + return $this->newConduitEditTypes(); } protected function newConduitEditTypes() { diff --git a/src/applications/transactions/editfield/PhabricatorPointsEditField.php b/src/applications/transactions/editfield/PhabricatorPointsEditField.php index 48c1b3b35b..a8c6d345f3 100644 --- a/src/applications/transactions/editfield/PhabricatorPointsEditField.php +++ b/src/applications/transactions/editfield/PhabricatorPointsEditField.php @@ -14,4 +14,9 @@ final class PhabricatorPointsEditField protected function newCommentAction() { return id(new PhabricatorEditEnginePointsCommentAction()); } + + protected function newBulkParameterType() { + return new BulkPointsParameterType(); + } + } diff --git a/src/applications/transactions/editfield/PhabricatorRemarkupEditField.php b/src/applications/transactions/editfield/PhabricatorRemarkupEditField.php index bb221a0de1..039f0b368f 100644 --- a/src/applications/transactions/editfield/PhabricatorRemarkupEditField.php +++ b/src/applications/transactions/editfield/PhabricatorRemarkupEditField.php @@ -11,4 +11,8 @@ final class PhabricatorRemarkupEditField return new ConduitStringParameterType(); } + protected function newBulkParameterType() { + return new BulkRemarkupParameterType(); + } + } diff --git a/src/applications/transactions/editfield/PhabricatorSelectEditField.php b/src/applications/transactions/editfield/PhabricatorSelectEditField.php index fa565dbe61..7e98d84b2f 100644 --- a/src/applications/transactions/editfield/PhabricatorSelectEditField.php +++ b/src/applications/transactions/editfield/PhabricatorSelectEditField.php @@ -54,6 +54,11 @@ final class PhabricatorSelectEditField return new ConduitStringParameterType(); } + protected function newBulkParameterType() { + return id(new BulkSelectParameterType()) + ->setOptions($this->getOptions()); + } + private function getCanonicalValue($value) { $options = $this->getOptions(); if (!isset($options[$value])) { diff --git a/src/applications/transactions/editfield/PhabricatorTextEditField.php b/src/applications/transactions/editfield/PhabricatorTextEditField.php index fa51ff6142..68854cf2e2 100644 --- a/src/applications/transactions/editfield/PhabricatorTextEditField.php +++ b/src/applications/transactions/editfield/PhabricatorTextEditField.php @@ -29,4 +29,8 @@ final class PhabricatorTextEditField return new ConduitStringParameterType(); } + protected function newBulkParameterType() { + return new BulkStringParameterType(); + } + } diff --git a/src/applications/transactions/editfield/PhabricatorTokenizerEditField.php b/src/applications/transactions/editfield/PhabricatorTokenizerEditField.php index 237f315b51..ba7514085d 100644 --- a/src/applications/transactions/editfield/PhabricatorTokenizerEditField.php +++ b/src/applications/transactions/editfield/PhabricatorTokenizerEditField.php @@ -56,4 +56,16 @@ abstract class PhabricatorTokenizerEditField return $action; } + protected function newBulkParameterType() { + $datasource = $this->newDatasource() + ->setViewer($this->getViewer()); + + if ($this->getIsSingleValue()) { + $datasource->setLimit(1); + } + + return id(new BulkTokenizerParameterType()) + ->setDatasource($datasource); + } + } diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index c08451624d..7dbd41f1e4 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -59,7 +59,6 @@ abstract class PhabricatorApplicationTransactionEditor private $isHeraldEditor; private $isInverseEdgeEditor; private $actingAsPHID; - private $disableEmail; private $heraldEmailPHIDs = array(); private $heraldForcedEmailPHIDs = array(); @@ -69,7 +68,9 @@ abstract class PhabricatorApplicationTransactionEditor private $feedNotifyPHIDs = array(); private $feedRelatedPHIDs = array(); private $feedShouldPublish = false; + private $mailShouldSend = false; private $modularTypes; + private $silent; private $transactionQueue = array(); @@ -188,6 +189,15 @@ abstract class PhabricatorApplicationTransactionEditor return $this->isPreview; } + public function setIsSilent($silent) { + $this->silent = $silent; + return $this; + } + + public function getIsSilent() { + return $this->silent; + } + public function setIsInverseEdgeEditor($is_inverse_edge_editor) { $this->isInverseEdgeEditor = $is_inverse_edge_editor; return $this; @@ -206,21 +216,6 @@ abstract class PhabricatorApplicationTransactionEditor return $this->isHeraldEditor; } - /** - * Prevent this editor from generating email when applying transactions. - * - * @param bool True to disable email. - * @return this - */ - public function setDisableEmail($disable_email) { - $this->disableEmail = $disable_email; - return $this; - } - - public function getDisableEmail() { - return $this->disableEmail; - } - public function setUnmentionablePHIDMap(array $map) { $this->unmentionablePHIDMap = $map; return $this; @@ -806,6 +801,10 @@ abstract class PhabricatorApplicationTransactionEditor $xaction->setObjectPHID($object->getPHID()); } + if ($this->getIsSilent()) { + $xaction->setIsSilentTransaction(true); + } + return $xaction; } @@ -1152,17 +1151,22 @@ abstract class PhabricatorApplicationTransactionEditor // Editors need to pass into workers. $object = $this->willPublish($object, $xactions); - if (!$this->getDisableEmail()) { + if (!$this->getIsSilent()) { if ($this->shouldSendMail($object, $xactions)) { + $this->mailShouldSend = true; $this->mailToPHIDs = $this->getMailTo($object); $this->mailCCPHIDs = $this->getMailCC($object); } - } - if ($this->shouldPublishFeedStory($object, $xactions)) { - $this->feedShouldPublish = true; - $this->feedRelatedPHIDs = $this->getFeedRelatedPHIDs($object, $xactions); - $this->feedNotifyPHIDs = $this->getFeedNotifyPHIDs($object, $xactions); + if ($this->shouldPublishFeedStory($object, $xactions)) { + $this->feedShouldPublish = true; + $this->feedRelatedPHIDs = $this->getFeedRelatedPHIDs( + $object, + $xactions); + $this->feedNotifyPHIDs = $this->getFeedNotifyPHIDs( + $object, + $xactions); + } } PhabricatorWorker::scheduleTask( @@ -1204,10 +1208,8 @@ abstract class PhabricatorApplicationTransactionEditor $this->object = $object; $messages = array(); - if (!$this->getDisableEmail()) { - if ($this->shouldSendMail($object, $xactions)) { - $messages = $this->buildMail($object, $xactions); - } + if ($this->mailShouldSend) { + $messages = $this->buildMail($object, $xactions); } if ($this->supportsSearch()) { @@ -2544,6 +2546,12 @@ abstract class PhabricatorApplicationTransactionEditor return $messages; } + protected function getTransactionsForMail( + PhabricatorLiskDAO $object, + array $xactions) { + return $xactions; + } + private function buildMailForTarget( PhabricatorLiskDAO $object, array $xactions, @@ -2564,17 +2572,19 @@ abstract class PhabricatorApplicationTransactionEditor return null; } - $mail = $this->buildMailTemplate($object); - $body = $this->buildMailBody($object, $xactions); + $mail_xactions = $this->getTransactionsForMail($object, $xactions); - $mail_tags = $this->getMailTags($object, $xactions); - $action = $this->getMailAction($object, $xactions); + $mail = $this->buildMailTemplate($object); + $body = $this->buildMailBody($object, $mail_xactions); + + $mail_tags = $this->getMailTags($object, $mail_xactions); + $action = $this->getMailAction($object, $mail_xactions); if (PhabricatorEnv::getEnvConfig('metamta.email-preferences')) { $this->addEmailPreferenceSectionToMailBody( $body, $object, - $xactions); + $mail_xactions); } $mail @@ -3158,6 +3168,16 @@ abstract class PhabricatorApplicationTransactionEditor $adapter->setApplicationEmail($this->getApplicationEmail()); } + // If this editor is operating in silent mode, tell Herald that we aren't + // going to send any mail. This allows it to skip "the first time this + // rule matches, send me an email" rules which would otherwise match even + // though we aren't going to send any mail. + if ($this->getIsSilent()) { + $adapter->setForbiddenAction( + HeraldMailableState::STATECONST, + HeraldCoreStateReasons::REASON_SILENT); + } + $xscript = HeraldEngine::loadAndApplyRules($adapter); $this->setHeraldAdapter($adapter); @@ -3504,7 +3524,6 @@ abstract class PhabricatorApplicationTransactionEditor private function getAutomaticStateProperties() { return array( 'parentMessageID', - 'disableEmail', 'isNewObject', 'heraldEmailPHIDs', 'heraldForcedEmailPHIDs', @@ -3514,6 +3533,7 @@ abstract class PhabricatorApplicationTransactionEditor 'feedNotifyPHIDs', 'feedRelatedPHIDs', 'feedShouldPublish', + 'mailShouldSend', ); } @@ -3896,7 +3916,8 @@ abstract class PhabricatorApplicationTransactionEditor ->setActor($this->getActor()) ->setContentSource($this->getContentSource()) ->setContinueOnNoEffect($this->getContinueOnNoEffect()) - ->setContinueOnMissingFields($this->getContinueOnMissingFields()); + ->setContinueOnMissingFields($this->getContinueOnMissingFields()) + ->setIsSilent($this->getIsSilent()); if ($this->actingAsPHID !== null) { $editor->setActingAsPHID($this->actingAsPHID); diff --git a/src/applications/transactions/edittype/PhabricatorCommentEditType.php b/src/applications/transactions/edittype/PhabricatorCommentEditType.php index 2ab24677f7..38194e4498 100644 --- a/src/applications/transactions/edittype/PhabricatorCommentEditType.php +++ b/src/applications/transactions/edittype/PhabricatorCommentEditType.php @@ -6,6 +6,10 @@ final class PhabricatorCommentEditType extends PhabricatorEditType { return new ConduitStringParameterType(); } + protected function newBulkParameterType() { + return new BulkRemarkupParameterType(); + } + public function generateTransactions( PhabricatorApplicationTransaction $template, array $spec) { diff --git a/src/applications/transactions/edittype/PhabricatorEdgeEditType.php b/src/applications/transactions/edittype/PhabricatorEdgeEditType.php index 0c376423cd..9398cfb9aa 100644 --- a/src/applications/transactions/edittype/PhabricatorEdgeEditType.php +++ b/src/applications/transactions/edittype/PhabricatorEdgeEditType.php @@ -34,4 +34,13 @@ final class PhabricatorEdgeEditType return array($xaction); } + protected function newBulkParameterType() { + if (!$this->getDatasource()) { + return null; + } + + return id(new BulkTokenizerParameterType()) + ->setDatasource($this->getDatasource()); + } + } diff --git a/src/applications/transactions/edittype/PhabricatorEditType.php b/src/applications/transactions/edittype/PhabricatorEditType.php index 1451ec2c04..95fa5bf3c7 100644 --- a/src/applications/transactions/edittype/PhabricatorEditType.php +++ b/src/applications/transactions/edittype/PhabricatorEditType.php @@ -6,7 +6,6 @@ abstract class PhabricatorEditType extends Phobject { private $editField; private $transactionType; private $label; - private $field; private $metadata = array(); private $conduitDescription; @@ -14,6 +13,10 @@ abstract class PhabricatorEditType extends Phobject { private $conduitTypeDescription; private $conduitParameterType; + private $bulkParameterType; + private $bulkEditLabel; + private $bulkEditGroupKey; + public function setLabel($label) { $this->label = $label; return $this; @@ -23,13 +26,30 @@ abstract class PhabricatorEditType extends Phobject { return $this->label; } - public function setField(PhabricatorEditField $field) { - $this->field = $field; + public function setBulkEditLabel($bulk_edit_label) { + $this->bulkEditLabel = $bulk_edit_label; return $this; } - public function getField() { - return $this->field; + public function getBulkEditLabel() { + if ($this->bulkEditLabel !== null) { + return $this->bulkEditLabel; + } + + return $this->getEditField()->getBulkEditLabel(); + } + + public function setBulkEditGroupKey($key) { + $this->bulkEditGroupKey = $key; + return $this; + } + + public function getBulkEditGroupKey() { + if ($this->bulkEditGroupKey !== null) { + return $this->bulkEditGroupKey; + } + + return $this->getEditField()->getBulkEditGroupKey(); } public function setEditType($edit_type) { @@ -85,6 +105,38 @@ abstract class PhabricatorEditType extends Phobject { return $this->editField; } + protected function getTransactionValueFromValue($value) { + return $value; + } + + +/* -( Bulk )--------------------------------------------------------------- */ + + + protected function newBulkParameterType() { + if ($this->bulkParameterType) { + return clone $this->bulkParameterType; + } + + return null; + } + + + public function setBulkParameterType(BulkParameterType $type) { + $this->bulkParameterType = $type; + return $this; + } + + + public function getBulkParameterType() { + return $this->newBulkParameterType(); + } + + public function getTransactionValueFromBulkEdit($value) { + return $this->getTransactionValueFromValue($value); + } + + /* -( Conduit )------------------------------------------------------------ */ @@ -162,4 +214,8 @@ abstract class PhabricatorEditType extends Phobject { return $this->conduitDocumentation; } + public function getTransactionValueFromConduit($value) { + return $this->getTransactionValueFromValue($value); + } + } diff --git a/src/applications/transactions/engineextension/PhabricatorCommentEditEngineExtension.php b/src/applications/transactions/engineextension/PhabricatorCommentEditEngineExtension.php index 3a01756ea7..c5da130b68 100644 --- a/src/applications/transactions/engineextension/PhabricatorCommentEditEngineExtension.php +++ b/src/applications/transactions/engineextension/PhabricatorCommentEditEngineExtension.php @@ -33,6 +33,14 @@ final class PhabricatorCommentEditEngineExtension return (bool)$comment; } + public function newBulkEditGroups(PhabricatorEditEngine $engine) { + return array( + id(new PhabricatorBulkEditGroup()) + ->setKey('comments') + ->setLabel(pht('Comments')), + ); + } + public function buildCustomEditFields( PhabricatorEditEngine $engine, PhabricatorApplicationTransactionInterface $object) { @@ -46,6 +54,8 @@ final class PhabricatorCommentEditEngineExtension $comment_field = id(new PhabricatorCommentEditField()) ->setKey(self::EDITKEY) ->setLabel(pht('Comments')) + ->setBulkEditLabel(pht('Add comment')) + ->setBulkEditGroupKey('comments') ->setAliases(array('comments')) ->setIsHidden(true) ->setIsReorderable(false) diff --git a/src/applications/transactions/engineextension/PhabricatorEditEngineExtension.php b/src/applications/transactions/engineextension/PhabricatorEditEngineExtension.php index 9a799d4e5b..f2f22b158b 100644 --- a/src/applications/transactions/engineextension/PhabricatorEditEngineExtension.php +++ b/src/applications/transactions/engineextension/PhabricatorEditEngineExtension.php @@ -32,6 +32,10 @@ abstract class PhabricatorEditEngineExtension extends Phobject { PhabricatorEditEngine $engine, PhabricatorApplicationTransactionInterface $object); + public function newBulkEditGroups(PhabricatorEditEngine $engine) { + return array(); + } + final public static function getAllExtensions() { return id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) diff --git a/src/applications/transactions/engineextension/PhabricatorSubtypeEditEngineExtension.php b/src/applications/transactions/engineextension/PhabricatorSubtypeEditEngineExtension.php index a47a4d1b55..260c0d2acf 100644 --- a/src/applications/transactions/engineextension/PhabricatorSubtypeEditEngineExtension.php +++ b/src/applications/transactions/engineextension/PhabricatorSubtypeEditEngineExtension.php @@ -30,6 +30,9 @@ final class PhabricatorSubtypeEditEngineExtension $subtype_type = PhabricatorTransactions::TYPE_SUBTYPE; + $map = $object->newEditEngineSubtypeMap(); + $options = mpull($map, 'getName'); + $subtype_field = id(new PhabricatorSelectEditField()) ->setKey(self::EDITKEY) ->setLabel(pht('Subtype')) @@ -41,7 +44,8 @@ final class PhabricatorSubtypeEditEngineExtension ->setTransactionType($subtype_type) ->setConduitDescription(pht('Change the object subtype.')) ->setConduitTypeDescription(pht('New object subtype key.')) - ->setValue($object->getEditEngineSubtype()); + ->setValue($object->getEditEngineSubtype()) + ->setOptions($options); return array( $subtype_field, diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index bf3c616df9..d49c3bf675 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -158,6 +158,14 @@ abstract class PhabricatorApplicationTransaction return (bool)$this->getMetadataValue('core.default', false); } + public function setIsSilentTransaction($silent) { + return $this->setMetadataValue('core.silent', $silent); + } + + public function getIsSilentTransaction() { + return (bool)$this->getMetadataValue('core.silent', false); + } + public function attachComment( PhabricatorApplicationTransactionComment $comment) { $this->comment = $comment; @@ -1515,6 +1523,12 @@ abstract class PhabricatorApplicationTransaction if ($apart > (60 * 2)) { return false; } + + // Don't group silent and nonsilent transactions together. + $is_silent = $this->getIsSilentTransaction(); + if ($is_silent != $xaction->getIsSilentTransaction()) { + return false; + } } return true; diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionView.php index 0d427419d5..e10f5c008e 100644 --- a/src/applications/transactions/view/PhabricatorApplicationTransactionView.php +++ b/src/applications/transactions/view/PhabricatorApplicationTransactionView.php @@ -423,7 +423,8 @@ class PhabricatorApplicationTransactionView extends AphrontView { ->setUserHandle($xaction->getHandle($xaction->getAuthorPHID())) ->setIcon($xaction->getIcon()) ->setColor($xaction->getColor()) - ->setHideCommentOptions($this->getHideCommentOptions()); + ->setHideCommentOptions($this->getHideCommentOptions()) + ->setIsSilent($xaction->getIsSilentTransaction()); list($token, $token_removed) = $xaction->getToken(); if ($token) { diff --git a/src/docs/user/field/revoking_credentials.diviner b/src/docs/user/field/revoking_credentials.diviner new file mode 100644 index 0000000000..b1e18bcd97 --- /dev/null +++ b/src/docs/user/field/revoking_credentials.diviner @@ -0,0 +1,101 @@ +@title Revoking Credentials +@group fieldmanual + +Revoking credentials, tokens, and sessions. + +Overview +======== + +If you've become aware of a security breach that affects you, you may want to +revoke or cycle credentials in case anything was leaked. + +You can revoke credentials with the `bin/auth revoke` tool. This document +describes how to use the tool and how revocation works. + + +bin/auth revoke +=============== + +The `bin/auth revoke` tool revokes specified sets of credentials from +specified targets. For example, if you believe `@alice` may have had her SSH +key compromised, you can revoke her keys like this: + +``` +phabricator/ $ ./bin/auth revoke --type ssh --from @alice +``` + +The flag `--everything` revokes all credential types. + +The flag `--everywhere` revokes credentials from all objects. For most +credential types this means "all users", but some credentials (like SSH keys) +can also be associated with other kinds of objects. + +Note that revocation can be disruptive (users must choose new passwords, +generate new API tokens, configure new SSH keys, etc) and can not be easily +undone if you perform an excessively broad revocation. + +You can use the `--list` flag to get a list of available credential types +which can be revoked. This includes upstream credential types, and may include +third-party credential types if you have extensions installed. + +To list all revokable credential types: + +``` +phabricator/ $ ./bin/auth revoke --list +``` + +To get details about exactly how a specific revoker works: + +``` +phabricator/ $ ./bin/auth revoke --list --type ssh +``` + + +Revocation vs Removal +===================== + +Generally, `bin/auth revoke` **revokes** credentials, rather than just deleting +or removing them. That is, the credentials are moved to a permanent revocation +list of invalid credentials. + +For example, revoking an SSH key prevents users from adding that key back to +their account: they must generate and add a new, unique key. Likewise, revoked +passwords can not be reused. + +Although it is technically possible to reinstate credentials by removing them +from revocation lists, there are no tools available for this and you should +treat revocation lists as permanent. + + +Scenarios +========= + +**Network Compromise**: If you believe you may have been affected by a network +compromise (where an attacker may have observed data transmitted over the +network), you should revoke the `password`, `conduit`, `session`, and +`temporary` credentials for all users. This will revoke all credentials which +are normally sent over the network. + +You can revoke these credentials by running these commands: + +``` +phabricator/ $ ./bin/auth revoke --type password --everywhere +phabricator/ $ ./bin/auth revoke --type conduit --everywhere +phabricator/ $ ./bin/auth revoke --type session --everywhere +phabricator/ $ ./bin/auth revoke --type temporary --everywhere +``` + +Depending on the nature of the compromise you may also consider revoking `ssh` +credentials, although these are usually not sent over the network because +they are asymmetric. + +**User Compromise**: If you believe a user's credentials have been compromised +(for example, maybe they lost a phone or laptop) you should revoke +`--everything` from their account. This will revoke all of their outstanding +credentials without affecting other users. + +You can revoke all credentials for a user by running this command: + +``` +phabricator/ $ ./bin/auth revoke --everything --from @alice +``` diff --git a/src/docs/user/userguide/herald.diviner b/src/docs/user/userguide/herald.diviner index c5a008474d..b7cfdeca95 100644 --- a/src/docs/user/userguide/herald.diviner +++ b/src/docs/user/userguide/herald.diviner @@ -20,7 +20,7 @@ For example, you can write a personal rule like this which triggers on tasks: > When [ all of ] these conditions are met: > [ Title ][ contains ][ quasar ] -> Take these actions [ every time ] this rule matches: +> Take these actions [ every time this rule matches: ] > [ Add me as a subscriber ] This rule will automatically subscribe you to any newly created or updated @@ -106,6 +106,44 @@ had it actually been updated. Dry runs executed via the test console don't take any actions. +Action Repetition Settings +========================== + +Rules can be configured to act in different ways: + +**Every time the rule matches:** The rule will take actions every time the +object is updated if the rule's conditions match the current object state. + +**Only the first time the rule matches:** The rule will take actions only once +per object, regardless of how many times the object is updated. After the rule +acts once, it won't run on the same object again. + +**If this rule did not match the last time:** This rule will take actions the +first time it matches for an object. After that, it won't act unless the object +just changed from not matching to matching. + +For example, suppose you have a rule like this: + +> When: +> [ Title ][ contains ][ duck ] +> Take actions [ if this rule did not match the last time: ] +> [ Add comment ][ "Please prefer the term 'budget goose'." ] + +If you set this rule to act "every time", it will leave a comment on the task +for every single update until the title is edited. This is usually pretty noisy. + +If you set this rule to act "only the first time", it will only leave one +comment. This fixes the noise problem, but creates a new problem: if someone +edits the title, then a later change breaks it again, the rule won't leave +another reminder comment. + +If you set this rule to act "if it did not match the last time", it will leave +one comment on matching tasks. If the task is fixed (by replacing the term +"duck" with the term "budget goose", so the object no longer matches the rule) +and then later changed to violate the rule again (by putting the term +"duck" back in the title), the rule will act again. + + Advanced Herald =============== diff --git a/src/infrastructure/customfield/editor/PhabricatorCustomFieldEditField.php b/src/infrastructure/customfield/editor/PhabricatorCustomFieldEditField.php index ffdb157d8e..1153f82a34 100644 --- a/src/infrastructure/customfield/editor/PhabricatorCustomFieldEditField.php +++ b/src/infrastructure/customfield/editor/PhabricatorCustomFieldEditField.php @@ -6,6 +6,7 @@ final class PhabricatorCustomFieldEditField private $customField; private $httpParameterType; private $conduitParameterType; + private $bulkParameterType; public function setCustomField(PhabricatorCustomField $custom_field) { $this->customField = $custom_field; @@ -36,6 +37,16 @@ final class PhabricatorCustomFieldEditField return $this->conduitParameterType; } + public function setCustomFieldBulkParameterType( + BulkParameterType $type) { + $this->bulkParameterType = $type; + return $this; + } + + public function getCustomFieldBulkParameterType() { + return $this->bulkParameterType; + } + protected function buildControl() { if ($this->getIsConduitOnly()) { return null; @@ -51,15 +62,8 @@ final class PhabricatorCustomFieldEditField } protected function newEditType() { - $type = id(new PhabricatorCustomFieldEditType()) + return id(new PhabricatorCustomFieldEditType()) ->setCustomField($this->getCustomField()); - - $conduit_type = $this->newConduitParameterType(); - if ($conduit_type) { - $type->setConduitParameterType($conduit_type); - } - - return $type; } public function getValueForTransaction() { @@ -116,6 +120,16 @@ final class PhabricatorCustomFieldEditField return null; } + protected function newBulkParameterType() { + $type = $this->getCustomFieldBulkParameterType(); + + if ($type) { + return clone $type; + } + + return null; + } + public function getAllReadValueFromRequestKeys() { $keys = array(); diff --git a/src/infrastructure/customfield/editor/PhabricatorCustomFieldEditType.php b/src/infrastructure/customfield/editor/PhabricatorCustomFieldEditType.php index 2f8008c699..88c8fe07a8 100644 --- a/src/infrastructure/customfield/editor/PhabricatorCustomFieldEditType.php +++ b/src/infrastructure/customfield/editor/PhabricatorCustomFieldEditType.php @@ -40,4 +40,14 @@ final class PhabricatorCustomFieldEditType return array($xaction); } + protected function getTransactionValueFromValue($value) { + $field = $this->getCustomField(); + + // Avoid changing the value of the field itself, since later calls would + // incorrectly reflect the new value. + $clone = clone $field; + $clone->setValueFromApplicationTransactions($value); + return $clone->getNewValueForApplicationTransactions(); + } + } diff --git a/src/infrastructure/customfield/engineextension/PhabricatorCustomFieldEditEngineExtension.php b/src/infrastructure/customfield/engineextension/PhabricatorCustomFieldEditEngineExtension.php index 3edc898cb7..cec665a037 100644 --- a/src/infrastructure/customfield/engineextension/PhabricatorCustomFieldEditEngineExtension.php +++ b/src/infrastructure/customfield/engineextension/PhabricatorCustomFieldEditEngineExtension.php @@ -23,6 +23,14 @@ final class PhabricatorCustomFieldEditEngineExtension return ($object instanceof PhabricatorCustomFieldInterface); } + public function newBulkEditGroups(PhabricatorEditEngine $engine) { + return array( + id(new PhabricatorBulkEditGroup()) + ->setKey('custom') + ->setLabel(pht('Custom Fields')), + ); + } + public function buildCustomEditFields( PhabricatorEditEngine $engine, PhabricatorApplicationTransactionInterface $object) { @@ -43,6 +51,11 @@ final class PhabricatorCustomFieldEditEngineExtension foreach ($field_list->getFields() as $field) { $edit_fields = $field->getEditEngineFields($engine); foreach ($edit_fields as $edit_field) { + $group_key = $edit_field->getBulkEditGroupKey(); + if ($group_key === null) { + $edit_field->setBulkEditGroupKey('custom'); + } + $results[] = $edit_field; } } diff --git a/src/infrastructure/customfield/field/PhabricatorCustomField.php b/src/infrastructure/customfield/field/PhabricatorCustomField.php index f7f4403402..c96f09f369 100644 --- a/src/infrastructure/customfield/field/PhabricatorCustomField.php +++ b/src/infrastructure/customfield/field/PhabricatorCustomField.php @@ -1119,6 +1119,11 @@ abstract class PhabricatorCustomField extends Phobject { $field->setCustomFieldConduitParameterType($conduit_type); } + $bulk_type = $this->getBulkParameterType(); + if ($bulk_type) { + $field->setCustomFieldBulkParameterType($bulk_type); + } + return $field; } @@ -1133,16 +1138,38 @@ abstract class PhabricatorCustomField extends Phobject { $conduit_only = false; } + $bulk_label = $this->getBulkEditLabel(); + return $this->newEditField() ->setKey($this->getFieldKey()) ->setEditTypeKey($this->getModernFieldKey()) ->setLabel($this->getFieldName()) + ->setBulkEditLabel($bulk_label) ->setDescription($this->getFieldDescription()) ->setTransactionType($this->getApplicationTransactionType()) ->setIsConduitOnly($conduit_only) ->setValue($this->getNewValueForApplicationTransactions()); } + protected function getBulkEditLabel() { + if ($this->proxy) { + return $this->proxy->getBulkEditLabel(); + } + + return pht('Set "%s" to', $this->getFieldName()); + } + + public function getBulkParameterType() { + return $this->newBulkParameterType(); + } + + protected function newBulkParameterType() { + if ($this->proxy) { + return $this->proxy->newBulkParameterType(); + } + return null; + } + protected function getHTTPParameterType() { if ($this->proxy) { return $this->proxy->getHTTPParameterType(); diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldSelect.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldSelect.php index db84fc82e8..036b7301a1 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldSelect.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldSelect.php @@ -148,4 +148,9 @@ final class PhabricatorStandardCustomFieldSelect return new ConduitStringParameterType(); } + protected function newBulkParameterType() { + return id(new BulkSelectParameterType()) + ->setOptions($this->getOptions()); + } + } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php index 9cb19d029d..cdb54fb7e9 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php @@ -65,6 +65,18 @@ abstract class PhabricatorStandardCustomFieldTokenizer return new ConduitPHIDListParameterType(); } + protected function newBulkParameterType() { + $datasource = $this->getDatasource(); + + $limit = $this->getFieldConfigValue('limit'); + if ($limit) { + $datasource->setLimit($limit); + } + + return id(new BulkTokenizerParameterType()) + ->setDatasource($datasource); + } + public function shouldAppearInHeraldActions() { return true; } @@ -87,7 +99,14 @@ abstract class PhabricatorStandardCustomFieldTokenizer } public function getHeraldActionDatasource() { - return $this->getDatasource(); + $datasource = $this->getDatasource(); + + $limit = $this->getFieldConfigValue('limit'); + if ($limit) { + $datasource->setLimit($limit); + } + + return $datasource; } private function renderHeraldHandleList($value) { diff --git a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php index feb25a0128..40e43c2ec7 100644 --- a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php +++ b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php @@ -21,6 +21,7 @@ final class PhabricatorWorkerBulkJob protected $status; protected $parameters = array(); protected $size; + protected $isSilent; private $jobImplementation = self::ATTACHABLE; @@ -34,6 +35,7 @@ final class PhabricatorWorkerBulkJob 'jobTypeKey' => 'text32', 'status' => 'text32', 'size' => 'uint32', + 'isSilent' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_type' => array( @@ -58,7 +60,8 @@ final class PhabricatorWorkerBulkJob ->setAuthorPHID($actor->getPHID()) ->setJobTypeKey($type->getBulkJobTypeKey()) ->setParameters($parameters) - ->attachJobImplementation($type); + ->attachJobImplementation($type) + ->setIsSilent(0); $job->setSize($job->computeSize()); diff --git a/src/infrastructure/export/PhabricatorCSVExportFormat.php b/src/infrastructure/export/PhabricatorCSVExportFormat.php new file mode 100644 index 0000000000..d9662467ec --- /dev/null +++ b/src/infrastructure/export/PhabricatorCSVExportFormat.php @@ -0,0 +1,47 @@ + $field) { + $value = $map[$key]; + $value = $field->getTextValue($value); + + if (preg_match('/\s|,|\"/', $value)) { + $value = str_replace('"', '""', $value); + $value = '"'.$value.'"'; + } + + $values[] = $value; + } + + $this->rows[] = implode(',', $values); + } + + public function newFileData() { + return implode("\n", $this->rows); + } + +} diff --git a/src/infrastructure/export/PhabricatorEpochExportField.php b/src/infrastructure/export/PhabricatorEpochExportField.php new file mode 100644 index 0000000000..a19e60b50e --- /dev/null +++ b/src/infrastructure/export/PhabricatorEpochExportField.php @@ -0,0 +1,27 @@ +zone)) { + $this->zone = new DateTimeZone('UTC'); + } + + try { + $date = new DateTime('@'.$value); + } catch (Exception $ex) { + return null; + } + + $date->setTimezone($this->zone); + return $date->format('c'); + } + + public function getNaturalValue($value) { + return (int)$value; + } + +} diff --git a/src/infrastructure/export/PhabricatorExportField.php b/src/infrastructure/export/PhabricatorExportField.php new file mode 100644 index 0000000000..3efb7a8b9a --- /dev/null +++ b/src/infrastructure/export/PhabricatorExportField.php @@ -0,0 +1,35 @@ +key = $key; + return $this; + } + + public function getKey() { + return $this->key; + } + + public function setLabel($label) { + $this->label = $label; + return $this; + } + + public function getLabel() { + return $this->label; + } + + public function getTextValue($value) { + return (string)$this->getNaturalValue($value); + } + + public function getNaturalValue($value) { + return $value; + } + +} diff --git a/src/infrastructure/export/PhabricatorExportFormat.php b/src/infrastructure/export/PhabricatorExportFormat.php new file mode 100644 index 0000000000..a1da4e90d8 --- /dev/null +++ b/src/infrastructure/export/PhabricatorExportFormat.php @@ -0,0 +1,51 @@ +getPhobjectClassConstant('EXPORTKEY'); + } + + final public function setViewer(PhabricatorUser $viewer) { + $this->viewer = $viewer; + return $this; + } + + final public function getViewer() { + return $this->viewer; + } + + abstract public function getExportFormatName(); + abstract public function getMIMEContentType(); + abstract public function getFileExtension(); + + abstract public function addObject($object, array $fields, array $map); + abstract public function newFileData(); + + public function isExportFormatEnabled() { + return true; + } + + final public static function getAllExportFormats() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getExportFormatKey') + ->execute(); + } + + final public static function getAllEnabledExportFormats() { + $formats = self::getAllExportFormats(); + + foreach ($formats as $key => $format) { + if (!$format->isExportFormatEnabled()) { + unset($formats[$key]); + } + } + + return $formats; + } + +} diff --git a/src/infrastructure/export/PhabricatorIDExportField.php b/src/infrastructure/export/PhabricatorIDExportField.php new file mode 100644 index 0000000000..5b29fdb21d --- /dev/null +++ b/src/infrastructure/export/PhabricatorIDExportField.php @@ -0,0 +1,10 @@ + $field) { + $value = $map[$key]; + $value = $field->getNaturalValue($value); + + $values[$key] = $value; + } + + $this->objects[] = $values; + } + + public function newFileData() { + return id(new PhutilJSON()) + ->encodeAsList($this->objects); + } + +} diff --git a/src/infrastructure/export/PhabricatorPHIDExportField.php b/src/infrastructure/export/PhabricatorPHIDExportField.php new file mode 100644 index 0000000000..7c08ae0226 --- /dev/null +++ b/src/infrastructure/export/PhabricatorPHIDExportField.php @@ -0,0 +1,4 @@ + $field) { + $value = $map[$key]; + $value = $field->getTextValue($value); + $value = addcslashes($value, "\0..\37\\\177..\377"); + + $values[] = $value; + } + + $this->rows[] = implode("\t", $values); + } + + public function newFileData() { + return implode("\n", $this->rows)."\n"; + } + +} diff --git a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php index 184c2d5151..40cedacf06 100644 --- a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php +++ b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php @@ -1639,6 +1639,18 @@ final class PhabricatorUSEnglishTranslation ), ), + 'You are about to apply a bulk edit which will affect '. + '%s object(s).' => array( + 'You are about to apply a bulk edit to a single object.', + 'You are about to apply a bulk edit which will affect '. + '%s objects.', + ), + + 'Destroyed %s credential(s) of type "%s".' => array( + 'Destroyed one credential of type "%2$s".', + 'Destroyed %s credentials of type "%s".', + ), + ); } diff --git a/src/infrastructure/ssh/PhabricatorSSHWorkflow.php b/src/infrastructure/ssh/PhabricatorSSHWorkflow.php index b6af62d35a..6b6222304c 100644 --- a/src/infrastructure/ssh/PhabricatorSSHWorkflow.php +++ b/src/infrastructure/ssh/PhabricatorSSHWorkflow.php @@ -1,8 +1,14 @@ errorChannel; } - public function setUser(PhabricatorUser $user) { - $this->user = $user; + public function setSSHUser(PhabricatorUser $ssh_user) { + $this->sshUser = $ssh_user; return $this; } - public function getUser() { - return $this->user; + public function getSSHUser() { + return $this->sshUser; } public function setIOChannel(PhutilChannel $channel) { diff --git a/src/infrastructure/util/PhabricatorHash.php b/src/infrastructure/util/PhabricatorHash.php index 5ca4e9b53c..ce48b0966b 100644 --- a/src/infrastructure/util/PhabricatorHash.php +++ b/src/infrastructure/util/PhabricatorHash.php @@ -3,6 +3,7 @@ final class PhabricatorHash extends Phobject { const INDEX_DIGEST_LENGTH = 12; + const ANCHOR_DIGEST_LENGTH = 12; /** * Digest a string using HMAC+SHA1. @@ -29,24 +30,6 @@ final class PhabricatorHash extends Phobject { } - /** - * Digest a string into a password hash. This is similar to @{method:digest}, - * but requires a salt and iterates the hash to increase cost. - */ - public static function digestPassword(PhutilOpaqueEnvelope $envelope, $salt) { - $result = $envelope->openEnvelope(); - if (!$result) { - throw new Exception(pht('Trying to digest empty password!')); - } - - for ($ii = 0; $ii < 1000; $ii++) { - $result = self::weakDigest($result, $salt); - } - - return $result; - } - - /** * Digest a string for use in, e.g., a MySQL index. This produces a short * (12-byte), case-sensitive alphanumeric string with 72 bits of entropy, @@ -56,8 +39,8 @@ final class PhabricatorHash extends Phobject { * related hashing (for general purpose hashing, see @{method:digest}). * * @param string Input string. - * @return string 12-byte, case-sensitive alphanumeric hash of the string - * which + * @return string 12-byte, case-sensitive, mostly-alphanumeric hash of + * the string. */ public static function digestForIndex($string) { $hash = sha1($string, $raw_output = true); @@ -81,6 +64,62 @@ final class PhabricatorHash extends Phobject { return $result; } + /** + * Digest a string for use in HTML page anchors. This is similar to + * @{method:digestForIndex} but produces purely alphanumeric output. + * + * This tries to be mostly compatible with the index digest to limit how + * much stuff we're breaking by switching to it. For additional discussion, + * see T13045. + * + * @param string Input string. + * @return string 12-byte, case-sensitive, purely-alphanumeric hash of + * the string. + */ + public static function digestForAnchor($string) { + $hash = sha1($string, $raw_output = true); + + static $map; + if ($map === null) { + $map = '0123456789'. + 'abcdefghij'. + 'klmnopqrst'. + 'uvwxyzABCD'. + 'EFGHIJKLMN'. + 'OPQRSTUVWX'. + 'YZ'; + } + + $result = ''; + $accum = 0; + $map_size = strlen($map); + for ($ii = 0; $ii < self::ANCHOR_DIGEST_LENGTH; $ii++) { + $byte = ord($hash[$ii]); + $low_bits = ($byte & 0x3F); + $accum = ($accum + $byte) % $map_size; + + if ($low_bits < $map_size) { + // If an index digest would produce any alphanumeric character, just + // use that character. This means that these digests are the same as + // digests created with "digestForIndex()" in all positions where the + // output character is some character other than "." or "_". + $result .= $map[$low_bits]; + } else { + // If an index digest would produce a non-alphumeric character ("." or + // "_"), pick an alphanumeric character instead. We accumulate an + // index into the alphanumeric character list to try to preserve + // entropy here. We could use this strategy for all bytes instead, + // but then these digests would differ from digests created with + // "digestForIndex()" in all positions, instead of just a small number + // of positions. + $result .= $map[$accum]; + } + } + + return $result; + } + + public static function digestToRange($string, $min, $max) { if ($min > $max) { throw new Exception(pht('Maximum must be larger than minimum.')); diff --git a/src/infrastructure/util/__tests__/PhabricatorHashTestCase.php b/src/infrastructure/util/__tests__/PhabricatorHashTestCase.php index b89515701d..008270e767 100644 --- a/src/infrastructure/util/__tests__/PhabricatorHashTestCase.php +++ b/src/infrastructure/util/__tests__/PhabricatorHashTestCase.php @@ -52,4 +52,47 @@ final class PhabricatorHashTestCase extends PhabricatorTestCase { pht('Distinct characters in hash of: %s', $input)); } + public function testHashForAnchor() { + $map = array( + // For inputs with no "." or "_" in the output, digesting for an index + // or an anchor should be the same. + 'dog' => array( + 'Aliif7Qjd5ct', + 'Aliif7Qjd5ct', + ), + // When an output would contain "." or "_", it should be replaced with + // an alphanumeric character in those positions instead. + 'fig' => array( + 'OpB9tY4i.MOX', + 'OpB9tY4imMOX', + ), + 'cot' => array( + '_iF26XU_PsVY', + '3iF26XUkPsVY', + ), + // The replacement characters are well-distributed and generally keep + // the entropy of the output high: here, "_" characters in the initial + // positions of the digests of "cot" (above) and "dug" (this test) have + // different outputs. + 'dug' => array( + '_XuQnp0LUlUW', + '7XuQnp0LUlUW', + ), + ); + + foreach ($map as $input => $expect) { + list($expect_index, $expect_anchor) = $expect; + + $this->assertEqual( + $expect_index, + PhabricatorHash::digestForIndex($input), + pht('Index digest of "%s".', $input)); + + $this->assertEqual( + $expect_anchor, + PhabricatorHash::digestForAnchor($input), + pht('Anchor digest of "%s".', $input)); + } + } + } diff --git a/src/view/phui/PHUIObjectItemView.php b/src/view/phui/PHUIObjectItemView.php index 070d436cf6..c1e57632c4 100644 --- a/src/view/phui/PHUIObjectItemView.php +++ b/src/view/phui/PHUIObjectItemView.php @@ -29,6 +29,11 @@ final class PHUIObjectItemView extends AphrontTagView { private $coverImage; private $description; + private $selectableName; + private $selectableValue; + private $isSelected; + private $isForbidden; + public function setDisabled($disabled) { $this->disabled = $disabled; return $this; @@ -160,6 +165,20 @@ final class PHUIObjectItemView extends AphrontTagView { return $this; } + public function setSelectable( + $name, + $value, + $is_selected, + $is_forbidden = false) { + + $this->selectableName = $name; + $this->selectableValue = $value; + $this->isSelected = $is_selected; + $this->isForbidden = $is_forbidden; + + return $this; + } + public function setEpoch($epoch) { $date = phabricator_datetime($epoch, $this->getUser()); $this->addIcon('none', $date); @@ -239,6 +258,8 @@ final class PHUIObjectItemView extends AphrontTagView { } protected function getTagAttributes() { + $sigils = array(); + $item_classes = array(); $item_classes[] = 'phui-oi'; @@ -286,6 +307,19 @@ final class PHUIObjectItemView extends AphrontTagView { throw new Exception(pht('Invalid effect!')); } + if ($this->isForbidden) { + $item_classes[] = 'phui-oi-forbidden'; + } else if ($this->isSelected) { + $item_classes[] = 'phui-oi-selected'; + } + + if ($this->selectableName !== null && !$this->isForbidden) { + $item_classes[] = 'phui-oi-selectable'; + $sigils[] = 'phui-oi-selectable'; + + Javelin::initBehavior('phui-selectable-list'); + } + if ($this->getGrippable()) { $item_classes[] = 'phui-oi-grippable'; } @@ -300,6 +334,7 @@ final class PHUIObjectItemView extends AphrontTagView { return array( 'class' => $item_classes, + 'sigil' => $sigils, ); } @@ -628,6 +663,28 @@ final class PHUIObjectItemView extends AphrontTagView { $countdown); } + if ($this->selectableName !== null) { + if (!$this->isForbidden) { + $checkbox = phutil_tag( + 'input', + array( + 'type' => 'checkbox', + 'name' => $this->selectableName, + 'value' => $this->selectableValue, + 'checked' => ($this->isSelected ? 'checked' : null), + )); + } else { + $checkbox = null; + } + + $column0 = phutil_tag( + 'div', + array( + 'class' => 'phui-oi-col0 phui-oi-checkbox', + ), + $checkbox); + } + $column1 = phutil_tag( 'div', array( diff --git a/src/view/phui/PHUITimelineEventView.php b/src/view/phui/PHUITimelineEventView.php index 51a0ea5aae..161dd0c944 100644 --- a/src/view/phui/PHUITimelineEventView.php +++ b/src/view/phui/PHUITimelineEventView.php @@ -29,6 +29,7 @@ final class PHUITimelineEventView extends AphrontView { private $authorPHID; private $badges = array(); private $pinboardItems = array(); + private $isSilent; public function setAuthorPHID($author_phid) { $this->authorPHID = $author_phid; @@ -177,6 +178,15 @@ final class PHUITimelineEventView extends AphrontView { return $this; } + public function setIsSilent($is_silent) { + $this->isSilent = $is_silent; + return $this; + } + + public function getIsSilent() { + return $this->isSilent; + } + public function setReallyMajorEvent($me) { $this->reallyMajorEvent = $me; return $this; @@ -574,6 +584,14 @@ final class PHUITimelineEventView extends AphrontView { } $extra[] = $date; } + + // If this edit was applied silently, give user a hint that they should + // not expect to have received any mail or notifications. + if ($this->getIsSilent()) { + $extra[] = id(new PHUIIconView()) + ->setIcon('fa-bell-slash', 'red') + ->setTooltip(pht('Silent Edit')); + } } $extra = javelin_tag( diff --git a/webroot/rsrc/css/application/maniphest/batch-editor.css b/webroot/rsrc/css/application/maniphest/batch-editor.css deleted file mode 100644 index ea98fd013c..0000000000 --- a/webroot/rsrc/css/application/maniphest/batch-editor.css +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @provides maniphest-batch-editor - */ -.maniphest-batch-actions-table { - width: 100%; - margin: 12px 0; -} - -.maniphest-batch-actions-table td { - padding: 4px 8px; - vertical-align: middle; -} - -.batch-editor-input { - width: 100%; - text-align: left; -} diff --git a/webroot/rsrc/css/phui/object-item/phui-oi-list-view.css b/webroot/rsrc/css/phui/object-item/phui-oi-list-view.css index e356396451..ff79a8d70b 100644 --- a/webroot/rsrc/css/phui/object-item/phui-oi-list-view.css +++ b/webroot/rsrc/css/phui/object-item/phui-oi-list-view.css @@ -455,6 +455,10 @@ ul.phui-oi-list-view .phui-oi-selected border-color: {$sh-blueborder}; } +.phui-oi-forbidden { + background: {$sh-redbackground}; +} + /* - Handle Icons -------------------------------------------------------------- @@ -664,3 +668,22 @@ ul.phui-oi-list-view .phui-oi-selected padding: 0 8px 8px; text-align: left; } + +.phui-oi-col0.phui-oi-checkbox { + width: 28px; + text-align: center; +} + +.phui-oi-selectable { + cursor: pointer; + user-select: none; + -webkit-user-select: none; +} + +/* When the list selection state can be toggled on the client (as in the bulk + editor), keep the border color consistent to make the interaction feel more + robust. */ +ul.phui-oi-list-view .phui-oi-selectable + .phui-oi-frame { + border-color: {$blueborder}; +} diff --git a/webroot/rsrc/css/phui/phui-box.css b/webroot/rsrc/css/phui/phui-box.css index 278f1365e8..c5fd1d8d3b 100644 --- a/webroot/rsrc/css/phui/phui-box.css +++ b/webroot/rsrc/css/phui/phui-box.css @@ -103,6 +103,10 @@ body.device .phui-box-blue-property.phui-object-box.phui-object-box-collapsed padding: 2px 8px; } +.phui-box-blue-property .phui-oi-list-view.phui-oi-list-flush { + padding: 0; +} + body .phui-box-blue-property.phui-object-box.phui-object-box-collapsed { padding: 0; } diff --git a/webroot/rsrc/css/phui/phui-bulk-editor.css b/webroot/rsrc/css/phui/phui-bulk-editor.css new file mode 100644 index 0000000000..41ab2567e6 --- /dev/null +++ b/webroot/rsrc/css/phui/phui-bulk-editor.css @@ -0,0 +1,27 @@ +/** + * @provides phui-bulk-editor-css + */ + +.bulk-edit-table { + width: 100%; + margin: 12px 0; +} + +.bulk-edit-table td { + padding: 4px 8px; + vertical-align: middle; +} + +.bulk-edit-input { + width: 100%; + text-align: left; +} + +.bulk-edit-input input { + width: 100%; +} + +.bulk-edit-input textarea { + width: 100%; + height: 8em; +} diff --git a/webroot/rsrc/js/application/herald/HeraldRuleEditor.js b/webroot/rsrc/js/application/herald/HeraldRuleEditor.js index 69eeea4a26..443fe9811e 100644 --- a/webroot/rsrc/js/application/herald/HeraldRuleEditor.js +++ b/webroot/rsrc/js/application/herald/HeraldRuleEditor.js @@ -283,7 +283,8 @@ JX.install('HeraldRuleEditor', { var tokenizerConfig = { src: spec.datasourceURI, placeholder: spec.placeholder, - browseURI: spec.browseURI + browseURI: spec.browseURI, + limit: spec.limit }; var build = JX.Prefab.newTokenizerFromTemplate( diff --git a/webroot/rsrc/js/application/maniphest/behavior-batch-editor.js b/webroot/rsrc/js/application/maniphest/behavior-batch-editor.js deleted file mode 100644 index ad2cf32d9e..0000000000 --- a/webroot/rsrc/js/application/maniphest/behavior-batch-editor.js +++ /dev/null @@ -1,158 +0,0 @@ -/** - * @provides javelin-behavior-maniphest-batch-editor - * @requires javelin-behavior - * javelin-dom - * javelin-util - * phabricator-prefab - * multirow-row-manager - * javelin-json - */ - -JX.behavior('maniphest-batch-editor', function(config) { - var root = JX.$(config.root); - var editor_table = JX.DOM.find(root, 'table', 'maniphest-batch-actions'); - var manager = new JX.MultirowRowManager(editor_table); - var action_rows = []; - - function renderRow() { - var action_select = JX.Prefab.renderSelect( - { - 'add_project': 'Add Projects', - 'remove_project' : 'Remove Projects', - 'priority': 'Change Priority', - 'status': 'Change Status', - 'add_comment': 'Comment', - 'assign': 'Assign', - 'add_ccs' : 'Add CCs', - 'remove_ccs' : 'Remove CCs', - 'space': 'Shift to Space' - }); - - var proj_tokenizer = build_tokenizer(config.sources.project); - var owner_tokenizer = build_tokenizer(config.sources.owner); - var cc_tokenizer = build_tokenizer(config.sources.cc); - var space_tokenizer = build_tokenizer(config.sources.spaces); - - var priority_select = JX.Prefab.renderSelect(config.priorityMap); - var status_select = JX.Prefab.renderSelect(config.statusMap); - var comment_input = JX.$N('input', {style: {width: '100%'}}); - - var cell = JX.$N('td', {className: 'batch-editor-input'}); - var vfunc = null; - - function update() { - switch (action_select.value) { - case 'add_project': - case 'remove_project': - JX.DOM.setContent(cell, proj_tokenizer.template); - vfunc = function() { - return JX.keys(proj_tokenizer.object.getTokens()); - }; - break; - case 'add_ccs': - case 'remove_ccs': - JX.DOM.setContent(cell, cc_tokenizer.template); - vfunc = function() { - return JX.keys(cc_tokenizer.object.getTokens()); - }; - break; - case 'assign': - JX.DOM.setContent(cell, owner_tokenizer.template); - vfunc = function() { - return JX.keys(owner_tokenizer.object.getTokens()); - }; - break; - case 'space': - JX.DOM.setContent(cell, space_tokenizer.template); - vfunc = function() { - return JX.keys(space_tokenizer.object.getTokens()); - }; - break; - case 'add_comment': - JX.DOM.setContent(cell, comment_input); - vfunc = function() { - return comment_input.value; - }; - break; - case 'priority': - JX.DOM.setContent(cell, priority_select); - vfunc = function() { return priority_select.value; }; - break; - case 'status': - JX.DOM.setContent(cell, status_select); - vfunc = function() { return status_select.value; }; - break; - } - } - - JX.DOM.listen(action_select, 'change', null, update); - update(); - - return { - nodes : [JX.$N('td', {}, action_select), cell], - dataCallback : function() { - return { - action: action_select.value, - value: vfunc() - }; - } - }; - } - - function onaddaction(e) { - e.kill(); - addRow({}); - } - - function addRow(info) { - var data = renderRow(info); - var row = manager.addRow(data.nodes); - var id = manager.getRowID(row); - - action_rows[id] = data.dataCallback; - } - - function onsubmit() { - var input = JX.$(config.input); - - var actions = []; - for (var k in action_rows) { - actions.push(action_rows[k]()); - } - - input.value = JX.JSON.stringify(actions); - } - - addRow({}); - - JX.DOM.listen( - root, - 'click', - 'add-action', - onaddaction); - - JX.DOM.listen( - root, - 'submit', - null, - onsubmit); - - manager.listen( - 'row-removed', - function(row_id) { - delete action_rows[row_id]; - }); - - function build_tokenizer(tconfig) { - var built = JX.Prefab.newTokenizerFromTemplate( - config.tokenizerTemplate, - JX.copy({}, tconfig)); - built.tokenizer.start(); - - return { - object: built.tokenizer, - template: built.node - }; - } - -}); diff --git a/webroot/rsrc/js/application/maniphest/behavior-batch-selector.js b/webroot/rsrc/js/application/maniphest/behavior-batch-selector.js index b7728abb57..b62f40a589 100644 --- a/webroot/rsrc/js/application/maniphest/behavior-batch-selector.js +++ b/webroot/rsrc/js/application/maniphest/behavior-batch-selector.js @@ -157,12 +157,15 @@ JX.behavior('maniphest-batch-selector', function(config) { 'submit', null, function() { - var inputs = []; + var ids = []; for (var k in selected) { - inputs.push( - JX.$N('input', {type: 'hidden', name: 'batch[]', value: k})); + ids.push(k); } - JX.DOM.setContent(JX.$(config.idContainer), inputs); + ids = ids.join(','); + + var input = JX.$N('input', {type: 'hidden', name: 'ids', value: ids}); + + JX.DOM.setContent(JX.$(config.idContainer), input); }); update(); diff --git a/webroot/rsrc/js/application/repository/repository-crossreference.js b/webroot/rsrc/js/application/repository/repository-crossreference.js index 138ce4bf3e..5f2f8306bf 100644 --- a/webroot/rsrc/js/application/repository/repository-crossreference.js +++ b/webroot/rsrc/js/application/repository/repository-crossreference.js @@ -28,8 +28,8 @@ JX.behavior('repository-crossreference', function(config, statics) { nf : 'function', na : null, nb : 'builtin', - n : null, - }; + n : null + }; function link(element, lang) { JX.DOM.alterClass(element, 'repository-crossreference', true); @@ -58,8 +58,15 @@ JX.behavior('repository-crossreference', function(config, statics) { // Continue if we're not inside an inline comment. } + // If only part of the symbol was edited, the symbol name itself will + // have another "" inside of it which highlights only the + // edited part. Skip over it. + if (JX.DOM.isNode(target, 'span') && (target.className === 'bright')) { + target = target.parentNode; + } + if (e.getType() === 'mouseover') { - while (target !== document.body) { + while (target && target !== document.body) { if (JX.DOM.isNode(target, 'span') && (target.className in class_map)) { highlighted = target; @@ -87,15 +94,34 @@ JX.behavior('repository-crossreference', function(config, statics) { }; var c = target.className; c = c.replace(classHighlight, '').trim(); + if (class_map[c]) { query.type = class_map[c]; } + if (target.hasAttribute('data-symbol-context')) { query.context = target.getAttribute('data-symbol-context'); } + if (target.hasAttribute('data-symbol-name')) { symbol = target.getAttribute('data-symbol-name'); } + + var line = getLineNumber(target); + if (line !== null) { + query.line = line; + } + + var path = getPath(target); + if (path !== null) { + query.path = path; + } + + var char = getChar(target); + if (char !== null) { + query.char = char; + } + var uri = JX.$U('/diffusion/symbol/' + symbol + '/'); uri.addQueryParams(query); window.open(uri); @@ -111,6 +137,96 @@ JX.behavior('repository-crossreference', function(config, statics) { } } + function getLineNumber(target) { + + // Figure out the line number by finding the most recent "" in this + // row with a number in it. We may need to skip over one "" if the + // diff is being displayed in unified mode. + + var cell = JX.DOM.findAbove(target, 'td'); + if (!cell) { + return null; + } + + var row = JX.DOM.findAbove(target, 'tr'); + if (!row) { + return null; + } + + var ii; + + var cell_list = []; + for (ii = 0; ii < row.childNodes.length; ii++) { + cell_list.push(row.childNodes[ii]); + } + cell_list.reverse(); + + var found = false; + for (ii = 0; ii < cell_list.length; ii++) { + if (cell_list[ii] === cell) { + found = true; + } + + if (found && JX.DOM.isType(cell_list[ii], 'th')) { + var int_value = parseInt(cell_list[ii].textContent, 10); + if (int_value) { + return int_value; + } + } + } + + return null; + } + + function getPath(target) { + // This method works in Differential, when browsing a changset. + var changeset; + try { + changeset = JX.DOM.findAbove(target, 'div', 'differential-changeset'); + return JX.Stratcom.getData(changeset).path; + } catch (ex) { + // Ignore. + } + + // This method works in Diffusion, when viewing the content of a file at + // a particular commit. + var file; + try { + file = JX.DOM.findAbove(target, 'div', 'diffusion-file-content-view'); + return JX.Stratcom.getData(file).path; + } catch (ex) { + // Ignore. + } + + return null; + } + + function getChar(target) { + var cell = JX.DOM.findAbove(target, 'td'); + if (!cell) { + return null; + } + + var char = 1; + for (var ii = 0; ii < cell.childNodes.length; ii++) { + var node = cell.childNodes[ii]; + + if (node === target) { + return char; + } + + var content = '' + node.textContent; + + // Strip off any ZWS characters. These are marker characters used to + // improve copy/paste behavior. + content = content.replace(/\u200B/g, ''); + + char += content.length; + } + + return null; + } + if (config.container) { link(JX.$(config.container), config.lang); } else if (config.section) { diff --git a/webroot/rsrc/js/core/behavior-bulk-editor.js b/webroot/rsrc/js/core/behavior-bulk-editor.js new file mode 100644 index 0000000000..79e1c2714e --- /dev/null +++ b/webroot/rsrc/js/core/behavior-bulk-editor.js @@ -0,0 +1,110 @@ +/** + * @provides javelin-behavior-bulk-editor + * @requires javelin-behavior + * javelin-dom + * javelin-util + * multirow-row-manager + * javelin-json + * phuix-form-control-view + */ + +JX.behavior('bulk-editor', function(config) { + + var root = JX.$(config.rootNodeID); + var editor_table = JX.DOM.find(root, 'table', 'bulk-actions'); + + var manager = new JX.MultirowRowManager(editor_table); + var action_rows = []; + + var option_map = {}; + var option_order = []; + var spec_map = {}; + + for (var ii = 0; ii < config.edits.length; ii++) { + var edit = config.edits[ii]; + + option_map[edit.xaction] = edit.label; + option_order.push(edit.xaction); + + spec_map[edit.xaction] = edit; + } + + function renderRow() { + var action_select = new JX.PHUIXFormControl() + .setControl('optgroups', config.optgroups) + .getRawInputNode(); + + var cell = JX.$N('td', {className: 'bulk-edit-input'}); + var vfunc = null; + + function update() { + var spec = spec_map[action_select.value]; + var control = spec.control; + + var phuix = new JX.PHUIXFormControl() + .setControl(control.type, control.spec); + + JX.DOM.setContent(cell, phuix.getRawInputNode()); + + vfunc = JX.bind(phuix, phuix.getValue); + } + + JX.DOM.listen(action_select, 'change', null, update); + update(); + + return { + nodes : [JX.$N('td', {}, action_select), cell], + dataCallback : function() { + return { + type: action_select.value, + value: vfunc() + }; + } + }; + } + + function onaddaction(e) { + e.kill(); + addRow({}); + } + + function addRow(info) { + var data = renderRow(info); + var row = manager.addRow(data.nodes); + var id = manager.getRowID(row); + + action_rows[id] = data.dataCallback; + } + + function onsubmit() { + var input = JX.$(config.inputNodeID); + + var actions = []; + for (var k in action_rows) { + actions.push(action_rows[k]()); + } + + input.value = JX.JSON.stringify(actions); + } + + addRow({}); + + JX.DOM.listen( + root, + 'click', + 'add-action', + onaddaction); + + JX.DOM.listen( + root, + 'submit', + null, + onsubmit); + + manager.listen( + 'row-removed', + function(row_id) { + delete action_rows[row_id]; + }); + +}); diff --git a/webroot/rsrc/js/phui/behavior-phui-selectable-list.js b/webroot/rsrc/js/phui/behavior-phui-selectable-list.js new file mode 100644 index 0000000000..eaa33565d0 --- /dev/null +++ b/webroot/rsrc/js/phui/behavior-phui-selectable-list.js @@ -0,0 +1,44 @@ +/** + * @provides javelin-behavior-phui-selectable-list + * @requires javelin-behavior + * javelin-stratcom + * javelin-dom + */ + +JX.behavior('phui-selectable-list', function() { + + JX.Stratcom.listen('click', 'phui-oi-selectable', function(e) { + if (!e.isNormalClick()) { + return; + } + + // If the user clicked a link, ignore it. + if (e.getNode('tag:a')) { + return; + } + + var root = e.getNode('phui-oi-selectable'); + + // If the user did not click the checkbox, pretend they did. This makes + // the entire element a click target to make changing the selection set a + // bit easier. + if (!e.getNode('tag:input')) { + var checkbox = getCheckbox(root); + checkbox.checked = !checkbox.checked; + + e.kill(); + } + + setTimeout(JX.bind(null, redraw, root), 0); + }); + + function getCheckbox(root) { + return JX.DOM.find(root, 'input'); + } + + function redraw(root) { + var checkbox = getCheckbox(root); + JX.DOM.alterClass(root, 'phui-oi-selected', !!checkbox.checked); + } + +}); diff --git a/webroot/rsrc/js/phuix/PHUIXFormControl.js b/webroot/rsrc/js/phuix/PHUIXFormControl.js index 5640b95ae8..cd67e89691 100644 --- a/webroot/rsrc/js/phuix/PHUIXFormControl.js +++ b/webroot/rsrc/js/phuix/PHUIXFormControl.js @@ -14,6 +14,7 @@ JX.install('PHUIXFormControl', { _className: null, _valueSetCallback: null, _valueGetCallback: null, + _rawInputNode: null, setLabel: function(label) { JX.DOM.setContent(this._getLabelNode(), label); @@ -53,6 +54,12 @@ JX.install('PHUIXFormControl', { case 'checkboxes': input = this._newCheckboxes(spec); break; + case 'text': + input = this._newText(spec); + break; + case 'remarkup': + input = this._newRemarkup(spec); + break; default: // TODO: Default or better error? JX.$E('Bad Input Type'); @@ -62,6 +69,7 @@ JX.install('PHUIXFormControl', { JX.DOM.setContent(node, input.node); this._valueGetCallback = input.get; this._valueSetCallback = input.set; + this._rawInputNode = input.node; return this; }, @@ -75,6 +83,10 @@ JX.install('PHUIXFormControl', { return this._valueGetCallback(); }, + getRawInputNode: function() { + return this._rawInputNode; + }, + getNode: function() { if (!this._node) { @@ -281,6 +293,10 @@ JX.install('PHUIXFormControl', { }, _newPoints: function(spec) { + return this._newText(); + }, + + _newText: function(spec) { var attrs = { type: 'text', value: spec.value @@ -299,6 +315,28 @@ JX.install('PHUIXFormControl', { }; }, + _newRemarkup: function(spec) { + var attrs = {}; + + // We could imagine a world where this renders a full remarkup control + // with all the hint buttons and client behaviors, but today much of that + // behavior is defined server-side and thus this isn't a world we + // currently live in. + + var node = JX.$N('textarea', attrs); + node.value = spec.value || ''; + + return { + node: node, + get: function() { + return node.value; + }, + set: function(value) { + node.value = value; + } + }; + }, + _newOptgroups: function(spec) { var value = spec.value || null;