mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
fix(shell): reload the packaged renderer back onto its in-app route (#1622)
The packaged renderer is index.html over file:// with path routing, so after in-app navigation the document URL names a path with no file behind it. A main-process reload (macOS View > Reload, DevTools) failed with ERR_FILE_NOT_FOUND and stranded the window on Chromium's error page; a renderer-initiated reload (the settings unsaved-changes guard's confirmed location.reload()) was cancelled by the will-navigate trust check and silently did nothing. Both legs now re-load the packaged index with the route in a restoreRoute query parameter, which main.ts restores with history.replaceState before Angular bootstraps. The did-fail-load recovery is deferred to the error page's dom-ready: a load issued from inside the failure event yields a document that never receives animation frames and never paints. Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
fa8ce26991
commit
6f987d79d8
14 files changed
+1113
-12
No files matched your search
@@ -47,6 +47,7 @@ export * from './lib/provider-overview.util';
|
||||
export * from './lib/random-id.util';
|
||||
export * from './lib/recording-metadata.interface';
|
||||
export * from './lib/recording-program-overlap.util';
|
||||
export * from './lib/renderer-reload-route.util';
|
||||
export * from './lib/security-policy-error.utils';
|
||||
export * from './lib/settings.interface';
|
||||
export * from './lib/stalker-auth-failure.util';
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
RENDERER_RESTORE_ROUTE_QUERY_PARAM,
|
||||
resolveRestoredRendererRoute,
|
||||
} from './renderer-reload-route.util';
|
||||
|
||||
const rendererRoot = 'file:///Applications/IPTVnator.app/Contents/web/';
|
||||
const packagedIndex = `${rendererRoot}index.html`;
|
||||
|
||||
function reloadedIndex(route: string): string {
|
||||
const url = new URL(packagedIndex);
|
||||
url.searchParams.set(RENDERER_RESTORE_ROUTE_QUERY_PARAM, route);
|
||||
return url.href;
|
||||
}
|
||||
|
||||
describe('resolveRestoredRendererRoute', () => {
|
||||
it('leaves a document without a restore request alone', () => {
|
||||
expect(resolveRestoredRendererRoute(packagedIndex, packagedIndex)).toBe(
|
||||
null
|
||||
);
|
||||
expect(
|
||||
resolveRestoredRendererRoute(
|
||||
`${packagedIndex}?other=1`,
|
||||
packagedIndex
|
||||
)
|
||||
).toBe(null);
|
||||
});
|
||||
|
||||
it('restores a route relative to the renderer directory', () => {
|
||||
expect(
|
||||
resolveRestoredRendererRoute(
|
||||
reloadedIndex('workspace/sources'),
|
||||
packagedIndex
|
||||
)
|
||||
).toBe(`${rendererRoot}workspace/sources`);
|
||||
});
|
||||
|
||||
it('keeps the restored route query and fragment', () => {
|
||||
expect(
|
||||
resolveRestoredRendererRoute(
|
||||
reloadedIndex('workspace/xtreams/3/search?q=dune#top'),
|
||||
packagedIndex
|
||||
)
|
||||
).toBe(`${rendererRoot}workspace/xtreams/3/search?q=dune#top`);
|
||||
});
|
||||
|
||||
it('resolves against the base URI, not the document URL', () => {
|
||||
// The packaged <base href="./"> resolves to the renderer directory
|
||||
// even when the document itself sits deeper.
|
||||
expect(
|
||||
resolveRestoredRendererRoute(
|
||||
`${rendererRoot}workspace/sources?${RENDERER_RESTORE_ROUTE_QUERY_PARAM}=workspace%2Fdashboard`,
|
||||
rendererRoot
|
||||
)
|
||||
).toBe(`${rendererRoot}workspace/dashboard`);
|
||||
});
|
||||
|
||||
it('works for an http origin with a root base href', () => {
|
||||
expect(
|
||||
resolveRestoredRendererRoute(
|
||||
`http://localhost:4200/?${RENDERER_RESTORE_ROUTE_QUERY_PARAM}=workspace%2Fsettings%2Fplayback`,
|
||||
'http://localhost:4200/'
|
||||
)
|
||||
).toBe('http://localhost:4200/workspace/settings/playback');
|
||||
});
|
||||
|
||||
it.each([
|
||||
['an absolute URL', 'https://example.com/phish'],
|
||||
['another scheme', 'javascript:alert(1)'],
|
||||
['a directory escape', '../../etc/passwd'],
|
||||
['a root-absolute path outside the renderer', '/etc/passwd'],
|
||||
['a scheme-relative URL', '//example.com/'],
|
||||
['the renderer directory itself', './'],
|
||||
['an empty route', ''],
|
||||
])('drops %s and only removes the parameter', (_label, route) => {
|
||||
expect(
|
||||
resolveRestoredRendererRoute(reloadedIndex(route), packagedIndex)
|
||||
).toBe(packagedIndex);
|
||||
});
|
||||
|
||||
it('returns null for an unparsable document URL', () => {
|
||||
expect(resolveRestoredRendererRoute('not a url', packagedIndex)).toBe(
|
||||
null
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Route hand-off for a reloaded packaged renderer.
|
||||
*
|
||||
* The packaged renderer is loaded from `dist/apps/web/index.html` over
|
||||
* `file://` and Angular uses PATH routing, so after in-app navigation the
|
||||
* document URL is `file:///…/web/workspace/sources` — a path with no file
|
||||
* behind it. A reload (the macOS View › Reload menu, DevTools, the settings
|
||||
* unsaved-changes guard's confirmed reload) therefore fails with
|
||||
* `ERR_FILE_NOT_FOUND` and strands the window on Chromium's error page.
|
||||
*
|
||||
* The Electron main process recovers such a failure by loading `index.html`
|
||||
* again with the failed URL's route (its path relative to the renderer root,
|
||||
* plus query and fragment) carried in this query parameter. The renderer
|
||||
* consumes it before Angular bootstraps: `resolveRestoredRendererRoute`
|
||||
* turns the current document URL into the in-app URL the router should
|
||||
* start from, and `main.ts` installs it with `history.replaceState`, so the
|
||||
* router's initial navigation lands on the route the user was on.
|
||||
*/
|
||||
|
||||
/** Query parameter carrying the route to restore on the reloaded index. */
|
||||
export const RENDERER_RESTORE_ROUTE_QUERY_PARAM = 'restoreRoute';
|
||||
|
||||
/**
|
||||
* The URL to present instead of `currentHref` before the router's initial
|
||||
* navigation, or `null` when the document carries no restore request.
|
||||
*
|
||||
* `baseUri` is `document.baseURI`: the packaged build's `<base href="./">`
|
||||
* resolves to the renderer's directory, which is also what Angular strips
|
||||
* from `location.pathname` to obtain the route. A route that would leave
|
||||
* that directory — an absolute URL, another scheme, a `..` escape — is
|
||||
* dropped and only the parameter is removed, so the app boots at its
|
||||
* default route rather than following an arbitrary target.
|
||||
*/
|
||||
export function resolveRestoredRendererRoute(
|
||||
currentHref: string,
|
||||
baseUri: string
|
||||
): string | null {
|
||||
let current: URL;
|
||||
let baseDirectory: URL;
|
||||
|
||||
try {
|
||||
current = new URL(currentHref);
|
||||
baseDirectory = new URL('./', baseUri);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
const route = current.searchParams.get(RENDERER_RESTORE_ROUTE_QUERY_PARAM);
|
||||
|
||||
if (route === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
current.searchParams.delete(RENDERER_RESTORE_ROUTE_QUERY_PARAM);
|
||||
const stripped = current.href;
|
||||
|
||||
let target: URL;
|
||||
|
||||
try {
|
||||
target = new URL(route, baseDirectory);
|
||||
} catch {
|
||||
return stripped;
|
||||
}
|
||||
|
||||
const staysInsideRenderer =
|
||||
target.protocol === baseDirectory.protocol &&
|
||||
target.host === baseDirectory.host &&
|
||||
target.pathname.startsWith(baseDirectory.pathname) &&
|
||||
target.pathname !== baseDirectory.pathname;
|
||||
|
||||
return staysInsideRenderer ? target.href : stripped;
|
||||
}
|
||||
Reference in new issue
Block a user