esc_url_raw( rest_url( 'desktop-mode/v1/feedback/usage' ) ), ); } /** * Sanitise the submitted answers: known questions only, plain text, * trimmed and capped. * * @param array $raw Question key => free text. * @return array Every question key, '' when unanswered. */ function openstation_usage_feedback_answers( array $raw ) { $answers = array(); foreach ( OPENSTATION_USAGE_FEEDBACK_QUESTIONS as $key ) { $text = isset( $raw[ $key ] ) ? sanitize_textarea_field( (string) $raw[ $key ] ) : ''; if ( mb_strlen( $text ) > OPENSTATION_USAGE_FEEDBACK_ANSWER_MAX ) { $text = mb_substr( $text, 0, OPENSTATION_USAGE_FEEDBACK_ANSWER_MAX ); } $answers[ $key ] = $text; } return $answers; } /** * Build the payload for one submission. * * The answers, the optional email, and what a reader needs to place * them: versions, language, and how long the person has had * OpenStation on. Nothing that identifies the site. The random * per-submission id exists only so the intake can ignore a retry. * Every field is listed in `readme.txt` under "External services"; * add one here and add it there in the same change. * * @param array $answers Sanitised answers, from {@see openstation_usage_feedback_answers()}. * @param string $email The address the user typed, already validated, or ''. * @param int $user_id User ID, for the days-enabled count. * @return array */ function openstation_usage_feedback_payload( array $answers, $email, $user_id ) { $days = openstation_usage_feedback_days_enabled( (int) $user_id ); return array( 'id' => wp_generate_uuid4(), 'requests' => (string) $answers['requests'], 'use_case' => (string) $answers['use_case'], 'blockers' => (string) $answers['blockers'], 'email' => (string) $email, 'plugin_version' => OPENSTATION_VERSION, 'wp_version' => get_bloginfo( 'version' ), 'locale' => get_user_locale( (int) $user_id ), 'days_enabled' => null === $days ? 0 : $days, ); } /** * Forward one payload to the intake. Synchronous and short: the user * is waiting on the form, and a failed send is reported so they can * try again. * * @param array $payload The filtered payload. * @return bool True on a 2xx answer. */ function openstation_usage_feedback_forward( array $payload ) { /** * Filters the intake URL for usage feedback. Hosts that run their * own intake point this at it; it receives the JSON payload by * POST. An empty string skips the forward. * * @param string $endpoint Default {@see OPENSTATION_USAGE_FEEDBACK_ENDPOINT}. */ $endpoint = (string) apply_filters( 'openstation_usage_feedback_endpoint', OPENSTATION_USAGE_FEEDBACK_ENDPOINT ); if ( '' === $endpoint ) { return false; } $response = wp_remote_post( $endpoint, array( 'timeout' => 5, 'redirection' => 0, 'user-agent' => 'WP OpenStation feedback/' . OPENSTATION_VERSION, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( $payload ), ) ); return ! is_wp_error( $response ) && 2 === (int) floor( wp_remote_retrieve_response_code( $response ) / 100 ); } /** * Register `POST /desktop-mode/v1/feedback/usage`. */ function openstation_register_usage_feedback_route() { $text = array( 'type' => 'string', 'default' => '', ); register_rest_route( 'desktop-mode/v1', '/feedback/usage', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => 'openstation_rest_usage_feedback', 'permission_callback' => 'openstation_rest_usage_feedback_permission', 'args' => array( 'requests' => $text, 'use_case' => $text, 'blockers' => $text, // Not `format: email`: the field is optional, and an // empty string has to pass. The handler validates it. 'email' => array( 'type' => 'string', 'default' => '', 'maxLength' => 254, ), ), ) ); } add_action( 'rest_api_init', 'openstation_register_usage_feedback_route' ); /** * Permission gate: OpenStation on for this account, plus the feature * flag. The prompt only ever shows inside the shell, so the strict * gate is the right one. No object-level check: the route stores * nothing on the site but the caller's own seen-intro flag. * * @return true|WP_Error */ function openstation_rest_usage_feedback_permission() { $enabled = openstation_rest_require_enabled(); if ( true !== $enabled ) { return $enabled; } if ( ! openstation_usage_feedback_enabled() ) { return new WP_Error( 'rest_forbidden', __( 'You are not allowed to do that.', 'desktop-mode' ), array( 'status' => 403 ) ); } return true; } /** * Handler: validate, build, filter, forward, and on success record * the prompt as answered so it never comes back. * * A failed forward answers `502` rather than a quiet `sent: false`: * the user is waiting to know whether their answers arrived. The * intro is NOT marked seen on failure, so they can try again or * close the form. * * @param WP_REST_Request $request REST request. * @return WP_REST_Response|WP_Error */ function openstation_rest_usage_feedback( WP_REST_Request $request ) { $answers = openstation_usage_feedback_answers( array( 'requests' => $request->get_param( 'requests' ), 'use_case' => $request->get_param( 'use_case' ), 'blockers' => $request->get_param( 'blockers' ), ) ); if ( '' === implode( '', $answers ) ) { return new WP_Error( 'openstation_empty_feedback', __( 'Answer at least one question first.', 'desktop-mode' ), array( 'status' => 400 ) ); } $email = trim( (string) $request->get_param( 'email' ) ); if ( '' !== $email ) { $email = sanitize_email( $email ); if ( '' === $email || ! is_email( $email ) ) { return new WP_Error( 'openstation_invalid_email', __( 'That does not look like an email address.', 'desktop-mode' ), array( 'status' => 400 ) ); } } $user_id = get_current_user_id(); $payload = openstation_usage_feedback_payload( $answers, $email, $user_id ); /** * Filters the payload before it is forwarded. Return an empty * array to suppress the send; the route then answers as if the * forward failed. * * @param array $payload The submission. */ $payload = (array) apply_filters( 'openstation_usage_feedback_payload', $payload ); $sent = ! empty( $payload ) && openstation_usage_feedback_forward( $payload ); if ( ! $sent ) { return new WP_Error( 'openstation_usage_feedback_not_sent', __( 'We could not send that right now.', 'desktop-mode' ), array( 'status' => 502 ) ); } openstation_mark_intro_seen( $user_id, OPENSTATION_USAGE_FEEDBACK_INTRO_SLUG ); return rest_ensure_response( array( 'sent' => true ) ); }