vendor/isotope/isotope-core/system/modules/isotope/library/Isotope/Module/Checkout.php line 602

Open in your IDE?
  1. <?php
  2. /*
  3.  * Isotope eCommerce for Contao Open Source CMS
  4.  *
  5.  * Copyright (C) 2009 - 2019 terminal42 gmbh & Isotope eCommerce Workgroup
  6.  *
  7.  * @link       https://isotopeecommerce.org
  8.  * @license    https://opensource.org/licenses/lgpl-3.0.html
  9.  */
  10. namespace Isotope\Module;
  11. use Contao\Controller;
  12. use Contao\CoreBundle\Exception\PageNotFoundException;
  13. use Contao\CoreBundle\Exception\RedirectResponseException;
  14. use Contao\CoreBundle\Exception\ResponseException;
  15. use Contao\CoreBundle\Routing\ResponseContext\HtmlHeadBag\HtmlHeadBag;
  16. use Contao\PageModel;
  17. use Contao\StringUtil;
  18. use Contao\System;
  19. use Haste\Generator\RowClass;
  20. use Haste\Input\Input;
  21. use Haste\Util\Url;
  22. use Isotope\CheckoutStep\OrderConditions;
  23. use Isotope\Interfaces\IsotopeCheckoutStep;
  24. use Isotope\Interfaces\IsotopeNotificationTokens;
  25. use Isotope\Interfaces\IsotopeProductCollection;
  26. use Isotope\Isotope;
  27. use Isotope\Model\ProductCollection\Order;
  28. use Isotope\Template;
  29. use Symfony\Component\HttpFoundation\Response;
  30. /**
  31.  * Class ModuleIsotopeCheckout
  32.  * Front end module Isotope "checkout".
  33.  *
  34.  * @property array $iso_payment_modules
  35.  * @property array $iso_shipping_modules
  36.  * @property bool  $iso_forward_review
  37.  * @property array $iso_notifications
  38.  * @property bool  $iso_addToAddressbook
  39.  * @property array $iso_checkout_skippable
  40.  * @property array $iso_order_conditions
  41.  * @property int   $orderCompleteJumpTo
  42.  */
  43. class Checkout extends Module
  44. {
  45.     const STEP_ADDRESS 'address';
  46.     const STEP_SHIPPING 'shipping';
  47.     const STEP_PAYMENT 'payment';
  48.     const STEP_REVIEW 'review';
  49.     const STEP_PROCESS 'process';
  50.     const STEP_COMPLETE 'complete';
  51.     const STEP_FAILED 'failed';
  52.     /**
  53.      * Template
  54.      * @var string
  55.      */
  56.     protected $strTemplate 'mod_iso_checkout';
  57.     /**
  58.      * Do not continue to next step
  59.      * @var boolean
  60.      */
  61.     public $doNotSubmit false;
  62.     /**
  63.      * Disable caching of the frontend page if this module is in use.
  64.      * @var boolean
  65.      */
  66.     protected $blnDisableCache true;
  67.     /**
  68.      * Current step
  69.      * @var string
  70.      */
  71.     protected $strCurrentStep;
  72.     /**
  73.      * Checkout steps that can be skipped
  74.      * @var array
  75.      */
  76.     protected $skippableSteps = array();
  77.     /**
  78.      * Form ID
  79.      * @var string
  80.      */
  81.     protected $strFormId 'iso_mod_checkout';
  82.     /**
  83.      * @inheritDoc
  84.      */
  85.     protected function getSerializedProperties()
  86.     {
  87.         $props parent::getSerializedProperties();
  88.         $props[] = 'iso_checkout_skippable';
  89.         return $props;
  90.     }
  91.     /**
  92.      * Display a wildcard in the back end
  93.      *
  94.      * @return string
  95.      */
  96.     public function generate()
  97.     {
  98.         if ('BE' === TL_MODE) {
  99.             return $this->generateWildcard();
  100.         }
  101.         $this->strCurrentStep Input::getAutoItem('step');
  102.         if ($this->strCurrentStep == '') {
  103.             $this->redirectToNextStep();
  104.         }
  105.         return parent::generate();
  106.     }
  107.     /**
  108.      * Returns the current form ID
  109.      *
  110.      * @return string
  111.      */
  112.     public function getFormId()
  113.     {
  114.         return $this->strFormId;
  115.     }
  116.     /**
  117.      * Generate module
  118.      */
  119.     protected function compile()
  120.     {
  121.         $arrBuffer = array();
  122.         // Default template settings. Must be set at beginning so they can be overwritten later (eg. trough callback)
  123.         $this->Template->formId $this->strFormId;
  124.         $this->Template->formSubmit $this->strFormId;
  125.         $this->Template->enctype 'application/x-www-form-urlencoded';
  126.         $this->Template->previousLabel StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['previousStep']);
  127.         $this->Template->nextLabel StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['nextStep']);
  128.         $this->Template->nextClass 'next';
  129.         $this->Template->showPrevious true;
  130.         $this->Template->showNext true;
  131.         $this->Template->showForm true;
  132.         $this->Template->steps = array();
  133.         // These steps are handled internally by the checkout module and are not in the config array
  134.         switch ($this->strCurrentStep) {
  135.             // Complete order after successful payment
  136.             // At this stage, we do no longer use the client's cart but the order through UID in URL
  137.             case self::STEP_COMPLETE:
  138.                 /** @var Order $objOrder */
  139.                 if (($objOrder Order::findOneBy('uniqid', (string) Input::get('uid'))) === null) {
  140.                     if (Isotope::getCart()->isEmpty()) {
  141.                         throw new PageNotFoundException();
  142.                     }
  143.                     static::redirectToStep(self::STEP_FAILED);
  144.                 }
  145.                 // Order already completed (see #1441)
  146.                 if ($objOrder->checkout_complete) {
  147.                     Controller::redirect(Url::addQueryString('uid=' $objOrder->uniqid$this->orderCompleteJumpTo));
  148.                 }
  149.                 $strBuffer $objOrder->hasPayment() ? $objOrder->getPaymentMethod()->processPayment($objOrder$this) : true;
  150.                 // true means the payment is successful and order should be completed
  151.                 if ($strBuffer === true) {
  152.                     // If checkout is successful, complete order and redirect to confirmation page
  153.                     if ($objOrder->checkout() && $objOrder->complete()) {
  154.                         Controller::redirect(
  155.                             Url::addQueryString('uid=' $objOrder->uniqid$this->orderCompleteJumpTo)
  156.                         );
  157.                     }
  158.                     // Checkout failed, show error message
  159.                     static::redirectToStep(self::STEP_FAILED);
  160.                 }
  161.                 // False means payment has failed
  162.                 elseif ($strBuffer === false) {
  163.                     static::redirectToStep(self::STEP_FAILED);
  164.                 }
  165.                 // Otherwise we assume a string that shows a message to customer
  166.                 else {
  167.                     $this->Template->showNext     false;
  168.                     $this->Template->showPrevious false;
  169.                     $arrBuffer                    = array(array('html' => $strBuffer'class' => $this->strCurrentStep));
  170.                 }
  171.                 break;
  172.             // Process order and initiate payment method if necessary
  173.             case self::STEP_PROCESS:
  174.                 // canCheckout will override the template and show a message
  175.                 if (!$this->canCheckout()) {
  176.                     return;
  177.                 }
  178.                 $arrSteps $this->getSteps();
  179.                 // Make sure all steps have passed successfully
  180.                 foreach ($arrSteps as $step => $arrModules) {
  181.                     /** @var IsotopeCheckoutStep $objModule */
  182.                     foreach ($arrModules as $objModule) {
  183.                         $objModule->generate();
  184.                         if ($objModule->hasError()) {
  185.                             static::redirectToStep($step);
  186.                         }
  187.                     }
  188.                 }
  189.                 $objOrder Isotope::getCart()->getDraftOrder();
  190.                 $objOrder->checkout_info        $this->getCheckoutInfo($arrSteps);
  191.                 $objOrder->nc_notification      $this->iso_notifications;
  192.                 $objOrder->iso_addToAddressbook $this->iso_addToAddressbook;
  193.                 $objOrder->iso_checkout_skippable $this->iso_checkout_skippable;
  194.                 $objOrder->orderdetails_page $this->orderCompleteJumpTo;
  195.                 $objOrder->email_data           $this->getNotificationTokensFromSteps($arrSteps$objOrder);
  196.                 // !HOOK: pre-process checkout
  197.                 if (isset($GLOBALS['ISO_HOOKS']['preCheckout']) && \is_array($GLOBALS['ISO_HOOKS']['preCheckout'])) {
  198.                     foreach ($GLOBALS['ISO_HOOKS']['preCheckout'] as $callback) {
  199.                         if (System::importStatic($callback[0])->{$callback[1]}($objOrder$this) === false) {
  200.                             System::log('Callback ' $callback[0] . '::' $callback[1] . '() cancelled checkout for Order ID ' $this->id__METHOD__TL_ERROR);
  201.                             static::redirectToStep(self::STEP_FAILED);
  202.                         }
  203.                     }
  204.                 }
  205.                 $objOrder->lock();
  206.                 $strBuffer $objOrder->hasPayment() ? $objOrder->getPaymentMethod()->checkoutForm($objOrder$this) : false;
  207.                 if ($strBuffer instanceof Response) {
  208.                     throw new ResponseException($strBuffer);
  209.                 }
  210.                 if (false === $strBuffer) {
  211.                     static::redirectToStep(self::STEP_COMPLETE$objOrder);
  212.                 }
  213.                 $this->Template->showForm false;
  214.                 $this->doNotSubmit        true;
  215.                 $arrBuffer                = array(array('html' => $strBuffer'class' => $this->strCurrentStep));
  216.                 break;
  217.             // Checkout/payment has failed, show the review page again with an error message
  218.             /** @noinspection PhpMissingBreakStatementInspection */
  219.             case self::STEP_FAILED:
  220.                 $this->Template->mtype   'error';
  221.                 $this->Template->message = \strlen(Input::get('reason')) ? Input::get('reason') : $GLOBALS['TL_LANG']['ERR']['orderFailed'];
  222.                 $this->strCurrentStep    'review';
  223.                 // no break
  224.             default:
  225.                 // canCheckout will override the template and show a message
  226.                 if (!$this->canCheckout()) {
  227.                     return;
  228.                 }
  229.                 $arrBuffer $this->generateSteps($this->getSteps());
  230.                 break;
  231.         }
  232.         RowClass::withKey('class')->addFirstLast()->applyTo($arrBuffer);
  233.         $this->Template->fields $arrBuffer;
  234.     }
  235.     /**
  236.      * Run through all steps until we find the current one or one reports failure
  237.      *
  238.      * @param array $arrSteps
  239.      *
  240.      * @return array
  241.      */
  242.     protected function generateSteps(array $arrSteps)
  243.     {
  244.         $arrBuffer = array();
  245.         $intCurrentStep 0;
  246.         $intTotalSteps  = \count($arrSteps);
  247.         if (!isset($arrSteps[$this->strCurrentStep])) {
  248.             $this->redirectToNextStep();
  249.         }
  250.         $arrStepKeys array_keys($arrSteps);
  251.         $this->skippableSteps = array();
  252.         /**
  253.          * Run trough all steps until we find the current one or one reports failure
  254.          * @var string                $step
  255.          * @var IsotopeCheckoutStep[] $arrModules
  256.          */
  257.         foreach ($arrSteps as $step => $arrModules) {
  258.             $this->strFormId            'iso_mod_checkout_' $step;
  259.             $this->Template->formId     $this->strFormId;
  260.             $this->Template->formSubmit $this->strFormId;
  261.             ++$intCurrentStep;
  262.             $arrBuffer                   = array();
  263.             $this->skippableSteps[$step] = true;
  264.             foreach ($arrModules as $objModule) {
  265.                 $arrBuffer[] = array(
  266.                     'class' => StringUtil::standardize($step) . ' ' $objModule->getStepClass(),
  267.                     'html'  => $objModule->generate()
  268.                 );
  269.                 if (!$objModule->isSkippable()) {
  270.                     $this->skippableSteps[$step] = false;
  271.                 }
  272.                 if ($objModule->hasError()) {
  273.                     $this->doNotSubmit true;
  274.                     $this->skippableSteps[$step]  = false;
  275.                 }
  276.                 // the user wanted to proceed but the current step is not completed yet
  277.                 if ($this->doNotSubmit && $step != $this->strCurrentStep) {
  278.                     static::redirectToStep($step);
  279.                 }
  280.             }
  281.             if ($this->skippableSteps[$step] ?? false) {
  282.                 unset($arrStepKeys[array_search($step$arrStepKeys)]);
  283.                 $intCurrentStep -= 1;
  284.                 $intTotalSteps -= 1;
  285.             }
  286.             if ($step == $this->strCurrentStep) {
  287.                 if ($this->skippableSteps[$step] ?? false) {
  288.                     $this->redirectToNextStep();
  289.                 }
  290.                 global $objPage;
  291.                 $pageTitle sprintf($GLOBALS['TL_LANG']['MSC']['checkoutStep'], $intCurrentStep$intTotalSteps, ($GLOBALS['TL_LANG']['MSC']['checkout_' $step] ?: $step)) . ($objPage->pageTitle ?: $objPage->title);
  292.                 // Support response context in Contao 4.13
  293.                 if (System::getContainer()->has('contao.routing.response_context_accessor')) {
  294.                     $responseContext System::getContainer()->get('contao.routing.response_context_accessor')->getResponseContext();
  295.                     $htmlDecoder System::getContainer()->get('contao.string.html_decoder');
  296.                     if ($responseContext && $htmlDecoder && $responseContext->has(HtmlHeadBag::class)) {
  297.                         $responseContext
  298.                             ->get(HtmlHeadBag::class)
  299.                             ->setTitle($htmlDecoder->inputEncodedToPlainText($pageTitle))
  300.                         ;
  301.                         break;
  302.                     }
  303.                 }
  304.                 $objPage->pageTitle $pageTitle;
  305.                 break;
  306.             }
  307.         }
  308.         $arrStepKeys array_values($arrStepKeys);
  309.         $this->Template->steps      $this->generateStepNavigation($arrStepKeys);
  310.         $this->Template->activeStep $GLOBALS['TL_LANG']['MSC']['activeStep'];
  311.         // Hide back buttons it this is the first step
  312.         if (array_search($this->strCurrentStep$arrStepKeys) === 0) {
  313.             $this->Template->showPrevious false;
  314.         }
  315.         // Show "confirm order" button if this is the last step
  316.         if (array_search($this->strCurrentStep$arrStepKeys) === (\count($arrStepKeys) - 1)) {
  317.             $this->Template->nextClass 'confirm';
  318.             $this->Template->nextLabel StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['confirmOrder']);
  319.         }
  320.         // User pressed "back" button
  321.         if (\strlen(Input::post('previousStep'))) {
  322.             $this->redirectToPreviousStep();
  323.         } // Valid input data, redirect to next step
  324.         elseif (Input::post('FORM_SUBMIT') == $this->strFormId && !$this->doNotSubmit) {
  325.             $this->redirectToNextStep();
  326.         }
  327.         return $arrBuffer;
  328.     }
  329.     /**
  330.      * Redirect visitor to the next step in ISO_CHECKOUTSTEP
  331.      */
  332.     protected function redirectToNextStep()
  333.     {
  334.         $arrSteps array_keys($this->getSteps());
  335.         $intKey   array_search($this->strCurrentStep$arrSteps);
  336.         if (false === $intKey) {
  337.             if ($this->iso_forward_review) {
  338.                 static::redirectToStep('review');
  339.             }
  340.             $intKey = -1;
  341.         } // redirect to step "process" if the next step is the last one
  342.         elseif (($intKey 1) == \count($arrSteps)) {
  343.             static::redirectToStep(self::STEP_PROCESS);
  344.         }
  345.         $step $arrSteps[$intKey 1];
  346.         if ($this->skippableSteps[$step] ?? false) {
  347.             $this->strCurrentStep $step;
  348.             $this->redirectToNextStep();
  349.         }
  350.         static::redirectToStep($step);
  351.     }
  352.     /**
  353.      * Redirect visitor to the previous step in ISO_CHECKOUTSTEP
  354.      */
  355.     protected function redirectToPreviousStep()
  356.     {
  357.         $arrSteps array_keys($this->getSteps());
  358.         $intKey   array_search($this->strCurrentStep$arrSteps);
  359.         if (false === $intKey || === $intKey) {
  360.             $intKey 1;
  361.         }
  362.         $step $arrSteps[$intKey 1];
  363.         if ($this->skippableSteps[$step] ?? false) {
  364.             $this->strCurrentStep $step;
  365.             $this->redirectToPreviousStep();
  366.         }
  367.         static::redirectToStep($step);
  368.     }
  369.     /**
  370.      * Return the checkout information as array
  371.      *
  372.      * @param array $arrSteps
  373.      *
  374.      * @return array
  375.      */
  376.     public function getCheckoutInfo(array $arrSteps null)
  377.     {
  378.         if (null === $arrSteps) {
  379.             $arrSteps $this->getSteps();
  380.         }
  381.         $arrCheckoutInfo = array();
  382.         // Run trough all steps to collect checkout information
  383.         /** @var IsotopeCheckoutStep[] $arrModules */
  384.         foreach ($arrSteps as $arrModules) {
  385.             foreach ($arrModules as $objModule) {
  386.                 $arrInfo $objModule->review();
  387.                 if (!empty($arrInfo) && \is_array($arrInfo)) {
  388.                     /** @noinspection AdditionOperationOnArraysInspection */
  389.                     $arrCheckoutInfo += $arrInfo;
  390.                 }
  391.             }
  392.         }
  393.         RowClass::withKey('class')->addFirstLast()->applyTo($arrCheckoutInfo);
  394.         return $arrCheckoutInfo;
  395.     }
  396.     /**
  397.      * Retrieve the array of notification data for parsing simple tokens
  398.      *
  399.      * @param array                    $arrSteps
  400.      * @param IsotopeProductCollection $objOrder
  401.      *
  402.      * @return array
  403.      */
  404.     protected function getNotificationTokensFromSteps(array $arrStepsIsotopeProductCollection $objOrder)
  405.     {
  406.         $arrTokens = array();
  407.         // Run through all steps to collect checkout information
  408.         foreach ($arrSteps as $arrModules) {
  409.             /** @var IsotopeCheckoutStep $objModule */
  410.             foreach ($arrModules as $objModule) {
  411.                 // Method check is BC for when IsotopeCheckoutStep contained "getNotificationTokens" method
  412.                 if ($objModule instanceof IsotopeNotificationTokens || \method_exists($objModule'getNotificationTokens')) {
  413.                     $arrTokens array_merge($arrTokens$objModule->getNotificationTokens($objOrder));
  414.                 }
  415.             }
  416.         }
  417.         return $arrTokens;
  418.     }
  419.     /**
  420.      * Check if the checkout can be executed
  421.      *
  422.      * @return bool
  423.      */
  424.     protected function canCheckout()
  425.     {
  426.         // Redirect to login page if not logged in
  427.         if ('member' === $this->iso_checkout_method && true !== FE_USER_LOGGED_IN) {
  428.             /** @var PageModel $objJump */
  429.             $objJump PageModel::findPublishedById($this->iso_login_jumpTo);
  430.             if (null === $objJump) {
  431.                 $this->Template          = new Template('mod_message');
  432.                 $this->Template->type    'error';
  433.                 $this->Template->message $GLOBALS['TL_LANG']['ERR']['isoLoginRequired'];
  434.                 return false;
  435.             }
  436.             $objJump->loadDetails();
  437.             Controller::redirect($objJump->getFrontendUrl(null$objJump->language));
  438.         } elseif ('guest' === $this->iso_checkout_method && true === FE_USER_LOGGED_IN) {
  439.             $this->Template          = new Template('mod_message');
  440.             $this->Template->type    'error';
  441.             $this->Template->message $GLOBALS['TL_LANG']['ERR']['checkoutNotAllowed'];
  442.             return false;
  443.         }
  444.         // Return error message if cart is empty
  445.         if (Isotope::getCart()->isEmpty()) {
  446.             $this->Template          = new Template('mod_message');
  447.             $this->Template->type    'empty';
  448.             $this->Template->message $GLOBALS['TL_LANG']['MSC']['noItemsInCart'];
  449.             return false;
  450.         }
  451.         // Insufficient cart subtotal
  452.         if (Isotope::getCart()->hasErrors()) {
  453.             if ($this->iso_cart_jumpTo 0) {
  454.                 /** @var PageModel $objJump */
  455.                 $objJump PageModel::findPublishedById($this->iso_cart_jumpTo);
  456.                 if (null !== $objJump) {
  457.                     $objJump->loadDetails();
  458.                     Controller::redirect($objJump->getFrontendUrl(null$objJump->language));
  459.                 }
  460.             }
  461.             $this->Template          = new Template('mod_message');
  462.             $this->Template->type    'error';
  463.             $this->Template->message implode("</p>\n<p class=\"error message\">"Isotope::getCart()->getErrors());
  464.             return false;
  465.         }
  466.         return true;
  467.     }
  468.     /**
  469.      * Returns whether the checkout step by given name can be skipped.
  470.      *
  471.      * @param string $step
  472.      *
  473.      * @return bool
  474.      */
  475.     public function canSkipStep($step)
  476.     {
  477.         return \in_array($step$this->iso_checkout_skippabletrue);
  478.     }
  479.     /**
  480.      * Return array of instantiated checkout step modules
  481.      *
  482.      * @return array
  483.      */
  484.     protected function getSteps()
  485.     {
  486.         $arrOrderConditions = [];
  487.         $arrSteps = [];
  488.         $productTypeIds = [];
  489.         foreach (Isotope::getCart()->getItems() as $objItem) {
  490.             if ($objItem->hasProduct()) {
  491.                 $productType $objItem->getProduct()->getType();
  492.                 $productTypeIds[] = null === $productType : (int) $productType->id;
  493.             }
  494.         }
  495.         $productTypeIds array_unique($productTypeIds);
  496.         foreach (StringUtil::deserialize($this->iso_order_conditionstrue) as $config) {
  497.             if (empty($config['form'])) {
  498.                 continue;
  499.             }
  500.             $configProductTypes StringUtil::deserialize($config['product_types']);
  501.             if (!empty($configProductTypes) && \is_array($configProductTypes)) {
  502.                 switch ($config['product_types_condition']) {
  503.                     case 'onlyAvailable':
  504.                         if (\count(array_diff($productTypeIds$configProductTypes)) > 0) {
  505.                             continue(2);
  506.                         }
  507.                         break;
  508.                     case 'oneAvailable':
  509.                         if (\count(array_intersect($configProductTypes$productTypeIds)) === 0) {
  510.                             continue(2);
  511.                         }
  512.                         break;
  513.                     case 'allAvailable':
  514.                         if (\count(array_intersect($configProductTypes$productTypeIds)) !== \count($configProductTypes)) {
  515.                             continue(2);
  516.                         }
  517.                         break;
  518.                 }
  519.             }
  520.             $arrOrderConditions[$config['step']][$config['position']][] = $config['form'];
  521.         }
  522.         foreach ($GLOBALS['ISO_CHECKOUTSTEP'] as $strStep => $arrModules) {
  523.             foreach ($arrModules as $strClass) {
  524.                 $objModule = new $strClass($this);
  525.                 if (!$objModule instanceof IsotopeCheckoutStep) {
  526.                     throw new \RuntimeException("$strClass has to implement Isotope\\Interfaces\\IsotopeCheckoutStep");
  527.                 }
  528.                 if (isset($arrOrderConditions[$strClass]['before'])) {
  529.                     foreach ($arrOrderConditions[$strClass]['before'] as $form) {
  530.                         $arrSteps[$strStep][] = new OrderConditions($this$form);
  531.                     }
  532.                 }
  533.                 if ($objModule->isAvailable()) {
  534.                     $arrSteps[$strStep][] = $objModule;
  535.                 }
  536.                 if (isset($arrOrderConditions[$strClass]['after'])) {
  537.                     foreach ($arrOrderConditions[$strClass]['after'] as $form) {
  538.                         $arrSteps[$strStep][] = new OrderConditions($this$form);
  539.                     }
  540.                 }
  541.             }
  542.         }
  543.         return $arrSteps;
  544.     }
  545.     /**
  546.      * Generate checkout step navigation
  547.      *
  548.      * @param array $arrStepKeys
  549.      *
  550.      * @return array
  551.      */
  552.     protected function generateStepNavigation(array $arrStepKeys)
  553.     {
  554.         $arrItems  = array();
  555.         $blnPassed true;
  556.         foreach ($arrStepKeys as $step) {
  557.             $blnActive false;
  558.             $href      '';
  559.             $class     StringUtil::standardize($step);
  560.             if ($this->strCurrentStep == $step) {
  561.                 $blnPassed false;
  562.                 $blnActive true;
  563.                 $class .= ' active';
  564.             } elseif ($blnPassed) {
  565.                 $href = static::generateUrlForStep($step);
  566.                 $class .= ' passed';
  567.             }
  568.             $arrItems[] = array
  569.             (
  570.                 'isActive' => $blnActive,
  571.                 'class'    => $class,
  572.                 'link'     => $GLOBALS['TL_LANG']['MSC']['checkout_' $step] ? : $step,
  573.                 'href'     => $href,
  574.                 'title'    => StringUtil::specialchars(sprintf($GLOBALS['TL_LANG']['MSC']['checkboutStepBack'], ($GLOBALS['TL_LANG']['MSC']['checkout_' $step] ? : $step))),
  575.             );
  576.         }
  577.         // Add first/last classes
  578.         RowClass::withKey('class')->addFirstLast()->applyTo($arrItems);
  579.         return $arrItems;
  580.     }
  581.     /**
  582.      * Redirect to given checkout step
  583.      *
  584.      * @param string                   $strStep
  585.      * @param IsotopeProductCollection $objCollection
  586.      */
  587.     public static function redirectToStep($strStepIsotopeProductCollection $objCollection null)
  588.     {
  589.         Isotope::getCart()->save();
  590.         throw new RedirectResponseException(static::generateUrlForStep($strStep$objCollectionnulltrue));
  591.     }
  592.     /**
  593.      * Generate frontend URL for current page including the given checkout step
  594.      *
  595.      * @param string $strStep
  596.      *
  597.      * @return string
  598.      */
  599.     public static function generateUrlForStep($strStepIsotopeProductCollection $objCollection nullPageModel $objTarget nullbool $absolute false)
  600.     {
  601.         if (null === $objTarget) {
  602.             global $objPage;
  603.             $objTarget $objPage;
  604.         }
  605.         if (!$GLOBALS['TL_CONFIG']['useAutoItem'] || !\in_array('step'$GLOBALS['TL_AUTO_ITEM'], true)) {
  606.             $strStep 'step/' $strStep;
  607.         }
  608.         $strUrl $absolute $objTarget->getAbsoluteUrl('/' $strStep) : $objTarget->getFrontendUrl('/' $strStep);
  609.         if (null !== $objCollection) {
  610.             $strUrl Url::addQueryString('uid=' $objCollection->getUniqueId(), $strUrl);
  611.         }
  612.         return $strUrl;
  613.     }
  614. }