PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.13.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.13.0
2.13.0 2.12.0 2.11.0 2.10.0 2.9.0 2.8.0 2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 All 54 releases
thinkrank / includes / admin / importers / class-abstract-plugin-exporter.php

class-abstract-plugin-exporter.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 2.13.0, at includes/admin/importers/class-abstract-plugin-exporter.php

756 lines 25.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Abstract Plugin Exporter
5 *
6 * Base class for all plugin-specific exporters. Provides shared orchestration
7 * logic for reading source data, normalizing it, and writing to wp_options
8 * via Snapshot_Store.
9 *
10 * @package ThinkRank\Admin\Importers
11 * @since 2.0.0
12 */
13
14 declare(strict_types=1);
15
16 namespace ThinkRank\Admin\Importers;
17
18 if (!defined('ABSPATH')) {
19 exit;
20 }
21
22 /**
23 * Abstract Plugin Exporter Class
24 *
25 * @since 2.0.0
26 */
27 abstract class Abstract_Plugin_Exporter {
28
29 /**
30 * Plugin slug identifier (e.g., 'yoast')
31 *
32 * @var string
33 */
34 protected string $plugin_slug;
35
36 /**
37 * Human-readable plugin name
38 *
39 * @var string
40 */
41 protected string $plugin_name;
42
43 /**
44 * Plugin file path for detection
45 *
46 * @var string
47 */
48 protected string $plugin_file;
49
50 /**
51 * Meta key prefix used by the source plugin
52 *
53 * @var string
54 */
55 protected string $meta_key_prefix;
56
57 /**
58 * WordPress option keys used by the source plugin for settings
59 *
60 * @var array
61 */
62 protected array $option_keys = [];
63
64 /**
65 * Number of records per chunk
66 *
67 * @var int
68 */
69 protected int $chunk_size = 100;
70
71 /**
72 * Raw number of rows the current page's paginated query returned, before any
73 * per-record filtering. Page methods that skip records (e.g. users with no
74 * migratable title/description) set this via the id helpers so export_chunk()
75 * can decide has_more from the fetched-row count, not the emitted count.
76 * Null means the page method didn't report one (1:1 methods) — fall back to
77 * the emitted count.
78 *
79 * @var int|null
80 */
81 protected ?int $last_page_row_count = null;
82
83 /**
84 * Ordered list of data types to export
85 */
86 private const EXPORT_TYPES = ['postmeta', 'termmeta', 'usermeta', 'redirections', '404_logs', 'settings'];
87
88 /**
89 * Detect whether this plugin's data exists in the database
90 *
91 * @return bool True if source data is detected
92 */
93 abstract public function detect(): bool;
94
95 /**
96 * Get available data types with their record counts
97 *
98 * @return array Associative array of type => count
99 */
100 abstract public function get_available_types(): array;
101
102 /**
103 * Export a page of post meta data
104 *
105 * @param int $page Page number (1-indexed)
106 * @return array Array of normalized records
107 */
108 abstract protected function export_postmeta_page(int $page): array;
109
110 /**
111 * Export a page of term meta data
112 *
113 * @param int $page Page number (1-indexed)
114 * @return array Array of normalized records
115 */
116 abstract protected function export_termmeta_page(int $page): array;
117
118 /**
119 * Export a page of user meta data
120 *
121 * @param int $page Page number (1-indexed)
122 * @return array Array of normalized records
123 */
124 abstract protected function export_usermeta_page(int $page): array;
125
126 /**
127 * Export global settings
128 *
129 * @return array Array with single settings record
130 */
131 abstract protected function export_settings(): array;
132
133 /**
134 * Export a page of redirections
135 *
136 * @param int $page Page number (1-indexed)
137 * @return array Array of normalized records
138 */
139 abstract protected function export_redirections_page(int $page): array;
140
141 /**
142 * Export a page of logged 404 hits.
143 *
144 * Concrete, not abstract: only source plugins that ship a 404 monitor have
145 * anything to hand over, so the default is "nothing to export" and the
146 * exporters that do (Rank Math) override it.
147 *
148 * @param int $page Page number (1-indexed)
149 * @return array Array of normalized records
150 */
151 protected function export_404_logs_page(int $page): array {
152 return [];
153 }
154
155 /**
156 * Records for a data type outside the fixed set above.
157 *
158 * Nothing to hand over by default. It exists so an exporter can gain a type
159 * (ThinkRank's own export lets Pro register its tables this way) without
160 * every subclass having to reimplement export_chunk()'s chunk writing and
161 * manifest bookkeeping.
162 *
163 * @since 2.2.0
164 *
165 * @param string $type Data type
166 * @param int $page Page number (1-indexed)
167 * @return array Records
168 */
169 protected function export_custom_type_page(string $type, int $page): array {
170 return [];
171 }
172
173 /**
174 * Capture the source plugin's Role Manager assignments: role slug => the
175 * capabilities that role holds whose name starts with $prefix.
176 *
177 * Shared by every exporter because role capabilities live on the roles
178 * themselves (`wp_user_roles`), not in any of the plugin's own options — so
179 * an exporter's raw option capture never covers them, whichever plugin it is.
180 *
181 * The administrator is skipped: ThinkRank's Capability_Manager never
182 * modifies it (it always passes via `manage_options`), so carrying its caps
183 * over would be meaningless.
184 *
185 * @param string $prefix Source capability prefix, e.g. 'rank_math_' or 'wpseo_'
186 * @return array<string,string[]> Role slug => granted capabilities
187 */
188 protected function extract_role_capabilities(string $prefix): array {
189 if (!function_exists('wp_roles') || $prefix === '') {
190 return [];
191 }
192
193 $captured = [];
194 foreach (wp_roles()->roles as $slug => $role) {
195 if ($slug === 'administrator' || empty($role['capabilities'])) {
196 continue;
197 }
198
199 $caps = [];
200 foreach ($role['capabilities'] as $cap => $granted) {
201 // Roles store revoked caps as `false`; only carry over grants.
202 if ($granted && strpos((string) $cap, $prefix) === 0) {
203 $caps[] = (string) $cap;
204 }
205 }
206
207 if (!empty($caps)) {
208 sort($caps);
209 $captured[(string) $slug] = $caps;
210 }
211 }
212
213 return $captured;
214 }
215
216 /**
217 * Convert plugin-specific template variables to literal values
218 *
219 * Accepts mixed because the input is another plugin's stored data, over
220 * which we have no schema guarantees. Rank Math in particular can hold
221 * booleans inside its options arrays where a template string is expected,
222 * and the `?? ''` at the call sites only guards against a MISSING key —
223 * a present-but-boolean value sailed straight into a string-typed
224 * parameter and fataled the whole migration at the snapshot step.
225 * Implementations MUST start with stringify_template_value().
226 *
227 * @param mixed $value Value potentially containing template variables
228 * @param int|null $post_id Post ID for context-specific variables
229 * @return string Converted string
230 */
231 abstract protected function convert_template_variables($value, ?int $post_id = null): string;
232
233 /**
234 * Coerce a foreign settings/meta value into a template string.
235 *
236 * Strings pass through; ints and floats are kept as their string form (a
237 * purely numeric title is odd but meaningful); everything else — booleans,
238 * arrays, objects, null — has no sensible reading as a template, so it
239 * becomes '', which downstream already treats as "not set" and replaces
240 * with defaults. Dropping garbage beats failing the migration over it.
241 *
242 * @param mixed $value Raw value from the source plugin's storage.
243 * @return string Usable template string, possibly ''.
244 */
245 final protected function stringify_template_value($value): string {
246 if (is_string($value)) {
247 return $value;
248 }
249
250 if (is_int($value) || is_float($value)) {
251 return (string) $value;
252 }
253
254 return '';
255 }
256
257 /**
258 * Placeholder a mapped token travels as while the rest of a template is
259 * resolved literally. A control character no source vocabulary uses, so
260 * no literal resolver matches, strips or splits on it.
261 */
262 private const TOKEN_MARK = "\x1A";
263
264 /**
265 * Convert a per-object (post or term) SEO value into ThinkRank variable
266 * tags wherever ThinkRank resolves the same thing per request, and into
267 * literal text everywhere else.
268 *
269 * A source title such as "%%title%% %%sep%% %%sitename%%" used to be
270 * resolved to literal text at import, so the site name, separator and
271 * post title were frozen as they stood that day: renaming the site or
272 * switching the separator left every imported title behind, and every
273 * exporter wrote " - " whatever separator the source used. Mapped tokens
274 * now become `%title%`, `%sep%`, `%sitename%`… — the tags the metabox
275 * inserts and Pattern_Resolver resolves on every output path — and only
276 * tokens ThinkRank has no per-object equivalent for are resolved here.
277 *
278 * Mapped tokens are masked before `$resolve_rest` runs, so a literal
279 * resolver that knows them (Rank Math's shares ThinkRank's syntax) never
280 * sees them, and unmasked afterwards. A value with no mapped token is
281 * returned exactly as `$resolve_rest` produces it.
282 *
283 * @param string $value Raw source value (already stringified)
284 * @param array $token_map Regex matching a source token => ThinkRank tag
285 * @param callable $resolve_rest fn(string): string — the exporter's literal resolver
286 * @return string ThinkRank value
287 */
288 final protected function tokenize_object_template(string $value, array $token_map, callable $resolve_rest): string {
289 $tags = [];
290 $masked = $value;
291
292 if ($value !== '') {
293 foreach ($token_map as $pattern => $tag) {
294 $masked = (string) preg_replace_callback(
295 $pattern,
296 static function () use (&$tags, $tag): string {
297 $index = array_search($tag, $tags, true);
298 if ($index === false) {
299 $tags[] = $tag;
300 $index = count($tags) - 1;
301 }
302
303 return self::TOKEN_MARK . $index . self::TOKEN_MARK;
304 },
305 $masked
306 );
307 }
308 }
309
310 if (empty($tags)) {
311 return (string) $resolve_rest($value);
312 }
313
314 $resolved = (string) preg_replace_callback(
315 '/' . self::TOKEN_MARK . '(\d+)' . self::TOKEN_MARK . '/',
316 static fn(array $m): string => $tags[(int) $m[1]] ?? '',
317 (string) $resolve_rest($masked)
318 );
319
320 // Tokens resolved to '' (e.g. %%page%%) leave separators with nothing
321 // between or around them; Pattern_Resolver tidies those too, but the
322 // stored value should read cleanly in the metabox.
323 $resolved = trim((string) preg_replace('/\s+/', ' ', $resolved));
324 $resolved = (string) preg_replace('/%sep%(?:\s*%sep%)+/', '%sep%', $resolved);
325 $resolved = (string) preg_replace('/^(?:%sep%\s*)+|(?:\s*%sep%)+$/', '', $resolved);
326
327 return trim($resolved);
328 }
329
330 /**
331 * Token map for tokenize_object_template() from literal source tokens
332 * (`%%title%%`, `%title%`), matched case-insensitively.
333 *
334 * @param array<string,string> $map Source token => ThinkRank tag
335 * @return array<string,string> Regex => ThinkRank tag
336 */
337 final protected static function literal_token_patterns(array $map): array {
338 $patterns = [];
339 foreach ($map as $token => $tag) {
340 $patterns['/' . preg_quote((string) $token, '/') . '/i'] = $tag;
341 }
342
343 return $patterns;
344 }
345
346 /**
347 * Token map for tokenize_object_template() from brace-style variable
348 * names (`title` matches `{{title}}` and `{{ title }}`).
349 *
350 * @param array<string,string> $map Variable name => ThinkRank tag
351 * @return array<string,string> Regex => ThinkRank tag
352 */
353 final protected static function brace_token_patterns(array $map): array {
354 $patterns = [];
355 foreach ($map as $name => $tag) {
356 $patterns['/\{\{\s*' . preg_quote((string) $name, '/') . '\s*\}\}/i'] = $tag;
357 }
358
359 return $patterns;
360 }
361
362 /**
363 * Export a chunk of data and write to snapshot
364 *
365 * This is the main orchestration method. It calls the appropriate
366 * export_*_page() method, writes the chunk via Snapshot_Store, and
367 * updates the manifest.
368 *
369 * @param string $type Data type to export
370 * @param int $page Page number (1-indexed)
371 * @return array Result with status, has_more, page, total, exported
372 */
373 public function export_chunk(string $type, int $page): array {
374 // Reset before the page method runs; it (or its id helper) records the
375 // raw fetched-row count here.
376 $this->last_page_row_count = null;
377
378 switch ($type) {
379 case 'postmeta':
380 $records = $this->export_postmeta_page($page);
381 break;
382 case 'termmeta':
383 $records = $this->export_termmeta_page($page);
384 break;
385 case 'usermeta':
386 $records = $this->export_usermeta_page($page);
387 break;
388 case 'settings':
389 $records = $this->export_settings();
390 break;
391 case 'redirections':
392 $records = $this->export_redirections_page($page);
393 break;
394 case '404_logs':
395 $records = $this->export_404_logs_page($page);
396 break;
397 default:
398 $records = $this->export_custom_type_page($type, $page);
399 }
400
401 $exported_count = count($records);
402
403 // Write chunk to snapshot store
404 if ($exported_count > 0) {
405 Snapshot_Store::write_chunk($this->plugin_slug, $type, $page, $records);
406 }
407
408 // Determine if there are more pages from the number of rows the paginated
409 // query returned, NOT the emitted count. A page method may fetch a full
410 // chunk_size of rows but emit fewer after filtering (e.g. users without a
411 // migratable title/description); keying has_more off the emitted count
412 // would halt pagination early and silently skip later pages. Fall back to
413 // the emitted count for 1:1 page methods that don't report a row count.
414 $fetched_count = $this->last_page_row_count ?? $exported_count;
415 $has_more = $fetched_count >= $this->chunk_size && $type !== 'settings';
416
417 // Get total count for this type
418 $types = $this->get_available_types();
419 $total = $types[$type] ?? 0;
420
421 // Update manifest
422 $this->update_manifest($type, $page, $exported_count, $has_more, $total);
423
424 return [
425 'status' => $has_more ? 'processing' : 'complete',
426 'message' => sprintf(
427 'Exported %d %s records (page %d)',
428 $exported_count,
429 $type,
430 $page
431 ),
432 'has_more' => $has_more,
433 'page' => $page,
434 'total' => $total,
435 'exported' => $exported_count,
436 ];
437 }
438
439 /**
440 * Update the snapshot manifest after writing a chunk
441 *
442 * @param string $type Data type
443 * @param int $page Current page
444 * @param int $count Records in this chunk
445 * @param bool $has_more Whether more pages remain
446 * @param int $total Total records for this type
447 * @return void
448 */
449 private function update_manifest(string $type, int $page, int $count, bool $has_more, int $total): void {
450 $manifest = Snapshot_Store::get_manifest($this->plugin_slug) ?? [
451 'plugin' => $this->plugin_slug,
452 'plugin_name' => $this->plugin_name,
453 'exported_at' => gmdate('c'),
454 'version' => '1.0',
455 'types' => [],
456 'status' => 'exporting',
457 'last_migrated' => null,
458 'migration_version' => null,
459 ];
460
461 // Update type info
462 if (!isset($manifest['types'][$type])) {
463 $manifest['types'][$type] = [
464 'total_records' => $total,
465 'total_chunks' => 0,
466 ];
467 }
468
469 $manifest['types'][$type]['total_chunks'] = $page;
470 $manifest['types'][$type]['total_records'] = $total;
471 $manifest['status'] = 'exporting';
472 $manifest['exported_at'] = gmdate('c');
473
474 Snapshot_Store::write_manifest($this->plugin_slug, $manifest);
475 }
476
477 /**
478 * Mark the export as complete in the manifest
479 *
480 * Called by the controller after all types have been exported.
481 *
482 * @return void
483 */
484 public function finalize_export(): void {
485 $manifest = Snapshot_Store::get_manifest($this->plugin_slug);
486 if ($manifest) {
487 $manifest['status'] = 'complete';
488 $manifest['exported_at'] = gmdate('c');
489 Snapshot_Store::write_manifest($this->plugin_slug, $manifest);
490 }
491 }
492
493 /**
494 * Normalize robots directives from any plugin format to standard 0/1 values
495 *
496 * Handles:
497 * - Integer/string 1/0 (Yoast noindex/nofollow)
498 * - Serialized array containing 'noindex'/'nofollow' strings (Rank Math)
499 * - Boolean true/false (AIOSEO)
500 * - String 'yes' for noindex (SEOPress inverted logic — caller must handle inversion)
501 *
502 * @param mixed $noindex_value Raw noindex value from source
503 * @param mixed $nofollow_value Raw nofollow value from source
504 * @return array ['noindex' => 0|1, 'nofollow' => 0|1]
505 */
506 protected function normalize_robots($noindex_value, $nofollow_value = null): array {
507 $result = ['noindex' => 0, 'nofollow' => 0];
508
509 // Handle serialized array (Rank Math stores robots as serialized array)
510 if (is_string($noindex_value) && is_serialized($noindex_value)) {
511 $noindex_value = Safe_Unserializer::unserialize($noindex_value);
512 }
513
514 if (is_array($noindex_value)) {
515 // Rank Math format: serialized array with 'noindex', 'nofollow' as values
516 $result['noindex'] = in_array('noindex', $noindex_value, true) ? 1 : 0;
517 $result['nofollow'] = in_array('nofollow', $noindex_value, true) ? 1 : 0;
518 return $result;
519 }
520
521 // Handle individual values
522 $result['noindex'] = $this->normalize_bool_value($noindex_value);
523
524 if ($nofollow_value !== null) {
525 $result['nofollow'] = $this->normalize_bool_value($nofollow_value);
526 }
527
528 return $result;
529 }
530
531 /**
532 * Normalize a value to 0 or 1
533 *
534 * @param mixed $value Value to normalize
535 * @return int 0 or 1
536 */
537 private function normalize_bool_value($value): int {
538 if ($value === null || $value === '' || $value === false) {
539 return 0;
540 }
541
542 if (is_bool($value)) {
543 return $value ? 1 : 0;
544 }
545
546 return (int) $value ? 1 : 0;
547 }
548
549 /**
550 * Get paginated post IDs that have meta keys with the plugin's prefix
551 *
552 * @param int $page Page number (1-indexed)
553 * @return array Array of post IDs
554 */
555 protected function get_post_ids_with_meta(int $page): array {
556 global $wpdb;
557
558 $offset = ($page - 1) * $this->chunk_size;
559 $post_types = $this->get_exportable_post_types();
560
561 // Defensive: a site with no viewable post types has nothing to export.
562 if (empty($post_types)) {
563 return [];
564 }
565
566 $placeholders = implode(', ', array_fill(0, count($post_types), '%s'));
567
568 // Restrict to publicly-viewable post types so WordPress-internal objects
569 // (oembed_cache, revisions, nav menu items, block/template CPTs, …) never
570 // enter the snapshot — SEO meta left on them is noise. Filtering in the
571 // query (rather than per-record) keeps the chunk-size based has_more
572 // pagination in export_chunk() accurate.
573 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- table name is $wpdb->prefix plus a literal, and every value is passed as a placeholder replacement.
574 $sql = $wpdb->prepare(
575 "SELECT DISTINCT pm.post_id
576 FROM {$wpdb->postmeta} pm
577 INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
578 WHERE pm.meta_key LIKE %s
579 AND p.post_type IN ({$placeholders})
580 ORDER BY pm.post_id ASC
581 LIMIT %d OFFSET %d",
582 array_merge(
583 [$wpdb->esc_like($this->meta_key_prefix) . '%'],
584 $post_types,
585 [$this->chunk_size, $offset]
586 )
587 );
588 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
589
590 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- table name is $wpdb->prefix plus a literal, and every value is passed as a placeholder replacement.
591 $ids = $wpdb->get_col($sql);
592 $this->last_page_row_count = count($ids);
593
594 return $ids;
595 }
596
597 /**
598 * Publicly-viewable post types whose meta is worth exporting.
599 *
600 * Excludes WordPress-internal types (oembed_cache, revision, nav_menu_item,
601 * wp_* block/template CPTs) that are never served to visitors and therefore
602 * carry no meaningful SEO data.
603 *
604 * @return string[] Post type slugs
605 */
606 protected function get_exportable_post_types(): array {
607 return array_values(
608 array_filter(get_post_types([], 'names'), 'is_post_type_viewable')
609 );
610 }
611
612 /**
613 * Get paginated term IDs that have meta keys with the plugin's prefix
614 *
615 * @param int $page Page number (1-indexed)
616 * @return array Array of term IDs
617 */
618 protected function get_term_ids_with_meta(int $page): array {
619 global $wpdb;
620
621 $offset = ($page - 1) * $this->chunk_size;
622
623 $ids = $wpdb->get_col(
624 $wpdb->prepare(
625 "SELECT DISTINCT term_id FROM {$wpdb->termmeta} WHERE meta_key LIKE %s ORDER BY term_id ASC LIMIT %d OFFSET %d",
626 $wpdb->esc_like($this->meta_key_prefix) . '%',
627 $this->chunk_size,
628 $offset
629 )
630 );
631 $this->last_page_row_count = count($ids);
632
633 return $ids;
634 }
635
636 /**
637 * Get paginated user IDs that have meta keys with the plugin's prefix
638 *
639 * @param int $page Page number (1-indexed)
640 * @return array Array of user IDs
641 */
642 protected function get_user_ids_with_meta(int $page): array {
643 global $wpdb;
644
645 $offset = ($page - 1) * $this->chunk_size;
646
647 $ids = $wpdb->get_col(
648 $wpdb->prepare(
649 "SELECT DISTINCT user_id FROM {$wpdb->usermeta} WHERE meta_key LIKE %s ORDER BY user_id ASC LIMIT %d OFFSET %d",
650 $wpdb->esc_like($this->meta_key_prefix) . '%',
651 $this->chunk_size,
652 $offset
653 )
654 );
655 $this->last_page_row_count = count($ids);
656
657 return $ids;
658 }
659
660 /**
661 * Get all meta values for a user with the plugin's prefix
662 *
663 * @param int $user_id User ID
664 * @return array Associative array of meta_key => meta_value
665 */
666 protected function get_all_plugin_user_meta(int $user_id): array {
667 global $wpdb;
668
669 $results = $wpdb->get_results(
670 $wpdb->prepare(
671 "SELECT meta_key, meta_value FROM {$wpdb->usermeta} WHERE user_id = %d AND meta_key LIKE %s",
672 $user_id,
673 $wpdb->esc_like($this->meta_key_prefix) . '%'
674 ),
675 ARRAY_A
676 );
677
678 $meta = [];
679 foreach ($results as $row) {
680 $meta[$row['meta_key']] = $row['meta_value'];
681 }
682
683 return $meta;
684 }
685
686 /**
687 * Get all meta values for a post with the plugin's prefix
688 *
689 * @param int $post_id Post ID
690 * @return array Associative array of meta_key => meta_value
691 */
692 protected function get_all_plugin_meta(int $post_id): array {
693 global $wpdb;
694
695 $results = $wpdb->get_results(
696 $wpdb->prepare(
697 "SELECT meta_key, meta_value FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key LIKE %s",
698 $post_id,
699 $wpdb->esc_like($this->meta_key_prefix) . '%'
700 ),
701 ARRAY_A
702 );
703
704 $meta = [];
705 foreach ($results as $row) {
706 $meta[$row['meta_key']] = $row['meta_value'];
707 }
708
709 return $meta;
710 }
711
712 /**
713 * Get all meta values for a term with the plugin's prefix
714 *
715 * @param int $term_id Term ID
716 * @return array Associative array of meta_key => meta_value
717 */
718 protected function get_all_plugin_term_meta(int $term_id): array {
719 global $wpdb;
720
721 $results = $wpdb->get_results(
722 $wpdb->prepare(
723 "SELECT meta_key, meta_value FROM {$wpdb->termmeta} WHERE term_id = %d AND meta_key LIKE %s",
724 $term_id,
725 $wpdb->esc_like($this->meta_key_prefix) . '%'
726 ),
727 ARRAY_A
728 );
729
730 $meta = [];
731 foreach ($results as $row) {
732 $meta[$row['meta_key']] = $row['meta_value'];
733 }
734
735 return $meta;
736 }
737
738 /**
739 * Get the plugin slug
740 *
741 * @return string
742 */
743 public function get_plugin_slug(): string {
744 return $this->plugin_slug;
745 }
746
747 /**
748 * Get the plugin name
749 *
750 * @return string
751 */
752 public function get_plugin_name(): string {
753 return $this->plugin_name;
754 }
755 }
756