PluginProbe
Edit Flow / 0.9
Edit Flow v0.9
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 / custom-status / custom-status.php

custom-status.php in Edit Flow 0.9, at modules/custom-status/custom-status.php

1,996 lines 73.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * class EF_Custom_Status
4 * Custom statuses make it simple to define the different stages in your publishing workflow.
5 *
6 * @todo for v0.7
7 * - Improve the copy
8 * - Thoroughly test what happens when the default post statuses 'Draft' and 'Pending Review' no longer exist
9 * - Ensure all of the form processing uses our messages functionality
10 */
11
12
13 if ( !class_exists( 'EF_Custom_Status' ) ) {
14
15 class EF_Custom_Status extends EF_Module {
16
17 var $module;
18
19 private $custom_statuses_cache = array();
20
21 /**
22 * Define the hooks that need to be unhooked/rehooked to make the module Gutenberg-ready.
23 *
24 * @var array
25 */
26 protected $compat_hooks = [
27 'admin_enqueue_scripts' => 'action_admin_enqueue_scripts',
28 ];
29
30 // This is taxonomy name used to store all our custom statuses
31 const taxonomy_key = 'post_status';
32
33 /**
34 * Register the module with Edit Flow but don't do anything else
35 */
36 function __construct() {
37
38 $this->module_url = $this->get_module_url( __FILE__ );
39 // Register the module with Edit Flow
40 $args = array(
41 'title' => __( 'Custom Statuses', 'edit-flow' ),
42 'short_description' => __( 'Create custom post statuses to define the stages of your workflow.', 'edit-flow' ),
43 'extended_description' => __( 'Create your own post statuses to add structure your publishing workflow. You can change existing or add new ones anytime, and drag and drop to change their order.', 'edit-flow' ),
44 'module_url' => $this->module_url,
45 'img_url' => $this->module_url . 'lib/custom_status_s128.png',
46 'slug' => 'custom-status',
47 'default_options' => array(
48 'enabled' => 'on',
49 'default_status' => 'pitch',
50 'always_show_dropdown' => 'off',
51 'post_types' => array(
52 'post' => 'on',
53 'page' => 'on',
54 ),
55 ),
56 'post_type_support' => 'ef_custom_statuses', // This has been plural in all of our docs
57 'configure_page_cb' => 'print_configure_view',
58 'configure_link_text' => __( 'Edit Statuses', 'edit-flow' ),
59 'messages' => array(
60 'status-added' => __( 'Post status created.', 'edit-flow' ),
61 'status-missing' => __( "Post status doesn't exist.", 'edit-flow' ),
62 'default-status-changed' => __( 'Default post status has been changed.', 'edit-flow'),
63 'term-updated' => __( "Post status updated.", 'edit-flow' ),
64 'status-deleted' => __( 'Post status deleted.', 'edit-flow' ),
65 'status-position-updated' => __( "Status order updated.", 'edit-flow' ),
66 ),
67 'autoload' => false,
68 'settings_help_tab' => array(
69 'id' => 'ef-custom-status-overview',
70 'title' => __('Overview', 'edit-flow'),
71 'content' => __('<p>Edit Flow’s custom statuses allow you to define the most important stages of your editorial workflow. Out of the box, WordPress only offers “Draft” and “Pending Review” as post states. With custom statuses, you can create your own post states like “In Progress”, “Pitch”, or “Waiting for Edit” and keep or delete the originals. You can also drag and drop statuses to set the best order for your workflow.</p><p>Custom statuses are fully integrated into the rest of Edit Flow and the WordPress admin. On the calendar and story budget, you can filter your view to see only posts of a specific post state. Furthermore, email notifications can be sent to a specific group of users when a post changes state.</p>', 'edit-flow'),
72 ),
73 'settings_help_sidebar' => __( '<p><strong>For more information:</strong></p><p><a href="http://editflow.org/features/custom-statuses/">Custom Status 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/Automattic/Edit-Flow">Edit Flow on Github</a></p>', 'edit-flow' ),
74 );
75 $this->module = EditFlow()->register_module( 'custom_status', $args );
76 }
77
78 /**
79 * Initialize the EF_Custom_Status class if the module is active
80 */
81 function init() {
82 global $edit_flow;
83
84 // Register custom statuses as a taxonomy
85 $this->register_custom_statuses();
86
87 // Register our settings
88 add_action( 'admin_init', array( $this, 'register_settings' ) );
89
90 // Load CSS and JS resources that we probably need
91 add_action( 'admin_enqueue_scripts', array( $this, 'action_admin_enqueue_scripts' ) );
92 add_action( 'admin_notices', array( $this, 'no_js_notice' ) );
93 add_action( 'admin_print_scripts', array( $this, 'post_admin_header' ) );
94
95 // Methods for handling the actions of creating, making default, and deleting post stati
96 add_action( 'admin_init', array( $this, 'handle_add_custom_status' ) );
97 add_action( 'admin_init', array( $this, 'handle_edit_custom_status' ) );
98 add_action( 'admin_init', array( $this, 'handle_make_default_custom_status' ) );
99 add_action( 'admin_init', array( $this, 'handle_delete_custom_status' ) );
100 add_action( 'wp_ajax_update_status_positions', array( $this, 'handle_ajax_update_status_positions' ) );
101 add_action( 'wp_ajax_inline_save_status', array( $this, 'ajax_inline_save_status' ) );
102
103 // Hook to add the status column to Manage Posts
104
105 add_filter( 'manage_posts_columns', array( $this, '_filter_manage_posts_columns') );
106 add_action( 'manage_posts_custom_column', array( $this, '_filter_manage_posts_custom_column') );
107
108 // We need these for pages (http://core.trac.wordpress.org/browser/tags/3.3.1/wp-admin/includes/class-wp-posts-list-table.php#L283)
109 add_filter( 'manage_pages_columns', array( $this, '_filter_manage_posts_columns' ) );
110 add_action( 'manage_pages_custom_column', array( $this, '_filter_manage_posts_custom_column' ) );
111
112 // These seven-ish methods are hacks for fixing bugs in WordPress core
113 add_action( 'admin_init', array( $this, 'check_timestamp_on_publish' ) );
114 add_filter( 'wp_insert_post_data', array( $this, 'fix_custom_status_timestamp' ), 10, 2 );
115 add_action( 'wp_insert_post', array( $this, 'fix_post_name' ), 10, 2 );
116 add_filter( 'preview_post_link', array( $this, 'fix_preview_link_part_one' ) );
117 add_filter( 'post_link', array( $this, 'fix_preview_link_part_two' ), 10, 3 );
118 add_filter( 'page_link', array( $this, 'fix_preview_link_part_two' ), 10, 3 );
119 add_filter( 'post_type_link', array( $this, 'fix_preview_link_part_two' ), 10, 3 );
120 add_filter( 'get_sample_permalink', array( $this, 'fix_get_sample_permalink' ), 10, 5 );
121 add_filter( 'get_sample_permalink_html', array( $this, 'fix_get_sample_permalink_html' ), 10, 5);
122 add_filter( 'post_row_actions', array( $this, 'fix_post_row_actions' ), 10, 2 );
123 add_filter( 'page_row_actions', array( $this, 'fix_post_row_actions' ), 10, 2 );
124
125 // Pagination for custom post statuses when previewing posts
126 add_filter( 'wp_link_pages_link', array( $this, 'modify_preview_link_pagination_url' ), 10, 2 );
127
128 // Filter through Post States and run a function to check if they are also a Status
129 add_filter( 'display_post_states', array( $this, 'check_if_post_state_is_status' ), 10, 2 );
130 }
131
132 /**
133 * Create the default set of custom statuses the first time the module is loaded
134 *
135 * @since 0.7
136 */
137 function install() {
138
139 $default_terms = array(
140 array(
141 'term' => __( 'Pitch', 'edit-flow' ),
142 'args' => array(
143 'slug' => 'pitch',
144 'description' => __( 'Idea proposed; waiting for acceptance.', 'edit-flow' ),
145 'position' => 1,
146 ),
147 ),
148 array(
149 'term' => __( 'Assigned', 'edit-flow' ),
150 'args' => array(
151 'slug' => 'assigned',
152 'description' => __( 'Post idea assigned to writer.', 'edit-flow' ),
153 'position' => 2,
154 ),
155 ),
156 array(
157 'term' => __( 'In Progress', 'edit-flow' ),
158 'args' => array(
159 'slug' => 'in-progress',
160 'description' => __( 'Writer is working on the post.', 'edit-flow' ),
161 'position' => 3,
162 ),
163 ),
164 array(
165 'term' => __( 'Draft', 'edit-flow' ),
166 'args' => array(
167 'slug' => 'draft',
168 'description' => __( 'Post is a draft; not ready for review or publication.', 'edit-flow' ),
169 'position' => 4,
170 ),
171 ),
172 array(
173 'term' => __( 'Pending Review' ),
174 'args' => array(
175 'slug' => 'pending',
176 'description' => __( 'Post needs to be reviewed by an editor.', 'edit-flow' ),
177 'position' => 5,
178 ),
179 ),
180 );
181
182 // Okay, now add the default statuses to the db if they don't already exist
183 foreach( $default_terms as $term ) {
184 if( !term_exists( $term['term'], self::taxonomy_key ) )
185 $this->add_custom_status( $term['term'], $term['args'] );
186 }
187
188 }
189
190 /**
191 * Upgrade our data in case we need to
192 *
193 * @since 0.7
194 */
195 function upgrade( $previous_version ) {
196 global $edit_flow;
197
198 // Upgrade path to v0.7
199 if ( version_compare( $previous_version, '0.7' , '<' ) ) {
200 // Migrate dropdown visibility option
201 if ( $dropdown_visible = get_option( 'edit_flow_status_dropdown_visible' ) )
202 $dropdown_visible = 'on';
203 else
204 $dropdown_visible = 'off';
205 $edit_flow->update_module_option( $this->module->name, 'always_show_dropdown', $dropdown_visible );
206 delete_option( 'edit_flow_status_dropdown_visible' );
207 // Migrate default status option
208 if ( $default_status = get_option( 'edit_flow_custom_status_default_status' ) )
209 $edit_flow->update_module_option( $this->module->name, 'default_status', $default_status );
210 delete_option( 'edit_flow_custom_status_default_status' );
211
212 // Technically we've run this code before so we don't want to auto-install new data
213 $edit_flow->update_module_option( $this->module->name, 'loaded_once', true );
214 }
215 // Upgrade path to v0.7.4
216 if ( version_compare( $previous_version, '0.7.4', '<' ) ) {
217 // Custom status descriptions become base64_encoded, instead of maybe json_encoded.
218 $this->upgrade_074_term_descriptions( self::taxonomy_key );
219 }
220
221 }
222
223 /**
224 * Makes the call to register_post_status to register the user's custom statuses.
225 * Also unregisters draft and pending, in case the user doesn't want them.
226 */
227 function register_custom_statuses() {
228 global $wp_post_statuses;
229
230 if ( $this->disable_custom_statuses_for_post_type() )
231 return;
232
233 // Register new taxonomy so that we can store all our fancy new custom statuses (or is it stati?)
234 if ( !taxonomy_exists( self::taxonomy_key ) ) {
235 $args = array( 'hierarchical' => false,
236 'update_count_callback' => '_update_post_term_count',
237 'label' => false,
238 'query_var' => false,
239 'rewrite' => false,
240 'show_ui' => false
241 );
242 register_taxonomy( self::taxonomy_key, 'post', $args );
243 }
244
245 if ( function_exists( 'register_post_status' ) ) {
246 // Users can delete draft and pending statuses if they want, so let's get rid of them
247 // They'll get re-added if the user hasn't "deleted" them
248 unset( $wp_post_statuses[ 'draft' ] );
249 unset( $wp_post_statuses[ 'pending' ] );
250
251 $custom_statuses = $this->get_custom_statuses();
252
253 // Unfortunately, register_post_status() doesn't accept a
254 // post type argument, so we have to register the post
255 // statuses for all post types. This results in
256 // all post statuses for a post type appearing at the top
257 // of manage posts if there is a post with the status
258 foreach ( $custom_statuses as $status ) {
259 register_post_status( $status->slug, array(
260 'label' => $status->name
261 , 'protected' => true
262 , '_builtin' => false
263 , 'label_count' => _n_noop( "{$status->name} <span class='count'>(%s)</span>", "{$status->name} <span class='count'>(%s)</span>" )
264 ) );
265 }
266 }
267 }
268
269 /**
270 * Whether custom post statuses should be disabled for this post type.
271 * Used to stop custom statuses from being registered for post types that don't support them.
272 *
273 * @since 0.7.5
274 *
275 * @return bool
276 */
277 function disable_custom_statuses_for_post_type( $post_type = null ) {
278 global $pagenow;
279
280 // Only allow deregistering on 'edit.php' and 'post.php'
281 if ( ! in_array( $pagenow, array( 'edit.php', 'post.php', 'post-new.php' ) ) )
282 return false;
283
284 if ( is_null( $post_type ) )
285 $post_type = $this->get_current_post_type();
286
287 if ( $post_type && ! in_array( $post_type, $this->get_post_types_for_module( $this->module ) ) )
288 return true;
289
290 return false;
291 }
292
293 /**
294 * Enqueue Javascript resources that we need in the admin:
295 * - Primary use of Javascript is to manipulate the post status dropdown on Edit Post and Manage Posts
296 * - jQuery Sortable plugin is used for drag and dropping custom statuses
297 * - We have other custom code for Quick Edit and JS niceties
298 */
299 function action_admin_enqueue_scripts() {
300 global $pagenow;
301
302 if ( $this->disable_custom_statuses_for_post_type() )
303 return;
304
305 // Load Javascript we need to use on the configuration views (jQuery Sortable and Quick Edit)
306 if ( $this->is_whitelisted_settings_view( $this->module->name ) ) {
307 wp_enqueue_script( 'jquery-ui-sortable' );
308 wp_enqueue_script( 'edit-flow-custom-status-configure', $this->module_url . 'lib/custom-status-configure.js', array( 'jquery', 'jquery-ui-sortable', 'edit-flow-settings-js' ), EDIT_FLOW_VERSION, true );
309 }
310
311 // Custom javascript to modify the post status dropdown where it shows up
312 if ( $this->is_whitelisted_page() ) {
313 wp_enqueue_script( 'edit_flow-custom_status', $this->module_url . 'lib/custom-status.js', array( 'jquery','post' ), EDIT_FLOW_VERSION, true );
314 wp_enqueue_style( 'edit_flow-custom_status', $this->module_url . 'lib/custom-status.css', false, EDIT_FLOW_VERSION, 'all' );
315 wp_localize_script('edit_flow-custom_status', '__ef_localize_custom_status', array(
316 'no_change' => esc_html__( "&mdash; No Change &mdash;", 'edit-flow' ),
317 'published' => esc_html__( 'Published', 'edit-flow' ),
318 'save_as' => esc_html__( 'Save as', 'edit-flow' ),
319 'save' => esc_html__( 'Save', 'edit-flow' ),
320 'edit' => esc_html__( 'Edit', 'edit-flow' ),
321 'ok' => esc_html__( 'OK', 'edit-flow' ),
322 'cancel' => esc_html__( 'Cancel', 'edit-flow' ),
323 ));
324 }
325
326
327 }
328
329 /**
330 * Displays a notice to users if they have JS disabled
331 * Javascript is needed for custom statuses to be fully functional
332 */
333 function no_js_notice() {
334 if( $this->is_whitelisted_page() ) :
335 ?>
336 <style type="text/css">
337 /* Hide post status dropdown by default in case of JS issues **/
338 label[for=post_status],
339 #post-status-display,
340 #post-status-select,
341 #publish {
342 display: none;
343 }
344 </style>
345 <div class="update-nag hide-if-js">
346 <?php _e( '<strong>Note:</strong> Your browser does not support JavaScript or has JavaScript disabled. You will not be able to access or change the post status.', 'edit-flow' ); ?>
347 </div>
348 <?php
349 endif;
350 }
351
352 /**
353 * Check whether custom status stuff should be loaded on this page
354 *
355 * @todo migrate this to the base module class
356 */
357 function is_whitelisted_page() {
358 global $pagenow;
359
360 if ( !in_array( $this->get_current_post_type(), $this->get_post_types_for_module( $this->module ) ) )
361 return false;
362
363 $post_type_obj = get_post_type_object( $this->get_current_post_type() );
364
365 if( ! current_user_can( $post_type_obj->cap->edit_posts ) )
366 return false;
367
368 // Only add the script to Edit Post and Edit Page pages -- don't want to bog down the rest of the admin with unnecessary javascript
369 return in_array( $pagenow, array( 'post.php', 'edit.php', 'post-new.php', 'page.php', 'edit-pages.php', 'page-new.php' ) );
370 }
371
372 /**
373 * Adds all necessary javascripts to make custom statuses work
374 *
375 * @todo Support private and future posts on edit.php view
376 */
377 function post_admin_header() {
378 global $post, $edit_flow, $pagenow, $current_user;
379
380 if ( $this->disable_custom_statuses_for_post_type() )
381 return;
382
383 // Get current user
384 wp_get_current_user() ;
385
386 // Only add the script to Edit Post and Edit Page pages -- don't want to bog down the rest of the admin with unnecessary javascript
387 if ( $this->is_whitelisted_page() ) {
388
389 $custom_statuses = $this->get_custom_statuses();
390
391 // $selected can be empty, but must be set because it's used as a JS variable
392 $selected = '';
393 $selected_name = '';
394
395 if( ! empty( $post ) ) {
396 // Get the status of the current post
397 if ( $post->ID == 0 || $post->post_status == 'auto-draft' || $pagenow == 'edit.php' ) {
398 // TODO: check to make sure that the default exists
399 $selected = $this->get_default_custom_status()->slug;
400
401 } else {
402 $selected = $post->post_status;
403 }
404
405 // Get the label of current status
406 foreach ( $custom_statuses as $status ) {
407 if ( $status->slug == $selected ) {
408 $selected_name = $status->name;
409 }
410 }
411 }
412
413 $custom_statuses = apply_filters( 'ef_custom_status_list', $custom_statuses, $post );
414
415 // All right, we want to set up the JS var which contains all custom statuses
416 $all_statuses = array();
417
418 // The default statuses from WordPress
419 $all_statuses[] = array(
420 'name' => __( 'Published', 'edit-flow' ),
421 'slug' => 'publish',
422 'description' => '',
423 );
424 $all_statuses[] = array(
425 'name' => __( 'Privately Published', 'edit-flow' ),
426 'slug' => 'private',
427 'description' => '',
428 );
429 $all_statuses[] = array(
430 'name' => __( 'Scheduled', 'edit-flow' ),
431 'slug' => 'future',
432 'description' => '',
433 );
434
435 // Load the custom statuses
436 foreach( $custom_statuses as $status ) {
437 $all_statuses[] = array(
438 'name' => esc_js( $status->name ),
439 'slug' => esc_js( $status->slug ),
440 'description' => esc_js( $status->description ),
441 );
442 }
443
444 $always_show_dropdown = ( $this->module->options->always_show_dropdown == 'on' ) ? 1 : 0;
445
446 $post_type_obj = get_post_type_object( $this->get_current_post_type() );
447
448 // Now, let's print the JS vars
449 ?>
450 <script type="text/javascript">
451 var custom_statuses = <?php echo json_encode( $all_statuses ); ?>;
452 var ef_default_custom_status = '<?php echo esc_js( $this->get_default_custom_status()->slug ); ?>';
453 var current_status = '<?php echo esc_js( $selected ); ?>';
454 var current_status_name = '<?php echo esc_js( $selected_name ); ?>';
455 var status_dropdown_visible = <?php echo esc_js( $always_show_dropdown ); ?>;
456 var current_user_can_publish_posts = <?php echo current_user_can( $post_type_obj->cap->publish_posts ) ? 1 : 0; ?>;
457 var current_user_can_edit_published_posts = <?php echo current_user_can( $post_type_obj->cap->edit_published_posts ) ? 1 : 0; ?>;
458 </script>
459
460 <?php
461
462 }
463
464 }
465
466 /**
467 * Adds a new custom status as a term in the wp_terms table.
468 * Basically a wrapper for the wp_insert_term class.
469 *
470 * The arguments decide how the term is handled based on the $args parameter.
471 * The following is a list of the available overrides and the defaults.
472 *
473 * 'description'. There is no default. If exists, will be added to the database
474 * along with the term. Expected to be a string.
475 *
476 * 'slug'. Expected to be a string. There is no default.
477 *
478 * @param int|string $term The status to add or update
479 * @param array|string $args Change the values of the inserted term
480 * @return array|WP_Error $response The Term ID and Term Taxonomy ID
481 */
482 function add_custom_status( $term, $args = array() ) {
483 $slug = ( ! empty( $args['slug'] ) ) ? $args['slug'] : sanitize_title( $term );
484 unset( $args['slug'] );
485 $encoded_description = $this->get_encoded_description( $args );
486 $response = wp_insert_term( $term, self::taxonomy_key, array( 'slug' => $slug, 'description' => $encoded_description ) );
487
488 // Reset our internal object cache
489 $this->custom_statuses_cache = array();
490
491 return $response;
492
493 }
494
495 /**
496 * Update an existing custom status
497 *
498 * @param int @status_id ID for the status
499 * @param array $args Any arguments to be updated
500 * @return object $updated_status Newly updated status object
501 */
502 function update_custom_status( $status_id, $args = array() ) {
503 global $edit_flow;
504
505 $old_status = $this->get_custom_status_by( 'id', $status_id );
506 if ( !$old_status || is_wp_error( $old_status ) )
507 return new WP_Error( 'invalid', __( "Custom status doesn't exist.", 'edit-flow' ) );
508
509 // Reset our internal object cache
510 $this->custom_statuses_cache = array();
511
512 // Prevent user from changing draft name or slug
513 if ( 'draft' === $old_status->slug
514 && (
515 ( isset( $args['name'] ) && $args['name'] !== $old_status->name )
516 ||
517 ( isset( $args['slug'] ) && $args['slug'] !== $old_status->slug )
518 ) ) {
519 return new WP_Error( 'invalid', __( 'Changing the name and slug of "Draft" is not allowed', 'edit-flow' ) );
520 }
521
522 // If the name was changed, we need to change the slug
523 if ( isset( $args['name'] ) && $args['name'] != $old_status->name )
524 $args['slug'] = sanitize_title( $args['name'] );
525
526 // Reassign posts to new status slug if the slug changed and isn't restricted
527 if ( isset( $args['slug'] ) && $args['slug'] != $old_status->slug && !$this->is_restricted_status( $old_status->slug ) ) {
528 $new_status = $args['slug'];
529 $this->reassign_post_status( $old_status->slug, $new_status );
530
531 $default_status = $this->get_default_custom_status()->slug;
532 if ( $old_status->slug == $default_status )
533 $edit_flow->update_module_option( $this->module->name, 'default_status', $new_status );
534 }
535 // We're encoding metadata that isn't supported by default in the term's description field
536 $args_to_encode = array();
537 $args_to_encode['description'] = ( isset( $args['description'] ) ) ? $args['description'] : $old_status->description;
538 $args_to_encode['position'] = ( isset( $args['position'] ) ) ? $args['position'] : $old_status->position;
539 $encoded_description = $this->get_encoded_description( $args_to_encode );
540 $args['description'] = $encoded_description;
541
542 $updated_status_array = wp_update_term( $status_id, self::taxonomy_key, $args );
543 $updated_status = $this->get_custom_status_by( 'id', $updated_status_array['term_id'] );
544
545 return $updated_status;
546
547 }
548
549 /**
550 * Deletes a custom status from the wp_terms table.
551 *
552 * Partly a wrapper for the wp_delete_term function.
553 * BUT, also reassigns posts that currently have the deleted status assigned.
554 */
555 function delete_custom_status( $status_id, $args = array(), $reassign = '' ) {
556 global $edit_flow;
557 // Reassign posts to alternate status
558
559 // Get slug for the old status
560 $old_status = $this->get_custom_status_by( 'id', $status_id )->slug;
561
562 if ( $reassign == $old_status )
563 return new WP_Error( 'invalid', __( 'Cannot reassign to the status you want to delete', 'edit-flow' ) );
564
565 // Reset our internal object cache
566 $this->custom_statuses_cache = array();
567
568 if( !$this->is_restricted_status( $old_status ) && 'draft' !== $old_status ) {
569 $default_status = $this->get_default_custom_status()->slug;
570 // If new status in $reassign, use that for all posts of the old_status
571 if( !empty( $reassign ) )
572 $new_status = $this->get_custom_status_by( 'id', $reassign )->slug;
573 else
574 $new_status = $default_status;
575 if ( $old_status == $default_status && $this->get_custom_status_by( 'slug', 'draft' ) ) { // Deleting default status
576 $new_status = 'draft';
577 $edit_flow->update_module_option( $this->module->name, 'default_status', $new_status );
578 }
579
580 $this->reassign_post_status( $old_status, $new_status );
581
582 return wp_delete_term( $status_id, self::taxonomy_key, $args );
583 } else
584 return new WP_Error( 'restricted', __( 'Restricted status ', 'edit-flow' ) . '(' . $this->get_custom_status_by( 'id', $status_id )->name . ')' );
585
586 }
587
588 /**
589 * Get all custom statuses as an ordered array
590 *
591 * @param array|string $statuses
592 * @param array $args
593 * @return array $statuses All of the statuses
594 */
595 function get_custom_statuses( $args = array() ) {
596 global $wp_post_statuses;
597
598 if ( $this->disable_custom_statuses_for_post_type() ) {
599 return $this->get_core_post_statuses();
600 }
601
602 // Internal object cache for repeat requests
603 $arg_hash = md5( serialize( $args ) );
604 if ( ! empty( $this->custom_statuses_cache[ $arg_hash ] ) ) {
605 return $this->custom_statuses_cache[ $arg_hash ];
606 }
607
608 // Handle if the requested taxonomy doesn't exist
609 $args = array_merge( array( 'hide_empty' => false ), $args );
610 $statuses = get_terms( self::taxonomy_key, $args );
611
612 if ( is_wp_error( $statuses ) || empty( $statuses ) ) {
613 $statuses = array();
614 }
615
616 // Expand and order the statuses
617 $ordered_statuses = array();
618 $hold_to_end = array();
619 foreach ( $statuses as $key => $status ) {
620 // Unencode and set all of our psuedo term meta because we need the position if it exists
621 $unencoded_description = $this->get_unencoded_description( $status->description );
622 if ( is_array( $unencoded_description ) ) {
623 foreach( $unencoded_description as $key => $value ) {
624 $status->$key = $value;
625 }
626 }
627 // We require the position key later on (e.g. management table)
628 if ( !isset( $status->position ) )
629 $status->position = false;
630 // Only add the status to the ordered array if it has a set position and doesn't conflict with another key
631 // Otherwise, hold it for later
632 if ( $status->position && !array_key_exists( $status->position, $ordered_statuses ) ) {
633 $ordered_statuses[(int)$status->position] = $status;
634 } else {
635 $hold_to_end[] = $status;
636 }
637 }
638 // Sort the items numerically by key
639 ksort( $ordered_statuses, SORT_NUMERIC );
640 // Append all of the statuses that didn't have an existing position
641 foreach( $hold_to_end as $unpositioned_status )
642 $ordered_statuses[] = $unpositioned_status;
643
644 $this->custom_statuses_cache[ $arg_hash ] = $ordered_statuses;
645
646 return $ordered_statuses;
647 }
648
649 /**
650 * Returns the a single status object based on ID, title, or slug
651 *
652 * @param string|int $string_or_int The status to search for, either by slug, name or ID
653 * @return object|WP_Error $status The object for the matching status
654 */
655 function get_custom_status_by( $field, $value ) {
656
657 if ( ! in_array( $field, array( 'id', 'slug', 'name' ) ) )
658 return false;
659
660 if ( 'id' == $field )
661 $field = 'term_id';
662
663 $custom_statuses = $this->get_custom_statuses();
664 $custom_status = wp_filter_object_list( $custom_statuses, array( $field => $value ) );
665
666 if ( ! empty( $custom_status ) )
667 return array_shift( $custom_status );
668 else
669 return false;
670 }
671
672 /**
673 * Get the term object for the default custom post status
674 *
675 * @return object $default_status Default post status object
676 */
677 function get_default_custom_status() {
678 $default_status = $this->get_custom_status_by( 'slug', $this->module->options->default_status );
679 if ( ! $default_status ) {
680 $custom_statuses = $this->get_custom_statuses();
681 $default_status = array_shift( $custom_statuses );
682 }
683 return $default_status;
684
685 }
686
687 /**
688 * Assign new statuses to posts using value provided or the default
689 *
690 * @param string $old_status Slug for the old status
691 * @param string $new_status Slug for the new status
692 */
693 function reassign_post_status( $old_status, $new_status = '' ) {
694 global $wpdb;
695
696 if ( empty( $new_status ) )
697 $new_status = $this->get_default_custom_status()->slug;
698
699 // Make the database call
700 $result = $wpdb->update( $wpdb->posts, array( 'post_status' => $new_status ), array( 'post_status' => $old_status ), array( '%s' ));
701 }
702
703 /**
704 * Insert new column header for post status after the title column
705 *
706 * @param array $posts_columns Columns currently shown on the Edit Posts screen
707 * @return array Same array as the input array with a "status" column added after the "title" column
708 */
709 function _filter_manage_posts_columns( $posts_columns ) {
710 // Return immediately if the supplied parameter isn't an array (which shouldn't happen in practice?)
711 // http://wordpress.org/support/topic/plugin-edit-flow-bug-shows-2-drafts-when-there-are-none-leads-to-error-messages
712 if ( !is_array( $posts_columns ) )
713 return $posts_columns;
714
715 // Only do it for the post types this module is activated for
716 if ( !in_array( $this->get_current_post_type(), $this->get_post_types_for_module( $this->module ) ) )
717 return $posts_columns;
718
719 $result = array();
720 foreach ( $posts_columns as $key => $value ) {
721 if ($key == 'title') {
722 $result[$key] = $value;
723 $result['status'] = __('Status', 'edit-flow');
724 } else $result[$key] = $value;
725 }
726 return $result;
727
728 }
729
730 /**
731 * Adds a Post's status to its row on the Edit page
732 *
733 * @param string $column_name
734 **/
735 function _filter_manage_posts_custom_column( $column_name ) {
736
737 if ( $column_name == 'status' ) {
738 global $post;
739 $post_status_obj = get_post_status_object( get_post_status( $post->ID ) );
740 echo esc_html( $post_status_obj->label );
741 }
742 }
743 /**
744 * Check if Post State is a Status and display if it is not.
745 *
746 * @param array $post_states An array of post display states.
747 */
748 function check_if_post_state_is_status($post_states) {
749
750 global $post;
751 $statuses = get_post_status_object(get_post_status($post->ID));
752 foreach ( $post_states as $state ) {
753 if ( $state !== $statuses->label ) {
754 echo '<span class="show"></span>';
755 }
756 }
757 return $post_states;
758 }
759
760 /**
761 * Determines whether the slug indicated belongs to a restricted status or not
762 *
763 * @param string $slug Slug of the status
764 * @return bool $restricted True if restricted, false if not
765 */
766 function is_restricted_status( $slug ) {
767
768 switch( $slug ) {
769 case 'publish':
770 case 'private':
771 case 'future':
772 case 'new':
773 case 'inherit':
774 case 'auto-draft':
775 case 'trash':
776 $restricted = true;
777 break;
778
779 default:
780 $restricted = false;
781 break;
782 }
783 return $restricted;
784
785 }
786
787 /**
788 * Handles a form's POST request to add a custom status
789 *
790 * @since 0.7
791 */
792 function handle_add_custom_status() {
793
794 // Check that the current POST request is our POST request
795 if ( !isset( $_POST['submit'], $_GET['page'], $_POST['action'] )
796 || $_GET['page'] != $this->module->settings_slug || $_POST['action'] != 'add-new' )
797 return;
798
799 if ( !wp_verify_nonce( $_POST['_wpnonce'], 'custom-status-add-nonce' ) )
800 wp_die( $this->module->messages['nonce-failed'] );
801
802 // Validate and sanitize the form data
803 $status_name = sanitize_text_field( trim( $_POST['status_name'] ) );
804 $status_slug = sanitize_title( $status_name );
805 $status_description = stripslashes( wp_filter_nohtml_kses( trim( $_POST['status_description'] ) ) );
806
807 /**
808 * Form validation
809 * - Name is required and can't conflict with an existing name or slug
810 * - Description is optional
811 */
812 $_REQUEST['form-errors'] = array();
813 // Check if name field was filled in
814 if( empty( $status_name ) )
815 $_REQUEST['form-errors']['name'] = __( 'Please enter a name for the status', 'edit-flow' );
816 // Check that the name isn't numeric
817 if ( (int)$status_name != 0 )
818 $_REQUEST['form-errors']['name'] = __( 'Please enter a valid, non-numeric name for the status.', 'edit-flow' );
819 // Check that the status name doesn't exceed 20 chars
820 if ( strlen( $status_name ) > 20 )
821 $_REQUEST['form-errors']['name'] = __( 'Status name cannot exceed 20 characters. Please try a shorter name.', 'edit-flow' );
822 // Check to make sure the status doesn't already exist as another term because otherwise we'd get a weird slug
823 if ( term_exists( $status_slug, self::taxonomy_key ) )
824 $_REQUEST['form-errors']['name'] = __( 'Status name conflicts with existing term. Please choose another.', 'edit-flow' );
825 // Check to make sure the name is not restricted
826 if ( $this->is_restricted_status( strtolower( $status_slug ) ) )
827 $_REQUEST['form-errors']['name'] = __( 'Status name is restricted. Please choose another name.', 'edit-flow' );
828
829 // If there were any form errors, kick out and return them
830 if ( count( $_REQUEST['form-errors'] ) ) {
831 $_REQUEST['error'] = 'form-error';
832 return;
833 }
834
835 // Try to add the status
836 $status_args = array(
837 'description' => $status_description,
838 'slug' => $status_slug,
839 );
840 $return = $this->add_custom_status( $status_name, $status_args );
841 if ( is_wp_error( $return ) )
842 wp_die( __( 'Could not add status: ', 'edit-flow' ) . $return->get_error_message() );
843 // Redirect if successful
844 $redirect_url = $this->get_link( array( 'message' => 'status-added' ) );
845 wp_redirect( $redirect_url );
846 exit;
847
848 }
849
850 /**
851 * Handles a POST request to edit an custom status
852 *
853 * @since 0.7
854 */
855 function handle_edit_custom_status() {
856 if ( !isset( $_POST['submit'], $_GET['page'], $_GET['action'], $_GET['term-id'] )
857 || $_GET['page'] != $this->module->settings_slug || $_GET['action'] != 'edit-status' )
858 return;
859
860 if ( !wp_verify_nonce( $_POST['_wpnonce'], 'edit-status' ) )
861 wp_die( $this->module->messages['nonce-failed'] );
862
863 if ( !current_user_can( 'manage_options' ) )
864 wp_die( $this->module->messages['invalid-permissions'] );
865
866 if ( !$existing_status = $this->get_custom_status_by( 'id', (int)$_GET['term-id'] ) )
867 wp_die( $this->module->messages['status-missing'] );
868
869 $name = sanitize_text_field( trim( $_POST['name'] ) );
870 $description = stripslashes( wp_filter_nohtml_kses( trim( $_POST['description'] ) ) );
871
872 /**
873 * Form validation for editing custom status
874 *
875 * Details
876 * - 'name' is a required field and can't conflict with existing name or slug
877 * - 'description' is optional
878 */
879 $_REQUEST['form-errors'] = array();
880 // Check if name field was filled in
881 if( empty( $name ) )
882 $_REQUEST['form-errors']['name'] = __( 'Please enter a name for the status', 'edit-flow' );
883 // Check that the name isn't numeric
884 if ( is_numeric( $name ) )
885 $_REQUEST['form-errors']['name'] = __( 'Please enter a valid, non-numeric name for the status.', 'edit-flow' );
886 // Check that the status name doesn't exceed 20 chars
887 if ( strlen( $name ) > 20 )
888 $_REQUEST['form-errors']['name'] = __( 'Status name cannot exceed 20 characters. Please try a shorter name.', 'edit-flow' );
889 // Check to make sure the status doesn't already exist as another term because otherwise we'd get a weird slug
890 $term_exists = term_exists( sanitize_title( $name ), self::taxonomy_key );
891 if ( $term_exists && isset( $term_exists['term_id'] ) && $term_exists['term_id'] != $existing_status->term_id )
892 $_REQUEST['form-errors']['name'] = __( 'Status name conflicts with existing term. Please choose another.', 'edit-flow' );
893 // Check to make sure the status doesn't already exist
894 $search_status = $this->get_custom_status_by( 'slug', sanitize_title( $name ) );
895 if ( $search_status && $search_status->term_id != $existing_status->term_id )
896 $_REQUEST['form-errors']['name'] = __( 'Status name conflicts with existing status. Please choose another.', 'edit-flow' );
897 // Check to make sure the name is not restricted
898 if ( $this->is_restricted_status( strtolower( sanitize_title( $name ) ) ) )
899 $_REQUEST['form-errors']['name'] = __( 'Status name is restricted. Please choose another name.', 'edit-flow' );
900
901 // Kick out if there are any errors
902 if ( count( $_REQUEST['form-errors'] ) ) {
903 $_REQUEST['error'] = 'form-error';
904 return;
905 }
906
907 // Try to add the new post status
908 $args = array(
909 'name' => $name,
910 'slug' => sanitize_title( $name ),
911 'description' => $description,
912 );
913 $return = $this->update_custom_status( $existing_status->term_id, $args );
914 if ( is_wp_error( $return ) )
915 wp_die( __( 'Error updating post status.', 'edit-flow' ) );
916
917 $redirect_url = $this->get_link( array( 'message' => 'status-updated' ) );
918 wp_redirect( $redirect_url );
919 exit;
920 }
921
922 /**
923 * Handles a GET request to make the identified status default
924 *
925 * @since 0.7
926 */
927 function handle_make_default_custom_status() {
928 global $edit_flow;
929
930 // Check that the current GET request is our GET request
931 if ( !isset( $_GET['page'], $_GET['action'], $_GET['term-id'], $_GET['nonce'] )
932 || $_GET['page'] != $this->module->settings_slug || $_GET['action'] != 'make-default' )
933 return;
934
935 // Check for proper nonce
936 if ( !wp_verify_nonce( $_GET['nonce'], 'make-default' ) )
937 wp_die( __( 'Invalid nonce for submission.', 'edit-flow' ) );
938
939 // Only allow users with the proper caps
940 if ( !current_user_can( 'manage_options' ) )
941 wp_die( __( 'Sorry, you do not have permission to edit custom statuses.', 'edit-flow' ) );
942
943 $term_id = (int)$_GET['term-id'];
944 $term = $this->get_custom_status_by( 'id', $term_id );
945 if ( is_object( $term ) ) {
946 $edit_flow->update_module_option( $this->module->name, 'default_status', $term->slug );
947 // @todo How do we want to handle users who click the link from "Add New Status"
948 $redirect_url = $this->get_link( array( 'message' => 'default-status-changed' ) );
949 wp_redirect( $redirect_url );
950 exit;
951 } else {
952 wp_die( __( 'Status doesn&#39;t exist.', 'edit-flow' ) );
953 }
954
955 }
956
957 /**
958 * Handles a GET request to delete a specific term
959 *
960 * @since 0.7
961 */
962 function handle_delete_custom_status() {
963
964 // Check that this GET request is our GET request
965 if ( !isset( $_GET['page'], $_GET['action'], $_GET['term-id'], $_GET['nonce'] )
966 || $_GET['page'] != $this->module->settings_slug || $_GET['action'] != 'delete-status' )
967 return;
968
969 // Check for proper nonce
970 if ( !wp_verify_nonce( $_GET['nonce'], 'delete-status' ) )
971 wp_die( __( 'Invalid nonce for submission.', 'edit-flow' ) );
972
973 // Only allow users with the proper caps
974 if ( !current_user_can( 'manage_options' ) )
975 wp_die( __( 'Sorry, you do not have permission to edit custom statuses.', 'edit-flow' ) );
976
977 // Check to make sure the status isn't already deleted
978 $term_id = (int)$_GET['term-id'];
979 $term = $this->get_custom_status_by( 'id', $term_id );
980 if( !$term )
981 wp_die( __( 'Status does not exist.', 'edit-flow' ) );
982
983 // Don't allow deletion of default status
984 if ( $term->slug == $this->get_default_custom_status()->slug )
985 wp_die( __( 'Cannot delete default status.', 'edit-flow' ) );
986
987 $return = $this->delete_custom_status( $term_id );
988 if ( is_wp_error( $return ) )
989 wp_die( __( 'Could not delete the status: ', 'edit-flow' ) . $return->get_error_message() );
990
991 $redirect_url = $this->get_link( array( 'message' => 'status-deleted' ) );
992 wp_redirect( $redirect_url );
993 exit;
994
995 }
996
997 /**
998 * Generate a link to one of the custom status actions
999 *
1000 * @since 0.7
1001 *
1002 * @param array $args (optional) Action and any query args to add to the URL
1003 * @return string $link Direct link to complete the action
1004 */
1005 function get_link( $args = array() ) {
1006 if ( !isset( $args['action'] ) )
1007 $args['action'] = '';
1008 if ( !isset( $args['page'] ) )
1009 $args['page'] = $this->module->settings_slug;
1010 // Add other things we may need depending on the action
1011 switch( $args['action'] ) {
1012 case 'make-default':
1013 case 'delete-status':
1014 $args['nonce'] = wp_create_nonce( $args['action'] );
1015 break;
1016 default:
1017 break;
1018 }
1019 return add_query_arg( $args, get_admin_url( null, 'admin.php' ) );
1020 }
1021
1022 /**
1023 * Handle an ajax request to update the order of custom statuses
1024 *
1025 * @since 0.7
1026 */
1027 function handle_ajax_update_status_positions() {
1028
1029 if ( !wp_verify_nonce( $_POST['custom_status_sortable_nonce'], 'custom-status-sortable' ) )
1030 $this->print_ajax_response( 'error', $this->module->messages['nonce-failed'] );
1031
1032 if ( !current_user_can( 'manage_options') )
1033 $this->print_ajax_response( 'error', $this->module->messages['invalid-permissions'] );
1034
1035 if ( !isset( $_POST['status_positions'] ) || !is_array( $_POST['status_positions'] ) )
1036 $this->print_ajax_response( 'error', __( 'Terms not set.', 'edit-flow' ) );
1037
1038 // Update each custom status with its new position
1039 foreach ( $_POST['status_positions'] as $position => $term_id ) {
1040
1041 // Have to add 1 to the position because the index started with zero
1042 $args = array(
1043 'position' => (int)$position + 1,
1044 );
1045 $return = $this->update_custom_status( (int)$term_id, $args );
1046 // @todo check that this was a valid return
1047 }
1048 $this->print_ajax_response( 'success', $this->module->messages['status-position-updated'] );
1049 }
1050
1051 /**
1052 * Handle an Inline Edit POST request to update status values
1053 *
1054 * @since 0.7
1055 */
1056 function ajax_inline_save_status() {
1057 global $edit_flow;
1058
1059 if ( !wp_verify_nonce( $_POST['inline_edit'], 'custom-status-inline-edit-nonce' ) )
1060 die( $this->module->messages['nonce-failed'] );
1061
1062 if ( !current_user_can( 'manage_options') )
1063 die( $this->module->messages['invalid-permissions'] );
1064
1065 $term_id = (int) $_POST['status_id'];
1066 $status_name = sanitize_text_field( trim( $_POST['name'] ) );
1067 $status_slug = sanitize_title( $_POST['name'] );
1068 $status_description = stripslashes( wp_filter_nohtml_kses( trim( $_POST['description'] ) ) );
1069
1070 // Check if name field was filled in
1071 if ( empty( $status_name ) ) {
1072 $change_error = new WP_Error( 'invalid', __( 'Please enter a name for the status.', 'edit-flow' ) );
1073 die( $change_error->get_error_message() );
1074 }
1075
1076 // Check that the name isn't numeric
1077 if( is_numeric( $status_name) ) {
1078 $change_error = new WP_Error( 'invalid', __( 'Please enter a valid, non-numeric name for the status.', 'edit-flow' ) );
1079 die( $change_error->get_error_message() );
1080 }
1081
1082 // Check that the status name doesn't exceed 20 chars
1083 if ( strlen( $status_name ) > 20 ) {
1084 $change_error = new WP_Error( 'invalid', __( 'Status name cannot exceed 20 characters. Please try a shorter name.', 'edit-flow' ) );
1085 die( $change_error->get_error_message() );
1086 }
1087
1088 // Check to make sure the name is not restricted
1089 if ( $edit_flow->custom_status->is_restricted_status( strtolower( $status_name ) ) ) {
1090 $change_error = new WP_Error( 'invalid', __( 'Status name is restricted. Please chose another name.', 'edit-flow' ) );
1091 die( $change_error->get_error_message() );
1092 }
1093
1094 // Check to make sure the status doesn't already exist
1095 if ( $this->get_custom_status_by( 'slug', $status_slug ) && ( $this->get_custom_status_by( 'id', $term_id )->slug != $status_slug ) ) {
1096 $change_error = new WP_Error( 'invalid', __( 'Status already exists. Please choose another name.', 'edit-flow' ) );
1097 die( $change_error->get_error_message() );
1098 }
1099
1100 // Check to make sure the status doesn't already exist as another term because otherwise we'd get a fatal error
1101 $term_exists = term_exists( sanitize_title( $status_name ), self::taxonomy_key );
1102 if ( $term_exists && isset( $term_exists['term_id'] ) && $term_exists['term_id'] != $term_id ) {
1103 $change_error = new WP_Error( 'invalid', __( 'Status name conflicts with existing term. Please choose another.', 'edit-flow' ) );
1104 die( $change_error->get_error_message() );
1105 }
1106
1107 // get status_name & status_description
1108 $args = array(
1109 'name' => $status_name,
1110 'description' => $status_description,
1111 'slug' => $status_slug,
1112 );
1113 $return = $this->update_custom_status( $term_id, $args );
1114 if( !is_wp_error( $return ) ) {
1115 set_current_screen( 'edit-custom-status' );
1116 $wp_list_table = new EF_Custom_Status_List_Table();
1117 $wp_list_table->prepare_items();
1118 echo $wp_list_table->single_row( $return );
1119 die();
1120 } else {
1121 $change_error = new WP_Error( 'invalid', sprintf( __( 'Could not update the status: <strong>%s</strong>', 'edit-flow' ), $status_name ) );
1122 die( $change_error->get_error_message() );
1123 }
1124
1125 }
1126
1127 /**
1128 * Register settings for notifications so we can partially use the Settings API
1129 * (We use the Settings API for form generation, but not saving)
1130 *
1131 * @since 0.7
1132 */
1133 function register_settings() {
1134
1135 add_settings_section( $this->module->options_group_name . '_general', false, '__return_false', $this->module->options_group_name );
1136 add_settings_field( 'post_types', __( 'Use on these post types:', 'edit-flow' ), array( $this, 'settings_post_types_option' ), $this->module->options_group_name, $this->module->options_group_name . '_general' );
1137 add_settings_field( 'always_show_dropdown', __( 'Always show dropdown:', 'edit-flow' ), array( $this, 'settings_always_show_dropdown_option'), $this->module->options_group_name, $this->module->options_group_name . '_general' );
1138
1139 }
1140
1141 /**
1142 * Choose the post types that should be displayed on the calendar
1143 *
1144 * @since 0.7
1145 */
1146 function settings_post_types_option() {
1147 global $edit_flow;
1148 $edit_flow->settings->helper_option_custom_post_type( $this->module );
1149 }
1150
1151 /**
1152 * Option for whether the blog admin email address should be always notified or not
1153 *
1154 * @since 0.7
1155 */
1156 function settings_always_show_dropdown_option() {
1157 $options = array(
1158 'off' => __( 'Disabled', 'edit-flow' ),
1159 'on' => __( 'Enabled', 'edit-flow' ),
1160 );
1161 echo '<select id="always_show_dropdown" name="' . $this->module->options_group_name . '[always_show_dropdown]">';
1162 foreach ( $options as $value => $label ) {
1163 echo '<option value="' . esc_attr( $value ) . '"';
1164 echo selected( $this->module->options->always_show_dropdown, $value );
1165 echo '>' . esc_html( $label ) . '</option>';
1166 }
1167 echo '</select>';
1168 }
1169
1170 /**
1171 * Validate input from the end user
1172 *
1173 * @since 0.7
1174 */
1175 function settings_validate( $new_options ) {
1176
1177 // Whitelist validation for the post type options
1178 if ( !isset( $new_options['post_types'] ) )
1179 $new_options['post_types'] = array();
1180 $new_options['post_types'] = $this->clean_post_type_options( $new_options['post_types'], $this->module->post_type_support );
1181
1182 // Whitelist validation for the 'always_show_dropdown' optoins
1183 if ( !isset( $new_options['always_show_dropdown'] ) || $new_options['always_show_dropdown'] != 'on' )
1184 $new_options['always_show_dropdown'] = 'off';
1185
1186 return $new_options;
1187 }
1188
1189 /**
1190 * Primary configuration page for custom status class.
1191 * Shows form to add new custom statuses on the left and a
1192 * WP_List_Table with the custom status terms on the right
1193 */
1194 function print_configure_view() {
1195 global $edit_flow;
1196
1197 /** Full width view for editing a custom status **/
1198 if ( isset( $_GET['action'], $_GET['term-id'] ) && $_GET['action'] == 'edit-status' ): ?>
1199 <?php
1200 // Check whether the term exists
1201 $term_id = (int)$_GET['term-id'];
1202 $status = $this->get_custom_status_by( 'id', $term_id );
1203 if ( !$status ) {
1204 echo '<div class="error"><p>' . $this->module->messages['status-missing'] . '</p></div>';
1205 return;
1206 }
1207 $edit_status_link = $this->get_link( array( 'action' => 'edit-status', 'term-id' => $term_id ) );
1208
1209 $name = ( isset( $_POST['name'] ) ) ? stripslashes( $_POST['name'] ) : $status->name;
1210 $description = ( isset( $_POST['description'] ) ) ? strip_tags( stripslashes( $_POST['description'] ) ) : $status->description;
1211 ?>
1212
1213 <div id="ajax-response"></div>
1214 <form method="post" action="<?php echo esc_attr( $edit_status_link ); ?>" >
1215 <input type="hidden" name="term-id" value="<?php echo esc_attr( $term_id ); ?>" />
1216 <?php
1217 wp_original_referer_field();
1218 wp_nonce_field( 'edit-status' );
1219 ?>
1220 <table class="form-table">
1221 <tr class="form-field form-required">
1222 <th scope="row" valign="top"><label for="name"><?php _e( 'Custom Status', 'edit-flow' ); ?></label></th>
1223 <td><input name="name" id="name" type="text" value="<?php echo esc_attr( $name ); ?>" size="40" aria-required="true" <?php if( 'draft' === $status->slug ) echo 'readonly="readonly"' ?> />
1224 <?php $edit_flow->settings->helper_print_error_or_description( 'name', __( 'The name is used to identify the status. (Max: 20 characters)', 'edit-flow' ) ); ?>
1225 </td>
1226 </tr>
1227 <tr class="form-field">
1228 <th scope="row" valign="top"><?php _e( 'Slug', 'edit-flow' ); ?></th>
1229 <td>
1230 <input type="text" disabled="disabled" value="<?php echo esc_attr( $status->slug ); ?>" />
1231 <?php $edit_flow->settings->helper_print_error_or_description( 'slug', __( 'The slug is the unique ID for the status and is changed when the name is changed.', 'edit-flow' ) ); ?>
1232 </td>
1233 </tr>
1234 <tr class="form-field">
1235 <th scope="row" valign="top"><label for="description"><?php _e( 'Description', 'edit-flow' ); ?></label></th>
1236 <td>
1237 <textarea name="description" id="description" rows="5" cols="50" style="width: 97%;"><?php echo esc_textarea( $description ); ?></textarea>
1238 <?php $edit_flow->settings->helper_print_error_or_description( 'description', __( 'The description is primarily for administrative use, to give you some context on what the custom status is to be used for.', 'edit-flow' ) ); ?>
1239 </td>
1240 </tr>
1241 </table>
1242 <p class="submit">
1243 <?php submit_button( __( 'Update Status', 'edit-flow' ), 'primary', 'submit', false ); ?>
1244 <a class="cancel-settings-link" href="<?php echo esc_url( $this->get_link() ); ?>"><?php _e( 'Cancel', 'edit-flow' ); ?></a>
1245 </p>
1246 </form>
1247
1248 <?php else: ?>
1249 <?php
1250 $wp_list_table = new EF_Custom_Status_List_Table();
1251 $wp_list_table->prepare_items();
1252 ?>
1253 <script type="text/javascript">
1254 var ef_confirm_delete_status_string = "<?php echo esc_js( __( 'Are you sure you want to delete the post status? All posts with this status will be assigned to the default status.', 'edit-flow' ) ); ?>";
1255 </script>
1256 <div id="col-right">
1257 <div class="col-wrap">
1258 <?php $wp_list_table->display(); ?>
1259 <?php wp_nonce_field( 'custom-status-sortable', 'custom-status-sortable' ); ?>
1260 <p class="description" style="padding-top:10px;"><?php _e( 'Deleting a post status will assign all posts to the default post status.', 'edit-flow' ); ?></p>
1261 </div>
1262 </div>
1263 <div id="col-left">
1264 <div class="col-wrap">
1265 <div class="form-wrap">
1266 <h3 class="nav-tab-wrapper">
1267 <a href="<?php echo esc_url( $this->get_link() ); ?>" class="nav-tab<?php if ( !isset( $_GET['action'] ) || $_GET['action'] != 'change-options' ) echo ' nav-tab-active'; ?>"><?php _e( 'Add New', 'edit-flow' ); ?></a>
1268 <a href="<?php echo esc_url( $this->get_link( array( 'action' => 'change-options' ) ) ); ?>" class="nav-tab<?php if ( isset( $_GET['action'] ) && $_GET['action'] == 'change-options' ) echo ' nav-tab-active'; ?>"><?php _e( 'Options', 'edit-flow' ); ?></a>
1269 </h3>
1270 <?php if ( isset( $_GET['action'] ) && $_GET['action'] == 'change-options' ): ?>
1271 <form class="basic-settings" action="<?php echo esc_url( $this->get_link( array( 'action' => 'change-options' ) ) ); ?>" method="post">
1272 <?php settings_fields( $this->module->options_group_name ); ?>
1273 <?php do_settings_sections( $this->module->options_group_name ); ?>
1274 <?php echo '<input id="edit_flow_module_name" name="edit_flow_module_name" type="hidden" value="' . esc_attr( $this->module->name ) . '" />'; ?>
1275 <?php submit_button(); ?>
1276 </form>
1277 <?php else: ?>
1278 <?php /** Custom form for adding a new Custom Status term **/ ?>
1279 <form class="add:the-list:" action="<?php echo esc_url( $this->get_link() ); ?>" method="post" id="addstatus" name="addstatus">
1280 <div class="form-field form-required">
1281 <label for="status_name"><?php _e( 'Name', 'edit-flow' ); ?></label>
1282 <input type="text" aria-required="true" size="20" maxlength="20" id="status_name" name="status_name" value="<?php if ( !empty( $_POST['status_name'] ) ) echo esc_attr( $_POST['status_name'] ) ?>" />
1283 <?php $edit_flow->settings->helper_print_error_or_description( 'name', __( 'The name is used to identify the status. (Max: 20 characters)', 'edit-flow' ) ); ?>
1284 </div>
1285 <div class="form-field">
1286 <label for="status_description"><?php _e( 'Description', 'edit-flow' ); ?></label>
1287 <textarea cols="40" rows="5" id="status_description" name="status_description"><?php if ( !empty( $_POST['status_description'] ) ) echo esc_textarea( $_POST['status_description'] ) ?></textarea>
1288 <?php $edit_flow->settings->helper_print_error_or_description( 'description', __( 'The description is primarily for administrative use, to give you some context on what the custom status is to be used for.', 'edit-flow' ) ); ?>
1289 </div>
1290 <?php wp_nonce_field( 'custom-status-add-nonce' ); ?>
1291 <?php echo '<input id="action" name="action" type="hidden" value="add-new" />'; ?>
1292 <p class="submit"><?php submit_button( __( 'Add New Status', 'edit-flow' ), 'primary', 'submit', false ); ?><a class="cancel-settings-link" href="<?php echo EDIT_FLOW_SETTINGS_PAGE; ?>"><?php _e( 'Back to Edit Flow', 'edit-flow' ); ?></a></p>
1293 </form>
1294 <?php endif; ?>
1295 </div>
1296 </div>
1297 </div>
1298 <?php $wp_list_table->inline_edit(); ?>
1299 <?php endif; ?>
1300 <?php
1301 }
1302
1303 /**
1304 * This is a hack! hack! hack! until core is fixed/better supports custom statuses
1305 *
1306 * When publishing a post with a custom status, set the status to 'pending' temporarily
1307 * @see Works around this limitation: http://core.trac.wordpress.org/browser/tags/3.2.1/wp-includes/post.php#L2694
1308 * @see Original thread: http://wordpress.org/support/topic/plugin-edit-flow-custom-statuses-create-timestamp-problem
1309 * @see Core ticket: http://core.trac.wordpress.org/ticket/18362
1310 */
1311 function check_timestamp_on_publish() {
1312 global $edit_flow, $pagenow, $wpdb;
1313
1314 if ( $this->disable_custom_statuses_for_post_type() )
1315 return;
1316
1317 // Handles the transition to 'publish' on edit.php
1318 if ( isset( $edit_flow ) && $pagenow == 'edit.php' && isset( $_REQUEST['bulk_edit'] ) ) {
1319 // For every post_id, set the post_status as 'pending' only when there's no timestamp set for $post_date_gmt
1320 if ( $_REQUEST['_status'] == 'publish' ) {
1321 $post_ids = array_map( 'intval', (array) $_REQUEST['post'] );
1322 foreach ( $post_ids as $post_id ) {
1323 $wpdb->update( $wpdb->posts, array( 'post_status' => 'pending' ), array( 'ID' => $post_id, 'post_date_gmt' => '0000-00-00 00:00:00' ) );
1324 clean_post_cache( $post_id );
1325 }
1326 }
1327 }
1328
1329 // Handles the transition to 'publish' on post.php
1330 if ( isset( $edit_flow ) && $pagenow == 'post.php' && isset( $_POST['publish'] ) ) {
1331 // Set the post_status as 'pending' only when there's no timestamp set for $post_date_gmt
1332 if ( isset( $_POST['post_ID'] ) ) {
1333 $post_id = (int) $_POST['post_ID'];
1334 $ret = $wpdb->update( $wpdb->posts, array( 'post_status' => 'pending' ), array( 'ID' => $post_id, 'post_date_gmt' => '0000-00-00 00:00:00' ) );
1335 clean_post_cache( $post_id );
1336 foreach ( array('aa', 'mm', 'jj', 'hh', 'mn') as $timeunit ) {
1337 if ( !empty( $_POST['hidden_' . $timeunit] ) && $_POST['hidden_' . $timeunit] != $_POST[$timeunit] ) {
1338 $edit_date = '1';
1339 break;
1340 }
1341 }
1342 if ( $ret && empty( $edit_date ) ) {
1343 add_filter( 'pre_post_date', array( $this, 'helper_timestamp_hack' ) );
1344 add_filter( 'pre_post_date_gmt', array( $this, 'helper_timestamp_hack' ) );
1345 }
1346 }
1347 }
1348
1349 }
1350
1351 /**
1352 * PHP < 5.3.x doesn't support anonymous functions
1353 * This helper is only used for the check_timestamp_on_publish method above
1354 *
1355 * @since 0.7.3
1356 */
1357 function helper_timestamp_hack() {
1358 return ( 'pre_post_date' == current_filter() ) ? current_time('mysql') : '';
1359 }
1360
1361 /**
1362 * This is a hack! hack! hack! until core is fixed/better supports custom statuses
1363 *
1364 * @since 0.6.5
1365 *
1366 * Normalize post_date_gmt if it isn't set to the past or the future
1367 * @see Works around this limitation: https://core.trac.wordpress.org/browser/tags/4.5.1/src/wp-includes/post.php#L3182
1368 * @see Original thread: http://wordpress.org/support/topic/plugin-edit-flow-custom-statuses-create-timestamp-problem
1369 * @see Core ticket: http://core.trac.wordpress.org/ticket/18362
1370 */
1371 function fix_custom_status_timestamp( $data, $postarr ) {
1372 global $edit_flow;
1373 // Don't run this if Edit Flow isn't active, or we're on some other page
1374 if ( $this->disable_custom_statuses_for_post_type()
1375 || !isset( $edit_flow ) ) {
1376 return $data;
1377 }
1378
1379 $status_slugs = wp_list_pluck( $this->get_custom_statuses(), 'slug' );
1380
1381 //Post is scheduled or published? Ignoring.
1382 if ( !in_array( $postarr['post_status'], $status_slugs ) ) {
1383 return $data;
1384 }
1385
1386 //If empty, keep empty.
1387 if ( empty( $postarr['post_date_gmt'] )
1388 || '0000-00-00 00:00:00' == $postarr['post_date_gmt'] ) {
1389 $data['post_date_gmt'] = '0000-00-00 00:00:00';
1390 }
1391
1392 return $data;
1393 }
1394
1395 /**
1396 * Another hack! hack! hack! until core better supports custom statuses`
1397 *
1398 * @since 0.7.4
1399 *
1400 * Keep the post_name value empty for posts with custom statuses
1401 * Unless they've set it customly
1402 * @see https://github.com/Automattic/Edit-Flow/issues/123
1403 * @see http://core.trac.wordpress.org/browser/tags/3.4.2/wp-includes/post.php#L2530
1404 * @see http://core.trac.wordpress.org/browser/tags/3.4.2/wp-includes/post.php#L2646
1405 */
1406 public function fix_post_name( $post_id, $post ) {
1407 global $pagenow;
1408
1409 /*
1410 * Filters the $post object that will be modified
1411 *
1412 * @param $post WP_Post Post object being processed.
1413 */
1414 $post = apply_filters( 'ef_fix_post_name_post', $post );
1415
1416 // Only modify if we're using a pre-publish status on a supported custom post type
1417 $status_slugs = wp_list_pluck( $this->get_custom_statuses(), 'slug' );
1418 if ( 'post.php' != $pagenow
1419 || ! in_array( $post->post_status, $status_slugs )
1420 || ! in_array( $post->post_type, $this->get_post_types_for_module( $this->module ) ) )
1421 return;
1422
1423 // The slug has been set by the meta box
1424 if ( ! empty( $_POST['post_name'] ) )
1425 return;
1426
1427 global $wpdb;
1428
1429 $wpdb->update( $wpdb->posts, array( 'post_name' => '' ), array( 'ID' => $post_id ) );
1430 clean_post_cache( $post_id );
1431 }
1432
1433
1434 /**
1435 * Another hack! hack! hack! until core better supports custom statuses
1436 *
1437 * @since 0.7.4
1438 *
1439 * The preview link for an unpublished post should always be ?p=
1440 */
1441 public function fix_preview_link_part_one( $preview_link ) {
1442 global $pagenow;
1443
1444 $post = get_post( get_the_ID() );
1445
1446 // Only modify if we're using a pre-publish status on a supported custom post type
1447 $status_slugs = wp_list_pluck( $this->get_custom_statuses(), 'slug' );
1448 if ( !$post
1449 || !is_admin()
1450 || 'post.php' != $pagenow
1451 || !in_array( $post->post_status, $status_slugs )
1452 || !in_array( $post->post_type, $this->get_post_types_for_module( $this->module ) )
1453 || strpos( $preview_link, 'preview_id' ) !== false
1454 || $post->filter == 'sample' )
1455 return $preview_link;
1456
1457 return $this->get_preview_link( $post );
1458 }
1459
1460 /**
1461 * Another hack! hack! hack! until core better supports custom statuses
1462 *
1463 * @since 0.7.4
1464 *
1465 * The preview link for an unpublished post should always be ?p=
1466 * The code used to trigger a post preview doesn't also apply the 'preview_post_link' filter
1467 * So we can't do a targeted filter. Instead, we can even more hackily filter get_permalink
1468 * @see http://core.trac.wordpress.org/ticket/19378
1469 */
1470 public function fix_preview_link_part_two( $permalink, $post, $sample ) {
1471 global $pagenow;
1472
1473 if ( is_int( $post ) )
1474 $post = get_post( $post );
1475
1476 //Should we be doing anything at all?
1477 if( !in_array( $post->post_type, $this->get_post_types_for_module( $this->module ) ) )
1478 return $permalink;
1479
1480 //Is this published?
1481 if( in_array( $post->post_status, $this->published_statuses ) )
1482 return $permalink;
1483
1484 //Are we overriding the permalink? Don't do anything
1485 if( isset( $_POST['action'] ) && $_POST['action'] == 'sample-permalink' )
1486 return $permalink;
1487
1488 //Are we previewing the post from the normal post screen?
1489 if( ( $pagenow == 'post.php' || $pagenow == 'post-new.php' )
1490 && !isset( $_POST['wp-preview'] ) )
1491 return $permalink;
1492
1493 //If it's a sample permalink, not a preview
1494 if ( $sample ) {
1495 return $permalink;
1496 }
1497
1498 return $this->get_preview_link( $post );
1499 }
1500
1501 /**
1502 * Fix get_sample_permalink. Previosuly the 'editable_slug' filter was leveraged
1503 * to correct the sample permalink a user could edit on post.php. Since 4.4.40
1504 * the `get_sample_permalink` filter was added which allows greater flexibility in
1505 * manipulating the slug. Critical for cases like editing the sample permalink on
1506 * hierarchical post types.
1507 * @since 0.8.2
1508 *
1509 * @param string $permalink Sample permalink
1510 * @param int $post_id Post ID
1511 * @param string $title Post title
1512 * @param string $name Post name (slug)
1513 * @param WP_Post $post Post object
1514 * @return string $link Direct link to complete the action
1515 */
1516 public function fix_get_sample_permalink( $permalink, $post_id, $title, $name, $post ) {
1517 //Should we be doing anything at all?
1518 if( !in_array( $post->post_type, $this->get_post_types_for_module( $this->module ) ) )
1519 return $permalink;
1520
1521 //Is this published?
1522 if( in_array( $post->post_status, $this->published_statuses ) )
1523 return $permalink;
1524
1525 //Are we overriding the permalink? Don't do anything
1526 if( isset( $_POST['action'] ) && $_POST['action'] == 'sample-permalink' )
1527 return $permalink;
1528
1529 list( $permalink, $post_name ) = $permalink;
1530
1531 $post_name = $post->post_name ? $post->post_name : sanitize_title( $post->post_title );
1532 $post->post_name = $post_name;
1533
1534 $ptype = get_post_type_object( $post->post_type );
1535
1536 if ( $ptype->hierarchical ) {
1537 $post->filter = 'sample';
1538
1539 $uri = get_page_uri( $post->ID ) . $post_name;
1540
1541 if ( $uri ) {
1542 $uri = untrailingslashit($uri);
1543 $uri = strrev( stristr( strrev( $uri ), '/' ) );
1544 $uri = untrailingslashit($uri);
1545 }
1546
1547 /** This filter is documented in wp-admin/edit-tag-form.php */
1548 $uri = apply_filters( 'editable_slug', $uri, $post );
1549
1550 if ( !empty($uri) ) {
1551 $uri .= '/';
1552 }
1553
1554 $permalink = str_replace('%pagename%', "{$uri}%pagename%", $permalink);
1555 }
1556
1557 unset($post->post_name);
1558
1559 return array( $permalink, $post_name );
1560 }
1561
1562 /**
1563 * Hack to work around post status check in get_sample_permalink_html
1564 *
1565 *
1566 * The get_sample_permalink_html checks the status of the post and if it's
1567 * a draft generates a certain permalink structure.
1568 * We need to do the same work it's doing for custom statuses in order
1569 * to support this link
1570 * @see https://core.trac.wordpress.org/browser/tags/4.5.2/src/wp-admin/includes/post.php#L1296
1571 *
1572 * @since 0.8.2
1573 *
1574 * @param string $return Sample permalink HTML markup
1575 * @param int $post_id Post ID
1576 * @param string $new_title New sample permalink title
1577 * @param string $new_slug New sample permalink kslug
1578 * @param WP_Post $post Post object
1579 */
1580 function fix_get_sample_permalink_html( $return, $post_id, $new_title, $new_slug, $post ) {
1581 $status_slugs = wp_list_pluck( $this->get_custom_statuses(), 'slug' );
1582
1583 list($permalink, $post_name) = get_sample_permalink($post->ID, $new_title, $new_slug);
1584
1585 $view_link = false;
1586 $preview_target = '';
1587
1588 if ( current_user_can( 'read_post', $post_id ) ) {
1589 if ( in_array( $post->post_status, $status_slugs ) ) {
1590 $view_link = $this->get_preview_link( $post );
1591 $preview_target = " target='wp-preview-{$post->ID}'";
1592 } else {
1593 if ( 'publish' === $post->post_status || 'attachment' === $post->post_type ) {
1594 $view_link = get_permalink( $post );
1595 } else {
1596 // Allow non-published (private, future) to be viewed at a pretty permalink.
1597 $view_link = str_replace( array( '%pagename%', '%postname%' ), $post->post_name, $permalink );
1598 }
1599 }
1600 }
1601
1602 // Permalinks without a post/page name placeholder don't have anything to edit
1603 if ( false === strpos( $permalink, '%postname%' ) && false === strpos( $permalink, '%pagename%' ) ) {
1604 $return = '<strong>' . __( 'Permalink:' ) . "</strong>\n";
1605
1606 if ( false !== $view_link ) {
1607 $display_link = urldecode( $view_link );
1608 $return .= '<a id="sample-permalink" href="' . esc_url( $view_link ) . '"' . $preview_target . '>' . $display_link . "</a>\n";
1609 } else {
1610 $return .= '<span id="sample-permalink">' . $permalink . "</span>\n";
1611 }
1612
1613 // Encourage a pretty permalink setting
1614 if ( '' == get_option( 'permalink_structure' ) && current_user_can( 'manage_options' ) && !( 'page' == get_option('show_on_front') && $id == get_option('page_on_front') ) ) {
1615 $return .= '<span id="change-permalinks"><a href="options-permalink.php" class="button button-small" target="_blank">' . __('Change Permalinks') . "</a></span>\n";
1616 }
1617 } else {
1618 if ( function_exists( 'mb_strlen' ) ) {
1619 if ( mb_strlen( $post_name ) > 34 ) {
1620 $post_name_abridged = mb_substr( $post_name, 0, 16 ) . '&hellip;' . mb_substr( $post_name, -16 );
1621 } else {
1622 $post_name_abridged = $post_name;
1623 }
1624 } else {
1625 if ( strlen( $post_name ) > 34 ) {
1626 $post_name_abridged = substr( $post_name, 0, 16 ) . '&hellip;' . substr( $post_name, -16 );
1627 } else {
1628 $post_name_abridged = $post_name;
1629 }
1630 }
1631
1632 $post_name_html = '<span id="editable-post-name">' . $post_name_abridged . '</span>';
1633 $display_link = str_replace( array( '%pagename%', '%postname%' ), $post_name_html, urldecode( $permalink ) );
1634
1635 $return = '<strong>' . __( 'Permalink:' ) . "</strong>\n";
1636 $return .= '<span id="sample-permalink"><a href="' . esc_url( $view_link ) . '"' . $preview_target . '>' . $display_link . "</a></span>\n";
1637 $return .= '&lrm;'; // Fix bi-directional text display defect in RTL languages.
1638 $return .= '<span id="edit-slug-buttons"><button type="button" class="edit-slug button button-small hide-if-no-js" aria-label="' . __( 'Edit permalink' ) . '">' . __( 'Edit' ) . "</button></span>\n";
1639 $return .= '<span id="editable-post-name-full">' . $post_name . "</span>\n";
1640 }
1641
1642 return $return;
1643 }
1644
1645
1646 /**
1647 * Fixes a bug where post-pagination doesn't work when previewing a post with a custom status
1648 * @link https://github.com/Automattic/Edit-Flow/issues/192
1649 *
1650 * This filter only modifies output if `is_preview()` is true
1651 *
1652 * Used by `wp_link_pages_link` filter
1653 *
1654 * @param $link
1655 * @param $i
1656 *
1657 * @return string
1658 */
1659 function modify_preview_link_pagination_url( $link, $i ) {
1660
1661 // Use the original $link when not in preview mode
1662 if( ! is_preview() ) {
1663 return $link;
1664 }
1665
1666 // Get an array of valid custom status slugs
1667 $custom_statuses = wp_list_pluck( $this->get_custom_statuses(), 'slug');
1668
1669 // Apply original link filters from core `wp_link_pages()`
1670 $r = apply_filters( 'wp_link_pages_args', array(
1671 'link_before' => '',
1672 'link_after' => '',
1673 'pagelink' => '%',
1674 )
1675 );
1676
1677 // _wp_link_page() && _ef_wp_link_page() produce an opening link tag ( <a href=".."> )
1678 // This is necessary to replicate core behavior:
1679 $link = $r['link_before'] . str_replace( '%', $i, $r['pagelink'] ) . $r['link_after'];
1680 $link = _ef_wp_link_page( $i, $custom_statuses ) . $link . '</a>';
1681
1682
1683 return $link;
1684 }
1685
1686 /**
1687 * Get the proper preview link for a post
1688 *
1689 * @since 0.8
1690 */
1691 private function get_preview_link( $post ) {
1692
1693 if ( 'page' == $post->post_type ) {
1694 $args = array(
1695 'page_id' => $post->ID,
1696 );
1697 } else if ( 'post' == $post->post_type ) {
1698 $args = array(
1699 'p' => $post->ID,
1700 'preview' => 'true'
1701 );
1702 } else {
1703 $args = array(
1704 'p' => $post->ID,
1705 'post_type' => $post->post_type,
1706 );
1707 }
1708
1709 $args['preview_id'] = $post->ID;
1710 return add_query_arg( $args, home_url( '/' ) );
1711 }
1712
1713 /**
1714 * Another hack! hack! hack! until core better supports custom statuses
1715 *
1716 * @since 0.7.4
1717 *
1718 * The preview link for an unpublished post should always be ?p=, even in the list table
1719 * @see http://core.trac.wordpress.org/ticket/19378
1720 */
1721 public function fix_post_row_actions( $actions, $post ) {
1722 global $pagenow;
1723
1724 // Only modify if we're using a pre-publish status on a supported custom post type
1725 $status_slugs = wp_list_pluck( $this->get_custom_statuses(), 'slug' );
1726 if ( 'edit.php' != $pagenow
1727 || ! in_array( $post->post_status, $status_slugs )
1728 || ! in_array( $post->post_type, $this->get_post_types_for_module( $this->module ) ) )
1729 return $actions;
1730
1731 // 'view' is only set if the user has permission to post
1732 if ( empty( $actions['view'] ) )
1733 return $actions;
1734
1735 if ( 'page' == $post->post_type ) {
1736 $args = array(
1737 'page_id' => $post->ID,
1738 );
1739 } else if ( 'post' == $post->post_type ) {
1740 $args = array(
1741 'p' => $post->ID,
1742 );
1743 } else {
1744 $args = array(
1745 'p' => $post->ID,
1746 'post_type' => $post->post_type,
1747 );
1748 }
1749 $args['preview'] = 'true';
1750 $preview_link = add_query_arg( $args, home_url( '/' ) );
1751
1752 $actions['view'] = '<a href="' . esc_url( $preview_link ) . '" title="' . esc_attr( sprintf( __( 'Preview &#8220;%s&#8221;' ), $post->post_title ) ) . '" rel="permalink">' . __( 'Preview' ) . '</a>';
1753 return $actions;
1754 }
1755 }
1756
1757 }
1758
1759 /**
1760 * Custom Statuses uses WordPress' List Table API for generating the custom status management table
1761 *
1762 * @since 0.7
1763 */
1764 class EF_Custom_Status_List_Table extends WP_List_Table
1765 {
1766
1767 var $callback_args;
1768 var $default_status;
1769
1770 /**
1771 * Construct the extended class
1772 */
1773 function __construct() {
1774
1775 parent::__construct( array(
1776 'plural' => 'custom statuses',
1777 'singular' => 'custom status',
1778 'ajax' => true
1779 ) );
1780
1781 }
1782
1783 /**
1784 * Pull in the data we'll be displaying on the table
1785 *
1786 * @since 0.7
1787 */
1788 function prepare_items() {
1789 global $edit_flow;
1790
1791 $columns = $this->get_columns();
1792 $hidden = array(
1793 'position',
1794 );
1795 $sortable = array();
1796 $this->_column_headers = array($columns, $hidden, $sortable);
1797
1798 $this->items = $edit_flow->custom_status->get_custom_statuses();
1799 $total_items = count( $this->items );
1800 $this->default_status = $edit_flow->custom_status->get_default_custom_status()->slug;
1801
1802 $this->set_pagination_args( array(
1803 'total_items' => $total_items,
1804 'per_page' => $total_items,
1805 ) );
1806 }
1807
1808 /**
1809 * Message to be displayed when there are no custom statuses. Should never be displayed, but we'll customize it
1810 * just in case.
1811 *
1812 * @since 0.7
1813 */
1814 function no_items() {
1815 _e( 'No custom statuses found.', 'edit-flow' );
1816 }
1817
1818 /**
1819 * Table shows (hidden) position, status name, status description, and the post count for each activated
1820 * post type
1821 *
1822 * @since 0.7
1823 *
1824 * @return array $columns Columns to be registered with the List Table
1825 */
1826 function get_columns() {
1827 global $edit_flow;
1828
1829 $columns = array(
1830 'position' => __( 'Position', 'edit-flow' ),
1831 'name' => __( 'Name', 'edit-flow' ),
1832 'description' => __( 'Description', 'edit-flow' ),
1833 );
1834
1835 $post_types = get_post_types( '', 'objects' );
1836 $supported_post_types = $edit_flow->helpers->get_post_types_for_module( $edit_flow->custom_status->module );
1837 foreach ( $post_types as $post_type )
1838 if ( in_array( $post_type->name, $supported_post_types ) )
1839 $columns[$post_type->name] = $post_type->label;
1840
1841 return $columns;
1842 }
1843
1844 /**
1845 * Fallback column callback.
1846 * Primarily used to display post count for each post type
1847 *
1848 * @since 0.7
1849 *
1850 * @param object $item Custom status as an object
1851 * @param string $column_name Name of the column as registered in $this->prepare_items()
1852 * @return string $output What will be rendered
1853 */
1854 function column_default( $item, $column_name ) {
1855 global $edit_flow;
1856
1857 // Handle custom post counts for different post types
1858 $post_types = get_post_types( '', 'names' );
1859 if ( in_array( $column_name, $post_types ) ) {
1860
1861 // @todo Cachify this
1862 $post_count = wp_cache_get( "ef_custom_status_count_$column_name" );
1863 if ( false === $post_count ) {
1864 $posts = wp_count_posts( $column_name );
1865 $post_status = $item->slug;
1866 // To avoid error notices when changing the name of non-standard statuses
1867 if ( isset( $posts->$post_status ) )
1868 $post_count = $posts->$post_status;
1869 else
1870 $post_count = 0;
1871 //wp_cache_set( "ef_custom_status_count_$column_name", $post_count );
1872 }
1873 $output = sprintf( '<a title="See all %1$ss saved as \'%2$s\'" href="%3$s">%4$s</a>', $column_name, $item->name, $edit_flow->helpers->filter_posts_link( $item->slug, $column_name ), $post_count );
1874 return $output;
1875 }
1876
1877 }
1878
1879 /**
1880 * Hidden column for storing the status position
1881 *
1882 * @since 0.7
1883 *
1884 * @param object $item Custom status as an object
1885 * @return string $output What will be rendered
1886 */
1887 function column_position( $item ) {
1888 return esc_html( $item->position );
1889 }
1890
1891 /**
1892 * Displayed column showing the name of the status
1893 *
1894 * @since 0.7
1895 *
1896 * @param object $item Custom status as an object
1897 * @return string $output What will be rendered
1898 */
1899 function column_name( $item ) {
1900 global $edit_flow;
1901
1902 $item_edit_link = esc_url( $edit_flow->custom_status->get_link( array( 'action' => 'edit-status', 'term-id' => $item->term_id ) ) );
1903
1904 $output = '<strong><a href="' . $item_edit_link . '">' . esc_html( $item->name ) . '</a>';
1905 if ( $item->slug == $this->default_status )
1906 $output .= ' - ' . __( 'Default', 'edit-flow' );
1907 $output .= '</strong>';
1908
1909 // Don't allow for any of these status actions when adding a new custom status
1910 if ( isset( $_GET['action'] ) && $_GET['action'] == 'add' )
1911 return $output;
1912
1913 $actions = array();
1914 $actions['edit'] = "<a href='$item_edit_link'>" . __( 'Edit', 'edit-flow' ) . "</a>";
1915 $actions['inline hide-if-no-js'] = '<a href="#" class="editinline">' . __( 'Quick&nbsp;Edit' ) . '</a>';
1916 $actions['make_default'] = sprintf( '<a href="%1$s">' . __( 'Make&nbsp;Default', 'edit-flow' ) . '</a>', $edit_flow->custom_status->get_link( array( 'action' => 'make-default', 'term-id' => $item->term_id ) ) );
1917
1918 // Prevent deleting draft status
1919 if( 'draft' !== $item->slug && $item->slug !== $this->default_status ) {
1920 $actions['delete delete-status'] = sprintf( '<a href="%1$s">' . __( 'Delete', 'edit-flow' ) . '</a>', $edit_flow->custom_status->get_link( array( 'action' => 'delete-status', 'term-id' => $item->term_id ) ) );
1921 }
1922
1923 $output .= $this->row_actions( $actions, false );
1924 $output .= '<div class="hidden" id="inline_' . esc_attr( $item->term_id ) . '">';
1925 $output .= '<div class="name">' . esc_html( $item->name ) . '</div>';
1926 $output .= '<div class="description">' . esc_html( $item->description ) . '</div>';
1927 $output .= '</div>';
1928
1929 return $output;
1930
1931 }
1932
1933 /**
1934 * Displayed column showing the description of the status
1935 *
1936 * @since 0.7
1937 *
1938 * @param object $item Custom status as an object
1939 * @return string $output What will be rendered
1940 */
1941 function column_description( $item ) {
1942 return esc_html( $item->description );
1943 }
1944
1945 /**
1946 * Prepare and echo a single custom status row
1947 *
1948 * @since 0.7
1949 */
1950 function single_row( $item ) {
1951 static $alternate_class = '';
1952 $alternate_class = ( $alternate_class == '' ? ' alternate' : '' );
1953 $row_class = ' class="term-static' . $alternate_class . '"';
1954
1955 echo '<tr id="term-' . $item->term_id . '"' . $row_class . '>';
1956 echo $this->single_row_columns( $item );
1957 echo '</tr>';
1958 }
1959
1960 /**
1961 * Hidden form used for inline editing functionality
1962 *
1963 * @since 0.7
1964 */
1965 function inline_edit() {
1966 global $edit_flow;
1967 ?>
1968 <form method="get" action=""><table style="display: none"><tbody id="inlineedit">
1969 <tr id="inline-edit" class="inline-edit-row" style="display: none"><td colspan="<?php echo $this->get_column_count(); ?>" class="colspanchange">
1970 <fieldset><div class="inline-edit-col">
1971 <h4><?php _e( 'Quick Edit' ); ?></h4>
1972 <label>
1973 <span class="title"><?php _e( 'Name', 'edit-flow' ); ?></span>
1974 <span class="input-text-wrap"><input type="text" name="name" class="ptitle" value="" maxlength="20" /></span>
1975 </label>
1976 <label>
1977 <span class="title"><?php _e( 'Description', 'edit-flow' ); ?></span>
1978 <span class="input-text-wrap"><input type="text" name="description" class="pdescription" value="" /></span>
1979 </label>
1980 </div></fieldset>
1981 <p class="inline-edit-save submit">
1982 <a accesskey="c" href="#inline-edit" title="<?php _e( 'Cancel' ); ?>" class="cancel button-secondary alignleft"><?php _e( 'Cancel' ); ?></a>
1983 <?php $update_text = __( 'Update Status', 'edit-flow' ); ?>
1984 <a accesskey="s" href="#inline-edit" title="<?php echo esc_attr( $update_text ); ?>" class="save button-primary alignright"><?php echo $update_text; ?></a>
1985 <img class="waiting" style="display:none;" src="<?php echo esc_url( admin_url( 'images/wpspin_light.gif' ) ); ?>" alt="" />
1986 <span class="error" style="display:none;"></span>
1987 <?php wp_nonce_field( 'custom-status-inline-edit-nonce', 'inline_edit', false ); ?>
1988 <br class="clear" />
1989 </p>
1990 </td></tr>
1991 </tbody></table></form>
1992 <?php
1993 }
1994
1995 }
1996