vendor/shopware/core/Framework/Update/Api/UpdateController.php line 107

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Framework\Update\Api;
  3. use Shopware\Core\Framework\Api\Context\AdminApiSource;
  4. use Shopware\Core\Framework\Api\Context\Exception\InvalidContextSourceException;
  5. use Shopware\Core\Framework\Api\Context\Exception\InvalidContextSourceUserException;
  6. use Shopware\Core\Framework\Context;
  7. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
  8. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  9. use Shopware\Core\Framework\Log\Package;
  10. use Shopware\Core\Framework\Plugin\KernelPluginLoader\StaticKernelPluginLoader;
  11. use Shopware\Core\Framework\Routing\Annotation\Acl;
  12. use Shopware\Core\Framework\Routing\Annotation\RouteScope;
  13. use Shopware\Core\Framework\Routing\Annotation\Since;
  14. use Shopware\Core\Framework\Store\Services\AbstractExtensionLifecycle;
  15. use Shopware\Core\Framework\Update\Event\UpdatePostFinishEvent;
  16. use Shopware\Core\Framework\Update\Event\UpdatePostPrepareEvent;
  17. use Shopware\Core\Framework\Update\Event\UpdatePreFinishEvent;
  18. use Shopware\Core\Framework\Update\Event\UpdatePrePrepareEvent;
  19. use Shopware\Core\Framework\Update\Exception\UpdateFailedException;
  20. use Shopware\Core\Framework\Update\Services\ApiClient;
  21. use Shopware\Core\Framework\Update\Services\PluginCompatibility;
  22. use Shopware\Core\Framework\Update\Services\RequirementsValidator;
  23. use Shopware\Core\Framework\Update\Steps\DeactivateExtensionsStep;
  24. use Shopware\Core\Framework\Update\Steps\DownloadStep;
  25. use Shopware\Core\Framework\Update\Steps\FinishResult;
  26. use Shopware\Core\Framework\Update\Steps\UnpackStep;
  27. use Shopware\Core\Framework\Update\Steps\ValidResult;
  28. use Shopware\Core\Framework\Update\Struct\Version;
  29. use Shopware\Core\Framework\Update\VersionFactory;
  30. use Shopware\Core\Framework\Uuid\Uuid;
  31. use Shopware\Core\Kernel;
  32. use Shopware\Core\System\SystemConfig\SystemConfigService;
  33. use Shopware\Core\System\User\UserEntity;
  34. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  35. use Symfony\Component\DependencyInjection\ContainerInterface;
  36. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  37. use Symfony\Component\Filesystem\Filesystem;
  38. use Symfony\Component\HttpFoundation\JsonResponse;
  39. use Symfony\Component\HttpFoundation\Request;
  40. use Symfony\Component\HttpFoundation\Response;
  41. use Symfony\Component\Routing\Annotation\Route;
  42. use Symfony\Component\Routing\Exception\RouteNotFoundException;
  43. /**
  44.  * @deprecated tag:v6.5.0 - reason:becomes-internal - will be considered internal
  45.  * @Route(defaults={"_routeScope"={"api"}})
  46.  */
  47. #[Package('system-settings')]
  48. class UpdateController extends AbstractController
  49. {
  50.     public const UPDATE_TOKEN_KEY 'core.update.token';
  51.     public const UPDATE_PREVIOUS_VERSION_KEY 'core.update.previousVersion';
  52.     private ApiClient $apiClient;
  53.     private string $rootDir;
  54.     private RequirementsValidator $requirementsValidator;
  55.     private PluginCompatibility $pluginCompatibility;
  56.     private EventDispatcherInterface $eventDispatcher;
  57.     private SystemConfigService $systemConfig;
  58.     private string $shopwareVersion;
  59.     private bool $isUpdateTest;
  60.     private EntityRepositoryInterface $userRepository;
  61.     private AbstractExtensionLifecycle $extensionLifecycleService;
  62.     /**
  63.      * @internal
  64.      */
  65.     public function __construct(
  66.         string $rootDir,
  67.         ApiClient $apiClient,
  68.         RequirementsValidator $requirementsValidator,
  69.         PluginCompatibility $pluginCompatibility,
  70.         EventDispatcherInterface $eventDispatcher,
  71.         SystemConfigService $systemConfig,
  72.         AbstractExtensionLifecycle $extensionLifecycleService,
  73.         EntityRepositoryInterface $userRepository,
  74.         string $shopwareVersion,
  75.         bool $isUpdateTest false
  76.     ) {
  77.         $this->rootDir $rootDir;
  78.         $this->apiClient $apiClient;
  79.         $this->requirementsValidator $requirementsValidator;
  80.         $this->pluginCompatibility $pluginCompatibility;
  81.         $this->eventDispatcher $eventDispatcher;
  82.         $this->systemConfig $systemConfig;
  83.         $this->shopwareVersion $shopwareVersion;
  84.         $this->isUpdateTest $isUpdateTest;
  85.         $this->userRepository $userRepository;
  86.         $this->extensionLifecycleService $extensionLifecycleService;
  87.     }
  88.     /**
  89.      * @Since("6.0.0.0")
  90.      * @Route("/api/_action/update/check", name="api.custom.updateapi.check", methods={"GET"}, defaults={"_acl"={"system:core:update"}})
  91.      */
  92.     public function updateApiCheck(): JsonResponse
  93.     {
  94.         if ($this->isUpdateTest) {
  95.             $version VersionFactory::createTestVersion();
  96.             return new JsonResponse($version);
  97.         }
  98.         try {
  99.             $updates $this->apiClient->checkForUpdates();
  100.             if (!$updates->isNewer) {
  101.                 return new JsonResponse();
  102.             }
  103.             return new JsonResponse($updates);
  104.         } catch (\Throwable $e) {
  105.             return new JsonResponse([
  106.                 '__class' => \get_class($e),
  107.                 '__message' => $e->getMessage(),
  108.             ]);
  109.         }
  110.     }
  111.     /**
  112.      * @Since("6.0.0.0")
  113.      * @Route("/api/_action/update/check-requirements", name="api.custom.update.check_requirements", methods={"GET"}, defaults={"_acl"={"system:core:update"}})
  114.      */
  115.     public function checkRequirements(): JsonResponse
  116.     {
  117.         $update $this->apiClient->checkForUpdates($this->shopwareVersion === Kernel::SHOPWARE_FALLBACK_VERSION);
  118.         return new JsonResponse($this->requirementsValidator->validate($update));
  119.     }
  120.     /**
  121.      * @Since("6.0.0.0")
  122.      * @Route("/api/_action/update/plugin-compatibility", name="api.custom.updateapi.plugin_compatibility", methods={"GET"}, defaults={"_acl"={"system:core:update", "system_config:read"}})
  123.      */
  124.     public function pluginCompatibility(Context $context): JsonResponse
  125.     {
  126.         $update $this->apiClient->checkForUpdates($this->shopwareVersion === Kernel::SHOPWARE_FALLBACK_VERSION);
  127.         return new JsonResponse($this->pluginCompatibility->getExtensionCompatibilities($update$context));
  128.     }
  129.     /**
  130.      * @Since("6.0.0.0")
  131.      * @Route("/api/_action/update/download-latest-update", name="api.custom.updateapi.download_latest_update", methods={"GET"}, defaults={"_acl"={"system:core:update", "system_config:read"}})
  132.      */
  133.     public function downloadLatestUpdate(Request $request): JsonResponse
  134.     {
  135.         $update $this->apiClient->checkForUpdates($this->shopwareVersion === Kernel::SHOPWARE_FALLBACK_VERSION);
  136.         $offset $request->query->getInt('offset');
  137.         $destination $this->createDestinationFromVersion($update);
  138.         if ($offset === && file_exists($destination)) {
  139.             unlink($destination);
  140.         }
  141.         $result = (new DownloadStep(
  142.             $update,
  143.             $destination,
  144.             $this->shopwareVersion === Kernel::SHOPWARE_FALLBACK_VERSION
  145.         ))->run($offset);
  146.         return $this->toJson($result);
  147.     }
  148.     /**
  149.      * @Since("6.0.0.0")
  150.      * @Route("/api/_action/update/unpack", name="api.custom.updateapi.unpack", methods={"GET"}, defaults={"_acl"={"system:core:update", "system_config:read"}})
  151.      */
  152.     public function unpack(Request $requestContext $context): JsonResponse
  153.     {
  154.         $update $this->apiClient->checkForUpdates($this->shopwareVersion === Kernel::SHOPWARE_FALLBACK_VERSION);
  155.         $source $this->createDestinationFromVersion($update);
  156.         $offset $request->query->getInt('offset');
  157.         $fs = new Filesystem();
  158.         $updateDir $this->rootDir '/files/update/';
  159.         $fileDir $this->rootDir '/files/update/files';
  160.         $unpackStep = new UnpackStep($source$fileDir$this->shopwareVersion === Kernel::SHOPWARE_FALLBACK_VERSION);
  161.         if ($offset === 0) {
  162.             $fs->remove($updateDir);
  163.         }
  164.         $result $unpackStep->run($offset);
  165.         // Test Mode
  166.         if ($result instanceof FinishResult && $this->shopwareVersion === Kernel::SHOPWARE_FALLBACK_VERSION) {
  167.             $updateToken Uuid::randomHex();
  168.             $this->systemConfig->set(self::UPDATE_TOKEN_KEY$updateToken);
  169.             return new JsonResponse([
  170.                 'redirectTo' => $request->getBaseUrl() . '/api/_action/update/finish/' $updateToken,
  171.             ]);
  172.         }
  173.         if ($result instanceof FinishResult) {
  174.             $fs->rename($fileDir '/update-assets/'$updateDir '/update-assets/');
  175.             $this->replaceRecoveryFiles($fileDir);
  176.             $payload = [
  177.                 'clientIp' => $request->getClientIp(),
  178.                 'version' => $update->version,
  179.                 'locale' => $this->getUpdateLocale($context),
  180.             ];
  181.             $updateFilePath $this->rootDir '/files/update/update.json';
  182.             if (!file_put_contents($updateFilePathjson_encode($payload))) {
  183.                 throw new UpdateFailedException(sprintf('Could not write file %s'$updateFilePath));
  184.             }
  185.             $this->systemConfig->set(self::UPDATE_PREVIOUS_VERSION_KEY$update->version);
  186.             return new JsonResponse([
  187.                 'redirectTo' => $request->getBaseUrl() . '/recovery/update/index.php',
  188.             ]);
  189.         }
  190.         return $this->toJson($result);
  191.     }
  192.     /**
  193.      * @Since("6.1.0.0")
  194.      * @Route("/api/_action/update/deactivate-plugins", name="api.custom.updateapi.deactivate-plugins", methods={"GET"}, defaults={"_acl"={"system:core:update", "system_config:read"}})
  195.      */
  196.     public function deactivatePlugins(Request $requestContext $context): JsonResponse
  197.     {
  198.         $update $this->apiClient->checkForUpdates($this->shopwareVersion === Kernel::SHOPWARE_FALLBACK_VERSION);
  199.         $offset $request->query->getInt('offset');
  200.         if ($offset === 0) {
  201.             // plugins can subscribe to this events, check compatibility and throw exceptions to prevent the update
  202.             $this->eventDispatcher->dispatch(
  203.                 new UpdatePrePrepareEvent($context$this->shopwareVersion$update->version)
  204.             );
  205.         }
  206.         // disable plugins - save active plugins
  207.         $deactivationFilter = (string) $request->query->get(
  208.             'deactivationFilter',
  209.             PluginCompatibility::PLUGIN_DEACTIVATION_FILTER_NOT_COMPATIBLE
  210.         );
  211.         $deactivatePluginStep = new DeactivateExtensionsStep(
  212.             $update,
  213.             $deactivationFilter,
  214.             $this->pluginCompatibility,
  215.             $this->extensionLifecycleService,
  216.             $this->systemConfig,
  217.             $context
  218.         );
  219.         $result $deactivatePluginStep->run($offset);
  220.         if ($result instanceof FinishResult) {
  221.             $containerWithoutPlugins $this->rebootKernelWithoutPlugins();
  222.             // @internal plugins are deactivated
  223.             $containerWithoutPlugins->get('event_dispatcher')->dispatch(
  224.                 new UpdatePostPrepareEvent($context$this->shopwareVersion$update->version)
  225.             );
  226.         }
  227.         return $this->toJson($result);
  228.     }
  229.     /**
  230.      * @Since("6.0.0.0")
  231.      * @Route("/api/_action/update/finish/{token}", defaults={"auth_required"=false, "_acl"={"system:core:update", "system_config:read"}}, name="api.custom.updateapi.finish", methods={"GET"})
  232.      */
  233.     public function finish(string $tokenRequest $requestContext $context): Response
  234.     {
  235.         $offset $request->query->getInt('offset');
  236.         $oldVersion $this->systemConfig->getString(self::UPDATE_PREVIOUS_VERSION_KEY);
  237.         if ($offset === 0) {
  238.             if (!$token) {
  239.                 return $this->getFinishResponse();
  240.             }
  241.             $dbUpdateToken $this->systemConfig->getString(self::UPDATE_TOKEN_KEY);
  242.             if (!$dbUpdateToken || $token !== $dbUpdateToken) {
  243.                 return $this->getFinishResponse();
  244.             }
  245.             $_unusedPreviousSetting ignore_user_abort(true);
  246.             $this->eventDispatcher->dispatch(new UpdatePreFinishEvent($context$oldVersion$this->shopwareVersion));
  247.         }
  248.         // TODO: NEXT-8271 - The kernel should be rebooted with the plugins reactivated. This does not happen to save some time, because plugins were not reactivated anyway.
  249.         $this->eventDispatcher->dispatch(
  250.             new UpdatePostFinishEvent($context$oldVersion$this->shopwareVersion)
  251.         );
  252.         return $this->getFinishResponse();
  253.     }
  254.     private function getUpdateLocale(Context $context): string
  255.     {
  256.         $contextSource $context->getSource();
  257.         if (!($contextSource instanceof AdminApiSource)) {
  258.             throw new InvalidContextSourceException(AdminApiSource::class, \get_class($contextSource));
  259.         }
  260.         $userId $contextSource->getUserId();
  261.         if ($userId === null) {
  262.             throw new InvalidContextSourceUserException(\get_class($contextSource));
  263.         }
  264.         $criteria = new Criteria([$userId]);
  265.         $criteria->getAssociation('locale');
  266.         /** @var UserEntity|null $user */
  267.         $user $this->userRepository->search($criteria$context)->first();
  268.         if ($user && $user->getLocale()) {
  269.             $code $user->getLocale()->getCode();
  270.             return mb_strtolower(explode('-'$code)[0]);
  271.         }
  272.         return 'en';
  273.     }
  274.     private function rebootKernelWithoutPlugins(): ContainerInterface
  275.     {
  276.         /** @var Kernel $kernel */
  277.         $kernel $this->container->get('kernel');
  278.         $classLoad $kernel->getPluginLoader()->getClassLoader();
  279.         $kernel->reboot(null, new StaticKernelPluginLoader($classLoad));
  280.         return $kernel->getContainer();
  281.     }
  282.     /**
  283.      * @param ValidResult|FinishResult $result
  284.      */
  285.     private function toJson(ValidResult $result): JsonResponse
  286.     {
  287.         if ($result instanceof FinishResult) {
  288.             return new JsonResponse([
  289.                 'valid' => false,
  290.                 'offset' => $result->getOffset(),
  291.                 'total' => $result->getTotal(),
  292.                 'success' => true,
  293.                 '_class' => \get_class($result),
  294.             ]);
  295.         }
  296.         return new JsonResponse([
  297.             'valid' => true,
  298.             'offset' => $result->getOffset(),
  299.             'total' => $result->getTotal(),
  300.             'success' => true,
  301.             '_class' => \get_class($result),
  302.         ]);
  303.     }
  304.     private function replaceRecoveryFiles(string $fileDir): void
  305.     {
  306.         $recoveryDir $fileDir '/vendor/shopware/recovery';
  307.         if (!is_dir($recoveryDir)) {
  308.             return;
  309.         }
  310.         $iterator $this->createRecursiveFileIterator($recoveryDir);
  311.         $fs = new Filesystem();
  312.         /** @var \SplFileInfo $file */
  313.         foreach ($iterator as $file) {
  314.             $sourceFile $file->getPathname();
  315.             $destinationFile $this->rootDir '/' str_replace($fileDir''$file->getPathname());
  316.             $destinationDirectory \dirname($destinationFile);
  317.             $fs->mkdir($destinationDirectory);
  318.             $fs->rename($sourceFile$destinationFiletrue);
  319.         }
  320.     }
  321.     private function createRecursiveFileIterator(string $path): \RecursiveIteratorIterator
  322.     {
  323.         $directoryIterator = new \RecursiveDirectoryIterator(
  324.             $path,
  325.             \RecursiveDirectoryIterator::SKIP_DOTS
  326.         );
  327.         return new \RecursiveIteratorIterator(
  328.             $directoryIterator,
  329.             \RecursiveIteratorIterator::LEAVES_ONLY
  330.         );
  331.     }
  332.     private function createDestinationFromVersion(Version $version): string
  333.     {
  334.         $filename 'update_' $version->sha1 '.zip';
  335.         return $this->rootDir '/' $filename;
  336.     }
  337.     private function getFinishResponse(): Response
  338.     {
  339.         try {
  340.             return $this->redirectToRoute('administration.index');
  341.         } catch (RouteNotFoundException $e) {
  342.             return new Response(''Response::HTTP_NO_CONTENT);
  343.         }
  344.     }
  345. }