File: //home/globfdxw/diasporameetsafrica.com/wp-content/plugins/wpforms-pdf/src/PDF/Renderer.php
<?php
// Added to the PDF addon composer.json.
// phpcs:ignore Generic.Commenting.DocComment.MissingShort
/** @noinspection PhpComposerExtensionStubsInspection */
namespace WPFormsPDF\PDF;
use WPForms\SmartTags\SmartTag\SmartTag;
use WPForms\Emails\Notifications;
use WPForms\Helpers\File;
use WPFormsPDF\Storage;
use WPFormsPDF\Templates\Templates;
use DOMDocument;
use DOMElement;
/**
* Renderer class.
*
* @since 1.0.0
*/
class Renderer {
/**
* Default settings.
*
* @since 1.0.0
*
* @var array
*/
private const DEFAULT_SETTINGS = [
'template' => 'notification-modern',
'theme' => 'creamsicle',
'pdf_id' => 0,
'is_preview' => false,
];
/**
* 1x1 transparent PNG image.
*
* @since 1.0.0
*
* @var string
*/
private const TRANSPARENT_PNG = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==';
/**
* Supported image types.
*
* @since 1.0.0
*
* @var array
*/
private const IMAGE_TYPES = [
'svg' => 'image/svg+xml',
'png' => 'image/png',
'gif' => 'image/gif',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
];
/**
* Settings.
*
* @since 1.0.0
*
* @var array
*/
private $settings = [];
/**
* Templates class instance.
*
* @since 1.0.0
*
* @var Templates
*/
private $templates_obj;
/**
* Template data.
*
* @since 1.0.0
*
* @var array
*/
private $template;
/**
* Current entry fields data.
*
* @since 1.0.0
*
* @var array
*/
private $entry_fields;
/**
* Current entry ID.
*
* @since 1.0.0
*
* @var int
*/
private $entry_id;
/**
* Current form data.
*
* @since 1.0.0
*
* @var array
*/
private $form_data;
/**
* Current PDF images data.
*
* @since 1.0.0
*
* @var array
*/
private $images;
/**
* Class constructor.
*
* @since 1.0.0
*
* @param string $template Template slug.
* @param string $theme Theme slug.
* @param array $form_data Form data.
* @param array $entry_fields Entry fields.
*
* @return void
*/
public function __construct( string $template = '', string $theme = '', array $form_data = [], array $entry_fields = [] ) {
$this->templates_obj = wpforms_pdf()->templates;
$this->hooks();
if ( empty( $template ) || empty( $theme ) || empty( $form_data ) ) {
return;
}
$this->init( $template, $theme, $form_data, $entry_fields );
}
/**
* Initialize class.
*
* @since 1.0.0
*
* @param string $template Template slug.
* @param string $theme Theme slug.
* @param array $form_data Form data.
* @param array $entry_fields Entry fields.
*
* @return Renderer
*/
public function init( string $template, string $theme, array $form_data, array $entry_fields ): self {
$this->settings = [
'template' => $template,
'theme' => $theme,
];
$this->form_data = $form_data;
$this->entry_fields = $entry_fields;
$this->settings = wp_parse_args( $this->settings, self::DEFAULT_SETTINGS );
$this->init_template();
return $this;
}
/**
* Register hooks.
*
* @since 1.0.0
*/
private function hooks(): void {
add_filter( 'wpforms_smarttags_process_value', [ $this, 'filter_smarttags_process_value' ], 10, 7 );
}
/**
* Initialize certain PDF from the form settings.
*
* @since 1.0.0
*
* @param int $pdf_id PDF id.
* @param array $form_data Form data.
* @param array $entry_fields Entry fields.
* @param int $entry_id Entry id.
* @param bool $is_preview Is preview.
*
* @return Renderer
*/
public function init_pdf_settings( int $pdf_id, array $form_data, array $entry_fields, int $entry_id = 0, bool $is_preview = false ): self {
$pdf = $form_data['settings']['pdfs'][ $pdf_id ] ?? [];
$theme_slug = $pdf['theme'] ?? '';
$template_slug = $pdf['template_style'] ?? '';
$this->settings = [
'template' => $template_slug,
'theme' => $theme_slug,
'pdf_id' => $pdf_id,
'is_preview' => $is_preview,
'orientation' => $pdf['orientation'] ?? 'portrait',
'paper_size' => $pdf['paper_size'] ?? 'A4',
];
$this->form_data = $form_data;
$this->entry_fields = $entry_fields;
$this->entry_id = $entry_id;
$this->settings = wp_parse_args( $this->settings, self::DEFAULT_SETTINGS );
$this->images = [];
$this->init_template();
return $this;
}
/**
* Initialize template.
*
* @since 1.0.0
*
* @return void
*/
private function init_template(): void {
// Get template data.
$this->template = $this->templates_obj->get_template( $this->settings['template'], $this->settings['theme'] );
// Default template.
if ( empty( $this->template ) ) {
$this->template = $this->templates_obj->get_template( Templates::DEFAULT_TEMPLATE, $this->settings['theme'] );
}
$this->apply_pdf_settings();
// Update template data.
$this->init_template_appearance();
$this->init_template_text();
}
/**
* Initialize a template appearance section.
*
* @since 1.0.0
*
* @return void
*/
private function init_template_appearance(): void {
$this->template['appearance']['page_background_image'] = $this->get_background_image();
// Available fonts.
$fonts = [
'serif' => '"Literata", serif',
'sans-serif' => '"Inter", sans-serif',
];
// Update font to actual CSS value.
$template_font = $this->template['appearance']['font'] ?? 'sans-serif';
$this->template['appearance']['font'] = $fonts[ $template_font ] ?? $fonts['sans-serif'];
$this->template['appearance']['logo_url'] = $this->get_image( $this->template['appearance']['logo_url'] ?? '' );
}
/**
* Initialize a template text section.
*
* @since 1.0.0
*
* @return void
*/
private function init_template_text(): void {
$this->template['text'] = $this->template['text'] ?? [];
if ( isset( $this->template['text']['badge_show'] ) ) {
$this->template['text']['badge_image'] = $this->get_badge_image();
}
if ( isset( $this->template['text']['signature_url'] ) ) {
$this->template['text']['signature_url'] = $this->get_image( $this->template['text']['signature_url'] );
}
}
/**
* Apply PDF settings to the template.
*
* @since 1.0.0
*
* @return void
*/
private function apply_pdf_settings(): void {
$pdf_settings = $this->get_pdf_settings();
if ( empty( $pdf_settings ) || empty( $this->template ) ) {
return;
}
$template = $this->template;
$prefix = $template['category'] === 'notification' ? 'notification_' : 'general_';
// Apply appearance settings.
foreach ( $template['appearance'] as $key => $value ) {
$appearance_key = $prefix . $key;
$appearance_key = isset( $pdf_settings[ $appearance_key ] ) ? $appearance_key : $key;
$template['appearance'][ $key ] = $pdf_settings[ $appearance_key ] ?? $template['appearance'][ $key ];
}
// Apply text settings.
$template['text'] = $this->apply_template_settings( $template['text'], $pdf_settings );
$this->template = $template;
}
/**
* Get PDF settings.
*
* @since 1.0.0
*
* @return array
*/
private function get_pdf_settings(): array {
if ( ! isset( $this->settings['pdf_id'] ) ) {
return [];
}
return $this->form_data['settings']['pdfs'][ $this->settings['pdf_id'] ?? null ] ?? [];
}
/**
* Apply template settings.
*
* @since 1.0.0
*
* @param array $template_section Template section.
* @param array $pdf_settings PDF settings.
*
* @return array
*/
private function apply_template_settings( array $template_section, array $pdf_settings ): array {
// Apply template section settings.
foreach ( $template_section as $key => $value ) {
$template_section[ $key ] = $pdf_settings[ $key ] ?? $value;
}
return $template_section;
}
/**
* Get rendered HTML.
*
* @since 1.0.0
*
* @param array $texts Texts.
*
* @return string
*/
public function render_html( array $texts = [] ): string {
// Process content.
$processed_texts = $this->process_texts( $texts );
// Render template.
$body = wpforms_render(
$this->get_template_location(),
[
'appearance' => $this->get_appearance_settings(),
'theme' => $this->get_theme_colors(),
'content' => $processed_texts['content'],
'texts' => $processed_texts,
'is_preview' => $this->settings['is_preview'],
'base_url' => $this->get_pdf_base_url(),
],
true
);
$pdf_tuning_css = empty( $this->settings['is_preview'] ) ? $this->get_css_file_content( 'pdf_tuning' ) : '';
$is_preview_class = $this->settings['is_preview'] ? 'preview' : '';
// Render final HTML.
$html = wpforms_render(
$this->get_template_location( 'html' ),
[
'html_css' => $this->get_css_file_content(),
'pdf_tuning_css' => $pdf_tuning_css,
'body' => $body,
'body_class' => $this->settings['orientation'] . ' ' . $this->settings['paper_size'] . ' ' . $is_preview_class,
],
true
);
// Process rendered HTML.
if ( empty( $this->settings['is_preview'] ) ) {
$html = $this->process_rendered_html( $html );
$this->update_images_data( $html );
}
// Output the final HTML to the file for debugging.
if ( wpforms_pdf()->helpers->is_debug() ) {
File::put_contents( Storage::get_dir() . '/debug.html', $html );
}
return $html;
}
/**
* Process rendered HTML.
*
* @since 1.0.0
*
* @param string $html HTML content.
*
* @return string
*/
private function process_rendered_html( string $html ): string {
$emojis = [
'star' => '⭐',
'heart' => '❤️',
'thumb' => '👍',
'smiley' => '🙂',
];
$emojis_replacement = [];
foreach ( $emojis as $key => $value ) {
$emojis_replacement[] = sprintf(
'<span><img src="%1$sassets/images/emoji/%2$s.png" alt="" /></span>',
WPFORMS_PDF_URL,
$key
);
}
$html = str_replace( array_values( $emojis ), $emojis_replacement, $html );
return $html;
}
/**
* Get appearance settings for the template.
*
* @since 1.0.0
*
* @return array
*/
private function get_appearance_settings(): array {
$appearance = $this->template['appearance'];
if ( ! $this->settings['is_preview'] ) {
return $appearance;
}
$as_is_keys = [ 'logo_id', 'logo_url', 'logo_position', 'logo_size', 'page_background_image', 'container_shadow' ];
foreach ( $appearance as $key => $value ) {
if ( in_array( $key, $as_is_keys, true ) ) {
continue;
}
$appearance[ $key ] = "var( --wpforms-appearance-$key, $value )";
}
return $appearance;
}
/**
* Get theme colors for the template.
*
* @since 1.0.0
*
* @return array
*/
private function get_theme_colors(): array {
$colors = $this->template['theme']['colors'];
if ( ! $this->settings['is_preview'] ) {
return $colors;
}
$colors = $this->get_unsaved_theme_colors();
foreach ( $colors as $key => $value ) {
$colors[ $key ] = "var( --wpforms-theme-color-$key, $value )";
}
return $colors;
}
/**
* Get theme colors for the template.
*
* @since 1.0.0
*
* @return array
*/
private function get_unsaved_theme_colors(): array {
$colors = $this->template['theme']['colors'];
$pdf = $this->get_pdf_settings();
foreach ( $colors as $key => $value ) {
$colors[ $key ] = $pdf[ 'theme_color_' . $key ] ?? $value;
}
return $colors;
}
/**
* Process texts.
*
* @since 1.0.0
*
* @param array $texts Texts.
*
* @return array
*/
private function process_texts( array $texts ): array {
// Get template texts.
$template_text = $this->template['text'];
$template_text = wp_parse_args( $texts, $template_text );
$all_fields = $template_text['all_fields'] ?? $this->render_all_fields();
$context = $this->settings['is_preview'] ? 'wpforms-pdf-preview-process-texts' : 'wpforms-pdf-process-texts';
// Process texts.
foreach ( $template_text as $key => $value ) {
if ( $this->settings['is_preview'] && strpos( $key, '_color' ) !== false ) {
$template_text[ $key ] = "var( --wpforms-text-color-$key, $value )";
continue;
}
$template_text[ $key ] = wpforms_process_smart_tags( wp_unslash( $value ), $this->form_data, $this->entry_fields, $this->entry_id, $context );
$template_text[ $key ] = str_replace( '{all_fields}', $all_fields, $template_text[ $key ] );
}
$template_text['content'] = $template_text['content'] ?? $all_fields;
return $template_text;
}
/**
* Get rendered {all_fields} for a given email template.
*
* @since 1.0.0
*
* @param string $template Template slug.
*
* @return string
*/
public function render_all_fields( string $template = '' ): string {
$template = empty( $template ) ? $this->template['email_style'] : $template;
// Create a new email.
$emails = ( new Notifications() )->init( $template, 'pdf' );
$emails->__set( 'form_data', $this->form_data );
$emails->__set( 'fields', $this->entry_fields );
// Render all fields.
$all_fields = $emails->get_processed_field_values();
return '<table class="all_fields">' . $all_fields . '</table>';
}
/**
* Get a base template slug.
*
* @since 1.0.0
*
* @param string $template Template slug. Optional.
*
* @return string
*/
private function get_base_template_slug( string $template = '' ): string {
$template = empty( $template ) ? $this->settings['template'] : $template;
return ! empty( $this->template['baseTemplate'] ) ? $this->template['baseTemplate'] : $template;
}
/**
* Get a template location.
*
* @since 1.0.0
*
* @param string $template Template slug. Optional.
*
* @return string
*/
private function get_template_location( string $template = '' ): string {
$template = $template !== 'html' ? $this->get_base_template_slug( $template ) : $template;
return sprintf(
'%1$stemplates/pdf/%2$s',
WPFORMS_PDF_PATH,
$template
);
}
/**
* Get a template CSS file path.
*
* @since 1.0.0
*
* @param string $purpose CSS purpose `html` or `pdf_tuning`. Defaults to `html`.
*
* @return string
* @noinspection PhpSameParameterValueInspection
*/
private function get_css_file( string $purpose = '' ): string {
$template = $this->get_base_template_slug();
$sub_dir = $purpose === 'pdf_tuning' ? '/tuning' : '';
return sprintf(
'%1$sassets/css/pdf%2$s/%3$s.css',
WPFORMS_PDF_PATH,
$sub_dir,
$template
);
}
/**
* Get template CSS file content.
*
* @since 1.0.0
*
* @param string $purpose CSS purpose `html` or `pdf_tuning`. Defaults to `html`.
*
* @return string
* @noinspection PhpSameParameterValueInspection
*/
private function get_css_file_content( string $purpose = 'html' ): string {
$css_content = File::get_contents( $this->get_css_file( $purpose ) );
if ( empty( $css_content ) ) {
return '';
}
return str_replace( '{WPFORMS_PDF_URL}', $this->get_pdf_base_url(), $css_content );
}
/**
* Get PDF base URL.
*
* @since 1.0.0
*
* @return string
*/
private function get_pdf_base_url(): string {
return empty( $this->settings['is_preview'] ) ? '' : WPFORMS_PDF_URL;
}
/**
* Get image content.
*
* @since 1.0.0
*
* @param string $location Image location.
* It could be a URL or a path to an image located in the `assets/images` folder.
*
* @return string
*/
private function get_image( string $location ): string {
if ( empty( $location ) ) {
return '';
}
[ $location_norm, $site_url ] = $this->normalize_urls( [ $location, site_url() ] );
$upload_dir = wpforms_pdf()->helpers->get_wp_upload_dir();
// If the image doesn't start with the full local path to addon
// or the full URL to the default logos, assume this is the image in the `assets /images/` directory.
if (
strpos( $location, WPFORMS_PDF_PATH ) === false &&
strpos( $location_norm, $site_url ) === false
) {
$location = WPFORMS_PDF_PATH . 'assets/images/' . $location;
}
// Convert the image URL to the full local path.
$location = str_replace(
[ $upload_dir['url'], WPFORMS_PDF_URL ],
[ $upload_dir['dir'], WPFORMS_PDF_PATH ],
$location
);
return $this->get_image_data( $location );
}
/**
* Get data:image string from given image location.
*
* @since 1.0.0
*
* @param string $location Image location.
*
* @return string
*/
private function get_image_data( string $location ): string {
$image_ext = pathinfo( $location, PATHINFO_EXTENSION );
$image_content = File::get_contents( $location );
if ( empty( $image_content ) ) {
return '';
}
// Skip unsupported image types.
if ( ! isset( self::IMAGE_TYPES[ $image_ext ] ) ) {
return '';
}
// Add colors to SVG image.
if ( $image_ext === 'svg' ) {
$image_content = $this->templates_obj->replace_colors( $image_content, $this->get_colors_for_image( $location ) );
}
$type = self::IMAGE_TYPES[ $image_ext ];
if ( ! empty( $this->settings['is_preview'] ) ) {
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
return "data:$type;base64," . base64_encode( $image_content );
}
// Add an image to the `images` array in case it is an actual PDF generation process.
$upload_dir = wpforms_pdf()->helpers->get_wp_upload_dir();
$short_location = str_replace( [ WPFORMS_PDF_PATH, $upload_dir['dir'] ], '', $location );
$this->images[ $short_location ] = $image_ext !== 'svg' ? base64_encode( $image_content ) : $image_content; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
// Return a short path to the image.
return $short_location;
}
/**
* Get colors for the image.
*
* @since 1.0.0
*
* @param string $location Image location.
*
* @return array
*/
private function get_colors_for_image( string $location ): array {
$colors = $this->template['theme']['colors'];
if ( $this->settings['is_preview'] ) {
$colors = $this->get_unsaved_theme_colors();
}
if ( strpos( $location, 'background' ) === false ) {
return $colors;
}
// Adjust theme colors according to the custom colors defined in the template.
$colors['background'] = $this->templates_obj->replace_colors( $this->template['appearance']['page_background_color'], $colors );
$colors['background_light'] = $this->templates_obj->replace_colors( $this->template['appearance']['page_background_color_end'], $colors );
return $colors;
}
/**
* Get normalized URLs.
* For cases when the image was uploaded before enabling https on the server.
*
* @since 1.0.0
*
* @param array $urls URLs.
*/
private function normalize_urls( array $urls ): array {
foreach ( $urls as $key => $url ) {
$urls[ $key ] = str_replace( 'http://', 'https://', $url );
}
return $urls;
}
/**
* Get badge image content.
*
* @since 1.0.0
*
* @return string
*/
private function get_badge_image(): string {
$style = $this->template['style'] ?? '';
return $this->get_image( "badges/$style.svg" );
}
/**
* Get background image content.
*
* @since 1.0.0
*
* @return string
*/
private function get_background_image(): string {
$location = $this->get_background_image_location();
// If there is no background image, return transparent PNG.
// This is required for PDF export.
if ( empty( $location ) ) {
return self::TRANSPARENT_PNG;
}
return $this->get_image( $location );
}
/**
* Get a background image file location.
*
* @since 1.0.0
*
* @param string $image Image file name, without `.svg`. Optional.
*
* @return string
* @noinspection PhpSameParameterValueInspection
*/
private function get_background_image_location( string $image = '' ): string {
$image = empty( $image ) ? $this->template['appearance']['page_background_image'] : $image;
$orientation = $this->settings['orientation'] ?? '';
$orientation = $orientation === 'landscape' ? 'landscape' : 'portrait';
$file = wp_normalize_path( WPFORMS_PDF_PATH . "assets/images/background/$orientation/$image" );
$svg_file = $file . '.svg';
if ( File::exists( $svg_file ) ) {
return $svg_file;
}
$png_file = $file . '.png';
if ( ! File::exists( $png_file ) ) {
return '';
}
// Return URL to the PNG file.
return esc_url( str_replace( WPFORMS_PDF_PATH, WPFORMS_PDF_URL, $png_file ) );
}
/**
* Get images data.
*
* @since 1.0.0
*
* @return array
*/
public function get_images_data(): array {
return $this->images;
}
/**
* Update images data by finding all img tags in HTML.
*
* @since 1.0.0
*
* @param string $html HTML content.
*
* @return void
*/
private function update_images_data( string $html ): void {
// Skip if HTML is empty.
if ( empty( $html ) ) {
return;
}
// Create a new DOMDocument.
$dom = new DOMDocument();
// Suppress warnings from malformed HTML.
libxml_use_internal_errors( true );
// Load HTML content.
$dom->loadHTML( $html );
// Reset errors.
libxml_clear_errors();
// Get all image tags.
$images = $dom->getElementsByTagName( 'img' );
// Skip if no images found.
if ( $images->length === 0 ) {
return;
}
$upload_dir = wpforms_pdf()->helpers->get_wp_upload_dir();
$site_url = site_url();
// Process each image.
foreach ( $images as $image ) {
$this->update_images_data_process_image( $image, $upload_dir, $site_url );
}
}
/**
* Process a single image from the DOM.
*
* @since 1.0.0
*
* @param DOMElement $image Image DOM element.
* @param array $upload_dir WordPress upload directory information.
* @param string $site_url Site URL.
*
* @return void
*/
private function update_images_data_process_image( DOMElement $image, array $upload_dir, string $site_url ): void {
// Get image source.
$src = $image->getAttribute( 'src' );
// Skip if the source is empty or already processed.
if ( empty( $src ) || isset( $this->images[ $src ] ) || strpos( $src, 'data:' ) ) {
return;
}
// Convert URL to local path.
$local_path = str_replace( $upload_dir['url'], $upload_dir['dir'], $src );
// Also try with site URL.
if ( ! file_exists( $local_path ) ) {
$local_path = str_replace( $site_url, ABSPATH, $src );
}
$local_path = wp_normalize_path( $local_path );
// Skip if file doesn't exist.
if ( ! File::exists( $local_path ) ) {
return;
}
// Get file extension.
$image_ext = pathinfo( $local_path, PATHINFO_EXTENSION );
// Skip unsupported image types.
if ( ! isset( self::IMAGE_TYPES[ $image_ext ] ) ) {
return;
}
// Get image content.
$image_content = File::get_contents( $local_path );
// Skip if content is empty.
if ( empty( $image_content ) ) {
return;
}
// Add image to the images array.
$this->images[ $src ] = $image_ext !== 'svg' ? base64_encode( $image_content ) : $image_content; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
}
/**
* Filter the smart tag value.
*
* @since 1.0.0
*
* @param string|mixed $value Smart Tag value.
* @param string $tag_name Smart tag name.
* @param array $form_data Form data.
* @param array $fields List of fields.
* @param string $entry_id Entry ID.
* @param SmartTag $smart_tag_object The smart tag object or the Generic object.
* @param string $context Context.
*
* @return string|null
* @noinspection PhpUnusedParameterInspection
*/
public static function filter_smarttags_process_value( $value, string $tag_name, array $form_data, array $fields, string $entry_id, SmartTag $smart_tag_object, string $context ): ?string {
// Don't replace `field_id` smart tag in the PDF preview texts.
if ( $context === 'wpforms-pdf-preview-process-texts' && $tag_name === 'field_id' ) {
return null;
}
return $value;
}
}