PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / trunk
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! vtrunk
3.8.0 3.7.5 3.7.4 3.7.3 3.7.2 1-final 3.7.1 3.7.0 3.6.8 3.6.7 3.6.6 3.6.5 3.6.4 3.6.3 3.6.2 3.6.1 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 All 112 releases
templately / modules / post-import-feedback / REST / Feedback.php

Feedback.php in Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! trunk, at modules/post-import-feedback/REST/Feedback.php

172 lines 7.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Templately\Modules\PostImportFeedback\REST;
4
5 use Templately\API\API;
6 use Templately\Modules\PostImportFeedback\Widget;
7 use Templately\Utils\Helper;
8 use Templately\Utils\Response\ErrorCode;
9 use Templately\Utils\Response\ResponseNormalizer;
10 use WP_Error;
11 use WP_REST_Request;
12
13 /**
14 * REST replacement for the two post-import-feedback admin-ajax actions
15 * (constitution III REST migration; PRD §9 ajax→REST backlog):
16 *
17 * - `wp_ajax_templately_pack_feedback_form` → POST /templately/v1/feedback
18 * - `wp_ajax_templately_pack_import_close_feedback_modal` → POST /templately/v1/feedback/skip
19 *
20 * Behavior is preserved byte-for-byte from `Ajax\FeedbackController` — same cloud
21 * forwarding (`v2/feedback/store` / `v2/feedback/close` via the shared `Helper` API
22 * client, never raw wp_remote_*), same `Widget::LAST_SHOWN_META` cooldown writes, same
23 * `templately_fsi_complete = 'done'` write on skip/close. Only the transport moved.
24 *
25 * The old ajax handlers (`Ajax\FeedbackController::feedback_form()` /
26 * `::import_close_feedback_modal()`, still wired through
27 * `modules/full-site-import`'s shared nonce/capability AJAX registrar) are kept
28 * running as deprecated shims: a browser tab holding an already-cached feedback JS
29 * bundle would still POST/GET the old action names, and dropping the handlers would
30 * 400 those requests. Same reasoning documented for the B8 GlobalSettings migration.
31 */
32 class Feedback extends API {
33
34 /**
35 * Auth comparison — REST is a strict SUPERSET of the ajax path, never weaker:
36 *
37 * ajax path (FullSiteImport::add_ajax_action wrapper): a valid `templately_nonce`
38 * nonce + `install_plugins` AND `install_themes`.
39 *
40 * REST path: WP core verifies the `X-WP-Nonce` (wp_rest) BEFORE permission_callback
41 * runs, then `_permission_check()` (base API) requires `delete_posts`, this override
42 * requires the same `install_plugins` + `install_themes` pair, and finally
43 * `parent::permission_check()` requires a connected/verified account (`api_key`).
44 *
45 * The api_key requirement is the one addition over ajax — always satisfied here since
46 * feedback only fires after an FSI import, which itself requires a connected account.
47 */
48 public function permission_check( WP_REST_Request $request ) {
49 $this->request = $request;
50
51 if ( ! current_user_can( 'install_plugins' ) || ! current_user_can( 'install_themes' ) ) {
52 return new WP_Error(
53 'rest_forbidden',
54 __( 'Sorry, you are not allowed to submit feedback.', 'templately' ),
55 [ 'status' => rest_authorization_required_code() ]
56 );
57 }
58
59 return parent::permission_check( $request );
60 }
61
62 public function register_routes() {
63 $this->post( 'feedback', [ $this, 'submit_feedback' ], [
64 'rating' => [
65 'required' => true,
66 ],
67 ] );
68
69 $this->post( 'feedback/skip', [ $this, 'skip_feedback' ] );
70 }
71
72 /**
73 * POST /templately/v1/feedback — submit the star rating + optional review.
74 *
75 * Mirrors `Ajax\FeedbackController::feedback_form()`: forwards
76 * `{description, email, rating, pack_id}` to the cloud at `v2/feedback/store`,
77 * and on success writes `Widget::LAST_SHOWN_META` to start the FR-005 30-day
78 * cooldown (FR-013). Failure paths do NOT write the cooldown — same as the ajax
79 * handler — because the widget's subsequent close call (skip_feedback) records it.
80 */
81 public function submit_feedback() {
82 $review_description = $this->get_param( 'review-description', '', 'sanitize_textarea_field' );
83 $review_email = $this->get_param( 'review-email', '', 'sanitize_email' );
84 $rating = $this->get_param( 'rating', 0, 'absint' );
85 $pack_id = get_user_meta( get_current_user_id(), 'templately_fsi_pack_id', true );
86
87 $response = Helper::make_api_post_request( 'v2/feedback/store', [
88 'description' => $review_description,
89 'email' => $review_email,
90 'rating' => (int) $rating,
91 'pack_id' => (int) $pack_id,
92 ], [], 30 );
93
94 // 043 FR-003 — the central normalizer classifies transport failures and
95 // error bodies alike, and it is what guarantees no upstream stack trace
96 // reaches the client (the old `extract_error_from_response()` could return
97 // a whole decoded debug-500 body as the "message").
98 $normalized = ResponseNormalizer::normalize( $response );
99
100 if ( $normalized->is_error() ) {
101 $error = $normalized->error();
102
103 // A repeat submission is NOT a failure. The cloud answers it with
104 // HTTP 400 `{hasFeedback:true}` — captured live as
105 // `fixtures/rest-feedback-already-submitted.json`, and the gate is per
106 // USER, not per pack. Treating it as an error meant the cooldown was
107 // never written, so the widget came back and asked again for feedback
108 // the user had already given. Their feedback IS recorded; say so.
109 if ( ErrorCode::ALREADY_SUBMITTED === $error->code() ) {
110 update_user_meta( get_current_user_id(), Widget::LAST_SHOWN_META, time() );
111
112 return $this->success( __( 'Your feedback has already been submitted. Thank you!', 'templately' ) );
113 }
114
115 return $this->error( $error->code(), $error->message(), 'feedback', $error->status() );
116 }
117
118 $payload = $normalized->payload();
119 $message = is_array( $payload ) && isset( $payload['message'] ) ? $payload['message'] : '';
120
121 if ( '' === $message ) {
122 // The cloud answers a successful store with a message; its absence
123 // means we did not get the response we think we did.
124 return $this->error(
125 ErrorCode::SERVER_ERROR,
126 __( 'The feedback service returned an unexpected response.', 'templately' ),
127 'feedback',
128 500
129 );
130 }
131
132 // FR-005 / FR-013: a submission starts the 30-day cooldown, same as a skip/close.
133 update_user_meta( get_current_user_id(), Widget::LAST_SHOWN_META, time() );
134
135 return $this->success( $message );
136 }
137
138 /**
139 * POST /templately/v1/feedback/skip — record a skip/dismiss and start the cooldown.
140 *
141 * Mirrors `Ajax\FeedbackController::import_close_feedback_modal()`: when a
142 * `closeAction` dismiss reason is present, forwards `{action, email, pack_id}` to
143 * the cloud at `v2/feedback/close`; in ALL cases writes
144 * `templately_fsi_complete = 'done'` and `Widget::LAST_SHOWN_META` (FR-014).
145 */
146 public function skip_feedback() {
147 $return = null;
148
149 $close_action = $this->get_param( 'closeAction', '', 'sanitize_text_field' );
150 if ( ! empty( $close_action ) ) {
151 $review_email = $this->get_param( 'review-email', '', 'sanitize_email' );
152 $pack_id = get_user_meta( get_current_user_id(), 'templately_fsi_pack_id', true );
153
154 $response = Helper::make_api_post_request( 'v2/feedback/close', [
155 'action' => $close_action,
156 'email' => $review_email,
157 'pack_id' => (int) $pack_id,
158 ], [], 30 );
159
160 $return = json_decode( wp_remote_retrieve_body( $response ), true );
161 }
162
163 update_user_meta( get_current_user_id(), 'templately_fsi_complete', 'done' );
164 // FR-005 / FR-014: record when the widget was dismissed so the 30-day cooldown
165 // (evaluated by Widget::is_eligible()) can expire on a rolling basis instead of
166 // suppressing forever.
167 update_user_meta( get_current_user_id(), Widget::LAST_SHOWN_META, time() );
168
169 return $this->success( $return );
170 }
171 }
172