Skip to content

Commit

Permalink
Allow URL based content-type guessing
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
f6p committed Jul 20, 2022
1 parent 975054b commit 98f6bc6
Show file tree
Hide file tree
Showing 7 changed files with 70 additions and 19 deletions.
5 changes: 3 additions & 2 deletions src/core/drive/navigator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<VisitOptions>): void
notifyApplicationAfterVisitingSamePageLocation(oldURL: URL, newURL: URL): void
}
Expand All @@ -25,7 +26,7 @@ export class Navigator {

proposeVisit(location: URL, options: Partial<VisitOptions> = {}) {
if (this.delegate.allowsVisitingLocationWithAction(location, options.action)) {
if (locationIsVisitable(location, this.view.snapshot.rootLocation)) {
if (this.delegate.locationIsVisitable(location, this.view.snapshot.rootLocation)) {
this.delegate.visitProposedToLocation(location, options)
} else {
window.location.href = location.toString()
Expand Down
4 changes: 2 additions & 2 deletions src/core/frames/frame_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { clearBusyState, dispatch, getAttribute, parseHTMLDocument, markAsBusy }
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 { LinkInterceptor, LinkInterceptorDelegate } from "./link_interceptor"
Expand Down Expand Up @@ -382,7 +382,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) {
Expand Down
9 changes: 6 additions & 3 deletions src/core/frames/frame_redirector.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { FormSubmitObserver, FormSubmitObserverDelegate } from "../../observers/form_submit_observer"
import { FrameElement } from "../../elements/frame_element"
import { LinkInterceptor, LinkInterceptorDelegate } from "./link_interceptor"
import { expandURL, getAction, locationIsVisitable } from "../url"
import { Session } from "../session"
import { expandURL, getAction } from "../url"

export class FrameRedirector implements LinkInterceptorDelegate, FormSubmitObserverDelegate {
readonly element: Element
readonly session: Session
readonly linkInterceptor: LinkInterceptor
readonly formSubmitObserver: FormSubmitObserver

constructor(element: Element) {
constructor(element: Element, session: Session) {
this.element = element
this.session = session
this.linkInterceptor = new LinkInterceptor(this, element)
this.formSubmitObserver = new FormSubmitObserver(this, element)
}
Expand Down Expand Up @@ -55,7 +58,7 @@ export class FrameRedirector implements LinkInterceptorDelegate, FormSubmitObser
const meta = this.element.ownerDocument.querySelector<HTMLMetaElement>(`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) {
Expand Down
16 changes: 12 additions & 4 deletions src/core/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { FormLinkInterceptor, FormLinkInterceptorDelegate } from "../observers/form_link_interceptor"
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"
Expand Down Expand Up @@ -58,7 +58,7 @@ export class Session
readonly scrollObserver = new ScrollObserver(this)
readonly streamObserver = new StreamObserver(this)
readonly formLinkInterceptor = new FormLinkInterceptor(this, document.documentElement)
readonly frameRedirector = new FrameRedirector(document.documentElement)
readonly frameRedirector = new FrameRedirector(document.documentElement, this)

drive = true
enabled = true
Expand Down Expand Up @@ -126,6 +126,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
}
Expand Down Expand Up @@ -176,7 +184,7 @@ export class Session
willFollowLinkToLocation(link: Element, location: URL, event: MouseEvent) {
return (
this.elementDriveEnabled(link) &&
locationIsVisitable(location, this.snapshot.rootLocation) &&
this.locationIsVisitable(location, this.snapshot.rootLocation) &&
this.applicationAllowsFollowingLinkToLocation(link, location, event)
)
}
Expand Down Expand Up @@ -224,7 +232,7 @@ export class Session
return (
this.elementDriveEnabled(form) &&
(!submitter || this.formElementDriveEnabled(submitter)) &&
locationIsVisitable(expandURL(action), this.snapshot.rootLocation)
this.locationIsVisitable(expandURL(action), this.snapshot.rootLocation)
)
}

Expand Down
8 changes: 0 additions & 8 deletions src/core/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions src/tests/fixtures/visitable.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Visitable</title>
<script src="/dist/turbo.es2017-umd.js" data-turbo-track="reload"></script>
<script src="/src/tests/fixtures/test.js"></script>
</head>
<body>
<h1>Visitable</h1>

<div>
<a id="link" href="/src/tests/fixtures/visitable.html">link</a>
</div>
</body>
</html>
31 changes: 31 additions & 0 deletions src/tests/functional/visitable_tests.ts
Original file line number Diff line number Diff line change
@@ -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")
})

0 comments on commit 98f6bc6

Please sign in to comment.