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

editorial-metadata.php in Edit Flow 0.11.0, at modules/editorial-metadata/editorial-metadata.php

2,043 lines 83.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Editorial Metadata module for Edit Flow.
4 *
5 * This class gives publishers arbitrary structured content details to go along with every post.
6 *
7 * @package EditFlow
8 *
9 * @author sbressler, danielbachhuber
10 *
11 * Ways to test and play with this class:
12 * 1) Create a new term by selecting Editorial Metadata from the Edit Flow settings
13 * 2) Edit an existing term (slug, description, etc.)
14 * 3) Create a post and assign metadata to it
15 * 4) Look at the list of terms again - the count should go up!
16 * 5) Play with adding more metadata to a post
17 * 6) Clear the metadata for a single term in a post and watch the count go down!
18 * 6) Delete a term and note the metadata disappears from posts
19 * 7) Re-add the term (same slug) and the metadata returns!
20 *
21 * Improvements to make:
22 *
23 * @todo Abstract the permissions check for management to class level
24 */
25
26 if ( ! class_exists( 'EF_Editorial_Metadata' ) ) {
27
28 /**
29 * Editorial Metadata module class.
30 *
31 * Allows publishers to add arbitrary structured content details to posts.
32 */
33 class EF_Editorial_Metadata extends EF_Module {
34
35 /**
36 * The name of the taxonomy we're going to register for editorial metadata.
37 */
38 // phpcs:ignore Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase
39 const metadata_taxonomy = 'ef_editorial_meta';
40 // phpcs:ignore Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase
41 const metadata_postmeta_key = '_ef_editorial_meta';
42
43 /**
44 * Module name.
45 *
46 * @var string
47 */
48 public $module_name = 'editorial_metadata';
49
50 /**
51 * Cache for editorial metadata terms.
52 *
53 * @var array
54 */
55 private $editorial_metadata_terms_cache = array();
56
57 /**
58 * Construct the EF_Editorial_Metadata class.
59 */
60 public function __construct() {
61
62 $this->module_url = $this->get_module_url( __FILE__ );
63 // Register the module with Edit Flow.
64 $args = array(
65 'title' => __( 'Editorial Metadata', 'edit-flow' ),
66 'short_description' => __( 'Track details about your posts in progress.', 'edit-flow' ),
67 'extended_description' => __( 'Log details on every assignment using configurable editorial metadata. It’s completely customizable; create fields for everything from due date to location to contact information to role assignments.', 'edit-flow' ),
68 'module_url' => $this->module_url,
69 'img_url' => $this->module_url . 'lib/editorial_metadata_s128.png',
70 'slug' => 'editorial-metadata',
71 'default_options' => array(
72 'enabled' => 'on',
73 'post_types' => array(
74 'post' => 'on',
75 'page' => 'off',
76 ),
77 ),
78 'messages' => array(
79 'term-added' => __( 'Metadata term added.', 'edit-flow' ),
80 'term-updated' => __( 'Metadata term updated.', 'edit-flow' ),
81 'term-missing' => __( "Metadata term doesn't exist.", 'edit-flow' ),
82 'term-deleted' => __( 'Metadata term deleted.', 'edit-flow' ),
83 'term-position-updated' => __( 'Term order updated.', 'edit-flow' ),
84 'term-visibility-changed' => __( 'Term visibility changed.', 'edit-flow' ),
85 ),
86 'configure_page_cb' => 'print_configure_view',
87 'settings_help_tab' => array(
88 'id' => 'ef-editorial-metadata-overview',
89 'title' => __( 'Overview', 'edit-flow' ),
90 'content' => __( '<p>Keep track of important details about your content with editorial metadata. This feature allows you to create as many date, text, number, etc. fields as you like, and then use them to store information like contact details, required word count, or the location of an interview.</p><p>Once you’ve set your fields up, editorial metadata integrates with both the calendar and the story budget. Make an editorial metadata item visible to have it appear to the rest of your team. Keep it hidden to restrict the information between the writer and their editor.</p>', 'edit-flow' ),
91 ),
92 'settings_help_sidebar' => __( '<p><strong>For more information:</strong></p><p><a href="https://editflow.org/features/editorial-metadata/">Editorial Metadata 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' ),
93 );
94 EditFlow()->register_module( $this->module_name, $args );
95 }
96
97 /**
98 * Initialize the module. Conditionally loads if the module is enabled.
99 */
100 public function init() {
101
102 // Register the taxonomy we use for Editorial Metadata with WordPress core.
103 $this->register_taxonomy();
104
105 // Register post meta for REST API support (enables Gutenberg saving).
106 $this->register_metadata_for_rest_api();
107
108 // Anything that needs to happen in the admin.
109 add_action( 'admin_init', array( $this, 'action_admin_init' ) );
110
111 // Register our settings.
112 add_action( 'admin_init', array( $this, 'register_settings' ) );
113
114 // Actions relevant to the configuration view (adding, editing, or sorting existing Editorial Metadata).
115 add_action( 'admin_init', array( $this, 'handle_add_editorial_metadata' ) );
116 add_action( 'admin_init', array( $this, 'handle_edit_editorial_metadata' ) );
117 add_action( 'admin_init', array( $this, 'handle_change_editorial_metadata_visibility' ) );
118 add_action( 'admin_init', array( $this, 'handle_delete_editorial_metadata' ) );
119 add_action( 'wp_ajax_inline_save_term', array( $this, 'handle_ajax_inline_save_term' ) );
120 add_action( 'wp_ajax_update_term_positions', array( $this, 'handle_ajax_update_term_positions' ) );
121
122 add_action( 'add_meta_boxes', array( $this, 'handle_post_metaboxes' ) );
123 add_action( 'save_post', array( $this, 'save_meta_box' ), 10, 2 );
124
125 // Add Editorial Metadata columns to the Manage Posts view.
126 $supported_post_types = $this->get_post_types_for_module( $this->module );
127 foreach ( $supported_post_types as $post_type ) {
128 add_filter( "manage_{$post_type}_posts_columns", array( $this, 'filter_manage_posts_columns' ) );
129 add_action( "manage_{$post_type}_posts_custom_column", array( $this, 'action_manage_posts_custom_column' ), 10, 2 );
130 }
131
132 // Add Editorial Metadata to the calendar if the calendar is activated.
133 if ( $this->module_enabled( 'calendar' ) ) {
134 add_filter( 'ef_calendar_item_information_fields', array( $this, 'filter_calendar_item_fields' ), 10, 2 );
135 }
136
137 // Add Editorial Metadata columns to the Story Budget if it exists.
138 if ( $this->module_enabled( 'story_budget' ) ) {
139 add_filter( 'ef_story_budget_term_columns', array( $this, 'filter_story_budget_term_columns' ) );
140 // Register an action to handle this data later.
141 add_filter( 'ef_story_budget_term_column_value', array( $this, 'filter_story_budget_term_column_values' ), 10, 3 );
142 }
143
144 // Load necessary scripts and stylesheets.
145 add_action( 'admin_enqueue_scripts', array( $this, 'add_admin_scripts' ) );
146 }
147
148 /**
149 * Load default editorial metadata the first time the module is loaded.
150 *
151 * @since 0.7
152 */
153 public function install() {
154 // Our default metadata fields.
155 $default_metadata = array(
156 array(
157 'name' => __( 'First Draft Date', 'edit-flow' ),
158 'slug' => 'first-draft-date',
159 'type' => 'date',
160 'description' => __( 'When the first draft needs to be ready.', 'edit-flow' ),
161 ),
162 array(
163 'name' => __( 'Assignment', 'edit-flow' ),
164 'slug' => 'assignment',
165 'type' => 'paragraph',
166 'description' => __( 'What the post needs to cover.', 'edit-flow' ),
167 ),
168 array(
169 'name' => __( 'Needs Photo', 'edit-flow' ),
170 'slug' => 'needs-photo',
171 'type' => 'checkbox',
172 'description' => __( 'Checked if this post needs a photo.', 'edit-flow' ),
173 ),
174 array(
175 'name' => __( 'Word Count', 'edit-flow' ),
176 'slug' => 'word-count',
177 'type' => 'number',
178 'description' => __( 'Required post length in words.', 'edit-flow' ),
179 ),
180 );
181 // Load the metadata fields if the slugs don't conflict.
182 foreach ( $default_metadata as $args ) {
183 if ( ! term_exists( $args['slug'], self::metadata_taxonomy ) ) {
184 $this->insert_editorial_metadata_term( $args );
185 }
186 }
187 }
188
189 /**
190 * Upgrade our data in case we need to.
191 *
192 * @since 0.7
193 *
194 * @param string $previous_version The previous version number.
195 */
196 public function upgrade( $previous_version ) {
197 global $edit_flow;
198
199 // Upgrade path to v0.7.
200 if ( version_compare( $previous_version, '0.7', '<' ) ) {
201 // Technically we've run this code before so we don't want to auto-install new data.
202 $edit_flow->update_module_option( $this->module->name, 'loaded_once', true );
203 }
204 // Upgrade path to v0.7.4.
205 if ( version_compare( $previous_version, '0.7.4', '<' ) ) {
206 // Editorial metadata descriptions become base64_encoded, instead of maybe json_encoded.
207 $this->upgrade_074_term_descriptions( self::metadata_taxonomy );
208 }
209 }
210
211 /**
212 * Anything that needs to happen on the 'admin_init' hook.
213 *
214 * @since 0.7.4
215 */
216 public function action_admin_init() {
217
218 // Parse the query when we're ordering by an editorial metadata term.
219 add_action( 'parse_query', array( $this, 'action_parse_query' ) );
220 }
221
222 /**
223 * Generate select HTML for all of the metadata types.
224 *
225 * @param object $description The term description object.
226 */
227 public function get_select_html( $description ) {
228 $current_metadata_type = $description->type;
229 $metadata_types = $this->get_supported_metadata_types();
230 ?>
231 <select id="<?php echo esc_attr( self::metadata_taxonomy ); ?>'_type" name="<?php echo esc_attr( self::metadata_taxonomy ); ?>'_type">
232 <?php foreach ( $metadata_types as $metadata_type => $metadata_type_name ) : ?>
233 <option value="<?php echo esc_attr( $metadata_type ); ?>" <?php selected( $metadata_type, $current_metadata_type ); ?>><?php echo esc_html( $metadata_type_name ); ?></option>
234 <?php endforeach; ?>
235 </select>
236 <?php
237 }
238
239 /**
240 * Prepare an array of supported editorial metadata types.
241 *
242 * @return array $supported_metadata_types All of the supported metadata.
243 */
244 public function get_supported_metadata_types() {
245 $supported_metadata_types = array(
246 'checkbox' => __( 'Checkbox', 'edit-flow' ),
247 'date' => __( 'Date', 'edit-flow' ),
248 'location' => __( 'Location', 'edit-flow' ),
249 'number' => __( 'Number', 'edit-flow' ),
250 'paragraph' => __( 'Paragraph', 'edit-flow' ),
251 'text' => __( 'Text', 'edit-flow' ),
252 'user' => __( 'User', 'edit-flow' ),
253 );
254 return $supported_metadata_types;
255 }
256
257 /**
258 * Enqueue relevant admin Javascript.
259 */
260 public function add_admin_scripts() {
261 global $current_screen, $pagenow;
262
263 // Add the metabox date picker JS and CSS.
264 $current_post_type = $this->get_current_post_type();
265 $supported_post_types = $this->get_post_types_for_module( $this->module );
266 if ( in_array( $current_post_type, $supported_post_types, true ) ) {
267 $this->enqueue_datepicker_resources();
268
269 // Now add the rest of the metabox CSS.
270 wp_enqueue_style( 'edit_flow-editorial_metadata-styles', $this->module_url . 'lib/editorial-metadata.css', false, EDIT_FLOW_VERSION, 'all' );
271 }
272 // A bit of custom CSS for the Manage Posts view if we have viewable metadata.
273 if ( 'edit' === $current_screen->base && in_array( $current_post_type, $supported_post_types, true ) ) {
274 $terms = $this->get_editorial_metadata_terms();
275 $viewable_terms = array();
276 foreach ( $terms as $term ) {
277 if ( $term->viewable ) {
278 $viewable_terms[] = $term;
279 }
280 }
281 if ( ! empty( $viewable_terms ) ) {
282 $css_rules = array(
283 '.wp-list-table.fixed .column-author' => array(
284 'min-width: 7em;',
285 'width: auto;',
286 ),
287 '.wp-list-table.fixed .column-tags' => array(
288 'min-width: 7em;',
289 'width: auto;',
290 ),
291 '.wp-list-table.fixed .column-categories' => array(
292 'min-width: 7em;',
293 'width: auto;',
294 ),
295 );
296 foreach ( $viewable_terms as $viewable_term ) {
297 switch ( $viewable_term->type ) {
298 case 'checkbox':
299 case 'number':
300 case 'date':
301 $css_rules[ '.wp-list-table.fixed .column-' . $this->module->slug . '-' . $viewable_term->slug ] = array(
302 'min-width: 6em;',
303 );
304 break;
305 case 'location':
306 case 'text':
307 case 'user':
308 $css_rules[ '.wp-list-table.fixed .column-' . $this->module->slug . '-' . $viewable_term->slug ] = array(
309 'min-width: 7em;',
310 );
311 break;
312 case 'paragraph':
313 $css_rules[ '.wp-list-table.fixed .column-' . $this->module->slug . '-' . $viewable_term->slug ] = array(
314 'min-width: 8em;',
315 );
316 break;
317 }
318 }
319 // Allow users to filter out rules if there's something wonky.
320 $css_rules = apply_filters( 'ef_editorial_metadata_manage_posts_css_rules', $css_rules );
321
322 $css_lines = array();
323 foreach ( (array) $css_rules as $css_property => $rules ) {
324 // Strip any HTML tags that a misbehaving filter hook may have
325 // smuggled into the selector or declarations; this prevents
326 // an injected `</style>` sequence from breaking out of the
327 // style block into arbitrary HTML.
328 $safe_property = wp_strip_all_tags( (string) $css_property );
329 $safe_rules = implode( ' ', array_map( 'wp_strip_all_tags', (array) $rules ) );
330 $css_lines[] = $safe_property . ' {' . $safe_rules . '}';
331 }
332 wp_add_inline_style( 'edit_flow-editorial_metadata-styles', implode( "\n", $css_lines ) );
333 }
334 }
335
336 // Load Javascript specific to the editorial metadata configuration view.
337 if ( $this->is_whitelisted_settings_view( $this->module->name ) ) {
338 wp_enqueue_script( 'jquery-ui-sortable' );
339 wp_enqueue_script( 'edit-flow-editorial-metadata-configure', EDIT_FLOW_URL . 'modules/editorial-metadata/lib/editorial-metadata-configure.js', array( 'jquery', 'jquery-ui-sortable', 'edit-flow-settings-js' ), EDIT_FLOW_VERSION, true );
340 }
341 }
342
343 /**
344 * Register the post metadata taxonomy.
345 */
346 public function register_taxonomy() {
347
348 // We need to make sure taxonomy is registered for all of the post types that support it.
349 $supported_post_types = $this->get_post_types_for_module( $this->module );
350
351 register_taxonomy( self::metadata_taxonomy, $supported_post_types,
352 array(
353 'public' => false,
354 'labels' => array(
355 'name' => _x( 'Editorial Metadata', 'taxonomy general name', 'edit-flow' ),
356 'singular_name' => _x( 'Editorial Metadata', 'taxonomy singular name', 'edit-flow' ),
357 'search_items' => __( 'Search Editorial Metadata', 'edit-flow' ),
358 'popular_items' => __( 'Popular Editorial Metadata', 'edit-flow' ),
359 'all_items' => __( 'All Editorial Metadata', 'edit-flow' ),
360 'edit_item' => __( 'Edit Editorial Metadata', 'edit-flow' ),
361 'update_item' => __( 'Update Editorial Metadata', 'edit-flow' ),
362 'add_new_item' => __( 'Add New Editorial Metadata', 'edit-flow' ),
363 'new_item_name' => __( 'New Editorial Metadata', 'edit-flow' ),
364 ),
365 'rewrite' => false,
366 )
367 );
368 }
369
370 /**
371 * Register editorial metadata post meta keys for REST API support.
372 *
373 * This enables Gutenberg to save metadata values directly through the REST API
374 * when the post is saved, rather than relying on legacy metabox form submission.
375 */
376 private function register_metadata_for_rest_api() {
377 $terms = $this->get_editorial_metadata_terms();
378 $supported_post_types = $this->get_post_types_for_module( $this->module );
379
380 foreach ( $terms as $term ) {
381 $meta_key = $this->get_postmeta_key( $term );
382
383 // Mirror the sanitisation applied on the classic (metabox) save path so the
384 // REST/Gutenberg write path cannot store unsanitised values. Paragraph fields
385 // keep their line breaks; every other type is treated as a single line.
386 $sanitize_callback = ( 'paragraph' === $term->type ) ? 'sanitize_textarea_field' : 'sanitize_text_field';
387
388 foreach ( $supported_post_types as $post_type ) {
389 register_post_meta(
390 $post_type,
391 $meta_key,
392 array(
393 'show_in_rest' => true,
394 'single' => true,
395 'type' => 'string',
396 'sanitize_callback' => $sanitize_callback,
397 'auth_callback' => function ( $allowed, $meta_key, $post_id ) {
398 return current_user_can( 'edit_post', $post_id );
399 },
400 )
401 );
402 }
403 }
404 }
405
406 /*****************************************************
407 * Post meta box generation and processing
408 ****************************************************/
409
410 /**
411 * Load the post metaboxes for all of the post types that are supported.
412 */
413 public function handle_post_metaboxes() {
414 $title = __( 'Editorial Metadata', 'edit-flow' );
415
416 $supported_post_types = $this->get_post_types_for_module( $this->module );
417 foreach ( $supported_post_types as $post_type ) {
418 add_meta_box( self::metadata_taxonomy, $title, array( $this, 'display_meta_box' ), $post_type, 'side' );
419 }
420 }
421
422 /**
423 * Displays HTML output for Editorial Metadata post meta box.
424 *
425 * @param object $post Current post.
426 */
427 public function display_meta_box( $post ) {
428 echo "<div id='" . esc_attr( self::metadata_taxonomy ) . "_meta_box'>";
429 // Add nonce for verification upon save.
430 echo "<input type='hidden' name='" . esc_attr( self::metadata_taxonomy ) . "_nonce' value='" . esc_attr( wp_create_nonce( 'ef-save-metabox' ) ) . "' />";
431
432 if ( current_user_can( 'manage_options' ) ) {
433 // Make the metabox title include a link to edit the Editorial Metadata terms. Logic similar to how Core dashboard widgets work.
434 $url = add_query_arg( 'page', 'ef-editorial-metadata-settings', get_admin_url( null, 'admin.php' ) );
435 echo '<p><a href="' . esc_url( $url ) . '">' . esc_html__( 'Configure', 'edit-flow' ) . '</a></p>';
436 }
437
438 $terms = $this->get_editorial_metadata_terms();
439 if ( ! count( $terms ) ) {
440 $message = __( 'No editorial metadata available.', 'edit-flow' );
441 if ( current_user_can( 'manage_options' ) ) {
442 /* translators: 1: The link to add editorial metadata fields */
443 $message .= sprintf( __( ' <a href="%s">Add fields to get started</a>.', 'edit-flow' ), $this->get_link() );
444 } else {
445 $message .= esc_html__( ' Encourage your site administrator to configure your editorial workflow by adding editorial metadata.', 'edit-flow' );
446 }
447 echo '<p>' . wp_kses( $message, 'a' ) . '</p>';
448 } else {
449 foreach ( $terms as $term ) {
450 $postmeta_key = $this->get_postmeta_key( $term );
451 // Raw stored value; each field type below escapes it for its own output context.
452 $current_metadata = $this->get_postmeta_value( $term, $post->ID );
453 $type = $term->type;
454 $description = $term->description;
455 if ( $description ) {
456 $description_span = "<span class='description'>$description</span>";
457 } else {
458 $description_span = '';
459 }
460 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- This is for the escaping of type.
461 echo "<div class='" . esc_attr( self::metadata_taxonomy ) . ' ' . esc_attr( self::metadata_taxonomy ) . "_$type'>";
462 switch ( $type ) {
463 case 'date':
464 $date_value = '';
465 $time_value = '';
466 $hidden_value = '';
467 if ( ! empty( $current_metadata ) ) {
468 $timestamp = intval( $current_metadata );
469 $date_value = date_i18n( 'M d Y', $timestamp );
470 $hidden_value = date( 'Y-m-d', $timestamp );
471 // Only show time if it's not midnight (00:00).
472 if ( date( 'Hi', $timestamp ) !== '0000' ) {
473 $time_value = date( 'H:i', $timestamp );
474 $hidden_value .= ' ' . $time_value;
475 } else {
476 $hidden_value .= ' 00:00';
477 }
478 }
479 echo '<label for="' . esc_attr( $postmeta_key ) . '_date">' . esc_html( $term->name ) . '</label>';
480 if ( $description_span ) {
481 echo '<label for="' . esc_attr( $postmeta_key ) . '_date">' . wp_kses_post( $description_span ) . '</label>';
482 }
483 echo '<div class="ef-datetime-wrapper">';
484 // The date input has name="$postmeta_key" to pass the empty check in save_meta_box().
485 // The hidden field has the machine-readable value used for actual parsing.
486 echo '<input id="' . esc_attr( $postmeta_key ) . '_date" name="' . esc_attr( $postmeta_key ) . '" type="text" class="date-pick" value="' . esc_attr( $date_value ) . '" placeholder="' . esc_attr__( 'Select date', 'edit-flow' ) . '" />';
487 echo '<input id="' . esc_attr( $postmeta_key ) . '_time" type="time" class="time-pick" value="' . esc_attr( $time_value ) . '" />';
488 echo '</div>';
489 echo '<input type="hidden" id="' . esc_attr( $postmeta_key ) . '_hidden" name="' . esc_attr( $postmeta_key ) . '_hidden" value="' . esc_attr( $hidden_value ) . '" />';
490 break;
491 case 'location':
492 echo '<label for="' . esc_attr( $postmeta_key ) . '">' . esc_html( $term->name ) . '</label>';
493 if ( $description_span ) {
494 echo '<label for="' . esc_attr( $postmeta_key ) . '">' . wp_kses_post( $description_span ) . '</label>';
495 }
496 echo '<input id="' . esc_attr( $postmeta_key ) . '" name="' . esc_attr( $postmeta_key ) . '" type="text" value="' . esc_attr( $current_metadata ) . '" />';
497 if ( ! empty( $current_metadata ) ) {
498 // http_build_query() (unlike add_query_arg()) URL-encodes the value,
499 // so spaces, "&" and "=" cannot alter the URL structure.
500 $google_maps_url = 'https://maps.google.com/?' . http_build_query(
501 array(
502 'q' => $current_metadata,
503 't' => 'm',
504 )
505 );
506 $google_maps_text = sprintf(
507 /* translators: %s: the location entered in the editorial metadata field. */
508 __( 'View “%s” on Google Maps', 'edit-flow' ),
509 $current_metadata
510 );
511 // Escape late: the label is translatable and filterable (gettext), so escape the
512 // whole composed string at output rather than trusting either source.
513 echo '<div><a href="' . esc_url( $google_maps_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $google_maps_text ) . '</a></div>';
514 }
515 break;
516 case 'text':
517 echo '<label for="' . esc_attr( $postmeta_key ) . '">' . esc_html( $term->name ) . wp_kses_post( $description_span ) . '</label>';
518 echo '<input id="' . esc_attr( $postmeta_key ) . '" name="' . esc_attr( $postmeta_key ) . '"type=text value="' . esc_attr( $current_metadata ) . '" />';
519 break;
520 case 'paragraph':
521 echo '<label for="' . esc_attr( $postmeta_key ) . '">' . esc_html( $term->name ) . wp_kses_post( $description_span ) . '</label>';
522 echo '<textarea id="' . esc_attr( $postmeta_key ) . '" name="' . esc_attr( $postmeta_key ) . '">' . esc_textarea( $current_metadata ) . '</textarea>';
523 break;
524 case 'checkbox':
525 echo '<label for="' . esc_attr( $postmeta_key ) . '">' . esc_html( $term->name ) . wp_kses_post( $description_span ) . '</label>';
526 echo '<input id="' . esc_attr( $postmeta_key ) . '" name="' . esc_attr( $postmeta_key ) . '" type=checkbox value=1 "' . checked( $current_metadata, 1, false ) . '" />';
527 break;
528 case 'user':
529 echo '<label for="' . esc_attr( $postmeta_key ) . '">' . esc_html( $term->name ) . wp_kses_post( $description_span ) . '</label>';
530 $user_dropdown_args = array(
531 'show_option_all' => __( '-- Select a user --', 'edit-flow' ),
532 'name' => $postmeta_key,
533 'selected' => $current_metadata,
534 );
535 $user_dropdown_args = apply_filters( 'ef_editorial_metadata_user_dropdown_args', $user_dropdown_args );
536 wp_dropdown_users( $user_dropdown_args );
537 break;
538 case 'number':
539 echo '<label for="' . esc_attr( $postmeta_key ) . '">' . esc_html( $term->name ) . wp_kses_post( $description_span ) . '</label>';
540 echo '<input id="' . esc_attr( $postmeta_key ) . '" name="' . esc_attr( $postmeta_key ) . '"type=text value="' . esc_attr( $current_metadata ) . '" />';
541 break;
542 default:
543 echo '<p>' . esc_html__( 'This editorial metadata type is not yet supported.', 'edit-flow' ) . '</p>';
544 }
545 echo '</div>';
546 echo "<div class='clear'></div>";
547 } // Done iterating through metadata terms.
548 }
549 echo '</div>';
550 }
551
552 /**
553 * Show date or datetime.
554 *
555 * @since 0.8
556 *
557 * @param int $current_date The timestamp to display.
558 * @return string The formatted date string.
559 */
560 private function show_date_or_datetime( $current_date ) {
561
562 if ( date( 'Hi', $current_date ) == '0000' ) {
563 return date_i18n( 'M d Y', $current_date );
564 } else {
565 return date_i18n( 'M d Y H:i', $current_date );
566 }
567 }
568
569 /**
570 * Save editorial metadata values from POST data.
571 * Called from save_post hook for Classic Editor form submissions.
572 *
573 * Note: In Gutenberg, metadata is saved via REST API (see register_metadata_for_rest_api).
574 *
575 * @param int $post_id Post ID.
576 */
577 private function save_meta_box_data( $post_id ) {
578 // Verify nonce.
579 $nonce_key = self::metadata_taxonomy . '_nonce';
580 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce value passed directly to wp_verify_nonce().
581 if ( ! isset( $_POST[ $nonce_key ] ) || ! wp_verify_nonce( $_POST[ $nonce_key ], 'ef-save-metabox' ) ) {
582 return;
583 }
584
585 // Verify user can edit.
586 if ( ! current_user_can( 'edit_post', $post_id ) ) {
587 return;
588 }
589
590 $terms = $this->get_editorial_metadata_terms();
591
592 foreach ( $terms as $term ) {
593 $key = $this->get_postmeta_key( $term );
594 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized based on type below.
595 $new_metadata = isset( $_POST[ $key ] ) ? $_POST[ $key ] : '';
596 $type = $term->type;
597
598 if ( empty( $new_metadata ) ) {
599 delete_post_meta( $post_id, $key );
600 } else {
601 if ( 'date' === $type ) {
602 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Validated by DateTime::createFromFormat below.
603 $date_to_parse = isset( $_POST[ $key . '_hidden' ] ) ? $_POST[ $key . '_hidden' ] : '';
604 $date = DateTime::createFromFormat( 'Y-m-d H:i', $date_to_parse );
605
606 if ( false !== $date ) {
607 $new_metadata = $date->getTimestamp();
608 } else {
609 $new_metadata = strtotime( $new_metadata );
610 }
611 }
612
613 if ( 'number' === $type ) {
614 $new_metadata = (int) $new_metadata;
615 }
616
617 $new_metadata = wp_strip_all_tags( $new_metadata );
618 update_post_meta( $post_id, $key, $new_metadata );
619 }
620
621 do_action( 'ef_editorial_metadata_field_updated', $key, $new_metadata, $post_id, $type );
622 }
623 }
624
625 /**
626 * Save any values in the editorial metadata post meta box.
627 *
628 * @param int $id Unique ID for the post being saved.
629 * @param object $post Post object.
630 */
631 public function save_meta_box( $id, $post ) {
632 // Skip autosave.
633 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
634 return $id;
635 }
636
637 // Skip if post type not supported.
638 if ( ! in_array( $post->post_type, $this->get_post_types_for_module( $this->module ), true ) ) {
639 return $id;
640 }
641
642 // Save the data.
643 $this->save_meta_box_data( $id );
644 }
645
646 /**
647 * Generate a unique key based on the term.
648 *
649 * @param object $term Term object.
650 * @return string $postmeta_key Unique key.
651 */
652 public function get_postmeta_key( $term ) {
653 $key = self::metadata_postmeta_key;
654 $type = $term->type;
655 $prefix = "{$key}_{$type}";
656 $postmeta_key = "{$prefix}_" . ( is_object( $term ) ? $term->slug : $term );
657 return $postmeta_key;
658 }
659
660 /**
661 * Returns the value for the given metadata.
662 *
663 * @param object|string|int $term The term object, slug or ID for the metadata field term.
664 * @param int $post_id The ID of the post.
665 * @return mixed The metadata value.
666 */
667 public function get_postmeta_value( $term, $post_id ) {
668 if ( ! is_object( $term ) ) {
669 if ( is_int( $term ) ) {
670 $term = $this->get_editorial_metadata_term_by( 'id', $term );
671 } else {
672 $term = $this->get_editorial_metadata_term_by( 'slug', $term );
673 }
674 }
675 $postmeta_key = $this->get_postmeta_key( $term );
676 return get_metadata( 'post', $post_id, $postmeta_key, true );
677 }
678
679 /**
680 * Get all of the editorial metadata terms as objects and sort by position.
681 *
682 * @todo Figure out what we should do with the filter.
683 *
684 * @param array $filter_args Filter to specific arguments.
685 * @return array $ordered_terms The terms as they should be ordered.
686 */
687 public function get_editorial_metadata_terms( $filter_args = array() ) {
688
689 // Try to fetch from internal object cache.
690 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- Used for cache key generation only.
691 $arg_hash = md5( serialize( $filter_args ) );
692 if ( isset( $this->editorial_metadata_terms_cache[ $arg_hash ] ) ) {
693 return $this->editorial_metadata_terms_cache[ $arg_hash ];
694 }
695
696 $terms = get_terms( array(
697 'taxonomy' => self::metadata_taxonomy,
698 'orderby' => apply_filters( 'ef_editorial_metadata_term_order', 'name' ),
699 'hide_empty' => false,
700 ));
701
702 $ordered_terms = array();
703 $hold_to_end = array();
704 // Order the terms.
705 foreach ( $terms as $key => $term ) {
706
707 // Unencode and set all of our pseudo term meta because we need the position and viewable if they exist.
708 // First do an array_merge() on the term object to make sure the keys exist, then array_merge()
709 // any values that may already exist.
710 $unencoded_description = $this->get_unencoded_description( $term->description );
711 $defaults = array(
712 'description' => '',
713 'viewable' => false,
714 'position' => false,
715 );
716 $term = array_merge( $defaults, (array) $term );
717 if ( is_array( $unencoded_description ) ) {
718 $term = array_merge( $term, $unencoded_description );
719 }
720 $term = (object) $term;
721 // We used to store the description field in a funny way.
722 if ( isset( $term->desc ) ) {
723 $term->description = $term->desc;
724 unset( $term->desc );
725 }
726 // Only add the term to the ordered array if it has a set position and doesn't conflict with another key.
727 // Otherwise, hold it for later.
728 if ( $term->position && ! array_key_exists( $term->position, $ordered_terms ) ) {
729 $ordered_terms[ (int) $term->position ] = $term;
730 } else {
731 $hold_to_end[] = $term;
732 }
733 }
734 // Sort the items numerically by key.
735 ksort( $ordered_terms, SORT_NUMERIC );
736 // Append all of the terms that didn't have an existing position.
737 foreach ( $hold_to_end as $unpositioned_term ) {
738 $ordered_terms[] = $unpositioned_term;
739 }
740
741 // If filter arguments were passed, do our filtering.
742 $ordered_terms = wp_filter_object_list( $ordered_terms, $filter_args );
743
744 // Set the internal object cache.
745 $this->editorial_metadata_terms_cache[ $arg_hash ] = $ordered_terms;
746
747 return $ordered_terms;
748 }
749
750 /**
751 * Returns a term for single metadata field.
752 *
753 * @param string $field The field to match (id, slug, or name).
754 * @param int|string $value The value to match against the field.
755 * @return object|false $term Term's object representation or false if not found.
756 */
757 public function get_editorial_metadata_term_by( $field, $value ) {
758
759 if ( ! in_array( $field, array( 'id', 'slug', 'name' ) ) ) {
760 return false;
761 }
762
763 if ( 'id' == $field ) {
764 $field = 'term_id';
765 }
766
767 $terms = $this->get_editorial_metadata_terms();
768 $term = wp_filter_object_list( $terms, array( $field => $value ) );
769
770 if ( ! empty( $term ) ) {
771 return array_shift( $term );
772 } else {
773 return false;
774 }
775 }
776
777 /**
778 * Register editorial metadata fields as columns in the manage posts view.
779 *
780 * Only adds columns for the currently active post types - logic controlled in $this->init().
781 *
782 * @since 0.7
783 * @uses apply_filters( 'manage_posts_columns' ) in wp-admin/includes/class-wp-posts-list-table.php
784 *
785 * @param array $posts_columns Existing post columns prepared by WP_List_Table.
786 * @return array $posts_columns Previous post columns with the new values.
787 */
788 public function filter_manage_posts_columns( $posts_columns ) {
789 $screen = get_current_screen();
790 if ( $screen ) {
791 add_filter( "manage_{$screen->id}_sortable_columns", array( $this, 'filter_manage_posts_sortable_columns' ) );
792 $terms = $this->get_editorial_metadata_terms( array( 'viewable' => true ) );
793 foreach ( $terms as $term ) {
794 // Prefixing slug with module slug because it isn't stored prefixed and we want to avoid collisions.
795 $key = $this->module->slug . '-' . $term->slug;
796 $posts_columns[ $key ] = $term->name;
797 }
798 }
799 return $posts_columns;
800 }
801
802 /**
803 * Register any viewable date editorial metadata as a sortable column.
804 *
805 * @since 0.7.4
806 *
807 * @param array $sortable_columns Any existing sortable columns (e.g. Title).
808 * @return array $sortable_columns Sortable columns with editorial metadata date fields added.
809 */
810 public function filter_manage_posts_sortable_columns( $sortable_columns ) {
811
812 $terms = $this->get_editorial_metadata_terms( array(
813 'viewable' => true,
814 'type' => 'date',
815 ) );
816 foreach ( $terms as $term ) {
817 // Prefixing slug with module slug because it isn't stored prefixed and we want to avoid collisions.
818 $key = $this->module->slug . '-' . $term->slug;
819 $sortable_columns[ $key ] = $key;
820 }
821 return $sortable_columns;
822 }
823
824 /**
825 * If we're ordering by a sortable column, let's modify the query.
826 *
827 * @since 0.7.4
828 *
829 * @param WP_Query $query The query object.
830 */
831 public function action_parse_query( $query ) {
832
833 if ( is_admin() && false !== stripos( get_query_var( 'orderby' ), $this->module->slug ) ) {
834 $term_slug = sanitize_key( str_replace( $this->module->slug . '-', '', get_query_var( 'orderby' ) ) );
835 $term = $this->get_editorial_metadata_term_by( 'slug', $term_slug );
836 $meta_key = $this->get_postmeta_key( $term );
837 set_query_var( 'meta_key', $meta_key );
838 set_query_var( 'orderby', 'meta_value_num' );
839 }
840 }
841
842 /**
843 * Handle the output of an editorial metadata custom column.
844 *
845 * Logic for the post types this is called on is controlled in $this->init().
846 *
847 * @since 0.7
848 * @uses do_action( 'manage_posts_custom_column' ) in wp-admin/includes/class-wp-posts-list-table.php
849 *
850 * @param string $column_name Unique string for the column.
851 * @param int $post_id ID for the post of the row.
852 */
853 public function action_manage_posts_custom_column( $column_name, $post_id ) {
854
855 $terms = $this->get_editorial_metadata_terms();
856 // We're looking for the proper term to display its saved value.
857 foreach ( $terms as $term ) {
858 $key = $this->module->slug . '-' . $term->slug;
859 if ( $column_name != $key ) {
860 continue;
861 }
862
863 $current_metadata = $this->get_postmeta_value( $term, $post_id );
864 echo esc_html( $this->generate_editorial_metadata_term_output( $term, $current_metadata ) );
865 }
866 }
867
868 /**
869 * If the Edit Flow Calendar is enabled, add viewable Editorial Metadata terms.
870 *
871 * @since 0.7
872 * @uses apply_filters( 'ef_calendar_item_information_fields' )
873 *
874 * @param array $calendar_fields Additional data fields to include on the calendar.
875 * @param int $post_id Unique ID for the post data we're building.
876 * @return array $calendar_fields Calendar fields with our viewable Editorial Metadata added.
877 */
878 public function filter_calendar_item_fields( $calendar_fields, $post_id ) {
879
880
881 // Make sure we respect which post type we're on.
882 if ( ! in_array( get_post_type( $post_id ), $this->get_post_types_for_module( $this->module ) ) ) {
883 return $calendar_fields;
884 }
885
886 $terms = $this->get_editorial_metadata_terms( array( 'viewable' => true ) );
887
888 foreach ( $terms as $term ) {
889 $key = $this->module->slug . '-' . $term->slug;
890
891 // Default values.
892 $current_metadata = $this->get_postmeta_value( $term, $post_id );
893 $term_data = array(
894 'label' => $term->name,
895 'value' => $this->generate_editorial_metadata_term_output( $term, $current_metadata ),
896 );
897 $term_data['editable'] = true;
898 $term_data['type'] = $term->type;
899 $calendar_fields[ $key ] = $term_data;
900 }
901 return $calendar_fields;
902 }
903
904 /**
905 * If the Edit Flow Story Budget is enabled, register our viewable terms as columns.
906 *
907 * @since 0.7
908 * @uses apply_filters( 'ef_story_budget_term_columns' )
909 *
910 * @param array $term_columns The existing columns on the story budget.
911 * @return array $term_columns Term columns with viewable Editorial Metadata terms.
912 */
913 public function filter_story_budget_term_columns( $term_columns ) {
914
915 $terms = $this->get_editorial_metadata_terms( array( 'viewable' => true ) );
916 foreach ( $terms as $term ) {
917 // Prefixing slug with module slug because it isn't stored prefixed and we want to avoid collisions.
918 $key = $this->module->slug . '-' . $term->slug;
919 // Switch to underscores.
920 $key = str_replace( '-', '_', $key );
921 $term_columns[ $key ] = $term->name;
922 }
923 return $term_columns;
924 }
925
926 /**
927 * If the Edit Flow Story Budget is enabled, generate metadata column values.
928 *
929 * @since 0.7
930 * @uses apply_filters( 'ef_story_budget_term_column_value' )
931 *
932 * @param string $column_name Name of the column, as registered with EF_Story_Budget::register_term_columns.
933 * @param object $post The post we're displaying.
934 * @param object $parent_term The parent term for the term column.
935 * @return string The column value or the original column name.
936 */
937 public function filter_story_budget_term_column_values( $column_name, $post, $parent_term ) {
938
939 $local_column_name = str_replace( '_', '-', $column_name );
940 // Don't accidentally handle values not our own.
941 if ( false === strpos( $local_column_name, $this->module->slug ) ) {
942 return $column_name;
943 }
944
945 $term_slug = str_replace( $this->module->slug . '-', '', $local_column_name );
946 $term = $this->get_editorial_metadata_term_by( 'slug', $term_slug );
947
948 // Don't allow non-viewable term data to be displayed.
949 if ( ! $term->viewable ) {
950 return $column_name;
951 }
952
953 $current_metadata = $this->get_postmeta_value( $term, $post->ID );
954 $output = $this->generate_editorial_metadata_term_output( $term, $current_metadata );
955
956 return $output;
957 }
958
959 /**
960 * Generate the presentational output for an editorial metadata term.
961 *
962 * @since 0.8
963 *
964 * @param object $term The editorial metadata term.
965 * @param mixed $pm_value The post meta value.
966 * @return string $html How the term should be rendered.
967 */
968 private function generate_editorial_metadata_term_output( $term, $pm_value ) {
969
970 $output = '';
971 switch ( $term->type ) {
972 case 'date':
973 if ( empty( $pm_value ) ) {
974 break;
975 }
976
977 // All day vs. day and time.
978 $date = date( get_option( 'date_format' ), $pm_value );
979 $time = date( get_option( 'time_format' ), $pm_value );
980 if ( '0000' == date( 'Hi', $pm_value ) ) {
981 $pm_value = $date;
982 } else {
983 /* translators: 1: date, 2: time */
984 $pm_value = sprintf( __( '%1$s at %2$s', 'edit-flow' ), $date, $time );
985 }
986 $output = esc_html( $pm_value );
987 break;
988 case 'location':
989 case 'text':
990 case 'number':
991 case 'paragraph':
992 if ( $pm_value ) {
993 $output = esc_html( $pm_value );
994 }
995 break;
996 case 'checkbox':
997 if ( $pm_value ) {
998 $output = __( 'Yes', 'edit-flow' );
999 } else {
1000 $output = __( 'No', 'edit-flow' );
1001 }
1002 break;
1003 case 'user':
1004 if ( empty( $pm_value ) ) {
1005 break;
1006 }
1007 $userdata = get_user_by( 'id', $pm_value );
1008 if ( is_object( $userdata ) ) {
1009 $output = esc_html( $userdata->display_name );
1010 }
1011 break;
1012 default:
1013 break;
1014 }
1015 return $output;
1016 }
1017
1018 /**
1019 * Update an existing editorial metadata term if the term_id exists.
1020 *
1021 * @since 0.7
1022 *
1023 * @param int $term_id The term's unique ID.
1024 * @param array $args Any values that need to be updated for the term.
1025 * @return object|WP_Error $updated_term The updated term or a WP_Error object if something disastrous happened.
1026 */
1027 public function update_editorial_metadata_term( $term_id, $args ) {
1028
1029 $new_args = array();
1030 $old_term = $this->get_editorial_metadata_term_by( 'id', $term_id );
1031 if ( $old_term ) {
1032 $old_args = array(
1033 'position' => $old_term->position,
1034 'name' => $old_term->name,
1035 'slug' => $old_term->slug,
1036 'description' => $old_term->description,
1037 'type' => $old_term->type,
1038 'viewable' => $old_term->viewable,
1039 );
1040 }
1041 $new_args = array_merge( $old_args, $args );
1042
1043 // We're encoding metadata that isn't supported by default in the term's description field.
1044 $args_to_encode = array(
1045 'description' => $new_args['description'],
1046 'position' => $new_args['position'],
1047 'type' => $new_args['type'],
1048 'viewable' => $new_args['viewable'],
1049 );
1050 $encoded_description = $this->get_encoded_description( $args_to_encode );
1051 $new_args['description'] = $encoded_description;
1052
1053 $updated_term = wp_update_term( $term_id, self::metadata_taxonomy, $new_args );
1054
1055 // Reset the internal object cache.
1056 $this->editorial_metadata_terms_cache = array();
1057
1058 $updated_term = $this->get_editorial_metadata_term_by( 'id', $term_id );
1059 return $updated_term;
1060 }
1061
1062 /**
1063 * Insert a new editorial metadata term.
1064 *
1065 * @todo Handle conflicts with existing terms at that position (if relevant).
1066 *
1067 * @since 0.7
1068 *
1069 * @param array $args Arguments for creating the term.
1070 * @return object|WP_Error The new term or a WP_Error object.
1071 */
1072 public function insert_editorial_metadata_term( $args ) {
1073
1074
1075 // Term is always added to the end of the list.
1076 $default_position = count( $this->get_editorial_metadata_terms() ) + 2;
1077 $defaults = array(
1078 'position' => $default_position,
1079 'name' => '',
1080 'slug' => '',
1081 'description' => '',
1082 'type' => '',
1083 'viewable' => false,
1084 );
1085 $args = array_merge( $defaults, $args );
1086 $term_name = $args['name'];
1087 unset( $args['name'] );
1088
1089 // We're encoding metadata that isn't supported by default in the term's description field.
1090 $args_to_encode = array(
1091 'description' => $args['description'],
1092 'position' => $args['position'],
1093 'type' => $args['type'],
1094 'viewable' => $args['viewable'],
1095 );
1096 $encoded_description = $this->get_encoded_description( $args_to_encode );
1097 $args['description'] = $encoded_description;
1098
1099 $inserted_term = wp_insert_term( $term_name, self::metadata_taxonomy, $args );
1100
1101 // Reset the internal object cache.
1102 $this->editorial_metadata_terms_cache = array();
1103
1104 return $inserted_term;
1105 }
1106
1107 /**
1108 * Settings and other management code.
1109 */
1110
1111 /**
1112 * Delete an existing editorial metadata term.
1113 *
1114 * @since 0.7
1115 *
1116 * @param int $term_id The term we want deleted.
1117 * @return bool $result Whether or not the term was deleted.
1118 */
1119 public function delete_editorial_metadata_term( $term_id ) {
1120 $result = wp_delete_term( $term_id, self::metadata_taxonomy );
1121
1122 // Reset the internal object cache.
1123 $this->editorial_metadata_terms_cache = array();
1124
1125 return $result;
1126 }
1127
1128 /**
1129 * Generate a link to one of the editorial metadata actions.
1130 *
1131 * @since 0.7
1132 *
1133 * @param array $args (optional) Action and any query args to add to the URL.
1134 * @return string $link Direct link to complete the action.
1135 */
1136 public function get_link( $args = array() ) {
1137 if ( ! isset( $args['action'] ) ) {
1138 $args['action'] = '';
1139 }
1140 if ( ! isset( $args['page'] ) ) {
1141 $args['page'] = $this->module->settings_slug;
1142 }
1143 // Add other things we may need depending on the action.
1144 switch ( $args['action'] ) {
1145 case 'make-viewable':
1146 case 'make-hidden':
1147 case 'delete-term':
1148 $args['nonce'] = wp_create_nonce( $args['action'] );
1149 break;
1150 default:
1151 break;
1152 }
1153 return add_query_arg( $args, get_admin_url( null, 'admin.php' ) );
1154 }
1155
1156 /**
1157 * Handles a request to add a new piece of editorial metadata.
1158 */
1159 public function handle_add_editorial_metadata() {
1160 // phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended -- Nonce verified below.
1161 if ( ! isset( $_POST['submit'], $_POST['form-action'], $_GET['page'] )
1162 || $_GET['page'] != $this->module->settings_slug || 'add-term' != $_POST['form-action'] ) {
1163 return;
1164 }
1165 // phpcs:enable WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended
1166
1167 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce value passed directly to wp_verify_nonce().
1168 if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'editorial-metadata-add-nonce' ) ) {
1169 wp_die( esc_html( $this->module->messages['nonce-failed'] ) );
1170 }
1171
1172 if ( ! current_user_can( 'manage_options' ) ) {
1173 wp_die( esc_html( $this->module->messages['invalid-permissions'] ) );
1174 }
1175
1176 // Sanitize all of the user-entered values.
1177 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized by sanitize_text_field().
1178 $term_name = isset( $_POST['metadata_name'] ) ? sanitize_text_field( trim( $_POST['metadata_name'] ) ) : '';
1179 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized by sanitize_title().
1180 $term_slug = ( ! empty( $_POST['metadata_slug'] ) ) ? sanitize_title( $_POST['metadata_slug'] ) : sanitize_title( $term_name );
1181 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized by wp_filter_nohtml_kses().
1182 $term_description = isset( $_POST['metadata_description'] ) ? stripslashes( wp_filter_nohtml_kses( trim( $_POST['metadata_description'] ) ) ) : '';
1183 $term_type = isset( $_POST['metadata_type'] ) ? sanitize_key( $_POST['metadata_type'] ) : '';
1184
1185 EditFlow()->settings->form_errors = array();
1186
1187 /**
1188 * Form validation for adding new editorial metadata term.
1189 *
1190 * Details:
1191 * - "name", "slug", and "type" are required fields
1192 * - "description" can accept a limited amount of HTML, and is optional
1193 */
1194 // Field is required.
1195 if ( empty( $term_name ) ) {
1196 EditFlow()->settings->form_errors['name'] = __( 'Please enter a name for the editorial metadata.', 'edit-flow' );
1197 }
1198 // Field is required.
1199 if ( empty( $term_slug ) ) {
1200 EditFlow()->settings->form_errors['slug'] = __( 'Please enter a slug for the editorial metadata.', 'edit-flow' );
1201 }
1202 if ( term_exists( $term_slug ) ) {
1203 EditFlow()->settings->form_errors['name'] = __( 'Name conflicts with existing term. Please choose another.', 'edit-flow' );
1204 }
1205 // Check to ensure a term with the same name doesn't exist.
1206 if ( $this->get_editorial_metadata_term_by( 'name', $term_name, self::metadata_taxonomy ) ) {
1207 EditFlow()->settings->form_errors['name'] = __( 'Name already in use. Please choose another.', 'edit-flow' );
1208 }
1209 // Check to ensure a term with the same slug doesn't exist.
1210 if ( $this->get_editorial_metadata_term_by( 'slug', $term_slug ) ) {
1211 EditFlow()->settings->form_errors['slug'] = __( 'Slug already in use. Please choose another.', 'edit-flow' );
1212 }
1213 // Check to make sure the status doesn't already exist as another term because otherwise we'd get a weird slug.
1214 // Check that the term name doesn't exceed 200 chars.
1215 if ( strlen( $term_name ) > 200 ) {
1216 EditFlow()->settings->form_errors['name'] = __( 'Name cannot exceed 200 characters. Please try a shorter name.', 'edit-flow' );
1217 }
1218 // Metadata type needs to pass our whitelist check.
1219 $metadata_types = $this->get_supported_metadata_types();
1220 if ( empty( $_POST['metadata_type'] ) || ! isset( $metadata_types[ $_POST['metadata_type'] ] ) ) {
1221 EditFlow()->settings->form_errors['type'] = __( 'Please select a valid metadata type.', 'edit-flow' );
1222 }
1223 // Metadata viewable needs to be a valid Yes or No.
1224 $term_viewable = false;
1225 if ( isset( $_POST['metadata_viewable'] ) && 'yes' == $_POST['metadata_viewable'] ) {
1226 $term_viewable = true;
1227 }
1228
1229 // Kick out if there are any errors.
1230 if ( count( EditFlow()->settings->form_errors ) ) {
1231 $_REQUEST['error'] = 'form-error';
1232 return;
1233 }
1234
1235 // Try to add the status.
1236 $args = array(
1237 'name' => $term_name,
1238 'description' => $term_description,
1239 'slug' => $term_slug,
1240 'type' => $term_type,
1241 'viewable' => $term_viewable,
1242 );
1243 $return = $this->insert_editorial_metadata_term( $args );
1244 if ( is_wp_error( $return ) ) {
1245 wp_die( esc_html__( 'Error adding term.', 'edit-flow' ) );
1246 }
1247
1248 $redirect_url = add_query_arg( array(
1249 'page' => $this->module->settings_slug,
1250 'message' => 'term-added',
1251 ), get_admin_url( null, 'admin.php' ) );
1252 // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- Redirect URL is constructed internally.
1253 wp_redirect( $redirect_url );
1254 exit;
1255 }
1256
1257 /**
1258 * Handles a request to edit an editorial metadata
1259 */
1260 public function handle_edit_editorial_metadata() {
1261 if ( ! isset( $_POST['submit'], $_GET['page'], $_GET['action'], $_GET['term-id'] )
1262 || $_GET['page'] != $this->module->settings_slug || 'edit-term' != $_GET['action'] ) {
1263 return;
1264 }
1265
1266 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce value passed directly to wp_verify_nonce().
1267 if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'editorial-metadata-edit-nonce' ) ) {
1268 wp_die( esc_html( $this->module->messages['nonce-failed'] ) );
1269 }
1270
1271 if ( ! current_user_can( 'manage_options' ) ) {
1272 wp_die( esc_html( $this->module->messages['invalid-permissions'] ) );
1273 }
1274
1275 $existing_term = $this->get_editorial_metadata_term_by( 'id', (int) $_GET['term-id'] );
1276 if ( ! $existing_term ) {
1277 wp_die( esc_html( $this->module->messages['term-missing'] ) );
1278 }
1279
1280 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized by sanitize_text_field().
1281 $new_name = isset( $_POST['name'] ) ? sanitize_text_field( trim( $_POST['name'] ) ) : '';
1282 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized by wp_filter_nohtml_kses().
1283 $new_description = isset( $_POST['description'] ) ? stripslashes( wp_filter_nohtml_kses( trim( $_POST['description'] ) ) ) : '';
1284
1285 /**
1286 * Form validation for editing editorial metadata term
1287 *
1288 * Details
1289 * - "name", "slug", and "type" are required fields
1290 * - "description" can accept a limited amount of HTML, and is optional
1291 */
1292 EditFlow()->settings->form_errors = array();
1293 // Check if name field was filled in.
1294 if ( empty( $new_name ) ) {
1295 EditFlow()->settings->form_errors['name'] = __( 'Please enter a name for the editorial metadata', 'edit-flow' );
1296 }
1297
1298 // Check that the name isn't numeric.
1299 if ( is_numeric( $new_name ) ) {
1300 EditFlow()->settings->form_errors['name'] = __( 'Please enter a valid, non-numeric name for the editorial metadata.', 'edit-flow' );
1301 }
1302
1303 $term_exists = term_exists( sanitize_title( $new_name ) );
1304 if ( $term_exists && (int) $term_exists !== (int) $existing_term->term_id ) {
1305 EditFlow()->settings->form_errors['name'] = __( 'Metadata name conflicts with existing term. Please choose another.', 'edit-flow' );
1306 }
1307
1308 // Check to ensure a term with the same name doesn't exist.
1309 $search_term = $this->get_editorial_metadata_term_by( 'name', $new_name );
1310 if ( is_object( $search_term ) && (int) $search_term->term_id !== (int) $existing_term->term_id ) {
1311 EditFlow()->settings->form_errors['name'] = __( 'Name already in use. Please choose another.', 'edit-flow' );
1312 }
1313 // Or that the term name doesn't map to an existing term's slug.
1314 $search_term = $this->get_editorial_metadata_term_by( 'slug', sanitize_title( $new_name ) );
1315 if ( is_object( $search_term ) && (int) $search_term->term_id !== (int) $existing_term->term_id ) {
1316 EditFlow()->settings->form_errors['name'] = __( 'Name conflicts with slug for another term. Please choose something else.', 'edit-flow' );
1317 }
1318
1319 // Check that the term name doesn't exceed 200 chars.
1320 if ( strlen( $new_name ) > 200 ) {
1321 EditFlow()->settings->form_errors['name'] = __( 'Name cannot exceed 200 characters. Please try a shorter name.', 'edit-flow' );
1322 }
1323 // Make sure the viewable state is valid.
1324 $new_viewable = false;
1325 if ( isset( $_POST['viewable'] ) && 'yes' == $_POST['viewable'] ) {
1326 $new_viewable = true;
1327 }
1328
1329 // Kick out if there are any errors.
1330 if ( count( EditFlow()->settings->form_errors ) ) {
1331 $_REQUEST['error'] = 'form-error';
1332 return;
1333 }
1334
1335 // Try to add the metadata term.
1336 $args = array(
1337 'name' => $new_name,
1338 'description' => $new_description,
1339 'viewable' => $new_viewable,
1340 );
1341 $return = $this->update_editorial_metadata_term( $existing_term->term_id, $args );
1342 if ( is_wp_error( $return ) ) {
1343 wp_die( esc_html__( 'Error updating term.', 'edit-flow' ) );
1344 }
1345
1346 $redirect_url = add_query_arg( array(
1347 'page' => $this->module->settings_slug,
1348 'message' => 'term-updated',
1349 ), get_admin_url( null, 'admin.php' ) );
1350 // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- Redirect URL is constructed internally.
1351 wp_redirect( $redirect_url );
1352 exit;
1353 }
1354
1355 /**
1356 * Handle a $_GET request to change the visibility of an Editorial Metadata term.
1357 *
1358 * @since 0.7
1359 */
1360 public function handle_change_editorial_metadata_visibility() {
1361 // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Nonce verified below.
1362 // Check that the current GET request is our GET request.
1363 if ( ! isset( $_GET['page'], $_GET['action'], $_GET['term-id'], $_GET['nonce'] )
1364 || $_GET['page'] !== $this->module->settings_slug || ! in_array( $_GET['action'], array( 'make-viewable', 'make-hidden' ), true ) ) {
1365 return;
1366 }
1367 // phpcs:enable WordPress.Security.NonceVerification.Recommended
1368
1369 // Check for proper nonce.
1370 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce value passed directly to wp_verify_nonce().
1371 if ( ! isset( $_GET['nonce'] ) || ( ! wp_verify_nonce( $_GET['nonce'], 'make-viewable' ) && ! wp_verify_nonce( $_GET['nonce'], 'make-hidden' ) ) ) {
1372 wp_die( esc_html( $this->module->messages['nonce-failed'] ) );
1373 }
1374
1375 // Only allow users with the proper caps.
1376 if ( ! current_user_can( 'manage_options' ) ) {
1377 wp_die( esc_html( $this->module->messages['invalid-permissions'] ) );
1378 }
1379
1380 $term_id = (int) $_GET['term-id'];
1381 $args = array();
1382 if ( 'make-viewable' == $_GET['action'] ) {
1383 $args['viewable'] = true;
1384 } elseif ( 'make-hidden' == $_GET['action'] ) {
1385 $args['viewable'] = false;
1386 }
1387
1388 $return = $this->update_editorial_metadata_term( $term_id, $args );
1389 if ( is_wp_error( $return ) ) {
1390 wp_die( esc_html__( 'Error updating term.', 'edit-flow' ) );
1391 }
1392
1393 $redirect_url = $this->get_link( array( 'message' => 'term-visibility-changed' ) );
1394 // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- Redirect URL is constructed internally.
1395 wp_redirect( $redirect_url );
1396 exit;
1397 }
1398
1399 /**
1400 * Handle the request to update a given Editorial Metadata term via inline edit.
1401 *
1402 * @since 0.7
1403 */
1404 public function handle_ajax_inline_save_term() {
1405 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce value passed directly to wp_verify_nonce().
1406 if ( ! isset( $_POST['inline_edit'] ) || ! wp_verify_nonce( $_POST['inline_edit'], 'editorial-metadata-inline-edit-nonce' ) ) {
1407 wp_die( esc_html( $this->module->messages['nonce-failed'] ) );
1408 }
1409
1410 if ( ! current_user_can( 'manage_options' ) ) {
1411 wp_die( esc_html( $this->module->messages['invalid-permissions'] ) );
1412 }
1413
1414 $term_id = isset( $_POST['term_id'] ) ? (int) $_POST['term_id'] : 0;
1415 $existing_term = $this->get_editorial_metadata_term_by( 'id', $term_id );
1416 if ( ! $existing_term ) {
1417 wp_die( esc_html( $this->module->messages['term-missing'] ) );
1418 }
1419
1420 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized by sanitize_text_field().
1421 $metadata_name = isset( $_POST['name'] ) ? sanitize_text_field( trim( $_POST['name'] ) ) : '';
1422 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized by wp_filter_nohtml_kses().
1423 $metadata_description = isset( $_POST['description'] ) ? stripslashes( wp_filter_nohtml_kses( trim( $_POST['description'] ) ) ) : '';
1424
1425 /**
1426 * Form validation for editing editorial metadata term.
1427 */
1428 // Check if name field was filled in.
1429 if ( empty( $metadata_name ) ) {
1430 $change_error = new WP_Error( 'invalid', esc_html__( 'Please enter a name for the editorial metadata', 'edit-flow' ) );
1431 wp_die( esc_html( $change_error->get_error_message() ) );
1432 }
1433
1434 // Check that the name isn't numeric.
1435 if ( is_numeric( $metadata_name ) ) {
1436 $change_error = new WP_Error( 'invalid', esc_html__( 'Please enter a valid, non-numeric name for the editorial metadata.', 'edit-flow' ) );
1437 wp_die( esc_html( $change_error->get_error_message() ) );
1438 }
1439
1440 // Check that the term name doesn't exceed 200 chars.
1441 if ( strlen( $metadata_name ) > 200 ) {
1442 $change_error = new WP_Error( 'invalid', esc_html__( 'Name cannot exceed 200 characters. Please try a shorter name.', 'edit-flow' ) );
1443 wp_die( esc_html( $change_error->get_error_message() ) );
1444 }
1445
1446 // Check to make sure the status doesn't already exist as another term because otherwise we'd get a fatal error.
1447 $term_exists = term_exists( sanitize_title( $metadata_name ) );
1448 if ( $term_exists && (int) $term_exists !== (int) $term_id ) {
1449 $change_error = new WP_Error( 'invalid', esc_html__( 'Metadata name conflicts with existing term. Please choose another.', 'edit-flow' ) );
1450 wp_die( esc_html( $change_error->get_error_message() ) );
1451 }
1452
1453 // Check to ensure a term with the same name doesn't exist.
1454 $search_term = $this->get_editorial_metadata_term_by( 'name', $metadata_name );
1455 if ( is_object( $search_term ) && (int) $search_term->term_id !== (int) $existing_term->term_id ) {
1456 $change_error = new WP_Error( 'invalid', esc_html__( 'Name already in use. Please choose another.', 'edit-flow' ) );
1457 wp_die( esc_html( $change_error->get_error_message() ) );
1458 }
1459
1460 // Or that the term name doesn't map to an existing term's slug.
1461 $search_term = $this->get_editorial_metadata_term_by( 'slug', sanitize_title( $metadata_name ) );
1462 if ( is_object( $search_term ) && (int) $search_term->term_id !== (int) $existing_term->term_id ) {
1463 $change_error = new WP_Error( 'invalid', esc_html__( 'Name conflicts with slug for another term. Please choose again.', 'edit-flow' ) );
1464 wp_die( esc_html( $change_error->get_error_message() ) );
1465 }
1466
1467 // Prepare the term name and description for saving.
1468 $args = array(
1469 'name' => $metadata_name,
1470 'description' => $metadata_description,
1471 );
1472 $return = $this->update_editorial_metadata_term( $existing_term->term_id, $args );
1473 if ( ! is_wp_error( $return ) ) {
1474 set_current_screen( 'edit-editorial-metadata' );
1475 $wp_list_table = new EF_Editorial_Metadata_List_Table();
1476 $wp_list_table->prepare_items();
1477 // single_row() echoes its own output; column_* callbacks are
1478 // responsible for escaping, as per WP_List_Table's contract.
1479 $wp_list_table->single_row( $return );
1480 wp_die();
1481 } else {
1482 /* Translators: 1: the name of the term that could not be found */
1483 $change_error = new WP_Error( 'invalid', sprintf( __( 'Could not update the term: <strong>%s</strong>', 'edit-flow' ), esc_html( $metadata_name ) ) );
1484 wp_die( wp_kses( $change_error->get_error_message(), array( 'strong' => array() ) ) );
1485 }
1486 }
1487
1488 /**
1489 * Handle the ajax request to update all of the term positions.
1490 *
1491 * @since 0.7
1492 */
1493 public function handle_ajax_update_term_positions() {
1494 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce value passed directly to wp_verify_nonce().
1495 if ( ! isset( $_POST['editorial_metadata_sortable_nonce'] ) || ! wp_verify_nonce( $_POST['editorial_metadata_sortable_nonce'], 'editorial-metadata-sortable' ) ) {
1496 $this->print_ajax_response( 'error', $this->module->messages['nonce-failed'] );
1497 return;
1498 }
1499
1500 if ( ! current_user_can( 'manage_options' ) ) {
1501 $this->print_ajax_response( 'error', $this->module->messages['invalid-permissions'] );
1502 return;
1503 }
1504
1505 if ( ! isset( $_POST['term_positions'] ) || ! is_array( $_POST['term_positions'] ) ) {
1506 $this->print_ajax_response( 'error', __( 'Terms not set.', 'edit-flow' ) );
1507 return;
1508 }
1509
1510 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Values are cast to int below.
1511 foreach ( $_POST['term_positions'] as $position => $term_id ) {
1512
1513 // Have to add 1 to the position because the index started with zero.
1514 $args = array(
1515 'position' => (int) $position + 1,
1516 );
1517 $return = $this->update_editorial_metadata_term( (int) $term_id, $args );
1518 // @todo Check that this was a valid return.
1519 }
1520 $this->print_ajax_response( 'success', $this->module->messages['term-position-updated'] );
1521 }
1522
1523 /**
1524 * Handles a request to delete an editorial metadata term.
1525 */
1526 public function handle_delete_editorial_metadata() {
1527 // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Nonce verified below.
1528 if ( ! isset( $_GET['page'], $_GET['action'], $_GET['term-id'] )
1529 || $_GET['page'] != $this->module->settings_slug || 'delete-term' != $_GET['action'] ) {
1530 return;
1531 }
1532 // phpcs:enable WordPress.Security.NonceVerification.Recommended
1533
1534 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce value passed directly to wp_verify_nonce().
1535 if ( ! isset( $_GET['nonce'] ) || ! wp_verify_nonce( $_GET['nonce'], 'delete-term' ) ) {
1536 wp_die( esc_html( $this->module->messages['nonce-failed'] ) );
1537 }
1538
1539 if ( ! current_user_can( 'manage_options' ) ) {
1540 wp_die( esc_html( $this->module->messages['invalid-permissions'] ) );
1541 }
1542
1543 $existing_term = $this->get_editorial_metadata_term_by( 'id', (int) $_GET['term-id'] );
1544 if ( ! $existing_term ) {
1545 wp_die( esc_html( $this->module->messages['term-missing'] ) );
1546 }
1547
1548 $result = $this->delete_editorial_metadata_term( $existing_term->term_id );
1549 if ( ! $result || is_wp_error( $result ) ) {
1550 wp_die( esc_html__( 'Error deleting term.', 'edit-flow' ) );
1551 }
1552
1553 $redirect_url = add_query_arg( array(
1554 'page' => $this->module->settings_slug,
1555 'message' => 'term-deleted',
1556 ), get_admin_url( null, 'admin.php' ) );
1557 // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- Redirect URL is constructed internally.
1558 wp_redirect( $redirect_url );
1559 exit;
1560 }
1561
1562 /**
1563 * Register settings for notifications so we can partially use the Settings API
1564 * (We use the Settings API for form generation, but not saving)
1565 *
1566 * @since 0.7
1567 * @uses add_settings_section(), add_settings_field()
1568 */
1569 public function register_settings() {
1570 add_settings_section( $this->module->options_group_name . '_general', false, '__return_false', $this->module->options_group_name );
1571 add_settings_field( 'post_types', __( 'Add to these post types:', 'edit-flow' ), array( $this, 'settings_post_types_option' ), $this->module->options_group_name, $this->module->options_group_name . '_general' );
1572 }
1573
1574 /**
1575 * Choose the post types for editorial metadata
1576 *
1577 * @since 0.7
1578 */
1579 public function settings_post_types_option() {
1580 global $edit_flow;
1581 $edit_flow->settings->helper_option_custom_post_type( $this->module );
1582 }
1583
1584 /**
1585 * Validate data entered by the user.
1586 *
1587 * @since 0.7
1588 *
1589 * @param array $new_options New values that have been entered by the user.
1590 * @return array $new_options Form values after they've been sanitized.
1591 */
1592 public function settings_validate( $new_options ) {
1593
1594 // Whitelist validation for the post type options.
1595 if ( ! isset( $new_options['post_types'] ) ) {
1596 $new_options['post_types'] = array();
1597 }
1598 $new_options['post_types'] = $this->clean_post_type_options( $new_options['post_types'], $this->module->post_type_support );
1599
1600 return $new_options;
1601 }
1602
1603 /**
1604 * Prepare and display the configuration view for editorial metadata.
1605 *
1606 * There are four primary components:
1607 * - Form to add a new Editorial Metadata term
1608 * - Form generated by the settings API for managing Editorial Metadata options
1609 * - Table of existing Editorial Metadata terms with ability to take actions on each
1610 * - Full page width view for editing a single Editorial Metadata term
1611 *
1612 * @since 0.7
1613 */
1614 public function print_configure_view() {
1615 // phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- This is a view function only, data is escaped on output.
1616 global $edit_flow;
1617 $wp_list_table = new EF_Editorial_Metadata_List_Table();
1618 $wp_list_table->prepare_items();
1619 ?>
1620 <script type="text/javascript">
1621 var ef_confirm_delete_term_string = "<?php echo esc_js( __( 'Are you sure you want to delete this term? Any metadata for this term will remain but will not be visible unless this term is re-added.', 'edit-flow' ) ); ?>";
1622 </script>
1623 <?php if ( ! isset( $_GET['action'] ) || ( isset( $_GET['action'] ) && 'edit-term' != $_GET['action'] ) ) : ?>
1624 <div id="col-right">
1625 <div class="col-wrap">
1626 <form id="" action="" method="post">
1627 <?php $wp_list_table->display(); ?>
1628 <?php wp_nonce_field( 'editorial-metadata-sortable', 'editorial-metadata-sortable' ); ?>
1629 </form>
1630 </div>
1631 </div><!-- /col-right -->
1632 <?php $wp_list_table->inline_edit(); ?>
1633 <?php endif; ?>
1634
1635 <?php if ( isset( $_GET['action'], $_GET['term-id'] ) && 'edit-term' == $_GET['action'] ) : ?>
1636 <?php /** Full page width view for editing a given editorial metadata term. **/ ?>
1637 <?php
1638 // Check whether the term exists.
1639 $term_id = (int) $_GET['term-id'];
1640 $term = $this->get_editorial_metadata_term_by( 'id', $term_id );
1641 if ( ! $term ) {
1642 echo '<div class="error"><p>' . esc_html( $this->module->messages['term-missing'] ) . '</p></div>';
1643 return;
1644 }
1645 $metadata_types = $this->get_supported_metadata_types();
1646 $type = $term->type;
1647 $edit_term_link = $this->get_link( array(
1648 'action' => 'edit-term',
1649 'term-id' => $term->term_id,
1650 ) );
1651
1652 // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Values are used for form re-population, escaped on output.
1653 $name = ( isset( $_POST['name'] ) ) ? stripslashes( $_POST['name'] ) : $term->name;
1654 $description = ( isset( $_POST['description'] ) ) ? stripslashes( $_POST['description'] ) : $term->description;
1655 if ( $term->viewable ) {
1656 $viewable = 'yes';
1657 } else {
1658 $viewable = 'no';
1659 }
1660 $viewable = ( isset( $_POST['viewable'] ) ) ? stripslashes( $_POST['viewable'] ) : $viewable;
1661 // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1662 ?>
1663
1664 <form method="post" action="<?php echo esc_url( $edit_term_link ); ?>" >
1665 <input type="hidden" name="action" value="editedtag" />
1666 <input type="hidden" name="tag_id" value="<?php echo esc_attr( $term->term_id ); ?>" />
1667 <input type="hidden" name="taxonomy" value="<?php echo esc_attr( self::metadata_taxonomy ); ?>" />
1668 <?php
1669 wp_original_referer_field();
1670 wp_nonce_field( 'editorial-metadata-edit-nonce' );
1671 ?>
1672 <table class="form-table">
1673 <tr class="form-field form-required">
1674 <th scope="row" valign="top"><label for="name"><?php _e( 'Name', 'edit-flow' ); ?></label></th>
1675 <td><input name="name" id="name" type="text" value="<?php echo esc_attr( $name ); ?>" size="40" aria-required="true" />
1676 <?php $edit_flow->settings->helper_print_error_or_description( 'name', __( 'The name is for labeling the metadata field.', 'edit-flow' ) ); ?>
1677 </tr>
1678 <tr class="form-field">
1679 <th scope="row" valign="top"><?php _e( 'Slug', 'edit-flow' ); ?></th>
1680 <td>
1681 <input type="text" disabled="disabled" value="<?php echo esc_attr( $term->slug ); ?>" />
1682 <p class="description"><?php _e( 'The slug cannot be changed once the term has been created.', 'edit-flow' ); ?></p>
1683 </td>
1684 </tr>
1685 <tr class="form-field">
1686 <th scope="row" valign="top"><label for="description"><?php _e( 'Description', 'edit-flow' ); ?></label></th>
1687 <td>
1688 <textarea name="description" id="description" rows="5" cols="50" style="width: 97%;"><?php echo esc_html( $description ); ?></textarea>
1689 <?php $edit_flow->settings->helper_print_error_or_description( 'description', __( 'The description can be used to communicate with your team about what the metadata is for.', 'edit-flow' ) ); ?>
1690 </td>
1691 </tr>
1692 <tr class="form-field">
1693 <th scope="row" valign="top"><?php _e( 'Type', 'edit-flow' ); ?></th>
1694 <td>
1695 <input type="text" disabled="disabled" value="<?php echo esc_attr( $metadata_types[ $type ] ); ?>" />
1696 <p class="description"><?php _e( 'The metadata type cannot be changed once created.', 'edit-flow' ); ?></p>
1697 </td>
1698 </tr>
1699 <tr class="form-field">
1700 <th scope="row" valign="top"><?php _e( 'Viewable', 'edit-flow' ); ?></th>
1701 <td>
1702 <?php
1703 $metadata_viewable_options = array(
1704 'no' => __( 'No', 'edit-flow' ),
1705 'yes' => __( 'Yes', 'edit-flow' ),
1706 );
1707 ?>
1708 <select id="viewable" name="viewable">
1709 <?php foreach ( $metadata_viewable_options as $metadata_viewable_key => $metadata_viewable_value ) : ?>
1710 <option value="<?php echo esc_attr( $metadata_viewable_key ); ?>" <?php selected( $viewable, $metadata_viewable_key ); ?>><?php echo esc_html( $metadata_viewable_value ); ?></option>
1711 <?php endforeach; ?>
1712 </select>
1713 <?php $edit_flow->settings->helper_print_error_or_description( 'viewable', __( 'When viewable, metadata can be seen on views other than the edit post view (e.g. calendar, manage posts, story budget, etc.)', 'edit-flow' ) ); ?>
1714 </td>
1715 </tr>
1716 <input type="hidden" name="<?php echo esc_attr( self::metadata_taxonomy ); ?>'_type" value="<?php echo esc_attr( $type ); ?>" />
1717 </table>
1718 <p class="submit">
1719 <?php submit_button( __( 'Update Metadata Term', 'edit-flow' ), 'primary', 'submit', false ); ?>
1720 <a class="cancel-settings-link" href="<?php echo esc_url( add_query_arg( 'page', $this->module->settings_slug, get_admin_url( null, 'admin.php' ) ) ); ?>"><?php _e( 'Cancel', 'edit-flow' ); ?></a>
1721 </p>
1722 </form>
1723
1724 <?php else : ?>
1725 <?php /** If not in full-screen edit term mode, we can create new terms or change options **/ ?>
1726 <div id="col-left">
1727 <div class="col-wrap">
1728 <div class="form-wrap">
1729 <h3 class="nav-tab-wrapper">
1730 <?php $add_new_nav_class = ! isset( $_GET['action'] ) || 'change-options' != $_GET['action'] ? 'nav-tab-active' : ''; ?>
1731 <a href="<?php echo esc_url( add_query_arg( array( 'page' => $this->module->settings_slug ), get_admin_url( null, 'admin.php' ) ) ); ?>" class="nav-tab <?php echo esc_attr( $add_new_nav_class ); ?>"><?php esc_html_e( 'Add New', 'edit-flow' ); ?></a>
1732 <?php $options_nav_class = isset( $_GET['action'] ) && 'change-options' == $_GET['action'] ? 'nav-tab-active' : ''; ?>
1733 <a href="
1734 <?php
1735 echo esc_url( add_query_arg( array(
1736 'page' => $this->module->settings_slug,
1737 'action' => 'change-options',
1738 ), get_admin_url( null, 'admin.php' ) ) );
1739 ?>
1740 " class="nav-tab <?php echo esc_attr( $options_nav_class ); ?>"><?php esc_html_e( 'Options', 'edit-flow' ); ?></a>
1741 </h3>
1742 <?php if ( isset( $_GET['action'] ) && 'change-options' == $_GET['action'] ) : ?>
1743 <?php /** Basic form built on WP Settings API for outputting Editorial Metadata options **/ ?>
1744 <form class="basic-settings" action="
1745 <?php
1746 echo esc_url( add_query_arg( array(
1747 'page' => $this->module->settings_slug,
1748 'action' => 'change-options',
1749 ), get_admin_url( null, 'admin.php' ) ) );
1750 ?>
1751 " method="post">
1752 <?php settings_fields( $this->module->options_group_name ); ?>
1753 <?php do_settings_sections( $this->module->options_group_name ); ?>
1754 <input id="edit_flow_module_name" name="edit_flow_module_name" type="hidden" value="<?php echo esc_attr( $this->module->name ); ?>" />
1755 <?php submit_button(); ?>
1756 </form>
1757 <?php else : ?>
1758 <?php /** Custom form for adding a new Editorial Metadata term. **/ ?>
1759 <?php // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Form fields escaped with esc_attr() or compared against whitelists. ?>
1760 <form class="add:the-list:" action="<?php echo esc_url( add_query_arg( array( 'page' => $this->module->settings_slug ), get_admin_url( null, 'admin.php' ) ) ); ?>" method="post" id="addmetadata" name="addmetadata">
1761 <div class="form-field form-required">
1762 <label for="metadata_name"><?php _e( 'Name', 'edit-flow' ); ?></label>
1763 <input type="text" aria-required="true" size="20" maxlength="200" id="metadata_name" name="metadata_name" value="<?php echo ( empty( $_POST['metadata_name'] ) ? '' : esc_attr( wp_unslash( $_POST['metadata_name'] ) ) ); ?>" />
1764 <?php $edit_flow->settings->helper_print_error_or_description( 'name', __( 'The name is for labeling the metadata field.', 'edit-flow' ) ); ?>
1765 </div>
1766 <div class="form-field form-required">
1767 <label for="metadata_slug"><?php _e( 'Slug', 'edit-flow' ); ?></label>
1768 <input type="text" aria-required="true" size="20" maxlength="200" id="metadata_slug" name="metadata_slug" value="<?php echo ( empty( $_POST['metadata_slug'] ) ? '' : esc_attr( wp_unslash( $_POST['metadata_slug'] ) ) ); ?>" />
1769 <?php $edit_flow->settings->helper_print_error_or_description( 'slug', __( 'The "slug" is the URL-friendly version of the name. It is usually all lowercase and contains only letters, numbers, and hyphens.', 'edit-flow' ) ); ?>
1770 </div>
1771 <div class="form-field">
1772 <label for="metadata_description"><?php _e( 'Description', 'edit-flow' ); ?></label>
1773 <textarea cols="40" rows="5" id="metadata_description" name="metadata_description"><?php echo ( empty( $_POST['metadata_description'] ) ? '' : esc_textarea( sanitize_textarea_field( wp_unslash( $_POST['metadata_description'] ) ) ) ); ?></textarea>
1774 <?php $edit_flow->settings->helper_print_error_or_description( 'description', __( 'The description can be used to communicate with your team about what the metadata is for.', 'edit-flow' ) ); ?>
1775 </div>
1776 <div class="form-field form-required">
1777 <label for="metadata_type"><?php _e( 'Type', 'edit-flow' ); ?></label>
1778 <?php
1779 $metadata_types = $this->get_supported_metadata_types();
1780 // Select the previously selected metadata type if a valid one exists.
1781 $current_metadata_type = ( isset( $_POST['metadata_type'] ) && in_array( $_POST['metadata_type'], array_keys( $metadata_types ), true ) ) ? $_POST['metadata_type'] : false;
1782 ?>
1783 <select id="metadata_type" name="metadata_type">
1784 <?php foreach ( $metadata_types as $metadata_type => $metadata_type_name ) : ?>
1785 <option value="<?php echo esc_attr( $metadata_type ); ?>" <?php selected( $metadata_type, $current_metadata_type ); ?>><?php echo esc_html( $metadata_type_name ); ?></option>
1786 <?php endforeach; ?>
1787 </select>
1788 <?php $edit_flow->settings->helper_print_error_or_description( 'type', __( 'Indicate the type of editorial metadata.', 'edit-flow' ) ); ?>
1789 </div>
1790 <div class="form-field form-required">
1791 <label for="metadata_viewable"><?php _e( 'Viewable', 'edit-flow' ); ?></label>
1792 <?php
1793 $metadata_viewable_options = array(
1794 'no' => __( 'No', 'edit-flow' ),
1795 'yes' => __( 'Yes', 'edit-flow' ),
1796 );
1797 $current_metadata_viewable = ( isset( $_POST['metadata_viewable'] ) && in_array( $_POST['metadata_viewable'], array_keys( $metadata_viewable_options ), true ) ) ? $_POST['metadata_viewable'] : 'no';
1798 ?>
1799 <select id="metadata_viewable" name="metadata_viewable">
1800 <?php foreach ( $metadata_viewable_options as $metadata_viewable_key => $metadata_viewable_value ) : ?>
1801 <option value="<?php echo esc_attr( $metadata_viewable_key ); ?>" <?php selected( $current_metadata_viewable, $metadata_viewable_key ); ?>><?php echo esc_html( $metadata_viewable_value ); ?></option>
1802 <?php endforeach; ?>
1803 </select>
1804 <?php $edit_flow->settings->helper_print_error_or_description( 'viewable', __( 'When viewable, metadata can be seen on views other than the edit post view (e.g. calendar, manage posts, story budget, etc.)', 'edit-flow' ) ); ?>
1805 </div>
1806 <?php wp_nonce_field( 'editorial-metadata-add-nonce' ); ?>
1807 <input type="hidden" id="form-action" name="form-action" value="add-term" />
1808 <?php submit_button( __( 'Add New Metadata Term', 'edit-flow' ) ); ?>
1809 </form>
1810 <?php // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized ?>
1811 <?php endif; ?>
1812 </div>
1813 </div>
1814 </div>
1815
1816 <?php
1817 endif;
1818 // phpcs:enable WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1819 }
1820 }
1821
1822 }
1823
1824 // phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound
1825
1826 /**
1827 * Management interface for Editorial Metadata. Extends WP_List_Table class.
1828 */
1829 class EF_Editorial_Metadata_List_Table extends WP_List_Table {
1830
1831 /**
1832 * Callback arguments.
1833 *
1834 * @var array
1835 */
1836 protected $callback_args;
1837
1838 /**
1839 * Taxonomy name.
1840 *
1841 * @var string
1842 */
1843 protected $taxonomy;
1844
1845 /**
1846 * Taxonomy object.
1847 *
1848 * @var object
1849 */
1850 protected $tax;
1851
1852 /**
1853 * Construct the class
1854 */
1855 public function __construct() {
1856 global $edit_flow;
1857
1858 $this->taxonomy = EF_Editorial_Metadata::metadata_taxonomy;
1859
1860 $this->tax = get_taxonomy( $this->taxonomy );
1861
1862 $columns = $this->get_columns();
1863 $hidden = array(
1864 'position',
1865 );
1866 $sortable = array();
1867
1868 $this->_column_headers = array( $columns, $hidden, $sortable );
1869
1870 parent::__construct( array(
1871 'plural' => 'editorial metadata',
1872 'singular' => 'editorial metadata',
1873 ) );
1874 }
1875
1876 /**
1877 * Prepare the items to be displayed on the list table
1878 *
1879 * @since 0.7
1880 */
1881 public function prepare_items() {
1882 global $edit_flow;
1883 $this->items = $edit_flow->editorial_metadata->get_editorial_metadata_terms();
1884
1885 $this->set_pagination_args( array(
1886 'total_items' => count( $this->items ),
1887 'per_page' => count( $this->items ),
1888 ) );
1889 }
1890
1891 /**
1892 * Message to be displayed when there is no editorial metadata
1893 *
1894 * @since 0.7
1895 */
1896 public function no_items() {
1897 _e( 'No editorial metadata found.', 'edit-flow' );
1898 }
1899
1900 /**
1901 * Register the columns to appear in the table
1902 *
1903 * @since 0.7
1904 */
1905 public function get_columns() {
1906
1907 $columns = array(
1908 'position' => __( 'Position', 'edit-flow' ),
1909 'name' => __( 'Name', 'edit-flow' ),
1910 'type' => __( 'Metadata Type', 'edit-flow' ),
1911 'description' => __( 'Description', 'edit-flow' ),
1912 'viewable' => __( 'Viewable', 'edit-flow' ),
1913 );
1914 return $columns;
1915 }
1916
1917 /**
1918 * Prepare a single row of Editorial Metadata.
1919 *
1920 * @since 0.7
1921 *
1922 * @param object $term The current term we're displaying.
1923 * @param int $level Level is always zero because it isn't a parent-child tax.
1924 */
1925 public function single_row( $term, $level = 0 ) {
1926 static $alternate_class = '';
1927 $alternate_class = ( '' == $alternate_class ? ' alternate' : '' );
1928
1929 printf( '<tr id="term-%d" class="term-static%s">', (int) $term->term_id, esc_attr( $alternate_class ) );
1930 // single_row_columns() echoes its own output; the column_* callbacks are
1931 // responsible for escaping, as per WP_List_Table's contract.
1932 $this->single_row_columns( $term );
1933 echo '</tr>';
1934 }
1935
1936 /**
1937 * Handle the column output when there's no method for it.
1938 *
1939 * @since 0.7
1940 *
1941 * @param object $item Editorial Metadata term as an object.
1942 * @param string $column_name How the column was registered at birth.
1943 */
1944 public function column_default( $item, $column_name ) {
1945
1946 switch ( $column_name ) {
1947 case 'position':
1948 case 'type':
1949 case 'description':
1950 return esc_html( $item->$column_name );
1951 case 'viewable':
1952 if ( $item->viewable ) {
1953 return __( 'Yes', 'edit-flow' );
1954 } else {
1955 return __( 'No', 'edit-flow' );
1956 }
1957 break;
1958 default:
1959 break;
1960 }
1961 }
1962
1963 /**
1964 * Column for displaying the term's name and associated actions.
1965 *
1966 * @since 0.7
1967 *
1968 * @param object $item Editorial Metadata term as an object.
1969 */
1970 public function column_name( $item ) {
1971 global $edit_flow;
1972 $item_edit_link = esc_url( $edit_flow->editorial_metadata->get_link( array(
1973 'action' => 'edit-term',
1974 'term-id' => $item->term_id,
1975 ) ) );
1976 $item_delete_link = esc_url( $edit_flow->editorial_metadata->get_link( array(
1977 'action' => 'delete-term',
1978 'term-id' => $item->term_id,
1979 ) ) );
1980
1981 $out = '<strong><a class="row-title" href="' . $item_edit_link . '">' . esc_html( $item->name ) . '</a></strong>';
1982
1983 $actions = array();
1984 $actions['edit'] = "<a href='$item_edit_link'>" . __( 'Edit', 'edit-flow' ) . '</a>';
1985 $actions['inline hide-if-no-js'] = '<a href="#" class="editinline">' . __( 'Quick&nbsp;Edit', 'edit-flow' ) . '</a>';
1986 if ( $item->viewable ) {
1987 $actions['change-visibility make-hidden'] = '<a title="' . esc_attr( __( 'Hidden metadata can only be viewed on the edit post view.', 'edit-flow' ) ) . '" href="' . esc_url( $edit_flow->editorial_metadata->get_link( array(
1988 'action' => 'make-hidden',
1989 'term-id' => $item->term_id,
1990 ) ) ) . '">' . __( 'Make Hidden', 'edit-flow' ) . '</a>';
1991 } else {
1992 $actions['change-visibility make-viewable'] = '<a title="' . esc_attr( __( 'When viewable, metadata can be seen on views other than the edit post view (e.g. calendar, manage posts, story budget, etc.)', 'edit-flow' ) ) . '" href="' . esc_url( $edit_flow->editorial_metadata->get_link( array(
1993 'action' => 'make-viewable',
1994 'term-id' => $item->term_id,
1995 ) ) ) . '">' . __( 'Make Viewable', 'edit-flow' ) . '</a>';
1996 }
1997 $actions['delete delete-status'] = "<a href='$item_delete_link'>" . __( 'Delete', 'edit-flow' ) . '</a>';
1998
1999 $out .= $this->row_actions( $actions, false );
2000 $out .= '<div class="hidden" id="inline_' . $item->term_id . '">';
2001 $out .= '<div class="name">' . esc_html( $item->name ) . '</div>';
2002 $out .= '<div class="description">' . esc_html( $item->description ) . '</div>';
2003 $out .= '</div>';
2004
2005 return $out;
2006 }
2007
2008 /**
2009 * Admins can use the inline edit capability to quickly make changes to the title or description.
2010 *
2011 * @since 0.7
2012 */
2013 public function inline_edit() {
2014
2015 ?>
2016 <form method="get" action=""><table style="display: none"><tbody id="inlineedit">
2017 <tr id="inline-edit" class="inline-edit-row" style="display: none"><td colspan="<?php echo esc_attr( $this->get_column_count() ); ?>" class="colspanchange">
2018 <fieldset><div class="inline-edit-col">
2019 <h4><?php _e( 'Quick Edit', 'edit-flow' ); ?></h4>
2020 <label>
2021 <span class="title"><?php _e( 'Name', 'edit-flow' ); ?></span>
2022 <span class="input-text-wrap"><input type="text" name="name" class="ptitle" value="" maxlength="200" /></span>
2023 </label>
2024 <label>
2025 <span class="title"><?php _e( 'Description', 'edit-flow' ); ?></span>
2026 <span class="input-text-wrap"><input type="text" name="description" class="pdescription" value="" /></span>
2027 </label>
2028 </div></fieldset>
2029 <p class="inline-edit-save submit">
2030 <a accesskey="c" href="#inline-edit" title="<?php _e( 'Cancel', 'edit-flow' ); ?>" class="cancel button-secondary alignleft"><?php _e( 'Cancel', 'edit-flow' ); ?></a>
2031 <?php $update_text = __( 'Update Metadata Term', 'edit-flow' ); ?>
2032 <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>
2033 <img class="waiting" style="display:none;" src="<?php echo esc_url( admin_url( 'images/wpspin_light.gif' ) ); ?>" alt="" />
2034 <span class="error" style="display:none;"></span>
2035 <?php wp_nonce_field( 'editorial-metadata-inline-edit-nonce', 'inline_edit', false ); ?>
2036 <br class="clear" />
2037 </p>
2038 </td></tr>
2039 </tbody></table></form>
2040 <?php
2041 }
2042 }
2043