rest_url( 'ago-legal/v1' ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'settings' => self::get_settings(), ] ); } /* ───── Frontend Assets ───── */ public function frontend_assets(): void { $settings = self::get_settings(); if ( empty( $settings['banner_enabled'] ) ) { // Expose the consent API for downstream scripts even without the banner. wp_enqueue_script( 'agolegal-consent-api', AGOLEGAL_URL . 'assets/js/consent-api.js', [], AGOLEGAL_VERSION, false ); wp_localize_script( 'agolegal-consent-api', 'agolegalConfig', [ 'enabled' => false, 'categories' => [], 'gaMeasurementId' => '', ] ); return; } wp_enqueue_style( 'agolegal-banner', AGOLEGAL_URL . 'assets/css/banner.css', [], AGOLEGAL_VERSION ); wp_enqueue_script( 'agolegal-consent-api', AGOLEGAL_URL . 'assets/js/consent-api.js', [], AGOLEGAL_VERSION, false ); wp_enqueue_script( 'agolegal-banner', AGOLEGAL_URL . 'assets/js/banner.js', [ 'agolegal-consent-api' ], AGOLEGAL_VERSION, true ); $i18n = [ 'title' => $settings['banner_title'] ?: __( 'We use cookies', 'ago-legal' ), 'message' => $settings['banner_message'] ?: __( 'This website uses cookies to ensure you get the best experience.', 'ago-legal' ), 'acceptAll' => __( 'Accept All', 'ago-legal' ), 'rejectAll' => __( 'Reject All', 'ago-legal' ), 'policyLink' => $settings['cookie_policy_url'] ?: '', 'policyText' => __( 'Cookie Policy', 'ago-legal' ), 'customize' => __( 'Customize', 'ago-legal' ), 'save' => __( 'Save Preferences', 'ago-legal' ), 'necessary' => __( 'Necessary', 'ago-legal' ), 'analytics' => __( 'Analytics', 'ago-legal' ), 'marketing' => __( 'Marketing', 'ago-legal' ), 'functional' => __( 'Functional', 'ago-legal' ), 'necessaryDesc' => __( 'Essential for the website to function.', 'ago-legal' ), 'analyticsDesc' => __( 'Help us understand how visitors use the site.', 'ago-legal' ), 'marketingDesc' => __( 'Used for targeted advertising.', 'ago-legal' ), 'functionalDesc' => __( 'Enable enhanced features and personalization.', 'ago-legal' ), ]; $categories = [ 'necessary' => true, 'analytics' => ! empty( $settings['cat_analytics'] ), 'marketing' => ! empty( $settings['cat_marketing'] ), 'functional' => ! empty( $settings['cat_functional'] ), ]; wp_localize_script( 'agolegal-consent-api', 'agolegalConfig', [ 'enabled' => true, 'categories' => $categories, 'gaMeasurementId' => $settings['ga_measurement_id'] ?? '', 'restUrl' => rest_url( 'ago-legal/v1/consent' ), 'nonce' => wp_create_nonce( 'wp_rest' ), ] ); wp_localize_script( 'agolegal-banner', 'agolegalBanner', [ 'i18n' => $i18n, 'position' => $settings['banner_position'] ?? 'bottom', 'style' => $settings['banner_style'] ?? 'bar', ] ); // Floating "cookie preferences" button behaviour. Runs in the footer where the // button markup already exists in the DOM (rendered at wp_footer priority 5). $prefs_js = "(function(){var b=document.getElementById('ago-cookie-prefs-btn');if(!b)return;" . "if(localStorage.getItem('ago_cookie_consent')){b.style.display='block';}" . "b.addEventListener('click',function(){localStorage.removeItem('ago_cookie_consent');b.style.display='none';location.reload();});})();"; wp_add_inline_script( 'agolegal-banner', $prefs_js ); } public function render_banner(): void { $settings = self::get_settings(); if ( empty( $settings['banner_enabled'] ) ) return; echo ''; } public function render_floating_btn(): void { $settings = self::get_settings(); if ( empty( $settings['banner_enabled'] ) ) return; printf( '', esc_attr__( 'Cookie Preferences', 'ago-legal' ) ); } /* ───── Shortcodes ───── */ public function shortcode_privacy(): string { return PolicyGenerator::privacy_policy( self::get_settings() ); } public function shortcode_cookies(): string { return PolicyGenerator::cookie_policy( self::get_settings() ); } /* ───── REST API ───── */ public function register_routes(): void { $ns = 'ago-legal/v1'; register_rest_route( $ns, '/settings', [ [ 'methods' => 'GET', 'callback' => [ $this, 'rest_get_settings' ], 'permission_callback' => [ $this, 'can_manage' ] ], [ 'methods' => 'POST', 'callback' => [ $this, 'rest_save_settings' ], 'permission_callback' => [ $this, 'can_manage' ] ], ] ); /* Public by design: the cookie banner serves anonymous visitors, so these * endpoints cannot require authentication. They only record the visitor's * own consent choice into this site's database. Input is sanitized. */ register_rest_route( $ns, '/consent', [ 'methods' => 'POST', 'callback' => [ $this, 'rest_save_consent' ], 'permission_callback' => '__return_true', ] ); register_rest_route( $ns, '/update-consent', [ 'methods' => 'POST', 'callback' => [ $this, 'rest_save_consent' ], 'permission_callback' => '__return_true', ] ); register_rest_route( $ns, '/consents', [ 'methods' => 'GET', 'callback' => [ $this, 'rest_get_consents' ], 'permission_callback' => [ $this, 'can_manage' ], ] ); register_rest_route( $ns, '/cookie-scan', [ 'methods' => 'POST', 'callback' => [ $this, 'rest_cookie_scan' ], 'permission_callback' => [ $this, 'can_manage' ], ] ); register_rest_route( $ns, '/rat', [ [ 'methods' => 'GET', 'callback' => [ $this, 'rest_rat_list' ], 'permission_callback' => [ $this, 'can_manage' ] ], [ 'methods' => 'POST', 'callback' => [ $this, 'rest_rat_save' ], 'permission_callback' => [ $this, 'can_manage' ] ], ] ); register_rest_route( $ns, '/rat/(?P\d+)', [ 'methods' => 'DELETE', 'callback' => [ $this, 'rest_rat_delete' ], 'permission_callback' => [ $this, 'can_manage' ], 'args' => [ 'id' => [ 'validate_callback' => static fn( $p ) => is_numeric( $p ) ] ], ] ); } public function can_manage(): bool { return current_user_can( 'manage_options' ); } /* ── REST: Settings ── */ public function rest_get_settings(): \WP_REST_Response { return new \WP_REST_Response( [ 'settings' => self::get_settings() ] ); } public function rest_save_settings( \WP_REST_Request $request ): \WP_REST_Response { $data = $request->get_json_params(); $settings = self::sanitize_settings( $data ); update_option( 'agolegal_settings', $settings ); return new \WP_REST_Response( [ 'saved' => true, 'settings' => $settings ] ); } /* ── REST: Consent (public) ── */ public function rest_save_consent( \WP_REST_Request $request ): \WP_REST_Response { $data = $request->get_json_params(); $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '0.0.0.0'; $ua = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; Consent::insert( [ 'necessary' => true, 'analytics' => ! empty( $data['analytics'] ), 'marketing' => ! empty( $data['marketing'] ), 'functional' => ! empty( $data['functional'] ), ], $ip, $ua ); return new \WP_REST_Response( [ 'ok' => true ] ); } /* ── REST: Consents (view) ── */ public function rest_get_consents( \WP_REST_Request $request ): \WP_REST_Response { $page = (int) ( $request->get_param( 'page' ) ?: 1 ); return new \WP_REST_Response( Consent::get_list( $page, 20 ) ); } /* ── REST: Cookie Scan ── */ public function rest_cookie_scan( \WP_REST_Request $request ): \WP_REST_Response { $params = $request->get_json_params(); $url = esc_url_raw( $params['url'] ?? '' ); $persist = ! empty( $params['persist'] ); $scan = CookieScanner::scan( $url ); if ( $persist && ! empty( $scan['ok'] ) ) { CookieScanner::persist_as_cookie_table( $scan ); } return new \WP_REST_Response( $scan ); } /* ── REST: Processing Records (CRUD) ── */ public function rest_rat_list(): \WP_REST_Response { return new \WP_REST_Response( [ 'items' => Rat::all() ] ); } public function rest_rat_save( \WP_REST_Request $request ): \WP_REST_Response { $data = (array) $request->get_json_params(); $id = (int) ( $data['id'] ?? 0 ); $saved = Rat::upsert( $data, $id ); return new \WP_REST_Response( [ 'saved' => true, 'id' => $saved ] ); } public function rest_rat_delete( \WP_REST_Request $request ): \WP_REST_Response { Rat::delete( (int) $request['id'] ); return new \WP_REST_Response( [ 'deleted' => true ] ); } /* ── Exports (CSV / audit HTML) via admin-post download ── */ public function handle_export(): void { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to export this data.', 'ago-legal' ) ); } check_admin_referer( 'agolegal_export' ); $type = isset( $_GET['type'] ) ? sanitize_key( wp_unslash( $_GET['type'] ) ) : ''; $stamp = gmdate( 'Y-m-d' ); switch ( $type ) { case 'consents_csv': $this->send_download( 'text/csv', "consent-log-{$stamp}.csv", Consent::export_csv() ); break; case 'consents_html': $this->send_download( 'text/html', "consent-log-audit-{$stamp}.html", Consent::export_html_report() ); break; case 'rat_csv': $this->send_download( 'text/csv', "processing-records-{$stamp}.csv", Rat::export_csv() ); break; default: wp_die( esc_html__( 'Unknown export type.', 'ago-legal' ) ); } } private function send_download( string $mime, string $filename, string $body ): void { nocache_headers(); header( 'Content-Type: ' . $mime . '; charset=utf-8' ); header( 'Content-Disposition: attachment; filename="' . $filename . '"' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Raw file download (CSV/standalone HTML report), not HTML page output. echo $body; exit; } /* ───── Settings ───── */ public static function get_settings(): array { $defaults = [ 'banner_enabled' => false, 'banner_position' => 'bottom', 'banner_style' => 'bar', 'banner_title' => '', 'banner_message' => '', 'banner_bg_color' => '#1d2327', 'banner_text_color' => '#ffffff', 'banner_btn_color' => '#2271b1', 'cat_analytics' => true, 'cat_marketing' => true, 'cat_functional' => true, 'blocked_scripts' => '', 'ga_measurement_id' => '', 'company_name' => get_bloginfo( 'name' ), 'company_email' => get_option( 'admin_email' ), 'company_address' => '', 'dpo_name' => '', 'dpo_email' => '', 'dpo_phone' => '', 'data_retention' => '12', 'cookie_policy_url' => '', 'privacy_policy_url'=> '', 'cookie_table' => '[]', 'compliance_mode' => 'global', ]; return wp_parse_args( get_option( 'agolegal_settings', [] ), $defaults ); } private static function sanitize_settings( array $d ): array { return [ 'banner_enabled' => ! empty( $d['banner_enabled'] ), 'banner_position' => in_array( $d['banner_position'] ?? '', [ 'top', 'bottom' ], true ) ? $d['banner_position'] : 'bottom', 'banner_style' => in_array( $d['banner_style'] ?? '', [ 'bar', 'modal', 'floating' ], true ) ? $d['banner_style'] : 'bar', 'banner_title' => sanitize_text_field( $d['banner_title'] ?? '' ), 'banner_message' => sanitize_textarea_field( $d['banner_message'] ?? '' ), 'banner_bg_color' => sanitize_hex_color( $d['banner_bg_color'] ?? '#1d2327' ) ?: '#1d2327', 'banner_text_color' => sanitize_hex_color( $d['banner_text_color'] ?? '#ffffff' ) ?: '#ffffff', 'banner_btn_color' => sanitize_hex_color( $d['banner_btn_color'] ?? '#2271b1' ) ?: '#2271b1', 'cat_analytics' => ! empty( $d['cat_analytics'] ), 'cat_marketing' => ! empty( $d['cat_marketing'] ), 'cat_functional' => ! empty( $d['cat_functional'] ), 'blocked_scripts' => sanitize_textarea_field( $d['blocked_scripts'] ?? '' ), 'ga_measurement_id' => sanitize_text_field( $d['ga_measurement_id'] ?? '' ), 'company_name' => sanitize_text_field( $d['company_name'] ?? '' ), 'company_email' => sanitize_email( $d['company_email'] ?? '' ), 'company_address' => sanitize_text_field( $d['company_address'] ?? '' ), 'dpo_name' => sanitize_text_field( $d['dpo_name'] ?? '' ), 'dpo_email' => sanitize_email( $d['dpo_email'] ?? '' ), 'dpo_phone' => sanitize_text_field( $d['dpo_phone'] ?? '' ), 'data_retention' => max( 1, (int) ( $d['data_retention'] ?? 12 ) ), 'cookie_policy_url' => esc_url_raw( $d['cookie_policy_url'] ?? '' ), 'privacy_policy_url'=> esc_url_raw( $d['privacy_policy_url'] ?? '' ), 'cookie_table' => sanitize_textarea_field( $d['cookie_table'] ?? '[]' ), 'compliance_mode' => in_array( $d['compliance_mode'] ?? '', [ 'global', 'gdpr', 'chile', 'lgpd', 'custom' ], true ) ? $d['compliance_mode'] : 'global', ]; } }