Ticket #2955: 2955.2.diff
File 2955.2.diff, 59.5 KB (added by , 4 years ago) |
---|
-
new file wordpress.org/public_html/wp-content/plugins/wporg-meeting-ical/inc/class-ical-generator.php
diff --git wordpress.org/public_html/wp-content/plugins/wporg-meeting-ical/inc/class-ical-generator.php wordpress.org/public_html/wp-content/plugins/wporg-meeting-ical/inc/class-ical-generator.php new file mode 100644 index 000000000..bcedf3444
- + 1 <?php 2 3 namespace WordPressdotorg\Meetings\Calendar; 4 5 class ICAL_Generator { 6 7 8 const NEWLINE = "\r\n"; 9 10 /** 11 * Generate an iCalendar for the given set of meetings. 12 * 13 * @param WP_Post[] $posts 14 * @return string 15 */ 16 public function generate( $posts ) { 17 $ical = 'BEGIN:VCALENDAR' . self::NEWLINE; 18 $ical .= 'VERSION:2.0' . self::NEWLINE; 19 $ical .= 'PRODID:-//WPORG Make//Meeting Events Calendar//EN' . self::NEWLINE; 20 $ical .= 'METHOD:PUBLISH' . self::NEWLINE; 21 $ical .= 'CALSCALE:GREGORIAN' . self::NEWLINE; 22 23 foreach ( $posts as $post ) { 24 $ical .= $this->generate_event( $post ); 25 } 26 27 $ical .= 'END:VCALENDAR'; 28 return $ical; 29 } 30 31 /** 32 * Generate an event for a meeting. 33 * 34 * @param WP_Post $post 35 * @return string 36 */ 37 private function generate_event( $post ) { 38 $id = $post->ID; 39 $title = $post->post_title; 40 $location = $post->location; 41 $link = $post->link; 42 $team = $post->team; 43 $recurring = $post->recurring; 44 $sequence = empty( $post->sequence ) ? 0 : intval( $post->sequence ); 45 46 $start_date = strftime( '%Y%m%d', strtotime( $post->next_date ) ); 47 $start_time = strftime( '%H%M%S', strtotime( $post->time ) ); 48 $start_date_time = "{$start_date}T{$start_time}Z"; 49 50 $end_date = $start_date; 51 $end_time = strftime( '%H%M%S', strtotime( "{$post->time} +1 hour" ) ); 52 $end_date_time = "{$end_date}T{$end_time}Z"; 53 54 $description = ''; 55 $slack_channel = null; 56 57 if ( $location && preg_match( '/^#([-\w]+)$/', trim( $location ), $match ) ) { 58 $slack_channel = '#' . sanitize_title( $match[1] ); 59 $location = "{$slack_channel} channel on Slack"; 60 } 61 62 if ( $link ) { 63 if ( $slack_channel ) { 64 $description .= "Slack channel link: https://wordpress.slack.com/messages/{$slack_channel}\\n"; 65 } 66 67 $description .= "For more information visit {$link}"; 68 } 69 70 $frequency = $this->get_frequency( $recurring, $post->next_date, $post->occurrence ); 71 72 $event = 'BEGIN:VEVENT' . self::NEWLINE; 73 $event .= "UID:{$id}" . self::NEWLINE; 74 75 $event .= "DTSTAMP:{$start_date_time}" . self::NEWLINE; 76 $event .= "DTSTART;VALUE=DATE:{$start_date_time}" . self::NEWLINE; 77 $event .= "DTEND;VALUE=DATE:{$end_date_time}" . self::NEWLINE; 78 $event .= 'CATEGORIES:WordPress' . self::NEWLINE; 79 // Some calendars require the organizer's name and email address 80 $event .= "ORGANIZER;CN=WordPress {$team} Team:mailto:mail@example.com" . self::NEWLINE; 81 $event .= "SUMMARY:{$team}: {$title}" . self::NEWLINE; 82 // Incrementing the sequence number updates the specified event 83 $event .= "SEQUENCE:{$sequence}" . self::NEWLINE; 84 $event .= 'STATUS:CONFIRMED' . self::NEWLINE; 85 $event .= 'TRANSP:OPAQUE' . self::NEWLINE; 86 87 if ( ! empty( $location ) ) { 88 $event .= "LOCATION:{$location}" . self::NEWLINE; 89 } 90 91 if ( ! empty( $description ) ) { 92 $event .= "DESCRIPTION:{$description}" . self::NEWLINE; 93 } 94 95 if ( ! is_null( $frequency ) ) { 96 $event .= "RRULE:FREQ={$frequency}" . self::NEWLINE; 97 } 98 99 $event .= 'END:VEVENT' . self::NEWLINE; 100 101 return $event; 102 } 103 104 private function get_frequency( $recurrence, $date, $occurrences ) { 105 switch ( $recurrence ) { 106 case 'weekly': 107 $frequency = 'WEEKLY'; 108 break; 109 case 'biweekly': 110 $frequency = 'WEEKLY;INTERVAL=2'; 111 break; 112 case 'monthly': 113 $frequency = 'MONTHLY'; 114 break; 115 case 'occurrence': 116 $frequency = $this->get_frequencies_by_day( $occurrences, $date ); 117 break; 118 default: 119 $frequency = null; 120 } 121 122 return $frequency; 123 } 124 125 /** 126 * Returns a comma separated list of days in which the event should repeat for the month. 127 * 128 * For example, given: 129 * $occurrences = array( 1, 3 ) // 1st and 3rd week in the month 130 * $date = '2019-09-15' // the day is Sunday 131 * it will return: 'MONTHLY;BYDAY=1SU,3SU' 132 * 133 * @param array $occurrences 134 * @param string $date 135 * @return string 136 */ 137 private function get_frequencies_by_day( $occurrences, $date ) { 138 // Get the first two letters of the day of the start date in uppercase letters 139 $day = strtoupper( 140 substr( strftime( '%a', strtotime( $date ) ), 0, 2 ) 141 ); 142 143 $by_days = array_reduce( 144 array_keys( $occurrences ), 145 function ( $carry, $key ) use ( $day, $occurrences ) { 146 $carry .= $occurrences[ $key ] . $day; 147 148 if ( $key < count( $occurrences ) - 1 ) { 149 $carry .= ','; 150 } 151 152 return $carry; 153 } 154 ); 155 156 return "MONTHLY;BYDAY={$by_days}"; 157 } 158 } -
new file wordpress.org/public_html/wp-content/plugins/wporg-meeting-ical/inc/class-plugin.php
diff --git wordpress.org/public_html/wp-content/plugins/wporg-meeting-ical/inc/class-plugin.php wordpress.org/public_html/wp-content/plugins/wporg-meeting-ical/inc/class-plugin.php new file mode 100644 index 000000000..09253d6b0
- + 1 <?php 2 3 namespace WordPressdotorg\Meetings\Calendar; 4 5 use WordPressdotorg\Meetings\Common\Meeting_Query; 6 7 class Plugin { 8 9 const QUERY_KEY = 'meeting_ical'; 10 const QUERY_TEAM_KEY = 'meeting_team'; 11 12 /** 13 * @var Plugin The singleton instance. 14 */ 15 private static $instance; 16 17 /** 18 * Returns always the same instance of this plugin. 19 * 20 * @return Plugin 21 */ 22 public static function get_instance() { 23 if ( ! ( self::$instance instanceof Plugin ) ) { 24 self::$instance = new Plugin(); 25 } 26 return self::$instance; 27 } 28 29 /** 30 * Instantiates a new Plugin object. 31 */ 32 private function __construct() { 33 add_action( 'plugins_loaded', array( $this, 'plugins_loaded' ) ); 34 } 35 36 public function plugins_loaded() { 37 // Stop loading if "Meeting Post Type" plugin not available. 38 if ( ! class_exists( '\WordPressdotorg\Meetings\PostType\Plugin' ) ) { 39 add_action( 40 'admin_notices', 41 function () { 42 echo '<div class="error"><p><strong>' . 'The Meetings iCalendar API requires the "Meetings Post Type" plugin to be installed and active' . '</strong></p></div>'; 43 } 44 ); 45 46 return; 47 } 48 49 register_activation_hook( __FILE__, array( $this, 'on_activate' ) ); 50 register_deactivation_hook( __FILE__, array( $this, 'on_deactivate' ) ); 51 52 add_action( 'init', array( $this, 'add_rewrite_rules' ) ); 53 add_action( 'parse_request', array( $this, 'parse_request' ) ); 54 55 add_filter( 'query_vars', array( $this, 'query_vars' ) ); 56 } 57 58 public function on_activate() { 59 $this->add_rewrite_rules(); 60 flush_rewrite_rules(); 61 } 62 63 public function on_deactivate() { 64 flush_rewrite_rules(); // remove custom rewrite rule 65 delete_option( self::QUERY_KEY ); // remove cache 66 } 67 68 public function add_rewrite_rules() { 69 add_rewrite_rule( 70 '^meetings/?([a-zA-Z\d\s_-]+)?/calendar\.ics$', 71 array( 72 self::QUERY_KEY => 1, 73 self::QUERY_TEAM_KEY => '$matches[1]', 74 ), 75 'top' 76 ); 77 } 78 79 public function parse_request( $request ) { 80 if ( ! array_key_exists( self::QUERY_KEY, $request->query_vars ) ) { 81 return; 82 } 83 84 $team = strtolower( $request->query_vars[ self::QUERY_TEAM_KEY ] ); 85 86 // Generate a calendar if such a team exists 87 $ical = $this->get_ical_contents( $team ); 88 89 if ( null !== $ical ) { 90 /** 91 * If the calendar has a 'method' property, the 'Content-Type' header must also specify it 92 */ 93 header( 'Content-Type: text/calendar; charset=utf-8; method=publish' ); 94 header( 'Content-Disposition: inline; filename=calendar.ics' ); 95 echo $ical; 96 exit; 97 } 98 99 return; 100 } 101 102 public function query_vars( $query_vars ) { 103 array_push( $query_vars, self::QUERY_KEY ); 104 array_push( $query_vars, self::QUERY_TEAM_KEY ); 105 return $query_vars; 106 } 107 108 private function get_ical_contents( $team ) { 109 $ttl = 1; // in seconds 110 $option = $team ? self::QUERY_KEY . "_{$team}" : self::QUERY_KEY; 111 $cache = get_option( $option, false ); 112 113 if ( is_array( $cache ) && $cache['timestamp'] > time() - $ttl ) { 114 return $cache['contents']; 115 } 116 117 $contents = $this->generate_ical_contents( $team ); 118 119 if ( null !== $contents ) { 120 $cache = array( 121 'contents' => $contents, 122 'timestamp' => time(), 123 ); 124 delete_option( $option ); 125 add_option( $option, $cache, false, false ); 126 127 return $cache['contents']; 128 } 129 130 return null; 131 } 132 133 private function generate_ical_contents( $team ) { 134 $posts = $this->get_meeting_posts( $team ); 135 136 // Don't generate a calendar if there are no meetings for that team 137 if ( empty( $posts ) ) { 138 return null; 139 } 140 141 $ical_generator = new ICAL_Generator(); 142 return $ical_generator->generate( $posts ); 143 } 144 145 /** 146 * Get all meetings for a team. If the 'team' parameter is empty, all meetings are returned. 147 * 148 * @param string $team Name of the team to fetch meetings for. 149 * @return array 150 */ 151 private function get_meeting_posts( $team = '' ) { 152 $query = new Meeting_Query( $team ); 153 154 return $query->get_posts(); 155 } 156 } 157 -
new file wordpress.org/public_html/wp-content/plugins/wporg-meeting-ical/vendor/wordpressdotorg/autoload/class-autoloader.php
diff --git wordpress.org/public_html/wp-content/plugins/wporg-meeting-ical/vendor/wordpressdotorg/autoload/class-autoloader.php wordpress.org/public_html/wp-content/plugins/wporg-meeting-ical/vendor/wordpressdotorg/autoload/class-autoloader.php new file mode 100644 index 000000000..8c2258ba3
- + 1 <?php 2 namespace WordPressdotorg\Autoload; 3 4 /** 5 * An Autoloader which respects WordPress's filename standards. 6 * 7 * @package WordPressdotorg\Autoload 8 */ 9 class Autoloader { 10 11 /** 12 * Namespace separator. 13 */ 14 const NS_SEPARATOR = '\\'; 15 16 /** 17 * The prefix to compare classes against. 18 * 19 * @var string 20 * @access protected 21 */ 22 protected $prefix; 23 24 /** 25 * Length of the prefix string. 26 * 27 * @var int 28 * @access protected 29 */ 30 protected $prefix_length; 31 32 /** 33 * Path to the file to be loaded. 34 * 35 * @var string 36 * @access protected 37 */ 38 protected $path; 39 40 /** 41 * Constructor. 42 * 43 * @param string $prefix Prefix all classes have in common. 44 * @param string $path Path to the files to be loaded. 45 */ 46 public function __construct( $prefix, $path ) { 47 $this->prefix = $prefix; 48 $this->prefix_length = strlen( $prefix ); 49 $this->path = trailingslashit( $path ); 50 } 51 52 /** 53 * Loads a class if it starts with `$this->prefix`. 54 * 55 * @param string $class The class to be loaded. 56 */ 57 public function load( $class ) { 58 if ( strpos( $class, $this->prefix . self::NS_SEPARATOR ) !== 0 ) { 59 return; 60 } 61 62 // Strip prefix from the start (ala PSR-4) 63 $class = substr( $class, $this->prefix_length + 1 ); 64 $class = strtolower( $class ); 65 $file = ''; 66 67 if ( false !== ( $last_ns_pos = strripos( $class, self::NS_SEPARATOR ) ) ) { 68 $namespace = substr( $class, 0, $last_ns_pos ); 69 $namespace = str_replace( '_', '-', $namespace ); 70 $class = substr( $class, $last_ns_pos + 1 ); 71 $file = str_replace( self::NS_SEPARATOR, DIRECTORY_SEPARATOR, $namespace ) . DIRECTORY_SEPARATOR; 72 } 73 74 $file .= 'class-' . str_replace( '_', '-', $class ) . '.php'; 75 76 $path = $this->path . $file; 77 78 if ( file_exists( $path ) ) { 79 require $path; 80 } 81 } 82 } 83 84 /** 85 * Registers Autoloader's autoload function. 86 * 87 * @param string $prefix 88 * @param string $path 89 */ 90 function register_class_path( $prefix, $path ) { 91 $loader = new Autoloader( $prefix, $path ); 92 spl_autoload_register( array( $loader, 'load' ) ); 93 } -
new file wordpress.org/public_html/wp-content/plugins/wporg-meeting-ical/wporg-meeting-ical.php
diff --git wordpress.org/public_html/wp-content/plugins/wporg-meeting-ical/wporg-meeting-ical.php wordpress.org/public_html/wp-content/plugins/wporg-meeting-ical/wporg-meeting-ical.php new file mode 100644 index 000000000..507a38f6d
- + 1 <?php 2 /* 3 Plugin Name: WPORG Make Homepage Meeting iCalendar API 4 Description: Provides API endpoints for iCalendar files that are generated dynamically from WPORG Meetings 5 Version: 1.0.0 6 License: GPLv2 or later 7 Author: WordPress.org 8 Author URI: http://wordpress.org/ 9 Text Domain: wporg 10 */ 11 12 namespace WordPressdotorg\Meetings\Calendar; 13 14 use WordPressdotorg\Autoload; 15 16 if ( ! class_exists( '\WordPressdotorg\Autoload\Autoloader', false ) ) { 17 include __DIR__ . '/vendor/wordpressdotorg/autoload/class-autoloader.php'; 18 } 19 20 // Register an Autoloader for all files. 21 Autoload\register_class_path( __NAMESPACE__, __DIR__ . '/inc' ); 22 23 // Instantiate the Plugin. 24 Plugin::get_instance(); -
new file wordpress.org/public_html/wp-content/plugins/wporg-meeting-posttype/inc/class-plugin.php
diff --git wordpress.org/public_html/wp-content/plugins/wporg-meeting-posttype/inc/class-plugin.php wordpress.org/public_html/wp-content/plugins/wporg-meeting-posttype/inc/class-plugin.php new file mode 100644 index 000000000..496ddf41f
- + 1 <?php 2 3 4 namespace WordPressdotorg\Meetings\PostType; 5 6 use DateTime; 7 use Exception; 8 use WordPressdotorg\Meetings\Common\Meeting_Query; 9 10 class Plugin { 11 12 /** 13 * @var Plugin The singleton instance. 14 */ 15 private static $instance; 16 17 /** 18 * Returns always the same instance of this plugin. 19 * 20 * @return Plugin 21 */ 22 public static function get_instance() { 23 if ( ! ( self::$instance instanceof Plugin ) ) { 24 self::$instance = new Plugin(); 25 } 26 return self::$instance; 27 } 28 29 /** 30 * Instantiates a new Plugin object. 31 */ 32 private function __construct() { 33 add_action( 'plugins_loaded', array( $this, 'plugins_loaded' ) ); 34 } 35 36 public function plugins_loaded() { 37 add_action( 'init', array( $this, 'register_meeting_post_type' ) ); 38 add_action( 'save_post_meeting', array( $this, 'save_meta_boxes' ), 10, 2 ); 39 add_filter( 'pre_get_posts', array( $this, 'meeting_archive_page_query' ) ); 40 add_filter( 'the_posts', array( $this, 'meeting_set_next_meeting' ), 10, 2 ); 41 add_filter( 'manage_meeting_posts_columns', array( $this, 'meeting_add_custom_columns' ) ); 42 add_action( 'manage_meeting_posts_custom_column', array( $this, 'meeting_custom_columns' ), 10, 2 ); 43 add_action( 'admin_head', array( $this, 'meeting_column_width' ) ); 44 add_action( 'admin_bar_menu', array( $this, 'add_edit_meetings_item_to_admin_bar' ), 80 ); 45 add_action( 'wp_enqueue_scripts', array( $this, 'add_edit_meetings_icon_to_admin_bar' ) ); 46 add_shortcode( 'meeting_time', array( $this, 'meeting_time_shortcode' ) ); 47 } 48 49 public function meeting_column_width() { ?> 50 <style type="text/css"> 51 .column-team { width: 10em !important; overflow: hidden; } 52 #meeting-info .recurring label { padding-right: 10px; } 53 </style> 54 <?php 55 } 56 57 public function meeting_add_custom_columns( $columns ) { 58 $columns = array_slice( $columns, 0, 1, true ) 59 + array( 'team' => __( 'Team', 'wporg' ) ) 60 + array_slice( $columns, 1, null, true ); 61 return $columns; 62 } 63 64 public function meeting_custom_columns( $column, $post_id ) { 65 switch ( $column ) { 66 case 'team': 67 $team = get_post_meta( $post_id, 'team', true ); 68 echo esc_html( $team ); 69 break; 70 } 71 } 72 73 public function meeting_archive_page_query( $query ) { 74 if ( is_admin() || ! $query->is_main_query() || ! $query->is_post_type_archive( 'meeting' ) ) { 75 return; 76 } 77 // turn off paging on the archive page, to show all meetings in the table 78 $query->set( 'nopaging', true ); 79 80 // meta query to eliminate expired meetings from query 81 $query->set( 'meta_query', Meeting_Query::get_meta_query() ); 82 83 // WP doesn't understand CURDATE() and prepares it as a quoted string. Repair this: 84 add_filter( 85 'get_meta_sql', 86 function ( $sql ) { 87 return str_replace( "'CURDATE()'", 'CURDATE()', $sql ); 88 } 89 ); 90 91 } 92 93 public function meeting_set_next_meeting( $posts, $query ) { 94 if ( ! $query->is_post_type_archive( 'meeting' ) ) { 95 return $posts; 96 } 97 98 // for each entry, set a fake meta value to show the next date for recurring meetings 99 array_walk( 100 $posts, 101 function ( &$post ) { 102 if ( 'weekly' === $post->recurring || '1' === $post->recurring ) { 103 try { 104 // from the start date, advance the week until it's past now 105 $start = new DateTime( sprintf( '%s %s GMT', $post->start_date, $post->time ) ); 106 $next = $start; 107 // minus 30 minutes to account for currently ongoing meetings 108 $now = new DateTime( '-30 minutes' ); 109 110 if ( $next < $now ) { 111 $interval = $start->diff( $now ); 112 // add one to days to account for events that happened earlier today 113 $weekdiff = ceil( ( $interval->days + 1 ) / 7 ); 114 $next->modify( '+ ' . $weekdiff . ' weeks' ); 115 } 116 117 $post->next_date = $next->format( 'Y-m-d' ); 118 } catch ( Exception $e ) { 119 // if the datetime is invalid, then set the post->next_date to the start date instead 120 $post->next_date = $post->start_date; 121 } 122 } elseif ( 'biweekly' === $post->recurring ) { 123 try { 124 // advance the start date 2 weeks at a time until it's past now 125 $start = new DateTime( sprintf( '%s %s GMT', $post->start_date, $post->time ) ); 126 $next = $start; 127 // minus 30 minutes to account for currently ongoing meetings 128 $now = new DateTime( '-30 minutes' ); 129 130 while ( $next < $now ) { 131 $next->modify( '+2 weeks' ); 132 } 133 134 $post->next_date = $next->format( 'Y-m-d' ); 135 } catch ( Exception $e ) { 136 // if the datetime is invalid, then set the post->next_date to the start date instead 137 $post->next_date = $post->start_date; 138 } 139 } elseif ( 'occurrence' === $post->recurring ) { 140 try { 141 // advance the occurrence day in the current month until it's past now 142 $start = new DateTime( sprintf( '%s %s GMT', $post->start_date, $post->time ) ); 143 $next = $start; 144 // minus 30 minutes to account for currently ongoing meetings 145 $now = new DateTime( '-30 minutes' ); 146 147 $day_index = date( 'w', strtotime( sprintf( '%s %s GMT', $post->start_date, $post->time ) ) ); 148 $day_name = $GLOBALS['wp_locale']->get_weekday( $day_index ); 149 $numerals = array( 'first', 'second', 'third', 'fourth' ); 150 $months = array( 'this month', 'next month' ); 151 152 foreach ( $months as $month ) { 153 foreach ( $post->occurrence as $index ) { 154 $next = new DateTime( sprintf( '%s %s of %s %s GMT', $numerals[ $index - 1 ], $day_name, $month, $post->time ) ); 155 if ( $next > $now ) { 156 break 2; 157 } 158 } 159 } 160 161 $post->next_date = $next->format( 'Y-m-d' ); 162 } catch ( Exception $e ) { 163 // if the datetime is invalid, then set the post->next_date to the start date instead 164 $post->next_date = $post->start_date; 165 } 166 } elseif ( 'monthly' === $post->recurring ) { 167 try { 168 // advance the start date 1 month at a time until it's past now 169 $start = new DateTime( sprintf( '%s %s GMT', $post->start_date, $post->time ) ); 170 $next = $start; 171 // minus 30 minutes to account for currently ongoing meetings 172 $now = new DateTime( '-30 minutes' ); 173 174 while ( $next < $now ) { 175 $next->modify( '+1 month' ); 176 } 177 178 $post->next_date = $next->format( 'Y-m-d' ); 179 } catch ( Exception $e ) { 180 // if the datetime is invalid, then set the post->next_date to the start date instead 181 $post->next_date = $post->start_date; 182 } 183 } else { 184 $post->next_date = $post->start_date; 185 } 186 } 187 ); 188 189 // reorder the posts by next_date + time 190 usort( 191 $posts, 192 function ( $a, $b ) { 193 $adate = strtotime( $a->next_date . ' ' . $a->time ); 194 $bdate = strtotime( $b->next_date . ' ' . $b->time ); 195 if ( $adate == $bdate ) { 196 return 0; 197 } 198 return ( $adate < $bdate ) ? -1 : 1; 199 } 200 ); 201 202 return $posts; 203 } 204 205 public function register_meeting_post_type() { 206 $labels = array( 207 'name' => _x( 'Meetings', 'Post Type General Name', 'wporg' ), 208 'singular_name' => _x( 'Meeting', 'Post Type Singular Name', 'wporg' ), 209 'menu_name' => __( 'Meetings', 'wporg' ), 210 'name_admin_bar' => __( 'Meeting', 'wporg' ), 211 'parent_item_colon' => __( 'Parent Meeting:', 'wporg' ), 212 'all_items' => __( 'All Meetings', 'wporg' ), 213 'add_new_item' => __( 'Add New Meeting', 'wporg' ), 214 'add_new' => __( 'Add New', 'wporg' ), 215 'new_item' => __( 'New Meeting', 'wporg' ), 216 'edit_item' => __( 'Edit Meeting', 'wporg' ), 217 'update_item' => __( 'Update Meeting', 'wporg' ), 218 'view_item' => __( 'View Meeting', 'wporg' ), 219 'view_items' => __( 'View Meetings', 'wporg' ), 220 'search_items' => __( 'Search Meeting', 'wporg' ), 221 'not_found' => __( 'Not found', 'wporg' ), 222 'not_found_in_trash' => __( 'Not found in Trash', 'wporg' ), 223 ); 224 $args = array( 225 'label' => __( 'meeting', 'wporg' ), 226 'description' => __( 'Meeting', 'wporg' ), 227 'labels' => $labels, 228 'supports' => array( 'title' ), 229 'hierarchical' => false, 230 'public' => true, 231 'show_ui' => true, 232 'show_in_menu' => true, 233 'menu_position' => 20, 234 'menu_icon' => 'dashicons-calendar', 235 'show_in_admin_bar' => true, 236 'show_in_nav_menus' => false, 237 'can_export' => false, 238 'has_archive' => true, 239 'exclude_from_search' => true, 240 'publicly_queryable' => true, 241 'capability_type' => 'post', 242 'register_meta_box_cb' => array( $this, 'add_meta_boxes' ), 243 'rewrite' => array( 244 'with_front' => false, 245 'slug' => __( 'meetings', 'wporg' ), 246 ), 247 ); 248 register_post_type( 'meeting', $args ); 249 } 250 251 public function add_meta_boxes() { 252 add_meta_box( 253 'meeting-info', 254 'Meeting Info', 255 array( $this, 'render_meta_boxes' ), 256 'meeting', 257 'normal', 258 'high' 259 ); 260 } 261 262 function render_meta_boxes( $post ) { 263 wp_enqueue_script( 'jquery-ui-datepicker' ); 264 wp_enqueue_style( 'jquery-ui-style', 'https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/themes/smoothness/jquery-ui.css', true ); 265 266 $meta = get_post_custom( $post->ID ); 267 $team = isset( $meta['team'][0] ) ? $meta['team'][0] : ''; 268 $start = isset( $meta['start_date'][0] ) ? $meta['start_date'][0] : ''; 269 $end = isset( $meta['end_date'][0] ) ? $meta['end_date'][0] : ''; 270 $time = isset( $meta['time'][0] ) ? $meta['time'][0] : ''; 271 $recurring = isset( $meta['recurring'][0] ) ? $meta['recurring'][0] : ''; 272 if ( '1' === $recurring ) { 273 $recurring = 'weekly'; 274 } 275 $occurrence = isset( $meta['occurrence'][0] ) ? unserialize( $meta['occurrence'][0] ) : array(); 276 $link = isset( $meta['link'][0] ) ? $meta['link'][0] : ''; 277 $location = isset( $meta['location'][0] ) ? $meta['location'][0] : ''; 278 wp_nonce_field( 'save_meeting_meta_' . $post->ID, 'meeting_nonce' ); 279 ?> 280 281 <p> 282 <label for="team"> 283 <?php _e( 'Team: ', 'wporg' ); ?> 284 <input type="text" id="team" name="team" class="regular-text wide" value="<?php echo esc_attr( $team ); ?>"> 285 </label> 286 </p> 287 <p> 288 <label for="start_date"> 289 <?php _e( 'Start Date', 'wporg' ); ?> 290 <input type="text" name="start_date" id="start_date" class="date" value="<?php echo esc_attr( $start ); ?>"> 291 </label> 292 <label for="end_date"> 293 <?php _e( 'End Date', 'wporg' ); ?> 294 <input type="text" name="end_date" id="end_date" class="date" value="<?php echo esc_attr( $end ); ?>"> 295 </label> 296 </p> 297 <p> 298 <label for="time"> 299 <?php _e( 'Time (UTC)', 'wporg' ); ?> 300 <input type="text" name="time" id="time" class="time" value="<?php echo esc_attr( $time ); ?>"> 301 </label> 302 </p> 303 <p class="recurring"> 304 <?php _e( 'Recurring: ', 'wporg' ); ?><br /> 305 <label for="weekly"> 306 <input type="radio" name="recurring" value="weekly" id="weekly" class="regular-radio" <?php checked( $recurring, 'weekly' ); ?>> 307 <?php _e( 'Weekly', 'wporg' ); ?> 308 </label><br /> 309 310 <label for="biweekly"> 311 <input type="radio" name="recurring" value="biweekly" id="biweekly" class="regular-radio" <?php checked( $recurring, 'biweekly' ); ?>> 312 <?php _e( 'Biweekly', 'wporg' ); ?> 313 </label><br /> 314 315 <label for="occurrence"> 316 <input type="radio" name="recurring" value="occurrence" id="occurrence" class="regular-radio" <?php checked( $recurring, 'occurrence' ); ?>> 317 <?php _e( 'Occurrence in a month:', 'wporg' ); ?> 318 </label> 319 <label for="week-1"> 320 <input type="checkbox" name="occurrence[]" value="1" id="week-1" <?php checked( in_array( 1, $occurrence ) ); ?>> 321 <?php _e( '1st', 'wporg' ); ?> 322 </label> 323 <label for="week-2"> 324 <input type="checkbox" name="occurrence[]" value="2" id="week-2" <?php checked( in_array( 2, $occurrence ) ); ?>> 325 <?php _e( '2nd', 'wporg' ); ?> 326 </label> 327 <label for="week-3"> 328 <input type="checkbox" name="occurrence[]" value="3" id="week-3" <?php checked( in_array( 3, $occurrence ) ); ?>> 329 <?php _e( '3rd', 'wporg' ); ?> 330 </label> 331 <label for="week-4"> 332 <input type="checkbox" name="occurrence[]" value="4" id="week-4" <?php checked( in_array( 4, $occurrence ) ); ?>> 333 <?php _e( '4th', 'wporg' ); ?> 334 </label><br /> 335 336 <label for="monthly"> 337 <input type="radio" name="recurring" value="monthly" id="monthly" class="regular-radio" <?php checked( $recurring, 'monthly' ); ?>> 338 <?php _e( 'Monthly', 'wporg' ); ?> 339 </label> 340 </p> 341 <p> 342 <label for="link"><?php _e( 'Link: ', 'wporg' ); ?> 343 <input type="text" name="link" id="link" class="regular-text wide" value="<?php echo esc_url( $link ); ?>"> 344 </label> 345 </p> 346 <p> 347 <label for="location"><?php _e( 'Location: ', 'wporg' ); ?> 348 <input type="text" name="location" id="location" class="regular-text wide" value="<?php echo esc_attr( $location ); ?>"> 349 </label> 350 </p> 351 <script> 352 jQuery(document).ready( function($) { 353 $('.date').datepicker({ 354 dateFormat: 'yy-mm-dd' 355 }); 356 357 $('input[name="recurring"]').change( function() { 358 var disabled = ( 'occurrence' !== $(this).val() ); 359 $('#meeting-info').find('[name^="occurrence"]').prop('disabled', disabled); 360 }); 361 362 if ( 'occurrence' !== $('input[name="recurring"]:checked').val() ) { 363 $('#meeting-info').find('[name^="occurrence"]').prop('disabled', true); 364 } 365 }); 366 </script> 367 <?php 368 } 369 370 function save_meta_boxes( $post_id ) { 371 372 global $post; 373 374 // Verify nonce 375 if ( ! isset( $_POST['meeting_nonce'] ) || ! wp_verify_nonce( $_POST['meeting_nonce'], 'save_meeting_meta_' . $post_id ) ) { 376 return $post_id; 377 } 378 379 // Check autosave 380 if ( ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) || ( defined( 'DOING_AJAX' ) && DOING_AJAX ) || isset( $_REQUEST['bulk_edit'] ) ) { 381 return $post_id; 382 } 383 384 // Don't save for revisions 385 if ( isset( $post->post_type ) && 'revision' === $post->post_type ) { 386 return $post_id; 387 } 388 389 // Check permissions 390 if ( ! current_user_can( 'edit_post', $post->ID ) ) { 391 return $post_id; 392 } 393 394 $meta['team'] = ( isset( $_POST['team'] ) ? esc_textarea( $_POST['team'] ) : '' ); 395 $meta['start_date'] = ( isset( $_POST['start_date'] ) ? esc_textarea( $_POST['start_date'] ) : '' ); 396 $meta['end_date'] = ( isset( $_POST['end_date'] ) ? esc_textarea( $_POST['end_date'] ) : '' ); 397 $meta['time'] = ( isset( $_POST['time'] ) ? esc_textarea( $_POST['time'] ) : '' ); 398 $meta['recurring'] = ( isset( $_POST['recurring'] ) 399 && in_array( $_POST['recurring'], array( 'weekly', 'biweekly', 'occurrence', 'monthly' ) ) 400 ? ( $_POST['recurring'] ) : '' ); 401 $meta['occurrence'] = ( isset( $_POST['occurrence'] ) && 'occurrence' === $meta['recurring'] 402 && is_array( $_POST['occurrence'] ) 403 ? array_map( 'intval', $_POST['occurrence'] ) : array() ); 404 $meta['link'] = ( isset( $_POST['link'] ) ? esc_url( $_POST['link'] ) : '' ); 405 $meta['location'] = ( isset( $_POST['location'] ) ? esc_textarea( $_POST['location'] ) : '' ); 406 407 foreach ( $meta as $key => $value ) { 408 update_post_meta( $post->ID, $key, $value ); 409 } 410 } 411 412 /** 413 * Adds "Edit Meetings" item after "Add New" menu. 414 * 415 * @param \WP_Admin_Bar $wp_admin_bar The admin bar instance. 416 */ 417 public function add_edit_meetings_item_to_admin_bar( $wp_admin_bar ) { 418 if ( ! current_user_can( 'edit_posts' ) ) { 419 return; 420 } 421 422 if ( is_admin() || ! is_post_type_archive( 'meeting' ) ) { 423 return; 424 } 425 426 $wp_admin_bar->add_menu( 427 array( 428 'id' => 'edit-meetings', 429 'title' => '<span class="ab-icon"></span>' . __( 'Edit Meetings', 'wporg' ), 430 'href' => admin_url( 'edit.php?post_type=meeting' ), 431 ) 432 ); 433 } 434 435 /** 436 * Adds icon for the "Edit Meetings" item. 437 */ 438 public function add_edit_meetings_icon_to_admin_bar() { 439 if ( ! current_user_can( 'edit_posts' ) ) { 440 return; 441 } 442 443 wp_add_inline_style( 444 'admin-bar', 445 ' 446 #wpadminbar #wp-admin-bar-edit-meetings .ab-icon:before { 447 content: "\f145"; 448 top: 2px; 449 } 450 ' 451 ); 452 } 453 454 /** 455 * Renders meeting information with the next meeting time based on user's local timezone. Used in Make homepage. 456 */ 457 public function meeting_time_shortcode( $attr, $content = '' ) { 458 459 $attr = shortcode_atts( 460 array( 461 'team' => null, 462 'limit' => 1, 463 'before' => __( 'Next meeting: ', 'wporg' ), 464 'titletag' => 'strong', 465 'more' => true, 466 ), 467 $attr 468 ); 469 470 if ( empty( $attr['team'] ) ) { 471 return ''; 472 } 473 474 if ( $attr['team'] === 'Documentation' ) { 475 $attr['team'] = 'Docs'; 476 } 477 478 if ( ! has_action( 'wp_footer', array( $this, 'time_conversion_script' ) ) ) { 479 add_action( 'wp_footer', array( $this, 'time_conversion_script' ), 999 ); 480 } 481 482 $query = new Meeting_Query( $attr['team'] ); 483 $posts = $query->get_posts(); 484 485 $limit = $attr['limit'] > 0 ? $attr['limit'] : count( $posts ); 486 487 $out = ''; 488 foreach ( array_slice( $posts, 0, $limit ) as $post ) { 489 $next_meeting_datestring = $post->next_date; 490 $utc_time = strftime( '%H:%M:%S', strtotime( $post->time ) ); 491 $next_meeting_iso = $next_meeting_datestring . 'T' . $utc_time . '+00:00'; 492 $next_meeting_timestamp = strtotime( $next_meeting_datestring . ' ' . $utc_time ); 493 $next_meeting_display = strftime( '%c %Z', $next_meeting_timestamp ); 494 495 $slack_channel = null; 496 if ( $post->location && preg_match( '/^#([-\w]+)$/', trim( $post->location ), $match ) ) { 497 $slack_channel = sanitize_title( $match[1] ); 498 } 499 500 $out .= '<p>'; 501 $out .= esc_html( $attr['before'] ); 502 $out .= '<strong class="meeting-title">' . esc_html( $post->post_title ) . '</strong>'; 503 $display_more = $query->found_posts - intval( $limit ); 504 if ( $display_more > 0 ) { 505 $out .= ' <a title="Click to view all meetings for this team" href="/meetings/#' . esc_attr( strtolower( $attr['team'] ) ) . '">' . sprintf( __( '(+%s more)' ), $display_more ) . '</a>'; 506 } 507 $out .= '</br>'; 508 $out .= '<time class="date" date-time="' . esc_attr( $next_meeting_iso ) . '" title="' . esc_attr( $next_meeting_iso ) . '">' . $next_meeting_display . '</time> '; 509 $out .= sprintf( esc_html__( '(%s from now)' ), human_time_diff( $next_meeting_timestamp, current_time( 'timestamp' ) ) ); 510 if ( $post->location && $slack_channel ) { 511 $out .= ' ' . sprintf( wp_kses( __( 'at <a href="%1$s">%2$s</a> on Slack' ), array( 'a' => array( 'href' => array() ) ) ), 'https://wordpress.slack.com/messages/' . $slack_channel, $post->location ); 512 } 513 $out .= '</p>'; 514 } 515 516 return $out; 517 } 518 519 public function time_conversion_script() { 520 echo <<<EOF 521 <script type="text/javascript"> 522 523 var parse_date = function (text) { 524 var m = /^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})\+00:00$/.exec(text); 525 var d = new Date(); 526 d.setUTCFullYear(+m[1]); 527 d.setUTCDate(+m[3]); 528 d.setUTCMonth(+m[2]-1); 529 d.setUTCHours(+m[4]); 530 d.setUTCMinutes(+m[5]); 531 d.setUTCSeconds(+m[6]); 532 return d; 533 } 534 var format_time = function (d) { 535 return d.toLocaleTimeString(navigator.language, {weekday: 'long', hour: '2-digit', minute: '2-digit', timeZoneName: 'short'}); 536 } 537 538 var nodes = document.getElementsByTagName('time'); 539 for (var i=0; i<nodes.length; ++i) { 540 var node = nodes[i]; 541 if (node.className === 'date') { 542 var d = parse_date(node.getAttribute('date-time')); 543 if (d) { 544 node.textContent = format_time(d); 545 } 546 } 547 } 548 </script> 549 EOF; 550 } 551 } -
new file wordpress.org/public_html/wp-content/plugins/wporg-meeting-posttype/inc/common/class-meeting-query.php
diff --git wordpress.org/public_html/wp-content/plugins/wporg-meeting-posttype/inc/common/class-meeting-query.php wordpress.org/public_html/wp-content/plugins/wporg-meeting-posttype/inc/common/class-meeting-query.php new file mode 100644 index 000000000..76fdf35eb
- + 1 <?php 2 3 namespace WordPressdotorg\Meetings\Common; 4 5 use WP_Query; 6 7 class Meeting_Query { 8 9 /** 10 * @var WP_Query 11 */ 12 private $query; 13 14 public function __construct( $team ) { 15 $this->query = new WP_Query(); 16 $this->build_query( $team ); 17 } 18 19 public static function get_meta_query() { 20 return array( 21 'relation' => 'OR', 22 // not recurring AND start_date >= CURDATE() = one-time meeting today or still in future 23 array( 24 'relation' => 'AND', 25 array( 26 'key' => 'recurring', 27 'value' => array( 'weekly', 'biweekly', 'occurrence', 'monthly', '1' ), 28 'compare' => 'NOT IN', 29 ), 30 array( 31 'key' => 'start_date', 32 'type' => 'DATE', 33 'compare' => '>=', 34 'value' => 'CURDATE()', 35 ), 36 ), 37 // recurring = 1 AND ( end_date = '' OR end_date > CURDATE() ) = recurring meeting that has no end or has not ended yet 38 array( 39 'relation' => 'AND', 40 array( 41 'key' => 'recurring', 42 'value' => array( 'weekly', 'biweekly', 'occurrence', 'monthly', '1' ), 43 'compare' => 'IN', 44 ), 45 array( 46 'relation' => 'OR', 47 array( 48 'key' => 'end_date', 49 'value' => '', 50 'compare' => '=', 51 ), 52 array( 53 'key' => 'end_date', 54 'type' => 'DATE', 55 'compare' => '>', 56 'value' => 'CURDATE()', 57 ), 58 ), 59 ), 60 ); 61 } 62 63 public function get_posts() { 64 return $this->query->get_posts(); 65 } 66 67 private function build_query( $team ) { 68 // meta query to eliminate expired meetings from query 69 add_filter( 70 'get_meta_sql', 71 function ( $sql ) { 72 return str_replace( "'CURDATE()'", 'CURDATE()', $sql ); 73 } 74 ); 75 76 switch_to_blog( get_main_site_id() ); 77 78 $this->query->set( 'post_type', 'meeting' ); 79 $this->query->set( 'nopaging', true ); 80 $this->query->set( 81 'meta_query', 82 array( 83 'relation' => 'AND', 84 array( 85 'key' => 'team', 86 'value' => $team, 87 'compare' => empty( $team ) ? '!=' : '=', 88 ), 89 self::get_meta_query(), 90 ) 91 ); 92 93 restore_current_blog(); 94 } 95 } -
new file wordpress.org/public_html/wp-content/plugins/wporg-meeting-posttype/vendor/wordpressdotorg/autoload/class-autoloader.php
diff --git wordpress.org/public_html/wp-content/plugins/wporg-meeting-posttype/vendor/wordpressdotorg/autoload/class-autoloader.php wordpress.org/public_html/wp-content/plugins/wporg-meeting-posttype/vendor/wordpressdotorg/autoload/class-autoloader.php new file mode 100644 index 000000000..8c2258ba3
- + 1 <?php 2 namespace WordPressdotorg\Autoload; 3 4 /** 5 * An Autoloader which respects WordPress's filename standards. 6 * 7 * @package WordPressdotorg\Autoload 8 */ 9 class Autoloader { 10 11 /** 12 * Namespace separator. 13 */ 14 const NS_SEPARATOR = '\\'; 15 16 /** 17 * The prefix to compare classes against. 18 * 19 * @var string 20 * @access protected 21 */ 22 protected $prefix; 23 24 /** 25 * Length of the prefix string. 26 * 27 * @var int 28 * @access protected 29 */ 30 protected $prefix_length; 31 32 /** 33 * Path to the file to be loaded. 34 * 35 * @var string 36 * @access protected 37 */ 38 protected $path; 39 40 /** 41 * Constructor. 42 * 43 * @param string $prefix Prefix all classes have in common. 44 * @param string $path Path to the files to be loaded. 45 */ 46 public function __construct( $prefix, $path ) { 47 $this->prefix = $prefix; 48 $this->prefix_length = strlen( $prefix ); 49 $this->path = trailingslashit( $path ); 50 } 51 52 /** 53 * Loads a class if it starts with `$this->prefix`. 54 * 55 * @param string $class The class to be loaded. 56 */ 57 public function load( $class ) { 58 if ( strpos( $class, $this->prefix . self::NS_SEPARATOR ) !== 0 ) { 59 return; 60 } 61 62 // Strip prefix from the start (ala PSR-4) 63 $class = substr( $class, $this->prefix_length + 1 ); 64 $class = strtolower( $class ); 65 $file = ''; 66 67 if ( false !== ( $last_ns_pos = strripos( $class, self::NS_SEPARATOR ) ) ) { 68 $namespace = substr( $class, 0, $last_ns_pos ); 69 $namespace = str_replace( '_', '-', $namespace ); 70 $class = substr( $class, $last_ns_pos + 1 ); 71 $file = str_replace( self::NS_SEPARATOR, DIRECTORY_SEPARATOR, $namespace ) . DIRECTORY_SEPARATOR; 72 } 73 74 $file .= 'class-' . str_replace( '_', '-', $class ) . '.php'; 75 76 $path = $this->path . $file; 77 78 if ( file_exists( $path ) ) { 79 require $path; 80 } 81 } 82 } 83 84 /** 85 * Registers Autoloader's autoload function. 86 * 87 * @param string $prefix 88 * @param string $path 89 */ 90 function register_class_path( $prefix, $path ) { 91 $loader = new Autoloader( $prefix, $path ); 92 spl_autoload_register( array( $loader, 'load' ) ); 93 } -
wordpress.org/public_html/wp-content/plugins/wporg-meeting-posttype/wporg-meeting-posttype.php
diff --git wordpress.org/public_html/wp-content/plugins/wporg-meeting-posttype/wporg-meeting-posttype.php wordpress.org/public_html/wp-content/plugins/wporg-meeting-posttype/wporg-meeting-posttype.php index 9dd64b010..0b58b2ce5 100644
2 2 /* 3 3 Plugin Name: WPORG Make Homepage Meeting Post Type 4 4 Description: Creates the meeting post type and assorted filters for https://make.wordpress.org/meetings 5 Version: 1.0 5 Version: 1.0.1 6 6 License: GPLv2 or later 7 7 Author: WordPress.org 8 8 Author URI: http://wordpress.org/ 9 9 Text Domain: wporg 10 10 */ 11 11 12 if ( !class_exists('Meeting_Post_Type') ): 13 class Meeting_Post_Type { 12 namespace WordPressdotorg\Meetings\PostType; 14 13 15 protected static $instance = NULL;14 use WordPressdotorg\Autoload; 16 15 17 public static function getInstance() { 18 NULL === self::$instance and self::$instance = new self; 19 return self::$instance; 20 } 21 22 public static function init() { 23 $mpt = Meeting_Post_Type::getInstance(); 24 add_action( 'init', array( $mpt, 'register_meeting_post_type' ) ); 25 add_action( 'save_post_meeting', array( $mpt, 'save_meta_boxes' ), 10, 2 ); 26 add_filter( 'pre_get_posts', array( $mpt, 'meeting_archive_page_query' ) ); 27 add_filter( 'the_posts', array( $mpt, 'meeting_set_next_meeting' ), 10, 2 ); 28 add_filter( 'manage_meeting_posts_columns', array( $mpt, 'meeting_add_custom_columns' ) ); 29 add_action( 'manage_meeting_posts_custom_column', array( $mpt, 'meeting_custom_columns' ), 10, 2 ); 30 add_action( 'admin_head', array( $mpt, 'meeting_column_width' ) ); 31 add_action( 'admin_bar_menu', array( $mpt, 'add_edit_meetings_item_to_admin_bar' ), 80 ); 32 add_action( 'wp_enqueue_scripts', array( $mpt, 'add_edit_meetings_icon_to_admin_bar' ) ); 33 add_shortcode( 'meeting_time', array( $mpt, 'meeting_time_shortcode' ) ); 34 } 35 36 public function meeting_column_width() { ?> 37 <style type="text/css"> 38 .column-team { width: 10em !important; overflow: hidden; } 39 #meeting-info .recurring label { padding-right: 10px; } 40 </style> 41 <?php 42 } 43 44 public function meeting_add_custom_columns( $columns ) { 45 $columns = array_slice( $columns, 0, 1, true ) 46 + array( 'team' => __('Team', 'wporg') ) 47 + array_slice( $columns, 1, null, true ); 48 return $columns; 49 } 50 51 public function meeting_custom_columns( $column, $post_id ) { 52 switch ( $column ) { 53 case 'team' : 54 $team = get_post_meta( $post_id, 'team', true ); 55 echo esc_html( $team ); 56 break; 57 } 58 } 59 60 public function meeting_archive_page_query( $query ) { 61 if ( is_admin() || ! $query->is_main_query() || ! $query->is_post_type_archive( 'meeting' ) ) { 62 return; 63 } 64 // turn off paging on the archive page, to show all meetings in the table 65 $query->set( 'nopaging', true ); 66 67 // meta query to eliminate expired meetings from query 68 $query->set( 'meta_query', $this->meeting_meta_query ); 69 70 // WP doesn't understand CURDATE() and prepares it as a quoted string. Repair this: 71 add_filter( 'get_meta_sql', function ($sql) { 72 return str_replace( "'CURDATE()'", 'CURDATE()', $sql ); 73 } ); 74 75 } 76 77 public function meeting_set_next_meeting( $posts, $query ) { 78 if ( !$query->is_post_type_archive( 'meeting' ) ) { 79 return $posts; 80 } 81 82 // for each entry, set a fake meta value to show the next date for recurring meetings 83 array_walk( $posts, function ( &$post ) { 84 if ( 'weekly' === $post->recurring || '1' === $post->recurring ) { 85 try { 86 // from the start date, advance the week until it's past now 87 $start = new DateTime( sprintf( '%s %s GMT', $post->start_date, $post->time ) ); 88 $next = $start; 89 // minus 30 minutes to account for currently ongoing meetings 90 $now = new DateTime( '-30 minutes' ); 91 92 if ( $next < $now ) { 93 $interval = $start->diff( $now ); 94 // add one to days to account for events that happened earlier today 95 $weekdiff = ceil( ( $interval->days + 1 ) / 7 ); 96 $next->modify( '+ ' . $weekdiff . ' weeks' ); 97 } 98 99 $post->next_date = $next->format( 'Y-m-d' ); 100 } catch ( Exception $e ) { 101 // if the datetime is invalid, then set the post->next_date to the start date instead 102 $post->next_date = $post->start_date; 103 } 104 } else if ( 'biweekly' === $post->recurring ) { 105 try { 106 // advance the start date 2 weeks at a time until it's past now 107 $start = new DateTime( sprintf( '%s %s GMT', $post->start_date, $post->time ) ); 108 $next = $start; 109 // minus 30 minutes to account for currently ongoing meetings 110 $now = new DateTime( '-30 minutes' ); 111 112 while ( $next < $now ) { 113 $next->modify( '+2 weeks' ); 114 } 115 116 $post->next_date = $next->format( 'Y-m-d' ); 117 } catch ( Exception $e ) { 118 // if the datetime is invalid, then set the post->next_date to the start date instead 119 $post->next_date = $post->start_date; 120 } 121 } else if ( 'occurrence' === $post->recurring ) { 122 try { 123 // advance the occurrence day in the current month until it's past now 124 $start = new DateTime( sprintf( '%s %s GMT', $post->start_date, $post->time ) ); 125 $next = $start; 126 // minus 30 minutes to account for currently ongoing meetings 127 $now = new DateTime( '-30 minutes' ); 128 129 $day_index = date( 'w', strtotime( sprintf( '%s %s GMT', $post->start_date, $post->time ) ) ); 130 $day_name = $GLOBALS['wp_locale']->get_weekday( $day_index ); 131 $numerals = array( 'first', 'second', 'third', 'fourth' ); 132 $months = array( 'this month', 'next month' ); 133 134 foreach ( $months as $month ) { 135 foreach ( $post->occurrence as $index ) { 136 $next = new DateTime( sprintf( '%s %s of %s %s GMT', $numerals[ $index - 1 ], $day_name, $month, $post->time ) ); 137 if ( $next > $now ) { 138 break 2; 139 } 140 } 141 } 142 143 $post->next_date = $next->format( 'Y-m-d' ); 144 } catch ( Exception $e ) { 145 // if the datetime is invalid, then set the post->next_date to the start date instead 146 $post->next_date = $post->start_date; 147 } 148 } else if ( 'monthly' === $post->recurring ) { 149 try { 150 // advance the start date 1 month at a time until it's past now 151 $start = new DateTime( sprintf( '%s %s GMT', $post->start_date, $post->time ) ); 152 $next = $start; 153 // minus 30 minutes to account for currently ongoing meetings 154 $now = new DateTime( '-30 minutes' ); 155 156 while ( $next < $now ) { 157 $next->modify( '+1 month' ); 158 } 159 160 $post->next_date = $next->format( 'Y-m-d' ); 161 } catch ( Exception $e ) { 162 // if the datetime is invalid, then set the post->next_date to the start date instead 163 $post->next_date = $post->start_date; 164 } 165 } else { 166 $post->next_date = $post->start_date; 167 } 168 }); 169 170 // reorder the posts by next_date + time 171 usort( $posts, function ($a, $b) { 172 $adate = strtotime( $a->next_date . ' ' . $a->time ); 173 $bdate = strtotime( $b->next_date . ' ' . $b->time ); 174 if ( $adate == $bdate ) { 175 return 0; 176 } 177 return ( $adate < $bdate ) ? -1 : 1; 178 }); 179 180 return $posts; 181 } 182 183 public function register_meeting_post_type() { 184 $labels = array( 185 'name' => _x( 'Meetings', 'Post Type General Name', 'wporg' ), 186 'singular_name' => _x( 'Meeting', 'Post Type Singular Name', 'wporg' ), 187 'menu_name' => __( 'Meetings', 'wporg' ), 188 'name_admin_bar' => __( 'Meeting', 'wporg' ), 189 'parent_item_colon' => __( 'Parent Meeting:', 'wporg' ), 190 'all_items' => __( 'All Meetings', 'wporg' ), 191 'add_new_item' => __( 'Add New Meeting', 'wporg' ), 192 'add_new' => __( 'Add New', 'wporg' ), 193 'new_item' => __( 'New Meeting', 'wporg' ), 194 'edit_item' => __( 'Edit Meeting', 'wporg' ), 195 'update_item' => __( 'Update Meeting', 'wporg' ), 196 'view_item' => __( 'View Meeting', 'wporg' ), 197 'view_items' => __( 'View Meetings', 'wporg' ), 198 'search_items' => __( 'Search Meeting', 'wporg' ), 199 'not_found' => __( 'Not found', 'wporg' ), 200 'not_found_in_trash' => __( 'Not found in Trash', 'wporg' ), 201 ); 202 $args = array( 203 'label' => __( 'meeting', 'wporg' ), 204 'description' => __( 'Meeting', 'wporg' ), 205 'labels' => $labels, 206 'supports' => array( 'title' ), 207 'hierarchical' => false, 208 'public' => true, 209 'show_ui' => true, 210 'show_in_menu' => true, 211 'menu_position' => 20, 212 'menu_icon' => 'dashicons-calendar', 213 'show_in_admin_bar' => true, 214 'show_in_nav_menus' => false, 215 'can_export' => false, 216 'has_archive' => true, 217 'exclude_from_search' => true, 218 'publicly_queryable' => true, 219 'capability_type' => 'post', 220 'register_meta_box_cb'=> array( $this, 'add_meta_boxes' ), 221 'rewrite' => array( 222 'with_front' => false, 223 'slug' => __( 'meetings', 'wporg' ), 224 ), 225 ); 226 register_post_type( 'meeting', $args ); 227 } 228 229 public function add_meta_boxes() { 230 add_meta_box( 231 'meeting-info', 232 'Meeting Info', 233 array( $this, 'render_meta_boxes' ), 234 'meeting', 235 'normal', 236 'high' 237 ); 238 } 239 240 function render_meta_boxes( $post ) { 241 wp_enqueue_script( 'jquery-ui-datepicker' ); 242 wp_enqueue_style( 'jquery-ui-style', 'https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/themes/smoothness/jquery-ui.css', true); 243 244 $meta = get_post_custom( $post->ID ); 245 $team = isset( $meta['team'][0] ) ? $meta['team'][0] : ''; 246 $start = isset( $meta['start_date'][0] ) ? $meta['start_date'][0] : ''; 247 $end = isset( $meta['end_date'][0] ) ? $meta['end_date'][0] : ''; 248 $time = isset( $meta['time'][0] ) ? $meta['time'][0] : ''; 249 $recurring = isset( $meta['recurring'][0] ) ? $meta['recurring'][0] : ''; 250 if ( '1' === $recurring ) { 251 $recurring = 'weekly'; 252 } 253 $occurrence = isset( $meta['occurrence'][0] ) ? unserialize( $meta['occurrence'][0] ) : array(); 254 $link = isset( $meta['link'][0] ) ? $meta['link'][0] : ''; 255 $location = isset( $meta['location'][0] ) ? $meta['location'][0] : ''; 256 wp_nonce_field( 'save_meeting_meta_'.$post->ID , 'meeting_nonce' ); 257 ?> 258 259 <p> 260 <label for="team"> 261 <?php _e( 'Team: ', 'wporg' ); ?> 262 <input type="text" id="team" name="team" class="regular-text wide" value="<?php echo esc_attr( $team ); ?>"> 263 </label> 264 </p> 265 <p> 266 <label for="start_date"> 267 <?php _e( 'Start Date', 'wporg' ); ?> 268 <input type="text" name="start_date" id="start_date" class="date" value="<?php echo esc_attr( $start ); ?>"> 269 </label> 270 <label for="end_date"> 271 <?php _e( 'End Date', 'wporg' ); ?> 272 <input type="text" name="end_date" id="end_date" class="date" value="<?php echo esc_attr( $end ); ?>"> 273 </label> 274 </p> 275 <p> 276 <label for="time"> 277 <?php _e( 'Time (UTC)', 'wporg' ); ?> 278 <input type="text" name="time" id="time" class="time" value="<?php echo esc_attr( $time ); ?>"> 279 </label> 280 </p> 281 <p class="recurring"> 282 <?php _e( 'Recurring: ', 'wporg' ); ?><br /> 283 <label for="weekly"> 284 <input type="radio" name="recurring" value="weekly" id="weekly" class="regular-radio" <?php checked( $recurring, 'weekly' ); ?>> 285 <?php _e( 'Weekly', 'wporg' ); ?> 286 </label><br /> 287 288 <label for="biweekly"> 289 <input type="radio" name="recurring" value="biweekly" id="biweekly" class="regular-radio" <?php checked( $recurring, 'biweekly' ); ?>> 290 <?php _e( 'Biweekly', 'wporg' ); ?> 291 </label><br /> 292 293 <label for="occurrence"> 294 <input type="radio" name="recurring" value="occurrence" id="occurrence" class="regular-radio" <?php checked( $recurring, 'occurrence' ); ?>> 295 <?php _e( 'Occurrence in a month:', 'wporg' ); ?> 296 </label> 297 <label for="week-1"> 298 <input type="checkbox" name="occurrence[]" value="1" id="week-1" <?php checked( in_array( 1, $occurrence ) ); ?>> 299 <?php _e( '1st', 'wporg' ); ?> 300 </label> 301 <label for="week-2"> 302 <input type="checkbox" name="occurrence[]" value="2" id="week-2" <?php checked( in_array( 2, $occurrence ) ); ?>> 303 <?php _e( '2nd', 'wporg' ); ?> 304 </label> 305 <label for="week-3"> 306 <input type="checkbox" name="occurrence[]" value="3" id="week-3" <?php checked( in_array( 3, $occurrence ) ); ?>> 307 <?php _e( '3rd', 'wporg' ); ?> 308 </label> 309 <label for="week-4"> 310 <input type="checkbox" name="occurrence[]" value="4" id="week-4" <?php checked( in_array( 4, $occurrence ) ); ?>> 311 <?php _e( '4th', 'wporg' ); ?> 312 </label><br /> 313 314 <label for="monthly"> 315 <input type="radio" name="recurring" value="monthly" id="monthly" class="regular-radio" <?php checked( $recurring, 'monthly' ); ?>> 316 <?php _e( 'Monthly', 'wporg' ); ?> 317 </label> 318 </p> 319 <p> 320 <label for="link"><?php _e( 'Link: ', 'wporg' ); ?> 321 <input type="text" name="link" id="link" class="regular-text wide" value="<?php echo esc_url( $link ); ?>"> 322 </label> 323 </p> 324 <p> 325 <label for="location"><?php _e( 'Location: ', 'wporg' ); ?> 326 <input type="text" name="location" id="location" class="regular-text wide" value="<?php echo esc_attr( $location ); ?>"> 327 </label> 328 </p> 329 <script> 330 jQuery(document).ready( function($) { 331 $('.date').datepicker({ 332 dateFormat: 'yy-mm-dd' 333 }); 334 335 $('input[name="recurring"]').change( function() { 336 var disabled = ( 'occurrence' !== $(this).val() ); 337 $('#meeting-info').find('[name^="occurrence"]').prop('disabled', disabled); 338 }); 339 340 if ( 'occurrence' !== $('input[name="recurring"]:checked').val() ) { 341 $('#meeting-info').find('[name^="occurrence"]').prop('disabled', true); 342 } 343 }); 344 </script> 345 <?php 346 } 347 348 function save_meta_boxes( $post_id ) { 349 350 global $post; 351 352 // Verify nonce 353 if ( !isset( $_POST['meeting_nonce'] ) || !wp_verify_nonce( $_POST['meeting_nonce'], 'save_meeting_meta_'.$post_id ) ) { 354 return $post_id; 355 } 356 357 // Check autosave 358 if ( (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) || ( defined('DOING_AJAX') && DOING_AJAX) || isset($_REQUEST['bulk_edit']) ) { 359 return $post_id; 360 } 361 362 // Don't save for revisions 363 if ( isset( $post->post_type ) && 'revision' === $post->post_type ) { 364 return $post_id; 365 } 366 367 // Check permissions 368 if ( !current_user_can( 'edit_post', $post->ID ) ) { 369 return $post_id; 370 } 371 372 $meta['team'] = ( isset( $_POST['team'] ) ? esc_textarea( $_POST['team'] ) : '' ); 373 $meta['start_date'] = ( isset( $_POST['start_date'] ) ? esc_textarea( $_POST['start_date'] ) : '' ); 374 $meta['end_date'] = ( isset( $_POST['end_date'] ) ? esc_textarea( $_POST['end_date'] ) : '' ); 375 $meta['time'] = ( isset( $_POST['time'] ) ? esc_textarea( $_POST['time'] ) : '' ); 376 $meta['recurring'] = ( isset( $_POST['recurring'] ) 377 && in_array( $_POST['recurring'], array( 'weekly', 'biweekly', 'occurrence', 'monthly' ) ) 378 ? ( $_POST['recurring'] ) : '' ); 379 $meta['occurrence'] = ( isset( $_POST['occurrence'] ) && 'occurrence' === $meta['recurring'] 380 && is_array( $_POST['occurrence'] ) 381 ? array_map( 'intval', $_POST['occurrence'] ) : array() ); 382 $meta['link'] = ( isset( $_POST['link'] ) ? esc_url( $_POST['link'] ) : '' ); 383 $meta['location'] = ( isset( $_POST['location'] ) ? esc_textarea( $_POST['location'] ) : '' ); 384 385 foreach ( $meta as $key => $value ) { 386 update_post_meta( $post->ID, $key, $value ); 387 } 388 } 389 390 /** 391 * Adds "Edit Meetings" item after "Add New" menu. 392 * 393 * @param \WP_Admin_Bar $wp_admin_bar The admin bar instance. 394 */ 395 public function add_edit_meetings_item_to_admin_bar( $wp_admin_bar ) { 396 if ( ! current_user_can( 'edit_posts' ) ) { 397 return; 398 } 399 400 if ( is_admin() || ! is_post_type_archive( 'meeting' ) ) { 401 return; 402 } 403 404 $wp_admin_bar->add_menu( 405 array( 406 'id' => 'edit-meetings', 407 'title' => '<span class="ab-icon"></span>' . __( 'Edit Meetings', 'wporg' ), 408 'href' => admin_url( 'edit.php?post_type=meeting' ), 409 ) 410 ); 411 } 412 413 /** 414 * Adds icon for the "Edit Meetings" item. 415 */ 416 public function add_edit_meetings_icon_to_admin_bar() { 417 if ( ! current_user_can( 'edit_posts' ) ) { 418 return; 419 } 420 421 wp_add_inline_style( 'admin-bar', ' 422 #wpadminbar #wp-admin-bar-edit-meetings .ab-icon:before { 423 content: "\f145"; 424 top: 2px; 425 } 426 ' ); 427 } 428 429 /** 430 * Renders meeting information with the next meeting time based on user's local timezone. Used in Make homepage. 431 */ 432 public function meeting_time_shortcode( $attr, $content = '' ) { 433 434 $attr = shortcode_atts( array( 435 'team' => null, 436 'limit' => 1, 437 'before' => __( 'Next meeting: ', 'wporg' ), 438 'titletag' => 'strong', 439 'more' => true, 440 ), $attr ); 441 442 if ( empty( $attr['team'] ) ) { 443 return ''; 444 } 445 446 if ( $attr['team'] === 'Documentation' ) { 447 $attr['team'] = 'Docs'; 448 } 449 450 if ( ! has_action( 'wp_footer', array( $this, 'time_conversion_script' ) ) ) { 451 add_action( 'wp_footer', array( $this, 'time_conversion_script' ), 999 ); 452 } 453 454 455 // meta query to eliminate expired meetings from query 456 add_filter( 'get_meta_sql', function ($sql) { 457 return str_replace( "'CURDATE()'", 'CURDATE()', $sql ); 458 } ); 459 460 switch_to_blog( get_main_site_id() ); 461 462 $query = new WP_Query( 463 array( 464 'post_type' => 'meeting', 465 'nopaging' => true, 466 'meta_query' => array( 467 'relation' => 'AND', 468 array( 469 'key' => 'team', 470 'value' => $attr['team'], 471 'compare' => 'EQUALS', 472 ), 473 $this->meeting_meta_query 474 ) 475 ) 476 ); 477 478 $limit = $attr['limit'] > 0 ? $attr['limit'] : count( $query->posts ); 479 480 $out = ''; 481 foreach ( array_slice( $query->posts, 0, $limit ) as $post ) { 482 $next_meeting_datestring = $post->next_date; 483 $utc_time = strftime( '%H:%M:%S', strtotime( $post->time ) ); 484 $next_meeting_iso = $next_meeting_datestring . 'T' . $utc_time . '+00:00'; 485 $next_meeting_timestamp = strtotime( $next_meeting_datestring . ' '. $utc_time ); 486 $next_meeting_display = strftime( '%c %Z', $next_meeting_timestamp ); 487 488 $slack_channel = null; 489 if ( $post->location && preg_match( '/^#([-\w]+)$/', trim( $post->location ), $match ) ) { 490 $slack_channel = sanitize_title( $match[1] ); 491 } 492 493 $out .= '<p>'; 494 $out .= esc_html( $attr['before'] ); 495 $out .= '<strong class="meeting-title">' . esc_html( $post->post_title ) . '</strong>'; 496 $display_more = $query->found_posts - intval( $limit ); 497 if ( $display_more > 0 ) { 498 $out .= ' <a title="Click to view all meetings for this team" href="/meetings/#' . esc_attr( strtolower( $attr['team'] ) ) . '">' . sprintf( __( '(+%s more)'), $display_more ) . '</a>'; 499 } 500 $out .= '</br>'; 501 $out .= '<time class="date" date-time="' . esc_attr( $next_meeting_iso ) . '" title="' . esc_attr( $next_meeting_iso ) . '">' . $next_meeting_display . '</time> '; 502 $out .= sprintf( esc_html__( '(%s from now)' ), human_time_diff( $next_meeting_timestamp, current_time('timestamp') ) ); 503 if ( $post->location && $slack_channel ) { 504 $out .= ' ' . sprintf( wp_kses( __('at <a href="%s">%s</a> on Slack'), array( 'a' => array( 'href' => array() ) ) ), 'https://wordpress.slack.com/messages/' . $slack_channel, $post->location ); 505 } 506 $out .= '</p>'; 507 } 508 509 restore_current_blog(); 510 511 return $out; 512 } 513 514 private $meeting_meta_query = array( 515 'relation'=>'OR', 516 // not recurring AND start_date >= CURDATE() = one-time meeting today or still in future 517 array( 518 'relation'=>'AND', 519 array( 520 'key'=>'recurring', 521 'value'=>array( 'weekly', 'biweekly', 'occurrence', 'monthly', '1' ), 522 'compare'=>'NOT IN', 523 ), 524 array( 525 'key'=>'start_date', 526 'type'=>'DATE', 527 'compare'=>'>=', 528 'value'=>'CURDATE()', 529 ) 530 ), 531 // recurring = 1 AND ( end_date = '' OR end_date > CURDATE() ) = recurring meeting that has no end or has not ended yet 532 array( 533 'relation'=>'AND', 534 array( 535 'key'=>'recurring', 536 'value'=>array( 'weekly', 'biweekly', 'occurrence', 'monthly', '1' ), 537 'compare'=>'IN', 538 ), 539 array( 540 'relation'=>'OR', 541 array( 542 'key'=>'end_date', 543 'value'=>'', 544 'compare'=>'=', 545 ), 546 array( 547 'key'=>'end_date', 548 'type'=>'DATE', 549 'compare'=>'>', 550 'value'=>'CURDATE()', 551 ) 552 ) 553 ), 554 ); 555 556 public function time_conversion_script() { 557 echo <<<EOF 558 <script type="text/javascript"> 559 560 var parse_date = function (text) { 561 var m = /^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})\+00:00$/.exec(text); 562 var d = new Date(); 563 d.setUTCFullYear(+m[1]); 564 d.setUTCDate(+m[3]); 565 d.setUTCMonth(+m[2]-1); 566 d.setUTCHours(+m[4]); 567 d.setUTCMinutes(+m[5]); 568 d.setUTCSeconds(+m[6]); 569 return d; 570 } 571 var format_time = function (d) { 572 return d.toLocaleTimeString(navigator.language, {weekday: 'long', hour: '2-digit', minute: '2-digit', timeZoneName: 'short'}); 573 } 574 575 var nodes = document.getElementsByTagName('time'); 576 for (var i=0; i<nodes.length; ++i) { 577 var node = nodes[i]; 578 if (node.className === 'date') { 579 var d = parse_date(node.getAttribute('date-time')); 580 if (d) { 581 node.textContent = format_time(d); 582 } 583 } 584 } 585 </script> 586 EOF; 587 } 16 if ( ! class_exists( '\WordPressdotorg\Autoload\Autoloader', false ) ) { 17 include __DIR__ . '/vendor/wordpressdotorg/autoload/class-autoloader.php'; 588 18 } 589 19 590 // fire it up 591 Meeting_Post_Type::init(); 592 593 endif; 594 20 // Register an Autoloader for all files. 21 Autoload\register_class_path( __NAMESPACE__, __DIR__ . '/inc' ); 22 Autoload\register_class_path( 'WordPressdotorg\Meetings\Common', __DIR__ . '/inc/common' ); 595 23 24 // Instantiate the Plugin. 25 Plugin::get_instance();