0 && PostAccess::can_change( $space, $event['user_id'], $user_id ); } /** * Whether a member may answer an event. * * @param array $event Event row. * @param array $space Space row. * @param int $user_id Member. * @return bool */ public static function can_rsvp( array $event, array $space, $user_id ) { return 'published' === $event['status'] && strtotime( $event['ends_at'] . ' UTC' ) > time() && PostAccess::can_comment( $space, $user_id ); } /** * Finds an event. * * @param int $id Event ID. * @return array|null */ public static function find( $id ) { global $wpdb; $row = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table, fresh read. $wpdb->prepare( 'SELECT ' . self::COLUMNS . ' FROM %i WHERE id = %d', Tables::name( 'events' ), absint( $id ) ), // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- COLUMNS is a class constant. ARRAY_A ); return $row ? self::normalize( $row ) : null; } /** * Lists events the viewer may read. * * @param array $args space_id, from, to (UTC datetimes), when (upcoming|past|range), rsvp (going), page, per_page. * @param int $viewer_id Viewer. * @return array{items: array[], total: int} */ public static function query( array $args, $viewer_id ) { global $wpdb; $spaces = PostAccess::readable_space_ids( $viewer_id ); if ( ! empty( $args['space_id'] ) ) { $spaces = in_array( (int) $args['space_id'], $spaces, true ) ? array( (int) $args['space_id'] ) : array(); } if ( ! $spaces ) { return array( 'items' => array(), 'total' => 0, ); } $per_page = max( 1, min( 100, absint( $args['per_page'] ?? 20 ) ) ); $offset = ( max( 1, absint( $args['page'] ?? 1 ) ) - 1 ) * $per_page; $when = $args['when'] ?? 'upcoming'; $now = Time::now(); $from = 'range' === $when ? (string) $args['from'] : ( 'upcoming' === $when ? $now : '1970-01-01 00:00:00' ); $to = 'range' === $when ? (string) $args['to'] : ( 'upcoming' === $when ? '9999-12-31 00:00:00' : $now ); $going = 'going' === ( $args['rsvp'] ?? '' ) ? absint( $viewer_id ) : 0; $past = 'past' === $when ? 1 : 0; $live = 'upcoming' === $when ? 1 : 0; if ( $past ) { $from = '1970-01-01 00:00:00'; $to = $now; } $placeholders = implode( ',', array_fill( 0, count( $spaces ), '%d' ) ); $params = array_merge( array( Tables::name( 'events' ), Tables::name( 'event_rsvps' ), $going, $going, $from, $past, $to, $past, $to, $live ), $spaces ); // Upcoming lists show published events that have not ended; past lists show ended ones (newest // first); ranges show events that overlap the range. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Placeholders are built from %d only. $rows = $wpdb->get_results( $wpdb->prepare( "SELECT e.id, e.space_id, e.user_id, e.title, e.description, e.description_rendered, e.location_type, e.location, e.online_url, e.capacity, e.status, e.going_count, e.maybe_count, e.reminder_sent, e.starts_at, e.ends_at, e.created_at, e.updated_at FROM %i e LEFT JOIN %i r ON r.event_id = e.id AND r.user_id = %d AND r.status = 'going' WHERE ( %d = 0 OR r.id IS NOT NULL ) AND e.ends_at >= %s AND ( ( %d = 1 AND e.ends_at < %s ) OR ( %d = 0 AND e.starts_at <= %s ) ) AND ( %d = 0 OR e.status = 'published' ) AND e.space_id IN ($placeholders) ORDER BY CASE WHEN %d = 1 THEN e.starts_at END DESC, e.starts_at ASC, e.id ASC LIMIT %d OFFSET %d", array_merge( $params, array( $past, $per_page, $offset ) ) ), ARRAY_A ); $total = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(e.id) FROM %i e LEFT JOIN %i r ON r.event_id = e.id AND r.user_id = %d AND r.status = 'going' WHERE ( %d = 0 OR r.id IS NOT NULL ) AND e.ends_at >= %s AND ( ( %d = 1 AND e.ends_at < %s ) OR ( %d = 0 AND e.starts_at <= %s ) ) AND ( %d = 0 OR e.status = 'published' ) AND e.space_id IN ($placeholders)", $params ) ); // phpcs:enable return array( 'items' => array_map( array( __CLASS__, 'normalize' ), (array) $rows ), 'total' => $total, ); } /** * Creates an event. * * @param array $space Space row. * @param array $input Raw values. * @param int $user_id Organizer. * @return array|WP_Error Event row. */ public static function create( array $space, array $input, $user_id ) { global $wpdb; $clean = self::clean( $input, null ); if ( is_wp_error( $clean ) ) { return $clean; } if ( strtotime( $clean['starts_at'] . ' UTC' ) < time() - HOUR_IN_SECONDS ) { return self::field_error( 'spacebay_event_past', __( 'Choose a start time in the future.', 'spacebay' ), 'starts_at' ); } // Counted after the form is valid, so typing mistakes do not use up attempts. $limited = RateLimit::hit( 'event', $user_id, 10, HOUR_IN_SECONDS ); if ( is_wp_error( $limited ) ) { return $limited; } $now = Time::now(); $wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table. Tables::name( 'events' ), array_merge( $clean, array( 'space_id' => (int) $space['id'], 'user_id' => absint( $user_id ), 'status' => 'published', 'created_at' => $now, 'updated_at' => $now, ) ) ); $event = self::find( (int) $wpdb->insert_id ); if ( ! $event ) { return new WP_Error( 'spacebay_event_failed', __( 'The event could not be saved. Please try again.', 'spacebay' ), array( 'status' => 500 ) ); } // The organizer is going. self::answer( $event, $user_id, 'going', true ); $event = self::find( $event['id'] ); // Only space moderators may announce an event to every member of the space. if ( ! empty( $input['notify'] ) && SpaceAccess::can_moderate( $space, $user_id ) ) { wp_schedule_single_event( time(), self::NOTIFY_HOOK, array( (int) $event['id'], 'event_new', absint( $user_id ) ) ); } /** * Fires after an event is created. * * @since 1.0.0 * * @param array $event Event row. * @param array $space Space row. */ do_action( 'spacebay_event_created', $event, $space ); return $event; } /** * Changes an event. Members who answered hear about a new time or place. * * @param array $event Event row. * @param array $input Raw values. * @param int $user_id Member who changes it. * @return array|WP_Error */ public static function update( array $event, array $input, $user_id ) { global $wpdb; $clean = self::clean( $input, $event ); if ( is_wp_error( $clean ) ) { return $clean; } $moved = $clean['starts_at'] !== $event['starts_at'] || $clean['location'] !== $event['location'] || $clean['online_url'] !== $event['online_url'] || $clean['location_type'] !== $event['location_type']; if ( $clean['starts_at'] !== $event['starts_at'] ) { $clean['reminder_sent'] = 0; } $clean['updated_at'] = Time::now(); $wpdb->update( Tables::name( 'events' ), $clean, array( 'id' => (int) $event['id'] ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table. if ( $moved && 'published' === $event['status'] ) { wp_schedule_single_event( time(), self::NOTIFY_HOOK, array( (int) $event['id'], 'event_updated', absint( $user_id ) ) ); } $updated = self::find( $event['id'] ); /** * Fires after an event is changed. * * @since 1.0.0 * * @param array $updated Event row after the change. * @param array $event Event row before the change. */ do_action( 'spacebay_event_updated', $updated, $event ); return $updated; } /** * Cancels an event and tells members who said they would come. * * @param array $event Event row. * @param int $user_id Member who cancels it. * @return array|null Null when the event was deleted meanwhile. */ public static function cancel( array $event, $user_id ) { global $wpdb; if ( 'cancelled' !== $event['status'] ) { $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table. Tables::name( 'events' ), array( 'status' => 'cancelled', 'updated_at' => Time::now(), ), array( 'id' => (int) $event['id'] ) ); Points::remove_ref( 'event:' . (int) $event['id'] ); if ( strtotime( $event['ends_at'] . ' UTC' ) > time() ) { wp_schedule_single_event( time(), self::NOTIFY_HOOK, array( (int) $event['id'], 'event_cancelled', absint( $user_id ) ) ); } } return self::find( $event['id'] ); } /** * Deletes an event with its answers. * * @param array $event Event row. * @return void */ public static function delete( array $event ) { global $wpdb; // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom tables. $wpdb->delete( Tables::name( 'event_rsvps' ), array( 'event_id' => (int) $event['id'] ) ); $wpdb->delete( Tables::name( 'events' ), array( 'id' => (int) $event['id'] ) ); $wpdb->delete( Tables::name( 'notifications' ), array( 'object_type' => 'event', 'object_id' => (int) $event['id'], ) ); // phpcs:enable Points::remove_ref( 'event:' . (int) $event['id'] ); /** * Fires after an event is deleted. * * @since 1.0.0 * * @param array $event Deleted event row. */ do_action( 'spacebay_event_deleted', $event ); } /** * Saves a member's answer. * * @param array $event Event row. * @param int $user_id Member. * @param string $answer "going", "maybe", "not_going" or "" to remove it. * @param bool $force Skip checks such as a place limit (the organizer). * @return true|WP_Error */ public static function answer( array $event, $user_id, $answer, $force = false ) { global $wpdb; $user_id = absint( $user_id ); $table = Tables::name( 'event_rsvps' ); $current = self::answer_of( $event['id'], $user_id ); if ( '' !== $answer && ! in_array( $answer, self::ANSWERS, true ) ) { return new WP_Error( 'spacebay_rsvp_invalid', __( 'Choose going, maybe or can’t go.', 'spacebay' ), array( 'status' => 400 ) ); } /** * Filters whether an answer may be saved, for example to enforce a place * limit (SpaceBay Pro). Return a WP_Error to refuse it. * * @since 1.0.0 * * @param true|WP_Error $allowed Whether the answer may be saved. * @param array $event Event row. * @param int $user_id Member. * @param string $answer New answer. * @param string $current Current answer. * @param bool $force Whether the organizer answers (no limits). */ $allowed = apply_filters( 'spacebay_event_answer_check', true, $event, $user_id, $answer, (string) $current, (bool) $force ); if ( is_wp_error( $allowed ) ) { return $allowed; } if ( '' === $answer ) { $wpdb->delete( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table. $table, array( 'event_id' => (int) $event['id'], 'user_id' => $user_id, ) ); } elseif ( $current ) { $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table. $table, array( 'status' => $answer, 'updated_at' => Time::now(), ), array( 'event_id' => (int) $event['id'], 'user_id' => $user_id, ) ); } else { $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table; the unique key keeps one answer per member. $wpdb->prepare( 'INSERT IGNORE INTO %i (event_id, user_id, status, created_at, updated_at) VALUES (%d, %d, %s, %s, %s)', $table, (int) $event['id'], $user_id, $answer, Time::now(), Time::now() ) ); } self::recount( $event['id'] ); if ( 'going' === $answer ) { Gamification::earn( $user_id, 'event', 'event:' . (int) $event['id'], 'event', (int) $event['id'] ); } elseif ( 'going' === $current ) { Points::remove( $user_id, 'event:' . (int) $event['id'] ); } /** * Fires after a member answers an event. * * @since 1.0.0 * * @param array $event Event row. * @param int $user_id Member. * @param string $answer New answer, empty when removed. * @param string $current Previous answer. */ do_action( 'spacebay_event_rsvp', $event, $user_id, $answer, (string) $current ); return true; } /** * A member's answer. * * @param int $event_id Event ID. * @param int $user_id Member. * @return string Empty when the member did not answer. */ public static function answer_of( $event_id, $user_id ) { global $wpdb; if ( ! $user_id ) { return ''; } return (string) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Fresh read. $wpdb->prepare( 'SELECT status FROM %i WHERE event_id = %d AND user_id = %d', Tables::name( 'event_rsvps' ), absint( $event_id ), absint( $user_id ) ) ); } /** * Answers of a member for several events. * * @param int[] $event_ids Event IDs. * @param int $user_id Member. * @return array */ public static function answers_of( array $event_ids, $user_id ) { global $wpdb; $event_ids = array_values( array_filter( array_map( 'absint', $event_ids ) ) ); if ( ! $event_ids || ! $user_id ) { return array(); } $placeholders = implode( ',', array_fill( 0, count( $event_ids ), '%d' ) ); $rows = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- One query per list. $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- The IN() list has one %d per ID. "SELECT event_id, status FROM %i WHERE user_id = %d AND event_id IN ($placeholders)", // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Placeholders are built from %d only. array_merge( array( Tables::name( 'event_rsvps' ), absint( $user_id ) ), $event_ids ) ), ARRAY_A ); return wp_list_pluck( (array) $rows, 'status', 'event_id' ); } /** * Members who answered, newest answers last. * * @param int $event_id Event ID. * @param string $answer "going" or "maybe". * @param int $page Page. * @param int $per_page Per page. * @return array{ids: int[], total: int} */ public static function attendees( $event_id, $answer, $page = 1, $per_page = 50 ) { global $wpdb; $table = Tables::name( 'event_rsvps' ); $per_page = max( 1, min( 100, absint( $per_page ) ) ); $offset = ( max( 1, absint( $page ) ) - 1 ) * $per_page; // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table, paged. $ids = $wpdb->get_col( $wpdb->prepare( 'SELECT user_id FROM %i WHERE event_id = %d AND status = %s ORDER BY created_at ASC, id ASC LIMIT %d OFFSET %d', $table, absint( $event_id ), $answer, $per_page, $offset ) ); $total = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(id) FROM %i WHERE event_id = %d AND status = %s', $table, absint( $event_id ), $answer ) ); // phpcs:enable return array( 'ids' => array_map( 'intval', (array) $ids ), 'total' => $total, ); } /** * Notifies members about an event. Runs in the background. * * New events go to every active member of the space; changes, cancellations * and reminders go to members who answered going or maybe. * * @param int $event_id Event ID. * @param string $action "event_new", "event_updated", "event_cancelled" or "event_reminder". * @param int $actor_id Member who caused it. * @return void */ public static function notify( $event_id, $action, $actor_id ) { global $wpdb; $event = self::find( $event_id ); $space = $event ? SpaceRepository::find( $event['space_id'] ) : null; if ( ! $event || ! $space ) { return; } // Only the cancellation itself goes out for a cancelled event. if ( 'cancelled' === $event['status'] && 'event_cancelled' !== $action ) { return; } if ( 'event_new' === $action ) { $ids = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Background job. $wpdb->prepare( "SELECT user_id FROM %i WHERE space_id = %d AND status = 'active'", Tables::name( 'space_members' ), (int) $space['id'] ) ); } else { $ids = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Background job. $wpdb->prepare( "SELECT user_id FROM %i WHERE event_id = %d AND status IN ('going', 'maybe')", Tables::name( 'event_rsvps' ), (int) $event['id'] ) ); } $data = array( 'title' => $event['title'], 'space_title' => $space['title'], 'starts_at' => Time::iso( $event['starts_at'] ), 'excerpt' => self::when_text( $event ), ); foreach ( array_map( 'intval', (array) $ids ) as $user_id ) { if ( $user_id !== (int) $actor_id && SpaceAccess::can_read( $space, $user_id ) ) { Notifications::add( $user_id, (int) $actor_id, $action, 'event', (int) $event['id'], $data, 'events/' . (int) $event['id'] ); } } } /** * Date and time of an event in the site's time zone, for emails. * * @param array $event Event row. * @return string */ public static function when_text( array $event ) { $start = strtotime( $event['starts_at'] . ' UTC' ); return wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $start ) . ' (' . wp_timezone_string() . ')'; } /** * Deletes the events of a deleted space. * * @param array $space Deleted space row. * @return void */ public static function on_space_deleted( $space ) { global $wpdb; $ids = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Cleanup. $wpdb->prepare( 'SELECT id FROM %i WHERE space_id = %d', Tables::name( 'events' ), (int) $space['id'] ) ); foreach ( (array) $ids as $id ) { $event = self::find( (int) $id ); if ( $event ) { self::delete( $event ); } } } /** * Removes the answers and the events of a deleted member, like their posts. * * @param int $user_id Member. * @return void */ public static function delete_user_data( $user_id ) { global $wpdb; $table = Tables::name( 'event_rsvps' ); $events = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Cleanup. $wpdb->prepare( 'SELECT event_id FROM %i WHERE user_id = %d', $table, absint( $user_id ) ) ); $wpdb->delete( $table, array( 'user_id' => absint( $user_id ) ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table. foreach ( (array) $events as $event_id ) { self::recount( (int) $event_id ); } $own = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Cleanup. $wpdb->prepare( 'SELECT id FROM %i WHERE user_id = %d', Tables::name( 'events' ), absint( $user_id ) ) ); foreach ( (array) $own as $id ) { $event = self::find( (int) $id ); if ( $event ) { self::delete( $event ); } } } /** * Stores the number of answers on the event row. * * @param int $event_id Event ID. * @return void */ private static function recount( $event_id ) { global $wpdb; $table = Tables::name( 'event_rsvps' ); $counts = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Counter refresh after a write. $wpdb->prepare( 'SELECT status, COUNT(id) AS total FROM %i WHERE event_id = %d GROUP BY status', $table, absint( $event_id ) ), ARRAY_A ); $map = wp_list_pluck( (array) $counts, 'total', 'status' ); $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table. Tables::name( 'events' ), array( 'going_count' => (int) ( $map['going'] ?? 0 ), 'maybe_count' => (int) ( $map['maybe'] ?? 0 ), ), array( 'id' => absint( $event_id ) ) ); } /** * Validates event fields. * * @param array $input Raw values. * @param array|null $event Event being changed, or null for a new one. * @return array|WP_Error Columns to save. */ private static function clean( array $input, $event ) { $title = trim( sanitize_text_field( (string) ( $input['title'] ?? ( $event['title'] ?? '' ) ) ) ); if ( '' === $title ) { return self::field_error( 'spacebay_event_title', __( 'Give the event a name.', 'spacebay' ), 'title' ); } if ( mb_strlen( $title ) > 120 ) { return self::field_error( 'spacebay_event_title', __( 'Keep the name under 120 characters.', 'spacebay' ), 'title' ); } $text = array_key_exists( 'description', $input ) ? (string) $input['description'] : (string) ( $event['description'] ?? '' ); if ( mb_strlen( $text ) > self::MAX_DESCRIPTION ) { return self::field_error( 'spacebay_event_description', __( 'The description is too long.', 'spacebay' ), 'description' ); } $starts = self::datetime( $input['starts_at'] ?? null, $event['starts_at'] ?? '' ); if ( '' === $starts ) { return self::field_error( 'spacebay_event_start', __( 'Choose when the event starts.', 'spacebay' ), 'starts_at' ); } $ends = self::datetime( $input['ends_at'] ?? null, $event['ends_at'] ?? '' ); $ends = '' === $ends ? gmdate( 'Y-m-d H:i:s', strtotime( $starts . ' UTC' ) + HOUR_IN_SECONDS ) : $ends; $span = strtotime( $ends . ' UTC' ) - strtotime( $starts . ' UTC' ); if ( $span <= 0 ) { return self::field_error( 'spacebay_event_end', __( 'The event must end after it starts.', 'spacebay' ), 'ends_at' ); } if ( $span > 14 * DAY_IN_SECONDS ) { return self::field_error( 'spacebay_event_end', __( 'An event can last 14 days at most.', 'spacebay' ), 'ends_at' ); } $type = sanitize_key( (string) ( $input['location_type'] ?? ( $event['location_type'] ?? 'online' ) ) ); $type = in_array( $type, array( 'online', 'in_person' ), true ) ? $type : 'online'; $url = array_key_exists( 'online_url', $input ) ? trim( (string) $input['online_url'] ) : (string) ( $event['online_url'] ?? '' ); $url = '' === $url ? '' : Url::clean( $url ); if ( array_key_exists( 'online_url', $input ) && '' !== trim( (string) $input['online_url'] ) && '' === $url ) { return self::field_error( 'spacebay_event_url', __( 'Enter a full link that starts with https://, for example https://meet.example.com/team.', 'spacebay' ), 'online_url' ); } $place = mb_substr( trim( sanitize_text_field( (string) ( $input['location'] ?? ( $event['location'] ?? '' ) ) ) ), 0, 200 ); $rendered = Renderer::render( $text ); $check = $title . "\n" . $rendered['text'] . "\n" . $place; // Events cannot wait for review, so blocked words are refused or hidden like in private messages. if ( WordFilter::find( $check ) ) { if ( 'block' === Settings::get( 'blocked_words_action', 'review' ) || WordFilter::find( $title . "\n" . $place ) ) { return new WP_Error( 'spacebay_blocked_words', __( 'The event contains words that are not allowed here.', 'spacebay' ), array( 'status' => 400 ) ); } $rendered = Renderer::render( WordFilter::mask( $rendered['text'] ) ); } $clean = array( 'title' => $title, 'description' => $rendered['text'], 'description_rendered' => $rendered['html'], 'location_type' => $type, 'location' => 'in_person' === $type ? $place : '', 'online_url' => 'online' === $type ? substr( $url, 0, 255 ) : '', 'starts_at' => $starts, 'ends_at' => $ends, ); /** * Filters the clean values of an event before it is saved. Add-ons add * their own columns here, such as "capacity" (SpaceBay Pro). * * @since 1.0.0 * * @param array $clean Clean values. * @param array $input Raw values. * @param array|null $event Event being changed, null for a new one. */ return apply_filters( 'spacebay_event_clean', $clean, $input, $event ); } /** * An error about one form field. * * @param string $code Error code. * @param string $message Message. * @param string $field Field name. * @return WP_Error */ private static function field_error( $code, $message, $field ) { return new WP_Error( $code, $message, array( 'status' => 400, 'field' => $field, ) ); } /** * Turns an ISO 8601 time into a UTC MySQL datetime. * * @param mixed $value Raw value, or null to keep the fallback. * @param string $fallback Current value. * @return string Empty when not valid. */ private static function datetime( $value, $fallback ) { if ( null === $value || '' === $value ) { return null === $value ? (string) $fallback : ''; } $time = strtotime( (string) $value ); if ( false === $time || $time < strtotime( '2000-01-01' ) || $time > strtotime( '2100-01-01' ) ) { return ''; } return gmdate( 'Y-m-d H:i:s', $time ); } /** * Casts a row. * * @param array $row Row. * @return array */ public static function normalize( array $row ) { foreach ( array( 'id', 'space_id', 'user_id', 'capacity', 'going_count', 'maybe_count', 'reminder_sent' ) as $key ) { $row[ $key ] = (int) $row[ $key ]; } return $row; } }