Page MenuHome GnuPG

No OneTemporary

diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php
index 2276a032c..a44cb08a5 100644
--- a/src/applications/people/storage/PhabricatorUser.php
+++ b/src/applications/people/storage/PhabricatorUser.php
@@ -1,920 +1,941 @@
<?php
/**
* @task factors Multi-Factor Authentication
*/
final class PhabricatorUser
extends PhabricatorUserDAO
implements
PhutilPerson,
PhabricatorPolicyInterface,
PhabricatorCustomFieldInterface,
PhabricatorDestructibleInterface,
PhabricatorSSHPublicKeyInterface {
const SESSION_TABLE = 'phabricator_session';
const NAMETOKEN_TABLE = 'user_nametoken';
const MAXIMUM_USERNAME_LENGTH = 64;
protected $userName;
protected $realName;
protected $sex;
protected $translation;
protected $passwordSalt;
protected $passwordHash;
protected $profileImagePHID;
protected $timezoneIdentifier = '';
protected $consoleEnabled = 0;
protected $consoleVisible = 0;
protected $consoleTab = '';
protected $conduitCertificate;
protected $isSystemAgent = 0;
protected $isAdmin = 0;
protected $isDisabled = 0;
protected $isEmailVerified = 0;
protected $isApproved = 0;
protected $isEnrolledInMultiFactor = 0;
protected $accountSecret;
private $profileImage = self::ATTACHABLE;
private $profile = null;
private $status = self::ATTACHABLE;
private $preferences = null;
private $omnipotent = false;
private $customFields = self::ATTACHABLE;
private $alternateCSRFString = self::ATTACHABLE;
private $session = self::ATTACHABLE;
+ private $authorities = array();
+
protected function readField($field) {
switch ($field) {
case 'timezoneIdentifier':
// If the user hasn't set one, guess the server's time.
return nonempty(
$this->timezoneIdentifier,
date_default_timezone_get());
// Make sure these return booleans.
case 'isAdmin':
return (bool)$this->isAdmin;
case 'isDisabled':
return (bool)$this->isDisabled;
case 'isSystemAgent':
return (bool)$this->isSystemAgent;
case 'isEmailVerified':
return (bool)$this->isEmailVerified;
case 'isApproved':
return (bool)$this->isApproved;
default:
return parent::readField($field);
}
}
/**
* Is this a live account which has passed required approvals? Returns true
* if this is an enabled, verified (if required), approved (if required)
* account, and false otherwise.
*
* @return bool True if this is a standard, usable account.
*/
public function isUserActivated() {
if ($this->isOmnipotent()) {
return true;
}
if ($this->getIsDisabled()) {
return false;
}
if (!$this->getIsApproved()) {
return false;
}
if (PhabricatorUserEmail::isEmailVerificationRequired()) {
if (!$this->getIsEmailVerified()) {
return false;
}
}
return true;
}
/**
* Returns `true` if this is a standard user who is logged in. Returns `false`
* for logged out, anonymous, or external users.
*
* @return bool `true` if the user is a standard user who is logged in with
* a normal session.
*/
public function getIsStandardUser() {
$type_user = PhabricatorPeopleUserPHIDType::TYPECONST;
return $this->getPHID() && (phid_get_type($this->getPHID()) == $type_user);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'userName' => 'sort64',
'realName' => 'text128',
'sex' => 'text4?',
'translation' => 'text64?',
'passwordSalt' => 'text32?',
'passwordHash' => 'text128?',
'profileImagePHID' => 'phid?',
'consoleEnabled' => 'bool',
'consoleVisible' => 'bool',
'consoleTab' => 'text64',
'conduitCertificate' => 'text255',
'isSystemAgent' => 'bool',
'isDisabled' => 'bool',
'isAdmin' => 'bool',
'timezoneIdentifier' => 'text255',
'isEmailVerified' => 'uint32',
'isApproved' => 'uint32',
'accountSecret' => 'bytes64',
'isEnrolledInMultiFactor' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'userName' => array(
'columns' => array('userName'),
'unique' => true,
),
'realName' => array(
'columns' => array('realName'),
),
'key_approved' => array(
'columns' => array('isApproved'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPeopleUserPHIDType::TYPECONST);
}
public function setPassword(PhutilOpaqueEnvelope $envelope) {
if (!$this->getPHID()) {
throw new Exception(
'You can not set a password for an unsaved user because their PHID '.
'is a salt component in the password hash.');
}
if (!strlen($envelope->openEnvelope())) {
$this->setPasswordHash('');
} else {
$this->setPasswordSalt(md5(Filesystem::readRandomBytes(32)));
$hash = $this->hashPassword($envelope);
$this->setPasswordHash($hash->openEnvelope());
}
return $this;
}
// To satisfy PhutilPerson.
public function getSex() {
return $this->sex;
}
public function getMonogram() {
return '@'.$this->getUsername();
}
public function isLoggedIn() {
return !($this->getPHID() === null);
}
public function save() {
if (!$this->getConduitCertificate()) {
$this->setConduitCertificate($this->generateConduitCertificate());
}
if (!strlen($this->getAccountSecret())) {
$this->setAccountSecret(Filesystem::readRandomCharacters(64));
}
$result = parent::save();
if ($this->profile) {
$this->profile->save();
}
$this->updateNameTokens();
id(new PhabricatorSearchIndexer())
->queueDocumentForIndexing($this->getPHID());
return $result;
}
public function attachSession(PhabricatorAuthSession $session) {
$this->session = $session;
return $this;
}
public function getSession() {
return $this->assertAttached($this->session);
}
public function hasSession() {
return ($this->session !== self::ATTACHABLE);
}
private function generateConduitCertificate() {
return Filesystem::readRandomCharacters(255);
}
public function comparePassword(PhutilOpaqueEnvelope $envelope) {
if (!strlen($envelope->openEnvelope())) {
return false;
}
if (!strlen($this->getPasswordHash())) {
return false;
}
return PhabricatorPasswordHasher::comparePassword(
$this->getPasswordHashInput($envelope),
new PhutilOpaqueEnvelope($this->getPasswordHash()));
}
private function getPasswordHashInput(PhutilOpaqueEnvelope $password) {
$input =
$this->getUsername().
$password->openEnvelope().
$this->getPHID().
$this->getPasswordSalt();
return new PhutilOpaqueEnvelope($input);
}
private function hashPassword(PhutilOpaqueEnvelope $password) {
$hasher = PhabricatorPasswordHasher::getBestHasher();
$input_envelope = $this->getPasswordHashInput($password);
return $hasher->getPasswordHashForStorage($input_envelope);
}
const CSRF_CYCLE_FREQUENCY = 3600;
const CSRF_SALT_LENGTH = 8;
const CSRF_TOKEN_LENGTH = 16;
const CSRF_BREACH_PREFIX = 'B@';
const EMAIL_CYCLE_FREQUENCY = 86400;
const EMAIL_TOKEN_LENGTH = 24;
private function getRawCSRFToken($offset = 0) {
return $this->generateToken(
time() + (self::CSRF_CYCLE_FREQUENCY * $offset),
self::CSRF_CYCLE_FREQUENCY,
PhabricatorEnv::getEnvConfig('phabricator.csrf-key'),
self::CSRF_TOKEN_LENGTH);
}
/**
* @phutil-external-symbol class PhabricatorStartup
*/
public function getCSRFToken() {
$salt = PhabricatorStartup::getGlobal('csrf.salt');
if (!$salt) {
$salt = Filesystem::readRandomCharacters(self::CSRF_SALT_LENGTH);
PhabricatorStartup::setGlobal('csrf.salt', $salt);
}
// Generate a token hash to mitigate BREACH attacks against SSL. See
// discussion in T3684.
$token = $this->getRawCSRFToken();
$hash = PhabricatorHash::digest($token, $salt);
return 'B@'.$salt.substr($hash, 0, self::CSRF_TOKEN_LENGTH);
}
public function validateCSRFToken($token) {
$salt = null;
$version = 'plain';
// This is a BREACH-mitigating token. See T3684.
$breach_prefix = self::CSRF_BREACH_PREFIX;
$breach_prelen = strlen($breach_prefix);
if (!strncmp($token, $breach_prefix, $breach_prelen)) {
$version = 'breach';
$salt = substr($token, $breach_prelen, self::CSRF_SALT_LENGTH);
$token = substr($token, $breach_prelen + self::CSRF_SALT_LENGTH);
}
// When the user posts a form, we check that it contains a valid CSRF token.
// Tokens cycle each hour (every CSRF_CYLCE_FREQUENCY seconds) and we accept
// either the current token, the next token (users can submit a "future"
// token if you have two web frontends that have some clock skew) or any of
// the last 6 tokens. This means that pages are valid for up to 7 hours.
// There is also some Javascript which periodically refreshes the CSRF
// tokens on each page, so theoretically pages should be valid indefinitely.
// However, this code may fail to run (if the user loses their internet
// connection, or there's a JS problem, or they don't have JS enabled).
// Choosing the size of the window in which we accept old CSRF tokens is
// an issue of balancing concerns between security and usability. We could
// choose a very narrow (e.g., 1-hour) window to reduce vulnerability to
// attacks using captured CSRF tokens, but it's also more likely that real
// users will be affected by this, e.g. if they close their laptop for an
// hour, open it back up, and try to submit a form before the CSRF refresh
// can kick in. Since the user experience of submitting a form with expired
// CSRF is often quite bad (you basically lose data, or it's a big pain to
// recover at least) and I believe we gain little additional protection
// by keeping the window very short (the overwhelming value here is in
// preventing blind attacks, and most attacks which can capture CSRF tokens
// can also just capture authentication information [sniffing networks]
// or act as the user [xss]) the 7 hour default seems like a reasonable
// balance. Other major platforms have much longer CSRF token lifetimes,
// like Rails (session duration) and Django (forever), which suggests this
// is a reasonable analysis.
$csrf_window = 6;
for ($ii = -$csrf_window; $ii <= 1; $ii++) {
$valid = $this->getRawCSRFToken($ii);
switch ($version) {
// TODO: We can remove this after the BREACH version has been in the
// wild for a while.
case 'plain':
if ($token == $valid) {
return true;
}
break;
case 'breach':
$digest = PhabricatorHash::digest($valid, $salt);
if (substr($digest, 0, self::CSRF_TOKEN_LENGTH) == $token) {
return true;
}
break;
default:
throw new Exception('Unknown CSRF token format!');
}
}
return false;
}
private function generateToken($epoch, $frequency, $key, $len) {
if ($this->getPHID()) {
$vec = $this->getPHID().$this->getAccountSecret();
} else {
$vec = $this->getAlternateCSRFString();
}
if ($this->hasSession()) {
$vec = $vec.$this->getSession()->getSessionKey();
}
$time_block = floor($epoch / $frequency);
$vec = $vec.$key.$time_block;
return substr(PhabricatorHash::digest($vec), 0, $len);
}
public function attachUserProfile(PhabricatorUserProfile $profile) {
$this->profile = $profile;
return $this;
}
public function loadUserProfile() {
if ($this->profile) {
return $this->profile;
}
$profile_dao = new PhabricatorUserProfile();
$this->profile = $profile_dao->loadOneWhere('userPHID = %s',
$this->getPHID());
if (!$this->profile) {
$profile_dao->setUserPHID($this->getPHID());
$this->profile = $profile_dao;
}
return $this->profile;
}
public function loadPrimaryEmailAddress() {
$email = $this->loadPrimaryEmail();
if (!$email) {
throw new Exception('User has no primary email address!');
}
return $email->getAddress();
}
public function loadPrimaryEmail() {
return $this->loadOneRelative(
new PhabricatorUserEmail(),
'userPHID',
'getPHID',
'(isPrimary = 1)');
}
public function loadPreferences() {
if ($this->preferences) {
return $this->preferences;
}
$preferences = null;
if ($this->getPHID()) {
$preferences = id(new PhabricatorUserPreferences())->loadOneWhere(
'userPHID = %s',
$this->getPHID());
}
if (!$preferences) {
$preferences = new PhabricatorUserPreferences();
$preferences->setUserPHID($this->getPHID());
$default_dict = array(
PhabricatorUserPreferences::PREFERENCE_TITLES => 'glyph',
PhabricatorUserPreferences::PREFERENCE_EDITOR => '',
PhabricatorUserPreferences::PREFERENCE_MONOSPACED => '',
PhabricatorUserPreferences::PREFERENCE_DARK_CONSOLE => 0,
);
$preferences->setPreferences($default_dict);
}
$this->preferences = $preferences;
return $preferences;
}
public function loadEditorLink($path, $line, $callsign) {
$editor = $this->loadPreferences()->getPreference(
PhabricatorUserPreferences::PREFERENCE_EDITOR);
if (is_array($path)) {
$multiedit = $this->loadPreferences()->getPreference(
PhabricatorUserPreferences::PREFERENCE_MULTIEDIT);
switch ($multiedit) {
case '':
$path = implode(' ', $path);
break;
case 'disable':
return null;
}
}
if (!strlen($editor)) {
return null;
}
$uri = strtr($editor, array(
'%%' => '%',
'%f' => phutil_escape_uri($path),
'%l' => phutil_escape_uri($line),
'%r' => phutil_escape_uri($callsign),
));
// The resulting URI must have an allowed protocol. Otherwise, we'll return
// a link to an error page explaining the misconfiguration.
$ok = PhabricatorHelpEditorProtocolController::hasAllowedProtocol($uri);
if (!$ok) {
return '/help/editorprotocol/';
}
return (string)$uri;
}
public function getAlternateCSRFString() {
return $this->assertAttached($this->alternateCSRFString);
}
public function attachAlternateCSRFString($string) {
$this->alternateCSRFString = $string;
return $this;
}
/**
* Populate the nametoken table, which used to fetch typeahead results. When
* a user types "linc", we want to match "Abraham Lincoln" from on-demand
* typeahead sources. To do this, we need a separate table of name fragments.
*/
public function updateNameTokens() {
$table = self::NAMETOKEN_TABLE;
$conn_w = $this->establishConnection('w');
$tokens = PhabricatorTypeaheadDatasource::tokenizeString(
$this->getUserName().' '.$this->getRealName());
$sql = array();
foreach ($tokens as $token) {
$sql[] = qsprintf(
$conn_w,
'(%d, %s)',
$this->getID(),
$token);
}
queryfx(
$conn_w,
'DELETE FROM %T WHERE userID = %d',
$table,
$this->getID());
if ($sql) {
queryfx(
$conn_w,
'INSERT INTO %T (userID, token) VALUES %Q',
$table,
implode(', ', $sql));
}
}
public function sendWelcomeEmail(PhabricatorUser $admin) {
$admin_username = $admin->getUserName();
$admin_realname = $admin->getRealName();
$user_username = $this->getUserName();
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
$base_uri = PhabricatorEnv::getProductionURI('/');
$engine = new PhabricatorAuthSessionEngine();
$uri = $engine->getOneTimeLoginURI(
$this,
$this->loadPrimaryEmail(),
PhabricatorAuthSessionEngine::ONETIME_WELCOME);
$body = <<<EOBODY
Welcome to Phabricator!
{$admin_username} ({$admin_realname}) has created an account for you.
Username: {$user_username}
To login to Phabricator, follow this link and set a password:
{$uri}
After you have set a password, you can login in the future by going here:
{$base_uri}
EOBODY;
if (!$is_serious) {
$body .= <<<EOBODY
Love,
Phabricator
EOBODY;
}
$mail = id(new PhabricatorMetaMTAMail())
->addTos(array($this->getPHID()))
->setForceDelivery(true)
->setSubject('[Phabricator] Welcome to Phabricator')
->setBody($body)
->saveAndSend();
}
public function sendUsernameChangeEmail(
PhabricatorUser $admin,
$old_username) {
$admin_username = $admin->getUserName();
$admin_realname = $admin->getRealName();
$new_username = $this->getUserName();
$password_instructions = null;
if (PhabricatorPasswordAuthProvider::getPasswordProvider()) {
$engine = new PhabricatorAuthSessionEngine();
$uri = $engine->getOneTimeLoginURI(
$this,
null,
PhabricatorAuthSessionEngine::ONETIME_USERNAME);
$password_instructions = <<<EOTXT
If you use a password to login, you'll need to reset it before you can login
again. You can reset your password by following this link:
{$uri}
And, of course, you'll need to use your new username to login from now on. If
you use OAuth to login, nothing should change.
EOTXT;
}
$body = <<<EOBODY
{$admin_username} ({$admin_realname}) has changed your Phabricator username.
Old Username: {$old_username}
New Username: {$new_username}
{$password_instructions}
EOBODY;
$mail = id(new PhabricatorMetaMTAMail())
->addTos(array($this->getPHID()))
->setForceDelivery(true)
->setSubject('[Phabricator] Username Changed')
->setBody($body)
->saveAndSend();
}
public static function describeValidUsername() {
return pht(
'Usernames must contain only numbers, letters, period, underscore and '.
'hyphen, and can not end with a period. They must have no more than %d '.
'characters.',
new PhutilNumber(self::MAXIMUM_USERNAME_LENGTH));
}
public static function validateUsername($username) {
// NOTE: If you update this, make sure to update:
//
// - Remarkup rule for @mentions.
// - Routing rule for "/p/username/".
// - Unit tests, obviously.
// - describeValidUsername() method, above.
if (strlen($username) > self::MAXIMUM_USERNAME_LENGTH) {
return false;
}
return (bool)preg_match('/^[a-zA-Z0-9._-]*[a-zA-Z0-9_-]\z/', $username);
}
public static function getDefaultProfileImageURI() {
return celerity_get_resource_uri('/rsrc/image/avatar.png');
}
public function attachStatus(PhabricatorCalendarEvent $status) {
$this->status = $status;
return $this;
}
public function getStatus() {
return $this->assertAttached($this->status);
}
public function hasStatus() {
return $this->status !== self::ATTACHABLE;
}
public function attachProfileImageURI($uri) {
$this->profileImage = $uri;
return $this;
}
public function getProfileImageURI() {
return $this->assertAttached($this->profileImage);
}
public function getFullName() {
if (strlen($this->getRealName())) {
return $this->getUsername().' ('.$this->getRealName().')';
} else {
return $this->getUsername();
}
}
public function __toString() {
return $this->getUsername();
}
public static function loadOneWithEmailAddress($address) {
$email = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$address);
if (!$email) {
return null;
}
return id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$email->getUserPHID());
}
+
+ /**
+ * 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;
+ }
+
+
/* -( Multi-Factor Authentication )---------------------------------------- */
/**
* Update the flag storing this user's enrollment in multi-factor auth.
*
* With certain settings, we need to check if a user has MFA on every page,
* so we cache MFA enrollment on the user object for performance. Calling this
* method synchronizes the cache by examining enrollment records. After
* updating the cache, use @{method:getIsEnrolledInMultiFactor} to check if
* the user is enrolled.
*
* This method should be called after any changes are made to a given user's
* multi-factor configuration.
*
* @return void
* @task factors
*/
public function updateMultiFactorEnrollment() {
$factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
$enrolled = count($factors) ? 1 : 0;
if ($enrolled !== $this->isEnrolledInMultiFactor) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
queryfx(
$this->establishConnection('w'),
'UPDATE %T SET isEnrolledInMultiFactor = %d WHERE id = %d',
$this->getTableName(),
$enrolled,
$this->getID());
unset($unguarded);
$this->isEnrolledInMultiFactor = $enrolled;
}
}
/**
* Check if the user is enrolled in multi-factor authentication.
*
* Enrolled users have one or more multi-factor authentication sources
* attached to their account. For performance, this value is cached. You
* can use @{method:updateMultiFactorEnrollment} to update the cache.
*
* @return bool True if the user is enrolled.
* @task factors
*/
public function getIsEnrolledInMultiFactor() {
return $this->isEnrolledInMultiFactor;
}
/* -( Omnipotence )-------------------------------------------------------- */
/**
* Returns true if this user is omnipotent. Omnipotent users bypass all policy
* checks.
*
* @return bool True if the user bypasses policy checks.
*/
public function isOmnipotent() {
return $this->omnipotent;
}
/**
* Get an omnipotent user object for use in contexts where there is no acting
* user, notably daemons.
*
* @return PhabricatorUser An omnipotent user.
*/
public static function getOmnipotentUser() {
static $user = null;
if (!$user) {
$user = new PhabricatorUser();
$user->omnipotent = true;
$user->makeEphemeral();
}
return $user;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::POLICY_PUBLIC;
case PhabricatorPolicyCapability::CAN_EDIT:
if ($this->getIsSystemAgent()) {
return PhabricatorPolicies::POLICY_ADMIN;
} else {
return PhabricatorPolicies::POLICY_NOONE;
}
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getPHID() && ($viewer->getPHID() === $this->getPHID());
}
public function describeAutomaticCapability($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_EDIT:
return pht('Only you can edit your information.');
default:
return null;
}
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return PhabricatorEnv::getEnvConfig('user.fields');
}
public function getCustomFieldBaseClass() {
return 'PhabricatorUserCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$externals = id(new PhabricatorExternalAccount())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($externals as $external) {
$external->delete();
}
$prefs = id(new PhabricatorUserPreferences())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($prefs as $pref) {
$pref->delete();
}
$profiles = id(new PhabricatorUserProfile())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($profiles as $profile) {
$profile->delete();
}
$keys = id(new PhabricatorAuthSSHKey())->loadAllWhere(
'objectPHID = %s',
$this->getPHID());
foreach ($keys as $key) {
$key->delete();
}
$emails = id(new PhabricatorUserEmail())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($emails as $email) {
$email->delete();
}
$sessions = id(new PhabricatorAuthSession())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($sessions as $session) {
$session->delete();
}
$factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($factors as $factor) {
$factor->delete();
}
$this->saveTransaction();
}
/* -( PhabricatorSSHPublicKeyInterface )----------------------------------- */
public function getSSHPublicKeyManagementURI(PhabricatorUser $viewer) {
if ($viewer->getPHID() == $this->getPHID()) {
// If the viewer is managing their own keys, take them to the normal
// panel.
return '/settings/panel/ssh/';
} else {
// Otherwise, take them to the administrative panel for this user.
return '/settings/'.$this->getID().'/panel/ssh/';
}
}
public function getSSHKeyDefaultName() {
return 'id_rsa_phabricator';
}
}
diff --git a/src/applications/phortune/application/PhabricatorPhortuneApplication.php b/src/applications/phortune/application/PhabricatorPhortuneApplication.php
index ee573e266..db3f52d61 100644
--- a/src/applications/phortune/application/PhabricatorPhortuneApplication.php
+++ b/src/applications/phortune/application/PhabricatorPhortuneApplication.php
@@ -1,111 +1,117 @@
<?php
final class PhabricatorPhortuneApplication extends PhabricatorApplication {
public function getName() {
return pht('Phortune');
}
public function getBaseURI() {
return '/phortune/';
}
public function getShortDescription() {
return pht('Accounts and Billing');
}
public function getFontIcon() {
return 'fa-diamond';
}
public function getTitleGlyph() {
return "\xE2\x97\x87";
}
public function getApplicationGroup() {
return self::GROUP_UTILITIES;
}
public function isPrototype() {
return true;
}
public function getRoutes() {
return array(
'/phortune/' => array(
'' => 'PhortuneLandingController',
'(?P<accountID>\d+)/' => array(
'' => 'PhortuneAccountViewController',
'card/' => array(
'new/' => 'PhortunePaymentMethodCreateController',
),
'order/(?:query/(?P<queryKey>[^/]+)/)?'
=> 'PhortuneCartListController',
'subscription/' => array(
'(?:query/(?P<queryKey>[^/]+)/)?'
=> 'PhortuneSubscriptionListController',
'view/(?P<id>\d+)/'
=> 'PhortuneSubscriptionViewController',
'edit/(?P<id>\d+)/'
=> 'PhortuneSubscriptionEditController',
'order/(?P<subscriptionID>\d+)/'
=> 'PhortuneCartListController',
),
'charge/(?:query/(?P<queryKey>[^/]+)/)?'
=> 'PhortuneChargeListController',
),
'card/(?P<id>\d+)/' => array(
'edit/' => 'PhortunePaymentMethodEditController',
'disable/' => 'PhortunePaymentMethodDisableController',
),
'cart/(?P<id>\d+)/' => array(
'' => 'PhortuneCartViewController',
'checkout/' => 'PhortuneCartCheckoutController',
'(?P<action>cancel|refund)/' => 'PhortuneCartCancelController',
'update/' => 'PhortuneCartUpdateController',
'accept/' => 'PhortuneCartAcceptController',
),
'account/' => array(
'' => 'PhortuneAccountListController',
'edit/(?:(?P<id>\d+)/)?' => 'PhortuneAccountEditController',
),
'product/' => array(
'' => 'PhortuneProductListController',
'view/(?P<id>\d+)/' => 'PhortuneProductViewController',
'edit/(?:(?P<id>\d+)/)?' => 'PhortuneProductEditController',
),
'provider/' => array(
'edit/(?:(?P<id>\d+)/)?' => 'PhortuneProviderEditController',
'disable/(?P<id>\d+)/' => 'PhortuneProviderDisableController',
'(?P<id>\d+)/(?P<action>[^/]+)/'
=> 'PhortuneProviderActionController',
),
'merchant/' => array(
'(?:query/(?P<queryKey>[^/]+)/)?' => 'PhortuneMerchantListController',
'edit/(?:(?P<id>\d+)/)?' => 'PhortuneMerchantEditController',
'orders/(?P<merchantID>\d+)/(?:query/(?P<queryKey>[^/]+)/)?'
=> 'PhortuneCartListController',
+ '(?P<merchantID>\d+)/cart/(?P<id>\d+)/' => array(
+ '' => 'PhortuneCartViewController',
+ '(?P<action>cancel|refund)/' => 'PhortuneCartCancelController',
+ 'update/' => 'PhortuneCartUpdateController',
+ 'accept/' => 'PhortuneCartAcceptController',
+ ),
'(?P<merchantID>\d+)/subscription/' => array(
'(?:query/(?P<queryKey>[^/]+)/)?'
=> 'PhortuneSubscriptionListController',
'view/(?P<id>\d+)/'
=> 'PhortuneSubscriptionViewController',
'order/(?P<subscriptionID>\d+)/'
=> 'PhortuneCartListController',
),
'(?P<id>\d+)/' => 'PhortuneMerchantViewController',
),
),
);
}
protected function getCustomCapabilities() {
return array(
PhortuneMerchantCapability::CAPABILITY => array(
'caption' => pht('Merchant accounts can receive payments.'),
'default' => PhabricatorPolicies::POLICY_ADMIN,
),
);
}
}
diff --git a/src/applications/phortune/controller/PhortuneCartListController.php b/src/applications/phortune/controller/PhortuneCartListController.php
index bc73ecfd1..f07fad381 100644
--- a/src/applications/phortune/controller/PhortuneCartListController.php
+++ b/src/applications/phortune/controller/PhortuneCartListController.php
@@ -1,129 +1,130 @@
<?php
final class PhortuneCartListController
extends PhortuneController {
private $merchant;
private $account;
private $subscription;
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$merchant_id = $request->getURIData('merchantID');
$account_id = $request->getURIData('accountID');
$subscription_id = $request->getURIData('subscriptionID');
$engine = new PhortuneCartSearchEngine();
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);
}
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($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()));
id(new PhortuneCartSearchEngine())
->setViewer($viewer)
->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/PhortuneCartViewController.php b/src/applications/phortune/controller/PhortuneCartViewController.php
index e5557ca8f..b9f3d19b4 100644
--- a/src/applications/phortune/controller/PhortuneCartViewController.php
+++ b/src/applications/phortune/controller/PhortuneCartViewController.php
@@ -1,282 +1,291 @@
<?php
final class PhortuneCartViewController
extends PhortuneCartController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
+ $authority = $this->loadMerchantAuthority();
+
+ // TODO: This (and the rest of the Cart controllers) need to be updated
+ // to use merchant URIs and merchant authority.
+
$cart = id(new PhortuneCartQuery())
->setViewer($viewer)
->withIDs(array($this->id))
->needPurchases(true)
->executeOne();
if (!$cart) {
return new Aphront404Response();
}
$can_admin = PhabricatorPolicyFilter::hasCapability(
$viewer,
$cart->getMerchant(),
PhabricatorPolicyCapability::CAN_EDIT);
$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_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 ($can_admin) {
$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_NOTICE)
->appendChild(pht('This purchase has been completed.'));
break;
}
$properties = $this->buildPropertyListView($cart);
$actions = $this->buildActionListView(
$cart,
$can_edit,
$can_admin,
$resume_uri);
$properties->setActionList($actions);
$header = id(new PHUIHeaderView())
->setUser($viewer)
->setHeader(pht('Order Detail'));
if ($cart->getStatus() == PhortuneCart::STATUS_PURCHASED) {
$done_uri = $cart->getDoneURI();
if ($done_uri) {
$header->addActionLink(
id(new PHUIButtonView())
->setTag('a')
->setHref($done_uri)
->setIcon(id(new PHUIIconView())
->setIconFont('fa-check-square green'))
->setText($cart->getDoneActionName()));
}
}
$cart_box = id(new PHUIObjectBoxView())
->setHeader($header)
->appendChild($properties)
->appendChild($cart_table);
if ($errors) {
$cart_box->setFormErrors($errors);
} else if ($error_view) {
$cart_box->setErrorView($error_view);
}
$charges = id(new PhortuneChargeQuery())
->setViewer($viewer)
->withCartPHIDs(array($cart->getPHID()))
->needCarts(true)
->execute();
$phids = array();
foreach ($charges as $charge) {
$phids[] = $charge->getProviderPHID();
$phids[] = $charge->getCartPHID();
$phids[] = $charge->getMerchantPHID();
$phids[] = $charge->getPaymentMethodPHID();
}
$handles = $this->loadViewerHandles($phids);
$charges_table = id(new PhortuneChargeTableView())
->setUser($viewer)
->setHandles($handles)
->setCharges($charges)
->setShowOrder(false);
$charges = id(new PHUIObjectBoxView())
->setHeaderText(pht('Charges'))
->appendChild($charges_table);
$account = $cart->getAccount();
$crumbs = $this->buildApplicationCrumbs();
- $this->addAccountCrumb($crumbs, $cart->getAccount());
+ if ($authority) {
+ $this->addMerchantCrumb($crumbs, $authority);
+ } else {
+ $this->addAccountCrumb($crumbs, $cart->getAccount());
+ }
$crumbs->addTextCrumb(pht('Cart %d', $cart->getID()));
$timeline = $this->buildTransactionTimeline(
$cart,
new PhortuneCartTransactionQuery());
$timeline
->setShouldTerminate(true);
return $this->buildApplicationPage(
array(
$crumbs,
$cart_box,
$charges,
$timeline,
),
array(
'title' => pht('Cart'),
));
}
private function buildPropertyListView(PhortuneCart $cart) {
$viewer = $this->getRequest()->getUser();
$view = id(new PHUIPropertyListView())
->setUser($viewer)
->setObject($cart);
$handles = $this->loadViewerHandles(
array(
$cart->getAccountPHID(),
$cart->getAuthorPHID(),
$cart->getMerchantPHID(),
));
$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 $view;
}
private function buildActionListView(
PhortuneCart $cart,
$can_edit,
$can_admin,
$resume_uri) {
$viewer = $this->getRequest()->getUser();
$id = $cart->getID();
$view = id(new PhabricatorActionListView())
->setUser($viewer)
->setObject($cart);
$can_cancel = ($can_edit && $cart->canCancelOrder());
$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/");
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Cancel Order'))
->setIcon('fa-times')
->setDisabled(!$can_cancel)
->setWorkflow(true)
->setHref($cancel_uri));
if ($can_admin) {
if ($cart->getStatus() == PhortuneCart::STATUS_REVIEW) {
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Accept Order'))
->setIcon('fa-check')
->setWorkflow(true)
->setHref($accept_uri));
}
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Refund Order'))
->setIcon('fa-reply')
->setWorkflow(true)
->setHref($refund_uri));
}
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Update Status'))
->setIcon('fa-refresh')
->setHref($update_uri));
if ($can_edit && $resume_uri) {
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Continue Checkout'))
->setIcon('fa-shopping-cart')
->setHref($resume_uri));
}
return $view;
}
}
diff --git a/src/applications/phortune/controller/PhortuneController.php b/src/applications/phortune/controller/PhortuneController.php
index 2900a3e85..655dcee4e 100644
--- a/src/applications/phortune/controller/PhortuneController.php
+++ b/src/applications/phortune/controller/PhortuneController.php
@@ -1,87 +1,113 @@
<?php
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();
$provider_configs = id(new PhortunePaymentProviderConfigQuery())
->setViewer($viewer)
->withMerchantPHIDs(array($merchant->getPHID()))
->execute();
$providers = mpull($provider_configs, 'buildProvider', 'getID');
foreach ($providers as $key => $provider) {
if (!$provider->isEnabled()) {
unset($providers[$key]);
}
}
return $providers;
}
protected function loadCreatePaymentMethodProvidersForMerchant(
PhortuneMerchant $merchant) {
$providers = $this->loadEnabledProvidersForMerchant($merchant);
foreach ($providers as $key => $provider) {
if (!$provider->canCreatePaymentMethods()) {
unset($providers[$key]);
continue;
}
}
return $providers;
}
protected function loadOneTimePaymentProvidersForMerchant(
PhortuneMerchant $merchant) {
$providers = $this->loadEnabledProvidersForMerchant($merchant);
foreach ($providers as $key => $provider) {
if (!$provider->canProcessOneTimePayments()) {
unset($providers[$key]);
continue;
}
}
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/PhortuneSubscriptionListController.php b/src/applications/phortune/controller/PhortuneSubscriptionListController.php
index 06fb1115a..4fbdb804c 100644
--- a/src/applications/phortune/controller/PhortuneSubscriptionListController.php
+++ b/src/applications/phortune/controller/PhortuneSubscriptionListController.php
@@ -1,106 +1,107 @@
<?php
final class PhortuneSubscriptionListController
extends PhortuneController {
private $accountID;
private $merchantID;
private $queryKey;
private $merchant;
private $account;
public function willProcessRequest(array $data) {
$this->merchantID = idx($data, 'merchantID');
$this->accountID = idx($data, 'accountID');
$this->queryKey = idx($data, 'queryKey');
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$engine = new PhortuneSubscriptionSearchEngine();
if ($this->merchantID) {
$merchant = id(new PhortuneMerchantQuery())
->setViewer($viewer)
->withIDs(array($this->merchantID))
->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 ($this->accountID) {
$account = id(new PhortuneAccountQuery())
->setViewer($viewer)
->withIDs(array($this->accountID))
->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($this->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()));
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/PhortuneSubscriptionViewController.php b/src/applications/phortune/controller/PhortuneSubscriptionViewController.php
index d9483271e..e3c5d3e75 100644
--- a/src/applications/phortune/controller/PhortuneSubscriptionViewController.php
+++ b/src/applications/phortune/controller/PhortuneSubscriptionViewController.php
@@ -1,202 +1,203 @@
<?php
final class PhortuneSubscriptionViewController extends PhortuneController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
+ $is_merchant = (bool)$this->loadMerchantAuthority();
+
$subscription = id(new PhortuneSubscriptionQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->needTriggers(true)
->executeOne();
if (!$subscription) {
return new Aphront404Response();
}
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$subscription,
PhabricatorPolicyCapability::CAN_EDIT);
- $is_merchant = (bool)$request->getURIData('merchantID');
$merchant = $subscription->getMerchant();
$account = $subscription->getAccount();
$account_id = $account->getID();
$subscription_id = $subscription->getID();
$title = $subscription->getSubscriptionFullName();
$header = id(new PHUIHeaderView())
->setHeader($title);
$actions = id(new PhabricatorActionListView())
->setUser($viewer)
->setObjectURI($request->getRequestURI());
$edit_uri = $subscription->getEditURI();
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setName(pht('Edit Subscription'))
->setHref($edit_uri)
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$crumbs = $this->buildApplicationCrumbs();
if ($is_merchant) {
$this->addMerchantCrumb($crumbs, $merchant);
} else {
$this->addAccountCrumb($crumbs, $account);
}
$crumbs->addTextCrumb($subscription->getSubscriptionCrumbName());
$properties = id(new PHUIPropertyListView())
->setUser($viewer)
->setActionList($actions);
$next_invoice = $subscription->getTrigger()->getNextEventPrediction();
$properties->addProperty(
pht('Next Invoice'),
phabricator_datetime($next_invoice, $viewer));
$default_method = $subscription->getDefaultPaymentMethodPHID();
if ($default_method) {
$handles = $this->loadViewerHandles(array($default_method));
$autopay_method = $handles[$default_method]->renderLink();
} else {
$autopay_method = phutil_tag(
'em',
array(),
pht('No Autopay Method Configured'));
}
$properties->addProperty(
pht('Autopay With'),
$autopay_method);
$object_box = id(new PHUIObjectBoxView())
->setHeader($header)
->addPropertyList($properties);
$due_box = $this->buildDueInvoices($subscription, $is_merchant);
$invoice_box = $this->buildPastInvoices($subscription, $is_merchant);
return $this->buildApplicationPage(
array(
$crumbs,
$object_box,
$due_box,
$invoice_box,
),
array(
'title' => $title,
));
}
private function buildDueInvoices(
PhortuneSubscription $subscription,
$is_merchant) {
$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($is_merchant)
->setHandles($handles);
$invoice_header = id(new PHUIHeaderView())
->setHeader(pht('Invoices Due'));
return id(new PHUIObjectBoxView())
->setHeader($invoice_header)
->appendChild($invoice_table);
}
private function buildPastInvoices(
PhortuneSubscription $subscription,
$is_merchant) {
$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 ($is_merchant) {
$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(
id(new PHUIIconView())
->setIconFont('fa-list'))
->setHref($invoices_uri)
->setText(pht('View All Invoices')));
return id(new PHUIObjectBoxView())
->setHeader($invoice_header)
->appendChild($invoice_table);
}
}
diff --git a/src/applications/phortune/query/PhortuneCartQuery.php b/src/applications/phortune/query/PhortuneCartQuery.php
index df2003949..e324d3f7c 100644
--- a/src/applications/phortune/query/PhortuneCartQuery.php
+++ b/src/applications/phortune/query/PhortuneCartQuery.php
@@ -1,215 +1,223 @@
<?php
final class PhortuneCartQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $accountPHIDs;
private $merchantPHIDs;
private $subscriptionPHIDs;
private $statuses;
private $invoices;
private $needPurchases;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withAccountPHIDs(array $account_phids) {
$this->accountPHIDs = $account_phids;
return $this;
}
public function withMerchantPHIDs(array $merchant_phids) {
$this->merchantPHIDs = $merchant_phids;
return $this;
}
public function withSubscriptionPHIDs(array $subscription_phids) {
$this->subscriptionPHIDs = $subscription_phids;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
/**
* Include or exclude carts which represent invoices with payments due.
*
* @param bool `true` to select invoices; `false` to exclude invoices.
* @return this
*/
public function withInvoices($invoices) {
$this->invoices = $invoices;
return $this;
}
public function needPurchases($need_purchases) {
$this->needPurchases = $need_purchases;
return $this;
}
protected function loadPage() {
$table = new PhortuneCart();
$conn = $table->establishConnection('r');
$rows = queryfx_all(
$conn,
'SELECT cart.* FROM %T cart %Q %Q %Q',
$table->getTableName(),
$this->buildWhereClause($conn),
$this->buildOrderClause($conn),
$this->buildLimitClause($conn));
return $table->loadAllFromArray($rows);
}
protected function willFilterPage(array $carts) {
$accounts = id(new PhortuneAccountQuery())
->setViewer($this->getViewer())
->withPHIDs(mpull($carts, 'getAccountPHID'))
->execute();
$accounts = mpull($accounts, null, 'getPHID');
foreach ($carts as $key => $cart) {
$account = idx($accounts, $cart->getAccountPHID());
if (!$account) {
unset($carts[$key]);
continue;
}
$cart->attachAccount($account);
}
+ if (!$carts) {
+ return array();
+ }
+
$merchants = id(new PhortuneMerchantQuery())
->setViewer($this->getViewer())
->withPHIDs(mpull($carts, 'getMerchantPHID'))
->execute();
$merchants = mpull($merchants, null, 'getPHID');
foreach ($carts as $key => $cart) {
$merchant = idx($merchants, $cart->getMerchantPHID());
if (!$merchant) {
unset($carts[$key]);
continue;
}
$cart->attachMerchant($merchant);
}
+ if (!$carts) {
+ return array();
+ }
+
$implementations = array();
$cart_map = mgroup($carts, 'getCartClass');
foreach ($cart_map as $class => $class_carts) {
$implementations += newv($class, array())->loadImplementationsForCarts(
$this->getViewer(),
$class_carts);
}
foreach ($carts as $key => $cart) {
$implementation = idx($implementations, $key);
if (!$implementation) {
unset($carts[$key]);
continue;
}
$cart->attachImplementation($implementation);
}
return $carts;
}
protected function didFilterPage(array $carts) {
if ($this->needPurchases) {
$purchases = id(new PhortunePurchaseQuery())
->setViewer($this->getViewer())
->setParentQuery($this)
->withCartPHIDs(mpull($carts, 'getPHID'))
->execute();
$purchases = mgroup($purchases, 'getCartPHID');
foreach ($carts as $cart) {
$cart->attachPurchases(idx($purchases, $cart->getPHID(), array()));
}
}
return $carts;
}
private function buildWhereClause(AphrontDatabaseConnection $conn) {
$where = array();
$where[] = $this->buildPagingClause($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'cart.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'cart.phid IN (%Ls)',
$this->phids);
}
if ($this->accountPHIDs !== null) {
$where[] = qsprintf(
$conn,
'cart.accountPHID IN (%Ls)',
$this->accountPHIDs);
}
if ($this->merchantPHIDs !== null) {
$where[] = qsprintf(
$conn,
'cart.merchantPHID IN (%Ls)',
$this->merchantPHIDs);
}
if ($this->subscriptionPHIDs !== null) {
$where[] = qsprintf(
$conn,
'cart.subscriptionPHID IN (%Ls)',
$this->subscriptionPHIDs);
}
if ($this->statuses !== null) {
$where[] = qsprintf(
$conn,
'cart.status IN (%Ls)',
$this->statuses);
}
if ($this->invoices !== null) {
if ($this->invoices) {
$where[] = qsprintf(
$conn,
'cart.status = %s AND cart.subscriptionPHID IS NOT NULL',
PhortuneCart::STATUS_READY);
} else {
$where[] = qsprintf(
$conn,
'cart.status != %s OR cart.subscriptionPHID IS NULL',
PhortuneCart::STATUS_READY);
}
}
return $this->formatWhereClause($where);
}
public function getQueryApplicationClass() {
return 'PhabricatorPhortuneApplication';
}
}
diff --git a/src/applications/phortune/query/PhortuneCartSearchEngine.php b/src/applications/phortune/query/PhortuneCartSearchEngine.php
index 72f4fa346..83ef32b52 100644
--- a/src/applications/phortune/query/PhortuneCartSearchEngine.php
+++ b/src/applications/phortune/query/PhortuneCartSearchEngine.php
@@ -1,214 +1,227 @@
<?php
final class PhortuneCartSearchEngine
extends PhabricatorApplicationSearchEngine {
private $merchant;
private $account;
private $subscription;
public function setAccount(PhortuneAccount $account) {
$this->account = $account;
return $this;
}
public function getAccount() {
return $this->account;
}
public function setMerchant(PhortuneMerchant $merchant) {
$this->merchant = $merchant;
return $this;
}
public function getMerchant() {
return $this->merchant;
}
public function setSubscription(PhortuneSubscription $subscription) {
$this->subscription = $subscription;
return $this;
}
public function getSubscription() {
return $this->subscription;
}
public function getResultTypeDescription() {
return pht('Phortune Orders');
}
public function getApplicationClassName() {
return 'PhabricatorPhortuneApplication';
}
public function buildSavedQueryFromRequest(AphrontRequest $request) {
$saved = new PhabricatorSavedQuery();
return $saved;
}
public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
$query = id(new PhortuneCartQuery())
->needPurchases(true)
->withStatuses(
array(
PhortuneCart::STATUS_PURCHASING,
PhortuneCart::STATUS_CHARGED,
PhortuneCart::STATUS_HOLD,
PhortuneCart::STATUS_REVIEW,
PhortuneCart::STATUS_PURCHASED,
));
$viewer = $this->requireViewer();
$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())
->withMemberPHIDs(array($viewer->getPHID()))
->execute();
if ($accounts) {
$query->withAccountPHIDs(mpull($accounts, 'getPHID'));
} else {
throw new Exception(pht('You have no accounts!'));
}
}
$subscription = $this->getSubscription();
if ($subscription) {
$query->withSubscriptionPHIDs(array($subscription->getPHID()));
}
return $query;
}
public function buildSearchForm(
AphrontFormView $form,
PhabricatorSavedQuery $saved_query) {}
protected function getURI($path) {
$merchant = $this->getMerchant();
$account = $this->getAccount();
if ($merchant) {
return '/phortune/merchant/'.$merchant->getID().'/order/'.$path;
} else if ($account) {
return '/phortune/'.$account->getID().'/order/';
} else {
return '/phortune/order/'.$path;
}
}
protected function getBuiltinQueryNames() {
$names = array(
'all' => pht('All Orders'),
);
return $names;
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
switch ($query_key) {
case 'all':
return $query;
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
protected function getRequiredHandlePHIDsForResultList(
array $carts,
PhabricatorSavedQuery $query) {
$phids = array();
foreach ($carts as $cart) {
$phids[] = $cart->getPHID();
$phids[] = $cart->getMerchantPHID();
$phids[] = $cart->getAuthorPHID();
}
return $phids;
}
protected function renderResultList(
array $carts,
PhabricatorSavedQuery $query,
array $handles) {
assert_instances_of($carts, 'PhortuneCart');
$viewer = $this->requireViewer();
$rows = array();
foreach ($carts as $cart) {
$merchant = $cart->getMerchant();
+ if ($this->getMerchant()) {
+ $href = $this->getApplicationURI(
+ 'merchant/'.$merchant->getID().'/cart/'.$cart->getID().'/');
+ } else {
+ $href = $cart->getDetailURI();
+ }
+
$rows[] = array(
$cart->getID(),
+ phutil_tag(
+ 'a',
+ array(
+ 'href' => $href,
+ ),
+ $cart->getName()),
$handles[$cart->getPHID()]->renderLink(),
$handles[$merchant->getPHID()]->renderLink(),
$handles[$cart->getAuthorPHID()]->renderLink(),
$cart->getTotalPriceAsCurrency()->formatForDisplay(),
PhortuneCart::getNameForStatus($cart->getStatus()),
phabricator_datetime($cart->getDateModified(), $viewer),
);
}
$table = id(new AphrontTableView($rows))
->setNoDataString(pht('No orders match the query.'))
->setHeaders(
array(
pht('ID'),
pht('Order'),
pht('Merchant'),
pht('Authorized By'),
pht('Amount'),
pht('Status'),
pht('Updated'),
))
->setColumnClasses(
array(
'',
'pri',
'',
'',
'wide right',
'',
'right',
));
$merchant = $this->getMerchant();
if ($merchant) {
$header = pht('Orders for %s', $merchant->getName());
} else {
$header = pht('Your Orders');
}
return id(new PHUIObjectBoxView())
->setHeaderText($header)
->appendChild($table);
}
}
diff --git a/src/applications/phortune/query/PhortunePaymentMethodQuery.php b/src/applications/phortune/query/PhortunePaymentMethodQuery.php
index 87455e4bd..c6b1fd878 100644
--- a/src/applications/phortune/query/PhortunePaymentMethodQuery.php
+++ b/src/applications/phortune/query/PhortunePaymentMethodQuery.php
@@ -1,148 +1,156 @@
<?php
final class PhortunePaymentMethodQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $accountPHIDs;
private $merchantPHIDs;
private $statuses;
public function withIDs(array $ids) {
$this->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 withMerchantPHIDs(array $phids) {
$this->merchantPHIDs = $phids;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
protected function loadPage() {
$table = new PhortunePaymentMethod();
$conn = $table->establishConnection('r');
$rows = queryfx_all(
$conn,
'SELECT * FROM %T %Q %Q %Q',
$table->getTableName(),
$this->buildWhereClause($conn),
$this->buildOrderClause($conn),
$this->buildLimitClause($conn));
return $table->loadAllFromArray($rows);
}
protected function willFilterPage(array $methods) {
$accounts = id(new PhortuneAccountQuery())
->setViewer($this->getViewer())
->withPHIDs(mpull($methods, 'getAccountPHID'))
->execute();
$accounts = mpull($accounts, null, 'getPHID');
foreach ($methods as $key => $method) {
$account = idx($accounts, $method->getAccountPHID());
if (!$account) {
unset($methods[$key]);
continue;
}
$method->attachAccount($account);
}
+ if (!$methods) {
+ return $methods;
+ }
+
$merchants = id(new PhortuneMerchantQuery())
->setViewer($this->getViewer())
->withPHIDs(mpull($methods, 'getMerchantPHID'))
->execute();
$merchants = mpull($merchants, null, 'getPHID');
foreach ($methods as $key => $method) {
$merchant = idx($merchants, $method->getMerchantPHID());
if (!$merchant) {
unset($methods[$key]);
continue;
}
$method->attachMerchant($merchant);
}
+ if (!$methods) {
+ return $methods;
+ }
+
$provider_configs = id(new PhortunePaymentProviderConfigQuery())
->setViewer($this->getViewer())
->withPHIDs(mpull($methods, 'getProviderPHID'))
->execute();
$provider_configs = mpull($provider_configs, null, 'getPHID');
foreach ($methods as $key => $method) {
$provider_config = idx($provider_configs, $method->getProviderPHID());
if (!$provider_config) {
unset($methods[$key]);
continue;
}
$method->attachProviderConfig($provider_config);
}
return $methods;
}
private function buildWhereClause(AphrontDatabaseConnection $conn) {
$where = array();
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
if ($this->accountPHIDs !== null) {
$where[] = qsprintf(
$conn,
'accountPHID IN (%Ls)',
$this->accountPHIDs);
}
if ($this->merchantPHIDs !== null) {
$where[] = qsprintf(
$conn,
'merchantPHID IN (%Ls)',
$this->merchantPHIDs);
}
if ($this->statuses !== null) {
$where[] = qsprintf(
$conn,
'status IN (%Ls)',
$this->statuses);
}
$where[] = $this->buildPagingClause($conn);
return $this->formatWhereClause($where);
}
public function getQueryApplicationClass() {
return 'PhabricatorPhortuneApplication';
}
}
diff --git a/src/applications/phortune/storage/PhortuneAccount.php b/src/applications/phortune/storage/PhortuneAccount.php
index c53aa002e..facb9d508 100644
--- a/src/applications/phortune/storage/PhortuneAccount.php
+++ b/src/applications/phortune/storage/PhortuneAccount.php
@@ -1,160 +1,170 @@
<?php
/**
* An account represents a purchasing entity. An account may have multiple users
* on it (e.g., several employees of a company have access to the company
* account), and a user may have several accounts (e.g., a company account and
* a personal account).
*/
final class PhortuneAccount extends PhortuneDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface {
protected $name;
private $memberPHIDs = self::ATTACHABLE;
public static function initializeNewAccount(PhabricatorUser $actor) {
$account = id(new PhortuneAccount());
$account->memberPHIDs = array();
return $account;
}
public static function createNewAccount(
PhabricatorUser $actor,
PhabricatorContentSource $content_source) {
$account = PhortuneAccount::initializeNewAccount($actor);
$xactions = array();
$xactions[] = id(new PhortuneAccountTransaction())
->setTransactionType(PhortuneAccountTransaction::TYPE_NAME)
->setNewValue(pht('Default Account'));
$xactions[] = id(new PhortuneAccountTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue(
'edge:type',
PhortuneAccountHasMemberEdgeType::EDGECONST)
->setNewValue(
array(
'=' => array($actor->getPHID() => $actor->getPHID()),
));
$editor = id(new PhortuneAccountEditor())
->setActor($actor)
->setContentSource($content_source);
// We create an account for you the first time you visit Phortune.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$editor->applyTransactions($account, $xactions);
unset($unguarded);
return $account;
}
public function newCart(
PhabricatorUser $actor,
PhortuneCartImplementation $implementation,
PhortuneMerchant $merchant) {
$cart = PhortuneCart::initializeNewCart($actor, $this, $merchant);
$cart->setCartClass(get_class($implementation));
$cart->attachImplementation($implementation);
$implementation->willCreateCart($actor, $cart);
return $cart->save();
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text255',
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhortuneAccountPHIDType::TYPECONST);
}
public function getMemberPHIDs() {
return $this->assertAttached($this->memberPHIDs);
}
public function attachMemberPHIDs(array $phids) {
$this->memberPHIDs = $phids;
return $this;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhortuneAccountEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhortuneAccountTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
- // Accounts are technically visible to all users, because merchant
- // controllers need to be able to see accounts in order to process
- // orders. We lock things down more tightly at the application level.
- return PhabricatorPolicies::POLICY_USER;
case PhabricatorPolicyCapability::CAN_EDIT:
if ($this->getPHID() === null) {
// Allow a user to create an account for themselves.
return PhabricatorPolicies::POLICY_USER;
} else {
return PhabricatorPolicies::POLICY_NOONE;
}
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
$members = array_fuse($this->getMemberPHIDs());
- return isset($members[$viewer->getPHID()]);
+ if (isset($members[$viewer->getPHID()])) {
+ return true;
+ }
+
+ // If the viewer is acting on behalf of a merchant, they can see
+ // payment accounts.
+ if ($capability == PhabricatorPolicyCapability::CAN_VIEW) {
+ foreach ($viewer->getAuthorities() as $authority) {
+ if ($authority instanceof PhortuneMerchant) {
+ return true;
+ }
+ }
+ }
+
+ return false;
}
public function describeAutomaticCapability($capability) {
return pht('Members of an account can always view and edit it.');
}
}

File Metadata

Mime Type
text/x-diff
Expires
Tue, Jul 8, 12:27 PM (1 d, 24 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
d4/54/158c3302a6653569be6dbb79de40

Event Timeline