From aaa3efca23cb3f524c69dd75cfe80dcf91ae549b Mon Sep 17 00:00:00 2001
From: Bohdan Korablov <bkorablov@magento.com>
Date: Mon, 17 Oct 2016 16:53:34 +0300
Subject: [PATCH] MAGETWO-56317: [GitHub] -[2.1.0] underscore in site url
 breaks admin redirect - The page isn't redirecting properly #5809

---
 .../Config/Model/Config/Backend/Baseurl.php   |  44 ++++++-
 .../Model/Config/Backend/BaseurlTest.php      |  11 ++
 setup/config/di.config.php                    |   1 +
 setup/pub/magento/setup/web-configuration.js  |  26 +++-
 .../InstallStoreConfigurationCommand.php      |  47 +++++---
 .../src/Magento/Setup/Controller/UrlCheck.php |  42 +++++++
 .../InstallStoreConfigurationCommandTest.php  |  13 +-
 .../Test/Unit/Controller/UrlCheckTest.php     | 114 ++++++++++++++++++
 .../magento/setup/web-configuration.phtml     |   9 +-
 9 files changed, 285 insertions(+), 22 deletions(-)
 create mode 100644 setup/src/Magento/Setup/Controller/UrlCheck.php
 create mode 100644 setup/src/Magento/Setup/Test/Unit/Controller/UrlCheckTest.php

diff --git a/app/code/Magento/Config/Model/Config/Backend/Baseurl.php b/app/code/Magento/Config/Model/Config/Backend/Baseurl.php
index 09fd9977381..8a7a75d3d8d 100644
--- a/app/code/Magento/Config/Model/Config/Backend/Baseurl.php
+++ b/app/code/Magento/Config/Model/Config/Backend/Baseurl.php
@@ -12,6 +12,11 @@ class Baseurl extends \Magento\Framework\App\Config\Value
      */
     protected $_mergeService;
 
+    /**
+     * @var array
+     */
+    private $allowedSchemes = ['http', 'https'];
+
     /**
      * @param \Magento\Framework\Model\Context $context
      * @param \Magento\Framework\Registry $registry
@@ -185,6 +190,36 @@ class Baseurl extends \Magento\Framework\App\Config\Value
         }
     }
 
+    /**
+     * Check that URL contains allowed symbols
+     *
+     * @param string $value
+     * @return void
+     * @throws \Magento\Framework\Exception\LocalizedException
+     */
+    private function validateUrl($value) {
+        if (!filter_var($value, FILTER_VALIDATE_URL)) {
+            throw new \Magento\Framework\Exception\LocalizedException(
+                __('Domain Name can contain only letters, digits and hyphen.')
+            );
+        }
+    }
+
+    /**
+     * Check that scheme there is in list of allowed schemes
+     *
+     * @param string $value
+     * @return void
+     * @throws \Magento\Framework\Exception\LocalizedException
+     */
+    private function validateSchemes($value) {
+        if (!in_array($value, $this->allowedSchemes)) {
+            throw new \Magento\Framework\Exception\LocalizedException(
+                __('You can use only following schemes: %1', implode(', ', $this->allowedSchemes))
+            );
+        }
+    }
+
     /**
      * Whether the provided value can be considered as a fully qualified URL
      *
@@ -194,7 +229,14 @@ class Baseurl extends \Magento\Framework\App\Config\Value
     private function _isFullyQualifiedUrl($value)
     {
         $url = parse_url($value);
-        return isset($url['scheme']) && isset($url['host']) && preg_match('/\/$/', $value);
+
+        $result = isset($url['scheme']) && isset($url['host']) && preg_match('/\/$/', $value);
+        if ($result) {
+            $this->validateSchemes($url['scheme']);
+            $this->validateUrl($value);
+        }
+
+        return $result;
     }
 
     /**
diff --git a/dev/tests/integration/testsuite/Magento/Config/Model/Config/Backend/BaseurlTest.php b/dev/tests/integration/testsuite/Magento/Config/Model/Config/Backend/BaseurlTest.php
index 0ce30e5b298..5a2db8ccb22 100644
--- a/dev/tests/integration/testsuite/Magento/Config/Model/Config/Backend/BaseurlTest.php
+++ b/dev/tests/integration/testsuite/Magento/Config/Model/Config/Backend/BaseurlTest.php
@@ -95,33 +95,44 @@ class BaseurlTest extends \PHPUnit_Framework_TestCase
         $unsecurePlaceholder = '{{unsecure_base_url}}';
         $unsecureSuffix = '{{unsecure_base_url}}test/';
         $unsecureWrongSuffix = '{{unsecure_base_url}}test';
+        $unsecureWrongDomainName = 'http://example.com_test/';
         $securePlaceholder = '{{secure_base_url}}';
         $secureSuffix = '{{secure_base_url}}test/';
         $secureWrongSuffix = '{{secure_base_url}}test';
+        $secureWrongDomainName = 'https://example.com_test/';
 
         return [
             ['', 'not a valid URL'],
             ['', 'example.com'],
             ['', 'http://example.com'],
             ['', 'http://example.com/uri'],
+            ['', $unsecureWrongDomainName],
             [\Magento\Store\Model\Store::XML_PATH_UNSECURE_BASE_URL, ''],
             [\Magento\Store\Model\Store::XML_PATH_UNSECURE_BASE_URL, $baseSuffix],
             [\Magento\Store\Model\Store::XML_PATH_UNSECURE_BASE_URL, $unsecureSuffix],
             [\Magento\Store\Model\Store::XML_PATH_UNSECURE_BASE_URL, $unsecurePlaceholder],
+            [\Magento\Store\Model\Store::XML_PATH_UNSECURE_BASE_URL, $unsecureWrongDomainName],
             [\Magento\Store\Model\Store::XML_PATH_UNSECURE_BASE_LINK_URL, ''],
             [\Magento\Store\Model\Store::XML_PATH_UNSECURE_BASE_LINK_URL, $baseSuffix],
             [\Magento\Store\Model\Store::XML_PATH_UNSECURE_BASE_LINK_URL, $unsecureWrongSuffix],
+            [\Magento\Store\Model\Store::XML_PATH_UNSECURE_BASE_LINK_URL, $unsecureWrongDomainName],
             [\Magento\Store\Model\Store::XML_PATH_UNSECURE_BASE_MEDIA_URL, $unsecureWrongSuffix],
+            [\Magento\Store\Model\Store::XML_PATH_UNSECURE_BASE_MEDIA_URL, $unsecureWrongDomainName],
             [\Magento\Store\Model\Store::XML_PATH_UNSECURE_BASE_STATIC_URL, $unsecureWrongSuffix],
+            [\Magento\Store\Model\Store::XML_PATH_UNSECURE_BASE_STATIC_URL, $unsecureWrongDomainName],
             [\Magento\Store\Model\Store::XML_PATH_SECURE_BASE_URL, ''],
             [\Magento\Store\Model\Store::XML_PATH_SECURE_BASE_URL, $baseSuffix],
             [\Magento\Store\Model\Store::XML_PATH_SECURE_BASE_URL, $secureSuffix],
             [\Magento\Store\Model\Store::XML_PATH_SECURE_BASE_URL, $securePlaceholder],
+            [\Magento\Store\Model\Store::XML_PATH_SECURE_BASE_URL, $secureWrongDomainName],
             [\Magento\Store\Model\Store::XML_PATH_SECURE_BASE_LINK_URL, ''],
             [\Magento\Store\Model\Store::XML_PATH_SECURE_BASE_LINK_URL, $baseSuffix],
             [\Magento\Store\Model\Store::XML_PATH_SECURE_BASE_LINK_URL, $secureWrongSuffix],
+            [\Magento\Store\Model\Store::XML_PATH_SECURE_BASE_LINK_URL, $secureWrongDomainName],
             [\Magento\Store\Model\Store::XML_PATH_SECURE_BASE_MEDIA_URL, $secureWrongSuffix],
+            [\Magento\Store\Model\Store::XML_PATH_SECURE_BASE_MEDIA_URL, $secureWrongDomainName],
             [\Magento\Store\Model\Store::XML_PATH_SECURE_BASE_STATIC_URL, $secureWrongSuffix],
+            [\Magento\Store\Model\Store::XML_PATH_SECURE_BASE_STATIC_URL, $secureWrongDomainName],
         ];
     }
 }
diff --git a/setup/config/di.config.php b/setup/config/di.config.php
index 804f1846206..b0dcb452ccd 100644
--- a/setup/config/di.config.php
+++ b/setup/config/di.config.php
@@ -21,6 +21,7 @@ return [
             \Magento\Setup\Controller\Environment::class,
             \Magento\Setup\Controller\DependencyCheck::class,
             \Magento\Setup\Controller\DatabaseCheck::class,
+            \Magento\Setup\Controller\UrlCheck::class,
             \Magento\Setup\Controller\ValidateAdminCredentials::class,
             \Magento\Setup\Controller\AddDatabase::class,
             \Magento\Setup\Controller\WebConfiguration::class,
diff --git a/setup/pub/magento/setup/web-configuration.js b/setup/pub/magento/setup/web-configuration.js
index 03a0fc7845d..47458056b33 100644
--- a/setup/pub/magento/setup/web-configuration.js
+++ b/setup/pub/magento/setup/web-configuration.js
@@ -5,7 +5,7 @@
 
 'use strict';
 angular.module('web-configuration', ['ngStorage'])
-    .controller('webConfigurationController', ['$scope', '$state', '$localStorage', function ($scope, $state, $localStorage) {
+    .controller('webConfigurationController', ['$scope', '$state', '$localStorage', '$http', function ($scope, $state, $localStorage, $http) {
         $scope.config = {
             address: {
                 base_url: '',
@@ -119,4 +119,28 @@ angular.module('web-configuration', ['ngStorage'])
                 $scope.webconfig.submitted = false;
             }
         });
+
+        // Validate URL
+        $scope.validateUrl = function () {
+            if (!$scope.webconfig.submitted) {
+                $http.post('index.php/url-check', $scope.config)
+                    .success(function (data) {
+                        $scope.validateUrl.result = data;
+                        if ($scope.validateUrl.result.successUrl && $scope.validateUrl.result.successSecureUrl) {
+                            $scope.nextState();
+                        }
+                        if (!$scope.validateUrl.result.successUrl) {
+                            $scope.webconfig.submitted = true;
+                            $scope.webconfig.base_url.$setValidity('url', false);
+                        }
+                        if (!$scope.validateUrl.result.successSecureUrl) {
+                            $scope.webconfig.submitted = true;
+                            $scope.webconfig.https.$setValidity('url', false);
+                        }
+                    })
+                    .error(function (data) {
+                        $scope.validateUrl.failed = data;
+                    });
+            }
+        };
     }]);
diff --git a/setup/src/Magento/Setup/Console/Command/InstallStoreConfigurationCommand.php b/setup/src/Magento/Setup/Console/Command/InstallStoreConfigurationCommand.php
index 928fe1e3d4b..6ebd387d409 100644
--- a/setup/src/Magento/Setup/Console/Command/InstallStoreConfigurationCommand.php
+++ b/setup/src/Magento/Setup/Console/Command/InstallStoreConfigurationCommand.php
@@ -20,7 +20,6 @@ use Magento\Store\Model\Store;
 use Magento\Framework\Validator\Locale;
 use Magento\Framework\Validator\Timezone;
 use Magento\Framework\Validator\Currency;
-use Magento\Framework\Url\Validator;
 
 /**
  * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
@@ -180,15 +179,12 @@ class InstallStoreConfigurationCommand extends AbstractSetupCommand
             }
             switch ($key) {
                 case StoreConfigurationDataMapper::KEY_BASE_URL:
-                    /** @var Validator $url */
                     if (strcmp($value, '{{base_url}}') == 0) {
                         break;
                     }
-                    $url = $this->objectManager->get(\Magento\Framework\Url\Validator::class);
-                    if (!$url->isValid($value)) {
-                        $errorMsgs = $url->getMessages();
-                        $errors[] = '<error>' . 'Command option \'' . StoreConfigurationDataMapper::KEY_BASE_URL
-                            . '\': ' . $errorMsgs[Validator::INVALID_URL] .'</error>';
+                    $errorMsg = $this->validateUrl($value, StoreConfigurationDataMapper::KEY_BASE_URL);
+                    if ($errorMsg !== '') {
+                        $errors[] = $errorMsg;
                     }
                     break;
                 case StoreConfigurationDataMapper::KEY_LANGUAGE:
@@ -229,18 +225,11 @@ class InstallStoreConfigurationCommand extends AbstractSetupCommand
                     break;
                 case StoreConfigurationDataMapper::KEY_BASE_URL_SECURE:
                     try {
-                        /** @var Validator $url */
-                        $url = $this->objectManager->get(\Magento\Framework\Url\Validator::class);
-                        $errorMsgs = '';
-                        if (!$url->isValid($value)) {
-                            $errorMsgs = $url->getMessages();
-                            if (!empty($errorMsgs)) {
-                                $errors[] = '<error>' . 'Command option \''
-                                    . StoreConfigurationDataMapper::KEY_BASE_URL_SECURE
-                                    . '\': ' . $errorMsgs[Validator::INVALID_URL] .'</error>';
-                            }
+                        $errorMsg = $this->validateUrl($value, StoreConfigurationDataMapper::KEY_BASE_URL_SECURE);
+                        if ($errorMsg !== '') {
+                            $errors[] = $errorMsg;
                         }
-                        if (empty($errorMsgs) && strpos($value, 'https:') === false) {
+                        if (empty($errorMsg) && strpos($value, 'https:') === false) {
                             throw new LocalizedException(new \Magento\Framework\Phrase("Invalid secure URL."));
                         }
                     } catch (LocalizedException $e) {
@@ -310,4 +299,26 @@ class InstallStoreConfigurationCommand extends AbstractSetupCommand
         }
         return $errorMsg;
     }
+
+    /**
+     * Validate URL
+     *
+     * @param string $url
+     * @param string $option
+     * @return string
+     */
+    private function validateUrl($url, $option)
+    {
+        $errorMsg = '';
+
+        if (!filter_var($url, FILTER_VALIDATE_URL)) {
+            $errorMsg = sprintf(
+                '<error>Command option \'%s\': Invalid URL \'%s\'.</error>',
+                $option,
+                $url
+            );
+        }
+
+        return $errorMsg;
+    }
 }
diff --git a/setup/src/Magento/Setup/Controller/UrlCheck.php b/setup/src/Magento/Setup/Controller/UrlCheck.php
new file mode 100644
index 00000000000..f7bee654c18
--- /dev/null
+++ b/setup/src/Magento/Setup/Controller/UrlCheck.php
@@ -0,0 +1,42 @@
+<?php
+/**
+ * Copyright © 2016 Magento. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+namespace Magento\Setup\Controller;
+
+use Zend\Mvc\Controller\AbstractActionController;
+use Zend\View\Model\JsonModel;
+use Zend\Json\Json;
+
+class UrlCheck extends AbstractActionController
+{
+    /**
+     * Validate URL
+     *
+     * @return JsonModel
+     */
+    public function indexAction()
+    {
+        $params = Json::decode($this->getRequest()->getContent(), Json::TYPE_ARRAY);
+        $result = ['successUrl' => false, 'successSecureUrl' => true];
+
+        // Validating of Base URL
+        if (isset($params['address']['actual_base_url'])
+            && filter_var($params['address']['actual_base_url'], FILTER_VALIDATE_URL)
+        ) {
+            $result['successUrl'] = true;
+        }
+
+        // Validating of Secure Base URL
+        if (!empty($params['https']['admin']) || !empty($params['https']['front'])) {
+            if (!(isset($params['https']['text'])
+                && filter_var($params['https']['text'], FILTER_VALIDATE_URL))
+            ) {
+                $result['successSecureUrl'] = false;
+            }
+        }
+
+        return new JsonModel(array_merge($result));
+    }
+}
diff --git a/setup/src/Magento/Setup/Test/Unit/Console/Command/InstallStoreConfigurationCommandTest.php b/setup/src/Magento/Setup/Test/Unit/Console/Command/InstallStoreConfigurationCommandTest.php
index 6f5ab3a5a64..17ba8b11d55 100644
--- a/setup/src/Magento/Setup/Test/Unit/Console/Command/InstallStoreConfigurationCommandTest.php
+++ b/setup/src/Magento/Setup/Test/Unit/Console/Command/InstallStoreConfigurationCommandTest.php
@@ -155,7 +155,12 @@ class InstallStoreConfigurationCommandTest extends \PHPUnit_Framework_TestCase
         return [
             [
                 ['--' . StoreConfigurationDataMapper::KEY_BASE_URL => 'sampleUrl'],
-                'Command option \'' . StoreConfigurationDataMapper::KEY_BASE_URL . '\': Invalid URL.'
+                'Command option \'' . StoreConfigurationDataMapper::KEY_BASE_URL . '\': Invalid URL \'sampleUrl\'.'
+            ],
+            [
+                ['--' . StoreConfigurationDataMapper::KEY_BASE_URL => 'http://example.com_test'],
+                'Command option \'' . StoreConfigurationDataMapper::KEY_BASE_URL
+                    . '\': Invalid URL \'http://example.com_test\'.'
             ],
             [
                 ['--' . StoreConfigurationDataMapper::KEY_LANGUAGE => 'sampleLanguage'],
@@ -187,6 +192,11 @@ class InstallStoreConfigurationCommandTest extends \PHPUnit_Framework_TestCase
                 'Command option \'' . StoreConfigurationDataMapper::KEY_BASE_URL_SECURE
                 . '\': Invalid secure URL.'
             ],
+            [
+                ['--' . StoreConfigurationDataMapper::KEY_BASE_URL_SECURE => 'https://sample.com_test'],
+                'Command option \'' . StoreConfigurationDataMapper::KEY_BASE_URL_SECURE
+                . '\': Invalid URL \'https://sample.com_test\'.'
+            ],
             [
                 ['--' . StoreConfigurationDataMapper::KEY_IS_SECURE_ADMIN => 'invalidValue'],
                 'Command option \'' . StoreConfigurationDataMapper::KEY_IS_SECURE_ADMIN
@@ -197,6 +207,7 @@ class InstallStoreConfigurationCommandTest extends \PHPUnit_Framework_TestCase
                 'Command option \'' . StoreConfigurationDataMapper::KEY_ADMIN_USE_SECURITY_KEY
                 . '\': Invalid value. Possible values (0|1).'
             ],
+
         ];
     }
 }
diff --git a/setup/src/Magento/Setup/Test/Unit/Controller/UrlCheckTest.php b/setup/src/Magento/Setup/Test/Unit/Controller/UrlCheckTest.php
new file mode 100644
index 00000000000..bad4ce0f035
--- /dev/null
+++ b/setup/src/Magento/Setup/Test/Unit/Controller/UrlCheckTest.php
@@ -0,0 +1,114 @@
+<?php
+/**
+ * Copyright © 2016 Magento. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+namespace Magento\Setup\Test\Unit\Controller;
+
+use Magento\Setup\Controller\UrlCheck;
+use Zend\Stdlib\RequestInterface;
+use Zend\View\Model\JsonModel;
+use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper;
+
+class UrlCheckTest extends \PHPUnit_Framework_TestCase
+{
+    /**
+     * @param string $requestJson
+     * @param array $expectedResult
+     * @dataProvider indexActionDataProvider
+     */
+    public function testIndexAction($requestJson, $expectedResult)
+    {
+        /** @var ObjectManagerHelper $objectManagerHelper */
+        $objectManagerHelper = new ObjectManagerHelper($this);
+
+        /** @var RequestInterface|\PHPUnit_Framework_MockObject_MockObject $requestMock */
+        $requestMock = $this->getMockBuilder(RequestInterface::class)
+            ->getMockForAbstractClass();
+        $requestMock->expects($this->once())
+            ->method('getContent')
+            ->willReturn($requestJson);
+
+        $controller = $objectManagerHelper->getObject(UrlCheck::class);
+        $objectManagerHelper->setBackwardCompatibleProperty($controller, 'request', $requestMock);
+
+        $this->assertEquals(new JsonModel($expectedResult), $controller->indexAction());
+    }
+
+    /**
+     * @return array
+     */
+    public function indexActionDataProvider()
+    {
+        return [
+            [
+                'requestJson' => json_encode([
+                    'address' => [
+                        'actual_base_url' => 'http://localhost'
+                    ]
+                ]),
+                'expectedResult' => ['successUrl' => true, 'successSecureUrl' => true]
+            ],
+            [
+                'requestJson' => json_encode([
+                    'address' => [
+                        'actual_base_url' => 'http://localhost.com_test'
+                    ]
+                ]),
+                'expectedResult' => ['successUrl' => false, 'successSecureUrl' => true]
+            ],
+            [
+                'requestJson' => json_encode([
+                    'address' => [
+                        'actual_base_url' => 'http://localhost.com_test'
+                    ],
+                    'https' => [
+                        'admin' => false,
+                        'front' => false,
+                        'text' => ''
+                    ]
+                ]),
+                'expectedResult' => ['successUrl' => false, 'successSecureUrl' => true]
+            ],
+            [
+                'requestJson' => json_encode([
+                    'address' => [
+                        'actual_base_url' => 'http://localhost.com:8080'
+                    ],
+                    'https' => [
+                        'admin' => true,
+                        'front' => false,
+                        'text' => 'https://example.com.ua/'
+                    ]
+                ]),
+                'expectedResult' => ['successUrl' => true, 'successSecureUrl' => true]
+            ],
+            [
+                'requestJson' => json_encode([
+                    'address' => [
+                        'actual_base_url' => 'http://localhost.com:8080/folder_name/'
+                    ],
+                    'https' => [
+                        'admin' => false,
+                        'front' => true,
+                        'text' => 'https://example.com.ua/'
+                    ]
+                ]),
+                'expectedResult' => ['successUrl' => true, 'successSecureUrl' => true]
+            ],
+            [
+                'requestJson' => json_encode([
+                    'address' => [
+                        'actual_base_url' => 'http://localhost.com:8080/folder_name/'
+                    ],
+                    'https' => [
+                        'admin' => true,
+                        'front' => true,
+                        'text' => 'https://example.com.ua:8090/folder_name/'
+                    ]
+                ]),
+                'expectedResult' => ['successUrl' => true, 'successSecureUrl' => true]
+            ],
+        ];
+    }
+}
diff --git a/setup/view/magento/setup/web-configuration.phtml b/setup/view/magento/setup/web-configuration.phtml
index 35aa19484ed..ab2a00342b7 100644
--- a/setup/view/magento/setup/web-configuration.phtml
+++ b/setup/view/magento/setup/web-configuration.phtml
@@ -30,7 +30,7 @@ $hints = [
 <div class="nav-bar-outer-actions">
     <div class="outer-actions-inner-wrap">
         <div class="btn-wrap btn-wrap-triangle-right btn-wrap-next">
-            <button type="button" class="btn btn-prime" ng-click="nextState()" dis>Next</button>
+            <button type="button" class="btn btn-prime" ng-click="validateUrl()" dis>Next</button>
         </div>
         <div class="btn-wrap btn-wrap-triangle-left btn-wrap-prev">
             <button type="button" class="btn" ng-click="previousState()">Back</button>
@@ -40,6 +40,13 @@ $hints = [
 
 <h2 class="page-sub-title">{{$state.current.header}}</h2>
 
+<div
+    class="message message-error"
+    ng-show="validateUrl.failed !== undefined"
+>
+    <span class="message-text">{{validateUrl.failed}}</span>
+</div>
+
 <form
     name="webconfig"
     role="form"
-- 
GitLab