logsdu v0.1 skeleton
This commit is contained in:
103
src/format.ts
Normal file
103
src/format.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
// *************************************************************************
|
||||
// * (C)opyright 2026 by Ruben Carlo Benante *
|
||||
// * *
|
||||
// * This program is free software; you can redistribute it and/or modify *
|
||||
// * it under the terms of the GNU General Public License as published by *
|
||||
// * the Free Software Foundation, either version 3 of the License, or *
|
||||
// * (at your option) any later version. *
|
||||
// * *
|
||||
// * This program is distributed in the hope that it will be useful, *
|
||||
// * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
// * GNU General Public License for more details. *
|
||||
// * *
|
||||
// * You should have received a copy of the GNU General Public License *
|
||||
// * along with this program. If not, see http://www.gnu.org/licenses/. *
|
||||
// * *
|
||||
// * Contact author at: *
|
||||
// * Ruben Carlo Benante *
|
||||
// * rcb@beco.cc *
|
||||
// *************************************************************************
|
||||
|
||||
// The portal drives its three inputs with Inputmask, so the values we store
|
||||
// have to already be in the shape the masks expect (the placeholders on the
|
||||
// page: "2000101010", "01/01/2000", "000.000.000-00"). Normalising here means
|
||||
// the options page accepts a raw paste of digits and stores something the
|
||||
// masks will not fight with.
|
||||
|
||||
/** Strip everything that is not a digit. */
|
||||
export function digits(value: string): string {
|
||||
return value.replace(/\D+/g, '');
|
||||
}
|
||||
|
||||
/** Registration number: digits only, at most 10. */
|
||||
export function formatRa(value: string): string {
|
||||
return digits(value).slice(0, 10);
|
||||
}
|
||||
|
||||
/** Birth date as dd/mm/yyyy, formatted progressively while typing. */
|
||||
export function formatDate(value: string): string {
|
||||
const d = digits(value).slice(0, 8);
|
||||
const parts: string[] = [];
|
||||
if (d.length > 0) parts.push(d.slice(0, 2));
|
||||
if (d.length > 2) parts.push(d.slice(2, 4));
|
||||
if (d.length > 4) parts.push(d.slice(4, 8));
|
||||
return parts.join('/');
|
||||
}
|
||||
|
||||
/** CPF as 000.000.000-00, formatted progressively while typing. */
|
||||
export function formatCpf(value: string): string {
|
||||
const d = digits(value).slice(0, 11);
|
||||
let out = d.slice(0, 3);
|
||||
if (d.length > 3) out += `.${d.slice(3, 6)}`;
|
||||
if (d.length > 6) out += `.${d.slice(6, 9)}`;
|
||||
if (d.length > 9) out += `-${d.slice(9, 11)}`;
|
||||
return out;
|
||||
}
|
||||
|
||||
export function isCompleteRa(value: string): boolean {
|
||||
return digits(value).length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* A complete, calendar-valid date. Deliberately checks the round trip rather
|
||||
* than just the digit count, so 31/02/2000 is rejected before it ever reaches
|
||||
* the portal.
|
||||
*/
|
||||
export function isCompleteDate(value: string): boolean {
|
||||
const m = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(value);
|
||||
if (!m) return false;
|
||||
const day = Number(m[1]);
|
||||
const month = Number(m[2]);
|
||||
const year = Number(m[3]);
|
||||
const date = new Date(year, month - 1, day);
|
||||
return (
|
||||
date.getFullYear() === year &&
|
||||
date.getMonth() === month - 1 &&
|
||||
date.getDate() === day
|
||||
);
|
||||
}
|
||||
|
||||
export function isCompleteCpf(value: string): boolean {
|
||||
return digits(value).length === 11;
|
||||
}
|
||||
|
||||
/**
|
||||
* Origin of a user-typed site address, or null if it cannot be parsed.
|
||||
* Accepts input without a scheme ("saladigital.example.com") by assuming
|
||||
* https, which is what someone pasting an address from the URL bar expects.
|
||||
*/
|
||||
export function toOrigin(value: string): string | null {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return null;
|
||||
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)
|
||||
? trimmed
|
||||
: `https://${trimmed}`;
|
||||
try {
|
||||
const url = new URL(withScheme);
|
||||
if (url.protocol !== 'http:' && url.protocol !== 'https:') return null;
|
||||
return url.origin;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
43
src/manifest.json
Normal file
43
src/manifest.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "logsdu",
|
||||
"version": "0.1.0",
|
||||
"description": "Fills and submits a three-field academic portal login.",
|
||||
"icons": {
|
||||
"48": "icons/logsdu-48.png",
|
||||
"96": "icons/logsdu-96.png"
|
||||
},
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "logsdu@beco.cc",
|
||||
"strict_min_version": "115.0",
|
||||
"data_collection_permissions": {
|
||||
"required": ["none"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"permissions": ["storage"],
|
||||
"host_permissions": ["*://*/*"],
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["*://*/*"],
|
||||
"js": ["content.js"],
|
||||
"run_at": "document_idle",
|
||||
"all_frames": false
|
||||
}
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": ["injected.js"],
|
||||
"matches": ["*://*/*"]
|
||||
}
|
||||
],
|
||||
"options_ui": {
|
||||
"page": "options.html",
|
||||
"open_in_tab": true
|
||||
},
|
||||
"action": {
|
||||
"default_popup": "popup.html",
|
||||
"default_title": "logsdu"
|
||||
}
|
||||
}
|
||||
71
src/portal.ts
Normal file
71
src/portal.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
// *************************************************************************
|
||||
// * (C)opyright 2026 by Ruben Carlo Benante *
|
||||
// * *
|
||||
// * This program is free software; you can redistribute it and/or modify *
|
||||
// * it under the terms of the GNU General Public License as published by *
|
||||
// * the Free Software Foundation, either version 3 of the License, or *
|
||||
// * (at your option) any later version. *
|
||||
// * *
|
||||
// * This program is distributed in the hope that it will be useful, *
|
||||
// * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
// * GNU General Public License for more details. *
|
||||
// * *
|
||||
// * You should have received a copy of the GNU General Public License *
|
||||
// * along with this program. If not, see http://www.gnu.org/licenses/. *
|
||||
// * *
|
||||
// * Contact author at: *
|
||||
// * Ruben Carlo Benante *
|
||||
// * rcb@beco.cc *
|
||||
// *************************************************************************
|
||||
|
||||
// Every selector the extension knows about the target page, in one place.
|
||||
//
|
||||
// These describe a form shape, not a site: the address itself is configured
|
||||
// at runtime and lives only in local storage, so nothing here names the
|
||||
// institution.
|
||||
|
||||
/**
|
||||
* The login form. Every field lookup is scoped to it on purpose.
|
||||
*
|
||||
* The same page carries a "forgot your registration number" modal whose
|
||||
* inputs are id="CPF" and id="DTNASC". The three login inputs have no id at
|
||||
* all, so an unscoped lookup for a CPF field finds the modal's copy and
|
||||
* writes the value into the wrong form.
|
||||
*/
|
||||
export const FORM = 'form[name="LoginAP"]';
|
||||
|
||||
/** The three credential inputs, relative to FORM. */
|
||||
export const FIELDS = {
|
||||
ra: 'input[name="RA"]',
|
||||
dn: 'input[name="DN"]',
|
||||
cpf: 'input[name="CPF"]',
|
||||
} as const;
|
||||
|
||||
/** The "Entrar" button, relative to FORM. */
|
||||
export const SUBMIT = 'button[type="submit"]';
|
||||
|
||||
/**
|
||||
* The logout control, present on the pages behind the login. Clicking it is
|
||||
* the signal that the user wants to stay logged out, which suppresses the
|
||||
* auto-submit that would otherwise fire the moment the logout redirect lands
|
||||
* back on the login page.
|
||||
*/
|
||||
export const LOGOUT = '.js_logout';
|
||||
|
||||
/** Event names used to talk to the page-world filler. */
|
||||
export const EVENT_RESULT = 'logsdu:result';
|
||||
|
||||
/** Payload handed to the page-world script through its own dataset. */
|
||||
export interface FillRequest {
|
||||
ra: string;
|
||||
dn: string;
|
||||
cpf: string;
|
||||
submit: boolean;
|
||||
}
|
||||
|
||||
export interface FillResult {
|
||||
filled: boolean;
|
||||
submitted: boolean;
|
||||
error?: string;
|
||||
}
|
||||
67
src/webext.d.ts
vendored
Normal file
67
src/webext.d.ts
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
// *************************************************************************
|
||||
// * (C)opyright 2026 by Ruben Carlo Benante *
|
||||
// * *
|
||||
// * This program is free software; you can redistribute it and/or modify *
|
||||
// * it under the terms of the GNU General Public License as published by *
|
||||
// * the Free Software Foundation, either version 3 of the License, or *
|
||||
// * (at your option) any later version. *
|
||||
// * *
|
||||
// * This program is distributed in the hope that it will be useful, *
|
||||
// * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
// * GNU General Public License for more details. *
|
||||
// * *
|
||||
// * You should have received a copy of the GNU General Public License *
|
||||
// * along with this program. If not, see http://www.gnu.org/licenses/. *
|
||||
// * *
|
||||
// * Contact author at: *
|
||||
// * Ruben Carlo Benante *
|
||||
// * rcb@beco.cc *
|
||||
// *************************************************************************
|
||||
|
||||
// Minimal ambient declarations for the slice of the WebExtension API this
|
||||
// project uses. Hand-written on purpose: pulling in @types/chrome costs a
|
||||
// dependency and several megabytes for about a dozen call signatures.
|
||||
//
|
||||
// The `chrome` namespace is used rather than `browser` because it exists in
|
||||
// both Chrome and Firefox under MV3, and under MV3 Firefox also returns
|
||||
// promises from it. That keeps a future Chrome build a manifest concern
|
||||
// rather than a source concern.
|
||||
|
||||
declare namespace chrome {
|
||||
namespace runtime {
|
||||
const lastError: { message?: string } | undefined;
|
||||
function getURL(path: string): string;
|
||||
function openOptionsPage(): Promise<void>;
|
||||
const onMessage: {
|
||||
addListener(
|
||||
callback: (
|
||||
message: unknown,
|
||||
sender: unknown,
|
||||
sendResponse: (response?: unknown) => void,
|
||||
) => boolean | undefined | void,
|
||||
): void;
|
||||
};
|
||||
}
|
||||
|
||||
namespace storage {
|
||||
interface StorageArea {
|
||||
get(keys: string | string[] | null): Promise<Record<string, unknown>>;
|
||||
set(items: Record<string, unknown>): Promise<void>;
|
||||
remove(keys: string | string[]): Promise<void>;
|
||||
}
|
||||
const local: StorageArea;
|
||||
}
|
||||
|
||||
namespace tabs {
|
||||
interface Tab {
|
||||
id?: number;
|
||||
url?: string;
|
||||
}
|
||||
function query(queryInfo: {
|
||||
active?: boolean;
|
||||
currentWindow?: boolean;
|
||||
}): Promise<Tab[]>;
|
||||
function sendMessage(tabId: number, message: unknown): Promise<unknown>;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user