diff --git a/app/code/Magento/Authorizenet/etc/adminhtml/system.xml b/app/code/Magento/Authorizenet/etc/adminhtml/system.xml
index fc86c0d2dc68..3f2037f70b2d 100644
--- a/app/code/Magento/Authorizenet/etc/adminhtml/system.xml
+++ b/app/code/Magento/Authorizenet/etc/adminhtml/system.xml
@@ -85,9 +85,11 @@
+ validate-number validate-zero-or-greater
+ validate-number validate-zero-or-greater
diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/ActionGroup/ConfigureAuthorizenetAcceptjsActionGroup.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/ActionGroup/ConfigureAuthorizenetAcceptjsActionGroup.xml
index e9a194435e3e..eac4affcb9db 100644
--- a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/ActionGroup/ConfigureAuthorizenetAcceptjsActionGroup.xml
+++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/ActionGroup/ConfigureAuthorizenetAcceptjsActionGroup.xml
@@ -7,40 +7,43 @@
-->
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/ConfigureAuthorizenetAcceptjsWithoutRequiredOptionsTest.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/ConfigureAuthorizenetAcceptjsWithoutRequiredOptionsTest.xml
new file mode 100644
index 000000000000..cbb702c26f17
--- /dev/null
+++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/ConfigureAuthorizenetAcceptjsWithoutRequiredOptionsTest.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/FullCaptureAuthorizenetAcceptjsTest.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/FullCaptureAuthorizenetAcceptjsTest.xml
index 6aa6792e0e0d..7f25482d627e 100644
--- a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/FullCaptureAuthorizenetAcceptjsTest.xml
+++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/FullCaptureAuthorizenetAcceptjsTest.xml
@@ -31,9 +31,11 @@
+
+
diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/GuestCheckoutVirtualProductAuthorizenetAcceptjsTest.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/GuestCheckoutVirtualProductAuthorizenetAcceptjsTest.xml
index 6f71bd180766..919c32d8f70d 100644
--- a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/GuestCheckoutVirtualProductAuthorizenetAcceptjsTest.xml
+++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/GuestCheckoutVirtualProductAuthorizenetAcceptjsTest.xml
@@ -36,9 +36,11 @@
+
+
diff --git a/app/code/Magento/AuthorizenetAcceptjs/etc/adminhtml/system.xml b/app/code/Magento/AuthorizenetAcceptjs/etc/adminhtml/system.xml
index 279a904d916a..8623919cf5d6 100644
--- a/app/code/Magento/AuthorizenetAcceptjs/etc/adminhtml/system.xml
+++ b/app/code/Magento/AuthorizenetAcceptjs/etc/adminhtml/system.xml
@@ -17,7 +17,7 @@
-
+
1
Magento\Config\Block\System\Config\Form\Fieldset
@@ -39,25 +39,44 @@
Magento\Config\Model\Config\Backend\Encrypted
payment/authorizenet_acceptjs/login
+ required-entry
+
+ 1
+
Magento\Config\Model\Config\Backend\Encrypted
payment/authorizenet_acceptjs/trans_key
+ required-entry
+
+ 1
+
payment/authorizenet_acceptjs/public_client_key
+ required-entry
+
+ 1
+
Magento\Config\Model\Config\Backend\Encrypted
payment/authorizenet_acceptjs/trans_signature_key
+ required-entry
+
+ 1
+
Magento\Config\Model\Config\Backend\Encrypted
payment/authorizenet_acceptjs/trans_md5
+
+ 1
+
@@ -101,10 +120,12 @@
payment/authorizenet_acceptjs/min_order_total
+ validate-number validate-zero-or-greater
payment/authorizenet_acceptjs/max_order_total
+ validate-number validate-zero-or-greater
diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/frontend/web/js/view/payment/method-renderer/authorizenet-accept.js b/app/code/Magento/AuthorizenetAcceptjs/view/frontend/web/js/view/payment/method-renderer/authorizenet-accept.js
index 983318c4cdaa..bba1290a9eed 100644
--- a/app/code/Magento/AuthorizenetAcceptjs/view/frontend/web/js/view/payment/method-renderer/authorizenet-accept.js
+++ b/app/code/Magento/AuthorizenetAcceptjs/view/frontend/web/js/view/payment/method-renderer/authorizenet-accept.js
@@ -91,8 +91,10 @@ define([
return;
}
- authData.clientKey = window.checkoutConfig.payment[this.getCode()].clientKey;
- authData.apiLoginID = window.checkoutConfig.payment[this.getCode()].apiLoginID;
+ authData.clientKey = window.checkoutConfig.payment[this.getCode()].clientKey !== null ?
+ window.checkoutConfig.payment[this.getCode()].clientKey : '';
+ authData.apiLoginID = window.checkoutConfig.payment[this.getCode()].apiLoginID !== null ?
+ window.checkoutConfig.payment[this.getCode()].apiLoginID : '';
cardData.cardNumber = this.creditCardNumber();
cardData.month = this.creditCardExpMonth();
diff --git a/app/code/Magento/AuthorizenetGraphQl/Model/AuthorizenetDataProvider.php b/app/code/Magento/AuthorizenetGraphQl/Model/AuthorizenetDataProvider.php
index eb241d376e24..207d21994308 100644
--- a/app/code/Magento/AuthorizenetGraphQl/Model/AuthorizenetDataProvider.php
+++ b/app/code/Magento/AuthorizenetGraphQl/Model/AuthorizenetDataProvider.php
@@ -11,7 +11,7 @@
use Magento\Framework\Stdlib\ArrayManager;
/**
- * DataProvider Model for Authorizenet
+ * SetPaymentMethod additional data provider model for Authorizenet payment method
*/
class AuthorizenetDataProvider implements AdditionalDataProviderInterface
{
@@ -23,7 +23,6 @@ class AuthorizenetDataProvider implements AdditionalDataProviderInterface
private $arrayManager;
/**
- * AuthorizenetDataProvider constructor.
* @param ArrayManager $arrayManager
*/
public function __construct(
@@ -42,19 +41,19 @@ public function getData(array $data): array
{
$additionalData = $this->arrayManager->get(static::PATH_ADDITIONAL_DATA, $data) ?? [];
foreach ($additionalData as $key => $value) {
- $additionalData[$this->snakeCaseToCamelCase($key)] = $value;
+ $additionalData[$this->convertSnakeCaseToCamelCase($key)] = $value;
unset($additionalData[$key]);
}
return $additionalData;
}
/**
- * Converts an input string from snake_case to camelCase.
+ * Convert an input string from snake_case to camelCase.
*
* @param string $input
* @return string
*/
- private function snakeCaseToCamelCase($input)
+ private function convertSnakeCaseToCamelCase($input): string
{
return lcfirst(str_replace('_', '', ucwords($input, '_')));
}
diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/ClickSaveActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/ClickSaveActionGroup.xml
new file mode 100644
index 000000000000..4fa8bf1ce6ab
--- /dev/null
+++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/ClickSaveActionGroup.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Braintree/view/frontend/requirejs-config.js b/app/code/Magento/Braintree/view/frontend/requirejs-config.js
index 8e6d7f8062eb..5c9bcd88de73 100644
--- a/app/code/Magento/Braintree/view/frontend/requirejs-config.js
+++ b/app/code/Magento/Braintree/view/frontend/requirejs-config.js
@@ -6,11 +6,11 @@
var config = {
map: {
'*': {
- braintreeClient: 'https://js.braintreegateway.com/web/3.46.0-beta-3ds.8/js/client.min.js',
- braintreeHostedFields: 'https://js.braintreegateway.com/web/3.46.0-beta-3ds.8/js/hosted-fields.min.js',
- braintreePayPal: 'https://js.braintreegateway.com/web/3.46.0-beta-3ds.8/js/paypal-checkout.min.js',
- braintree3DSecure: 'https://js.braintreegateway.com/web/3.46.0-beta-3ds.8/js/three-d-secure.min.js',
- braintreeDataCollector: 'https://js.braintreegateway.com/web/3.46.0-beta-3ds.8/js/data-collector.min.js'
+ braintreeClient: 'https://js.braintreegateway.com/web/3.48.0/js/client.min.js',
+ braintreeHostedFields: 'https://js.braintreegateway.com/web/3.48.0/js/hosted-fields.min.js',
+ braintreePayPal: 'https://js.braintreegateway.com/web/3.48.0/js/paypal-checkout.min.js',
+ braintree3DSecure: 'https://js.braintreegateway.com/web/3.48.0/js/three-d-secure.min.js',
+ braintreeDataCollector: 'https://js.braintreegateway.com/web/3.48.0/js/data-collector.min.js'
}
},
paths: {
diff --git a/app/code/Magento/Bundle/Setup/Patch/Data/ApplyAttributesUpdate.php b/app/code/Magento/Bundle/Setup/Patch/Data/ApplyAttributesUpdate.php
index d8ad1757ab2e..cdbe1906df84 100644
--- a/app/code/Magento/Bundle/Setup/Patch/Data/ApplyAttributesUpdate.php
+++ b/app/code/Magento/Bundle/Setup/Patch/Data/ApplyAttributesUpdate.php
@@ -14,8 +14,7 @@
use Magento\Eav\Setup\EavSetupFactory;
/**
- * Class ApplyAttributesUpdate
- * @package Magento\Bundle\Setup\Patch
+ * Class \Magento\Bundle\Setup\Patch\ApplyAttributesUpdate
*/
class ApplyAttributesUpdate implements DataPatchInterface, PatchVersionInterface
{
@@ -44,7 +43,7 @@ public function __construct(
}
/**
- * {@inheritdoc}
+ * @inheritdoc
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*/
public function apply()
@@ -66,8 +65,8 @@ public function apply()
',',
$eavSetup->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $field, 'apply_to')
);
- if (!in_array('bundle', $applyTo)) {
- $applyTo[] = 'bundle';
+ if (!in_array(\Magento\Catalog\Model\Product\Type::TYPE_BUNDLE, $applyTo)) {
+ $applyTo[] = \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE;
$eavSetup->updateAttribute(
\Magento\Catalog\Model\Product::ENTITY,
$field,
@@ -78,7 +77,7 @@ public function apply()
}
$applyTo = explode(',', $eavSetup->getAttribute(\Magento\Catalog\Model\Product::ENTITY, 'cost', 'apply_to'));
- unset($applyTo[array_search('bundle', $applyTo)]);
+ unset($applyTo[array_search(\Magento\Catalog\Model\Product\Type::TYPE_BUNDLE, $applyTo)]);
$eavSetup->updateAttribute(\Magento\Catalog\Model\Product::ENTITY, 'cost', 'apply_to', implode(',', $applyTo));
/**
@@ -106,7 +105,7 @@ public function apply()
'visible_on_front' => false,
'used_in_product_listing' => true,
'unique' => false,
- 'apply_to' => 'bundle'
+ 'apply_to' => \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE
]
);
@@ -131,7 +130,7 @@ public function apply()
'comparable' => false,
'visible_on_front' => false,
'unique' => false,
- 'apply_to' => 'bundle'
+ 'apply_to' => \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE
]
);
@@ -157,7 +156,7 @@ public function apply()
'visible_on_front' => false,
'used_in_product_listing' => true,
'unique' => false,
- 'apply_to' => 'bundle'
+ 'apply_to' => \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE
]
);
@@ -184,7 +183,7 @@ public function apply()
'visible_on_front' => false,
'used_in_product_listing' => true,
'unique' => false,
- 'apply_to' => 'bundle'
+ 'apply_to' => \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE
]
);
@@ -210,13 +209,13 @@ public function apply()
'visible_on_front' => false,
'used_in_product_listing' => true,
'unique' => false,
- 'apply_to' => 'bundle'
+ 'apply_to' => \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE
]
);
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public static function getDependencies()
{
@@ -224,7 +223,7 @@ public static function getDependencies()
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public static function getVersion()
{
@@ -232,7 +231,7 @@ public static function getVersion()
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function getAliases()
{
diff --git a/app/code/Magento/Bundle/Setup/Patch/Schema/ChangeTmpTablesEngine.php b/app/code/Magento/Bundle/Setup/Patch/Schema/ChangeTmpTablesEngine.php
index adb178796f88..c6a67cc5a110 100644
--- a/app/code/Magento/Bundle/Setup/Patch/Schema/ChangeTmpTablesEngine.php
+++ b/app/code/Magento/Bundle/Setup/Patch/Schema/ChangeTmpTablesEngine.php
@@ -41,7 +41,10 @@ public function apply()
'catalog_product_index_price_bundle_opt_tmp',
];
foreach ($tables as $table) {
- $this->schemaSetup->getConnection()->changeTableEngine($table, 'InnoDB');
+ $tableName = $this->schemaSetup->getTable($table);
+ if ($this->schemaSetup->getConnection()->isTableExists($tableName)) {
+ $this->schemaSetup->getConnection()->changeTableEngine($tableName, 'InnoDB');
+ }
}
$this->schemaSetup->endSetup();
diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/options.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/options.phtml
index cac96a8aca7c..ec425ca7711a 100644
--- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/options.phtml
+++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/options.phtml
@@ -26,7 +26,7 @@ $options = $block->decorateArray($block->getOptions($stripSelection));
+ validate-number validate-zero-or-greater
+ validate-number validate-zero-or-greater
@@ -76,9 +78,11 @@
+ validate-number validate-zero-or-greater
+ validate-number validate-zero-or-greater
@@ -109,12 +113,15 @@
+ validate-number validate-zero-or-greater
+ validate-number validate-zero-or-greater
+ validate-number
@@ -144,12 +151,15 @@
+ validate-number validate-zero-or-greater
+ validate-number validate-zero-or-greater
+ validate-number
diff --git a/app/code/Magento/PageCache/Model/Config.php b/app/code/Magento/PageCache/Model/Config.php
index 079e371d3436..10ae41be21d4 100644
--- a/app/code/Magento/PageCache/Model/Config.php
+++ b/app/code/Magento/PageCache/Model/Config.php
@@ -214,18 +214,18 @@ protected function _getReplacements()
*/
protected function _getAccessList()
{
- $result = '';
- $tpl = " \"%s\";";
+ $tpl = ' "%s";';
$accessList = $this->_scopeConfig->getValue(self::XML_VARNISH_PAGECACHE_ACCESS_LIST);
if (!empty($accessList)) {
- $result = [];
+ $ipsList = [];
$ips = explode(',', $accessList);
foreach ($ips as $ip) {
- $result[] = sprintf($tpl, trim($ip));
+ $ipsList[] = sprintf($tpl, trim($ip));
}
- return implode("\n", $result);
+ return implode("\n", $ipsList);
}
- return $result;
+
+ return '';
}
/**
diff --git a/app/code/Magento/PageCache/view/frontend/web/js/page-cache.js b/app/code/Magento/PageCache/view/frontend/web/js/page-cache.js
index 2e8a4769be10..735fe9a6cb23 100644
--- a/app/code/Magento/PageCache/view/frontend/web/js/page-cache.js
+++ b/app/code/Magento/PageCache/view/frontend/web/js/page-cache.js
@@ -7,7 +7,7 @@ define([
'jquery',
'domReady',
'consoleLogger',
- 'jquery/ui',
+ 'jquery-ui-modules/widget',
'mage/cookies'
], function ($, domReady, consoleLogger) {
'use strict';
diff --git a/app/code/Magento/Payment/view/frontend/web/js/cc-type.js b/app/code/Magento/Payment/view/frontend/web/js/cc-type.js
index e970c77888d4..d6997e536eaa 100644
--- a/app/code/Magento/Payment/view/frontend/web/js/cc-type.js
+++ b/app/code/Magento/Payment/view/frontend/web/js/cc-type.js
@@ -6,7 +6,7 @@
/* @api */
define([
'jquery',
- 'jquery/ui'
+ 'jquery-ui-modules/widget'
], function ($) {
'use strict';
diff --git a/app/code/Magento/Payment/view/frontend/web/js/transparent.js b/app/code/Magento/Payment/view/frontend/web/js/transparent.js
index d7c2aa368e7e..d5c51d2d100b 100644
--- a/app/code/Magento/Payment/view/frontend/web/js/transparent.js
+++ b/app/code/Magento/Payment/view/frontend/web/js/transparent.js
@@ -8,7 +8,7 @@ define([
'jquery',
'mage/template',
'Magento_Ui/js/modal/alert',
- 'jquery/ui',
+ 'jquery-ui-modules/widget',
'Magento_Payment/js/model/credit-card-validation/validator',
'Magento_Checkout/js/model/full-screen-loader'
], function ($, mageTemplate, alert, ui, validator, fullScreenLoader) {
diff --git a/app/code/Magento/Paypal/Model/Hostedpro.php b/app/code/Magento/Paypal/Model/Hostedpro.php
index 24701e084d7d..848a6e0247f5 100644
--- a/app/code/Magento/Paypal/Model/Hostedpro.php
+++ b/app/code/Magento/Paypal/Model/Hostedpro.php
@@ -140,6 +140,7 @@ public function __construct(
/**
* Return available CC types for gateway based on merchant country.
+ *
* We do not have to check the availability of card types.
*
* @return true
@@ -150,8 +151,9 @@ public function getAllowedCcTypes()
}
/**
- * Return merchant country code from config,
- * use default country if it not specified in General settings
+ * Return merchant country code from config
+ *
+ * Use default country if it not specified in General settings
*
* @return string
*/
@@ -274,7 +276,9 @@ protected function buildBasicRequest()
*/
public function getReturnUrl($storeId = null)
{
- return $this->getUrl('paypal/hostedpro/return', $storeId);
+ $payment = $this->getInfoInstance();
+ $urlRoute = $payment->getAdditionalInformation('return_url') ?? 'paypal/hostedpro/return';
+ return $this->getUrl($urlRoute, $storeId);
}
/**
@@ -296,7 +300,9 @@ public function getNotifyUrl($storeId = null)
*/
public function getCancelUrl($storeId = null)
{
- return $this->getUrl('paypal/hostedpro/cancel', $storeId);
+ $payment = $this->getInfoInstance();
+ $urlRoute = $payment->getAdditionalInformation('cancel_url') ?? 'paypal/hostedpro/cancel';
+ return $this->getUrl($urlRoute, $storeId);
}
/**
diff --git a/app/code/Magento/Paypal/Model/Payflowlink.php b/app/code/Magento/Paypal/Model/Payflowlink.php
index 47efd562570f..690e09ffd877 100644
--- a/app/code/Magento/Paypal/Model/Payflowlink.php
+++ b/app/code/Magento/Paypal/Model/Payflowlink.php
@@ -425,7 +425,6 @@ protected function _buildTokenRequest(\Magento\Sales\Model\Order\Payment $paymen
$request->setCreatesecuretoken('Y')
->setSecuretokenid($this->mathRandom->getUniqueHash())
->setTrxtype($this->_getTrxTokenType());
- $request = $this->updateRequestReturnUrls($request, $payment);
$order = $payment->getOrder();
$request->setAmt(sprintf('%.2F', $order->getBaseTotalDue()))
@@ -601,30 +600,4 @@ protected function _getCallbackUrl($actionName)
return $websiteUrl . 'paypal/' . $this->_callbackController . '/' . $actionName;
}
-
- /**
- * Update the redirect urls on the request if they are set on the payment
- *
- * @param \Magento\Paypal\Model\Payflow\Request $request
- * @param \Magento\Sales\Model\Order\Payment $payment
- * @return \Magento\Paypal\Model\Payflow\Request
- */
- private function updateRequestReturnUrls(
- \Magento\Paypal\Model\Payflow\Request $request,
- \Magento\Sales\Model\Order\Payment $payment
- ): \Magento\Paypal\Model\Payflow\Request {
- $paymentData = $payment->getAdditionalInformation();
-
- if (!empty($paymentData['cancel_url'])) {
- $request->setCancelurl($paymentData['cancel_url']);
- }
- if (!empty($paymentData['return_url'])) {
- $request->setReturnurl($paymentData['return_url']);
- }
- if (!empty($paymentData['error_url'])) {
- $request->setErrorurl($paymentData['error_url']);
- }
-
- return $request;
- }
}
diff --git a/app/code/Magento/Paypal/view/frontend/web/js/order-review.js b/app/code/Magento/Paypal/view/frontend/web/js/order-review.js
index 2155b52f4081..1deee1bd7659 100644
--- a/app/code/Magento/Paypal/view/frontend/web/js/order-review.js
+++ b/app/code/Magento/Paypal/view/frontend/web/js/order-review.js
@@ -6,7 +6,7 @@
define([
'jquery',
'Magento_Ui/js/modal/alert',
- 'jquery/ui',
+ 'jquery-ui-modules/widget',
'mage/translate',
'mage/mage',
'mage/validation'
diff --git a/app/code/Magento/Paypal/view/frontend/web/js/paypal-checkout.js b/app/code/Magento/Paypal/view/frontend/web/js/paypal-checkout.js
index 62c9fed0a7dd..6704540ec060 100644
--- a/app/code/Magento/Paypal/view/frontend/web/js/paypal-checkout.js
+++ b/app/code/Magento/Paypal/view/frontend/web/js/paypal-checkout.js
@@ -7,7 +7,7 @@ define([
'jquery',
'Magento_Ui/js/modal/confirm',
'Magento_Customer/js/customer-data',
- 'jquery/ui',
+ 'jquery-ui-modules/widget',
'mage/mage'
], function ($, confirm, customerData) {
'use strict';
diff --git a/app/code/Magento/PaypalGraphQl/Model/HostedProAdditionalDataProvider.php b/app/code/Magento/PaypalGraphQl/Model/HostedProAdditionalDataProvider.php
new file mode 100644
index 000000000000..3e41045f34b3
--- /dev/null
+++ b/app/code/Magento/PaypalGraphQl/Model/HostedProAdditionalDataProvider.php
@@ -0,0 +1,66 @@
+urlService = $urlService;
+ }
+
+ /**
+ * Returns additional data
+ *
+ * @param array $data
+ * @return array
+ * @throws GraphQlInputException
+ */
+ public function getData(array $data): array
+ {
+ $additionalData = $data[Config::METHOD_HOSTEDPRO] ?? [];
+ $this->validateUrlPaths($additionalData);
+
+ return $additionalData;
+ }
+
+ /**
+ * Validate redirect url paths
+ *
+ * @param array $data
+ * @throws GraphQlInputException
+ */
+ private function validateUrlPaths(array $data): void
+ {
+ $urlKeys = ['cancel_url', 'return_url'];
+
+ foreach ($urlKeys as $urlKey) {
+ if (isset($data[$urlKey])) {
+ if (!$this->urlService->isPath($data[$urlKey])) {
+ throw new GraphQlInputException(__('Invalid Url.'));
+ }
+ }
+ }
+ }
+}
diff --git a/app/code/Magento/PaypalGraphQl/Model/PayflowLinkAdditionalDataProvider.php b/app/code/Magento/PaypalGraphQl/Model/PayflowLinkAdditionalDataProvider.php
index 2bd10f474bfc..8ff7ce80ce49 100644
--- a/app/code/Magento/PaypalGraphQl/Model/PayflowLinkAdditionalDataProvider.php
+++ b/app/code/Magento/PaypalGraphQl/Model/PayflowLinkAdditionalDataProvider.php
@@ -8,30 +8,30 @@
namespace Magento\PaypalGraphQl\Model;
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
-use Magento\Framework\Url\Validator as UrlValidator;
use Magento\Paypal\Model\Config;
use Magento\QuoteGraphQl\Model\Cart\Payment\AdditionalDataProviderInterface;
+use Magento\PaypalGraphQl\Model\Resolver\Store\Url;
/**
- * Get payment additional data for Paypal Payflow Link payment
+ * Get payment additional data for Paypal Payflow Link payment method
*/
class PayflowLinkAdditionalDataProvider implements AdditionalDataProviderInterface
{
/**
- * @var UrlValidator
+ * @var Url
*/
- private $urlValidator;
+ private $urlService;
/**
- * @param UrlValidator $urlValidator
+ * @param Url $urlService
*/
- public function __construct(UrlValidator $urlValidator)
+ public function __construct(Url $urlService)
{
- $this->urlValidator = $urlValidator;
+ $this->urlService = $urlService;
}
/**
- * Returns additional data
+ * Return additional data from payflow_link paymentMethodInput
*
* @param array $data
* @return array
@@ -40,26 +40,25 @@ public function __construct(UrlValidator $urlValidator)
public function getData(array $data): array
{
$additionalData = $data[Config::METHOD_PAYFLOWLINK] ?? [];
- $this->validateUrls($additionalData);
+ $this->validatePathsInArray($additionalData);
return $additionalData;
}
/**
- * Validate redirect urls
+ * Validate paths in known keys of the additional data array
*
* @param array $data
* @throws GraphQlInputException
*/
- private function validateUrls(array $data): void
+ private function validatePathsInArray(array $data): void
{
$urlKeys = ['cancel_url', 'return_url', 'error_url'];
foreach ($urlKeys as $urlKey) {
if (isset($data[$urlKey])) {
- if (!$this->urlValidator->isValid($data[$urlKey])) {
- $errorMessage = $this->urlValidator->getMessages()['invalidUrl'] ?? "Invalid Url.";
- throw new GraphQlInputException(__($errorMessage));
+ if (!$this->urlService->isPath($data[$urlKey])) {
+ throw new GraphQlInputException(__('Invalid Url.'));
}
}
}
diff --git a/app/code/Magento/PaypalGraphQl/Model/Plugin/Cart/HostedPro/SetPaymentMethodOnCart.php b/app/code/Magento/PaypalGraphQl/Model/Plugin/Cart/HostedPro/SetPaymentMethodOnCart.php
new file mode 100644
index 000000000000..f3353afb7f5b
--- /dev/null
+++ b/app/code/Magento/PaypalGraphQl/Model/Plugin/Cart/HostedPro/SetPaymentMethodOnCart.php
@@ -0,0 +1,73 @@
+paymentRepository = $paymentRepository;
+ $this->additionalDataProviderPool = $additionalDataProviderPool;
+ }
+
+ /**
+ * Set redirect URL paths on payment additionalInformation
+ *
+ * @param \Magento\QuoteGraphQl\Model\Cart\SetPaymentMethodOnCart $subject
+ * @param mixed $result
+ * @param Quote $cart
+ * @param array $paymentData
+ * @return void
+ * @throws GraphQlInputException
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function afterExecute(
+ \Magento\QuoteGraphQl\Model\Cart\SetPaymentMethodOnCart $subject,
+ $result,
+ Quote $cart,
+ array $paymentData
+ ): void {
+ $paymentData = $this->additionalDataProviderPool->getData(Config::METHOD_HOSTEDPRO, $paymentData);
+
+ if (!empty($paymentData)) {
+ $urlKeys = ['cancel_url', 'return_url'];
+ $payment = $cart->getPayment();
+ foreach ($urlKeys as $urlKey) {
+ if (isset($paymentData[$urlKey])) {
+ $payment->setAdditionalInformation($urlKey, $paymentData[$urlKey]);
+ }
+ }
+ $payment->save();
+ }
+ }
+}
diff --git a/app/code/Magento/PaypalGraphQl/Model/Plugin/Payflowlink.php b/app/code/Magento/PaypalGraphQl/Model/Plugin/Payflowlink.php
new file mode 100644
index 000000000000..0c949341f952
--- /dev/null
+++ b/app/code/Magento/PaypalGraphQl/Model/Plugin/Payflowlink.php
@@ -0,0 +1,73 @@
+url = $url;
+ $this->storeRepository = $storeRepository;
+ }
+
+ /**
+ * Update redirect URLs in request with values stored in payment additionalInformation
+ *
+ * Relative URL paths are converted to absolute URLs
+ *
+ * @param \Magento\Paypal\Model\Payflowlink $subject
+ * @param DataObject $request
+ * @return mixed
+ */
+ public function afterBuildBasicRequest(
+ \Magento\Paypal\Model\Payflowlink $subject,
+ DataObject $request
+ ): DataObject {
+ $payment = $subject->getInfoInstance();
+ $storeId = $subject->getData('store');
+ $store = $this->storeRepository->getById($storeId);
+
+ $cancelUrl = $payment->getAdditionalInformation('cancel_url');
+ if ($cancelUrl) {
+ $request->setCancelurl($this->url->getUrlFromPath($cancelUrl, $store));
+ }
+
+ $returnUrl = $payment->getAdditionalInformation('return_url');
+ if ($returnUrl) {
+ $request->setReturnurl($this->url->getUrlFromPath($returnUrl, $store));
+ }
+
+ $errorUrl = $payment->getAdditionalInformation('error_url');
+ if ($errorUrl) {
+ $request->setErrorurl($this->url->getUrlFromPath($errorUrl, $store));
+ }
+
+ return $request;
+ }
+}
diff --git a/app/code/Magento/PaypalGraphQl/Model/Resolver/HostedProUrl.php b/app/code/Magento/PaypalGraphQl/Model/Resolver/HostedProUrl.php
new file mode 100644
index 000000000000..136944d9091c
--- /dev/null
+++ b/app/code/Magento/PaypalGraphQl/Model/Resolver/HostedProUrl.php
@@ -0,0 +1,98 @@
+maskedQuoteIdToQuoteId = $maskedQuoteIdToQuoteId;
+ $this->orderCollectionFactory = $orderCollectionFactory;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function resolve(
+ Field $field,
+ $context,
+ ResolveInfo $info,
+ array $value = null,
+ array $args = null
+ ) {
+ $customerId = $context->getUserId();
+ $storeId = (int)$context->getExtensionAttributes()->getStore()->getId();
+ $maskedCartId = $args['input']['cart_id'] ?? '';
+ try {
+ $cartId = $this->maskedQuoteIdToQuoteId->execute($maskedCartId);
+ } catch (NoSuchEntityException $e) {
+ throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e);
+ }
+
+ $order = $this->getOrderFromQuoteId($cartId, $customerId, $storeId);
+ $payment = $order->getPayment();
+ $paymentAdditionalInformation = $payment->getAdditionalInformation();
+
+ return [
+ 'secure_form_url' => $paymentAdditionalInformation['secure_form_url']
+ ];
+ }
+
+ /**
+ * Retrieve an order from its corresponding quote id
+ *
+ * @param int $quoteId
+ * @param int $customerId
+ * @param int $storeId
+ * @return Order
+ * @throws GraphQlNoSuchEntityException
+ */
+ private function getOrderFromQuoteId(int $quoteId, int $customerId, int $storeId): Order
+ {
+ $orderCollection = $this->orderCollectionFactory->create($customerId ?? null);
+ $orderCollection->addFilter(Order::QUOTE_ID, $quoteId);
+ $orderCollection->addFilter(Order::STATUS, Order::STATE_PENDING_PAYMENT);
+ $orderCollection->addFilter(Order::STORE_ID, $storeId);
+
+ if ($orderCollection->getTotalCount() !== 1) {
+ throw new GraphQlNoSuchEntityException(__('Could not find payment information for cart.'));
+ }
+ /** @var Order $order */
+ $order = $orderCollection->getFirstItem();
+
+ return $order;
+ }
+}
diff --git a/app/code/Magento/PaypalGraphQl/Model/Resolver/PayflowProToken.php b/app/code/Magento/PaypalGraphQl/Model/Resolver/PayflowProToken.php
index 409145ca9a96..c0256e580e4f 100644
--- a/app/code/Magento/PaypalGraphQl/Model/Resolver/PayflowProToken.php
+++ b/app/code/Magento/PaypalGraphQl/Model/Resolver/PayflowProToken.php
@@ -11,10 +11,12 @@
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
-use Magento\Framework\Url\Validator as UrlValidator;
use Magento\QuoteGraphQl\Model\Cart\GetCartForUser;
use Magento\Paypal\Model\Payflow\Service\Request\SecureToken;
use Magento\Framework\Exception\LocalizedException;
+use Magento\PaypalGraphQl\Model\Resolver\Store\Url;
+use Magento\Store\Api\Data\StoreInterface;
+use Magento\Framework\Validation\ValidationException;
/**
* Resolver for generating PayflowProToken
@@ -27,28 +29,28 @@ class PayflowProToken implements ResolverInterface
private $getCartForUser;
/**
- * @var UrlValidator
+ * @var SecureToken
*/
- private $urlValidator;
+ private $secureTokenService;
/**
- * @var SecureToken
+ * @var Url
*/
- private $secureTokenService;
+ private $urlService;
/**
* @param GetCartForUser $getCartForUser
- * @param UrlValidator $urlValidator
* @param SecureToken $secureTokenService
+ * @param Url $urlService
*/
public function __construct(
GetCartForUser $getCartForUser,
- UrlValidator $urlValidator,
- SecureToken $secureTokenService
+ SecureToken $secureTokenService,
+ Url $urlService
) {
$this->getCartForUser = $getCartForUser;
- $this->urlValidator = $urlValidator;
$this->secureTokenService = $secureTokenService;
+ $this->urlService = $urlService;
}
/**
@@ -65,11 +67,16 @@ public function resolve(
$urls = $args['input']['urls'] ?? null ;
$customerId = $context->getUserId();
- $storeId = (int)$context->getExtensionAttributes()->getStore()->getId();
+
+ /** @var StoreInterface $store */
+ $store = $context->getExtensionAttributes()->getStore();
+
+ $storeId = (int)$store->getId();
+
$cart = $this->getCartForUser->execute($cartId, $customerId, $storeId);
- if (!empty($args['input']['urls'])) {
- $this->validateUrls($args['input']['urls']);
+ if (!empty($urls)) {
+ $urls = $this->validateAndConvertPathsToUrls($urls, $store);
}
try {
@@ -88,20 +95,23 @@ public function resolve(
}
/**
- * Validate redirect Urls
+ * Validate and convert to redirect urls from given paths
*
- * @param array $urls
- * @return boolean
+ * @param string $paths
+ * @param StoreInterface $store
+ * @return array
* @throws GraphQlInputException
*/
- private function validateUrls(array $urls): bool
+ private function validateAndConvertPathsToUrls(array $paths, StoreInterface $store): array
{
- foreach ($urls as $url) {
- if (!$this->urlValidator->isValid($url)) {
- $errorMessage = $this->urlValidator->getMessages()['invalidUrl'] ?? "Invalid Url.";
- throw new GraphQlInputException(__($errorMessage));
+ $urls = [];
+ foreach ($paths as $key => $path) {
+ try {
+ $urls[$key] = $this->urlService->getUrlFromPath($path, $store);
+ } catch (ValidationException $e) {
+ throw new GraphQlInputException(__($e->getMessage()), $e);
}
}
- return true;
+ return $urls;
}
}
diff --git a/app/code/Magento/PaypalGraphQl/Model/Resolver/PaypalExpressToken.php b/app/code/Magento/PaypalGraphQl/Model/Resolver/PaypalExpressToken.php
index 89db082e715c..25ad6c7d3d68 100644
--- a/app/code/Magento/PaypalGraphQl/Model/Resolver/PaypalExpressToken.php
+++ b/app/code/Magento/PaypalGraphQl/Model/Resolver/PaypalExpressToken.php
@@ -12,11 +12,13 @@
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
-use Magento\Framework\Url\Validator as UrlValidator;
use Magento\Checkout\Helper\Data as CheckoutHelper;
use Magento\PaypalGraphQl\Model\Provider\Checkout as CheckoutProvider;
use Magento\PaypalGraphQl\Model\Provider\Config as ConfigProvider;
use Magento\QuoteGraphQl\Model\Cart\GetCartForUser;
+use Magento\PaypalGraphQl\Model\Resolver\Store\Url;
+use Magento\Store\Api\Data\StoreInterface;
+use Magento\Framework\Validation\ValidationException;
/**
* Resolver for generating Paypal token
@@ -39,34 +41,34 @@ class PaypalExpressToken implements ResolverInterface
private $checkoutProvider;
/**
- * @var UrlValidator
+ * @var CheckoutHelper
*/
- private $urlValidator;
+ private $checkoutHelper;
/**
- * @var CheckoutHelper
+ * @var Url
*/
- private $checkoutHelper;
+ private $urlService;
/**
* @param GetCartForUser $getCartForUser
* @param CheckoutProvider $checkoutProvider
* @param ConfigProvider $configProvider
- * @param UrlValidator $urlValidator
* @param CheckoutHelper $checkoutHelper
+ * @param Url $urlService
*/
public function __construct(
GetCartForUser $getCartForUser,
CheckoutProvider $checkoutProvider,
ConfigProvider $configProvider,
- UrlValidator $urlValidator,
- CheckoutHelper $checkoutHelper
+ CheckoutHelper $checkoutHelper,
+ Url $urlService
) {
$this->getCartForUser = $getCartForUser;
$this->checkoutProvider = $checkoutProvider;
$this->configProvider = $configProvider;
- $this->urlValidator = $urlValidator;
$this->checkoutHelper = $checkoutHelper;
+ $this->urlService = $urlService;
}
/**
@@ -85,7 +87,10 @@ public function resolve(
$usedExpressButton = isset($args['input']['express_button']) ? $args['input']['express_button'] : false;
$customerId = $context->getUserId();
- $storeId = (int)$context->getExtensionAttributes()->getStore()->getId();
+ /** @var StoreInterface $store */
+ $store = $context->getExtensionAttributes()->getStore();
+
+ $storeId = (int)$store->getId();
$cart = $this->getCartForUser->execute($cartId, $customerId, $storeId);
$config = $this->configProvider->getConfig($paymentCode);
$checkout = $this->checkoutProvider->getCheckout($config, $cart);
@@ -109,7 +114,7 @@ public function resolve(
}
if (!empty($args['input']['urls'])) {
- $this->validateUrls($args['input']['urls']);
+ $args['input']['urls'] = $this->validateAndConvertPathsToUrls($args['input']['urls'], $store);
}
$checkout->prepareGiropayUrls(
$args['input']['urls']['success_url'] ?? '',
@@ -137,20 +142,23 @@ public function resolve(
}
/**
- * Validate redirect Urls
+ * Validate and convert to redirect urls from given paths
*
- * @param array $urls
- * @return boolean
+ * @param string $paths
+ * @param StoreInterface $store
+ * @return array
* @throws GraphQlInputException
*/
- private function validateUrls(array $urls): bool
+ private function validateAndConvertPathsToUrls(array $paths, StoreInterface $store): array
{
- foreach ($urls as $url) {
- if (!$this->urlValidator->isValid($url)) {
- $errorMessage = $this->urlValidator->getMessages()['invalidUrl'] ?? "Invalid Url.";
- throw new GraphQlInputException(__($errorMessage));
+ $urls = [];
+ foreach ($paths as $key => $path) {
+ try {
+ $urls[$key] = $this->urlService->getUrlFromPath($path, $store);
+ } catch (ValidationException $e) {
+ throw new GraphQlInputException(__($e->getMessage()), $e);
}
}
- return true;
+ return $urls;
}
}
diff --git a/app/code/Magento/PaypalGraphQl/Model/Resolver/Store/Url.php b/app/code/Magento/PaypalGraphQl/Model/Resolver/Store/Url.php
new file mode 100644
index 000000000000..bd506dd8c763
--- /dev/null
+++ b/app/code/Magento/PaypalGraphQl/Model/Resolver/Store/Url.php
@@ -0,0 +1,164 @@
+urlValidator = $urlValidator;
+ $this->urlInterface = $urlInterface;
+ }
+
+ /**
+ * Validate path
+ *
+ * @param string $path
+ * @return bool
+ */
+ public function isPath(string $path): bool
+ {
+ $result = true;
+
+ if (empty($path)) {
+ $result = false;
+ } elseif ($path[0] == '/'
+ || $this->containsProtocolDelimiter($path)
+ || $this->containsDirectoryTraversal($path)
+ || $this->containsParametersDelimiter($path)
+ || $this->startsWithPortDelimiter($path)
+ || $this->isUrl($path)) {
+ $result = false;
+ }
+ return $result;
+ }
+
+ /**
+ * Validate url format
+ *
+ * @param array $url
+ * @return boolean
+ */
+ private function isUrl(string $url): bool
+ {
+ $result = false;
+ if ($this->urlValidator->isValid($url)) {
+ $result = true;
+ }
+ return $result;
+ }
+
+ /**
+ * Get full url with base path from a path
+ *
+ * @param string $path
+ * @param StoreInterface $store
+ * @return string
+ * @throws ValidationException
+ */
+ public function getUrlFromPath(string $path, StoreInterface $store): string
+ {
+ //if it's a url then don't proceed with further validation
+ if (!$this->isPath($path)) {
+ throw new ValidationException(__('Invalid Url.'));
+ }
+
+ $params = ["_secure" => $store->isCurrentlySecure()];
+ $this->urlInterface->setScope($store);
+
+ $baseUrl = $this->urlInterface->getBaseUrl($params);
+ $resultUrl = $this->urlInterface->getUrl($path, $params);
+
+ // validate the resulting url
+ if (substr($resultUrl, 0, strlen($baseUrl)) != $baseUrl
+ || $path == $resultUrl
+ || $resultUrl == $baseUrl
+ || !$this->isUrl($resultUrl)
+ ) {
+ throw new ValidationException(__('Invalid Url.'));
+ }
+
+ return $resultUrl;
+ }
+
+ /**
+ * Validate if url contains protocol delimiter
+ *
+ * @param array $url
+ * @return boolean
+ */
+ private function containsProtocolDelimiter(string $url): bool
+ {
+ if (strpos($url, '://') !== false) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Validate if url contains directory traversal
+ *
+ * @param array $url
+ * @return boolean
+ */
+ private function containsDirectoryTraversal(string $url): bool
+ {
+ if (strpos($url, '..') !== false) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Validate if url contains parameters delimiter
+ *
+ * @param array $url
+ * @return boolean
+ */
+ private function containsParametersDelimiter(string $url): bool
+ {
+ if (strpos($url, '?') !== false) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Validate if path starts with port delimiter
+ *
+ * @param array $path
+ * @return boolean
+ */
+ private function startsWithPortDelimiter(string $path): bool
+ {
+ if (preg_match('/^\:[0-9]+/', $path)) {
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/app/code/Magento/PaypalGraphQl/etc/graphql/di.xml b/app/code/Magento/PaypalGraphQl/etc/graphql/di.xml
index 8fa71bb75831..cd5d6e2062bb 100644
--- a/app/code/Magento/PaypalGraphQl/etc/graphql/di.xml
+++ b/app/code/Magento/PaypalGraphQl/etc/graphql/di.xml
@@ -9,6 +9,12 @@
+
+
+
+
+
+
@@ -41,7 +47,9 @@
- Magento\PaypalGraphQl\Model\PayflowLinkAdditionalDataProvider
+ - Magento\PaypalGraphQl\Model\PayflowLinkAdditionalDataProvider
- \Magento\PaypalGraphQl\Model\PayflowProAdditionalDataProvider
+ - \Magento\PaypalGraphQl\Model\HostedProAdditionalDataProvider
diff --git a/app/code/Magento/PaypalGraphQl/etc/graphql/events.xml b/app/code/Magento/PaypalGraphQl/etc/graphql/events.xml
index 165727fe63e4..41154e5ae06e 100644
--- a/app/code/Magento/PaypalGraphQl/etc/graphql/events.xml
+++ b/app/code/Magento/PaypalGraphQl/etc/graphql/events.xml
@@ -9,4 +9,7 @@
+
+
+
diff --git a/app/code/Magento/PaypalGraphQl/etc/schema.graphqls b/app/code/Magento/PaypalGraphQl/etc/schema.graphqls
index 33cbb7366873..2bacfb18054d 100644
--- a/app/code/Magento/PaypalGraphQl/etc/schema.graphqls
+++ b/app/code/Magento/PaypalGraphQl/etc/schema.graphqls
@@ -2,102 +2,117 @@
# See COPYING.txt for license details.
type Query {
- getPayflowLinkToken(input: PayflowLinkTokenInput!): PayflowLinkToken @resolver(class: "\\Magento\\PaypalGraphQl\\Model\\Resolver\\PayflowLinkToken") @doc(description: "Retrieve PayPal payment credentials for Payflow transaction.")
+ getPayflowLinkToken(input: PayflowLinkTokenInput!): PayflowLinkToken @resolver(class: "\\Magento\\PaypalGraphQl\\Model\\Resolver\\PayflowLinkToken") @doc(description: "Retrieve payment credentials for transaction. Use this query for Payflow Link and Payments Advanced payment methods.")
+ getHostedProUrl(input: HostedProUrlInput!): HostedProUrl @resolver(class: "\\Magento\\PaypalGraphQl\\Model\\Resolver\\HostedProUrl") @doc(description: "Retrieve secure PayPal url for Payments Pro Hosted Solution transaction.")
}
type Mutation {
- createPaypalExpressToken(input: PaypalExpressTokenInput!): PaypalExpressToken @resolver(class: "\\Magento\\PaypalGraphQl\\Model\\Resolver\\PaypalExpressToken") @doc(description:"Initiates a PayPal checkout transaction and receives a token.")
- createPayflowProToken(input: PayflowProTokenInput!): PayflowProToken @resolver(class: "\\Magento\\PaypalGraphQl\\Model\\Resolver\\PayflowProToken") @doc(description: "Initiates a PayFlowPro transaction and receives a token")
- handlePayflowProResponse(input: PayflowProResponseInput!): PayflowProResponseOutput @resolver(class: "\\Magento\\PaypalGraphQl\\Model\\Resolver\\PayflowProResponse") @doc(description: "Handles PayFlowPro response and saves payment in Quote")
+ createPaypalExpressToken(input: PaypalExpressTokenInput!): PaypalExpressToken @resolver(class: "\\Magento\\PaypalGraphQl\\Model\\Resolver\\PaypalExpressToken") @doc(description:"Initiates an Express Checkout transaction and receives a token. Use this mutation for Express Checkout and Payments Standard payment methods.")
+ createPayflowProToken(input: PayflowProTokenInput!): PayflowProToken @resolver(class: "\\Magento\\PaypalGraphQl\\Model\\Resolver\\PayflowProToken") @doc(description: "Initiates a transaction and receives a token. Use this mutation for Payflow Pro and Payments Pro payment methods")
+ handlePayflowProResponse(input: PayflowProResponseInput!): PayflowProResponseOutput @resolver(class: "\\Magento\\PaypalGraphQl\\Model\\Resolver\\PayflowProResponse") @doc(description: "Handles payment response and saves payment in Quote. Use this mutations for Payflow Pro and Payments Pro payment methods.")
}
-input PaypalExpressTokenInput @doc(description:"Defines the attributes required to receive a payment token from PayPal") {
+input PaypalExpressTokenInput @doc(description: "Defines the attributes required to receive a payment token for Express Checkout and Payments Standard payment methods.") {
cart_id: String! @doc(description:"The unique ID that identifies the customer's cart")
code: String! @doc(description:"Payment method code")
- urls: PaypalExpressUrlsInput! @doc(description:"A set of URLs that PayPal uses to respond to a token request")
+ urls: PaypalExpressUrlsInput! @doc(description:"A set of relative URLs that PayPal uses in response to various actions during the authorization process")
use_paypal_credit: Boolean @doc(description: "Indicates whether the buyer clicked the PayPal credit button. The default value is false")
express_button: Boolean @doc(description: "Indicates whether the buyer selected the quick checkout button. The default value is false")
}
-type PaypalExpressToken @doc(description: "Contains the token returned by PayPal and a set of URLs that allow the buyer to authorize payment and adjust checkout details") {
+type PaypalExpressToken @doc(description: "Contains the token returned by PayPal and a set of URLs that allow the buyer to authorize payment and adjust checkout details. Applies to Express Checkout and Payments Standard payment methods.") {
token: String @doc(description:"The token returned by PayPal")
paypal_urls: PaypalExpressUrlList @doc(description:"A set of URLs that allow the buyer to authorize payment and adjust checkout details")
}
-type PayflowLinkToken {
+type PayflowLinkToken @doc(description:"Contains information used to generate PayPal iframe for transaction. Applies to Payflow Link and Payments Advanced payment methods.") {
secure_token: String @doc(description:"Secure token generated by PayPal")
secure_token_id: String @doc(description:"Secure token ID generated by PayPal")
mode: PayflowLinkMode @doc(description:"Mode for Payflow transaction")
paypal_url: String @doc(description:"PayPal URL used for requesting Payflow form")
}
+type HostedProUrl @doc(desription:"Contains secure URL used for Payments Pro Hosted Solution payment method.") {
+ secure_form_url: String @doc(description:"Secure Url generated by PayPal")
+}
+
+input HostedProUrlInput @doc(description:"The required input to request the secure URL for Payments Pro Hosted Solution payment."){
+ cart_id: String! @doc(description:"The unique ID that identifies the customer's cart")
+}
+
input PaymentMethodInput {
- paypal_express: PaypalExpressInput @doc(description:"Required input for PayPal Express Checkout payments")
- payflow_express: PayflowExpressInput @doc(description:"Required input for PayPal Payflow Express Checkout payments")
- payflow_link: PayflowLinkAdditionalDataInput @doc(description:"Required input for PayPal Payflow Link payments")
- payflowpro: PayflowProInput @doc(description: "Required input type for Paypal payflow pro payments")
+ paypal_express: PaypalExpressInput @doc(description:"Required input for Express Checkout and Payments Standard payments")
+ payflow_express: PayflowExpressInput @doc(description:"Required input for Payflow Express Checkout payments")
+ payflow_link: PayflowLinkInput @doc(description:"Required input for PayPal Payflow Link and Payments Advanced payments")
+ payflowpro: PayflowProInput @doc(description: "Required input type for PayPal Payflow Pro and Payment Pro payments")
+ hosted_pro: HostedProInput @doc(description:"Required input for PayPal Hosted pro payments")
+}
+
+input HostedProInput @doc(description:"A set of relative URLs that PayPal will use in response to various actions during the authorization process. Magento prepends the base URL to this value to create a full URL. For example, if the full URL is https://www.example.com/path/to/page.html, the relative URL is path/to/page.html. Use this input for Payments Pro Hosted Solution payment method.") {
+ return_url: String! @doc(description:"The relative URL of the final confirmation page that PayPal will redirect to upon payment success. If the full URL to this page is https://www.example.com/paypal/action/return.html, the relative URL is paypal/action/return.html.")
+ cancel_url: String! @doc(description:"The relative URL of the page that PayPal will redirect to when the buyer cancels the transaction in order to choose a different payment method. If the full URL to this page is https://www.example.com/paypal/action/cancel.html, the relative URL is paypal/action/cancel.html.")
}
-input PaypalExpressInput @doc(description:"Required input for PayPal Express Checkout payments") {
+input PaypalExpressInput @doc(description:"Required input for Express Checkout and Payments Standard payments") {
payer_id: String! @doc(description:"The unique ID of the PayPal user")
token: String! @doc(description:"The token returned by the createPaypalExpressToken mutation")
}
-input PayflowExpressInput @doc(description:"Required input for PayPal Payflow Express Checkout payments") {
+input PayflowExpressInput @doc(description:"Required input for Payflow Express Checkout payments") {
payer_id: String! @doc(description:"The unique ID of the PayPal user")
token: String! @doc(description:"The token returned by the createPaypalExpressToken mutation")
}
-input PaypalExpressUrlsInput @doc(description:"A set of URLs that PayPal uses to respond to a token request") {
- return_url: String! @doc(description:"The URL of the final review page on your website where the buyer confirms the order and payment")
- cancel_url: String! @doc(description:"The URL of the original page on your website where the buyer initially chose PayPal as a payment type")
- success_url: String @doc(description:"The URL to redirect upon success. Not applicable to most PayPal solutions")
- pending_url: String @doc(description:"The URL to redirect for a pending transactions. Not applicable to most PayPal solutions")
+input PaypalExpressUrlsInput @doc(description:"A set of relative URLs that PayPal will use in response to various actions during the authorization process. Magento prepends the base URL to this value to create a full URL. For example, if the full URL is https://www.example.com/path/to/page.html, the relative URL is path/to/page.html. Use this input for Express Checkout and Payments Standard payment methods.") {
+ return_url: String! @doc(description:"The relative URL of the final confirmation page that PayPal will redirect to upon payment success. If the full URL to this page is https://www.example.com/paypal/action/return.html, the relative URL is paypal/action/return.html.")
+ cancel_url: String! @doc(description:"The relative URL of the page that PayPal will redirect to when the buyer cancels the transaction in order to choose a different payment method. If the full URL to this page is https://www.example.com/paypal/action/cancel.html, the relative URL is paypal/action/cancel.html.")
+ success_url: String @doc(description:"The relative URL of the order confirmation page that PayPal will redirect to when the payment is successful and additional confirmation is not needed. Not applicable to most PayPal solutions. If the full URL to this page is https://www.example.com/paypal/action/success.html, the relative URL is paypal/action/success.html.")
+ pending_url: String @doc(description:"The relative URL of the page that PayPal will redirect to when the payment has been put on hold for additional review. This condition mostly applies to ACH transactions, and is not applicable to most PayPal solutions. If the full URL to this page is https://www.example.com/paypal/action/success_pending.html, the relative URL is paypal/action/success_pending.html. ")
}
-type PaypalExpressUrlList @doc(description:"A set of URLs that allow the buyer to authorize payment and adjust checkout details") {
+type PaypalExpressUrlList @doc(description:"A set of URLs that allow the buyer to authorize payment and adjust checkout details for Express Checkout and Payments Standard transactions.") {
start: String @doc(description:"The URL to the PayPal login page")
edit: String @doc(description:"The PayPal URL that allows the buyer to edit their checkout details")
}
-input PayflowLinkAdditionalDataInput {
- return_url: String! @doc(description:"The URL PayPal will redirect back to upon payment success")
- cancel_url: String! @doc(description:"The URL PayPal will redirect back to upon payment cancellation")
- error_url: String! @doc(description:"The URL PayPal will redirect back to upon payment error")
+input PayflowLinkInput @doc(description:"A set of relative URLs that PayPal will use in response to various actions during the authorization process. Magento prepends the base URL to this value to create a full URL. For example, if the full URL is https://www.example.com/path/to/page.html, the relative URL is path/to/page.html. Use this input for Payflow Link and Payments Advanced payment methods.") {
+ return_url: String! @doc(description:"The relative URL of the order confirmation page that PayPal will redirect to when the payment is successful and additional confirmation is not needed. If the full URL to this page is https://www.example.com/paypal/action/return.html, the relative URL is paypal/action/return.html.")
+ cancel_url: String! @doc(description:"The relative URL of the page that PayPal will redirect to when the buyer cancels the transaction in order to choose a different payment method. If the full URL to this page is https://www.example.com/paypal/action/cancel.html, the relative URL is paypal/action/cancel.html.")
+ error_url: String! @doc(description:"The relative URL of the transaction error page that PayPal will redirect to upon payment error. If the full URL to this page is https://www.example.com/paypal/action/error.html, the relative URL is paypal/action/error.html.")
}
-input PayflowLinkTokenInput {
+input PayflowLinkTokenInput @doc(description:"Input required to fetch payment token information for Payflow Link and Payments Advanced payment methods.") {
cart_id: String! @doc(description:"The unique ID that identifies the customer's cart")
}
-enum PayflowLinkMode @doc(description:"Mode for Payflow Link payment: TEST or LIVE") {
+enum PayflowLinkMode @doc(description:"Mode for payment: TEST or LIVE. Applies to Payflow Link and Payments Advanced payment methods.") {
TEST
LIVE
}
-input PayflowProTokenInput {
+input PayflowProTokenInput @doc(description:"Input required to fetch payment token information for Payflow Pro and Payments Pro payment methods."){
cart_id: String! @doc(description:"The unique ID that identifies the customer's cart")
- urls: PayflowProUrlInput! @doc(description:"URL that PayPal uses for callback.")
+ urls: PayflowProUrlInput! @doc(description:"A set of relative URLs that PayPal uses for callback.")
}
-input PayflowProInput {
+input PayflowProInput @doc(description:"Required input for Payflow Pro and Payments Pro payment methods.") {
cc_details: CreditCardDetailsInput! @doc(description: "Required input for credit card related information")
}
-input CreditCardDetailsInput {
+input CreditCardDetailsInput @doc(description:"Required fields for Payflow Pro and Payments Pro credit card payments") {
cc_type: String! @doc(description: "Credit card type")
cc_exp_year: Int! @doc(description: "Credit card expiration year")
cc_exp_month: Int! @doc(description: "Credit card expiration month")
cc_last_4: Int! @doc(description: "Last 4 digits of the credit card")
}
-input PayflowProUrlInput {
- return_url: String! @doc(description:"The URL of the final review page on your website where the buyer confirms the order and payment")
- cancel_url: String! @doc(description:"The URL of the original page on your website where the buyer initially chose PayPal as a payment type")
- error_url: String! @doc(description:"The URL of the page on your website where any error in the transaction is handled")
+input PayflowProUrlInput @doc(description:"A set of relative URLs that PayPal will use in response to various actions during the authorization process. Magento prepends the base URL to this value to create a full URL. For example, if the full URL is https://www.example.com/path/to/page.html, the relative URL is path/to/page.html. Use this input for Payflow Pro and Payment Pro payment methods.") {
+ return_url: String! @doc(description:"The relative URL of the final confirmation page that PayPal will redirect to upon payment success. If the full URL to this page is https://www.example.com/paypal/action/return.html, the relative URL is paypal/action/return.html.")
+ cancel_url: String! @doc(description:"The relative URL of the page that PayPal will redirect to when the buyer cancels the transaction in order to choose a different payment method. If the full URL to this page is https://www.example.com/paypal/action/cancel.html, the relative URL is paypal/action/cancel.html.")
+ error_url: String! @doc(description:"The relative URL of the transaction error page that PayPal will redirect to upon payment error. If the full URL to this page is https://www.example.com/paypal/action/error.html, the relative URL is paypal/action/error.html.")
}
-type PayflowProToken {
+type PayflowProToken @doc(description: "Contains the secure information used to authorize transaction. Applies to Payflow Pro and Payments Pro payment methods.") {
secure_token: String!
secure_token_id: String!
response_message: String!
@@ -105,7 +120,7 @@ type PayflowProToken {
result_code: Int!
}
-input PayflowProResponseInput {
+input PayflowProResponseInput @doc(description:"Input required to complete payment. Applies to Payflow Pro and Payments Pro payment methods.") {
cart_id: String!
paypal_payload: String!
}
diff --git a/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php b/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php
index 8c56aafa04be..1261a90b5843 100644
--- a/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php
+++ b/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php
@@ -6,6 +6,7 @@
namespace Magento\Persistent\Observer;
use Magento\Framework\Event\ObserverInterface;
+use Magento\Quote\Model\Quote;
/**
* Observer of expired session
@@ -68,6 +69,11 @@ class CheckExpirePersistentQuoteObserver implements ObserverInterface
*/
private $checkoutPagePath = 'checkout';
+ /**
+ * @var Quote
+ */
+ private $quote;
+
/**
* @param \Magento\Persistent\Helper\Session $persistentSession
* @param \Magento\Persistent\Helper\Data $persistentData
@@ -100,6 +106,8 @@ public function __construct(
*
* @param \Magento\Framework\Event\Observer $observer
* @return void
+ * @throws \Magento\Framework\Exception\LocalizedException
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
*/
public function execute(\Magento\Framework\Event\Observer $observer)
{
@@ -107,16 +115,21 @@ public function execute(\Magento\Framework\Event\Observer $observer)
return;
}
+ //clear persistent when persistent data is disabled
+ if ($this->isPersistentQuoteOutdated()) {
+ $this->_eventManager->dispatch('persistent_session_expired');
+ $this->quoteManager->expire();
+ $this->_checkoutSession->clearQuote();
+ return;
+ }
+
if ($this->_persistentData->isEnabled() &&
!$this->_persistentSession->isPersistent() &&
!$this->_customerSession->isLoggedIn() &&
$this->_checkoutSession->getQuoteId() &&
!$this->isRequestFromCheckoutPage($this->request) &&
// persistent session does not expire on onepage checkout page
- (
- $this->_checkoutSession->getQuote()->getIsPersistent() ||
- $this->_checkoutSession->getQuote()->getCustomerIsGuest()
- )
+ $this->isNeedToExpireSession()
) {
$this->_eventManager->dispatch('persistent_session_expired');
$this->quoteManager->expire();
@@ -124,6 +137,50 @@ public function execute(\Magento\Framework\Event\Observer $observer)
}
}
+ /**
+ * Checks if current quote marked as persistent and Persistence Functionality is disabled.
+ *
+ * @return bool
+ * @throws \Magento\Framework\Exception\LocalizedException
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
+ */
+ private function isPersistentQuoteOutdated(): bool
+ {
+ if ((!$this->_persistentData->isEnabled() || !$this->_persistentData->isShoppingCartPersist())
+ && !$this->_customerSession->isLoggedIn()
+ && $this->_checkoutSession->getQuoteId()) {
+ return (bool)$this->getQuote()->getIsPersistent();
+ }
+ return false;
+ }
+
+ /**
+ * Condition checker
+ *
+ * @return bool
+ * @throws \Magento\Framework\Exception\LocalizedException
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
+ */
+ private function isNeedToExpireSession(): bool
+ {
+ return $this->getQuote()->getIsPersistent() || $this->getQuote()->getCustomerIsGuest();
+ }
+
+ /**
+ * Getter for Quote with micro optimization
+ *
+ * @return Quote
+ * @throws \Magento\Framework\Exception\LocalizedException
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
+ */
+ private function getQuote(): Quote
+ {
+ if ($this->quote === null) {
+ $this->quote = $this->_checkoutSession->getQuote();
+ }
+ return $this->quote;
+ }
+
/**
* Check current request is coming from onepage checkout page.
*
diff --git a/app/code/Magento/Persistent/Test/Mftf/ActionGroup/StorefrontPersistentAssertCustomerWelcomeMessageActionGroup.xml b/app/code/Magento/Persistent/Test/Mftf/ActionGroup/StorefrontPersistentAssertCustomerWelcomeMessageActionGroup.xml
index 2e57c8f8e1ee..b081f5c3eb0c 100644
--- a/app/code/Magento/Persistent/Test/Mftf/ActionGroup/StorefrontPersistentAssertCustomerWelcomeMessageActionGroup.xml
+++ b/app/code/Magento/Persistent/Test/Mftf/ActionGroup/StorefrontPersistentAssertCustomerWelcomeMessageActionGroup.xml
@@ -20,4 +20,4 @@
-
\ No newline at end of file
+
diff --git a/app/code/Magento/Persistent/Test/Unit/Observer/CheckExpirePersistentQuoteObserverTest.php b/app/code/Magento/Persistent/Test/Unit/Observer/CheckExpirePersistentQuoteObserverTest.php
index b096dd2317a3..2c5b9dad48eb 100644
--- a/app/code/Magento/Persistent/Test/Unit/Observer/CheckExpirePersistentQuoteObserverTest.php
+++ b/app/code/Magento/Persistent/Test/Unit/Observer/CheckExpirePersistentQuoteObserverTest.php
@@ -116,7 +116,7 @@ public function testExecuteWhenPersistentIsNotEnabled()
->method('canProcess')
->with($this->observerMock)
->willReturn(true);
- $this->persistentHelperMock->expects($this->once())->method('isEnabled')->willReturn(false);
+ $this->persistentHelperMock->expects($this->exactly(2))->method('isEnabled')->willReturn(false);
$this->eventManagerMock->expects($this->never())->method('dispatch');
$this->model->execute($this->observerMock);
}
@@ -144,8 +144,13 @@ public function testExecuteWhenPersistentIsEnabled(
->method('canProcess')
->with($this->observerMock)
->willReturn(true);
- $this->persistentHelperMock->expects($this->once())->method('isEnabled')->willReturn(true);
- $this->sessionMock->expects($this->once())->method('isPersistent')->willReturn(false);
+ $this->persistentHelperMock->expects($this->atLeastOnce())
+ ->method('isEnabled')
+ ->willReturn(true);
+ $this->persistentHelperMock->expects($this->atLeastOnce())
+ ->method('isShoppingCartPersist')
+ ->willReturn(true);
+ $this->sessionMock->expects($this->atLeastOnce())->method('isPersistent')->willReturn(false);
$this->checkoutSessionMock
->method('getQuote')
->willReturn($this->quoteMock);
diff --git a/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js b/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js
index cd0f3b3d630a..acaf2afeb6c2 100644
--- a/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js
+++ b/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js
@@ -5,7 +5,7 @@
define([
'jquery',
- 'jquery/ui',
+ 'jquery-ui-modules/widget',
'catalogGallery',
'loadPlayer'
], function ($) {
diff --git a/app/code/Magento/ProductVideo/view/frontend/web/js/load-player.js b/app/code/Magento/ProductVideo/view/frontend/web/js/load-player.js
index 75a2c1d75da1..ede0d2019309 100644
--- a/app/code/Magento/ProductVideo/view/frontend/web/js/load-player.js
+++ b/app/code/Magento/ProductVideo/view/frontend/web/js/load-player.js
@@ -7,7 +7,10 @@
@version 0.0.1
@requires jQuery & jQuery UI
*/
-define(['jquery', 'jquery/ui'], function ($) {
+define([
+ 'jquery',
+ 'jquery-ui-modules/widget'
+], function ($) {
'use strict';
var videoRegister = {
diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php
index 6346acfcb9af..7053e365114d 100644
--- a/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php
+++ b/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php
@@ -11,7 +11,6 @@
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException;
-use Magento\Framework\Stdlib\ArrayManager;
use Magento\Quote\Model\Quote;
use Magento\QuoteGraphQl\Model\Cart\BuyRequest\BuyRequestBuilder;
@@ -87,7 +86,11 @@ public function execute(Quote $cart, array $cartItemData): void
*/
private function extractSku(array $cartItemData): string
{
- if (!isset($cartItemData['data']['sku']) || empty($cartItemData['data']['sku'])) {
+ // Need to keep this for configurable product and backward compatibility.
+ if (!empty($cartItemData['parent_sku'])) {
+ return (string)$cartItemData['parent_sku'];
+ }
+ if (empty($cartItemData['data']['sku'])) {
throw new GraphQlInputException(__('Missed "sku" in cart item data'));
}
return (string)$cartItemData['data']['sku'];
diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/Product/Downloads.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/Product/Downloads.php
index f2c03d0dc22a..8864609bb884 100644
--- a/app/code/Magento/Reports/Controller/Adminhtml/Report/Product/Downloads.php
+++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/Product/Downloads.php
@@ -1,13 +1,17 @@
getId();
}
+ $websiteIds[$index] = (int) $websiteIds[$index];
}
+
+ $websiteSelect = $this->getConnection()->select();
+ $websiteSelect->from(
+ $this->getTable($entityInfo['associations_table']),
+ [$entityInfo['rule_id_field']]
+ )->distinct(
+ true
+ )->where(
+ $this->getConnection()->quoteInto($entityInfo['entity_id_field'] . ' IN (?)', $websiteIds)
+ );
$this->getSelect()->join(
- ['website' => $this->getTable($entityInfo['associations_table'])],
- $this->getConnection()->quoteInto('website.' . $entityInfo['entity_id_field'] . ' IN (?)', $websiteIds)
- . ' AND main_table.' . $entityInfo['rule_id_field'] . ' = website.' . $entityInfo['rule_id_field'],
+ ['website' => $websiteSelect],
+ 'main_table.' . $entityInfo['rule_id_field'] . ' = website.' . $entityInfo['rule_id_field'],
[]
);
}
@@ -127,11 +138,11 @@ public function addIsActiveFilter($isActive = 1)
}
/**
- * Retrieve correspondent entity information (associations table name, columns names)
- * of rule's associated entity by specified entity type
+ * Retrieve correspondent entity information of rule's associated entity by specified entity type
*
- * @param string $entityType
+ * (associations table name, columns names)
*
+ * @param string $entityType
* @throws \Magento\Framework\Exception\LocalizedException
* @return array
*/
diff --git a/app/code/Magento/Rule/Test/Unit/Model/ResourceModel/Rule/Collection/AbstractCollectionTest.php b/app/code/Magento/Rule/Test/Unit/Model/ResourceModel/Rule/Collection/AbstractCollectionTest.php
deleted file mode 100644
index c4e7a591212c..000000000000
--- a/app/code/Magento/Rule/Test/Unit/Model/ResourceModel/Rule/Collection/AbstractCollectionTest.php
+++ /dev/null
@@ -1,200 +0,0 @@
-_entityFactoryMock = $this->createMock(\Magento\Framework\Data\Collection\EntityFactoryInterface::class);
- $this->_loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
- $this->_fetchStrategyMock = $this->createMock(
- \Magento\Framework\Data\Collection\Db\FetchStrategyInterface::class
- );
- $this->_managerMock = $this->createMock(\Magento\Framework\Event\ManagerInterface::class);
- $this->_db = $this->getMockForAbstractClass(
- \Magento\Framework\Model\ResourceModel\Db\AbstractDb::class,
- [],
- '',
- false,
- false,
- true,
- ['__sleep', '__wakeup', 'getTable']
- );
- $this->objectManagerHelper = new ObjectManagerHelper($this);
- $this->abstractCollection = $this->getMockForAbstractClass(
- \Magento\Rule\Model\ResourceModel\Rule\Collection\AbstractCollection::class,
- [
- 'entityFactory' => $this->_entityFactoryMock,
- 'logger' => $this->_loggerMock,
- 'fetchStrategy' => $this->_fetchStrategyMock,
- 'eventManager' => $this->_managerMock,
- null,
- $this->_db
- ],
- '',
- false,
- false,
- true,
- ['__sleep', '__wakeup', '_getAssociatedEntityInfo', 'getConnection', 'getSelect', 'getTable']
- );
- }
-
- /**
- * @return array
- */
- public function addWebsitesToResultDataProvider()
- {
- return [
- [null, true],
- [true, true],
- [false, false]
- ];
- }
-
- /**
- * @dataProvider addWebsitesToResultDataProvider
- */
- public function testAddWebsitesToResult($flag, $expectedResult)
- {
- $this->abstractCollection->addWebsitesToResult($flag);
- $this->assertEquals($expectedResult, $this->abstractCollection->getFlag('add_websites_to_result'));
- }
-
- protected function _prepareAddFilterStubs()
- {
- $entityInfo = [];
- $entityInfo['entity_id_field'] = 'entity_id';
- $entityInfo['rule_id_field'] = 'rule_id';
- $entityInfo['associations_table'] = 'assoc_table';
-
- $connection = $this->createMock(\Magento\Framework\DB\Adapter\AdapterInterface::class);
- $select = $this->createMock(\Magento\Framework\DB\Select::class);
- $collectionSelect = $this->createMock(\Magento\Framework\DB\Select::class);
-
- $connection->expects($this->any())
- ->method('select')
- ->will($this->returnValue($select));
-
- $select->expects($this->any())
- ->method('from')
- ->will($this->returnSelf());
-
- $select->expects($this->any())
- ->method('where')
- ->will($this->returnSelf());
-
- $this->abstractCollection->expects($this->any())
- ->method('getConnection')
- ->will($this->returnValue($connection));
-
- $this->_db->expects($this->any())
- ->method('getTable')
- ->will($this->returnArgument(0));
-
- $this->abstractCollection->expects($this->any())
- ->method('getSelect')
- ->will($this->returnValue($collectionSelect));
-
- $this->abstractCollection->expects($this->any())
- ->method('_getAssociatedEntityInfo')
- ->will($this->returnValue($entityInfo));
- }
-
- public function testAddWebsiteFilter()
- {
- $this->_prepareAddFilterStubs();
- $website = $this->createPartialMock(\Magento\Store\Model\Website::class, ['getId', '__sleep', '__wakeup']);
-
- $website->expects($this->any())
- ->method('getId')
- ->will($this->returnValue(1));
-
- $this->assertInstanceOf(
- \Magento\Rule\Model\ResourceModel\Rule\Collection\AbstractCollection::class,
- $this->abstractCollection->addWebsiteFilter($website)
- );
- }
-
- public function testAddWebsiteFilterArray()
- {
- $this->selectMock = $this->getMockBuilder(\Magento\Framework\DB\Select::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->connectionMock = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class)
- ->disableOriginalConstructor()
- ->getMockForAbstractClass();
- $this->connectionMock->expects($this->atLeastOnce())
- ->method('quoteInto')
- ->with($this->equalTo('website. IN (?)'), $this->equalTo(['2', '3']))
- ->willReturn(true);
-
- $this->abstractCollection->expects($this->atLeastOnce())->method('getSelect')->willReturn($this->selectMock);
- $this->abstractCollection->expects($this->atLeastOnce())->method('getConnection')
- ->willReturn($this->connectionMock);
-
- $this->assertInstanceOf(
- \Magento\Rule\Model\ResourceModel\Rule\Collection\AbstractCollection::class,
- $this->abstractCollection->addWebsiteFilter(['2', '3'])
- );
- }
-
- public function testAddFieldToFilter()
- {
- $this->_prepareAddFilterStubs();
- $result = $this->abstractCollection->addFieldToFilter('website_ids', []);
- $this->assertNotNull($result);
- }
-}
diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Adjustments.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Adjustments.php
index 50d29c195968..1210391f70dd 100644
--- a/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Adjustments.php
+++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Adjustments.php
@@ -111,4 +111,20 @@ public function getShippingLabel()
}
return $label;
}
+
+ /**
+ * Get update totals url.
+ *
+ * @return string
+ */
+ public function getUpdateTotalsUrl(): string
+ {
+ return $this->getUrl(
+ 'sales/*/updateQty',
+ [
+ 'order_id' => $this->getSource()->getOrderId(),
+ 'invoice_id' => $this->getRequest()->getParam('invoice_id', null),
+ ]
+ );
+ }
}
diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Items.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Items.php
index 65163f9ed5d8..389c29bedf4c 100644
--- a/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Items.php
+++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Items.php
@@ -56,7 +56,12 @@ protected function _prepareLayout()
$this->addChild(
'update_button',
\Magento\Backend\Block\Widget\Button::class,
- ['label' => __('Update Qty\'s'), 'class' => 'update-button', 'onclick' => $onclick]
+ ['label' => __('Update Qty\'s'), 'class' => 'update-button secondary', 'onclick' => $onclick]
+ );
+ $this->addChild(
+ 'update_totals_button',
+ \Magento\Backend\Block\Widget\Button::class,
+ ['label' => __('Update Totals'), 'class' => 'update-totals-button secondary', 'onclick' => $onclick]
);
if ($this->getCreditmemo()->canRefund()) {
@@ -176,6 +181,16 @@ public function getUpdateButtonHtml()
return $this->getChildHtml('update_button');
}
+ /**
+ * Get update totals button html
+ *
+ * @return string
+ */
+ public function getUpdateTotalsButtonHtml(): string
+ {
+ return $this->getChildHtml('update_totals_button');
+ }
+
/**
* Get update url
*
diff --git a/app/code/Magento/Sales/Model/Order/Email/SenderBuilder.php b/app/code/Magento/Sales/Model/Order/Email/SenderBuilder.php
index ed9e38822245..1ae5d7479952 100644
--- a/app/code/Magento/Sales/Model/Order/Email/SenderBuilder.php
+++ b/app/code/Magento/Sales/Model/Order/Email/SenderBuilder.php
@@ -85,11 +85,9 @@ public function sendCopyTo()
$copyTo = $this->identityContainer->getEmailCopyTo();
if (!empty($copyTo) && $this->identityContainer->getCopyMethod() == 'copy') {
+ $this->configureEmailTemplate();
foreach ($copyTo as $email) {
- $this->configureEmailTemplate();
-
$this->transportBuilder->addTo($email);
-
$transport = $this->transportBuilder->getTransport();
$transport->sendMessage();
}
diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminCreditMemoActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminCreditMemoActionGroup.xml
index 58c7752626c8..6eeee6064974 100644
--- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminCreditMemoActionGroup.xml
+++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminCreditMemoActionGroup.xml
@@ -37,4 +37,25 @@
-
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoTotalSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoTotalSection.xml
index ee8cf05e3d7c..27612cc079b1 100644
--- a/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoTotalSection.xml
+++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoTotalSection.xml
@@ -22,5 +22,6 @@
+
diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingCreditMemoUpdateTotalsTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingCreditMemoUpdateTotalsTest.xml
new file mode 100644
index 000000000000..8cd2b8ee60ed
--- /dev/null
+++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingCreditMemoUpdateTotalsTest.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items.phtml
index 31d3b281532d..31aefd8d2ca5 100644
--- a/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items.phtml
+++ b/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items.phtml
@@ -100,6 +100,7 @@
= $block->escapeHtml(__('Refund Totals')) ?>
= $block->getChildHtml('creditmemo_totals') ?>
+
= $block->getUpdateTotalsButtonHtml() ?>
':i=g.settings.video_template_callback?g.settings.video_template_callback(j):'
"}return i};return{dataToHtml:g}}),g("l",["8"],function(a){return a("tinymce.util.Promise")}),g("i",["k","l"],function(a,b){var c=function(a,c,d){var e={};return new b(function(b,f){var g=function(d){return d.html&&(e[a.source1]=d),b({url:a.source1,html:d.html?d.html:c(a)})};e[a.source1]?g(e[a.source1]):d({url:a.source1},g,f)})},d=function(a,c){return new b(function(b){b({html:c(a),url:a.source1})})},e=function(b){return function(c){return a.dataToHtml(b,c)}},f=function(a,b){var f=a.settings.media_url_resolver;return f?c(b,e(a),f):d(b,e(a))};return{getEmbedHtml:f}}),g("j",[],function(){var a=function(a,b){a.state.set("oldVal",a.value()),b.state.set("oldVal",b.value())},b=function(a,b){var c=a.find("#width")[0],d=a.find("#height")[0],e=a.find("#constrain")[0];c&&d&&e&&b(c,d,e.checked())},c=function(b,c,d){var e=b.state.get("oldVal"),f=c.state.get("oldVal"),g=b.value(),h=c.value();d&&e&&f&&g&&h&&(g!==e?(h=Math.round(g/e*h),isNaN(h)||c.value(h)):(g=Math.round(h/f*g),isNaN(g)||b.value(g))),a(b,c)},d=function(c){b(c,a)},e=function(a){b(a,c)},f=function(a){var b=function(){a(function(a){e(a)})};return{type:"container",label:"Dimensions",layout:"flex",align:"center",spacing:5,items:[{name:"width",type:"textbox",maxLength:5,size:5,onchange:b,ariaLabel:"Width"},{type:"label",text:"x"},{name:"height",type:"textbox",maxLength:5,size:5,onchange:b,ariaLabel:"Height"},{name:"constrain",type:"checkbox",checked:!0,text:"Constrain proportions"}]}};return{createUi:f,syncSize:d,updateSize:e}}),g("7",["g","h","6","i","f","3","d","j"],function(a,b,c,d,e,f,g,h){var i=g.ie&&g.ie<=8?"onChange":"onInput",j=function(a){return function(b){var c=b&&b.msg?"Media embed handler error: "+b.msg:"Media embed handler threw unknown error.";a.notificationManager.open({type:"error",text:c})}},k=function(a){var c=a.selection.getNode(),d=c.getAttribute("data-ephox-embed-iri");return d?{source1:d,"data-ephox-embed-iri":d,width:e.getMaxWidth(c),height:e.getMaxHeight(c)}:c.getAttribute("data-mce-object")?b.htmlToData(a.settings.media_scripts,a.serializer.serialize(c,{selection:!0})):{}},l=function(a){var b=a.selection.getNode();if(b.getAttribute("data-mce-object")||b.getAttribute("data-ephox-embed-iri"))return a.selection.getContent()},m=function(a,c){return function(d){var e=d.html,g=a.find("#embed")[0],i=f.extend(b.htmlToData(c.settings.media_scripts,e),{source1:d.url});a.fromJSON(i),g&&(g.value(e),h.updateSize(a))}},n=function(a,b){var c,d,e=a.dom.select("img[data-mce-object]");for(c=0;c
=0;d--)b[c]===e[d]&&e.splice(d,1);a.selection.select(e[0])},o=function(a,b){var c=a.dom.select("img[data-mce-object]");a.insertContent(b),n(a,c),a.nodeChanged()},p=function(a,b){var e=a.toJSON();e.embed=c.updateHtml(e.embed,e),e.embed?o(b,e.embed):d.getEmbedHtml(b,e).then(function(a){o(b,a.html)})["catch"](j(b))},q=function(a,b){f.each(b,function(b,c){a.find("#"+c).value(b)})},r=function(a){var e,g,n=[{name:"source1",type:"filepicker",filetype:"media",size:40,autofocus:!0,label:"Source",onpaste:function(){setTimeout(function(){d.getEmbedHtml(a,e.toJSON()).then(m(e,a))["catch"](j(a))},1)},onchange:function(b){d.getEmbedHtml(a,e.toJSON()).then(m(e,a))["catch"](j(a)),q(e,b.meta)},onbeforecall:function(a){a.meta=e.toJSON()}}],o=[],r=function(a){a(e),g=e.toJSON(),e.find("#embed").value(c.updateHtml(g.embed,g))};if(a.settings.media_alt_source!==!1&&o.push({name:"source2",type:"filepicker",filetype:"media",size:40,label:"Alternative source"}),a.settings.media_poster!==!1&&o.push({name:"poster",type:"filepicker",filetype:"image",size:40,label:"Poster"}),a.settings.media_dimensions!==!1){var s=h.createUi(r);n.push(s)}g=k(a);var t={id:"mcemediasource",type:"textbox",flex:1,name:"embed",value:l(a),multiline:!0,rows:5,label:"Source"},u=function(){g=f.extend({},b.htmlToData(a.settings.media_scripts,this.value())),this.parent().parent().fromJSON(g)};t[i]=u,e=a.windowManager.open({title:"Insert/edit media",data:g,bodyType:"tabpanel",body:[{title:"General",type:"form",items:n},{title:"Embed",type:"container",layout:"flex",direction:"column",align:"stretch",padding:10,spacing:10,items:[{type:"label",text:"Paste your embed code below:",forId:"mcemediasource"},t]},{title:"Advanced",type:"form",items:o}],onSubmit:function(){h.updateSize(e),p(e,a)}}),h.syncSize(e)};return{showDialog:r}}),g("0",["1","2","3","4","5","6","7"],function(a,b,c,d,e,f,g){var h=function(b){b.on("ResolveName",function(a){var b;1===a.target.nodeType&&(b=a.target.getAttribute("data-mce-object"))&&(a.name=b)}),b.on("preInit",function(){var f=b.schema.getSpecialElements();c.each("video audio iframe object".split(" "),function(a){f[a]=new RegExp(""+a+"[^>]*>","gi")});var g=b.schema.getBoolAttrs();c.each("webkitallowfullscreen mozallowfullscreen allowfullscreen".split(" "),function(a){g[a]={}}),b.parser.addNodeFilter("iframe,video,audio,object,embed,script",d.placeHolderConverter(b)),b.serializer.addAttributeFilter("data-mce-object",function(c,d){for(var f,g,h,i,j,k,l,m,n=c.length;n--;)if(f=c[n],f.parent){for(l=f.attr(d),g=new a(l,1),"audio"!==l&&"script"!==l&&(m=f.attr("class"),m&&m.indexOf("mce-preview-object")!==-1?g.attr({width:f.firstChild.attr("width"),height:f.firstChild.attr("height")}):g.attr({width:f.attr("width"),height:f.attr("height")})),g.attr({style:f.attr("style")}),i=f.attributes,h=i.length;h--;){var o=i[h].name;0===o.indexOf("data-mce-p-")&&g.attr(o.substr(11),i[h].value)}"script"===l&&g.attr("type","text/javascript"),j=f.attr("data-mce-html"),j&&(k=new a("#text",3),k.raw=!0,k.value=e.sanitize(b,unescape(j)),g.append(k)),f.replace(g)}})}),b.on("click keyup",function(){var a=b.selection.getNode();a&&b.dom.hasClass(a,"mce-preview-object")&&b.dom.getAttrib(a,"data-mce-selected")&&a.setAttribute("data-mce-selected","2")}),b.on("ObjectSelected",function(a){var b=a.target.getAttribute("data-mce-object");"audio"!==b&&"script"!==b||a.preventDefault()}),b.on("objectResized",function(a){var b,c=a.target;c.getAttribute("data-mce-object")&&(b=c.getAttribute("data-mce-html"),b&&(b=unescape(b),c.setAttribute("data-mce-html",escape(f.updateHtml(b,{width:a.width,height:a.height})))))}),this.showDialog=function(){g.showDialog(b)},b.addButton("media",{tooltip:"Insert/edit media",onclick:this.showDialog,stateSelector:["img[data-mce-object]","span[data-mce-object]","div[data-ephox-embed-iri]"]}),b.addMenuItem("media",{icon:"media",text:"Media",onclick:this.showDialog,context:"insert",prependToContext:!0}),b.on("setContent",function(){b.$("span.mce-preview-object").each(function(a,c){var d=b.$(c);0===d.find("span.mce-shim",c).length&&d.append('')})}),b.addCommand("mceMedia",this.showDialog)};return b.add("media",h),function(){}}),d("0")()}();
\ No newline at end of file
+!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),o=tinymce.util.Tools.resolve("tinymce.Env"),v=tinymce.util.Tools.resolve("tinymce.util.Tools"),w=function(e){return e.getParam("media_scripts")},b=function(e){return e.getParam("audio_template_callback")},y=function(e){return e.getParam("video_template_callback")},n=function(e){return e.getParam("media_live_embeds",!0)},t=function(e){return e.getParam("media_filter_html",!0)},s=function(e){return e.getParam("media_url_resolver")},m=function(e){return e.getParam("media_alt_source",!0)},d=function(e){return e.getParam("media_poster",!0)},h=function(e){return e.getParam("media_dimensions",!0)},p=tinymce.util.Tools.resolve("tinymce.html.SaxParser"),r=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),x=function(e,t){if(e)for(var r=0;r"):"application/x-shockwave-flash"===i.source1mime?(d='"):-1!==i.source1mime.indexOf("audio")?(u=i,(l=f)?l(u):'"):"script"===i.type?' ',k=a.settings.directionality?' dir="'+a.settings.directionality+'"':"";if(b=""+f+'"+a.getContent()+j+"",e)this.getEl("body").firstChild.src="data:text/html;charset=utf-8,"+encodeURIComponent(b);else{var l=this.getEl("body").firstChild.contentWindow.document;l.open(),l.write(b),l.close()}}})}),a.addButton("preview",{title:"Preview",cmd:"mcePreview"}),a.addMenuItem("preview",{text:"Preview",cmd:"mcePreview",context:"view"})}),function(){}}),d("0")()}();
\ No newline at end of file
+!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),r=tinymce.util.Tools.resolve("tinymce.Env"),c=function(e){return parseInt(e.getParam("plugin_preview_width","650"),10)},a=function(e){return parseInt(e.getParam("plugin_preview_height","500"),10)},s=function(e){return e.getParam("content_style","")},d=tinymce.util.Tools.resolve("tinymce.util.Tools"),l=function(t){var n="",i=t.dom.encode,e=s(t);n+='',e&&(n+='"),d.each(t.contentCSS,function(e){n+=''});var o=t.settings.body_id||"tinymce";-1!==o.indexOf("=")&&(o=(o=t.getParam("body_id","","hash"))[t.id]||o);var r=t.settings.body_class||"";-1!==r.indexOf("=")&&(r=(r=t.getParam("body_class","","hash"))[t.id]||"");var c=t.settings.directionality?' dir="'+t.settings.directionality+'"':"";return""+n+'"+t.getContent()+'