# Índice dos Sites Municipais: Acessibilidade, RGPD e Segurança

Medição pública e repetível dos sites das 308 câmaras municipais portuguesas em três frentes.
Cada critério é uma pergunta de **sim ou não**, medida da mesma forma em todos os sites, com a
**prova guardada** e a data da medição. Nada aqui é opinião: se não conseguimos provar, não conta.

Edição de estreia: **2026-09** (primeira medição a 25 de Setembro de 2026).

---

## 1. O que é medido, em linguagem simples

### Acessibilidade (6 critérios que contam + 1 informativo)

| Critério | Passa quando | Referência |
|---|---|---|
| Zoom não bloqueado | Nenhuma página da amostra impede a ampliação: o último `meta viewport` não tem `user-scalable=no` nem `maximum-scale` abaixo de 2 | WCAG 2.1, 1.4.4 (AA) |
| Língua declarada | Todas as páginas da amostra têm `lang` válido no `<html>` | WCAG 2.1, 3.1.1 (A) |
| Imagens com texto alternativo | Nenhuma imagem **visível** aparece sem `alt` e sem outro nome (title, aria-label, aria-labelledby). `alt=""`, `role="presentation"` e `aria-hidden` são decorativas assumidas e não contam. Imagens com menos de 8×8 px (pixéis de contagem) não contam | WCAG 2.1, 1.1.1 (A) |
| Campos de formulário com nome | Nenhum campo visível aparece sem rótulo (`label`, aria-label, aria-labelledby, title) **nem** placeholder. Os que só têm placeholder são registados mas **não contam** | WCAG 2.1, 1.3.1, 3.3.2, 4.1.2 (A) |
| Contraste de texto | Nenhum texto fica abaixo de 4,5:1 (3:1 em letra grande) **com a falha confirmada nos pixéis** da captura (ver 3.1) | WCAG 2.1, 1.4.3 (AA) |
| Declaração de Acessibilidade | A inicial tem uma ligação para a Declaração (directa, ou a um salto de uma ligação de acessibilidade, legal, política, termos, privacidade ou cookies da inicial: observação «ligação indirecta», decisão do utilizador de 26/09/2026) e ela segue o modelo do DL 83/2018 (página, ou PDF confirmado pelo tipo de ficheiro). Observações sem baixar a nota: endereço sem `/acessibilidade` (e `/acessibilidade` não leva lá), PDF em vez de página, falta de ligação no rodapé das páginas da amostra. Estados na ficha: `ligada_da_inicial`, `publicada_sem_ligacao` (encontrada por endereço habitual, mapa do site ou outra página, sem ligação da inicial: não cumpre), `fora_do_modelo`, `nao_encontrada`. Regra de 26/09/2026 | DL 83/2018, art. 7.º e art. 8.º, n.º 2 |
| Selo (informativo) | Selo de Usabilidade e Acessibilidade com data pública de atribuição há menos de 12 meses. **Não entra na pontuação** | ARTE |

### RGPD (4 critérios)

Tudo medido numa **sessão de browser limpa, sem cookies anteriores e sem nenhum clique**. O que
acontece nesse intervalo acontece antes de qualquer consentimento.

| Critério | Passa quando | Referência |
|---|---|---|
| Sem cookies de rastreio antes do consentimento | O site não grava cookies identificadores de analítica, publicidade ou redes sociais (lista de nomes e domínios conhecidos). Cookies técnicos isentos (sessão, CSRF, balanceadores, o registo do próprio consentimento) não contam | Lei 41/2004, art. 5.º |
| Sem envio de medição antes do consentimento | O browser não **envia dados de medição ou eventos** a serviços de analítica, publicidade ou redes sociais (endereços de recolha na secção 5: `/g/collect` do Google Analytics, `/tr` da Meta, `/api/stats` do YouTube...). **Carregar o script** (GTM, gtag, SDK) sem enviar medição **não conta**: fica na ficha como informação. Decisão de método de 25/09/2026 | Lei 41/2004, art. 5.º; RGPD, art. 6.º |
| Recusar ao mesmo nível de aceitar | O aviso de cookies tem um botão de recusa no primeiro nível («Rejeitar», «Recusar», «Desativar cookies não essenciais», «Apenas necessários»...). Aviso de consentimento implícito («ao continuar a navegar aceita», só «OK», «Continuar» ou «Fechar») sem botão de recusa: falha. Aviso cujos botões não se deixam ler como aceitar ou recusar: «não medível». Sem aviso e sem rastreio: «não se aplica». Sem aviso **e** com rastreio: falha. Se a primeira leitura apanhar um painel de acessibilidade (tem «Limpar cookies»), o painel é escondido e a leitura repete-se, sem clicar | Orientações CEPD 05/2020 |
| Política de privacidade ligada da inicial | Há ligação para a política de privacidade ou de protecção de dados na página inicial | RGPD, art. 12.º e 13.º |

### Segurança (11 critérios)

Só leitura de páginas públicas e cabeçalhos. **Sem sondagens intrusivas, sem varrimento de
portas, sem tentativas de login, sem pedidos a caminhos administrativos.** O único pedido fora
da navegação normal é `readme.html`, e só em sites WordPress (é um ficheiro público do próprio
WordPress).

| Critério | Passa quando |
|---|---|
| HTTPS | A inicial serve-se em HTTPS com certificado aceite pelo Chrome |
| http redirecciona para https | Um pedido a `http://` acaba em `https://`. Porta 80 sem resposta: «não se aplica» |
| HSTS | `Strict-Transport-Security` com `max-age` maior que zero |
| Recusa TLS 1.0 e 1.1 | Um aperto de mão TLS 1.0 e outro 1.1 são recusados pelo servidor (RFC 8996) |
| Content-Security-Policy | Cabeçalho ou `meta http-equiv` com CSP |
| Protecção contra enquadramento | `X-Frame-Options` DENY/SAMEORIGIN ou `frame-ancestors` na CSP |
| X-Content-Type-Options | `nosniff` |
| Referrer-Policy | Presente e diferente de `unsafe-url` |
| Sem conteúdo misto | Nenhuma página da amostra pede imagens, scripts, estilos ou iframes por `http://`, nem envia formulários por `http://` |
| Não expõe versões de software | Nem `meta generator` (do CMS **ou de plugins**, por exemplo «qTranslate-X 3.4.6.8»; decisão de 25/09/2026), nem `Server`, `X-Powered-By` ou `X-AspNet-Version`, nem `readme.html` mostram números de versão |
| Sem bibliotecas JS vulneráveis | Nenhuma biblioteca carregada está numa versão da lista da secção 5. A versão lê-se **no próprio browser** (`jQuery.fn.jquery`, etc.), não no nome do ficheiro |

---

## 2. A amostra: igual para todos e escolhida por regra

- **Página inicial + 4 páginas** do menu principal (5 por câmara).
- **Menu principal** = o elemento `<nav>` (ou `role="navigation"`) fora do rodapé com mais
  ligações internas, excluindo navegações utilitárias (ligações de salto, migalhas, paginação,
  redes sociais, idioma, painéis de acessibilidade). Se nenhum tiver pelo menos duas, a lista
  (`<ul>` de topo) do `<header>` com mais ligações; depois o `<header>`; por fim o corpo.
- Ordem: primeiro as ligações **menos profundas** (itens de topo); em cada nível, uma de cada
  secção do menu à vez, para a amostra não cair toda na mesma secção; depois a ordem do
  documento. Só entram ligações do mesmo host; ficam de fora PDF e outros ficheiros, feeds,
  âncoras, `mailto:`, `tel:`, a própria inicial e páginas utilitárias (cookies, login, registo,
  newsletter, pesquisa, mapa do site), incluindo as que só se revelam depois de um
  redireccionamento.
- **Uma página que não devolva 200 com conteúdo sai da amostra** e é substituída pela seguinte
  da lista (até 8 tentativas). Nunca conta como falha. As descartadas ficam registadas na ficha
  com o motivo.
- Um critério **falha a câmara** quando falha em **pelo menos uma** página da amostra. A ficha diz
  em quantas e em quais.

## 3. As regras que tornam o método à prova de contestação

1. **Só sim ou não, e cada «não» traz prova.** Excerto do HTML, captura PNG recortada do
   elemento, cabeçalhos HTTP brutos, lista de pedidos de rede com tempos, ou o resultado do
   aperto de mão TLS. Tudo com carimbo de data em `dados/fichas/<concelho>.json` e
   `dados/provas/<concelho>/`.
2. **Bloqueado não é falha.** Se a inicial não abre (Cloudflare, firewall, desafio anti-robô,
   tempo esgotado, página de erro), a câmara fica **«não medível»**, é publicada como tal e fica
   fora do ranking. Só depois de **3 tentativas em momentos diferentes** se aceita o estado como
   definitivo. Na edição 2026-09 as não medíveis tiveram 2 tentativas (Horta abriu à segunda);
   ficaram 2: **Oeiras** (firewall DiamWall, «Verifying your browser») e **Abrantes** (aplicação
   Flutter desenhada em canvas: o conteúdo não existe no HTML e os critérios deste índice leem o
   HTML; não é bloqueio, é tecnologia, e fica declarado assim). A 26/09/2026 **Oeiras foi medida**
   numa janela real (`--perfil`): uma pessoa passou a verificação, apagaram-se todos os cookies
   excepto os da DiamWall e o armazenamento local, e mediu-se nesse contexto; as 5 páginas abriram
   com 200 e a ficha regista a sessão (`sessao`, `cookiesAntesDaMedicao`). No mesmo dia **Abrantes
   foi medida** pelo modo Flutter do `medir.mjs` (`medirFlutter`, activa-se sozinho quando a página
   é Flutter): RGPD e Segurança como nos outros sites; na Acessibilidade liga-se a árvore de
   semântica (Tab até ao botão escondido «Enable accessibility» e Enter) e leem-se nela imagens e
   campos, com língua e zoom do HTML real; contraste fica «não medível»; imagens e campos só dão
   «sim» se a árvore mostrar o que é preciso para o afirmar, senão «não medível». A amostra são as
   secções do menu da aplicação (rotas `/#/...`). Nesta edição ficaram medidas as 308.
3. **Na dúvida, não conta.** Um critério que não se consegue decidir é «não medível» e sai da
   conta da frente (não entra nem como sim nem como não).
4. **Browser real e actual.** Chrome 150 com o User-Agent e as client hints coerentes, janela real
   (não headless), cabeçalhos de navegação de um Chrome verdadeiro, `Accept-Language: pt-PT`.
   Nada nos pedidos identifica uma ferramenta.
5. **Ritmo educado.** No máximo 4 câmaras em paralelo (a medição de 2026-09 correu com `--concorrencia 4`; o valor por omissão do `medir.mjs` é 3), cada uma num servidor diferente; dentro de
   cada site, uma página de cada vez com 2 a 4 segundos de pausa. Cerca de 6 a 10 pedidos de
   página por câmara.
6. **«Não encontrámos» nunca é «não existe».** O critério da Declaração diz «não encontrámos a
   partir da página inicial nem nos caminhos habituais», e é assim que se publica.

### 3.1 Contraste: porque é que o CSS não chega

Os estilos calculados dizem que cor tem cada caixa, **não o que fica pintado**. Um fundo pintado
por um pseudo-elemento, uma imagem ou um gradiente, texto escondido para leitores de ecrã, um aviso
que só aparece a browsers desactualizados: tudo isto já produziu falhas falsas em medições
anteriores. Por isso um contraste só conta quando **três coisas se confirmam na captura** do
elemento (feita a 2× para os glifos terem pixéis de cor plena):

- a cor dominante do recorte é a cor de fundo calculada (tolerância de 20 em cada canal) e ocupa
  pelo menos 30% do recorte;
- existem pixéis com a cor do texto calculada (tolerância de 48);
- **nenhum pixel** do recorte chega ao contraste exigido contra esse fundo.

Se alguma falhar, o candidato é descartado. O valor publicado é o rácio das cores calculadas,
que a captura confirmou estarem de facto pintadas. A captura fica guardada.

Candidatos que nunca chegam ao teste dos pixéis: texto com sombra, texto que transborda de uma
caixa com imagem ou gradiente, fundo semitransparente, antepassado com `::before`/`::after`
opaco por cima, texto só para leitores de ecrã (caixa ≤ 2 px, `clip`, `text-indent` negativo),
texto recortado por um antepassado de 1×1 px, texto em `<details>` fechado ou escondido
(`checkVisibility`), texto em várias linhas, e os nós do aviso do browser-update.org.

## 4. Pontuação

- Em cada frente: **percentagem de critérios cumpridos entre os medidos** (0 a 100). Critérios
  «não medível» e «não se aplica» saem do denominador. O Selo não entra.
- Global: média simples das três frentes.
- O ranking ordena pela global; empates partilham a posição.
- Os pesos são iguais de propósito: qualquer outra ponderação é uma opinião nossa.

## 5. Listas usadas (publicadas para poderem ser verificadas)

**Bibliotecas com vulnerabilidade pública conhecida**

| Biblioteca | Versões afectadas | CVE |
|---|---|---|
| jQuery | < 3.5.0 | CVE-2020-11022, CVE-2020-11023 |
| jQuery UI | < 1.13.2 | CVE-2021-41182, 41183, 41184, CVE-2022-31160 |
| Bootstrap | < 3.4.1 e 4.0.0 a 4.3.0 | CVE-2019-8331 |
| Lodash | < 4.17.21 | CVE-2021-23337 |
| Underscore | 1.3.2 a 1.12.0 | CVE-2021-23358 |
| Moment.js | < 2.29.4 | CVE-2022-31129 |
| AngularJS (1.x) | todas (fim de vida) | CVE-2022-25869 |
| Handlebars | < 4.7.7 | CVE-2021-23369 |

A presença de uma versão vulnerável é um facto verificável; a exploração depende de como a
biblioteca é usada, e isso não se mede de fora. Publica-se como «biblioteca com vulnerabilidade
conhecida carregada», nunca como «site vulnerável».

**Endereços de envio de medição** (o que conta no critério de rastreio; caminho do URL):

| Domínio | Caminhos que contam como envio |
|---|---|
| google-analytics.com | `/collect`, `/g/collect`, `/j/collect`, `/r/collect`, `/__utm.gif`, `/batch` |
| doubleclick.net | `/g/collect`, `/pagead/id`, `/pagead/viewthroughconversion`, `/pagead/1p-user-list`, `/activity`, `/ddm/`, `/collect` |
| googletagmanager.com | `/a`, `/td` (os pings da etiqueta Google; `/gtag/js` e `/gtm.js` são scripts e não contam) |
| googleadservices.com | `/pagead/conversion` |
| facebook.com | `/tr` |
| youtube.com | `/api/stats/`, `/ptracking`, `/youtubei/v1/log_event` |
| sharethis.com | `/pview`, `/log` |
| addthis.com | `/live/red_lojson`, `/live/red_pixel` |
| clarity.ms, hotjar.com, linkedin.com, bing.com, tiktok.com, twitter.com, scorecardresearch.com | os endereços de recolha de cada um (lista `ENVIO_DE_MEDICAO` em `medir.mjs`) |

**Domínios de rastreio** (os pedidos a estes ficam registados; só os envios acima contam): doubleclick.net, google-analytics.com,
googletagmanager.com, googleadservices.com, googlesyndication.com, facebook.com, facebook.net,
instagram.com, hotjar.com, clarity.ms, bing.com, linkedin.com, licdn.com, tiktok.com, criteo.com,
criteo.net, adform.net, taboola.com, outbrain.com, pinterest.com, snapchat.com, reddit.com,
hubspot.com, hs-analytics.net, hs-scripts.com, segment.io, mixpanel.com, amplitude.com, yandex.ru,
quantserve.com, scorecardresearch.com, addthis.com, sharethis.com, twitter.com, ads-twitter.com,
youtube.com, matomo.cloud. O `youtube-nocookie.com` não conta (é a versão sem cookies).

**Cookies identificadores**: os de `audit/legal-checks.mjs` (`_ga`, `_gid`, `_gcl_*`, `_fbp`,
`fr`, `_hj*`, `_clck`, `MUID`, `VISITOR_INFO1_LIVE`, `YSC`, `_pk_id`, e outros), mais qualquer
cookie gravado num domínio da lista acima. Uma guarda marca a sessão como contaminada (e o
critério como «não medível») se aparecer um cookie de um domínio que o site nunca contactou.

## 6. Limites declarados

- **É uma amostra de 5 páginas.** Um site pode falhar noutras páginas; um «sim» diz que não
  encontrámos falhas na amostra, não que o site cumpre a WCAG. As ferramentas automáticas cobrem
  uma parte pequena dos requisitos WCAG (a própria ARTE diz «no máximo 30%»).
- **Critérios de acessibilidade escolhidos por serem inequívocos**, não por serem os mais
  importantes. Navegação por teclado, legendas de vídeo, PDF acessíveis e muito mais ficam de fora.
- **O contraste é conservador**: só se contam falhas confirmadas nos pixéis, até 6 candidatos por
  página. Falhas sobre fotografias, gradientes ou em texto só visível ao passar o rato não se
  medem. O número real é quase sempre maior.
- **O Selo** vem do registo datado em `data/selo-titulares.json`: com data de atribuição pública
  há menos de 12 meses diz-se «válido»; sem data, não se afirma. O Selo não certifica ausência de
  falhas (exige limiares, não zero), por isso não entra na pontuação.
- **Declaração**: procurada nas ligações da inicial (as que dizem «acessibilidade» ou
  «declaração»), um nível abaixo, e em três caminhos habituais. Declarações só em PDF contam se a
  ligação disser «declaração», mas o PDF não é lido.
- **RGPD**: mede-se o que acontece **sem clicar**. Não testamos se o «recusar» funciona depois de
  clicado, nem o conteúdo da política de privacidade. Um envio de medição é um facto (o browser
  do visitante mandou dados à Google, à Meta...); a sua legalidade concreta é uma questão jurídica
  que o índice não decide. O simples carregamento de um script de terceiros fica registado mas não
  conta. A lista de endereços de recolha é finita: um envio para um endereço que não esteja nela
  não é contado (o erro possível é a favor da câmara).
- **Segurança**: cabeçalhos e TLS medem configuração, não intrusão. Ausência de CSP não é uma
  vulnerabilidade explorada; é uma defesa em falta. O critério http→https não conta com a lista
  de pré-carregamento HSTS dos browsers: mede o que o servidor responde.
- **Declaração, segunda leitura.** Na edição 2026-09 as 145 câmaras sem Declaração encontrada
  foram relidas (só este critério) depois de se acrescentar o texto alternativo das imagens-ligação
  à procura; 4 passaram a «sim». Quando o registo do Observatório da ARTE inscreve uma Declaração
  que não encontrámos, a ficha di-lo.
- **Língua**: `xml:lang` sem `lang` conta como falha (os browsers ignoram-no em HTML) e a ficha
  explica-o.
- **Uma medição é uma fotografia datada.** Os sites mudam; a data de cada ficha é a que vale.
