├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── composer.lock ├── ecs.php ├── phpcs.xml.dist ├── phpstan.neon └── src ├── Facade └── AppHelper.php ├── Http ├── Controllers │ └── AppRegistrationController.php └── Middleware │ ├── SwAppHeaderMiddleware.php │ ├── SwAppIframeMiddleware.php │ └── SwAppMiddleware.php ├── Models └── SwShop.php ├── Repositories └── ShopRepository.php ├── ServiceProvider ├── ContextServiceProvider.php └── ShopwareSdkServiceProvider.php ├── Utils └── AppHelper.php ├── config └── sas_app.php ├── database └── migrations │ └── 2021_07_16_161755_create_sw_shops_table.php └── routes └── app.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | .idea/ 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 2.0.0 2 | - Drop primary key from `shop_id` of `sw_shop` 3 | - Add `app_id` as primary in `sw_shop` 4 | - Change behavior of middleware (using `app_name` and `app_id`) 5 | 6 | ### 1.3.2 7 | - Add get middleware for Iframe 8 | 9 | ### 1.3.1 10 | - Add SwAppHeaderMiddleware to verify incoming headers requests from Shopware 11 | 12 | ### 1.3.0 13 | - Add SwAppIframeMiddleware to verify incoming requests from Iframe Shopware 14 | 15 | ### 1.2.1 16 | - Fix wrong shopId parameter 17 | 18 | ### 1.2.0 19 | - Update SwAppMiddleware check post request key parameter same as get queries 20 | 21 | ### 1.1.0 22 | - Changed the version of the shopware-sdk to 1.* 23 | 24 | ### 1.0.0 25 | - Package initial structure 26 | - First release 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2021 vin 4 | 5 | > Permission is hereby granted, free of charge, to any person obtaining a copy 6 | > of this software and associated documentation files (the "Software"), to deal 7 | > in the Software without restriction, including without limitation the rights 8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | > copies of the Software, and to permit persons to whom the Software is 10 | > furnished to do so, subject to the following conditions: 11 | > 12 | > The above copyright notice and this permission notice shall be included in 13 | > all copies or substantial portions of the Software. 14 | > 15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shopware 6 Laravel SDK 2 | 3 | ![php](https://img.shields.io/badge/PHP-777BB4?style=for-the-badge&logo=php&logoColor=white) 4 | 5 | [![Latest Version on Packagist][ico-version]][link-packagist] 6 | [![Software License][ico-license]](LICENSE.md) 7 | 8 | A Laravel package to help integrate [Shopware PHP SDK](link-shopware-php-sdk) much more easier 9 | 10 | ## Installation 11 | 12 | Install with Composer 13 | 14 | ```shell 15 | composer require sas/shopware-laravel-sdk 16 | ``` 17 | 18 | Migrate shop table 19 | 20 | ```shell 21 | php artisan migrate 22 | ``` 23 | 24 | Publish config file - Change `/config/sas_app.php` for your specific app's configuration 25 | 26 | ```shell 27 | php artisan vendor:publish 28 | ``` 29 | 30 | ```php 31 | env('SW_APP_NAME', 'MyApp'), 39 | "app_secret" => env('SW_APP_SECRET', 'MyAppSecret'), 40 | "registration_url" => env('SW_APP_REGISTRATION_URL', '/app-registration'), 41 | "confirmation_url" => env('SW_APP_CONFIRMATION_URL', '/app-registration-confirmation'), 42 | ]; 43 | ``` 44 | 45 | Your app is now ready to install by a Shopware application! 46 | 47 | ## Usage 48 | - Context, ShopRepository auto-binding 49 | - SwAppMiddleware _(alias: 'sas.app.auth')_: A middleware to verify incoming webhook requests 50 | - SwAppIframeMiddleware _(alias: 'sas.app.auth.iframe:?app_name')_: A middleware to verify incoming requests from Iframe Shopware (`app_name` is the name of the App) 51 | - SwAppHeaderMiddleware _(alias: 'sas.app.auth.header')_: A middleware to verify incoming requests from Headers requests 52 | 53 | ## Change log 54 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 55 | 56 | ## Contribution 57 | Feels free to create an issue on Github issues page or contact us directly at hello@shapeandshift.dev 58 | 59 | ## Security 60 | If you discover any security related issues, please email hello@shapeandshift.dev instead of using the issue tracker. 61 | 62 | ### Requirements 63 | - ext-curl 64 | - PHP 7.4 / 8.0 65 | - vin-sw/shopware-php-sdk >= 1.0 66 | 67 | This SDK is mainly dedicated to Shopware 6.4 and onwards, earlier SW application may still be usable without test 68 | 69 | ## Credits 70 | 71 | - [vienthuong][link-author] 72 | - [All Contributors][link-contributors] 73 | 74 | ## License 75 | 76 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 77 | 78 | [ico-version]: https://img.shields.io/packagist/v/vin-sw/shopware-sdk.svg?style=flat-square 79 | [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square 80 | [link-packagist]: https://packagist.org/packages/vin-sw/shopware-sdk 81 | [link-downloads]: https://packagist.org/packages/vin-sw/shopware-sdk 82 | [link-author]: https://github.com/vienthuong 83 | [link-contributors]: ../../contributors 84 | [link-shopware-php-sdk]: https://github.com/vienthuong/shopware-php-sdk 85 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sas/shopware-laravel-sdk", 3 | "description": "Shopware SDK for Laravel 8", 4 | "type": "library", 5 | "version": "2.0.0", 6 | "require": { 7 | "php": "^7.4 || ^8.0", 8 | "ext-json": "*", 9 | "symfony/psr-http-message-bridge": "*", 10 | "vin-sw/shopware-sdk": "1.*" 11 | }, 12 | "license": "MIT", 13 | "autoload": { 14 | "psr-4": { 15 | "Sas\\ShopwareLaravelSdk\\": "src/" 16 | } 17 | }, 18 | "authors": [ 19 | { 20 | "name": "Shape & Shift", 21 | "email": "hello@shapeandshift.dev", 22 | "homepage": "https://shapeandshift.dev" 23 | } 24 | ], 25 | "minimum-stability": "dev", 26 | "extra": { 27 | "laravel": { 28 | "providers": [ 29 | "Sas\\ShopwareLaravelSdk\\ServiceProvider\\ShopwareSdkServiceProvider", 30 | "Sas\\ShopwareLaravelSdk\\ServiceProvider\\ContextServiceProvider" 31 | ], 32 | "aliases": { 33 | "AppHelper": "Sas\\ShopwareLaravelSdk\\Facade\\AppHelper" 34 | } 35 | } 36 | }, 37 | "require-dev": { 38 | "illuminate/auth": "*", 39 | "illuminate/database": "*", 40 | "illuminate/support": "*", 41 | "phpunit/phpunit": "*", 42 | "squizlabs/php_codesniffer": "3.*", 43 | "symplify/easy-coding-standard": "9.3.20", 44 | "symplify/config-transformer": "^9.3", 45 | "phpstan/phpstan": "^0.12.89" 46 | }, 47 | "scripts": { 48 | "ecs": "vendor/bin/ecs check src", 49 | "check-style": "phpcs src", 50 | "analyse": "vendor/bin/phpstan analyse src", 51 | "fix-style": "phpcbf src", 52 | "lint": "vendor/bin/ecs check src && phpcs src", 53 | "lint-fix": "vendor/bin/ecs check src --fix && phpcbf src" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /ecs.php: -------------------------------------------------------------------------------- 1 | services(); 32 | 33 | $services->set(ModernizeTypesCastingFixer::class); 34 | 35 | $services->set(ClassAttributesSeparationFixer::class) 36 | ->call('configure', [['elements' => ['property' => 'one', 'method' => 'one', 'const' => 'one']]]); 37 | 38 | $services->set(MethodArgumentSpaceFixer::class) 39 | ->call('configure', [['on_multiline' => 'ensure_fully_multiline']]); 40 | 41 | $services->set(NullableTypeDeclarationForDefaultNullValueFixer::class); 42 | 43 | $services->set(VoidReturnFixer::class); 44 | 45 | $services->set(ConcatSpaceFixer::class) 46 | ->call('configure', [['spacing' => 'one']]); 47 | 48 | $services->set(GeneralPhpdocAnnotationRemoveFixer::class) 49 | ->call('configure', [['annotations' => ['copyright', 'category']]]); 50 | 51 | // $services->set(NoSuperfluousPhpdocTagsFixer::class); 52 | 53 | $services->set(PhpdocOrderFixer::class); 54 | 55 | $services->set(NoUselessReturnFixer::class); 56 | 57 | $services->set(\PhpCsFixer\Fixer\Import\NoUnusedImportsFixer::class); 58 | 59 | $services->set(DeclareStrictTypesFixer::class); 60 | 61 | $services->set(CompactNullableTypehintFixer::class); 62 | 63 | $services->set(NoImportFromGlobalNamespaceFixer::class); 64 | 65 | $services->set(NoSuperfluousConcatenationFixer::class); 66 | 67 | $services->set(NoUselessCommentFixer::class); 68 | 69 | $services->set(OperatorLinebreakFixer::class); 70 | 71 | $services->set(PhpdocNoIncorrectVarAnnotationFixer::class); 72 | 73 | $services->set(SingleSpaceAfterStatementFixer::class); 74 | 75 | $parameters = $containerConfigurator->parameters(); 76 | 77 | $parameters->set('skip', [SelfAccessorFixer::class => null, ExplicitIndirectVariableFixer::class => null, BlankLineAfterOpeningTagFixer::class => null, PhpdocSummaryFixer::class => null, ExplicitStringVariableFixer::class => null, 'PhpCsFixerCustomFixers\Fixer\NoUselessCommentFixer' => null, 'PHP_CodeSniffer\Standards\Generic\Sniffs\CodeAnalysis\AssignmentInConditionSniff' => null]); 78 | }; 79 | -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | The coding standard of kiot-viet-client package 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 8 3 | treatPhpDocTypesAsCertain: false 4 | checkMissingIterableValueType: false 5 | checkGenericClassInNonGenericObjectType: false 6 | inferPrivatePropertyTypeFromConstructor: true 7 | reportUnmatchedIgnoredErrors: true # Could be set to false if necessary during PHPStan update 8 | tmpDir: ./var/cache/phpstan 9 | paths: 10 | - src 11 | 12 | featureToggles: 13 | unusedClassElements: true 14 | 15 | excludes_analyse: 16 | 17 | ignoreErrors: 18 | - 19 | message: '#Unsafe usage of new static\(\)#' 20 | path: src/Data/Collection.php 21 | -------------------------------------------------------------------------------- /src/Facade/AppHelper.php: -------------------------------------------------------------------------------- 1 | register($app); 22 | 23 | $shopRepository->createShop($response->getShop()); 24 | 25 | $confirmationUrl = route('sas.app.auth.confirmation'); 26 | 27 | return new RegistrationResponse($response, $confirmationUrl); 28 | } 29 | 30 | public function confirm(Request $request, ShopRepository $shopRepository): Response 31 | { 32 | $shopId = $request->request->get('shopId'); 33 | 34 | $shopSecret = $shopRepository->getSecretByShopId($shopId); 35 | 36 | if (!WebhookAuthenticator::authenticatePostRequest($shopSecret)) { 37 | return new Response(null, 401); 38 | } 39 | 40 | $shopRepository->updateAccessKeysForShop( 41 | $shopId, 42 | $request->request->get('apiKey'), 43 | $request->request->get('secretKey') 44 | ); 45 | 46 | return new Response(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Http/Middleware/SwAppHeaderMiddleware.php: -------------------------------------------------------------------------------- 1 | shopRepository = $shopRepository; 24 | } 25 | 26 | /** 27 | * Handle an incoming request. 28 | * 29 | * @param Request $request 30 | * @param Closure $next 31 | * @return mixed 32 | * @throws AuthorizationFailedException 33 | */ 34 | public function handle(Request $request, Closure $next) 35 | { 36 | $shop = $this->authenticateHeaderRequest($request); 37 | 38 | Auth::setUser($shop); 39 | 40 | return $next($request); 41 | } 42 | 43 | protected function authenticateHeaderRequest(Request $request): SwShop 44 | { 45 | $headers = $request->headers; 46 | $shopId = $headers->get(ShopRequest::SHOP_ID_REQUEST_PARAMETER); 47 | $appId = $headers->get(AppHelper::APP_ID_REQUEST_PARAMETER); 48 | if ($headers->has(AppHelper::APP_ID_REQUEST_PARAMETER) && empty($appId)) { 49 | throw new AuthorizationFailedException(sprintf('%s in headers is invalid', AppHelper::APP_ID_REQUEST_PARAMETER)); 50 | } 51 | 52 | $shop = $this->shopRepository->getShopById($shopId, ['app_id' => $appId]); 53 | 54 | $authenticated = $shop && $this->checkHeaderRequests($headers, $shop); 55 | 56 | if (!$authenticated) { 57 | throw new AuthorizationFailedException($request->getMethod() . ' is not supported or the data is invalid'); 58 | } 59 | 60 | return $shop; 61 | } 62 | 63 | protected function checkHeaderRequests(HeaderBag $headers, SwShop $shop): bool 64 | { 65 | $queries = []; 66 | 67 | if ($headers->has(AppHelper::LOCATION_ID_REQUEST_PARAMETER)) { 68 | $queries[AppHelper::LOCATION_ID_REQUEST_PARAMETER] = $headers->get(AppHelper::LOCATION_ID_REQUEST_PARAMETER); 69 | } 70 | 71 | if ($headers->has(AppHelper::PRIVILEGES_REQUEST_PARAMETER)) { 72 | $queries[AppHelper::PRIVILEGES_REQUEST_PARAMETER] = urlencode((string)$headers->get(AppHelper::PRIVILEGES_REQUEST_PARAMETER)); 73 | } 74 | 75 | if ($headers->has(ShopRequest::SHOP_ID_REQUEST_PARAMETER)) { 76 | $queries[ShopRequest::SHOP_ID_REQUEST_PARAMETER] = $headers->get(ShopRequest::SHOP_ID_REQUEST_PARAMETER); 77 | } 78 | 79 | if ($headers->has(ShopRequest::SHOP_URL_REQUEST_PARAMETER)) { 80 | $queries[ShopRequest::SHOP_URL_REQUEST_PARAMETER] = $headers->get(ShopRequest::SHOP_URL_REQUEST_PARAMETER); 81 | } 82 | 83 | if ($headers->has(ShopRequest::TIME_STAMP_REQUEST_PARAMETER)) { 84 | $queries[ShopRequest::TIME_STAMP_REQUEST_PARAMETER] = $headers->get(ShopRequest::TIME_STAMP_REQUEST_PARAMETER); 85 | } 86 | 87 | if ($headers->has(ShopRequest::SHOPWARE_VERSION_REQUEST_PARAMETER)) { 88 | $queries[ShopRequest::SHOPWARE_VERSION_REQUEST_PARAMETER] = $headers->get(ShopRequest::SHOPWARE_VERSION_REQUEST_PARAMETER); 89 | } 90 | 91 | if ($headers->has(ShopRequest::SHOP_CONTEXT_LANGUAGE)) { 92 | $queries[ShopRequest::SHOP_CONTEXT_LANGUAGE] = $headers->get(ShopRequest::SHOP_CONTEXT_LANGUAGE); 93 | } 94 | 95 | if ($headers->has(ShopRequest::SHOP_USER_LANGUAGE)) { 96 | $queries[ShopRequest::SHOP_USER_LANGUAGE] = $headers->get(ShopRequest::SHOP_USER_LANGUAGE); 97 | } 98 | 99 | $queryString = htmlspecialchars_decode(urldecode(http_build_query($queries))); 100 | 101 | $hmac = hash_hmac('sha256', htmlspecialchars_decode($queryString), $shop->shop_secret); 102 | 103 | return hash_equals($hmac, (string)$headers->get(ShopRequest::SHOP_SIGNATURE_REQUEST_PARAMETER)); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Http/Middleware/SwAppIframeMiddleware.php: -------------------------------------------------------------------------------- 1 | getContent(), true); 19 | $sourceRequest = $requestContent['source']; 20 | $shopId = $sourceRequest[ShopRequest::SHOP_ID_REQUEST_PARAMETER]; 21 | 22 | $shop = $this->shopRepository->getShopById($shopId, ['app_name' => $this->appName]); 23 | 24 | $authenticated = $shop && $this->checkPostRequest($sourceRequest, $shop->shop_secret); 25 | if (!$authenticated) { 26 | throw new AuthorizationFailedException($request->getMethod() . ' is not supported or the data is invalid'); 27 | } 28 | 29 | return $shop; 30 | } 31 | 32 | protected function authenticateGetRequest(Request $request): SwShop 33 | { 34 | $queries = $request->query; 35 | $shopId = (string)$queries->get(ShopRequest::SHOP_ID_REQUEST_PARAMETER); 36 | 37 | $shop = $this->shopRepository->getShopById($shopId, ['app_name' => $this->appName]); 38 | 39 | $authenticated = $shop && $this->checkGetRequests($queries, $shop); 40 | if (!$authenticated) { 41 | throw new AuthorizationFailedException($request->getMethod() . ' is not supported or the data is invalid'); 42 | } 43 | 44 | return $shop; 45 | } 46 | 47 | private function checkPostRequest(array $sourceRequests, string $shopSecret): bool 48 | { 49 | $shopwareShopSignature = $sourceRequests['shopware-shop-signature']; 50 | 51 | unset($sourceRequests[ShopRequest::SHOP_SIGNATURE_REQUEST_PARAMETER]); 52 | 53 | $results = []; 54 | foreach ($sourceRequests as $key => $sourceRequest) { 55 | if (!in_array($key, self::REQUIRED_KEYS)) { 56 | $sourceRequest = urlencode($sourceRequest); 57 | } 58 | 59 | $results[$key] = $sourceRequest; 60 | } 61 | 62 | $queryString = htmlspecialchars_decode(urldecode(http_build_query($results))); 63 | $hmac = hash_hmac('sha256', $queryString, $shopSecret); 64 | 65 | return hash_equals($hmac, $shopwareShopSignature); 66 | } 67 | 68 | protected function checkGetRequests(InputBag $inputBag, SwShop $shop): bool 69 | { 70 | $queries = []; 71 | 72 | if ($inputBag->has(AppHelper::LOCATION_ID_REQUEST_PARAMETER)) { 73 | $queries[AppHelper::LOCATION_ID_REQUEST_PARAMETER] = $inputBag->get(AppHelper::LOCATION_ID_REQUEST_PARAMETER); 74 | } 75 | 76 | if ($inputBag->has(AppHelper::PRIVILEGES_REQUEST_PARAMETER)) { 77 | $queries[AppHelper::PRIVILEGES_REQUEST_PARAMETER] = urlencode((string)$inputBag->get(AppHelper::PRIVILEGES_REQUEST_PARAMETER)); 78 | } 79 | 80 | if ($inputBag->has(ShopRequest::SHOP_ID_REQUEST_PARAMETER)) { 81 | $queries[ShopRequest::SHOP_ID_REQUEST_PARAMETER] = $inputBag->get(ShopRequest::SHOP_ID_REQUEST_PARAMETER); 82 | } 83 | 84 | if ($inputBag->has(ShopRequest::SHOP_URL_REQUEST_PARAMETER)) { 85 | $queries[ShopRequest::SHOP_URL_REQUEST_PARAMETER] = $inputBag->get(ShopRequest::SHOP_URL_REQUEST_PARAMETER); 86 | } 87 | 88 | if ($inputBag->has(ShopRequest::TIME_STAMP_REQUEST_PARAMETER)) { 89 | $queries[ShopRequest::TIME_STAMP_REQUEST_PARAMETER] = $inputBag->get(ShopRequest::TIME_STAMP_REQUEST_PARAMETER); 90 | } 91 | 92 | if ($inputBag->has(ShopRequest::SHOPWARE_VERSION_REQUEST_PARAMETER)) { 93 | $queries[ShopRequest::SHOPWARE_VERSION_REQUEST_PARAMETER] = $inputBag->get(ShopRequest::SHOPWARE_VERSION_REQUEST_PARAMETER); 94 | } 95 | 96 | if ($inputBag->has(ShopRequest::SHOP_CONTEXT_LANGUAGE)) { 97 | $queries[ShopRequest::SHOP_CONTEXT_LANGUAGE] = $inputBag->get(ShopRequest::SHOP_CONTEXT_LANGUAGE); 98 | } 99 | 100 | if ($inputBag->has(ShopRequest::SHOP_USER_LANGUAGE)) { 101 | $queries[ShopRequest::SHOP_USER_LANGUAGE] = $inputBag->get(ShopRequest::SHOP_USER_LANGUAGE); 102 | } 103 | 104 | $queryString = htmlspecialchars_decode(urldecode(http_build_query($queries))); 105 | 106 | $hmac = hash_hmac('sha256', htmlspecialchars_decode($queryString), $shop->shop_secret); 107 | 108 | return hash_equals($hmac, (string)$inputBag->get(ShopRequest::SHOP_SIGNATURE_REQUEST_PARAMETER)); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Http/Middleware/SwAppMiddleware.php: -------------------------------------------------------------------------------- 1 | shopRepository = $shopRepository; 31 | } 32 | 33 | /** 34 | * Handle an incoming request. 35 | * 36 | * @param Request $request 37 | * @param Closure $next 38 | * @param ?string $appName 39 | * @return mixed 40 | * @throws AuthorizationFailedException 41 | */ 42 | public function handle(Request $request, Closure $next, ?string $appName) 43 | { 44 | $this->appName = $appName; 45 | $shop = null; 46 | 47 | if ($request->getMethod() === 'POST' && $this->supportsPostRequest($request)) { 48 | $shop = $this->authenticatePostRequest($request); 49 | } elseif ($request->getMethod() === 'GET' && $this->supportsGetRequest($request)) { 50 | $shop = $this->authenticateGetRequest($request); 51 | } elseif ($request->getMethod() === 'DELETE' && $this->supportsGetRequest($request)) { 52 | $shop = $this->authenticateDeleteRequest($request); 53 | } 54 | 55 | // TODO: set custom guard for app 56 | if ($shop) { 57 | Auth::setUser($shop); 58 | } 59 | 60 | return $next($request); 61 | } 62 | 63 | protected function supportsPostRequest(Request $request): bool 64 | { 65 | $requestContent = json_decode((string)$request->getContent(), true); 66 | 67 | $hasSource = $requestContent && array_key_exists('source', $requestContent); 68 | 69 | if (!$hasSource) { 70 | return false; 71 | } 72 | 73 | return $this->checkRequiredKeys($requestContent['source']); 74 | } 75 | 76 | protected function checkRequiredKeys(array $data): bool 77 | { 78 | foreach (self::REQUIRED_KEYS as $key) { 79 | if (!array_key_exists($key, $data)) { 80 | return false; 81 | } 82 | } 83 | 84 | return true; 85 | } 86 | 87 | protected function authenticatePostRequest(Request $request): SwShop 88 | { 89 | $requestContent = json_decode((string)$request->getContent(), true); 90 | $sourceRequest = $requestContent['source']; 91 | $shopId = $sourceRequest[ShopRequest::SHOP_ID_REQUEST_PARAMETER]; 92 | 93 | $shop = $this->shopRepository->getShopById($shopId); 94 | 95 | $authenticated = $shop && WebhookAuthenticator::authenticatePostRequest($shop->shop_secret); 96 | 97 | if (!$authenticated) { 98 | throw new AuthorizationFailedException($request->getMethod() . ' is not supported or the data is invalid'); 99 | } 100 | 101 | return $shop; 102 | } 103 | 104 | protected function supportsGetRequest(Request $request): bool 105 | { 106 | return $this->checkRequiredKeys($request->query->all()); 107 | } 108 | 109 | protected function authenticateGetRequest(Request $request): SwShop 110 | { 111 | $shopId = (string)$request->query->get(ShopRequest::SHOP_ID_REQUEST_PARAMETER); 112 | $shop = $this->shopRepository->getShopById($shopId); 113 | 114 | $authenticated = $shop && WebhookAuthenticator::authenticateGetRequest($shop->shop_secret); 115 | 116 | if (!$authenticated) { 117 | throw new AuthorizationFailedException($request->getMethod() . ' is not supported or the data is invalid'); 118 | } 119 | 120 | return $shop; 121 | } 122 | 123 | protected function authenticateDeleteRequest(Request $request): SwShop 124 | { 125 | $shopId = (string)$request->query->get(ShopRequest::SHOP_ID_REQUEST_PARAMETER); 126 | $shop = $this->shopRepository->getShopById($shopId); 127 | 128 | $authenticated = $shop && WebhookAuthenticator::authenticateGetRequest($shop->shop_secret); 129 | 130 | if (!$authenticated) { 131 | throw new AuthorizationFailedException($request->getMethod() . ' is not supported or the data is invalid'); 132 | } 133 | 134 | return $shop; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Models/SwShop.php: -------------------------------------------------------------------------------- 1 | attributes['access_token'] = $value ? serialize($value) : null; 47 | } 48 | 49 | public function getAuthIdentifierName(): string 50 | { 51 | return 'secret_key'; 52 | } 53 | 54 | public function getAuthIdentifier(): string 55 | { 56 | return $this->secret_key; 57 | } 58 | 59 | public function getAuthPassword(): string 60 | { 61 | return ''; 62 | } 63 | 64 | public function getRememberToken(): string 65 | { 66 | return ''; 67 | } 68 | 69 | public function setRememberToken($value) 70 | { 71 | // TODO: Implement setRememberToken() method. 72 | } 73 | 74 | public function getRememberTokenName(): string 75 | { 76 | return ''; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Repositories/ShopRepository.php: -------------------------------------------------------------------------------- 1 | shopModel = $shopModel; 24 | } 25 | 26 | public function updateAccessKeysForShop(string $shopId, string $apiKey, string $secretKey): void 27 | { 28 | $shop = $this->getShopById($shopId); 29 | 30 | if (!$shop) { 31 | throw new Exception('Shop not found'); 32 | } 33 | 34 | $shop->update([ 35 | 'api_key' => $apiKey, 36 | 'secret_key' => $secretKey 37 | ]); 38 | } 39 | 40 | public function createShop(Shop $shop, array $condition = [], array $update = []): void 41 | { 42 | $this->shopModel->updateOrCreate( 43 | array_merge([ 44 | 'shop_id' => $shop->getShopId() 45 | ], $condition), 46 | array_merge([ 47 | 'app_id' => Uuid::uuid4()->toString(), 48 | 'shop_url' => $shop->getShopUrl(), 49 | 'shop_secret' => $shop->getShopSecret(), 50 | 'access_token' => null, 51 | 'api_key' => null, 52 | 'secret_key' => null 53 | ], $update) 54 | ); 55 | } 56 | 57 | public function getShopById(?string $shopId, array $queries = []): ?SwShop 58 | { 59 | if (!$shopId) { 60 | return null; 61 | } 62 | 63 | if (array_key_exists($shopId, $this->shops)) { 64 | return $this->shops[$shopId]; 65 | } 66 | 67 | $query = $this->shopModel->where('shop_id', $shopId); 68 | foreach ($queries as $column => $value) { 69 | if (empty($value) || empty($column)) { 70 | continue; 71 | } 72 | 73 | $query = $query->where($column, $value); 74 | } 75 | 76 | $shop = $query->first(); 77 | if (!$shop) { 78 | return null; 79 | } 80 | 81 | return $this->shops[$shopId] = $shop; 82 | } 83 | 84 | public function removeShop(string $shopId, array $queries = []): void 85 | { 86 | $shop = $this->getShopById($shopId, $queries); 87 | 88 | $shop->delete(); 89 | 90 | unset($this->shops[$shopId]); 91 | } 92 | 93 | public function getSecretByShopId(string $shopId, array $queries = []): string 94 | { 95 | $shop = $this->getShopById($shopId, $queries); 96 | 97 | if (!$shop) { 98 | throw new Exception('Shop not found'); 99 | } 100 | 101 | return $shop->shop_secret; 102 | } 103 | 104 | public function getShopContext(string $shopId, array $queries = []): Context 105 | { 106 | $shop = $this->getShopById($shopId, $queries); 107 | if (!$shop) { 108 | throw new Exception('Shop not found'); 109 | } 110 | 111 | $token = $shop->access_token; 112 | if (!$token instanceof AccessToken || $token->isExpired()) { 113 | $grantType = new ClientCredentialsGrantType($shop->api_key, $shop->secret_key); 114 | $authenticator = new AdminAuthenticator($grantType, $shop->shop_url); 115 | 116 | $token = $authenticator->fetchAccessToken(); 117 | 118 | $shop->update([ 119 | 'access_token' => $token 120 | ]); 121 | } 122 | 123 | return new Context($shop->shop_url, $token); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/ServiceProvider/ContextServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton(ShopRepository::class, function (Application $app) { 28 | return new ShopRepository(new SwShop()); 29 | }); 30 | 31 | $this->app->singleton(Context::class, function (Application $app) { 32 | /** @var ShopRepository $shopRepository */ 33 | $shopRepository = $app->get(ShopRepository::class); 34 | 35 | /** @var Request $request */ 36 | $request = $app->get(Request::class); 37 | 38 | if ($request->headers->has(ShopRequest::SHOP_ID_REQUEST_PARAMETER)) { 39 | $shopId = $request->headers->get(ShopRequest::SHOP_ID_REQUEST_PARAMETER); 40 | $appId = (string) $request->headers->get(AppHelper::APP_ID_REQUEST_PARAMETER); 41 | } else { 42 | if ($request->getMethod() === 'POST') { 43 | $requestContent = \json_decode($request->getContent(), true); 44 | $shopId = $requestContent['source']['shopId']; 45 | } else { 46 | $shopId = $request->query->get(ShopRequest::SHOP_ID_REQUEST_PARAMETER); 47 | } 48 | 49 | $appId = (string)$request->query->get(AppHelper::APP_ID_REQUEST_PARAMETER); 50 | } 51 | 52 | if (!$shopId) { 53 | return null; 54 | } 55 | 56 | return $shopRepository->getShopContext($shopId, ['app_id' => $appId]); 57 | }); 58 | 59 | $this->app->singleton(AppAction::class, function (Application $app) { 60 | /** @var Request $request */ 61 | $request = $app->get(Request::class); 62 | 63 | $requestContent = \json_decode($request->getContent(), true); 64 | 65 | return AppAction::createFromPayload($requestContent, $request->headers->all()); 66 | }); 67 | 68 | $this->app->singleton(Event::class, function (Application $app) { 69 | /** @var Request $request */ 70 | $request = $app->get(Request::class); 71 | 72 | $requestContent = \json_decode($request->getContent(), true); 73 | 74 | return Event::createFromPayload($requestContent, $request->headers->all()); 75 | }); 76 | 77 | $this->app->singleton(IFrameRequest::class, function (Application $app) { 78 | /** @var Request $request */ 79 | $request = $app->get(Request::class); 80 | 81 | return new IFrameRequest($request->all()); 82 | }); 83 | } 84 | 85 | /** 86 | * Get the services provided by the provider. 87 | * 88 | * @return array 89 | */ 90 | public function provides(): array 91 | { 92 | return [Context::class, AppAction::class, ShopRepository::class, Event::class, IFrameRequest::class]; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/ServiceProvider/ShopwareSdkServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom( 21 | __DIR__ . '/../config/sas_app.php', 22 | 'sas_app' 23 | ); 24 | 25 | $this->app->bind('AppHelper', function () { 26 | return new AppHelper($this->app->get('request')); 27 | }); 28 | } 29 | 30 | /** 31 | * Bootstrap any application services. 32 | * 33 | * @return void 34 | */ 35 | public function boot(): void 36 | { 37 | $this->publishes([ 38 | __DIR__ . '/../config/sas_app.php' => config_path('sas_app.php'), 39 | ]); 40 | 41 | $this->loadMigrationsFrom(__DIR__ . '/../database/migrations'); 42 | $this->loadRoutesFrom(__DIR__ . '/../routes/app.php'); 43 | 44 | $this->app->get('router')->aliasMiddleware('sas.app.auth', SwAppMiddleware::class); 45 | $this->app->get('router')->aliasMiddleware('sas.app.auth.iframe', SwAppIframeMiddleware::class); 46 | $this->app->get('router')->aliasMiddleware('sas.app.auth.header', SwAppHeaderMiddleware::class); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Utils/AppHelper.php: -------------------------------------------------------------------------------- 1 | request = $request; 23 | } 24 | 25 | public function shop_route(string $route, array $params = [], $absolute = true, ?AppAction $action = null): string 26 | { 27 | if ($action !== null) { 28 | $queryString = sprintf( 29 | 'shop-id=%s&shop-url=%s×tamp=%s&sw-version=%s', 30 | $action->getSource()->getShopId(), 31 | $action->getSource()->getShopUrl(), 32 | $action->getMeta()->getTimestamp(), 33 | $this->request->headers->get(ShopRequest::SHOPWARE_VERSION_REQUEST_PARAMETER), 34 | ); 35 | 36 | $hmac = \hash_hmac('sha256', htmlspecialchars_decode($queryString), Auth::user()->shop_secret); 37 | 38 | $params = array_merge(array_filter([ 39 | ShopRequest::SHOP_ID_REQUEST_PARAMETER => $action->getSource()->getShopId(), 40 | ShopRequest::SHOP_URL_REQUEST_PARAMETER => $action->getSource()->getShopUrl(), 41 | ShopRequest::SHOPWARE_VERSION_REQUEST_PARAMETER => $this->request->headers->get(ShopRequest::SHOPWARE_VERSION_REQUEST_PARAMETER), 42 | ShopRequest::SHOP_SIGNATURE_REQUEST_PARAMETER => $hmac, 43 | ShopRequest::TIME_STAMP_REQUEST_PARAMETER => $action->getMeta()->getTimestamp(), 44 | ]), $params); 45 | } 46 | 47 | $params = array_merge(array_filter([ 48 | ShopRequest::SHOP_ID_REQUEST_PARAMETER => $this->request->get(ShopRequest::SHOP_ID_REQUEST_PARAMETER), 49 | ShopRequest::SHOP_URL_REQUEST_PARAMETER => $this->request->get(ShopRequest::SHOP_URL_REQUEST_PARAMETER), 50 | ShopRequest::SHOPWARE_VERSION_REQUEST_PARAMETER => $this->request->get(ShopRequest::SHOPWARE_VERSION_REQUEST_PARAMETER), 51 | ShopRequest::SHOP_SIGNATURE_REQUEST_PARAMETER => $this->request->get(ShopRequest::SHOP_SIGNATURE_REQUEST_PARAMETER), 52 | ShopRequest::TIME_STAMP_REQUEST_PARAMETER => $this->request->get(ShopRequest::TIME_STAMP_REQUEST_PARAMETER), 53 | ]), $params); 54 | 55 | return route($route, $params, $absolute); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/config/sas_app.php: -------------------------------------------------------------------------------- 1 | env('SW_APP_NAME', 'MyApp'), 5 | "app_secret" => env('SW_APP_SECRET', 'MyAppSecret'), 6 | "registration_url" => env('SW_APP_REGISTRATION_URL', '/app-registration'), 7 | "confirmation_url" => env('SW_APP_CONFIRMATION_URL', '/app-registration-confirmation'), 8 | ]; 9 | -------------------------------------------------------------------------------- /src/database/migrations/2021_07_16_161755_create_sw_shops_table.php: -------------------------------------------------------------------------------- 1 | string('app_id')->primary(); 19 | $table->string('shop_id'); 20 | $table->string('shop_url', 255); 21 | $table->string('shop_secret', 255); 22 | $table->string('app_name', 255)->nullable(); 23 | $table->string('api_key', 255)->nullable(); 24 | $table->string('secret_key', 255)->nullable(); 25 | $table->longText('access_token')->nullable(); 26 | $table->timestamps(); 27 | }); 28 | } 29 | 30 | /** 31 | * Reverse the migrations. 32 | * 33 | * @return void 34 | */ 35 | public function down(): void 36 | { 37 | Schema::dropIfExists('sw_shops'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/routes/app.php: -------------------------------------------------------------------------------- 1 | group(function (): void { 7 | Route::get( 8 | config('sas_app.registration_url'), 9 | [AppRegistrationController::class, 'register'] 10 | )->name('registration'); 11 | 12 | Route::post( 13 | config('sas_app.confirmation_url'), 14 | [AppRegistrationController::class, 'confirm'] 15 | )->name('confirmation'); 16 | }); 17 | --------------------------------------------------------------------------------