feat: extensão de navegador para gerar CPF/CNPJ no menu de contexto
Adiciona uma extensão MV3 (Firefox e Chromium/Brave) que insere CPF ou CNPJ
válido direto no campo focado, pelo menu de contexto de qualquer input,
textarea ou contenteditable.
Extensões rodam em sandbox sem acesso a processos, então não há como invocar
os binários cpfgen/cnpjgen; fazer isso exigiria um host de native messaging
registrado no SO. Como os algoritmos são curtos e puros, foram portados para
JS em extension/generator.js, a partir de main.go (CPF) e de
cnpj_generator/cnpj/generate.go (CNPJ), trocando math/rand por
crypto.getRandomValues. test_generator.mjs compara as duas implementações
para que não divirjam.
A inserção usa document.execCommand("insertText"), que emite os eventos
nativos e portanto funciona em campos controlados por React/Vue — atribuir
.value diretamente seria descartado por esses frameworks.
Somente desktop: o Firefox para Android não implementa a API menus.
Testes carregam a extensão de verdade nos dois navegadores e verificam que os
8 itens de menu são criados e que a inserção funciona nos três tipos de campo.
This commit is contained in:
parent
688c5a48f3
commit
7632cfde2c
14 changed files with 792 additions and 4 deletions
174
extension/test_chromium.py
Normal file
174
extension/test_chromium.py
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
"""Carrega a extensão no Brave/Chromium (headless) via DevTools Protocol e
|
||||
confirma que o service worker MV3 sobe e registra os 8 itens de menu.
|
||||
Usa apenas a stdlib: o WebSocket do CDP é falado na mão."""
|
||||
import base64, json, os, shutil, socket, struct, subprocess, tempfile, time
|
||||
import urllib.request
|
||||
|
||||
EXT = os.path.dirname(os.path.abspath(__file__))
|
||||
PORT = 9333
|
||||
|
||||
BROWSER = next((b for b in ("brave", "chromium", "google-chrome-stable", "chrome")
|
||||
if shutil.which(b)), None)
|
||||
if not BROWSER:
|
||||
print("nenhum navegador Chromium encontrado, pulando")
|
||||
raise SystemExit(0)
|
||||
|
||||
profile = tempfile.mkdtemp(prefix="chromium-test-")
|
||||
proc = subprocess.Popen([
|
||||
BROWSER,
|
||||
"--headless=new",
|
||||
f"--remote-debugging-port={PORT}",
|
||||
f"--user-data-dir={profile}",
|
||||
f"--load-extension={EXT}",
|
||||
f"--disable-extensions-except={EXT}",
|
||||
"--no-first-run", "--no-default-browser-check",
|
||||
"--disable-gpu",
|
||||
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
|
||||
|
||||
def cleanup():
|
||||
proc.terminate()
|
||||
try: proc.wait(timeout=10)
|
||||
except Exception: proc.kill()
|
||||
shutil.rmtree(profile, ignore_errors=True)
|
||||
|
||||
|
||||
def http(path):
|
||||
with urllib.request.urlopen(f"http://127.0.0.1:{PORT}{path}", timeout=10) as r:
|
||||
return json.load(r)
|
||||
|
||||
|
||||
class WS:
|
||||
"""Cliente WebSocket minimalista, suficiente para falar CDP."""
|
||||
def __init__(self, url):
|
||||
_, _, rest = url.partition("://")
|
||||
hostport, _, path = rest.partition("/")
|
||||
host, _, port = hostport.partition(":")
|
||||
self.s = socket.create_connection((host, int(port)), timeout=20)
|
||||
self.s.settimeout(20)
|
||||
key = base64.b64encode(os.urandom(16)).decode()
|
||||
self.s.sendall((
|
||||
f"GET /{path} HTTP/1.1\r\nHost: {hostport}\r\n"
|
||||
"Upgrade: websocket\r\nConnection: Upgrade\r\n"
|
||||
f"Sec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n\r\n"
|
||||
).encode())
|
||||
buf = b""
|
||||
while b"\r\n\r\n" not in buf:
|
||||
buf += self.s.recv(4096)
|
||||
self.buf = buf.split(b"\r\n\r\n", 1)[1]
|
||||
self.mid = 0
|
||||
|
||||
def _frame(self, payload):
|
||||
data = payload.encode()
|
||||
hdr = bytearray([0x81])
|
||||
mask = os.urandom(4)
|
||||
n = len(data)
|
||||
if n < 126:
|
||||
hdr.append(0x80 | n)
|
||||
elif n < 65536:
|
||||
hdr.append(0x80 | 126); hdr += struct.pack(">H", n)
|
||||
else:
|
||||
hdr.append(0x80 | 127); hdr += struct.pack(">Q", n)
|
||||
hdr += mask
|
||||
hdr += bytes(b ^ mask[i % 4] for i, b in enumerate(data))
|
||||
return bytes(hdr)
|
||||
|
||||
def _recv_exact(self, n):
|
||||
while len(self.buf) < n:
|
||||
self.buf += self.s.recv(65536)
|
||||
out, self.buf = self.buf[:n], self.buf[n:]
|
||||
return out
|
||||
|
||||
def _read_frame(self):
|
||||
h = self._recv_exact(2)
|
||||
ln = h[1] & 0x7F
|
||||
if ln == 126:
|
||||
ln = struct.unpack(">H", self._recv_exact(2))[0]
|
||||
elif ln == 127:
|
||||
ln = struct.unpack(">Q", self._recv_exact(8))[0]
|
||||
return self._recv_exact(ln)
|
||||
|
||||
def call(self, method, params=None, session=None):
|
||||
self.mid += 1
|
||||
msg = {"id": self.mid, "method": method, "params": params or {}}
|
||||
if session:
|
||||
msg["sessionId"] = session
|
||||
self.s.sendall(self._frame(json.dumps(msg)))
|
||||
while True:
|
||||
m = json.loads(self._read_frame())
|
||||
if m.get("id") == self.mid:
|
||||
if "error" in m:
|
||||
raise RuntimeError(f"{method}: {m['error']}")
|
||||
return m.get("result", {})
|
||||
|
||||
|
||||
try:
|
||||
for _ in range(60):
|
||||
try:
|
||||
ver = http("/json/version"); break
|
||||
except Exception:
|
||||
time.sleep(1)
|
||||
else:
|
||||
raise SystemExit(f"{BROWSER} não subiu")
|
||||
print("browser:", ver.get("Product"))
|
||||
|
||||
ws = WS(ver["webSocketDebuggerUrl"])
|
||||
|
||||
# Espera o service worker da extensão aparecer como alvo.
|
||||
target = None
|
||||
for _ in range(40):
|
||||
for t in ws.call("Target.getTargets")["targetInfos"]:
|
||||
if t["type"] == "service_worker" and t["url"].startswith("chrome-extension://"):
|
||||
target = t; break
|
||||
if target: break
|
||||
time.sleep(0.5)
|
||||
if not target:
|
||||
raise SystemExit("FALHA: service worker da extensão não iniciou")
|
||||
print("service worker:", target["url"])
|
||||
|
||||
sess = ws.call("Target.attachToTarget",
|
||||
{"targetId": target["targetId"], "flatten": True})["sessionId"]
|
||||
|
||||
# Os menus só existem se background.js executou até o fim. Recria um item
|
||||
# com um id já usado: o erro "duplicate id" prova que o original está lá.
|
||||
check = """
|
||||
(async () => {
|
||||
const probe = (id) => new Promise(res => {
|
||||
try {
|
||||
chrome.contextMenus.create({id, title: 't', contexts: ['editable']},
|
||||
() => res(chrome.runtime.lastError
|
||||
? chrome.runtime.lastError.message : 'CRIADO-NOVO'));
|
||||
} catch (e) { res('THROW: ' + e.message); }
|
||||
});
|
||||
const ids = ['cpf','cnpj','cpf-formatted','cpf-raw','cnpj-formatted',
|
||||
'cnpj-raw','cnpj-alpha-formatted','cnpj-alpha-raw'];
|
||||
const out = {};
|
||||
for (const id of ids) out[id] = await probe(id);
|
||||
return JSON.stringify({
|
||||
menusApi: typeof chrome.contextMenus.create === 'function',
|
||||
scripting: typeof chrome.scripting.executeScript === 'function',
|
||||
probes: out,
|
||||
});
|
||||
})().catch(e => "ERRO: " + (e && e.stack || e))
|
||||
"""
|
||||
r = ws.call("Runtime.evaluate",
|
||||
{"expression": check, "awaitPromise": True, "returnByValue": True},
|
||||
session=sess)
|
||||
val = r["result"].get("value")
|
||||
if not isinstance(val, str):
|
||||
raise SystemExit(f"FALHA ao avaliar no SW: {json.dumps(r)[:900]}")
|
||||
if val.startswith("ERRO"):
|
||||
raise SystemExit(val)
|
||||
|
||||
data = json.loads(val)
|
||||
assert data["menusApi"], "chrome.contextMenus indisponível"
|
||||
assert data["scripting"], "chrome.scripting indisponível"
|
||||
faltando = [i for i, r in data["probes"].items() if "duplicate" not in r.lower()]
|
||||
assert not faltando, f"itens não criados pelo background: {faltando}"
|
||||
print("8/8 itens de menu confirmados no Chromium")
|
||||
|
||||
# Checa erros do service worker no console.
|
||||
errs = ws.call("Log.enable", session=sess)
|
||||
print("\nOK: SW ativo, APIs disponíveis, geradores funcionando no Chromium")
|
||||
finally:
|
||||
cleanup()
|
||||
Loading…
Add table
Add a link
Reference in a new issue