all-in-one-event-calendar/app/model/event.php
2017-11-09 17:36:04 +01:00

895 lines
30 KiB
PHP

<?php
/**
* Model representing an event or an event instance.
*
* @author Time.ly Network, Inc.
* @since 2.0
* @instantiator Ai1ec_Factory_Event.create_event_instance
* @package Ai1EC
* @subpackage Ai1EC.Model
*/
class Ai1ec_Event extends Ai1ec_Base {
/**
* @var Ai1ec_Event_Entity Data store object reference.
*/
protected $_entity = null;
/**
* @var array Map of fields that require special care during set/get
* operations. Values have following meanings:
* [0] - both way care required;
* [1] - only `set` operations require care;
* [-1] - only `get` (for storage) operations require care.
*/
protected $_swizzable = array(
'cost' => 0,
'start' => -1,
'end' => -1,
'timezone_name' => -1,
'recurrence_dates' => 1,
'exception_dates' => 1,
);
/**
* @var array Runtime properties
*/
protected $_runtime_props = array();
/**
* @var bool|null Boolean cache-definition indicating if event is multiday.
*/
protected $_is_multiday = null;
/**
* Wrapper to get property value.
*
* @param string $property Name of property to get.
* @param mixed $default Default value to return.
*
* @return mixed Actual property.
*/
public function get( $property, $default = null ) {
return $this->_entity->get( $property, $default );
}
/**
* Get properties generated at runtime
*
* @param string $property
*
* @return string
*/
public function get_runtime( $property, $default = '' ) {
return isset( $this->_runtime_props[$property] ) ?
$this->_runtime_props[$property] :
$default;
}
/**
* Set properties generated at runtime
*
* @param string $property
* @param string $value
*/
public function set_runtime( $property, $value ) {
$this->_runtime_props[$property] = $value;
}
/**
* Handle property initiation.
*
* Decides, how to extract value stored in permanent storage.
*
* @param string $property Name of property to handle
* @param mixed $value Value, read from permanent storage
*
* @return bool Success
*/
public function set( $property, $value ) {
if (
isset( $this->_swizzable[$property] ) &&
$this->_swizzable[$property] >= 0
) {
$method = '_handle_property_construct_' . $property;
$value = $this->{$method}( $value );
}
$this->_entity->set( $property, $value );
return $this;
}
/**
* Set the event is all day, during the specified number of days
*
* @param number $length
*/
public function set_all_day( $length = 1 ) {
// set allday as true
$this->set( 'allday', true );
$start = $this->get( 'start' );
// reset time component
$start->set_time( 0, 0, 0 );
$end = $this->_registry->get( 'date.time', $start );
// set the correct length
$end->adjust_day( $length );
$this->set( 'end', $end );
}
/**
* Set the event as if it has no end time
*/
public function set_no_end_time() {
$this->set( 'instant_event', true );
$start = $this->get( 'start' );
$end = $this->_registry->get( 'date.time', $start );
$end->set_time(
$start->format( 'H' ),
$start->format( 'i' ) + 15,
$start->format( 's' )
);
$this->set( 'end', $end );
}
/**
* Set object fields from arbitrary array.
*
* @param array $data Supposedly map of fields to initiate.
*
* @return Ai1ec_Event Instance of self for chaining.
*/
public function initialize_from_array( array $data ) {
// =======================================================
// = Assign each event field the value from the database =
// =======================================================
foreach ( $this->_entity->list_properties() as $property ) {
if ( 'post' !== $property && isset( $data[$property] ) ) {
$this->set( $property, $data[$property] );
unset( $data[$property] );
}
}
if ( isset( $data['post'] ) ) {
$this->set( 'post', (object)$data['post'] );
} else {
// ========================================
// = Remaining fields are the post fields =
// ========================================
$this->set( 'post', (object)$data );
}
return $this;
}
/**
* Delete the events from all tables
*/
public function delete() {
// delete post (this will trigger deletion of cached events, and
// remove the event from events table)
wp_delete_post( $this->get( 'post_id' ), true );
}
/**
* Initialize object from ID.
*
* Attempts to retrieve entity from database and if succeeds - uses
* {@see self::initialize_from_array} to initiate actual values.
*
* @param int $post_id ID of post (event) to initiate.
* @param int|bool $instance ID of event instance, false for base event.
*
* @return Ai1ec_Event Instance of self for chaining.
*
* @throws Ai1ec_Event_Not_Found_Exception If entity is not locatable.
*/
public function initialize_from_id( $post_id, $instance = false ) {
$post = get_post( $post_id );
if ( ! $post || $post->post_status == 'auto-draft' ) {
throw new Ai1ec_Event_Not_Found_Exception(
'Post with ID \'' . $post_id .
'\' could not be retrieved from the database.'
);
}
$post_id = (int)$post_id;
$dbi = $this->_registry->get( 'dbi.dbi' );
$left_join = '';
$select_sql = '
e.post_id,
e.timezone_name,
e.recurrence_rules,
e.exception_rules,
e.allday,
e.instant_event,
e.recurrence_dates,
e.exception_dates,
e.venue,
e.country,
e.address,
e.city,
e.province,
e.postal_code,
e.show_map,
e.contact_name,
e.contact_phone,
e.contact_email,
e.contact_url,
e.cost,
e.ticket_url,
e.ical_feed_url,
e.ical_source_url,
e.ical_organizer,
e.ical_contact,
e.ical_uid,
e.longitude,
e.latitude,
e.show_coordinates,
GROUP_CONCAT( ttc.term_id ) AS categories,
GROUP_CONCAT( ttt.term_id ) AS tags
';
if (
false !== $instance &&
is_numeric( $instance ) &&
$instance > 0
) {
$select_sql .= ', IF( aei.start IS NOT NULL, aei.start, e.start ) as start,' .
' IF( aei.start IS NOT NULL, aei.end, e.end ) as end ';
$instance = (int)$instance;
$this->set( 'instance_id', $instance );
$left_join = 'LEFT JOIN ' . $dbi->get_table_name( 'ai1ec_event_instances' ) .
' aei ON aei.id = ' . $instance . ' AND e.post_id = aei.post_id ';
} else {
$select_sql .= ', e.start as start, e.end as end, e.allday ';
if ( -1 === (int)$instance ) {
$select_sql .= ', aei.id as instance_id ';
$left_join = 'LEFT JOIN ' .
$dbi->get_table_name( 'ai1ec_event_instances' ) .
' aei ON e.post_id = aei.post_id ' .
'AND e.start = aei.start AND e.end = aei.end ';
}
}
// =============================
// = Fetch event from database =
// =============================
$query = 'SELECT ' . $select_sql . '
FROM ' . $dbi->get_table_name( 'ai1ec_events' ) . ' e
LEFT JOIN ' .
$dbi->get_table_name( 'term_relationships' ) . ' tr
ON ( e.post_id = tr.object_id )
LEFT JOIN ' . $dbi->get_table_name( 'term_taxonomy' ) . ' ttc
ON (
tr.term_taxonomy_id = ttc.term_taxonomy_id AND
ttc.taxonomy = \'events_categories\'
)
LEFT JOIN ' . $dbi->get_table_name( 'term_taxonomy' ) . ' ttt
ON (
tr.term_taxonomy_id = ttt.term_taxonomy_id AND
ttt.taxonomy = \'events_tags\'
)
' . $left_join . '
WHERE e.post_id = ' . $post_id . '
GROUP BY e.post_id';
$event = $dbi->get_row( $query, ARRAY_A );
if ( null === $event || null === $event['post_id'] ) {
throw new Ai1ec_Event_Not_Found_Exception(
'Event with ID \'' . $post_id .
'\' could not be retrieved from the database.'
);
}
$event['post'] = $post;
return $this->initialize_from_array( $event );
}
public function getenddate() {
$end = $this->get( 'end' );
if ( $this->is_allday() ) {
$end->set_time(
$end->format( 'H' ),
$end->format( 'i' ),
$end->format( 's' ) - 1
);
}
return $end;
}
/**
* Returns enddate specific info.
*
* @return array Date info structure.
*/
public function getenddate_info() {
$end = $this->getenddate();
return array(
'month' => $this->get( 'end' )->format_i18n( 'M' ),
'day' => $this->get( 'end' )->format_i18n( 'j' ),
'weekday' => $this->get( 'end' )->format_i18n( 'D' ),
'year' => $this->get( 'end' )->format_i18n( 'Y' ),
);
}
/**
* Create new event object, using provided data for initialization.
*
* @param Ai1ec_Registry_Object $registry Injected object registry.
* @param int|array|null $data Look up post with id $data, or
* initialize fields with associative
* array $data containing both post
* and event fields.
* @param int|bool $instance Optionally instance ID. When ID
* value is -1 then it is
* retrieved from db.
*
* @throws Ai1ec_Invalid_Argument_Exception When $data is not one
* of int|array|null.
* @throws Ai1ec_Event_Not_Found_Exception When $data relates to
* non-existent ID.
*
*/
function __construct(
Ai1ec_Registry_Object $registry,
$data = null,
$instance = false
) {
parent::__construct( $registry );
$this->_entity = $this->_registry->get( 'model.event.entity' );
if ( null === $data ) {
return; // empty object
} else if ( is_numeric( $data ) ) {
$this->initialize_from_id( $data, $instance );
} else if ( is_array( $data ) ) {
$this->initialize_from_array( $data );
} else {
throw new Ai1ec_Invalid_Argument_Exception(
'Argument to constructor must be integer, array or null' .
', not ' . var_export( $data, true )
);
}
if ( $this->is_allday() ) {
try {
$timezone = $this->_registry->get( 'date.timezone' )
->get( $this->get( 'timezone_name' ) );
$this->_entity->set_preferred_timezone( $timezone );
} catch ( Exception $excpt ) {
// ignore
}
}
}
/**
* Twig method for retrieving avatar.
*
* @param bool $wrap_permalink Whether to wrap avatar in <a> element or not
*
* @return string Avatar markup
*/
public function getavatar( $wrap_permalink = true ) {
return $this->_registry->
get( 'view.event.avatar' )->get_event_avatar(
$this,
$this->_registry->get( 'view.calendar.fallbacks' )->get_all(),
'',
$wrap_permalink
);
}
/**
* Returns whether Event has geo information.
*
* @return bool True or false.
*/
public function has_geoinformation() {
$latitude = floatval( $this->get( 'latitude') );
$longitude = floatval( $this->get( 'longitude' ) );
return (
(
$latitude >= 0.000000000000001 ||
$latitude <= -0.000000000000001
) &&
(
$longitude >= 0.000000000000001 ||
$longitude <= -0.000000000000001
)
);
}
protected function _handle_property_construct_recurrence_dates( $value ) {
if ( $value ) {
$this->_entity->set( 'recurrence_rules', 'RDATE=' . $value );
}
return $value;
}
protected function _handle_property_construct_exception_dates( $value ) {
if ( $value ) {
$this->_entity->set( 'exception_rules', 'EXDATE=' . $value );
}
return $value;
}
/**
* Handle `cost` value reading from permanent storage.
*
* @param string $value Value stored in permanent storage
*
* @return bool Success: true, always
*/
protected function _handle_property_construct_cost( $value ) {
$test_value = false;
if (
isset( $value{1} ) && (
':' === $value{1} || ';' === $value{1}
)
) {
$test_value = unserialize( $value );
}
$cost = $is_free = NULL;
if ( false === $test_value ) {
$cost = trim( $value );
$is_free = false;
} else {
extract( $test_value, EXTR_IF_EXISTS );
}
$this->_entity->set( 'is_free', (bool)$is_free );
return (string)$cost;
}
public function get_uid_pattern() {
static $format = null;
if ( null === $format ) {
$site_url = parse_url( ai1ec_get_site_url() );
$format = 'ai1ec-%d@' . $site_url['host'];
if ( isset( $site_url['path'] ) ) {
$format .= $site_url['path'];
}
}
return $format;
}
/**
* Get UID to be used for current event.
*
* The generated format is cached in static variable within this function
* to re-use when generating UIDs for different entries.
*
* @return string Generated UID.
*
* @staticvar string $format Cached format.
*/
public function get_uid() {
$ical_uid = $this->get( 'ical_uid' );
if ( ! empty( $ical_uid ) ) {
return $ical_uid;
}
return sprintf( $this->get_uid_pattern(), $this->get( 'post_id' ) );
}
/**
* Check if event is free.
*
* @return bool Free status.
*/
public function is_free() {
return (bool)$this->get( 'is_free' );
}
/**
* Check if event is taking all day.
*
* @return bool True for all-day long events.
*/
public function is_allday() {
return (bool)$this->get( 'allday' );
}
/**
* Check if event has virtually no time.
*
* @return bool True for instant events.
*/
public function is_instant() {
return (bool)$this->get( 'instant_event' );
}
/**
* Check if event is taking multiple days.
*
* Uses object-wide variable {@see self::$_is_multiday} to store
* calculated value after first call.
*
* @return bool True for multiday events.
*/
public function is_multiday() {
if ( null === $this->_is_multiday ) {
$start = $this->get( 'start' );
$end = $this->get( 'end' );
$diff = $end->diff_sec( $start );
$this->_is_multiday = $diff > 86400 &&
$start->format( 'Y-m-d' ) !== $end->format( 'Y-m-d' );
}
return $this->_is_multiday;
}
/**
* Get the duration of the event
*
* @return number
*/
public function get_duration() {
$duration = $this->get_runtime( 'duration', null );
if ( null === $duration ) {
$duration = $this->get( 'end' )->format() -
$this->get( 'start' )->format();
$this->set_runtime( 'duration', $duration );
}
return $duration;
}
/**
* Create/update entity representation.
*
* Saves the current event data to the database. If $this->post_id exists,
* but $update is false, creates a new record in the ai1ec_events table of
* this event data, but does not try to create a new post. Else if $update
* is true, updates existing event record. If $this->post_id is empty,
* creates a new post AND record in the ai1ec_events table for this event.
*
* @param bool $update Whether to update an existing event or create a
* new one
* @param bool $backward_compatibility The (wpdb) ofr the new wordpress 4.4
* now inserts NULL as null values. The previous version, if you insert a NULL
* value in an int value, the values saved would be 0 instead of null.
* @return int The post_id of the new or existing event.
*/
function save( $update = false, $backward_compatibility = true ) {
do_action( 'ai1ec_pre_save_event', $this, $update );
if ( ! $update ) {
$response = apply_filters( 'ai1ec_event_save_new', $this );
if ( is_wp_error( $response ) ) {
throw new Ai1ec_Event_Create_Exception(
'Failed to create event: ' . $response->get_error_message()
);
}
}
$dbi = $this->_registry->get( 'dbi.dbi' );
$columns = $this->prepare_store_entity();
$format = $this->prepare_store_format( $columns, $backward_compatibility );
$table_name = $dbi->get_table_name( 'ai1ec_events' );
$post_id = $columns['post_id'];
if ( $this->get( 'end' )->is_empty() ) {
$this->set_no_end_time();
}
if ( $post_id ) {
$success = false;
if ( ! $update ) {
$success = $dbi->insert(
$table_name,
$columns,
$format
);
} else {
$success = $dbi->update(
$table_name,
$columns,
array( 'post_id' => $columns['post_id'] ),
$format,
array( '%d' )
);
}
if ( false === $success ) {
return false;
}
} else {
// ===================
// = Insert new post =
// ===================
$post_id = wp_insert_post( $this->get( 'post' ), false );
if ( 0 === $post_id ) {
return false;
}
$this->set( 'post_id', $post_id );
$columns['post_id'] = $post_id;
// =========================
// = Insert new event data =
// =========================
if ( false === $dbi->insert( $table_name, $columns, $format ) ) {
return false;
}
}
$taxonomy = $this->_registry->get(
'model.event.taxonomy',
$post_id
);
$cats = $this->get( 'categories' );
if (
is_array( $cats ) &&
! empty( $cats )
) {
$taxonomy->set_categories( $cats );
}
$tags = $this->get( 'tags' );
if (
is_array( $tags ) &&
! empty( $tags )
) {
$taxonomy->set_tags( $tags );
}
if (
$feed = $this->get( 'feed' ) &&
isset( $feed->feed_id )
) {
$taxonomy->set_feed( $feed );
}
// give other plugins / extensions the ability to do things
// when saving, like fetching authors which i removed as it's not core.
do_action( 'ai1ec_save_event' );
$instance_model = $this->_registry->get( 'model.event.instance' );
$instance_model->recreate( $this );
do_action( 'ai1ec_event_saved', $post_id, $this, $update );
return $post_id;
}
/**
* Prepare fields format flags to use in database operations.
*
* @param array $columns Array of columns with data to insert.
*
* @return array List of format flags to use in integrations with DBI.
*/
public function prepare_store_format( array &$columns, $backward_compatibility = true ) {
$format = array(
'%d', // post_id
'%d', // start
'%d', // end
'%s', // timezone_name
'%d', // allday
'%d', // instant_event
'%s', // recurrence_rules
'%s', // exception_rules
'%s', // recurrence_dates
'%s', // exception_dates
'%s', // venue
'%s', // country
'%s', // address
'%s', // city
'%s', // province
'%s', // postal_code
'%d', // show_map
'%s', // contact_name
'%s', // contact_phone
'%s', // contact_email
'%s', // contact_url
'%s', // cost
'%s', // ticket_url
'%s', // ical_feed_url
'%s', // ical_source_url
'%s', // ical_uid
'%d', // show_coordinates
'%f', // latitude
'%f', // longitude
);
if ( $backward_compatibility ) {
$columns_count = count( $columns );
if ( count( $format ) !== $columns_count ) {
throw new Ai1ec_Event_Not_Found_Exception(
'Data columns count differs from format columns count'
);
}
$index = 0;
foreach ( $columns as $key => $value ) {
if ( '%d' === $format[ $index ] ) {
if ( is_null( $value ) ) {
$columns[ $key ] = 0;
}
}
$index++;
}
}
return $format;
}
/**
* Prepare event entity {@see self::$_entity} for persistent storage.
*
* Creates an array of database fields and corresponding values.
*
* @return array Map of fields to store.
*/
public function prepare_store_entity() {
$entity = array(
'post_id' => $this->storage_format( 'post_id' ),
'start' => $this->storage_format( 'start' ),
'end' => $this->storage_format( 'end' ),
'timezone_name' => $this->storage_format( 'timezone_name' ),
'allday' => $this->storage_format( 'allday' ),
'instant_event' => $this->storage_format( 'instant_event' ),
'recurrence_rules' => $this->storage_format( 'recurrence_rules' ),
'exception_rules' => $this->storage_format( 'exception_rules' ),
'recurrence_dates' => $this->storage_format( 'recurrence_dates' ),
'exception_dates' => $this->storage_format( 'exception_dates' ),
'venue' => $this->storage_format( 'venue' ),
'country' => $this->storage_format( 'country' ),
'address' => $this->storage_format( 'address' ),
'city' => $this->storage_format( 'city' ),
'province' => $this->storage_format( 'province' ),
'postal_code' => $this->storage_format( 'postal_code' ),
'show_map' => $this->storage_format( 'show_map' ),
'contact_name' => $this->storage_format( 'contact_name' ),
'contact_phone' => $this->storage_format( 'contact_phone' ),
'contact_email' => $this->storage_format( 'contact_email' ),
'contact_url' => $this->storage_format( 'contact_url' ),
'cost' => $this->storage_format( 'cost' ),
'ticket_url' => $this->storage_format( 'ticket_url' ),
'ical_feed_url' => $this->storage_format( 'ical_feed_url' ),
'ical_source_url' => $this->storage_format( 'ical_source_url' ),
'ical_uid' => $this->storage_format( 'ical_uid' ),
'show_coordinates' => $this->storage_format( 'show_coordinates' ),
'latitude' => $this->storage_format( 'latitude', '' ),
'longitude' => $this->storage_format( 'longitude', '' ),
);
return $entity;
}
/**
* Compact field for writing to persistent storage.
*
* @param string $field Name of field to compact.
* @param mixed $default Default value to use for undescribed fields.
*
* @return mixed Value or $default.
*/
public function storage_format( $field, $default = null ) {
$value = $this->_entity->get( $field, $default );
if (
isset( $this->_swizzable[$field] ) &&
$this->_swizzable[$field] <= 0
) {
$value = $this->{ '_handle_property_destruct_' . $field }( $value );
}
return $value;
}
/**
* Allow properties to be modified after cloning.
*
* @return void
*/
public function __clone() {
$this->_entity = clone $this->_entity;
}
/**
* Decode timezone to use for event.
*
* Following algorythm is used to detect a value:
* - take value provided in input;
* - if empty - take value associated with start time;
* - if empty - take current environment timezone.
*
* @param string $timezone_name Timezone provided in input.
*
* @return string Timezone name to use for event in future.
*/
protected function _handle_property_destruct_timezone_name(
$timezone_name
) {
if ( empty( $timezone_name ) ) {
$timezone_name = $this->get( 'start' )->get_timezone();
if ( empty( $timezone_name ) ) {
$timezone_name = $this->_registry->get( 'date.timezone' )
->get_default_timezone();
}
}
return $timezone_name;
}
/**
* Format datetime to UNIX timestamp for storage.
*
* @param Ai1ec_Date_Time $start Datetime object to compact.
*
* @return int UNIX timestamp.
*/
protected function _handle_property_destruct_start( Ai1ec_Date_Time $start ) {
return $start->format_to_gmt();
}
/**
* Format datetime to UNIX timestamp for storage.
*
* @param Ai1ec_Date_Time $end Datetime object to compact.
*
* @return int UNIX timestamp.
*/
protected function _handle_property_destruct_end( Ai1ec_Date_Time $end ) {
return $end->format_to_gmt();
}
/**
* Handle `cost` writing to permanent storage.
*
* @param string $cost Value of cost.
*
* @return string Serialized value to store.
*/
protected function _handle_property_destruct_cost( $cost ) {
$cost = array(
'cost' => $cost,
'is_free' => false,
);
if ( $this->get( 'is_free' ) ) {
$cost['is_free'] = true;
}
return serialize( $cost );
}
/**
* Get the submitter information array
* @return array (
* is_organizer => 1 if the organizer is the submitter,
* email => if is_organizer is 0, them this property has the email of the submitter,
* name => if is_organizer is 0, them this property has the name of the submitter
* )
*/
public function get_submitter_info() {
$post_id = $this->get( 'post_id' );
if ( empty( $post_id ) ) {
return null;
}
$submitter_info = get_post_meta(
$post_id,
'_submitter_info',
true
);
if ( false == ai1ec_is_blank( $submitter_info ) ) {
$submitter_info = json_decode( $submitter_info, true );
if ( is_array( $submitter_info ) ) {
return $submitter_info;
}
}
return null;
}
/**
* Save the submitter information into post metadata
*/
public function save_submitter_info( $is_submitter, $submitter_email, $submitter_name ) {
$post_id = $this->get( 'post_id' );
if ( empty( $post_id ) ) {
throw new Exception( 'Post id empty' );
}
$save = false;
if ( 1 === intval( $is_submitter ) ) {
$submitter_info['is_organizer'] = 1;
if ( false === ai1ec_is_blank( $this->get( 'contact_email' ) ) ) {
$save = true;
}
} else {
$submitter_info['is_organizer'] = 0;
if ( false === ai1ec_is_blank( $submitter_email ) ) {
$submitter_info['email'] = trim( $submitter_email );
$submitter_info['name'] = trim( $submitter_name );
$save = true;
}
}
if ( $save ) {
update_post_meta( $post_id, '_submitter_info', json_encode( $submitter_info ) );
}
}
}