0; } /** * A page that IS on page one and gets scrolled past. * * Nothing is wrong with such a page — what a searcher reads before deciding * is the title and description pair, so that pair is the whole ask, and * writing both is the whole of the owner's side. Whether anyone then clicks * is the searcher's business and the engine's to report. * * Both fields must be written BY HAND. A derived description means the owner * has not rewritten anything, and counting a fallback as work done would * retire a finding nobody acted on. * * @param \WP_Post $post The post. * @return bool */ public static function seen_done( $post ) { // With an SEO plugin owning titles, the field we asked them to rewrite is // not ours to read. Unreadable is not done: we say nothing rather than // retire a finding on a guess. if ( ! Seo::title_ui_enabled() ) { return false; } if ( '' === Seo::sanitize_title( (string) get_post_meta( $post->ID, Seo::META_TITLE, true ) ) ) { return false; } return '' !== trim( Description::explicit( $post->ID ) ); } /** * The same question for a set of pages, answered once and cached for an hour. * * Measuring is not free — coverage runs the block renderer and the stemmer * over the post body, and the inbound-link count is a LIKE across every * published post — and this runs on every admin page load. * * The cache key hangs on the last time ANY post was modified, not on these * posts' own timestamps. Adding the internal link that finishes a page is an * edit to a DIFFERENT post, so a per-post key would have gone stale in * precisely the case this exists for: the fix made, and the screen still * asking for it. * * @param array $ids Post IDs. * @param string $kind Opportunities::KIND_NEAR or ::KIND_SEEN. * @return array Post ID → the owner's side is done. */ public static function map( array $ids, $kind ) { $ids = array_values( array_unique( array_filter( array_map( 'intval', $ids ) ) ) ); if ( ! $ids ) { return array(); } sort( $ids ); $key = 'agentimus_side_done_' . md5( (string) $kind . '|' . implode( ',', $ids ) . '|' . (string) get_lastpostmodified( 'gmt' ) ); $cached = get_transient( $key ); if ( is_array( $cached ) ) { return $cached; } $out = array(); foreach ( $ids as $id ) { $out[ $id ] = self::done( $id, $kind ); } set_transient( $key, $out, HOUR_IN_SECONDS ); return $out; } /** * The post IDs behind a set of worklist cards. * * Both card shapes are accepted on purpose: the same card is called `page_id` * on the way out of {@see Opportunities} and `postId` once it has been shaped * for the wire, and both callers need this identical loop. One place to get * it right beats two that agree today. * * @param array $cards Raw or wire cards. * @return array IDs, zeros dropped. */ public static function card_ids( array $cards ) { $ids = array(); foreach ( $cards as $card ) { if ( ! is_array( $card ) ) { continue; } $id = isset( $card['postId'] ) ? (int) $card['postId'] : (int) ( isset( $card['page_id'] ) ? $card['page_id'] : 0 ); if ( $id > 0 ) { $ids[] = $id; } } return array_values( array_unique( $ids ) ); } /** * How many of the site's own published posts link to this one. * * One COUNT over post_content for the slug as a link path — matched with a * closing boundary (/ or ") so "the-okf" never counts "the-okf-2". Memoized * per request: the editor's advice and this class's verdict both want it. * * @param \WP_Post $post The post. * @return int */ public static function inbound_links( $post ) { static $memo = array(); $id = (int) $post->ID; if ( isset( $memo[ $id ] ) ) { return $memo[ $id ]; } $slug = (string) $post->post_name; if ( '' === $slug ) { $memo[ $id ] = 0; return 0; } global $wpdb; $types = array_map( 'esc_sql', Content::post_types() ); $in = "'" . implode( "','", $types ) . "'"; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $in is esc_sql'd above. $memo[ $id ] = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_status = 'publish' AND ID != %d AND post_type IN ($in) AND (post_content LIKE %s OR post_content LIKE %s)", $id, '%' . $wpdb->esc_like( '/' . $slug . '/' ) . '%', '%' . $wpdb->esc_like( '/' . $slug . '"' ) . '%' ) ); return $memo[ $id ]; } }