Skip to content
Merged
275 changes: 274 additions & 1 deletion plugins/hc-custom/includes/bp-event-organiser.php
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,10 @@ function hc_custom_bpeo_group_event_meta_cap( $caps, $cap, $user_id, $args ) {
}

switch ( $cap ) {
case 'read_private_events':
// 'read_private_events' is deliberately not handled here: it is a
// primitive cap with no event context, so group membership cannot be
// evaluated for it. Mapping it to 'exist' (as this once did) handed
// every visitor the ability to list all private events.
case 'read_event':
// we've already parsed this logic in bpeo_map_basic_meta_caps().
if ( 'exist' === $caps[0] ) {
Expand Down Expand Up @@ -549,3 +552,273 @@ function hc_custom_bpeo_activity_action_format_for_groups( $action, $activity )




/** Group calendar scoping *****************************************************/

/**
* Resolve the group a calendar request is being built for.
*
* @param array|null $request Request vars; defaults to $_GET.
* @return int Group ID, or 0 when the request is not for a group.
*/
function hc_custom_bpeo_get_calendar_group_id( $request = null ) {
// On the group page itself BuddyPress knows the group.
if ( function_exists( 'bp_is_group' ) && bp_is_group() ) {
return (int) bp_get_current_group_id();
}

// Inside the eventorganiser-fullcal admin-ajax request BuddyPress has no
// group context (it skips URI parsing for AJAX), so the calendar passes
// the group along explicitly.
if ( null === $request ) {
$request = $_GET; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
}

if ( ! isset( $request['bp_group'] ) || ! is_scalar( $request['bp_group'] ) ) {
return 0;
}

$bp_group = (string) $request['bp_group'];
if ( '' === $bp_group || ! ctype_digit( $bp_group ) ) {
return 0;
}

return (int) $bp_group;
}

/**
* Restrict the Event Organiser calendar query to the current group's events.
*
* @param array $query Query vars as set up by EO.
* @return array
*/
function hc_custom_bpeo_filter_calendar_query_for_group( $query ) {
$group_id = hc_custom_bpeo_get_calendar_group_id();

if ( ! $group_id ) {
return $query;
}

// The admin-ajax endpoint has none of the group page's access control, so
// only honour the scope for groups the caller may see. A denied request
// gets an empty scope, which bpeo's pre_get_posts handler turns into
// post__in => array( 0 ), i.e. no events, rather than the unscoped calendar.
if ( ! hc_custom_bpeo_user_can_view_group_calendar( get_current_user_id(), $group_id ) ) {
$query['bp_group'] = array();
return $query;
}

// Consumed by bpeo's pre_get_posts handling of 'bp_group', which turns it
// into a bpeo_event_group tax query (and admits private events, which is
// how events in non-public groups are stored).
$query['bp_group'] = $group_id;

// Members of the group (and moderators) may see its private events, so for
// them do not let EO's capability-based 'readable' restriction hide those
// on top of the group check. Anyone else viewing a public group keeps the
// role/author check: an event connected to both this public group and a
// private group is a private post and must not leak through here.
if ( current_user_can( 'bp_moderate' ) || groups_is_user_member( get_current_user_id(), $group_id ) ) {
$query['perm'] = '';
} else {
$query['perm'] = 'readable';
}

return $query;
}

/**
* Whether a user may view a group's calendar.
*
* Mirrors the visibility rule bp-event-organiser applies when listing an
* event's connected groups: public groups are open to all; private and hidden
* groups to their members and to community moderators.
*
* @param int $user_id User ID (0 for anonymous).
* @param int $group_id Group ID.
* @return bool
*/
function hc_custom_bpeo_user_can_view_group_calendar( $user_id, $group_id ) {
$group = groups_get_group( array( 'group_id' => (int) $group_id ) );

if ( empty( $group->id ) ) {
return false;
}

if ( 'public' === $group->status ) {
return true;
}

if ( current_user_can( 'bp_moderate' ) ) {
return true;
}

return (bool) groups_is_user_member( (int) $user_id, (int) $group_id );
}
// After bp-event-organiser's own filter (priority 10), which only works when
// bp_is_group() is true and so is a no-op inside admin-ajax.
add_filter( 'eventorganiser_fullcalendar_query', 'hc_custom_bpeo_filter_calendar_query_for_group', 20 );

/**
* Load the script that sends the calendar's group along with its AJAX requests.
*
* Event Organiser enqueues 'eo_front' from wp_footer only when a calendar is on
* the page, so this runs just before footer scripts print and piggybacks on it.
* Hooked to wp_print_footer_scripts rather than wp_footer because the embedded
* group calendar (?embedded=true) strips wp_footer actions.
*/
function hc_custom_bpeo_enqueue_group_calendar_script() {
if ( ! wp_script_is( 'eo_front', 'enqueued' ) ) {
return;
}

$js_path = 'includes/js/bpeo-group-calendar.js';

wp_enqueue_script(
'hc-custom-bpeo-group-calendar',
plugins_url( $js_path, __DIR__ ),
array( 'eo_front' ),
filemtime( trailingslashit( plugin_dir_path( __DIR__ ) ) . $js_path ),
true
);
}
add_action( 'wp_print_footer_scripts', 'hc_custom_bpeo_enqueue_group_calendar_script', 5 );

/**
* Point calendar event links at the group's rendering of the event.
*
* @param string $link Current event permalink.
* @param int $event_id Event post ID.
* @param int $occurrence_id Occurrence ID.
* @return string
*/
function hc_custom_bpeo_filter_calendar_event_link_for_group( $link, $event_id = 0, $occurrence_id = 0 ) {
$group_id = hc_custom_bpeo_get_calendar_group_id();
$event_id = (int) $event_id;

if ( ! $group_id || ! $event_id ) {
return $link;
}

// Only events actually connected to this group have a rendering under it.
$event_groups = array_map( 'intval', (array) bpeo_get_event_groups( $event_id ) );
if ( ! in_array( $group_id, $event_groups, true ) ) {
return $link;
}

$event = get_post( $event_id );
if ( ! $event || empty( $event->post_name ) ) {
return $link;
}

return trailingslashit( bpeo_get_group_permalink( $group_id ) . $event->post_name );
}
// After bp-event-organiser's own filter (priority 10), which only works when
// bp_is_group() is true and so is a no-op inside admin-ajax.
add_filter( 'eventorganiser_calendar_event_link', 'hc_custom_bpeo_filter_calendar_event_link_for_group', 20, 3 );

/** Sitewide calendar: members' private group events ***************************/

/**
* IDs of the groups a user belongs to.
*
* @param int $user_id User ID.
* @return int[]
*/
function hc_custom_bpeo_get_member_group_ids( $user_id ) {
$user_id = (int) $user_id;
if ( ! $user_id ) {
return array();
}

$user_groups = groups_get_user_groups( $user_id );
if ( empty( $user_groups['groups'] ) ) {
return array();
}

return array_values( array_filter( array_map( 'intval', (array) $user_groups['groups'] ) ) );
}

/**
* Let a logged-in member see their private groups' events on the unscoped
* (sitewide) calendar.
*
* @param array $query Query vars as set up by EO.
* @return array
*/
function hc_custom_bpeo_filter_calendar_query_for_member_groups( $query ) {
// Group-scoped (including denied) and member calendars manage their own
// visibility.
if ( isset( $query['bp_group'] ) || isset( $query['bp_displayed_user_id'] ) ) {
return $query;
}

// Callers whose role grants read_private_events already see every private
// event through EO's 'readable' query; the membership-based restriction
// below would only take events away from them.
if ( current_user_can( 'read_private_events' ) ) {
return $query;
}

$group_ids = hc_custom_bpeo_get_member_group_ids( get_current_user_id() );
if ( empty( $group_ids ) ) {
return $query;
}

// EO's 'readable' perm would limit private posts to the caller's own,
// which, now that read_private_events is no longer handed to everyone,
// would hide their groups' events. Admit private posts and let the
// posts_where restriction confine them to the caller's groups, inside
// the main query so it stays bounded by the calendar's date range.
$query['hc_bpeo_member_group_ids'] = $group_ids;
$query['perm'] = '';
$query['post_status'] = array_values( array_unique( array_merge( (array) ( $query['post_status'] ?? array( 'publish' ) ), array( 'private' ) ) ) );

return $query;
}
add_filter( 'eventorganiser_fullcalendar_query', 'hc_custom_bpeo_filter_calendar_query_for_member_groups', 30 );

/**
* Keep private events outside the caller's groups out of the query.
*
* Expressed as a subquery on the bpeo_event_group term relationships rather
* than a materialised ID list, so the cost follows the main query's date
* range, not the groups' full event history.
*
* @param string $where SQL WHERE clause.
* @param WP_Query $wp_query The query.
* @return string
*/
function hc_custom_bpeo_restrict_private_events_where( $where, $wp_query ) {
$group_ids = $wp_query->get( 'hc_bpeo_member_group_ids' );
if ( empty( $group_ids ) ) {
return $where;
}

$group_ids = array_values( array_filter( array_map( 'intval', (array) $group_ids ) ) );
if ( empty( $group_ids ) ) {
return $where;
}

global $wpdb;
$posts = $wpdb->posts;
$user_id = (int) get_current_user_id();

// Term names are 'group_<id>' and built from integers only, so they need
// no further escaping.
$term_names = array();
foreach ( $group_ids as $group_id ) {
$term_names[] = "'group_{$group_id}'";
}
$term_list = implode( ',', $term_names );

$in_member_groups = "SELECT tr.object_id FROM {$wpdb->term_relationships} tr"
. " INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id"
. " INNER JOIN {$wpdb->terms} t ON t.term_id = tt.term_id"
. " WHERE tt.taxonomy = 'bpeo_event_group' AND t.name IN ({$term_list})";

$where .= " AND ( {$posts}.post_status <> 'private' OR {$posts}.post_author = {$user_id} OR {$posts}.ID IN ({$in_member_groups}) )";

return $where;
}
add_filter( 'posts_where', 'hc_custom_bpeo_restrict_private_events_where', 20, 2 );
49 changes: 49 additions & 0 deletions plugins/hc-custom/includes/js/bpeo-group-calendar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Send the group a calendar belongs to along with its event requests.
*
* Event Organiser fetches calendar events via admin-ajax, where BuddyPress has
* no group context. bp-event-organiser already places 'bp_group' in the
* localized calendar settings (eventorganiser.calendars); this forwards it on
* the request so the server can scope the results to the group.
*/
( function ( window ) {
'use strict';

/**
* Map a calendar element ID ("#eo_fullcalendar_N") to its index in
* eventorganiser.calendars.
*/
function calendarIndexFromId( id ) {
var match = /eo_fullcalendar_(\d+)/.exec( String( id || '' ) );
return match ? parseInt( match[ 1 ], 10 ) - 1 : -1;
}

function addGroupToRequest( request, start, end, timezone, options ) {
var calendars = window.eventorganiser && window.eventorganiser.calendars;
var index = calendarIndexFromId( options && options.id );
var calendar = calendars && index >= 0 ? calendars[ index ] : null;

if ( calendar && calendar.bp_group ) {
request.bp_group = calendar.bp_group;
}

return request;
}

var hooks = window.wp && window.wp.hooks;

if ( ! hooks || typeof hooks.addFilter !== 'function' ) {
return;
}

// Event Organiser ships a legacy hooks shim (event-manager.js) with the
// signature addFilter( name, callback, priority ), but defers to WordPress
// core's @wordpress/hooks when that is already on the page, which takes
// addFilter( name, namespace, callback, priority ). Core exposes hasFilter;
// the shim does not.
if ( typeof hooks.hasFilter === 'function' ) {
hooks.addFilter( 'eventorganiser.fullcalendar_request', 'hc-custom/bpeo-group-calendar', addGroupToRequest, 10 );
} else {
hooks.addFilter( 'eventorganiser.fullcalendar_request', addGroupToRequest, 10 );
}
} )( window );
Loading
Loading