diff --git a/bin/accountadmin b/bin/accountadmin deleted file mode 120000 index a846766c26..0000000000 --- a/bin/accountadmin +++ /dev/null @@ -1 +0,0 @@ -../scripts/user/account_admin.php \ No newline at end of file diff --git a/bin/people b/bin/people deleted file mode 120000 index 985dc4849a..0000000000 --- a/bin/people +++ /dev/null @@ -1 +0,0 @@ -../scripts/people/manage_people.php \ No newline at end of file diff --git a/bin/user b/bin/user new file mode 120000 index 0000000000..4b4b6b7ab5 --- /dev/null +++ b/bin/user @@ -0,0 +1 @@ +../scripts/setup/manage_user.php \ No newline at end of file diff --git a/resources/celerity/map.php b/resources/celerity/map.php index c6e5b9bcad..d710846c2c 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,10 +9,10 @@ return array( 'names' => array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', - 'core.pkg.css' => 'efa1b78b', - 'core.pkg.js' => '8225dc58', + 'core.pkg.css' => 'eef4903d', + 'core.pkg.js' => '73a06a9f', 'differential.pkg.css' => '8d8360fb', - 'differential.pkg.js' => '67e02996', + 'differential.pkg.js' => '0b037a4f', 'diffusion.pkg.css' => '42c75c37', 'diffusion.pkg.js' => 'a98c0bf7', 'maniphest.pkg.css' => '35995d6d', @@ -24,7 +24,7 @@ return array( 'rsrc/audio/basic/ting.mp3' => 'a6b6540e', 'rsrc/css/aphront/aphront-bars.css' => '4a327b4a', 'rsrc/css/aphront/dark-console.css' => '7f06cda2', - 'rsrc/css/aphront/dialog-view.css' => 'b70c70df', + 'rsrc/css/aphront/dialog-view.css' => '874f5c06', 'rsrc/css/aphront/list-filter-view.css' => 'feb64255', 'rsrc/css/aphront/multi-column.css' => 'fbc00ba3', 'rsrc/css/aphront/notification.css' => '30240bd2', @@ -92,7 +92,7 @@ return array( 'rsrc/css/application/pholio/pholio.css' => '88ef5ef1', 'rsrc/css/application/phortune/phortune-credit-card-form.css' => '3b9868a8', 'rsrc/css/application/phortune/phortune-invoice.css' => '4436b241', - 'rsrc/css/application/phortune/phortune.css' => '12e8251a', + 'rsrc/css/application/phortune/phortune.css' => '508a1a5e', 'rsrc/css/application/phrequent/phrequent.css' => 'bd79cc67', 'rsrc/css/application/phriction/phriction-document-css.css' => '03380da0', 'rsrc/css/application/policy/policy-edit.css' => '8794e2ed', @@ -255,7 +255,7 @@ return array( 'rsrc/externals/javelin/lib/URI.js' => '2e255291', 'rsrc/externals/javelin/lib/Vector.js' => 'e9c80beb', 'rsrc/externals/javelin/lib/WebSocket.js' => 'fdc13e4e', - 'rsrc/externals/javelin/lib/Workflow.js' => '851f642d', + 'rsrc/externals/javelin/lib/Workflow.js' => '945ff654', 'rsrc/externals/javelin/lib/__tests__/Cookie.js' => 'ca686f71', 'rsrc/externals/javelin/lib/__tests__/DOM.js' => '4566e249', 'rsrc/externals/javelin/lib/__tests__/JSON.js' => '710377ae', @@ -414,16 +414,16 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f', 'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172', - 'rsrc/js/application/projects/WorkboardBoard.js' => 'c02a5497', + 'rsrc/js/application/projects/WorkboardBoard.js' => 'b46d88c5', 'rsrc/js/application/projects/WorkboardCard.js' => '0392a5d8', - 'rsrc/js/application/projects/WorkboardCardTemplate.js' => '2a61f8d4', + 'rsrc/js/application/projects/WorkboardCardTemplate.js' => '84f82dad', 'rsrc/js/application/projects/WorkboardColumn.js' => 'c3d24e63', - 'rsrc/js/application/projects/WorkboardController.js' => '42c7a5a7', + 'rsrc/js/application/projects/WorkboardController.js' => 'b9d0c2f3', 'rsrc/js/application/projects/WorkboardDropEffect.js' => '8e0aa661', 'rsrc/js/application/projects/WorkboardHeader.js' => '111bfd2d', 'rsrc/js/application/projects/WorkboardHeaderTemplate.js' => 'ebe83a6b', 'rsrc/js/application/projects/WorkboardOrderTemplate.js' => '03e8891f', - 'rsrc/js/application/projects/behavior-project-boards.js' => 'aad45445', + 'rsrc/js/application/projects/behavior-project-boards.js' => '58cb6a88', 'rsrc/js/application/projects/behavior-project-create.js' => '34c53422', 'rsrc/js/application/projects/behavior-reorder-columns.js' => '8ac32fd9', 'rsrc/js/application/releeph/releeph-preview-branch.js' => '75184d68', @@ -486,7 +486,7 @@ return array( 'rsrc/js/core/behavior-line-linker.js' => 'e15c8b1f', 'rsrc/js/core/behavior-linked-container.js' => '74446546', 'rsrc/js/core/behavior-more.js' => '506aa3f4', - 'rsrc/js/core/behavior-object-selector.js' => 'a4af0b4a', + 'rsrc/js/core/behavior-object-selector.js' => '98ef467f', 'rsrc/js/core/behavior-oncopy.js' => 'ff7b3f22', 'rsrc/js/core/behavior-phabricator-nav.js' => 'f166c949', 'rsrc/js/core/behavior-phabricator-remarkup-assist.js' => '2f80333f', @@ -532,7 +532,7 @@ return array( 'almanac-css' => '2e050f4f', 'aphront-bars' => '4a327b4a', 'aphront-dark-console-css' => '7f06cda2', - 'aphront-dialog-view-css' => 'b70c70df', + 'aphront-dialog-view-css' => '874f5c06', 'aphront-list-filter-view-css' => 'feb64255', 'aphront-multi-column-view-css' => 'fbc00ba3', 'aphront-panel-view-css' => '46923d46', @@ -647,7 +647,7 @@ return array( 'javelin-behavior-phabricator-line-linker' => 'e15c8b1f', 'javelin-behavior-phabricator-nav' => 'f166c949', 'javelin-behavior-phabricator-notification-example' => '29819b75', - 'javelin-behavior-phabricator-object-selector' => 'a4af0b4a', + 'javelin-behavior-phabricator-object-selector' => '98ef467f', 'javelin-behavior-phabricator-oncopy' => 'ff7b3f22', 'javelin-behavior-phabricator-remarkup-assist' => '2f80333f', 'javelin-behavior-phabricator-reveal-content' => 'b105a3a6', @@ -669,7 +669,7 @@ return array( 'javelin-behavior-phuix-example' => 'c2c500a7', 'javelin-behavior-policy-control' => '0eaa33a9', 'javelin-behavior-policy-rule-editor' => '9347f172', - 'javelin-behavior-project-boards' => 'aad45445', + 'javelin-behavior-project-boards' => '58cb6a88', 'javelin-behavior-project-create' => '34c53422', 'javelin-behavior-quicksand-blacklist' => '5a6f6a06', 'javelin-behavior-read-only-warning' => 'b9109f8f', @@ -745,16 +745,16 @@ return array( 'javelin-view-renderer' => '9aae2b66', 'javelin-view-visitor' => '308f9fe4', 'javelin-websocket' => 'fdc13e4e', - 'javelin-workboard-board' => 'c02a5497', + 'javelin-workboard-board' => 'b46d88c5', 'javelin-workboard-card' => '0392a5d8', - 'javelin-workboard-card-template' => '2a61f8d4', + 'javelin-workboard-card-template' => '84f82dad', 'javelin-workboard-column' => 'c3d24e63', - 'javelin-workboard-controller' => '42c7a5a7', + 'javelin-workboard-controller' => 'b9d0c2f3', 'javelin-workboard-drop-effect' => '8e0aa661', 'javelin-workboard-header' => '111bfd2d', 'javelin-workboard-header-template' => 'ebe83a6b', 'javelin-workboard-order-template' => '03e8891f', - 'javelin-workflow' => '851f642d', + 'javelin-workflow' => '945ff654', 'maniphest-report-css' => '3d53188b', 'maniphest-task-edit-css' => '272daa84', 'maniphest-task-summary-css' => '61d1667e', @@ -813,7 +813,7 @@ return array( 'pholio-inline-comments-css' => '722b48c2', 'phortune-credit-card-form' => 'd12d214f', 'phortune-credit-card-form-css' => '3b9868a8', - 'phortune-css' => '12e8251a', + 'phortune-css' => '508a1a5e', 'phortune-invoice-css' => '4436b241', 'phrequent-css' => 'bd79cc67', 'phriction-document-css' => '03380da0', @@ -1133,9 +1133,6 @@ return array( 'javelin-stratcom', 'javelin-behavior', ), - '2a61f8d4' => array( - 'javelin-install', - ), '2a8b62d9' => array( 'multirow-row-manager', 'javelin-install', @@ -1264,16 +1261,6 @@ return array( '4234f572' => array( 'syntax-default-css', ), - '42c7a5a7' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-drag-and-drop-file-upload', - 'javelin-workboard-board', - ), '4370900d' => array( 'javelin-install', 'javelin-util', @@ -1412,6 +1399,16 @@ return array( 'javelin-vector', 'javelin-typeahead-static-source', ), + '58cb6a88' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-vector', + 'javelin-stratcom', + 'javelin-workflow', + 'javelin-workboard-controller', + 'javelin-workboard-drop-effect', + ), '5902260c' => array( 'javelin-util', 'javelin-magical-init', @@ -1607,16 +1604,8 @@ return array( 'javelin-resource', 'javelin-routable', ), - '851f642d' => array( - 'javelin-stratcom', - 'javelin-request', - 'javelin-dom', - 'javelin-vector', + '84f82dad' => array( 'javelin-install', - 'javelin-util', - 'javelin-mask', - 'javelin-uri', - 'javelin-routable', ), '87428eb2' => array( 'javelin-behavior', @@ -1709,6 +1698,17 @@ return array( 'javelin-typeahead-preloaded-source', 'javelin-util', ), + '945ff654' => array( + 'javelin-stratcom', + 'javelin-request', + 'javelin-dom', + 'javelin-vector', + 'javelin-install', + 'javelin-util', + 'javelin-mask', + 'javelin-uri', + 'javelin-routable', + ), '94681e22' => array( 'javelin-magical-init', 'javelin-install', @@ -1730,6 +1730,12 @@ return array( 'javelin-dom', 'javelin-router', ), + '98ef467f' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-request', + 'javelin-util', + ), '9aae2b66' => array( 'javelin-install', 'javelin-util', @@ -1790,12 +1796,6 @@ return array( 'phui-button-css', 'phui-button-simple-css', ), - 'a4af0b4a' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-request', - 'javelin-util', - ), 'a5257c4e' => array( 'javelin-install', 'javelin-dom', @@ -1843,16 +1843,6 @@ return array( 'javelin-dom', 'javelin-util', ), - 'aad45445' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - 'javelin-workflow', - 'javelin-workboard-controller', - 'javelin-workboard-drop-effect', - ), 'ab85e184' => array( 'javelin-install', 'javelin-dom', @@ -1901,6 +1891,18 @@ return array( 'b347a301' => array( 'javelin-behavior', ), + 'b46d88c5' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'javelin-workboard-column', + 'javelin-workboard-header-template', + 'javelin-workboard-card-template', + 'javelin-workboard-order-template', + ), 'b49fd60c' => array( 'multirow-row-manager', 'trigger-rule', @@ -1943,20 +1945,18 @@ return array( 'javelin-uri', 'phabricator-notification', ), - 'bde53589' => array( - 'phui-inline-comment-view-css', - ), - 'c02a5497' => array( + 'b9d0c2f3' => array( 'javelin-install', 'javelin-dom', 'javelin-util', + 'javelin-vector', 'javelin-stratcom', 'javelin-workflow', - 'phabricator-draggable-list', - 'javelin-workboard-column', - 'javelin-workboard-header-template', - 'javelin-workboard-card-template', - 'javelin-workboard-order-template', + 'phabricator-drag-and-drop-file-upload', + 'javelin-workboard-board', + ), + 'bde53589' => array( + 'phui-inline-comment-view-css', ), 'c03f2fb4' => array( 'javelin-install', diff --git a/resources/sql/autopatches/20190718.paste.01.edge.sql b/resources/sql/autopatches/20190718.paste.01.edge.sql new file mode 100644 index 0000000000..ba138a3b92 --- /dev/null +++ b/resources/sql/autopatches/20190718.paste.01.edge.sql @@ -0,0 +1,2 @@ +RENAME TABLE {$NAMESPACE}_pastebin.edge + TO {$NAMESPACE}_paste.edge; diff --git a/resources/sql/autopatches/20190718.paste.02.edgedata.sql b/resources/sql/autopatches/20190718.paste.02.edgedata.sql new file mode 100644 index 0000000000..18b0c3ff4e --- /dev/null +++ b/resources/sql/autopatches/20190718.paste.02.edgedata.sql @@ -0,0 +1,2 @@ +RENAME TABLE {$NAMESPACE}_pastebin.edgedata + TO {$NAMESPACE}_paste.edgedata; diff --git a/resources/sql/autopatches/20190718.paste.03.paste.sql b/resources/sql/autopatches/20190718.paste.03.paste.sql new file mode 100644 index 0000000000..cc8d100773 --- /dev/null +++ b/resources/sql/autopatches/20190718.paste.03.paste.sql @@ -0,0 +1,2 @@ +RENAME TABLE {$NAMESPACE}_pastebin.pastebin_paste + TO {$NAMESPACE}_paste.paste; diff --git a/resources/sql/autopatches/20190718.paste.04.xaction.sql b/resources/sql/autopatches/20190718.paste.04.xaction.sql new file mode 100644 index 0000000000..5ebfcdfeaf --- /dev/null +++ b/resources/sql/autopatches/20190718.paste.04.xaction.sql @@ -0,0 +1,2 @@ +RENAME TABLE {$NAMESPACE}_pastebin.pastebin_pastetransaction + TO {$NAMESPACE}_paste.paste_transaction; diff --git a/resources/sql/autopatches/20190718.paste.05.comment.sql b/resources/sql/autopatches/20190718.paste.05.comment.sql new file mode 100644 index 0000000000..0221d0f668 --- /dev/null +++ b/resources/sql/autopatches/20190718.paste.05.comment.sql @@ -0,0 +1,2 @@ +RENAME TABLE {$NAMESPACE}_pastebin.pastebin_pastetransaction_comment + TO {$NAMESPACE}_paste.paste_transaction_comment; diff --git a/resources/sql/autopatches/20190802.email.01.storage.sql b/resources/sql/autopatches/20190802.email.01.storage.sql new file mode 100644 index 0000000000..f362067e6d --- /dev/null +++ b/resources/sql/autopatches/20190802.email.01.storage.sql @@ -0,0 +1,12 @@ +CREATE TABLE {$NAMESPACE}_phortune.phortune_accountemail ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + accountPHID VARBINARY(64) NOT NULL, + authorPHID VARBINARY(64) NOT NULL, + address VARCHAR(128) NOT NULL COLLATE {$COLLATE_SORT}, + status VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT}, + addressKey VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT}, + accessKey VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT}, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20190802.email.02.xaction.sql b/resources/sql/autopatches/20190802.email.02.xaction.sql new file mode 100644 index 0000000000..d65f8d8b32 --- /dev/null +++ b/resources/sql/autopatches/20190802.email.02.xaction.sql @@ -0,0 +1,19 @@ +CREATE TABLE {$NAMESPACE}_phortune.phortune_accountemailtransaction ( + 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) NOT NULL, + oldValue LONGTEXT NOT NULL, + newValue LONGTEXT NOT NULL, + contentSource LONGTEXT NOT NULL, + metadata LONGTEXT NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20190815.account.01.carts.php b/resources/sql/autopatches/20190815.account.01.carts.php new file mode 100644 index 0000000000..2332dd642f --- /dev/null +++ b/resources/sql/autopatches/20190815.account.01.carts.php @@ -0,0 +1,10 @@ +addEdge($cart->getAccountPHID(), $edge_type, $cart->getMerchantPHID()) + ->save(); +} diff --git a/resources/sql/autopatches/20190815.account.02.subscriptions.php b/resources/sql/autopatches/20190815.account.02.subscriptions.php new file mode 100644 index 0000000000..38db05b0ef --- /dev/null +++ b/resources/sql/autopatches/20190815.account.02.subscriptions.php @@ -0,0 +1,10 @@ +addEdge($sub->getAccountPHID(), $edge_type, $sub->getMerchantPHID()) + ->save(); +} diff --git a/resources/sql/autopatches/20190816.payment.01.xaction.sql b/resources/sql/autopatches/20190816.payment.01.xaction.sql new file mode 100644 index 0000000000..22d7baae7e --- /dev/null +++ b/resources/sql/autopatches/20190816.payment.01.xaction.sql @@ -0,0 +1,19 @@ +CREATE TABLE {$NAMESPACE}_phortune.phortune_paymentmethodtransaction ( + 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) NOT NULL, + oldValue LONGTEXT NOT NULL, + newValue LONGTEXT NOT NULL, + contentSource LONGTEXT NOT NULL, + metadata LONGTEXT NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20190816.subscription.01.xaction.sql b/resources/sql/autopatches/20190816.subscription.01.xaction.sql new file mode 100644 index 0000000000..8866ce3a57 --- /dev/null +++ b/resources/sql/autopatches/20190816.subscription.01.xaction.sql @@ -0,0 +1,19 @@ +CREATE TABLE {$NAMESPACE}_phortune.phortune_subscriptiontransaction ( + 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) NOT NULL, + oldValue LONGTEXT NOT NULL, + newValue LONGTEXT NOT NULL, + contentSource LONGTEXT NOT NULL, + metadata LONGTEXT NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20190822.merchant.01.view.sql b/resources/sql/autopatches/20190822.merchant.01.view.sql new file mode 100644 index 0000000000..cb609f054e --- /dev/null +++ b/resources/sql/autopatches/20190822.merchant.01.view.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_phortune.phortune_merchant + DROP viewPolicy; diff --git a/scripts/people/manage_people.php b/scripts/setup/manage_user.php similarity index 83% rename from scripts/people/manage_people.php rename to scripts/setup/manage_user.php index aeada86da0..f571cb9346 100755 --- a/scripts/people/manage_people.php +++ b/scripts/setup/manage_user.php @@ -6,8 +6,8 @@ require_once $root.'/scripts/__init_script__.php'; $args = new PhutilArgumentParser($argv); $args->setSynopsis(<<establishConnection('r'), - 'SELECT * FROM %T LIMIT 1', - $table->getTableName()); -$is_first_user = (!$any_user); - -if ($is_first_user) { - echo pht( - "WARNING\n\n". - "You're about to create the first account on this install. Normally, ". - "you should use the web interface to create the first account, not ". - "this script.\n\n". - "If you use the web interface, it will drop you into a nice UI workflow ". - "which gives you more help setting up your install. If you create an ". - "account with this script instead, you will skip the setup help and you ". - "will not be able to access it later."); - if (!phutil_console_confirm(pht('Skip easy setup and create account?'))) { - echo pht('Cancelled.')."\n"; - exit(1); - } -} - -echo pht( - 'Enter a username to create a new account or edit an existing account.'); - -$username = phutil_console_prompt(pht('Enter a username:')); -if (!strlen($username)) { - echo pht('Cancelled.')."\n"; - exit(1); -} - -if (!PhabricatorUser::validateUsername($username)) { - $valid = PhabricatorUser::describeValidUsername(); - echo pht("The username '%s' is invalid. %s", $username, $valid)."\n"; - exit(1); -} - - -$user = id(new PhabricatorUser())->loadOneWhere( - 'username = %s', - $username); - -if (!$user) { - $original = new PhabricatorUser(); - - echo pht("There is no existing user account '%s'.", $username)."\n"; - $ok = phutil_console_confirm( - pht("Do you want to create a new '%s' account?", $username), - $default_no = false); - if (!$ok) { - echo pht('Cancelled.')."\n"; - exit(1); - } - $user = new PhabricatorUser(); - $user->setUsername($username); - - $is_new = true; -} else { - $original = clone $user; - - echo pht("There is an existing user account '%s'.", $username)."\n"; - $ok = phutil_console_confirm( - pht("Do you want to edit the existing '%s' account?", $username), - $default_no = false); - if (!$ok) { - echo pht('Cancelled.')."\n"; - exit(1); - } - - $is_new = false; -} - -$user_realname = $user->getRealName(); -if (strlen($user_realname)) { - $realname_prompt = ' ['.$user_realname.']:'; -} else { - $realname_prompt = ':'; -} -$realname = nonempty( - phutil_console_prompt(pht('Enter user real name').$realname_prompt), - $user_realname); -$user->setRealName($realname); - -// When creating a new user we prompt for an email address; when editing an -// existing user we just skip this because it would be quite involved to provide -// a reasonable CLI interface for editing multiple addresses and managing email -// verification and primary addresses. - -$create_email = null; -if ($is_new) { - do { - $email = phutil_console_prompt(pht('Enter user email address:')); - $duplicate = id(new PhabricatorUserEmail())->loadOneWhere( - 'address = %s', - $email); - if ($duplicate) { - echo pht( - "ERROR: There is already a user with that email address. ". - "Each user must have a unique email address.\n"); - } else { - break; - } - } while (true); - - $create_email = $email; -} - -$is_system_agent = $user->getIsSystemAgent(); -$set_system_agent = phutil_console_confirm( - pht('Is this user a bot?'), - $default_no = !$is_system_agent); - -$verify_email = null; -$set_verified = false; -// Allow administrators to verify primary email addresses at this time in edit -// scenarios. (Create will work just fine from here as we auto-verify email -// on create.) -if (!$is_new) { - $verify_email = $user->loadPrimaryEmail(); - if (!$verify_email->getIsVerified()) { - $set_verified = phutil_console_confirm( - pht('Should the primary email address be verified?'), - $default_no = true); - } else { - // Already verified so let's not make a fuss. - $verify_email = null; - } -} - -$is_admin = $user->getIsAdmin(); -$set_admin = phutil_console_confirm( - pht('Should this user be an administrator?'), - $default_no = !$is_admin); - -echo "\n\n".pht('ACCOUNT SUMMARY')."\n\n"; -$tpl = "%12s %-30s %-30s\n"; -printf($tpl, null, pht('OLD VALUE'), pht('NEW VALUE')); -printf($tpl, pht('Username'), $original->getUsername(), $user->getUsername()); -printf($tpl, pht('Real Name'), $original->getRealName(), $user->getRealName()); -if ($is_new) { - printf($tpl, pht('Email'), '', $create_email); -} - -printf( - $tpl, - pht('Bot'), - $original->getIsSystemAgent() ? 'Y' : 'N', - $set_system_agent ? 'Y' : 'N'); - -if ($verify_email) { - printf( - $tpl, - pht('Verify Email'), - $verify_email->getIsVerified() ? 'Y' : 'N', - $set_verified ? 'Y' : 'N'); -} - -printf( - $tpl, - pht('Admin'), - $original->getIsAdmin() ? 'Y' : 'N', - $set_admin ? 'Y' : 'N'); - -echo "\n"; - -if (!phutil_console_confirm(pht('Save these changes?'), $default_no = false)) { - echo pht('Cancelled.')."\n"; - exit(1); -} - -$user->openTransaction(); - - $editor = new PhabricatorUserEditor(); - - // TODO: This is wrong, but we have a chicken-and-egg problem when you use - // this script to create the first user. - $editor->setActor($user); - - if ($is_new) { - $email = id(new PhabricatorUserEmail()) - ->setAddress($create_email) - ->setIsVerified(1); - - // Unconditionally approve new accounts created from the CLI. - $user->setIsApproved(1); - - $editor->createNewUser($user, $email); - } else { - if ($verify_email) { - $user->setIsEmailVerified(1); - $verify_email->setIsVerified($set_verified ? 1 : 0); - } - $editor->updateUser($user, $verify_email); - } - - $editor->makeSystemAgentUser($user, $set_system_agent); - - $xactions = array(); - $xactions[] = id(new PhabricatorUserTransaction()) - ->setTransactionType( - PhabricatorUserEmpowerTransaction::TRANSACTIONTYPE) - ->setNewValue($set_admin); - - $actor = PhabricatorUser::getOmnipotentUser(); - $content_source = PhabricatorContentSource::newForSource( - PhabricatorConsoleContentSource::SOURCECONST); - - $people_application_phid = id(new PhabricatorPeopleApplication())->getPHID(); - - $transaction_editor = id(new PhabricatorUserTransactionEditor()) - ->setActor($actor) - ->setActingAsPHID($people_application_phid) - ->setContentSource($content_source) - ->setContinueOnNoEffect(true) - ->setContinueOnMissingFields(true); - - $transaction_editor->applyTransactions($user, $xactions); - -$user->saveTransaction(); - -echo pht('Saved changes.')."\n"; diff --git a/scripts/user/add_user.php b/scripts/user/add_user.php deleted file mode 100755 index 2554ab3ddc..0000000000 --- a/scripts/user/add_user.php +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env php - '); - exit(1); -} - -$username = $argv[1]; -$email = $argv[2]; -$realname = $argv[3]; -$admin = $argv[4]; - -$admin = id(new PhabricatorUser())->loadOneWhere( - 'username = %s', - $argv[4]); -if (!$admin) { - throw new Exception( - pht( - 'Admin user must be the username of a valid Phabricator account, used '. - 'to send the new user a welcome email.')); -} - -$existing_user = id(new PhabricatorUser())->loadOneWhere( - 'username = %s', - $username); -if ($existing_user) { - throw new Exception( - pht( - "There is already a user with the username '%s'!", - $username)); -} - -$existing_email = id(new PhabricatorUserEmail())->loadOneWhere( - 'address = %s', - $email); -if ($existing_email) { - throw new Exception( - pht( - "There is already a user with the email '%s'!", - $email)); -} - -$user = new PhabricatorUser(); -$user->setUsername($username); -$user->setRealname($realname); -$user->setIsApproved(1); - -$email_object = id(new PhabricatorUserEmail()) - ->setAddress($email) - ->setIsVerified(1); - -id(new PhabricatorUserEditor()) - ->setActor($admin) - ->createNewUser($user, $email_object); - -$welcome_engine = id(new PhabricatorPeopleWelcomeMailEngine()) - ->setSender($admin) - ->setRecipient($user); -if ($welcome_engine->canSendMail()) { - $welcome_engine->sendMail(); -} - -echo pht( - "Created user '%s' (realname='%s', email='%s').\n", - $username, - $realname, - $email); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index ddcd36aacf..67e7a49755 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1222,6 +1222,7 @@ phutil_register_library_map(array( 'DrydockResourceQuery' => 'applications/drydock/query/DrydockResourceQuery.php', 'DrydockResourceReclaimLogType' => 'applications/drydock/logtype/DrydockResourceReclaimLogType.php', 'DrydockResourceReleaseController' => 'applications/drydock/controller/DrydockResourceReleaseController.php', + 'DrydockResourceSearchConduitAPIMethod' => 'applications/drydock/conduit/DrydockResourceSearchConduitAPIMethod.php', 'DrydockResourceSearchEngine' => 'applications/drydock/query/DrydockResourceSearchEngine.php', 'DrydockResourceStatus' => 'applications/drydock/constants/DrydockResourceStatus.php', 'DrydockResourceUpdateWorker' => 'applications/drydock/worker/DrydockResourceUpdateWorker.php', @@ -2118,6 +2119,8 @@ phutil_register_library_map(array( 'PhabricatorActionListView' => 'view/layout/PhabricatorActionListView.php', 'PhabricatorActionView' => 'view/layout/PhabricatorActionView.php', 'PhabricatorActivitySettingsPanel' => 'applications/settings/panel/PhabricatorActivitySettingsPanel.php', + 'PhabricatorAddEmailUserLogType' => 'applications/people/userlog/PhabricatorAddEmailUserLogType.php', + 'PhabricatorAddMultifactorUserLogType' => 'applications/people/userlog/PhabricatorAddMultifactorUserLogType.php', 'PhabricatorAdministratorsPolicyRule' => 'applications/people/policyrule/PhabricatorAdministratorsPolicyRule.php', 'PhabricatorAjaxRequestExceptionHandler' => 'aphront/handler/PhabricatorAjaxRequestExceptionHandler.php', 'PhabricatorAlmanacApplication' => 'applications/almanac/application/PhabricatorAlmanacApplication.php', @@ -2175,9 +2178,11 @@ phutil_register_library_map(array( 'PhabricatorApplicationTransactionCommentView' => 'applications/transactions/view/PhabricatorApplicationTransactionCommentView.php', 'PhabricatorApplicationTransactionController' => 'applications/transactions/controller/PhabricatorApplicationTransactionController.php', 'PhabricatorApplicationTransactionDetailController' => 'applications/transactions/controller/PhabricatorApplicationTransactionDetailController.php', + 'PhabricatorApplicationTransactionDetailView' => 'applications/transactions/view/PhabricatorApplicationTransactionDetailView.php', 'PhabricatorApplicationTransactionEditor' => 'applications/transactions/editor/PhabricatorApplicationTransactionEditor.php', 'PhabricatorApplicationTransactionFeedStory' => 'applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php', 'PhabricatorApplicationTransactionInterface' => 'applications/transactions/interface/PhabricatorApplicationTransactionInterface.php', + 'PhabricatorApplicationTransactionJSONDiffDetailView' => 'applications/transactions/view/PhabricatorApplicationTransactionJSONDiffDetailView.php', 'PhabricatorApplicationTransactionNoEffectException' => 'applications/transactions/exception/PhabricatorApplicationTransactionNoEffectException.php', 'PhabricatorApplicationTransactionNoEffectResponse' => 'applications/transactions/response/PhabricatorApplicationTransactionNoEffectResponse.php', 'PhabricatorApplicationTransactionPublishWorker' => 'applications/transactions/worker/PhabricatorApplicationTransactionPublishWorker.php', @@ -2266,6 +2271,9 @@ phutil_register_library_map(array( 'PhabricatorAuthDisableController' => 'applications/auth/controller/config/PhabricatorAuthDisableController.php', 'PhabricatorAuthDowngradeSessionController' => 'applications/auth/controller/PhabricatorAuthDowngradeSessionController.php', 'PhabricatorAuthEditController' => 'applications/auth/controller/config/PhabricatorAuthEditController.php', + 'PhabricatorAuthEmailLoginAction' => 'applications/auth/action/PhabricatorAuthEmailLoginAction.php', + 'PhabricatorAuthEmailLoginMessageType' => 'applications/auth/message/PhabricatorAuthEmailLoginMessageType.php', + 'PhabricatorAuthEmailSetPasswordMessageType' => 'applications/auth/message/PhabricatorAuthEmailSetPasswordMessageType.php', 'PhabricatorAuthFactor' => 'applications/auth/factor/PhabricatorAuthFactor.php', 'PhabricatorAuthFactorConfig' => 'applications/auth/storage/PhabricatorAuthFactorConfig.php', 'PhabricatorAuthFactorConfigQuery' => 'applications/auth/query/PhabricatorAuthFactorConfigQuery.php', @@ -2427,7 +2435,10 @@ phutil_register_library_map(array( 'PhabricatorAuthTemporaryTokenTypeModule' => 'applications/auth/tokentype/PhabricatorAuthTemporaryTokenTypeModule.php', 'PhabricatorAuthTerminateSessionController' => 'applications/auth/controller/PhabricatorAuthTerminateSessionController.php', 'PhabricatorAuthTestSMSAction' => 'applications/auth/action/PhabricatorAuthTestSMSAction.php', + 'PhabricatorAuthTryEmailLoginAction' => 'applications/auth/action/PhabricatorAuthTryEmailLoginAction.php', 'PhabricatorAuthTryFactorAction' => 'applications/auth/action/PhabricatorAuthTryFactorAction.php', + 'PhabricatorAuthTryPasswordAction' => 'applications/auth/action/PhabricatorAuthTryPasswordAction.php', + 'PhabricatorAuthTryPasswordWithoutCAPTCHAAction' => 'applications/auth/action/PhabricatorAuthTryPasswordWithoutCAPTCHAAction.php', 'PhabricatorAuthUnlinkController' => 'applications/auth/controller/PhabricatorAuthUnlinkController.php', 'PhabricatorAuthValidateController' => 'applications/auth/controller/PhabricatorAuthValidateController.php', 'PhabricatorAuthWaitForApprovalMessageType' => 'applications/auth/message/PhabricatorAuthWaitForApprovalMessageType.php', @@ -2664,6 +2675,7 @@ phutil_register_library_map(array( 'PhabricatorCelerityApplication' => 'applications/celerity/application/PhabricatorCelerityApplication.php', 'PhabricatorCelerityTestCase' => '__tests__/PhabricatorCelerityTestCase.php', 'PhabricatorChangeParserTestCase' => 'applications/repository/worker/__tests__/PhabricatorChangeParserTestCase.php', + 'PhabricatorChangePasswordUserLogType' => 'applications/people/userlog/PhabricatorChangePasswordUserLogType.php', 'PhabricatorChangesetCachePurger' => 'applications/cache/purger/PhabricatorChangesetCachePurger.php', 'PhabricatorChangesetResponse' => 'infrastructure/diff/PhabricatorChangesetResponse.php', 'PhabricatorChartAxis' => 'applications/fact/chart/PhabricatorChartAxis.php', @@ -2717,7 +2729,9 @@ phutil_register_library_map(array( 'PhabricatorConduitAPIController' => 'applications/conduit/controller/PhabricatorConduitAPIController.php', 'PhabricatorConduitApplication' => 'applications/conduit/application/PhabricatorConduitApplication.php', 'PhabricatorConduitCallManagementWorkflow' => 'applications/conduit/management/PhabricatorConduitCallManagementWorkflow.php', + 'PhabricatorConduitCertificateFailureUserLogType' => 'applications/people/userlog/PhabricatorConduitCertificateFailureUserLogType.php', 'PhabricatorConduitCertificateToken' => 'applications/conduit/storage/PhabricatorConduitCertificateToken.php', + 'PhabricatorConduitCertificateUserLogType' => 'applications/people/userlog/PhabricatorConduitCertificateUserLogType.php', 'PhabricatorConduitConsoleController' => 'applications/conduit/controller/PhabricatorConduitConsoleController.php', 'PhabricatorConduitContentSource' => 'infrastructure/contentsource/PhabricatorConduitContentSource.php', 'PhabricatorConduitController' => 'applications/conduit/controller/PhabricatorConduitController.php', @@ -3155,16 +3169,26 @@ phutil_register_library_map(array( 'PhabricatorEditEngineConfigurationTransactionQuery' => 'applications/transactions/query/PhabricatorEditEngineConfigurationTransactionQuery.php', 'PhabricatorEditEngineConfigurationViewController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationViewController.php', 'PhabricatorEditEngineController' => 'applications/transactions/controller/PhabricatorEditEngineController.php', + 'PhabricatorEditEngineCreateOrderTransaction' => 'applications/transactions/xaction/PhabricatorEditEngineCreateOrderTransaction.php', 'PhabricatorEditEngineDatasource' => 'applications/transactions/typeahead/PhabricatorEditEngineDatasource.php', + 'PhabricatorEditEngineDefaultCreateTransaction' => 'applications/transactions/xaction/PhabricatorEditEngineDefaultCreateTransaction.php', 'PhabricatorEditEngineDefaultLock' => 'applications/transactions/editengine/PhabricatorEditEngineDefaultLock.php', + 'PhabricatorEditEngineDefaultTransaction' => 'applications/transactions/xaction/PhabricatorEditEngineDefaultTransaction.php', + 'PhabricatorEditEngineDisableTransaction' => 'applications/transactions/xaction/PhabricatorEditEngineDisableTransaction.php', + 'PhabricatorEditEngineEditOrderTransaction' => 'applications/transactions/xaction/PhabricatorEditEngineEditOrderTransaction.php', 'PhabricatorEditEngineExtension' => 'applications/transactions/engineextension/PhabricatorEditEngineExtension.php', 'PhabricatorEditEngineExtensionModule' => 'applications/transactions/engineextension/PhabricatorEditEngineExtensionModule.php', + 'PhabricatorEditEngineIsEditTransaction' => 'applications/transactions/xaction/PhabricatorEditEngineIsEditTransaction.php', 'PhabricatorEditEngineListController' => 'applications/transactions/controller/PhabricatorEditEngineListController.php', 'PhabricatorEditEngineLock' => 'applications/transactions/editengine/PhabricatorEditEngineLock.php', 'PhabricatorEditEngineLockableInterface' => 'applications/transactions/editengine/PhabricatorEditEngineLockableInterface.php', + 'PhabricatorEditEngineLocksTransaction' => 'applications/transactions/xaction/PhabricatorEditEngineLocksTransaction.php', 'PhabricatorEditEngineMFAEngine' => 'applications/transactions/editengine/PhabricatorEditEngineMFAEngine.php', 'PhabricatorEditEngineMFAInterface' => 'applications/transactions/editengine/PhabricatorEditEngineMFAInterface.php', + 'PhabricatorEditEngineNameTransaction' => 'applications/transactions/xaction/PhabricatorEditEngineNameTransaction.php', + 'PhabricatorEditEngineOrderTransaction' => 'applications/transactions/xaction/PhabricatorEditEngineOrderTransaction.php', 'PhabricatorEditEnginePointsCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEnginePointsCommentAction.php', + 'PhabricatorEditEnginePreambleTransaction' => 'applications/transactions/xaction/PhabricatorEditEnginePreambleTransaction.php', 'PhabricatorEditEngineProfileMenuItem' => 'applications/search/menuitem/PhabricatorEditEngineProfileMenuItem.php', 'PhabricatorEditEngineQuery' => 'applications/transactions/query/PhabricatorEditEngineQuery.php', 'PhabricatorEditEngineSearchEngine' => 'applications/transactions/query/PhabricatorEditEngineSearchEngine.php', @@ -3175,7 +3199,9 @@ phutil_register_library_map(array( 'PhabricatorEditEngineSubtypeInterface' => 'applications/transactions/editengine/PhabricatorEditEngineSubtypeInterface.php', 'PhabricatorEditEngineSubtypeMap' => 'applications/transactions/editengine/PhabricatorEditEngineSubtypeMap.php', 'PhabricatorEditEngineSubtypeTestCase' => 'applications/transactions/editengine/__tests__/PhabricatorEditEngineSubtypeTestCase.php', + 'PhabricatorEditEngineSubtypeTransaction' => 'applications/transactions/xaction/PhabricatorEditEngineSubtypeTransaction.php', 'PhabricatorEditEngineTokenizerCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineTokenizerCommentAction.php', + 'PhabricatorEditEngineTransactionType' => 'applications/transactions/xaction/PhabricatorEditEngineTransactionType.php', 'PhabricatorEditField' => 'applications/transactions/editfield/PhabricatorEditField.php', 'PhabricatorEditPage' => 'applications/transactions/editengine/PhabricatorEditPage.php', 'PhabricatorEditType' => 'applications/transactions/edittype/PhabricatorEditType.php', @@ -3195,6 +3221,7 @@ phutil_register_library_map(array( 'PhabricatorEmailFormatSetting' => 'applications/settings/setting/PhabricatorEmailFormatSetting.php', 'PhabricatorEmailFormatSettingsPanel' => 'applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php', 'PhabricatorEmailLoginController' => 'applications/auth/controller/PhabricatorEmailLoginController.php', + 'PhabricatorEmailLoginUserLogType' => 'applications/people/userlog/PhabricatorEmailLoginUserLogType.php', 'PhabricatorEmailNotificationsSetting' => 'applications/settings/setting/PhabricatorEmailNotificationsSetting.php', 'PhabricatorEmailPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorEmailPreferencesSettingsPanel.php', 'PhabricatorEmailRePrefixSetting' => 'applications/settings/setting/PhabricatorEmailRePrefixSetting.php', @@ -3208,6 +3235,7 @@ phutil_register_library_map(array( 'PhabricatorEmojiRemarkupRule' => 'applications/macro/markup/PhabricatorEmojiRemarkupRule.php', 'PhabricatorEmojiTranslation' => 'infrastructure/internationalization/translation/PhabricatorEmojiTranslation.php', 'PhabricatorEmptyQueryException' => 'infrastructure/query/exception/PhabricatorEmptyQueryException.php', + 'PhabricatorEnterHisecUserLogType' => 'applications/people/userlog/PhabricatorEnterHisecUserLogType.php', 'PhabricatorEnumConfigType' => 'applications/config/type/PhabricatorEnumConfigType.php', 'PhabricatorEnv' => 'infrastructure/env/PhabricatorEnv.php', 'PhabricatorEnvTestCase' => 'infrastructure/env/__tests__/PhabricatorEnvTestCase.php', @@ -3220,6 +3248,7 @@ phutil_register_library_map(array( 'PhabricatorExampleEventListener' => 'infrastructure/events/PhabricatorExampleEventListener.php', 'PhabricatorExcelExportFormat' => 'infrastructure/export/format/PhabricatorExcelExportFormat.php', 'PhabricatorExecFutureFileUploadSource' => 'applications/files/uploadsource/PhabricatorExecFutureFileUploadSource.php', + 'PhabricatorExitHisecUserLogType' => 'applications/people/userlog/PhabricatorExitHisecUserLogType.php', 'PhabricatorExportEngine' => 'infrastructure/export/engine/PhabricatorExportEngine.php', 'PhabricatorExportEngineBulkJobType' => 'infrastructure/export/engine/PhabricatorExportEngineBulkJobType.php', 'PhabricatorExportEngineExtension' => 'infrastructure/export/engine/PhabricatorExportEngineExtension.php', @@ -3260,6 +3289,7 @@ phutil_register_library_map(array( 'PhabricatorFactObjectDimension' => 'applications/fact/storage/PhabricatorFactObjectDimension.php', 'PhabricatorFactRaw' => 'applications/fact/storage/PhabricatorFactRaw.php', 'PhabricatorFactUpdateIterator' => 'applications/fact/extract/PhabricatorFactUpdateIterator.php', + 'PhabricatorFailHisecUserLogType' => 'applications/people/userlog/PhabricatorFailHisecUserLogType.php', 'PhabricatorFaviconRef' => 'applications/files/favicon/PhabricatorFaviconRef.php', 'PhabricatorFaviconRefQuery' => 'applications/files/favicon/PhabricatorFaviconRefQuery.php', 'PhabricatorFavoritesApplication' => 'applications/favorites/application/PhabricatorFavoritesApplication.php', @@ -3394,6 +3424,7 @@ phutil_register_library_map(array( 'PhabricatorFlaggableInterface' => 'applications/flag/interface/PhabricatorFlaggableInterface.php', 'PhabricatorFlagsApplication' => 'applications/flag/application/PhabricatorFlagsApplication.php', 'PhabricatorFlagsUIEventListener' => 'applications/flag/events/PhabricatorFlagsUIEventListener.php', + 'PhabricatorFullLoginUserLogType' => 'applications/people/userlog/PhabricatorFullLoginUserLogType.php', 'PhabricatorFulltextEngine' => 'applications/search/index/PhabricatorFulltextEngine.php', 'PhabricatorFulltextEngineExtension' => 'applications/search/index/PhabricatorFulltextEngineExtension.php', 'PhabricatorFulltextEngineExtensionModule' => 'applications/search/index/PhabricatorFulltextEngineExtensionModule.php', @@ -3532,7 +3563,10 @@ phutil_register_library_map(array( 'PhabricatorLockLogManagementWorkflow' => 'applications/daemon/management/PhabricatorLockLogManagementWorkflow.php', 'PhabricatorLockManagementWorkflow' => 'applications/daemon/management/PhabricatorLockManagementWorkflow.php', 'PhabricatorLogTriggerAction' => 'infrastructure/daemon/workers/action/PhabricatorLogTriggerAction.php', + 'PhabricatorLoginFailureUserLogType' => 'applications/people/userlog/PhabricatorLoginFailureUserLogType.php', + 'PhabricatorLoginUserLogType' => 'applications/people/userlog/PhabricatorLoginUserLogType.php', 'PhabricatorLogoutController' => 'applications/auth/controller/PhabricatorLogoutController.php', + 'PhabricatorLogoutUserLogType' => 'applications/people/userlog/PhabricatorLogoutUserLogType.php', 'PhabricatorLunarPhasePolicyRule' => 'applications/policy/rule/PhabricatorLunarPhasePolicyRule.php', 'PhabricatorMacroApplication' => 'applications/macro/application/PhabricatorMacroApplication.php', 'PhabricatorMacroAudioBehaviorTransaction' => 'applications/macro/xaction/PhabricatorMacroAudioBehaviorTransaction.php', @@ -3849,7 +3883,21 @@ phutil_register_library_map(array( 'PhabricatorOwnersPathsSearchEngineAttachment' => 'applications/owners/engineextension/PhabricatorOwnersPathsSearchEngineAttachment.php', 'PhabricatorOwnersSchemaSpec' => 'applications/owners/storage/PhabricatorOwnersSchemaSpec.php', 'PhabricatorOwnersSearchField' => 'applications/owners/searchfield/PhabricatorOwnersSearchField.php', + 'PhabricatorPDFCatalogObject' => 'applications/phortune/pdf/PhabricatorPDFCatalogObject.php', + 'PhabricatorPDFContentsObject' => 'applications/phortune/pdf/PhabricatorPDFContentsObject.php', 'PhabricatorPDFDocumentEngine' => 'applications/files/document/PhabricatorPDFDocumentEngine.php', + 'PhabricatorPDFFontObject' => 'applications/phortune/pdf/PhabricatorPDFFontObject.php', + 'PhabricatorPDFFragment' => 'applications/phortune/pdf/PhabricatorPDFFragment.php', + 'PhabricatorPDFFragmentOffset' => 'applications/phortune/pdf/PhabricatorPDFFragmentOffset.php', + 'PhabricatorPDFGenerator' => 'applications/phortune/pdf/PhabricatorPDFGenerator.php', + 'PhabricatorPDFHeadFragment' => 'applications/phortune/pdf/PhabricatorPDFHeadFragment.php', + 'PhabricatorPDFInfoObject' => 'applications/phortune/pdf/PhabricatorPDFInfoObject.php', + 'PhabricatorPDFIterator' => 'applications/phortune/pdf/PhabricatorPDFIterator.php', + 'PhabricatorPDFObject' => 'applications/phortune/pdf/PhabricatorPDFObject.php', + 'PhabricatorPDFPageObject' => 'applications/phortune/pdf/PhabricatorPDFPageObject.php', + 'PhabricatorPDFPagesObject' => 'applications/phortune/pdf/PhabricatorPDFPagesObject.php', + 'PhabricatorPDFResourcesObject' => 'applications/phortune/pdf/PhabricatorPDFResourcesObject.php', + 'PhabricatorPDFTailFragment' => 'applications/phortune/pdf/PhabricatorPDFTailFragment.php', 'PhabricatorPHDConfigOptions' => 'applications/config/option/PhabricatorPHDConfigOptions.php', 'PhabricatorPHID' => 'applications/phid/storage/PhabricatorPHID.php', 'PhabricatorPHIDConstants' => 'applications/phid/PhabricatorPHIDConstants.php', @@ -3941,6 +3989,7 @@ phutil_register_library_map(array( 'PhabricatorPackagesVersionViewController' => 'applications/packages/controller/PhabricatorPackagesVersionViewController.php', 'PhabricatorPackagesView' => 'applications/packages/view/PhabricatorPackagesView.php', 'PhabricatorPagerUIExample' => 'applications/uiexample/examples/PhabricatorPagerUIExample.php', + 'PhabricatorPartialLoginUserLogType' => 'applications/people/userlog/PhabricatorPartialLoginUserLogType.php', 'PhabricatorPassphraseApplication' => 'applications/passphrase/application/PhabricatorPassphraseApplication.php', 'PhabricatorPasswordAuthProvider' => 'applications/auth/provider/PhabricatorPasswordAuthProvider.php', 'PhabricatorPasswordDestructionEngineExtension' => 'applications/auth/extension/PhabricatorPasswordDestructionEngineExtension.php', @@ -3991,6 +4040,7 @@ phutil_register_library_map(array( 'PhabricatorPeopleDeleteController' => 'applications/people/controller/PhabricatorPeopleDeleteController.php', 'PhabricatorPeopleDetailsProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeopleDetailsProfileMenuItem.php', 'PhabricatorPeopleDisableController' => 'applications/people/controller/PhabricatorPeopleDisableController.php', + 'PhabricatorPeopleEmailLoginMailEngine' => 'applications/people/mail/PhabricatorPeopleEmailLoginMailEngine.php', 'PhabricatorPeopleEmpowerController' => 'applications/people/controller/PhabricatorPeopleEmpowerController.php', 'PhabricatorPeopleExternalPHIDType' => 'applications/people/phid/PhabricatorPeopleExternalPHIDType.php', 'PhabricatorPeopleIconSet' => 'applications/people/icon/PhabricatorPeopleIconSet.php', @@ -4000,10 +4050,13 @@ phutil_register_library_map(array( 'PhabricatorPeopleListController' => 'applications/people/controller/PhabricatorPeopleListController.php', 'PhabricatorPeopleLogQuery' => 'applications/people/query/PhabricatorPeopleLogQuery.php', 'PhabricatorPeopleLogSearchEngine' => 'applications/people/query/PhabricatorPeopleLogSearchEngine.php', + 'PhabricatorPeopleLogViewController' => 'applications/people/controller/PhabricatorPeopleLogViewController.php', 'PhabricatorPeopleLogsController' => 'applications/people/controller/PhabricatorPeopleLogsController.php', 'PhabricatorPeopleMailEngine' => 'applications/people/mail/PhabricatorPeopleMailEngine.php', 'PhabricatorPeopleMailEngineException' => 'applications/people/mail/PhabricatorPeopleMailEngineException.php', 'PhabricatorPeopleManageProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeopleManageProfileMenuItem.php', + 'PhabricatorPeopleManagementEmpowerWorkflow' => 'applications/people/management/PhabricatorPeopleManagementEmpowerWorkflow.php', + 'PhabricatorPeopleManagementEnableWorkflow' => 'applications/people/management/PhabricatorPeopleManagementEnableWorkflow.php', 'PhabricatorPeopleManagementWorkflow' => 'applications/people/management/PhabricatorPeopleManagementWorkflow.php', 'PhabricatorPeopleNewController' => 'applications/people/controller/PhabricatorPeopleNewController.php', 'PhabricatorPeopleNoOwnerDatasource' => 'applications/people/typeahead/PhabricatorPeopleNoOwnerDatasource.php', @@ -4013,7 +4066,6 @@ phutil_register_library_map(array( 'PhabricatorPeopleProfileCommitsController' => 'applications/people/controller/PhabricatorPeopleProfileCommitsController.php', 'PhabricatorPeopleProfileController' => 'applications/people/controller/PhabricatorPeopleProfileController.php', 'PhabricatorPeopleProfileEditController' => 'applications/people/controller/PhabricatorPeopleProfileEditController.php', - 'PhabricatorPeopleProfileImageWorkflow' => 'applications/people/management/PhabricatorPeopleProfileImageWorkflow.php', 'PhabricatorPeopleProfileManageController' => 'applications/people/controller/PhabricatorPeopleProfileManageController.php', 'PhabricatorPeopleProfileMenuEngine' => 'applications/people/engine/PhabricatorPeopleProfileMenuEngine.php', 'PhabricatorPeopleProfilePictureController' => 'applications/people/controller/PhabricatorPeopleProfilePictureController.php', @@ -4128,6 +4180,7 @@ phutil_register_library_map(array( 'PhabricatorPolicyTestObject' => 'applications/policy/__tests__/PhabricatorPolicyTestObject.php', 'PhabricatorPolicyType' => 'applications/policy/constants/PhabricatorPolicyType.php', 'PhabricatorPonderApplication' => 'applications/ponder/application/PhabricatorPonderApplication.php', + 'PhabricatorPrimaryEmailUserLogType' => 'applications/people/userlog/PhabricatorPrimaryEmailUserLogType.php', 'PhabricatorProfileMenuEditEngine' => 'applications/search/editor/PhabricatorProfileMenuEditEngine.php', 'PhabricatorProfileMenuEditor' => 'applications/search/editor/PhabricatorProfileMenuEditor.php', 'PhabricatorProfileMenuEngine' => 'applications/search/engine/PhabricatorProfileMenuEngine.php', @@ -4148,9 +4201,12 @@ phutil_register_library_map(array( 'PhabricatorProjectArchiveController' => 'applications/project/controller/PhabricatorProjectArchiveController.php', 'PhabricatorProjectBoardBackgroundController' => 'applications/project/controller/PhabricatorProjectBoardBackgroundController.php', 'PhabricatorProjectBoardController' => 'applications/project/controller/PhabricatorProjectBoardController.php', + 'PhabricatorProjectBoardDefaultController' => 'applications/project/controller/PhabricatorProjectBoardDefaultController.php', 'PhabricatorProjectBoardDisableController' => 'applications/project/controller/PhabricatorProjectBoardDisableController.php', + 'PhabricatorProjectBoardFilterController' => 'applications/project/controller/PhabricatorProjectBoardFilterController.php', 'PhabricatorProjectBoardImportController' => 'applications/project/controller/PhabricatorProjectBoardImportController.php', 'PhabricatorProjectBoardManageController' => 'applications/project/controller/PhabricatorProjectBoardManageController.php', + 'PhabricatorProjectBoardReloadController' => 'applications/project/controller/PhabricatorProjectBoardReloadController.php', 'PhabricatorProjectBoardReorderController' => 'applications/project/controller/PhabricatorProjectBoardReorderController.php', 'PhabricatorProjectBoardViewController' => 'applications/project/controller/PhabricatorProjectBoardViewController.php', 'PhabricatorProjectBuiltinsExample' => 'applications/uiexample/examples/PhabricatorProjectBuiltinsExample.php', @@ -4160,6 +4216,8 @@ phutil_register_library_map(array( 'PhabricatorProjectColorsConfigType' => 'applications/project/config/PhabricatorProjectColorsConfigType.php', 'PhabricatorProjectColumn' => 'applications/project/storage/PhabricatorProjectColumn.php', 'PhabricatorProjectColumnAuthorOrder' => 'applications/project/order/PhabricatorProjectColumnAuthorOrder.php', + 'PhabricatorProjectColumnBulkEditController' => 'applications/project/controller/PhabricatorProjectColumnBulkEditController.php', + 'PhabricatorProjectColumnBulkMoveController' => 'applications/project/controller/PhabricatorProjectColumnBulkMoveController.php', 'PhabricatorProjectColumnCreatedOrder' => 'applications/project/order/PhabricatorProjectColumnCreatedOrder.php', 'PhabricatorProjectColumnDetailController' => 'applications/project/controller/PhabricatorProjectColumnDetailController.php', 'PhabricatorProjectColumnEditController' => 'applications/project/controller/PhabricatorProjectColumnEditController.php', @@ -4186,6 +4244,7 @@ phutil_register_library_map(array( 'PhabricatorProjectColumnTransactionQuery' => 'applications/project/query/PhabricatorProjectColumnTransactionQuery.php', 'PhabricatorProjectColumnTransactionType' => 'applications/project/xaction/column/PhabricatorProjectColumnTransactionType.php', 'PhabricatorProjectColumnTriggerTransaction' => 'applications/project/xaction/column/PhabricatorProjectColumnTriggerTransaction.php', + 'PhabricatorProjectColumnViewQueryController' => 'applications/project/controller/PhabricatorProjectColumnViewQueryController.php', 'PhabricatorProjectConfigOptions' => 'applications/project/config/PhabricatorProjectConfigOptions.php', 'PhabricatorProjectConfiguredCustomField' => 'applications/project/customfield/PhabricatorProjectConfiguredCustomField.php', 'PhabricatorProjectController' => 'applications/project/controller/PhabricatorProjectController.php', @@ -4197,7 +4256,6 @@ phutil_register_library_map(array( 'PhabricatorProjectCustomFieldStringIndex' => 'applications/project/storage/PhabricatorProjectCustomFieldStringIndex.php', 'PhabricatorProjectDAO' => 'applications/project/storage/PhabricatorProjectDAO.php', 'PhabricatorProjectDatasource' => 'applications/project/typeahead/PhabricatorProjectDatasource.php', - 'PhabricatorProjectDefaultController' => 'applications/project/controller/PhabricatorProjectDefaultController.php', 'PhabricatorProjectDescriptionField' => 'applications/project/customfield/PhabricatorProjectDescriptionField.php', 'PhabricatorProjectDetailsProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectDetailsProfileMenuItem.php', 'PhabricatorProjectDropEffect' => 'applications/project/icon/PhabricatorProjectDropEffect.php', @@ -4346,6 +4404,7 @@ phutil_register_library_map(array( 'PhabricatorQueryOrderTestCase' => 'infrastructure/query/order/__tests__/PhabricatorQueryOrderTestCase.php', 'PhabricatorQueryOrderVector' => 'infrastructure/query/order/PhabricatorQueryOrderVector.php', 'PhabricatorRateLimitRequestExceptionHandler' => 'aphront/handler/PhabricatorRateLimitRequestExceptionHandler.php', + 'PhabricatorReassignEmailUserLogType' => 'applications/people/userlog/PhabricatorReassignEmailUserLogType.php', 'PhabricatorRebuildIndexesWorker' => 'applications/search/worker/PhabricatorRebuildIndexesWorker.php', 'PhabricatorRecaptchaConfigOptions' => 'applications/config/option/PhabricatorRecaptchaConfigOptions.php', 'PhabricatorRedirectController' => 'applications/base/controller/PhabricatorRedirectController.php', @@ -4364,6 +4423,8 @@ phutil_register_library_map(array( 'PhabricatorRemarkupFigletBlockInterpreter' => 'infrastructure/markup/interpreter/PhabricatorRemarkupFigletBlockInterpreter.php', 'PhabricatorRemarkupHyperlinkEngineExtension' => 'applications/remarkup/engineextension/PhabricatorRemarkupHyperlinkEngineExtension.php', 'PhabricatorRemarkupUIExample' => 'applications/uiexample/examples/PhabricatorRemarkupUIExample.php', + 'PhabricatorRemoveEmailUserLogType' => 'applications/people/userlog/PhabricatorRemoveEmailUserLogType.php', + 'PhabricatorRemoveMultifactorUserLogType' => 'applications/people/userlog/PhabricatorRemoveMultifactorUserLogType.php', 'PhabricatorRepositoriesSetupCheck' => 'applications/config/check/PhabricatorRepositoriesSetupCheck.php', 'PhabricatorRepository' => 'applications/repository/storage/PhabricatorRepository.php', 'PhabricatorRepositoryActivateTransaction' => 'applications/repository/xaction/PhabricatorRepositoryActivateTransaction.php', @@ -4413,6 +4474,7 @@ phutil_register_library_map(array( 'PhabricatorRepositoryIdentityTransaction' => 'applications/repository/storage/PhabricatorRepositoryIdentityTransaction.php', 'PhabricatorRepositoryIdentityTransactionQuery' => 'applications/repository/query/PhabricatorRepositoryIdentityTransactionQuery.php', 'PhabricatorRepositoryIdentityTransactionType' => 'applications/repository/xaction/PhabricatorRepositoryIdentityTransactionType.php', + 'PhabricatorRepositoryMaintenanceTransaction' => 'applications/repository/xaction/PhabricatorRepositoryMaintenanceTransaction.php', 'PhabricatorRepositoryManagementCacheWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementCacheWorkflow.php', 'PhabricatorRepositoryManagementClusterizeWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementClusterizeWorkflow.php', 'PhabricatorRepositoryManagementDiscoverWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementDiscoverWorkflow.php', @@ -4421,6 +4483,7 @@ phutil_register_library_map(array( 'PhabricatorRepositoryManagementListPathsWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementListPathsWorkflow.php', 'PhabricatorRepositoryManagementListWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementListWorkflow.php', 'PhabricatorRepositoryManagementLookupUsersWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementLookupUsersWorkflow.php', + 'PhabricatorRepositoryManagementMaintenanceWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementMaintenanceWorkflow.php', 'PhabricatorRepositoryManagementMarkImportedWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementMarkImportedWorkflow.php', 'PhabricatorRepositoryManagementMarkReachableWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementMarkReachableWorkflow.php', 'PhabricatorRepositoryManagementMirrorWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementMirrorWorkflow.php', @@ -4502,6 +4565,7 @@ phutil_register_library_map(array( 'PhabricatorRepositoryVCSTransaction' => 'applications/repository/xaction/PhabricatorRepositoryVCSTransaction.php', 'PhabricatorRepositoryWorkingCopyVersion' => 'applications/repository/storage/PhabricatorRepositoryWorkingCopyVersion.php', 'PhabricatorRequestExceptionHandler' => 'aphront/handler/PhabricatorRequestExceptionHandler.php', + 'PhabricatorResetPasswordUserLogType' => 'applications/people/userlog/PhabricatorResetPasswordUserLogType.php', 'PhabricatorResourceSite' => 'aphront/site/PhabricatorResourceSite.php', 'PhabricatorRobotsController' => 'applications/system/controller/PhabricatorRobotsController.php', 'PhabricatorS3FileStorageEngine' => 'applications/files/engine/PhabricatorS3FileStorageEngine.php', @@ -4609,6 +4673,7 @@ phutil_register_library_map(array( 'PhabricatorShiftChartFunction' => 'applications/fact/chart/PhabricatorShiftChartFunction.php', 'PhabricatorShortSite' => 'aphront/site/PhabricatorShortSite.php', 'PhabricatorShowFiletreeSetting' => 'applications/settings/setting/PhabricatorShowFiletreeSetting.php', + 'PhabricatorSignDocumentsUserLogType' => 'applications/people/userlog/PhabricatorSignDocumentsUserLogType.php', 'PhabricatorSimpleEditType' => 'applications/transactions/edittype/PhabricatorSimpleEditType.php', 'PhabricatorSinChartFunction' => 'applications/fact/chart/PhabricatorSinChartFunction.php', 'PhabricatorSite' => 'aphront/site/PhabricatorSite.php', @@ -4899,6 +4964,8 @@ phutil_register_library_map(array( 'PhabricatorUserFulltextEngine' => 'applications/people/search/PhabricatorUserFulltextEngine.php', 'PhabricatorUserIconField' => 'applications/people/customfield/PhabricatorUserIconField.php', 'PhabricatorUserLog' => 'applications/people/storage/PhabricatorUserLog.php', + 'PhabricatorUserLogType' => 'applications/people/userlog/PhabricatorUserLogType.php', + 'PhabricatorUserLogTypeDatasource' => 'applications/people/typeahead/PhabricatorUserLogTypeDatasource.php', 'PhabricatorUserLogView' => 'applications/people/view/PhabricatorUserLogView.php', 'PhabricatorUserMessageCountCacheType' => 'applications/people/cache/PhabricatorUserMessageCountCacheType.php', 'PhabricatorUserNotificationCountCacheType' => 'applications/people/cache/PhabricatorUserNotificationCountCacheType.php', @@ -4929,6 +4996,7 @@ phutil_register_library_map(array( 'PhabricatorUsersPolicyRule' => 'applications/people/policyrule/PhabricatorUsersPolicyRule.php', 'PhabricatorUsersSearchField' => 'applications/people/searchfield/PhabricatorUsersSearchField.php', 'PhabricatorVCSResponse' => 'applications/repository/response/PhabricatorVCSResponse.php', + 'PhabricatorVerifyEmailUserLogType' => 'applications/people/userlog/PhabricatorVerifyEmailUserLogType.php', 'PhabricatorVersionedDraft' => 'applications/draft/storage/PhabricatorVersionedDraft.php', 'PhabricatorVeryWowEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorVeryWowEnglishTranslation.php', 'PhabricatorVideoDocumentEngine' => 'applications/files/document/PhabricatorVideoDocumentEngine.php', @@ -4940,6 +5008,7 @@ phutil_register_library_map(array( 'PhabricatorWeekStartDaySetting' => 'applications/settings/setting/PhabricatorWeekStartDaySetting.php', 'PhabricatorWildConfigType' => 'applications/config/type/PhabricatorWildConfigType.php', 'PhabricatorWordPressAuthProvider' => 'applications/auth/provider/PhabricatorWordPressAuthProvider.php', + 'PhabricatorWorkboardViewState' => 'applications/project/state/PhabricatorWorkboardViewState.php', 'PhabricatorWorker' => 'infrastructure/daemon/workers/PhabricatorWorker.php', 'PhabricatorWorkerActiveTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php', 'PhabricatorWorkerActiveTaskQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerActiveTaskQuery.php', @@ -5008,6 +5077,7 @@ phutil_register_library_map(array( 'PhabricatorXHProfSampleQuery' => 'applications/xhprof/query/PhabricatorXHProfSampleQuery.php', 'PhabricatorXHProfSampleSearchEngine' => 'applications/xhprof/query/PhabricatorXHProfSampleSearchEngine.php', 'PhabricatorYoutubeRemarkupRule' => 'infrastructure/markup/rule/PhabricatorYoutubeRemarkupRule.php', + 'PhabricatorZipSetupCheck' => 'applications/config/check/PhabricatorZipSetupCheck.php', 'Phame404Response' => 'applications/phame/site/Phame404Response.php', 'PhameBlog' => 'applications/phame/storage/PhameBlog.php', 'PhameBlog404Controller' => 'applications/phame/controller/blog/PhameBlog404Controller.php', @@ -5158,25 +5228,50 @@ phutil_register_library_map(array( 'PhortuneAccount' => 'applications/phortune/storage/PhortuneAccount.php', 'PhortuneAccountAddManagerController' => 'applications/phortune/controller/account/PhortuneAccountAddManagerController.php', 'PhortuneAccountBillingAddressTransaction' => 'applications/phortune/xaction/PhortuneAccountBillingAddressTransaction.php', - 'PhortuneAccountBillingController' => 'applications/phortune/controller/account/PhortuneAccountBillingController.php', 'PhortuneAccountBillingNameTransaction' => 'applications/phortune/xaction/PhortuneAccountBillingNameTransaction.php', 'PhortuneAccountChargeListController' => 'applications/phortune/controller/account/PhortuneAccountChargeListController.php', + 'PhortuneAccountChargesController' => 'applications/phortune/controller/account/PhortuneAccountChargesController.php', 'PhortuneAccountController' => 'applications/phortune/controller/account/PhortuneAccountController.php', + 'PhortuneAccountDetailsController' => 'applications/phortune/controller/account/PhortuneAccountDetailsController.php', 'PhortuneAccountEditController' => 'applications/phortune/controller/account/PhortuneAccountEditController.php', 'PhortuneAccountEditEngine' => 'applications/phortune/editor/PhortuneAccountEditEngine.php', 'PhortuneAccountEditor' => 'applications/phortune/editor/PhortuneAccountEditor.php', + 'PhortuneAccountEmail' => 'applications/phortune/storage/PhortuneAccountEmail.php', + 'PhortuneAccountEmailAddressTransaction' => 'applications/phortune/xaction/PhortuneAccountEmailAddressTransaction.php', + 'PhortuneAccountEmailAddressesController' => 'applications/phortune/controller/account/PhortuneAccountEmailAddressesController.php', + 'PhortuneAccountEmailEditController' => 'applications/phortune/controller/account/PhortuneAccountEmailEditController.php', + 'PhortuneAccountEmailEditEngine' => 'applications/phortune/editor/PhortuneAccountEmailEditEngine.php', + 'PhortuneAccountEmailEditor' => 'applications/phortune/editor/PhortuneAccountEmailEditor.php', + 'PhortuneAccountEmailPHIDType' => 'applications/phortune/phid/PhortuneAccountEmailPHIDType.php', + 'PhortuneAccountEmailQuery' => 'applications/phortune/query/PhortuneAccountEmailQuery.php', + 'PhortuneAccountEmailRotateController' => 'applications/phortune/controller/account/PhortuneAccountEmailRotateController.php', + 'PhortuneAccountEmailRotateTransaction' => 'applications/phortune/xaction/PhortuneAccountEmailRotateTransaction.php', + 'PhortuneAccountEmailStatus' => 'applications/phortune/constants/PhortuneAccountEmailStatus.php', + 'PhortuneAccountEmailStatusController' => 'applications/phortune/controller/account/PhortuneAccountEmailStatusController.php', + 'PhortuneAccountEmailStatusTransaction' => 'applications/phortune/xaction/PhortuneAccountEmailStatusTransaction.php', + 'PhortuneAccountEmailTransaction' => 'applications/phortune/storage/PhortuneAccountEmailTransaction.php', + 'PhortuneAccountEmailTransactionQuery' => 'applications/phortune/query/PhortuneAccountEmailTransactionQuery.php', + 'PhortuneAccountEmailTransactionType' => 'applications/phortune/xaction/PhortuneAccountEmailTransactionType.php', + 'PhortuneAccountEmailViewController' => 'applications/phortune/controller/account/PhortuneAccountEmailViewController.php', 'PhortuneAccountHasMemberEdgeType' => 'applications/phortune/edge/PhortuneAccountHasMemberEdgeType.php', + 'PhortuneAccountHasMerchantEdgeType' => 'applications/phortune/edge/PhortuneAccountHasMerchantEdgeType.php', 'PhortuneAccountListController' => 'applications/phortune/controller/account/PhortuneAccountListController.php', - 'PhortuneAccountManagerController' => 'applications/phortune/controller/account/PhortuneAccountManagerController.php', + 'PhortuneAccountManagersController' => 'applications/phortune/controller/account/PhortuneAccountManagersController.php', 'PhortuneAccountNameTransaction' => 'applications/phortune/xaction/PhortuneAccountNameTransaction.php', + 'PhortuneAccountOrderListController' => 'applications/phortune/controller/account/PhortuneAccountOrderListController.php', + 'PhortuneAccountOrdersController' => 'applications/phortune/controller/account/PhortuneAccountOrdersController.php', + 'PhortuneAccountOverviewController' => 'applications/phortune/controller/account/PhortuneAccountOverviewController.php', 'PhortuneAccountPHIDType' => 'applications/phortune/phid/PhortuneAccountPHIDType.php', + 'PhortuneAccountPaymentMethodController' => 'applications/phortune/controller/account/PhortuneAccountPaymentMethodController.php', + 'PhortuneAccountPaymentMethodViewController' => 'applications/phortune/controller/account/PhortuneAccountPaymentMethodViewController.php', 'PhortuneAccountProfileController' => 'applications/phortune/controller/account/PhortuneAccountProfileController.php', 'PhortuneAccountQuery' => 'applications/phortune/query/PhortuneAccountQuery.php', + 'PhortuneAccountSubscriptionAutopayController' => 'applications/phortune/controller/account/PhortuneAccountSubscriptionAutopayController.php', 'PhortuneAccountSubscriptionController' => 'applications/phortune/controller/account/PhortuneAccountSubscriptionController.php', + 'PhortuneAccountSubscriptionViewController' => 'applications/phortune/controller/account/PhortuneAccountSubscriptionViewController.php', 'PhortuneAccountTransaction' => 'applications/phortune/storage/PhortuneAccountTransaction.php', 'PhortuneAccountTransactionQuery' => 'applications/phortune/query/PhortuneAccountTransactionQuery.php', 'PhortuneAccountTransactionType' => 'applications/phortune/xaction/PhortuneAccountTransactionType.php', - 'PhortuneAccountViewController' => 'applications/phortune/controller/account/PhortuneAccountViewController.php', 'PhortuneAdHocCart' => 'applications/phortune/cart/PhortuneAdHocCart.php', 'PhortuneAdHocProduct' => 'applications/phortune/product/PhortuneAdHocProduct.php', 'PhortuneAddPaymentMethodAction' => 'applications/phortune/action/PhortuneAddPaymentMethodAction.php', @@ -5187,7 +5282,6 @@ phutil_register_library_map(array( 'PhortuneCartController' => 'applications/phortune/controller/cart/PhortuneCartController.php', 'PhortuneCartEditor' => 'applications/phortune/editor/PhortuneCartEditor.php', 'PhortuneCartImplementation' => 'applications/phortune/cart/PhortuneCartImplementation.php', - 'PhortuneCartListController' => 'applications/phortune/controller/cart/PhortuneCartListController.php', 'PhortuneCartPHIDType' => 'applications/phortune/phid/PhortuneCartPHIDType.php', 'PhortuneCartQuery' => 'applications/phortune/query/PhortuneCartQuery.php', 'PhortuneCartReplyHandler' => 'applications/phortune/mail/PhortuneCartReplyHandler.php', @@ -5196,6 +5290,7 @@ phutil_register_library_map(array( 'PhortuneCartTransactionQuery' => 'applications/phortune/query/PhortuneCartTransactionQuery.php', 'PhortuneCartUpdateController' => 'applications/phortune/controller/cart/PhortuneCartUpdateController.php', 'PhortuneCartViewController' => 'applications/phortune/controller/cart/PhortuneCartViewController.php', + 'PhortuneCartVoidController' => 'applications/phortune/controller/cart/PhortuneCartVoidController.php', 'PhortuneCharge' => 'applications/phortune/storage/PhortuneCharge.php', 'PhortuneChargePHIDType' => 'applications/phortune/phid/PhortuneChargePHIDType.php', 'PhortuneChargeQuery' => 'applications/phortune/query/PhortuneChargeQuery.php', @@ -5210,7 +5305,10 @@ phutil_register_library_map(array( 'PhortuneDAO' => 'applications/phortune/storage/PhortuneDAO.php', 'PhortuneDisplayException' => 'applications/phortune/exception/PhortuneDisplayException.php', 'PhortuneErrCode' => 'applications/phortune/constants/PhortuneErrCode.php', - 'PhortuneInvoiceView' => 'applications/phortune/view/PhortuneInvoiceView.php', + 'PhortuneExternalController' => 'applications/phortune/controller/external/PhortuneExternalController.php', + 'PhortuneExternalOrderController' => 'applications/phortune/controller/external/PhortuneExternalOrderController.php', + 'PhortuneExternalOverviewController' => 'applications/phortune/controller/external/PhortuneExternalOverviewController.php', + 'PhortuneExternalUnsubscribeController' => 'applications/phortune/controller/external/PhortuneExternalUnsubscribeController.php', 'PhortuneLandingController' => 'applications/phortune/controller/PhortuneLandingController.php', 'PhortuneMemberHasAccountEdgeType' => 'applications/phortune/edge/PhortuneMemberHasAccountEdgeType.php', 'PhortuneMemberHasMerchantEdgeType' => 'applications/phortune/edge/PhortuneMemberHasMerchantEdgeType.php', @@ -5220,35 +5318,56 @@ phutil_register_library_map(array( 'PhortuneMerchantContactInfoTransaction' => 'applications/phortune/xaction/PhortuneMerchantContactInfoTransaction.php', 'PhortuneMerchantController' => 'applications/phortune/controller/merchant/PhortuneMerchantController.php', 'PhortuneMerchantDescriptionTransaction' => 'applications/phortune/xaction/PhortuneMerchantDescriptionTransaction.php', + 'PhortuneMerchantDetailsController' => 'applications/phortune/controller/merchant/PhortuneMerchantDetailsController.php', 'PhortuneMerchantEditController' => 'applications/phortune/controller/merchant/PhortuneMerchantEditController.php', 'PhortuneMerchantEditEngine' => 'applications/phortune/editor/PhortuneMerchantEditEngine.php', 'PhortuneMerchantEditor' => 'applications/phortune/editor/PhortuneMerchantEditor.php', + 'PhortuneMerchantHasAccountEdgeType' => 'applications/phortune/edge/PhortuneMerchantHasAccountEdgeType.php', 'PhortuneMerchantHasMemberEdgeType' => 'applications/phortune/edge/PhortuneMerchantHasMemberEdgeType.php', 'PhortuneMerchantInvoiceCreateController' => 'applications/phortune/controller/merchant/PhortuneMerchantInvoiceCreateController.php', 'PhortuneMerchantInvoiceEmailTransaction' => 'applications/phortune/xaction/PhortuneMerchantInvoiceEmailTransaction.php', 'PhortuneMerchantInvoiceFooterTransaction' => 'applications/phortune/xaction/PhortuneMerchantInvoiceFooterTransaction.php', 'PhortuneMerchantListController' => 'applications/phortune/controller/merchant/PhortuneMerchantListController.php', - 'PhortuneMerchantManagerController' => 'applications/phortune/controller/merchant/PhortuneMerchantManagerController.php', + 'PhortuneMerchantManagersController' => 'applications/phortune/controller/merchant/PhortuneMerchantManagersController.php', 'PhortuneMerchantNameTransaction' => 'applications/phortune/xaction/PhortuneMerchantNameTransaction.php', + 'PhortuneMerchantOrderListController' => 'applications/phortune/controller/merchant/PhortuneMerchantOrderListController.php', + 'PhortuneMerchantOrdersController' => 'applications/phortune/controller/merchant/PhortuneMerchantOrdersController.php', + 'PhortuneMerchantOverviewController' => 'applications/phortune/controller/merchant/PhortuneMerchantOverviewController.php', 'PhortuneMerchantPHIDType' => 'applications/phortune/phid/PhortuneMerchantPHIDType.php', 'PhortuneMerchantPictureController' => 'applications/phortune/controller/merchant/PhortuneMerchantPictureController.php', 'PhortuneMerchantPictureTransaction' => 'applications/phortune/xaction/PhortuneMerchantPictureTransaction.php', 'PhortuneMerchantProfileController' => 'applications/phortune/controller/merchant/PhortuneMerchantProfileController.php', + 'PhortuneMerchantProviderDisableController' => 'applications/phortune/controller/merchant/PhortuneMerchantProviderDisableController.php', + 'PhortuneMerchantProviderEditController' => 'applications/phortune/controller/merchant/PhortuneMerchantProviderEditController.php', + 'PhortuneMerchantProviderViewController' => 'applications/phortune/controller/merchant/PhortuneMerchantProviderViewController.php', + 'PhortuneMerchantProvidersController' => 'applications/phortune/controller/merchant/PhortuneMerchantProvidersController.php', 'PhortuneMerchantQuery' => 'applications/phortune/query/PhortuneMerchantQuery.php', 'PhortuneMerchantSearchEngine' => 'applications/phortune/query/PhortuneMerchantSearchEngine.php', + 'PhortuneMerchantSubscriptionListController' => 'applications/phortune/controller/merchant/PhortuneMerchantSubscriptionListController.php', + 'PhortuneMerchantSubscriptionsController' => 'applications/phortune/controller/merchant/PhortuneMerchantSubscriptionsController.php', 'PhortuneMerchantTransaction' => 'applications/phortune/storage/PhortuneMerchantTransaction.php', 'PhortuneMerchantTransactionQuery' => 'applications/phortune/query/PhortuneMerchantTransactionQuery.php', 'PhortuneMerchantTransactionType' => 'applications/phortune/xaction/PhortuneMerchantTransactionType.php', - 'PhortuneMerchantViewController' => 'applications/phortune/controller/merchant/PhortuneMerchantViewController.php', 'PhortuneMonthYearExpiryControl' => 'applications/phortune/control/PhortuneMonthYearExpiryControl.php', + 'PhortuneOrderDescriptionView' => 'applications/phortune/view/PhortuneOrderDescriptionView.php', + 'PhortuneOrderItemsView' => 'applications/phortune/view/PhortuneOrderItemsView.php', + 'PhortuneOrderSummaryView' => 'applications/phortune/view/PhortuneOrderSummaryView.php', 'PhortuneOrderTableView' => 'applications/phortune/view/PhortuneOrderTableView.php', + 'PhortuneOrderView' => 'applications/phortune/view/PhortuneOrderView.php', 'PhortunePayPalPaymentProvider' => 'applications/phortune/provider/PhortunePayPalPaymentProvider.php', 'PhortunePaymentMethod' => 'applications/phortune/storage/PhortunePaymentMethod.php', - 'PhortunePaymentMethodCreateController' => 'applications/phortune/controller/payment/PhortunePaymentMethodCreateController.php', - 'PhortunePaymentMethodDisableController' => 'applications/phortune/controller/payment/PhortunePaymentMethodDisableController.php', - 'PhortunePaymentMethodEditController' => 'applications/phortune/controller/payment/PhortunePaymentMethodEditController.php', + 'PhortunePaymentMethodCreateController' => 'applications/phortune/controller/paymentmethod/PhortunePaymentMethodCreateController.php', + 'PhortunePaymentMethodDisableController' => 'applications/phortune/controller/paymentmethod/PhortunePaymentMethodDisableController.php', + 'PhortunePaymentMethodEditController' => 'applications/phortune/controller/paymentmethod/PhortunePaymentMethodEditController.php', + 'PhortunePaymentMethodEditor' => 'applications/phortune/editor/PhortunePaymentMethodEditor.php', + 'PhortunePaymentMethodNameTransaction' => 'applications/phortune/xaction/paymentmethod/PhortunePaymentMethodNameTransaction.php', 'PhortunePaymentMethodPHIDType' => 'applications/phortune/phid/PhortunePaymentMethodPHIDType.php', + 'PhortunePaymentMethodPolicyCodex' => 'applications/phortune/codex/PhortunePaymentMethodPolicyCodex.php', 'PhortunePaymentMethodQuery' => 'applications/phortune/query/PhortunePaymentMethodQuery.php', + 'PhortunePaymentMethodStatusTransaction' => 'applications/phortune/xaction/paymentmethod/PhortunePaymentMethodStatusTransaction.php', + 'PhortunePaymentMethodTransaction' => 'applications/phortune/storage/PhortunePaymentMethodTransaction.php', + 'PhortunePaymentMethodTransactionQuery' => 'applications/phortune/query/PhortunePaymentMethodTransactionQuery.php', + 'PhortunePaymentMethodTransactionType' => 'applications/phortune/xaction/paymentmethod/PhortunePaymentMethodTransactionType.php', 'PhortunePaymentProvider' => 'applications/phortune/provider/PhortunePaymentProvider.php', 'PhortunePaymentProviderConfig' => 'applications/phortune/storage/PhortunePaymentProviderConfig.php', 'PhortunePaymentProviderConfigEditor' => 'applications/phortune/editor/PhortunePaymentProviderConfigEditor.php', @@ -5264,24 +5383,25 @@ phutil_register_library_map(array( 'PhortuneProductQuery' => 'applications/phortune/query/PhortuneProductQuery.php', 'PhortuneProductViewController' => 'applications/phortune/controller/product/PhortuneProductViewController.php', 'PhortuneProviderActionController' => 'applications/phortune/controller/provider/PhortuneProviderActionController.php', - 'PhortuneProviderDisableController' => 'applications/phortune/controller/provider/PhortuneProviderDisableController.php', - 'PhortuneProviderEditController' => 'applications/phortune/controller/provider/PhortuneProviderEditController.php', 'PhortunePurchase' => 'applications/phortune/storage/PhortunePurchase.php', 'PhortunePurchasePHIDType' => 'applications/phortune/phid/PhortunePurchasePHIDType.php', 'PhortunePurchaseQuery' => 'applications/phortune/query/PhortunePurchaseQuery.php', 'PhortuneSchemaSpec' => 'applications/phortune/storage/PhortuneSchemaSpec.php', 'PhortuneStripePaymentProvider' => 'applications/phortune/provider/PhortuneStripePaymentProvider.php', 'PhortuneSubscription' => 'applications/phortune/storage/PhortuneSubscription.php', + 'PhortuneSubscriptionAutopayTransaction' => 'applications/phortune/xaction/subscription/PhortuneSubscriptionAutopayTransaction.php', 'PhortuneSubscriptionCart' => 'applications/phortune/cart/PhortuneSubscriptionCart.php', - 'PhortuneSubscriptionEditController' => 'applications/phortune/controller/subscription/PhortuneSubscriptionEditController.php', + 'PhortuneSubscriptionEditor' => 'applications/phortune/editor/PhortuneSubscriptionEditor.php', 'PhortuneSubscriptionImplementation' => 'applications/phortune/subscription/PhortuneSubscriptionImplementation.php', - 'PhortuneSubscriptionListController' => 'applications/phortune/controller/subscription/PhortuneSubscriptionListController.php', 'PhortuneSubscriptionPHIDType' => 'applications/phortune/phid/PhortuneSubscriptionPHIDType.php', + 'PhortuneSubscriptionPolicyCodex' => 'applications/phortune/codex/PhortuneSubscriptionPolicyCodex.php', 'PhortuneSubscriptionProduct' => 'applications/phortune/product/PhortuneSubscriptionProduct.php', 'PhortuneSubscriptionQuery' => 'applications/phortune/query/PhortuneSubscriptionQuery.php', 'PhortuneSubscriptionSearchEngine' => 'applications/phortune/query/PhortuneSubscriptionSearchEngine.php', 'PhortuneSubscriptionTableView' => 'applications/phortune/view/PhortuneSubscriptionTableView.php', - 'PhortuneSubscriptionViewController' => 'applications/phortune/controller/subscription/PhortuneSubscriptionViewController.php', + 'PhortuneSubscriptionTransaction' => 'applications/phortune/storage/PhortuneSubscriptionTransaction.php', + 'PhortuneSubscriptionTransactionQuery' => 'applications/phortune/query/PhortuneSubscriptionTransactionQuery.php', + 'PhortuneSubscriptionTransactionType' => 'applications/phortune/xaction/subscription/PhortuneSubscriptionTransactionType.php', 'PhortuneSubscriptionWorker' => 'applications/phortune/worker/PhortuneSubscriptionWorker.php', 'PhortuneTestPaymentProvider' => 'applications/phortune/provider/PhortuneTestPaymentProvider.php', 'PhragmentBrowseController' => 'applications/phragment/controller/PhragmentBrowseController.php', @@ -5569,6 +5689,7 @@ phutil_register_library_map(array( 'SlowvoteEmbedView' => 'applications/slowvote/view/SlowvoteEmbedView.php', 'SlowvoteInfoConduitAPIMethod' => 'applications/slowvote/conduit/SlowvoteInfoConduitAPIMethod.php', 'SlowvoteRemarkupRule' => 'applications/slowvote/remarkup/SlowvoteRemarkupRule.php', + 'SlowvoteSearchConduitAPIMethod' => 'applications/slowvote/conduit/SlowvoteSearchConduitAPIMethod.php', 'SubscriptionListDialogBuilder' => 'applications/subscriptions/view/SubscriptionListDialogBuilder.php', 'SubscriptionListStringBuilder' => 'applications/subscriptions/view/SubscriptionListStringBuilder.php', 'TokenConduitAPIMethod' => 'applications/tokens/conduit/TokenConduitAPIMethod.php', @@ -6961,6 +7082,7 @@ phutil_register_library_map(array( 'DrydockResource' => array( 'DrydockDAO', 'PhabricatorPolicyInterface', + 'PhabricatorConduitResultInterface', ), 'DrydockResourceActivationFailureLogType' => 'DrydockLogType', 'DrydockResourceActivationYieldLogType' => 'DrydockLogType', @@ -6974,6 +7096,7 @@ phutil_register_library_map(array( 'DrydockResourceQuery' => 'DrydockQuery', 'DrydockResourceReclaimLogType' => 'DrydockLogType', 'DrydockResourceReleaseController' => 'DrydockResourceController', + 'DrydockResourceSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'DrydockResourceSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DrydockResourceStatus' => 'PhabricatorObjectStatus', 'DrydockResourceUpdateWorker' => 'DrydockWorker', @@ -8031,6 +8154,8 @@ phutil_register_library_map(array( 'PhabricatorActionListView' => 'AphrontTagView', 'PhabricatorActionView' => 'AphrontView', 'PhabricatorActivitySettingsPanel' => 'PhabricatorSettingsPanel', + 'PhabricatorAddEmailUserLogType' => 'PhabricatorUserLogType', + 'PhabricatorAddMultifactorUserLogType' => 'PhabricatorUserLogType', 'PhabricatorAdministratorsPolicyRule' => 'PhabricatorPolicyRule', 'PhabricatorAjaxRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler', 'PhabricatorAlmanacApplication' => 'PhabricatorApplication', @@ -8101,8 +8226,10 @@ phutil_register_library_map(array( 'PhabricatorApplicationTransactionCommentView' => 'AphrontView', 'PhabricatorApplicationTransactionController' => 'PhabricatorController', 'PhabricatorApplicationTransactionDetailController' => 'PhabricatorApplicationTransactionController', + 'PhabricatorApplicationTransactionDetailView' => 'AphrontView', 'PhabricatorApplicationTransactionEditor' => 'PhabricatorEditor', 'PhabricatorApplicationTransactionFeedStory' => 'PhabricatorFeedStory', + 'PhabricatorApplicationTransactionJSONDiffDetailView' => 'PhabricatorApplicationTransactionDetailView', 'PhabricatorApplicationTransactionNoEffectException' => 'Exception', 'PhabricatorApplicationTransactionNoEffectResponse' => 'AphrontProxyResponse', 'PhabricatorApplicationTransactionPublishWorker' => 'PhabricatorWorker', @@ -8113,7 +8240,7 @@ phutil_register_library_map(array( 'PhabricatorApplicationTransactionShowOlderController' => 'PhabricatorApplicationTransactionController', 'PhabricatorApplicationTransactionStructureException' => 'Exception', 'PhabricatorApplicationTransactionTemplatedCommentQuery' => 'PhabricatorApplicationTransactionCommentQuery', - 'PhabricatorApplicationTransactionTextDiffDetailView' => 'AphrontView', + 'PhabricatorApplicationTransactionTextDiffDetailView' => 'PhabricatorApplicationTransactionDetailView', 'PhabricatorApplicationTransactionTransactionPHIDType' => 'PhabricatorPHIDType', 'PhabricatorApplicationTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorApplicationTransactionValidationError' => 'Phobject', @@ -8206,6 +8333,9 @@ phutil_register_library_map(array( 'PhabricatorAuthDisableController' => 'PhabricatorAuthProviderConfigController', 'PhabricatorAuthDowngradeSessionController' => 'PhabricatorAuthController', 'PhabricatorAuthEditController' => 'PhabricatorAuthProviderConfigController', + 'PhabricatorAuthEmailLoginAction' => 'PhabricatorSystemAction', + 'PhabricatorAuthEmailLoginMessageType' => 'PhabricatorAuthMessageType', + 'PhabricatorAuthEmailSetPasswordMessageType' => 'PhabricatorAuthMessageType', 'PhabricatorAuthFactor' => 'Phobject', 'PhabricatorAuthFactorConfig' => array( 'PhabricatorAuthDAO', @@ -8404,7 +8534,10 @@ phutil_register_library_map(array( 'PhabricatorAuthTemporaryTokenTypeModule' => 'PhabricatorConfigModule', 'PhabricatorAuthTerminateSessionController' => 'PhabricatorAuthController', 'PhabricatorAuthTestSMSAction' => 'PhabricatorSystemAction', + 'PhabricatorAuthTryEmailLoginAction' => 'PhabricatorSystemAction', 'PhabricatorAuthTryFactorAction' => 'PhabricatorSystemAction', + 'PhabricatorAuthTryPasswordAction' => 'PhabricatorSystemAction', + 'PhabricatorAuthTryPasswordWithoutCAPTCHAAction' => 'PhabricatorSystemAction', 'PhabricatorAuthUnlinkController' => 'PhabricatorAuthController', 'PhabricatorAuthValidateController' => 'PhabricatorAuthController', 'PhabricatorAuthWaitForApprovalMessageType' => 'PhabricatorAuthMessageType', @@ -8691,6 +8824,7 @@ phutil_register_library_map(array( 'PhabricatorCelerityApplication' => 'PhabricatorApplication', 'PhabricatorCelerityTestCase' => 'PhabricatorTestCase', 'PhabricatorChangeParserTestCase' => 'PhabricatorWorkingCopyTestCase', + 'PhabricatorChangePasswordUserLogType' => 'PhabricatorUserLogType', 'PhabricatorChangesetCachePurger' => 'PhabricatorCachePurger', 'PhabricatorChangesetResponse' => 'AphrontProxyResponse', 'PhabricatorChartAxis' => 'Phobject', @@ -8749,7 +8883,9 @@ phutil_register_library_map(array( 'PhabricatorConduitAPIController' => 'PhabricatorConduitController', 'PhabricatorConduitApplication' => 'PhabricatorApplication', 'PhabricatorConduitCallManagementWorkflow' => 'PhabricatorConduitManagementWorkflow', + 'PhabricatorConduitCertificateFailureUserLogType' => 'PhabricatorUserLogType', 'PhabricatorConduitCertificateToken' => 'PhabricatorConduitDAO', + 'PhabricatorConduitCertificateUserLogType' => 'PhabricatorUserLogType', 'PhabricatorConduitConsoleController' => 'PhabricatorConduitController', 'PhabricatorConduitContentSource' => 'PhabricatorContentSource', 'PhabricatorConduitController' => 'PhabricatorController', @@ -9239,18 +9375,28 @@ phutil_register_library_map(array( 'PhabricatorEditEngineConfigurationSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorEditEngineConfigurationSortController' => 'PhabricatorEditEngineController', 'PhabricatorEditEngineConfigurationSubtypeController' => 'PhabricatorEditEngineController', - 'PhabricatorEditEngineConfigurationTransaction' => 'PhabricatorApplicationTransaction', + 'PhabricatorEditEngineConfigurationTransaction' => 'PhabricatorModularTransaction', 'PhabricatorEditEngineConfigurationTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorEditEngineConfigurationViewController' => 'PhabricatorEditEngineController', 'PhabricatorEditEngineController' => 'PhabricatorApplicationTransactionController', + 'PhabricatorEditEngineCreateOrderTransaction' => 'PhabricatorEditEngineTransactionType', 'PhabricatorEditEngineDatasource' => 'PhabricatorTypeaheadDatasource', + 'PhabricatorEditEngineDefaultCreateTransaction' => 'PhabricatorEditEngineTransactionType', 'PhabricatorEditEngineDefaultLock' => 'PhabricatorEditEngineLock', + 'PhabricatorEditEngineDefaultTransaction' => 'PhabricatorEditEngineTransactionType', + 'PhabricatorEditEngineDisableTransaction' => 'PhabricatorEditEngineTransactionType', + 'PhabricatorEditEngineEditOrderTransaction' => 'PhabricatorEditEngineTransactionType', 'PhabricatorEditEngineExtension' => 'Phobject', 'PhabricatorEditEngineExtensionModule' => 'PhabricatorConfigModule', + 'PhabricatorEditEngineIsEditTransaction' => 'PhabricatorEditEngineTransactionType', 'PhabricatorEditEngineListController' => 'PhabricatorEditEngineController', 'PhabricatorEditEngineLock' => 'Phobject', + 'PhabricatorEditEngineLocksTransaction' => 'PhabricatorEditEngineTransactionType', 'PhabricatorEditEngineMFAEngine' => 'Phobject', + 'PhabricatorEditEngineNameTransaction' => 'PhabricatorEditEngineTransactionType', + 'PhabricatorEditEngineOrderTransaction' => 'PhabricatorEditEngineTransactionType', 'PhabricatorEditEnginePointsCommentAction' => 'PhabricatorEditEngineCommentAction', + 'PhabricatorEditEnginePreambleTransaction' => 'PhabricatorEditEngineTransactionType', 'PhabricatorEditEngineProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorEditEngineQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorEditEngineSearchEngine' => 'PhabricatorApplicationSearchEngine', @@ -9260,7 +9406,9 @@ phutil_register_library_map(array( 'PhabricatorEditEngineSubtype' => 'Phobject', 'PhabricatorEditEngineSubtypeMap' => 'Phobject', 'PhabricatorEditEngineSubtypeTestCase' => 'PhabricatorTestCase', + 'PhabricatorEditEngineSubtypeTransaction' => 'PhabricatorEditEngineTransactionType', 'PhabricatorEditEngineTokenizerCommentAction' => 'PhabricatorEditEngineCommentAction', + 'PhabricatorEditEngineTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorEditField' => 'Phobject', 'PhabricatorEditPage' => 'Phobject', 'PhabricatorEditType' => 'Phobject', @@ -9279,6 +9427,7 @@ phutil_register_library_map(array( 'PhabricatorEmailFormatSetting' => 'PhabricatorSelectSetting', 'PhabricatorEmailFormatSettingsPanel' => 'PhabricatorEditEngineSettingsPanel', 'PhabricatorEmailLoginController' => 'PhabricatorAuthController', + 'PhabricatorEmailLoginUserLogType' => 'PhabricatorUserLogType', 'PhabricatorEmailNotificationsSetting' => 'PhabricatorSelectSetting', 'PhabricatorEmailPreferencesSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorEmailRePrefixSetting' => 'PhabricatorSelectSetting', @@ -9292,6 +9441,7 @@ phutil_register_library_map(array( 'PhabricatorEmojiRemarkupRule' => 'PhutilRemarkupRule', 'PhabricatorEmojiTranslation' => 'PhutilTranslation', 'PhabricatorEmptyQueryException' => 'Exception', + 'PhabricatorEnterHisecUserLogType' => 'PhabricatorUserLogType', 'PhabricatorEnumConfigType' => 'PhabricatorTextConfigType', 'PhabricatorEnv' => 'Phobject', 'PhabricatorEnvTestCase' => 'PhabricatorTestCase', @@ -9304,6 +9454,7 @@ phutil_register_library_map(array( 'PhabricatorExampleEventListener' => 'PhabricatorEventListener', 'PhabricatorExcelExportFormat' => 'PhabricatorExportFormat', 'PhabricatorExecFutureFileUploadSource' => 'PhabricatorFileUploadSource', + 'PhabricatorExitHisecUserLogType' => 'PhabricatorUserLogType', 'PhabricatorExportEngine' => 'Phobject', 'PhabricatorExportEngineBulkJobType' => 'PhabricatorWorkerSingleBulkJobType', 'PhabricatorExportEngineExtension' => 'Phobject', @@ -9349,6 +9500,7 @@ phutil_register_library_map(array( 'PhabricatorFactObjectDimension' => 'PhabricatorFactDimension', 'PhabricatorFactRaw' => 'PhabricatorFactDAO', 'PhabricatorFactUpdateIterator' => 'PhutilBufferedIterator', + 'PhabricatorFailHisecUserLogType' => 'PhabricatorUserLogType', 'PhabricatorFaviconRef' => 'Phobject', 'PhabricatorFaviconRefQuery' => 'Phobject', 'PhabricatorFavoritesApplication' => 'PhabricatorApplication', @@ -9520,6 +9672,7 @@ phutil_register_library_map(array( 'PhabricatorFlaggableInterface' => 'PhabricatorPHIDInterface', 'PhabricatorFlagsApplication' => 'PhabricatorApplication', 'PhabricatorFlagsUIEventListener' => 'PhabricatorEventListener', + 'PhabricatorFullLoginUserLogType' => 'PhabricatorUserLogType', 'PhabricatorFulltextEngine' => 'Phobject', 'PhabricatorFulltextEngineExtension' => 'Phobject', 'PhabricatorFulltextEngineExtensionModule' => 'PhabricatorConfigModule', @@ -9665,7 +9818,10 @@ phutil_register_library_map(array( 'PhabricatorLockLogManagementWorkflow' => 'PhabricatorLockManagementWorkflow', 'PhabricatorLockManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorLogTriggerAction' => 'PhabricatorTriggerAction', + 'PhabricatorLoginFailureUserLogType' => 'PhabricatorUserLogType', + 'PhabricatorLoginUserLogType' => 'PhabricatorUserLogType', 'PhabricatorLogoutController' => 'PhabricatorAuthController', + 'PhabricatorLogoutUserLogType' => 'PhabricatorUserLogType', 'PhabricatorLunarPhasePolicyRule' => 'PhabricatorPolicyRule', 'PhabricatorMacroApplication' => 'PhabricatorApplication', 'PhabricatorMacroAudioBehaviorTransaction' => 'PhabricatorMacroTransactionType', @@ -10026,7 +10182,24 @@ phutil_register_library_map(array( 'PhabricatorOwnersPathsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', 'PhabricatorOwnersSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorOwnersSearchField' => 'PhabricatorSearchTokenizerField', + 'PhabricatorPDFCatalogObject' => 'PhabricatorPDFObject', + 'PhabricatorPDFContentsObject' => 'PhabricatorPDFObject', 'PhabricatorPDFDocumentEngine' => 'PhabricatorDocumentEngine', + 'PhabricatorPDFFontObject' => 'PhabricatorPDFObject', + 'PhabricatorPDFFragment' => 'Phobject', + 'PhabricatorPDFFragmentOffset' => 'Phobject', + 'PhabricatorPDFGenerator' => 'Phobject', + 'PhabricatorPDFHeadFragment' => 'PhabricatorPDFFragment', + 'PhabricatorPDFInfoObject' => 'PhabricatorPDFObject', + 'PhabricatorPDFIterator' => array( + 'Phobject', + 'Iterator', + ), + 'PhabricatorPDFObject' => 'PhabricatorPDFFragment', + 'PhabricatorPDFPageObject' => 'PhabricatorPDFObject', + 'PhabricatorPDFPagesObject' => 'PhabricatorPDFObject', + 'PhabricatorPDFResourcesObject' => 'PhabricatorPDFObject', + 'PhabricatorPDFTailFragment' => 'PhabricatorPDFFragment', 'PhabricatorPHDConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPHID' => 'Phobject', 'PhabricatorPHIDConstants' => 'Phobject', @@ -10145,6 +10318,7 @@ phutil_register_library_map(array( 'PhabricatorPackagesVersionViewController' => 'PhabricatorPackagesVersionController', 'PhabricatorPackagesView' => 'AphrontView', 'PhabricatorPagerUIExample' => 'PhabricatorUIExample', + 'PhabricatorPartialLoginUserLogType' => 'PhabricatorUserLogType', 'PhabricatorPassphraseApplication' => 'PhabricatorApplication', 'PhabricatorPasswordAuthProvider' => 'PhabricatorAuthProvider', 'PhabricatorPasswordDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension', @@ -10207,6 +10381,7 @@ phutil_register_library_map(array( 'PhabricatorPeopleDeleteController' => 'PhabricatorPeopleController', 'PhabricatorPeopleDetailsProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorPeopleDisableController' => 'PhabricatorPeopleController', + 'PhabricatorPeopleEmailLoginMailEngine' => 'PhabricatorPeopleMailEngine', 'PhabricatorPeopleEmpowerController' => 'PhabricatorPeopleController', 'PhabricatorPeopleExternalPHIDType' => 'PhabricatorPHIDType', 'PhabricatorPeopleIconSet' => 'PhabricatorIconSet', @@ -10216,10 +10391,13 @@ phutil_register_library_map(array( 'PhabricatorPeopleListController' => 'PhabricatorPeopleController', 'PhabricatorPeopleLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorPeopleLogSearchEngine' => 'PhabricatorApplicationSearchEngine', + 'PhabricatorPeopleLogViewController' => 'PhabricatorPeopleController', 'PhabricatorPeopleLogsController' => 'PhabricatorPeopleController', 'PhabricatorPeopleMailEngine' => 'Phobject', 'PhabricatorPeopleMailEngineException' => 'Exception', 'PhabricatorPeopleManageProfileMenuItem' => 'PhabricatorProfileMenuItem', + 'PhabricatorPeopleManagementEmpowerWorkflow' => 'PhabricatorPeopleManagementWorkflow', + 'PhabricatorPeopleManagementEnableWorkflow' => 'PhabricatorPeopleManagementWorkflow', 'PhabricatorPeopleManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorPeopleNewController' => 'PhabricatorPeopleController', 'PhabricatorPeopleNoOwnerDatasource' => 'PhabricatorTypeaheadDatasource', @@ -10229,7 +10407,6 @@ phutil_register_library_map(array( 'PhabricatorPeopleProfileCommitsController' => 'PhabricatorPeopleProfileController', 'PhabricatorPeopleProfileController' => 'PhabricatorPeopleController', 'PhabricatorPeopleProfileEditController' => 'PhabricatorPeopleProfileController', - 'PhabricatorPeopleProfileImageWorkflow' => 'PhabricatorPeopleManagementWorkflow', 'PhabricatorPeopleProfileManageController' => 'PhabricatorPeopleProfileController', 'PhabricatorPeopleProfileMenuEngine' => 'PhabricatorProfileMenuEngine', 'PhabricatorPeopleProfilePictureController' => 'PhabricatorPeopleProfileController', @@ -10364,6 +10541,7 @@ phutil_register_library_map(array( ), 'PhabricatorPolicyType' => 'PhabricatorPolicyConstants', 'PhabricatorPonderApplication' => 'PhabricatorApplication', + 'PhabricatorPrimaryEmailUserLogType' => 'PhabricatorUserLogType', 'PhabricatorProfileMenuEditEngine' => 'PhabricatorEditEngine', 'PhabricatorProfileMenuEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorProfileMenuEngine' => 'Phobject', @@ -10404,9 +10582,12 @@ phutil_register_library_map(array( 'PhabricatorProjectArchiveController' => 'PhabricatorProjectController', 'PhabricatorProjectBoardBackgroundController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardController' => 'PhabricatorProjectController', + 'PhabricatorProjectBoardDefaultController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardDisableController' => 'PhabricatorProjectBoardController', + 'PhabricatorProjectBoardFilterController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardImportController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardManageController' => 'PhabricatorProjectBoardController', + 'PhabricatorProjectBoardReloadController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardReorderController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardViewController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBuiltinsExample' => 'PhabricatorUIExample', @@ -10423,6 +10604,8 @@ phutil_register_library_map(array( 'PhabricatorConduitResultInterface', ), 'PhabricatorProjectColumnAuthorOrder' => 'PhabricatorProjectColumnOrder', + 'PhabricatorProjectColumnBulkEditController' => 'PhabricatorProjectBoardController', + 'PhabricatorProjectColumnBulkMoveController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectColumnCreatedOrder' => 'PhabricatorProjectColumnOrder', 'PhabricatorProjectColumnDetailController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectColumnEditController' => 'PhabricatorProjectBoardController', @@ -10452,6 +10635,7 @@ phutil_register_library_map(array( 'PhabricatorProjectColumnTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorProjectColumnTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorProjectColumnTriggerTransaction' => 'PhabricatorProjectColumnTransactionType', + 'PhabricatorProjectColumnViewQueryController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorProjectConfiguredCustomField' => array( 'PhabricatorProjectStandardCustomField', @@ -10466,7 +10650,6 @@ phutil_register_library_map(array( 'PhabricatorProjectCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage', 'PhabricatorProjectDAO' => 'PhabricatorLiskDAO', 'PhabricatorProjectDatasource' => 'PhabricatorTypeaheadDatasource', - 'PhabricatorProjectDefaultController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectDescriptionField' => 'PhabricatorProjectStandardCustomField', 'PhabricatorProjectDetailsProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorProjectDropEffect' => 'Phobject', @@ -10626,6 +10809,7 @@ phutil_register_library_map(array( 'Iterator', ), 'PhabricatorRateLimitRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler', + 'PhabricatorReassignEmailUserLogType' => 'PhabricatorUserLogType', 'PhabricatorRebuildIndexesWorker' => 'PhabricatorWorker', 'PhabricatorRecaptchaConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorRedirectController' => 'PhabricatorController', @@ -10644,6 +10828,8 @@ phutil_register_library_map(array( 'PhabricatorRemarkupFigletBlockInterpreter' => 'PhutilRemarkupBlockInterpreter', 'PhabricatorRemarkupHyperlinkEngineExtension' => 'PhutilRemarkupHyperlinkEngineExtension', 'PhabricatorRemarkupUIExample' => 'PhabricatorUIExample', + 'PhabricatorRemoveEmailUserLogType' => 'PhabricatorUserLogType', + 'PhabricatorRemoveMultifactorUserLogType' => 'PhabricatorUserLogType', 'PhabricatorRepositoriesSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorRepository' => array( 'PhabricatorRepositoryDAO', @@ -10738,6 +10924,7 @@ phutil_register_library_map(array( 'PhabricatorRepositoryIdentityTransaction' => 'PhabricatorModularTransaction', 'PhabricatorRepositoryIdentityTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorRepositoryIdentityTransactionType' => 'PhabricatorModularTransactionType', + 'PhabricatorRepositoryMaintenanceTransaction' => 'PhabricatorRepositoryTransactionType', 'PhabricatorRepositoryManagementCacheWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementClusterizeWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementDiscoverWorkflow' => 'PhabricatorRepositoryManagementWorkflow', @@ -10746,6 +10933,7 @@ phutil_register_library_map(array( 'PhabricatorRepositoryManagementListPathsWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementListWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementLookupUsersWorkflow' => 'PhabricatorRepositoryManagementWorkflow', + 'PhabricatorRepositoryManagementMaintenanceWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementMarkImportedWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementMarkReachableWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementMirrorWorkflow' => 'PhabricatorRepositoryManagementWorkflow', @@ -10851,6 +11039,7 @@ phutil_register_library_map(array( 'PhabricatorRepositoryVCSTransaction' => 'PhabricatorRepositoryTransactionType', 'PhabricatorRepositoryWorkingCopyVersion' => 'PhabricatorRepositoryDAO', 'PhabricatorRequestExceptionHandler' => 'AphrontRequestExceptionHandler', + 'PhabricatorResetPasswordUserLogType' => 'PhabricatorUserLogType', 'PhabricatorResourceSite' => 'PhabricatorSite', 'PhabricatorRobotsController' => 'PhabricatorController', 'PhabricatorS3FileStorageEngine' => 'PhabricatorFileStorageEngine', @@ -10960,6 +11149,7 @@ phutil_register_library_map(array( 'PhabricatorShiftChartFunction' => 'PhabricatorChartFunction', 'PhabricatorShortSite' => 'PhabricatorSite', 'PhabricatorShowFiletreeSetting' => 'PhabricatorSelectSetting', + 'PhabricatorSignDocumentsUserLogType' => 'PhabricatorUserLogType', 'PhabricatorSimpleEditType' => 'PhabricatorEditType', 'PhabricatorSinChartFunction' => 'PhabricatorChartFunction', 'PhabricatorSite' => 'AphrontSite', @@ -10988,6 +11178,7 @@ phutil_register_library_map(array( 'PhabricatorProjectInterface', 'PhabricatorDestructibleInterface', 'PhabricatorSpacesInterface', + 'PhabricatorConduitResultInterface', ), 'PhabricatorSlowvotePollController' => 'PhabricatorSlowvoteController', 'PhabricatorSlowvotePollPHIDType' => 'PhabricatorPHIDType', @@ -11294,6 +11485,8 @@ phutil_register_library_map(array( 'PhabricatorUserDAO', 'PhabricatorPolicyInterface', ), + 'PhabricatorUserLogType' => 'Phobject', + 'PhabricatorUserLogTypeDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorUserLogView' => 'AphrontView', 'PhabricatorUserMessageCountCacheType' => 'PhabricatorUserCacheType', 'PhabricatorUserNotificationCountCacheType' => 'PhabricatorUserCacheType', @@ -11329,6 +11522,7 @@ phutil_register_library_map(array( 'PhabricatorUsersPolicyRule' => 'PhabricatorPolicyRule', 'PhabricatorUsersSearchField' => 'PhabricatorSearchTokenizerField', 'PhabricatorVCSResponse' => 'AphrontResponse', + 'PhabricatorVerifyEmailUserLogType' => 'PhabricatorUserLogType', 'PhabricatorVersionedDraft' => 'PhabricatorDraftDAO', 'PhabricatorVeryWowEnglishTranslation' => 'PhutilTranslation', 'PhabricatorVideoDocumentEngine' => 'PhabricatorDocumentEngine', @@ -11340,6 +11534,7 @@ phutil_register_library_map(array( 'PhabricatorWeekStartDaySetting' => 'PhabricatorSelectSetting', 'PhabricatorWildConfigType' => 'PhabricatorJSONConfigType', 'PhabricatorWordPressAuthProvider' => 'PhabricatorOAuth2AuthProvider', + 'PhabricatorWorkboardViewState' => 'Phobject', 'PhabricatorWorker' => 'Phobject', 'PhabricatorWorkerActiveTask' => 'PhabricatorWorkerTask', 'PhabricatorWorkerActiveTaskQuery' => 'PhabricatorWorkerTaskQuery', @@ -11421,6 +11616,7 @@ phutil_register_library_map(array( 'PhabricatorXHProfSampleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorXHProfSampleSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorYoutubeRemarkupRule' => 'PhutilRemarkupRule', + 'PhabricatorZipSetupCheck' => 'PhabricatorSetupCheck', 'Phame404Response' => 'AphrontHTMLResponse', 'PhameBlog' => array( 'PhameDAO', @@ -11621,27 +11817,57 @@ phutil_register_library_map(array( 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', ), - 'PhortuneAccountAddManagerController' => 'PhortuneController', + 'PhortuneAccountAddManagerController' => 'PhortuneAccountController', 'PhortuneAccountBillingAddressTransaction' => 'PhortuneAccountTransactionType', - 'PhortuneAccountBillingController' => 'PhortuneAccountProfileController', 'PhortuneAccountBillingNameTransaction' => 'PhortuneAccountTransactionType', - 'PhortuneAccountChargeListController' => 'PhortuneController', + 'PhortuneAccountChargeListController' => 'PhortuneAccountProfileController', + 'PhortuneAccountChargesController' => 'PhortuneAccountProfileController', 'PhortuneAccountController' => 'PhortuneController', + 'PhortuneAccountDetailsController' => 'PhortuneAccountProfileController', 'PhortuneAccountEditController' => 'PhortuneController', 'PhortuneAccountEditEngine' => 'PhabricatorEditEngine', 'PhortuneAccountEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhortuneAccountEmail' => array( + 'PhortuneDAO', + 'PhabricatorApplicationTransactionInterface', + 'PhabricatorPolicyInterface', + 'PhabricatorExtendedPolicyInterface', + ), + 'PhortuneAccountEmailAddressTransaction' => 'PhortuneAccountEmailTransactionType', + 'PhortuneAccountEmailAddressesController' => 'PhortuneAccountProfileController', + 'PhortuneAccountEmailEditController' => 'PhortuneAccountController', + 'PhortuneAccountEmailEditEngine' => 'PhabricatorEditEngine', + 'PhortuneAccountEmailEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhortuneAccountEmailPHIDType' => 'PhabricatorPHIDType', + 'PhortuneAccountEmailQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhortuneAccountEmailRotateController' => 'PhortuneAccountController', + 'PhortuneAccountEmailRotateTransaction' => 'PhortuneAccountEmailTransactionType', + 'PhortuneAccountEmailStatus' => 'Phobject', + 'PhortuneAccountEmailStatusController' => 'PhortuneAccountController', + 'PhortuneAccountEmailStatusTransaction' => 'PhortuneAccountEmailTransactionType', + 'PhortuneAccountEmailTransaction' => 'PhabricatorModularTransaction', + 'PhortuneAccountEmailTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhortuneAccountEmailTransactionType' => 'PhabricatorModularTransactionType', + 'PhortuneAccountEmailViewController' => 'PhortuneAccountController', 'PhortuneAccountHasMemberEdgeType' => 'PhabricatorEdgeType', + 'PhortuneAccountHasMerchantEdgeType' => 'PhabricatorEdgeType', 'PhortuneAccountListController' => 'PhortuneController', - 'PhortuneAccountManagerController' => 'PhortuneAccountProfileController', + 'PhortuneAccountManagersController' => 'PhortuneAccountProfileController', 'PhortuneAccountNameTransaction' => 'PhortuneAccountTransactionType', + 'PhortuneAccountOrderListController' => 'PhortuneAccountProfileController', + 'PhortuneAccountOrdersController' => 'PhortuneAccountProfileController', + 'PhortuneAccountOverviewController' => 'PhortuneAccountProfileController', 'PhortuneAccountPHIDType' => 'PhabricatorPHIDType', + 'PhortuneAccountPaymentMethodController' => 'PhortuneAccountProfileController', + 'PhortuneAccountPaymentMethodViewController' => 'PhortuneAccountController', 'PhortuneAccountProfileController' => 'PhortuneAccountController', 'PhortuneAccountQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhortuneAccountSubscriptionAutopayController' => 'PhortuneAccountController', 'PhortuneAccountSubscriptionController' => 'PhortuneAccountProfileController', + 'PhortuneAccountSubscriptionViewController' => 'PhortuneAccountController', 'PhortuneAccountTransaction' => 'PhabricatorModularTransaction', 'PhortuneAccountTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhortuneAccountTransactionType' => 'PhabricatorModularTransactionType', - 'PhortuneAccountViewController' => 'PhortuneAccountProfileController', 'PhortuneAdHocCart' => 'PhortuneCartImplementation', 'PhortuneAdHocProduct' => 'PhortuneProductImplementation', 'PhortuneAddPaymentMethodAction' => 'PhabricatorSystemAction', @@ -11649,6 +11875,7 @@ phutil_register_library_map(array( 'PhortuneDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', + 'PhabricatorExtendedPolicyInterface', ), 'PhortuneCartAcceptController' => 'PhortuneCartController', 'PhortuneCartCancelController' => 'PhortuneCartController', @@ -11656,7 +11883,6 @@ phutil_register_library_map(array( 'PhortuneCartController' => 'PhortuneController', 'PhortuneCartEditor' => 'PhabricatorApplicationTransactionEditor', 'PhortuneCartImplementation' => 'Phobject', - 'PhortuneCartListController' => 'PhortuneController', 'PhortuneCartPHIDType' => 'PhabricatorPHIDType', 'PhortuneCartQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhortuneCartReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', @@ -11665,9 +11891,11 @@ phutil_register_library_map(array( 'PhortuneCartTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhortuneCartUpdateController' => 'PhortuneCartController', 'PhortuneCartViewController' => 'PhortuneCartController', + 'PhortuneCartVoidController' => 'PhortuneCartController', 'PhortuneCharge' => array( 'PhortuneDAO', 'PhabricatorPolicyInterface', + 'PhabricatorExtendedPolicyInterface', ), 'PhortuneChargePHIDType' => 'PhabricatorPHIDType', 'PhortuneChargeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', @@ -11682,7 +11910,10 @@ phutil_register_library_map(array( 'PhortuneDAO' => 'PhabricatorLiskDAO', 'PhortuneDisplayException' => 'Exception', 'PhortuneErrCode' => 'PhortuneConstants', - 'PhortuneInvoiceView' => 'AphrontTagView', + 'PhortuneExternalController' => 'PhortuneController', + 'PhortuneExternalOrderController' => 'PhortuneExternalController', + 'PhortuneExternalOverviewController' => 'PhortuneExternalController', + 'PhortuneExternalUnsubscribeController' => 'PhortuneExternalController', 'PhortuneLandingController' => 'PhortuneController', 'PhortuneMemberHasAccountEdgeType' => 'PhabricatorEdgeType', 'PhortuneMemberHasMerchantEdgeType' => 'PhabricatorEdgeType', @@ -11691,43 +11922,67 @@ phutil_register_library_map(array( 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', ), - 'PhortuneMerchantAddManagerController' => 'PhortuneController', + 'PhortuneMerchantAddManagerController' => 'PhortuneMerchantController', 'PhortuneMerchantCapability' => 'PhabricatorPolicyCapability', 'PhortuneMerchantContactInfoTransaction' => 'PhortuneMerchantTransactionType', 'PhortuneMerchantController' => 'PhortuneController', 'PhortuneMerchantDescriptionTransaction' => 'PhortuneMerchantTransactionType', - 'PhortuneMerchantEditController' => 'PhortuneMerchantController', + 'PhortuneMerchantDetailsController' => 'PhortuneMerchantProfileController', + 'PhortuneMerchantEditController' => 'PhortuneController', 'PhortuneMerchantEditEngine' => 'PhabricatorEditEngine', 'PhortuneMerchantEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhortuneMerchantHasAccountEdgeType' => 'PhabricatorEdgeType', 'PhortuneMerchantHasMemberEdgeType' => 'PhabricatorEdgeType', - 'PhortuneMerchantInvoiceCreateController' => 'PhortuneMerchantProfileController', + 'PhortuneMerchantInvoiceCreateController' => 'PhortuneMerchantController', 'PhortuneMerchantInvoiceEmailTransaction' => 'PhortuneMerchantTransactionType', 'PhortuneMerchantInvoiceFooterTransaction' => 'PhortuneMerchantTransactionType', - 'PhortuneMerchantListController' => 'PhortuneMerchantController', - 'PhortuneMerchantManagerController' => 'PhortuneMerchantProfileController', + 'PhortuneMerchantListController' => 'PhortuneController', + 'PhortuneMerchantManagersController' => 'PhortuneMerchantProfileController', 'PhortuneMerchantNameTransaction' => 'PhortuneMerchantTransactionType', + 'PhortuneMerchantOrderListController' => 'PhortuneMerchantProfileController', + 'PhortuneMerchantOrdersController' => 'PhortuneMerchantProfileController', + 'PhortuneMerchantOverviewController' => 'PhortuneMerchantProfileController', 'PhortuneMerchantPHIDType' => 'PhabricatorPHIDType', - 'PhortuneMerchantPictureController' => 'PhortuneMerchantProfileController', + 'PhortuneMerchantPictureController' => 'PhortuneMerchantController', 'PhortuneMerchantPictureTransaction' => 'PhortuneMerchantTransactionType', - 'PhortuneMerchantProfileController' => 'PhortuneController', + 'PhortuneMerchantProfileController' => 'PhortuneMerchantController', + 'PhortuneMerchantProviderDisableController' => 'PhortuneMerchantController', + 'PhortuneMerchantProviderEditController' => 'PhortuneMerchantController', + 'PhortuneMerchantProviderViewController' => 'PhortuneMerchantController', + 'PhortuneMerchantProvidersController' => 'PhortuneMerchantProfileController', 'PhortuneMerchantQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhortuneMerchantSearchEngine' => 'PhabricatorApplicationSearchEngine', + 'PhortuneMerchantSubscriptionListController' => 'PhortuneMerchantProfileController', + 'PhortuneMerchantSubscriptionsController' => 'PhortuneMerchantProfileController', 'PhortuneMerchantTransaction' => 'PhabricatorModularTransaction', 'PhortuneMerchantTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhortuneMerchantTransactionType' => 'PhabricatorModularTransactionType', - 'PhortuneMerchantViewController' => 'PhortuneMerchantProfileController', 'PhortuneMonthYearExpiryControl' => 'AphrontFormControl', + 'PhortuneOrderDescriptionView' => 'AphrontView', + 'PhortuneOrderItemsView' => 'PhortuneOrderView', + 'PhortuneOrderSummaryView' => 'PhortuneOrderView', 'PhortuneOrderTableView' => 'AphrontView', + 'PhortuneOrderView' => 'AphrontView', 'PhortunePayPalPaymentProvider' => 'PhortunePaymentProvider', 'PhortunePaymentMethod' => array( 'PhortuneDAO', 'PhabricatorPolicyInterface', + 'PhabricatorExtendedPolicyInterface', + 'PhabricatorPolicyCodexInterface', + 'PhabricatorApplicationTransactionInterface', ), 'PhortunePaymentMethodCreateController' => 'PhortuneController', 'PhortunePaymentMethodDisableController' => 'PhortuneController', 'PhortunePaymentMethodEditController' => 'PhortuneController', + 'PhortunePaymentMethodEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhortunePaymentMethodNameTransaction' => 'PhortunePaymentMethodTransactionType', 'PhortunePaymentMethodPHIDType' => 'PhabricatorPHIDType', + 'PhortunePaymentMethodPolicyCodex' => 'PhabricatorPolicyCodex', 'PhortunePaymentMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhortunePaymentMethodStatusTransaction' => 'PhortunePaymentMethodTransactionType', + 'PhortunePaymentMethodTransaction' => 'PhabricatorModularTransaction', + 'PhortunePaymentMethodTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhortunePaymentMethodTransactionType' => 'PhabricatorModularTransactionType', 'PhortunePaymentProvider' => 'Phobject', 'PhortunePaymentProviderConfig' => array( 'PhortuneDAO', @@ -11750,8 +12005,6 @@ phutil_register_library_map(array( 'PhortuneProductQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhortuneProductViewController' => 'PhortuneController', 'PhortuneProviderActionController' => 'PhortuneController', - 'PhortuneProviderDisableController' => 'PhortuneMerchantController', - 'PhortuneProviderEditController' => 'PhortuneMerchantController', 'PhortunePurchase' => array( 'PhortuneDAO', 'PhabricatorPolicyInterface', @@ -11763,17 +12016,23 @@ phutil_register_library_map(array( 'PhortuneSubscription' => array( 'PhortuneDAO', 'PhabricatorPolicyInterface', + 'PhabricatorExtendedPolicyInterface', + 'PhabricatorPolicyCodexInterface', + 'PhabricatorApplicationTransactionInterface', ), + 'PhortuneSubscriptionAutopayTransaction' => 'PhortuneSubscriptionTransactionType', 'PhortuneSubscriptionCart' => 'PhortuneCartImplementation', - 'PhortuneSubscriptionEditController' => 'PhortuneController', + 'PhortuneSubscriptionEditor' => 'PhabricatorApplicationTransactionEditor', 'PhortuneSubscriptionImplementation' => 'Phobject', - 'PhortuneSubscriptionListController' => 'PhortuneController', 'PhortuneSubscriptionPHIDType' => 'PhabricatorPHIDType', + 'PhortuneSubscriptionPolicyCodex' => 'PhabricatorPolicyCodex', 'PhortuneSubscriptionProduct' => 'PhortuneProductImplementation', 'PhortuneSubscriptionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhortuneSubscriptionSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhortuneSubscriptionTableView' => 'AphrontView', - 'PhortuneSubscriptionViewController' => 'PhortuneController', + 'PhortuneSubscriptionTransaction' => 'PhabricatorModularTransaction', + 'PhortuneSubscriptionTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhortuneSubscriptionTransactionType' => 'PhabricatorModularTransactionType', 'PhortuneSubscriptionWorker' => 'PhabricatorWorker', 'PhortuneTestPaymentProvider' => 'PhortunePaymentProvider', 'PhragmentBrowseController' => 'PhragmentController', @@ -12131,6 +12390,7 @@ phutil_register_library_map(array( 'SlowvoteEmbedView' => 'AphrontView', 'SlowvoteInfoConduitAPIMethod' => 'SlowvoteConduitAPIMethod', 'SlowvoteRemarkupRule' => 'PhabricatorObjectRemarkupRule', + 'SlowvoteSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'SubscriptionListDialogBuilder' => 'Phobject', 'SubscriptionListStringBuilder' => 'Phobject', 'TokenConduitAPIMethod' => 'ConduitAPIMethod', diff --git a/src/aphront/configuration/AphrontApplicationConfiguration.php b/src/aphront/configuration/AphrontApplicationConfiguration.php index a479209125..c24d59ac90 100644 --- a/src/aphront/configuration/AphrontApplicationConfiguration.php +++ b/src/aphront/configuration/AphrontApplicationConfiguration.php @@ -312,11 +312,17 @@ final class AphrontApplicationConfiguration if ($response_exception) { // If we encountered an exception while building a normal response, then // encountered another exception while building a response for the first - // exception, just throw the original exception. It is more likely to be - // useful and point at a root cause than the second exception we ran into - // while telling the user about it. + // exception, throw an aggregate exception that will be unpacked by the + // higher-level handler. This is above our pay grade. if ($original_exception) { - throw $original_exception; + throw new PhutilAggregateException( + pht( + 'Encountered a processing exception, then another exception when '. + 'trying to build a response for the first exception.'), + array( + $response_exception, + $original_exception, + )); } // If we built a response successfully and then ran into an exception diff --git a/src/aphront/response/Aphront404Response.php b/src/aphront/response/Aphront404Response.php index 1284cb62e6..ea98e9102b 100644 --- a/src/aphront/response/Aphront404Response.php +++ b/src/aphront/response/Aphront404Response.php @@ -8,16 +8,19 @@ final class Aphront404Response extends AphrontHTMLResponse { public function buildResponseString() { $request = $this->getRequest(); - $user = $request->getUser(); + $viewer = $request->getViewer(); $dialog = id(new AphrontDialogView()) - ->setUser($user) + ->setViewer($viewer) ->setTitle(pht('404 Not Found')) - ->addCancelButton('/', pht('Focus')) + ->addCancelButton('/', pht('Return to Charted Waters')) ->appendParagraph( pht( - 'Do not dwell in the past, do not dream of the future, '. - 'concentrate the mind on the present moment.')); + 'You arrive at your destination, but there is nothing here.')) + ->appendParagraph( + pht( + 'Perhaps the real treasure was the friends you made '. + 'along the way.')); $view = id(new PhabricatorStandardPageView()) ->setTitle(pht('404 Not Found')) diff --git a/src/aphront/response/AphrontUnhandledExceptionResponse.php b/src/aphront/response/AphrontUnhandledExceptionResponse.php index 32d612ca50..2c605cf150 100644 --- a/src/aphront/response/AphrontUnhandledExceptionResponse.php +++ b/src/aphront/response/AphrontUnhandledExceptionResponse.php @@ -61,9 +61,39 @@ final class AphrontUnhandledExceptionResponse return 'unhandled-exception'; } - protected function getResponseBody() { - $ex = $this->exception; + private function getExceptionList() { + return $this->expandException($this->exception); + } + private function expandException($root) { + if ($root instanceof PhutilAggregateException) { + $list = array(); + + $list[] = $root; + + foreach ($root->getExceptions() as $ex) { + foreach ($this->expandException($ex) as $child) { + $list[] = $child; + } + } + + return $list; + } + + return array($root); + } + + protected function getResponseBody() { + $body = array(); + + foreach ($this->getExceptionList() as $ex) { + $body[] = $this->newHTMLMessage($ex); + } + + return $body; + } + + private function newHTMLMessage($ex) { if ($ex instanceof AphrontMalformedRequestException) { $title = $ex->getTitle(); } else { @@ -122,12 +152,20 @@ final class AphrontUnhandledExceptionResponse } protected function buildPlainTextResponseString() { - $ex = $this->exception; + $messages = array(); + foreach ($this->getExceptionList() as $exception) { + $messages[] = $this->newPlainTextMessage($exception); + } + + return implode("\n\n", $messages); + } + + private function newPlainTextMessage($exception) { return pht( '%s: %s', - get_class($ex), - $ex->getMessage()); + get_class($exception), + $exception->getMessage()); } } diff --git a/src/applications/auth/action/PhabricatorAuthChangePasswordAction.php b/src/applications/auth/action/PhabricatorAuthChangePasswordAction.php index 323c3e65b6..41aac1ec12 100644 --- a/src/applications/auth/action/PhabricatorAuthChangePasswordAction.php +++ b/src/applications/auth/action/PhabricatorAuthChangePasswordAction.php @@ -5,10 +5,6 @@ final class PhabricatorAuthChangePasswordAction const TYPECONST = 'auth.password'; - public function getActionConstant() { - return self::TYPECONST; - } - public function getScoreThreshold() { return 20 / phutil_units('1 hour in seconds'); } diff --git a/src/applications/auth/action/PhabricatorAuthEmailLoginAction.php b/src/applications/auth/action/PhabricatorAuthEmailLoginAction.php new file mode 100644 index 0000000000..97fe5b48e1 --- /dev/null +++ b/src/applications/auth/action/PhabricatorAuthEmailLoginAction.php @@ -0,0 +1,17 @@ +getEditRoutePattern('edit/') => 'PhabricatorAuthMessageEditController', - '(?P[1-9]\d*)/' => + '(?P[^/]+)/' => 'PhabricatorAuthMessageViewController', ), diff --git a/src/applications/auth/controller/PhabricatorEmailLoginController.php b/src/applications/auth/controller/PhabricatorEmailLoginController.php index 76b288f059..a744a90a6c 100644 --- a/src/applications/auth/controller/PhabricatorEmailLoginController.php +++ b/src/applications/auth/controller/PhabricatorEmailLoginController.php @@ -53,6 +53,14 @@ final class PhabricatorEmailLoginController // it expensive to fish for valid email addresses while giving the user // a better error if they goof their email. + $action_actor = PhabricatorSystemActionEngine::newActorFromRequest( + $request); + + PhabricatorSystemActionEngine::willTakeAction( + array($action_actor), + new PhabricatorAuthTryEmailLoginAction(), + 1); + $target_email = id(new PhabricatorUserEmail())->loadOneWhere( 'address = %s', $v_email); @@ -94,29 +102,40 @@ final class PhabricatorEmailLoginController } if (!$errors) { - $body = $this->newAccountLoginMailBody( - $target_user, - $is_logged_in); + $target_address = new PhutilEmailAddress($target_email->getAddress()); + + $user_log = PhabricatorUserLog::initializeNewLog( + $viewer, + $target_user->getPHID(), + PhabricatorEmailLoginUserLogType::LOGTYPE); + + $mail_engine = id(new PhabricatorPeopleEmailLoginMailEngine()) + ->setSender($viewer) + ->setRecipient($target_user) + ->setRecipientAddress($target_address) + ->setActivityLog($user_log); + + try { + $mail_engine->validateMail(); + } catch (PhabricatorPeopleMailEngineException $ex) { + return $this->newDialog() + ->setTitle($ex->getTitle()) + ->appendParagraph($ex->getBody()) + ->addCancelButton('/auth/start/', pht('Done')); + } + + $mail_engine->sendMail(); if ($is_logged_in) { - $subject = pht('[Phabricator] Account Password Link'); $instructions = pht( 'An email has been sent containing a link you can use to set '. 'a password for your account.'); } else { - $subject = pht('[Phabricator] Account Login Link'); $instructions = pht( 'An email has been sent containing a link you can use to log '. 'in to your account.'); } - $mail = id(new PhabricatorMetaMTAMail()) - ->setSubject($subject) - ->setForceDelivery(true) - ->addRawTos(array($target_email->getAddress())) - ->setBody($body) - ->saveAndSend(); - return $this->newDialog() ->setTitle(pht('Check Your Email')) ->setShortTitle(pht('Email Sent')) @@ -182,55 +201,6 @@ final class PhabricatorEmailLoginController ->addSubmitButton(pht('Send Email')); } - private function newAccountLoginMailBody( - PhabricatorUser $user, - $is_logged_in) { - - $engine = new PhabricatorAuthSessionEngine(); - $uri = $engine->getOneTimeLoginURI( - $user, - null, - PhabricatorAuthSessionEngine::ONETIME_RESET); - - $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); - $have_passwords = $this->isPasswordAuthEnabled(); - - if ($have_passwords) { - if ($is_logged_in) { - $body = pht( - 'You can use this link to set a password on your account:'. - "\n\n %s\n", - $uri); - } else if ($is_serious) { - $body = pht( - "You can use this link to reset your Phabricator password:". - "\n\n %s\n", - $uri); - } else { - $body = pht( - "Condolences on forgetting your password. You can use this ". - "link to reset it:\n\n". - " %s\n\n". - "After you set a new password, consider writing it down on a ". - "sticky note and attaching it to your monitor so you don't ". - "forget again! Choosing a very short, easy-to-remember password ". - "like \"cat\" or \"1234\" might also help.\n\n". - "Best Wishes,\nPhabricator\n", - $uri); - - } - } else { - $body = pht( - "You can use this login link to regain access to your Phabricator ". - "account:". - "\n\n". - " %s\n", - $uri); - } - - return $body; - } - private function isPasswordAuthEnabled() { return (bool)PhabricatorPasswordAuthProvider::getPasswordProvider(); } diff --git a/src/applications/auth/controller/config/PhabricatorAuthEditController.php b/src/applications/auth/controller/config/PhabricatorAuthEditController.php index d3cd2fef98..f602c4fb24 100644 --- a/src/applications/auth/controller/config/PhabricatorAuthEditController.php +++ b/src/applications/auth/controller/config/PhabricatorAuthEditController.php @@ -79,6 +79,7 @@ final class PhabricatorAuthEditController } $errors = array(); + $validation_exception = null; $v_login = $config->getShouldAllowLogin(); $v_registration = $config->getShouldAllowRegistration(); @@ -153,12 +154,16 @@ final class PhabricatorAuthEditController $editor = id(new PhabricatorAuthProviderConfigEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) - ->setContinueOnNoEffect(true) - ->applyTransactions($config, $xactions); + ->setContinueOnNoEffect(true); - $next_uri = $config->getURI(); + try { + $editor->applyTransactions($config, $xactions); + $next_uri = $config->getURI(); - return id(new AphrontRedirectResponse())->setURI($next_uri); + return id(new AphrontRedirectResponse())->setURI($next_uri); + } catch (Exception $ex) { + $validation_exception = $ex; + } } } else { $properties = $provider->readFormValuesFromProvider(); @@ -325,12 +330,35 @@ final class PhabricatorAuthEditController $provider->extendEditForm($request, $form, $properties, $issues); + $locked_config_key = 'auth.lock-config'; + $is_locked = PhabricatorEnv::getEnvConfig($locked_config_key); + + $locked_warning = null; + if ($is_locked && !$validation_exception) { + $message = pht( + 'Authentication provider configuration is locked, and can not be '. + 'changed without being unlocked. See the configuration setting %s '. + 'for details.', + phutil_tag( + 'a', + array( + 'href' => '/config/edit/'.$locked_config_key, + ), + $locked_config_key)); + $locked_warning = id(new PHUIInfoView()) + ->setViewer($viewer) + ->setSeverity(PHUIInfoView::SEVERITY_WARNING) + ->setErrors(array($message)); + } + $form ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri) + ->setDisabled($is_locked) ->setValue($button)); + $help = $provider->getConfigurationHelp(); if ($help) { $form->appendChild(id(new PHUIFormDividerControl())); @@ -346,12 +374,16 @@ final class PhabricatorAuthEditController $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Provider')) ->setFormErrors($errors) + ->setValidationException($validation_exception) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setForm($form); + + $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setFooter(array( + $locked_warning, $form_box, $footer, )); diff --git a/src/applications/auth/controller/config/PhabricatorAuthListController.php b/src/applications/auth/controller/config/PhabricatorAuthListController.php index b6ba91e7cd..5d1d85cca6 100644 --- a/src/applications/auth/controller/config/PhabricatorAuthListController.php +++ b/src/applications/auth/controller/config/PhabricatorAuthListController.php @@ -78,12 +78,14 @@ final class PhabricatorAuthListController ->setGuidanceContext($guidance_context) ->newInfoView(); + $is_disabled = (!$can_manage || $is_locked); $button = id(new PHUIButtonView()) ->setTag('a') ->setButtonType(PHUIButtonView::BUTTONTYPE_SIMPLE) - ->setHref($this->getApplicationURI('config/new/')) ->setIcon('fa-plus') - ->setDisabled(!$can_manage || $is_locked) + ->setDisabled($is_disabled) + ->setWorkflow($is_disabled) + ->setHref($this->getApplicationURI('config/new/')) ->setText(pht('Add Provider')); $list->setFlush(true); diff --git a/src/applications/auth/controller/config/PhabricatorAuthNewController.php b/src/applications/auth/controller/config/PhabricatorAuthNewController.php index 770c43208d..cb1c537ca8 100644 --- a/src/applications/auth/controller/config/PhabricatorAuthNewController.php +++ b/src/applications/auth/controller/config/PhabricatorAuthNewController.php @@ -9,6 +9,27 @@ final class PhabricatorAuthNewController $viewer = $this->getViewer(); $cancel_uri = $this->getApplicationURI(); + $locked_config_key = 'auth.lock-config'; + $is_locked = PhabricatorEnv::getEnvConfig($locked_config_key); + + if ($is_locked) { + $message = pht( + 'Authentication provider configuration is locked, and can not be '. + 'changed without being unlocked. See the configuration setting %s '. + 'for details.', + phutil_tag( + 'a', + array( + 'href' => '/config/edit/'.$locked_config_key, + ), + $locked_config_key)); + + return $this->newDialog() + ->setUser($viewer) + ->setTitle(pht('Authentication Config Locked')) + ->appendChild($message) + ->addCancelButton($cancel_uri); + } $providers = PhabricatorAuthProvider::getAllBaseProviders(); diff --git a/src/applications/auth/controller/config/PhabricatorAuthProviderViewController.php b/src/applications/auth/controller/config/PhabricatorAuthProviderViewController.php index 532744001c..abf9bf8eff 100644 --- a/src/applications/auth/controller/config/PhabricatorAuthProviderViewController.php +++ b/src/applications/auth/controller/config/PhabricatorAuthProviderViewController.php @@ -114,6 +114,86 @@ final class PhabricatorAuthProviderViewController pht('Provider Type'), $config->getProvider()->getProviderName()); + $status = $this->buildStatus($config); + $view->addProperty(pht('Status'), $status); + return $view; } + + private function buildStatus(PhabricatorAuthProviderConfig $config) { + $viewer = $this->getViewer(); + $view = id(new PHUIStatusListView()) + ->setViewer($viewer); + + $icon_enabled = PHUIStatusItemView::ICON_ACCEPT; + $icon_disabled = PHUIStatusItemView::ICON_REJECT; + + $icon_map = array( + true => $icon_enabled, + false => $icon_disabled, + ); + + $color_map = array( + true => 'green', + false => 'red', + ); + + $provider = $config->getProvider(); + + $view->addItem( + id(new PHUIStatusItemView()) + ->setIcon( + $icon_map[$config->getIsEnabled()], + $color_map[$config->getIsEnabled()]) + ->setTarget(pht('Provider Enabled'))); + + $view->addItem( + id(new PHUIStatusItemView()) + ->setIcon( + $icon_map[$config->getShouldAllowLogin()], + $color_map[$config->getShouldAllowLogin()]) + ->setTarget(pht('Allow Logins'))); + + $view->addItem( + id(new PHUIStatusItemView()) + ->setIcon( + $icon_map[$config->getShouldAllowRegistration()], + $color_map[$config->getShouldAllowRegistration()]) + ->setTarget(pht('Allow Registration'))); + + $view->addItem( + id(new PHUIStatusItemView()) + ->setIcon( + $icon_map[$config->getShouldAllowLink()], + $color_map[$config->getShouldAllowLink()]) + ->setTarget(pht('Allow Account Linking'))); + + $view->addItem( + id(new PHUIStatusItemView()) + ->setIcon( + $icon_map[$config->getShouldAllowUnlink()], + $color_map[$config->getShouldAllowUnlink()]) + ->setTarget(pht('Allow Account Unlinking'))); + + if ($provider->shouldAllowEmailTrustConfiguration()) { + $view->addItem( + id(new PHUIStatusItemView()) + ->setIcon( + $icon_map[$config->getShouldTrustEmails()], + $color_map[$config->getShouldTrustEmails()]) + ->setTarget(pht('Trust Email Addresses'))); + } + + if ($provider->supportsAutoLogin()) { + $view->addItem( + id(new PHUIStatusItemView()) + ->setIcon( + $icon_map[$config->getShouldAutoLogin()], + $color_map[$config->getShouldAutoLogin()]) + ->setTarget(pht('Allow Auto Login'))); + } + + return $view; + } + } diff --git a/src/applications/auth/controller/message/PhabricatorAuthMessageListController.php b/src/applications/auth/controller/message/PhabricatorAuthMessageListController.php index a3c518ab36..7981a03f16 100644 --- a/src/applications/auth/controller/message/PhabricatorAuthMessageListController.php +++ b/src/applications/auth/controller/message/PhabricatorAuthMessageListController.php @@ -19,11 +19,14 @@ final class PhabricatorAuthMessageListController $list = new PHUIObjectItemListView(); foreach ($types as $type) { $message = idx($messages, $type->getMessageTypeKey()); + if ($message) { $href = $message->getURI(); $name = $message->getMessageTypeDisplayName(); } else { - $href = '/auth/message/edit/?messageKey='.$type->getMessageTypeKey(); + $href = urisprintf( + '/auth/message/%s/', + $type->getMessageTypeKey()); $name = $type->getDisplayName(); } diff --git a/src/applications/auth/controller/message/PhabricatorAuthMessageViewController.php b/src/applications/auth/controller/message/PhabricatorAuthMessageViewController.php index db7e7e65e0..5665744463 100644 --- a/src/applications/auth/controller/message/PhabricatorAuthMessageViewController.php +++ b/src/applications/auth/controller/message/PhabricatorAuthMessageViewController.php @@ -9,26 +9,61 @@ final class PhabricatorAuthMessageViewController $this->requireApplicationCapability( AuthManageProvidersCapability::CAPABILITY); - $message = id(new PhabricatorAuthMessageQuery()) - ->setViewer($viewer) - ->withIDs(array($request->getURIData('id'))) - ->executeOne(); - if (!$message) { - return new Aphront404Response(); + // The "id" in the URI may either be an actual storage record ID (if a + // message has already been created) or a message type key (for a message + // type which does not have a record yet). + + // This flow allows messages which have not been set yet to have a detail + // page (so users can get detailed information about the message and see + // any default value). + + $id = $request->getURIData('id'); + if (ctype_digit($id)) { + $message = id(new PhabricatorAuthMessageQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + if (!$message) { + return new Aphront404Response(); + } + } else { + $types = PhabricatorAuthMessageType::getAllMessageTypes(); + if (!isset($types[$id])) { + return new Aphront404Response(); + } + + // If this message type already has a storage record, redirect to the + // canonical page for the record. + $message = id(new PhabricatorAuthMessageQuery()) + ->setViewer($viewer) + ->withMessageKeys(array($id)) + ->executeOne(); + if ($message) { + $message_uri = $message->getURI(); + return id(new AphrontRedirectResponse())->setURI($message_uri); + } + + // Otherwise, create an empty placeholder message object with the + // appropriate message type. + $message = PhabricatorAuthMessage::initializeNewMessage($types[$id]); } $crumbs = $this->buildApplicationCrumbs() - ->addTextCrumb($message->getObjectName()) + ->addTextCrumb($message->getMessageType()->getDisplayName()) ->setBorder(true); $header = $this->buildHeaderView($message); $properties = $this->buildPropertiesView($message); $curtain = $this->buildCurtain($message); - $timeline = $this->buildTransactionTimeline( - $message, - new PhabricatorAuthMessageTransactionQuery()); - $timeline->setShouldTerminate(true); + if ($message->getID()) { + $timeline = $this->buildTransactionTimeline( + $message, + new PhabricatorAuthMessageTransactionQuery()); + $timeline->setShouldTerminate(true); + } else { + $timeline = null; + } $view = id(new PHUITwoColumnView()) ->setHeader($header) @@ -62,19 +97,36 @@ final class PhabricatorAuthMessageViewController private function buildPropertiesView(PhabricatorAuthMessage $message) { $viewer = $this->getViewer(); + $message_type = $message->getMessageType(); + $view = id(new PHUIPropertyListView()) ->setViewer($viewer); - $view->addProperty( - pht('Description'), - $message->getMessageType()->getShortDescription()); + $full_description = $message_type->getFullDescription(); + if (strlen($full_description)) { + $view->addTextContent(new PHUIRemarkupView($viewer, $full_description)); + } else { + $short_description = $message_type->getShortDescription(); + $view->addProperty(pht('Description'), $short_description); + } - $view->addSectionHeader( - pht('Message Preview'), - PHUIPropertyListView::ICON_SUMMARY); + $message_text = $message->getMessageText(); + if (strlen($message_text)) { + $view->addSectionHeader( + pht('Message Preview'), + PHUIPropertyListView::ICON_SUMMARY); - $view->addTextContent( - new PHUIRemarkupView($viewer, $message->getMessageText())); + $view->addTextContent(new PHUIRemarkupView($viewer, $message_text)); + } + + $default_text = $message_type->getDefaultMessageText(); + if (strlen($default_text)) { + $view->addSectionHeader( + pht('Default Message'), + PHUIPropertyListView::ICON_SUMMARY); + + $view->addTextContent(new PHUIRemarkupView($viewer, $default_text)); + } return $view; } @@ -88,13 +140,27 @@ final class PhabricatorAuthMessageViewController $message, PhabricatorPolicyCapability::CAN_EDIT); + if ($id) { + $edit_uri = urisprintf('message/edit/%s/', $id); + $edit_name = pht('Edit Message'); + } else { + $edit_uri = urisprintf('message/edit/'); + $params = array( + 'messageKey' => $message->getMessageKey(), + ); + $edit_uri = new PhutilURI($edit_uri, $params); + + $edit_name = pht('Customize Message'); + } + $edit_uri = $this->getApplicationURI($edit_uri); + $curtain = $this->newCurtainView($message); $curtain->addAction( id(new PhabricatorActionView()) - ->setName(pht('Edit Message')) + ->setName($edit_name) ->setIcon('fa-pencil') - ->setHref($this->getApplicationURI("message/edit/{$id}/")) + ->setHref($edit_uri) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); diff --git a/src/applications/auth/editor/PhabricatorAuthProviderConfigEditor.php b/src/applications/auth/editor/PhabricatorAuthProviderConfigEditor.php index 5599ff5364..1e75edfbf0 100644 --- a/src/applications/auth/editor/PhabricatorAuthProviderConfigEditor.php +++ b/src/applications/auth/editor/PhabricatorAuthProviderConfigEditor.php @@ -125,4 +125,25 @@ final class PhabricatorAuthProviderConfigEditor return parent::mergeTransactions($u, $v); } + protected function validateAllTransactions( + PhabricatorLiskDAO $object, + array $xactions) { + + $errors = parent::validateAllTransactions($object, $xactions); + + $locked_config_key = 'auth.lock-config'; + $is_locked = PhabricatorEnv::getEnvConfig($locked_config_key); + + if ($is_locked) { + $errors[] = new PhabricatorApplicationTransactionValidationError( + null, + pht('Config Locked'), + pht('Authentication provider configuration is locked, and can not be '. + 'changed without being unlocked.'), + null); + } + + return $errors; + } + } diff --git a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php index 7d73cb194d..7358a61a40 100644 --- a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php +++ b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php @@ -294,8 +294,8 @@ final class PhabricatorAuthSessionEngine extends Phobject { null, $identity_phid, ($partial - ? PhabricatorUserLog::ACTION_LOGIN_PARTIAL - : PhabricatorUserLog::ACTION_LOGIN)); + ? PhabricatorPartialLoginUserLogType::LOGTYPE + : PhabricatorLoginUserLogType::LOGTYPE)); $log->setDetails( array( @@ -366,7 +366,7 @@ final class PhabricatorAuthSessionEngine extends Phobject { $log = PhabricatorUserLog::initializeNewLog( $user, $user->getPHID(), - PhabricatorUserLog::ACTION_LOGOUT); + PhabricatorLogoutUserLogType::LOGTYPE); $log->save(); $extensions = PhabricatorAuthSessionEngineExtension::getAllExtensions(); @@ -688,13 +688,13 @@ final class PhabricatorAuthSessionEngine extends Phobject { $log = PhabricatorUserLog::initializeNewLog( $viewer, $viewer->getPHID(), - PhabricatorUserLog::ACTION_ENTER_HISEC); + PhabricatorEnterHisecUserLogType::LOGTYPE); $log->save(); } else { $log = PhabricatorUserLog::initializeNewLog( $viewer, $viewer->getPHID(), - PhabricatorUserLog::ACTION_FAIL_HISEC); + PhabricatorFailHisecUserLogType::LOGTYPE); $log->save(); } } @@ -831,7 +831,7 @@ final class PhabricatorAuthSessionEngine extends Phobject { $log = PhabricatorUserLog::initializeNewLog( $viewer, $viewer->getPHID(), - PhabricatorUserLog::ACTION_EXIT_HISEC); + PhabricatorExitHisecUserLogType::LOGTYPE); $log->save(); } @@ -872,7 +872,7 @@ final class PhabricatorAuthSessionEngine extends Phobject { $log = PhabricatorUserLog::initializeNewLog( $viewer, $viewer->getPHID(), - PhabricatorUserLog::ACTION_LOGIN_FULL); + PhabricatorFullLoginUserLogType::LOGTYPE); $log->save(); unset($unguarded); } @@ -917,7 +917,7 @@ final class PhabricatorAuthSessionEngine extends Phobject { $log = PhabricatorUserLog::initializeNewLog( $viewer, $viewer->getPHID(), - PhabricatorUserLog::ACTION_LOGIN_LEGALPAD); + PhabricatorSignDocumentsUserLogType::LOGTYPE); $log->save(); } unset($unguarded); diff --git a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php index 7e77dfc11a..ebdf1b7218 100644 --- a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php @@ -194,6 +194,7 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { $control = id(new PHUIFormNumberControl()) ->setName($name) ->setDisableAutocomplete(true) + ->setAutofocus(true) ->setValue($value) ->setError($error); } diff --git a/src/applications/auth/message/PhabricatorAuthEmailLoginMessageType.php b/src/applications/auth/message/PhabricatorAuthEmailLoginMessageType.php new file mode 100644 index 0000000000..0bb55a7461 --- /dev/null +++ b/src/applications/auth/message/PhabricatorAuthEmailLoginMessageType.php @@ -0,0 +1,41 @@ +getFragment())) { + return null; + } + + if ($uri->getQueryParamsAsPairList()) { + return null; + } + $context_id = $matches[1]; $task_id = $matches[2]; diff --git a/src/applications/auth/provider/PhabricatorJIRAAuthProvider.php b/src/applications/auth/provider/PhabricatorJIRAAuthProvider.php index 23f7e7f706..c26c70ce37 100644 --- a/src/applications/auth/provider/PhabricatorJIRAAuthProvider.php +++ b/src/applications/auth/provider/PhabricatorJIRAAuthProvider.php @@ -341,6 +341,14 @@ final class PhabricatorJIRAAuthProvider return null; } + if (strlen($uri->getFragment())) { + return null; + } + + if ($uri->getQueryParamsAsPairList()) { + return null; + } + $domain = $matches[1]; $issue = $matches[2]; diff --git a/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php b/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php index 6bf145a393..12046497a5 100644 --- a/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php +++ b/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php @@ -282,48 +282,29 @@ final class PhabricatorPasswordAuthProvider extends PhabricatorAuthProvider { $viewer = $request->getUser(); $content_source = PhabricatorContentSource::newFromRequest($request); - $captcha_limit = 5; - $hard_limit = 32; - $limit_window = phutil_units('15 minutes in seconds'); + $rate_actor = PhabricatorSystemActionEngine::newActorFromRequest($request); - $failed_attempts = PhabricatorUserLog::loadRecentEventsFromThisIP( - PhabricatorUserLog::ACTION_LOGIN_FAILURE, - $limit_window); + PhabricatorSystemActionEngine::willTakeAction( + array($rate_actor), + new PhabricatorAuthTryPasswordAction(), + 1); // If the same remote address has submitted several failed login attempts // recently, require they provide a CAPTCHA response for new attempts. $require_captcha = false; $captcha_valid = false; if (AphrontFormRecaptchaControl::isRecaptchaEnabled()) { - if (count($failed_attempts) > $captcha_limit) { + try { + PhabricatorSystemActionEngine::willTakeAction( + array($rate_actor), + new PhabricatorAuthTryPasswordWithoutCAPTCHAAction(), + 1); + } catch (PhabricatorSystemActionRateLimitException $ex) { $require_captcha = true; $captcha_valid = AphrontFormRecaptchaControl::processCaptcha($request); } } - // If the user has submitted quite a few failed login attempts recently, - // give them a hard limit. - if (count($failed_attempts) > $hard_limit) { - $guidance = array(); - - $guidance[] = pht( - 'Your remote address has failed too many login attempts recently. '. - 'Wait a few minutes before trying again.'); - - $guidance[] = pht( - 'If you are unable to log in to your account, you can '. - '[[ /login/email | send a reset link to your email address ]].'); - - $guidance = implode("\n\n", $guidance); - - $dialog = $controller->newDialog() - ->setTitle(pht('Too Many Login Attempts')) - ->appendChild(new PHUIRemarkupView($viewer, $guidance)) - ->addCancelButton('/auth/start/', pht('Wait Patiently')); - - return array(null, $dialog); - } - $response = null; $account = null; $log_user = null; @@ -364,7 +345,7 @@ final class PhabricatorPasswordAuthProvider extends PhabricatorAuthProvider { $log = PhabricatorUserLog::initializeNewLog( null, $log_user ? $log_user->getPHID() : null, - PhabricatorUserLog::ACTION_LOGIN_FAILURE); + PhabricatorLoginFailureUserLogType::LOGTYPE); $log->save(); } diff --git a/src/applications/auth/storage/PhabricatorAuthMessage.php b/src/applications/auth/storage/PhabricatorAuthMessage.php index 00f5fbfbaa..9969d7aded 100644 --- a/src/applications/auth/storage/PhabricatorAuthMessage.php +++ b/src/applications/auth/storage/PhabricatorAuthMessage.php @@ -45,7 +45,7 @@ final class PhabricatorAuthMessage } public function getURI() { - return urisprintf('/auth/message/%s', $this->getID()); + return urisprintf('/auth/message/%s/', $this->getID()); } public function attachMessageType(PhabricatorAuthMessageType $type) { @@ -75,12 +75,16 @@ final class PhabricatorAuthMessage $message_key) { $message = self::loadMessage($viewer, $message_key); - - if (!$message) { - return null; + if ($message) { + $message_text = $message->getMessageText(); + if (strlen($message_text)) { + return $message_text; + } } - return $message->getMessageText(); + $message_type = PhabricatorAuthMessageType::newFromKey($message_key); + + return $message_type->getDefaultMessageText(); } diff --git a/src/applications/auth/storage/PhabricatorAuthProviderConfigTransaction.php b/src/applications/auth/storage/PhabricatorAuthProviderConfigTransaction.php index d5a3588d59..f60ba8c734 100644 --- a/src/applications/auth/storage/PhabricatorAuthProviderConfigTransaction.php +++ b/src/applications/auth/storage/PhabricatorAuthProviderConfigTransaction.php @@ -14,15 +14,8 @@ final class PhabricatorAuthProviderConfigTransaction const PROPERTY_KEY = 'auth:property'; - private $provider; - - public function setProvider(PhabricatorAuthProvider $provider) { - $this->provider = $provider; - return $this; - } - public function getProvider() { - return $this->provider; + return $this->getObject()->getProvider(); } public function getApplicationName() { diff --git a/src/applications/base/controller/PhabricatorController.php b/src/applications/base/controller/PhabricatorController.php index cfd0eaee65..a463c741d1 100644 --- a/src/applications/base/controller/PhabricatorController.php +++ b/src/applications/base/controller/PhabricatorController.php @@ -481,7 +481,7 @@ abstract class PhabricatorController extends AphrontController { protected function buildTransactionTimeline( PhabricatorApplicationTransactionInterface $object, - PhabricatorApplicationTransactionQuery $query, + PhabricatorApplicationTransactionQuery $query = null, PhabricatorMarkupEngine $engine = null, $view_data = array()) { @@ -489,6 +489,17 @@ abstract class PhabricatorController extends AphrontController { $viewer = $this->getViewer(); $xaction = $object->getApplicationTransactionTemplate(); + if (!$query) { + $query = PhabricatorApplicationTransactionQuery::newQueryForObject( + $object); + if (!$query) { + throw new Exception( + pht( + 'Unable to find transaction query for object of class "%s".', + get_class($object))); + } + } + $pager = id(new AphrontCursorPagerView()) ->readFromRequest($request) ->setURI(new PhutilURI( diff --git a/src/applications/cache/storage/PhabricatorMarkupCache.php b/src/applications/cache/storage/PhabricatorMarkupCache.php index 03a4b08681..e008a18ee1 100644 --- a/src/applications/cache/storage/PhabricatorMarkupCache.php +++ b/src/applications/cache/storage/PhabricatorMarkupCache.php @@ -30,4 +30,8 @@ final class PhabricatorMarkupCache extends PhabricatorCacheDAO { ) + parent::getConfiguration(); } + public function getSchemaPersistence() { + return PhabricatorConfigTableSchema::PERSISTENCE_CACHE; + } + } diff --git a/src/applications/conduit/method/ConduitGetCertificateConduitAPIMethod.php b/src/applications/conduit/method/ConduitGetCertificateConduitAPIMethod.php index 6248b5a6ba..41716748e0 100644 --- a/src/applications/conduit/method/ConduitGetCertificateConduitAPIMethod.php +++ b/src/applications/conduit/method/ConduitGetCertificateConduitAPIMethod.php @@ -41,7 +41,7 @@ final class ConduitGetCertificateConduitAPIMethod extends ConduitAPIMethod { protected function execute(ConduitAPIRequest $request) { $failed_attempts = PhabricatorUserLog::loadRecentEventsFromThisIP( - PhabricatorUserLog::ACTION_CONDUIT_CERTIFICATE_FAILURE, + PhabricatorConduitCertificateFailureUserLogType::LOGTYPE, 60 * 5); if (count($failed_attempts) > 5) { @@ -61,7 +61,7 @@ final class ConduitGetCertificateConduitAPIMethod extends ConduitAPIMethod { $log = PhabricatorUserLog::initializeNewLog( $request->getUser(), $info->getUserPHID(), - PhabricatorUserLog::ACTION_CONDUIT_CERTIFICATE) + PhabricatorConduitCertificateUserLogType::LOGTYPE) ->save(); } @@ -85,7 +85,7 @@ final class ConduitGetCertificateConduitAPIMethod extends ConduitAPIMethod { $log = PhabricatorUserLog::initializeNewLog( $request->getUser(), $info ? $info->getUserPHID() : '-', - PhabricatorUserLog::ACTION_CONDUIT_CERTIFICATE_FAILURE) + PhabricatorConduitCertificateFailureUserLogType::LOGTYPE) ->save(); } diff --git a/src/applications/conduit/parametertype/ConduitEpochParameterType.php b/src/applications/conduit/parametertype/ConduitEpochParameterType.php index e8fe095c50..8f2ca2c98a 100644 --- a/src/applications/conduit/parametertype/ConduitEpochParameterType.php +++ b/src/applications/conduit/parametertype/ConduitEpochParameterType.php @@ -3,8 +3,24 @@ final class ConduitEpochParameterType extends ConduitParameterType { + private $allowNull; + + public function setAllowNull($allow_null) { + $this->allowNull = $allow_null; + return $this; + } + + public function getAllowNull() { + return $this->allowNull; + } + protected function getParameterValue(array $request, $key, $strict) { $value = parent::getParameterValue($request, $key, $strict); + + if ($this->allowNull && ($value === null)) { + return $value; + } + $value = $this->parseIntValue($request, $key, $value, $strict); if ($value <= 0) { diff --git a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php index a8a883ca96..d863c928b7 100644 --- a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php +++ b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php @@ -536,6 +536,9 @@ final class PhabricatorExtraConfigSetupCheck extends PhabricatorSetupCheck { 'differential.whitespace-matters' => pht( 'Whitespace rendering is now handled automatically.'), + + 'phd.pid-directory' => pht( + 'Phabricator daemons no longer use PID files.'), ); return $ancient_config; diff --git a/src/applications/config/check/PhabricatorZipSetupCheck.php b/src/applications/config/check/PhabricatorZipSetupCheck.php new file mode 100644 index 0000000000..e440cd4c52 --- /dev/null +++ b/src/applications/config/check/PhabricatorZipSetupCheck.php @@ -0,0 +1,29 @@ +newIssue('extension.zip') + ->setName(pht('Missing "zip" Extension')) + ->setMessage($message) + ->addPHPExtension('zip'); + } + } +} diff --git a/src/applications/config/management/PhabricatorConfigManagementSetWorkflow.php b/src/applications/config/management/PhabricatorConfigManagementSetWorkflow.php index 22b760872e..9eb83bd61e 100644 --- a/src/applications/config/management/PhabricatorConfigManagementSetWorkflow.php +++ b/src/applications/config/management/PhabricatorConfigManagementSetWorkflow.php @@ -30,11 +30,10 @@ final class PhabricatorConfigManagementSetWorkflow } public function execute(PhutilArgumentParser $args) { - $console = PhutilConsole::getConsole(); $argv = $args->getArg('args'); - if (count($argv) == 0) { + if (!$argv) { throw new PhutilArgumentUsageException( - pht('Specify a configuration key and a value to set it to.')); + pht('Specify the configuration key you want to set.')); } $is_stdin = $args->getArg('stdin'); @@ -45,7 +44,8 @@ final class PhabricatorConfigManagementSetWorkflow if (count($argv) > 1) { throw new PhutilArgumentUsageException( pht( - 'Too many arguments: expected only a key when using "--stdin".')); + 'Too many arguments: expected only a configuration key when '. + 'using "--stdin".')); } fprintf(STDERR, tsprintf("%s\n", pht('Reading value from stdin...'))); @@ -54,7 +54,8 @@ final class PhabricatorConfigManagementSetWorkflow if (count($argv) == 1) { throw new PhutilArgumentUsageException( pht( - "Specify a value to set the key '%s' to.", + 'Specify a value to set the configuration key "%s" to, or '. + 'use "--stdin" to read a value from stdin.', $key)); } @@ -67,14 +68,13 @@ final class PhabricatorConfigManagementSetWorkflow $value = $argv[1]; } - $options = PhabricatorApplicationConfigOptions::loadAllOptions(); if (empty($options[$key])) { throw new PhutilArgumentUsageException( pht( - "No such configuration key '%s'! Use `%s` to list all keys.", - $key, - 'config list')); + 'Configuration key "%s" is unknown. Use "bin/config list" to list '. + 'all known keys.', + $key)); } $option = $options[$key]; @@ -99,7 +99,7 @@ final class PhabricatorConfigManagementSetWorkflow switch ($type) { default: $message = pht( - 'Config key "%s" is of type "%s". Specify it in JSON.', + 'Configuration key "%s" is of type "%s". Specify it in JSON.', $key, $type); break; @@ -128,7 +128,6 @@ final class PhabricatorConfigManagementSetWorkflow } if ($use_database) { - $config_type = 'database'; $config_entry = PhabricatorConfigEntry::loadConfigEntry($key); $config_entry->setValue($value); @@ -136,15 +135,28 @@ final class PhabricatorConfigManagementSetWorkflow $config_entry->setIsDeleted(0); $config_entry->save(); + + $write_message = pht( + 'Wrote configuration key "%s" to database storage.', + $key); } else { - $config_type = 'local'; - id(new PhabricatorConfigLocalSource()) + $config_source = id(new PhabricatorConfigLocalSource()) ->setKeys(array($key => $value)); + + $local_path = $config_source->getReadablePath(); + + $write_message = pht( + 'Wrote configuration key "%s" to local storage (in file "%s").', + $key, + $local_path); } - $console->writeOut( - "%s\n", - pht("Set '%s' in %s configuration.", $key, $config_type)); + echo tsprintf( + "** %s ** %s\n", + pht('DONE'), + $write_message); + + return 0; } } diff --git a/src/applications/config/option/PhabricatorPHDConfigOptions.php b/src/applications/config/option/PhabricatorPHDConfigOptions.php index e04353876a..7a1d39e617 100644 --- a/src/applications/config/option/PhabricatorPHDConfigOptions.php +++ b/src/applications/config/option/PhabricatorPHDConfigOptions.php @@ -21,10 +21,6 @@ final class PhabricatorPHDConfigOptions public function getOptions() { return array( - $this->newOption('phd.pid-directory', 'string', '/var/tmp/phd/pid') - ->setLocked(true) - ->setDescription( - pht('Directory that phd should use to track running daemons.')), $this->newOption('phd.log-directory', 'string', '/var/tmp/phd/log') ->setLocked(true) ->setDescription( diff --git a/src/applications/config/schema/PhabricatorConfigSchemaSpec.php b/src/applications/config/schema/PhabricatorConfigSchemaSpec.php index 8e67391b59..adccdc1267 100644 --- a/src/applications/config/schema/PhabricatorConfigSchemaSpec.php +++ b/src/applications/config/schema/PhabricatorConfigSchemaSpec.php @@ -48,11 +48,19 @@ abstract class PhabricatorConfigSchemaSpec extends Phobject { abstract public function buildSchemata(); protected function buildLiskObjectSchema(PhabricatorLiskDAO $object) { + $index_options = array(); + + $persistence = $object->getSchemaPersistence(); + if ($persistence !== null) { + $index_options['persistence'] = $persistence; + } + $this->buildRawSchema( $object->getApplicationName(), $object->getTableName(), $object->getSchemaColumns(), - $object->getSchemaKeys()); + $object->getSchemaKeys(), + $index_options); } protected function buildFerretIndexSchema(PhabricatorFerretEngine $engine) { diff --git a/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php b/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php index 421008082f..8d9a9986e4 100644 --- a/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php +++ b/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php @@ -85,6 +85,7 @@ final class PhabricatorDaemonConsoleController phutil_tag('em', array(), pht('Temporary Failures')), count($failed), null, + null, ); } diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementRestartWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementRestartWorkflow.php index 3e9f0af8c3..eb2d5d229c 100644 --- a/src/applications/daemon/management/PhabricatorDaemonManagementRestartWorkflow.php +++ b/src/applications/daemon/management/PhabricatorDaemonManagementRestartWorkflow.php @@ -6,7 +6,10 @@ final class PhabricatorDaemonManagementRestartWorkflow protected function didConstruct() { $this ->setName('restart') - ->setSynopsis(pht('Stop, then start the standard daemon loadout.')) + ->setSynopsis( + pht( + 'Stop daemon processes on this host, then start the standard '. + 'daemon loadout.')) ->setArguments( array( array( @@ -17,17 +20,15 @@ final class PhabricatorDaemonManagementRestartWorkflow 'seconds. Defaults to __15__ seconds.'), 'default' => 15, ), - array( - 'name' => 'gently', - 'help' => pht( - 'Ignore running processes that look like daemons but do not '. - 'have corresponding PID files.'), - ), array( 'name' => 'force', 'help' => pht( - 'Also stop running processes that look like daemons but do '. - 'not have corresponding PID files.'), + 'Stop all daemon processes on this host, even if they belong '. + 'to another Phabricator instance.'), + ), + array( + 'name' => 'gently', + 'help' => pht('Deprecated. Has no effect.'), ), $this->getAutoscaleReserveArgument(), )); @@ -35,12 +36,11 @@ final class PhabricatorDaemonManagementRestartWorkflow public function execute(PhutilArgumentParser $args) { $err = $this->executeStopCommand( - array(), array( 'graceful' => $args->getArg('graceful'), 'force' => $args->getArg('force'), - 'gently' => $args->getArg('gently'), )); + if ($err) { return $err; } diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php index 343e42ba63..d5af149869 100644 --- a/src/applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php +++ b/src/applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php @@ -6,101 +6,52 @@ final class PhabricatorDaemonManagementStatusWorkflow protected function didConstruct() { $this ->setName('status') - ->setSynopsis(pht('Show status of running daemons.')) - ->setArguments( - array( - array( - 'name' => 'local', - 'help' => pht('Show only local daemons.'), - ), - )); + ->setSynopsis(pht('Show daemon processes on this host.')); } public function execute(PhutilArgumentParser $args) { - $console = PhutilConsole::getConsole(); + $process_refs = $this->getOverseerProcessRefs(); - if ($args->getArg('local')) { - $daemons = $this->loadRunningDaemons(); - } else { - $daemons = $this->loadAllRunningDaemons(); - } + if (!$process_refs) { + $instance = $this->getInstance(); + if ($instance !== null) { + $this->logInfo( + pht('NO DAEMONS'), + pht( + 'There are no running daemon processes for the current '. + 'instance ("%s").', + $instance)); + } else { + $this->logInfo( + pht('NO DAEMONS'), + pht('There are no running daemon processes.')); + } - if (!$daemons) { - $console->writeErr( - "%s\n", - pht('There are no running Phabricator daemons.')); return 1; } - $status = 0; - $table = id(new PhutilConsoleTable()) - ->addColumns(array( - 'id' => array( - 'title' => pht('Log'), - ), - 'daemonID' => array( - 'title' => pht('Daemon'), - ), - 'host' => array( - 'title' => pht('Host'), - ), - 'pid' => array( - 'title' => pht('Overseer'), - ), - 'started' => array( - 'title' => pht('Started'), - ), - 'daemon' => array( - 'title' => pht('Class'), - ), - 'argv' => array( - 'title' => pht('Arguments'), - ), - )); - - foreach ($daemons as $daemon) { - if ($daemon instanceof PhabricatorDaemonLog) { - $table->addRow(array( - 'id' => $daemon->getID(), - 'daemonID' => $daemon->getDaemonID(), - 'host' => $daemon->getHost(), - 'pid' => $daemon->getPID(), - 'started' => date('M j Y, g:i:s A', $daemon->getDateCreated()), - 'daemon' => $daemon->getDaemon(), - 'argv' => csprintf('%LR', $daemon->getExplicitArgv()), + ->addColumns( + array( + 'pid' => array( + 'title' => pht('PID'), + ), + 'command' => array( + 'title' => pht('Command'), + ), )); - } else if ($daemon instanceof PhabricatorDaemonReference) { - $name = $daemon->getName(); - if (!$daemon->isRunning()) { - $daemon->updateStatus(PhabricatorDaemonLog::STATUS_DEAD); - $status = 2; - $name = pht(' %s', $name); - } - $daemon_log = $daemon->getDaemonLog(); - $id = null; - $daemon_id = null; - if ($daemon_log) { - $id = $daemon_log->getID(); - $daemon_id = $daemon_log->getDaemonID(); - } - - $table->addRow(array( - 'id' => $id, - 'daemonID' => $daemon_id, - 'host' => 'localhost', - 'pid' => $daemon->getPID(), - 'started' => $daemon->getEpochStarted() - ? date('M j Y, g:i:s A', $daemon->getEpochStarted()) - : null, - 'daemon' => $name, - 'argv' => csprintf('%LR', $daemon->getArgv()), + foreach ($process_refs as $process_ref) { + $table->addRow( + array( + 'pid' => $process_ref->getPID(), + 'command' => $process_ref->getCommand(), )); - } } $table->draw(); + + return 0; } } diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementStopWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementStopWorkflow.php index c54a7e9fee..19b9fc44fb 100644 --- a/src/applications/daemon/management/PhabricatorDaemonManagementStopWorkflow.php +++ b/src/applications/daemon/management/PhabricatorDaemonManagementStopWorkflow.php @@ -6,11 +6,7 @@ final class PhabricatorDaemonManagementStopWorkflow protected function didConstruct() { $this ->setName('stop') - ->setSynopsis( - pht( - 'Stop all running daemons, or specific daemons identified by PIDs. '. - 'Use **%s** to find PIDs.', - 'phd status')) + ->setSynopsis(pht('Stop daemon processes on this host.')) ->setArguments( array( array( @@ -24,29 +20,21 @@ final class PhabricatorDaemonManagementStopWorkflow array( 'name' => 'force', 'help' => pht( - 'Also stop running processes that look like daemons but do '. - 'not have corresponding PID files.'), + 'Stop all daemon processes on this host, even if they belong '. + 'to another Phabricator instance.'), ), array( 'name' => 'gently', - 'help' => pht( - 'Ignore running processes that look like daemons but do not '. - 'have corresponding PID files.'), - ), - array( - 'name' => 'pids', - 'wildcard' => true, + 'help' => pht('Deprecated. Has no effect.'), ), )); } public function execute(PhutilArgumentParser $args) { return $this->executeStopCommand( - $args->getArg('pids'), array( 'graceful' => $args->getArg('graceful'), 'force' => $args->getArg('force'), - 'gently' => $args->getArg('gently'), )); } diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php index d5b4ed23e5..05d94e218d 100644 --- a/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php +++ b/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php @@ -12,11 +12,6 @@ abstract class PhabricatorDaemonManagementWorkflow ->selectSymbolsWithoutLoading(); } - final protected function getPIDDirectory() { - $path = PhabricatorEnv::getEnvConfig('phd.pid-directory'); - return $this->getControlDirectory($path); - } - final protected function getLogDirectory() { $path = PhabricatorEnv::getEnvConfig('phd.log-directory'); return $this->getControlDirectory($path); @@ -30,56 +25,16 @@ abstract class PhabricatorDaemonManagementWorkflow pht( "%s requires the directory '%s' to exist, but it does not exist ". "and could not be created. Create this directory or update ". - "'%s' / '%s' in your configuration to point to an existing ". + "'%s' in your configuration to point to an existing ". "directory.", 'phd', $path, - 'phd.pid-directory', 'phd.log-directory')); } } return $path; } - final protected function loadRunningDaemons() { - $daemons = array(); - - $pid_dir = $this->getPIDDirectory(); - $pid_files = Filesystem::listDirectory($pid_dir); - - foreach ($pid_files as $pid_file) { - $path = $pid_dir.'/'.$pid_file; - $daemons[] = PhabricatorDaemonReference::loadReferencesFromFile($path); - } - - return array_mergev($daemons); - } - - final protected function loadAllRunningDaemons() { - $local_daemons = $this->loadRunningDaemons(); - - $local_ids = array(); - foreach ($local_daemons as $daemon) { - $daemon_log = $daemon->getDaemonLog(); - - if ($daemon_log) { - $local_ids[] = $daemon_log->getID(); - } - } - - $daemon_query = id(new PhabricatorDaemonLogQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE); - - if ($local_ids) { - $daemon_query->withoutIDs($local_ids); - } - - $remote_daemons = $daemon_query->execute(); - - return array_merge($local_daemons, $remote_daemons); - } - private function findDaemonClass($substring) { $symbols = $this->loadAvailableDaemonClasses(); @@ -169,7 +124,7 @@ abstract class PhabricatorDaemonManagementWorkflow $flags[] = '--verbose'; } - $instance = PhabricatorEnv::getEnvConfig('cluster.instance'); + $instance = $this->getInstance(); if ($instance) { $flags[] = '-l'; $flags[] = $instance; @@ -185,14 +140,6 @@ abstract class PhabricatorDaemonManagementWorkflow $config['log'] = $this->getLogDirectory().'/daemons.log'; } - $pid_dir = $this->getPIDDirectory(); - - // TODO: This should be a much better user experience. - Filesystem::assertExists($pid_dir); - Filesystem::assertIsDirectory($pid_dir); - Filesystem::assertWritable($pid_dir); - - $config['piddir'] = $pid_dir; $config['daemons'] = $daemons; $command = csprintf('./phd-daemon %Ls', $flags); @@ -324,28 +271,31 @@ abstract class PhabricatorDaemonManagementWorkflow $console = PhutilConsole::getConsole(); if (!idx($options, 'force')) { - $running = $this->loadRunningDaemons(); + $process_refs = $this->getOverseerProcessRefs(); + if ($process_refs) { + $this->logWarn( + pht('RUNNING DAEMONS'), + pht('Daemons are already running:')); - // This may include daemons which were launched but which are no longer - // running; check that we actually have active daemons before failing. - foreach ($running as $daemon) { - if ($daemon->isRunning()) { - $message = pht( - "phd start: Unable to start daemons because daemons are already ". - "running.\n\n". - "You can view running daemons with '%s'.\n". - "You can stop running daemons with '%s'.\n". - "You can use '%s' to stop all daemons before starting ". - "new daemons.\n". - "You can force daemons to start anyway with %s.", - 'phd status', - 'phd stop', - 'phd restart', - '--force'); - - $console->writeErr("%s\n", $message); - exit(1); + fprintf(STDERR, '%s', "\n"); + foreach ($process_refs as $process_ref) { + fprintf( + STDERR, + '%s', + tsprintf( + " %s %s\n", + $process_ref->getPID(), + $process_ref->getCommand())); } + fprintf(STDERR, '%s', "\n"); + + $this->logFail( + pht('RUNNING DAEMONS'), + pht( + 'Use "phd stop" to stop daemons, "phd restart" to restart '. + 'daemons, or "phd start --force" to ignore running processes.')); + + exit(1); } } @@ -386,148 +336,115 @@ abstract class PhabricatorDaemonManagementWorkflow return 0; } - final protected function executeStopCommand( - array $pids, - array $options) { - - $console = PhutilConsole::getConsole(); - + final protected function executeStopCommand(array $options) { $grace_period = idx($options, 'graceful', 15); $force = idx($options, 'force'); - $gently = idx($options, 'gently'); - if ($gently && $force) { - throw new PhutilArgumentUsageException( + $query = id(new PhutilProcessQuery()) + ->withIsOverseer(true); + + $instance = $this->getInstance(); + if ($instance !== null && !$force) { + $query->withInstances(array($instance)); + } + + try { + $process_refs = $query->execute(); + } catch (Exception $ex) { + // See T13321. If this fails for some reason, just continue for now so + // that daemon management still works. In the long run, we don't expect + // this to fail, but I don't want to break this workflow while we iron + // bugs out. + + // See T12827. Particularly, this is likely to fail on Solaris. + + phlog($ex); + + $process_refs = array(); + } + + if (!$process_refs) { + if ($instance !== null && !$force) { + $this->logInfo( + pht('NO DAEMONS'), + pht( + 'There are no running daemons for the current instance ("%s"). '. + 'Use "--force" to stop daemons for all instances.', + $instance)); + } else { + $this->logInfo( + pht('NO DAEMONS'), + pht('There are no running daemons.')); + } + + return 0; + } + + $process_refs = mpull($process_refs, null, 'getPID'); + + $stop_pids = array_keys($process_refs); + $live_pids = $this->sendStopSignals($stop_pids, $grace_period); + + $stop_pids = array_fuse($stop_pids); + $live_pids = array_fuse($live_pids); + + $dead_pids = array_diff_key($stop_pids, $live_pids); + + foreach ($dead_pids as $dead_pid) { + $dead_ref = $process_refs[$dead_pid]; + $this->logOkay( + pht('STOP'), pht( - 'You can not specify conflicting options %s and %s together.', - '--gently', - '--force')); + 'Stopped PID %d ("%s")', + $dead_pid, + $dead_ref->getCommand())); } - $daemons = $this->loadRunningDaemons(); - if (!$daemons) { - $survivors = array(); - if (!$pids && !$gently) { - $survivors = $this->processRogueDaemons( - $grace_period, - $warn = true, - $force); - } - if (!$survivors) { - $console->writeErr( - "%s\n", - pht('There are no running Phabricator daemons.')); - } - return 0; + foreach ($live_pids as $live_pid) { + $live_ref = $process_refs[$live_pid]; + $this->logFail( + pht('SURVIVED'), + pht( + 'Unable to stop PID %d ("%s").', + $live_pid, + $live_ref->getCommand())); } - $stop_pids = $this->selectDaemonPIDs($daemons, $pids); - - if (!$stop_pids) { - $console->writeErr("%s\n", pht('No daemons to kill.')); - return 0; - } - - $survivors = $this->sendStopSignals($stop_pids, $grace_period); - - // Try to clean up PID files for daemons we killed. - $remove = array(); - foreach ($daemons as $daemon) { - $pid = $daemon->getPID(); - if (empty($stop_pids[$pid])) { - // We did not try to stop this overseer. - continue; - } - - if (isset($survivors[$pid])) { - // We weren't able to stop this overseer. - continue; - } - - if (!$daemon->getPIDFile()) { - // We don't know where the PID file is. - continue; - } - - $remove[] = $daemon->getPIDFile(); - } - - foreach (array_unique($remove) as $remove_file) { - Filesystem::remove($remove_file); - } - - if (!$gently) { - $this->processRogueDaemons($grace_period, !$pids, $force); + if ($live_pids) { + $this->logWarn( + pht('SURVIVORS'), + pht( + 'Unable to stop all daemon processes. You may need to run this '. + 'command as root with "sudo".')); } return 0; } final protected function executeReloadCommand(array $pids) { - $console = PhutilConsole::getConsole(); + $process_refs = $this->getOverseerProcessRefs(); + + if (!$process_refs) { + $this->logInfo( + pht('NO DAEMONS'), + pht('There are no running daemon processes to reload.')); - $daemons = $this->loadRunningDaemons(); - if (!$daemons) { - $console->writeErr( - "%s\n", - pht('There are no running daemons to reload.')); return 0; } - $reload_pids = $this->selectDaemonPIDs($daemons, $pids); - if (!$reload_pids) { - $console->writeErr( - "%s\n", - pht('No daemons to reload.')); - return 0; - } + foreach ($process_refs as $process_ref) { + $pid = $process_ref->getPID(); - foreach ($reload_pids as $pid) { - $console->writeOut( - "%s\n", + $this->logInfo( + pht('RELOAD'), pht('Reloading process %d...', $pid)); + posix_kill($pid, SIGHUP); } return 0; } - private function processRogueDaemons($grace_period, $warn, $force_stop) { - $console = PhutilConsole::getConsole(); - - $rogue_daemons = PhutilDaemonOverseer::findRunningDaemons(); - if ($rogue_daemons) { - if ($force_stop) { - $rogue_pids = ipull($rogue_daemons, 'pid'); - $survivors = $this->sendStopSignals($rogue_pids, $grace_period); - if ($survivors) { - $console->writeErr( - "%s\n", - pht( - 'Unable to stop processes running without PID files. '. - 'Try running this command again with sudo.')); - } - } else if ($warn) { - $console->writeErr("%s\n", $this->getForceStopHint($rogue_daemons)); - } - } - - return $rogue_daemons; - } - - private function getForceStopHint($rogue_daemons) { - $debug_output = ''; - foreach ($rogue_daemons as $rogue) { - $debug_output .= $rogue['pid'].' '.$rogue['command']."\n"; - } - return pht( - "There are processes running that look like Phabricator daemons but ". - "have no corresponding PID files:\n\n%s\n\n". - "Stop these processes by re-running this command with the %s parameter.", - $debug_output, - '--force'); - } - private function sendStopSignals($pids, $grace_period) { // If we're doing a graceful shutdown, try SIGINT first. if ($grace_period) { @@ -674,4 +591,21 @@ abstract class PhabricatorDaemonManagementWorkflow return $select_pids; } + protected function getOverseerProcessRefs() { + $query = id(new PhutilProcessQuery()) + ->withIsOverseer(true); + + $instance = PhabricatorEnv::getEnvConfig('cluster.instance'); + if ($instance !== null) { + $query->withInstances(array($instance)); + } + + return $query->execute(); + } + + protected function getInstance() { + return PhabricatorEnv::getEnvConfig('cluster.instance'); + } + + } diff --git a/src/applications/differential/conduit/DifferentialCreateDiffConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialCreateDiffConduitAPIMethod.php index 8a0da78865..b0582f8f67 100644 --- a/src/applications/differential/conduit/DifferentialCreateDiffConduitAPIMethod.php +++ b/src/applications/differential/conduit/DifferentialCreateDiffConduitAPIMethod.php @@ -119,8 +119,11 @@ final class DifferentialCreateDiffConduitAPIMethod break; } + $source_path = $request->getValue('sourcePath'); + $source_path = $this->normalizeSourcePath($source_path); + $diff_data_dict = array( - 'sourcePath' => $request->getValue('sourcePath'), + 'sourcePath' => $source_path, 'sourceMachine' => $request->getValue('sourceMachine'), 'branch' => $request->getValue('branch'), 'creationMethod' => $request->getValue('creationMethod'), @@ -158,4 +161,18 @@ final class DifferentialCreateDiffConduitAPIMethod ); } + private function normalizeSourcePath($source_path) { + // See T13385. This property is probably headed for deletion. Until we get + // there, stop errors arising from running "arc diff" in a working copy + // with too many characters. + + $max_size = id(new DifferentialDiff()) + ->getColumnMaximumByteLength('sourcePath'); + + return id(new PhutilUTF8StringTruncator()) + ->setMaximumBytes($max_size) + ->setTerminator('') + ->truncateString($source_path); + } + } diff --git a/src/applications/differential/query/DifferentialRevisionSearchEngine.php b/src/applications/differential/query/DifferentialRevisionSearchEngine.php index ebbf858e2b..b39c86fecb 100644 --- a/src/applications/differential/query/DifferentialRevisionSearchEngine.php +++ b/src/applications/differential/query/DifferentialRevisionSearchEngine.php @@ -51,6 +51,12 @@ final class DifferentialRevisionSearchEngine $map['createdEnd']); } + if ($map['modifiedStart'] || $map['modifiedEnd']) { + $query->withUpdatedEpochBetween( + $map['modifiedStart'], + $map['modifiedEnd']); + } + return $query; } @@ -100,6 +106,18 @@ final class DifferentialRevisionSearchEngine ->setKey('createdEnd') ->setDescription( pht('Find revisions created at or before a particular time.')), + id(new PhabricatorSearchDateField()) + ->setLabel(pht('Modified After')) + ->setKey('modifiedStart') + ->setIsHidden(true) + ->setDescription( + pht('Find revisions modified at or after a particular time.')), + id(new PhabricatorSearchDateField()) + ->setLabel(pht('Modified Before')) + ->setKey('modifiedEnd') + ->setIsHidden(true) + ->setDescription( + pht('Find revisions modified at or before a particular time.')), ); } diff --git a/src/applications/differential/storage/DifferentialRevision.php b/src/applications/differential/storage/DifferentialRevision.php index 5e70b52188..c5eaa71b2d 100644 --- a/src/applications/differential/storage/DifferentialRevision.php +++ b/src/applications/differential/storage/DifferentialRevision.php @@ -112,11 +112,6 @@ final class DifferentialRevision extends DifferentialDAO 'repositoryPHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( - 'key_phid' => null, - 'phid' => array( - 'columns' => array('phid'), - 'unique' => true, - ), 'authorPHID' => array( 'columns' => array('authorPHID', 'status'), ), @@ -131,6 +126,9 @@ final class DifferentialRevision extends DifferentialDAO 'key_status' => array( 'columns' => array('status', 'phid'), ), + 'key_modified' => array( + 'columns' => array('dateModified'), + ), ), ) + parent::getConfiguration(); } diff --git a/src/applications/diffusion/conduit/DiffusionQueryConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionQueryConduitAPIMethod.php index 716824f9f8..ca32cc0127 100644 --- a/src/applications/diffusion/conduit/DiffusionQueryConduitAPIMethod.php +++ b/src/applications/diffusion/conduit/DiffusionQueryConduitAPIMethod.php @@ -7,17 +7,6 @@ abstract class DiffusionQueryConduitAPIMethod return true; } - public function getMethodStatus() { - return self::METHOD_STATUS_UNSTABLE; - } - - public function getMethodStatusDescription() { - return pht( - 'See T2784 - migrating Diffusion working copy calls to conduit methods. '. - 'Until that task is completed (and possibly after) these methods are '. - 'unstable.'); - } - private $diffusionRequest; private $repository; diff --git a/src/applications/diffusion/controller/DiffusionRepositoryController.php b/src/applications/diffusion/controller/DiffusionRepositoryController.php index 12aaefe12a..f00521257e 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryController.php @@ -145,13 +145,26 @@ final class DiffusionRepositoryController extends DiffusionController { ->setRight(array($this->branchButton, $actions_button, $clone_button)) ->addClass('diffusion-action-bar'); + $status_view = null; + if ($repository->isReadOnly()) { + $status_view = id(new PHUIInfoView()) + ->setSeverity(PHUIInfoView::SEVERITY_WARNING) + ->setErrors( + array( + phutil_escape_html_newlines( + $repository->getReadOnlyMessageForDisplay()), + )); + } + $view = id(new PHUITwoColumnView()) ->setHeader($header) - ->setFooter(array( - $bar, - $description, - $content, - )); + ->setFooter( + array( + $status_view, + $bar, + $description, + $content, + )); if ($page_has_content) { $view->setTabs($tabs); @@ -327,6 +340,8 @@ final class DiffusionRepositoryController extends DiffusionController { if (!$repository->isTracked()) { $header->setStatus('fa-ban', 'dark', pht('Inactive')); + } else if ($repository->isReadOnly()) { + $header->setStatus('fa-wrench', 'indigo', pht('Under Maintenance')); } else if ($repository->isImporting()) { $ratio = $repository->loadImportProgress(); $percentage = sprintf('%.2f%%', 100 * $ratio); diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditDeleteController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditDeleteController.php index 0380b99f0f..93fe70c0fe 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryEditDeleteController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryEditDeleteController.php @@ -17,32 +17,31 @@ final class DiffusionRepositoryEditDeleteController ->setRepository($repository) ->getPanelURI(); - $dialog = new AphrontDialogView(); - $text_1 = pht( - 'If you really want to delete the repository, run this command from '. - 'the command line:'); - $command = csprintf( - 'phabricator/ $ ./bin/remove destroy %R', - $repository->getMonogram()); - $text_2 = pht( - 'Repositories touch many objects and as such deletes are '. - 'prohibitively expensive to run from the web UI.'); - $body = phutil_tag( - 'div', - array( - 'class' => 'phabricator-remarkup', - ), - array( - phutil_tag('p', array(), $text_1), - phutil_tag('p', array(), - phutil_tag('tt', array(), $command)), - phutil_tag('p', array(), $text_2), - )); + $doc_uri = PhabricatorEnv::getDoclink( + 'Permanently Destroying Data'); return $this->newDialog() - ->setTitle(pht('Really want to delete the repository?')) - ->appendChild($body) - ->addCancelButton($panel_uri, pht('Okay')); + ->setTitle(pht('Delete Repository')) + ->appendParagraph( + pht( + 'To permanently destroy this repository, run this command from '. + 'the command line:')) + ->appendCommand( + csprintf( + 'phabricator/ $ ./bin/remove destroy %R', + $repository->getMonogram())) + ->appendParagraph( + pht( + 'Repositories can not be permanently destroyed from the web '. + 'interface. See %s in the documentation for more information.', + phutil_tag( + 'a', + array( + 'href' => $doc_uri, + 'target' => '_blank', + ), + pht('Permanently Destroying Data')))) + ->addCancelButton($panel_uri, pht('Close')); } } diff --git a/src/applications/diffusion/controller/DiffusionServeController.php b/src/applications/diffusion/controller/DiffusionServeController.php index aea901f100..60d5c1578d 100644 --- a/src/applications/diffusion/controller/DiffusionServeController.php +++ b/src/applications/diffusion/controller/DiffusionServeController.php @@ -302,6 +302,12 @@ final class DiffusionServeController extends DiffusionController { } if ($is_push) { + if ($repository->isReadOnly()) { + return new PhabricatorVCSResponse( + 503, + $repository->getReadOnlyMessageForDisplay()); + } + $can_write = $repository->canServeProtocol($proto_https, true) || $repository->canServeProtocol($proto_http, true); diff --git a/src/applications/diffusion/editor/DiffusionCommitEditEngine.php b/src/applications/diffusion/editor/DiffusionCommitEditEngine.php index 5b7277000f..6e45241d68 100644 --- a/src/applications/diffusion/editor/DiffusionCommitEditEngine.php +++ b/src/applications/diffusion/editor/DiffusionCommitEditEngine.php @@ -48,7 +48,8 @@ final class DiffusionCommitEditEngine return id(new DiffusionCommitQuery()) ->needCommitData(true) ->needAuditRequests(true) - ->needAuditAuthority(array($viewer)); + ->needAuditAuthority(array($viewer)) + ->needIdentities(true); } protected function getEditorURI() { diff --git a/src/applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php b/src/applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php index 2d006136e3..02df82c7c0 100644 --- a/src/applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php +++ b/src/applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php @@ -155,8 +155,6 @@ final class DiffusionRepositoryBasicsManagementPanel ->setName(pht('Delete Repository')) ->setHref($delete_uri) ->setIcon('fa-times') - ->setColor(PhabricatorActionView::RED) - ->setDisabled(true) ->setWorkflow(true)); return $this->newCurtainView() diff --git a/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php index 57dc83953d..08144eb0c9 100644 --- a/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php +++ b/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php @@ -255,6 +255,10 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow { 'user account.')); } + if ($repository->isReadOnly()) { + throw new Exception($repository->getReadOnlyMessageForDisplay()); + } + $protocol = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_SSH; if ($repository->canServeProtocol($protocol, true)) { $can_push = PhabricatorPolicyFilter::hasCapability( diff --git a/src/applications/diffusion/view/DiffusionPatternSearchView.php b/src/applications/diffusion/view/DiffusionPatternSearchView.php index 93ef42d46d..39a1df4e89 100644 --- a/src/applications/diffusion/view/DiffusionPatternSearchView.php +++ b/src/applications/diffusion/view/DiffusionPatternSearchView.php @@ -47,7 +47,7 @@ final class DiffusionPatternSearchView extends DiffusionView { $offset = $match[1]; if ($cursor != $offset) { $output[] = array( - 'text' => substr($string, $cursor, $offset), + 'text' => substr($string, $cursor, ($offset - $cursor)), 'highlight' => false, ); } diff --git a/src/applications/drydock/conduit/DrydockResourceSearchConduitAPIMethod.php b/src/applications/drydock/conduit/DrydockResourceSearchConduitAPIMethod.php new file mode 100644 index 0000000000..03363b0542 --- /dev/null +++ b/src/applications/drydock/conduit/DrydockResourceSearchConduitAPIMethod.php @@ -0,0 +1,18 @@ +withOwnerPHIDs($map['ownerPHIDs']); } + if ($map['resourcePHIDs']) { + $query->withResourcePHIDs($map['resourcePHIDs']); + } + return $query; } @@ -58,6 +62,11 @@ final class DrydockLeaseSearchEngine ->setKey('ownerPHIDs') ->setAliases(array('owner', 'owners', 'ownerPHID')) ->setDescription(pht('Search leases by owner.')), + id(new PhabricatorPHIDsSearchField()) + ->setLabel(pht('Resources')) + ->setKey('resourcePHIDs') + ->setAliases(array('resorucePHID', 'resource', 'resources')) + ->setDescription(pht('Search leases by resource.')), ); } diff --git a/src/applications/drydock/query/DrydockResourceQuery.php b/src/applications/drydock/query/DrydockResourceQuery.php index c477da20b5..bcbff03663 100644 --- a/src/applications/drydock/query/DrydockResourceQuery.php +++ b/src/applications/drydock/query/DrydockResourceQuery.php @@ -100,46 +100,50 @@ final class DrydockResourceQuery extends DrydockQuery { if ($this->ids !== null) { $where[] = qsprintf( $conn, - 'id IN (%Ld)', + 'resource.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, - 'phid IN (%Ls)', + 'resource.phid IN (%Ls)', $this->phids); } if ($this->types !== null) { $where[] = qsprintf( $conn, - 'type IN (%Ls)', + 'resource.type IN (%Ls)', $this->types); } if ($this->statuses !== null) { $where[] = qsprintf( $conn, - 'status IN (%Ls)', + 'resource.status IN (%Ls)', $this->statuses); } if ($this->blueprintPHIDs !== null) { $where[] = qsprintf( $conn, - 'blueprintPHID IN (%Ls)', + 'resource.blueprintPHID IN (%Ls)', $this->blueprintPHIDs); } if ($this->datasourceQuery !== null) { $where[] = qsprintf( $conn, - 'name LIKE %>', + 'resource.name LIKE %>', $this->datasourceQuery); } return $where; } + protected function getPrimaryTableAlias() { + return 'resource'; + } + } diff --git a/src/applications/drydock/query/DrydockResourceSearchEngine.php b/src/applications/drydock/query/DrydockResourceSearchEngine.php index 8f72fdf217..9015f92408 100644 --- a/src/applications/drydock/query/DrydockResourceSearchEngine.php +++ b/src/applications/drydock/query/DrydockResourceSearchEngine.php @@ -40,6 +40,10 @@ final class DrydockResourceSearchEngine $query->withStatuses($map['statuses']); } + if ($map['blueprintPHIDs']) { + $query->withBlueprintPHIDs($map['blueprintPHIDs']); + } + return $query; } @@ -49,6 +53,12 @@ final class DrydockResourceSearchEngine ->setLabel(pht('Statuses')) ->setKey('statuses') ->setOptions(DrydockResourceStatus::getStatusMap()), + id(new PhabricatorPHIDsSearchField()) + ->setLabel(pht('Blueprints')) + ->setKey('blueprintPHIDs') + ->setAliases(array('blueprintPHID', 'blueprints', 'blueprint')) + ->setDescription( + pht('Search for resources generated by particular blueprints.')), ); } diff --git a/src/applications/drydock/storage/DrydockResource.php b/src/applications/drydock/storage/DrydockResource.php index 8ec63cb097..bc672dba3c 100644 --- a/src/applications/drydock/storage/DrydockResource.php +++ b/src/applications/drydock/storage/DrydockResource.php @@ -1,7 +1,9 @@ setKey('blueprintPHID') + ->setType('phid') + ->setDescription(pht('The blueprint which generated this resource.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('status') + ->setType('map') + ->setDescription(pht('Information about resource status.')), + ); + } + + public function getFieldValuesForConduit() { + $status = $this->getStatus(); + + return array( + 'blueprintPHID' => $this->getBlueprintPHID(), + 'status' => array( + 'value' => $status, + 'name' => DrydockResourceStatus::getNameForStatus($status), + ), + ); + } + + public function getConduitSearchAttachments() { + return array(); + } + } diff --git a/src/applications/files/action/PhabricatorFilesOutboundRequestAction.php b/src/applications/files/action/PhabricatorFilesOutboundRequestAction.php index acba2f8882..7a1d3d2d56 100644 --- a/src/applications/files/action/PhabricatorFilesOutboundRequestAction.php +++ b/src/applications/files/action/PhabricatorFilesOutboundRequestAction.php @@ -5,10 +5,6 @@ final class PhabricatorFilesOutboundRequestAction const TYPECONST = 'files.outbound'; - public function getActionConstant() { - return self::TYPECONST; - } - public function getScoreThreshold() { return 60 / phutil_units('1 hour in seconds'); } diff --git a/src/applications/files/management/PhabricatorFilesManagementCompactWorkflow.php b/src/applications/files/management/PhabricatorFilesManagementCompactWorkflow.php index 0c8e7153e5..5d8115389b 100644 --- a/src/applications/files/management/PhabricatorFilesManagementCompactWorkflow.php +++ b/src/applications/files/management/PhabricatorFilesManagementCompactWorkflow.php @@ -4,41 +4,25 @@ final class PhabricatorFilesManagementCompactWorkflow extends PhabricatorFilesManagementWorkflow { protected function didConstruct() { + $arguments = $this->newIteratorArguments(); + $arguments[] = array( + 'name' => 'dry-run', + 'help' => pht('Show what would be compacted.'), + ); + $this ->setName('compact') ->setSynopsis( pht( 'Merge identical files to share the same storage. In some cases, '. 'this can repair files with missing data.')) - ->setArguments( - array( - array( - 'name' => 'dry-run', - 'help' => pht('Show what would be compacted.'), - ), - array( - 'name' => 'all', - 'help' => pht('Compact all files.'), - ), - array( - 'name' => 'names', - 'wildcard' => true, - ), - )); + ->setArguments($arguments); } public function execute(PhutilArgumentParser $args) { $console = PhutilConsole::getConsole(); $iterator = $this->buildIterator($args); - if (!$iterator) { - throw new PhutilArgumentUsageException( - pht( - 'Either specify a list of files to compact, or use `%s` '. - 'to compact all files.', - '--all')); - } - $is_dry_run = $args->getArg('dry-run'); foreach ($iterator as $file) { diff --git a/src/applications/files/management/PhabricatorFilesManagementCycleWorkflow.php b/src/applications/files/management/PhabricatorFilesManagementCycleWorkflow.php index 6d574d633a..cbcfac2cc8 100644 --- a/src/applications/files/management/PhabricatorFilesManagementCycleWorkflow.php +++ b/src/applications/files/management/PhabricatorFilesManagementCycleWorkflow.php @@ -4,36 +4,22 @@ final class PhabricatorFilesManagementCycleWorkflow extends PhabricatorFilesManagementWorkflow { protected function didConstruct() { + $arguments = $this->newIteratorArguments(); + $arguments[] = array( + 'name' => 'key', + 'param' => 'keyname', + 'help' => pht('Select a specific storage key to cycle to.'), + ); + $this ->setName('cycle') ->setSynopsis( pht('Cycle master key for encrypted files.')) - ->setArguments( - array( - array( - 'name' => 'key', - 'param' => 'keyname', - 'help' => pht('Select a specific storage key to cycle to.'), - ), - array( - 'name' => 'all', - 'help' => pht('Change encoding for all files.'), - ), - array( - 'name' => 'names', - 'wildcard' => true, - ), - )); + ->setArguments($arguments); } public function execute(PhutilArgumentParser $args) { $iterator = $this->buildIterator($args); - if (!$iterator) { - throw new PhutilArgumentUsageException( - pht( - 'Either specify a list of files to cycle, or use --all to cycle '. - 'all files.')); - } $format_map = PhabricatorFileStorageFormat::getAllFormats(); $engines = PhabricatorFileStorageEngine::loadAllEngines(); diff --git a/src/applications/files/management/PhabricatorFilesManagementEncodeWorkflow.php b/src/applications/files/management/PhabricatorFilesManagementEncodeWorkflow.php index 1d972326da..f7d299bc5f 100644 --- a/src/applications/files/management/PhabricatorFilesManagementEncodeWorkflow.php +++ b/src/applications/files/management/PhabricatorFilesManagementEncodeWorkflow.php @@ -4,47 +4,36 @@ final class PhabricatorFilesManagementEncodeWorkflow extends PhabricatorFilesManagementWorkflow { protected function didConstruct() { + $arguments = $this->newIteratorArguments(); + + $arguments[] = array( + 'name' => 'as', + 'param' => 'format', + 'help' => pht('Select the storage format to use.'), + ); + + $arguments[] = array( + 'name' => 'key', + 'param' => 'keyname', + 'help' => pht('Select a specific storage key.'), + ); + + $arguments[] = array( + 'name' => 'force', + 'help' => pht( + 'Re-encode files which are already stored in the target '. + 'encoding.'), + ); + $this ->setName('encode') ->setSynopsis( pht('Change the storage encoding of files.')) - ->setArguments( - array( - array( - 'name' => 'as', - 'param' => 'format', - 'help' => pht('Select the storage format to use.'), - ), - array( - 'name' => 'key', - 'param' => 'keyname', - 'help' => pht('Select a specific storage key.'), - ), - array( - 'name' => 'all', - 'help' => pht('Change encoding for all files.'), - ), - array( - 'name' => 'force', - 'help' => pht( - 'Re-encode files which are already stored in the target '. - 'encoding.'), - ), - array( - 'name' => 'names', - 'wildcard' => true, - ), - )); + ->setArguments($arguments); } public function execute(PhutilArgumentParser $args) { $iterator = $this->buildIterator($args); - if (!$iterator) { - throw new PhutilArgumentUsageException( - pht( - 'Either specify a list of files to encode, or use --all to '. - 'encode all files.')); - } $force = (bool)$args->getArg('force'); diff --git a/src/applications/files/management/PhabricatorFilesManagementIntegrityWorkflow.php b/src/applications/files/management/PhabricatorFilesManagementIntegrityWorkflow.php index 344df71460..a30f4f970b 100644 --- a/src/applications/files/management/PhabricatorFilesManagementIntegrityWorkflow.php +++ b/src/applications/files/management/PhabricatorFilesManagementIntegrityWorkflow.php @@ -4,52 +4,50 @@ final class PhabricatorFilesManagementIntegrityWorkflow extends PhabricatorFilesManagementWorkflow { protected function didConstruct() { + $arguments = $this->newIteratorArguments(); + + $arguments[] = array( + 'name' => 'strip', + 'help' => pht( + 'DANGEROUS. Strip integrity hashes from files. This makes '. + 'files vulnerable to corruption or tampering.'), + ); + + $arguments[] = array( + 'name' => 'corrupt', + 'help' => pht( + 'Corrupt integrity hashes for given files. This is intended '. + 'for debugging.'), + ); + + $arguments[] = array( + 'name' => 'compute', + 'help' => pht( + 'Compute and update integrity hashes for files which do not '. + 'yet have them.'), + ); + + $arguments[] = array( + 'name' => 'overwrite', + 'help' => pht( + 'DANGEROUS. Recompute and update integrity hashes, overwriting '. + 'invalid hashes. This may mark corrupt or dangerous files as '. + 'valid.'), + ); + + $arguments[] = array( + 'name' => 'force', + 'short' => 'f', + 'help' => pht( + 'Execute dangerous operations without prompting for '. + 'confirmation.'), + ); + + $this ->setName('integrity') ->setSynopsis(pht('Verify or recalculate file integrity hashes.')) - ->setArguments( - array( - array( - 'name' => 'all', - 'help' => pht('Affect all files.'), - ), - array( - 'name' => 'strip', - 'help' => pht( - 'DANGEROUS. Strip integrity hashes from files. This makes '. - 'files vulnerable to corruption or tampering.'), - ), - array( - 'name' => 'corrupt', - 'help' => pht( - 'Corrupt integrity hashes for given files. This is intended '. - 'for debugging.'), - ), - array( - 'name' => 'compute', - 'help' => pht( - 'Compute and update integrity hashes for files which do not '. - 'yet have them.'), - ), - array( - 'name' => 'overwrite', - 'help' => pht( - 'DANGEROUS. Recompute and update integrity hashes, overwriting '. - 'invalid hashes. This may mark corrupt or dangerous files as '. - 'valid.'), - ), - array( - 'name' => 'force', - 'short' => 'f', - 'help' => pht( - 'Execute dangerous operations without prompting for '. - 'confirmation.'), - ), - array( - 'name' => 'names', - 'wildcard' => true, - ), - )); + ->setArguments($arguments); } public function execute(PhutilArgumentParser $args) { @@ -120,12 +118,6 @@ final class PhabricatorFilesManagementIntegrityWorkflow } $iterator = $this->buildIterator($args); - if (!$iterator) { - throw new PhutilArgumentUsageException( - pht( - 'Either specify a list of files to affect, or use "--all" to '. - 'affect all files.')); - } $failure_count = 0; $total_count = 0; diff --git a/src/applications/files/management/PhabricatorFilesManagementMigrateWorkflow.php b/src/applications/files/management/PhabricatorFilesManagementMigrateWorkflow.php index 58d5155aed..859ee0bf2d 100644 --- a/src/applications/files/management/PhabricatorFilesManagementMigrateWorkflow.php +++ b/src/applications/files/management/PhabricatorFilesManagementMigrateWorkflow.php @@ -4,61 +4,54 @@ final class PhabricatorFilesManagementMigrateWorkflow extends PhabricatorFilesManagementWorkflow { protected function didConstruct() { + $arguments = $this->newIteratorArguments(); + + $arguments[] = array( + 'name' => 'engine', + 'param' => 'storage-engine', + 'help' => pht('Migrate to the named storage engine.'), + ); + + $arguments[] = array( + 'name' => 'dry-run', + 'help' => pht('Show what would be migrated.'), + ); + + $arguments[] = array( + 'name' => 'min-size', + 'param' => 'bytes', + 'help' => pht( + 'Do not migrate data for files which are smaller than a given '. + 'filesize.'), + ); + + $arguments[] = array( + 'name' => 'max-size', + 'param' => 'bytes', + 'help' => pht( + 'Do not migrate data for files which are larger than a given '. + 'filesize.'), + ); + + $arguments[] = array( + 'name' => 'copy', + 'help' => pht( + 'Copy file data instead of moving it: after migrating, do not '. + 'remove the old data even if it is no longer referenced.'), + ); + + $arguments[] = array( + 'name' => 'local-disk-source', + 'param' => 'path', + 'help' => pht( + 'When migrating from a local disk source, use the specified '. + 'path as the root directory.'), + ); + $this ->setName('migrate') ->setSynopsis(pht('Migrate files between storage engines.')) - ->setArguments( - array( - array( - 'name' => 'engine', - 'param' => 'storage_engine', - 'help' => pht('Migrate to the named storage engine.'), - ), - array( - 'name' => 'dry-run', - 'help' => pht('Show what would be migrated.'), - ), - array( - 'name' => 'min-size', - 'param' => 'bytes', - 'help' => pht( - 'Do not migrate data for files which are smaller than a given '. - 'filesize.'), - ), - array( - 'name' => 'max-size', - 'param' => 'bytes', - 'help' => pht( - 'Do not migrate data for files which are larger than a given '. - 'filesize.'), - ), - array( - 'name' => 'all', - 'help' => pht('Migrate all files.'), - ), - array( - 'name' => 'copy', - 'help' => pht( - 'Copy file data instead of moving it: after migrating, do not '. - 'remove the old data even if it is no longer referenced.'), - ), - array( - 'name' => 'names', - 'wildcard' => true, - ), - array( - 'name' => 'from-engine', - 'param' => 'engine', - 'help' => pht('Migrate files from the named storage engine.'), - ), - array( - 'name' => 'local-disk-source', - 'param' => 'path', - 'help' => pht( - 'When migrating from a local disk source, use the specified '. - 'path as the root directory.'), - ), - )); + ->setArguments($arguments); } public function execute(PhutilArgumentParser $args) { @@ -97,14 +90,6 @@ final class PhabricatorFilesManagementMigrateWorkflow $target_engine = PhabricatorFile::buildEngine($target_key); $iterator = $this->buildIterator($args); - if (!$iterator) { - throw new PhutilArgumentUsageException( - pht( - 'Either specify a list of files to migrate, or use `%s` '. - 'to migrate all files.', - '--all')); - } - $is_dry_run = $args->getArg('dry-run'); $min_size = (int)$args->getArg('min-size'); diff --git a/src/applications/files/management/PhabricatorFilesManagementRebuildWorkflow.php b/src/applications/files/management/PhabricatorFilesManagementRebuildWorkflow.php index f7fc890ae4..e577a04d55 100644 --- a/src/applications/files/management/PhabricatorFilesManagementRebuildWorkflow.php +++ b/src/applications/files/management/PhabricatorFilesManagementRebuildWorkflow.php @@ -4,45 +4,33 @@ final class PhabricatorFilesManagementRebuildWorkflow extends PhabricatorFilesManagementWorkflow { protected function didConstruct() { + $arguments = $this->newIteratorArguments(); + + $arguments[] = array( + 'name' => 'dry-run', + 'help' => pht('Show what would be updated.'), + ); + + $arguments[] = array( + 'name' => 'rebuild-mime', + 'help' => pht('Rebuild MIME information.'), + ); + + $arguments[] = array( + 'name' => 'rebuild-dimensions', + 'help' => pht('Rebuild image dimension information.'), + ); + $this ->setName('rebuild') ->setSynopsis(pht('Rebuild metadata of old files.')) - ->setArguments( - array( - array( - 'name' => 'all', - 'help' => pht('Update all files.'), - ), - array( - 'name' => 'dry-run', - 'help' => pht('Show what would be updated.'), - ), - array( - 'name' => 'rebuild-mime', - 'help' => pht('Rebuild MIME information.'), - ), - array( - 'name' => 'rebuild-dimensions', - 'help' => pht('Rebuild image dimension information.'), - ), - array( - 'name' => 'names', - 'wildcard' => true, - ), - )); + ->setArguments($arguments); } public function execute(PhutilArgumentParser $args) { $console = PhutilConsole::getConsole(); $iterator = $this->buildIterator($args); - if (!$iterator) { - throw new PhutilArgumentUsageException( - pht( - 'Either specify a list of files to update, or use `%s` '. - 'to update all files.', - '--all')); - } $update = array( 'mime' => $args->getArg('rebuild-mime'), diff --git a/src/applications/files/management/PhabricatorFilesManagementWorkflow.php b/src/applications/files/management/PhabricatorFilesManagementWorkflow.php index 44d43dc66a..6debf9c711 100644 --- a/src/applications/files/management/PhabricatorFilesManagementWorkflow.php +++ b/src/applications/files/management/PhabricatorFilesManagementWorkflow.php @@ -3,11 +3,30 @@ abstract class PhabricatorFilesManagementWorkflow extends PhabricatorManagementWorkflow { + protected function newIteratorArguments() { + return array( + array( + 'name' => 'all', + 'help' => pht('Operate on all files.'), + ), + array( + 'name' => 'names', + 'wildcard' => true, + ), + array( + 'name' => 'from-engine', + 'param' => 'storage-engine', + 'help' => pht('Operate on files stored in a specified engine.'), + ), + ); + } + protected function buildIterator(PhutilArgumentParser $args) { $viewer = $this->getViewer(); - $names = $args->getArg('names'); $is_all = $args->getArg('all'); + + $names = $args->getArg('names'); $from_engine = $args->getArg('from-engine'); $any_constraint = ($from_engine || $names); @@ -15,15 +34,16 @@ abstract class PhabricatorFilesManagementWorkflow if (!$is_all && !$any_constraint) { throw new PhutilArgumentUsageException( pht( - 'Use "--all" to migrate all files, or choose files to migrate '. - 'with "--names" or "--from-engine".')); + 'Specify which files to operate on, or use "--all" to operate on '. + 'all files.')); } if ($is_all && $any_constraint) { throw new PhutilArgumentUsageException( pht( - 'You can not migrate all files with "--all" and also migrate only '. - 'a subset of files with "--from-engine" or "--names".')); + 'You can not operate on all files with "--all" and also operate '. + 'on a subset of files by naming them explicitly or using '. + 'constraint flags like "--from-engine".')); } // If we're migrating specific named files, convert the names into IDs diff --git a/src/applications/flag/query/PhabricatorFlagQuery.php b/src/applications/flag/query/PhabricatorFlagQuery.php index 3418f10746..c6c905465d 100644 --- a/src/applications/flag/query/PhabricatorFlagQuery.php +++ b/src/applications/flag/query/PhabricatorFlagQuery.php @@ -6,6 +6,7 @@ final class PhabricatorFlagQuery const GROUP_COLOR = 'color'; const GROUP_NONE = 'none'; + private $ids; private $ownerPHIDs; private $types; private $objectPHIDs; @@ -15,6 +16,11 @@ final class PhabricatorFlagQuery private $needHandles; private $needObjects; + public function withIDs(array $ids) { + $this->ids = $ids; + return $this; + } + public function withOwnerPHIDs(array $owner_phids) { $this->ownerPHIDs = $owner_phids; return $this; @@ -126,6 +132,13 @@ final class PhabricatorFlagQuery protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'flag.id IN (%Ld)', + $this->ids); + } + if ($this->ownerPHIDs) { $where[] = qsprintf( $conn, diff --git a/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php b/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php index b3f60cd1dc..b701274eb0 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php @@ -24,26 +24,27 @@ final class HarbormasterBuildableActionController $issuable = array(); - foreach ($buildable->getBuilds() as $build) { + $builds = $buildable->getBuilds(); + foreach ($builds as $key => $build) { switch ($action) { case HarbormasterBuildCommand::COMMAND_RESTART: if ($build->canRestartBuild()) { - $issuable[] = $build; + $issuable[$key] = $build; } break; case HarbormasterBuildCommand::COMMAND_PAUSE: if ($build->canPauseBuild()) { - $issuable[] = $build; + $issuable[$key] = $build; } break; case HarbormasterBuildCommand::COMMAND_RESUME: if ($build->canResumeBuild()) { - $issuable[] = $build; + $issuable[$key] = $build; } break; case HarbormasterBuildCommand::COMMAND_ABORT: if ($build->canAbortBuild()) { - $issuable[] = $build; + $issuable[$key] = $build; } break; default: @@ -59,6 +60,14 @@ final class HarbormasterBuildableActionController } } + $building = false; + foreach ($issuable as $key => $build) { + if ($build->isBuilding()) { + $building = true; + break; + } + } + $return_uri = '/'.$buildable->getMonogram(); if ($request->isDialogFormPost() && $issuable) { $editor = id(new HarbormasterBuildableTransactionEditor()) @@ -89,34 +98,137 @@ final class HarbormasterBuildableActionController return id(new AphrontRedirectResponse())->setURI($return_uri); } + $width = AphrontDialogView::WIDTH_DEFAULT; + switch ($action) { case HarbormasterBuildCommand::COMMAND_RESTART: + // See T13348. The "Restart Builds" action may restart only a subset + // of builds, so show the user a preview of which builds will actually + // restart. + + $body = array(); + if ($issuable) { - $title = pht('Really restart builds?'); - - if ($restricted) { - $body = pht( - 'You only have permission to restart some builds. Progress '. - 'on builds you have permission to restart will be discarded '. - 'and they will restart. Side effects of these builds will '. - 'occur again. Really restart all builds?'); - } else { - $body = pht( - 'Progress on all builds will be discarded, and all builds will '. - 'restart. Side effects of the builds will occur again. Really '. - 'restart all builds?'); - } - + $title = pht('Restart Builds'); $submit = pht('Restart Builds'); } else { $title = pht('Unable to Restart Builds'); + } + + if ($builds) { + $width = AphrontDialogView::WIDTH_FORM; + + $body[] = pht('Builds for this buildable:'); + + $rows = array(); + foreach ($builds as $key => $build) { + if (isset($issuable[$key])) { + $icon = id(new PHUIIconView()) + ->setIcon('fa-repeat green'); + $build_note = pht('Will Restart'); + } else { + $icon = null; + + try { + $build->assertCanRestartBuild(); + } catch (HarbormasterRestartException $ex) { + $icon = id(new PHUIIconView()) + ->setIcon('fa-times red'); + $build_note = pht( + '%s: %s', + phutil_tag('strong', array(), pht('Not Restartable')), + $ex->getTitle()); + } + + if (!$icon) { + try { + $build->assertCanIssueCommand($viewer, $action); + } catch (PhabricatorPolicyException $ex) { + $icon = id(new PHUIIconView()) + ->setIcon('fa-lock red'); + $build_note = pht( + '%s: %s', + phutil_tag('strong', array(), pht('Not Restartable')), + pht('You do not have permission to restart this build.')); + } + } + + if (!$icon) { + $icon = id(new PHUIIconView()) + ->setIcon('fa-times red'); + $build_note = pht('Will Not Restart'); + } + } + + $build_name = phutil_tag( + 'a', + array( + 'href' => $build->getURI(), + 'target' => '_blank', + ), + pht('%s %s', $build->getObjectName(), $build->getName())); + + $rows[] = array( + $icon, + $build_name, + $build_note, + ); + } + + $table = id(new AphrontTableView($rows)) + ->setHeaders( + array( + null, + pht('Build'), + pht('Action'), + )) + ->setColumnClasses( + array( + null, + 'pri', + 'wide', + )); + + $table = phutil_tag( + 'div', + array( + 'class' => 'mlt mlb', + ), + $table); + + $body[] = $table; + } + + if ($issuable) { + $warnings = array(); if ($restricted) { - $body = pht('You do not have permission to restart any builds.'); + $warnings[] = pht( + 'You only have permission to restart some builds.'); + } + + if ($building) { + $warnings[] = pht( + 'Progress on running builds will be discarded.'); + } + + $warnings[] = pht( + 'When a build is restarted, side effects associated with '. + 'the build may occur again.'); + + $body[] = id(new PHUIInfoView()) + ->setSeverity(PHUIInfoView::SEVERITY_WARNING) + ->setErrors($warnings); + + $body[] = pht('Really restart builds?'); + } else { + if ($restricted) { + $body[] = pht('You do not have permission to restart any builds.'); } else { - $body = pht('No builds can be restarted.'); + $body[] = pht('No builds can be restarted.'); } } + break; case HarbormasterBuildCommand::COMMAND_PAUSE: if ($issuable) { @@ -193,6 +305,7 @@ final class HarbormasterBuildableActionController $dialog = id(new AphrontDialogView()) ->setUser($viewer) + ->setWidth($width) ->setTitle($title) ->appendChild($body) ->addCancelButton($return_uri); diff --git a/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php index 40f6587116..aa433be656 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php @@ -128,7 +128,7 @@ final class HarbormasterBuildableViewController $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-repeat') - ->setName(pht('Restart All Builds')) + ->setName(pht('Restart Builds')) ->setHref($this->getApplicationURI($restart_uri)) ->setWorkflow(true) ->setDisabled(!$can_restart || !$can_edit)); @@ -136,7 +136,7 @@ final class HarbormasterBuildableViewController $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pause') - ->setName(pht('Pause All Builds')) + ->setName(pht('Pause Builds')) ->setHref($this->getApplicationURI($pause_uri)) ->setWorkflow(true) ->setDisabled(!$can_pause || !$can_edit)); @@ -144,7 +144,7 @@ final class HarbormasterBuildableViewController $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-play') - ->setName(pht('Resume All Builds')) + ->setName(pht('Resume Builds')) ->setHref($this->getApplicationURI($resume_uri)) ->setWorkflow(true) ->setDisabled(!$can_resume || !$can_edit)); @@ -152,7 +152,7 @@ final class HarbormasterBuildableViewController $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-exclamation-triangle') - ->setName(pht('Abort All Builds')) + ->setName(pht('Abort Builds')) ->setHref($this->getApplicationURI($abort_uri)) ->setWorkflow(true) ->setDisabled(!$can_abort || !$can_edit)); diff --git a/src/applications/harbormaster/engine/HarbormasterBuildGraph.php b/src/applications/harbormaster/engine/HarbormasterBuildGraph.php index dc9e4b7691..64c12c2e8f 100644 --- a/src/applications/harbormaster/engine/HarbormasterBuildGraph.php +++ b/src/applications/harbormaster/engine/HarbormasterBuildGraph.php @@ -25,8 +25,7 @@ final class HarbormasterBuildGraph extends AbstractDirectedGraph { $graph = id(new HarbormasterBuildGraph($steps_by_phid)) ->addNodes($step_phids); - $raw_results = - $graph->getBestEffortTopographicallySortedNodes(); + $raw_results = $graph->getNodesInRoughTopologicalOrder(); $results = array(); foreach ($raw_results as $node) { diff --git a/src/applications/herald/xaction/HeraldRuleEditTransaction.php b/src/applications/herald/xaction/HeraldRuleEditTransaction.php index c4b03983fb..653d300906 100644 --- a/src/applications/herald/xaction/HeraldRuleEditTransaction.php +++ b/src/applications/herald/xaction/HeraldRuleEditTransaction.php @@ -40,17 +40,10 @@ final class HeraldRuleEditTransaction public function newChangeDetailView() { $viewer = $this->getViewer(); - $old = $this->getOldValue(); - $new = $this->getNewValue(); - - $json = new PhutilJSON(); - $old_json = $json->encodeFormatted($old); - $new_json = $json->encodeFormatted($new); - - return id(new PhabricatorApplicationTransactionTextDiffDetailView()) + return id(new PhabricatorApplicationTransactionJSONDiffDetailView()) ->setViewer($viewer) - ->setOldText($old_json) - ->setNewText($new_json); + ->setOld($this->getOldValue()) + ->setNew($this->getNewValue()); } } diff --git a/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php b/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php index f1916cffec..077fc511ce 100644 --- a/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php +++ b/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php @@ -451,15 +451,20 @@ You can choose the default priority for newly created tasks with EOTEXT )); + $fields_description = $this->deformat(pht(<<newOption('maniphest.custom-field-definitions', 'wild', array()) ->setSummary(pht('Custom Maniphest fields.')) - ->setDescription( - pht( - 'Array of custom fields for Maniphest tasks. For details on '. - 'adding custom fields to Maniphest, see "Configuring Custom '. - 'Fields" in the documentation.')) + ->setDescription($fields_description) ->addExample($fields_json, pht('Valid setting')), $this->newOption('maniphest.fields', $custom_field_type, $default_fields) ->setCustomData(id(new ManiphestTask())->getCustomFieldBaseClass()) diff --git a/src/applications/maniphest/editor/ManiphestEditEngine.php b/src/applications/maniphest/editor/ManiphestEditEngine.php index 426ac89b04..19080b5e59 100644 --- a/src/applications/maniphest/editor/ManiphestEditEngine.php +++ b/src/applications/maniphest/editor/ManiphestEditEngine.php @@ -264,6 +264,7 @@ EODOCS $parent_type = ManiphestTaskDependedOnByTaskEdgeType::EDGECONST; $subtask_type = ManiphestTaskDependsOnTaskEdgeType::EDGECONST; + $commit_type = ManiphestTaskHasCommitEdgeType::EDGECONST; $src_phid = $object->getPHID(); if ($src_phid) { @@ -273,6 +274,7 @@ EODOCS array( $parent_type, $subtask_type, + $commit_type, )); $edge_query->execute(); @@ -283,9 +285,14 @@ EODOCS $subtask_phids = $edge_query->getDestinationPHIDs( array($src_phid), array($subtask_type)); + + $commit_phids = $edge_query->getDestinationPHIDs( + array($src_phid), + array($commit_type)); } else { $parent_phids = array(); $subtask_phids = array(); + $commit_phids = array(); } $fields[] = id(new PhabricatorHandlesEditField()) @@ -310,7 +317,19 @@ EODOCS ->setIsFormField(false) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $subtask_type) - ->setValue($parent_phids); + ->setValue($subtask_phids); + + $fields[] = id(new PhabricatorHandlesEditField()) + ->setKey('commits') + ->setLabel(pht('Commits')) + ->setDescription(pht('Related commits.')) + ->setConduitDescription(pht('Change the related commits for this task.')) + ->setConduitTypeDescription(pht('List of related commit PHIDs.')) + ->setUseEdgeTransactions(true) + ->setIsFormField(false) + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue('edge:type', $commit_type) + ->setValue($commit_phids); return $fields; } @@ -437,7 +456,7 @@ EODOCS $engine = id(new PhabricatorBoardResponseEngine()) ->setViewer($viewer) ->setBoardPHID($board_phid) - ->setObjectPHID($object_phid) + ->setUpdatePHIDs(array($object_phid)) ->setVisiblePHIDs($visible_phids); if ($ordering) { diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php index 6255903eff..ed98ad8ad8 100644 --- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php +++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php @@ -3,6 +3,7 @@ final class ManiphestTransactionEditor extends PhabricatorApplicationTransactionEditor { + private $oldProjectPHIDs; private $moreValidationErrors = array(); public function getEditorApplicationClass() { @@ -378,6 +379,11 @@ final class ManiphestTransactionEditor } } + $send_notifications = PhabricatorNotificationClient::isEnabled(); + if ($send_notifications) { + $this->oldProjectPHIDs = $this->loadProjectPHIDs($object); + } + return $results; } @@ -859,4 +865,71 @@ final class ManiphestTransactionEditor return array_values($phid_list); } + protected function didApplyTransactions($object, array $xactions) { + $send_notifications = PhabricatorNotificationClient::isEnabled(); + if ($send_notifications) { + $old_phids = $this->oldProjectPHIDs; + $new_phids = $this->loadProjectPHIDs($object); + + // We want to emit update notifications for all old and new tagged + // projects, and all parents of those projects. For example, if an + // edit removes project "A > B" from a task, the "A" workboard should + // receive an update event. + + $project_phids = array_fuse($old_phids) + array_fuse($new_phids); + $project_phids = array_keys($project_phids); + + if ($project_phids) { + $projects = id(new PhabricatorProjectQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs($project_phids) + ->execute(); + + $notify_projects = array(); + foreach ($projects as $project) { + $notify_projects[$project->getPHID()] = $project; + foreach ($project->getAncestorProjects() as $ancestor) { + $notify_projects[$ancestor->getPHID()] = $ancestor; + } + } + + foreach ($notify_projects as $key => $project) { + if (!$project->getHasWorkboard()) { + unset($notify_projects[$key]); + } + } + + $notify_phids = array_keys($notify_projects); + + if ($notify_phids) { + $data = array( + 'type' => 'workboards', + 'subscribers' => $notify_phids, + ); + + PhabricatorNotificationClient::tryToPostMessage($data); + } + } + } + + return $xactions; + } + + private function loadProjectPHIDs(ManiphestTask $task) { + if (!$task->getPHID()) { + return array(); + } + + $edge_query = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs(array($task->getPHID())) + ->withEdgeTypes( + array( + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, + )); + + $edge_query->execute(); + + return $edge_query->getDestinationPHIDs(); + } + } diff --git a/src/applications/metamta/action/PhabricatorMetaMTAErrorMailAction.php b/src/applications/metamta/action/PhabricatorMetaMTAErrorMailAction.php index 0bc8b29172..cabb8c82b0 100644 --- a/src/applications/metamta/action/PhabricatorMetaMTAErrorMailAction.php +++ b/src/applications/metamta/action/PhabricatorMetaMTAErrorMailAction.php @@ -2,9 +2,7 @@ final class PhabricatorMetaMTAErrorMailAction extends PhabricatorSystemAction { - public function getActionConstant() { - return 'email.error'; - } + const TYPECONST = 'email.error'; public function getScoreThreshold() { return 6 / phutil_units('1 hour in seconds'); diff --git a/src/applications/notification/client/PhabricatorNotificationClient.php b/src/applications/notification/client/PhabricatorNotificationClient.php index ff5538dbcf..1cede1498d 100644 --- a/src/applications/notification/client/PhabricatorNotificationClient.php +++ b/src/applications/notification/client/PhabricatorNotificationClient.php @@ -37,4 +37,8 @@ final class PhabricatorNotificationClient extends Phobject { } } + public static function isEnabled() { + return (bool)PhabricatorNotificationServerRef::getEnabledAdminServers(); + } + } diff --git a/src/applications/oauthserver/controller/client/PhabricatorOAuthClientViewController.php b/src/applications/oauthserver/controller/client/PhabricatorOAuthClientViewController.php index 394ace52a4..6492f78f5a 100644 --- a/src/applications/oauthserver/controller/client/PhabricatorOAuthClientViewController.php +++ b/src/applications/oauthserver/controller/client/PhabricatorOAuthClientViewController.php @@ -15,12 +15,11 @@ final class PhabricatorOAuthClientViewController } $header = $this->buildHeaderView($client); - $actions = $this->buildActionView($client); $properties = $this->buildPropertyListView($client); - $properties->setActionList($actions); - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb($client->getName()); + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb($client->getName()) + ->setBorder(true); $timeline = $this->buildTransactionTimeline( $client, @@ -28,19 +27,27 @@ final class PhabricatorOAuthClientViewController $timeline->setShouldTerminate(true); $box = id(new PHUIObjectBoxView()) - ->setHeader($header) + ->setHeaderText(pht('Details')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->addPropertyList($properties); $title = pht('OAuth Application: %s', $client->getName()); - return $this->newPage() - ->setCrumbs($crumbs) - ->setTitle($title) - ->appendChild( + $curtain = $this->buildCurtain($client); + + $columns = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->setMainColumn( array( $box, $timeline, )); + + return $this->newPage() + ->setCrumbs($crumbs) + ->setTitle($title) + ->appendChild($columns); } private function buildHeaderView(PhabricatorOAuthServerClient $client) { @@ -60,8 +67,9 @@ final class PhabricatorOAuthClientViewController return $header; } - private function buildActionView(PhabricatorOAuthServerClient $client) { + private function buildCurtain(PhabricatorOAuthServerClient $client) { $viewer = $this->getViewer(); + $actions = array(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, @@ -70,24 +78,19 @@ final class PhabricatorOAuthClientViewController $id = $client->getID(); - $view = id(new PhabricatorActionListView()) - ->setUser($viewer); + $actions[] = id(new PhabricatorActionView()) + ->setName(pht('Edit Application')) + ->setIcon('fa-pencil') + ->setWorkflow(!$can_edit) + ->setDisabled(!$can_edit) + ->setHref($client->getEditURI()); - $view->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Edit Application')) - ->setIcon('fa-pencil') - ->setWorkflow(!$can_edit) - ->setDisabled(!$can_edit) - ->setHref($client->getEditURI())); - - $view->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Show Application Secret')) - ->setIcon('fa-eye') - ->setHref($this->getApplicationURI("client/secret/{$id}/")) - ->setDisabled(!$can_edit) - ->setWorkflow(true)); + $actions[] = id(new PhabricatorActionView()) + ->setName(pht('Show Application Secret')) + ->setIcon('fa-eye') + ->setHref($this->getApplicationURI("client/secret/{$id}/")) + ->setDisabled(!$can_edit) + ->setWorkflow(true); $is_disabled = $client->getIsDisabled(); if ($is_disabled) { @@ -100,22 +103,26 @@ final class PhabricatorOAuthClientViewController $disable_uri = $this->getApplicationURI("client/disable/{$id}/"); - $view->addAction( - id(new PhabricatorActionView()) - ->setName($disable_text) - ->setIcon($disable_icon) - ->setWorkflow(true) - ->setDisabled(!$can_edit) - ->setHref($disable_uri)); + $actions[] = id(new PhabricatorActionView()) + ->setName($disable_text) + ->setIcon($disable_icon) + ->setWorkflow(true) + ->setDisabled(!$can_edit) + ->setHref($disable_uri); - $view->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Generate Test Token')) - ->setIcon('fa-plus') - ->setWorkflow(true) - ->setHref($this->getApplicationURI("client/test/{$id}/"))); + $actions[] = id(new PhabricatorActionView()) + ->setName(pht('Generate Test Token')) + ->setIcon('fa-plus') + ->setWorkflow(true) + ->setHref($this->getApplicationURI("client/test/{$id}/")); - return $view; + $curtain = $this->newCurtainView($client); + + foreach ($actions as $action) { + $curtain->addAction($action); + } + + return $curtain; } private function buildPropertyListView(PhabricatorOAuthServerClient $client) { @@ -132,10 +139,6 @@ final class PhabricatorOAuthClientViewController pht('Redirect URI'), $client->getRedirectURI()); - $view->addProperty( - pht('Created'), - phabricator_datetime($client->getDateCreated(), $viewer)); - return $view; } } diff --git a/src/applications/oauthserver/phid/PhabricatorOAuthServerClientPHIDType.php b/src/applications/oauthserver/phid/PhabricatorOAuthServerClientPHIDType.php index a4d8834b96..4d3d64738b 100644 --- a/src/applications/oauthserver/phid/PhabricatorOAuthServerClientPHIDType.php +++ b/src/applications/oauthserver/phid/PhabricatorOAuthServerClientPHIDType.php @@ -32,7 +32,9 @@ final class PhabricatorOAuthServerClientPHIDType extends PhabricatorPHIDType { foreach ($handles as $phid => $handle) { $client = $objects[$phid]; - $handle->setName($client->getName()); + $handle + ->setName($client->getName()) + ->setURI($client->getURI()); } } diff --git a/src/applications/oauthserver/storage/PhabricatorOAuthServerClient.php b/src/applications/oauthserver/storage/PhabricatorOAuthServerClient.php index a951bf5781..471433ad4b 100644 --- a/src/applications/oauthserver/storage/PhabricatorOAuthServerClient.php +++ b/src/applications/oauthserver/storage/PhabricatorOAuthServerClient.php @@ -59,6 +59,12 @@ final class PhabricatorOAuthServerClient PhabricatorOAuthServerClientPHIDType::TYPECONST); } + public function getURI() { + return urisprintf( + '/oauthserver/client/view/%d/', + $this->getID()); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/owners/storage/PhabricatorOwnersPath.php b/src/applications/owners/storage/PhabricatorOwnersPath.php index 7022cb887c..5bb32f8407 100644 --- a/src/applications/owners/storage/PhabricatorOwnersPath.php +++ b/src/applications/owners/storage/PhabricatorOwnersPath.php @@ -79,15 +79,27 @@ final class PhabricatorOwnersPath extends PhabricatorOwnersDAO { public static function getSetFromTransactionValue(array $v) { $set = array(); foreach ($v as $ref) { - $set[$ref['repositoryPHID']][$ref['path']][$ref['excluded']] = true; + $key = self::getScalarKeyForRef($ref); + $set[$key] = true; } return $set; } public static function isRefInSet(array $ref, array $set) { - return isset($set[$ref['repositoryPHID']][$ref['path']][$ref['excluded']]); + $key = self::getScalarKeyForRef($ref); + return isset($set[$key]); } + private static function getScalarKeyForRef(array $ref) { + return sprintf( + 'repository=%s path=%s display=%s excluded=%d', + $ref['repositoryPHID'], + $ref['path'], + $ref['display'], + $ref['excluded']); + } + + /** * Get the number of directory matches between this path specification and * some real path. diff --git a/src/applications/owners/xaction/PhabricatorOwnersPackagePathsTransaction.php b/src/applications/owners/xaction/PhabricatorOwnersPackagePathsTransaction.php index 753b6ff9e9..a8bb1cc259 100644 --- a/src/applications/owners/xaction/PhabricatorOwnersPackagePathsTransaction.php +++ b/src/applications/owners/xaction/PhabricatorOwnersPackagePathsTransaction.php @@ -12,12 +12,33 @@ final class PhabricatorOwnersPackagePathsTransaction public function generateNewValue($object, $value) { $new = $value; + foreach ($new as $key => $info) { - $new[$key]['excluded'] = (int)idx($info, 'excluded'); + $info['excluded'] = (int)idx($info, 'excluded'); + + // The input has one "path" key with the display path. + // Move it to "display", then normalize the value in "path". + + $display_path = $info['path']; + $raw_path = rtrim($display_path, '/').'/'; + + $info['path'] = $raw_path; + $info['display'] = $display_path; + + $new[$key] = $info; } + return $new; } + public function getTransactionHasEffect($object, $old, $new) { + list($add, $rem) = PhabricatorOwnersPath::getTransactionValueChanges( + $old, + $new); + + return ($add || $rem); + } + public function validateTransactions($object, array $xactions) { $errors = array(); @@ -110,8 +131,8 @@ final class PhabricatorOwnersPackagePathsTransaction $display_map = array(); $seen_map = array(); foreach ($new as $key => $spec) { - $display_path = $spec['path']; - $raw_path = rtrim($display_path, '/').'/'; + $raw_path = $spec['path']; + $display_path = $spec['display']; // If the user entered two paths in the same repository which normalize // to the same value (like "src/main.c" and "src/main.c/"), discard the @@ -193,11 +214,18 @@ final class PhabricatorOwnersPackagePathsTransaction $rowc = array(); foreach ($rows as $key => $row) { $rowc[] = $row['class']; + + if (array_key_exists('display', $row)) { + $display_path = $row['display']; + } else { + $display_path = $row['path']; + } + $rows[$key] = array( $row['change'], $row['excluded'] ? pht('Exclude') : pht('Include'), $this->renderHandle($row['repositoryPHID']), - $row['path'], + $display_path, ); } diff --git a/src/applications/paste/storage/PhabricatorPasteDAO.php b/src/applications/paste/storage/PhabricatorPasteDAO.php index dd61ff7920..0decb81055 100644 --- a/src/applications/paste/storage/PhabricatorPasteDAO.php +++ b/src/applications/paste/storage/PhabricatorPasteDAO.php @@ -3,7 +3,7 @@ abstract class PhabricatorPasteDAO extends PhabricatorLiskDAO { public function getApplicationName() { - return 'pastebin'; + return 'paste'; } } diff --git a/src/applications/paste/storage/PhabricatorPasteTransaction.php b/src/applications/paste/storage/PhabricatorPasteTransaction.php index 1cd77a7048..18bf984259 100644 --- a/src/applications/paste/storage/PhabricatorPasteTransaction.php +++ b/src/applications/paste/storage/PhabricatorPasteTransaction.php @@ -8,7 +8,7 @@ final class PhabricatorPasteTransaction const MAILTAG_COMMENT = 'paste-comment'; public function getApplicationName() { - return 'pastebin'; + return 'paste'; } public function getApplicationTransactionType() { diff --git a/src/applications/people/application/PhabricatorPeopleApplication.php b/src/applications/people/application/PhabricatorPeopleApplication.php index 9cc3607930..ec6892d022 100644 --- a/src/applications/people/application/PhabricatorPeopleApplication.php +++ b/src/applications/people/application/PhabricatorPeopleApplication.php @@ -44,6 +44,7 @@ final class PhabricatorPeopleApplication extends PhabricatorApplication { $this->getQueryRoutePattern() => 'PhabricatorPeopleListController', 'logs/' => array( $this->getQueryRoutePattern() => 'PhabricatorPeopleLogsController', + '(?P\d+)/' => 'PhabricatorPeopleLogViewController', ), 'invite/' => array( '(?:query/(?P[^/]+)/)?' diff --git a/src/applications/people/controller/PhabricatorPeopleDeleteController.php b/src/applications/people/controller/PhabricatorPeopleDeleteController.php index 29ae7edd93..8e6ac91da7 100644 --- a/src/applications/people/controller/PhabricatorPeopleDeleteController.php +++ b/src/applications/people/controller/PhabricatorPeopleDeleteController.php @@ -17,58 +17,35 @@ final class PhabricatorPeopleDeleteController $manage_uri = $this->getApplicationURI("manage/{$id}/"); - if ($user->getPHID() == $viewer->getPHID()) { - return $this->buildDeleteSelfResponse($manage_uri); - } - - $str1 = pht( - 'Be careful when deleting users! This will permanently and '. - 'irreversibly destroy this user account.'); - - $str2 = pht( - 'If this user interacted with anything, it is generally better to '. - 'disable them, not delete them. If you delete them, it will no longer '. - 'be possible to (for example) search for objects they created, and you '. - 'will lose other information about their history. Disabling them '. - 'instead will prevent them from logging in, but will not destroy any of '. - 'their data.'); - - $str3 = pht( - 'It is generally safe to delete newly created users (and test users and '. - 'so on), but less safe to delete established users. If possible, '. - 'disable them instead.'); - - $str4 = pht('To permanently destroy this user, run this command:'); - - $form = id(new AphrontFormView()) - ->setUser($viewer) - ->appendRemarkupInstructions( - csprintf( - " phabricator/ $ ./bin/remove destroy %R\n", - '@'.$user->getUsername())); + $doc_uri = PhabricatorEnv::getDoclink( + 'Permanently Destroying Data'); return $this->newDialog() - ->setWidth(AphrontDialogView::WIDTH_FORM) - ->setTitle(pht('Permanently Delete User')) - ->setShortTitle(pht('Delete User')) - ->appendParagraph($str1) - ->appendParagraph($str2) - ->appendParagraph($str3) - ->appendParagraph($str4) - ->appendChild($form->buildLayoutView()) + ->setTitle(pht('Delete User')) + ->appendParagraph( + pht( + 'To permanently destroy this user, run this command from the '. + 'command line:')) + ->appendCommand( + csprintf( + 'phabricator/ $ ./bin/remove destroy %R', + $user->getMonogram())) + ->appendParagraph( + pht( + 'Unless you have a very good reason to delete this user, consider '. + 'disabling them instead.')) + ->appendParagraph( + pht( + 'Users can not be permanently destroyed from the web interface. '. + 'See %s in the documentation for more information.', + phutil_tag( + 'a', + array( + 'href' => $doc_uri, + 'target' => '_blank', + ), + pht('Permanently Destroying Data')))) ->addCancelButton($manage_uri, pht('Close')); } - private function buildDeleteSelfResponse($cancel_uri) { - return $this->newDialog() - ->setTitle(pht('You Shall Journey No Farther')) - ->appendParagraph( - pht( - 'As you stare into the gaping maw of the abyss, something '. - 'holds you back.')) - ->appendParagraph(pht('You can not delete your own account.')) - ->addCancelButton($cancel_uri, pht('Turn Back')); - } - - } diff --git a/src/applications/people/controller/PhabricatorPeopleEmpowerController.php b/src/applications/people/controller/PhabricatorPeopleEmpowerController.php index 09021bf73e..22e7c22b68 100644 --- a/src/applications/people/controller/PhabricatorPeopleEmpowerController.php +++ b/src/applications/people/controller/PhabricatorPeopleEmpowerController.php @@ -17,14 +17,8 @@ final class PhabricatorPeopleEmpowerController $done_uri = $this->getApplicationURI("manage/{$id}/"); - id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( - $viewer, - $request, - $done_uri); - $validation_exception = null; - - if ($request->isFormPost()) { + if ($request->isFormOrHisecPost()) { $xactions = array(); $xactions[] = id(new PhabricatorUserTransaction()) ->setTransactionType( @@ -34,7 +28,8 @@ final class PhabricatorPeopleEmpowerController $editor = id(new PhabricatorUserTransactionEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) - ->setContinueOnMissingFields(true); + ->setContinueOnMissingFields(true) + ->setCancelURI($done_uri); try { $editor->applyTransactions($user, $xactions); diff --git a/src/applications/people/controller/PhabricatorPeopleLogViewController.php b/src/applications/people/controller/PhabricatorPeopleLogViewController.php new file mode 100644 index 0000000000..faaf4fd5ca --- /dev/null +++ b/src/applications/people/controller/PhabricatorPeopleLogViewController.php @@ -0,0 +1,92 @@ +getViewer(); + $id = $request->getURIData('id'); + + $log = id(new PhabricatorPeopleLogQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + if (!$log) { + return new Aphront404Response(); + } + + $logs_uri = $this->getApplicationURI('logs/'); + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb(pht('Activity Logs'), $logs_uri) + ->addTextCrumb($log->getObjectName()) + ->setBorder(true); + + $header = $this->buildHeaderView($log); + $properties = $this->buildPropertiesView($log); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->addPropertySection(pht('Details'), $properties); + + return $this->newPage() + ->setCrumbs($crumbs) + ->setTitle($log->getObjectName()) + ->appendChild($view); + } + + private function buildHeaderView(PhabricatorUserLog $log) { + $viewer = $this->getViewer(); + + $view = id(new PHUIHeaderView()) + ->setViewer($viewer) + ->setHeader($log->getObjectName()); + + return $view; + } + + private function buildPropertiesView(PhabricatorUserLog $log) { + $viewer = $this->getViewer(); + + $view = id(new PHUIPropertyListView()) + ->setViewer($viewer); + + $type_map = PhabricatorUserLogType::getAllLogTypes(); + $type_map = mpull($type_map, 'getLogTypeName', 'getLogTypeKey'); + + $action = $log->getAction(); + $type_name = idx($type_map, $action, $action); + + $view->addProperty(pht('Event Type'), $type_name); + + $view->addProperty( + pht('Event Date'), + phabricator_datetime($log->getDateCreated(), $viewer)); + + $actor_phid = $log->getActorPHID(); + if ($actor_phid) { + $view->addProperty( + pht('Acting User'), + $viewer->renderHandle($actor_phid)); + } + + $user_phid = $log->getUserPHID(); + if ($user_phid) { + $view->addProperty( + pht('Affected User'), + $viewer->renderHandle($user_phid)); + } + + $remote_address = $log->getRemoteAddressForViewer($viewer); + if ($remote_address !== null) { + $view->addProperty(pht('Remote Address'), $remote_address); + } + + return $view; + } + +} diff --git a/src/applications/people/controller/PhabricatorPeopleProfileBadgesController.php b/src/applications/people/controller/PhabricatorPeopleProfileBadgesController.php index f98970ef73..e4861e488a 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileBadgesController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileBadgesController.php @@ -34,20 +34,6 @@ final class PhabricatorPeopleProfileBadgesController $user, PhabricatorPeopleProfileMenuEngine::ITEM_BADGES); - // Best option? - $badges = id(new PhabricatorBadgesQuery()) - ->setViewer($viewer) - ->withStatuses(array( - PhabricatorBadgesBadge::STATUS_ACTIVE, - )) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->setLimit(1) - ->execute(); - $button = id(new PHUIButtonView()) ->setTag('a') ->setIcon('fa-plus') @@ -55,17 +41,16 @@ final class PhabricatorPeopleProfileBadgesController ->setWorkflow(true) ->setHref('/badges/award/'.$user->getID().'/'); - if ($badges) { - $header->addActionLink($button); - } + $header->addActionLink($button); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->addClass('project-view-home') ->addClass('project-view-people-home') - ->setFooter(array( - $this->buildBadgesView($user) - )); + ->setFooter( + array( + $badges, + )); return $this->newPage() ->setTitle($title) diff --git a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php index b5c0e2b816..b929d980d5 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php @@ -252,15 +252,30 @@ final class PhabricatorPeopleProfileViewController PhabricatorUser $user, $viewer) { - $query = new PhabricatorFeedQuery(); - $query->withFilterPHIDs( - array( - $user->getPHID(), - )); - $query->setLimit(100); - $query->setViewer($viewer); + $query = id(new PhabricatorFeedQuery()) + ->setViewer($viewer) + ->withFilterPHIDs(array($user->getPHID())) + ->setLimit(100) + ->setReturnPartialResultsOnOverheat(true); + $stories = $query->execute(); + $overheated_view = null; + $is_overheated = $query->getIsOverheated(); + if ($is_overheated) { + $overheated_message = + PhabricatorApplicationSearchController::newOverheatedError( + (bool)$stories); + + $overheated_view = id(new PHUIInfoView()) + ->setSeverity(PHUIInfoView::SEVERITY_WARNING) + ->setTitle(pht('Query Overheated')) + ->setErrors( + array( + $overheated_message, + )); + } + $builder = new PhabricatorFeedBuilder($stories); $builder->setUser($viewer); $builder->setShowHovercards(true); @@ -268,8 +283,10 @@ final class PhabricatorPeopleProfileViewController 'requires but just a single step.')); $view = $builder->buildView(); - return $view->render(); - + return array( + $overheated_view, + $view->render(), + ); } } diff --git a/src/applications/people/editor/PhabricatorUserEditor.php b/src/applications/people/editor/PhabricatorUserEditor.php index c8068858da..81f427ada8 100644 --- a/src/applications/people/editor/PhabricatorUserEditor.php +++ b/src/applications/people/editor/PhabricatorUserEditor.php @@ -74,18 +74,11 @@ final class PhabricatorUserEditor extends PhabricatorEditor { throw $ex; } - $log = PhabricatorUserLog::initializeNewLog( - $this->requireActor(), - $user->getPHID(), - PhabricatorUserLog::ACTION_CREATE); - $log->setNewValue($email->getAddress()); - $log->save(); - if ($is_reassign) { $log = PhabricatorUserLog::initializeNewLog( $this->requireActor(), $user->getPHID(), - PhabricatorUserLog::ACTION_EMAIL_REASSIGN); + PhabricatorReassignEmailUserLogType::LOGTYPE); $log->setNewValue($email->getAddress()); $log->save(); } @@ -100,35 +93,6 @@ final class PhabricatorUserEditor extends PhabricatorEditor { } - /** - * @task edit - */ - public function updateUser( - PhabricatorUser $user, - PhabricatorUserEmail $email = null) { - - if (!$user->getID()) { - throw new Exception(pht('User has not been created yet!')); - } - - $user->openTransaction(); - $user->save(); - if ($email) { - $email->save(); - } - - $log = PhabricatorUserLog::initializeNewLog( - $this->requireActor(), - $user->getPHID(), - PhabricatorUserLog::ACTION_EDIT); - $log->save(); - - $user->saveTransaction(); - - return $this; - } - - /* -( Editing Roles )------------------------------------------------------ */ /** @@ -151,18 +115,9 @@ final class PhabricatorUserEditor extends PhabricatorEditor { return $this; } - $log = PhabricatorUserLog::initializeNewLog( - $actor, - $user->getPHID(), - PhabricatorUserLog::ACTION_SYSTEM_AGENT); - $log->setOldValue($user->getIsSystemAgent()); - $log->setNewValue($system_agent); - $user->setIsSystemAgent((int)$system_agent); $user->save(); - $log->save(); - $user->endWriteLocking(); $user->saveTransaction(); @@ -189,18 +144,9 @@ final class PhabricatorUserEditor extends PhabricatorEditor { return $this; } - $log = PhabricatorUserLog::initializeNewLog( - $actor, - $user->getPHID(), - PhabricatorUserLog::ACTION_MAILING_LIST); - $log->setOldValue($user->getIsMailingList()); - $log->setNewValue($mailing_list); - $user->setIsMailingList((int)$mailing_list); $user->save(); - $log->save(); - $user->endWriteLocking(); $user->saveTransaction(); @@ -249,7 +195,7 @@ final class PhabricatorUserEditor extends PhabricatorEditor { $log = PhabricatorUserLog::initializeNewLog( $actor, $user->getPHID(), - PhabricatorUserLog::ACTION_EMAIL_ADD); + PhabricatorAddEmailUserLogType::LOGTYPE); $log->setNewValue($email->getAddress()); $log->save(); @@ -300,7 +246,7 @@ final class PhabricatorUserEditor extends PhabricatorEditor { $log = PhabricatorUserLog::initializeNewLog( $actor, $user->getPHID(), - PhabricatorUserLog::ACTION_EMAIL_REMOVE); + PhabricatorRemoveEmailUserLogType::LOGTYPE); $log->setOldValue($email->getAddress()); $log->save(); @@ -366,7 +312,7 @@ final class PhabricatorUserEditor extends PhabricatorEditor { $log = PhabricatorUserLog::initializeNewLog( $actor, $user->getPHID(), - PhabricatorUserLog::ACTION_EMAIL_PRIMARY); + PhabricatorPrimaryEmailUserLogType::LOGTYPE); $log->setOldValue($old_primary ? $old_primary->getAddress() : null); $log->setNewValue($email->getAddress()); @@ -425,7 +371,7 @@ final class PhabricatorUserEditor extends PhabricatorEditor { $log = PhabricatorUserLog::initializeNewLog( $actor, $user->getPHID(), - PhabricatorUserLog::ACTION_EMAIL_VERIFY); + PhabricatorVerifyEmailUserLogType::LOGTYPE); $log->setNewValue($email->getAddress()); $log->save(); } @@ -487,7 +433,7 @@ final class PhabricatorUserEditor extends PhabricatorEditor { $log = PhabricatorUserLog::initializeNewLog( $actor, $user->getPHID(), - PhabricatorUserLog::ACTION_EMAIL_REASSIGN); + PhabricatorReassignEmailUserLogType::LOGTYPE); $log->setNewValue($email->getAddress()); $log->save(); } diff --git a/src/applications/people/mail/PhabricatorPeopleEmailLoginMailEngine.php b/src/applications/people/mail/PhabricatorPeopleEmailLoginMailEngine.php new file mode 100644 index 0000000000..c25b19c3c1 --- /dev/null +++ b/src/applications/people/mail/PhabricatorPeopleEmailLoginMailEngine.php @@ -0,0 +1,130 @@ +getRecipient(); + + if ($recipient->getIsDisabled()) { + $this->throwValidationException( + pht('User is Disabled'), + pht( + 'You can not send an email login link to this email address '. + 'because the associated user account is disabled.')); + } + + if (!$recipient->canEstablishWebSessions()) { + $this->throwValidationException( + pht('Not a Normal User'), + pht( + 'You can not send an email login link to this email address '. + 'because the associated user account is not a normal user account '. + 'and can not log in to the web interface.')); + } + } + + protected function newMail() { + $is_set_password = $this->isSetPasswordWorkflow(); + + if ($is_set_password) { + $subject = pht('[Phabricator] Account Password Link'); + } else { + $subject = pht('[Phabricator] Account Login Link'); + } + + $recipient = $this->getRecipient(); + + PhabricatorSystemActionEngine::willTakeAction( + array($recipient->getPHID()), + new PhabricatorAuthEmailLoginAction(), + 1); + + $engine = new PhabricatorAuthSessionEngine(); + $login_uri = $engine->getOneTimeLoginURI( + $recipient, + null, + PhabricatorAuthSessionEngine::ONETIME_RESET); + + $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); + $have_passwords = $this->isPasswordAuthEnabled(); + + $body = array(); + + if ($is_set_password) { + $message_key = PhabricatorAuthEmailSetPasswordMessageType::MESSAGEKEY; + } else { + $message_key = PhabricatorAuthEmailLoginMessageType::MESSAGEKEY; + } + + $message_body = PhabricatorAuthMessage::loadMessageText( + $recipient, + $message_key); + if (strlen($message_body)) { + $body[] = $this->newRemarkupText($message_body); + } + + if ($have_passwords) { + if ($is_set_password) { + $body[] = pht( + 'You can use this link to set a password on your account:'. + "\n\n %s\n", + $login_uri); + } else if ($is_serious) { + $body[] = pht( + "You can use this link to reset your Phabricator password:". + "\n\n %s\n", + $login_uri); + } else { + $body[] = pht( + "Condolences on forgetting your password. You can use this ". + "link to reset it:\n\n". + " %s\n\n". + "After you set a new password, consider writing it down on a ". + "sticky note and attaching it to your monitor so you don't ". + "forget again! Choosing a very short, easy-to-remember password ". + "like \"cat\" or \"1234\" might also help.\n\n". + "Best Wishes,\nPhabricator\n", + $login_uri); + + } + } else { + $body[] = pht( + "You can use this login link to regain access to your Phabricator ". + "account:". + "\n\n". + " %s\n", + $login_uri); + } + + $body = implode("\n\n", $body); + + return id(new PhabricatorMetaMTAMail()) + ->setSubject($subject) + ->setBody($body); + } + + private function isPasswordAuthEnabled() { + return (bool)PhabricatorPasswordAuthProvider::getPasswordProvider(); + } + + private function isSetPasswordWorkflow() { + $sender = $this->getSender(); + $recipient = $this->getRecipient(); + + // Users can hit the "login with an email link" workflow while trying to + // set a password on an account which does not yet have a password. We + // require they verify that they own the email address and send them + // through the email login flow. In this case, the messaging is slightly + // different. + + if ($sender->getPHID()) { + if ($sender->getPHID() === $recipient->getPHID()) { + return true; + } + } + + return false; + } + +} diff --git a/src/applications/people/mail/PhabricatorPeopleMailEngine.php b/src/applications/people/mail/PhabricatorPeopleMailEngine.php index 281009341d..6b7aa7818e 100644 --- a/src/applications/people/mail/PhabricatorPeopleMailEngine.php +++ b/src/applications/people/mail/PhabricatorPeopleMailEngine.php @@ -5,6 +5,8 @@ abstract class PhabricatorPeopleMailEngine private $sender; private $recipient; + private $recipientAddress; + private $activityLog; final public function setSender(PhabricatorUser $sender) { $this->sender = $sender; @@ -30,6 +32,31 @@ abstract class PhabricatorPeopleMailEngine return $this->recipient; } + final public function setRecipientAddress(PhutilEmailAddress $address) { + $this->recipientAddress = $address; + return $this; + } + + final public function getRecipientAddress() { + if (!$this->recipientAddress) { + throw new PhutilInvalidStateException('recipientAddress'); + } + return $this->recipientAddress; + } + + final public function hasRecipientAddress() { + return ($this->recipientAddress !== null); + } + + final public function setActivityLog(PhabricatorUserLog $activity_log) { + $this->activityLog = $activity_log; + return $this; + } + + final public function getActivityLog() { + return $this->activityLog; + } + final public function canSendMail() { try { $this->validateMail(); @@ -43,6 +70,26 @@ abstract class PhabricatorPeopleMailEngine $this->validateMail(); $mail = $this->newMail(); + if ($this->hasRecipientAddress()) { + $recipient_address = $this->getRecipientAddress(); + $mail->addRawTos(array($recipient_address->getAddress())); + } else { + $recipient = $this->getRecipient(); + $mail->addTos(array($recipient->getPHID())); + } + + $activity_log = $this->getActivityLog(); + if ($activity_log) { + $activity_log->save(); + + $body = array(); + $body[] = rtrim($mail->getBody(), "\n"); + $body[] = pht('Activity Log ID: #%d', $activity_log->getID()); + $body = implode("\n\n", $body)."\n"; + + $mail->setBody($body); + } + $mail ->setForceDelivery(true) ->save(); @@ -53,7 +100,6 @@ abstract class PhabricatorPeopleMailEngine abstract public function validateMail(); abstract protected function newMail(); - final protected function throwValidationException($title, $body) { throw new PhabricatorPeopleMailEngineException($title, $body); } @@ -66,7 +112,10 @@ abstract class PhabricatorPeopleMailEngine ->setConfig('uri.base', PhabricatorEnv::getProductionURI('/')) ->setMode(PhutilRemarkupEngine::MODE_TEXT); - return $engine->markupText($text); + $rendered_text = $engine->markupText($text); + $rendered_text = rtrim($rendered_text, "\n"); + + return $rendered_text; } } diff --git a/src/applications/people/mail/PhabricatorPeopleUsernameMailEngine.php b/src/applications/people/mail/PhabricatorPeopleUsernameMailEngine.php index c954b7c38e..e62a6a4859 100644 --- a/src/applications/people/mail/PhabricatorPeopleUsernameMailEngine.php +++ b/src/applications/people/mail/PhabricatorPeopleUsernameMailEngine.php @@ -30,7 +30,6 @@ final class PhabricatorPeopleUsernameMailEngine protected function newMail() { $sender = $this->getSender(); - $recipient = $this->getRecipient(); $sender_username = $sender->getUsername(); $sender_realname = $sender->getRealName(); @@ -52,7 +51,6 @@ final class PhabricatorPeopleUsernameMailEngine $new_username)); return id(new PhabricatorMetaMTAMail()) - ->addTos(array($recipient->getPHID())) ->setSubject(pht('[Phabricator] Username Changed')) ->setBody($body); } diff --git a/src/applications/people/mail/PhabricatorPeopleWelcomeMailEngine.php b/src/applications/people/mail/PhabricatorPeopleWelcomeMailEngine.php index ff7ee71272..ec99a5a484 100644 --- a/src/applications/people/mail/PhabricatorPeopleWelcomeMailEngine.php +++ b/src/applications/people/mail/PhabricatorPeopleWelcomeMailEngine.php @@ -104,7 +104,6 @@ final class PhabricatorPeopleWelcomeMailEngine $message = implode("\n\n", $message); return id(new PhabricatorMetaMTAMail()) - ->addTos(array($recipient->getPHID())) ->setSubject(pht('[Phabricator] Welcome to Phabricator')) ->setBody($message); } diff --git a/src/applications/people/management/PhabricatorPeopleManagementEmpowerWorkflow.php b/src/applications/people/management/PhabricatorPeopleManagementEmpowerWorkflow.php new file mode 100644 index 0000000000..0393a96224 --- /dev/null +++ b/src/applications/people/management/PhabricatorPeopleManagementEmpowerWorkflow.php @@ -0,0 +1,44 @@ +getUserSelectionArguments(), + array()); + + $this + ->setName('empower') + ->setExamples('**empower** --user __username__') + ->setSynopsis(pht('Turn a user account into an administrator account.')) + ->setArguments($arguments); + } + + public function execute(PhutilArgumentParser $args) { + $user = $this->selectUser($args); + $display_name = $user->getUsername(); + + if ($user->getIsAdmin()) { + throw new PhutilArgumentUsageException( + pht( + 'User account "%s" is already an administrator. You can only '. + 'empower accounts that are not yet administrators.', + $display_name)); + } + + $xactions = array(); + $xactions[] = $user->getApplicationTransactionTemplate() + ->setTransactionType(PhabricatorUserEmpowerTransaction::TRANSACTIONTYPE) + ->setNewValue(true); + + $this->applyTransactions($user, $xactions); + + $this->logOkay( + pht('DONE'), + pht('Empowered user account "%s".', $display_name)); + + return 0; + } + +} diff --git a/src/applications/people/management/PhabricatorPeopleManagementEnableWorkflow.php b/src/applications/people/management/PhabricatorPeopleManagementEnableWorkflow.php new file mode 100644 index 0000000000..b721bf221e --- /dev/null +++ b/src/applications/people/management/PhabricatorPeopleManagementEnableWorkflow.php @@ -0,0 +1,44 @@ +getUserSelectionArguments(), + array()); + + $this + ->setName('enable') + ->setExamples('**enable** --user __username__') + ->setSynopsis(pht('Enable a disabled user account.')) + ->setArguments($arguments); + } + + public function execute(PhutilArgumentParser $args) { + $user = $this->selectUser($args); + $display_name = $user->getUsername(); + + if (!$user->getIsDisabled()) { + throw new PhutilArgumentUsageException( + pht( + 'User account "%s" is not disabled. You can only enable accounts '. + 'that are disabled.', + $display_name)); + } + + $xactions = array(); + $xactions[] = $user->getApplicationTransactionTemplate() + ->setTransactionType(PhabricatorUserDisableTransaction::TRANSACTIONTYPE) + ->setNewValue(false); + + $this->applyTransactions($user, $xactions); + + $this->logOkay( + pht('DONE'), + pht('Enabled user account "%s".', $display_name)); + + return 0; + } + +} diff --git a/src/applications/people/management/PhabricatorPeopleManagementWorkflow.php b/src/applications/people/management/PhabricatorPeopleManagementWorkflow.php index 67b474f7e2..d504b8c11b 100644 --- a/src/applications/people/management/PhabricatorPeopleManagementWorkflow.php +++ b/src/applications/people/management/PhabricatorPeopleManagementWorkflow.php @@ -3,45 +3,55 @@ abstract class PhabricatorPeopleManagementWorkflow extends PhabricatorManagementWorkflow { - protected function buildIterator(PhutilArgumentParser $args) { - $usernames = $args->getArg('users'); - - if ($args->getArg('all')) { - if ($usernames) { - throw new PhutilArgumentUsageException( - pht( - 'Specify either a list of users or `%s`, but not both.', - '--all')); - } - return new LiskMigrationIterator(new PhabricatorUser()); - } - - if ($usernames) { - return $this->loadUsersWithUsernames($usernames); - } - - return null; + final protected function getUserSelectionArguments() { + return array( + array( + 'name' => 'user', + 'param' => 'username', + 'help' => pht('User account to act on.'), + ), + ); } - protected function loadUsersWithUsernames(array $usernames) { - $users = array(); - foreach($usernames as $username) { - $query = id(new PhabricatorPeopleQuery()) - ->setViewer($this->getViewer()) - ->withUsernames(array($username)) - ->executeOne(); + final protected function selectUser(PhutilArgumentParser $argv) { + $username = $argv->getArg('user'); - if (!$query) { - throw new PhutilArgumentUsageException( - pht( - '"%s" is not a valid username.', - $username)); - } - $users[] = $query; + if (!strlen($username)) { + throw new PhutilArgumentUsageException( + pht( + 'Select a user account to act on with "--user ".')); } - return $users; + $user = id(new PhabricatorPeopleQuery()) + ->setViewer($this->getViewer()) + ->withUsernames(array($username)) + ->executeOne(); + if (!$user) { + throw new PhutilArgumentUsageException( + pht( + 'No user with username "%s" exists.', + $username)); + } + + return $user; } + final protected function applyTransactions( + PhabricatorUser $user, + array $xactions) { + assert_instances_of($xactions, 'PhabricatorUserTransaction'); + + $viewer = $this->getViewer(); + $application = id(new PhabricatorPeopleApplication())->getPHID(); + $content_source = $this->newContentSource(); + + $editor = $user->getApplicationTransactionEditor() + ->setActor($viewer) + ->setActingAsPHID($application) + ->setContentSource($content_source) + ->setContinueOnMissingFields(true); + + return $editor->applyTransactions($user, $xactions); + } } diff --git a/src/applications/people/management/PhabricatorPeopleProfileImageWorkflow.php b/src/applications/people/management/PhabricatorPeopleProfileImageWorkflow.php deleted file mode 100644 index 8bf3c8e118..0000000000 --- a/src/applications/people/management/PhabricatorPeopleProfileImageWorkflow.php +++ /dev/null @@ -1,85 +0,0 @@ -setName('profileimage') - ->setExamples('**profileimage** --users __username__') - ->setSynopsis(pht('Generate default profile images.')) - ->setArguments( - array( - array( - 'name' => 'all', - 'help' => pht( - 'Generate default profile images for all users.'), - ), - array( - 'name' => 'force', - 'short' => 'f', - 'help' => pht( - 'Force a default profile image to be replaced.'), - ), - array( - 'name' => 'users', - 'wildcard' => true, - ), - )); - } - - public function execute(PhutilArgumentParser $args) { - $console = PhutilConsole::getConsole(); - - $is_force = $args->getArg('force'); - $is_all = $args->getArg('all'); - - $gd = function_exists('imagecreatefromstring'); - if (!$gd) { - throw new PhutilArgumentUsageException( - pht( - 'GD is not installed for php-cli. Aborting.')); - } - - $iterator = $this->buildIterator($args); - if (!$iterator) { - throw new PhutilArgumentUsageException( - pht( - 'Either specify a list of users to update, or use `%s` '. - 'to update all users.', - '--all')); - } - - $version = PhabricatorFilesComposeAvatarBuiltinFile::VERSION; - $generator = new PhabricatorFilesComposeAvatarBuiltinFile(); - - foreach ($iterator as $user) { - $username = $user->getUsername(); - $default_phid = $user->getDefaultProfileImagePHID(); - $gen_version = $user->getDefaultProfileImageVersion(); - - $generate = false; - if ($gen_version != $version) { - $generate = true; - } - - if ($default_phid == null || $is_force || $generate) { - $console->writeOut( - "%s\n", - pht( - 'Generating profile image for "%s".', - $username)); - - $generator->updateUser($user); - } else { - $console->writeOut( - "%s\n", - pht( - 'Default profile image "%s" already set for "%s".', - $version, - $username)); - } - } - } - -} diff --git a/src/applications/people/query/PhabricatorPeopleLogQuery.php b/src/applications/people/query/PhabricatorPeopleLogQuery.php index fc6a87b335..203f79579a 100644 --- a/src/applications/people/query/PhabricatorPeopleLogQuery.php +++ b/src/applications/people/query/PhabricatorPeopleLogQuery.php @@ -3,6 +3,7 @@ final class PhabricatorPeopleLogQuery extends PhabricatorCursorPagedPolicyAwareQuery { + private $ids; private $actorPHIDs; private $userPHIDs; private $relatedPHIDs; @@ -12,6 +13,11 @@ final class PhabricatorPeopleLogQuery private $dateCreatedMin; private $dateCreatedMax; + public function withIDs(array $ids) { + $this->ids = $ids; + return $this; + } + public function withActorPHIDs(array $actor_phids) { $this->actorPHIDs = $actor_phids; return $this; @@ -59,6 +65,13 @@ final class PhabricatorPeopleLogQuery protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'id IN (%Ld)', + $this->ids); + } + if ($this->actorPHIDs !== null) { $where[] = qsprintf( $conn, diff --git a/src/applications/people/query/PhabricatorPeopleLogSearchEngine.php b/src/applications/people/query/PhabricatorPeopleLogSearchEngine.php index b052456cd3..7e7fca1cd4 100644 --- a/src/applications/people/query/PhabricatorPeopleLogSearchEngine.php +++ b/src/applications/people/query/PhabricatorPeopleLogSearchEngine.php @@ -64,6 +64,9 @@ final class PhabricatorPeopleLogSearchEngine } protected function buildCustomSearchFields() { + $types = PhabricatorUserLogType::getAllLogTypes(); + $types = mpull($types, 'getLogTypeName', 'getLogTypeKey'); + return array( id(new PhabricatorUsersSearchField()) ->setKey('userPHIDs') @@ -75,11 +78,11 @@ final class PhabricatorPeopleLogSearchEngine ->setAliases(array('actors', 'actor', 'actorPHID')) ->setLabel(pht('Actors')) ->setDescription(pht('Search for activity by specific users.')), - id(new PhabricatorSearchCheckboxesField()) + id(new PhabricatorSearchDatasourceField()) ->setKey('actions') ->setLabel(pht('Actions')) ->setDescription(pht('Search for particular types of activity.')) - ->setOptions(PhabricatorUserLog::getActionTypeMap()), + ->setDatasource(new PhabricatorUserLogTypeDatasource()), id(new PhabricatorSearchTextField()) ->setKey('ip') ->setLabel(pht('Filter IP')) @@ -194,7 +197,8 @@ final class PhabricatorPeopleLogSearchEngine } $handles = $viewer->loadHandles($phids); - $action_map = PhabricatorUserLog::getActionTypeMap(); + $types = PhabricatorUserLogType::getAllLogTypes(); + $types = mpull($types, 'getLogTypeName', 'getLogTypeKey'); $export = array(); foreach ($logs as $log) { @@ -214,7 +218,7 @@ final class PhabricatorPeopleLogSearchEngine } $action = $log->getAction(); - $action_name = idx($action_map, $action, pht('Unknown ("%s")', $action)); + $action_name = idx($types, $action, pht('Unknown ("%s")', $action)); $map = array( 'actorPHID' => $actor_phid, diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php index 63e5a24db9..655f75340c 100644 --- a/src/applications/people/storage/PhabricatorUser.php +++ b/src/applications/people/storage/PhabricatorUser.php @@ -59,7 +59,6 @@ final class PhabricatorUser private $rawCacheData = array(); private $usableCacheData = array(); - private $authorities = array(); private $handlePool; private $csrfSalt; @@ -775,23 +774,6 @@ final class PhabricatorUser } - /** - * Grant a user a source of authority, to let them bypass policy checks they - * could not otherwise. - */ - public function grantAuthority($authority) { - $this->authorities[] = $authority; - return $this; - } - - - /** - * Get authorities granted to the user. - */ - public function getAuthorities() { - return $this->authorities; - } - public function hasConduitClusterToken() { return ($this->conduitClusterToken !== self::ATTACHABLE); } diff --git a/src/applications/people/storage/PhabricatorUserLog.php b/src/applications/people/storage/PhabricatorUserLog.php index 12cb4cb626..61ccaff1ed 100644 --- a/src/applications/people/storage/PhabricatorUserLog.php +++ b/src/applications/people/storage/PhabricatorUserLog.php @@ -3,43 +3,6 @@ final class PhabricatorUserLog extends PhabricatorUserDAO implements PhabricatorPolicyInterface { - const ACTION_LOGIN = 'login'; - const ACTION_LOGIN_PARTIAL = 'login-partial'; - const ACTION_LOGIN_FULL = 'login-full'; - const ACTION_LOGOUT = 'logout'; - const ACTION_LOGIN_FAILURE = 'login-fail'; - const ACTION_LOGIN_LEGALPAD = 'login-legalpad'; - const ACTION_RESET_PASSWORD = 'reset-pass'; - - const ACTION_CREATE = 'create'; - const ACTION_EDIT = 'edit'; - - const ACTION_ADMIN = 'admin'; - const ACTION_SYSTEM_AGENT = 'system-agent'; - const ACTION_MAILING_LIST = 'mailing-list'; - const ACTION_DISABLE = 'disable'; - const ACTION_APPROVE = 'approve'; - const ACTION_DELETE = 'delete'; - - const ACTION_CONDUIT_CERTIFICATE = 'conduit-cert'; - const ACTION_CONDUIT_CERTIFICATE_FAILURE = 'conduit-cert-fail'; - - const ACTION_EMAIL_PRIMARY = 'email-primary'; - const ACTION_EMAIL_REMOVE = 'email-remove'; - const ACTION_EMAIL_ADD = 'email-add'; - const ACTION_EMAIL_VERIFY = 'email-verify'; - const ACTION_EMAIL_REASSIGN = 'email-reassign'; - - const ACTION_CHANGE_PASSWORD = 'change-password'; - const ACTION_CHANGE_USERNAME = 'change-username'; - - const ACTION_ENTER_HISEC = 'hisec-enter'; - const ACTION_EXIT_HISEC = 'hisec-exit'; - const ACTION_FAIL_HISEC = 'hisec-fail'; - - const ACTION_MULTI_ADD = 'multi-add'; - const ACTION_MULTI_REMOVE = 'multi-remove'; - protected $actorPHID; protected $userPHID; protected $action; @@ -49,44 +12,6 @@ final class PhabricatorUserLog extends PhabricatorUserDAO protected $remoteAddr; protected $session; - public static function getActionTypeMap() { - return array( - self::ACTION_LOGIN => pht('Login'), - self::ACTION_LOGIN_PARTIAL => pht('Login: Partial Login'), - self::ACTION_LOGIN_FULL => pht('Login: Upgrade to Full'), - self::ACTION_LOGIN_FAILURE => pht('Login: Failure'), - self::ACTION_LOGIN_LEGALPAD => - pht('Login: Signed Required Legalpad Documents'), - self::ACTION_LOGOUT => pht('Logout'), - self::ACTION_RESET_PASSWORD => pht('Reset Password'), - self::ACTION_CREATE => pht('Create Account'), - self::ACTION_EDIT => pht('Edit Account'), - self::ACTION_ADMIN => pht('Add/Remove Administrator'), - self::ACTION_SYSTEM_AGENT => pht('Add/Remove System Agent'), - self::ACTION_MAILING_LIST => pht('Add/Remove Mailing List'), - self::ACTION_DISABLE => pht('Enable/Disable'), - self::ACTION_APPROVE => pht('Approve Registration'), - self::ACTION_DELETE => pht('Delete User'), - self::ACTION_CONDUIT_CERTIFICATE - => pht('Conduit: Read Certificate'), - self::ACTION_CONDUIT_CERTIFICATE_FAILURE - => pht('Conduit: Read Certificate Failure'), - self::ACTION_EMAIL_PRIMARY => pht('Email: Change Primary'), - self::ACTION_EMAIL_ADD => pht('Email: Add Address'), - self::ACTION_EMAIL_REMOVE => pht('Email: Remove Address'), - self::ACTION_EMAIL_VERIFY => pht('Email: Verify'), - self::ACTION_EMAIL_REASSIGN => pht('Email: Reassign'), - self::ACTION_CHANGE_PASSWORD => pht('Change Password'), - self::ACTION_CHANGE_USERNAME => pht('Change Username'), - self::ACTION_ENTER_HISEC => pht('Hisec: Enter'), - self::ACTION_EXIT_HISEC => pht('Hisec: Exit'), - self::ACTION_FAIL_HISEC => pht('Hisec: Failed Attempt'), - self::ACTION_MULTI_ADD => pht('Multi-Factor: Add Factor'), - self::ACTION_MULTI_REMOVE => pht('Multi-Factor: Remove Factor'), - ); - } - - public static function initializeNewLog( PhabricatorUser $actor = null, $object_phid = null, @@ -175,6 +100,43 @@ final class PhabricatorUserLog extends PhabricatorUserDAO ) + parent::getConfiguration(); } + public function getURI() { + return urisprintf('/people/logs/%s/', $this->getID()); + } + + public function getObjectName() { + return pht('Activity Log %d', $this->getID()); + } + + public function getRemoteAddressForViewer(PhabricatorUser $viewer) { + $viewer_phid = $viewer->getPHID(); + $actor_phid = $this->getActorPHID(); + $user_phid = $this->getUserPHID(); + + if (!$viewer_phid) { + $can_see_ip = false; + } else if ($viewer->getIsAdmin()) { + $can_see_ip = true; + } else if ($viewer_phid == $actor_phid) { + // You can see the address if you took the action. + $can_see_ip = true; + } else if (!$actor_phid && ($viewer_phid == $user_phid)) { + // You can see the address if it wasn't authenticated and applied + // to you (partial login). + $can_see_ip = true; + } else { + // You can't see the address when an administrator disables your + // account, since it's their address. + $can_see_ip = false; + } + + if (!$can_see_ip) { + return null; + } + + return $this->getRemoteAddr(); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/people/typeahead/PhabricatorUserLogTypeDatasource.php b/src/applications/people/typeahead/PhabricatorUserLogTypeDatasource.php new file mode 100644 index 0000000000..39241a020c --- /dev/null +++ b/src/applications/people/typeahead/PhabricatorUserLogTypeDatasource.php @@ -0,0 +1,43 @@ +buildResults(); + return $this->filterResultsAgainstTokens($results); + } + + protected function renderSpecialTokens(array $values) { + return $this->renderTokensFromResults($this->buildResults(), $values); + } + + private function buildResults() { + $results = array(); + + $type_map = PhabricatorUserLogType::getAllLogTypes(); + foreach ($type_map as $type_key => $type) { + + $result = id(new PhabricatorTypeaheadResult()) + ->setPHID($type_key) + ->setName($type->getLogTypeName()); + + $results[$type_key] = $result; + } + + return $results; + } + +} diff --git a/src/applications/people/userlog/PhabricatorAddEmailUserLogType.php b/src/applications/people/userlog/PhabricatorAddEmailUserLogType.php new file mode 100644 index 0000000000..5587f46ed3 --- /dev/null +++ b/src/applications/people/userlog/PhabricatorAddEmailUserLogType.php @@ -0,0 +1,12 @@ +getPhobjectClassConstant('LOGTYPE', 32); + } + + abstract public function getLogTypeName(); + + final public static function getAllLogTypes() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getLogTypeKey') + ->execute(); + } + +} diff --git a/src/applications/people/userlog/PhabricatorVerifyEmailUserLogType.php b/src/applications/people/userlog/PhabricatorVerifyEmailUserLogType.php new file mode 100644 index 0000000000..b6f39a2e7b --- /dev/null +++ b/src/applications/people/userlog/PhabricatorVerifyEmailUserLogType.php @@ -0,0 +1,12 @@ +loadHandles($phids); - $action_map = PhabricatorUserLog::getActionTypeMap(); + $types = PhabricatorUserLogType::getAllLogTypes(); + $types = mpull($types, 'getLogTypeName', 'getLogTypeKey'); + $base_uri = $this->searchBaseURI; $viewer_phid = $viewer->getPHID(); @@ -39,37 +41,20 @@ final class PhabricatorUserLogView extends AphrontView { $actor_phid = $log->getActorPHID(); $user_phid = $log->getUserPHID(); - if ($viewer->getIsAdmin()) { - $can_see_ip = true; - } else if ($viewer_phid == $actor_phid) { - // You can see the address if you took the action. - $can_see_ip = true; - } else if (!$actor_phid && ($viewer_phid == $user_phid)) { - // You can see the address if it wasn't authenticated and applied - // to you (partial login). - $can_see_ip = true; - } else { - // You can't see the address when an administrator disables your - // account, since it's their address. - $can_see_ip = false; - } - - if ($can_see_ip) { - $ip = $log->getRemoteAddr(); + $remote_address = $log->getRemoteAddressForViewer($viewer); + if ($remote_address !== null) { if ($base_uri) { - $ip = phutil_tag( + $remote_address = phutil_tag( 'a', array( - 'href' => $base_uri.'?ip='.$ip.'#R', + 'href' => $base_uri.'?ip='.$remote_address.'#R', ), - $ip); + $remote_address); } - } else { - $ip = null; } $action = $log->getAction(); - $action_name = idx($action_map, $action, $action); + $action_name = idx($types, $action, $action); if ($actor_phid) { $actor_name = $handles[$actor_phid]->renderLink(); @@ -83,37 +68,47 @@ final class PhabricatorUserLogView extends AphrontView { $user_name = null; } + $action_link = phutil_tag( + 'a', + array( + 'href' => $log->getURI(), + ), + $action_name); + $rows[] = array( - phabricator_date($log->getDateCreated(), $viewer), - phabricator_time($log->getDateCreated(), $viewer), - $action_name, + $log->getID(), + $action_link, $actor_name, $user_name, - $ip, + $remote_address, $session, + phabricator_date($log->getDateCreated(), $viewer), + phabricator_time($log->getDateCreated(), $viewer), ); } $table = new AphrontTableView($rows); $table->setHeaders( array( - pht('Date'), - pht('Time'), + pht('ID'), pht('Action'), pht('Actor'), pht('User'), pht('IP'), pht('Session'), + pht('Date'), + pht('Time'), )); $table->setColumnClasses( array( '', - 'right', 'wide', '', '', '', 'n', + '', + 'right', )); return $table; diff --git a/src/applications/people/xaction/PhabricatorUserApproveTransaction.php b/src/applications/people/xaction/PhabricatorUserApproveTransaction.php index e458c5822c..77d58bebdf 100644 --- a/src/applications/people/xaction/PhabricatorUserApproveTransaction.php +++ b/src/applications/people/xaction/PhabricatorUserApproveTransaction.php @@ -19,10 +19,6 @@ final class PhabricatorUserApproveTransaction public function applyExternalEffects($object, $value) { $user = $object; - $this->newUserLog(PhabricatorUserLog::ACTION_APPROVE) - ->setOldValue((bool)$user->getIsApproved()) - ->setNewValue((bool)$value) - ->save(); $actor = $this->getActor(); $title = pht( diff --git a/src/applications/people/xaction/PhabricatorUserDisableTransaction.php b/src/applications/people/xaction/PhabricatorUserDisableTransaction.php index 7a8a1c7966..f259e78ee4 100644 --- a/src/applications/people/xaction/PhabricatorUserDisableTransaction.php +++ b/src/applications/people/xaction/PhabricatorUserDisableTransaction.php @@ -17,13 +17,6 @@ final class PhabricatorUserDisableTransaction $object->setIsDisabled((int)$value); } - public function applyExternalEffects($object, $value) { - $this->newUserLog(PhabricatorUserLog::ACTION_DISABLE) - ->setOldValue((bool)$object->getIsDisabled()) - ->setNewValue((bool)$value) - ->save(); - } - public function getTitle() { $new = $this->getNewValue(); if ($new) { diff --git a/src/applications/people/xaction/PhabricatorUserEmpowerTransaction.php b/src/applications/people/xaction/PhabricatorUserEmpowerTransaction.php index 1b561d3236..d17418636f 100644 --- a/src/applications/people/xaction/PhabricatorUserEmpowerTransaction.php +++ b/src/applications/people/xaction/PhabricatorUserEmpowerTransaction.php @@ -17,15 +17,6 @@ final class PhabricatorUserEmpowerTransaction $object->setIsAdmin((int)$value); } - public function applyExternalEffects($object, $value) { - $user = $object; - - $this->newUserLog(PhabricatorUserLog::ACTION_ADMIN) - ->setOldValue($this->getOldValue()) - ->setNewValue($value) - ->save(); - } - public function validateTransactions($object, array $xactions) { $user = $object; $actor = $this->getActor(); @@ -95,4 +86,11 @@ final class PhabricatorUserEmpowerTransaction return null; } + + public function shouldTryMFA( + $object, + PhabricatorApplicationTransaction $xaction) { + return true; + } + } diff --git a/src/applications/people/xaction/PhabricatorUserTransactionType.php b/src/applications/people/xaction/PhabricatorUserTransactionType.php index dcd45d480e..89392fd039 100644 --- a/src/applications/people/xaction/PhabricatorUserTransactionType.php +++ b/src/applications/people/xaction/PhabricatorUserTransactionType.php @@ -1,13 +1,4 @@ getActor(), - $this->getObject()->getPHID(), - $action); - } - -} + extends PhabricatorModularTransactionType {} diff --git a/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php b/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php index b436b76716..338b296335 100644 --- a/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php +++ b/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php @@ -24,11 +24,6 @@ final class PhabricatorUserUsernameTransaction $old_username = $this->getOldValue(); $new_username = $this->getNewValue(); - $this->newUserLog(PhabricatorUserLog::ACTION_CHANGE_USERNAME) - ->setOldValue($old_username) - ->setNewValue($new_username) - ->save(); - // The SSH key cache currently includes usernames, so dirty it. See T12554 // for discussion. PhabricatorAuthSSHKeyQuery::deleteSSHKeyCache(); diff --git a/src/applications/phame/controller/PhameLiveController.php b/src/applications/phame/controller/PhameLiveController.php index b5b1984816..472f73c8c1 100644 --- a/src/applications/phame/controller/PhameLiveController.php +++ b/src/applications/phame/controller/PhameLiveController.php @@ -93,10 +93,6 @@ abstract class PhameLiveController extends PhameController { ->needHeaderImage(true) ->withIDs(array($post_id)); - if ($blog) { - $post_query->withBlogPHIDs(array($blog->getPHID())); - } - // Only show published posts on external domains. if ($is_external) { $post_query->withVisibility( @@ -123,10 +119,15 @@ abstract class PhameLiveController extends PhameController { $this->post = $post; // If we have a post, canonicalize the URI to the post's current slug and - // redirect the user if it isn't correct. + // redirect the user if it isn't correct. Likewise, canonicalize the URI + // if the blog ID is wrong. See T13353. if ($post) { $slug = $request->getURIData('slug'); - if ($post->getSlug() != $slug) { + + $wrong_slug = ($post->getSlug() !== $slug); + $wrong_blog = ($post->getBlog()->getID() !== $blog->getID()); + + if ($wrong_slug || $wrong_blog) { if ($is_live) { if ($is_external) { $uri = $post->getExternalLiveURI(); diff --git a/src/applications/pholio/view/PholioMockImagesView.php b/src/applications/pholio/view/PholioMockImagesView.php index 786de07cfd..319daa3169 100644 --- a/src/applications/pholio/view/PholioMockImagesView.php +++ b/src/applications/pholio/view/PholioMockImagesView.php @@ -103,7 +103,7 @@ final class PholioMockImagesView extends AphrontView { 'width' => $x, 'height' => $y, 'title' => $image->getName(), - 'descriptionMarkup' => $description, + 'descriptionMarkup' => hsprintf('%s', $description), 'isObsolete' => (bool)$image->getIsObsolete(), 'isImage' => $file->isViewableImage(), 'isViewable' => $file->isViewableInBrowser(), diff --git a/src/applications/phortune/action/PhortuneAddPaymentMethodAction.php b/src/applications/phortune/action/PhortuneAddPaymentMethodAction.php index 09a8cd2f5d..5f32e67cee 100644 --- a/src/applications/phortune/action/PhortuneAddPaymentMethodAction.php +++ b/src/applications/phortune/action/PhortuneAddPaymentMethodAction.php @@ -5,10 +5,6 @@ final class PhortuneAddPaymentMethodAction const TYPECONST = 'phortune.payment-method.add'; - public function getActionConstant() { - return self::TYPECONST; - } - public function getScoreThreshold() { return 60 / phutil_units('1 hour in seconds'); } diff --git a/src/applications/phortune/application/PhabricatorPhortuneApplication.php b/src/applications/phortune/application/PhabricatorPhortuneApplication.php index 0ffc138f65..79b246770e 100644 --- a/src/applications/phortune/application/PhabricatorPhortuneApplication.php +++ b/src/applications/phortune/application/PhabricatorPhortuneApplication.php @@ -34,26 +34,6 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication { return array( '/phortune/' => array( '' => 'PhortuneLandingController', - '(?P\d+)/' => array( - '' => 'PhortuneAccountViewController', - 'card/' => array( - 'new/' => 'PhortunePaymentMethodCreateController', - ), - 'order/(?:query/(?P[^/]+)/)?' - => 'PhortuneCartListController', - 'subscription/' => array( - '(?:query/(?P[^/]+)/)?' - => 'PhortuneSubscriptionListController', - 'view/(?P\d+)/' - => 'PhortuneSubscriptionViewController', - 'edit/(?P\d+)/' - => 'PhortuneSubscriptionEditController', - 'order/(?P\d+)/' - => 'PhortuneCartListController', - ), - 'charge/(?:query/(?P[^/]+)/)?' - => 'PhortuneAccountChargeListController', - ), 'card/(?P\d+)/' => array( 'edit/' => 'PhortunePaymentMethodEditController', 'disable/' => 'PhortunePaymentMethodDisableController', @@ -63,21 +43,59 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication { 'checkout/' => 'PhortuneCartCheckoutController', '(?Pprint)/' => 'PhortuneCartViewController', '(?Pcancel|refund)/' => 'PhortuneCartCancelController', + 'accept/' => 'PhortuneCartAcceptController', + 'void/' => 'PhortuneCartVoidController', 'update/' => 'PhortuneCartUpdateController', ), 'account/' => array( '' => 'PhortuneAccountListController', + $this->getEditRoutePattern('edit/') => 'PhortuneAccountEditController', - 'edit/(?:(?P\d+)/)?' => 'PhortuneAccountEditController', - 'add/manager/(?:(?P\d+)/)?' - => 'PhortuneAccountAddManagerController', - 'billing/(?:(?P\d+)/)?' => 'PhortuneAccountBillingController', - 'subscription/(?:(?P\d+)/)?' - => 'PhortuneAccountSubscriptionController', - 'manager/' => array( - '(?:(?P\d+)/)?' => 'PhortuneAccountManagerController', - 'add/(?:(?P\d+)/)?' => 'PhortuneAccountAddManagerController', + + '(?P\d+)/' => array( + '' => 'PhortuneAccountOverviewController', + 'details/' => 'PhortuneAccountDetailsController', + 'methods/' => array( + '' => 'PhortuneAccountPaymentMethodController', + '(?P\d+)/' => 'PhortuneAccountPaymentMethodViewController', + 'new/' => 'PhortunePaymentMethodCreateController', + ), + 'orders/' => array( + '' => 'PhortuneAccountOrdersController', + $this->getQueryRoutePattern('list/') + => 'PhortuneAccountOrderListController', + ), + 'charges/' => array( + '' => 'PhortuneAccountChargesController', + $this->getQueryRoutePattern('list/') + => 'PhortuneAccountChargeListController', + ), + 'subscriptions/' => array( + '' => 'PhortuneAccountSubscriptionController', + '(?P\d+)/' => array( + '' => 'PhortuneAccountSubscriptionViewController', + 'autopay/(?P\d+)/' + => 'PhortuneAccountSubscriptionAutopayController', + $this->getQueryRoutePattern('orders/') + => 'PhortuneAccountOrderListController', + ), + ), + 'managers/' => array( + '' => 'PhortuneAccountManagersController', + 'add/' => 'PhortuneAccountAddManagerController', + ), + 'addresses/' => array( + '' => 'PhortuneAccountEmailAddressesController', + '(?P\d+)/' => array( + '' => 'PhortuneAccountEmailViewController', + 'rotate/' => 'PhortuneAccountEmailRotateController', + '(?Pdisable|enable)/' + => 'PhortuneAccountEmailStatusController', + ), + $this->getEditRoutePattern('edit/') + => 'PhortuneAccountEmailEditController', + ), ), ), 'product/' => array( @@ -91,37 +109,51 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication { '(?P\d+)/(?P[^/]+)/' => 'PhortuneProviderActionController', ), + 'external/(?P[^/]+)/(?P[^/]+)/' => array( + '' => 'PhortuneExternalOverviewController', + 'unsubscribe/' => 'PhortuneExternalUnsubscribeController', + 'order/' => array( + '(?P[^/]+)/' => array( + '' => 'PhortuneExternalOrderController', + '(?Pprint)/' => 'PhortuneExternalOrderController', + ), + ), + ), 'merchant/' => array( - '(?:query/(?P[^/]+)/)?' => 'PhortuneMerchantListController', - 'picture/(?:(?P\d+)/)?' => 'PhortuneMerchantPictureController', + $this->getQueryRoutePattern() + => 'PhortuneMerchantListController', $this->getEditRoutePattern('edit/') => 'PhortuneMerchantEditController', - 'orders/(?P\d+)/(?:query/(?P[^/]+)/)?' - => 'PhortuneCartListController', - 'manager/' => array( - '(?:(?P\d+)/)?' => 'PhortuneMerchantManagerController', - 'add/(?:(?P\d+)/)?' => 'PhortuneMerchantAddManagerController', - ), '(?P\d+)/' => array( - 'cart/(?P\d+)/' => array( - '' => 'PhortuneCartViewController', - '(?Pcancel|refund)/' => 'PhortuneCartCancelController', - 'update/' => 'PhortuneCartUpdateController', - 'accept/' => 'PhortuneCartAcceptController', + '' => 'PhortuneMerchantOverviewController', + 'details/' => 'PhortuneMerchantDetailsController', + 'providers/' => array( + '' => 'PhortuneMerchantProvidersController', + '(?P\d+)/' => array( + '' => 'PhortuneMerchantProviderViewController', + 'disable/' => 'PhortuneMerchantProviderDisableController', + ), + $this->getEditRoutePattern('edit/') + => 'PhortuneMerchantProviderEditController', ), - 'subscription/' => array( - '(?:query/(?P[^/]+)/)?' - => 'PhortuneSubscriptionListController', - 'view/(?P\d+)/' - => 'PhortuneSubscriptionViewController', - 'order/(?P\d+)/' - => 'PhortuneCartListController', + 'orders/' => array( + '' => 'PhortuneMerchantOrdersController', + $this->getQueryRoutePattern('list/') + => 'PhortuneMerchantOrderListController', ), - 'invoice/' => array( - 'new/' => 'PhortuneMerchantInvoiceCreateController', + 'picture/' => array( + 'edit/' => 'PhortuneMerchantPictureController', + ), + 'subscriptions/' => array( + '' => 'PhortuneMerchantSubscriptionsController', + $this->getQueryRoutePattern('list/') + => 'PhortuneMerchantSubscriptionListController', + ), + 'managers/' => array( + '' => 'PhortuneMerchantManagersController', + 'new/' => 'PhortuneMerchantAddManagerController', ), ), - '(?P\d+)/' => 'PhortuneMerchantViewController', ), ), ); diff --git a/src/applications/phortune/codex/PhortunePaymentMethodPolicyCodex.php b/src/applications/phortune/codex/PhortunePaymentMethodPolicyCodex.php new file mode 100644 index 0000000000..389580147a --- /dev/null +++ b/src/applications/phortune/codex/PhortunePaymentMethodPolicyCodex.php @@ -0,0 +1,36 @@ +getObject(); + + $rules = array(); + + $rules[] = $this->newRule() + ->setCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->setIsActive(true) + ->setDescription( + pht( + 'Account members may view and edit payment methods.')); + + $rules[] = $this->newRule() + ->setCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + )) + ->setIsActive(true) + ->setDescription( + pht( + 'Merchants you have a relationship with may view associated '. + 'payment methods.')); + + return $rules; + } + +} diff --git a/src/applications/phortune/codex/PhortuneSubscriptionPolicyCodex.php b/src/applications/phortune/codex/PhortuneSubscriptionPolicyCodex.php new file mode 100644 index 0000000000..484e215663 --- /dev/null +++ b/src/applications/phortune/codex/PhortuneSubscriptionPolicyCodex.php @@ -0,0 +1,36 @@ +getObject(); + + $rules = array(); + + $rules[] = $this->newRule() + ->setCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->setIsActive(true) + ->setDescription( + pht( + 'Account members may view and edit subscriptions.')); + + $rules[] = $this->newRule() + ->setCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + )) + ->setIsActive(true) + ->setDescription( + pht( + 'Merchants you have a relationship with may view associated '. + 'subscriptions.')); + + return $rules; + } + +} diff --git a/src/applications/phortune/constants/PhortuneAccountEmailStatus.php b/src/applications/phortune/constants/PhortuneAccountEmailStatus.php new file mode 100644 index 0000000000..fa18c3f466 --- /dev/null +++ b/src/applications/phortune/constants/PhortuneAccountEmailStatus.php @@ -0,0 +1,31 @@ + array( + 'name' => pht('Active'), + 'closed' => false, + ), + self::STATUS_DISABLED => array( + 'name' => pht('Disabled'), + 'closed' => true, + ), + self::STATUS_UNSUBSCRIBED => array( + 'name' => pht('Unsubscribed'), + 'closed' => true, + ), + ); + } + +} diff --git a/src/applications/phortune/controller/PhortuneController.php b/src/applications/phortune/controller/PhortuneController.php index 655dcee4e1..37896cc941 100644 --- a/src/applications/phortune/controller/PhortuneController.php +++ b/src/applications/phortune/controller/PhortuneController.php @@ -2,42 +2,6 @@ abstract class PhortuneController extends PhabricatorController { - protected function addAccountCrumb( - $crumbs, - PhortuneAccount $account, - $link = true) { - - $name = $account->getName(); - $href = null; - - if ($link) { - $href = $this->getApplicationURI($account->getID().'/'); - $crumbs->addTextCrumb($name, $href); - } else { - $crumbs->addTextCrumb($name); - } - } - - protected function addMerchantCrumb( - $crumbs, - PhortuneMerchant $merchant, - $link = true) { - - $name = $merchant->getName(); - $href = null; - - $crumbs->addTextCrumb( - pht('Merchants'), - $this->getApplicationURI('merchant/')); - - if ($link) { - $href = $this->getApplicationURI('merchant/'.$merchant->getID().'/'); - $crumbs->addTextCrumb($name, $href); - } else { - $crumbs->addTextCrumb($name); - } - } - private function loadEnabledProvidersForMerchant(PhortuneMerchant $merchant) { $viewer = $this->getRequest()->getUser(); @@ -84,30 +48,4 @@ abstract class PhortuneController extends PhabricatorController { return $providers; } - protected function loadMerchantAuthority() { - $request = $this->getRequest(); - $viewer = $this->getViewer(); - - $is_merchant = (bool)$request->getURIData('merchantID'); - if (!$is_merchant) { - return null; - } - - $merchant = id(new PhortuneMerchantQuery()) - ->setViewer($viewer) - ->withIDs(array($request->getURIData('merchantID'))) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$merchant) { - return null; - } - - $viewer->grantAuthority($merchant); - return $merchant; - } - } diff --git a/src/applications/phortune/controller/PhortuneLandingController.php b/src/applications/phortune/controller/PhortuneLandingController.php index e6906095d2..e20d222a86 100644 --- a/src/applications/phortune/controller/PhortuneLandingController.php +++ b/src/applications/phortune/controller/PhortuneLandingController.php @@ -11,7 +11,7 @@ final class PhortuneLandingController extends PhortuneController { if (count($accounts) == 1) { $account = head($accounts); - $next_uri = $this->getApplicationURI($account->getID().'/'); + $next_uri = $account->getURI(); } else { $next_uri = $this->getApplicationURI('account/'); } diff --git a/src/applications/phortune/controller/account/PhortuneAccountAddManagerController.php b/src/applications/phortune/controller/account/PhortuneAccountAddManagerController.php index 34bb0a480b..4a2b42ab69 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountAddManagerController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountAddManagerController.php @@ -1,27 +1,21 @@ getViewer(); - $id = $request->getURIData('id'); + $account = $this->getAccount(); - $account = id(new PhortuneAccountQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$account) { - return new Aphront404Response(); - } + $id = $account->getID(); $v_managers = array(); $e_managers = null; - $account_uri = $this->getApplicationURI("/account/manager/{$id}/"); + $account_uri = $this->getApplicationURI("/account/{$id}/managers/"); if ($request->isFormPost()) { $xactions = array(); @@ -53,23 +47,34 @@ final class PhortuneAccountAddManagerController extends PhortuneController { } } + $account_phid = $account->getPHID(); + $handles = $viewer->loadHandles(array($account_phid)); + $handle = $handles[$account_phid]; + $form = id(new AphrontFormView()) - ->setUser($viewer) + ->setViewer($viewer) + ->appendInstructions( + pht( + 'Choose one or more users to add as account managers. Managers '. + 'have full control of the account.')) + ->appendControl( + id(new AphrontFormStaticControl()) + ->setLabel(pht('Payment Account')) + ->setValue($handle->renderLink())) ->appendControl( id(new AphrontFormTokenizerControl()) ->setDatasource(new PhabricatorPeopleDatasource()) - ->setLabel(pht('Managers')) + ->setLabel(pht('Add Managers')) ->setName('managerPHIDs') ->setValue($v_managers) ->setError($e_managers)); return $this->newDialog() - ->setTitle(pht('Add New Manager')) + ->setTitle(pht('Add New Managers')) ->appendForm($form) ->setWidth(AphrontDialogView::WIDTH_FORM) ->addCancelButton($account_uri) - ->addSubmitButton(pht('Add Manager')); - + ->addSubmitButton(pht('Add Managers')); } } diff --git a/src/applications/phortune/controller/account/PhortuneAccountBillingController.php b/src/applications/phortune/controller/account/PhortuneAccountBillingController.php deleted file mode 100644 index 0660358af7..0000000000 --- a/src/applications/phortune/controller/account/PhortuneAccountBillingController.php +++ /dev/null @@ -1,161 +0,0 @@ -loadAccount(); - if ($response) { - return $response; - } - - $account = $this->getAccount(); - $title = $account->getName(); - - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('Billing')); - - $header = $this->buildHeaderView(); - $methods = $this->buildPaymentMethodsSection($account); - $charge_history = $this->buildChargeHistorySection($account); - - $view = id(new PHUITwoColumnView()) - ->setHeader($header) - ->setFooter(array( - $methods, - $charge_history, - )); - - $navigation = $this->buildSideNavView('billing'); - - return $this->newPage() - ->setTitle($title) - ->setCrumbs($crumbs) - ->setNavigation($navigation) - ->appendChild($view); - - } - - private function buildPaymentMethodsSection(PhortuneAccount $account) { - $viewer = $this->getViewer(); - - $can_edit = PhabricatorPolicyFilter::hasCapability( - $viewer, - $account, - PhabricatorPolicyCapability::CAN_EDIT); - - $id = $account->getID(); - - // TODO: Allow adding a card here directly - $add = id(new PHUIButtonView()) - ->setTag('a') - ->setText(pht('New Payment Method')) - ->setIcon('fa-plus') - ->setHref($this->getApplicationURI("{$id}/card/new/")); - - $header = id(new PHUIHeaderView()) - ->setHeader(pht('Payment Methods')); - - $list = id(new PHUIObjectItemListView()) - ->setUser($viewer) - ->setFlush(true) - ->setNoDataString( - pht('No payment methods associated with this account.')); - - $methods = id(new PhortunePaymentMethodQuery()) - ->setViewer($viewer) - ->withAccountPHIDs(array($account->getPHID())) - ->withStatuses( - array( - PhortunePaymentMethod::STATUS_ACTIVE, - )) - ->execute(); - - foreach ($methods as $method) { - $id = $method->getID(); - - $item = new PHUIObjectItemView(); - $item->setHeader($method->getFullDisplayName()); - - switch ($method->getStatus()) { - case PhortunePaymentMethod::STATUS_ACTIVE: - $item->setStatusIcon('fa-check green'); - - $disable_uri = $this->getApplicationURI('card/'.$id.'/disable/'); - $item->addAction( - id(new PHUIListItemView()) - ->setIcon('fa-times') - ->setHref($disable_uri) - ->setDisabled(!$can_edit) - ->setWorkflow(true)); - break; - case PhortunePaymentMethod::STATUS_DISABLED: - $item->setStatusIcon('fa-ban lightbluetext'); - $item->setDisabled(true); - break; - } - - $provider = $method->buildPaymentProvider(); - $item->addAttribute($provider->getPaymentMethodProviderDescription()); - - $edit_uri = $this->getApplicationURI('card/'.$id.'/edit/'); - - $item->addAction( - id(new PHUIListItemView()) - ->setIcon('fa-pencil') - ->setHref($edit_uri) - ->setDisabled(!$can_edit) - ->setWorkflow(!$can_edit)); - - $list->addItem($item); - } - - return id(new PHUIObjectBoxView()) - ->setHeader($header) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->setObjectList($list); - } - - private function buildChargeHistorySection(PhortuneAccount $account) { - $viewer = $this->getViewer(); - - $charges = id(new PhortuneChargeQuery()) - ->setViewer($viewer) - ->withAccountPHIDs(array($account->getPHID())) - ->needCarts(true) - ->setLimit(10) - ->execute(); - - $phids = array(); - foreach ($charges as $charge) { - $phids[] = $charge->getProviderPHID(); - $phids[] = $charge->getCartPHID(); - $phids[] = $charge->getMerchantPHID(); - $phids[] = $charge->getPaymentMethodPHID(); - } - - $handles = $this->loadViewerHandles($phids); - - $charges_uri = $this->getApplicationURI($account->getID().'/charge/'); - - $table = id(new PhortuneChargeTableView()) - ->setUser($viewer) - ->setCharges($charges) - ->setHandles($handles); - - $header = id(new PHUIHeaderView()) - ->setHeader(pht('Charge History')) - ->addActionLink( - id(new PHUIButtonView()) - ->setTag('a') - ->setIcon('fa-list') - ->setHref($charges_uri) - ->setText(pht('View All Charges'))); - - return id(new PHUIObjectBoxView()) - ->setHeader($header) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->setTable($table); - } - -} diff --git a/src/applications/phortune/controller/account/PhortuneAccountChargeListController.php b/src/applications/phortune/controller/account/PhortuneAccountChargeListController.php index ed3f901675..882e37f3de 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountChargeListController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountChargeListController.php @@ -1,71 +1,32 @@ getViewer(); - $querykey = $request->getURIData('queryKey'); - $account_id = $request->getURIData('accountID'); - - $engine = new PhortuneChargeSearchEngine(); - - if ($account_id) { - $account = id(new PhortuneAccountQuery()) - ->setViewer($viewer) - ->withIDs(array($account_id)) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$account) { - return new Aphront404Response(); - } - $this->account = $account; - $engine->setAccount($account); - } else { - return new Aphront404Response(); - } - - $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($querykey) - ->setSearchEngine($engine) - ->setNavigation($this->buildSideNavView()); - - return $this->delegateToController($controller); + protected function shouldRequireAccountEditCapability() { + return false; } - public function buildSideNavView() { - $viewer = $this->getViewer(); + protected function handleAccountRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $account = $this->getAccount(); - $nav = new AphrontSideNavFilterView(); - $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); - - id(new PhortuneChargeSearchEngine()) - ->setViewer($viewer) - ->addNavigationItems($nav->getMenu()); - - $nav->selectFilter(null); - - return $nav; + return id(new PhortuneChargeSearchEngine()) + ->setAccount($account) + ->setController($this) + ->buildResponse(); } protected function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); - $account = $this->account; - if ($account) { + if ($this->hasAccount()) { + $account = $this->getAccount(); $id = $account->getID(); - $crumbs->addTextCrumb( - $account->getName(), - $this->getApplicationURI("{$id}/")); + $crumbs->addTextCrumb( pht('Charges'), - $this->getApplicationURI("{$id}/charge/")); + $account->getChargesURI()); } return $crumbs; diff --git a/src/applications/phortune/controller/account/PhortuneAccountChargesController.php b/src/applications/phortune/controller/account/PhortuneAccountChargesController.php new file mode 100644 index 0000000000..ea6105504f --- /dev/null +++ b/src/applications/phortune/controller/account/PhortuneAccountChargesController.php @@ -0,0 +1,70 @@ +getAccount(); + $title = $account->getName(); + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb(pht('Orders')) + ->setBorder(true); + + $header = $this->buildHeaderView(); + $authority = $this->newAccountAuthorityView(); + $charge_history = $this->buildChargeHistorySection($account); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter( + array( + $authority, + $charge_history, + )); + + $navigation = $this->buildSideNavView('charges'); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->setNavigation($navigation) + ->appendChild($view); + } + + private function buildChargeHistorySection(PhortuneAccount $account) { + $viewer = $this->getViewer(); + + $charges = id(new PhortuneChargeQuery()) + ->setViewer($viewer) + ->withAccountPHIDs(array($account->getPHID())) + ->needCarts(true) + ->setLimit(100) + ->execute(); + + $charges_uri = $account->getChargeListURI(); + + $table = id(new PhortuneChargeTableView()) + ->setUser($viewer) + ->setCharges($charges); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Recent Charges')) + ->addActionLink( + id(new PHUIButtonView()) + ->setTag('a') + ->setIcon('fa-list') + ->setHref($charges_uri) + ->setText(pht('View All Charges'))); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setTable($table); + } + +} diff --git a/src/applications/phortune/controller/account/PhortuneAccountController.php b/src/applications/phortune/controller/account/PhortuneAccountController.php index 2ba3b393a2..d3d60d2789 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountController.php @@ -4,61 +4,159 @@ abstract class PhortuneAccountController extends PhortuneController { private $account; + private $merchants; + + final public function handleRequest(AphrontRequest $request) { + if ($this->shouldRequireAccountEditCapability()) { + $response = $this->loadAccountForEdit(); + } else { + $response = $this->loadAccountForView(); + } + + if ($response) { + return $response; + } + + return $this->handleAccountRequest($request); + } + + abstract protected function shouldRequireAccountEditCapability(); + abstract protected function handleAccountRequest(AphrontRequest $request); + + final protected function hasAccount() { + return (bool)$this->account; + } + + final protected function getAccount() { + if ($this->account === null) { + throw new Exception( + pht( + 'Unable to "getAccount()" before loading or setting account '. + 'context.')); + } - protected function getAccount() { return $this->account; } protected function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); - $account = $this->getAccount(); - if ($account) { + // If we hit a policy exception, we can make it here without finding + // an account. + if ($this->hasAccount()) { + $account = $this->getAccount(); $crumbs->addTextCrumb($account->getName(), $account->getURI()); } return $crumbs; } - protected function loadAccount() { - // TODO: Currently, you must be able to edit an account to view the detail - // page, because the account must be broadly visible so merchants can - // process orders but merchants should not be able to see all the details - // of an account. Ideally the profile pages should be visible to merchants, - // too, just with less information. - return $this->loadAccountForEdit(); + private function loadAccountForEdit() { + return $this->loadAccountWithCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )); } + private function loadAccountForView() { + return $this->loadAccountWithCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + )); + } - protected function loadAccountForEdit() { + private function loadAccountWithCapabilities(array $capabilities) { $viewer = $this->getViewer(); $request = $this->getRequest(); $account_id = $request->getURIData('accountID'); if (!$account_id) { - $account_id = $request->getURIData('id'); - } - - if (!$account_id) { - return new Aphront404Response(); + throw new Exception( + pht( + 'Controller ("%s") extends controller "%s", but is reachable '. + 'with no "accountID" in URI.', + get_class($this), + __CLASS__)); } $account = id(new PhortuneAccountQuery()) ->setViewer($viewer) ->withIDs(array($account_id)) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) + ->requireCapabilities($capabilities) ->executeOne(); if (!$account) { return new Aphront404Response(); } - $this->account = $account; + $this->setAccount($account); return null; } + private function setAccount(PhortuneAccount $account) { + $this->account = $account; + + $viewer = $this->getViewer(); + if (!$account->isUserAccountMember($viewer)) { + $merchant_phids = $account->getMerchantPHIDs(); + $merchants = id(new PhortuneMerchantQuery()) + ->setViewer($viewer) + ->withPHIDs($merchant_phids) + ->withMemberPHIDs(array($viewer->getPHID())) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->execute(); + + $this->merchants = $merchants; + } else { + $this->merchants = array(); + } + + return $this; + } + + final protected function getMerchants() { + if ($this->merchants === null) { + throw new Exception( + pht( + 'Unable to "getMerchants()" before loading or setting account '. + 'context.')); + } + + return $this->merchants; + } + + final protected function newAccountAuthorityView() { + $viewer = $this->getViewer(); + + $merchants = $this->getMerchants(); + if (!$merchants) { + return null; + } + + $merchant_phids = mpull($merchants, 'getPHID'); + $merchant_handles = $viewer->loadHandles($merchant_phids); + $merchant_handles = iterator_to_array($merchant_handles); + + $merchant_list = mpull($merchant_handles, 'renderLink'); + $merchant_list = phutil_implode_html(', ', $merchant_list); + + $merchant_message = pht( + 'You can view this account because you control %d merchant(s) it '. + 'has a relationship with: %s.', + phutil_count($merchants), + $merchant_list); + + return id(new PHUIInfoView()) + ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) + ->setErrors( + array( + $merchant_message, + )); + } + } diff --git a/src/applications/phortune/controller/account/PhortuneAccountDetailsController.php b/src/applications/phortune/controller/account/PhortuneAccountDetailsController.php new file mode 100644 index 0000000000..e6003c8159 --- /dev/null +++ b/src/applications/phortune/controller/account/PhortuneAccountDetailsController.php @@ -0,0 +1,155 @@ +getAccount(); + $title = $account->getName(); + + $viewer = $this->getViewer(); + + $invoices = id(new PhortuneCartQuery()) + ->setViewer($viewer) + ->withAccountPHIDs(array($account->getPHID())) + ->needPurchases(true) + ->withInvoices(true) + ->execute(); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->setBorder(true); + + $header = $this->buildHeaderView(); + + $authority = $this->newAccountAuthorityView(); + $details = $this->newDetailsView($account); + + $curtain = $this->buildCurtainView($account); + + $timeline = $this->buildTransactionTimeline( + $account, + new PhortuneAccountTransactionQuery()); + $timeline->setShouldTerminate(true); + + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->setMainColumn( + array( + $authority, + $details, + $timeline, + )); + + $navigation = $this->buildSideNavView('details'); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->setNavigation($navigation) + ->appendChild($view); + + } + + private function buildCurtainView(PhortuneAccount $account) { + $viewer = $this->getViewer(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $account, + PhabricatorPolicyCapability::CAN_EDIT); + + $edit_uri = $this->getApplicationURI('account/edit/'.$account->getID().'/'); + + $curtain = $this->newCurtainView($account); + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Account')) + ->setIcon('fa-pencil') + ->setHref($edit_uri) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + + $member_phids = $account->getMemberPHIDs(); + $handles = $viewer->loadHandles($member_phids); + + $member_list = id(new PHUIObjectItemListView()) + ->setSimple(true); + + foreach ($member_phids as $member_phid) { + $image_uri = $handles[$member_phid]->getImageURI(); + $image_href = $handles[$member_phid]->getURI(); + $person = $handles[$member_phid]; + + $member = id(new PHUIObjectItemView()) + ->setImageURI($image_uri) + ->setHref($image_href) + ->setHeader($person->getFullName()); + + $member_list->addItem($member); + } + + $curtain->newPanel() + ->setHeaderText(pht('Managers')) + ->appendChild($member_list); + + $merchant_list = id(new PHUIObjectItemListView()) + ->setSimple(true) + ->setNoDataString(pht('No purchase history.')); + + $merchant_phids = $account->getMerchantPHIDs(); + $handles = $viewer->loadHandles($merchant_phids); + + foreach ($merchant_phids as $merchant_phid) { + $handle = $handles[$merchant_phid]; + + $merchant = id(new PHUIObjectItemView()) + ->setImageURI($handle->getImageURI()) + ->setHref($handle->getURI()) + ->setHeader($handle->getFullName()); + + $merchant_list->addItem($merchant); + } + + $curtain->newPanel() + ->setHeaderText(pht('Merchants')) + ->appendChild($merchant_list); + + return $curtain; + } + + private function newDetailsView(PhortuneAccount $account) { + $viewer = $this->getViewer(); + + $view = id(new PHUIPropertyListView()) + ->setUser($viewer); + + $view->addProperty(pht('Account Name'), $account->getName()); + + $display_name = $account->getBillingName(); + if (!strlen($display_name)) { + $display_name = phutil_tag('em', array(), pht('None')); + } + + $display_address = $account->getBillingAddress(); + if (!strlen($display_address)) { + $display_address = phutil_tag('em', array(), pht('None')); + } else { + $display_address = phutil_escape_html_newlines($display_address); + } + + $view->addProperty(pht('Billing Name'), $display_name); + $view->addProperty(pht('Billing Address'), $display_address); + + return id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Account Details')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->addPropertyList($view); + } + +} diff --git a/src/applications/phortune/controller/account/PhortuneAccountEmailAddressesController.php b/src/applications/phortune/controller/account/PhortuneAccountEmailAddressesController.php new file mode 100644 index 0000000000..a946e02efb --- /dev/null +++ b/src/applications/phortune/controller/account/PhortuneAccountEmailAddressesController.php @@ -0,0 +1,92 @@ +getAccount(); + $title = $account->getName(); + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb(pht('Email Addresses')) + ->setBorder(true); + + $header = $this->buildHeaderView(); + $authority = $this->newAccountAuthorityView(); + $addresses = $this->buildAddressesSection($account); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter( + array( + $authority, + $addresses, + )); + + $navigation = $this->buildSideNavView('addresses'); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->setNavigation($navigation) + ->appendChild($view); + } + + private function buildAddressesSection(PhortuneAccount $account) { + $viewer = $this->getViewer(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $account, + PhabricatorPolicyCapability::CAN_EDIT); + + $id = $account->getID(); + + $add = id(new PHUIButtonView()) + ->setTag('a') + ->setText(pht('Add Address')) + ->setIcon('fa-plus') + ->setWorkflow(!$can_edit) + ->setDisabled(!$can_edit) + ->setHref("/phortune/account/{$id}/addresses/edit/"); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Billing Email Addresses')) + ->addActionLink($add); + + $addresses = id(new PhortuneAccountEmailQuery()) + ->setViewer($viewer) + ->withAccountPHIDs(array($account->getPHID())) + ->execute(); + + $list = id(new PHUIObjectItemListView()) + ->setUser($viewer) + ->setNoDataString( + pht( + 'There are no billing email addresses associated '. + 'with this account.')); + + $addresses = id(new PhortuneAccountEmailQuery()) + ->setViewer($viewer) + ->withAccountPHIDs(array($account->getPHID())) + ->execute(); + foreach ($addresses as $address) { + $item = id(new PHUIObjectItemView()) + ->setObjectName($address->getObjectName()) + ->setHeader($address->getAddress()) + ->setHref($address->getURI()); + + $list->addItem($item); + } + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setObjectList($list); + } + +} diff --git a/src/applications/phortune/controller/account/PhortuneAccountEmailEditController.php b/src/applications/phortune/controller/account/PhortuneAccountEmailEditController.php new file mode 100644 index 0000000000..117139fd55 --- /dev/null +++ b/src/applications/phortune/controller/account/PhortuneAccountEmailEditController.php @@ -0,0 +1,22 @@ +getAccount(); + + $engine = id(new PhortuneAccountEmailEditEngine()) + ->setController($this); + + if (!$request->getURIData('id')) { + $engine->setAccount($account); + } + + return $engine->buildResponse(); + } +} diff --git a/src/applications/phortune/controller/account/PhortuneAccountEmailRotateController.php b/src/applications/phortune/controller/account/PhortuneAccountEmailRotateController.php new file mode 100644 index 0000000000..2155ebcd20 --- /dev/null +++ b/src/applications/phortune/controller/account/PhortuneAccountEmailRotateController.php @@ -0,0 +1,62 @@ +getViewer(); + $account = $this->getAccount(); + + $address = id(new PhortuneAccountEmailQuery()) + ->setViewer($viewer) + ->withAccountPHIDs(array($account->getPHID())) + ->withIDs(array($request->getURIData('addressID'))) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$address) { + return new Aphront404Response(); + } + + $address_uri = $address->getURI(); + + if ($request->isFormOrHisecPost()) { + $xactions = array(); + + $xactions[] = $address->getApplicationTransactionTemplate() + ->setTransactionType( + PhortuneAccountEmailRotateTransaction::TRANSACTIONTYPE) + ->setNewValue(true); + + $address->getApplicationTransactionEditor() + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnMissingFields(true) + ->setContinueOnNoEffect(true) + ->setCancelURI($address_uri) + ->applyTransactions($address, $xactions); + + return id(new AphrontRedirectResponse())->setURI($address_uri); + } + + return $this->newDialog() + ->setTitle(pht('Rotate Access Key')) + ->appendParagraph( + pht( + 'Rotate the access key for email address %s?', + phutil_tag('strong', array(), $address->getAddress()))) + ->appendParagraph( + pht( + 'Existing access links which have been sent to this email address '. + 'will stop working.')) + ->addSubmitButton(pht('Rotate Access Key')) + ->addCancelButton($address_uri); + } +} diff --git a/src/applications/phortune/controller/account/PhortuneAccountEmailStatusController.php b/src/applications/phortune/controller/account/PhortuneAccountEmailStatusController.php new file mode 100644 index 0000000000..c11564b000 --- /dev/null +++ b/src/applications/phortune/controller/account/PhortuneAccountEmailStatusController.php @@ -0,0 +1,137 @@ +getViewer(); + $account = $this->getAccount(); + + $address = id(new PhortuneAccountEmailQuery()) + ->setViewer($viewer) + ->withAccountPHIDs(array($account->getPHID())) + ->withIDs(array($request->getURIData('addressID'))) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$address) { + return new Aphront404Response(); + } + + $address_uri = $address->getURI(); + + $is_enable = false; + $is_disable = false; + + $old_status = $address->getStatus(); + switch ($request->getURIData('action')) { + case 'enable': + if ($old_status === PhortuneAccountEmailStatus::STATUS_ACTIVE) { + return $this->newDialog() + ->setTitle(pht('Already Enabled')) + ->appendParagraph( + pht( + 'You can not enable this address because it is already '. + 'active.')) + ->addCancelButton($address_uri); + } + + if ($old_status === PhortuneAccountEmailStatus::STATUS_UNSUBSCRIBED) { + return $this->newDialog() + ->setTitle(pht('Permanently Unsubscribed')) + ->appendParagraph( + pht( + 'You can not enable this address because it has been '. + 'permanently unsubscribed.')) + ->addCancelButton($address_uri); + } + + $new_status = PhortuneAccountEmailStatus::STATUS_ACTIVE; + $is_enable = true; + break; + case 'disable': + if ($old_status === PhortuneAccountEmailStatus::STATUS_DISABLED) { + return $this->newDialog() + ->setTitle(pht('Already Disabled')) + ->appendParagraph( + pht( + 'You can not disabled this address because it is already '. + 'disabled.')) + ->addCancelButton($address_uri); + } + + if ($old_status === PhortuneAccountEmailStatus::STATUS_UNSUBSCRIBED) { + return $this->newDialog() + ->setTitle(pht('Permanently Unsubscribed')) + ->appendParagraph( + pht( + 'You can not disable this address because it has been '. + 'permanently unsubscribed.')) + ->addCancelButton($address_uri); + } + + $new_status = PhortuneAccountEmailStatus::STATUS_DISABLED; + $is_disable = true; + break; + default: + return new Aphront404Response(); + } + + if ($request->isFormOrHisecPost()) { + $xactions = array(); + + $xactions[] = $address->getApplicationTransactionTemplate() + ->setTransactionType( + PhortuneAccountEmailStatusTransaction::TRANSACTIONTYPE) + ->setNewValue($new_status); + + $address->getApplicationTransactionEditor() + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnMissingFields(true) + ->setContinueOnNoEffect(true) + ->setCancelURI($address_uri) + ->applyTransactions($address, $xactions); + + return id(new AphrontRedirectResponse())->setURI($address_uri); + } + + $dialog = $this->newDialog(); + + $body = array(); + + if ($is_disable) { + $title = pht('Disable Address'); + + $body[] = pht( + 'This address will no longer receive email, and access links will '. + 'no longer function.'); + + $submit = pht('Disable Address'); + } else { + $title = pht('Enable Address'); + + $body[] = pht( + 'This address will receive email again, and existing links '. + 'to access order history will work again.'); + + $submit = pht('Enable Address'); + } + + foreach ($body as $graph) { + $dialog->appendParagraph($graph); + } + + return $dialog + ->setTitle($title) + ->addCancelButton($address_uri) + ->addSubmitButton($submit); + } +} diff --git a/src/applications/phortune/controller/account/PhortuneAccountEmailViewController.php b/src/applications/phortune/controller/account/PhortuneAccountEmailViewController.php new file mode 100644 index 0000000000..4e15210890 --- /dev/null +++ b/src/applications/phortune/controller/account/PhortuneAccountEmailViewController.php @@ -0,0 +1,176 @@ +getViewer(); + $account = $this->getAccount(); + + $address = id(new PhortuneAccountEmailQuery()) + ->setViewer($viewer) + ->withAccountPHIDs(array($account->getPHID())) + ->withIDs(array($request->getURIData('addressID'))) + ->executeOne(); + if (!$address) { + return new Aphront404Response(); + } + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb(pht('Email Addresses'), $account->getEmailAddressesURI()) + ->addTextCrumb($address->getObjectName()) + ->setBorder(true); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Account Email: %s', $address->getAddress())); + + $details = $this->newDetailsView($address); + + $timeline = $this->buildTransactionTimeline( + $address, + new PhortuneAccountEmailTransactionQuery()); + $timeline->setShouldTerminate(true); + + $curtain = $this->buildCurtainView($address); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->setMainColumn( + array( + $details, + $timeline, + )); + + return $this->newPage() + ->setTitle($address->getObjectName()) + ->setCrumbs($crumbs) + ->appendChild($view); + } + + private function buildCurtainView(PhortuneAccountEmail $address) { + $viewer = $this->getViewer(); + $account = $address->getAccount(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $address, + PhabricatorPolicyCapability::CAN_EDIT); + + $edit_uri = $this->getApplicationURI( + urisprintf( + 'account/%d/addresses/edit/%d/', + $account->getID(), + $address->getID())); + + if ($can_edit) { + $external_uri = $address->getExternalURI(); + } else { + $external_uri = null; + } + + $curtain = $this->newCurtainView($account); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Address')) + ->setIcon('fa-pencil') + ->setHref($edit_uri) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + + switch ($address->getStatus()) { + case PhortuneAccountEmailStatus::STATUS_ACTIVE: + $disable_name = pht('Disable Address'); + $disable_icon = 'fa-times'; + $can_disable = true; + $disable_action = 'disable'; + break; + case PhortuneAccountEmailStatus::STATUS_DISABLED: + $disable_name = pht('Enable Address'); + $disable_icon = 'fa-check'; + $can_disable = true; + $disable_action = 'enable'; + break; + case PhortuneAccountEmailStatus::STATUS_UNSUBSCRIBED: + $disable_name = pht('Disable Address'); + $disable_icon = 'fa-times'; + $can_disable = false; + $disable_action = 'disable'; + break; + } + + $disable_uri = $this->getApplicationURI( + urisprintf( + 'account/%d/addresses/%d/%s/', + $account->getID(), + $address->getID(), + $disable_action)); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName($disable_name) + ->setIcon($disable_icon) + ->setHref($disable_uri) + ->setDisabled(!$can_disable) + ->setWorkflow(true)); + + $rotate_uri = $this->getApplicationURI( + urisprintf( + 'account/%d/addresses/%d/rotate/', + $account->getID(), + $address->getID())); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Rotate Access Key')) + ->setIcon('fa-refresh') + ->setHref($rotate_uri) + ->setDisabled(!$can_edit) + ->setWorkflow(true)); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Show External View')) + ->setIcon('fa-eye') + ->setHref($external_uri) + ->setDisabled(!$can_edit) + ->setOpenInNewWindow(true)); + + return $curtain; + } + + private function newDetailsView(PhortuneAccountEmail $address) { + $viewer = $this->getViewer(); + + $view = id(new PHUIPropertyListView()) + ->setUser($viewer); + + $access_key = $address->getAccessKey(); + + // This is not a meaningful security barrier: the full plaintext of the + // access key is visible on the page in the link target of the "Show + // External View" action. It's just here to make it clear "Rotate Access + // Key" actually does something. + + $prefix_length = 4; + $visible_part = substr($access_key, 0, $prefix_length); + $masked_part = str_repeat( + "\xE2\x80\xA2", + strlen($access_key) - $prefix_length); + $access_display = $visible_part.$masked_part; + $access_display = phutil_tag('tt', array(), $access_display); + + $view->addProperty(pht('Email Address'), $address->getAddress()); + $view->addProperty(pht('Access Key'), $access_display); + + return id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Email Address Details')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->addPropertyList($view); + } +} diff --git a/src/applications/phortune/controller/account/PhortuneAccountListController.php b/src/applications/phortune/controller/account/PhortuneAccountListController.php index 177f84d75d..0ac8cecd8b 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountListController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountListController.php @@ -38,7 +38,7 @@ final class PhortuneAccountListController extends PhortuneController { $item = id(new PHUIObjectItemView()) ->setSubhead(pht('Account %d', $account->getID())) ->setHeader($account->getName()) - ->setHref($this->getApplicationURI($account->getID().'/')) + ->setHref($account->getURI()) ->setObject($account) ->setImageIcon('fa-user-circle'); diff --git a/src/applications/phortune/controller/account/PhortuneAccountManagerController.php b/src/applications/phortune/controller/account/PhortuneAccountManagersController.php similarity index 76% rename from src/applications/phortune/controller/account/PhortuneAccountManagerController.php rename to src/applications/phortune/controller/account/PhortuneAccountManagersController.php index 502fbfe52e..538fadeac9 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountManagerController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountManagersController.php @@ -1,28 +1,31 @@ loadAccount(); - if ($response) { - return $response; - } + protected function shouldRequireAccountEditCapability() { + return false; + } + protected function handleAccountRequest(AphrontRequest $request) { $account = $this->getAccount(); $title = $account->getName(); - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('Managers')); + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb(pht('Managers')) + ->setBorder(true); $header = $this->buildHeaderView(); + $authority = $this->newAccountAuthorityView(); $members = $this->buildMembersSection($account); $view = id(new PHUITwoColumnView()) ->setHeader($header) - ->setFooter(array( - $members, - )); + ->setFooter( + array( + $authority, + $members, + )); $navigation = $this->buildSideNavView('managers'); @@ -46,11 +49,11 @@ final class PhortuneAccountManagerController $add = id(new PHUIButtonView()) ->setTag('a') - ->setText(pht('New Manager')) + ->setText(pht('Add Managers')) ->setIcon('fa-plus') ->setWorkflow(true) ->setDisabled(!$can_edit) - ->setHref("/phortune/account/manager/add/{$id}/"); + ->setHref("/phortune/account/{$id}/managers/add/"); $header = id(new PHUIHeaderView()) ->setHeader(pht('Account Managers')) diff --git a/src/applications/phortune/controller/account/PhortuneAccountOrderListController.php b/src/applications/phortune/controller/account/PhortuneAccountOrderListController.php new file mode 100644 index 0000000000..afdfc9fcce --- /dev/null +++ b/src/applications/phortune/controller/account/PhortuneAccountOrderListController.php @@ -0,0 +1,55 @@ +getViewer(); + $account = $this->getAccount(); + + $engine = id(new PhortuneCartSearchEngine()) + ->setController($this) + ->setAccount($account); + + $subscription_id = $request->getURIData('subscriptionID'); + if ($subscription_id) { + $subscription = id(new PhortuneSubscriptionQuery()) + ->setViewer($viewer) + ->withIDs(array($subscription_id)) + ->executeOne(); + if (!$subscription) { + return new Aphront404Response(); + } + + $engine->setSubscription($subscription); + $this->subscription = $subscription; + } + + return $engine->buildResponse(); + } + + protected function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + + $subscription = $this->subscription; + if ($subscription) { + $crumbs->addTextCrumb( + $subscription->getObjectName(), + $subscription->getURI()); + } else if ($this->hasAccount()) { + $account = $this->getAccount(); + + $crumbs->addTextCrumb(pht('Orders'), $account->getOrdersURI()); + } + + return $crumbs; + } + + +} diff --git a/src/applications/phortune/controller/account/PhortuneAccountOrdersController.php b/src/applications/phortune/controller/account/PhortuneAccountOrdersController.php new file mode 100644 index 0000000000..6ca57453ed --- /dev/null +++ b/src/applications/phortune/controller/account/PhortuneAccountOrdersController.php @@ -0,0 +1,40 @@ +getAccount(); + $title = $account->getName(); + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb(pht('Orders')) + ->setBorder(true); + + $header = $this->buildHeaderView(); + $authority = $this->newAccountAuthorityView(); + + $order_history = $this->newRecentOrdersView($account, 100); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter( + array( + $authority, + $order_history, + )); + + $navigation = $this->buildSideNavView('orders'); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->setNavigation($navigation) + ->appendChild($view); + } + +} diff --git a/src/applications/phortune/controller/account/PhortuneAccountOverviewController.php b/src/applications/phortune/controller/account/PhortuneAccountOverviewController.php new file mode 100644 index 0000000000..3b8e8497db --- /dev/null +++ b/src/applications/phortune/controller/account/PhortuneAccountOverviewController.php @@ -0,0 +1,126 @@ +getAccount(); + $title = $account->getName(); + + $viewer = $this->getViewer(); + + $invoices = id(new PhortuneCartQuery()) + ->setViewer($viewer) + ->withAccountPHIDs(array($account->getPHID())) + ->needPurchases(true) + ->withInvoices(true) + ->execute(); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->setBorder(true); + + $header = $this->buildHeaderView(); + + $authority = $this->newAccountAuthorityView(); + $status = $this->buildStatusView($account, $invoices); + $invoices = $this->buildInvoicesSection($account, $invoices); + $purchase_history = $this->newRecentOrdersView($account, 10); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter( + array( + $authority, + $status, + $invoices, + $purchase_history, + )); + + $navigation = $this->buildSideNavView('overview'); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->setNavigation($navigation) + ->appendChild($view); + } + + private function buildStatusView(PhortuneAccount $account, $invoices) { + $status_items = $this->getStatusItemsForAccount($account, $invoices); + $view = array(); + foreach ($status_items as $item) { + $view[] = id(new PHUIInfoView()) + ->setSeverity(idx($item, 'severity')) + ->appendChild(idx($item, 'note')); + } + return $view; + } + + private function buildInvoicesSection( + PhortuneAccount $account, + array $carts) { + + $viewer = $this->getViewer(); + + $table = id(new PhortuneOrderTableView()) + ->setNoDataString(pht('You have no unpaid invoices.')) + ->setIsInvoices(true) + ->setUser($viewer) + ->setCarts($carts); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Invoices Due')); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setTable($table); + } + + protected function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + + $crumbs->addAction( + id(new PHUIListItemView()) + ->setIcon('fa-exchange') + ->setHref($this->getApplicationURI('account/')) + ->setName(pht('Switch Accounts'))); + + return $crumbs; + } + + private function getStatusItemsForAccount( + PhortuneAccount $account, + array $invoices) { + $viewer = $this->getViewer(); + + assert_instances_of($invoices, 'PhortuneCart'); + $items = array(); + + $methods = id(new PhortunePaymentMethodQuery()) + ->setViewer($viewer) + ->withAccountPHIDs(array($account->getPHID())) + ->withStatuses( + array( + PhortunePaymentMethod::STATUS_ACTIVE, + )) + ->execute(); + + if ($invoices) { + $items[] = array( + 'severity' => PHUIInfoView::SEVERITY_ERROR, + 'note' => pht('You have %d unpaid invoice(s).', count($invoices)), + ); + } + + // TODO: If a payment method has expired or is expiring soon, we should + // add a status check for it. + + return $items; + } + +} diff --git a/src/applications/phortune/controller/account/PhortuneAccountPaymentMethodController.php b/src/applications/phortune/controller/account/PhortuneAccountPaymentMethodController.php new file mode 100644 index 0000000000..05a2f33d8d --- /dev/null +++ b/src/applications/phortune/controller/account/PhortuneAccountPaymentMethodController.php @@ -0,0 +1,96 @@ +getAccount(); + $title = $account->getName(); + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb(pht('Payment Methods')) + ->setBorder(true); + + $authority = $this->newAccountAuthorityView(); + $header = $this->buildHeaderView(); + $methods = $this->buildPaymentMethodsSection($account); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter( + array( + $authority, + $methods, + )); + + $navigation = $this->buildSideNavView('methods'); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->setNavigation($navigation) + ->appendChild($view); + } + + private function buildPaymentMethodsSection(PhortuneAccount $account) { + $viewer = $this->getViewer(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $account, + PhabricatorPolicyCapability::CAN_EDIT); + + $id = $account->getID(); + + $add = id(new PHUIButtonView()) + ->setTag('a') + ->setText(pht('Add Payment Method')) + ->setIcon('fa-plus') + ->setHref($this->getApplicationURI("account/{$id}/methods/new/")) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Payment Methods')) + ->addActionLink($add); + + $list = id(new PHUIObjectItemListView()) + ->setUser($viewer) + ->setFlush(true) + ->setNoDataString( + pht('There are no payment methods associated with this account.')); + + $methods = id(new PhortunePaymentMethodQuery()) + ->setViewer($viewer) + ->withAccountPHIDs(array($account->getPHID())) + ->withStatuses( + array( + PhortunePaymentMethod::STATUS_ACTIVE, + )) + ->execute(); + + foreach ($methods as $method) { + $id = $method->getID(); + + $item = id(new PHUIObjectItemView()) + ->setObjectName($method->getObjectName()) + ->setHeader($method->getFullDisplayName()) + ->setHref($method->getURI()); + + $provider = $method->buildPaymentProvider(); + $item->addAttribute($provider->getPaymentMethodProviderDescription()); + + $list->addItem($item); + } + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setObjectList($list); + } + +} diff --git a/src/applications/phortune/controller/account/PhortuneAccountPaymentMethodViewController.php b/src/applications/phortune/controller/account/PhortuneAccountPaymentMethodViewController.php new file mode 100644 index 0000000000..00c7e9798e --- /dev/null +++ b/src/applications/phortune/controller/account/PhortuneAccountPaymentMethodViewController.php @@ -0,0 +1,154 @@ +getViewer(); + $account = $this->getAccount(); + + $method = id(new PhortunePaymentMethodQuery()) + ->setViewer($viewer) + ->withAccountPHIDs(array($account->getPHID())) + ->withIDs(array($request->getURIData('id'))) + ->withStatuses( + array( + PhortunePaymentMethod::STATUS_ACTIVE, + )) + ->executeOne(); + if (!$method) { + return new Aphront404Response(); + } + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb(pht('Payment Methods'), $account->getPaymentMethodsURI()) + ->addTextCrumb($method->getObjectName()) + ->setBorder(true); + + $header = id(new PHUIHeaderView()) + ->setHeader($method->getFullDisplayName()); + + $details = $this->newDetailsView($method); + + $timeline = $this->buildTransactionTimeline( + $method, + new PhortunePaymentMethodTransactionQuery()); + $timeline->setShouldTerminate(true); + + $autopay = $this->newAutopayView($method); + + $curtain = $this->buildCurtainView($method); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->setMainColumn( + array( + $details, + $autopay, + $timeline, + )); + + return $this->newPage() + ->setTitle($method->getObjectName()) + ->setCrumbs($crumbs) + ->appendChild($view); + } + + private function buildCurtainView(PhortunePaymentMethod $method) { + $viewer = $this->getViewer(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $method, + PhabricatorPolicyCapability::CAN_EDIT); + + $edit_uri = $this->getApplicationURI( + urisprintf( + 'card/%d/edit/', + $method->getID())); + + $remove_uri = $this->getApplicationURI( + urisprintf( + 'card/%d/disable/', + $method->getID())); + + $curtain = $this->newCurtainView($method); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Payment Method')) + ->setIcon('fa-pencil') + ->setHref($edit_uri) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Remove Payment Method')) + ->setIcon('fa-times') + ->setHref($remove_uri) + ->setDisabled(!$can_edit) + ->setWorkflow(true)); + + return $curtain; + } + + private function newDetailsView(PhortunePaymentMethod $method) { + $viewer = $this->getViewer(); + + $merchant_phid = $method->getMerchantPHID(); + $handles = $viewer->loadHandles( + array( + $merchant_phid, + )); + + $view = id(new PHUIPropertyListView()) + ->setUser($viewer); + + if (strlen($method->getName())) { + $view->addProperty(pht('Name'), $method->getDisplayName()); + } + + $view->addProperty(pht('Summary'), $method->getSummary()); + $view->addProperty(pht('Expires'), $method->getDisplayExpires()); + + $view->addProperty( + pht('Merchant'), + $handles[$merchant_phid]->renderLink()); + + return id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Payment Method Details')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->addPropertyList($view); + } + + private function newAutopayView(PhortunePaymentMethod $method) { + $viewer = $this->getViewer(); + + $subscriptions = id(new PhortuneSubscriptionQuery()) + ->setViewer($viewer) + ->withPaymentMethodPHIDs(array($method->getPHID())) + ->execute(); + + $table = id(new PhortuneSubscriptionTableView()) + ->setViewer($viewer) + ->setSubscriptions($subscriptions) + ->newTableView(); + + $table->setNoDataString( + pht( + 'This payment method is not the default payment method for '. + 'any subscriptions.')); + + return id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Autopay Subscriptions')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setTable($table); + } + +} diff --git a/src/applications/phortune/controller/account/PhortuneAccountProfileController.php b/src/applications/phortune/controller/account/PhortuneAccountProfileController.php index 2a5448d5cc..212dfcd5a7 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountProfileController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountProfileController.php @@ -3,10 +3,6 @@ abstract class PhortuneAccountProfileController extends PhortuneAccountController { - public function buildApplicationMenu() { - return $this->buildSideNavView()->getMenu(); - } - protected function buildHeaderView() { $viewer = $this->getViewer(); $account = $this->getAccount(); @@ -17,13 +13,16 @@ abstract class PhortuneAccountProfileController ->setHeader($title) ->setHeaderIcon('fa-user-circle'); - return $header; - } + if ($this->getMerchants()) { + $customer_tag = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_SHADE) + ->setName(pht('Customer Account')) + ->setColor('indigo') + ->setIcon('fa-credit-card'); + $header->addTag($customer_tag); + } - protected function buildApplicationCrumbs() { - $crumbs = parent::buildApplicationCrumbs(); - $crumbs->setBorder(true); - return $crumbs; + return $header; } protected function buildSideNavView($filter = null) { @@ -31,6 +30,8 @@ abstract class PhortuneAccountProfileController $account = $this->getAccount(); $id = $account->getID(); + $can_edit = !$this->getMerchants(); + $nav = id(new AphrontSideNavFilterView()) ->setBaseURI(new PhutilURI($this->getApplicationURI())); @@ -39,30 +40,103 @@ abstract class PhortuneAccountProfileController $nav->addFilter( 'overview', pht('Overview'), - $this->getApplicationURI("/{$id}/"), + $account->getURI(), 'fa-user-circle'); + $nav->newLink('details') + ->setName(pht('Account Details')) + ->setHref($this->getApplicationURI("/account/{$id}/details/")) + ->setIcon('fa-address-card-o') + ->setWorkflow(!$can_edit) + ->setDisabled(!$can_edit); + + $nav->addLabel(pht('Payments')); + + $nav->addFilter( + 'methods', + pht('Payment Methods'), + $account->getPaymentMethodsURI(), + 'fa-credit-card'); + $nav->addFilter( 'subscriptions', pht('Subscriptions'), - $this->getApplicationURI("/account/subscription/{$id}/"), + $account->getSubscriptionsURI(), 'fa-retweet'); $nav->addFilter( - 'billing', - pht('Billing / History'), - $this->getApplicationURI("/account/billing/{$id}/"), - 'fa-credit-card'); + 'orders', + pht('Orders'), + $account->getOrdersURI(), + 'fa-shopping-bag'); + + $nav->addFilter( + 'charges', + pht('Charges'), + $account->getChargesURI(), + 'fa-calculator'); + + $nav->addLabel(pht('Personnel')); $nav->addFilter( 'managers', - pht('Managers'), - $this->getApplicationURI("/account/manager/{$id}/"), + pht('Account Managers'), + $this->getApplicationURI("/account/{$id}/managers/"), 'fa-group'); + $nav->newLink('addresses') + ->setname(pht('Email Addresses')) + ->setHref($account->getEmailAddressesURI()) + ->setIcon('fa-envelope-o') + ->setWorkflow(!$can_edit) + ->setDisabled(!$can_edit); + $nav->selectFilter($filter); return $nav; } + final protected function newRecentOrdersView( + PhortuneAccount $account, + $limit) { + + $viewer = $this->getViewer(); + + $carts = id(new PhortuneCartQuery()) + ->setViewer($viewer) + ->withAccountPHIDs(array($account->getPHID())) + ->needPurchases(true) + ->withStatuses( + array( + PhortuneCart::STATUS_PURCHASING, + PhortuneCart::STATUS_CHARGED, + PhortuneCart::STATUS_HOLD, + PhortuneCart::STATUS_REVIEW, + PhortuneCart::STATUS_PURCHASED, + )) + ->setLimit($limit) + ->execute(); + + $orders_uri = $account->getOrderListURI(); + + $table = id(new PhortuneOrderTableView()) + ->setUser($viewer) + ->setCarts($carts); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Recent Orders')) + ->addActionLink( + id(new PHUIButtonView()) + ->setTag('a') + ->setIcon('fa-list') + ->setHref($orders_uri) + ->setText(pht('View All Orders'))); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setTable($table); + } + + } diff --git a/src/applications/phortune/controller/account/PhortuneAccountSubscriptionAutopayController.php b/src/applications/phortune/controller/account/PhortuneAccountSubscriptionAutopayController.php new file mode 100644 index 0000000000..218c46bb0f --- /dev/null +++ b/src/applications/phortune/controller/account/PhortuneAccountSubscriptionAutopayController.php @@ -0,0 +1,137 @@ +getViewer(); + $account = $this->getAccount(); + + $subscription = id(new PhortuneSubscriptionQuery()) + ->setViewer($viewer) + ->withIDs(array($request->getURIData('subscriptionID'))) + ->withAccountPHIDs(array($account->getPHID())) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$subscription) { + return new Aphront404Response(); + } + + $method = id(new PhortunePaymentMethodQuery()) + ->setViewer($viewer) + ->withIDs(array($request->getURIData('methodID'))) + ->withAccountPHIDs(array($subscription->getAccountPHID())) + ->withMerchantPHIDs(array($subscription->getMerchantPHID())) + ->withStatuses( + array( + PhortunePaymentMethod::STATUS_ACTIVE, + )) + ->executeOne(); + if (!$method) { + return new Aphront404Response(); + } + + $next_uri = $subscription->getURI(); + + $autopay_phid = $subscription->getDefaultPaymentMethodPHID(); + $is_stop = ($autopay_phid === $method->getPHID()); + + if ($request->isFormOrHisecPost()) { + if ($is_stop) { + $new_phid = null; + } else { + $new_phid = $method->getPHID(); + } + + $xactions = array(); + + $xactions[] = $subscription->getApplicationTransactionTemplate() + ->setTransactionType( + PhortuneSubscriptionAutopayTransaction::TRANSACTIONTYPE) + ->setNewValue($new_phid); + + $editor = $subscription->getApplicationTransactionEditor() + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->setCancelURI($next_uri); + + $editor->applyTransactions($subscription, $xactions); + + return id(new AphrontRedirectResponse())->setURI($next_uri); + } + + $method_phid = $method->getPHID(); + $subscription_phid = $subscription->getPHID(); + + $handles = $viewer->loadHandles( + array( + $method_phid, + $subscription_phid, + )); + + $method_handle = $handles[$method_phid]; + $subscription_handle = $handles[$subscription_phid]; + + $method_display = $method_handle->renderLink(); + $method_display = phutil_tag( + 'strong', + array(), + $method_display); + + $subscription_display = $subscription_handle->renderLink(); + $subscription_display = phutil_tag( + 'strong', + array(), + $subscription_display); + + $body = array(); + if ($is_stop) { + $title = pht('Stop Autopay'); + + $body[] = pht( + 'Remove %s as the automatic payment method for subscription %s?', + $method_display, + $subscription_display); + + $body[] = pht( + 'This payment method will no longer be charged automatically.'); + + $submit = pht('Stop Autopay'); + } else { + $title = pht('Start Autopay'); + + $body[] = pht( + 'Set %s as the automatic payment method for subscription %s?', + $method_display, + $subscription_display); + + $body[] = pht( + 'This payment method will be used to automatically pay future '. + 'charges.'); + + $submit = pht('Start Autopay'); + } + + $dialog = $this->newDialog() + ->setTitle($title) + ->addCancelButton($next_uri) + ->addSubmitButton($submit); + + foreach ($body as $graph) { + $dialog->appendParagraph($graph); + } + + return $dialog; + } + +} diff --git a/src/applications/phortune/controller/account/PhortuneAccountSubscriptionController.php b/src/applications/phortune/controller/account/PhortuneAccountSubscriptionController.php index 418507e9c2..779721c4f3 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountSubscriptionController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountSubscriptionController.php @@ -3,26 +3,30 @@ final class PhortuneAccountSubscriptionController extends PhortuneAccountProfileController { - public function handleRequest(AphrontRequest $request) { - $response = $this->loadAccount(); - if ($response) { - return $response; - } + protected function shouldRequireAccountEditCapability() { + return false; + } + protected function handleAccountRequest(AphrontRequest $request) { $account = $this->getAccount(); $title = $account->getName(); - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('Subscriptions')); + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb(pht('Subscriptions')) + ->setBorder(true); $header = $this->buildHeaderView(); + $authority = $this->newAccountAuthorityView(); + $subscriptions = $this->buildSubscriptionsSection($account); $view = id(new PHUITwoColumnView()) ->setHeader($header) - ->setFooter(array( - $subscriptions, - )); + ->setFooter( + array( + $authority, + $subscriptions, + )); $navigation = $this->buildSideNavView('subscriptions'); @@ -43,11 +47,8 @@ final class PhortuneAccountSubscriptionController ->setLimit(25) ->execute(); - $handles = $this->loadViewerHandles(mpull($subscriptions, 'getPHID')); - $table = id(new PhortuneSubscriptionTableView()) ->setUser($viewer) - ->setHandles($handles) ->setSubscriptions($subscriptions); $header = id(new PHUIHeaderView()) diff --git a/src/applications/phortune/controller/account/PhortuneAccountSubscriptionViewController.php b/src/applications/phortune/controller/account/PhortuneAccountSubscriptionViewController.php new file mode 100644 index 0000000000..e38c18779b --- /dev/null +++ b/src/applications/phortune/controller/account/PhortuneAccountSubscriptionViewController.php @@ -0,0 +1,317 @@ +getViewer(); + + $subscription = id(new PhortuneSubscriptionQuery()) + ->setViewer($viewer) + ->withIDs(array($request->getURIData('subscriptionID'))) + ->needTriggers(true) + ->executeOne(); + if (!$subscription) { + return new Aphront404Response(); + } + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $subscription, + PhabricatorPolicyCapability::CAN_EDIT); + + $merchant = $subscription->getMerchant(); + $account = $subscription->getAccount(); + + $account_id = $account->getID(); + $subscription_id = $subscription->getID(); + + $title = $subscription->getSubscriptionFullName(); + + $header = id(new PHUIHeaderView()) + ->setHeader($title) + ->setHeaderIcon('fa-retweet'); + + $edit_uri = $subscription->getEditURI(); + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb($subscription->getSubscriptionCrumbName()) + ->setBorder(true); + + $properties = id(new PHUIPropertyListView()) + ->setUser($viewer); + + $next_invoice = $subscription->getTrigger()->getNextEventPrediction(); + $properties->addProperty( + pht('Next Invoice'), + phabricator_datetime($next_invoice, $viewer)); + + $autopay = $this->newAutopayView($subscription); + + $details = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Subscription Details')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->addPropertyList($properties); + + $due_box = $this->buildDueInvoices($subscription); + $invoice_box = $this->buildPastInvoices($subscription); + + $timeline = $this->buildTransactionTimeline( + $subscription, + new PhortuneSubscriptionTransactionQuery()); + $timeline->setShouldTerminate(true); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter( + array( + $details, + $autopay, + $due_box, + $invoice_box, + $timeline, + )); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->appendChild($view); + } + + private function buildDueInvoices(PhortuneSubscription $subscription) { + $viewer = $this->getViewer(); + + $invoices = id(new PhortuneCartQuery()) + ->setViewer($viewer) + ->withSubscriptionPHIDs(array($subscription->getPHID())) + ->needPurchases(true) + ->withInvoices(true) + ->execute(); + + $invoice_table = id(new PhortuneOrderTableView()) + ->setUser($viewer) + ->setCarts($invoices) + ->setIsInvoices(true); + + $invoice_header = id(new PHUIHeaderView()) + ->setHeader(pht('Invoices Due')); + + return id(new PHUIObjectBoxView()) + ->setHeader($invoice_header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->appendChild($invoice_table); + } + + private function buildPastInvoices(PhortuneSubscription $subscription) { + $viewer = $this->getViewer(); + + $invoices = id(new PhortuneCartQuery()) + ->setViewer($viewer) + ->withSubscriptionPHIDs(array($subscription->getPHID())) + ->needPurchases(true) + ->withStatuses( + array( + PhortuneCart::STATUS_PURCHASING, + PhortuneCart::STATUS_CHARGED, + PhortuneCart::STATUS_HOLD, + PhortuneCart::STATUS_REVIEW, + PhortuneCart::STATUS_PURCHASED, + )) + ->setLimit(50) + ->execute(); + + $invoice_table = id(new PhortuneOrderTableView()) + ->setUser($viewer) + ->setCarts($invoices); + + $account = $subscription->getAccount(); + $merchant = $subscription->getMerchant(); + + $account_id = $account->getID(); + $merchant_id = $merchant->getID(); + $subscription_id = $subscription->getID(); + + $invoices_uri = $this->getApplicationURI( + "{$account_id}/subscription/order/{$subscription_id}/"); + + $invoice_header = id(new PHUIHeaderView()) + ->setHeader(pht('Past Invoices')) + ->addActionLink( + id(new PHUIButtonView()) + ->setTag('a') + ->setIcon('fa-list') + ->setHref($invoices_uri) + ->setText(pht('View All Invoices'))); + + return id(new PHUIObjectBoxView()) + ->setHeader($invoice_header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->appendChild($invoice_table); + } + + private function newAutopayView(PhortuneSubscription $subscription) { + $viewer = $this->getViewer(); + $account = $subscription->getAccount(); + + $add_method_uri = urisprintf( + '/account/%d/methods/new/?subscriptionID=%s', + $account->getID(), + $subscription->getID()); + $add_method_uri = $this->getApplicationURI($add_method_uri); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $subscription, + PhabricatorPolicyCapability::CAN_EDIT); + + $methods = id(new PhortunePaymentMethodQuery()) + ->setViewer($viewer) + ->withAccountPHIDs(array($subscription->getAccountPHID())) + ->withMerchantPHIDs(array($subscription->getMerchantPHID())) + ->withStatuses( + array( + PhortunePaymentMethod::STATUS_ACTIVE, + )) + ->execute(); + $methods = mpull($methods, null, 'getPHID'); + + $autopay_phid = $subscription->getDefaultPaymentMethodPHID(); + $autopay_method = idx($methods, $autopay_phid); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Autopay')) + ->addActionLink( + id(new PHUIButtonView()) + ->setTag('a') + ->setIcon('fa-plus') + ->setHref($add_method_uri) + ->setText(pht('Add Payment Method')) + ->setWorkflow(!$can_edit) + ->setDisabled(!$can_edit)); + + $methods = array_select_keys($methods, array($autopay_phid)) + $methods; + + $rows = array(); + $rowc = array(); + foreach ($methods as $method) { + $is_autopay = ($autopay_method === $method); + + $remove_uri = urisprintf( + '/card/%d/disable/?subscriptionID=%d', + $method->getID(), + $subscription->getID()); + $remove_uri = $this->getApplicationURI($remove_uri); + + $autopay_uri = urisprintf( + '/account/%d/subscriptions/%d/autopay/%d/', + $account->getID(), + $subscription->getID(), + $method->getID()); + $autopay_uri = $this->getApplicationURI($autopay_uri); + + $remove_button = id(new PHUIButtonView()) + ->setTag('a') + ->setColor('grey') + ->setIcon('fa-times') + ->setText(pht('Delete')) + ->setHref($remove_uri) + ->setWorkflow(true) + ->setDisabled(!$can_edit); + + if ($is_autopay) { + $autopay_button = id(new PHUIButtonView()) + ->setColor('red') + ->setIcon('fa-times') + ->setText(pht('Stop Autopay')); + } else { + if ($autopay_method) { + $make_color = 'grey'; + } else { + $make_color = 'green'; + } + + $autopay_button = id(new PHUIButtonView()) + ->setColor($make_color) + ->setIcon('fa-retweet') + ->setText(pht('Start Autopay')); + } + + $autopay_button + ->setTag('a') + ->setHref($autopay_uri) + ->setWorkflow(true) + ->setDisabled(!$can_edit); + + $rows[] = array( + $method->getID(), + phutil_tag( + 'a', + array( + 'href' => $method->getURI(), + ), + $method->getFullDisplayName()), + $method->getDisplayExpires(), + $autopay_button, + $remove_button, + ); + + if ($is_autopay) { + $rowc[] = 'highlighted'; + } else { + $rowc[] = null; + } + } + + $method_table = id(new AphrontTableView($rows)) + ->setHeaders( + array( + pht('ID'), + pht('Payment Method'), + pht('Expires'), + null, + null, + )) + ->setRowClasses($rowc) + ->setColumnClasses( + array( + null, + 'pri wide', + null, + 'right', + null, + )); + + if (!$autopay_method) { + $method_table->setNotice( + array( + id(new PHUIIconView())->setIcon('fa-warning yellow'), + ' ', + pht('Autopay is not currently configured for this subscription.'), + )); + } else { + $method_table->setNotice( + array( + id(new PHUIIconView())->setIcon('fa-check green'), + ' ', + pht( + 'Autopay is configured using %s.', + phutil_tag( + 'a', + array( + 'href' => $autopay_method->getURI(), + ), + $autopay_method->getFullDisplayName())), + )); + } + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setTable($method_table); + } + +} diff --git a/src/applications/phortune/controller/account/PhortuneAccountViewController.php b/src/applications/phortune/controller/account/PhortuneAccountViewController.php deleted file mode 100644 index 6537577921..0000000000 --- a/src/applications/phortune/controller/account/PhortuneAccountViewController.php +++ /dev/null @@ -1,238 +0,0 @@ -loadAccount(); - if ($response) { - return $response; - } - - $account = $this->getAccount(); - $title = $account->getName(); - - $viewer = $this->getViewer(); - - $invoices = id(new PhortuneCartQuery()) - ->setViewer($viewer) - ->withAccountPHIDs(array($account->getPHID())) - ->needPurchases(true) - ->withInvoices(true) - ->execute(); - - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->setBorder(true); - - $header = $this->buildHeaderView(); - - $curtain = $this->buildCurtainView($account); - $status = $this->buildStatusView($account, $invoices); - $invoices = $this->buildInvoicesSection($account, $invoices); - $purchase_history = $this->buildPurchaseHistorySection($account); - - $timeline = $this->buildTransactionTimeline( - $account, - new PhortuneAccountTransactionQuery()); - $timeline->setShouldTerminate(true); - - $view = id(new PHUITwoColumnView()) - ->setHeader($header) - ->setCurtain($curtain) - ->setMainColumn(array( - $status, - $invoices, - $purchase_history, - $timeline, - )); - - $navigation = $this->buildSideNavView('overview'); - - return $this->newPage() - ->setTitle($title) - ->setCrumbs($crumbs) - ->setNavigation($navigation) - ->appendChild($view); - - } - - private function buildStatusView(PhortuneAccount $account, $invoices) { - $status_items = $this->getStatusItemsForAccount($account, $invoices); - $view = array(); - foreach ($status_items as $item) { - $view[] = id(new PHUIInfoView()) - ->setSeverity(idx($item, 'severity')) - ->appendChild(idx($item, 'note')); - } - return $view; - } - - private function buildCurtainView(PhortuneAccount $account) { - $viewer = $this->getViewer(); - - $can_edit = PhabricatorPolicyFilter::hasCapability( - $viewer, - $account, - PhabricatorPolicyCapability::CAN_EDIT); - - $edit_uri = $this->getApplicationURI('account/edit/'.$account->getID().'/'); - - $curtain = $this->newCurtainView($account); - $curtain->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Edit Account')) - ->setIcon('fa-pencil') - ->setHref($edit_uri) - ->setDisabled(!$can_edit) - ->setWorkflow(!$can_edit)); - - $member_phids = $account->getMemberPHIDs(); - $handles = $viewer->loadHandles($member_phids); - - $member_list = id(new PHUIObjectItemListView()) - ->setSimple(true); - - foreach ($member_phids as $member_phid) { - $image_uri = $handles[$member_phid]->getImageURI(); - $image_href = $handles[$member_phid]->getURI(); - $person = $handles[$member_phid]; - - $member = id(new PHUIObjectItemView()) - ->setImageURI($image_uri) - ->setHref($image_href) - ->setHeader($person->getFullName()); - - $member_list->addItem($member); - } - - $curtain->newPanel() - ->setHeaderText(pht('Managers')) - ->appendChild($member_list); - - return $curtain; - } - - private function buildInvoicesSection( - PhortuneAccount $account, - array $carts) { - - $viewer = $this->getViewer(); - - $phids = array(); - foreach ($carts as $cart) { - $phids[] = $cart->getPHID(); - $phids[] = $cart->getMerchantPHID(); - foreach ($cart->getPurchases() as $purchase) { - $phids[] = $purchase->getPHID(); - } - } - $handles = $this->loadViewerHandles($phids); - - $table = id(new PhortuneOrderTableView()) - ->setNoDataString(pht('You have no unpaid invoices.')) - ->setIsInvoices(true) - ->setUser($viewer) - ->setCarts($carts) - ->setHandles($handles); - - $header = id(new PHUIHeaderView()) - ->setHeader(pht('Invoices Due')); - - return id(new PHUIObjectBoxView()) - ->setHeader($header) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->setTable($table); - } - - private function buildPurchaseHistorySection(PhortuneAccount $account) { - $viewer = $this->getViewer(); - - $carts = id(new PhortuneCartQuery()) - ->setViewer($viewer) - ->withAccountPHIDs(array($account->getPHID())) - ->needPurchases(true) - ->withStatuses( - array( - PhortuneCart::STATUS_PURCHASING, - PhortuneCart::STATUS_CHARGED, - PhortuneCart::STATUS_HOLD, - PhortuneCart::STATUS_REVIEW, - PhortuneCart::STATUS_PURCHASED, - )) - ->setLimit(10) - ->execute(); - - $phids = array(); - foreach ($carts as $cart) { - $phids[] = $cart->getPHID(); - foreach ($cart->getPurchases() as $purchase) { - $phids[] = $purchase->getPHID(); - } - } - $handles = $this->loadViewerHandles($phids); - - $orders_uri = $this->getApplicationURI($account->getID().'/order/'); - - $table = id(new PhortuneOrderTableView()) - ->setUser($viewer) - ->setCarts($carts) - ->setHandles($handles); - - $header = id(new PHUIHeaderView()) - ->setHeader(pht('Recent Orders')) - ->addActionLink( - id(new PHUIButtonView()) - ->setTag('a') - ->setIcon('fa-list') - ->setHref($orders_uri) - ->setText(pht('View All Orders'))); - - return id(new PHUIObjectBoxView()) - ->setHeader($header) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->setTable($table); - } - - protected function buildApplicationCrumbs() { - $crumbs = parent::buildApplicationCrumbs(); - - $crumbs->addAction( - id(new PHUIListItemView()) - ->setIcon('fa-exchange') - ->setHref($this->getApplicationURI('account/')) - ->setName(pht('Switch Accounts'))); - - return $crumbs; - } - - private function getStatusItemsForAccount( - PhortuneAccount $account, - array $invoices) { - $viewer = $this->getViewer(); - - assert_instances_of($invoices, 'PhortuneCart'); - $items = array(); - - $methods = id(new PhortunePaymentMethodQuery()) - ->setViewer($viewer) - ->withAccountPHIDs(array($account->getPHID())) - ->withStatuses( - array( - PhortunePaymentMethod::STATUS_ACTIVE, - )) - ->execute(); - - if ($invoices) { - $items[] = array( - 'severity' => PHUIInfoView::SEVERITY_ERROR, - 'note' => pht('You have %d unpaid invoice(s).', count($invoices)), - ); - } - - // TODO: If a payment method has expired or is expiring soon, we should - // add a status check for it. - - return $items; - } - -} diff --git a/src/applications/phortune/controller/cart/PhortuneCartAcceptController.php b/src/applications/phortune/controller/cart/PhortuneCartAcceptController.php index cb53e66f50..79969b999d 100644 --- a/src/applications/phortune/controller/cart/PhortuneCartAcceptController.php +++ b/src/applications/phortune/controller/cart/PhortuneCartAcceptController.php @@ -3,27 +3,19 @@ final class PhortuneCartAcceptController extends PhortuneCartController { - public function handleRequest(AphrontRequest $request) { + protected function shouldRequireAccountAuthority() { + return false; + } + + protected function shouldRequireMerchantAuthority() { + return true; + } + + protected function handleCartRequest(AphrontRequest $request) { $viewer = $request->getViewer(); - $id = $request->getURIData('id'); + $cart = $this->getCart(); - // You must control the merchant to accept orders. - $authority = $this->loadMerchantAuthority(); - if (!$authority) { - return new Aphront404Response(); - } - - $cart = id(new PhortuneCartQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->withMerchantPHIDs(array($authority->getPHID())) - ->needPurchases(true) - ->executeOne(); - if (!$cart) { - return new Aphront404Response(); - } - - $cancel_uri = $cart->getDetailURI($authority); + $cancel_uri = $cart->getDetailURI(); if ($cart->getStatus() !== PhortuneCart::STATUS_REVIEW) { return $this->newDialog() diff --git a/src/applications/phortune/controller/cart/PhortuneCartCancelController.php b/src/applications/phortune/controller/cart/PhortuneCartCancelController.php index c4a26c0d00..bad476ea1c 100644 --- a/src/applications/phortune/controller/cart/PhortuneCartCancelController.php +++ b/src/applications/phortune/controller/cart/PhortuneCartCancelController.php @@ -3,26 +3,21 @@ final class PhortuneCartCancelController extends PhortuneCartController { - public function handleRequest(AphrontRequest $request) { + protected function shouldRequireAccountAuthority() { + return false; + } + + protected function shouldRequireMerchantAuthority() { + return false; + } + + protected function handleCartRequest(AphrontRequest $request) { $viewer = $request->getViewer(); $id = $request->getURIData('id'); $action = $request->getURIData('action'); - $authority = $this->loadMerchantAuthority(); - - $cart_query = id(new PhortuneCartQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->needPurchases(true); - - if ($authority) { - $cart_query->withMerchantPHIDs(array($authority->getPHID())); - } - - $cart = $cart_query->executeOne(); - if (!$cart) { - return new Aphront404Response(); - } + $cart = $this->getCart(); + $authority = $this->getMerchantAuthority(); switch ($action) { case 'cancel': @@ -45,7 +40,7 @@ final class PhortuneCartCancelController return new Aphront404Response(); } - $cancel_uri = $cart->getDetailURI($authority); + $cancel_uri = $cart->getDetailURI(); $merchant = $cart->getMerchant(); try { @@ -60,7 +55,7 @@ final class PhortuneCartCancelController return $this->newDialog() ->setTitle($title) ->appendChild($ex->getMessage()) - ->addCancelButton($cancel_uri); + ->addCancelButton($cancel_uri, pht('Rats')); } $charges = id(new PhortuneChargeQuery()) diff --git a/src/applications/phortune/controller/cart/PhortuneCartCheckoutController.php b/src/applications/phortune/controller/cart/PhortuneCartCheckoutController.php index 874ecf63aa..cbd434c900 100644 --- a/src/applications/phortune/controller/cart/PhortuneCartCheckoutController.php +++ b/src/applications/phortune/controller/cart/PhortuneCartCheckoutController.php @@ -3,18 +3,17 @@ final class PhortuneCartCheckoutController extends PhortuneCartController { - public function handleRequest(AphrontRequest $request) { - $viewer = $request->getViewer(); - $id = $request->getURIData('id'); + protected function shouldRequireAccountAuthority() { + return true; + } - $cart = id(new PhortuneCartQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->needPurchases(true) - ->executeOne(); - if (!$cart) { - return new Aphront404Response(); - } + protected function shouldRequireMerchantAuthority() { + return false; + } + + protected function handleCartRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $cart = $this->getCart(); $cancel_uri = $cart->getCancelURI(); $merchant = $cart->getMerchant(); @@ -102,13 +101,9 @@ final class PhortuneCartCheckoutController } } - $cart_table = $this->buildCartContentTable($cart); - - $cart_box = id(new PHUIObjectBoxView()) - ->setFormErrors($errors) - ->setHeaderText(pht('Cart Contents')) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->setTable($cart_table); + $cart_box = id(new PhortuneOrderItemsView()) + ->setViewer($viewer) + ->setOrder($cart); $title = $cart->getName(); @@ -139,7 +134,10 @@ final class PhortuneCartCheckoutController 'cartID' => $cart->getID(), ); - $payment_method_uri = $this->getApplicationURI("{$account_id}/card/new/"); + $payment_method_uri = urisprintf( + 'account/%d/methods/new/', + $account->getID()); + $payment_method_uri = $this->getApplicationURI($payment_method_uri); $payment_method_uri = new PhutilURI($payment_method_uri, $params); $form = id(new AphrontFormView()) @@ -205,7 +203,9 @@ final class PhortuneCartCheckoutController ->appendChild($form) ->appendChild($provider_form); - $description_box = $this->renderCartDescription($cart); + $description_view = id(new PhortuneOrderDescriptionView()) + ->setViewer($viewer) + ->setOrder($cart); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Checkout')); @@ -218,11 +218,12 @@ final class PhortuneCartCheckoutController $view = id(new PHUITwoColumnView()) ->setHeader($header) - ->setFooter(array( - $cart_box, - $description_box, - $payment_box, - )); + ->setFooter( + array( + $description_view, + $cart_box, + $payment_box, + )); return $this->newPage() ->setTitle($title) diff --git a/src/applications/phortune/controller/cart/PhortuneCartController.php b/src/applications/phortune/controller/cart/PhortuneCartController.php index b8f926d3b2..c28581bc47 100644 --- a/src/applications/phortune/controller/cart/PhortuneCartController.php +++ b/src/applications/phortune/controller/cart/PhortuneCartController.php @@ -3,61 +3,75 @@ abstract class PhortuneCartController extends PhortuneController { - protected function buildCartContentTable(PhortuneCart $cart) { + private $cart; + private $merchantAuthority; - $rows = array(); - foreach ($cart->getPurchases() as $purchase) { - $rows[] = array( - $purchase->getFullDisplayName(), - $purchase->getBasePriceAsCurrency()->formatForDisplay(), - $purchase->getQuantity(), - $purchase->getTotalPriceAsCurrency()->formatForDisplay(), + abstract protected function shouldRequireAccountAuthority(); + abstract protected function shouldRequireMerchantAuthority(); + abstract protected function handleCartRequest(AphrontRequest $request); + + final public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); + + if ($this->shouldRequireAccountAuthority()) { + $capabilities = array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } else { + $capabilities = array( + PhabricatorPolicyCapability::CAN_VIEW, ); } - $rows[] = array( - phutil_tag('strong', array(), pht('Total')), - '', - '', - phutil_tag('strong', array(), - $cart->getTotalPriceAsCurrency()->formatForDisplay()), - ); - - $table = new AphrontTableView($rows); - $table->setHeaders( - array( - pht('Item'), - pht('Price'), - pht('Qty.'), - pht('Total'), - )); - $table->setColumnClasses( - array( - 'wide', - 'right', - 'right', - 'right', - )); - - return $table; - } - - protected function renderCartDescription(PhortuneCart $cart) { - $description = $cart->getDescription(); - if (!strlen($description)) { - return null; + $cart = id(new PhortuneCartQuery()) + ->setViewer($viewer) + ->withIDs(array($request->getURIData('id'))) + ->needPurchases(true) + ->requireCapabilities($capabilities) + ->executeOne(); + if (!$cart) { + return new Aphront404Response(); } - $output = new PHUIRemarkupView($this->getViewer(), $description); + if ($this->shouldRequireMerchantAuthority()) { + PhabricatorPolicyFilter::requireCapability( + $viewer, + $cart->getMerchant(), + PhabricatorPolicyCapability::CAN_EDIT); + } - $box = id(new PHUIBoxView()) - ->addMargin(PHUI::MARGIN_LARGE) - ->appendChild($output); + $this->cart = $cart; - return id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Description')) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->appendChild($box); + $can_edit = PhortuneMerchantQuery::canViewersEditMerchants( + array($viewer->getPHID()), + array($cart->getMerchantPHID())); + if ($can_edit) { + $this->merchantAuthority = $cart->getMerchant(); + } else { + $this->merchantAuthority = null; + } + + return $this->handleCartRequest($request); + } + + final protected function getCart() { + return $this->cart; + } + + final protected function getMerchantAuthority() { + return $this->merchantAuthority; + } + + final protected function hasMerchantAuthority() { + return (bool)$this->merchantAuthority; + } + + final protected function hasAccountAuthority() { + return (bool)PhabricatorPolicyFilter::hasCapability( + $this->getViewer(), + $this->getCart(), + PhabricatorPolicyCapability::CAN_EDIT); } } diff --git a/src/applications/phortune/controller/cart/PhortuneCartListController.php b/src/applications/phortune/controller/cart/PhortuneCartListController.php deleted file mode 100644 index 0537417f7c..0000000000 --- a/src/applications/phortune/controller/cart/PhortuneCartListController.php +++ /dev/null @@ -1,134 +0,0 @@ -getViewer(); - - $merchant_id = $request->getURIData('merchantID'); - $account_id = $request->getURIData('accountID'); - $subscription_id = $request->getURIData('subscriptionID'); - - $engine = id(new PhortuneCartSearchEngine()) - ->setViewer($viewer); - - if ($merchant_id) { - $merchant = id(new PhortuneMerchantQuery()) - ->setViewer($viewer) - ->withIDs(array($merchant_id)) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$merchant) { - return new Aphront404Response(); - } - $this->merchant = $merchant; - $viewer->grantAuthority($merchant); - $engine->setMerchant($merchant); - } else if ($account_id) { - $account = id(new PhortuneAccountQuery()) - ->setViewer($viewer) - ->withIDs(array($account_id)) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$account) { - return new Aphront404Response(); - } - $this->account = $account; - $engine->setAccount($account); - } else { - return new Aphront404Response(); - } - - // NOTE: We must process this after processing the merchant authority, so - // it becomes visible in merchant contexts. - if ($subscription_id) { - $subscription = id(new PhortuneSubscriptionQuery()) - ->setViewer($viewer) - ->withIDs(array($subscription_id)) - ->executeOne(); - if (!$subscription) { - return new Aphront404Response(); - } - $this->subscription = $subscription; - $engine->setSubscription($subscription); - } - - $this->engine = $engine; - - $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($request->getURIData('queryKey')) - ->setSearchEngine($engine) - ->setNavigation($this->buildSideNavView()); - - return $this->delegateToController($controller); - } - - public function buildSideNavView() { - $viewer = $this->getRequest()->getUser(); - - $nav = new AphrontSideNavFilterView(); - $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); - - $this->engine->addNavigationItems($nav->getMenu()); - - $nav->selectFilter(null); - - return $nav; - } - - protected function buildApplicationCrumbs() { - $crumbs = parent::buildApplicationCrumbs(); - - $subscription = $this->subscription; - - $merchant = $this->merchant; - if ($merchant) { - $id = $merchant->getID(); - $this->addMerchantCrumb($crumbs, $merchant); - if (!$subscription) { - $crumbs->addTextCrumb( - pht('Orders'), - $this->getApplicationURI("merchant/orders/{$id}/")); - } - } - - $account = $this->account; - if ($account) { - $id = $account->getID(); - $this->addAccountCrumb($crumbs, $account); - if (!$subscription) { - $crumbs->addTextCrumb( - pht('Orders'), - $this->getApplicationURI("{$id}/order/")); - } - } - - if ($subscription) { - if ($merchant) { - $subscription_uri = $subscription->getMerchantURI(); - } else { - $subscription_uri = $subscription->getURI(); - } - $crumbs->addTextCrumb( - $subscription->getSubscriptionName(), - $subscription_uri); - } - - return $crumbs; - } - -} diff --git a/src/applications/phortune/controller/cart/PhortuneCartUpdateController.php b/src/applications/phortune/controller/cart/PhortuneCartUpdateController.php index 3d49611d2d..64b2ab3cfa 100644 --- a/src/applications/phortune/controller/cart/PhortuneCartUpdateController.php +++ b/src/applications/phortune/controller/cart/PhortuneCartUpdateController.php @@ -3,25 +3,20 @@ final class PhortuneCartUpdateController extends PhortuneCartController { - public function handleRequest(AphrontRequest $request) { + protected function shouldRequireAccountAuthority() { + return false; + } + + protected function shouldRequireMerchantAuthority() { + return false; + } + + protected function handleCartRequest(AphrontRequest $request) { $viewer = $request->getViewer(); $id = $request->getURIData('id'); - $authority = $this->loadMerchantAuthority(); - - $cart_query = id(new PhortuneCartQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->needPurchases(true); - - if ($authority) { - $cart_query->withMerchantPHIDs(array($authority->getPHID())); - } - - $cart = $cart_query->executeOne(); - if (!$cart) { - return new Aphront404Response(); - } + $cart = $this->getCart(); + $authority = $this->getMerchantAuthority(); $charges = id(new PhortuneChargeQuery()) ->setViewer($viewer) @@ -60,7 +55,7 @@ final class PhortuneCartUpdateController } return id(new AphrontRedirectResponse()) - ->setURI($cart->getDetailURI($authority)); + ->setURI($cart->getDetailURI()); } } diff --git a/src/applications/phortune/controller/cart/PhortuneCartViewController.php b/src/applications/phortune/controller/cart/PhortuneCartViewController.php index 8108c83b39..9412d11045 100644 --- a/src/applications/phortune/controller/cart/PhortuneCartViewController.php +++ b/src/applications/phortune/controller/cart/PhortuneCartViewController.php @@ -3,312 +3,111 @@ final class PhortuneCartViewController extends PhortuneCartController { - private $action = null; + protected function shouldRequireAccountAuthority() { + return false; + } - public function handleRequest(AphrontRequest $request) { + protected function shouldRequireMerchantAuthority() { + return false; + } + + protected function handleCartRequest(AphrontRequest $request) { $viewer = $request->getViewer(); - $id = $request->getURIData('id'); - $this->action = $request->getURIData('action'); + $order = $this->getCart(); + $authority = $this->getMerchantAuthority(); + $can_edit = $this->hasAccountAuthority(); - $authority = $this->loadMerchantAuthority(); - require_celerity_resource('phortune-css'); + $is_printable = ($request->getURIData('action') === 'print'); - $query = id(new PhortuneCartQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->needPurchases(true); - - if ($authority) { - $query->withMerchantPHIDs(array($authority->getPHID())); - } - - $cart = $query->executeOne(); - if (!$cart) { - return new Aphront404Response(); - } - - $cart_table = $this->buildCartContentTable($cart); - - $can_edit = PhabricatorPolicyFilter::hasCapability( - $viewer, - $cart, - PhabricatorPolicyCapability::CAN_EDIT); - - $errors = array(); - $error_view = null; $resume_uri = null; - switch ($cart->getStatus()) { - case PhortuneCart::STATUS_READY: - if ($authority && $cart->getIsInvoice()) { - // We arrived here by following the ad-hoc invoice workflow, and - // are acting with merchant authority. - - $checkout_uri = PhabricatorEnv::getURI($cart->getCheckoutURI()); - - $invoice_message = array( - pht( - 'Manual invoices do not automatically notify recipients yet. '. - 'Send the payer this checkout link:'), - ' ', - phutil_tag( - 'a', - array( - 'href' => $checkout_uri, - ), - $checkout_uri), - ); - - $error_view = id(new PHUIInfoView()) - ->setSeverity(PHUIInfoView::SEVERITY_WARNING) - ->setErrors(array($invoice_message)); - } - break; - case PhortuneCart::STATUS_PURCHASING: - if ($can_edit) { - $resume_uri = $cart->getMetadataValue('provider.checkoutURI'); - if ($resume_uri) { - $errors[] = pht( - 'The checkout process has been started, but not yet completed. '. - 'You can continue checking out by clicking %s, or cancel the '. - 'order, or contact the merchant for assistance.', - phutil_tag('strong', array(), pht('Continue Checkout'))); - } else { - $errors[] = pht( - 'The checkout process has been started, but an error occurred. '. - 'You can cancel the order or contact the merchant for '. - 'assistance.'); - } - } - break; - case PhortuneCart::STATUS_CHARGED: - if ($can_edit) { - $errors[] = pht( - 'You have been charged, but processing could not be completed. '. - 'You can cancel your order, or contact the merchant for '. - 'assistance.'); - } - break; - case PhortuneCart::STATUS_HOLD: - if ($can_edit) { - $errors[] = pht( - 'Payment for this order is on hold. You can click %s to check '. - 'for updates, cancel the order, or contact the merchant for '. - 'assistance.', - phutil_tag('strong', array(), pht('Update Status'))); - } - break; - case PhortuneCart::STATUS_REVIEW: - if ($authority) { - $errors[] = pht( - 'This order has been flagged for manual review. Review the order '. - 'and choose %s to accept it or %s to reject it.', - phutil_tag('strong', array(), pht('Accept Order')), - phutil_tag('strong', array(), pht('Refund Order'))); - } else if ($can_edit) { - $errors[] = pht( - 'This order requires manual processing and will complete once '. - 'the merchant accepts it.'); - } - break; - case PhortuneCart::STATUS_PURCHASED: - $error_view = id(new PHUIInfoView()) - ->setSeverity(PHUIInfoView::SEVERITY_SUCCESS) - ->appendChild(pht('This purchase has been completed.')); - break; + if ($order->getStatus() === PhortuneCart::STATUS_PURCHASING) { + if ($can_edit) { + $resume_uri = $order->getMetadataValue('provider.checkoutURI'); + } } - if ($errors) { - $error_view = id(new PHUIInfoView()) - ->setSeverity(PHUIInfoView::SEVERITY_WARNING) - ->appendChild($errors); - } - - $details = $this->buildDetailsView($cart); - $curtain = $this->buildCurtainView( - $cart, - $can_edit, - $authority, - $resume_uri); - $header = id(new PHUIHeaderView()) ->setUser($viewer) - ->setHeader($cart->getName()) - ->setHeaderIcon('fa-shopping-cart'); + ->setHeader($order->getName()) + ->setHeaderIcon('fa-shopping-bag'); - if ($cart->getStatus() == PhortuneCart::STATUS_PURCHASED) { - $done_uri = $cart->getDoneURI(); + if ($order->getStatus() == PhortuneCart::STATUS_PURCHASED) { + $done_uri = $order->getDoneURI(); if ($done_uri) { $header->addActionLink( id(new PHUIButtonView()) ->setTag('a') ->setHref($done_uri) ->setIcon('fa-check-square green') - ->setText($cart->getDoneActionName())); + ->setText($order->getDoneActionName())); } } - $cart_box = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Cart Items')) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->setTable($cart_table); - - $description = $this->renderCartDescription($cart); - - $charges = id(new PhortuneChargeQuery()) + $order_view = id(new PhortuneOrderSummaryView()) ->setViewer($viewer) - ->withCartPHIDs(array($cart->getPHID())) - ->needCarts(true) - ->execute(); + ->setOrder($order) + ->setResumeURI($resume_uri) + ->setPrintable($is_printable); - $phids = array(); - foreach ($charges as $charge) { - $phids[] = $charge->getProviderPHID(); - $phids[] = $charge->getCartPHID(); - $phids[] = $charge->getMerchantPHID(); - $phids[] = $charge->getPaymentMethodPHID(); - } - $handles = $this->loadViewerHandles($phids); + $crumbs = null; + $curtain = null; - $charges_table = id(new PhortuneChargeTableView()) - ->setUser($viewer) - ->setHandles($handles) - ->setCharges($charges) - ->setShowOrder(false); + $main = array(); + $tail = array(); - $charges = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Charges')) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->setTable($charges_table); + require_celerity_resource('phortune-invoice-css'); - $account = $cart->getAccount(); + if ($is_printable) { + $body_class = 'phortune-invoice-view'; - $crumbs = $this->buildApplicationCrumbs(); - if ($authority) { - $this->addMerchantCrumb($crumbs, $authority); + $tail[] = $order_view; } else { - $this->addAccountCrumb($crumbs, $cart->getAccount()); + $body_class = 'phortune-cart-page'; + + $curtain = $this->buildCurtainView( + $order, + $can_edit, + $authority, + $resume_uri); + + $account = $order->getAccount(); + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb($account->getName(), $account->getURI()) + ->addTextCrumb(pht('Orders'), $account->getOrdersURI()) + ->addTextCrumb($order->getObjectName()) + ->setBorder(true); + + $timeline = $this->buildTransactionTimeline($order) + ->setShouldTerminate(true); + + $main[] = $order_view; + $main[] = $timeline; } - $crumbs->addTextCrumb(pht('Cart %d', $cart->getID())); - $crumbs->setBorder(true); - if (!$this->action) { - $class = 'phortune-cart-page'; - $timeline = $this->buildTransactionTimeline( - $cart, - new PhortuneCartTransactionQuery()); - $timeline - ->setShouldTerminate(true); + $column_view = id(new PHUITwoColumnView()) + ->setMainColumn($main) + ->setFooter($tail); - $view = id(new PHUITwoColumnView()) - ->setHeader($header) - ->setCurtain($curtain) - ->setMainColumn(array( - $error_view, - $details, - $cart_box, - $description, - $charges, - $timeline, - )); - - } else { - $class = 'phortune-invoice-view'; - $crumbs = null; - $merchant_phid = $cart->getMerchantPHID(); - $buyer_phid = $cart->getAuthorPHID(); - $merchant = id(new PhortuneMerchantQuery()) - ->setViewer($viewer) - ->withPHIDs(array($merchant_phid)) - ->needProfileImage(true) - ->executeOne(); - $buyer = id(new PhabricatorPeopleQuery()) - ->setViewer($viewer) - ->withPHIDs(array($buyer_phid)) - ->needProfileImage(true) - ->executeOne(); - - $merchant_contact = new PHUIRemarkupView( - $viewer, - $merchant->getContactInfo()); - - $account_name = $account->getBillingName(); - if (!strlen($account_name)) { - $account_name = $buyer->getRealName(); - } - - $account_contact = $account->getBillingAddress(); - if (strlen($account_contact)) { - $account_contact = new PHUIRemarkupView( - $viewer, - $account_contact); - } - - $view = id(new PhortuneInvoiceView()) - ->setMerchantName($merchant->getName()) - ->setMerchantLogo($merchant->getProfileImageURI()) - ->setMerchantContact($merchant_contact) - ->setMerchantFooter($merchant->getInvoiceFooter()) - ->setAccountName($account_name) - ->setAccountContact($account_contact) - ->setStatus($error_view) - ->setContent( - array( - $details, - $cart_box, - $charges, - )); + if ($curtain) { + $column_view->setCurtain($curtain); } $page = $this->newPage() - ->setTitle(pht('Cart %d', $cart->getID())) - ->addClass($class) - ->appendChild($view); + ->addClass($body_class) + ->setTitle( + array( + $order->getObjectName(), + $order->getName(), + )) + ->appendChild($column_view); if ($crumbs) { $page->setCrumbs($crumbs); } + return $page; } - private function buildDetailsView(PhortuneCart $cart) { - $viewer = $this->getViewer(); - $view = id(new PHUIPropertyListView()) - ->setUser($viewer) - ->setObject($cart); - - $handles = $this->loadViewerHandles( - array( - $cart->getAccountPHID(), - $cart->getAuthorPHID(), - $cart->getMerchantPHID(), - )); - - if ($this->action == 'print') { - $view->addProperty(pht('Order Name'), $cart->getName()); - } - - $view->addProperty( - pht('Account'), - $handles[$cart->getAccountPHID()]->renderLink()); - $view->addProperty( - pht('Authorized By'), - $handles[$cart->getAuthorPHID()]->renderLink()); - $view->addProperty( - pht('Merchant'), - $handles[$cart->getMerchantPHID()]->renderLink()); - $view->addProperty( - pht('Status'), - PhortuneCart::getNameForStatus($cart->getStatus())); - $view->addProperty( - pht('Updated'), - phabricator_datetime($cart->getDateModified(), $viewer)); - - return id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Details')) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->appendChild($view); - } - private function buildCurtainView( PhortuneCart $cart, $can_edit, @@ -318,20 +117,43 @@ final class PhortuneCartViewController $viewer = $this->getViewer(); $id = $cart->getID(); $curtain = $this->newCurtainView($cart); + $status = $cart->getStatus(); + + $is_ready = ($status === PhortuneCart::STATUS_READY); $can_cancel = ($can_edit && $cart->canCancelOrder()); + $can_checkout = ($can_edit && $is_ready); + $can_accept = ($status === PhortuneCart::STATUS_REVIEW); + $can_refund = ($authority && $cart->canRefundOrder()); + $can_void = ($authority && $cart->canVoidOrder()); - if ($authority) { - $prefix = 'merchant/'.$authority->getID().'/'; - } else { - $prefix = ''; - } + $cancel_uri = $this->getApplicationURI("cart/{$id}/cancel/"); + $refund_uri = $this->getApplicationURI("cart/{$id}/refund/"); + $update_uri = $this->getApplicationURI("cart/{$id}/update/"); + $accept_uri = $this->getApplicationURI("cart/{$id}/accept/"); + $print_uri = $this->getApplicationURI("cart/{$id}/print/"); + $checkout_uri = $cart->getCheckoutURI(); + $void_uri = $this->getApplicationURI("cart/{$id}/void/"); - $cancel_uri = $this->getApplicationURI("{$prefix}cart/{$id}/cancel/"); - $refund_uri = $this->getApplicationURI("{$prefix}cart/{$id}/refund/"); - $update_uri = $this->getApplicationURI("{$prefix}cart/{$id}/update/"); - $accept_uri = $this->getApplicationURI("{$prefix}cart/{$id}/accept/"); - $print_uri = $this->getApplicationURI("{$prefix}cart/{$id}/print/"); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Printable Version')) + ->setHref($print_uri) + ->setOpenInNewWindow(true) + ->setIcon('fa-print')); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setType(PhabricatorActionView::TYPE_DIVIDER)); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Pay Now')) + ->setIcon('fa-credit-card') + ->setDisabled(!$can_checkout) + ->setWorkflow(!$can_checkout) + ->setHref($checkout_uri)); $curtain->addAction( id(new PhabricatorActionView()) @@ -341,24 +163,6 @@ final class PhortuneCartViewController ->setWorkflow(true) ->setHref($cancel_uri)); - if ($authority) { - if ($cart->getStatus() == PhortuneCart::STATUS_REVIEW) { - $curtain->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Accept Order')) - ->setIcon('fa-check') - ->setWorkflow(true) - ->setHref($accept_uri)); - } - - $curtain->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Refund Order')) - ->setIcon('fa-reply') - ->setWorkflow(true) - ->setHref($refund_uri)); - } - $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Update Status')) @@ -369,16 +173,39 @@ final class PhortuneCartViewController $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Continue Checkout')) - ->setIcon('fa-shopping-cart') + ->setIcon('fa-shopping-bag') ->setHref($resume_uri)); } - $curtain->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Printable Version')) - ->setHref($print_uri) - ->setOpenInNewWindow(true) - ->setIcon('fa-print')); + if ($authority) { + $curtain->addAction( + id(new PhabricatorActionView()) + ->setType(PhabricatorActionView::TYPE_DIVIDER)); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Accept Order')) + ->setIcon('fa-check') + ->setWorkflow(true) + ->setDisabled(!$can_accept) + ->setHref($accept_uri)); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Refund Order')) + ->setIcon('fa-reply') + ->setWorkflow(true) + ->setDisabled(!$can_refund) + ->setHref($refund_uri)); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Void Invoice')) + ->setIcon('fa-times') + ->setWorkflow(true) + ->setDisabled(!$can_void) + ->setHref($void_uri)); + } return $curtain; } diff --git a/src/applications/phortune/controller/cart/PhortuneCartVoidController.php b/src/applications/phortune/controller/cart/PhortuneCartVoidController.php new file mode 100644 index 0000000000..184775b10d --- /dev/null +++ b/src/applications/phortune/controller/cart/PhortuneCartVoidController.php @@ -0,0 +1,43 @@ +getViewer(); + $cart = $this->getCart(); + + $cancel_uri = $cart->getDetailURI(); + + try { + $title = pht('Unable to Void Invoice'); + $cart->assertCanVoidOrder(); + } catch (Exception $ex) { + return $this->newDialog() + ->setTitle($title) + ->appendChild($ex->getMessage()) + ->addCancelButton($cancel_uri); + } + + if ($request->isFormPost()) { + return id(new AphrontRedirectResponse())->setURI($cancel_uri); + } + + return $this->newDialog() + ->setTitle(pht('Void Invoice?')) + ->appendParagraph( + pht( + 'Really void this invoice? The customer will no longer be asked '. + 'to submit payment for it.')) + ->addCancelButton($cancel_uri) + ->addSubmitButton(pht('Void Invoice')); + } +} diff --git a/src/applications/phortune/controller/external/PhortuneExternalController.php b/src/applications/phortune/controller/external/PhortuneExternalController.php new file mode 100644 index 0000000000..2810142531 --- /dev/null +++ b/src/applications/phortune/controller/external/PhortuneExternalController.php @@ -0,0 +1,172 @@ +email; + } + + final protected function getAccountEmail() { + return $this->email; + } + + final protected function getExternalViewer() { + return PhabricatorUser::getOmnipotentUser(); + } + + final public function handleRequest(AphrontRequest $request) { + $address_key = $request->getURIData('addressKey'); + $access_key = $request->getURIData('accessKey'); + + $viewer = $this->getViewer(); + $xviewer = $this->getExternalViewer(); + + $email = id(new PhortuneAccountEmailQuery()) + ->setViewer($xviewer) + ->withAddressKeys(array($address_key)) + ->executeOne(); + if (!$email) { + return new Aphront404Response(); + } + + $account = $email->getAccount(); + + $can_see = PhabricatorPolicyFilter::hasCapability( + $viewer, + $account, + PhabricatorPolicyCapability::CAN_EDIT); + + $email_display = phutil_tag('strong', array(), $email->getAddress()); + $user_display = phutil_tag('strong', array(), $viewer->getUsername()); + + $actual_key = $email->getAccessKey(); + if (!phutil_hashes_are_identical($access_key, $actual_key)) { + $dialog = $this->newDialog() + ->setTitle(pht('Email Access Link Out of Date')) + ->appendParagraph( + pht( + 'You are trying to access this payment account as: %s', + $email_display)) + ->appendParagraph( + pht( + 'The access link you have followed is out of date and no longer '. + 'works.')); + + if ($can_see) { + $dialog->appendParagraph( + pht( + 'You are currently logged in as a user (%s) who has '. + 'permission to manage the payment account, so you can '. + 'continue to the updated link.', + $user_display)); + + $dialog->addCancelButton( + $email->getExternalURI(), + pht('Continue to Updated Link')); + } else { + $dialog->appendParagraph( + pht( + 'To access information about this payment account, follow '. + 'a more recent link or ask a user with access to give you '. + 'an updated link.')); + } + + return $dialog; + } + + switch ($email->getStatus()) { + case PhortuneAccountEmailStatus::STATUS_ACTIVE: + break; + case PhortuneAccountEmailStatus::STATUS_DISABLED: + return $this->newDialog() + ->setTitle(pht('Address Disabled')) + ->appendParagraph( + pht( + 'This email address (%s) has been disabled and no longer has '. + 'access to this payment account.', + $email_display)); + case PhortuneAccountEmailStatus::STATUS_UNSUBSCRIBED: + return $this->newDialog() + ->setTitle(pht('Permanently Unsubscribed')) + ->appendParagraph( + pht( + 'This email address (%s) has been permanently unsubscribed '. + 'and no longer has access to this payment account.', + $email_display)); + break; + default: + return new Aphront404Response(); + } + + $this->email = $email; + + return $this->handleExternalRequest($request); + } + + final protected function newExternalCrumbs() { + $viewer = $this->getViewer(); + + $crumbs = new PHUICrumbsView(); + + if ($this->hasAccountEmail()) { + $email = $this->getAccountEmail(); + $account = $email->getAccount(); + + $crumb_name = pht( + 'Payment Account: %s', + $account->getName()); + + $crumb = id(new PHUICrumbView()) + ->setIcon('fa-diamond') + ->setName($crumb_name) + ->setHref($email->getExternalURI()); + + $crumbs + ->addCrumb($crumb); + } else { + $crumb = id(new PHUICrumbView()) + ->setIcon('fa-diamond') + ->setText(pht('External Account View')); + + $crumbs->addCrumb($crumb); + } + + return $crumbs; + } + + final protected function newExternalView() { + $email = $this->getAccountEmail(); + $xviewer = $this->getExternalViewer(); + + $origin_phid = $email->getAuthorPHID(); + + $handles = $xviewer->loadHandles(array($origin_phid)); + + + $messages = array(); + $messages[] = pht( + 'You are viewing this payment account as: %s', + phutil_tag('strong', array(), $email->getAddress())); + + $messages[] = pht( + 'This email address was added to this payment account by: %s', + phutil_tag('strong', array(), $handles[$origin_phid]->getFullName())); + + $messages[] = pht( + 'Anyone who has a link to this page can view order history for '. + 'this payment account.'); + + return id(new PHUIInfoView()) + ->setSeverity(PHUIInfoView::SEVERITY_WARNING) + ->setErrors($messages); + } +} diff --git a/src/applications/phortune/controller/external/PhortuneExternalOrderController.php b/src/applications/phortune/controller/external/PhortuneExternalOrderController.php new file mode 100644 index 0000000000..36522f95af --- /dev/null +++ b/src/applications/phortune/controller/external/PhortuneExternalOrderController.php @@ -0,0 +1,98 @@ +getExternalViewer(); + $email = $this->getAccountEmail(); + $account = $email->getAccount(); + + $order = id(new PhortuneCartQuery()) + ->setViewer($xviewer) + ->withAccountPHIDs(array($account->getPHID())) + ->withIDs(array($request->getURIData('orderID'))) + ->executeOne(); + if (!$order) { + return new Aphront404Response(); + } + + $is_printable = ($request->getURIData('action') === 'print'); + + $order_view = id(new PhortuneOrderSummaryView()) + ->setViewer($xviewer) + ->setOrder($order) + ->setPrintable($is_printable); + + $crumbs = null; + $curtain = null; + + $main = array(); + $tail = array(); + + require_celerity_resource('phortune-invoice-css'); + + if ($is_printable) { + $body_class = 'phortune-invoice-view'; + + $tail[] = $order_view; + } else { + $body_class = 'phortune-cart-page'; + + $curtain = $this->newCurtain($order); + + $crumbs = $this->newExternalCrumbs() + ->addTextCrumb($order->getObjectName()) + ->setBorder(true); + + $timeline = $this->buildTransactionTimeline($order) + ->setShouldTerminate(true); + + $main[] = $order_view; + $main[] = $timeline; + } + + $column_view = id(new PHUITwoColumnView()) + ->setMainColumn($main) + ->setFooter($tail); + + if ($curtain) { + $column_view->setCurtain($curtain); + } + + $page = $this->newPage() + ->addClass($body_class) + ->setTitle( + array( + $order->getObjectName(), + $order->getName(), + )) + ->appendChild($column_view); + + if ($crumbs) { + $page->setCrumbs($crumbs); + } + + return $page; + } + + + private function newCurtain(PhortuneCart $order) { + $xviewer = $this->getExternalViewer(); + $email = $this->getAccountEmail(); + + $curtain = $this->newCurtainView($order); + + $print_uri = $email->getExternalOrderPrintURI($order); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Printable Version')) + ->setHref($print_uri) + ->setOpenInNewWindow(true) + ->setIcon('fa-print')); + + return $curtain; + } + +} diff --git a/src/applications/phortune/controller/external/PhortuneExternalOverviewController.php b/src/applications/phortune/controller/external/PhortuneExternalOverviewController.php new file mode 100644 index 0000000000..5db74ac75e --- /dev/null +++ b/src/applications/phortune/controller/external/PhortuneExternalOverviewController.php @@ -0,0 +1,101 @@ +getExternalViewer(); + $email = $this->getAccountEmail(); + $account = $email->getAccount(); + + $crumbs = $this->newExternalCrumbs() + ->addTextCrumb(pht('Viewing As "%s"', $email->getAddress())) + ->setBorder(true); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Invoices and Receipts: %s', $account->getName())) + ->addActionLink( + id(new PHUIButtonView()) + ->setTag('a') + ->setIcon('fa-times') + ->setText(pht('Unsubscribe')) + ->setHref($email->getUnsubscribeURI()) + ->setWorkflow(true)); + + $external_view = $this->newExternalView(); + $invoices_view = $this->newInvoicesView(); + $receipts_view = $this->newReceiptsView(); + + $column_view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter( + array( + $external_view, + $invoices_view, + $receipts_view, + )); + + return $this->newPage() + ->setCrumbs($crumbs) + ->setTitle( + array( + pht('Invoices and Receipts'), + $account->getName(), + )) + ->appendChild($column_view); + } + + private function newInvoicesView() { + $xviewer = $this->getExternalViewer(); + $email = $this->getAccountEmail(); + $account = $email->getAccount(); + + $invoices = id(new PhortuneCartQuery()) + ->setViewer($xviewer) + ->withAccountPHIDs(array($account->getPHID())) + ->needPurchases(true) + ->withInvoices(true) + ->execute(); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Invoices')); + + $invoices_table = id(new PhortuneOrderTableView()) + ->setViewer($xviewer) + ->setAccountEmail($email) + ->setCarts($invoices) + ->setIsInvoices(true); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setTable($invoices_table); + } + + private function newReceiptsView() { + $xviewer = $this->getExternalViewer(); + $email = $this->getAccountEmail(); + $account = $email->getAccount(); + + $receipts = id(new PhortuneCartQuery()) + ->setViewer($xviewer) + ->withAccountPHIDs(array($account->getPHID())) + ->needPurchases(true) + ->withInvoices(false) + ->execute(); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Receipts')); + + $receipts_table = id(new PhortuneOrderTableView()) + ->setViewer($xviewer) + ->setAccountEmail($email) + ->setCarts($receipts); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setTable($receipts_table); + } + +} diff --git a/src/applications/phortune/controller/external/PhortuneExternalUnsubscribeController.php b/src/applications/phortune/controller/external/PhortuneExternalUnsubscribeController.php new file mode 100644 index 0000000000..c7c29129d3 --- /dev/null +++ b/src/applications/phortune/controller/external/PhortuneExternalUnsubscribeController.php @@ -0,0 +1,67 @@ +getExternalViewer(); + $email = $this->getAccountEmail(); + $account = $email->getAccount(); + + $email_uri = $email->getExternalURI(); + + if ($request->isFormOrHisecPost()) { + $xactions = array(); + + $xactions[] = $email->getApplicationTransactionTemplate() + ->setTransactionType( + PhortuneAccountEmailStatusTransaction::TRANSACTIONTYPE) + ->setNewValue(PhortuneAccountEmailStatus::STATUS_UNSUBSCRIBED); + + $email->getApplicationTransactionEditor() + ->setActor($xviewer) + ->setActingAsPHID($email->getPHID()) + ->setContentSourceFromRequest($request) + ->setContinueOnMissingFields(true) + ->setContinueOnNoEffect(true) + ->setCancelURI($email_uri) + ->applyTransactions($email, $xactions); + + return id(new AphrontRedirectResponse())->setURI($email_uri); + } + + $email_display = phutil_tag( + 'strong', + array(), + $email->getAddress()); + + $account_display = phutil_tag( + 'strong', + array(), + $account->getName()); + + $submit = pht( + 'Permanently Unsubscribe (%s)', + $email->getAddress()); + + return $this->newDialog() + ->setTitle(pht('Permanently Unsubscribe')) + ->appendParagraph( + pht( + 'Permanently unsubscribe this email address (%s) from this '. + 'payment account (%s)?', + $email_display, + $account_display)) + ->appendParagraph( + pht( + 'You will no longer receive email and access links will no longer '. + 'function.')) + ->appendParagraph( + pht( + 'This action is permanent and can not be undone.')) + ->addCancelButton($email_uri) + ->addSubmitButton($submit); + + } + +} diff --git a/src/applications/phortune/controller/merchant/PhortuneMerchantAddManagerController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantAddManagerController.php index 3ef0a53874..0f4bb479e1 100644 --- a/src/applications/phortune/controller/merchant/PhortuneMerchantAddManagerController.php +++ b/src/applications/phortune/controller/merchant/PhortuneMerchantAddManagerController.php @@ -1,32 +1,23 @@ getViewer(); - $id = $request->getURIData('id'); - - $merchant = id(new PhortuneMerchantQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->needProfileImage(true) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$merchant) { - return new Aphront404Response(); - } + $merchant = $this->getMerchant(); $v_members = array(); $e_members = null; - $merchant_uri = $this->getApplicationURI("/merchant/manager/{$id}/"); + $merchant_uri = $merchant->getManagersURI(); - if ($request->isFormPost()) { + if ($request->isFormOrHiSecPost()) { $xactions = array(); - $v_members = $request->getArr('memberPHIDs'); + $v_members = $request->getArr('managerPHIDs'); $type_edge = PhabricatorTransactions::TYPE_EDGE; $xactions[] = id(new PhortuneMerchantTransaction()) @@ -59,13 +50,13 @@ final class PhortuneMerchantAddManagerController extends PhortuneController { ->appendControl( id(new AphrontFormTokenizerControl()) ->setDatasource(new PhabricatorPeopleDatasource()) - ->setLabel(pht('Members')) - ->setName('memberPHIDs') + ->setLabel(pht('New Managers')) + ->setName('managerPHIDs') ->setValue($v_members) ->setError($e_members)); return $this->newDialog() - ->setTitle(pht('Add New Manager')) + ->setTitle(pht('Add New Managers')) ->appendForm($form) ->setWidth(AphrontDialogView::WIDTH_FORM) ->addCancelButton($merchant_uri) diff --git a/src/applications/phortune/controller/merchant/PhortuneMerchantController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantController.php index 23b96197a0..15c4e0047a 100644 --- a/src/applications/phortune/controller/merchant/PhortuneMerchantController.php +++ b/src/applications/phortune/controller/merchant/PhortuneMerchantController.php @@ -3,11 +3,80 @@ abstract class PhortuneMerchantController extends PhortuneController { - protected function buildApplicationCrumbs() { - $crumbs = parent::buildApplicationCrumbs(); - $crumbs->addTextCrumb( - pht('Merchants'), - $this->getApplicationURI('merchant/')); - return $crumbs; + private $merchant; + + final protected function setMerchant(PhortuneMerchant $merchant) { + $this->merchant = $merchant; + return $this; } + + final protected function getMerchant() { + return $this->merchant; + } + + final protected function hasMerchant() { + return (bool)$this->merchant; + } + + final public function handleRequest(AphrontRequest $request) { + if ($this->shouldRequireMerchantEditCapability()) { + $response = $this->loadMerchantForEdit(); + } else { + $response = $this->loadMerchantForView(); + } + + if ($response) { + return $response; + } + + return $this->handleMerchantRequest($request); + } + + abstract protected function shouldRequireMerchantEditCapability(); + abstract protected function handleMerchantRequest(AphrontRequest $request); + + private function loadMerchantForEdit() { + return $this->loadMerchantWithCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )); + } + + private function loadMerchantForView() { + return $this->loadMerchantWithCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + )); + } + + private function loadMerchantWithCapabilities(array $capabilities) { + $viewer = $this->getViewer(); + $request = $this->getRequest(); + + $merchant_id = $request->getURIData('merchantID'); + if (!$merchant_id) { + throw new Exception( + pht( + 'Controller ("%s") extends controller "%s", but is reachable '. + 'with no "merchantID" in URI.', + get_class($this), + __CLASS__)); + } + + $merchant = id(new PhortuneMerchantQuery()) + ->setViewer($viewer) + ->withIDs(array($merchant_id)) + ->needProfileImage(true) + ->requireCapabilities($capabilities) + ->executeOne(); + if (!$merchant) { + return new Aphront404Response(); + } + + $this->setMerchant($merchant); + + return null; + } + } diff --git a/src/applications/phortune/controller/merchant/PhortuneMerchantDetailsController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantDetailsController.php new file mode 100644 index 0000000000..8b558ac387 --- /dev/null +++ b/src/applications/phortune/controller/merchant/PhortuneMerchantDetailsController.php @@ -0,0 +1,151 @@ +getViewer(); + $id = $request->getURIData('id'); + + $merchant = $this->getMerchant(); + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb(pht('Account Details')) + ->setBorder(true); + $header = $this->buildHeaderView(); + + $title = pht( + '%s %s', + $merchant->getObjectName(), + $merchant->getName()); + + $details = $this->buildDetailsView($merchant); + $curtain = $this->buildCurtainView($merchant); + + $timeline = $this->buildTransactionTimeline( + $merchant, + new PhortuneMerchantTransactionQuery()); + $timeline->setShouldTerminate(true); + + $navigation = $this->buildSideNavView('details'); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->setMainColumn(array( + $details, + $timeline, + )); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->setNavigation($navigation) + ->appendChild($view); + } + + private function buildDetailsView(PhortuneMerchant $merchant) { + $viewer = $this->getViewer(); + + $view = id(new PHUIPropertyListView()) + ->setUser($viewer) + ->setObject($merchant); + + $invoice_from = $merchant->getInvoiceEmail(); + if (!$invoice_from) { + $invoice_from = pht('No email address set'); + $invoice_from = phutil_tag('em', array(), $invoice_from); + } + $view->addProperty(pht('Invoice From'), $invoice_from); + + $description = $merchant->getDescription(); + if (strlen($description)) { + $description = new PHUIRemarkupView($viewer, $description); + $view->addSectionHeader( + pht('Description'), + PHUIPropertyListView::ICON_SUMMARY); + $view->addTextContent($description); + } + + $contact_info = $merchant->getContactInfo(); + if (strlen($contact_info)) { + $contact_info = new PHUIRemarkupView($viewer, $contact_info); + $view->addSectionHeader( + pht('Contact Information'), + PHUIPropertyListView::ICON_SUMMARY); + $view->addTextContent($contact_info); + } + + $footer_info = $merchant->getInvoiceFooter(); + if (strlen($footer_info)) { + $footer_info = new PHUIRemarkupView($viewer, $footer_info); + $view->addSectionHeader( + pht('Invoice Footer'), + PHUIPropertyListView::ICON_SUMMARY); + $view->addTextContent($footer_info); + } + + return id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Details')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->appendChild($view); + } + + private function buildCurtainView(PhortuneMerchant $merchant) { + $viewer = $this->getRequest()->getUser(); + $id = $merchant->getID(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $merchant, + PhabricatorPolicyCapability::CAN_EDIT); + + $curtain = $this->newCurtainView($merchant); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Merchant')) + ->setIcon('fa-pencil') + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit) + ->setHref($this->getApplicationURI("merchant/edit/{$id}/"))); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Logo')) + ->setIcon('fa-picture-o') + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit) + ->setHref($this->getApplicationURI("merchant/{$id}/picture/edit/"))); + + $member_phids = $merchant->getMemberPHIDs(); + $handles = $viewer->loadHandles($member_phids); + + $member_list = id(new PHUIObjectItemListView()) + ->setSimple(true); + + foreach ($member_phids as $member_phid) { + $image_uri = $handles[$member_phid]->getImageURI(); + $image_href = $handles[$member_phid]->getURI(); + $person = $handles[$member_phid]; + + $member = id(new PHUIObjectItemView()) + ->setImageURI($image_uri) + ->setHref($image_href) + ->setHeader($person->getFullName()); + + $member_list->addItem($member); + } + + $curtain->newPanel() + ->setHeaderText(pht('Managers')) + ->appendChild($member_list); + + return $curtain; + } + +} diff --git a/src/applications/phortune/controller/merchant/PhortuneMerchantEditController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantEditController.php index 07c743251c..78125d43d6 100644 --- a/src/applications/phortune/controller/merchant/PhortuneMerchantEditController.php +++ b/src/applications/phortune/controller/merchant/PhortuneMerchantEditController.php @@ -1,7 +1,7 @@ getUser(); $merchant = $this->loadMerchantAuthority(); @@ -58,9 +65,10 @@ final class PhortuneMerchantInvoiceCreateController } if (!$target_account) { - $accounts = PhortuneAccountQuery::loadAccountsForUser( - $target_user, - PhabricatorContentSource::newFromRequest($request)); + $accounts = id(new PhortuneAccountQuery()) + ->setViewer($viewer) + ->withMemberPHIDs(array($target_user->getPHID())) + ->execute(); $form = id(new AphrontFormView()) ->setUser($viewer) diff --git a/src/applications/phortune/controller/merchant/PhortuneMerchantListController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantListController.php index 48fb02195b..87cfaaf880 100644 --- a/src/applications/phortune/controller/merchant/PhortuneMerchantListController.php +++ b/src/applications/phortune/controller/merchant/PhortuneMerchantListController.php @@ -1,37 +1,12 @@ getViewer(); - $querykey = $request->getURIData('queryKey'); - - $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($querykey) - ->setSearchEngine(new PhortuneMerchantSearchEngine()) - ->setNavigation($this->buildSideNavView()); - - return $this->delegateToController($controller); - } - - public function buildSideNavView() { - $viewer = $this->getViewer(); - - $nav = new AphrontSideNavFilterView(); - $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); - - id(new PhortuneMerchantSearchEngine()) - ->setViewer($viewer) - ->addNavigationItems($nav->getMenu()); - - $nav->selectFilter(null); - - return $nav; + return id(new PhortuneMerchantSearchEngine()) + ->setController($this) + ->buildResponse(); } protected function buildApplicationCrumbs() { diff --git a/src/applications/phortune/controller/merchant/PhortuneMerchantManagerController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantManagersController.php similarity index 73% rename from src/applications/phortune/controller/merchant/PhortuneMerchantManagerController.php rename to src/applications/phortune/controller/merchant/PhortuneMerchantManagersController.php index 0b9c0c6598..6b3ff568ff 100644 --- a/src/applications/phortune/controller/merchant/PhortuneMerchantManagerController.php +++ b/src/applications/phortune/controller/merchant/PhortuneMerchantManagersController.php @@ -1,35 +1,29 @@ getViewer(); - $id = $request->getURIData('id'); + $merchant = $this->getMerchant(); - $merchant = id(new PhortuneMerchantQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->needProfileImage(true) - ->executeOne(); - if (!$merchant) { - return new Aphront404Response(); - } - - $this->setMerchant($merchant); - $header = $this->buildHeaderView(); - - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('Managers')); + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb(pht('Managers')) + ->setBorder(true); $header = $this->buildHeaderView(); $members = $this->buildMembersSection($merchant); $view = id(new PHUITwoColumnView()) ->setHeader($header) - ->setFooter(array( - $members, - )); + ->setFooter( + array( + $members, + )); $navigation = $this->buildSideNavView('managers'); @@ -38,7 +32,6 @@ final class PhortuneMerchantManagerController ->setCrumbs($crumbs) ->setNavigation($navigation) ->appendChild($view); - } private function buildMembersSection(PhortuneMerchant $merchant) { @@ -51,12 +44,18 @@ final class PhortuneMerchantManagerController $id = $merchant->getID(); + $add_uri = urisprintf( + 'merchant/%d/managers/new/', + $merchant->getID()); + $add_uri = $this->getApplicationURI($add_uri); + $add = id(new PHUIButtonView()) ->setTag('a') ->setText(pht('New Manager')) ->setIcon('fa-plus') ->setWorkflow(true) - ->setHref("/phortune/merchant/manager/add/{$id}/"); + ->setDisabled(!$can_edit) + ->setHref($add_uri); $header = id(new PHUIHeaderView()) ->setHeader(pht('Merchant Account Managers')) diff --git a/src/applications/phortune/controller/merchant/PhortuneMerchantOrderListController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantOrderListController.php new file mode 100644 index 0000000000..5d33564ba7 --- /dev/null +++ b/src/applications/phortune/controller/merchant/PhortuneMerchantOrderListController.php @@ -0,0 +1,55 @@ +getViewer(); + $merchant = $this->getMerchant(); + + $engine = id(new PhortuneCartSearchEngine()) + ->setController($this) + ->setMerchant($merchant); + + $subscription_id = $request->getURIData('subscriptionID'); + if ($subscription_id) { + $subscription = id(new PhortuneSubscriptionQuery()) + ->setViewer($viewer) + ->withIDs(array($subscription_id)) + ->executeOne(); + if (!$subscription) { + return new Aphront404Response(); + } + + $engine->setSubscription($subscription); + $this->subscription = $subscription; + } + + return $engine->buildResponse(); + } + + protected function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + + $subscription = $this->subscription; + if ($subscription) { + $crumbs->addTextCrumb( + $subscription->getObjectName(), + $subscription->getURI()); + } else if ($this->hasMerchant()) { + $merchant = $this->getMerchant(); + + $crumbs->addTextCrumb(pht('Orders'), $merchant->getOrdersURI()); + } + + return $crumbs; + } + + +} diff --git a/src/applications/phortune/controller/merchant/PhortuneMerchantOrdersController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantOrdersController.php new file mode 100644 index 0000000000..4f84d0cce7 --- /dev/null +++ b/src/applications/phortune/controller/merchant/PhortuneMerchantOrdersController.php @@ -0,0 +1,78 @@ +getMerchant(); + $title = $merchant->getName(); + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb(pht('Orders')) + ->setBorder(true); + + $header = $this->buildHeaderView(); + $order_history = $this->newRecentOrdersView($merchant, 100); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter( + array( + $order_history, + )); + + $navigation = $this->buildSideNavView('orders'); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->setNavigation($navigation) + ->appendChild($view); + } + + private function newRecentOrdersView( + PhortuneMerchant $merchant, + $limit) { + + $viewer = $this->getViewer(); + + $carts = id(new PhortuneCartQuery()) + ->setViewer($viewer) + ->withMerchantPHIDs(array($merchant->getPHID())) + ->needPurchases(true) + ->withStatuses( + array( + PhortuneCart::STATUS_PURCHASING, + PhortuneCart::STATUS_CHARGED, + PhortuneCart::STATUS_HOLD, + PhortuneCart::STATUS_REVIEW, + PhortuneCart::STATUS_PURCHASED, + )) + ->setLimit($limit) + ->execute(); + + $orders_uri = $merchant->getOrderListURI(); + + $table = id(new PhortuneOrderTableView()) + ->setUser($viewer) + ->setCarts($carts); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Recent Orders')) + ->addActionLink( + id(new PHUIButtonView()) + ->setTag('a') + ->setIcon('fa-list') + ->setHref($orders_uri) + ->setText(pht('View All Orders'))); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setTable($table); + } +} diff --git a/src/applications/phortune/controller/merchant/PhortuneMerchantOverviewController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantOverviewController.php new file mode 100644 index 0000000000..2925a7d34b --- /dev/null +++ b/src/applications/phortune/controller/merchant/PhortuneMerchantOverviewController.php @@ -0,0 +1,136 @@ +getViewer(); + $merchant = $this->getMerchant(); + + $crumbs = $this->buildApplicationCrumbs() + ->setBorder(true); + + $header = $this->buildHeaderView(); + + $title = pht( + '%s %s', + $merchant->getObjectName(), + $merchant->getName()); + + $providers = id(new PhortunePaymentProviderConfigQuery()) + ->setViewer($viewer) + ->withMerchantPHIDs(array($merchant->getPHID())) + ->execute(); + + $details = $this->buildDetailsView($merchant, $providers); + $navigation = $this->buildSideNavView('overview'); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter( + array( + $details, + )); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->setNavigation($navigation) + ->appendChild($view); + } + + private function buildDetailsView( + PhortuneMerchant $merchant, + array $providers) { + + $viewer = $this->getRequest()->getUser(); + + $view = id(new PHUIPropertyListView()) + ->setUser($viewer) + ->setObject($merchant); + + $status_view = new PHUIStatusListView(); + + $have_any = false; + $any_test = false; + foreach ($providers as $provider_config) { + $provider = $provider_config->buildProvider(); + if ($provider->isEnabled()) { + $have_any = true; + } + if (!$provider->isAcceptingLivePayments()) { + $any_test = true; + } + } + + if ($have_any) { + $status_view->addItem( + id(new PHUIStatusItemView()) + ->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green') + ->setTarget(pht('Accepts Payments')) + ->setNote(pht('This merchant can accept payments.'))); + + if ($any_test) { + $status_view->addItem( + id(new PHUIStatusItemView()) + ->setIcon(PHUIStatusItemView::ICON_WARNING, 'yellow') + ->setTarget(pht('Test Mode')) + ->setNote(pht('This merchant is accepting test payments.'))); + } else { + $status_view->addItem( + id(new PHUIStatusItemView()) + ->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green') + ->setTarget(pht('Live Mode')) + ->setNote(pht('This merchant is accepting live payments.'))); + } + } else if ($providers) { + $status_view->addItem( + id(new PHUIStatusItemView()) + ->setIcon(PHUIStatusItemView::ICON_REJECT, 'red') + ->setTarget(pht('No Enabled Providers')) + ->setNote( + pht( + 'All of the payment providers for this merchant are '. + 'disabled.'))); + } else { + $status_view->addItem( + id(new PHUIStatusItemView()) + ->setIcon(PHUIStatusItemView::ICON_WARNING, 'yellow') + ->setTarget(pht('No Providers')) + ->setNote( + pht( + 'This merchant does not have any payment providers configured '. + 'yet, so it can not accept payments. Add a provider.'))); + } + + $view->addProperty(pht('Status'), $status_view); + + $description = $merchant->getDescription(); + if (strlen($description)) { + $description = new PHUIRemarkupView($viewer, $description); + $view->addSectionHeader( + pht('Description'), + PHUIPropertyListView::ICON_SUMMARY); + $view->addTextContent($description); + } + + $contact_info = $merchant->getContactInfo(); + if (strlen($contact_info)) { + $contact_info = new PHUIRemarkupView($viewer, $contact_info); + $view->addSectionHeader( + pht('Contact Information'), + PHUIPropertyListView::ICON_SUMMARY); + $view->addTextContent($contact_info); + } + + return id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Details')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->appendChild($view); + } + +} diff --git a/src/applications/phortune/controller/merchant/PhortuneMerchantPictureController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantPictureController.php index cbd122f682..96f4f8e367 100644 --- a/src/applications/phortune/controller/merchant/PhortuneMerchantPictureController.php +++ b/src/applications/phortune/controller/merchant/PhortuneMerchantPictureController.php @@ -1,28 +1,17 @@ getViewer(); - $id = $request->getURIData('id'); + $merchant = $this->getMerchant(); - $merchant = id(new PhortuneMerchantQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->needProfileImage(true) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$merchant) { - return new Aphront404Response(); - } - - $this->setMerchant($merchant); - $uri = $merchant->getURI(); + $uri = $merchant->getDetailsURI(); $supported_formats = PhabricatorFile::getTransformableImageFormats(); $e_file = true; @@ -222,12 +211,9 @@ final class PhortuneMerchantPictureController $upload_box, )); - $navigation = $this->buildSideNavView(); - return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) - ->setNavigation($navigation) ->appendChild( array( $view, diff --git a/src/applications/phortune/controller/merchant/PhortuneMerchantProfileController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantProfileController.php index 45911bfef9..bc54756ffe 100644 --- a/src/applications/phortune/controller/merchant/PhortuneMerchantProfileController.php +++ b/src/applications/phortune/controller/merchant/PhortuneMerchantProfileController.php @@ -1,22 +1,7 @@ merchant = $merchant; - return $this; - } - - public function getMerchant() { - return $this->merchant; - } - - public function buildApplicationMenu() { - return $this->buildSideNavView()->getMenu(); - } + extends PhortuneMerchantController { protected function buildHeaderView() { $viewer = $this->getViewer(); @@ -26,20 +11,20 @@ abstract class PhortuneMerchantProfileController $header = id(new PHUIHeaderView()) ->setHeader($title) ->setUser($viewer) - ->setPolicyObject($merchant) ->setImage($merchant->getProfileImageURI()); return $header; } protected function buildApplicationCrumbs() { - $merchant = $this->getMerchant(); - $id = $merchant->getID(); - $merchant_uri = $this->getApplicationURI("/merchant/{$id}/"); - $crumbs = parent::buildApplicationCrumbs(); - $crumbs->addTextCrumb($merchant->getName(), $merchant_uri); - $crumbs->setBorder(true); + + if ($this->hasMerchant()) { + $merchant = $this->getMerchant(); + $merchant_uri = $merchant->getURI(); + $crumbs->addTextCrumb($merchant->getName(), $merchant_uri); + } + return $crumbs; } @@ -58,31 +43,47 @@ abstract class PhortuneMerchantProfileController $nav->addLabel(pht('Merchant')); - $nav->addFilter( - 'overview', - pht('Overview'), - $this->getApplicationURI("/merchant/{$id}/"), - 'fa-building-o'); + $nav->newLink('overview') + ->setName(pht('Overview')) + ->setHref($merchant->getURI()) + ->setIcon('fa-building-o'); - if ($can_edit) { - $nav->addFilter( - 'orders', - pht('Orders'), - $this->getApplicationURI("merchant/orders/{$id}/"), - 'fa-retweet'); + $nav->newLink('details') + ->setName(pht('Account Details')) + ->setHref($merchant->getDetailsURI()) + ->setIcon('fa-address-card-o') + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit); - $nav->addFilter( - 'subscriptions', - pht('Subscriptions'), - $this->getApplicationURI("merchant/{$id}/subscription/"), - 'fa-shopping-cart'); + $nav->addLabel(pht('Payments')); - $nav->addFilter( - 'managers', - pht('Managers'), - $this->getApplicationURI("/merchant/manager/{$id}/"), - 'fa-group'); - } + $nav->newLink('providers') + ->setName(pht('Payment Providers')) + ->setHref($merchant->getPaymentProvidersURI()) + ->setIcon('fa-credit-card') + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit); + + $nav->newLink('orders') + ->setName(pht('Orders')) + ->setHref($merchant->getOrdersURI()) + ->setIcon('fa-shopping-bag') + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit); + + $nav->newLink('subscriptions') + ->setName(pht('Subscriptions')) + ->setHref($merchant->getSubscriptionsURI()) + ->setIcon('fa-retweet') + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit); + + $nav->addLabel(pht('Personnel')); + + $nav->newLink('managers') + ->setName(pht('Managers')) + ->setHref($merchant->getManagersURI()) + ->setIcon('fa-group'); $nav->selectFilter($filter); diff --git a/src/applications/phortune/controller/provider/PhortuneProviderDisableController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantProviderDisableController.php similarity index 84% rename from src/applications/phortune/controller/provider/PhortuneProviderDisableController.php rename to src/applications/phortune/controller/merchant/PhortuneMerchantProviderDisableController.php index 03236b54bc..01928fca1b 100644 --- a/src/applications/phortune/controller/provider/PhortuneProviderDisableController.php +++ b/src/applications/phortune/controller/merchant/PhortuneMerchantProviderDisableController.php @@ -1,11 +1,17 @@ getViewer(); - $id = $request->getURIData('id'); + $merchant = $this->getMerchant(); + + $id = $request->getURIData('providerID'); $provider_config = id(new PhortunePaymentProviderConfigQuery()) ->setViewer($viewer) @@ -20,9 +26,8 @@ final class PhortuneProviderDisableController return new Aphront404Response(); } - $merchant = $provider_config->getMerchant(); $merchant_id = $merchant->getID(); - $cancel_uri = $this->getApplicationURI("merchant/{$merchant_id}/"); + $cancel_uri = $provider_config->getURI(); $provider = $provider_config->buildProvider(); diff --git a/src/applications/phortune/controller/provider/PhortuneProviderEditController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantProviderEditController.php similarity index 87% rename from src/applications/phortune/controller/provider/PhortuneProviderEditController.php rename to src/applications/phortune/controller/merchant/PhortuneMerchantProviderEditController.php index f7ad2486c4..bd577ecfb5 100644 --- a/src/applications/phortune/controller/provider/PhortuneProviderEditController.php +++ b/src/applications/phortune/controller/merchant/PhortuneMerchantProviderEditController.php @@ -1,16 +1,23 @@ getViewer(); + $merchant = $this->getMerchant(); + $id = $request->getURIData('id'); if ($id) { $provider_config = id(new PhortunePaymentProviderConfigQuery()) ->setViewer($viewer) ->withIDs(array($id)) + ->withMerchantPHIDs(array($merchant->getPHID())) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, @@ -25,20 +32,8 @@ final class PhortuneProviderEditController $merchant = $provider_config->getMerchant(); $merchant_id = $merchant->getID(); - $cancel_uri = $this->getApplicationURI("merchant/{$merchant_id}/"); + $cancel_uri = $provider_config->getURI(); } else { - $merchant = id(new PhortuneMerchantQuery()) - ->setViewer($viewer) - ->withIDs(array($request->getStr('merchantID'))) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$merchant) { - return new Aphront404Response(); - } $merchant_id = $merchant->getID(); $current_providers = id(new PhortunePaymentProviderConfigQuery()) @@ -62,9 +57,7 @@ final class PhortuneProviderEditController } $provider_config->setProviderClass($class); - - $cancel_uri = $this->getApplicationURI( - 'provider/edit/?merchantID='.$merchant_id); + $cancel_uri = $merchant->getPaymentProvidersURI(); } $provider = $provider_config->buildProvider(); @@ -123,10 +116,12 @@ final class PhortuneProviderEditController $xactions = array(); - $xactions[] = id(new PhortunePaymentProviderConfigTransaction()) - ->setTransactionType( - PhortunePaymentProviderConfigTransaction::TYPE_CREATE) - ->setNewValue(true); + if (!$provider_config->getID()) { + $xactions[] = id(new PhortunePaymentProviderConfigTransaction()) + ->setTransactionType( + PhortunePaymentProviderConfigTransaction::TYPE_CREATE) + ->setNewValue(true); + } foreach ($xaction_values as $key => $value) { $xactions[] = id(clone $template) @@ -143,9 +138,9 @@ final class PhortuneProviderEditController $editor->applyTransactions($provider_config, $xactions); - $merchant_uri = $this->getApplicationURI( - 'merchant/'.$merchant->getID().'/'); - return id(new AphrontRedirectResponse())->setURI($merchant_uri); + $next_uri = $provider_config->getURI(); + + return id(new AphrontRedirectResponse())->setURI($next_uri); } } } else { @@ -155,7 +150,6 @@ final class PhortuneProviderEditController $form = id(new AphrontFormView()) ->setUser($viewer) - ->addHiddenInput('merchantID', $merchant->getID()) ->addHiddenInput('class', $provider_config->getProviderClass()) ->addHiddenInput('edit', true) ->appendChild( @@ -261,7 +255,6 @@ final class PhortuneProviderEditController $form = id(new AphrontFormView()) ->setUser($viewer) - ->addHiddenInput('merchantID', $merchant->getID()) ->appendRemarkupInstructions( pht('Choose the type of payment provider to add:')) ->appendChild($panel_classes) diff --git a/src/applications/phortune/controller/merchant/PhortuneMerchantProviderViewController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantProviderViewController.php new file mode 100644 index 0000000000..2c1be9f8af --- /dev/null +++ b/src/applications/phortune/controller/merchant/PhortuneMerchantProviderViewController.php @@ -0,0 +1,127 @@ +getViewer(); + $merchant = $this->getMerchant(); + + $provider = id(new PhortunePaymentProviderConfigQuery()) + ->setViewer($viewer) + ->withIDs(array($request->getURIData('providerID'))) + ->executeOne(); + if (!$provider) { + return new Aphront404Response(); + } + + $provider_type = $provider->buildProvider(); + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb($merchant->getName(), $merchant->getURI()) + ->addTextCrumb( + pht('Payment Providers'), + $merchant->getPaymentProvidersURI()) + ->addTextCrumb($provider->getObjectName()) + ->setBorder(true); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Payment Provider: %s', $provider_type->getName())); + + $details = $this->newDetailsView($provider); + + $timeline = $this->buildTransactionTimeline( + $provider, + new PhortunePaymentProviderConfigTransactionQuery()); + $timeline->setShouldTerminate(true); + + $curtain = $this->buildCurtainView($provider); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->setMainColumn( + array( + $details, + $timeline, + )); + + return $this->newPage() + ->setTitle($provider->getObjectName()) + ->setCrumbs($crumbs) + ->appendChild($view); + } + + private function buildCurtainView(PhortunePaymentProviderConfig $provider) { + $viewer = $this->getViewer(); + $merchant = $this->getMerchant(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $provider, + PhabricatorPolicyCapability::CAN_EDIT); + + $edit_uri = $this->getApplicationURI( + urisprintf( + 'merchant/%d/providers/edit/%d/', + $merchant->getID(), + $provider->getID())); + + $disable_uri = $this->getApplicationURI( + urisprintf( + 'merchant/%d/providers/%d/disable/', + $merchant->getID(), + $provider->getID())); + + $curtain = $this->newCurtainView($provider); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Provider')) + ->setIcon('fa-pencil') + ->setHref($edit_uri) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + + $provider_type = $provider->buildProvider(); + + if ($provider_type->isEnabled()) { + $disable_icon = 'fa-times'; + $disable_name = pht('Disable Provider'); + } else { + $disable_icon = 'fa-check'; + $disable_name = pht('Enable Provider'); + } + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName($disable_name) + ->setIcon($disable_icon) + ->setHref($disable_uri) + ->setDisabled(!$can_edit) + ->setWorkflow(true)); + + return $curtain; + } + + private function newDetailsView(PhortunePaymentProviderConfig $provider) { + $viewer = $this->getViewer(); + + $view = id(new PHUIPropertyListView()) + ->setUser($viewer); + + $provider_type = $provider->buildProvider(); + + $view->addProperty(pht('Provider Type'), $provider_type->getName()); + + return id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Payment Provider Details')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->addPropertyList($view); + } + +} diff --git a/src/applications/phortune/controller/merchant/PhortuneMerchantProvidersController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantProvidersController.php new file mode 100644 index 0000000000..2d64bdc726 --- /dev/null +++ b/src/applications/phortune/controller/merchant/PhortuneMerchantProvidersController.php @@ -0,0 +1,116 @@ +getViewer(); + $merchant = $this->getMerchant(); + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb(pht('Payment Providers')) + ->setBorder(true); + + $header = $this->buildHeaderView(); + + $title = pht( + '%s %s', + $merchant->getObjectName(), + $merchant->getName()); + + $providers = id(new PhortunePaymentProviderConfigQuery()) + ->setViewer($viewer) + ->withMerchantPHIDs(array($merchant->getPHID())) + ->execute(); + + $provider_list = $this->buildProviderList( + $merchant, + $providers); + + $navigation = $this->buildSideNavView('providers'); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter( + array( + $provider_list, + )); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->setNavigation($navigation) + ->appendChild($view); + } + + private function buildProviderList( + PhortuneMerchant $merchant, + array $providers) { + + $viewer = $this->getRequest()->getUser(); + $id = $merchant->getID(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $merchant, + PhabricatorPolicyCapability::CAN_EDIT); + + $provider_list = id(new PHUIObjectItemListView()) + ->setNoDataString(pht('This merchant has no payment providers.')); + + foreach ($providers as $provider_config) { + $provider = $provider_config->buildProvider(); + $provider_id = $provider_config->getID(); + + $item = id(new PHUIObjectItemView()) + ->setObjectName($provider_config->getObjectName()) + ->setHeader($provider->getName()) + ->setHref($provider_config->getURI()); + + if ($provider->isEnabled()) { + if ($provider->isAcceptingLivePayments()) { + $item->setStatusIcon('fa-check green'); + } else { + $item->setStatusIcon('fa-warning yellow'); + $item->addIcon('fa-exclamation-triangle', pht('Test Mode')); + } + + $item->addAttribute($provider->getConfigureProvidesDescription()); + } else { + $item->setDisabled(true); + $item->addAttribute( + phutil_tag('em', array(), pht('This payment provider is disabled.'))); + } + + $provider_list->addItem($item); + } + + $add_uri = urisprintf( + 'merchant/%d/providers/edit/', + $merchant->getID()); + $add_uri = $this->getApplicationURI($add_uri); + + $add_action = id(new PHUIButtonView()) + ->setTag('a') + ->setHref($add_uri) + ->setText(pht('Add Payment Provider')) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit) + ->setIcon('fa-plus'); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Payment Providers')) + ->addActionLink($add_action); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setObjectList($provider_list); + } + + +} diff --git a/src/applications/phortune/controller/merchant/PhortuneMerchantSubscriptionListController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantSubscriptionListController.php new file mode 100644 index 0000000000..d5a8d4eef2 --- /dev/null +++ b/src/applications/phortune/controller/merchant/PhortuneMerchantSubscriptionListController.php @@ -0,0 +1,50 @@ +getViewer(); + $merchant = $this->getMerchant(); + + $engine = id(new PhortuneCartSearchEngine()) + ->setController($this) + ->setMerchant($merchant); + + $subscription_id = $request->getURIData('subscriptionID'); + if ($subscription_id) { + $subscription = id(new PhortuneSubscriptionQuery()) + ->setViewer($viewer) + ->withIDs(array($subscription_id)) + ->executeOne(); + if (!$subscription) { + return new Aphront404Response(); + } + + $engine->setSubscription($subscription); + $this->subscription = $subscription; + } + + return $engine->buildResponse(); + } + + protected function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + + if ($this->hasMerchant()) { + $merchant = $this->getMerchant(); + + $crumbs->addTextCrumb( + pht('Subscriptions'), + $merchant->getSubscriptionsURI()); + } + + return $crumbs; + } + + +} diff --git a/src/applications/phortune/controller/merchant/PhortuneMerchantSubscriptionsController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantSubscriptionsController.php new file mode 100644 index 0000000000..c2fe0d429a --- /dev/null +++ b/src/applications/phortune/controller/merchant/PhortuneMerchantSubscriptionsController.php @@ -0,0 +1,68 @@ +getMerchant(); + $title = $merchant->getName(); + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb(pht('Subscriptions')) + ->setBorder(true); + + $header = $this->buildHeaderView(); + + $subscriptions = $this->buildSubscriptionsSection($merchant); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter( + array( + $subscriptions, + )); + + $navigation = $this->buildSideNavView('subscriptions'); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->setNavigation($navigation) + ->appendChild($view); + } + + private function buildSubscriptionsSection(PhortuneMerchant $merchant) { + $viewer = $this->getViewer(); + + $subscriptions = id(new PhortuneSubscriptionQuery()) + ->setViewer($viewer) + ->withMerchantPHIDs(array($merchant->getPHID())) + ->setLimit(25) + ->execute(); + + $subscriptions_uri = $merchant->getSubscriptionListURI(); + + $table = id(new PhortuneSubscriptionTableView()) + ->setUser($viewer) + ->setSubscriptions($subscriptions); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Subscriptions')) + ->addActionLink( + id(new PHUIButtonView()) + ->setTag('a') + ->setIcon('fa-list') + ->setHref($subscriptions_uri) + ->setText(pht('View All Subscriptions'))); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setTable($table); + } + +} diff --git a/src/applications/phortune/controller/merchant/PhortuneMerchantViewController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantViewController.php deleted file mode 100644 index 10dee4b0ea..0000000000 --- a/src/applications/phortune/controller/merchant/PhortuneMerchantViewController.php +++ /dev/null @@ -1,324 +0,0 @@ -getViewer(); - $id = $request->getURIData('id'); - - $merchant = id(new PhortuneMerchantQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->needProfileImage(true) - ->executeOne(); - if (!$merchant) { - return new Aphront404Response(); - } - - $this->setMerchant($merchant); - $crumbs = $this->buildApplicationCrumbs(); - $header = $this->buildHeaderView(); - - $title = pht( - 'Merchant %d %s', - $merchant->getID(), - $merchant->getName()); - - $providers = id(new PhortunePaymentProviderConfigQuery()) - ->setViewer($viewer) - ->withMerchantPHIDs(array($merchant->getPHID())) - ->execute(); - - $details = $this->buildDetailsView($merchant, $providers); - $curtain = $this->buildCurtainView($merchant); - - $provider_list = $this->buildProviderList( - $merchant, - $providers); - - $timeline = $this->buildTransactionTimeline( - $merchant, - new PhortuneMerchantTransactionQuery()); - $timeline->setShouldTerminate(true); - - $navigation = $this->buildSideNavView('overview'); - - $view = id(new PHUITwoColumnView()) - ->setHeader($header) - ->setCurtain($curtain) - ->setMainColumn(array( - $details, - $provider_list, - $timeline, - )); - - return $this->newPage() - ->setTitle($title) - ->setCrumbs($crumbs) - ->setNavigation($navigation) - ->appendChild($view); - } - - private function buildDetailsView( - PhortuneMerchant $merchant, - array $providers) { - - $viewer = $this->getRequest()->getUser(); - - $view = id(new PHUIPropertyListView()) - ->setUser($viewer) - ->setObject($merchant); - - $status_view = new PHUIStatusListView(); - - $have_any = false; - $any_test = false; - foreach ($providers as $provider_config) { - $provider = $provider_config->buildProvider(); - if ($provider->isEnabled()) { - $have_any = true; - } - if (!$provider->isAcceptingLivePayments()) { - $any_test = true; - } - } - - if ($have_any) { - $status_view->addItem( - id(new PHUIStatusItemView()) - ->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green') - ->setTarget(pht('Accepts Payments')) - ->setNote(pht('This merchant can accept payments.'))); - - if ($any_test) { - $status_view->addItem( - id(new PHUIStatusItemView()) - ->setIcon(PHUIStatusItemView::ICON_WARNING, 'yellow') - ->setTarget(pht('Test Mode')) - ->setNote(pht('This merchant is accepting test payments.'))); - } else { - $status_view->addItem( - id(new PHUIStatusItemView()) - ->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green') - ->setTarget(pht('Live Mode')) - ->setNote(pht('This merchant is accepting live payments.'))); - } - } else if ($providers) { - $status_view->addItem( - id(new PHUIStatusItemView()) - ->setIcon(PHUIStatusItemView::ICON_REJECT, 'red') - ->setTarget(pht('No Enabled Providers')) - ->setNote( - pht( - 'All of the payment providers for this merchant are '. - 'disabled.'))); - } else { - $status_view->addItem( - id(new PHUIStatusItemView()) - ->setIcon(PHUIStatusItemView::ICON_WARNING, 'yellow') - ->setTarget(pht('No Providers')) - ->setNote( - pht( - 'This merchant does not have any payment providers configured '. - 'yet, so it can not accept payments. Add a provider.'))); - } - - $view->addProperty(pht('Status'), $status_view); - - $invoice_from = $merchant->getInvoiceEmail(); - if (!$invoice_from) { - $invoice_from = pht('No email address set'); - $invoice_from = phutil_tag('em', array(), $invoice_from); - } - $view->addProperty(pht('Invoice From'), $invoice_from); - - $description = $merchant->getDescription(); - if (strlen($description)) { - $description = new PHUIRemarkupView($viewer, $description); - $view->addSectionHeader( - pht('Description'), - PHUIPropertyListView::ICON_SUMMARY); - $view->addTextContent($description); - } - - $contact_info = $merchant->getContactInfo(); - if (strlen($contact_info)) { - $contact_info = new PHUIRemarkupView($viewer, $contact_info); - $view->addSectionHeader( - pht('Contact Info'), - PHUIPropertyListView::ICON_SUMMARY); - $view->addTextContent($contact_info); - } - - $footer_info = $merchant->getInvoiceFooter(); - if (strlen($footer_info)) { - $footer_info = new PHUIRemarkupView($viewer, $footer_info); - $view->addSectionHeader( - pht('Invoice Footer'), - PHUIPropertyListView::ICON_SUMMARY); - $view->addTextContent($footer_info); - } - - return id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Details')) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->appendChild($view); - } - - private function buildCurtainView(PhortuneMerchant $merchant) { - $viewer = $this->getRequest()->getUser(); - $id = $merchant->getID(); - - $can_edit = PhabricatorPolicyFilter::hasCapability( - $viewer, - $merchant, - PhabricatorPolicyCapability::CAN_EDIT); - - $curtain = $this->newCurtainView($merchant); - - $curtain->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Edit Merchant')) - ->setIcon('fa-pencil') - ->setDisabled(!$can_edit) - ->setWorkflow(!$can_edit) - ->setHref($this->getApplicationURI("merchant/edit/{$id}/"))); - - $curtain->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Edit Logo')) - ->setIcon('fa-camera') - ->setDisabled(!$can_edit) - ->setWorkflow(!$can_edit) - ->setHref($this->getApplicationURI("merchant/picture/{$id}/"))); - - $curtain->addAction( - id(new PhabricatorActionView()) - ->setName(pht('New Invoice')) - ->setIcon('fa-fax') - ->setHref($this->getApplicationURI("merchant/{$id}/invoice/new/")) - ->setDisabled(!$can_edit) - ->setWorkflow(!$can_edit)); - - $member_phids = $merchant->getMemberPHIDs(); - $handles = $viewer->loadHandles($member_phids); - - $member_list = id(new PHUIObjectItemListView()) - ->setSimple(true); - - foreach ($member_phids as $member_phid) { - $image_uri = $handles[$member_phid]->getImageURI(); - $image_href = $handles[$member_phid]->getURI(); - $person = $handles[$member_phid]; - - $member = id(new PHUIObjectItemView()) - ->setImageURI($image_uri) - ->setHref($image_href) - ->setHeader($person->getFullName()); - - $member_list->addItem($member); - } - - $curtain->newPanel() - ->setHeaderText(pht('Managers')) - ->appendChild($member_list); - - return $curtain; - } - - private function buildProviderList( - PhortuneMerchant $merchant, - array $providers) { - - $viewer = $this->getRequest()->getUser(); - $id = $merchant->getID(); - - $can_edit = PhabricatorPolicyFilter::hasCapability( - $viewer, - $merchant, - PhabricatorPolicyCapability::CAN_EDIT); - - $provider_list = id(new PHUIObjectItemListView()) - ->setFlush(true) - ->setNoDataString(pht('This merchant has no payment providers.')); - - foreach ($providers as $provider_config) { - $provider = $provider_config->buildProvider(); - $provider_id = $provider_config->getID(); - - $item = id(new PHUIObjectItemView()) - ->setHeader($provider->getName()); - - if ($provider->isEnabled()) { - if ($provider->isAcceptingLivePayments()) { - $item->setStatusIcon('fa-check green'); - } else { - $item->setStatusIcon('fa-warning yellow'); - $item->addIcon('fa-exclamation-triangle', pht('Test Mode')); - } - - $item->addAttribute($provider->getConfigureProvidesDescription()); - } else { - // Don't show disabled providers to users who can't manage the merchant - // account. - if (!$can_edit) { - continue; - } - $item->setDisabled(true); - $item->addAttribute( - phutil_tag('em', array(), pht('This payment provider is disabled.'))); - } - - - if ($can_edit) { - $edit_uri = $this->getApplicationURI( - "/provider/edit/{$provider_id}/"); - $disable_uri = $this->getApplicationURI( - "/provider/disable/{$provider_id}/"); - - if ($provider->isEnabled()) { - $disable_icon = 'fa-times'; - $disable_name = pht('Disable'); - } else { - $disable_icon = 'fa-check'; - $disable_name = pht('Enable'); - } - - $item->addAction( - id(new PHUIListItemView()) - ->setIcon($disable_icon) - ->setHref($disable_uri) - ->setName($disable_name) - ->setWorkflow(true)); - - $item->addAction( - id(new PHUIListItemView()) - ->setIcon('fa-pencil') - ->setHref($edit_uri) - ->setName(pht('Edit'))); - } - - $provider_list->addItem($item); - } - - $add_action = id(new PHUIButtonView()) - ->setTag('a') - ->setHref($this->getApplicationURI('provider/edit/?merchantID='.$id)) - ->setText(pht('Add Payment Provider')) - ->setDisabled(!$can_edit) - ->setWorkflow(!$can_edit) - ->setIcon('fa-plus'); - - $header = id(new PHUIHeaderView()) - ->setHeader(pht('Payment Providers')) - ->addActionLink($add_action); - - return id(new PHUIObjectBoxView()) - ->setHeader($header) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->setObjectList($provider_list); - } - - -} diff --git a/src/applications/phortune/controller/payment/PhortunePaymentMethodCreateController.php b/src/applications/phortune/controller/payment/PhortunePaymentMethodCreateController.php deleted file mode 100644 index c068862631..0000000000 --- a/src/applications/phortune/controller/payment/PhortunePaymentMethodCreateController.php +++ /dev/null @@ -1,303 +0,0 @@ -getViewer(); - $account_id = $request->getURIData('accountID'); - - $account = id(new PhortuneAccountQuery()) - ->setViewer($viewer) - ->withIDs(array($account_id)) - ->executeOne(); - if (!$account) { - return new Aphront404Response(); - } - $account_id = $account->getID(); - - $merchant = id(new PhortuneMerchantQuery()) - ->setViewer($viewer) - ->withIDs(array($request->getInt('merchantID'))) - ->executeOne(); - if (!$merchant) { - return new Aphront404Response(); - } - - $cart_id = $request->getInt('cartID'); - $subscription_id = $request->getInt('subscriptionID'); - if ($cart_id) { - $cancel_uri = $this->getApplicationURI("cart/{$cart_id}/checkout/"); - } else if ($subscription_id) { - $cancel_uri = $this->getApplicationURI( - "{$account_id}/subscription/edit/{$subscription_id}/"); - } else { - $cancel_uri = $this->getApplicationURI($account->getID().'/'); - } - - $providers = $this->loadCreatePaymentMethodProvidersForMerchant($merchant); - if (!$providers) { - throw new Exception( - pht( - 'There are no payment providers enabled that can add payment '. - 'methods.')); - } - - if (count($providers) == 1) { - // If there's only one provider, always choose it. - $provider_id = head_key($providers); - } else { - $provider_id = $request->getInt('providerID'); - if (empty($providers[$provider_id])) { - $choices = array(); - foreach ($providers as $provider) { - $choices[] = $this->renderSelectProvider($provider); - } - - $content = phutil_tag( - 'div', - array( - 'class' => 'phortune-payment-method-list', - ), - $choices); - - return $this->newDialog() - ->setRenderDialogAsDiv(true) - ->setTitle(pht('Add Payment Method')) - ->appendParagraph(pht('Choose a payment method to add:')) - ->appendChild($content) - ->addCancelButton($cancel_uri); - } - } - - $provider = $providers[$provider_id]; - - $errors = array(); - $display_exception = null; - if ($request->isFormPost() && $request->getBool('isProviderForm')) { - $method = id(new PhortunePaymentMethod()) - ->setAccountPHID($account->getPHID()) - ->setAuthorPHID($viewer->getPHID()) - ->setMerchantPHID($merchant->getPHID()) - ->setProviderPHID($provider->getProviderConfig()->getPHID()) - ->setStatus(PhortunePaymentMethod::STATUS_ACTIVE); - - // Limit the rate at which you can attempt to add payment methods. This - // is intended as a line of defense against using Phortune to validate a - // large list of stolen credit card numbers. - - PhabricatorSystemActionEngine::willTakeAction( - array($viewer->getPHID()), - new PhortuneAddPaymentMethodAction(), - 1); - - if (!$errors) { - $errors = $this->processClientErrors( - $provider, - $request->getStr('errors')); - } - - if (!$errors) { - $client_token_raw = $request->getStr('token'); - $client_token = null; - try { - $client_token = phutil_json_decode($client_token_raw); - } catch (PhutilJSONParserException $ex) { - $errors[] = pht( - 'There was an error decoding token information submitted by the '. - 'client. Expected a JSON-encoded token dictionary, received: %s.', - nonempty($client_token_raw, pht('nothing'))); - } - - if (!$provider->validateCreatePaymentMethodToken($client_token)) { - $errors[] = pht( - 'There was an error with the payment token submitted by the '. - 'client. Expected a valid dictionary, received: %s.', - $client_token_raw); - } - - if (!$errors) { - try { - $provider->createPaymentMethodFromRequest( - $request, - $method, - $client_token); - } catch (PhortuneDisplayException $exception) { - $display_exception = $exception; - } catch (Exception $ex) { - $errors = array( - pht('There was an error adding this payment method:'), - $ex->getMessage(), - ); - } - } - } - - if (!$errors && !$display_exception) { - $method->save(); - - // If we added this method on a cart flow, return to the cart to - // check out. - if ($cart_id) { - $next_uri = $this->getApplicationURI( - "cart/{$cart_id}/checkout/?paymentMethodID=".$method->getID()); - } else if ($subscription_id) { - $next_uri = new PhutilURI($cancel_uri); - $next_uri->replaceQueryParam('added', true); - } else { - $account_uri = $this->getApplicationURI($account->getID().'/'); - $next_uri = new PhutilURI($account_uri); - $next_uri->setFragment('payment'); - } - - return id(new AphrontRedirectResponse())->setURI($next_uri); - } else { - if ($display_exception) { - $dialog_body = $display_exception->getView(); - } else { - $dialog_body = id(new PHUIInfoView()) - ->setErrors($errors); - } - - return $this->newDialog() - ->setTitle(pht('Error Adding Payment Method')) - ->appendChild($dialog_body) - ->addCancelButton($request->getRequestURI()); - } - } - - $form = $provider->renderCreatePaymentMethodForm($request, $errors); - - $form - ->setUser($viewer) - ->setAction($request->getRequestURI()) - ->setWorkflow(true) - ->addHiddenInput('providerID', $provider_id) - ->addHiddenInput('cartID', $request->getInt('cartID')) - ->addHiddenInput('subscriptionID', $request->getInt('subscriptionID')) - ->addHiddenInput('isProviderForm', true) - ->appendChild( - id(new AphrontFormSubmitControl()) - ->setValue(pht('Add Payment Method')) - ->addCancelButton($cancel_uri)); - - $box = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Method')) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->setForm($form); - - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('Add Payment Method')); - $crumbs->setBorder(true); - - $header = id(new PHUIHeaderView()) - ->setHeader(pht('Add Payment Method')) - ->setHeaderIcon('fa-plus-square'); - - $view = id(new PHUITwoColumnView()) - ->setHeader($header) - ->setFooter(array( - $box, - )); - - return $this->newPage() - ->setTitle($provider->getPaymentMethodDescription()) - ->setCrumbs($crumbs) - ->appendChild($view); - - } - - private function renderSelectProvider( - PhortunePaymentProvider $provider) { - - $request = $this->getRequest(); - $viewer = $request->getUser(); - - $description = $provider->getPaymentMethodDescription(); - $icon_uri = $provider->getPaymentMethodIcon(); - $details = $provider->getPaymentMethodProviderDescription(); - - $this->requireResource('phortune-css'); - - $icon = id(new PHUIIconView()) - ->setSpriteSheet(PHUIIconView::SPRITE_LOGIN) - ->setSpriteIcon($provider->getPaymentMethodIcon()); - - $button = id(new PHUIButtonView()) - ->setSize(PHUIButtonView::BIG) - ->setColor(PHUIButtonView::GREY) - ->setIcon($icon) - ->setText($description) - ->setSubtext($details) - ->setMetadata(array('disableWorkflow' => true)); - - $form = id(new AphrontFormView()) - ->setUser($viewer) - ->setAction($request->getRequestURI()) - ->addHiddenInput('providerID', $provider->getProviderConfig()->getID()) - ->appendChild($button); - - return $form; - } - - private function processClientErrors( - PhortunePaymentProvider $provider, - $client_errors_raw) { - - $errors = array(); - - $client_errors = null; - try { - $client_errors = phutil_json_decode($client_errors_raw); - } catch (PhutilJSONParserException $ex) { - $errors[] = pht( - 'There was an error decoding error information submitted by the '. - 'client. Expected a JSON-encoded list of error codes, received: %s.', - nonempty($client_errors_raw, pht('nothing'))); - } - - foreach (array_unique($client_errors) as $key => $client_error) { - $client_errors[$key] = $provider->translateCreatePaymentMethodErrorCode( - $client_error); - } - - foreach (array_unique($client_errors) as $client_error) { - switch ($client_error) { - case PhortuneErrCode::ERR_CC_INVALID_NUMBER: - $message = pht( - 'The card number you entered is not a valid card number. Check '. - 'that you entered it correctly.'); - break; - case PhortuneErrCode::ERR_CC_INVALID_CVC: - $message = pht( - 'The CVC code you entered is not a valid CVC code. Check that '. - 'you entered it correctly. The CVC code is a 3-digit or 4-digit '. - 'numeric code which usually appears on the back of the card.'); - break; - case PhortuneErrCode::ERR_CC_INVALID_EXPIRY: - $message = pht( - 'The card expiration date is not a valid expiration date. Check '. - 'that you entered it correctly. You can not add an expired card '. - 'as a payment method.'); - break; - default: - $message = $provider->getCreatePaymentMethodErrorMessage( - $client_error); - if (!$message) { - $message = pht( - "There was an unexpected error ('%s') processing payment ". - "information.", - $client_error); - - phlog($message); - } - break; - } - - $errors[$client_error] = $message; - } - - return $errors; - } - -} diff --git a/src/applications/phortune/controller/payment/PhortunePaymentMethodDisableController.php b/src/applications/phortune/controller/payment/PhortunePaymentMethodDisableController.php deleted file mode 100644 index f5feec8a29..0000000000 --- a/src/applications/phortune/controller/payment/PhortunePaymentMethodDisableController.php +++ /dev/null @@ -1,58 +0,0 @@ -getViewer(); - $method_id = $request->getURIData('id'); - - $method = id(new PhortunePaymentMethodQuery()) - ->setViewer($viewer) - ->withIDs(array($method_id)) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$method) { - return new Aphront404Response(); - } - - if ($method->getStatus() == PhortunePaymentMethod::STATUS_DISABLED) { - return new Aphront400Response(); - } - - $account = $method->getAccount(); - $account_id = $account->getID(); - $account_uri = $this->getApplicationURI("/account/billing/{$account_id}/"); - - if ($request->isFormPost()) { - - // TODO: ApplicationTransactions!!!! - $method - ->setStatus(PhortunePaymentMethod::STATUS_DISABLED) - ->save(); - - return id(new AphrontRedirectResponse())->setURI($account_uri); - } - - return $this->newDialog() - ->setTitle(pht('Remove Payment Method')) - ->appendParagraph( - pht( - 'Remove the payment method "%s" from your account?', - phutil_tag( - 'strong', - array(), - $method->getFullDisplayName()))) - ->appendParagraph( - pht( - 'You will no longer be able to make payments using this payment '. - 'method.')) - ->addCancelButton($account_uri) - ->addSubmitButton(pht('Remove Payment Method')); - } - -} diff --git a/src/applications/phortune/controller/paymentmethod/PhortunePaymentMethodCreateController.php b/src/applications/phortune/controller/paymentmethod/PhortunePaymentMethodCreateController.php new file mode 100644 index 0000000000..d32cb70f37 --- /dev/null +++ b/src/applications/phortune/controller/paymentmethod/PhortunePaymentMethodCreateController.php @@ -0,0 +1,462 @@ +getViewer(); + + $account_id = $request->getURIData('accountID'); + $account = id(new PhortuneAccountQuery()) + ->setViewer($viewer) + ->withIDs(array($account_id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$account) { + return new Aphront404Response(); + } + + $cart_id = $request->getInt('cartID'); + $subscription_id = $request->getInt('subscriptionID'); + $merchant_id = $request->getInt('merchantID'); + + if ($cart_id) { + $cart = id(new PhortuneCartQuery()) + ->setViewer($viewer) + ->withAccountPHIDs(array($account->getPHID())) + ->withIDs(array($cart_id)) + ->executeOne(); + if (!$cart) { + return new Aphront404Response(); + } + + $subscription_phid = $cart->getSubscriptionPHID(); + if ($subscription_phid) { + $subscription = id(new PhortuneSubscriptionQuery()) + ->setViewer($viewer) + ->withAccountPHIDs(array($account->getPHID())) + ->withPHIDs(array($subscription_phid)) + ->executeOne(); + if (!$subscription) { + return new Aphront404Response(); + } + } else { + $subscription = null; + } + + $merchant = $cart->getMerchant(); + + $cart_id = $cart->getID(); + $subscription_id = null; + $merchant_id = null; + + $next_uri = $cart->getCheckoutURI(); + } else if ($subscription_id) { + $subscription = id(new PhortuneSubscriptionQuery()) + ->setViewer($viewer) + ->withAccountPHIDs(array($account->getPHID())) + ->withIDs(array($subscription_id)) + ->executeOne(); + if (!$subscription) { + return new Aphront404Response(); + } + + $cart = null; + $merchant = $subscription->getMerchant(); + + $cart_id = null; + $subscription_id = $subscription->getID(); + $merchant_id = null; + + $next_uri = $subscription->getURI(); + } else if ($merchant_id) { + $merchant_phids = $account->getMerchantPHIDs(); + if ($merchant_phids) { + $merchant = id(new PhortuneMerchantQuery()) + ->setViewer($viewer) + ->withIDs(array($merchant_id)) + ->withPHIDs($merchant_phids) + ->executeOne(); + } else { + $merchant = null; + } + + if (!$merchant) { + return new Aphront404Response(); + } + + $cart = null; + $subscription = null; + + $cart_id = null; + $subscription_id = null; + $merchant_id = $merchant->getID(); + + $next_uri = $account->getPaymentMethodsURI(); + } else { + $next_uri = $account->getPaymentMethodsURI(); + + $merchant_phids = $account->getMerchantPHIDs(); + if ($merchant_phids) { + $merchants = id(new PhortuneMerchantQuery()) + ->setViewer($viewer) + ->withPHIDs($merchant_phids) + ->needProfileImage(true) + ->execute(); + } else { + $merchants = array(); + } + + if (!$merchants) { + return $this->newDialog() + ->setTitle(pht('No Merchants')) + ->appendParagraph( + pht( + 'You have not established a relationship with any merchants '. + 'yet. Create an order or subscription before adding payment '. + 'methods.')) + ->addCancelButton($next_uri); + } + + // If there's more than one merchant, ask the user to pick which one they + // want to pay. If there's only one, just pick it for them. + if (count($merchants) > 1) { + $menu = $this->newMerchantMenu($merchants); + + $form = id(new AphrontFormView()) + ->appendInstructions( + pht( + 'Choose the merchant you want to pay.')); + + return $this->newDialog() + ->setTitle(pht('Choose a Merchant')) + ->appendForm($form) + ->appendChild($menu) + ->addCancelButton($next_uri); + } + + $cart = null; + $subscription = null; + $merchant = head($merchants); + + $cart_id = null; + $subscription_id = null; + $merchant_id = $merchant->getID(); + } + + $providers = $this->loadCreatePaymentMethodProvidersForMerchant($merchant); + if (!$providers) { + throw new Exception( + pht( + 'There are no payment providers enabled that can add payment '. + 'methods.')); + } + + $state_params = array( + 'cartID' => $cart_id, + 'subscriptionID' => $subscription_id, + 'merchantID' => $merchant_id, + ); + $state_params = array_filter($state_params); + + $state_uri = new PhutilURI($request->getRequestURI()); + foreach ($state_params as $key => $value) { + $state_uri->replaceQueryParam($key, $value); + } + + $provider_id = $request->getInt('providerID'); + if (isset($providers[$provider_id])) { + $provider = $providers[$provider_id]; + } else { + // If there's more than one provider, ask the user to pick how they + // want to pay. If there's only one, just pick it. + if (count($providers) > 1) { + $menu = $this->newProviderMenu($providers, $state_uri); + + return $this->newDialog() + ->setTitle(pht('Choose a Payment Method')) + ->appendChild($menu) + ->addCancelButton($next_uri); + } + + $provider = head($providers); + } + + $provider_id = $provider->getProviderConfig()->getID(); + + $state_params['providerID'] = $provider_id; + + $errors = array(); + $display_exception = null; + if ($request->isFormPost() && $request->getBool('isProviderForm')) { + $method = id(new PhortunePaymentMethod()) + ->setAccountPHID($account->getPHID()) + ->setAuthorPHID($viewer->getPHID()) + ->setMerchantPHID($merchant->getPHID()) + ->setProviderPHID($provider->getProviderConfig()->getPHID()) + ->setStatus(PhortunePaymentMethod::STATUS_ACTIVE); + + // Limit the rate at which you can attempt to add payment methods. This + // is intended as a line of defense against using Phortune to validate a + // large list of stolen credit card numbers. + + PhabricatorSystemActionEngine::willTakeAction( + array($viewer->getPHID()), + new PhortuneAddPaymentMethodAction(), + 1); + + if (!$errors) { + $errors = $this->processClientErrors( + $provider, + $request->getStr('errors')); + } + + if (!$errors) { + $client_token_raw = $request->getStr('token'); + $client_token = null; + try { + $client_token = phutil_json_decode($client_token_raw); + } catch (PhutilJSONParserException $ex) { + $errors[] = pht( + 'There was an error decoding token information submitted by the '. + 'client. Expected a JSON-encoded token dictionary, received: %s.', + nonempty($client_token_raw, pht('nothing'))); + } + + if (!$provider->validateCreatePaymentMethodToken($client_token)) { + $errors[] = pht( + 'There was an error with the payment token submitted by the '. + 'client. Expected a valid dictionary, received: %s.', + $client_token_raw); + } + + if (!$errors) { + try { + $provider->createPaymentMethodFromRequest( + $request, + $method, + $client_token); + } catch (PhortuneDisplayException $exception) { + $display_exception = $exception; + } catch (Exception $ex) { + $errors = array( + pht('There was an error adding this payment method:'), + $ex->getMessage(), + ); + } + } + } + + if (!$errors && !$display_exception) { + $xactions = array(); + + $xactions[] = $method->getApplicationTransactionTemplate() + ->setTransactionType(PhabricatorTransactions::TYPE_CREATE) + ->setNewValue(true); + + $editor = id(new PhortunePaymentMethodEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true); + + $editor->applyTransactions($method, $xactions); + + $next_uri = new PhutilURI($next_uri); + + // If we added this method on a cart flow, return to the cart to + // checkout with this payment method selected. + if ($cart_id) { + $next_uri->replaceQueryParam('paymentMethodID', $method->getID()); + } + + return id(new AphrontRedirectResponse())->setURI($next_uri); + } else { + if ($display_exception) { + $dialog_body = $display_exception->getView(); + } else { + $dialog_body = id(new PHUIInfoView()) + ->setErrors($errors); + } + + return $this->newDialog() + ->setTitle(pht('Error Adding Payment Method')) + ->appendChild($dialog_body) + ->addCancelButton($request->getRequestURI()); + } + } + + $form = $provider->renderCreatePaymentMethodForm($request, $errors); + + $form + ->setViewer($viewer) + ->setAction($request->getPath()) + ->setWorkflow(true) + ->addHiddenInput('isProviderForm', true) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue(pht('Add Payment Method')) + ->addCancelButton($next_uri)); + + foreach ($state_params as $key => $value) { + $form->addHiddenInput($key, $value); + } + + $box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Method')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setForm($form); + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb(pht('Add Payment Method')) + ->setBorder(true); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Add Payment Method')) + ->setHeaderIcon('fa-plus-square'); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter( + array( + $box, + )); + + return $this->newPage() + ->setTitle($provider->getPaymentMethodDescription()) + ->setCrumbs($crumbs) + ->appendChild($view); + + } + + private function processClientErrors( + PhortunePaymentProvider $provider, + $client_errors_raw) { + + $errors = array(); + + $client_errors = null; + try { + $client_errors = phutil_json_decode($client_errors_raw); + } catch (PhutilJSONParserException $ex) { + $errors[] = pht( + 'There was an error decoding error information submitted by the '. + 'client. Expected a JSON-encoded list of error codes, received: %s.', + nonempty($client_errors_raw, pht('nothing'))); + } + + foreach (array_unique($client_errors) as $key => $client_error) { + $client_errors[$key] = $provider->translateCreatePaymentMethodErrorCode( + $client_error); + } + + foreach (array_unique($client_errors) as $client_error) { + switch ($client_error) { + case PhortuneErrCode::ERR_CC_INVALID_NUMBER: + $message = pht( + 'The card number you entered is not a valid card number. Check '. + 'that you entered it correctly.'); + break; + case PhortuneErrCode::ERR_CC_INVALID_CVC: + $message = pht( + 'The CVC code you entered is not a valid CVC code. Check that '. + 'you entered it correctly. The CVC code is a 3-digit or 4-digit '. + 'numeric code which usually appears on the back of the card.'); + break; + case PhortuneErrCode::ERR_CC_INVALID_EXPIRY: + $message = pht( + 'The card expiration date is not a valid expiration date. Check '. + 'that you entered it correctly. You can not add an expired card '. + 'as a payment method.'); + break; + default: + $message = $provider->getCreatePaymentMethodErrorMessage( + $client_error); + if (!$message) { + $message = pht( + "There was an unexpected error ('%s') processing payment ". + "information.", + $client_error); + + phlog($message); + } + break; + } + + $errors[$client_error] = $message; + } + + return $errors; + } + + private function newMerchantMenu(array $merchants) { + assert_instances_of($merchants, 'PhortuneMerchant'); + + $request = $this->getRequest(); + $viewer = $this->getViewer(); + + $menu = id(new PHUIObjectItemListView()) + ->setUser($viewer) + ->setBig(true) + ->setFlush(true); + + foreach ($merchants as $merchant) { + $merchant_uri = id(new PhutilURI($request->getRequestURI())) + ->replaceQueryParam('merchantID', $merchant->getID()); + + $item = id(new PHUIObjectItemView()) + ->setObjectName($merchant->getObjectName()) + ->setHeader($merchant->getName()) + ->setHref($merchant_uri) + ->setClickable(true) + ->setImageURI($merchant->getProfileImageURI()); + + $menu->addItem($item); + } + + return $menu; + } + + private function newProviderMenu(array $providers, PhutilURI $state_uri) { + assert_instances_of($providers, 'PhortunePaymentProvider'); + + $request = $this->getRequest(); + $viewer = $this->getViewer(); + + $menu = id(new PHUIObjectItemListView()) + ->setUser($viewer) + ->setBig(true) + ->setFlush(true); + + foreach ($providers as $provider) { + $provider_id = $provider->getProviderConfig()->getID(); + + $provider_uri = id(clone $state_uri) + ->replaceQueryParam('providerID', $provider_id); + + $description = $provider->getPaymentMethodDescription(); + $icon_uri = $provider->getPaymentMethodIcon(); + $details = $provider->getPaymentMethodProviderDescription(); + + $icon = id(new PHUIIconView()) + ->setSpriteSheet(PHUIIconView::SPRITE_LOGIN) + ->setSpriteIcon($icon_uri); + + $item = id(new PHUIObjectItemView()) + ->setHeader($description) + ->setHref($provider_uri) + ->setClickable(true) + ->addAttribute($details) + ->setImageIcon($icon); + + $menu->addItem($item); + } + + return $menu; + } + +} diff --git a/src/applications/phortune/controller/paymentmethod/PhortunePaymentMethodDisableController.php b/src/applications/phortune/controller/paymentmethod/PhortunePaymentMethodDisableController.php new file mode 100644 index 0000000000..146ee64a48 --- /dev/null +++ b/src/applications/phortune/controller/paymentmethod/PhortunePaymentMethodDisableController.php @@ -0,0 +1,96 @@ +getViewer(); + $method_id = $request->getURIData('id'); + + $method = id(new PhortunePaymentMethodQuery()) + ->setViewer($viewer) + ->withIDs(array($method_id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$method) { + return new Aphront404Response(); + } + + if ($method->getStatus() == PhortunePaymentMethod::STATUS_DISABLED) { + return new Aphront400Response(); + } + + $subscription_id = $request->getInt('subscriptionID'); + if ($subscription_id) { + $subscription = id(new PhortuneSubscriptionQuery()) + ->setViewer($viewer) + ->withIDs(array($subscription_id)) + ->withAccountPHIDs(array($method->getAccountPHID())) + ->withMerchantPHIDs(array($method->getMerchantPHID())) + ->executeOne(); + if (!$subscription) { + return new Aphront404Response(); + } + } else { + $subscription = null; + } + + $account = $method->getAccount(); + $account_id = $account->getID(); + $account_uri = $account->getPaymentMethodsURI(); + + if ($request->isFormPost()) { + $xactions = array(); + + $xactions[] = $method->getApplicationTransactionTemplate() + ->setTransactionType( + PhortunePaymentMethodStatusTransaction::TRANSACTIONTYPE) + ->setNewValue(PhortunePaymentMethod::STATUS_DISABLED); + + $editor = id(new PhortunePaymentMethodEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true); + + $editor->applyTransactions($method, $xactions); + + if ($subscription) { + $next_uri = $subscription->getURI(); + } else { + $next_uri = $account_uri; + } + + return id(new AphrontRedirectResponse())->setURI($next_uri); + } + + $method_phid = $method->getPHID(); + $handles = $viewer->loadHandles( + array( + $method_phid, + )); + + $method_handle = $handles[$method_phid]; + $method_display = $method_handle->renderLink(); + $method_display = phutil_tag('strong', array(), $method_display); + + return $this->newDialog() + ->setTitle(pht('Remove Payment Method')) + ->addHiddenInput('subscriptionID', $subscription_id) + ->appendParagraph( + pht( + 'Remove the payment method %s from your account?', + $method_display)) + ->appendParagraph( + pht( + 'You will no longer be able to make payments using this payment '. + 'method.')) + ->addCancelButton($account_uri) + ->addSubmitButton(pht('Remove Payment Method')); + } + +} diff --git a/src/applications/phortune/controller/payment/PhortunePaymentMethodEditController.php b/src/applications/phortune/controller/paymentmethod/PhortunePaymentMethodEditController.php similarity index 62% rename from src/applications/phortune/controller/payment/PhortunePaymentMethodEditController.php rename to src/applications/phortune/controller/paymentmethod/PhortunePaymentMethodEditController.php index dc23b81ad0..349f08d319 100644 --- a/src/applications/phortune/controller/payment/PhortunePaymentMethodEditController.php +++ b/src/applications/phortune/controller/paymentmethod/PhortunePaymentMethodEditController.php @@ -20,25 +20,36 @@ final class PhortunePaymentMethodEditController return new Aphront404Response(); } + $next_uri = $method->getURI(); + $account = $method->getAccount(); - $account_uri = $this->getApplicationURI($account->getID().'/'); + $v_name = $method->getName(); if ($request->isFormPost()) { + $v_name = $request->getStr('name'); - $name = $request->getStr('name'); + $xactions = array(); - // TODO: Use ApplicationTransactions + $xactions[] = $method->getApplicationTransactionTemplate() + ->setTransactionType( + PhortunePaymentMethodNameTransaction::TRANSACTIONTYPE) + ->setNewValue($v_name); - $method->setName($name); - $method->save(); + $editor = id(new PhortunePaymentMethodEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true); - return id(new AphrontRedirectResponse())->setURI($account_uri); + $editor->applyTransactions($method, $xactions); + + return id(new AphrontRedirectResponse())->setURI($next_uri); } $provider = $method->buildPaymentProvider(); $form = id(new AphrontFormView()) - ->setUser($viewer) + ->setViewer($viewer) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Name')) @@ -54,7 +65,7 @@ final class PhortunePaymentMethodEditController ->setValue($method->getDisplayExpires())) ->appendChild( id(new AphrontFormSubmitControl()) - ->addCancelButton($account_uri) + ->addCancelButton($next_uri) ->setValue(pht('Save Changes'))); $box = id(new PHUIObjectBoxView()) @@ -62,11 +73,12 @@ final class PhortunePaymentMethodEditController ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setForm($form); - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb($account->getName(), $account_uri); - $crumbs->addTextCrumb($method->getDisplayName()); - $crumbs->addTextCrumb(pht('Edit')); - $crumbs->setBorder(true); + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb($account->getName(), $account->getURI()) + ->addTextCrumb(pht('Payment Methods'), $account->getPaymentMethodsURI()) + ->addTextCrumb($method->getObjectName(), $method->getURI()) + ->addTextCrumb(pht('Edit')) + ->setBorder(true); $header = id(new PHUIHeaderView()) ->setHeader(pht('Edit Payment Method')) @@ -74,15 +86,15 @@ final class PhortunePaymentMethodEditController $view = id(new PHUITwoColumnView()) ->setHeader($header) - ->setFooter(array( - $box, - )); + ->setFooter( + array( + $box, + )); return $this->newPage() ->setTitle(pht('Edit Payment Method')) ->setCrumbs($crumbs) ->appendChild($view); - } } diff --git a/src/applications/phortune/controller/subscription/PhortuneSubscriptionEditController.php b/src/applications/phortune/controller/subscription/PhortuneSubscriptionEditController.php deleted file mode 100644 index 04367a88a0..0000000000 --- a/src/applications/phortune/controller/subscription/PhortuneSubscriptionEditController.php +++ /dev/null @@ -1,185 +0,0 @@ -getViewer(); - $added = $request->getBool('added'); - - $subscription = id(new PhortuneSubscriptionQuery()) - ->setViewer($viewer) - ->withIDs(array($request->getURIData('id'))) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$subscription) { - return new Aphront404Response(); - } - - id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( - $viewer, - $request, - $subscription->getURI()); - $merchant = $subscription->getMerchant(); - $account = $subscription->getAccount(); - - $title = pht('Subscription: %s', $subscription->getSubscriptionName()); - - $header = id(new PHUIHeaderView()) - ->setHeader($subscription->getSubscriptionName()); - - $view_uri = $subscription->getURI(); - - $valid_methods = id(new PhortunePaymentMethodQuery()) - ->setViewer($viewer) - ->withAccountPHIDs(array($account->getPHID())) - ->withStatuses( - array( - PhortunePaymentMethod::STATUS_ACTIVE, - )) - ->withMerchantPHIDs(array($merchant->getPHID())) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->execute(); - $valid_methods = mpull($valid_methods, null, 'getPHID'); - - $current_phid = $subscription->getDefaultPaymentMethodPHID(); - - $e_method = null; - if ($current_phid && empty($valid_methods[$current_phid])) { - $e_method = pht('Needs Update'); - } - - $errors = array(); - if ($request->isFormPost()) { - - $default_method_phid = $request->getStr('defaultPaymentMethodPHID'); - if (!$default_method_phid) { - $default_method_phid = null; - $e_method = null; - } else if (empty($valid_methods[$default_method_phid])) { - $e_method = pht('Invalid'); - if ($default_method_phid == $current_phid) { - $errors[] = pht( - 'This subscription is configured to autopay with a payment method '. - 'that has been deleted. Choose a valid payment method or disable '. - 'autopay.'); - } else { - $errors[] = pht('You must select a valid default payment method.'); - } - } - - // TODO: We should use transactions here, and move the validation logic - // inside the Editor. - - if (!$errors) { - $subscription->setDefaultPaymentMethodPHID($default_method_phid); - $subscription->save(); - - return id(new AphrontRedirectResponse()) - ->setURI($view_uri); - } - } - - // Add the option to disable autopay. - $disable_options = array( - '' => pht('(Disable Autopay)'), - ); - - // Don't require the user to make a valid selection if the current method - // has become invalid. - if ($current_phid && empty($valid_methods[$current_phid])) { - $current_options = array( - $current_phid => pht(''), - ); - } else { - $current_options = array(); - } - - // Add any available options. - $valid_options = mpull($valid_methods, 'getFullDisplayName', 'getPHID'); - - $options = $disable_options + $current_options + $valid_options; - - $crumbs = $this->buildApplicationCrumbs(); - $this->addAccountCrumb($crumbs, $account); - $crumbs->addTextCrumb( - pht('Subscription %d', $subscription->getID()), - $view_uri); - $crumbs->addTextCrumb(pht('Edit')); - $crumbs->setBorder(true); - - - $uri = $this->getApplicationURI($account->getID().'/card/new/'); - $uri = new PhutilURI($uri); - $uri->replaceQueryParam('merchantID', $merchant->getID()); - $uri->replaceQueryParam('subscriptionID', $subscription->getID()); - - $add_method_button = phutil_tag( - 'a', - array( - 'href' => $uri, - 'class' => 'button button-grey', - ), - pht('Add Payment Method...')); - - $radio = id(new AphrontFormRadioButtonControl()) - ->setName('defaultPaymentMethodPHID') - ->setLabel(pht('Autopay With')) - ->setValue($current_phid) - ->setError($e_method); - - foreach ($options as $key => $value) { - $radio->addButton($key, $value, null); - } - - $form = id(new AphrontFormView()) - ->setUser($viewer) - ->appendChild($radio) - ->appendChild( - id(new AphrontFormMarkupControl()) - ->setValue($add_method_button)) - ->appendChild( - id(new AphrontFormSubmitControl()) - ->setValue(pht('Save Changes')) - ->addCancelButton($view_uri)); - - $box = id(new PHUIObjectBoxView()) - ->setUser($viewer) - ->setHeaderText(pht('Subscription')) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->setFormErrors($errors) - ->appendChild($form); - - if ($added) { - $info_view = id(new PHUIInfoView()) - ->setSeverity(PHUIInfoView::SEVERITY_SUCCESS) - ->appendChild(pht('Payment method has been successfully added.')); - $box->setInfoView($info_view); - } - - $header = id(new PHUIHeaderView()) - ->setHeader(pht('Edit %s', $subscription->getSubscriptionName())) - ->setHeaderIcon('fa-pencil'); - - $view = id(new PHUITwoColumnView()) - ->setHeader($header) - ->setFooter(array( - $box, - )); - - return $this->newPage() - ->setTitle($title) - ->setCrumbs($crumbs) - ->appendChild($view); - - } - - -} diff --git a/src/applications/phortune/controller/subscription/PhortuneSubscriptionListController.php b/src/applications/phortune/controller/subscription/PhortuneSubscriptionListController.php deleted file mode 100644 index 469960f4bc..0000000000 --- a/src/applications/phortune/controller/subscription/PhortuneSubscriptionListController.php +++ /dev/null @@ -1,99 +0,0 @@ -getViewer(); - $querykey = $request->getURIData('queryKey'); - $merchant_id = $request->getURIData('merchantID'); - $account_id = $request->getURIData('accountID'); - - $engine = new PhortuneSubscriptionSearchEngine(); - - if ($merchant_id) { - $merchant = id(new PhortuneMerchantQuery()) - ->setViewer($viewer) - ->withIDs(array($merchant_id)) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$merchant) { - return new Aphront404Response(); - } - $this->merchant = $merchant; - $viewer->grantAuthority($merchant); - $engine->setMerchant($merchant); - } else if ($account_id) { - $account = id(new PhortuneAccountQuery()) - ->setViewer($viewer) - ->withIDs(array($account_id)) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$account) { - return new Aphront404Response(); - } - $this->account = $account; - $engine->setAccount($account); - } else { - return new Aphront404Response(); - } - - $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($querykey) - ->setSearchEngine($engine) - ->setNavigation($this->buildSideNavView()); - - return $this->delegateToController($controller); - } - - public function buildSideNavView() { - $viewer = $this->getViewer(); - - $nav = new AphrontSideNavFilterView(); - $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); - - id(new PhortuneSubscriptionSearchEngine()) - ->setViewer($viewer) - ->addNavigationItems($nav->getMenu()); - - $nav->selectFilter(null); - - return $nav; - } - - protected function buildApplicationCrumbs() { - $crumbs = parent::buildApplicationCrumbs(); - - $merchant = $this->merchant; - if ($merchant) { - $id = $merchant->getID(); - $this->addMerchantCrumb($crumbs, $merchant); - $crumbs->addTextCrumb( - pht('Subscriptions'), - $this->getApplicationURI("merchant/subscriptions/{$id}/")); - } - - $account = $this->account; - if ($account) { - $id = $account->getID(); - $this->addAccountCrumb($crumbs, $account); - $crumbs->addTextCrumb( - pht('Subscriptions'), - $this->getApplicationURI("{$id}/subscription/")); - } - - return $crumbs; - } - -} diff --git a/src/applications/phortune/controller/subscription/PhortuneSubscriptionViewController.php b/src/applications/phortune/controller/subscription/PhortuneSubscriptionViewController.php deleted file mode 100644 index 2e78d37d5c..0000000000 --- a/src/applications/phortune/controller/subscription/PhortuneSubscriptionViewController.php +++ /dev/null @@ -1,224 +0,0 @@ -getViewer(); - - $authority = $this->loadMerchantAuthority(); - - $subscription_query = id(new PhortuneSubscriptionQuery()) - ->setViewer($viewer) - ->withIDs(array($request->getURIData('id'))) - ->needTriggers(true); - - if ($authority) { - $subscription_query->withMerchantPHIDs(array($authority->getPHID())); - } - - $subscription = $subscription_query->executeOne(); - if (!$subscription) { - return new Aphront404Response(); - } - - $can_edit = PhabricatorPolicyFilter::hasCapability( - $viewer, - $subscription, - PhabricatorPolicyCapability::CAN_EDIT); - - $merchant = $subscription->getMerchant(); - $account = $subscription->getAccount(); - - $account_id = $account->getID(); - $subscription_id = $subscription->getID(); - - $title = $subscription->getSubscriptionFullName(); - - $header = id(new PHUIHeaderView()) - ->setHeader($title) - ->setHeaderIcon('fa-calendar-o'); - - $curtain = $this->newCurtainView($subscription); - $edit_uri = $subscription->getEditURI(); - - $curtain->addAction( - id(new PhabricatorActionView()) - ->setIcon('fa-credit-card') - ->setName(pht('Manage Autopay')) - ->setHref($edit_uri) - ->setDisabled(!$can_edit) - ->setWorkflow(!$can_edit)); - - $crumbs = $this->buildApplicationCrumbs(); - if ($authority) { - $this->addMerchantCrumb($crumbs, $merchant); - } else { - $this->addAccountCrumb($crumbs, $account); - } - $crumbs->addTextCrumb($subscription->getSubscriptionCrumbName()); - $crumbs->setBorder(true); - - $properties = id(new PHUIPropertyListView()) - ->setUser($viewer); - - $next_invoice = $subscription->getTrigger()->getNextEventPrediction(); - $properties->addProperty( - pht('Next Invoice'), - phabricator_datetime($next_invoice, $viewer)); - - $default_method = $subscription->getDefaultPaymentMethodPHID(); - if ($default_method) { - $method = id(new PhortunePaymentMethodQuery()) - ->setViewer($viewer) - ->withPHIDs(array($default_method)) - ->withStatuses( - array( - PhortunePaymentMethod::STATUS_ACTIVE, - )) - ->executeOne(); - if ($method) { - $handles = $this->loadViewerHandles(array($default_method)); - $autopay_method = $handles[$default_method]->renderLink(); - } else { - $autopay_method = phutil_tag( - 'em', - array(), - pht('')); - } - } else { - $autopay_method = phutil_tag( - 'em', - array(), - pht('No Autopay Method Configured')); - } - - $properties->addProperty( - pht('Autopay With'), - $autopay_method); - - $details = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Details')) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->addPropertyList($properties); - - $due_box = $this->buildDueInvoices($subscription, $authority); - $invoice_box = $this->buildPastInvoices($subscription, $authority); - - $view = id(new PHUITwoColumnView()) - ->setHeader($header) - ->setCurtain($curtain) - ->setMainColumn(array( - $details, - $due_box, - $invoice_box, - )); - - return $this->newPage() - ->setTitle($title) - ->setCrumbs($crumbs) - ->appendChild($view); - } - - private function buildDueInvoices( - PhortuneSubscription $subscription, - $authority) { - $viewer = $this->getViewer(); - - $invoices = id(new PhortuneCartQuery()) - ->setViewer($viewer) - ->withSubscriptionPHIDs(array($subscription->getPHID())) - ->needPurchases(true) - ->withInvoices(true) - ->execute(); - - $phids = array(); - foreach ($invoices as $invoice) { - $phids[] = $invoice->getPHID(); - $phids[] = $invoice->getMerchantPHID(); - foreach ($invoice->getPurchases() as $purchase) { - $phids[] = $purchase->getPHID(); - } - } - $handles = $this->loadViewerHandles($phids); - - $invoice_table = id(new PhortuneOrderTableView()) - ->setUser($viewer) - ->setCarts($invoices) - ->setIsInvoices(true) - ->setIsMerchantView((bool)$authority) - ->setHandles($handles); - - $invoice_header = id(new PHUIHeaderView()) - ->setHeader(pht('Invoices Due')); - - return id(new PHUIObjectBoxView()) - ->setHeader($invoice_header) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->appendChild($invoice_table); - } - - private function buildPastInvoices( - PhortuneSubscription $subscription, - $authority) { - $viewer = $this->getViewer(); - - $invoices = id(new PhortuneCartQuery()) - ->setViewer($viewer) - ->withSubscriptionPHIDs(array($subscription->getPHID())) - ->needPurchases(true) - ->withStatuses( - array( - PhortuneCart::STATUS_PURCHASING, - PhortuneCart::STATUS_CHARGED, - PhortuneCart::STATUS_HOLD, - PhortuneCart::STATUS_REVIEW, - PhortuneCart::STATUS_PURCHASED, - )) - ->setLimit(50) - ->execute(); - - $phids = array(); - foreach ($invoices as $invoice) { - $phids[] = $invoice->getPHID(); - foreach ($invoice->getPurchases() as $purchase) { - $phids[] = $purchase->getPHID(); - } - } - $handles = $this->loadViewerHandles($phids); - - $invoice_table = id(new PhortuneOrderTableView()) - ->setUser($viewer) - ->setCarts($invoices) - ->setHandles($handles); - - $account = $subscription->getAccount(); - $merchant = $subscription->getMerchant(); - - $account_id = $account->getID(); - $merchant_id = $merchant->getID(); - $subscription_id = $subscription->getID(); - - if ($authority) { - $invoices_uri = $this->getApplicationURI( - "merchant/{$merchant_id}/subscription/order/{$subscription_id}/"); - } else { - $invoices_uri = $this->getApplicationURI( - "{$account_id}/subscription/order/{$subscription_id}/"); - } - - $invoice_header = id(new PHUIHeaderView()) - ->setHeader(pht('Past Invoices')) - ->addActionLink( - id(new PHUIButtonView()) - ->setTag('a') - ->setIcon('fa-list') - ->setHref($invoices_uri) - ->setText(pht('View All Invoices'))); - - return id(new PHUIObjectBoxView()) - ->setHeader($invoice_header) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->appendChild($invoice_table); - } - -} diff --git a/src/applications/phortune/edge/PhortuneAccountHasMerchantEdgeType.php b/src/applications/phortune/edge/PhortuneAccountHasMerchantEdgeType.php new file mode 100644 index 0000000000..3da3da95dd --- /dev/null +++ b/src/applications/phortune/edge/PhortuneAccountHasMerchantEdgeType.php @@ -0,0 +1,11 @@ +getURI(); + if ($this->getIsCreate()) { + return $object->getURI(); + } else { + return $object->getDetailsURI(); + } } protected function buildCustomEditFields($object) { diff --git a/src/applications/phortune/editor/PhortuneAccountEmailEditEngine.php b/src/applications/phortune/editor/PhortuneAccountEmailEditEngine.php new file mode 100644 index 0000000000..c732e4215f --- /dev/null +++ b/src/applications/phortune/editor/PhortuneAccountEmailEditEngine.php @@ -0,0 +1,114 @@ +account = $account; + return $this; + } + + public function getAccount() { + return $this->account; + } + + public function getEngineName() { + return pht('Phortune Account Emails'); + } + + public function getEngineApplicationClass() { + return 'PhabricatorPhortuneApplication'; + } + + public function getSummaryHeader() { + return pht('Configure Phortune Account Email Forms'); + } + + public function getSummaryText() { + return pht( + 'Configure creation and editing forms for Phortune Account '. + 'Email Addresses.'); + } + + public function isEngineConfigurable() { + return false; + } + + protected function newEditableObject() { + $viewer = $this->getViewer(); + + $account = $this->getAccount(); + if (!$account) { + $account = new PhortuneAccount(); + } + + return PhortuneAccountEmail::initializeNewAddress( + $account, + $viewer->getPHID()); + } + + protected function newObjectQuery() { + return new PhortuneAccountEmailQuery(); + } + + protected function getObjectCreateTitleText($object) { + return pht('Add Email Address'); + } + + protected function getObjectEditTitleText($object) { + return pht('Edit Account Email: %s', $object->getAddress()); + } + + protected function getObjectEditShortText($object) { + return pht('%s', $object->getAddress()); + } + + protected function getObjectCreateShortText() { + return pht('Add Email Address'); + } + + protected function getObjectName() { + return pht('Account Email'); + } + + protected function getObjectCreateCancelURI($object) { + return $this->getAccount()->getEmailAddressesURI(); + } + + protected function getEditorURI() { + return $this->getApplication()->getApplicationURI('address/edit/'); + } + + protected function getObjectViewURI($object) { + return $object->getURI(); + } + + protected function buildCustomEditFields($object) { + $viewer = $this->getViewer(); + + if ($this->getIsCreate()) { + $address_field = id(new PhabricatorTextEditField()) + ->setTransactionType( + PhortuneAccountEmailAddressTransaction::TRANSACTIONTYPE) + ->setIsRequired(true); + } else { + $address_field = new PhabricatorStaticEditField(); + } + + $address_field + ->setKey('address') + ->setLabel(pht('Email Address')) + ->setDescription(pht('Email address.')) + ->setConduitTypeDescription(pht('New email address.')) + ->setValue($object->getAddress()); + + return array( + $address_field, + ); + } + +} diff --git a/src/applications/phortune/editor/PhortuneAccountEmailEditor.php b/src/applications/phortune/editor/PhortuneAccountEmailEditor.php new file mode 100644 index 0000000000..40d12a97ba --- /dev/null +++ b/src/applications/phortune/editor/PhortuneAccountEmailEditor.php @@ -0,0 +1,36 @@ +getAddress()), + null); + + throw new PhabricatorApplicationTransactionValidationException($errors); + } + +} diff --git a/src/applications/phortune/editor/PhortuneCartEditor.php b/src/applications/phortune/editor/PhortuneCartEditor.php index dcf1f2d0e0..aef59f059a 100644 --- a/src/applications/phortune/editor/PhortuneCartEditor.php +++ b/src/applications/phortune/editor/PhortuneCartEditor.php @@ -188,8 +188,8 @@ final class PhortuneCartEditor protected function getMailTo(PhabricatorLiskDAO $object) { $phids = array(); - // Reload the cart to pull merchant and account information, in case we - // just created the object. + // Reload the cart to pull account information, in case we just created the + // object. $cart = id(new PhortuneCartQuery()) ->setViewer($this->requireActor()) ->withPHIDs(array($object->getPHID())) @@ -199,10 +199,6 @@ final class PhortuneCartEditor $phids[] = $account_member; } - foreach ($cart->getMerchant()->getMemberPHIDs() as $merchant_member) { - $phids[] = $merchant_member; - } - return $phids; } @@ -239,4 +235,113 @@ final class PhortuneCartEditor return $this; } + protected function applyFinalEffects( + PhabricatorLiskDAO $object, + array $xactions) { + + $account = $object->getAccount(); + $merchant = $object->getMerchant(); + $account->writeMerchantEdge($merchant); + + return $xactions; + } + + protected function newAuxiliaryMail($object, array $xactions) { + $xviewer = PhabricatorUser::getOmnipotentUser(); + $account = $object->getAccount(); + + $addresses = id(new PhortuneAccountEmailQuery()) + ->setViewer($xviewer) + ->withAccountPHIDs(array($account->getPHID())) + ->withStatuses( + array( + PhortuneAccountEmailStatus::STATUS_ACTIVE, + )) + ->execute(); + + $messages = array(); + foreach ($addresses as $address) { + $message = $this->newExternalMail($address, $object, $xactions); + if ($message) { + $messages[] = $message; + } + } + + return $messages; + } + + private function newExternalMail( + PhortuneAccountEmail $email, + PhortuneCart $cart, + array $xactions) { + $xviewer = PhabricatorUser::getOmnipotentUser(); + $account = $cart->getAccount(); + + $id = $cart->getID(); + $name = $cart->getName(); + + $origin_user = id(new PhabricatorPeopleQuery()) + ->setViewer($xviewer) + ->withPHIDs(array($email->getAuthorPHID())) + ->executeOne(); + if (!$origin_user) { + return null; + } + + if ($this->isInvoice()) { + $subject = pht('[Invoice #%d] %s', $id, $name); + $order_header = pht('INVOICE DETAIL'); + } else { + $subject = pht('[Order #%d] %s', $id, $name); + $order_header = pht('ORDER DETAIL'); + } + + $body = id(new PhabricatorMetaMTAMailBody()) + ->setViewer($xviewer) + ->setContextObject($cart); + + $origin_username = $origin_user->getUsername(); + $origin_realname = $origin_user->getRealName(); + if (strlen($origin_realname)) { + $origin_display = pht('%s (%s)', $origin_username, $origin_realname); + } else { + $origin_display = pht('%s', $origin_username); + } + + $body->addRawSection( + pht( + 'This email address (%s) was added to a payment account (%s) '. + 'by %s.', + $email->getAddress(), + $account->getName(), + $origin_display)); + + $body->addLinkSection( + $order_header, + PhabricatorEnv::getProductionURI($email->getExternalOrderURI($cart))); + + $body->addLinkSection( + pht('FULL ORDER HISTORY'), + PhabricatorEnv::getProductionURI($email->getExternalURI())); + + $body->addLinkSection( + pht('UNSUBSCRIBE'), + PhabricatorEnv::getProductionURI($email->getUnsubscribeURI())); + + return id(new PhabricatorMetaMTAMail()) + ->setFrom($this->getActingAsPHID()) + ->setSubject($subject) + ->addRawTos( + array( + $email->getAddress(), + )) + ->setForceDelivery(true) + ->setIsBulk(true) + ->setSensitiveContent(true) + ->setBody($body->render()) + ->setHTMLBody($body->renderHTML()); + + } + + } diff --git a/src/applications/phortune/editor/PhortuneMerchantEditEngine.php b/src/applications/phortune/editor/PhortuneMerchantEditEngine.php index 04e07401c7..e09fba99ab 100644 --- a/src/applications/phortune/editor/PhortuneMerchantEditEngine.php +++ b/src/applications/phortune/editor/PhortuneMerchantEditEngine.php @@ -58,7 +58,7 @@ final class PhortuneMerchantEditEngine } protected function getObjectViewURI($object) { - return $object->getURI(); + return $object->getDetailsURI(); } public function isEngineConfigurable() { diff --git a/src/applications/phortune/editor/PhortuneMerchantEditor.php b/src/applications/phortune/editor/PhortuneMerchantEditor.php index 954570be3f..79fc7d534e 100644 --- a/src/applications/phortune/editor/PhortuneMerchantEditor.php +++ b/src/applications/phortune/editor/PhortuneMerchantEditor.php @@ -18,7 +18,6 @@ final class PhortuneMerchantEditor public function getTransactionTypes() { $types = parent::getTransactionTypes(); - $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; $types[] = PhabricatorTransactions::TYPE_EDGE; return $types; diff --git a/src/applications/phortune/editor/PhortunePaymentMethodEditor.php b/src/applications/phortune/editor/PhortunePaymentMethodEditor.php new file mode 100644 index 0000000000..4b6c8cedb3 --- /dev/null +++ b/src/applications/phortune/editor/PhortunePaymentMethodEditor.php @@ -0,0 +1,18 @@ +pagesObject = $this->newChildObject($pages_object); + return $this; + } + + public function getPagesObject() { + return $this->pagesObject; + } + + protected function writeObject() { + $this->writeLine('/Type /Catalog'); + + $pages_object = $this->getPagesObject(); + if ($pages_object) { + $this->writeLine('/Pages %d 0 R', $pages_object->getObjectIndex()); + } + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFContentsObject.php b/src/applications/phortune/pdf/PhabricatorPDFContentsObject.php new file mode 100644 index 0000000000..f49a32df33 --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFContentsObject.php @@ -0,0 +1,25 @@ +rawContent = $raw_content; + return $this; + } + + public function getRawContent() { + return $this->rawContent; + } + + protected function writeObject() { + $data = $this->getRawContent(); + + $stream_length = $this->newStream($data); + + $this->writeLine('/Filter /FlateDecode /Length %d', $stream_length); + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFFontObject.php b/src/applications/phortune/pdf/PhabricatorPDFFontObject.php new file mode 100644 index 0000000000..71f128d3a5 --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFFontObject.php @@ -0,0 +1,14 @@ +writeLine('/Type /Font'); + + $this->writeLine('/BaseFont /Helvetica-Bold'); + $this->writeLine('/Subtype /Type1'); + $this->writeLine('/Encoding /WinAnsiEncoding'); + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFFragment.php b/src/applications/phortune/pdf/PhabricatorPDFFragment.php new file mode 100644 index 0000000000..eb113b6140 --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFFragment.php @@ -0,0 +1,38 @@ +rope = new PhutilRope(); + + $this->writeFragment(); + + $rope = $this->rope; + $this->rope = null; + + return $rope->getAsString(); + } + + public function hasRefTableEntry() { + return false; + } + + abstract protected function writeFragment(); + + final protected function writeLine($pattern) { + $pattern = $pattern."\n"; + + $argv = func_get_args(); + $argv[0] = $pattern; + + $line = call_user_func_array('sprintf', $argv); + + $this->rope->append($line); + + return $this; + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFFragmentOffset.php b/src/applications/phortune/pdf/PhabricatorPDFFragmentOffset.php new file mode 100644 index 0000000000..c8b2769d7a --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFFragmentOffset.php @@ -0,0 +1,27 @@ +fragment = $fragment; + return $this; + } + + public function getFragment() { + return $this->fragment; + } + + public function setOffset($offset) { + $this->offset = $offset; + return $this; + } + + public function getOffset() { + return $this->offset; + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFGenerator.php b/src/applications/phortune/pdf/PhabricatorPDFGenerator.php new file mode 100644 index 0000000000..f2c5c92359 --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFGenerator.php @@ -0,0 +1,59 @@ +hasIterator) { + throw new Exception( + pht( + 'This generator has already emitted an iterator. You can not '. + 'modify the PDF document after you begin writing it.')); + } + + $this->objects[] = $object; + $index = count($this->objects); + + $object->setGenerator($this, $index); + + return $this; + } + + public function getObjects() { + return $this->objects; + } + + public function newIterator() { + $this->hasIterator = true; + return id(new PhabricatorPDFIterator()) + ->setGenerator($this); + } + + public function setInfoObject(PhabricatorPDFInfoObject $info_object) { + $this->addObject($info_object); + $this->infoObject = $info_object; + return $this; + } + + public function getInfoObject() { + return $this->infoObject; + } + + public function setCatalogObject( + PhabricatorPDFCatalogObject $catalog_object) { + $this->addObject($catalog_object); + $this->catalogObject = $catalog_object; + return $this; + } + + public function getCatalogObject() { + return $this->catalogObject; + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFHeadFragment.php b/src/applications/phortune/pdf/PhabricatorPDFHeadFragment.php new file mode 100644 index 0000000000..ef12bacce9 --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFHeadFragment.php @@ -0,0 +1,10 @@ +writeLine('%s', '%PDF-1.3'); + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFInfoObject.php b/src/applications/phortune/pdf/PhabricatorPDFInfoObject.php new file mode 100644 index 0000000000..2aba63c407 --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFInfoObject.php @@ -0,0 +1,11 @@ +writeLine('/Producer (Phabricator 20190801)'); + $this->writeLine('/CreationDate (D:%s)', date('YmdHis')); + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFIterator.php b/src/applications/phortune/pdf/PhabricatorPDFIterator.php new file mode 100644 index 0000000000..d39168369d --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFIterator.php @@ -0,0 +1,103 @@ +generator) { + throw new Exception( + pht( + 'This iterator already has a generator. You can not modify the '. + 'generator for a given iterator.')); + } + + $this->generator = $generator; + + return $this; + } + + public function getGenerator() { + if (!$this->generator) { + throw new Exception( + pht( + 'This PDF iterator has no associated PDF generator.')); + } + + return $this->generator; + } + + public function getFragmentOffsets() { + return $this->fragmentOffsets; + } + + public function current() { + return $this->fragmentBytes; + } + + public function key() { + return $this->framgentKey; + } + + public function next() { + $this->fragmentKey++; + + if (!$this->valid()) { + return; + } + + $fragment = $this->fragments[$this->fragmentKey]; + + $this->fragmentOffsets[] = id(new PhabricatorPDFFragmentOffset()) + ->setFragment($fragment) + ->setOffset($this->byteLength); + + $bytes = $fragment->getAsBytes(); + + $this->fragmentBytes = $bytes; + $this->byteLength += strlen($bytes); + } + + public function rewind() { + if ($this->hasRewound) { + throw new Exception( + pht( + 'PDF iterators may not be rewound. Create a new iterator to emit '. + 'another PDF.')); + } + + $generator = $this->getGenerator(); + $objects = $generator->getObjects(); + + $this->fragments = array(); + $this->fragments[] = new PhabricatorPDFHeadFragment(); + + foreach ($objects as $object) { + $this->fragments[] = $object; + } + + $this->fragments[] = id(new PhabricatorPDFTailFragment()) + ->setIterator($this); + + $this->hasRewound = true; + + $this->fragmentKey = -1; + $this->byteLength = 0; + + $this->next(); + } + + public function valid() { + return isset($this->fragments[$this->fragmentKey]); + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFObject.php b/src/applications/phortune/pdf/PhabricatorPDFObject.php new file mode 100644 index 0000000000..49c14d2f00 --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFObject.php @@ -0,0 +1,95 @@ +writeLine('%d 0 obj', $this->getObjectIndex()); + $this->writeLine('<<'); + $this->writeObject(); + $this->writeLine('>>'); + + $streams = $this->streams; + $this->streams = array(); + foreach ($streams as $stream) { + $this->writeLine('stream'); + $this->writeLine('%s', $stream); + $this->writeLine('endstream'); + } + + $this->writeLine('endobj'); + } + + final public function setGenerator( + PhabricatorPDFGenerator $generator, + $index) { + + if ($this->getGenerator()) { + throw new Exception( + pht( + 'This PDF object is already registered with a PDF generator. You '. + 'can not register an object with more than one generator.')); + } + + $this->generator = $generator; + $this->objectIndex = $index; + + foreach ($this->getChildren() as $child) { + $generator->addObject($child); + } + + return $this; + } + + final public function getGenerator() { + return $this->generator; + } + + final public function getObjectIndex() { + if (!$this->objectIndex) { + throw new Exception( + pht( + 'Trying to get index for object ("%s") which has not been '. + 'registered with a generator.', + get_class($this))); + } + + return $this->objectIndex; + } + + final protected function newChildObject(PhabricatorPDFObject $object) { + if ($this->generator) { + throw new Exception( + pht( + 'Trying to add a new PDF Object child after already registering '. + 'the object with a generator.')); + } + + $this->children[] = $object; + return $object; + } + + private function getChildren() { + return $this->children; + } + + abstract protected function writeObject(); + + final protected function newStream($raw_data) { + $stream_data = gzcompress($raw_data); + + $this->streams[] = $stream_data; + + return strlen($stream_data); + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFPageObject.php b/src/applications/phortune/pdf/PhabricatorPDFPageObject.php new file mode 100644 index 0000000000..3137d45d12 --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFPageObject.php @@ -0,0 +1,48 @@ +pagesObject = $pages; + return $this; + } + + public function setContentsObject(PhabricatorPDFContentsObject $contents) { + $this->contentsObject = $this->newChildObject($contents); + return $this; + } + + public function setResourcesObject(PhabricatorPDFResourcesObject $resources) { + $this->resourcesObject = $this->newChildObject($resources); + return $this; + } + + protected function writeObject() { + $this->writeLine('/Type /Page'); + + $pages_object = $this->pagesObject; + $contents_object = $this->contentsObject; + $resources_object = $this->resourcesObject; + + if ($pages_object) { + $pages_index = $pages_object->getObjectIndex(); + $this->writeLine('/Parent %d 0 R', $pages_index); + } + + if ($contents_object) { + $contents_index = $contents_object->getObjectIndex(); + $this->writeLine('/Contents %d 0 R', $contents_index); + } + + if ($resources_object) { + $resources_index = $resources_object->getObjectIndex(); + $this->writeLine('/Resources %d 0 R', $resources_index); + } + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFPagesObject.php b/src/applications/phortune/pdf/PhabricatorPDFPagesObject.php new file mode 100644 index 0000000000..4f0b89886e --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFPagesObject.php @@ -0,0 +1,38 @@ +setPagesObject($this); + $this->pageObjects[] = $this->newChildObject($page); + return $this; + } + + public function getPageObjects() { + return $this->pageObjects; + } + + protected function writeObject() { + $this->writeLine('/Type /Pages'); + + $page_objects = $this->getPageObjects(); + + $this->writeLine('/Count %d', count($page_objects)); + $this->writeLine('/MediaBox [%d %d %0.2f %0.2f]', 0, 0, 595.28, 841.89); + + if ($page_objects) { + $kids = array(); + foreach ($page_objects as $page_object) { + $kids[] = sprintf( + '%d 0 R', + $page_object->getObjectIndex()); + } + + $this->writeLine('/Kids [%s]', implode(' ', $kids)); + } + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFResourcesObject.php b/src/applications/phortune/pdf/PhabricatorPDFResourcesObject.php new file mode 100644 index 0000000000..9414708f6d --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFResourcesObject.php @@ -0,0 +1,28 @@ +fontObjects[] = $this->newChildObject($font); + return $this; + } + + public function getFontObjects() { + return $this->fontObjects; + } + + protected function writeObject() { + $this->writeLine('/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]'); + + $fonts = $this->getFontObjects(); + foreach ($fonts as $font) { + $this->writeLine('/Font <<'); + $this->writeLine('/F%d %d 0 R', 1, $font->getObjectIndex()); + $this->writeLine('>>'); + } + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFTailFragment.php b/src/applications/phortune/pdf/PhabricatorPDFTailFragment.php new file mode 100644 index 0000000000..2f606a1c8c --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFTailFragment.php @@ -0,0 +1,72 @@ +iterator = $iterator; + return $this; + } + + public function getIterator() { + return $this->iterator; + } + + protected function writeFragment() { + $iterator = $this->getIterator(); + $generator = $iterator->getGenerator(); + $objects = $generator->getObjects(); + + $xref_offset = null; + + $this->writeLine('xref'); + $this->writeLine('0 %d', count($objects) + 1); + $this->writeLine('%010d %05d f ', 0, 0xFFFF); + + $offset_map = array(); + + $fragment_offsets = $iterator->getFragmentOffsets(); + foreach ($fragment_offsets as $fragment_offset) { + $fragment = $fragment_offset->getFragment(); + $offset = $fragment_offset->getOffset(); + + if ($fragment === $this) { + $xref_offset = $offset; + } + + if (!$fragment->hasRefTableEntry()) { + continue; + } + + $offset_map[$fragment->getObjectIndex()] = $offset; + } + + ksort($offset_map); + + foreach ($offset_map as $offset) { + $this->writeLine('%010d %05d n ', $offset, 0); + } + + $this->writeLine('trailer'); + $this->writeLine('<<'); + $this->writeLine('/Size %d', count($objects) + 1); + + $info_object = $generator->getInfoObject(); + if ($info_object) { + $this->writeLine('/Info %d 0 R', $info_object->getObjectIndex()); + } + + $catalog_object = $generator->getCatalogObject(); + if ($catalog_object) { + $this->writeLine('/Root %d 0 R', $catalog_object->getObjectIndex()); + } + + $this->writeLine('>>'); + $this->writeLine('startxref'); + $this->writeLine('%d', $xref_offset); + $this->writeLine('%s', '%%EOF'); + } + +} diff --git a/src/applications/phortune/phid/PhortuneAccountEmailPHIDType.php b/src/applications/phortune/phid/PhortuneAccountEmailPHIDType.php new file mode 100644 index 0000000000..fccd50cf16 --- /dev/null +++ b/src/applications/phortune/phid/PhortuneAccountEmailPHIDType.php @@ -0,0 +1,41 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $email = $objects[$phid]; + + $id = $email->getID(); + + $handle->setName($email->getObjectName()); + } + } + +} diff --git a/src/applications/phortune/phid/PhortuneAccountPHIDType.php b/src/applications/phortune/phid/PhortuneAccountPHIDType.php index cf5f5d06f2..90632a980d 100644 --- a/src/applications/phortune/phid/PhortuneAccountPHIDType.php +++ b/src/applications/phortune/phid/PhortuneAccountPHIDType.php @@ -32,10 +32,9 @@ final class PhortuneAccountPHIDType extends PhabricatorPHIDType { foreach ($handles as $phid => $handle) { $account = $objects[$phid]; - $id = $account->getID(); - - $handle->setName($account->getName()); - $handle->setURI("/phortune/{$id}/"); + $handle + ->setName($account->getName()) + ->setURI($account->getURI()); } } diff --git a/src/applications/phortune/phid/PhortuneMerchantPHIDType.php b/src/applications/phortune/phid/PhortuneMerchantPHIDType.php index 941f704ab8..69b93582b3 100644 --- a/src/applications/phortune/phid/PhortuneMerchantPHIDType.php +++ b/src/applications/phortune/phid/PhortuneMerchantPHIDType.php @@ -21,7 +21,8 @@ final class PhortuneMerchantPHIDType extends PhabricatorPHIDType { array $phids) { return id(new PhortuneMerchantQuery()) - ->withPHIDs($phids); + ->withPHIDs($phids) + ->needProfileImage(true); } public function loadHandles( @@ -32,10 +33,10 @@ final class PhortuneMerchantPHIDType extends PhabricatorPHIDType { foreach ($handles as $phid => $handle) { $merchant = $objects[$phid]; - $id = $merchant->getID(); - - $handle->setName($merchant->getName()); - $handle->setURI("/phortune/merchant/{$id}/"); + $handle + ->setName($merchant->getName()) + ->setURI($merchant->getURI()) + ->setImageURI($merchant->getProfileImageURI()); } } diff --git a/src/applications/phortune/phid/PhortunePaymentMethodPHIDType.php b/src/applications/phortune/phid/PhortunePaymentMethodPHIDType.php index 7906f87414..7de421a7b4 100644 --- a/src/applications/phortune/phid/PhortunePaymentMethodPHIDType.php +++ b/src/applications/phortune/phid/PhortunePaymentMethodPHIDType.php @@ -32,9 +32,9 @@ final class PhortunePaymentMethodPHIDType extends PhabricatorPHIDType { foreach ($handles as $phid => $handle) { $method = $objects[$phid]; - $id = $method->getID(); - - $handle->setName($method->getFullDisplayName()); + $handle + ->setName($method->getFullDisplayName()) + ->setURI($method->getURI()); } } diff --git a/src/applications/phortune/phid/PhortuneSubscriptionPHIDType.php b/src/applications/phortune/phid/PhortuneSubscriptionPHIDType.php index 6d7275c62b..e07dce12f4 100644 --- a/src/applications/phortune/phid/PhortuneSubscriptionPHIDType.php +++ b/src/applications/phortune/phid/PhortuneSubscriptionPHIDType.php @@ -32,11 +32,9 @@ final class PhortuneSubscriptionPHIDType extends PhabricatorPHIDType { foreach ($handles as $phid => $handle) { $subscription = $objects[$phid]; - $id = $subscription->getID(); - - $handle->setName($subscription->getSubscriptionName()); - $handle->setURI($subscription->getURI()); - + $handle + ->setName($subscription->getSubscriptionName()) + ->setURI($subscription->getURI()); } } diff --git a/src/applications/phortune/provider/PhortuneTestPaymentProvider.php b/src/applications/phortune/provider/PhortuneTestPaymentProvider.php index 02d57e3b6a..8b8c731a84 100644 --- a/src/applications/phortune/provider/PhortuneTestPaymentProvider.php +++ b/src/applications/phortune/provider/PhortuneTestPaymentProvider.php @@ -26,7 +26,7 @@ final class PhortuneTestPaymentProvider extends PhortunePaymentProvider { } public function getConfigureInstructions() { - return pht('This providers does not require any special configuration.'); + return pht('This provider does not require any special configuration.'); } public function canRunConfigurationTest() { diff --git a/src/applications/phortune/query/PhortuneAccountEmailQuery.php b/src/applications/phortune/query/PhortuneAccountEmailQuery.php new file mode 100644 index 0000000000..0bdfdb78dc --- /dev/null +++ b/src/applications/phortune/query/PhortuneAccountEmailQuery.php @@ -0,0 +1,117 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withAccountPHIDs(array $phids) { + $this->accountPHIDs = $phids; + return $this; + } + + public function withAddressKeys(array $keys) { + $this->addressKeys = $keys; + return $this; + } + + public function withStatuses(array $statuses) { + $this->statuses = $statuses; + return $this; + } + + public function newResultObject() { + return new PhortuneAccountEmail(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function willFilterPage(array $addresses) { + $accounts = id(new PhortuneAccountQuery()) + ->setViewer($this->getViewer()) + ->setParentQuery($this) + ->withPHIDs(mpull($addresses, 'getAccountPHID')) + ->execute(); + $accounts = mpull($accounts, null, 'getPHID'); + + foreach ($addresses as $key => $address) { + $account = idx($accounts, $address->getAccountPHID()); + + if (!$account) { + $this->didRejectResult($addresses[$key]); + unset($addresses[$key]); + continue; + } + + $address->attachAccount($account); + } + + return $addresses; + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'address.id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'address.phid IN (%Ls)', + $this->phids); + } + + if ($this->accountPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'address.accountPHID IN (%Ls)', + $this->accountPHIDs); + } + + if ($this->addressKeys !== null) { + $where[] = qsprintf( + $conn, + 'address.addressKey IN (%Ls)', + $this->addressKeys); + } + + if ($this->statuses !== null) { + $where[] = qsprintf( + $conn, + 'address.status IN (%Ls)', + $this->statuses); + } + + return $where; + } + + public function getQueryApplicationClass() { + return 'PhabricatorPhortuneApplication'; + } + + protected function getPrimaryTableAlias() { + return 'address'; + } + +} diff --git a/src/applications/phortune/query/PhortuneAccountEmailTransactionQuery.php b/src/applications/phortune/query/PhortuneAccountEmailTransactionQuery.php new file mode 100644 index 0000000000..2aa9d8418e --- /dev/null +++ b/src/applications/phortune/query/PhortuneAccountEmailTransactionQuery.php @@ -0,0 +1,10 @@ +withSourcePHIDs(mpull($accounts, 'getPHID')) - ->withEdgeTypes(array(PhortuneAccountHasMemberEdgeType::EDGECONST)); + ->withEdgeTypes( + array( + PhortuneAccountHasMemberEdgeType::EDGECONST, + PhortuneAccountHasMerchantEdgeType::EDGECONST, + )); + $query->execute(); foreach ($accounts as $account) { - $member_phids = $query->getDestinationPHIDs(array($account->getPHID())); + $member_phids = $query->getDestinationPHIDs( + array( + $account->getPHID(), + ), + array( + PhortuneAccountHasMemberEdgeType::EDGECONST, + )); $member_phids = array_reverse($member_phids); $account->attachMemberPHIDs($member_phids); + + $merchant_phids = $query->getDestinationPHIDs( + array( + $account->getPHID(), + ), + array( + PhortuneAccountHasMerchantEdgeType::EDGECONST, + )); + $merchant_phids = array_reverse($merchant_phids); + $account->attachMerchantPHIDs($merchant_phids); } return $accounts; diff --git a/src/applications/phortune/query/PhortuneCartSearchEngine.php b/src/applications/phortune/query/PhortuneCartSearchEngine.php index 193764c2cd..a4a0f2848d 100644 --- a/src/applications/phortune/query/PhortuneCartSearchEngine.php +++ b/src/applications/phortune/query/PhortuneCartSearchEngine.php @@ -62,26 +62,8 @@ final class PhortuneCartSearchEngine $merchant = $this->getMerchant(); $account = $this->getAccount(); if ($merchant) { - $can_edit = PhabricatorPolicyFilter::hasCapability( - $viewer, - $merchant, - PhabricatorPolicyCapability::CAN_EDIT); - if (!$can_edit) { - throw new Exception( - pht('You can not query orders for a merchant you do not control.')); - } $query->withMerchantPHIDs(array($merchant->getPHID())); } else if ($account) { - $can_edit = PhabricatorPolicyFilter::hasCapability( - $viewer, - $account, - PhabricatorPolicyCapability::CAN_EDIT); - if (!$can_edit) { - throw new Exception( - pht( - 'You can not query orders for an account you are not '. - 'a member of.')); - } $query->withAccountPHIDs(array($account->getPHID())); } else { $accounts = id(new PhortuneAccountQuery()) @@ -123,9 +105,9 @@ final class PhortuneCartSearchEngine $merchant = $this->getMerchant(); $account = $this->getAccount(); if ($merchant) { - return '/phortune/merchant/orders/'.$merchant->getID().'/'.$path; + return $merchant->getOrderListURI($path); } else if ($account) { - return '/phortune/'.$account->getID().'/order/'.$path; + return $account->getOrderListURI($path); } else { return '/phortune/order/'.$path; } diff --git a/src/applications/phortune/query/PhortuneChargeSearchEngine.php b/src/applications/phortune/query/PhortuneChargeSearchEngine.php index 0d6c2cfd59..e1fb5a47ba 100644 --- a/src/applications/phortune/query/PhortuneChargeSearchEngine.php +++ b/src/applications/phortune/query/PhortuneChargeSearchEngine.php @@ -40,16 +40,6 @@ final class PhortuneChargeSearchEngine $account = $this->getAccount(); if ($account) { - $can_edit = PhabricatorPolicyFilter::hasCapability( - $viewer, - $account, - PhabricatorPolicyCapability::CAN_EDIT); - if (!$can_edit) { - throw new Exception( - pht( - 'You can not query charges for an account you are not '. - 'a member of.')); - } $query->withAccountPHIDs(array($account->getPHID())); } else { $accounts = id(new PhortuneAccountQuery()) @@ -72,7 +62,7 @@ final class PhortuneChargeSearchEngine protected function getURI($path) { $account = $this->getAccount(); if ($account) { - return '/phortune/'.$account->getID().'/charge/'; + return $account->getChargeListURI($path); } else { return '/phortune/charge/'.$path; } @@ -99,20 +89,6 @@ final class PhortuneChargeSearchEngine return parent::buildSavedQueryFromBuiltin($query_key); } - protected function getRequiredHandlePHIDsForResultList( - array $charges, - PhabricatorSavedQuery $query) { - - $phids = array(); - foreach ($charges as $charge) { - $phids[] = $charge->getProviderPHID(); - $phids[] = $charge->getCartPHID(); - $phids[] = $charge->getMerchantPHID(); - $phids[] = $charge->getPaymentMethodPHID(); - } - - return $phids; - } protected function renderResultList( array $charges, @@ -124,8 +100,7 @@ final class PhortuneChargeSearchEngine $table = id(new PhortuneChargeTableView()) ->setUser($viewer) - ->setCharges($charges) - ->setHandles($handles); + ->setCharges($charges); $result = new PhabricatorApplicationSearchResultView(); $result->setTable($table); diff --git a/src/applications/phortune/query/PhortuneMerchantQuery.php b/src/applications/phortune/query/PhortuneMerchantQuery.php index b6cab7dbc2..aef7d8aaf1 100644 --- a/src/applications/phortune/query/PhortuneMerchantQuery.php +++ b/src/applications/phortune/query/PhortuneMerchantQuery.php @@ -86,14 +86,14 @@ final class PhortuneMerchantQuery if ($this->ids !== null) { $where[] = qsprintf( $conn, - 'id IN (%Ld)', + 'merchant.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, - 'phid IN (%Ls)', + 'merchant.phid IN (%Ls)', $this->phids); } @@ -113,7 +113,7 @@ final class PhortuneMerchantQuery if ($this->memberPHIDs !== null) { $joins[] = qsprintf( $conn, - 'LEFT JOIN %T e ON m.phid = e.src AND e.type = %d', + 'LEFT JOIN %T e ON merchant.phid = e.src AND e.type = %d', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhortuneMerchantHasMemberEdgeType::EDGECONST); } @@ -126,7 +126,68 @@ final class PhortuneMerchantQuery } protected function getPrimaryTableAlias() { - return 'm'; + return 'merchant'; + } + + public static function canViewersEditMerchants( + array $viewer_phids, + array $merchant_phids) { + + // See T13366 for some discussion. This is an unusual caching construct to + // make policy filtering of Accounts easier. + + foreach ($viewer_phids as $key => $viewer_phid) { + if (!$viewer_phid) { + unset($viewer_phids[$key]); + } + } + + if (!$viewer_phids) { + return array(); + } + + $cache_key = 'phortune.merchant.can-edit'; + $cache = PhabricatorCaches::getRequestCache(); + + $cache_data = $cache->getKey($cache_key); + if (!$cache_data) { + $cache_data = array(); + } + + $load_phids = array(); + foreach ($viewer_phids as $viewer_phid) { + if (!isset($cache_data[$viewer_phid])) { + $load_phids[] = $viewer_phid; + } + } + + $did_write = false; + foreach ($load_phids as $load_phid) { + $merchants = id(new self()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withMemberPHIDs(array($load_phid)) + ->execute(); + foreach ($merchants as $merchant) { + $cache_data[$load_phid][$merchant->getPHID()] = true; + $did_write = true; + } + } + + if ($did_write) { + $cache->setKey($cache_key, $cache_data); + } + + $results = array(); + foreach ($viewer_phids as $viewer_phid) { + foreach ($merchant_phids as $merchant_phid) { + if (!isset($cache_data[$viewer_phid][$merchant_phid])) { + continue; + } + $results[$viewer_phid][$merchant_phid] = true; + } + } + + return $results; } } diff --git a/src/applications/phortune/query/PhortunePaymentMethodQuery.php b/src/applications/phortune/query/PhortunePaymentMethodQuery.php index 42d54805e6..013fa147ec 100644 --- a/src/applications/phortune/query/PhortunePaymentMethodQuery.php +++ b/src/applications/phortune/query/PhortunePaymentMethodQuery.php @@ -53,6 +53,7 @@ final class PhortunePaymentMethodQuery $account = idx($accounts, $method->getAccountPHID()); if (!$account) { unset($methods[$key]); + $this->didRejectResult($method); continue; } $method->attachAccount($account); @@ -72,6 +73,7 @@ final class PhortunePaymentMethodQuery $merchant = idx($merchants, $method->getMerchantPHID()); if (!$merchant) { unset($methods[$key]); + $this->didRejectResult($method); continue; } $method->attachMerchant($merchant); @@ -91,6 +93,7 @@ final class PhortunePaymentMethodQuery $provider_config = idx($provider_configs, $method->getProviderPHID()); if (!$provider_config) { unset($methods[$key]); + $this->didRejectResult($method); continue; } $method->attachProviderConfig($provider_config); diff --git a/src/applications/phortune/query/PhortunePaymentMethodTransactionQuery.php b/src/applications/phortune/query/PhortunePaymentMethodTransactionQuery.php new file mode 100644 index 0000000000..2067e1e360 --- /dev/null +++ b/src/applications/phortune/query/PhortunePaymentMethodTransactionQuery.php @@ -0,0 +1,10 @@ +paymentMethodPHIDs = $method_phids; + return $this; + } + public function needTriggers($need_triggers) { $this->needTriggers = $need_triggers; return $this; } + public function newResultObject() { + return new PhortuneSubscription(); + } + protected function loadPage() { - $table = new PhortuneSubscription(); - $conn = $table->establishConnection('r'); - - $rows = queryfx_all( - $conn, - 'SELECT subscription.* FROM %T subscription %Q %Q %Q', - $table->getTableName(), - $this->buildWhereClause($conn), - $this->buildOrderClause($conn), - $this->buildLimitClause($conn)); - - return $table->loadAllFromArray($rows); + return $this->loadStandardPage($this->newResultObject()); } protected function willFilterPage(array $subscriptions) { @@ -67,6 +66,7 @@ final class PhortuneSubscriptionQuery $account = idx($accounts, $subscription->getAccountPHID()); if (!$account) { unset($subscriptions[$key]); + $this->didRejectResult($subscription); continue; } $subscription->attachAccount($account); @@ -86,6 +86,7 @@ final class PhortuneSubscriptionQuery $merchant = idx($merchants, $subscription->getMerchantPHID()); if (!$merchant) { unset($subscriptions[$key]); + $this->didRejectResult($subscription); continue; } $subscription->attachMerchant($merchant); @@ -112,6 +113,7 @@ final class PhortuneSubscriptionQuery $implementation = idx($implementations, $ref); if (!$implementation) { unset($subscriptions[$key]); + $this->didRejectResult($subscription); continue; } $subscription->attachImplementation($implementation); @@ -133,6 +135,7 @@ final class PhortuneSubscriptionQuery $trigger = idx($triggers, $subscription->getTriggerPHID()); if (!$trigger) { unset($subscriptions[$key]); + $this->didRejectResult($subscription); continue; } $subscription->attachTrigger($trigger); @@ -142,10 +145,8 @@ final class PhortuneSubscriptionQuery return $subscriptions; } - protected function buildWhereClause(AphrontDatabaseConnection $conn) { - $where = array(); - - $where[] = $this->buildPagingClause($conn); + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( @@ -182,7 +183,18 @@ final class PhortuneSubscriptionQuery $this->statuses); } - return $this->formatWhereClause($conn, $where); + if ($this->paymentMethodPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'subscription.defaultPaymentMethodPHID IN (%Ls)', + $this->paymentMethodPHIDs); + } + + return $where; + } + + protected function getPrimaryTableAlias() { + return 'subscription'; } public function getQueryApplicationClass() { diff --git a/src/applications/phortune/query/PhortuneSubscriptionSearchEngine.php b/src/applications/phortune/query/PhortuneSubscriptionSearchEngine.php index 62d4f79a25..0d2e720aa7 100644 --- a/src/applications/phortune/query/PhortuneSubscriptionSearchEngine.php +++ b/src/applications/phortune/query/PhortuneSubscriptionSearchEngine.php @@ -96,9 +96,9 @@ final class PhortuneSubscriptionSearchEngine $merchant = $this->getMerchant(); $account = $this->getAccount(); if ($merchant) { - return '/phortune/merchant/'.$merchant->getID().'/subscription/'.$path; + return $merchant->getSubscriptionListURI($path); } else if ($account) { - return '/phortune/'.$account->getID().'/subscription/'; + return $account->getSubscriptionListURI($path); } else { return '/phortune/subscription/'.$path; } @@ -125,18 +125,6 @@ final class PhortuneSubscriptionSearchEngine return parent::buildSavedQueryFromBuiltin($query_key); } - protected function getRequiredHandlePHIDsForResultList( - array $subscriptions, - PhabricatorSavedQuery $query) { - $phids = array(); - foreach ($subscriptions as $subscription) { - $phids[] = $subscription->getPHID(); - $phids[] = $subscription->getMerchantPHID(); - $phids[] = $subscription->getAuthorPHID(); - } - return $phids; - } - protected function renderResultList( array $subscriptions, PhabricatorSavedQuery $query, @@ -147,7 +135,6 @@ final class PhortuneSubscriptionSearchEngine $table = id(new PhortuneSubscriptionTableView()) ->setUser($viewer) - ->setHandles($handles) ->setSubscriptions($subscriptions); $merchant = $this->getMerchant(); diff --git a/src/applications/phortune/query/PhortuneSubscriptionTransactionQuery.php b/src/applications/phortune/query/PhortuneSubscriptionTransactionQuery.php new file mode 100644 index 0000000000..db97925b39 --- /dev/null +++ b/src/applications/phortune/query/PhortuneSubscriptionTransactionQuery.php @@ -0,0 +1,10 @@ +setBillingName('') ->setBillingAddress('') + ->attachMerchantPHIDs(array()) ->attachMemberPHIDs(array()); } @@ -100,9 +102,92 @@ final class PhortuneAccount extends PhortuneDAO } public function getURI() { - return '/phortune/'.$this->getID().'/'; + return urisprintf( + '/phortune/account/%d/', + $this->getID()); } + public function getDetailsURI() { + return urisprintf( + '/phortune/account/%d/details/', + $this->getID()); + } + + public function getOrdersURI() { + return urisprintf( + '/phortune/account/%d/orders/', + $this->getID()); + } + + public function getOrderListURI($path = '') { + return urisprintf( + '/phortune/account/%d/orders/list/%s', + $this->getID(), + $path); + } + + public function getSubscriptionsURI() { + return urisprintf( + '/phortune/account/%d/subscriptions/', + $this->getID()); + } + + public function getEmailAddressesURI() { + return urisprintf( + '/phortune/account/%d/addresses/', + $this->getID()); + } + + public function getPaymentMethodsURI() { + return urisprintf( + '/phortune/account/%d/methods/', + $this->getID()); + } + + public function getChargesURI() { + return urisprintf( + '/phortune/account/%d/charges/', + $this->getID()); + } + + public function getChargeListURI($path = '') { + return urisprintf( + '/phortune/account/%d/charges/list/%s', + $this->getID(), + $path); + } + + public function attachMerchantPHIDs(array $merchant_phids) { + $this->merchantPHIDs = $merchant_phids; + return $this; + } + + public function getMerchantPHIDs() { + return $this->assertAttached($this->merchantPHIDs); + } + + public function writeMerchantEdge(PhortuneMerchant $merchant) { + $edge_src = $this->getPHID(); + $edge_type = PhortuneAccountHasMerchantEdgeType::EDGECONST; + $edge_dst = $merchant->getPHID(); + + id(new PhabricatorEdgeEditor()) + ->addEdge($edge_src, $edge_type, $edge_dst) + ->save(); + + return $this; + } + + public function isUserAccountMember(PhabricatorUser $user) { + $user_phid = $user->getPHID(); + if (!$user_phid) { + return null; + } + + $member_map = array_fuse($this->getMemberPHIDs()); + + return isset($member_map[$user_phid]); + } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ @@ -140,18 +225,22 @@ final class PhortuneAccount extends PhortuneDAO } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { - $members = array_fuse($this->getMemberPHIDs()); - if (isset($members[$viewer->getPHID()])) { + if ($this->isUserAccountMember($viewer)) { return true; } - // If the viewer is acting on behalf of a merchant, they can see - // payment accounts. + // See T13366. If the viewer can edit any merchant that this payment + // account has a relationship with, they can see the payment account. if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { - foreach ($viewer->getAuthorities() as $authority) { - if ($authority instanceof PhortuneMerchant) { - return true; - } + $viewer_phids = array($viewer->getPHID()); + $merchant_phids = $this->getMerchantPHIDs(); + + $any_edit = PhortuneMerchantQuery::canViewersEditMerchants( + $viewer_phids, + $merchant_phids); + + if ($any_edit) { + return true; } } @@ -159,7 +248,10 @@ final class PhortuneAccount extends PhortuneDAO } public function describeAutomaticCapability($capability) { - return pht('Members of an account can always view and edit it.'); + return array( + pht('Members of an account can always view and edit it.'), + pht('Merchants an account has established a relationship can view it.'), + ); } diff --git a/src/applications/phortune/storage/PhortuneAccountEmail.php b/src/applications/phortune/storage/PhortuneAccountEmail.php new file mode 100644 index 0000000000..50c8e90025 --- /dev/null +++ b/src/applications/phortune/storage/PhortuneAccountEmail.php @@ -0,0 +1,152 @@ + true, + self::CONFIG_COLUMN_SCHEMA => array( + 'address' => 'sort128', + 'status' => 'text32', + 'addressKey' => 'text32', + 'accessKey' => 'text32', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_account' => array( + 'columns' => array('accountPHID', 'address'), + 'unique' => true, + ), + 'key_address' => array( + 'columns' => array('addressKey'), + ), + ), + ) + parent::getConfiguration(); + } + + public function getPHIDType() { + return PhortuneAccountEmailPHIDType::TYPECONST; + } + + public static function initializeNewAddress( + PhortuneAccount $account, + $author_phid) { + + $address_key = Filesystem::readRandomCharacters(16); + $access_key = Filesystem::readRandomCharacters(16); + $default_status = PhortuneAccountEmailStatus::getDefaultStatusConstant(); + + return id(new self()) + ->setAuthorPHID($author_phid) + ->setAccountPHID($account->getPHID()) + ->setStatus($default_status) + ->attachAccount($account) + ->setAddressKey($address_key) + ->setAccessKey($access_key); + } + + public function attachAccount(PhortuneAccount $account) { + $this->account = $account; + return $this; + } + + public function getAccount() { + return $this->assertAttached($this->account); + } + + public function getObjectName() { + return pht('Account Email %d', $this->getID()); + } + + public function getURI() { + return urisprintf( + '/phortune/account/%d/addresses/%d/', + $this->getAccount()->getID(), + $this->getID()); + } + + public function getExternalURI() { + return urisprintf( + '/phortune/external/%s/%s/', + $this->getAddressKey(), + $this->getAccessKey()); + } + + public function getUnsubscribeURI() { + return urisprintf( + '/phortune/external/%s/%s/unsubscribe/', + $this->getAddressKey(), + $this->getAccessKey()); + } + + public function getExternalOrderURI(PhortuneCart $cart) { + return urisprintf( + '/phortune/external/%s/%s/order/%d/', + $this->getAddressKey(), + $this->getAccessKey(), + $cart->getID()); + } + + public function getExternalOrderPrintURI(PhortuneCart $cart) { + return urisprintf( + '/phortune/external/%s/%s/order/%d/print/', + $this->getAddressKey(), + $this->getAccessKey(), + $cart->getID()); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + 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->getAccount(), $capability), + ); + } + + +/* -( PhabricatorApplicationTransactionInterface )------------------------- */ + + + public function getApplicationTransactionEditor() { + return new PhortuneAccountEmailEditor(); + } + + public function getApplicationTransactionTemplate() { + return new PhortuneAccountEmailTransaction(); + } + +} diff --git a/src/applications/phortune/storage/PhortuneAccountEmailTransaction.php b/src/applications/phortune/storage/PhortuneAccountEmailTransaction.php new file mode 100644 index 0000000000..699c209438 --- /dev/null +++ b/src/applications/phortune/storage/PhortuneAccountEmailTransaction.php @@ -0,0 +1,18 @@ +getImplementation()->getDescription($this); } - public function getDetailURI(PhortuneMerchant $authority = null) { - if ($authority) { - $prefix = 'merchant/'.$authority->getID().'/'; - } else { - $prefix = ''; - } - return '/phortune/'.$prefix.'cart/'.$this->getID().'/'; + public function getDetailURI() { + return urisprintf( + '/phortune/cart/%d/', + $this->getID()); } public function getCheckoutURI() { @@ -501,6 +499,15 @@ final class PhortuneCart extends PhortuneDAO } } + public function canVoidOrder() { + try { + $this->assertCanVoidOrder(); + return true; + } catch (Exception $ex) { + return false; + } + } + public function assertCanCancelOrder() { switch ($this->getStatus()) { case self::STATUS_BUILDING: @@ -533,6 +540,27 @@ final class PhortuneCart extends PhortuneDAO return $this->getImplementation()->assertCanRefundOrder($this); } + public function assertCanVoidOrder() { + if (!$this->getIsInvoice()) { + throw new Exception( + pht( + 'This order can not be voided because it is not an invoice.')); + } + + switch ($this->getStatus()) { + case self::STATUS_READY: + break; + default: + throw new Exception( + pht( + 'This order can not be voided because it is not ready for '. + 'payment.')); + } + + return null; + } + + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, @@ -628,6 +656,10 @@ final class PhortuneCart extends PhortuneDAO return idx($this->metadata, $key, $default); } + public function getObjectName() { + return pht('Order %d', $this->getID()); + } + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ @@ -652,26 +684,15 @@ final class PhortuneCart extends PhortuneDAO } public function getPolicy($capability) { - // NOTE: Both view and edit use the account's edit policy. We punch a hole - // through this for merchants, below. - return $this - ->getAccount() - ->getPolicy(PhabricatorPolicyCapability::CAN_EDIT); + return PhabricatorPolicies::getMostOpenPolicy(); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { - if ($this->getAccount()->hasAutomaticCapability($capability, $viewer)) { - return true; - } - - // If the viewer controls the merchant this order was placed with, they - // can view the order. - if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { - $can_admin = PhabricatorPolicyFilter::hasCapability( - $viewer, - $this->getMerchant(), - PhabricatorPolicyCapability::CAN_EDIT); - if ($can_admin) { + if ($capability === PhabricatorPolicyCapability::CAN_VIEW) { + $any_edit = PhortuneMerchantQuery::canViewersEditMerchants( + array($viewer->getPHID()), + array($this->getMerchantPHID())); + if ($any_edit) { return true; } } @@ -679,10 +700,20 @@ final class PhortuneCart extends PhortuneDAO return false; } - public function describeAutomaticCapability($capability) { + +/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ + + + public function getExtendedPolicy($capability, PhabricatorUser $viewer) { + if ($this->hasAutomaticCapability($capability, $viewer)) { + return array(); + } + return array( - pht('Orders inherit the policies of the associated account.'), - pht('The merchant you placed an order with can review and manage it.'), + array( + $this->getAccount(), + PhabricatorPolicyCapability::CAN_EDIT, + ), ); } diff --git a/src/applications/phortune/storage/PhortuneCharge.php b/src/applications/phortune/storage/PhortuneCharge.php index da199d0751..41808a31e5 100644 --- a/src/applications/phortune/storage/PhortuneCharge.php +++ b/src/applications/phortune/storage/PhortuneCharge.php @@ -7,7 +7,9 @@ * charge followed by a successful charge. */ final class PhortuneCharge extends PhortuneDAO - implements PhabricatorPolicyInterface { + implements + PhabricatorPolicyInterface, + PhabricatorExtendedPolicyInterface { const STATUS_CHARGING = 'charge:charging'; const STATUS_CHARGED = 'charge:charged'; @@ -162,19 +164,42 @@ final class PhortuneCharge extends PhortuneDAO public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { - return $this->getAccount()->getPolicy($capability); + return PhabricatorPolicies::getMostOpenPolicy(); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { - return $this->getAccount()->hasAutomaticCapability($capability, $viewer); + if ($capability === PhabricatorPolicyCapability::CAN_VIEW) { + $any_edit = PhortuneMerchantQuery::canViewersEditMerchants( + array($viewer->getPHID()), + array($this->getMerchantPHID())); + if ($any_edit) { + return true; + } + } + + return false; } - public function describeAutomaticCapability($capability) { - return pht('Charges inherit the policies of the associated account.'); + +/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ + + + public function getExtendedPolicy($capability, PhabricatorUser $viewer) { + if ($this->hasAutomaticCapability($capability, $viewer)) { + return array(); + } + + return array( + array( + $this->getAccount(), + PhabricatorPolicyCapability::CAN_EDIT, + ), + ); } } diff --git a/src/applications/phortune/storage/PhortuneMerchant.php b/src/applications/phortune/storage/PhortuneMerchant.php index 4916cfede7..6e0bf81e22 100644 --- a/src/applications/phortune/storage/PhortuneMerchant.php +++ b/src/applications/phortune/storage/PhortuneMerchant.php @@ -6,7 +6,6 @@ final class PhortuneMerchant extends PhortuneDAO PhabricatorPolicyInterface { protected $name; - protected $viewPolicy; protected $description; protected $contactInfo; protected $invoiceEmail; @@ -18,7 +17,6 @@ final class PhortuneMerchant extends PhortuneDAO public static function initializeNewMerchant(PhabricatorUser $actor) { return id(new PhortuneMerchant()) - ->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy()) ->attachMemberPHIDs(array()) ->setContactInfo('') ->setInvoiceEmail('') @@ -70,6 +68,53 @@ final class PhortuneMerchant extends PhortuneDAO return $this->assertAttached($this->profileImageFile); } + public function getObjectName() { + return pht('Merchant %d', $this->getID()); + } + + public function getDetailsURI() { + return urisprintf( + '/phortune/merchant/%d/details/', + $this->getID()); + } + + public function getOrdersURI() { + return urisprintf( + '/phortune/merchant/%d/orders/', + $this->getID()); + } + + public function getOrderListURI($path = '') { + return urisprintf( + '/phortune/merchant/%d/orders/list/%s', + $this->getID(), + $path); + } + + public function getSubscriptionsURI() { + return urisprintf( + '/phortune/merchant/%d/subscriptions/', + $this->getID()); + } + + public function getSubscriptionListURI($path = '') { + return urisprintf( + '/phortune/merchant/%d/subscriptions/list/%s', + $this->getID(), + $path); + } + + public function getManagersURI() { + return urisprintf( + '/phortune/merchant/%d/managers/', + $this->getID()); + } + + public function getPaymentProvidersURI() { + return urisprintf( + '/phortune/merchant/%d/providers/', + $this->getID()); + } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ @@ -96,7 +141,7 @@ final class PhortuneMerchant extends PhortuneDAO public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: - return $this->getViewPolicy(); + return PhabricatorPolicies::getMostOpenPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return PhabricatorPolicies::POLICY_NOONE; } diff --git a/src/applications/phortune/storage/PhortunePaymentMethod.php b/src/applications/phortune/storage/PhortunePaymentMethod.php index 8937d6ee84..bbbf6e6b02 100644 --- a/src/applications/phortune/storage/PhortunePaymentMethod.php +++ b/src/applications/phortune/storage/PhortunePaymentMethod.php @@ -4,8 +4,13 @@ * A payment method is a credit card; it is associated with an account and * charges can be made against it. */ -final class PhortunePaymentMethod extends PhortuneDAO - implements PhabricatorPolicyInterface { +final class PhortunePaymentMethod + extends PhortuneDAO + implements + PhabricatorPolicyInterface, + PhabricatorExtendedPolicyInterface, + PhabricatorPolicyCodexInterface, + PhabricatorApplicationTransactionInterface { const STATUS_ACTIVE = 'payment:active'; const STATUS_DISABLED = 'payment:disabled'; @@ -136,6 +141,29 @@ final class PhortunePaymentMethod extends PhortuneDAO return ($this->getStatus() === self::STATUS_ACTIVE); } + public function getURI() { + return urisprintf( + '/phortune/account/%d/methods/%d/', + $this->getAccount()->getID(), + $this->getID()); + } + + public function getObjectName() { + return pht('Payment Method %d', $this->getID()); + } + + +/* -( PhabricatorApplicationTransactionInterface )------------------------- */ + + + public function getApplicationTransactionEditor() { + return new PhortunePaymentMethodEditor(); + } + + public function getApplicationTransactionTemplate() { + return new PhortunePaymentMethodTransaction(); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ @@ -148,18 +176,49 @@ final class PhortunePaymentMethod extends PhortuneDAO } public function getPolicy($capability) { - return $this->getAccount()->getPolicy($capability); + return PhabricatorPolicies::getMostOpenPolicy(); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { - return $this->getAccount()->hasAutomaticCapability( - $capability, - $viewer); + // See T13366. If you can edit the merchant associated with this payment + // method, you can view the payment method. + if ($capability === PhabricatorPolicyCapability::CAN_VIEW) { + $any_edit = PhortuneMerchantQuery::canViewersEditMerchants( + array($viewer->getPHID()), + array($this->getMerchantPHID())); + if ($any_edit) { + return true; + } + } + + return false; } - public function describeAutomaticCapability($capability) { - return pht( - 'Members of an account can always view and edit its payment methods.'); + +/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ + + + public function getExtendedPolicy($capability, PhabricatorUser $viewer) { + if ($this->hasAutomaticCapability($capability, $viewer)) { + return array(); + } + + // See T13366. For blanket view and edit permissions on all payment + // methods, you must be able to edit the associated account. + return array( + array( + $this->getAccount(), + PhabricatorPolicyCapability::CAN_EDIT, + ), + ); + } + + +/* -( PhabricatorPolicyCodexInterface )------------------------------------ */ + + + public function newPolicyCodex() { + return new PhortunePaymentMethodPolicyCodex(); } } diff --git a/src/applications/phortune/storage/PhortunePaymentMethodTransaction.php b/src/applications/phortune/storage/PhortunePaymentMethodTransaction.php new file mode 100644 index 0000000000..16ba306d90 --- /dev/null +++ b/src/applications/phortune/storage/PhortunePaymentMethodTransaction.php @@ -0,0 +1,18 @@ +setMerchantPHID($merchant->getPHID()) + ->attachMerchant($merchant) ->setIsEnabled(1); } @@ -75,6 +76,17 @@ final class PhortunePaymentProviderConfig extends PhortuneDAO ->setProviderConfig($this); } + public function getObjectName() { + return pht('Provider %d', $this->getID()); + } + + public function getURI() { + return urisprintf( + '/phortune/merchant/%d/providers/%d/', + $this->getMerchant()->getID(), + $this->getID()); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/phortune/storage/PhortuneSubscription.php b/src/applications/phortune/storage/PhortuneSubscription.php index a996dbf5d2..84fa23a812 100644 --- a/src/applications/phortune/storage/PhortuneSubscription.php +++ b/src/applications/phortune/storage/PhortuneSubscription.php @@ -3,8 +3,13 @@ /** * A subscription bills users regularly. */ -final class PhortuneSubscription extends PhortuneDAO - implements PhabricatorPolicyInterface { +final class PhortuneSubscription + extends PhortuneDAO + implements + PhabricatorPolicyInterface, + PhabricatorExtendedPolicyInterface, + PhabricatorPolicyCodexInterface, + PhabricatorApplicationTransactionInterface { const STATUS_ACTIVE = 'active'; const STATUS_CANCELLED = 'cancelled'; @@ -55,9 +60,8 @@ final class PhortuneSubscription extends PhortuneDAO ) + parent::getConfiguration(); } - public function generatePHID() { - return PhabricatorPHID::generateNewPHID( - PhortuneSubscriptionPHIDType::TYPECONST); + public function getPHIDType() { + return PhortuneSubscriptionPHIDType::TYPECONST; } public static function initializeNewSubscription( @@ -161,6 +165,10 @@ final class PhortuneSubscription extends PhortuneDAO } $this->saveTransaction(); + $account = $this->getAccount(); + $merchant = $this->getMerchant(); + $account->writeMerchantEdge($merchant); + return $result; } @@ -181,10 +189,10 @@ final class PhortuneSubscription extends PhortuneDAO } public function getURI() { - $account_id = $this->getAccount()->getID(); - $id = $this->getID(); - - return "/phortune/{$account_id}/subscription/view/{$id}/"; + return urisprintf( + '/phortune/account/%d/subscriptions/%d/', + $this->getAccount()->getID(), + $this->getID()); } public function getEditURI() { @@ -241,6 +249,16 @@ final class PhortuneSubscription extends PhortuneDAO $purchase); } +/* -( PhabricatorApplicationTransactionInterface )------------------------- */ + + + public function getApplicationTransactionEditor() { + return new PhortuneSubscriptionEditor(); + } + + public function getApplicationTransactionTemplate() { + return new PhortuneSubscriptionTransaction(); + } /* -( PhabricatorPolicyInterface )----------------------------------------- */ @@ -253,26 +271,17 @@ final class PhortuneSubscription extends PhortuneDAO } public function getPolicy($capability) { - // NOTE: Both view and edit use the account's edit policy. We punch a hole - // through this for merchants, below. - return $this - ->getAccount() - ->getPolicy(PhabricatorPolicyCapability::CAN_EDIT); + return PhabricatorPolicies::getMostOpenPolicy(); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { - if ($this->getAccount()->hasAutomaticCapability($capability, $viewer)) { - return true; - } - - // If the viewer controls the merchant this subscription bills to, they can - // view the subscription. - if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { - $can_admin = PhabricatorPolicyFilter::hasCapability( - $viewer, - $this->getMerchant(), - PhabricatorPolicyCapability::CAN_EDIT); - if ($can_admin) { + // See T13366. If you can edit the merchant associated with this + // subscription, you can view the subscription. + if ($capability === PhabricatorPolicyCapability::CAN_VIEW) { + $any_edit = PhortuneMerchantQuery::canViewersEditMerchants( + array($viewer->getPHID()), + array($this->getMerchantPHID())); + if ($any_edit) { return true; } } @@ -280,12 +289,31 @@ final class PhortuneSubscription extends PhortuneDAO return false; } - public function describeAutomaticCapability($capability) { + +/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ + + + public function getExtendedPolicy($capability, PhabricatorUser $viewer) { + if ($this->hasAutomaticCapability($capability, $viewer)) { + return array(); + } + + // See T13366. For blanket view and edit permissions on all subscriptions, + // you must be able to edit the associated account. return array( - pht('Subscriptions inherit the policies of the associated account.'), - pht( - 'The merchant you are subscribed with can review and manage the '. - 'subscription.'), + array( + $this->getAccount(), + PhabricatorPolicyCapability::CAN_EDIT, + ), ); } + + +/* -( PhabricatorPolicyCodexInterface )------------------------------------ */ + + + public function newPolicyCodex() { + return new PhortuneSubscriptionPolicyCodex(); + } + } diff --git a/src/applications/phortune/storage/PhortuneSubscriptionTransaction.php b/src/applications/phortune/storage/PhortuneSubscriptionTransaction.php new file mode 100644 index 0000000000..78054a5aba --- /dev/null +++ b/src/applications/phortune/storage/PhortuneSubscriptionTransaction.php @@ -0,0 +1,18 @@ +showOrder; } - public function setHandles(array $handles) { - $this->handles = $handles; - return $this; - } - - public function getHandles() { - return $this->handles; - } - public function setCharges(array $charges) { $this->charges = $charges; return $this; @@ -35,8 +25,17 @@ final class PhortuneChargeTableView extends AphrontView { public function render() { $charges = $this->getCharges(); - $handles = $this->getHandles(); - $viewer = $this->getUser(); + $viewer = $this->getViewer(); + + $phids = array(); + foreach ($charges as $charge) { + $phids[] = $charge->getCartPHID(); + $phids[] = $charge->getProviderPHID(); + $phids[] = $charge->getPaymentMethodPHID(); + $phids[] = $charge->getMerchantPHID(); + } + + $handles = $viewer->loadHandles($phids); $rows = array(); foreach ($charges as $charge) { diff --git a/src/applications/phortune/view/PhortuneInvoiceView.php b/src/applications/phortune/view/PhortuneInvoiceView.php deleted file mode 100644 index 65da418cc2..0000000000 --- a/src/applications/phortune/view/PhortuneInvoiceView.php +++ /dev/null @@ -1,159 +0,0 @@ -merchantName = $name; - return $this; - } - - public function setMerchantLogo($logo) { - $this->merchantLogo = $logo; - return $this; - } - - public function setMerchantContact($contact) { - $this->merchantContact = $contact; - return $this; - } - - public function setMerchantFooter($footer) { - $this->merchantFooter = $footer; - return $this; - } - - public function setAccountName($name) { - $this->accountName = $name; - return $this; - } - - public function setAccountContact($contact) { - $this->accountContact = $contact; - return $this; - } - - public function setStatus($status) { - $this->status = $status; - return $this; - } - - public function setContent($content) { - $this->content = $content; - return $this; - } - - protected function getTagAttributes() { - $classes = array(); - $classes[] = 'phortune-invoice-view'; - - return array( - 'class' => implode(' ', $classes), - ); - } - - protected function getTagContent() { - require_celerity_resource('phortune-invoice-css'); - - $logo = phutil_tag( - 'div', - array( - 'class' => 'phortune-invoice-logo', - ), - phutil_tag( - 'img', - array( - 'height' => '50', - 'width' => '50', - 'alt' => $this->merchantName, - 'src' => $this->merchantLogo, - ))); - - $to_title = phutil_tag( - 'div', - array( - 'class' => 'phortune-mini-header', - ), - pht('Bill To:')); - - $bill_to = phutil_tag( - 'td', - array( - 'class' => 'phortune-invoice-to', - 'width' => '50%', - ), - array( - $to_title, - phutil_tag('strong', array(), $this->accountName), - phutil_tag('br', array()), - $this->accountContact, - )); - - $from_title = phutil_tag( - 'div', - array( - 'class' => 'phortune-mini-header', - ), - pht('From:')); - - $bill_from = phutil_tag( - 'td', - array( - 'class' => 'phortune-invoice-from', - 'width' => '50%', - ), - array( - $from_title, - phutil_tag('strong', array(), $this->merchantName), - phutil_tag('br', array()), - $this->merchantContact, - )); - - $contact = phutil_tag( - 'table', - array( - 'class' => 'phortune-invoice-contact', - 'width' => '100%', - ), - phutil_tag( - 'tr', - array(), - array( - $bill_to, - $bill_from, - ))); - - $status = null; - if ($this->status) { - $status = phutil_tag( - 'div', - array( - 'class' => 'phortune-invoice-status', - ), - $this->status); - } - - $footer = phutil_tag( - 'div', - array( - 'class' => 'phortune-invoice-footer', - ), - $this->merchantFooter); - - return array( - $logo, - $contact, - $status, - $this->content, - $footer, - ); - } -} diff --git a/src/applications/phortune/view/PhortuneOrderDescriptionView.php b/src/applications/phortune/view/PhortuneOrderDescriptionView.php new file mode 100644 index 0000000000..1eaf290341 --- /dev/null +++ b/src/applications/phortune/view/PhortuneOrderDescriptionView.php @@ -0,0 +1,39 @@ +order = $order; + return $this; + } + + public function getOrder() { + return $this->order; + } + + public function render() { + $viewer = $this->getViewer(); + $order = $this->getOrder(); + + $description = $order->getDescription(); + if (!strlen($description)) { + return null; + } + + $output = new PHUIRemarkupView($viewer, $description); + + $description_box = id(new PHUIBoxView()) + ->addMargin(PHUI::MARGIN_LARGE) + ->appendChild($output); + + return id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Description')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->appendChild($description_box); + } + + +} diff --git a/src/applications/phortune/view/PhortuneOrderItemsView.php b/src/applications/phortune/view/PhortuneOrderItemsView.php new file mode 100644 index 0000000000..8c7f06db13 --- /dev/null +++ b/src/applications/phortune/view/PhortuneOrderItemsView.php @@ -0,0 +1,58 @@ +getViewer(); + $order = $this->getOrder(); + + $purchases = id(new PhortunePurchaseQuery()) + ->setViewer($viewer) + ->withCartPHIDs(array($order->getPHID())) + ->execute(); + + $order->attachPurchases($purchases); + + $rows = array(); + foreach ($purchases as $purchase) { + $rows[] = array( + $purchase->getFullDisplayName(), + $purchase->getBasePriceAsCurrency()->formatForDisplay(), + $purchase->getQuantity(), + $purchase->getTotalPriceAsCurrency()->formatForDisplay(), + ); + } + + $rows[] = array( + phutil_tag('strong', array(), pht('Total')), + '', + '', + phutil_tag('strong', array(), + $order->getTotalPriceAsCurrency()->formatForDisplay()), + ); + + $table = new AphrontTableView($rows); + $table->setHeaders( + array( + pht('Item'), + pht('Price'), + pht('Qty.'), + pht('Total'), + )); + $table->setColumnClasses( + array( + 'wide', + 'right', + 'right', + 'right', + )); + + return id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Items')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setTable($table); + } + + +} diff --git a/src/applications/phortune/view/PhortuneOrderSummaryView.php b/src/applications/phortune/view/PhortuneOrderSummaryView.php new file mode 100644 index 0000000000..77152b9ff7 --- /dev/null +++ b/src/applications/phortune/view/PhortuneOrderSummaryView.php @@ -0,0 +1,370 @@ +resumeURI = $resume_uri; + return $this; + } + + public function getResumeURI() { + return $this->resumeURI; + } + + public function setPrintable($printable) { + $this->printable = $printable; + return $this; + } + + public function getPrintable() { + return $this->printable; + } + + public function render() { + $is_printable = $this->getPrintable(); + + $content = array(); + + if ($is_printable) { + $content[] = $this->newContactHeader(); + } + + $content[] = $this->newMessagesView(); + $content[] = $this->newDetailsView(); + $content[] = $this->newDescriptionView(); + $content[] = $this->newItemsView(); + $content[] = $this->newChargesView(); + + if ($is_printable) { + $content[] = $this->newContactFooter(); + } + + return $content; + } + + private function newMessagesView() { + $viewer = $this->getViewer(); + $order = $this->getOrder(); + + $messages = array(); + $severity = null; + + $resume_uri = $this->getResumeURI(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $order, + PhabricatorPolicyCapability::CAN_EDIT); + + $can_merchant = PhabricatorPolicyFilter::hasCapability( + $viewer, + $order->getMerchant(), + PhabricatorPolicyCapability::CAN_EDIT); + + switch ($order->getStatus()) { + case PhortuneCart::STATUS_READY: + if ($order->getIsInvoice()) { + $severity = PHUIInfoView::SEVERITY_NOTICE; + $messages[] = pht('This invoice is ready for payment.'); + } + break; + case PhortuneCart::STATUS_PURCHASING: + if ($can_edit) { + if ($resume_uri) { + $messages[] = pht( + 'The checkout process has been started, but not yet completed. '. + 'You can continue checking out by clicking %s, or cancel the '. + 'order, or contact the merchant for assistance.', + phutil_tag('strong', array(), pht('Continue Checkout'))); + } else { + $messages[] = pht( + 'The checkout process has been started, but an error occurred. '. + 'You can cancel the order or contact the merchant for '. + 'assistance.'); + } + } + break; + case PhortuneCart::STATUS_CHARGED: + if ($can_edit) { + $messages[] = pht( + 'You have been charged, but processing could not be completed. '. + 'You can cancel your order, or contact the merchant for '. + 'assistance.'); + } + break; + case PhortuneCart::STATUS_HOLD: + if ($can_edit) { + $messages[] = pht( + 'Payment for this order is on hold. You can click %s to check '. + 'for updates, cancel the order, or contact the merchant for '. + 'assistance.', + phutil_tag('strong', array(), pht('Update Status'))); + } + break; + case PhortuneCart::STATUS_REVIEW: + if ($can_merchant) { + $messages[] = pht( + 'This order has been flagged for manual review. Review the order '. + 'and choose %s to accept it or %s to reject it.', + phutil_tag('strong', array(), pht('Accept Order')), + phutil_tag('strong', array(), pht('Refund Order'))); + } else if ($can_edit) { + $messages[] = pht( + 'This order requires manual processing and will complete once '. + 'the merchant accepts it.'); + } + break; + case PhortuneCart::STATUS_PURCHASED: + $severity = PHUIInfoView::SEVERITY_SUCCESS; + $messages[] = pht('This purchase has been completed.'); + break; + } + + if (!$messages) { + return null; + } + + if ($severity === null) { + $severity = PHUIInfoView::SEVERITY_WARNING; + } + + $messages_view = id(new PHUIInfoView()) + ->setSeverity($severity) + ->appendChild($messages); + + $is_printable = $this->getPrintable(); + if ($is_printable) { + $messages_view = phutil_tag( + 'div', + array( + 'class' => 'phortune-invoice-status', + ), + $messages_view); + } + + return $messages_view; + } + + private function newDetailsView() { + $viewer = $this->getViewer(); + $order = $this->getOrder(); + $is_printable = $this->getPrintable(); + + $view = id(new PHUIPropertyListView()) + ->setViewer($viewer) + ->setObject($order); + + $account_phid = $order->getAccountPHID(); + $author_phid = $order->getAuthorPHID(); + $merchant_phid = $order->getMerchantPHID(); + + $handles = $viewer->loadHandles( + array( + $account_phid, + $author_phid, + $merchant_phid, + )); + + if ($is_printable) { + $account_link = $handles[$account_phid]->getFullName(); + $author_link = $handles[$author_phid]->getFullName(); + $merchant_link = $handles[$merchant_phid]->getFullName(); + } else { + $account_link = $handles[$account_phid]->renderLink(); + $author_link = $handles[$author_phid]->renderLink(); + $merchant_link = $handles[$merchant_phid]->renderLink(); + } + + if ($is_printable) { + $view->addProperty(pht('Order Name'), $order->getName()); + } + + $view->addProperty(pht('Account'), $account_link); + $view->addProperty(pht('Authorized By'), $author_link); + $view->addProperty(pht('Merchant'), $merchant_link); + + $view->addProperty( + pht('Order Status'), + PhortuneCart::getNameForStatus($order->getStatus())); + $view->addProperty( + pht('Created'), + phabricator_datetime($order->getDateCreated(), $viewer)); + $view->addProperty( + pht('Updated'), + phabricator_datetime($order->getDateModified(), $viewer)); + + return id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Details')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->appendChild($view); + } + + private function newChargesView() { + $viewer = $this->getViewer(); + $order = $this->getOrder(); + + $charges = id(new PhortuneChargeQuery()) + ->setViewer($viewer) + ->withCartPHIDs(array($order->getPHID())) + ->needCarts(true) + ->execute(); + + $charges_table = id(new PhortuneChargeTableView()) + ->setUser($viewer) + ->setCharges($charges) + ->setShowOrder(false); + + $charges_view = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Charges')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setTable($charges_table); + + return $charges_view; + } + + private function newDescriptionView() { + $viewer = $this->getViewer(); + $order = $this->getOrder(); + + return id(new PhortuneOrderDescriptionView()) + ->setViewer($viewer) + ->setOrder($order); + } + + private function newItemsView() { + $viewer = $this->getViewer(); + $order = $this->getOrder(); + + return id(new PhortuneOrderItemsView()) + ->setViewer($viewer) + ->setOrder($order); + } + + private function newContactHeader() { + $viewer = $this->getViewer(); + $order = $this->getOrder(); + + $merchant = id(new PhortuneMerchantQuery()) + ->setViewer($viewer) + ->withPHIDs(array($order->getMerchant()->getPHID())) + ->needProfileImage(true) + ->executeOne(); + + $merchant_name = $merchant->getName(); + $merchant_image = $merchant->getProfileImageURI(); + + $account = $order->getAccount(); + $account_name = $account->getBillingName(); + + $account_contact = $account->getBillingAddress(); + if (strlen($account_contact)) { + $account_contact = new PHUIRemarkupView( + $viewer, + $account_contact); + } + + $merchant_contact = $merchant->getContactInfo(); + if (strlen($merchant_contact)) { + $merchant_contact = new PHUIRemarkupView( + $viewer, + $merchant->getContactInfo()); + } + + $logo = phutil_tag( + 'div', + array( + 'class' => 'phortune-invoice-logo', + ), + phutil_tag( + 'img', + array( + 'height' => '50', + 'width' => '50', + 'alt' => $merchant_name, + 'src' => $merchant_image, + ))); + + $to_title = phutil_tag( + 'div', + array( + 'class' => 'phortune-mini-header', + ), + pht('Bill To:')); + + $bill_to = phutil_tag( + 'td', + array( + 'class' => 'phortune-invoice-to', + 'width' => '50%', + ), + array( + $to_title, + phutil_tag('strong', array(), $account_name), + phutil_tag('br', array()), + $account_contact, + )); + + $from_title = phutil_tag( + 'div', + array( + 'class' => 'phortune-mini-header', + ), + pht('From:')); + + $bill_from = phutil_tag( + 'td', + array( + 'class' => 'phortune-invoice-from', + 'width' => '50%', + ), + array( + $from_title, + phutil_tag('strong', array(), $merchant_name), + phutil_tag('br', array()), + $merchant_contact, + )); + + $contact = phutil_tag( + 'table', + array( + 'class' => 'phortune-invoice-contact', + 'width' => '100%', + ), + phutil_tag( + 'tr', + array(), + array( + $bill_to, + $bill_from, + ))); + + return array( + $logo, + $contact, + ); + } + + private function newContactFooter() { + $viewer = $this->getViewer(); + $order = $this->getOrder(); + + $merchant = $order->getMerchant(); + $footer = $merchant->getInvoiceFooter(); + + if (!strlen($footer)) { + return null; + } + + return phutil_tag( + 'div', + array( + 'class' => 'phortune-invoice-footer', + ), + $footer); + } + +} diff --git a/src/applications/phortune/view/PhortuneOrderTableView.php b/src/applications/phortune/view/PhortuneOrderTableView.php index 435f5151d0..d7868f8d49 100644 --- a/src/applications/phortune/view/PhortuneOrderTableView.php +++ b/src/applications/phortune/view/PhortuneOrderTableView.php @@ -3,19 +3,10 @@ final class PhortuneOrderTableView extends AphrontView { private $carts; - private $handles; private $noDataString; private $isInvoices; private $isMerchantView; - - public function setHandles(array $handles) { - $this->handles = $handles; - return $this; - } - - public function getHandles() { - return $this->handles; - } + private $accountEmail; public function setCarts(array $carts) { $this->carts = $carts; @@ -53,30 +44,65 @@ final class PhortuneOrderTableView extends AphrontView { return $this->isMerchantView; } + public function setAccountEmail(PhortuneAccountEmail $account_email) { + $this->accountEmail = $account_email; + return $this; + } + + public function getAccountEmail() { + return $this->accountEmail; + } + public function render() { $carts = $this->getCarts(); - $handles = $this->getHandles(); $viewer = $this->getUser(); $is_invoices = $this->getIsInvoices(); $is_merchant = $this->getIsMerchantView(); + $is_external = (bool)$this->getAccountEmail(); + + $email = $this->getAccountEmail(); + + $phids = array(); + foreach ($carts as $cart) { + $phids[] = $cart->getPHID(); + foreach ($cart->getPurchases() as $purchase) { + $phids[] = $purchase->getPHID(); + } + $phids[] = $cart->getMerchantPHID(); + } + + $handles = $viewer->loadHandles($phids); $rows = array(); $rowc = array(); foreach ($carts as $cart) { - $cart_link = $handles[$cart->getPHID()]->renderLink(); + if ($is_external) { + $cart_link = phutil_tag( + 'a', + array( + 'href' => $email->getExternalOrderURI($cart), + ), + $handles[$cart->getPHID()]->getName()); + } else { + $cart_link = $handles[$cart->getPHID()]->renderLink(); + } $purchases = $cart->getPurchases(); if (count($purchases) == 1) { $purchase = head($purchases); - $purchase_name = $handles[$purchase->getPHID()]->renderLink(); + $purchase_name = $handles[$purchase->getPHID()]->getName(); $purchases = array(); } else { $purchase_name = ''; } if ($is_invoices) { - $merchant_link = $handles[$cart->getMerchantPHID()]->renderLink(); + if ($is_external) { + $merchant_link = $handles[$cart->getMerchantPHID()]->getName(); + } else { + $merchant_link = $handles[$cart->getMerchantPHID()]->renderLink(); + } } else { $merchant_link = null; } @@ -97,13 +123,12 @@ final class PhortuneOrderTableView extends AphrontView { PhortuneCart::getNameForStatus($cart->getStatus()), phabricator_datetime($cart->getDateModified(), $viewer), phabricator_datetime($cart->getDateCreated(), $viewer), - phutil_tag( - 'a', - array( - 'href' => $cart->getCheckoutURI(), - 'class' => 'small button button-green', - ), - pht('Pay Now')), + id(new PHUIButtonView()) + ->setTag('a') + ->setColor('green') + ->setHref($cart->getCheckoutURI()) + ->setText(pht('Pay Now')) + ->setIcon('fa-credit-card'), ); foreach ($purchases as $purchase) { $id = $purchase->getID(); @@ -164,7 +189,7 @@ final class PhortuneOrderTableView extends AphrontView { // We show "Pay Now" for due invoices, but not if the viewer is the // merchant, since it doesn't make sense for them to pay. - ($is_invoices && !$is_merchant), + ($is_invoices && !$is_merchant && !$is_external), )); return $table; diff --git a/src/applications/phortune/view/PhortuneOrderView.php b/src/applications/phortune/view/PhortuneOrderView.php new file mode 100644 index 0000000000..25261aa423 --- /dev/null +++ b/src/applications/phortune/view/PhortuneOrderView.php @@ -0,0 +1,17 @@ +order = $order; + return $this; + } + + final public function getOrder() { + return $this->order; + } + +} diff --git a/src/applications/phortune/view/PhortuneSubscriptionTableView.php b/src/applications/phortune/view/PhortuneSubscriptionTableView.php index f081ac3ddb..0c02249097 100644 --- a/src/applications/phortune/view/PhortuneSubscriptionTableView.php +++ b/src/applications/phortune/view/PhortuneSubscriptionTableView.php @@ -3,19 +3,9 @@ final class PhortuneSubscriptionTableView extends AphrontView { private $subscriptions; - private $handles; private $isMerchantView; private $notice; - public function setHandles(array $handles) { - $this->handles = $handles; - return $this; - } - - public function getHandles() { - return $this->handles; - } - public function setSubscriptions(array $subscriptions) { $this->subscriptions = $subscriptions; return $this; @@ -40,9 +30,15 @@ final class PhortuneSubscriptionTableView extends AphrontView { } public function render() { + return $this->newTableView(); + } + + public function newTableView() { $subscriptions = $this->getSubscriptions(); - $handles = $this->getHandles(); - $viewer = $this->getUser(); + $viewer = $this->getViewer(); + + $phids = mpull($subscriptions, 'getPHID'); + $handles = $viewer->loadHandles($phids); $rows = array(); $rowc = array(); diff --git a/src/applications/phortune/worker/PhortuneSubscriptionWorker.php b/src/applications/phortune/worker/PhortuneSubscriptionWorker.php index d05aacbb7c..438ebf5b1b 100644 --- a/src/applications/phortune/worker/PhortuneSubscriptionWorker.php +++ b/src/applications/phortune/worker/PhortuneSubscriptionWorker.php @@ -22,6 +22,7 @@ final class PhortuneSubscriptionWorker extends PhabricatorWorker { return; } + $account = $subscription->getAccount(); $merchant = $subscription->getMerchant(); @@ -48,12 +49,15 @@ final class PhortuneSubscriptionWorker extends PhabricatorWorker { ->withPHIDs($account->getMemberPHIDs()) ->execute(); $actor = null; + + $any_disabled = false; foreach ($members as $member) { // Don't act as a disabled user. If all of the users on the account are // disabled this means we won't charge the subscription, but that's // probably correct since it means no one can cancel or pay it anyway. if ($member->getIsDisabled()) { + $any_disabled = true; continue; } @@ -63,7 +67,26 @@ final class PhortuneSubscriptionWorker extends PhabricatorWorker { } if (!$actor) { - throw new Exception(pht('Failed to load actor to bill subscription!')); + if ($any_disabled) { + $message = pht( + 'All members of the account ("%s") for this subscription ("%s") '. + 'are disabled.', + $account->getPHID(), + $subscription->getPHID()); + } else if ($account->getMemberPHIDs()) { + $message = pht( + 'Unable to load any of the members of the account ("%s") for this '. + 'subscription ("%s").', + $account->getPHID(), + $subscription->getPHID()); + } else { + $message = pht( + 'The account ("%s") for this subscription ("%s") has no '. + 'members.', + $account->getPHID(), + $subscription->getPHID()); + } + throw new PhabricatorWorkerPermanentFailureException($message); } $cart = $account->newCart($actor, $cart_implementation, $merchant); diff --git a/src/applications/phortune/xaction/PhortuneAccountEmailAddressTransaction.php b/src/applications/phortune/xaction/PhortuneAccountEmailAddressTransaction.php new file mode 100644 index 0000000000..87e6bb87e4 --- /dev/null +++ b/src/applications/phortune/xaction/PhortuneAccountEmailAddressTransaction.php @@ -0,0 +1,63 @@ +getAddress(); + } + + public function applyInternalEffects($object, $value) { + $object->setAddress($value); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + if ($this->isEmptyTextTransaction($object->getAddress(), $xactions)) { + $errors[] = $this->newRequiredError( + pht('You must provide an email address.')); + } + + $max_length = $object->getColumnMaximumByteLength('address'); + foreach ($xactions as $xaction) { + $old_value = $xaction->getOldValue(); + $new_value = $xaction->getNewValue(); + + $new_length = strlen($new_value); + if ($new_length > $max_length) { + $errors[] = $this->newInvalidError( + pht( + 'The address can be no longer than %s characters.', + new PhutilNumber($max_length)), + $xaction); + continue; + } + + if (!PhabricatorUserEmail::isValidAddress($new_value)) { + $errors[] = $this->newInvalidError( + PhabricatorUserEmail::describeValidAddresses(), + $xaction); + continue; + } + + if ($new_value !== $old_value) { + if (!$this->isNewObject()) { + $errors[] = $this->newInvalidError( + pht( + 'Account email addresses can not be edited once they are '. + 'created. To change the billing address for an account, '. + 'disable the old address and then add a new address.'), + $xaction); + continue; + } + } + + } + + return $errors; + } + +} diff --git a/src/applications/phortune/xaction/PhortuneAccountEmailRotateTransaction.php b/src/applications/phortune/xaction/PhortuneAccountEmailRotateTransaction.php new file mode 100644 index 0000000000..e885b2d67b --- /dev/null +++ b/src/applications/phortune/xaction/PhortuneAccountEmailRotateTransaction.php @@ -0,0 +1,23 @@ +setAccessKey($access_key); + } + + public function getTitle() { + return pht( + '%s rotated the access key for this email address.', + $this->renderAuthor()); + } + +} diff --git a/src/applications/phortune/xaction/PhortuneAccountEmailStatusTransaction.php b/src/applications/phortune/xaction/PhortuneAccountEmailStatusTransaction.php new file mode 100644 index 0000000000..e607db2f99 --- /dev/null +++ b/src/applications/phortune/xaction/PhortuneAccountEmailStatusTransaction.php @@ -0,0 +1,23 @@ +getStatus(); + } + + public function applyInternalEffects($object, $value) { + $object->setStatus($value); + } + + public function getTitle() { + return pht( + '%s changed the status for this address to %s.', + $this->renderAuthor(), + $this->renderNewValue()); + } + +} diff --git a/src/applications/phortune/xaction/PhortuneAccountEmailTransactionType.php b/src/applications/phortune/xaction/PhortuneAccountEmailTransactionType.php new file mode 100644 index 0000000000..2654796207 --- /dev/null +++ b/src/applications/phortune/xaction/PhortuneAccountEmailTransactionType.php @@ -0,0 +1,4 @@ +getName(); + } + + public function applyInternalEffects($object, $value) { + $object->setName($value); + } + + public function getTitle() { + $old_value = $this->getOldValue(); + $new_value = $this->getNewValue(); + + if (strlen($old_value) && strlen($new_value)) { + return pht( + '%s renamed this payment method from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } else if (strlen($new_value)) { + return pht( + '%s set the name of this payment method to %s.', + $this->renderAuthor(), + $this->renderNewValue()); + } else { + return pht( + '%s removed the name of this payment method (was: %s).', + $this->renderAuthor(), + $this->renderOldValue()); + } + } + +} diff --git a/src/applications/phortune/xaction/paymentmethod/PhortunePaymentMethodStatusTransaction.php b/src/applications/phortune/xaction/paymentmethod/PhortunePaymentMethodStatusTransaction.php new file mode 100644 index 0000000000..f53c90057d --- /dev/null +++ b/src/applications/phortune/xaction/paymentmethod/PhortunePaymentMethodStatusTransaction.php @@ -0,0 +1,22 @@ +getStatus(); + } + + public function applyInternalEffects($object, $value) { + $object->setStatus($value); + } + + public function getTitle() { + return pht( + '%s changed the status of this payment method.', + $this->renderAuthor()); + } + +} diff --git a/src/applications/phortune/xaction/paymentmethod/PhortunePaymentMethodTransactionType.php b/src/applications/phortune/xaction/paymentmethod/PhortunePaymentMethodTransactionType.php new file mode 100644 index 0000000000..97c7c3a887 --- /dev/null +++ b/src/applications/phortune/xaction/paymentmethod/PhortunePaymentMethodTransactionType.php @@ -0,0 +1,4 @@ +getDefaultPaymentMethodPHID(); + } + + public function applyInternalEffects($object, $value) { + $object->setDefaultPaymentMethodPHID($value); + } + + public function getTitle() { + $old_phid = $this->getOldValue(); + $new_phid = $this->getNewValue(); + + if ($old_phid && $new_phid) { + return pht( + '%s changed the automatic payment method for this subscription.', + $this->renderAuthor()); + } else if ($new_phid) { + return pht( + '%s configured an automatic payment method for this subscription.', + $this->renderAuthor()); + } else { + return pht( + '%s stopped automatic payments for this subscription.', + $this->renderAuthor()); + } + } + + public function shouldTryMFA( + $object, + PhabricatorApplicationTransaction $xaction) { + return true; + } + +} diff --git a/src/applications/phortune/xaction/subscription/PhortuneSubscriptionTransactionType.php b/src/applications/phortune/xaction/subscription/PhortuneSubscriptionTransactionType.php new file mode 100644 index 0000000000..676b36122b --- /dev/null +++ b/src/applications/phortune/xaction/subscription/PhortuneSubscriptionTransactionType.php @@ -0,0 +1,4 @@ +getPolicySpecialRuleDescriptions() as $rule) { - if (in_array($capability, $rule->getCapabilities())) { - return $rule; - } - } - - return null; - } - final protected function newRule() { return new PhabricatorPolicyCodexRuleDescription(); } diff --git a/src/applications/project/application/PhabricatorProjectApplication.php b/src/applications/project/application/PhabricatorProjectApplication.php index 1588cca8ff..66132a831b 100644 --- a/src/applications/project/application/PhabricatorProjectApplication.php +++ b/src/applications/project/application/PhabricatorProjectApplication.php @@ -66,7 +66,6 @@ final class PhabricatorProjectApplication extends PhabricatorApplication { 'subprojects/(?P[1-9]\d*)/' => 'PhabricatorProjectSubprojectsController', 'board/(?P[1-9]\d*)/'. - '(?Pfilter/)?'. '(?:query/(?P[^/]+)/)?' => 'PhabricatorProjectBoardViewController', 'move/(?P[1-9]\d*)/' => 'PhabricatorProjectMoveController', @@ -80,6 +79,12 @@ final class PhabricatorProjectApplication extends PhabricatorApplication { => 'PhabricatorProjectColumnHideController', 'column/(?:(?P\d+)/)?' => 'PhabricatorProjectColumnDetailController', + 'viewquery/(?P\d+)/' + => 'PhabricatorProjectColumnViewQueryController', + 'bulk/(?P\d+)/' + => 'PhabricatorProjectColumnBulkEditController', + 'bulkmove/(?P\d+)/(?Pproject|column)/' + => 'PhabricatorProjectColumnBulkMoveController', 'import/' => 'PhabricatorProjectBoardImportController', 'reorder/' @@ -90,6 +95,12 @@ final class PhabricatorProjectApplication extends PhabricatorApplication { => 'PhabricatorProjectBoardManageController', 'background/' => 'PhabricatorProjectBoardBackgroundController', + 'default/(?P[^/]+)/' + => 'PhabricatorProjectBoardDefaultController', + 'filter/(?:query/(?P[^/]+)/)?' + => 'PhabricatorProjectBoardFilterController', + 'reload/' + => 'PhabricatorProjectBoardReloadController', ), 'column/' => array( 'remove/(?P\d+)/' => @@ -112,8 +123,6 @@ final class PhabricatorProjectApplication extends PhabricatorApplication { => 'PhabricatorProjectSilenceController', 'warning/(?P[1-9]\d*)/' => 'PhabricatorProjectSubprojectWarningController', - 'default/(?P[1-9]\d*)/(?P[^/]+)/' - => 'PhabricatorProjectDefaultController', ), '/tag/' => array( '(?P[^/]+)/' => 'PhabricatorProjectProfileController', diff --git a/src/applications/project/controller/PhabricatorProjectBoardController.php b/src/applications/project/controller/PhabricatorProjectBoardController.php index b889bc75da..5427d1b93f 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardController.php @@ -1,4 +1,36 @@ viewState === null) { + $this->viewState = $this->newViewState(); + } + + return $this->viewState; + } + + private function newViewState() { + $project = $this->getProject(); + $request = $this->getRequest(); + + return id(new PhabricatorWorkboardViewState()) + ->setProject($project) + ->readFromRequest($request); + } + + final protected function newWorkboardDialog() { + $dialog = $this->newDialog(); + + $state = $this->getViewState(); + foreach ($state->getQueryParameters() as $key => $value) { + $dialog->addHiddenInput($key, $value); + } + + return $dialog; + } + +} diff --git a/src/applications/project/controller/PhabricatorProjectDefaultController.php b/src/applications/project/controller/PhabricatorProjectBoardDefaultController.php similarity index 60% rename from src/applications/project/controller/PhabricatorProjectDefaultController.php rename to src/applications/project/controller/PhabricatorProjectBoardDefaultController.php index 2c7a47b2df..c531105a0d 100644 --- a/src/applications/project/controller/PhabricatorProjectDefaultController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardDefaultController.php @@ -1,25 +1,20 @@ getViewer(); - $project_id = $request->getURIData('projectID'); - $project = id(new PhabricatorProjectQuery()) - ->setViewer($viewer) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->withIDs(array($project_id)) - ->executeOne(); - if (!$project) { - return new Aphront404Response(); + $response = $this->loadProjectForEdit(); + if ($response) { + return $response; } - $this->setProject($project); + + $project = $this->getProject(); + $state = $this->getViewState(); + $board_uri = $state->newWorkboardURI(); + $remove_param = null; $target = $request->getURIData('target'); switch ($target) { @@ -31,8 +26,10 @@ final class PhabricatorProjectDefaultController 'the board.'); $button = pht('Save Default Filter'); - $xaction_value = $request->getStr('filter'); + $xaction_value = $state->getQueryKey(); $xaction_type = PhabricatorProjectFilterTransaction::TRANSACTIONTYPE; + + $remove_param = 'filter'; break; case 'sort': $title = pht('Set Board Default Order'); @@ -42,8 +39,10 @@ final class PhabricatorProjectDefaultController 'the board.'); $button = pht('Save Default Order'); - $xaction_value = $request->getStr('order'); + $xaction_value = $state->getOrder(); $xaction_type = PhabricatorProjectSortTransaction::TRANSACTIONTYPE; + + $remove_param = 'order'; break; default: return new Aphront404Response(); @@ -51,12 +50,6 @@ final class PhabricatorProjectDefaultController $id = $project->getID(); - $view_uri = $this->getApplicationURI("board/{$id}/"); - $view_uri = new PhutilURI($view_uri); - foreach ($request->getPassthroughRequestData() as $key => $value) { - $view_uri->replaceQueryParam($key, $value); - } - if ($request->isFormPost()) { $xactions = array(); @@ -71,20 +64,18 @@ final class PhabricatorProjectDefaultController ->setContinueOnMissingFields(true) ->applyTransactions($project, $xactions); - return id(new AphrontRedirectResponse())->setURI($view_uri); + // If the parameter we just modified is present in the query string, + // throw it away so the user is redirected back to the default view of + // the board, allowing them to see the new default behavior. + $board_uri->removeQueryParam($remove_param); + + return id(new AphrontRedirectResponse())->setURI($board_uri); } - $dialog = $this->newDialog() + return $this->newWorkboardDialog() ->setTitle($title) ->appendChild($body) - ->setDisableWorkflowOnCancel(true) - ->addCancelButton($view_uri) + ->addCancelButton($board_uri) ->addSubmitButton($title); - - foreach ($request->getPassthroughRequestData() as $key => $value) { - $dialog->addHiddenInput($key, $value); - } - - return $dialog; } } diff --git a/src/applications/project/controller/PhabricatorProjectBoardFilterController.php b/src/applications/project/controller/PhabricatorProjectBoardFilterController.php new file mode 100644 index 0000000000..ee69cf3ae1 --- /dev/null +++ b/src/applications/project/controller/PhabricatorProjectBoardFilterController.php @@ -0,0 +1,56 @@ +getViewer(); + + $response = $this->loadProject(); + if ($response) { + return $response; + } + + $project = $this->getProject(); + $state = $this->getViewState(); + $board_uri = $state->newWorkboardURI(); + + $search_engine = $state->getSearchEngine(); + + $is_submit = $request->isFormPost(); + + if ($is_submit) { + $saved_query = $search_engine->buildSavedQueryFromRequest($request); + $search_engine->saveQuery($saved_query); + } else { + $saved_query = $state->getSavedQuery(); + if (!$saved_query) { + return new Aphront404Response(); + } + } + + $filter_form = id(new AphrontFormView()) + ->setUser($viewer); + + $search_engine->buildSearchForm($filter_form, $saved_query); + + $errors = $search_engine->getErrors(); + + if ($is_submit && !$errors) { + $query_key = $saved_query->getQueryKey(); + + $state->setQueryKey($query_key); + $board_uri = $state->newWorkboardURI(); + + return id(new AphrontRedirectResponse())->setURI($board_uri); + } + + return $this->newWorkboardDialog() + ->setWidth(AphrontDialogView::WIDTH_FULL) + ->setTitle(pht('Advanced Filter')) + ->appendChild($filter_form->buildLayoutView()) + ->setErrors($errors) + ->addSubmitButton(pht('Apply Filter')) + ->addCancelButton($board_uri); + } +} diff --git a/src/applications/project/controller/PhabricatorProjectBoardReloadController.php b/src/applications/project/controller/PhabricatorProjectBoardReloadController.php new file mode 100644 index 0000000000..6204671505 --- /dev/null +++ b/src/applications/project/controller/PhabricatorProjectBoardReloadController.php @@ -0,0 +1,73 @@ +getViewer(); + + $response = $this->loadProject(); + if ($response) { + return $response; + } + + $order = $request->getStr('order'); + if (!strlen($order)) { + $order = PhabricatorProjectColumnNaturalOrder::ORDERKEY; + } + + $ordering = PhabricatorProjectColumnOrder::getOrderByKey($order); + $ordering = id(clone $ordering) + ->setViewer($viewer); + + $project = $this->getProject(); + $state = $this->getViewState(); + $board_uri = $state->newWorkboardURI(); + + $layout_engine = $state->getLayoutEngine(); + + $board_phid = $project->getPHID(); + + $objects = $state->getObjects(); + $objects = mpull($objects, null, 'getPHID'); + + try { + $client_state = $request->getStr('state'); + $client_state = phutil_json_decode($client_state); + } catch (PhutilJSONParserException $ex) { + $client_state = array(); + } + + // Figure out which objects need to be updated: either the client has an + // out-of-date version of them (objects which have been edited); or they + // exist on the client but not on the server (objects which have been + // removed from the board); or they exist on the server but not on the + // client (objects which have been added to the board). + + $update_objects = array(); + foreach ($objects as $object_phid => $object) { + + // TODO: For now, this is always hard-coded. + $object_version = 2; + + $client_version = idx($client_state, $object_phid, 0); + if ($object_version > $client_version) { + $update_objects[$object_phid] = $object; + } + } + + $update_phids = array_keys($update_objects); + $visible_phids = array_keys($client_state); + + $engine = id(new PhabricatorBoardResponseEngine()) + ->setViewer($viewer) + ->setBoardPHID($board_phid) + ->setOrdering($ordering) + ->setObjects($objects) + ->setUpdatePHIDs($update_phids) + ->setVisiblePHIDs($visible_phids); + + return $engine->buildResponse(); + } + +} diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 8ceb576b6f..13a75c5a73 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -3,14 +3,6 @@ final class PhabricatorProjectBoardViewController extends PhabricatorProjectBoardController { - const BATCH_EDIT_ALL = 'all'; - - private $id; - private $slug; - private $queryKey; - private $sortKey; - private $showHidden; - public function shouldAllowPublic() { return true; } @@ -24,120 +16,25 @@ final class PhabricatorProjectBoardViewController } $project = $this->getProject(); + $state = $this->getViewState(); + $board_uri = $project->getWorkboardURI(); - $this->readRequestState(); - - $board_uri = $this->getApplicationURI('board/'.$project->getID().'/'); - - $search_engine = id(new ManiphestTaskSearchEngine()) - ->setViewer($viewer) - ->setBaseURI($board_uri) - ->setIsBoardView(true); - - if ($request->isFormPost() - && !$request->getBool('initialize') - && !$request->getStr('move') - && !$request->getStr('queryColumnID')) { - $saved = $search_engine->buildSavedQueryFromRequest($request); - $search_engine->saveQuery($saved); - $filter_form = id(new AphrontFormView()) - ->setUser($viewer); - $search_engine->buildSearchForm($filter_form, $saved); - if ($search_engine->getErrors()) { - return $this->newDialog() - ->setWidth(AphrontDialogView::WIDTH_FULL) - ->setTitle(pht('Advanced Filter')) - ->appendChild($filter_form->buildLayoutView()) - ->setErrors($search_engine->getErrors()) - ->setSubmitURI($board_uri) - ->addSubmitButton(pht('Apply Filter')) - ->addCancelButton($board_uri); - } - return id(new AphrontRedirectResponse())->setURI( - $this->getURIWithState( - $search_engine->getQueryResultsPageURI($saved->getQueryKey()))); + $search_engine = $state->getSearchEngine(); + $query_key = $state->getQueryKey(); + $saved = $state->getSavedQuery(); + if (!$saved) { + return new Aphront404Response(); } - $query_key = $this->getDefaultFilter($project); - - $request_query = $request->getStr('filter'); - if (strlen($request_query)) { - $query_key = $request_query; - } - - $uri_query = $request->getURIData('queryKey'); - if (strlen($uri_query)) { - $query_key = $uri_query; - } - - $this->queryKey = $query_key; - - $custom_query = null; - 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(); - } - + if ($saved->getID()) { $custom_query = $saved; + } else { + $custom_query = null; } - if ($request->getURIData('filter')) { - $filter_form = id(new AphrontFormView()) - ->setUser($viewer); - $search_engine->buildSearchForm($filter_form, $saved); - - return $this->newDialog() - ->setWidth(AphrontDialogView::WIDTH_FULL) - ->setTitle(pht('Advanced Filter')) - ->appendChild($filter_form->buildLayoutView()) - ->setSubmitURI($board_uri) - ->addSubmitButton(pht('Apply Filter')) - ->addCancelButton($board_uri); - } - - $task_query = $search_engine->buildQueryFromSavedQuery($saved); - - $select_phids = array($project->getPHID()); - if ($project->getHasSubprojects() || $project->getHasMilestones()) { - $descendants = id(new PhabricatorProjectQuery()) - ->setViewer($viewer) - ->withAncestorProjectPHIDs($select_phids) - ->execute(); - foreach ($descendants as $descendant) { - $select_phids[] = $descendant->getPHID(); - } - } - - $tasks = $task_query - ->withEdgeLogicPHIDs( - PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, - PhabricatorQueryConstraint::OPERATOR_ANCESTOR, - array($select_phids)) - ->setOrder(ManiphestTaskQuery::ORDER_PRIORITY) - ->setViewer($viewer) - ->execute(); - $tasks = mpull($tasks, null, 'getPHID'); + $layout_engine = $state->getLayoutEngine(); $board_phid = $project->getPHID(); - - // Regardless of display order, pass tasks to the layout engine in ID order - // so layout is consistent. - $board_tasks = msort($tasks, 'getID'); - - $layout_engine = id(new PhabricatorBoardLayoutEngine()) - ->setViewer($viewer) - ->setBoardPHIDs(array($board_phid)) - ->setObjectPHIDs(array_keys($board_tasks)) - ->setFetchAllBoards(true) - ->executeLayout(); - $columns = $layout_engine->getColumns($board_phid); if (!$columns || !$project->getHasWorkboard()) { $has_normal_columns = false; @@ -190,307 +87,13 @@ final class PhabricatorProjectBoardViewController ->appendChild($content); } - // If the user wants to turn a particular column into a query, build an - // apropriate filter and redirect them to the query results page. - $query_column_id = $request->getInt('queryColumnID'); - if ($query_column_id) { - $column_id_map = mpull($columns, null, 'getID'); - $query_column = idx($column_id_map, $query_column_id); - if (!$query_column) { - return new Aphront404Response(); - } - - // Create a saved query to combine the active filter on the workboard - // with the column filter. If the user currently has constraints on the - // board, we want to add a new column or project constraint, not - // completely replace the constraints. - $saved_query = $saved->newCopy(); - - if ($query_column->getProxyPHID()) { - $project_phids = $saved_query->getParameter('projectPHIDs'); - if (!$project_phids) { - $project_phids = array(); - } - $project_phids[] = $query_column->getProxyPHID(); - $saved_query->setParameter('projectPHIDs', $project_phids); - } else { - $saved_query->setParameter( - 'columnPHIDs', - array($query_column->getPHID())); - } - - $search_engine = id(new ManiphestTaskSearchEngine()) - ->setViewer($viewer); - $search_engine->saveQuery($saved_query); - - $query_key = $saved_query->getQueryKey(); - $query_uri = new PhutilURI("/maniphest/query/{$query_key}/#R"); - - return id(new AphrontRedirectResponse()) - ->setURI($query_uri); - } + $tasks = $state->getObjects(); $task_can_edit_map = id(new PhabricatorPolicyFilter()) ->setViewer($viewer) ->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT)) ->apply($tasks); - // If this is a batch edit, select the editable tasks in the chosen column - // and ship the user into the batch editor. - $batch_edit = $request->getStr('batch'); - if ($batch_edit) { - if ($batch_edit !== self::BATCH_EDIT_ALL) { - $column_id_map = mpull($columns, null, 'getID'); - $batch_column = idx($column_id_map, $batch_edit); - if (!$batch_column) { - return new Aphront404Response(); - } - - $batch_task_phids = $layout_engine->getColumnObjectPHIDs( - $board_phid, - $batch_column->getPHID()); - - foreach ($batch_task_phids as $key => $batch_task_phid) { - if (empty($task_can_edit_map[$batch_task_phid])) { - unset($batch_task_phids[$key]); - } - } - - $batch_tasks = array_select_keys($tasks, $batch_task_phids); - } else { - $batch_tasks = $task_can_edit_map; - } - - if (!$batch_tasks) { - $cancel_uri = $this->getURIWithState($board_uri); - return $this->newDialog() - ->setTitle(pht('No Editable Tasks')) - ->appendParagraph( - pht( - 'The selected column contains no visible tasks which you '. - 'have permission to edit.')) - ->addCancelButton($board_uri); - } - - // 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->replaceQueryParam('board', $this->id); - - return id(new AphrontRedirectResponse()) - ->setURI($bulk_uri); - } - - $move_id = $request->getStr('move'); - if (strlen($move_id)) { - $column_id_map = mpull($columns, null, 'getID'); - $move_column = idx($column_id_map, $move_id); - if (!$move_column) { - return new Aphront404Response(); - } - - $move_task_phids = $layout_engine->getColumnObjectPHIDs( - $board_phid, - $move_column->getPHID()); - - foreach ($move_task_phids as $key => $move_task_phid) { - if (empty($task_can_edit_map[$move_task_phid])) { - unset($move_task_phids[$key]); - } - } - - $move_tasks = array_select_keys($tasks, $move_task_phids); - $cancel_uri = $this->getURIWithState($board_uri); - - if (!$move_tasks) { - return $this->newDialog() - ->setTitle(pht('No Movable Tasks')) - ->appendParagraph( - pht( - 'The selected column contains no visible tasks which you '. - 'have permission to move.')) - ->addCancelButton($cancel_uri); - } - - $move_project_phid = $project->getPHID(); - $move_column_phid = null; - $move_project = null; - $move_column = null; - $columns = null; - $errors = array(); - - if ($request->isFormOrHiSecPost()) { - $move_project_phid = head($request->getArr('moveProjectPHID')); - if (!$move_project_phid) { - $move_project_phid = $request->getStr('moveProjectPHID'); - } - - if (!$move_project_phid) { - if ($request->getBool('hasProject')) { - $errors[] = pht('Choose a project to move tasks to.'); - } - } else { - $target_project = id(new PhabricatorProjectQuery()) - ->setViewer($viewer) - ->withPHIDs(array($move_project_phid)) - ->executeOne(); - if (!$target_project) { - $errors[] = pht('You must choose a valid project.'); - } else if (!$project->getHasWorkboard()) { - $errors[] = pht( - 'You must choose a project with a workboard.'); - } else { - $move_project = $target_project; - } - } - - if ($move_project) { - $move_engine = id(new PhabricatorBoardLayoutEngine()) - ->setViewer($viewer) - ->setBoardPHIDs(array($move_project->getPHID())) - ->setFetchAllBoards(true) - ->executeLayout(); - - $columns = $move_engine->getColumns($move_project->getPHID()); - $columns = mpull($columns, null, 'getPHID'); - - foreach ($columns as $key => $column) { - if ($column->isHidden()) { - unset($columns[$key]); - } - } - - $move_column_phid = $request->getStr('moveColumnPHID'); - if (!$move_column_phid) { - if ($request->getBool('hasColumn')) { - $errors[] = pht('Choose a column to move tasks to.'); - } - } else { - if (empty($columns[$move_column_phid])) { - $errors[] = pht( - 'Choose a valid column on the target workboard to move '. - 'tasks to.'); - } else if ($columns[$move_column_phid]->getID() == $move_id) { - $errors[] = pht( - 'You can not move tasks from a column to itself.'); - } else { - $move_column = $columns[$move_column_phid]; - } - } - } - } - - if ($move_column && $move_project) { - foreach ($move_tasks as $move_task) { - $xactions = array(); - - // If we're switching projects, get out of the old project first - // and move to the new project. - if ($move_project->getID() != $project->getID()) { - $xactions[] = id(new ManiphestTransaction()) - ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) - ->setMetadataValue( - 'edge:type', - PhabricatorProjectObjectHasProjectEdgeType::EDGECONST) - ->setNewValue( - array( - '-' => array( - $project->getPHID() => $project->getPHID(), - ), - '+' => array( - $move_project->getPHID() => $move_project->getPHID(), - ), - )); - } - - $xactions[] = id(new ManiphestTransaction()) - ->setTransactionType(PhabricatorTransactions::TYPE_COLUMNS) - ->setNewValue( - array( - array( - 'columnPHID' => $move_column->getPHID(), - ), - )); - - $editor = id(new ManiphestTransactionEditor()) - ->setActor($viewer) - ->setContinueOnMissingFields(true) - ->setContinueOnNoEffect(true) - ->setContentSourceFromRequest($request) - ->setCancelURI($cancel_uri); - - $editor->applyTransactions($move_task, $xactions); - } - - return id(new AphrontRedirectResponse()) - ->setURI($cancel_uri); - } - - if ($move_project) { - $column_form = id(new AphrontFormView()) - ->setViewer($viewer) - ->appendControl( - id(new AphrontFormSelectControl()) - ->setName('moveColumnPHID') - ->setLabel(pht('Move to Column')) - ->setValue($move_column_phid) - ->setOptions(mpull($columns, 'getDisplayName', 'getPHID'))); - - return $this->newDialog() - ->setTitle(pht('Move Tasks')) - ->setWidth(AphrontDialogView::WIDTH_FORM) - ->setErrors($errors) - ->addHiddenInput('move', $move_id) - ->addHiddenInput('moveProjectPHID', $move_project->getPHID()) - ->addHiddenInput('hasColumn', true) - ->addHiddenInput('hasProject', true) - ->appendParagraph( - pht( - 'Choose a column on the %s workboard to move tasks to:', - $viewer->renderHandle($move_project->getPHID()))) - ->appendForm($column_form) - ->addSubmitButton(pht('Move Tasks')) - ->addCancelButton($cancel_uri); - } - - if ($move_project_phid) { - $move_project_phid_value = array($move_project_phid); - } else { - $move_project_phid_value = array(); - } - - $project_form = id(new AphrontFormView()) - ->setViewer($viewer) - ->appendControl( - id(new AphrontFormTokenizerControl()) - ->setName('moveProjectPHID') - ->setLimit(1) - ->setLabel(pht('Move to Project')) - ->setValue($move_project_phid_value) - ->setDatasource(new PhabricatorProjectDatasource())); - - return $this->newDialog() - ->setTitle(pht('Move Tasks')) - ->setWidth(AphrontDialogView::WIDTH_FORM) - ->setErrors($errors) - ->addHiddenInput('move', $move_id) - ->addHiddenInput('hasProject', true) - ->appendForm($project_form) - ->addSubmitButton(pht('Continue')) - ->addCancelButton($cancel_uri); - } - - $board_id = celerity_generate_unique_node_id(); $board = id(new PHUIWorkboardView()) @@ -506,7 +109,7 @@ final class PhabricatorProjectBoardViewController $column_phids = array(); $visible_phids = array(); foreach ($columns as $column) { - if (!$this->showHidden) { + if (!$state->getShowHidden()) { if ($column->isHidden()) { continue; } @@ -534,11 +137,13 @@ final class PhabricatorProjectBoardViewController } } + $container_phids = $state->getBoardContainerPHIDs(); + $rendering_engine = id(new PhabricatorBoardRenderingEngine()) ->setViewer($viewer) ->setObjects(array_select_keys($tasks, $visible_phids)) ->setEditMap($task_can_edit_map) - ->setExcludedProjectPHIDs($select_phids); + ->setExcludedProjectPHIDs($container_phids); $templates = array(); $all_tasks = array(); @@ -651,7 +256,7 @@ final class PhabricatorProjectBoardViewController ); } - $order_key = $this->sortKey; + $order_key = $state->getOrder(); $ordering_map = PhabricatorProjectColumnOrder::getEnabledOrders(); $ordering = id(clone $ordering_map[$order_key]) @@ -681,11 +286,12 @@ final class PhabricatorProjectBoardViewController 'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'), 'uploadURI' => '/file/dropupload/', 'coverURI' => $this->getApplicationURI('cover/'), + 'reloadURI' => phutil_string_cast($state->newWorkboardURI('reload/')), 'chunkThreshold' => PhabricatorFileStorageEngine::getChunkThreshold(), 'pointsEnabled' => ManiphestTaskPoints::getIsEnabled(), 'boardPHID' => $project->getPHID(), - 'order' => $this->sortKey, + 'order' => $state->getOrder(), 'orders' => $order_maps, 'headers' => $headers, 'headerKeys' => $header_keys, @@ -703,7 +309,7 @@ final class PhabricatorProjectBoardViewController $sort_menu = $this->buildSortMenu( $viewer, $project, - $this->sortKey, + $state->getOrder(), $ordering_map); $filter_menu = $this->buildFilterMenu( @@ -713,7 +319,7 @@ final class PhabricatorProjectBoardViewController $search_engine, $query_key); - $manage_menu = $this->buildManageMenu($project, $this->showHidden); + $manage_menu = $this->buildManageMenu($project, $state->getShowHidden()); $header_link = phutil_tag( 'a', @@ -777,55 +383,14 @@ final class PhabricatorProjectBoardViewController return $page; } - private function readRequestState() { - $request = $this->getRequest(); - $project = $this->getProject(); - - $this->showHidden = $request->getBool('hidden'); - $this->id = $project->getID(); - - $sort_key = $this->getDefaultSort($project); - - $request_sort = $request->getStr('order'); - if ($this->isValidSort($request_sort)) { - $sort_key = $request_sort; - } - - $this->sortKey = $sort_key; - } - - private function getDefaultSort(PhabricatorProject $project) { - $default_sort = $project->getDefaultWorkboardSort(); - - if ($this->isValidSort($default_sort)) { - return $default_sort; - } - - return PhabricatorProjectColumnNaturalOrder::ORDERKEY; - } - - private function getDefaultFilter(PhabricatorProject $project) { - $default_filter = $project->getDefaultWorkboardFilter(); - - if (strlen($default_filter)) { - return $default_filter; - } - - return 'open'; - } - - private function isValidSort($sort) { - $map = PhabricatorProjectColumnOrder::getEnabledOrders(); - return isset($map[$sort]); - } - private function buildSortMenu( PhabricatorUser $viewer, PhabricatorProject $project, $sort_key, array $ordering_map) { - $base_uri = $this->getURIWithState(); + $state = $this->getViewState(); + $base_uri = $state->newWorkboardURI(); $items = array(); foreach ($ordering_map as $key => $ordering) { @@ -855,9 +420,7 @@ final class PhabricatorProjectBoardViewController $id = $project->getID(); - $save_uri = "default/{$id}/sort/"; - $save_uri = $this->getApplicationURI($save_uri); - $save_uri = $this->getURIWithState($save_uri, $force = true); + $save_uri = $state->newWorkboardURI('default/sort/'); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, @@ -900,6 +463,8 @@ final class PhabricatorProjectBoardViewController PhabricatorApplicationSearchEngine $engine, $query_key) { + $state = $this->getViewState(); + $named = array( 'open' => pht('Open Tasks'), 'all' => pht('All Tasks'), @@ -931,24 +496,26 @@ final class PhabricatorProjectBoardViewController ->setName($name); if ($is_custom) { - $uri = $this->getApplicationURI( - 'board/'.$this->id.'/filter/query/'.$key.'/'); + // When you're using a custom filter already and you select "Custom + // Filter", you get a dialog back to let you edit the filter. This is + // equivalent to selecting "Advanced Filter..." to configure a new + // filter. + $filter_uri = $state->newWorkboardURI('filter/'); $item->setWorkflow(true); } else { - $uri = $engine->getQueryResultsPageURI($key); + $filter_uri = urisprintf('query/%s/', $key); + $filter_uri = $state->newWorkboardURI($filter_uri); + $filter_uri->removeQueryParam('filter'); } - $uri = $this->getURIWithState($uri) - ->removeQueryParam('filter'); - $item->setHref($uri); + $item->setHref($filter_uri); $items[] = $item; } $id = $project->getID(); - $filter_uri = $this->getApplicationURI("board/{$id}/filter/"); - $filter_uri = $this->getURIWithState($filter_uri, $force = true); + $filter_uri = $state->newWorkboardURI('filter/'); $items[] = id(new PhabricatorActionView()) ->setIcon('fa-cog') @@ -956,9 +523,7 @@ final class PhabricatorProjectBoardViewController ->setWorkflow(true) ->setName(pht('Advanced Filter...')); - $save_uri = "default/{$id}/filter/"; - $save_uri = $this->getApplicationURI($save_uri); - $save_uri = $this->getURIWithState($save_uri, $force = true); + $save_uri = $state->newWorkboardURI('default/filter/'); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, @@ -1000,6 +565,7 @@ final class PhabricatorProjectBoardViewController $request = $this->getRequest(); $viewer = $request->getUser(); + $state = $this->getViewState(); $id = $project->getID(); @@ -1029,12 +595,12 @@ final class PhabricatorProjectBoardViewController ->setWorkflow(true); if ($show_hidden) { - $hidden_uri = $this->getURIWithState() + $hidden_uri = $state->newWorkboardURI() ->removeQueryParam('hidden'); $hidden_icon = 'fa-eye-slash'; $hidden_text = pht('Hide Hidden Columns'); } else { - $hidden_uri = $this->getURIWithState() + $hidden_uri = $state->newWorkboardURI() ->replaceQueryParam('hidden', 'true'); $hidden_icon = 'fa-eye'; $hidden_text = pht('Show Hidden Columns'); @@ -1062,13 +628,6 @@ final class PhabricatorProjectBoardViewController ->setName(pht('Manage Workboard')) ->setHref($manage_uri); - $batch_edit_uri = $request->getRequestURI(); - $batch_edit_uri->replaceQueryParam('batch', self::BATCH_EDIT_ALL); - $can_batch_edit = PhabricatorPolicyFilter::hasCapability( - $viewer, - PhabricatorApplication::getByClass('PhabricatorManiphestApplication'), - ManiphestBulkEditCapability::CAPABILITY); - $manage_menu = id(new PhabricatorActionListView()) ->setUser($viewer); foreach ($manage_items as $item) { @@ -1114,6 +673,7 @@ final class PhabricatorProjectBoardViewController $request = $this->getRequest(); $viewer = $request->getUser(); + $state = $this->getViewState(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, @@ -1151,36 +711,51 @@ final class PhabricatorProjectBoardViewController $column_items[] = id(new PhabricatorActionView()) ->setType(PhabricatorActionView::TYPE_DIVIDER); - $batch_edit_uri = $request->getRequestURI(); - $batch_edit_uri->replaceQueryParam('batch', $column->getID()); - $can_batch_edit = PhabricatorPolicyFilter::hasCapability( + $query_uri = urisprintf('viewquery/%d/', $column->getID()); + $query_uri = $state->newWorkboardURI($query_uri); + + $column_items[] = id(new PhabricatorActionView()) + ->setName(pht('View Tasks as Query')) + ->setIcon('fa-search') + ->setHref($query_uri); + + $column_move_uri = urisprintf('bulkmove/%d/column/', $column->getID()); + $column_move_uri = $state->newWorkboardURI($column_move_uri); + + $column_items[] = id(new PhabricatorActionView()) + ->setIcon('fa-arrows-h') + ->setName(pht('Move Tasks to Column...')) + ->setHref($column_move_uri) + ->setWorkflow(true); + + $project_move_uri = urisprintf('bulkmove/%d/project/', $column->getID()); + $project_move_uri = $state->newWorkboardURI($project_move_uri); + + $column_items[] = id(new PhabricatorActionView()) + ->setIcon('fa-arrows') + ->setName(pht('Move Tasks to Project...')) + ->setHref($project_move_uri) + ->setWorkflow(true); + + $bulk_edit_uri = urisprintf('bulk/%d/', $column->getID()); + $bulk_edit_uri = $state->newWorkboardURI($bulk_edit_uri); + + $can_bulk_edit = PhabricatorPolicyFilter::hasCapability( $viewer, PhabricatorApplication::getByClass('PhabricatorManiphestApplication'), ManiphestBulkEditCapability::CAPABILITY); $column_items[] = id(new PhabricatorActionView()) - ->setIcon('fa-list-ul') + ->setIcon('fa-pencil-square-o') ->setName(pht('Bulk Edit Tasks...')) - ->setHref($batch_edit_uri) - ->setDisabled(!$can_batch_edit); - - $batch_move_uri = $request->getRequestURI(); - $batch_move_uri->replaceQueryParam('move', $column->getID()); - $column_items[] = id(new PhabricatorActionView()) - ->setIcon('fa-arrow-right') - ->setName(pht('Move Tasks to Column...')) - ->setHref($batch_move_uri) - ->setWorkflow(true); - - $query_uri = $request->getRequestURI(); - $query_uri->replaceQueryParam('queryColumnID', $column->getID()); + ->setHref($bulk_edit_uri) + ->setDisabled(!$can_bulk_edit); $column_items[] = id(new PhabricatorActionView()) - ->setName(pht('View as Query')) - ->setIcon('fa-search') - ->setHref($query_uri); + ->setType(PhabricatorActionView::TYPE_DIVIDER); - $edit_uri = 'board/'.$this->id.'/edit/'.$column->getID().'/'; + + $edit_uri = 'board/'.$project->getID().'/edit/'.$column->getID().'/'; $column_items[] = id(new PhabricatorActionView()) ->setName(pht('Edit Column')) ->setIcon('fa-pencil') @@ -1189,9 +764,9 @@ final class PhabricatorProjectBoardViewController ->setWorkflow(true); $can_hide = ($can_edit && !$column->isDefaultColumn()); - $hide_uri = 'board/'.$this->id.'/hide/'.$column->getID().'/'; - $hide_uri = $this->getApplicationURI($hide_uri); - $hide_uri = $this->getURIWithState($hide_uri); + + $hide_uri = urisprintf('hide/%d/', $column->getID()); + $hide_uri = $state->newWorkboardURI($hide_uri); if (!$column->isHidden()) { $column_items[] = id(new PhabricatorActionView()) @@ -1297,56 +872,6 @@ final class PhabricatorProjectBoardViewController return $trigger_button; } - /** - * Add current state parameters (like order and the visibility of hidden - * columns) to a URI. - * - * This allows actions which toggle or adjust one piece of state to keep - * the rest of the board state persistent. If no URI is provided, this method - * starts with the request URI. - * - * @param string|null URI to add state parameters to. - * @param bool True to explicitly include all state. - * @return PhutilURI URI with state parameters. - */ - private function getURIWithState($base = null, $force = false) { - $project = $this->getProject(); - - if ($base === null) { - $base = $this->getRequest()->getPath(); - } - - $base = new PhutilURI($base); - - if ($force || ($this->sortKey != $this->getDefaultSort($project))) { - if ($this->sortKey !== null) { - $base->replaceQueryParam('order', $this->sortKey); - } else { - $base->removeQueryParam('order'); - } - } else { - $base->removeQueryParam('order'); - } - - if ($force || ($this->queryKey != $this->getDefaultFilter($project))) { - if ($this->queryKey !== null) { - $base->replaceQueryParam('filter', $this->queryKey); - } else { - $base->removeQueryParam('filter'); - } - } else { - $base->removeQueryParam('filter'); - } - - if ($this->showHidden) { - $base->replaceQueryParam('hidden', 'true'); - } else { - $base->removeQueryParam('hidden'); - } - - return $base; - } - private function buildInitializeContent(PhabricatorProject $project) { $request = $this->getRequest(); $viewer = $this->getViewer(); diff --git a/src/applications/project/controller/PhabricatorProjectColumnBulkEditController.php b/src/applications/project/controller/PhabricatorProjectColumnBulkEditController.php new file mode 100644 index 0000000000..0d9c2ff78f --- /dev/null +++ b/src/applications/project/controller/PhabricatorProjectColumnBulkEditController.php @@ -0,0 +1,72 @@ +getViewer(); + + $response = $this->loadProject(); + if ($response) { + return $response; + } + + $project = $this->getProject(); + $state = $this->getViewState(); + $board_uri = $state->newWorkboardURI(); + + $layout_engine = $state->getLayoutEngine(); + + $board_phid = $project->getPHID(); + $columns = $layout_engine->getColumns($board_phid); + $columns = mpull($columns, null, 'getID'); + + $column_id = $request->getURIData('columnID'); + $bulk_column = idx($columns, $column_id); + if (!$bulk_column) { + return new Aphront404Response(); + } + + $bulk_task_phids = $layout_engine->getColumnObjectPHIDs( + $board_phid, + $bulk_column->getPHID()); + + $tasks = $state->getObjects(); + + $bulk_tasks = array_select_keys($tasks, $bulk_task_phids); + + $bulk_tasks = id(new PhabricatorPolicyFilter()) + ->setViewer($viewer) + ->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT)) + ->apply($bulk_tasks); + + if (!$bulk_tasks) { + return $this->newDialog() + ->setTitle(pht('No Editable Tasks')) + ->appendParagraph( + pht( + 'The selected column contains no visible tasks which you '. + 'have permission to edit.')) + ->addCancelButton($board_uri); + } + + // 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($bulk_tasks, 'getID')); + $search_engine->saveQuery($saved_query); + + $query_key = $saved_query->getQueryKey(); + + $bulk_uri = new PhutilURI("/maniphest/bulk/query/{$query_key}/"); + $bulk_uri->replaceQueryParam('board', $project->getID()); + + return id(new AphrontRedirectResponse()) + ->setURI($bulk_uri); + } + +} diff --git a/src/applications/project/controller/PhabricatorProjectColumnBulkMoveController.php b/src/applications/project/controller/PhabricatorProjectColumnBulkMoveController.php new file mode 100644 index 0000000000..0931e4f7be --- /dev/null +++ b/src/applications/project/controller/PhabricatorProjectColumnBulkMoveController.php @@ -0,0 +1,301 @@ +getViewer(); + + $response = $this->loadProject(); + if ($response) { + return $response; + } + + // See T13316. If we're operating in "column" mode, we're going to skip + // the prompt for a project and just have the user select a target column. + // In "project" mode, we prompt them for a project first. + $is_column_mode = ($request->getURIData('mode') === 'column'); + + $src_project = $this->getProject(); + $state = $this->getViewState(); + $board_uri = $state->newWorkboardURI(); + + $layout_engine = $state->getLayoutEngine(); + + $board_phid = $src_project->getPHID(); + $columns = $layout_engine->getColumns($board_phid); + $columns = mpull($columns, null, 'getID'); + + $column_id = $request->getURIData('columnID'); + $src_column = idx($columns, $column_id); + if (!$src_column) { + return new Aphront404Response(); + } + + $move_task_phids = $layout_engine->getColumnObjectPHIDs( + $board_phid, + $src_column->getPHID()); + + $tasks = $state->getObjects(); + + $move_tasks = array_select_keys($tasks, $move_task_phids); + + $move_tasks = id(new PhabricatorPolicyFilter()) + ->setViewer($viewer) + ->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT)) + ->apply($move_tasks); + + if (!$move_tasks) { + return $this->newDialog() + ->setTitle(pht('No Movable Tasks')) + ->appendParagraph( + pht( + 'The selected column contains no visible tasks which you '. + 'have permission to move.')) + ->addCancelButton($board_uri); + } + + $dst_project_phid = null; + $dst_project = null; + $has_project = false; + if ($is_column_mode) { + $has_project = true; + $dst_project_phid = $src_project->getPHID(); + } else { + if ($request->isFormOrHiSecPost()) { + $has_project = $request->getStr('hasProject'); + if ($has_project) { + // We may read this from a tokenizer input as an array, or from a + // hidden input as a string. + $dst_project_phid = head($request->getArr('dstProjectPHID')); + if (!$dst_project_phid) { + $dst_project_phid = $request->getStr('dstProjectPHID'); + } + } + } + } + + $errors = array(); + $hidden = array(); + + if ($has_project) { + if (!$dst_project_phid) { + $errors[] = pht('Choose a project to move tasks to.'); + } else { + $dst_project = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withPHIDs(array($dst_project_phid)) + ->executeOne(); + if (!$dst_project) { + $errors[] = pht('Choose a valid project to move tasks to.'); + } + + if (!$dst_project->getHasWorkboard()) { + $errors[] = pht('You must choose a project with a workboard.'); + $dst_project = null; + } + } + } + + if ($dst_project) { + $same_project = ($src_project->getID() === $dst_project->getID()); + + $layout_engine = id(new PhabricatorBoardLayoutEngine()) + ->setViewer($viewer) + ->setBoardPHIDs(array($dst_project->getPHID())) + ->setFetchAllBoards(true) + ->executeLayout(); + + $dst_columns = $layout_engine->getColumns($dst_project->getPHID()); + $dst_columns = mpull($dst_columns, null, 'getPHID'); + + // Prevent moves to milestones or subprojects by selecting their + // columns, since the implications aren't obvious and this doesn't + // work the same way as normal column moves. + foreach ($dst_columns as $key => $dst_column) { + if ($dst_column->getProxyPHID()) { + unset($dst_columns[$key]); + } + } + + $has_column = false; + $dst_column = null; + + // If we're performing a move on the same board, default the + // control value to the current column. + if ($same_project) { + $dst_column_phid = $src_column->getPHID(); + } else { + $dst_column_phid = null; + } + + if ($request->isFormOrHiSecPost()) { + $has_column = $request->getStr('hasColumn'); + if ($has_column) { + $dst_column_phid = $request->getStr('dstColumnPHID'); + } + } + + if ($has_column) { + $dst_column = idx($dst_columns, $dst_column_phid); + if (!$dst_column) { + $errors[] = pht('Choose a column to move tasks to.'); + } else { + if ($dst_column->isHidden()) { + $errors[] = pht('You can not move tasks to a hidden column.'); + $dst_column = null; + } else if ($dst_column->getPHID() === $src_column->getPHID()) { + $errors[] = pht('You can not move tasks from a column to itself.'); + $dst_column = null; + } + } + } + + if ($dst_column) { + foreach ($move_tasks as $move_task) { + $xactions = array(); + + // If we're switching projects, get out of the old project first + // and move to the new project. + if (!$same_project) { + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue( + 'edge:type', + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST) + ->setNewValue( + array( + '-' => array( + $src_project->getPHID() => $src_project->getPHID(), + ), + '+' => array( + $dst_project->getPHID() => $dst_project->getPHID(), + ), + )); + } + + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_COLUMNS) + ->setNewValue( + array( + array( + 'columnPHID' => $dst_column->getPHID(), + ), + )); + + $editor = id(new ManiphestTransactionEditor()) + ->setActor($viewer) + ->setContinueOnMissingFields(true) + ->setContinueOnNoEffect(true) + ->setContentSourceFromRequest($request) + ->setCancelURI($board_uri); + + $editor->applyTransactions($move_task, $xactions); + } + + // If we did a move on the same workboard, redirect and preserve the + // state parameters. If we moved to a different workboard, go there + // with clean default state. + if ($same_project) { + $done_uri = $board_uri; + } else { + $done_uri = $dst_project->getWorkboardURI(); + } + + return id(new AphrontRedirectResponse())->setURI($done_uri); + } + + $title = pht('Move Tasks to Column'); + + $form = id(new AphrontFormView()) + ->setViewer($viewer); + + // If we're moving between projects, add a reminder about which project + // you selected in the previous step. + if (!$is_column_mode) { + $form->appendControl( + id(new AphrontFormStaticControl()) + ->setLabel(pht('Project')) + ->setValue($dst_project->getDisplayName())); + } + + $column_options = array( + 'visible' => array(), + 'hidden' => array(), + ); + + $any_hidden = false; + foreach ($dst_columns as $column) { + if (!$column->isHidden()) { + $group = 'visible'; + } else { + $group = 'hidden'; + } + + $phid = $column->getPHID(); + $display_name = $column->getDisplayName(); + + $column_options[$group][$phid] = $display_name; + } + + if ($column_options['hidden']) { + $column_options = array( + pht('Visible Columns') => $column_options['visible'], + pht('Hidden Columns') => $column_options['hidden'], + ); + } else { + $column_options = $column_options['visible']; + } + + $form->appendControl( + id(new AphrontFormSelectControl()) + ->setName('dstColumnPHID') + ->setLabel(pht('Move to Column')) + ->setValue($dst_column_phid) + ->setOptions($column_options)); + + $submit = pht('Move Tasks'); + + $hidden['dstProjectPHID'] = $dst_project->getPHID(); + $hidden['hasColumn'] = true; + $hidden['hasProject'] = true; + } else { + $title = pht('Move Tasks to Project'); + + if ($dst_project_phid) { + $dst_project_phid_value = array($dst_project_phid); + } else { + $dst_project_phid_value = array(); + } + + $form = id(new AphrontFormView()) + ->setViewer($viewer) + ->appendControl( + id(new AphrontFormTokenizerControl()) + ->setName('dstProjectPHID') + ->setLimit(1) + ->setLabel(pht('Move to Project')) + ->setValue($dst_project_phid_value) + ->setDatasource(new PhabricatorProjectDatasource())); + + $submit = pht('Continue'); + + $hidden['hasProject'] = true; + } + + $dialog = $this->newWorkboardDialog() + ->setWidth(AphrontDialogView::WIDTH_FORM) + ->setTitle($title) + ->setErrors($errors) + ->appendForm($form) + ->addSubmitButton($submit) + ->addCancelButton($board_uri); + + foreach ($hidden as $key => $value) { + $dialog->addHiddenInput($key, $value); + } + + return $dialog; + } + +} diff --git a/src/applications/project/controller/PhabricatorProjectColumnViewQueryController.php b/src/applications/project/controller/PhabricatorProjectColumnViewQueryController.php new file mode 100644 index 0000000000..7b8a304daa --- /dev/null +++ b/src/applications/project/controller/PhabricatorProjectColumnViewQueryController.php @@ -0,0 +1,72 @@ +getViewer(); + + $response = $this->loadProject(); + if ($response) { + return $response; + } + + $project = $this->getProject(); + $state = $this->getViewState(); + $board_uri = $state->newWorkboardURI(); + + // NOTE: We're performing layout without handing the "LayoutEngine" any + // object PHIDs. We only want to get access to the column object the user + // is trying to query, so we do not need to actually position any cards on + // the board. + + $board_phid = $project->getPHID(); + + $layout_engine = id(new PhabricatorBoardLayoutEngine()) + ->setViewer($viewer) + ->setBoardPHIDs(array($board_phid)) + ->setFetchAllBoards(true) + ->executeLayout(); + + $columns = $layout_engine->getColumns($board_phid); + $columns = mpull($columns, null, 'getID'); + + $column_id = $request->getURIData('columnID'); + $column = idx($columns, $column_id); + if (!$column) { + return new Aphront404Response(); + } + + // Create a saved query to combine the active filter on the workboard + // with the column filter. If the user currently has constraints on the + // board, we want to add a new column or project constraint, not + // completely replace the constraints. + $default_query = $state->getSavedQuery(); + $saved_query = $default_query->newCopy(); + + if ($column->getProxyPHID()) { + $project_phids = $saved_query->getParameter('projectPHIDs'); + if (!$project_phids) { + $project_phids = array(); + } + $project_phids[] = $column->getProxyPHID(); + $saved_query->setParameter('projectPHIDs', $project_phids); + } else { + $saved_query->setParameter( + 'columnPHIDs', + array($column->getPHID())); + } + + $search_engine = id(new ManiphestTaskSearchEngine()) + ->setViewer($viewer); + + $search_engine->saveQuery($saved_query); + + $query_key = $saved_query->getQueryKey(); + $query_uri = new PhutilURI("/maniphest/query/{$query_key}/#R"); + + return id(new AphrontRedirectResponse()) + ->setURI($query_uri); + } + +} diff --git a/src/applications/project/controller/PhabricatorProjectController.php b/src/applications/project/controller/PhabricatorProjectController.php index 8551a09cbf..14b4aa151e 100644 --- a/src/applications/project/controller/PhabricatorProjectController.php +++ b/src/applications/project/controller/PhabricatorProjectController.php @@ -16,6 +16,21 @@ abstract class PhabricatorProjectController extends PhabricatorController { } protected function loadProject() { + return $this->loadProjectWithCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + )); + } + + protected function loadProjectForEdit() { + return $this->loadProjectWithCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )); + } + + private function loadProjectWithCapabilities(array $capabilities) { $viewer = $this->getViewer(); $request = $this->getRequest(); @@ -35,6 +50,7 @@ abstract class PhabricatorProjectController extends PhabricatorController { $query = id(new PhabricatorProjectQuery()) ->setViewer($viewer) + ->requireCapabilities($capabilities) ->needMembers(true) ->needWatchers(true) ->needImages(true) @@ -168,7 +184,7 @@ abstract class PhabricatorProjectController extends PhabricatorController { $engine = id(new PhabricatorBoardResponseEngine()) ->setViewer($viewer) ->setBoardPHID($board_phid) - ->setObjectPHID($object_phid) + ->setUpdatePHIDs(array($object_phid)) ->setVisiblePHIDs($visible_phids) ->setSounds($sounds); diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php index 67b94f7fa9..386a649238 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileController.php @@ -78,14 +78,29 @@ final class PhabricatorProjectProfileController $project, PhabricatorProject::ITEM_PROFILE); - $stories = id(new PhabricatorFeedQuery()) + $query = id(new PhabricatorFeedQuery()) ->setViewer($viewer) - ->withFilterPHIDs( - array( - $project->getPHID(), - )) + ->withFilterPHIDs(array($project->getPHID())) ->setLimit(50) - ->execute(); + ->setReturnPartialResultsOnOverheat(true); + + $stories = $query->execute(); + + $overheated_view = null; + $is_overheated = $query->getIsOverheated(); + if ($is_overheated) { + $overheated_message = + PhabricatorApplicationSearchController::newOverheatedError( + (bool)$stories); + + $overheated_view = id(new PHUIInfoView()) + ->setSeverity(PHUIInfoView::SEVERITY_WARNING) + ->setTitle(pht('Query Overheated')) + ->setErrors( + array( + $overheated_message, + )); + } $view_all = id(new PHUIButtonView()) ->setTag('a') @@ -103,7 +118,11 @@ final class PhabricatorProjectProfileController $feed = id(new PHUIObjectBoxView()) ->setHeader($feed_header) ->addClass('project-view-feed') - ->appendChild($feed); + ->appendChild( + array( + $overheated_view, + $feed, + )); require_celerity_resource('project-view-css'); diff --git a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php index e614ec2f94..22aaed2d4b 100644 --- a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php +++ b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php @@ -4,7 +4,7 @@ final class PhabricatorBoardLayoutEngine extends Phobject { private $viewer; private $boardPHIDs; - private $objectPHIDs; + private $objectPHIDs = array(); private $boards; private $columnMap = array(); private $objectColumnMap = array(); @@ -229,7 +229,7 @@ final class PhabricatorBoardLayoutEngine extends Phobject { $this->addQueue[] = $object_position; $positions[$object_phid] = $object_position; - $positions = msort($positions, 'getOrderingKey'); + $positions = msortv($positions, 'newColumnPositionOrderVector'); $this->boardLayout[$board_phid][$column_phid] = $positions; @@ -404,7 +404,7 @@ final class PhabricatorBoardLayoutEngine extends Phobject { ->withBoardPHIDs(array_keys($boards)) ->withObjectPHIDs($object_phids) ->execute(); - $positions = msort($positions, 'getOrderingKey'); + $positions = msortv($positions, 'newColumnPositionOrderVector'); $positions = mgroup($positions, 'getBoardPHID'); return $positions; @@ -581,7 +581,7 @@ final class PhabricatorBoardLayoutEngine extends Phobject { } foreach ($layout as $column_phid => $map) { - $map = msort($map, 'getOrderingKey'); + $map = msortv($map, 'newColumnPositionOrderVector'); $layout[$column_phid] = $map; foreach ($map as $object_phid => $position) { diff --git a/src/applications/project/engine/PhabricatorBoardResponseEngine.php b/src/applications/project/engine/PhabricatorBoardResponseEngine.php index f22254e43a..81e56d2116 100644 --- a/src/applications/project/engine/PhabricatorBoardResponseEngine.php +++ b/src/applications/project/engine/PhabricatorBoardResponseEngine.php @@ -3,9 +3,10 @@ final class PhabricatorBoardResponseEngine extends Phobject { private $viewer; + private $objects; private $boardPHID; - private $objectPHID; - private $visiblePHIDs; + private $visiblePHIDs = array(); + private $updatePHIDs = array(); private $ordering; private $sounds; @@ -27,13 +28,13 @@ final class PhabricatorBoardResponseEngine extends Phobject { return $this->boardPHID; } - public function setObjectPHID($object_phid) { - $this->objectPHID = $object_phid; + public function setObjects(array $objects) { + $this->objects = $objects; return $this; } - public function getObjectPHID() { - return $this->objectPHID; + public function getObjects() { + return $this->objects; } public function setVisiblePHIDs(array $visible_phids) { @@ -45,6 +46,15 @@ final class PhabricatorBoardResponseEngine extends Phobject { return $this->visiblePHIDs; } + public function setUpdatePHIDs(array $update_phids) { + $this->updatePHIDs = $update_phids; + return $this; + } + + public function getUpdatePHIDs() { + return $this->updatePHIDs; + } + public function setOrdering(PhabricatorProjectColumnOrder $ordering) { $this->ordering = $ordering; return $this; @@ -65,42 +75,64 @@ final class PhabricatorBoardResponseEngine extends Phobject { public function buildResponse() { $viewer = $this->getViewer(); - $object_phid = $this->getObjectPHID(); $board_phid = $this->getBoardPHID(); $ordering = $this->getOrdering(); + $update_phids = $this->getUpdatePHIDs(); + $update_phids = array_fuse($update_phids); + + $visible_phids = $this->getVisiblePHIDs(); + $visible_phids = array_fuse($visible_phids); + + $all_phids = $update_phids + $visible_phids; + // Load all the other tasks that are visible in the affected columns and // perform layout for them. - $visible_phids = $this->getAllVisiblePHIDs(); + + if ($this->objects !== null) { + $all_objects = $this->getObjects(); + $all_objects = mpull($all_objects, null, 'getPHID'); + } else { + $all_objects = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withPHIDs($all_phids) + ->execute(); + $all_objects = mpull($all_objects, null, 'getPHID'); + } + + // NOTE: The board layout engine is sensitive to PHID input order, and uses + // the input order as a component of the "natural" column ordering if no + // explicit ordering is specified. Rearrange the PHIDs in ID order. + + $all_objects = msort($all_objects, 'getID'); + $ordered_phids = mpull($all_objects, 'getPHID'); $layout_engine = id(new PhabricatorBoardLayoutEngine()) ->setViewer($viewer) ->setBoardPHIDs(array($board_phid)) - ->setObjectPHIDs($visible_phids) + ->setObjectPHIDs($ordered_phids) ->executeLayout(); - $object_columns = $layout_engine->getObjectColumns( - $board_phid, - $object_phid); - $natural = array(); - foreach ($object_columns as $column_phid => $column) { + + $update_columns = array(); + foreach ($update_phids as $update_phid) { + $update_columns += $layout_engine->getObjectColumns( + $board_phid, + $update_phid); + } + + foreach ($update_columns as $column_phid => $column) { $column_object_phids = $layout_engine->getColumnObjectPHIDs( $board_phid, $column_phid); $natural[$column_phid] = array_values($column_object_phids); } - $all_visible = id(new ManiphestTaskQuery()) - ->setViewer($viewer) - ->withPHIDs($visible_phids) - ->execute(); - $all_visible = mpull($all_visible, null, 'getPHID'); - if ($ordering) { - $vectors = $ordering->getSortVectorsForObjects($all_visible); - $header_keys = $ordering->getHeaderKeysForObjects($all_visible); - $headers = $ordering->getHeadersForObjects($all_visible); + $vectors = $ordering->getSortVectorsForObjects($all_objects); + $header_keys = $ordering->getHeaderKeysForObjects($all_objects); + $headers = $ordering->getHeadersForObjects($all_objects); $headers = mpull($headers, 'toDictionary'); } else { $vectors = array(); @@ -108,19 +140,10 @@ final class PhabricatorBoardResponseEngine extends Phobject { $headers = array(); } - $object = id(new ManiphestTaskQuery()) - ->setViewer($viewer) - ->withPHIDs(array($object_phid)) - ->needProjectPHIDs(true) - ->executeOne(); - if (!$object) { - return new Aphront404Response(); - } - - $template = $this->buildTemplate($object); + $templates = $this->newCardTemplates(); $cards = array(); - foreach ($all_visible as $card_phid => $object) { + foreach ($all_objects as $card_phid => $object) { $card = array( 'vectors' => array(), 'headers' => array(), @@ -144,8 +167,11 @@ final class PhabricatorBoardResponseEngine extends Phobject { $card['properties'] = self::newTaskProperties($object); } - if ($card_phid === $object_phid) { - $card['nodeHTMLTemplate'] = hsprintf('%s', $template); + if (isset($templates[$card_phid])) { + $card['nodeHTMLTemplate'] = hsprintf('%s', $templates[$card_phid]); + $card['update'] = true; + } else { + $card['update'] = false; } $card['vectors'] = (object)$card['vectors']; @@ -155,8 +181,18 @@ final class PhabricatorBoardResponseEngine extends Phobject { $cards[$card_phid] = $card; } + // Mark cards which are currently visible on the client but not visible + // on the board on the server for removal from the client view of the + // board state. + foreach ($visible_phids as $card_phid) { + if (!isset($cards[$card_phid])) { + $cards[$card_phid] = array( + 'remove' => true, + ); + } + } + $payload = array( - 'objectPHID' => $object_phid, 'columnMaps' => $natural, 'cards' => $cards, 'headers' => $headers, @@ -176,22 +212,6 @@ final class PhabricatorBoardResponseEngine extends Phobject { ); } - private function buildTemplate($object) { - $viewer = $this->getViewer(); - $object_phid = $this->getObjectPHID(); - - $excluded_phids = $this->loadExcludedProjectPHIDs(); - - $rendering_engine = id(new PhabricatorBoardRenderingEngine()) - ->setViewer($viewer) - ->setObjects(array($object)) - ->setExcludedProjectPHIDs($excluded_phids); - - $card = $rendering_engine->renderCard($object_phid); - - return hsprintf('%s', $card->getItem()); - } - private function loadExcludedProjectPHIDs() { $viewer = $this->getViewer(); $board_phid = $this->getBoardPHID(); @@ -210,11 +230,50 @@ final class PhabricatorBoardResponseEngine extends Phobject { return array_fuse($exclude_phids); } - private function getAllVisiblePHIDs() { - $visible_phids = $this->getVisiblePHIDs(); - $visible_phids[] = $this->getObjectPHID(); - $visible_phids = array_fuse($visible_phids); - return $visible_phids; + private function newCardTemplates() { + $viewer = $this->getViewer(); + + $update_phids = $this->getUpdatePHIDs(); + if (!$update_phids) { + return array(); + } + $update_phids = array_fuse($update_phids); + + if ($this->objects === null) { + $objects = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withPHIDs($update_phids) + ->needProjectPHIDs(true) + ->execute(); + } else { + $objects = $this->getObjects(); + $objects = mpull($objects, null, 'getPHID'); + $objects = array_select_keys($objects, $update_phids); + } + + if (!$objects) { + return array(); + } + + $excluded_phids = $this->loadExcludedProjectPHIDs(); + + $rendering_engine = id(new PhabricatorBoardRenderingEngine()) + ->setViewer($viewer) + ->setObjects($objects) + ->setExcludedProjectPHIDs($excluded_phids); + + $templates = array(); + foreach ($objects as $object) { + $object_phid = $object->getPHID(); + + $card = $rendering_engine->renderCard($object_phid); + $item = $card->getItem(); + $template = hsprintf('%s', $item); + + $templates[$object_phid] = $template; + } + + return $templates; } } diff --git a/src/applications/project/query/PhabricatorProjectQuery.php b/src/applications/project/query/PhabricatorProjectQuery.php index b08a58501f..b68c638363 100644 --- a/src/applications/project/query/PhabricatorProjectQuery.php +++ b/src/applications/project/query/PhabricatorProjectQuery.php @@ -464,28 +464,28 @@ final class PhabricatorProjectQuery } $where[] = qsprintf( $conn, - 'status IN (%Ld)', + 'project.status IN (%Ld)', $filter); } if ($this->statuses !== null) { $where[] = qsprintf( $conn, - 'status IN (%Ls)', + 'project.status IN (%Ls)', $this->statuses); } if ($this->ids !== null) { $where[] = qsprintf( $conn, - 'id IN (%Ld)', + 'project.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, - 'phid IN (%Ls)', + 'project.phid IN (%Ls)', $this->phids); } @@ -513,7 +513,7 @@ final class PhabricatorProjectQuery if ($this->names !== null) { $where[] = qsprintf( $conn, - 'name IN (%Ls)', + 'project.name IN (%Ls)', $this->names); } @@ -522,7 +522,7 @@ final class PhabricatorProjectQuery foreach ($this->namePrefixes as $name_prefix) { $parts[] = qsprintf( $conn, - 'name LIKE %>', + 'project.name LIKE %>', $name_prefix); } $where[] = qsprintf($conn, '%LO', $parts); @@ -531,21 +531,21 @@ final class PhabricatorProjectQuery if ($this->icons !== null) { $where[] = qsprintf( $conn, - 'icon IN (%Ls)', + 'project.icon IN (%Ls)', $this->icons); } if ($this->colors !== null) { $where[] = qsprintf( $conn, - 'color IN (%Ls)', + 'project.color IN (%Ls)', $this->colors); } if ($this->parentPHIDs !== null) { $where[] = qsprintf( $conn, - 'parentProjectPHID IN (%Ls)', + 'project.parentProjectPHID IN (%Ls)', $this->parentPHIDs); } @@ -563,7 +563,7 @@ final class PhabricatorProjectQuery foreach ($ancestor_paths as $ancestor_path) { $sql[] = qsprintf( $conn, - '(projectPath LIKE %> AND projectDepth > %d)', + '(project.projectPath LIKE %> AND project.projectDepth > %d)', $ancestor_path['projectPath'], $ancestor_path['projectDepth']); } @@ -572,18 +572,18 @@ final class PhabricatorProjectQuery $where[] = qsprintf( $conn, - 'parentProjectPHID IS NOT NULL'); + 'project.parentProjectPHID IS NOT NULL'); } if ($this->isMilestone !== null) { if ($this->isMilestone) { $where[] = qsprintf( $conn, - 'milestoneNumber IS NOT NULL'); + 'project.milestoneNumber IS NOT NULL'); } else { $where[] = qsprintf( $conn, - 'milestoneNumber IS NULL'); + 'project.milestoneNumber IS NULL'); } } @@ -591,42 +591,42 @@ final class PhabricatorProjectQuery if ($this->hasSubprojects !== null) { $where[] = qsprintf( $conn, - 'hasSubprojects = %d', + 'project.hasSubprojects = %d', (int)$this->hasSubprojects); } if ($this->minDepth !== null) { $where[] = qsprintf( $conn, - 'projectDepth >= %d', + 'project.projectDepth >= %d', $this->minDepth); } if ($this->maxDepth !== null) { $where[] = qsprintf( $conn, - 'projectDepth <= %d', + 'project.projectDepth <= %d', $this->maxDepth); } if ($this->minMilestoneNumber !== null) { $where[] = qsprintf( $conn, - 'milestoneNumber >= %d', + 'project.milestoneNumber >= %d', $this->minMilestoneNumber); } if ($this->maxMilestoneNumber !== null) { $where[] = qsprintf( $conn, - 'milestoneNumber <= %d', + 'project.milestoneNumber <= %d', $this->maxMilestoneNumber); } if ($this->subtypes !== null) { $where[] = qsprintf( $conn, - 'subtype IN (%Ls)', + 'project.subtype IN (%Ls)', $this->subtypes); } @@ -646,7 +646,7 @@ final class PhabricatorProjectQuery if ($this->memberPHIDs !== null) { $joins[] = qsprintf( $conn, - 'JOIN %T e ON e.src = p.phid AND e.type = %d', + 'JOIN %T e ON e.src = project.phid AND e.type = %d', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhabricatorProjectMaterializedMemberEdgeType::EDGECONST); } @@ -654,7 +654,7 @@ final class PhabricatorProjectQuery if ($this->watcherPHIDs !== null) { $joins[] = qsprintf( $conn, - 'JOIN %T w ON w.src = p.phid AND w.type = %d', + 'JOIN %T w ON w.src = project.phid AND w.type = %d', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhabricatorObjectHasWatcherEdgeType::EDGECONST); } @@ -662,7 +662,7 @@ final class PhabricatorProjectQuery if ($this->slugs !== null) { $joins[] = qsprintf( $conn, - 'JOIN %T slug on slug.projectPHID = p.phid', + 'JOIN %T slug on slug.projectPHID = project.phid', id(new PhabricatorProjectSlug())->getTableName()); } @@ -672,7 +672,7 @@ final class PhabricatorProjectQuery $token_table = 'token_'.$key; $joins[] = qsprintf( $conn, - 'JOIN %T %T ON %T.projectID = p.id AND %T.token LIKE %>', + 'JOIN %T %T ON %T.projectID = project.id AND %T.token LIKE %>', PhabricatorProject::TABLE_DATASOURCE_TOKEN, $token_table, $token_table, @@ -689,7 +689,7 @@ final class PhabricatorProjectQuery } protected function getPrimaryTableAlias() { - return 'p'; + return 'project'; } private function linkProjectGraph(array $projects, array $ancestors) { diff --git a/src/applications/project/state/PhabricatorWorkboardViewState.php b/src/applications/project/state/PhabricatorWorkboardViewState.php new file mode 100644 index 0000000000..04f8498d49 --- /dev/null +++ b/src/applications/project/state/PhabricatorWorkboardViewState.php @@ -0,0 +1,291 @@ +project = $project; + return $this; + } + + public function getProject() { + return $this->project; + } + + public function readFromRequest(AphrontRequest $request) { + if ($request->getExists('hidden')) { + $this->requestState['hidden'] = $request->getBool('hidden'); + } + + if ($request->getExists('order')) { + $this->requestState['order'] = $request->getStr('order'); + } + + // On some pathways, the search engine query key may be specified with + // either a "?filter=X" query parameter or with a "/query/X/" URI + // component. If both are present, the URI component is controlling. + + // In particular, the "queryKey" URI parameter is used by + // "buildSavedQueryFromRequest()" when we are building custom board filters + // by invoking SearchEngine code. + + if ($request->getExists('filter')) { + $this->requestState['filter'] = $request->getStr('filter'); + } + + if (strlen($request->getURIData('queryKey'))) { + $this->requestState['filter'] = $request->getURIData('queryKey'); + } + + $this->viewer = $request->getViewer(); + + return $this; + } + + public function getViewer() { + return $this->viewer; + } + + public function getSavedQuery() { + if ($this->savedQuery === null) { + $this->savedQuery = $this->newSavedQuery(); + } + + return $this->savedQuery; + } + + private function newSavedQuery() { + $search_engine = $this->getSearchEngine(); + $query_key = $this->getQueryKey(); + $viewer = $this->getViewer(); + + if ($search_engine->isBuiltinQuery($query_key)) { + $saved_query = $search_engine->buildSavedQueryFromBuiltin($query_key); + } else { + $saved_query = id(new PhabricatorSavedQueryQuery()) + ->setViewer($viewer) + ->withQueryKeys(array($query_key)) + ->executeOne(); + } + + return $saved_query; + } + + public function getSearchEngine() { + if ($this->searchEngine === null) { + $this->searchEngine = $this->newSearchEngine(); + } + + return $this->searchEngine; + } + + private function newSearchEngine() { + $viewer = $this->getViewer(); + + // TODO: This URI is not fully state-preserving, because "SearchEngine" + // does not preserve URI parameters when constructing some URIs at time of + // writing. + $board_uri = $this->getProject()->getWorkboardURI(); + + return id(new ManiphestTaskSearchEngine()) + ->setViewer($viewer) + ->setBaseURI($board_uri) + ->setIsBoardView(true); + } + + public function newWorkboardURI($path = null) { + $project = $this->getProject(); + $uri = urisprintf('%s%s', $project->getWorkboardURI(), $path); + return $this->newURI($uri); + } + + public function newURI($path) { + $project = $this->getProject(); + $uri = new PhutilURI($path); + + $request_order = $this->getOrder(); + $default_order = $this->getDefaultOrder(); + if ($request_order !== $default_order) { + $request_value = idx($this->requestState, 'order'); + if ($request_value !== null) { + $uri->replaceQueryParam('order', $request_value); + } else { + $uri->removeQueryParam('order'); + } + } else { + $uri->removeQueryParam('order'); + } + + $request_query = $this->getQueryKey(); + $default_query = $this->getDefaultQueryKey(); + if ($request_query !== $default_query) { + $request_value = idx($this->requestState, 'filter'); + if ($request_value !== null) { + $uri->replaceQueryParam('filter', $request_value); + } else { + $uri->removeQueryParam('filter'); + } + } else { + $uri->removeQueryParam('filter'); + } + + if ($this->getShowHidden()) { + $uri->replaceQueryParam('hidden', 'true'); + } else { + $uri->removeQueryParam('hidden'); + } + + return $uri; + } + + public function getShowHidden() { + $request_show = idx($this->requestState, 'hidden'); + + if ($request_show !== null) { + return $request_show; + } + + return false; + } + + public function getOrder() { + $request_order = idx($this->requestState, 'order'); + if ($request_order !== null) { + if ($this->isValidOrder($request_order)) { + return $request_order; + } + } + + return $this->getDefaultOrder(); + } + + public function getQueryKey() { + $request_query = idx($this->requestState, 'filter'); + if (strlen($request_query)) { + return $request_query; + } + + return $this->getDefaultQueryKey(); + } + + public function setQueryKey($query_key) { + $this->requestState['filter'] = $query_key; + return $this; + } + + private function isValidOrder($order) { + $map = PhabricatorProjectColumnOrder::getEnabledOrders(); + return isset($map[$order]); + } + + private function getDefaultOrder() { + $project = $this->getProject(); + + $default_order = $project->getDefaultWorkboardSort(); + + if ($this->isValidOrder($default_order)) { + return $default_order; + } + + return PhabricatorProjectColumnNaturalOrder::ORDERKEY; + } + + private function getDefaultQueryKey() { + $project = $this->getProject(); + + $default_query = $project->getDefaultWorkboardFilter(); + + if (strlen($default_query)) { + return $default_query; + } + + return 'open'; + } + + public function getQueryParameters() { + return $this->requestState; + } + + public function getLayoutEngine() { + if ($this->layoutEngine === null) { + $this->layoutEngine = $this->newLayoutEngine(); + } + return $this->layoutEngine; + } + + private function newLayoutEngine() { + $project = $this->getProject(); + $viewer = $this->getViewer(); + + $board_phid = $project->getPHID(); + $objects = $this->getObjects(); + + // Regardless of display order, pass tasks to the layout engine in ID order + // so layout is consistent. + $objects = msort($objects, 'getID'); + + $layout_engine = id(new PhabricatorBoardLayoutEngine()) + ->setViewer($viewer) + ->setObjectPHIDs(array_keys($objects)) + ->setBoardPHIDs(array($board_phid)) + ->setFetchAllBoards(true) + ->executeLayout(); + + return $layout_engine; + } + + public function getBoardContainerPHIDs() { + $project = $this->getProject(); + $viewer = $this->getViewer(); + + $container_phids = array($project->getPHID()); + if ($project->getHasSubprojects() || $project->getHasMilestones()) { + $descendants = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withAncestorProjectPHIDs($container_phids) + ->execute(); + foreach ($descendants as $descendant) { + $container_phids[] = $descendant->getPHID(); + } + } + + return $container_phids; + } + + public function getObjects() { + if ($this->objects === null) { + $this->objects = $this->newObjects(); + } + + return $this->objects; + } + + private function newObjects() { + $viewer = $this->getViewer(); + $saved_query = $this->getSavedQuery(); + $search_engine = $this->getSearchEngine(); + + $container_phids = $this->getBoardContainerPHIDs(); + + $task_query = $search_engine->buildQueryFromSavedQuery($saved_query) + ->setViewer($viewer) + ->withEdgeLogicPHIDs( + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, + PhabricatorQueryConstraint::OPERATOR_ANCESTOR, + array($container_phids)); + + $tasks = $task_query->execute(); + $tasks = mpull($tasks, null, 'getPHID'); + + return $tasks; + } + +} diff --git a/src/applications/project/storage/PhabricatorProjectColumnPosition.php b/src/applications/project/storage/PhabricatorProjectColumnPosition.php index 0bd9be6d4a..1a094dec37 100644 --- a/src/applications/project/storage/PhabricatorProjectColumnPosition.php +++ b/src/applications/project/storage/PhabricatorProjectColumnPosition.php @@ -46,7 +46,7 @@ final class PhabricatorProjectColumnPosition extends PhabricatorProjectDAO return $this; } - public function getOrderingKey() { + public function newColumnPositionOrderVector() { // We're ordering both real positions and "virtual" positions which we have // created but not saved yet. @@ -61,11 +61,10 @@ final class PhabricatorProjectColumnPosition extends PhabricatorProjectDAO // Broadly, this collectively makes newly added stuff float to the top. - return sprintf( - '~%012d%012d%012d', - $this->getSequence(), - ((1 << 31) - $this->viewSequence), - ((1 << 31) - $this->getID())); + return id(new PhutilSortVector()) + ->addInt($this->getSequence()) + ->addInt(-1 * $this->viewSequence) + ->addInt(-1 * $this->getID()); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php b/src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php index 038fa416f8..790b95d775 100644 --- a/src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php +++ b/src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php @@ -93,6 +93,8 @@ final class PhabricatorRepositoryDiscoveryEngine // Clear the working set cache. $this->workingSet = array(); + $task_priority = $this->getImportTaskPriority($repository, $refs); + // Record discovered commits and mark them in the cache. foreach ($refs as $ref) { $this->recordCommit( @@ -100,7 +102,8 @@ final class PhabricatorRepositoryDiscoveryEngine $ref->getIdentifier(), $ref->getEpoch(), $ref->getCanCloseImmediately(), - $ref->getParents()); + $ref->getParents(), + $task_priority); $this->commitCache[$ref->getIdentifier()] = true; } @@ -398,7 +401,7 @@ final class PhabricatorRepositoryDiscoveryEngine } } - // Now, sort them topographically. + // Now, sort them topologically. $commits = $this->reduceGraph($graph); $refs = array(); @@ -437,7 +440,7 @@ final class PhabricatorRepositoryDiscoveryEngine $graph = new PhutilDirectedScalarGraph(); $graph->addNodes($edges); - $commits = $graph->getTopographicallySortedNodes(); + $commits = $graph->getNodesInTopologicalOrder(); // NOTE: We want the most ancestral nodes first, so we need to reverse the // list we get out of AbstractDirectedGraph. @@ -536,7 +539,8 @@ final class PhabricatorRepositoryDiscoveryEngine $commit_identifier, $epoch, $close_immediately, - array $parents) { + array $parents, + $task_priority) { $commit = new PhabricatorRepositoryCommit(); $conn_w = $repository->establishConnection('w'); @@ -559,7 +563,7 @@ final class PhabricatorRepositoryDiscoveryEngine $commit_identifier); // After reviving a commit, schedule new daemons for it. - $this->didDiscoverCommit($repository, $commit, $epoch); + $this->didDiscoverCommit($repository, $commit, $epoch, $task_priority); return; } @@ -620,7 +624,7 @@ final class PhabricatorRepositoryDiscoveryEngine } $commit->saveTransaction(); - $this->didDiscoverCommit($repository, $commit, $epoch); + $this->didDiscoverCommit($repository, $commit, $epoch, $task_priority); if ($this->repairMode) { // Normally, the query should throw a duplicate key exception. If we @@ -648,9 +652,10 @@ final class PhabricatorRepositoryDiscoveryEngine private function didDiscoverCommit( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit, - $epoch) { + $epoch, + $task_priority) { - $this->insertTask($repository, $commit); + $this->insertTask($repository, $commit, $task_priority); // Update the repository summary table. queryfx( @@ -677,6 +682,7 @@ final class PhabricatorRepositoryDiscoveryEngine private function insertTask( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit, + $task_priority, $data = array()) { $vcs = $repository->getVersionControlSystem(); @@ -696,27 +702,6 @@ final class PhabricatorRepositoryDiscoveryEngine $data['commitID'] = $commit->getID(); - // If the repository is importing for the first time, we schedule tasks - // at IMPORT priority, which is very low. Making progress on importing a - // new repository for the first time is less important than any other - // daemon task. - - // If the repository has finished importing and we're just catching up - // on recent commits, we schedule discovery at COMMIT priority, which is - // slightly below the default priority. - - // Note that followup tasks and triggered tasks (like those generated by - // Herald or Harbormaster) will queue at DEFAULT priority, so that each - // commit tends to fully import before we start the next one. This tends - // to give imports fairly predictable progress. See T11677 for some - // discussion. - - if ($repository->isImporting()) { - $task_priority = PhabricatorWorker::PRIORITY_IMPORT; - } else { - $task_priority = PhabricatorWorker::PRIORITY_COMMIT; - } - $options = array( 'priority' => $task_priority, ); @@ -934,4 +919,71 @@ final class PhabricatorRepositoryDiscoveryEngine $data['epoch']); } + private function getImportTaskPriority( + PhabricatorRepository $repository, + array $refs) { + + // If the repository is importing for the first time, we schedule tasks + // at IMPORT priority, which is very low. Making progress on importing a + // new repository for the first time is less important than any other + // daemon task. + + // If the repository has finished importing and we're just catching up + // on recent commits, we usually schedule discovery at COMMIT priority, + // which is slightly below the default priority. + + // Note that followup tasks and triggered tasks (like those generated by + // Herald or Harbormaster) will queue at DEFAULT priority, so that each + // commit tends to fully import before we start the next one. This tends + // to give imports fairly predictable progress. See T11677 for some + // discussion. + + if ($repository->isImporting()) { + $this->log( + pht( + 'Importing %s commit(s) at low priority ("PRIORITY_IMPORT") '. + 'because this repository is still importing.', + phutil_count($refs))); + + return PhabricatorWorker::PRIORITY_IMPORT; + } + + // See T13369. If we've discovered a lot of commits at once, import them + // at lower priority. + + // This is mostly aimed at reducing the impact that synchronizing thousands + // of commits from a remote upstream has on other repositories. The queue + // is "mostly FIFO", so queueing a thousand commit imports can stall other + // repositories. + + // In a perfect world we'd probably give repositories round-robin queue + // priority, but we don't currently have the primitives for this and there + // isn't a strong case for building them. + + // Use "a whole lot of commits showed up at once" as a heuristic for + // detecting "someone synchronized an upstream", and import them at a lower + // priority to more closely approximate fair scheduling. + + if (count($refs) >= PhabricatorRepository::LOWPRI_THRESHOLD) { + $this->log( + pht( + 'Importing %s commit(s) at low priority ("PRIORITY_IMPORT") '. + 'because many commits were discovered at once.', + phutil_count($refs))); + + return PhabricatorWorker::PRIORITY_IMPORT; + } + + // Otherwise, import at normal priority. + + if ($refs) { + $this->log( + pht( + 'Importing %s commit(s) at normal priority ("PRIORITY_COMMIT").', + phutil_count($refs))); + } + + return PhabricatorWorker::PRIORITY_COMMIT; + } + } diff --git a/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php b/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php index 2c6ac8e83f..ea70f380aa 100644 --- a/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php +++ b/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php @@ -52,6 +52,13 @@ final class PhabricatorRepositoryPullEngine $repository = $this->getRepository(); $viewer = PhabricatorUser::getOmnipotentUser(); + if ($repository->isReadOnly()) { + $this->skipPull( + pht( + "Skipping pull on read-only repository.\n\n%s", + $repository->getReadOnlyMessageForDisplay())); + } + $is_hg = false; $is_git = false; $is_svn = false; diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementMaintenanceWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementMaintenanceWorkflow.php new file mode 100644 index 0000000000..65fe0adaad --- /dev/null +++ b/src/applications/repository/management/PhabricatorRepositoryManagementMaintenanceWorkflow.php @@ -0,0 +1,104 @@ +setName('maintenance') + ->setExamples( + "**maintenance** --start __message__ __repository__ ...\n". + "**maintenance** --stop __repository__") + ->setSynopsis( + pht('Set or clear read-only mode for repository maintenance.')) + ->setArguments( + array( + array( + 'name' => 'start', + 'param' => 'message', + 'help' => pht( + 'Put repositories into maintenance mode.'), + ), + array( + 'name' => 'stop', + 'help' => pht( + 'Take repositories out of maintenance mode, returning them '. + 'to normal serice.'), + ), + array( + 'name' => 'repositories', + 'wildcard' => true, + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $viewer = $this->getViewer(); + + $repositories = $this->loadRepositories($args, 'repositories'); + if (!$repositories) { + throw new PhutilArgumentUsageException( + pht('Specify one or more repositories to act on.')); + } + + $message = $args->getArg('start'); + $is_start = (bool)strlen($message); + $is_stop = $args->getArg('stop'); + + if (!$is_start && !$is_stop) { + throw new PhutilArgumentUsageException( + pht( + 'Use "--start " to put repositories into maintenance '. + 'mode, or "--stop" to take them out of maintenance mode.')); + } + + if ($is_start && $is_stop) { + throw new PhutilArgumentUsageException( + pht( + 'Specify either "--start" or "--stop", but not both.')); + } + + $content_source = $this->newContentSource(); + $diffusion_phid = id(new PhabricatorDiffusionApplication())->getPHID(); + + if ($is_start) { + $new_value = $message; + } else { + $new_value = null; + } + + foreach ($repositories as $repository) { + $xactions = array(); + + $xactions[] = $repository->getApplicationTransactionTemplate() + ->setTransactionType( + PhabricatorRepositoryMaintenanceTransaction::TRANSACTIONTYPE) + ->setNewValue($new_value); + + $repository->getApplicationTransactionEditor() + ->setActor($viewer) + ->setActingAsPHID($diffusion_phid) + ->setContentSource($content_source) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->applyTransactions($repository, $xactions); + + if ($is_start) { + echo tsprintf( + "%s\n", + pht( + 'Put repository "%s" into maintenance mode.', + $repository->getDisplayName())); + } else { + echo tsprintf( + "%s\n", + pht( + 'Took repository "%s" out of maintenance mode.', + $repository->getDisplayName())); + } + } + + return 0; + } + +} diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php index 30eb56cfd5..bd15e3e8de 100644 --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -34,6 +34,8 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO */ const IMPORT_THRESHOLD = 7; + const LOWPRI_THRESHOLD = 64; + const TABLE_PATH = 'repository_path'; const TABLE_PATHCHANGE = 'repository_pathchange'; const TABLE_FILESYSTEM = 'repository_filesystem'; @@ -1408,6 +1410,12 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO } } + if ($write) { + if ($this->isReadOnly()) { + return false; + } + } + return false; } @@ -2264,6 +2272,35 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO return $this->isGit(); } + public function isReadOnly() { + return (bool)$this->getDetail('read-only'); + } + + public function setReadOnly($read_only) { + return $this->setDetail('read-only', $read_only); + } + + public function getReadOnlyMessage() { + return $this->getDetail('read-only-message'); + } + + public function setReadOnlyMessage($message) { + return $this->setDetail('read-only-message', $message); + } + + public function getReadOnlyMessageForDisplay() { + $parts = array(); + $parts[] = pht( + 'This repository is currently in read-only maintenance mode.'); + + $message = $this->getReadOnlyMessage(); + if ($message !== null) { + $parts[] = $message; + } + + return implode("\n\n", $parts); + } + /* -( Repository URIs )---------------------------------------------------- */ diff --git a/src/applications/repository/xaction/PhabricatorRepositoryMaintenanceTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryMaintenanceTransaction.php new file mode 100644 index 0000000000..caf9e84527 --- /dev/null +++ b/src/applications/repository/xaction/PhabricatorRepositoryMaintenanceTransaction.php @@ -0,0 +1,43 @@ +getReadOnlyMessage(); + } + + public function applyInternalEffects($object, $value) { + if ($value === null) { + $object + ->setReadOnly(false) + ->setReadOnlyMessage(null); + } else { + $object + ->setReadOnly(true) + ->setReadOnlyMessage($value); + } + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + if (strlen($old) && !strlen($new)) { + return pht( + '%s took this repository out of maintenance mode.', + $this->renderAuthor()); + } else if (!strlen($old) && strlen($new)) { + return pht( + '%s put this repository into maintenance mode.', + $this->renderAuthor()); + } else { + return pht( + '%s updated the maintenance message for this repository.', + $this->renderAuthor()); + } + } + +} diff --git a/src/applications/search/controller/PhabricatorApplicationSearchController.php b/src/applications/search/controller/PhabricatorApplicationSearchController.php index ce331fd722..37c6dc5c2c 100644 --- a/src/applications/search/controller/PhabricatorApplicationSearchController.php +++ b/src/applications/search/controller/PhabricatorApplicationSearchController.php @@ -198,9 +198,10 @@ if ($run_query && !$named_query && $user->isLoggedIn()) { $save_button = id(new PHUIButtonView()) ->setTag('a') + ->setColor(PHUIButtonView::GREY) ->setHref('/search/edit/key/'.$saved_query->getQueryKey().'/') ->setText(pht('Save Query')) - ->setIcon('fa-floppy-o'); + ->setIcon('fa-bookmark'); $submit->addButton($save_button); } diff --git a/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php b/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php index f4e2dc918f..3af4f349fa 100644 --- a/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php +++ b/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php @@ -198,6 +198,7 @@ EOTEXT $label = $field->getLabel(); $constants = $field->newConduitConstants(); + $show_table = false; $type_object = $field->getConduitParameterType(); if ($type_object) { @@ -209,6 +210,7 @@ EOTEXT ' ', phutil_tag('em', array(), pht('(See table below.)')), ); + $show_table = true; } } else { $type = null; @@ -222,11 +224,11 @@ EOTEXT $description, ); - if ($constants) { + if ($show_table) { $constant_lists[] = $this->newRemarkupDocumentationView( pht( 'Constants supported by the `%s` constraint:', - 'statuses')); + $key)); $constants_rows = array(); foreach ($constants as $constant) { diff --git a/src/applications/settings/action/PhabricatorSettingsAddEmailAction.php b/src/applications/settings/action/PhabricatorSettingsAddEmailAction.php index 764db7f543..4038e37c9f 100644 --- a/src/applications/settings/action/PhabricatorSettingsAddEmailAction.php +++ b/src/applications/settings/action/PhabricatorSettingsAddEmailAction.php @@ -4,10 +4,6 @@ final class PhabricatorSettingsAddEmailAction extends PhabricatorSystemAction { const TYPECONST = 'email.add'; - public function getActionConstant() { - return self::TYPECONST; - } - public function getScoreThreshold() { return 6 / phutil_units('1 hour in seconds'); } diff --git a/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php b/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php index abbb88c0a5..0054610c28 100644 --- a/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php @@ -315,7 +315,7 @@ final class PhabricatorMultiFactorSettingsPanel $log = PhabricatorUserLog::initializeNewLog( $viewer, $user->getPHID(), - PhabricatorUserLog::ACTION_MULTI_ADD); + PhabricatorAddMultifactorUserLogType::LOGTYPE); $log->save(); $user->updateMultiFactorEnrollment(); @@ -423,7 +423,7 @@ final class PhabricatorMultiFactorSettingsPanel $log = PhabricatorUserLog::initializeNewLog( $viewer, $user->getPHID(), - PhabricatorUserLog::ACTION_MULTI_REMOVE); + PhabricatorRemoveMultifactorUserLogType::LOGTYPE); $log->save(); $user->updateMultiFactorEnrollment(); diff --git a/src/applications/slowvote/conduit/SlowvoteInfoConduitAPIMethod.php b/src/applications/slowvote/conduit/SlowvoteInfoConduitAPIMethod.php index cecd799ad0..1b4cde9191 100644 --- a/src/applications/slowvote/conduit/SlowvoteInfoConduitAPIMethod.php +++ b/src/applications/slowvote/conduit/SlowvoteInfoConduitAPIMethod.php @@ -6,6 +6,14 @@ final class SlowvoteInfoConduitAPIMethod extends SlowvoteConduitAPIMethod { return 'slowvote.info'; } + public function getMethodStatus() { + return self::METHOD_STATUS_DEPRECATED; + } + + public function getMethodStatusDescription() { + return pht('Replaced by "slowvote.poll.search".'); + } + public function getMethodDescription() { return pht('Retrieve an array of information about a poll.'); } @@ -27,8 +35,14 @@ final class SlowvoteInfoConduitAPIMethod extends SlowvoteConduitAPIMethod { } protected function execute(ConduitAPIRequest $request) { + $viewer = $this->getViewer(); + $poll_id = $request->getValue('poll_id'); - $poll = id(new PhabricatorSlowvotePoll())->load($poll_id); + + $poll = id(new PhabricatorSlowvoteQuery()) + ->setViewer($viewer) + ->withIDs(array($poll_id)) + ->executeOne(); if (!$poll) { throw new ConduitException('ERR_BAD_POLL'); } diff --git a/src/applications/slowvote/conduit/SlowvoteSearchConduitAPIMethod.php b/src/applications/slowvote/conduit/SlowvoteSearchConduitAPIMethod.php new file mode 100644 index 0000000000..01f3255f37 --- /dev/null +++ b/src/applications/slowvote/conduit/SlowvoteSearchConduitAPIMethod.php @@ -0,0 +1,18 @@ +setKey('authorPHIDs') ->setAliases(array('authors')) ->setLabel(pht('Authors')), - id(new PhabricatorSearchCheckboxesField()) ->setKey('voted') + ->setLabel(pht('Voted')) + + // TODO: This should probably become a list of "voterPHIDs", so hide + // the field from Conduit to avoid a backward compatibility break when + // this changes. + + ->setEnableForConduit(false) ->setOptions(array( 'voted' => pht("Show only polls I've voted in."), )), - id(new PhabricatorSearchCheckboxesField()) ->setKey('statuses') ->setLabel(pht('Statuses')) - ->setOptions(array( + ->setOptions( + array( 'open' => pht('Open'), 'closed' => pht('Closed'), - )), + )), ); } diff --git a/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php b/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php index b8355c0586..215549f7db 100644 --- a/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php +++ b/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php @@ -9,7 +9,8 @@ final class PhabricatorSlowvotePoll extends PhabricatorSlowvoteDAO PhabricatorTokenReceiverInterface, PhabricatorProjectInterface, PhabricatorDestructibleInterface, - PhabricatorSpacesInterface { + PhabricatorSpacesInterface, + PhabricatorConduitResultInterface { const RESPONSES_VISIBLE = 0; const RESPONSES_VOTERS = 1; @@ -202,10 +203,36 @@ final class PhabricatorSlowvotePoll extends PhabricatorSlowvoteDAO $this->saveTransaction(); } - /* -( PhabricatorSpacesInterface )--------------------------------------- */ +/* -( PhabricatorSpacesInterface )----------------------------------------- */ public function getSpacePHID() { return $this->spacePHID; } +/* -( PhabricatorConduitResultInterface )---------------------------------- */ + + public function getFieldSpecificationsForConduit() { + return array( + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('name') + ->setType('string') + ->setDescription(pht('The name of the poll.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('authorPHID') + ->setType('string') + ->setDescription(pht('The author of the poll.')), + ); + } + + public function getFieldValuesForConduit() { + return array( + 'name' => $this->getQuestion(), + 'authorPHID' => $this->getAuthorPHID(), + ); + } + + public function getConduitSearchAttachments() { + return array(); + } + } diff --git a/src/applications/system/action/PhabricatorSystemAction.php b/src/applications/system/action/PhabricatorSystemAction.php index 329824bacc..b712dfca8c 100644 --- a/src/applications/system/action/PhabricatorSystemAction.php +++ b/src/applications/system/action/PhabricatorSystemAction.php @@ -2,7 +2,10 @@ abstract class PhabricatorSystemAction extends Phobject { - abstract public function getActionConstant(); + final public function getActionConstant() { + return $this->getPhobjectClassConstant('TYPECONST', 32); + } + abstract public function getScoreThreshold(); public function shouldBlockActor($actor, $score) { diff --git a/src/applications/system/engine/PhabricatorSystemActionEngine.php b/src/applications/system/engine/PhabricatorSystemActionEngine.php index 6b8352a29e..c097fa04a4 100644 --- a/src/applications/system/engine/PhabricatorSystemActionEngine.php +++ b/src/applications/system/engine/PhabricatorSystemActionEngine.php @@ -198,4 +198,8 @@ final class PhabricatorSystemActionEngine extends Phobject { return $conn_w->getAffectedRows(); } + public static function newActorFromRequest(AphrontRequest $request) { + return $request->getRemoteAddress(); + } + } diff --git a/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentRemoveController.php b/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentRemoveController.php index f81535e4ae..4d6570b13d 100644 --- a/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentRemoveController.php +++ b/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentRemoveController.php @@ -38,10 +38,9 @@ final class PhabricatorApplicationTransactionCommentRemoveController // from locked threads. $object = $xaction->getObject(); - $can_interact = PhabricatorPolicyFilter::hasCapability( + $can_interact = PhabricatorPolicyFilter::canInteract( $viewer, - $object, - PhabricatorPolicyCapability::CAN_INTERACT); + $object); if (!$can_interact && !$viewer->getIsAdmin()) { return $this->newDialog() ->setTitle(pht('Conversation Locked')) diff --git a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationDefaultCreateController.php b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationDefaultCreateController.php index d7ea8810a5..039bfb0f44 100644 --- a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationDefaultCreateController.php +++ b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationDefaultCreateController.php @@ -15,7 +15,7 @@ final class PhabricatorEditEngineConfigurationDefaultCreateController $key = $config->getIdentifier(); $cancel_uri = "/transactions/editengine/{$engine_key}/view/{$key}/"; - $type = PhabricatorEditEngineConfigurationTransaction::TYPE_DEFAULTCREATE; + $type = PhabricatorEditEngineDefaultCreateTransaction::TRANSACTIONTYPE; if ($request->isFormPost()) { $xactions = array(); diff --git a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationDefaultsController.php b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationDefaultsController.php index f7361d50cf..3d63a4a098 100644 --- a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationDefaultsController.php +++ b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationDefaultsController.php @@ -52,7 +52,7 @@ final class PhabricatorEditEngineConfigurationDefaultsController $field->readValueFromSubmit($request); } - $type = PhabricatorEditEngineConfigurationTransaction::TYPE_DEFAULT; + $type = PhabricatorEditEngineDefaultTransaction::TRANSACTIONTYPE; $xactions = array(); foreach ($fields as $field) { diff --git a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationDisableController.php b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationDisableController.php index 24a32c5598..a3311b2d49 100644 --- a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationDisableController.php +++ b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationDisableController.php @@ -15,7 +15,7 @@ final class PhabricatorEditEngineConfigurationDisableController $key = $config->getIdentifier(); $cancel_uri = "/transactions/editengine/{$engine_key}/view/{$key}/"; - $type = PhabricatorEditEngineConfigurationTransaction::TYPE_DISABLE; + $type = PhabricatorEditEngineDisableTransaction::TRANSACTIONTYPE; if ($request->isFormPost()) { $xactions = array(); diff --git a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationIsEditController.php b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationIsEditController.php index 970a2512f1..b93390ff66 100644 --- a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationIsEditController.php +++ b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationIsEditController.php @@ -15,8 +15,7 @@ final class PhabricatorEditEngineConfigurationIsEditController $key = $config->getIdentifier(); $cancel_uri = "/transactions/editengine/{$engine_key}/view/{$key}/"; - $type = PhabricatorEditEngineConfigurationTransaction::TYPE_ISEDIT; - + $type = PhabricatorEditEngineIsEditTransaction::TRANSACTIONTYPE; if ($request->isFormPost()) { $xactions = array(); diff --git a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationLockController.php b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationLockController.php index 34b099b9f0..1375124585 100644 --- a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationLockController.php +++ b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationLockController.php @@ -30,8 +30,7 @@ final class PhabricatorEditEngineConfigurationLockController $xactions = array(); $locks = $request->getArr('locks'); - $type_locks = PhabricatorEditEngineConfigurationTransaction::TYPE_LOCKS; - + $type_locks = PhabricatorEditEngineLocksTransaction::TRANSACTIONTYPE; $xactions[] = id(new PhabricatorEditEngineConfigurationTransaction()) ->setTransactionType($type_locks) ->setNewValue($locks); diff --git a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationReorderController.php b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationReorderController.php index 6ff36cdfa4..563c3141b2 100644 --- a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationReorderController.php +++ b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationReorderController.php @@ -31,8 +31,7 @@ final class PhabricatorEditEngineConfigurationReorderController $xactions = array(); $key_order = $request->getStrList('keyOrder'); - $type_order = PhabricatorEditEngineConfigurationTransaction::TYPE_ORDER; - + $type_order = PhabricatorEditEngineOrderTransaction::TRANSACTIONTYPE; $xactions[] = id(new PhabricatorEditEngineConfigurationTransaction()) ->setTransactionType($type_order) ->setNewValue($key_order); diff --git a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationSortController.php b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationSortController.php index 613a847326..5e8680b651 100644 --- a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationSortController.php +++ b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationSortController.php @@ -70,10 +70,10 @@ final class PhabricatorEditEngineConfigurationSortController if ($is_create) { $xaction_type = - PhabricatorEditEngineConfigurationTransaction::TYPE_CREATEORDER; + PhabricatorEditEngineCreateOrderTransaction::TRANSACTIONTYPE; } else { $xaction_type = - PhabricatorEditEngineConfigurationTransaction::TYPE_EDITORDER; + PhabricatorEditEngineEditOrderTransaction::TRANSACTIONTYPE; } $xactions[] = id(new PhabricatorEditEngineConfigurationTransaction()) diff --git a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationSubtypeController.php b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationSubtypeController.php index 9ce8d7a0e5..918782cd81 100644 --- a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationSubtypeController.php +++ b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationSubtypeController.php @@ -35,9 +35,7 @@ final class PhabricatorEditEngineConfigurationSubtypeController $xactions = array(); $subtype = $request->getStr('subtype'); - $type_subtype = - PhabricatorEditEngineConfigurationTransaction::TYPE_SUBTYPE; - + $type_subtype = PhabricatorEditEngineSubtypeTransaction::TRANSACTIONTYPE; $xactions[] = id(new PhabricatorEditEngineConfigurationTransaction()) ->setTransactionType($type_subtype) ->setNewValue($subtype); diff --git a/src/applications/transactions/editfield/PhabricatorEpochEditField.php b/src/applications/transactions/editfield/PhabricatorEpochEditField.php index 9ac9726593..b50f013177 100644 --- a/src/applications/transactions/editfield/PhabricatorEpochEditField.php +++ b/src/applications/transactions/editfield/PhabricatorEpochEditField.php @@ -37,7 +37,8 @@ final class PhabricatorEpochEditField } protected function newConduitParameterType() { - return new ConduitEpochParameterType(); + return id(new ConduitEpochParameterType()) + ->setAllowNull($this->getAllowNull()); } } diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index f27ae1ba5b..5683455b87 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -1520,6 +1520,10 @@ abstract class PhabricatorApplicationTransactionEditor } } + foreach ($this->newAuxiliaryMail($object, $xactions) as $message) { + $messages[] = $message; + } + // NOTE: This actually sends the mail. We do this last to reduce the chance // that we send some mail, hit an exception, then send the mail again when // retrying. @@ -1801,6 +1805,11 @@ abstract class PhabricatorApplicationTransactionEditor // you don't need permissions. If you can eventually mute an object // for other users, this would need to be revisited. return null; + case PhabricatorProjectSilencedEdgeType::EDGECONST: + // At time of writing, you can only write this edge for yourself, so + // you don't need permissions. If you can eventually silence project + // for other users, this would need to be revisited. + return null; case PhabricatorObjectMentionsObjectEdgeType::EDGECONST: return null; case PhabricatorProjectProjectHasMemberEdgeType::EDGECONST: @@ -4797,6 +4806,10 @@ abstract class PhabricatorApplicationTransactionEditor return $extensions; } + protected function newAuxiliaryMail($object, array $xactions) { + return array(); + } + private function generateMailStamps($object, $data) { if (!$data || !is_array($data)) { return null; diff --git a/src/applications/transactions/editor/PhabricatorEditEngineConfigurationEditEngine.php b/src/applications/transactions/editor/PhabricatorEditEngineConfigurationEditEngine.php index a9d96337ed..5a30fbfdde 100644 --- a/src/applications/transactions/editor/PhabricatorEditEngineConfigurationEditEngine.php +++ b/src/applications/transactions/editor/PhabricatorEditEngineConfigurationEditEngine.php @@ -99,14 +99,14 @@ final class PhabricatorEditEngineConfigurationEditEngine ->setLabel(pht('Name')) ->setDescription(pht('Name of the form.')) ->setTransactionType( - PhabricatorEditEngineConfigurationTransaction::TYPE_NAME) + PhabricatorEditEngineNameTransaction::TRANSACTIONTYPE) ->setValue($object->getName()), id(new PhabricatorRemarkupEditField()) ->setKey('preamble') ->setLabel(pht('Preamble')) ->setDescription(pht('Optional instructions, shown above the form.')) ->setTransactionType( - PhabricatorEditEngineConfigurationTransaction::TYPE_PREAMBLE) + PhabricatorEditEnginePreambleTransaction::TRANSACTIONTYPE) ->setValue($object->getPreamble()), ); } diff --git a/src/applications/transactions/editor/PhabricatorEditEngineConfigurationEditor.php b/src/applications/transactions/editor/PhabricatorEditEngineConfigurationEditor.php index ccadf9b819..34b7653001 100644 --- a/src/applications/transactions/editor/PhabricatorEditEngineConfigurationEditor.php +++ b/src/applications/transactions/editor/PhabricatorEditEngineConfigurationEditor.php @@ -13,191 +13,9 @@ final class PhabricatorEditEngineConfigurationEditor public function getTransactionTypes() { $types = parent::getTransactionTypes(); - $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; - $types[] = PhabricatorEditEngineConfigurationTransaction::TYPE_NAME; - $types[] = PhabricatorEditEngineConfigurationTransaction::TYPE_PREAMBLE; - $types[] = PhabricatorEditEngineConfigurationTransaction::TYPE_ORDER; - $types[] = PhabricatorEditEngineConfigurationTransaction::TYPE_DEFAULT; - $types[] = PhabricatorEditEngineConfigurationTransaction::TYPE_LOCKS; - $types[] = PhabricatorEditEngineConfigurationTransaction::TYPE_SUBTYPE; - $types[] = - PhabricatorEditEngineConfigurationTransaction::TYPE_DEFAULTCREATE; - $types[] = PhabricatorEditEngineConfigurationTransaction::TYPE_ISEDIT; - $types[] = PhabricatorEditEngineConfigurationTransaction::TYPE_DISABLE; - - $types[] = PhabricatorEditEngineConfigurationTransaction::TYPE_CREATEORDER; - $types[] = PhabricatorEditEngineConfigurationTransaction::TYPE_EDITORDER; - return $types; } - protected function validateTransaction( - PhabricatorLiskDAO $object, - $type, - array $xactions) { - - $errors = parent::validateTransaction($object, $type, $xactions); - switch ($type) { - case PhabricatorEditEngineConfigurationTransaction::TYPE_NAME: - $missing = $this->validateIsEmptyTextField( - $object->getName(), - $xactions); - - if ($missing) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Required'), - pht('Form name is required.'), - nonempty(last($xactions), null)); - - $error->setIsMissingFieldError(true); - $errors[] = $error; - } - break; - case PhabricatorEditEngineConfigurationTransaction::TYPE_SUBTYPE: - if ($xactions) { - $map = $object->getEngine() - ->setViewer($this->getActor()) - ->newSubtypeMap(); - foreach ($xactions as $xaction) { - $new = $xaction->getNewValue(); - - if ($map->isValidSubtype($new)) { - continue; - } - - $errors[] = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Invalid'), - pht('Subtype "%s" is not a valid subtype.', $new), - $xaction); - } - } - break; - } - - return $errors; - } - - protected function getCustomTransactionOldValue( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case PhabricatorEditEngineConfigurationTransaction::TYPE_NAME: - return $object->getName(); - case PhabricatorEditEngineConfigurationTransaction::TYPE_PREAMBLE; - return $object->getPreamble(); - case PhabricatorEditEngineConfigurationTransaction::TYPE_ORDER: - return $object->getFieldOrder(); - case PhabricatorEditEngineConfigurationTransaction::TYPE_DEFAULT: - $field_key = $xaction->getMetadataValue('field.key'); - return $object->getFieldDefault($field_key); - case PhabricatorEditEngineConfigurationTransaction::TYPE_LOCKS: - return $object->getFieldLocks(); - case PhabricatorEditEngineConfigurationTransaction::TYPE_SUBTYPE: - return $object->getSubtype(); - case PhabricatorEditEngineConfigurationTransaction::TYPE_DEFAULTCREATE: - return (int)$object->getIsDefault(); - case PhabricatorEditEngineConfigurationTransaction::TYPE_ISEDIT: - return (int)$object->getIsEdit(); - case PhabricatorEditEngineConfigurationTransaction::TYPE_DISABLE: - return (int)$object->getIsDisabled(); - case PhabricatorEditEngineConfigurationTransaction::TYPE_CREATEORDER: - return (int)$object->getCreateOrder(); - case PhabricatorEditEngineConfigurationTransaction::TYPE_EDITORDER: - return (int)$object->getEditOrder(); - - } - } - - protected function getCustomTransactionNewValue( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case PhabricatorEditEngineConfigurationTransaction::TYPE_NAME: - case PhabricatorEditEngineConfigurationTransaction::TYPE_PREAMBLE; - case PhabricatorEditEngineConfigurationTransaction::TYPE_ORDER: - case PhabricatorEditEngineConfigurationTransaction::TYPE_DEFAULT: - case PhabricatorEditEngineConfigurationTransaction::TYPE_LOCKS: - case PhabricatorEditEngineConfigurationTransaction::TYPE_SUBTYPE: - return $xaction->getNewValue(); - case PhabricatorEditEngineConfigurationTransaction::TYPE_DEFAULTCREATE: - case PhabricatorEditEngineConfigurationTransaction::TYPE_ISEDIT: - case PhabricatorEditEngineConfigurationTransaction::TYPE_DISABLE: - case PhabricatorEditEngineConfigurationTransaction::TYPE_CREATEORDER: - case PhabricatorEditEngineConfigurationTransaction::TYPE_EDITORDER: - return (int)$xaction->getNewValue(); - } - } - - protected function applyCustomInternalTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case PhabricatorEditEngineConfigurationTransaction::TYPE_NAME: - $object->setName($xaction->getNewValue()); - return; - case PhabricatorEditEngineConfigurationTransaction::TYPE_PREAMBLE; - $object->setPreamble($xaction->getNewValue()); - return; - case PhabricatorEditEngineConfigurationTransaction::TYPE_ORDER: - $object->setFieldOrder($xaction->getNewValue()); - return; - case PhabricatorEditEngineConfigurationTransaction::TYPE_DEFAULT: - $field_key = $xaction->getMetadataValue('field.key'); - $object->setFieldDefault($field_key, $xaction->getNewValue()); - return; - case PhabricatorEditEngineConfigurationTransaction::TYPE_LOCKS: - $object->setFieldLocks($xaction->getNewValue()); - return; - case PhabricatorEditEngineConfigurationTransaction::TYPE_SUBTYPE: - $object->setSubtype($xaction->getNewValue()); - return; - case PhabricatorEditEngineConfigurationTransaction::TYPE_DEFAULTCREATE: - $object->setIsDefault($xaction->getNewValue()); - return; - case PhabricatorEditEngineConfigurationTransaction::TYPE_ISEDIT: - $object->setIsEdit($xaction->getNewValue()); - return; - case PhabricatorEditEngineConfigurationTransaction::TYPE_DISABLE: - $object->setIsDisabled($xaction->getNewValue()); - return; - case PhabricatorEditEngineConfigurationTransaction::TYPE_CREATEORDER: - $object->setCreateOrder($xaction->getNewValue()); - return; - case PhabricatorEditEngineConfigurationTransaction::TYPE_EDITORDER: - $object->setEditOrder($xaction->getNewValue()); - return; - } - - return parent::applyCustomInternalTransaction($object, $xaction); - } - - protected function applyCustomExternalTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case PhabricatorEditEngineConfigurationTransaction::TYPE_NAME: - case PhabricatorEditEngineConfigurationTransaction::TYPE_PREAMBLE; - case PhabricatorEditEngineConfigurationTransaction::TYPE_ORDER; - case PhabricatorEditEngineConfigurationTransaction::TYPE_DEFAULT: - case PhabricatorEditEngineConfigurationTransaction::TYPE_ISEDIT: - case PhabricatorEditEngineConfigurationTransaction::TYPE_LOCKS: - case PhabricatorEditEngineConfigurationTransaction::TYPE_SUBTYPE: - case PhabricatorEditEngineConfigurationTransaction::TYPE_DEFAULTCREATE: - case PhabricatorEditEngineConfigurationTransaction::TYPE_DISABLE: - case PhabricatorEditEngineConfigurationTransaction::TYPE_CREATEORDER: - case PhabricatorEditEngineConfigurationTransaction::TYPE_EDITORDER: - return; - } - - return parent::applyCustomExternalTransaction($object, $xaction); - } - } diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index 775d59d4db..7c327393f8 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -1375,16 +1375,16 @@ abstract class PhabricatorApplicationTransaction public function getActionStrength() { if ($this->isInlineCommentTransaction()) { - return 250; + return 25; } switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: - return 500; + return 50; case PhabricatorTransactions::TYPE_SUBSCRIBERS: if ($this->isSelfSubscription()) { // Make this weaker than TYPE_COMMENT. - return 250; + return 25; } if ($this->isApplicationAuthor()) { @@ -1396,14 +1396,14 @@ abstract class PhabricatorApplicationTransaction // In other cases, subscriptions are more interesting than comments // (which are shown anyway) but less interesting than any other type of // transaction. - return 750; + return 75; case PhabricatorTransactions::TYPE_MFA: // We want MFA signatures to render at the top of transaction groups, // on top of the things they signed. - return 10000; + return 1000; } - return 1000; + return 100; } public function isCommentTransaction() { diff --git a/src/applications/transactions/storage/PhabricatorEditEngineConfigurationTransaction.php b/src/applications/transactions/storage/PhabricatorEditEngineConfigurationTransaction.php index bf23bd3b4a..a9dca32e3d 100644 --- a/src/applications/transactions/storage/PhabricatorEditEngineConfigurationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorEditEngineConfigurationTransaction.php @@ -1,19 +1,7 @@ getAuthorPHID(); - - $old = $this->getOldValue(); - $new = $this->getNewValue(); - - $type = $this->getTransactionType(); - switch ($type) { - case PhabricatorTransactions::TYPE_CREATE: - return pht( - '%s created this form configuration.', - $this->renderHandleLink($author_phid)); - case self::TYPE_NAME: - if (strlen($old)) { - return pht( - '%s renamed this form from "%s" to "%s".', - $this->renderHandleLink($author_phid), - $old, - $new); - } else { - return pht( - '%s named this form "%s".', - $this->renderHandleLink($author_phid), - $new); - } - case self::TYPE_PREAMBLE: - return pht( - '%s updated the preamble for this form.', - $this->renderHandleLink($author_phid)); - case self::TYPE_ORDER: - return pht( - '%s reordered the fields in this form.', - $this->renderHandleLink($author_phid)); - case self::TYPE_DEFAULT: - $key = $this->getMetadataValue('field.key'); - - $object = $this->getObject(); - $engine = $object->getEngine(); - $fields = $engine->getFieldsForConfig($object); - - $field = idx($fields, $key); - if (!$field) { - return pht( - '%s changed the default value for field "%s".', - $this->renderHandleLink($author_phid), - $key); - } - - return pht( - '%s changed the default value for field "%s".', - $this->renderHandleLink($author_phid), - $field->getLabel()); - case self::TYPE_LOCKS: - return pht( - '%s changed locked and hidden fields.', - $this->renderHandleLink($author_phid)); - case self::TYPE_DEFAULTCREATE: - if ($new) { - return pht( - '%s added this form to the "Create" menu.', - $this->renderHandleLink($author_phid)); - } else { - return pht( - '%s removed this form from the "Create" menu.', - $this->renderHandleLink($author_phid)); - } - case self::TYPE_ISEDIT: - if ($new) { - return pht( - '%s marked this form as an edit form.', - $this->renderHandleLink($author_phid)); - } else { - return pht( - '%s unmarked this form as an edit form.', - $this->renderHandleLink($author_phid)); - } - case self::TYPE_DISABLE: - if ($new) { - return pht( - '%s disabled this form.', - $this->renderHandleLink($author_phid)); - } else { - return pht( - '%s enabled this form.', - $this->renderHandleLink($author_phid)); - } - case self::TYPE_SUBTYPE: - return pht( - '%s changed the subtype of this form from "%s" to "%s".', - $this->renderHandleLink($author_phid), - $old, - $new); - } - - return parent::getTitle(); - } - - public function getColor() { - $author_phid = $this->getAuthorPHID(); - - $old = $this->getOldValue(); - $new = $this->getNewValue(); - - $type = $this->getTransactionType(); - switch ($type) { - case PhabricatorTransactions::TYPE_CREATE: - return 'green'; - case self::TYPE_DISABLE: - if ($new) { - return 'indigo'; - } else { - return 'green'; - } - } - - return parent::getColor(); - } - - public function getIcon() { - $author_phid = $this->getAuthorPHID(); - - $old = $this->getOldValue(); - $new = $this->getNewValue(); - - $type = $this->getTransactionType(); - switch ($type) { - case PhabricatorTransactions::TYPE_CREATE: - return 'fa-plus'; - case self::TYPE_DISABLE: - if ($new) { - return 'fa-ban'; - } else { - return 'fa-check'; - } - } - - return parent::getIcon(); - } - - protected function newRemarkupChanges() { - $changes = array(); - - $type = $this->getTransactionType(); - switch ($type) { - case self::TYPE_PREAMBLE: - $changes[] = $this->newRemarkupChange() - ->setOldValue($this->getOldValue()) - ->setNewValue($this->getNewValue()); - break; - } - - return $changes; - } - - public function hasChangeDetails() { - switch ($this->getTransactionType()) { - case self::TYPE_DEFAULT: - return true; - } - - return parent::hasChangeDetails(); - } - - public function renderChangeDetails(PhabricatorUser $viewer) { - switch ($this->getTransactionType()) { - case self::TYPE_DEFAULT: - $old_value = $this->getOldValue(); - $new_value = $this->getNewValue(); - - $old_value = $this->renderDefaultValueAsFallbackText($old_value); - $new_value = $this->renderDefaultValueAsFallbackText($new_value); - - return $this->renderTextCorpusChangeDetails( - $viewer, - $old_value, - $new_value); - } - - return parent::renderChangeDetails($viewer); - } - - private function renderDefaultValueAsFallbackText($default_value) { - // See T13319. When rendering an "alice changed the default value for - // field X." story on custom forms, we may fall back to a generic - // rendering. Try to present the value change in a comprehensible way - // even if it isn't especially human readable (for example, it may - // contain PHIDs or other internal identifiers). - - if (is_scalar($default_value) || is_null($default_value)) { - return $default_value; - } - - if (phutil_is_natural_list($default_value)) { - return id(new PhutilJSON())->encodeAsList($default_value); - } - - return id(new PhutilJSON())->encodeAsObject($default_value); + public function getBaseTransactionClass() { + return 'PhabricatorEditEngineTransactionType'; } } diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionDetailView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionDetailView.php new file mode 100644 index 0000000000..1220e5bc7f --- /dev/null +++ b/src/applications/transactions/view/PhabricatorApplicationTransactionDetailView.php @@ -0,0 +1,172 @@ +newText = $new_text; + return $this; + } + + public function setOldText($old_text) { + $this->oldText = $old_text; + return $this; + } + + public function renderForMail() { + $diff = $this->buildDiff(); + + $viewer = $this->getViewer(); + $old_bright = $viewer->getCSSValue('old-bright'); + $new_bright = $viewer->getCSSValue('new-bright'); + + $old_styles = array( + 'padding: 0 2px;', + 'color: #333333;', + "background: {$old_bright};", + ); + $old_styles = implode(' ', $old_styles); + + $new_styles = array( + 'padding: 0 2px;', + 'color: #333333;', + "background: {$new_bright};", + ); + $new_styles = implode(' ', $new_styles); + + $omit_styles = array( + 'padding: 8px 0;', + ); + $omit_styles = implode(' ', $omit_styles); + + $result = array(); + foreach ($diff->getSummaryParts() as $part) { + $type = $part['type']; + $text = $part['text']; + switch ($type) { + case '.': + $result[] = phutil_tag( + 'div', + array( + 'style' => $omit_styles, + ), + pht('...')); + break; + case '-': + $result[] = phutil_tag( + 'span', + array( + 'style' => $old_styles, + ), + $text); + break; + case '+': + $result[] = phutil_tag( + 'span', + array( + 'style' => $new_styles, + ), + $text); + break; + case '=': + $result[] = $text; + break; + } + } + + $styles = array( + 'white-space: pre-wrap;', + 'color: #74777D;', + ); + + // Beyond applying "pre-wrap", convert newlines to "
" explicitly + // to improve behavior in clients like Airmail. + $result = phutil_escape_html_newlines($result); + + return phutil_tag( + 'div', + array( + 'style' => implode(' ', $styles), + ), + $result); + } + + public function render() { + $diff = $this->buildDiff(); + + require_celerity_resource('differential-changeset-view-css'); + + $result = array(); + foreach ($diff->getParts() as $part) { + $type = $part['type']; + $text = $part['text']; + switch ($type) { + case '-': + $result[] = phutil_tag( + 'span', + array( + 'class' => 'old', + ), + $text); + break; + case '+': + $result[] = phutil_tag( + 'span', + array( + 'class' => 'new', + ), + $text); + break; + case '=': + $result[] = $text; + break; + } + } + + $diff_view = phutil_tag( + 'div', + array( + 'class' => 'prose-diff', + ), + $result); + + $old_view = phutil_tag( + 'div', + array( + 'class' => 'prose-diff', + ), + $this->oldText); + + $new_view = phutil_tag( + 'div', + array( + 'class' => 'prose-diff', + ), + $this->newText); + + return id(new PHUITabGroupView()) + ->addTab( + id(new PHUITabView()) + ->setKey('old') + ->setName(pht('Old')) + ->appendChild($old_view)) + ->addTab( + id(new PHUITabView()) + ->setKey('new') + ->setName(pht('New')) + ->appendChild($new_view)) + ->addTab( + id(new PHUITabView()) + ->setKey('diff') + ->setName(pht('Diff')) + ->appendChild($diff_view)) + ->selectTab('diff'); + } + + private function buildDiff() { + $engine = new PhutilProseDifferenceEngine(); + return $engine->getDiff($this->oldText, $this->newText); + } +} diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionJSONDiffDetailView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionJSONDiffDetailView.php new file mode 100644 index 0000000000..91111c1e0b --- /dev/null +++ b/src/applications/transactions/view/PhabricatorApplicationTransactionJSONDiffDetailView.php @@ -0,0 +1,17 @@ +setNewText($json->encodeFormatted($new_object)); + return $this; + } + + public function setOld($old_object) { + $json = new PhutilJSON(); + $this->setOldText($json->encodeFormatted($old_object)); + return $this; + } +} diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionTextDiffDetailView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionTextDiffDetailView.php index 755d6a9fcf..d778d8e95d 100644 --- a/src/applications/transactions/view/PhabricatorApplicationTransactionTextDiffDetailView.php +++ b/src/applications/transactions/view/PhabricatorApplicationTransactionTextDiffDetailView.php @@ -1,174 +1,4 @@ newText = $new_text; - return $this; - } - - public function setOldText($old_text) { - $this->oldText = $old_text; - return $this; - } - - public function renderForMail() { - $diff = $this->buildDiff(); - - $viewer = $this->getViewer(); - $old_bright = $viewer->getCSSValue('old-bright'); - $new_bright = $viewer->getCSSValue('new-bright'); - - $old_styles = array( - 'padding: 0 2px;', - 'color: #333333;', - "background: {$old_bright};", - ); - $old_styles = implode(' ', $old_styles); - - $new_styles = array( - 'padding: 0 2px;', - 'color: #333333;', - "background: {$new_bright};", - ); - $new_styles = implode(' ', $new_styles); - - $omit_styles = array( - 'padding: 8px 0;', - ); - $omit_styles = implode(' ', $omit_styles); - - $result = array(); - foreach ($diff->getSummaryParts() as $part) { - $type = $part['type']; - $text = $part['text']; - switch ($type) { - case '.': - $result[] = phutil_tag( - 'div', - array( - 'style' => $omit_styles, - ), - pht('...')); - break; - case '-': - $result[] = phutil_tag( - 'span', - array( - 'style' => $old_styles, - ), - $text); - break; - case '+': - $result[] = phutil_tag( - 'span', - array( - 'style' => $new_styles, - ), - $text); - break; - case '=': - $result[] = $text; - break; - } - } - - $styles = array( - 'white-space: pre-wrap;', - 'color: #74777D;', - ); - - // Beyond applying "pre-wrap", convert newlines to "
" explicitly - // to improve behavior in clients like Airmail. - $result = phutil_escape_html_newlines($result); - - return phutil_tag( - 'div', - array( - 'style' => implode(' ', $styles), - ), - $result); - } - - public function render() { - $diff = $this->buildDiff(); - - require_celerity_resource('differential-changeset-view-css'); - - $result = array(); - foreach ($diff->getParts() as $part) { - $type = $part['type']; - $text = $part['text']; - switch ($type) { - case '-': - $result[] = phutil_tag( - 'span', - array( - 'class' => 'old', - ), - $text); - break; - case '+': - $result[] = phutil_tag( - 'span', - array( - 'class' => 'new', - ), - $text); - break; - case '=': - $result[] = $text; - break; - } - } - - $diff_view = phutil_tag( - 'div', - array( - 'class' => 'prose-diff', - ), - $result); - - $old_view = phutil_tag( - 'div', - array( - 'class' => 'prose-diff', - ), - $this->oldText); - - $new_view = phutil_tag( - 'div', - array( - 'class' => 'prose-diff', - ), - $this->newText); - - return id(new PHUITabGroupView()) - ->addTab( - id(new PHUITabView()) - ->setKey('old') - ->setName(pht('Old')) - ->appendChild($old_view)) - ->addTab( - id(new PHUITabView()) - ->setKey('new') - ->setName(pht('New')) - ->appendChild($new_view)) - ->addTab( - id(new PHUITabView()) - ->setKey('diff') - ->setName(pht('Diff')) - ->appendChild($diff_view)) - ->selectTab('diff'); - } - - private function buildDiff() { - $engine = new PhutilProseDifferenceEngine(); - return $engine->getDiff($this->oldText, $this->newText); - } - -} + extends PhabricatorApplicationTransactionDetailView {} diff --git a/src/applications/transactions/xaction/PhabricatorEditEngineCreateOrderTransaction.php b/src/applications/transactions/xaction/PhabricatorEditEngineCreateOrderTransaction.php new file mode 100644 index 0000000000..9a9483c227 --- /dev/null +++ b/src/applications/transactions/xaction/PhabricatorEditEngineCreateOrderTransaction.php @@ -0,0 +1,26 @@ +getCreateOrder(); + } + + public function generateNewValue($object, $value) { + return (int)$value; + } + + public function applyInternalEffects($object, $value) { + $object->setCreateOrder($value); + } + + public function getTitle() { + return pht( + '%s changed the order in which this form appears in the "Create" menu.', + $this->renderAuthor()); + } + +} diff --git a/src/applications/transactions/xaction/PhabricatorEditEngineDefaultCreateTransaction.php b/src/applications/transactions/xaction/PhabricatorEditEngineDefaultCreateTransaction.php new file mode 100644 index 0000000000..2c4a544013 --- /dev/null +++ b/src/applications/transactions/xaction/PhabricatorEditEngineDefaultCreateTransaction.php @@ -0,0 +1,35 @@ +getIsDefault(); + } + + public function generateNewValue($object, $value) { + return (int)$value; + } + + public function applyInternalEffects($object, $value) { + $object->setIsDefault($value); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + if ($new) { + return pht( + '%s added this form to the "Create" menu.', + $this->renderAuthor()); + } else { + return pht( + '%s removed this form from the "Create" menu.', + $this->renderAuthor()); + } + } + +} diff --git a/src/applications/transactions/xaction/PhabricatorEditEngineDefaultTransaction.php b/src/applications/transactions/xaction/PhabricatorEditEngineDefaultTransaction.php new file mode 100644 index 0000000000..7b980cdb1a --- /dev/null +++ b/src/applications/transactions/xaction/PhabricatorEditEngineDefaultTransaction.php @@ -0,0 +1,71 @@ +getMetadataValue('field.key'); + return $object->getFieldDefault($field_key); + } + + public function applyInternalEffects($object, $value) { + $field_key = $this->getMetadataValue('field.key'); + $object->setFieldDefault($field_key, $value); + } + + public function getTitle() { + $key = $this->getMetadataValue('field.key'); + $object = $this->getObject(); + $engine = $object->getEngine(); + $fields = $engine->getFieldsForConfig($object); + $field = idx($fields, $key); + + if (!$field) { + return pht( + '%s changed the default values for field %s.', + $this->renderAuthor(), + $this->renderValue($key)); + } + + return pht( + '%s changed the default value for field %s.', + $this->renderAuthor(), + $this->renderValue($field->getLabel())); + } + + public function hasChangeDetailView() { + return true; + } + + public function newChangeDetailView() { + $viewer = $this->getViewer(); + $old = $this->renderDefaultValueAsFallbackText($this->getOldValue()); + $new = $this->renderDefaultValueAsFallbackText($this->getNewValue()); + + return id(new PhabricatorApplicationTransactionTextDiffDetailView()) + ->setViewer($viewer) + ->setOldText($old) + ->setNewText($new); + } + + private function renderDefaultValueAsFallbackText($default_value) { + // See T13319. When rendering an "alice changed the default value for + // field X." story on custom forms, we may fall back to a generic + // rendering. Try to present the value change in a comprehensible way + // even if it isn't especially human readable (for example, it may + // contain PHIDs or other internal identifiers). + + if (is_scalar($default_value) || is_null($default_value)) { + return $default_value; + } + + if (phutil_is_natural_list($default_value)) { + return id(new PhutilJSON())->encodeAsList($default_value); + } + + return id(new PhutilJSON())->encodeAsObject($default_value); + } + +} diff --git a/src/applications/transactions/xaction/PhabricatorEditEngineDisableTransaction.php b/src/applications/transactions/xaction/PhabricatorEditEngineDisableTransaction.php new file mode 100644 index 0000000000..ef64c20974 --- /dev/null +++ b/src/applications/transactions/xaction/PhabricatorEditEngineDisableTransaction.php @@ -0,0 +1,51 @@ +getIsDisabled(); + } + + public function generateNewValue($object, $value) { + return (int)$value; + } + + public function applyInternalEffects($object, $value) { + $object->setIsDisabled($value); + } + + public function getTitle() { + $new = $this->getNewValue(); + if ($new) { + return pht( + '%s disabled this form.', + $this->renderAuthor()); + } else { + return pht( + '%s enabled this form.', + $this->renderAuthor()); + } + } + + public function getColor() { + $new = $this->getNewValue(); + if ($new) { + return 'indigo'; + } else { + return 'green'; + } + } + + public function getIcon() { + $new = $this->getNewValue(); + if ($new) { + return 'fa-ban'; + } else { + return 'fa-check'; + } + } + +} diff --git a/src/applications/transactions/xaction/PhabricatorEditEngineEditOrderTransaction.php b/src/applications/transactions/xaction/PhabricatorEditEngineEditOrderTransaction.php new file mode 100644 index 0000000000..a9109e6475 --- /dev/null +++ b/src/applications/transactions/xaction/PhabricatorEditEngineEditOrderTransaction.php @@ -0,0 +1,26 @@ +getEditOrder(); + } + + public function generateNewValue($object, $value) { + return (int)$value; + } + + public function applyInternalEffects($object, $value) { + $object->setEditOrder($value); + } + + public function getTitle() { + return pht( + '%s changed the order in which this form appears in the "Edit" menu.', + $this->renderAuthor()); + } + +} diff --git a/src/applications/transactions/xaction/PhabricatorEditEngineIsEditTransaction.php b/src/applications/transactions/xaction/PhabricatorEditEngineIsEditTransaction.php new file mode 100644 index 0000000000..a4a3a38543 --- /dev/null +++ b/src/applications/transactions/xaction/PhabricatorEditEngineIsEditTransaction.php @@ -0,0 +1,34 @@ +getIsEdit(); + } + + public function generateNewValue($object, $value) { + return (int)$value; + } + + public function applyInternalEffects($object, $value) { + $object->setIsEdit($value); + } + + public function getTitle() { + $new = $this->getNewValue(); + + if ($new) { + return pht( + '%s marked this form as an edit form.', + $this->renderAuthor()); + } else { + return pht( + '%s unmarked this form as an edit form.', + $this->renderAuthor()); + } + } + +} diff --git a/src/applications/transactions/xaction/PhabricatorEditEngineLocksTransaction.php b/src/applications/transactions/xaction/PhabricatorEditEngineLocksTransaction.php new file mode 100644 index 0000000000..b919a4de08 --- /dev/null +++ b/src/applications/transactions/xaction/PhabricatorEditEngineLocksTransaction.php @@ -0,0 +1,35 @@ +getFieldLocks(); + } + + public function applyInternalEffects($object, $value) { + $object->setFieldLocks($value); + } + + public function getTitle() { + return pht( + '%s changed locked and hidden fields.', + $this->renderAuthor()); + } + + public function hasChangeDetailView() { + return true; + } + + public function newChangeDetailView() { + $viewer = $this->getViewer(); + + return id(new PhabricatorApplicationTransactionJSONDiffDetailView()) + ->setViewer($viewer) + ->setOld($this->getOldValue()) + ->setNew($this->getNewValue()); + } + +} diff --git a/src/applications/transactions/xaction/PhabricatorEditEngineNameTransaction.php b/src/applications/transactions/xaction/PhabricatorEditEngineNameTransaction.php new file mode 100644 index 0000000000..0ab4b7029f --- /dev/null +++ b/src/applications/transactions/xaction/PhabricatorEditEngineNameTransaction.php @@ -0,0 +1,54 @@ +getName(); + } + + public function applyInternalEffects($object, $value) { + $object->setName($value); + } + + public function getTitle() { + if (strlen($this->getOldValue())) { + return pht( + '%s renamed this form from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } else { + return pht( + '%s named this form %s.', + $this->renderAuthor(), + $this->renderNewValue()); + } + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + foreach ($xactions as $xaction) { + $new = $xaction->getNewValue(); + if (!strlen($new)) { + $errors[] = $this->newRequiredError( + pht('Form name is required.'), + $xaction); + continue; + } + } + + if (!$errors) { + if ($this->isEmptyTextTransaction($object->getName(), $xactions)) { + $errors[] = $this->newRequiredError( + pht('Forms must have a name.')); + } + } + + return $errors; + } + +} diff --git a/src/applications/transactions/xaction/PhabricatorEditEngineOrderTransaction.php b/src/applications/transactions/xaction/PhabricatorEditEngineOrderTransaction.php new file mode 100644 index 0000000000..96376113c4 --- /dev/null +++ b/src/applications/transactions/xaction/PhabricatorEditEngineOrderTransaction.php @@ -0,0 +1,35 @@ +getFieldOrder(); + } + + public function applyInternalEffects($object, $value) { + $object->setFieldOrder($value); + } + + public function getTitle() { + return pht( + '%s reordered the fields in this form.', + $this->renderAuthor()); + } + + public function hasChangeDetailView() { + return true; + } + + public function newChangeDetailView() { + $viewer = $this->getViewer(); + + return id(new PhabricatorApplicationTransactionJSONDiffDetailView()) + ->setViewer($viewer) + ->setOld($this->getOldValue()) + ->setNew($this->getNewValue()); + } + +} diff --git a/src/applications/transactions/xaction/PhabricatorEditEnginePreambleTransaction.php b/src/applications/transactions/xaction/PhabricatorEditEnginePreambleTransaction.php new file mode 100644 index 0000000000..e450bc592d --- /dev/null +++ b/src/applications/transactions/xaction/PhabricatorEditEnginePreambleTransaction.php @@ -0,0 +1,45 @@ +getPreamble(); + } + + public function applyInternalEffects($object, $value) { + $object->setPreamble($value); + } + + public function getTitle() { + return pht( + '%s updated the preamble for this form.', + $this->renderAuthor()); + } + + public function hasChangeDetailView() { + return true; + } + + public function newChangeDetailView() { + $viewer = $this->getViewer(); + + return id(new PhabricatorApplicationTransactionTextDiffDetailView()) + ->setViewer($viewer) + ->setOldText($this->getOldValue()) + ->setNewText($this->getNewValue()); + } + + public function newRemarkupChanges() { + $changes = array(); + + $changes[] = $this->newRemarkupChange() + ->setOldValue($this->getOldValue()) + ->setNewValue($this->getNewValue()); + + return $changes; + } + +} diff --git a/src/applications/transactions/xaction/PhabricatorEditEngineSubtypeTransaction.php b/src/applications/transactions/xaction/PhabricatorEditEngineSubtypeTransaction.php new file mode 100644 index 0000000000..a2a8538115 --- /dev/null +++ b/src/applications/transactions/xaction/PhabricatorEditEngineSubtypeTransaction.php @@ -0,0 +1,67 @@ +getSubtype(); + } + + public function applyInternalEffects($object, $value) { + $object->setSubtype($value); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + return pht( + '%s changed the subtype of this form from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + if (!$xactions) { + return $errors; + } + + $engine = $object->getEngine(); + + if (!$engine->supportsSubtypes()) { + foreach ($xactions as $xaction) { + $errors[] = $this->newInvalidError( + pht( + 'Edit engine (of class "%s") does not support subtypes, so '. + 'subtype transactions can not be applied to it.', + get_class($engine)), + $xaction); + } + return $errors; + } + + $map = $engine + ->setViewer($this->getActor()) + ->newSubtypeMap(); + + foreach ($xactions as $xaction) { + $new = $xaction->getNewValue(); + + if ($map->isValidSubtype($new)) { + continue; + } + + $errors[] = $this->newInvalidError( + pht('Subtype "%s" is not a valid subtype.', $new), + $xaction); + } + + return $errors; + } + +} diff --git a/src/applications/transactions/xaction/PhabricatorEditEngineTransactionType.php b/src/applications/transactions/xaction/PhabricatorEditEngineTransactionType.php new file mode 100644 index 0000000000..c49d1e4654 --- /dev/null +++ b/src/applications/transactions/xaction/PhabricatorEditEngineTransactionType.php @@ -0,0 +1,4 @@ + ...where `` is the account username you want to recover access to. This will generate a link which will log you in as the specified user. - -Managing Accounts with the Web Console -====================================== - -To manage accounts from the web, login as an administrator account and go to -`/people/` or click "People" on the homepage. Provided you're an admin, -you'll see options to create or edit accounts. - - -Manually Creating New Accounts -============================== - -There are two ways to manually create new accounts: via the web UI using -the "People" application (this is easiest), or via the CLI using the -`accountadmin` binary (this has a few more options). - -To use the CLI script, run: - - phabricator/ $ ./bin/accountadmin - -Some options (like changing certain account flags) are only available from -the CLI. You can also use this script to make a user -an administrator (if you accidentally remove your admin flag) or to create an -administrative account. +For more details on recovering access to accounts and unlocking objects, see +@{article:User Guide: Unlocking Objects}. Next Steps diff --git a/src/docs/user/configuration/custom_fields.diviner b/src/docs/user/configuration/custom_fields.diviner index ecb7382648..75d83fc8ba 100644 --- a/src/docs/user/configuration/custom_fields.diviner +++ b/src/docs/user/configuration/custom_fields.diviner @@ -121,6 +121,10 @@ When defining custom fields using a configuration option like supported in text, int and remarkup fields (optional). - **copy**: If true, this field's value will be copied when an object is created using another object as a template. + - **limit**: For control types which use a tokenizer control to let the user + select a list of values, this limits how many values can be selected. For + example, a "users" field with a limit of "1" will behave like the "Owner" + field in Maniphest and only allow selection of a single user. The `strings` value supports different strings per control type. They are: diff --git a/src/docs/user/configuration/managing_caches.diviner b/src/docs/user/configuration/managing_caches.diviner index 53b48250ea..e873b99d8c 100644 --- a/src/docs/user/configuration/managing_caches.diviner +++ b/src/docs/user/configuration/managing_caches.diviner @@ -41,7 +41,7 @@ with the `--help` flag to see options: This tool can purge caches in a granular way, but it's normally easiest to just purge all of the caches: - phabricator/ $ ./bin/cache purge --purge-all + phabricator/ $ ./bin/cache purge --all You can purge caches safely. The data they contain can always be rebuilt from other data if Phabricator needs it. diff --git a/src/docs/user/field/permanently_destroying_data.diviner b/src/docs/user/field/permanently_destroying_data.diviner new file mode 100644 index 0000000000..04907fc0be --- /dev/null +++ b/src/docs/user/field/permanently_destroying_data.diviner @@ -0,0 +1,92 @@ +@title Permanently Destroying Data +@group fieldmanual + +How to permanently destroy data and manage leaked secrets. + +Overview +======== + +Phabricator intentionally makes it difficult to permanently destroy data, but +provides a command-line tool for destroying objects if you're certain that +you want to destroy something. + +**Disable vs Destroy**: Most kinds of objects can be disabled, deactivated, +closed, or archived. These operations place them in inactive states and +preserve their transaction history. + +(NOTE) Disabling (rather than destroying) objects is strongly recommended +unless you have a very good reason to want to permanently destroy an object. + + +Destroying Data +=============== + +To permanently destroy an object, run this command from the command line: + +``` +phabricator/ $ ./bin/remove destroy +``` + +The `` may be an object monogram or PHID. For instance, you can use +`@alice` to destroy a particular user, or `T123` to destroy a particular task. + +(IMPORTANT) This operation is permanent and can not be undone. + + +CLI Access Required +=================== + +In almost all cases, Phabricator requires operational access from the CLI to +permanently destroy data. One major reason for this requirement is that it +limits the reach of an attacker who compromises a privileged account. + +The web UI is generally append-only and actions generally leave an audit +trail, usually in the transaction log. Thus, an attacker who compromises an +account but only gains access to the web UI usually can not do much permanent +damage and usually can not hide their actions or cover their tracks. + +Another reason that destroying data is hard is simply that it's permanent and +can not be undone, so there's no way to recover from mistakes. + + +Leaked Secrets +============== + +Sometimes you may want to destroy an object because it has leaked a secret, +like an API key or another credential. For example, an engineer might +accidentally send a change for review which includes a sensitive private key. + +No Phabricator command can rewind time, and once data is written to Phabricator +the cat is often out of the bag: it has often been transmitted to external +systems which Phabricator can not interact with via email, webhooks, API calls, +repository mirroring, CDN caching, and so on. You can try to clean up the mess, +but you're generally already too late. + +The `bin/remove destroy` command will make a reasonable attempt to completely +destroy objects, but this is just an attempt. It can not unsend email or uncall +the API, and no command can rewind time and undo a leak. + +**Revoking Credentials**: If Phabricator credentials were accidentally +disclosed, you can revoke them so they no longer function. See +@{article:Revoking Credentials} for more information. + + +Preventing Leaks +================ + +Because time can not be rewound, it is best to prevent sensitive data from +leaking in the first place. Phabricator supports some technical measures that +can make it more difficult to accidentally disclose secrets: + +**Differential Diff Herald Rules**: You can write "Differential Diff" rules +in Herald that reject diffs before they are written to disk by using the +"Block diff with message" action. + +These rules can reject diffs based on affected file names or file content. +This is a coarse tool, but rejecting diffs which contain strings like +`BEGIN RSA PRIVATE KEY` may make it more difficult to accidentally disclose +certain secrets. + +**Commit Content Herald Rules**: For hosted repositories, you can write +"Commit Hook: Commit Content" rules in Herald which reject pushes that contain +commit which match certain rules (like file name or file content rules). diff --git a/src/docs/user/userguide/diffusion_managing.diviner b/src/docs/user/userguide/diffusion_managing.diviner index aa52c1c475..138bc918bc 100644 --- a/src/docs/user/userguide/diffusion_managing.diviner +++ b/src/docs/user/userguide/diffusion_managing.diviner @@ -169,8 +169,8 @@ start working normally. Basics: Delete Repository ========================= -Repositories can not be deleted from the web UI, so this option is always -disabled. Clicking it gives you information about how to delete a repository. +Repositories can not be deleted from the web UI, so this option only gives you +information about how to delete a repository. Repositories can only be deleted from the command line, with `bin/remove`: @@ -178,9 +178,8 @@ Repositories can only be deleted from the command line, with `bin/remove`: $ ./bin/remove destroy ``` -WARNING: This command will issue you a dire warning about the severity of the -action you are taking. Heed this warning. You are **strongly discouraged** from -destroying repositories. Instead, deactivate them. +This command will permanently destroy the repository. For more information +about destroying things, see @{article:Permanently Destroying Data}. Policies diff --git a/src/docs/user/userguide/unlocking.diviner b/src/docs/user/userguide/unlocking.diviner new file mode 100644 index 0000000000..7dc29f69bd --- /dev/null +++ b/src/docs/user/userguide/unlocking.diviner @@ -0,0 +1,116 @@ +@title User Guide: Unlocking Objects +@group userguide + +Explains how to access locked or invisible objects and accounts. + +Overview +======== + +Phabricator tries to make it difficult for users to lock themselves out of +things, but you can occasionally end up in situations where no one has access +to an object that you need access to. + +For example, sometimes the only user who had edit permission for something has +left the organization, or you configured a "Phase of the Moon" policy rule and +the stars aren't currently aligned. + +You can use various CLI tools to unlock objects and accounts if you need to +regain access. + + +Unlocking Accounts +================== + +If you need to regain access to an object, the easiest approach is usually to +recover access to the account which owns it, then change the object policies +to be more open using the web UI. + +For example, if an important task was accidentally locked so that only a user +who is currently on vacation can edit it, you can log in as that user and +change the edit policy to something more permissive. + +To regain access to an account: + +``` +$ ./bin/auth recover +``` + +If the account you're recovering access to has MFA or other session prompts, +use the `--force-full-session` to bypass them: + +``` +$ ./bin/auth recover --force-full-session +``` + +In either case, the command will give you a link you a one-time link you can +use to access the account from the web UI. From there, you can open up objects +or change settings. + + +Unlocking MFA +============= + +You can completely strip MFA from a user account with: + +``` +$ ./bin/auth strip --user ... +``` + +For detailed help on managing and stripping MFA, see the instructions in +@{article:User Guide: Multi-Factor Authentication} + + +Unlocking Objects +================= + +If you aren't sure who owns an object, or no user account has access to an +object, you can directly change object policies from the CLI: + +``` +$ ./bin/policy unlock [--view ...] [--edit ...] [--owner ...] +``` + +To identify the object you want to unlock, you can specify an object name (like +`T123`) or a PHID as the `` parameter. + +Use the `--view` and `--edit` flags (and, for some objects, the `--owner` +flag) to specify new policies for the object. + +For example, to make task `T123` editable by user `@alice`, run: + +``` +$ ./bin/policy unlock T123 --edit alice +``` + +Not every object has mutable view and edit policies, and not every object has +an owner, so each flag only works on some types of objects. + +From here, you can log in to the web UI and change the relevant policies to +whatever you want to set them to. + + +No Enabled Users +================ + +If you accidentally disabled all administrator accounts, you can enable a +disabled account from the CLI like this: + +``` +$ ./bin/user enable --user +``` + +From here, recover the account or log in normally. + + +No Administrators +================= + +If you accidentally deleted all the administrator accounts, you can empower +a user as an administrator from the CLI like this: + +``` +$ ./bin/user empower --user +``` + +This will upgrade the user account from a regular account to an administrator +account. diff --git a/src/infrastructure/customfield/field/PhabricatorCustomField.php b/src/infrastructure/customfield/field/PhabricatorCustomField.php index c6c70a9614..9e2bf6895f 100644 --- a/src/infrastructure/customfield/field/PhabricatorCustomField.php +++ b/src/infrastructure/customfield/field/PhabricatorCustomField.php @@ -1648,6 +1648,7 @@ abstract class PhabricatorCustomField extends Phobject { $subtype_roles = array( self::ROLE_EDITENGINE, self::ROLE_VIEW, + self::ROLE_EDIT, ); $subtype_roles = array_fuse($subtype_roles); diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldDate.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldDate.php index 981d45b9b0..4aba7543e7 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldDate.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldDate.php @@ -226,20 +226,18 @@ final class PhabricatorStandardCustomFieldDate } } - - public function shouldAppearInConduitTransactions() { - // TODO: Dates are complicated and we don't yet support handling them from - // Conduit. - return false; - } - protected function newConduitSearchParameterType() { // TODO: Build a new "pair" type or similar. return null; } protected function newConduitEditParameterType() { - return new ConduitEpochParameterType(); + return id(new ConduitEpochParameterType()) + ->setAllowNull(!$this->getRequired()); + } + + protected function newExportFieldType() { + return new PhabricatorEpochExportField(); } } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldRemarkup.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldRemarkup.php index 7709233454..b0b9a3ef8e 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldRemarkup.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldRemarkup.php @@ -107,4 +107,8 @@ final class PhabricatorStandardCustomFieldRemarkup return new ConduitStringParameterType(); } + protected function newExportFieldType() { + return new PhabricatorStringExportField(); + } + } diff --git a/src/infrastructure/daemon/control/PhabricatorDaemonReference.php b/src/infrastructure/daemon/control/PhabricatorDaemonReference.php index d9f8180935..6c90e1eed4 100644 --- a/src/infrastructure/daemon/control/PhabricatorDaemonReference.php +++ b/src/infrastructure/daemon/control/PhabricatorDaemonReference.php @@ -1,128 +1,10 @@ setViewer(PhabricatorUser::getOmnipotentUser()) - ->withDaemonIDs($daemon_ids) - ->execute(); - } catch (AphrontQueryException $ex) { - // Ignore any issues here; getting this information only allows us - // to provide a more complete picture of daemon status, and we want - // these commands to work if the database is inaccessible. - } - - $logs = mpull($logs, null, 'getDaemonID'); - } - - // Support PID files that use the old daemon format, where each overseer - // had exactly one daemon. We can eventually remove this; they will still - // be stopped by `phd stop --force` even if we don't identify them here. - if (!$daemons && idx($dict, 'name')) { - $daemons = array( - array( - 'config' => array( - 'class' => idx($dict, 'name'), - 'argv' => idx($dict, 'argv', array()), - ), - ), - ); - } - - foreach ($daemons as $daemon) { - $ref = new PhabricatorDaemonReference(); - - // NOTE: This is the overseer PID, not the actual daemon process PID. - // This is correct for checking status and sending signals (the only - // things we do with it), but might be confusing. $daemon['pid'] has - // the daemon PID, and we could expose that if we had some use for it. - - $ref->pid = idx($dict, 'pid'); - $ref->start = idx($dict, 'start'); - - $config = idx($daemon, 'config', array()); - $ref->name = idx($config, 'class'); - $ref->argv = idx($config, 'argv', array()); - - $log = idx($logs, idx($daemon, 'id')); - if ($log) { - $ref->daemonLog = $log; - } - - $ref->pidFile = $path; - $refs[] = $ref; - } - - return $refs; - } - - public function updateStatus($new_status) { - if (!$this->daemonLog) { - return; - } - - try { - $this->daemonLog - ->setStatus($new_status) - ->save(); - } catch (AphrontQueryException $ex) { - // Ignore anything that goes wrong here. - } - } - - public function getPID() { - return $this->pid; - } - - public function getName() { - return $this->name; - } - - public function getArgv() { - return $this->argv; - } - - public function getEpochStarted() { - return $this->start; - } - - public function getPIDFile() { - return $this->pidFile; - } - - public function getDaemonLog() { - return $this->daemonLog; - } - - public function isRunning() { - return self::isProcessRunning($this->getPID()); - } - public static function isProcessRunning($pid) { if (!$pid) { return false; @@ -148,15 +30,4 @@ final class PhabricatorDaemonReference extends Phobject { return $is_running; } - public function waitForExit($seconds) { - $start = time(); - while (time() < $start + $seconds) { - usleep(100000); - if (!$this->isRunning()) { - return true; - } - } - return !$this->isRunning(); - } - } diff --git a/src/infrastructure/env/PhabricatorConfigLocalSource.php b/src/infrastructure/env/PhabricatorConfigLocalSource.php index 16dc43a9bc..fc1c83f812 100644 --- a/src/infrastructure/env/PhabricatorConfigLocalSource.php +++ b/src/infrastructure/env/PhabricatorConfigLocalSource.php @@ -65,4 +65,9 @@ final class PhabricatorConfigLocalSource extends PhabricatorConfigProxySource { return $path; } + public function getReadablePath() { + $path = $this->getConfigPath(); + return Filesystem::readablePath($path); + } + } diff --git a/src/infrastructure/export/field/PhabricatorEpochExportField.php b/src/infrastructure/export/field/PhabricatorEpochExportField.php index 4dffde5aa8..a4d03f72da 100644 --- a/src/infrastructure/export/field/PhabricatorEpochExportField.php +++ b/src/infrastructure/export/field/PhabricatorEpochExportField.php @@ -6,6 +6,10 @@ final class PhabricatorEpochExportField private $zone; public function getTextValue($value) { + if ($value === null) { + return ''; + } + if (!isset($this->zone)) { $this->zone = new DateTimeZone('UTC'); } @@ -21,12 +25,20 @@ final class PhabricatorEpochExportField } public function getNaturalValue($value) { + if ($value === null) { + return $value; + } + return (int)$value; } public function getPHPExcelValue($value) { $epoch = $this->getNaturalValue($value); + if ($epoch === null) { + return null; + } + $seconds_per_day = phutil_units('1 day in seconds'); $offset = ($seconds_per_day * 25569); diff --git a/src/infrastructure/export/format/PhabricatorExcelExportFormat.php b/src/infrastructure/export/format/PhabricatorExcelExportFormat.php index 606df393d0..e7135bd9db 100644 --- a/src/infrastructure/export/format/PhabricatorExcelExportFormat.php +++ b/src/infrastructure/export/format/PhabricatorExcelExportFormat.php @@ -14,12 +14,23 @@ final class PhabricatorExcelExportFormat } public function isExportFormatEnabled() { - // TODO: PHPExcel has a dependency on the PHP zip extension. We should test - // for that here, since it fatals if we don't have the ZipArchive class. + if (!extension_loaded('zip')) { + return false; + } + return @include_once 'PHPExcel.php'; } public function getInstallInstructions() { + if (!extension_loaded('zip')) { + return pht(<<addNodes($ancestry) - ->getTopographicallySortedNodes(); + ->getNodesInTopologicalOrder(); $ancestry = array_select_keys($ancestry, $order); diff --git a/src/infrastructure/markup/PhabricatorMarkupEngine.php b/src/infrastructure/markup/PhabricatorMarkupEngine.php index 3a63cb97a6..b9467e549d 100644 --- a/src/infrastructure/markup/PhabricatorMarkupEngine.php +++ b/src/infrastructure/markup/PhabricatorMarkupEngine.php @@ -42,7 +42,7 @@ final class PhabricatorMarkupEngine extends Phobject { private $objects = array(); private $viewer; private $contextObject; - private $version = 18; + private $version = 19; private $engineCaches = array(); private $auxiliaryConfig = array(); diff --git a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php index 978de0fcf8..a8300c9da2 100644 --- a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php +++ b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php @@ -1825,6 +1825,11 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery $table_map['rank'] = array( 'alias' => 'ft_rank', 'key' => PhabricatorSearchDocumentFieldType::FIELD_TITLE, + + // See T13345. Not every document has a title, so we want to LEFT JOIN + // this table to avoid excluding documents with no title that match + // the query in other fields. + 'optional' => true, ); $this->ferretTables = $table_map; @@ -2103,10 +2108,17 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery foreach ($this->ferretTables as $table) { $alias = $table['alias']; + if (empty($table['optional'])) { + $join_type = qsprintf($conn, 'JOIN'); + } else { + $join_type = qsprintf($conn, 'LEFT JOIN'); + } + $joins[] = qsprintf( $conn, - 'JOIN %T %T ON ft_doc.id = %T.documentID + '%Q %T %T ON ft_doc.id = %T.documentID AND %T.fieldKey = %s', + $join_type, $field_table, $alias, $alias, diff --git a/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php index 8780584f94..a770c326f9 100644 --- a/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php +++ b/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php @@ -233,7 +233,10 @@ abstract class PhabricatorPolicyAwareQuery extends PhabricatorOffsetPagedQuery { // number of records when the viewer can see few or none of them. See // T11773 for some discussion. $this->isOverheated = false; - $overheat_limit = $limit * 10; + + // See T13386. If we on an old offset-based paging workflow, we need + // to base the overheating limit on both the offset and limit. + $overheat_limit = $need * 10; $total_seen = 0; do { diff --git a/src/infrastructure/storage/lisk/LiskDAO.php b/src/infrastructure/storage/lisk/LiskDAO.php index 81005ab30d..0bbbdd83a7 100644 --- a/src/infrastructure/storage/lisk/LiskDAO.php +++ b/src/infrastructure/storage/lisk/LiskDAO.php @@ -1881,6 +1881,10 @@ abstract class LiskDAO extends Phobject ->getMaximumByteLengthForDataType($data_type); } + public function getSchemaPersistence() { + return null; + } + /* -( AphrontDatabaseTableRefInterface )----------------------------------- */ diff --git a/src/infrastructure/storage/management/PhabricatorStorageManagementAPI.php b/src/infrastructure/storage/management/PhabricatorStorageManagementAPI.php index e66ba784f7..b838c8a5d9 100644 --- a/src/infrastructure/storage/management/PhabricatorStorageManagementAPI.php +++ b/src/infrastructure/storage/management/PhabricatorStorageManagementAPI.php @@ -298,6 +298,14 @@ final class PhabricatorStorageManagementAPI extends Phobject { return self::isCharacterSetAvailableOnConnection($character_set, $conn); } + public function getClientCharset() { + if ($this->isCharacterSetAvailable('utf8mb4')) { + return 'utf8mb4'; + } else { + return 'utf8'; + } + } + public static function isCharacterSetAvailableOnConnection( $character_set, AphrontDatabaseConnection $conn) { diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php index c3b9a32327..ebe1c77f40 100644 --- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php +++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php @@ -14,7 +14,8 @@ final class PhabricatorStorageManagementDumpWorkflow 'name' => 'for-replica', 'help' => pht( 'Add __--master-data__ to the __mysqldump__ command, '. - 'generating a CHANGE MASTER statement in the output.'), + 'generating a CHANGE MASTER statement in the output. This '. + 'option also dumps all data, including caches.'), ), array( 'name' => 'output', @@ -54,6 +55,8 @@ final class PhabricatorStorageManagementDumpWorkflow $output_file = $args->getArg('output'); $is_compress = $args->getArg('compress'); $is_overwrite = $args->getArg('overwrite'); + $is_noindex = $args->getArg('no-indexes'); + $is_replica = $args->getArg('for-replica'); if ($is_compress) { if ($output_file === null) { @@ -79,6 +82,14 @@ final class PhabricatorStorageManagementDumpWorkflow } } + if ($is_replica && $is_noindex) { + throw new PhutilArgumentUsageException( + pht( + 'The "--for-replica" flag can not be used with the '. + '"--no-indexes" flag. Replication dumps must contain a complete '. + 'representation of database state.')); + } + if ($output_file !== null) { if (Filesystem::pathExists($output_file)) { if (!$is_overwrite) { @@ -94,8 +105,6 @@ final class PhabricatorStorageManagementDumpWorkflow $api = $this->getSingleAPI(); $patches = $this->getPatches(); - $with_indexes = !$args->getArg('no-indexes'); - $applied = $api->getAppliedPatches(); if ($applied === null) { throw new PhutilArgumentUsageException( @@ -119,6 +128,9 @@ final class PhabricatorStorageManagementDumpWorkflow $schemata = $actual_map[$ref_key]; $expect = $expect_map[$ref_key]; + $with_caches = $is_replica; + $with_indexes = !$is_noindex; + $targets = array(); foreach ($schemata->getDatabases() as $database_name => $database) { $expect_database = $expect->getDatabase($database_name); @@ -143,7 +155,7 @@ final class PhabricatorStorageManagementDumpWorkflow // When dumping tables, leave the data in cache tables in the // database. This will be automatically rebuild after the data // is restored and does not need to be persisted in backups. - $with_data = false; + $with_data = $with_caches; break; case PhabricatorConfigTableSchema::PERSISTENCE_INDEX: // When dumping tables, leave index data behind of the caller @@ -179,9 +191,11 @@ final class PhabricatorStorageManagementDumpWorkflow $argv = array(); $argv[] = '--hex-blob'; $argv[] = '--single-transaction'; - $argv[] = '--default-character-set=utf8'; - if ($args->getArg('for-replica')) { + $argv[] = '--default-character-set'; + $argv[] = $api->getClientCharset(); + + if ($is_replica) { $argv[] = '--master-data'; } @@ -285,10 +299,13 @@ final class PhabricatorStorageManagementDumpWorkflow $preamble = implode('', $preamble); $this->writeData($preamble, $file, $is_compress, $output_file); - $future = new ExecFuture('%C', $spec['command']); + // See T13328. The "mysql" command may produce output very quickly. + // Don't buffer more than a fixed amount. + $future = id(new ExecFuture('%C', $spec['command'])) + ->setReadBufferSize(32 * 1024 * 1024); $iterator = id(new FutureIterator(array($future))) - ->setUpdateInterval(0.100); + ->setUpdateInterval(0.010); foreach ($iterator as $ready) { list($stdout, $stderr) = $future->read(); $future->discardBuffers(); @@ -337,7 +354,6 @@ final class PhabricatorStorageManagementDumpWorkflow return 0; } - private function writeData($data, $file, $is_compress, $output_file) { if (!strlen($data)) { return; diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementShellWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementShellWorkflow.php index 0bf185a086..f376ea3e14 100644 --- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementShellWorkflow.php +++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementShellWorkflow.php @@ -31,8 +31,8 @@ final class PhabricatorStorageManagementShellWorkflow } return phutil_passthru( - 'mysql --protocol=TCP --default-character-set=utf8mb4 '. - '-u %s %C -h %s %C', + 'mysql --protocol=TCP --default-character-set %R -u %s %C -h %s %C', + $api->getClientCharset(), $api->getUser(), $flag_password, $host, diff --git a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php index 2d5245459b..8adcfa64df 100644 --- a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php +++ b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php @@ -67,7 +67,9 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList { 'db.metamta' => array(), 'db.oauth_server' => array(), 'db.owners' => array(), - 'db.pastebin' => array(), + 'db.pastebin' => array( + 'dead' => true, + ), 'db.phame' => array(), 'db.phriction' => array(), 'db.project' => array(), @@ -113,6 +115,7 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList { 'db.badges' => array(), 'db.packages' => array(), 'db.application' => array(), + 'db.paste' => array(), '0000.legacy.sql' => array( 'legacy' => 0, ), diff --git a/src/view/AphrontDialogView.php b/src/view/AphrontDialogView.php index 52681845d2..09fc8e7a16 100644 --- a/src/view/AphrontDialogView.php +++ b/src/view/AphrontDialogView.php @@ -161,15 +161,36 @@ final class AphrontDialogView } public function appendParagraph($paragraph) { - return $this->appendChild( - phutil_tag( - 'p', - array( - 'class' => 'aphront-dialog-view-paragraph', - ), - $paragraph)); + return $this->appendParagraphTag($paragraph); } + public function appendCommand($command) { + $command_tag = phutil_tag('tt', array(), $command); + return $this->appendParagraphTag( + $command_tag, + 'aphront-dialog-view-command'); + } + + private function appendParagraphTag($content, $classes = null) { + if ($classes) { + $classes = (array)$classes; + } else { + $classes = array(); + } + + array_unshift($classes, 'aphront-dialog-view-paragraph'); + + $paragraph_tag = phutil_tag( + 'p', + array( + 'class' => implode(' ', $classes), + ), + $content); + + return $this->appendChild($paragraph_tag); + } + + public function appendList(array $items) { $listitems = array(); foreach ($items as $item) { diff --git a/src/view/layout/AphrontSideNavFilterView.php b/src/view/layout/AphrontSideNavFilterView.php index 9abcea5a02..8757a935b0 100644 --- a/src/view/layout/AphrontSideNavFilterView.php +++ b/src/view/layout/AphrontSideNavFilterView.php @@ -111,6 +111,11 @@ final class AphrontSideNavFilterView extends AphrontView { $key, $name, $uri, PHUIListItemView::TYPE_BUTTON); } + public function newLink($key) { + $this->addFilter($key, ''); + return $this->getMenuView()->getItem($key); + } + private function addThing($key, $name, $uri, $type, $icon = null) { $item = id(new PHUIListItemView()) ->setName($name) diff --git a/webroot/rsrc/css/aphront/dialog-view.css b/webroot/rsrc/css/aphront/dialog-view.css index 153685548e..b47ca14850 100644 --- a/webroot/rsrc/css/aphront/dialog-view.css +++ b/webroot/rsrc/css/aphront/dialog-view.css @@ -158,6 +158,11 @@ margin-top: 16px; } +.aphront-dialog-view-command { + padding: 8px 16px; + background: {$greybackground}; +} + .device-desktop .aphront-dialog-flush .phui-oi-list-view { margin: 0; padding: 0; diff --git a/webroot/rsrc/css/application/phortune/phortune.css b/webroot/rsrc/css/application/phortune/phortune.css index 02199f93e6..0d739eeb80 100644 --- a/webroot/rsrc/css/application/phortune/phortune.css +++ b/webroot/rsrc/css/application/phortune/phortune.css @@ -7,15 +7,6 @@ height: 34px; } -.phortune-payment-method-list { - margin: 8px 24px 0; -} - -.phortune-payment-method-list button { - margin: 4px 0; - width: 100%; -} - .phortune-payment-onetime-list { width: 280px; } diff --git a/webroot/rsrc/externals/javelin/lib/Workflow.js b/webroot/rsrc/externals/javelin/lib/Workflow.js index d398d33774..25de547deb 100644 --- a/webroot/rsrc/externals/javelin/lib/Workflow.js +++ b/webroot/rsrc/externals/javelin/lib/Workflow.js @@ -104,7 +104,11 @@ JX.install('Workflow', { var link = event.getNode('tag:a'); // If the link is an anchor, or does not go anywhere, ignore the event. - var href = '' + link.getAttribute('href'); + var href = link.getAttribute('href'); + if (typeof href !== 'string') { + return; + } + if (!href.length || href[0] === '#') { return; } @@ -114,6 +118,11 @@ JX.install('Workflow', { return; } + // This link is really a dialog button which we'll handle elsewhere. + if (JX.Stratcom.hasSigil(link, 'jx-workflow-button')) { + return; + } + // Close the dialog. JX.Workflow._pop(); }, diff --git a/webroot/rsrc/js/application/projects/WorkboardBoard.js b/webroot/rsrc/js/application/projects/WorkboardBoard.js index 74c0bdf23e..cce0ed9f69 100644 --- a/webroot/rsrc/js/application/projects/WorkboardBoard.js +++ b/webroot/rsrc/js/application/projects/WorkboardBoard.js @@ -129,6 +129,43 @@ JX.install('WorkboardBoard', { start: function() { this._setupDragHandlers(); + // TODO: This is temporary code to make it easier to debug this workflow + // by pressing the "R" key. + var on_reload = JX.bind(this, this._reloadCards); + new JX.KeyboardShortcut('R', 'Reload Card State (Prototype)') + .setHandler(on_reload) + .register(); + + var board_phid = this.getPHID(); + + JX.Stratcom.listen('aphlict-server-message', null, function(e) { + var message = e.getData(); + + if (message.type != 'workboards') { + return; + } + + // Check if this update notification is about the currently visible + // board. If it is, update the board state. + + var found_board = false; + for (var ii = 0; ii < message.subscribers.length; ii++) { + var subscriber_phid = message.subscribers[ii]; + if (subscriber_phid === board_phid) { + found_board = true; + break; + } + } + + if (found_board) { + on_reload(); + } + }); + + JX.Stratcom.listen('aphlict-reconnect', null, function(e) { + on_reload(); + }); + for (var k in this._columns) { this._columns[k].redraw(); } @@ -551,15 +588,6 @@ JX.install('WorkboardBoard', { }, _oncardupdate: function(list, src_phid, dst_phid, after_phid, response) { - var src_column = this.getColumn(src_phid); - var dst_column = this.getColumn(dst_phid); - - var card = src_column.removeCard(response.objectPHID); - dst_column.addCard(card, after_phid); - - src_column.markForRedraw(); - dst_column.markForRedraw(); - this.updateCard(response); var sounds = response.sounds || []; @@ -570,42 +598,78 @@ JX.install('WorkboardBoard', { list.unlock(); }, - updateCard: function(response, options) { - options = options || {}; - options.dirtyColumns = options.dirtyColumns || {}; - + updateCard: function(response) { var columns = this.getColumns(); + var column_phid; + var card_phid; + var card_data; - var phid = response.objectPHID; + // The server may send us a full or partial update for a card. If we've + // received a full update, we're going to redraw the entire card and may + // need to change which columns it appears in. - for (var add_phid in response.columnMaps) { - var target_column = this.getColumn(add_phid); + // For a partial update, we've just received supplemental sorting or + // property information and do not need to perform a full redraw. + + // When we reload card state, edit a card, or move a card, we get a full + // update for the card. + + // Ween we move a card in a column, we may get a partial update for other + // visible cards in the column. + + + // Figure out which columns each card now appears in. For cards that + // have received a full update, we'll use this map to move them into + // the correct columns. + var update_map = {}; + for (column_phid in response.columnMaps) { + var target_column = this.getColumn(column_phid); if (!target_column) { // If the column isn't visible, don't try to add a card to it. continue; } - target_column.newCard(phid); + var column_map = response.columnMaps[column_phid]; + + for (var ii = 0; ii < column_map.length; ii++) { + card_phid = column_map[ii]; + if (!update_map[card_phid]) { + update_map[card_phid] = {}; + } + update_map[card_phid][column_phid] = true; + } } - var column_maps = response.columnMaps; - var natural_column; - for (var natural_phid in column_maps) { - natural_column = this.getColumn(natural_phid); - if (!natural_column) { - // Our view of the board may be out of date, so we might get back - // information about columns that aren't visible. Just ignore the - // position information for any columns we aren't displaying on the - // client. + // Process card removals. These are cases where the client still sees + // a particular card on a board but it has been removed on the server. + for (card_phid in response.cards) { + card_data = response.cards[card_phid]; + + if (!card_data.remove) { continue; } - natural_column.setNaturalOrder(column_maps[natural_phid]); + for (column_phid in columns) { + var column = columns[column_phid]; + + var card = column.getCard(card_phid); + if (card) { + column.removeCard(card_phid); + column.markForRedraw(); + } + } } - for (var card_phid in response.cards) { - var card_data = response.cards[card_phid]; + // Process partial updates for cards. This is supplemental data which + // we can just merge in without any special handling. + for (card_phid in response.cards) { + card_data = response.cards[card_phid]; + + if (card_data.remove) { + continue; + } + var card_template = this.getCardTemplate(card_phid); if (card_data.nodeHTMLTemplate) { @@ -626,6 +690,56 @@ JX.install('WorkboardBoard', { } } + // Process full updates for cards which we have a full update for. This + // may involve moving them between columns. + for (card_phid in response.cards) { + card_data = response.cards[card_phid]; + + if (!card_data.update) { + continue; + } + + for (column_phid in columns) { + var column = columns[column_phid]; + var card = column.getCard(card_phid); + + if (card) { + card.redraw(); + column.markForRedraw(); + } + + // Compare the server state to the client state, and add or remove + // cards on the client as necessary to synchronize them. + + if (update_map[card_phid] && update_map[card_phid][column_phid]) { + if (!card) { + column.newCard(card_phid); + column.markForRedraw(); + } + } else { + if (card) { + column.removeCard(card_phid); + column.markForRedraw(); + } + } + } + } + + var column_maps = response.columnMaps; + var natural_column; + for (var natural_phid in column_maps) { + natural_column = this.getColumn(natural_phid); + if (!natural_column) { + // Our view of the board may be out of date, so we might get back + // information about columns that aren't visible. Just ignore the + // position information for any columns we aren't displaying on the + // client. + continue; + } + + natural_column.setNaturalOrder(column_maps[natural_phid]); + } + var headers = response.headers; for (var jj = 0; jj < headers.length; jj++) { var header = headers[jj]; @@ -637,22 +751,6 @@ JX.install('WorkboardBoard', { .setEditProperties(header.editProperties); } - for (var column_phid in columns) { - var column = columns[column_phid]; - - var cards = column.getCards(); - for (var object_phid in cards) { - if (object_phid !== phid) { - continue; - } - - var card = cards[object_phid]; - card.redraw(); - - column.markForRedraw(); - } - } - this._redrawColumns(); }, @@ -663,6 +761,33 @@ JX.install('WorkboardBoard', { columns[k].redraw(); } } + }, + + _reloadCards: function() { + var state = {}; + + var columns = this.getColumns(); + for (var column_phid in columns) { + var cards = columns[column_phid].getCards(); + for (var card_phid in cards) { + state[card_phid] = this.getCardTemplate(card_phid).getVersion(); + } + } + + var data = { + state: JX.JSON.stringify(state), + order: this.getOrder() + }; + + var on_reload = JX.bind(this, this._onReloadResponse); + + new JX.Request(this.getController().getReloadURI(), on_reload) + .setData(data) + .send(); + }, + + _onReloadResponse: function(response) { + this.updateCard(response); } } diff --git a/webroot/rsrc/js/application/projects/WorkboardCardTemplate.js b/webroot/rsrc/js/application/projects/WorkboardCardTemplate.js index 58f3f9e97f..e3387a4e18 100644 --- a/webroot/rsrc/js/application/projects/WorkboardCardTemplate.js +++ b/webroot/rsrc/js/application/projects/WorkboardCardTemplate.js @@ -28,6 +28,11 @@ JX.install('WorkboardCardTemplate', { return this._phid; }, + getVersion: function() { + // TODO: For now, just return a constant version number. + return 1; + }, + setNodeHTMLTemplate: function(html) { this._html = html; return this; diff --git a/webroot/rsrc/js/application/projects/WorkboardController.js b/webroot/rsrc/js/application/projects/WorkboardController.js index 8fe88eb50c..da5d177bb9 100644 --- a/webroot/rsrc/js/application/projects/WorkboardController.js +++ b/webroot/rsrc/js/application/projects/WorkboardController.js @@ -21,6 +21,7 @@ JX.install('WorkboardController', { uploadURI: null, coverURI: null, moveURI: null, + reloadURI: null, chunkThreshold: null }, diff --git a/webroot/rsrc/js/application/projects/behavior-project-boards.js b/webroot/rsrc/js/application/projects/behavior-project-boards.js index bba6db7a49..26e5d90f8e 100644 --- a/webroot/rsrc/js/application/projects/behavior-project-boards.js +++ b/webroot/rsrc/js/application/projects/behavior-project-boards.js @@ -71,6 +71,7 @@ JX.behavior('project-boards', function(config, statics) { .setUploadURI(config.uploadURI) .setCoverURI(config.coverURI) .setMoveURI(config.moveURI) + .setReloadURI(config.reloadURI) .setChunkThreshold(config.chunkThreshold) .start(); } diff --git a/webroot/rsrc/js/core/behavior-object-selector.js b/webroot/rsrc/js/core/behavior-object-selector.js index 722cfdd562..b28dac0926 100644 --- a/webroot/rsrc/js/core/behavior-object-selector.js +++ b/webroot/rsrc/js/core/behavior-object-selector.js @@ -132,7 +132,7 @@ JX.behavior('phabricator-object-selector', function(config) { var select_object_link = JX.$N( 'a', - {href: h.uri, sigil: 'object-attacher'}, + {href: '#', sigil: 'object-attacher'}, h.name); var select_object_button = JX.$N(