PluginProbe
Code Engine – PHP Snippets, AI Functions & Automation for WordPress / 0.4.8
Code Engine – PHP Snippets, AI Functions & Automation for WordPress v0.4.8
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.4.8, at classes/modules/snippet.php

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