story = $story; } /** * Renders the story. * * @since 1.0.0 * * @return string The complete HTML markup for the story. */ public function render(): string { $markup = $this->story->get_markup(); $markup = $this->fix_incorrect_charset( $markup ); $markup = $this->fix_malformed_script_link_tags( $markup ); $markup = $this->replace_html_head( $markup ); $markup = wp_replace_insecure_home_url( $markup ); $markup = $this->print_analytics( $markup ); $markup = $this->print_social_share( $markup ); return $markup; } /** * Fix incorrect tags. * * React/JSX outputs the charset attribute name as "charSet", * but libdom and the AMP toolbox only recognize lowercase "charset" * * @since 1.28.0 * * @param string $content Story markup. * @return string Filtered content */ public function fix_incorrect_charset( string $content ): string { return (string) preg_replace( '//i', '', $content ); } /** * Fix malformed tags in the . * * On certain environments like WordPress.com VIP, there is additional KSES * hardening that prevents saving `` * and `https://cdn.ampproject.org/v0/amp-story-1.0.js` * into ``. * * @since 1.13.0 * * @param string $content Story markup. * @return string Filtered content */ protected function fix_malformed_script_link_tags( string $content ): string { $replaced_content = preg_replace_callback( '/]+href="(?P[^"]+)"[^>]*>\1<\/a>/m', static function ( $matches ) { if ( str_starts_with( $matches['href'], 'https://cdn.ampproject.org/' ) ) { $script_url = $matches['href']; // Turns `https://cdn.ampproject.org/v0.js` // into ``. if ( 'https://cdn.ampproject.org/v0.js' === $script_url ) { return ""; // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript } // Extract 'amp-story' from 'https://cdn.ampproject.org/v0/amp-story-1.0.js'. $sub_matches = []; preg_match( '/v0\/(?P[\w-]+)-[\d.]+\.js/', $script_url, $sub_matches ); $custom_element = $sub_matches['custom_element']; // Turns `https://cdn.ampproject.org/v0/amp-story-1.0.js` // into . return ""; // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript } return $matches[0]; }, $content ); // On errors the return value of preg_replace_callback() is null. return $replaced_content ?: $content; } /** * Returns the full HTML markup for a given story besides boilerplate. * * @since 1.0.0 * * @return string Filtered content. */ protected function get_html_head_markup(): string { ob_start(); ?> with dynamic content. * * @since 1.0.0 * * @param string $content Story markup. * @return string Filtered content. */ protected function replace_html_head( string $content ): string { $start_tag = ''; $end_tag = ''; // Replace malformed meta tags with correct tags. $content = (string) preg_replace( '//i', $start_tag, $content ); $content = (string) preg_replace( '//i', $end_tag, $content ); $start_tag_pos = strpos( $content, $start_tag ); $end_tag_pos = strpos( $content, $end_tag ); if ( false !== $start_tag_pos && false !== $end_tag_pos ) { $end_tag_pos += \strlen( $end_tag ); $content = substr_replace( $content, $this->get_html_head_markup(), $start_tag_pos, $end_tag_pos - $start_tag_pos ); } return $content; } /** * Print analytics code before closing ``. * * @since 1.2.0 * * @param string $content String to replace. */ protected function print_analytics( string $content ): string { ob_start(); /** * Fires before the closing tag. * * Can be used to print configuration. * * @since 1.1.0 */ do_action( 'web_stories_print_analytics' ); $output = (string) ob_get_clean(); return str_replace( '', $output . '', $content ); } /** * Print amp-story-social-share before closing ``. * * @since 1.6.0 * * @param string $content String to replace. */ protected function print_social_share( string $content ): string { $share_providers = [ [ 'provider' => 'twitter', ], [ 'provider' => 'linkedin', ], [ 'provider' => 'email', ], [ 'provider' => 'system', ], ]; /** * Filters the list of sharing providers in the Web Stories sharing dialog. * * @since 1.3.0 * * @link https://amp.dev/documentation/components/amp-social-share/?format=stories#pre-configured-providers * * @param array[] $share_providers List of sharing providers. */ $share_providers = apply_filters( 'web_stories_share_providers', $share_providers ); if ( empty( $share_providers ) ) { return $content; } $config = [ 'shareProviders' => $share_providers, ]; $social_share = sprintf( '', wp_json_encode( $config ) ); return str_replace( '', $social_share . '', $content ); } }