Skip to content

Commit 2e0a5df

Browse files
authored
feat: add ShopwareFilesChecker (#202)
1 parent 9fe7ddb commit 2e0a5df

13 files changed

Lines changed: 503 additions & 1 deletion

File tree

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Frosh\Tools\Controller;
4+
5+
use Shopware\Core\Kernel;
6+
use Symfony\Component\HttpFoundation\JsonResponse;
7+
use Symfony\Component\HttpFoundation\Request;
8+
use Symfony\Component\Routing\Annotation\Route;
9+
10+
#[Route(path: '/api/_action/frosh-tools', defaults: ['_routeScope' => ['api'], '_acl' => ['frosh_tools:read']])]
11+
class ShopwareFilesController
12+
{
13+
private const STATUS_OK = 0;
14+
private const STATUS_IGNORED_ALL = 1;
15+
private const STATUS_IGNORED_IN_PROJECT = 2;
16+
private bool $isPlatform;
17+
18+
public function __construct(
19+
private readonly string $shopwareVersion,
20+
private readonly string $projectDir,
21+
private readonly array $projectIgnoredFiles
22+
)
23+
{
24+
$this->isPlatform = !is_dir($this->projectDir . '/vendor/shopware/core') && is_dir($this->projectDir . '/src/Core');
25+
}
26+
27+
#[Route(path: '/shopware-files', name: 'api.frosh.tools.shopware-files', methods: ['GET'])]
28+
public function listShopwareFiles(): JsonResponse
29+
{
30+
if ($this->shopwareVersion === Kernel::SHOPWARE_FALLBACK_VERSION) {
31+
return new JsonResponse(['error' => 'Git version is not supported']);
32+
}
33+
34+
$url = sprintf('https://swagger.docs.fos.gg/version/%s/Files.xxhsums', $this->shopwareVersion);
35+
36+
$data = trim(@file_get_contents($url));
37+
38+
if (empty($data)) {
39+
return new JsonResponse(['error' => 'No file information for this Shopware version']);
40+
}
41+
42+
$invalidFiles = [];
43+
$allFilesAreOkay = true;
44+
45+
foreach (explode("\n", $data) as $row) {
46+
if ($this->isPlatform) {
47+
$row = preg_replace_callback('/vendor\/shopware\/(.)/', function ($matches) {
48+
return 'src/' . strtoupper($matches[1]);
49+
}, $row);
50+
}
51+
52+
[$expectedMd5Sum, $file] = explode(' ', trim($row));
53+
54+
$path = $this->projectDir . '/' . $file;
55+
56+
if (!is_file($path)) {
57+
continue;
58+
}
59+
60+
$xxhSum = hash_file('xxh64', $path);
61+
62+
$ignoredState = $this->isIgnoredFileHash($file);
63+
if ($ignoredState === self::STATUS_IGNORED_ALL) {
64+
continue;
65+
}
66+
67+
if ($xxhSum !== $expectedMd5Sum) {
68+
if ($ignoredState === self::STATUS_OK) {
69+
$allFilesAreOkay = false;
70+
}
71+
72+
$invalidFiles[] = [
73+
'name' => $file,
74+
'shopwareUrl' => $this->getShopwareUrl($file),
75+
'expected' => $ignoredState === self::STATUS_IGNORED_IN_PROJECT,
76+
];
77+
}
78+
79+
//WE SHOULD STOP HERE, WHILE THERE MIGHT BE ANY BIG PROBLEM!
80+
if (count($invalidFiles) > 100) {
81+
break;
82+
}
83+
}
84+
85+
if ($allFilesAreOkay) {
86+
return new JsonResponse(['ok' => true, 'files' => $invalidFiles]);
87+
}
88+
89+
return new JsonResponse(['ok' => false, 'files' => $invalidFiles]);
90+
}
91+
92+
#[Route(path: '/file-contents', name: 'api.frosh.tools.file-contents', methods: ['GET'])]
93+
public function getFileContents(Request $request): JsonResponse
94+
{
95+
if ($this->shopwareVersion === Kernel::SHOPWARE_FALLBACK_VERSION) {
96+
return new JsonResponse(['error' => 'Git version is not supported']);
97+
}
98+
99+
$file = $request->query->get('file');
100+
if (!$file) {
101+
return new JsonResponse(['error' => 'no file provided']);
102+
}
103+
104+
$path = realpath($this->projectDir . '/' . $file);
105+
if ($path === false || !str_starts_with($path, $this->projectDir) || !is_file($path)) {
106+
return new JsonResponse(['error' => 'File is invalid']);
107+
}
108+
109+
return new JsonResponse([
110+
'name' => $file,
111+
'shopwareUrl' => $this->getShopwareUrl($file),
112+
'content' => file_get_contents($path),
113+
'originalContent' => $this->getOriginalFileContent($file),
114+
]);
115+
}
116+
117+
#[Route(path: '/shopware-file/restore', name: 'api.frosh.tools.shopware-file.restore', methods: ['GET'])]
118+
public function restoreShopwareFile(Request $request): JsonResponse
119+
{
120+
if ($this->shopwareVersion === Kernel::SHOPWARE_FALLBACK_VERSION) {
121+
return new JsonResponse(['error' => 'Git version is not supported']);
122+
}
123+
124+
$file = $request->query->get('file');
125+
if (!$file) {
126+
return new JsonResponse(['error' => 'no file provided']);
127+
}
128+
129+
$path = realpath($this->projectDir . '/' . $file);
130+
if ($path === false || !str_starts_with($path, $this->projectDir) || !is_file($path)) {
131+
return new JsonResponse(['error' => 'File is invalid']);
132+
}
133+
134+
$content = $this->getOriginalFileContent($file);
135+
if ($content === null || $content === '') {
136+
return new JsonResponse(['error' => 'File would be empty!']);
137+
}
138+
139+
file_put_contents($path, $content);
140+
141+
if (\function_exists('opcache_reset')) {
142+
opcache_reset();
143+
}
144+
145+
return new JsonResponse(['status' => sprintf('File at "%s" has been restored', $file)]);
146+
}
147+
148+
private function getShopwareUrl(string $name): ?string
149+
{
150+
if ($this->isPlatform) {
151+
$name = preg_replace('/^src\//', '', $name);
152+
} else {
153+
$name = preg_replace('/^vendor\/shopware\//', '', $name);
154+
}
155+
156+
$pathParts = \explode('/', $name);
157+
$repo = $pathParts[0];
158+
array_shift($pathParts);
159+
160+
return 'https://github.com/shopware/' .
161+
$repo .
162+
'/blob/v' .
163+
$this->shopwareVersion .
164+
'/' .
165+
\implode('/', $pathParts);
166+
}
167+
168+
private function getOriginalFileContent(string $name): ?string
169+
{
170+
return @file_get_contents($this->getShopwareUrl($name) . '?raw=true') ?: null;
171+
}
172+
173+
private function isIgnoredFileHash(string $file): int
174+
{
175+
if (in_array($file, $this->projectIgnoredFiles, true)) {
176+
return self::STATUS_IGNORED_IN_PROJECT;
177+
}
178+
179+
return self::STATUS_OK;
180+
}
181+
182+
private function assertNoGitVersion(): ?JsonResponse
183+
{
184+
if ($this->shopwareVersion === Kernel::SHOPWARE_FALLBACK_VERSION) {
185+
return new JsonResponse(['error' => 'Git version is not supported']);
186+
}
187+
188+
return null;
189+
}
190+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Frosh\Tools\DependencyInjection;
4+
5+
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
6+
use Symfony\Component\Config\Definition\ConfigurationInterface;
7+
8+
class Configuration implements ConfigurationInterface
9+
{
10+
public function getConfigTreeBuilder(): TreeBuilder
11+
{
12+
$treeBuilder = new TreeBuilder('frosh_tools');
13+
14+
$rootNode = $treeBuilder->getRootNode();
15+
$rootNode
16+
->children()
17+
->arrayNode('file_checker')
18+
->children()
19+
->arrayNode('exclude_files')
20+
->scalarPrototype()
21+
->end()
22+
->end()
23+
->end()
24+
->end()
25+
->end()
26+
;
27+
28+
return $treeBuilder;
29+
}
30+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Frosh\Tools\DependencyInjection;
4+
5+
use Symfony\Component\Config\Definition\ConfigurationInterface;
6+
use Symfony\Component\DependencyInjection\ContainerBuilder;
7+
use Symfony\Component\DependencyInjection\Extension\Extension;
8+
9+
class FroshToolsExtension extends Extension
10+
{
11+
public function load(array $configs, ContainerBuilder $container): void
12+
{
13+
$config = $this->processConfiguration($this->getConfiguration($configs, $container), $configs);
14+
$this->addConfig($container, $this->getAlias(), $config);
15+
}
16+
17+
public function getConfiguration(array $config, ContainerBuilder $container): ConfigurationInterface
18+
{
19+
return new Configuration();
20+
}
21+
22+
private function addConfig(ContainerBuilder $container, string $alias, array $options): void
23+
{
24+
foreach ($options as $key => $option) {
25+
$container->setParameter($alias . '.' . $key, $option);
26+
27+
if (\is_array($option)) {
28+
$this->addConfig($container, $alias . '.' . $key, $option);
29+
}
30+
}
31+
32+
/** @phpstan-ignore-next-line */
33+
if (!$container->hasParameter('frosh_tools.file_checker.exclude_files')) {
34+
$container->setParameter('frosh_tools.file_checker.exclude_files', []);
35+
}
36+
}
37+
}

src/FroshTools.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Frosh\Tools\DependencyInjection\CacheCompilerPass;
77
use Frosh\Tools\DependencyInjection\DisableElasticsearchCompilerPass;
88
use Frosh\Tools\DependencyInjection\SymfonyConfigCompilerPass;
9+
use Frosh\Tools\DependencyInjection\FroshToolsExtension;
910
use Shopware\Core\Framework\Plugin;
1011
use Symfony\Component\DependencyInjection\ContainerBuilder;
1112

@@ -19,4 +20,9 @@ public function build(ContainerBuilder $container): void
1920
$container->addCompilerPass(new SymfonyConfigCompilerPass());
2021
$container->addCompilerPass(new DisableElasticsearchCompilerPass());
2122
}
23+
24+
public function createContainerExtension(): FroshToolsExtension
25+
{
26+
return new FroshToolsExtension();
27+
}
2228
}

src/Resources/app/administration/package-lock.json

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "Resources",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"dependencies": {
6+
"diff-match-patch": "^1.0.5"
7+
},
8+
"license": "MIT"
9+
}

0 commit comments

Comments
 (0)