PluginProbe
Edit Flow / 0.9.6
Edit Flow v0.9.6
0.11.1 0.11.0 0.7.2 0.7.3 0.7.4 0.7.5 0.7.6 0.8 0.8.1 0.8.2 0.9 0.9.1 0.9.2 0.9.3 0.9.4 0.9.5 0.9.6 0.9.7 0.9.8 0.9.9 trunk 0.1.5 0.10.0 0.10.1 0.10.2 All 44 releases
edit-flow / modules / calendar / calendar.php

calendar.php in Edit Flow 0.9.6, at modules/calendar/calendar.php

1,881 lines 70.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * class EF_Calendar
4 * This class displays an editorial calendar for viewing upcoming and past content at a glance
5 *
6 * @author danielbachhuber
7 */
8 if ( !class_exists('EF_Calendar') ) {
9
10 class EF_Calendar extends EF_Module {
11
12 const usermeta_key_prefix = 'ef_calendar_';
13 const screen_id = 'dashboard_page_calendar';
14
15 var $module;
16
17 var $start_date = '';
18 var $current_week = 1;
19 var $total_weeks = 6; // default number of weeks to show per screen
20 var $hidden = 0; // counter of hidden posts per date square
21 var $max_visible_posts_per_date = 4; // total number of posts to be shown per square before 'more' link
22
23 private $post_date_cache = array();
24 private static $post_li_html_cache_key = 'ef_calendar_post_li_html';
25
26 /**
27 * Construct the EF_Calendar class
28 */
29 function __construct() {
30 $this->max_weeks = 12;
31
32 $this->module_url = $this->get_module_url( __FILE__ );
33 // Register the module with Edit Flow
34 $args = array(
35 'title' => __( 'Calendar', 'edit-flow' ),
36 'short_description' => sprintf( __( 'View upcoming content in a <a href="%s">customizable calendar</a>.', 'edit-flow' ), admin_url( 'index.php?page=calendar' ) ),
37 'extended_description' => __( 'Edit Flow’s calendar lets you see your posts over a customizable date range. Filter by status or click on the post title to see its details. Drag and drop posts between days to change their publication date.', 'edit-flow' ),
38 'module_url' => $this->module_url,
39 'img_url' => $this->module_url . 'lib/calendar_s128.png',
40 'slug' => 'calendar',
41 'post_type_support' => 'ef_calendar',
42 'default_options' => array(
43 'enabled' => 'on',
44 'post_types' => array(
45 'post' => 'on',
46 'page' => 'off',
47 ),
48 'quick_create_post_type' => 'post',
49 'ics_subscription' => 'off',
50 'ics_secret_key' => '',
51 ),
52 'messages' => array(
53 'post-date-updated' => __( "Post date updated.", 'edit-flow' ),
54 'update-error' => __( 'There was an error updating the post. Please try again.', 'edit-flow' ),
55 'published-post-ajax' => __( "Updating the post date dynamically doesn't work for published content. Please <a href='%s'>edit the post</a>.", 'edit-flow' ),
56 'key-regenerated' => __( 'iCal secret key regenerated. Please inform all users they will need to resubscribe.', 'edit-flow' ),
57 ),
58 'configure_page_cb' => 'print_configure_view',
59 'configure_link_text' => __( 'Calendar Options', 'edit-flow' ),
60 'settings_help_tab' => array(
61 'id' => 'ef-calendar-overview',
62 'title' => __('Overview', 'edit-flow'),
63 'content' => __('<p>The calendar is a convenient week-by-week or month-by-month view into your content. Quickly see which stories are on track to being published on time, and which will need extra effort.</p>', 'edit-flow'),
64 ),
65 'settings_help_sidebar' => __( '<p><strong>For more information:</strong></p><p><a href="http://editflow.org/features/calendar/">Calendar Documentation</a></p><p><a href="http://wordpress.org/tags/edit-flow?forum_id=10">Edit Flow Forum</a></p><p><a href="https://github.com/danielbachhuber/Edit-Flow">Edit Flow on Github</a></p>', 'edit-flow' ),
66 );
67 $this->module = EditFlow()->register_module( 'calendar', $args );
68
69 }
70
71 /**
72 * Initialize all of our methods and such. Only runs if the module is active
73 *
74 * @uses add_action()
75 */
76 function init() {
77
78 // .ics calendar subscriptions
79 add_action( 'wp_ajax_ef_calendar_ics_subscription', array( $this, 'handle_ics_subscription' ) );
80 add_action( 'wp_ajax_nopriv_ef_calendar_ics_subscription', array( $this, 'handle_ics_subscription' ) );
81
82 // Check whether the user should have the ability to view the calendar
83 $view_calendar_cap = 'ef_view_calendar';
84 $view_calendar_cap = apply_filters( 'ef_view_calendar_cap', $view_calendar_cap );
85 if ( !current_user_can( $view_calendar_cap ) ) return false;
86
87 // Define the create-post capability
88 $this->create_post_cap = apply_filters( 'ef_calendar_create_post_cap', 'edit_posts' );
89
90 add_action( 'admin_init', array( $this, 'add_screen_options_panel' ) );
91 add_action( 'admin_init', array( $this, 'handle_save_screen_options' ) );
92
93 add_action( 'admin_init', array( $this, 'register_settings' ) );
94 add_action( 'admin_menu', array( $this, 'action_admin_menu' ) );
95 add_action( 'admin_print_styles', array( $this, 'add_admin_styles' ) );
96 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) );
97
98 // Ajax manipulation for the calendar
99 add_action( 'wp_ajax_ef_calendar_drag_and_drop', array( $this, 'handle_ajax_drag_and_drop' ) );
100
101 // Ajax insert post placeholder for a specific date
102 add_action( 'wp_ajax_ef_insert_post', array( $this, 'handle_ajax_insert_post' ) );
103
104 //Update metadata
105 add_action( 'wp_ajax_ef_calendar_update_metadata', array( $this, 'handle_ajax_update_metadata' ) );
106
107 // Clear li cache for a post when post cache is cleared
108 add_action( 'clean_post_cache', array( $this, 'action_clean_li_html_cache' ) );
109
110 // Action to regenerate the calendar feed sekret
111 add_action( 'admin_init', array( $this, 'handle_regenerate_calendar_feed_secret' ) );
112
113 // Hacks to fix deficiencies in core
114 add_action( 'pre_post_update', array( $this, 'fix_post_date_on_update_part_one' ), 10, 2 );
115 add_action( 'post_updated', array( $this, 'fix_post_date_on_update_part_two' ), 10, 3 );
116 }
117
118 /**
119 * Load the capabilities onto users the first time the module is run
120 *
121 * @since 0.7
122 */
123 function install() {
124
125 // Add necessary capabilities to allow management of calendar
126 // view_calendar - administrator --> contributor
127 $calendar_roles = array(
128 'administrator' => array('ef_view_calendar'),
129 'editor' => array('ef_view_calendar'),
130 'author' => array('ef_view_calendar'),
131 'contributor' => array('ef_view_calendar')
132 );
133
134 foreach ( $calendar_roles as $role => $caps ) {
135 $this->add_caps_to_role( $role, $caps );
136 }
137 }
138
139 /**
140 * Upgrade our data in case we need to
141 *
142 * @since 0.7
143 */
144 function upgrade( $previous_version ) {
145 global $edit_flow;
146
147 // Upgrade path to v0.7
148 if ( version_compare( $previous_version, '0.7' , '<' ) ) {
149 // Migrate whether the calendar was enabled or not and clean up old option
150 if ( $enabled = get_option( 'edit_flow_calendar_enabled' ) )
151 $enabled = 'on';
152 else
153 $enabled = 'off';
154 $edit_flow->update_module_option( $this->module->name, 'enabled', $enabled );
155 delete_option( 'edit_flow_calendar_enabled' );
156
157 // Technically we've run this code before so we don't want to auto-install new data
158 $edit_flow->update_module_option( $this->module->name, 'loaded_once', true );
159 }
160
161 }
162
163 /**
164 * Add the calendar link underneath the "Dashboard"
165 *
166 * @uses add_submenu_page
167 */
168 function action_admin_menu() {
169 add_submenu_page('index.php', __('Calendar', 'edit-flow'), __('Calendar', 'edit-flow'), apply_filters( 'ef_view_calendar_cap', 'ef_view_calendar' ), $this->module->slug, array( $this, 'view_calendar' ) );
170 }
171
172 /**
173 * Add any necessary CSS to the WordPress admin
174 *
175 * @uses wp_enqueue_style()
176 */
177 function add_admin_styles() {
178 global $pagenow;
179 // Only load calendar styles on the calendar page
180 if ( 'index.php' === $pagenow && isset( $_GET['page'] ) && 'calendar' === $_GET['page'] ) {
181 wp_enqueue_style( 'edit-flow-calendar-css', $this->module_url . 'lib/calendar.css', false, EDIT_FLOW_VERSION );
182 wp_enqueue_style( 'edit-flow-calendar-react-css', $this->module_url . 'lib/dist/calendar.react.style.build.css', array( 'wp-components' ), EDIT_FLOW_VERSION );
183 }
184 }
185
186 /**
187 * Add any necessary JS to the WordPress admin
188 *
189 * @since 0.7
190 * @uses wp_enqueue_script()
191 */
192 function enqueue_admin_scripts() {
193 global $pagenow;
194
195 if ( 'index.php' === $pagenow && isset( $_GET['page'] ) && 'calendar' === $_GET['page'] ) {
196 $this->enqueue_datepicker_resources();
197
198 $js_libraries = array(
199 'jquery',
200 'jquery-ui-core',
201 'jquery-ui-sortable',
202 'jquery-ui-draggable',
203 'jquery-ui-droppable',
204 'wp-data'
205 );
206 foreach( $js_libraries as $js_library ) {
207 wp_enqueue_script( $js_library );
208 }
209 wp_enqueue_script( 'edit-flow-calendar-js', $this->module_url . 'lib/calendar.js', $js_libraries, EDIT_FLOW_VERSION, true );
210
211 $ef_cal_js_params = array( 'can_add_posts' => current_user_can( $this->create_post_cap ) ? 'true' : 'false' );
212 wp_localize_script( 'edit-flow-calendar-js', 'ef_calendar_params', $ef_cal_js_params );
213
214 /**
215 * Powering the new React interface
216 */
217 wp_enqueue_script( 'edit-flow-calendar-react-js', $this->module_url . 'lib/dist/calendar.react.build.js', array( 'react', 'react-dom', 'wp-components', 'wp-url', 'wp-data', 'moment' ), EDIT_FLOW_VERSION, true );
218
219 wp_add_inline_script(
220 'edit-flow-calendar-react-js',
221 'var EF_CALENDAR = ' . wp_json_encode( $this->get_calendar_frontend_config() ),
222 'before'
223 );
224 }
225
226 }
227
228 /**
229 * Prepare the options that need to appear in Screen Options
230 *
231 * @since 0.7
232 */
233 function generate_screen_options() {
234
235 $output = '';
236
237 $args = array(
238 'action' => 'ef_calendar_ics_subscription',
239 'user' => wp_get_current_user()->user_login,
240 'user_key' => md5( wp_get_current_user()->user_login . $this->module->options->ics_secret_key ),
241 );
242 $subscription_link = add_query_arg( $args, admin_url( 'admin-ajax.php' ) );
243 $output .= '<br />';
244 $output .= __( 'Subscribe in iCal or Google Calendar', 'edit-flow' );
245 $output .= ':<br /><input type="text" size="100" value="' . esc_attr( $subscription_link ) . '" />';
246
247 return $output;
248 }
249
250 /**
251 * Add module options to the screen panel
252 *
253 * @since 0.8.3
254 */
255 function add_screen_options_panel() {
256 require_once( EDIT_FLOW_ROOT . '/common/php/' . 'screen-options.php' );
257 if ( 'on' == $this->module->options->ics_subscription && $this->module->options->ics_secret_key ) {
258 add_screen_options_panel( self::usermeta_key_prefix . 'screen_options', __( 'Calendar Options', 'edit-flow' ), array( $this, 'generate_screen_options' ), self::screen_id, false, true );
259 }
260 }
261
262 /**
263 * Handle the request to save the screen options
264 *
265 * @since 0.7
266 */
267 function handle_save_screen_options() {
268
269 // Only handle screen options submissions from the current screen
270 if ( ! isset( $_POST['screen-options-apply'] ) )
271 return;
272
273 // Nonce check
274 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
275 if ( !isset( $_POST['_wpnonce-' . self::usermeta_key_prefix . 'screen_options'] ) || !wp_verify_nonce ( $_POST['_wpnonce-' . self::usermeta_key_prefix . 'screen_options'], 'save_settings-' . self::usermeta_key_prefix . 'screen_options' ) ) {
276 wp_die( esc_html( $this->module->messages['nonce-failed'] ) );
277 }
278
279 // Get the current screen options
280 $screen_options = $this->get_screen_options();
281
282 // Save the screen options
283 $current_user = wp_get_current_user();
284 $this->update_user_meta( $current_user->ID, self::usermeta_key_prefix . 'screen_options', $screen_options );
285
286 // Redirect after we're complete
287 $redirect_to = menu_page_url( $this->module->slug, false );
288 wp_redirect( $redirect_to );
289 exit;
290 }
291
292 /**
293 * Handle an AJAX request from the calendar to update a post's timestamp.
294 * Notes:
295 * - For Post Time, if the post is unpublished, the change sets the publication timestamp
296 * - If the post was published or scheduled for the future, the change will change the timestamp. 'publish' posts
297 * will become scheduled if moved past today and 'future' posts will be published if moved before today
298 * - Need to respect user permissions. Editors can move all, authors can move their own, and contributors can't move at all
299 *
300 * @since 0.7
301 */
302 function handle_ajax_drag_and_drop() {
303 global $wpdb;
304
305 // Nonce check!
306 if ( !isset( $_POST['nonce'] ) || !wp_verify_nonce( $_POST['nonce'], 'ef-calendar-modify' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
307 $this->print_ajax_response( 'error', $this->module->messages['nonce-failed'] );
308 }
309
310 if ( !isset( $_POST['post_id'] ) ) {
311 $this->print_ajax_response( 'error', $this->module->messages['missing-post'] );
312 }
313
314 // Check that we got a proper post
315 $post_id = (int) $_POST['post_id'];
316 $post = get_post( $post_id );
317 if ( !$post ) {
318 $this->print_ajax_response( 'error', $this->module->messages['missing-post'] );
319 }
320
321 // Check that the user can modify the post
322 if ( !$this->current_user_can_modify_post( $post ) ) {
323 $this->print_ajax_response( 'error', $this->module->messages['invalid-permissions'] );
324 }
325
326 // Check that it's not yet published
327 if ( in_array( $post->post_status, $this->published_statuses ) ) {
328 $this->print_ajax_response( 'error', sprintf( $this->module->messages['published-post-ajax'], get_edit_post_link( $post_id ) ) );
329 }
330
331 if ( !isset( $_POST['next_date'] ) ) {
332 $this->print_ajax_response( 'error', __( 'Missing new date.', 'edit-flow' ) );
333 }
334
335 // Check that the new date passed is a valid one
336 $next_date_full = strtotime( $_POST['next_date'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
337 if ( !$next_date_full ) {
338 $this->print_ajax_response( 'error', __( 'Something is wrong with the format for the new date.', 'edit-flow' ) );
339 }
340
341 // Persist the old hourstamp because we can't manipulate the exact time on the calendar
342 // Bump the last modified timestamps too
343 $existing_time = date( 'H:i:s', strtotime( $post->post_date ) );
344 $existing_time_gmt = date( 'H:i:s', strtotime( $post->post_date_gmt ) );
345 $new_values = array(
346 'post_date' => date( 'Y-m-d', $next_date_full ) . ' ' . $existing_time,
347 'post_modified' => current_time( 'mysql' ),
348 'post_modified_gmt' => current_time( 'mysql', 1 ),
349 );
350
351 // By default, changing a post on the calendar won't set the timestamp.
352 // If the user desires that to be the behaviour, they can set the result of this filter to 'true'
353 // With how WordPress works internally, setting 'post_date_gmt' will set the timestamp
354 if ( apply_filters( 'ef_calendar_allow_ajax_to_set_timestamp', false ) ) {
355 $new_values['post_date_gmt'] = date( 'Y-m-d', $next_date_full ) . ' ' . $existing_time_gmt;
356 }
357
358 // We have to do SQL unfortunately because of core bugginess
359 // Note to those reading this: bug Nacin to allow us to finish the custom status API
360 // See http://core.trac.wordpress.org/ticket/18362
361 $response = $wpdb->update( $wpdb->posts, $new_values, array( 'ID' => $post->ID ) );
362 clean_post_cache( $post->ID );
363
364 if ( !$response ) {
365 $this->print_ajax_response( 'error', $this->module->messages['update-error'] );
366 }
367
368 $this->print_ajax_response( 'success', $this->module->messages['post-date-updated'] );
369 exit;
370 }
371
372 /**
373 * After checking that the request is valid, do an .ics file
374 *
375 * @since 0.8
376 */
377 function handle_ics_subscription() {
378
379 // Only do .ics subscriptions when the option is active
380 if ( 'on' != $this->module->options->ics_subscription )
381 die(); // @todo return accepted response value.
382
383 // Confirm all of the arguments are present
384 if ( ! isset( $_GET['user'], $_GET['user_key'] ) ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended
385 die(); // @todo return an error response
386
387 // Confirm this is a valid request
388 $user = sanitize_user( $_GET['user'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
389 $user_key = sanitize_user( $_GET['user_key'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
390 $ics_secret_key = $this->module->options->ics_secret_key;
391 if ( ! $ics_secret_key || md5( $user . $ics_secret_key ) !== $user_key ) {
392 die( esc_html( $this->module->messages['nonce-failed'] ) );
393 }
394
395 // Set up the post data to be printed
396 $post_query_args = array();
397 $calendar_filters = $this->calendar_filters();
398 foreach( $calendar_filters as $filter ) {
399 if ( isset( $_GET[$filter] ) && false !== ( $value = $this->sanitize_filter( $filter, $_GET[$filter] ) ) ) //phpcs:ignore WordPress.Security.NonceVerification.Recommended
400 $post_query_args[$filter] = $value;
401 }
402
403 // Set the start date for the posts_where filter
404 $this->start_date = apply_filters( 'ef_calendar_ics_subscription_start_date', $this->get_beginning_of_week( date( 'Y-m-d', current_time( 'timestamp' ) ) ) );
405
406 $this->total_weeks = apply_filters( 'ef_calendar_total_weeks', $this->total_weeks, 'ics_subscription' );
407
408 $formatted_posts = array();
409 for( $current_week = 1; $current_week <= $this->total_weeks; $current_week++ ) {
410 // We need to set the object variable for our posts_where filter
411 $this->current_week = $current_week;
412 $week_posts = $this->get_calendar_posts_for_week( $post_query_args, 'ics_subscription' );
413 foreach( $week_posts as $date => $day_posts ) {
414 foreach( $day_posts as $num => $post ) {
415
416 $start_date = self::ics_format_time( $post->post_date );
417 $end_date = self::ics_format_time( $post->post_date, 5 * MINUTE_IN_SECONDS );
418 $last_modified = self::ics_format_time( $post->post_modified );
419 $post_status_obj = get_post_status_object( get_post_status( $post->ID ) );
420 // Remove the convert chars and wptexturize filters from the title
421 remove_filter( 'the_title', 'convert_chars' );
422 remove_filter( 'the_title', 'wptexturize' );
423
424 $formatted_post = array(
425 'BEGIN' => 'VEVENT',
426 'UID' => $post->guid,
427 'SUMMARY' => $this->do_ics_escaping( apply_filters( 'the_title', $post->post_title ) ) . ' - ' . $post_status_obj->label,
428 'DTSTART' => $start_date,
429 'DTEND' => $end_date,
430 'LAST-MODIFIED' => $last_modified,
431 'URL' => get_post_permalink( $post->ID ),
432 );
433
434 // Description should include everything visible in the calendar popup
435 $information_fields = $this->get_post_information_fields( $post );
436 $formatted_post['DESCRIPTION'] = '';
437 if ( ! empty( $information_fields ) ) {
438 foreach( $information_fields as $key => $values ) {
439 $formatted_post['DESCRIPTION'] .= $values['label'] . ': ' . $values['value'] . '\n';
440 }
441 $formatted_post['DESCRIPTION'] = rtrim( $formatted_post['DESCRIPTION'] );
442 }
443
444 $formatted_post['END'] = 'VEVENT';
445
446 // @todo auto format any field longer than 75 bytes
447
448 $formatted_posts[] = $formatted_post;
449 }
450 }
451 }
452
453 // Other template data
454 $header = array(
455 'BEGIN' => 'VCALENDAR',
456 'VERSION' => '2.0',
457 'PRODID' => '-//Edit Flow//Edit Flow ' . EDIT_FLOW_VERSION . '//EN',
458 );
459
460 $footer = array(
461 'END' => 'VCALENDAR',
462 );
463
464 // Render the .ics template and set the content type
465 header( 'Content-type: text/calendar' );
466 foreach( array( $header, $formatted_posts, $footer ) as $section ) {
467 foreach( $section as $key => $value ) {
468 /**
469 * This is output to text/calendar content-type
470 */
471 if ( is_string( $value ) )
472 echo $this->do_ics_line_folding( $key . ':' . $value ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
473 else
474 foreach( $value as $k => $v ) {
475 echo $this->do_ics_line_folding( $k . ':' . $v ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
476 }
477 }
478 }
479 die();
480
481 }
482
483 /**
484 * Perform line folding according to RFC 5545.
485 *
486 * @param string $line The line without trailing CRLF
487 * @return string The line after line-folding with all necessary CRLF.
488 */
489 function do_ics_line_folding( $line ) {
490 $len = mb_strlen( $line );
491 if ( $len <= 75) {
492 return $line . "\r\n";
493 }
494
495 $chunks = array();
496 $start = 0;
497 while( true ) {
498 $chunk = mb_substr( $line, $start, 75 );
499 $chunkLen = mb_strlen( $chunk );
500 $start += $chunkLen;
501 if ( $start < $len ) {
502 $chunks[] = $chunk . "\r\n ";
503 }
504 else {
505 $chunks[] = $chunk ."\r\n";
506 return implode( "", $chunks );
507 }
508 }
509 }
510
511 /**
512 * Perform the encoding necessary for ICS feed text.
513 *
514 * @param string $text The string that needs to be escaped
515 * @return string The string after escaping for ICS.
516 * @since 0.8
517 * */
518
519 function do_ics_escaping( $text ) {
520 $text = str_replace( ",", "\,", $text );
521 $text = str_replace( ";", "\:", $text );
522 $text = str_replace( "\\", "\\\\", $text );
523 return $text;
524 }
525
526 /**
527 * Convert a time string into a `.ics` formatted time string with the proper GMT offset
528 *
529 * @param $time_string - Any time string that `strtotime()` can understand
530 * @param int $offset_in_seconds - Allows to offset the timestamp generated from $time_string
531 *
532 * @return string|false
533 */
534 public static function ics_format_time( $time_string, $offset_in_seconds = 0) {
535
536 // Timestamp it
537 $timestamp = strtotime( $time_string );
538
539 if( ! $timestamp ) {
540 return false;
541 }
542
543 // Subtract GMT Offset to return to UTC+0
544 $timestamp -= get_option('gmt_offset') * HOUR_IN_SECONDS;
545
546 // Add manual offset
547 $timestamp += $offset_in_seconds;
548
549 // \T and \Z are escaped for literal T and Z characters
550 return date( 'Ymd\THis\Z', $timestamp );
551
552 }
553
554 /**
555 * Handle a request to regenerate the calendar feed secret
556 *
557 * @since 0.8
558 */
559 public function handle_regenerate_calendar_feed_secret() {
560
561 if ( ! isset( $_GET['action'] ) || 'ef_calendar_regenerate_calendar_feed_secret' != $_GET['action'] )
562 return;
563
564 if ( ! current_user_can( 'manage_options' ) ) {
565 wp_die( esc_html( $this->module->messages['invalid-permissions'] ) );
566 }
567
568 if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( $_GET['_wpnonce'], 'ef-regenerate-ics-key' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
569 wp_die( esc_html( $this->module->messages['nonce-failed'] ) );
570 }
571
572 EditFlow()->update_module_option( $this->module->name, 'ics_secret_key', wp_generate_password() );
573
574 wp_safe_redirect( add_query_arg( 'message', 'key-regenerated', menu_page_url( $this->module->settings_slug, false ) ) );
575 exit;
576 }
577
578 /**
579 * Get a user's screen options
580 *
581 * @since 0.7
582 * @uses get_user_meta()
583 *
584 * @return array $screen_options The screen options values
585 */
586 function get_screen_options() {
587
588 /**
589 * `num_weeks` has been moved to a filter and out of screen options, it's maintained here for legacy purposes
590 * @deprecated `num_weeks`
591 */
592 $defaults = array(
593 'num_weeks' => (int)$this->total_weeks,
594 );
595 $current_user = wp_get_current_user();
596 $screen_options = $this->get_user_meta( $current_user->ID, self::usermeta_key_prefix . 'screen_options', true );
597 $screen_options = array_merge( (array)$defaults, (array)$screen_options );
598
599 return $screen_options;
600 }
601
602 /**
603 * Get the user's filters for calendar, either with $_GET or from saved
604 *
605 * @uses get_user_meta()
606 * @return array $filters All of the set or saved calendar filters
607 */
608 function get_filters() {
609 $current_user = wp_get_current_user();
610 $filters = array();
611 $old_filters = $this->get_user_meta( $current_user->ID, self::usermeta_key_prefix . 'filters', true );
612
613 /**
614 * To support legacy screen option for num_weeks
615 */
616 $screen_options = $this->get_user_meta( $current_user->ID, self::usermeta_key_prefix . 'screen_options', true );
617
618 $default_filters = array(
619 'post_status' => '',
620 'cpt' => '',
621 'cat' => '',
622 'author' => '',
623 'num_weeks' => $this->total_weeks,
624 'start_date' => date( 'Y-m-d', current_time( 'timestamp' ) ),
625 );
626 $old_filters = array_merge( $default_filters, isset( $screen_options['num_weeks'] ) ? array( 'num_weeks' => $screen_options['num_weeks'] ) : array(), (array)$old_filters );
627
628 // Sanitize and validate any newly added filters
629 foreach( $old_filters as $key => $old_value ) {
630 if ( isset( $_GET[$key] ) && false !== ( $new_value = $this->sanitize_filter( $key, $_GET[$key] ) ) )
631 $filters[$key] = $new_value;
632 else
633 $filters[$key] = $old_value;
634 }
635
636 // Set the start date as the beginning of the week, according to blog settings
637 $filters['start_date'] = $this->get_beginning_of_week( $filters['start_date'] );
638
639 $filters = apply_filters( 'ef_calendar_filter_values', $filters, $old_filters );
640
641 $this->update_user_meta( $current_user->ID, self::usermeta_key_prefix . 'filters', $filters );
642
643 return $filters;
644 }
645
646 /**
647 * Build all of the HTML for the calendar view
648 */
649 function view_calendar() {
650
651 $this->dropdown_taxonomies = array();
652
653 $supported_post_types = $this->get_post_types_for_module( $this->module );
654
655 // Get filters either from $_GET or from user settings
656 $filters = $this->get_filters();
657
658 // Total number of weeks to display on the calendar. Run it through a filter in case we want to override the
659 // user's standard
660 $this->total_weeks = apply_filters( 'ef_calendar_total_weeks', $filters['num_weeks'], 'dashboard' );
661
662 $dotw = array(
663 'Sat',
664 'Sun',
665 );
666 $dotw = apply_filters( 'ef_calendar_weekend_days', $dotw );
667
668 // For generating the WP Query objects later on
669 $post_query_args = array(
670 'post_status' => $filters['post_status'],
671 'post_type' => $filters['cpt'],
672 'cat' => $filters['cat'],
673 'author' => $filters['author']
674 );
675 $this->start_date = $filters['start_date'];
676
677 // We use this later to label posts if they need labeling
678 if ( count( $supported_post_types ) > 1 ) {
679 $all_post_types = get_post_types( null, 'objects' );
680 }
681 $dates = array();
682 $heading_date = $filters['start_date'];
683 for ( $i=0; $i<7; $i++ ) {
684 $dates[$i] = $heading_date;
685 $heading_date = date( 'Y-m-d', strtotime( "+1 day", strtotime( $heading_date ) ) );
686 }
687
688 // we sort by post statuses....... eventually
689 $post_statuses = $this->get_calendar_post_stati();
690 ?>
691 <div class="wrap">
692 <div id="ef-calendar-title"><!-- Calendar Title -->
693 <?php echo '<img src="' . esc_url( $this->module->img_url ) . '" class="module-icon icon32" />'; ?>
694 <h2><?php esc_html_e( 'Calendar', 'edit-flow' ); ?>&nbsp;<span class="time-range"><?php $this->calendar_time_range(); ?></span></h2>
695 </div><!-- /Calendar Title -->
696
697 <?php
698 // Handle posts that have been trashed or untrashed
699 if ( isset( $_GET['trashed'] ) || isset( $_GET['untrashed'] ) ) {
700
701 echo '<div id="trashed-message" class="updated"><p>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
702 if ( isset( $_GET['trashed'] ) && (int) $_GET['trashed'] ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
703 echo esc_html( sprintf( _n( 'Post moved to the trash.', '%d posts moved to the trash.', $_GET['trashed'] ), number_format_i18n( $_GET['trashed'] ) ) );
704 $ids = isset($_GET['ids']) ? $_GET['ids'] : 0;
705 $pid = explode( ',', $ids );
706 $post_type = get_post_type( $pid[0] );
707 echo ' <a href="' . esc_url( wp_nonce_url( "edit.php?post_type=$post_type&doaction=undo&action=untrash&ids=$ids", "bulk-posts" ) ) . '">' . esc_html__( 'Undo', 'edit-flow' ) . '</a><br />';
708 unset( $_GET['trashed'] );
709 }
710 if ( isset($_GET['untrashed'] ) && (int) $_GET['untrashed'] ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
711 echo esc_html( sprintf( _n( 'Post restored from the Trash.', '%d posts restored from the Trash.', $_GET['untrashed'] ), number_format_i18n( $_GET['untrashed'] ) ) );
712 unset( $_GET['undeleted'] );
713 }
714 echo '</p></div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
715 }
716 ?>
717
718 <div id="ef-calendar-navigation-mount"></div> <!-- Mount point for React -->
719
720 <div id="ef-calendar-wrap"><!-- Calendar Wrapper -->
721
722 <?php
723 $table_classes = array();
724 // CSS don't like our classes to start with numbers
725 if ( $this->total_weeks == 1 )
726 $table_classes[] = 'one-week-showing';
727 elseif ( $this->total_weeks == 2 )
728 $table_classes[] = 'two-weeks-showing';
729 elseif ( $this->total_weeks == 3 )
730 $table_classes[] = 'three-weeks-showing';
731
732 $table_classes = apply_filters( 'ef_calendar_table_classes', $table_classes );
733 ?>
734 <table id="ef-calendar-view" class="<?php echo esc_attr( implode( ' ', $table_classes ) ); ?>">
735 <thead>
736 <tr class="calendar-heading">
737 <?php echo $this->get_time_period_header( $dates ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
738 </tr>
739 </thead>
740 <tbody>
741
742 <?php
743 $current_month = date_i18n( 'F', strtotime( $filters['start_date'] ) );
744 for( $current_week = 1; $current_week <= $this->total_weeks; $current_week++ ):
745 // We need to set the object variable for our posts_where filter
746 $this->current_week = $current_week;
747 $week_posts = $this->get_calendar_posts_for_week( $post_query_args );
748 $date_format = 'Y-m-d';
749 $week_single_date = $this->get_beginning_of_week( $filters['start_date'], $date_format, $current_week );
750 $week_dates = array();
751 $split_month = false;
752 for ( $i = 0 ; $i < 7; $i++ ) {
753 $week_dates[$i] = $week_single_date;
754 $single_date_month = date_i18n( 'F', strtotime( $week_single_date ) );
755 if ( $single_date_month != $current_month ) {
756 $split_month = $single_date_month;
757 $current_month = $single_date_month;
758 }
759 $week_single_date = date( 'Y-m-d', strtotime( "+1 day", strtotime( $week_single_date ) ) );
760 }
761 ?>
762 <?php if ( $split_month ): ?>
763 <tr class="month-marker">
764 <?php foreach( $week_dates as $key => $week_single_date ) {
765 if ( date_i18n( 'F', strtotime( $week_single_date ) ) != $split_month && date_i18n( 'F', strtotime( "+1 day", strtotime( $week_single_date ) ) ) == $split_month ) {
766 $previous_month = date_i18n( 'F', strtotime( $week_single_date ) );
767 echo '<td class="month-marker-previous">' . esc_html( $previous_month ) . '</td>';
768 } else if ( date_i18n( 'F', strtotime( $week_single_date ) ) == $split_month && date_i18n( 'F', strtotime( "-1 day", strtotime( $week_single_date ) ) ) != $split_month ) {
769 echo '<td class="month-marker-current">' . esc_html( $split_month ) . '</td>';
770 } else {
771 echo '<td class="month-marker-empty"></td>';
772 }
773 } ?>
774 </tr>
775 <?php endif; ?>
776
777 <tr class="week-unit">
778 <?php foreach( $week_dates as $day_num => $week_single_date ): ?>
779 <?php
780 // Somewhat ghetto way of sorting all of the day's posts by post status order
781 if ( !empty( $week_posts[$week_single_date] ) ) {
782 $week_posts_by_status = array();
783 foreach( $post_statuses as $post_status ) {
784 $week_posts_by_status[$post_status->name] = array();
785 }
786 // These statuses aren't handled by custom statuses or post statuses
787 $week_posts_by_status['private'] = array();
788 $week_posts_by_status['publish'] = array();
789 $week_posts_by_status['future'] = array();
790 foreach( $week_posts[$week_single_date] as $num => $post ) {
791 $week_posts_by_status[$post->post_status][$num] = $post;
792 }
793 unset( $week_posts[$week_single_date] );
794 foreach( $week_posts_by_status as $status ) {
795 foreach( $status as $num => $post ) {
796 $week_posts[$week_single_date][] = $post;
797 }
798 }
799 }
800
801 $td_classes = array(
802 'day-unit',
803 );
804 $day_name = date( 'D', strtotime( $week_single_date ) );
805
806 if ( in_array( $day_name, $dotw ) )
807 $td_classes[] = 'weekend-day';
808
809 if ( $week_single_date == date( 'Y-m-d', current_time( 'timestamp' ) ) )
810 $td_classes[] = 'today';
811
812 // Last day of the week
813 if ( $day_num == 6 )
814 $td_classes[] = 'last-day';
815
816 $td_classes = apply_filters( 'ef_calendar_table_td_classes', $td_classes, $week_single_date );
817 ?>
818 <td class="<?php echo esc_attr( implode( ' ', $td_classes ) ); ?>" id="<?php echo esc_attr( $week_single_date ); ?>">
819 <button class='schedule-new-post-button'>+</button>
820 <?php if ( $week_single_date == date( 'Y-m-d', current_time( 'timestamp' ) ) ): ?>
821 <div class="day-unit-today"><?php esc_html_e( 'Today', 'edit-flow' ); ?></div>
822 <?php endif; ?>
823 <div class="day-unit-label"><?php echo esc_html( date( 'j', strtotime( $week_single_date ) ) ); ?></div>
824 <ul class="post-list">
825 <?php
826 $this->hidden = 0;
827 if ( !empty( $week_posts[$week_single_date] ) ) {
828
829 $week_posts[$week_single_date] = apply_filters( 'ef_calendar_posts_for_week', $week_posts[$week_single_date], $week_single_date );
830
831 foreach ( $week_posts[$week_single_date] as $num => $post ) {
832 $output = apply_filters( 'ef_pre_calendar_single_date_item_html', '', $this, $num, $post, $week_single_date );
833 if ( ! $output ) {
834 $output = $this->generate_post_li_html( $post, $week_single_date, $num );
835 }
836 echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
837 }
838
839 }
840 ?>
841 </ul>
842 <?php if ( $this->hidden ): ?>
843 <a class="show-more" href="#"><?php esc_html_e( sprintf( __( 'Show %d more', 'edit-flow' ), $this->hidden ) ); ?></a>
844 <?php endif; ?>
845
846 <?php if( current_user_can( $this->create_post_cap ) ) :
847 $date_formatted = date( 'D, M jS, Y', strtotime( $week_single_date ) );
848 ?>
849
850 <form method="POST" class="post-insert-dialog">
851 <?php /* translators: %1$s = post type name, %2$s = date */ ?>
852 <h1><?php esc_html_e( sprintf( __( 'Schedule a %1$s for %2$s', 'edit-flow' ), $this->get_quick_create_post_type_name(), $date_formatted ) ); ?></h1>
853 <?php /* translators: %s = post type name */ ?>
854 <input type="text" class="post-insert-dialog-post-title" name="post-insert-dialog-post-title" placeholder="<?php echo esc_attr( sprintf( _x( '%s Title', 'post type name', 'edit-flow' ), $this->get_quick_create_post_type_name() ) ); ?>">
855 <input type="hidden" class="post-insert-dialog-post-date" name="post-insert-dialog-post-title" value="<?php echo esc_attr( $week_single_date ); ?>">
856 <div class="post-insert-dialog-controls">
857 <input type="submit" class="button left" value="<?php echo esc_attr( sprintf( _x( 'Create %s', 'post type name', 'edit-flow' ), $this->get_quick_create_post_type_name() ) ); ?>">
858 <a class="post-insert-dialog-edit-post-link" href="#"><?php echo esc_attr( sprintf( _x( 'Edit %s', 'post type name', 'edit-flow' ), $this->get_quick_create_post_type_name() ) ); ?>&nbsp;&raquo;</a>
859 </div>
860 <div class="spinner">&nbsp;</div>
861 </form>
862 <?php endif; ?>
863
864 </td>
865 <?php endforeach; ?>
866 </tr>
867
868 <?php endfor; ?>
869
870 </tbody>
871 </table><!-- /Week Wrapper -->
872 <?php
873 // Nonce field for AJAX actions
874 wp_nonce_field( 'ef-calendar-modify', 'ef-calendar-modify' ); ?>
875
876 <div class="clear"></div>
877 </div><!-- /Calendar Wrapper -->
878
879 </div>
880
881 <?php
882
883 }
884
885 /**
886 * Generates the HTML for a single post item in the calendar
887 * @param obj $post The WordPress post in question
888 * @param str $post_date The date of the post
889 * @param int $num The index of the post
890 *
891 * @return str HTML for a single post item
892 */
893 function generate_post_li_html( $post, $post_date, $num = 0 ){
894
895 $can_modify = ( $this->current_user_can_modify_post( $post ) ) ? 'can_modify' : 'read_only';
896 $cache_key = $post->ID . $can_modify . '_' . get_current_user_id();
897 $cache_val = wp_cache_get( $cache_key, self::$post_li_html_cache_key );
898 // Because $num is pertinent to the display of the post LI, need to make sure that's what's in cache
899 if ( is_array( $cache_val ) && $cache_val['num'] == $num ) {
900 $this->hidden = $cache_val['hidden'];
901 return $cache_val['post_li_html'];
902 }
903
904 ob_start();
905 $post_id = $post->ID;
906 $edit_post_link = get_edit_post_link( $post_id );
907 $status_object = get_post_status_object( get_post_status( $post_id ) );
908
909 $post_classes = array(
910 'day-item',
911 'custom-status-' . $post->post_status,
912 );
913 // Only allow the user to drag the post if they have permissions to
914 // or if it's in an approved post status
915 // This is checked on the ajax request too.
916 if ( $this->current_user_can_modify_post( $post ) && !in_array( $post->post_status, $this->published_statuses ) )
917 $post_classes[] = 'sortable';
918
919 if ( in_array( $post->post_status, $this->published_statuses ) )
920 $post_classes[] = 'is-published';
921
922 // Hide posts over a certain number to prevent clutter, unless user is only viewing 1 or 2 weeks
923 $max_visible_posts = apply_filters( 'ef_calendar_max_visible_posts_per_date', $this->max_visible_posts_per_date);
924
925 if ( $num >= $max_visible_posts && $this->total_weeks > 2 ) {
926 $post_classes[] = 'hidden';
927 $this->hidden++;
928 }
929 $post_classes = apply_filters( 'ef_calendar_table_td_li_classes', $post_classes, $post_date, $post->ID );
930
931 ?>
932 <li class="<?php echo esc_attr( implode( ' ', $post_classes ) ); ?>" id="post-<?php echo esc_attr( $post->ID ); ?>">
933 <div style="clear:right;"></div>
934 <div class="item-static">
935 <div class="item-default-visible">
936 <div class="item-status"><span class="status-text"><?php echo esc_html( $status_object->label ); ?></span></div>
937 <div class="inner">
938 <span class="item-headline post-title"><strong><?php echo esc_html( _draft_or_post_title( $post->ID ) ); ?></strong></span>
939 </div>
940 <?php do_action( 'ef_calendar_item_html', $post->ID ); ?>
941 </div>
942 <div class="item-inner">
943 <?php $this->get_inner_information( $this->get_post_information_fields( $post ), $post ); ?>
944 </div>
945 </div>
946 </li>
947 <?php
948
949 $post_li_html = ob_get_contents();
950 ob_end_clean();
951
952 $post_li_cache = array(
953 'num' => $num,
954 'post_li_html' => $post_li_html,
955 'hidden' => $this->hidden,
956 );
957 wp_cache_set( $cache_key, $post_li_cache, self::$post_li_html_cache_key );
958
959 return $post_li_html;
960
961 } // generate_post_li_html()
962
963 /**
964 * get_inner_information description
965 * Functionality for generating the inner html elements on the calendar
966 * has been separated out so various ajax functions can reload certain
967 * parts of an inner html element.
968 * @param array $ef_calendar_item_information_fields
969 * @param WP_Post $post
970 * @param array $published_statuses
971 *
972 * @since 0.8
973 */
974 function get_inner_information( $ef_calendar_item_information_fields, $post ) {
975 ?>
976 <table class="item-information">
977 <?php foreach( $this->get_post_information_fields( $post ) as $field => $values ): ?>
978 <tr class="item-field item-information-<?php echo esc_attr( $field ); ?>">
979 <th class="label"><?php echo esc_html( $values['label'] ); ?>:</th>
980 <?php if ( $values['value'] && isset($values['type']) ): ?>
981 <?php if( isset( $values['editable'] ) && $this->current_user_can_modify_post( $post ) ) : ?>
982 <td class="value<?php if( $values['editable'] ) { ?> editable-value<?php } ?>"><?php echo esc_html( $values['value'] ); ?></td>
983 <?php if( $values['editable'] ): ?>
984 <td class="editable-html hidden" data-type="<?php echo esc_attr( $values['type'] ); ?>" data-metadataterm="<?php echo esc_attr( str_replace( 'editorial-metadata-', '', str_replace( 'tax_', '', $field ) ) ); ?>"><?php echo $this->get_editable_html( $values['type'], $values['value'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></td>
985 <?php endif; ?>
986 <?php else: ?>
987 <td class="value"><?php echo esc_html( $values['value'] ); ?></td>
988 <?php endif; ?>
989 <?php elseif( $values['value'] ): ?>
990 <td class="value"><?php echo esc_html( $values['value'] ); ?></td>
991 <?php else: ?>
992 <td class="value"><em class="none"><?php esc_html_e( 'None', 'edit-flow' ); ?></em></td>
993 <?php endif; ?>
994 </tr>
995 <?php endforeach; ?>
996 <?php do_action( 'ef_calendar_item_additional_html', $post->ID ); ?>
997 </table>
998 <?php
999 $post_type_object = get_post_type_object( $post->post_type );
1000 $item_actions = array();
1001 if ( $this->current_user_can_modify_post( $post ) ) {
1002 // Edit this post
1003 $item_actions['edit'] = '<a href="' . get_edit_post_link( $post->ID, true ) . '" title="' . esc_attr( __( 'Edit this item', 'edit-flow' ) ) . '">' . __( 'Edit', 'edit-flow' ) . '</a>';
1004 // Trash this post
1005 $item_actions['trash'] = '<a href="'. get_delete_post_link( $post->ID) . '" title="' . esc_attr( __( 'Trash this item' ), 'edit-flow' ) . '">' . __( 'Trash', 'edit-flow' ) . '</a>';
1006 // Preview/view this post
1007 if ( !in_array( $post->post_status, $this->published_statuses ) ) {
1008 $item_actions['view'] = '<a href="' . esc_url( apply_filters( 'preview_post_link', add_query_arg( 'preview', 'true', get_permalink( $post->ID ) ), $post ) ) . '" title="' . esc_attr( sprintf( __( 'Preview &#8220;%s&#8221;', 'edit-flow' ), $post->post_title ) ) . '" rel="permalink">' . __( 'Preview', 'edit-flow' ) . '</a>';
1009 } elseif ( 'trash' != $post->post_status ) {
1010 $item_actions['view'] = '<a href="' . get_permalink( $post->ID ) . '" title="' . esc_attr( sprintf( __( 'View &#8220;%s&#8221;', 'edit-flow' ), $post->post_title ) ) . '" rel="permalink">' . __( 'View', 'edit-flow' ) . '</a>';
1011 }
1012 //Save metadata
1013 $item_actions['save hidden'] = '<a href="#savemetadata" id="save-editorial-metadata" class="post-'. esc_attr( $post->ID ) .'" title="'. esc_attr( sprintf( __( 'Save &#8220;%s&#8221;', 'edit-flow' ), $post->post_title ) ) . '" >' . __( 'Save', 'edit-flow') . '</a>';
1014 }
1015 // Allow other plugins to add actions
1016 $item_actions = apply_filters( 'ef_calendar_item_actions', $item_actions, $post->ID );
1017 if ( count( $item_actions ) ) {
1018 echo '<div class="item-actions">';
1019 $html = '';
1020 foreach ( $item_actions as $class => $item_action ) {
1021 $html .= '<span class="' . esc_attr( $class ) . '">' . $item_action . ' | </span> '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1022 }
1023 echo rtrim( $html, '| ' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1024 echo '</div>';
1025 }
1026 ?>
1027 <div style="clear:right;"></div>
1028 <?php
1029
1030 } // generate_post_li_html()
1031
1032 function get_editable_html( $type, $value ) {
1033
1034 switch( $type ) {
1035 case 'text':
1036 case 'location':
1037 case 'number':
1038 return '<input type="text" class="metadata-edit-' . esc_attr( $type ) . '" value="' . esc_attr( $value ) . '"/>';
1039 break;
1040 case 'paragraph':
1041 return '<textarea type="text" class="metadata-edit-' . esc_attr( $type ) . '">' . esc_html( $value ) . '</textarea>';
1042 break;
1043 case 'date':
1044 return '<input type="text" value="' . esc_attr( $value ) . '" class="date-time-pick metadata-edit-' . esc_attr( $type ) . '"/>';
1045 break;
1046 case 'checkbox':
1047 $output = '<select class="metadata-edit">';
1048
1049 if( $value == 'No' )
1050 $output .= '<option value="0">No</option><option value="1">Yes</option>';
1051 else
1052 $output .= '<option value="1">Yes</option><option value="0">No</option>';
1053
1054 $output .= '</select>';
1055
1056 return $output;
1057 break;
1058 case 'user':
1059 return wp_dropdown_users( array( 'echo' => false ) );
1060 break;
1061 case 'taxonomy':
1062 return '<input type="text" class="metadata-edit-' . esc_attr( $type ) . '" value="' . esc_attr( $value ) . '" />';
1063 break;
1064 case 'taxonomy hierarchical':
1065 return wp_dropdown_categories( array( 'echo' => 0, 'hide_empty' => 0 ) );
1066 break;
1067 }
1068 }
1069
1070 /**
1071 * Get the information fields to be presented with each post popup
1072 *
1073 * @since 0.8
1074 *
1075 * @param obj $post Post to gather information fields for
1076 * @return array $information_fields All of the information fields to be presented
1077 */
1078 function get_post_information_fields( $post ) {
1079
1080 $information_fields = array();
1081 // Post author
1082 $information_fields['author'] = array(
1083 'label' => __( 'Author', 'edit-flow' ),
1084 'value' => get_the_author_meta( 'display_name', $post->post_author ),
1085 'type' => 'author',
1086 );
1087
1088 // If the calendar supports more than one post type, show the post type label
1089 if ( count( $this->get_post_types_for_module( $this->module ) ) > 1 ) {
1090 $information_fields['post_type'] = array(
1091 'label' => __( 'Post Type', 'edit-flow' ),
1092 'value' => get_post_type_object( $post->post_type )->labels->singular_name,
1093 );
1094 }
1095 // Publication time for published statuses
1096 $published_statuses = array(
1097 'publish',
1098 'future',
1099 'private',
1100 );
1101 if ( in_array( $post->post_status, $published_statuses ) ) {
1102 if ( $post->post_status == 'future' ) {
1103 $information_fields['post_date'] = array(
1104 'label' => __( 'Scheduled', 'edit-flow' ),
1105 'value' => get_the_time( null, $post->ID ),
1106 );
1107 } else {
1108 $information_fields['post_date'] = array(
1109 'label' => __( 'Published', 'edit-flow' ),
1110 'value' => get_the_time( null, $post->ID ),
1111 );
1112 }
1113 }
1114 // Taxonomies and their values
1115 $args = array(
1116 'post_type' => $post->post_type,
1117 );
1118 $taxonomies = get_object_taxonomies( $args, 'object' );
1119 foreach( (array)$taxonomies as $taxonomy ) {
1120 // Sometimes taxonomies skip by, so let's make sure it has a label too
1121 if ( !$taxonomy->public || !$taxonomy->label )
1122 continue;
1123
1124 $terms = get_the_terms( $post->ID, $taxonomy->name );
1125 if ( ! $terms || is_wp_error( $terms ) )
1126 continue;
1127
1128 $key = 'tax_' . $taxonomy->name;
1129 if ( count( $terms ) ) {
1130 $value = '';
1131 foreach( (array)$terms as $term ) {
1132 $value .= $term->name . ', ';
1133 }
1134 $value = rtrim( $value, ', ' );
1135 } else {
1136 $value = '';
1137 }
1138 //Used when editing editorial metadata and post meta
1139 if ( is_taxonomy_hierarchical( $taxonomy->name ) )
1140 $type = 'taxonomy hierarchical';
1141 else
1142 $type = 'taxonomy';
1143
1144 $information_fields[$key] = array(
1145 'label' => $taxonomy->label,
1146 'value' => $value,
1147 'type' => $type,
1148 );
1149
1150 if( $post->post_type == 'page' )
1151 $ed_cap = 'edit_page';
1152 else
1153 $ed_cap = 'edit_post';
1154
1155 if( current_user_can( $ed_cap, $post->ID ) )
1156 $information_fields[$key]['editable'] = true;
1157 }
1158
1159 $information_fields = apply_filters( 'ef_calendar_item_information_fields', $information_fields, $post->ID );
1160 foreach( $information_fields as $field => $values ) {
1161 // Allow filters to hide empty fields or to hide any given individual field. Hide empty fields by default.
1162 if ( ( apply_filters( 'ef_calendar_hide_empty_item_information_fields', true, $post->ID ) && empty( $values['value'] ) )
1163 || apply_filters( "ef_calendar_hide_{$field}_item_information_field", false, $post->ID ) )
1164 unset( $information_fields[$field] );
1165 }
1166 return $information_fields;
1167 }
1168
1169 /**
1170 * Generate the calendar header for a given range of dates
1171 *
1172 * @param array $dates Date range for the header
1173 * @return string $html Generated HTML for the header
1174 */
1175 function get_time_period_header( $dates ) {
1176
1177 $html = '';
1178 foreach( $dates as $date ) {
1179 $html .= '<th class="column-heading" >';
1180 $html .= esc_html( date_i18n('l', strtotime( $date ) ) );
1181 $html .= '</th>';
1182 }
1183
1184 return $html;
1185
1186 }
1187
1188 /**
1189 * Query to get all of the calendar posts for a given day
1190 *
1191 * @param array $args Any filter arguments we want to pass
1192 * @param string $request_context Where the query is coming from, to distinguish dashboard and subscriptions
1193 * @return array $posts All of the posts as an array sorted by date
1194 */
1195 function get_calendar_posts_for_week( $args = array(), $context = 'dashboard' ) {
1196
1197 $supported_post_types = $this->get_post_types_for_module( $this->module );
1198 $defaults = array(
1199 'post_status' => null,
1200 'cat' => null,
1201 'author' => null,
1202 'post_type' => $supported_post_types,
1203 'posts_per_page' => 200,
1204 );
1205
1206 $args = array_merge( $defaults, $args );
1207
1208 // Unpublished as a status is just an array of everything but 'publish'.
1209 if ( 'unpublish' == $args['post_status'] ) {
1210 $args['post_status'] = '';
1211 $post_stati = wp_filter_object_list( $this->get_calendar_post_stati(), array( 'name' => 'publish' ), 'not' );
1212
1213 if ( ! apply_filters( 'ef_show_scheduled_as_unpublished', false ) ) {
1214 $post_stati = wp_filter_object_list( $post_stati, array( 'name' => 'future' ), 'not' );
1215 }
1216
1217 $args['post_status'] .= join( wp_list_pluck( $post_stati, 'name' ), ',' );
1218 }
1219 // The WP functions for printing the category and author assign a value of 0 to the default
1220 // options, but passing this to the query is bad (trashed and auto-draft posts appear!), so
1221 // unset those arguments.
1222 if ( $args['cat'] === '0' ) {
1223 unset( $args['cat'] );
1224 }
1225 if ( $args['author'] === '0' ) {
1226 unset( $args['author'] );
1227 }
1228
1229 if ( empty( $args['post_type'] ) || ! in_array( $args['post_type'], $supported_post_types ) ) {
1230 $args['post_type'] = $supported_post_types;
1231 }
1232
1233 $beginning_date = $this->get_beginning_of_week( $this->start_date, 'Y-m-d', $this->current_week );
1234 $ending_date = date( "Y-m-d", strtotime( $beginning_date ) + WEEK_IN_SECONDS );
1235
1236 $args['date_query'] = array(
1237 'after' => $beginning_date,
1238 'before' => $ending_date,
1239 'inclusive' => true,
1240 );
1241
1242 // Filter for an end user to implement any of their own query args
1243 $args = apply_filters( 'ef_calendar_posts_query_args', $args, $context );
1244 $post_results = new WP_Query( $args );
1245
1246 $posts = array();
1247 while ( $post_results->have_posts() ) {
1248 $post_results->the_post();
1249 global $post;
1250 $key_date = date( 'Y-m-d', strtotime( $post->post_date ) );
1251 $posts[$key_date][] = $post;
1252 }
1253
1254 return $posts;
1255
1256 }
1257
1258 /**
1259 * Gets the link for the next time period
1260 *
1261 * @param string $direction 'previous' or 'next', direction to go in time
1262 * @param array $filters Any filters that need to be applied
1263 * @param int $weeks_offset Number of weeks we're offsetting the range
1264 * @return string $url The URL for the next page
1265 */
1266 function get_pagination_link( $direction = 'next', $filters = array(), $weeks_offset = null ) {
1267
1268 $supported_post_types = $this->get_post_types_for_module( $this->module );
1269
1270 if ( !isset( $weeks_offset ) )
1271 $weeks_offset = $this->total_weeks;
1272 else if ( $weeks_offset == 0 )
1273 $filters['start_date'] = $this->get_beginning_of_week( date( 'Y-m-d', current_time( 'timestamp' ) ) );
1274
1275 if ( $direction == 'previous' )
1276 $weeks_offset = '-' . $weeks_offset;
1277
1278 $filters['start_date'] = date( 'Y-m-d', strtotime( $weeks_offset . " weeks", strtotime( $filters['start_date'] ) ) );
1279 $url = add_query_arg( $filters, menu_page_url( $this->module->slug, false ) );
1280
1281 if ( count( $supported_post_types ) > 1 )
1282 $url = add_query_arg( 'cpt', $filters['cpt'] , $url );
1283
1284 return $url;
1285
1286 }
1287
1288 /**
1289 * Given a day in string format, returns the day at the beginning of that week, which can be the given date.
1290 * The beginning of the week is determined by the blog option, 'start_of_week'.
1291 *
1292 * @see http://www.php.net/manual/en/datetime.formats.date.php for valid date formats
1293 *
1294 * @param string $date String representing a date
1295 * @param string $format Date format in which the beginning of the week should be returned
1296 * @param int $week Number of weeks we're offsetting the range
1297 * @return string $formatted_start_of_week Beginning of the week
1298 */
1299 function get_beginning_of_week( $date, $format = 'Y-m-d', $week = 1 ) {
1300
1301 $date = strtotime( $date );
1302 $start_of_week = get_option( 'start_of_week' );
1303 $day_of_week = date( 'w', $date );
1304 $date += (( $start_of_week - $day_of_week - 7 ) % 7) * 60 * 60 * 24 ;
1305 $date = strtotime ( '+' . ( $week - 1 ) . ' week', $date ) ;
1306 $formatted_start_of_week = date( $format, $date );
1307 return $formatted_start_of_week;
1308
1309 }
1310
1311 /**
1312 * Given a day in string format, returns the day at the end of that week, which can be the given date.
1313 * The end of the week is determined by the blog option, 'start_of_week'.
1314 *
1315 * @see http://www.php.net/manual/en/datetime.formats.date.php for valid date formats
1316 *
1317 * @param string $date String representing a date
1318 * @param string $format Date format in which the end of the week should be returned
1319 * @param int $week Number of weeks we're offsetting the range
1320 * @return string $formatted_end_of_week End of the week
1321 */
1322 function get_ending_of_week( $date, $format = 'Y-m-d', $week = 1 ) {
1323
1324 $date = strtotime( $date );
1325 $end_of_week = get_option( 'start_of_week' ) - 1;
1326 $day_of_week = date( 'w', $date );
1327 $date += (( $end_of_week - $day_of_week + 7 ) % 7) * 60 * 60 * 24;
1328 $date = strtotime ( '+' . ( $week - 1 ) . ' week', $date ) ;
1329 $formatted_end_of_week = date( $format, $date );
1330 return $formatted_end_of_week;
1331
1332 }
1333
1334 /**
1335 * Human-readable time range for the calendar
1336 * Shows something like "for October 30th through November 26th" for a four-week period
1337 *
1338 * @since 0.7
1339 */
1340 function calendar_time_range() {
1341
1342 $first_datetime = strtotime( $this->start_date );
1343 $first_date = date_i18n( get_option( 'date_format' ), $first_datetime );
1344 $total_days = ( $this->total_weeks * 7 ) - 1;
1345 $last_datetime = strtotime( "+" . $total_days . " days", date( 'U', strtotime( $this->start_date ) ) );
1346 $last_date = date_i18n( get_option( 'date_format' ), $last_datetime );
1347 echo esc_html( sprintf( __( 'for %1$s through %2$s', 'edit-flow' ), $first_date, $last_date ) );
1348 }
1349
1350 /**
1351 * Check whether the current user should have the ability to modify the post
1352 *
1353 * @since 0.7
1354 *
1355 * @param object $post The post object we're checking
1356 * @return bool $can Whether or not the current user can modify the post
1357 */
1358 function current_user_can_modify_post( $post ) {
1359
1360 if ( !$post )
1361 return false;
1362
1363 $post_type_object = get_post_type_object( $post->post_type );
1364
1365 // Editors and admins are fine
1366 if ( current_user_can( $post_type_object->cap->edit_others_posts, $post->ID ) )
1367 return true;
1368 // Authors and contributors can move their own stuff if it's not published
1369 if ( current_user_can( $post_type_object->cap->edit_post, $post->ID ) && wp_get_current_user()->ID == $post->post_author && !in_array( $post->post_status, $this->published_statuses ) )
1370 return true;
1371 // Those who can publish posts can move any of their own stuff
1372 if ( current_user_can( $post_type_object->cap->publish_posts, $post->ID ) && wp_get_current_user()->ID == $post->post_author )
1373 return true;
1374
1375 return false;
1376 }
1377
1378 /**
1379 * Register settings for notifications so we can partially use the Settings API
1380 * We use the Settings API for form generation, but not saving because we have our
1381 * own way of handling the data.
1382 *
1383 * @since 0.7
1384 */
1385 function register_settings() {
1386
1387 add_settings_section( $this->module->options_group_name . '_general', false, '__return_false', $this->module->options_group_name );
1388 add_settings_field( 'post_types', __( 'Post types to show', 'edit-flow' ), array( $this, 'settings_post_types_option' ), $this->module->options_group_name, $this->module->options_group_name . '_general' );
1389 add_settings_field( 'quick_create_post_type', __( 'Post type to create directly from calendar', 'edit-flow' ), array( $this, 'settings_quick_create_post_type_option' ), $this->module->options_group_name, $this->module->options_group_name . '_general' );
1390 add_settings_field( 'ics_subscription', __( 'Subscription in iCal or Google Calendar', 'edit-flow' ), array( $this, 'settings_ics_subscription_option' ), $this->module->options_group_name, $this->module->options_group_name . '_general' );
1391
1392 }
1393
1394 /**
1395 * Choose the post types that should be displayed on the calendar
1396 *
1397 * @since 0.7
1398 */
1399 function settings_post_types_option() {
1400 global $edit_flow;
1401 $edit_flow->settings->helper_option_custom_post_type( $this->module );
1402 }
1403
1404 /**
1405 * Choose the post type that should be created on the calendar
1406 *
1407 * @since 0.8
1408 */
1409 function settings_quick_create_post_type_option() {
1410
1411 $allowed_post_types = $this->get_all_post_types();
1412
1413 echo "<select name='" . esc_attr( $this->module->options_group_name ) . "[quick_create_post_type]'>";
1414 foreach( $allowed_post_types as $post_type => $title )
1415 echo "<option value='" . esc_attr( $post_type ) . "' " . selected( $post_type, $this->module->options->quick_create_post_type, false ) . ">".esc_html( $title )."</option>";
1416 echo "</select>";
1417
1418 }
1419
1420 /**
1421 * Enable calendar subscriptions via .ics in iCal or Google Calendar
1422 *
1423 * @since 0.8
1424 */
1425 function settings_ics_subscription_option() {
1426 $options = array(
1427 'off' => __( 'Disabled', 'edit-flow' ),
1428 'on' => __( 'Enabled', 'edit-flow' ),
1429 );
1430 echo '<select id="ics_subscription" name="' . esc_attr( $this->module->options_group_name ) . '[ics_subscription]">';
1431 foreach ( $options as $value => $label ) {
1432 echo '<option value="' . esc_attr( $value ) . '"';
1433 echo selected( $this->module->options->ics_subscription, $value );
1434 echo '>' . esc_html( $label ) . '</option>';
1435 }
1436 echo '</select>';
1437
1438
1439 $regenerate_url = add_query_arg( 'action', 'ef_calendar_regenerate_calendar_feed_secret', admin_url( 'index.php' ) );
1440 $regenerate_url = wp_nonce_url( $regenerate_url, 'ef-regenerate-ics-key' );
1441 echo '&nbsp;&nbsp;&nbsp;<a href="' . esc_url( $regenerate_url ) . '">' . esc_html_e( 'Regenerate calendar feed secret', 'edit-flow' ) . '</a>';
1442
1443 // If our secret key doesn't exist, create a new one
1444 if ( empty( $this->module->options->ics_secret_key ) )
1445 EditFlow()->update_module_option( $this->module->name, 'ics_secret_key', wp_generate_password() );
1446 }
1447
1448 /**
1449 * Validate the data submitted by the user in calendar settings
1450 *
1451 * @since 0.7
1452 */
1453 function settings_validate( $new_options ) {
1454
1455 $options = (array)$this->module->options;
1456
1457 $options['post_types'] = $this->clean_post_type_options( $new_options['post_types'], $this->module->post_type_support );
1458
1459 if ( in_array( $new_options['quick_create_post_type'], array_keys( $this->get_all_post_types() ) ) )
1460 $options['quick_create_post_type'] = $new_options['quick_create_post_type'];
1461
1462 if ( 'on' != $new_options['ics_subscription'] )
1463 $options['ics_subscription'] = 'off';
1464 else
1465 $options['ics_subscription'] = 'on';
1466
1467 return $options;
1468 }
1469
1470 /**
1471 * Settings page for calendar
1472 */
1473 function print_configure_view() {
1474 global $edit_flow;
1475 ?>
1476 <form class="basic-settings" action="<?php echo esc_url( menu_page_url( $this->module->settings_slug, false ) ); ?>" method="post">
1477 <?php settings_fields( $this->module->options_group_name ); ?>
1478 <?php do_settings_sections( $this->module->options_group_name ); ?>
1479 <?php
1480 echo '<input id="edit_flow_module_name" name="edit_flow_module_name" type="hidden" value="' . esc_attr( $this->module->name ) . '" />';
1481 ?>
1482 <p class="submit"><?php submit_button( null, 'primary', 'submit', false ); ?><a class="cancel-settings-link" href="<?php echo esc_url( EDIT_FLOW_SETTINGS_PAGE ); ?>"><?php esc_html_e( 'Back to Edit Flow', 'edit-flow' ); ?></a></p>
1483 </form>
1484 <?php
1485 }
1486
1487 /**
1488 * Ajax callback to insert a post placeholder for a particular date
1489 *
1490 * @since 0.8
1491 */
1492 function handle_ajax_insert_post() {
1493
1494 // Nonce check!
1495 if ( !isset( $_POST['nonce'] ) || !wp_verify_nonce( $_POST['nonce'], 'ef-calendar-modify' ) ) {
1496 $this->print_ajax_response( 'error', $this->module->messages['nonce-failed'] );
1497 }
1498
1499 // Check that the user has the right capabilities to add posts to the calendar (defaults to 'edit_posts')
1500 if ( !current_user_can( $this->create_post_cap ) )
1501 $this->print_ajax_response( 'error', $this->module->messages['invalid-permissions'] );
1502
1503 if ( empty( $_POST['ef_insert_date'] ) ) {
1504 $this->print_ajax_response( 'error', __( 'No date supplied.', 'edit-flow' ) );
1505 }
1506
1507 // Post type has to be visible on the calendar to create a placeholder
1508 if ( ! in_array( $this->module->options->quick_create_post_type, $this->get_post_types_for_module( $this->module ) ) )
1509 $this->print_ajax_response( 'error', __( 'Please change Quick Create to use a post type viewable on the calendar.', 'edit-flow' ) );
1510
1511 // Sanitize post values
1512 $post_title = isset( $_POST['ef_insert_title'] ) ? sanitize_text_field( $_POST['ef_insert_title'] ) : null;
1513
1514 if( ! $post_title ) {
1515 $post_title = esc_html__( 'Untitled', 'edit-flow' );
1516 }
1517
1518 $post_date = sanitize_text_field( $_POST['ef_insert_date'] );
1519
1520 $post_status = $this->get_default_post_status();
1521
1522 // Set new post parameters
1523 $post_placeholder = array(
1524 'post_title' => $post_title,
1525 'post_status' => $post_status,
1526 'post_date' => date( 'Y-m-d H:i:s', strtotime( $post_date ) ),
1527 'post_type' => $this->module->options->quick_create_post_type,
1528 );
1529
1530 // By default, adding a post to the calendar won't set the timestamp.
1531 // If the user desires that to be the behavior, they can set the result of this filter to 'true'
1532 // With how WordPress works internally, setting 'post_date_gmt' will set the timestamp
1533 if ( apply_filters( 'ef_calendar_allow_ajax_to_set_timestamp', false ) )
1534 $post_placeholder['post_date_gmt'] = date( 'Y-m-d H:i:s', strtotime( $post_date ) );
1535
1536 // Create the post
1537 $post_id = wp_insert_post( $post_placeholder );
1538
1539 if( $post_id ) { // success!
1540
1541 $post = get_post( $post_id );
1542
1543 // Generate the HTML for the post item so it can be injected
1544 $post_li_html = $this->generate_post_li_html( $post, $post_date );
1545
1546 // announce success and send back the html to inject
1547 $this->print_ajax_response( 'success', $post_li_html );
1548
1549 } else {
1550 $this->print_ajax_response( 'error', __( 'Post could not be created', 'edit-flow' ) );
1551 }
1552 }
1553
1554 /**
1555 * Returns the singular label for the posts that are
1556 * quick-created on the calendar
1557 *
1558 * @return str Singular label for a post-type
1559 */
1560 function get_quick_create_post_type_name(){
1561
1562 $post_type_slug = $this->module->options->quick_create_post_type;
1563 $post_type_obj = get_post_type_object( $post_type_slug );
1564
1565 return $post_type_obj->labels->singular_name ? $post_type_obj->labels->singular_name : $post_type_slug;
1566 }
1567
1568 /**
1569 * ajax_ef_calendar_update_metadata
1570 * Update the metadata from the calendar.
1571 * @return string representing the overlay
1572 *
1573 * @since 0.8
1574 */
1575 function handle_ajax_update_metadata() {
1576 global $wpdb;
1577
1578 if ( !isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'ef-calendar-modify' ) ) {
1579 $this->print_ajax_response( 'error', $this->module->messages['nonce-failed'] );
1580 }
1581
1582 if ( !isset( $_POST['post_id'] ) ) {
1583 $this->print_ajax_response( 'error', $this->module->messages['missing-post'] );
1584 }
1585
1586 // Check that we got a proper post
1587 $post_id = ( int )$_POST['post_id'];
1588 $post = get_post( $post_id );
1589
1590 if ( ! $post )
1591 $this->print_ajax_response( 'error', $this->module->messages['missing-post'] );
1592
1593
1594 if( $post->post_type == 'page' )
1595 $edit_check = 'edit_page';
1596 else
1597 $edit_check = 'edit_post';
1598
1599 if ( !current_user_can( $edit_check, $post->ID ) )
1600 $this->print_ajax_response( 'error', $this->module->messages['invalid-permissions'] );
1601
1602 // Check that the user can modify the post
1603 if ( ! $this->current_user_can_modify_post( $post ) )
1604 $this->print_ajax_response( 'error', $this->module->messages['invalid-permissions'] );
1605
1606 $default_types = array(
1607 'author',
1608 'taxonomy',
1609 );
1610
1611 $metadata_types = array();
1612
1613 if ( !$this->module_enabled( 'editorial_metadata' ) )
1614 $this->print_ajax_response( 'error', $this->module->messages['update-error'] );
1615
1616 $metadata_types = array_keys( EditFlow()->editorial_metadata->get_supported_metadata_types() );
1617
1618 // Update an editorial metadata field
1619 $metadata_term = isset( $_POST['metadata_term'] ) ? $_POST['metadata_term'] : '';
1620 $metadata_type = isset( $_POST['metadata_type'] ) ? $_POST['metadata_type'] : '';
1621 $incoming_metadata_value = isset( $_POST['metadata_value'] ) ? $_POST['metadata_value'] : '';
1622
1623 if ( isset( $_POST['metadata_type'] ) && in_array( $_POST['metadata_type'], $metadata_types ) ) {
1624 $post_meta_key = sanitize_text_field( '_ef_editorial_meta_' . $_POST['metadata_type'] . '_' . $metadata_term );
1625
1626 //Javascript date parsing is terrible, so use strtotime in php
1627 if ( $metadata_type == 'date' )
1628 $metadata_value = strtotime( sanitize_text_field( $incoming_metadata_value ) );
1629 else
1630 $metadata_value = sanitize_text_field( $incoming_metadata_value );
1631
1632 update_post_meta( $post->ID, $post_meta_key, $metadata_value );
1633 $response = 'success';
1634 } else {
1635 switch( $_POST['metadata_type'] ) {
1636 case 'taxonomy':
1637 case 'taxonomy hierarchical':
1638 $response = wp_set_post_terms( $post->ID, $incoming_metadata_value, $metadata_term );
1639 break;
1640 default:
1641 $response = new WP_Error( 'invalid-type', __( 'Invalid metadata type', 'edit-flow' ) );
1642 break;
1643 }
1644 }
1645
1646 //Assuming we've got to this point, just regurgitate the value
1647 if ( ! is_wp_error( $response ) )
1648 $this->print_ajax_response( 'success', $incoming_metadata_value );
1649 else
1650 $this->print_ajax_response( 'error', __( 'Metadata could not be updated.', 'edit-flow' ) );
1651 }
1652
1653 function calendar_filters() {
1654 $select_filter_names = array();
1655
1656 $select_filter_names['post_status'] = 'post_status';
1657 $select_filter_names['cat'] = 'cat';
1658 $select_filter_names['author'] = 'author';
1659 $select_filter_names['type'] = 'cpt';
1660 $select_filter_name['num_weeks'] = 'num_weeks';
1661
1662 return apply_filters( 'ef_calendar_filter_names', $select_filter_names );
1663 }
1664
1665 /**
1666 * Sanitize a $_GET or similar filter being used on the calendar
1667 *
1668 * @since 0.8
1669 *
1670 * @param string $key Filter being sanitized
1671 * @param string $dirty_value Value to be sanitized
1672 * @return string $sanitized_value Safe to use value
1673 */
1674 function sanitize_filter( $key, $dirty_value ) {
1675
1676 switch( $key ) {
1677 case 'post_status':
1678 // Whitelist-based validation for this parameter
1679 $valid_statuses = wp_list_pluck( $this->get_calendar_post_stati(), 'name' );
1680 $valid_statuses[] = 'unpublish';
1681
1682 if ( in_array( $dirty_value, $valid_statuses ) )
1683 return $dirty_value;
1684 else
1685 return '';
1686 break;
1687 case 'cpt':
1688 $cpt = sanitize_key( $dirty_value );
1689 $supported_post_types = $this->get_post_types_for_module( $this->module );
1690 if ( $cpt && in_array( $cpt, $supported_post_types ) )
1691 return $cpt;
1692 else
1693 return '';
1694 break;
1695 case 'start_date':
1696 return date( 'Y-m-d', strtotime( $dirty_value ) );
1697 break;
1698 case 'cat':
1699 case 'author':
1700 return intval( $dirty_value );
1701 break;
1702 case 'num_weeks':
1703 $num_weeks = intval( $dirty_value );
1704 if ( $num_weeks <= 0 ) {
1705 return $this->total_weeks;
1706 } else if ( $num_weeks > $this->max_weeks ) {
1707 return $this->max_weeks;
1708 } else {
1709 return $num_weeks;
1710 }
1711 default:
1712 return false;
1713 break;
1714 }
1715 }
1716
1717 /**
1718 * When a post is updated, clean the <li> html post cache for it
1719 */
1720 public function action_clean_li_html_cache( $post_id ) {
1721
1722 wp_cache_delete( $post_id . 'can_modify', self::$post_li_html_cache_key );
1723 wp_cache_delete( $post_id . 'read_only', self::$post_li_html_cache_key );
1724 }
1725
1726 /**
1727 * This is a hack! hack! hack! until core is fixed
1728 *
1729 * The calendar uses 'post_date' field to store the position on the calendar
1730 * If a post has a core post status assigned (e.g. 'draft' or 'pending'), the `post_date`
1731 * field will be reset when `wp_update_post()`
1732 * is used: http://core.trac.wordpress.org/browser/tags/3.7.1/src/wp-includes/post.php#L2998
1733 *
1734 * This method temporarily caches the `post_date` field if it needs to be restored.
1735 *
1736 * @uses fix_post_date_on_update_part_two()
1737 */
1738 public function fix_post_date_on_update_part_one( $post_ID, $data ) {
1739
1740 $post = get_post( $post_ID );
1741
1742 // `post_date` is only nooped for these three statuses,
1743 // but don't try to persist if `post_date_gmt` is set
1744 if ( ! in_array( $post->post_status, array( 'draft', 'pending', 'auto-draft' ) )
1745 || '0000-00-00 00:00:00' !== $post->post_date_gmt
1746 || '0000-00-00 00:00:00' !== $data['post_date_gmt'] )
1747 return;
1748
1749 $this->post_date_cache[ $post_ID ] = $post->post_date;
1750
1751 }
1752
1753 /**
1754 * This is a hack! hack! hack! until core is fixed
1755 *
1756 * The calendar uses 'post_date' field to store the position on the calendar
1757 * If a post has a core post status assigned (e.g. 'draft' or 'pending'), the `post_date`
1758 * field will be reset when `wp_update_post()`
1759 * is used: http://core.trac.wordpress.org/browser/tags/3.7.1/src/wp-includes/post.php#L2998
1760 *
1761 * This method restores the `post_date` field if it needs to be restored.
1762 *
1763 * @uses fix_post_date_on_update_part_one()
1764 */
1765 public function fix_post_date_on_update_part_two( $post_ID, $post_after, $post_before ) {
1766 global $wpdb;
1767
1768 if ( empty( $this->post_date_cache[ $post_ID ] ) )
1769 return;
1770
1771 $post_date = $this->post_date_cache[ $post_ID ];
1772 unset( $this->post_date_cache[ $post_ID ] );
1773 $wpdb->update( $wpdb->posts, array( 'post_date' => $post_date ), array( 'ID' => $post_ID ) );
1774 clean_post_cache( $post_ID );
1775 }
1776
1777 /**
1778 * Returns a list of custom status objects used by the calendar
1779 *
1780 * @return array An array of StdClass objects representing statuses
1781 */
1782 public function get_calendar_post_stati() {
1783 $post_stati = get_post_stati( array(), 'object' );
1784 $custom_status_slugs = wp_list_pluck( $this->get_post_statuses(), 'slug' );
1785 $custom_status_slugs[] = 'future';
1786 $custom_status_slugs[] = 'publish';
1787
1788 $custom_status_slug_keys = array_flip( $custom_status_slugs );
1789
1790 $final_statuses = [];
1791
1792 foreach( $post_stati as $status ) {
1793 if ( !empty( $custom_status_slug_keys[ $status->name ] ) ) {
1794 $final_statuses[] = $status;
1795 }
1796 }
1797
1798 return apply_filters( 'ef_calendar_post_stati', $final_statuses );
1799 }
1800
1801 public function get_calendar_dropdown_users() {
1802 $users_dropdown_args = array(
1803 'orderby' => 'display_name',
1804 'order' => 'ASC',
1805 'blog_id' => get_current_blog_id()
1806 );
1807
1808 $users_dropdown_args = apply_filters( 'ef_calendar_dropdown_users_args', $users_dropdown_args );
1809
1810 return get_users( $users_dropdown_args );
1811 }
1812
1813 public function get_calendar_categories_dropdown() {
1814 $categories_dropdown_args = array(
1815 'orderby' => 'id',
1816 'order' => 'ASC',
1817 'hide_empty' => 0,
1818 'hierarchical' => 0,
1819 'taxonomy' => 'category'
1820 );
1821
1822 return get_terms( $categories_dropdown_args );
1823 }
1824
1825 public function get_calendar_frontend_config() {
1826 global $wp_version;
1827
1828 $all_post_types = get_post_types( null, 'objects' );
1829
1830 $config = array(
1831 'POST_STATI' => array_map(
1832 function( $item ) {
1833 return array(
1834 'name' => $item->name,
1835 'label' => $item->label,
1836 );
1837 },
1838 $this->get_calendar_post_stati()
1839 ),
1840 'USERS' => array_map(
1841 function( $item ) {
1842 return array(
1843 'id' => $item->ID,
1844 'display_name' => $item->display_name,
1845 );
1846 },
1847 $this->get_calendar_dropdown_users()
1848 ),
1849 'CATEGORIES' => array_map(
1850 function( $item ) {
1851 return array(
1852 'term_id' => $item->term_id,
1853 'name' => $item->name,
1854 'parent' => $item->parent
1855 );
1856 },
1857 $this->get_calendar_categories_dropdown()
1858 ),
1859 'CALENDAR_POST_TYPES' => array_map( function ( $item ) use ( $all_post_types ) {
1860 return array(
1861 'name' => $all_post_types[ $item ]->name,
1862 'label' => $all_post_types[ $item ]->label
1863 );
1864 }, $this->get_post_types_for_module( $this->module ) ),
1865 'NUM_WEEKS' => array(
1866 'MAX' => $this->max_weeks,
1867 'DEFAULT' => $this->total_weeks,
1868 ),
1869 'BEGINNING_OF_WEEK' => $this->get_beginning_of_week( date( 'Y-m-d', current_time( 'timestamp' ) ) ),
1870 'FILTERS' => $this->get_filters(),
1871 'PAGE_URL' => menu_page_url( $this->module->slug, false ),
1872 'WP_VERSION' => $wp_version
1873 );
1874
1875 return apply_filters( 'ef_calendar_frontend_config', $config );
1876 }
1877
1878 } // EF_Calendar
1879
1880 } // class_exists('EF_Calendar')
1881