$events Events straight from core. * @param array $shared_logins Logins of shared calendars the * caller explicitly requested. * @return list */ public static function filter( iterable $events, array $shared_logins = [] ): array { $visible = []; foreach ( $events as $event ) { if ( self::can_view( $event, $shared_logins ) ) { $visible[] = $event; } } return $visible; } /** * Whether the current viewer may see one event. * * @param array $shared_logins Logins of shared calendars the * caller explicitly requested. */ public static function can_view( Event $event, array $shared_logins = [] ): bool { if ( ! self::passes_moderation_check( $event ) ) { return false; } if ( current_user_can( 'webcal_can_view_all_events' ) ) { return true; } $login = self::current_login(); if ( $login !== null && $event->createdBy() === $login ) { return true; } if ( isset( $shared_logins[ $event->createdBy() ] ) ) { return true; } return $event->access() === AccessLevel::PUBLIC; } /** * Whether asking for a shared calendar may show the current viewer its * non-Public events (rule 3). * * Only a calendar that is public, or one the viewer administers, or any * calendar for a viewer who manages shared calendars. Without this, any * Subscriber could name a private team calendar in `users=` and read * everything on it. Remote calendars (those with a URL) never qualify. * * @psalm-suppress PossiblyUnusedMethod -- Called only from REST route callbacks, which Psalm cannot see registered. */ public static function entitled_to_shared_calendar( Calendar $calendar ): bool { if ( $calendar->url() !== null ) { return false; } if ( $calendar->isPublic() || current_user_can( 'webcal_can_manage_shared_calendars' ) ) { return true; } $login = self::current_login(); return $login !== null && $calendar->admin() === $login; } /** * Number of events the current viewer may see. * * Counts in SQL rather than loading every event through filter(); the * WHERE clause is count_query(), which states can_view()'s rules. */ public static function count_visible_events(): int { global $wpdb; [ $sql, $args ] = self::count_query( $wpdb->prefix . 'webcal_entry', self::current_login(), current_user_can( 'webcal_can_view_all_events' ), current_user_can( 'webcal_can_approve' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- $sql is built from fixed fragments with placeholders only. return (int) $wpdb->get_var( $wpdb->prepare( $sql, ...$args ) ); } /** * COUNT query matching can_view(), for a given viewer. * * Kept free of WordPress calls so the database tests can run it against * MySQL directly. Any change to can_view() must be mirrored here; * VisibleEventCountTest covers one row per rule. * * @param string $table Fully prefixed webcal_entry table name. * @param string|null $login Viewer's login, or null when anonymous. * @param bool $view_all Viewer holds webcal_can_view_all_events. * @param bool $approve Viewer holds webcal_can_approve. * @return array{0: string, 1: list} SQL with placeholders, and its arguments. */ public static function count_query( string $table, ?string $login, bool $view_all, bool $approve ): array { $args = [ $table ]; // Others' events: pending submissions only for a moderator, and // non-Public ones only for a calendar admin. $others = []; if ( ! $approve ) { $others[] = "(cal_status IS NULL OR cal_status <> 'TENTATIVE')"; } if ( ! $view_all ) { $others[] = "cal_access = 'P'"; } $where = $others === [] ? '1 = 1' : '(' . implode( ' AND ', $others ) . ')'; // A viewer's own events pass every rule. if ( $login !== null ) { $where = 'cal_create_by = %s OR ' . $where; $args[] = $login; } return [ 'SELECT COUNT(*) FROM %i WHERE ' . $where, $args ]; } /** * Pending frontend submissions stay off the calendar until approved. * * Visible to whoever submitted them, and to anyone who can approve. This * is what holds an unmoderated submission back, so it is checked before * the access-level rules rather than alongside them. */ private static function passes_moderation_check( Event $event ): bool { if ( $event->status() !== 'TENTATIVE' ) { return true; } return self::passes_moderation_for( $event, self::current_login(), current_user_can( 'webcal_can_approve' ) ); } /** * The moderation rule for a viewer who is not the current session. * * The iCal feed is read with a token rather than a login, so it states * who the reader is instead of asking WordPress. * * @param string|null $login Reader's login, or null when anonymous. * @param bool $can_approve Reader holds webcal_can_approve. */ public static function passes_moderation_for( Event $event, ?string $login, bool $can_approve ): bool { if ( $event->status() !== 'TENTATIVE' ) { return true; } if ( $login !== null && $event->createdBy() === $login ) { return true; } return $can_approve; } /** * The current viewer's login, or null when there is no usable one. * * An anonymous request carries an empty WP_User whose `user_login` reads * back as `false` (WP_User::__get() falls through to get_user_meta( 0 )), * so this deliberately returns null rather than a falsy string: comparing * `'' === $event->createdBy()` would hand ownership of every * blank-creator event to every logged-out visitor. * * @psalm-suppress RedundantCastGivenDocblockType -- The cast is not * redundant at runtime. wordpress-stubs types WP_User::$user_login as * string, but an empty user returns false from the magic getter; both * Psalm and PHPStan believe the stub. Dropping the cast to satisfy Psalm * makes PHPStan demand the is_string() check back, and dropping both * reintroduces the false-vs-'' bug this guards. */ private static function current_login(): ?string { if ( ! is_user_logged_in() ) { return null; } // The cast is load-bearing: WP_User types user_login as string, but an // empty user reports it as false at runtime, and `false === (string)` // comparisons behave differently from `'' === (string)`. $login = (string) wp_get_current_user()->user_login; return $login === '' ? null : $login; } }