@@ -3736,7 +3736,26 @@ public function set_modifiable_text( string $plaintext_content ): bool {
37363736 * that previously worked. Resolve this by not sending `</script`
37373737 */
37383738 if ( false !== stripos ( $ plaintext_content , '</script ' ) ) {
3739- return false ;
3739+ /*
3740+ * JavaScript can be safely escaped.
3741+ * Non-JavaScript script tags have unknown semantics.
3742+ *
3743+ * @todo consider applying to JSON and importmap script tags as well.
3744+ */
3745+ if ( $ this ->is_javascript_script_tag () ) {
3746+ $ plaintext_content = preg_replace_callback (
3747+ '~<(/?)(s)(cript)~i ' ,
3748+ static function ( $ matches ) {
3749+ $ escaped_s_char = 's ' === $ matches [2 ]
3750+ ? '\u0073 '
3751+ : '\u0053 ' ;
3752+ return "< {$ matches [1 ]}{$ escaped_s_char }{$ matches [3 ]}" ;
3753+ },
3754+ $ plaintext_content
3755+ );
3756+ } else {
3757+ return false ;
3758+ }
37403759 }
37413760
37423761 $ this ->lexical_updates ['modifiable text ' ] = new WP_HTML_Text_Replacement (
@@ -3794,6 +3813,123 @@ static function ( $tag_match ) {
37943813 return false ;
37953814 }
37963815
3816+ /**
3817+ * Indicates if the currently matched tag is a JavaScript script tag.
3818+ *
3819+ * @see https://html.spec.whatwg.org/multipage/scripting.html#prepare-the-script-element
3820+ *
3821+ * @since {WP_VERSION}
3822+ *
3823+ * @return boolean True if the script tag will be evaluated as JavaScript.
3824+ */
3825+ public function is_javascript_script_tag (): bool {
3826+ if ( 'SCRIPT ' !== $ this ->get_tag () || $ this ->get_namespace () !== 'html ' ) {
3827+ return false ;
3828+ }
3829+
3830+ /*
3831+ * > If any of the following are true:
3832+ * > - el has a type attribute whose value is the empty string;
3833+ * > - el has no type attribute but it has a language attribute and that attribute's
3834+ * > value is the empty string; or
3835+ * > - el has neither a type attribute nor a language attribute,
3836+ * > then let the script block's type string for this script element be "text/javascript".
3837+ */
3838+ $ type_attr = $ this ->get_attribute ( 'type ' );
3839+ $ language_attr = $ this ->get_attribute ( 'language ' );
3840+
3841+ if ( true === $ type_attr || '' === $ type_attr ) {
3842+ return true ;
3843+ }
3844+ if (
3845+ null === $ type_attr && (
3846+ true === $ language_attr ||
3847+ '' === $ language_attr ||
3848+ null === $ language_attr
3849+ )
3850+ ) {
3851+ return true ;
3852+ }
3853+
3854+ /*
3855+ * > Otherwise, if el has a type attribute, then let the script block's type string be
3856+ * > the value of that attribute with leading and trailing ASCII whitespace stripped.
3857+ * > Otherwise, el has a non-empty language attribute; let the script block's type string
3858+ * > be the concatenation of "text/" and the value of el's language attribute.
3859+ */
3860+ $ type_string = $ type_attr ? trim ( $ type_attr , " \t\f\r\n" ) : "text/ {$ language_attr }" ;
3861+
3862+ /*
3863+ * > If the script block's type string is a JavaScript MIME type essence match, then
3864+ * > set el's type to "classic".
3865+ *
3866+ * > A string is a JavaScript MIME type essence match if it is an ASCII case-insensitive
3867+ * > match for one of the JavaScript MIME type essence strings.
3868+
3869+ * > A JavaScript MIME type is any MIME type whose essence is one of the following:
3870+ * >
3871+ * > - application/ecmascript
3872+ * > - application/javascript
3873+ * > - application/x-ecmascript
3874+ * > - application/x-javascript
3875+ * > - text/ecmascript
3876+ * > - text/javascript
3877+ * > - text/javascript1.0
3878+ * > - text/javascript1.1
3879+ * > - text/javascript1.2
3880+ * > - text/javascript1.3
3881+ * > - text/javascript1.4
3882+ * > - text/javascript1.5
3883+ * > - text/jscript
3884+ * > - text/livescript
3885+ * > - text/x-ecmascript
3886+ * > - text/x-javascript
3887+ *
3888+ * @see https://mimesniff.spec.whatwg.org/#javascript-mime-type
3889+ * @see https://mimesniff.spec.whatwg.org/#javascript-mime-type-essence-match
3890+ */
3891+ switch ( strtolower ( $ type_string ) ) {
3892+ case 'application/ecmascript ' :
3893+ case 'application/javascript ' :
3894+ case 'application/x-ecmascript ' :
3895+ case 'application/x-javascript ' :
3896+ case 'text/ecmascript ' :
3897+ case 'text/javascript ' :
3898+ case 'text/javascript1.0 ' :
3899+ case 'text/javascript1.1 ' :
3900+ case 'text/javascript1.2 ' :
3901+ case 'text/javascript1.3 ' :
3902+ case 'text/javascript1.4 ' :
3903+ case 'text/javascript1.5 ' :
3904+ case 'text/jscript ' :
3905+ case 'text/livescript ' :
3906+ case 'text/x-ecmascript ' :
3907+ case 'text/x-javascript ' :
3908+ return true ;
3909+
3910+ /*
3911+ * > Otherwise, if the script block's type string is an ASCII case-insensitive match for
3912+ * > the string "module", then set el's type to "module".
3913+ *
3914+ * A module is evaluated as JavaScript
3915+ */
3916+ case 'module ' :
3917+ return true ;
3918+ }
3919+
3920+ /*
3921+ * > - Otherwise, if the script block's type string is an ASCII case-insensitive match for
3922+ * > the string "importmap", then set el's type to "importmap".
3923+ *
3924+ * An importmap is JSON and not evaluated as JavaScript. This case is not handled here.
3925+ */
3926+
3927+ /*
3928+ * > Otherwise, return. (No script is executed, and el's type is left as null.)
3929+ */
3930+ return false ;
3931+ }
3932+
37973933 /**
37983934 * Updates or creates a new attribute on the currently matched tag with the passed value.
37993935 *
0 commit comments