options = wp_parse_args( $options, array( 'auto_br' => true, 'auto_indent' => true, 'allowed_html' => wpcf7_kses_allowed_html(), ) ); } /** * Separates the given text into chunks of HTML. Each chunk must be an * associative array that includes 'position', 'type', and 'content' keys. * * @param string $input Text to be separated into chunks. * @return iterable Iterable of chunks. */ public function separate_into_chunks( $input ) { $input_bytelength = strlen( $input ); $position = 0; while ( $position < $input_bytelength ) { $next_tag = preg_match( '/(?:|<(?:\/?)[a-z].*?>)/is', $input, $matches, PREG_OFFSET_CAPTURE, $position ); if ( ! $next_tag ) { yield array( 'position' => $position, 'type' => self::text, 'content' => substr( $input, $position ), ); break; } $next_tag = $matches[0][0]; $next_tag_position = $matches[0][1]; if ( $position < $next_tag_position ) { yield array( 'position' => $position, 'type' => self::text, 'content' => substr( $input, $position, $next_tag_position - $position ), ); } if ( ' $next_tag_position, 'type' => $next_tag_type, 'content' => substr( $input, $next_tag_position, strlen( $next_tag ) ), ); $position = $next_tag_position + strlen( $next_tag ); } } /** * Normalizes content in each chunk. This may change the type and position * of the chunk. * * @param iterable $chunks The original chunks. * @return iterable Normalized chunks. */ public function pre_format( $chunks ) { $position = 0; foreach ( $chunks as $chunk ) { $chunk['position'] = $position; // Standardize newline characters to "\n". $chunk['content'] = str_replace( array( "\r\n", "\r" ), "\n", $chunk['content'] ); if ( self::start_tag === $chunk['type'] ) { list( $chunk['content'] ) = self::normalize_start_tag( $chunk['content'] ); // Replace
by a line break. if ( $this->options['auto_br'] and preg_match( '/^$/i', $chunk['content'] ) ) { $chunk['type'] = self::text; $chunk['content'] = "\n"; } } yield $chunk; $position = self::calc_next_position( $chunk ); } } /** * Concatenates neighboring text chunks to create a single chunk. * * @param iterable $chunks The original chunks. * @return iterable Processed chunks. */ public function concatenate_texts( $chunks ) { $position = 0; $text_left = null; foreach ( $chunks as $chunk ) { $chunk['position'] = $position; if ( self::text === $chunk['type'] ) { if ( isset( $text_left ) ) { $text_left['content'] .= $chunk['content']; } else { $text_left = $chunk; } continue; } if ( isset( $text_left ) ) { yield $text_left; $chunk['position'] = self::calc_next_position( $text_left ); $text_left = null; } yield $chunk; $position = self::calc_next_position( $chunk ); } if ( isset( $text_left ) ) { yield $text_left; } } /** * Outputs formatted HTML based on the given chunks. * * @param iterable $chunks The original chunks. * @return string Formatted HTML. */ public function format( $chunks ) { $chunks = $this->pre_format( $chunks ); $chunks = $this->concatenate_texts( $chunks ); $this->output = ''; $this->stacked_elements = array(); foreach ( $chunks as $chunk ) { if ( self::text === $chunk['type'] ) { $this->append_text( $chunk['content'] ); } if ( self::start_tag === $chunk['type'] ) { $this->start_tag( $chunk['content'] ); } if ( self::end_tag === $chunk['type'] ) { $this->end_tag( $chunk['content'] ); } if ( self::comment === $chunk['type'] ) { $this->append_comment( $chunk['content'] ); } } return $this->output(); } /** * Appends preformatted text to the output property. */ public function append_preformatted( $content ) { $this->output .= $content; } /** * Appends whitespace to the output property. */ public function append_whitespace() { $this->append_preformatted( ' ' ); } /** * Appends a text node content to the output property. * * @param string $content Text node content. */ public function append_text( $content ) { if ( $this->is_inside( array( 'pre', 'template' ) ) ) { $this->append_preformatted( $content ); return; } if ( empty( $this->stacked_elements ) or $this->has_parent( 'p' ) or $this->has_parent( self::p_parent_elements ) ) { // Close

if the content starts with multiple line breaks. if ( preg_match( '/^\s*\n\s*\n\s*/', $content ) ) { $this->end_tag( 'p' ); } // Split up the contents into paragraphs, separated by double line breaks. $paragraphs = preg_split( '/\s*\n\s*\n\s*/', $content ); $paragraphs = array_filter( $paragraphs, static function ( $paragraph ) { return '' !== wpcf7_strip_whitespaces( $paragraph ); } ); $paragraphs = array_values( $paragraphs ); if ( $paragraphs ) { if ( $this->is_inside( 'p' ) ) { $paragraph = array_shift( $paragraphs ); $paragraph = self::normalize_paragraph( $paragraph, $this->options['auto_br'] ); $this->append_preformatted( $paragraph ); } foreach ( $paragraphs as $paragraph ) { $this->start_tag( 'p' ); $paragraph = wpcf7_strip_whitespaces( $paragraph, 'start' ); $paragraph = self::normalize_paragraph( $paragraph, $this->options['auto_br'] ); $this->append_preformatted( $paragraph ); } } // Close

if the content ends with multiple line breaks. if ( preg_match( '/\s*\n\s*\n\s*$/', $content ) ) { $this->end_tag( 'p' ); } // Cases where the content is a single line break. if ( preg_match( '/^\s*\n\s*$/', $content ) ) { $auto_br = $this->options['auto_br'] && $this->is_inside( 'p' ); $content = self::normalize_paragraph( $content, $auto_br ); $this->append_preformatted( $content ); } } else { $auto_br = $this->options['auto_br'] && $this->has_parent( self::br_parent_elements ); $content = self::normalize_paragraph( $content, $auto_br ); $this->append_preformatted( $content ); } } /** * Appends a start tag to the output property. * * @param string $tag A start tag. */ public function start_tag( $tag ) { list( $tag, $tag_name ) = self::normalize_start_tag( $tag ); if ( in_array( $tag_name, self::p_child_elements, true ) and ! $this->is_inside( 'p' ) and ! $this->is_inside( self::p_child_elements ) and ! $this->has_parent( self::p_nonparent_elements ) ) { // Open

if it does not exist. $this->start_tag( 'p' ); } $this->append_start_tag( $tag_name, array(), $tag ); } /** * Appends a start tag to the output property. * * @param string $tag_name Tag name. * @param array $atts Associative array of attribute name and value pairs. * @param string $tag A start tag. */ public function append_start_tag( $tag_name, $atts = array(), $tag = '' ) { if ( ! self::validate_tag_name( $tag_name ) ) { wp_trigger_error( __METHOD__, sprintf( /* translators: %s: Invalid HTML tag name */ __( 'Invalid tag name (%s) is specified.', 'contact-form-7' ), $tag_name ), E_USER_WARNING ); return false; } if ( WP_DEBUG and ! empty( $this->options['allowed_html'] ) ) { $html_disallowance = array(); if ( ! isset( $this->options['allowed_html'][$tag_name] ) ) { $html_disallowance = array( 'element' => $tag_name, ); } else { $atts_allowed = $this->options['allowed_html'][$tag_name]; $atts_disallowed = array_diff_ukey( $atts, $atts_allowed, static function ( $key_1, $key_2 ) use ( $atts_allowed ) { if ( str_starts_with( $key_1, 'data-' ) and ! empty( $atts_allowed['data-*'] ) and preg_match( '/^data-[a-z0-9_-]+$/', $key_1 ) ) { return 0; } else { return $key_1 === $key_2 ? 0 : 1; } } ); if ( ! empty( $atts_disallowed ) ) { $html_disallowance = array( 'element' => $tag_name, 'attributes' => array_keys( $atts_disallowed ), ); } } if ( $html_disallowance ) { $notice = sprintf( /* translators: %s: JSON-formatted array of disallowed HTML */ __( 'HTML Disallowance: %s', 'contact-form-7' ), wp_json_encode( $html_disallowance, JSON_PRETTY_PRINT ) ); wp_trigger_error( __METHOD__, $notice, E_USER_NOTICE ); } } if ( 'p' === $tag_name or in_array( $tag_name, self::p_parent_elements, true ) or in_array( $tag_name, self::p_nonparent_elements, true ) ) { // Close

if it exists. $this->end_tag( 'p' ); } if ( 'dd' === $tag_name or 'dt' === $tag_name ) { // Close

and
if closing tag is omitted. $this->end_tag( 'dd' ); $this->end_tag( 'dt' ); } if ( 'li' === $tag_name ) { // Close
  • if closing tag is omitted. $this->end_tag( 'li' ); } if ( 'optgroup' === $tag_name ) { // Close if closing tag is omitted. $this->end_tag( 'option' ); $this->end_tag( 'optgroup' ); } if ( 'option' === $tag_name ) { // Close