diff --git a/admin/class-create-block-theme-admin.php b/admin/class-create-block-theme-admin.php index 28a530a9..7c6b498e 100644 --- a/admin/class-create-block-theme-admin.php +++ b/admin/class-create-block-theme-admin.php @@ -556,6 +556,428 @@ function get_theme_templates( $export_type, $new_slug ) { } + function is_absolute_url( $url ) { + return ! empty( $url ) && isset( parse_url( $url )[ 'host' ] ); + } + + function make_relative_media_url ( $absolute_url ) { + if ( ! empty ( $absolute_url ) && $this->is_absolute_url( $absolute_url ) ) { + $folder_path = $this->get_media_folder_path_from_url( $absolute_url ); + return '' . $folder_path . basename( $absolute_url ); + } + return $absolute_url; + } + + function make_html_media_local ( $html ) { + if ( empty( $html ) ) { + return $html; + } + + // If the WP_HTML_Tag_Processor class exists, use it to parse the HTML. + // This API was recently in Gutenberg and not yet available in WordPress core. https://github.com/WordPress/gutenberg/pull/42485 + // If it's not available, fallb ack to DOMDocument which can not be installed in all the systems and has some issues. + // When WP_HTML_Tag_Processor is availabe in core (6.2) we can remove the DOMDocument fallback. + if ( class_exists( 'WP_HTML_Tag_Processor' ) ) { + $html = new WP_HTML_Tag_Processor( $html ); + while ( $html->next_tag( 'img' ) ) { + if ( $this->is_absolute_url( $html->get_attribute( 'src' ) ) ) { + $html->set_attribute( 'src', $this->make_relative_media_url( $html->get_attribute( 'src' ) ) ); + } + } + $html = new WP_HTML_Tag_Processor( $html->__toString() ); + while ( $html->next_tag( 'video' ) ) { + if ( $this->is_absolute_url( $html->get_attribute( 'src' ) ) ) { + $html->set_attribute( 'src', $this->make_relative_media_url( $html->get_attribute( 'src' ) ) ); + } + if ( $this->is_absolute_url( $html->get_attribute( 'poster' ) ) ) { + $html->set_attribute( 'poster', $this->make_relative_media_url( $html->get_attribute( 'poster' ) ) ); + } + } + $html = new WP_HTML_Tag_Processor( $html->__toString() ); + while ( $html->next_tag( 'div' ) ) { + $style = $html->get_attribute( 'style' ); + if ( $style ) { + preg_match_all('#\bhttps?://[^,\s()<>]+(?:\([\w\d]+\)|([^,[:punct:]\s]|/))#', $style, $match); + $urls = $match[0]; + foreach ( $urls as $url ) { + if ( $this->is_absolute_url( $url ) ) { + $html->set_attribute( 'style', str_replace( $url, $this->make_relative_media_url( $url ), $style ) ); + } + } + } + } + return $html->__toString(); + } + + // Fallback to DOMDocument. + // TODO: When WP_HTML_Tag_Processor is availabe in core (6.2) we can remove this implementation entirely. + if ( ! class_exists( 'WP_HTML_Tag_Processor' ) ) { + $doc = new DOMDocument(); + @$doc->loadHTML( $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD ); + // replace all images that have absolute urls + $img_tags = $doc->getElementsByTagName( 'img' ); + foreach ( $img_tags as $tag ) { + $image_url = $tag->getAttribute( 'src' ); + if ( $this->is_absolute_url( $image_url ) ) { + $img_src = $tag->getAttribute( 'src' ); + $html = str_replace( $img_src, $this->make_relative_media_url( $img_src ), $html ); + } + } + // replace all video that have absolute urls + $video_tags = $doc->getElementsByTagName( 'video' ); + foreach ( $video_tags as $tag ) { + $video_url = $tag->getAttribute( 'src' ); + if ( !empty( $video_url ) && $this->is_absolute_url( $video_url ) ) { + $video_src = $tag->getAttribute( 'src' ); + $html = str_replace( $video_src, $this->make_relative_media_url( $video_src ), $html ); + } + $poster_url = $tag->getAttribute( 'poster' ); + if ( !empty ( $poster_url ) && $this->is_absolute_url( $poster_url ) ) { + $html = str_replace( $poster_url, $this->make_relative_media_url( $poster_url ), $html ); + } + } + // also replace background images with absolute urls (used in cover blocks) + $div_tags = $doc->getElementsByTagName( 'div' ); + foreach ( $div_tags as $tag ) { + $style = $tag->getAttribute( 'style' ); + if ( $style ) { + preg_match_all('#\bhttps?://[^,\s()<>]+(?:\([\w\d]+\)|([^,[:punct:]\s]|/))#', $style, $match); + $urls = $match[0]; + foreach ( $urls as $url ) { + if ( $this->is_absolute_url( $url ) ) { + $html = str_replace( $url, $this->make_relative_media_url( $url ), $html ); + } + } + } + } + return $html; + } + + } + + function make_image_video_block_local ( $block ) { + if ( 'core/image' === $block[ 'blockName' ] || 'core/video' === $block[ 'blockName' ] ) { + $inner_html = $this->make_html_media_local( $block[ 'innerHTML' ] ); + $inner_html = $this->escape_alt_for_pattern ( $inner_html ); + $block['innerHTML'] = $inner_html; + $block['innerContent'] = array ( $inner_html ); + } + return $block; + } + + function make_cover_block_local ( $block ) { + if ( 'core/cover' === $block[ 'blockName' ] ) { + $inner_html = $this->make_html_media_local( $block[ 'innerHTML' ] ); + $inner_html = $this->escape_alt_for_pattern ( $inner_html ); + $inner_content = []; + foreach ( $block['innerContent'] as $content ) { + $content_html = $this->make_html_media_local( $content ); + $content_html = $this->escape_alt_for_pattern ( $content_html ); + $inner_content[] = $content_html; + } + $block['innerHTML'] = $inner_html; + $block['innerContent'] = $inner_content; + if ( isset ( $block['attrs']['url'] ) && $this->is_absolute_url( $block['attrs']['url'] ) ) { + $block['attrs']['url'] = $this->make_relative_media_url( $block['attrs']['url'] ); + } + } + return $block; + } + + function make_mediatext_block_local ( $block ) { + if ( 'core/media-text' === $block[ 'blockName' ] ) { + $inner_html = $this->make_html_media_local( $block[ 'innerHTML' ] ); + $inner_html = $this->escape_alt_for_pattern ( $inner_html ); + $inner_content = []; + foreach ( $block['innerContent'] as $content ) { + $content_html = $this->make_html_media_local( $content ); + $content_html = $this->escape_alt_for_pattern ( $content_html ); + $inner_content[] = $content_html; + } + $block['innerHTML'] = $inner_html; + $block['innerContent'] = $inner_content; + if ( isset ( $block['attrs']['mediaLink'] ) && $this->is_absolute_url( $block['attrs']['mediaLink'] ) ) { + $block['attrs']['mediaLink'] = $this->make_relative_media_url( $block['attrs']['mediaLink'] ); + } + } + return $block; + } + + function add_theme_attr_to_template_part_block ( $block ) { + // The template parts included in the patterns need to indicate the theme they belong to + if ( 'core/template-part' === $block[ 'blockName' ] ) { + $block['attrs']['theme'] = ( $_POST['theme']['type'] === "export" || $_POST['theme']['type'] === "save" ) + ? strtolower( wp_get_theme()->get( 'Name' ) ) + : $_POST['theme']['name']; + } + return $block; + } + + function make_media_blocks_local ( $nested_blocks ) { + $new_blocks = []; + foreach ( $nested_blocks as $block ) { + $inner_blocks = $block['innerBlocks']; + switch ( $block[ 'blockName' ] ) { + case 'core/image': + case 'core/video': + $block = $this->make_image_video_block_local( $block ); + break; + case 'core/cover': + $block = $this->make_cover_block_local( $block ); + break; + case 'core/media-text': + $block = $this->make_mediatext_block_local( $block ); + break; + case 'core/template-part': + $block = $this->add_theme_attr_to_template_part_block( $block ); + break; + } + // recursive call for inner blocks + if ( !empty ( $block['innerBlocks'] ) ) { + $block['innerBlocks'] = $this->make_media_blocks_local( $inner_blocks ); + } + $new_blocks[] = $block; + } + return $new_blocks; + } + + function get_media_absolute_urls_from_blocks ( $flatten_blocks ) { + $media = []; + + // If WP_HTML_Tag_Processor is available, use it to get the absolute URLs of img and background images + // This class is available in core yet, but it will be available in the future (6.2) + // see https://github.com/WordPress/gutenberg/pull/42485 + if ( class_exists( 'WP_HTML_Tag_Processor' ) ) { + foreach ( $flatten_blocks as $block ) { + // Gets the absolute URLs of img in these blocks + if ( + 'core/image' === $block[ 'blockName' ] || + 'core/video' === $block[ 'blockName' ] || + 'core/cover' === $block[ 'blockName' ] || + 'core/media-text' === $block[ 'blockName' ] + ) { + $html = new WP_HTML_Tag_Processor( $block[ 'innerHTML' ] ); + while ( $html->next_tag( 'img' ) ) { + $url = $html->get_attribute( 'src' ); + if ( $this->is_absolute_url( $url ) ) { + $media[] = $url; + } + } + $html = new WP_HTML_Tag_Processor( $html->__toString() ); + while ( $html->next_tag( 'video' ) ) { + $url = $html->get_attribute( 'src' ); + if ( $this->is_absolute_url( $url ) ) { + $media[] = $url; + } + $poster_url = $html->get_attribute( 'poster' ); + if ( $this->is_absolute_url( $poster_url ) ) { + $media[] = $poster_url; + } + } + } + + // Gets the absolute URLs of background images in these blocks + if ( 'core/cover' === $block['blockName'] ) { + $html = new WP_HTML_Tag_Processor( $block[ 'innerHTML' ] ); + while ( $html->next_tag( 'div' ) ) { + $style = $html->get_attribute( 'style' ); + if ( $style ) { + $matches = []; + preg_match( '/background-image: url\((.*)\)/', $style, $matches ); + if ( isset( $matches[1] ) ) { + $url = $matches[1]; + if ( $this->is_absolute_url( $url ) ) { + $media[] = $url; + } + } + } + } + } + + } + } + + // Fallback to DOMDocument. + // TODO: When WP_HTML_Tag_Processor is availabe in core (6.2) we can remove this implementation entirely. + if ( ! class_exists ( 'WP_HTML_Tag_Processor' ) ) { + foreach ( $flatten_blocks as $block ) { + if ( + 'core/image' === $block[ 'blockName' ] || + 'core/video' === $block[ 'blockName' ] || + 'core/cover' === $block[ 'blockName' ] || + 'core/media-text' === $block[ 'blockName' ] + ) { + $doc = new DOMDocument(); + @$doc->loadHTML( $block['innerHTML'], LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD ); + + // Get the media urls from img tags + $tags = $doc->getElementsByTagName( 'img' ); + foreach ( $tags as $tag ) { + $image_url = $tag->getAttribute( 'src' ); + if ($this->is_absolute_url( $image_url )) { + $media[] = $tag->getAttribute( 'src' ); + } + } + // Get the media urls from video tags + $tags = $doc->getElementsByTagName( 'video' ); + foreach ( $tags as $tag ) { + $video_url = $tag->getAttribute( 'src' ); + if ($this->is_absolute_url( $video_url )) { + $media[] = $tag->getAttribute( 'src' ); + } + $poster_url = $tag->getAttribute( 'poster' ); + if ($this->is_absolute_url( $poster_url )) { + $media[] = $tag->getAttribute( 'poster' ); + } + } + // Get the media urls from div style tags (used in cover blocks) + $div_tags = $doc->getElementsByTagName( 'div' ); + foreach ( $div_tags as $tag ) { + $style = $tag->getAttribute( 'style' ); + if ( $style ) { + preg_match_all('#\bhttps?://[^,\s()<>]+(?:\([\w\d]+\)|([^,[:punct:]\s]|/))#', $style, $match); + $urls = $match[0]; + foreach ( $urls as $url ) { + if ( $this->is_absolute_url( $url ) ) { + $media[] = $url; + } + } + } + } + } + } + } + + return $media; + } + + // find all the media files used in the templates and add them to the zip + function make_template_images_local ( $template ) { + $new_content = $template->content; + $template_blocks = parse_blocks( $template->content ); + $flatten_blocks = _flatten_blocks( $template_blocks ); + + $blocks = $this->make_media_blocks_local( $template_blocks ); + $blocks = serialize_blocks ( $blocks ); + + $template->content = $this->clean_serialized_markup ( $blocks ); + $template->media = $this->get_media_absolute_urls_from_blocks ( $flatten_blocks ); + return $template; + } + + function clean_serialized_markup ( $markup ) { + $markup = str_replace( '%20', ' ', $markup ); + $markup = str_replace( '\u003c', '<', $markup ); + $markup = str_replace( '\u003e', '>', $markup ); + $markup = html_entity_decode( $markup, ENT_QUOTES | ENT_XML1, 'UTF-8' ); + return $markup; + } + + function pattern_from_template ( $template ) { + $theme_slug = wp_get_theme()->get( 'TextDomain' ); + $pattern_slug = $theme_slug . '/' . $template->slug; + $pattern_content = ( +'slug .' + * Slug: ' . $pattern_slug. ' + * Categories: hidden + * Inserter: no + */ +?> +'. $template->content + ); + return array ( + 'slug' => $pattern_slug, + 'content' => $pattern_content + ); + } + + function escape_text_for_pattern( $text ) { + if ( $text && trim ( $text ) !== "" ) { + return "get( "Name" ) ."' ); ?>"; + + } + } + + function escape_alt_for_pattern ( $html ) { + if ( empty ( $html ) ){ + return $html; + } + + // Use WP_HTML_Tag_Processor if available + // see: https://github.com/WordPress/gutenberg/pull/42485 + if ( class_exists( 'WP_HTML_Tag_Processor' ) ) { + $html = new WP_HTML_Tag_Processor( $html ); + while ( $html->next_tag( 'img' ) ) { + $alt_attribute = $html->get_attribute( 'alt' ); + if ( !empty ( $alt_attribute ) ) { + $html->set_attribute( 'alt', $this->escape_text_for_pattern( $alt_attribute ) ); + } + } + return $html->__toString(); + } + + // Fallback to regex + // TODO: When WP_HTML_Tag_Processor is availabe in core (6.2) we can remove this implementation entirely. + if ( ! class_exists( 'WP_HTML_Tag_Processor' ) ) { + preg_match( '@alt="([^"]+)"@' , $html, $match ); + if ( isset( $match[0] ) ) { + $alt_attribute = $match[0]; + $alt_value= $match[1]; + $html = str_replace( + $alt_attribute, + 'alt="'.$this->escape_text_for_pattern( $alt_value ).'"', + $html + ); + } + return $html; + } + } + + function get_media_folder_path_from_url ( $url ) { + $extension = strtolower( pathinfo( $url, PATHINFO_EXTENSION ) ); + $folder_path = ""; + $image_extensions = [ 'jpg', 'jpeg', 'png', 'gif', 'svg', 'webp' ]; + $video_extensions = [ 'mp4', 'm4v', 'webm', 'ogv', 'wmv', 'avi', 'mov', 'mpg', 'ogv', '3gp', '3g2' ]; + if ( in_array( $extension, $image_extensions ) ) { + $folder_path = "/assets/images/"; + } else if ( in_array( $extension, $video_extensions ) ) { + $folder_path = "/assets/videos/"; + } else { + $folder_path = "/assets/"; + } + return $folder_path; + } + + function get_file_extension_from_url ( $url ) { + $extension = pathinfo( $url, PATHINFO_EXTENSION ); + return $extension; + } + + function add_media_to_zip ( $zip, $media ) { + $media = array_unique( $media ); + foreach ( $media as $url ) { + $folder_path = $this->get_media_folder_path_from_url( $url ); + $download_file = file_get_contents( $url ); + $zip->addFromString( $folder_path . basename( $url ), $download_file ); + } + } + + function add_media_to_local ( $media ) { + foreach ( $media as $url ) { + $download_file = file_get_contents( $url ); + $media_path = get_stylesheet_directory() . DIRECTORY_SEPARATOR . $this->get_media_folder_path_from_url ( $url ); + if ( ! is_dir( $media_path ) ) { + wp_mkdir_p( $media_path ); + } + file_put_contents( + $media_path . basename( $url ), + $download_file + ); + } + } + /** * Add block templates and parts to the zip. * @@ -567,7 +989,6 @@ function get_theme_templates( $export_type, $new_slug ) { * all = all templates no matter what */ function add_templates_to_zip( $zip, $export_type, $new_slug ) { - $theme_templates = $this->get_theme_templates( $export_type, $new_slug ); if ( $theme_templates->templates ) { @@ -579,16 +1000,53 @@ function add_templates_to_zip( $zip, $export_type, $new_slug ) { } foreach ( $theme_templates->templates as $template ) { + $template_data = $this->make_template_images_local( $template ); + + // If there are images in the template, add it as a pattern + if ( count( $template_data->media ) > 0 ) { + $pattern = $this->pattern_from_template( $template_data ); + $template_data->content = ''; + + // Add pattern to zip + $zip->addFromString( + 'patterns/' . $template_data->slug . '.php', + $pattern[ 'content' ] + ); + + // Add media assets to zip + $this->add_media_to_zip( $zip, $template_data->media ); + } + + // Add template to zip $zip->addFromString( - 'templates/' . $template->slug . '.html', - $template->content + 'templates/' . $template_data->slug . '.html', + $template_data->content ); + } foreach ( $theme_templates->parts as $template_part ) { + $template_data = $this->make_template_images_local( $template_part ); + + // If there are images in the template, add it as a pattern + if ( count( $template_data->media ) > 0 ) { + $pattern = $this->pattern_from_template( $template_data ); + $template_data->content = ''; + + // Add pattern to zip + $zip->addFromString( + 'patterns/' . $template_data->slug . '.php', + $pattern[ 'content' ] + ); + + // Add media assets to zip + $this->add_media_to_zip( $zip, $template_data->media ); + } + + // Add template to zip $zip->addFromString( - 'parts/' . $template_part->slug . '.html', - $template_part->content + 'parts/' . $template_data->slug . '.html', + $template_data->content ); } @@ -601,27 +1059,76 @@ function add_templates_to_local( $export_type ) { $template_folders = get_block_theme_folders(); // If there is no templates folder, create it. - if ( ! is_dir( get_stylesheet_directory() . '/' . $template_folders['wp_template'] ) ) { - wp_mkdir_p( get_stylesheet_directory() . '/' . $template_folders['wp_template'] ); + if ( ! is_dir( get_stylesheet_directory() . DIRECTORY_SEPARATOR . $template_folders['wp_template'] ) ) { + wp_mkdir_p( get_stylesheet_directory() . DIRECTORY_SEPARATOR . $template_folders['wp_template'] ); } foreach ( $theme_templates->templates as $template ) { + $template_data = $this->make_template_images_local( $template ); + + // If there are images in the template, add it as a pattern + if ( ! empty ( $template_data->media ) ) { + // If there is no templates folder, create it. + if ( ! is_dir( get_stylesheet_directory() . DIRECTORY_SEPARATOR . 'patterns' ) ) { + wp_mkdir_p( get_stylesheet_directory() . DIRECTORY_SEPARATOR . 'patterns' ); + } + + // If there are external images, add it as a pattern + $pattern = $this->pattern_from_template( $template_data ); + $template_data->content = ''; + + // Write the pattern + file_put_contents( + get_stylesheet_directory() . DIRECTORY_SEPARATOR . 'patterns' . DIRECTORY_SEPARATOR . $template_data->slug . '.php', + $pattern[ 'content' ] + ); + } + + // Write the template content file_put_contents( - get_stylesheet_directory() . '/' . $template_folders['wp_template'] . '/' . $template->slug . '.html', - $template->content + get_stylesheet_directory() . DIRECTORY_SEPARATOR . $template_folders['wp_template'] . DIRECTORY_SEPARATOR . $template->slug . '.html', + $template_data->content ); + + // Write the media assets + $this->add_media_to_local( $template_data->media ); + } // If there is no parts folder, create it. - if ( ! is_dir( get_stylesheet_directory() . '/' . $template_folders['wp_template_part'] ) ) { - wp_mkdir_p( get_stylesheet_directory() . '/' . $template_folders['wp_template_part'] ); + if ( ! is_dir( get_stylesheet_directory() . DIRECTORY_SEPARATOR . $template_folders['wp_template_part'] ) ) { + wp_mkdir_p( get_stylesheet_directory() . DIRECTORY_SEPARATOR . $template_folders['wp_template_part'] ); } foreach ( $theme_templates->parts as $template_part ) { + $template_data = $this->make_template_images_local( $template_part ); + + // If there are images in the template, add it as a pattern + if ( ! empty ( $template_data->media ) ) { + // If there is no templates folder, create it. + if ( ! is_dir( get_stylesheet_directory() . DIRECTORY_SEPARATOR . 'patterns' ) ) { + wp_mkdir_p( get_stylesheet_directory() . DIRECTORY_SEPARATOR . 'patterns' ); + } + + // If there are external images, add it as a pattern + $pattern = $this->pattern_from_template( $template_data ); + $template_data->content = ''; + + // Write the pattern + file_put_contents( + get_stylesheet_directory() . DIRECTORY_SEPARATOR . 'patterns' . DIRECTORY_SEPARATOR . $template_data->slug . '.php', + $pattern[ 'content' ] + ); + } + + // Write the template content file_put_contents( - get_stylesheet_directory() . '/' . $template_folders['wp_template_part'] . '/' . $template_part->slug . '.html', - $template_part->content + get_stylesheet_directory() . DIRECTORY_SEPARATOR . $template_folders['wp_template_part'] . DIRECTORY_SEPARATOR . $template_data->slug . '.html', + $template_data->content ); + + // Write the media assets + $this->add_media_to_local( $template_data->media ); } }