PluginProbe
Code Engine – PHP Snippets, AI Functions & Automation for WordPress / 0.5.6
Code Engine – PHP Snippets, AI Functions & Automation for WordPress v0.5.6
0.5.6 0.5.5 0.5.4 0.5.3 0.5.2 0.5.1 0.5.0 0.4.9 0.4.8 0.4.7 0.4.6 trunk 0.0.1 0.0.2 0.2.8 0.2.9 0.3.0 0.3.1 0.3.2 0.3.3 0.3.4 0.3.5 0.3.6 0.3.7 0.3.8 All 32 releases
code-engine / classes / modules / snippet.php

snippet.php in Code Engine – PHP Snippets, AI Functions & Automation for WordPress 0.5.6, at classes/modules/snippet.php

1,047 lines 36.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 class Meow_MWCODE_Modules_Snippet
3 {
4 private $table = 'mwcode_snippets';
5 private $option_functions = 'mwcode_functions';
6 private $option_intervals = 'mwcode_intervals';
7
8 private $wpdb = null;
9 private $db_check = false;
10 private $table_name = null;
11 private $core = null;
12
13 private $mwcode_db_snippet_version = '1.0';
14
15
16
17 public function __construct( $core = null )
18 {
19 global $wpdb;
20 $this->wpdb = $wpdb;
21 $this->table_name = $this->wpdb->prefix . $this->table;
22
23 if ( $core == null ) {
24 $this->core = new Meow_MWCODE_Core();
25 }
26
27 $this->core = $core;
28 }
29
30 #region Interval Snippets
31
32 public function get_scheduled( )
33 {
34
35 $interval_snippets = get_option( $this->option_intervals, [] );
36
37 return $interval_snippets;
38 }
39
40 public function create_or_update_interval_snippet( $params )
41 {
42 if ( $params['scope'] != 'scheduled' ) return;
43
44 $interval_snippets = get_option( $this->option_intervals, [] );
45
46 $snippetId = $params['id'];
47 $snippet = [
48 'snippetId' => $snippetId,
49 'hours' => $params['intervalHours'],
50 'minutes' => $params['intervalMinutes'],
51 ];
52
53 $snippetExists = false;
54 foreach ( $interval_snippets as $key => $existingSnippet ) {
55 if ( $existingSnippet['snippetId'] == $snippetId ) {
56 $interval_snippets[$key] = $snippet;
57 $snippetExists = true;
58 break;
59 }
60 }
61
62 if ( !$snippetExists ) {
63 $interval_snippets[] = $snippet;
64 }
65
66 update_option( $this->option_intervals, $interval_snippets );
67 }
68
69 public function delete_interval_snippet( $params )
70 {
71 $interval_snippets = get_option( $this->option_intervals, [] );
72
73 $snippetId = $params['id'];
74 $updatedSnippets = [];
75
76 foreach ( $interval_snippets as $existingSnippet ) {
77 if ( $existingSnippet['snippetId'] != $snippetId ) {
78 $updatedSnippets[] = $existingSnippet;
79 } else {
80 $hook = 'mwcode_execute_snippet_' . $existingSnippet['snippetId'];
81 $timestamp = wp_next_scheduled( $hook );
82 if ( $timestamp ) {
83 wp_unschedule_event( $timestamp, $hook );
84 }
85 }
86 }
87
88 update_option( $this->option_intervals, $updatedSnippets );
89 }
90
91 private function get_interval_snippets_data( &$snippets )
92 {
93 if ( isset( $snippets['id'] ) ) {
94 $this->get_interval_snippet_data( $snippets );
95 } else {
96 foreach ( $snippets as &$snippet ) {
97 $this->get_interval_snippet_data( $snippet );
98 }
99 }
100 }
101
102 private function get_interval_snippet_data( &$snippet )
103 {
104 if ( $snippet['scope'] != 'scheduled' ) {
105 return;
106 }
107
108 $interval_snippets = get_option( $this->option_intervals, [] );
109
110 $snippetId = $snippet['id'];
111 $snippet['intervalHours'] = '';
112 $snippet['intervalMinutes'] = '';
113
114 foreach ( $interval_snippets as $interval_snippet ) {
115 if ( $interval_snippet['snippetId'] == $snippetId ) {
116 $snippet['intervalHours'] = $interval_snippet['hours'];
117 $snippet['intervalMinutes'] = $interval_snippet['minutes'];
118 }
119 }
120 }
121
122
123 #endregion
124
125 #region Functions Snippets
126
127 private function get_function_snippet_data( &$snippet )
128 {
129 if ( $snippet['scope'] != 'function' ) {
130 return;
131 }
132
133 $functions_snippet = get_option( $this->option_functions, [] );
134
135 $snippetId = $snippet['id'];
136 $snippet['functionName'] = '';
137 $snippet['functionArgs'] = [];
138 $snippet['functionArgsDict'] = [];
139 $snippet['functionBehavior'] = '';
140 $snippet['functionTarget'] = 'PHP';
141 $snippet['functionMcp'] = false;
142
143 foreach ( $functions_snippet as $function_snippet ) {
144 if ( $function_snippet['snippetId'] == $snippetId ) {
145
146 $snippet['functionName'] = $function_snippet['name'];
147 $snippet['functionMcp'] = !empty( $function_snippet['mcp'] );
148 if ( !isset( $function_snippet['behavior'] ) || empty( $function_snippet['behavior'] ) ) {
149 $snippet['functionBehavior'] = 'dynamic';
150 }
151 else {
152 $snippet['functionBehavior'] = $function_snippet['behavior'];
153 }
154 if ( !isset( $function_snippet['target'] ) || empty( $function_snippet['target'] ) ) {
155 $snippet['functionTarget'] = 'PHP';
156 }
157 else {
158 $snippet['functionTarget'] = $function_snippet['target'];
159 }
160 foreach ( $function_snippet['args'] as $arg ) {
161 $snippet['functionArgs'][] = $arg['name'];
162 $snippet['functionArgsDict'][$arg['name']] = $arg;
163 }
164 }
165 }
166 }
167
168 public function get_function_snippets_data( &$snippets )
169 {
170 if ( isset( $snippets['id'] ) ) {
171 $this->get_function_snippet_data( $snippets );
172 } else {
173 foreach ( $snippets as &$snippet ) {
174 $this->get_function_snippet_data( $snippet );
175 }
176 }
177 }
178
179 public function delete_function_snippet( $params )
180 {
181 $functions_snippet = get_option( $this->option_functions, [] );
182
183 $snippetId = $params['id'];
184 $updatedSnippets = [];
185
186 foreach ( $functions_snippet as $existingSnippet ) {
187 if ( $existingSnippet['snippetId'] != $snippetId ) {
188 $updatedSnippets[] = $existingSnippet;
189 }
190 }
191
192 update_option( $this->option_functions, $updatedSnippets );
193 }
194
195 private function sanitize_function_snippet( $snippet )
196 {
197 // Add logic to make sure the function snippet is valid
198 // 1 - Make sure the function always has a behavior ( is none set it to "dynamic" )
199 if ( !isset( $snippet['behavior'] ) || empty( $snippet['behavior'] ) ) {
200 $snippet['behavior'] = 'dynamic';
201 }
202
203
204 return $snippet;
205 }
206
207 public function create_or_update_function_snippet( $params )
208 {
209 if ( $params['scope'] != 'function' ) return;
210
211 $functions_snippet = get_option( $this->option_functions, [] );
212
213 $snippetId = $params['id'];
214 $snippet = [
215 'snippetId' => $snippetId,
216 'active' => $params['active'],
217 'name' => $params['functionName'],
218 'behavior' => $params['functionBehavior'],
219 'desc' => $params['description'] ?? '',
220 'target' => $params['functionTarget'] ?? 'PHP',
221 'mcp' => !empty( $params['functionMcp'] ),
222 'args' => [],
223 ];
224
225 foreach ( $params['functionArgs'] as $argName ) {
226 if ( !empty( $argName ) ) {
227 $argData = $params['functionArgsDict'][$argName];
228 $snippet['args'][] = [
229 'name' => $argName,
230 'desc' => $argData['description'] ?? $argData['desc'] ?? '', // Support both 'description' and 'desc'
231 'default' => $argData['default'],
232 'required' => empty( $argData['default'] ),
233 'type' => $argData['type'],
234 ];
235 } else {
236 // TODO: The client-side sends an empty string when there is no argument.
237 // This has to be fixed on the client-side.
238 $this->core->log( '❌ ( Code Engine ) Empty argument name.' );
239 }
240 }
241
242 $snippet = $this->sanitize_function_snippet( $snippet );
243
244 $snippetExists = false;
245 foreach ( $functions_snippet as $key => $existingSnippet ) {
246 if ( $existingSnippet['snippetId'] == $snippetId ) {
247 $functions_snippet[$key] = $snippet;
248 $snippetExists = true;
249 break;
250 }
251 }
252
253 if ( !$snippetExists ) {
254 $functions_snippet[] = $snippet;
255 }
256
257 update_option( $this->option_functions, $functions_snippet );
258 }
259
260 public function get_functions_raw( )
261 {
262 return get_option( $this->option_functions, [] );
263 }
264
265 public function set_functions_raw( $functions )
266 {
267 return update_option( $this->option_functions, $functions );
268 }
269
270 public function get_functions( )
271 {
272 $functions = get_option( $this->option_functions, array( ) );
273
274 //TODO: Delete this later, it's just to make sure all functions have the "active" key, as it's new.
275 $needs_update = false;
276 $filtered_functions = [];
277
278
279
280 foreach ( $functions as $function ) {
281
282 if ( !array_key_exists( 'active', $function ) ) {
283 $snippet = $this->select_one( $function['snippetId'] );
284
285 if ( !empty( $snippet ) ) {
286 $function['active'] = $snippet['active'];
287 $this->core->log( '�
288 Function\'s related snippet found: ' . $function['snippetId'] . '. Active: ' . $function['active'] );
289 } else {
290 $this->core->log( '❌ Function\'s related snippet not found: ' . $function['snippetId'] . '. Disabling function.' );
291 $function['active'] = false;
292 }
293
294 $needs_update = true;
295 }
296
297 $filtered_functions[] = $function;
298 }
299
300 if ( $needs_update ) {
301 update_option( $this->option_functions, $filtered_functions );
302 }
303
304 $filtered_functions = array_filter( $filtered_functions, function ( $function ) {
305 return $function['snippetId'] !== null && $function['active'];
306 } );
307
308 // Reindex the keys
309 $filtered_functions = array_values( $filtered_functions );
310
311 return $filtered_functions;
312 }
313
314 public function set_functions( $functions )
315 {
316 update_option( $this->option_functions, $functions );
317 }
318
319
320 public function get_function( $id, $options = [] )
321 {
322 $functions = $this->get_functions( );
323
324 foreach ( $functions as $function ) {
325 if ( $function['snippetId'] == $id ) {
326
327 if ( array_key_exists( 'php_ready_args', $options ) && $options['php_ready_args'] === false ) {
328 $function['args'] = array_map( function ( $arg ) {
329 $arg['name'] = ltrim( $arg['name'], '$' );
330 return $arg;
331 }, $function['args'] );
332 }
333
334 return $function;
335 }
336 }
337 return null;
338 }
339
340 public function get_function_by_name( $name, $options = [] )
341 {
342 $functions = $this->get_functions( );
343
344 foreach ( $functions as $function ) {
345 if ( $function['name'] == $name ) {
346
347 if ( array_key_exists( 'php_ready_args', $options ) && $options['php_ready_args'] === false ) {
348 $function['args'] = array_map( function ( $arg ) {
349 $arg['name'] = ltrim( $arg['name'], '$' );
350 return $arg;
351 }, $function['args'] );
352 }
353
354 return $function;
355 }
356 }
357
358 return null;
359 }
360
361 /**
362 * Return the snippet IDs of every function that opted in to MCP exposure.
363 * Used by the "MCP" list filter and its count (functionMcp lives in the
364 * functions option, not a snippets table column, so it can't be queried in SQL).
365 *
366 * @return int[]
367 */
368 public function get_mcp_function_ids()
369 {
370 $functions = get_option( $this->option_functions, [] );
371 $ids = [];
372 foreach ( $functions as $fn ) {
373 if ( !empty( $fn['mcp'] ) && !empty( $fn['snippetId'] ) ) {
374 $ids[] = (int) $fn['snippetId'];
375 }
376 }
377 return $ids;
378 }
379
380 #endregion
381
382 #region Utilities
383
384 /**
385 * Validate the parameters with consistency with other snippets.
386 * For example, the endpoint should be unique.
387 *
388 * @param array $params
389 * @return void
390 * @throws Exception
391 */
392 public function validate( $params )
393 {
394 $valide_scopes = ['backend', 'frontend', 'function', 'persistent', 'scheduled', 'content_php', 'content_js'];
395 $errors = [];
396 if ( isset( $params['endpoint'] ) && !empty( $params['endpoint'] ) ) {
397 $query = $this->wpdb->prepare(
398 "SELECT * FROM $this->table_name where endpoint = %s",
399 $params['endpoint']
400 );
401 if ( isset( $params['id'] ) && !empty( $params['id'] ) ) {
402 $query .= $this->wpdb->prepare( " AND id <> %d", ( int ) $params['id'] );
403 }
404 $exists = ( int ) $this->wpdb->get_var( "SELECT COUNT( * ) FROM ( $query ) AS t" ) > 0;
405 if ( $exists ) {
406 $errors[] = __( 'The endpoint is already used.', 'code-engine' );
407 }
408 }
409
410
411 if ( isset( $params['scope'] ) && !empty( $params['scope'] ) ) {
412
413 $scopes = [
414 "global" => "persistent",
415 "front-end" => "frontend",
416 "admin" => "backend",
417 "content" => "content_php",
418 ];
419
420 if ( array_key_exists( $params['scope'], $scopes ) ) {
421 $params['scope'] = $scopes[$params['scope']];
422 }
423
424
425 if ( !empty( $params['tags'] ) ) {
426 $tags = is_array( $params['tags'] ) ? $params['tags'] : explode( ',', $params['tags'] );
427 // Add the current scope to tags and remove duplicates
428 $tags[] = $params['scope'];
429 $tags = array_unique( $tags );
430
431 // Filter tags to include only valid scopes or the current scope
432 $tags = array_filter( $tags, function ( $tag ) use ( $valide_scopes, $params ) {
433 return !in_array( $tag, $valide_scopes ) || $tag === $params['scope'];
434 } );
435
436 // Reset array keys and assign back to params
437 $params['tags'] = array_values( $tags );
438 } else {
439 $params['tags'] = $params['scope'];
440 }
441
442 if ( !in_array( $params['scope'], $valide_scopes ) ) {
443 $errors[] = sprintf( __( 'Invalid scope: %s', 'code-engine' ), $params['scope'] );
444 }
445 }
446
447 // Make sure that the function name declared in the are unique
448 if ( isset( $params['code'] ) && !empty( $params['code'] ) ) {
449 $is_updating = array_key_exists( 'update', $params ) && $params['update'] === true;
450 $function_data = $this->sanitize_and_check_functions( $params['code'], $is_updating );
451 if ( !$function_data['is_valid'] ) {
452 $errors = array_merge( $errors, $function_data['errors'] );
453 }
454 }
455
456 if ( isset( $params['scope'] ) && $params['scope'] === 'function' ) {
457 if ( !isset( $params['functionName'] ) || empty( $params['functionName'] ) ) {
458 $errors[] = __( 'Function name is required.', 'code-engine' );
459 }
460 }
461
462 if ( isset( $params['functionError'] ) && !empty( $params['functionError'] ) ) {
463 $errors[] = __( 'An error was not resolved. (' . $params['functionError'] . ')', 'code-engine' );
464 }
465
466 if ( !empty( $errors ) ) {
467 throw new Exception( "❌ Your code could not be saved because of the following issue( s ): \n\n" . implode( ', ', $errors ) );
468 }
469
470 return $params;
471 }
472
473 public function sanitize_and_check_functions( $code, $is_updating = false )
474 {
475 $errors = [];
476 $function_names = [];
477 $attributes = [];
478
479 // Regular expression to match function declarations
480 $pattern = '/function\s+(\w+)\s*\(/';
481
482 // Find all function declarations
483 preg_match_all( $pattern, $code, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER );
484
485 if ( !empty( $matches ) ) {
486 foreach ( $matches as $match ) {
487 $function_name = $match[1][0];
488 $start_pos = $match[0][1];
489
490 // Calculate line numbers and positions
491 $lines_before = substr_count( substr( $code, 0, $start_pos ), "\n" );
492 $line_start = strrpos( substr( $code, 0, $start_pos ), "\n" ) + 1;
493 $line_end = strpos( $code, "\n", $start_pos );
494 if ( $line_end === false ) $line_end = strlen( $code );
495
496 $attr = [
497 'startLine' => $lines_before + 1,
498 'startTokenPos' => $start_pos - $line_start,
499 'startFilePos' => $start_pos,
500 'endLine' => $lines_before + 1,
501 'endTokenPos' => $line_end - $line_start,
502 'endFilePos' => $line_end
503 ];
504
505 // Check if function already exists in PHP
506 if ( function_exists( $function_name ) && !$is_updating ) {
507 $errors[] = [
508 'message' => "Function '$function_name' is already declared in PHP.",
509 'attributes' => $attr
510 ];
511 }
512 // Check if function is already declared in this code block
513 elseif ( in_array( $function_name, $function_names ) ) {
514 $errors[] = [
515 'message' => "Function '$function_name' is declared multiple times in the provided code.",
516 'attributes' => $attr
517 ];
518 } else {
519 $function_names[] = $function_name;
520 }
521
522 $attributes[] = $attr;
523 }
524 }
525
526 return [
527 'is_valid' => empty( $errors ),
528 'errors' => $errors,
529 'function_names' => $function_names,
530 'attributes' => $attributes
531 ];
532 }
533
534 /**
535 * Format parameters for saving in the database
536 *
537 * @param array $params
538 * @return array
539 */
540 public function formatParamsForDatabase( $params )
541 {
542 // Gather the scope tags into the tags
543 $tags = null;
544 if ( isset( $params['tags'] ) ) {
545 if ( is_array( $params['tags'] ) ) {
546 $tags = array_map( function ( $tag ) {
547 return trim( $tag );
548 }, $params['tags'] );
549 } else {
550 $tags = array_map( function ( $tag ) {
551 return trim( $tag );
552 }, explode( ',', $params['tags'] ) );
553 }
554 }
555 if ( isset( $params['scope'] ) ) {
556 $tags = array_merge( $tags, is_array( $params['scope'] ) ? $params['scope'] : explode( ',', $params['scope'] ) );
557 }
558 if ( count( $tags ) > 0 ) {
559 $tags = array_filter( $tags, function ( $tag ) {
560 return $tag !== '';
561 } );
562 }
563 $params['tags'] = $tags ? implode( ',', $tags ) : '';
564 //$params['code'] = $this->sanitize_code( $params['code'] );
565 return $params;
566 }
567
568 /**
569 * Format parameters for the front-end
570 *
571 * @param array $params
572 * @return array
573 */
574 private function formatParamsForFront( $params )
575 {
576 // Separate the scope tags from the tags
577 $scopes = ['backend', 'frontend', 'function', 'persistent', 'scheduled', 'content_php', 'content_js'];
578
579 if ( isset( $params['tags'] ) && !empty( $params['tags'] ) ) {
580
581 $tags = array_map( function ( $tag ) use ( $scopes ) {
582 if ( in_array( $tag, $scopes ) ) {
583 return null;
584 }
585 return trim( $tag );
586 }, explode( ',', $params['tags'] ) );
587 $params['tags'] = array_filter( $tags, function ( $tag ) {
588 return $tag !== null;
589 } );
590 }
591 return $params;
592 }
593
594 public function stats()
595 {
596 $scopes = ['function', 'scheduled', 'global', 'content_php', 'content_js'];
597 $globalScopes = ['backend', 'frontend', 'persistent'];
598
599 $stats = [
600 'all' => 0,
601 'disabled' => $this->wpdb->get_var( "SELECT COUNT( * ) FROM $this->table_name WHERE active = 0" ),
602 ];
603
604 foreach ( $scopes as $scope ) {
605 if ( $scope === 'global' ) {
606 $globalScopeQuery = implode( "', '", array_map( 'esc_sql', $globalScopes ) );
607 $stats[$scope] = $this->wpdb->get_var( "SELECT COUNT( * ) FROM $this->table_name WHERE scope IN ('$globalScopeQuery')" );
608 } else {
609 $stats[$scope] = $this->wpdb->get_var( $this->wpdb->prepare( "SELECT COUNT( * ) FROM $this->table_name WHERE scope = %s", $scope ) );
610 }
611 $stats['all'] += $stats[$scope];
612 }
613
614 // Combined count for the "Content" filter (PHP + JS). Not added to 'all', already counted above.
615 $stats['content'] = (int) $stats['content_php'] + (int) $stats['content_js'];
616
617 // Functions exposed via MCP. A subset of 'function', so not added to 'all'.
618 $stats['mcp'] = count( $this->get_mcp_function_ids() );
619
620 return $stats;
621 }
622
623 public function import( )
624 {
625 $table = $this->wpdb->prefix . 'snippets';
626 $snippets = $this->wpdb->get_results( "SELECT * FROM $table", ARRAY_A );
627
628 if ( !$snippets ) {
629 return 0;
630 }
631
632 // Disable ( set active = 0 ) all the snippets in the old table
633 $this->wpdb->update( $table, ['active' => 0], ['active' => 1] );
634
635 foreach ( $snippets as $snippet ) {
636 $snippet['id'] = null; // Reset the ID so it will be inserted as a new snippet.
637 $snippet = $this->validate( $snippet );
638 $this->insert( $this->formatParamsForDatabase( $snippet ) );
639 }
640
641 return count( $snippets );
642 }
643
644 public function sanitize_code( $code )
645 {
646 $code = ltrim( $code );
647
648 $first_chats = substr( $code, 0, 5 );
649 if( $first_chats === '<?php' ) {
650 $code = substr( $code, 5 );
651 $code = ltrim( $code );
652 }
653
654 return $code;
655 }
656
657 public function delete_duplicates() {
658 // Delete snippets that have the same code and scope, keeping only the most recent one ( based on the updated column )
659 $query = "DELETE t1 FROM $this->table_name t1
660 INNER JOIN $this->table_name t2
661 WHERE t1.id < t2.id
662 AND t1.code = t2.code
663 AND t1.scope = t2.scope";
664
665 $this->wpdb->query( $query );
666
667 return $this->wpdb->rows_affected;
668 }
669
670 #endregion
671
672 #region Snippet CRUD
673
674 public function select( $offset, $limit, $filters, $sort )
675 {
676 if ( !$this->check_db() ) {
677 throw new Exception( __( 'Could not access the database.', 'code-engine' ) );
678 }
679
680 $list = [];
681 $offset = !empty( $offset ) ? intval( $offset ) : 0;
682 $limit = !empty( $limit ) ? intval( $limit ) : 10;
683 $filters = !empty( $filters ) ? $filters : [];
684 $sort = !empty( $sort ) ? $sort : ['accessor' => 'updated', 'by' => 'desc'];
685 $query = "SELECT * FROM $this->table_name";
686
687 // Filters
688 if ( is_array( $filters ) && count( $filters ) > 0 ) {
689 $where = [];
690
691 $freshFilters = [];
692 // Little trick that allows searching by tags using the snippet accessor
693 // And to have a global scope that will search for backend, frontend, and persistent
694 foreach ( $filters as $filter ) {
695 if ( $filter['accessor'] === 'snippet' ) {
696 //$freshFilters['accessor'] = 'tags';
697 $freshFilters[] = [ 'accessor' => 'tags', 'value' => $filter['value'] ];
698 }
699 else if ( $filter['accessor'] === 'scope' && $filter['value'] === 'global' ) {
700 $freshFilters[] = [ 'accessor' => 'scope', 'value' => ['backend', 'frontend', 'persistent'] ];
701 }
702 else if ( $filter['accessor'] === 'scope' && $filter['value'] === 'content' ) {
703 $freshFilters[] = [ 'accessor' => 'scope', 'value' => ['content_php', 'content_js'] ];
704 }
705 else {
706 $freshFilters[] = $filter;
707 }
708 }
709 $filters = $freshFilters;
710
711 foreach ( $filters as $filter ) {
712 if ( $filter['accessor'] === 'tags' ) {
713 $value = ( array )$filter['value'];
714
715 if ( count( $value ) === 0 ) {
716 continue;
717 }
718 $where_unit = [];
719 foreach ( $value as $tag ) {
720 if ( strpos( $tag, ',' ) !== false ) {
721 $tags = explode( ',', $tag );
722 $where_combination_unit = [];
723 foreach ( $tags as $t ) {
724 $where_combination_unit[] = "FIND_IN_SET( '{$t}', tags )";
725 }
726 $where_unit[] = '( ' . implode( ' AND ', $where_combination_unit ) . ' )';
727 continue;
728 }
729 $where_unit[] = "FIND_IN_SET( '{$tag}', tags )";
730 }
731 $where[] = '( ' . implode( ' OR ', $where_unit ) . ' )';
732 } elseif ( $filter['accessor'] === 'active' ) {
733 $value = esc_sql( $filter['value'] );
734 $where[] = $this->wpdb->prepare( "active = %d", $value );
735 } elseif ( $filter['accessor'] === 'endpoint' ) {
736 $where[] = boolval( $filter['value'] ) ? "endpoint <> ''" : "endpoint = ''";
737 } elseif ( $filter['accessor'] === 'scope' ) {
738 if ( $filter['value'] === 'mcp' ) {
739 // Not a scope: constrain to the functions opted in to MCP exposure.
740 $ids = $this->get_mcp_function_ids();
741 if ( empty( $ids ) ) {
742 $where[] = '1 = 0';
743 } else {
744 $where[] = 'id IN (' . implode( ',', array_map( 'intval', $ids ) ) . ')';
745 }
746 } else if ( is_array( $filter['value'] ) ) {
747 $scopes = array_map( function( $scope ) {
748 return esc_sql( $scope );
749 }, $filter['value'] );
750 $where[] = "scope IN ('" . implode( "', '", $scopes ) . "')";
751 } else if ( !empty( $filter['value'] ) ) {
752 $value = esc_sql( $filter['value'] );
753 $where[] = $this->wpdb->prepare( "scope = %s", $value );
754 }
755 }
756 }
757 if ( count( $where ) > 0 ) {
758 $query .= " WHERE " . implode( " AND ", $where );
759 }
760 }
761
762 // Count based on this query
763 $list['total'] = $this->wpdb->get_var( "SELECT COUNT( * ) FROM ( $query ) AS t" );
764
765 // Order by
766 $query .= " ORDER BY " . esc_sql( $sort['accessor'] ) . " " . esc_sql( $sort['by'] );
767
768 // Limits
769 if ( $limit > 0 ) {
770 $query .= " LIMIT $offset, $limit";
771 }
772
773 $list['data'] = array_map( function ( $snippet ) {
774 return $this->formatParamsForFront( $snippet );
775 }, $this->wpdb->get_results( $query, ARRAY_A ) );
776
777 $this->get_function_snippets_data( $list['data'] );
778 $this->get_interval_snippets_data( $list['data'] );
779
780 return $list;
781 }
782
783 public function select_tags( )
784 {
785 if ( !$this->check_db( ) ) {
786 throw new Exception( __( 'Could not access the database.', 'code-engine' ) );
787 }
788
789 $tags = [];
790 $query = "SELECT tags FROM $this->table_name";
791 $result = $this->wpdb->get_results( $query, ARRAY_A );
792 foreach ( $result as $row ) {
793 $tags = array_merge( $tags, explode( ',', $row['tags'] ) );
794 }
795 // Remove the scope tags: admin, front, once.
796 $tags = array_diff( $tags, ['backend', 'frontend', 'function', 'persistent', 'scheduled'] );
797 return array_values( array_unique( $tags ) );
798 }
799
800 public function select_one( $id, $options = [] )
801 {
802 if ( !$this->check_db( ) ) {
803 throw new Exception( __( 'Could not access the database.', 'code-engine' ) );
804 }
805
806 $query = "SELECT * FROM $this->table_name WHERE id = %s";
807 if ( isset( $options['active'] ) ) {
808 $query .= " AND active = " . ( $options['active'] ? '1' : '0' );
809 }
810
811 return $this->formatParamsForFront(
812 $this->wpdb->get_row(
813 $this->wpdb->prepare( $query, ( string ) $id ),
814 ARRAY_A
815 )
816 );
817 }
818
819 public function insert( $insert_data )
820 {
821 if ( !$this->check_db( ) ) {
822 throw new Exception( __( 'Could not access the database.', 'code-engine' ) );
823 }
824
825 $data = [];
826 $update_columns = array_keys( MWCODE_SNIPPET_COLUMNS );
827
828 foreach ( $update_columns as $column ) {
829 if ( isset( $insert_data[$column] ) ) {
830 $data[$column] = $insert_data[$column];
831 } else {
832 unset( $data[$column] ); // Remove it if it's empty, so it uses the default db value.
833 }
834 }
835
836 $data['created'] = date( 'Y-m-d H:i:s' );
837 $data['updated'] = date( 'Y-m-d H:i:s' );
838
839 $this->wpdb->insert( $this->table_name, $data );
840 $id = $this->wpdb->insert_id;
841 if ( !$id ) {
842 throw new Exception( __( 'Could not insert the snippet.', 'code-engine' ) );
843 }
844 return $id;
845 }
846
847 public function update( $update_data )
848 {
849 if ( !$this->check_db( ) ) {
850 throw new Exception( __( 'Could not access the database.', 'code-engine' ) );
851 }
852
853 $data = [];
854 $update_columns = array_keys( MWCODE_SNIPPET_COLUMNS );
855 foreach ( $update_columns as $column ) {
856 if ( isset( $update_data[$column] ) ) {
857 $data[$column] = $update_data[$column];
858 }
859 }
860 if ( count( $data ) === 0 ) {
861 throw new Exception( __( 'No data to update.', 'code-engine' ) );
862 }
863 $data['updated'] = date( 'Y-m-d H:i:s' );
864 $result = $this->wpdb->update( $this->table_name, $data, ['id' => $update_data['id']] );
865 if ( $result === false ) {
866 throw new Exception( __( 'Could not insert the snippet.', 'code-engine' ) );
867 }
868 return $result;
869 }
870
871 public function force_disable( $id )
872 {
873 if ( !$this->check_db( ) ) {
874 throw new Exception( __( 'Could not access the database.', 'code-engine' ) );
875 }
876
877 $result = $this->wpdb->update( $this->table_name, ['active' => 0], ['id' => $id] );
878 if ( $result === false ) {
879 throw new Exception( __( 'Could not disable the snippet.', 'code-engine' ) );
880 }
881 return $result;
882 }
883
884 public function delete( $delete_data )
885 {
886 if ( !$this->check_db( ) ) {
887 throw new Exception( __( 'Could not access the database.', 'code-engine' ) );
888 }
889
890 $result = $this->wpdb->delete( $this->table_name, ['id' => $delete_data['id']] );
891 if ( $result === false ) {
892 throw new Exception( __( 'Could not delete the snippet.', 'code-engine' ) );
893 }
894 return $result;
895 }
896
897 public function delete_all( )
898 {
899 if ( !$this->check_db( ) ) {
900 throw new Exception( __( 'Could not access the database.', 'code-engine' ) );
901 }
902
903 $result = $this->wpdb->query( "TRUNCATE TABLE $this->table_name" );
904 if ( $result === false ) {
905 throw new Exception( __( 'Could not delete all snippets.', 'code-engine' ) );
906 }
907 return $result;
908 }
909
910 #endregion
911
912 #region Database
913
914 function create_db( )
915 {
916 $this->core->log( '💾 ( Code Engine ) Creating Table: ' . $this->table_name );
917 try {
918 $charset_collate = $this->wpdb->get_charset_collate( );
919
920 $column_definitions = array_map( function ( $column_name, $column_definition ) {
921 return "$column_name $column_definition";
922 }, array_keys( MWCODE_SNIPPET_COLUMNS ), MWCODE_SNIPPET_COLUMNS );
923 $column_definitions = implode( ",\n", $column_definitions ) . ', PRIMARY KEY ( id )';
924
925 $sql = "CREATE TABLE $this->table_name ( $column_definitions ) $charset_collate;";
926 $this->core->log( '💾 ( Code Engine ) Create table request: ' . $sql );
927 require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
928 dbDelta( $sql );
929 } catch ( Exception $e ) {
930 $this->core->log( '💾 ( Code Engine ) Error creating Table: ' . $e->getMessage( ) );
931 }
932
933 add_option( 'mwcode_db_snippet_version', $this->mwcode_db_snippet_version );
934 }
935
936 function check_db( )
937 {
938 if ( $this->db_check ) {
939 return true;
940 }
941
942 if ( $this->does_table_exist( $this->table_name ) ) {
943 $this->check_columns( );
944 $this->db_check = true;
945 } else {
946 $this->create_db( );
947 $this->core->log( '💾 ( Code Engine ) Table created, checking if it was successful.' );
948 $this->db_check = $this->does_table_exist( $this->table_name );
949 }
950
951 return $this->db_check;
952 }
953
954 private function check_columns( )
955 {
956 $db_version = get_option( 'mwcode_db_snippet_version' );
957 if ( $db_version == $this->mwcode_db_snippet_version ) {
958 return;
959 }
960
961 $this->core->log( '💾 ( Code Engine ) Database version is ' . $db_version . ', upgrading to ' . $this->mwcode_db_snippet_version . '.' );
962
963 global $wpdb;
964 $table_name = $this->table_name;
965 $charset = $wpdb->get_charset_collate( );
966 $desired_columns = MWCODE_SNIPPET_COLUMNS;
967 $existing_columns = $wpdb->get_results( "DESCRIBE $table_name", ARRAY_A );
968
969 // Handle column removals
970 $columns_to_remove = array_diff( array_column( $existing_columns, 'Field' ), array_keys( $desired_columns ) );
971 if ( !empty( $columns_to_remove ) ) {
972 $remove_queries = array_map( function ( $column_name ) use ( $table_name ) {
973 return "DROP COLUMN $column_name";
974 }, $columns_to_remove );
975 $remove_query = "ALTER TABLE $table_name " . implode( ', ', $remove_queries );
976 $wpdb->query( $remove_query );
977 }
978
979 // Handle column additions and updates
980 $alter_queries = array( );
981 foreach ( $desired_columns as $column_name => $column_definition ) {
982 $existing_column = array_filter( $existing_columns, function ( $column ) use ( $column_name ) {
983 return $column['Field'] === $column_name;
984 } );
985
986 if ( empty( $existing_column ) ) {
987 $alter_queries[] = "ADD COLUMN $column_name $column_definition";
988 } else {
989 $existing_column = array_shift( $existing_column );
990 $existing_column_definition = $existing_column['Type'];
991 if ( $existing_column_definition !== $column_definition ) {
992 $alter_queries[] = "MODIFY COLUMN $column_name $column_definition";
993 }
994 }
995 }
996
997 if ( !empty( $alter_queries ) ) {
998 $alter_query = "ALTER TABLE $table_name " . implode( ', ', $alter_queries );
999 $wpdb->query( $alter_query );
1000 }
1001
1002 update_option( 'mwcode_db_snippet_version', $this->mwcode_db_snippet_version );
1003 }
1004
1005 private function does_table_exist( $table_name )
1006 {
1007
1008 $found = false;
1009 $table_name = strtolower( $table_name );
1010
1011 // Try the fast way first
1012 try {
1013 $query = "SHOW TABLES LIKE '{$table_name}'";
1014 $result = strtolower( $this->wpdb->get_var( $query ) );
1015
1016 $found = $result === $table_name;
1017 } catch ( Exception $e ) {
1018 $this->core->log( '💾 ( Code Engine ) Database Check 1 Error: ' . $e->getMessage( ) );
1019 }
1020
1021 // If not found, try the slow way
1022 if ( !$found ) {
1023 try {
1024 $query = "SHOW TABLES";
1025 $tables = $this->wpdb->get_results( $query, ARRAY_N );
1026 foreach ( $tables as $table ) {
1027 $result = strtolower( $table[0] );
1028 if ( $result === $table_name ) {
1029 $found = true;
1030 break;
1031 }
1032 }
1033 } catch ( Exception $e ) {
1034 $this->core->log( '💾 ( Code Engine ) Database Check 2 Error: ' . $e->getMessage( ) );
1035 }
1036 }
1037
1038 if ( !$found ) {
1039 $this->core->log( '💾 ( Code Engine ) Database table doesn\'t seem to exist.' );
1040 }
1041
1042 return $found;
1043 }
1044
1045 #endregion
1046 }
1047