diff --git a/src/applications/phortune/controller/merchant/PhortuneMerchantInvoiceCreateController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantInvoiceCreateController.php index 300525cdd2..8bd070853c 100644 --- a/src/applications/phortune/controller/merchant/PhortuneMerchantInvoiceCreateController.php +++ b/src/applications/phortune/controller/merchant/PhortuneMerchantInvoiceCreateController.php @@ -1,257 +1,258 @@ getUser(); $merchant = $this->loadMerchantAuthority(); if (!$merchant) { return new Aphront404Response(); } $this->setMerchant($merchant); $merchant_id = $merchant->getID(); $cancel_uri = $this->getApplicationURI("/merchant/{$merchant_id}/"); // Load the user to invoice, or prompt the viewer to select one. $target_user = null; $user_phid = head($request->getArr('userPHID')); if (!$user_phid) { $user_phid = $request->getStr('userPHID'); } if ($user_phid) { $target_user = id(new PhabricatorPeopleQuery()) ->setViewer($viewer) ->withPHIDs(array($user_phid)) ->executeOne(); } if (!$target_user) { $form = id(new AphrontFormView()) ->setUser($viewer) ->appendRemarkupInstructions(pht('Choose a user to invoice.')) ->appendControl( id(new AphrontFormTokenizerControl()) ->setLabel(pht('User')) ->setDatasource(new PhabricatorPeopleDatasource()) ->setName('userPHID') ->setLimit(1)); return $this->newDialog() ->setTitle(pht('Choose User')) ->appendForm($form) ->addCancelButton($cancel_uri) ->addSubmitButton(pht('Continue')); } // Load the account to invoice, or prompt the viewer to select one. $target_account = null; $account_phid = $request->getStr('accountPHID'); if ($account_phid) { $target_account = id(new PhortuneAccountQuery()) ->setViewer($viewer) ->withPHIDs(array($account_phid)) ->withMemberPHIDs(array($target_user->getPHID())) ->executeOne(); } if (!$target_account) { - $accounts = PhortuneAccountQuery::loadAccountsForUser( - $target_user, - PhabricatorContentSource::newFromRequest($request)); + $accounts = id(new PhortuneAccountQuery()) + ->setViewer($viewer) + ->withMemberPHIDs(array($target_user->getPHID())) + ->execute(); $form = id(new AphrontFormView()) ->setUser($viewer) ->addHiddenInput('userPHID', $target_user->getPHID()) ->appendRemarkupInstructions(pht('Choose which account to invoice.')) ->appendControl( id(new AphrontFormMarkupControl()) ->setLabel(pht('User')) ->setValue($viewer->renderHandle($target_user->getPHID()))) ->appendControl( id(new AphrontFormSelectControl()) ->setLabel(pht('Account')) ->setName('accountPHID') ->setValue($account_phid) ->setOptions(mpull($accounts, 'getName', 'getPHID'))); return $this->newDialog() ->setTitle(pht('Choose Account')) ->appendForm($form) ->addCancelButton($cancel_uri) ->addSubmitButton(pht('Continue')); } // Now we build the actual invoice. $title = pht('New Invoice'); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($title); $v_title = $request->getStr('title'); $e_title = true; $v_name = $request->getStr('name'); $e_name = true; $v_cost = $request->getStr('cost'); $e_cost = true; $v_desc = $request->getStr('description'); $v_quantity = 1; $e_quantity = null; $errors = array(); if ($request->isFormPost() && $request->getStr('invoice')) { $v_quantity = $request->getStr('quantity'); $e_title = null; $e_name = null; $e_cost = null; $e_quantity = null; if (!strlen($v_title)) { $e_title = pht('Required'); $errors[] = pht('You must title this invoice.'); } if (!strlen($v_name)) { $e_name = pht('Required'); $errors[] = pht('You must provide a name for this purchase.'); } if (!strlen($v_cost)) { $e_cost = pht('Required'); $errors[] = pht('You must provide a cost for this purchase.'); } else { try { $v_currency = PhortuneCurrency::newFromUserInput( $viewer, $v_cost); } catch (Exception $ex) { $errors[] = $ex->getMessage(); $e_cost = pht('Invalid'); } } if ((int)$v_quantity <= 0) { $e_quantity = pht('Invalid'); $errors[] = pht('Quantity must be a positive integer.'); } if (!$errors) { $unique = Filesystem::readRandomCharacters(16); $product = id(new PhortuneProductQuery()) ->setViewer($target_user) ->withClassAndRef('PhortuneAdHocProduct', $unique) ->executeOne(); $cart_implementation = new PhortuneAdHocCart(); $cart = $target_account->newCart( $target_user, $cart_implementation, $merchant); $cart ->setMetadataValue('adhoc.title', $v_title) ->setMetadataValue('adhoc.description', $v_desc); $purchase = $cart->newPurchase($target_user, $product) ->setBasePriceAsCurrency($v_currency) ->setQuantity((int)$v_quantity) ->setMetadataValue('adhoc.name', $v_name) ->save(); $cart ->setIsInvoice(1) ->save(); $cart->activateCart(); $cart_id = $cart->getID(); $uri = "/merchant/{$merchant_id}/cart/{$cart_id}/"; $uri = $this->getApplicationURI($uri); return id(new AphrontRedirectResponse())->setURI($uri); } } $form = id(new AphrontFormView()) ->setUser($viewer) ->addHiddenInput('userPHID', $target_user->getPHID()) ->addHiddenInput('accountPHID', $target_account->getPHID()) ->addHiddenInput('invoice', true) ->appendControl( id(new AphrontFormMarkupControl()) ->setLabel(pht('User')) ->setValue($viewer->renderHandle($target_user->getPHID()))) ->appendControl( id(new AphrontFormMarkupControl()) ->setLabel(pht('Account')) ->setValue($viewer->renderHandle($target_account->getPHID()))) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Invoice Title')) ->setName('title') ->setValue($v_title) ->setError($e_title)) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Purchase Name')) ->setName('name') ->setValue($v_name) ->setError($e_name)) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Purchase Cost')) ->setName('cost') ->setValue($v_cost) ->setError($e_cost)) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Quantity')) ->setName('quantity') ->setValue($v_quantity) ->setError($e_quantity)) ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel(pht('Invoice Description')) ->setName('description') ->setValue($v_desc)) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri) ->setValue(pht('Send Invoice'))); $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Details')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setFormErrors($errors) ->setForm($form); $header = id(new PHUIHeaderView()) ->setHeader($title) ->setHeaderIcon('fa-plus-square'); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setFooter(array( $box, )); $navigation = $this->buildSideNavView('orders'); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->setNavigation($navigation) ->appendChild($view); } } diff --git a/src/applications/phortune/query/PhortuneMerchantQuery.php b/src/applications/phortune/query/PhortuneMerchantQuery.php index b6cab7dbc2..aef7d8aaf1 100644 --- a/src/applications/phortune/query/PhortuneMerchantQuery.php +++ b/src/applications/phortune/query/PhortuneMerchantQuery.php @@ -1,132 +1,193 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withMemberPHIDs(array $member_phids) { $this->memberPHIDs = $member_phids; return $this; } public function needProfileImage($need) { $this->needProfileImage = $need; return $this; } public function newResultObject() { return new PhortuneMerchant(); } protected function loadPage() { return $this->loadStandardPage($this->newResultObject()); } protected function willFilterPage(array $merchants) { $query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(mpull($merchants, 'getPHID')) ->withEdgeTypes(array(PhortuneMerchantHasMemberEdgeType::EDGECONST)); $query->execute(); foreach ($merchants as $merchant) { $member_phids = $query->getDestinationPHIDs(array($merchant->getPHID())); $member_phids = array_reverse($member_phids); $merchant->attachMemberPHIDs($member_phids); } if ($this->needProfileImage) { $default = null; $file_phids = mpull($merchants, 'getProfileImagePHID'); $file_phids = array_filter($file_phids); if ($file_phids) { $files = id(new PhabricatorFileQuery()) ->setParentQuery($this) ->setViewer($this->getViewer()) ->withPHIDs($file_phids) ->execute(); $files = mpull($files, null, 'getPHID'); } else { $files = array(); } foreach ($merchants as $merchant) { $file = idx($files, $merchant->getProfileImagePHID()); if (!$file) { if (!$default) { $default = PhabricatorFile::loadBuiltin( $this->getViewer(), 'merchant.png'); } $file = $default; } $merchant->attachProfileImageFile($file); } } return $merchants; } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( $conn, - 'id IN (%Ld)', + 'merchant.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, - 'phid IN (%Ls)', + 'merchant.phid IN (%Ls)', $this->phids); } if ($this->memberPHIDs !== null) { $where[] = qsprintf( $conn, 'e.dst IN (%Ls)', $this->memberPHIDs); } return $where; } protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { $joins = parent::buildJoinClauseParts($conn); if ($this->memberPHIDs !== null) { $joins[] = qsprintf( $conn, - 'LEFT JOIN %T e ON m.phid = e.src AND e.type = %d', + 'LEFT JOIN %T e ON merchant.phid = e.src AND e.type = %d', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhortuneMerchantHasMemberEdgeType::EDGECONST); } return $joins; } public function getQueryApplicationClass() { return 'PhabricatorPhortuneApplication'; } protected function getPrimaryTableAlias() { - return 'm'; + return 'merchant'; + } + + public static function canViewersEditMerchants( + array $viewer_phids, + array $merchant_phids) { + + // See T13366 for some discussion. This is an unusual caching construct to + // make policy filtering of Accounts easier. + + foreach ($viewer_phids as $key => $viewer_phid) { + if (!$viewer_phid) { + unset($viewer_phids[$key]); + } + } + + if (!$viewer_phids) { + return array(); + } + + $cache_key = 'phortune.merchant.can-edit'; + $cache = PhabricatorCaches::getRequestCache(); + + $cache_data = $cache->getKey($cache_key); + if (!$cache_data) { + $cache_data = array(); + } + + $load_phids = array(); + foreach ($viewer_phids as $viewer_phid) { + if (!isset($cache_data[$viewer_phid])) { + $load_phids[] = $viewer_phid; + } + } + + $did_write = false; + foreach ($load_phids as $load_phid) { + $merchants = id(new self()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withMemberPHIDs(array($load_phid)) + ->execute(); + foreach ($merchants as $merchant) { + $cache_data[$load_phid][$merchant->getPHID()] = true; + $did_write = true; + } + } + + if ($did_write) { + $cache->setKey($cache_key, $cache_data); + } + + $results = array(); + foreach ($viewer_phids as $viewer_phid) { + foreach ($merchant_phids as $merchant_phid) { + if (!isset($cache_data[$viewer_phid][$merchant_phid])) { + continue; + } + $results[$viewer_phid][$merchant_phid] = true; + } + } + + return $results; } } diff --git a/src/applications/phortune/storage/PhortuneAccount.php b/src/applications/phortune/storage/PhortuneAccount.php index 6b31805671..182c80f40f 100644 --- a/src/applications/phortune/storage/PhortuneAccount.php +++ b/src/applications/phortune/storage/PhortuneAccount.php @@ -1,200 +1,208 @@ setBillingName('') ->setBillingAddress('') ->attachMerchantPHIDs(array()) ->attachMemberPHIDs(array()); } public static function createNewAccount( PhabricatorUser $actor, PhabricatorContentSource $content_source) { $account = self::initializeNewAccount($actor); $xactions = array(); $xactions[] = id(new PhortuneAccountTransaction()) ->setTransactionType(PhortuneAccountNameTransaction::TRANSACTIONTYPE) ->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', 'billingName' => 'text255', 'billingAddress' => 'text', ), ) + 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; } public function getURI() { return '/phortune/'.$this->getID().'/'; } public function getDetailsURI() { return urisprintf( '/phortune/account/%d/details/', $this->getID()); } public function getEmailAddressesURI() { return urisprintf( '/phortune/account/%d/addresses/', $this->getID()); } public function attachMerchantPHIDs(array $merchant_phids) { $this->merchantPHIDs = $merchant_phids; return $this; } public function getMerchantPHIDs() { return $this->assertAttached($this->merchantPHIDs); } public function writeMerchantEdge(PhortuneMerchant $merchant) { $edge_src = $this->getPHID(); $edge_type = PhortuneAccountHasMerchantEdgeType::EDGECONST; $edge_dst = $merchant->getPHID(); id(new PhabricatorEdgeEditor()) ->addEdge($edge_src, $edge_type, $edge_dst) ->save(); return $this; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhortuneAccountEditor(); } public function getApplicationTransactionTemplate() { return new PhortuneAccountTransaction(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: 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()); if (isset($members[$viewer->getPHID()])) { return true; } - // If the viewer is acting on behalf of a merchant, they can see - // payment accounts. + // See T13366. If the viewer can edit any merchant that this payment + // account has a relationship with, they can see the payment account. if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { - foreach ($viewer->getAuthorities() as $authority) { - if ($authority instanceof PhortuneMerchant) { - return true; - } + $viewer_phids = array($viewer->getPHID()); + $merchant_phids = $this->getMerchantPHIDs(); + + $any_edit = PhortuneMerchantQuery::canViewersEditMerchants( + $viewer_phids, + $merchant_phids); + + if ($any_edit) { + return true; } } return false; } public function describeAutomaticCapability($capability) { - return pht('Members of an account can always view and edit it.'); + return array( + pht('Members of an account can always view and edit it.'), + pht('Merchants an account has established a relationship can view it.'), + ); } }