status(); return '' !== $this->key() && ! empty( $st['domain'] ); } /** * Host name of this site. * * @return string */ private function site_host() { return (string) wp_parse_url( home_url(), PHP_URL_HOST ); } /** * Request to the Adsovo plugin API. Only used after the user entered a plugin key. * * @param string $method HTTP method. * @param string $path Path below /api/plugin/. * @param array $body Request data. * @return array|WP_Error */ private function api( $method, $path, $body = array() ) { $url = $this->api_base() . '/api/plugin/' . ltrim( $path, '/' ); $body = array_merge( $body, array( 'site' => $this->site_host(), 'locale' => determine_locale(), ) ); $args = array( 'method' => $method, 'timeout' => 20, 'headers' => array( 'X-Adsovo-Key' => $this->key(), 'Accept' => 'application/json', 'User-Agent' => 'Adsovo-WordPress/' . ADSOVO_WP_VERSION . '; ' . home_url(), ), ); if ( 'GET' === $method ) { $url = add_query_arg( $body, $url ); } else { $args['headers']['Content-Type'] = 'application/json'; $args['body'] = wp_json_encode( $body ); } $res = wp_remote_request( $url, $args ); if ( is_wp_error( $res ) ) { /* translators: %s: technical error message */ return new WP_Error( 'adsovo_http', sprintf( __( 'Adsovo is currently not reachable: %s', 'adsovo' ), $res->get_error_message() ) ); } $code = (int) wp_remote_retrieve_response_code( $res ); $data = json_decode( wp_remote_retrieve_body( $res ), true ); if ( $code < 200 || $code >= 300 || ! is_array( $data ) || empty( $data['ok'] ) ) { if ( is_array( $data ) && ! empty( $data['error'] ) ) { $msg = (string) $data['error']; } else { /* translators: %d: HTTP status code */ $msg = sprintf( __( 'Unexpected response from Adsovo (HTTP %d).', 'adsovo' ), $code ); } return new WP_Error( 'adsovo_api', $msg ); } return $data; } /** * Store the status returned by the service. * * @param array $data Status data. */ private function store_status( array $data ) { $data['fetched'] = time(); update_option( self::OPT_STATUS, $data, false ); } /** * Redirect back to the settings page with a notice. * * @param string $type success, warning or error. * @param string $message Message text. */ private function redirect_back( $type, $message ) { set_transient( 'adsovo_notice_' . get_current_user_id(), array( $type, $message ), 120 ); wp_safe_redirect( admin_url( 'options-general.php?page=' . self::PAGE ) ); exit; } /** * Capability and nonce check for admin-post handlers. * * @param string $action Nonce action. */ private function guard( $action ) { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You are not allowed to do this.', 'adsovo' ), '', array( 'response' => 403 ) ); } check_admin_referer( $action ); } /* ------------------------------------------------------------------ */ /* Frontend */ /* ------------------------------------------------------------------ */ /** * Serve the file that Adsovo requests to verify the domain, even without a real file. */ public function serve_tracker_file() { $st = $this->status(); $file = isset( $st['verify_file'] ) ? (string) $st['verify_file'] : ''; $code = isset( $st['verify_code'] ) ? (string) $st['verify_code'] : ''; if ( '' === $file || '' === $code || empty( $_SERVER['REQUEST_URI'] ) ) { return; } $uri = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ); $path = rawurldecode( (string) wp_parse_url( $uri, PHP_URL_PATH ) ); $base = trailingslashit( (string) wp_parse_url( home_url( '/' ), PHP_URL_PATH ) ); if ( $path !== $base . $file ) { return; } status_header( 200 ); header( 'Content-Type: text/plain; charset=utf-8' ); header( 'Cache-Control: no-store' ); echo esc_html( $code ); exit; } /** * Should ads be served on this request? * * @return bool */ private function should_serve() { if ( ! $this->is_connected() || is_admin() || is_feed() || is_embed() || is_robots() || is_trackback() ) { return false; } if ( function_exists( 'is_customize_preview' ) && is_customize_preview() ) { return false; } if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { return false; } // Clicking your own ads is forbidden: no ads for logged-in editors, except in the preview. $preview = isset( $_GET['adsovo_preview'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! $preview && get_option( self::OPT_EDITORS, 1 ) && is_user_logged_in() && current_user_can( 'edit_posts' ) ) { return false; } if ( is_singular() && get_post_meta( get_queried_object_id(), self::META_OFF, true ) ) { return false; } return true; } /** * Enqueue the Adsovo ad script. */ public function enqueue_script() { if ( ! $this->should_serve() ) { return; } $st = $this->status(); $src = ! empty( $st['script_url'] ) ? $st['script_url'] : $this->api_base() . '/assets/ads/ad.js'; // The version is null on purpose: the script derives the service address from its own URL. wp_register_script( self::HANDLE, esc_url_raw( $src ), array(), null, (bool) get_option( self::OPT_FOOTER, 0 ) ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion wp_enqueue_script( self::HANDLE ); } /** * The ad script finds itself through the class adScriptClass. * * @param string $tag Script tag. * @param string $handle Script handle. * @return string */ public function script_tag( $tag, $handle ) { if ( self::HANDLE === $handle && false === strpos( $tag, 'adScriptClass' ) ) { $tag = preg_replace( '/ '300x250', 'align' => 'center', ), $atts, 'adsovo' ); if ( ! in_array( $atts['size'], self::SIZES, true ) ) { return ''; } $st = $this->status(); if ( ! $this->should_serve() || empty( $st['publisher_token'] ) ) { return ''; } $align = in_array( $atts['align'], array( 'left', 'center', 'right' ), true ) ? $atts['align'] : 'center'; return '
'; } /* ------------------------------------------------------------------ */ /* Per-post option */ /* ------------------------------------------------------------------ */ /** * Add the exclusion box to posts and pages. */ public function add_meta_box() { if ( ! $this->is_connected() ) { return; } foreach ( array( 'post', 'page' ) as $type ) { add_meta_box( 'adsovo_meta', 'Adsovo', array( $this, 'render_meta_box' ), $type, 'side', 'low' ); } } /** * Render the exclusion box. * * @param WP_Post $post Current post. */ public function render_meta_box( $post ) { wp_nonce_field( 'adsovo_meta_' . $post->ID, 'adsovo_meta_nonce' ); $off = (bool) get_post_meta( $post->ID, self::META_OFF, true ); echo ''; } /** * Save the exclusion box. * * @param int $post_id Post ID. */ public function save_meta_box( $post_id ) { if ( ! isset( $_POST['adsovo_meta_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['adsovo_meta_nonce'] ) ), 'adsovo_meta_' . $post_id ) ) { return; } if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { return; } if ( ! current_user_can( 'edit_post', $post_id ) ) { return; } if ( ! empty( $_POST['adsovo_disable'] ) ) { update_post_meta( $post_id, self::META_OFF, 1 ); } else { delete_post_meta( $post_id, self::META_OFF ); } } /* ------------------------------------------------------------------ */ /* Admin: handlers */ /* ------------------------------------------------------------------ */ /** * Connect with a plugin key. */ public function handle_connect() { $this->guard( 'adsovo_connect' ); check_admin_referer( 'adsovo_connect' ); $key = isset( $_POST['adsovo_key'] ) ? preg_replace( '/[^A-Za-z0-9]/', '', sanitize_text_field( wp_unslash( $_POST['adsovo_key'] ) ) ) : ''; if ( strlen( $key ) < 20 ) { $this->redirect_back( 'error', __( 'Please paste the complete key from the Adsovo panel.', 'adsovo' ) ); } $old = $this->key(); update_option( self::OPT_KEY, $key, false ); $data = $this->api( 'GET', 'status' ); if ( is_wp_error( $data ) ) { update_option( self::OPT_KEY, $old, false ); $this->redirect_back( 'error', $data->get_error_message() ); } $this->store_status( $data ); /* translators: %s: domain name */ $msg = sprintf( __( 'Connected to %s.', 'adsovo' ), $data['domain'] ); if ( 'unverified' === $data['status'] ) { $res = $this->api( 'POST', 'verify' ); if ( ! is_wp_error( $res ) ) { $this->store_status( $res ); $msg .= ' ' . $res['message']; if ( ! empty( $res['verified_now'] ) ) { $msg .= ' ' . __( 'The domain is now verified.', 'adsovo' ); } } } $this->redirect_back( 'success', $msg ); } /** * Disconnect and forget the key. */ public function handle_disconnect() { $this->guard( 'adsovo_disconnect' ); delete_option( self::OPT_KEY ); delete_option( self::OPT_STATUS ); $this->redirect_back( 'success', __( 'Disconnected. The ad script is no longer loaded.', 'adsovo' ) ); } /** * Refresh the cached status. */ public function handle_refresh() { $this->guard( 'adsovo_refresh' ); $data = $this->api( 'GET', 'status' ); if ( is_wp_error( $data ) ) { $this->redirect_back( 'error', $data->get_error_message() ); } $this->store_status( $data ); $this->redirect_back( 'success', __( 'Status updated.', 'adsovo' ) ); } /** * Ask Adsovo to verify the domain / check the integration. */ public function handle_verify() { $this->guard( 'adsovo_verify' ); $res = $this->api( 'POST', 'verify' ); if ( is_wp_error( $res ) ) { $this->redirect_back( 'error', $res->get_error_message() ); } $this->store_status( $res ); if ( ! empty( $res['check']['file_ok'] ) && ! empty( $res['check']['script_ok'] ) ) { $msg = $res['message']; if ( ! empty( $res['verified_now'] ) ) { $msg .= ' ' . __( 'The domain is now verified.', 'adsovo' ); } $this->redirect_back( 'success', $msg ); } $this->redirect_back( 'warning', $res['message'] . ' ' . __( 'If you use a caching plugin, clear the cache and try again. If your server does not pass unknown files to WordPress, add the verification file shown below to your site root manually.', 'adsovo' ) ); } /** * Save settings. */ public function handle_save() { $this->guard( 'adsovo_save' ); check_admin_referer( 'adsovo_save' ); update_option( self::OPT_EDITORS, empty( $_POST['hide_editors'] ) ? 0 : 1, false ); update_option( self::OPT_FOOTER, empty( $_POST['in_footer'] ) ? 0 : 1, false ); $max = isset( $_POST['auto_max'] ) ? max( 1, min( 4, absint( $_POST['auto_max'] ) ) ) : 3; $res = $this->api( 'POST', 'auto', array( 'enabled' => ! empty( $_POST['auto_enabled'] ), 'max' => $max, 'end' => ! empty( $_POST['auto_end'] ), 'paths' => isset( $_POST['auto_exclude'] ) ? sanitize_textarea_field( wp_unslash( $_POST['auto_exclude'] ) ) : '', ) ); if ( is_wp_error( $res ) ) { $this->redirect_back( 'error', $res->get_error_message() ); } $this->store_status( $res ); $this->redirect_back( 'success', __( 'Settings saved. Changes to the automatic ads apply within a minute.', 'adsovo' ) ); } /** * Dismiss the "not connected" notice for the current user. */ public function handle_dismiss() { check_admin_referer( 'adsovo_dismiss' ); update_user_meta( get_current_user_id(), self::USER_NOTICE, 1 ); wp_safe_redirect( admin_url( 'plugins.php' ) ); exit; } /* ------------------------------------------------------------------ */ /* Admin: screens */ /* ------------------------------------------------------------------ */ /** * Add the settings page. */ public function admin_menu() { add_options_page( 'Adsovo', 'Adsovo', 'manage_options', self::PAGE, array( $this, 'render_page' ) ); } /** * Load the admin stylesheet on the settings page only. * * @param string $hook Current admin page. */ public function admin_assets( $hook ) { if ( 'settings_page_' . self::PAGE !== $hook ) { return; } wp_enqueue_style( 'adsovo-admin', plugins_url( 'admin/admin.css', ADSOVO_WP_FILE ), array(), ADSOVO_WP_VERSION ); } /** * Settings link on the plugins list. * * @param array $links Action links. * @return array */ public function action_links( $links ) { array_unshift( $links, '' . esc_html__( 'Settings', 'adsovo' ) . '' ); return $links; } /** * Dismissible hint on the Plugins screen while the plugin is not connected. */ public function admin_notice_unconnected() { if ( $this->is_connected() || ! current_user_can( 'manage_options' ) || get_user_meta( get_current_user_id(), self::USER_NOTICE, true ) ) { return; } $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( ! $screen || 'plugins' !== $screen->id ) { return; } $dismiss = wp_nonce_url( admin_url( 'admin-post.php?action=adsovo_dismiss' ), 'adsovo_dismiss' ); echo '

Adsovo ' . esc_html__( 'is installed but not connected yet.', 'adsovo' ) . ' ' . '' . esc_html__( 'Connect now', 'adsovo' ) . ' | ' . '' . esc_html__( 'Dismiss', 'adsovo' ) . '

'; } /** * Small coloured status badge. * * @param string $state ok, warn, bad or off. * @param string $label Text. * @return string Escaped HTML. */ private function badge( $state, $label ) { $states = array( 'ok', 'warn', 'bad', 'off' ); $state = in_array( $state, $states, true ) ? $state : 'off'; return '' . esc_html( $label ) . ''; } /** * Small admin-post form with a single button. * * @param string $action Action and nonce name. * @param string $label Button label. * @param string $class Button class. */ private function post_form( $action, $label, $class = 'button' ) { echo '
'; echo ''; wp_nonce_field( $action ); echo '
'; } /** * Settings page. */ public function render_page() { if ( ! current_user_can( 'manage_options' ) ) { return; } // Refresh the status when it is older than five minutes. $st = $this->status(); if ( $this->is_connected() && ( empty( $st['fetched'] ) || time() - (int) $st['fetched'] > 300 ) ) { $data = $this->api( 'GET', 'status' ); if ( ! is_wp_error( $data ) ) { $this->store_status( $data ); $st = $data; } } $notice = get_transient( 'adsovo_notice_' . get_current_user_id() ); if ( $notice ) { delete_transient( 'adsovo_notice_' . get_current_user_id() ); } echo '

' . esc_html__( 'Adsovo Ad Network', 'adsovo' ) . ' ' . esc_html( ADSOVO_WP_VERSION ) . '

'; if ( $notice ) { $cls = 'notice-error'; if ( 'success' === $notice[0] ) { $cls = 'notice-success'; } elseif ( 'warning' === $notice[0] ) { $cls = 'notice-warning'; } echo '

' . esc_html( $notice[1] ) . '

'; } if ( ! empty( $st['latest_version'] ) && version_compare( $st['latest_version'], ADSOVO_WP_VERSION, '>' ) ) { /* translators: %s: version number */ echo '

' . esc_html( sprintf( __( 'A new version of the plugin is available (%s). Update it on the Plugins screen or download it from adsovo.de.', 'adsovo' ), $st['latest_version'] ) ) . '

'; } if ( ! $this->is_connected() ) { $this->render_connect_card(); } else { $this->render_status_card( $st ); $this->render_settings_card( $st ); $this->render_shortcode_card(); } echo '
'; } /** * Connection card (not connected yet). */ private function render_connect_card() { echo '

' . esc_html__( 'Connect to Adsovo', 'adsovo' ) . '

    '; echo '
  1. ' . wp_kses( sprintf( /* translators: %s: link to the Adsovo panel */ __( 'Log in to the %s and add your domain (if you have not done so yet).', 'adsovo' ), '' . esc_html__( 'Adsovo panel', 'adsovo' ) . '' ), array( 'a' => array( 'href' => array(), 'target' => array(), 'rel' => array(), ), ) ) . '
  2. '; echo '
  3. ' . wp_kses( __( 'Open Integration for the domain and copy the plugin key.', 'adsovo' ), array( 'strong' => array() ) ) . '
  4. '; echo '
  5. ' . wp_kses( __( 'Paste the key here and click Connect.', 'adsovo' ), array( 'strong' => array() ) ) . '
'; echo '
'; echo ''; wp_nonce_field( 'adsovo_connect' ); echo '

'; echo '

'; echo '

' . esc_html__( 'The plugin then loads the ad script, provides the verification file and verifies your domain automatically. The key only works for this domain.', 'adsovo' ) . '

'; echo '

' . esc_html__( 'Privacy: after you connect, this site contacts adsovo.de (site address and plugin key). Visitors load the ad script and the ads from adsovo.de.', 'adsovo' ) . '

'; } /** * Status card (connected). * * @param array $st Status. */ private function render_status_card( array $st ) { $status = isset( $st['status'] ) ? $st['status'] : 'unverified'; $map = array( 'verified' => array( 'ok', __( 'Verified', 'adsovo' ) ), 'pending' => array( 'warn', __( 'Waiting for approval', 'adsovo' ) ), 'unverified' => array( 'bad', __( 'Not verified', 'adsovo' ) ), ); $pair = isset( $map[ $status ] ) ? $map[ $status ] : $map['unverified']; $h = isset( $st['health'] ) ? $st['health'] : array( 'state' => 'na', 'label' => '', 'sub' => '', ); $hc = 'off'; if ( 'ok' === $h['state'] ) { $hc = 'ok'; } elseif ( 'bad' === $h['state'] ) { $hc = 'bad'; } elseif ( 'wait' === $h['state'] ) { $hc = 'warn'; } $file = isset( $st['verify_file'] ) ? $st['verify_file'] : ''; $url = $file ? home_url( '/' . $file ) : ''; echo '

' . esc_html__( 'Connection', 'adsovo' ) . '

'; echo ''; echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped if ( $url ) { echo ''; } echo '
' . esc_html__( 'Domain', 'adsovo' ) . '' . esc_html( $st['domain'] ) . '
' . esc_html__( 'Domain status', 'adsovo' ) . '' . $this->badge( $pair[0], $pair[1] ) . '
' . esc_html__( 'Ad script', 'adsovo' ) . '' . $this->badge( $hc, '' !== $h['label'] ? $h['label'] : __( 'unknown', 'adsovo' ) ) . ( ! empty( $h['sub'] ) ? ' ' . esc_html( $h['sub'] ) . '' : '' ) . '
' . esc_html__( 'Verification file', 'adsovo' ) . '' . esc_html( $url ) . '

'; $this->post_form( 'adsovo_refresh', __( 'Refresh status', 'adsovo' ) ); $this->post_form( 'adsovo_verify', 'unverified' === $status ? __( 'Verify now', 'adsovo' ) : __( 'Check integration', 'adsovo' ), 'unverified' === $status ? 'button button-primary' : 'button' ); $this->post_form( 'adsovo_disconnect', __( 'Disconnect', 'adsovo' ) ); if ( ! empty( $st['panel_url'] ) ) { echo '' . esc_html__( 'Open in the Adsovo panel', 'adsovo' ) . ''; } echo '

'; } /** * Settings card (connected). * * @param array $st Status. */ private function render_settings_card( array $st ) { $auto = isset( $st['auto'] ) ? $st['auto'] : array( 'enabled' => 0, 'max' => 3, 'end' => 1, 'paths' => '', ); $ready = isset( $st['status'] ) && 'verified' === $st['status']; echo '

' . esc_html__( 'Automatic ads', 'adsovo' ) . '

'; echo '
'; echo ''; wp_nonce_field( 'adsovo_save' ); echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo '
' . esc_html__( 'Place automatically', 'adsovo' ) . ''; if ( ! $ready ) { echo '
' . esc_html__( 'Available after the domain is verified.', 'adsovo' ) . ''; } echo '
' . esc_html__( 'Maximum per page', 'adsovo' ) . '
' . esc_html__( 'Ad at the end', 'adsovo' ) . '
' . esc_html__( 'Pages without ads', 'adsovo' ) . ''; echo '
' . esc_html__( 'One path per line. Always excluded: imprint, privacy, terms, contact, login, cart, checkout, account and search pages.', 'adsovo' ) . '
' . esc_html__( 'Preview', 'adsovo' ) . '' . esc_html__( 'View the placement on my home page', 'adsovo' ) . '' . '
' . wp_kses( __( 'Shows dashed placeholders only. Nothing is loaded or counted. Append ?adsovo_preview=1 to any page.', 'adsovo' ), array( 'code' => array() ) ) . '
'; echo '

' . esc_html__( 'More options', 'adsovo' ) . '

'; echo ''; echo ''; echo '
' . esc_html__( 'Hide ads for editors', 'adsovo' ) . '' . '
' . esc_html__( 'Recommended. This way you never click your own ads by accident, which is forbidden and can lead to a ban.', 'adsovo' ) . '
' . esc_html__( 'Script in footer', 'adsovo' ) . '

'; } /** * Shortcode card. */ private function render_shortcode_card() { echo '

' . esc_html__( 'Place an ad manually', 'adsovo' ) . '

'; echo '

' . esc_html__( 'Use the shortcode to place a specific format at a fixed spot in posts, pages or widgets:', 'adsovo' ) . '

'; echo '

[adsovo size="728x90"]   [adsovo size="300x250" align="left"]

'; /* translators: %s: list of ad formats */ echo '

' . esc_html( sprintf( __( 'Formats: %s. Manual placements count towards the maximum of the automatic ads. Exclude single posts and pages in the editor with the Adsovo box.', 'adsovo' ), implode( ', ', self::SIZES ) ) ) . '

'; } } Adsovo_Plugin::boot();