|null Dismissed searches, normalized. Null = unread. */ private static $dismissed = null; /** * One spelling for one search, so a difference in case or spacing can never * store an entry that matches nothing and cannot be restored. * * @param string $query Any search. * @return string */ public static function normal( $query ) { return trim( preg_replace( '/\s+/u', ' ', strtolower( (string) $query ) ) ); } /** * The searches this owner has dismissed, normalized. * * ⭐ Public so the owner's noise report can list them WHOLE and offer each * one back. A ledger that filters silently and cannot be read is the thing * this whole class exists not to be. * * @return array */ public static function dismissed() { if ( null === self::$dismissed ) { $list = ( new Settings() )->get( 'search_dismissed', array() ); self::$dismissed = array_map( array( __CLASS__, 'normal' ), (array) $list ); } return self::$dismissed; } /** * Has the owner dismissed this search? * * @param string $query The reported search. * @return bool */ public static function is_dismissed( $query ) { $q = self::normal( $query ); return '' !== $q && in_array( $q, self::dismissed(), true ); } /** * Search operators — the shape of a machine probe. * * ⛔ Kept NARROW on purpose. This answers "who searched", and the Search * screen puts the word "machine traffic" behind the answer. See the class * docblock for why {@see is_noise()} is the one that grew instead. * * @param string $query The reported search. * @return bool */ public static function is_operator( $query ) { $q = strtolower( trim( (string) $query ) ); if ( '' === $q ) { return false; } foreach ( array( 'site:', 'intext:', 'inurl:', 'intitle:', 'filetype:', 'cache:', 'related:' ) as $operator ) { if ( preg_match( '/(?:^|[\s("\'\[])[-+]?' . preg_quote( $operator, '/' ) . '/u', $q ) ) { return true; } } return false; } /** * A web address, not a question. * * ⚠️ ONLY THE UNAMBIGUOUS FORMS — a scheme, or a leading `www.`. A bare * `example.com` is deliberately NOT caught, because "vue.js", "node.js" and * "next.js" are domain-shaped and are among the most searched strings on * earth. Killing a real search to catch a fake one is the wrong trade: this * under-reaches on purpose, and says so. * * @param string $query The reported search. * @return bool */ public static function is_address( $query ) { $q = strtolower( trim( (string) $query ) ); return '' !== $q && ( false !== strpos( $q, '://' ) || 0 === strpos( $q, 'www.' ) ); } /** * Longer than a search: a prompt, a paragraph, a question pasted whole. * * ⭐ THE LIMIT IS NOT INVENTED. It is {@see Focus::MAX_LEN}, the cap on the * focus field the owner types into — commented there as "A search, not a * sentence". If the owner could not enter it as what this page is for, the * plugin must not judge the page against it. One idea, one number. * * ⚠️ Measured in characters, matching how Focus::sanitize() truncates. On the * real corpus this was calibrated against, the longest genuine search ran to * 74 characters against a cap of 120 — the headroom is deliberate, because * dropping a real search is the costlier mistake. * * @param string $query The reported search. * @return bool */ public static function is_paste( $query ) { return mb_strlen( trim( (string) $query ) ) > Focus::MAX_LEN; } /** * WHY this search cannot judge a page, or '' when it can. * * Returning the reason rather than a bare true keeps the owner's noise * report able to say what it dropped and on what grounds — the difference * between a filter they can audit and one they have to trust. * * @param string $query The reported search. * @return string One of the class constants, or '' for a real search. */ public static function kind( $query ) { if ( self::is_operator( $query ) ) { return self::OPERATOR; } if ( self::is_address( $query ) ) { return self::ADDRESS; } if ( self::is_paste( $query ) ) { return self::PASTE; } // LAST — see the constant. An automatic rule that also applies is the // reason that binds, because undoing this one would not bring it back. if ( self::is_dismissed( $query ) ) { return self::DISMISSED; } return ''; } /** * Is this a string no page should be marked down for failing? * * ⛔ THE ONE PREDICATE for every surface that judges a page by a search — * the coverage worklist, the focus choices offered, split-search collisions, * the opportunity list and the progress index. One owner, so a query dropped * from one of them can never still be judged by another. * * @param string $query The reported search. * @return bool */ public static function is_noise( $query ) { return '' !== self::kind( $query ); } }