"lorem" and/or "email". */ public static function placeholders( string $text ): array { $found = array(); if ( 1 === preg_match( '/\blorem\s+ipsum\b|\bdolor\s+sit\s+amet\b/i', $text ) ) { $found[] = 'lorem'; } if ( 1 === preg_match( self::PLACEHOLDER_MAIL, $text ) ) { $found[] = 'email'; } return $found; } /** * Whether a post is the sample content WordPress creates on installation. * * @param string $type Post type. * @param string $slug Post slug. * @param string $title Post title. */ public static function is_default_post( string $type, string $slug, string $title ): bool { return ( 'post' === $type && 'hello-world' === $slug && 'Hello world!' === $title ) || ( 'page' === $type && 'sample-page' === $slug && 'Sample Page' === $title ); } /** * Number of empty paragraphs and headings in a piece of content. * * @param string $html Post content. */ public static function empty_elements( string $html ): int { $blank = '(?:\s| |\xC2\xA0|)*'; $count = (int) preg_match_all( '/]*>' . $blank . '<\/p>/i', $html ); return $count + (int) preg_match_all( '/]*>' . $blank . '<\/h\1>/i', $html ); } /** * Elementor element types that only hold other elements: classic sections, columns and * containers, and the atomic (V4) div block, flexbox and grid. Any other element type, such * as atomic tabs or a form, counts as content. * * @var string[] */ public const ELEMENTOR_CONTAINERS = array( 'section', 'column', 'container', 'e-div-block', 'e-flexbox', 'e-grid' ); /** * Elementor sections, columns and containers that hold no widget at all. An empty one is * counted once, without the empty elements inside it. * * @param array $elements Decoded `_elementor_data` (a list of elements). */ public static function empty_containers( array $elements ): int { $count = 0; foreach ( $elements as $element ) { if ( ! is_array( $element ) || self::is_content( $element ) ) { continue; } if ( ! self::has_widget( $element ) ) { ++$count; continue; } $count += self::empty_containers( is_array( $element['elements'] ?? null ) ? $element['elements'] : array() ); } return $count; } /** * Whether an Elementor element is a widget or holds one. * * @param array $element Element. */ private static function has_widget( array $element ): bool { if ( self::is_content( $element ) ) { return true; } foreach ( is_array( $element['elements'] ?? null ) ? $element['elements'] : array() as $child ) { if ( is_array( $child ) && self::has_widget( $child ) ) { return true; } } return false; } /** * Whether an Elementor element is content itself: a widget, or an element type that is not * a plain container. * * @param array $element Element. */ public static function is_content( array $element ): bool { return ! empty( $element['widgetType'] ) || ! in_array( $element['elType'] ?? '', self::ELEMENTOR_CONTAINERS, true ); } /** * Whether a post was last changed longer ago than the given number of months. * * @param int $modified Unix time of the last change. * @param int $now Unix time now. * @param int $months Months. */ public static function is_stale( int $modified, int $now, int $months ): bool { return $modified > 0 && $now - $modified > $months * 30 * 86400; } /** * Dates in the past that sit in a sentence about something still to come ("join us on", * "sale ends", "register by"). Only full dates are read: 2024-05-12, 12 May 2024 and * May 12, 2024. Plain statements such as "founded on 3 June 2001" are not touched. * * @param string $text Post content. * @param int $now Unix time now. * @return array The dates as written, at most five. */ public static function past_event_dates( string $text, int $now ): array { $text = html_entity_decode( wp_strip_all_tags( $text ), ENT_QUOTES, 'UTF-8' ); $found = array(); $month = '(?:January|February|March|April|May|June|July|August|September|October|November|December|Jan|Feb|Mar|Apr|Jun|Jul|Aug|Sep|Sept|Oct|Nov|Dec)\.?'; $date = '(\d{4}-\d{2}-\d{2}|\d{1,2}(?:st|nd|rd|th)?\s+' . $month . ',?\s+\d{4}|' . $month . '\s+\d{1,2}(?:st|nd|rd|th)?,?\s+\d{4})'; $cue = '/\b(?:join us|takes? place|will be held|register(?:ation)?\s+(?:by|until|before)|sign up\s+(?:by|before)|sale ends|offer (?:ends|valid)|valid until|until|deadline|early bird|doors open|starts? on|ends? on|coming soon|save the date|event|tickets?|book (?:now|by|before)|expires?|closes?)\b/i'; foreach ( (array) preg_split( '/(?<=[.!?])\s+|\n+/', $text ) as $sentence ) { if ( 1 !== preg_match( $cue, (string) $sentence ) || 1 !== preg_match_all( '/' . $date . '/i', (string) $sentence, $m ) ) { continue; } foreach ( $m[1] as $written ) { $time = strtotime( preg_replace( '/(\d)(?:st|nd|rd|th)/i', '$1', str_replace( 'Sept', 'Sep', $written ) ) . ' 23:59:59 UTC' ); if ( false !== $time && $time < $now - DAY_IN_SECONDS ) { $found[ $written ] = true; } } } return array_slice( array_keys( $found ), 0, 5 ); } /** * Hashes of the longer paragraphs of a piece of content, for finding text that appears on * several pages. Short paragraphs are skipped, because short sentences repeat by chance. * * @param string $html Post content. * @return array Hash values without repeats. */ public static function paragraph_hashes( string $html ): array { $out = array(); if ( false === preg_match_all( '/]*>(.*?)<\/p>/is', $html, $m ) ) { return $out; } foreach ( $m[1] as $paragraph ) { $text = strtolower( trim( (string) preg_replace( '/\s+/', ' ', html_entity_decode( wp_strip_all_tags( $paragraph ), ENT_QUOTES, 'UTF-8' ) ) ) ); if ( mb_strlen( $text ) >= self::DUPLICATE_MIN ) { $out[ crc32( $text ) ] = crc32( $text ); } } return array_values( $out ); } }