[ type, slug ] learned from this page's local assets. * * @var array */ private $owners = array(); /** * Active plugin slugs. * * @var string[] */ private $plugins = array(); /** * Theme slugs (child first). * * @var string[] */ private $themes = array(); /** * Byte offset of . * * @var int */ private $head_end; /** * Page HTML. * * @var string */ private $html; /** * Page URL. * * @var string */ private $url; /** * Code-snippet plugins: slug => name. * * @return array */ private static function snippet_plugins() { return array( 'insert-headers-and-footers' => 'WPCode', 'wpcode-premium' => 'WPCode', 'header-footer-code-manager' => 'Header Footer Code Manager', 'code-snippets' => 'Code Snippets', 'code-snippets-pro' => 'Code Snippets', 'header-and-footer-scripts' => 'Header and Footer Scripts', 'insert-headers-and-footers-' => 'Insert Headers and Footers', 'wp-headers-and-footers' => 'Headers & Footers', 'head-footer-code' => 'Head & Footer Code', 'google-site-kit' => 'Site Kit by Google', ); } /** * Well-known handles that don't share their plugin's prefix. * * @return array handle prefix => plugin slug. */ private static function aliases() { return array( 'google_gtagjs' => 'google-site-kit', 'googlesitekit' => 'google-site-kit', 'gtm4wp' => 'duracelltomi-google-tag-manager', 'monsterinsights' => 'google-analytics-for-wordpress', 'exactmetrics' => 'google-analytics-dashboard-for-wp', 'wc' => 'woocommerce', 'woocommerce' => 'woocommerce', 'fl-builder' => 'bb-plugin', 'uabb' => 'bb-ultimate-addon', 'elementor' => 'elementor', 'elementor-pro' => 'elementor-pro', 'jetpack' => 'jetpack', 'facebook-for-woo' => 'facebook-for-woocommerce', 'pys' => 'pixelyoursite', 'hq-rental' => 'hq-rental-software', ); } /** * Prepare for one page. * * @param string $html Page HTML. * @param string $url Page URL. */ public function __construct( $html, $url ) { $this->html = $html; $this->url = $url; $end = stripos( $html, '' ); $this->head_end = false === $end ? 0 : $end; foreach ( (array) get_option( 'active_plugins', array() ) as $file ) { $this->plugins[] = strtolower( dirname( (string) $file ) ); } $this->themes = array_values( array_unique( array( get_stylesheet(), get_template() ) ) ); $this->learn(); } /** * Learn handle prefixes from local plugin/theme assets on this page. */ private function learn() { if ( ! preg_match_all( '#<(?:script|link)\b[^>]*\bid\s*=\s*["\']([a-z0-9_.-]+?)-(?:js|css)["\'][^>]*>#i', $this->html, $m, PREG_SET_ORDER ) ) { return; } foreach ( $m as $tag ) { if ( ! preg_match( '#/wp-content/(plugins|mu-plugins|themes)/([a-z0-9_.-]+)/#i', $tag[0], $p ) ) { continue; } $owner = array( 'themes' === strtolower( $p[1] ) ? 'theme' : 'plugin', strtolower( $p[2] ) ); foreach ( self::prefixes( strtolower( $tag[1] ) ) as $prefix ) { if ( ! isset( $this->owners[ $prefix ] ) ) { $this->owners[ $prefix ] = $owner; } } } } /** * Full handle, then its first two tokens, then its first token. * * @param string $handle Handle. * @return string[] */ private static function prefixes( $handle ) { $parts = preg_split( '/[-_.]/', $handle ); $out = array( $handle ); if ( count( $parts ) > 2 ) { $out[] = $parts[0] . '-' . $parts[1]; } if ( strlen( $parts[0] ) >= 2 ) { $out[] = $parts[0]; } return array_values( array_unique( $out ) ); } /** * Source of one script tag. * * @param string $attrs Tag attributes. * @param int $offset Byte offset of the tag in the page. * @return array{type:string,slug:string,name:string,detail:string,post:int} */ public function detect( $attrs, $offset ) { $out = array( 'type' => 'unknown', 'slug' => '', 'name' => '', 'detail' => '', 'post' => 0, ); // Enqueued: id="{handle}-js" (or -js-extra / -js-before / -js-after for its inline parts). if ( preg_match( '#\bid\s*=\s*["\']([a-z0-9_.-]+?)-js(?:-extra|-before|-after)?["\']#i', $attrs, $hm ) ) { $handle = strtolower( $hm[1] ); $out['detail'] = $handle; $owner = $this->owner_of( $handle ); if ( $owner ) { $out['type'] = $owner[0]; $out['slug'] = $owner[1]; $out['name'] = self::name_of( $owner[0], $owner[1] ); return $out; } $out['type'] = 'handle'; return $out; } // No handle: placed in the page content or by a header/footer code tool. $before = substr( $this->html, max( 0, $offset - 8000 ), min( 8000, $offset ) ); $module = $offset > $this->head_end ? self::builder_module( $before ) : ''; if ( $module ) { $out['type'] = 'content'; $out['name'] = $module; $out['detail'] = self::section_name( $before ); $out['post'] = (int) url_to_postid( $this->url ); if ( ! $out['post'] && untrailingslashit( (string) wp_parse_url( $this->url, PHP_URL_PATH ) ) === untrailingslashit( (string) wp_parse_url( home_url( '/' ), PHP_URL_PATH ) ) ) { $out['post'] = (int) get_option( 'page_on_front' ); } return $out; } foreach ( self::snippet_plugins() as $slug => $name ) { if ( in_array( $slug, $this->plugins, true ) ) { $out['type'] = 'snippet'; $out['slug'] = $slug; $out['name'] = $name; $out['detail'] = $offset < $this->head_end ? 'head' : 'footer'; return $out; } } $out['detail'] = $offset < $this->head_end ? 'head' : 'body'; return $out; } /** * Owner of a handle. * * @param string $handle Handle. * @return array|null [ type, slug ]. */ private function owner_of( $handle ) { foreach ( self::prefixes( $handle ) as $prefix ) { if ( isset( $this->owners[ $prefix ] ) ) { return $this->owners[ $prefix ]; } } foreach ( self::aliases() as $prefix => $slug ) { if ( ( $handle === $prefix || 0 === strpos( $handle, $prefix . '-' ) || 0 === strpos( $handle, $prefix . '_' ) ) && in_array( $slug, $this->plugins, true ) ) { return array( 'plugin', $slug ); } } foreach ( $this->themes as $slug ) { if ( 0 === strpos( $handle, strtolower( $slug ) ) ) { return array( 'theme', $slug ); } } foreach ( $this->plugins as $slug ) { $flat = str_replace( '-', '', $slug ); if ( strlen( $slug ) >= 3 && ( 0 === strpos( $handle, $slug ) || 0 === strpos( str_replace( array( '-', '_' ), '', $handle ), $flat ) ) ) { return array( 'plugin', $slug ); } } return null; } /** * Page-builder module wrapping a script, from the markup just before it. * * @param string $before Preceding HTML. * @return string Module name or ''. */ private static function builder_module( $before ) { $tail = substr( $before, -3000 ); $map = array( 'fl-module-html' => __( 'Beaver Builder HTML module', 'goconsent' ), 'fl-html' => __( 'Beaver Builder HTML module', 'goconsent' ), 'elementor-widget-html' => __( 'Elementor HTML widget', 'goconsent' ), 'elementor-shortcode' => __( 'Elementor shortcode widget', 'goconsent' ), 'et_pb_code' => __( 'Divi Code module', 'goconsent' ), 'wp-block-html' => __( 'Custom HTML block', 'goconsent' ), 'widget_custom_html' => __( 'Custom HTML widget', 'goconsent' ), 'vc_raw_html' => __( 'WPBakery Raw HTML element', 'goconsent' ), 'brxe-code' => __( 'Bricks Code element', 'goconsent' ), 'oxy-code-block' => __( 'Oxygen Code Block', 'goconsent' ), ); $best = ''; $pos = -1; foreach ( $map as $needle => $name ) { $p = strripos( $tail, $needle ); if ( false !== $p && $p > $pos ) { $pos = $p; $best = $name; } } return $best; } /** * Nearest human-named section id before the script ("BUSINESS REPORT FORM"). * * @param string $before Preceding HTML. * @return string */ private static function section_name( $before ) { if ( ! preg_match_all( '#\bid\s*=\s*["\']([^"\']{2,80})["\']#i', $before, $m ) ) { return ''; } foreach ( array_reverse( $m[1] ) as $id ) { if ( preg_match( '#^(fl-|elementor-|et_pb|wp-|vc_|brxe-|oxy-|post-|menu-|page-)|^[a-f0-9]{6,}$|-js$|-css$#i', $id ) ) { continue; } return $id; } return ''; } /** * Display name of a plugin or theme. * * @param string $type plugin|theme. * @param string $slug Slug. * @return string */ public static function name_of( $type, $slug ) { if ( 'theme' === $type ) { $t = wp_get_theme( $slug ); return $t->exists() ? (string) $t->get( 'Name' ) : $slug; } if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } foreach ( get_plugins() as $file => $data ) { if ( strtolower( dirname( $file ) ) === $slug && ! empty( $data['Name'] ) ) { return (string) $data['Name']; } } return $slug; } }