Skip to content

Commit accc0c4

Browse files
committed
fix: backend error detection for syntax and function conflicts, frontend parsing and fatal error handling
1 parent db47a5e commit accc0c4

File tree

3 files changed

+209
-98
lines changed

3 files changed

+209
-98
lines changed

src/js/hooks/useSubmitSnippet.ts

Lines changed: 147 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -7,119 +7,169 @@ import { useRestAPI } from './useRestAPI'
77
import { useSnippetForm } from './useSnippetForm'
88
import type { Snippet } from '../types/Snippet'
99

10-
const snippetMessages = <const> {
11-
addNew: __('Add New Snippet', 'code-snippets'),
12-
edit: __('Edit Snippet', 'code-snippets'),
13-
created: __('Snippet <strong>created</strong>.', 'code-snippets'),
14-
updated: __('Snippet <strong>updated</strong>.', 'code-snippets'),
15-
createdActivated: __('Snippet <strong>created</strong> and <strong>activated</strong>.', 'code-snippets'),
16-
updatedActivated: __('Snippet <strong>updated</strong> and <strong>activated</strong>.', 'code-snippets'),
17-
updatedDeactivated: __('Snippet <strong>updated</strong> and <strong>deactivated</strong>'),
18-
updatedExecuted: __('Snippet <strong>updated</strong> and <strong>executed</strong>.', 'code-snippets'),
19-
failedCreate: __('Could not create snippet.', 'code-snippets'),
20-
failedUpdate: __('Could not update snippet.', 'code-snippets')
10+
const snippetMessages = <const>{
11+
addNew: __('Add New Snippet', 'code-snippets'),
12+
edit: __('Edit Snippet', 'code-snippets'),
13+
created: __('Snippet <strong>created</strong>.', 'code-snippets'),
14+
updated: __('Snippet <strong>updated</strong>.', 'code-snippets'),
15+
createdActivated: __(
16+
'Snippet <strong>created</strong> and <strong>activated</strong>.',
17+
'code-snippets'
18+
),
19+
updatedActivated: __(
20+
'Snippet <strong>updated</strong> and <strong>activated</strong>.',
21+
'code-snippets'
22+
),
23+
updatedDeactivated: __(
24+
'Snippet <strong>updated</strong> and <strong>deactivated</strong>'
25+
),
26+
updatedExecuted: __(
27+
'Snippet <strong>updated</strong> and <strong>executed</strong>.',
28+
'code-snippets'
29+
),
30+
failedCreate: __('Could not create snippet.', 'code-snippets'),
31+
failedUpdate: __('Could not update snippet.', 'code-snippets'),
2132
}
2233

23-
const conditionCreated = __('Condition <strong>created</strong>.', 'code-snippets')
24-
const conditionUpdated = __('Condition <strong>updated</strong>.', 'code-snippets')
34+
const conditionCreated = __(
35+
'Condition <strong>created</strong>.',
36+
'code-snippets'
37+
)
38+
const conditionUpdated = __(
39+
'Condition <strong>updated</strong>.',
40+
'code-snippets'
41+
)
2542

2643
const conditionMessages: typeof snippetMessages = {
27-
addNew: __('Add New Condition', 'code-snippets'),
28-
edit: __('Edit Condition', 'code-snippets'),
29-
created: conditionCreated,
30-
updated: conditionUpdated,
31-
createdActivated: conditionCreated,
32-
updatedActivated: conditionUpdated,
33-
updatedDeactivated: conditionUpdated,
34-
updatedExecuted: conditionUpdated,
35-
failedCreate: __('Could not create condition.', 'code-snippets'),
36-
failedUpdate: __('Could not update condition.', 'code-snippets')
44+
addNew: __('Add New Condition', 'code-snippets'),
45+
edit: __('Edit Condition', 'code-snippets'),
46+
created: conditionCreated,
47+
updated: conditionUpdated,
48+
createdActivated: conditionCreated,
49+
updatedActivated: conditionUpdated,
50+
updatedDeactivated: conditionUpdated,
51+
updatedExecuted: conditionUpdated,
52+
failedCreate: __('Could not create condition.', 'code-snippets'),
53+
failedUpdate: __('Could not update condition.', 'code-snippets'),
3754
}
3855

3956
export enum SubmitSnippetAction {
40-
SAVE = 'save_snippet',
41-
SAVE_AND_ACTIVATE = 'save_snippet_activate',
42-
SAVE_AND_EXECUTE = 'save_snippet_execute',
43-
SAVE_AND_DEACTIVATE = 'save_snippet_deactivate'
57+
SAVE = 'save_snippet',
58+
SAVE_AND_ACTIVATE = 'save_snippet_activate',
59+
SAVE_AND_EXECUTE = 'save_snippet_execute',
60+
SAVE_AND_DEACTIVATE = 'save_snippet_deactivate',
4461
}
4562

46-
const getSuccessNotice = (request: Snippet, response: Snippet, action: SubmitSnippetAction): string => {
47-
const messages = 'condition' === request.scope ? conditionMessages : snippetMessages
48-
const wasCreated = 0 === request.id
49-
50-
switch (action) {
51-
case SubmitSnippetAction.SAVE:
52-
return wasCreated ? messages.created : messages.updated
53-
54-
case SubmitSnippetAction.SAVE_AND_EXECUTE:
55-
return messages.updatedExecuted
56-
57-
case SubmitSnippetAction.SAVE_AND_ACTIVATE:
58-
if ('single-use' === response.scope) {
59-
return messages.updatedExecuted
60-
} else {
61-
return wasCreated
62-
? messages.createdActivated
63-
: messages.updatedActivated
64-
}
65-
66-
case SubmitSnippetAction.SAVE_AND_DEACTIVATE:
67-
return messages.updatedDeactivated
68-
}
63+
const getSuccessNotice = (
64+
request: Snippet,
65+
response: Snippet,
66+
action: SubmitSnippetAction
67+
): string => {
68+
const messages =
69+
'condition' === request.scope ? conditionMessages : snippetMessages
70+
const wasCreated = 0 === request.id
71+
72+
switch (action) {
73+
case SubmitSnippetAction.SAVE:
74+
return wasCreated ? messages.created : messages.updated
75+
76+
case SubmitSnippetAction.SAVE_AND_EXECUTE:
77+
return messages.updatedExecuted
78+
79+
case SubmitSnippetAction.SAVE_AND_ACTIVATE:
80+
if ('single-use' === response.scope) {
81+
return messages.updatedExecuted
82+
} else {
83+
return wasCreated
84+
? messages.createdActivated
85+
: messages.updatedActivated
86+
}
87+
88+
case SubmitSnippetAction.SAVE_AND_DEACTIVATE:
89+
return messages.updatedDeactivated
90+
}
6991
}
7092

7193
const SUBMIT_ACTION_DELTA: Record<SubmitSnippetAction, Partial<Snippet>> = {
72-
[SubmitSnippetAction.SAVE]: {},
73-
[SubmitSnippetAction.SAVE_AND_ACTIVATE]: { active: true },
74-
[SubmitSnippetAction.SAVE_AND_DEACTIVATE]: { active: false },
75-
[SubmitSnippetAction.SAVE_AND_EXECUTE]: { active: true }
94+
[SubmitSnippetAction.SAVE]: {},
95+
[SubmitSnippetAction.SAVE_AND_ACTIVATE]: { active: true },
96+
[SubmitSnippetAction.SAVE_AND_DEACTIVATE]: { active: false },
97+
[SubmitSnippetAction.SAVE_AND_EXECUTE]: { active: true },
7698
}
7799

78100
export interface UseSubmitSnippet {
79-
submitSnippet: (action?: SubmitSnippetAction) => Promise<Snippet | undefined>
101+
submitSnippet: (action?: SubmitSnippetAction) => Promise<Snippet | undefined>
80102
}
81103

82104
export const useSubmitSnippet = (): UseSubmitSnippet => {
83-
const { snippetsAPI } = useRestAPI()
84-
const { setIsWorking, setCurrentNotice, snippet, setSnippet } = useSnippetForm()
85-
86-
const submitSnippet = useCallback(async (action: SubmitSnippetAction = SubmitSnippetAction.SAVE) => {
87-
setCurrentNotice(undefined)
88-
89-
const result = await (async (): Promise<Snippet | string | undefined> => {
90-
try {
91-
const request: Snippet = { ...snippet, ...SUBMIT_ACTION_DELTA[action] }
92-
const response = await (0 === request.id ? snippetsAPI.create(request) : snippetsAPI.update(request))
93-
setIsWorking(false)
94-
return response.id ? response : undefined
95-
} catch (error) {
96-
setIsWorking(false)
97-
return isAxiosError(error) ? error.message : undefined
98-
}
99-
})()
100-
101-
const messages = isCondition(snippet) ? conditionMessages : snippetMessages
102-
103-
if (undefined === result || 'string' === typeof result) {
104-
const message = [
105-
snippet.id ? messages.failedUpdate : messages.failedCreate,
106-
result ?? __('The server did not send a valid response.', 'code-snippets')
107-
]
108-
109-
setCurrentNotice(['error', message.filter(Boolean).join(' ')])
110-
return undefined
111-
} else {
112-
setSnippet(createSnippetObject(result))
113-
setCurrentNotice(['updated', getSuccessNotice(snippet, result, action)])
114-
115-
if (snippet.id && result.id) {
116-
window.document.title = window.document.title.replace(snippetMessages.addNew, messages.edit)
117-
window.history.replaceState({}, '', addQueryArgs(window.CODE_SNIPPETS?.urls.edit, { id: result.id }))
118-
}
119-
120-
return result
121-
}
122-
}, [snippetsAPI, setIsWorking, setCurrentNotice, snippet, setSnippet])
123-
124-
return { submitSnippet }
105+
const { snippetsAPI } = useRestAPI()
106+
const { setIsWorking, setCurrentNotice, snippet, setSnippet } =
107+
useSnippetForm()
108+
109+
const submitSnippet = useCallback(
110+
async (action: SubmitSnippetAction = SubmitSnippetAction.SAVE) => {
111+
setCurrentNotice(undefined)
112+
113+
const result = await (async (): Promise<Snippet | string | undefined> => {
114+
try {
115+
const request: Snippet = {
116+
...snippet,
117+
...SUBMIT_ACTION_DELTA[action],
118+
}
119+
const response = await (0 === request.id
120+
? snippetsAPI.create(request)
121+
: snippetsAPI.update(request))
122+
setIsWorking(false)
123+
return response.id ? response : undefined
124+
} catch (error) {
125+
setIsWorking(false)
126+
return isAxiosError(error) ? error.message : undefined
127+
}
128+
})()
129+
130+
const messages = isCondition(snippet)
131+
? conditionMessages
132+
: snippetMessages
133+
134+
if (undefined === result || 'string' === typeof result) {
135+
const message = [
136+
snippet.id ? messages.failedUpdate : messages.failedCreate,
137+
result ??
138+
__('The server did not send a valid response.', 'code-snippets'),
139+
]
140+
141+
setCurrentNotice(['error', message.filter(Boolean).join(' ')])
142+
return undefined
143+
} else {
144+
setSnippet(createSnippetObject(result))
145+
146+
// Check for code_error and show error if present
147+
if (result.code_error) {
148+
setCurrentNotice(['error', result.code_error[0]])
149+
} else {
150+
setCurrentNotice([
151+
'updated',
152+
getSuccessNotice(snippet, result, action),
153+
])
154+
}
155+
156+
if (snippet.id && result.id) {
157+
window.document.title = window.document.title.replace(
158+
snippetMessages.addNew,
159+
messages.edit
160+
)
161+
window.history.replaceState(
162+
{},
163+
'',
164+
addQueryArgs(window.CODE_SNIPPETS?.urls.edit, { id: result.id })
165+
)
166+
}
167+
168+
return result
169+
}
170+
},
171+
[snippetsAPI, setIsWorking, setCurrentNotice, snippet, setSnippet]
172+
)
173+
174+
return { submitSnippet }
125175
}

src/js/utils/snippets/objects.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,13 @@ export const parseSnippetObject = (fields: unknown): Snippet => {
4747
...'shared_network' in fields && 'boolean' === typeof fields.shared_network && { shared_network: fields.shared_network },
4848
...'priority' in fields && 'number' === typeof fields.priority && { priority: fields.priority },
4949
...'condition_id' in fields && isAbsInt(fields.condition_id) && { conditionId: fields.condition_id }
50+
...('code_error' in fields &&
51+
Array.isArray(fields.code_error) &&
52+
fields.code_error.length === 2 &&
53+
'string' === typeof fields.code_error[0] &&
54+
'number' === typeof fields.code_error[1] && {
55+
code_error: fields.code_error as readonly [string, number],
56+
}),
5057
}
58+
5159
}

src/php/snippet-ops.php

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,16 @@ function test_snippet_code( Snippet $snippet ) {
477477
ucfirst( rtrim( $result->getMessage(), '.' ) ) . '.',
478478
$result->getLine(),
479479
];
480+
} elseif ( $result instanceof Error ) {
481+
$snippet->code_error = [
482+
ucfirst( rtrim( $result->getMessage(), '.' ) ) . '.',
483+
$result->getLine(),
484+
];
485+
} elseif ( is_object( $result ) && isset( $result->type ) && $result->type === 'fatal_error' ) {
486+
$snippet->code_error = [
487+
ucfirst( rtrim( $result->message, '.' ) ) . '.',
488+
$result->line,
489+
];
480490
}
481491
}
482492
}
@@ -507,10 +517,11 @@ function save_snippet( $snippet ) {
507517
$snippet->code = preg_replace( '|^\s*<\?(php)?|', '', $snippet->code );
508518
$snippet->code = preg_replace( '|\?>\s*$|', '', $snippet->code );
509519

510-
// Deactivate snippet if code contains errors.
520+
// Test snippet code when user is trying to activate it
511521
if ( $snippet->active && 'single-use' !== $snippet->scope ) {
512522
test_snippet_code( $snippet );
513523

524+
// Deactivate snippet if code contains errors
514525
if ( $snippet->code_error ) {
515526
$snippet->active = 0;
516527
}
@@ -589,12 +600,22 @@ function execute_snippet( string $code, int $id = 0, bool $force = false ) {
589600
return false;
590601
}
591602

603+
// Since fatal errors cannot be caught with error handlers
604+
// Try to detect function redeclaration by parsing the code
605+
$function_redeclaration_error = detect_function_redeclaration( $code );
606+
if ( $function_redeclaration_error ) {
607+
return $function_redeclaration_error;
608+
}
609+
592610
ob_start();
593611

594612
try {
595613
$result = eval( $code );
596614
} catch ( ParseError $parse_error ) {
597615
$result = $parse_error;
616+
} catch ( Error $error ) {
617+
// Catch other fatal errors
618+
$result = $error;
598619
}
599620

600621
ob_end_clean();
@@ -603,6 +624,38 @@ function execute_snippet( string $code, int $id = 0, bool $force = false ) {
603624
return $result;
604625
}
605626

627+
/**
628+
* Detect function redeclaration errors by checking if functions already exist
629+
*
630+
* @param string $code The code to check
631+
* @return object|null Error object if redeclaration detected, null otherwise
632+
*/
633+
function detect_function_redeclaration( string $code ) {
634+
// Extract function names from the code
635+
preg_match_all( '/function\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/', $code, $matches );
636+
637+
if ( empty( $matches[1] ) ) {
638+
return null; // No functions found
639+
}
640+
641+
$function_names = $matches[1];
642+
643+
// Check if any of these functions already exist
644+
foreach ( $function_names as $function_name ) {
645+
if ( function_exists( $function_name ) ) {
646+
// Create a custom error object that mimics ParseError
647+
$error = new \stdClass();
648+
$error->type = 'fatal_error';
649+
$error->message = "Cannot redeclare {$function_name}() (previously declared)";
650+
$error->line = 1; // We can't determine the exact line easily
651+
$error->file = '';
652+
return $error;
653+
}
654+
}
655+
656+
return null; // No redeclaration detected
657+
}
658+
606659
/**
607660
* Retrieve a single snippets from the database using its cloud ID.
608661
*

0 commit comments

Comments
 (0)