diff --git a/bash/functions/browser/apply_chromium_extension_policy.md b/bash/functions/browser/apply_chromium_extension_policy.md index 84150cab..860c894c 100644 --- a/bash/functions/browser/apply_chromium_extension_policy.md +++ b/bash/functions/browser/apply_chromium_extension_policy.md @@ -3,10 +3,10 @@ name: apply_chromium_extension_policy kind: function lang: bash domain: browser -version: "1.0.0" +version: "1.1.0" purity: impure -signature: "apply_chromium_extension_policy --keep [--keep ]... [--policy-path ] [--update-url ] [--dry-run]" -description: "Escribe de forma idempotente la política managed de Chromium (ExtensionInstallForcelist) que fuerza la instalación de un conjunto de extensiones en cualquier perfil del navegador. Crea backup con sufijo de fecha si el archivo preexiste y su contenido difiere. Requiere sudo para escribir en /etc; en modo --dry-run no toca el sistema." +signature: "apply_chromium_extension_policy [--keep ]... [--block ]... [--policy-path ] [--update-url ] [--dry-run]" +description: "Escribe de forma idempotente la política managed de Chromium combinando ExtensionInstallForcelist (force-instala la whitelist --keep) y ExtensionInstallBlocklist (bloquea y desinstala la blocklist --block). No usa el comodín \"*\" blocked, por lo que NO afecta a las extensiones unpacked cargadas con --load-extension. Guarda backup fuera del directorio managed/ (que Chromium lee entero). Requiere sudo para escribir en /etc; en --dry-run no toca el sistema." tags: [chromium, extensions, policy, browser, navegator, managed-policy, idempotent] uses_functions: [] uses_types: [] @@ -16,14 +16,16 @@ error_type: "error_go_core" imports: [] params: - name: "--keep " - desc: "ID de extensión de Chrome Web Store a force-instalar (repetible, al menos uno obligatorio). Ejemplo: ddkjiahejlhfcafbddmgiahcphecmpfh (uBlock Origin Lite)." + desc: "ID de extensión de Chrome Web Store a force-instalar (repetible). Va a ExtensionInstallForcelist. Ejemplo: ddkjiahejlhfcafbddmgiahcphecmpfh (uBlock Origin Lite)." + - name: "--block " + desc: "ID de extensión a bloquear y desinstalar en cualquier perfil (repetible). Va a ExtensionInstallBlocklist. Solo afecta a los IDs listados; el resto de extensiones no se toca." - name: "--policy-path " desc: "Ruta del JSON de managed policy. Default: /etc/chromium/policies/managed/extensions.json." - name: "--update-url " desc: "URL del servicio de actualización de extensiones. Default: https://clients2.google.com/service/update2/crx." - name: "--dry-run" desc: "Imprime el JSON que se escribiría sin tocar el sistema (no requiere sudo)." -output: "Escribe el JSON de política en policy-path y emite a stdout un resumen: extensiones aplicadas, ruta, backup creado y recordatorio de reinicio de Chromium. Sale 0 si la política se aplicó o ya estaba vigente. Sale != 0 en error." +output: "Escribe el JSON de política en policy-path y emite a stdout un resumen: extensiones forzadas, bloqueadas, ruta, backup creado y recordatorio de reinicio de Chromium. Sale 0 si la política se aplicó o ya estaba vigente. Sale != 0 en error. Requiere al menos un --keep o --block." tested: false tests: [] test_file_path: "" @@ -33,34 +35,55 @@ file_path: "bash/functions/browser/apply_chromium_extension_policy.sh" ## Ejemplo ```bash -# Forzar solo uBlock Origin Lite en todos los perfiles (proyecto web_scraping, regla 9) +# Dejar el perfil con solo uBlock Origin Lite: forzar uBlock + bloquear las 3 que estorban +# al scraping (Dark Reader, NoScript, OneTab). Proyecto web_scraping, regla 9. source bash/functions/browser/apply_chromium_extension_policy.sh -apply_chromium_extension_policy --keep ddkjiahejlhfcafbddmgiahcphecmpfh +apply_chromium_extension_policy \ + --keep ddkjiahejlhfcafbddmgiahcphecmpfh \ + --block eimadpbcbfnmbkopoojfekhnkhdbieeh \ + --block doojmbjmlfjjnbmnoijecmcbfeoakpjm \ + --block chphlpgkkbolifaimnlloiipkdnihall # Previsualizar sin tocar el sistema (sin sudo) apply_chromium_extension_policy --keep ddkjiahejlhfcafbddmgiahcphecmpfh --dry-run -# Forzar varias extensiones a la vez -apply_chromium_extension_policy \ - --keep ddkjiahejlhfcafbddmgiahcphecmpfh \ - --keep cjpalhdlnbpafiamejdnhcphjbkeiagm \ - --policy-path /etc/chromium/policies/managed/extensions.json - -# Usando pass para el sudo no interactivo (setup de este equipo) -# apply_chromium_extension_policy ya usa sudo tee internamente; -# si el entorno requiere pass: ejecutar como root o con SUDO_ASKPASS configurado. +# Ejecutar como root para el sudo no interactivo de este equipo +pass show claude/sudo | sudo -S bash bash/functions/browser/apply_chromium_extension_policy.sh \ + --keep ddkjiahejlhfcafbddmgiahcphecmpfh --block eimadpbcbfnmbkopoojfekhnkhdbieeh ``` +La policy por sí sola evita la reinstalación pero NO desinstala lo ya presente en un perfil concreto: +combínala con `clean_chrome_profile_extensions_bash_browser` (con Chromium cerrado) para purgar del +disco las extensiones ya instaladas. + ## Cuando usarla -Al preparar un PC nuevo o cambiar qué extensiones de Chrome Web Store deben estar presentes en cualquier perfil de Chromium del equipo. Reemplaza el paso manual de editar el JSON de policy con sudo. Indispensable tras un fresh install o cuando se añade o retira una extensión del conjunto obligatorio del proyecto `web_scraping`. +Al preparar un PC nuevo o cambiar qué extensiones de Chrome Web Store deben estar (o no estar) en +cualquier perfil de Chromium del equipo. Reemplaza el paso manual de editar el JSON de policy con +sudo. `--keep` fuerza y fija las imprescindibles; `--block` elimina las molestas sin tocar el resto. ## Gotchas -- **Requiere sudo** para escribir en `/etc/chromium/policies/managed/`. En este equipo se alimenta con `pass show claude/sudo | sudo -S ` para operaciones autónomas. -- **Chrome cachea la política en memoria**: hay que cerrar TODOS los procesos Chromium (`pkill -9 chromium`) y relanzar para que el cambio surta efecto en runtime. Alternativa sin cerrar: `chrome://policy` → botón "Reload policies". -- **Idempotente**: si el archivo de policy ya tiene el mismo contenido, la función detecta el no-op y sale 0 sin escribir nada. -- **Backup por día**: si el contenido difiere, crea `.bak.YYYYMMDD`. Si el backup del día ya existe, no lo sobreescribe. Los backups no se limpian automáticamente. -- **`ExtensionInstallForcelist` solo gestiona las extensiones listadas** — no elimina extensiones que el usuario haya instalado manualmente; solo garantiza que las listadas estén instaladas. -- **No aplica a Chrome stable 138+**: en Chrome estable la flag `--load-extension` está desactivada, pero las managed policies siguen siendo el mecanismo correcto para force-install desde Web Store. -- Para referencia del sistema completo: `projects/web_scraping/CHROMIUM_SYSTEM.md`. +- **El backup NUNCA va dentro de `managed/`** (lo gestiona la función, pero es la lección clave): Chromium + lee **todos** los archivos del directorio `policies/managed/` sin filtrar por extensión de nombre. Un + `extensions.json.bak.YYYYMMDD` dentro de `managed/` se mergea con la policy efectiva y **reinyecta** las + extensiones del backup (se ven como `location=7` external_policy_download y vuelven aunque las borres). + Por eso la función guarda los backups en `policies/policy-backups/`, fuera de `managed/`. Si encuentras + backups antiguos dentro de `managed/`, muévelos fuera. +- **No usa el comodín `"*": blocked`**: ese modo desinstala todo lo no-whitelist pero también **bloquea las + extensiones unpacked** (`--load-extension`), rompiendo cosas como la extensión de captura de `web_proxy` + con el error "Loading of unpacked extensions is disabled by the administrator". Esta función bloquea solo + los IDs de `--block`. +- **`--load-extension` y managed policy son incompatibles en Chromium 137+**: con CUALQUIER managed policy + presente, Chromium desactiva `--load-extension` ("disabled by the administrator"). Para cargar una + extensión local junto a una managed policy hay que empaquetarla (.crx + update_url) o usar `--proxy-server` + directo en el caso de `web_proxy`. +- **Requiere sudo** para escribir en `/etc/chromium/policies/managed/`. En este equipo: `pass show claude/sudo | sudo -S `. +- **Chrome cachea la política en memoria**: cerrar TODOS los Chromium (`pkill -9 chromium`) y relanzar, o `chrome://policy` → "Reload policies". +- **Idempotente**: si el archivo ya tiene el mismo contenido, no-op y sale 0. +- Referencia del sistema completo: `projects/web_scraping/CHROMIUM_SYSTEM.md`. + +## Capability growth log + +- v1.1.0 (2026-06-05) — añade `--block` (ExtensionInstallBlocklist); reemplaza el modo `ExtensionSettings "*": blocked` (rompía extensiones unpacked) por blocklist específica; mueve los backups fuera de `managed/` (Chromium lee todo el directorio y un `.bak` ahí reinyectaba extensiones). +- v1.0.0 (2026-06-05) — baseline: ExtensionInstallForcelist con whitelist `--keep`. diff --git a/bash/functions/browser/apply_chromium_extension_policy.sh b/bash/functions/browser/apply_chromium_extension_policy.sh index b2d05aed..80f7f0b7 100644 --- a/bash/functions/browser/apply_chromium_extension_policy.sh +++ b/bash/functions/browser/apply_chromium_extension_policy.sh @@ -1,12 +1,14 @@ #!/usr/bin/env bash # apply_chromium_extension_policy — Escribe de forma idempotente la política managed de Chromium -# que fuerza la instalación de un conjunto de extensiones en CUALQUIER perfil del navegador. +# que fuerza la instalación de una whitelist de extensiones y bloquea (desinstala) una blocklist +# concreta, sin tocar el resto. Usa ExtensionInstallForcelist + ExtensionInstallBlocklist. apply_chromium_extension_policy() { local policy_path="/etc/chromium/policies/managed/extensions.json" local update_url="https://clients2.google.com/service/update2/crx" local dry_run=0 - local -a ext_ids=() + local -a keep_ids=() + local -a block_ids=() # --- Parseo de argumentos --- while [[ $# -gt 0 ]]; do @@ -16,7 +18,15 @@ apply_chromium_extension_policy() { echo "apply_chromium_extension_policy: --keep requiere un ID de extensión" >&2 return 1 fi - ext_ids+=("$2") + keep_ids+=("$2") + shift 2 + ;; + --block) + if [[ -z "${2:-}" ]]; then + echo "apply_chromium_extension_policy: --block requiere un ID de extensión" >&2 + return 1 + fi + block_ids+=("$2") shift 2 ;; --policy-path) @@ -41,31 +51,53 @@ apply_chromium_extension_policy() { ;; *) echo "apply_chromium_extension_policy: argumento desconocido: $1" >&2 - echo "Uso: apply_chromium_extension_policy --keep [--keep ]... [--policy-path ] [--update-url ] [--dry-run]" >&2 + echo "Uso: apply_chromium_extension_policy [--keep ]... [--block ]... [--policy-path ] [--update-url ] [--dry-run]" >&2 return 1 ;; esac done - # --- Validar que hay al menos una extensión --- - if [[ ${#ext_ids[@]} -eq 0 ]]; then - echo "apply_chromium_extension_policy: se requiere al menos un --keep " >&2 + # --- Validar que hay al menos una extensión a forzar o bloquear --- + if [[ ${#keep_ids[@]} -eq 0 && ${#block_ids[@]} -eq 0 ]]; then + echo "apply_chromium_extension_policy: se requiere al menos un --keep o un --block " >&2 return 1 fi # --- Construir el JSON --- - local forcelist_entries="" - local first=1 - for id in "${ext_ids[@]}"; do - if [[ $first -eq 0 ]]; then - forcelist_entries+=","$'\n' - fi - forcelist_entries+=" \"${id};${update_url}\"" - first=0 - done + # Dos claves complementarias, ninguna bloquea las extensiones unpacked (--load-extension), + # de modo que extensiones locales como la de captura de web_proxy siguen cargando: + # 1. ExtensionInstallForcelist: fuerza la instalación de la whitelist (--keep), que además + # no se puede desinstalar desde la UI. + # 2. ExtensionInstallBlocklist: bloquea Y desinstala las extensiones de la blocklist + # (--block) en cualquier perfil. Solo afecta a los IDs listados; el resto no se toca. + local forcelist_json="[]" blocklist_json="[]" + if [[ ${#keep_ids[@]} -gt 0 ]]; then + local entries="" first=1 + for id in "${keep_ids[@]}"; do + [[ $first -eq 0 ]] && entries+=","$'\n' + entries+=" \"${id};${update_url}\"" + first=0 + done + forcelist_json=$(printf '[\n%s\n ]' "$entries") + fi + if [[ ${#block_ids[@]} -gt 0 ]]; then + local entries="" first=1 + for id in "${block_ids[@]}"; do + [[ $first -eq 0 ]] && entries+=","$'\n' + entries+=" \"${id}\"" + first=0 + done + blocklist_json=$(printf '[\n%s\n ]' "$entries") + fi local new_json - new_json=$(printf '{\n "ExtensionInstallForcelist": [\n%s\n ]\n}\n' "$forcelist_entries") + new_json=$(cat </dev/null; else mkdir -p "$backup_dir"; fi + fi if [[ ! -f "$backup_path" ]]; then echo "apply_chromium_extension_policy: creando backup → ${backup_path}" if [[ $EUID -ne 0 ]]; then @@ -129,7 +174,7 @@ apply_chromium_extension_policy() { fi fi - # --- Escribir el JSON vía tmpfile + sudo cp / tee --- + # --- Escribir el JSON vía tmpfile + sudo cp --- local tmpfile tmpfile=$(mktemp /tmp/chromium_policy_XXXXXX.json) echo "$new_json" > "$tmpfile" @@ -138,7 +183,6 @@ apply_chromium_extension_policy() { sudo cp "$tmpfile" "$policy_path" || { echo "apply_chromium_extension_policy: no se pudo escribir ${policy_path}" >&2 rm -f "$tmpfile" - # Restaurar backup si hubo if [[ -n "$backup_path" && -f "$backup_path" ]]; then echo "apply_chromium_extension_policy: restaurando backup tras error..." sudo cp "$backup_path" "$policy_path" 2>/dev/null || true @@ -165,7 +209,6 @@ apply_chromium_extension_policy() { elif command -v jq &>/dev/null; then jq . "$policy_path" &>/dev/null && validation_ok=1 else - # Sin parser disponible, asumir OK (el JSON lo generamos nosotros) validation_ok=1 fi @@ -184,10 +227,15 @@ apply_chromium_extension_policy() { # --- Resumen final --- echo "apply_chromium_extension_policy: política aplicada correctamente." echo " Ruta : ${policy_path}" - echo " Extensiones forzadas (${#ext_ids[@]}):" - for id in "${ext_ids[@]}"; do - echo " - ${id}" - done + if [[ ${#keep_ids[@]} -gt 0 ]]; then + echo " Forzadas (${#keep_ids[@]}):" + for id in "${keep_ids[@]}"; do echo " - ${id}"; done + fi + if [[ ${#block_ids[@]} -gt 0 ]]; then + echo " Bloqueadas/desinstaladas (${#block_ids[@]}):" + for id in "${block_ids[@]}"; do echo " - ${id}"; done + fi + echo " Extensiones unpacked (--load-extension, p.ej. web_proxy): NO afectadas." if [[ -n "$backup_path" && -f "$backup_path" ]]; then echo " Backup : ${backup_path}" fi diff --git a/bash/functions/browser/clean_chrome_profile_extensions.md b/bash/functions/browser/clean_chrome_profile_extensions.md new file mode 100644 index 00000000..f16404ca --- /dev/null +++ b/bash/functions/browser/clean_chrome_profile_extensions.md @@ -0,0 +1,84 @@ +--- +name: clean_chrome_profile_extensions +kind: function +lang: bash +domain: browser +version: "1.0.0" +purity: impure +signature: "clean_chrome_profile_extensions [--user-data-dir ] [--profile-directory ] [--keep ]... [--dry-run]" +description: "Purga in-place las extensiones de un perfil Chrome/Chromium existente que no estén en la whitelist --keep: borra sus carpetas de disco y elimina sus referencias de Preferences y Secure Preferences para que Chromium no las reinstale. Complementaria a apply_chromium_extension_policy_bash_browser que evita reinstalación pero no desinstala lo ya instalado en Chromium 148." +tags: [navegator, chromium, extensions, profile, cleanup, browser, scraping] +uses_functions: [apply_chromium_extension_policy_bash_browser] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [] +tested: false +tests: [] +test_file_path: "" +file_path: "bash/functions/browser/clean_chrome_profile_extensions.sh" +params: + - name: --user-data-dir + desc: "Ruta raíz del user-data-dir de Chrome/Chromium. Default: ~/.config/chromium" + - name: --profile-directory + desc: "Nombre del subperfil dentro de user-data-dir. Default: Default" + - name: --keep + desc: "ID de extensión Chrome a conservar (repetible, 32 chars minúsculas). Si no se pasa ninguno el default es ddkjiahejlhfcafbddmgiahcphecmpfh (uBlock Origin Lite)" + - name: --dry-run + desc: "Muestra qué IDs se conservarían y cuáles se borrarían sin tocar disco ni archivos de preferencias" +output: "JSON en stdout: {profile: \"\", kept: [id...], removed: [id...]}. Exit 0 en éxito o dry-run. Errores a stderr con exit != 0." +--- + +## Ejemplo + +```bash +# Cerrar Chromium primero (OBLIGATORIO en modo real) +pkill -TERM chromium + +# Purgar perfil Default dejando solo uBlock Origin Lite +source $HOME/fn_registry/bash/functions/browser/clean_chrome_profile_extensions.sh +clean_chrome_profile_extensions --keep ddkjiahejlhfcafbddmgiahcphecmpfh + +# Previsualizar antes de tocar nada +clean_chrome_profile_extensions --keep ddkjiahejlhfcafbddmgiahcphecmpfh --dry-run + +# Perfil no-default con whitelist de dos extensiones +clean_chrome_profile_extensions \ + --user-data-dir "$HOME/.config/chromium" \ + --profile-directory "Profile 1" \ + --keep ddkjiahejlhfcafbddmgiahcphecmpfh \ + --keep cjpalhdlnbpafiamejdnhcphjbkeiagm + +# Salida esperada (ejemplo): +# {"profile":"/home/enmanuel/.config/chromium/Default","kept":["ddkjiahejlhfcafbddmgiahcphecmpfh"],"removed":["dark-reader-id","another-ext-id"]} +``` + +También ejecutable directamente con `fn run`: + +```bash +cd $HOME/fn_registry +./fn run clean_chrome_profile_extensions_bash_browser -- --dry-run +``` + +## Cuando usarla + +Úsala después de reducir la whitelist de extensiones con `apply_chromium_extension_policy_bash_browser` (modo `blocked`), para quitar del disco las que ya estaban instaladas en el perfil: la policy evita que Chromium reinstale extensiones nuevas, pero en Chromium 148 no desinstala las que ya estaban force-instaladas. Esta función hace la purga determinista del estado existente. También útil antes de una sesión de scraping para dejar el perfil con solo las extensiones necesarias. + +## Gotchas + +- **Chromium DEBE estar cerrado** antes de ejecutar en modo real. Chromium reescribe `Preferences` desde memoria al cerrar y desharía toda la purga. La función lo comprueba con `pgrep -x chromium` y aborta con exit 2 si hay procesos vivos. En `--dry-run` no se hace este check. +- **Combínala con `apply_chromium_extension_policy_bash_browser` (blocked)** para que las extensiones no vuelvan a instalarse la próxima vez que arranques Chromium. Esta función purga el estado actual; la policy evita la reinstalación futura. +- **Backup automático de prefs**: antes de editar `Preferences` y `Secure Preferences` la función crea `.bak.YYYYMMDD`. Si ya existe un backup del día no lo sobreescribe. En caso de problemas: `cp Preferences.bak.YYYYMMDD Preferences`. +- **Opera por perfil**: actúa sobre `--user-data-dir`/`--profile-directory`/Extensions. Si tienes varios perfiles (`Default`, `Profile 1`, etc.) debes invocarla una vez por cada uno. +- **python3 > jq > warn**: para editar el JSON de Preferences usa python3 si está disponible, jq como fallback, y emite un warning a stderr (sin abortar) si ninguno está. En ese caso las carpetas sí se borran pero las referencias en Preferences quedan — Chromium podría intentar reinstalar desde Web Store. +- **Secure Preferences HMAC**: la tabla `protection.macs.extensions.settings` también se limpia para evitar que Chromium detecte inconsistencia entre el HMAC y la entrada eliminada y resetee configuraciones. Si la HMAC falla de todas formas, Chromium lo trata como perfil potencialmente corrupto y puede resetear algunas prefs — comportamiento esperado de Chromium, no un bug de esta función. + +## Exit codes + +| Código | Significado | +|--------|------------| +| 0 | Éxito o dry-run completado | +| 1 | Argumento inválido o perfil no encontrado | +| 2 | Chromium está corriendo (solo en modo real) | +| 3 | Directorio Extensions no encontrado | diff --git a/bash/functions/browser/clean_chrome_profile_extensions.sh b/bash/functions/browser/clean_chrome_profile_extensions.sh new file mode 100644 index 00000000..1196e647 --- /dev/null +++ b/bash/functions/browser/clean_chrome_profile_extensions.sh @@ -0,0 +1,245 @@ +#!/usr/bin/env bash +# clean_chrome_profile_extensions — purga in-place extensiones fuera de la whitelist +# de un perfil Chrome/Chromium existente. Borra las carpetas de disco y limpia las +# referencias en Preferences y Secure Preferences para que Chromium no las reinstale. + +set -euo pipefail + +clean_chrome_profile_extensions() { + # ── defaults ────────────────────────────────────────────────────────────── + local _user_data_dir="${HOME}/.config/chromium" + local _profile_dir="Default" + local _keep=() + local _default_ext="ddkjiahejlhfcafbddmgiahcphecmpfh" + local _dry_run=0 + + # ── parse args ───────────────────────────────────────────────────────────── + _usage() { + cat >&2 <<'EOF' +Usage: clean_chrome_profile_extensions [--user-data-dir ] [--profile-directory ] + [--keep ]... [--dry-run] + + --user-data-dir Raíz del perfil. Default: ~/.config/chromium + --profile-directory Subperfil. Default: Default + --keep ID de extensión a conservar (repetible). + Default si no se pasa ninguno: ddkjiahejlhfcafbddmgiahcphecmpfh (uBlock Origin Lite) + --dry-run Lista qué se borraría sin tocar nada. + +Exit codes: + 0 éxito (o dry-run completado) + 1 error de argumento o validación + 2 chromium está corriendo (solo en modo real) + 3 directorio de extensiones no encontrado +EOF + return 1 + } + + while [[ $# -gt 0 ]]; do + case "$1" in + --user-data-dir) _user_data_dir="$2"; shift 2 ;; + --profile-directory) _profile_dir="$2"; shift 2 ;; + --keep) _keep+=("$2"); shift 2 ;; + --dry-run) _dry_run=1; shift ;; + -h|--help) _usage; return 0 ;; + *) echo "clean_chrome_profile_extensions: argumento desconocido: $1" >&2; return 1 ;; + esac + done + + # ── whitelist por defecto ────────────────────────────────────────────────── + if [[ ${#_keep[@]} -eq 0 ]]; then + _keep=("$_default_ext") + fi + + # ── construir paths base ─────────────────────────────────────────────────── + local _profile_path + _profile_path="${_user_data_dir}/${_profile_dir}" + local _ext_dir="${_profile_path}/Extensions" + + # ── validaciones ────────────────────────────────────────────────────────── + if [[ ! -d "$_profile_path" ]]; then + echo "clean_chrome_profile_extensions: perfil no encontrado: ${_profile_path}" >&2 + return 1 + fi + + if [[ ! -d "$_ext_dir" ]]; then + echo "clean_chrome_profile_extensions: directorio de extensiones no encontrado: ${_ext_dir}" >&2 + return 3 + fi + + # ── guard: chromium NO debe estar corriendo (excepto en dry-run) ────────── + if [[ $_dry_run -eq 0 ]]; then + if pgrep -x chromium >/dev/null 2>&1; then + echo "clean_chrome_profile_extensions: chromium está corriendo — ciérralo antes de limpiar:" >&2 + echo " pkill -TERM chromium" >&2 + echo "(Chromium reescribe Preferences desde memoria al cerrar y desharía la purga)" >&2 + return 2 + fi + fi + + # ── enumerar extensiones instaladas ─────────────────────────────────────── + local _to_remove=() + local _to_keep=() + + while IFS= read -r -d '' _ext_path; do + local _ext_id + _ext_id="$(basename "$_ext_path")" + + # Siempre ignorar la carpeta Temp (usada durante installs en curso) + if [[ "$_ext_id" == "Temp" ]]; then + continue + fi + + # Comprobar si está en la whitelist + local _in_keep=0 + local _k + for _k in "${_keep[@]}"; do + if [[ "$_ext_id" == "$_k" ]]; then + _in_keep=1 + break + fi + done + + if [[ $_in_keep -eq 1 ]]; then + _to_keep+=("$_ext_id") + else + _to_remove+=("$_ext_id") + fi + done < <(find "$_ext_dir" -mindepth 1 -maxdepth 1 -type d -print0 | sort -z) + + # ── modo dry-run: solo informar ──────────────────────────────────────────── + if [[ $_dry_run -eq 1 ]]; then + echo "=== clean_chrome_profile_extensions DRY-RUN ===" >&2 + echo " Perfil : ${_profile_path}" >&2 + echo " Conservar (${#_to_keep[@]}): ${_to_keep[*]+"${_to_keep[*]}"}" >&2 + echo " Borrar (${#_to_remove[@]}): ${_to_remove[*]+"${_to_remove[*]}"}" >&2 + _emit_json "$_profile_path" _to_keep _to_remove + return 0 + fi + + # ── borrar extensiones fuera de la whitelist ─────────────────────────────── + if [[ ${#_to_remove[@]} -gt 0 ]]; then + local _id + for _id in "${_to_remove[@]}"; do + rm -rf "${_ext_dir}/${_id}" + done + + # ── purgar referencias en Preferences y Secure Preferences ──────────── + # Construir lista Python de IDs eliminados + local _py_ids_list="" + for _id in "${_to_remove[@]}"; do + _py_ids_list+="\"${_id}\"," + done + _py_ids_list="[${_py_ids_list%,}]" + + local _today + _today="$(date +%Y%m%d)" + + local _prefs_file + for _prefs_file in "${_profile_path}/Preferences" "${_profile_path}/Secure Preferences"; do + if [[ ! -f "$_prefs_file" ]]; then + continue + fi + + # Backup (no sobreescribir backup del mismo día) + local _backup="${_prefs_file}.bak.${_today}" + if [[ ! -f "$_backup" ]]; then + cp "$_prefs_file" "$_backup" + fi + + # Editar con python3 si está disponible + if command -v python3 >/dev/null 2>&1; then + python3 - "$_prefs_file" "$_py_ids_list" <<'PY' || \ + echo "clean_chrome_profile_extensions: advertencia — no se pudo purgar ${_prefs_file} con python3" >&2 +import sys, json + +prefs_path = sys.argv[1] +removed_ids = json.loads(sys.argv[2]) + +with open(prefs_path, "r", encoding="utf-8") as f: + data = json.load(f) + +# 1. extensions.settings. +ext_settings = data.get("extensions", {}).get("settings", {}) +for ext_id in removed_ids: + ext_settings.pop(ext_id, None) + +# 2. extensions.pinned_extensions (lista de IDs) +pinned = data.get("extensions", {}).get("pinned_extensions", None) +if isinstance(pinned, list): + data["extensions"]["pinned_extensions"] = [ + pid for pid in pinned if pid not in removed_ids + ] + +# 3. protection.macs.extensions.settings. (Secure Preferences HMAC table) +try: + mac_ext = data["protection"]["macs"]["extensions"]["settings"] + for ext_id in removed_ids: + mac_ext.pop(ext_id, None) +except (KeyError, TypeError): + pass + +with open(prefs_path, "w", encoding="utf-8") as f: + json.dump(data, f, separators=(",", ":")) +PY + + # Fallback con jq si python3 no está disponible + elif command -v jq >/dev/null 2>&1; then + local _tmp_prefs + _tmp_prefs="$(mktemp)" + local _jq_del="" + for _id in "${_to_remove[@]}"; do + _jq_del+=" | del(.extensions.settings[\"${_id}\"])" + _jq_del+=" | del(.protection.macs.extensions.settings[\"${_id}\"])" + done + # pinned_extensions como lista + _jq_del+=" | if .extensions.pinned_extensions then .extensions.pinned_extensions -= [$(printf '"%s",' "${_to_remove[@]}" | sed 's/,$//')] else . end" + jq "${_jq_del:1}" "$_prefs_file" > "$_tmp_prefs" && mv "$_tmp_prefs" "$_prefs_file" || { + echo "clean_chrome_profile_extensions: advertencia — jq falló procesando ${_prefs_file}" >&2 + rm -f "$_tmp_prefs" + } + else + echo "clean_chrome_profile_extensions: advertencia — ni python3 ni jq disponibles; se borraron las carpetas pero no las referencias en $(basename "$_prefs_file")" >&2 + fi + done + fi + + # ── emitir resultado JSON ────────────────────────────────────────────────── + _emit_json "$_profile_path" _to_keep _to_remove +} + +# ── helpers ──────────────────────────────────────────────────────────────────── + +# _json_array_from_nameref +# Convierte un array bash (pasado por nombre de variable) en JSON array de strings. +_json_array_from_nameref() { + local -n _arr_ref="$1" + local _out="[" + local _first=1 + local _item + for _item in "${_arr_ref[@]+"${_arr_ref[@]}"}"; do + if [[ $_first -eq 1 ]]; then + _out+="\"${_item}\"" + _first=0 + else + _out+=",\"${_item}\"" + fi + done + _out+="]" + echo "$_out" +} + +# _emit_json +_emit_json() { + local _p="$1" + local _kept_json + _kept_json="$(_json_array_from_nameref "$2")" + local _removed_json + _removed_json="$(_json_array_from_nameref "$3")" + printf '{"profile":"%s","kept":%s,"removed":%s}\n' \ + "$_p" "$_kept_json" "$_removed_json" +} + +# ── auto-ejecución ──────────────────────────────────────────────────────────── +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + clean_chrome_profile_extensions "$@" +fi