diff --git a/admin/em-options.php b/admin/em-options.php index cf1ef99..0d2e2f6 100644 --- a/admin/em-options.php +++ b/admin/em-options.php @@ -19,6 +19,10 @@ function em_options_save(){ //Do nothing, keep old setting. }elseif( ($postKey == 'dbem_category_default_color' || $postKey == 'dbem_tag_default_color') && !sanitize_hex_color($postValue) ){ $EM_Notices->add_error( sprintf(esc_html_x('Colors must be in a valid %s format, such as #FF00EE.', 'hex format', 'events-manager'), 'hex').' '. esc_html__('This setting was not changed.', 'events-manager'), true); + }elseif( $postKey == 'dbem_oauth' && is_array($postValue) ){ + foreach($postValue as $postValue_key=>$postValue_val){ + EM_Options::set($postValue_key, wp_unslash($postValue_val), 'dbem_oauth'); + } }else{ //TODO slashes being added? if( is_array($postValue) ){ @@ -845,7 +849,7 @@ function em_admin_option_box_uninstall(){ - +

diff --git a/admin/settings/tabs/general.php b/admin/settings/tabs/general.php index 87b7ec7..f7ed367 100644 --- a/admin/settings/tabs/general.php +++ b/admin/settings/tabs/general.php @@ -50,23 +50,6 @@ if( get_option('dbem_attributes_enabled') ){ em_options_textarea ( sprintf(__( '%s Attributes', 'events-manager'),__('Event','events-manager')), 'dbem_placeholders_custom', sprintf(__( "You can also add event attributes here, one per line in this format #_ATT{key}. They will not appear on event pages unless you insert them into another template below, but you may want to store extra information about an event for other uses. More information on placeholders.", 'events-manager'), EM_ADMIN_URL .'&page=events-manager-help') ); } - if( get_option('dbem_locations_enabled') ){ - /*default location*/ - if( defined('EM_OPTIMIZE_SETTINGS_PAGE_LOCATIONS') && EM_OPTIMIZE_SETTINGS_PAGE_LOCATIONS ){ - em_options_input_text( __( 'Default Location', 'events-manager'), 'dbem_default_location', __('Please enter your Location ID, or leave blank for no location.','events-manager').' '.__( 'This option allows you to select the default location when adding an event.','events-manager')." ".__('(not applicable with event ownership on presently, coming soon!)','events-manager') ); - }else{ - $location_options = array(); - $location_options[0] = __('no default location','events-manager'); - $EM_Locations = EM_Locations::get(); - foreach($EM_Locations as $EM_Location){ - $location_options[$EM_Location->location_id] = $EM_Location->location_name; - } - em_options_select ( __( 'Default Location', 'events-manager'), 'dbem_default_location', $location_options, __('Please enter your Location ID.','events-manager').' '.__( 'This option allows you to select the default location when adding an event.','events-manager')." ".__('(not applicable with event ownership on presently, coming soon!)','events-manager') ); - } - - /*default location country*/ - em_options_select ( __( 'Default Location Country', 'events-manager'), 'dbem_location_default_country', em_get_countries(__('no default country', 'events-manager')), __('If you select a default country, that will be pre-selected when creating a new location.','events-manager') ); - } ?> @@ -74,17 +57,69 @@ formats.','events-manager') ); - if( get_option('dbem_locations_enabled') ){ - em_options_radio_binary ( __( 'Require locations for events?', 'events-manager'), 'dbem_require_location', __( 'Setting this to no will allow you to submit events without locations. You can use the {no_location}...{/no_location} or {has_location}..{/has_location} conditional placeholder to selectively display location information.','events-manager') ); - em_options_radio_binary ( __( 'Use dropdown for locations?', 'events-manager'), 'dbem_use_select_for_locations', __( 'Select yes to select location from a drop-down menu; location selection will be faster, but you will lose the ability to insert locations with events','events-manager') ); - em_options_radio_binary ( sprintf(__( 'Enable %s attributes?', 'events-manager'),__('location','events-manager')), 'dbem_location_attributes_enabled', __( 'Select yes to enable the attributes feature','events-manager') ); - em_options_radio_binary ( sprintf(__( 'Enable %s custom fields?', 'events-manager'),__('location','events-manager')), 'dbem_cp_locations_custom_fields', __( 'Custom fields are the same as attributes, except you cannot restrict specific values, users can add any kind of custom field name/value pair. Only available in the WordPress admin area.','events-manager') ); - if( get_option('dbem_location_attributes_enabled') ){ - em_options_textarea ( sprintf(__( '%s Attributes', 'events-manager'),__('Location','events-manager')), 'dbem_location_placeholders_custom', sprintf(__( "You can also add location attributes here, one per line in this format #_LATT{key}. They will not appear on location pages unless you insert them into another template below, but you may want to store extra information about an event for other uses. More information on placeholders.", 'events-manager'), EM_ADMIN_URL .'&page=events-manager-help') ); - } - } + em_options_radio_binary ( __( 'Enable locations?', 'events-manager'), 'dbem_locations_enabled', __( 'If you disable locations, bear in mind that you should remove your location page, shortcodes and related placeholders from your formats.','events-manager'), '', '.em-location-type-option' ); ?> + + {no_location}...{/no_location} or {has_location}..{/has_location} conditional placeholder to selectively display location information.','events-manager') ); + ?> + + + + + + $EM_Event_Location_Class): /* @var EM_Event_Locations\Event_Location $EM_Event_Location_Class */ ?> +
+ + +

'.esc_html__('documentation', 'events-manager').''); ?>

+ + + + + + + + + + #_LATT{key}. They will not appear on location pages unless you insert them into another template below, but you may want to store extra information about an event for other uses. More information on placeholders.", 'events-manager'), EM_ADMIN_URL .'&page=events-manager-help') ); + } + /*default location*/ + if( defined('EM_OPTIMIZE_SETTINGS_PAGE_LOCATIONS') && EM_OPTIMIZE_SETTINGS_PAGE_LOCATIONS ){ + em_options_input_text( __( 'Default Location', 'events-manager'), 'dbem_default_location', __('Please enter your Location ID, or leave blank for no location.','events-manager').' '.__( 'This option allows you to select the default location when adding an event.','events-manager')." ".__('(not applicable with event ownership on presently, coming soon!)','events-manager') ); + }else{ + $location_options = array(); + $location_options[0] = __('no default location','events-manager'); + $EM_Locations = EM_Locations::get(); + foreach($EM_Locations as $EM_Location){ + $location_options[$EM_Location->location_id] = $EM_Location->location_name; + } + em_options_select ( __( 'Default Location', 'events-manager'), 'dbem_default_location', $location_options, __('Please enter your Location ID.','events-manager').' '.__( 'This option allows you to select the default location when adding an event.','events-manager')." ".__('(not applicable with event ownership on presently, coming soon!)','events-manager') ); + } + + /*default location country*/ + em_options_select ( __( 'Default Location Country', 'events-manager'), 'dbem_location_default_country', em_get_countries(__('no default country', 'events-manager')), __('If you select a default country, that will be pre-selected when creating a new location.','events-manager') ); + } + ?> + +
+
+
+

diff --git a/buddypress/screens/my-bookings.php b/buddypress/screens/my-bookings.php index 82cef45..e6a94a4 100644 --- a/buddypress/screens/my-bookings.php +++ b/buddypress/screens/my-bookings.php @@ -12,10 +12,6 @@ function bp_em_my_bookings() { bp_notifications_delete_notifications_by_type(get_current_user_id(), 'events','pending_booking'); bp_notifications_delete_notifications_by_type(get_current_user_id(), 'events','confirmed_booking'); bp_notifications_delete_notifications_by_type(get_current_user_id(), 'events','cancelled_booking'); - }else{ - bp_core_delete_notifications_by_type(get_current_user_id(), 'events','pending_booking'); - bp_core_delete_notifications_by_type(get_current_user_id(), 'events','confirmed_booking'); - bp_core_delete_notifications_by_type(get_current_user_id(), 'events','cancelled_booking'); } em_load_event(); diff --git a/classes/em-admin-notices.php b/classes/em-admin-notices.php index cfadf6f..aa06dee 100644 --- a/classes/em-admin-notices.php +++ b/classes/em-admin-notices.php @@ -200,4 +200,5 @@ public static function admin_footer(){ booking_meta['lang']) ){ return $this->booking_meta['lang']; } + }elseif( $var == 'booking_status' ){ + return ($this->booking_status == 0 && !get_option('dbem_bookings_approval') ) ? 1:$this->booking_status; } return null; } @@ -261,14 +264,14 @@ function save($mail = true){ $this->email(); } $this->compat_keys(); - return apply_filters('em_booking_save', ( count($this->errors) == 0 ), $this); + return apply_filters('em_booking_save', ( count($this->errors) == 0 ), $this, $update); }else{ $this->feedback_message = __('There was a problem saving the booking.', 'events-manager'); if( !$this->can_manage() ){ $this->add_error(sprintf(__('You cannot manage this %s.', 'events-manager'),__('Booking','events-manager'))); } } - return apply_filters('em_booking_save', false, $this); + return apply_filters('em_booking_save', false, $this, false); } /** @@ -899,11 +902,12 @@ function delete(){ $this->booking_status = false; $this->feedback_message = sprintf(__('%s deleted', 'events-manager'), __('Booking','events-manager')); $wpdb->delete( EM_META_TABLE, array('meta_key'=>'booking-note', 'object_id' => $this->booking_id), array('%s','%d')); + do_action('em_booking_deleted', $this); }else{ $this->add_error(sprintf(__('%s could not be deleted', 'events-manager'), __('Booking','events-manager'))); } } - do_action('em_bookings_deleted', $result, array($this->booking_id)); + do_action('em_bookings_deleted', $result, array($this->booking_id), $this); return apply_filters('em_booking_delete',( $result !== false ), $this); } diff --git a/classes/em-datetime.php b/classes/em-datetime.php index 4e25564..50aad75 100644 --- a/classes/em-datetime.php +++ b/classes/em-datetime.php @@ -180,6 +180,8 @@ public function i18n( $format = 'Y-m-d H:i:s' ){ $timestamp = parent::format('U'); $server_offset = date('Z', $timestamp); return date_i18n( $format, $timestamp - ($server_offset * 2) + $this->getOffset() ); + }elseif( function_exists('wp_date') ){ + return wp_date( $format, $this->getTimestamp(), $this->getTimezone() ); }else{ return date_i18n( $format, $this->getTimestampWithOffset(true) ); } diff --git a/classes/em-event-post-admin.php b/classes/em-event-post-admin.php index 9ad70cc..2338c04 100755 --- a/classes/em-event-post-admin.php +++ b/classes/em-event-post-admin.php @@ -270,6 +270,7 @@ public static function meta_boxes( $post ){ add_meta_box('em-event-anonymous', __('Anonymous Submitter Info','events-manager'), array('EM_Event_Post_Admin','meta_box_anonymous'),EM_POST_TYPE_EVENT, 'side','high'); } add_meta_box('em-event-when', __('When','events-manager'), array('EM_Event_Post_Admin','meta_box_date'),EM_POST_TYPE_EVENT, 'side','high'); + // GGG add_meta_box('em-event-when-ec-rsvp', __('RSVP Date','events-manager'), array('EM_Event_Post_Admin','meta_box_ec_rsvp_date'),EM_POST_TYPE_EVENT, 'side','high'); // GGG if(get_option('dbem_locations_enabled', true)){ add_meta_box('em-event-where', __('Where','events-manager'), array('EM_Event_Post_Admin','meta_box_location'),EM_POST_TYPE_EVENT, 'normal','high'); diff --git a/classes/em-event-post.php b/classes/em-event-post.php index 7cfae01..88a205c 100755 --- a/classes/em-event-post.php +++ b/classes/em-event-post.php @@ -405,7 +405,7 @@ public static function parse_query(){ } // END else { - $wp_query->query_vars['orderby'] = sanitize_key($_REQUEST['orderby']); + $wp_query->query_vars['orderby'] = sanitize_key($_REQUEST['orderby']); } }else{ $wp_query->query_vars['orderby'] = 'meta_value'; diff --git a/classes/em-event-posts-admin.php b/classes/em-event-posts-admin.php index 19d3cf0..15ab7fb 100755 --- a/classes/em-event-posts-admin.php +++ b/classes/em-event-posts-admin.php @@ -212,7 +212,7 @@ public static function columns_output( $column ) { case 'location': //get meta value to see if post has location, otherwise $EM_Location = $EM_Event->get_location(); - if( !empty($EM_Location->location_id) ){ + if( $EM_Event->has_location() ){ $actions = array(); $actions[] = "". esc_html__('View') .""; if( $EM_Location->can_manage('edit_locations', 'edit_others_locations') ) { @@ -221,6 +221,8 @@ public static function columns_output( $column ) { echo "" . $EM_Location->location_name . ""; echo " - ". implode(' | ', $actions) . ""; echo "
" . $EM_Location->location_address . " - " . $EM_Location->location_town; + }elseif( $EM_Event->has_event_location() ) { + echo $EM_Event->get_event_location()->get_admin_column(); }else{ echo __('None','events-manager'); } diff --git a/classes/em-event.php b/classes/em-event.php index bc03fb6..5670b8e 100644 --- a/classes/em-event.php +++ b/classes/em-event.php @@ -124,6 +124,14 @@ class EM_Event extends EM_Object{ var $event_spaces; var $event_private; var $location_id; + /** + * Key name of event location type associated to this event. + * + * Events can have an event-specific location type, such as a url, webinar or another custom type instead of a regular geographical location. If this value is set, then a registered event location type is loaded and relevant saved event meta is used. + * + * @var string + */ + public $event_location_type; var $recurrence_id; var $event_status; var $blog_id = 0; @@ -178,6 +186,7 @@ class EM_Event extends EM_Object{ 'event_rsvp_spaces' => array( 'name'=>'rsvp_spaces', 'type'=>'%d', 'null'=>true ), 'event_spaces' => array( 'name'=>'spaces', 'type'=>'%d', 'null'=>true), 'location_id' => array( 'name'=>'location_id', 'type'=>'%d', 'null'=>true ), + 'event_location_type' => array( 'type'=>'%s', 'null'=>true ), 'recurrence_id' => array( 'name'=>'recurrence_id', 'type'=>'%d', 'null'=>true ), 'event_status' => array( 'name'=>'status', 'type'=>'%d', 'null'=>true ), 'event_private' => array( 'name'=>'status', 'type'=>'%d', 'null'=>true ), @@ -233,6 +242,10 @@ class EM_Event extends EM_Object{ * @var EM_Location */ var $location; + /** + * @var array + */ + var $event_location_data = array(); /** * @var EM_Bookings */ @@ -497,11 +510,12 @@ function load_postdata($event_post, $search_by = false){ //load meta data and other related information if( $event_post->post_status != 'auto-draft' ){ $event_meta = $this->get_event_meta($search_by); + if( !empty($even_meta['_event_location_type']) ) $this->event_location_type = $even_meta['_event_location_type']; //load this directly so we know further down whether this has an event location type to load //Get custom fields and post meta foreach($event_meta as $event_meta_key => $event_meta_val){ $field_name = substr($event_meta_key, 1); if($event_meta_key[0] != '_'){ - $this->event_attributes[$event_meta_key] = ( is_array($event_meta_val) ) ? $event_meta_val[0]:$event_meta_val; + $this->event_attributes[$event_meta_key] = ( is_array($event_meta_val) ) ? $event_meta_val[0]:$event_meta_val; }elseif( is_string($field_name) && !in_array($field_name, $this->post_fields) ){ if( array_key_exists($field_name, $this->fields) ){ $this->$field_name = $event_meta_val[0]; @@ -510,6 +524,7 @@ function load_postdata($event_post, $search_by = false){ } } } + if( $this->has_event_location() ) $this->get_event_location()->load_postdata($event_meta); //quick compatability fix in case _event_id isn't loaded or somehow got erased in post meta if( empty($this->event_id) && !$this->is_recurring() ){ global $wpdb; @@ -661,17 +676,42 @@ function get_post_meta(){ //reset start and end objects so they are recreated with the new dates/times if and when needed $this->start = $this->end = null; - //Get Location info - if( !get_option('dbem_locations_enabled') || (!empty($_POST['no_location']) && !get_option('dbem_require_location',true)) || (empty($_POST['location_id']) && !get_option('dbem_require_location',true) && get_option('dbem_use_select_for_locations')) ){ - $this->location_id = 0; - }elseif( !empty($_POST['location_id']) && is_numeric($_POST['location_id']) ){ - $this->location_id = absint($_POST['location_id']); + //Get Location Info + if( get_option('dbem_locations_enabled') ){ + // determine location type, with backward compatibility considerations for those overriding the location forms + $location_type = isset($_POST['location_type']) ? sanitize_key($_POST['location_type']) : 'location'; + if( !empty($_POST['no_location']) ) $location_type = 0; //backwards compat + if( $location_type == 'location' && empty($_POST['location_id']) && get_option('dbem_use_select_for_locations')) $location_type = 0; //backward compat + // assign location data + if( $location_type === 0 || $location_type === '0' ){ + // no location + $this->location_id = 0; + $this->event_location_type = null; + }elseif( $location_type == 'location' && EM_Locations::is_enabled() ){ + // a physical location, old school + $this->event_location_type = null; // if location resides in locations table, location type is null since we have a location_id table value + if( !empty($_POST['location_id']) && is_numeric($_POST['location_id']) ){ + // we're using a previously created location + $this->location_id = absint($_POST['location_id']); + }else{ + $this->location_id = null; + //we're adding a new location place, so create an empty location and populate + $this->get_location()->get_post(false); + $this->get_location()->post_content = ''; //reset post content, as it'll grab the event description otherwise + } + }else{ + // we're dealing with an event location such as a url or webinar + $this->location_id = null; // no location ID + $this->event_location_type = $location_type; + if( EM_Event_Locations\Event_Locations::is_enabled($location_type) ){ + $this->get_event_location()->get_post(); + } + } }else{ - //we're adding a new location, so create an empty location and populate - $this->location_id = null; - $this->get_location()->get_post(false); - $this->get_location()->post_content = ''; //reset post content, as it'll grab the event description otherwise + $this->location_id = 0; + $this->event_location_type = null; } + // GGG: RSVP Info (for ECs) if( empty($_POST['event_ec_rsvp_date']) ){ // If it's empty, default to the event start date @@ -724,7 +764,7 @@ function get_post_meta(){ if(get_option('dbem_attributes_enabled')){ global $allowedtags; if( !is_array($this->event_attributes) ){ $this->event_attributes = array(); } - $event_available_attributes = em_get_attributes(); + $event_available_attributes = !empty($event_available_attributes) ? $event_available_attributes : em_get_attributes(); //we use this in locations, no need to repeat if needed if( !empty($_POST['em_attributes']) && is_array($_POST['em_attributes']) ){ foreach($_POST['em_attributes'] as $att_key => $att_value ){ if( (in_array($att_key, $event_available_attributes['names']) || array_key_exists($att_key, $this->event_attributes) ) ){ @@ -921,11 +961,22 @@ function validate_meta(){ $this->add_error(__('RSVP Date cannot be after the event start.','events-manager')); } } - if( get_option('dbem_locations_enabled') && empty($this->location_id) ){ //location ids don't need validating as we're not saving a location - if( get_option('dbem_require_location',true) || $this->location_id !== 0 ){ - if( !$this->get_location()->validate() ){ + if( get_option('dbem_locations_enabled') ){ + if( $this->location_id === 0 && get_option('dbem_require_location',true) ){ + // no location chosen, yet we require a location + $this->add_error(__('No location associated with this event.', 'events-manager')); + }elseif( $this->has_location() ){ + // physical location + if( empty($this->location_id) && !$this->get_location()->validate() ){ + // new location doesn't validate $this->add_error($this->get_location()->get_errors()); + }elseif( !empty($this->location_id) && !$this->get_location()->location_id ){ + // non-existent location selected + $this->add_error( __('Please select a valid location.', 'events-manager') ); } + }elseif( $this->has_event_location() ){ + // event location, validation applies errors directly to $this + $this->get_event_location()->validate(); } } if ( count($missing_fields) > 0){ @@ -1109,6 +1160,12 @@ function save_meta(){ } } } + //update event location via post meta + if( $this->has_event_location() ){ + $this->get_event_location()->save(); + }else{ + $this->get_event_location()->reset_data(); + } //update timestamps, dates and times update_post_meta($this->post_id, '_event_start_local', $this->start()->getDateTime()); update_post_meta($this->post_id, '_event_end_local', $this->end()->getDateTime()); @@ -1303,9 +1360,10 @@ function duplicate_url($raw = false){ /** * Delete whole event, including bookings, tickets, etc. + * @param boolean $force_delete * @return boolean */ - function delete($force_delete = false){ //atm wp seems to force cp deletions anyway + function delete( $force_delete = false ){ if( $this->can_manage('delete_events', 'delete_others_events') ){ if( !is_admin() ){ include_once('em-event-post-admin.php'); @@ -1627,7 +1685,7 @@ public function get_parent(){ } /** - * Returns the location object this event belongs to. + * Returns the physical location object this event belongs to. * @return EM_Location */ function get_location() { @@ -1642,6 +1700,40 @@ function get_location() { return $this->location; } + /** + * Returns whether this event has a phyisical location assigned to it. + * @return bool + */ + public function has_location(){ + return !empty($this->location_id) || (!empty($this->location) && !empty($this->location->location_name)); + } + + /** + * Gets the event's event location (note, different from a regular event location, which uses get_location()) + * Returns implementation of Event_Location or false if no event location assigned. + * @return EM_Event_Locations\URL|EM_Event_Locations\Event_Location|false + */ + public function get_event_location(){ + if( $this->has_event_location() ){ + $EM_Location_Type = EM_Event_Locations\Event_Locations::get( $this->event_location_type, $this ); + }else{ + $EM_Location_Type = new EM_Event_Locations\Event_Location( $this ); + } + return apply_filters('em_event_get_event_location', $EM_Location_Type, $this); + } + + /** + * Returns whether the event has an event location associated with it (different from a physical location). If supplied, can check against a specific type. + * @param string $event_location_type + * @return bool + */ + public function has_event_location( $event_location_type = null ){ + if( !empty($event_location_type) ){ + return !empty($this->event_location_type) && $this->event_location_type === $event_location_type; + } + return !empty($this->event_location_type); + } + /** * Returns the location object this event belongs to. * @return EM_Person @@ -1944,10 +2036,28 @@ function output($format, $target="html") { $show_condition = (!$this->event_rsvp && get_option('dbem_rsvp_enabled')); }elseif ($condition == 'no_location'){ //does this event have a valid location? - $show_condition = ( empty($this->location_id) || !$this->get_location()->location_status ); + $show_condition = !$this->has_event_location() && !$this->has_location(); }elseif ($condition == 'has_location'){ //does this event have a valid location? - $show_condition = ( !empty($this->location_id) && $this->get_location()->location_status ); + $show_condition = ( $this->has_location() && $this->get_location()->location_status ) || $this->has_event_location(); + }elseif ($condition == 'has_location_venue'){ + //does this event have a valid physical location? + $show_condition = ( $this->has_location() && $this->get_location()->location_status ) || $this->has_event_location(); + }elseif ($condition == 'no_location_venue'){ + //does this event NOT have a valid physical location? + $show_condition = !$this->has_location(); + }elseif ($condition == 'has_event_location'){ + //does this event have a valid event location? + $show_condition = $this->has_event_location(); + }elseif ( preg_match('/^has_event_location_([a-zA-Z0-9_\-]+)$/', $condition, $type_match)){ + //event has a specific category + $show_condition = $this->has_event_location($type_match[1]); + }elseif ($condition == 'no_event_location'){ + //does this event not have a valid event location? + $show_condition = !$this->has_event_location(); + }elseif ( preg_match('/^no_event_location_([a-zA-Z0-9_\-]+)$/', $condition, $type_match)){ + //does this event NOT have a specific event location? + $show_condition = !$this->has_event_location($type_match[1]); }elseif ($condition == 'has_image'){ //does this event have an image? $show_condition = ( $this->get_image_url() != '' ); @@ -2085,6 +2195,12 @@ function output($format, $target="html") { }elseif ( preg_match('/^no_tag_([a-zA-Z0-9_\-,]+)$/', $condition, $tag_match)){ //event doesn't have this tag $show_condition = !has_term(explode(',', $tag_match[1]), EM_TAXONOMY_TAG, $this->post_id); + }elseif ( preg_match('/^has_att_([a-zA-Z0-9_\-,]+)$/', $condition, $att_match)){ + //event has a specific custom field + $show_condition = !empty($this->event_attributes[$att_match[1]]) || !empty($this->event_attributes[str_replace('_', ' ', $att_match[1])]); + }elseif ( preg_match('/^no_att_([a-zA-Z0-9_\-,]+)$/', $condition, $att_match)){ + //event has a specific custom field + $show_condition = empty($this->event_attributes[$att_match[1]]) && empty($this->event_attributes[str_replace('_', ' ', $att_match[1])]); } //other potential ones - has_attribute_... no_attribute_... has_categories_... $show_condition = apply_filters('em_event_output_show_condition', $show_condition, $condition, $conditionals[0][$key], $this); @@ -2504,6 +2620,14 @@ function output($format, $target="html") { $replace = '0'; } break; + //Event location (not physical location) + case '#_EVENTLOCATION': + if( !empty($placeholders[3][$key]) ){ + $replace = $this->get_event_location()->output($placeholders[3][$key]); + }else{ + $replace = $this->get_event_location()->output(); + } + break; // GGG case "#_RSVPDATE": $replace_format = em_get_date_format(); diff --git a/classes/em-events.php b/classes/em-events.php index 4fba8cd..7a77eb3 100644 --- a/classes/em-events.php +++ b/classes/em-events.php @@ -1,4 +1,5 @@ false, //search events with a location 'no_location' => false, //search events without a location - 'location_status' => false //search events with locations of a specific publish status + 'location_status' => false, //search events with locations of a specific publish status + 'event_location_type' => false, + 'has_event_location' => false, ); //sort out whether defaults were supplied or just the array of search values if( empty($array) ){ diff --git a/classes/em-exception.php b/classes/em-exception.php new file mode 100644 index 0000000..cb0eb36 --- /dev/null +++ b/classes/em-exception.php @@ -0,0 +1,107 @@ +error_messages = $error; + $message = $this->get_message(); + }elseif( is_wp_error($error) ){ /* @var WP_Error $error */ + $this->wp_error = $error; + $code = $error->get_error_code(); + $message = $error->get_error_message(); + }else{ + $message = $error; + } + if( !is_numeric($code) ){ + $this->error_code = $code; + $code = 0; + } + parent::__construct($message, $code, $previous); + } + + /** + * Returns either a string code reference, or a regular Exception code number. + * @return int|string + */ + public function get_error_code(){ + if( $this->error_code ){ + return $this->error_code; + } + return $this->getCode(); + } + + /** + * Provides a paragraph-formatted message which may contain multiple paragraphs for multiple errors. + * @return string + */ + public function get_message(){ + if( $this->is_wp_error() ){ + $message = '

' . implode('

', $this->wp_error->get_error_messages()) . '

'; + }elseif( !empty($this->error_messages) ){ + $message = '

' . implode('

', $this->error_messages) . '

'; + }else{ + $message = '

' . $this->getMessage() . '

'; + } + return $message; + } + + /** + * @return array|string + */ + public function get_messages(){ + if( $this->is_wp_error() ){ + return $this->wp_error->get_error_messages(); + }elseif( !empty($this->error_messages) ){ + return $this->error_messages; + }else{ + return array($this->getMessage()); + } + } + + /** + * Whether or not this exception was triggered by a WP_Error + * @return bool + */ + public function is_wp_error(){ + return is_wp_error( $this->wp_error ); + } + + /** + * Returns exception in WP_Error format, whether or not it was originally a WP_Error in the first place. + * @return WP_Error + */ + public function get_wp_error(){ + if( $this->is_wp_error() ){ + return $this->wp_error; + } + $WP_Error = new WP_Error(); + $WP_Error->add_data( $this->get_messages(), $this->getCode() ); + return $WP_Error; + } + } +} \ No newline at end of file diff --git a/classes/em-locations.php b/classes/em-locations.php index 29af972..99ca309 100644 --- a/classes/em-locations.php +++ b/classes/em-locations.php @@ -22,6 +22,16 @@ class EM_Locations extends EM_Object { protected static $context = 'location'; + + /** + * Returns whether or not locations are enabled for use with events. + * @return bool + */ + public static function is_enabled(){ + $location_types = get_option('dbem_location_types', array()); + return !empty($location_types['location']); + } + /** * Returns an array of EM_Location objects * @param array $args diff --git a/classes/em-oauth/oauth-admin-settings.php b/classes/em-oauth/oauth-admin-settings.php new file mode 100644 index 0000000..af965e2 --- /dev/null +++ b/classes/em-oauth/oauth-admin-settings.php @@ -0,0 +1,186 @@ + +
+

+
+ +

+

'. $api::get_service_url() .''); ?>

+

$callback_url") ?>

+

+ + +
+ +
+
+ get_oauth_url(); + //we don't need to verify connections at this point, we just need to know if there are any + if( $api_client->authorization_scope == 'user' ){ + $user_id = get_current_user_id(); + $access_tokens = $api::get_user_tokens(); + }else{ + $user_id = null; + $access_tokens = $api::get_site_tokens(); + } + $oauth_accounts = array(); + $connected = $reconnect_required = false; + foreach( $access_tokens as $account_id => $oauth_account ){ + try { + $api_client->load_token( $account_id, $user_id ); + $verification = true; + } catch ( EM_Exception $e ) { + $verification = false; + } + $oauth_account['id'] = !empty($oauth_account['email']) ? $oauth_account['email'] : $account_id; + $disconnect_url_args = array( 'action' => 'em_oauth_'. $api::get_option_name(), 'callback' => 'disconnect', 'account' => $account_id, 'nonce' => wp_create_nonce('em-oauth-'. $option_name .'-disconnect-'.$account_id) ); + $oauth_account['disconnect'] = add_query_arg( $disconnect_url_args, admin_url( 'admin-ajax.php' ) ); + if( !$verification ){ + $oauth_account['reconnect'] = true; + $reconnect_required = true; + }else{ + $connected = true; + } + $oauth_accounts[] = $oauth_account; + } + if( $connected ){ + $button_url = add_query_arg( array( 'action' => 'em_oauth_'. $option_name, 'callback' => 'disconnect', 'nonce' => wp_create_nonce('em-oauth-'. $option_name .'-disconnect') ), admin_url( 'admin-ajax.php' ) ); + $button_text = count($oauth_accounts) > 1 ? __('Disconnect All', 'events-manager') : __('Disonnect', 'events-manager'); + $button_class = 'button-secondary'; + }else{ + $button_url = $oauth_url; + $button_text = __('Connect', 'events-manager'); + $button_class = 'button-primary'; + } + } + ?> +
+ +

+ +

+ + +

+
    + + + +
+

+ + + + +

+ +

+ +

+ + +
+ +
+

get_message(); ?>

+
+ SomeService_API_Client, this will be automatically deduced. + * @var OAuth_API + */ + protected static $api_class; + /** + * @var string + */ + public $id; + /** + * @var string + */ + public $secret; + /** + * @var string + */ + public $scope; + /** + * @var + */ + public $client; + + /** + * @var OAuth_API_Token + */ + public $token; + /** + * @var string + */ + public $user_id; + /** + * @var bool + */ + public $authorized = false; + + /** + * The URL without trailing slash for the API base URL, to which endpoints can be appended to. + * @var string + */ + public $api_base = 'https://api.oauth.com'; + /** + * The URL that'll be used to request an authorization code from the user. Can include strings CLIENT_ID, ACCESS_SCOPE, REDIRECT_URI, and STATE which will be replaced dynamically. + * @var string + */ + public $oauth_authorize_url = 'https://api.oauth.com/authorize'; + /** + * Required by child class unless it overrides the request_access_token() method. + * @var string + */ + public $oauth_request_token_url = 'https://api.oauth.com/token'; + /** + * Defaults to $oauuth_request_token_url if not set. + * @var string + */ + public $oauth_refresh_token_url = null; + /** + * Required by child class unless it overrides the verify_access_token() method. + * @var string + */ + public $oauth_verification_url = 'https://api.oauth.com/verify_token'; + /** + * Required by child class unless it overrides the revoke_access_token() method. + * @var string + */ + public $oauth_revoke_url = 'https://api.oauth.com/revoke'; + public $oauth_authentication = 'parameters'; + /** + * Whether or not an OAuth Service should pass on the state param for security check + * @var bool + */ + public $oauth_state = true; + + /** + * OAuth_API_Client constructor. + * + * @throws EM_Exception + */ + public function __construct(){ + // check credentials + $creds = array( + 'id' => EM_Options::get( $this->option_name. '_app_id', '', $this->option_dataset), + 'secret' => EM_Options::get( $this->option_name. '_app_secret', '', $this->option_dataset) + ); + foreach( array('id', 'secret', 'scope') as $k ){ + if( !empty($creds[$k]) ){ + $this->$k = $creds[$k]; + }elseif( empty($this->$k) ) { // constructors can be overriden to add any of the above + throw new EM_Exception( __('OAuth application information incomplete.', 'events-manager') ); + } + } + if( !$this->oauth_refresh_token_url ){ + $this->oauth_refresh_token_url = $this->oauth_request_token_url; + } + } + + /** + * Shortcut for base class properties. + * @param $name + * @return mixed + */ + public function __get( $name ){ + $api = static::get_api_class(); + if( $name == 'option_name' ){ + return $api::get_option_name(); + }elseif( $name == 'option_dataset' ){ + return $api::get_option_dataset(); + }elseif( $name == 'authorization_scope' ){ + return $api::get_authorization_scope(); + }elseif( $name == 'multiple_tokens' ){ + return $api::supports_multiple_tokens(); + }elseif( $name == 'token_class' ){ + return $api::get_token_class(); + } + return null; + } + + /** + * @return OAuth_API + */ + public static function get_api_class(){ + // set default API class name if not defined by parent + if( self::$api_class === static::$api_class && class_exists(str_replace('_Client', '', get_called_class())) ){ + static::$api_class = str_replace('_Client', '', get_called_class()); + } + return static::$api_class; + } + + /** + * @return mixed + */ + public function get_oauth_url(){ + $scope = is_array($this->scope) ? urlencode(implode('+', $this->scope)) : $this->scope; + $state_nonce = wp_create_nonce($this->option_name.'_authorize'); + $replacements = array( urlencode($this->id), $scope, urlencode(static::get_oauth_callback_url()), $state_nonce ); + $return = str_replace( array('CLIENT_ID','ACCESS_SCOPE','REDIRECT_URI', 'STATE'), $replacements, $this->oauth_authorize_url ); + if( $this->oauth_state ){ + // check there's a STATE value in the authorize url, otherwise add it proactively + if( !preg_match('/STATE/', $this->oauth_authorize_url) ){ + $return = add_query_arg('state', $state_nonce, $return); + } + } + return $return; + } + + /** + * @return string + */ + public static function get_oauth_callback_url(){ + $redirect_base_uri = defined('OAUTH_REDIRECT') ? OAUTH_REDIRECT : admin_url('admin-ajax.php'); // you can completely replace the oauth redirect link for testing locally via a proxy for example + if( defined('EM_OAUTH_TUNNEL') ){ // for local development or other reasons, you can replace the domain with a tunnel domain, which should be with http(s):// included + $redirect_base_uri = str_replace(get_home_url(), EM_OAUTH_TUNNEL, $redirect_base_uri); + } + $api = static::get_api_class(); + $callback_action = 'em_oauth_'. $api::get_option_name(); + return add_query_arg(array('action'=>strtolower($callback_action), 'callback'=>'authorize'), $redirect_base_uri); + } + + /** + * Returns a native client for this service, in the event we want to load an SDK provided by the service. + * @return stdClass + */ + public function client(){ + return new stdClass(); + } + + // GET, POST, PUT, PATCH, DELETE functions + + /** + * @param $endpoint + * @param array $request_args + * @param bool $json_decode + * @return array + * @throws EM_Exception + */ + public function http_request( $endpoint, array $request_args = array(), $json_decode = true ){ + // clean up whether endpoint or full URL is provided + $endpoint = str_replace($this->api_base, '', $endpoint); + $request_url = $this->api_base. $endpoint; + //$request_url = add_query_arg('access_token', $this->token->access_token, $request_url); + // add oauth and method heaeders + if( empty($request_args['headers']) ) $request_args['headers'] = array(); + $request_args['headers']['authorization'] = 'Bearer '.$this->token->access_token; + $request_args['method'] = in_array($request_args['method'], array('GET','POST','PUT','PATCH','DELETE')) ? $request_args['method'] : 'GET'; + // prepare JSON format if sending via that content type + if( !empty($request_args['headers']['Content-Type']) && $request_args['headers']['Content-Type'] == 'application/json' ){ + if( !empty($request_args['body']) && (is_array($request_args['body']) || is_object($request_args['body'])) ){ + $request_args['body'] = json_encode($request_args['body']); + } + } + // request and parse + $response = wp_remote_request($request_url, $request_args); /* @var \Requests_Response_Headers $response['headers'] */ + if( is_wp_error($response) ){ + throw new EM_Exception($response); + }elseif( $response['response']['code'] >= 300 ){ //anything not 20x will indicate an issue + $errors = json_decode($response['body']); + if( is_array($errors) ){ + $error = current($errors); + }elseif( !empty($errors->code) ){ + $error = $errors; + }else{ + $error = (object) array('code' => $response['response']['code'], 'message' => $response['body']); + } + throw new EM_Exception($error->message, $error->code); + } + if( $json_decode ){ + $response['body'] = json_decode($response['body']); + } + return $response; + } + + /** + * Fetches event data from the given endpoint with supplied arguments according to Meetup API v3 + * + * @param string $endpoint Full URL or endpoint accepted. + * @param array $args + * @param array $request_args + * @return array + * @throws EM_Exception + */ + public function get($endpoint, array $args = array(), array $request_args = array() ){ + $request_url = add_query_arg( $args, $this->api_base.$endpoint ); + $request_args['method'] = 'GET'; + return static::http_request( $request_url, $request_args ); + } + + /** + * @param $endpoint + * @param array $vars + * @param array $request_args + * @param bool $json Shorthand for setting Content-Type in headers to application/json + * @return array|mixed + * @throws EM_Exception + */ + public function post($endpoint, array $vars = array(), array $request_args = array(), $json = false ){ + $request_args['body'] = $vars; + $request_args = array_merge(array('method' => 'POST'), $request_args); + if( $json ){ + if( empty($request_args['headers'])) $request_args['headers'] = array(); + $request_args['headers']['Content-Type'] = 'application/json'; + } + return static::http_request($endpoint, $request_args, $json); + } + + /** + * @param $endpoint + * @param array $vars + * @param array $request_args + * @param bool $json + * @return array|mixed + * @throws EM_Exception + */ + public function patch($endpoint, array $vars = array(), array $request_args = array(), $json = false ){ + $request_args['method'] = 'PATCH'; + return static::post($endpoint, $vars, $request_args, $json); + } + + /** + * @param $endpoint + * @param array $vars + * @param array $request_args + * @param bool $json + * @return array|mixed + * @throws EM_Exception + */ + public function put($endpoint, array $vars = array(), array $request_args = array(), $json = false ){ + $request_args['method'] = 'PUT'; + return static::post($endpoint, $vars, $request_args, $json); + } + + /** + * @param $endpoint + * @param array $args + * @param array $request_args + * @return array|mixed + * @throws EM_Exception + */ + public function delete($endpoint, array $args = array(), array $request_args = array() ){ + $request_url = add_query_arg( $args, $this->api_base.$endpoint ); + $request_args['method'] = 'DELETE'; + return static::http_request( $request_url, $request_args ); + } + + // Baseic OAuth interaction functions, loading a token into client as well as requesting, refreshing, verifying and revoking tokens. + + /** + * @param int $user_id + * @param int $account_id + * @throws EM_Exception + */ + public function load_token( $account_id = null, $user_id = null ){ + if( $this->authorization_scope !== 'user' ) $user_id = null; // user id is not relevant + // return value if already authorized + if( $this->authorized && $this->authorized = $user_id.'|'.$account_id && $this->user_id == $user_id && $this->token->id == $account_id) return; + // not authorized, re/load token + $this->authorized = $this->token = false; + $this->user_id = $user_id; + // get token information from user account + $this->get_access_token( $account_id ); + // renew token if expired + if ( $this->token->is_expired() ) { + // Refresh the token if it's expired and update WP user meta. + $this->refresh(); + }else{ + $this->authorized = $user_id .'|'. $this->token->id; + } + } + + /** + * Requests an access token from the supplied authorization code. The access token is further verified and populated with service account meta. + * If successful, token and meta information is saved for the user $user_id or current user if not specified. + * Throws an EM_Exception if unsuccessful at any stage in this process. + * + * @var string $code + * @var int $user_id + * @throws EM_Exception + */ + public function request($code, $user_id = null ){ + if( $this->authorization_scope == 'user' ){ + $this->user_id = empty($user_id) ? get_current_user_id() : $user_id; // used in $this->save_access_token() + }else{ + $this->user_id = null; + } + $access_token = $this->request_access_token($code); + $this->token = new OAuth_API_Token($access_token); + if( $this->token->refresh_token === true ) $this->token->refresh_token = false; // if no token was provided, we may be able to obtain it here, otherwise validation will fail upon refresh. + // verify the access token so we can establish the id of this account and then save it to user profile + $access_token_meta = $this->verify_access_token(); + // now, check for previous tokens and save to it instead of overwriting (we do this in case ppl reauthorize the same account and get a new token with no refresh_token) + if( empty($this->token->id) ){ + $token = $this->token; + try{ + $this->get_access_token($access_token_meta['id']); + $this->token->refresh( $token->to_array() ); // merge in new token info to old token + } catch ( EM_Exception $ex ){ + $this->token = $token; // revert back to new token + } + } + // refresh current or new token with the meta info and save + $this->token->refresh( $access_token_meta ); + $this->save_access_token(); + } + + /** + * @throws EM_Exception + */ + public function refresh(){ + if ( $this->token->refresh_token ) { + try{ + $access_token = $this->refresh_access_token(); + $this->token->refresh($access_token, true); + $this->save_access_token(); + $this->authorized = $this->user_id .'|'. $this->token->id; + }catch( EM_Exception $ex ){ + throw new EM_Exception( array( + $this->option_name.'-error' => sprintf(esc_html__( 'There was an error connecting to %s: %s', 'events-manager' ), $this->service_name, "{$ex->getMessage()}"), + $this->option_name.'-token-expired' => $this->reauthorize_error_string() + )); + } + }else{ + throw new EM_Exception( $this->reauthorize_error_string() ); + } + } + + /** + * Verify the token for this client by obtaining meta data of the account associated to this token and saving it to the current token. + * @var boolean $update_token + * @return boolean + * @throws EM_Exception + */ + public function verify( $update_token = true ){ + $access_token_meta = $this->verify_access_token(); + // refresh current or new token with the meta info and save + $updated = $this->token->refresh( $access_token_meta ); + if( $updated && $update_token ) $this->save_access_token(); + return true; // if we get here, verification passed. + } + + /** + * @return boolean + * @throws EM_Exception + */ + public function revoke(){ + return $this->revoke_access_token(); + } + + /* START OVERRIDABLE FUNCTIONS - THESE FUNCTIONS COULD BE OVERRIDDEN TO SPECIFICALLY DEAL WITH PARTICULAR OAUTH PROVIDERS */ + + /** + * Specific function which requests the access token from the API Service and returns the access token array, an error array if service replies. + * Throws an EM_Exception if there are any other connection issues. + * + * @var string $code + * @return array + * @throws EM_Exception + */ + public function request_access_token( $code ){ + $args = array( + 'body' => array( + 'client_id' => $this->id, + 'grant_type' => 'authorization_code', + 'redirect_uri' => static::get_oauth_callback_url(), + 'code' => $code + ) + ); + return $this->oauth_request( 'post', $this->oauth_request_token_url, $args ); + } + + /** + * @return array + * @throws EM_Exception + */ + public function refresh_access_token(){ + $args = array( + 'body' => array( + 'grant_type' => 'refresh_token', + 'refresh_token' => $this->token->refresh_token, + ) + ); + return $this->oauth_request('post', $this->oauth_refresh_token_url, $args); + } + + /** + * Verifies an access token by obtaining further meta data about the account associated with that token. + * Expected return is an associative array containing the id (service account id the token belongs to), name, photo and email (optional). + * + * @return array + * @throws EM_Exception + */ + public function verify_access_token(){ + $request_url = str_replace('ACCESS_TOKEN', $this->token->access_token, $this->oauth_verification_url); + $access_token = $this->oauth_request('get', $request_url); + return $access_token; // we may want to override this depending on what's returned + } + + /** + * @return bool + * @throws EM_Exception + */ + public function revoke_access_token(){ + if( empty($this->oauth_revoke_url) ) return false; + $request_url = str_replace('ACCESS_TOKEN', $this->token->access_token, $this->oauth_revoke_url); + return $this->oauth_request('get', $request_url); // we may want to override this depending on what's returned + } + + /** + * @param string $method + * @param $request_url + * @param array $args + * @return mixed + * @throws EM_Exception + */ + public function oauth_request($method, $request_url, $args = array() ){ + $args = array_merge( array('headers' => array(), 'body' => array()), $args ); + if( $this->oauth_authentication == 'basic' ){ + $args['headers']['authorization'] = 'Basic '.base64_encode($this->id.':'.$this->secret); + } + if( $method === 'get'){ + if( $this->oauth_authentication == 'parameters' ){ + // add client params to URL if using get + $request_url = add_query_arg( array('client_id' => $this->id), $request_url ); + } + $response = wp_remote_get($request_url, $args); + }elseif( $method === 'post' ){ + if( $this->oauth_authentication == 'parameters' ){ + // add auth params to body if using post + $args['body']['client_id'] = $this->id; + $args['body']['client_secret'] = $this->secret; + } + if( empty($args['Content-Type'])){ + // by defaulut post will send this content type + $args['headers']['Content-Type'] = 'application/x-www-form-urlencoded'; + } + $response = wp_remote_post($request_url, $args); + }else{ + throw new EM_Exception('Unknown request method.'); + } + if( is_wp_error($response) ){ + throw new EM_Exception($response->get_error_messages()); + }elseif( $response['response']['code'] != '200' ){ + $errors = json_decode($response['body']); + $error = current($errors); + if( !empty($error->message) ){ + $message = $error->message; + }elseif( !empty($error->error) ){ + $message = $error->error; + }elseif( !empty($error->reason) ){ + $message = $error->reason; + }elseif( is_string($error) ){ + $message = $error; + }elseif( is_string($errors) ){ + $message = $errors; + }else{ + $message = var_export($errors); + } + $error_code = !empty($error->code) ? $error->code : 'oauth-error'; + throw new EM_Exception($message, $error_code); + } + return json_decode($response['body'], true); // we may want to override this depending on what's returned + } + + /* END OVERRIDABLE FUNCTIONS */ + + /** + * @param int $api_user_id + * @return string + */ + public function reauthorize_error_string($api_user_id = 0 ){ + $settings_page_url = ''. esc_html__('settings page', 'events-manager-google').''; + if( !$api_user_id && !empty($this->token->id) ){ + $api_user_id = !empty($this->token->email) ? $this->token->email : $this->token->id; + } + if( $api_user_id ){ + return sprintf(__('You need to reauthorize access to account %s by visiting the %s page.', 'events-manager-google'), $api_user_id, $settings_page_url); + } + return sprintf(__('You need to authorize access to your %s account by visiting the %s page.', 'events-manager-google'), $this->service_name, $settings_page_url); + } + + /** + * Gets an access token for a specific account, or provides first account user has available, if any. If no access token is available, an EM_Exception is thrown. + * + * @param int $api_user_id The ID (e.g. number or email) of the OAuth account + * @return OAuth_API_Token + * @throws EM_Exception + */ + public function get_access_token( $api_user_id = 0 ){ + if( $this->authorization_scope == 'site' ){ + $site_tokens = EM_Options::get($this->option_name.'_token', array(), $this->option_dataset); + if( !empty($site_tokens) ){ + if( $this->multiple_tokens && !empty($api_user_id) && !empty($site_tokens[$api_user_id]) ){ + $token_data = $site_tokens[$api_user_id]; + $token_data['id'] = $api_user_id; + }else{ + $token_data = current($site_tokens); + $token_data['id'] = key($site_tokens); + } + } + }elseif( $this->authorization_scope == 'user' ){ + $user_tokens = get_user_meta( $this->user_id, $this->option_dataset.'_'.$this->option_name, true ); + if( !empty($user_tokens) ){ + if( $api_user_id ){ + if( !empty($user_tokens[$api_user_id]) ){ + $token_data = $user_tokens[$api_user_id]; + $token_data['id'] = $api_user_id; + } + }elseif( !empty($user_tokens) ){ + $token_data = current($user_tokens); + $token_data['id'] = key($user_tokens); + } + } + } + if( empty($token_data) ) throw new EM_Exception( $this->reauthorize_error_string($api_user_id) ); + $this->token = new $this->token_class($token_data); + return $this->token; + } + + /** + * Sets the access token to the user meta storage where all connected accounts for the user of that token are stored. + */ + public function save_access_token(){ + if( $this->authorization_scope == 'site' ){ + $token = $this->token->to_array(); + if( $this->multiple_tokens ){ + $site_tokens = EM_Options::get($this->option_name.'_token', array(), $this->option_dataset); + } + if( empty($site_tokens) ) $site_tokens = array(); + $site_tokens[$this->token->id] = $token; + EM_Options::set($this->option_name.'_token', $site_tokens, $this->option_dataset); + }elseif( $this->authorization_scope == 'user' ){ + if( $this->multiple_tokens ){ + $user_tokens = get_user_meta($this->user_id, $this->option_dataset.'_'.$this->option_name, true); + } + if( empty($user_tokens) ) $user_tokens = array(); + $token = $this->token->to_array(); + $user_tokens[$this->token->id] = $token; + update_user_meta($this->user_id, $this->option_dataset.'_'.$this->option_name, $user_tokens); + } + } +} \ No newline at end of file diff --git a/classes/em-oauth/oauth-api-token.php b/classes/em-oauth/oauth-api-token.php new file mode 100644 index 0000000..833320f --- /dev/null +++ b/classes/em-oauth/oauth-api-token.php @@ -0,0 +1,88 @@ +refresh($token); + if( empty($token['created']) ) $this->created = time(); + } + + /** + * @param array $token + * @return boolean $updated + * @throws EM_Exception + */ + public function refresh( $token, $reset = false ){ + $updated = false; + // reset values + if( $reset ){ + $this->expires_in = $this->expires_at = $this->created = 0; + $this->access_token = $this->refresh_token = $this->token_type = ''; + } + // add new values + foreach( $token as $k => $v ){ + if( empty($this->$k) || $this->$k != $token[$k] ){ + $this->$k = $token[$k]; + $updated = true; + } + } + // set values that may not have been added + if( empty($this->id) && !empty($this->email) ) $this->id = $this->email; + if( !$this->created ) $this->created = time(); + // set expires_at, which is what we'll use for expiry checking + if( $this->expires_at ){ + $this->expires_in = $this->expires_at - time(); + }elseif( $this->created && $this->expires_in ){ + $this->expires_at = $this->expires_in + $this->created; + }else{ + $this->expires_in = $this->expires_at = time(); + } + $this->verify(); + return $updated; + } + + /** + * @throws EM_Exception + */ + public function verify(){ + $missing = array(); + foreach( array('access_token', 'expires_at') as $k ){ + if( empty($this->$k) ) $missing[] = $k; + } + if( !empty($missing) ) throw new EM_Exception( sprintf(__('Involid token credentials, the folloiwng are missing: %s.', 'events-manager'), implode(', ', $missing)) ); + } + + public function is_expired(){ + return $this->expires_at < time(); + } + + public function to_array(){ + $array = array(); + $ignore = array('id'); + foreach( get_object_vars($this) as $k => $v ){ + if( !in_array($k, $ignore) && !empty($this->$k) ) $array[$k] = $this->$k; + } + return $array; + } +} \ No newline at end of file diff --git a/classes/em-oauth/oauth-api.php b/classes/em-oauth/oauth-api.php new file mode 100644 index 0000000..14a7ae7 --- /dev/null +++ b/classes/em-oauth/oauth-api.php @@ -0,0 +1,251 @@ +load_token( $user_id, $api_user_id ); + } + return $client; + } + + public static function get_user_tokens( $user_id = false ){ + if( static::$authorization_scope !== 'user' ) return array(); + if( empty($user_id) ) $user_id = get_current_user_id(); + $user_tokens = get_user_meta( $user_id, static::$option_dataset.'_'.static::$option_name, true ); + if( empty($user_tokens) ) $user_tokens = array(); + return $user_tokens; + } + + /** + * @return array[OAuth_API_Token] + */ + public static function get_site_tokens(){ + if( static::$authorization_scope !== 'site' ) return array(); + $site_tokens = EM_Options::get(static::$option_name.'_token', array(), static::$option_dataset); + if( empty($site_tokens) ) $site_tokens = array(); + return $site_tokens; + } + + /** + * Includes and calls the code required to handle a callback from FB to store user auth token. + */ + public static function oauth_authorize() { + global $EM_Notices; + if( !empty($EM_Notices) ) $EM_Notices = new EM_Notices(); + if( !empty($_REQUEST['code']) ){ + try{ + $client = static::get_client(false); + if( !$client->oauth_state || (!empty($_REQUEST['state']) && !wp_verify_nonce( $_REQUEST['state'], static::$option_name.'_authorize')) ){ + $EM_Notices->add_error( sprintf( esc_html__( 'There was an error connecting to %s: %s', 'events-manager' ), static::$service_name, 'No State Provided'), true ); + }else{ + try { + $client->request( $_REQUEST['code'] ); + $EM_Notices->add_confirm( sprintf( esc_html__( 'Your account has been successfully connected with %s!', 'events-manager' ), static::$service_name ), true); + } catch ( EM_Exception $e ){ + $EM_Notices->add_error( sprintf( esc_html__( 'There was an error connecting to %s: %s', 'events-manager' ), static::$service_name, ''.$e->getMessage().'' ), true ); + } + } + } catch ( EM_Exception $ex ){ + $EM_Notices->add_error($ex->get_messages(), true); + } + }else{ + $EM_Notices->add_error( sprintf( esc_html__( 'There was an error connecting to %s: %s', 'events-manager' ), static::$service_name, 'No Authorization Code Provided'), true ); + } + // Redirect to settings page + $query_args = array( 'page' => 'events-manager-options' ); + $url = add_query_arg( $query_args, admin_url( 'admin.php' ) ); + wp_redirect( $url ); + die(); + } + + /** + * Handles disconnecting a user from one or all their connected Google accounts, attempting to revoke their key in the process. + */ + public static function oauth_disconnect(){ + global $EM_Notices; + if( !empty($EM_Notices) ) $EM_Notices = new EM_Notices(); + + if( static::$authorization_scope == 'user' ){ + $account_tokens = static::get_user_tokens(); + }else{ + $account_tokens = static::get_site_tokens(); + } + $accounts_to_disconnect = array(); + if( empty($_REQUEST['user']) && !empty($_REQUEST['nonce']) && wp_verify_nonce($_REQUEST['nonce'], 'em-oauth-'. static::$option_name .'-disconnect') ){ + $accounts_to_disconnect = array_keys($account_tokens); + }elseif( !empty($_REQUEST['account']) && !empty($_REQUEST['nonce']) && wp_verify_nonce($_REQUEST['nonce'], 'em-oauth-'. static::$option_name .'-disconnect-'.$_REQUEST['account']) ){ + if( !empty($account_tokens[$_REQUEST['account']]) ){ + $accounts_to_disconnect[] = $_REQUEST['account']; + } + }else{ + $EM_Notices->add_error('Missing nonce, please contact your administrator.', true); + } + if( !empty($accounts_to_disconnect) ){ + $errors = $disconnected_accounts = array(); + foreach( $accounts_to_disconnect as $account_id ){ + try{ + $client = static::get_client( get_current_user_id(), $account_id); + $client->revoke(); + } catch ( EM_Exception $ex ){ + $account_name = !empty( $client->token->email ) ? $client->token->email : $client->token->name; + $errors[] = "$account_name - " . $ex->getMessage(); + } finally{ + $disconnected_accounts[] = $account_id; + unset($account_tokens[$account_id]); + } + } + if( !empty($disconnected_accounts) ){ + if( static::$authorization_scope == 'user' ){ + if( empty($account_tokens) ){ + delete_user_meta( get_current_user_id(), 'em_oauth_'.static::$option_name ); + }else{ + update_user_meta( get_current_user_id(), 'em_oauth_'.static::$option_name, $account_tokens ); + } + }else{ + EM_Options::set(static::$option_name.'_token', $account_tokens, static::$option_dataset); + } + $success = _n('You have successfully disconnected from your %s account.', 'You have successfully disconnected from your %s accounts.', count($accounts_to_disconnect), 'events-manager'); + $EM_Notices->add_confirm(sprintf($success, static::$service_name), true); + } + if( !empty($errors) ){ + $error_msg = sprintf( esc_html__('There were some issues whilst disconnecting from your %s account(s) :', 'events-manager'), static::$service_name ); + array_unshift( $errors, $error_msg ); + $EM_Notices->add_error( $errors, true ); + } + } + + // Redirect to settings page + $query_args = array( 'page' => 'events-manager-options' ); + $url = add_query_arg( $query_args, admin_url( 'admin.php' ) ); + wp_redirect( $url ); + die(); + } +} +//include dependents +include('oauth-api-token.php'); +include('oauth-api-client.php'); +if( is_admin() ){ + include('oauth-admin-settings.php'); +} \ No newline at end of file diff --git a/classes/em-object.php b/classes/em-object.php index c53a482..7d7f161 100644 --- a/classes/em-object.php +++ b/classes/em-object.php @@ -634,7 +634,7 @@ public static function get_taxonomies(){ //non default taxonomy, so create new item for the taxonomies array $tax_name = str_replace('-','_',$tax_name); $prefix = !array_key_exists($tax_name, $taxonomies_array) ? '':'post_'; - if( is_array($tax->object_type) ){ + if( is_array($tax->object_type) && !empty($tax->rewrite) ){ if( $event_tax || $loc_tax ){ $taxonomies_array[$prefix.$tax_name] = array('name'=>$tax_name, 'context'=>array(), 'slug'=> $tax->rewrite['slug'], 'query_var'=> $tax->query_var ); } @@ -1028,6 +1028,7 @@ public static function get_post_search($args = array(), $filter = false, $reques if( empty($request) ) $request = $_REQUEST; if( !empty($request['em_search']) && empty($args['search']) ) $request['search'] = $request['em_search']; //em_search is included to circumvent wp search GET/POST clashes $accepted_searches = !empty($accepted_searches) ? $accepted_searches : self::get_default_search(); + $accepted_searches = array_diff($accepted_searches, array('format', 'format_header', 'format_footer')); $accepted_searches = apply_filters('em_accepted_searches', $accepted_searches, $args); //merge variables from the $request into $args foreach($request as $post_key => $post_value){ diff --git a/classes/event-locations/em-event-location-url.php b/classes/event-locations/em-event-location-url.php new file mode 100644 index 0000000..106b8ab --- /dev/null +++ b/classes/event-locations/em-event-location-url.php @@ -0,0 +1,72 @@ +event->event_location_data['url'] = esc_url_raw($_POST['event_location_url']); + } + if( !empty($_POST['event_location_url_text']) ){ + $this->event->event_location_data['text'] = sanitize_text_field($_POST['event_location_url_text']); + } + return $return; + } + + public function validate(){ + $result = false; + if( empty($this->event->event_location_data['url']) ){ + $this->event->add_error( __('Please enter a valid URL for this event location.', 'events-manager') ); + $result = false; + } + if( empty($this->event->event_location_data['text']) ){ + $this->event->add_error( __('Please provide some link text for this event location URL.', 'events-manager') ); + $result = false; + } + return $result; + } + + public function get_link( $new_target = true ){ + return ''. esc_html($this->text).''; + } + + public function get_admin_column() { + return ''. static::get_label() . ' - ' . $this->get_link().''; + } + + public static function get_label( $label_type = 'singular' ){ + switch( $label_type ){ + case 'plural': + return esc_html__('URLs', 'events-manager'); + break; + case 'singular': + return esc_html__('URL', 'events-manager'); + break; + } + return parent::get_label($label_type); + } + + public function output( $what = null ){ + if( $what === null ){ + return ''. esc_html($this->text) .''; + }elseif( $what === '_self' ){ + return ''. esc_html($this->text) .''; + }elseif( $what === '_parent' || $what === '_top' ){ + return ''. esc_html($this->text) .''; + }else{ + return parent::output($what); + } + } +} +URL::init(); \ No newline at end of file diff --git a/classes/event-locations/em-event-location.php b/classes/event-locations/em-event-location.php new file mode 100644 index 0000000..08e5d46 --- /dev/null +++ b/classes/event-locations/em-event-location.php @@ -0,0 +1,167 @@ +event = $EM_Event; + } + } + + /** + * @param $name + * @return string|null + */ + public function __get( $name ) { + if( $name == 'type' ){ + return static::$type; + }elseif( in_array($name, $this->properties) ){ + return isset($this->event->event_location_data[$name]) ? $this->event->event_location_data[$name] : null; + } + return null; + } + + public function __set($name, $value) { + if( in_array($name, $this->properties) ){ + $this->event->event_location_data[$name] = $value; + } + } + + public function __isset($name) { + if( in_array($name, $this->properties) ){ + return isset($this->event->event_location_data[$name]); + } + return false; + } + + public function load_postdata( $event_meta = array() ){ + if( empty($event_meta) ) $event_meta = $this->event->get_event_meta(); + $base_key = '_event_location_'.static::$type; + foreach( $event_meta as $event_meta_key => $event_meta_val ){ + if( $event_meta_key == $base_key ){ + $this->event->event_location_data[static::$type] = ( is_array($event_meta_val) ) ? $event_meta_val[0]:$event_meta_val; + $this->event->event_location_data[static::$type] = maybe_unserialize($this->event->event_location_data[static::$type]); + }elseif( substr($event_meta_key, 0, strlen($base_key) ) == $base_key ){ + //event location data is placed directly into the event_location_data array and referenced via get_event_location() + $key = str_replace('_event_location_'.static::$type.'_', '', $event_meta_key); + $this->event->event_location_data[$key] = ( is_array($event_meta_val) ) ? $event_meta_val[0]:$event_meta_val; + $this->event->event_location_data[$key] = maybe_unserialize($this->event->event_location_data[$key]); + } + } + } + + /** + * @param array $post + * @return boolean + */ + public function get_post(){ + $this->event->event_location_data = array(); + return true; + } + + /** + * @return boolean + */ + public function validate(){ + return false; + } + + public function save(){ + if( is_numeric($this->event->post_id) && $this->event->post_id > 0 && static::$type !== null ){ + $this->reset_data(); + foreach( $this->event->event_location_data as $prop => $value ){ + $meta_key = $prop == static::$type ? '_event_location_'.$prop : '_event_location_'.static::$type.'_'.$prop; + if( $value !== null ){ + update_post_meta( $this->event->post_id, $meta_key, $value ); + }else{ + delete_post_meta( $this->event->post_id, $meta_key ); + } + } + } + return true; + } + + final function reset_data( $preserve_type_data = false ){ + if( is_numeric($this->event->post_id) && $this->event->post_id > 0 ){ + global $wpdb; + $result = $wpdb->query( $wpdb->prepare('DELETE FROM '.$wpdb->postmeta." WHERE post_id=%d AND meta_key LIKE '_event_location_%' AND meta_key != '_event_location_type'", $this->event->post_id) ); + wp_cache_delete( $this->event->post_id, 'post_meta' ); //refresh cache to prevent looking at old data + return $result; + } + return false; + } + + public function get_admin_column(){ + return $this->get_label('singular'); + } + + /** + * Returns whether or not this event location is enabled for use. + * @return bool + */ + public static function is_enabled(){ + $location_types = get_option('dbem_location_types', array()); + return !empty($location_types[static::$type]); + } + + /** + * Loads admin template automatically if static $admin_template is set to a valid path in templates folder. + * Classes with custom forms outside of template folders can override this function and provide their own HTML that will go in the loop of event location type forms. + */ + public static function load_admin_template(){ + if( static::$admin_template ){ + em_locate_template( static::$admin_template, true ); + } + } + + public static function get_label( $label_type = 'siingular' ){ + //override and return plural name. + return static::$type; + } + + public function output( $what = null ){ + if( $what !== null && $what !== 'type' ){ + return esc_html($this->$what); + }else{ + return static::get_label(); + } + } +} + +//include default Event Locations +include('em-event-location-url.php'); \ No newline at end of file diff --git a/classes/event-locations/em-event-locations.php b/classes/event-locations/em-event-locations.php new file mode 100644 index 0000000..6bb734a --- /dev/null +++ b/classes/event-locations/em-event-locations.php @@ -0,0 +1,68 @@ + class name. + * @var array + */ + private static $types = array(); + + /** + * @return array[EM_Event_Location::] + */ + public static function get_types(){ + return static::$types; + } + + /** + * @param string $type + * @param string $classname + * @return bool + */ + public static function register( $type, $classname ){ + self::$types[$type] = $classname; + return true; + } + + /** + * @param string $type + * @return bool + */ + public static function unregister( $type ){ + if( !empty(self::$types[$type]) ){ + unset(self::$types[$type]); + return true; + } + return false; + } + + /** + * @param string $type + * @param \EM_Event $EM_Event + * @return bool + */ + public static function get( $type = null, $EM_Event = null ){ + if( !empty(self::$types[$type]) && class_exists(self::$types[$type]) ){ + $location_type = self::$types[$type]; + return new $location_type( $EM_Event ); + } + return false; + } + + /** + * Returns whether or not the supplied event location $type is enabled for use. + * @param string $type + * @return bool + */ + public static function is_enabled( $type ){ + $location_types = get_option('dbem_location_types', array()); + return !empty($location_types[$type]); + } + +} +require('em-event-location.php'); \ No newline at end of file diff --git a/em-events.php b/em-events.php index fc1164c..f0c3797 100644 --- a/em-events.php +++ b/em-events.php @@ -245,7 +245,26 @@ function em_content_wp_title($title, $sep = '', $seplocation = ''){ return $title; } add_filter ( 'wp_title', 'em_content_wp_title',100,3 ); //override other plugin SEO due to way EM works. -add_filter( 'wpseo_title', 'em_content_wp_title', 100, 3 ); //WP SEO friendly + +/** + * Yoast SEO friendly short circuit, fixes issues in Yoast 14 update by changing the $sep function into the actual separator. + * @param $title + * @param string|mixed $sep + * @param string $seplocation + * @return string + */ +function em_content_wpseo_title($title, $sep = '', $seplocation = ''){ + if( class_exists('WPSEO_Utils') && method_exists('WPSEO_Utils', 'get_title_separator') ){ + $sep = WPSEO_Utils::get_title_separator(); + }elseif( !is_string( $sep ) ){ + $sep = ''; + } + if( !is_string($seplocation) ){ + $seplocation = ''; + } + return em_content_wp_title( $title, $sep, $seplocation = '' ); +} +add_filter( 'wpseo_title', 'em_content_wpseo_title', 100, 2 ); //WP SEO friendly /** * Makes sure we're in "THE Loop", which is determinied by a flag set when the_post() (start) is first called, and when have_posts() (end) returns false. diff --git a/em-functions.php b/em-functions.php index 30b9508..7569a76 100644 --- a/em-functions.php +++ b/em-functions.php @@ -653,11 +653,16 @@ function em_checkbox_items($name, $array, $saved_values, $horizontal = true) { } function em_options_input_text($title, $name, $description ='', $default='') { $translate = EM_ML::is_option_translatable($name); + if( preg_match('/^(.+)\[(.+)?\]$/', $name, $matches) ){ + $value = EM_Options::get($matches[2], $default, $matches[1]); + }else{ + $value = get_option($name, $default); + } ?>
- +
'; + $default_lang = ''; } } echo '
'; diff --git a/em-install.php b/em-install.php index 96319aa..ef23d75 100644 --- a/em-install.php +++ b/em-install.php @@ -157,6 +157,7 @@ function em_create_events_table() { event_spaces int(5) NULL DEFAULT 0, event_private tinyint(1) unsigned NOT NULL DEFAULT 0, location_id bigint(20) unsigned NULL DEFAULT NULL, + event_location_type VARCHAR(15) NULL DEFAULT NULL, recurrence_id bigint(20) unsigned NULL DEFAULT NULL, event_date_created datetime NULL DEFAULT NULL, event_date_modified datetime NULL DEFAULT NULL, @@ -755,6 +756,7 @@ function em_add_options() { 'dbem_timezone_default' => EM_DateTimeZone::create()->getName(), 'dbem_require_location' => 0, 'dbem_locations_enabled' => 1, + 'dbem_location_types' => array('location' => 1, 'url' => 1), 'dbem_use_select_for_locations' => 0, 'dbem_attributes_enabled' => 1, 'dbem_recurrence_enabled'=> 1, @@ -1202,6 +1204,13 @@ function em_upgrade_current_installation(){ update_option('dbem_smtp_encryption', 0); } } + if( get_option('dbem_version') != '' && get_option('dbem_version') < 5.975 ){ + update_option('dbem_location_types', array('location'=>1)); + $message = esc_html__('Events Manager has introduced location types, which can include online locations such as a URL or integrations with webinar platforms such as Zoom! Enable different location types in your settings page, for more information see our %s.', 'events-manager'); + $message = sprintf( $message, ''. esc_html__('documentation', 'events-manager')).''; + $EM_Admin_Notice = new EM_Admin_Notice(array( 'name' => 'location-types-update', 'who' => 'admin', 'where' => 'all', 'message' => "$message" )); + EM_Admin_Notices::add($EM_Admin_Notice, is_multisite()); + } // GGG if( get_option('dbem_version') != '' && get_option('dbem_version') < 5.951 ){ em_create_costumes_table(); diff --git a/events-manager.php b/events-manager.php index 49cb657..bc1d23a 100644 --- a/events-manager.php +++ b/events-manager.php @@ -1,7 +1,7 @@ getVcsApi()->enableReleaseAssets(); // Setting constants -define('EM_VERSION', 5.9731); //self expanatory +define('EM_VERSION', 5.9810); //self expanatory define('EM_PRO_MIN_VERSION', 2.6712); //self expanatory define('EM_PRO_MIN_VERSION_CRITICAL', 2.377); //self expanatory define('EM_DIR', dirname( __FILE__ )); //an absolute path to this directory @@ -78,6 +78,7 @@ function dbem_debug_mode(){ // INCLUDES //Base classes +include('classes/em-exception.php'); include('classes/em-options.php'); include('classes/em-object.php'); include('classes/em-datetime.php'); @@ -111,6 +112,7 @@ function dbem_debug_mode(){ include('classes/em-categories.php'); include('classes/em-categories-frontend.php'); include('classes/em-event.php'); +include('classes/event-locations/em-event-locations.php'); include('classes/em-event-post.php'); include('classes/em-events.php'); include('classes/em-location.php'); @@ -130,7 +132,6 @@ function dbem_debug_mode(){ include('classes/em-tickets.php'); //Admin Files if( is_admin() ){ - include('classes/em-admin-notice.php'); include('classes/em-admin-notices.php'); include('admin/em-admin.php'); include('admin/em-bookings.php'); @@ -203,6 +204,22 @@ function bp_em_init() { define("EM_IMAGE_DS",'/'); } +/** + * Provides a way to proactively load groups of files, once, when needed. + * @since 5.9.7.4 + */ +class EM_Loader { + public static $oauth = false; + + public static function oauth(){ + require_once('classes/em-oauth/oauth-api.php'); + add_action('em_enqueue_admin_styles', function(){ + wp_enqueue_style('events-manager-oauth-admin', plugins_url('includes/css/events-manager-oauth-admin.css',__FILE__), array(), EM_VERSION); + }); + self::$oauth = true; + } +} + /** * @author marcus * Contains functions for loading styles on both admin and public sides. @@ -529,7 +546,8 @@ function em_load_event(){ if( preg_match('/\-([0-9]+)$/', $_REQUEST['event_slug'], $matches) ){ $event_id = $matches[1]; }else{ - $event_id = $wpdb->get_var('SELECT event_id FROM '.EM_EVENTS_TABLE." WHERE event_slug='{$_REQUEST['event_slug']}' AND blog_id!=".get_current_blog_id()); + $query = $wpdb->prepare('SELECT event_id FROM '.EM_EVENTS_TABLE.' WHERE event_slug = %s AND blog_id != %d', $_REQUEST['event_slug'], get_current_blog_id()); + $event_id = $wpdb->get_var($query); } $EM_Event = em_get_event($event_id); } @@ -544,7 +562,8 @@ function em_load_event(){ if( preg_match('/\-([0-9]+)$/', $_REQUEST['location_slug'], $matches) ){ $location_id = $matches[1]; }else{ - $location_id = $wpdb->get_var('SELECT location_id FROM '.EM_LOCATIONS_TABLE." WHERE location_slug='{$_REQUEST['location_slug']}' AND blog_id!=".get_current_blog_id()); + $query = $wpdb->prepare('SELECT location_id FROM '.EM_LOCATIONS_TABLE." WHERE location_slug = %s AND blog_id != %d", $_REQUEST['location_slug'], get_current_blog_id()); + $location_id = $wpdb->get_var($query); } $EM_Location = em_get_location($location_id); } diff --git a/includes/css/events-manager-oauth-admin.css b/includes/css/events-manager-oauth-admin.css new file mode 100644 index 0000000..b332a35 --- /dev/null +++ b/includes/css/events-manager-oauth-admin.css @@ -0,0 +1,16 @@ +/* Settings Page */ +#em-options-form .postbox.em-oaut-connect-user .em-oauth-service-info { max-width:1000px; min-height: 75px; } +#em-options-form .postbox.em-oaut-connect-user .em-oauth-service-logo { float:left; width:50px; height:50px; } + +#em-options-form .postbox.em-oaut-connect li.em-oauth-service-account { clear:both; height:16px; padding-bottom:10px; } +#em-options-form .postbox.em-oaut-connect li.em-oauth-service-account img, #em-options-form .postbox.em-oaut-connect li.em-oauth-service-account > div { float:left; } +#em-options-form .postbox.em-oaut-connect li.em-oauth-service-account > div { padding: 5px 0 5px 8px; } +#em-options-form .postbox.em-oaut-connect li.em-oauth-account-disconnected img, #em-options-form .postbox.em-oaut-connect li.em-oauth-account-disconnected .em-oauth-account-label { opacity:0.5; } +#em-options-form .postbox.em-oaut-connect li.em-oauth-account-disconnected .dashicons { width:16px; height: 16px; font-size: 16px; } + +@media screen and (max-width: 600px) { + #em-options-form .postbox.em-oaut-connect .em-oauth-service-logo { margin:0px 10px 10px 0px; } + #em-options-form .postbox.em-oaut-connect .em-oauth-connect-button { margin: 10px 0px 10px 10px; } + #em-options-form .postbox.em-oaut-connect .em-oauth-service-info h2 { margin: 0px 0px 5px; padding:0px 5px; } + #em-options-form .postbox.em-oaut-connect .em-oauth-service-info { clear:both; margin:0px; } +} \ No newline at end of file diff --git a/includes/css/events_manager.css b/includes/css/events_manager.css index cbacd15..79173a4 100644 --- a/includes/css/events_manager.css +++ b/includes/css/events_manager.css @@ -161,6 +161,15 @@ div#em-loading { position:absolute; width:100%; height:100%; background:#FFFFFF /* The editor */ #wp-em-editor-content-wrap table { margin-bottom:0px; } /* Location form */ + #event-form .em-location-type { border-top: 1px solid #dedede; margin-top:20px; padding-top:20px; } + #event-form .em-location-type.em-location-type-single { border:none; } + #event-form .em-location-type p:first-child { margin-top:0; } + #event-form div.em-location-data table { float:left; margin:0px 15px 0px 0px; } + #event-form .em-event-location-data h4 { padding-bottom:5px; border-bottom:1px solid #dedede; margin-bottom:15px; } + #event-form .em-input-field { margin-bottom:10px; } + #event-form .em-input-field label { display: block; margin-bottom:5px; } + #event-form .em-input-field.em-input-field-boolean label { display: inline-block; } + #event-form .em-input-field em { display: block; margin-top:2px; } #em-location-data table.em-location-data td, #em-location-data table.em-location-data th { vertical-align:top; border:none; } #em-location-data table.em-location-data select { width:100%; } #em-location-data table.em-location-data { width:50%; float:left; border:none; } diff --git a/includes/css/events_manager_admin.css b/includes/css/events_manager_admin.css index 4a4c571..a7ae1fd 100644 --- a/includes/css/events_manager_admin.css +++ b/includes/css/events_manager_admin.css @@ -32,10 +32,20 @@ table.events-table .category { color:#888; } #event-form .recurrence-reschedule-warning, #post .recurrence-reschedule-warning { margin-bottom:25px; color:#c45500; } #event-form .recurrence-reschedule-warning p, #post .recurrence-reschedule-warning p { margin-top:0px; font-size:14px; } /*Locations*/ -div.em-location-data table { float:left; margin:0px 15px 0px 0px; } +.em-location-type { border-top: 1px solid #dedede; margin-top:20px; padding-top:20px; } +.em-location-type.em-location-type-single { border:none; margin-top:10px; padding-top:10px; } +.em-location-type p:first-child { margin-top:0; } +div.em-location-data table { float:left; margin:0 15px 0 0; } +.em-event-location-data h4 { padding-bottom:5px; border-bottom:1px solid #dedede; margin-bottom:15px; } +.em-input-field { margin-bottom:10px; } +.em-input-field label { display: block; margin-bottom:5px; } +.em-input-field.em-input-field-boolean label { display: inline-block; } +.em-input-field em { display: block; margin-top:2px; } /* Location form */ +div.em-location-types-single { display:none; visibility: hidden; } div.em-location-data table.em-location-data td, div.em-location-data table.em-location-data th { vertical-align:top; border:none; } div.em-location-data table.em-location-data { width:50%; float:left; border:none; } +div.em-location-data table.em-location-data .em-location-data-url input { width:100%; } div.em-location-data .em-location-map-container, div.em-location-data .em-location-map-404, div.em-location-data .em-location-map-content { width: 400px; height: 300px; float:left; } div.em-location-data .em-location-map-404 { vertical-align:middle; text-align: center; } /* MS Global Categories */ @@ -151,7 +161,7 @@ table.em-tickets-bookings-table tbody td { text-align:center; } #em-options-form th { padding: 15px 20px; margin:0 !important; font-size:0.97em; } #em-options-form .em-boxheader { font-style:italic; margin:0; padding:10px 5px; } #em-options-form tr.em-header td { font-style:italic; padding:10px 5px; margin:0; } -#em-options-form tr.em-header h4 { font-weight:bold; font-size:15px; font-style:normal; border-bottom: 1px solid #dedede; margin:0 0 10px; padding:0 0 10px; } +#em-options-form tr.em-header h4, #em-options-form .postbox h4 { font-weight:bold; font-size:15px; font-style:normal; border-bottom: 1px solid #dedede; margin:0 0 10px; padding:0 0 10px; } #em-options-form tr.em-subheader td { font-style:italic; margin:0; padding:5px 20px 2px; } #em-options-form tr.em-subheader h5 { font-style:normal; margin:10px 0; padding:0 0 5px; font-weight:bold; font-size:15px; border-bottom: 1px solid #efefef; color:#000; } #em-options-form tbody.em-subsection th { padding-left:35px; } diff --git a/includes/js/admin-settings.js b/includes/js/admin-settings.js index 0afd0e8..2f7933b 100755 --- a/includes/js/admin-settings.js +++ b/includes/js/admin-settings.js @@ -104,16 +104,27 @@ jQuery(document).ready(function($){ $(this).nextAll('.em-ml-options').toggle(); }); //radio triggers - $('input.em-trigger').change(function(e){ + $('input[type="radio"].em-trigger').change(function(e){ var el = $(this); el.val() == '1' ? $(el.attr('data-trigger')).show() : $(el.attr('data-trigger')).hide(); }); - $('input.em-trigger:checked').trigger('change'); - $('input.em-untrigger').change(function(e){ + $('input[type="radio"].em-trigger:checked').trigger('change'); + $('input[type="radio"].em-untrigger').change(function(e){ var el = $(this); el.val() == '0' ? $(el.attr('data-trigger')).show() : $(el.attr('data-trigger')).hide(); }); - $('input.em-untrigger:checked').trigger('change'); + $('input[type="radio"].em-untrigger:checked').trigger('change'); + //checkbox triggers + $('input[type="checkbox"].em-trigger').change(function(e){ + var el = $(this); + el.prop('checked') ? $(el.attr('data-trigger')).show() : $(el.attr('data-trigger')).hide(); + }); + $('input[type="checkbox"].em-trigger').trigger('change'); + $('input[type="checkbox"].em-untrigger').change(function(e){ + var el = $(this); + !el.prop('checked') ? $(el.attr('data-trigger')).show() : $(el.attr('data-trigger')).hide(); + }); + $('input[type="checkbox"].em-untrigger').trigger('change'); //admin tools confirm $('a.admin-tools-db-cleanup').click( function( e ){ if( !confirm(EM.admin_db_cleanup_warning) ){ diff --git a/includes/js/events-manager.js b/includes/js/events-manager.js index 3d07e9d..adb548e 100644 --- a/includes/js/events-manager.js +++ b/includes/js/events-manager.js @@ -703,7 +703,8 @@ jQuery(document).ready( function($){ }else{ jQuery('select#location-country option[value="'+ui.item.country+'"]').attr('selected', 'selected'); } - jQuery('div.em-location-data input, div.em-location-data select').css('background-color','#ccc').prop('readonly', true); + jQuery('div.em-location-data input').css('background-color','#ccc').prop('readonly', true); + jQuery('div.em-location-data select').css('background-color','#ccc').css('color', '#666666').prop('disabled', true); jQuery('#em-location-reset').show(); jQuery('#em-location-search-tip').hide(); jQuery(document).triggerHandler('em_locations_autocomplete_selected', [event, ui]); @@ -715,7 +716,7 @@ jQuery(document).ready( function($){ }; jQuery('#em-location-reset a').click( function(){ jQuery('div.em-location-data input').css('background-color','#fff').val('').prop('readonly', false); - jQuery('div.em-location-data select').css('background-color','#fff'); + jQuery('div.em-location-data select').css('background-color','#fff').css('color', 'auto').prop('disabled', false); jQuery('div.em-location-data option:selected').removeAttr('selected'); jQuery('input#location-id').val(''); jQuery('#em-location-reset').hide(); @@ -730,7 +731,8 @@ jQuery(document).ready( function($){ return false; }); if( jQuery('input#location-id').val() != '0' && jQuery('input#location-id').val() != '' ){ - jQuery('div.em-location-data input, div.em-location-data select').css('background-color','#ccc').prop('readonly', true); + jQuery('div.em-location-data input').css('background-color','#ccc').prop('readonly', true); + jQuery('div.em-location-data select').css('background-color','#ccc').css('color', '#666666').prop('disabled', true); jQuery('#em-location-reset').show(); jQuery('#em-location-search-tip').hide(); } diff --git a/multilingual/em-ml-admin.php b/multilingual/em-ml-admin.php index ac690e9..91d647c 100644 --- a/multilingual/em-ml-admin.php +++ b/multilingual/em-ml-admin.php @@ -17,11 +17,13 @@ public static function settings_pages(){ } public static function meta_boxes(){ - global $EM_Event, $EM_Location; + global $EM_Event, $EM_Location, $wp_meta_boxes; //decide if it's a master event, if not then hide the meta boxes if( !empty($EM_Event) && !EM_ML::is_original($EM_Event) ){ //remove meta boxes for events remove_meta_box('em-event-when', EM_POST_TYPE_EVENT, 'side'); + //if( isset($wp_meta_boxes[convert_to_screen(EM_POST_TYPE_EVENT)->id]['side']['high']['em-event-when']) ) unset($wp_meta_boxes[convert_to_screen(EM_POST_TYPE_EVENT)->id]['side']['high']['em-event-when']); + //if( isset($wp_meta_boxes[convert_to_screen(EM_POST_TYPE_EVENT)->id]['side']['core']['em-event-when']) ) unset($wp_meta_boxes[convert_to_screen(EM_POST_TYPE_EVENT)->id]['side']['core']['em-event-when']); remove_meta_box('em-event-recurring', 'event-recurring', 'normal'); remove_meta_box('em-event-when-recurring', 'event-recurring', 'side'); remove_meta_box('em-event-where', EM_POST_TYPE_EVENT, 'normal'); diff --git a/readme.txt b/readme.txt index fce3a6b..d34a105 100644 --- a/readme.txt +++ b/readme.txt @@ -1,11 +1,11 @@ === Events Manager === Contributors: netweblogic, nutsmuggler Donate link: http://wp-events-plugin.com -Tags: bookings, calendar, tickets, events, buddypress, event management, google maps, maps, locations, registration +Tags: bookings, calendar, tickets, events, buddypress, event management, google maps, maps, locations, registration, zoom Text Domain: events-manager -Requires at least: 4.8 -Tested up to: 5.4 -Stable tag: 5.9.7.31 +Requires at least: 5.2 +Tested up to: 5.6 +Stable tag: 5.9.810 Requires PHP: 5.3 Fully featured event registration management including recurring events, locations management, calendar, Google map integration, booking management @@ -27,6 +27,10 @@ Version 5 now makes events and locations WordPress Custom Post Types, allowing f * Bookings Management (including approval/rejections, export CVS, and more!) * Multiple Tickets * MultiSite Support +* Multiple Location Types + * Physical Locations + * Online Events (URLs) + * [Zoom Webinars/Meetings Integration](https://wordpress.org/plugins/events-manager-zoom/) * BuddyPress Support * Submit Events * Group Events @@ -62,7 +66,7 @@ We provide the tools to [help you be GDPR compliant](http://wp-events-plugin.com = Go Pro = We have a premium "Pro" add-on for Events Manager which not only demonstrates the flexibility of Events Manager, but also adds some important features including but not limited to: -* PayPal, Authorize.net and Offline Payments +* PayPal, Stripe, Authorize.net and Offline Payments * Custom booking forms * Individual Attendee custom forms * Coupon Codes @@ -111,8 +115,22 @@ See our [FAQ](http://wp-events-plugin.com/documentation/faq/) page, which is upd 6. Manage attendees with various booking reports == Changelog == -= 5.9.7.31 = -* 5.9.7.3 with GGG mods += = 5.9.810 = +* 5.9.8.1 with GGG mods + += 5.9.8.1 = +* fixed html structure error in location template breaking editor if location dropdowns are enabled + += 5.9.8 = +* added Location Types including URL and (via external free add-on) Zoom support! +* added native OAuth support for third party integrations (e.g. Zoom) +* added $EM_Event object to booking form template actions +* changed $EM_Booking->booking_status to protected so that status returns 1 even if approvals are disabled +* fixed XSS vulnerability (kudos to Jakob Wierzba) +* fixed potential SQL injection vulnerability (kudos to Antony Garand from Godaddy) +* fixed fatal errors in BuddyPress if notifications are disabled +* fixed minor PHP warning +* fixed Yoast SEO 14.0 conflict = 5.9.7.3 = * minor JS conflict fix for those overriding the tickets admin template (or those incorrectly overriding ALL EM templates) in their theme with older HTML structure diff --git a/templates/forms/event/event-locations/url.php b/templates/forms/event/event-locations/url.php new file mode 100644 index 0000000..a87ff2e --- /dev/null +++ b/templates/forms/event/event-locations/url.php @@ -0,0 +1,11 @@ + +
+ + +
+
+ + +
\ No newline at end of file diff --git a/templates/forms/event/location.php b/templates/forms/event/location.php index 6cffc08..6e50309 100755 --- a/templates/forms/event/location.php +++ b/templates/forms/event/location.php @@ -1,66 +1,100 @@ *'); + +//determine location types (if neexed) +$location_types = array(); +if( !get_option('dbem_require_location') && !get_option('dbem_use_select_for_locations') ){ + $location_types[0] = array( + 'selected' => $EM_Event->location_id === '0' || $EM_Event->location_id === 0, + 'description' => esc_html__('No Location','events-manager'), + ); +} +if( EM_Locations::is_enabled() ){ + $location_types['location'] = array( + 'selected' => !empty($EM_Event->location_id), + 'display-class' => 'em-location-type-place', + 'description' => esc_html__('Physical Location','events-manager'), + ); +} +foreach( EM_Event_Locations\Event_Locations::get_types() as $event_location_type => $EM_Event_Location_Class ){ /* @var EM_Event_Locations\Event_Location $EM_Event_Location_Class */ + if( $EM_Event_Location_Class::is_enabled() ){ + $location_types[$EM_Event_Location_Class::$type] = array( + 'display-class' => 'em-event-location-type-'. $EM_Event_Location_Class::$type, + 'selected' => $EM_Event_Location_Class::$type == $EM_Event->event_location_type, + 'description' => $EM_Event_Location_Class::get_label(), + ); + } +} ?> - -
-

- location_id === '0' || $EM_Event->location_id === 0 ) echo 'checked="checked"'; ?> /> - -

+
+ +
- -
+ +
- can_manage('edit_locations','edit_others_locations') ) : ?> - - - + + + - -
-
+ -
- - - location_id) ){ + $EM_Location = $EM_Event->get_location(); + if( $EM_Location->post_id ){ + ?> + + + + + + + + + location_id !== 0 ){ $EM_Location = $EM_Event->get_location(); @@ -69,60 +103,79 @@ }else{ $EM_Location = new EM_Location(); } - ?> - - - - - - - - - - - - - - - - - - - - - - - - - - - - + ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
- - -
- - -
  - -
  - -
  - -
  - -
  - -
  - -
+ + +
+ + +
  + +
  + +
  + +
  + +
  + +
  + +
  + +
+ + -
+
+ +
+ $EM_Event_Location_Class ): /* @var EM_Event_Locations\Event_Location $EM_Event_Location_Class */ ?> + +
+ +
+ +
\ No newline at end of file diff --git a/templates/placeholders/bookingform.php b/templates/placeholders/bookingform.php index b8d366b..db8d0fa 100644 --- a/templates/placeholders/bookingform.php +++ b/templates/placeholders/bookingform.php @@ -29,7 +29,7 @@ get_bookings()->has_booking(); - do_action('em_booking_form_top'); + do_action('em_booking_form_top', $EM_Event); ?>

@@ -47,7 +47,7 @@ 0) : ?>

#em-booking'> - + '/> @@ -107,5 +107,7 @@
- +
\ No newline at end of file diff --git a/templates/tables/events.php b/templates/tables/events.php index 6750efb..823825e 100755 --- a/templates/tables/events.php +++ b/templates/tables/events.php @@ -118,9 +118,17 @@ - - " . esc_html($EM_Event->get_location()->location_name) . "
" . esc_html($EM_Event->get_location()->location_address) . " - " . esc_html($EM_Event->get_location()->location_town); ?> - + + has_location() ){ + echo "" . esc_html($EM_Event->get_location()->location_name) . "
" . esc_html($EM_Event->get_location()->location_address) . " - " . esc_html($EM_Event->get_location()->location_town); + }elseif( $EM_Event->has_event_location() ) { + echo $EM_Event->get_event_location()->get_admin_column(); + }else{ + echo __('None','events-manager'); + } + ?> + output_dates(); ?>