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 +71 -55 0.5.1trunk 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,
@@ -190,8 +191,12 @@
190 191
191 192 private function updateAIEngineStatus( &$options ) {
192 193 global $mwai;
193 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 );
194 199 $options['mwai_has_ai'] = !empty( $mwai ) && method_exists( $mwai, 'hasAI' ) && $mwai->hasAI();
195 200 // Legacy
196 201 $options['ai_engine_status'] = $options['mwai_has_ai'];
197 202
@@ -225,9 +230,24 @@
225 230
226 231 $this->snippet->validate( $params );
227 232
228 233 $params = $this->snippet->formatParamsForDatabase( $params );
229 - $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 + }
230 250 $snippet = $this->snippet->select_one( $result );
231 251
232 252 if( $result ) {
233 253 $params['id'] = (string)$result;
@@ -342,35 +362,17 @@
342 362 'values' => $snippet['functionArgsDict'] // Contains the default values of the arguments
343 363 ];
344 364 }
345 365
346 - // Sanitize all the arguments if the option is enabled
347 - 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.
348 374
349 - if ( $args ) {
350 - foreach ( $args as $name => $value ) {
351 - list( $sanitizedName, $sanitizedValue ) = $this->sanitize_arg( $name, $value );
352 - unset( $args[$name] );
353 -
354 - $args[$sanitizedName] = $sanitizedValue;
355 - }
356 - }
357 -
358 - foreach ( $params['values'] as $name => $value ) {
359 -
360 - if( array_key_exists( 'input', $value) ) {
361 - list( $sanitizedInputName, $sanitizedInputValue ) = $this->sanitize_arg( $name, $value['input'], $value['type'] );
362 - $params['values'][$sanitizedInputName]['input'] = $sanitizedInputValue;
363 - }
364 -
365 - if( array_key_exists( 'default', $value) ) {
366 - list( $sanitizedDefaultValueName, $sanitizedDefaultValue ) = $this->sanitize_arg( $name, $value['default'], $value['type'] );
367 - $params['values'][$sanitizedDefaultValueName]['default'] = $sanitizedDefaultValue;
368 - }
369 - }
370 -
371 - }
372 -
373 375 // Make sure the function is existing and is the one in the snippet
374 376 if ( empty( $params['code'] ) ) {
375 377 throw new Exception( 'Code Engine: The snippet code appears to be empty.' );
376 378 }
@@ -378,12 +380,17 @@
378 380 if ( empty( $params['name'] ) || ! str_contains( $params['code'], $params['name'] ) ) {
379 381 throw new Exception( "Code Engine: Function name does not match. The name should be {$params['name']}." );
380 382 }
381 383
382 - // 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 = [];
383 390 if ( $args ) {
384 391 foreach ( $args as $name => $value ) {
385 - $params['values'][$name]['input'] = $value;
392 + $provided[ ltrim( $name, '$' ) ] = $value;
386 393 }
387 394
388 395 $this->log( '⚡ Arguments provided: ' . json_encode( $args ) );
389 396 }
@@ -416,49 +423,58 @@
416 423 // If already defined, just prepare to call the function without redefining it
417 424 $params['code'] = '';
418 425 }
419 426
420 - // Prepare the code to be executed
421 - $params['code'] .= "\n\$mwcode_result = {$params['name']}(";
422 - foreach ( $params['args'] as $index => $arg ) {
423 - $value = 'null'; // In case the argument is not provided it will be null
424 -
425 - if ( array_key_exists( $arg, $params['values'] ) ) { // Avoid warnings if the argument is not provided
426 -
427 - // If the argument is provided, use it, if not use the default value
428 - if ( !empty( $params['values'][$arg]['input'] ) ) {
429 - $value = $params['values'][$arg]['input'];
430 -
431 - } else if ( !empty( $params['values'][$arg]['default'] ) ) {
432 - $value = $params['values'][$arg]['default'];
433 - }
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'];
434 442 }
435 -
436 - $params['code'] .= "{$value}";
437 - if ( $index < count( $params['args'] ) - 1 ) {
438 - $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" ) ) );
439 448 }
449 + $callArgs[] = $value;
440 450 }
441 451
442 - $params['code'] .= ");\necho print_r(\$mwcode_result, true);";
443 -
444 452 $error = null;
445 453 $output = null;
446 -
454 +
447 455 try {
448 456 ob_start();
449 - 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 );
450 464 $output = ob_get_clean();
451 -
452 - if ( $params['test'] ){
465 +
466 + if ( $params['test'] ) {
453 467 $output = explode( "\n", $output );
454 468 }
455 -
469 +
456 470 } catch ( Throwable $e ) {
457 471 //$this->log('Code Engine: Error executing the function: ' . $e->getMessage());
458 472 $error = new Exception(' Error executing the function, ' . $e->getMessage());
459 473
460 - ob_clean();
474 + if ( ob_get_level() > 0 ) {
475 + ob_end_clean();
476 + }
461 477 } finally {
462 478 restore_error_handler();
463 479 }
464 480