translated labels. * * @return array */ public static function categories(): array { return array( 'idea' => __( 'An idea', 'ai-eshop-optimizer' ), 'problem' => __( 'Something is broken', 'ai-eshop-optimizer' ), 'praise' => __( 'Something you like', 'ai-eshop-optimizer' ), 'other' => __( 'Something else', 'ai-eshop-optimizer' ), ); } /** * The product's sender, or null when nothing takes its feedback. * * @param string $product Product slug. */ public static function sender( string $product ): ?callable { /** * Filters the callable that sends a product's feedback (0.20.0): `function ( array $payload, string * $product ): array{ok: bool, queued: bool}|WP_Error`. A product answers for its own slug (priority 10); * Rooster One answers for every product without one (priority 20). Null leaves the bar with the product's * feedback link and prints no dialog. * * @since 0.20.0 * * @param callable|null $sender The sender so far. * @param string $product The product. */ $sender = apply_filters( 'rooster/admin/feedback_sender', null, $product ); // self::FILTER, named literally for Plugin Check. return is_callable( $sender ) ? $sender : null; } /** * Whether the current user may send: whoever may open the product's Home. * * @param array $product Product. */ public static function can_send( array $product ): bool { return current_user_can( (string) ( $product['home']['capability'] ?? 'manage_options' ) ); } /** * Whether the dialog belongs on the product's screens: a sender, and a user who may send. * * @param array $product Product. */ public static function applies( array $product ): bool { return self::can_send( $product ) && null !== self::sender( (string) $product['slug'] ); } /** * The brand bar's item: the button that opens the dialog. */ public static function button(): string { $label = __( 'Send feedback or an idea', 'ai-eshop-optimizer' ); return '
  • '; } /** * The current screen as it may travel: the page slug (`plugin_page`), else the screen id (edit-rooster_course). */ public static function screen(): string { $page = (string) ( $GLOBALS['plugin_page'] ?? '' ); if ( '' === $page && function_exists( 'get_current_screen' ) ) { $screen = get_current_screen(); $page = null !== $screen ? (string) $screen->id : ''; } return self::clean_screen( $page ); } /** * A screen slug as it may travel: up to 64 characters of a slug (a page slug, `page:tab`, a screen id), else ''. * * @param string $slug Raw slug. */ public static function clean_screen( string $slug ): string { $slug = strtolower( trim( $slug ) ); return 1 === preg_match( '/^[a-z0-9][a-z0-9_:.-]{0,63}$/', $slug ) ? $slug : ''; } /** * The dialog (escaped here): the categories, the message, the rating, the identity block, the consent sentence * naming the product, and the thanks panel with the product's review link. * * @param array $product Product. */ public static function dialog( array $product ): string { $id = self::DIALOG_ID; $name = (string) $product['name']; $host = (string) wp_parse_url( home_url(), PHP_URL_HOST ); $user = wp_get_current_user(); $email = '' !== (string) $user->user_email ? (string) $user->user_email : (string) get_option( 'admin_email' ); $html = '' . '

    ' . esc_html__( 'Send feedback', 'ai-eshop-optimizer' ) . '

    ' . '
    ' . '
    ' /* translators: %s: the product's name. */ . '

    ' . esc_html( sprintf( __( 'We read every message. Tell us what would make %s better, or what went wrong.', 'ai-eshop-optimizer' ), $name ) ) . '

    ' . '
    ' . esc_html__( 'What kind of feedback is this?', 'ai-eshop-optimizer' ) . ''; $first = true; foreach ( self::categories() as $code => $label ) { $html .= ''; $first = false; } $html .= '
    ' . '

    ' . '

    ' . '
    ' . esc_html__( 'How is it going so far?', 'ai-eshop-optimizer' ) . '' . ''; for ( $i = 1; $i <= 5; $i++ ) { /* translators: %d: star rating from 1 to 5. */ $html .= ''; } $html .= '' . esc_html__( 'optional', 'ai-eshop-optimizer' ) . '
    ' . '
    ' /* translators: %s: this site's host name, e.g. shop.example.com */ . '

    ' . sprintf( esc_html__( 'From: %s', 'ai-eshop-optimizer' ), '' . esc_html( $host ) . '' ) . '

    ' . '

    ' . '

    ' . '
    ' /* translators: %s: the product's name. */ . '' . '
    ' . '' . ''; return $html . '
    '; } /** * The payload a sender receives, sanitised: the message, the category, the rating, the reply address, the * product and its version, the locale, the environment, and the context (the screen, the versions). * * @param array $product Product. * @param array $input Unslashed fields: message, category, rating, anonymous, email, screen. * @return array|\WP_Error */ public static function payload( array $product, array $input ) { $message = is_string( $input['message'] ?? null ) ? $input['message'] : ''; $message = trim( (string) preg_replace( '/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', (string) wp_check_invalid_utf8( $message ) ) ); if ( mb_strlen( $message ) < 3 ) { return new \WP_Error( 'rooster_admin_feedback_short', __( 'Please add a short message first.', 'ai-eshop-optimizer' ), array( 'status' => 400 ) ); } $category = sanitize_key( is_string( $input['category'] ?? null ) ? $input['category'] : '' ); $rating = max( 0, min( 5, (int) ( is_scalar( $input['rating'] ?? null ) ? $input['rating'] : 0 ) ) ); $email = sanitize_email( is_string( $input['email'] ?? null ) ? $input['email'] : '' ); if ( ! empty( $input['anonymous'] ) || ! is_email( $email ) ) { $email = ''; } $slug = (string) $product['slug']; $version = (string) $product['version']; return array( 'product' => $slug, 'category' => array_key_exists( $category, self::categories() ) ? $category : 'other', 'message' => mb_substr( $message, 0, self::MAX_MESSAGE ), 'rating' => $rating > 0 ? $rating : null, 'contact_email' => $email, 'plugin_version' => $version, 'locale' => get_user_locale(), 'environment_type' => wp_get_environment_type(), 'context' => array( 'product' => $slug, 'page' => self::clean_screen( is_string( $input['screen'] ?? null ) ? $input['screen'] : '' ), 'version' => $version, 'wp' => (string) get_bloginfo( 'version' ), 'php' => PHP_VERSION, ), ); } /** * Sends one submission through the product's sender. The answer: `message` (for the thanks panel) and * `queued` (rooster.systems could not take it now and the sender keeps it for a retry). * * @param Registry $registry The registry. * @param string $slug Product slug. * @param array $input Unslashed fields (payload()). * @return array|\WP_Error */ public static function submit( Registry $registry, string $slug, array $input ) { $product = $registry->product( $slug ); if ( null === $product ) { return new \WP_Error( 'rooster_admin_product', __( 'Unknown product.', 'ai-eshop-optimizer' ), array( 'status' => 400 ) ); } $sender = self::sender( $slug ); if ( null === $sender ) { return new \WP_Error( 'rooster_admin_feedback_none', __( 'This product takes no feedback here.', 'ai-eshop-optimizer' ), array( 'status' => 404 ) ); } if ( ! self::can_send( $product ) ) { return new \WP_Error( 'rest_forbidden', __( 'You are not allowed to send feedback.', 'ai-eshop-optimizer' ), array( 'status' => rest_authorization_required_code() ) ); } $payload = self::payload( $product, $input ); if ( is_wp_error( $payload ) ) { return $payload; } $result = call_user_func( $sender, $payload, $slug ); if ( is_wp_error( $result ) ) { return $result; } $ok = is_array( $result ) && ! empty( $result['ok'] ); $queued = is_array( $result ) && ! empty( $result['queued'] ); if ( ! $ok && ! $queued ) { // Refused for good (neither taken nor queued): say so rather than thank for a message that went nowhere. return new \WP_Error( 'rooster_admin_feedback_refused', __( 'rooster.systems did not accept this message. Please try again later.', 'ai-eshop-optimizer' ), array( 'status' => 502 ) ); } return array( 'message' => __( 'Thank you', 'ai-eshop-optimizer' ), 'queued' => $queued && ! $ok, ); } }