Changeset 3783334
- Timestamp:
- 11/04/2026 07:53:57 AM (12 hours ago)
- Location:
- es-football-bypass-for-cloudflare
- Files:
-
- 1 added
- 10 edited
- 1 copied
-
assets/screenshot-1.png (modified) ( previous)
-
assets/screenshot-2.png (modified) ( previous)
-
assets/screenshot-3.png (added)
-
tags/2.0.0 (copied) (copied from es-football-bypass-for-cloudflare/trunk )
-
tags/2.0.0/README.md (modified) (1 diff)
-
tags/2.0.0/es-football-bypass.php (modified) (22 diffs)
-
tags/2.0.0/readme.txt (modified) (5 diffs)
-
tags/2.0.0/uninstall.php (modified) (2 diffs)
-
trunk/README.md (modified) (1 diff)
-
trunk/es-football-bypass.php (modified) (22 diffs)
-
trunk/readme.txt (modified) (5 diffs)
-
trunk/uninstall.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
es-football-bypass-for-cloudflare/tags/2.0.0/README.md
r3707647 r3783334 1 1 # ES Football Bypass for Cloudflare 2 2 3 **Versión 1.9.8** | [Changelog](readme.txt)3 **Versión 2.0.0** | [Changelog](readme.txt) 4 4 5 5 Plugin de WordPress que automatiza el cambio entre modo Proxied y DNS Only en Cloudflare cuando hay bloqueos masivos ordenados durante los partidos de fútbol en España. Consulta el feed de [hayahora.futbol](https://hayahora.futbol/) y activa/desactiva los registros seleccionados para mantener accesible tu sitio legítimo, con un periodo de enfriamiento configurable antes de volver a activar Cloudflare. 6 7 ## Capturas 8 9 **Operación:** estado de los bloqueos, registros DNS gestionados y acciones manuales. 10 11  12 13 **¿Hay fútbol ahora?:** respuesta rápida sobre si hay bloqueos activos. 14 15  16 17 **Configuración:** credenciales de Cloudflare, comportamiento del bypass y avisos. 18 19  6 20 7 21 ## Instalación rápida -
es-football-bypass-for-cloudflare/tags/2.0.0/es-football-bypass.php
r3707647 r3783334 4 4 * Plugin URI: https://github.com/dcarrero/cf-football-bypass 5 5 * Description: Operates with Cloudflare to toggle between Proxy (ON/CDN) and DNS Only (OFF) based on IP blocks, with persistent DNS cache and AJAX actions. Separate UI: Operation and Settings. 6 * Version: 1.9.86 * Version: 2.0.0 7 7 * Author: David Carrero Fernandez-Baillo 8 8 * Author URI: https://carrero.es … … 11 11 * Text Domain: es-football-bypass-for-cloudflare 12 12 * Domain Path: /languages 13 * Requires at least: 5.013 * Requires at least: 6.9 14 14 * Requires PHP: 7.4 15 15 * Update URI: https://wordpress.org/plugins/es-football-bypass-for-cloudflare/ … … 118 118 add_action( 'wp_ajax_cfbcolorvivo_feed_diagnostics', array( $this, 'ajax_feed_diagnostics' ) ); 119 119 add_action( 'init', array( $this, 'maybe_process_external_cron' ) ); 120 121 // Abilities API (WordPress 6.9+): permite a REST, MCP y agentes operar el bypass. 122 add_action( 'wp_abilities_api_categories_init', array( $this, 'register_ability_category' ) ); 123 add_action( 'wp_abilities_api_init', array( $this, 'register_abilities' ) ); 120 124 } 121 125 … … 271 275 return true; 272 276 } 273 $local = array( 'localhost', 'localhost.localdomain' ); 274 if ( in_array( strtolower( $domain ), $local, true ) ) { 275 return true; 276 } 277 if ( preg_match( '/\.(local|test|ddev\.site|lndo\.site|wp\.lan)$/i', $domain ) ) { 278 return true; 279 } 280 return false; 277 $local = array( 'localhost', 'localhost.localdomain' ); 278 $is_local = in_array( strtolower( $domain ), $local, true ) 279 || (bool) preg_match( '/\.(local|test|ddev\.site|lndo\.site|wp\.lan)$/i', $domain ); 280 281 /** 282 * Filters whether the current domain is a local environment. 283 * 284 * Returning false unlocks the Operation page on a local or preview install, 285 * which is what a staging copy or a WordPress Playground demo needs. 286 * 287 * @param bool $is_local Whether the domain looks local. 288 * @param string $domain Domain being checked. 289 */ 290 return (bool) apply_filters( 'cfbcolorvivo_is_local_domain', $is_local, $domain ); 281 291 } 282 292 /** Detect the outgoing IP addresses of this server (cached 1 hour). */ … … 949 959 950 960 // Registrar script handle para asociar inline scripts. 951 wp_register_script( 'cfbcolorvivo-admin', false, array(), ' 1.9.8', true );961 wp_register_script( 'cfbcolorvivo-admin', false, array(), '2.0.0', true ); 952 962 wp_enqueue_script( 'cfbcolorvivo-admin' ); 953 963 … … 964 974 965 975 // Estilos de la interfaz de administración. 966 wp_register_style( 'cfbcolorvivo-admin', false, array(), ' 1.9.8' );976 wp_register_style( 'cfbcolorvivo-admin', false, array(), '2.0.0' ); 967 977 wp_enqueue_style( 'cfbcolorvivo-admin' ); 968 978 wp_add_inline_style( 'cfbcolorvivo-admin', $this->get_admin_css() ); … … 1004 1014 .cfbcolorvivo-count #cfbcolorvivo-sel-count{font-weight:700;color:#1d2327} 1005 1015 .cfbcolorvivo-dirty{display:inline-block;margin-left:8px;padding:2px 8px;background:#fcf9e8;border-left:3px solid #dba617;color:#674d00} 1016 .cfbcolorvivo-dirty[hidden]{display:none} 1006 1017 .cfbcolorvivo-actions{margin:12px 0 0} 1007 1018 .cfbcolorvivo-actionrow{display:flex;flex-wrap:wrap;gap:8px;align-items:center} … … 2695 2706 } 2696 2707 2708 if ( ! empty( $settings['selected_records'] ) ) { 2709 // Los PUT no tocan la caché: la recargamos para que refleje el estado que acabamos de aplicar. 2710 if ( $updated > 0 ) { 2711 $after = $this->fetch_dns_records(); 2712 if ( ! empty( $after ) ) { 2713 $this->persist_dns_cache( $after ); 2714 } 2715 } 2716 // $settings se leyó antes de refrescar la caché: guardarlo tal cual la sobrescribiría con datos viejos. 2717 $settings = $this->get_settings(); 2718 } 2719 2697 2720 $settings['last_check'] = $now_mysql; 2698 2721 $settings['last_status_general'] = ( 'SÍ' === $general ) ? 'SI' : 'NO'; … … 2884 2907 $home = home_url( '/' ); 2885 2908 $host = wp_parse_url( $home, PHP_URL_HOST ); 2886 if ( $host ) { 2887 return $host; 2888 } 2889 // Fallback to HTTP_HOST only if home_url parsing fails. 2890 return isset( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : ''; 2909 if ( ! $host ) { 2910 // Fallback to HTTP_HOST only if home_url parsing fails. 2911 $host = isset( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : ''; 2912 } 2913 2914 /** 2915 * Filters the domain the plugin considers to be this site. 2916 * 2917 * Useful behind a reverse proxy, or on a staging copy that must be checked 2918 * against the production domain. 2919 * 2920 * @param string $host Domain resolved from home_url(). 2921 */ 2922 return (string) apply_filters( 'cfbcolorvivo_site_domain', $host ); 2891 2923 } 2892 2924 /** … … 2949 2981 return null; 2950 2982 } 2983 /** 2984 * Read lastUpdate from a raw feed body without building the whole array. 2985 * 2986 * The feed decodes to about 50 MB, so it should not be decoded just to compare a 2987 * timestamp. lastUpdate is the first key of the feed; if that ever changes this 2988 * falls back to a full decode, released before returning. 2989 * 2990 * @param string $body Raw JSON body. 2991 * @return string|null lastUpdate value, or null if the body has none. 2992 */ 2993 private function read_feed_last_update( $body ) { 2994 if ( ! is_string( $body ) || '' === $body ) { 2995 return null; 2996 } 2997 if ( preg_match( '/\A\s*\{\s*"lastUpdate"\s*:\s*"([^"\\\\]+)"/', substr( $body, 0, 256 ), $matches ) ) { 2998 return $matches[1]; 2999 } 3000 $json = json_decode( $body, true ); 3001 return ( is_array( $json ) && ! empty( $json['lastUpdate'] ) && is_string( $json['lastUpdate'] ) ) ? $json['lastUpdate'] : null; 3002 } 3003 2951 3004 /** Fetch and cache the football block status JSON from remote or local. */ 2952 3005 private function fetch_status_json() { … … 2960 3013 $local_body = $wp_fs->get_contents( $local_path ); 2961 3014 if ( false !== $local_body ) { 2962 $json_local = json_decode( $local_body, true ); 2963 if ( is_array( $json_local ) && ! empty( $json_local['lastUpdate'] ) && is_string( $json_local['lastUpdate'] ) ) { 2964 $last_local = $json_local['lastUpdate']; 2965 } 3015 $last_local = $this->read_feed_last_update( $local_body ); 2966 3016 } 2967 3017 } … … 2969 3019 $remote_ok = false; 2970 3020 $remote_body = null; 3021 $remote_json = null; 2971 3022 $last_remote = null; 2972 3023 $remote_code = 0; … … 2990 3041 'timeout' => 25, 2991 3042 'redirection' => 5, 2992 'user-agent' => 'ESFB/ 1.9.8; ' . home_url( '/' ),3043 'user-agent' => 'ESFB/2.0.0; ' . home_url( '/' ), 2993 3044 ) 2994 3045 ); … … 2999 3050 if ( 200 === $remote_code ) { 3000 3051 $remote_body = wp_remote_retrieve_body( $resp ); 3001 $ tmp= json_decode( $remote_body, true );3002 if ( is_array( $ tmp) ) {3052 $remote_json = json_decode( $remote_body, true ); 3053 if ( is_array( $remote_json ) ) { 3003 3054 $remote_ok = true; 3004 if ( ! empty( $ tmp['lastUpdate'] ) && is_string( $tmp['lastUpdate'] ) ) {3005 $last_remote = $ tmp['lastUpdate'];3055 if ( ! empty( $remote_json['lastUpdate'] ) && is_string( $remote_json['lastUpdate'] ) ) { 3056 $last_remote = $remote_json['lastUpdate']; 3006 3057 } 3007 3058 } else { … … 3066 3117 } 3067 3118 3068 $json = json_decode( $body, true ); 3119 // Reutilizamos el remoto ya decodificado si es el que vamos a usar; si no, se libera 3120 // antes de decodificar el local para no tener nunca dos copias en memoria (issue #8). 3121 if ( is_array( $remote_json ) && $body === $remote_body ) { 3122 $json = $remote_json; 3123 } else { 3124 $remote_json = null; 3125 $json = json_decode( $body, true ); 3126 // Copia local corrupta con cabecera válida (p. ej. escritura truncada): se repara con el remoto. 3127 if ( ! is_array( $json ) && $remote_ok ) { 3128 $json = json_decode( $remote_body, true ); 3129 if ( $wp_fs && is_array( $json ) ) { 3130 $wp_fs->put_contents( $local_path, $remote_body, FS_CHMOD_FILE ); 3131 $this->log( '[CFB] Local data.json corrupto; restaurado desde el remoto.' ); 3132 } 3133 } 3134 } 3135 unset( $remote_json, $remote_body, $local_body, $body ); 3136 3069 3137 if ( ! is_array( $json ) ) { 3070 3138 $this->log( '[CFB] JSON decode failed' ); … … 3094 3162 } 3095 3163 3096 $extracted = $this->extract_ip_block_map( $json ); 3164 $extracted = $this->extract_ip_block_map( $json ); 3165 unset( $json ); 3166 3097 3167 $map = $extracted['ip_map'] ?? array(); 3098 3168 $isps_blocked = $extracted['isps_blocked'] ?? array(); … … 3375 3445 $contents = file_get_contents( $local_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents 3376 3446 if ( is_string( $contents ) ) { 3377 $decoded = json_decode( $contents, true ); 3378 if ( is_array( $decoded ) && ! empty( $decoded['lastUpdate'] ) && is_string( $decoded['lastUpdate'] ) ) { 3379 $local_last_update = $decoded['lastUpdate']; 3380 } 3447 $local_last_update = (string) $this->read_feed_last_update( $contents ); 3381 3448 } 3382 3449 } … … 3512 3579 $home = home_url( '/' ); 3513 3580 $mgr_url = admin_url( 'admin.php?page=cfbcolorvivo-main' ); 3514 $version = ' 1.9.8';3581 $version = '2.0.0'; 3515 3582 3516 3583 if ( $now_active ) { … … 3545 3612 } 3546 3613 $this->log( '[CFB] Email notificación ' . ( $sent ? 'enviada' : 'FALLÓ' ) . ' a ' . $to . ' (estado=' . ( $now_active ? 'bypass activo' : 'bypass inactivo' ) . ')' ); 3614 } 3615 3616 /* ================== Abilities API (WordPress 6.9+) ================== */ 3617 3618 /** 3619 * Register the ability category for this plugin. 3620 * 3621 * Requires WordPress 6.9 or newer, which is the plugin minimum since 2.0.0. 3622 */ 3623 public function register_ability_category() { 3624 wp_register_ability_category( 3625 'es-football-bypass', 3626 array( 3627 'label' => __( 'ES Football Bypass', 'es-football-bypass-for-cloudflare' ), 3628 'description' => __( 'Check La Liga IP block status and control the Cloudflare proxy on the managed DNS records.', 'es-football-bypass-for-cloudflare' ), 3629 ) 3630 ); 3631 } 3632 3633 /** 3634 * Permission check shared by every ability. 3635 * 3636 * Abilities run outside the admin screens (REST, MCP, AI agents), so they enforce 3637 * the same capability the admin pages require. 3638 * 3639 * @return bool True when the current user may operate the plugin. 3640 */ 3641 public function abilities_permission_check() { 3642 return current_user_can( 'manage_options' ); 3643 } 3644 3645 /** 3646 * Register the plugin abilities so REST and MCP clients can drive the bypass. 3647 */ 3648 public function register_abilities() { 3649 $record_schema = array( 3650 'type' => 'object', 3651 'properties' => array( 3652 'id' => array( 3653 'type' => 'string', 3654 'description' => __( 'Cloudflare DNS record ID.', 'es-football-bypass-for-cloudflare' ), 3655 ), 3656 'name' => array( 'type' => 'string' ), 3657 'type' => array( 'type' => 'string' ), 3658 'content' => array( 'type' => 'string' ), 3659 'proxied' => array( 3660 'type' => array( 'boolean', 'null' ), 3661 'description' => __( 'True when the record goes through the Cloudflare CDN, null when the record cannot be proxied.', 'es-football-bypass-for-cloudflare' ), 3662 ), 3663 'managed' => array( 3664 'type' => 'boolean', 3665 'description' => __( 'True when the plugin is allowed to switch this record.', 'es-football-bypass-for-cloudflare' ), 3666 ), 3667 ), 3668 ); 3669 3670 wp_register_ability( 3671 'es-football-bypass/get-status', 3672 array( 3673 'label' => __( 'Get bypass status', 'es-football-bypass-for-cloudflare' ), 3674 'description' => __( 'Returns whether La Liga IP blocks are active right now, whether this site is among the blocked IPs, and whether the Cloudflare bypass is currently engaged.', 'es-football-bypass-for-cloudflare' ), 3675 'category' => 'es-football-bypass', 3676 'output_schema' => array( 3677 'type' => 'object', 3678 'properties' => array( 3679 'blocks_active' => array( 3680 'type' => 'boolean', 3681 'description' => __( 'Blocks are active on some IPs right now.', 'es-football-bypass-for-cloudflare' ), 3682 ), 3683 'domain_blocked' => array( 3684 'type' => 'boolean', 3685 'description' => __( 'This site resolves to at least one blocked IP.', 'es-football-bypass-for-cloudflare' ), 3686 ), 3687 'bypass_active' => array( 3688 'type' => 'boolean', 3689 'description' => __( 'The plugin currently has the managed records on DNS Only.', 'es-football-bypass-for-cloudflare' ), 3690 ), 3691 'domain' => array( 'type' => 'string' ), 3692 'domain_ips' => array( 3693 'type' => 'array', 3694 'items' => array( 'type' => 'string' ), 3695 ), 3696 'blocked_domain_ips' => array( 3697 'type' => 'array', 3698 'items' => array( 'type' => 'string' ), 3699 ), 3700 'managed_records' => array( 'type' => 'integer' ), 3701 'last_check' => array( 'type' => 'string' ), 3702 'feed_last_update' => array( 'type' => 'string' ), 3703 ), 3704 ), 3705 'execute_callback' => array( $this, 'ability_get_status' ), 3706 'permission_callback' => array( $this, 'abilities_permission_check' ), 3707 'meta' => array( 3708 'public' => true, 3709 'show_in_rest' => true, 3710 'annotations' => array( 3711 'readonly' => true, 3712 'destructive' => false, 3713 'idempotent' => true, 3714 ), 3715 ), 3716 ) 3717 ); 3718 3719 wp_register_ability( 3720 'es-football-bypass/list-dns-records', 3721 array( 3722 'label' => __( 'List DNS records', 'es-football-bypass-for-cloudflare' ), 3723 'description' => __( 'Lists the cached Cloudflare DNS records with their current proxy state and whether the plugin manages each one. Optionally filtered by record type.', 'es-football-bypass-for-cloudflare' ), 3724 'category' => 'es-football-bypass', 3725 'input_schema' => array( 3726 'type' => 'object', 3727 'default' => array(), 3728 'properties' => array( 3729 'type' => array( 3730 'type' => 'string', 3731 'enum' => array( 'A', 'AAAA', 'CNAME' ), 3732 'description' => __( 'Return only records of this type.', 'es-football-bypass-for-cloudflare' ), 3733 ), 3734 'managed_only' => array( 3735 'type' => 'boolean', 3736 'description' => __( 'Return only the records the plugin manages.', 'es-football-bypass-for-cloudflare' ), 3737 ), 3738 ), 3739 ), 3740 'output_schema' => array( 3741 'type' => 'object', 3742 'properties' => array( 3743 'records' => array( 3744 'type' => 'array', 3745 'items' => $record_schema, 3746 ), 3747 'total' => array( 'type' => 'integer' ), 3748 ), 3749 ), 3750 'execute_callback' => array( $this, 'ability_list_dns_records' ), 3751 'permission_callback' => array( $this, 'abilities_permission_check' ), 3752 'meta' => array( 3753 'public' => true, 3754 'show_in_rest' => true, 3755 'annotations' => array( 3756 'readonly' => true, 3757 'destructive' => false, 3758 'idempotent' => true, 3759 ), 3760 ), 3761 ) 3762 ); 3763 3764 wp_register_ability( 3765 'es-football-bypass/set-managed-records', 3766 array( 3767 'label' => __( 'Set managed DNS records', 'es-football-bypass-for-cloudflare' ), 3768 'description' => __( 'Replaces the list of DNS records the plugin is allowed to switch. Record IDs that do not exist in the Cloudflare zone are ignored. Pass an empty array to stop the plugin from touching DNS at all.', 'es-football-bypass-for-cloudflare' ), 3769 'category' => 'es-football-bypass', 3770 'input_schema' => array( 3771 'type' => 'object', 3772 'properties' => array( 3773 'record_ids' => array( 3774 'type' => 'array', 3775 'items' => array( 'type' => 'string' ), 3776 'description' => __( 'Cloudflare DNS record IDs to manage.', 'es-football-bypass-for-cloudflare' ), 3777 ), 3778 ), 3779 'required' => array( 'record_ids' ), 3780 ), 3781 'output_schema' => array( 3782 'type' => 'object', 3783 'properties' => array( 3784 'managed_records' => array( 'type' => 'integer' ), 3785 'ignored_ids' => array( 3786 'type' => 'array', 3787 'items' => array( 'type' => 'string' ), 3788 ), 3789 ), 3790 ), 3791 'execute_callback' => array( $this, 'ability_set_managed_records' ), 3792 'permission_callback' => array( $this, 'abilities_permission_check' ), 3793 'meta' => array( 3794 'public' => true, 3795 'show_in_rest' => true, 3796 'annotations' => array( 3797 'readonly' => false, 3798 'destructive' => false, 3799 'idempotent' => true, 3800 ), 3801 ), 3802 ) 3803 ); 3804 3805 wp_register_ability( 3806 'es-football-bypass/run-check', 3807 array( 3808 'label' => __( 'Run block check now', 'es-football-bypass-for-cloudflare' ), 3809 'description' => __( 'Runs the same check the cron job runs: fetches the hayahora.futbol feed and applies the bypass policy, which may switch the managed records between Proxied and DNS Only.', 'es-football-bypass-for-cloudflare' ), 3810 'category' => 'es-football-bypass', 3811 'output_schema' => array( 3812 'type' => 'object', 3813 'properties' => array( 3814 'blocks_active' => array( 'type' => 'boolean' ), 3815 'domain_blocked' => array( 'type' => 'boolean' ), 3816 'bypass_active' => array( 'type' => 'boolean' ), 3817 'last_check' => array( 'type' => 'string' ), 3818 ), 3819 ), 3820 'execute_callback' => array( $this, 'ability_run_check' ), 3821 'permission_callback' => array( $this, 'abilities_permission_check' ), 3822 'meta' => array( 3823 'public' => true, 3824 'show_in_rest' => true, 3825 'annotations' => array( 3826 'readonly' => false, 3827 'destructive' => false, 3828 'idempotent' => false, 3829 ), 3830 ), 3831 ) 3832 ); 3833 3834 wp_register_ability( 3835 'es-football-bypass/set-proxy', 3836 array( 3837 'label' => __( 'Force proxy on or off', 'es-football-bypass-for-cloudflare' ), 3838 'description' => __( 'Forces the managed DNS records to Proxied (CDN) or DNS Only, exactly like the manual buttons on the Operation page. Changes live DNS at Cloudflare.', 'es-football-bypass-for-cloudflare' ), 3839 'category' => 'es-football-bypass', 3840 'input_schema' => array( 3841 'type' => 'object', 3842 'properties' => array( 3843 'mode' => array( 3844 'type' => 'string', 3845 'enum' => array( 'on', 'off' ), 3846 'description' => __( '"on" restores Proxied (CDN); "off" switches to DNS Only.', 'es-football-bypass-for-cloudflare' ), 3847 ), 3848 ), 3849 'required' => array( 'mode' ), 3850 ), 3851 'output_schema' => array( 3852 'type' => 'object', 3853 'properties' => array( 3854 'mode' => array( 'type' => 'string' ), 3855 'changed' => array( 'type' => 'integer' ), 3856 'unchanged' => array( 'type' => 'integer' ), 3857 'failed' => array( 'type' => 'integer' ), 3858 'report' => array( 3859 'type' => 'array', 3860 'items' => array( 'type' => 'string' ), 3861 ), 3862 ), 3863 ), 3864 'execute_callback' => array( $this, 'ability_set_proxy' ), 3865 'permission_callback' => array( $this, 'abilities_permission_check' ), 3866 'meta' => array( 3867 'public' => true, 3868 'show_in_rest' => true, 3869 'annotations' => array( 3870 'readonly' => false, 3871 'destructive' => true, 3872 'idempotent' => true, 3873 ), 3874 ), 3875 ) 3876 ); 3877 } 3878 3879 /** 3880 * Ability callback: current block and bypass status. 3881 * 3882 * @return array Status payload matching the registered output schema. 3883 */ 3884 public function ability_get_status() { 3885 $s = $this->get_settings(); 3886 $calc = $this->compute_statuses_from_json( false ); 3887 3888 return array( 3889 'blocks_active' => ( 'SÍ' === $calc['general'] ), 3890 'domain_blocked' => ( 'SÍ' === $calc['domain'] ), 3891 'bypass_active' => ! empty( $s['bypass_active'] ), 3892 'domain' => $this->get_site_domain(), 3893 'domain_ips' => array_values( (array) $calc['domain_ips'] ), 3894 'blocked_domain_ips' => array_values( (array) $calc['blocked_domain_ips'] ), 3895 'managed_records' => count( (array) ( $s['selected_records'] ?? array() ) ), 3896 'last_check' => (string) ( $s['last_check'] ?? '' ), 3897 'feed_last_update' => (string) ( $calc['last_update'] ?? '' ), 3898 ); 3899 } 3900 3901 /** 3902 * Ability callback: list the cached DNS records. 3903 * 3904 * @param array $input Optional 'type' and 'managed_only' filters. 3905 * @return array Records payload matching the registered output schema. 3906 */ 3907 public function ability_list_dns_records( $input = array() ) { 3908 $s = $this->get_settings(); 3909 $selected = (array) ( $s['selected_records'] ?? array() ); 3910 $filter = isset( $input['type'] ) ? (string) $input['type'] : ''; 3911 $only = ! empty( $input['managed_only'] ); 3912 3913 $out = array(); 3914 foreach ( (array) ( $s['dns_records_cache'] ?? array() ) as $r ) { 3915 $id = $r['id'] ?? ''; 3916 $managed = in_array( $id, $selected, true ); 3917 if ( $filter && ( $r['type'] ?? '' ) !== $filter ) { 3918 continue; 3919 } 3920 if ( $only && ! $managed ) { 3921 continue; 3922 } 3923 $out[] = array( 3924 'id' => (string) $id, 3925 'name' => (string) ( $r['name'] ?? '' ), 3926 'type' => (string) ( $r['type'] ?? '' ), 3927 'content' => (string) ( $r['content'] ?? '' ), 3928 'proxied' => array_key_exists( 'proxied', $r ) ? $r['proxied'] : null, 3929 'managed' => $managed, 3930 ); 3931 } 3932 3933 return array( 3934 'records' => $out, 3935 'total' => count( $out ), 3936 ); 3937 } 3938 3939 /** 3940 * Ability callback: replace the managed record selection. 3941 * 3942 * @param array $input Payload with the 'record_ids' array. 3943 * @return array Result payload matching the registered output schema. 3944 */ 3945 public function ability_set_managed_records( $input = array() ) { 3946 $s = $this->get_settings(); 3947 $wanted = array_values( array_unique( array_filter( array_map( 'sanitize_text_field', (array) ( $input['record_ids'] ?? array() ) ) ) ) ); 3948 $known = array(); 3949 foreach ( (array) ( $s['dns_records_cache'] ?? array() ) as $r ) { 3950 if ( ! empty( $r['id'] ) ) { 3951 $known[] = $r['id']; 3952 } 3953 } 3954 $valid = empty( $known ) ? $wanted : array_values( array_intersect( $wanted, $known ) ); 3955 $ignored = array_values( array_diff( $wanted, $valid ) ); 3956 3957 $s['selected_records'] = $valid; 3958 $this->save_settings( $s ); 3959 $this->log_event( 3960 'manual', 3961 'Selección de registros DNS guardada (Abilities API)', 3962 array( 3963 'usuario' => $this->current_user_label(), 3964 'seleccionados' => count( $valid ), 3965 'ignorados' => count( $ignored ), 3966 ) 3967 ); 3968 3969 return array( 3970 'managed_records' => count( $valid ), 3971 'ignored_ids' => $ignored, 3972 ); 3973 } 3974 3975 /** 3976 * Ability callback: run the block check and apply the policy. 3977 * 3978 * @return array Result payload matching the registered output schema. 3979 */ 3980 public function ability_run_check() { 3981 $this->check_football_and_manage_cloudflare(); 3982 $s = $this->get_settings(); 3983 $this->log_event( 3984 'manual', 3985 'Comprobación ejecutada (Abilities API)', 3986 array( 3987 'usuario' => $this->current_user_label(), 3988 'general' => $s['last_status_general'] ?? 'NO', 3989 'dominio' => $s['last_status_domain'] ?? 'NO', 3990 ) 3991 ); 3992 3993 return array( 3994 'blocks_active' => ( 'SI' === ( $s['last_status_general'] ?? 'NO' ) ), 3995 'domain_blocked' => ( 'SI' === ( $s['last_status_domain'] ?? 'NO' ) ), 3996 'bypass_active' => ! empty( $s['bypass_active'] ), 3997 'last_check' => (string) ( $s['last_check'] ?? '' ), 3998 ); 3999 } 4000 4001 /** 4002 * Ability callback: force the managed records to Proxied or DNS Only. 4003 * 4004 * @param array $input Payload with the 'mode' value ("on" or "off"). 4005 * @return array|WP_Error Result payload, or WP_Error when nothing can be done. 4006 */ 4007 public function ability_set_proxy( $input = array() ) { 4008 $mode = isset( $input['mode'] ) ? (string) $input['mode'] : ''; 4009 $proxied = ( 'on' === $mode ); 4010 4011 $s = $this->get_settings(); 4012 $sel = (array) ( $s['selected_records'] ?? array() ); 4013 if ( empty( $sel ) ) { 4014 return new WP_Error( 4015 'cfbcolorvivo_no_records', 4016 __( 'No DNS records are managed by the plugin. Set them first with the set-managed-records ability.', 'es-football-bypass-for-cloudflare' ) 4017 ); 4018 } 4019 if ( empty( $s['cloudflare_zone_id'] ) || empty( $s['cloudflare_api_key'] ) ) { 4020 return new WP_Error( 4021 'cfbcolorvivo_not_configured', 4022 __( 'Cloudflare credentials are not configured.', 'es-football-bypass-for-cloudflare' ) 4023 ); 4024 } 4025 4026 $log = array(); 4027 $report = $this->apply_proxy_to_records( $sel, $proxied, $log ); 4028 4029 $this->log_event( 4030 'manual', 4031 'Forzar Proxy ' . ( $proxied ? 'ON' : 'OFF' ) . ' (Abilities API)', 4032 array( 4033 'usuario' => $this->current_user_label(), 4034 'procesados' => count( $sel ), 4035 'ok' => $report['ok'], 4036 'errores' => $report['failed'], 4037 ) 4038 ); 4039 4040 return array( 4041 'mode' => $proxied ? 'on' : 'off', 4042 'changed' => $report['ok'], 4043 'unchanged' => $report['skipped'], 4044 'failed' => $report['failed'], 4045 'report' => $report['lines'], 4046 ); 3547 4047 } 3548 4048 … … 3926 4426 } 3927 4427 4428 /** 4429 * Apply a proxy state to a set of DNS records, refreshing the cache before and after. 4430 * 4431 * Shared by the "Force Proxy ON/OFF" AJAX actions and the Abilities API, so both 4432 * follow exactly the same path against Cloudflare. 4433 * 4434 * @param array $ids Cloudflare DNS record IDs to update. 4435 * @param bool $proxied True for Proxied (CDN), false for DNS Only. 4436 * @param array $log Trace log, appended by reference. 4437 * @return array { 4438 * @type int $ok Records actually changed. 4439 * @type int $skipped Records already in the requested state. 4440 * @type int $failed Records that could not be updated. 4441 * @type array $lines Human-readable report, one line per record. 4442 * } 4443 */ 4444 private function apply_proxy_to_records( array $ids, $proxied, array &$log ) { 4445 $state = $proxied ? 'ON' : 'OFF'; 4446 4447 $before = $this->fetch_dns_records( array( 'A', 'AAAA', 'CNAME' ), $log ); 4448 if ( ! empty( $before ) ) { 4449 $this->persist_dns_cache( $before ); 4450 } 4451 4452 $ok = 0; 4453 $failed = 0; 4454 $skipped = 0; 4455 $lines = array(); 4456 foreach ( $ids as $rid ) { 4457 $res = $this->update_record_proxy_status( $rid, $proxied, true ); 4458 if ( is_array( $res ) && ! empty( $res['success'] ) ) { 4459 if ( ! empty( $res['skipped'] ) ) { 4460 ++$skipped; 4461 $lines[] = 'SKIP: ' . ( $res['record']['name'] ?? $rid ) . ' (ya ' . $state . ')'; 4462 } else { 4463 ++$ok; 4464 $lines[] = 'OK: ' . ( $res['record']['name'] ?? $rid ) . ' -> ' . $state; 4465 } 4466 } else { 4467 ++$failed; 4468 $lines[] = 'ERR: ' . ( $res['record']['name'] ?? $rid ) . ' ' . ( is_array( $res ) && ! empty( $res['error'] ) ? $res['error'] : '' ); 4469 } 4470 } 4471 4472 $after = $this->fetch_dns_records( array( 'A', 'AAAA', 'CNAME' ), $log ); 4473 if ( ! empty( $after ) ) { 4474 $this->persist_dns_cache( $after ); 4475 } 4476 4477 return array( 4478 'ok' => $ok, 4479 'skipped' => $skipped, 4480 'failed' => $failed, 4481 'lines' => $lines, 4482 ); 4483 } 4484 3928 4485 /** AJAX handler: force proxy OFF (DNS Only) on selected records. */ 3929 4486 public function ajax_force_deactivate() { … … 3950 4507 } 3951 4508 3952 $before = $this->fetch_dns_records( array( 'A', 'AAAA', 'CNAME' ), $log ); 3953 if ( ! empty( $before ) ) { 3954 $this->persist_dns_cache( $before ); 3955 } 3956 $ok = 0; 3957 $fail = 0; 3958 $lines = array(); 3959 foreach ( $sel as $rid ) { 3960 $res = $this->update_record_proxy_status( $rid, false, true ); 3961 if ( is_array( $res ) && ! empty( $res['success'] ) ) { 3962 if ( ! empty( $res['skipped'] ) ) { 3963 $lines[] = 'SKIP: ' . ( $res['record']['name'] ?? $rid ) . ' (ya OFF)'; 3964 } else { 3965 ++$ok; 3966 $lines[] = 'OK: ' . ( $res['record']['name'] ?? $rid ) . ' -> OFF'; } 3967 } else { 3968 ++$fail; 3969 $lines[] = 'ERR: ' . ( ( $res['record']['name'] ?? $rid ) ) . ' ' . ( is_array( $res ) && ! empty( $res['error'] ) ? $res['error'] : '' ); 3970 } 3971 } 3972 $after = $this->fetch_dns_records( array( 'A', 'AAAA', 'CNAME' ), $log ); 3973 if ( ! empty( $after ) ) { 3974 $this->persist_dns_cache( $after ); 3975 } 4509 $report = $this->apply_proxy_to_records( $sel, false, $log ); 4510 $ok = $report['ok']; 4511 $fail = $report['failed']; 4512 $lines = $report['lines']; 3976 4513 3977 4514 $s2 = $this->get_settings(); … … 4024 4561 } 4025 4562 4026 $before = $this->fetch_dns_records( array( 'A', 'AAAA', 'CNAME' ), $log ); 4027 if ( ! empty( $before ) ) { 4028 $this->persist_dns_cache( $before ); 4029 } 4030 $ok = 0; 4031 $fail = 0; 4032 $lines = array(); 4033 foreach ( $sel as $rid ) { 4034 $res = $this->update_record_proxy_status( $rid, true, true ); 4035 if ( is_array( $res ) && ! empty( $res['success'] ) ) { 4036 if ( ! empty( $res['skipped'] ) ) { 4037 $lines[] = 'SKIP: ' . ( $res['record']['name'] ?? $rid ) . ' (ya ON)'; 4038 } else { 4039 ++$ok; 4040 $lines[] = 'OK: ' . ( $res['record']['name'] ?? $rid ) . ' -> ON'; } 4041 } else { 4042 ++$fail; 4043 $lines[] = 'ERR: ' . ( ( $res['record']['name'] ?? $rid ) ) . ' ' . ( is_array( $res ) && ! empty( $res['error'] ) ? $res['error'] : '' ); 4044 } 4045 } 4046 $after = $this->fetch_dns_records( array( 'A', 'AAAA', 'CNAME' ), $log ); 4047 if ( ! empty( $after ) ) { 4048 $this->persist_dns_cache( $after ); 4049 } 4563 $report = $this->apply_proxy_to_records( $sel, true, $log ); 4564 $ok = $report['ok']; 4565 $fail = $report['failed']; 4566 $lines = $report['lines']; 4050 4567 4051 4568 $s2 = $this->get_settings(); -
es-football-bypass-for-cloudflare/tags/2.0.0/readme.txt
r3707647 r3783334 2 2 Contributors: dcarrero 3 3 Tags: cloudflare, dns, football, bypass, ip-blocking 4 Requires at least: 5.04 Requires at least: 6.9 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 1.9.87 Stable tag: 2.0.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 178 178 In the Operation tab, click "WP-Cron Diagnostics" to see the next execution and the result of the last check. You can also review the integrated logs. 179 179 180 == Abilities API == 181 182 Since 2.0.0 the plugin registers five abilities through the WordPress Abilities API (WordPress 6.9 and newer), under the `es-football-bypass` category. This lets the REST API, MCP servers and AI agents read the block status and operate the bypass without going through the admin screens. 183 184 Every ability requires the `manage_options` capability, declares JSON Schemas for its input and output, and carries readonly/destructive/idempotent annotations so clients know what is safe to call. 185 186 = HTTP methods over REST = 187 WordPress derives the HTTP method from each ability's annotations, so REST clients must use the right verb (MCP clients call abilities by name and are unaffected): 188 189 * `get-status` and `list-dns-records` are `readonly`, so they are served over **GET**, with input as query parameters (`?input[type]=A`). 190 * `set-managed-records` and `run-check` are served over **POST**, with input in the JSON body (`{"input": {...}}`). 191 * `set-proxy` is annotated `destructive`, so it is served over **DELETE**. Pass its input as query parameters (`?input[mode]=off`): a JSON body is not read on DELETE requests. 192 193 = es-football-bypass/get-status = 194 Read-only. Returns whether La Liga IP blocks are active right now, whether this site resolves to a blocked IP, whether the bypass is engaged, the resolved IPs, how many records are managed, and the last check and feed timestamps. 195 196 = es-football-bypass/list-dns-records = 197 Read-only. Returns the cached DNS records with their proxy state and whether the plugin manages each one. Optional input: `type` (`A`, `AAAA` or `CNAME`) and `managed_only`. 198 199 = es-football-bypass/set-managed-records = 200 Replaces the list of records the plugin is allowed to switch. Input: `record_ids` (array of Cloudflare record IDs). IDs that do not exist in the zone are returned in `ignored_ids`. Pass an empty array to stop the plugin from touching DNS at all. 201 202 = es-football-bypass/run-check = 203 Runs the same check as the cron job: fetches the hayahora.futbol feed and applies the bypass policy, which may switch the managed records. 204 205 = es-football-bypass/set-proxy = 206 Forces the managed records to Proxied (CDN) or DNS Only. Input: `mode` (`on` or `off`). Annotated as destructive, since it changes live DNS at Cloudflare. Returns an error when no records are managed or Cloudflare credentials are missing. 207 180 208 == Logs and Auditing == 181 209 … … 191 219 == Screenshots == 192 220 193 1. Settings page: select Cloudflare authentication type (Global API Key, User Token or Account Token), enter credentials, configure check interval, cooldown after disabling Cloudflare, the "Force Proxy OFF during football" override, log retention, and the external cron token. 194 2. Operation page: live block status from hayahora.futbol, cached DNS records with current Proxied state per record, manual controls (save selection, reload from Cloudflare, force Proxy ON/OFF, manual check, WP-Cron diagnostics), and a sidebar with related links. 221 1. Settings page: Simple or Advanced interface mode, Cloudflare authentication type and credentials, server outbound IP, bypass behaviour (check interval, cooldown, "Force Proxy OFF during football" override, minimum ISPs with blocks), email notifications and uninstall options. 222 2. Operation page: live block status from hayahora.futbol and the DNS records the plugin manages, with type filters, quick selection buttons and the current proxy state per record, plus manual actions (check for blocks, force Proxy OFF or ON). 223 3. "Is there football now?" page: a quick yes or no answer on whether La Liga IP blocks are active, with the time of the last feed update and the affected ISPs. 195 224 196 225 == Changelog == 226 227 = 2.0.0 = 228 * NEW: Abilities API support (WordPress 6.9+). The plugin now registers five abilities under the "ES Football Bypass" category, so the REST API, MCP servers and AI agents can drive the bypass without going through the admin screens: `get-status`, `list-dns-records`, `set-managed-records`, `run-check` and `set-proxy` 229 * NEW: `es-football-bypass/get-status` — whether La Liga IP blocks are active, whether this site resolves to a blocked IP, and whether the bypass is currently engaged 230 * NEW: `es-football-bypass/list-dns-records` — the cached DNS records with their proxy state and whether the plugin manages each one, filterable by record type or to managed records only 231 * NEW: `es-football-bypass/set-managed-records` — replaces the list of records the plugin may switch; IDs missing from the Cloudflare zone are reported back as ignored 232 * NEW: `es-football-bypass/run-check` — runs the same check as the cron job and applies the bypass policy 233 * NEW: `es-football-bypass/set-proxy` — forces the managed records to Proxied (CDN) or DNS Only. Annotated as destructive so clients ask for confirmation, and it returns a clear error when no records are managed or Cloudflare is not configured 234 * All abilities require the `manage_options` capability, declare JSON Schemas for input and output, and carry readonly/destructive/idempotent annotations so agents know what is safe to call 235 * NEW: Filters `cfbcolorvivo_site_domain` and `cfbcolorvivo_is_local_domain`, so a staging copy, a reverse-proxied install or a WordPress Playground preview can unlock the Operation page and be checked against the production domain 236 * FIX: The "Unsaved changes" warning was visible on page load before touching anything — the stylesheet was overriding the `hidden` attribute 237 * FIX: PHP memory exhaustion while processing the status feed. The feed (about 5 MB, 50 MB once decoded) was decoded up to three times in a row and every copy was kept in memory until the end; a fourth decode on the Operation page only read a timestamp. It is now decoded once, so peak memory falls to roughly a third. Thanks to joserikko for the report 238 * FIX: After an automatic check the cached DNS records kept the proxy state from before the change, so the Operation page and `list-dns-records` could report the opposite of what was live at Cloudflare until the next refresh 239 * FIX: Uninstalling now removes the logs folder completely; a hidden `.htaccess` file was being left behind 240 * COMPAT: Minimum WordPress version raised to 6.9, which is where the Abilities API landed. Sites on older versions stay on 1.9.8, which contains the DNS record selection fix 241 * INTERNAL: The "Force Proxy ON/OFF" logic is now shared between the admin AJAX actions and the abilities, so both follow exactly the same path against Cloudflare 197 242 198 243 = 1.9.8 = … … 338 383 == Upgrade Notice == 339 384 385 = 2.0.0 = 386 Adds Abilities API support (WordPress 6.9+): five abilities let the REST API, MCP servers and AI agents read the block status and control the Cloudflare proxy. Minimum WordPress version is now 6.9. 387 340 388 = 1.9.8 = 341 389 Important fix: unchecking DNS records did nothing, so manual actions were applied to every record in the zone. Adds a "Save selection" button, record type filters (Only A / A + AAAA) and clearer feedback. Review your selected records after updating. … … 388 436 == Server Requirements == 389 437 390 - WordPress 5.0or higher438 - WordPress 6.9 or higher 391 439 - PHP 7.4 or higher 392 440 - PHP Extensions: curl, json -
es-football-bypass-for-cloudflare/tags/2.0.0/uninstall.php
r3602202 r3783334 63 63 } 64 64 } 65 // glob() no lista los ficheros ocultos: sin esto el .htaccess impide borrar el directorio. 66 wp_delete_file( $cfbcolorvivo_dir . '/.htaccess' ); 65 67 $wp_filesystem->rmdir( $cfbcolorvivo_dir ); 66 68 } … … 82 84 } 83 85 } 86 // glob() no lista los ficheros ocultos: sin esto el .htaccess impide borrar el directorio. 87 wp_delete_file( $cfbcolorvivo_dir . '/.htaccess' ); 84 88 $wp_filesystem->rmdir( $cfbcolorvivo_dir ); 85 89 } -
es-football-bypass-for-cloudflare/trunk/README.md
r3707647 r3783334 1 1 # ES Football Bypass for Cloudflare 2 2 3 **Versión 1.9.8** | [Changelog](readme.txt)3 **Versión 2.0.0** | [Changelog](readme.txt) 4 4 5 5 Plugin de WordPress que automatiza el cambio entre modo Proxied y DNS Only en Cloudflare cuando hay bloqueos masivos ordenados durante los partidos de fútbol en España. Consulta el feed de [hayahora.futbol](https://hayahora.futbol/) y activa/desactiva los registros seleccionados para mantener accesible tu sitio legítimo, con un periodo de enfriamiento configurable antes de volver a activar Cloudflare. 6 7 ## Capturas 8 9 **Operación:** estado de los bloqueos, registros DNS gestionados y acciones manuales. 10 11  12 13 **¿Hay fútbol ahora?:** respuesta rápida sobre si hay bloqueos activos. 14 15  16 17 **Configuración:** credenciales de Cloudflare, comportamiento del bypass y avisos. 18 19  6 20 7 21 ## Instalación rápida -
es-football-bypass-for-cloudflare/trunk/es-football-bypass.php
r3707647 r3783334 4 4 * Plugin URI: https://github.com/dcarrero/cf-football-bypass 5 5 * Description: Operates with Cloudflare to toggle between Proxy (ON/CDN) and DNS Only (OFF) based on IP blocks, with persistent DNS cache and AJAX actions. Separate UI: Operation and Settings. 6 * Version: 1.9.86 * Version: 2.0.0 7 7 * Author: David Carrero Fernandez-Baillo 8 8 * Author URI: https://carrero.es … … 11 11 * Text Domain: es-football-bypass-for-cloudflare 12 12 * Domain Path: /languages 13 * Requires at least: 5.013 * Requires at least: 6.9 14 14 * Requires PHP: 7.4 15 15 * Update URI: https://wordpress.org/plugins/es-football-bypass-for-cloudflare/ … … 118 118 add_action( 'wp_ajax_cfbcolorvivo_feed_diagnostics', array( $this, 'ajax_feed_diagnostics' ) ); 119 119 add_action( 'init', array( $this, 'maybe_process_external_cron' ) ); 120 121 // Abilities API (WordPress 6.9+): permite a REST, MCP y agentes operar el bypass. 122 add_action( 'wp_abilities_api_categories_init', array( $this, 'register_ability_category' ) ); 123 add_action( 'wp_abilities_api_init', array( $this, 'register_abilities' ) ); 120 124 } 121 125 … … 271 275 return true; 272 276 } 273 $local = array( 'localhost', 'localhost.localdomain' ); 274 if ( in_array( strtolower( $domain ), $local, true ) ) { 275 return true; 276 } 277 if ( preg_match( '/\.(local|test|ddev\.site|lndo\.site|wp\.lan)$/i', $domain ) ) { 278 return true; 279 } 280 return false; 277 $local = array( 'localhost', 'localhost.localdomain' ); 278 $is_local = in_array( strtolower( $domain ), $local, true ) 279 || (bool) preg_match( '/\.(local|test|ddev\.site|lndo\.site|wp\.lan)$/i', $domain ); 280 281 /** 282 * Filters whether the current domain is a local environment. 283 * 284 * Returning false unlocks the Operation page on a local or preview install, 285 * which is what a staging copy or a WordPress Playground demo needs. 286 * 287 * @param bool $is_local Whether the domain looks local. 288 * @param string $domain Domain being checked. 289 */ 290 return (bool) apply_filters( 'cfbcolorvivo_is_local_domain', $is_local, $domain ); 281 291 } 282 292 /** Detect the outgoing IP addresses of this server (cached 1 hour). */ … … 949 959 950 960 // Registrar script handle para asociar inline scripts. 951 wp_register_script( 'cfbcolorvivo-admin', false, array(), ' 1.9.8', true );961 wp_register_script( 'cfbcolorvivo-admin', false, array(), '2.0.0', true ); 952 962 wp_enqueue_script( 'cfbcolorvivo-admin' ); 953 963 … … 964 974 965 975 // Estilos de la interfaz de administración. 966 wp_register_style( 'cfbcolorvivo-admin', false, array(), ' 1.9.8' );976 wp_register_style( 'cfbcolorvivo-admin', false, array(), '2.0.0' ); 967 977 wp_enqueue_style( 'cfbcolorvivo-admin' ); 968 978 wp_add_inline_style( 'cfbcolorvivo-admin', $this->get_admin_css() ); … … 1004 1014 .cfbcolorvivo-count #cfbcolorvivo-sel-count{font-weight:700;color:#1d2327} 1005 1015 .cfbcolorvivo-dirty{display:inline-block;margin-left:8px;padding:2px 8px;background:#fcf9e8;border-left:3px solid #dba617;color:#674d00} 1016 .cfbcolorvivo-dirty[hidden]{display:none} 1006 1017 .cfbcolorvivo-actions{margin:12px 0 0} 1007 1018 .cfbcolorvivo-actionrow{display:flex;flex-wrap:wrap;gap:8px;align-items:center} … … 2695 2706 } 2696 2707 2708 if ( ! empty( $settings['selected_records'] ) ) { 2709 // Los PUT no tocan la caché: la recargamos para que refleje el estado que acabamos de aplicar. 2710 if ( $updated > 0 ) { 2711 $after = $this->fetch_dns_records(); 2712 if ( ! empty( $after ) ) { 2713 $this->persist_dns_cache( $after ); 2714 } 2715 } 2716 // $settings se leyó antes de refrescar la caché: guardarlo tal cual la sobrescribiría con datos viejos. 2717 $settings = $this->get_settings(); 2718 } 2719 2697 2720 $settings['last_check'] = $now_mysql; 2698 2721 $settings['last_status_general'] = ( 'SÍ' === $general ) ? 'SI' : 'NO'; … … 2884 2907 $home = home_url( '/' ); 2885 2908 $host = wp_parse_url( $home, PHP_URL_HOST ); 2886 if ( $host ) { 2887 return $host; 2888 } 2889 // Fallback to HTTP_HOST only if home_url parsing fails. 2890 return isset( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : ''; 2909 if ( ! $host ) { 2910 // Fallback to HTTP_HOST only if home_url parsing fails. 2911 $host = isset( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : ''; 2912 } 2913 2914 /** 2915 * Filters the domain the plugin considers to be this site. 2916 * 2917 * Useful behind a reverse proxy, or on a staging copy that must be checked 2918 * against the production domain. 2919 * 2920 * @param string $host Domain resolved from home_url(). 2921 */ 2922 return (string) apply_filters( 'cfbcolorvivo_site_domain', $host ); 2891 2923 } 2892 2924 /** … … 2949 2981 return null; 2950 2982 } 2983 /** 2984 * Read lastUpdate from a raw feed body without building the whole array. 2985 * 2986 * The feed decodes to about 50 MB, so it should not be decoded just to compare a 2987 * timestamp. lastUpdate is the first key of the feed; if that ever changes this 2988 * falls back to a full decode, released before returning. 2989 * 2990 * @param string $body Raw JSON body. 2991 * @return string|null lastUpdate value, or null if the body has none. 2992 */ 2993 private function read_feed_last_update( $body ) { 2994 if ( ! is_string( $body ) || '' === $body ) { 2995 return null; 2996 } 2997 if ( preg_match( '/\A\s*\{\s*"lastUpdate"\s*:\s*"([^"\\\\]+)"/', substr( $body, 0, 256 ), $matches ) ) { 2998 return $matches[1]; 2999 } 3000 $json = json_decode( $body, true ); 3001 return ( is_array( $json ) && ! empty( $json['lastUpdate'] ) && is_string( $json['lastUpdate'] ) ) ? $json['lastUpdate'] : null; 3002 } 3003 2951 3004 /** Fetch and cache the football block status JSON from remote or local. */ 2952 3005 private function fetch_status_json() { … … 2960 3013 $local_body = $wp_fs->get_contents( $local_path ); 2961 3014 if ( false !== $local_body ) { 2962 $json_local = json_decode( $local_body, true ); 2963 if ( is_array( $json_local ) && ! empty( $json_local['lastUpdate'] ) && is_string( $json_local['lastUpdate'] ) ) { 2964 $last_local = $json_local['lastUpdate']; 2965 } 3015 $last_local = $this->read_feed_last_update( $local_body ); 2966 3016 } 2967 3017 } … … 2969 3019 $remote_ok = false; 2970 3020 $remote_body = null; 3021 $remote_json = null; 2971 3022 $last_remote = null; 2972 3023 $remote_code = 0; … … 2990 3041 'timeout' => 25, 2991 3042 'redirection' => 5, 2992 'user-agent' => 'ESFB/ 1.9.8; ' . home_url( '/' ),3043 'user-agent' => 'ESFB/2.0.0; ' . home_url( '/' ), 2993 3044 ) 2994 3045 ); … … 2999 3050 if ( 200 === $remote_code ) { 3000 3051 $remote_body = wp_remote_retrieve_body( $resp ); 3001 $ tmp= json_decode( $remote_body, true );3002 if ( is_array( $ tmp) ) {3052 $remote_json = json_decode( $remote_body, true ); 3053 if ( is_array( $remote_json ) ) { 3003 3054 $remote_ok = true; 3004 if ( ! empty( $ tmp['lastUpdate'] ) && is_string( $tmp['lastUpdate'] ) ) {3005 $last_remote = $ tmp['lastUpdate'];3055 if ( ! empty( $remote_json['lastUpdate'] ) && is_string( $remote_json['lastUpdate'] ) ) { 3056 $last_remote = $remote_json['lastUpdate']; 3006 3057 } 3007 3058 } else { … … 3066 3117 } 3067 3118 3068 $json = json_decode( $body, true ); 3119 // Reutilizamos el remoto ya decodificado si es el que vamos a usar; si no, se libera 3120 // antes de decodificar el local para no tener nunca dos copias en memoria (issue #8). 3121 if ( is_array( $remote_json ) && $body === $remote_body ) { 3122 $json = $remote_json; 3123 } else { 3124 $remote_json = null; 3125 $json = json_decode( $body, true ); 3126 // Copia local corrupta con cabecera válida (p. ej. escritura truncada): se repara con el remoto. 3127 if ( ! is_array( $json ) && $remote_ok ) { 3128 $json = json_decode( $remote_body, true ); 3129 if ( $wp_fs && is_array( $json ) ) { 3130 $wp_fs->put_contents( $local_path, $remote_body, FS_CHMOD_FILE ); 3131 $this->log( '[CFB] Local data.json corrupto; restaurado desde el remoto.' ); 3132 } 3133 } 3134 } 3135 unset( $remote_json, $remote_body, $local_body, $body ); 3136 3069 3137 if ( ! is_array( $json ) ) { 3070 3138 $this->log( '[CFB] JSON decode failed' ); … … 3094 3162 } 3095 3163 3096 $extracted = $this->extract_ip_block_map( $json ); 3164 $extracted = $this->extract_ip_block_map( $json ); 3165 unset( $json ); 3166 3097 3167 $map = $extracted['ip_map'] ?? array(); 3098 3168 $isps_blocked = $extracted['isps_blocked'] ?? array(); … … 3375 3445 $contents = file_get_contents( $local_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents 3376 3446 if ( is_string( $contents ) ) { 3377 $decoded = json_decode( $contents, true ); 3378 if ( is_array( $decoded ) && ! empty( $decoded['lastUpdate'] ) && is_string( $decoded['lastUpdate'] ) ) { 3379 $local_last_update = $decoded['lastUpdate']; 3380 } 3447 $local_last_update = (string) $this->read_feed_last_update( $contents ); 3381 3448 } 3382 3449 } … … 3512 3579 $home = home_url( '/' ); 3513 3580 $mgr_url = admin_url( 'admin.php?page=cfbcolorvivo-main' ); 3514 $version = ' 1.9.8';3581 $version = '2.0.0'; 3515 3582 3516 3583 if ( $now_active ) { … … 3545 3612 } 3546 3613 $this->log( '[CFB] Email notificación ' . ( $sent ? 'enviada' : 'FALLÓ' ) . ' a ' . $to . ' (estado=' . ( $now_active ? 'bypass activo' : 'bypass inactivo' ) . ')' ); 3614 } 3615 3616 /* ================== Abilities API (WordPress 6.9+) ================== */ 3617 3618 /** 3619 * Register the ability category for this plugin. 3620 * 3621 * Requires WordPress 6.9 or newer, which is the plugin minimum since 2.0.0. 3622 */ 3623 public function register_ability_category() { 3624 wp_register_ability_category( 3625 'es-football-bypass', 3626 array( 3627 'label' => __( 'ES Football Bypass', 'es-football-bypass-for-cloudflare' ), 3628 'description' => __( 'Check La Liga IP block status and control the Cloudflare proxy on the managed DNS records.', 'es-football-bypass-for-cloudflare' ), 3629 ) 3630 ); 3631 } 3632 3633 /** 3634 * Permission check shared by every ability. 3635 * 3636 * Abilities run outside the admin screens (REST, MCP, AI agents), so they enforce 3637 * the same capability the admin pages require. 3638 * 3639 * @return bool True when the current user may operate the plugin. 3640 */ 3641 public function abilities_permission_check() { 3642 return current_user_can( 'manage_options' ); 3643 } 3644 3645 /** 3646 * Register the plugin abilities so REST and MCP clients can drive the bypass. 3647 */ 3648 public function register_abilities() { 3649 $record_schema = array( 3650 'type' => 'object', 3651 'properties' => array( 3652 'id' => array( 3653 'type' => 'string', 3654 'description' => __( 'Cloudflare DNS record ID.', 'es-football-bypass-for-cloudflare' ), 3655 ), 3656 'name' => array( 'type' => 'string' ), 3657 'type' => array( 'type' => 'string' ), 3658 'content' => array( 'type' => 'string' ), 3659 'proxied' => array( 3660 'type' => array( 'boolean', 'null' ), 3661 'description' => __( 'True when the record goes through the Cloudflare CDN, null when the record cannot be proxied.', 'es-football-bypass-for-cloudflare' ), 3662 ), 3663 'managed' => array( 3664 'type' => 'boolean', 3665 'description' => __( 'True when the plugin is allowed to switch this record.', 'es-football-bypass-for-cloudflare' ), 3666 ), 3667 ), 3668 ); 3669 3670 wp_register_ability( 3671 'es-football-bypass/get-status', 3672 array( 3673 'label' => __( 'Get bypass status', 'es-football-bypass-for-cloudflare' ), 3674 'description' => __( 'Returns whether La Liga IP blocks are active right now, whether this site is among the blocked IPs, and whether the Cloudflare bypass is currently engaged.', 'es-football-bypass-for-cloudflare' ), 3675 'category' => 'es-football-bypass', 3676 'output_schema' => array( 3677 'type' => 'object', 3678 'properties' => array( 3679 'blocks_active' => array( 3680 'type' => 'boolean', 3681 'description' => __( 'Blocks are active on some IPs right now.', 'es-football-bypass-for-cloudflare' ), 3682 ), 3683 'domain_blocked' => array( 3684 'type' => 'boolean', 3685 'description' => __( 'This site resolves to at least one blocked IP.', 'es-football-bypass-for-cloudflare' ), 3686 ), 3687 'bypass_active' => array( 3688 'type' => 'boolean', 3689 'description' => __( 'The plugin currently has the managed records on DNS Only.', 'es-football-bypass-for-cloudflare' ), 3690 ), 3691 'domain' => array( 'type' => 'string' ), 3692 'domain_ips' => array( 3693 'type' => 'array', 3694 'items' => array( 'type' => 'string' ), 3695 ), 3696 'blocked_domain_ips' => array( 3697 'type' => 'array', 3698 'items' => array( 'type' => 'string' ), 3699 ), 3700 'managed_records' => array( 'type' => 'integer' ), 3701 'last_check' => array( 'type' => 'string' ), 3702 'feed_last_update' => array( 'type' => 'string' ), 3703 ), 3704 ), 3705 'execute_callback' => array( $this, 'ability_get_status' ), 3706 'permission_callback' => array( $this, 'abilities_permission_check' ), 3707 'meta' => array( 3708 'public' => true, 3709 'show_in_rest' => true, 3710 'annotations' => array( 3711 'readonly' => true, 3712 'destructive' => false, 3713 'idempotent' => true, 3714 ), 3715 ), 3716 ) 3717 ); 3718 3719 wp_register_ability( 3720 'es-football-bypass/list-dns-records', 3721 array( 3722 'label' => __( 'List DNS records', 'es-football-bypass-for-cloudflare' ), 3723 'description' => __( 'Lists the cached Cloudflare DNS records with their current proxy state and whether the plugin manages each one. Optionally filtered by record type.', 'es-football-bypass-for-cloudflare' ), 3724 'category' => 'es-football-bypass', 3725 'input_schema' => array( 3726 'type' => 'object', 3727 'default' => array(), 3728 'properties' => array( 3729 'type' => array( 3730 'type' => 'string', 3731 'enum' => array( 'A', 'AAAA', 'CNAME' ), 3732 'description' => __( 'Return only records of this type.', 'es-football-bypass-for-cloudflare' ), 3733 ), 3734 'managed_only' => array( 3735 'type' => 'boolean', 3736 'description' => __( 'Return only the records the plugin manages.', 'es-football-bypass-for-cloudflare' ), 3737 ), 3738 ), 3739 ), 3740 'output_schema' => array( 3741 'type' => 'object', 3742 'properties' => array( 3743 'records' => array( 3744 'type' => 'array', 3745 'items' => $record_schema, 3746 ), 3747 'total' => array( 'type' => 'integer' ), 3748 ), 3749 ), 3750 'execute_callback' => array( $this, 'ability_list_dns_records' ), 3751 'permission_callback' => array( $this, 'abilities_permission_check' ), 3752 'meta' => array( 3753 'public' => true, 3754 'show_in_rest' => true, 3755 'annotations' => array( 3756 'readonly' => true, 3757 'destructive' => false, 3758 'idempotent' => true, 3759 ), 3760 ), 3761 ) 3762 ); 3763 3764 wp_register_ability( 3765 'es-football-bypass/set-managed-records', 3766 array( 3767 'label' => __( 'Set managed DNS records', 'es-football-bypass-for-cloudflare' ), 3768 'description' => __( 'Replaces the list of DNS records the plugin is allowed to switch. Record IDs that do not exist in the Cloudflare zone are ignored. Pass an empty array to stop the plugin from touching DNS at all.', 'es-football-bypass-for-cloudflare' ), 3769 'category' => 'es-football-bypass', 3770 'input_schema' => array( 3771 'type' => 'object', 3772 'properties' => array( 3773 'record_ids' => array( 3774 'type' => 'array', 3775 'items' => array( 'type' => 'string' ), 3776 'description' => __( 'Cloudflare DNS record IDs to manage.', 'es-football-bypass-for-cloudflare' ), 3777 ), 3778 ), 3779 'required' => array( 'record_ids' ), 3780 ), 3781 'output_schema' => array( 3782 'type' => 'object', 3783 'properties' => array( 3784 'managed_records' => array( 'type' => 'integer' ), 3785 'ignored_ids' => array( 3786 'type' => 'array', 3787 'items' => array( 'type' => 'string' ), 3788 ), 3789 ), 3790 ), 3791 'execute_callback' => array( $this, 'ability_set_managed_records' ), 3792 'permission_callback' => array( $this, 'abilities_permission_check' ), 3793 'meta' => array( 3794 'public' => true, 3795 'show_in_rest' => true, 3796 'annotations' => array( 3797 'readonly' => false, 3798 'destructive' => false, 3799 'idempotent' => true, 3800 ), 3801 ), 3802 ) 3803 ); 3804 3805 wp_register_ability( 3806 'es-football-bypass/run-check', 3807 array( 3808 'label' => __( 'Run block check now', 'es-football-bypass-for-cloudflare' ), 3809 'description' => __( 'Runs the same check the cron job runs: fetches the hayahora.futbol feed and applies the bypass policy, which may switch the managed records between Proxied and DNS Only.', 'es-football-bypass-for-cloudflare' ), 3810 'category' => 'es-football-bypass', 3811 'output_schema' => array( 3812 'type' => 'object', 3813 'properties' => array( 3814 'blocks_active' => array( 'type' => 'boolean' ), 3815 'domain_blocked' => array( 'type' => 'boolean' ), 3816 'bypass_active' => array( 'type' => 'boolean' ), 3817 'last_check' => array( 'type' => 'string' ), 3818 ), 3819 ), 3820 'execute_callback' => array( $this, 'ability_run_check' ), 3821 'permission_callback' => array( $this, 'abilities_permission_check' ), 3822 'meta' => array( 3823 'public' => true, 3824 'show_in_rest' => true, 3825 'annotations' => array( 3826 'readonly' => false, 3827 'destructive' => false, 3828 'idempotent' => false, 3829 ), 3830 ), 3831 ) 3832 ); 3833 3834 wp_register_ability( 3835 'es-football-bypass/set-proxy', 3836 array( 3837 'label' => __( 'Force proxy on or off', 'es-football-bypass-for-cloudflare' ), 3838 'description' => __( 'Forces the managed DNS records to Proxied (CDN) or DNS Only, exactly like the manual buttons on the Operation page. Changes live DNS at Cloudflare.', 'es-football-bypass-for-cloudflare' ), 3839 'category' => 'es-football-bypass', 3840 'input_schema' => array( 3841 'type' => 'object', 3842 'properties' => array( 3843 'mode' => array( 3844 'type' => 'string', 3845 'enum' => array( 'on', 'off' ), 3846 'description' => __( '"on" restores Proxied (CDN); "off" switches to DNS Only.', 'es-football-bypass-for-cloudflare' ), 3847 ), 3848 ), 3849 'required' => array( 'mode' ), 3850 ), 3851 'output_schema' => array( 3852 'type' => 'object', 3853 'properties' => array( 3854 'mode' => array( 'type' => 'string' ), 3855 'changed' => array( 'type' => 'integer' ), 3856 'unchanged' => array( 'type' => 'integer' ), 3857 'failed' => array( 'type' => 'integer' ), 3858 'report' => array( 3859 'type' => 'array', 3860 'items' => array( 'type' => 'string' ), 3861 ), 3862 ), 3863 ), 3864 'execute_callback' => array( $this, 'ability_set_proxy' ), 3865 'permission_callback' => array( $this, 'abilities_permission_check' ), 3866 'meta' => array( 3867 'public' => true, 3868 'show_in_rest' => true, 3869 'annotations' => array( 3870 'readonly' => false, 3871 'destructive' => true, 3872 'idempotent' => true, 3873 ), 3874 ), 3875 ) 3876 ); 3877 } 3878 3879 /** 3880 * Ability callback: current block and bypass status. 3881 * 3882 * @return array Status payload matching the registered output schema. 3883 */ 3884 public function ability_get_status() { 3885 $s = $this->get_settings(); 3886 $calc = $this->compute_statuses_from_json( false ); 3887 3888 return array( 3889 'blocks_active' => ( 'SÍ' === $calc['general'] ), 3890 'domain_blocked' => ( 'SÍ' === $calc['domain'] ), 3891 'bypass_active' => ! empty( $s['bypass_active'] ), 3892 'domain' => $this->get_site_domain(), 3893 'domain_ips' => array_values( (array) $calc['domain_ips'] ), 3894 'blocked_domain_ips' => array_values( (array) $calc['blocked_domain_ips'] ), 3895 'managed_records' => count( (array) ( $s['selected_records'] ?? array() ) ), 3896 'last_check' => (string) ( $s['last_check'] ?? '' ), 3897 'feed_last_update' => (string) ( $calc['last_update'] ?? '' ), 3898 ); 3899 } 3900 3901 /** 3902 * Ability callback: list the cached DNS records. 3903 * 3904 * @param array $input Optional 'type' and 'managed_only' filters. 3905 * @return array Records payload matching the registered output schema. 3906 */ 3907 public function ability_list_dns_records( $input = array() ) { 3908 $s = $this->get_settings(); 3909 $selected = (array) ( $s['selected_records'] ?? array() ); 3910 $filter = isset( $input['type'] ) ? (string) $input['type'] : ''; 3911 $only = ! empty( $input['managed_only'] ); 3912 3913 $out = array(); 3914 foreach ( (array) ( $s['dns_records_cache'] ?? array() ) as $r ) { 3915 $id = $r['id'] ?? ''; 3916 $managed = in_array( $id, $selected, true ); 3917 if ( $filter && ( $r['type'] ?? '' ) !== $filter ) { 3918 continue; 3919 } 3920 if ( $only && ! $managed ) { 3921 continue; 3922 } 3923 $out[] = array( 3924 'id' => (string) $id, 3925 'name' => (string) ( $r['name'] ?? '' ), 3926 'type' => (string) ( $r['type'] ?? '' ), 3927 'content' => (string) ( $r['content'] ?? '' ), 3928 'proxied' => array_key_exists( 'proxied', $r ) ? $r['proxied'] : null, 3929 'managed' => $managed, 3930 ); 3931 } 3932 3933 return array( 3934 'records' => $out, 3935 'total' => count( $out ), 3936 ); 3937 } 3938 3939 /** 3940 * Ability callback: replace the managed record selection. 3941 * 3942 * @param array $input Payload with the 'record_ids' array. 3943 * @return array Result payload matching the registered output schema. 3944 */ 3945 public function ability_set_managed_records( $input = array() ) { 3946 $s = $this->get_settings(); 3947 $wanted = array_values( array_unique( array_filter( array_map( 'sanitize_text_field', (array) ( $input['record_ids'] ?? array() ) ) ) ) ); 3948 $known = array(); 3949 foreach ( (array) ( $s['dns_records_cache'] ?? array() ) as $r ) { 3950 if ( ! empty( $r['id'] ) ) { 3951 $known[] = $r['id']; 3952 } 3953 } 3954 $valid = empty( $known ) ? $wanted : array_values( array_intersect( $wanted, $known ) ); 3955 $ignored = array_values( array_diff( $wanted, $valid ) ); 3956 3957 $s['selected_records'] = $valid; 3958 $this->save_settings( $s ); 3959 $this->log_event( 3960 'manual', 3961 'Selección de registros DNS guardada (Abilities API)', 3962 array( 3963 'usuario' => $this->current_user_label(), 3964 'seleccionados' => count( $valid ), 3965 'ignorados' => count( $ignored ), 3966 ) 3967 ); 3968 3969 return array( 3970 'managed_records' => count( $valid ), 3971 'ignored_ids' => $ignored, 3972 ); 3973 } 3974 3975 /** 3976 * Ability callback: run the block check and apply the policy. 3977 * 3978 * @return array Result payload matching the registered output schema. 3979 */ 3980 public function ability_run_check() { 3981 $this->check_football_and_manage_cloudflare(); 3982 $s = $this->get_settings(); 3983 $this->log_event( 3984 'manual', 3985 'Comprobación ejecutada (Abilities API)', 3986 array( 3987 'usuario' => $this->current_user_label(), 3988 'general' => $s['last_status_general'] ?? 'NO', 3989 'dominio' => $s['last_status_domain'] ?? 'NO', 3990 ) 3991 ); 3992 3993 return array( 3994 'blocks_active' => ( 'SI' === ( $s['last_status_general'] ?? 'NO' ) ), 3995 'domain_blocked' => ( 'SI' === ( $s['last_status_domain'] ?? 'NO' ) ), 3996 'bypass_active' => ! empty( $s['bypass_active'] ), 3997 'last_check' => (string) ( $s['last_check'] ?? '' ), 3998 ); 3999 } 4000 4001 /** 4002 * Ability callback: force the managed records to Proxied or DNS Only. 4003 * 4004 * @param array $input Payload with the 'mode' value ("on" or "off"). 4005 * @return array|WP_Error Result payload, or WP_Error when nothing can be done. 4006 */ 4007 public function ability_set_proxy( $input = array() ) { 4008 $mode = isset( $input['mode'] ) ? (string) $input['mode'] : ''; 4009 $proxied = ( 'on' === $mode ); 4010 4011 $s = $this->get_settings(); 4012 $sel = (array) ( $s['selected_records'] ?? array() ); 4013 if ( empty( $sel ) ) { 4014 return new WP_Error( 4015 'cfbcolorvivo_no_records', 4016 __( 'No DNS records are managed by the plugin. Set them first with the set-managed-records ability.', 'es-football-bypass-for-cloudflare' ) 4017 ); 4018 } 4019 if ( empty( $s['cloudflare_zone_id'] ) || empty( $s['cloudflare_api_key'] ) ) { 4020 return new WP_Error( 4021 'cfbcolorvivo_not_configured', 4022 __( 'Cloudflare credentials are not configured.', 'es-football-bypass-for-cloudflare' ) 4023 ); 4024 } 4025 4026 $log = array(); 4027 $report = $this->apply_proxy_to_records( $sel, $proxied, $log ); 4028 4029 $this->log_event( 4030 'manual', 4031 'Forzar Proxy ' . ( $proxied ? 'ON' : 'OFF' ) . ' (Abilities API)', 4032 array( 4033 'usuario' => $this->current_user_label(), 4034 'procesados' => count( $sel ), 4035 'ok' => $report['ok'], 4036 'errores' => $report['failed'], 4037 ) 4038 ); 4039 4040 return array( 4041 'mode' => $proxied ? 'on' : 'off', 4042 'changed' => $report['ok'], 4043 'unchanged' => $report['skipped'], 4044 'failed' => $report['failed'], 4045 'report' => $report['lines'], 4046 ); 3547 4047 } 3548 4048 … … 3926 4426 } 3927 4427 4428 /** 4429 * Apply a proxy state to a set of DNS records, refreshing the cache before and after. 4430 * 4431 * Shared by the "Force Proxy ON/OFF" AJAX actions and the Abilities API, so both 4432 * follow exactly the same path against Cloudflare. 4433 * 4434 * @param array $ids Cloudflare DNS record IDs to update. 4435 * @param bool $proxied True for Proxied (CDN), false for DNS Only. 4436 * @param array $log Trace log, appended by reference. 4437 * @return array { 4438 * @type int $ok Records actually changed. 4439 * @type int $skipped Records already in the requested state. 4440 * @type int $failed Records that could not be updated. 4441 * @type array $lines Human-readable report, one line per record. 4442 * } 4443 */ 4444 private function apply_proxy_to_records( array $ids, $proxied, array &$log ) { 4445 $state = $proxied ? 'ON' : 'OFF'; 4446 4447 $before = $this->fetch_dns_records( array( 'A', 'AAAA', 'CNAME' ), $log ); 4448 if ( ! empty( $before ) ) { 4449 $this->persist_dns_cache( $before ); 4450 } 4451 4452 $ok = 0; 4453 $failed = 0; 4454 $skipped = 0; 4455 $lines = array(); 4456 foreach ( $ids as $rid ) { 4457 $res = $this->update_record_proxy_status( $rid, $proxied, true ); 4458 if ( is_array( $res ) && ! empty( $res['success'] ) ) { 4459 if ( ! empty( $res['skipped'] ) ) { 4460 ++$skipped; 4461 $lines[] = 'SKIP: ' . ( $res['record']['name'] ?? $rid ) . ' (ya ' . $state . ')'; 4462 } else { 4463 ++$ok; 4464 $lines[] = 'OK: ' . ( $res['record']['name'] ?? $rid ) . ' -> ' . $state; 4465 } 4466 } else { 4467 ++$failed; 4468 $lines[] = 'ERR: ' . ( $res['record']['name'] ?? $rid ) . ' ' . ( is_array( $res ) && ! empty( $res['error'] ) ? $res['error'] : '' ); 4469 } 4470 } 4471 4472 $after = $this->fetch_dns_records( array( 'A', 'AAAA', 'CNAME' ), $log ); 4473 if ( ! empty( $after ) ) { 4474 $this->persist_dns_cache( $after ); 4475 } 4476 4477 return array( 4478 'ok' => $ok, 4479 'skipped' => $skipped, 4480 'failed' => $failed, 4481 'lines' => $lines, 4482 ); 4483 } 4484 3928 4485 /** AJAX handler: force proxy OFF (DNS Only) on selected records. */ 3929 4486 public function ajax_force_deactivate() { … … 3950 4507 } 3951 4508 3952 $before = $this->fetch_dns_records( array( 'A', 'AAAA', 'CNAME' ), $log ); 3953 if ( ! empty( $before ) ) { 3954 $this->persist_dns_cache( $before ); 3955 } 3956 $ok = 0; 3957 $fail = 0; 3958 $lines = array(); 3959 foreach ( $sel as $rid ) { 3960 $res = $this->update_record_proxy_status( $rid, false, true ); 3961 if ( is_array( $res ) && ! empty( $res['success'] ) ) { 3962 if ( ! empty( $res['skipped'] ) ) { 3963 $lines[] = 'SKIP: ' . ( $res['record']['name'] ?? $rid ) . ' (ya OFF)'; 3964 } else { 3965 ++$ok; 3966 $lines[] = 'OK: ' . ( $res['record']['name'] ?? $rid ) . ' -> OFF'; } 3967 } else { 3968 ++$fail; 3969 $lines[] = 'ERR: ' . ( ( $res['record']['name'] ?? $rid ) ) . ' ' . ( is_array( $res ) && ! empty( $res['error'] ) ? $res['error'] : '' ); 3970 } 3971 } 3972 $after = $this->fetch_dns_records( array( 'A', 'AAAA', 'CNAME' ), $log ); 3973 if ( ! empty( $after ) ) { 3974 $this->persist_dns_cache( $after ); 3975 } 4509 $report = $this->apply_proxy_to_records( $sel, false, $log ); 4510 $ok = $report['ok']; 4511 $fail = $report['failed']; 4512 $lines = $report['lines']; 3976 4513 3977 4514 $s2 = $this->get_settings(); … … 4024 4561 } 4025 4562 4026 $before = $this->fetch_dns_records( array( 'A', 'AAAA', 'CNAME' ), $log ); 4027 if ( ! empty( $before ) ) { 4028 $this->persist_dns_cache( $before ); 4029 } 4030 $ok = 0; 4031 $fail = 0; 4032 $lines = array(); 4033 foreach ( $sel as $rid ) { 4034 $res = $this->update_record_proxy_status( $rid, true, true ); 4035 if ( is_array( $res ) && ! empty( $res['success'] ) ) { 4036 if ( ! empty( $res['skipped'] ) ) { 4037 $lines[] = 'SKIP: ' . ( $res['record']['name'] ?? $rid ) . ' (ya ON)'; 4038 } else { 4039 ++$ok; 4040 $lines[] = 'OK: ' . ( $res['record']['name'] ?? $rid ) . ' -> ON'; } 4041 } else { 4042 ++$fail; 4043 $lines[] = 'ERR: ' . ( ( $res['record']['name'] ?? $rid ) ) . ' ' . ( is_array( $res ) && ! empty( $res['error'] ) ? $res['error'] : '' ); 4044 } 4045 } 4046 $after = $this->fetch_dns_records( array( 'A', 'AAAA', 'CNAME' ), $log ); 4047 if ( ! empty( $after ) ) { 4048 $this->persist_dns_cache( $after ); 4049 } 4563 $report = $this->apply_proxy_to_records( $sel, true, $log ); 4564 $ok = $report['ok']; 4565 $fail = $report['failed']; 4566 $lines = $report['lines']; 4050 4567 4051 4568 $s2 = $this->get_settings(); -
es-football-bypass-for-cloudflare/trunk/readme.txt
r3707647 r3783334 2 2 Contributors: dcarrero 3 3 Tags: cloudflare, dns, football, bypass, ip-blocking 4 Requires at least: 5.04 Requires at least: 6.9 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 1.9.87 Stable tag: 2.0.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 178 178 In the Operation tab, click "WP-Cron Diagnostics" to see the next execution and the result of the last check. You can also review the integrated logs. 179 179 180 == Abilities API == 181 182 Since 2.0.0 the plugin registers five abilities through the WordPress Abilities API (WordPress 6.9 and newer), under the `es-football-bypass` category. This lets the REST API, MCP servers and AI agents read the block status and operate the bypass without going through the admin screens. 183 184 Every ability requires the `manage_options` capability, declares JSON Schemas for its input and output, and carries readonly/destructive/idempotent annotations so clients know what is safe to call. 185 186 = HTTP methods over REST = 187 WordPress derives the HTTP method from each ability's annotations, so REST clients must use the right verb (MCP clients call abilities by name and are unaffected): 188 189 * `get-status` and `list-dns-records` are `readonly`, so they are served over **GET**, with input as query parameters (`?input[type]=A`). 190 * `set-managed-records` and `run-check` are served over **POST**, with input in the JSON body (`{"input": {...}}`). 191 * `set-proxy` is annotated `destructive`, so it is served over **DELETE**. Pass its input as query parameters (`?input[mode]=off`): a JSON body is not read on DELETE requests. 192 193 = es-football-bypass/get-status = 194 Read-only. Returns whether La Liga IP blocks are active right now, whether this site resolves to a blocked IP, whether the bypass is engaged, the resolved IPs, how many records are managed, and the last check and feed timestamps. 195 196 = es-football-bypass/list-dns-records = 197 Read-only. Returns the cached DNS records with their proxy state and whether the plugin manages each one. Optional input: `type` (`A`, `AAAA` or `CNAME`) and `managed_only`. 198 199 = es-football-bypass/set-managed-records = 200 Replaces the list of records the plugin is allowed to switch. Input: `record_ids` (array of Cloudflare record IDs). IDs that do not exist in the zone are returned in `ignored_ids`. Pass an empty array to stop the plugin from touching DNS at all. 201 202 = es-football-bypass/run-check = 203 Runs the same check as the cron job: fetches the hayahora.futbol feed and applies the bypass policy, which may switch the managed records. 204 205 = es-football-bypass/set-proxy = 206 Forces the managed records to Proxied (CDN) or DNS Only. Input: `mode` (`on` or `off`). Annotated as destructive, since it changes live DNS at Cloudflare. Returns an error when no records are managed or Cloudflare credentials are missing. 207 180 208 == Logs and Auditing == 181 209 … … 191 219 == Screenshots == 192 220 193 1. Settings page: select Cloudflare authentication type (Global API Key, User Token or Account Token), enter credentials, configure check interval, cooldown after disabling Cloudflare, the "Force Proxy OFF during football" override, log retention, and the external cron token. 194 2. Operation page: live block status from hayahora.futbol, cached DNS records with current Proxied state per record, manual controls (save selection, reload from Cloudflare, force Proxy ON/OFF, manual check, WP-Cron diagnostics), and a sidebar with related links. 221 1. Settings page: Simple or Advanced interface mode, Cloudflare authentication type and credentials, server outbound IP, bypass behaviour (check interval, cooldown, "Force Proxy OFF during football" override, minimum ISPs with blocks), email notifications and uninstall options. 222 2. Operation page: live block status from hayahora.futbol and the DNS records the plugin manages, with type filters, quick selection buttons and the current proxy state per record, plus manual actions (check for blocks, force Proxy OFF or ON). 223 3. "Is there football now?" page: a quick yes or no answer on whether La Liga IP blocks are active, with the time of the last feed update and the affected ISPs. 195 224 196 225 == Changelog == 226 227 = 2.0.0 = 228 * NEW: Abilities API support (WordPress 6.9+). The plugin now registers five abilities under the "ES Football Bypass" category, so the REST API, MCP servers and AI agents can drive the bypass without going through the admin screens: `get-status`, `list-dns-records`, `set-managed-records`, `run-check` and `set-proxy` 229 * NEW: `es-football-bypass/get-status` — whether La Liga IP blocks are active, whether this site resolves to a blocked IP, and whether the bypass is currently engaged 230 * NEW: `es-football-bypass/list-dns-records` — the cached DNS records with their proxy state and whether the plugin manages each one, filterable by record type or to managed records only 231 * NEW: `es-football-bypass/set-managed-records` — replaces the list of records the plugin may switch; IDs missing from the Cloudflare zone are reported back as ignored 232 * NEW: `es-football-bypass/run-check` — runs the same check as the cron job and applies the bypass policy 233 * NEW: `es-football-bypass/set-proxy` — forces the managed records to Proxied (CDN) or DNS Only. Annotated as destructive so clients ask for confirmation, and it returns a clear error when no records are managed or Cloudflare is not configured 234 * All abilities require the `manage_options` capability, declare JSON Schemas for input and output, and carry readonly/destructive/idempotent annotations so agents know what is safe to call 235 * NEW: Filters `cfbcolorvivo_site_domain` and `cfbcolorvivo_is_local_domain`, so a staging copy, a reverse-proxied install or a WordPress Playground preview can unlock the Operation page and be checked against the production domain 236 * FIX: The "Unsaved changes" warning was visible on page load before touching anything — the stylesheet was overriding the `hidden` attribute 237 * FIX: PHP memory exhaustion while processing the status feed. The feed (about 5 MB, 50 MB once decoded) was decoded up to three times in a row and every copy was kept in memory until the end; a fourth decode on the Operation page only read a timestamp. It is now decoded once, so peak memory falls to roughly a third. Thanks to joserikko for the report 238 * FIX: After an automatic check the cached DNS records kept the proxy state from before the change, so the Operation page and `list-dns-records` could report the opposite of what was live at Cloudflare until the next refresh 239 * FIX: Uninstalling now removes the logs folder completely; a hidden `.htaccess` file was being left behind 240 * COMPAT: Minimum WordPress version raised to 6.9, which is where the Abilities API landed. Sites on older versions stay on 1.9.8, which contains the DNS record selection fix 241 * INTERNAL: The "Force Proxy ON/OFF" logic is now shared between the admin AJAX actions and the abilities, so both follow exactly the same path against Cloudflare 197 242 198 243 = 1.9.8 = … … 338 383 == Upgrade Notice == 339 384 385 = 2.0.0 = 386 Adds Abilities API support (WordPress 6.9+): five abilities let the REST API, MCP servers and AI agents read the block status and control the Cloudflare proxy. Minimum WordPress version is now 6.9. 387 340 388 = 1.9.8 = 341 389 Important fix: unchecking DNS records did nothing, so manual actions were applied to every record in the zone. Adds a "Save selection" button, record type filters (Only A / A + AAAA) and clearer feedback. Review your selected records after updating. … … 388 436 == Server Requirements == 389 437 390 - WordPress 5.0or higher438 - WordPress 6.9 or higher 391 439 - PHP 7.4 or higher 392 440 - PHP Extensions: curl, json -
es-football-bypass-for-cloudflare/trunk/uninstall.php
r3602202 r3783334 63 63 } 64 64 } 65 // glob() no lista los ficheros ocultos: sin esto el .htaccess impide borrar el directorio. 66 wp_delete_file( $cfbcolorvivo_dir . '/.htaccess' ); 65 67 $wp_filesystem->rmdir( $cfbcolorvivo_dir ); 66 68 } … … 82 84 } 83 85 } 86 // glob() no lista los ficheros ocultos: sin esto el .htaccess impide borrar el directorio. 87 wp_delete_file( $cfbcolorvivo_dir . '/.htaccess' ); 84 88 $wp_filesystem->rmdir( $cfbcolorvivo_dir ); 85 89 }
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)