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