'autocomplete', self::ACTION_COMPANY => 'company' ) as $action => $method ) { add_action( 'wp_ajax_' . $action, array( __CLASS__, $method ) ); add_action( 'wp_ajax_nopriv_' . $action, array( __CLASS__, $method ) ); } } /** * Registers the widget and its configuration. Call it on the pages that show a company field. */ public static function enqueue_widget() { wp_register_script( 'jsonpage-autocomplete', JSONPAGE_URL . 'assets/js/jsonpage-autocomplete.js', array(), JSONPAGE_VERSION, true ); wp_register_style( 'jsonpage-autocomplete', JSONPAGE_URL . 'assets/css/jsonpage-autocomplete.css', array(), JSONPAGE_VERSION ); // Nonce localized to the script: the browser sends it with every request, the server checks it with check_ajax_referer(). $nonce = wp_create_nonce( self::NONCE ); $base = add_query_arg( array( '_ajax_nonce' => $nonce ), admin_url( 'admin-ajax.php' ) ); $config = array( 'autocompleteUrl' => $base . '&action=' . self::ACTION_AUTOCOMPLETE . '&q={q}&country={country}', 'companyUrl' => $base . '&action=' . self::ACTION_COMPANY . '&id={id}', 'nonce' => $nonce, 'countries' => jsonpage_allowed_countries(), 'lang' => substr( determine_locale(), 0, 2 ), 'enabled' => Jsonpage_Client::has_key() ? '1' : '', 'closedNotice' => __( 'This company is shown as closed in the register.', 'jsonpage' ), ); wp_localize_script( 'jsonpage-autocomplete', 'JsonpageConfig', $config ); wp_enqueue_style( 'jsonpage-autocomplete' ); wp_enqueue_script( 'jsonpage-autocomplete' ); } /** * At most 60 calls a minute per visitor. The address is only used as a hash for one minute. * * @return bool */ private static function within_rate_limit() { $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; $key = 'jsonpage_rl_' . md5( $ip . '|' . gmdate( 'YmdHi' ) ); $n = (int) get_transient( $key ); if ( $n >= self::RATE_PER_MINUTE ) { return false; } set_transient( $key, $n + 1, 2 * MINUTE_IN_SECONDS ); return true; } /** * Common checks, called after the nonce check. Stops the request with a JSON answer when something is wrong. */ private static function guard() { nocache_headers(); if ( ! Jsonpage_Client::has_key() ) { wp_send_json( array( 'data' => array(), 'error' => 'not_configured' ), 503 ); } if ( ! self::within_rate_limit() ) { wp_send_json( array( 'data' => array(), 'error' => 'rate_limited' ), 429 ); } } /** * Answer sent when the nonce localized to the widget script is missing or wrong. */ private static function forbidden() { nocache_headers(); wp_send_json( array( 'data' => array(), 'error' => 'forbidden' ), 403 ); } /** * GET admin-ajax.php?action=jsonpage_autocomplete&q=...&country=FR,BE * * Read only. The nonce is verified first, then the key and the rate limit. */ public static function autocomplete() { if ( ! check_ajax_referer( self::NONCE, '_ajax_nonce', false ) ) { self::forbidden(); } self::guard(); $q = isset( $_GET['q'] ) ? sanitize_text_field( wp_unslash( $_GET['q'] ) ) : ''; $q = function_exists( 'mb_substr' ) ? mb_substr( $q, 0, 80 ) : substr( $q, 0, 80 ); if ( strlen( trim( $q ) ) < 3 ) { wp_send_json( array( 'data' => array() ) ); } $asked = isset( $_GET['country'] ) ? array_filter( array_map( 'strtoupper', array_map( 'trim', explode( ',', sanitize_text_field( wp_unslash( $_GET['country'] ) ) ) ) ) ) : array(); $countries = array_values( array_intersect( jsonpage_allowed_countries(), $asked ) ); if ( empty( $countries ) ) { $countries = jsonpage_allowed_countries(); } $data = Jsonpage_Client::autocomplete( $q, $countries ); if ( null === $data ) { // Degraded mode: the field stays a normal text field. wp_send_json( array( 'data' => array(), 'error' => 'unavailable' ), 503 ); } wp_send_json( array( 'data' => $data ) ); } /** * GET admin-ajax.php?action=jsonpage_company&id=FR-552032534 */ public static function company() { if ( ! check_ajax_referer( self::NONCE, '_ajax_nonce', false ) ) { self::forbidden(); } self::guard(); $id = isset( $_GET['id'] ) ? sanitize_text_field( wp_unslash( $_GET['id'] ) ) : ''; $id = strtoupper( substr( $id, 0, 2 ) ) . substr( $id, 2 ); if ( ! preg_match( '/^[A-Z]{2}-[A-Za-z0-9]{3,20}$/', $id ) || ! in_array( substr( $id, 0, 2 ), jsonpage_allowed_countries(), true ) ) { wp_send_json( array( 'data' => null, 'error' => 'invalid_id' ), 400 ); } $data = Jsonpage_Client::company( $id ); if ( null === $data ) { wp_send_json( array( 'data' => null, 'error' => 'unavailable' ), 503 ); } wp_send_json( array( 'data' => $data ) ); } }