| 1 |
<?php |
| 2 |
/** |
| 3 |
* Custom Status module for Edit Flow. |
| 4 |
* |
| 5 |
* Custom statuses make it simple to define the different stages in your publishing workflow. |
| 6 |
* |
| 7 |
* @todo Improve the copy. |
| 8 |
* @todo Thoroughly test what happens when the default post statuses 'Draft' and 'Pending Review' no longer exist. |
| 9 |
* @todo Ensure all of the form processing uses our messages functionality. |
| 10 |
* |
| 11 |
* @package EditFlow |
| 12 |
*/ |
| 13 |
|
| 14 |
if ( ! class_exists( 'EF_Custom_Status' ) ) { |
| 15 |
|
| 16 |
/** |
| 17 |
* Custom Status module class. |
| 18 |
* |
| 19 |
* Allows defining custom post statuses to create structured publishing workflows. |
| 20 |
*/ |
| 21 |
class EF_Custom_Status extends EF_Module { |
| 22 |
|
| 23 |
/** |
| 24 |
* The module object. |
| 25 |
* |
| 26 |
* @var object |
| 27 |
*/ |
| 28 |
public $module; |
| 29 |
|
| 30 |
/** |
| 31 |
* Cache for custom statuses. |
| 32 |
* |
| 33 |
* @var array |
| 34 |
*/ |
| 35 |
private $custom_statuses_cache = []; |
| 36 |
|
| 37 |
/** |
| 38 |
* Taxonomy name used to store all our custom statuses. |
| 39 |
* |
| 40 |
* @var string |
| 41 |
*/ |
| 42 |
// phpcs:ignore Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase |
| 43 |
const taxonomy_key = 'post_status'; |
| 44 |
|
| 45 |
/** |
| 46 |
* Register the module with Edit Flow but don't do anything else. |
| 47 |
*/ |
| 48 |
public function __construct() { |
| 49 |
|
| 50 |
$this->module_url = $this->get_module_url( __FILE__ ); |
| 51 |
// Register the module with Edit Flow. |
| 52 |
$args = [ |
| 53 |
'title' => __( 'Custom Statuses', 'edit-flow' ), |
| 54 |
'short_description' => __( 'Create custom post statuses to define the stages of your workflow.', 'edit-flow' ), |
| 55 |
'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' ), |
| 56 |
'module_url' => $this->module_url, |
| 57 |
'img_url' => $this->module_url . 'lib/custom_status_s128.png', |
| 58 |
'slug' => 'custom-status', |
| 59 |
'default_options' => [ |
| 60 |
'enabled' => 'on', |
| 61 |
'default_status' => 'pitch', |
| 62 |
'always_show_dropdown' => 'off', |
| 63 |
'post_types' => [ |
| 64 |
'post' => 'on', |
| 65 |
'page' => 'on', |
| 66 |
], |
| 67 |
], |
| 68 |
'post_type_support' => 'ef_custom_statuses', // This has been plural in all of our docs. |
| 69 |
'configure_page_cb' => 'print_configure_view', |
| 70 |
'configure_link_text' => __( 'Edit Statuses', 'edit-flow' ), |
| 71 |
'messages' => [ |
| 72 |
'status-added' => __( 'Post status created.', 'edit-flow' ), |
| 73 |
'status-missing' => __( "Post status doesn't exist.", 'edit-flow' ), |
| 74 |
'default-status-changed' => __( 'Default post status has been changed.', 'edit-flow' ), |
| 75 |
'term-updated' => __( 'Post status updated.', 'edit-flow' ), |
| 76 |
'status-deleted' => __( 'Post status deleted.', 'edit-flow' ), |
| 77 |
'status-position-updated' => __( 'Status order updated.', 'edit-flow' ), |
| 78 |
'status-migrated' => __( 'Posts migrated successfully.', 'edit-flow' ), |
| 79 |
], |
| 80 |
'autoload' => false, |
| 81 |
'settings_help_tab' => [ |
| 82 |
'id' => 'ef-custom-status-overview', |
| 83 |
'title' => __( 'Overview', 'edit-flow' ), |
| 84 |
'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' ), |
| 85 |
], |
| 86 |
'settings_help_sidebar' => __( '<p><strong>For more information:</strong></p><p><a href="https://editflow.org/features/custom-statuses/">Custom Status Documentation</a></p><p><a href="https://wordpress.org/support/plugin/edit-flow/">Edit Flow Forum</a></p><p><a href="https://github.com/Automattic/Edit-Flow">Edit Flow on GitHub</a></p>', 'edit-flow' ), |
| 87 |
]; |
| 88 |
$this->module = EditFlow()->register_module( 'custom_status', $args ); |
| 89 |
} |
| 90 |
|
| 91 |
/** |
| 92 |
* Initialize the EF_Custom_Status class if the module is active. |
| 93 |
*/ |
| 94 |
public function init() { |
| 95 |
global $edit_flow; |
| 96 |
|
| 97 |
// Load WP-CLI commands. |
| 98 |
if ( defined( 'WP_CLI' ) && WP_CLI ) { |
| 99 |
require_once __DIR__ . '/lib/class-cli.php'; |
| 100 |
} |
| 101 |
|
| 102 |
// Register custom statuses as a taxonomy. |
| 103 |
$this->register_custom_statuses(); |
| 104 |
|
| 105 |
// Register our settings. |
| 106 |
add_action( 'admin_init', [ $this, 'register_settings' ] ); |
| 107 |
|
| 108 |
if ( ! $this->disable_custom_statuses_for_post_type() ) { |
| 109 |
// Load CSS and JS resources that we probably need in the admin page. |
| 110 |
add_action( 'admin_enqueue_scripts', [ $this, 'action_admin_enqueue_scripts' ] ); |
| 111 |
|
| 112 |
// Assets for block editor UI. |
| 113 |
add_action( 'enqueue_block_editor_assets', [ $this, 'load_scripts_for_block_editor' ] ); |
| 114 |
|
| 115 |
// Assets for iframed block editor and editor UI. |
| 116 |
add_action( 'enqueue_block_editor_assets', [ $this, 'load_styles_for_block_editor' ] ); |
| 117 |
} |
| 118 |
|
| 119 |
add_action( 'admin_notices', [ $this, 'no_js_notice' ] ); |
| 120 |
add_action( 'admin_print_scripts', [ $this, 'post_admin_header' ] ); |
| 121 |
|
| 122 |
// Add custom statuses to the post states. |
| 123 |
add_filter( 'display_post_states', [ $this, 'add_status_to_post_states' ], 10, 2 ); |
| 124 |
|
| 125 |
// Methods for handling the actions of creating, making default, and deleting post stati. |
| 126 |
add_action( 'admin_init', [ $this, 'handle_add_custom_status' ] ); |
| 127 |
add_action( 'admin_init', [ $this, 'handle_edit_custom_status' ] ); |
| 128 |
add_action( 'admin_init', [ $this, 'handle_make_default_custom_status' ] ); |
| 129 |
add_action( 'admin_init', [ $this, 'handle_delete_custom_status' ] ); |
| 130 |
add_action( 'admin_init', [ $this, 'handle_migrate_status' ] ); |
| 131 |
add_action( 'wp_ajax_update_status_positions', [ $this, 'handle_ajax_update_status_positions' ] ); |
| 132 |
add_action( 'wp_ajax_inline_save_status', [ $this, 'ajax_inline_save_status' ] ); |
| 133 |
|
| 134 |
// These seven-ish methods are hacks for fixing bugs in WordPress core. |
| 135 |
add_action( 'admin_init', [ $this, 'check_timestamp_on_publish' ] ); |
| 136 |
add_filter( 'wp_insert_post_data', [ $this, 'fix_custom_status_timestamp' ], 10, 2 ); |
| 137 |
add_filter( 'wp_insert_post_data', [ $this, 'maybe_keep_post_name_empty' ], 10, 2 ); |
| 138 |
add_filter( 'wp_insert_post_data', [ $this, 'update_post_date_on_publish_from_custom_status' ], 10, 2 ); |
| 139 |
add_action( 'rest_api_init', [ $this, 'register_rest_api_filters' ] ); |
| 140 |
add_filter( 'pre_wp_unique_post_slug', [ $this, 'fix_unique_post_slug' ], 10, 6 ); |
| 141 |
add_filter( 'preview_post_link', [ $this, 'fix_preview_link_part_one' ] ); |
| 142 |
add_filter( 'post_link', [ $this, 'fix_preview_link_part_two' ], 10, 3 ); |
| 143 |
add_filter( 'page_link', [ $this, 'fix_preview_link_part_two' ], 10, 3 ); |
| 144 |
add_filter( 'post_type_link', [ $this, 'fix_preview_link_part_two' ], 10, 3 ); |
| 145 |
add_filter( 'preview_post_link', [ $this, 'fix_preview_link_part_three' ], 11, 2 ); |
| 146 |
add_action( 'template_redirect', [ $this, 'fix_preview_template' ] ); |
| 147 |
add_filter( 'get_sample_permalink', [ $this, 'fix_get_sample_permalink' ], 10, 5 ); |
| 148 |
add_filter( 'get_sample_permalink_html', [ $this, 'fix_get_sample_permalink_html' ], 10, 5 ); |
| 149 |
add_filter( 'post_row_actions', [ $this, 'fix_post_row_actions' ], 10, 2 ); |
| 150 |
add_filter( 'page_row_actions', [ $this, 'fix_post_row_actions' ], 10, 2 ); |
| 151 |
|
| 152 |
// Pagination for custom post statuses when previewing posts. |
| 153 |
add_filter( 'wp_link_pages_link', [ $this, 'modify_preview_link_pagination_url' ], 10, 2 ); |
| 154 |
} |
| 155 |
|
| 156 |
/** |
| 157 |
* Create the default set of custom statuses the first time the module is loaded |
| 158 |
* |
| 159 |
* @since 0.7 |
| 160 |
*/ |
| 161 |
public function install() { |
| 162 |
|
| 163 |
$default_terms = [ |
| 164 |
[ |
| 165 |
'term' => __( 'Pitch', 'edit-flow' ), |
| 166 |
'args' => [ |
| 167 |
'slug' => 'pitch', |
| 168 |
'description' => __( 'Idea proposed; waiting for acceptance.', 'edit-flow' ), |
| 169 |
'position' => 1, |
| 170 |
], |
| 171 |
], |
| 172 |
[ |
| 173 |
'term' => __( 'Assigned', 'edit-flow' ), |
| 174 |
'args' => [ |
| 175 |
'slug' => 'assigned', |
| 176 |
'description' => __( 'Post idea assigned to writer.', 'edit-flow' ), |
| 177 |
'position' => 2, |
| 178 |
], |
| 179 |
], |
| 180 |
[ |
| 181 |
'term' => __( 'In Progress', 'edit-flow' ), |
| 182 |
'args' => [ |
| 183 |
'slug' => 'in-progress', |
| 184 |
'description' => __( 'Writer is working on the post.', 'edit-flow' ), |
| 185 |
'position' => 3, |
| 186 |
], |
| 187 |
], |
| 188 |
[ |
| 189 |
'term' => __( 'Draft', 'edit-flow' ), |
| 190 |
'args' => [ |
| 191 |
'slug' => 'draft', |
| 192 |
'description' => __( 'Post is a draft; not ready for review or publication.', 'edit-flow' ), |
| 193 |
'position' => 4, |
| 194 |
], |
| 195 |
], |
| 196 |
[ |
| 197 |
'term' => __( 'Pending Review', 'edit-flow' ), |
| 198 |
'args' => [ |
| 199 |
'slug' => 'pending', |
| 200 |
'description' => __( 'Post needs to be reviewed by an editor.', 'edit-flow' ), |
| 201 |
'position' => 5, |
| 202 |
], |
| 203 |
], |
| 204 |
]; |
| 205 |
|
| 206 |
// Okay, now add the default statuses to the db if they don't already exist. |
| 207 |
foreach ( $default_terms as $term ) { |
| 208 |
if ( ! term_exists( $term['term'], self::taxonomy_key ) ) { |
| 209 |
$this->add_custom_status( $term['term'], $term['args'] ); |
| 210 |
} |
| 211 |
} |
| 212 |
} |
| 213 |
|
| 214 |
/** |
| 215 |
* Upgrade our data in case we need to. |
| 216 |
* |
| 217 |
* @since 0.7 |
| 218 |
* |
| 219 |
* @param string $previous_version Previous plugin version. |
| 220 |
*/ |
| 221 |
public function upgrade( $previous_version ) { |
| 222 |
global $edit_flow; |
| 223 |
|
| 224 |
// Upgrade path to v0.7. |
| 225 |
if ( version_compare( $previous_version, '0.7', '<' ) ) { |
| 226 |
// Migrate dropdown visibility option. |
| 227 |
$dropdown_visible = get_option( 'edit_flow_status_dropdown_visible' ); |
| 228 |
if ( $dropdown_visible ) { |
| 229 |
$dropdown_visible = 'on'; |
| 230 |
} else { |
| 231 |
$dropdown_visible = 'off'; |
| 232 |
} |
| 233 |
$edit_flow->update_module_option( $this->module->name, 'always_show_dropdown', $dropdown_visible ); |
| 234 |
delete_option( 'edit_flow_status_dropdown_visible' ); |
| 235 |
// Migrate default status option. |
| 236 |
$default_status = get_option( 'edit_flow_custom_status_default_status' ); |
| 237 |
if ( $default_status ) { |
| 238 |
$edit_flow->update_module_option( $this->module->name, 'default_status', $default_status ); |
| 239 |
} |
| 240 |
delete_option( 'edit_flow_custom_status_default_status' ); |
| 241 |
|
| 242 |
// Technically we've run this code before so we don't want to auto-install new data. |
| 243 |
$edit_flow->update_module_option( $this->module->name, 'loaded_once', true ); |
| 244 |
} |
| 245 |
// Upgrade path to v0.7.4. |
| 246 |
if ( version_compare( $previous_version, '0.7.4', '<' ) ) { |
| 247 |
// Custom status descriptions become base64_encoded, instead of maybe json_encoded. |
| 248 |
$this->upgrade_074_term_descriptions( self::taxonomy_key ); |
| 249 |
} |
| 250 |
} |
| 251 |
|
| 252 |
/** |
| 253 |
* Makes the call to register_post_status to register the user's custom statuses. |
| 254 |
* |
| 255 |
* Also unregisters draft and pending, in case the user doesn't want them. |
| 256 |
*/ |
| 257 |
public function register_custom_statuses() { |
| 258 |
global $wp_post_statuses; |
| 259 |
|
| 260 |
if ( $this->disable_custom_statuses_for_post_type() ) { |
| 261 |
return; |
| 262 |
} |
| 263 |
|
| 264 |
// Register new taxonomy so that we can store all our fancy new custom statuses (or is it stati?). |
| 265 |
if ( ! taxonomy_exists( self::taxonomy_key ) ) { |
| 266 |
$args = [ |
| 267 |
'hierarchical' => false, |
| 268 |
'update_count_callback' => '_update_post_term_count', |
| 269 |
'label' => false, |
| 270 |
'query_var' => false, |
| 271 |
'rewrite' => false, |
| 272 |
'show_ui' => false, |
| 273 |
]; |
| 274 |
register_taxonomy( self::taxonomy_key, 'post', $args ); |
| 275 |
} |
| 276 |
|
| 277 |
if ( function_exists( 'register_post_status' ) ) { |
| 278 |
// Users can delete draft and pending statuses if they want, so let's get rid of them. |
| 279 |
// They'll get re-added if the user hasn't "deleted" them. |
| 280 |
unset( $wp_post_statuses['draft'] ); |
| 281 |
unset( $wp_post_statuses['pending'] ); |
| 282 |
|
| 283 |
$custom_statuses = $this->get_custom_statuses(); |
| 284 |
|
| 285 |
// Unfortunately, register_post_status() doesn't accept a post type argument, |
| 286 |
// so we have to register the post statuses for all post types. |
| 287 |
// This results in all post statuses for a post type appearing at the top |
| 288 |
// of manage posts if there is a post with the status. |
| 289 |
foreach ( $custom_statuses as $status ) { |
| 290 |
register_post_status( $status->slug, [ |
| 291 |
'label' => $status->name, |
| 292 |
'protected' => true, |
| 293 |
'_builtin' => false, |
| 294 |
// phpcs:ignore WordPress.WP.I18n.InterpolatedVariableSingular,WordPress.WP.I18n.InterpolatedVariablePlural -- Status name is user-defined and dynamic. |
| 295 |
'label_count' => _n_noop( "{$status->name} <span class='count'>(%s)</span>", "{$status->name} <span class='count'>(%s)</span>", 'edit-flow' ), |
| 296 |
] ); |
| 297 |
} |
| 298 |
} |
| 299 |
} |
| 300 |
|
| 301 |
/** |
| 302 |
* Whether custom post statuses should be disabled for this post type. |
| 303 |
* |
| 304 |
* Used to stop custom statuses from being registered for post types that don't support them. |
| 305 |
* |
| 306 |
* @since 0.7.5 |
| 307 |
* |
| 308 |
* @param string|null $post_type The post type to check, or null to auto-detect. |
| 309 |
* @return bool |
| 310 |
*/ |
| 311 |
public function disable_custom_statuses_for_post_type( $post_type = null ) { |
| 312 |
global $pagenow; |
| 313 |
|
| 314 |
// Only allow deregistering on 'edit.php' and 'post.php'. |
| 315 |
if ( ! in_array( $pagenow, [ 'edit.php', 'post.php', 'post-new.php' ] ) ) { |
| 316 |
return false; |
| 317 |
} |
| 318 |
|
| 319 |
if ( is_null( $post_type ) ) { |
| 320 |
$post_type = $this->get_current_post_type(); |
| 321 |
} |
| 322 |
|
| 323 |
if ( $post_type && ! in_array( $post_type, $this->get_post_types_for_module( $this->module ) ) ) { |
| 324 |
return true; |
| 325 |
} |
| 326 |
|
| 327 |
return false; |
| 328 |
} |
| 329 |
|
| 330 |
/** |
| 331 |
* Enqueue Javascript resources that we need in the admin: |
| 332 |
* - Primary use of Javascript is to manipulate the post status dropdown on Edit Post and Manage Posts |
| 333 |
* - jQuery Sortable plugin is used for drag and dropping custom statuses |
| 334 |
* - We have other custom code for Quick Edit and JS niceties |
| 335 |
*/ |
| 336 |
public function action_admin_enqueue_scripts() { |
| 337 |
// Load Javascript we need to use on the configuration views (jQuery Sortable and Quick Edit). |
| 338 |
if ( $this->is_whitelisted_settings_view( $this->module->name ) ) { |
| 339 |
wp_enqueue_script( 'jquery-ui-sortable' ); |
| 340 |
wp_enqueue_script( 'edit-flow-custom-status-configure', $this->module_url . 'lib/custom-status-configure.js', [ 'jquery', 'jquery-ui-sortable', 'edit-flow-settings-js' ], EDIT_FLOW_VERSION, true ); |
| 341 |
|
| 342 |
wp_localize_script( 'edit-flow-custom-status-configure', '__ef_localize_custom_status_configure', [ |
| 343 |
'delete_status_string' => __( 'Are you sure you want to delete the post status? All posts with this status will be assigned to the default status.', 'edit-flow' ), |
| 344 |
] ); |
| 345 |
} |
| 346 |
|
| 347 |
// Custom javascript to modify the post status dropdown where it shows up. |
| 348 |
if ( $this->is_whitelisted_page() ) { |
| 349 |
wp_enqueue_script( 'edit_flow-custom_status', $this->module_url . 'lib/custom-status.js', [ 'jquery', 'post' ], EDIT_FLOW_VERSION, true ); |
| 350 |
wp_localize_script('edit_flow-custom_status', '__ef_localize_custom_status', [ |
| 351 |
'no_change' => esc_html__( '— No Change —', 'edit-flow' ), |
| 352 |
'published' => esc_html__( 'Published', 'edit-flow' ), |
| 353 |
'private' => esc_html__( 'Private', 'edit-flow' ), |
| 354 |
'save_as' => esc_html__( 'Save as', 'edit-flow' ), |
| 355 |
'save' => esc_html__( 'Save', 'edit-flow' ), |
| 356 |
'edit' => esc_html__( 'Edit', 'edit-flow' ), |
| 357 |
'ok' => esc_html__( 'OK', 'edit-flow' ), |
| 358 |
'cancel' => esc_html__( 'Cancel', 'edit-flow' ), |
| 359 |
] ); |
| 360 |
} |
| 361 |
} |
| 362 |
|
| 363 |
/** |
| 364 |
* Load scripts for the block editor. |
| 365 |
*/ |
| 366 |
public function load_scripts_for_block_editor() { |
| 367 |
global $post; |
| 368 |
|
| 369 |
$asset_file = EDIT_FLOW_ROOT . '/build/custom-status-block.asset.php'; |
| 370 |
$asset = file_exists( $asset_file ) ? require $asset_file : [ |
| 371 |
'dependencies' => [], |
| 372 |
'version' => EDIT_FLOW_VERSION, |
| 373 |
]; |
| 374 |
|
| 375 |
wp_enqueue_script( |
| 376 |
'edit-flow-block-custom-status-script', |
| 377 |
EDIT_FLOW_URL . 'build/custom-status-block.js', |
| 378 |
$asset['dependencies'], |
| 379 |
$asset['version'], |
| 380 |
true |
| 381 |
); |
| 382 |
|
| 383 |
$custom_statuses = apply_filters( 'ef_custom_status_list', $this->get_custom_statuses(), $post ); |
| 384 |
|
| 385 |
wp_localize_script( 'edit-flow-block-custom-status-script', 'EditFlowCustomStatuses', array_values( $custom_statuses ) ); |
| 386 |
} |
| 387 |
|
| 388 |
/** |
| 389 |
* Load styles for the block editor. |
| 390 |
*/ |
| 391 |
public function load_styles_for_block_editor() { |
| 392 |
$asset_file = EDIT_FLOW_ROOT . '/build/custom-status-block.asset.php'; |
| 393 |
$asset = file_exists( $asset_file ) ? require $asset_file : [ |
| 394 |
'dependencies' => [], |
| 395 |
'version' => EDIT_FLOW_VERSION, |
| 396 |
]; |
| 397 |
|
| 398 |
wp_enqueue_style( |
| 399 |
'edit-flow-block-custom-status-styles', |
| 400 |
EDIT_FLOW_URL . 'build/custom-status-block.css', |
| 401 |
[], |
| 402 |
$asset['version'] |
| 403 |
); |
| 404 |
} |
| 405 |
|
| 406 |
/** |
| 407 |
* Displays a notice to users if they have JS disabled |
| 408 |
* Javascript is needed for custom statuses to be fully functional |
| 409 |
*/ |
| 410 |
public function no_js_notice() { |
| 411 |
if ( $this->is_whitelisted_page() ) : |
| 412 |
?> |
| 413 |
<style type="text/css"> |
| 414 |
/* Hide post status dropdown by default in case of JS issues **/ |
| 415 |
label[for=post_status], |
| 416 |
#post-status-display, |
| 417 |
#post-status-select, |
| 418 |
#publish { |
| 419 |
display: none; |
| 420 |
} |
| 421 |
</style> |
| 422 |
<div class="update-nag hide-if-js"> |
| 423 |
<?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' ); ?> |
| 424 |
</div> |
| 425 |
<?php |
| 426 |
endif; |
| 427 |
} |
| 428 |
|
| 429 |
/** |
| 430 |
* Check whether custom status stuff should be loaded on this page. |
| 431 |
* |
| 432 |
* @todo Migrate this to the base module class. |
| 433 |
* |
| 434 |
* @return bool Whether the page is whitelisted. |
| 435 |
*/ |
| 436 |
public function is_whitelisted_page() { |
| 437 |
global $pagenow; |
| 438 |
|
| 439 |
if ( ! in_array( $this->get_current_post_type(), $this->get_post_types_for_module( $this->module ) ) ) { |
| 440 |
return false; |
| 441 |
} |
| 442 |
|
| 443 |
$post_type_obj = get_post_type_object( $this->get_current_post_type() ); |
| 444 |
|
| 445 |
if ( ! current_user_can( $post_type_obj->cap->edit_posts ) ) { |
| 446 |
return false; |
| 447 |
} |
| 448 |
|
| 449 |
// Only add the script to Edit Post and Edit Page pages. |
| 450 |
return in_array( $pagenow, [ 'post.php', 'edit.php', 'post-new.php', 'page.php', 'edit-pages.php', 'page-new.php' ] ); |
| 451 |
} |
| 452 |
|
| 453 |
/** |
| 454 |
* Adds all necessary javascripts to make custom statuses work. |
| 455 |
* |
| 456 |
* @todo Support private and future posts on edit.php view. |
| 457 |
*/ |
| 458 |
public function post_admin_header() { |
| 459 |
global $post, $edit_flow, $pagenow, $current_user; |
| 460 |
|
| 461 |
if ( $this->disable_custom_statuses_for_post_type() ) { |
| 462 |
return; |
| 463 |
} |
| 464 |
|
| 465 |
// Get current user. |
| 466 |
wp_get_current_user(); |
| 467 |
|
| 468 |
// Only add the script to Edit Post and Edit Page pages. |
| 469 |
if ( $this->is_whitelisted_page() ) { |
| 470 |
$custom_statuses = $this->get_custom_statuses(); |
| 471 |
|
| 472 |
// $selected can be empty, but must be set because it's used as a JS variable. |
| 473 |
$selected = ''; |
| 474 |
$selected_name = ''; |
| 475 |
|
| 476 |
if ( ! empty( $post ) ) { |
| 477 |
// Get the status of the current post. |
| 478 |
if ( 0 == $post->ID || 'auto-draft' == $post->post_status || 'edit.php' == $pagenow ) { |
| 479 |
// @todo Check to make sure that the default exists. |
| 480 |
$selected = $this->get_default_custom_status()->slug; |
| 481 |
} else { |
| 482 |
$selected = $post->post_status; |
| 483 |
} |
| 484 |
|
| 485 |
// Get the label of current status. |
| 486 |
foreach ( $custom_statuses as $status ) { |
| 487 |
if ( $status->slug == $selected ) { |
| 488 |
$selected_name = $status->name; |
| 489 |
} |
| 490 |
} |
| 491 |
} |
| 492 |
|
| 493 |
$custom_statuses = apply_filters( 'ef_custom_status_list', $custom_statuses, $post ); |
| 494 |
|
| 495 |
// All right, we want to set up the JS var which contains all custom statuses. |
| 496 |
$all_statuses = []; |
| 497 |
|
| 498 |
// The default statuses from WordPress. |
| 499 |
$all_statuses[] = [ |
| 500 |
'name' => __( 'Published', 'edit-flow' ), |
| 501 |
'slug' => 'publish', |
| 502 |
'description' => '', |
| 503 |
]; |
| 504 |
$all_statuses[] = [ |
| 505 |
'name' => __( 'Privately Published', 'edit-flow' ), |
| 506 |
'slug' => 'private', |
| 507 |
'description' => '', |
| 508 |
]; |
| 509 |
$all_statuses[] = [ |
| 510 |
'name' => __( 'Scheduled', 'edit-flow' ), |
| 511 |
'slug' => 'future', |
| 512 |
'description' => '', |
| 513 |
]; |
| 514 |
|
| 515 |
// Load the custom statuses. |
| 516 |
foreach ( $custom_statuses as $status ) { |
| 517 |
$all_statuses[] = [ |
| 518 |
'name' => esc_js( $status->name ), |
| 519 |
'slug' => esc_js( $status->slug ), |
| 520 |
'description' => esc_js( $status->description ), |
| 521 |
]; |
| 522 |
} |
| 523 |
|
| 524 |
$always_show_dropdown = ( 'on' == $this->module->options->always_show_dropdown ) ? 1 : 0; |
| 525 |
|
| 526 |
$post_type_obj = get_post_type_object( $this->get_current_post_type() ); |
| 527 |
|
| 528 |
// Now, let's print the JS vars. |
| 529 |
?> |
| 530 |
<script type="text/javascript"> |
| 531 |
var custom_statuses = <?php echo wp_json_encode( $all_statuses ); ?>; |
| 532 |
var ef_default_custom_status = '<?php echo esc_js( $this->get_default_custom_status()->slug ); ?>'; |
| 533 |
var current_status = '<?php echo esc_js( $selected ); ?>'; |
| 534 |
var current_status_name = '<?php echo esc_js( $selected_name ); ?>'; |
| 535 |
var status_dropdown_visible = <?php echo esc_js( $always_show_dropdown ); ?>; |
| 536 |
var current_user_can_publish_posts = <?php echo current_user_can( $post_type_obj->cap->publish_posts ) ? 1 : 0; ?>; |
| 537 |
var current_user_can_edit_published_posts = <?php echo current_user_can( $post_type_obj->cap->edit_published_posts ) ? 1 : 0; ?>; |
| 538 |
</script> |
| 539 |
|
| 540 |
<?php |
| 541 |
} |
| 542 |
} |
| 543 |
|
| 544 |
/** |
| 545 |
* Adds a new custom status as a term in the wp_terms table. |
| 546 |
* |
| 547 |
* Basically a wrapper for the wp_insert_term class. |
| 548 |
* |
| 549 |
* The arguments decide how the term is handled based on the $args parameter. |
| 550 |
* The following is a list of the available overrides and the defaults. |
| 551 |
* |
| 552 |
* 'description'. There is no default. If exists, will be added to the database |
| 553 |
* along with the term. Expected to be a string. |
| 554 |
* |
| 555 |
* 'slug'. Expected to be a string. There is no default. |
| 556 |
* |
| 557 |
* @param int|string $term The status to add or update. |
| 558 |
* @param array|string $args Change the values of the inserted term. |
| 559 |
* @return array|WP_Error The Term ID and Term Taxonomy ID. |
| 560 |
*/ |
| 561 |
public function add_custom_status( $term, $args = [] ) { |
| 562 |
$slug = ( ! empty( $args['slug'] ) ) ? $args['slug'] : sanitize_title( $term ); |
| 563 |
unset( $args['slug'] ); |
| 564 |
$encoded_description = $this->get_encoded_description( $args ); |
| 565 |
$response = wp_insert_term( $term, self::taxonomy_key, [ |
| 566 |
'slug' => $slug, |
| 567 |
'description' => $encoded_description, |
| 568 |
] ); |
| 569 |
|
| 570 |
// Reset our internal object cache. |
| 571 |
$this->custom_statuses_cache = []; |
| 572 |
|
| 573 |
return $response; |
| 574 |
} |
| 575 |
|
| 576 |
/** |
| 577 |
* Update an existing custom status. |
| 578 |
* |
| 579 |
* @param int $status_id ID for the status. |
| 580 |
* @param array $args Any arguments to be updated. |
| 581 |
* @return object|WP_Error Newly updated status object. |
| 582 |
*/ |
| 583 |
public function update_custom_status( $status_id, $args = [] ) { |
| 584 |
global $edit_flow; |
| 585 |
|
| 586 |
$old_status = $this->get_custom_status_by( 'id', $status_id ); |
| 587 |
if ( ! $old_status || is_wp_error( $old_status ) ) { |
| 588 |
return new WP_Error( 'invalid', __( "Custom status doesn't exist.", 'edit-flow' ) ); |
| 589 |
} |
| 590 |
|
| 591 |
// Prevent user from changing draft name or slug. |
| 592 |
if ( 'draft' === $old_status->slug |
| 593 |
&& ( |
| 594 |
( isset( $args['name'] ) && $args['name'] !== $old_status->name ) |
| 595 |
|| |
| 596 |
( isset( $args['slug'] ) && $args['slug'] !== $old_status->slug ) |
| 597 |
) ) { |
| 598 |
return new WP_Error( 'invalid', __( 'Changing the name and slug of "Draft" is not allowed', 'edit-flow' ) ); |
| 599 |
} |
| 600 |
|
| 601 |
// If the name was changed, we need to change the slug. |
| 602 |
if ( isset( $args['name'] ) && $args['name'] != $old_status->name ) { |
| 603 |
$args['slug'] = sanitize_title( $args['name'] ); |
| 604 |
} |
| 605 |
|
| 606 |
// Reassign posts to new status slug if the slug changed and isn't restricted. |
| 607 |
if ( isset( $args['slug'] ) && $args['slug'] != $old_status->slug && ! $this->is_restricted_status( $old_status->slug ) ) { |
| 608 |
$new_status = $args['slug']; |
| 609 |
$this->reassign_post_status( $old_status->slug, $new_status ); |
| 610 |
|
| 611 |
$default_status = $this->get_default_custom_status()->slug; |
| 612 |
if ( $old_status->slug == $default_status ) { |
| 613 |
$edit_flow->update_module_option( $this->module->name, 'default_status', $new_status ); |
| 614 |
} |
| 615 |
} |
| 616 |
// We're encoding metadata that isn't supported by default in the term's description field. |
| 617 |
$args_to_encode = []; |
| 618 |
$args_to_encode['description'] = ( isset( $args['description'] ) ) ? $args['description'] : $old_status->description; |
| 619 |
$args_to_encode['position'] = ( isset( $args['position'] ) ) ? $args['position'] : $old_status->position; |
| 620 |
$encoded_description = $this->get_encoded_description( $args_to_encode ); |
| 621 |
$args['description'] = $encoded_description; |
| 622 |
|
| 623 |
$updated_status_array = wp_update_term( $status_id, self::taxonomy_key, $args ); |
| 624 |
|
| 625 |
// Reset our internal object cache after the update, not before. |
| 626 |
// This ensures get_custom_status_by() returns fresh data. |
| 627 |
$this->custom_statuses_cache = []; |
| 628 |
|
| 629 |
$updated_status = $this->get_custom_status_by( 'id', $updated_status_array['term_id'] ); |
| 630 |
|
| 631 |
return $updated_status; |
| 632 |
} |
| 633 |
|
| 634 |
/** |
| 635 |
* Deletes a custom status from the wp_terms table. |
| 636 |
* |
| 637 |
* Partly a wrapper for the wp_delete_term function. |
| 638 |
* BUT, also reassigns posts that currently have the deleted status assigned. |
| 639 |
* |
| 640 |
* @param int $status_id ID of the status to delete. |
| 641 |
* @param array $args Arguments for wp_delete_term. |
| 642 |
* @param string $reassign Status ID to reassign posts to. |
| 643 |
* @return bool|WP_Error True on success, WP_Error on failure. |
| 644 |
*/ |
| 645 |
public function delete_custom_status( $status_id, $args = [], $reassign = '' ) { |
| 646 |
global $edit_flow; |
| 647 |
// Reassign posts to alternate status. |
| 648 |
|
| 649 |
// Get slug for the old status. |
| 650 |
$old_status = $this->get_custom_status_by( 'id', $status_id )->slug; |
| 651 |
|
| 652 |
if ( $reassign == $old_status ) { |
| 653 |
return new WP_Error( 'invalid', __( 'Cannot reassign to the status you want to delete', 'edit-flow' ) ); |
| 654 |
} |
| 655 |
|
| 656 |
if ( ! $this->is_restricted_status( $old_status ) && 'draft' !== $old_status ) { |
| 657 |
$default_status = $this->get_default_custom_status()->slug; |
| 658 |
// If new status in $reassign, use that for all posts of the old_status. |
| 659 |
if ( ! empty( $reassign ) ) { |
| 660 |
$new_status = $this->get_custom_status_by( 'id', $reassign )->slug; |
| 661 |
} else { |
| 662 |
$new_status = $default_status; |
| 663 |
} |
| 664 |
// Deleting default status. |
| 665 |
if ( $old_status == $default_status && $this->get_custom_status_by( 'slug', 'draft' ) ) { |
| 666 |
$new_status = 'draft'; |
| 667 |
$edit_flow->update_module_option( $this->module->name, 'default_status', $new_status ); |
| 668 |
} |
| 669 |
|
| 670 |
$this->reassign_post_status( $old_status, $new_status ); |
| 671 |
|
| 672 |
$result = wp_delete_term( $status_id, self::taxonomy_key, $args ); |
| 673 |
|
| 674 |
// Reset our internal object cache after the delete, not before. |
| 675 |
// This ensures subsequent calls to get_custom_statuses() return fresh data. |
| 676 |
$this->custom_statuses_cache = []; |
| 677 |
|
| 678 |
return $result; |
| 679 |
} else { |
| 680 |
return new WP_Error( 'restricted', __( 'Restricted status ', 'edit-flow' ) . '(' . $this->get_custom_status_by( 'id', $status_id )->name . ')' ); |
| 681 |
} |
| 682 |
} |
| 683 |
|
| 684 |
/** |
| 685 |
* Get all custom statuses as an ordered array. |
| 686 |
* |
| 687 |
* @param array $args Optional arguments for filtering. |
| 688 |
* @return array All of the statuses. |
| 689 |
*/ |
| 690 |
public function get_custom_statuses( $args = [] ) { |
| 691 |
global $wp_post_statuses; |
| 692 |
|
| 693 |
if ( $this->disable_custom_statuses_for_post_type() ) { |
| 694 |
return $this->get_core_post_statuses(); |
| 695 |
} |
| 696 |
|
| 697 |
// Internal object cache for repeat requests. |
| 698 |
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- Used for cache key generation only. |
| 699 |
$arg_hash = md5( serialize( $args ) ); |
| 700 |
if ( ! empty( $this->custom_statuses_cache[ $arg_hash ] ) ) { |
| 701 |
return $this->custom_statuses_cache[ $arg_hash ]; |
| 702 |
} |
| 703 |
|
| 704 |
// Handle if the requested taxonomy doesn't exist. |
| 705 |
$statuses = get_terms( [ |
| 706 |
'taxonomy' => self::taxonomy_key, |
| 707 |
'hide_empty' => false, |
| 708 |
]); |
| 709 |
|
| 710 |
if ( is_wp_error( $statuses ) || empty( $statuses ) ) { |
| 711 |
$statuses = []; |
| 712 |
} |
| 713 |
|
| 714 |
// Expand and order the statuses. |
| 715 |
$ordered_statuses = []; |
| 716 |
$hold_to_end = []; |
| 717 |
foreach ( $statuses as $key => $status ) { |
| 718 |
// Unencode and set all of our pseudo term meta because we need the position if it exists. |
| 719 |
// Create a new object to avoid modifying the original term object from WordPress's cache. |
| 720 |
$unencoded_description = $this->get_unencoded_description( $status->description ); |
| 721 |
$defaults = [ |
| 722 |
'position' => false, |
| 723 |
]; |
| 724 |
$status = array_merge( $defaults, (array) $status ); |
| 725 |
if ( is_array( $unencoded_description ) ) { |
| 726 |
$status = array_merge( $status, $unencoded_description ); |
| 727 |
} |
| 728 |
$status = (object) $status; |
| 729 |
// Only add the status to the ordered array if it has a set position and doesn't conflict with another key. |
| 730 |
// Otherwise, hold it for later. |
| 731 |
if ( $status->position && ! array_key_exists( $status->position, $ordered_statuses ) ) { |
| 732 |
$ordered_statuses[ (int) $status->position ] = $status; |
| 733 |
} else { |
| 734 |
$hold_to_end[] = $status; |
| 735 |
} |
| 736 |
} |
| 737 |
// Sort the items numerically by key. |
| 738 |
ksort( $ordered_statuses, SORT_NUMERIC ); |
| 739 |
// Append all of the statuses that didn't have an existing position. |
| 740 |
foreach ( $hold_to_end as $unpositioned_status ) { |
| 741 |
$ordered_statuses[] = $unpositioned_status; |
| 742 |
} |
| 743 |
|
| 744 |
$this->custom_statuses_cache[ $arg_hash ] = $ordered_statuses; |
| 745 |
|
| 746 |
return $ordered_statuses; |
| 747 |
} |
| 748 |
|
| 749 |
/** |
| 750 |
* Returns the a single status object based on ID, title, or slug. |
| 751 |
* |
| 752 |
* @param string $field The field to search by ('id', 'slug', or 'name'). |
| 753 |
* @param string|int $value The value to search for. |
| 754 |
* @return object|false The object for the matching status. |
| 755 |
*/ |
| 756 |
public function get_custom_status_by( $field, $value ) { |
| 757 |
|
| 758 |
if ( ! in_array( $field, [ 'id', 'slug', 'name' ] ) ) { |
| 759 |
return false; |
| 760 |
} |
| 761 |
|
| 762 |
if ( 'id' == $field ) { |
| 763 |
$field = 'term_id'; |
| 764 |
} |
| 765 |
|
| 766 |
$custom_statuses = $this->get_custom_statuses(); |
| 767 |
$custom_status = wp_filter_object_list( $custom_statuses, [ $field => $value ] ); |
| 768 |
|
| 769 |
if ( ! empty( $custom_status ) ) { |
| 770 |
return array_shift( $custom_status ); |
| 771 |
} else { |
| 772 |
return false; |
| 773 |
} |
| 774 |
} |
| 775 |
|
| 776 |
/** |
| 777 |
* Get the term object for the default custom post status. |
| 778 |
* |
| 779 |
* @return object Default post status object. |
| 780 |
*/ |
| 781 |
public function get_default_custom_status() { |
| 782 |
$default_status = $this->get_custom_status_by( 'slug', $this->module->options->default_status ); |
| 783 |
if ( ! $default_status ) { |
| 784 |
$custom_statuses = $this->get_custom_statuses(); |
| 785 |
$default_status = array_shift( $custom_statuses ); |
| 786 |
} |
| 787 |
return $default_status; |
| 788 |
} |
| 789 |
|
| 790 |
/** |
| 791 |
* Assign new statuses to posts using value provided or the default. |
| 792 |
* |
| 793 |
* @param string $old_status Slug for the old status. |
| 794 |
* @param string $new_status Slug for the new status. |
| 795 |
*/ |
| 796 |
public function reassign_post_status( $old_status, $new_status = '' ) { |
| 797 |
global $wpdb; |
| 798 |
|
| 799 |
if ( empty( $new_status ) ) { |
| 800 |
$new_status = $this->get_default_custom_status()->slug; |
| 801 |
} |
| 802 |
|
| 803 |
// Make the database call. |
| 804 |
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Bulk status reassignment requires direct query. |
| 805 |
$result = $wpdb->update( $wpdb->posts, [ 'post_status' => $new_status ], [ 'post_status' => $old_status ], [ '%s' ] ); |
| 806 |
} |
| 807 |
|
| 808 |
/** |
| 809 |
* Get every status slug a post may validly be assigned: the core statuses plus all |
| 810 |
* registered custom statuses. Used to validate migration/reassignment targets. |
| 811 |
* |
| 812 |
* @return string[] Valid status slugs. |
| 813 |
*/ |
| 814 |
public function get_all_valid_statuses() { |
| 815 |
$core_statuses = [ 'publish', 'pending', 'draft', 'private', 'trash', 'future' ]; |
| 816 |
$custom_slugs = wp_list_pluck( $this->get_custom_statuses(), 'slug' ); |
| 817 |
|
| 818 |
return array_values( array_unique( array_merge( $core_statuses, $custom_slugs ) ) ); |
| 819 |
} |
| 820 |
|
| 821 |
/** |
| 822 |
* Display our custom post statuses in post listings when needed. |
| 823 |
* |
| 824 |
* @param array $post_states An array of post display states. |
| 825 |
* @param WP_Post $post The current post object. |
| 826 |
* |
| 827 |
* @return array Modified post states. |
| 828 |
*/ |
| 829 |
public function add_status_to_post_states( $post_states, $post ) { |
| 830 |
if ( ! in_array( $post->post_type, $this->get_post_types_for_module( $this->module ), true ) ) { |
| 831 |
// Return early if this post type doesn't support custom statuses. |
| 832 |
return $post_states; |
| 833 |
} |
| 834 |
|
| 835 |
$post_status = get_post_status_object( get_post_status( $post->ID ) ); |
| 836 |
|
| 837 |
if ( ! is_object( $post_status ) ) { |
| 838 |
return $post_states; |
| 839 |
} |
| 840 |
|
| 841 |
// phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce not needed for display filtering; value only used for comparison. |
| 842 |
$filtered_status = isset( $_REQUEST['post_status'] ) ? $_REQUEST['post_status'] : ''; |
| 843 |
if ( $filtered_status === $post_status->name ) { |
| 844 |
// No need to display the post status if a specific status was already requested. |
| 845 |
return $post_states; |
| 846 |
} |
| 847 |
|
| 848 |
$statuses_to_ignore = [ 'future', 'trash', 'publish' ]; |
| 849 |
if ( in_array( $post_status->name, $statuses_to_ignore, true ) ) { |
| 850 |
// Let WP core handle these more gracefully. |
| 851 |
return $post_states; |
| 852 |
} |
| 853 |
|
| 854 |
// Add the post status to display. Will also ensure the same status isn't shown twice. |
| 855 |
$post_states[ $post_status->name ] = $post_status->label; |
| 856 |
|
| 857 |
return $post_states; |
| 858 |
} |
| 859 |
|
| 860 |
/** |
| 861 |
* Determines whether the slug indicated belongs to a restricted status or not. |
| 862 |
* |
| 863 |
* @param string $slug Slug of the status. |
| 864 |
* @return bool True if restricted, false if not. |
| 865 |
*/ |
| 866 |
public function is_restricted_status( $slug ) { |
| 867 |
|
| 868 |
switch ( $slug ) { |
| 869 |
case 'publish': |
| 870 |
case 'private': |
| 871 |
case 'future': |
| 872 |
case 'new': |
| 873 |
case 'inherit': |
| 874 |
case 'auto-draft': |
| 875 |
case 'trash': |
| 876 |
$restricted = true; |
| 877 |
break; |
| 878 |
|
| 879 |
default: |
| 880 |
$restricted = false; |
| 881 |
break; |
| 882 |
} |
| 883 |
return $restricted; |
| 884 |
} |
| 885 |
|
| 886 |
/** |
| 887 |
* Handles a form's POST request to add a custom status. |
| 888 |
* |
| 889 |
* @since 0.7 |
| 890 |
*/ |
| 891 |
public function handle_add_custom_status() { |
| 892 |
// Check that the current POST request is our POST request. |
| 893 |
// phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce verified below. |
| 894 |
if ( ! isset( $_POST['submit'], $_GET['page'], $_POST['action'] ) |
| 895 |
|| $_GET['page'] != $this->module->settings_slug || 'add-new' != $_POST['action'] ) { |
| 896 |
return; |
| 897 |
} |
| 898 |
// phpcs:enable |
| 899 |
|
| 900 |
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce value passed directly to wp_verify_nonce(). |
| 901 |
if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'custom-status-add-nonce' ) ) { |
| 902 |
wp_die( esc_html( $this->module->messages['nonce-failed'] ) ); |
| 903 |
} |
| 904 |
|
| 905 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 906 |
wp_die( esc_html( $this->module->messages['invalid-permissions'] ) ); |
| 907 |
} |
| 908 |
|
| 909 |
// Validate and sanitize the form data. |
| 910 |
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized with sanitize_text_field(). |
| 911 |
$status_name = isset( $_POST['status_name'] ) ? sanitize_text_field( trim( $_POST['status_name'] ) ) : ''; |
| 912 |
$status_slug = sanitize_title( $status_name ); |
| 913 |
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized with wp_filter_nohtml_kses(). |
| 914 |
$status_description = isset( $_POST['status_description'] ) ? stripslashes( wp_filter_nohtml_kses( trim( $_POST['status_description'] ) ) ) : ''; |
| 915 |
|
| 916 |
/** |
| 917 |
* Form validation: |
| 918 |
* - Name is required and can't conflict with an existing name or slug. |
| 919 |
* - Description is optional. |
| 920 |
*/ |
| 921 |
EditFlow()->settings->form_errors = []; |
| 922 |
// Check if name field was filled in. |
| 923 |
if ( empty( $status_name ) ) { |
| 924 |
EditFlow()->settings->form_errors['name'] = __( 'Please enter a name for the status', 'edit-flow' ); |
| 925 |
} |
| 926 |
// Check that the name isn't numeric. |
| 927 |
if ( 0 != (int) $status_name ) { |
| 928 |
EditFlow()->settings->form_errors['name'] = __( 'Please enter a valid, non-numeric name for the status.', 'edit-flow' ); |
| 929 |
} |
| 930 |
// Check that the status name doesn't exceed 20 chars. |
| 931 |
if ( strlen( $status_name ) > 20 ) { |
| 932 |
EditFlow()->settings->form_errors['name'] = __( 'Status name cannot exceed 20 characters. Please try a shorter name.', 'edit-flow' ); |
| 933 |
} |
| 934 |
// Check to make sure the status doesn't already exist as another term. |
| 935 |
if ( term_exists( $status_slug, self::taxonomy_key ) ) { |
| 936 |
EditFlow()->settings->form_errors['name'] = __( 'Status name conflicts with existing term. Please choose another.', 'edit-flow' ); |
| 937 |
} |
| 938 |
// Check to make sure the name is not restricted. |
| 939 |
if ( $this->is_restricted_status( strtolower( $status_slug ) ) ) { |
| 940 |
EditFlow()->settings->form_errors['name'] = __( 'Status name is restricted. Please choose another name.', 'edit-flow' ); |
| 941 |
} |
| 942 |
|
| 943 |
// If there were any form errors, kick out and return them. |
| 944 |
if ( count( EditFlow()->settings->form_errors ) ) { |
| 945 |
$_REQUEST['error'] = 'form-error'; |
| 946 |
return; |
| 947 |
} |
| 948 |
|
| 949 |
// Try to add the status. |
| 950 |
$status_args = [ |
| 951 |
'description' => $status_description, |
| 952 |
'slug' => $status_slug, |
| 953 |
]; |
| 954 |
$return = $this->add_custom_status( $status_name, $status_args ); |
| 955 |
if ( is_wp_error( $return ) ) { |
| 956 |
/* translators: %s: error message */ |
| 957 |
wp_die( esc_html( sprintf( __( 'Could not add status: %s', 'edit-flow' ), $return->get_error_message() ) ) ); |
| 958 |
} |
| 959 |
// Redirect if successful. |
| 960 |
$redirect_url = $this->get_link( [ 'message' => 'status-added' ] ); |
| 961 |
// phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- Admin page redirect with controlled URL. |
| 962 |
wp_redirect( $redirect_url ); |
| 963 |
exit; |
| 964 |
} |
| 965 |
|
| 966 |
/** |
| 967 |
* Handles a POST request to edit a custom status. |
| 968 |
* |
| 969 |
* @since 0.7 |
| 970 |
*/ |
| 971 |
public function handle_edit_custom_status() { |
| 972 |
// phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce verified below. |
| 973 |
if ( ! isset( $_POST['submit'], $_GET['page'], $_GET['action'], $_GET['term-id'] ) |
| 974 |
|| $_GET['page'] != $this->module->settings_slug || 'edit-status' != $_GET['action'] ) { |
| 975 |
return; |
| 976 |
} |
| 977 |
// phpcs:enable |
| 978 |
|
| 979 |
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce value passed directly to wp_verify_nonce(). |
| 980 |
if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'edit-status' ) ) { |
| 981 |
wp_die( esc_html( $this->module->messages['nonce-failed'] ) ); |
| 982 |
} |
| 983 |
|
| 984 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 985 |
wp_die( esc_html( $this->module->messages['invalid-permissions'] ) ); |
| 986 |
} |
| 987 |
|
| 988 |
$existing_status = $this->get_custom_status_by( 'id', (int) $_GET['term-id'] ); |
| 989 |
if ( ! $existing_status ) { |
| 990 |
wp_die( esc_html( $this->module->messages['status-missing'] ) ); |
| 991 |
} |
| 992 |
|
| 993 |
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized with sanitize_text_field(). |
| 994 |
$name = isset( $_POST['name'] ) ? sanitize_text_field( trim( $_POST['name'] ) ) : ''; |
| 995 |
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized with wp_filter_nohtml_kses(). |
| 996 |
$description = isset( $_POST['description'] ) ? stripslashes( wp_filter_nohtml_kses( trim( $_POST['description'] ) ) ) : ''; |
| 997 |
|
| 998 |
/** |
| 999 |
* Form validation for editing custom status. |
| 1000 |
* |
| 1001 |
* Details: |
| 1002 |
* - 'name' is a required field and can't conflict with existing name or slug. |
| 1003 |
* - 'description' is optional. |
| 1004 |
*/ |
| 1005 |
EditFlow()->settings->form_errors = []; |
| 1006 |
// Check if name field was filled in. |
| 1007 |
if ( empty( $name ) ) { |
| 1008 |
EditFlow()->settings->form_errors['name'] = __( 'Please enter a name for the status', 'edit-flow' ); |
| 1009 |
} |
| 1010 |
// Check that the name isn't numeric. |
| 1011 |
if ( is_numeric( $name ) ) { |
| 1012 |
EditFlow()->settings->form_errors['name'] = __( 'Please enter a valid, non-numeric name for the status.', 'edit-flow' ); |
| 1013 |
} |
| 1014 |
// Check that the status name doesn't exceed 20 chars. |
| 1015 |
if ( strlen( $name ) > 20 ) { |
| 1016 |
EditFlow()->settings->form_errors['name'] = __( 'Status name cannot exceed 20 characters. Please try a shorter name.', 'edit-flow' ); |
| 1017 |
} |
| 1018 |
// Check to make sure the status doesn't already exist as another term. |
| 1019 |
$term_exists = term_exists( sanitize_title( $name ), self::taxonomy_key ); |
| 1020 |
if ( $term_exists && isset( $term_exists['term_id'] ) && $term_exists['term_id'] != $existing_status->term_id ) { |
| 1021 |
EditFlow()->settings->form_errors['name'] = __( 'Status name conflicts with existing term. Please choose another.', 'edit-flow' ); |
| 1022 |
} |
| 1023 |
// Check to make sure the status doesn't already exist. |
| 1024 |
$search_status = $this->get_custom_status_by( 'slug', sanitize_title( $name ) ); |
| 1025 |
if ( $search_status && $search_status->term_id != $existing_status->term_id ) { |
| 1026 |
EditFlow()->settings->form_errors['name'] = __( 'Status name conflicts with existing status. Please choose another.', 'edit-flow' ); |
| 1027 |
} |
| 1028 |
// Check to make sure the name is not restricted. |
| 1029 |
if ( $this->is_restricted_status( strtolower( sanitize_title( $name ) ) ) ) { |
| 1030 |
EditFlow()->settings->form_errors['name'] = __( 'Status name is restricted. Please choose another name.', 'edit-flow' ); |
| 1031 |
} |
| 1032 |
|
| 1033 |
// Kick out if there are any errors. |
| 1034 |
if ( count( EditFlow()->settings->form_errors ) ) { |
| 1035 |
$_REQUEST['error'] = 'form-error'; |
| 1036 |
return; |
| 1037 |
} |
| 1038 |
|
| 1039 |
// Try to add the new post status. |
| 1040 |
$args = [ |
| 1041 |
'name' => $name, |
| 1042 |
'slug' => sanitize_title( $name ), |
| 1043 |
'description' => $description, |
| 1044 |
]; |
| 1045 |
$return = $this->update_custom_status( $existing_status->term_id, $args ); |
| 1046 |
if ( is_wp_error( $return ) ) { |
| 1047 |
wp_die( esc_html__( 'Error updating post status.', 'edit-flow' ) ); |
| 1048 |
} |
| 1049 |
|
| 1050 |
$redirect_url = $this->get_link( [ 'message' => 'status-updated' ] ); |
| 1051 |
// phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- Admin page redirect with controlled URL. |
| 1052 |
wp_redirect( $redirect_url ); |
| 1053 |
exit; |
| 1054 |
} |
| 1055 |
|
| 1056 |
/** |
| 1057 |
* Handles a GET request to make the identified status default. |
| 1058 |
* |
| 1059 |
* @since 0.7 |
| 1060 |
*/ |
| 1061 |
public function handle_make_default_custom_status() { |
| 1062 |
global $edit_flow; |
| 1063 |
|
| 1064 |
// Check that the current GET request is our GET request. |
| 1065 |
// phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce verified below. |
| 1066 |
if ( ! isset( $_GET['page'], $_GET['action'], $_GET['term-id'], $_GET['nonce'] ) |
| 1067 |
|| $_GET['page'] != $this->module->settings_slug || 'make-default' != $_GET['action'] ) { |
| 1068 |
return; |
| 1069 |
} |
| 1070 |
// phpcs:enable |
| 1071 |
|
| 1072 |
// Check for proper nonce. |
| 1073 |
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce value passed directly to wp_verify_nonce(). |
| 1074 |
if ( ! isset( $_GET['nonce'] ) || ! wp_verify_nonce( $_GET['nonce'], 'make-default' ) ) { |
| 1075 |
wp_die( esc_html__( 'Invalid nonce for submission.', 'edit-flow' ) ); |
| 1076 |
} |
| 1077 |
|
| 1078 |
// Only allow users with the proper caps. |
| 1079 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 1080 |
wp_die( esc_html__( 'Sorry, you do not have permission to edit custom statuses.', 'edit-flow' ) ); |
| 1081 |
} |
| 1082 |
|
| 1083 |
$term_id = (int) $_GET['term-id']; |
| 1084 |
$term = $this->get_custom_status_by( 'id', $term_id ); |
| 1085 |
if ( is_object( $term ) ) { |
| 1086 |
$edit_flow->update_module_option( $this->module->name, 'default_status', $term->slug ); |
| 1087 |
// @todo How do we want to handle users who click the link from "Add New Status"? |
| 1088 |
$redirect_url = $this->get_link( [ 'message' => 'default-status-changed' ] ); |
| 1089 |
// phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- Admin page redirect with controlled URL. |
| 1090 |
wp_redirect( $redirect_url ); |
| 1091 |
exit; |
| 1092 |
} else { |
| 1093 |
wp_die( esc_html__( 'Status doesn't exist.', 'edit-flow' ) ); |
| 1094 |
} |
| 1095 |
} |
| 1096 |
|
| 1097 |
/** |
| 1098 |
* Handles a GET request to delete a specific term. |
| 1099 |
* |
| 1100 |
* @since 0.7 |
| 1101 |
*/ |
| 1102 |
public function handle_delete_custom_status() { |
| 1103 |
// Check that this GET request is our GET request. |
| 1104 |
// phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce verified below. |
| 1105 |
if ( ! isset( $_GET['page'], $_GET['action'], $_GET['term-id'], $_GET['nonce'] ) |
| 1106 |
|| $_GET['page'] != $this->module->settings_slug || 'delete-status' != $_GET['action'] ) { |
| 1107 |
return; |
| 1108 |
} |
| 1109 |
// phpcs:enable |
| 1110 |
|
| 1111 |
// Check for proper nonce. |
| 1112 |
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce value passed directly to wp_verify_nonce(). |
| 1113 |
if ( ! isset( $_GET['nonce'] ) || ! wp_verify_nonce( $_GET['nonce'], 'delete-status' ) ) { |
| 1114 |
wp_die( esc_html__( 'Invalid nonce for submission.', 'edit-flow' ) ); |
| 1115 |
} |
| 1116 |
|
| 1117 |
// Only allow users with the proper caps. |
| 1118 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 1119 |
wp_die( esc_html__( 'Sorry, you do not have permission to edit custom statuses.', 'edit-flow' ) ); |
| 1120 |
} |
| 1121 |
|
| 1122 |
// Check to make sure the status isn't already deleted. |
| 1123 |
$term_id = (int) $_GET['term-id']; |
| 1124 |
$term = $this->get_custom_status_by( 'id', $term_id ); |
| 1125 |
if ( ! $term ) { |
| 1126 |
wp_die( esc_html__( 'Status does not exist.', 'edit-flow' ) ); |
| 1127 |
} |
| 1128 |
|
| 1129 |
// Don't allow deletion of default status. |
| 1130 |
if ( $term->slug == $this->get_default_custom_status()->slug ) { |
| 1131 |
wp_die( esc_html__( 'Cannot delete default status.', 'edit-flow' ) ); |
| 1132 |
} |
| 1133 |
|
| 1134 |
$return = $this->delete_custom_status( $term_id ); |
| 1135 |
if ( is_wp_error( $return ) ) { |
| 1136 |
wp_die( esc_html( __( 'Could not delete the status: ', 'edit-flow' ) . $return->get_error_message() ) ); |
| 1137 |
} |
| 1138 |
|
| 1139 |
$redirect_url = $this->get_link( [ 'message' => 'status-deleted' ] ); |
| 1140 |
// phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- Admin page redirect with controlled URL. |
| 1141 |
wp_redirect( $redirect_url ); |
| 1142 |
exit; |
| 1143 |
} |
| 1144 |
|
| 1145 |
/** |
| 1146 |
* Handle a POST request to migrate posts between statuses. |
| 1147 |
* |
| 1148 |
* @since 0.9.10 |
| 1149 |
*/ |
| 1150 |
public function handle_migrate_status() { |
| 1151 |
// Check that this is our POST request. |
| 1152 |
if ( ! isset( $_POST['action'] ) || 'migrate' !== $_POST['action'] ) { |
| 1153 |
return; |
| 1154 |
} |
| 1155 |
|
| 1156 |
// Verify the page. |
| 1157 |
if ( ! isset( $_GET['page'] ) || $_GET['page'] !== $this->module->settings_slug ) { |
| 1158 |
return; |
| 1159 |
} |
| 1160 |
|
| 1161 |
// Check for proper nonce. |
| 1162 |
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce value passed directly to wp_verify_nonce(). |
| 1163 |
if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'custom-status-migrate-nonce' ) ) { |
| 1164 |
wp_die( esc_html__( 'Invalid nonce for submission.', 'edit-flow' ) ); |
| 1165 |
} |
| 1166 |
|
| 1167 |
// Only allow users with the proper caps. |
| 1168 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 1169 |
wp_die( esc_html__( 'Sorry, you do not have permission to migrate posts.', 'edit-flow' ) ); |
| 1170 |
} |
| 1171 |
|
| 1172 |
$from_status = isset( $_POST['migrate_from'] ) ? sanitize_key( $_POST['migrate_from'] ) : ''; |
| 1173 |
$to_status = isset( $_POST['migrate_to'] ) ? sanitize_key( $_POST['migrate_to'] ) : ''; |
| 1174 |
|
| 1175 |
// Validate inputs. |
| 1176 |
if ( empty( $from_status ) || empty( $to_status ) ) { |
| 1177 |
wp_die( esc_html__( 'Please select both a source and target status.', 'edit-flow' ) ); |
| 1178 |
} |
| 1179 |
|
| 1180 |
if ( $from_status === $to_status ) { |
| 1181 |
wp_die( esc_html__( 'Source and target status cannot be the same.', 'edit-flow' ) ); |
| 1182 |
} |
| 1183 |
|
| 1184 |
// The target must be a real status, otherwise posts would be stranded in a status that does not exist. |
| 1185 |
if ( ! in_array( $to_status, $this->get_all_valid_statuses(), true ) ) { |
| 1186 |
wp_die( esc_html__( 'Please select a valid target status.', 'edit-flow' ) ); |
| 1187 |
} |
| 1188 |
|
| 1189 |
// Perform the migration. |
| 1190 |
$this->reassign_post_status( $from_status, $to_status ); |
| 1191 |
|
| 1192 |
// Clear caches. |
| 1193 |
wp_cache_flush(); |
| 1194 |
|
| 1195 |
$redirect_url = $this->get_link( |
| 1196 |
[ |
| 1197 |
'action' => 'migrate-status', |
| 1198 |
'message' => 'status-migrated', |
| 1199 |
] |
| 1200 |
); |
| 1201 |
// phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- Admin page redirect with controlled URL. |
| 1202 |
wp_redirect( $redirect_url ); |
| 1203 |
exit; |
| 1204 |
} |
| 1205 |
|
| 1206 |
/** |
| 1207 |
* Get count of posts with a specific status. |
| 1208 |
* |
| 1209 |
* @since 0.9.10 |
| 1210 |
* |
| 1211 |
* @param string $status The status slug. |
| 1212 |
* @return int The number of posts with this status. |
| 1213 |
*/ |
| 1214 |
public function get_post_count_for_status( $status ) { |
| 1215 |
global $wpdb; |
| 1216 |
|
| 1217 |
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Used for status migration count check. |
| 1218 |
return (int) $wpdb->get_var( |
| 1219 |
$wpdb->prepare( |
| 1220 |
"SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_status = %s", |
| 1221 |
$status |
| 1222 |
) |
| 1223 |
); |
| 1224 |
} |
| 1225 |
|
| 1226 |
/** |
| 1227 |
* Generate a link to one of the custom status actions. |
| 1228 |
* |
| 1229 |
* @since 0.7 |
| 1230 |
* |
| 1231 |
* @param array $args Optional. Action and any query args to add to the URL. |
| 1232 |
* @return string Direct link to complete the action. |
| 1233 |
*/ |
| 1234 |
public function get_link( $args = [] ) { |
| 1235 |
if ( ! isset( $args['action'] ) ) { |
| 1236 |
$args['action'] = ''; |
| 1237 |
} |
| 1238 |
if ( ! isset( $args['page'] ) ) { |
| 1239 |
$args['page'] = $this->module->settings_slug; |
| 1240 |
} |
| 1241 |
// Add other things we may need depending on the action. |
| 1242 |
switch ( $args['action'] ) { |
| 1243 |
case 'make-default': |
| 1244 |
case 'delete-status': |
| 1245 |
$args['nonce'] = wp_create_nonce( $args['action'] ); |
| 1246 |
break; |
| 1247 |
default: |
| 1248 |
break; |
| 1249 |
} |
| 1250 |
return add_query_arg( $args, get_admin_url( null, 'admin.php' ) ); |
| 1251 |
} |
| 1252 |
|
| 1253 |
/** |
| 1254 |
* Handle an ajax request to update the order of custom statuses. |
| 1255 |
* |
| 1256 |
* @since 0.7 |
| 1257 |
*/ |
| 1258 |
public function handle_ajax_update_status_positions() { |
| 1259 |
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce value passed directly to wp_verify_nonce(). |
| 1260 |
if ( ! isset( $_POST['custom_status_sortable_nonce'] ) || ! wp_verify_nonce( $_POST['custom_status_sortable_nonce'], 'custom-status-sortable' ) ) { |
| 1261 |
$this->print_ajax_response( 'error', esc_html( $this->module->messages['nonce-failed'] ) ); |
| 1262 |
return; |
| 1263 |
} |
| 1264 |
|
| 1265 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 1266 |
$this->print_ajax_response( 'error', esc_html( $this->module->messages['invalid-permissions'] ) ); |
| 1267 |
return; |
| 1268 |
} |
| 1269 |
|
| 1270 |
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Array is sanitized when processing each item. |
| 1271 |
if ( ! isset( $_POST['status_positions'] ) || ! is_array( $_POST['status_positions'] ) ) { |
| 1272 |
$this->print_ajax_response( 'error', esc_html__( 'Terms not set.', 'edit-flow' ) ); |
| 1273 |
return; |
| 1274 |
} |
| 1275 |
|
| 1276 |
// Update each custom status with its new position. |
| 1277 |
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Values sanitized with absint() below. |
| 1278 |
foreach ( $_POST['status_positions'] as $position => $term_id ) { |
| 1279 |
// Have to add 1 to the position because the index started with zero. |
| 1280 |
$args = [ |
| 1281 |
'position' => (int) $position + 1, |
| 1282 |
]; |
| 1283 |
$return = $this->update_custom_status( (int) $term_id, $args ); |
| 1284 |
// @todo check that this was a valid return |
| 1285 |
} |
| 1286 |
$this->print_ajax_response( 'success', $this->module->messages['status-position-updated'] ); |
| 1287 |
} |
| 1288 |
|
| 1289 |
/** |
| 1290 |
* Handle an Inline Edit POST request to update status values. |
| 1291 |
* |
| 1292 |
* @since 0.7 |
| 1293 |
*/ |
| 1294 |
public function ajax_inline_save_status() { |
| 1295 |
global $edit_flow; |
| 1296 |
|
| 1297 |
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce value passed directly to wp_verify_nonce(). |
| 1298 |
if ( ! isset( $_POST['inline_edit'] ) || ! wp_verify_nonce( $_POST['inline_edit'], 'custom-status-inline-edit-nonce' ) ) { |
| 1299 |
wp_die( esc_html( $this->module->messages['nonce-failed'] ) ); |
| 1300 |
} |
| 1301 |
|
| 1302 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 1303 |
wp_die( esc_html( $this->module->messages['invalid-permissions'] ) ); |
| 1304 |
} |
| 1305 |
|
| 1306 |
$term_id = isset( $_POST['status_id'] ) ? (int) $_POST['status_id'] : 0; |
| 1307 |
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized with sanitize_text_field(), sanitize_title(), and wp_filter_nohtml_kses(). |
| 1308 |
$status_name = isset( $_POST['name'] ) ? sanitize_text_field( trim( $_POST['name'] ) ) : ''; |
| 1309 |
$status_slug = isset( $_POST['name'] ) ? sanitize_title( trim( $_POST['name'] ) ) : ''; |
| 1310 |
$status_description = isset( $_POST['description'] ) ? stripslashes( wp_filter_nohtml_kses( trim( $_POST['description'] ) ) ) : ''; |
| 1311 |
// phpcs:enable |
| 1312 |
|
| 1313 |
// Check if name field was filled in. |
| 1314 |
if ( empty( $status_name ) ) { |
| 1315 |
$change_error = new WP_Error( 'invalid', esc_html__( 'Please enter a name for the status.', 'edit-flow' ) ); |
| 1316 |
wp_die( esc_html( $change_error->get_error_message() ) ); |
| 1317 |
} |
| 1318 |
|
| 1319 |
// Check that the name isn't numeric. |
| 1320 |
if ( is_numeric( $status_name ) ) { |
| 1321 |
$change_error = new WP_Error( 'invalid', esc_html__( 'Please enter a valid, non-numeric name for the status.', 'edit-flow' ) ); |
| 1322 |
wp_die( esc_html( $change_error->get_error_message() ) ); |
| 1323 |
} |
| 1324 |
|
| 1325 |
// Check that the status name doesn't exceed 20 chars. |
| 1326 |
if ( strlen( $status_name ) > 20 ) { |
| 1327 |
$change_error = new WP_Error( 'invalid', esc_html__( 'Status name cannot exceed 20 characters. Please try a shorter name.', 'edit-flow' ) ); |
| 1328 |
wp_die( esc_html( $change_error->get_error_message() ) ); |
| 1329 |
} |
| 1330 |
|
| 1331 |
// Check to make sure the name is not restricted. |
| 1332 |
if ( $edit_flow->custom_status->is_restricted_status( strtolower( $status_name ) ) ) { |
| 1333 |
$change_error = new WP_Error( 'invalid', esc_html__( 'Status name is restricted. Please chose another name.', 'edit-flow' ) ); |
| 1334 |
wp_die( esc_html( $change_error->get_error_message() ) ); |
| 1335 |
} |
| 1336 |
|
| 1337 |
// Check to make sure the status doesn't already exist. |
| 1338 |
if ( $this->get_custom_status_by( 'slug', $status_slug ) && ( $this->get_custom_status_by( 'id', $term_id )->slug != $status_slug ) ) { |
| 1339 |
$change_error = new WP_Error( 'invalid', esc_html__( 'Status already exists. Please choose another name.', 'edit-flow' ) ); |
| 1340 |
wp_die( esc_html( $change_error->get_error_message() ) ); |
| 1341 |
} |
| 1342 |
|
| 1343 |
// Check to make sure the status doesn't already exist as another term. |
| 1344 |
$term_exists = term_exists( sanitize_title( $status_name ), self::taxonomy_key ); |
| 1345 |
if ( $term_exists && isset( $term_exists['term_id'] ) && $term_exists['term_id'] != $term_id ) { |
| 1346 |
$change_error = new WP_Error( 'invalid', esc_html__( 'Status name conflicts with existing term. Please choose another.', 'edit-flow' ) ); |
| 1347 |
wp_die( esc_html( $change_error->get_error_message() ) ); |
| 1348 |
} |
| 1349 |
|
| 1350 |
// Get status_name & status_description. |
| 1351 |
$args = [ |
| 1352 |
'name' => $status_name, |
| 1353 |
'description' => $status_description, |
| 1354 |
'slug' => $status_slug, |
| 1355 |
]; |
| 1356 |
$return = $this->update_custom_status( $term_id, $args ); |
| 1357 |
if ( ! is_wp_error( $return ) ) { |
| 1358 |
set_current_screen( 'edit-custom-status' ); |
| 1359 |
$wp_list_table = new EF_Custom_Status_List_Table(); |
| 1360 |
$wp_list_table->prepare_items(); |
| 1361 |
// single_row() echoes its own output; column_* callbacks are |
| 1362 |
// responsible for escaping, as per WP_List_Table's contract. |
| 1363 |
$wp_list_table->single_row( $return ); |
| 1364 |
wp_die(); |
| 1365 |
} else { |
| 1366 |
/* translators: 1: the status's name */ |
| 1367 |
$change_error = new WP_Error( 'invalid', sprintf( __( 'Could not update the status: <strong>%s</strong>', 'edit-flow' ), esc_html( $status_name ) ) ); |
| 1368 |
wp_die( wp_kses( $change_error->get_error_message(), array( 'strong' => array() ) ) ); |
| 1369 |
} |
| 1370 |
} |
| 1371 |
|
| 1372 |
/** |
| 1373 |
* Register settings for notifications so we can partially use the Settings API. |
| 1374 |
* |
| 1375 |
* We use the Settings API for form generation, but not saving. |
| 1376 |
* |
| 1377 |
* @since 0.7 |
| 1378 |
*/ |
| 1379 |
public function register_settings() { |
| 1380 |
add_settings_section( $this->module->options_group_name . '_general', false, '__return_false', $this->module->options_group_name ); |
| 1381 |
add_settings_field( 'post_types', __( 'Use on these post types:', 'edit-flow' ), [ $this, 'settings_post_types_option' ], $this->module->options_group_name, $this->module->options_group_name . '_general' ); |
| 1382 |
add_settings_field( 'always_show_dropdown', __( 'Always show dropdown:', 'edit-flow' ), [ $this, 'settings_always_show_dropdown_option' ], $this->module->options_group_name, $this->module->options_group_name . '_general' ); |
| 1383 |
} |
| 1384 |
|
| 1385 |
/** |
| 1386 |
* Choose the post types that should be displayed on the calendar. |
| 1387 |
* |
| 1388 |
* @since 0.7 |
| 1389 |
*/ |
| 1390 |
public function settings_post_types_option() { |
| 1391 |
global $edit_flow; |
| 1392 |
$edit_flow->settings->helper_option_custom_post_type( $this->module ); |
| 1393 |
} |
| 1394 |
|
| 1395 |
/** |
| 1396 |
* Option for whether the status dropdown should always be shown. |
| 1397 |
* |
| 1398 |
* @since 0.7 |
| 1399 |
*/ |
| 1400 |
public function settings_always_show_dropdown_option() { |
| 1401 |
$options = [ |
| 1402 |
'off' => __( 'Disabled', 'edit-flow' ), |
| 1403 |
'on' => __( 'Enabled', 'edit-flow' ), |
| 1404 |
]; |
| 1405 |
echo '<select id="always_show_dropdown" name="' . esc_attr( $this->module->options_group_name ) . '[always_show_dropdown]">'; |
| 1406 |
foreach ( $options as $value => $label ) { |
| 1407 |
echo '<option value="' . esc_attr( $value ) . '"'; |
| 1408 |
echo selected( $this->module->options->always_show_dropdown, $value ); |
| 1409 |
echo '>' . esc_html( $label ) . '</option>'; |
| 1410 |
} |
| 1411 |
echo '</select>'; |
| 1412 |
} |
| 1413 |
|
| 1414 |
/** |
| 1415 |
* Validate input from the end user. |
| 1416 |
* |
| 1417 |
* @since 0.7 |
| 1418 |
* |
| 1419 |
* @param array $new_options The new options to validate. |
| 1420 |
* @return array The validated options. |
| 1421 |
*/ |
| 1422 |
public function settings_validate( $new_options ) { |
| 1423 |
// Whitelist validation for the post type options. |
| 1424 |
if ( ! isset( $new_options['post_types'] ) ) { |
| 1425 |
$new_options['post_types'] = []; |
| 1426 |
} |
| 1427 |
$new_options['post_types'] = $this->clean_post_type_options( $new_options['post_types'], $this->module->post_type_support ); |
| 1428 |
|
| 1429 |
// Whitelist validation for the 'always_show_dropdown' options. |
| 1430 |
if ( ! isset( $new_options['always_show_dropdown'] ) || 'on' != $new_options['always_show_dropdown'] ) { |
| 1431 |
$new_options['always_show_dropdown'] = 'off'; |
| 1432 |
} |
| 1433 |
|
| 1434 |
return $new_options; |
| 1435 |
} |
| 1436 |
|
| 1437 |
// phpcs:disable:WordPress.Security.NonceVerification.Missing -- Disabling nonce verification because that is not available here, it's just rendering it. The actual save is done in helper_settings_validate_and_save and that's guarded well. |
| 1438 |
|
| 1439 |
/** |
| 1440 |
* Primary configuration page for custom status class. |
| 1441 |
* Shows form to add new custom statuses on the left and a |
| 1442 |
* WP_List_Table with the custom status terms on the right |
| 1443 |
*/ |
| 1444 |
public function print_configure_view() { |
| 1445 |
// phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- No verification required for unprivileged URL check. |
| 1446 |
$action = isset( $_GET['action'] ) && in_array( $_GET['action'], [ 'edit-status', 'change-options', 'migrate-status' ] ) ? $_GET['action'] : ''; |
| 1447 |
|
| 1448 |
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- No verification required for unprivileged URL check. |
| 1449 |
$term_id = isset( $_GET['term-id'] ) ? absint( $_GET['term-id'] ) : false; |
| 1450 |
|
| 1451 |
if ( $term_id && 'edit-status' === $action ) { |
| 1452 |
// Check whether the term exists. |
| 1453 |
$custom_status = $this->get_custom_status_by( 'id', $term_id ); |
| 1454 |
|
| 1455 |
if ( ! $custom_status ) { |
| 1456 |
printf( '<div class="error"><p>%s</p></div>', esc_html( $this->module->messages['status-missing'] ) ); |
| 1457 |
return; |
| 1458 |
} |
| 1459 |
|
| 1460 |
$edit_status_link = $this->get_link( [ |
| 1461 |
'action' => 'edit-status', |
| 1462 |
'term-id' => $term_id, |
| 1463 |
] ); |
| 1464 |
|
| 1465 |
$name = $custom_status->name; |
| 1466 |
$description = $custom_status->description; |
| 1467 |
|
| 1468 |
$is_nonce_valid = isset( $_POST['_wpnonce'] ) && wp_verify_nonce( wp_strip_all_tags( $_POST['_wpnonce'] ), 'edit-status' ); |
| 1469 |
|
| 1470 |
if ( $is_nonce_valid ) { |
| 1471 |
$name = ( isset( $_POST['name'] ) ) ? wp_strip_all_tags( $_POST['name'] ) : $custom_status->name; |
| 1472 |
$description = ( isset( $_POST['description'] ) ) ? wp_strip_all_tags( $_POST['description'] ) : $custom_status->description; |
| 1473 |
} |
| 1474 |
|
| 1475 |
include_once __DIR__ . '/views/edit-status.php'; |
| 1476 |
} else { |
| 1477 |
$custom_status_list_table = new EF_Custom_Status_List_Table(); |
| 1478 |
$custom_status_list_table->prepare_items(); |
| 1479 |
include_once __DIR__ . '/views/configure.php'; |
| 1480 |
} |
| 1481 |
} |
| 1482 |
|
| 1483 |
/** |
| 1484 |
* This is a hack! hack! hack! until core is fixed/better supports custom statuses. |
| 1485 |
* |
| 1486 |
* When publishing a post with a custom status, set the status to 'pending' temporarily. |
| 1487 |
* |
| 1488 |
* @see Works around this limitation: http://core.trac.wordpress.org/browser/tags/3.2.1/wp-includes/post.php#L2694 |
| 1489 |
* @see Original thread: http://wordpress.org/support/topic/plugin-edit-flow-custom-statuses-create-timestamp-problem |
| 1490 |
* @see Core ticket: http://core.trac.wordpress.org/ticket/18362 |
| 1491 |
*/ |
| 1492 |
public function check_timestamp_on_publish() { |
| 1493 |
global $edit_flow, $pagenow, $wpdb; |
| 1494 |
|
| 1495 |
if ( $this->disable_custom_statuses_for_post_type() ) { |
| 1496 |
return; |
| 1497 |
} |
| 1498 |
|
| 1499 |
// Handles the transition to 'publish' on edit.php (bulk edit). |
| 1500 |
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- The bulk-edit nonce is verified below before any write. |
| 1501 |
if ( isset( $edit_flow ) && 'edit.php' === $pagenow && isset( $_REQUEST['bulk_edit'] ) ) { |
| 1502 |
// Verify the bulk-edit nonce before touching any posts. |
| 1503 |
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce value passed directly to wp_verify_nonce(). |
| 1504 |
if ( ! isset( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'bulk-posts' ) ) { |
| 1505 |
return; |
| 1506 |
} |
| 1507 |
|
| 1508 |
// For every post_id, set the post_status as 'pending' only when there's no timestamp set for $post_date_gmt. |
| 1509 |
if ( isset( $_REQUEST['post'] ) && isset( $_REQUEST['_status'] ) && 'publish' == $_REQUEST['_status'] ) { |
| 1510 |
$post_ids = array_map( 'intval', (array) $_REQUEST['post'] ); |
| 1511 |
foreach ( $post_ids as $post_id ) { |
| 1512 |
// Only act on posts the current user is allowed to edit. |
| 1513 |
if ( ! current_user_can( 'edit_post', $post_id ) ) { |
| 1514 |
continue; |
| 1515 |
} |
| 1516 |
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Core workaround for custom status timestamp. |
| 1517 |
$wpdb->update( $wpdb->posts, [ 'post_status' => 'pending' ], [ |
| 1518 |
'ID' => $post_id, |
| 1519 |
'post_date_gmt' => '0000-00-00 00:00:00', |
| 1520 |
] ); |
| 1521 |
clean_post_cache( $post_id ); |
| 1522 |
} |
| 1523 |
} |
| 1524 |
} |
| 1525 |
// phpcs:enable |
| 1526 |
|
| 1527 |
// Handles the transition to 'publish' on post.php. |
| 1528 |
// phpcs:disable WordPress.Security.NonceVerification.Missing -- The post-edit nonce is verified below before any write. |
| 1529 |
if ( isset( $edit_flow ) && 'post.php' == $pagenow && isset( $_POST['publish'] ) && isset( $_POST['post_ID'] ) ) { |
| 1530 |
$post_id = (int) $_POST['post_ID']; |
| 1531 |
|
| 1532 |
// Verify the post-edit nonce and capability before touching the post. |
| 1533 |
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce value passed directly to wp_verify_nonce(). |
| 1534 |
if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'update-post_' . $post_id ) || ! current_user_can( 'edit_post', $post_id ) ) { |
| 1535 |
return; |
| 1536 |
} |
| 1537 |
|
| 1538 |
// Set the post_status as 'pending' only when there's no timestamp set for $post_date_gmt. |
| 1539 |
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Core workaround for custom status timestamp. |
| 1540 |
$ret = $wpdb->update( $wpdb->posts, [ 'post_status' => 'pending' ], [ |
| 1541 |
'ID' => $post_id, |
| 1542 |
'post_date_gmt' => '0000-00-00 00:00:00', |
| 1543 |
] ); |
| 1544 |
clean_post_cache( $post_id ); |
| 1545 |
foreach ( [ 'aa', 'mm', 'jj', 'hh', 'mn' ] as $timeunit ) { |
| 1546 |
if ( isset( $_POST[ $timeunit ] ) && ! empty( $_POST[ 'hidden_' . $timeunit ] ) && $_POST[ 'hidden_' . $timeunit ] != $_POST[ $timeunit ] ) { |
| 1547 |
$edit_date = '1'; |
| 1548 |
break; |
| 1549 |
} |
| 1550 |
} |
| 1551 |
if ( $ret && empty( $edit_date ) ) { |
| 1552 |
add_filter( 'pre_post_date', [ $this, 'helper_timestamp_hack' ] ); |
| 1553 |
add_filter( 'pre_post_date_gmt', [ $this, 'helper_timestamp_hack' ] ); |
| 1554 |
} |
| 1555 |
} |
| 1556 |
// phpcs:enable WordPress.Security.NonceVerification.Missing |
| 1557 |
} |
| 1558 |
|
| 1559 |
/** |
| 1560 |
* PHP < 5.3.x doesn't support anonymous functions. |
| 1561 |
* |
| 1562 |
* This helper is only used for the check_timestamp_on_publish method above. |
| 1563 |
* |
| 1564 |
* @since 0.7.3 |
| 1565 |
* |
| 1566 |
* @return string The current time or empty string. |
| 1567 |
*/ |
| 1568 |
public function helper_timestamp_hack() { |
| 1569 |
return ( 'pre_post_date' == current_filter() ) ? current_time( 'mysql' ) : ''; |
| 1570 |
} |
| 1571 |
|
| 1572 |
/** |
| 1573 |
* This is a hack! hack! hack! until core is fixed/better supports custom statuses. |
| 1574 |
* |
| 1575 |
* Normalize post_date_gmt if it isn't set to the past or the future. |
| 1576 |
* |
| 1577 |
* @since 0.6.5 |
| 1578 |
* |
| 1579 |
* @see Works around this limitation: https://core.trac.wordpress.org/browser/tags/4.5.1/src/wp-includes/post.php#L3182 |
| 1580 |
* @see Original thread: http://wordpress.org/support/topic/plugin-edit-flow-custom-statuses-create-timestamp-problem |
| 1581 |
* @see Core ticket: http://core.trac.wordpress.org/ticket/18362 |
| 1582 |
* |
| 1583 |
* @param array $data An array of slashed, sanitized post data. |
| 1584 |
* @param array $postarr An array of sanitized post data. |
| 1585 |
* @return array Modified post data. |
| 1586 |
*/ |
| 1587 |
public function fix_custom_status_timestamp( $data, $postarr ) { |
| 1588 |
global $edit_flow; |
| 1589 |
// Don't run this if Edit Flow isn't active, or we're on some other page. |
| 1590 |
if ( $this->disable_custom_statuses_for_post_type() |
| 1591 |
|| ! isset( $edit_flow ) ) { |
| 1592 |
return $data; |
| 1593 |
} |
| 1594 |
|
| 1595 |
$status_slugs = wp_list_pluck( $this->get_custom_statuses(), 'slug' ); |
| 1596 |
|
| 1597 |
// Post is scheduled or published? Ignoring. |
| 1598 |
if ( ! in_array( $postarr['post_status'], $status_slugs ) ) { |
| 1599 |
return $data; |
| 1600 |
} |
| 1601 |
|
| 1602 |
// phpcs:ignore Squiz.PHP.CommentedOutCode.Found -- Not commented out code, just describes conditional logic. |
| 1603 |
// If empty, keep empty. |
| 1604 |
if ( empty( $postarr['post_date_gmt'] ) |
| 1605 |
|| '0000-00-00 00:00:00' == $postarr['post_date_gmt'] ) { |
| 1606 |
$data['post_date_gmt'] = '0000-00-00 00:00:00'; |
| 1607 |
} |
| 1608 |
|
| 1609 |
return $data; |
| 1610 |
} |
| 1611 |
|
| 1612 |
/** |
| 1613 |
* Update post_date to current time when publishing from a custom status. |
| 1614 |
* |
| 1615 |
* When a post with a custom status (like "Pitch" or "Assigned") is published, |
| 1616 |
* the post_date should reflect the actual publication time, not the original |
| 1617 |
* creation time. This matches WordPress core behavior for 'draft' and 'pending'. |
| 1618 |
* |
| 1619 |
* @since 0.10.0 |
| 1620 |
* |
| 1621 |
* @see https://github.com/Automattic/Edit-Flow/issues/750 |
| 1622 |
* |
| 1623 |
* @param array $data An array of slashed, sanitized post data. |
| 1624 |
* @param array $postarr An array of sanitized post data. |
| 1625 |
* @return array Modified post data with updated post_date if applicable. |
| 1626 |
*/ |
| 1627 |
public function update_post_date_on_publish_from_custom_status( $data, $postarr ) { |
| 1628 |
// Only process when transitioning to 'publish' status. |
| 1629 |
if ( 'publish' !== $data['post_status'] ) { |
| 1630 |
return $data; |
| 1631 |
} |
| 1632 |
|
| 1633 |
// Must be an existing post (have an ID) for this to be a status transition. |
| 1634 |
if ( empty( $postarr['ID'] ) ) { |
| 1635 |
return $data; |
| 1636 |
} |
| 1637 |
|
| 1638 |
// Get the current post from the database to check its current status. |
| 1639 |
$current_post = get_post( $postarr['ID'] ); |
| 1640 |
if ( ! $current_post ) { |
| 1641 |
return $data; |
| 1642 |
} |
| 1643 |
|
| 1644 |
// If already published, scheduled, or private, don't change the date. |
| 1645 |
$published_statuses = [ 'publish', 'future', 'private' ]; |
| 1646 |
if ( in_array( $current_post->post_status, $published_statuses, true ) ) { |
| 1647 |
return $data; |
| 1648 |
} |
| 1649 |
|
| 1650 |
// If the post had an explicitly set GMT date (scheduled), don't change it. |
| 1651 |
if ( ! empty( $current_post->post_date_gmt ) |
| 1652 |
&& '0000-00-00 00:00:00' !== $current_post->post_date_gmt ) { |
| 1653 |
return $data; |
| 1654 |
} |
| 1655 |
|
| 1656 |
// If the user is explicitly setting a different date in this update, respect it. |
| 1657 |
// Compare the incoming post_date with the current post_date. |
| 1658 |
if ( ! empty( $postarr['post_date'] ) && $postarr['post_date'] !== $current_post->post_date ) { |
| 1659 |
return $data; |
| 1660 |
} |
| 1661 |
|
| 1662 |
// Update post_date to current time. |
| 1663 |
$data['post_date'] = current_time( 'mysql' ); |
| 1664 |
$data['post_date_gmt'] = current_time( 'mysql', true ); |
| 1665 |
|
| 1666 |
return $data; |
| 1667 |
} |
| 1668 |
|
| 1669 |
/** |
| 1670 |
* Register REST API filters for every post type that supports custom statuses. |
| 1671 |
* |
| 1672 |
* @since 0.10.4 |
| 1673 |
*/ |
| 1674 |
public function register_rest_api_filters() { |
| 1675 |
$post_types = $this->get_post_types_for_module( $this->module ); |
| 1676 |
foreach ( $post_types as $post_type ) { |
| 1677 |
add_filter( "rest_prepare_{$post_type}", [ $this, 'fix_custom_status_rest_date_gmt' ], 10, 2 ); |
| 1678 |
} |
| 1679 |
} |
| 1680 |
|
| 1681 |
/** |
| 1682 |
* Return null for date and date_gmt in REST responses for posts in a |
| 1683 |
* custom status whose GMT date has not been explicitly set. |
| 1684 |
* |
| 1685 |
* WP core's WP_REST_Posts_Controller::prepare_item_for_response() only returns |
| 1686 |
* null for date_gmt when a post's status is 'draft' or 'pending'. Custom |
| 1687 |
* statuses fall through to the branch that converts post_date_gmt of |
| 1688 |
* '0000-00-00 00:00:00' into a concrete ISO 8601 date derived from post_date. |
| 1689 |
* |
| 1690 |
* Even nulling date_gmt alone is insufficient: Gutenberg's |
| 1691 |
* isEditedPostDateFloating selector hardcodes the status whitelist to |
| 1692 |
* 'draft'/'auto-draft'/'pending', so for a custom status it short-circuits |
| 1693 |
* to false and the Publish field falls back to formatting the concrete date. |
| 1694 |
* Nulling date as well pushes Gutenberg's label renderer into its |
| 1695 |
* "Immediately" branch (which triggers when date itself is null) without |
| 1696 |
* needing to modify core. |
| 1697 |
* |
| 1698 |
* The saved post_date in the database is untouched: Gutenberg only sends |
| 1699 |
* changed fields on save, so a nulled date in the response does not |
| 1700 |
* propagate back unless the user actively edits the schedule. |
| 1701 |
* |
| 1702 |
* @since 0.10.4 |
| 1703 |
* |
| 1704 |
* @see https://github.com/Automattic/edit-flow/issues/925 |
| 1705 |
* |
| 1706 |
* @param \WP_REST_Response $response The response object. |
| 1707 |
* @param \WP_Post $post Post object. |
| 1708 |
* @return \WP_REST_Response |
| 1709 |
*/ |
| 1710 |
public function fix_custom_status_rest_date_gmt( $response, $post ) { |
| 1711 |
if ( ! $response instanceof \WP_REST_Response ) { |
| 1712 |
return $response; |
| 1713 |
} |
| 1714 |
|
| 1715 |
if ( '0000-00-00 00:00:00' !== $post->post_date_gmt ) { |
| 1716 |
return $response; |
| 1717 |
} |
| 1718 |
|
| 1719 |
$status_slugs = wp_list_pluck( $this->get_custom_statuses(), 'slug' ); |
| 1720 |
if ( ! in_array( $post->post_status, $status_slugs, true ) ) { |
| 1721 |
return $response; |
| 1722 |
} |
| 1723 |
|
| 1724 |
$data = $response->get_data(); |
| 1725 |
if ( ! is_array( $data ) ) { |
| 1726 |
return $response; |
| 1727 |
} |
| 1728 |
|
| 1729 |
if ( array_key_exists( 'date_gmt', $data ) ) { |
| 1730 |
$data['date_gmt'] = null; |
| 1731 |
} |
| 1732 |
if ( array_key_exists( 'date', $data ) ) { |
| 1733 |
$data['date'] = null; |
| 1734 |
} |
| 1735 |
|
| 1736 |
$response->set_data( $data ); |
| 1737 |
|
| 1738 |
return $response; |
| 1739 |
} |
| 1740 |
|
| 1741 |
/** |
| 1742 |
* A new hack! hack! hack! until core better supports custom statuses. |
| 1743 |
* |
| 1744 |
* If the post_name is set, set it, otherwise keep it empty. |
| 1745 |
* |
| 1746 |
* @since 0.9.4 |
| 1747 |
* |
| 1748 |
* @see https://github.com/Automattic/Edit-Flow/issues/523 |
| 1749 |
* @see https://github.com/Automattic/Edit-Flow/issues/633 |
| 1750 |
* |
| 1751 |
* @param array $data An array of slashed, sanitized post data. |
| 1752 |
* @param array $postarr An array of sanitized post data. |
| 1753 |
* @return array Modified post data. |
| 1754 |
*/ |
| 1755 |
public function maybe_keep_post_name_empty( $data, $postarr ) { |
| 1756 |
$status_slugs = wp_list_pluck( $this->get_custom_statuses(), 'slug' ); |
| 1757 |
|
| 1758 |
// Ignore if it's not a post status and post type we support. |
| 1759 |
if ( ! in_array( $data['post_status'], $status_slugs ) |
| 1760 |
|| ! in_array( $data['post_type'], $this->get_post_types_for_module( $this->module ) ) ) { |
| 1761 |
return $data; |
| 1762 |
} |
| 1763 |
|
| 1764 |
// If the post_name was intentionally set, set the post_name. |
| 1765 |
if ( ! empty( $postarr['post_name'] ) ) { |
| 1766 |
$data['post_name'] = sanitize_title( $postarr['post_name'] ); |
| 1767 |
return $data; |
| 1768 |
} |
| 1769 |
|
| 1770 |
// Otherwise, keep the post_name empty. |
| 1771 |
$data['post_name'] = ''; |
| 1772 |
|
| 1773 |
return $data; |
| 1774 |
} |
| 1775 |
|
| 1776 |
/** |
| 1777 |
* A new hack! hack! hack! until core better supports custom statuses. |
| 1778 |
* |
| 1779 |
* `wp_unique_post_slug` is used to set the `post_name`. When a custom status is used, WordPress will try |
| 1780 |
* really hard to set `post_name`, and we leverage `wp_unique_post_slug` to prevent it being set. |
| 1781 |
* |
| 1782 |
* @since 0.9.4 |
| 1783 |
* |
| 1784 |
* @see https://github.com/WordPress/WordPress/blob/396647666faebb109d9cd4aada7bb0c7d0fb8aca/wp-includes/post.php#L3932 |
| 1785 |
* |
| 1786 |
* @param string|null $override_slug Short-circuit return value. |
| 1787 |
* @param string $slug The desired slug. |
| 1788 |
* @param int $post_ID Post ID. |
| 1789 |
* @param string $post_status The post status. |
| 1790 |
* @param string $post_type Post type. |
| 1791 |
* @param int $post_parent Post parent ID. |
| 1792 |
* @return string|null The override slug or null. |
| 1793 |
*/ |
| 1794 |
public function fix_unique_post_slug( $override_slug, $slug, $post_ID, $post_status, $post_type, $post_parent ) { |
| 1795 |
$status_slugs = wp_list_pluck( $this->get_custom_statuses(), 'slug' ); |
| 1796 |
|
| 1797 |
if ( ! in_array( $post_status, $status_slugs ) |
| 1798 |
|| ! in_array( $post_type, $this->get_post_types_for_module( $this->module ) ) ) { |
| 1799 |
return null; |
| 1800 |
} |
| 1801 |
|
| 1802 |
$post = get_post( $post_ID ); |
| 1803 |
|
| 1804 |
if ( empty( $post ) ) { |
| 1805 |
return null; |
| 1806 |
} |
| 1807 |
|
| 1808 |
if ( $post->post_name ) { |
| 1809 |
return $slug; |
| 1810 |
} |
| 1811 |
|
| 1812 |
return ''; |
| 1813 |
} |
| 1814 |
|
| 1815 |
|
| 1816 |
/** |
| 1817 |
* Another hack! hack! hack! until core better supports custom statuses. |
| 1818 |
* |
| 1819 |
* The preview link for an unpublished post should always be ?p= |
| 1820 |
* |
| 1821 |
* @since 0.7.4 |
| 1822 |
* |
| 1823 |
* @param string $preview_link URL used for the post preview. |
| 1824 |
* @return string Modified preview link. |
| 1825 |
*/ |
| 1826 |
public function fix_preview_link_part_one( $preview_link ) { |
| 1827 |
global $pagenow; |
| 1828 |
|
| 1829 |
$post = get_post( get_the_ID() ); |
| 1830 |
|
| 1831 |
// Only modify if we're using a pre-publish status on a supported custom post type. |
| 1832 |
$status_slugs = wp_list_pluck( $this->get_custom_statuses(), 'slug' ); |
| 1833 |
if ( ! $post |
| 1834 |
|| ! is_admin() |
| 1835 |
|| 'post.php' != $pagenow |
| 1836 |
|| ! in_array( $post->post_status, $status_slugs ) |
| 1837 |
|| ! in_array( $post->post_type, $this->get_post_types_for_module( $this->module ) ) |
| 1838 |
|| strpos( $preview_link, 'preview_id' ) !== false |
| 1839 |
|| 'sample' == $post->filter ) { |
| 1840 |
return $preview_link; |
| 1841 |
} |
| 1842 |
|
| 1843 |
return $this->get_preview_link( $post ); |
| 1844 |
} |
| 1845 |
|
| 1846 |
/** |
| 1847 |
* Another hack! hack! hack! until core better supports custom statuses. |
| 1848 |
* |
| 1849 |
* The preview link for an unpublished post should always be ?p= |
| 1850 |
* The code used to trigger a post preview doesn't also apply the 'preview_post_link' filter. |
| 1851 |
* So we can't do a targeted filter. Instead, we can even more hackily filter get_permalink. |
| 1852 |
* |
| 1853 |
* @since 0.7.4 |
| 1854 |
* |
| 1855 |
* @see http://core.trac.wordpress.org/ticket/19378 |
| 1856 |
* |
| 1857 |
* @param string $permalink The post's permalink. |
| 1858 |
* @param WP_Post|int $post The post in question. |
| 1859 |
* @param bool $sample Is it a sample permalink. |
| 1860 |
* @return string Modified permalink. |
| 1861 |
*/ |
| 1862 |
public function fix_preview_link_part_two( $permalink, $post, $sample ) { |
| 1863 |
global $pagenow; |
| 1864 |
|
| 1865 |
if ( is_int( $post ) ) { |
| 1866 |
$post = get_post( $post ); |
| 1867 |
} |
| 1868 |
|
| 1869 |
// Bail if $post is not a valid post object. |
| 1870 |
if ( ! $post instanceof WP_Post ) { |
| 1871 |
return $permalink; |
| 1872 |
} |
| 1873 |
|
| 1874 |
// Should we be doing anything at all? |
| 1875 |
if ( ! in_array( $post->post_type, $this->get_post_types_for_module( $this->module ) ) ) { |
| 1876 |
return $permalink; |
| 1877 |
} |
| 1878 |
|
| 1879 |
// Is this published? |
| 1880 |
if ( in_array( $post->post_status, $this->published_statuses ) ) { |
| 1881 |
return $permalink; |
| 1882 |
} |
| 1883 |
|
| 1884 |
// Are we overriding the permalink? Don't do anything. |
| 1885 |
// phpcs:ignore WordPress.Security.NonceVerification.Missing |
| 1886 |
if ( isset( $_POST['action'] ) && 'sample-permalink' == $_POST['action'] ) { |
| 1887 |
return $permalink; |
| 1888 |
} |
| 1889 |
|
| 1890 |
// Are we previewing the post from the normal post screen? |
| 1891 |
if ( ( 'post.php' == $pagenow || 'post-new.php' == $pagenow ) |
| 1892 |
// phpcs:ignore WordPress.Security.NonceVerification.Missing |
| 1893 |
&& ! isset( $_POST['wp-preview'] ) ) { |
| 1894 |
return $permalink; |
| 1895 |
} |
| 1896 |
|
| 1897 |
// If it's a sample permalink, not a preview. |
| 1898 |
if ( $sample ) { |
| 1899 |
return $permalink; |
| 1900 |
} |
| 1901 |
|
| 1902 |
return $this->get_preview_link( $post ); |
| 1903 |
} |
| 1904 |
|
| 1905 |
/** |
| 1906 |
* Another hack! hack! hack! until core better supports custom statuses. |
| 1907 |
* |
| 1908 |
* The preview link for a saved unpublished post with a custom status returns a 'preview_nonce' |
| 1909 |
* in it and needs to be removed when previewing it to return a viewable preview link. |
| 1910 |
* |
| 1911 |
* @since 0.9 |
| 1912 |
* |
| 1913 |
* @see https://github.com/Automattic/Edit-Flow/issues/513 |
| 1914 |
* |
| 1915 |
* @param string $preview_link URL used for the post preview. |
| 1916 |
* @param WP_Post $query_args Post object. |
| 1917 |
* @return string Modified preview link. |
| 1918 |
*/ |
| 1919 |
public function fix_preview_link_part_three( $preview_link, $query_args ) { |
| 1920 |
$autosave = wp_get_post_autosave( $query_args->ID, get_current_user_id() ); |
| 1921 |
if ( $autosave ) { |
| 1922 |
foreach ( array_intersect( array_keys( _wp_post_revision_fields( $query_args ) ), array_keys( _wp_post_revision_fields( $autosave ) ) ) as $field ) { |
| 1923 |
if ( normalize_whitespace( $query_args->$field ) != normalize_whitespace( $autosave->$field ) ) { |
| 1924 |
// Pass through, it's a personal preview. |
| 1925 |
return $preview_link; |
| 1926 |
} |
| 1927 |
} |
| 1928 |
} |
| 1929 |
return remove_query_arg( [ 'preview_nonce' ], $preview_link ); |
| 1930 |
} |
| 1931 |
|
| 1932 |
/** |
| 1933 |
* Another hack! hack! hack! until core better supports custom statuses. |
| 1934 |
* |
| 1935 |
* Posts and pages with a custom status are kept with an empty `post_name` |
| 1936 |
* (see `maybe_keep_post_name_empty()` and `fix_unique_post_slug()`). WordPress's |
| 1937 |
* template hierarchy resolves slug-specific templates — `page-{slug}.php` and |
| 1938 |
* `single-{post-type}-{slug}.php` — from `$post->post_name`, so an empty slug |
| 1939 |
* silently degrades template selection when the post is previewed: only the |
| 1940 |
* generic `page.php` / `single.php` fallback can match. The same empty slug also |
| 1941 |
* breaks template-based conditional logic such as ACF location rules. |
| 1942 |
* |
| 1943 |
* Here we synthesise a slug from the title on the queried object in memory only, |
| 1944 |
* for the duration of the preview request. Nothing is persisted: the stored |
| 1945 |
* `post_name` stays empty, so none of the slug-emptying behaviour changes. |
| 1946 |
* |
| 1947 |
* @since 0.11.0 |
| 1948 |
* |
| 1949 |
* @see https://github.com/Automattic/Edit-Flow/issues/933 |
| 1950 |
*/ |
| 1951 |
public function fix_preview_template() { |
| 1952 |
// Only singular requests resolve a slug-specific template from post_name. |
| 1953 |
if ( ! is_singular() ) { |
| 1954 |
return; |
| 1955 |
} |
| 1956 |
|
| 1957 |
$post = get_queried_object(); |
| 1958 |
|
| 1959 |
// Nothing to do unless we have a titled post whose slug is empty. |
| 1960 |
if ( ! $post instanceof WP_Post |
| 1961 |
|| ! empty( $post->post_name ) |
| 1962 |
|| empty( $post->post_title ) ) { |
| 1963 |
return; |
| 1964 |
} |
| 1965 |
|
| 1966 |
// Only act on the pre-publish custom statuses and post types we support. |
| 1967 |
$status_slugs = wp_list_pluck( $this->get_custom_statuses(), 'slug' ); |
| 1968 |
if ( ! in_array( $post->post_status, $status_slugs, true ) |
| 1969 |
|| ! in_array( $post->post_type, $this->get_post_types_for_module( $this->module ), true ) ) { |
| 1970 |
return; |
| 1971 |
} |
| 1972 |
|
| 1973 |
/** |
| 1974 |
* Filters the slug synthesised in memory for template selection while |
| 1975 |
* previewing a custom-status post. Return an empty string to leave the |
| 1976 |
* slug empty and disable this behaviour for the post. |
| 1977 |
* |
| 1978 |
* @since 0.11.0 |
| 1979 |
* |
| 1980 |
* @param string $post_name Slug derived from the post title. |
| 1981 |
* @param WP_Post $post The post being previewed. |
| 1982 |
*/ |
| 1983 |
$post_name = apply_filters( 'ef_preview_template_post_name', sanitize_title( $post->post_title ), $post ); |
| 1984 |
|
| 1985 |
if ( '' === $post_name ) { |
| 1986 |
return; |
| 1987 |
} |
| 1988 |
|
| 1989 |
// Set it on the queried object (used by the template hierarchy) and keep the |
| 1990 |
// loop post and global in sync for template tags and conditional logic. |
| 1991 |
$post->post_name = $post_name; |
| 1992 |
|
| 1993 |
global $wp_query; |
| 1994 |
if ( isset( $wp_query->post ) && $wp_query->post instanceof WP_Post && $wp_query->post->ID === $post->ID ) { |
| 1995 |
$wp_query->post->post_name = $post_name; |
| 1996 |
} |
| 1997 |
if ( isset( $GLOBALS['post'] ) && $GLOBALS['post'] instanceof WP_Post && $GLOBALS['post']->ID === $post->ID ) { |
| 1998 |
$GLOBALS['post']->post_name = $post_name; |
| 1999 |
} |
| 2000 |
} |
| 2001 |
|
| 2002 |
/** |
| 2003 |
* Fix get_sample_permalink. Previously the 'editable_slug' filter was leveraged |
| 2004 |
* to correct the sample permalink a user could edit on post.php. Since 4.4.40 |
| 2005 |
* the `get_sample_permalink` filter was added which allows greater flexibility in |
| 2006 |
* manipulating the slug. Critical for cases like editing the sample permalink on |
| 2007 |
* hierarchical post types. |
| 2008 |
* |
| 2009 |
* @since 0.8.2 |
| 2010 |
* |
| 2011 |
* @param string $permalink Sample permalink. |
| 2012 |
* @param int $post_id Post ID. |
| 2013 |
* @param string $title Post title. |
| 2014 |
* @param string $name Post name (slug). |
| 2015 |
* @param WP_Post $post Post object. |
| 2016 |
* @return array Modified permalink array. |
| 2017 |
*/ |
| 2018 |
public function fix_get_sample_permalink( $permalink, $post_id, $title, $name, $post ) { |
| 2019 |
|
| 2020 |
$status_slugs = wp_list_pluck( $this->get_custom_statuses(), 'slug' ); |
| 2021 |
|
| 2022 |
if ( ! in_array( $post->post_status, $status_slugs ) |
| 2023 |
|| ! in_array( $post->post_type, $this->get_post_types_for_module( $this->module ) ) ) { |
| 2024 |
return $permalink; |
| 2025 |
} |
| 2026 |
|
| 2027 |
remove_filter( 'get_sample_permalink', [ $this, 'fix_get_sample_permalink' ], 10, 5 ); |
| 2028 |
|
| 2029 |
$new_name = ! is_null( $name ) ? $name : $post->post_name; |
| 2030 |
$new_title = ! is_null( $title ) ? $title : $post->post_title; |
| 2031 |
|
| 2032 |
$post = get_post( $post_id ); |
| 2033 |
$status_before = $post->post_status; |
| 2034 |
$post->post_status = 'draft'; |
| 2035 |
|
| 2036 |
$permalink = get_sample_permalink( $post, $title, sanitize_title( $new_name ? $new_name : $new_title, $post->ID ) ); |
| 2037 |
|
| 2038 |
$post->post_status = $status_before; |
| 2039 |
|
| 2040 |
add_filter( 'get_sample_permalink', [ $this, 'fix_get_sample_permalink' ], 10, 5 ); |
| 2041 |
|
| 2042 |
return $permalink; |
| 2043 |
} |
| 2044 |
|
| 2045 |
/** |
| 2046 |
* Hack to work around post status check in get_sample_permalink_html. |
| 2047 |
* |
| 2048 |
* The get_sample_permalink_html checks the status of the post and if it's |
| 2049 |
* a draft generates a certain permalink structure. |
| 2050 |
* We need to do the same work it's doing for custom statuses in order |
| 2051 |
* to support this link. |
| 2052 |
* |
| 2053 |
* @see https://core.trac.wordpress.org/browser/tags/4.5.2/src/wp-admin/includes/post.php#L1296 |
| 2054 |
* |
| 2055 |
* @since 0.8.2 |
| 2056 |
* |
| 2057 |
* @param string $permalink Sample permalink HTML markup. |
| 2058 |
* @param int $post_id Post ID. |
| 2059 |
* @param string $new_title New sample permalink title. |
| 2060 |
* @param string $new_slug New sample permalink slug. |
| 2061 |
* @param WP_Post $post Post object. |
| 2062 |
* @return string Modified sample permalink HTML. |
| 2063 |
*/ |
| 2064 |
public function fix_get_sample_permalink_html( $permalink, $post_id, $new_title, $new_slug, $post ) { |
| 2065 |
$status_slugs = wp_list_pluck( $this->get_custom_statuses(), 'slug' ); |
| 2066 |
|
| 2067 |
if ( ! in_array( $post->post_status, $status_slugs ) |
| 2068 |
|| ! in_array( $post->post_type, $this->get_post_types_for_module( $this->module ) ) ) { |
| 2069 |
return $permalink; |
| 2070 |
} |
| 2071 |
|
| 2072 |
remove_filter( 'get_sample_permalink_html', [ $this, 'fix_get_sample_permalink_html' ], 10, 5 ); |
| 2073 |
|
| 2074 |
$post->post_status = 'draft'; |
| 2075 |
$sample_permalink_html = get_sample_permalink_html( $post, $new_title, $new_slug ); |
| 2076 |
|
| 2077 |
add_filter( 'get_sample_permalink_html', [ $this, 'fix_get_sample_permalink_html' ], 10, 5 ); |
| 2078 |
|
| 2079 |
return $sample_permalink_html; |
| 2080 |
} |
| 2081 |
|
| 2082 |
|
| 2083 |
/** |
| 2084 |
* Fixes a bug where post-pagination doesn't work when previewing a post with a custom status. |
| 2085 |
* |
| 2086 |
* This filter only modifies output if `is_preview()` is true. |
| 2087 |
* Used by `wp_link_pages_link` filter. |
| 2088 |
* |
| 2089 |
* @link https://github.com/Automattic/Edit-Flow/issues/192 |
| 2090 |
* |
| 2091 |
* @param string $link The page number HTML output. |
| 2092 |
* @param int $i Page number for paginated posts' page links. |
| 2093 |
* @return string Modified link. |
| 2094 |
*/ |
| 2095 |
public function modify_preview_link_pagination_url( $link, $i ) { |
| 2096 |
// Use the original $link when not in preview mode. |
| 2097 |
if ( ! is_preview() ) { |
| 2098 |
return $link; |
| 2099 |
} |
| 2100 |
|
| 2101 |
// Get an array of valid custom status slugs. |
| 2102 |
$custom_statuses = wp_list_pluck( $this->get_custom_statuses(), 'slug' ); |
| 2103 |
|
| 2104 |
// Apply original link filters from core `wp_link_pages()`. |
| 2105 |
$r = apply_filters( 'wp_link_pages_args', [ |
| 2106 |
'link_before' => '', |
| 2107 |
'link_after' => '', |
| 2108 |
'pagelink' => '%', |
| 2109 |
]); |
| 2110 |
|
| 2111 |
// _wp_link_page() && _ef_wp_link_page() produce an opening link tag ( <a href=".."> ) |
| 2112 |
// This is necessary to replicate core behavior: |
| 2113 |
$link = $r['link_before'] . str_replace( '%', $i, $r['pagelink'] ) . $r['link_after']; |
| 2114 |
$link = _ef_wp_link_page( $i, $custom_statuses ) . $link . '</a>'; |
| 2115 |
|
| 2116 |
|
| 2117 |
return $link; |
| 2118 |
} |
| 2119 |
|
| 2120 |
/** |
| 2121 |
* Get the proper preview link for a post. |
| 2122 |
* |
| 2123 |
* @since 0.8 |
| 2124 |
* |
| 2125 |
* @param WP_Post $post The post object. |
| 2126 |
* @return string The preview URL. |
| 2127 |
*/ |
| 2128 |
private function get_preview_link( $post ) { |
| 2129 |
|
| 2130 |
if ( 'page' == $post->post_type ) { |
| 2131 |
$args = [ |
| 2132 |
'page_id' => $post->ID, |
| 2133 |
]; |
| 2134 |
} elseif ( 'post' == $post->post_type ) { |
| 2135 |
$args = [ |
| 2136 |
'p' => $post->ID, |
| 2137 |
'preview' => 'true', |
| 2138 |
]; |
| 2139 |
} else { |
| 2140 |
$args = [ |
| 2141 |
'p' => $post->ID, |
| 2142 |
'post_type' => $post->post_type, |
| 2143 |
]; |
| 2144 |
} |
| 2145 |
|
| 2146 |
$args['preview_id'] = $post->ID; |
| 2147 |
return add_query_arg( $args, home_url( '/' ) ); |
| 2148 |
} |
| 2149 |
|
| 2150 |
/** |
| 2151 |
* Another hack! hack! hack! until core better supports custom statuses. |
| 2152 |
* |
| 2153 |
* The preview link for an unpublished post should always be ?p=, even in the list table. |
| 2154 |
* |
| 2155 |
* @since 0.7.4 |
| 2156 |
* |
| 2157 |
* @see http://core.trac.wordpress.org/ticket/19378 |
| 2158 |
* |
| 2159 |
* @param array $actions An array of row action links. |
| 2160 |
* @param WP_Post $post The post object. |
| 2161 |
* @return array Modified row action links. |
| 2162 |
*/ |
| 2163 |
public function fix_post_row_actions( $actions, $post ) { |
| 2164 |
global $pagenow; |
| 2165 |
|
| 2166 |
// Only modify if we're using a pre-publish status on a supported custom post type. |
| 2167 |
$status_slugs = wp_list_pluck( $this->get_custom_statuses(), 'slug' ); |
| 2168 |
if ( 'edit.php' != $pagenow |
| 2169 |
|| ! in_array( $post->post_status, $status_slugs ) |
| 2170 |
|| ! in_array( $post->post_type, $this->get_post_types_for_module( $this->module ) ) ) { |
| 2171 |
return $actions; |
| 2172 |
} |
| 2173 |
|
| 2174 |
// 'view' is only set if the user has permission to post |
| 2175 |
if ( empty( $actions['view'] ) ) { |
| 2176 |
return $actions; |
| 2177 |
} |
| 2178 |
|
| 2179 |
if ( 'page' == $post->post_type ) { |
| 2180 |
$args = [ |
| 2181 |
'page_id' => $post->ID, |
| 2182 |
]; |
| 2183 |
} elseif ( 'post' == $post->post_type ) { |
| 2184 |
$args = [ |
| 2185 |
'p' => $post->ID, |
| 2186 |
]; |
| 2187 |
} else { |
| 2188 |
$args = [ |
| 2189 |
'p' => $post->ID, |
| 2190 |
'post_type' => $post->post_type, |
| 2191 |
]; |
| 2192 |
} |
| 2193 |
$args['preview'] = 'true'; |
| 2194 |
$preview_link = add_query_arg( $args, home_url( '/' ) ); |
| 2195 |
|
| 2196 |
/* translators: %s: post title */ |
| 2197 |
$actions['view'] = '<a href="' . esc_url( $preview_link ) . '" title="' . esc_attr( sprintf( __( 'Preview “%s”', 'edit-flow' ), $post->post_title ) ) . '" rel="permalink">' . __( 'Preview', 'edit-flow' ) . '</a>'; |
| 2198 |
return $actions; |
| 2199 |
} |
| 2200 |
} |
| 2201 |
|
| 2202 |
} |
| 2203 |
|
| 2204 |
|
| 2205 |
// phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound |
| 2206 |
|
| 2207 |
/** |
| 2208 |
* Custom Statuses uses WordPress' List Table API for generating the custom status management table |
| 2209 |
* |
| 2210 |
* @since 0.7 |
| 2211 |
*/ |
| 2212 |
class EF_Custom_Status_List_Table extends WP_List_Table { |
| 2213 |
|
| 2214 |
/** |
| 2215 |
* Callback arguments for the list table. |
| 2216 |
* |
| 2217 |
* @var array |
| 2218 |
*/ |
| 2219 |
protected $callback_args; |
| 2220 |
|
| 2221 |
/** |
| 2222 |
* Default post status slug. |
| 2223 |
* |
| 2224 |
* @var string |
| 2225 |
*/ |
| 2226 |
protected $default_status; |
| 2227 |
|
| 2228 |
/** |
| 2229 |
* Construct the extended class. |
| 2230 |
*/ |
| 2231 |
public function __construct() { |
| 2232 |
|
| 2233 |
parent::__construct( [ |
| 2234 |
'plural' => 'custom statuses', |
| 2235 |
'singular' => 'custom status', |
| 2236 |
'ajax' => true, |
| 2237 |
] ); |
| 2238 |
} |
| 2239 |
|
| 2240 |
/** |
| 2241 |
* Pull in the data we'll be displaying on the table |
| 2242 |
* |
| 2243 |
* @since 0.7 |
| 2244 |
*/ |
| 2245 |
public function prepare_items() { |
| 2246 |
global $edit_flow; |
| 2247 |
|
| 2248 |
$columns = $this->get_columns(); |
| 2249 |
$hidden = [ |
| 2250 |
'position', |
| 2251 |
]; |
| 2252 |
$sortable = []; |
| 2253 |
$this->_column_headers = [ $columns, $hidden, $sortable ]; |
| 2254 |
|
| 2255 |
$this->items = $edit_flow->custom_status->get_custom_statuses(); |
| 2256 |
$total_items = count( $this->items ); |
| 2257 |
$this->default_status = $edit_flow->custom_status->get_default_custom_status()->slug; |
| 2258 |
|
| 2259 |
$this->set_pagination_args( [ |
| 2260 |
'total_items' => $total_items, |
| 2261 |
'per_page' => $total_items, |
| 2262 |
] ); |
| 2263 |
} |
| 2264 |
|
| 2265 |
/** |
| 2266 |
* Message to be displayed when there are no custom statuses. Should never be displayed, but we'll customize it |
| 2267 |
* just in case. |
| 2268 |
* |
| 2269 |
* @since 0.7 |
| 2270 |
*/ |
| 2271 |
public function no_items() { |
| 2272 |
_e( 'No custom statuses found.', 'edit-flow' ); |
| 2273 |
} |
| 2274 |
|
| 2275 |
/** |
| 2276 |
* Table shows (hidden) position, status name, status description, and the post count for each activated |
| 2277 |
* post type |
| 2278 |
* |
| 2279 |
* @since 0.7 |
| 2280 |
* |
| 2281 |
* @return array $columns Columns to be registered with the List Table |
| 2282 |
*/ |
| 2283 |
public function get_columns() { |
| 2284 |
global $edit_flow; |
| 2285 |
|
| 2286 |
$columns = [ |
| 2287 |
'position' => __( 'Position', 'edit-flow' ), |
| 2288 |
'name' => __( 'Name', 'edit-flow' ), |
| 2289 |
'description' => __( 'Description', 'edit-flow' ), |
| 2290 |
]; |
| 2291 |
|
| 2292 |
$post_types = get_post_types( '', 'objects' ); |
| 2293 |
$supported_post_types = $edit_flow->helpers->get_post_types_for_module( $edit_flow->custom_status->module ); |
| 2294 |
foreach ( $post_types as $post_type ) { |
| 2295 |
if ( in_array( $post_type->name, $supported_post_types ) ) { |
| 2296 |
$columns[ $post_type->name ] = $post_type->label; |
| 2297 |
} |
| 2298 |
} |
| 2299 |
|
| 2300 |
return $columns; |
| 2301 |
} |
| 2302 |
|
| 2303 |
/** |
| 2304 |
* Fallback column callback. |
| 2305 |
* Primarily used to display post count for each post type. |
| 2306 |
* |
| 2307 |
* @since 0.7 |
| 2308 |
* |
| 2309 |
* @param object $item Custom status as an object. |
| 2310 |
* @param string $column_name Name of the column as registered in $this->prepare_items(). |
| 2311 |
* @return string $output What will be rendered. |
| 2312 |
*/ |
| 2313 |
public function column_default( $item, $column_name ) { |
| 2314 |
global $edit_flow; |
| 2315 |
|
| 2316 |
// Handle custom post counts for different post types. |
| 2317 |
$post_types = get_post_types( '', 'names' ); |
| 2318 |
if ( in_array( $column_name, $post_types ) ) { |
| 2319 |
|
| 2320 |
// @todo Cachify this. |
| 2321 |
$post_count = wp_cache_get( "ef_custom_status_count_$column_name" ); |
| 2322 |
if ( false === $post_count ) { |
| 2323 |
$posts = wp_count_posts( $column_name ); |
| 2324 |
$post_status = $item->slug; |
| 2325 |
// To avoid error notices when changing the name of non-standard statuses. |
| 2326 |
if ( isset( $posts->$post_status ) ) { |
| 2327 |
$post_count = $posts->$post_status; |
| 2328 |
} else { |
| 2329 |
$post_count = 0; |
| 2330 |
} |
| 2331 |
// phpcs:ignore Squiz.PHP.CommentedOutCode.Found, Squiz.Commenting.InlineComment.InvalidEndChar -- Intentionally commented out caching line for future implementation. |
| 2332 |
// wp_cache_set( "ef_custom_status_count_$column_name", $post_count ); |
| 2333 |
} |
| 2334 |
$output = sprintf( |
| 2335 |
'<a title="See all %1$ss saved as \'%2$s\'" href="%3$s">%4$s</a>', |
| 2336 |
esc_attr( $column_name ), |
| 2337 |
esc_attr( $item->name ), |
| 2338 |
esc_url( $edit_flow->helpers->filter_posts_link( $item->slug, $column_name ) ), |
| 2339 |
esc_html( $post_count ) |
| 2340 |
); |
| 2341 |
return $output; |
| 2342 |
} |
| 2343 |
} |
| 2344 |
|
| 2345 |
/** |
| 2346 |
* Hidden column for storing the status position. |
| 2347 |
* |
| 2348 |
* @since 0.7 |
| 2349 |
* |
| 2350 |
* @param object $item Custom status as an object. |
| 2351 |
* @return string $output What will be rendered. |
| 2352 |
*/ |
| 2353 |
public function column_position( $item ) { |
| 2354 |
return esc_html( $item->position ); |
| 2355 |
} |
| 2356 |
|
| 2357 |
/** |
| 2358 |
* Displayed column showing the name of the status. |
| 2359 |
* |
| 2360 |
* @since 0.7 |
| 2361 |
* |
| 2362 |
* @param object $item Custom status as an object. |
| 2363 |
* @return string $output What will be rendered. |
| 2364 |
*/ |
| 2365 |
public function column_name( $item ) { |
| 2366 |
global $edit_flow; |
| 2367 |
|
| 2368 |
$item_edit_link = esc_url( $edit_flow->custom_status->get_link( [ |
| 2369 |
'action' => 'edit-status', |
| 2370 |
'term-id' => $item->term_id, |
| 2371 |
] ) ); |
| 2372 |
|
| 2373 |
$output = '<strong><a href="' . $item_edit_link . '">' . esc_html( $item->name ) . '</a>'; |
| 2374 |
if ( $item->slug == $this->default_status ) { |
| 2375 |
$output .= ' - ' . __( 'Default', 'edit-flow' ); |
| 2376 |
} |
| 2377 |
$output .= '</strong>'; |
| 2378 |
|
| 2379 |
// Don't allow for any of these status actions when adding a new custom status. |
| 2380 |
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only display check. |
| 2381 |
if ( isset( $_GET['action'] ) && 'add' == $_GET['action'] ) { |
| 2382 |
return $output; |
| 2383 |
} |
| 2384 |
|
| 2385 |
$actions = []; |
| 2386 |
$actions['edit'] = "<a href='$item_edit_link'>" . __( 'Edit', 'edit-flow' ) . '</a>'; |
| 2387 |
$actions['inline hide-if-no-js'] = '<a href="#" class="editinline">' . __( 'Quick Edit', 'edit-flow' ) . '</a>'; |
| 2388 |
$actions['make_default'] = sprintf( '<a href="%1$s">' . __( 'Make Default', 'edit-flow' ) . '</a>', $edit_flow->custom_status->get_link( [ |
| 2389 |
'action' => 'make-default', |
| 2390 |
'term-id' => $item->term_id, |
| 2391 |
] ) ); |
| 2392 |
|
| 2393 |
// Prevent deleting draft status. |
| 2394 |
if ( 'draft' !== $item->slug && $item->slug !== $this->default_status ) { |
| 2395 |
$actions['delete delete-status'] = sprintf( '<a href="%1$s">' . __( 'Delete', 'edit-flow' ) . '</a>', $edit_flow->custom_status->get_link( [ |
| 2396 |
'action' => 'delete-status', |
| 2397 |
'term-id' => $item->term_id, |
| 2398 |
] ) ); |
| 2399 |
} |
| 2400 |
|
| 2401 |
$output .= $this->row_actions( $actions, false ); |
| 2402 |
$output .= '<div class="hidden" id="inline_' . esc_attr( $item->term_id ) . '">'; |
| 2403 |
$output .= '<div class="name">' . esc_html( $item->name ) . '</div>'; |
| 2404 |
$output .= '<div class="description">' . esc_html( $item->description ) . '</div>'; |
| 2405 |
$output .= '</div>'; |
| 2406 |
|
| 2407 |
return $output; |
| 2408 |
} |
| 2409 |
|
| 2410 |
/** |
| 2411 |
* Displayed column showing the description of the status. |
| 2412 |
* |
| 2413 |
* @since 0.7 |
| 2414 |
* |
| 2415 |
* @param object $item Custom status as an object. |
| 2416 |
* @return string $output What will be rendered. |
| 2417 |
*/ |
| 2418 |
public function column_description( $item ) { |
| 2419 |
return esc_html( $item->description ); |
| 2420 |
} |
| 2421 |
|
| 2422 |
/** |
| 2423 |
* Prepare and echo a single custom status row. |
| 2424 |
* |
| 2425 |
* @since 0.7 |
| 2426 |
* |
| 2427 |
* @param object $item Custom status as an object. |
| 2428 |
*/ |
| 2429 |
public function single_row( $item ) { |
| 2430 |
static $alternate_class = ''; |
| 2431 |
$alternate_class = ( '' == $alternate_class ? ' alternate' : '' ); |
| 2432 |
|
| 2433 |
printf( '<tr id="term-%d" class="term-static%s">', (int) $item->term_id, esc_attr( $alternate_class ) ); |
| 2434 |
// single_row_columns() echoes its own output; the column_* callbacks are |
| 2435 |
// responsible for escaping, as per WP_List_Table's contract. |
| 2436 |
$this->single_row_columns( $item ); |
| 2437 |
echo '</tr>'; |
| 2438 |
} |
| 2439 |
|
| 2440 |
/** |
| 2441 |
* Hidden form used for inline editing functionality. |
| 2442 |
* |
| 2443 |
* @since 0.7 |
| 2444 |
*/ |
| 2445 |
public function inline_edit() { |
| 2446 |
global $edit_flow; |
| 2447 |
?> |
| 2448 |
<form method="get" action=""><table style="display: none"><tbody id="inlineedit"> |
| 2449 |
<tr id="inline-edit" class="inline-edit-row" style="display: none"><td colspan="<?php echo esc_attr( $this->get_column_count() ); ?>" class="colspanchange"> |
| 2450 |
<fieldset><div class="inline-edit-col"> |
| 2451 |
<h4><?php _e( 'Quick Edit', 'edit-flow' ); ?></h4> |
| 2452 |
<label> |
| 2453 |
<span class="title"><?php _e( 'Name', 'edit-flow' ); ?></span> |
| 2454 |
<span class="input-text-wrap"><input type="text" name="name" class="ptitle" value="" maxlength="20" /></span> |
| 2455 |
</label> |
| 2456 |
<label> |
| 2457 |
<span class="title"><?php _e( 'Description', 'edit-flow' ); ?></span> |
| 2458 |
<span class="input-text-wrap"><input type="text" name="description" class="pdescription" value="" /></span> |
| 2459 |
</label> |
| 2460 |
</div></fieldset> |
| 2461 |
<p class="inline-edit-save submit"> |
| 2462 |
<a accesskey="c" href="#inline-edit" title="<?php _e( 'Cancel', 'edit-flow' ); ?>" class="cancel button-secondary alignleft"><?php _e( 'Cancel', 'edit-flow' ); ?></a> |
| 2463 |
<?php $update_text = __( 'Update Status', 'edit-flow' ); ?> |
| 2464 |
<a accesskey="s" href="#inline-edit" title="<?php echo esc_attr( $update_text ); ?>" class="save button-primary alignright"><?php echo esc_html( $update_text ); ?></a> |
| 2465 |
<img class="waiting" style="display:none;" src="<?php echo esc_url( admin_url( 'images/wpspin_light.gif' ) ); ?>" alt="" /> |
| 2466 |
<span class="error" style="display:none;"></span> |
| 2467 |
<?php wp_nonce_field( 'custom-status-inline-edit-nonce', 'inline_edit', false ); ?> |
| 2468 |
<br class="clear" /> |
| 2469 |
</p> |
| 2470 |
</td></tr> |
| 2471 |
</tbody></table></form> |
| 2472 |
<?php |
| 2473 |
} |
| 2474 |
} |
| 2475 |
|