diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..5363627
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,15 @@
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 4
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.{less,css,yml,json}]
+indent_size = 2
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..8960d84
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,38 @@
+name: CI
+
+on:
+ push:
+ branches:
+ - '**'
+ tags-ignore:
+ - '*.*'
+ pull_request:
+
+jobs:
+ test:
+ name: "PHPUnit"
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ php-version: ['7.3', '7.4']
+ steps:
+ - name: Check out code into the workspace
+ uses: actions/checkout@v2
+ - name: Setup PHP ${{ matrix.php-version }}
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php-version }}
+ coverage: pcov
+ - name: Composer cache
+ uses: actions/cache@v2
+ with:
+ path: ${{ env.HOME }}/.composer/cache
+ key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
+ - name: Install dependencies
+ run: composer install -o
+ - name: Configure matchers
+ uses: mheap/phpunit-matcher-action@v1
+ - name: Run tests
+ run: composer run-script phpunit-ci
+ - name: Coverage
+ run: bash <(curl -s https://codecov.io/bash)
\ No newline at end of file
diff --git a/.github/workflows/code_quality.yml b/.github/workflows/code_quality.yml
new file mode 100644
index 0000000..c9f9b2a
--- /dev/null
+++ b/.github/workflows/code_quality.yml
@@ -0,0 +1,42 @@
+name: "Code Quality Check"
+
+on:
+ pull_request:
+ paths:
+ - "**.php"
+ - "phpcs.xml"
+ - ".github/workflows/code_quality.yml"
+
+jobs:
+ phpcs:
+ name: "PHP CodeSniffer"
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out code into the workspace
+ uses: actions/checkout@v2
+ - name: Run PHPCS
+ uses: chekalsky/phpcs-action@v1
+ phpmd:
+ name: "PHP MessDetector"
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out code into the workspace
+ uses: actions/checkout@v2
+ - name: Run PHPMD
+ uses: GeneaLabs/action-reviewdog-phpmd@1.0.0
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ level: 'warning'
+ reporter: github-pr-check
+ standard: './phpmd.xml'
+ target_directory: 'src'
+ phpstan:
+ name: PHPStan
+ runs-on: ubuntu-18.04
+ steps:
+ - name: Check out code into the workspace
+ uses: actions/checkout@v2
+ - name: Run PHPStan
+ uses: docker://oskarstark/phpstan-ga:0.12.88
+ with:
+ args: analyse src -c phpstan.neon --memory-limit=1G --no-progress
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..bdbce05
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,21 @@
+# Composer files.
+/vendor
+composer.lock
+composer.phar
+
+# Code Quality tools artifacts.
+coverage.xml
+test-report.xml
+phpunit.xml
+.php_cs.cache
+.phpunit.result.cache
+
+# Different environment-related files.
+.idea
+.DS_Store
+.settings
+.buildpath
+.project
+.swp
+/nbproject
+.env
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..ecbb4f7
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015-2021 RetailDriver LLC
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/README.md b/README.md
index 11776b3..4b5fe05 100644
--- a/README.md
+++ b/README.md
@@ -1 +1 @@
-# url-validator
+Validator for RetailCRM projects on Symfony
\ No newline at end of file
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..1a9cb35
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,53 @@
+{
+ "name": "retailcrm/validator",
+ "description": "Validator for symfony projects",
+ "type": "library",
+ "keywords": ["Validator", "RetailCRM"],
+ "homepage": "https://www.retailcrm.ru/",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "RetailCRM",
+ "email": "support@retailcrm.ru"
+ }
+ ],
+ "require": {
+ "php": ">=7.3",
+ "ext-json": "*",
+ "symfony/validator": "^5.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.5.7",
+ "squizlabs/php_codesniffer": "3.*",
+ "phpstan/phpstan": "^0.12.92",
+ "phpmd/phpmd": "^2.10"
+ },
+ "support": {
+ "email": "support@retailcrm.ru"
+ },
+ "autoload": {
+ "psr-4": { "RetailCrm\\": "src/" }
+ },
+ "extra": {
+ "branch-alias": {
+ "dev-master": "0.1-dev"
+ }
+ },
+ "config": {
+ "bin-dir": "vendor/bin",
+ "process-timeout": 600
+ },
+ "scripts": {
+ "phpunit": "./vendor/bin/phpunit -c phpunit.xml.dist --coverage-text",
+ "phpunit-ci": "@php -dpcov.enabled=1 -dpcov.directory=. -dpcov.exclude=\"~vendor~\" ./vendor/bin/phpunit --teamcity -c phpunit.xml.dist",
+ "phpmd": "./vendor/bin/phpmd src text ./phpmd.xml",
+ "phpcs": "./vendor/bin/phpcs -p src --runtime-set testVersion 7.3-8.0 && ./vendor/bin/phpcs -p tests --runtime-set testVersion 7.3-8.0 --warning-severity=0",
+ "phpstan": "./vendor/bin/phpstan analyse -c phpstan.neon src --memory-limit=-1",
+ "verify": [
+ "@phpcs",
+ "@phpmd",
+ "@phpstan",
+ "@phpunit"
+ ]
+ }
+}
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
new file mode 100644
index 0000000..2c3bdd9
--- /dev/null
+++ b/phpcs.xml.dist
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+ src/
+ tests/
+
\ No newline at end of file
diff --git a/phpmd.xml b/phpmd.xml
new file mode 100644
index 0000000..3cd0206
--- /dev/null
+++ b/phpmd.xml
@@ -0,0 +1,47 @@
+
+
+ Ruleset
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ tests/*
+
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
new file mode 100644
index 0000000..7f4adad
--- /dev/null
+++ b/phpstan-baseline.neon
@@ -0,0 +1,52 @@
+parameters:
+ ignoreErrors:
+ -
+ message: "#^Access to an undefined property Symfony\\\\Component\\\\Validator\\\\Constraint\\:\\:\\$authFail\\.$#"
+ count: 1
+ path: src/Validator/CrmUrlValidator.php
+
+ -
+ message: "#^Access to an undefined property Symfony\\\\Component\\\\Validator\\\\Constraint\\:\\:\\$domainFail\\.$#"
+ count: 1
+ path: src/Validator/CrmUrlValidator.php
+
+ -
+ message: "#^Access to an undefined property Symfony\\\\Component\\\\Validator\\\\Constraint\\:\\:\\$fragmentFail\\.$#"
+ count: 1
+ path: src/Validator/CrmUrlValidator.php
+
+ -
+ message: "#^Access to an undefined property Symfony\\\\Component\\\\Validator\\\\Constraint\\:\\:\\$getFileError\\.$#"
+ count: 1
+ path: src/Validator/CrmUrlValidator.php
+
+ -
+ message: "#^Access to an undefined property Symfony\\\\Component\\\\Validator\\\\Constraint\\:\\:\\$noValidUrl\\.$#"
+ count: 1
+ path: src/Validator/CrmUrlValidator.php
+
+ -
+ message: "#^Access to an undefined property Symfony\\\\Component\\\\Validator\\\\Constraint\\:\\:\\$noValidUrlHost\\.$#"
+ count: 1
+ path: src/Validator/CrmUrlValidator.php
+
+ -
+ message: "#^Access to an undefined property Symfony\\\\Component\\\\Validator\\\\Constraint\\:\\:\\$pathFail\\.$#"
+ count: 1
+ path: src/Validator/CrmUrlValidator.php
+
+ -
+ message: "#^Access to an undefined property Symfony\\\\Component\\\\Validator\\\\Constraint\\:\\:\\$portFail\\.$#"
+ count: 1
+ path: src/Validator/CrmUrlValidator.php
+
+ -
+ message: "#^Access to an undefined property Symfony\\\\Component\\\\Validator\\\\Constraint\\:\\:\\$queryFail\\.$#"
+ count: 1
+ path: src/Validator/CrmUrlValidator.php
+
+ -
+ message: "#^Access to an undefined property Symfony\\\\Component\\\\Validator\\\\Constraint\\:\\:\\$schemeFail\\.$#"
+ count: 1
+ path: src/Validator/CrmUrlValidator.php
+
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000..1475fdd
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,8 @@
+includes:
+ - phpstan-baseline.neon
+
+parameters:
+ level: 5
+ paths:
+ - src
+ - tests
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..97a7981
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,36 @@
+
+
+
+
+
+ src
+ dev
+
+
+
+
+
+
+
+ tests
+
+
+
+
+
+
diff --git a/src/Validator/CrmUrl.php b/src/Validator/CrmUrl.php
new file mode 100644
index 0000000..647f88a
--- /dev/null
+++ b/src/Validator/CrmUrl.php
@@ -0,0 +1,70 @@
+constraint = $constraint;
+ $filteredUrl = filter_var($value, FILTER_VALIDATE_URL);
+
+ if (false === $filteredUrl) {
+ $this->context->buildViolation($constraint->noValidUrl)->addViolation();
+
+ return;
+ }
+
+ $urlArray = parse_url($filteredUrl);
+
+ if ($this->checkUrlFormat($urlArray)) {
+ $mainDomain = $this->getMainDomain($urlArray['host']);
+ $existInCrm = $this->checkDomains(self::CRM_DOMAINS_URL, $mainDomain);
+ $existInBox = $this->checkDomains(self::BOX_DOMAINS_URL, $urlArray['host']);
+
+ if (false === $existInCrm && false === $existInBox) {
+ $this->context->buildViolation($constraint->domainFail)->addViolation();
+ }
+ }
+ }
+
+ /**
+ * @param array $crmUrl
+ *
+ * @return bool
+ */
+ private function checkUrlFormat(array $crmUrl): bool
+ {
+ $checkResult = true;
+ $checkAuth = $this->checkAuth($crmUrl);
+ $checkFragment = $this->checkFragment($crmUrl);
+ $checkHost = $this->checkHost($crmUrl);
+ $checkPath = $this->checkPath($crmUrl);
+ $checkPort = $this->checkPort($crmUrl);
+ $checkQuery = $this->checkQuery($crmUrl);
+ $checkScheme = $this->checkScheme($crmUrl);
+
+ if (
+ !$checkAuth
+ || !$checkFragment
+ || !$checkHost
+ || !$checkPath
+ || !$checkPort
+ || !$checkQuery
+ || !$checkScheme
+ ) {
+ $checkResult = false;
+ }
+
+ return $checkResult;
+ }
+
+ /**
+ * @param array $crmUrl
+ *
+ * @return bool
+ */
+ private function checkHost(array $crmUrl): bool
+ {
+ if (!isset($crmUrl['host'])) {
+ $this->context->buildViolation($this->constraint->noValidUrlHost)->addViolation();
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @param array $crmUrl
+ *
+ * @return bool
+ */
+ private function checkQuery(array $crmUrl): bool
+ {
+ if (isset($crmUrl['query']) && !empty($crmUrl['query'])) {
+ $this->context->buildViolation($this->constraint->queryFail)->addViolation();
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @param array $crmUrl
+ *
+ * @return bool
+ */
+ private function checkAuth(array $crmUrl): bool
+ {
+ if (
+ (isset($crmUrl['pass']) && !empty($crmUrl['pass']))
+ || (isset($crmUrl['user']) && !empty($crmUrl['user']))
+ ) {
+ $this->context->buildViolation($this->constraint->authFail)->addViolation();
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @param array $crmUrl
+ *
+ * @return bool
+ */
+ private function checkFragment(array $crmUrl): bool
+ {
+ if (isset($crmUrl['fragment']) && !empty($crmUrl['fragment'])) {
+ $this->context->buildViolation($this->constraint->fragmentFail)->addViolation();
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @param array $crmUrl
+ *
+ * @return bool
+ */
+ private function checkScheme(array $crmUrl): bool
+ {
+ if (isset($crmUrl['scheme']) && $crmUrl['scheme'] !== 'https') {
+ $this->context->buildViolation($this->constraint->schemeFail)->addViolation();
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @param array $crmUrl
+ *
+ * @return bool
+ */
+ private function checkPath(array $crmUrl): bool
+ {
+ if (isset($crmUrl['path']) && $crmUrl['path'] !== '/' && $crmUrl['path'] !== '') {
+ $this->context->buildViolation($this->constraint->pathFail)->addViolation();
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @param array $crmUrl
+ *
+ * @return bool
+ */
+ private function checkPort(array $crmUrl): bool
+ {
+ if (isset($crmUrl['port']) && !empty($crmUrl['port'])) {
+ $this->context->buildViolation($this->constraint->portFail)->addViolation();
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @param string $domainUrl
+ *
+ * @return array
+ */
+ private function getValidDomains(string $domainUrl): array
+ {
+ try {
+ $content = json_decode(file_get_contents($domainUrl), true, 512, JSON_THROW_ON_ERROR);
+
+ return array_column($content['domains'], 'domain');
+ } catch (JsonException $exception) {
+ $this->context->buildViolation($this->constraint->getFileError)->addViolation();
+
+ return [];
+ }
+ }
+
+ /**
+ * @param string $host
+ *
+ * @return string
+ */
+ private function getMainDomain(string $host): string
+ {
+ $hostArray = explode('.', $host);
+ unset($hostArray[0]);
+
+ return implode('.', $hostArray);
+ }
+
+ /**
+ * @param string $crmDomainsUrl
+ * @param string $domainHost
+ *
+ * @return bool
+ */
+ private function checkDomains(string $crmDomainsUrl, string $domainHost): bool
+ {
+ return in_array($domainHost, $this->getValidDomains($crmDomainsUrl), true);
+ }
+}
diff --git a/tests/Validator/CrmUrlTest.php b/tests/Validator/CrmUrlTest.php
new file mode 100644
index 0000000..32b2b26
--- /dev/null
+++ b/tests/Validator/CrmUrlTest.php
@@ -0,0 +1,20 @@
+getTargets());
+ }
+}
diff --git a/tests/Validator/CrmUrlValidatorTest.php b/tests/Validator/CrmUrlValidatorTest.php
new file mode 100644
index 0000000..68e25d7
--- /dev/null
+++ b/tests/Validator/CrmUrlValidatorTest.php
@@ -0,0 +1,138 @@
+setConstraint(new CrmUrl());
+ $validator->initialize($context);
+ $validator->validate($validCrm, new CrmUrl());
+
+ self::assertEmpty($context->getViolations());
+ }
+ }
+
+ public function testValidateFailed(): void
+ {
+ $failedUrls = [
+ [
+ 'url' => 'http://asd.retailcrm.ru',
+ 'errors' => ['Incorrect protocol. Only https is allowed.'],
+ ],
+ [
+ 'url' => 'https://test.retailcrm.pro:8080',
+ 'errors' => ['The port does not need to be specified.'],
+ ],
+ [
+ 'url' => 'https://raisa.retailcrm.ess',
+ 'errors' => ['An invalid domain is specified.'],
+ ],
+ [
+ 'url' => 'https://blabla.simlla.com',
+ 'errors' => ['An invalid domain is specified.'],
+ ],
+ [
+ 'url' => 'https:/blabla.simlachat.ru',
+ 'errors' => [
+ 'Incorrect URL.',
+ ],
+ ],
+ [
+ 'url' => 'htttps://blabla.ecomlogic.com',
+ 'errors' => ['Incorrect protocol. Only https is allowed.'],
+ ],
+ [
+ 'url' => 'https://blabla.ecomlogic.com/test',
+ 'errors' => ['The domain path must be empty.'],
+ ],
+ [
+ 'url' => 'htttps://blabla.eecomlogic.com/test',
+ 'errors' => [
+ 'The domain path must be empty.',
+ 'Incorrect protocol. Only https is allowed.',
+ ],
+ ],
+ [
+ 'url' => 'https://test:test@blabla.eecomlogic.com/test?test=test#fragment',
+ 'errors' => [
+ 'No need to provide authorization data.',
+ 'The fragment should be blank.',
+ 'The domain path must be empty.',
+ 'The query must be blank.',
+ ],
+ ],
+ ];
+
+ $translator = new class () implements TranslatorInterface, LocaleAwareInterface {
+ use TranslatorTrait;
+ };
+
+ $metadata = new LazyLoadingMetadataFactory();
+ $factory = new ExecutionContextFactory($translator);
+ $validator = new CrmUrlValidator();
+
+ foreach ($failedUrls as $failedUrl) {
+ $context = new ExecutionContext(
+ new RecursiveValidator($factory, $metadata, new ConstraintValidatorFactory()),
+ CrmUrl::class,
+ $translator
+ );
+ $context->setConstraint(new CrmUrl());
+ $validator->initialize($context);
+ $validator->validate($failedUrl['url'], new CrmUrl());
+
+ foreach ($failedUrl['errors'] as $key => $error) {
+ self::assertEquals($context->getViolations()->get($key)->getMessage(), $failedUrl['errors'][$key]);
+ }
+
+ self::assertCount($context->getViolations()->count(), $failedUrl['errors']);
+ }
+ }
+}
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
new file mode 100644
index 0000000..b3d9bbc
--- /dev/null
+++ b/tests/bootstrap.php
@@ -0,0 +1 @@
+