Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
57.78% |
104 / 180 |
|
18.18% |
2 / 11 |
CRAP | |
0.00% |
0 / 1 |
Inspect | |
57.78% |
104 / 180 |
|
18.18% |
2 / 11 |
146.69 | |
0.00% |
0 / 1 |
init | |
60.00% |
6 / 10 |
|
0.00% |
0 / 1 |
3.58 | |||
adminEnqueueScripts | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
hideMetaBox | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
addMetaBox | |
92.86% |
13 / 14 |
|
0.00% |
0 / 1 |
3.00 | |||
renderMetaBoxContent | |
100.00% |
29 / 29 |
|
100.00% |
1 / 1 |
1 | |||
postMetaTable | |
95.24% |
40 / 42 |
|
0.00% |
0 / 1 |
8 | |||
formatPostMetaValue | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
3.33 | |||
getClipboardText | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
save | |
81.82% |
9 / 11 |
|
0.00% |
0 / 1 |
6.22 | |||
restApiInit | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
restApiResponse | |
0.00% |
0 / 49 |
|
0.00% |
0 / 1 |
56 |
1 | <?php |
2 | |
3 | declare(strict_types=1); |
4 | |
5 | /** |
6 | * BeyondWords Post Inspect Panel. |
7 | * |
8 | * @package Beyondwords\Wordpress |
9 | * @author Stuart McAlpine <stu@beyondwords.io> |
10 | * @since 3.0.0 |
11 | */ |
12 | |
13 | namespace Beyondwords\Wordpress\Component\Post\Panel\Inspect; |
14 | |
15 | use Beyondwords\Wordpress\Component\Post\PostMetaUtils; |
16 | use Beyondwords\Wordpress\Component\Settings\SettingsUtils; |
17 | use Beyondwords\Wordpress\Core\ApiClient; |
18 | |
19 | /** |
20 | * Inspect |
21 | * |
22 | * @since 3.0.0 |
23 | */ |
24 | class Inspect |
25 | { |
26 | /** |
27 | * Constructor |
28 | */ |
29 | public function init() |
30 | { |
31 | add_action('admin_enqueue_scripts', array($this, 'adminEnqueueScripts')); |
32 | add_action('add_meta_boxes', array($this, 'addMetaBox')); |
33 | add_action('rest_api_init', array($this, 'restApiInit')); |
34 | |
35 | add_filter('default_hidden_meta_boxes', array($this, 'hideMetaBox')); |
36 | |
37 | add_action('wp_loaded', function () { |
38 | $postTypes = SettingsUtils::getCompatiblePostTypes(); |
39 | |
40 | if (is_array($postTypes)) { |
41 | foreach ($postTypes as $postType) { |
42 | add_action("save_post_{$postType}", array($this, 'save'), 5); |
43 | } |
44 | } |
45 | }); |
46 | } |
47 | |
48 | /** |
49 | * Enqueue JS for Inspect feature. |
50 | */ |
51 | public function adminEnqueueScripts($hook) |
52 | { |
53 | // Only enqueue for Post screens |
54 | if ($hook === 'post.php' || $hook === 'post-new.php') { |
55 | wp_enqueue_script( |
56 | 'beyondwords-inspect', |
57 | BEYONDWORDS__PLUGIN_URI . 'src/Component/Post/Panel/Inspect/js/inspect.js', |
58 | array('jquery'), |
59 | BEYONDWORDS__PLUGIN_VERSION, |
60 | true |
61 | ); |
62 | } |
63 | } |
64 | |
65 | /** |
66 | * Hides the metabox by default. |
67 | * |
68 | * @param string[] $hidden An array of IDs of meta boxes hidden by default. |
69 | */ |
70 | public function hideMetaBox($hidden) |
71 | { |
72 | $hidden[] = 'beyondwords__inspect'; |
73 | return $hidden; |
74 | } |
75 | |
76 | /** |
77 | * Adds the meta box container for the Classic Editor. |
78 | * |
79 | * The Block Editor UI is handled using JavaScript. |
80 | * |
81 | * @param string $postType |
82 | */ |
83 | public function addMetaBox($postType) |
84 | { |
85 | $postTypes = SettingsUtils::getCompatiblePostTypes(); |
86 | |
87 | if (is_array($postTypes) && ! in_array($postType, $postTypes)) { |
88 | return; |
89 | } |
90 | |
91 | add_meta_box( |
92 | 'beyondwords__inspect', |
93 | __('BeyondWords', 'speechkit') . ': ' . __('Inspect', 'speechkit'), |
94 | array($this, 'renderMetaBoxContent'), |
95 | $postType, |
96 | 'advanced', |
97 | 'low', |
98 | [ |
99 | '__back_compat_meta_box' => true, |
100 | ] |
101 | ); |
102 | } |
103 | |
104 | /** |
105 | * Render Meta Box content. |
106 | * |
107 | * @param WP_Post $post The post object. |
108 | */ |
109 | public function renderMetaBoxContent($post) |
110 | { |
111 | $metadata = PostMetaUtils::getAllBeyondwordsMetadata($post->ID); |
112 | |
113 | $this->postMetaTable($metadata); |
114 | ?> |
115 | <button |
116 | type="button" |
117 | id="beyondwords__inspect--copy" |
118 | class="button button-large" |
119 | style="margin: 10px 0 0;" |
120 | data-clipboard-text="<?php echo esc_attr($this->getClipboardText($metadata)); ?>" |
121 | > |
122 | <?php esc_html_e('Copy', 'speechkit'); ?> |
123 | <span |
124 | id="beyondwords__inspect--copy-confirm" |
125 | style="display: none; margin: 5px 0 0;" |
126 | class="dashicons dashicons-yes" |
127 | ></span> |
128 | </button> |
129 | |
130 | <button |
131 | type="button" |
132 | id="beyondwords__inspect--remove" |
133 | class="button button-large button-link-delete" |
134 | style="margin: 10px 0 0; float: right;" |
135 | > |
136 | <?php esc_html_e('Remove', 'speechkit'); ?> |
137 | <span |
138 | id="beyondwords__inspect--remove" |
139 | style="display: none; margin: 5px 0 0;" |
140 | class="dashicons dashicons-yes" |
141 | ></span> |
142 | </button> |
143 | |
144 | <?php |
145 | } |
146 | |
147 | /** |
148 | * Render Meta Box table. |
149 | * |
150 | * @param array $metadata The metadata returned by has_meta. |
151 | * |
152 | * @since v3.0.0 |
153 | * @since v3.9.0 Change $postMetaKeys param to $metadata, to support meta_ids. |
154 | */ |
155 | public function postMetaTable($metadata) |
156 | { |
157 | if (! is_array($metadata)) { |
158 | return; |
159 | } |
160 | ?> |
161 | <div id="postcustomstuff"> |
162 | <table id="inspect-table"> |
163 | <thead> |
164 | <tr> |
165 | <th class="left"><?php esc_html_e('Name', 'speechkit'); ?></th> |
166 | <th><?php esc_html_e('Value', 'speechkit'); ?></th> |
167 | </tr> |
168 | </thead> |
169 | <tbody id="inspect-table-list"> |
170 | <?php |
171 | foreach ($metadata as $item) : |
172 | if ( |
173 | ! is_array($item) || |
174 | ! array_key_exists('meta_id', $item) || |
175 | ! array_key_exists('meta_key', $item) || |
176 | ! array_key_exists('meta_value', $item) |
177 | ) { |
178 | continue; |
179 | } |
180 | |
181 | $metaId = $item['meta_id'] ? $item['meta_id'] : $item['meta_key']; |
182 | $metaKey = $item['meta_key']; |
183 | $metaValue = $this->formatPostMetaValue($item['meta_value']); |
184 | ?> |
185 | <tr id="beyondwords-inspect-<?php echo esc_attr($metaId); ?>" class="alternate"> |
186 | <td class="left"> |
187 | <label |
188 | class="screen-reader-text" |
189 | for="beyondwords-inspect-<?php echo esc_attr($metaId); ?>-key" |
190 | > |
191 | <?php esc_html_e('Key', 'speechkit'); ?> |
192 | </label> |
193 | <input |
194 | id="beyondwords-inspect-<?php echo esc_attr($metaId); ?>-key" |
195 | type="text" |
196 | size="20" |
197 | value="<?php echo esc_attr($metaKey); ?>" |
198 | readonly |
199 | /> |
200 | </td> |
201 | <td> |
202 | <label |
203 | class="screen-reader-text" |
204 | for="beyondwords-inspect-<?php echo esc_attr($metaId); ?>-value" |
205 | > |
206 | <?php esc_html_e('Value', 'speechkit'); ?> |
207 | </label> |
208 | <textarea |
209 | id="beyondwords-inspect-<?php echo esc_attr($metaId); ?>-value" |
210 | rows="2" |
211 | cols="30" |
212 | data-beyondwords-metavalue="true" |
213 | readonly |
214 | ><?php echo esc_html($metaValue); ?></textarea> |
215 | </td> |
216 | </tr> |
217 | <?php |
218 | endforeach; |
219 | |
220 | wp_nonce_field('beyondwords_delete_content', 'beyondwords_delete_content_nonce'); |
221 | ?> |
222 | <input |
223 | type="hidden" |
224 | id="beyondwords_delete_content" |
225 | name="beyondwords_delete_content" |
226 | value="1" |
227 | disabled |
228 | /> |
229 | </tbody> |
230 | </table> |
231 | </div> |
232 | <?php |
233 | } |
234 | |
235 | /** |
236 | * Format post meta value. |
237 | * |
238 | * @param mixed $value The metadata value. |
239 | * |
240 | * @since v3.9.0 |
241 | */ |
242 | public function formatPostMetaValue($value) |
243 | { |
244 | if (is_numeric($value) || is_string($value)) { |
245 | return $value; |
246 | } |
247 | |
248 | return wp_json_encode($value); |
249 | } |
250 | |
251 | /** |
252 | * Get Clipboard Text. |
253 | * |
254 | * @param array $metadata Post metadata. |
255 | * |
256 | * @since 3.0.0 |
257 | * @since 3.9.0 Output all post meta data from the earlier has_meta() call instead of |
258 | * the previous multiple get_post_meta() calls. |
259 | * |
260 | * @return string |
261 | */ |
262 | public function getClipboardText($metadata) |
263 | { |
264 | $lines = []; |
265 | |
266 | foreach ($metadata as $m) { |
267 | $lines[] = $m['meta_key'] . "\r\n" . $this->formatPostMetaValue($m['meta_value']); |
268 | } |
269 | |
270 | $lines[] = "=== " . __('Copied using the Classic Editor', 'speechkit') . " ===\r\n\r\n"; |
271 | |
272 | return implode("\r\n\r\n", $lines); |
273 | } |
274 | |
275 | /** |
276 | * Runs when a post is saved. |
277 | * |
278 | * If "Remove" has been pressed in the Classic Editor we set the `beyondwords_delete_content` |
279 | * custom field. At a later priority we check for this custom field and if it's set |
280 | * we make a DELETE request to the BeyondWords REST API, keeping WordPress and the |
281 | * REST API in sync. |
282 | * |
283 | * If we don't perform a DELETE REST API request to keep them in sync then the |
284 | * API will respond with a "source_id is already in use" error message whenver we |
285 | * attempt to regenerate audio for a post that has audio content "Removed" in |
286 | * WordPress but still exists in the REST API. |
287 | * |
288 | * @since 4.0.7 |
289 | * |
290 | * @param int $postId The ID of the post being saved. |
291 | */ |
292 | public function save($postId) |
293 | { |
294 | if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { |
295 | return $postId; |
296 | } |
297 | |
298 | // "save_post" can be triggered at other times, so verify this request came from the our component |
299 | if ( |
300 | ! isset($_POST['beyondwords_delete_content_nonce']) || |
301 | ! wp_verify_nonce( |
302 | sanitize_key($_POST['beyondwords_delete_content_nonce']), |
303 | 'beyondwords_delete_content' |
304 | ) |
305 | ) { |
306 | return $postId; |
307 | } |
308 | |
309 | if (isset($_POST['beyondwords_delete_content'])) { |
310 | // Set the flag - the DELETE request is performed at a later priority |
311 | update_post_meta($postId, 'beyondwords_delete_content', '1'); |
312 | } |
313 | |
314 | return $postId; |
315 | } |
316 | |
317 | /** |
318 | * REST API init. |
319 | * |
320 | * Register REST API routes. |
321 | **/ |
322 | public function restApiInit() |
323 | { |
324 | register_rest_route('beyondwords/v1', '/projects/(?P<projectId>[0-9]+)/content/(?P<contentId>[a-zA-Z0-9\-]+)', array( // phpcs:ignore Generic.Files.LineLength.TooLong |
325 | 'methods' => \WP_REST_Server::READABLE, |
326 | 'callback' => array($this, 'restApiResponse'), |
327 | 'permission_callback' => function () { |
328 | return current_user_can('edit_posts'); |
329 | }, |
330 | )); |
331 | } |
332 | |
333 | /** |
334 | * REST API response. |
335 | * |
336 | * Fetches a content object from the BeyondWords REST API. |
337 | * |
338 | * @param \WP_REST_Request $request The REST request object. |
339 | * |
340 | * @return \WP_REST_Response |
341 | **/ |
342 | public function restApiResponse(\WP_REST_Request $request) |
343 | { |
344 | $projectId = $request['projectId'] ?? ''; |
345 | $contentId = $request['contentId'] ?? ''; |
346 | |
347 | if (! is_numeric($projectId)) { |
348 | return rest_ensure_response( |
349 | new \WP_Error( |
350 | 400, |
351 | __('Invalid Project ID', 'speechkit'), |
352 | ['projectId' => $projectId] |
353 | ) |
354 | ); |
355 | } |
356 | |
357 | if (empty($contentId)) { |
358 | return rest_ensure_response( |
359 | new \WP_Error( |
360 | 400, |
361 | __('Invalid Content ID', 'speechkit'), |
362 | ['contentId' => $contentId] |
363 | ) |
364 | ); |
365 | } |
366 | |
367 | $response = ApiClient::getContent($contentId, $projectId); |
368 | |
369 | // Check for REST API connection errors. |
370 | if (is_wp_error($response)) { |
371 | return rest_ensure_response( |
372 | new \WP_Error( |
373 | 500, |
374 | __('Could not connect to BeyondWords API', 'speechkit'), |
375 | $response->get_error_data() |
376 | ) |
377 | ); |
378 | } |
379 | |
380 | $code = wp_remote_retrieve_response_code($response); |
381 | $body = wp_remote_retrieve_body($response); |
382 | |
383 | // Check for REST API response errors. |
384 | if ($code < 200 || $code >= 300) { |
385 | return rest_ensure_response( |
386 | new \WP_Error( |
387 | $code, |
388 | /* translators: %d is replaced with the error code. */ |
389 | sprintf(__('BeyondWords REST API returned error code %d', 'speechkit'), $code), |
390 | [ |
391 | 'body' => $body, |
392 | ] |
393 | ) |
394 | ); |
395 | } |
396 | |
397 | $data = json_decode($body, true); |
398 | |
399 | // Check for REST API JSON response. |
400 | if (! is_array($data)) { |
401 | return rest_ensure_response( |
402 | new \WP_Error( |
403 | 500, |
404 | __('Invalid response from BeyondWords API', 'speechkit') |
405 | ) |
406 | ); |
407 | } |
408 | |
409 | // Return the project ID in the response. |
410 | $data['project_id'] = $projectId; |
411 | |
412 | return rest_ensure_response($data); |
413 | } |
414 | } |