diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php index 2276a032cb..a44cb08a56 100644 --- a/src/applications/people/storage/PhabricatorUser.php +++ b/src/applications/people/storage/PhabricatorUser.php @@ -1,920 +1,941 @@ 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 = <<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 = <<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 ee573e2664..db3f52d61d 100644 --- a/src/applications/phortune/application/PhabricatorPhortuneApplication.php +++ b/src/applications/phortune/application/PhabricatorPhortuneApplication.php @@ -1,111 +1,117 @@ 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[^/]+)/)?' => 'PhortuneChargeListController', ), 'card/(?P\d+)/' => array( 'edit/' => 'PhortunePaymentMethodEditController', 'disable/' => 'PhortunePaymentMethodDisableController', ), 'cart/(?P\d+)/' => array( '' => 'PhortuneCartViewController', 'checkout/' => 'PhortuneCartCheckoutController', '(?Pcancel|refund)/' => 'PhortuneCartCancelController', 'update/' => 'PhortuneCartUpdateController', 'accept/' => 'PhortuneCartAcceptController', ), 'account/' => array( '' => 'PhortuneAccountListController', 'edit/(?:(?P\d+)/)?' => 'PhortuneAccountEditController', ), 'product/' => array( '' => 'PhortuneProductListController', 'view/(?P\d+)/' => 'PhortuneProductViewController', 'edit/(?:(?P\d+)/)?' => 'PhortuneProductEditController', ), 'provider/' => array( 'edit/(?:(?P\d+)/)?' => 'PhortuneProviderEditController', 'disable/(?P\d+)/' => 'PhortuneProviderDisableController', '(?P\d+)/(?P[^/]+)/' => 'PhortuneProviderActionController', ), 'merchant/' => array( '(?:query/(?P[^/]+)/)?' => 'PhortuneMerchantListController', 'edit/(?:(?P\d+)/)?' => 'PhortuneMerchantEditController', 'orders/(?P\d+)/(?:query/(?P[^/]+)/)?' => 'PhortuneCartListController', + '(?P\d+)/cart/(?P\d+)/' => array( + '' => 'PhortuneCartViewController', + '(?Pcancel|refund)/' => 'PhortuneCartCancelController', + 'update/' => 'PhortuneCartUpdateController', + 'accept/' => 'PhortuneCartAcceptController', + ), '(?P\d+)/subscription/' => array( '(?:query/(?P[^/]+)/)?' => 'PhortuneSubscriptionListController', 'view/(?P\d+)/' => 'PhortuneSubscriptionViewController', 'order/(?P\d+)/' => 'PhortuneCartListController', ), '(?P\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 bc73ecfd1a..f07fad3819 100644 --- a/src/applications/phortune/controller/PhortuneCartListController.php +++ b/src/applications/phortune/controller/PhortuneCartListController.php @@ -1,129 +1,130 @@ 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 e5557ca8fd..b9f3d19b44 100644 --- a/src/applications/phortune/controller/PhortuneCartViewController.php +++ b/src/applications/phortune/controller/PhortuneCartViewController.php @@ -1,282 +1,291 @@ 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 2900a3e850..655dcee4e1 100644 --- a/src/applications/phortune/controller/PhortuneController.php +++ b/src/applications/phortune/controller/PhortuneController.php @@ -1,87 +1,113 @@ 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 06fb1115a4..4fbdb804c3 100644 --- a/src/applications/phortune/controller/PhortuneSubscriptionListController.php +++ b/src/applications/phortune/controller/PhortuneSubscriptionListController.php @@ -1,106 +1,107 @@ 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 d9483271e0..e3c5d3e759 100644 --- a/src/applications/phortune/controller/PhortuneSubscriptionViewController.php +++ b/src/applications/phortune/controller/PhortuneSubscriptionViewController.php @@ -1,202 +1,203 @@ 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 df2003949c..e324d3f7c0 100644 --- a/src/applications/phortune/query/PhortuneCartQuery.php +++ b/src/applications/phortune/query/PhortuneCartQuery.php @@ -1,215 +1,223 @@ 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 72f4fa3469..83ef32b526 100644 --- a/src/applications/phortune/query/PhortuneCartSearchEngine.php +++ b/src/applications/phortune/query/PhortuneCartSearchEngine.php @@ -1,214 +1,227 @@ 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 87455e4bd9..c6b1fd878a 100644 --- a/src/applications/phortune/query/PhortunePaymentMethodQuery.php +++ b/src/applications/phortune/query/PhortunePaymentMethodQuery.php @@ -1,148 +1,156 @@ 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 c53aa002e0..facb9d5089 100644 --- a/src/applications/phortune/storage/PhortuneAccount.php +++ b/src/applications/phortune/storage/PhortuneAccount.php @@ -1,160 +1,170 @@ 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.'); } }