429 lines
9.0 KiB
PHP
429 lines
9.0 KiB
PHP
<?php
|
|
|
|
namespace Gravity_Forms\Gravity_Forms\Libraries;
|
|
|
|
class Dom_Parser {
|
|
|
|
const NO_PARSE_QUERY_ARG = 'gf_disable_hooks_injection';
|
|
|
|
/**
|
|
* The string representation of the current DOM.
|
|
*
|
|
* @since 2.5.6
|
|
*
|
|
* @var string $content
|
|
*/
|
|
public $content;
|
|
|
|
/**
|
|
* A DOMDocument object made by calling loadXML().
|
|
*
|
|
* @since 2.5.6
|
|
*
|
|
* @var DOMDocument $dom_xml
|
|
*/
|
|
public $dom_xml;
|
|
|
|
/**
|
|
* A DOMDocument object made by calling loadHTML().
|
|
*
|
|
* @since 2.5.6
|
|
*
|
|
* @var DOMDocument $dom_html
|
|
*/
|
|
public $dom_html;
|
|
|
|
/**
|
|
* Whether the current server has DOMDocument active.
|
|
*
|
|
* @since 2.5.6
|
|
*
|
|
* @var bool $has_domdocument
|
|
*/
|
|
public $has_domdocument;
|
|
|
|
/**
|
|
* The position at which to insert the hooks script.
|
|
*
|
|
* @since 2.5.6
|
|
*
|
|
* @var int $insert_position
|
|
*/
|
|
public $insert_position = 0;
|
|
|
|
/**
|
|
* GF_Dom_Parser constructor.
|
|
*
|
|
* @param string $content
|
|
*/
|
|
public function __construct( $content ) {
|
|
$this->content = $content;
|
|
$this->has_domdocument = class_exists( 'DOMDocument' );
|
|
|
|
if ( ! $this->has_domdocument || empty( $this->content ) ) {
|
|
return;
|
|
}
|
|
|
|
$this->parse_dom();
|
|
}
|
|
|
|
/**
|
|
* Parse the DOM content into XML and HTML DOMDocuments.
|
|
*
|
|
* @since 2.5.6
|
|
*
|
|
* @return void
|
|
*/
|
|
private function parse_dom() {
|
|
libxml_use_internal_errors( true );
|
|
$this->dom_xml = $this->get_dom_xml();
|
|
$this->dom_html = $this->get_dom_html();
|
|
libxml_clear_errors();
|
|
}
|
|
|
|
/**
|
|
* Callback to fire when ob_flush() is called. Allows us to ensure that our Hooks JS has been output on the page,
|
|
* even in heavily-cached or concatenated environments.
|
|
*
|
|
* @since 2.5.6
|
|
*
|
|
* @return string
|
|
*/
|
|
public function get_injected_html() {
|
|
require_once \GFCommon::get_base_path() . '/form_display.php';
|
|
|
|
$has_printed = \GFFormDisplay::$hooks_js_printed;
|
|
|
|
/**
|
|
* Allow plugins to force the hook vars to output no matter what. Useful for certain edge-cases.
|
|
*
|
|
* @since 2.5.3
|
|
*
|
|
* @param bool $force_output Whether to force the script output.
|
|
*
|
|
* @return bool
|
|
*/
|
|
$force_output = apply_filters( 'gform_force_hooks_js_output', false );
|
|
|
|
if ( ! $force_output && ! $has_printed ) {
|
|
return $this->content;
|
|
}
|
|
|
|
if ( ! $this->should_inject_hooks_js() ) {
|
|
return $this->content;
|
|
}
|
|
|
|
return $this->inject_hooks_js();
|
|
}
|
|
|
|
/**
|
|
* Take the given DOM Content and inject the Hooks JS code in the correct position.
|
|
*
|
|
* @return string
|
|
*/
|
|
private function inject_hooks_js() {
|
|
$insert_position = $this->get_insert_position();
|
|
$hooks_javascript = \GFCommon::get_hooks_javascript_code();
|
|
|
|
$content = str_replace( $hooks_javascript, '', $this->content );
|
|
$string = \GFCommon::get_inline_script_tag( $hooks_javascript );
|
|
$pieces = preg_split( "/\r\n|\n|\r/", $content );
|
|
|
|
if ( count( $pieces ) > 1 && $insert_position > 0 ) {
|
|
array_splice( $pieces, $insert_position, 0, $string );
|
|
$content = implode( "\n", $pieces );
|
|
} else {
|
|
$content = preg_replace( '/(<[\s]*head(?!e)[^>]*>)/', '$0 ' . $string, $this->content, 1 );
|
|
}
|
|
|
|
return $content;
|
|
}
|
|
|
|
/**
|
|
* There are some contexts in which we do not want to inject our Hooks JS. This determines
|
|
* whether we are in one of those contexts.
|
|
*
|
|
* @since 2.5.6
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function should_inject_hooks_js() {
|
|
if ( ! $this->is_parseable_request() ) {
|
|
return false;
|
|
}
|
|
|
|
if ( $this->is_xml() ) {
|
|
return false;
|
|
}
|
|
|
|
if ( ! $this->is_full_html_doc() ) {
|
|
return false;
|
|
}
|
|
|
|
if ( $this->is_amp() ) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Attempt to parse the DOM Content into a DOMDocument XML model.
|
|
*
|
|
* @since 2.5.6
|
|
*
|
|
* @return DOMDocument|false
|
|
*/
|
|
private function get_dom_xml() {
|
|
if ( empty( $this->content ) ) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
$xdom = new \DOMDocument();
|
|
$xdom->loadXML( $this->content );
|
|
|
|
return $xdom;
|
|
} catch ( \Exception $e ) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Attempt to parse the DOM Content into a DOMDocument HTML model.
|
|
*
|
|
* @since 2.5.6
|
|
*
|
|
* @return DOMDocument|false
|
|
*/
|
|
private function get_dom_html() {
|
|
if ( empty( $this->content ) ) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
$dom = new \DOMDocument();
|
|
$dom->loadHTML( $this->content );
|
|
|
|
return $dom;
|
|
} catch ( \Exception $e ) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the correct line position at which the Hooks JS should be inserted. A location of 0 will result
|
|
* in the script being added right after the opening <head> tag, while anything greater will inject it
|
|
* at the defined line number.
|
|
*
|
|
* @since 2.5.6
|
|
*
|
|
* @return int
|
|
*/
|
|
private function get_insert_position() {
|
|
// Default to 0 to inject right after head.
|
|
$insert_position = 0;
|
|
$insert_el = false;
|
|
|
|
if ( ! $this->has_domdocument ) {
|
|
return 0;
|
|
}
|
|
|
|
if ( ! $this->dom_html ) {
|
|
return 0;
|
|
}
|
|
|
|
$meta_els = $this->dom_html->getElementsByTagName( 'meta' );
|
|
|
|
foreach ( $meta_els as $meta_el ) {
|
|
if (
|
|
// Some charsets are defined via a charset attribute
|
|
$meta_el->hasAttribute( 'charset' ) ||
|
|
|
|
// Other charsets are defined via a combo of http-equiv and content attritbutes
|
|
(
|
|
$meta_el->hasAttribute( 'http-equiv' ) &&
|
|
$meta_el->hasAttribute( 'content' )
|
|
)
|
|
) {
|
|
$insert_position = $meta_el->getLineNo();
|
|
$insert_el = $meta_el;
|
|
}
|
|
}
|
|
|
|
if ( $insert_position === 0 ) {
|
|
return $insert_position;
|
|
}
|
|
|
|
$pieces = preg_split( "/\r\n|\n|\r/", $this->content );
|
|
$previous = $pieces[ $insert_position - 1 ];
|
|
|
|
// Only use injection position if the detected line # actually falls after the meta tag.
|
|
preg_match( '/<\s*meta[^>]*>$/', $previous, $pos_matches );
|
|
|
|
if ( empty( $pos_matches ) ) {
|
|
return 0;
|
|
}
|
|
|
|
return $insert_position;
|
|
}
|
|
|
|
/**
|
|
* Determine if the current server request is one which requires us to add our hooks scripts.
|
|
*
|
|
* @since 2.5.6
|
|
* @since 2.5.13 - Added $check_empty param
|
|
*
|
|
* @param bool $check_empty Whether or not to validate that the DOM content isn't empty.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function is_parseable_request( $check_empty = true ) {
|
|
if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
|
|
return false;
|
|
}
|
|
|
|
if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
|
|
return false;
|
|
}
|
|
|
|
if ( ! empty( $_POST['gform_ajax'] ) ) {
|
|
return false;
|
|
}
|
|
|
|
$query_arg = rgget( self::NO_PARSE_QUERY_ARG );
|
|
|
|
if ( ! empty( $query_arg ) ) {
|
|
return false;
|
|
}
|
|
|
|
if ( $check_empty && empty( $this->content ) ) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Determine if the current document is an XML document.
|
|
*
|
|
* @since 2.5.6
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function is_xml() {
|
|
if ( ! $this->has_domdocument ) {
|
|
return false;
|
|
}
|
|
|
|
if ( ! $this->dom_xml ) {
|
|
return false;
|
|
}
|
|
|
|
if ( ! is_null( $this->dom_xml->documentElement ) && $this->dom_xml->documentElement->tagName !== 'html' ) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Determine if the current document has the required (<html>, <head>) elements, and thus
|
|
* should be treated as a full doc.
|
|
*
|
|
* @since 2.5.6
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function is_full_html_doc() {
|
|
if ( ! $this->has_domdocument ) {
|
|
return $this->has_head_regex();
|
|
}
|
|
|
|
if ( ! $this->dom_html ) {
|
|
return false;
|
|
}
|
|
|
|
$html = $this->dom_html->getElementsByTagName( 'html' );
|
|
$head = $this->dom_html->getElementsByTagName( 'head' );
|
|
|
|
// No HTML tag or head tag - we shouldn't mess with this so we bail.
|
|
if ( empty( $head->length ) || empty( $html->length ) ) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* If the current server doesn't have DOMDocument defined, use a regex method to find the
|
|
* <head> element. Less reliable than the DOMDocument method, but a decent fallback.
|
|
*
|
|
* @since 2.5.6
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function has_head_regex() {
|
|
preg_match( '/(<[\s]*head(?!e)[^>]*>)/', $content, $hmatches );
|
|
|
|
if ( empty( $hmatches ) ) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Determine whether the current document is an AMP document.
|
|
*
|
|
* @since 2.5.6
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function is_amp() {
|
|
if ( ! $this->has_domdocument || ! $this->dom_html ) {
|
|
return $this->is_amp_regex();
|
|
}
|
|
|
|
$html = $this->dom_html->getElementsByTagName( 'html' );
|
|
|
|
$html_el = $html[0];
|
|
|
|
// Markup is AMP using the amp attribute - bail.
|
|
if ( $html_el->hasAttribute( 'amp' ) ) {
|
|
return true;
|
|
}
|
|
|
|
// Pattern copied from the official AMP project Repository: https://github.com/ampproject/amp-toolbox-php
|
|
$pattern = '/<html\s([^>]*?(?:'
|
|
. "\xE2\x9A\xA1"
|
|
. '|'
|
|
. "\xE2\x9A\xA1\xEF\xB8\x8F"
|
|
. ')[^>]*?)>/i';
|
|
|
|
preg_match( $pattern, $this->content, $emoji_matches );
|
|
|
|
// Markup is AMP using the ⚡ symbol - bail.
|
|
if ( ! empty( $emoji_matches ) ) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* If the current server doesn't have DOMDocument defined, use a regex method to detect AMP.
|
|
*
|
|
* @since 2.5.6
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function is_amp_regex() {
|
|
// Bail if this markup is AMP'd
|
|
preg_match( '/^<!DOCTYPE html>[\r\n]*<[\s]*html[\s]+[^>]*amp[=\s>]+/i', trim( $content ), $amatches );
|
|
|
|
if ( ! empty( $amatches ) ) {
|
|
return true;
|
|
}
|
|
}
|
|
} |