From cb4dcd3c22fe739da5a675a4758ebf598bcc3953 Mon Sep 17 00:00:00 2001 From: CarolinaOP Date: Mon, 8 Sep 2025 15:24:13 +0100 Subject: [PATCH 1/7] starting point before stash From db47a5e53e23c4f4ed2f75cfcdc7d118928ffaa5 Mon Sep 17 00:00:00 2001 From: CarolinaOP Date: Mon, 8 Sep 2025 15:41:12 +0100 Subject: [PATCH 2/7] updated REST API response in order to preserve code_error --- src/php/rest-api/class-snippets-rest-controller.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/php/rest-api/class-snippets-rest-controller.php b/src/php/rest-api/class-snippets-rest-controller.php index 18a1f827..22b1bdc7 100644 --- a/src/php/rest-api/class-snippets-rest-controller.php +++ b/src/php/rest-api/class-snippets-rest-controller.php @@ -270,8 +270,8 @@ public function update_item( $request ) { $result = save_snippet( $item ); if ( $result ) { - $request->set_param( 'id', $result->id ); - return $this->get_item( $request ); + $data = $this->prepare_item_for_response( $result, $request ); + return rest_ensure_response( $data ); } return new WP_Error( From accc0c4115acb4ea83c5a370b843943a6a7805c9 Mon Sep 17 00:00:00 2001 From: CarolinaOP Date: Mon, 8 Sep 2025 18:03:12 +0100 Subject: [PATCH 3/7] fix: backend error detection for syntax and function conflicts, frontend parsing and fatal error handling --- src/js/hooks/useSubmitSnippet.ts | 244 +++++++++++++++++++------------ src/js/utils/snippets/objects.ts | 8 + src/php/snippet-ops.php | 55 ++++++- 3 files changed, 209 insertions(+), 98 deletions(-) diff --git a/src/js/hooks/useSubmitSnippet.ts b/src/js/hooks/useSubmitSnippet.ts index 2dc3530a..c2086faf 100644 --- a/src/js/hooks/useSubmitSnippet.ts +++ b/src/js/hooks/useSubmitSnippet.ts @@ -7,119 +7,169 @@ import { useRestAPI } from './useRestAPI' import { useSnippetForm } from './useSnippetForm' import type { Snippet } from '../types/Snippet' -const snippetMessages = { - addNew: __('Add New Snippet', 'code-snippets'), - edit: __('Edit Snippet', 'code-snippets'), - created: __('Snippet created.', 'code-snippets'), - updated: __('Snippet updated.', 'code-snippets'), - createdActivated: __('Snippet created and activated.', 'code-snippets'), - updatedActivated: __('Snippet updated and activated.', 'code-snippets'), - updatedDeactivated: __('Snippet updated and deactivated'), - updatedExecuted: __('Snippet updated and executed.', 'code-snippets'), - failedCreate: __('Could not create snippet.', 'code-snippets'), - failedUpdate: __('Could not update snippet.', 'code-snippets') +const snippetMessages = { + addNew: __('Add New Snippet', 'code-snippets'), + edit: __('Edit Snippet', 'code-snippets'), + created: __('Snippet created.', 'code-snippets'), + updated: __('Snippet updated.', 'code-snippets'), + createdActivated: __( + 'Snippet created and activated.', + 'code-snippets' + ), + updatedActivated: __( + 'Snippet updated and activated.', + 'code-snippets' + ), + updatedDeactivated: __( + 'Snippet updated and deactivated' + ), + updatedExecuted: __( + 'Snippet updated and executed.', + 'code-snippets' + ), + failedCreate: __('Could not create snippet.', 'code-snippets'), + failedUpdate: __('Could not update snippet.', 'code-snippets'), } -const conditionCreated = __('Condition created.', 'code-snippets') -const conditionUpdated = __('Condition updated.', 'code-snippets') +const conditionCreated = __( + 'Condition created.', + 'code-snippets' +) +const conditionUpdated = __( + 'Condition updated.', + 'code-snippets' +) const conditionMessages: typeof snippetMessages = { - addNew: __('Add New Condition', 'code-snippets'), - edit: __('Edit Condition', 'code-snippets'), - created: conditionCreated, - updated: conditionUpdated, - createdActivated: conditionCreated, - updatedActivated: conditionUpdated, - updatedDeactivated: conditionUpdated, - updatedExecuted: conditionUpdated, - failedCreate: __('Could not create condition.', 'code-snippets'), - failedUpdate: __('Could not update condition.', 'code-snippets') + addNew: __('Add New Condition', 'code-snippets'), + edit: __('Edit Condition', 'code-snippets'), + created: conditionCreated, + updated: conditionUpdated, + createdActivated: conditionCreated, + updatedActivated: conditionUpdated, + updatedDeactivated: conditionUpdated, + updatedExecuted: conditionUpdated, + failedCreate: __('Could not create condition.', 'code-snippets'), + failedUpdate: __('Could not update condition.', 'code-snippets'), } export enum SubmitSnippetAction { - SAVE = 'save_snippet', - SAVE_AND_ACTIVATE = 'save_snippet_activate', - SAVE_AND_EXECUTE = 'save_snippet_execute', - SAVE_AND_DEACTIVATE = 'save_snippet_deactivate' + SAVE = 'save_snippet', + SAVE_AND_ACTIVATE = 'save_snippet_activate', + SAVE_AND_EXECUTE = 'save_snippet_execute', + SAVE_AND_DEACTIVATE = 'save_snippet_deactivate', } -const getSuccessNotice = (request: Snippet, response: Snippet, action: SubmitSnippetAction): string => { - const messages = 'condition' === request.scope ? conditionMessages : snippetMessages - const wasCreated = 0 === request.id - - switch (action) { - case SubmitSnippetAction.SAVE: - return wasCreated ? messages.created : messages.updated - - case SubmitSnippetAction.SAVE_AND_EXECUTE: - return messages.updatedExecuted - - case SubmitSnippetAction.SAVE_AND_ACTIVATE: - if ('single-use' === response.scope) { - return messages.updatedExecuted - } else { - return wasCreated - ? messages.createdActivated - : messages.updatedActivated - } - - case SubmitSnippetAction.SAVE_AND_DEACTIVATE: - return messages.updatedDeactivated - } +const getSuccessNotice = ( + request: Snippet, + response: Snippet, + action: SubmitSnippetAction +): string => { + const messages = + 'condition' === request.scope ? conditionMessages : snippetMessages + const wasCreated = 0 === request.id + + switch (action) { + case SubmitSnippetAction.SAVE: + return wasCreated ? messages.created : messages.updated + + case SubmitSnippetAction.SAVE_AND_EXECUTE: + return messages.updatedExecuted + + case SubmitSnippetAction.SAVE_AND_ACTIVATE: + if ('single-use' === response.scope) { + return messages.updatedExecuted + } else { + return wasCreated + ? messages.createdActivated + : messages.updatedActivated + } + + case SubmitSnippetAction.SAVE_AND_DEACTIVATE: + return messages.updatedDeactivated + } } const SUBMIT_ACTION_DELTA: Record> = { - [SubmitSnippetAction.SAVE]: {}, - [SubmitSnippetAction.SAVE_AND_ACTIVATE]: { active: true }, - [SubmitSnippetAction.SAVE_AND_DEACTIVATE]: { active: false }, - [SubmitSnippetAction.SAVE_AND_EXECUTE]: { active: true } + [SubmitSnippetAction.SAVE]: {}, + [SubmitSnippetAction.SAVE_AND_ACTIVATE]: { active: true }, + [SubmitSnippetAction.SAVE_AND_DEACTIVATE]: { active: false }, + [SubmitSnippetAction.SAVE_AND_EXECUTE]: { active: true }, } export interface UseSubmitSnippet { - submitSnippet: (action?: SubmitSnippetAction) => Promise + submitSnippet: (action?: SubmitSnippetAction) => Promise } export const useSubmitSnippet = (): UseSubmitSnippet => { - const { snippetsAPI } = useRestAPI() - const { setIsWorking, setCurrentNotice, snippet, setSnippet } = useSnippetForm() - - const submitSnippet = useCallback(async (action: SubmitSnippetAction = SubmitSnippetAction.SAVE) => { - setCurrentNotice(undefined) - - const result = await (async (): Promise => { - try { - const request: Snippet = { ...snippet, ...SUBMIT_ACTION_DELTA[action] } - const response = await (0 === request.id ? snippetsAPI.create(request) : snippetsAPI.update(request)) - setIsWorking(false) - return response.id ? response : undefined - } catch (error) { - setIsWorking(false) - return isAxiosError(error) ? error.message : undefined - } - })() - - const messages = isCondition(snippet) ? conditionMessages : snippetMessages - - if (undefined === result || 'string' === typeof result) { - const message = [ - snippet.id ? messages.failedUpdate : messages.failedCreate, - result ?? __('The server did not send a valid response.', 'code-snippets') - ] - - setCurrentNotice(['error', message.filter(Boolean).join(' ')]) - return undefined - } else { - setSnippet(createSnippetObject(result)) - setCurrentNotice(['updated', getSuccessNotice(snippet, result, action)]) - - if (snippet.id && result.id) { - window.document.title = window.document.title.replace(snippetMessages.addNew, messages.edit) - window.history.replaceState({}, '', addQueryArgs(window.CODE_SNIPPETS?.urls.edit, { id: result.id })) - } - - return result - } - }, [snippetsAPI, setIsWorking, setCurrentNotice, snippet, setSnippet]) - - return { submitSnippet } + const { snippetsAPI } = useRestAPI() + const { setIsWorking, setCurrentNotice, snippet, setSnippet } = + useSnippetForm() + + const submitSnippet = useCallback( + async (action: SubmitSnippetAction = SubmitSnippetAction.SAVE) => { + setCurrentNotice(undefined) + + const result = await (async (): Promise => { + try { + const request: Snippet = { + ...snippet, + ...SUBMIT_ACTION_DELTA[action], + } + const response = await (0 === request.id + ? snippetsAPI.create(request) + : snippetsAPI.update(request)) + setIsWorking(false) + return response.id ? response : undefined + } catch (error) { + setIsWorking(false) + return isAxiosError(error) ? error.message : undefined + } + })() + + const messages = isCondition(snippet) + ? conditionMessages + : snippetMessages + + if (undefined === result || 'string' === typeof result) { + const message = [ + snippet.id ? messages.failedUpdate : messages.failedCreate, + result ?? + __('The server did not send a valid response.', 'code-snippets'), + ] + + setCurrentNotice(['error', message.filter(Boolean).join(' ')]) + return undefined + } else { + setSnippet(createSnippetObject(result)) + + // Check for code_error and show error if present + if (result.code_error) { + setCurrentNotice(['error', result.code_error[0]]) + } else { + setCurrentNotice([ + 'updated', + getSuccessNotice(snippet, result, action), + ]) + } + + if (snippet.id && result.id) { + window.document.title = window.document.title.replace( + snippetMessages.addNew, + messages.edit + ) + window.history.replaceState( + {}, + '', + addQueryArgs(window.CODE_SNIPPETS?.urls.edit, { id: result.id }) + ) + } + + return result + } + }, + [snippetsAPI, setIsWorking, setCurrentNotice, snippet, setSnippet] + ) + + return { submitSnippet } } diff --git a/src/js/utils/snippets/objects.ts b/src/js/utils/snippets/objects.ts index f542ab64..1f460f3b 100644 --- a/src/js/utils/snippets/objects.ts +++ b/src/js/utils/snippets/objects.ts @@ -47,5 +47,13 @@ export const parseSnippetObject = (fields: unknown): Snippet => { ...'shared_network' in fields && 'boolean' === typeof fields.shared_network && { shared_network: fields.shared_network }, ...'priority' in fields && 'number' === typeof fields.priority && { priority: fields.priority }, ...'condition_id' in fields && isAbsInt(fields.condition_id) && { conditionId: fields.condition_id } + ...('code_error' in fields && + Array.isArray(fields.code_error) && + fields.code_error.length === 2 && + 'string' === typeof fields.code_error[0] && + 'number' === typeof fields.code_error[1] && { + code_error: fields.code_error as readonly [string, number], + }), } + } diff --git a/src/php/snippet-ops.php b/src/php/snippet-ops.php index 10581eb3..6ce7d9c2 100644 --- a/src/php/snippet-ops.php +++ b/src/php/snippet-ops.php @@ -477,6 +477,16 @@ function test_snippet_code( Snippet $snippet ) { ucfirst( rtrim( $result->getMessage(), '.' ) ) . '.', $result->getLine(), ]; + } elseif ( $result instanceof Error ) { + $snippet->code_error = [ + ucfirst( rtrim( $result->getMessage(), '.' ) ) . '.', + $result->getLine(), + ]; + } elseif ( is_object( $result ) && isset( $result->type ) && $result->type === 'fatal_error' ) { + $snippet->code_error = [ + ucfirst( rtrim( $result->message, '.' ) ) . '.', + $result->line, + ]; } } } @@ -507,10 +517,11 @@ function save_snippet( $snippet ) { $snippet->code = preg_replace( '|^\s*<\?(php)?|', '', $snippet->code ); $snippet->code = preg_replace( '|\?>\s*$|', '', $snippet->code ); - // Deactivate snippet if code contains errors. + // Test snippet code when user is trying to activate it if ( $snippet->active && 'single-use' !== $snippet->scope ) { test_snippet_code( $snippet ); + // Deactivate snippet if code contains errors if ( $snippet->code_error ) { $snippet->active = 0; } @@ -589,12 +600,22 @@ function execute_snippet( string $code, int $id = 0, bool $force = false ) { return false; } + // Since fatal errors cannot be caught with error handlers + // Try to detect function redeclaration by parsing the code + $function_redeclaration_error = detect_function_redeclaration( $code ); + if ( $function_redeclaration_error ) { + return $function_redeclaration_error; + } + ob_start(); try { $result = eval( $code ); } catch ( ParseError $parse_error ) { $result = $parse_error; + } catch ( Error $error ) { + // Catch other fatal errors + $result = $error; } ob_end_clean(); @@ -603,6 +624,38 @@ function execute_snippet( string $code, int $id = 0, bool $force = false ) { return $result; } +/** + * Detect function redeclaration errors by checking if functions already exist + * + * @param string $code The code to check + * @return object|null Error object if redeclaration detected, null otherwise + */ +function detect_function_redeclaration( string $code ) { + // Extract function names from the code + preg_match_all( '/function\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/', $code, $matches ); + + if ( empty( $matches[1] ) ) { + return null; // No functions found + } + + $function_names = $matches[1]; + + // Check if any of these functions already exist + foreach ( $function_names as $function_name ) { + if ( function_exists( $function_name ) ) { + // Create a custom error object that mimics ParseError + $error = new \stdClass(); + $error->type = 'fatal_error'; + $error->message = "Cannot redeclare {$function_name}() (previously declared)"; + $error->line = 1; // We can't determine the exact line easily + $error->file = ''; + return $error; + } + } + + return null; // No redeclaration detected +} + /** * Retrieve a single snippets from the database using its cloud ID. * From 4c5ca40536ce47cd00623cd02dbaa7df0bfa3c6e Mon Sep 17 00:00:00 2001 From: CarolinaOP Date: Mon, 8 Sep 2025 18:32:45 +0100 Subject: [PATCH 4/7] updated - missing comma --- src/js/utils/snippets/objects.ts | 90 +++++++++++++++++++------------- 1 file changed, 55 insertions(+), 35 deletions(-) diff --git a/src/js/utils/snippets/objects.ts b/src/js/utils/snippets/objects.ts index 1f460f3b..5b0e618c 100644 --- a/src/js/utils/snippets/objects.ts +++ b/src/js/utils/snippets/objects.ts @@ -3,50 +3,71 @@ import { isNetworkAdmin } from '../screen' import type { Snippet, SnippetScope } from '../../types/Snippet' const defaults: Omit = { - id: 0, - name: '', - code: '', - desc: '', - scope: 'global', - modified: '', - active: false, - network: isNetworkAdmin(), - shared_network: null, - priority: 10, - conditionId: 0 + id: 0, + name: '', + code: '', + desc: '', + scope: 'global', + modified: '', + active: false, + network: isNetworkAdmin(), + shared_network: null, + priority: 10, + conditionId: 0, } const isAbsInt = (value: unknown): value is number => - 'number' === typeof value && 0 < value + 'number' === typeof value && 0 < value const parseStringArray = (value: unknown): string[] | undefined => - Array.isArray(value) ? value.filter(entry => 'string' === typeof entry) : undefined + Array.isArray(value) + ? value.filter((entry) => 'string' === typeof entry) + : undefined export const isValidScope = (scope: unknown): scope is SnippetScope => - 'string' === typeof scope && Object.values(SNIPPET_TYPE_SCOPES).some(typeScopes => - typeScopes.some(typeScope => typeScope === scope)) + 'string' === typeof scope && + Object.values(SNIPPET_TYPE_SCOPES).some((typeScopes) => + typeScopes.some((typeScope) => typeScope === scope) + ) export const parseSnippetObject = (fields: unknown): Snippet => { - const result: { -readonly [F in keyof Snippet]: Snippet[F] } = { ...defaults, tags: [] } + const result: { -readonly [F in keyof Snippet]: Snippet[F] } = { + ...defaults, + tags: [], + } - if ('object' !== typeof fields || null === fields) { - return result - } + if ('object' !== typeof fields || null === fields) { + return result + } - return { - ...result, - ...'id' in fields && isAbsInt(fields.id) && { id: fields.id }, - ...'name' in fields && 'string' === typeof fields.name && { name: fields.name }, - ...'desc' in fields && 'string' === typeof fields.desc && { desc: fields.desc }, - ...'code' in fields && 'string' === typeof fields.code && { code: fields.code }, - ...'tags' in fields && { tags: parseStringArray(fields.tags) ?? result.tags }, - ...'scope' in fields && isValidScope(fields.scope) && { scope: fields.scope }, - ...'modified' in fields && 'string' === typeof fields.modified && { modified: fields.modified }, - ...'active' in fields && 'boolean' === typeof fields.active && { active: fields.active }, - ...'network' in fields && 'boolean' === typeof fields.network && { network: fields.network }, - ...'shared_network' in fields && 'boolean' === typeof fields.shared_network && { shared_network: fields.shared_network }, - ...'priority' in fields && 'number' === typeof fields.priority && { priority: fields.priority }, - ...'condition_id' in fields && isAbsInt(fields.condition_id) && { conditionId: fields.condition_id } + return { + ...result, + ...('id' in fields && isAbsInt(fields.id) && { id: fields.id }), + ...('name' in fields && + 'string' === typeof fields.name && { name: fields.name }), + ...('desc' in fields && + 'string' === typeof fields.desc && { desc: fields.desc }), + ...('code' in fields && + 'string' === typeof fields.code && { code: fields.code }), + ...('tags' in fields && { + tags: parseStringArray(fields.tags) ?? result.tags, + }), + ...('scope' in fields && + isValidScope(fields.scope) && { scope: fields.scope }), + ...('modified' in fields && + 'string' === typeof fields.modified && { modified: fields.modified }), + ...('active' in fields && + 'boolean' === typeof fields.active && { active: fields.active }), + ...('network' in fields && + 'boolean' === typeof fields.network && { network: fields.network }), + ...('shared_network' in fields && + 'boolean' === typeof fields.shared_network && { + shared_network: fields.shared_network, + }), + ...('priority' in fields && + 'number' === typeof fields.priority && { priority: fields.priority }), + ...('condition_id' in fields && + isAbsInt(fields.condition_id) && { conditionId: fields.condition_id }), ...('code_error' in fields && Array.isArray(fields.code_error) && fields.code_error.length === 2 && @@ -54,6 +75,5 @@ export const parseSnippetObject = (fields: unknown): Snippet => { 'number' === typeof fields.code_error[1] && { code_error: fields.code_error as readonly [string, number], }), - } - + } } From a7a5eeac316c342707de267f7ea7cf2fffddb8df Mon Sep 17 00:00:00 2001 From: CarolinaOP Date: Wed, 10 Sep 2025 00:20:44 +0100 Subject: [PATCH 5/7] Clean comments and formatting --- src/js/hooks/useSubmitSnippet.ts | 255 ++++++++---------- src/js/utils/snippets/objects.ts | 101 +++---- .../class-snippets-rest-controller.php | 2 +- src/php/snippet-ops.php | 10 +- 4 files changed, 147 insertions(+), 221 deletions(-) diff --git a/src/js/hooks/useSubmitSnippet.ts b/src/js/hooks/useSubmitSnippet.ts index c2086faf..6322c5df 100644 --- a/src/js/hooks/useSubmitSnippet.ts +++ b/src/js/hooks/useSubmitSnippet.ts @@ -2,174 +2,131 @@ import { __ } from '@wordpress/i18n' import { addQueryArgs } from '@wordpress/url' import { isAxiosError } from 'axios' import { useCallback } from 'react' +import type { Snippet } from '../types/Snippet' import { createSnippetObject, isCondition } from '../utils/snippets/snippets' import { useRestAPI } from './useRestAPI' import { useSnippetForm } from './useSnippetForm' -import type { Snippet } from '../types/Snippet' -const snippetMessages = { - addNew: __('Add New Snippet', 'code-snippets'), - edit: __('Edit Snippet', 'code-snippets'), - created: __('Snippet created.', 'code-snippets'), - updated: __('Snippet updated.', 'code-snippets'), - createdActivated: __( - 'Snippet created and activated.', - 'code-snippets' - ), - updatedActivated: __( - 'Snippet updated and activated.', - 'code-snippets' - ), - updatedDeactivated: __( - 'Snippet updated and deactivated' - ), - updatedExecuted: __( - 'Snippet updated and executed.', - 'code-snippets' - ), - failedCreate: __('Could not create snippet.', 'code-snippets'), - failedUpdate: __('Could not update snippet.', 'code-snippets'), +const snippetMessages = { + addNew: __('Add New Snippet', 'code-snippets'), + edit: __('Edit Snippet', 'code-snippets'), + created: __('Snippet created.', 'code-snippets'), + updated: __('Snippet updated.', 'code-snippets'), + createdActivated: __('Snippet created and activated.', 'code-snippets'), + updatedActivated: __('Snippet updated and activated.', 'code-snippets'), + updatedDeactivated: __('Snippet updated and deactivated'), + updatedExecuted: __('Snippet updated and executed.', 'code-snippets'), + failedCreate: __('Could not create snippet.', 'code-snippets'), + failedUpdate: __('Could not update snippet.', 'code-snippets') } -const conditionCreated = __( - 'Condition created.', - 'code-snippets' -) -const conditionUpdated = __( - 'Condition updated.', - 'code-snippets' -) +const conditionCreated = __('Condition created.', 'code-snippets') +const conditionUpdated = __('Condition updated.', 'code-snippets') const conditionMessages: typeof snippetMessages = { - addNew: __('Add New Condition', 'code-snippets'), - edit: __('Edit Condition', 'code-snippets'), - created: conditionCreated, - updated: conditionUpdated, - createdActivated: conditionCreated, - updatedActivated: conditionUpdated, - updatedDeactivated: conditionUpdated, - updatedExecuted: conditionUpdated, - failedCreate: __('Could not create condition.', 'code-snippets'), - failedUpdate: __('Could not update condition.', 'code-snippets'), + addNew: __('Add New Condition', 'code-snippets'), + edit: __('Edit Condition', 'code-snippets'), + created: conditionCreated, + updated: conditionUpdated, + createdActivated: conditionCreated, + updatedActivated: conditionUpdated, + updatedDeactivated: conditionUpdated, + updatedExecuted: conditionUpdated, + failedCreate: __('Could not create condition.', 'code-snippets'), + failedUpdate: __('Could not update condition.', 'code-snippets') } export enum SubmitSnippetAction { - SAVE = 'save_snippet', - SAVE_AND_ACTIVATE = 'save_snippet_activate', - SAVE_AND_EXECUTE = 'save_snippet_execute', - SAVE_AND_DEACTIVATE = 'save_snippet_deactivate', + SAVE = 'save_snippet', + SAVE_AND_ACTIVATE = 'save_snippet_activate', + SAVE_AND_EXECUTE = 'save_snippet_execute', + SAVE_AND_DEACTIVATE = 'save_snippet_deactivate' } -const getSuccessNotice = ( - request: Snippet, - response: Snippet, - action: SubmitSnippetAction -): string => { - const messages = - 'condition' === request.scope ? conditionMessages : snippetMessages - const wasCreated = 0 === request.id - - switch (action) { - case SubmitSnippetAction.SAVE: - return wasCreated ? messages.created : messages.updated - - case SubmitSnippetAction.SAVE_AND_EXECUTE: - return messages.updatedExecuted - - case SubmitSnippetAction.SAVE_AND_ACTIVATE: - if ('single-use' === response.scope) { - return messages.updatedExecuted - } else { - return wasCreated - ? messages.createdActivated - : messages.updatedActivated - } - - case SubmitSnippetAction.SAVE_AND_DEACTIVATE: - return messages.updatedDeactivated - } +const getSuccessNotice = (request: Snippet, response: Snippet, action: SubmitSnippetAction): string => { + const messages = 'condition' === request.scope ? conditionMessages : snippetMessages + const wasCreated = 0 === request.id + + switch (action) { + case SubmitSnippetAction.SAVE: + return wasCreated ? messages.created : messages.updated + + case SubmitSnippetAction.SAVE_AND_EXECUTE: + return messages.updatedExecuted + + case SubmitSnippetAction.SAVE_AND_ACTIVATE: + if ('single-use' === response.scope) { + return messages.updatedExecuted + } else { + return wasCreated + ? messages.createdActivated + : messages.updatedActivated + } + + case SubmitSnippetAction.SAVE_AND_DEACTIVATE: + return messages.updatedDeactivated + } } const SUBMIT_ACTION_DELTA: Record> = { - [SubmitSnippetAction.SAVE]: {}, - [SubmitSnippetAction.SAVE_AND_ACTIVATE]: { active: true }, - [SubmitSnippetAction.SAVE_AND_DEACTIVATE]: { active: false }, - [SubmitSnippetAction.SAVE_AND_EXECUTE]: { active: true }, + [SubmitSnippetAction.SAVE]: {}, + [SubmitSnippetAction.SAVE_AND_ACTIVATE]: { active: true }, + [SubmitSnippetAction.SAVE_AND_DEACTIVATE]: { active: false }, + [SubmitSnippetAction.SAVE_AND_EXECUTE]: { active: true } } export interface UseSubmitSnippet { - submitSnippet: (action?: SubmitSnippetAction) => Promise + submitSnippet: (action?: SubmitSnippetAction) => Promise } export const useSubmitSnippet = (): UseSubmitSnippet => { - const { snippetsAPI } = useRestAPI() - const { setIsWorking, setCurrentNotice, snippet, setSnippet } = - useSnippetForm() - - const submitSnippet = useCallback( - async (action: SubmitSnippetAction = SubmitSnippetAction.SAVE) => { - setCurrentNotice(undefined) - - const result = await (async (): Promise => { - try { - const request: Snippet = { - ...snippet, - ...SUBMIT_ACTION_DELTA[action], - } - const response = await (0 === request.id - ? snippetsAPI.create(request) - : snippetsAPI.update(request)) - setIsWorking(false) - return response.id ? response : undefined - } catch (error) { - setIsWorking(false) - return isAxiosError(error) ? error.message : undefined - } - })() - - const messages = isCondition(snippet) - ? conditionMessages - : snippetMessages - - if (undefined === result || 'string' === typeof result) { - const message = [ - snippet.id ? messages.failedUpdate : messages.failedCreate, - result ?? - __('The server did not send a valid response.', 'code-snippets'), - ] - - setCurrentNotice(['error', message.filter(Boolean).join(' ')]) - return undefined - } else { - setSnippet(createSnippetObject(result)) - - // Check for code_error and show error if present - if (result.code_error) { - setCurrentNotice(['error', result.code_error[0]]) - } else { - setCurrentNotice([ - 'updated', - getSuccessNotice(snippet, result, action), - ]) - } - - if (snippet.id && result.id) { - window.document.title = window.document.title.replace( - snippetMessages.addNew, - messages.edit - ) - window.history.replaceState( - {}, - '', - addQueryArgs(window.CODE_SNIPPETS?.urls.edit, { id: result.id }) - ) - } - - return result - } - }, - [snippetsAPI, setIsWorking, setCurrentNotice, snippet, setSnippet] - ) - - return { submitSnippet } -} + const { snippetsAPI } = useRestAPI() + const { setIsWorking, setCurrentNotice, snippet, setSnippet } = useSnippetForm() + + const submitSnippet = useCallback(async (action: SubmitSnippetAction = SubmitSnippetAction.SAVE) => { + setCurrentNotice(undefined) + + const result = await (async (): Promise => { + try { + const request: Snippet = { ...snippet, ...SUBMIT_ACTION_DELTA[action] } + const response = await (0 === request.id ? snippetsAPI.create(request) : snippetsAPI.update(request)) + setIsWorking(false) + return response.id ? response : undefined + } catch (error) { + setIsWorking(false) + return isAxiosError(error) ? error.message : undefined + } + })() + + const messages = isCondition(snippet) ? conditionMessages : snippetMessages + + if (undefined === result || 'string' === typeof result) { + const message = [ + snippet.id ? messages.failedUpdate : messages.failedCreate, + result ?? __('The server did not send a valid response.', 'code-snippets') + ] + + setCurrentNotice(['error', message.filter(Boolean).join(' ')]) + return undefined + } else { + const updatedSnippet = createSnippetObject(result) + setSnippet(updatedSnippet) + + // Check if there's a code error and show appropriate notice + if (updatedSnippet.code_error) { + setCurrentNotice(['error', `Code Error: ${updatedSnippet.code_error[0]}`]) + } else { + setCurrentNotice(['updated', getSuccessNotice(snippet, result, action)]) + } + + if (snippet.id && result.id) { + window.document.title = window.document.title.replace(snippetMessages.addNew, messages.edit) + window.history.replaceState({}, '', addQueryArgs(window.CODE_SNIPPETS?.urls.edit, { id: result.id })) + } + + return result + } + }, [snippetsAPI, setIsWorking, setCurrentNotice, snippet, setSnippet]) + + return { submitSnippet } +} \ No newline at end of file diff --git a/src/js/utils/snippets/objects.ts b/src/js/utils/snippets/objects.ts index 5b0e618c..43a625fb 100644 --- a/src/js/utils/snippets/objects.ts +++ b/src/js/utils/snippets/objects.ts @@ -1,79 +1,52 @@ +import type { Snippet, SnippetScope } from '../../types/Snippet' import { SNIPPET_TYPE_SCOPES } from '../../types/Snippet' import { isNetworkAdmin } from '../screen' -import type { Snippet, SnippetScope } from '../../types/Snippet' const defaults: Omit = { - id: 0, - name: '', - code: '', - desc: '', - scope: 'global', - modified: '', - active: false, - network: isNetworkAdmin(), - shared_network: null, - priority: 10, - conditionId: 0, + id: 0, + name: '', + code: '', + desc: '', + scope: 'global', + modified: '', + active: false, + network: isNetworkAdmin(), + shared_network: null, + priority: 10, + conditionId: 0 } const isAbsInt = (value: unknown): value is number => - 'number' === typeof value && 0 < value + 'number' === typeof value && 0 < value const parseStringArray = (value: unknown): string[] | undefined => - Array.isArray(value) - ? value.filter((entry) => 'string' === typeof entry) - : undefined + Array.isArray(value) ? value.filter(entry => 'string' === typeof entry) : undefined export const isValidScope = (scope: unknown): scope is SnippetScope => - 'string' === typeof scope && - Object.values(SNIPPET_TYPE_SCOPES).some((typeScopes) => - typeScopes.some((typeScope) => typeScope === scope) - ) + 'string' === typeof scope && Object.values(SNIPPET_TYPE_SCOPES).some(typeScopes => + typeScopes.some(typeScope => typeScope === scope)) export const parseSnippetObject = (fields: unknown): Snippet => { - const result: { -readonly [F in keyof Snippet]: Snippet[F] } = { - ...defaults, - tags: [], - } + const result: { -readonly [F in keyof Snippet]: Snippet[F] } = { ...defaults, tags: [] } - if ('object' !== typeof fields || null === fields) { - return result - } + if ('object' !== typeof fields || null === fields) { + return result + } - return { - ...result, - ...('id' in fields && isAbsInt(fields.id) && { id: fields.id }), - ...('name' in fields && - 'string' === typeof fields.name && { name: fields.name }), - ...('desc' in fields && - 'string' === typeof fields.desc && { desc: fields.desc }), - ...('code' in fields && - 'string' === typeof fields.code && { code: fields.code }), - ...('tags' in fields && { - tags: parseStringArray(fields.tags) ?? result.tags, - }), - ...('scope' in fields && - isValidScope(fields.scope) && { scope: fields.scope }), - ...('modified' in fields && - 'string' === typeof fields.modified && { modified: fields.modified }), - ...('active' in fields && - 'boolean' === typeof fields.active && { active: fields.active }), - ...('network' in fields && - 'boolean' === typeof fields.network && { network: fields.network }), - ...('shared_network' in fields && - 'boolean' === typeof fields.shared_network && { - shared_network: fields.shared_network, - }), - ...('priority' in fields && - 'number' === typeof fields.priority && { priority: fields.priority }), - ...('condition_id' in fields && - isAbsInt(fields.condition_id) && { conditionId: fields.condition_id }), - ...('code_error' in fields && - Array.isArray(fields.code_error) && - fields.code_error.length === 2 && - 'string' === typeof fields.code_error[0] && - 'number' === typeof fields.code_error[1] && { - code_error: fields.code_error as readonly [string, number], - }), - } -} + return { + ...result, + ...'id' in fields && isAbsInt(fields.id) && { id: fields.id }, + ...'name' in fields && 'string' === typeof fields.name && { name: fields.name }, + ...'desc' in fields && 'string' === typeof fields.desc && { desc: fields.desc }, + ...'code' in fields && 'string' === typeof fields.code && { code: fields.code }, + ...'tags' in fields && { tags: parseStringArray(fields.tags) ?? result.tags }, + ...'scope' in fields && isValidScope(fields.scope) && { scope: fields.scope }, + ...'modified' in fields && 'string' === typeof fields.modified && { modified: fields.modified }, + ...'active' in fields && 'boolean' === typeof fields.active && { active: fields.active }, + ...'network' in fields && 'boolean' === typeof fields.network && { network: fields.network }, + ...'shared_network' in fields && 'boolean' === typeof fields.shared_network && { shared_network: fields.shared_network }, + ...'priority' in fields && 'number' === typeof fields.priority && { priority: fields.priority }, + ...'condition_id' in fields && isAbsInt(fields.condition_id) && { conditionId: fields.condition_id }, + ...'code_error' in fields && Array.isArray(fields.code_error) && fields.code_error.length === 2 && 'string' === typeof fields.code_error[0] && 'number' === typeof fields.code_error[1] && { code_error: fields.code_error as readonly [string, number] } + } +} \ No newline at end of file diff --git a/src/php/rest-api/class-snippets-rest-controller.php b/src/php/rest-api/class-snippets-rest-controller.php index 22b1bdc7..bf77f7b1 100644 --- a/src/php/rest-api/class-snippets-rest-controller.php +++ b/src/php/rest-api/class-snippets-rest-controller.php @@ -556,4 +556,4 @@ public function get_item_schema(): array { return $this->schema; } -} +} \ No newline at end of file diff --git a/src/php/snippet-ops.php b/src/php/snippet-ops.php index 6ce7d9c2..fb064020 100644 --- a/src/php/snippet-ops.php +++ b/src/php/snippet-ops.php @@ -517,11 +517,10 @@ function save_snippet( $snippet ) { $snippet->code = preg_replace( '|^\s*<\?(php)?|', '', $snippet->code ); $snippet->code = preg_replace( '|\?>\s*$|', '', $snippet->code ); - // Test snippet code when user is trying to activate it + // Deactivate snippet if code contains errors. if ( $snippet->active && 'single-use' !== $snippet->scope ) { test_snippet_code( $snippet ); - // Deactivate snippet if code contains errors if ( $snippet->code_error ) { $snippet->active = 0; } @@ -600,8 +599,7 @@ function execute_snippet( string $code, int $id = 0, bool $force = false ) { return false; } - // Since fatal errors cannot be caught with error handlers - // Try to detect function redeclaration by parsing the code + // Fatal errors - try to detect function redeclaration by parsing the code $function_redeclaration_error = detect_function_redeclaration( $code ); if ( $function_redeclaration_error ) { return $function_redeclaration_error; @@ -614,7 +612,6 @@ function execute_snippet( string $code, int $id = 0, bool $force = false ) { } catch ( ParseError $parse_error ) { $result = $parse_error; } catch ( Error $error ) { - // Catch other fatal errors $result = $error; } @@ -643,7 +640,6 @@ function detect_function_redeclaration( string $code ) { // Check if any of these functions already exist foreach ( $function_names as $function_name ) { if ( function_exists( $function_name ) ) { - // Create a custom error object that mimics ParseError $error = new \stdClass(); $error->type = 'fatal_error'; $error->message = "Cannot redeclare {$function_name}() (previously declared)"; @@ -724,4 +720,4 @@ function update_snippet_fields( int $snippet_id, array $fields, ?bool $network = do_action( 'code_snippets/update_snippet', $snippet->id, $table ); clean_snippets_cache( $table ); -} +} \ No newline at end of file From 1909a7e6debdc09b123788e19a11efc368f822b0 Mon Sep 17 00:00:00 2001 From: CarolinaOP Date: Thu, 11 Sep 2025 18:41:14 +0100 Subject: [PATCH 6/7] activate_snippet() function now uses test_snippet_code() for validation --- src/js/services/manage/activation.ts | 16 ++- src/php/admin-menus/class-manage-menu.php | 3 +- .../class-snippets-rest-controller.php | 14 ++- src/php/snippet-ops.php | 101 ++++++++++-------- 4 files changed, 86 insertions(+), 48 deletions(-) diff --git a/src/js/services/manage/activation.ts b/src/js/services/manage/activation.ts index 039edf1c..ae35238a 100644 --- a/src/js/services/manage/activation.ts +++ b/src/js/services/manage/activation.ts @@ -1,6 +1,6 @@ import { __ } from '@wordpress/i18n' -import { updateSnippet } from './requests' import type { Snippet } from '../../types/Snippet' +import { updateSnippet } from './requests' /** * Update the snippet count of a specific view @@ -65,9 +65,21 @@ export const toggleSnippetActive = (link: HTMLAnchorElement, event: Event) => { } else { row.className += ' erroneous-snippet' + // Handle different types of errors + const errorData = <{ type?: string; message?: string } | undefined>response.data + const errorType = errorData?.type ?? 'action_error' + let errorMessage = __('An error occurred when attempting to activate', 'code-snippets') + + if ('validation_error' === errorType && errorData?.message) { + errorMessage = errorData.message + } + if (button) { - button.title = __('An error occurred when attempting to activate', 'code-snippets') + button.title = errorMessage } + + // Show error message to user + console.error('Snippet activation failed:', errorMessage) } }) } diff --git a/src/php/admin-menus/class-manage-menu.php b/src/php/admin-menus/class-manage-menu.php index ba53ea90..56ee0ad2 100644 --- a/src/php/admin-menus/class-manage-menu.php +++ b/src/php/admin-menus/class-manage-menu.php @@ -321,9 +321,10 @@ public function ajax_callback() { } elseif ( $snippet->active ) { $result = activate_snippet( $snippet->id, $snippet->network ); if ( is_string( $result ) ) { + // Return validation error with proper error type wp_send_json_error( array( - 'type' => 'action_error', + 'type' => 'validation_error', 'message' => $result, ) ); diff --git a/src/php/rest-api/class-snippets-rest-controller.php b/src/php/rest-api/class-snippets-rest-controller.php index bf77f7b1..a3aada33 100644 --- a/src/php/rest-api/class-snippets-rest-controller.php +++ b/src/php/rest-api/class-snippets-rest-controller.php @@ -310,15 +310,23 @@ public function delete_item( $request ) { */ public function activate_item( WP_REST_Request $request ) { $item = $this->prepare_item_for_database( $request ); + + // DEBUG: Log REST API activation request + error_log( "Code Snippets DEBUG: REST API activation request - ID: {$item->id}, Network: " . ( $item->network ? 'true' : 'false' ) ); + $result = activate_snippet( $item->id, $item->network ); - return $result instanceof Snippet ? - rest_ensure_response( $result ) : - new WP_Error( + if ( $result instanceof Snippet ) { + error_log( "Code Snippets DEBUG: REST API activation successful" ); + return rest_ensure_response( $result ); + } else { + error_log( "Code Snippets DEBUG: REST API activation failed - Error: $result" ); + return new WP_Error( 'rest_cannot_activate', $result, [ 'status' => 500 ] ); + } } /** diff --git a/src/php/snippet-ops.php b/src/php/snippet-ops.php index fb064020..51a7c556 100644 --- a/src/php/snippet-ops.php +++ b/src/php/snippet-ops.php @@ -297,9 +297,16 @@ function activate_snippet( int $id, ?bool $network = null ) { return sprintf( __( 'Could not locate snippet with ID %d.', 'code-snippets' ), $id ); } - $validator = new Validator( $snippet->code ); - if ( $validator->validate() ) { - return __( 'Could not activate snippet: code did not pass validation.', 'code-snippets' ); + // Use the same comprehensive validation as the edit page + if ( 'php' === $snippet->type ) { + test_snippet_code( $snippet ); + + if ( $snippet->code_error ) { + return sprintf( + __( 'Could not activate snippet: %s', 'code-snippets' ), + $snippet->code_error[0] + ); + } } $result = $wpdb->update( @@ -342,15 +349,21 @@ function activate_snippets( array $ids, ?bool $network = null ): ?array { return null; } - // Loop through each snippet code and validate individually. + // Loop through each snippet code and validate individually using comprehensive validation $valid_ids = []; $valid_snippets = []; foreach ( $snippets as $snippet ) { - $validator = new Validator( $snippet->code ); - $code_error = $validator->validate(); + // Use the same comprehensive validation as single snippet activation + if ( 'php' === $snippet->type ) { + test_snippet_code( $snippet ); - if ( ! $code_error ) { + if ( ! $snippet->code_error ) { + $valid_ids[] = $snippet->id; + $valid_snippets[] = $snippet; + } + } else { + // Non-PHP snippets can be activated without validation $valid_ids[] = $snippet->id; $valid_snippets[] = $snippet; } @@ -459,17 +472,23 @@ function test_snippet_code( Snippet $snippet ) { $snippet->code_error = null; if ( 'php' !== $snippet->type ) { + error_log( "Code Snippets DEBUG: Skipping validation for non-PHP snippet type: {$snippet->type}" ); return; } + error_log( "Code Snippets DEBUG: Running Validator on snippet ID {$snippet->id}" ); $validator = new Validator( $snippet->code ); $result = $validator->validate(); if ( $result ) { + error_log( "Code Snippets DEBUG: Validator found error: " . $result['message'] ); $snippet->code_error = [ $result['message'], $result['line'] ]; + } else { + error_log( "Code Snippets DEBUG: Validator passed, running execute_snippet" ); } - if ( ! $snippet->code_error && 'single-use' !== $snippet->scope ) { + if ( ! $snippet->code_error ) { + error_log( "Code Snippets DEBUG: Calling execute_snippet for redeclaration check" ); $result = execute_snippet( $snippet->code, $snippet->id, true ); if ( $result instanceof ParseError ) { @@ -599,11 +618,14 @@ function execute_snippet( string $code, int $id = 0, bool $force = false ) { return false; } - // Fatal errors - try to detect function redeclaration by parsing the code - $function_redeclaration_error = detect_function_redeclaration( $code ); - if ( $function_redeclaration_error ) { - return $function_redeclaration_error; - } + // Set up error handling for fatal errors + $old_error_handler = set_error_handler( function( $severity, $message, $file, $line ) { + // Convert fatal errors to exceptions + if ( $severity & ( E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR ) ) { + throw new Error( $message, 0, $severity, $file, $line ); + } + return false; // Let other errors be handled normally + }); ob_start(); @@ -613,45 +635,40 @@ function execute_snippet( string $code, int $id = 0, bool $force = false ) { $result = $parse_error; } catch ( Error $error ) { $result = $error; + } catch ( Throwable $throwable ) { + // Handle function redeclaration and other fatal errors + if ( strpos( $throwable->getMessage(), 'Cannot redeclare' ) !== false ) { + $error = new \stdClass(); + $error->type = 'fatal_error'; + $error->message = $throwable->getMessage(); + $error->line = $throwable->getLine(); + $error->file = $throwable->getFile(); + $result = $error; + } else { + $result = $throwable; + } } - ob_end_clean(); - - do_action( 'code_snippets/after_execute_snippet', $code, $id, $result ); - return $result; -} - -/** - * Detect function redeclaration errors by checking if functions already exist - * - * @param string $code The code to check - * @return object|null Error object if redeclaration detected, null otherwise - */ -function detect_function_redeclaration( string $code ) { - // Extract function names from the code - preg_match_all( '/function\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/', $code, $matches ); + $output = ob_get_clean(); - if ( empty( $matches[1] ) ) { - return null; // No functions found - } - - $function_names = $matches[1]; + // Restore original error handler + restore_error_handler(); - // Check if any of these functions already exist - foreach ( $function_names as $function_name ) { - if ( function_exists( $function_name ) ) { + // If we have output but no result, it might be a fatal error that wasn't caught + if ( ! empty( $output ) && null === $result ) { $error = new \stdClass(); $error->type = 'fatal_error'; - $error->message = "Cannot redeclare {$function_name}() (previously declared)"; - $error->line = 1; // We can't determine the exact line easily + $error->message = 'Fatal error during execution'; + $error->line = 1; $error->file = ''; - return $error; - } + $result = $error; } - - return null; // No redeclaration detected + + do_action( 'code_snippets/after_execute_snippet', $code, $id, $result ); + return $result; } + /** * Retrieve a single snippets from the database using its cloud ID. * From 3a7bf978c695634b1b36fea47e089d99f4e10b65 Mon Sep 17 00:00:00 2001 From: CarolinaOP Date: Fri, 12 Sep 2025 17:58:25 +0100 Subject: [PATCH 7/7] wip --- src/php/snippet-ops.php | 66 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 59 insertions(+), 7 deletions(-) diff --git a/src/php/snippet-ops.php b/src/php/snippet-ops.php index 51a7c556..f114efe5 100644 --- a/src/php/snippet-ops.php +++ b/src/php/snippet-ops.php @@ -472,24 +472,26 @@ function test_snippet_code( Snippet $snippet ) { $snippet->code_error = null; if ( 'php' !== $snippet->type ) { - error_log( "Code Snippets DEBUG: Skipping validation for non-PHP snippet type: {$snippet->type}" ); return; } - error_log( "Code Snippets DEBUG: Running Validator on snippet ID {$snippet->id}" ); $validator = new Validator( $snippet->code ); $result = $validator->validate(); if ( $result ) { - error_log( "Code Snippets DEBUG: Validator found error: " . $result['message'] ); $snippet->code_error = [ $result['message'], $result['line'] ]; - } else { - error_log( "Code Snippets DEBUG: Validator passed, running execute_snippet" ); } if ( ! $snippet->code_error ) { - error_log( "Code Snippets DEBUG: Calling execute_snippet for redeclaration check" ); - $result = execute_snippet( $snippet->code, $snippet->id, true ); + // First check for conflicts with other active snippets + $conflict_error = detect_function_redeclaration_with_active_snippets( $snippet->code, $snippet->id ); + if ( $conflict_error ) { + $snippet->code_error = [ + ucfirst( rtrim( $conflict_error->message, '.' ) ) . '.', + $conflict_error->line, + ]; + } else { + $result = execute_snippet( $snippet->code, $snippet->id, true ); if ( $result instanceof ParseError ) { $snippet->code_error = [ @@ -507,6 +509,7 @@ function test_snippet_code( Snippet $snippet ) { $result->line, ]; } + } } } @@ -669,6 +672,55 @@ function execute_snippet( string $code, int $id = 0, bool $force = false ) { } +/** + * Check for function redeclaration conflicts with other active snippets + * + * @param string $code The code to check + * @param int $current_snippet_id The ID of the current snippet being validated + * @return object|null Error object if redeclaration detected, null otherwise + */ +function detect_function_redeclaration_with_active_snippets( string $code, int $current_snippet_id ) { + // Extract function names from the current code + preg_match_all( '/function\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/', $code, $matches ); + + if ( empty( $matches[1] ) ) { + return null; // No functions found + } + + $current_functions = $matches[1]; + + // Get all active snippets except the current one + $active_snippets = get_snippets( [], null ); + $active_snippets = array_filter( $active_snippets, function( $snippet ) use ( $current_snippet_id ) { + return $snippet->active && $snippet->id !== $current_snippet_id && 'php' === $snippet->type; + }); + + // Check each active snippet for function conflicts + foreach ( $active_snippets as $snippet ) { + // Extract function names from the active snippet + preg_match_all( '/function\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/', $snippet->code, $active_matches ); + + if ( ! empty( $active_matches[1] ) ) { + $active_functions = $active_matches[1]; + + // Check for conflicts + $conflicts = array_intersect( $current_functions, $active_functions ); + if ( ! empty( $conflicts ) ) { + $conflict_function = reset( $conflicts ); + + $error = new \stdClass(); + $error->type = 'fatal_error'; + $error->message = "Cannot redeclare {$conflict_function}() (already declared in snippet ID {$snippet->id})"; + $error->line = 1; + $error->file = ''; + return $error; + } + } + } + + return null; // No conflicts detected +} + /** * Retrieve a single snippets from the database using its cloud ID. *