IZINI_PAGE, 'tab' => $tab ), $args ), admin_url( 'admin.php' ) ); } /* -------------------------------------------------------------- menu */ /** * The Izini mark for the admin menu: "IZ" cut out of a rounded square, as an * SVG WordPress colours like its own menu icons. */ function izini_menu_icon() { $svg = ''; return 'data:image/svg+xml;base64,' . base64_encode( $svg ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- an inline SVG icon, as WordPress documents for add_menu_page } /** The Izini look, only on the Izini page. */ function izini_admin_assets( $hook ) { if ( 'toplevel_page_' . IZINI_PAGE !== $hook ) { return; } wp_enqueue_style( 'izini-admin', IZINI_PLUGIN_URL . 'assets/admin.css', array(), IZINI_VERSION ); wp_enqueue_script( 'izini-admin', IZINI_PLUGIN_URL . 'assets/admin.js', array(), IZINI_VERSION, true ); } add_action( 'admin_enqueue_scripts', 'izini_admin_assets' ); function izini_add_menu() { add_menu_page( __( 'Izini', 'izini' ), __( 'Izini', 'izini' ), 'manage_options', IZINI_PAGE, 'izini_render_page', izini_menu_icon(), 80 ); } add_action( 'admin_menu', 'izini_add_menu' ); /** Settings › Izini (plugin 1.0) now opens the Izini menu. */ function izini_redirect_old_page() { global $pagenow; // Read-only: only sends the administrator on to the Izini page, passing // a Connect with Izini return along. Nothing is saved here; that return is // checked on the Izini page against its one-time state. // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( 'options-general.php' === $pagenow && isset( $_GET['page'] ) && IZINI_PAGE === $_GET['page'] && current_user_can( 'manage_options' ) ) { $args = array(); foreach ( array( 'izini_code', 'izini_state' ) as $k ) { if ( isset( $_GET[ $k ] ) ) { $args[ $k ] = sanitize_text_field( wp_unslash( $_GET[ $k ] ) ); } } wp_safe_redirect( izini_admin_url( 'overview', $args ) ); exit; } // phpcs:enable WordPress.Security.NonceVerification.Recommended } add_action( 'admin_init', 'izini_redirect_old_page' ); function izini_settings_link( $links ) { array_unshift( $links, sprintf( '%s', esc_url( izini_admin_url() ), esc_html__( 'Settings', 'izini' ) ) ); return $links; } add_filter( 'plugin_action_links_' . plugin_basename( IZINI_PLUGIN_FILE ), 'izini_settings_link' ); /** After activation, and until a project is connected: where to go next. */ function izini_admin_notice() { if ( ! current_user_can( 'manage_options' ) || '' !== izini_project_code() ) { return; } $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( $screen && 'toplevel_page_' . IZINI_PAGE === $screen->id ) { return; } $just = get_transient( 'izini_just_activated' ); if ( ! $just && ( ! $screen || ! in_array( $screen->id, array( 'dashboard', 'plugins' ), true ) ) ) { return; } delete_transient( 'izini_just_activated' ); printf( '
', esc_url( IZINI_PLUGIN_URL . 'assets/izini-logo.png' ), esc_html__( 'Izini is active.', 'izini' ), esc_html__( 'Connect it to your Izini project to show the consent banner.', 'izini' ), esc_url( izini_admin_url() ), esc_html__( 'Connect with Izini', 'izini' ) ); } add_action( 'admin_notices', 'izini_admin_notice' ); /* --------------------------------------------------------- settings */ function izini_sanitize_code( $value ) { $value = is_string( $value ) ? trim( $value ) : ''; if ( '' === $value ) { delete_option( 'izini_project_meta' ); return ''; } if ( ! izini_is_valid_code( $value ) ) { add_settings_error( IZINI_OPTION, 'izini_invalid', __( 'A project code has only letters, numbers, "_" and "-". Copy it from Izini: Consent › Installation.', 'izini' ) ); return izini_project_code(); } return $value; } function izini_sanitize_bool( $value ) { return $value ? '1' : '0'; } function izini_register_settings() { register_setting( 'izini', IZINI_OPTION, array( 'type' => 'string', 'sanitize_callback' => 'izini_sanitize_code', 'default' => '' ) ); register_setting( 'izini_advanced', 'izini_consent_defaults', array( 'type' => 'string', 'sanitize_callback' => 'izini_sanitize_bool', 'default' => '1' ) ); } add_action( 'admin_init', 'izini_register_settings' ); /** After a project code is saved, go back to the Izini page (not Settings). */ function izini_after_save( $location ) { if ( ! isset( $_POST['option_page'], $_POST['_wpnonce'] ) ) { return $location; } $page = sanitize_key( wp_unslash( $_POST['option_page'] ) ); // The nonce options.php has already checked before saving. if ( ! in_array( $page, array( 'izini', 'izini_advanced' ), true ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), $page . '-options' ) ) { return $location; } return add_query_arg( 'settings-updated', 'true', izini_admin_url( 'izini_advanced' === $page ? 'advanced' : 'overview' ) ); } add_filter( 'wp_redirect', 'izini_after_save' ); /* ------------------------------------------------------- the project */ /** * Asks Izini for the project behind a code (its name, id and categories) and * keeps the name and id for the Overview. Only when an administrator connects * or checks (WordPress.org guideline 7). * * @return array{ok: bool, message: string} */ function izini_check_code( $code ) { $response = wp_remote_get( IZINI_CDN . '/' . rawurlencode( $code ) . '/config?language=en', array( 'timeout' => 10 ) ); if ( is_wp_error( $response ) ) { return array( 'ok' => false, 'message' => __( 'Izini could not be reached from this site. Try again in a minute.', 'izini' ) ); } $status = wp_remote_retrieve_response_code( $response ); if ( 404 === $status ) { return array( 'ok' => false, 'message' => __( 'Izini has no project with this code. Copy it from Izini: Consent › Installation.', 'izini' ) ); } $body = json_decode( wp_remote_retrieve_body( $response ), true ); $name = isset( $body['project']['name'] ) && is_string( $body['project']['name'] ) ? $body['project']['name'] : ''; if ( 200 !== $status || '' === $name ) { return array( 'ok' => false, 'message' => __( 'Izini answered, but not as expected. Try again in a minute.', 'izini' ) ); } $categories = array(); if ( isset( $body['categories'] ) && is_array( $body['categories'] ) ) { foreach ( $body['categories'] as $c ) { if ( isset( $c['id'], $c['name'] ) && izini_is_valid_category( $c['id'] ) ) { $categories[] = array( 'id' => $c['id'], 'name' => (string) $c['name'], 'necessary' => ! empty( $c['necessary'] ) ); } } } update_option( 'izini_project_meta', array( 'name' => $name, 'id' => isset( $body['project']['id'] ) && is_string( $body['project']['id'] ) ? $body['project']['id'] : '', 'categories' => $categories, 'checked_at' => time(), ), false ); /* translators: %s: the Izini project name */ return array( 'ok' => true, 'message' => sprintf( __( 'Connected to the Izini project "%s". The banner shows on every page.', 'izini' ), $name ) ); } function izini_project_meta() { $meta = get_option( 'izini_project_meta', array() ); return is_array( $meta ) ? $meta : array(); } /** A link into the Izini dashboard, opened on this project. */ function izini_app_link( $path ) { $meta = izini_project_meta(); $url = IZINI_APP . $path; return ! empty( $meta['id'] ) ? add_query_arg( 'project', rawurlencode( $meta['id'] ), $url ) : $url; } /* ----------------------------------------------- Connect with Izini */ function izini_connect_state_key() { return 'izini_connect_state_' . get_current_user_id(); } /** * The Connect with Izini link: a one-time state for the browser's return and * a challenge for the site token, whose verifier stays on this server. */ function izini_connect_url() { $state = wp_generate_password( 32, false, false ); $pkce = izini_pkce_pair(); set_transient( izini_connect_state_key(), array( 'state' => $state, 'verifier' => $pkce['verifier'], ), 20 * MINUTE_IN_SECONDS ); return add_query_arg( array( 'site' => rawurlencode( home_url( '/' ) ), 'return' => rawurlencode( add_query_arg( 'page', IZINI_PAGE, admin_url( 'admin.php' ) ) ), 'state' => $state, 'challenge' => $pkce['challenge'], ), IZINI_CONNECT_URL ); } /** * The trip back from Izini. Saves the code only when the state matches the * one this administrator was given, and Izini knows the project. * * @return array{ok: bool, message: string}|null */ function izini_handle_connect_return() { // No WordPress nonce: the return comes from app.izini.ng, which cannot // make one. It is protected instead by izini_state, a random one-time // value given to this administrator only, valid for 20 minutes, compared // with hash_equals() below and deleted on first use (OAuth-style state). // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( ! isset( $_GET['izini_code'], $_GET['izini_state'] ) ) { return null; } $code = sanitize_text_field( wp_unslash( $_GET['izini_code'] ) ); $state = sanitize_text_field( wp_unslash( $_GET['izini_state'] ) ); $grant = isset( $_GET['izini_grant'] ) ? sanitize_text_field( wp_unslash( $_GET['izini_grant'] ) ) : ''; // phpcs:enable WordPress.Security.NonceVerification.Recommended $pending = get_transient( izini_connect_state_key() ); delete_transient( izini_connect_state_key() ); $expected = is_array( $pending ) && isset( $pending['state'] ) ? (string) $pending['state'] : ''; if ( '' === $expected || ! hash_equals( $expected, $state ) ) { return array( 'ok' => false, 'message' => __( 'This connection did not start from this site, or it took too long. Press "Connect with Izini" again.', 'izini' ) ); } if ( ! izini_is_valid_code( $code ) ) { return array( 'ok' => false, 'message' => __( 'Izini sent back a code this plugin cannot use. Press "Connect with Izini" again.', 'izini' ) ); } $check = izini_check_code( $code ); if ( ! $check['ok'] ) { return $check; } // Another project's token, if any, is revoked here (izini_on_code_change). update_option( IZINI_OPTION, $code ); if ( 1 === preg_match( '/^[A-Za-z0-9_-]{43}$/', $grant ) && ! empty( $pending['verifier'] ) ) { $token = izini_exchange_grant( $grant, $pending['verifier'] ); $check['message'] .= ' ' . $token['message']; } elseif ( '' === izini_site_token() ) { $check['message'] .= ' ' . __( 'Statistics and cookie scans in WordPress need an Izini admin, owner or Privacy Engineer to connect the site.', 'izini' ); } return $check; } /* ------------------------------------------------------- script rules */ /** Adding and removing script rules (Scripts tab), with a nonce. */ function izini_handle_rules_post() { if ( ! isset( $_POST['izini_rules_action'] ) || ! current_user_can( 'manage_options' ) ) { return null; } check_admin_referer( 'izini_rules' ); $rules = izini_script_rules(); $action = sanitize_key( wp_unslash( $_POST['izini_rules_action'] ) ); if ( 'add' === $action ) { $kind = isset( $_POST['kind'] ) && 'inline' === $_POST['kind'] ? 'inline' : 'src'; $match = isset( $_POST['match'] ) ? trim( sanitize_text_field( wp_unslash( $_POST['match'] ) ) ) : ''; $category = isset( $_POST['category'] ) ? sanitize_text_field( wp_unslash( $_POST['category'] ) ) : ''; if ( '' === $category ) { return array( 'ok' => true, 'message' => __( 'Nothing changed: no category chosen.', 'izini' ) ); } if ( strlen( $match ) < 4 || ! izini_is_valid_category( $category ) ) { return array( 'ok' => false, 'message' => __( 'Give at least 4 characters to match and a category.', 'izini' ) ); } $rules = array_values( array_filter( $rules, function ( $r ) use ( $kind, $match ) { return ! ( $r['kind'] === $kind && $r['match'] === $match ); } ) ); $rules[] = array( 'kind' => $kind, 'match' => $match, 'category' => $category ); } elseif ( 'remove' === $action && isset( $_POST['index'] ) ) { unset( $rules[ absint( $_POST['index'] ) ] ); $rules = array_values( $rules ); } update_option( IZINI_RULES_OPTION, $rules, false ); izini_sync_scripts( array(), true ); return array( 'ok' => true, 'message' => __( 'Saved. The change shows on the site\'s next page view.', 'izini' ) ); } /* -------------------------------------------------------------- page */ function izini_render_page() { if ( ! current_user_can( 'manage_options' ) ) { return; } $notice = izini_handle_connect_return(); $tabs = array( 'overview' => __( 'Overview', 'izini' ), 'scripts' => __( 'Scripts', 'izini' ), 'privacy' => __( 'Privacy link', 'izini' ), 'advanced' => __( 'Advanced', 'izini' ), ); $tab = isset( $_GET['tab'], $tabs[ $_GET['tab'] ] ) ? sanitize_key( $_GET['tab'] ) : 'overview'; if ( ! $notice && 'scripts' === $tab ) { $notice = izini_handle_rules_post(); } if ( ! $notice && isset( $_GET['izini_check'] ) && '' !== izini_project_code() ) { check_admin_referer( 'izini_check' ); $notice = izini_check_code( izini_project_code() ); } if ( ! $notice && isset( $_GET['izini_site_scan'] ) && '' !== izini_site_token() ) { check_admin_referer( 'izini_site_scan' ); $notice = izini_start_site_scan(); } if ( ! $notice && isset( $_GET['izini_site_off'] ) ) { check_admin_referer( 'izini_site_off' ); izini_revoke_site_token(); $notice = array( 'ok' => true, 'message' => __( 'Statistics and scans are off for this site, and its token is revoked in Izini. The banner keeps working.', 'izini' ), ); } $code = izini_project_code(); $meta = izini_project_meta(); ?>' . esc_html( $lead ) . '
'; } echo '' . esc_html__( 'Izini could not be reached just now. Try again in a minute.', 'izini' ) . '
'; } izini_card_close(); // A token Izini refused is forgotten by the call above; the next view offers to connect again. if ( '' === izini_site_token() ) { return; } $scan = izini_site_request( 'GET', 'scan' ); izini_card_open( __( 'Cookie scan', 'izini' ), __( 'Izini visits this site as a visitor who accepted everything and lists the cookies it sets. New cookies wait in Izini for your review.', 'izini' ), 'dashicons-search' ); if ( $scan && 200 === $scan['status'] ) { $latest = isset( $scan['body']['scan'] ) && is_array( $scan['body']['scan'] ) ? $scan['body']['scan'] : null; $cookies = isset( $scan['body']['cookies'] ) && is_array( $scan['body']['cookies'] ) ? $scan['body']['cookies'] : array(); if ( ! $latest ) { echo '' . esc_html__( 'This site has not been scanned yet.', 'izini' ) . '
'; } elseif ( 'failed' === $latest['status'] ) { echo '' . esc_html( __( 'The last scan failed:', 'izini' ) . ' ' . ( isset( $latest['error'] ) ? (string) $latest['error'] : '' ) ) . '
'; } elseif ( 'scanning' === $latest['status'] ) { echo '' . esc_html__( 'A scan is running. It takes a few minutes; reload this page to see the result.', 'izini' ) . '
'; } else { /* translators: 1: how long ago, 2: number of cookies */ echo '' . esc_html( sprintf( __( 'Last scanned %1$s ago: %2$s cookies seen.', 'izini' ), human_time_diff( strtotime( $latest['started_at'] ) ), number_format_i18n( isset( $latest['cookie_count'] ) ? (int) $latest['cookie_count'] : 0 ) ) ) . '
'; } if ( $cookies ) { $by = array(); foreach ( $cookies as $c ) { $cat = isset( $c['category'] ) ? (string) $c['category'] : ''; $by[ $cat ] = isset( $by[ $cat ] ) ? $by[ $cat ] + 1 : 1; } $meta = izini_project_meta(); $names = array(); foreach ( isset( $meta['categories'] ) && is_array( $meta['categories'] ) ? $meta['categories'] : array() as $cat ) { $names[ $cat['id'] ] = $cat['name']; } /* translators: %s: number of cookies */ echo '' . esc_html( sprintf( __( 'Your cookie list in Izini: %s cookies.', 'izini' ), number_format_i18n( count( $cookies ) ) ) ) . '
'; echo '' . esc_html__( 'Izini could not be reached just now. Try again in a minute.', 'izini' ) . '
'; } ?> true, 'message' => __( 'The scan has started. It takes a few minutes; reload this page to see the result.', 'izini' ), ); } $message = $result && isset( $result['body']['message'] ) && is_string( $result['body']['message'] ) ? $result['body']['message'] : __( 'Izini could not be reached just now. Try again in a minute.', 'izini' ); return array( 'ok' => false, 'message' => $message, ); } /** The fallback: paste the project code. */ function izini_render_code_form( $code ) { ?>' . esc_html__( 'On this version of WordPress the rules below are saved but every script runs straight away. Update WordPress to have them wait for consent. The banner itself works as it is.', 'izini' ) . '
' . esc_html__( 'No script is held back yet. Pick a category for a script below.', 'izini' ) . '
'; } else { ?> |
|
' . esc_html( $text ) . '';
}
/**
* A how-to that opens and closes: a title, numbered steps (each already
* escaped HTML) and an optional note.
*/
function izini_guide( $title, $steps, $open = false, $note = '' ) {
echo '' . wp_kses_post( $note ) . '
'; } echo '
data-izini-preferences #izini-preferences.<script type="text/plain" data-izini-category="analytics_storage">. ()
add_filter( 'izini_consent_mode_defaults', '__return_false' );