settings = $settings; } /** * Category for markup or a URL after admin choices. * * @param string $haystack Tag attributes + body, or a URL. * @param string|null $tag_attrs Only the tag's attributes (src is read from here). * @return string necessary|analytics|marketing|'' (unknown, first-party or harmless). */ public function category( $haystack, $tag_attrs = null ) { $overrides = (array) $this->settings->get( 'overrides' ); foreach ( $overrides as $id => $cat ) { if ( 0 === strpos( $id, 'host:' ) && false !== stripos( $haystack, substr( $id, 5 ) ) ) { return $cat; } } foreach ( $this->sorted() as $id => $sig ) { foreach ( $sig[2] as $needle ) { if ( false !== stripos( $haystack, $needle ) ) { return isset( $overrides[ $id ] ) ? $overrides[ $id ] : $sig[1]; } } } // Unknown third-party script: sort by what its URL looks like. $src = self::attr( null === $tag_attrs ? $haystack : $tag_attrs, 'src' ); if ( $src && $this->is_third_party( $src ) ) { if ( 'necessary' === self::guess( $src ) ) { return 'necessary'; } // During the 24-hour watch period unknown scripts are listed, not held. return $this->settings->enforcing() ? 'marketing' : ''; } return ''; } /** * Sort an unknown URL: tracking-looking or unclear => marketing, essential-looking => necessary. * * @param string $url URL. * @return string */ public static function guess( $url ) { $u = strtolower( $url ); foreach ( Signatures::tracking_words() as $w ) { if ( false !== strpos( $u, $w ) ) { return 'marketing'; } } foreach ( Signatures::functional_words() as $w ) { if ( false !== strpos( $u, $w ) ) { return 'necessary'; } } return 'marketing'; } /** * Whether a URL points to another site (and not a library CDN). * * @param string $url URL. * @return bool */ public function is_third_party( $url ) { $url = html_entity_decode( $url, ENT_QUOTES | ENT_HTML5 ); $host = strtolower( (string) wp_parse_url( ( 0 === strpos( $url, '//' ) ? 'https:' : '' ) . $url, PHP_URL_HOST ) ); return $host && ! self::same_site( $host ) && ! in_array( $host, Signatures::benign_hosts(), true ); } /** * Whether a host belongs to this site. * * @param string $host Host. * @return bool */ public static function same_site( $host ) { $host = preg_replace( '/^www\./', '', strtolower( $host ) ); $site = preg_replace( '/^www\./', '', strtolower( (string) wp_parse_url( home_url(), PHP_URL_HOST ) ) ); return $host === $site || substr( $host, -strlen( '.' . $site ) ) === '.' . $site; } /** * Read an attribute value from a tag's attribute string. * * @param string $attrs Attribute string. * @param string $name Attribute name. * @return string */ public static function attr( $attrs, $name ) { if ( preg_match( '#\b' . preg_quote( $name, '#' ) . '\s*=\s*(["\'])(.*?)\1#is', $attrs, $m ) ) { return $m[2]; } if ( preg_match( '#\b' . preg_quote( $name, '#' ) . '\s*=\s*([^\s>]+)#i', $attrs, $m ) ) { return $m[1]; } return ''; } /** * Needles for the browser-side guard (URL-shaped only), built from the same data. * * @return array{m:string[],a:string[],ok:string[],tw:string[],enf:bool,site:string} */ public function guard_config() { $cfg = array( 'm' => array(), 'a' => array(), 'ok' => array(), 'tw' => array(), 'enf' => $this->settings->enforcing(), 'site' => (string) wp_parse_url( home_url(), PHP_URL_HOST ), ); $overrides = (array) $this->settings->get( 'overrides' ); foreach ( Signatures::all() as $id => $sig ) { $c = isset( $overrides[ $id ] ) ? $overrides[ $id ] : $sig[1]; foreach ( $sig[2] as $needle ) { if ( false !== strpbrk( $needle, "('\"" ) ) { continue; // inline-code needle, never part of a URL. } if ( 'necessary' !== $c ) { $cfg[ 'marketing' === $c ? 'm' : 'a' ][] = strtolower( $needle ); } } } foreach ( $overrides as $id => $c ) { if ( 0 === strpos( $id, 'host:' ) ) { $h = strtolower( substr( $id, 5 ) ); if ( 'necessary' === $c ) { $cfg['ok'][] = $h; } else { $cfg[ 'marketing' === $c ? 'm' : 'a' ][] = $h; } } } $cfg['ok'] = array_values( array_unique( array_merge( $cfg['ok'], Signatures::benign_hosts() ) ) ); $cfg['m'] = array_values( array_unique( $cfg['m'] ) ); $cfg['a'] = array_values( array_unique( $cfg['a'] ) ); if ( $cfg['enf'] ) { $cfg['tw'] = array( 'pixel', 'track', 'analytic', 'beacon', 'collect', 'retarget', 'remarket', 'conversion', 'adserver', 'advert', 'heatmap', 'fingerprint', 'telemetry' ); } return $cfg; } /** * Signatures with advertising checked first, so mixed tags are held as advertising. * * @return array */ private function sorted() { if ( null === $this->sorted ) { $rank = array( 'marketing' => 0, 'analytics' => 1, 'necessary' => 2, ); $sigs = Signatures::all(); uasort( $sigs, static function ( $a, $b ) use ( $rank ) { return $rank[ $a[1] ] - $rank[ $b[1] ]; } ); $this->sorted = $sigs; } return $this->sorted; } }