'ids', 'number' => 0, ) ) as $site_id ) { switch_to_blog( (int) $site_id ); hermes_conector_desativar_site(); restore_current_blog(); } } /** * Desativação no site atual: tira a poda do cron e apaga prefixos e marcas. */ function hermes_conector_desativar_site() { wp_clear_scheduled_hook( 'hermes_conector_podar' ); global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- plugin's own table. if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $wpdb->prefix . 'blog_juridico_visitas' ) ) ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- plugin's own table. $wpdb->query( "UPDATE {$wpdb->prefix}blog_juridico_visitas SET ip_prefixo = '', marca = '' WHERE ip_prefixo <> '' OR marca <> ''" ); } } /** * Cria a tabela de leituras e migra para a versão 2 do esquema. */ function hermes_conector_garantir_tabela() { if ( get_option( 'blog_juridico_db_versao' ) === HERMES_CONECTOR_DB_VERSAO ) { return; } global $wpdb; // phpcs:disable WordPress.DB.DirectDatabaseQuery -- plugin's own table: schema migration. // GET_LOCK: 0 = outra requisição está migrando. Qualquer outra resposta (1, NULL, // ou o que um banco sem a função devolver, ex.: SQLite) segue: a migração é idempotente. $trava = $wpdb->prefix . 'hermes_conector_db'; $travou = $wpdb->get_var( $wpdb->prepare( 'SELECT GET_LOCK(%s, 0)', $trava ) ); if ( '0' === (string) $travou ) { return; } require_once ABSPATH . 'wp-admin/includes/upgrade.php'; $charset = $wpdb->get_charset_collate(); dbDelta( "CREATE TABLE {$wpdb->prefix}blog_juridico_visitas ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, post_id bigint(20) unsigned NOT NULL, em datetime NOT NULL, origem varchar(120) NOT NULL DEFAULT '', utm_source varchar(80) NOT NULL DEFAULT '', utm_medium varchar(80) NOT NULL DEFAULT '', utm_campaign varchar(120) NOT NULL DEFAULT '', dispositivo varchar(12) NOT NULL DEFAULT '', sistema varchar(20) NOT NULL DEFAULT '', ip_prefixo varchar(45) NOT NULL DEFAULT '', pais varchar(2) NOT NULL DEFAULT '', marca char(16) NOT NULL DEFAULT '', PRIMARY KEY (id), KEY post_em (post_id, em), KEY em (em) ) $charset;" ); $tem_linha = (bool) $wpdb->get_var( "SELECT id FROM {$wpdb->prefix}blog_juridico_visitas LIMIT 1" ); // Tabela recriada depois de excluir o plugin: os ids continuam de onde pararam // (o Hermes lê "id > cursor" e não aceita id repetido). $ultimo = (int) get_option( 'blog_juridico_visitas_ultimo_id', 0 ); if ( $ultimo > 0 && ! $tem_linha ) { $wpdb->query( $wpdb->prepare( "ALTER TABLE {$wpdb->prefix}blog_juridico_visitas AUTO_INCREMENT = %d", $ultimo + 1 ) ); } delete_option( 'blog_juridico_visitas_ultimo_id' ); // Atualização a partir da 0.6.0 com leituras guardadas: o Hermes já lê este site. if ( $tem_linha && ! get_option( 'blog_juridico_hermes_visto_em' ) ) { update_option( 'blog_juridico_hermes_visto_em', time(), false ); } // Minimização: utm_campaign não é usado pelo Hermes; o freio antigo (md5 do IP) sai do wp_options. $wpdb->query( "UPDATE {$wpdb->prefix}blog_juridico_visitas SET utm_campaign = '' WHERE utm_campaign <> ''" ); $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s", $wpdb->esc_like( '_transient_bj_bat_' ) . '%', $wpdb->esc_like( '_transient_timeout_bj_bat_' ) . '%' ) ); add_option( 'blog_juridico_gsc_token', '' ); // Opção inexistente custa 1 SELECT por página. add_option( 'blog_juridico_gsc_arquivo', '' ); add_option( 'blog_juridico_contar_leituras', '1' ); update_option( 'blog_juridico_db_versao', HERMES_CONECTOR_DB_VERSAO ); $wpdb->query( $wpdb->prepare( 'SELECT RELEASE_LOCK(%s)', $trava ) ); // phpcs:enable } /** * Tabela apagada com a opção de versão ainda gravada (restauração parcial): a próxima carga recria. */ function hermes_conector_conferir_tabela() { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- plugin's own table. if ( ! $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $wpdb->prefix . 'blog_juridico_visitas' ) ) ) ) { delete_option( 'blog_juridico_db_versao' ); } } /** * Cópias ativas do Hermes (este arquivo incluído, zip manual em outra pasta, ou o * antigo "Conector Blog Jurídico"). Lê só os ativos, sem get_plugins() a cada tela. * * @return string[] Arquivos de plugin (pasta/arquivo.php). */ function hermes_conector_copias_ativas() { $ativos = (array) get_option( 'active_plugins', array() ); if ( is_multisite() ) { $ativos = array_merge( $ativos, array_keys( (array) get_site_option( 'active_sitewide_plugins', array() ) ) ); } $copias = array(); foreach ( array_unique( $ativos ) as $arquivo ) { $arquivo = (string) $arquivo; if ( ! in_array( basename( $arquivo ), array( 'hermes-conector.php', 'conector-blog-juridico.php' ), true ) ) { continue; } $caminho = WP_PLUGIN_DIR . '/' . $arquivo; if ( ! is_readable( $caminho ) ) { continue; } $dados = get_file_data( $caminho, array( 'nome' => 'Plugin Name', 'uri' => 'Plugin URI', ) ); if ( 'Conector Blog Jurídico' === $dados['nome'] || false !== strpos( (string) $dados['uri'], 'hermes.adv.br' ) ) { $copias[] = $arquivo; } } return $copias; } /** * Telas onde os avisos do Hermes aparecem: Plugins e Configurações › Hermes. * * @return bool */ function hermes_conector_tela_dos_avisos() { $tela = function_exists( 'get_current_screen' ) ? get_current_screen() : null; return $tela && in_array( $tela->id, array( 'plugins', 'plugins-network', 'settings_page_hermes-conector' ), true ); } /** * Ganchos do aviso de cópias: o aviso, o script que grava a dispensa e a rota AJAX. */ function hermes_conector_ganchos_do_aviso() { add_action( 'admin_notices', 'hermes_conector_aviso_copias' ); add_action( 'network_admin_notices', 'hermes_conector_aviso_copias' ); add_action( 'admin_enqueue_scripts', 'hermes_conector_script_dispensar' ); add_action( 'wp_ajax_hermes_conector_dispensar', 'hermes_conector_dispensar_aviso' ); } /** * Cópias a mais (todas menos a que fica). Fica a da pasta hermes-conector/ (a que * recebe atualização da loja); sem ela, fica a que está rodando. * * @return string[] Arquivos de plugin (pasta/arquivo.php), em ordem. */ function hermes_conector_copias_a_mais() { $copias = hermes_conector_copias_ativas(); if ( count( $copias ) < 2 ) { return array(); } $fica = in_array( 'hermes-conector/hermes-conector.php', $copias, true ) ? 'hermes-conector/hermes-conector.php' : plugin_basename( HERMES_CONECTOR_ARQUIVO ); $a_mais = array_values( array_diff( $copias, array( $fica ) ) ); sort( $a_mais ); return $a_mais; } /** * O usuário dispensou este aviso, nesta mesma situação? Uma cópia nova volta a avisar. * * @param string $aviso Chave do aviso. * @param string $assinatura A situação que ele dispensou. * @return bool */ function hermes_conector_aviso_dispensado( $aviso, $assinatura ) { $dispensados = get_user_meta( get_current_user_id(), 'hermes_conector_dispensado', true ); return is_array( $dispensados ) && isset( $dispensados[ $aviso ] ) && $dispensados[ $aviso ] === $assinatura; } /** * Aviso quando há mais de uma cópia ativa, só na tela de Plugins e na do Hermes, e * dispensável por usuário. Nunca desativa outro plugin sozinho (a loja proíbe): o * aviso leva ao "Desativar" do próprio WordPress. */ function hermes_conector_aviso_copias() { if ( ! current_user_can( 'activate_plugins' ) || ! hermes_conector_tela_dos_avisos() ) { return; } $a_mais = hermes_conector_copias_a_mais(); if ( ! $a_mais || hermes_conector_aviso_dispensado( 'copias', implode( ',', $a_mais ) ) ) { return; } foreach ( $a_mais as $arquivo ) { if ( is_multisite() && is_plugin_active_for_network( $arquivo ) && ! current_user_can( 'manage_network_plugins' ) ) { continue; } $url = wp_nonce_url( self_admin_url( 'plugins.php?action=deactivate&plugin=' . rawurlencode( $arquivo ) ), 'deactivate-plugin_' . $arquivo ); /* translators: %s: plugin file, e.g. hermes-conector-1/hermes-conector.php */ $texto = sprintf( __( 'Another copy of Hermes is active (%s). Only one copy works at a time: deactivate this extra copy, then delete it. Your data is kept.', 'hermes-conector' ), $arquivo ); printf( '

%1$s %3$s

', esc_html( $texto ), esc_url( $url ), esc_html__( 'Deactivate the extra copy', 'hermes-conector' ) ); } } /** * O X do aviso (is-dismissible) só esconde na tela; este script grava a dispensa. */ function hermes_conector_script_dispensar() { if ( ! current_user_can( 'activate_plugins' ) || ! hermes_conector_tela_dos_avisos() || ! hermes_conector_copias_a_mais() ) { return; } $config = wp_json_encode( array( 'url' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'hermes_conector_dispensar' ), ) ); wp_add_inline_script( 'common', '(function (c) { document.addEventListener("click", function (e) {' . ' var b = e.target && e.target.closest ? e.target.closest(".hermes-conector-aviso .notice-dismiss") : null;' . ' if (!b || !window.fetch) { return; }' . ' var d = new FormData(); d.append("action", "hermes_conector_dispensar");' . ' d.append("aviso", b.closest(".hermes-conector-aviso").getAttribute("data-aviso")); d.append("_ajax_nonce", c.nonce);' . ' fetch(c.url, {method: "POST", body: d, credentials: "same-origin"});' . ' }); })(' . $config . ');' ); } /** * AJAX: grava, para o usuário logado, que ele dispensou o aviso de cópias — nesta * situação (a lista de cópias a mais é lida aqui, nunca vem do navegador). */ function hermes_conector_dispensar_aviso() { check_ajax_referer( 'hermes_conector_dispensar' ); if ( ! current_user_can( 'activate_plugins' ) ) { wp_send_json_error( null, 403 ); } $aviso = isset( $_POST['aviso'] ) ? sanitize_key( wp_unslash( $_POST['aviso'] ) ) : ''; if ( 'copias' !== $aviso ) { wp_send_json_error( null, 400 ); } $dispensados = get_user_meta( get_current_user_id(), 'hermes_conector_dispensado', true ); $dispensados = is_array( $dispensados ) ? $dispensados : array(); $dispensados['copias'] = implode( ',', hermes_conector_copias_a_mais() ); update_user_meta( get_current_user_id(), 'hermes_conector_dispensado', $dispensados ); wp_send_json_success(); } /** * Metas de SEO graváveis pela REST: Yoast e Rank Math não registram os campos com * show_in_rest, e a REST devolve 200 descartando o meta em silêncio. */ function hermes_conector_registrar_meta() { foreach ( array( '_yoast_wpseo_title', '_yoast_wpseo_metadesc', 'rank_math_title', 'rank_math_description' ) as $campo ) { register_post_meta( 'post', $campo, array( 'show_in_rest' => true, 'single' => true, 'type' => 'string', 'sanitize_callback' => 'hermes_conector_texto_seo', 'auth_callback' => function ( $permitido, $chave, $post_id ) { return current_user_can( 'edit_post', (int) $post_id ); }, ) ); } } /** * Nunca sanitize_text_field: ele apaga %xx e estragaria %%title%%/%category% do Yoast/Rank Math. * Quem tem unfiltered_html grava exatamente o que mandou (a leitura de volta do Hermes compara igual). * * @param mixed $valor Valor recebido. * @return string */ function hermes_conector_texto_seo( $valor ) { $valor = is_scalar( $valor ) ? (string) $valor : ''; return current_user_can( 'unfiltered_html' ) ? $valor : wp_strip_all_tags( $valor ); } /** * Cabeçalhos e UA. NÃO usar para REQUEST_URI (sanitize_text_field apaga %xx). * * @param string $chave Chave de $_SERVER. * @return string */ function hermes_conector_servidor( $chave ) { return isset( $_SERVER[ $chave ] ) ? trim( sanitize_text_field( wp_unslash( $_SERVER[ $chave ] ) ) ) : ''; } /** * Host de uma URL, sem www, ou '' se não for um host simples. * * @param string $url URL ou origem. * @return string */ function hermes_conector_host( $url ) { $host = strtolower( (string) wp_parse_url( (string) $url, PHP_URL_HOST ) ); $host = (string) preg_replace( '/^www\./', '', $host ); return preg_match( '/^[a-z0-9.-]{1,120}$/', $host ) ? $host : ''; } /** * Robôs, monitores e prévias de link não contam como leitura. * * @return bool */ function hermes_conector_eh_robo() { $ua = strtolower( hermes_conector_servidor( 'HTTP_USER_AGENT' ) ); if ( '' === $ua || 0 === strpos( $ua, 'whatsapp/' ) ) { // Prévia de link do WhatsApp. return true; } $marcas = (array) apply_filters( 'hermes_conector_marcas_robo', array( 'bot', 'crawl', 'spider', 'slurp', 'curl', 'wget', 'python', 'headless', 'lighthouse', 'preview', 'monitor', 'facebookexternalhit', 'meta-externalagent', 'embedly', 'go-http-client', 'okhttp', 'axios', 'node-fetch', 'java/', 'httpclient', 'libwww', 'scrapy', 'gtmetrix', 'pingdom', 'uptime', 'wordpress/', ) ); foreach ( $marcas as $marca ) { if ( '' !== (string) $marca && false !== strpos( $ua, (string) $marca ) ) { return true; } } return false; } /** * Oposição automática: Global Privacy Control ou Do Not Track. * * @return bool */ function hermes_conector_visitante_recusou() { return '1' === hermes_conector_servidor( 'HTTP_SEC_GPC' ) || '1' === hermes_conector_servidor( 'HTTP_DNT' ); } /** * Tipo de aparelho pelo UA; sem pista no UA, a largura de tela (0/1 no sinal novo, * pixels no HTML antigo em cache). * * @param int $largura 1 = tela estreita (ou a largura em pixels). * @return string */ function hermes_conector_dispositivo( $largura ) { $ua = strtolower( hermes_conector_servidor( 'HTTP_USER_AGENT' ) ); if ( false !== strpos( $ua, 'ipad' ) || ( false !== strpos( $ua, 'android' ) && false === strpos( $ua, 'mobile' ) ) ) { return 'tablet'; } if ( false !== strpos( $ua, 'mobile' ) || false !== strpos( $ua, 'iphone' ) || false !== strpos( $ua, 'android' ) ) { return 'celular'; } if ( $largura > 0 && $largura < 700 ) { return 'celular'; } return 'computador'; } /** * Sistema operacional pelo UA (o UA em si não é guardado). * * @return string */ function hermes_conector_sistema() { $ua = strtolower( hermes_conector_servidor( 'HTTP_USER_AGENT' ) ); if ( false !== strpos( $ua, 'iphone' ) || false !== strpos( $ua, 'ipad' ) ) { return 'ios'; } if ( false !== strpos( $ua, 'android' ) ) { return 'android'; } if ( false !== strpos( $ua, 'windows' ) ) { return 'windows'; } if ( false !== strpos( $ua, 'mac os' ) || false !== strpos( $ua, 'macintosh' ) ) { return 'mac'; } if ( false !== strpos( $ua, 'linux' ) ) { return 'linux'; } return ''; } /** * IPv4 escrito como IPv6 (::ffff:1.2.3.4) volta a ser IPv4. * * @param string $ip Endereço. * @return string */ function hermes_conector_ipv4_mapeado( $ip ) { if ( 0 === stripos( $ip, '::ffff:' ) && filter_var( substr( $ip, 7 ), FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) { return substr( $ip, 7 ); } return $ip; } /** * Rede privada ou reservada (proxy local, loopback), ou a faixa compartilhada * 100.64.0.0/10, que algumas hospedagens usam entre o proxy e o PHP. * * @param string $ip Endereço. * @return bool */ function hermes_conector_ip_interno( $ip ) { if ( ! filter_var( $ip, FILTER_VALIDATE_IP ) ) { return false; } return ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) || hermes_conector_ip_na_faixa( $ip, '100.64.0.0/10' ); } /** * O endereço está na faixa CIDR? * * @param string $ip Endereço. * @param string $faixa Faixa, ex.: 173.245.48.0/20. * @return bool */ function hermes_conector_ip_na_faixa( $ip, $faixa ) { $partes = explode( '/', (string) $faixa, 2 ); if ( ! filter_var( $ip, FILTER_VALIDATE_IP ) || ! filter_var( $partes[0], FILTER_VALIDATE_IP ) ) { return false; } $a = inet_pton( $ip ); $b = inet_pton( $partes[0] ); if ( false === $a || false === $b || strlen( $a ) !== strlen( $b ) ) { return false; } $bits = isset( $partes[1] ) ? (int) $partes[1] : strlen( $a ) * 8; $bits = max( 0, min( $bits, strlen( $a ) * 8 ) ); $cheios = intdiv( $bits, 8 ); if ( substr( $a, 0, $cheios ) !== substr( $b, 0, $cheios ) ) { return false; } $resto = $bits % 8; if ( 0 === $resto ) { return true; } $mascara = ( 0xff << ( 8 - $resto ) ) & 0xff; return ( ord( $a[ $cheios ] ) & $mascara ) === ( ord( $b[ $cheios ] ) & $mascara ); } /** * Faixas de https://www.cloudflare.com/ips/ (conferidas em 10/10/2026). * * @param string $ip Endereço de quem entregou o pedido. * @return bool */ function hermes_conector_eh_cloudflare( $ip ) { $faixas = (array) apply_filters( 'hermes_conector_proxies_confiaveis', array( '173.245.48.0/20', '103.21.244.0/22', '103.22.200.0/22', '103.31.4.0/22', '141.101.64.0/18', '108.162.192.0/18', '190.93.240.0/20', '188.114.96.0/20', '197.234.240.0/22', '198.41.128.0/17', '162.158.0.0/15', '104.16.0.0/13', '104.24.0.0/14', '172.64.0.0/13', '131.0.72.0/22', '2400:cb00::/32', '2606:4700::/32', '2803:f800::/32', '2405:b500::/32', '2405:8100::/32', '2a06:98c0::/29', '2c0f:f248::/32', ) ); foreach ( $faixas as $faixa ) { if ( hermes_conector_ip_na_faixa( $ip, (string) $faixa ) ) { return true; } } return false; } /** * IP do visitante. Cabeçalho de proxy só vale quando quem entregou o pedido é * proxy local (rede privada) ou a Cloudflare. * * @return array{ip: string, cloudflare: bool} */ function hermes_conector_cliente() { $ip = hermes_conector_ipv4_mapeado( hermes_conector_servidor( 'REMOTE_ADDR' ) ); if ( ! filter_var( $ip, FILTER_VALIDATE_IP ) ) { return array( 'ip' => '', 'cloudflare' => false, ); } // Cada proxy interno acrescenta o salto anterior no FIM: anda da direita para a esquerda. $saltos = array_reverse( array_filter( array_map( 'trim', explode( ',', hermes_conector_servidor( 'HTTP_X_FORWARDED_FOR' ) ) ) ) ); foreach ( $saltos as $salto ) { if ( ! hermes_conector_ip_interno( $ip ) || ! filter_var( $salto, FILTER_VALIDATE_IP ) ) { break; } $ip = hermes_conector_ipv4_mapeado( $salto ); } if ( hermes_conector_ip_interno( $ip ) ) { $real = hermes_conector_servidor( 'HTTP_X_REAL_IP' ); if ( filter_var( $real, FILTER_VALIDATE_IP ) ) { $ip = hermes_conector_ipv4_mapeado( $real ); } } $cf = hermes_conector_eh_cloudflare( $ip ); if ( $cf ) { $c = hermes_conector_servidor( 'HTTP_CF_CONNECTING_IP' ); if ( filter_var( $c, FILTER_VALIDATE_IP ) ) { $ip = hermes_conector_ipv4_mapeado( $c ); } } return array( 'ip' => (string) apply_filters( 'hermes_conector_ip_visitante', $ip ), 'cloudflare' => $cf, ); } /** * Só o "bairro" da rede: basta para a cidade, não aponta a pessoa. Rede interna ou * reservada não indica cidade nenhuma. * * @param string $ip Endereço. * @return string */ function hermes_conector_ip_prefixo( $ip ) { if ( ! filter_var( $ip, FILTER_VALIDATE_IP ) || hermes_conector_ip_interno( $ip ) ) { return ''; } if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) { $p = explode( '.', $ip ); return $p[0] . '.' . $p[1] . '.' . $p[2] . '.0'; } $bin = inet_pton( $ip ); if ( false === $bin || 16 !== strlen( $bin ) ) { return ''; } $bytes = max( 4, min( 8, (int) apply_filters( 'hermes_conector_bytes_ipv6', 6 ) ) ); // 6 = /48. return (string) inet_ntop( substr( $bin, 0, $bytes ) . str_repeat( "\0", 16 - $bytes ) ); } /** * "Mesmo leitor em 30 s" sem guardar o IP: HMAC com segredo do wp-config que muda todo dia. * Só serve 30 s: a poda diária a apaga depois de 1 dia, e o /visitas junto com o prefixo. * * @param string $ip Endereço do visitante. * @return string */ function hermes_conector_marca_do_leitor( $ip ) { return substr( hash_hmac( 'sha256', $ip . '|' . hermes_conector_servidor( 'HTTP_USER_AGENT' ), wp_salt( 'nonce' ) . gmdate( 'Y-m-d' ) ), 0, 16 ); } /** * Interruptor de Configurações › Hermes (padrão ligado) ou o filtro. * * @return bool */ function hermes_conector_contagem_ligada() { return (bool) apply_filters( 'hermes_conector_contar_leituras', '1' === (string) get_option( 'blog_juridico_contar_leituras', '1' ) ); } /** * O Hermes passou por aqui? Só vale a chamada autenticada por senha de aplicação: * assim um colaborador navegando no painel não liga a coleta. Grava no máximo 1x/hora. */ function hermes_conector_hermes_passou() { if ( ! did_action( 'application_password_did_authenticate' ) ) { return; } if ( time() - (int) get_option( 'blog_juridico_hermes_visto_em', 0 ) > HOUR_IN_SECONDS ) { $antes = hermes_conector_leituras_ativas(); update_option( 'blog_juridico_hermes_visto_em', time(), false ); if ( ! $antes && hermes_conector_leituras_ativas() ) { hermes_conector_limpar_cache(); // As páginas guardadas sem o sinal de leitura passam a tê-lo. } } } /** * Decide se a batida é gravada e se o sinal entra na página. * * @return bool */ function hermes_conector_leituras_ativas() { $visto = (int) get_option( 'blog_juridico_hermes_visto_em', 0 ); return hermes_conector_contagem_ligada() && $visto > 0 && ( time() - $visto ) < HERMES_CONECTOR_DIAS_SEM_HERMES * DAY_IN_SECONDS; } /** * Quem pode ler as leituras detalhadas (padrão: editor ou administrador). * * @return bool */ function hermes_conector_pode_ler_leituras() { return current_user_can( (string) apply_filters( 'hermes_conector_capacidade_leituras', 'edit_others_posts' ) ); } /** * UPDATE atômico: sem corrida e sem disparar os ganchos de meta de todo plugin * (updated_post_meta invalida o cache de WP_Query do site inteiro). * * @param int $post_id Post lido. */ function hermes_conector_somar_vista( $post_id ) { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- atomic counter; cache cleared below. $mudou = $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_value = CAST(meta_value AS UNSIGNED) + 1 WHERE post_id = %d AND meta_key = %s", $post_id, HERMES_CONECTOR_META_VISTAS ) ); if ( ! $mudou ) { add_post_meta( $post_id, HERMES_CONECTOR_META_VISTAS, 1, true ); } wp_cache_delete( $post_id, 'post_meta' ); } /** * Página em que o sinal de leitura entra. * * @return bool */ function hermes_conector_pagina_de_leitura() { if ( ! is_singular( 'post' ) || is_user_logged_in() || is_preview() || is_feed() || is_embed() || is_customize_preview() ) { return false; } if ( post_password_required( get_queried_object() ) ) { return false; } if ( function_exists( 'amp_is_request' ) && amp_is_request() ) { return false; } // Só o estado do site decide (nunca um cabeçalho da requisição: o HTML vai para o // cache). Sem o Hermes conectado, cada leitura seria um pedido que /batida descarta. return hermes_conector_leituras_ativas(); } /** * Sinal de leitura: script inline (sem arquivo) no rodapé, pela API de scripts. * Respeita GPC/DNT e, se houver, a WP Consent API (categoria "statistics"). */ function hermes_conector_sinal_de_leitura() { if ( ! hermes_conector_pagina_de_leitura() ) { return; } $id = (int) get_queried_object_id(); if ( ! $id ) { return; } $config = wp_json_encode( array( 'post' => $id, 'url' => esc_url_raw( rest_url( 'blog-juridico/v1/batida' ) ), 'categoria' => (string) apply_filters( 'hermes_conector_categoria_consentimento', 'statistics' ), ), JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ); $js = implode( "\n", array( '(function (config) {', ' try {', ' var nav = navigator, enviado = false;', ' if (nav.globalPrivacyControl === true || nav.doNotTrack === "1" || window.doNotTrack === "1") {', ' return;', ' }', ' function enviar() {', ' if (enviado) {', ' return;', ' }', ' if (typeof wp_has_consent === "function" && !wp_has_consent(config.categoria)) {', ' return;', ' }', ' enviado = true;', ' var busca = new URLSearchParams(location.search), veioDe = "";', ' try {', ' veioDe = document.referrer ? new URL(document.referrer).origin : "";', ' } catch (erro) {}', ' var dados = JSON.stringify({', ' post: config.post,', ' ref: veioDe,', ' us: busca.get("utm_source") || "",', ' um: busca.get("utm_medium") || "",', ' w: (screen.width > 0 && screen.width < 700) ? 1 : 0', ' });', ' try {', ' if (nav.sendBeacon && nav.sendBeacon(config.url, new Blob([dados], {type: "text/plain"}))) {', ' return;', ' }', ' } catch (erro) {}', ' if (window.fetch) {', ' fetch(config.url, {method: "POST", body: dados, keepalive: true, credentials: "omit"});', ' }', ' }', ' document.addEventListener("wp_listen_for_consent_change", enviar);', ' if (document.prerendering) {', ' document.addEventListener("prerenderingchange", enviar, {once: true});', ' } else {', ' enviar();', ' }', ' } catch (erro) {}', '})(' . $config . ');', ) ); $deps = wp_script_is( 'wp-consent-api', 'registered' ) ? array( 'wp-consent-api' ) : array(); // Sem arquivo: só o inline, no rodapé. wp_register_script( 'hermes-conector-leitura', false, $deps, HERMES_CONECTOR_VERSAO, true ); wp_enqueue_script( 'hermes-conector-leitura' ); wp_add_inline_script( 'hermes-conector-leitura', $js ); } /** * Public on purpose (anonymous readers): always answers 204 with no body, accepts only * published, non-password posts, checks Origin, caps size and rate, and stores no full IP. * The body is read raw because navigator.sendBeacon sends text/plain. * * @param WP_REST_Request $requisicao Request. * @return WP_REST_Response */ function hermes_conector_registrar_batida( $requisicao ) { $nada = new WP_REST_Response( null, 204 ); if ( ! hermes_conector_leituras_ativas() || hermes_conector_eh_robo() || hermes_conector_visitante_recusou() ) { return $nada; } $meu = hermes_conector_host( home_url() ); $origem = (string) $requisicao->get_header( 'origin' ); if ( '' !== $origem && ! in_array( hermes_conector_host( $origem ), (array) apply_filters( 'hermes_conector_hosts_do_site', array( $meu ) ), true ) ) { return $nada; // Outro site (ou "null") não faz os visitantes DELE contarem aqui. } $bruto = (string) $requisicao->get_body(); if ( '' === $bruto || strlen( $bruto ) > 4096 ) { return $nada; } $corpo = json_decode( $bruto, true, 3 ); if ( ! is_array( $corpo ) || ! isset( $corpo['post'] ) || ! is_numeric( $corpo['post'] ) ) { return $nada; } $post_id = absint( $corpo['post'] ); $post = $post_id ? get_post( $post_id ) : null; if ( ! $post || 'post' !== $post->post_type || 'publish' !== $post->post_status || '' !== (string) $post->post_password ) { return $nada; } if ( ! apply_filters( 'hermes_conector_registrar_leitura', true, $post_id, $requisicao ) ) { return $nada; } global $wpdb; $agora = time(); // phpcs:disable WordPress.DB.DirectDatabaseQuery -- plugin's own table; counters must be fresh. // Tetos do site inteiro: só leem pela chave primária, sem gravar nada. $min = max( 10, (int) apply_filters( 'hermes_conector_batidas_por_minuto', 120 ) ); $dia = max( $min, (int) apply_filters( 'hermes_conector_batidas_por_dia', 20000 ) ); foreach ( array( array( $min, MINUTE_IN_SECONDS ), array( $dia, DAY_IN_SECONDS ) ) as $teto ) { $em = $wpdb->get_var( $wpdb->prepare( "SELECT em FROM {$wpdb->prefix}blog_juridico_visitas ORDER BY id DESC LIMIT 1 OFFSET %d", $teto[0] - 1 ) ); if ( $em && $em >= gmdate( 'Y-m-d H:i:s', $agora - $teto[1] ) ) { return $nada; } } $cliente = hermes_conector_cliente(); $marca = hermes_conector_marca_do_leitor( $cliente['ip'] ); $repetida = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}blog_juridico_visitas WHERE post_id = %d AND em >= %s AND marca = %s LIMIT 1", $post_id, gmdate( 'Y-m-d H:i:s', $agora - 30 ), $marca ) ); if ( $repetida ) { return $nada; } $texto = function ( $chave, $tamanho ) use ( $corpo ) { $v = ( isset( $corpo[ $chave ] ) && is_scalar( $corpo[ $chave ] ) ) ? (string) $corpo[ $chave ] : ''; return mb_substr( sanitize_text_field( $v ), 0, $tamanho ); // substr cortaria UTF-8 e o insert falharia. }; // Aceita a origem (sinal novo) ou a URL inteira (HTML antigo em cache). $host = hermes_conector_host( $texto( 'ref', 2048 ) ); $pais = ''; if ( $cliente['cloudflare'] ) { $p = strtoupper( hermes_conector_servidor( 'HTTP_CF_IPCOUNTRY' ) ); if ( preg_match( '/^[A-Z]{2}$/', $p ) && ! in_array( $p, array( 'XX', 'T1' ), true ) ) { $pais = $p; } } $largura = ( isset( $corpo['w'] ) && is_numeric( $corpo['w'] ) ) ? (int) $corpo['w'] : 0; $gravou = $wpdb->insert( "{$wpdb->prefix}blog_juridico_visitas", array( 'post_id' => $post_id, 'em' => gmdate( 'Y-m-d H:i:s', $agora ), 'origem' => ( '' !== $host && $host === $meu ) ? 'proprio-site' : $host, 'utm_source' => $texto( 'us', 80 ), 'utm_medium' => $texto( 'um', 80 ), 'utm_campaign' => '', // O Hermes não usa: não se guarda (a coluna fica). 'dispositivo' => hermes_conector_dispositivo( $largura ), 'sistema' => hermes_conector_sistema(), 'ip_prefixo' => hermes_conector_ip_prefixo( $cliente['ip'] ), 'pais' => $pais, 'marca' => $marca, ), array( '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' ) ); // phpcs:enable if ( false === $gravou ) { hermes_conector_conferir_tabela(); return $nada; } hermes_conector_somar_vista( $post_id ); return $nada; } /** * Poda diária pelo cron, fora do pedido público. */ function hermes_conector_agendar_poda() { if ( ! wp_next_scheduled( 'hermes_conector_podar' ) ) { wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'hermes_conector_podar' ); } } /** * Apaga a marca do leitor depois de 1 dia, o prefixo de rede depois de 7 e as * leituras depois de 180. */ function hermes_conector_podar_visitas() { global $wpdb; // phpcs:disable WordPress.DB.DirectDatabaseQuery -- plugin's own table: retention. $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->prefix}blog_juridico_visitas SET marca = '' WHERE em < %s AND marca <> ''", gmdate( 'Y-m-d H:i:s', time() - DAY_IN_SECONDS ) ) ); $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->prefix}blog_juridico_visitas SET ip_prefixo = '' WHERE em < %s AND ip_prefixo <> ''", gmdate( 'Y-m-d H:i:s', time() - HERMES_CONECTOR_DIAS_PREFIXO * DAY_IN_SECONDS ) ) ); $velha = gmdate( 'Y-m-d H:i:s', time() - HERMES_CONECTOR_DIAS_NO_SITE * DAY_IN_SECONDS ); // A linha mais nova nunca sai: tabela vazia faz o MySQL 5.7 voltar o AUTO_INCREMENT para 1 ao reiniciar. $max = (int) $wpdb->get_var( "SELECT MAX(id) FROM {$wpdb->prefix}blog_juridico_visitas" ); $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}blog_juridico_visitas WHERE em < %s AND id < %d LIMIT 20000", $velha, $max ) ); // Ela fica só com id, artigo e hora: os dados do leitor também saem aos 180 dias. $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->prefix}blog_juridico_visitas SET origem = '', utm_source = '', utm_medium = '', utm_campaign = '', dispositivo = '', sistema = '', ip_prefixo = '', pais = '', marca = '' WHERE id = %d AND em < %s", $max, $velha ) ); // phpcs:enable } /** * Código da meta tag do Google. Aceita a TAG inteira (o Google devolve assim) ou só o código. * * @param mixed $valor Valor enviado. * @return string Código válido ou ''. */ function hermes_conector_codigo_meta_google( $valor ) { $valor = trim( is_string( $valor ) ? $valor : '' ); if ( preg_match( '/content\s*=\s*["\']([^"\']+)["\']/i', $valor, $m ) ) { $valor = trim( $m[1] ); } return preg_match( '/^[A-Za-z0-9_-]{10,100}$/', $valor ) ? $valor : ''; } /** * Nome do arquivo de verificação do Google (googleXXXX.html). * * @param mixed $valor Valor enviado. * @return string Nome válido ou ''. */ function hermes_conector_arquivo_google_valido( $valor ) { $valor = trim( is_string( $valor ) ? $valor : '' ); return preg_match( '/^google[0-9a-zA-Z]{8,64}\.html$/', $valor ) ? $valor : ''; } /** * Verificação do Search Console: a meta tag no faz o próprio escritório * dono da propriedade (o Hermes só lê os dados depois). */ function hermes_conector_meta_google() { $token = hermes_conector_codigo_meta_google( (string) get_option( 'blog_juridico_gsc_token', '' ) ); if ( '' !== $token ) { echo '' . "\n"; } } /** * Reserva: alguns temas e otimizadores comem meta tags do . O Google também * aceita um arquivo na raiz do site — servido sem escrever em disco. */ function hermes_conector_arquivo_google() { $uri = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; $pedido = (string) wp_parse_url( $uri, PHP_URL_PATH ); if ( '.html' !== substr( $pedido, -5 ) ) { return; // Só olha a opção para *.html. } $token = hermes_conector_arquivo_google_valido( (string) get_option( 'blog_juridico_gsc_arquivo', '' ) ); if ( '' === $token ) { return; } $metodo = hermes_conector_servidor( 'REQUEST_METHOD' ); if ( 'GET' !== $metodo && 'HEAD' !== $metodo ) { return; } $base = trailingslashit( (string) wp_parse_url( home_url( '/' ), PHP_URL_PATH ) ); // '/' ou '/blog/'. if ( $pedido !== $base . $token ) { return; } hermes_conector_nao_guardar(); status_header( 200 ); header( 'Content-Type: text/plain; charset=utf-8' ); header( 'X-Content-Type-Options: nosniff' ); echo esc_html( 'google-site-verification: ' . $token ); exit; } /** * Visível para o Google? É a opção "Evitar que mecanismos de busca indexem este * site" (Configurações › Leitura): marcada, blog_public vale 0. Valores negativos * vêm de plugins de site privado — também não é visível. * * @return bool */ function hermes_conector_indexavel() { return ( (int) get_option( 'blog_public' ) ) > 0; } /** * Quando a contagem de leituras passa a valer, as páginas guardadas pelo cache ainda * não têm o sinal de leitura. Limpa os caches mais comuns; falha aqui não atrapalha. */ function hermes_conector_limpar_cache() { try { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- LiteSpeed Cache public API. do_action( 'litespeed_purge_all' ); if ( function_exists( 'rocket_clean_domain' ) ) { rocket_clean_domain(); } if ( function_exists( 'w3tc_flush_all' ) ) { w3tc_flush_all(); } if ( function_exists( 'wp_cache_clear_cache' ) ) { wp_cache_clear_cache(); } } catch ( \Throwable $e ) { unset( $e ); // Segue: só atrasa o sinal de leitura nas páginas guardadas. } } /** * Alguns plugins de cache de página (LiteSpeed Cache) guardam respostas da REST, * inclusive autenticadas, e as servem a visitantes. Ignoram o Cache-Control do core. */ function hermes_conector_nao_guardar() { if ( ! defined( 'DONOTCACHEPAGE' ) ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- standard constant read by page-cache plugins. define( 'DONOTCACHEPAGE', true ); } // No WP < 6.3 a REST manda os cabeçalhos de "não guardar" do core DEPOIS dos da // resposta e sem no-store: o filtro faz o último a sair já ser o completo. add_filter( 'nocache_headers', 'hermes_conector_cabecalhos_sem_cache' ); nocache_headers(); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- LiteSpeed Cache public API. do_action( 'litespeed_control_set_nocache', 'hermes' ); } /** * Cache-Control completo nos cabeçalhos de "não guardar" do core. * * @param array $cabecalhos Cabeçalhos do nocache_headers(). * @return array */ function hermes_conector_cabecalhos_sem_cache( $cabecalhos ) { $cabecalhos['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0, private'; return $cabecalhos; } /** * Nada autenticado vai para o cache de página: as rotas do Hermes (menos /batida) * e qualquer resposta REST de quem entrou ou mandou Authorization. * * @param mixed $resposta Resposta. * @param WP_REST_Server $servidor Servidor (não usado). * @param WP_REST_Request $requisicao Requisição. * @return mixed */ function hermes_conector_sem_cache( $resposta, $servidor, $requisicao ) { unset( $servidor ); if ( ! ( $requisicao instanceof WP_REST_Request ) ) { return $resposta; } $rota = (string) $requisicao->get_route(); if ( '/blog-juridico/v1/batida' === $rota ) { return $resposta; } $nossa = 0 === strpos( $rota, '/blog-juridico/v1/' ); $autenticada = is_user_logged_in() || null !== $requisicao->get_header( 'authorization' ); if ( ! $nossa && ! $autenticada ) { return $resposta; // Vale também para /wp/v2/users?context=edit etc. lidos pelo Hermes. } hermes_conector_nao_guardar(); if ( $resposta instanceof WP_HTTP_Response ) { $resposta->header( 'Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0, private' ); $resposta->header( 'Pragma', 'no-cache' ); $resposta->header( 'Expires', '0' ); $resposta->header( 'X-LiteSpeed-Cache-Control', 'no-cache' ); } return $resposta; } /** * Rotas REST do Hermes. Os nomes (/blog-juridico/v1/*) não mudam: sites conectados os usam. */ function hermes_conector_rotas() { $permissao = function () { return current_user_can( 'edit_posts' ); }; $admin = function () { return current_user_can( 'manage_options' ); }; register_rest_route( 'blog-juridico/v1', '/saude', array( 'methods' => 'GET', 'permission_callback' => $permissao, 'callback' => function () { hermes_conector_hermes_passou(); $seo = null; if ( defined( 'WPSEO_VERSION' ) ) { $seo = 'yoast'; } elseif ( class_exists( 'RankMath' ) ) { $seo = 'rankmath'; } // 'usuario' saiu: ninguém lia e o cache já o entregou a visitantes. return array( 'ok' => true, 'versao' => HERMES_CONECTOR_VERSAO, 'wp' => get_bloginfo( 'version' ), 'plugin_seo' => $seo, 'cache' => defined( 'WP_CACHE' ) && WP_CACHE, 'indexavel' => hermes_conector_indexavel(), 'contagem' => hermes_conector_contagem_ligada(), 'leituras' => hermes_conector_leituras_ativas(), 'pode' => array( 'leituras' => hermes_conector_pode_ler_leituras(), 'admin' => current_user_can( 'manage_options' ), ), ); }, ) ); // Grava o que for válido; 400 só se nada prestar. Envio vazio NUNCA apaga: para apagar, limpar=true. register_rest_route( 'blog-juridico/v1', '/verificacao-google', array( 'methods' => 'POST', 'permission_callback' => $admin, 'args' => array( 'meta' => array( 'type' => 'string', 'default' => '', ), 'arquivo' => array( 'type' => 'string', 'default' => '', ), 'limpar' => array( 'type' => 'boolean', 'default' => false, ), ), 'callback' => function ( $requisicao ) { $meta_bruto = is_string( $requisicao->get_param( 'meta' ) ) ? trim( $requisicao->get_param( 'meta' ) ) : ''; $arq_bruto = is_string( $requisicao->get_param( 'arquivo' ) ) ? trim( $requisicao->get_param( 'arquivo' ) ) : ''; $meta = hermes_conector_codigo_meta_google( $meta_bruto ); $arquivo = hermes_conector_arquivo_google_valido( $arq_bruto ); if ( true === $requisicao->get_param( 'limpar' ) ) { update_option( 'blog_juridico_gsc_token', '' ); update_option( 'blog_juridico_gsc_arquivo', '' ); } elseif ( '' === $meta && '' === $arquivo && ( '' !== $meta_bruto || '' !== $arq_bruto ) ) { return new WP_Error( 'hermes_codigo_invalido', __( 'The Google verification code is not in the expected format.', 'hermes-conector' ), array( 'status' => 400 ) ); } if ( '' !== $meta ) { update_option( 'blog_juridico_gsc_token', $meta ); } if ( '' !== $arquivo ) { update_option( 'blog_juridico_gsc_arquivo', $arquivo ); } return array( 'ok' => true, 'meta' => get_option( 'blog_juridico_gsc_token', '' ), 'arquivo' => get_option( 'blog_juridico_gsc_arquivo', '' ), ); }, ) ); // Public on purpose: see hermes_conector_registrar_batida(). register_rest_route( 'blog-juridico/v1', '/batida', array( 'methods' => 'POST', 'permission_callback' => '__return_true', 'callback' => 'hermes_conector_registrar_batida', ) ); register_rest_route( 'blog-juridico/v1', '/visitas', array( 'methods' => 'GET', 'permission_callback' => 'hermes_conector_pode_ler_leituras', 'args' => array( 'desde' => array( 'type' => 'integer', 'minimum' => 0, 'default' => 0, 'sanitize_callback' => 'absint', ), 'limite' => array( 'type' => 'integer', 'minimum' => 1, 'default' => 1000, 'sanitize_callback' => 'absint', ), ), 'callback' => function ( $requisicao ) { hermes_conector_hermes_passou(); global $wpdb; $desde = (int) $requisicao->get_param( 'desde' ); $limite = min( 2000, max( 1, (int) $requisicao->get_param( 'limite' ) ) ); // Corte em código, não 400. // phpcs:disable WordPress.DB.DirectDatabaseQuery -- plugin's own table, cursor read. // Só linhas "assentadas": com inserts simultâneos, um id maior pode aparecer antes // de um menor, e "em" é a hora do início da batida, não do insert. O corte é por // id: nada a partir do menor id dos últimos 60 s (senão o cursor pularia um id). $corte = (int) $wpdb->get_var( $wpdb->prepare( "SELECT MIN(id) FROM {$wpdb->prefix}blog_juridico_visitas WHERE em > %s", gmdate( 'Y-m-d H:i:s', time() - MINUTE_IN_SECONDS ) ) ); $linhas = $wpdb->get_results( $wpdb->prepare( "SELECT id, post_id, em, origem, utm_source, utm_medium, dispositivo, sistema, ip_prefixo, pais FROM {$wpdb->prefix}blog_juridico_visitas WHERE id > %d AND ( %d = 0 OR id < %d ) ORDER BY id LIMIT %d", $desde, $corte, $corte, $limite ), ARRAY_A ); if ( ! is_array( $linhas ) ) { $linhas = array(); } $ultimo = $desde; foreach ( $linhas as $linha ) { $ultimo = max( $ultimo, (int) $linha['id'] ); } if ( $desde > 0 ) { // O Hermes só pede "desde N" depois de gravar tudo até N: lá o prefixo já virou cidade. // O piso evita varrer a tabela toda a cada hora; o que ficar abaixo dele a poda apaga. $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->prefix}blog_juridico_visitas SET ip_prefixo = '', marca = '' WHERE id > %d AND id <= %d AND ( ip_prefixo <> '' OR marca <> '' )", max( 0, $desde - 20000 ), $desde ) ); } $maior = (int) $wpdb->get_var( "SELECT MAX(id) FROM {$wpdb->prefix}blog_juridico_visitas" ); // phpcs:enable return array( 'visitas' => $linhas, 'ultimo_id' => $ultimo, 'maior_id' => $maior, 'versao' => HERMES_CONECTOR_VERSAO, ); }, ) ); register_rest_route( 'blog-juridico/v1', '/metricas', array( 'methods' => 'GET', 'permission_callback' => $permissao, 'args' => array( 'ids' => array( 'type' => 'string', // NÃO array: o callback lê a string '1,2,3'. 'default' => '', 'validate_callback' => function ( $v ) { return is_string( $v ) && 1 === preg_match( '/^[0-9,\s]{0,4000}$/', $v ); }, ), ), 'callback' => function ( $requisicao ) { hermes_conector_hermes_passou(); $ids = array_slice( array_values( array_unique( array_filter( wp_parse_id_list( $requisicao->get_param( 'ids' ) ) ) ) ), 0, 200 ); if ( ! $ids ) { return array( 'artigos' => array() ); } _prime_post_caches( $ids, false, true ); // ~3 consultas em vez de ~600. $saida = array(); foreach ( $ids as $id ) { $post = get_post( $id ); // Quem não pode LER o post (rascunho/privado/agendado de outro autor) nem sabe que ele existe. if ( ! $post || 'post' !== $post->post_type || ! current_user_can( 'read_post', $id ) ) { continue; } $saida[] = array( 'id' => (int) $id, 'vistas' => (int) get_post_meta( $id, HERMES_CONECTOR_META_VISTAS, true ), 'status' => $post->post_status, 'publicado_em' => $post->post_date_gmt, 'link' => get_permalink( $id ), ); } return array( 'artigos' => $saida ); }, ) ); } /** * Configurações › Hermes. */ function hermes_conector_menu() { add_options_page( 'Hermes', 'Hermes', 'manage_options', 'hermes-conector', 'hermes_conector_tela' ); } /** * Link "Configurações" na lista de plugins. * * @param string[] $links Links do plugin. * @return string[] */ function hermes_conector_links( $links ) { array_unshift( $links, '' . esc_html__( 'Settings', 'hermes-conector' ) . '' ); return $links; } /** * Interruptor da contagem de leituras. */ function hermes_conector_registrar_opcoes() { register_setting( 'hermes_conector', 'blog_juridico_contar_leituras', array( 'type' => 'string', 'default' => '1', 'sanitize_callback' => function ( $v ) { return '1' === (string) $v ? '1' : '0'; // Caixa desmarcada chega null. }, ) ); } /** * Senhas de aplicação do Hermes: app_id fixo do 1 clique; as manuais pelo nome. * A mais usada por último vem primeiro. * * @return array[] */ function hermes_conector_senhas_hermes() { if ( ! class_exists( 'WP_Application_Passwords' ) ) { return array(); } $achadas = array(); $usuarios = get_users( array( 'fields' => 'ID', 'number' => 100, 'meta_key' => '_application_passwords', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_query_meta_key -- admin-only screens, at most 100 users. ) ); foreach ( $usuarios as $uid ) { foreach ( (array) WP_Application_Passwords::get_user_application_passwords( (int) $uid ) as $s ) { $nome = strtolower( trim( isset( $s['name'] ) ? (string) $s['name'] : '' ) ); if ( ( ! empty( $s['app_id'] ) && HERMES_CONECTOR_APP_ID === $s['app_id'] ) || in_array( $nome, array( 'hermes', 'blog.juridico' ), true ) ) { $s['user_id'] = (int) $uid; $achadas[] = $s; } } } usort( $achadas, function ( $a, $b ) { return (int) $b['last_used'] - (int) $a['last_used']; } ); return $achadas; } /** * Leituras gravadas nos últimos dias. * * @param int $dias Janela em dias. * @return int */ function hermes_conector_leituras( $dias ) { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- plugin's own table (KEY em). return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}blog_juridico_visitas WHERE em >= %s", gmdate( 'Y-m-d H:i:s', time() - $dias * DAY_IN_SECONDS ) ) ); } /** * Tela Configurações › Hermes: conexão, Google e leituras num relance. */ function hermes_conector_tela() { if ( ! current_user_can( 'manage_options' ) ) { return; } $senhas = hermes_conector_senhas_hermes(); $u = $senhas ? get_userdata( $senhas[0]['user_id'] ) : null; $linhas = array(); if ( $u ) { if ( ! empty( $senhas[0]['last_used'] ) ) { /* translators: 1: WordPress user name, 2: how long ago, e.g. "2 hours" */ $txt = sprintf( __( 'Connected through the user %1$s. Last used %2$s ago.', 'hermes-conector' ), $u->display_name, human_time_diff( (int) $senhas[0]['last_used'] ) ); } else { /* translators: %s: WordPress user name */ $txt = sprintf( __( 'Connected through the user %s. Not used yet.', 'hermes-conector' ), $u->display_name ); } if ( count( $senhas ) > 1 ) { $txt .= ' ' . __( 'There are older Hermes passwords on this site; you can revoke them in Users > Profile.', 'hermes-conector' ); } $linhas[ __( 'Connection', 'hermes-conector' ) ] = esc_html( $txt ); } else { $linhas[ __( 'Connection', 'hermes-conector' ) ] = esc_html__( 'This site is not connected to Hermes yet.', 'hermes-conector' ) . ' ' . esc_html__( 'Connect to Hermes', 'hermes-conector' ) . ''; } $linhas['Google'] = hermes_conector_indexavel() ? esc_html__( 'Visible to search engines.', 'hermes-conector' ) : '' . esc_html__( 'Hidden from Google: WordPress is asking search engines not to index this site.', 'hermes-conector' ) . ' ' . esc_html__( 'Settings > Reading', 'hermes-conector' ) . ''; if ( ! hermes_conector_contagem_ligada() ) { $leit = esc_html__( 'Read counting is off (see below).', 'hermes-conector' ); } elseif ( ! hermes_conector_leituras_ativas() ) { $leit = esc_html__( 'Reads are recorded only while the site is connected to Hermes.', 'hermes-conector' ); } else { $l30 = hermes_conector_leituras( 30 ); /* translators: %s: number of reads */ $leit = esc_html( sprintf( _n( '%s read recorded in the last 30 days.', '%s reads recorded in the last 30 days.', $l30, 'hermes-conector' ), number_format_i18n( $l30 ) ) ); if ( 0 === hermes_conector_leituras( 7 ) && wp_count_posts( 'post' )->publish > 0 ) { $leit .= ' ' . esc_html__( 'No read arrived in the last 7 days. If the blog has visitors, a security or optimization plugin may be blocking the address /wp-json/blog-juridico/v1/batida.', 'hermes-conector' ); } } $linhas[ __( 'Reads', 'hermes-conector' ) ] = $leit; $linhas[ __( 'Version', 'hermes-conector' ) ] = esc_html( HERMES_CONECTOR_VERSAO ); echo '

Hermes

'; foreach ( $linhas as $rotulo => $html ) { echo ''; } echo '
'; settings_fields( 'hermes_conector' ); echo ''; submit_button(); echo '
'; } /** * Aviso logo depois de ativar, só na tela de Plugins e uma vez. */ function hermes_conector_aviso_ativado() { $tela = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( ! $tela || 'plugins' !== $tela->id || ! current_user_can( 'manage_options' ) ) { return; } $chave = 'hermes_conector_ativado_' . get_current_user_id(); if ( ! get_transient( $chave ) ) { return; } delete_transient( $chave ); $texto = hermes_conector_senhas_hermes() ? __( 'Hermes is active. You can go back to Hermes: it notices by itself.', 'hermes-conector' ) : __( 'Hermes is active. Next step: connect this site at hermes.adv.br.', 'hermes-conector' ); echo '

' . esc_html( $texto ) . ' ' . esc_html__( 'See status', 'hermes-conector' ) . '

'; } /** * Teste na Saúde do site. * * @param array $testes Testes do core. * @return array */ function hermes_conector_testes_saude( $testes ) { $testes['direct']['hermes_conexao'] = array( 'label' => __( 'Hermes can sign in to this site', 'hermes-conector' ), 'test' => 'hermes_conector_teste_conexao', ); return $testes; } /** * Senhas de aplicação desligadas (Wordfence, outro plugin de segurança, ou WordPress * sem enxergar o https) impedem o Hermes de publicar. * * @return array */ function hermes_conector_teste_conexao() { $r = array( 'label' => __( 'Hermes can sign in to this site', 'hermes-conector' ), 'status' => 'good', 'badge' => array( 'label' => 'Hermes', 'color' => 'blue', ), 'description' => '

' . esc_html__( 'Application passwords are on. Hermes uses one to publish your articles.', 'hermes-conector' ) . '

', 'actions' => '', 'test' => 'hermes_conexao', ); if ( ! function_exists( 'wp_is_application_passwords_available' ) ) { return $r; } $senhas = hermes_conector_senhas_hermes(); $usuario = $senhas ? get_userdata( $senhas[0]['user_id'] ) : null; $desligadas = ! wp_is_application_passwords_available() || ( $usuario && ! wp_is_application_passwords_available_for_user( $usuario ) ); if ( ! $desligadas ) { return $r; } // wp_is_application_passwords_supported() só existe a partir do WP 5.9. $suportadas = function_exists( 'wp_is_application_passwords_supported' ) ? wp_is_application_passwords_supported() : ( is_ssl() || ( function_exists( 'wp_get_environment_type' ) && 'local' === wp_get_environment_type() ) ); $r['status'] = $senhas ? 'critical' : 'recommended'; $r['badge']['color'] = $senhas ? 'red' : 'orange'; $r['label'] = $senhas ? __( 'Hermes can no longer publish: application passwords are turned off', 'hermes-conector' ) : __( 'Application passwords are turned off', 'hermes-conector' ); $r['description'] = '

' . ( $suportadas ? esc_html__( 'A plugin turned application passwords off. In Wordfence: Wordfence > All Options > Brute Force Protection > untick "Disable WordPress application passwords" and save. Other security plugins have a similar option.', 'hermes-conector' ) : esc_html__( 'WordPress only offers application passwords when it detects HTTPS. If the site already opens with the padlock, it is probably behind a proxy (such as Cloudflare "Flexible" SSL) that hides HTTPS from WordPress: ask your host to fix it.', 'hermes-conector' ) ) . '

'; return $r; } /** * Bloco do Hermes em Ferramentas › Saúde do site › Informação. * * @param array $info Informações do core. * @return array */ function hermes_conector_info_saude( $info ) { $senhas = hermes_conector_senhas_hermes(); $u = $senhas ? get_userdata( $senhas[0]['user_id'] ) : null; $info['hermes-conector'] = array( 'label' => 'Hermes', 'fields' => array( 'versao' => array( 'label' => __( 'Version', 'hermes-conector' ), 'value' => HERMES_CONECTOR_VERSAO, ), 'ligado' => array( 'label' => __( 'Connected to Hermes', 'hermes-conector' ), 'value' => $u ? __( 'Yes', 'hermes-conector' ) : __( 'No', 'hermes-conector' ), 'debug' => $u ? 'yes' : 'no', ), 'usuario' => array( 'label' => __( 'Hermes user', 'hermes-conector' ), 'value' => $u ? $u->user_login : '-', 'private' => true, ), 'senhas' => array( 'label' => __( 'Hermes application passwords', 'hermes-conector' ), 'value' => count( $senhas ), ), 'ultimo_uso' => array( 'label' => __( 'Last used', 'hermes-conector' ), 'value' => ( $senhas && ! empty( $senhas[0]['last_used'] ) ) ? wp_date( 'Y-m-d', (int) $senhas[0]['last_used'] ) : '-', ), 'contagem' => array( 'label' => __( 'Read counting', 'hermes-conector' ), 'value' => hermes_conector_leituras_ativas() ? 'on' : 'off', ), 'leituras' => array( 'label' => __( 'Reads recorded (30 days)', 'hermes-conector' ), 'value' => hermes_conector_leituras( 30 ), ), 'google' => array( 'label' => __( 'Search Console verification', 'hermes-conector' ), 'value' => ( get_option( 'blog_juridico_gsc_token' ) || get_option( 'blog_juridico_gsc_arquivo' ) ) ? 'yes' : 'no', ), ), ); return $info; } /** * Texto sugerido para a Política de Privacidade (Configurações › Privacidade › Guia). */ function hermes_conector_texto_privacidade() { if ( ! function_exists( 'wp_add_privacy_policy_content' ) ) { return; } /* translators: 1: days kept on the site, 2: days the network prefix is kept, 3: months kept at Hermes */ $prazos = sprintf( __( 'Records are kept on this site for up to %1$d days; the network prefix for at most %2$d days. At Hermes, reading data without the network address is kept for up to %3$d months.', 'hermes-conector' ), HERMES_CONECTOR_DIAS_NO_SITE, HERMES_CONECTOR_DIAS_PREFIXO, HERMES_CONECTOR_MESES_NO_HERMES ); $t = '

' . esc_html__( 'Hermes records article reading statistics for the Hermes dashboard. Copy the suggested text into your privacy policy and fill in the bracketed parts. If you turn read counting off in Settings > Hermes, this section is not needed.', 'hermes-conector' ) . '

' . '' . esc_html__( 'Suggested text:', 'hermes-conector' ) . '' . '

' . esc_html__( 'Article reading statistics', 'hermes-conector' ) . '

' . '

' . esc_html__( 'When you open an article on this blog, we record a reading statistic to learn which content is read and how readers find it. It uses no cookies, stores nothing on your device and does not include your name, e-mail address or full IP address.', 'hermes-conector' ) . '

' . '

' . esc_html__( 'We record: the article and the time; the domain of the site you came from (never the full page address); the utm_source and utm_medium link parameters, if any; the device type and operating system; the country, when our network provider supplies it; the first part of the network address, used only to estimate the approximate city and erased once the city is estimated; and a code derived from the network address and browser that changes every day, used only to avoid counting the same reading twice and erased within a day. People logged in to this site, and browsers that send Global Privacy Control or Do Not Track, are not counted.', 'hermes-conector' ) . '

' . '

' . esc_html__( '[LAW FIRM NAME] is the controller of this data. Hermes (hermes.adv.br), the service that writes and publishes the articles on this blog, processes it on our behalf only to show us how the articles perform. It is not sold, not used for advertising and not combined with other information to identify readers. Legal basis: our legitimate interest in measuring and improving our content (LGPD, Art. 7, IX).', 'hermes-conector' ) . '

' . '

' . esc_html( $prazos ) . '

' . '

' . esc_html__( 'Because the records contain no name, e-mail address or full IP address, and the daily code is erased within a day, we cannot link them to a person and cannot locate a specific reading of yours. For questions, write to [E-MAIL].', 'hermes-conector' ) . '

'; wp_add_privacy_policy_content( 'Hermes', wp_kses_post( $t ) ); } } elseif ( version_compare( HERMES_CONECTOR_VERSAO, '1.0.0', '<' ) ) { // A pre-1.0.0 copy (zip "Hermes" 0.6.0 in another folder) loaded first, so this copy // stays idle. Warn the owner on the Plugins screen only; dismissible per user (until // the older version changes). Anonymous functions on purpose: a named one here would // collide with a third copy. 1.0.0 = first WordPress.org release. $hermes_conector_mostrar_antiga = function () { $tela = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( ! $tela || ! in_array( $tela->id, array( 'plugins', 'plugins-network' ), true ) || ! current_user_can( 'activate_plugins' ) ) { return false; } $dispensados = get_user_meta( get_current_user_id(), 'hermes_conector_dispensado', true ); return ! ( is_array( $dispensados ) && isset( $dispensados['antiga'] ) && HERMES_CONECTOR_VERSAO === $dispensados['antiga'] ); }; $hermes_conector_aviso_antiga = function () use ( $hermes_conector_mostrar_antiga ) { if ( ! $hermes_conector_mostrar_antiga() ) { return; } /* translators: %s: version number of the older copy, e.g. 0.6.0 */ $texto = sprintf( __( 'An older copy of Hermes (version %s) is active, so the updated copy is paused. Deactivate and delete the older copy in Plugins. Your data is kept.', 'hermes-conector' ), HERMES_CONECTOR_VERSAO ); echo '

' . esc_html( $texto ) . '

'; }; add_action( 'admin_notices', $hermes_conector_aviso_antiga ); add_action( 'network_admin_notices', $hermes_conector_aviso_antiga ); add_action( 'admin_enqueue_scripts', function () use ( $hermes_conector_mostrar_antiga ) { if ( ! $hermes_conector_mostrar_antiga() ) { return; } $config = wp_json_encode( array( 'url' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'hermes_conector_dispensar' ), ) ); wp_add_inline_script( 'common', '(function (c) { document.addEventListener("click", function (e) {' . ' var b = e.target && e.target.closest ? e.target.closest(".hermes-conector-aviso .notice-dismiss") : null;' . ' if (!b || !window.fetch) { return; }' . ' var d = new FormData(); d.append("action", "hermes_conector_dispensar");' . ' d.append("aviso", b.closest(".hermes-conector-aviso").getAttribute("data-aviso")); d.append("_ajax_nonce", c.nonce);' . ' fetch(c.url, {method: "POST", body: d, credentials: "same-origin"});' . ' }); })(' . $config . ');' ); } ); add_action( 'wp_ajax_hermes_conector_dispensar', function () { check_ajax_referer( 'hermes_conector_dispensar' ); if ( ! current_user_can( 'activate_plugins' ) ) { wp_send_json_error( null, 403 ); } $aviso = isset( $_POST['aviso'] ) ? sanitize_key( wp_unslash( $_POST['aviso'] ) ) : ''; if ( 'antiga' !== $aviso ) { wp_send_json_error( null, 400 ); } $dispensados = get_user_meta( get_current_user_id(), 'hermes_conector_dispensado', true ); $dispensados = is_array( $dispensados ) ? $dispensados : array(); $dispensados['antiga'] = HERMES_CONECTOR_VERSAO; update_user_meta( get_current_user_id(), 'hermes_conector_dispensado', $dispensados ); wp_send_json_success(); } ); } // Fim do guard HERMES_CONECTOR_VERSAO.