__( 'Link copied', 'overlayqa' ), 'failed' => __( 'Copy unavailable. Select and copy the link below.', 'overlayqa' ), ) ); } public static function links( $links ) { array_unshift( $links, '' . esc_html__( 'Settings', 'overlayqa' ) . '' ); return $links; } private static function require_admin() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to manage OverlayQA.', 'overlayqa' ), '', array( 'response' => 403 ) ); } } private static function notice_key() { return 'overlayqa_notice_' . get_current_user_id(); } private static function finish( $result, $success = '' ) { set_transient( self::notice_key(), array( 'error' => is_wp_error( $result ), 'message' => is_wp_error( $result ) ? $result->get_error_message() : $success, ), MINUTE_IN_SECONDS ); // WordPress may serve its public site and admin from different hosts. $admin_host = wp_parse_url( admin_url(), PHP_URL_HOST ); add_filter( 'allowed_redirect_hosts', static function ( $hosts ) use ( $admin_host ) { $hosts[] = $admin_host; return $hosts; } ); wp_safe_redirect( self::url() ); exit; } private static function input( $source, $key ) { return isset( $source[ $key ] ) && is_string( $source[ $key ] ) ? sanitize_text_field( wp_unslash( $source[ $key ] ) ) : ''; } public static function action() { self::require_admin(); if ( 'POST' !== self::input( $_SERVER, 'REQUEST_METHOD' ) ) { wp_die( esc_html__( 'Use the OverlayQA settings form to continue.', 'overlayqa' ), '', array( 'response' => 405 ) ); } check_admin_referer( 'overlayqa_action' ); $action = self::input( $_POST, 'operation' ); if ( 'connect' === $action ) { if ( '1' !== self::input( $_POST, 'consent' ) ) { self::finish( new WP_Error( 'consent', __( 'Confirm the service connection before continuing.', 'overlayqa' ) ) ); } if ( ! empty( OverlayQA_Service::connection() ) ) { self::finish( new WP_Error( 'connected', __( 'Disconnect your current account before connecting another.', 'overlayqa' ) ) ); } $url = OverlayQA_Service::begin_connection(); if ( is_wp_error( $url ) ) { self::finish( $url ); } // The URL is built by begin_connection from our fixed service origin. wp_redirect( $url ); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- Fixed OverlayQA OAuth endpoint, never a request-supplied URL. exit; } if ( 'disconnect' === $action ) { self::finish( OverlayQA_Service::disconnect(), __( 'Disconnected. Existing reviews remain in OverlayQA.', 'overlayqa' ) ); } if ( 'project' === $action ) { $project_id = self::input( $_POST, 'project_id' ); $projects = OverlayQA_Service::api( '/projects' ); if ( is_wp_error( $projects ) ) { self::finish( $projects ); } foreach ( $projects['projects'] ?? array() as $project ) { if ( $project['id'] === $project_id ) { $lock = OverlayQA_Service::lock( 'refresh-' . get_current_user_id() ); if ( is_wp_error( $lock ) ) { self::finish( $lock ); } try { $connection = OverlayQA_Service::connection( true ); if ( empty( $connection['access_token'] ) ) { $result = new WP_Error( 'disconnected', __( 'Connect your OverlayQA account first.', 'overlayqa' ) ); } else { $connection['project_id'] = $project_id; $connection['project_name'] = $project['name']; OverlayQA_Service::save( $connection ); $result = true; } } finally { delete_option( $lock ); } self::finish( $result, __( 'Project saved for your WordPress account.', 'overlayqa' ) ); } } self::finish( new WP_Error( 'project', __( 'Choose a project you can access.', 'overlayqa' ) ) ); } if ( 'create' === $action ) { $post_id = absint( self::input( $_POST, 'post_id' ) ); self::finish( self::create_review( $post_id ), __( 'Your review is ready. Open it or copy its link below.', 'overlayqa' ) ); } self::finish( new WP_Error( 'operation', __( 'Choose an action in OverlayQA settings.', 'overlayqa' ) ) ); } public static function callback() { self::require_admin(); nocache_headers(); header( 'Referrer-Policy: no-referrer' ); // OAuth uses a single-use, user-bound state and PKCE instead of a WP form nonce. // phpcs:disable WordPress.Security.NonceVerification.Recommended $result = OverlayQA_Service::finish_connection( self::input( $_GET, 'state' ), self::input( $_GET, 'iss' ), self::input( $_GET, 'code' ), self::input( $_GET, 'error' ) ); // phpcs:enable WordPress.Security.NonceVerification.Recommended self::finish( $result, __( 'Connected. Choose the project for your reviews.', 'overlayqa' ) ); } private static function can_review( $post ) { return $post && in_array( $post->post_type, array( 'page', 'post' ), true ) && 'publish' === $post->post_status && '' === $post->post_password && is_post_publicly_viewable( $post ) && current_user_can( 'edit_post', $post->ID ); } private static function active_review( $reviews, $url ) { foreach ( $reviews as $review ) { if ( ( $review['pageUrl'] ?? '' ) === $url && empty( $review['revokedAt'] ) && ( empty( $review['expiresAt'] ) || strtotime( $review['expiresAt'] ) > time() ) ) { return $review; } } return null; } /** Check the server before creating: a lost response must not mean a duplicate review. */ private static function create_review( $post_id ) { $post = get_post( $post_id ); $connection = OverlayQA_Service::connection(); $project_id = $connection['project_id'] ?? ''; if ( ! self::can_review( $post ) ) { return new WP_Error( 'post', __( 'Choose a published page or post without password protection.', 'overlayqa' ) ); } if ( ! wp_is_uuid( $project_id ) ) { return new WP_Error( 'project', __( 'Choose an OverlayQA project first.', 'overlayqa' ) ); } $url = get_permalink( $post ); $lock = OverlayQA_Service::lock( 'review-' . $project_id . '-' . $url ); if ( is_wp_error( $lock ) ) { return $lock; } try { $reviews = OverlayQA_Service::api( '/client-reviews/project/' . rawurlencode( $project_id ) ); if ( is_wp_error( $reviews ) ) { return $reviews; } if ( ! isset( $reviews['data'] ) || ! is_array( $reviews['data'] ) ) { return new WP_Error( 'reviews', __( 'Your reviews could not be checked. Refresh before creating another.', 'overlayqa' ) ); } if ( self::active_review( $reviews['data'], $url ) ) { return true; } return OverlayQA_Service::api( '/client-reviews', 'POST', array( 'projectId' => $project_id, 'pageUrl' => $url, 'mode' => 'live', 'title' => mb_substr( wp_strip_all_tags( get_the_title( $post ) ), 0, 200 ), ) ); } finally { delete_option( $lock ); } } private static function form_start( $operation ) { ?>
array( 'page', 'post' ), 'post_status' => 'publish', 'has_password' => false, 'posts_per_page' => 20, 'paged' => $page, 'orderby' => array( 'title' => 'ASC', 'ID' => 'ASC' ), ) ); ?>