Completed
Push — labs ( 04d0b8...211687 )
by Christian
14:25
created

Router::createApiRouteCollection()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 44
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 44
c 0
b 0
f 0
rs 8.8571
ccs 0
cts 0
cp 0
cc 1
eloc 34
nc 1
nop 0
crap 2
1
<?php declare(strict_types=1);
2
/**
3
 * Shopware 5
4
 * Copyright (c) shopware AG
5
 *
6
 * According to our dual licensing model, this program can be used either
7
 * under the terms of the GNU Affero General Public License, version 3,
8
 * or under a proprietary license.
9
 *
10
 * The texts of the GNU Affero General Public License with an additional
11
 * permission and of our proprietary license can be found at and
12
 * in the LICENSE file you have received along with this program.
13
 *
14
 * This program is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
 * GNU Affero General Public License for more details.
18
 *
19
 * "Shopware" is a registered trademark of shopware AG.
20
 * The licensing of the program under the AGPLv3 does not imply a
21
 * trademark license. Therefore any rights, title and interest in
22
 * our trademarks remain entirely with us.
23
 */
24
25
namespace Shopware\Framework\Routing;
26
27
use Psr\Cache\CacheItemPoolInterface;
28
use Psr\Log\LoggerInterface;
29
use Ramsey\Uuid\Uuid;
30
use Shopware\Context\Struct\ShopContext;
31
use Shopware\Defaults;
32
use Shopware\Kernel;
33
use Symfony\Component\DependencyInjection\ContainerInterface;
34
use Symfony\Component\HttpFoundation\Request;
35
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
36
use Symfony\Component\Routing\Generator\UrlGenerator;
37
use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
38
use Symfony\Component\Routing\Matcher\UrlMatcher;
39
use Symfony\Component\Routing\RequestContext;
40
use Symfony\Component\Routing\Route;
41
use Symfony\Component\Routing\RouteCollection;
42
use Symfony\Component\Routing\RouterInterface;
43
44
class Router implements RouterInterface, RequestMatcherInterface
45
{
46
    public const SEO_REDIRECT_URL = 'seo_redirect_url';
47
    public const IS_API_REQUEST_ATTRIBUTE = 'is_api';
48
    public const REQUEST_TYPE_ATTRIBUTE = '_request_type';
49
    public const REQUEST_TYPE_STOREFRONT = 'storefront';
50
    public const REQUEST_TYPE_API = 'api';
51
    public const REQUEST_TYPE_ADMINISTRATION = 'administration';
52
53
    /**
54
     * Generates an seo URL, e.g. "http://example.com/example-category".
55
     */
56
    public const SEO_URL = 5;
57
58
    /**
59
     * @var RequestContext
60
     */
61
    private $context;
62
63
    /**
64
     * @var RouteCollection
65
     */
66
    private $routes;
67
68
    /**
69
     * @var string
70
     */
71
    private $resource;
72
73
    /**
74
     * @var LoggerInterface
75
     */
76
    private $logger;
77
78
    /**
79
     * @var ShopFinder
80
     */
81
    private $shopFinder;
82
83
    /**
84
     * @var UrlResolverInterface
85
     */
86
    private $urlResolver;
87
88
    /**
89
     * @var \Symfony\Component\HttpKernel\Bundle\BundleInterface[]
90
     */
91
    private $bundles;
92
93
    /**
94
     * @var CacheItemPoolInterface
95
     */
96
    private $cache;
97
98
    /**
99
     * @var ContainerInterface
100
     */
101
    private $container;
102
103
    public function __construct(
104
        ContainerInterface $container,
105
        $resource,
106 72
        Kernel $kernel,
107
        ?RequestContext $context = null,
108
        LoggerInterface $logger = null,
109
        UrlResolverInterface $urlResolver,
110
        ShopFinder $shopFinder,
111
        CacheItemPoolInterface $cache
112
    ) {
113
        $this->resource = $resource;
114
        $this->context = $context;
115
        $this->logger = $logger;
116
117 72
        $this->bundles = $kernel->getBundles();
118 72
        $this->urlResolver = $urlResolver;
119 72
        $this->shopFinder = $shopFinder;
120
        $this->cache = $cache;
121 72
        $this->container = $container;
122 72
    }
123 72
124 72
    public function setContext(RequestContext $context): void
125 72
    {
126 72
        $this->context = $context;
127 72
    }
128
129
    public function getContext(): ?RequestContext
130
    {
131
        return $this->context;
132
    }
133
134 19
    /**
135
     * @return RouteCollection
136 19
     */
137
    public function getRouteCollection(): RouteCollection
138
    {
139
        if ($this->routes !== null) {
140
            return $this->routes;
141
        }
142 19
143
        if ($this->container->getParameter('kernel.environment') !== 'prod') {
144 19
            $this->routes = $this->loadRoutes();
145
146
            return $this->routes;
147
        }
148 19
149 19
        $cacheItem = $this->cache->getItem('router_routes');
150
        if ($routes = $cacheItem->get()) {
151 19
            return $this->routes = $routes;
152
        }
153
154
        $this->routes = $this->loadRoutes();
155
        $cacheItem->set($this->routes);
156
        $this->cache->save($cacheItem);
157
158
        return $this->routes;
159
    }
160
161
    public function generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH): string
162
    {
163
        $generator = new UrlGenerator(
164
            $this->getRouteCollection(),
165
            $this->getContext(),
166
            $this->logger
167
        );
168
169
        if (!$context = $this->getContext()) {
170
            return $generator->generate($name, $parameters, $referenceType);
171
        }
172
173
        if (!$shop = $context->getParameter('router_shop')) {
174
            return $generator->generate($name, $parameters, $referenceType);
175
        }
176
177
        $route = $this->getRouteCollection()->get($name);
178
        if ($route && $route->getOption('seo') !== true) {
179
            return $generator->generate($name, $parameters, $referenceType);
180
        }
181
182
        if ($referenceType !== self::SEO_URL) {
183
            //generate new url with shop base path/url
184
            $url = $generator->generate($name, $parameters, $referenceType);
185
186
            return rtrim($url, '/');
187
        }
188
189
        //rewrite base url for url generator
190
        $stripBaseUrl = $this->rewriteBaseUrl($shop['base_url'], $shop['base_path']);
191
192
        //find seo url for path info
193
        $pathInfo = $generator->generate($name, $parameters, UrlGenerator::ABSOLUTE_PATH);
194
        if ($stripBaseUrl !== '/') {
195
            $pathInfo = str_replace($stripBaseUrl, '', $pathInfo);
196
        }
197
198
        $pathInfo = '/' . trim($pathInfo, '/');
199
200
        $shopContext = new ShopContext(
201
            $shop['id'],
202
            json_decode($shop['shop_catalog_ids'], true),
203
            [],
204
            $shop['currency_id'],
205
            Defaults::LANGUAGE,
206
            null,
207
            Defaults::LIVE_VERSION,
208
            (float) $shop['currency_factor']
209
        );
210
211
        $seoUrl = $this->urlResolver->getUrl($shop['id'], $pathInfo, $shopContext);
212
213
        //generate new url with shop base path/url
214
        $url = $generator->generate($name, $parameters, $referenceType);
215
216
        if ($seoUrl) {
217
            $seoPathInfo = '/' . ltrim($seoUrl->getSeoPathInfo(), '/');
218
            $url = str_replace($pathInfo, $seoPathInfo, $url);
219
        }
220
221
        return rtrim($url, '/');
222
    }
223
224
    public function match($pathinfo)
225
    {
226
        $pathinfo = '/' . trim($pathinfo, '/');
227
228
        $this->context->setPathInfo($pathinfo);
229
230
        $matcher = new UrlMatcher($this->getRouteCollection(), $this->getContext());
231
232
        return $matcher->match($pathinfo);
233
    }
234 19
235
    public function matchRequest(Request $request): array
236 19
    {
237
        $requestStack = $this->container->get('request_stack');
238 19
        $master = $requestStack->getMasterRequest();
239
240 19
        if ($master !== null && $master->attributes->has('router_shop')) {
241
            $shop = $master->attributes->get('router_shop');
242 19
        } else {
243
            $shop = $this->shopFinder->findShopByRequest($this->context, $request);
244
        }
245 19
246
        $pathInfo = $this->context->getPathInfo();
247 19
248 19
        // save decision which type of request is called (storefront, api, administration)
249
        $requestType = $this->getRequestType($request);
250 19
251
        $request->attributes->set(self::REQUEST_TYPE_ATTRIBUTE, $requestType);
252
        $request->attributes->set(
253 19
            self::IS_API_REQUEST_ATTRIBUTE,
254
            in_array($requestType, [self::REQUEST_TYPE_ADMINISTRATION, self::REQUEST_TYPE_API], true)
255
        );
256 19
257
        if (!$shop) {
258
            return $this->match($pathInfo);
259 19
        }
260
261 19
        //save detected shop to context for further processes
262 19
        $currencyId = $this->getCurrencyId($request, $shop['currency_id']);
263 19
264 19
        $this->context->setParameter('router_shop', $shop);
265
        $request->attributes->set('router_shop', $shop);
266
        $request->attributes->set('_shop_id', $shop['id']);
267 19
        $request->attributes->set('_currency_id', $currencyId);
268
        $request->attributes->set('_locale_id', $shop['locale_id']);
269
        $request->setLocale($shop['locale_code']);
270
271
        $stripBaseUrl = $this->rewriteBaseUrl($shop['base_url'], $shop['base_path']);
272 19
273
        // strip base url from path info
274 19
        $pathInfo = $request->getBaseUrl() . $request->getPathInfo();
275 19
        $pathInfo = preg_replace('#^' . $stripBaseUrl . '#i', '', $pathInfo);
276 19
        $pathInfo = '/' . trim($pathInfo, '/');
277 19
278 19
        if (strpos($pathInfo, '/widgets/') !== false) {
279 19
            return $this->match($pathInfo);
280
        }
281 19
282
        if ($request->attributes->get(self::IS_API_REQUEST_ATTRIBUTE)) {
283
            try {
284 19
                $match = $this->match($pathInfo);
285 19
            } catch (ResourceNotFoundException $e) {
286 19
                return $this->matchDynamicApi($pathInfo);
287
            }
288 19
289
            return $match;
290
        }
291
292 19
        $shopContext = new ShopContext(
293 19
            $shop['id'],
294
            json_decode($shop['shop_catalog_ids'], true),
295
            [],
296
            $shop['currency_id'],
297
            Defaults::LANGUAGE,
298
            null,
299
            Defaults::LIVE_VERSION,
300
            (float) $shop['currency_factor']
301
        );
302
303
        //resolve seo urls to use symfony url matcher for route detection
304
        $seoUrl = $this->urlResolver->getPathInfo($shop['id'], $pathInfo, $shopContext);
305
306
        if (!$seoUrl) {
307
            try {
308
                return $this->match($pathInfo);
309
            } catch (\Exception $e) {
310
                if ($requestType === self::REQUEST_TYPE_STOREFRONT) {
311
                    return $this->match('/');
312
                }
313
                throw $e;
314
            }
315
        }
316
317
        $pathInfo = $seoUrl->getPathInfo();
318
        if (!$seoUrl->getIsCanonical()) {
319
            $redirectUrl = $this->urlResolver->getUrl($shop['id'], $seoUrl->getPathInfo(), $shopContext);
320
            $request->attributes->set(self::SEO_REDIRECT_URL, $redirectUrl);
321
        }
322
323
        try {
324
            return $this->match($pathInfo);
325
        } catch (\Exception $e) {
326
            if ($requestType === self::REQUEST_TYPE_STOREFRONT) {
327
                return $this->match('/');
328
            }
329
            throw $e;
330
        }
331
    }
332
333
    public function assemble(string $url): string
334
    {
335
        $generator = new UrlGenerator(
336
            $this->getRouteCollection(),
337
            $this->getContext(),
338
            $this->logger
339
        );
340
341
        $base = $generator->generate('homepage', [], UrlGenerator::ABSOLUTE_URL);
342
343
        return rtrim($base, '/') . '/' . ltrim($url, '/');
344
    }
345
346
    protected function getCurrencyId(Request $request, string $fallback): string
347
    {
348
        if ($this->context->getMethod() === 'POST' && $request->get('__currency')) {
349
            return (string) $request->get('__currency');
350 19
        }
351
352 19
        if ($request->cookies->has('currency')) {
353
            return (string) $request->cookies->get('currency');
354
        }
355
356 19
        if ($request->attributes->has('_currency_id')) {
357 19
            return (string) $request->attributes->get('_currency_id');
358
        }
359
360
        return $fallback;
361
    }
362
363
    private function loadRoutes(): RouteCollection
364
    {
365
        $routeCollection = new RouteCollection();
366
367 19
        if (file_exists($this->resource)) {
368
            $routeCollection->addCollection(
369 19
                $this->container->get('routing.loader')->load($this->resource)
370
            );
371 19
        }
372
373
        foreach ($this->bundles as $bundle) {
374
            if (!file_exists($bundle->getPath() . '/Controller')) {
375
                continue;
376
            }
377 19
378 19
            $routeCollection->addCollection(
379 19
                $this->container->get('routing.loader')->import($bundle->getPath() . '/Controller/', 'annotation')
380
            );
381
        }
382 19
383 19
        return $routeCollection;
384
    }
385
386
    private function rewriteBaseUrl(?string $baseUrl, string $basePath): string
387 19
    {
388 19
        //generate new path info for detected shop
389
        $stripBaseUrl = $baseUrl ?? $basePath;
390
        $stripBaseUrl = rtrim($stripBaseUrl, '/') . '/';
391 19
392
        //rewrite base url for url generator
393
        $this->context->setBaseUrl(rtrim($stripBaseUrl, '/'));
394 19
395
        return $stripBaseUrl;
396
    }
397 19
398 19
    private function getRequestType(Request $request): string
399
    {
400
        $isApi = stripos($request->getPathInfo(), '/api/') === 0;
401 19
402
        if ($isApi && $request->query->has('administration')) {
403 19
            return self::REQUEST_TYPE_ADMINISTRATION;
404
        }
405
        if ($isApi) {
406 19
            return self::REQUEST_TYPE_API;
407
        }
408 19
409
        return self::REQUEST_TYPE_STOREFRONT;
410 19
    }
411
412
    private function matchDynamicApi(string $path)
413 19
    {
414 19
        $apiRoutes = $this->createApiRouteCollection();
415
416
        if (Uuid::isValid(basename($path))) {
417
            $apiRoutes->remove('api_controller.list');
418
        } else {
419
            $apiRoutes->remove('api_controller.detail');
420
        }
421
422
        $matcher = new UrlMatcher($apiRoutes, $this->getContext());
423
424
        return $matcher->match($path);
425
    }
426
427
    private function createApiRouteCollection(): RouteCollection
428
    {
429
        $collection = new RouteCollection();
430
431
        $class = 'Shopware\Rest\Controller\ApiController';
432
433
        $route = new Route('/api/{path}');
434
        $route->setMethods(['GET']);
435
        $route->setDefault('_controller', $class . ':listAction');
436
        $route->addRequirements(['path' => '.*']);
437
        $collection->add('api_controller.list', $route);
438
439
        $route = new Route('/api/{path}');
440
        $route->setMethods(['GET']);
441
        $route->setDefault('_controller', $class . '::detailAction');
442
        $route->addRequirements(['path' => '.*']);
443
        $collection->add('api_controller.detail', $route);
444
445
        $route = new Route('/api/search/{path}');
446
        $route->setMethods(['POST']);
447
        $route->setDefault('_controller', $class . '::searchAction');
448
        $route->addRequirements(['path' => '.*']);
449
        $collection->add('api_controller.search', $route);
450
451
        $route = new Route('/api/{path}');
452
        $route->setMethods(['POST']);
453
        $route->setDefault('_controller', $class . '::createAction');
454
        $route->addRequirements(['path' => '.*']);
455
        $collection->add('api_controller.create', $route);
456
457
        $route = new Route('/api/{path}');
458
        $route->setMethods(['PATCH']);
459
        $route->setDefault('_controller', $class . '::updateAction');
460
        $route->addRequirements(['path' => '.*']);
461
        $collection->add('api_controller.update', $route);
462
463
        $route = new Route('/api/{path}');
464
        $route->setMethods(['DELETE']);
465
        $route->setDefault('_controller', $class . '::deleteAction');
466
        $route->addRequirements(['path' => '.*']);
467
        $collection->add('api_controller.delete', $route);
468
469
        return $collection;
470
    }
471
}
472