This commit is contained in:
Сергей Чазов 2021-08-16 14:12:25 +03:00 committed by GitHub
parent 4169bc254c
commit 59b034425f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 817 additions and 1 deletions

15
.editorconfig Normal file
View File

@ -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

38
.github/workflows/ci.yml vendored Normal file
View File

@ -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)

42
.github/workflows/code_quality.yml vendored Normal file
View File

@ -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

21
.gitignore vendored Normal file
View File

@ -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

21
LICENSE Normal file
View File

@ -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.

View File

@ -1 +1 @@
# url-validator
Validator for RetailCRM projects on Symfony

53
composer.json Normal file
View File

@ -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"
]
}
}

13
phpcs.xml.dist Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/squizlabs/php_codesniffer/phpcs.xsd">
<arg name="basepath" value="."/>
<arg name="cache" value=".php_cs.cache"/>
<arg name="colors"/>
<arg name="extensions" value="php"/>
<rule ref="PSR12"/>
<file>src/</file>
<file>tests/</file>
</ruleset>

47
phpmd.xml Normal file
View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<ruleset name="Ruleset"
xmlns="http://pmd.sf.net/ruleset/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0 http://pmd.sf.net/ruleset_xml_schema.xsd"
xsi:noNamespaceSchemaLocation="http://pmd.sf.net/ruleset_xml_schema.xsd">
<description>Ruleset</description>
<rule ref="rulesets/controversial.xml" />
<rule ref="rulesets/unusedcode.xml" />
<rule ref="rulesets/design.xml">
<exclude name="CouplingBetweenObjects" />
</rule>
<rule ref="rulesets/cleancode.xml">
<exclude name="StaticAccess" />
</rule>
<rule ref="rulesets/codesize.xml">
<exclude name="TooManyPublicMethods" />
<exclude name="TooManyFields" />
</rule>
<rule ref="rulesets/naming.xml">
<exclude name="ShortVariable" />
</rule>
<rule ref="rulesets/naming.xml/ShortVariable">
<properties>
<property name="minimum" value="2" />
</properties>
</rule>
<rule ref="rulesets/codesize.xml/TooManyPublicMethods">
<properties>
<property name="maxmethods" value="20" />
</properties>
</rule>
<rule ref="rulesets/codesize.xml/TooManyFields">
<properties>
<property name="maxfields" value="30" />
</properties>
</rule>
<rule ref="rulesets/design.xml/CouplingBetweenObjects">
<properties>
<property name="maximum" value="15" />
</properties>
</rule>
<exclude-pattern>tests/*</exclude-pattern>
</ruleset>

52
phpstan-baseline.neon Normal file
View File

@ -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

8
phpstan.neon Normal file
View File

@ -0,0 +1,8 @@
includes:
- phpstan-baseline.neon
parameters:
level: 5
paths:
- src
- tests

36
phpunit.xml.dist Normal file
View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- https://phpunit.de/manual/current/en/appendixes.configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
backupGlobals="false"
colors="false"
bootstrap="tests/bootstrap.php"
backupStaticAttributes="false"
convertErrorsToExceptions="true"
convertNoticesToExceptions="false"
convertWarningsToExceptions="false"
processIsolation="true"
stopOnError="false"
stopOnFailure="false"
stopOnIncomplete="false"
stopOnSkipped="false"
stopOnRisky="false"
>
<coverage>
<include>
<directory>src</directory>
<directory>dev</directory>
</include>
<report>
<clover outputFile="coverage.xml"/>
</report>
</coverage>
<testsuites>
<testsuite name="Project Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<logging>
<junit outputFile="test-report.xml"/>
</logging>
</phpunit>

70
src/Validator/CrmUrl.php Normal file
View File

@ -0,0 +1,70 @@
<?php
namespace RetailCrm\Validator;
use Symfony\Component\Validator\Constraint;
/**
* Class CrmUrl
*
* @Annotation
* @Target({"PROPERTY"})
* @package Retailcrm\Validator
*/
class CrmUrl extends Constraint
{
/**
* @var string
*/
public $schemeFail = 'Incorrect protocol. Only https is allowed.';
/**
* @var string
*/
public $pathFail = 'The domain path must be empty.';
/**
* @var string
*/
public $portFail = 'The port does not need to be specified.';
/**
* @var string
*/
public $domainFail = 'An invalid domain is specified.';
/**
* @var string
*/
public $noValidUrlHost = 'Incorrect Host URL.';
/**
* @var string
*/
public $noValidUrl = 'Incorrect URL.';
/**
* @var string
*/
public $queryFail = 'The query must be blank.';
/**
* @var string
*/
public $fragmentFail = 'The fragment should be blank.';
/**
* @var string
*/
public $authFail = 'No need to provide authorization data.';
/**
* @var string
*/
public $getFileError = 'Unable to obtain reference values.';
public function getTargets()
{
return self::PROPERTY_CONSTRAINT;
}
}

View File

@ -0,0 +1,241 @@
<?php
namespace RetailCrm\Validator;
use JsonException;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Class CrmUrlValidator
*
* @package Retailcrm\Validator
*/
class CrmUrlValidator extends ConstraintValidator
{
public const BOX_DOMAINS_URL = "https://infra-data.retailcrm.tech/box-domains.json";
public const CRM_DOMAINS_URL = "https://infra-data.retailcrm.tech/crm-domains.json";
/**
* @var \Symfony\Component\Validator\Constraint
*/
private $constraint;
/**
* Validate CRM URL
*
* @param mixed $value URL from form
* @param Constraint $constraint Restriction for validation
*/
public function validate($value, Constraint $constraint): void
{
$this->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);
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace RetailCrm\Tests\Validator;
use PHPUnit\Framework\TestCase;
use RetailCrm\Validator\CrmUrl;
/**
* Class CrmUrlTest
*
* @package RetailCrm\Tests\Validator
*/
class CrmUrlTest extends TestCase
{
public function testGetTargets(): void
{
$crmUrl = new CrmUrl();
self::assertEquals('property', $crmUrl->getTargets());
}
}

View File

@ -0,0 +1,138 @@
<?php
namespace RetailCrm\Tests\Validator;
use PHPUnit\Framework\TestCase;
use RetailCrm\Validator\CrmUrl;
use RetailCrm\Validator\CrmUrlValidator;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidatorFactory;
use Symfony\Component\Validator\Context\ExecutionContext;
use Symfony\Component\Validator\Context\ExecutionContextFactory;
use Symfony\Component\Validator\Mapping\Factory\LazyLoadingMetadataFactory;
use Symfony\Component\Validator\Validation;
use Symfony\Component\Validator\Validator\RecursiveValidator;
use Symfony\Contracts\Translation\LocaleAwareInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Contracts\Translation\TranslatorTrait;
class CrmUrlValidatorTest extends TestCase
{
public function testValidateSuccess(): void
{
$validCrms = [
'https://asd.retailcrm.ru',
'https://test.retailcrm.pro',
'https://raisa.retailcrm.es',
'https://blabla.simla.com',
'https://blabla.simlachat.com',
'https://blabla.simlachat.ru',
'https://blabla.ecomlogic.com',
'https://retailcrm.inventive.ru',
'https://crm.baucenter.ru',
'https://crm.holodilnik.ru',
'https://crm.eco.lanit.ru',
'https://ecom.inventive.ru',
'https://retailcrm.tvoydom.ru',
];
$translator = new class () implements TranslatorInterface, LocaleAwareInterface {
use TranslatorTrait;
};
$metadata = new LazyLoadingMetadataFactory();
$factory = new ExecutionContextFactory($translator);
$validator = new CrmUrlValidator();
foreach ($validCrms as $validCrm) {
$context = new ExecutionContext(
new RecursiveValidator($factory, $metadata, new ConstraintValidatorFactory()),
CrmUrl::class,
$translator
);
$context->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']);
}
}
}

1
tests/bootstrap.php Normal file
View File

@ -0,0 +1 @@
<?php