diff --git a/_dev/src/ts/api/RequestHandler.ts b/_dev/src/ts/api/RequestHandler.ts index 7d3701860..bfba09d4f 100644 --- a/_dev/src/ts/api/RequestHandler.ts +++ b/_dev/src/ts/api/RequestHandler.ts @@ -32,16 +32,25 @@ export class RequestHandler { data.append('dir', window.AutoUpgradeVariables.admin_dir); try { - const response = await baseApi.post('', data, { + const response = await baseApi.post('', data, { params: { route }, signal }); - const responseData = response.data as ApiResponse; + const responseData = response.data; await this.#handleResponse(responseData, fromPopState); } catch (error) { - // TODO: catch errors - console.error(error); + // A couple or errors are returned in an actual response (i.e 404 or 500) + if (error instanceof AxiosError) { + if (error.response?.data) { + const responseData = error.response.data; + responseData.new_route = 'error-page'; + await this.#handleResponse(responseData, true); + } + } else { + // TODO: catch errors + console.error(error); + } } } diff --git a/_dev/src/ts/pages/ErrorPage.ts b/_dev/src/ts/pages/ErrorPage.ts new file mode 100644 index 000000000..92641e573 --- /dev/null +++ b/_dev/src/ts/pages/ErrorPage.ts @@ -0,0 +1,23 @@ +import DomLifecycle from '../types/DomLifecycle'; +import ErrorPage404 from './error/ErrorPage404'; +import PageAbstract from './PageAbstract'; + +export default class ErrorPage extends PageAbstract { + errorPage?: DomLifecycle; + + constructor() { + super(); + + if (document.getElementById('ua_error_404')) { + this.errorPage = new ErrorPage404(); + } + } + + public mount = () => { + this.errorPage?.mount(); + }; + + public beforeDestroy = () => { + this.errorPage?.beforeDestroy(); + }; +} diff --git a/_dev/src/ts/pages/error/ErrorPage404.ts b/_dev/src/ts/pages/error/ErrorPage404.ts new file mode 100644 index 000000000..5da372ccd --- /dev/null +++ b/_dev/src/ts/pages/error/ErrorPage404.ts @@ -0,0 +1,53 @@ +import api from '../../api/RequestHandler'; +import DomLifecycle from '../../types/DomLifecycle'; + +export default class ErrorPage404 implements DomLifecycle { + isOnHomePage: boolean = false; + + public constructor() { + this.isOnHomePage = new URLSearchParams(window.location.search).get('route') === 'home-page'; + } + + public mount = () => { + this.#activeActionButton.classList.remove('hidden'); + this.#form.addEventListener('submit', this.#onSubmit); + }; + + public beforeDestroy = () => { + this.#form.removeEventListener('submit', this.#onSubmit); + }; + + get #activeActionButton(): HTMLFormElement | HTMLAnchorElement { + return this.isOnHomePage ? this.#form : this.#exitButton; + } + + get #form(): HTMLFormElement { + const form = document.forms.namedItem('home-page-form'); + if (!form) { + throw new Error('Form not found'); + } + + ['routeToSubmit'].forEach((data) => { + if (!form.dataset[data]) { + throw new Error(`Missing data ${data} from form dataset.`); + } + }); + + return form; + } + + get #exitButton(): HTMLAnchorElement { + const link = document.getElementById('exit-button'); + + if (!link || !(link instanceof HTMLAnchorElement)) { + throw new Error('Link is not found or invalid'); + } + return link; + } + + readonly #onSubmit = async (event: Event) => { + event.preventDefault(); + + await api.post(this.#form.dataset.routeToSubmit!, new FormData(this.#form)); + }; +} diff --git a/_dev/src/ts/routing/ScriptHandler.ts b/_dev/src/ts/routing/ScriptHandler.ts index d92af31c4..78f153ad4 100644 --- a/_dev/src/ts/routing/ScriptHandler.ts +++ b/_dev/src/ts/routing/ScriptHandler.ts @@ -16,6 +16,7 @@ import SendErrorReportDialog from '../dialogs/SendErrorReportDialog'; import DomLifecycle from '../types/DomLifecycle'; import { RoutesMatching } from '../types/scriptHandlerTypes'; import { routeHandler } from '../autoUpgrade'; +import ErrorPage from '../pages/ErrorPage'; export default class ScriptHandler { #currentScript: DomLifecycle | undefined; @@ -38,6 +39,8 @@ export default class ScriptHandler { 'restore-page-restore': RestorePageRestore, 'restore-page-post-restore': RestorePagePostRestore, + 'error-page': ErrorPage, + 'start-update-dialog': StartUpdateDialog, 'send-error-report-dialog': SendErrorReportDialog }; @@ -61,16 +64,18 @@ export default class ScriptHandler { * @description Loads and mounts the page script associated with the specified route name. */ loadScript(scriptID: string) { - const classScript = this.#routesMatching[scriptID]; - if (classScript) { - try { - this.#currentScript = new classScript(); - this.#currentScript.mount(); - } catch (error) { - console.error(`Failed to load script with ID ${scriptID}:`, error); - } - } else { + let classScript = this.#routesMatching[scriptID]; + if (!classScript) { console.debug(`No matching class found for ID: ${scriptID}`); + // Each route must provide a script to load. If it does not exist, we load the error management script + classScript = this.#routesMatching['error-page']; + } + + try { + this.#currentScript = new classScript(); + this.#currentScript.mount(); + } catch (error) { + console.error(`Failed to load script with ID ${scriptID}:`, error); } } diff --git a/classes/Router/UrlGenerator.php b/classes/Router/UrlGenerator.php new file mode 100644 index 000000000..9d1220cdb --- /dev/null +++ b/classes/Router/UrlGenerator.php @@ -0,0 +1,74 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0) + */ + +namespace PrestaShop\Module\AutoUpgrade\Router; + +use Symfony\Component\HttpFoundation\Request; + +class UrlGenerator +{ + /** @var string */ + private $shopBasePath; + /** @var string */ + private $adminFolder; + + public function __construct(string $shopBasePath, string $adminFolder) + { + $this->shopBasePath = $shopBasePath; + $this->adminFolder = $adminFolder; + } + + public function getShopAbsolutePathFromRequest(Request $request): string + { + // Determine the subdirectories of the PHP entry point (the script being executed) + // relative to the shop root folder. + // This calculation helps generate a base path that correctly accounts for any subfolder in which + // the shop might be installed. + $subDirs = explode( + DIRECTORY_SEPARATOR, + trim( + str_replace( + $this->shopBasePath, + '', + dirname($request->server->get('SCRIPT_FILENAME', '') + ) + ), DIRECTORY_SEPARATOR) + ); + $numberOfSubDirs = count($subDirs); + + $path = explode('/', $request->getBasePath()); + + $path = array_splice($path, 0, -$numberOfSubDirs); + + return implode('/', $path) ?: '/'; + } + + public function getShopAdminAbsolutePathFromRequest(Request $request): string + { + return rtrim($this->getShopAbsolutePathFromRequest($request), '/') . '/' . $this->adminFolder; + } +} diff --git a/classes/Twig/AssetsEnvironment.php b/classes/Twig/AssetsEnvironment.php index cc8c700e1..414e521a2 100644 --- a/classes/Twig/AssetsEnvironment.php +++ b/classes/Twig/AssetsEnvironment.php @@ -27,18 +27,19 @@ namespace PrestaShop\Module\AutoUpgrade\Twig; +use PrestaShop\Module\AutoUpgrade\Router\UrlGenerator; use Symfony\Component\HttpFoundation\Request; class AssetsEnvironment { const DEV_BASE_URL = 'http://localhost:5173'; - /** @var string */ - private $shopBasePath; + /** @var UrlGenerator */ + protected $urlGenerator; - public function __construct(string $shopBasePath) + public function __construct(UrlGenerator $urlGenerator) { - $this->shopBasePath = $shopBasePath; + $this->urlGenerator = $urlGenerator; } public function isDevMode(): bool @@ -52,31 +53,6 @@ public function getAssetsBaseUrl(Request $request): string return self::DEV_BASE_URL; } - return $this->getShopUrlFromRequest($request) . '/modules/autoupgrade/views'; - } - - private function getShopUrlFromRequest(Request $request): string - { - // Determine the subdirectories of the PHP entry point (the script being executed) - // relative to the shop root folder. - // This calculation helps generate a base path that correctly accounts for any subfolder in which - // the shop might be installed. - $subDirs = explode( - DIRECTORY_SEPARATOR, - trim( - str_replace( - $this->shopBasePath, - '', - dirname($request->server->get('SCRIPT_FILENAME', '') - ) - ), DIRECTORY_SEPARATOR) - ); - $numberOfSubDirs = count($subDirs); - - $path = explode('/', $request->getBasePath()); - - $path = array_splice($path, 0, -$numberOfSubDirs); - - return implode('/', $path); + return rtrim($this->urlGenerator->getShopAbsolutePathFromRequest($request), '/') . '/modules/autoupgrade/views'; } } diff --git a/classes/UpgradeContainer.php b/classes/UpgradeContainer.php index e8d9dd62d..12ef7109e 100644 --- a/classes/UpgradeContainer.php +++ b/classes/UpgradeContainer.php @@ -41,6 +41,7 @@ use PrestaShop\Module\AutoUpgrade\Parameters\UpgradeConfiguration; use PrestaShop\Module\AutoUpgrade\Progress\CompletionCalculator; use PrestaShop\Module\AutoUpgrade\Repository\LocalArchiveRepository; +use PrestaShop\Module\AutoUpgrade\Router\UrlGenerator; use PrestaShop\Module\AutoUpgrade\Services\ComposerService; use PrestaShop\Module\AutoUpgrade\Services\DistributionApiService; use PrestaShop\Module\AutoUpgrade\Services\LogsService; @@ -269,6 +270,9 @@ class UpgradeContainer */ private $upgradeSelfCheck; + /** @var UrlGenerator */ + private $urlGenerator; + /** * @var PhpVersionResolverService */ @@ -943,11 +947,11 @@ public function getLocalArchiveRepository(): LocalArchiveRepository */ public function getAssetsEnvironment(): AssetsEnvironment { - if (null !== $this->assetsEnvironment) { - return $this->assetsEnvironment; + if (null === $this->assetsEnvironment) { + $this->assetsEnvironment = new AssetsEnvironment($this->getUrlGenerator()); } - return $this->assetsEnvironment = new AssetsEnvironment($this->getProperty(self::PS_ROOT_PATH)); + return $this->assetsEnvironment; } /** @@ -992,6 +996,21 @@ public function getPrestashopVersionService(): PrestashopVersionService return $this->prestashopVersionService = new PrestashopVersionService($this->getZipAction()); } + /** + * @return UrlGenerator + */ + public function getUrlGenerator(): UrlGenerator + { + if (null === $this->urlGenerator) { + $this->urlGenerator = new UrlGenerator( + $this->getProperty(self::PS_ROOT_PATH), + $this->getProperty(self::PS_ADMIN_SUBDIR), + ); + } + + return $this->urlGenerator; + } + /** * Checks if the composer autoload exists, and loads it. * diff --git a/controllers/admin/self-managed/Error404Controller.php b/controllers/admin/self-managed/Error404Controller.php index 1791baabc..fa38fe680 100644 --- a/controllers/admin/self-managed/Error404Controller.php +++ b/controllers/admin/self-managed/Error404Controller.php @@ -27,11 +27,29 @@ namespace PrestaShop\Module\AutoUpgrade\Controller; +use PrestaShop\Module\AutoUpgrade\Router\Routes; +use Symfony\Component\HttpFoundation\Response; + class Error404Controller extends AbstractPageController { + const ERROR_CODE = 404; + + public function index() + { + $response = parent::index(); + + if ($response instanceof Response) { + $response->setStatusCode(self::ERROR_CODE); + } else { + http_response_code(self::ERROR_CODE); + } + + return $response; + } + protected function getPageTemplate(): string { - return 'errors/404'; + return 'errors/' . self::ERROR_CODE; } protected function getParams(): array @@ -39,6 +57,11 @@ protected function getParams(): array return [ // TODO: assets_base_path is provided by all controllers. What about a asset() twig function instead? 'assets_base_path' => $this->upgradeContainer->getAssetsEnvironment()->getAssetsBaseUrl($this->request), + + 'error_code' => self::ERROR_CODE, + + 'exit_to_shop_admin' => $this->upgradeContainer->getUrlGenerator()->getShopAdminAbsolutePathFromRequest($this->request), + 'exit_to_app_home' => Routes::HOME_PAGE, ]; } } diff --git a/tests/unit/Router/UrlGeneratorTest.php b/tests/unit/Router/UrlGeneratorTest.php new file mode 100644 index 000000000..9752a763a --- /dev/null +++ b/tests/unit/Router/UrlGeneratorTest.php @@ -0,0 +1,117 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0) + */ +use PHPUnit\Framework\TestCase; +use PrestaShop\Module\AutoUpgrade\Router\UrlGenerator; +use Symfony\Component\HttpFoundation\Request; + +class UrlGeneratorTest extends TestCase +{ + /** @var UrlGenerator */ + private $urlGenerator; + + protected function setUp() + { + $shopBasePath = '/yo/doge'; + $adminPath = 'wololo'; + $this->urlGenerator = new UrlGenerator($shopBasePath, $adminPath); + } + + public function testGetShopUrlReturnsUrl() + { + $server = [ + 'HTTP_HOST' => 'localhost', + 'SERVER_PORT' => '80', + 'QUERY_STRING' => '', + 'PHP_SELF' => '/admin-wololo/index.php', + 'SCRIPT_FILENAME' => '/yo/doge/admin-wololo/index.php', + 'REQUEST_URI' => 'index.php', + ]; + + $request = new Request([], [], [], [], [], $server); + + $expectedAbsoluteUrlPathToShop = '/'; + $expectedAbsoluteUrlPathToAdmin = '/wololo'; + $this->assertSame($expectedAbsoluteUrlPathToShop, $this->urlGenerator->getShopAbsolutePathFromRequest($request)); + $this->assertSame($expectedAbsoluteUrlPathToAdmin, $this->urlGenerator->getShopAdminAbsolutePathFromRequest($request)); + } + + public function testGetShopUrlReturnsUrlWithShopInSubFolder() + { + $server = [ + 'HTTP_HOST' => 'localhost', + 'SERVER_PORT' => '80', + 'QUERY_STRING' => '', + 'PHP_SELF' => '/hello-world/admin-wololo/index.php', + 'SCRIPT_FILENAME' => '/yo/doge/admin-wololo/index.php', + 'REQUEST_URI' => 'hello-world/index.php', + ]; + + $request = new Request([], [], [], [], [], $server); + + $expectedAbsoluteUrlPathToShop = '/hello-world'; + $expectedAbsoluteUrlPathToAdmin = '/hello-world/wololo'; + $this->assertSame($expectedAbsoluteUrlPathToShop, $this->urlGenerator->getShopAbsolutePathFromRequest($request)); + $this->assertSame($expectedAbsoluteUrlPathToAdmin, $this->urlGenerator->getShopAdminAbsolutePathFromRequest($request)); + } + + public function testGetShopUrlReturnsUrlWithCustomEntrypoint() + { + $server = [ + 'HTTP_HOST' => 'localhost', + 'SERVER_PORT' => '80', + 'QUERY_STRING' => '', + 'PHP_SELF' => '/admin-wololo/autoupgrade/ajax-upgradetab.php', + 'SCRIPT_FILENAME' => '/yo/doge/admin-wololo/autoupgrade/ajax-upgradetab.php', + 'REQUEST_URI' => '/admin-wololo/autoupgrade/ajax-upgradetab.php?route=update-step-backup-submit', + ]; + + $request = new Request([], [], [], [], [], $server); + + $expectedAbsoluteUrlPathToShop = '/'; + $expectedAbsoluteUrlPathToAdmin = '/wololo'; + $this->assertSame($expectedAbsoluteUrlPathToShop, $this->urlGenerator->getShopAbsolutePathFromRequest($request)); + $this->assertSame($expectedAbsoluteUrlPathToAdmin, $this->urlGenerator->getShopAdminAbsolutePathFromRequest($request)); + } + + public function testGetShopUrlReturnsUrlWithShopInSubFolderAndParams() + { + $server = [ + 'HTTP_HOST' => 'localhost', + 'SERVER_PORT' => '80', + 'QUERY_STRING' => '', + 'PHP_SELF' => '/hello-world/admin-wololo/index.php', + 'SCRIPT_FILENAME' => '/yo/doge/admin-wololo/index.php', + 'REQUEST_URI' => 'hello-world/admin-wololo/index.php?controller=AdminSelfUpgrade', + ]; + + $request = new Request([], [], [], [], [], $server); + + $expectedAbsoluteUrlPathToShop = '/hello-world'; + $expectedAbsoluteUrlPathToAdmin = '/hello-world/wololo'; + $this->assertSame($expectedAbsoluteUrlPathToShop, $this->urlGenerator->getShopAbsolutePathFromRequest($request)); + $this->assertSame($expectedAbsoluteUrlPathToAdmin, $this->urlGenerator->getShopAdminAbsolutePathFromRequest($request)); + } +} diff --git a/tests/unit/Twig/AssetsEnvironmentTest.php b/tests/unit/Twig/AssetsEnvironmentTest.php index 37c7b42ae..8217b9937 100644 --- a/tests/unit/Twig/AssetsEnvironmentTest.php +++ b/tests/unit/Twig/AssetsEnvironmentTest.php @@ -24,6 +24,7 @@ * @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0) */ use PHPUnit\Framework\TestCase; +use PrestaShop\Module\AutoUpgrade\Router\UrlGenerator; use PrestaShop\Module\AutoUpgrade\Twig\AssetsEnvironment; use Symfony\Component\HttpFoundation\Request; @@ -34,7 +35,8 @@ class AssetsEnvironmentTest extends TestCase protected function setUp() { $shopBasePath = '/yo/doge'; - $this->assetsEnvironment = new AssetsEnvironment($shopBasePath); + $adminPath = 'wololo'; + $this->assetsEnvironment = new AssetsEnvironment(new UrlGenerator($shopBasePath, $adminPath)); } protected function tearDown() @@ -72,7 +74,7 @@ public function testGetAssetsBaseUrlReturnsDevUrlInDevMode() public function testGetAssetsBaseUrlReturnsProductionUrl() { - $expectedUrl = '/modules/autoupgrade/views'; + $expectedAbsoluteUrlPathToShop = '/modules/autoupgrade/views'; $server = [ 'HTTP_HOST' => 'localhost', 'SERVER_PORT' => '80', @@ -84,7 +86,7 @@ public function testGetAssetsBaseUrlReturnsProductionUrl() $request = new Request([], [], [], [], [], $server); - $this->assertSame($expectedUrl, $this->assetsEnvironment->getAssetsBaseUrl($request)); + $this->assertSame($expectedAbsoluteUrlPathToShop, $this->assetsEnvironment->getAssetsBaseUrl($request)); } public function testGetAssetsBaseUrlReturnsProductionUrlWithShopInSubFolder() @@ -100,8 +102,8 @@ public function testGetAssetsBaseUrlReturnsProductionUrlWithShopInSubFolder() $request = new Request([], [], [], [], [], $server); - $expectedUrl = '/hello-world/modules/autoupgrade/views'; - $this->assertSame($expectedUrl, $this->assetsEnvironment->getAssetsBaseUrl($request)); + $expectedAbsoluteUrlPathToShop = '/hello-world/modules/autoupgrade/views'; + $this->assertSame($expectedAbsoluteUrlPathToShop, $this->assetsEnvironment->getAssetsBaseUrl($request)); } public function testGetAssetsBaseUrlReturnsProductionUrlWithCustomEntrypoint() @@ -117,8 +119,8 @@ public function testGetAssetsBaseUrlReturnsProductionUrlWithCustomEntrypoint() $request = new Request([], [], [], [], [], $server); - $expectedUrl = '/modules/autoupgrade/views'; - $this->assertSame($expectedUrl, $this->assetsEnvironment->getAssetsBaseUrl($request)); + $expectedAbsoluteUrlPathToShop = '/modules/autoupgrade/views'; + $this->assertSame($expectedAbsoluteUrlPathToShop, $this->assetsEnvironment->getAssetsBaseUrl($request)); } public function testGetAssetsBaseUrlReturnsProductionUrlWithShopInSubFolderAndParams() @@ -134,7 +136,7 @@ public function testGetAssetsBaseUrlReturnsProductionUrlWithShopInSubFolderAndPa $request = new Request([], [], [], [], [], $server); - $expectedUrl = '/hello-world/modules/autoupgrade/views'; - $this->assertSame($expectedUrl, $this->assetsEnvironment->getAssetsBaseUrl($request)); + $expectedAbsoluteUrlPathToShop = '/hello-world/modules/autoupgrade/views'; + $this->assertSame($expectedAbsoluteUrlPathToShop, $this->assetsEnvironment->getAssetsBaseUrl($request)); } } diff --git a/views/templates/layouts/error.html.twig b/views/templates/layouts/error.html.twig index 3b0cee274..b50c90208 100644 --- a/views/templates/layouts/error.html.twig +++ b/views/templates/layouts/error.html.twig @@ -1,23 +1,27 @@ -
-
- {% block img %} - {# Default image can go here, or it can be left empty so that children can override it #} - {% endblock %} +{% extends "@ModuleAutoUpgrade/layouts/page.html.twig" %} -
- {% block title %} - {# Default title can go here, or it can be left empty so that children can override it #} +{% block update_assistant %} +
+
+ {% block img %} + {# Default image can go here, or it can be left empty so that children can override it #} {% endblock %} - {% block description %} - {# Default description can go here, or it can be left empty so that children can override it #} - {% endblock %} +
+ {% block title %} + {# Default title can go here, or it can be left empty so that children can override it #} + {% endblock %} -
- {% block button %} - {# Default button can go here, or it can be left empty so that children can override it #} + {% block description %} + {# Default description can go here, or it can be left empty so that children can override it #} {% endblock %} + +
+ {% block button %} + {# Default button can go here, or it can be left empty so that children can override it #} + {% endblock %} +
-
+{% endblock %} diff --git a/views/templates/layouts/page.html.twig b/views/templates/layouts/page.html.twig index 0696c09fa..f556d284c 100644 --- a/views/templates/layouts/page.html.twig +++ b/views/templates/layouts/page.html.twig @@ -20,6 +20,6 @@
{% endblock %}
- -
{% endblock %} + +
diff --git a/views/templates/pages/errors/404.html.twig b/views/templates/pages/errors/404.html.twig index b07e37aa3..357345953 100644 --- a/views/templates/pages/errors/404.html.twig +++ b/views/templates/pages/errors/404.html.twig @@ -17,7 +17,13 @@ {% endblock %} {% block button %} - - {{ 'Go back to Update assistant'|trans({}) }} + + + {% endblock %}