PluginProbe
Code Engine – PHP Snippets, AI Functions & Automation for WordPress / trunk
Code Engine – PHP Snippets, AI Functions & Automation for WordPress vtrunk
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
← All changes | classes/core.php +72 -55 0.5.0trunk View file →
@@ -106,8 +106,9 @@
106 106
107 107 //AI
108 108 "ai_suggestions" => false,
109 109 "ai_engine_status"=> false,
110 + "mwai_active" => false,
110 111 "ai_engine_message" => "",
111 112
112 113 //API
113 114 "api_endpoint" => false,
@@ -114,8 +115,9 @@
114 115 "api_token" => md5( time() . rand() ),
115 116
116 117 //MCP
117 118 "mcp_support" => false,
119 + "mcp_functions" => false,
118 120
119 121 //MAINTENANCE
120 122 "clean_uninstall" => false,
121 123 ];
@@ -189,8 +191,12 @@
189 191
190 192 private function updateAIEngineStatus( &$options ) {
191 193 global $mwai;
192 194
195 + // AI Engine is active (regardless of whether an API key is configured).
196 + // MCP exposure only needs AI Engine present, not a key, so the MCP toggles
197 + // gate on this rather than on mwai_has_ai.
198 + $options['mwai_active'] = !empty( $mwai );
193 199 $options['mwai_has_ai'] = !empty( $mwai ) && method_exists( $mwai, 'hasAI' ) && $mwai->hasAI();
194 200 // Legacy
195 201 $options['ai_engine_status'] = $options['mwai_has_ai'];
196 202
@@ -224,9 +230,24 @@
224 230
225 231 $this->snippet->validate( $params );
226 232
227 233 $params = $this->snippet->formatParamsForDatabase( $params );
228 - $result = $this->snippet->insert( $params );
234 +
235 + // Route to UPDATE when an existing snippet id is provided (updateSnippet / the
236 + // MCP mwcode_update_snippet tool). This previously always insert()ed, so an
237 + // update tried to INSERT a row with an already-used primary key: that fails on
238 + // the SQLite backend (Studio/Playground) with "Could not insert the snippet",
239 + // and duplicates or errors elsewhere. The admin UI was unaffected because it
240 + // calls snippet->update() directly.
241 + $existing = !empty( $params['id'] ) ? $this->snippet->select_one( $params['id'] ) : null;
242 + if ( $existing ) {
243 + $this->snippet->update( $params );
244 + $result = $params['id'];
245 + }
246 + else {
247 + unset( $params['id'] );
248 + $result = $this->snippet->insert( $params );
249 + }
229 250 $snippet = $this->snippet->select_one( $result );
230 251
231 252 if( $result ) {
232 253 $params['id'] = (string)$result;
@@ -341,35 +362,17 @@
341 362 'values' => $snippet['functionArgsDict'] // Contains the default values of the arguments
342 363 ];
343 364 }
344 365
345 - // Sanitize all the arguments if the option is enabled
346 - if ( $this->get_option( 'sanitize_arguments', true ) ) {
366 + // Arguments used to be sanitized into PHP-literal strings here (quoting,
367 + // esc_sql, var_export) so they could be concatenated into a string of PHP and
368 + // eval-ed. That is gone: the function is now called with call_user_func_array
369 + // (see below), so values are passed as data and need no literal-formatting.
370 + // The old formatting also prefixed argument keys with "$" via sanitize_arg,
371 + // which stored the provided value under "$name" while the call read "name", so
372 + // provided arguments never reached the function. Passing the raw values through
373 + // fixes both issues at once.
347 374
348 - if ( $args ) {
349 - foreach ( $args as $name => $value ) {
350 - list( $sanitizedName, $sanitizedValue ) = $this->sanitize_arg( $name, $value );
351 - unset( $args[$name] );
352 -
353 - $args[$sanitizedName] = $sanitizedValue;
354 - }
355 - }
356 -
357 - foreach ( $params['values'] as $name => $value ) {
358 -
359 - if( array_key_exists( 'input', $value) ) {
360 - list( $sanitizedInputName, $sanitizedInputValue ) = $this->sanitize_arg( $name, $value['input'], $value['type'] );
361 - $params['values'][$sanitizedInputName]['input'] = $sanitizedInputValue;
362 - }
363 -
364 - if( array_key_exists( 'default', $value) ) {
365 - list( $sanitizedDefaultValueName, $sanitizedDefaultValue ) = $this->sanitize_arg( $name, $value['default'], $value['type'] );
366 - $params['values'][$sanitizedDefaultValueName]['default'] = $sanitizedDefaultValue;
367 - }
368 - }
369 -
370 - }
371 -
372 375 // Make sure the function is existing and is the one in the snippet
373 376 if ( empty( $params['code'] ) ) {
374 377 throw new Exception( 'Code Engine: The snippet code appears to be empty.' );
375 378 }
@@ -377,12 +380,17 @@
377 380 if ( empty( $params['name'] ) || ! str_contains( $params['code'], $params['name'] ) ) {
378 381 throw new Exception( "Code Engine: Function name does not match. The name should be {$params['name']}." );
379 382 }
380 383
381 - // Overwrite the default values with the provided ones
384 + // Collect the provided values, keyed by their normalized (dollar-less) name.
385 + // Incoming keys come from the AI/MCP schema, where register_function_tools()
386 + // strips a leading "$" from the declared name. The stored arg names can still
387 + // carry the "$", so we normalize both sides before matching below. Without this
388 + // a value provided as "style" never binds to an argument declared "$style".
389 + $provided = [];
382 390 if ( $args ) {
383 391 foreach ( $args as $name => $value ) {
384 - $params['values'][$name]['input'] = $value;
392 + $provided[ ltrim( $name, '$' ) ] = $value;
385 393 }
386 394
387 395 $this->log( '⚡ Arguments provided: ' . json_encode( $args ) );
388 396 }
@@ -415,49 +423,58 @@
415 423 // If already defined, just prepare to call the function without redefining it
416 424 $params['code'] = '';
417 425 }
418 426
419 - // Prepare the code to be executed
420 - $params['code'] .= "\n\$mwcode_result = {$params['name']}(";
421 - foreach ( $params['args'] as $index => $arg ) {
422 - $value = 'null'; // In case the argument is not provided it will be null
423 -
424 - if ( array_key_exists( $arg, $params['values'] ) ) { // Avoid warnings if the argument is not provided
425 -
426 - // If the argument is provided, use it, if not use the default value
427 - if ( !empty( $params['values'][$arg]['input'] ) ) {
428 - $value = $params['values'][$arg]['input'];
429 -
430 - } else if ( !empty( $params['values'][$arg]['default'] ) ) {
431 - $value = $params['values'][$arg]['default'];
432 - }
427 + // Resolve the arguments as REAL PHP values, in the function's declared order.
428 + // The previous version concatenated each value into a string of PHP and eval-ed
429 + // the call, which broke on any string or edge-case value with a parse error
430 + // ("syntax error, unexpected token ')'"). call_user_func_array passes them as
431 + // data, so no value can ever corrupt the call syntax.
432 + $callArgs = [];
433 + foreach ( $params['args'] as $arg ) {
434 + $key = ltrim( $arg, '$' ); // Match the normalized name the caller sent.
435 + $value = null; // Not provided and no default -> null.
436 + // array_key_exists, not !empty: a legitimately provided 0, "0", "" or false
437 + // must reach the function instead of silently falling back to the default.
438 + if ( array_key_exists( $key, $provided ) ) {
439 + $value = $provided[ $key ];
440 + } else if ( isset( $params['values'][$arg]['default'] ) && $params['values'][$arg]['default'] !== '' ) {
441 + $value = $params['values'][$arg]['default'];
433 442 }
434 -
435 - $params['code'] .= "{$value}";
436 - if ( $index < count( $params['args'] ) - 1 ) {
437 - $params['code'] .= ', ';
443 + // An array-typed argument can arrive as a string like "[1, 2, 3]"; turn it
444 + // into a real array so the function receives what its signature expects.
445 + if ( ( $params['values'][$arg]['type'] ?? null ) === 'array' && is_string( $value ) ) {
446 + $decoded = json_decode( $value, true );
447 + $value = is_array( $decoded ) ? $decoded : array_map( 'trim', explode( ',', trim( $value, "[] \t\n\r" ) ) );
438 448 }
449 + $callArgs[] = $value;
439 450 }
440 451
441 - $params['code'] .= ");\necho print_r(\$mwcode_result, true);";
442 -
443 452 $error = null;
444 453 $output = null;
445 -
454 +
446 455 try {
447 456 ob_start();
448 - eval( $params['code'] );
457 + // $params['code'] holds the function definition (empty if it was already
458 + // defined earlier this request). Declare it, then invoke it as data.
459 + if ( $params['code'] !== '' ) {
460 + eval( $params['code'] );
461 + }
462 + $mwcode_result = call_user_func_array( $params['name'], $callArgs );
463 + echo print_r( $mwcode_result, true );
449 464 $output = ob_get_clean();
450 -
451 - if ( $params['test'] ){
465 +
466 + if ( $params['test'] ) {
452 467 $output = explode( "\n", $output );
453 468 }
454 -
469 +
455 470 } catch ( Throwable $e ) {
456 471 //$this->log('Code Engine: Error executing the function: ' . $e->getMessage());
457 472 $error = new Exception(' Error executing the function, ' . $e->getMessage());
458 473
459 - ob_clean();
474 + if ( ob_get_level() > 0 ) {
475 + ob_end_clean();
476 + }
460 477 } finally {
461 478 restore_error_handler();
462 479 }
463 480