<?php

namespace SilverShop\Checkout;

use SilverShop\Extension\MemberExtension;
use SilverStripe\Core\Config\Config;
use SilverStripe\ORM\ValidationException;
use SilverStripe\ORM\ValidationResult;
use SilverStripe\Security\Member;

class ShopMemberFactory
{
    /**
     * Create member account from data array.
     * Data must contain unique identifier.
     *
     * @throws ValidationException
     *
     * @param $data - map of member data
     *
     * @return Member - new member (not saved to db)
     */
    public function create($data)
    {
        $result = ValidationResult::create();
        if (!Checkout::member_creation_enabled()) {
            $result->addError(
                _t('SilverShop\Checkout\Checkout.MembershipIsNotAllowed', 'Creating new memberships is not allowed')
            );
            throw new ValidationException($result);
        }
        $idfield = Config::inst()->get(Member::class, 'unique_identifier_field');
        if (!isset($data[$idfield]) || empty($data[$idfield])) {
            $result->addError(
                _t(
                    'SilverShop\Checkout\Checkout.IdFieldNotFound',
                    'Required field not found: {IdentifierField}',
                    'Identifier is the field that holds the unique user-identifier, commonly this is \'Email\'',
                    ['IdentifierField' => $idfield]
                )
            );
            throw new ValidationException($result);
        }
        if (!isset($data['Password']) || empty($data['Password'])) {
            $result->addError(_t('SilverShop\Checkout\Checkout.PasswordRequired', 'A password is required'));
            throw new ValidationException($result);
        }
        $idval = $data[$idfield];
        if ($member = MemberExtension::get_by_identifier($idval)) {
            // get localized field labels
            $fieldLabels = $member->fieldLabels(false);
            // if a localized value exists, use this for our error-message
            $fieldLabel = isset($fieldLabels[$idfield]) ? $fieldLabels[$idfield] : $idfield;

            $result->addError(
                _t(
                    'SilverShop\Checkout\Checkout.MemberExists',
                    'A member already exists with the {Field} {Identifier}',
                    '',
                    ['Field' => $fieldLabel, 'Identifier' => $idval]
                )
            );
            throw new ValidationException($result);
        }

        /** @var Member $member */
        $member = Member::create()->update($data);
        // 3.2 changed validate to protected which made this fall through the DataExtension and error out
        $validation = $member->doValidate();
        if (!$validation->isValid()) {
            //TODO need to handle i18n here?
            foreach ($validation->getMessages() as $message) {
                $result->addError($message['message']);
            }
        }
        if (!$result->isValid()) {
            throw new ValidationException($result);
        }

        return $member;
    }
}


<?php

namespace SilverShop\Extension;

use SilverShop\Cart\ShoppingCart;
use SilverShop\Model\Address;
use SilverShop\Model\Order;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Forms\DropdownField;
use SilverStripe\Forms\FieldList;
use SilverStripe\ORM\DataExtension;
use SilverStripe\ORM\DataList;
use SilverStripe\Security\Member;
use SilverStripe\SiteConfig\SiteConfig;

/**
 * ShopMember provides customisations to {@link Member} for shop purposes
 */
class MemberExtension extends DataExtension
{
    private static $has_many = [
        'AddressBook' => Address::class,
    ];

    private static $has_one = [
        'DefaultShippingAddress' => Address::class,
        'DefaultBillingAddress' => Address::class,
    ];

    /**
     * Get member by unique field.
     *
     * @return Member|null
     */
    public static function get_by_identifier($idvalue)
    {
        return Member::get()->filter(
            Member::config()->unique_identifier_field,
            $idvalue
        )->first();
    }

    public function updateCMSFields(FieldList $fields)
    {
        $fields->removeByName('Country');
        $fields->removeByName('DefaultShippingAddressID');
        $fields->removeByName('DefaultBillingAddressID');
        $fields->addFieldToTab(
            'Root.Main',
            DropdownField::create(
                'Country',
                _t('SilverShop\Model\Address.db_Country', 'Country'),
                SiteConfig::current_site_config()->getCountriesList()
            )
        );
    }

    public function updateMemberFormFields($fields)
    {
        $fields->removeByName('DefaultShippingAddressID');
        $fields->removeByName('DefaultBillingAddressID');
        if ($gender = $fields->dataFieldByName('Gender')) {
            $gender->setHasEmptyDefault(true);
        }
    }

    /**
     * Link the current order to the current member on login,
     * if there is one, and if configuration is set to do so.
     */
    public function afterMemberLoggedIn()
    {
        if (Member::config()->login_joins_cart && $order = ShoppingCart::singleton()->current()) {
            $order->MemberID = $this->owner->ID;
            $order->write();
        }
    }

    /**
     * Clear the cart, and session variables on member logout
     */
    public function beforeMemberLoggedOut()
    {
        if (Member::config()->login_joins_cart) {
            ShoppingCart::singleton()->clear();
        }
    }

    /**
     * Get the past orders for this member
     *
     * @return DataList list of orders
     */
    public function getPastOrders()
    {
        return Order::get()
            ->filter('MemberID', $this->owner->ID)
            ->filter('Status:not', Order::config()->hidden_status);
    }
}


<?php

namespace SilverShop\Forms;

use SilverShop\Cart\ShoppingCart;
use SilverShop\Model\Buyable;
use SilverShop\Model\OrderItem;
use SilverShop\ShopTools;
use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\NumericField;
use SilverStripe\View\ViewableData;

class ShopQuantityField extends ViewableData
{
    /**
     * @var OrderItem
     */
    protected $item;

    protected $parameters;

    protected $classes = ['ajaxQuantityField'];

    protected $template = self::class;

    /**
     * @var Buyable
     */
    protected $buyable;

    /**
     * The max amount to enter
     *
     * @config
     * @var    int
     */
    private static $max = 0;

    public function __construct($object, $parameters = null)
    {
        parent::__construct();

        if ($object instanceof Buyable) {
            $this->item = ShoppingCart::singleton()->get($object, $parameters);
            //provide a 0-quantity facade item if there is no such item in cart
            if (!$this->item) {
                $this->item = $object->createItem();
            }
            $this->buyable = $object;
            //TODO: perhaps we should just store the product itself,
            //and do away with the facade, as it might be unnecessary complication
        } elseif ($object instanceof OrderItem) {
            $this->item = $object;
            $this->buyable = $object->Buyable();
        }
        if (!$this->item) {
            user_error('ShopQuantityField: no item or product passed to constructor.');
        }
        $this->parameters = $parameters;
        //TODO: include javascript for easy update
    }

    public function setClasses($newclasses, $overwrite = false)
    {
        if ($overwrite) {
            $this->classes = array_merge($this->classes, $newclasses);
        } else {
            $this->classes = $newclasses;
        }
    }

    public function setTemplate($template)
    {
        $this->template = $template;
    }

    public function Item()
    {
        return $this->item;
    }

    public function Quantity()
    {
        return $this->item->Quantity;
    }

    public function Field()
    {
        $field = NumericField::create(
            $this->MainID() . '_Quantity',
            // this title currently doesn't show up in the front end, better assign a translation anyway.
            _t('SilverShop\Model\Order.Quantity', 'Quantity'),
            $this->item->Quantity
        )->setHTML5(true);

        if ($this->config()->max > 0) {
            $field->setAttribute("max", $this->config()->max);
        }

        return $field;
    }

    public function MainID()
    {
        return ShopTools::sanitiseClassName(get_class($this->item)) . '_DB_' . $this->item->ID;
    }

    public function IncrementLink()
    {
        return $this->item->addLink();
    }

    public function DecrementLink()
    {
        return $this->item->removeLink();
    }

    public function forTemplate()
    {
        return $this->renderWith($this->template);
    }

    /**
     * Used for storing the quantity update link for ajax use.
     */
    public function AJAXLinkHiddenField()
    {
        if ($quantitylink = $this->item->setquantityLink()) {
            return HiddenField::create(
                $this->MainID() . '_Quantity_SetQuantityLink'
            )->setValue($quantitylink)->addExtraClass('ajaxQuantityField_qtylink');
        }
    }
}


<?php

namespace SilverShop\Checkout\Component;

use SilverShop\Model\Order;
use SilverStripe\Core\Config\Config;
use SilverStripe\Forms\CompositeField;
use SilverStripe\Forms\DropdownField;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\OptionsetField;
use SilverStripe\i18n\i18nEntityProvider;
use SilverStripe\ORM\ValidationException;
use SilverStripe\ORM\ValidationResult;
use SilverStripe\Security\Security;
use SilverStripe\View\Requirements;

/**
 * Adds the ability to use the member's address book for choosing addresses
 */
abstract class AddressBook extends Address implements i18nEntityProvider
{
    /**
     * The composite field tag to use
     *
     * @config
     * @var    string
     */
    private static $composite_field_tag = 'div';

    protected $addtoaddressbook = true;

    public function getFormFields(Order $order)
    {
        $fields = parent::getFormFields($order);

        if ($existingaddressfields = $this->getExistingAddressFields()) {
            Requirements::javascript('silverstripe/admin:thirdparty/jquery/jquery.js');
            Requirements::javascript('silvershop/core:client/dist/javascript/CheckoutPage.js');

            // add the fields for a new address after the dropdown field
            $existingaddressfields->merge($fields);
            // group under a composite field (invisible by default) so we
            // easily know which fields to show/hide
            $label = _t(
                "SilverShop\Model\Address.{$this->addresstype}Address",
                "{$this->addresstype} Address"
            );

            return FieldList::create(
                CompositeField::create($existingaddressfields)
                    ->addExtraClass('hasExistingValues')
                    ->setLegend($label)
                    ->setTag(Config::inst()->get(self::class, 'composite_field_tag'))
            );
        }

        return $fields;
    }

    /**
     * Allow choosing from an existing address
     *
     * @return FieldList|null fields for
     */
    public function getExistingAddressFields()
    {
        $member = Security::getCurrentUser();
        if ($member && $member->AddressBook()->exists()) {
            $addressoptions = $member->AddressBook()->sort('Created', 'DESC')->map('ID', 'toString')->toArray();
            $addressoptions['newaddress'] = _t('SilverShop\Model\Address.CreateNewAddress', 'Create new address');
            $fieldtype = count($addressoptions) > 3 ? DropdownField::class : OptionsetField::class;

            $label = _t("SilverShop\Model\Address.Existing{$this->addresstype}Address", "Existing {$this->addresstype} Address");

            return FieldList::create(
                $fieldtype::create(
                    $this->addresstype . 'AddressID',
                    $label,
                    $addressoptions,
                    $member->{'Default' . $this->addresstype . 'AddressID'}
                )->addExtraClass('existingValues')
            );
        }

        return null;
    }

    /**
     * We don't know at the front end which fields are required so we defer to validateData
     *
     * @param Order $order
     *
     * @return array
     */
    public function getRequiredFields(Order $order)
    {
        return [];
    }

    /**
     * @param Order $order
     * @param array $data
     *
     * @throws ValidationException
     */
    public function validateData(Order $order, array $data)
    {
        $result = ValidationResult::create();
        $existingID =
            !empty($data[$this->addresstype . 'AddressID']) ? (int)$data[$this->addresstype . 'AddressID'] : 0;

        if ($existingID) {
            $member = Security::getCurrentUser();
            // If existing address selected, check that it exists in $member->AddressBook
            if (!$member || !$member->AddressBook()->byID($existingID)) {
                $result->addError('Invalid address supplied', $this->addresstype . 'AddressID');
                throw new ValidationException($result);
            }
        } else {
            // Otherwise, require the normal address fields
            $required = parent::getRequiredFields($order);
            $addressLabels = singleton(\SilverShop\Model\Address::class)->fieldLabels(false);

            foreach ($required as $fieldName) {
                if (empty($data[$fieldName])) {
                    // attempt to get the translated field name
                    $fieldLabel = isset($addressLabels[$fieldName]) ? $addressLabels[$fieldName] : $fieldName;
                    $errorMessage = _t(
                        'SilverShop\Forms.FIELDISREQUIRED',
                        '{name} is required',
                        array('name' => $fieldLabel)
                    );

                    $result->addError($errorMessage, $fieldName);
                    throw new ValidationException($result);
                }
            }
        }
    }

    /**
     * Create a new address if the existing address has changed, or is not yet
     * created.
     *
     * @param  Order $order order to get addresses from
     * @param  array $data  data to set
     * @throws ValidationException
     */
    public function setData(Order $order, array $data)
    {
        $existingID =
            !empty($data[$this->addresstype . 'AddressID']) ? (int)$data[$this->addresstype . 'AddressID'] : 0;
        if ($existingID > 0) {
            $order->{$this->addresstype . 'AddressID'} = $existingID;
            $order->write();
            $order->extend('onSet' . $this->addresstype . 'Address', $address);
        } else {
            parent::setData($order, $data);
        }
    }

    /**
     * Provide translatable entities for this class
     *
     * @return array
     */
    public function provideI18nEntities()
    {
        if ($this->addresstype) {
            return [

                "SilverShop\Model\Address.{$this->addresstype}Address" => [
                    "{$this->addresstype} Address",
                    "Label for the {$this->addresstype} address",
                ],
                "SilverShop\Model\Address.Existing{$this->addresstype}Address" => [
                    "Existing {$this->addresstype} Address",
                    "Label to select an existing {$this->addresstype} Address",
                ],
            ];
        }

        return array();
    }
}


<?php

namespace SilverShop\Model;

use SilverShop\Cart\OrderTotalCalculator;
use SilverShop\Checkout\OrderEmailNotifier;
use SilverShop\Extension\MemberExtension;
use SilverShop\Extension\ShopConfigExtension;
use SilverShop\Model\Modifiers\OrderModifier;
use SilverShop\ORM\Filters\MultiFieldPartialMatchFilter;
use SilverShop\ORM\OrderItemList;
use SilverShop\Page\AccountPage;
use SilverShop\Page\CheckoutPage;
use SilverShop\ShopTools;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Director;
use SilverStripe\Dev\Debug;
use SilverStripe\Forms\CheckboxSetField;
use SilverStripe\Forms\DateField;
use SilverStripe\Forms\DropdownField;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\LiteralField;
use SilverStripe\Forms\Tab;
use SilverStripe\Forms\TabSet;
use SilverStripe\Omnipay\Extensions\Payable;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\FieldType\DBCurrency;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\FieldType\DBEnum;
use SilverStripe\ORM\Filters\GreaterThanFilter;
use SilverStripe\ORM\Filters\LessThanFilter;
use SilverStripe\ORM\HasManyList;
use SilverStripe\ORM\Search\SearchContext;
use SilverStripe\ORM\UnsavedRelationList;
use SilverStripe\Security\Member;
use SilverStripe\Security\Security;

/**
 * The order class is a databound object for handling Orders
 * within SilverStripe.
 *
 * @mixin Payable
 *
 * @property DBCurrency $Total
 * @property string $Reference
 * @property DBDatetime $Placed
 * @property DBDatetime $Paid
 * @property DBDatetime $ReceiptSent
 * @property DBDatetime $Printed
 * @property DBDatetime $Dispatched
 * @property DBEnum $Status
 * @property string $FirstName
 * @property string $Surname
 * @property string $Email
 * @property string $Notes
 * @property string $IPAddress
 * @property bool $SeparateBillingAddress
 * @property string $Locale
 * @property int $MemberID
 * @property int $ShippingAddressID
 * @property int $BillingAddressID
 * @method   Member|MemberExtension Member()
 * @method   Address BillingAddress()
 * @method   Address ShippingAddress()
 * @method   OrderItem[]|HasManyList Items()
 * @method   OrderModifier[]|HasManyList Modifiers()
 * @method   OrderStatusLog[]|HasManyList OrderStatusLogs()
 */
class Order extends DataObject
{
    /**
     * Status codes and what they mean:
     *
     * Unpaid (default): Order created but no successful payment by customer yet
     * Query: Order not being processed yet (customer has a query, or could be out of stock)
     * Paid: Order successfully paid for by customer
     * Processing: Order paid for, package is currently being processed before shipping to customer
     * Sent: Order paid for, processed for shipping, and now sent to the customer
     * Complete: Order completed (paid and shipped). Customer assumed to have received their goods
     * AdminCancelled: Order cancelled by the administrator
     * MemberCancelled: Order cancelled by the customer (Member)
     */
    private static $db = [
        'Total' => 'Currency',
        'Reference' => 'Varchar', //allow for customised order numbering schemes
        //status
        'Placed' => 'Datetime', //date the order was placed (went from Cart to Order)
        'Paid' => 'Datetime', //no outstanding payment left
        'ReceiptSent' => 'Datetime', //receipt emailed to customer
        'Printed' => 'Datetime',
        'Dispatched' => 'Datetime', //products have been sent to customer
        'Status' => "Enum('Unpaid,Paid,Processing,Sent,Complete,AdminCancelled,MemberCancelled,Cart','Cart')",
        //customer (for guest orders)
        'FirstName' => 'Varchar',
        'Surname' => 'Varchar',
        'Email' => 'Varchar',
        'Notes' => 'Text',
        'IPAddress' => 'Varchar(15)',
        //separate shipping
        'SeparateBillingAddress' => 'Boolean',
        // keep track of customer locale
        'Locale' => 'Locale',
    ];

    private static $has_one = [
        'Member' => Member::class,
        'ShippingAddress' => Address::class,
        'BillingAddress' => Address::class,
    ];

    private static $has_many = [
        'Items' => OrderItem::class,
        'Modifiers' => OrderModifier::class,
        'OrderStatusLogs' => OrderStatusLog::class,
    ];

    private static $defaults = [
        'Status' => 'Cart',
    ];

    private static $casting = [
        'FullBillingAddress' => 'Text',
        'FullShippingAddress' => 'Text',
        'Total' => 'Currency',
        'SubTotal' => 'Currency',
        'TotalPaid' => 'Currency',
        'Shipping' => 'Currency',
        'TotalOutstanding' => 'Currency',
    ];

    private static $summary_fields = [
        'Reference',
        'Placed',
        'Name',
        'LatestEmail',
        'Total',
        'StatusI18N',
    ];

    private static $searchable_fields = [
        'Reference',
        'Name',
        'Email',
        'Status' => [
            'filter' => 'ExactMatchFilter',
            'field' => CheckboxSetField::class,
        ],
    ];

    private static $table_name = 'SilverShop_Order';

    private static $singular_name = 'Order';

    private static $plural_name = 'Orders';

    private static $default_sort = '"Placed" DESC, "Created" DESC';

    /**
     * Statuses for orders that have been placed.
     *
     * @config
     */
    private static $placed_status = [
        'Paid',
        'Unpaid',
        'Processing',
        'Sent',
        'Complete',
        'MemberCancelled',
        'AdminCancelled',
    ];

    /**
     * Statuses for which an order can be paid for
     *
     * @config
     */
    private static $payable_status = [
        'Cart',
        'Unpaid',
        'Processing',
        'Sent',
    ];

    /**
     * Statuses that shouldn't show in user account.
     *
     * @config
     */
    private static $hidden_status = ['Cart'];

    /**
     * Statuses that should be logged in the Order-Status-Log
     *
     * @config
     * @var    array
     */
    private static $log_status = [];

    /**
     * Whether or not an order can be cancelled before payment
     *
     * @config
     * @var    bool
     */
    private static $cancel_before_payment = true;

    /**
     * Whether or not an order can be cancelled before processing
     *
     * @config
     * @var    bool
     */
    private static $cancel_before_processing = false;

    /**
     * Whether or not an order can be cancelled before sending
     *
     * @config
     * @var    bool
     */
    private static $cancel_before_sending = false;

    /**
     * Whether or not an order can be cancelled after sending
     *
     * @config
     * @var    bool
     */
    private static $cancel_after_sending = false;

    /**
     * Place an order before payment processing begins
     *
     * @config
     * @var    boolean
     */
    private static $place_before_payment = false;

    /**
     * Modifiers represent the additional charges or
     * deductions associated to an order, such as
     * shipping, taxes, vouchers etc.
     *
     * @config
     * @var    array
     */
    private static $modifiers = [];

    /**
     * Rounding precision of order amounts
     *
     * @config
     * @var    int
     */
    private static $rounding_precision = 2;

    /**
     * Minimal length (number of decimals) of order reference ids
     *
     * @config
     * @var    int
     */
    private static $reference_id_padding = 5;

    /**
     * Will allow completion of orders with GrandTotal=0,
     * which could be the case for orders paid with loyalty points or vouchers.
     * Will send the "Paid" date on the order, even though no actual payment was taken.
     * Will trigger the payment related extension points:
     * Order->onPayment, OrderItem->onPayment, Order->onPaid.
     *
     * @config
     * @var    boolean
     */
    private static $allow_zero_order_total = false;

    /**
     * A flag indicating that an order-status-log entry should be written
     *
     * @var bool
     */
    protected $flagOrderStatusWrite = false;

    public static function get_order_status_options()
    {
        $values = array();
        foreach (singleton(Order::class)->dbObject('Status')->enumValues(false) as $value) {
            $values[$value] = _t(__CLASS__ . '.STATUS_' . strtoupper($value), $value);
        }
        return $values;
    }

    /**
     * Create CMS fields for cms viewing and editing orders
     */
    public function getCMSFields()
    {
        $fields = FieldList::create(TabSet::create('Root', Tab::create('Main')));
        $fs = '
';
        $fe = '
';
        $parts = array(
            DropdownField::create('Status', $this->fieldLabel('Status'), self::get_order_status_options()),
            LiteralField::create('Customer', $fs . $this->renderWith('SilverShop\Admin\OrderAdmin_Customer') . $fe),
            LiteralField::create('Addresses', $fs . $this->renderWith('SilverShop\Admin\OrderAdmin_Addresses') . $fe),
            LiteralField::create('Content', $fs . $this->renderWith('SilverShop\Admin\OrderAdmin_Content') . $fe),
        );
        if ($this->Notes) {
            $parts[] = LiteralField::create('Notes', $fs . $this->renderWith('SilverShop\Admin\OrderAdmin_Notes') . $fe);
        }
        $fields->addFieldsToTab('Root.Main', $parts);
        $this->extend('updateCMSFields', $fields);
        if ($payments = $fields->fieldByName('Root.Payments.Payments')) {
            $fields->removeByName('Payments');
            $fields->insertAfter('Content', $payments);
            $payments->addExtraClass('order-payments');
        }

        return $fields;
    }

    /**
     * Augment field labels
     */
    public function fieldLabels($includerelations = true)
    {
        $labels = parent::fieldLabels($includerelations);

        $labels['Name'] = _t('SilverShop\Generic.Customer', 'Customer');
        $labels['LatestEmail'] = _t(__CLASS__ . '.db_Email', 'Email');
        $labels['StatusI18N'] = _t(__CLASS__ . '.db_Status', 'Status');

        return $labels;
    }

    /**
     * Adjust scafolded search context
     *
     * @return SearchContext the updated search context
     */
    public function getDefaultSearchContext()
    {
        $context = parent::getDefaultSearchContext();
        $fields = $context->getFields();

        $validStates = self::config()->placed_status;
        $statusOptions = array_filter(self::get_order_status_options(), function ($k) use ($validStates) {
            return in_array($k, $validStates);
        }, ARRAY_FILTER_USE_KEY);

        $fields->push(
            // TODO: Allow filtering by multiple statuses
            DropdownField::create('Status', $this->fieldLabel('Status'))
                ->setSource($statusOptions)
                ->setHasEmptyDefault(true)
        );

        // add date range filtering
        $fields->insertBefore(
            'Status',
            DateField::create('DateFrom', _t(__CLASS__ . '.DateFrom', 'Date from'))
        );

        $fields->insertBefore(
            'Status',
            DateField::create('DateTo', _t(__CLASS__ . '.DateTo', 'Date to'))
        );

        // get the array, to maniplulate name, and fullname seperately
        $filters = $context->getFilters();
        $filters['DateFrom'] = GreaterThanFilter::create('Placed');
        $filters['DateTo'] = LessThanFilter::create('Placed');

        // filter customer need to use a bunch of different sources
        $filters['Name'] = MultiFieldPartialMatchFilter::create(
            'FirstName',
            false,
            ['SplitWords'],
            [
                'Surname',
                'Member.FirstName',
                'Member.Surname',
                'BillingAddress.FirstName',
                'BillingAddress.Surname',
                'ShippingAddress.FirstName',
                'ShippingAddress.Surname',
            ]
        );

        $context->setFilters($filters);

        $this->extend('updateDefaultSearchContext', $context);
        return $context;
    }

    /**
     * Hack for swapping out relation list with OrderItemList
     *
     * @inheritdoc
     */
    public function getComponents($componentName, $id = null)
    {
        $components = parent::getComponents($componentName, $id);
        if ($componentName === 'Items' && get_class($components) !== UnsavedRelationList::class) {
            $query = $components->dataQuery();
            $components = OrderItemList::create(OrderItem::class, 'OrderID');
            $components->setDataQuery($query);
            $components = $components->forForeignID($this->ID);
        }
        return $components;
    }

    /**
     * Returns the subtotal of the items for this order.
     */
    public function SubTotal()
    {
        if ($this->Items()->exists()) {
            return $this->Items()->SubTotal();
        }

        return 0;
    }

    /**
     * Calculate the total
     *
     * @return float the final total
     */
    public function calculate()
    {
        if (!$this->IsCart()) {
            return $this->Total;
        }
        $calculator = OrderTotalCalculator::create($this);
        return $this->Total = $calculator->calculate();
    }

    /**
     * This is needed to maintain backwards compatiability with
     * some subsystems using modifiers. eg discounts
     */
    public function getModifier($className, $forcecreate = false)
    {
        $calculator = OrderTotalCalculator::create($this);
        return $calculator->getModifier($className, $forcecreate);
    }

    /**
     * Enforce rounding precision when setting total
     */
    public function setTotal($val)
    {
        $this->setField('Total', round($val, self::$rounding_precision));
    }

    /**
     * Get final value of order.
     * Retrieves value from DataObject's record array.
     */
    public function Total()
    {
        return $this->getField('Total');
    }

    /**
     * Alias for Total.
     */
    public function GrandTotal()
    {
        return $this->Total();
    }

    /**
     * Calculate how much is left to be paid on the order.
     * Enforces rounding precision.
     *
     * Payments that have been authorized via a non-manual gateway should count towards the total paid amount.
     * However, it's possible to exclude these by setting the $includeAuthorized parameter to false, which is
     * useful to determine the status of the Order. Order status should only change to 'Paid' when all
     * payments are 'Captured'.
     *
     * @param  bool $includeAuthorized whether or not to include authorized payments (excluding manual payments)
     * @return float
     */
    public function TotalOutstanding($includeAuthorized = true)
    {
        return round(
            $this->GrandTotal() - ($includeAuthorized ? $this->TotalPaidOrAuthorized() : $this->TotalPaid()),
            self::config()->rounding_precision
        );
    }

    /**
     * Get the order status. This will return a localized value if available.
     *
     * @return string the payment status
     */
    public function getStatusI18N()
    {
        return _t(__CLASS__ . '.STATUS_' . strtoupper($this->Status), $this->Status);
    }

    /**
     * Get the link for finishing order processing.
     */
    public function Link()
    {
        if (Security::getCurrentUser()) {
            $link = Controller::join_links(AccountPage::find_link(), 'order', $this->ID);
        }

        $link = CheckoutPage::find_link(false, 'order', $this->ID);

        $this->extend('updateLink', $link);

        return $link;
    }

    /**
     * Returns TRUE if the order can be cancelled
     * PRECONDITION: Order is in the DB.
     *
     * @return boolean
     */
    public function canCancel($member = null)
    {
        $extended = $this->extendedCan(__FUNCTION__, $member);
        if ($extended !== null) {
            return $extended;
        }

        switch ($this->Status) {
            case 'Unpaid' :
            return self::config()->cancel_before_payment;
            case 'Paid' :
            return self::config()->cancel_before_processing;
            case 'Processing' :
            return self::config()->cancel_before_sending;
            case 'Sent' :
            case 'Complete' :
            return self::config()->cancel_after_sending;
        }
        return false;
    }

    /**
     * Check if an order can be paid for.
     *
     * @return boolean
     */
    public function canPay($member = null)
    {
        $extended = $this->extendedCan(__FUNCTION__, $member);
        if ($extended !== null) {
            return $extended;
        }

        if (!in_array($this->Status, self::config()->payable_status)) {
            return false;
        }
        if ($this->TotalOutstanding(true) > 0 && empty($this->Paid)) {
            return true;
        }
        return false;
    }

    /**
     * Prevent deleting orders.
     *
     * @return boolean
     */
    public function canDelete($member = null)
    {
        $extended = $this->extendedCan(__FUNCTION__, $member);
        if ($extended !== null) {
            return $extended;
        }

        return false;
    }

    /**
     * Check if an order can be viewed.
     *
     * @return boolean
     */
    public function canView($member = null)
    {
        $extended = $this->extendedCan(__FUNCTION__, $member);
        if ($extended !== null) {
            return $extended;
        }

        return true;
    }

    /**
     * Check if an order can be edited.
     *
     * @return boolean
     */
    public function canEdit($member = null)
    {
        $extended = $this->extendedCan(__FUNCTION__, $member);
        if ($extended !== null) {
            return $extended;
        }

        return true;
    }

    /**
     * Prevent standard creation of orders.
     *
     * @return boolean
     */
    public function canCreate($member = null, $context = array())
    {
        $extended = $this->extendedCan(__FUNCTION__, $member, $context);
        if ($extended !== null) {
            return $extended;
        }

        return false;
    }

    /**
     * Return the currency of this order.
     * Note: this is a fixed value across the entire site.
     *
     * @return string
     */
    public function Currency()
    {
        return ShopConfigExtension::get_site_currency();
    }

    /**
     * Get the latest email for this order.z
     */
    public function getLatestEmail()
    {
        if ($this->MemberID && ($this->Member()->LastEdited > $this->LastEdited || !$this->Email)) {
            return $this->Member()->Email;
        }
        return $this->getField('Email');
    }

    /**
     * Gets the name of the customer.
     */
    public function getName()
    {
        $firstname = $this->FirstName ? $this->FirstName : $this->Member()->FirstName;
        $surname = $this->FirstName ? $this->Surname : $this->Member()->Surname;
        return implode(' ', array_filter(array($firstname, $surname)));
    }

    public function getTitle()
    {
        return $this->Reference . ' - ' . $this->dbObject('Placed')->Nice();
    }

    /**
     * Get shipping address, or member default shipping address.
     */
    public function getShippingAddress()
    {
        return $this->getAddress('Shipping');
    }

    /**
     * Get billing address, if marked to use seperate address, otherwise use shipping address,
     * or the member default billing address.
     */
    public function getBillingAddress()
    {
        if (!$this->SeparateBillingAddress && $this->ShippingAddressID === $this->BillingAddressID) {
            return $this->getShippingAddress();
        } else {
            return $this->getAddress('Billing');
        }
    }

    /**
     * @param string $type - Billing or Shipping
     * @return Address
     * @throws \Exception
     */
    protected function getAddress($type)
    {
        $address = $this->getComponent($type . 'Address');

        if (!$address || !$address->exists() && $this->Member()) {
            $address = $this->Member()->{"Default${type}Address"}();
        }

        if (empty($address->Surname) && empty($address->FirstName)) {
            if ($member = $this->Member()) {
                // If there's a member object, use information from the Member.
                // The information from Order should have precendence if set though!
                $address->FirstName = $this->FirstName ?: $member->FirstName;
                $address->Surname = $this->Surname ?: $member->Surname;
            } else {
                $address->FirstName = $this->FirstName;
                $address->Surname = $this->Surname;
            }
        }

        return $address;
    }

    /**
     * Check if the two addresses saved differ.
     *
     * @return boolean
     */
    public function getAddressesDiffer()
    {
        return $this->SeparateBillingAddress || $this->ShippingAddressID !== $this->BillingAddressID;
    }

    /**
     * Has this order been sent to the customer?
     * (at "Sent" status).
     *
     * @return boolean
     */
    public function IsSent()
    {
        return $this->Status == 'Sent';
    }

    /**
     * Is this order currently being processed?
     * (at "Sent" OR "Processing" status).
     *
     * @return boolean
     */
    public function IsProcessing()
    {
        return $this->IsSent() || $this->Status == 'Processing';
    }

    /**
     * Return whether this Order has been paid for (Status == Paid)
     * or Status == Processing, where it's been paid for, but is
     * currently in a processing state.
     *
     * @return boolean
     */
    public function IsPaid()
    {
        return (boolean)$this->Paid || $this->Status == 'Paid';
    }

    public function IsCart()
    {
        return $this->Status == 'Cart';
    }

    /**
     * Create a unique reference identifier string for this order.
     */
    public function generateReference()
    {
        $reference = str_pad($this->ID, self::$reference_id_padding, '0', STR_PAD_LEFT);

        $this->extend('generateReference', $reference);

        $candidate = $reference;
        //prevent generating references that are the same
        $count = 0;
        while (Order::get()->filter('Reference', $candidate)->count() > 0) {
            $count++;
            $candidate = $reference . '' . $count;
        }
        $this->Reference = $candidate;
    }

    /**
     * Get the reference for this order, or fall back to order ID.
     */
    public function getReference()
    {
        return $this->getField('Reference') ? $this->getField('Reference') : $this->ID;
    }

    /**
     * Force creating an order reference
     */
    protected function onBeforeWrite()
    {
        parent::onBeforeWrite();
        if (!$this->getField('Reference') && in_array($this->Status, self::$placed_status)) {
            $this->generateReference();
        }

        // perform status transition
        if ($this->isInDB() && $this->isChanged('Status')) {
            $this->statusTransition(
                empty($this->original['Status']) ? 'Cart' : $this->original['Status'],
                $this->Status
            );
        }

        // While the order is unfinished/cart, always store the current locale with the order.
        // We do this everytime an order is saved, because the user might change locale (language-switch).
        if ($this->Status == 'Cart') {
            $this->Locale = ShopTools::get_current_locale();
        }
    }

    /**
     * Called from @see onBeforeWrite whenever status changes
     *
     * @param string $fromStatus status to transition away from
     * @param string $toStatus   target status
     */
    protected function statusTransition($fromStatus, $toStatus)
    {
        // Add extension hook to react to order status transitions.
        $this->extend('onStatusChange', $fromStatus, $toStatus);

        if ($toStatus == 'Paid' && !$this->Paid) {
            $this->Paid = DBDatetime::now()->Rfc2822();
            foreach ($this->Items() as $item) {
                $item->onPayment();
            }
            //all payment is settled
            $this->extend('onPaid');

            if (!$this->ReceiptSent) {
                OrderEmailNotifier::create($this)->sendReceipt();
                $this->ReceiptSent = DBDatetime::now()->Rfc2822();
            }
        }

        $logStatus = $this->config()->log_status;
        if (!empty($logStatus) && in_array($toStatus, $logStatus)) {
            $this->flagOrderStatusWrite = $fromStatus != $toStatus;
        }
    }

    /**
     * delete attributes, statuslogs, and payments
     */
    protected function onBeforeDelete()
    {
        foreach ($this->Items() as $item) {
            $item->delete();
        }

        foreach ($this->Modifiers() as $modifier) {
            $modifier->delete();
        }

        foreach ($this->OrderStatusLogs() as $logEntry) {
            $logEntry->delete();
        }

        // just remove the payment relations…
        // that way payment objects still persist (they might be relevant for book-keeping?)
        $this->Payments()->removeAll();

        parent::onBeforeDelete();
    }

    public function onAfterWrite()
    {
        parent::onAfterWrite();

        //create an OrderStatusLog
        if ($this->flagOrderStatusWrite) {
            $this->flagOrderStatusWrite = false;
            $log = OrderStatusLog::create();

            // populate OrderStatusLog
            $log->Title = _t(
                'SilverShop\ShopEmail.StatusChanged',
                'Status for order #{OrderNo} changed to "{OrderStatus}"',
                '',
                ['OrderNo' => $this->Reference, 'OrderStatus' => $this->getStatusI18N()]
            );
            $log->Note = _t('SilverShop\ShopEmail.StatusChange' . $this->Status . 'Note', $this->Status . 'Note');
            $log->OrderID = $this->ID;
            OrderEmailNotifier::create($this)->sendStatusChange($log->Title, $log->Note);
            $log->SentToCustomer = true;
            $this->extend('updateOrderStatusLog', $log);
            $log->write();
        }
    }

    public function debug()
    {
        if (Director::is_cli()) {
            // Temporarily disabled.
            // TODO: Reactivate when the following issue got fixed: https://github.com/silverstripe/silverstripe-framework/issues/7827
            return '';
        }

        $val = "

" . static::class . "
\n
\n";
        if ($this->record) {
            foreach ($this->record as $fieldName => $fieldVal) {
                $val .= "\t
$fieldName: " . Debug::text($fieldVal) . "
\n";
            }
        }
        $val .= "
\n";
        $val .= "

Items
";
        if ($items = $this->Items()) {
            $val .= $this->Items()->debug();
        }
        $val .= "

Modifiers
";
        if ($modifiers = $this->Modifiers()) {
            $val .= $modifiers->debug();
        }
        $val .= "

";

        return $val;
    }

    /**
     * Provide i18n entities for the order class
     *
     * @return array
     */
    public function provideI18nEntities()
    {
        $entities = parent::provideI18nEntities();

        // collect all the payment status values
        foreach ($this->dbObject('Status')->enumValues() as $value) {
            $key = strtoupper($value);
            $entities[__CLASS__ . ".STATUS_$key"] = array(
                $value,
                "Translation of the order status '$value'",
            );
        }

        return $entities;
    }
}


<?php

namespace SilverShop\Forms;

use Closure;
use SilverShop\Model\Order;
use SilverShop\Model\Variation\Variation;
use SilverStripe\Forms\CheckboxField;
use SilverStripe\Forms\DropdownField;
use SilverStripe\Forms\FormField;
use SilverStripe\Forms\TextField;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\SS_List;
use SilverStripe\View\SSViewer;

/**
 * Field for editing cart/items within a form
 *
 * @package shop
 */
class CartEditField extends FormField
{
    /**
     * @var Order
     */
    protected $cart;

    protected $items;

    protected $template = 'Cart';

    protected $editableItemsCallback;

    public function __construct($name, $title, $cart)
    {
        parent::__construct($name, $title);
        $this->cart = $cart;
        $this->items = $cart->Items();
    }

    /**
     * Set tempalte for rendering editable cart.
     *
     * @param string $template
     */
    public function setTemplate($template)
    {
        $this->template = $template;
        return $this;
    }

    /**
     * Allow overriding the given items list.
     * This helps with formatting, grouping, ordering etc.
     */
    public function setItemsList(SS_List $list)
    {
        $this->items = $list;
        return $this;
    }

    /**
     * Get the items list being used to produce the cart.
     *
     * @return SS_List
     */
    public function getItemsList()
    {
        return $this->items;
    }

    /**
     * Provides a way to modify the editableItems list
     * before it is rendered.
     *
     * @param Closure $callback
     */
    public function setEditableItemsCallback(Closure $callback)
    {
        $this->editableItemsCallback = $callback;
    }

    /**
     * Render the cart with editable item fields.
     *
     * @param array $properties
     */
    public function Field($properties = array())
    {
        $editables = $this->editableItems();
        $customcartdata = array(
            'Items' => $editables,
        );
        // NOTE: this was originally incorrect - passing just $editables and $customcartdata
        // which broke modules like Display_Logic.
        $this->extend('onBeforeRender', $this, $editables, $customcartdata);

        return SSViewer::execute_template(
            $this->template,
            $this->cart->customise($customcartdata),
            array('Editable' => true)
        );
    }

    /**
     * Add quantity, variation and remove fields to the
     * item set.
     *
     * @param SS_List $items
     */
    protected function editableItems()
    {
        $editables = ArrayList::create();
        foreach ($this->items as $item) {
            $buyable = $item->Buyable();
            if (!$buyable) {
                continue;
            }
            // If the buyable is a variation, use the belonging product instead for variation-form generation
            if ($buyable instanceof Variation) {
                $buyable = $buyable->Product();
            }
            $name = $this->name . "[$item->ID]";
            $quantity = TextField::create(
                $name . '[Quantity]',
                'Quantity',
                $item->Quantity
            )
                ->addExtraClass('numeric')
                ->setAttribute('type', 'number')
                ->setAttribute('min', '0');

            $variationfield = false;
            if ($buyable->hasMany('Variations')) {
                $variations = $buyable->Variations();
                if ($variations->exists()) {
                    $variationfield = DropdownField::create(
                        $name . '[ProductVariationID]',
                        _t('SilverShop\Model\Variation\Variation.SINGULARNAME', 'Variation'),
                        $variations->map('ID', 'Title'),
                        $item->ProductVariationID
                    );
                }
            }
            $remove = CheckboxField::create($name . '[Remove]', _t('SilverShop\Generic.Remove', 'Remove'));
            $editables->push(
                $item->customise(
                    array(
                        'QuantityField' => $quantity,
                        'VariationField' => $variationfield,
                        'RemoveField' => $remove,
                    )
                )
            );
        }

        if (is_callable($this->editableItemsCallback)) {
            $callback = $this->editableItemsCallback;
            $editables = $callback($editables);
        }

        return $editables;
    }
}


<?php

namespace SilverShop\Page;

use Page;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB;

/**
 * Account page shows order history and a form to allow
 * the member to edit his/her details.
 *
 * @package shop
 */
class AccountPage extends Page
{
    private static $icon = 'silvershop/core: client/dist/images/icons/account.gif';

    private static $table_name = 'SilverShop_AccountPage';

    public function canCreate($member = null, $context = array())
    {
        return !self::get()->exists();
    }

    /**
     * Returns the link or the URLSegment to the account page on this site
     *
     * @param boolean $urlSegment Return the URLSegment only
     *
     * @return mixed
     */
    public static function find_link($urlSegment = false)
    {
        $page = self::get_if_account_page_exists();
        return ($urlSegment) ? $page->URLSegment : $page->Link();
    }

    /**
     * Return a link to view the order on the account page.
     *
     * @param int|string $orderID    ID of the order
     * @param boolean    $urlSegment Return the URLSegment only
     *
     * @return string
     */
    public static function get_order_link($orderID, $urlSegment = false)
    {
        $page = self::get_if_account_page_exists();
        return ($urlSegment ? $page->URLSegment . '/' : $page->Link()) . 'order/' . $orderID;
    }

    /**
     * @return AccountPage
     */
    protected static function get_if_account_page_exists()
    {
        if ($page = DataObject::get_one(self::class)) {
            return $page;
        }
        user_error(_t(__CLASS__ . '.NoPage', 'No AccountPage was found. Please create one in the CMS!'), E_USER_ERROR);
        return null; // just to keep static analysis happy
    }

    /**
     * This module always requires a page model.
     */
    public function requireDefaultRecords()
    {
        parent::requireDefaultRecords();
        if (!self::get()->exists() && $this->config()->create_default_pages) {
            /**
             * @var AccountPage $page
             */
            $page = self::create()->update(
                [
                'Title' => 'Account',
                'URLSegment' => AccountPageController::config()->url_segment,
                'ShowInMenus' => 0,
                ]
            );
            $page->write();
            $page->publishSingle();
            $page->flushCache();
            DB::alteration_message('Account page created', 'created');
        }
    }
}


<?php

use Composer\Composer;
use Composer\Config;
use Composer\Config\JsonConfigSource;
use Composer\DependencyResolver\Pool;
use Composer\EventDispatcher\Event;
use Composer\Factory;
use Composer\IO\NullIO;
use Composer\Installer;
use Composer\Json\JsonFile;
use Composer\Package;
use Composer\Package\BasePackage;
use Composer\Package\PackageInterface;
use Composer\Package\Version\VersionParser;
use Composer\Package\Version\VersionSelector;
use Composer\Repository;
use Composer\Repository\CompositeRepository;
use Composer\Repository\ComposerRepository;
use Composer\Repository\RepositoryManager;
use Composer\Util\Filesystem;
use WP_CLI\ComposerIO;
use WP_CLI\Extractor;
use WP_CLI\Utils;
use WP_CLI\JsonManipulator;
use WP_CLI\PackageManagerEventSubscriber;

/**
 * Lists, installs, and removes WP-CLI packages.
 *
 * WP-CLI packages are community-maintained projects built on WP-CLI. They can
 * contain WP-CLI commands, but they can also just extend WP-CLI in some way.
 *
 * Installable packages are listed in the
 * [Package Index](http://wp-cli.org/package-index/).
 *
 * Learn how to create your own command from the
 * [Commands Cookbook](http://wp-cli.org/docs/commands-cookbook/)
 *
 * ## EXAMPLES
 *
 *     # List installed packages
 *     $ wp package list
 *     +-----------------------+------------------------------------------+---------+------------+
 *     | name                  | description                              | authors | version    |
 *     +-----------------------+------------------------------------------+---------+------------+
 *     | wp-cli/server-command | Start a development server for WordPress |         | dev-master |
 *     +-----------------------+------------------------------------------+---------+------------+
 *
 *     # Install the latest development version of the package
 *     $ wp package install wp-cli/server-command
 *     Installing package wp-cli/server-command (dev-master)
 *     Updating /home/person/.wp-cli/packages/composer.json to require the package...
 *     Using Composer to install the package...
 *     ---
 *     Loading composer repositories with package information
 *     Updating dependencies
 *     Resolving dependencies through SAT
 *     Dependency resolution completed in 0.005 seconds
 *     Analyzed 732 packages to resolve dependencies
 *     Analyzed 1034 rules to resolve dependencies
 *      - Installing package
 *     Writing lock file
 *     Generating autoload files
 *     ---
 *     Success: Package installed.
 *
 *     # Uninstall package
 *     $ wp package uninstall wp-cli/server-command
 *     Removing require statement from /home/person/.wp-cli/packages/composer.json
 *     Deleting package directory /home/person/.wp-cli/packages/vendor/wp-cli/server-command
 *     Regenerating Composer autoload.
 *     Success: Uninstalled package.
 *
 * @package WP-CLI
 *
 * @when before_wp_load
 */
class Package_Command extends WP_CLI_Command {

	const PACKAGE_INDEX_URL = 'https://wp-cli.org/package-index/';
	const SSL_CERTIFICATE   = '/rmccue/requests/library/Requests/Transport/cacert.pem';

	private $pool = false;

	/**
	 * Default author data used while creating default WP-CLI packages composer.json.
	 *
	 * @var array
	 */
	private $author_data = [
		'name'  => 'WP-CLI',
		'email' => 'noreply@wpcli.org',
	];

	/**
	 * Default repository data used while creating default WP-CLI packages composer.json.
	 * @var array
	 */
	private $composer_type_package = [
		'type' => 'composer',
		'url'  => self::PACKAGE_INDEX_URL,
	];

	/**
	 * Browses WP-CLI packages available for installation.
	 *
	 * Lists packages available for installation from the [Package Index](http://wp-cli.org/package-index/).
	 * Although the package index will remain in place for backward compatibility reasons, it has been
	 * deprecated and will not be updated further. Please refer to https://github.com/wp-cli/ideas/issues/51
	 * to read about its potential replacement.
	 *
	 * ## OPTIONS
	 *
	 * [--fields=<fields>]
	 * : Limit the output to specific fields. Defaults to all fields.
	 *
	 * [--format=<format>]
	 * : Render output in a particular format.
	 * ---
	 * default: table
	 * options:
	 *   - table
	 *   - csv
	 *   - ids
	 *   - json
	 *   - yaml
	 * ---
	 *
	 * ## AVAILABLE FIELDS
	 *
	 * These fields will be displayed by default for each package:
	 *
	 * * name
	 * * description
	 * * authors
	 * * version
	 *
	 * There are no optionally available fields.
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp package browse --format=yaml
	 *     ---
	 *     10up/mu-migration:
	 *       name: 10up/mu-migration
	 *       description: A set of WP-CLI commands to support the migration of single WordPress instances to multisite
	 *       authors: Nícholas André
	 *       version: dev-master, dev-develop
	 *     aaemnnosttv/wp-cli-dotenv-command:
	 *       name: aaemnnosttv/wp-cli-dotenv-command
	 *       description: Dotenv commands for WP-CLI
	 *       authors: Evan Mattson
	 *       version: v0.1, v0.1-beta.1, v0.2, dev-master, dev-dev, dev-develop, dev-tests/behat
	 *     aaemnnosttv/wp-cli-http-command:
	 *       name: aaemnnosttv/wp-cli-http-command
	 *       description: WP-CLI command for using the WordPress HTTP API
	 *       authors: Evan Mattson
	 *       version: dev-master
	 */
	public function browse( $_, $assoc_args ) {
		$this->set_composer_auth_env_var();
		if ( empty( $assoc_args['format'] ) || 'table' === $assoc_args['format'] ) {
			WP_CLI::line( WP_CLI::colorize( '%CAlthough the package index will remain in place for backward compatibility reasons, it has been deprecated and will not be updated further. Please refer to https://github.com/wp-cli/ideas/issues/51 to read about its potential replacement.%n' ) );
		}
		$this->show_packages( 'browse', $this->get_community_packages(), $assoc_args );
	}

	/**
	 * Installs a WP-CLI package.
	 *
	 * Packages are required to be a valid Composer package, and can be
	 * specified as:
	 *
	 * * Package name from WP-CLI's package index.
	 * * Git URL accessible by the current shell user.
	 * * Path to a directory on the local machine.
	 * * Local or remote .zip file.
	 *
	 * Packages are installed to `~/.wp-cli/packages/` by default. Use the
	 * `WP_CLI_PACKAGES_DIR` environment variable to provide a custom path.
	 *
	 * When installing a local directory, WP-CLI simply registers a
	 * reference to the directory. If you move or delete the directory, WP-CLI's
	 * reference breaks.
	 *
	 * When installing a .zip file, WP-CLI extracts the package to
	 * `~/.wp-cli/packages/local/<package-name>`.
	 *
	 * ## OPTIONS
	 *
	 * <name|git|path|zip>
	 * : Name, git URL, directory path, or .zip file for the package to install.
	 * Names can optionally include a version constraint
	 * (e.g. wp-cli/server-command:@stable).
	 *
	 * ## EXAMPLES
	 *
	 *     # Install the latest development version from the package index.
	 *     $ wp package install wp-cli/server-command
	 *     Installing package wp-cli/server-command (dev-master)
	 *     Updating /home/person/.wp-cli/packages/composer.json to require the package...
	 *     Using Composer to install the package...
	 *     ---
	 *     Loading composer repositories with package information
	 *     Updating dependencies
	 *     Resolving dependencies through SAT
	 *     Dependency resolution completed in 0.005 seconds
	 *     Analyzed 732 packages to resolve dependencies
	 *     Analyzed 1034 rules to resolve dependencies
	 *      - Installing package
	 *     Writing lock file
	 *     Generating autoload files
	 *     ---
	 *     Success: Package installed.
	 *
	 *     # Install the latest stable version.
	 *     $ wp package install wp-cli/server-command:@stable
	 *
	 *     # Install a package hosted at a git URL.
	 *     $ wp package install git@github.com:runcommand/hook.git
	 *
	 *     # Install a package in a .zip file.
	 *     $ wp package install google-sitemap-generator-cli.zip
	 */
	public function install( $args, $assoc_args ) {
		list( $package_name ) = $args;

		$this->set_composer_auth_env_var();
		$git_package = false;
		$dir_package = false;
		$version     = 'dev-master';
		if ( $this->is_git_repository( $package_name ) ) {
			$git_package = $package_name;
			preg_match( '#([^:\/]+\/[^\/]+)\.git#', $package_name, $matches );
			if ( ! empty( $matches[1] ) ) {
				$package_name = $this->check_git_package_name( $matches[1] );
			} else {
				WP_CLI::error( "Couldn't parse package name from expected path '<name>/<package>'." );
			}
		} elseif ( ( false !== strpos( $package_name, '://' ) && false !== stripos( $package_name, '.zip' ) )
			|| ( pathinfo( $package_name, PATHINFO_EXTENSION ) === 'zip' && is_file( $package_name ) ) ) {
			// Download the remote ZIP file to a temp directory
			$temp = false;
			if ( false !== strpos( $package_name, '://' ) ) {
				$temp     = Utils\get_temp_dir() . uniqid( 'wp-cli-package_', true /*more_entropy*/ ) . '.zip';
				$options  = [
					'timeout'  => 600,
					'filename' => $temp,
				];
				$response = Utils\http_request( 'GET', $package_name, null, [], $options );
				if ( 20 !== (int) substr( $response->status_code, 0, 2 ) ) {
					@unlink( $temp ); // @codingStandardsIgnoreLine
					WP_CLI::error( sprintf( "Couldn't download package from '%s' (HTTP code %d).", $package_name, $response->status_code ) );
				}
				$package_name = $temp;
			}
			$dir_package = Utils\get_temp_dir() . uniqid( 'wp-cli-package_', true /*more_entropy*/ );
			try {
				// Extract the package to get the package name
				Extractor::extract( $package_name, $dir_package );
				if ( $temp ) {
					unlink( $temp );
					$temp = false;
				}
				list( $package_name, $version ) = self::get_package_name_and_version_from_dir_package( $dir_package );
				// Move to a location based on the package name
				$local_dir          = rtrim( WP_CLI::get_runner()->get_packages_dir_path(), '/' ) . '/local/';
				$actual_dir_package = $local_dir . str_replace( '/', '-', $package_name );
				Extractor::copy_overwrite_files( $dir_package, $actual_dir_package );
				Extractor::rmdir( $dir_package );
				// Behold, the extracted package
				$dir_package = $actual_dir_package;
			} catch ( Exception $e ) {
				if ( $temp ) {
					unlink( $temp );
				}
				if ( file_exists( $dir_package ) ) {
					try {
						Extractor::rmdir( $dir_package );
					} catch ( Exception $rmdir_e ) {
						WP_CLI::warning( $rmdir_e->getMessage() );
					}
				}
				WP_CLI::error( $e->getMessage() );
			}
		} elseif ( is_dir( $package_name ) && file_exists( $package_name . '/composer.json' ) ) {
			$dir_package = $package_name;
			if ( ! Utils\is_path_absolute( $dir_package ) ) {
				$dir_package = getcwd() . DIRECTORY_SEPARATOR . $dir_package;
			}
			list( $package_name, $version ) = self::get_package_name_and_version_from_dir_package( $dir_package );
		} else {
			if ( false !== strpos( $package_name, ':' ) ) {
				list( $package_name, $version ) = explode( ':', $package_name );
			}
			$package = $this->get_package_by_shortened_identifier( $package_name );
			if ( ! $package ) {
				WP_CLI::error( sprintf( "Invalid package: shortened identifier '%s' not found.", $package_name ) );
			}
			if ( is_string( $package ) ) {
				if ( $this->is_git_repository( $package ) ) {
					$git_package = $package;
					if ( '@stable' === $version ) {
						$tag     = $this->get_github_latest_release_tag( $package_name );
						$version = $this->guess_version_constraint_from_tag( $tag );
					}
					$package_name = $this->check_git_package_name( $package_name, $version );
				}
			} elseif ( $package_name !== $package->getPrettyName() ) {
				// BC support for specifying lowercase names for mixed-case package index packages - don't bother warning.
				$package_name = $package->getPrettyName();
			}
		}

		WP_CLI::log( sprintf( 'Installing package %s (%s)', $package_name, $version ) );

		// Read the WP-CLI packages composer.json and do some initial error checking.
		list( $json_path, $composer_backup, $composer_backup_decoded ) = $this->get_composer_json_path_backup_decoded();

		// Revert on shutdown if `$revert` is true (set to false on success).
		$revert = true;
		$this->register_revert_shutdown_function( $json_path, $composer_backup, $revert );

		// Add the 'require' to composer.json
		WP_CLI::log( sprintf( 'Updating %s to require the package...', $json_path ) );
		$json_manipulator = new JsonManipulator( $composer_backup );
		$json_manipulator->addMainKey( 'name', 'wp-cli/wp-cli' );
		$json_manipulator->addMainKey( 'version', self::get_wp_cli_version_composer() );
		$json_manipulator->addLink( 'require', $package_name, $version, false /*sortPackages*/, true /*caseInsensitive*/ );
		$json_manipulator->addConfigSetting( 'secure-http', true );

		$package_args = [];
		if ( $git_package ) {
			WP_CLI::log( sprintf( 'Registering %s as a VCS repository...', $git_package ) );
			$package_args = [
				'type' => 'vcs',
				'url'  => $git_package,
			];
			$json_manipulator->addSubNode(
				'repositories',
				$package_name,
				$package_args,
				true /*caseInsensitive*/
			);
		} elseif ( $dir_package ) {
			WP_CLI::log( sprintf( 'Registering %s as a path repository...', $dir_package ) );
			$package_args = [
				'type' => 'path',
				'url'  => $dir_package,
			];
			$json_manipulator->addSubNode(
				'repositories',
				$package_name,
				$package_args,
				true /*caseInsensitive*/
			);
		}
		// If the composer file does not contain the current package index repository, refresh the repository definition.
		if ( empty( $composer_backup_decoded['repositories']['wp-cli']['url'] ) || self::PACKAGE_INDEX_URL !== $composer_backup_decoded['repositories']['wp-cli']['url'] ) {
			WP_CLI::log( 'Updating package index repository url...' );
			$package_args = $this->composer_type_package;
			$json_manipulator->addRepository(
				'wp-cli',
				$package_args
			);
		}

		file_put_contents( $json_path, $json_manipulator->getContents() );
		$composer = $this->get_composer();

		// Set up the EventSubscriber
		$event_subscriber = new PackageManagerEventSubscriber();
		$composer->getEventDispatcher()->addSubscriber( $event_subscriber );
		// Set up the installer
		$install = Installer::create( new ComposerIO(), $composer );
		$install->setUpdate( true ); // Installer class will only override composer.lock with this flag
		$install->setPreferSource( true ); // Use VCS when VCS for easier contributions.

		// Try running the installer, but revert composer.json if failed
		WP_CLI::log( 'Using Composer to install the package...' );
		WP_CLI::log( '---' );
		$res = false;
		try {
			$res = $install->run();
		} catch ( Exception $e ) {
			WP_CLI::warning( $e->getMessage() );
		}
		WP_CLI::log( '---' );

		if ( 0 === $res ) {
			$revert = false;
			WP_CLI::success( 'Package installed.' );
		} else {
			$res_msg = $res ? " (Composer return code {$res})" : ''; // $res may be null apparently.
			WP_CLI::debug( "composer.json content:\n" . file_get_contents( $json_path ), 'packages' );
			WP_CLI::error( "Package installation failed{$res_msg}." );
		}
	}

	/**
	 * Lists installed WP-CLI packages.
	 *
	 * ## OPTIONS
	 *
	 * [--fields=<fields>]
	 * : Limit the output to specific fields. Defaults to all fields.
	 *
	 * [--format=<format>]
	 * : Render output in a particular format.
	 * ---
	 * default: table
	 * options:
	 *   - table
	 *   - csv
	 *   - ids
	 *   - json
	 *   - yaml
	 * ---
	 *
	 * ## AVAILABLE FIELDS
	 *
	 * These fields will be displayed by default for each package:
	 *
	 * * name
	 * * authors
	 * * version
	 * * update
	 * * update_version
	 *
	 * These fields are optionally available:
	 *
	 * * description
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp package list
	 *     +-----------------------+------------------------------------------+---------+------------+
	 *     | name                  | description                              | authors | version    |
	 *     +-----------------------+------------------------------------------+---------+------------+
	 *     | wp-cli/server-command | Start a development server for WordPress |         | dev-master |
	 *     +-----------------------+------------------------------------------+---------+------------+
	 *
	 * @subcommand list
	 */
	public function list_( $args, $assoc_args ) {
		$this->set_composer_auth_env_var();
		$this->show_packages( 'list', $this->get_installed_packages(), $assoc_args );
	}

	/**
	 * Gets the path to an installed WP-CLI package, or the package directory.
	 *
	 * If you want to contribute to a package, this is a great way to jump to it.
	 *
	 * ## OPTIONS
	 *
	 * [<name>]
	 * : Name of the package to get the directory for.
	 *
	 * ## EXAMPLES
	 *
	 *     # Get package path
	 *     $ wp package path
	 *     /home/person/.wp-cli/packages/
	 *
	 *     # Change directory to package path
	 *     $ cd $(wp package path) && pwd
	 *     /home/vagrant/.wp-cli/packages
	 */
	public function path( $args ) {
		$packages_dir = WP_CLI::get_runner()->get_packages_dir_path();
		if ( ! empty( $args ) ) {
			$packages_dir .= 'vendor/' . $args[0];
			if ( ! is_dir( $packages_dir ) ) {
				WP_CLI::error( 'Invalid package name.' );
			}
		}
		WP_CLI::line( $packages_dir );
	}

	/**
	 * Updates all installed WP-CLI packages to their latest version.
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp package update
	 *     Using Composer to update packages...
	 *     ---
	 *     Loading composer repositories with package information
	 *     Updating dependencies
	 *     Resolving dependencies through SAT
	 *     Dependency resolution completed in 0.074 seconds
	 *     Analyzed 1062 packages to resolve dependencies
	 *     Analyzed 22383 rules to resolve dependencies
	 *     Writing lock file
	 *     Generating autoload files
	 *     ---
	 *     Success: Packages updated.
	 */
	public function update() {
		$this->set_composer_auth_env_var();
		$composer = $this->get_composer();

		// Set up the EventSubscriber
		$event_subscriber = new PackageManagerEventSubscriber();
		$composer->getEventDispatcher()->addSubscriber( $event_subscriber );

		// Set up the installer
		$install = Installer::create( new ComposerIO(), $composer );
		$install->setUpdate( true ); // Installer class will only override composer.lock with this flag
		$install->setPreferSource( true ); // Use VCS when VCS for easier contributions.
		WP_CLI::log( 'Using Composer to update packages...' );
		WP_CLI::log( '---' );
		$res = false;
		try {
			$res = $install->run();
		} catch ( Exception $e ) {
			WP_CLI::warning( $e->getMessage() );
		}
		WP_CLI::log( '---' );

		if ( 0 === $res ) {
			WP_CLI::success( 'Packages updated.' );
		} else {
			$res_msg = $res ? " (Composer return code {$res})" : ''; // $res may be null apparently.
			WP_CLI::error( "Failed to update packages{$res_msg}." );
		}
	}

	/**
	 * Uninstalls a WP-CLI package.
	 *
	 * ## OPTIONS
	 *
	 * <name>
	 * : Name of the package to uninstall.
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp package uninstall wp-cli/server-command
	 *     Removing require statement from /home/person/.wp-cli/packages/composer.json
	 *     Deleting package directory /home/person/.wp-cli/packages/vendor/wp-cli/server-command
	 *     Regenerating Composer autoload.
	 *     Success: Uninstalled package.
	 */
	public function uninstall( $args ) {
		list( $package_name ) = $args;

		$this->set_composer_auth_env_var();
		$package = $this->get_installed_package_by_name( $package_name );
		if ( false === $package ) {
			WP_CLI::error( 'Package not installed.' );
		}
		$package_name = $package->getPrettyName(); // Make sure package name is what's in composer.json.

		// Read the WP-CLI packages composer.json and do some initial error checking.
		list( $json_path, $composer_backup, $composer_backup_decoded ) = $this->get_composer_json_path_backup_decoded();

		// Revert on shutdown if `$revert` is true (set to false on success).
		$revert = true;
		$this->register_revert_shutdown_function( $json_path, $composer_backup, $revert );

		// Remove the 'require' from composer.json.
		WP_CLI::log( sprintf( 'Removing require statement from %s', $json_path ) );
		$manipulator = new JsonManipulator( $composer_backup );
		$manipulator->removeSubNode( 'require', $package_name, true /*caseInsensitive*/ );

		// Remove the 'repository' details from composer.json.
		WP_CLI::log( sprintf( 'Removing repository details from %s', $json_path ) );
		$manipulator->removeSubNode( 'repositories', $package_name, true /*caseInsensitive*/ );

		file_put_contents( $json_path, $manipulator->getContents() );
		$composer = $this->get_composer();

		// Set up the installer.
		$install = Installer::create( new NullIO(), $composer );
		$install->setUpdate( true ); // Installer class will only override composer.lock with this flag
		$install->setPreferSource( true ); // Use VCS when VCS for easier contributions.

		WP_CLI::log( 'Removing package directories and regenerating autoloader...' );
		$res = false;
		try {
			$res = $install->run();
		} catch ( Exception $e ) {
			WP_CLI::warning( $e->getMessage() );
		}

		if ( 0 === $res ) {
			$revert = false;
			WP_CLI::success( 'Uninstalled package.' );
		} else {
			$res_msg = $res ? " (Composer return code {$res})" : ''; // $res may be null apparently.
			WP_CLI::error( "Package removal failed{$res_msg}." );
		}
	}

	/**
	 * Checks whether a package is a WP-CLI community package based
	 * on membership in our package index.
	 *
	 * @param object      $package     A package object
	 * @return bool
	 */
	private function is_community_package( $package ) {
		return $this->package_index()->hasPackage( $package );
	}

	/**
	 * Gets a Composer instance.
	 */
	private function get_composer() {
		$this->avoid_composer_ca_bundle();
		try {
			$composer_path = $this->get_composer_json_path();

			// Composer's auto-load generating code makes some assumptions about where
			// the 'vendor-dir' is, and where Composer is running from.
			// Best to just pretend we're installing a package from ~/.wp-cli or similar
			chdir( pathinfo( $composer_path, PATHINFO_DIRNAME ) );

			// Prevent DateTime error/warning when no timezone set.
			// Note: The package is loaded before WordPress load, For environments that don't have set time in php.ini.
			// phpcs:ignore WordPress.WP.TimezoneChange.timezone_change_date_default_timezone_set,WordPress.PHP.NoSilencedErrors.Discouraged
			date_default_timezone_set( @date_default_timezone_get() );

			$composer = Factory::create( new NullIO(), $composer_path );
		} catch ( Exception $e ) {
			WP_CLI::error( sprintf( 'Failed to get composer instance: %s', $e->getMessage() ) );
		}
		return $composer;
	}

	/**
	 * Gets all of the community packages.
	 *
	 * @return array
	 */
	private function get_community_packages() {
		static $community_packages;

		if ( null === $community_packages ) {
			$this->avoid_composer_ca_bundle();
			try {
				$community_packages = $this->package_index()->getPackages();
			} catch ( Exception $e ) {
				WP_CLI::error( $e->getMessage() );
			}
		}

		return $community_packages;
	}

	/**
	 * Gets the package index instance
	 *
	 * We need to construct the instance manually, because there's no way to select
	 * a particular instance using $composer->getRepositoryManager()
	 *
	 * @return ComposerRepository
	 */
	private function package_index() {
		static $package_index;

		if ( ! $package_index ) {
			$config_args = [
				'config' => [
					'secure-http' => true,
					'home'        => dirname( $this->get_composer_json_path() ),
				],
			];
			$config      = new Config();
			$config->merge( $config_args );
			$config->setConfigSource( new JsonConfigSource( $this->get_composer_json() ) );

			try {
				$package_index = new ComposerRepository( [ 'url' => self::PACKAGE_INDEX_URL ], new NullIO(), $config );
			} catch ( Exception $e ) {
				WP_CLI::error( $e->getMessage() );
			}
		}

		return $package_index;
	}

	/**
	 * Displays a set of packages
	 *
	 * @param string $context
	 * @param array
	 * @param array
	 */
	private function show_packages( $context, $packages, $assoc_args ) {
		if ( 'list' === $context ) {
			$default_fields = [
				'name',
				'authors',
				'version',
				'update',
				'update_version',
			];
		} elseif ( 'browse' === $context ) {
			$default_fields = [
				'name',
				'description',
				'authors',
				'version',
			];
		}
		$defaults   = [
			'fields' => implode( ',', $default_fields ),
			'format' => 'table',
		];
		$assoc_args = array_merge( $defaults, $assoc_args );

		$composer = $this->get_composer();
		$list     = [];
		foreach ( $packages as $package ) {
			$name = $package->getPrettyName();
			if ( isset( $list[ $name ] ) ) {
				$list[ $name ]['version'][] = $package->getPrettyVersion();
			} else {
				$package_output                = [];
				$package_output['name']        = $package->getPrettyName();
				$package_output['description'] = $package->getDescription();
				$package_output['authors']     = implode( ', ', array_column( (array) $package->getAuthors(), 'name' ) );
				$package_output['version']     = [ $package->getPrettyVersion() ];
				$update                        = 'none';
				$update_version                = '';
				if ( 'list' === $context ) {
					try {
						$latest = $this->find_latest_package( $package, $composer, null );
						if ( $latest && $latest->getFullPrettyVersion() !== $package->getFullPrettyVersion() ) {
							$update         = 'available';
							$update_version = $latest->getPrettyVersion();
						}
					} catch ( Exception $e ) {
						WP_CLI::warning( $e->getMessage() );
						$update         = 'error';
						$update_version = $update;
					}
				}
				$package_output['update']         = $update;
				$package_output['update_version'] = $update_version;
				$package_output['pretty_name']    = $package->getPrettyName(); // Deprecated but kept for BC with package-command 1.0.8.
				$list[ $package_output['name'] ]  = $package_output;
			}
		}

		$list = array_map(
			function( $package ) {
				$package['version'] = implode( ', ', $package['version'] );
				return $package;
			},
			$list
		);

		ksort( $list );
		if ( 'ids' === $assoc_args['format'] ) {
			$list = array_keys( $list );
		}
		Utils\format_items( $assoc_args['format'], $list, $assoc_args['fields'] );
	}

	/**
	 * Gets a package by its shortened identifier.
	 *
	 * A shortened identifier has the form `<vendor>/<package>`.
	 *
	 * This method first checks the deprecated package index, for BC reasons,
	 * and then falls back to the corresponding GitHub URL.
	 */
	private function get_package_by_shortened_identifier( $package_name ) {
		// Check the package index first, so we don't break existing behavior.
		$lc_package_name = strtolower( $package_name ); // For BC check.
		foreach ( $this->get_community_packages() as $package ) {
			if ( $package_name === $package->getPrettyName() ) {
				return $package;
			}
			// For BC allow getting by lowercase name.
			if ( $lc_package_name === $package->getName() ) {
				return $package;
			}
		}

		// Check if the package exists on Packagist.
		$url      = "https://repo.packagist.org/p/{$package_name}.json";
		$response = Utils\http_request( 'GET', $url );
		if ( 20 === (int) substr( $response->status_code, 0, 2 ) ) {
			return $package_name;
		}

		// Fall back to GitHub URL if we had no match yet.
		$url          = "https://github.com/{$package_name}.git";
		$github_token = getenv( 'GITHUB_TOKEN' ); // Use GITHUB_TOKEN if available to avoid authorization failures or rate-limiting.
		$headers      = $github_token ? [ 'Authorization' => 'token ' . $github_token ] : [];
		$response     = Utils\http_request( 'GET', $url, null /*data*/, $headers );
		if ( 20 === (int) substr( $response->status_code, 0, 2 ) ) {
			return $url;
		}

		return false;
	}

	/**
	 * Gets the installed community packages.
	 */
	private function get_installed_packages() {
		$composer = $this->get_composer();

		$repo                   = $composer->getRepositoryManager()->getLocalRepository();
		$existing               = json_decode( file_get_contents( $this->get_composer_json_path() ), true );
		$installed_package_keys = ! empty( $existing['require'] ) ? array_keys( $existing['require'] ) : [];
		if ( empty( $installed_package_keys ) ) {
			return [];
		}
		// For use by legacy incorrect name check.
		$lc_installed_package_keys = array_map( 'strtolower', $installed_package_keys );
		$installed_packages        = [];
		foreach ( $repo->getCanonicalPackages() as $package ) {
			$idx = array_search( $package->getName(), $lc_installed_package_keys, true );
			// Use pretty name as it's case sensitive and what's in composer.json (or at least should be).
			if ( in_array( $package->getPrettyName(), $installed_package_keys, true ) ) {
				$installed_packages[] = $package;
			} elseif ( false !== $idx ) { // Legacy incorrect name check.
				WP_CLI::warning( sprintf( "Found package '%s' misnamed '%s' in '%s'.", $package->getPrettyName(), $installed_package_keys[ $idx ], $this->get_composer_json_path() ) );
				$installed_packages[] = $package;
			}
		}
		return $installed_packages;
	}

	/**
	 * Gets an installed package by its name.
	 */
	private function get_installed_package_by_name( $package_name ) {
		foreach ( $this->get_installed_packages() as $package ) {
			if ( $package_name === $package->getPrettyName() ) {
				return $package;
			}
			// Also check non-pretty (lowercase) name in case of legacy incorrect name.
			if ( $package_name === $package->getName() ) {
				return $package;
			}
		}
		return false;
	}

	/**
	 * Checks if the package name provided is already installed.
	 */
	private function is_package_installed( $package_name ) {
		if ( $this->get_installed_package_by_name( $package_name ) ) {
			return true;
		} else {
			return false;
		}
	}

	/**
	 * Gets the name of the package from the composer.json in a directory path
	 *
	 * @param string $dir_package
	 * @return array Two-element array containing package name and version.
	 */
	private static function get_package_name_and_version_from_dir_package( $dir_package ) {
		$composer_file = $dir_package . '/composer.json';
		if ( ! file_exists( $composer_file ) ) {
			WP_CLI::error( sprintf( "Invalid package: composer.json file '%s' not found.", $composer_file ) );
		}
		$composer_data = json_decode( file_get_contents( $composer_file ), true );
		if ( null === $composer_data ) {
			WP_CLI::error( sprintf( "Invalid package: failed to parse composer.json file '%s' as json.", $composer_file ) );
		}
		if ( empty( $composer_data['name'] ) ) {
			WP_CLI::error( sprintf( "Invalid package: no name in composer.json file '%s'.", $composer_file ) );
		}
		$package_name = $composer_data['name'];
		$version      = 'dev-master';
		if ( ! empty( $composer_data['version'] ) ) {
			$version = $composer_data['version'];
		}
		return [ $package_name, $version ];
	}

	/**
	 * Gets the WP-CLI packages composer.json object.
	 */
	private function get_composer_json() {
		return new JsonFile( $this->get_composer_json_path() );
	}

	/**
	 * Gets the absolute path to the WP-CLI packages composer.json.
	 */
	private function get_composer_json_path() {
		static $composer_path;

		if ( null === $composer_path || getenv( 'WP_CLI_TEST_PACKAGE_GET_COMPOSER_JSON_PATH' ) ) {

			if ( getenv( 'WP_CLI_PACKAGES_DIR' ) ) {
				$composer_path = Utils\trailingslashit( getenv( 'WP_CLI_PACKAGES_DIR' ) ) . 'composer.json';
			} else {
				$composer_path = Utils\trailingslashit( Utils\get_home_dir() ) . '.wp-cli/packages/composer.json';
			}

			// `composer.json` and its directory might need to be created
			if ( ! file_exists( $composer_path ) ) {
				$composer_path = $this->create_default_composer_json( $composer_path );
			} else {
				$composer_path = realpath( $composer_path );
				if ( false === $composer_path ) {
					$error = error_get_last();
					WP_CLI::error( sprintf( "Composer path '%s' for packages/composer.json not found: %s", $composer_path, $error['message'] ) );
				}
			}
		}

		return $composer_path;
	}

	/**
	 * Gets the WP-CLI version for composer.json
	 */
	private static function get_wp_cli_version_composer() {
		preg_match( '#^[0-9\.]+(-(alpha|beta)[^-]{0,})?#', WP_CLI_VERSION, $matches );
		$version = isset( $matches[0] ) ? $matches[0] : '';
		return $version;
	}

	/**
	 * Creates a default WP-CLI packages composer.json.
	 *
	 * @param string $composer_path Where the composer.json should be created
	 * @return string Returns the absolute path of the newly created default WP-CLI packages composer.json.
	 */
	private function create_default_composer_json( $composer_path ) {

		$composer_dir = pathinfo( $composer_path, PATHINFO_DIRNAME );
		if ( ! is_dir( $composer_dir ) ) {
			if ( ! @mkdir( $composer_dir, 0777, true ) ) { // @codingStandardsIgnoreLine
				$error = error_get_last();
				WP_CLI::error( sprintf( "Composer directory '%s' for packages couldn't be created: %s", $composer_dir, $error['message'] ) );
			}
		}

		$composer_dir = realpath( $composer_dir );
		if ( false === $composer_dir ) {
			$error = error_get_last();
			WP_CLI::error( sprintf( "Composer directory '%s' for packages not found: %s", $composer_dir, $error['message'] ) );
		}

		$composer_path = Utils\trailingslashit( $composer_dir ) . Utils\basename( $composer_path );

		$json_file = new JsonFile( $composer_path );

		$repositories = (object) [
			'wp-cli' => (object) $this->composer_type_package,
		];

		$options = [
			'name'              => 'wp-cli/wp-cli',
			'description'       => 'Installed community packages used by WP-CLI',
			'version'           => self::get_wp_cli_version_composer(),
			'authors'           => [ (object) $this->author_data ],
			'homepage'          => self::PACKAGE_INDEX_URL,
			'require'           => new stdClass(),
			'require-dev'       => new stdClass(),
			'minimum-stability' => 'dev',
			'prefer-stable'     => true,
			'license'           => 'MIT',
			'repositories'      => $repositories,
		];

		try {
			$json_file->write( $options );
		} catch ( Exception $e ) {
			WP_CLI::error( $e->getMessage() );
		}

		return $composer_path;
	}

	/**
	 * Given a package, this finds the latest package matching it
	 *
	 * @param  PackageInterface $package
	 * @param  Composer         $composer
	 * @param  string           $phpVersion
	 * @param  bool             $minorOnly
	 *
	 * @return PackageInterface|null
	 */
	private function find_latest_package( PackageInterface $package, Composer $composer, $php_version, $minor_only = false ) {
		// find the latest version allowed in this pool
		$name             = $package->getPrettyName();
		$version_selector = new VersionSelector( $this->get_pool( $composer ) );
		$stability        = $composer->getPackage()->getMinimumStability();
		$flags            = $composer->getPackage()->getStabilityFlags();
		if ( isset( $flags[ $name ] ) ) {
			$stability = array_search( $flags[ $name ], BasePackage::$stabilities, true );
		}
		$best_stability = $stability;
		if ( $composer->getPackage()->getPreferStable() ) {
			$best_stability = $package->getStability();
		}
		$target_version = null;
		if ( 0 === strpos( $package->getVersion(), 'dev-' ) ) {
			$target_version = $package->getVersion();
		}
		if ( null === $target_version && $minor_only ) {
			$target_version = '^' . $package->getVersion();
		}
		return $version_selector->findBestCandidate( $name, $target_version, $php_version, $best_stability );
	}

	private function get_pool( Composer $composer ) {
		if ( ! $this->pool ) {
			$this->pool = new Pool( $composer->getPackage()->getMinimumStability(), $composer->getPackage()->getStabilityFlags() );
			$this->pool->addRepository( new CompositeRepository( $composer->getRepositoryManager()->getRepositories() ) );
		}
		return $this->pool;
	}

	/**
	 * Checks whether a given package is a git repository.
	 *
	 * @param string $package Package name to check.
	 *
	 * @return bool Whether the package is a git repository.
	 */
	private function is_git_repository( $package ) {
		return '.git' === strtolower( substr( $package, -4, 4 ) );
	}

	/**
	 * Checks that `$package_name` matches the name in the repo composer.json, and return corrected value if not.
	 *
	 * @string $package_name Package name to check.
	 * @string $version Optional. Package version. Default 'master'.
	 * @string Package name, possibly changed to match that in repo.
	 */
	private function check_git_package_name( $package_name, $version = '' ) {
		// Generate raw git URL of composer.json file.
		$raw_content_url = 'https://raw.githubusercontent.com/' . $package_name . '/' . $this->get_raw_git_version( $version ) . '/composer.json';
		$github_token    = getenv( 'GITHUB_TOKEN' ); // Use GITHUB_TOKEN if available to avoid authorization failures or rate-limiting.
		$headers         = $github_token ? [ 'Authorization' => 'token ' . $github_token ] : [];

		$response = Utils\http_request( 'GET', $raw_content_url, null /*data*/, $headers );
		if ( 20 !== (int) substr( $response->status_code, 0, 2 ) ) {
			// Could not get composer.json. Possibly private so warn and return best guess from input (always xxx/xxx).
			WP_CLI::warning( sprintf( "Couldn't download composer.json file from '%s' (HTTP code %d). Presuming package name is '%s'.", $raw_content_url, $response->status_code, $package_name ) );
			return $package_name;
		}

		// Convert composer.json JSON to Array.
		$composer_content_as_array = json_decode( $response->body, true );
		if ( null === $composer_content_as_array ) {
			WP_CLI::error( sprintf( "Failed to parse '%s' as json.", $raw_content_url ) );
		}
		if ( empty( $composer_content_as_array['name'] ) ) {
			WP_CLI::error( sprintf( "Invalid package: no name in composer.json file '%s'.", $raw_content_url ) );
		}

		// Package name in composer.json that is hosted on GitHub.
		$package_name_on_repo = $composer_content_as_array['name'];

		// If package name and repository name are not identical, then fix it.
		if ( $package_name !== $package_name_on_repo ) {
			WP_CLI::warning( sprintf( "Package name mismatch...Updating from git name '%s' to composer.json name '%s'.", $package_name, $package_name_on_repo ) );
			$package_name = $package_name_on_repo;
		}
		return $package_name;
	}

	/**
	 * Get the version to use for raw github request. Very basic.
	 *
	 * @string $version Package version.
	 * @string Version to use for github request.
	 */
	private function get_raw_git_version( $version ) {
		if ( '' === $version ) {
			return 'master';
		}

		// If Composer hash given then just use whatever's after it.
		$hash_pos = strpos( $version, '#' );
		if ( false !== $hash_pos ) {
			return substr( $version, $hash_pos + 1 );
		}

		// Strip any Composer 'dev-' prefix.
		if ( 0 === strncmp( $version, 'dev-', 4 ) ) {
			$version = substr( $version, 4 );
		}

		// Ignore/strip any relative suffixes.
		return str_replace( [ '^', '~' ], '', $version );
	}

	/**
	 * Gets the release tag for the latest stable release of a GitHub repository.
	 *
	 * @param string $package_name Name of the repository.
	 *
	 * @return string Release tag.
	 */
	private function get_github_latest_release_tag( $package_name ) {
		$url      = "https://api.github.com/repos/{$package_name}/releases/latest";
		$response = Utils\http_request( 'GET', $url );
		if ( 20 !== (int) substr( $response->status_code, 0, 2 ) ) {
			WP_CLI::warning( 'Could not guess stable version from GitHub repository, falling back to master branch' );
			return 'master';
		}

		$package_data = json_decode( $response->body );
		if ( JSON_ERROR_NONE !== json_last_error() ) {
			WP_CLI::warning( 'Could not guess stable version from GitHub repository, falling back to master branch' );
			return 'master';
		}

		$tag = $package_data->tag_name;
		WP_CLI::debug( "Extracted latest stable release tag: {$tag}", 'packages' );

		return $tag;
	}

	/**
	 * Guesses the version constraint from a release tag.
	 *
	 * @param string $tag Release tag to guess the version constraint from.
	 *
	 * @return string Version constraint.
	 */
	private function guess_version_constraint_from_tag( $tag ) {
		$matches = [];
		if ( 1 !== preg_match( '/(?:version|v)\s*((?:[0-9]+\.?)+)(?:-.*)/i', $tag, $matches ) ) {
			return $tag;
		}

		$constraint = "^{$matches[1]}";
		WP_CLI::debug( "Guessing version constraint to use: {$constraint}", 'packages' );

		return $constraint;
	}

	/**
	 * Sets `COMPOSER_AUTH` environment variable (which Composer merges into the config setup in `Composer\Factory::createConfig()`) depending on available environment variables.
	 * Avoids authorization failures when accessing various sites.
	 */
	private function set_composer_auth_env_var() {
		$changed       = false;
		$composer_auth = getenv( 'COMPOSER_AUTH' );
		if ( false !== $composer_auth ) {
			$composer_auth = json_decode( $composer_auth, true /*assoc*/ );
		}
		if ( empty( $composer_auth ) || ! is_array( $composer_auth ) ) {
			$composer_auth = [];
		}
		$github_token = getenv( 'GITHUB_TOKEN' );
		if ( ! isset( $composer_auth['github-oauth'] ) && is_string( $github_token ) ) {
			$composer_auth['github-oauth'] = [ 'github.com' => $github_token ];
			$changed                       = true;
		}
		if ( $changed ) {
			putenv( 'COMPOSER_AUTH=' . json_encode( $composer_auth ) );
		}
	}

	/**
	 * Avoid using default Composer CA bundle if in phar as we don't include it.
	 * See https://github.com/composer/ca-bundle/blob/1.1.0/src/CaBundle.php#L64
	 */
	private function avoid_composer_ca_bundle() {
		if ( Utils\inside_phar() && ! getenv( 'SSL_CERT_FILE' ) && ! getenv( 'SSL_CERT_DIR' ) && ! ini_get( 'openssl.cafile' ) && ! ini_get( 'openssl.capath' ) ) {
			$certificate = Utils\extract_from_phar( WP_CLI_VENDOR_DIR . self::SSL_CERTIFICATE );
			putenv( "SSL_CERT_FILE={$certificate}" );
		}
	}

	/**
	 * Reads the WP-CLI packages composer.json, checking validity and returning array containing its path, contents, and decoded contents.
	 *
	 * @return array Indexed array containing the path, the contents, and the decoded contents of the WP-CLI packages composer.json.
	 */
	private function get_composer_json_path_backup_decoded() {
		$composer_json_obj = $this->get_composer_json();
		$json_path         = $composer_json_obj->getPath();
		$composer_backup   = file_get_contents( $json_path );
		if ( false === $composer_backup ) {
			$error = error_get_last();
			WP_CLI::error( sprintf( "Failed to read '%s': %s", $json_path, $error['message'] ) );
		}
		try {
			$composer_backup_decoded = $composer_json_obj->read();
		} catch ( Exception $e ) {
			WP_CLI::error( sprintf( "Failed to parse '%s' as json: %s", $json_path, $e->getMessage() ) );
		}

		return [ $json_path, $composer_backup, $composer_backup_decoded ];
	}

	/**
	 * Registers a PHP shutdown function to revert composer.json unless
	 * referenced `$revert` flag is false.
	 *
	 * @param string $json_path       Path to composer.json.
	 * @param string $composer_backup Original contents of composer.json.
	 * @param bool   &$revert         Flags whether to revert or not.
	 */
	private function register_revert_shutdown_function( $json_path, $composer_backup, &$revert ) {
		// Allocate all needed memory beforehand as much as possible.
		$revert_msg      = "Reverted composer.json.\n";
		$revert_fail_msg = "Failed to revert composer.json.\n";
		$memory_msg      = "WP-CLI ran out of memory. Please see https://bit.ly/wpclimem for further help.\n";
		$memory_string   = 'Allowed memory size of';
		$error_array     = [
			'type'    => 42,
			'message' => 'Some random dummy string to take up memory',
			'file'    => 'Another random string, which would be a filename this time',
			'line'    => 314,
		];

		register_shutdown_function(
			function () use (
				$json_path,
				$composer_backup,
				&$revert,
				$revert_msg,
				$revert_fail_msg,
				$memory_msg,
				$memory_string,
				$error_array
			) {
				if ( $revert ) {
					if ( false === file_put_contents( $json_path, $composer_backup ) ) {
						fwrite( STDERR, $revert_fail_msg );
					} else {
						fwrite( STDERR, $revert_msg );
					}
				}
				$error_array = error_get_last();
				if ( false !== strpos( $error_array['message'], $memory_string ) ) {
					fwrite( STDERR, $memory_msg );
				}
			}
		);
	}
}