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

963 lines 33.5 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( $snippet['functionBehavior'] ) || empty( $snippet['functionBehavior'] ) ) {
147 $snippet['functionBehavior'] = 'dynamic';
148 }
149 else {
150 $snippet['functionBehavior'] = $function_snippet['behavior'];
151 }
152 if ( !isset( $snippet['functionTarget'] ) || empty( $snippet['functionTarget'] ) ) {
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['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 #endregion
338
339 #region Utilities
340
341 /**
342 * Validate the parameters with consistency with other snippets.
343 * For example, the endpoint should be unique.
344 *
345 * @param array $params
346 * @return void
347 * @throws Exception
348 */
349 public function validate( $params )
350 {
351 $valide_scopes = ['backend', 'frontend', 'function', 'persistent', 'scheduled'];
352 $errors = [];
353 if ( isset( $params['endpoint'] ) && !empty( $params['endpoint'] ) ) {
354 $query = $this->wpdb->prepare(
355 "SELECT * FROM $this->table_name where endpoint = %s",
356 $params['endpoint']
357 );
358 if ( isset( $params['id'] ) && !empty( $params['id'] ) ) {
359 $query .= $this->wpdb->prepare( " AND id <> %d", ( int ) $params['id'] );
360 }
361 $exists = ( int ) $this->wpdb->get_var( "SELECT COUNT( * ) FROM ( $query ) AS t" ) > 0;
362 if ( $exists ) {
363 $errors[] = __( 'The endpoint is already used.', 'code-engine' );
364 }
365 }
366
367
368 if ( isset( $params['scope'] ) && !empty( $params['scope'] ) ) {
369
370 $scopes = [
371 "global" => "persistent",
372 "front-end" => "frontend",
373 "admin" => "backend",
374 ];
375
376 if ( array_key_exists( $params['scope'], $scopes ) ) {
377 $params['scope'] = $scopes[$params['scope']];
378 }
379
380
381 if ( !empty( $params['tags'] ) ) {
382 $tags = is_array( $params['tags'] ) ? $params['tags'] : explode( ',', $params['tags'] );
383 // Add the current scope to tags and remove duplicates
384 $tags[] = $params['scope'];
385 $tags = array_unique( $tags );
386
387 // Filter tags to include only valid scopes or the current scope
388 $tags = array_filter( $tags, function ( $tag ) use ( $valide_scopes, $params ) {
389 return !in_array( $tag, $valide_scopes ) || $tag === $params['scope'];
390 } );
391
392 // Reset array keys and assign back to params
393 $params['tags'] = array_values( $tags );
394 } else {
395 $params['tags'] = $params['scope'];
396 }
397
398 if ( !in_array( $params['scope'], $valide_scopes ) ) {
399 $errors[] = sprintf( __( 'Invalid scope: %s', 'code-engine' ), $params['scope'] );
400 }
401 }
402
403 // Make sure that the function name declared in the are unique
404 if ( isset( $params['code'] ) && !empty( $params['code'] ) ) {
405 $is_updating = array_key_exists( 'update', $params ) && $params['update'] === true;
406 $function_data = $this->sanitize_and_check_functions( $params['code'], $is_updating );
407 if ( !$function_data['is_valid'] ) {
408 $errors = array_merge( $errors, $function_data['errors'] );
409 }
410 }
411
412 if ( isset( $params['scope'] ) && $params['scope'] === 'function' ) {
413 if ( !isset( $params['functionName'] ) || empty( $params['functionName'] ) ) {
414 $errors[] = __( 'Function name is required.', 'code-engine' );
415 }
416 }
417
418 if ( isset( $params['functionError'] ) && !empty( $params['functionError'] ) ) {
419 $errors[] = __( 'An error was not resolved. (' . $params['functionError'] . ')', 'code-engine' );
420 }
421
422 if ( !empty( $errors ) ) {
423 throw new Exception( "❌ Your code could not be saved because of the following issue( s ): \n\n" . implode( ', ', $errors ) );
424 }
425
426 return $params;
427 }
428
429 public function sanitize_and_check_functions( $code, $is_updating = false )
430 {
431 $errors = [];
432 $function_names = [];
433 $attributes = [];
434
435 // Regular expression to match function declarations
436 $pattern = '/function\s+(\w+)\s*\(/';
437
438 // Find all function declarations
439 preg_match_all( $pattern, $code, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER );
440
441 if ( !empty( $matches ) ) {
442 foreach ( $matches as $match ) {
443 $function_name = $match[1][0];
444 $start_pos = $match[0][1];
445
446 // Calculate line numbers and positions
447 $lines_before = substr_count( substr( $code, 0, $start_pos ), "\n" );
448 $line_start = strrpos( substr( $code, 0, $start_pos ), "\n" ) + 1;
449 $line_end = strpos( $code, "\n", $start_pos );
450 if ( $line_end === false ) $line_end = strlen( $code );
451
452 $attr = [
453 'startLine' => $lines_before + 1,
454 'startTokenPos' => $start_pos - $line_start,
455 'startFilePos' => $start_pos,
456 'endLine' => $lines_before + 1,
457 'endTokenPos' => $line_end - $line_start,
458 'endFilePos' => $line_end
459 ];
460
461 // Check if function already exists in PHP
462 if ( function_exists( $function_name ) && !$is_updating ) {
463 $errors[] = [
464 'message' => "Function '$function_name' is already declared in PHP.",
465 'attributes' => $attr
466 ];
467 }
468 // Check if function is already declared in this code block
469 elseif ( in_array( $function_name, $function_names ) ) {
470 $errors[] = [
471 'message' => "Function '$function_name' is declared multiple times in the provided code.",
472 'attributes' => $attr
473 ];
474 } else {
475 $function_names[] = $function_name;
476 }
477
478 $attributes[] = $attr;
479 }
480 }
481
482 return [
483 'is_valid' => empty( $errors ),
484 'errors' => $errors,
485 'function_names' => $function_names,
486 'attributes' => $attributes
487 ];
488 }
489
490 /**
491 * Format parameters for saving in the database
492 *
493 * @param array $params
494 * @return array
495 */
496 public function formatParamsForDatabase( $params )
497 {
498 // Gather the scope tags into the tags
499 $tags = null;
500 if ( isset( $params['tags'] ) ) {
501 if ( is_array( $params['tags'] ) ) {
502 $tags = array_map( function ( $tag ) {
503 return trim( $tag );
504 }, $params['tags'] );
505 } else {
506 $tags = array_map( function ( $tag ) {
507 return trim( $tag );
508 }, explode( ',', $params['tags'] ) );
509 }
510 }
511 if ( isset( $params['scope'] ) ) {
512 $tags = array_merge( $tags, is_array( $params['scope'] ) ? $params['scope'] : explode( ',', $params['scope'] ) );
513 }
514 if ( count( $tags ) > 0 ) {
515 $tags = array_filter( $tags, function ( $tag ) {
516 return $tag !== '';
517 } );
518 }
519 $params['tags'] = $tags ? implode( ',', $tags ) : '';
520 $params['code'] = $this->sanitize_code( $params['code'] );
521 return $params;
522 }
523
524 /**
525 * Format parameters for the front-end
526 *
527 * @param array $params
528 * @return array
529 */
530 private function formatParamsForFront( $params )
531 {
532 // Separate the scope tags from the tags
533 $scopes = ['backend', 'frontend', 'function', 'persistent', 'scheduled'];
534
535 if ( isset( $params['tags'] ) && !empty( $params['tags'] ) ) {
536
537 $tags = array_map( function ( $tag ) use ( $scopes ) {
538 if ( in_array( $tag, $scopes ) ) {
539 return null;
540 }
541 return trim( $tag );
542 }, explode( ',', $params['tags'] ) );
543 $params['tags'] = array_filter( $tags, function ( $tag ) {
544 return $tag !== null;
545 } );
546 }
547 return $params;
548 }
549
550 public function stats()
551 {
552 $scopes = ['function', 'scheduled', 'global'];
553 $globalScopes = ['backend', 'frontend', 'persistent'];
554
555 $stats = [
556 'all' => 0,
557 'disabled' => $this->wpdb->get_var( "SELECT COUNT( * ) FROM $this->table_name WHERE active = 0" ),
558 ];
559
560 foreach ( $scopes as $scope ) {
561 if ( $scope === 'global' ) {
562 $globalScopeQuery = implode( "', '", array_map( 'esc_sql', $globalScopes ) );
563 $stats[$scope] = $this->wpdb->get_var( "SELECT COUNT( * ) FROM $this->table_name WHERE scope IN ('$globalScopeQuery')" );
564 } else {
565 $stats[$scope] = $this->wpdb->get_var( $this->wpdb->prepare( "SELECT COUNT( * ) FROM $this->table_name WHERE scope = %s", $scope ) );
566 }
567 $stats['all'] += $stats[$scope];
568 }
569
570 return $stats;
571 }
572
573 public function import( )
574 {
575 $table = $this->wpdb->prefix . 'snippets';
576 $snippets = $this->wpdb->get_results( "SELECT * FROM $table", ARRAY_A );
577
578 if ( !$snippets ) {
579 return 0;
580 }
581
582 // Disable ( set active = 0 ) all the snippets in the old table
583 $this->wpdb->update( $table, ['active' => 0], ['active' => 1] );
584
585 foreach ( $snippets as $snippet ) {
586 $snippet['id'] = null; // Reset the ID so it will be inserted as a new snippet.
587 $snippet = $this->validate( $snippet );
588 $this->insert( $this->formatParamsForDatabase( $snippet ) );
589 }
590
591 return count( $snippets );
592 }
593
594 private function sanitize_code( $code )
595 {
596 $code = preg_replace( '/<\?php/', '', $code, 1 );
597 $code = ltrim( $code );
598 return $code;
599 }
600
601 #endregion
602
603 #region Snippet CRUD
604
605 public function select( $offset, $limit, $filters, $sort )
606 {
607 if ( !$this->check_db() ) {
608 throw new Exception( __( 'Could not access the database.', 'code-engine' ) );
609 }
610
611 $list = [];
612 $offset = !empty( $offset ) ? intval( $offset ) : 0;
613 $limit = !empty( $limit ) ? intval( $limit ) : 10;
614 $filters = !empty( $filters ) ? $filters : [];
615 $sort = !empty( $sort ) ? $sort : ['accessor' => 'updated', 'by' => 'desc'];
616 $query = "SELECT * FROM $this->table_name";
617
618 // Filters
619 if ( is_array( $filters ) && count( $filters ) > 0 ) {
620 $where = [];
621
622 $freshFilters = [];
623 // Little trick that allows searching by tags using the snippet accessor
624 // And to have a global scope that will search for backend, frontend, and persistent
625 foreach ( $filters as $filter ) {
626 if ( $filter['accessor'] === 'snippet' ) {
627 //$freshFilters['accessor'] = 'tags';
628 $freshFilters[] = [ 'accessor' => 'tags', 'value' => $filter['value'] ];
629 }
630 else if ( $filter['accessor'] === 'scope' && $filter['value'] === 'global' ) {
631 $freshFilters[] = [ 'accessor' => 'scope', 'value' => ['backend', 'frontend', 'persistent'] ];
632 }
633 else {
634 $freshFilters[] = $filter;
635 }
636 }
637 $filters = $freshFilters;
638
639 foreach ( $filters as $filter ) {
640 if ( $filter['accessor'] === 'tags' ) {
641 $value = ( array )$filter['value'];
642
643 if ( count( $value ) === 0 ) {
644 continue;
645 }
646 $where_unit = [];
647 foreach ( $value as $tag ) {
648 if ( strpos( $tag, ',' ) !== false ) {
649 $tags = explode( ',', $tag );
650 $where_combination_unit = [];
651 foreach ( $tags as $t ) {
652 $where_combination_unit[] = "FIND_IN_SET( '{$t}', tags )";
653 }
654 $where_unit[] = '( ' . implode( ' AND ', $where_combination_unit ) . ' )';
655 continue;
656 }
657 $where_unit[] = "FIND_IN_SET( '{$tag}', tags )";
658 }
659 $where[] = '( ' . implode( ' OR ', $where_unit ) . ' )';
660 } elseif ( $filter['accessor'] === 'active' ) {
661 $value = esc_sql( $filter['value'] );
662 $where[] = $this->wpdb->prepare( "active = %d", $value );
663 } elseif ( $filter['accessor'] === 'endpoint' ) {
664 $where[] = boolval( $filter['value'] ) ? "endpoint <> ''" : "endpoint = ''";
665 } elseif ( $filter['accessor'] === 'scope' ) {
666 if ( is_array( $filter['value'] ) ) {
667 $scopes = array_map( function( $scope ) {
668 return esc_sql( $scope );
669 }, $filter['value'] );
670 $where[] = "scope IN ('" . implode( "', '", $scopes ) . "')";
671 } else {
672 $value = esc_sql( $filter['value'] );
673 $where[] = $this->wpdb->prepare( "scope = %s", $value );
674 }
675 }
676 }
677 if ( count( $where ) > 0 ) {
678 $query .= " WHERE " . implode( " AND ", $where );
679 }
680 }
681
682 // Count based on this query
683 $list['total'] = $this->wpdb->get_var( "SELECT COUNT( * ) FROM ( $query ) AS t" );
684
685 // Order by
686 $query .= " ORDER BY " . esc_sql( $sort['accessor'] ) . " " . esc_sql( $sort['by'] );
687
688 // Limits
689 if ( $limit > 0 ) {
690 $query .= " LIMIT $offset, $limit";
691 }
692
693 $list['data'] = array_map( function ( $snippet ) {
694 return $this->formatParamsForFront( $snippet );
695 }, $this->wpdb->get_results( $query, ARRAY_A ) );
696
697 $this->get_function_snippets_data( $list['data'] );
698 $this->get_interval_snippets_data( $list['data'] );
699
700 return $list;
701 }
702
703 public function select_tags( )
704 {
705 if ( !$this->check_db( ) ) {
706 throw new Exception( __( 'Could not access the database.', 'code-engine' ) );
707 }
708
709 $tags = [];
710 $query = "SELECT tags FROM $this->table_name";
711 $result = $this->wpdb->get_results( $query, ARRAY_A );
712 foreach ( $result as $row ) {
713 $tags = array_merge( $tags, explode( ',', $row['tags'] ) );
714 }
715 // Remove the scope tags: admin, front, once.
716 $tags = array_diff( $tags, ['backend', 'frontend', 'function', 'persistent', 'scheduled'] );
717 return array_values( array_unique( $tags ) );
718 }
719
720 public function select_one( $id, $options = [] )
721 {
722 if ( !$this->check_db( ) ) {
723 throw new Exception( __( 'Could not access the database.', 'code-engine' ) );
724 }
725
726 $query = "SELECT * FROM $this->table_name WHERE id = %s";
727 if ( isset( $options['active'] ) ) {
728 $query .= " AND active = " . ( $options['active'] ? '1' : '0' );
729 }
730
731 return $this->formatParamsForFront(
732 $this->wpdb->get_row(
733 $this->wpdb->prepare( $query, ( string ) $id ),
734 ARRAY_A
735 )
736 );
737 }
738
739 public function insert( $insert_data )
740 {
741 if ( !$this->check_db( ) ) {
742 throw new Exception( __( 'Could not access the database.', 'code-engine' ) );
743 }
744
745 $data = [];
746 $update_columns = array_keys( MWCODE_SNIPPET_COLUMNS );
747
748 foreach ( $update_columns as $column ) {
749 if ( isset( $insert_data[$column] ) ) {
750 $data[$column] = $insert_data[$column];
751 } else {
752 unset( $data[$column] ); // Remove it if it's empty, so it uses the default db value.
753 }
754 }
755
756 $data['created'] = date( 'Y-m-d H:i:s' );
757 $data['updated'] = date( 'Y-m-d H:i:s' );
758
759 $this->wpdb->insert( $this->table_name, $data );
760 $id = $this->wpdb->insert_id;
761 if ( !$id ) {
762 throw new Exception( __( 'Could not insert the snippet.', 'code-engine' ) );
763 }
764 return $id;
765 }
766
767 public function update( $update_data )
768 {
769 if ( !$this->check_db( ) ) {
770 throw new Exception( __( 'Could not access the database.', 'code-engine' ) );
771 }
772
773 $data = [];
774 $update_columns = array_keys( MWCODE_SNIPPET_COLUMNS );
775 foreach ( $update_columns as $column ) {
776 if ( isset( $update_data[$column] ) ) {
777 $data[$column] = $update_data[$column];
778 }
779 }
780 if ( count( $data ) === 0 ) {
781 throw new Exception( __( 'No data to update.', 'code-engine' ) );
782 }
783 $data['updated'] = date( 'Y-m-d H:i:s' );
784 $result = $this->wpdb->update( $this->table_name, $data, ['id' => $update_data['id']] );
785 if ( $result === false ) {
786 throw new Exception( __( 'Could not insert the snippet.', 'code-engine' ) );
787 }
788 return $result;
789 }
790
791 public function force_disable( $id )
792 {
793 if ( !$this->check_db( ) ) {
794 throw new Exception( __( 'Could not access the database.', 'code-engine' ) );
795 }
796
797 $result = $this->wpdb->update( $this->table_name, ['active' => 0], ['id' => $id] );
798 if ( $result === false ) {
799 throw new Exception( __( 'Could not disable the snippet.', 'code-engine' ) );
800 }
801 return $result;
802 }
803
804 public function delete( $delete_data )
805 {
806 if ( !$this->check_db( ) ) {
807 throw new Exception( __( 'Could not access the database.', 'code-engine' ) );
808 }
809
810 $result = $this->wpdb->delete( $this->table_name, ['id' => $delete_data['id']] );
811 if ( $result === false ) {
812 throw new Exception( __( 'Could not delete the snippet.', 'code-engine' ) );
813 }
814 return $result;
815 }
816
817 public function delete_all( )
818 {
819 if ( !$this->check_db( ) ) {
820 throw new Exception( __( 'Could not access the database.', 'code-engine' ) );
821 }
822
823 $result = $this->wpdb->query( "TRUNCATE TABLE $this->table_name" );
824 if ( $result === false ) {
825 throw new Exception( __( 'Could not delete all snippets.', 'code-engine' ) );
826 }
827 return $result;
828 }
829
830 #endregion
831
832 #region Database
833
834 function create_db( )
835 {
836 $this->core->log( '💾 ( Code Engine ) Creating Table: ' . $this->table_name );
837 try {
838 $charset_collate = $this->wpdb->get_charset_collate( );
839
840 $column_definitions = array_map( function ( $column_name, $column_definition ) {
841 return "$column_name $column_definition";
842 }, array_keys( MWCODE_SNIPPET_COLUMNS ), MWCODE_SNIPPET_COLUMNS );
843 $column_definitions = implode( ",\n", $column_definitions ) . ', PRIMARY KEY ( id )';
844
845 $sql = "CREATE TABLE $this->table_name ( $column_definitions ) $charset_collate;";
846 $this->core->log( '💾 ( Code Engine ) Create table request: ' . $sql );
847 require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
848 dbDelta( $sql );
849 } catch ( Exception $e ) {
850 $this->core->log( '💾 ( Code Engine ) Error creating Table: ' . $e->getMessage( ) );
851 }
852
853 add_option( 'mwcode_db_snippet_version', $this->mwcode_db_snippet_version );
854 }
855
856 function check_db( )
857 {
858 if ( $this->does_table_exist( $this->table_name ) ) {
859 $this->check_columns( );
860 $this->db_check = true;
861 } else {
862 $this->create_db( );
863 $this->core->log( '💾 ( Code Engine ) Table created, checking if it was successful.' );
864 $this->db_check = $this->does_table_exist( $this->table_name );
865 }
866
867 return $this->db_check;
868 }
869
870 private function check_columns( )
871 {
872 $db_version = get_option( 'mwcode_db_snippet_version' );
873 if ( $db_version == $this->mwcode_db_snippet_version ) {
874 return;
875 }
876
877 $this->core->log( '💾 ( Code Engine ) Database version is ' . $db_version . ', upgrading to ' . $this->mwcode_db_snippet_version . '.' );
878
879 global $wpdb;
880 $table_name = $this->table_name;
881 $charset = $wpdb->get_charset_collate( );
882 $desired_columns = MWCODE_SNIPPET_COLUMNS;
883 $existing_columns = $wpdb->get_results( "DESCRIBE $table_name", ARRAY_A );
884
885 // Handle column removals
886 $columns_to_remove = array_diff( array_column( $existing_columns, 'Field' ), array_keys( $desired_columns ) );
887 if ( !empty( $columns_to_remove ) ) {
888 $remove_queries = array_map( function ( $column_name ) use ( $table_name ) {
889 return "DROP COLUMN $column_name";
890 }, $columns_to_remove );
891 $remove_query = "ALTER TABLE $table_name " . implode( ', ', $remove_queries );
892 $wpdb->query( $remove_query );
893 }
894
895 // Handle column additions and updates
896 $alter_queries = array( );
897 foreach ( $desired_columns as $column_name => $column_definition ) {
898 $existing_column = array_filter( $existing_columns, function ( $column ) use ( $column_name ) {
899 return $column['Field'] === $column_name;
900 } );
901
902 if ( empty( $existing_column ) ) {
903 $alter_queries[] = "ADD COLUMN $column_name $column_definition";
904 } else {
905 $existing_column = array_shift( $existing_column );
906 $existing_column_definition = $existing_column['Type'];
907 if ( $existing_column_definition !== $column_definition ) {
908 $alter_queries[] = "MODIFY COLUMN $column_name $column_definition";
909 }
910 }
911 }
912
913 if ( !empty( $alter_queries ) ) {
914 $alter_query = "ALTER TABLE $table_name " . implode( ', ', $alter_queries );
915 $wpdb->query( $alter_query );
916 }
917
918 update_option( 'mwcode_db_snippet_version', $this->mwcode_db_snippet_version );
919 }
920
921 private function does_table_exist( $table_name )
922 {
923
924 $found = false;
925 $table_name = strtolower( $table_name );
926
927 // Try the fast way first
928 try {
929 $query = "SHOW TABLES LIKE '{$table_name}'";
930 $result = strtolower( $this->wpdb->get_var( $query ) );
931
932 $found = $result === $table_name;
933 } catch ( Exception $e ) {
934 $this->core->log( '💾 ( Code Engine ) Database Check 1 Error: ' . $e->getMessage( ) );
935 }
936
937 // If not found, try the slow way
938 if ( !$found ) {
939 try {
940 $query = "SHOW TABLES";
941 $tables = $this->wpdb->get_results( $query, ARRAY_N );
942 foreach ( $tables as $table ) {
943 $result = strtolower( $table[0] );
944 if ( $result === $table_name ) {
945 $found = true;
946 break;
947 }
948 }
949 } catch ( Exception $e ) {
950 $this->core->log( '💾 ( Code Engine ) Database Check 2 Error: ' . $e->getMessage( ) );
951 }
952 }
953
954 if ( !$found ) {
955 $this->core->log( '💾 ( Code Engine ) Database table doesn\'t seem to exist.' );
956 }
957
958 return $found;
959 }
960
961 #endregion
962 }
963