| 1 |
<?php |
| 2 |
|
| 3 |
namespace Templately\Modules\GutenbergIntegration\REST; |
| 4 |
|
| 5 |
use Templately\API\API; |
| 6 |
use WP_Error; |
| 7 |
use WP_REST_Request; |
| 8 |
use WP_REST_Server; |
| 9 |
|
| 10 |
/** |
| 11 |
* REST replacement for the `wp_ajax_update_gutenberg_hide_buttons` admin-ajax action |
| 12 |
* (FR-003 / FR-020, specs/033-gutenberg-integration/spec.md). Toggles the visibility of |
| 13 |
* the "Templately" / "Save in Templately" buttons in the Gutenberg editor toolbar and |
| 14 |
* persists the preference to the SITE-WIDE WordPress option |
| 15 |
* `templately-gutenberg-hide-buttons` (via `update_option()`/`get_option()` — NOT |
| 16 |
* per-user meta; the AJAX handler stored it site-wide and this route preserves that). |
| 17 |
* |
| 18 |
* The old ajax action (`Gutenberg::update_gutenberg_hide_buttons()`) is kept running as |
| 19 |
* a deprecated shim (see `Platform/Gutenberg.php::hooks()`) — the caller lives in the |
| 20 |
* browser-cached editor bundle (`assets/js/gutenberg.js`, enqueued on |
| 21 |
* `enqueue_block_editor_assets`); an already-open editor tab, or a soft reload served |
| 22 |
* stale JS from the HTTP cache, would still POST to admin-ajax with the old action name |
| 23 |
* after this route ships, so removing the handler would break the toggle for those tabs. |
| 24 |
* |
| 25 |
* Transport: the route is registered for `EDITABLE` methods (PUT/PATCH/POST) so an |
| 26 |
* external/API consumer can `PUT` per the constitution III target |
| 27 |
* (`PUT /templately/v1/gutenberg/visibility`, body `{ visibility: 'visible' | 'hidden' }`). |
| 28 |
* The plugin's own frontend calls it via POST because the shared `templatelyApi` wrapper |
| 29 |
* only attaches a JSON request body on POST (and query args on GET); EDITABLE accepts |
| 30 |
* both, so a single route serves both callers. |
| 31 |
*/ |
| 32 |
class Visibility extends API { |
| 33 |
|
| 34 |
/** |
| 35 |
* The ajax handler's authorization gate was `check_ajax_referer('templately_nonce', |
| 36 |
* 'nonce')` + `current_user_can('edit_posts')`. The REST equivalent is at least as |
| 37 |
* strict on every axis, never weaker: |
| 38 |
* |
| 39 |
* - Nonce: WP core verifies the `X-WP-Nonce` (wp_rest) header before any |
| 40 |
* permission_callback runs — the CSRF protection the ajax path got from |
| 41 |
* `templately_nonce`. |
| 42 |
* - Capability: the base `API::_permission_check()` ALREADY requires |
| 43 |
* `current_user_can('delete_posts')` before this method is reached; this override |
| 44 |
* adds the ajax handler's own `edit_posts` check on top. Net gate: |
| 45 |
* `delete_posts && edit_posts`. For every standard WP role `edit_posts` implies |
| 46 |
* `delete_posts`, so the allow-set equals the ajax path's; for an exotic custom role |
| 47 |
* with `edit_posts` but not `delete_posts`, REST is STRICTER (denies where ajax |
| 48 |
* allowed) — the desired direction. |
| 49 |
* - Connected account: this override deliberately does NOT call |
| 50 |
* `parent::permission_check()` (which would require a non-empty `api_key`). Showing |
| 51 |
* or hiding the LOCAL editor toolbar is a per-site editor preference, not a cloud |
| 52 |
* operation; the ajax handler never required a connected Templately account, and |
| 53 |
* requiring one here would regress the toggle for users editing before they connect. |
| 54 |
*/ |
| 55 |
public function permission_check( WP_REST_Request $request ) { |
| 56 |
$this->request = $request; |
| 57 |
|
| 58 |
if ( ! current_user_can( 'edit_posts' ) ) { |
| 59 |
return new WP_Error( |
| 60 |
'rest_forbidden', |
| 61 |
__( 'Sorry, you are not allowed to change the Templately toolbar visibility.', 'templately' ), |
| 62 |
[ 'status' => rest_authorization_required_code() ] |
| 63 |
); |
| 64 |
} |
| 65 |
|
| 66 |
return true; |
| 67 |
} |
| 68 |
|
| 69 |
public function register_routes() { |
| 70 |
$this->register_endpoint( |
| 71 |
'gutenberg/visibility', |
| 72 |
[ $this, 'update_visibility' ], |
| 73 |
[ |
| 74 |
'visibility' => [ |
| 75 |
'required' => true, |
| 76 |
'type' => 'string', |
| 77 |
'description' => __( 'Desired visibility of the Templately editor toolbar buttons: "visible" or "hidden".', 'templately' ), |
| 78 |
], |
| 79 |
], |
| 80 |
WP_REST_Server::EDITABLE |
| 81 |
); |
| 82 |
} |
| 83 |
|
| 84 |
public function update_visibility() { |
| 85 |
$visibility = $this->get_param( 'visibility', '', 'sanitize_key' ); |
| 86 |
|
| 87 |
if ( ! in_array( $visibility, [ 'visible', 'hidden' ], true ) ) { |
| 88 |
return $this->error( |
| 89 |
'invalid_visibility', |
| 90 |
__( 'The visibility value must be either "visible" or "hidden".', 'templately' ), |
| 91 |
'gutenberg/visibility', |
| 92 |
400 |
| 93 |
); |
| 94 |
} |
| 95 |
|
| 96 |
// Same site-wide option key the ajax handler wrote to. 'hidden' => 'yes'. |
| 97 |
$hide_buttons = 'hidden' === $visibility ? 'yes' : 'no'; |
| 98 |
update_option( 'templately-gutenberg-hide-buttons', $hide_buttons ); |
| 99 |
|
| 100 |
// Re-read and normalize exactly as the ajax handler did (any non-'yes' => 'no'). |
| 101 |
$stored = get_option( 'templately-gutenberg-hide-buttons', 'no' ); |
| 102 |
$hide_buttons = 'yes' === $stored ? 'yes' : 'no'; |
| 103 |
|
| 104 |
return $this->success( [ |
| 105 |
'visibility' => 'yes' === $hide_buttons ? 'hidden' : 'visible', |
| 106 |
'hide_buttons' => $hide_buttons, |
| 107 |
] ); |
| 108 |
} |
| 109 |
} |
| 110 |
|