Calendar Page; a published page * (or else post) that holds the WebCalendar block or [webcalendar] shortcode; * the home page. The `webcal_calendar_page_url` filter has the last word. */ final class CalendarPageLocator { /** Settings > Calendar Page: a page ID, 0 for automatic. */ public const OPTION = 'webcal_calendar_page'; /** The detected page ID, 0 when none was found. Cleared on any post save. */ public const DETECTED_TRANSIENT = 'webcal_calendar_page_detected'; /** * The URL "Back to Calendar" should link to. */ public function url(): string { $url = $this->chosen() ?? $this->detected() ?? home_url( '/' ); /** * Filters where "Back to Calendar" on an event page leads. * * @param string $url The chosen or detected calendar page, or the home page. */ $filtered = apply_filters( 'webcal_calendar_page_url', $url ); return '' !== $filtered ? $filtered : $url; } /** * Forget the detected page, so the next lookup sees a page just added, * edited or removed. Hooked to save_post and deleted_post. */ public function forget_detected(): void { delete_transient( self::DETECTED_TRANSIENT ); } /** * The page chosen in Settings, if it is still published. */ private function chosen(): ?string { $raw = get_option( self::OPTION, 0 ); $id = is_scalar( $raw ) ? absint( $raw ) : 0; if ( $id <= 0 || 'publish' !== get_post_status( $id ) ) { return null; } $url = get_permalink( $id ); return is_string( $url ) && '' !== $url ? $url : null; } /** * A published page, or failing that post, that shows the calendar. * * Matches the block and the shortcode with or without attributes, but * not [webcalendar-submit] or [webcalendar-rsvp], which share its prefix. * Cached, including "none found", since every event page asks. */ private function detected(): ?string { global $wpdb; $cached = get_transient( self::DETECTED_TRANSIENT ); if ( is_numeric( $cached ) ) { $id = (int) $cached; } else { $sql = $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish' AND post_password = '' AND post_type IN ('page', 'post') AND ( post_content LIKE %s OR post_content LIKE %s OR post_content LIKE %s ) ORDER BY ( post_type = 'page' ) DESC, menu_order ASC, ID ASC LIMIT 1", '%' . $wpdb->esc_like( '[webcalendar]' ) . '%', '%' . $wpdb->esc_like( '[webcalendar ' ) . '%', '%' . $wpdb->esc_like( '