PluginProbe ʕ •ᴥ•ʔ
WPCode – Insert Headers and Footers + Custom Code Snippets – WordPress Code Manager / 2.3.6
WPCode – Insert Headers and Footers + Custom Code Snippets – WordPress Code Manager v2.3.6
2.3.6 trunk 1.1 1.2 1.3 1.3.1 1.3.2 1.3.3 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.5.0 1.6.0 1.6.1 1.6.2 2.0.0 2.0.1 2.0.10 2.0.11 2.0.12 2.0.13 2.0.13.1 2.0.2 2.0.3 2.0.4 2.0.4.1 2.0.4.2 2.0.4.3 2.0.4.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.8.1 2.0.9 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.2 2.1.3 2.1.3.1 2.1.4 2.1.4.1 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 2.2.3 2.2.3.1 2.2.4 2.2.4.1 2.2.5 2.2.6 2.2.7 2.2.8 2.2.9 2.3.0 2.3.1 2.3.2 2.3.2.1 2.3.3 2.3.4 2.3.5
insert-headers-and-footers / includes / class-wpcode-library.php
insert-headers-and-footers / includes Last commit date
admin 1 week ago auto-insert 4 months ago conditional-logic 6 months ago execute 5 months ago generator 6 months ago lite 9 months ago capabilities.php 2 years ago class-wpcode-abilities-api.php 1 week ago class-wpcode-admin-bar-info.php 2 years ago class-wpcode-auto-insert.php 4 months ago class-wpcode-capabilities.php 3 years ago class-wpcode-conditional-logic.php 4 months ago class-wpcode-error.php 2 years ago class-wpcode-file-cache.php 1 year ago class-wpcode-file-logger.php 1 year ago class-wpcode-generator.php 4 months ago class-wpcode-install.php 1 year ago class-wpcode-library-auth.php 1 year ago class-wpcode-library.php 1 week ago class-wpcode-settings.php 6 months ago class-wpcode-smart-tags.php 11 months ago class-wpcode-snippet-cache.php 4 months ago class-wpcode-snippet-execute.php 1 year ago class-wpcode-snippet.php 1 year ago compat.php 2 years ago global-output.php 1 year ago helpers.php 1 year ago icons.php 5 months ago ihaf.php 3 years ago legacy.php 3 years ago pluggable.php 2 years ago post-type.php 1 week ago safe-mode.php 11 months ago shortcode.php 2 years ago
class-wpcode-library.php
1018 lines
1 <?php
2 /**
3 * Load snippets from the wpcode.com snippet library.
4 *
5 * @package WPCode
6 */
7
8 /**
9 * Class WPCode_Library.
10 */
11 class WPCode_Library {
12
13 /**
14 * Key for storing snippets in the cache.
15 *
16 * @var string
17 */
18 protected $cache_key = 'snippets';
19
20 /**
21 * Library endpoint for loading all data.
22 *
23 * @var string
24 */
25 protected $all_snippets_endpoint = 'get';
26
27 /**
28 * The key for storing individual snippets.
29 *
30 * @var string
31 */
32 protected $snippet_key = 'snippets/snippet';
33
34 /**
35 * The base cache folder for this class.
36 *
37 * @var string
38 */
39 protected $cache_folder = 'library';
40
41 /**
42 * The data.
43 *
44 * @var array
45 */
46 protected $data;
47
48 /**
49 * The default time to live for libary items that are cached.
50 *
51 * @var int
52 */
53 protected $ttl = DAY_IN_SECONDS;
54
55 /**
56 * Key for transient used to store already installed snippets.
57 *
58 * @var string
59 */
60 protected $used_snippets_transient_key = 'wpcode_used_library_snippets';
61
62 /**
63 * Array of snippet ids that were already loaded from the library.
64 *
65 * @var array
66 */
67 protected $library_snippets;
68
69 /**
70 * Meta Key used for storing the library id.
71 *
72 * @var string
73 */
74 protected $snippet_library_id_meta_key = '_wpcode_library_id';
75
76 /**
77 * Total number of snippets in the library atm.
78 *
79 * @var int
80 */
81 protected $snippets_count;
82
83 /**
84 * Snippets grouped by library username.
85 *
86 * @var array
87 */
88 protected $snippets_by_username = array();
89
90 /**
91 * Array of library usernames.
92 *
93 * @var array
94 */
95 protected $library_usernames = array();
96
97 /**
98 * Constructor.
99 */
100 public function __construct() {
101 $this->hooks();
102 }
103
104 /**
105 * Class-specific hooks.
106 *
107 * @return void
108 */
109 protected function hooks() {
110 add_action( 'trash_wpcode', array( $this, 'clear_used_snippets' ) );
111 add_action( 'transition_post_status', array( $this, 'clear_used_snippets_untrash' ), 10, 3 );
112 add_action( 'wpcode_library_api_auth_connected', array( $this, 'delete_cache' ) );
113 add_action( 'wpcode_library_api_auth_connected', array( $this, 'get_data_delayed' ), 15 );
114 add_action( 'wpcode_library_api_auth_deleted', array( $this, 'delete_cache' ) );
115 }
116
117 /**
118 * Wait for the file cache to be cleared before loading the data.
119 *
120 * @return void
121 */
122 public function get_data_delayed() {
123
124 // Wait for the cache to be cleared.
125 add_action( 'shutdown', array( $this, 'get_data' ) );
126 }
127
128 /**
129 * Grab all the available categories from the library.
130 *
131 * @return array
132 */
133 public function get_data() {
134 if ( ! isset( $this->data ) ) {
135 $this->data = $this->load_data();
136 }
137
138 return $this->data;
139 }
140
141 /**
142 * Get the number of snippets in the library.
143 *
144 * @return int
145 */
146 public function get_snippets_count() {
147 if ( ! isset( $this->snippets_count ) ) {
148 $this->snippets_count = 0;
149 $data = $this->get_data();
150 if ( ! empty( $data['snippets'] ) ) {
151 $this->snippets_count = count( $data['snippets'] );
152 }
153 }
154
155 return $this->snippets_count;
156 }
157
158 /**
159 * Grab data from the cache.
160 *
161 * @param string $key The key used to grab from cache.
162 * @param int $ttl The time to live for cached data, defaults to class ttl.
163 *
164 * @return array|false
165 */
166 public function get_from_cache( $key, $ttl = 0 ) {
167 if ( empty( $ttl ) ) {
168 $ttl = $this->ttl;
169 }
170
171 $data = wpcode()->file_cache->get( $this->cache_folder . '/' . $key, $ttl );
172
173 if ( isset( $data['error'] ) && isset( $data['time'] ) ) {
174 if ( $data['time'] + 10 * MINUTE_IN_SECONDS < time() ) {
175 return false;
176 } else {
177 return $this->get_empty_array();
178 }
179 }
180
181 return $data;
182 }
183
184 /**
185 * Load the library data either from the server or from cache.
186 *
187 * @return array
188 */
189 public function load_data() {
190 $this->data = $this->get_from_cache( $this->cache_key );
191
192 if ( empty( $this->data ) || ! is_array( $this->data ) ) {
193 $this->data = $this->get_from_server();
194 }
195
196 // Enforce shape BEFORE maybe_add_usernames_data() so that method's
197 // `$this->data['categories'][] = ...` and `array_merge($this->data['snippets'], ...)`
198 // can't fatal on a partial-shape cache payload like {"snippets": null}.
199 if ( ! is_array( $this->data ) ) {
200 $this->data = $this->get_empty_array();
201 }
202 if ( ! isset( $this->data['categories'] ) || ! is_array( $this->data['categories'] ) ) {
203 $this->data['categories'] = array();
204 }
205 if ( ! isset( $this->data['snippets'] ) || ! is_array( $this->data['snippets'] ) ) {
206 $this->data['snippets'] = array();
207 }
208
209 $this->maybe_add_usernames_data();
210
211 return $this->data;
212 }
213
214
215 /**
216 * Get data from the server.
217 *
218 * @return array
219 */
220 protected function get_from_server() {
221 $data = $this->process_response( $this->make_request( $this->all_snippets_endpoint ) );
222
223 if ( empty( $data['snippets'] ) ) {
224 return $this->save_temporary_response_fail( $this->cache_key );
225 }
226
227 $this->save_to_cache( $this->cache_key, $data );
228
229 return $data;
230 }
231
232 /**
233 * Generic request handler with support for authentication.
234 *
235 * @param string $endpoint The API endpoint to load data from.
236 * @param string $method The method used for the request (GET, POST, etc).
237 * @param array $data The data to pass in the body for POST-like requests.
238 *
239 * @return string
240 */
241 public function make_request( $endpoint = '', $method = 'GET', $data = array() ) {
242 $args = array(
243 'method' => $method,
244 'timeout' => 10,
245 );
246 if ( wpcode()->library_auth->has_auth() ) {
247 $args['headers'] = $this->get_authenticated_headers();
248 }
249 if ( ! empty( $data ) ) {
250 $args['body'] = $data;
251 }
252
253 $url = add_query_arg(
254 array(
255 'site' => rawurlencode( site_url() ),
256 'version' => WPCODE_VERSION,
257 ),
258 wpcode()->library_auth->get_api_url( $endpoint )
259 );
260
261 $response = wp_remote_request( $url, $args );
262
263 $response_code = wp_remote_retrieve_response_code( $response );
264 if ( $response_code > 299 ) {
265 // Temporary error so cache for just 10 minutes and then try again.
266 return '';
267 }
268
269 return wp_remote_retrieve_body( $response );
270 }
271
272 /**
273 * Get the headers for making an authenticated request.
274 *
275 * @return array
276 */
277 public function get_authenticated_headers() {
278 // Build the headers of the request.
279 return array(
280 'Content-Type' => 'application/x-www-form-urlencoded',
281 'Cache-Control' => 'no-store, no-cache, must-revalidate, max-age=0, post-check=0, pre-check=0',
282 'Pragma' => 'no-cache',
283 'Expires' => 0,
284 'Origin' => site_url(),
285 'WPCode-Referer' => site_url(),
286 'WPCode-Sender' => 'WordPress',
287 'WPCode-Site' => esc_attr( get_option( 'blogname' ) ),
288 'WPCode-Version' => esc_attr( WPCODE_VERSION ),
289 'WPCode-Client-Id' => wpcode()->library_auth->get_client_id(),
290 'X-WPCode-ApiKey' => wpcode()->library_auth->get_auth_key(),
291 );
292 }
293
294 /**
295 * When we can't fetch from the server we save a temporary error => true file to avoid
296 * subsequent requests for a while. Returns a properly formatted array for frontend output.
297 *
298 * @param string $key The key used for storing the data in the cache.
299 *
300 * @return array
301 */
302 public function save_temporary_response_fail( $key ) {
303 $data = array(
304 'error' => true,
305 'time' => time(),
306 );
307 $this->save_to_cache( $key, $data );
308
309 return $this->get_empty_array();
310 }
311
312 /**
313 * Get an empty array for a consistent response.
314 *
315 * @return array[]
316 */
317 public function get_empty_array() {
318 return array(
319 'categories' => array(),
320 'snippets' => array(),
321 );
322 }
323
324 /**
325 * Save to cache.
326 *
327 * @param string $key The key used to store the data in the cache.
328 * @param array|mixed $data The data that will be stored.
329 *
330 * @return void
331 */
332 public function save_to_cache( $key, $data ) {
333 wpcode()->file_cache->set( $this->cache_folder . '/' . $key, $data );
334 }
335
336 /**
337 * Generic handler for grabbing data by slug. Either all categories or the category slug.
338 *
339 * @param string $data Response body from server.
340 *
341 * @return array
342 */
343 public function process_response( $data ) {
344 $response = json_decode( $data, true );
345 if ( ! is_array( $response ) || ! isset( $response['status'] ) || 'success' !== $response['status'] ) {
346 return array();
347 }
348 if ( ! isset( $response['data'] ) || ! is_array( $response['data'] ) ) {
349 return array();
350 }
351
352 return $response['data'];
353 }
354
355 /**
356 * Get a cache key for a specific snippet id.
357 *
358 * @param int $id The snippet id.
359 *
360 * @return string
361 */
362 public function get_snippet_cache_key( $id ) {
363 return $this->snippet_key . '_' . $id;
364 }
365
366 /**
367 * Create a new snippet by the library id.
368 * This grabs the snippet by its id from the snippet library site and creates
369 * a new snippet on the current site using the response.
370 *
371 * @param int $library_id The id of the snippet on the library site.
372 *
373 * @return false|WPCode_Snippet
374 */
375 public function create_new_snippet( $library_id ) {
376
377 $snippet_data = $this->grab_snippet_from_api( $library_id );
378
379 if ( ! $snippet_data ) {
380 return false;
381 }
382
383 $snippet_data = apply_filters( 'wpcode_library_import_snippet_data', $snippet_data );
384
385 $snippet = wpcode_get_snippet( $snippet_data );
386
387 $snippet_id = $snippet->save();
388
389 // Save the version information if available in the API response.
390 if ( ! empty( $snippet_data['version'] ) ) {
391 update_post_meta( $snippet_id, '_wpcode_snippet_version', $snippet_data['version'] );
392 }
393
394 // Save the originating library author if available in the API response.
395 if ( ! empty( $snippet_data['username'] ) ) {
396 update_post_meta( $snippet_id, '_wpcode_library_author', sanitize_key( $snippet_data['username'] ) );
397 }
398
399 delete_transient( $this->used_snippets_transient_key );
400
401 return $snippet;
402 }
403
404 /**
405 * Grab a snippet data from the API.
406 *
407 * @param int $library_id The id of the snippet in the Library api.
408 *
409 * @return array|array[]|false
410 */
411 public function grab_snippet_from_api( $library_id ) {
412 $snippet_request = $this->make_request( 'get/' . $library_id );
413 $snippet_data = $this->process_response( $snippet_request );
414
415 if ( empty( $snippet_data ) ) {
416 return false;
417 }
418
419 return $snippet_data;
420 }
421
422 /**
423 * Get all the snippets that were created from the library, by library ID.
424 * Results are cached in a transient.
425 *
426 * @return array
427 */
428 public function get_used_library_snippets() {
429
430 if ( isset( $this->library_snippets ) ) {
431 return $this->library_snippets;
432 }
433
434 $snippets_from_library = get_transient( $this->used_snippets_transient_key );
435
436 if ( false === $snippets_from_library ) {
437 $snippets_from_library = array();
438
439 $args = array(
440 'post_type' => wpcode_get_post_type(),
441 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
442 array(
443 'key' => $this->snippet_library_id_meta_key,
444 'compare' => 'EXISTS',
445 ),
446 ),
447 'fields' => 'ids',
448 'post_status' => 'any',
449 'nopaging' => true,
450 );
451 $snippets = get_posts( $args );
452
453 foreach ( $snippets as $snippet_id ) {
454 $snippets_from_library[ $this->get_snippet_library_id( $snippet_id ) ] = $snippet_id;
455 }
456
457 set_transient( $this->used_snippets_transient_key, $snippets_from_library );
458 }
459
460 $this->library_snippets = $snippets_from_library;
461
462 return $this->library_snippets;
463 }
464
465 /**
466 * Grab the library id from the snippet by snippet id.
467 *
468 * @param int $snippet_id The snippet id.
469 *
470 * @return int
471 */
472 public function get_snippet_library_id( $snippet_id ) {
473 return absint( get_post_meta( $snippet_id, '_wpcode_library_id', true ) );
474 }
475
476 /**
477 * Resolve which library username (author) a snippet came from.
478 *
479 * Returns the persisted `_wpcode_library_author` meta if present. For
480 * snippets imported before this meta key was tracked, falls back to the
481 * (per-request memoized) reverse lookup map and backfills the meta when
482 * a match is found. Returns the generic 'wpcode' marker when a snippet
483 * is library-linked but its origin can't be identified — without
484 * persisting it, so a later call in the same request (or a later
485 * render) can re-attempt once registered-author data becomes available.
486 *
487 * @param int $snippet_id The local snippet id.
488 *
489 * @return string The library username slug, or empty string if not from the library.
490 */
491 public function get_snippet_author( $snippet_id ) {
492 $library_id = $this->get_snippet_library_id( $snippet_id );
493 if ( empty( $library_id ) ) {
494 return '';
495 }
496
497 $stored = get_post_meta( $snippet_id, '_wpcode_library_author', true );
498 if ( ! empty( $stored ) ) {
499 return $stored;
500 }
501
502 $map = $this->get_library_author_lookup_map();
503 if ( isset( $map[ $library_id ] ) ) {
504 update_post_meta( $snippet_id, '_wpcode_library_author', $map[ $library_id ] );
505
506 return $map[ $library_id ];
507 }
508
509 return 'wpcode';
510 }
511
512 /**
513 * Reverse map of library_id => registered author username, built once
514 * per request so backfill scans don't repeat per row.
515 *
516 * @return array<int, string>
517 */
518 protected function get_library_author_lookup_map() {
519 static $map = null;
520 if ( null !== $map ) {
521 return $map;
522 }
523
524 $map = array();
525 foreach ( $this->get_library_usernames() as $username => $data ) {
526 $version = isset( $data['version'] ) ? $data['version'] : '';
527 $snippets = $this->get_snippets_by_username( $username, $version );
528 foreach ( ( isset( $snippets['snippets'] ) ? $snippets['snippets'] : array() ) as $snippet ) {
529 if ( isset( $snippet['library_id'] ) ) {
530 $map[ absint( $snippet['library_id'] ) ] = $username;
531 }
532 }
533 }
534
535 return $map;
536 }
537
538 /**
539 * Get the human readable label for a library author username.
540 *
541 * @param string $username The library username slug.
542 *
543 * @return string
544 */
545 public function get_author_label( $username ) {
546 if ( 'wpcode' === $username ) {
547 return __( 'WPCode', 'insert-headers-and-footers' );
548 }
549
550 $usernames = $this->get_library_usernames();
551
552 return isset( $usernames[ $username ]['label'] ) ? $usernames[ $username ]['label'] : $username;
553 }
554
555 /**
556 * When a snippet is trashed, clear the used snippets transients
557 * for this class instance to avoid confusion in the library.
558 *
559 * @return void
560 */
561 public function clear_used_snippets() {
562 delete_transient( $this->used_snippets_transient_key );
563 }
564
565 /**
566 * Clear used snippets also when a snippet is un-trashed.
567 *
568 * @param string $new_status The new post status.
569 * @param string $old_status The old post status.
570 * @param WP_Post $post The post object.
571 *
572 * @return void
573 */
574 public function clear_used_snippets_untrash( $new_status, $old_status, $post ) {
575 if ( 'wpcode' !== $post->post_type || 'trash' !== $old_status ) {
576 return;
577 }
578
579 $this->clear_used_snippets();
580 }
581
582 /**
583 * Delete the file cache for the snippets library.
584 *
585 * @return void
586 */
587 public function delete_cache() {
588 wpcode()->file_cache->delete( $this->cache_folder . '/' . $this->cache_key );
589 if ( isset( $this->data ) ) {
590 unset( $this->data );
591 }
592 }
593
594 /**
595 * Makes a request to the snippet library API to grab a public snippet by its hash.
596 *
597 * @param string $hash The hash used to identify the snippet on the library server.
598 * @param string $auth The unique user hash used to authenticate the request on the library.
599 *
600 * @return array
601 */
602 public function get_public_snippet( $hash, $auth ) {
603 // Let's use transients for hashes to avoid unnecessary requests.
604 $transient_key = 'wpcode_public_snippet_' . $hash . '_' . $auth;
605 $snippet_data = get_transient( $transient_key );
606 if ( false === $snippet_data ) {
607 $snippet_request = $this->make_request(
608 'public/' . $hash,
609 'POST',
610 array(
611 'auth' => $auth,
612 )
613 );
614 $snippet_data = json_decode( $snippet_request, true );
615 // Transient for 1 minute if error otherwise 30 minutes.
616 $timeout = ! isset( $snippet_data['status'] ) || 'error' === $snippet_data['status'] ? 60 : 30 * 60;
617 set_transient( $transient_key, $snippet_data, $timeout );
618 }
619
620 return $snippet_data;
621 }
622
623 /**
624 * Get snippets by username.
625 *
626 * @param string $username The username to grab data for.
627 * @param string $version The version of the library to grab data for.
628 *
629 * @return array
630 */
631 public function get_snippets_by_username( $username, $version = '' ) {
632
633 $username = sanitize_key( $username );
634
635 if ( empty( $version ) ) {
636 // Let's grab the version from the registered username if no version is explicitly passed.
637 $version = $this->get_version_by_username( $username );
638 }
639
640 if ( ! isset( $this->snippets_by_username[ $username ] ) ) {
641 $this->load_snippets_by_username( $username, $version );
642 }
643
644 return $this->snippets_by_username[ $username ];
645 }
646
647 /**
648 * Grab the version from the registered username array.
649 *
650 * @param string $username The username to grab version for.
651 *
652 * @return string
653 */
654 public function get_version_by_username( $username ) {
655 return isset( $this->library_usernames[ $username ] ) ? $this->library_usernames[ $username ]['version'] : '';
656 }
657
658 /**
659 * Load snippets in the current instance, either from cache or from the server.
660 *
661 * @param string $username The username to grab data for.
662 * @param string $version The version of the plugin/theme to grab data for.
663 *
664 * @return array
665 */
666 private function load_snippets_by_username( $username, $version ) {
667
668 $this->snippets_by_username[ $username ] = $this->get_from_cache( 'profile_' . $username );
669
670 if ( empty( $this->snippets_by_username[ $username ] ) || ! is_array( $this->snippets_by_username[ $username ] ) ) {
671 $this->snippets_by_username[ $username ] = $this->get_from_server_by_username( $username );
672 }
673
674 // Let's filter the loaded data to make sure no snippets aimed at older versions are loaded.
675 $this->snippets_by_username[ $username ] = $this->filter_snippets_by_version( $this->snippets_by_username[ $username ], $version );
676
677 return $this->data;
678 }
679
680 /**
681 * Go through all the snippets and if they have a maximum version set, remove them if the current version is higher.
682 *
683 * @param array $profile_data The snippets to filter.
684 * @param string $version The version to filter by.
685 *
686 * @return array
687 */
688 public function filter_snippets_by_version( $profile_data, $version ) {
689 // If we have no version, we can't filter anything.
690 if ( empty( $version ) || empty( $profile_data['snippets'] ) ) {
691 return $profile_data;
692 }
693
694 $filtered_snippets = array();
695 foreach ( $profile_data['snippets'] as $snippet ) {
696 if ( empty( $snippet['max_version'] ) || version_compare( $version, $snippet['max_version'], '<=' ) ) {
697 $filtered_snippets[] = $snippet;
698 }
699 }
700
701 $profile_data['snippets'] = $filtered_snippets;
702
703 return $profile_data;
704 }
705
706 /**
707 * Grab data from the WPCode library by username.
708 *
709 * @param string $username The username to grab data for.
710 *
711 * @return array|array[]
712 */
713 private function get_from_server_by_username( $username ) {
714 $data = $this->process_response( $this->make_request( 'profile/' . $username ) );
715
716 if ( empty( $data['snippets'] ) ) {
717 return $this->save_temporary_response_fail( 'profile_' . $username );
718 }
719
720 $this->save_to_cache( 'profile_' . $username, $data );
721
722 return $data;
723 }
724
725 /**
726 * Get a list of usernames that we should attempt to load data from the library for.
727 *
728 * @return array
729 */
730 public function get_library_usernames() {
731 return $this->library_usernames;
732 }
733
734 /**
735 * Add a method to allow other plugins to register usernames to load data for.
736 *
737 * @param string $username The public username on the WPCode Library.
738 * @param string $label The label to display in the WPCode library view.
739 * @param string $max_version The plugin/theme version, used for excluding snippets aimed at older plugin/theme versions.
740 *
741 * @return void
742 */
743 public function register_library_username( $username, $label = '', $max_version = '' ) {
744 $username = sanitize_key( $username );
745 if ( empty( $label ) ) {
746 $label = $username;
747 }
748
749 $this->library_usernames[ $username ] = array(
750 'label' => $label,
751 'version' => $max_version,
752 );
753 }
754
755 /**
756 * If there are usernames to load data for, add them to the data array.
757 *
758 * @return void
759 */
760 public function maybe_add_usernames_data() {
761 $usernames = $this->get_library_usernames();
762 if ( empty( $usernames ) ) {
763 return;
764 }
765
766 foreach ( $usernames as $username => $data ) {
767 $snippets = $this->get_snippets_by_username( $username, $data['version'] );
768 if ( ! empty( $snippets['snippets'] ) ) {
769 $this->data['categories'][] = array(
770 'slug' => $username,
771 'name' => $data['label'],
772 'count' => count( $snippets['snippets'] ),
773 );
774 // Append snippets to the $this->data['snippets'] array.
775 $this->data['snippets'] = array_merge( $this->data['snippets'], $snippets['snippets'] );
776 }
777 }
778 }
779
780 /**
781 * Get the URL to edit a snippet.
782 *
783 * @param int $snippet_id The snippet id.
784 *
785 * @return string
786 */
787 public function get_edit_snippet_url( $snippet_id ) {
788 return add_query_arg(
789 array(
790 'page' => 'wpcode-snippet-manager',
791 'snippet_id' => absint( $snippet_id ),
792 ),
793 admin_url( 'admin.php' )
794 );
795 }
796
797 /**
798 * Get a direct link to install a snippet by its library URL.
799 *
800 * @param int $snippet_library_id The snippet ID on the WPCode library.
801 *
802 * @return string
803 */
804 public function get_install_snippet_url( $snippet_library_id ) {
805 return wp_nonce_url(
806 add_query_arg(
807 array(
808 'snippet_library_id' => absint( $snippet_library_id ),
809 'page' => 'wpcode-library',
810 ),
811 admin_url( 'admin.php' )
812 ),
813 'wpcode_add_from_library'
814 );
815 }
816
817 /**
818 * Get just the snippets from usernames.
819 *
820 * @return array
821 */
822 public function get_username_snippets() {
823 $usernames = $this->get_library_usernames();
824
825 $snippets = array();
826 $categories = array();
827
828 foreach ( $usernames as $username => $data ) {
829 $username_snippets = $this->get_snippets_by_username( $username, $data['version'] );
830 if ( ! empty( $username_snippets['snippets'] ) ) {
831 $categories[] = array(
832 'slug' => $username,
833 'name' => $data['label'],
834 'count' => count( $username_snippets['snippets'] ),
835 );
836 // Append snippets to the $this->data['snippets'] array.
837 $snippets = array_merge( $snippets, $username_snippets['snippets'] );
838 }
839 }
840
841 return array(
842 'categories' => $categories,
843 'snippets' => $snippets,
844 );
845 }
846
847 /**
848 * Check if a snippet has an update available by comparing with cached data.
849 *
850 * @param int $snippet_id The snippet ID.
851 * @param string $library_id The library ID.
852 *
853 * @return bool|array False if no update, array with version info if update available.
854 */
855 public function check_snippet_update( $snippet_id, $library_id ) {
856 // Get current version from post meta.
857 $current_version = get_post_meta( $snippet_id, '_wpcode_snippet_version', true );
858
859 // For library snippets, get from library cache.
860 $cached_data = $this->get_data();
861 $library_snippet = null;
862 if ( ! empty( $cached_data['snippets'] ) ) {
863 foreach ( $cached_data['snippets'] as $snippet ) {
864 if ( isset( $snippet['library_id'] ) && absint( $snippet['library_id'] ) === absint( $library_id ) ) {
865 $library_snippet = $snippet;
866 break;
867 }
868 }
869 }
870
871 if ( ! $library_snippet || empty( $library_snippet['version'] ) ) {
872 return false;
873 }
874
875 $latest_version = $library_snippet['version'];
876
877 // If either version is empty, set it to 1.0.0.
878 if ( empty( $current_version ) ) {
879 $current_version = '1.0.0';
880 }
881
882 if ( empty( $latest_version ) ) {
883 $latest_version = '1.0.0';
884 }
885
886 // If latest version is greater than current version, update is available.
887 if ( version_compare( $latest_version, $current_version, '>' ) ) {
888 return array(
889 'current_version' => $current_version,
890 'latest_version' => $latest_version,
891 );
892 }
893
894 return false;
895 }
896
897
898 /**
899 * Update a snippet from the library.
900 *
901 * @param int $snippet_id The ID of the snippet to update.
902 * @param int $library_id The ID of the library snippet to fetch.
903 *
904 * @return array|false Array with success data or false on failure.
905 */
906 public function update_snippet_from_library( $snippet_id, $library_id ) {
907 // Get snippet data from library.
908 $library_snippet = $this->grab_snippet_from_api( $library_id );
909
910 if ( ! $library_snippet ) {
911 return false;
912 }
913
914 // Update snippet.
915 $library_snippet['id'] = $snippet_id;
916 $snippet = wpcode_get_snippet( $library_snippet );
917 $result = $snippet->save();
918
919 if ( ! $result ) {
920 return false;
921 }
922
923 // Update local version metadata.
924 if ( ! empty( $library_snippet['version'] ) ) {
925 update_post_meta( $snippet_id, '_wpcode_snippet_version', $library_snippet['version'] );
926 }
927
928 // Persist the originating library author from the fresh API response.
929 // This is a backfill opportunity for snippets imported before that meta
930 // key existed: a user clicking "Update Available" hits the same API
931 // endpoint as a new import, so the username is available here too.
932 if ( ! empty( $library_snippet['username'] ) ) {
933 update_post_meta( $snippet_id, '_wpcode_library_author', sanitize_key( $library_snippet['username'] ) );
934 }
935
936 return array(
937 'success' => true,
938 'version' => ! empty( $library_snippet['version'] ) ? $library_snippet['version'] : '',
939 );
940 }
941
942 /**
943 * Search snippets in the library by keyword.
944 *
945 * @param string $keyword The keyword to search for.
946 *
947 * @return array Array of matching snippets.
948 */
949 public function search_snippets( $keyword ) {
950 $data = $this->get_data();
951 $all_snippets = isset( $data['snippets'] ) ? $data['snippets'] : array();
952 $results = array();
953
954 if ( empty( $all_snippets ) || ! is_array( $all_snippets ) ) {
955 return $results;
956 }
957
958 foreach ( $all_snippets as $snippet ) {
959 $found = false;
960
961 // Search in title.
962 if ( isset( $snippet['title'] ) && stripos( $snippet['title'], $keyword ) !== false ) {
963 $found = true;
964 }
965
966 // Search in description/note.
967 if ( ! $found && isset( $snippet['note'] ) && stripos( $snippet['note'], $keyword ) !== false ) {
968 $found = true;
969 }
970
971 // Search in tags.
972 if ( ! $found && isset( $snippet['tags'] ) && is_array( $snippet['tags'] ) ) {
973 foreach ( $snippet['tags'] as $tag ) {
974 if ( stripos( $tag, $keyword ) !== false ) {
975 $found = true;
976 break;
977 }
978 }
979 }
980
981 if ( $found ) {
982 $results[] = $snippet;
983 }
984 }
985
986 return $results;
987 }
988
989 /**
990 * Get the list of snippets that have updates available.
991 * Checks on the fly using cached data.
992 *
993 * @return array
994 */
995 public function get_snippets_with_updates() {
996 // Get all snippets with library IDs.
997 $library_snippets = $this->get_used_library_snippets();
998
999 $snippets_with_updates = array();
1000
1001 // Check library snippets.
1002 foreach ( $library_snippets as $library_id => $snippet_id ) {
1003 $update_info = $this->check_snippet_update( $snippet_id, $library_id );
1004 if ( $update_info ) {
1005 $snippets_with_updates[] = $snippet_id;
1006 }
1007 }
1008
1009 /**
1010 * Filter the list of snippets with updates.
1011 * This allows other classes to add their snippets with updates to the results.
1012 *
1013 * @param array $snippets_with_updates The list of snippets with updates.
1014 */
1015 return apply_filters( 'wpcode_snippets_with_updates', $snippets_with_updates );
1016 }
1017 }
1018