From eb04552d222762de2153cb37c38dbc71275809f3 Mon Sep 17 00:00:00 2001 From: Arpit Chakladar Date: Mon, 13 Jul 2026 23:52:20 +0530 Subject: feat: added responders to JQuery ajax post and get requests for some request we may want to prevent the request entirely and instead have the extension give a custom response (for caching for example). --- src/shared/intercept-jquery-ajax.ts | 134 +++++++++++++++++++++++++++++++----- 1 file changed, 118 insertions(+), 16 deletions(-) diff --git a/src/shared/intercept-jquery-ajax.ts b/src/shared/intercept-jquery-ajax.ts index b627d0e..03ea5e0 100644 --- a/src/shared/intercept-jquery-ajax.ts +++ b/src/shared/intercept-jquery-ajax.ts @@ -4,61 +4,131 @@ export type JQueryAjaxArgs = [ callback: (data: unknown, textStatus: string, jqXHR: JQuery.jqXHR) => void, dataType?: string ]; - type JQueryAjaxInterceptor = (args: JQueryAjaxArgs) => void; - type InterceptorRegistration = { urlSuffix: string; interceptor: JQueryAjaxInterceptor }; - const postInterceptors: InterceptorRegistration[] = []; const getInterceptors: InterceptorRegistration[] = []; +/** Data a responder wants to resolve the "request" with, instead of hitting the network. */ +type JQueryAjaxResponse = { + data: unknown; + textStatus?: string +}; + +/** + * A responder inspects the outgoing args and optionally returns a response. + * Returning `undefined` means "not handled, let the real request (or the next + * responder) proceed." + */ +type JQueryAjaxResponder = (args: JQueryAjaxArgs) => JQueryAjaxResponse | undefined; + +type ResponderRegistration = { + urlSuffix: string; + responder: JQueryAjaxResponder +}; +const postResponders: ResponderRegistration[] = []; +const getResponders: ResponderRegistration[] = []; + // Define a strict signature to replace the unsafe `Function` type type OriginalJQueryAjaxMethod = (...args: unknown[]) => JQuery.jqXHR; +/** + * Extracts the request URL from `$.post`/`$.get` args, whether called with a + * plain string URL or a settings object. + */ +function extractUrl(args: unknown[]): string | null { + const firstArg = args[0]; + if (typeof firstArg === "string") return firstArg; + if (firstArg && typeof firstArg === "object" && "url" in firstArg && typeof firstArg.url === "string") { + return firstArg.url; + } + return null; +} + +/** + * Checks registered responders for a match. If one matches and returns a + * response, synthesizes a resolved jqXHR-like object, invokes the caller's + * own success callback (3rd positional arg) to preserve `$.post`/`$.get` + * shorthand semantics, and returns it so the real ajax call can be skipped. + * Returns `null` if no responder handled the request. + */ +function tryRespond(responders: ResponderRegistration[], args: unknown[]): JQuery.jqXHR | null { + const requestUrl = extractUrl(args); + if (typeof requestUrl !== "string") return null; + + for (const { urlSuffix, responder } of responders) { + if (!requestUrl.endsWith(urlSuffix)) continue; + + const response = responder(args as unknown as JQueryAjaxArgs); + if (response === undefined) continue; + + const { data, textStatus = "success" } = response; + const deferred = $.Deferred(); + const jqXHR = deferred.promise() as unknown as JQuery.jqXHR; + + const callback = args[2]; + if (typeof callback === "function") { + (callback as JQueryAjaxArgs[2])(data, textStatus, jqXHR); + } + deferred.resolve(data, textStatus, jqXHR); + + return jqXHR; + } + return null; +} + /** * On DOMContentLoaded, proxies jQuery's `$.post` and `$.get` so that - * registered interceptors are called before the real request. + * registered interceptors are called before the real request, and + * registered responders get a chance to short-circuit the request entirely + * by resolving it with synthetic data. */ document.addEventListener("DOMContentLoaded", function() { // 1. Keep the original methods bound to the jQuery ($) object const originalPost = $.post.bind($); const originalGet = $.get.bind($); - /** Proxied `$.post` that invokes registered interceptors before the real call. */ + /** + * Proxied `$.post` that invokes registered interceptors before the real + * call, then checks registered responders for a short-circuit before + * falling back to the real `$.post`. + */ $.post = function(this: typeof $, ...args: unknown[]): JQuery.jqXHR { for (const { urlSuffix, interceptor } of postInterceptors) { // Safely inspect jQuery settings objects without using "any" - const firstArg = args[0]; - const requestUrl = typeof firstArg === "string" - ? firstArg - : (firstArg && typeof firstArg === "object" && "url" in firstArg && typeof firstArg.url === "string" ? firstArg.url : null); - + const requestUrl = extractUrl(args); if (typeof requestUrl === "string" && requestUrl.endsWith(urlSuffix)) { // Cast the unknown array to the expected tuple type interceptor(args as unknown as JQueryAjaxArgs); } } + const shortCircuited = tryRespond(postResponders, args); + if (shortCircuited) return shortCircuited; + // Use our strict signature instead of `Function` to satisfy ESLint return (originalPost as unknown as OriginalJQueryAjaxMethod)(...args); } as typeof $.post; // Reassert the original type so external consumers see the normal signature - /** Proxied `$.get` that invokes registered interceptors before the real call. */ + /** + * Proxied `$.get` that invokes registered interceptors before the real + * call, then checks registered responders for a short-circuit before + * falling back to the real `$.get`. + */ $.get = function(this: typeof $, ...args: unknown[]): JQuery.jqXHR { for (const { urlSuffix, interceptor } of getInterceptors) { - const firstArg = args[0]; - const requestUrl = typeof firstArg === "string" - ? firstArg - : (firstArg && typeof firstArg === "object" && "url" in firstArg && typeof firstArg.url === "string" ? firstArg.url : null); - + const requestUrl = extractUrl(args); if (typeof requestUrl === "string" && requestUrl.endsWith(urlSuffix)) { interceptor(args as unknown as JQueryAjaxArgs); } } + const shortCircuited = tryRespond(getResponders, args); + if (shortCircuited) return shortCircuited; + return (originalGet as unknown as OriginalJQueryAjaxMethod)(...args); } as typeof $.get; }); @@ -84,3 +154,35 @@ export function addPostInterceptor(urlSuffix: string, interceptor: JQueryAjaxInt export function addGetInterceptor(urlSuffix: string, interceptor: JQueryAjaxInterceptor): void { getInterceptors.push({ urlSuffix, interceptor }); } + +/** + * Registers a responder that can conditionally short-circuit a matching + * `$.post` call, resolving it with synthetic data instead of making the + * real network request. + * + * @param urlSuffix - The URL suffix to match (checked via `endsWith`). + * @param responder - Inspects the original `arguments` from `$.post` and + * either returns a `{ data, textStatus? }` response to + * short-circuit the request, or `undefined` to let the + * request proceed (checking the next responder, or + * falling through to the real `$.post`). + */ +export function addPostResponder(urlSuffix: string, responder: JQueryAjaxResponder): void { + postResponders.push({ urlSuffix, responder }); +} + +/** + * Registers a responder that can conditionally short-circuit a matching + * `$.get` call, resolving it with synthetic data instead of making the + * real network request. + * + * @param urlSuffix - The URL suffix to match (checked via `endsWith`). + * @param responder - Inspects the original `arguments` from `$.get` and + * either returns a `{ data, textStatus? }` response to + * short-circuit the request, or `undefined` to let the + * request proceed (checking the next responder, or + * falling through to the real `$.get`). + */ +export function addGetResponder(urlSuffix: string, responder: JQueryAjaxResponder): void { + getResponders.push({ urlSuffix, responder }); +} -- cgit v1.2.3 From 5c0cf8a22a04fae0fc6700a01007f10e833bc2cf Mon Sep 17 00:00:00 2001 From: Arpit Chakladar Date: Tue, 14 Jul 2026 00:07:01 +0530 Subject: feat: caching and displaying the login form The login/signin form is fetched from the server everytime sign in is clicked. This on top of the fact that the server is already notoriously slow makes the time for the login form to appear quite long. This fixes it by caching the response html of the login form the first time and then reusing it in successive login attempts. --- src/rules/block-scripts.json | 2 +- src/scripts.json | 3 +- src/scripts/common/login/form-caching.ts | 22 +++++ src/scripts/common/login/index.ts | 1 - src/shared/intercept-jquery-ajax.ts | 155 +++++++++++++++++++++++++++---- src/shared/script-injector.ts | 2 +- 6 files changed, 164 insertions(+), 21 deletions(-) create mode 100644 src/scripts/common/login/form-caching.ts delete mode 100644 src/scripts/common/login/index.ts diff --git a/src/rules/block-scripts.json b/src/rules/block-scripts.json index c0f6c02..0837f7d 100644 --- a/src/rules/block-scripts.json +++ b/src/rules/block-scripts.json @@ -4,6 +4,6 @@ "type": "block" }, "condition": { - "regexFilter": "^https:\/\/banglarbhumi.gov.in\/BanglarBhumi\/js\/(jquery-1.12.4|bootstrap-dropdownhover).js$" + "regexFilter": "^https:\/\/banglarbhumi.gov.in\/BanglarBhumi\/(js\/(jquery-1.12.4|bootstrap-dropdownhover).js|images\/Bhumi2.gif)$" } } diff --git a/src/scripts.json b/src/scripts.json index fb6a0ee..9b4f76b 100644 --- a/src/scripts.json +++ b/src/scripts.json @@ -1,6 +1,7 @@ { "injected": { "common/stop-blocking.ts": "*", + "common/login/form-caching.ts": "*", "pages/know-your-property/index.ts": "KnowYourProperty.action", "pages/application-receipt/index.ts": "Apprecrep.action", "pages/mutation-application/index.ts": "MuteApplication.action", @@ -8,6 +9,6 @@ "pages/sheet-map/index.ts": "SheetMapViewAction.action" }, "after": { - "common/login/index.ts": "*" + "common/login/captcha.ts": "*" } } diff --git a/src/scripts/common/login/form-caching.ts b/src/scripts/common/login/form-caching.ts new file mode 100644 index 0000000..bde6442 --- /dev/null +++ b/src/scripts/common/login/form-caching.ts @@ -0,0 +1,22 @@ +import { addPostListener, addPostResponder, JQueryAjaxResponse } from "@/shared/intercept-jquery-ajax"; + +let loginAreaHTML: string | null = null; + +addPostResponder( + "viewLoginAreaAction", + function(_args): JQueryAjaxResponse | undefined | null { + return loginAreaHTML + ? { + data: loginAreaHTML + } + : null; + } +); + +addPostListener( + "viewLoginAreaAction", + function(_args, result): void { + loginAreaHTML = result.data?.toString() ?? ""; + }, + false +); diff --git a/src/scripts/common/login/index.ts b/src/scripts/common/login/index.ts deleted file mode 100644 index 95b9c5b..0000000 --- a/src/scripts/common/login/index.ts +++ /dev/null @@ -1 +0,0 @@ -import "@/scripts/common/login/captcha"; diff --git a/src/shared/intercept-jquery-ajax.ts b/src/shared/intercept-jquery-ajax.ts index 03ea5e0..3215c43 100644 --- a/src/shared/intercept-jquery-ajax.ts +++ b/src/shared/intercept-jquery-ajax.ts @@ -13,7 +13,7 @@ const postInterceptors: InterceptorRegistration[] = []; const getInterceptors: InterceptorRegistration[] = []; /** Data a responder wants to resolve the "request" with, instead of hitting the network. */ -type JQueryAjaxResponse = { +export type JQueryAjaxResponse = { data: unknown; textStatus?: string }; @@ -23,7 +23,7 @@ type JQueryAjaxResponse = { * Returning `undefined` means "not handled, let the real request (or the next * responder) proceed." */ -type JQueryAjaxResponder = (args: JQueryAjaxArgs) => JQueryAjaxResponse | undefined; +type JQueryAjaxResponder = (args: JQueryAjaxArgs) => JQueryAjaxResponse | undefined | null; type ResponderRegistration = { urlSuffix: string; @@ -32,6 +32,30 @@ type ResponderRegistration = { const postResponders: ResponderRegistration[] = []; const getResponders: ResponderRegistration[] = []; +/** The finalized outcome of a request, whether it came from the network (success or failure) or a responder. */ +export type JQueryAjaxResult = { + data: unknown; + textStatus: string; + jqXHR: JQuery.jqXHR +}; + +/** + * A listener fires once a request settles — on success or failure alike — + * and always receives whatever response data is available. Unlike + * interceptors (which see the outgoing request) and responders (which can + * short-circuit it), listeners only observe the final result. + */ +type JQueryAjaxListener = (args: JQueryAjaxArgs, result: JQueryAjaxResult) => void; + +type ListenerRegistration = { + urlSuffix: string; + listener: JQueryAjaxListener; + /** Whether a responder-short-circuited response should also trigger this listener. */ + includeResponderResponses: boolean +}; +const postListeners: ListenerRegistration[] = []; +const getListeners: ListenerRegistration[] = []; + // Define a strict signature to replace the unsafe `Function` type type OriginalJQueryAjaxMethod = (...args: unknown[]) => JQuery.jqXHR; @@ -48,14 +72,41 @@ function extractUrl(args: unknown[]): string | null { return null; } +/** + * Invokes any registered listeners whose `urlSuffix` matches the request URL. + * `fromResponder` indicates whether this result came from a short-circuited + * responder rather than a genuine network response, so listeners registered + * with `includeResponderResponses: false` can be skipped. + */ +function runListeners( + listeners: ListenerRegistration[], + args: unknown[], + result: JQueryAjaxResult, + fromResponder: boolean +): void { + const requestUrl = extractUrl(args); + if (typeof requestUrl !== "string") return; + + for (const { urlSuffix, listener, includeResponderResponses } of listeners) { + if (!requestUrl.endsWith(urlSuffix)) continue; + if (fromResponder && !includeResponderResponses) continue; + listener(args as unknown as JQueryAjaxArgs, result); + } +} + /** * Checks registered responders for a match. If one matches and returns a * response, synthesizes a resolved jqXHR-like object, invokes the caller's * own success callback (3rd positional arg) to preserve `$.post`/`$.get` - * shorthand semantics, and returns it so the real ajax call can be skipped. - * Returns `null` if no responder handled the request. + * shorthand semantics, fires any matching listeners, and returns the jqXHR + * so the real ajax call can be skipped. Returns `null` if no responder + * handled the request. */ -function tryRespond(responders: ResponderRegistration[], args: unknown[]): JQuery.jqXHR | null { +function tryRespond( + responders: ResponderRegistration[], + listeners: ListenerRegistration[], + args: unknown[] +): JQuery.jqXHR | null { const requestUrl = extractUrl(args); if (typeof requestUrl !== "string") return null; @@ -63,7 +114,7 @@ function tryRespond(responders: ResponderRegistration[], args: unknown[]): JQuer if (!requestUrl.endsWith(urlSuffix)) continue; const response = responder(args as unknown as JQueryAjaxArgs); - if (response === undefined) continue; + if (!response) continue; const { data, textStatus = "success" } = response; const deferred = $.Deferred(); @@ -75,6 +126,8 @@ function tryRespond(responders: ResponderRegistration[], args: unknown[]): JQuer } deferred.resolve(data, textStatus, jqXHR); + runListeners(listeners, args, { data, textStatus, jqXHR }, /* fromResponder */ true); + return jqXHR; } return null; @@ -82,9 +135,11 @@ function tryRespond(responders: ResponderRegistration[], args: unknown[]): JQuer /** * On DOMContentLoaded, proxies jQuery's `$.post` and `$.get` so that - * registered interceptors are called before the real request, and - * registered responders get a chance to short-circuit the request entirely - * by resolving it with synthetic data. + * registered interceptors are called before the real request, registered + * responders get a chance to short-circuit the request entirely by + * resolving it with synthetic data, and registered listeners are notified + * once the request settles (whether real or synthesized, success or + * failure). */ document.addEventListener("DOMContentLoaded", function() { // 1. Keep the original methods bound to the jQuery ($) object @@ -93,8 +148,9 @@ document.addEventListener("DOMContentLoaded", function() { /** * Proxied `$.post` that invokes registered interceptors before the real - * call, then checks registered responders for a short-circuit before - * falling back to the real `$.post`. + * call, checks registered responders for a short-circuit, and otherwise + * falls back to the real `$.post`, notifying listeners once the request + * settles either way. */ $.post = function(this: typeof $, ...args: unknown[]): JQuery.jqXHR { for (const { urlSuffix, interceptor } of postInterceptors) { @@ -106,17 +162,30 @@ document.addEventListener("DOMContentLoaded", function() { } } - const shortCircuited = tryRespond(postResponders, args); + const shortCircuited = tryRespond(postResponders, postListeners, args); if (shortCircuited) return shortCircuited; // Use our strict signature instead of `Function` to satisfy ESLint - return (originalPost as unknown as OriginalJQueryAjaxMethod)(...args); + const jqXHR = (originalPost as unknown as OriginalJQueryAjaxMethod)(...args); + jqXHR.done((data: unknown, textStatus: string, doneJqXHR: JQuery.jqXHR) => { + runListeners(postListeners, args, { data, textStatus, jqXHR: doneJqXHR }, /* fromResponder */ false); + }); + jqXHR.fail((failJqXHR: JQuery.jqXHR, textStatus: string) => { + runListeners( + postListeners, + args, + { data: failJqXHR.responseJSON ?? failJqXHR.responseText, textStatus, jqXHR: failJqXHR }, + /* fromResponder */ false + ); + }); + return jqXHR; } as typeof $.post; // Reassert the original type so external consumers see the normal signature /** * Proxied `$.get` that invokes registered interceptors before the real - * call, then checks registered responders for a short-circuit before - * falling back to the real `$.get`. + * call, checks registered responders for a short-circuit, and otherwise + * falls back to the real `$.get`, notifying listeners once the request + * settles either way. */ $.get = function(this: typeof $, ...args: unknown[]): JQuery.jqXHR { for (const { urlSuffix, interceptor } of getInterceptors) { @@ -126,10 +195,22 @@ document.addEventListener("DOMContentLoaded", function() { } } - const shortCircuited = tryRespond(getResponders, args); + const shortCircuited = tryRespond(getResponders, getListeners, args); if (shortCircuited) return shortCircuited; - return (originalGet as unknown as OriginalJQueryAjaxMethod)(...args); + const jqXHR = (originalGet as unknown as OriginalJQueryAjaxMethod)(...args); + jqXHR.done((data: unknown, textStatus: string, doneJqXHR: JQuery.jqXHR) => { + runListeners(getListeners, args, { data, textStatus, jqXHR: doneJqXHR }, /* fromResponder */ false); + }); + jqXHR.fail((failJqXHR: JQuery.jqXHR, textStatus: string) => { + runListeners( + getListeners, + args, + { data: failJqXHR.responseJSON ?? failJqXHR.responseText, textStatus, jqXHR: failJqXHR }, + /* fromResponder */ false + ); + }); + return jqXHR; } as typeof $.get; }); @@ -186,3 +267,43 @@ export function addPostResponder(urlSuffix: string, responder: JQueryAjaxRespond export function addGetResponder(urlSuffix: string, responder: JQueryAjaxResponder): void { getResponders.push({ urlSuffix, responder }); } + +/** + * Registers a listener that fires once a matching `$.post` request settles + * — success or failure alike — receiving the original request args plus + * whatever response data, `textStatus`, and `jqXHR` are available. + * + * @param urlSuffix - The URL suffix to match (checked via `endsWith`). + * @param listener - Receives the original `arguments` from `$.post` + * plus the result. + * @param includeResponderResponses - Whether a responder-short-circuited response + * should also trigger this listener, as opposed + * to only genuine network responses. Defaults to `true`. + */ +export function addPostListener( + urlSuffix: string, + listener: JQueryAjaxListener, + includeResponderResponses = true +): void { + postListeners.push({ urlSuffix, listener, includeResponderResponses }); +} + +/** + * Registers a listener that fires once a matching `$.get` request settles + * — success or failure alike — receiving the original request args plus + * whatever response data, `textStatus`, and `jqXHR` are available. + * + * @param urlSuffix - The URL suffix to match (checked via `endsWith`). + * @param listener - Receives the original `arguments` from `$.get` + * plus the result. + * @param includeResponderResponses - Whether a responder-short-circuited response + * should also trigger this listener, as opposed + * to only genuine network responses. Defaults to `true`. + */ +export function addGetListener( + urlSuffix: string, + listener: JQueryAjaxListener, + includeResponderResponses = true +): void { + getListeners.push({ urlSuffix, listener, includeResponderResponses }); +} diff --git a/src/shared/script-injector.ts b/src/shared/script-injector.ts index 3422bab..bfdad1d 100644 --- a/src/shared/script-injector.ts +++ b/src/shared/script-injector.ts @@ -15,5 +15,5 @@ export function injectScriptHead(src: string, data: DataType = {}): void { for (const i in data) { s.setAttribute(`data-${i}`, data[i]); } - document.head.append(s); + document.documentElement.append(s); }; -- cgit v1.2.3