@@ -222,92 +222,133 @@ static function (array $m): string {
222222 */
223223 private static function normalizeBacktraceFrames (string $ html ): string
224224 {
225- $ html = preg_replace_callback (
226- "#<details class='frame(?P<class>[^']*)'(?P<open> \\s+open)?>(?P<body>.*?)</details>#s " ,
227- static function (array $ match ): string {
228- $ body = $ match ['body ' ];
229-
230- if (!preg_match (
231- "# \\s*<summary> \\s*(<div class='frame-head'>.*?</div>) \\s*</summary>#s " ,
232- $ body ,
233- $ summary
234- )) {
235- return $ match [0 ];
236- }
225+ $ offset = 0 ;
226+ $ normalized = '' ;
227+ $ openNeedle = "<details class='frame " ;
228+ $ closeNeedle = '</details> ' ;
237229
238- $ head = preg_replace ("# \\s*<span class='chev'>.*?</span>#s " , '' , $ summary [1 ]);
239- $ head = self ::requireRenderedHtml ($ head , 'removing the frame-level backtrace chevron ' );
240- $ body = str_replace ($ summary [0 ], '' , $ body );
230+ while (($ start = strpos ($ html , $ openNeedle , $ offset )) !== false ) {
231+ $ openEnd = strpos ($ html , '> ' , $ start );
241232
242- $ fileBlock = '' ;
243- $ file = null ;
244- $ line = null ;
233+ if ( $ openEnd === false ) {
234+ break ;
235+ }
245236
246- if (preg_match ("# \\s*(<div class='frame-file'[^>]*>.*?</div>)#s " , $ body , $ fileMatch )) {
247- $ fileBlock = $ fileMatch [1 ];
248- $ body = str_replace ($ fileMatch [0 ], '' , $ body );
249- $ file = self ::readHtmlAttribute ($ fileBlock , 'data-file ' );
250- $ lineValue = self ::readHtmlAttribute ($ fileBlock , 'data-line ' );
251- $ line = is_numeric ($ lineValue ) ? (int ) $ lineValue : null ;
252- }
237+ $ closeStart = strpos ($ html , $ closeNeedle , $ openEnd + 1 );
253238
254- $ codeBlock = '' ;
239+ if ($ closeStart === false ) {
240+ break ;
241+ }
255242
256- if ( preg_match ( " # \\ s*(<div class='code'>.*?</div>)#s " , $ body , $ codeMatch )) {
257- $ codeBlock = $ codeMatch [ 1 ] ;
258- $ body = str_replace ( $ codeMatch [ 0 ], '' , $ body );
259- }
243+ $ closeEnd = $ closeStart + strlen ( $ closeNeedle );
244+ $ openingTag = substr ( $ html , $ start , $ openEnd - $ start + 1 ) ;
245+ $ body = substr ( $ html , $ openEnd + 1 , $ closeStart - $ openEnd - 1 );
246+ $ originalFrame = substr ( $ html , $ start , $ closeEnd - $ start );
260247
261- $ isCodeOpen = ($ match ['open ' ] ?? '' ) !== '' ;
262- $ classes = trim (
263- 'frame '
264- . $ match ['class ' ]
265- . ($ codeBlock !== '' ? ' has-source ' : ' no-source ' )
266- . ($ codeBlock !== '' && $ isCodeOpen ? ' is-code-open ' : '' )
267- );
248+ $ normalized .= substr ($ html , $ offset , $ start - $ offset );
249+ $ normalized .= self ::normalizeBacktraceFrame ($ openingTag , $ body , $ originalFrame );
250+ $ offset = $ closeEnd ;
251+ }
268252
269- if ($ codeBlock !== '' ) {
270- $ head = self :: makeBacktraceFrameHeadToggle ( $ head , $ isCodeOpen ) ;
271- }
253+ if ($ offset === 0 ) {
254+ return $ html ;
255+ }
272256
273- $ frame = "\n <article class=' " . htmlspecialchars ($ classes , ENT_QUOTES ) . "'> \n {$ head }" ;
257+ return $ normalized . substr ($ html , $ offset );
258+ }
274259
275- if ($ fileBlock !== '' ) {
276- $ frame .= "\n {$ fileBlock }" ;
277- }
260+ /**
261+ * Normalize one native debug frame after the frame boundary is known.
262+ *
263+ * Frame extraction intentionally avoids a document-wide regex so large
264+ * backtraces cannot exhaust PCRE backtracking before the per-frame rewrite
265+ * runs. If Phalcon changes the opening tag shape, the original frame is
266+ * preserved instead of dropping debug output.
267+ *
268+ * @throws RuntimeException When an internal frame rewrite fails.
269+ */
270+ private static function normalizeBacktraceFrame (string $ openingTag , string $ body , string $ originalFrame ): string
271+ {
272+ if (!preg_match ("#^<details class='frame(?P<class>[^']*)'(?P<open> \\s+open)? \\s*>$#s " , $ openingTag , $ match )) {
273+ return $ originalFrame ;
274+ }
278275
279- if ($ codeBlock !== '' ) {
280- $ fullFileTemplate = self ::buildFullFileSourceTemplate ($ file , $ line );
281- $ fullFileButton = $ fullFileTemplate !== ''
282- ? "\n <button class='btn code-btn' "
283- . "data-action='toggle-full-file'>Show full file</button> "
284- : '' ;
285-
286- $ hidden = $ isCodeOpen ? '' : ' hidden ' ;
287- $ frame .= "\n <div class='frame-code-body' {$ hidden }> "
288- . "\n <div class='code-shell'> "
289- . "\n {$ codeBlock }"
290- . "\n <div class='code-actions'> "
291- . "\n <button class='btn code-btn' data-action='focus-line'>Focus line</button> "
292- . str_replace ("\n " , "\n " , $ fullFileButton )
293- . "\n </div> "
294- . "\n </div> "
295- . $ fullFileTemplate
296- . "\n </div> " ;
297- }
276+ if (!preg_match (
277+ "# \\s*<summary> \\s*(<div class='frame-head'>.*?</div>) \\s*</summary>#s " ,
278+ $ body ,
279+ $ summary
280+ )) {
281+ return $ originalFrame ;
282+ }
298283
299- $ extra = trim ($ body );
284+ $ head = preg_replace ("# \\s*<span class='chev'>.*?</span>#s " , '' , $ summary [1 ]);
285+ $ head = self ::requireRenderedHtml ($ head , 'removing the frame-level backtrace chevron ' );
286+ $ body = str_replace ($ summary [0 ], '' , $ body );
300287
301- if ( $ extra !== '' ) {
302- $ frame .= "\n <div class='frame-extra'> { $ extra } </div> " ;
303- }
288+ $ fileBlock = '' ;
289+ $ file = null ;
290+ $ line = null ;
304291
305- return $ frame . "\n </article> " ;
306- },
307- $ html
292+ if (preg_match ("# \\s*(<div class='frame-file'[^>]*>.*?</div>)#s " , $ body , $ fileMatch )) {
293+ $ fileBlock = $ fileMatch [1 ];
294+ $ body = str_replace ($ fileMatch [0 ], '' , $ body );
295+ $ file = self ::readHtmlAttribute ($ fileBlock , 'data-file ' );
296+ $ lineValue = self ::readHtmlAttribute ($ fileBlock , 'data-line ' );
297+ $ line = is_numeric ($ lineValue ) ? (int ) $ lineValue : null ;
298+ }
299+
300+ $ codeBlock = '' ;
301+
302+ if (preg_match ("# \\s*(<div class='code'>.*?</div>)#s " , $ body , $ codeMatch )) {
303+ $ codeBlock = $ codeMatch [1 ];
304+ $ body = str_replace ($ codeMatch [0 ], '' , $ body );
305+ }
306+
307+ $ isCodeOpen = ($ match ['open ' ] ?? '' ) !== '' ;
308+ $ classes = trim (
309+ 'frame '
310+ . $ match ['class ' ]
311+ . ($ codeBlock !== '' ? ' has-source ' : ' no-source ' )
312+ . ($ codeBlock !== '' && $ isCodeOpen ? ' is-code-open ' : '' )
308313 );
309314
310- return self ::requireRenderedHtml ($ html , 'normalizing backtrace frame layout ' );
315+ if ($ codeBlock !== '' ) {
316+ $ head = self ::makeBacktraceFrameHeadToggle ($ head , $ isCodeOpen );
317+ }
318+
319+ $ frame = "\n <article class=' " . htmlspecialchars ($ classes , ENT_QUOTES ) . "'> \n {$ head }" ;
320+
321+ if ($ fileBlock !== '' ) {
322+ $ frame .= "\n {$ fileBlock }" ;
323+ }
324+
325+ if ($ codeBlock !== '' ) {
326+ $ fullFileTemplate = self ::buildFullFileSourceTemplate ($ file , $ line );
327+ $ fullFileButton = $ fullFileTemplate !== ''
328+ ? "\n <button class='btn code-btn' "
329+ . "data-action='toggle-full-file'>Show full file</button> "
330+ : '' ;
331+
332+ $ hidden = $ isCodeOpen ? '' : ' hidden ' ;
333+ $ frame .= "\n <div class='frame-code-body' {$ hidden }> "
334+ . "\n <div class='code-shell'> "
335+ . "\n {$ codeBlock }"
336+ . "\n <div class='code-actions'> "
337+ . "\n <button class='btn code-btn' data-action='focus-line'>Focus line</button> "
338+ . str_replace ("\n " , "\n " , $ fullFileButton )
339+ . "\n </div> "
340+ . "\n </div> "
341+ . $ fullFileTemplate
342+ . "\n </div> " ;
343+ }
344+
345+ $ extra = trim ($ body );
346+
347+ if ($ extra !== '' ) {
348+ $ frame .= "\n <div class='frame-extra'> {$ extra }</div> " ;
349+ }
350+
351+ return $ frame . "\n </article> " ;
311352 }
312353
313354 /**
0 commit comments