PluginProbe
Easy Tabs Block – Smart, Lightweight & Responsive Tabs for the Block Editor / trunk
Easy Tabs Block – Smart, Lightweight & Responsive Tabs for the Block Editor vtrunk
2.0.2 2.0.1 2.0.0 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 trunk 1.0.0 1.0.1 1.0.2
easy-tabs-block / easy-tabs-block.php

easy-tabs-block.php in Easy Tabs Block – Smart, Lightweight & Responsive Tabs for the Block Editor trunk, at easy-tabs-block.php

388 lines 10.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin Name: Easy Tabs Block – Smart, Lightweight & Responsive Tabs for the Block Editor
4 * Plugin URI: https://xeroqode.com/wordpress/easy-tabs-block/
5 * Description: Create beautiful, responsive tabs in Gutenberg, no code needed. Choose a pattern, style it your way, and keep your content organized and clean.
6 * Version: 2.0.2
7 * Requires at least: 6.7
8 * Requires PHP: 7.4
9 * Author: XeroQode
10 * Author URI: https://xeroqode.com/
11 * Text Domain: easy-tabs-block
12 * Domain Path: /languages/
13 * License: GPLv2 or later
14 *
15 * @package Easy_Tabs_Block
16 */
17
18 if ( ! defined( 'ABSPATH' ) ) {
19 exit;
20 }
21
22 if ( ! defined( 'ETB_VERSION' ) ) {
23 define( 'ETB_VERSION', '2.0.2' );
24 }
25
26 if ( ! defined( 'ETB_HUB_URL' ) ) {
27 define( 'ETB_HUB_URL', 'https://hub.xeroqode.com/' );
28 }
29
30 if ( ! defined( 'ETB_SITE_URL' ) ) {
31 define( 'ETB_SITE_URL', 'https://xeroqode.com/' );
32 }
33
34 require_once __DIR__ . '/inc/class-pattern-sanitizer.php';
35 require_once __DIR__ . '/inc/class-hub-client.php';
36
37 /**
38 * Easy Tabs Block Plugin Class
39 */
40 class Easy_Tabs_Block_Plugin {
41
42 /**
43 * Constructor.
44 *
45 * @since 1.0.0
46 *
47 * @return void
48 */
49 public function __construct() {
50 $this->load_admin_notice();
51
52 add_action( 'plugins_loaded', array( $this, 'init_hooks' ) );
53 }
54
55 /**
56 * Load and initialize admin notice handler.
57 *
58 * @since 1.0.6
59 *
60 * @return void
61 */
62 private function load_admin_notice() {
63 if ( ! is_admin() ) {
64 return;
65 }
66
67 require_once __DIR__ . '/inc/class-admin-notice.php';
68 new Easy_Tabs_Block_Admin_Notice();
69 }
70
71 /**
72 * Initialize hooks.
73 *
74 * @since 1.0.0
75 *
76 * @return void
77 */
78 public function init_hooks() {
79 add_action( 'init', array( $this, 'block_init' ) );
80 add_action( 'rest_api_init', array( $this, 'api_init' ) );
81 add_action( 'enqueue_block_editor_assets', array( $this, 'localize_editor_data' ) );
82 add_action( 'activated_plugin', array( $this, 'flush_pattern_cache' ) );
83 add_action( 'deactivated_plugin', array( $this, 'flush_pattern_cache' ) );
84 add_action( 'etb_flush_pattern_cache', array( 'Easy_Tabs_Block_Hub_Client', 'flush' ) );
85 }
86
87 /**
88 * Drop the cached catalogue when the Pro plugin is switched on or off.
89 *
90 * @param string $plugin Plugin file that was activated or deactivated.
91 *
92 * @return void
93 */
94 public function flush_pattern_cache( $plugin ) {
95 if ( false !== strpos( (string) $plugin, 'easy-tabs-block-pro' ) ) {
96 Easy_Tabs_Block_Hub_Client::flush();
97 }
98 }
99
100 /**
101 * Localize editor data for JavaScript.
102 *
103 * @since 1.0.7
104 *
105 * @return void
106 */
107 public function localize_editor_data() {
108 wp_add_inline_script(
109 'easy-tabs-block-tabs-editor-script',
110 'window.easyTabsBlockPlugin = ' . wp_json_encode(
111 array(
112 'isProActive' => defined( 'ETBP_VERSION' ),
113 'siteUrl' => $this->site_url(),
114 'rendersProMarkup' => true,
115 )
116 ) . ';',
117 'before'
118 );
119 }
120
121 /**
122 * Marketing site base URL, with a trailing slash.
123 *
124 * The editor builds its upgrade links from this, so moving the site is a filter here
125 * rather than a JavaScript release.
126 *
127 * @since 2.0.1
128 *
129 * @return string
130 */
131 public function site_url() {
132 /**
133 * Filter the marketing site base URL.
134 *
135 * @param string $url Site base URL, with a trailing slash.
136 */
137 return trailingslashit( apply_filters( 'easy_tabs_block_site_url', ETB_SITE_URL ) );
138 }
139
140 /**
141 * Registers the block using the metadata loaded from the `block.json` file.
142 * Behind the scenes, it registers also all assets so they can be enqueued
143 * through the block editor in the corresponding context.
144 *
145 * @since 1.0.0
146 *
147 * @see https://developer.wordpress.org/reference/functions/register_block_type/
148 *
149 * @return void
150 */
151 public function block_init() {
152
153 /**
154 * Registers the block(s) metadata from the `blocks-manifest.php` file.
155 */
156 if ( function_exists( 'wp_register_block_metadata_collection' ) ) {
157 wp_register_block_metadata_collection( __DIR__ . '/build', __DIR__ . '/build/blocks-manifest.php' );
158 }
159
160 /**
161 * Registers the block type(s) in the `blocks-manifest.php` file.
162 */
163 $manifest_data = require __DIR__ . '/build/blocks-manifest.php';
164 foreach ( array_keys( $manifest_data ) as $block_type ) {
165 register_block_type( __DIR__ . "/build/{$block_type}" );
166 }
167 }
168
169 /**
170 * Register REST route for fetching patterns.
171 *
172 * @since 1.0.0
173 *
174 * @return void
175 */
176 public function api_init() {
177 register_rest_route(
178 'etb/v1',
179 '/patterns',
180 array(
181 'methods' => 'GET',
182 'callback' => array( $this, 'get_patterns' ),
183 'permission_callback' => function () {
184 return $this->can_use_patterns( 'catalogue' );
185 },
186 )
187 );
188
189 register_rest_route(
190 'etb/v1',
191 '/patterns/(?P<id>\d+)/content',
192 array(
193 'methods' => 'GET',
194 'callback' => array( $this, 'get_pattern_content' ),
195 'permission_callback' => function () {
196 return $this->can_use_patterns( 'content' );
197 },
198 'args' => array(
199 'id' => array(
200 'type' => 'integer',
201 'required' => true,
202 ),
203 ),
204 )
205 );
206 }
207
208 /**
209 * Whether the current user may call a pattern route right now.
210 *
211 * `edit_posts` is correct for browsing a pattern library, so the limit is on rate rather
212 * than role: without it a Contributor can hold a PHP worker for a full catalogue walk.
213 *
214 * @param string $bucket 'catalogue' or 'content'.
215 * @return bool|WP_Error
216 */
217 private function can_use_patterns( $bucket ) {
218 if ( ! current_user_can( 'edit_posts' ) ) {
219 return false;
220 }
221
222 $user_id = get_current_user_id();
223
224 if ( $user_id <= 0 ) {
225 return false;
226 }
227
228 /**
229 * Filter the per-user, per-minute request ceiling for the pattern routes.
230 *
231 * @param int $limit Requests per minute.
232 * @param string $bucket 'catalogue' or 'content'.
233 */
234 $limit = (int) apply_filters( 'easy_tabs_block_pattern_rate_limit', 'content' === $bucket ? 30 : 20, $bucket );
235
236 if ( $limit <= 0 ) {
237 return true;
238 }
239
240 $key = 'etb_rl_' . $bucket . '_' . $user_id;
241 $count = (int) get_transient( $key );
242
243 if ( $count >= $limit ) {
244 return new WP_Error(
245 'etb_rate_limited',
246 __( 'Too many requests. Please wait a moment and try again.', 'easy-tabs-block' ),
247 array( 'status' => 429 )
248 );
249 }
250
251 set_transient( $key, $count + 1, MINUTE_IN_SECONDS );
252
253 return true;
254 }
255
256 /**
257 * One free pattern's markup, resolved on insert. Bundled entries carry their own
258 * content; hub entries are fetched and cached.
259 *
260 * @param WP_REST_Request $request Request.
261 *
262 * @return WP_REST_Response|WP_Error
263 */
264 public function get_pattern_content( $request ) {
265 $id = absint( $request['id'] );
266
267 $catalogue = $this->get_patterns()->get_data();
268 $match = null;
269
270 foreach ( $catalogue as $pattern ) {
271 if ( isset( $pattern['id'] ) && (int) $pattern['id'] === $id ) {
272 $match = $pattern;
273 break;
274 }
275 }
276
277 // Withdrawn on the hub between the catalogue loading and this click, or never there.
278 if ( null === $match ) {
279 return new WP_Error(
280 'etb_pattern_not_found',
281 __( 'This pattern is no longer available. Refresh the editor to reload the library.', 'easy-tabs-block' ),
282 array( 'status' => 404 )
283 );
284 }
285
286 // Pro markup is never public; the editor routes those to the licensed endpoint.
287 if ( 'pro' === ( $match['etb_pattern_selected_plan'] ?? 'free' ) ) {
288 return new WP_Error(
289 'etb_pattern_licensed',
290 __( 'This pattern requires Easy Tabs Block Pro.', 'easy-tabs-block' ),
291 array( 'status' => 403 )
292 );
293 }
294
295 if ( ! empty( $match['content'] ) ) {
296 return rest_ensure_response(
297 array(
298 'status' => 'success',
299 'pattern' => array(
300 'id' => $id,
301 'title' => isset( $match['title'] ) ? (string) $match['title'] : '',
302 'content' => (string) $match['content'],
303 ),
304 )
305 );
306 }
307
308 $content = Easy_Tabs_Block_Hub_Client::content( $id, $match['modified'] ?? '' );
309
310 if ( null === $content ) {
311 return new WP_Error(
312 'etb_pattern_unavailable',
313 __( 'Could not load this pattern right now. Check your connection and try again.', 'easy-tabs-block' ),
314 array( 'status' => 503 )
315 );
316 }
317
318 return rest_ensure_response(
319 array(
320 'status' => 'success',
321 'pattern' => array(
322 'id' => $id,
323 'title' => isset( $match['title'] ) ? (string) $match['title'] : '',
324 'content' => $content,
325 ),
326 )
327 );
328 }
329
330 /**
331 * Callback function to get the pattern catalogue.
332 *
333 * The hub is the source of truth; patterns.json is a ten-pattern starter set covering
334 * every layout and category, so a site with no outbound access still has a picker.
335 */
336 public function get_patterns() {
337 $patterns = Easy_Tabs_Block_Hub_Client::patterns();
338 $patterns = is_array( $patterns )
339 ? array_values( array_filter( $patterns, array( 'Easy_Tabs_Block_Hub_Client', 'supported' ) ) )
340 : array();
341
342 // An empty hub answer means a misconfigured hub, not an empty library, so fall
343 // through to the bundle rather than emptying the picker on every site at once.
344 if ( ! empty( $patterns ) ) {
345 return rest_ensure_response( $patterns );
346 }
347
348 return rest_ensure_response( $this->get_bundled_patterns() );
349 }
350
351 /**
352 * The starter set shipped with the plugin. Free patterns only, each with its own
353 * markup, since a Pro entry could not be licence-checked without the hub.
354 *
355 * @return array Patterns, or an empty list when the file is missing or unreadable.
356 */
357 private function get_bundled_patterns() {
358 $cache_key = 'etb_bundled_patterns_' . ETB_VERSION;
359 $cached = wp_cache_get( $cache_key, 'easy-tabs-block' );
360
361 if ( is_array( $cached ) ) {
362 return $cached;
363 }
364
365 $patterns_file = plugin_dir_path( __FILE__ ) . '/patterns/patterns.json';
366
367 if ( ! file_exists( $patterns_file ) ) {
368 return array();
369 }
370
371 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Local, bundled file.
372 $contents = file_get_contents( $patterns_file );
373
374 if ( false === $contents ) {
375 return array();
376 }
377
378 $patterns = json_decode( $contents, true );
379 $patterns = is_array( $patterns ) ? $patterns : array();
380
381 wp_cache_set( $cache_key, $patterns, 'easy-tabs-block', HOUR_IN_SECONDS );
382
383 return $patterns;
384 }
385 }
386
387 new Easy_Tabs_Block_Plugin();
388