@@ -300,8 +300,7 @@ private function removeTokenInRequest(IncomingRequest $request): void
300300
301301 if (is_object ($ json )) {
302302 if (property_exists ($ json , $ tokenName )) {
303- unset($ json ->{$ tokenName });
304- $ request ->setBody (json_encode ($ json ));
303+ $ request ->setBody ($ this ->removeJsonMember ($ body , $ tokenName ));
305304 }
306305
307306 return ;
@@ -314,6 +313,199 @@ private function removeTokenInRequest(IncomingRequest $request): void
314313 $ request ->setBody (http_build_query ($ result ));
315314 }
316315
316+ /**
317+ * Removes a top-level member from a JSON object without re-encoding the
318+ * document, so the original formatting of the remaining data is preserved.
319+ */
320+ private function removeJsonMember (string $ json , string $ member ): string
321+ {
322+ $ length = strlen ($ json );
323+ $ pos = $ this ->skipJsonWhitespace ($ json , 0 );
324+
325+ if ($ pos >= $ length || $ json [$ pos ] !== '{ ' ) {
326+ return $ json ;
327+ }
328+
329+ $ pos ++;
330+
331+ $ memberStart = null ;
332+ $ memberEnd = null ;
333+ $ commaAfter = null ;
334+
335+ while ($ pos < $ length ) {
336+ $ pos = $ this ->skipJsonWhitespace ($ json , $ pos );
337+
338+ if ($ pos >= $ length || $ json [$ pos ] === '} ' ) {
339+ break ;
340+ }
341+
342+ if ($ json [$ pos ] !== '" ' ) {
343+ return $ json ;
344+ }
345+
346+ $ keyStart = $ pos ;
347+ $ pos = $ this ->skipJsonString ($ json , $ pos );
348+ $ key = json_decode (substr ($ json , $ keyStart , $ pos - $ keyStart ));
349+
350+ $ pos = $ this ->skipJsonWhitespace ($ json , $ pos );
351+
352+ if ($ pos >= $ length || $ json [$ pos ] !== ': ' ) {
353+ return $ json ;
354+ }
355+
356+ $ pos = $ this ->skipJsonWhitespace ($ json , $ pos + 1 );
357+ $ pos = $ this ->skipJsonValue ($ json , $ pos );
358+
359+ if ($ key === $ member ) {
360+ $ memberStart = $ keyStart ;
361+ $ memberEnd = $ pos ;
362+
363+ // Drop the comma and the whitespace that follows it when there
364+ // is a next member.
365+ $ after = $ this ->skipJsonWhitespace ($ json , $ pos );
366+
367+ if ($ after < $ length && $ json [$ after ] === ', ' ) {
368+ $ commaAfter = $ this ->skipJsonWhitespace ($ json , $ after + 1 );
369+ }
370+
371+ break ;
372+ }
373+
374+ // Skip the separator comma between members.
375+ $ pos = $ this ->skipJsonWhitespace ($ json , $ pos );
376+
377+ if ($ pos < $ length && $ json [$ pos ] === ', ' ) {
378+ $ pos ++;
379+ }
380+ }
381+
382+ if ($ memberStart === null ) {
383+ return $ json ;
384+ }
385+
386+ if ($ commaAfter !== null ) {
387+ // The member is not the last one.
388+ return substr ($ json , 0 , $ memberStart ) . substr ($ json , $ commaAfter );
389+ }
390+
391+ // The member is the last one: drop the preceding comma if there is one.
392+ $ before = $ memberStart - 1 ;
393+
394+ while ($ before >= 0 && ctype_space ($ json [$ before ])) {
395+ $ before --;
396+ }
397+
398+ if ($ before >= 0 && $ json [$ before ] === ', ' ) {
399+ return substr ($ json , 0 , $ before ) . substr ($ json , $ memberEnd );
400+ }
401+
402+ // The member is the only one: keep the surrounding whitespace clean.
403+ $ open = $ memberStart ;
404+
405+ while ($ open > 0 && ctype_space ($ json [$ open - 1 ])) {
406+ $ open --;
407+ }
408+
409+ return substr ($ json , 0 , $ open ) . substr ($ json , $ memberEnd );
410+ }
411+
412+ /**
413+ * Returns the position just after the JSON string that starts at the given
414+ * position (which must point to the opening quote).
415+ */
416+ private function skipJsonString (string $ json , int $ pos ): int
417+ {
418+ $ length = strlen ($ json );
419+ $ pos ++;
420+
421+ while ($ pos < $ length ) {
422+ if ($ json [$ pos ] === '\\' ) {
423+ $ pos += 2 ;
424+
425+ continue ;
426+ }
427+
428+ if ($ json [$ pos ] === '" ' ) {
429+ return $ pos + 1 ;
430+ }
431+
432+ $ pos ++;
433+ }
434+
435+ return $ pos ;
436+ }
437+
438+ /**
439+ * Returns the position after the JSON value that starts at the given
440+ * position.
441+ */
442+ private function skipJsonValue (string $ json , int $ pos ): int
443+ {
444+ $ length = strlen ($ json );
445+
446+ if ($ pos >= $ length ) {
447+ return $ pos ;
448+ }
449+
450+ $ char = $ json [$ pos ];
451+
452+ if ($ char === '" ' ) {
453+ return $ this ->skipJsonString ($ json , $ pos );
454+ }
455+
456+ if ($ char !== '{ ' && $ char !== '[ ' ) {
457+ // Number, true, false or null.
458+ while ($ pos < $ length && ! ctype_space ($ json [$ pos ]) && $ json [$ pos ] !== ', ' && $ json [$ pos ] !== '} ' && $ json [$ pos ] !== '] ' ) {
459+ $ pos ++;
460+ }
461+
462+ return $ pos ;
463+ }
464+
465+ $ open = $ char ;
466+ $ close = $ char === '{ ' ? '} ' : '] ' ;
467+ $ depth = 0 ;
468+
469+ while ($ pos < $ length ) {
470+ $ current = $ json [$ pos ];
471+
472+ if ($ current === '" ' ) {
473+ $ pos = $ this ->skipJsonString ($ json , $ pos );
474+
475+ continue ;
476+ }
477+
478+ if ($ current === $ open ) {
479+ $ depth ++;
480+ } elseif ($ current === $ close ) {
481+ $ depth --;
482+
483+ if ($ depth === 0 ) {
484+ return $ pos + 1 ;
485+ }
486+ }
487+
488+ $ pos ++;
489+ }
490+
491+ return $ pos ;
492+ }
493+
494+ /**
495+ * Returns the position just after the whitespace starting at the given
496+ * position.
497+ */
498+ private function skipJsonWhitespace (string $ json , int $ pos ): int
499+ {
500+ $ length = strlen ($ json );
501+
502+ while ($ pos < $ length && ctype_space ($ json [$ pos ])) {
503+ $ pos ++;
504+ }
505+
506+ return $ pos ;
507+ }
508+
317509 private function getPostedToken (IncomingRequest $ request ): ?string
318510 {
319511 $ tokenName = $ this ->config ->tokenName ;
0 commit comments