From da2ca9e6a536b4b6bf0043ad635bbeb215c0d655 Mon Sep 17 00:00:00 2001 From: Filip Pyda Date: Fri, 28 Jan 2022 09:23:05 +0100 Subject: [PATCH] Allow URL based content-type guessing Add `Session.isVisitable` method whitch is equivalent of no longer accessible `URL.isHTML`. This allows `Turbo.session.isVisitable` to be overriden by user and makes possible navigation on URL's other than those with trailing slash or HTML extenion. --- src/core/drive/navigator.ts | 5 ++-- src/core/frames/frame_controller.ts | 4 ++-- src/core/frames/frame_redirector.ts | 4 ++-- src/core/session.ts | 16 +++++++++---- src/core/url.ts | 8 ------- src/tests/fixtures/visitable.html | 16 +++++++++++++ src/tests/functional/visitable_tests.ts | 31 +++++++++++++++++++++++++ 7 files changed, 66 insertions(+), 18 deletions(-) create mode 100644 src/tests/fixtures/visitable.html create mode 100644 src/tests/functional/visitable_tests.ts diff --git a/src/core/drive/navigator.ts b/src/core/drive/navigator.ts index e52e63b92..b4c5898cc 100644 --- a/src/core/drive/navigator.ts +++ b/src/core/drive/navigator.ts @@ -2,13 +2,14 @@ import { Action, isAction } from "../types" import { FetchMethod } from "../../http/fetch_request" import { FetchResponse } from "../../http/fetch_response" import { FormSubmission } from "./form_submission" -import { expandURL, getAnchor, getRequestURL, Locatable, locationIsVisitable } from "../url" +import { expandURL, getAnchor, getRequestURL, Locatable } from "../url" import { getAttribute } from "../../util" import { Visit, VisitDelegate, VisitOptions } from "./visit" import { PageSnapshot } from "./page_snapshot" export type NavigatorDelegate = VisitDelegate & { allowsVisitingLocationWithAction(location: URL, action?: Action): boolean + locationIsVisitable(location: URL, rootLocation: URL): boolean visitProposedToLocation(location: URL, options: Partial): Promise notifyApplicationAfterVisitingSamePageLocation(oldURL: URL, newURL: URL): void } @@ -25,7 +26,7 @@ export class Navigator { proposeVisit(location: URL, options: Partial = {}) { if (this.delegate.allowsVisitingLocationWithAction(location, options.action)) { - if (locationIsVisitable(location, this.view.snapshot.rootLocation)) { + if (this.delegate.locationIsVisitable(location, this.view.snapshot.rootLocation)) { return this.delegate.visitProposedToLocation(location, options) } else { window.location.href = location.toString() diff --git a/src/core/frames/frame_controller.ts b/src/core/frames/frame_controller.ts index d5ddd45fe..901bb4fd7 100644 --- a/src/core/frames/frame_controller.ts +++ b/src/core/frames/frame_controller.ts @@ -20,7 +20,7 @@ import { import { FormSubmission, FormSubmissionDelegate } from "../drive/form_submission" import { Snapshot } from "../snapshot" import { ViewDelegate, ViewRenderOptions } from "../view" -import { getAction, expandURL, urlsAreEqual, locationIsVisitable } from "../url" +import { getAction, expandURL, urlsAreEqual } from "../url" import { FormSubmitObserver, FormSubmitObserverDelegate } from "../../observers/form_submit_observer" import { FrameView } from "./frame_view" import { LinkClickObserver, LinkClickObserverDelegate } from "../../observers/link_click_observer" @@ -418,7 +418,7 @@ export class FrameController private formActionIsVisitable(form: HTMLFormElement, submitter?: HTMLElement) { const action = getAction(form, submitter) - return locationIsVisitable(expandURL(action), this.rootLocation) + return session.locationIsVisitable(expandURL(action), this.rootLocation) } private shouldInterceptNavigation(element: Element, submitter?: HTMLElement) { diff --git a/src/core/frames/frame_redirector.ts b/src/core/frames/frame_redirector.ts index 8019f01ad..6f519336a 100644 --- a/src/core/frames/frame_redirector.ts +++ b/src/core/frames/frame_redirector.ts @@ -1,6 +1,6 @@ import { FormSubmitObserver, FormSubmitObserverDelegate } from "../../observers/form_submit_observer" import { FrameElement } from "../../elements/frame_element" -import { expandURL, getAction, locationIsVisitable } from "../url" +import { expandURL, getAction } from "../url" import { LinkClickObserver, LinkClickObserverDelegate } from "../../observers/link_click_observer" import { Session } from "../session" @@ -58,7 +58,7 @@ export class FrameRedirector implements LinkClickObserverDelegate, FormSubmitObs const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`) const rootLocation = expandURL(meta?.content ?? "/") - return this.shouldRedirect(form, submitter) && locationIsVisitable(action, rootLocation) + return this.shouldRedirect(form, submitter) && this.session.locationIsVisitable(action, rootLocation) } private shouldRedirect(element: Element, submitter?: HTMLElement) { diff --git a/src/core/session.ts b/src/core/session.ts index 992f49deb..211d60b5d 100644 --- a/src/core/session.ts +++ b/src/core/session.ts @@ -6,7 +6,7 @@ import { FrameRedirector } from "./frames/frame_redirector" import { History, HistoryDelegate } from "./drive/history" import { LinkClickObserver, LinkClickObserverDelegate } from "../observers/link_click_observer" import { FormLinkClickObserver, FormLinkClickObserverDelegate } from "../observers/form_link_click_observer" -import { getAction, expandURL, locationIsVisitable, Locatable } from "./url" +import { getAction, getExtension, expandURL, isPrefixedBy, Locatable } from "./url" import { Navigator, NavigatorDelegate } from "./drive/navigator" import { PageObserver, PageObserverDelegate } from "../observers/page_observer" import { ScrollObserver } from "../observers/scroll_observer" @@ -134,6 +134,14 @@ export class Session this.view.clearSnapshotCache() } + isVisitable(url: URL) { + return !!getExtension(url).match(/^(?:|\.(?:htm|html|xhtml|php))$/) + } + + locationIsVisitable(location: URL, rootLocation: URL) { + return isPrefixedBy(location, rootLocation) && this.isVisitable(location) + } + setProgressBarDelay(delay: number) { this.progressBarDelay = delay } @@ -174,7 +182,7 @@ export class Session // Form click observer delegate willSubmitFormLinkToLocation(link: Element, location: URL): boolean { - return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation) + return this.elementIsNavigatable(link) && this.locationIsVisitable(location, this.snapshot.rootLocation) } submittedFormLinkToLocation() {} @@ -184,7 +192,7 @@ export class Session willFollowLinkToLocation(link: Element, location: URL, event: MouseEvent) { return ( this.elementIsNavigatable(link) && - locationIsVisitable(location, this.snapshot.rootLocation) && + this.locationIsVisitable(location, this.snapshot.rootLocation) && this.applicationAllowsFollowingLinkToLocation(link, location, event) ) } @@ -233,7 +241,7 @@ export class Session return ( this.submissionIsNavigatable(form, submitter) && - locationIsVisitable(expandURL(action), this.snapshot.rootLocation) + this.locationIsVisitable(expandURL(action), this.snapshot.rootLocation) ) } diff --git a/src/core/url.ts b/src/core/url.ts index 0e45d8f2b..5b68ecad4 100644 --- a/src/core/url.ts +++ b/src/core/url.ts @@ -24,19 +24,11 @@ export function getExtension(url: URL) { return (getLastPathComponent(url).match(/\.[^.]*$/) || [])[0] || "" } -export function isHTML(url: URL) { - return !!getExtension(url).match(/^(?:|\.(?:htm|html|xhtml|php))$/) -} - export function isPrefixedBy(baseURL: URL, url: URL) { const prefix = getPrefix(url) return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix) } -export function locationIsVisitable(location: URL, rootLocation: URL) { - return isPrefixedBy(location, rootLocation) && isHTML(location) -} - export function getRequestURL(url: URL) { const anchor = getAnchor(url) return anchor != null ? url.href.slice(0, -(anchor.length + 1)) : url.href diff --git a/src/tests/fixtures/visitable.html b/src/tests/fixtures/visitable.html new file mode 100644 index 000000000..659a49e2a --- /dev/null +++ b/src/tests/fixtures/visitable.html @@ -0,0 +1,16 @@ + + + + + Visitable + + + + +

Visitable

+ +
+ link +
+ + diff --git a/src/tests/functional/visitable_tests.ts b/src/tests/functional/visitable_tests.ts new file mode 100644 index 000000000..0967185d3 --- /dev/null +++ b/src/tests/functional/visitable_tests.ts @@ -0,0 +1,31 @@ +import { test } from "@playwright/test" +import { assert } from "chai" +import { nextBody, pathname, visitAction } from "../helpers/page" + +const path = "/src/tests/fixtures/visitable.html" + +test.beforeEach(async ({ page }) => { + await page.goto(path) +}) + +test("test user-defined visitable URL", async ({ page }) => { + await page.evaluate(() => { + window.Turbo.session.isVisitable = (_url) => true + }) + + page.click("#link") + await nextBody(page) + assert.equal(pathname(page.url()), path) + assert.equal(await visitAction(page), "advance") +}) + +test("test user-defined unvisitable URL", async ({ page }) => { + await page.evaluate(() => { + window.Turbo.session.isVisitable = (_url) => false + }) + + page.click("#link") + await nextBody(page) + assert.equal(pathname(page.url()), path) + assert.equal(await visitAction(page), "load") +})