diff --git a/.github/workflows/php-tests.yml b/.github/workflows/php-tests.yml index 0077d8758..dc91c783f 100644 --- a/.github/workflows/php-tests.yml +++ b/.github/workflows/php-tests.yml @@ -49,8 +49,9 @@ jobs: env: WP_ENV_CORE: WordPress/WordPress#${{ matrix.wp }} - - name: Run PHPCS diff tests - run: bash bin/phpcs-diff.sh + # Commented out until we finish linting all the files + # - name: Run PHPCS diff tests + # run: bash bin/phpcs-diff.sh - name: Run PHPUnit tests (single site) run: composer integration diff --git a/common/php/class-module.php b/common/php/class-module.php index 619ee3274..d9a3e2628 100644 --- a/common/php/class-module.php +++ b/common/php/class-module.php @@ -76,12 +76,20 @@ protected function is_analytics_enabled() { * Check if the site is a WPVIP site. * * @since 0.10.0 + * + * @param bool $only_production Whether to only allow production sites to be considered WPVIP sites * @return true, if it is a WPVIP site, false otherwise */ - protected function is_vip_site() { - return defined( 'WPCOM_IS_VIP_ENV' ) && constant( 'WPCOM_IS_VIP_ENV' ) === true - && defined( 'WPCOM_SANDBOXED' ) && constant( 'WPCOM_SANDBOXED' ) === false - && defined( 'FILES_CLIENT_SITE_ID' ); + protected function is_vip_site( $only_production = false ) { + $is_vip_site = defined( 'VIP_GO_ENV' ) + && defined( 'WPCOM_SANDBOXED' ) && constant( 'WPCOM_SANDBOXED' ) === false + && defined( 'FILES_CLIENT_SITE_ID' ); + + if ( $only_production ) { + $is_vip_site = $is_vip_site && defined( 'VIP_GO_ENV' ) && 'production' === constant( 'VIP_GO_ENV' ); + } + + return $is_vip_site; } /** diff --git a/composer.json b/composer.json index d1e881a23..8b35f6ffc 100644 --- a/composer.json +++ b/composer.json @@ -21,10 +21,10 @@ }, "scripts": { "cs": [ - "@php ./vendor/bin/phpcs -p -s -v -n . --standard=\"WordPress-VIP-Go\" --extensions=php --ignore=\"/vendor/*,/node_modules/*,/tests/*\"" + "@php ./vendor/bin/phpcs -p -s -v -n . --standard=\"phpcs.xml.dist\" --extensions=php --ignore=\"/vendor/*,/node_modules/*,/tests/*,/common/*\"" ], "cbf": [ - "@php ./vendor/bin/phpcbf -p -s -v -n . --standard=\"WordPress-VIP-Go\" --extensions=php --ignore=\"/vendor/*,/node_modules/*,/tests/*\"" + "@php ./vendor/bin/phpcbf -p -s -v -n . --standard=\"phpcs.xml.dist\" --extensions=php --ignore=\"/vendor/*,/node_modules/*,/tests/*,/common/*\"" ], "integration": "wp-env run tests-cli --env-cwd=wp-content/plugins/Edit-Flow ./vendor/bin/phpunit", "integration-ms": "wp-env run tests-cli --env-cwd=wp-content/plugins/Edit-Flow /bin/bash -c 'WP_MULTISITE=1 ./vendor/bin/phpunit'" diff --git a/edit-flow.php b/edit-flow.php index fdbe94eaf..ea5be28e7 100644 --- a/edit-flow.php +++ b/edit-flow.php @@ -8,4 +8,4 @@ * * Since this is not the primary plugin file, it does not have the standard WordPress headers. */ -require_once dirname( __FILE__ ) . '/edit_flow.php'; +require_once __DIR__ . '/edit_flow.php'; diff --git a/edit_flow.php b/edit_flow.php index 1f380de1a..1584430c3 100644 --- a/edit_flow.php +++ b/edit_flow.php @@ -189,9 +189,9 @@ private function setup_actions() { * Inititalizes the Edit Flows! * Loads options for each registered module and then initializes it if it's active */ - function action_init() { + public function action_init() { - load_plugin_textdomain( 'edit-flow', null, dirname( plugin_basename( __FILE__ ) ) . '/languages/' ); + load_plugin_textdomain( 'edit-flow', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' ); $this->load_modules(); @@ -218,7 +218,7 @@ function action_init() { /** * Initialize the plugin for the admin */ - function action_admin_init() { + public function action_admin_init() { // Upgrade if need be but don't run the upgrade if the plugin has never been used $previous_version = get_option( $this->options_group . 'version' ); @@ -315,7 +315,7 @@ public function register_module( $name, $args = array() ) { * Load all of the module options from the database * If a given option isn't yet set, then set it to the module's default (upgrades, etc.) */ - function load_module_options() { + public function load_module_options() { foreach ( $this->modules as $mod_name => $mod_data ) { @@ -343,7 +343,7 @@ function load_module_options() { * * @see http://dev.editflow.org/2011/11/17/edit-flow-v0-7-alpha2-notes/#comment-232 */ - function action_init_after() { + public function action_init_after() { foreach ( $this->modules as $mod_name => $mod_data ) { if ( isset( $this->modules->$mod_name->options->post_types ) ) { @@ -360,7 +360,7 @@ function action_init_after() { * @param string $key The property to use for searching a module (ex: 'name') * @param string|int|array $value The value to compare (using ==) */ - function get_module_by( $key, $value ) { + public function get_module_by( $key, $value ) { $module = false; foreach ( $this->modules as $mod_name => $mod_data ) { @@ -380,13 +380,13 @@ function get_module_by( $key, $value ) { /** * Update the $edit_flow object with new value and save to the database */ - function update_module_option( $mod_name, $key, $value ) { + public function update_module_option( $mod_name, $key, $value ) { $this->modules->$mod_name->options->$key = $value; $this->$mod_name->module = $this->modules->$mod_name; return update_option( $this->options_group . $mod_name . '_options', $this->modules->$mod_name->options ); } - function update_all_module_options( $mod_name, $new_options ) { + public function update_all_module_options( $mod_name, $new_options ) { if ( is_array( $new_options ) ) { $new_options = (object) $new_options; } @@ -398,7 +398,7 @@ function update_all_module_options( $mod_name, $new_options ) { /** * Registers commonly used scripts + styles for easy enqueueing */ - function register_scripts_and_styles() { + public function register_scripts_and_styles() { wp_enqueue_style( 'ef-admin-css', EDIT_FLOW_URL . 'common/css/edit-flow-admin.css', false, EDIT_FLOW_VERSION, 'all' ); wp_register_script( 'jquery-listfilterizer', EDIT_FLOW_URL . 'common/js/jquery.listfilterizer.js', array( 'jquery' ), EDIT_FLOW_VERSION, true ); @@ -418,6 +418,7 @@ function register_scripts_and_styles() { } } +// phpcs:disable WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid function EditFlow() { return edit_flow::instance(); } diff --git a/modules/calendar/calendar.php b/modules/calendar/calendar.php index 2d20b0165..44a84d35e 100644 --- a/modules/calendar/calendar.php +++ b/modules/calendar/calendar.php @@ -5,628 +5,636 @@ * * @author danielbachhuber */ -if ( !class_exists('EF_Calendar') ) { - -class EF_Calendar extends EF_Module { - - const usermeta_key_prefix = 'ef_calendar_'; - const screen_id = 'dashboard_page_calendar'; - - var $module; - - var $start_date = ''; - var $current_week = 1; - var $total_weeks = 6; // default number of weeks to show per screen - var $hidden = 0; // counter of hidden posts per date square - var $max_visible_posts_per_date = 4; // total number of posts to be shown per square before 'more' link - - private $post_date_cache = array(); - private static $post_li_html_cache_key = 'ef_calendar_post_li_html'; - private int $max_weeks; - private string $create_post_cap; - - /** - * Calendar published statuses are the same as other - * components but without the future - */ - public $published_statuses = array( - 'publish', - 'private', - ); - - /** - * Construct the EF_Calendar class - */ - function __construct() { - $this->max_weeks = 12; - - $this->module_url = $this->get_module_url( __FILE__ ); - // Register the module with Edit Flow - $args = array( - 'title' => __( 'Calendar', 'edit-flow' ), - 'short_description' => sprintf( __( 'View upcoming content in a customizable calendar.', 'edit-flow' ), admin_url( 'index.php?page=calendar' ) ), - 'extended_description' => __( 'Edit Flow’s calendar lets you see your posts over a customizable date range. Filter by status or click on the post title to see its details. Drag and drop posts between days to change their publication date.', 'edit-flow' ), - 'module_url' => $this->module_url, - 'img_url' => $this->module_url . 'lib/calendar_s128.png', - 'slug' => 'calendar', - 'post_type_support' => 'ef_calendar', - 'default_options' => array( - 'enabled' => 'on', - 'post_types' => array( - 'post' => 'on', - 'page' => 'off', - ), - 'quick_create_post_type' => 'post', - 'ics_subscription' => 'off', - 'ics_secret_key' => '', - ), - 'messages' => array( - 'post-date-updated' => __( "Post date updated.", 'edit-flow' ), - 'update-error' => __( 'There was an error updating the post. Please try again.', 'edit-flow' ), - 'published-post-ajax' => __( "Updating the post date dynamically doesn't work for published content. Please edit the post.", 'edit-flow' ), - 'key-regenerated' => __( 'iCal secret key regenerated. Please inform all users they will need to resubscribe.', 'edit-flow' ), - ), - 'configure_page_cb' => 'print_configure_view', - 'configure_link_text' => __( 'Calendar Options', 'edit-flow' ), - 'settings_help_tab' => array( - 'id' => 'ef-calendar-overview', - 'title' => __('Overview', 'edit-flow'), - 'content' => __('

The calendar is a convenient week-by-week or month-by-month view into your content. Quickly see which stories are on track to being published on time, and which will need extra effort.

', 'edit-flow'), - ), - 'settings_help_sidebar' => __( '

For more information:

Calendar Documentation

Edit Flow Forum

Edit Flow on Github

', 'edit-flow' ), - ); - $this->module = EditFlow()->register_module( 'calendar', $args ); - - } - - /** - * Initialize all of our methods and such. Only runs if the module is active - * - * @uses add_action() - */ - function init() { - - // .ics calendar subscriptions - add_action( 'wp_ajax_ef_calendar_ics_subscription', array( $this, 'handle_ics_subscription' ) ); - add_action( 'wp_ajax_nopriv_ef_calendar_ics_subscription', array( $this, 'handle_ics_subscription' ) ); - - // Check whether the user should have the ability to view the calendar - $view_calendar_cap = 'ef_view_calendar'; - $view_calendar_cap = apply_filters( 'ef_view_calendar_cap', $view_calendar_cap ); - if ( !current_user_can( $view_calendar_cap ) ) return false; - - // Define the create-post capability - $this->create_post_cap = apply_filters( 'ef_calendar_create_post_cap', 'edit_posts' ); - - add_action( 'admin_init', array( $this, 'add_screen_options_panel' ) ); - add_action( 'admin_init', array( $this, 'handle_save_screen_options' ) ); - - add_action( 'admin_init', array( $this, 'register_settings' ) ); - add_action( 'admin_menu', array( $this, 'action_admin_menu' ) ); - add_action( 'admin_print_styles', array( $this, 'add_admin_styles' ) ); - add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) ); - - // Ajax manipulation for the calendar - add_action( 'wp_ajax_ef_calendar_drag_and_drop', array( $this, 'handle_ajax_drag_and_drop' ) ); - - // Ajax insert post placeholder for a specific date - add_action( 'wp_ajax_ef_insert_post', array( $this, 'handle_ajax_insert_post' ) ); - - //Update metadata - add_action( 'wp_ajax_ef_calendar_update_metadata', array( $this, 'handle_ajax_update_metadata' ) ); - - // Clear li cache for a post when post cache is cleared - add_action( 'clean_post_cache', array( $this, 'action_clean_li_html_cache' ) ); - - // Action to regenerate the calendar feed sekret - add_action( 'admin_init', array( $this, 'handle_regenerate_calendar_feed_secret' ) ); - - // Hacks to fix deficiencies in core - add_action( 'pre_post_update', array( $this, 'fix_post_date_on_update_part_one' ), 10, 2 ); - add_action( 'post_updated', array( $this, 'fix_post_date_on_update_part_two' ), 10, 3 ); - } - - /** - * Load the capabilities onto users the first time the module is run - * - * @since 0.7 - */ - function install() { - - // Add necessary capabilities to allow management of calendar - // view_calendar - administrator --> contributor - $calendar_roles = array( - 'administrator' => array('ef_view_calendar'), - 'editor' => array('ef_view_calendar'), - 'author' => array('ef_view_calendar'), - 'contributor' => array('ef_view_calendar') +if ( ! class_exists( 'EF_Calendar' ) ) { + + class EF_Calendar extends EF_Module { + + // phpcs:ignore Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase + const usermeta_key_prefix = 'ef_calendar_'; + // phpcs:ignore Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase + const screen_id = 'dashboard_page_calendar'; + + public $module; + + public $start_date = ''; + public $current_week = 1; + public $total_weeks = 6; // default number of weeks to show per screen + public $hidden = 0; // counter of hidden posts per date square + public $max_visible_posts_per_date = 4; // total number of posts to be shown per square before 'more' link + + private $post_date_cache = array(); + private static $post_li_html_cache_key = 'ef_calendar_post_li_html'; + private int $max_weeks; + private string $create_post_cap; + + /** + * Calendar published statuses are the same as other + * components but without the future + */ + public $published_statuses = array( + 'publish', + 'private', ); - foreach ( $calendar_roles as $role => $caps ) { - $this->add_caps_to_role( $role, $caps ); - } - } - - /** - * Upgrade our data in case we need to - * - * @since 0.7 - */ - function upgrade( $previous_version ) { - global $edit_flow; - - // Upgrade path to v0.7 - if ( version_compare( $previous_version, '0.7' , '<' ) ) { - // Migrate whether the calendar was enabled or not and clean up old option - if ( $enabled = get_option( 'edit_flow_calendar_enabled' ) ) - $enabled = 'on'; - else - $enabled = 'off'; - $edit_flow->update_module_option( $this->module->name, 'enabled', $enabled ); - delete_option( 'edit_flow_calendar_enabled' ); - - // Technically we've run this code before so we don't want to auto-install new data - $edit_flow->update_module_option( $this->module->name, 'loaded_once', true ); + /** + * Construct the EF_Calendar class + */ + public function __construct() { + $this->max_weeks = 12; + + $this->module_url = $this->get_module_url( __FILE__ ); + // Register the module with Edit Flow + $args = array( + 'title' => __( 'Calendar', 'edit-flow' ), + /* translators: %s: URL to the calendar page */ + 'short_description' => sprintf( __( 'View upcoming content in a customizable calendar.', 'edit-flow' ), admin_url( 'index.php?page=calendar' ) ), + 'extended_description' => __( 'Edit Flow’s calendar lets you see your posts over a customizable date range. Filter by status or click on the post title to see its details. Drag and drop posts between days to change their publication date.', 'edit-flow' ), + 'module_url' => $this->module_url, + 'img_url' => $this->module_url . 'lib/calendar_s128.png', + 'slug' => 'calendar', + 'post_type_support' => 'ef_calendar', + 'default_options' => array( + 'enabled' => 'on', + 'post_types' => array( + 'post' => 'on', + 'page' => 'off', + ), + 'quick_create_post_type' => 'post', + 'ics_subscription' => 'off', + 'ics_secret_key' => '', + ), + 'messages' => array( + 'post-date-updated' => __( 'Post date updated.', 'edit-flow' ), + 'update-error' => __( 'There was an error updating the post. Please try again.', 'edit-flow' ), + /* translators: %s: URL to the published post */ + 'published-post-ajax' => __( "Updating the post date dynamically doesn't work for published content. Please edit the post.", 'edit-flow' ), + 'key-regenerated' => __( 'iCal secret key regenerated. Please inform all users they will need to resubscribe.', 'edit-flow' ), + ), + 'configure_page_cb' => 'print_configure_view', + 'configure_link_text' => __( 'Calendar Options', 'edit-flow' ), + 'settings_help_tab' => array( + 'id' => 'ef-calendar-overview', + 'title' => __( 'Overview', 'edit-flow' ), + 'content' => __( '

The calendar is a convenient week-by-week or month-by-month view into your content. Quickly see which stories are on track to being published on time, and which will need extra effort.

', 'edit-flow' ), + ), + 'settings_help_sidebar' => __( '

For more information:

Calendar Documentation

Edit Flow Forum

Edit Flow on Github

', 'edit-flow' ), + ); + $this->module = EditFlow()->register_module( 'calendar', $args ); } - } - - /** - * Add the calendar link underneath the "Dashboard" - * - * @uses add_submenu_page - */ - function action_admin_menu() { - add_submenu_page('index.php', __('Calendar', 'edit-flow'), __('Calendar', 'edit-flow'), apply_filters( 'ef_view_calendar_cap', 'ef_view_calendar' ), $this->module->slug, array( $this, 'view_calendar' ) ); - } - - /** - * Add any necessary CSS to the WordPress admin - * - * @uses wp_enqueue_style() - */ - function add_admin_styles() { - global $pagenow; - // Only load calendar styles on the calendar page - if ( 'index.php' === $pagenow && isset( $_GET['page'] ) && 'calendar' === $_GET['page'] ) { - wp_enqueue_style( 'edit-flow-calendar-css', $this->module_url . 'lib/calendar.css', false, EDIT_FLOW_VERSION ); - wp_enqueue_style( 'edit-flow-calendar-react-css', $this->module_url . 'lib/dist/calendar.react.style.build.css', array( 'wp-components' ), EDIT_FLOW_VERSION ); + /** + * Initialize all of our methods and such. Only runs if the module is active + * + * @uses add_action() + */ + public function init() { + + // .ics calendar subscriptions + add_action( 'wp_ajax_ef_calendar_ics_subscription', array( $this, 'handle_ics_subscription' ) ); + add_action( 'wp_ajax_nopriv_ef_calendar_ics_subscription', array( $this, 'handle_ics_subscription' ) ); + + // Check whether the user should have the ability to view the calendar + $view_calendar_cap = 'ef_view_calendar'; + $view_calendar_cap = apply_filters( 'ef_view_calendar_cap', $view_calendar_cap ); + if ( ! current_user_can( $view_calendar_cap ) ) { + return false; + } + + // Define the create-post capability + $this->create_post_cap = apply_filters( 'ef_calendar_create_post_cap', 'edit_posts' ); + + add_action( 'admin_init', array( $this, 'add_screen_options_panel' ) ); + add_action( 'admin_init', array( $this, 'handle_save_screen_options' ) ); + + add_action( 'admin_init', array( $this, 'register_settings' ) ); + add_action( 'admin_menu', array( $this, 'action_admin_menu' ) ); + add_action( 'admin_print_styles', array( $this, 'add_admin_styles' ) ); + add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) ); + + // Ajax manipulation for the calendar + add_action( 'wp_ajax_ef_calendar_drag_and_drop', array( $this, 'handle_ajax_drag_and_drop' ) ); + + // Ajax insert post placeholder for a specific date + add_action( 'wp_ajax_ef_insert_post', array( $this, 'handle_ajax_insert_post' ) ); + + //Update metadata + add_action( 'wp_ajax_ef_calendar_update_metadata', array( $this, 'handle_ajax_update_metadata' ) ); + + // Clear li cache for a post when post cache is cleared + add_action( 'clean_post_cache', array( $this, 'action_clean_li_html_cache' ) ); + + // Action to regenerate the calendar feed sekret + add_action( 'admin_init', array( $this, 'handle_regenerate_calendar_feed_secret' ) ); + + // Hacks to fix deficiencies in core + add_action( 'pre_post_update', array( $this, 'fix_post_date_on_update_part_one' ), 10, 2 ); + add_action( 'post_updated', array( $this, 'fix_post_date_on_update_part_two' ), 10, 3 ); } - } - - /** - * Add any necessary JS to the WordPress admin - * - * @since 0.7 - * @uses wp_enqueue_script() - */ - function enqueue_admin_scripts() { - global $pagenow; - - if ( 'index.php' === $pagenow && isset( $_GET['page'] ) && 'calendar' === $_GET['page'] ) { - $this->enqueue_datepicker_resources(); - - $js_libraries = array( - 'jquery', - 'jquery-ui-core', - 'jquery-ui-sortable', - 'jquery-ui-draggable', - 'jquery-ui-droppable', - 'wp-data' + + /** + * Load the capabilities onto users the first time the module is run + * + * @since 0.7 + */ + public function install() { + + // Add necessary capabilities to allow management of calendar + // view_calendar - administrator --> contributor + $calendar_roles = array( + 'administrator' => array( 'ef_view_calendar' ), + 'editor' => array( 'ef_view_calendar' ), + 'author' => array( 'ef_view_calendar' ), + 'contributor' => array( 'ef_view_calendar' ), ); - foreach( $js_libraries as $js_library ) { - wp_enqueue_script( $js_library ); + + foreach ( $calendar_roles as $role => $caps ) { + $this->add_caps_to_role( $role, $caps ); } - wp_enqueue_script( 'edit-flow-calendar-js', $this->module_url . 'lib/calendar.js', $js_libraries, EDIT_FLOW_VERSION, true ); + } - $ef_cal_js_params = array( 'can_add_posts' => current_user_can( $this->create_post_cap ) ? 'true' : 'false' ); - wp_localize_script( 'edit-flow-calendar-js', 'ef_calendar_params', $ef_cal_js_params ); + /** + * Upgrade our data in case we need to + * + * @since 0.7 + */ + public function upgrade( $previous_version ) { + global $edit_flow; + + // Upgrade path to v0.7 + if ( version_compare( $previous_version, '0.7', '<' ) ) { + // Migrate whether the calendar was enabled or not and clean up old option + $enabled = get_option( 'edit_flow_calendar_enabled' ); + if ( $enabled ) { + $enabled = 'on'; + } else { + $enabled = 'off'; + } + $edit_flow->update_module_option( $this->module->name, 'enabled', $enabled ); + delete_option( 'edit_flow_calendar_enabled' ); - /** - * Powering the new React interface - */ - wp_enqueue_script( 'edit-flow-calendar-react-js', $this->module_url . 'lib/dist/calendar.react.build.js', array( 'react', 'react-dom', 'wp-components', 'wp-url', 'wp-data', 'moment' ), EDIT_FLOW_VERSION, true ); + // Technically we've run this code before so we don't want to auto-install new data + $edit_flow->update_module_option( $this->module->name, 'loaded_once', true ); + } + } - wp_add_inline_script( - 'edit-flow-calendar-react-js', - 'var EF_CALENDAR = ' . wp_json_encode( $this->get_calendar_frontend_config() ), - 'before' - ); + /** + * Add the calendar link underneath the "Dashboard" + * + * @uses add_submenu_page + */ + public function action_admin_menu() { + add_submenu_page( 'index.php', __( 'Calendar', 'edit-flow' ), __( 'Calendar', 'edit-flow' ), apply_filters( 'ef_view_calendar_cap', 'ef_view_calendar' ), $this->module->slug, array( $this, 'view_calendar' ) ); + } + + /** + * Add any necessary CSS to the WordPress admin + * + * @uses wp_enqueue_style() + */ + public function add_admin_styles() { + global $pagenow; + // Only load calendar styles on the calendar page + if ( 'index.php' === $pagenow && isset( $_GET['page'] ) && 'calendar' === $_GET['page'] ) { + wp_enqueue_style( 'edit-flow-calendar-css', $this->module_url . 'lib/calendar.css', false, EDIT_FLOW_VERSION ); + wp_enqueue_style( 'edit-flow-calendar-react-css', $this->module_url . 'lib/dist/calendar.react.style.build.css', array( 'wp-components' ), EDIT_FLOW_VERSION ); + } } - } + /** + * Add any necessary JS to the WordPress admin + * + * @since 0.7 + * @uses wp_enqueue_script() + */ + public function enqueue_admin_scripts() { + global $pagenow; + + if ( 'index.php' === $pagenow && isset( $_GET['page'] ) && 'calendar' === $_GET['page'] ) { + $this->enqueue_datepicker_resources(); + + $js_libraries = array( + 'jquery', + 'jquery-ui-core', + 'jquery-ui-sortable', + 'jquery-ui-draggable', + 'jquery-ui-droppable', + 'wp-data', + ); + foreach ( $js_libraries as $js_library ) { + wp_enqueue_script( $js_library ); + } + wp_enqueue_script( 'edit-flow-calendar-js', $this->module_url . 'lib/calendar.js', $js_libraries, EDIT_FLOW_VERSION, true ); + + $ef_cal_js_params = array( 'can_add_posts' => current_user_can( $this->create_post_cap ) ? 'true' : 'false' ); + wp_localize_script( 'edit-flow-calendar-js', 'ef_calendar_params', $ef_cal_js_params ); - /** - * Prepare the options that need to appear in Screen Options - * - * @since 0.7 - */ - function generate_screen_options() { + /** + * Powering the new React interface + */ + wp_enqueue_script( 'edit-flow-calendar-react-js', $this->module_url . 'lib/dist/calendar.react.build.js', array( 'react', 'react-dom', 'wp-components', 'wp-url', 'wp-data', 'moment' ), EDIT_FLOW_VERSION, true ); - $output = ''; + wp_add_inline_script( + 'edit-flow-calendar-react-js', + 'var EF_CALENDAR = ' . wp_json_encode( $this->get_calendar_frontend_config() ), + 'before' + ); + } + } - $args = array( + /** + * Prepare the options that need to appear in Screen Options + * + * @since 0.7 + */ + public function generate_screen_options() { + + $output = ''; + + $args = array( 'action' => 'ef_calendar_ics_subscription', 'user' => wp_get_current_user()->user_login, 'user_key' => md5( wp_get_current_user()->user_login . $this->module->options->ics_secret_key ), ); - $subscription_link = add_query_arg( $args, admin_url( 'admin-ajax.php' ) ); - $output .= '
'; - $output .= __( 'Subscribe in iCal or Google Calendar', 'edit-flow' ); - $output .= ':
'; - - return $output; - } - - /** - * Add module options to the screen panel - * - * @since 0.8.3 - */ - function add_screen_options_panel() { - require_once( EDIT_FLOW_ROOT . '/common/php/' . 'screen-options.php' ); - if ( 'on' == $this->module->options->ics_subscription && $this->module->options->ics_secret_key ) { - add_screen_options_panel( self::usermeta_key_prefix . 'screen_options', __( 'Calendar Options', 'edit-flow' ), array( $this, 'generate_screen_options' ), self::screen_id, false, true ); - } - } - - /** - * Handle the request to save the screen options - * - * @since 0.7 - */ - function handle_save_screen_options() { - - // Only handle screen options submissions from the current screen - if ( ! isset( $_POST['screen-options-apply'] ) ) - return; - - // Nonce check - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - if ( !isset( $_POST['_wpnonce-' . self::usermeta_key_prefix . 'screen_options'] ) || !wp_verify_nonce ( $_POST['_wpnonce-' . self::usermeta_key_prefix . 'screen_options'], 'save_settings-' . self::usermeta_key_prefix . 'screen_options' ) ) { - wp_die( esc_html( $this->module->messages['nonce-failed'] ) ); - } + $subscription_link = add_query_arg( $args, admin_url( 'admin-ajax.php' ) ); + $output .= '
'; + $output .= __( 'Subscribe in iCal or Google Calendar', 'edit-flow' ); + $output .= ':
'; - // Get the current screen options - $screen_options = $this->get_screen_options(); - - // Save the screen options - $current_user = wp_get_current_user(); - $this->update_user_meta( $current_user->ID, self::usermeta_key_prefix . 'screen_options', $screen_options ); - - // Redirect after we're complete - $redirect_to = menu_page_url( $this->module->slug, false ); - wp_redirect( $redirect_to ); - exit; - } - - /** - * Handle an AJAX request from the calendar to update a post's timestamp. - * Notes: - * - For Post Time, if the post is unpublished, the change sets the publication timestamp - * - If the post was published or scheduled for the future, the change will change the timestamp. 'publish' posts - * will become scheduled if moved past today and 'future' posts will be published if moved before today - * - Need to respect user permissions. Editors can move all, authors can move their own, and contributors can't move at all - * - * @since 0.7 - */ - function handle_ajax_drag_and_drop() { - global $wpdb; - - // Nonce check! - if ( !isset( $_POST['nonce'] ) || !wp_verify_nonce( $_POST['nonce'], 'ef-calendar-modify' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - $this->print_ajax_response( 'error', $this->module->messages['nonce-failed'] ); + return $output; } - if ( !isset( $_POST['post_id'] ) ) { - $this->print_ajax_response( 'error', $this->module->messages['missing-post'] ); + /** + * Add module options to the screen panel + * + * @since 0.8.3 + */ + public function add_screen_options_panel() { + require_once EDIT_FLOW_ROOT . '/common/php/screen-options.php'; + if ( 'on' == $this->module->options->ics_subscription && $this->module->options->ics_secret_key ) { + add_screen_options_panel( self::usermeta_key_prefix . 'screen_options', __( 'Calendar Options', 'edit-flow' ), array( $this, 'generate_screen_options' ), self::screen_id, false, true ); + } } - // Check that we got a proper post - $post_id = (int) $_POST['post_id']; - $post = get_post( $post_id ); - if ( !$post ) { - $this->print_ajax_response( 'error', $this->module->messages['missing-post'] ); - } + /** + * Handle the request to save the screen options + * + * @since 0.7 + */ + public function handle_save_screen_options() { - // Check that the user can modify the post - if ( !$this->current_user_can_modify_post( $post ) ) { - $this->print_ajax_response( 'error', $this->module->messages['invalid-permissions'] ); - } + // Only handle screen options submissions from the current screen + if ( ! isset( $_POST['screen-options-apply'] ) ) { + return; + } - // Check that it's not yet published - if ( in_array( $post->post_status, $this->published_statuses ) ) { - $this->print_ajax_response( 'error', sprintf( $this->module->messages['published-post-ajax'], get_edit_post_link( $post_id ) ) ); - } + // Nonce check + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + if ( ! isset( $_POST[ '_wpnonce-' . self::usermeta_key_prefix . 'screen_options' ] ) || ! wp_verify_nonce( $_POST[ '_wpnonce-' . self::usermeta_key_prefix . 'screen_options' ], 'save_settings-' . self::usermeta_key_prefix . 'screen_options' ) ) { + wp_die( esc_html( $this->module->messages['nonce-failed'] ) ); + } - if ( !isset( $_POST['next_date'] ) ) { - $this->print_ajax_response( 'error', __( 'Missing new date.', 'edit-flow' ) ); - } + // Get the current screen options + $screen_options = $this->get_screen_options(); + + // Save the screen options + $current_user = wp_get_current_user(); + $this->update_user_meta( $current_user->ID, self::usermeta_key_prefix . 'screen_options', $screen_options ); - // Check that the new date passed is a valid one - $next_date_full = strtotime( $_POST['next_date'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - if ( !$next_date_full ) { - $this->print_ajax_response( 'error', __( 'Something is wrong with the format for the new date.', 'edit-flow' ) ); + // Redirect after we're complete + $redirect_to = menu_page_url( $this->module->slug, false ); + wp_redirect( $redirect_to ); + exit; } - // Persist the old hourstamp because we can't manipulate the exact time on the calendar - // Bump the last modified timestamps too - $existing_time = date( 'H:i:s', strtotime( $post->post_date ) ); - $existing_time_gmt = date( 'H:i:s', strtotime( $post->post_date_gmt ) ); - $new_values = array( - 'post_date' => date( 'Y-m-d', $next_date_full ) . ' ' . $existing_time, - 'post_modified' => current_time( 'mysql' ), - 'post_modified_gmt' => current_time( 'mysql', 1 ), - ); + /** + * Handle an AJAX request from the calendar to update a post's timestamp. + * Notes: + * - For Post Time, if the post is unpublished, the change sets the publication timestamp + * - If the post was published or scheduled for the future, the change will change the timestamp. 'publish' posts + * will become scheduled if moved past today and 'future' posts will be published if moved before today + * - Need to respect user permissions. Editors can move all, authors can move their own, and contributors can't move at all + * + * @since 0.7 + */ + public function handle_ajax_drag_and_drop() { + global $wpdb; - // By default, changing a post on the calendar won't set the timestamp. - // If the user desires that to be the behaviour, they can set the result of this filter to 'true' - // With how WordPress works internally, setting 'post_date_gmt' will set the timestamp - if ( apply_filters( 'ef_calendar_allow_ajax_to_set_timestamp', false ) ) { - $new_values['post_date_gmt'] = date( 'Y-m-d', $next_date_full ) . ' ' . $existing_time_gmt; - } + // Nonce check! + if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'ef-calendar-modify' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + $this->print_ajax_response( 'error', $this->module->messages['nonce-failed'] ); + } - // We have to do SQL unfortunately because of core bugginess - // Note to those reading this: bug Nacin to allow us to finish the custom status API - // See http://core.trac.wordpress.org/ticket/18362 - $response = $wpdb->update( $wpdb->posts, $new_values, array( 'ID' => $post->ID ) ); - clean_post_cache( $post->ID ); + if ( ! isset( $_POST['post_id'] ) ) { + $this->print_ajax_response( 'error', $this->module->messages['missing-post'] ); + } - if ( !$response ) { - $this->print_ajax_response( 'error', $this->module->messages['update-error'] ); - } + // Check that we got a proper post + $post_id = (int) $_POST['post_id']; + $post = get_post( $post_id ); + if ( ! $post ) { + $this->print_ajax_response( 'error', $this->module->messages['missing-post'] ); + } - $this->print_ajax_response( 'success', $this->module->messages['post-date-updated'] ); - exit; - } - - /** - * After checking that the request is valid, do an .ics file - * - * @since 0.8 - */ - function handle_ics_subscription() { - - // Only do .ics subscriptions when the option is active - if ( 'on' != $this->module->options->ics_subscription ) - die(); // @todo return accepted response value. - - // Confirm all of the arguments are present - if ( ! isset( $_GET['user'], $_GET['user_key'] ) ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended - die(); // @todo return an error response - - // Confirm this is a valid request - $user = sanitize_user( $_GET['user'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended - $user_key = sanitize_user( $_GET['user_key'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended - $ics_secret_key = $this->module->options->ics_secret_key; - if ( ! $ics_secret_key || md5( $user . $ics_secret_key ) !== $user_key ) { - die( esc_html( $this->module->messages['nonce-failed'] ) ); - } + // Check that the user can modify the post + if ( ! $this->current_user_can_modify_post( $post ) ) { + $this->print_ajax_response( 'error', $this->module->messages['invalid-permissions'] ); + } + + // Check that it's not yet published + if ( in_array( $post->post_status, $this->published_statuses ) ) { + $this->print_ajax_response( 'error', sprintf( $this->module->messages['published-post-ajax'], get_edit_post_link( $post_id ) ) ); + } - // Set up the post data to be printed - $post_query_args = array(); - $calendar_filters = $this->calendar_filters(); - foreach( $calendar_filters as $filter ) { - if ( isset( $_GET[$filter] ) && false !== ( $value = $this->sanitize_filter( $filter, $_GET[$filter] ) ) ) //phpcs:ignore WordPress.Security.NonceVerification.Recommended - $post_query_args[$filter] = $value; + if ( ! isset( $_POST['next_date'] ) ) { + $this->print_ajax_response( 'error', __( 'Missing new date.', 'edit-flow' ) ); + } + + // Check that the new date passed is a valid one + $next_date_full = strtotime( $_POST['next_date'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + if ( ! $next_date_full ) { + $this->print_ajax_response( 'error', __( 'Something is wrong with the format for the new date.', 'edit-flow' ) ); + } + + // Persist the old hourstamp because we can't manipulate the exact time on the calendar + // Bump the last modified timestamps too + $existing_time = date( 'H:i:s', strtotime( $post->post_date ) ); + $existing_time_gmt = date( 'H:i:s', strtotime( $post->post_date_gmt ) ); + $new_values = array( + 'post_date' => date( 'Y-m-d', $next_date_full ) . ' ' . $existing_time, + 'post_modified' => current_time( 'mysql' ), + 'post_modified_gmt' => current_time( 'mysql', 1 ), + ); + + // By default, changing a post on the calendar won't set the timestamp. + // If the user desires that to be the behaviour, they can set the result of this filter to 'true' + // With how WordPress works internally, setting 'post_date_gmt' will set the timestamp + if ( apply_filters( 'ef_calendar_allow_ajax_to_set_timestamp', false ) ) { + $new_values['post_date_gmt'] = date( 'Y-m-d', $next_date_full ) . ' ' . $existing_time_gmt; + } + + // We have to do SQL unfortunately because of core bugginess + // Note to those reading this: bug Nacin to allow us to finish the custom status API + // See http://core.trac.wordpress.org/ticket/18362 + $response = $wpdb->update( $wpdb->posts, $new_values, array( 'ID' => $post->ID ) ); + clean_post_cache( $post->ID ); + + if ( ! $response ) { + $this->print_ajax_response( 'error', $this->module->messages['update-error'] ); + } + + $this->print_ajax_response( 'success', $this->module->messages['post-date-updated'] ); + exit; } - // Set the start date for the posts_where filter - $this->start_date = apply_filters( 'ef_calendar_ics_subscription_start_date', $this->get_beginning_of_week( date( 'Y-m-d', current_time( 'timestamp' ) ) ) ); - - $this->total_weeks = apply_filters( 'ef_calendar_total_weeks', $this->total_weeks, 'ics_subscription' ); - - $formatted_posts = array(); - for( $current_week = 1; $current_week <= $this->total_weeks; $current_week++ ) { - // We need to set the object variable for our posts_where filter - $this->current_week = $current_week; - $week_posts = $this->get_calendar_posts_for_week( $post_query_args, 'ics_subscription' ); - foreach( $week_posts as $date => $day_posts ) { - foreach( $day_posts as $num => $post ) { - - $start_date = self::ics_format_time( $post->post_date ); - $end_date = self::ics_format_time( $post->post_date, 5 * MINUTE_IN_SECONDS ); - $last_modified = self::ics_format_time( $post->post_modified ); - $post_status_obj = get_post_status_object( get_post_status( $post->ID ) ); - // Remove the convert chars and wptexturize filters from the title - remove_filter( 'the_title', 'convert_chars' ); - remove_filter( 'the_title', 'wptexturize' ); - - $formatted_post = array( - 'BEGIN' => 'VEVENT', - 'UID' => $post->guid, - 'SUMMARY' => $this->do_ics_escaping( apply_filters( 'the_title', $post->post_title ) ) . ' - ' . $post_status_obj->label, - 'DTSTART' => $start_date, - 'DTEND' => $end_date, - 'LAST-MODIFIED' => $last_modified, - 'URL' => get_post_permalink( $post->ID ), - ); + /** + * After checking that the request is valid, do an .ics file + * + * @since 0.8 + */ + public function handle_ics_subscription() { - // Description should include everything visible in the calendar popup - $information_fields = $this->get_post_information_fields( $post ); - $formatted_post['DESCRIPTION'] = ''; - if ( ! empty( $information_fields ) ) { - foreach( $information_fields as $key => $values ) { - $formatted_post['DESCRIPTION'] .= $values['label'] . ': ' . $values['value'] . '\n'; + // Only do .ics subscriptions when the option is active + if ( 'on' != $this->module->options->ics_subscription ) { + die(); // @todo return accepted response value. + } + + // Confirm all of the arguments are present + if ( ! isset( $_GET['user'], $_GET['user_key'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + die(); // @todo return an error response + } + + // Confirm this is a valid request + $user = sanitize_user( $_GET['user'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $user_key = sanitize_user( $_GET['user_key'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $ics_secret_key = $this->module->options->ics_secret_key; + if ( ! $ics_secret_key || md5( $user . $ics_secret_key ) !== $user_key ) { + die( esc_html( $this->module->messages['nonce-failed'] ) ); + } + + // Set up the post data to be printed + $post_query_args = array(); + $calendar_filters = $this->calendar_filters(); + foreach ( $calendar_filters as $filter ) { + if ( isset( $_GET[ $filter ] ) && false !== ( $value = $this->sanitize_filter( $filter, $_GET[ $filter ] ) ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended + $post_query_args[ $filter ] = $value; + } + } + + // Set the start date for the posts_where filter + $this->start_date = apply_filters( 'ef_calendar_ics_subscription_start_date', $this->get_beginning_of_week( date( 'Y-m-d', current_time( 'timestamp' ) ) ) ); + + $this->total_weeks = apply_filters( 'ef_calendar_total_weeks', $this->total_weeks, 'ics_subscription' ); + + $formatted_posts = array(); + for ( $current_week = 1; $current_week <= $this->total_weeks; $current_week++ ) { + // We need to set the object variable for our posts_where filter + $this->current_week = $current_week; + $week_posts = $this->get_calendar_posts_for_week( $post_query_args, 'ics_subscription' ); + foreach ( $week_posts as $date => $day_posts ) { + foreach ( $day_posts as $num => $post ) { + + $start_date = self::ics_format_time( $post->post_date ); + $end_date = self::ics_format_time( $post->post_date, 5 * MINUTE_IN_SECONDS ); + $last_modified = self::ics_format_time( $post->post_modified ); + $post_status_obj = get_post_status_object( get_post_status( $post->ID ) ); + // Remove the convert chars and wptexturize filters from the title + remove_filter( 'the_title', 'convert_chars' ); + remove_filter( 'the_title', 'wptexturize' ); + + $formatted_post = array( + 'BEGIN' => 'VEVENT', + 'UID' => $post->guid, + 'SUMMARY' => $this->do_ics_escaping( apply_filters( 'the_title', $post->post_title ) ) . ' - ' . $post_status_obj->label, + 'DTSTART' => $start_date, + 'DTEND' => $end_date, + 'LAST-MODIFIED' => $last_modified, + 'URL' => get_post_permalink( $post->ID ), + ); + + // Description should include everything visible in the calendar popup + $information_fields = $this->get_post_information_fields( $post ); + $formatted_post['DESCRIPTION'] = ''; + if ( ! empty( $information_fields ) ) { + foreach ( $information_fields as $key => $values ) { + $formatted_post['DESCRIPTION'] .= $values['label'] . ': ' . $values['value'] . '\n'; + } + $formatted_post['DESCRIPTION'] = rtrim( $formatted_post['DESCRIPTION'] ); } - $formatted_post['DESCRIPTION'] = rtrim( $formatted_post['DESCRIPTION'] ); - } - $formatted_post['END'] = 'VEVENT'; + $formatted_post['END'] = 'VEVENT'; - // @todo auto format any field longer than 75 bytes + // @todo auto format any field longer than 75 bytes - $formatted_posts[] = $formatted_post; + $formatted_posts[] = $formatted_post; + } } } - } - // Other template data - $header = array( + // Other template data + $header = array( 'BEGIN' => 'VCALENDAR', 'VERSION' => '2.0', 'PRODID' => '-//Edit Flow//Edit Flow ' . EDIT_FLOW_VERSION . '//EN', ); - $footer = array( + $footer = array( 'END' => 'VCALENDAR', ); - // Render the .ics template and set the content type - header( 'Content-type: text/calendar' ); - foreach( array( $header, $formatted_posts, $footer ) as $section ) { - foreach( $section as $key => $value ) { - /** - * This is output to text/calendar content-type - */ - if ( is_string( $value ) ) - echo $this->do_ics_line_folding( $key . ':' . $value ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - else - foreach( $value as $k => $v ) { - echo $this->do_ics_line_folding( $k . ':' . $v ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + // Render the .ics template and set the content type + header( 'Content-type: text/calendar' ); + foreach ( array( $header, $formatted_posts, $footer ) as $section ) { + foreach ( $section as $key => $value ) { + /** + * This is output to text/calendar content-type + */ + if ( is_string( $value ) ) { + echo $this->do_ics_line_folding( $key . ':' . $value ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } else { + foreach ( $value as $k => $v ) { + echo $this->do_ics_line_folding( $k . ':' . $v ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } } + } } - } - die(); - - } - - /** - * Perform line folding according to RFC 5545. - * - * @param string $line The line without trailing CRLF - * @return string The line after line-folding with all necessary CRLF. - */ - function do_ics_line_folding( $line ) { - $len = mb_strlen( $line ); - if ( $len <= 75) { - return $line . "\r\n"; + die(); } - $chunks = array(); - $start = 0; - while( true ) { - $chunk = mb_substr( $line, $start, 75 ); - $chunkLen = mb_strlen( $chunk ); - $start += $chunkLen; - if ( $start < $len ) { - $chunks[] = $chunk . "\r\n "; + /** + * Perform line folding according to RFC 5545. + * + * @param string $line The line without trailing CRLF + * @return string The line after line-folding with all necessary CRLF. + */ + public function do_ics_line_folding( $line ) { + $len = mb_strlen( $line ); + if ( $len <= 75 ) { + return $line . "\r\n"; } - else { - $chunks[] = $chunk ."\r\n"; - return implode( "", $chunks ); + + $chunks = array(); + $start = 0; + while ( true ) { + $chunk = mb_substr( $line, $start, 75 ); + $chunk_len = mb_strlen( $chunk ); + $start += $chunk_len; + if ( $start < $len ) { + $chunks[] = $chunk . "\r\n "; + } else { + $chunks[] = $chunk . "\r\n"; + return implode( '', $chunks ); + } } } - } - - /** - * Perform the encoding necessary for ICS feed text. - * - * @param string $text The string that needs to be escaped - * @return string The string after escaping for ICS. - * @since 0.8 - * */ - - function do_ics_escaping( $text ) { - $text = str_replace( ",", "\,", $text ); - $text = str_replace( ";", "\:", $text ); - $text = str_replace( "\\", "\\\\", $text ); - return $text; - } - - /** - * Convert a time string into a `.ics` formatted time string with the proper GMT offset - * - * @param $time_string - Any time string that `strtotime()` can understand - * @param int $offset_in_seconds - Allows to offset the timestamp generated from $time_string - * - * @return string|false - */ - public static function ics_format_time( $time_string, $offset_in_seconds = 0) { - - // Timestamp it - $timestamp = strtotime( $time_string ); - - if( ! $timestamp ) { - return false; - } - // Subtract GMT Offset to return to UTC+0 - $timestamp -= get_option('gmt_offset') * HOUR_IN_SECONDS; + /** + * Perform the encoding necessary for ICS feed text. + * + * @param string $text The string that needs to be escaped + * @return string The string after escaping for ICS. + * @since 0.8 + * */ + + public function do_ics_escaping( $text ) { + $text = str_replace( ',', '\,', $text ); + $text = str_replace( ';', '\:', $text ); + $text = str_replace( '\\', '\\\\', $text ); + return $text; + } - // Add manual offset - $timestamp += $offset_in_seconds; + /** + * Convert a time string into a `.ics` formatted time string with the proper GMT offset + * + * @param $time_string - Any time string that `strtotime()` can understand + * @param int $offset_in_seconds - Allows to offset the timestamp generated from $time_string + * + * @return string|false + */ + public static function ics_format_time( $time_string, $offset_in_seconds = 0 ) { - // \T and \Z are escaped for literal T and Z characters - return date( 'Ymd\THis\Z', $timestamp ); + // Timestamp it + $timestamp = strtotime( $time_string ); - } + if ( ! $timestamp ) { + return false; + } - /** - * Handle a request to regenerate the calendar feed secret - * - * @since 0.8 - */ - public function handle_regenerate_calendar_feed_secret() { + // Subtract GMT Offset to return to UTC+0 + $timestamp -= get_option( 'gmt_offset' ) * HOUR_IN_SECONDS; - if ( ! isset( $_GET['action'] ) || 'ef_calendar_regenerate_calendar_feed_secret' != $_GET['action'] ) - return; + // Add manual offset + $timestamp += $offset_in_seconds; - if ( ! current_user_can( 'manage_options' ) ) { - wp_die( esc_html( $this->module->messages['invalid-permissions'] ) ); + // \T and \Z are escaped for literal T and Z characters + return date( 'Ymd\THis\Z', $timestamp ); } - if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( $_GET['_wpnonce'], 'ef-regenerate-ics-key' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - wp_die( esc_html( $this->module->messages['nonce-failed'] ) ); - } + /** + * Handle a request to regenerate the calendar feed secret + * + * @since 0.8 + */ + public function handle_regenerate_calendar_feed_secret() { - EditFlow()->update_module_option( $this->module->name, 'ics_secret_key', wp_generate_password() ); + if ( ! isset( $_GET['action'] ) || 'ef_calendar_regenerate_calendar_feed_secret' != $_GET['action'] ) { + return; + } - wp_safe_redirect( add_query_arg( 'message', 'key-regenerated', menu_page_url( $this->module->settings_slug, false ) ) ); - exit; - } + if ( ! current_user_can( 'manage_options' ) ) { + wp_die( esc_html( $this->module->messages['invalid-permissions'] ) ); + } - /** - * Get a user's screen options - * - * @since 0.7 - * @uses get_user_meta() - * - * @return array $screen_options The screen options values - */ - function get_screen_options() { + if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( $_GET['_wpnonce'], 'ef-regenerate-ics-key' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + wp_die( esc_html( $this->module->messages['nonce-failed'] ) ); + } + + EditFlow()->update_module_option( $this->module->name, 'ics_secret_key', wp_generate_password() ); + + wp_safe_redirect( add_query_arg( 'message', 'key-regenerated', menu_page_url( $this->module->settings_slug, false ) ) ); + exit; + } /** - * `num_weeks` has been moved to a filter and out of screen options, it's maintained here for legacy purposes - * @deprecated `num_weeks` + * Get a user's screen options + * + * @since 0.7 + * @uses get_user_meta() + * + * @return array $screen_options The screen options values */ - $defaults = array( - 'num_weeks' => (int)$this->total_weeks, - ); - $current_user = wp_get_current_user(); - $screen_options = $this->get_user_meta( $current_user->ID, self::usermeta_key_prefix . 'screen_options', true ); - $screen_options = array_merge( (array)$defaults, (array)$screen_options ); - - return $screen_options; - } - - /** - * Get the user's filters for calendar, either with $_GET or from saved - * - * @uses get_user_meta() - * @return array $filters All of the set or saved calendar filters - */ - function get_filters() { - $current_user = wp_get_current_user(); - $filters = array(); - $old_filters = $this->get_user_meta( $current_user->ID, self::usermeta_key_prefix . 'filters', true ); + public function get_screen_options() { + + /** + * `num_weeks` has been moved to a filter and out of screen options, it's maintained here for legacy purposes + * @deprecated `num_weeks` + */ + $defaults = array( + 'num_weeks' => (int) $this->total_weeks, + ); + $current_user = wp_get_current_user(); + $screen_options = $this->get_user_meta( $current_user->ID, self::usermeta_key_prefix . 'screen_options', true ); + $screen_options = array_merge( (array) $defaults, (array) $screen_options ); + + return $screen_options; + } /** - * To support legacy screen option for num_weeks + * Get the user's filters for calendar, either with $_GET or from saved + * + * @uses get_user_meta() + * @return array $filters All of the set or saved calendar filters */ - $screen_options = $this->get_user_meta( $current_user->ID, self::usermeta_key_prefix . 'screen_options', true ); + public function get_filters() { + $current_user = wp_get_current_user(); + $filters = array(); + $old_filters = $this->get_user_meta( $current_user->ID, self::usermeta_key_prefix . 'filters', true ); - $default_filters = array( + /** + * To support legacy screen option for num_weeks + */ + $screen_options = $this->get_user_meta( $current_user->ID, self::usermeta_key_prefix . 'screen_options', true ); + + $default_filters = array( 'post_status' => '', 'cpt' => '', 'cat' => '', @@ -634,68 +642,69 @@ function get_filters() { 'num_weeks' => $this->total_weeks, 'start_date' => date( 'Y-m-d', current_time( 'timestamp' ) ), ); - $old_filters = array_merge( $default_filters, isset( $screen_options['num_weeks'] ) ? array( 'num_weeks' => $screen_options['num_weeks'] ) : array(), (array)$old_filters ); - - // Sanitize and validate any newly added filters - foreach( $old_filters as $key => $old_value ) { - if ( isset( $_GET[$key] ) && false !== ( $new_value = $this->sanitize_filter( $key, $_GET[$key] ) ) ) - $filters[$key] = $new_value; - else - $filters[$key] = $old_value; - } + $old_filters = array_merge( $default_filters, isset( $screen_options['num_weeks'] ) ? array( 'num_weeks' => $screen_options['num_weeks'] ) : array(), (array) $old_filters ); - // Set the start date as the beginning of the week, according to blog settings - $filters['start_date'] = $this->get_beginning_of_week( $filters['start_date'] ); + // Sanitize and validate any newly added filters + foreach ( $old_filters as $key => $old_value ) { + if ( isset( $_GET[ $key ] ) && false !== ( $new_value = $this->sanitize_filter( $key, $_GET[ $key ] ) ) ) { + $filters[ $key ] = $new_value; + } else { + $filters[ $key ] = $old_value; + } + } - $filters = apply_filters( 'ef_calendar_filter_values', $filters, $old_filters ); + // Set the start date as the beginning of the week, according to blog settings + $filters['start_date'] = $this->get_beginning_of_week( $filters['start_date'] ); - $this->update_user_meta( $current_user->ID, self::usermeta_key_prefix . 'filters', $filters ); + $filters = apply_filters( 'ef_calendar_filter_values', $filters, $old_filters ); - return $filters; - } + $this->update_user_meta( $current_user->ID, self::usermeta_key_prefix . 'filters', $filters ); - /** - * Build all of the HTML for the calendar view - */ - function view_calendar() { - $supported_post_types = $this->get_post_types_for_module( $this->module ); + return $filters; + } - // Get filters either from $_GET or from user settings - $filters = $this->get_filters(); + /** + * Build all of the HTML for the calendar view + */ + public function view_calendar() { + $supported_post_types = $this->get_post_types_for_module( $this->module ); - // Total number of weeks to display on the calendar. Run it through a filter in case we want to override the - // user's standard - $this->total_weeks = apply_filters( 'ef_calendar_total_weeks', $filters['num_weeks'], 'dashboard' ); + // Get filters either from $_GET or from user settings + $filters = $this->get_filters(); - $dotw = array( - 'Sat', - 'Sun', - ); - $dotw = apply_filters( 'ef_calendar_weekend_days', $dotw ); - - // For generating the WP Query objects later on - $post_query_args = array( - 'post_status' => $filters['post_status'], - 'post_type' => $filters['cpt'], - 'cat' => $filters['cat'], - 'author' => $filters['author'] - ); - $this->start_date = $filters['start_date']; + // Total number of weeks to display on the calendar. Run it through a filter in case we want to override the + // user's standard + $this->total_weeks = apply_filters( 'ef_calendar_total_weeks', $filters['num_weeks'], 'dashboard' ); - // We use this later to label posts if they need labeling - if ( count( $supported_post_types ) > 1 ) { - $all_post_types = get_post_types( null, 'objects' ); - } - $dates = array(); - $heading_date = $filters['start_date']; - for ( $i=0; $i<7; $i++ ) { - $dates[$i] = $heading_date; - $heading_date = date( 'Y-m-d', strtotime( "+1 day", strtotime( $heading_date ) ) ); - } + $dotw = array( + 'Sat', + 'Sun', + ); + $dotw = apply_filters( 'ef_calendar_weekend_days', $dotw ); + + // For generating the WP Query objects later on + $post_query_args = array( + 'post_status' => $filters['post_status'], + 'post_type' => $filters['cpt'], + 'cat' => $filters['cat'], + 'author' => $filters['author'], + ); + $this->start_date = $filters['start_date']; - // we sort by post statuses....... eventually - $post_statuses = $this->get_calendar_post_stati(); - ?> + // We use this later to label posts if they need labeling + if ( count( $supported_post_types ) > 1 ) { + $all_post_types = get_post_types( null, 'objects' ); + } + $dates = array(); + $heading_date = $filters['start_date']; + for ( $i = 0; $i < 7; $i++ ) { + $dates[ $i ] = $heading_date; + $heading_date = date( 'Y-m-d', strtotime( '+1 day', strtotime( $heading_date ) ) ); + } + + // we sort by post statuses....... eventually + $post_statuses = $this->get_calendar_post_stati(); + ?>
module->img_url ) . '" class="module-icon icon32" />'; ?> @@ -704,23 +713,25 @@ function view_calendar() {

'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - if ( isset( $_GET['trashed'] ) && (int) $_GET['trashed'] ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - echo esc_html( sprintf( _n( 'Post moved to the trash.', '%d posts moved to the trash.', $_GET['trashed'] ), number_format_i18n( $_GET['trashed'] ) ) ); - $ids = isset($_GET['ids']) ? $_GET['ids'] : 0; - $pid = explode( ',', $ids ); - $post_type = get_post_type( $pid[0] ); - echo ' ' . esc_html__( 'Undo', 'edit-flow' ) . '
'; - unset( $_GET['trashed'] ); - } - if ( isset($_GET['untrashed'] ) && (int) $_GET['untrashed'] ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - echo esc_html( sprintf( _n( 'Post restored from the Trash.', '%d posts restored from the Trash.', $_GET['untrashed'] ), number_format_i18n( $_GET['untrashed'] ) ) ); - unset( $_GET['undeleted'] ); - } - echo '

'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + if ( isset( $_GET['trashed'] ) || isset( $_GET['untrashed'] ) ) { + + echo '

'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + if ( isset( $_GET['trashed'] ) && (int) $_GET['trashed'] ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + /* translators: %d: number of posts trashed */ + echo esc_html( sprintf( _n( 'Post moved to the trash.', '%d posts moved to the trash.', $_GET['trashed'] ), number_format_i18n( $_GET['trashed'] ) ) ); + $ids = isset( $_GET['ids'] ) ? $_GET['ids'] : 0; + $pid = explode( ',', $ids ); + $post_type = get_post_type( $pid[0] ); + echo ' ' . esc_html__( 'Undo', 'edit-flow' ) . '
'; + unset( $_GET['trashed'] ); + } + if ( isset( $_GET['untrashed'] ) && (int) $_GET['untrashed'] ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + /* translators: %d: number of posts restored */ + echo esc_html( sprintf( _n( 'Post restored from the Trash.', '%d posts restored from the Trash.', $_GET['untrashed'] ), number_format_i18n( $_GET['untrashed'] ) ) ); + unset( $_GET['undeleted'] ); } + echo '

'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } ?>
@@ -730,12 +741,13 @@ function view_calendar() { total_weeks == 1 ) - $table_classes[] = 'one-week-showing'; - elseif ( $this->total_weeks == 2 ) - $table_classes[] = 'two-weeks-showing'; - elseif ( $this->total_weeks == 3 ) - $table_classes[] = 'three-weeks-showing'; + if ( 1 == $this->total_weeks ) { + $table_classes[] = 'one-week-showing'; + } elseif ( 2 == $this->total_weeks ) { + $table_classes[] = 'two-weeks-showing'; + } elseif ( 3 == $this->total_weeks ) { + $table_classes[] = 'three-weeks-showing'; + } $table_classes = apply_filters( 'ef_calendar_table_classes', $table_classes ); ?> @@ -749,7 +761,7 @@ function view_calendar() { total_weeks; $current_week++ ): + for ( $current_week = 1; $current_week <= $this->total_weeks; $current_week++ ) : // We need to set the object variable for our posts_where filter $this->current_week = $current_week; $week_posts = $this->get_calendar_posts_for_week( $post_query_args ); @@ -757,103 +769,108 @@ function view_calendar() { $week_single_date = $this->get_beginning_of_week( $filters['start_date'], $date_format, $current_week ); $week_dates = array(); $split_month = false; - for ( $i = 0 ; $i < 7; $i++ ) { - $week_dates[$i] = $week_single_date; + for ( $i = 0; $i < 7; $i++ ) { + $week_dates[ $i ] = $week_single_date; $single_date_month = date_i18n( 'F', strtotime( $week_single_date ) ); if ( $single_date_month != $current_month ) { $split_month = $single_date_month; $current_month = $single_date_month; } - $week_single_date = date( 'Y-m-d', strtotime( "+1 day", strtotime( $week_single_date ) ) ); + $week_single_date = date( 'Y-m-d', strtotime( '+1 day', strtotime( $week_single_date ) ) ); } - ?> - + ?> + - $week_single_date ) { - if ( date_i18n( 'F', strtotime( $week_single_date ) ) != $split_month && date_i18n( 'F', strtotime( "+1 day", strtotime( $week_single_date ) ) ) == $split_month ) { - $previous_month = date_i18n( 'F', strtotime( $week_single_date ) ); - echo '' . esc_html( $previous_month ) . ''; - } else if ( date_i18n( 'F', strtotime( $week_single_date ) ) == $split_month && date_i18n( 'F', strtotime( "-1 day", strtotime( $week_single_date ) ) ) != $split_month ) { - echo '' . esc_html( $split_month ) . ''; - } else { - echo ''; + $week_single_date ) { + if ( date_i18n( 'F', strtotime( $week_single_date ) ) != $split_month && date_i18n( 'F', strtotime( '+1 day', strtotime( $week_single_date ) ) ) == $split_month ) { + $previous_month = date_i18n( 'F', strtotime( $week_single_date ) ); + echo '' . esc_html( $previous_month ) . ''; + } else if ( date_i18n( 'F', strtotime( $week_single_date ) ) == $split_month && date_i18n( 'F', strtotime( '-1 day', strtotime( $week_single_date ) ) ) != $split_month ) { + echo '' . esc_html( $split_month ) . ''; + } else { + echo ''; + } } - } ?> + ?> - $week_single_date ): ?> - name] = array(); - } - // These statuses aren't handled by custom statuses or post statuses - $week_posts_by_status['private'] = array(); - $week_posts_by_status['publish'] = array(); - $week_posts_by_status['future'] = array(); - foreach( $week_posts[$week_single_date] as $num => $post ) { - $week_posts_by_status[$post->post_status][$num] = $post; - } - unset( $week_posts[$week_single_date] ); - foreach( $week_posts_by_status as $status ) { - foreach( $status as $num => $post ) { - $week_posts[$week_single_date][] = $post; + $week_single_date ) : ?> + name ] = array(); + } + // These statuses aren't handled by custom statuses or post statuses + $week_posts_by_status['private'] = array(); + $week_posts_by_status['publish'] = array(); + $week_posts_by_status['future'] = array(); + foreach ( $week_posts[ $week_single_date ] as $num => $post ) { + $week_posts_by_status[ $post->post_status ][ $num ] = $post; + } + unset( $week_posts[ $week_single_date ] ); + foreach ( $week_posts_by_status as $status ) { + foreach ( $status as $num => $post ) { + $week_posts[ $week_single_date ][] = $post; + } } } - } - $td_classes = array( - 'day-unit', - ); - $day_name = date( 'D', strtotime( $week_single_date ) ); + $td_classes = array( + 'day-unit', + ); + $day_name = date( 'D', strtotime( $week_single_date ) ); - if ( in_array( $day_name, $dotw ) ) - $td_classes[] = 'weekend-day'; + if ( in_array( $day_name, $dotw ) ) { + $td_classes[] = 'weekend-day'; + } - if ( $week_single_date == date( 'Y-m-d', current_time( 'timestamp' ) ) ) - $td_classes[] = 'today'; + if ( date( 'Y-m-d', current_time( 'timestamp' ) ) == $week_single_date ) { + $td_classes[] = 'today'; + } - // Last day of the week - if ( $day_num == 6 ) - $td_classes[] = 'last-day'; + // Last day of the week + if ( 6 == $day_num ) { + $td_classes[] = 'last-day'; + } - $td_classes = apply_filters( 'ef_calendar_table_td_classes', $td_classes, $week_single_date ); - ?> + $td_classes = apply_filters( 'ef_calendar_table_td_classes', $td_classes, $week_single_date ); + ?> - +
- hidden ): ?> - hidden ) ); ?> + hidden ) : ?> + hidden ) ); ?> - create_post_cap ) ) : - $date_formatted = date( 'D, M jS, Y', strtotime( $week_single_date ) ); - ?> + create_post_cap ) ) : + $date_formatted = date( 'D, M jS, Y', strtotime( $week_single_date ) ); + ?>
@@ -862,12 +879,12 @@ function view_calendar() {
- - get_quick_create_post_type_name() ) ); ?> » + + get_quick_create_post_type_name() ) ); ?> »
 
- + @@ -879,64 +896,66 @@ function view_calendar() { + wp_nonce_field( 'ef-calendar-modify', 'ef-calendar-modify' ); + ?>
- current_user_can_modify_post( $post ) ) ? 'can_modify' : 'read_only'; - $cache_key = $post->ID . $can_modify . '_' . get_current_user_id(); - $cache_val = wp_cache_get( $cache_key, self::$post_li_html_cache_key ); - // Because $num is pertinent to the display of the post LI, need to make sure that's what's in cache - if ( is_array( $cache_val ) && $cache_val['num'] == $num ) { - $this->hidden = $cache_val['hidden']; - return $cache_val['post_li_html']; + ID; - $edit_post_link = get_edit_post_link( $post_id ); - $status_object = get_post_status_object( get_post_status( $post_id ) ); + /** + * Generates the HTML for a single post item in the calendar + * @param obj $post The WordPress post in question + * @param str $post_date The date of the post + * @param int $num The index of the post + * + * @return str HTML for a single post item + */ + public function generate_post_li_html( $post, $post_date, $num = 0 ) { + + $can_modify = ( $this->current_user_can_modify_post( $post ) ) ? 'can_modify' : 'read_only'; + $cache_key = $post->ID . $can_modify . '_' . get_current_user_id(); + $cache_val = wp_cache_get( $cache_key, self::$post_li_html_cache_key ); + // Because $num is pertinent to the display of the post LI, need to make sure that's what's in cache + if ( is_array( $cache_val ) && $cache_val['num'] == $num ) { + $this->hidden = $cache_val['hidden']; + return $cache_val['post_li_html']; + } - $post_classes = array( - 'day-item', - 'custom-status-' . $post->post_status, - ); - // Only allow the user to drag the post if they have permissions to - // or if it's in an approved post status - // This is checked on the ajax request too. - if ( $this->current_user_can_modify_post( $post ) && !in_array( $post->post_status, $this->published_statuses ) ) - $post_classes[] = 'sortable'; + ob_start(); + $post_id = $post->ID; + $edit_post_link = get_edit_post_link( $post_id ); + $status_object = get_post_status_object( get_post_status( $post_id ) ); - if ( in_array( $post->post_status, $this->published_statuses ) ) - $post_classes[] = 'is-published'; + $post_classes = array( + 'day-item', + 'custom-status-' . $post->post_status, + ); + // Only allow the user to drag the post if they have permissions to + // or if it's in an approved post status + // This is checked on the ajax request too. + if ( $this->current_user_can_modify_post( $post ) && ! in_array( $post->post_status, $this->published_statuses ) ) { + $post_classes[] = 'sortable'; + } - // Hide posts over a certain number to prevent clutter, unless user is only viewing 1 or 2 weeks - $max_visible_posts = apply_filters( 'ef_calendar_max_visible_posts_per_date', $this->max_visible_posts_per_date); + if ( in_array( $post->post_status, $this->published_statuses ) ) { + $post_classes[] = 'is-published'; + } - if ( $num >= $max_visible_posts && $this->total_weeks > 2 ) { - $post_classes[] = 'hidden'; - $this->hidden++; - } - $post_classes = apply_filters( 'ef_calendar_table_td_li_classes', $post_classes, $post_date, $post->ID ); + // Hide posts over a certain number to prevent clutter, unless user is only viewing 1 or 2 weeks + $max_visible_posts = apply_filters( 'ef_calendar_max_visible_posts_per_date', $this->max_visible_posts_per_date ); + + if ( $num >= $max_visible_posts && $this->total_weeks > 2 ) { + $post_classes[] = 'hidden'; + $this->hidden++; + } + $post_classes = apply_filters( 'ef_calendar_table_td_li_classes', $post_classes, $post_date, $post->ID ); - ?> + ?>
  • @@ -952,51 +971,51 @@ function generate_post_li_html( $post, $post_date, $num = 0 ){
  • - $num, - 'post_li_html' => $post_li_html, - 'hidden' => $this->hidden, + $post_li_cache = array( + 'num' => $num, + 'post_li_html' => $post_li_html, + 'hidden' => $this->hidden, ); - wp_cache_set( $cache_key, $post_li_cache, self::$post_li_html_cache_key ); - - return $post_li_html; - - } // generate_post_li_html() - - /** - * get_inner_information description - * Functionality for generating the inner html elements on the calendar - * has been separated out so various ajax functions can reload certain - * parts of an inner html element. - * @param array $ef_calendar_item_information_fields - * @param WP_Post $post - * @param array $published_statuses - * - * @since 0.8 - */ - function get_inner_information( $ef_calendar_item_information_fields, $post ) { - ?> + wp_cache_set( $cache_key, $post_li_cache, self::$post_li_html_cache_key ); + + return $post_li_html; + } // generate_post_li_html() + + /** + * get_inner_information description + * Functionality for generating the inner html elements on the calendar + * has been separated out so various ajax functions can reload certain + * parts of an inner html element. + * @param array $ef_calendar_item_information_fields + * @param WP_Post $post + * @param array $published_statuses + * + * @since 0.8 + */ + public function get_inner_information( $ef_calendar_item_information_fields, $post ) { + ?> - get_post_information_fields( $post ) as $field => $values ): ?> + get_post_information_fields( $post ) as $field => $values ) : ?> - - current_user_can_modify_post( $post ) ) : ?> - - + + current_user_can_modify_post( $post ) ) : ?> + + + - + - + - + @@ -1006,481 +1025,498 @@ function get_inner_information( $ef_calendar_item_information_fields, $post ) { post_type ); $item_actions = array(); - if ( $this->current_user_can_modify_post( $post ) ) { - // Edit this post - $item_actions['edit'] = '' . __( 'Edit', 'edit-flow' ) . ''; - // Trash this post - $item_actions['trash'] = '' . __( 'Trash', 'edit-flow' ) . ''; - // Preview/view this post - if ( !in_array( $post->post_status, $this->published_statuses ) ) { - $item_actions['view'] = '' . __( 'Preview', 'edit-flow' ) . ''; - } elseif ( 'trash' != $post->post_status ) { - $item_actions['view'] = '' . __( 'View', 'edit-flow' ) . ''; - } - //Save metadata - $item_actions['save hidden'] = '' . __( 'Save', 'edit-flow') . ''; + if ( $this->current_user_can_modify_post( $post ) ) { + // Edit this post + $item_actions['edit'] = '' . __( 'Edit', 'edit-flow' ) . ''; + // Trash this post + $item_actions['trash'] = '' . __( 'Trash', 'edit-flow' ) . ''; + // Preview/view this post + if ( ! in_array( $post->post_status, $this->published_statuses ) ) { + /* translators: %s: post title */ + $item_actions['view'] = '' . __( 'Preview', 'edit-flow' ) . ''; + } elseif ( 'trash' != $post->post_status ) { + /* translators: %s: post title */ + $item_actions['view'] = '' . __( 'View', 'edit-flow' ) . ''; } + //Save metadata + /* translators: %s: post title */ + $item_actions['save hidden'] = '' . __( 'Save', 'edit-flow' ) . ''; + } // Allow other plugins to add actions $item_actions = apply_filters( 'ef_calendar_item_actions', $item_actions, $post->ID ); - if ( count( $item_actions ) ) { - echo '
    '; - $html = ''; - foreach ( $item_actions as $class => $item_action ) { - $html .= '' . $item_action . ' | '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - } - echo rtrim( $html, '| ' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - echo '
    '; + if ( count( $item_actions ) ) { + echo '
    '; + $html = ''; + foreach ( $item_actions as $class => $item_action ) { + $html .= '' . $item_action . ' | '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } + echo rtrim( $html, '| ' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo '
    '; + } ?>
    - '; - break; - case 'paragraph': - return ''; - break; - case 'date': - return ''; - break; - case 'checkbox': - $output = ''; - - return $output; - break; - case 'user': - return wp_dropdown_users( array( 'echo' => false ) ); - break; - case 'taxonomy': - return ''; - break; - case 'taxonomy hierarchical': - return wp_dropdown_categories( array( 'echo' => 0, 'hide_empty' => 0 ) ); - break; + '; + break; + case 'paragraph': + return ''; + break; + case 'date': + return ''; + break; + case 'checkbox': + $output = ''; + + return $output; + break; + case 'user': + return wp_dropdown_users( array( 'echo' => false ) ); + break; + case 'taxonomy': + return ''; + break; + case 'taxonomy hierarchical': + return wp_dropdown_categories( array( + 'echo' => 0, + 'hide_empty' => 0, + ) ); + break; + } } - } - - /** - * Get the information fields to be presented with each post popup - * - * @since 0.8 - * - * @param obj $post Post to gather information fields for - * @return array $information_fields All of the information fields to be presented - */ - function get_post_information_fields( $post ) { - - $information_fields = array(); - // Post author - $information_fields['author'] = array( - 'label' => __( 'Author', 'edit-flow' ), - 'value' => get_the_author_meta( 'display_name', $post->post_author ), - 'type' => 'author', - ); - // If the calendar supports more than one post type, show the post type label - if ( count( $this->get_post_types_for_module( $this->module ) ) > 1 ) { - $information_fields['post_type'] = array( - 'label' => __( 'Post Type', 'edit-flow' ), - 'value' => get_post_type_object( $post->post_type )->labels->singular_name, + /** + * Get the information fields to be presented with each post popup + * + * @since 0.8 + * + * @param obj $post Post to gather information fields for + * @return array $information_fields All of the information fields to be presented + */ + public function get_post_information_fields( $post ) { + + $information_fields = array(); + // Post author + $information_fields['author'] = array( + 'label' => __( 'Author', 'edit-flow' ), + 'value' => get_the_author_meta( 'display_name', $post->post_author ), + 'type' => 'author', ); - } - // Publication time for published statuses - $published_statuses = array( - 'publish', - 'future', - 'private', - ); - if ( in_array( $post->post_status, $published_statuses ) ) { - if ( $post->post_status == 'future' ) { - $information_fields['post_date'] = array( - 'label' => __( 'Scheduled', 'edit-flow' ), - 'value' => get_the_time( null, $post->ID ), - ); - } else { - $information_fields['post_date'] = array( - 'label' => __( 'Published', 'edit-flow' ), - 'value' => get_the_time( null, $post->ID ), + + // If the calendar supports more than one post type, show the post type label + if ( count( $this->get_post_types_for_module( $this->module ) ) > 1 ) { + $information_fields['post_type'] = array( + 'label' => __( 'Post Type', 'edit-flow' ), + 'value' => get_post_type_object( $post->post_type )->labels->singular_name, ); } - } - // Taxonomies and their values - $args = array( - 'post_type' => $post->post_type, - ); - $taxonomies = get_object_taxonomies( $args, 'object' ); - foreach( (array)$taxonomies as $taxonomy ) { - // Sometimes taxonomies skip by, so let's make sure it has a label too - if ( !$taxonomy->public || !$taxonomy->label ) - continue; - - $terms = get_the_terms( $post->ID, $taxonomy->name ); - if ( ! $terms || is_wp_error( $terms ) ) - continue; - - $key = 'tax_' . $taxonomy->name; - if ( count( $terms ) ) { - $value = ''; - foreach( (array)$terms as $term ) { - $value .= $term->name . ', '; + // Publication time for published statuses + $published_statuses = array( + 'publish', + 'future', + 'private', + ); + if ( in_array( $post->post_status, $published_statuses ) ) { + if ( 'future' == $post->post_status ) { + $information_fields['post_date'] = array( + 'label' => __( 'Scheduled', 'edit-flow' ), + 'value' => get_the_time( null, $post->ID ), + ); + } else { + $information_fields['post_date'] = array( + 'label' => __( 'Published', 'edit-flow' ), + 'value' => get_the_time( null, $post->ID ), + ); } - $value = rtrim( $value, ', ' ); - } else { - $value = ''; - } - //Used when editing editorial metadata and post meta - if ( is_taxonomy_hierarchical( $taxonomy->name ) ) - $type = 'taxonomy hierarchical'; - else - $type = 'taxonomy'; - - $information_fields[$key] = array( - 'label' => $taxonomy->label, - 'value' => $value, - 'type' => $type, + } + // Taxonomies and their values + $args = array( + 'post_type' => $post->post_type, ); + $taxonomies = get_object_taxonomies( $args, 'object' ); + foreach ( (array) $taxonomies as $taxonomy ) { + // Sometimes taxonomies skip by, so let's make sure it has a label too + if ( ! $taxonomy->public || ! $taxonomy->label ) { + continue; + } - if( $post->post_type == 'page' ) - $ed_cap = 'edit_page'; - else - $ed_cap = 'edit_post'; + $terms = get_the_terms( $post->ID, $taxonomy->name ); + if ( ! $terms || is_wp_error( $terms ) ) { + continue; + } + + $key = 'tax_' . $taxonomy->name; + if ( count( $terms ) ) { + $value = ''; + foreach ( (array) $terms as $term ) { + $value .= $term->name . ', '; + } + $value = rtrim( $value, ', ' ); + } else { + $value = ''; + } + //Used when editing editorial metadata and post meta + if ( is_taxonomy_hierarchical( $taxonomy->name ) ) { + $type = 'taxonomy hierarchical'; + } else { + $type = 'taxonomy'; + } + + $information_fields[ $key ] = array( + 'label' => $taxonomy->label, + 'value' => $value, + 'type' => $type, + ); + + if ( 'page' == $post->post_type ) { + $ed_cap = 'edit_page'; + } else { + $ed_cap = 'edit_post'; + } + + if ( current_user_can( $ed_cap, $post->ID ) ) { + $information_fields[ $key ]['editable'] = true; + } + } - if( current_user_can( $ed_cap, $post->ID ) ) - $information_fields[$key]['editable'] = true; + $information_fields = apply_filters( 'ef_calendar_item_information_fields', $information_fields, $post->ID ); + foreach ( $information_fields as $field => $values ) { + // Allow filters to hide empty fields or to hide any given individual field. Hide empty fields by default. + if ( ( apply_filters( 'ef_calendar_hide_empty_item_information_fields', true, $post->ID ) && empty( $values['value'] ) ) + || apply_filters( "ef_calendar_hide_{$field}_item_information_field", false, $post->ID ) ) { + unset( $information_fields[ $field ] ); + } + } + return $information_fields; } - $information_fields = apply_filters( 'ef_calendar_item_information_fields', $information_fields, $post->ID ); - foreach( $information_fields as $field => $values ) { - // Allow filters to hide empty fields or to hide any given individual field. Hide empty fields by default. - if ( ( apply_filters( 'ef_calendar_hide_empty_item_information_fields', true, $post->ID ) && empty( $values['value'] ) ) - || apply_filters( "ef_calendar_hide_{$field}_item_information_field", false, $post->ID ) ) - unset( $information_fields[$field] ); + /** + * Generate the calendar header for a given range of dates + * + * @param array $dates Date range for the header + * @return string $html Generated HTML for the header + */ + public function get_time_period_header( $dates ) { + + $html = ''; + foreach ( $dates as $date ) { + $html .= ''; + } + + return $html; } - return $information_fields; - } - - /** - * Generate the calendar header for a given range of dates - * - * @param array $dates Date range for the header - * @return string $html Generated HTML for the header - */ - function get_time_period_header( $dates ) { - - $html = ''; - foreach( $dates as $date ) { - $html .= ''; + + /** + * Query to get all of the calendar posts for a given day + * + * @param array $args Any filter arguments we want to pass + * @param string $request_context Where the query is coming from, to distinguish dashboard and subscriptions + * @return array $posts All of the posts as an array sorted by date + */ + public function get_calendar_posts_for_week( $args = array(), $context = 'dashboard' ) { + + $supported_post_types = $this->get_post_types_for_module( $this->module ); + $defaults = array( + 'post_status' => null, + 'cat' => null, + 'author' => null, + 'post_type' => $supported_post_types, + 'posts_per_page' => 200, + ); + + $args = array_merge( $defaults, $args ); + + // Unpublished as a status is just an array of everything but 'publish'. + if ( 'unpublish' == $args['post_status'] ) { + $args['post_status'] = ''; + $post_stati = wp_filter_object_list( $this->get_calendar_post_stati(), array( 'name' => 'publish' ), 'not' ); + + if ( ! apply_filters( 'ef_show_scheduled_as_unpublished', false ) ) { + $post_stati = wp_filter_object_list( $post_stati, array( 'name' => 'future' ), 'not' ); + } + + $args['post_status'] .= implode( ',', wp_list_pluck( $post_stati, 'name' ) ); + } + // The WP functions for printing the category and author assign a value of 0 to the default + // options, but passing this to the query is bad (trashed and auto-draft posts appear!), so + // unset those arguments. + if ( '0' === $args['cat'] ) { + unset( $args['cat'] ); + } + if ( '0' === $args['author'] ) { + unset( $args['author'] ); + } + + if ( empty( $args['post_type'] ) || ! in_array( $args['post_type'], $supported_post_types ) ) { + $args['post_type'] = $supported_post_types; + } + + $beginning_date = $this->get_beginning_of_week( $this->start_date, 'Y-m-d', $this->current_week ); + $ending_date = date( 'Y-m-d', strtotime( $beginning_date ) + WEEK_IN_SECONDS ); + + $args['date_query'] = array( + 'after' => $beginning_date, + 'before' => $ending_date, + 'inclusive' => true, + ); + + // Filter for an end user to implement any of their own query args + $args = apply_filters( 'ef_calendar_posts_query_args', $args, $context ); + $post_results = new WP_Query( $args ); + + $posts = array(); + while ( $post_results->have_posts() ) { + $post_results->the_post(); + global $post; + $key_date = date( 'Y-m-d', strtotime( $post->post_date ) ); + $posts[ $key_date ][] = $post; + } + + return $posts; } - return $html; - - } - - /** - * Query to get all of the calendar posts for a given day - * - * @param array $args Any filter arguments we want to pass - * @param string $request_context Where the query is coming from, to distinguish dashboard and subscriptions - * @return array $posts All of the posts as an array sorted by date - */ - function get_calendar_posts_for_week( $args = array(), $context = 'dashboard' ) { - - $supported_post_types = $this->get_post_types_for_module( $this->module ); - $defaults = array( - 'post_status' => null, - 'cat' => null, - 'author' => null, - 'post_type' => $supported_post_types, - 'posts_per_page' => 200, - ); + /** + * Gets the link for the next time period + * + * @param string $direction 'previous' or 'next', direction to go in time + * @param array $filters Any filters that need to be applied + * @param int $weeks_offset Number of weeks we're offsetting the range + * @return string $url The URL for the next page + */ + public function get_pagination_link( $direction = 'next', $filters = array(), $weeks_offset = null ) { - $args = array_merge( $defaults, $args ); + $supported_post_types = $this->get_post_types_for_module( $this->module ); - // Unpublished as a status is just an array of everything but 'publish'. - if ( 'unpublish' == $args['post_status'] ) { - $args['post_status'] = ''; - $post_stati = wp_filter_object_list( $this->get_calendar_post_stati(), array( 'name' => 'publish' ), 'not' ); + if ( ! isset( $weeks_offset ) ) { + $weeks_offset = $this->total_weeks; + } else if ( 0 == $weeks_offset ) { + $filters['start_date'] = $this->get_beginning_of_week( date( 'Y-m-d', current_time( 'timestamp' ) ) ); + } - if ( ! apply_filters( 'ef_show_scheduled_as_unpublished', false ) ) { - $post_stati = wp_filter_object_list( $post_stati, array( 'name' => 'future' ), 'not' ); + if ( 'previous' == $direction ) { + $weeks_offset = '-' . $weeks_offset; } - $args['post_status'] .= implode( ',', wp_list_pluck( $post_stati, 'name' ) ); + $filters['start_date'] = date( 'Y-m-d', strtotime( $weeks_offset . ' weeks', strtotime( $filters['start_date'] ) ) ); + $url = add_query_arg( $filters, menu_page_url( $this->module->slug, false ) ); + + if ( count( $supported_post_types ) > 1 ) { + $url = add_query_arg( 'cpt', $filters['cpt'], $url ); + } + + return $url; } - // The WP functions for printing the category and author assign a value of 0 to the default - // options, but passing this to the query is bad (trashed and auto-draft posts appear!), so - // unset those arguments. - if ( $args['cat'] === '0' ) { - unset( $args['cat'] ); + + /** + * Given a day in string format, returns the day at the beginning of that week, which can be the given date. + * The beginning of the week is determined by the blog option, 'start_of_week'. + * + * @see http://www.php.net/manual/en/datetime.formats.date.php for valid date formats + * + * @param string $date String representing a date + * @param string $format Date format in which the beginning of the week should be returned + * @param int $week Number of weeks we're offsetting the range + * @return string $formatted_start_of_week Beginning of the week + */ + public function get_beginning_of_week( $date, $format = 'Y-m-d', $week = 1 ) { + + $date = strtotime( $date ); + $start_of_week = get_option( 'start_of_week' ); + $day_of_week = date( 'w', $date ); + $date += ( ( $start_of_week - $day_of_week - 7 ) % 7 ) * 60 * 60 * 24; + $date = strtotime( '+' . ( $week - 1 ) . ' week', $date ); + $formatted_start_of_week = date( $format, $date ); + return $formatted_start_of_week; } - if ( $args['author'] === '0' ) { - unset( $args['author'] ); + + /** + * Given a day in string format, returns the day at the end of that week, which can be the given date. + * The end of the week is determined by the blog option, 'start_of_week'. + * + * @see http://www.php.net/manual/en/datetime.formats.date.php for valid date formats + * + * @param string $date String representing a date + * @param string $format Date format in which the end of the week should be returned + * @param int $week Number of weeks we're offsetting the range + * @return string $formatted_end_of_week End of the week + */ + public function get_ending_of_week( $date, $format = 'Y-m-d', $week = 1 ) { + + $date = strtotime( $date ); + $end_of_week = get_option( 'start_of_week' ) - 1; + $day_of_week = date( 'w', $date ); + $date += ( ( $end_of_week - $day_of_week + 7 ) % 7 ) * 60 * 60 * 24; + $date = strtotime( '+' . ( $week - 1 ) . ' week', $date ); + $formatted_end_of_week = date( $format, $date ); + return $formatted_end_of_week; } - if ( empty( $args['post_type'] ) || ! in_array( $args['post_type'], $supported_post_types ) ) { - $args['post_type'] = $supported_post_types; + /** + * Human-readable time range for the calendar + * Shows something like "for October 30th through November 26th" for a four-week period + * + * @since 0.7 + */ + public function calendar_time_range() { + + $first_datetime = strtotime( $this->start_date ); + $first_date = date_i18n( get_option( 'date_format' ), $first_datetime ); + $total_days = ( $this->total_weeks * 7 ) - 1; + $last_datetime = strtotime( '+' . $total_days . ' days', date( 'U', strtotime( $this->start_date ) ) ); + $last_date = date_i18n( get_option( 'date_format' ), $last_datetime ); + // translators: %1$s = first date, %2$s = last date + echo esc_html( sprintf( __( 'for %1$s through %2$s', 'edit-flow' ), $first_date, $last_date ) ); } - $beginning_date = $this->get_beginning_of_week( $this->start_date, 'Y-m-d', $this->current_week ); - $ending_date = date( "Y-m-d", strtotime( $beginning_date ) + WEEK_IN_SECONDS ); + /** + * Check whether the current user should have the ability to modify the post + * + * @since 0.7 + * + * @param object $post The post object we're checking + * @return bool $can Whether or not the current user can modify the post + */ + public function current_user_can_modify_post( $post ) { - $args['date_query'] = array( - 'after' => $beginning_date, - 'before' => $ending_date, - 'inclusive' => true, - ); + if ( ! $post ) { + return false; + } - // Filter for an end user to implement any of their own query args - $args = apply_filters( 'ef_calendar_posts_query_args', $args, $context ); - $post_results = new WP_Query( $args ); + $post_type_object = get_post_type_object( $post->post_type ); - $posts = array(); - while ( $post_results->have_posts() ) { - $post_results->the_post(); - global $post; - $key_date = date( 'Y-m-d', strtotime( $post->post_date ) ); - $posts[$key_date][] = $post; - } + // Editors and admins are fine + if ( current_user_can( $post_type_object->cap->edit_others_posts, $post->ID ) ) { + return true; + } + // Authors and contributors can move their own stuff if it's not published + if ( current_user_can( $post_type_object->cap->edit_post, $post->ID ) && wp_get_current_user()->ID == $post->post_author && ! in_array( $post->post_status, $this->published_statuses ) ) { + return true; + } + // Those who can publish posts can move any of their own stuff + if ( current_user_can( $post_type_object->cap->publish_posts, $post->ID ) && wp_get_current_user()->ID == $post->post_author ) { + return true; + } - return $posts; - - } - - /** - * Gets the link for the next time period - * - * @param string $direction 'previous' or 'next', direction to go in time - * @param array $filters Any filters that need to be applied - * @param int $weeks_offset Number of weeks we're offsetting the range - * @return string $url The URL for the next page - */ - function get_pagination_link( $direction = 'next', $filters = array(), $weeks_offset = null ) { - - $supported_post_types = $this->get_post_types_for_module( $this->module ); - - if ( !isset( $weeks_offset ) ) - $weeks_offset = $this->total_weeks; - else if ( $weeks_offset == 0 ) - $filters['start_date'] = $this->get_beginning_of_week( date( 'Y-m-d', current_time( 'timestamp' ) ) ); - - if ( $direction == 'previous' ) - $weeks_offset = '-' . $weeks_offset; - - $filters['start_date'] = date( 'Y-m-d', strtotime( $weeks_offset . " weeks", strtotime( $filters['start_date'] ) ) ); - $url = add_query_arg( $filters, menu_page_url( $this->module->slug, false ) ); - - if ( count( $supported_post_types ) > 1 ) - $url = add_query_arg( 'cpt', $filters['cpt'] , $url ); - - return $url; - - } - - /** - * Given a day in string format, returns the day at the beginning of that week, which can be the given date. - * The beginning of the week is determined by the blog option, 'start_of_week'. - * - * @see http://www.php.net/manual/en/datetime.formats.date.php for valid date formats - * - * @param string $date String representing a date - * @param string $format Date format in which the beginning of the week should be returned - * @param int $week Number of weeks we're offsetting the range - * @return string $formatted_start_of_week Beginning of the week - */ - function get_beginning_of_week( $date, $format = 'Y-m-d', $week = 1 ) { - - $date = strtotime( $date ); - $start_of_week = get_option( 'start_of_week' ); - $day_of_week = date( 'w', $date ); - $date += (( $start_of_week - $day_of_week - 7 ) % 7) * 60 * 60 * 24 ; - $date = strtotime ( '+' . ( $week - 1 ) . ' week', $date ) ; - $formatted_start_of_week = date( $format, $date ); - return $formatted_start_of_week; - - } - - /** - * Given a day in string format, returns the day at the end of that week, which can be the given date. - * The end of the week is determined by the blog option, 'start_of_week'. - * - * @see http://www.php.net/manual/en/datetime.formats.date.php for valid date formats - * - * @param string $date String representing a date - * @param string $format Date format in which the end of the week should be returned - * @param int $week Number of weeks we're offsetting the range - * @return string $formatted_end_of_week End of the week - */ - function get_ending_of_week( $date, $format = 'Y-m-d', $week = 1 ) { - - $date = strtotime( $date ); - $end_of_week = get_option( 'start_of_week' ) - 1; - $day_of_week = date( 'w', $date ); - $date += (( $end_of_week - $day_of_week + 7 ) % 7) * 60 * 60 * 24; - $date = strtotime ( '+' . ( $week - 1 ) . ' week', $date ) ; - $formatted_end_of_week = date( $format, $date ); - return $formatted_end_of_week; - - } - - /** - * Human-readable time range for the calendar - * Shows something like "for October 30th through November 26th" for a four-week period - * - * @since 0.7 - */ - function calendar_time_range() { - - $first_datetime = strtotime( $this->start_date ); - $first_date = date_i18n( get_option( 'date_format' ), $first_datetime ); - $total_days = ( $this->total_weeks * 7 ) - 1; - $last_datetime = strtotime( "+" . $total_days . " days", date( 'U', strtotime( $this->start_date ) ) ); - $last_date = date_i18n( get_option( 'date_format' ), $last_datetime ); - echo esc_html( sprintf( __( 'for %1$s through %2$s', 'edit-flow' ), $first_date, $last_date ) ); - } - - /** - * Check whether the current user should have the ability to modify the post - * - * @since 0.7 - * - * @param object $post The post object we're checking - * @return bool $can Whether or not the current user can modify the post - */ - function current_user_can_modify_post( $post ) { - - if ( !$post ) return false; + } - $post_type_object = get_post_type_object( $post->post_type ); - - // Editors and admins are fine - if ( current_user_can( $post_type_object->cap->edit_others_posts, $post->ID ) ) - return true; - // Authors and contributors can move their own stuff if it's not published - if ( current_user_can( $post_type_object->cap->edit_post, $post->ID ) && wp_get_current_user()->ID == $post->post_author && !in_array( $post->post_status, $this->published_statuses ) ) - return true; - // Those who can publish posts can move any of their own stuff - if ( current_user_can( $post_type_object->cap->publish_posts, $post->ID ) && wp_get_current_user()->ID == $post->post_author ) - return true; - - return false; - } - - /** - * Register settings for notifications so we can partially use the Settings API - * We use the Settings API for form generation, but not saving because we have our - * own way of handling the data. - * - * @since 0.7 - */ - function register_settings() { + /** + * Register settings for notifications so we can partially use the Settings API + * We use the Settings API for form generation, but not saving because we have our + * own way of handling the data. + * + * @since 0.7 + */ + public function register_settings() { add_settings_section( $this->module->options_group_name . '_general', false, '__return_false', $this->module->options_group_name ); add_settings_field( 'post_types', __( 'Post types to show', 'edit-flow' ), array( $this, 'settings_post_types_option' ), $this->module->options_group_name, $this->module->options_group_name . '_general' ); add_settings_field( 'quick_create_post_type', __( 'Post type to create directly from calendar', 'edit-flow' ), array( $this, 'settings_quick_create_post_type_option' ), $this->module->options_group_name, $this->module->options_group_name . '_general' ); add_settings_field( 'ics_subscription', __( 'Subscription in iCal or Google Calendar', 'edit-flow' ), array( $this, 'settings_ics_subscription_option' ), $this->module->options_group_name, $this->module->options_group_name . '_general' ); + } - } - - /** - * Choose the post types that should be displayed on the calendar - * - * @since 0.7 - */ - function settings_post_types_option() { - global $edit_flow; - $edit_flow->settings->helper_option_custom_post_type( $this->module ); - } - - /** - * Choose the post type that should be created on the calendar - * - * @since 0.8 - */ - function settings_quick_create_post_type_option() { - - $allowed_post_types = $this->get_all_post_types(); - - echo ""; - - } - - /** - * Enable calendar subscriptions via .ics in iCal or Google Calendar - * - * @since 0.8 - */ - function settings_ics_subscription_option() { - $options = array( - 'off' => __( 'Disabled', 'edit-flow' ), - 'on' => __( 'Enabled', 'edit-flow' ), - ); - echo ''; + /** + * Choose the post type that should be created on the calendar + * + * @since 0.8 + */ + public function settings_quick_create_post_type_option() { - $regenerate_url = add_query_arg( 'action', 'ef_calendar_regenerate_calendar_feed_secret', admin_url( 'index.php' ) ); - $regenerate_url = wp_nonce_url( $regenerate_url, 'ef-regenerate-ics-key' ); - echo '   ' . esc_html_e( 'Regenerate calendar feed secret', 'edit-flow' ) . ''; + $allowed_post_types = $this->get_all_post_types(); - // If our secret key doesn't exist, create a new one - if ( empty( $this->module->options->ics_secret_key ) ) - EditFlow()->update_module_option( $this->module->name, 'ics_secret_key', wp_generate_password() ); - } + echo "'; + } + + /** + * Enable calendar subscriptions via .ics in iCal or Google Calendar + * + * @since 0.8 + */ + public function settings_ics_subscription_option() { + $options = array( + 'off' => __( 'Disabled', 'edit-flow' ), + 'on' => __( 'Enabled', 'edit-flow' ), + ); + echo ''; + + + $regenerate_url = add_query_arg( 'action', 'ef_calendar_regenerate_calendar_feed_secret', admin_url( 'index.php' ) ); + $regenerate_url = wp_nonce_url( $regenerate_url, 'ef-regenerate-ics-key' ); + echo '   ' . esc_html_e( 'Regenerate calendar feed secret', 'edit-flow' ) . ''; + + // If our secret key doesn't exist, create a new one + if ( empty( $this->module->options->ics_secret_key ) ) { + EditFlow()->update_module_option( $this->module->name, 'ics_secret_key', wp_generate_password() ); + } + } - /** - * Validate the data submitted by the user in calendar settings - * - * @since 0.7 - */ - function settings_validate( $new_options ) { + /** + * Validate the data submitted by the user in calendar settings + * + * @since 0.7 + */ + public function settings_validate( $new_options ) { - $options = (array)$this->module->options; + $options = (array) $this->module->options; - $options['post_types'] = $this->clean_post_type_options( $new_options['post_types'], $this->module->post_type_support ); + $options['post_types'] = $this->clean_post_type_options( $new_options['post_types'], $this->module->post_type_support ); - if ( in_array( $new_options['quick_create_post_type'], array_keys( $this->get_all_post_types() ) ) ) - $options['quick_create_post_type'] = $new_options['quick_create_post_type']; + if ( in_array( $new_options['quick_create_post_type'], array_keys( $this->get_all_post_types() ) ) ) { + $options['quick_create_post_type'] = $new_options['quick_create_post_type']; + } - if ( 'on' != $new_options['ics_subscription'] ) - $options['ics_subscription'] = 'off'; - else - $options['ics_subscription'] = 'on'; + if ( 'on' != $new_options['ics_subscription'] ) { + $options['ics_subscription'] = 'off'; + } else { + $options['ics_subscription'] = 'on'; + } - return $options; - } + return $options; + } - /** - * Settings page for calendar - */ - function print_configure_view() { - global $edit_flow; - ?> + /** + * Settings page for calendar + */ + public function print_configure_view() { + global $edit_flow; + ?> module->options_group_name ); ?> module->options_group_name ); ?> @@ -1489,380 +1525,392 @@ function print_configure_view() { ?>

    - print_ajax_response( 'error', $this->module->messages['nonce-failed'] ); + create_post_cap ) ) - $this->print_ajax_response( 'error', $this->module->messages['invalid-permissions'] ); + /** + * Ajax callback to insert a post placeholder for a particular date + * + * @since 0.8 + */ + public function handle_ajax_insert_post() { - if ( empty( $_POST['ef_insert_date'] ) ) { - $this->print_ajax_response( 'error', __( 'No date supplied.', 'edit-flow' ) ); - } + // Nonce check! + if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'ef-calendar-modify' ) ) { + $this->print_ajax_response( 'error', $this->module->messages['nonce-failed'] ); + } - // Post type has to be visible on the calendar to create a placeholder - if ( ! in_array( $this->module->options->quick_create_post_type, $this->get_post_types_for_module( $this->module ) ) ) - $this->print_ajax_response( 'error', __( 'Please change Quick Create to use a post type viewable on the calendar.', 'edit-flow' ) ); + // Check that the user has the right capabilities to add posts to the calendar (defaults to 'edit_posts') + if ( ! current_user_can( $this->create_post_cap ) ) { + $this->print_ajax_response( 'error', $this->module->messages['invalid-permissions'] ); + } - // Sanitize post values - $post_title = isset( $_POST['ef_insert_title'] ) ? sanitize_text_field( $_POST['ef_insert_title'] ) : null; + if ( empty( $_POST['ef_insert_date'] ) ) { + $this->print_ajax_response( 'error', __( 'No date supplied.', 'edit-flow' ) ); + } - if( ! $post_title ) { - $post_title = esc_html__( 'Untitled', 'edit-flow' ); - } + // Post type has to be visible on the calendar to create a placeholder + if ( ! in_array( $this->module->options->quick_create_post_type, $this->get_post_types_for_module( $this->module ) ) ) { + $this->print_ajax_response( 'error', __( 'Please change Quick Create to use a post type viewable on the calendar.', 'edit-flow' ) ); + } - $post_date = sanitize_text_field( $_POST['ef_insert_date'] ); + // Sanitize post values + $post_title = isset( $_POST['ef_insert_title'] ) ? sanitize_text_field( $_POST['ef_insert_title'] ) : null; - $post_status = $this->get_default_post_status(); + if ( ! $post_title ) { + $post_title = esc_html__( 'Untitled', 'edit-flow' ); + } - // Set new post parameters - $post_placeholder = array( - 'post_title' => $post_title, - 'post_status' => $post_status, - 'post_date' => date( 'Y-m-d H:i:s', strtotime( $post_date ) ), - 'post_type' => $this->module->options->quick_create_post_type, - ); + $post_date = sanitize_text_field( $_POST['ef_insert_date'] ); - // By default, adding a post to the calendar won't set the timestamp. - // If the user desires that to be the behavior, they can set the result of this filter to 'true' - // With how WordPress works internally, setting 'post_date_gmt' will set the timestamp - if ( apply_filters( 'ef_calendar_allow_ajax_to_set_timestamp', false ) ) - $post_placeholder['post_date_gmt'] = date( 'Y-m-d H:i:s', strtotime( $post_date ) ); + $post_status = $this->get_default_post_status(); - // Create the post - $post_id = wp_insert_post( $post_placeholder ); + // Set new post parameters + $post_placeholder = array( + 'post_title' => $post_title, + 'post_status' => $post_status, + 'post_date' => date( 'Y-m-d H:i:s', strtotime( $post_date ) ), + 'post_type' => $this->module->options->quick_create_post_type, + ); - if( $post_id ) { // success! + // By default, adding a post to the calendar won't set the timestamp. + // If the user desires that to be the behavior, they can set the result of this filter to 'true' + // With how WordPress works internally, setting 'post_date_gmt' will set the timestamp + if ( apply_filters( 'ef_calendar_allow_ajax_to_set_timestamp', false ) ) { + $post_placeholder['post_date_gmt'] = date( 'Y-m-d H:i:s', strtotime( $post_date ) ); + } - $post = get_post( $post_id ); + // Create the post + $post_id = wp_insert_post( $post_placeholder ); - // Generate the HTML for the post item so it can be injected - $post_li_html = $this->generate_post_li_html( $post, $post_date ); + if ( $post_id ) { // success! - // announce success and send back the html to inject - $this->print_ajax_response( 'success', $post_li_html ); + $post = get_post( $post_id ); - } else { - $this->print_ajax_response( 'error', __( 'Post could not be created', 'edit-flow' ) ); - } - } - - /** - * Returns the singular label for the posts that are - * quick-created on the calendar - * - * @return str Singular label for a post-type - */ - function get_quick_create_post_type_name(){ - - $post_type_slug = $this->module->options->quick_create_post_type; - $post_type_obj = get_post_type_object( $post_type_slug ); - - return $post_type_obj->labels->singular_name ? $post_type_obj->labels->singular_name : $post_type_slug; - } - - /** - * ajax_ef_calendar_update_metadata - * Update the metadata from the calendar. - * @return string representing the overlay - * - * @since 0.8 - */ - function handle_ajax_update_metadata() { - global $wpdb; - - if ( !isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'ef-calendar-modify' ) ) { - $this->print_ajax_response( 'error', $this->module->messages['nonce-failed'] ); + // Generate the HTML for the post item so it can be injected + $post_li_html = $this->generate_post_li_html( $post, $post_date ); + + // announce success and send back the html to inject + $this->print_ajax_response( 'success', $post_li_html ); + + } else { + $this->print_ajax_response( 'error', __( 'Post could not be created', 'edit-flow' ) ); + } } - if ( !isset( $_POST['post_id'] ) ) { - $this->print_ajax_response( 'error', $this->module->messages['missing-post'] ); + /** + * Returns the singular label for the posts that are + * quick-created on the calendar + * + * @return str Singular label for a post-type + */ + public function get_quick_create_post_type_name() { + + $post_type_slug = $this->module->options->quick_create_post_type; + $post_type_obj = get_post_type_object( $post_type_slug ); + + return $post_type_obj->labels->singular_name ? $post_type_obj->labels->singular_name : $post_type_slug; } - // Check that we got a proper post - $post_id = ( int )$_POST['post_id']; - $post = get_post( $post_id ); + /** + * ajax_ef_calendar_update_metadata + * Update the metadata from the calendar. + * @return string representing the overlay + * + * @since 0.8 + */ + public function handle_ajax_update_metadata() { + global $wpdb; - if ( ! $post ) - $this->print_ajax_response( 'error', $this->module->messages['missing-post'] ); + if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'ef-calendar-modify' ) ) { + $this->print_ajax_response( 'error', $this->module->messages['nonce-failed'] ); + } + if ( ! isset( $_POST['post_id'] ) ) { + $this->print_ajax_response( 'error', $this->module->messages['missing-post'] ); + } - if( $post->post_type == 'page' ) - $edit_check = 'edit_page'; - else - $edit_check = 'edit_post'; + // Check that we got a proper post + $post_id = (int) $_POST['post_id']; + $post = get_post( $post_id ); - if ( !current_user_can( $edit_check, $post->ID ) ) - $this->print_ajax_response( 'error', $this->module->messages['invalid-permissions'] ); + if ( ! $post ) { + $this->print_ajax_response( 'error', $this->module->messages['missing-post'] ); + } - // Check that the user can modify the post - if ( ! $this->current_user_can_modify_post( $post ) ) - $this->print_ajax_response( 'error', $this->module->messages['invalid-permissions'] ); - $default_types = array( - 'author', - 'taxonomy', - ); + if ( 'page' == $post->post_type ) { + $edit_check = 'edit_page'; + } else { + $edit_check = 'edit_post'; + } - $metadata_types = array(); + if ( ! current_user_can( $edit_check, $post->ID ) ) { + $this->print_ajax_response( 'error', $this->module->messages['invalid-permissions'] ); + } - if ( !$this->module_enabled( 'editorial_metadata' ) ) - $this->print_ajax_response( 'error', $this->module->messages['update-error'] ); + // Check that the user can modify the post + if ( ! $this->current_user_can_modify_post( $post ) ) { + $this->print_ajax_response( 'error', $this->module->messages['invalid-permissions'] ); + } - $metadata_types = array_keys( EditFlow()->editorial_metadata->get_supported_metadata_types() ); + $default_types = array( + 'author', + 'taxonomy', + ); - // Update an editorial metadata field - $metadata_term = isset( $_POST['metadata_term'] ) ? $_POST['metadata_term'] : ''; - $metadata_type = isset( $_POST['metadata_type'] ) ? $_POST['metadata_type'] : ''; - $incoming_metadata_value = isset( $_POST['metadata_value'] ) ? $_POST['metadata_value'] : ''; + $metadata_types = array(); - if ( isset( $_POST['metadata_type'] ) && in_array( $_POST['metadata_type'], $metadata_types ) ) { - $post_meta_key = sanitize_text_field( '_ef_editorial_meta_' . $_POST['metadata_type'] . '_' . $metadata_term ); + if ( ! $this->module_enabled( 'editorial_metadata' ) ) { + $this->print_ajax_response( 'error', $this->module->messages['update-error'] ); + } - //Javascript date parsing is terrible, so use strtotime in php - if ( $metadata_type == 'date' ) - $metadata_value = strtotime( sanitize_text_field( $incoming_metadata_value ) ); - else - $metadata_value = sanitize_text_field( $incoming_metadata_value ); + $metadata_types = array_keys( EditFlow()->editorial_metadata->get_supported_metadata_types() ); - update_post_meta( $post->ID, $post_meta_key, $metadata_value ); - $response = 'success'; - } else { - switch( $_POST['metadata_type'] ) { - case 'taxonomy': - case 'taxonomy hierarchical': - $response = wp_set_post_terms( $post->ID, $incoming_metadata_value, $metadata_term ); - break; - default: - $response = new WP_Error( 'invalid-type', __( 'Invalid metadata type', 'edit-flow' ) ); - break; + // Update an editorial metadata field + $metadata_term = isset( $_POST['metadata_term'] ) ? $_POST['metadata_term'] : ''; + $metadata_type = isset( $_POST['metadata_type'] ) ? $_POST['metadata_type'] : ''; + $incoming_metadata_value = isset( $_POST['metadata_value'] ) ? $_POST['metadata_value'] : ''; + + if ( isset( $_POST['metadata_type'] ) && in_array( $_POST['metadata_type'], $metadata_types ) ) { + $post_meta_key = sanitize_text_field( '_ef_editorial_meta_' . $_POST['metadata_type'] . '_' . $metadata_term ); + + //Javascript date parsing is terrible, so use strtotime in php + if ( 'date' == $metadata_type ) { + $metadata_value = strtotime( sanitize_text_field( $incoming_metadata_value ) ); + } else { + $metadata_value = sanitize_text_field( $incoming_metadata_value ); + } + + update_post_meta( $post->ID, $post_meta_key, $metadata_value ); + $response = 'success'; + } else { + switch ( $_POST['metadata_type'] ) { + case 'taxonomy': + case 'taxonomy hierarchical': + $response = wp_set_post_terms( $post->ID, $incoming_metadata_value, $metadata_term ); + break; + default: + $response = new WP_Error( 'invalid-type', __( 'Invalid metadata type', 'edit-flow' ) ); + break; + } + } + + //Assuming we've got to this point, just regurgitate the value + if ( ! is_wp_error( $response ) ) { + $this->print_ajax_response( 'success', $incoming_metadata_value ); + } else { + $this->print_ajax_response( 'error', __( 'Metadata could not be updated.', 'edit-flow' ) ); } } - //Assuming we've got to this point, just regurgitate the value - if ( ! is_wp_error( $response ) ) - $this->print_ajax_response( 'success', $incoming_metadata_value ); - else - $this->print_ajax_response( 'error', __( 'Metadata could not be updated.', 'edit-flow' ) ); - } - - function calendar_filters() { - $select_filter_names = array(); - - $select_filter_names['post_status'] = 'post_status'; - $select_filter_names['cat'] = 'cat'; - $select_filter_names['author'] = 'author'; - $select_filter_names['type'] = 'cpt'; - $select_filter_name['num_weeks'] = 'num_weeks'; - - return apply_filters( 'ef_calendar_filter_names', $select_filter_names ); - } - - /** - * Sanitize a $_GET or similar filter being used on the calendar - * - * @since 0.8 - * - * @param string $key Filter being sanitized - * @param string $dirty_value Value to be sanitized - * @return string $sanitized_value Safe to use value - */ - function sanitize_filter( $key, $dirty_value ) { - - switch( $key ) { - case 'post_status': - // Whitelist-based validation for this parameter - $valid_statuses = wp_list_pluck( $this->get_calendar_post_stati(), 'name' ); - $valid_statuses[] = 'unpublish'; - - if ( in_array( $dirty_value, $valid_statuses ) ) - return $dirty_value; - else - return ''; - break; - case 'cpt': - $cpt = sanitize_key( $dirty_value ); - $supported_post_types = $this->get_post_types_for_module( $this->module ); - if ( $cpt && in_array( $cpt, $supported_post_types ) ) - return $cpt; - else - return ''; - break; - case 'start_date': - return date( 'Y-m-d', strtotime( $dirty_value ) ); + public function calendar_filters() { + $select_filter_names = array(); + + $select_filter_names['post_status'] = 'post_status'; + $select_filter_names['cat'] = 'cat'; + $select_filter_names['author'] = 'author'; + $select_filter_names['type'] = 'cpt'; + $select_filter_name['num_weeks'] = 'num_weeks'; + + return apply_filters( 'ef_calendar_filter_names', $select_filter_names ); + } + + /** + * Sanitize a $_GET or similar filter being used on the calendar + * + * @since 0.8 + * + * @param string $key Filter being sanitized + * @param string $dirty_value Value to be sanitized + * @return string $sanitized_value Safe to use value + */ + public function sanitize_filter( $key, $dirty_value ) { + + switch ( $key ) { + case 'post_status': + // Whitelist-based validation for this parameter + $valid_statuses = wp_list_pluck( $this->get_calendar_post_stati(), 'name' ); + $valid_statuses[] = 'unpublish'; + + if ( in_array( $dirty_value, $valid_statuses ) ) { + return $dirty_value; + } else { + return ''; + } + break; + case 'cpt': + $cpt = sanitize_key( $dirty_value ); + $supported_post_types = $this->get_post_types_for_module( $this->module ); + if ( $cpt && in_array( $cpt, $supported_post_types ) ) { + return $cpt; + } else { + return ''; + } + break; + case 'start_date': + return date( 'Y-m-d', strtotime( $dirty_value ) ); break; - case 'cat': - case 'author': - return intval( $dirty_value ); + case 'cat': + case 'author': + return intval( $dirty_value ); break; - case 'num_weeks': - $num_weeks = intval( $dirty_value ); - if ( $num_weeks <= 0 ) { - return $this->total_weeks; - } else if ( $num_weeks > $this->max_weeks ) { - return $this->max_weeks; - } else { - return $num_weeks; - } - default: - return false; + case 'num_weeks': + $num_weeks = intval( $dirty_value ); + if ( $num_weeks <= 0 ) { + return $this->total_weeks; + } else if ( $num_weeks > $this->max_weeks ) { + return $this->max_weeks; + } else { + return $num_weeks; + } + default: + return false; break; + } + } + + /** + * When a post is updated, clean the
  • html post cache for it + */ + public function action_clean_li_html_cache( $post_id ) { + + wp_cache_delete( $post_id . 'can_modify', self::$post_li_html_cache_key ); + wp_cache_delete( $post_id . 'read_only', self::$post_li_html_cache_key ); } - } - - /** - * When a post is updated, clean the
  • html post cache for it - */ - public function action_clean_li_html_cache( $post_id ) { - - wp_cache_delete( $post_id . 'can_modify', self::$post_li_html_cache_key ); - wp_cache_delete( $post_id . 'read_only', self::$post_li_html_cache_key ); - } - - /** - * This is a hack! hack! hack! until core is fixed - * - * The calendar uses 'post_date' field to store the position on the calendar - * If a post has a core post status assigned (e.g. 'draft' or 'pending'), the `post_date` - * field will be reset when `wp_update_post()` - * is used: http://core.trac.wordpress.org/browser/tags/3.7.1/src/wp-includes/post.php#L2998 - * - * This method temporarily caches the `post_date` field if it needs to be restored. - * - * @uses fix_post_date_on_update_part_two() - */ - public function fix_post_date_on_update_part_one( $post_ID, $data ) { - - $post = get_post( $post_ID ); - - // `post_date` is only nooped for these three statuses, - // but don't try to persist if `post_date_gmt` is set - if ( ! in_array( $post->post_status, array( 'draft', 'pending', 'auto-draft' ) ) + + /** + * This is a hack! hack! hack! until core is fixed + * + * The calendar uses 'post_date' field to store the position on the calendar + * If a post has a core post status assigned (e.g. 'draft' or 'pending'), the `post_date` + * field will be reset when `wp_update_post()` + * is used: http://core.trac.wordpress.org/browser/tags/3.7.1/src/wp-includes/post.php#L2998 + * + * This method temporarily caches the `post_date` field if it needs to be restored. + * + * @uses fix_post_date_on_update_part_two() + */ + public function fix_post_date_on_update_part_one( $post_ID, $data ) { + + $post = get_post( $post_ID ); + + // `post_date` is only nooped for these three statuses, + // but don't try to persist if `post_date_gmt` is set + if ( ! in_array( $post->post_status, array( 'draft', 'pending', 'auto-draft' ) ) || '0000-00-00 00:00:00' !== $post->post_date_gmt - || '0000-00-00 00:00:00' !== $data['post_date_gmt'] ) - return; - - $this->post_date_cache[ $post_ID ] = $post->post_date; - - } - - /** - * This is a hack! hack! hack! until core is fixed - * - * The calendar uses 'post_date' field to store the position on the calendar - * If a post has a core post status assigned (e.g. 'draft' or 'pending'), the `post_date` - * field will be reset when `wp_update_post()` - * is used: http://core.trac.wordpress.org/browser/tags/3.7.1/src/wp-includes/post.php#L2998 - * - * This method restores the `post_date` field if it needs to be restored. - * - * @uses fix_post_date_on_update_part_one() - */ - public function fix_post_date_on_update_part_two( $post_ID, $post_after, $post_before ) { - global $wpdb; - - if ( empty( $this->post_date_cache[ $post_ID ] ) ) - return; - - $post_date = $this->post_date_cache[ $post_ID ]; - unset( $this->post_date_cache[ $post_ID ] ); - $wpdb->update( $wpdb->posts, array( 'post_date' => $post_date ), array( 'ID' => $post_ID ) ); - clean_post_cache( $post_ID ); - } - - /** - * Returns a list of custom status objects used by the calendar - * - * @return array An array of StdClass objects representing statuses - */ - public function get_calendar_post_stati() { - $post_stati = get_post_stati( array(), 'object' ); - $custom_status_slugs = wp_list_pluck( $this->get_post_statuses(), 'slug' ); - $custom_status_slugs[] = 'future'; - $custom_status_slugs[] = 'publish'; - - $custom_status_slug_keys = array_flip( $custom_status_slugs ); - - $final_statuses = []; - - foreach( $post_stati as $status ) { - if ( !empty( $custom_status_slug_keys[ $status->name ] ) ) { - $final_statuses[] = $status; + || '0000-00-00 00:00:00' !== $data['post_date_gmt'] ) { + return; } + + $this->post_date_cache[ $post_ID ] = $post->post_date; } - return apply_filters( 'ef_calendar_post_stati', $final_statuses ); - } + /** + * This is a hack! hack! hack! until core is fixed + * + * The calendar uses 'post_date' field to store the position on the calendar + * If a post has a core post status assigned (e.g. 'draft' or 'pending'), the `post_date` + * field will be reset when `wp_update_post()` + * is used: http://core.trac.wordpress.org/browser/tags/3.7.1/src/wp-includes/post.php#L2998 + * + * This method restores the `post_date` field if it needs to be restored. + * + * @uses fix_post_date_on_update_part_one() + */ + public function fix_post_date_on_update_part_two( $post_ID, $post_after, $post_before ) { + global $wpdb; - public function get_calendar_users() { - $users_args = array( - 'orderby' => 'display_name', - 'order' => 'ASC', - 'blog_id' => get_current_blog_id() - ); + if ( empty( $this->post_date_cache[ $post_ID ] ) ) { + return; + } - $users_args = apply_filters( 'ef_calendar_dropdown_users_args', $users_args ); + $post_date = $this->post_date_cache[ $post_ID ]; + unset( $this->post_date_cache[ $post_ID ] ); + $wpdb->update( $wpdb->posts, array( 'post_date' => $post_date ), array( 'ID' => $post_ID ) ); + clean_post_cache( $post_ID ); + } - return get_users( $users_args ); - } + /** + * Returns a list of custom status objects used by the calendar + * + * @return array An array of StdClass objects representing statuses + */ + public function get_calendar_post_stati() { + $post_stati = get_post_stati( array(), 'object' ); + $custom_status_slugs = wp_list_pluck( $this->get_post_statuses(), 'slug' ); + $custom_status_slugs[] = 'future'; + $custom_status_slugs[] = 'publish'; - public function get_calendar_categories() { - $categories_args = array( - 'orderby' => 'id', - 'order' => 'ASC', - 'hide_empty' => 0, - 'hierarchical' => 0, - 'taxonomy' => 'category' - ); + $custom_status_slug_keys = array_flip( $custom_status_slugs ); - return get_terms( $categories_args ); - } + $final_statuses = []; - public function get_calendar_frontend_config() { - global $wp_version; + foreach ( $post_stati as $status ) { + if ( ! empty( $custom_status_slug_keys[ $status->name ] ) ) { + $final_statuses[] = $status; + } + } - $all_post_types = get_post_types( null, 'objects' ); + return apply_filters( 'ef_calendar_post_stati', $final_statuses ); + } - $config = array( - 'POST_STATI' => $this->get_calendar_post_stati(), - 'USERS' => array_map( - function( $item ) { - return array( - 'id' => $item->ID, - 'display_name' => $item->display_name, - ); - }, - $this->get_calendar_users() - ), - 'CATEGORIES' => $this->get_calendar_categories(), - 'POST_TYPES' => array_map( function ( $item ) use ( $all_post_types ) { - return $all_post_types[ $item ]; - }, $this->get_post_types_for_module( $this->module ) ), - 'NUM_WEEKS' => array( - 'MAX' => $this->max_weeks, - 'DEFAULT' => $this->total_weeks, - ), - 'BEGINNING_OF_WEEK' => $this->get_beginning_of_week( date( 'Y-m-d', current_time( 'timestamp' ) ) ), - 'FILTERS' => $this->get_filters(), - 'PAGE_URL' => menu_page_url( $this->module->slug, false ), - 'WP_VERSION' => $wp_version - ); + public function get_calendar_users() { + $users_args = array( + 'orderby' => 'display_name', + 'order' => 'ASC', + 'blog_id' => get_current_blog_id(), + ); - return apply_filters( 'ef_calendar_frontend_config', $config ); - } + $users_args = apply_filters( 'ef_calendar_dropdown_users_args', $users_args ); -} // EF_Calendar + return get_users( $users_args ); + } + + public function get_calendar_categories() { + $categories_args = array( + 'orderby' => 'id', + 'order' => 'ASC', + 'hide_empty' => 0, + 'hierarchical' => 0, + 'taxonomy' => 'category', + ); + + return get_terms( $categories_args ); + } + + public function get_calendar_frontend_config() { + global $wp_version; + + $all_post_types = get_post_types( null, 'objects' ); + + $config = array( + 'POST_STATI' => $this->get_calendar_post_stati(), + 'USERS' => array_map( + function ( $item ) { + return array( + 'id' => $item->ID, + 'display_name' => $item->display_name, + ); + }, + $this->get_calendar_users() + ), + 'CATEGORIES' => $this->get_calendar_categories(), + 'POST_TYPES' => array_map( function ( $item ) use ( $all_post_types ) { + return $all_post_types[ $item ]; + }, $this->get_post_types_for_module( $this->module ) ), + 'NUM_WEEKS' => array( + 'MAX' => $this->max_weeks, + 'DEFAULT' => $this->total_weeks, + ), + 'BEGINNING_OF_WEEK' => $this->get_beginning_of_week( date( 'Y-m-d', current_time( 'timestamp' ) ) ), + 'FILTERS' => $this->get_filters(), + 'PAGE_URL' => menu_page_url( $this->module->slug, false ), + 'WP_VERSION' => $wp_version, + ); + + return apply_filters( 'ef_calendar_frontend_config', $config ); + } + } // EF_Calendar } // class_exists('EF_Calendar') diff --git a/modules/custom-status/custom-status.php b/modules/custom-status/custom-status.php index df701d7a4..f8ec5739f 100644 --- a/modules/custom-status/custom-status.php +++ b/modules/custom-status/custom-status.php @@ -9,332 +9,337 @@ * - Ensure all of the form processing uses our messages functionality */ +if ( ! class_exists( 'EF_Custom_Status' ) ) { - if ( !class_exists( 'EF_Custom_Status' ) ) { + class EF_Custom_Status extends EF_Module { -class EF_Custom_Status extends EF_Module { + public $module; - var $module; + private $custom_statuses_cache = array(); - private $custom_statuses_cache = array(); + // This is taxonomy name used to store all our custom statuses + // phpcs:ignore Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase + const taxonomy_key = 'post_status'; - // This is taxonomy name used to store all our custom statuses - const taxonomy_key = 'post_status'; + /** + * Register the module with Edit Flow but don't do anything else + */ + public function __construct() { - /** - * Register the module with Edit Flow but don't do anything else - */ - function __construct() { - - $this->module_url = $this->get_module_url( __FILE__ ); - // Register the module with Edit Flow - $args = array( - 'title' => __( 'Custom Statuses', 'edit-flow' ), - 'short_description' => __( 'Create custom post statuses to define the stages of your workflow.', 'edit-flow' ), - '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' ), - 'module_url' => $this->module_url, - 'img_url' => $this->module_url . 'lib/custom_status_s128.png', - 'slug' => 'custom-status', - 'default_options' => array( - 'enabled' => 'on', - 'default_status' => 'pitch', - 'always_show_dropdown' => 'off', - 'post_types' => array( - 'post' => 'on', - 'page' => 'on', + $this->module_url = $this->get_module_url( __FILE__ ); + // Register the module with Edit Flow + $args = array( + 'title' => __( 'Custom Statuses', 'edit-flow' ), + 'short_description' => __( 'Create custom post statuses to define the stages of your workflow.', 'edit-flow' ), + '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' ), + 'module_url' => $this->module_url, + 'img_url' => $this->module_url . 'lib/custom_status_s128.png', + 'slug' => 'custom-status', + 'default_options' => array( + 'enabled' => 'on', + 'default_status' => 'pitch', + 'always_show_dropdown' => 'off', + 'post_types' => array( + 'post' => 'on', + 'page' => 'on', + ), ), - ), - 'post_type_support' => 'ef_custom_statuses', // This has been plural in all of our docs - 'configure_page_cb' => 'print_configure_view', - 'configure_link_text' => __( 'Edit Statuses', 'edit-flow' ), - 'messages' => array( - 'status-added' => __( 'Post status created.', 'edit-flow' ), - 'status-missing' => __( "Post status doesn't exist.", 'edit-flow' ), - 'default-status-changed' => __( 'Default post status has been changed.', 'edit-flow'), - 'term-updated' => __( "Post status updated.", 'edit-flow' ), - 'status-deleted' => __( 'Post status deleted.', 'edit-flow' ), - 'status-position-updated' => __( "Status order updated.", 'edit-flow' ), - ), - 'autoload' => false, - 'settings_help_tab' => array( - 'id' => 'ef-custom-status-overview', - 'title' => __('Overview', 'edit-flow'), - 'content' => __('

    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.

    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.

    ', 'edit-flow'), + 'post_type_support' => 'ef_custom_statuses', // This has been plural in all of our docs + 'configure_page_cb' => 'print_configure_view', + 'configure_link_text' => __( 'Edit Statuses', 'edit-flow' ), + 'messages' => array( + 'status-added' => __( 'Post status created.', 'edit-flow' ), + 'status-missing' => __( "Post status doesn't exist.", 'edit-flow' ), + 'default-status-changed' => __( 'Default post status has been changed.', 'edit-flow' ), + 'term-updated' => __( 'Post status updated.', 'edit-flow' ), + 'status-deleted' => __( 'Post status deleted.', 'edit-flow' ), + 'status-position-updated' => __( 'Status order updated.', 'edit-flow' ), ), - 'settings_help_sidebar' => __( '

    For more information:

    Custom Status Documentation

    Edit Flow Forum

    Edit Flow on Github

    ', 'edit-flow' ), - ); - $this->module = EditFlow()->register_module( 'custom_status', $args ); - } + 'autoload' => false, + 'settings_help_tab' => array( + 'id' => 'ef-custom-status-overview', + 'title' => __( 'Overview', 'edit-flow' ), + 'content' => __( '

    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.

    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.

    ', 'edit-flow' ), + ), + 'settings_help_sidebar' => __( '

    For more information:

    Custom Status Documentation

    Edit Flow Forum

    Edit Flow on Github

    ', 'edit-flow' ), + ); + $this->module = EditFlow()->register_module( 'custom_status', $args ); + } - /** - * Initialize the EF_Custom_Status class if the module is active - */ - function init() { - global $edit_flow; + /** + * Initialize the EF_Custom_Status class if the module is active + */ + public function init() { + global $edit_flow; - // Register custom statuses as a taxonomy - $this->register_custom_statuses(); + // Register custom statuses as a taxonomy + $this->register_custom_statuses(); - // Register our settings - add_action( 'admin_init', array( $this, 'register_settings' ) ); + // Register our settings + add_action( 'admin_init', array( $this, 'register_settings' ) ); - if ( ! $this->disable_custom_statuses_for_post_type() ) { - // Load CSS and JS resources that we probably need in the admin page - add_action( 'admin_enqueue_scripts', array( $this, 'action_admin_enqueue_scripts' ) ); + if ( ! $this->disable_custom_statuses_for_post_type() ) { + // Load CSS and JS resources that we probably need in the admin page + add_action( 'admin_enqueue_scripts', array( $this, 'action_admin_enqueue_scripts' ) ); - // Assets for block editor UI. - add_action( 'enqueue_block_editor_assets', array( $this, 'load_scripts_for_block_editor') ); + // Assets for block editor UI. + add_action( 'enqueue_block_editor_assets', array( $this, 'load_scripts_for_block_editor' ) ); - // Assets for iframed block editor and editor UI. - add_action( 'enqueue_block_editor_assets', array( $this, 'load_styles_for_block_editor') ); - } + // Assets for iframed block editor and editor UI. + add_action( 'enqueue_block_editor_assets', array( $this, 'load_styles_for_block_editor' ) ); + } - add_action( 'admin_notices', array( $this, 'no_js_notice' ) ); - add_action( 'admin_print_scripts', array( $this, 'post_admin_header' ) ); - - // Add custom statuses to the post states. - add_filter( 'display_post_states', array( $this, 'add_status_to_post_states' ), 10, 2 ); - - // Methods for handling the actions of creating, making default, and deleting post stati - add_action( 'admin_init', array( $this, 'handle_add_custom_status' ) ); - add_action( 'admin_init', array( $this, 'handle_edit_custom_status' ) ); - add_action( 'admin_init', array( $this, 'handle_make_default_custom_status' ) ); - add_action( 'admin_init', array( $this, 'handle_delete_custom_status' ) ); - add_action( 'wp_ajax_update_status_positions', array( $this, 'handle_ajax_update_status_positions' ) ); - add_action( 'wp_ajax_inline_save_status', array( $this, 'ajax_inline_save_status' ) ); - - // These seven-ish methods are hacks for fixing bugs in WordPress core - add_action( 'admin_init', array( $this, 'check_timestamp_on_publish' ) ); - add_filter( 'wp_insert_post_data', array( $this, 'fix_custom_status_timestamp' ), 10, 2 ); - add_filter( 'wp_insert_post_data', array( $this, 'maybe_keep_post_name_empty' ), 10, 2 ); - add_filter( 'pre_wp_unique_post_slug', array( $this, 'fix_unique_post_slug' ), 10, 6 ); - add_filter( 'preview_post_link', array( $this, 'fix_preview_link_part_one' ) ); - add_filter( 'post_link', array( $this, 'fix_preview_link_part_two' ), 10, 3 ); - add_filter( 'page_link', array( $this, 'fix_preview_link_part_two' ), 10, 3 ); - add_filter( 'post_type_link', array( $this, 'fix_preview_link_part_two' ), 10, 3 ); - add_filter( 'preview_post_link', array( $this, 'fix_preview_link_part_three' ), 11, 2 ); - add_filter( 'get_sample_permalink', array( $this, 'fix_get_sample_permalink' ), 10, 5 ); - add_filter( 'get_sample_permalink_html', array( $this, 'fix_get_sample_permalink_html' ), 10, 5); - add_filter( 'post_row_actions', array( $this, 'fix_post_row_actions' ), 10, 2 ); - add_filter( 'page_row_actions', array( $this, 'fix_post_row_actions' ), 10, 2 ); - - // Pagination for custom post statuses when previewing posts - add_filter( 'wp_link_pages_link', array( $this, 'modify_preview_link_pagination_url' ), 10, 2 ); - } + add_action( 'admin_notices', array( $this, 'no_js_notice' ) ); + add_action( 'admin_print_scripts', array( $this, 'post_admin_header' ) ); + + // Add custom statuses to the post states. + add_filter( 'display_post_states', array( $this, 'add_status_to_post_states' ), 10, 2 ); + + // Methods for handling the actions of creating, making default, and deleting post stati + add_action( 'admin_init', array( $this, 'handle_add_custom_status' ) ); + add_action( 'admin_init', array( $this, 'handle_edit_custom_status' ) ); + add_action( 'admin_init', array( $this, 'handle_make_default_custom_status' ) ); + add_action( 'admin_init', array( $this, 'handle_delete_custom_status' ) ); + add_action( 'wp_ajax_update_status_positions', array( $this, 'handle_ajax_update_status_positions' ) ); + add_action( 'wp_ajax_inline_save_status', array( $this, 'ajax_inline_save_status' ) ); + + // These seven-ish methods are hacks for fixing bugs in WordPress core + add_action( 'admin_init', array( $this, 'check_timestamp_on_publish' ) ); + add_filter( 'wp_insert_post_data', array( $this, 'fix_custom_status_timestamp' ), 10, 2 ); + add_filter( 'wp_insert_post_data', array( $this, 'maybe_keep_post_name_empty' ), 10, 2 ); + add_filter( 'pre_wp_unique_post_slug', array( $this, 'fix_unique_post_slug' ), 10, 6 ); + add_filter( 'preview_post_link', array( $this, 'fix_preview_link_part_one' ) ); + add_filter( 'post_link', array( $this, 'fix_preview_link_part_two' ), 10, 3 ); + add_filter( 'page_link', array( $this, 'fix_preview_link_part_two' ), 10, 3 ); + add_filter( 'post_type_link', array( $this, 'fix_preview_link_part_two' ), 10, 3 ); + add_filter( 'preview_post_link', array( $this, 'fix_preview_link_part_three' ), 11, 2 ); + add_filter( 'get_sample_permalink', array( $this, 'fix_get_sample_permalink' ), 10, 5 ); + add_filter( 'get_sample_permalink_html', array( $this, 'fix_get_sample_permalink_html' ), 10, 5 ); + add_filter( 'post_row_actions', array( $this, 'fix_post_row_actions' ), 10, 2 ); + add_filter( 'page_row_actions', array( $this, 'fix_post_row_actions' ), 10, 2 ); + + // Pagination for custom post statuses when previewing posts + add_filter( 'wp_link_pages_link', array( $this, 'modify_preview_link_pagination_url' ), 10, 2 ); + } - /** - * Create the default set of custom statuses the first time the module is loaded - * - * @since 0.7 - */ - function install() { - - $default_terms = array( - array( - 'term' => __( 'Pitch', 'edit-flow' ), - 'args' => array( - 'slug' => 'pitch', - 'description' => __( 'Idea proposed; waiting for acceptance.', 'edit-flow' ), - 'position' => 1, + /** + * Create the default set of custom statuses the first time the module is loaded + * + * @since 0.7 + */ + public function install() { + + $default_terms = array( + array( + 'term' => __( 'Pitch', 'edit-flow' ), + 'args' => array( + 'slug' => 'pitch', + 'description' => __( 'Idea proposed; waiting for acceptance.', 'edit-flow' ), + 'position' => 1, + ), ), - ), - array( - 'term' => __( 'Assigned', 'edit-flow' ), - 'args' => array( - 'slug' => 'assigned', - 'description' => __( 'Post idea assigned to writer.', 'edit-flow' ), - 'position' => 2, + array( + 'term' => __( 'Assigned', 'edit-flow' ), + 'args' => array( + 'slug' => 'assigned', + 'description' => __( 'Post idea assigned to writer.', 'edit-flow' ), + 'position' => 2, + ), ), - ), - array( - 'term' => __( 'In Progress', 'edit-flow' ), - 'args' => array( - 'slug' => 'in-progress', - 'description' => __( 'Writer is working on the post.', 'edit-flow' ), - 'position' => 3, + array( + 'term' => __( 'In Progress', 'edit-flow' ), + 'args' => array( + 'slug' => 'in-progress', + 'description' => __( 'Writer is working on the post.', 'edit-flow' ), + 'position' => 3, + ), ), - ), - array( - 'term' => __( 'Draft', 'edit-flow' ), - 'args' => array( - 'slug' => 'draft', - 'description' => __( 'Post is a draft; not ready for review or publication.', 'edit-flow' ), - 'position' => 4, + array( + 'term' => __( 'Draft', 'edit-flow' ), + 'args' => array( + 'slug' => 'draft', + 'description' => __( 'Post is a draft; not ready for review or publication.', 'edit-flow' ), + 'position' => 4, + ), ), - ), - array( - 'term' => __( 'Pending Review' ), - 'args' => array( - 'slug' => 'pending', - 'description' => __( 'Post needs to be reviewed by an editor.', 'edit-flow' ), - 'position' => 5, + array( + 'term' => __( 'Pending Review' ), + 'args' => array( + 'slug' => 'pending', + 'description' => __( 'Post needs to be reviewed by an editor.', 'edit-flow' ), + 'position' => 5, + ), ), - ), - ); + ); - // Okay, now add the default statuses to the db if they don't already exist - foreach( $default_terms as $term ) { - if( !term_exists( $term['term'], self::taxonomy_key ) ) - $this->add_custom_status( $term['term'], $term['args'] ); + // Okay, now add the default statuses to the db if they don't already exist + foreach ( $default_terms as $term ) { + if ( ! term_exists( $term['term'], self::taxonomy_key ) ) { + $this->add_custom_status( $term['term'], $term['args'] ); + } + } } - } - - /** - * Upgrade our data in case we need to - * - * @since 0.7 - */ - function upgrade( $previous_version ) { - global $edit_flow; + /** + * Upgrade our data in case we need to + * + * @since 0.7 + */ + public function upgrade( $previous_version ) { + global $edit_flow; + + // Upgrade path to v0.7 + if ( version_compare( $previous_version, '0.7', '<' ) ) { + // Migrate dropdown visibility option + $dropdown_visible = get_option( 'edit_flow_status_dropdown_visible' ); + if ( $dropdown_visible ) { + $dropdown_visible = 'on'; + } else { + $dropdown_visible = 'off'; + } + $edit_flow->update_module_option( $this->module->name, 'always_show_dropdown', $dropdown_visible ); + delete_option( 'edit_flow_status_dropdown_visible' ); + // Migrate default status option + $default_status = get_option( 'edit_flow_custom_status_default_status' ); + if ( $default_status ) { + $edit_flow->update_module_option( $this->module->name, 'default_status', $default_status ); + } + delete_option( 'edit_flow_custom_status_default_status' ); - // Upgrade path to v0.7 - if ( version_compare( $previous_version, '0.7' , '<' ) ) { - // Migrate dropdown visibility option - if ( $dropdown_visible = get_option( 'edit_flow_status_dropdown_visible' ) ) - $dropdown_visible = 'on'; - else - $dropdown_visible = 'off'; - $edit_flow->update_module_option( $this->module->name, 'always_show_dropdown', $dropdown_visible ); - delete_option( 'edit_flow_status_dropdown_visible' ); - // Migrate default status option - if ( $default_status = get_option( 'edit_flow_custom_status_default_status' ) ) - $edit_flow->update_module_option( $this->module->name, 'default_status', $default_status ); - delete_option( 'edit_flow_custom_status_default_status' ); - - // Technically we've run this code before so we don't want to auto-install new data - $edit_flow->update_module_option( $this->module->name, 'loaded_once', true ); - } - // Upgrade path to v0.7.4 - if ( version_compare( $previous_version, '0.7.4', '<' ) ) { - // Custom status descriptions become base64_encoded, instead of maybe json_encoded. - $this->upgrade_074_term_descriptions( self::taxonomy_key ); + // Technically we've run this code before so we don't want to auto-install new data + $edit_flow->update_module_option( $this->module->name, 'loaded_once', true ); + } + // Upgrade path to v0.7.4 + if ( version_compare( $previous_version, '0.7.4', '<' ) ) { + // Custom status descriptions become base64_encoded, instead of maybe json_encoded. + $this->upgrade_074_term_descriptions( self::taxonomy_key ); + } } - } + /** + * Makes the call to register_post_status to register the user's custom statuses. + * Also unregisters draft and pending, in case the user doesn't want them. + */ + public function register_custom_statuses() { + global $wp_post_statuses; - /** - * Makes the call to register_post_status to register the user's custom statuses. - * Also unregisters draft and pending, in case the user doesn't want them. - */ - function register_custom_statuses() { - global $wp_post_statuses; - - if ( $this->disable_custom_statuses_for_post_type() ) - return; - - // Register new taxonomy so that we can store all our fancy new custom statuses (or is it stati?) - if ( !taxonomy_exists( self::taxonomy_key ) ) { - $args = array( 'hierarchical' => false, - 'update_count_callback' => '_update_post_term_count', - 'label' => false, - 'query_var' => false, - 'rewrite' => false, - 'show_ui' => false - ); - register_taxonomy( self::taxonomy_key, 'post', $args ); - } + if ( $this->disable_custom_statuses_for_post_type() ) { + return; + } + + // Register new taxonomy so that we can store all our fancy new custom statuses (or is it stati?) + if ( ! taxonomy_exists( self::taxonomy_key ) ) { + $args = array( + 'hierarchical' => false, + 'update_count_callback' => '_update_post_term_count', + 'label' => false, + 'query_var' => false, + 'rewrite' => false, + 'show_ui' => false, + ); + register_taxonomy( self::taxonomy_key, 'post', $args ); + } - if ( function_exists( 'register_post_status' ) ) { - // Users can delete draft and pending statuses if they want, so let's get rid of them - // They'll get re-added if the user hasn't "deleted" them - unset( $wp_post_statuses[ 'draft' ] ); - unset( $wp_post_statuses[ 'pending' ] ); + if ( function_exists( 'register_post_status' ) ) { + // Users can delete draft and pending statuses if they want, so let's get rid of them + // They'll get re-added if the user hasn't "deleted" them + unset( $wp_post_statuses['draft'] ); + unset( $wp_post_statuses['pending'] ); - $custom_statuses = $this->get_custom_statuses(); + $custom_statuses = $this->get_custom_statuses(); - // Unfortunately, register_post_status() doesn't accept a - // post type argument, so we have to register the post - // statuses for all post types. This results in - // all post statuses for a post type appearing at the top - // of manage posts if there is a post with the status - foreach ( $custom_statuses as $status ) { - register_post_status( $status->slug, array( - 'label' => $status->name - , 'protected' => true - , '_builtin' => false - , 'label_count' => _n_noop( "{$status->name} (%s)", "{$status->name} (%s)" ) - ) ); + // Unfortunately, register_post_status() doesn't accept a + // post type argument, so we have to register the post + // statuses for all post types. This results in + // all post statuses for a post type appearing at the top + // of manage posts if there is a post with the status + foreach ( $custom_statuses as $status ) { + register_post_status( $status->slug, array( + 'label' => $status->name, + 'protected' => true, + '_builtin' => false, + 'label_count' => _n_noop( "{$status->name} (%s)", "{$status->name} (%s)" ), + ) ); + } } } - } - /** - * Whether custom post statuses should be disabled for this post type. - * Used to stop custom statuses from being registered for post types that don't support them. - * - * @since 0.7.5 - * - * @return bool - */ - function disable_custom_statuses_for_post_type( $post_type = null ) { - global $pagenow; - - // Only allow deregistering on 'edit.php' and 'post.php' - if ( ! in_array( $pagenow, array( 'edit.php', 'post.php', 'post-new.php' ) ) ) - return false; + /** + * Whether custom post statuses should be disabled for this post type. + * Used to stop custom statuses from being registered for post types that don't support them. + * + * @since 0.7.5 + * + * @return bool + */ + public function disable_custom_statuses_for_post_type( $post_type = null ) { + global $pagenow; - if ( is_null( $post_type ) ) { - $post_type = $this->get_current_post_type(); - } + // Only allow deregistering on 'edit.php' and 'post.php' + if ( ! in_array( $pagenow, array( 'edit.php', 'post.php', 'post-new.php' ) ) ) { + return false; + } - if ( $post_type && ! in_array( $post_type, $this->get_post_types_for_module( $this->module ) ) ) { - return true; - } + if ( is_null( $post_type ) ) { + $post_type = $this->get_current_post_type(); + } - return false; - } + if ( $post_type && ! in_array( $post_type, $this->get_post_types_for_module( $this->module ) ) ) { + return true; + } - /** - * Enqueue Javascript resources that we need in the admin: - * - Primary use of Javascript is to manipulate the post status dropdown on Edit Post and Manage Posts - * - jQuery Sortable plugin is used for drag and dropping custom statuses - * - We have other custom code for Quick Edit and JS niceties - */ - function action_admin_enqueue_scripts() { - // Load Javascript we need to use on the configuration views (jQuery Sortable and Quick Edit) - if ( $this->is_whitelisted_settings_view( $this->module->name ) ) { - wp_enqueue_script( 'jquery-ui-sortable' ); - wp_enqueue_script( 'edit-flow-custom-status-configure', $this->module_url . 'lib/custom-status-configure.js', array( 'jquery', 'jquery-ui-sortable', 'edit-flow-settings-js' ), EDIT_FLOW_VERSION, true ); + return false; } - // Custom javascript to modify the post status dropdown where it shows up - if ( $this->is_whitelisted_page() ) { - wp_enqueue_script( 'edit_flow-custom_status', $this->module_url . 'lib/custom-status.js', array( 'jquery','post' ), EDIT_FLOW_VERSION, true ); - wp_localize_script('edit_flow-custom_status', '__ef_localize_custom_status', array( - 'no_change' => esc_html__( "— No Change —", 'edit-flow' ), - 'published' => esc_html__( 'Published', 'edit-flow' ), - 'save_as' => esc_html__( 'Save as', 'edit-flow' ), - 'save' => esc_html__( 'Save', 'edit-flow' ), - 'edit' => esc_html__( 'Edit', 'edit-flow' ), - 'ok' => esc_html__( 'OK', 'edit-flow' ), - 'cancel' => esc_html__( 'Cancel', 'edit-flow' ), - )); - } + /** + * Enqueue Javascript resources that we need in the admin: + * - Primary use of Javascript is to manipulate the post status dropdown on Edit Post and Manage Posts + * - jQuery Sortable plugin is used for drag and dropping custom statuses + * - We have other custom code for Quick Edit and JS niceties + */ + public function action_admin_enqueue_scripts() { + // Load Javascript we need to use on the configuration views (jQuery Sortable and Quick Edit) + if ( $this->is_whitelisted_settings_view( $this->module->name ) ) { + wp_enqueue_script( 'jquery-ui-sortable' ); + wp_enqueue_script( 'edit-flow-custom-status-configure', $this->module_url . 'lib/custom-status-configure.js', array( 'jquery', 'jquery-ui-sortable', 'edit-flow-settings-js' ), EDIT_FLOW_VERSION, true ); + } - } + // Custom javascript to modify the post status dropdown where it shows up + if ( $this->is_whitelisted_page() ) { + wp_enqueue_script( 'edit_flow-custom_status', $this->module_url . 'lib/custom-status.js', array( 'jquery', 'post' ), EDIT_FLOW_VERSION, true ); + wp_localize_script('edit_flow-custom_status', '__ef_localize_custom_status', array( + 'no_change' => esc_html__( '— No Change —', 'edit-flow' ), + 'published' => esc_html__( 'Published', 'edit-flow' ), + 'save_as' => esc_html__( 'Save as', 'edit-flow' ), + 'save' => esc_html__( 'Save', 'edit-flow' ), + 'edit' => esc_html__( 'Edit', 'edit-flow' ), + 'ok' => esc_html__( 'OK', 'edit-flow' ), + 'cancel' => esc_html__( 'Cancel', 'edit-flow' ), + )); + } + } - function load_scripts_for_block_editor(){ - global $post; + public function load_scripts_for_block_editor() { + global $post; - wp_enqueue_script( 'edit-flow-block-custom-status-script', EDIT_FLOW_URL . 'dist/custom-status.build.js', array( 'wp-blocks', 'wp-element', 'wp-edit-post', 'wp-plugins', 'wp-components' ), EDIT_FLOW_VERSION ); + wp_enqueue_script( 'edit-flow-block-custom-status-script', EDIT_FLOW_URL . 'dist/custom-status.build.js', array( 'wp-blocks', 'wp-element', 'wp-edit-post', 'wp-plugins', 'wp-components' ), EDIT_FLOW_VERSION ); - $custom_statuses = apply_filters( 'ef_custom_status_list', $this->get_custom_statuses(), $post ); + $custom_statuses = apply_filters( 'ef_custom_status_list', $this->get_custom_statuses(), $post ); - wp_localize_script( 'edit-flow-block-custom-status-script', 'EditFlowCustomStatuses', array_values( $custom_statuses ) ); - } + wp_localize_script( 'edit-flow-block-custom-status-script', 'EditFlowCustomStatuses', array_values( $custom_statuses ) ); + } - function load_styles_for_block_editor(){ - wp_enqueue_style( 'edit-flow-block-custom-status-styles', EDIT_FLOW_URL . 'dist/custom-status.editor.build.css', false, EDIT_FLOW_VERSION ); - } + public function load_styles_for_block_editor() { + wp_enqueue_style( 'edit-flow-block-custom-status-styles', EDIT_FLOW_URL . 'dist/custom-status.editor.build.css', false, EDIT_FLOW_VERSION ); + } - /** - * Displays a notice to users if they have JS disabled - * Javascript is needed for custom statuses to be fully functional - */ - function no_js_notice() { - if( $this->is_whitelisted_page() ) : - ?> + /** + * Displays a notice to users if they have JS disabled + * Javascript is needed for custom statuses to be fully functional + */ + public function no_js_notice() { + if ( $this->is_whitelisted_page() ) : + ?> '; } - echo ''; } - - } - - // Load Javascript specific to the editorial metadata configuration view - if ( $this->is_whitelisted_settings_view( $this->module->name ) ) { - wp_enqueue_script( 'jquery-ui-sortable' ); - 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 ); + + // Load Javascript specific to the editorial metadata configuration view + if ( $this->is_whitelisted_settings_view( $this->module->name ) ) { + wp_enqueue_script( 'jquery-ui-sortable' ); + 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 ); + } } - } - - /** - * Register the post metadata taxonomy - */ - function register_taxonomy() { - - // We need to make sure taxonomy is registered for all of the post types that support it - $supported_post_types = $this->get_post_types_for_module( $this->module ); - - register_taxonomy( self::metadata_taxonomy, $supported_post_types, - array( - 'public' => false, - 'labels' => array( - 'name' => _x( 'Editorial Metadata', 'taxonomy general name', 'edit-flow' ), - 'singular_name' => _x( 'Editorial Metadata', 'taxonomy singular name', 'edit-flow' ), + + /** + * Register the post metadata taxonomy + */ + public function register_taxonomy() { + + // We need to make sure taxonomy is registered for all of the post types that support it + $supported_post_types = $this->get_post_types_for_module( $this->module ); + + register_taxonomy( self::metadata_taxonomy, $supported_post_types, + array( + 'public' => false, + 'labels' => array( + 'name' => _x( 'Editorial Metadata', 'taxonomy general name', 'edit-flow' ), + 'singular_name' => _x( 'Editorial Metadata', 'taxonomy singular name', 'edit-flow' ), 'search_items' => __( 'Search Editorial Metadata', 'edit-flow' ), 'popular_items' => __( 'Popular Editorial Metadata', 'edit-flow' ), 'all_items' => __( 'All Editorial Metadata', 'edit-flow' ), @@ -327,1114 +329,1201 @@ function register_taxonomy() { 'add_new_item' => __( 'Add New Editorial Metadata', 'edit-flow' ), 'new_item_name' => __( 'New Editorial Metadata', 'edit-flow' ), ), - 'rewrite' => false, - ) - ); - } - - /***************************************************** - * Post meta box generation and processing - ****************************************************/ - - /** - * Load the post metaboxes for all of the post types that are supported - */ - function handle_post_metaboxes() { - $title = __( 'Editorial Metadata', 'edit-flow' ); + 'rewrite' => false, + ) + ); + } + + /***************************************************** + * Post meta box generation and processing + ****************************************************/ - $supported_post_types = $this->get_post_types_for_module( $this->module ); - foreach ( $supported_post_types as $post_type ) { - add_meta_box( self::metadata_taxonomy, $title, array( $this, 'display_meta_box' ), $post_type, 'side' ); + /** + * Load the post metaboxes for all of the post types that are supported + */ + public function handle_post_metaboxes() { + $title = __( 'Editorial Metadata', 'edit-flow' ); + + $supported_post_types = $this->get_post_types_for_module( $this->module ); + foreach ( $supported_post_types as $post_type ) { + add_meta_box( self::metadata_taxonomy, $title, array( $this, 'display_meta_box' ), $post_type, 'side' ); + } } - } - - /** - * Displays HTML output for Editorial Metadata post meta box - * - * @param object $post Current post - */ - function display_meta_box( $post ) { - echo "
    "; - // Add nonce for verification upon save - echo ""; - - if ( current_user_can( 'manage_options' ) ) { - // Make the metabox title include a link to edit the Editorial Metadata terms. Logic similar to how Core dashboard widgets work. - $url = add_query_arg( 'page', 'ef-editorial-metadata-settings', get_admin_url( null, 'admin.php' ) ); - echo '

    ' . __( 'Configure', 'edit-flow' ) . '

    '; + + /** + * Displays HTML output for Editorial Metadata post meta box + * + * @param object $post Current post + */ + public function display_meta_box( $post ) { + echo "
    "; + // Add nonce for verification upon save + echo ""; + + if ( current_user_can( 'manage_options' ) ) { + // Make the metabox title include a link to edit the Editorial Metadata terms. Logic similar to how Core dashboard widgets work. + $url = add_query_arg( 'page', 'ef-editorial-metadata-settings', get_admin_url( null, 'admin.php' ) ); + echo '

    ' . esc_html__( 'Configure', 'edit-flow' ) . '

    '; + } + + $terms = $this->get_editorial_metadata_terms(); + if ( ! count( $terms ) ) { + $message = __( 'No editorial metadata available.' ); + if ( current_user_can( 'manage_options' ) ) { + /* translators: 1: The link to add editorial metadata fields */ + $message .= sprintf( __( ' Add fields to get started.' ), $this->get_link() ); + } else { + $message .= esc_html__( ' Encourage your site administrator to configure your editorial workflow by adding editorial metadata.' ); + } + echo '

    ' . wp_kses( $message, 'a' ) . '

    '; + } else { + foreach ( $terms as $term ) { + $postmeta_key = $this->get_postmeta_key( $term ); + $current_metadata = esc_attr( $this->get_postmeta_value( $term, $post->ID ) ); + $type = $term->type; + $description = $term->description; + if ( $description ) { + $description_span = "$description"; + } else { + $description_span = ''; + } + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- This is for the escaping of type. + echo "'; + echo "
    "; + } // Done iterating through metadata terms + } + echo '
    '; } - - $terms = $this->get_editorial_metadata_terms(); - if ( !count( $terms ) ) { - $message = __( 'No editorial metadata available.' ); - if ( current_user_can( 'manage_options' ) ) - $message .= sprintf( __( ' Add fields to get started.' ), $this->get_link() ); - else - $message .= __( ' Encourage your site administrator to configure your editorial workflow by adding editorial metadata.' ); - echo '

    ' . $message . '

    '; - } else { + + /** + * Show date or datetime + * @param int $current_date + * @return string + * @since 0.8 + */ + private function show_date_or_datetime( $current_date ) { + + if ( date( 'Hi', $current_date ) == '0000' ) { + return date_i18n( 'M d Y', $current_date ); + } else { + return date_i18n( 'M d Y H:i', $current_date ); + } + } + + /** + * Save any values in the editorial metadata post meta box + * + * @param int $id Unique ID for the post being saved + * @param object $post Post object + */ + public function save_meta_box( $id, $post ) { + + // Authentication checks: make sure data came from our meta box and that the current user is allowed to edit the post + // TODO: switch to using check_admin_referrer? See core (e.g. edit.php) for usage + if ( ! isset( $_POST[ self::metadata_taxonomy . '_nonce' ] ) + || ! wp_verify_nonce( $_POST[ self::metadata_taxonomy . '_nonce' ], 'ef-save-metabox' ) ) { + return $id; + } + + if ( ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) + || ! in_array( $post->post_type, $this->get_post_types_for_module( $this->module ) ) + || ( 'post' == $post->post_type && ! current_user_can( 'edit_post', $id ) ) + || ( 'page' == $post->post_type && ! current_user_can( 'edit_page', $id ) ) ) { + return $id; + } + + // Authentication passed, let's save the data + $terms = $this->get_editorial_metadata_terms(); + $term_slugs = array(); + foreach ( $terms as $term ) { - $postmeta_key = $this->get_postmeta_key( $term ); - $current_metadata = esc_attr( $this->get_postmeta_value( $term, $post->ID ) ); + // Setup the key for this editorial metadata term (same as what's in $_POST) + $key = $this->get_postmeta_key( $term ); + + // Get the current editorial metadata + // TODO: do we care about the current_metadata at all? + //$current_metadata = get_post_meta( $id, $key, true ); + + $new_metadata = isset( $_POST[ $key ] ) ? $_POST[ $key ] : ''; + $type = $term->type; - $description = $term->description; - if ( $description ) - $description_span = "$description"; - else - $description_span = ''; - echo ""; - echo "
    "; - } // Done iterating through metadata terms - } - echo "
    "; - } - - /** - * Show date or datetime - * @param int $current_date - * @return string - * @since 0.8 - */ - private function show_date_or_datetime( $current_date ) { + do_action( 'ef_editorial_metadata_field_updated', $key, $new_metadata, $id, $type ); + } - if( date( 'Hi', $current_date ) == '0000') - return date_i18n( 'M d Y', $current_date ); - else - return date_i18n( 'M d Y H:i', $current_date ); - } + // Relate the post to the terms used and taxonomy type (wp_term_relationships table). + // This will allow us to update and display the count of metadata in posts in use per term. + // TODO: Core only correlates posts with terms if the post_status is publish. Do we care what it is? + if ( 'publish' === $post->post_status ) { + wp_set_object_terms( $id, $term_slugs, self::metadata_taxonomy ); + } + } - /** - * Save any values in the editorial metadata post meta box - * - * @param int $id Unique ID for the post being saved - * @param object $post Post object - */ - function save_meta_box( $id, $post ) { + /** + * Generate a unique key based on the term + * + * @param object $term Term object + * @return string $postmeta_key Unique key + */ + public function get_postmeta_key( $term ) { + $key = self::metadata_postmeta_key; + $type = $term->type; + $prefix = "{$key}_{$type}"; + $postmeta_key = "{$prefix}_" . ( is_object( $term ) ? $term->slug : $term ); + return $postmeta_key; + } - // Authentication checks: make sure data came from our meta box and that the current user is allowed to edit the post - // TODO: switch to using check_admin_referrer? See core (e.g. edit.php) for usage - if ( ! isset( $_POST[self::metadata_taxonomy . "_nonce"] ) - || ! wp_verify_nonce( $_POST[self::metadata_taxonomy . "_nonce"], 'ef-save-metabox' ) ) { - return $id; + /** + * Returns the value for the given metadata + * + * @param object|string|int term The term object, slug or ID for the metadata field term + * @param int post_id The ID of the post + */ + public function get_postmeta_value( $term, $post_id ) { + if ( ! is_object( $term ) ) { + if ( is_int( $term ) ) { + $term = $this->get_editorial_metadata_term_by( 'id', $term ); + } else { + $term = $this->get_editorial_metadata_term_by( 'slug', $term ); + } + } + $postmeta_key = $this->get_postmeta_key( $term ); + return get_metadata( 'post', $post_id, $postmeta_key, true ); } - - if( ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) - || ! in_array( $post->post_type, $this->get_post_types_for_module( $this->module ) ) - || $post->post_type == 'post' && !current_user_can( 'edit_post', $id ) - || $post->post_type == 'page' && !current_user_can( 'edit_page', $id ) ) { - return $id; + + /** + * Get all of the editorial metadata terms as objects and sort by position + * @todo Figure out what we should do with the filter... + * + * @param array $filter_args Filter to specific arguments + * @return array $ordered_terms The terms as they should be ordered + */ + public function get_editorial_metadata_terms( $filter_args = array() ) { + + // Try to fetch from internal object cache + $arg_hash = md5( serialize( $filter_args ) ); + if ( isset( $this->editorial_metadata_terms_cache[ $arg_hash ] ) ) { + return $this->editorial_metadata_terms_cache[ $arg_hash ]; + } + + $terms = get_terms( array( + 'taxonomy' => self::metadata_taxonomy, + 'orderby' => apply_filters( 'ef_editorial_metadata_term_order', 'name' ), + 'hide_empty' => false, + )); + + $ordered_terms = array(); + $hold_to_end = array(); + // Order the terms + foreach ( $terms as $key => $term ) { + + // Unencode and set all of our psuedo term meta because we need the position and viewable if they exists + // First do an array_merge() on the term object to make sure the keys exist, then array_merge() + // any values that may already exist + $unencoded_description = $this->get_unencoded_description( $term->description ); + $defaults = array( + 'description' => '', + 'viewable' => false, + 'position' => false, + ); + $term = array_merge( $defaults, (array) $term ); + if ( is_array( $unencoded_description ) ) { + $term = array_merge( $term, $unencoded_description ); + } + $term = (object) $term; + // We used to store the description field in a funny way + if ( isset( $term->desc ) ) { + $term->description = $term->desc; + unset( $term->desc ); + } + // Only add the term to the ordered array if it has a set position and doesn't conflict with another key + // Otherwise, hold it for later + if ( $term->position && ! array_key_exists( $term->position, $ordered_terms ) ) { + $ordered_terms[ (int) $term->position ] = $term; + } else { + $hold_to_end[] = $term; + } + } + // Sort the items numerically by key + ksort( $ordered_terms, SORT_NUMERIC ); + // Append all of the terms that didn't have an existing position + foreach ( $hold_to_end as $unpositioned_term ) { + $ordered_terms[] = $unpositioned_term; + } + + // If filter arguments were passed, do our filtering + $ordered_terms = wp_filter_object_list( $ordered_terms, $filter_args ); + + // Set the internal object cache + $this->editorial_metadata_terms_cache[ $arg_hash ] = $ordered_terms; + + return $ordered_terms; } - - // Authentication passed, let's save the data - $terms = $this->get_editorial_metadata_terms(); - $term_slugs = array(); - - foreach ( $terms as $term ) { - // Setup the key for this editorial metadata term (same as what's in $_POST) - $key = $this->get_postmeta_key( $term ); - - // Get the current editorial metadata - // TODO: do we care about the current_metadata at all? - //$current_metadata = get_post_meta( $id, $key, true ); - - $new_metadata = isset( $_POST[$key] ) ? $_POST[$key] : ''; - $type = $term->type; - if ( empty ( $new_metadata ) ) { - delete_post_meta( $id, $key ); - } else { + /** + * Returns a term for single metadata field + * + * @param int|string $field The slug or ID for the metadata field term to return + * @return object $term Term's object representation + */ + public function get_editorial_metadata_term_by( $field, $value ) { - // TODO: Move this to a function - if ( 'date' === $type ) { - $date_to_parse = isset( $_POST[ $key . '_hidden' ] ) ? $_POST[ $key . '_hidden' ] : ''; - $date = DateTime::createFromFormat('Y-m-d H:i', $date_to_parse ); + if ( ! in_array( $field, array( 'id', 'slug', 'name' ) ) ) { + return false; + } - if ( false !== $date ) { - $new_metadata = $date->getTimestamp(); - } else { - // Fallback, in case $_POST[ $key . '_hidden' ] was not previosuly set - $new_metadata = strtotime( $new_metadata ); - } + if ( 'id' == $field ) { + $field = 'term_id'; + } + + $terms = $this->get_editorial_metadata_terms(); + $term = wp_filter_object_list( $terms, array( $field => $value ) ); + + if ( ! empty( $term ) ) { + return array_shift( $term ); + } else { + return false; + } + } + + /** + * Register editorial metadata fields as columns in the manage posts view + * Only adds columns for the currently active post types - logic controlled in $this->init() + * + * @since 0.7 + * @uses apply_filters( 'manage_posts_columns' ) in wp-admin/includes/class-wp-posts-list-table.php + * + * @param array $posts_columns Existing post columns prepared by WP_List_Table + * @param array $posts_columns Previous post columns with the new values + */ + public function filter_manage_posts_columns( $posts_columns ) { + $screen = get_current_screen(); + if ( $screen ) { + add_filter( "manage_{$screen->id}_sortable_columns", array( $this, 'filter_manage_posts_sortable_columns' ) ); + $terms = $this->get_editorial_metadata_terms( array( 'viewable' => true ) ); + foreach ( $terms as $term ) { + // Prefixing slug with module slug because it isn't stored prefixed and we want to avoid collisions + $key = $this->module->slug . '-' . $term->slug; + $posts_columns[ $key ] = $term->name; } - if ( 'number' === $type ) { - $new_metadata = (int)$new_metadata; + } + return $posts_columns; + } + + /** + * Register any viewable date editorial metadata as a sortable column + * + * @since 0.7.4 + * + * @param array $sortable_columns Any existing sortable columns (e.g. Title) + * @return array $sortable_columms Sortable columns with editorial metadata date fields added + */ + public function filter_manage_posts_sortable_columns( $sortable_columns ) { + + $terms = $this->get_editorial_metadata_terms( array( + 'viewable' => true, + 'type' => 'date', + ) ); + foreach ( $terms as $term ) { + // Prefixing slug with module slug because it isn't stored prefixed and we want to avoid collisions + $key = $this->module->slug . '-' . $term->slug; + $sortable_columns[ $key ] = $key; + } + return $sortable_columns; + } + + /** + * If we're ordering by a sortable column, let's modify the query + * + * @since 0.7.4 + */ + public function action_parse_query( $query ) { + + if ( is_admin() && false !== stripos( get_query_var( 'orderby' ), $this->module->slug ) ) { + $term_slug = sanitize_key( str_replace( $this->module->slug . '-', '', get_query_var( 'orderby' ) ) ); + $term = $this->get_editorial_metadata_term_by( 'slug', $term_slug ); + $meta_key = $this->get_postmeta_key( $term ); + set_query_var( 'meta_key', $meta_key ); + set_query_var( 'orderby', 'meta_value_num' ); + } + } + + /** + * Handle the output of an editorial metadata custom column + * Logic for the post types this is called on is controlled in $this->init() + * + * @since 0.7 + * @uses do_action( 'manage_posts_custom_column' ) in wp-admin/includes/class-wp-posts-list-table.php + * + * @param string $column_name Unique string for the column + * @param int $post_id ID for the post of the row + */ + public function action_manage_posts_custom_column( $column_name, $post_id ) { + + $terms = $this->get_editorial_metadata_terms(); + // We're looking for the proper term to display its saved value + foreach ( $terms as $term ) { + $key = $this->module->slug . '-' . $term->slug; + if ( $column_name != $key ) { + continue; } - - $new_metadata = strip_tags( $new_metadata ); - update_post_meta( $id, $key, $new_metadata ); - - // Add the slugs of the terms with non-empty new metadata to an array - $term_slugs[] = $term->slug; - } - do_action( 'ef_editorial_metadata_field_updated', $key, $new_metadata, $id, $type ); + + $current_metadata = $this->get_postmeta_value( $term, $post_id ); + echo esc_html( $this->generate_editorial_metadata_term_output( $term, $current_metadata ) ); + } } - - // Relate the post to the terms used and taxonomy type (wp_term_relationships table). - // This will allow us to update and display the count of metadata in posts in use per term. - // TODO: Core only correlates posts with terms if the post_status is publish. Do we care what it is? - if ( $post->post_status === 'publish' ) { - wp_set_object_terms( $id, $term_slugs, self::metadata_taxonomy ); + + /** + * If the Edit Flow Calendar is enabled, add viewable Editorial Metadata terms + * + * @since 0.7 + * @uses apply_filters( 'ef_calendar_item_information_fields' ) + * + * @param array $calendar_fields Additional data fields to include on the calendar + * @param int $post_id Unique ID for the post data we're building + * @return array $calendar_fields Calendar fields with our viewable Editorial Metadata added + */ + public function filter_calendar_item_fields( $calendar_fields, $post_id ) { + + + // Make sure we respect which post type we're on + if ( ! in_array( get_post_type( $post_id ), $this->get_post_types_for_module( $this->module ) ) ) { + return $calendar_fields; + } + + $terms = $this->get_editorial_metadata_terms( array( 'viewable' => true ) ); + + foreach ( $terms as $term ) { + $key = $this->module->slug . '-' . $term->slug; + + // Default values + $current_metadata = $this->get_postmeta_value( $term, $post_id ); + $term_data = array( + 'label' => $term->name, + 'value' => $this->generate_editorial_metadata_term_output( $term, $current_metadata ), + ); + $term_data['editable'] = true; + $term_data['type'] = $term->type; + $calendar_fields[ $key ] = $term_data; + } + return $calendar_fields; } - } - - /** - * Generate a unique key based on the term - * - * @param object $term Term object - * @return string $postmeta_key Unique key - */ - function get_postmeta_key( $term ) { - $key = self::metadata_postmeta_key; - $type = $term->type; - $prefix = "{$key}_{$type}"; - $postmeta_key = "{$prefix}_" . ( is_object( $term ) ? $term->slug : $term ); - return $postmeta_key; - } - - /** - * Returns the value for the given metadata - * - * @param object|string|int term The term object, slug or ID for the metadata field term - * @param int post_id The ID of the post - */ - function get_postmeta_value( $term, $post_id ) { - if( ! is_object( $term ) ) { - if ( is_int( $term ) ) - $term = $this->get_editorial_metadata_term_by( 'id', $term ); - else - $term = $this->get_editorial_metadata_term_by( 'slug', $term ); + + /** + * If the Edit Flow Story Budget is enabled, register our viewable terms as columns + * + * @since 0.7 + * @uses apply_filters( 'ef_story_budget_term_columns' ) + * + * @param array $term_columns The existing columns on the story budget + * @return array $term_columns Term columns with viewable Editorial Metadata terms + */ + public function filter_story_budget_term_columns( $term_columns ) { + + $terms = $this->get_editorial_metadata_terms( array( 'viewable' => true ) ); + foreach ( $terms as $term ) { + // Prefixing slug with module slug because it isn't stored prefixed and we want to avoid collisions + $key = $this->module->slug . '-' . $term->slug; + // Switch to underscores + $key = str_replace( '-', '_', $key ); + $term_columns[ $key ] = $term->name; + } + return $term_columns; } - $postmeta_key = $this->get_postmeta_key( $term ); - return get_metadata( 'post', $post_id, $postmeta_key, true ); - } - - /** - * Get all of the editorial metadata terms as objects and sort by position - * @todo Figure out what we should do with the filter... - * - * @param array $filter_args Filter to specific arguments - * @return array $ordered_terms The terms as they should be ordered - */ - function get_editorial_metadata_terms( $filter_args = array() ) { - - // Try to fetch from internal object cache - $arg_hash = md5( serialize( $filter_args ) ); - if ( isset( $this->editorial_metadata_terms_cache[ $arg_hash ] ) ) { - return $this->editorial_metadata_terms_cache[ $arg_hash ]; + + /** + * If the Edit Flow Story Budget is enabled, + * + * @since 0.7 + * @uses apply_filters( 'ef_story_budget_term_column_value' ) + * + * @param object $post The post we're displaying + * @param string $column_name Name of the column, as registered with EF_Story_Budget::register_term_columns + * @param object $parent_term The parent term for the term column + */ + public function filter_story_budget_term_column_values( $column_name, $post, $parent_term ) { + + $local_column_name = str_replace( '_', '-', $column_name ); + // Don't accidentally handle values not our own + if ( false === strpos( $local_column_name, $this->module->slug ) ) { + return $column_name; + } + + $term_slug = str_replace( $this->module->slug . '-', '', $local_column_name ); + $term = $this->get_editorial_metadata_term_by( 'slug', $term_slug ); + + // Don't allow non-viewable term data to be displayed + if ( ! $term->viewable ) { + return $column_name; + } + + $current_metadata = $this->get_postmeta_value( $term, $post->ID ); + $output = $this->generate_editorial_metadata_term_output( $term, $current_metadata ); + + return $output; + } + + /** + * Generate the presentational output for an editorial metadata term + * + * @since 0.8 + * + * @param object $term The editorial metadata term + * @return string $html How the term should be rendered + */ + private function generate_editorial_metadata_term_output( $term, $pm_value ) { + + $output = ''; + switch ( $term->type ) { + case 'date': + if ( empty( $pm_value ) ) { + break; + } + + // All day vs. day and time + $date = date( get_option( 'date_format' ), $pm_value ); + $time = date( get_option( 'time_format' ), $pm_value ); + if ( '0000' == date( 'Hi', $pm_value ) ) { + $pm_value = $date; + } else { + // translators: 1: date, 2: time + $pm_value = sprintf( __( '%1$s at %2$s', 'edit-flow' ), $date, $time ); + } + $output = esc_html( $pm_value ); + break; + case 'location': + case 'text': + case 'number': + case 'paragraph': + if ( $pm_value ) { + $output = esc_html( $pm_value ); + } + break; + case 'checkbox': + if ( $pm_value ) { + $output = __( 'Yes', 'edit-flow' ); + } else { + $output = __( 'No', 'edit-flow' ); + } + break; + case 'user': + if ( empty( $pm_value ) ) { + break; + } + $userdata = get_user_by( 'id', $pm_value ); + if ( is_object( $userdata ) ) { + $output = esc_html( $userdata->display_name ); + } + break; + default: + break; + } + return $output; } - - $args = array( - 'orderby' => apply_filters( 'ef_editorial_metadata_term_order', 'name' ), - 'hide_empty' => false + + /** + * Update an existing editorial metadata term if the term_id exists + * + * @since 0.7 + * + * @param int $term_id The term's unique ID + * @param array $args Any values that need to be updated for the term + * @return object|WP_Error $updated_term The updated term or a WP_Error object if something disastrous happened + */ + public function update_editorial_metadata_term( $term_id, $args ) { + + $new_args = array(); + $old_term = $this->get_editorial_metadata_term_by( 'id', $term_id ); + if ( $old_term ) { + $old_args = array( + 'position' => $old_term->position, + 'name' => $old_term->name, + 'slug' => $old_term->slug, + 'description' => $old_term->description, + 'type' => $old_term->type, + 'viewable' => $old_term->viewable, + ); + } + $new_args = array_merge( $old_args, $args ); + + // We're encoding metadata that isn't supported by default in the term's description field + $args_to_encode = array( + 'description' => $new_args['description'], + 'position' => $new_args['position'], + 'type' => $new_args['type'], + 'viewable' => $new_args['viewable'], ); + $encoded_description = $this->get_encoded_description( $args_to_encode ); + $new_args['description'] = $encoded_description; + + $updated_term = wp_update_term( $term_id, self::metadata_taxonomy, $new_args ); - $terms = get_terms( self::metadata_taxonomy, $args ); - $ordered_terms = array(); - $hold_to_end = array(); - // Order the terms - foreach ( $terms as $key => $term ) { - - // Unencode and set all of our psuedo term meta because we need the position and viewable if they exists - // First do an array_merge() on the term object to make sure the keys exist, then array_merge() - // any values that may already exist - $unencoded_description = $this->get_unencoded_description( $term->description ); + // Reset the internal object cache + $this->editorial_metadata_terms_cache = array(); + + $updated_term = $this->get_editorial_metadata_term_by( 'id', $term_id ); + return $updated_term; + } + + /** + * Insert a new editorial metadata term + * @todo Handle conflicts with existing terms at that position (if relevant) + * + * @since 0.7 + */ + public function insert_editorial_metadata_term( $args ) { + + + // Term is always added to the end of the list + $default_position = count( $this->get_editorial_metadata_terms() ) + 2; $defaults = array( + 'position' => $default_position, + 'name' => '', + 'slug' => '', 'description' => '', + 'type' => '', 'viewable' => false, - 'position' => false, ); - $term = array_merge( $defaults, (array)$term ); - if ( is_array( $unencoded_description ) ) { - $term = array_merge( $term, $unencoded_description ); - } - $term = (object)$term; - // We used to store the description field in a funny way - if ( isset( $term->desc ) ) { - $term->description = $term->desc; - unset( $term->desc ); - } - // Only add the term to the ordered array if it has a set position and doesn't conflict with another key - // Otherwise, hold it for later - if ( $term->position && !array_key_exists( $term->position, $ordered_terms ) ) - $ordered_terms[(int)$term->position] = $term; - else - $hold_to_end[] = $term; + $args = array_merge( $defaults, $args ); + $term_name = $args['name']; + unset( $args['name'] ); + + // We're encoding metadata that isn't supported by default in the term's description field + $args_to_encode = array( + 'description' => $args['description'], + 'position' => $args['position'], + 'type' => $args['type'], + 'viewable' => $args['viewable'], + ); + $encoded_description = $this->get_encoded_description( $args_to_encode ); + $args['description'] = $encoded_description; + + $inserted_term = wp_insert_term( $term_name, self::metadata_taxonomy, $args ); + + // Reset the internal object cache + $this->editorial_metadata_terms_cache = array(); + + return $inserted_term; + } + + /** + * Settings and other management code + */ + + /** + * Delete an existing editorial metadata term + * + * @since 0.7 + * + * @param int $term_id The term we want deleted + * @return bool $result Whether or not the term was deleted + */ + public function delete_editorial_metadata_term( $term_id ) { + $result = wp_delete_term( $term_id, self::metadata_taxonomy ); + + // Reset the internal object cache + $this->editorial_metadata_terms_cache = array(); + + return $result; + } + + /** + * Generate a link to one of the editorial metadata actions + * + * @since 0.7 + * + * @param array $args (optional) Action and any query args to add to the URL + * @return string $link Direct link to complete the action + */ + public function get_link( $args = array() ) { + if ( ! isset( $args['action'] ) ) { + $args['action'] = ''; + } + if ( ! isset( $args['page'] ) ) { + $args['page'] = $this->module->settings_slug; + } + // Add other things we may need depending on the action + switch ( $args['action'] ) { + case 'make-viewable': + case 'make-hidden': + case 'delete-term': + $args['nonce'] = wp_create_nonce( $args['action'] ); + break; + default: + break; + } + return add_query_arg( $args, get_admin_url( null, 'admin.php' ) ); + } + + /** + * Handles a request to add a new piece of editorial metadata + */ + public function handle_add_editorial_metadata() { + + if ( ! isset( $_POST['submit'], $_POST['form-action'], $_GET['page'] ) + || $_GET['page'] != $this->module->settings_slug || 'add-term' != $_POST['form-action'] ) { + return; + } + + if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'editorial-metadata-add-nonce' ) ) { + wp_die( esc_html( $this->module->messages['nonce-failed'] ) ); + } + + if ( ! current_user_can( 'manage_options' ) ) { + wp_die( esc_html( $this->module->messages['invalid-permissions'] ) ); + } + + // Sanitize all of the user-entered values + $term_name = isset( $_POST['metadata_name'] ) ? sanitize_text_field( trim( $_POST['metadata_name'] ) ) : ''; + $term_slug = ( ! empty( $_POST['metadata_slug'] ) ) ? sanitize_title( $_POST['metadata_slug'] ) : sanitize_title( $term_name ); + $term_description = isset( $_POST['metadata_description'] ) ? stripslashes( wp_filter_nohtml_kses( trim( $_POST['metadata_description'] ) ) ) : ''; + $term_type = isset( $_POST['metadata_type'] ) ? sanitize_key( $_POST['metadata_type'] ) : ''; + + $_REQUEST['form-errors'] = array(); + + /** + * Form validation for adding new editorial metadata term + * + * Details + * - "name", "slug", and "type" are required fields + * - "description" can accept a limited amount of HTML, and is optional + */ + // Field is required + if ( empty( $term_name ) ) { + $_REQUEST['form-errors']['name'] = __( 'Please enter a name for the editorial metadata.', 'edit-flow' ); + } + // Field is required + if ( empty( $term_slug ) ) { + $_REQUEST['form-errors']['slug'] = __( 'Please enter a slug for the editorial metadata.', 'edit-flow' ); + } + if ( term_exists( $term_slug ) ) { + $_REQUEST['form-errors']['name'] = __( 'Name conflicts with existing term. Please choose another.', 'edit-flow' ); + } + // Check to ensure a term with the same name doesn't exist + if ( $this->get_editorial_metadata_term_by( 'name', $term_name, self::metadata_taxonomy ) ) { + $_REQUEST['form-errors']['name'] = __( 'Name already in use. Please choose another.', 'edit-flow' ); + } + // Check to ensure a term with the same slug doesn't exist + if ( $this->get_editorial_metadata_term_by( 'slug', $term_slug ) ) { + $_REQUEST['form-errors']['slug'] = __( 'Slug already in use. Please choose another.', 'edit-flow' ); + } + // Check to make sure the status doesn't already exist as another term because otherwise we'd get a weird slug + // Check that the term name doesn't exceed 200 chars + if ( strlen( $term_name ) > 200 ) { + $_REQUEST['form-errors']['name'] = __( 'Name cannot exceed 200 characters. Please try a shorter name.', 'edit-flow' ); + } + // Metadata type needs to pass our whitelist check + $metadata_types = $this->get_supported_metadata_types(); + if ( empty( $_POST['metadata_type'] ) || ! isset( $metadata_types[ $_POST['metadata_type'] ] ) ) { + $_REQUEST['form-errors']['type'] = __( 'Please select a valid metadata type.', 'edit-flow' ); + } + // Metadata viewable needs to be a valid Yes or No + $term_viewable = false; + if ( isset( $_POST['metadata_viewable'] ) && 'yes' == $_POST['metadata_viewable'] ) { + $term_viewable = true; + } + + // Kick out if there are any errors + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated + if ( count( $_REQUEST['form-errors'] ) ) { + $_REQUEST['error'] = 'form-error'; + return; + } + + // Try to add the status + $args = array( + 'name' => $term_name, + 'description' => $term_description, + 'slug' => $term_slug, + 'type' => $term_type, + 'viewable' => $term_viewable, + ); + $return = $this->insert_editorial_metadata_term( $args ); + if ( is_wp_error( $return ) ) { + wp_die( esc_html__( 'Error adding term.', 'edit-flow' ) ); + } + + $redirect_url = add_query_arg( array( + 'page' => $this->module->settings_slug, + 'message' => 'term-added', + ), get_admin_url( null, 'admin.php' ) ); + wp_redirect( $redirect_url ); + exit; + } + + /** + * Handles a request to edit an editorial metadata + */ + public function handle_edit_editorial_metadata() { + if ( ! isset( $_POST['submit'], $_GET['page'], $_GET['action'], $_GET['term-id'] ) + || $_GET['page'] != $this->module->settings_slug || 'edit-term' != $_GET['action'] ) { + return; + } + + if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'editorial-metadata-edit-nonce' ) ) { + wp_die( esc_html( $this->module->messages['nonce-failed'] ) ); + } + + if ( ! current_user_can( 'manage_options' ) ) { + wp_die( esc_html( $this->module->messages['invalid-permissions'] ) ); + } + + if ( ! $existing_term = $this->get_editorial_metadata_term_by( 'id', (int) $_GET['term-id'] ) ) { + wp_die( esc_html( $this->module->messages['term-missing'] ) ); + } + + $new_name = isset( $_POST['name'] ) ? sanitize_text_field( trim( $_POST['name'] ) ) : ''; + $denew_descriptionscription = isset( $_POST['description'] ) ? stripslashes( wp_filter_nohtml_kses( trim( $_POST['description'] ) ) ) : ''; + + /** + * Form validation for editing editorial metadata term + * + * Details + * - "name", "slug", and "type" are required fields + * - "description" can accept a limited amount of HTML, and is optional + */ + $_REQUEST['form-errors'] = array(); + // Check if name field was filled in + if ( empty( $new_name ) ) { + $_REQUEST['form-errors']['name'] = __( 'Please enter a name for the editorial metadata', 'edit-flow' ); + } + + // Check that the name isn't numeric + if ( is_numeric( $new_name ) ) { + $_REQUEST['form-errors']['name'] = __( 'Please enter a valid, non-numeric name for the editorial metadata.', 'edit-flow' ); + } + + $term_exists = term_exists( sanitize_title( $new_name ) ); + if ( $term_exists && $term_exists != $existing_term->term_id ) { + $_REQUEST['form-errors']['name'] = __( 'Metadata name conflicts with existing term. Please choose another.', 'edit-flow' ); + } + + // Check to ensure a term with the same name doesn't exist, + $search_term = $this->get_editorial_metadata_term_by( 'name', $new_name ); + if ( is_object( $search_term ) && $search_term->term_id != $existing_term->term_id ) { + $_REQUEST['form-errors']['name'] = __( 'Name already in use. Please choose another.', 'edit-flow' ); + } + // or that the term name doesn't map to an existing term's slug + $search_term = $this->get_editorial_metadata_term_by( 'slug', sanitize_title( $new_name ) ); + if ( is_object( $search_term ) && $search_term->term_id != $existing_term->term_id ) { + $_REQUEST['form-errors']['name'] = __( 'Name conflicts with slug for another term. Please choose something else.', 'edit-flow' ); + } + + // Check that the term name doesn't exceed 200 chars + if ( strlen( $new_name ) > 200 ) { + $_REQUEST['form-errors']['name'] = __( 'Name cannot exceed 200 characters. Please try a shorter name.', 'edit-flow' ); + } + // Make sure the viewable state is valid + $new_viewable = false; + if ( isset( $_POST['viewable'] ) && 'yes' == $_POST['viewable'] ) { + $new_viewable = true; + } + + // Kick out if there are any errors + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated + if ( count( $_REQUEST['form-errors'] ) ) { + $_REQUEST['error'] = 'form-error'; + return; + } + + // Try to add the metadata term + $args = array( + 'name' => $new_name, + 'description' => $new_description, + 'viewable' => $new_viewable, + ); + $return = $this->update_editorial_metadata_term( $existing_term->term_id, $args ); + if ( is_wp_error( $return ) ) { + wp_die( esc_html__( 'Error updating term.', 'edit-flow' ) ); + } + + $redirect_url = add_query_arg( array( + 'page' => $this->module->settings_slug, + 'message' => 'term-updated', + ), get_admin_url( null, 'admin.php' ) ); + wp_redirect( $redirect_url ); + exit; + } + + /** + * Handle a $_GET request to change the visibility of an Editorial Metadata term + * + * @since 0.7 + */ + public function handle_change_editorial_metadata_visibility() { + + // Check that the current GET request is our GET request + if ( ! isset( $_GET['page'], $_GET['action'], $_GET['term-id'], $_GET['nonce'] ) + || $_GET['page'] != $this->module->settings_slug || ! in_array( $_GET['action'], array( 'make-viewable', 'make-hidden' ) ) ) { + return; + } + + // Check for proper nonce + if ( ! isset( $_GET['nonce'] ) || ( ! wp_verify_nonce( $_GET['nonce'], 'make-viewable' ) && ! wp_verify_nonce( $_GET['nonce'], 'make-hidden' ) ) ) { + wp_die( esc_html( $this->module->messages['nonce-failed'] ) ); + } + + // Only allow users with the proper caps + if ( ! current_user_can( 'manage_options' ) ) { + wp_die( esc_html( $this->module->messages['invalid-permissions'] ) ); + } + + $term_id = (int) $_GET['term-id']; + $args = array(); + if ( 'make-viewable' == $_GET['action'] ) { + $args['viewable'] = true; + } elseif ( 'make-hidden' == $_GET['action'] ) { + $args['viewable'] = false; + } + + $return = $this->update_editorial_metadata_term( $term_id, $args ); + if ( is_wp_error( $return ) ) { + wp_die( esc_html__( 'Error updating term.', 'edit-flow' ) ); + } + + $redirect_url = $this->get_link( array( 'message' => 'term-visibility-changed' ) ); + wp_redirect( $redirect_url ); + exit; } - // Sort the items numerically by key - ksort( $ordered_terms, SORT_NUMERIC ); - // Append all of the terms that didn't have an existing position - foreach( $hold_to_end as $unpositioned_term ) - $ordered_terms[] = $unpositioned_term; - // If filter arguments were passed, do our filtering - $ordered_terms = wp_filter_object_list( $ordered_terms, $filter_args ); + /** + * Handle the request to update a given Editorial Metadata term via inline edit + * + * @since 0.7 + */ + public function handle_ajax_inline_save_term() { - // Set the internal object cache - $this->editorial_metadata_terms_cache[ $arg_hash ] = $ordered_terms; + if ( ! isset( $_POST['inline_edit'] ) || ! wp_verify_nonce( $_POST['inline_edit'], 'editorial-metadata-inline-edit-nonce' ) ) { + die( esc_html( $this->module->messages['nonce-failed'] ) ); + } - return $ordered_terms; - } - - /** - * Returns a term for single metadata field - * - * @param int|string $field The slug or ID for the metadata field term to return - * @return object $term Term's object representation - */ - function get_editorial_metadata_term_by( $field, $value ) { + if ( ! current_user_can( 'manage_options' ) ) { + die( esc_html( $this->module->messages['invalid-permissions'] ) ); + } - if ( ! in_array( $field, array( 'id', 'slug', 'name' ) ) ) - return false; + $term_id = isset( $_POST['term_id'] ) ? (int) $_POST['term_id'] : 0; + if ( ! $existing_term = $this->get_editorial_metadata_term_by( 'id', $term_id ) ) { + die( esc_html( $this->module->messages['term-missing'] ) ); + } - if ( 'id' == $field ) - $field = 'term_id'; + $metadata_name = isset( $_POST['name'] ) ? sanitize_text_field( trim( $_POST['name'] ) ) : ''; + $metadata_description = isset( $_POST['description'] ) ? stripslashes( wp_filter_nohtml_kses( trim( $_POST['description'] ) ) ) : ''; - $terms = $this->get_editorial_metadata_terms(); - $term = wp_filter_object_list( $terms, array( $field => $value ) ); + /** + * Form validation for editing editorial metadata term + */ + // Check if name field was filled in + if ( empty( $metadata_name ) ) { + $change_error = new WP_Error( 'invalid', _esc_html__( 'Please enter a name for the editorial metadata', 'edit-flow' ) ); + die( esc_html( $change_error->get_error_message() ) ); + } - if ( ! empty( $term ) ) - return array_shift( $term ); - else - return false; - } - - /** - * Register editorial metadata fields as columns in the manage posts view - * Only adds columns for the currently active post types - logic controlled in $this->init() - * - * @since 0.7 - * @uses apply_filters( 'manage_posts_columns' ) in wp-admin/includes/class-wp-posts-list-table.php - * - * @param array $posts_columns Existing post columns prepared by WP_List_Table - * @param array $posts_columns Previous post columns with the new values - */ - function filter_manage_posts_columns( $posts_columns ) { - $screen = get_current_screen(); - if ( $screen ) { - add_filter( "manage_{$screen->id}_sortable_columns", array( $this, 'filter_manage_posts_sortable_columns' ) ); - $terms = $this->get_editorial_metadata_terms( array( 'viewable' => true ) ); - foreach( $terms as $term ) { - // Prefixing slug with module slug because it isn't stored prefixed and we want to avoid collisions - $key = $this->module->slug . '-' . $term->slug; - $posts_columns[$key] = $term->name; + // Check that the name isn't numeric + if ( is_numeric( $metadata_name ) ) { + $change_error = new WP_Error( 'invalid', esc_html__( 'Please enter a valid, non-numeric name for the editorial metadata.', 'edit-flow' ) ); + die( esc_html( $change_error->get_error_message() ) ); } - } - return $posts_columns; - } - - /** - * Register any viewable date editorial metadata as a sortable column - * - * @since 0.7.4 - * - * @param array $sortable_columns Any existing sortable columns (e.g. Title) - * @return array $sortable_columms Sortable columns with editorial metadata date fields added - */ - function filter_manage_posts_sortable_columns( $sortable_columns ) { - $terms = $this->get_editorial_metadata_terms( array( 'viewable' => true, 'type' => 'date' ) ); - foreach( $terms as $term ) { - // Prefixing slug with module slug because it isn't stored prefixed and we want to avoid collisions - $key = $this->module->slug . '-' . $term->slug; - $sortable_columns[$key] = $key; - } - return $sortable_columns; - } + // Check that the term name doesn't exceed 200 chars + if ( strlen( $metadata_name ) > 200 ) { + $change_error = new WP_Error( 'invalid', esc_html__( 'Name cannot exceed 200 characters. Please try a shorter name.' ) ); + die( esc_html( $change_error->get_error_message() ) ); + } - /** - * If we're ordering by a sortable column, let's modify the query - * - * @since 0.7.4 - */ - function action_parse_query( $query ) { + // Check to make sure the status doesn't already exist as another term because otherwise we'd get a fatal error + $term_exists = term_exists( sanitize_title( $metadata_name ) ); + if ( $term_exists && $term_exists != $term_id ) { + $change_error = new WP_Error( 'invalid', esc_html____( 'Metadata name conflicts with existing term. Please choose another.', 'edit-flow' ) ); + die( esc_html( $change_error->get_error_message() ) ); + } - if ( is_admin() && false !== stripos( get_query_var( 'orderby' ), $this->module->slug ) ) { - $term_slug = sanitize_key( str_replace( $this->module->slug . '-', '', get_query_var( 'orderby') ) ); - $term = $this->get_editorial_metadata_term_by( 'slug', $term_slug ); - $meta_key = $this->get_postmeta_key( $term ); - set_query_var( 'meta_key', $meta_key ); - set_query_var( 'orderby', 'meta_value_num' ); - } - } - - /** - * Handle the output of an editorial metadata custom column - * Logic for the post types this is called on is controlled in $this->init() - * - * @since 0.7 - * @uses do_action( 'manage_posts_custom_column' ) in wp-admin/includes/class-wp-posts-list-table.php - * - * @param string $column_name Unique string for the column - * @param int $post_id ID for the post of the row - */ - function action_manage_posts_custom_column( $column_name, $post_id ) { - - $terms = $this->get_editorial_metadata_terms(); - // We're looking for the proper term to display its saved value - foreach( $terms as $term ) { - $key = $this->module->slug . '-' . $term->slug; - if ( $column_name != $key ) - continue; - - $current_metadata = $this->get_postmeta_value( $term, $post_id ); - echo $this->generate_editorial_metadata_term_output( $term, $current_metadata ); - } - - } - - /** - * If the Edit Flow Calendar is enabled, add viewable Editorial Metadata terms - * - * @since 0.7 - * @uses apply_filters( 'ef_calendar_item_information_fields' ) - * - * @param array $calendar_fields Additional data fields to include on the calendar - * @param int $post_id Unique ID for the post data we're building - * @return array $calendar_fields Calendar fields with our viewable Editorial Metadata added - */ - function filter_calendar_item_fields( $calendar_fields, $post_id ) { + // Check to ensure a term with the same name doesn't exist, + $search_term = $this->get_editorial_metadata_term_by( 'name', $metadata_name ); + if ( is_object( $search_term ) && $search_term->term_id != $existing_term->term_id ) { + $change_error = new WP_Error( 'invalid', esc_html__( 'Name already in use. Please choose another.', 'edit-flow' ) ); + die( esc_html( $change_error->get_error_message() ) ); + } - - // Make sure we respect which post type we're on - if ( !in_array( get_post_type( $post_id ), $this->get_post_types_for_module( $this->module ) ) ) - return $calendar_fields; - - $terms = $this->get_editorial_metadata_terms( array( 'viewable' => true ) ); - - foreach( $terms as $term ) { - $key = $this->module->slug . '-' . $term->slug; - - // Default values - $current_metadata = $this->get_postmeta_value( $term, $post_id ); - $term_data = array( - 'label' => $term->name, - 'value' => $this->generate_editorial_metadata_term_output( $term, $current_metadata ), + // or that the term name doesn't map to an existing term's slug + $search_term = $this->get_editorial_metadata_term_by( 'slug', sanitize_title( $metadata_name ) ); + if ( is_object( $search_term ) && $search_term->term_id != $existing_term->term_id ) { + $change_error = new WP_Error( 'invalid', esc_html__( 'Name conflicts with slug for another term. Please choose again.', 'edit-flow' ) ); + die( esc_html( $change_error->get_error_message() ) ); + } + + // Prepare the term name and description for saving + $args = array( + 'name' => $metadata_name, + 'description' => $metadata_description, ); - $term_data['editable'] = true; - $term_data['type'] = $term->type; - $calendar_fields[$key] = $term_data; + $return = $this->update_editorial_metadata_term( $existing_term->term_id, $args ); + if ( ! is_wp_error( $return ) ) { + set_current_screen( 'edit-editorial-metadata' ); + $wp_list_table = new EF_Editorial_Metadata_List_Table(); + $wp_list_table->prepare_items(); + echo wp_kses_post( $wp_list_table->single_row( $return ) ); + die(); + } else { + /* Translators: 1: the name of the term that could not be found */ + $change_error = new WP_Error( 'invalid', sprintf( __( 'Could not update the term: %s', 'edit-flow' ), $metadata_name ) ); + die( wp_kses( $change_error->get_error_message() ) ); + } } - return $calendar_fields; - - } - - /** - * If the Edit Flow Story Budget is enabled, register our viewable terms as columns - * - * @since 0.7 - * @uses apply_filters( 'ef_story_budget_term_columns' ) - * - * @param array $term_columns The existing columns on the story budget - * @return array $term_columns Term columns with viewable Editorial Metadata terms - */ - function filter_story_budget_term_columns( $term_columns ) { - - $terms = $this->get_editorial_metadata_terms( array( 'viewable' => true ) ); - foreach( $terms as $term ) { - // Prefixing slug with module slug because it isn't stored prefixed and we want to avoid collisions - $key = $this->module->slug . '-' . $term->slug; - // Switch to underscores - $key = str_replace( '-', '_', $key ); - $term_columns[$key] = $term->name; - } - return $term_columns; - - } - - /** - * If the Edit Flow Story Budget is enabled, - * - * @since 0.7 - * @uses apply_filters( 'ef_story_budget_term_column_value' ) - * - * @param object $post The post we're displaying - * @param string $column_name Name of the column, as registered with EF_Story_Budget::register_term_columns - * @param object $parent_term The parent term for the term column - */ - function filter_story_budget_term_column_values( $column_name, $post, $parent_term ) { - - $local_column_name = str_replace( '_', '-', $column_name ); - // Don't accidentally handle values not our own - if ( false === strpos( $local_column_name, $this->module->slug ) ) - return $column_name; - - $term_slug = str_replace( $this->module->slug . '-', '', $local_column_name ); - $term = $this->get_editorial_metadata_term_by( 'slug', $term_slug ); - - // Don't allow non-viewable term data to be displayed - if ( !$term->viewable ) - return $column_name; - - $current_metadata = $this->get_postmeta_value( $term, $post->ID ); - $output = $this->generate_editorial_metadata_term_output( $term, $current_metadata ); - - return $output; - } - /** - * Generate the presentational output for an editorial metadata term - * - * @since 0.8 - * - * @param object $term The editorial metadata term - * @return string $html How the term should be rendered - */ - private function generate_editorial_metadata_term_output( $term, $pm_value ) { + /** + * Handle the ajax request to update all of the term positions + * + * @since 0.7 + */ + public function handle_ajax_update_term_positions() { - $output = ''; - switch( $term->type ) { - case "date": - if ( empty( $pm_value ) ) - break; + if ( ! isset( $_POST['editorial_metadata_sortable_nonce'] ) || ! wp_verify_nonce( $_POST['editorial_metadata_sortable_nonce'], 'editorial-metadata-sortable' ) ) { + $this->print_ajax_response( 'error', $this->module->messages['nonce-failed'] ); + } - // All day vs. day and time - $date = date( get_option( 'date_format' ), $pm_value ); - $time = date( get_option( 'time_format' ), $pm_value ); - if( date( 'Hi', $pm_value ) == '0000' ) - $pm_value = $date; - else - $pm_value = sprintf( __( '%1$s at %2$s', 'edit-flow' ), $date, $time ); - $output = esc_html( $pm_value ); - break; - case "location": - case "text": - case "number": - case "paragraph": - if ( $pm_value ) - $output = esc_html( $pm_value ); - break; - case "checkbox": - if ( $pm_value ) - $output = __( 'Yes', 'edit-flow' ); - else - $output = __( 'No', 'edit-flow' ); - break; - case "user": - if ( empty( $pm_value ) ) - break; - $userdata = get_user_by( 'id', $pm_value ); - if ( is_object( $userdata ) ) - $output = esc_html( $userdata->display_name ); - break; - default: - break; - } - return $output; - } - - /** - * Update an existing editorial metadata term if the term_id exists - * - * @since 0.7 - * - * @param int $term_id The term's unique ID - * @param array $args Any values that need to be updated for the term - * @return object|WP_Error $updated_term The updated term or a WP_Error object if something disastrous happened - */ - function update_editorial_metadata_term( $term_id, $args ) { - - $new_args = array(); - $old_term = $this->get_editorial_metadata_term_by( 'id', $term_id ); - if ( $old_term ) - $old_args = array( - 'position' => $old_term->position, - 'name' => $old_term->name, - 'slug' => $old_term->slug, - 'description' => $old_term->description, - 'type' => $old_term->type, - 'viewable' => $old_term->viewable, - ); - $new_args = array_merge( $old_args, $args ); - - // We're encoding metadata that isn't supported by default in the term's description field - $args_to_encode = array( - 'description' => $new_args['description'], - 'position' => $new_args['position'], - 'type' => $new_args['type'], - 'viewable' => $new_args['viewable'], - ); - $encoded_description = $this->get_encoded_description( $args_to_encode ); - $new_args['description'] = $encoded_description; - - $updated_term = wp_update_term( $term_id, self::metadata_taxonomy, $new_args ); - - // Reset the internal object cache - $this->editorial_metadata_terms_cache = array(); - - $updated_term = $this->get_editorial_metadata_term_by( 'id', $term_id ); - return $updated_term; - } - - /** - * Insert a new editorial metadata term - * @todo Handle conflicts with existing terms at that position (if relevant) - * - * @since 0.7 - */ - function insert_editorial_metadata_term( $args ) { - - - // Term is always added to the end of the list - $default_position = count( $this->get_editorial_metadata_terms() ) + 2; - $defaults = array( - 'position' => $default_position, - 'name' => '', - 'slug' => '', - 'description' => '', - 'type' => '', - 'viewable' => false, - ); - $args = array_merge( $defaults, $args ); - $term_name = $args['name']; - unset( $args['name'] ); - - // We're encoding metadata that isn't supported by default in the term's description field - $args_to_encode = array( - 'description' => $args['description'], - 'position' => $args['position'], - 'type' => $args['type'], - 'viewable' => $args['viewable'], - ); - $encoded_description = $this->get_encoded_description( $args_to_encode ); - $args['description'] = $encoded_description; - - $inserted_term = wp_insert_term( $term_name, self::metadata_taxonomy, $args ); - - // Reset the internal object cache - $this->editorial_metadata_terms_cache = array(); - - return $inserted_term; - } - - /** - * Settings and other management code - */ - - /** - * Delete an existing editorial metadata term - * - * @since 0.7 - * - * @param int $term_id The term we want deleted - * @return bool $result Whether or not the term was deleted - */ - function delete_editorial_metadata_term( $term_id ) { - $result = wp_delete_term( $term_id, self::metadata_taxonomy ); + if ( ! current_user_can( 'manage_options' ) ) { + $this->print_ajax_response( 'error', $this->module->messages['invalid-permissions'] ); + } - // Reset the internal object cache - $this->editorial_metadata_terms_cache = array(); + if ( ! isset( $_POST['term_positions'] ) || ! is_array( $_POST['term_positions'] ) ) { + $this->print_ajax_response( 'error', __( 'Terms not set.', 'edit-flow' ) ); + } - return $result; - } - - /** - * Generate a link to one of the editorial metadata actions - * - * @since 0.7 - * - * @param array $args (optional) Action and any query args to add to the URL - * @return string $link Direct link to complete the action - */ - function get_link( $args = array() ) { - if ( !isset( $args['action'] ) ) - $args['action'] = ''; - if ( !isset( $args['page'] ) ) - $args['page'] = $this->module->settings_slug; - // Add other things we may need depending on the action - switch( $args['action'] ) { - case 'make-viewable': - case 'make-hidden': - case 'delete-term': - $args['nonce'] = wp_create_nonce( $args['action'] ); - break; - default: - break; + foreach ( $_POST['term_positions'] as $position => $term_id ) { + + // Have to add 1 to the position because the index started with zero + $args = array( + 'position' => (int) $position + 1, + ); + $return = $this->update_editorial_metadata_term( (int) $term_id, $args ); + // @todo check that this was a valid return + } + $this->print_ajax_response( 'success', $this->module->messages['term-position-updated'] ); } - return add_query_arg( $args, get_admin_url( null, 'admin.php' ) ); - } - - /** - * Handles a request to add a new piece of editorial metadata - */ - function handle_add_editorial_metadata() { - if ( !isset( $_POST['submit'], $_POST['form-action'], $_GET['page'] ) - || $_GET['page'] != $this->module->settings_slug || $_POST['form-action'] != 'add-term' ) + /** + * Handles a request to delete an editorial metadata term + */ + public function handle_delete_editorial_metadata() { + if ( ! isset( $_GET['page'], $_GET['action'], $_GET['term-id'] ) + || $_GET['page'] != $this->module->settings_slug || 'delete-term' != $_GET['action'] ) { return; - - if ( !wp_verify_nonce( $_POST['_wpnonce'], 'editorial-metadata-add-nonce' ) ) - wp_die( $this->module->messages['nonce-failed'] ); - - if ( !current_user_can( 'manage_options' ) ) - wp_die( $this->module->messages['invalid-permissions'] ); - - // Sanitize all of the user-entered values - $term_name = sanitize_text_field( trim( $_POST['metadata_name'] ) ); - $term_slug = ( !empty( $_POST['metadata_slug'] ) ) ? sanitize_title( $_POST['metadata_slug'] ) : sanitize_title( $term_name ); - $term_description = stripslashes( wp_filter_post_kses( trim( $_POST['metadata_description'] ) ) ); - $term_type = sanitize_key( $_POST['metadata_type'] ); - - $_REQUEST['form-errors'] = array(); - + } + + if ( ! isset( $_GET['nonce'] ) || ! wp_verify_nonce( $_GET['nonce'], 'delete-term' ) ) { + wp_die( esc_html( $this->module->messages['nonce-failed'] ) ); + } + + if ( ! current_user_can( 'manage_options' ) ) { + wp_die( esc_html( $this->module->messages['invalid-permissions'] ) ); + } + + if ( ! $existing_term = $this->get_editorial_metadata_term_by( 'id', (int) $_GET['term-id'] ) ) { + wp_die( esc_html( $this->module->messages['term-missing'] ) ); + } + + $result = $this->delete_editorial_metadata_term( $existing_term->term_id ); + if ( ! $result || is_wp_error( $result ) ) { + wp_die( esc_html__( 'Error deleting term.', 'edit-flow' ) ); + } + + $redirect_url = add_query_arg( array( + 'page' => $this->module->settings_slug, + 'message' => 'term-deleted', + ), get_admin_url( null, 'admin.php' ) ); + wp_redirect( $redirect_url ); + exit; + } + /** - * Form validation for adding new editorial metadata term + * Register settings for notifications so we can partially use the Settings API + * (We use the Settings API for form generation, but not saving) * - * Details - * - "name", "slug", and "type" are required fields - * - "description" can accept a limited amount of HTML, and is optional + * @since 0.7 + * @uses add_settings_section(), add_settings_field() */ - // Field is required - if ( empty( $term_name ) ) - $_REQUEST['form-errors']['name'] = __( 'Please enter a name for the editorial metadata.', 'edit-flow' ); - // Field is required - if ( empty( $term_slug ) ) - $_REQUEST['form-errors']['slug'] = __( 'Please enter a slug for the editorial metadata.', 'edit-flow' ); - if ( term_exists( $term_slug ) ) - $_REQUEST['form-errors']['name'] = __( 'Name conflicts with existing term. Please choose another.', 'edit-flow' ); - // Check to ensure a term with the same name doesn't exist - if ( $this->get_editorial_metadata_term_by( 'name', $term_name, self::metadata_taxonomy ) ) - $_REQUEST['form-errors']['name'] = __( 'Name already in use. Please choose another.', 'edit-flow' ); - // Check to ensure a term with the same slug doesn't exist - if ( $this->get_editorial_metadata_term_by( 'slug', $term_slug ) ) - $_REQUEST['form-errors']['slug'] = __( 'Slug already in use. Please choose another.', 'edit-flow' ); - // Check to make sure the status doesn't already exist as another term because otherwise we'd get a weird slug - // Check that the term name doesn't exceed 200 chars - if ( strlen( $term_name ) > 200 ) - $_REQUEST['form-errors']['name'] = __( 'Name cannot exceed 200 characters. Please try a shorter name.', 'edit-flow' ); - // Metadata type needs to pass our whitelist check - $metadata_types = $this->get_supported_metadata_types(); - if ( empty( $_POST['metadata_type'] ) || !isset( $metadata_types[$_POST['metadata_type'] ] ) ) - $_REQUEST['form-errors']['type'] = __( 'Please select a valid metadata type.', 'edit-flow' ); - // Metadata viewable needs to be a valid Yes or No - $term_viewable = false; - if ( $_POST['metadata_viewable'] == 'yes' ) - $term_viewable = true; - - // Kick out if there are any errors - if ( count( $_REQUEST['form-errors'] ) ) { - $_REQUEST['error'] = 'form-error'; - return; + public function register_settings() { + add_settings_section( $this->module->options_group_name . '_general', false, '__return_false', $this->module->options_group_name ); + 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' ); } - // Try to add the status - $args = array( - 'name' => $term_name, - 'description' => $term_description, - 'slug' => $term_slug, - 'type' => $term_type, - 'viewable' => $term_viewable, - ); - $return = $this->insert_editorial_metadata_term( $args ); - if ( is_wp_error( $return ) ) - wp_die( __( 'Error adding term.', 'edit-flow' ) ); - - $redirect_url = add_query_arg( array( 'page' => $this->module->settings_slug, 'message' => 'term-added' ), get_admin_url( null, 'admin.php' ) ); - wp_redirect( $redirect_url ); - exit; - } - - /** - * Handles a request to edit an editorial metadata - */ - function handle_edit_editorial_metadata() { - if ( !isset( $_POST['submit'], $_GET['page'], $_GET['action'], $_GET['term-id'] ) - || $_GET['page'] != $this->module->settings_slug || $_GET['action'] != 'edit-term' ) - return; - - if ( !wp_verify_nonce( $_POST['_wpnonce'], 'editorial-metadata-edit-nonce' ) ) - wp_die( $this->module->messages['nonce-failed'] ); - - if ( !current_user_can( 'manage_options' ) ) - wp_die( $this->module->messages['invalid-permissions'] ); - - if ( !$existing_term = $this->get_editorial_metadata_term_by( 'id', (int)$_GET['term-id'] ) ) - wp_die( $this->module->messages['term-missing'] ); - - $new_name = sanitize_text_field( trim( $_POST['name'] ) ); - $new_description = stripslashes( wp_filter_post_kses( strip_tags( trim( $_POST['description'] ) ) ) ); - /** - * Form validation for editing editorial metadata term + * Choose the post types for editorial metadata * - * Details - * - "name", "slug", and "type" are required fields - * - "description" can accept a limited amount of HTML, and is optional + * @since 0.7 */ - $_REQUEST['form-errors'] = array(); - // Check if name field was filled in - if( empty( $new_name ) ) - $_REQUEST['form-errors']['name'] = __( 'Please enter a name for the editorial metadata', 'edit-flow' ); - - // Check that the name isn't numeric - if ( is_numeric( $new_name ) ) - $_REQUEST['form-errors']['name'] = __( 'Please enter a valid, non-numeric name for the editorial metadata.', 'edit-flow' ); - - $term_exists = term_exists( sanitize_title( $new_name ) ); - if ( $term_exists && $term_exists != $existing_term->term_id ) - $_REQUEST['form-errors']['name'] = __( 'Metadata name conflicts with existing term. Please choose another.', 'edit-flow' ); - - // Check to ensure a term with the same name doesn't exist, - $search_term = $this->get_editorial_metadata_term_by( 'name', $new_name ); - if ( is_object( $search_term ) && $search_term->term_id != $existing_term->term_id ) - $_REQUEST['form-errors']['name'] = __( 'Name already in use. Please choose another.', 'edit-flow' ); - // or that the term name doesn't map to an existing term's slug - $search_term = $this->get_editorial_metadata_term_by( 'slug', sanitize_title( $new_name ) ); - if ( is_object( $search_term ) && $search_term->term_id != $existing_term->term_id ) - $_REQUEST['form-errors']['name'] = __( 'Name conflicts with slug for another term. Please choose something else.', 'edit-flow' ); - - // Check that the term name doesn't exceed 200 chars - if ( strlen( $new_name ) > 200 ) - $_REQUEST['form-errors']['name'] = __( 'Name cannot exceed 200 characters. Please try a shorter name.', 'edit-flow' ); - // Make sure the viewable state is valid - $new_viewable = false; - if ( $_POST['viewable'] == 'yes' ) - $new_viewable = true; - - // Kick out if there are any errors - if ( count( $_REQUEST['form-errors'] ) ) { - $_REQUEST['error'] = 'form-error'; - return; + public function settings_post_types_option() { + global $edit_flow; + $edit_flow->settings->helper_option_custom_post_type( $this->module ); } - - // Try to add the metadata term - $args = array( - 'name' => $new_name, - 'description' => $new_description, - 'viewable' => $new_viewable, - ); - $return = $this->update_editorial_metadata_term( $existing_term->term_id, $args ); - if ( is_wp_error( $return ) ) - wp_die( __( 'Error updating term.', 'edit-flow' ) ); - - $redirect_url = add_query_arg( array( 'page' => $this->module->settings_slug, 'message' => 'term-updated' ), get_admin_url( null, 'admin.php' ) ); - wp_redirect( $redirect_url ); - exit; - } - - /** - * Handle a $_GET request to change the visibility of an Editorial Metadata term - * - * @since 0.7 - */ - function handle_change_editorial_metadata_visibility() { - - // Check that the current GET request is our GET request - if ( !isset( $_GET['page'], $_GET['action'], $_GET['term-id'], $_GET['nonce'] ) - || $_GET['page'] != $this->module->settings_slug || !in_array( $_GET['action'], array( 'make-viewable', 'make-hidden' ) ) ) - return; - - // Check for proper nonce - if ( !wp_verify_nonce( $_GET['nonce'], 'make-viewable' ) && !wp_verify_nonce( $_GET['nonce'], 'make-hidden' ) ) - wp_die( $this->module->messages['nonce-failed'] ); - - // Only allow users with the proper caps - if ( !current_user_can( 'manage_options' ) ) - wp_die( $this->module->messages['invalid-permissions'] ); - - $term_id = (int)$_GET['term-id']; - $args = array(); - if ( $_GET['action'] == 'make-viewable' ) - $args['viewable'] = true; - elseif ( $_GET['action'] == 'make-hidden' ) - $args['viewable'] = false; - - $return = $this->update_editorial_metadata_term( $term_id, $args ); - if ( is_wp_error( $return ) ) - wp_die( __( 'Error updating term.', 'edit-flow' ) ); - - $redirect_url = $this->get_link( array( 'message' => 'term-visibility-changed' ) ); - wp_redirect( $redirect_url ); - exit; - - } - - /** - * Handle the request to update a given Editorial Metadata term via inline edit - * - * @since 0.7 - */ - function handle_ajax_inline_save_term() { - - if ( !wp_verify_nonce( $_POST['inline_edit'], 'editorial-metadata-inline-edit-nonce' ) ) - die( $this->module->messages['nonce-failed'] ); - - if ( !current_user_can( 'manage_options') ) - die( $this->module->messages['invalid-permissions'] ); - - $term_id = (int) $_POST['term_id']; - if ( !$existing_term = $this->get_editorial_metadata_term_by( 'id', $term_id ) ) - die( $this->module->messages['term-missing'] ); - - $metadata_name = sanitize_text_field( trim( $_POST['name'] ) ); - $metadata_description = stripslashes( wp_filter_post_kses( trim( $_POST['description'] ) ) ); - + /** - * Form validation for editing editorial metadata term - */ - // Check if name field was filled in - if ( empty( $metadata_name ) ) { - $change_error = new WP_Error( 'invalid', __( 'Please enter a name for the editorial metadata', 'edit-flow' ) ); - die( $change_error->get_error_message() ); - } + * Validate data entered by the user + * + * @since 0.7 + * + * @param array $new_options New values that have been entered by the user + * @return array $new_options Form values after they've been sanitized + */ + public function settings_validate( $new_options ) { - // Check that the name isn't numeric - if( is_numeric( $metadata_name) ) { - $change_error = new WP_Error( 'invalid', __( 'Please enter a valid, non-numeric name for the editorial metadata.', 'edit-flow' ) ); - die( $change_error->get_error_message() ); - } - - // Check that the term name doesn't exceed 200 chars - if ( strlen( $metadata_name ) > 200 ) { - $change_error = new WP_Error( 'invalid', __( 'Name cannot exceed 200 characters. Please try a shorter name.' ) ); - die( $change_error->get_error_message() ); - } - - // Check to make sure the status doesn't already exist as another term because otherwise we'd get a fatal error - $term_exists = term_exists( sanitize_title( $metadata_name ) ); - if ( $term_exists && $term_exists != $term_id ) { - $change_error = new WP_Error( 'invalid', __( 'Metadata name conflicts with existing term. Please choose another.', 'edit-flow' ) ); - die( $change_error->get_error_message() ); - } - - // Check to ensure a term with the same name doesn't exist, - $search_term = $this->get_editorial_metadata_term_by( 'name', $metadata_name ); - if ( is_object( $search_term ) && $search_term->term_id != $existing_term->term_id ) { - $change_error = new WP_Error( 'invalid', __( 'Name already in use. Please choose another.', 'edit-flow' ) ); - die( $change_error->get_error_message() ); - } + // Whitelist validation for the post type options + if ( ! isset( $new_options['post_types'] ) ) { + $new_options['post_types'] = array(); + } + $new_options['post_types'] = $this->clean_post_type_options( $new_options['post_types'], $this->module->post_type_support ); - // or that the term name doesn't map to an existing term's slug - $search_term = $this->get_editorial_metadata_term_by( 'slug', sanitize_title( $metadata_name ) ); - if ( is_object( $search_term ) && $search_term->term_id != $existing_term->term_id ) { - $change_error = new WP_Error( 'invalid', __( 'Name conflicts with slug for another term. Please choose again.', 'edit-flow' ) ); - die( $change_error->get_error_message() ); + return $new_options; } - - // Prepare the term name and description for saving - $args = array( - 'name' => $metadata_name, - 'description' => $metadata_description, - ); - $return = $this->update_editorial_metadata_term( $existing_term->term_id, $args ); - if( !is_wp_error( $return ) ) { - set_current_screen( 'edit-editorial-metadata' ); + + /** + * Prepare and display the configuration view for editorial metadata. + * There are four primary components: + * - Form to add a new Editorial Metadata term + * - Form generated by the settings API for managing Editorial Metadata options + * - Table of existing Editorial Metadata terms with ability to take actions on each + * - Full page width view for editing a single Editorial Metadata term + * + * 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. + * phpcs:disable:WordPress.Security.NonceVerification.Missing + * @since 0.7 + */ + public function print_configure_view() { + global $edit_flow; $wp_list_table = new EF_Editorial_Metadata_List_Table(); $wp_list_table->prepare_items(); - echo $wp_list_table->single_row( $return ); - die(); - } else { - $change_error = new WP_Error( 'invalid', sprintf( __( 'Could not update the term: %s', 'edit-flow' ), $status_name ) ); - die( $change_error->get_error_message() ); - } - - } - - /** - * Handle the ajax request to update all of the term positions - * - * @since 0.7 - */ - function handle_ajax_update_term_positions() { - - if ( !wp_verify_nonce( $_POST['editorial_metadata_sortable_nonce'], 'editorial-metadata-sortable' ) ) - $this->print_ajax_response( 'error', $this->module->messages['nonce-failed'] ); - - if ( !current_user_can( 'manage_options') ) - $this->print_ajax_response( 'error', $this->module->messages['invalid-permissions'] ); - - if ( !isset( $_POST['term_positions'] ) || !is_array( $_POST['term_positions'] ) ) - $this->print_ajax_response( 'error', __( 'Terms not set.', 'edit-flow' ) ); - - foreach ( $_POST['term_positions'] as $position => $term_id ) { - - // Have to add 1 to the position because the index started with zero - $args = array( - 'position' => (int)$position + 1, - ); - $return = $this->update_editorial_metadata_term( (int)$term_id, $args ); - // @todo check that this was a valid return - } - $this->print_ajax_response( 'success', $this->module->messages['term-position-updated'] ); - } - - /** - * Handles a request to delete an editorial metadata term - */ - function handle_delete_editorial_metadata() { - if ( !isset( $_GET['page'], $_GET['action'], $_GET['term-id'] ) - || $_GET['page'] != $this->module->settings_slug || $_GET['action'] != 'delete-term' ) - return; - - if ( !wp_verify_nonce( $_GET['nonce'], 'delete-term' ) ) - wp_die( $this->module->messages['nonce-failed'] ); - - if ( !current_user_can( 'manage_options' ) ) - wp_die( $this->module->messages['invalid-permissions'] ); - - if ( !$existing_term = $this->get_editorial_metadata_term_by( 'id', (int)$_GET['term-id'] ) ) - wp_die( $this->module->messages['term-missing'] ); - - $result = $this->delete_editorial_metadata_term( $existing_term->term_id ); - if ( !$result || is_wp_error( $result ) ) - wp_die( __( 'Error deleting term.', 'edit-flow' ) ); - - $redirect_url = add_query_arg( array( 'page' => $this->module->settings_slug, 'message' => 'term-deleted' ), get_admin_url( null, 'admin.php' ) ); - wp_redirect( $redirect_url ); - exit; - } - - /** - * Register settings for notifications so we can partially use the Settings API - * (We use the Settings API for form generation, but not saving) - * - * @since 0.7 - * @uses add_settings_section(), add_settings_field() - */ - function register_settings() { - add_settings_section( $this->module->options_group_name . '_general', false, '__return_false', $this->module->options_group_name ); - 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' ); - } - - /** - * Choose the post types for editorial metadata - * - * @since 0.7 - */ - function settings_post_types_option() { - global $edit_flow; - $edit_flow->settings->helper_option_custom_post_type( $this->module ); - } - - /** - * Validate data entered by the user - * - * @since 0.7 - * - * @param array $new_options New values that have been entered by the user - * @return array $new_options Form values after they've been sanitized - */ - function settings_validate( $new_options ) { - - // Whitelist validation for the post type options - if ( !isset( $new_options['post_types'] ) ) - $new_options['post_types'] = array(); - $new_options['post_types'] = $this->clean_post_type_options( $new_options['post_types'], $this->module->post_type_support ); - - return $new_options; - } - - /** - * Prepare and display the configuration view for editorial metadata. - * There are four primary components: - * - Form to add a new Editorial Metadata term - * - Form generated by the settings API for managing Editorial Metadata options - * - Table of existing Editorial Metadata terms with ability to take actions on each - * - Full page width view for editing a single Editorial Metadata term - * - * @since 0.7 - */ - function print_configure_view() { - global $edit_flow; - $wp_list_table = new EF_Editorial_Metadata_List_Table(); - $wp_list_table->prepare_items(); - ?> + ?> - +
    - display(); ?> - + display(); ?> +
    - inline_edit(); ?> - - - - - get_editorial_metadata_term_by( 'id', $term_id ); - if ( !$term ) { - echo '

    ' . $this->module->messages['term-missing'] . '

    '; - return; - } - $metadata_types = $this->get_supported_metadata_types(); - $type = $term->type; - $edit_term_link = $this->get_link( array( 'action' => 'edit-term', 'term-id' => $term->term_id ) ); - - $name = ( isset( $_POST['name'] ) ) ? stripslashes( $_POST['name'] ) : $term->name; - $description = ( isset( $_POST['description'] ) ) ? stripslashes( $_POST['description'] ) : $term->description; - if ( $term->viewable ) - $viewable = 'yes'; - else - $viewable = 'no'; - $viewable = ( isset( $_POST['viewable'] ) ) ? stripslashes( $_POST['viewable'] ) : $viewable; - ?> - + inline_edit(); ?> + + + + + get_editorial_metadata_term_by( 'id', $term_id ); + if ( ! $term ) { + echo '

    ' . esc_html( $this->module->messages['term-missing'] ) . '

    '; + return; + } + $metadata_types = $this->get_supported_metadata_types(); + $type = $term->type; + $edit_term_link = $this->get_link( array( + 'action' => 'edit-term', + 'term-id' => $term->term_id, + ) ); + + $name = ( isset( $_POST['name'] ) ) ? stripslashes( $_POST['name'] ) : $term->name; + $description = ( isset( $_POST['description'] ) ) ? stripslashes( $_POST['description'] ) : $term->description; + if ( $term->viewable ) { + $viewable = 'yes'; + } else { + $viewable = 'no'; + } + $viewable = ( isset( $_POST['viewable'] ) ) ? stripslashes( $_POST['viewable'] ) : $viewable; + ?> +
    - - + +
  • :
    '; + $html .= esc_html( date_i18n( 'l', strtotime( $date ) ) ); + $html .= ''; - $html .= esc_html( date_i18n('l', strtotime( $date ) ) ); - $html .= '
    @@ -1447,7 +1536,7 @@ function print_configure_view() {

    - + @@ -1470,57 +1559,72 @@ function print_configure_view() { 'no' => __( 'No', 'edit-flow' ), 'yes' => __( 'Yes', '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' ) ); ?> - +
    @@ -1458,7 +1547,7 @@ function print_configure_view() {
    - +

    - +

    - - - + + +
    -
    +
    - - - -
    - module->options_group_name ); ?> - module->options_group_name ); ?> - module->name ) . '" />'; ?> - + + + + module->options_group_name ); ?> + module->options_group_name ); ?> + module->name ) . '" />'; ?> +
    - - + +
    - + settings->helper_print_error_or_description( 'name', __( 'The name is for labeling the metadata field.', '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' ) ); ?>
    - + 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' ) ); ?>
    @@ -1532,7 +1636,7 @@ function print_configure_view() { ?> settings->helper_print_error_or_description( 'type', __( 'Indicate the type of editorial metadata.', 'edit-flow' ) ); ?> @@ -1545,57 +1649,60 @@ function print_configure_view() { 'yes' => __( 'Yes', 'edit-flow' ), ); $current_metadata_viewable = ( isset( $_POST['metadata_viewable'] ) && in_array( $_POST['metadata_viewable'], array_keys( $metadata_viewable_options ) ) ) ? $_POST['metadata_viewable'] : 'no'; - ?> + ?> 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' ) ); ?>
    - + -

    +

    - - taxonomy = EF_Editorial_Metadata::metadata_taxonomy; - + $this->tax = get_taxonomy( $this->taxonomy ); - + $columns = $this->get_columns(); $hidden = array( 'position', ); $sortable = array(); - - $this->_column_headers = array( $columns, $hidden, $sortable ); + + $this->_column_headers = array( $columns, $hidden, $sortable ); parent::__construct( array( 'plural' => 'editorial metadata', @@ -1608,7 +1715,7 @@ function __construct() { * * @since 0.7 */ - function prepare_items() { + public function prepare_items() { global $edit_flow; $this->items = $edit_flow->editorial_metadata->get_editorial_metadata_terms(); @@ -1623,27 +1730,27 @@ function prepare_items() { * * @since 0.7 */ - function no_items() { + public function no_items() { _e( 'No editorial metadata found.', 'edit-flow' ); } - + /** * Register the columns to appear in the table * * @since 0.7 */ - function get_columns() { - + public function get_columns() { + $columns = array( - 'position' => __( 'Position', 'edit-flow' ), + 'position' => __( 'Position', 'edit-flow' ), 'name' => __( 'Name', 'edit-flow' ), - 'type' => __( 'Metadata Type', 'edit-flow' ), + 'type' => __( 'Metadata Type', 'edit-flow' ), 'description' => __( 'Description', 'edit-flow' ), 'viewable' => __( 'Viewable', 'edit-flow' ), - ); + ); return $columns; } - + /** * Prepare a single row of Editorial Metadata * @@ -1652,16 +1759,16 @@ function get_columns() { * @param object $term The current term we're displaying * @param int $level Level is always zero because it isn't a parent-child tax */ - function single_row( $term, $level = 0 ) { + public function single_row( $term, $level = 0 ) { static $alternate_class = ''; - $alternate_class = ( $alternate_class == '' ? ' alternate' : '' ); + $alternate_class = ( '' == $alternate_class ? ' alternate' : '' ); $row_class = ' class="term-static' . $alternate_class . '"'; - echo ''; - echo $this->single_row_columns( $term ); - echo ''; + echo wp_kses_post( '' ); + echo wp_kses_post( $this->single_row_columns( $term ) ); + echo ''; } - + /** * Handle the column output when there's no method for it * @@ -1670,24 +1777,24 @@ function single_row( $term, $level = 0 ) { * @param object $item Editorial Metadata term as an object * @param string $column_name How the column was registered at birth */ - function column_default( $item, $column_name ) { - - switch( $column_name ) { + public function column_default( $item, $column_name ) { + + switch ( $column_name ) { case 'position': case 'type': case 'description': return esc_html( $item->$column_name ); break; case 'viewable': - if ( $item->viewable ) + if ( $item->viewable ) { return __( 'Yes', 'edit-flow' ); - else + } else { return __( 'No', 'edit-flow' ); + } break; default: break; } - } /** @@ -1697,28 +1804,41 @@ function column_default( $item, $column_name ) { * * @param object $item Editorial Metadata term as an object */ - function column_name( $item ) { + public function column_name( $item ) { global $edit_flow; - $item_edit_link = esc_url( $edit_flow->editorial_metadata->get_link( array( 'action' => 'edit-term', 'term-id' => $item->term_id ) ) ); - $item_delete_link = esc_url( $edit_flow->editorial_metadata->get_link( array( 'action' => 'delete-term', 'term-id' => $item->term_id ) ) ); - + $item_edit_link = esc_url( $edit_flow->editorial_metadata->get_link( array( + 'action' => 'edit-term', + 'term-id' => $item->term_id, + ) ) ); + $item_delete_link = esc_url( $edit_flow->editorial_metadata->get_link( array( + 'action' => 'delete-term', + 'term-id' => $item->term_id, + ) ) ); + $out = '' . esc_html( $item->name ) . ''; - + $actions = array(); - $actions['edit'] = "" . __( 'Edit', 'edit-flow' ) . ""; + $actions['edit'] = "" . __( 'Edit', 'edit-flow' ) . ''; $actions['inline hide-if-no-js'] = '' . __( 'Quick Edit' ) . ''; - if ( $item->viewable ) - $actions['change-visibility make-hidden'] = '' . __( 'Make Hidden', 'edit-flow' ) . ''; - else - $actions['change-visibility make-viewable'] = '' . __( 'Make Viewable', 'edit-flow' ) . ''; - $actions['delete delete-status'] = "" . __( 'Delete', 'edit-flow' ) . ""; - + if ( $item->viewable ) { + $actions['change-visibility make-hidden'] = '' . __( 'Make Hidden', 'edit-flow' ) . ''; + } else { + $actions['change-visibility make-viewable'] = '' . __( 'Make Viewable', 'edit-flow' ) . ''; + } + $actions['delete delete-status'] = "" . __( 'Delete', 'edit-flow' ) . ''; + $out .= $this->row_actions( $actions, false ); $out .= ''; - + return $out; } @@ -1727,11 +1847,11 @@ function column_name( $item ) { * * @since 0.7 */ - function inline_edit() { + public function inline_edit() { -?> + ?>
    -
    - ID ) ); $body .= "\r\n"; /* translators: 1: date */ - $body .= sprintf( __( 'This email was sent %s.', 'edit-flow' ), gmdate( 'r' ) ); + $body .= sprintf( __( 'This email was sent %s.', 'edit-flow' ), date( 'r' ) ); $body .= "\r\n \r\n"; $body .= get_option( 'blogname' ) . ' | ' . get_bloginfo( 'url' ) . ' | ' . admin_url( '/' ) . "\r\n"; return $body; diff --git a/modules/settings/settings.php b/modules/settings/settings.php index cd3e22bf6..784c3e93b 100644 --- a/modules/settings/settings.php +++ b/modules/settings/settings.php @@ -23,7 +23,7 @@ public function __construct() { 'default_options' => array( 'enabled' => 'on', 'vip_features' => $this->is_vip_site() ? 'on' : 'off', - 'analytics' => $this->is_vip_site() ? 'on' : 'off', + 'analytics' => $this->is_vip_site( true ) ? 'on' : 'off', ), 'configure_page_cb' => 'print_default_settings', 'autoload' => true, @@ -210,7 +210,7 @@ public function print_default_header( $current_module ) {

    title ); ?>

    @@ -218,17 +218,17 @@ public function print_default_header( $current_module ) {

    short_description ) : ?> -

    short_description, 'a' ); ?>

    +

    short_description ); ?>

    extended_description ) : ?> -

    extended_description, 'a' ); ?>

    +

    extended_description ); ?>

    module_url = $this->get_module_url( __FILE__ ); // Register the module with Edit Flow $args = array( 'title' => __( 'Story Budget', 'edit-flow' ), + // translators: %s is a link to the story budget page 'short_description' => sprintf( __( 'View the status of all your content at a glance.', 'edit-flow' ), admin_url( 'index.php?page=story-budget' ) ), 'extended_description' => __( 'Use the story budget to see how content on your site is progressing. Filter by specific categories or date ranges to see details about each post in progress.', 'edit-flow' ), 'module_url' => $this->module_url, @@ -50,17 +54,17 @@ function __construct() { 'autoload' => false, ); $this->module = EditFlow()->register_module( 'story_budget', $args ); - } /** * Initialize the rest of the stuff in the class if the module is active */ - function init() { + public function init() { $view_story_budget_cap = apply_filters( 'ef_view_story_budget_cap', 'ef_view_story_budget' ); - if ( !current_user_can( $view_story_budget_cap ) ) + if ( ! current_user_can( $view_story_budget_cap ) ) { return; + } $this->num_columns = $this->get_num_columns(); $this->max_num_columns = apply_filters( 'ef_story_budget_max_num_columns', 3 ); @@ -78,7 +82,6 @@ function init() { // Load necessary scripts and stylesheets add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'action_enqueue_admin_styles' ) ); - } /** @@ -86,15 +89,15 @@ function init() { * * @since 0.7 */ - function install() { + public function install() { $story_budget_roles = array( 'administrator' => array( 'ef_view_story_budget' ), - 'editor' => array( 'ef_view_story_budget' ), - 'author' => array( 'ef_view_story_budget' ), - 'contributor' => array( 'ef_view_story_budget' ) + 'editor' => array( 'ef_view_story_budget' ), + 'author' => array( 'ef_view_story_budget' ), + 'contributor' => array( 'ef_view_story_budget' ), ); - foreach( $story_budget_roles as $role => $caps ) { + foreach ( $story_budget_roles as $role => $caps ) { $this->add_caps_to_role( $role, $caps ); } } @@ -104,23 +107,23 @@ function install() { * * @since 0.7 */ - function upgrade( $previous_version ) { + public function upgrade( $previous_version ) { global $edit_flow; // Upgrade path to v0.7 - if ( version_compare( $previous_version, '0.7' , '<' ) ) { + if ( version_compare( $previous_version, '0.7', '<' ) ) { // Migrate whether the story budget was enabled or not and clean up old option - if ( $enabled = get_option( 'edit_flow_story_budget_enabled' ) ) + if ( $enabled = get_option( 'edit_flow_story_budget_enabled' ) ) { $enabled = 'on'; - else + } else { $enabled = 'off'; + } $edit_flow->update_module_option( $this->module->name, 'enabled', $enabled ); delete_option( 'edit_flow_story_budget_enabled' ); // Technically we've run this code before so we don't want to auto-install new data $edit_flow->update_module_option( $this->module->name, 'loaded_once', true ); } - } /** @@ -128,8 +131,8 @@ function upgrade( $previous_version ) { * * @uses add_submenu_page() */ - function action_admin_menu() { - add_submenu_page( 'index.php', __('Story Budget', 'edit-flow'), __('Story Budget', 'edit-flow'), apply_filters( 'ef_view_story_budget_cap', 'ef_view_story_budget' ), $this->module->slug, array( $this, 'story_budget') ); + public function action_admin_menu() { + add_submenu_page( 'index.php', __( 'Story Budget', 'edit-flow' ), __( 'Story Budget', 'edit-flow' ), apply_filters( 'ef_view_story_budget_cap', 'ef_view_story_budget' ), $this->module->slug, array( $this, 'story_budget' ) ); } /** @@ -137,11 +140,12 @@ function action_admin_menu() { * * @uses enqueue_admin_script() */ - function enqueue_admin_scripts() { + public function enqueue_admin_scripts() { global $current_screen; - if ( $current_screen->id != self::screen_id ) + if ( self::screen_id != $current_screen->id ) { return; + } $num_columns = $this->get_num_columns(); echo ''; @@ -153,11 +157,12 @@ function enqueue_admin_scripts() { /** * Enqueue a screen and print stylesheet for the story budget. */ - function action_enqueue_admin_styles() { + public function action_enqueue_admin_styles() { global $current_screen; - if ( $current_screen->id != self::screen_id ) + if ( self::screen_id != $current_screen->id ) { return; + } wp_enqueue_style( 'edit_flow-story_budget-styles', $this->module_url . 'lib/story-budget.css', false, EDIT_FLOW_VERSION, 'screen' ); wp_enqueue_style( 'edit_flow-story_budget-print-styles', $this->module_url . 'lib/story-budget-print.css', false, EDIT_FLOW_VERSION, 'print' ); @@ -169,7 +174,7 @@ function action_enqueue_admin_styles() { * * @since 0.7 */ - function register_term_columns() { + public function register_term_columns() { $term_columns = array( 'title' => __( 'Title', 'edit-flow' ), @@ -188,17 +193,18 @@ function register_term_columns() { * * @since 0.7 */ - function handle_form_date_range_change() { + public function handle_form_date_range_change() { if ( ! isset( $_POST['ef-story-budget-range-submit'], $_POST['ef-story-budget-number-days'], $_POST['ef-story-budget-start-date_hidden'] ) ) { return; } - if ( !wp_verify_nonce( $_POST['nonce'], 'change-date' ) ) - wp_die( $this->module->messages['nonce-failed'] ); + if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'change-date' ) ) { + wp_die( esc_html( $this->module->messages['nonce-failed'] ) ); + } $current_user = wp_get_current_user(); - $new_filters = array ( + $new_filters = array( 'start_date' => $_POST['ef-story-budget-start-date_hidden'], 'number_days' => (int) $_POST['ef-story-budget-number-days'], ); @@ -245,7 +251,7 @@ public function update_user_filters_from_form_date_range_change( $current_user, /** * Get the number of columns to show on the story budget */ - function get_num_columns() { + public function get_num_columns() { if ( empty( $this->num_columns ) ) { $current_user = wp_get_current_user(); @@ -264,18 +270,18 @@ function get_num_columns() { * * @since 0.8.3 */ - function add_screen_options_panel() { - require_once( EDIT_FLOW_ROOT . '/common/php/' . 'screen-options.php' ); + public function add_screen_options_panel() { + require_once EDIT_FLOW_ROOT . '/common/php/screen-options.php'; add_screen_options_panel( self::usermeta_key_prefix . 'screen_columns', __( 'Screen Layout', 'edit-flow' ), array( $this, 'print_column_prefs' ), self::screen_id, array( $this, 'save_column_prefs' ), true ); } /** * Print column number preferences for screen options */ - function print_column_prefs() { + public function print_column_prefs() { $return_val = __( 'Number of Columns: ', 'edit-flow' ); for ( $i = 1; $i <= $this->max_num_columns; ++$i ) { - $return_val .= "\n"; + $return_val .= "\n"; } return $return_val; } @@ -283,7 +289,7 @@ function print_column_prefs() { /** * Save the current user's preference for number of columns. */ - function save_column_prefs( $posted_fields ) { + public function save_column_prefs( $posted_fields ) { $key = self::usermeta_key_prefix . 'screen_columns'; $this->num_columns = (int) $posted_fields[ $key ]; @@ -297,23 +303,23 @@ function save_column_prefs( $posted_fields ) { * ouput any messages, create the table navigation, then print the columns based on * get_num_columns(), which will in turn print the stories themselves. */ - function story_budget() { + public function story_budget() { // Update the current user's filters with the variables set in $_GET $this->user_filters = $this->update_user_filters(); - if ( !empty( $this->user_filters[$this->taxonomy_used] ) ) { + if ( ! empty( $this->user_filters[ $this->taxonomy_used ] ) ) { $terms = array(); - $terms[] = get_term( $this->user_filters[$this->taxonomy_used], $this->taxonomy_used ); + $terms[] = get_term( $this->user_filters[ $this->taxonomy_used ], $this->taxonomy_used ); } else { // Get all of the terms from the taxonomy, regardless whether there are published posts - $args = array( + $terms = get_terms( array( + 'taxonomy' => $this->taxonomy_used, 'orderby' => 'name', 'order' => 'asc', 'hide_empty' => 0, 'parent' => 0, - ); - $terms = get_terms( $this->taxonomy_used, $args ); + )); } $this->terms = apply_filters( 'ef_story_budget_filter_terms', $terms ); // allow for reordering or any other filtering of terms @@ -328,22 +334,22 @@ function story_budget() {
    num_columns ) . '">'; - foreach( (array) $this->terms as $term ) { - $this->print_term( $term ); - } + foreach ( (array) $this->terms as $term ) { + $this->print_term( $term ); + } echo '
    '; ?>
    @@ -355,9 +361,9 @@ function story_budget() { * * @since 0.7 */ - function story_budget_time_range() { + public function story_budget_time_range() { ?> -
    + user_filters['start_date'] ) ) ); ?> @@ -382,7 +388,7 @@ function story_budget_time_range() { * @param object $term The term we're getting posts for * @return array $term_posts An array of post objects for the term */ - function get_posts_for_term( $term, $args = null ) { + public function get_posts_for_term( $term, $args = null ) { $defaults = array( 'post_status' => null, @@ -395,7 +401,7 @@ function get_posts_for_term( $term, $args = null ) { $arg_terms = array( $term->term_id, ); - $arg_terms = array_merge( $arg_terms, get_term_children( $term->term_id, $this->taxonomy_used ) ) ; + $arg_terms = array_merge( $arg_terms, get_term_children( $term->term_id, $this->taxonomy_used ) ); $args['tax_query'] = array( array( 'taxonomy' => $this->taxonomy_used, @@ -418,15 +424,17 @@ function get_posts_for_term( $term, $args = null ) { } // Filter by post_author if it's set - if ( $args['author'] === '0' ) unset( $args['author'] ); + if ( '0' === $args['author'] ) { + unset( $args['author'] ); + } $beginning_date = strtotime( $this->user_filters['start_date'] ); $days_to_show = $this->user_filters['number_days']; $ending_date = $beginning_date + ( $days_to_show * DAY_IN_SECONDS ); $args['date_query'] = array( - 'after' => date( "Y-m-d", $beginning_date ), - 'before' => date( "Y-m-d", $ending_date ), + 'after' => date( 'Y-m-d', $beginning_date ), + 'before' => date( 'Y-m-d', $ending_date ), 'inclusive' => true, ); @@ -451,23 +459,29 @@ function get_posts_for_term( $term, $args = null ) { * * @param object $term The term to print. */ - function print_term( $term ) { + public function print_term( $term ) { global $wpdb; $posts = $this->get_posts_for_term( $term, $this->user_filters ); - if ( !empty( $posts ) ) + if ( ! empty( $posts ) ) { // Don't display the message for $no_matching_posts $this->no_matching_posts = false; + } - ?> -
    + ?> +

    name ); ?>

    - + - term_columns as $key => $name ): ?> + term_columns as $key => $name ) : ?> @@ -475,17 +489,18 @@ function print_term( $term ) { print_post( $post, $term ); + foreach ( $posts as $post ) { + $this->print_post( $post, $term ); + } ?>
    - +

    - - term_columns as $key => $name ) { + term_columns as $key => $name ) { echo ''; if ( method_exists( $this, 'term_column_' . $key ) ) { $method = 'term_column_' . $key; - echo $this->$method( $post, $parent_term ); + echo wp_kses_post( $this->$method( $post, $parent_term ) ); } else { - echo $this->term_column_default( $post, $key, $parent_term ); + echo wp_kses_post( $this->term_column_default( $post, $key, $parent_term ) ); } echo ''; - } ?> + } + ?> post_status ); return $status_name->label; @@ -545,12 +563,12 @@ function term_column_default( $post, $column_name, $parent_term ) { return $output; break; case 'post_modified': + // translators: %s is a human-readable time difference return sprintf( esc_html__( '%s ago', 'edit-flow' ), human_time_diff( get_the_time( 'U', $post->ID ), current_time( 'timestamp' ) ) ); break; default: break; } - } /** @@ -558,29 +576,35 @@ function term_column_default( $post, $column_name, $parent_term ) { * * @since 0.7 */ - function term_column_title( $post, $parent_term ) { + public function term_column_title( $post, $parent_term ) { $post_title = _draft_or_post_title( $post->ID ); $post_type_object = get_post_type_object( $post->post_type ); $can_edit_post = current_user_can( $post_type_object->cap->edit_post, $post->ID ); - if ( $can_edit_post ) + if ( $can_edit_post ) { $output = '' . esc_html( $post_title ) . ''; - else + } else { $output = '' . esc_html( $post_title ) . ''; + } // Edit or Trash or View $output .= '
    '; $item_actions = array(); - if ( $can_edit_post ) + if ( $can_edit_post ) { $item_actions['edit'] = '' . __( 'Edit', 'edit-flow' ) . ''; - if ( EMPTY_TRASH_DAYS > 0 && current_user_can( $post_type_object->cap->delete_post, $post->ID ) ) + } + if ( EMPTY_TRASH_DAYS > 0 && current_user_can( $post_type_object->cap->delete_post, $post->ID ) ) { $item_actions['trash'] = '' . __( 'Trash', 'edit-flow' ) . ''; + } // Display a View or a Preview link depending on whether the post has been published or not - if ( in_array( $post->post_status, array( 'publish' ) ) ) + if ( in_array( $post->post_status, array( 'publish' ) ) ) { + // translators: %s is the post title $item_actions['view'] = '' . __( 'View', 'edit-flow' ) . ''; - else if ( $can_edit_post ) + } else if ( $can_edit_post ) { + // translators: %s is the post title $item_actions['previewpost'] = '' . __( 'Preview', 'edit-flow' ) . ''; + } $item_actions = apply_filters( 'ef_story_budget_item_actions', $item_actions, $post->ID ); if ( count( $item_actions ) ) { @@ -599,26 +623,28 @@ function term_column_title( $post, $parent_term ) { /** * Print any messages that should appear based on the action performed */ - function print_messages() { - ?> + public function print_messages() { + ?> -

    '; // Following mostly stolen from edit.php if ( isset( $_GET['trashed'] ) && (int) $_GET['trashed'] ) { - printf( _n( 'Item moved to the trash.', '%d items moved to the trash.', $_GET['trashed'] ), number_format_i18n( $_GET['trashed'] ) ); - $ids = isset($_GET['ids']) ? $_GET['ids'] : 0; - echo ' ' . __( 'Undo', 'edit-flow' ) . '
    '; - unset($_GET['trashed']); + // translators: %d is the number of posts trashed + printf( esc_html( _n( 'Item moved to the trash.', '%d items moved to the trash.', $_GET['trashed'] ), number_format_i18n( $_GET['trashed'] ) ) ); + $ids = isset( $_GET['ids'] ) ? $_GET['ids'] : 0; + echo ' ' . esc_html__( 'Undo', 'edit-flow' ) . '
    '; + unset( $_GET['trashed'] ); } - if ( isset($_GET['untrashed'] ) && (int) $_GET['untrashed'] ) { - printf( _n( 'Item restored from the Trash.', '%d items restored from the Trash.', $_GET['untrashed'] ), number_format_i18n( $_GET['untrashed'] ) ); - unset($_GET['undeleted']); + if ( isset( $_GET['untrashed'] ) && (int) $_GET['untrashed'] ) { + // translators: %d is the number of posts restored from the trash + printf( esc_html( _n( 'Item restored from the Trash.', '%d items restored from the Trash.', $_GET['untrashed'] ), number_format_i18n( $_GET['untrashed'] ) ) ); + unset( $_GET['undeleted'] ); } echo '

    '; @@ -628,16 +654,16 @@ function print_messages() { /** * Print the table navigation and filter controls, using the current user's filters if any are set. */ - function table_navigation() { - ?> + public function table_navigation() { + ?>
    story_budget_filters() as $select_id => $select_name ) { - echo $this->story_budget_filter_options( $select_id, $select_name, $this->user_filters ); - } + foreach ( $this->story_budget_filters() as $select_id => $select_name ) { + echo wp_kses_post( $this->story_budget_filter_options( $select_id, $select_name, $this->user_filters ) ); + } ?> @@ -647,8 +673,8 @@ function table_navigation() { story_budget_filters() as $select_id => $select_name ) { - echo ''; + foreach ( $this->story_budget_filters() as $select_id => $select_name ) { + echo ''; } ?> @@ -661,23 +687,23 @@ function table_navigation() {
    - $this->filter_get_param( 'post_status' ), - 'cat' => $this->filter_get_param( 'cat' ), - 'author' => $this->filter_get_param( 'author' ), - 'start_date' => $this->filter_get_param( 'start_date' ), - 'number_days' => $this->filter_get_param( 'number_days' ) + 'post_status' => $this->filter_get_param( 'post_status' ), + 'cat' => $this->filter_get_param( 'cat' ), + 'author' => $this->filter_get_param( 'author' ), + 'start_date' => $this->filter_get_param( 'start_date' ), + 'number_days' => $this->filter_get_param( 'number_days' ), ); $current_user_filters = array(); @@ -685,18 +711,20 @@ function update_user_filters() { // If any of the $_GET vars are missing, then use the current user filter foreach ( $user_filters as $key => $value ) { - if ( is_null( $value ) && !empty( $current_user_filters[$key] ) ) { - $user_filters[$key] = $current_user_filters[$key]; + if ( is_null( $value ) && ! empty( $current_user_filters[ $key ] ) ) { + $user_filters[ $key ] = $current_user_filters[ $key ]; } } - if ( !$user_filters['start_date'] ) + if ( ! $user_filters['start_date'] ) { $user_filters['start_date'] = date( 'Y-m-d' ); + } - if ( !$user_filters['number_days'] ) + if ( ! $user_filters['number_days'] ) { $user_filters['number_days'] = 10; + } - $user_filters = apply_filters('ef_story_budget_filter_values', $user_filters, $current_user_filters); + $user_filters = apply_filters( 'ef_story_budget_filter_values', $user_filters, $current_user_filters ); $this->update_user_meta( $current_user->ID, self::usermeta_key_prefix . 'filters', $user_filters ); return $user_filters; @@ -708,15 +736,16 @@ function update_user_filters() { * * @return array The filters for the current user, or the default filters if the current user has none. */ - function get_user_filters() { + public function get_user_filters() { $current_user = wp_get_current_user(); $user_filters = array(); $user_filters = $this->get_user_meta( $current_user->ID, self::usermeta_key_prefix . 'filters', true ); // If usermeta didn't have filters already, insert defaults into DB - if ( empty( $user_filters ) ) + if ( empty( $user_filters ) ) { $user_filters = $this->update_user_filters(); + } return $user_filters; } @@ -724,72 +753,72 @@ function get_user_filters() { * * @param string $param The parameter to look for in $_GET * @return null if the parameter is not set in $_GET, empty string if the parameter is empty in $_GET, - * or a sanitized version of the parameter from $_GET if set and not empty + * or a sanitized version of the parameter from $_GET if set and not empty */ - function filter_get_param( $param ) { + public function filter_get_param( $param ) { // Sure, this could be done in one line. But we're cooler than that: let's make it more readable! - if ( !isset( $_GET[$param] ) ) { + if ( ! isset( $_GET[ $param ] ) ) { return null; - } else if ( empty( $_GET[$param] ) ) { + } else if ( empty( $_GET[ $param ] ) ) { return ''; } - return sanitize_key( $_GET[$param] ); + return sanitize_key( $_GET[ $param ] ); } - function story_budget_filters() { + public function story_budget_filters() { $select_filter_names = array(); $select_filter_names['post_status'] = 'post_status'; $select_filter_names['cat'] = 'cat'; $select_filter_names['author'] = 'author'; - return apply_filters('ef_story_budget_filter_names', $select_filter_names); + return apply_filters( 'ef_story_budget_filter_names', $select_filter_names ); } - function story_budget_filter_options( $select_id, $select_name, $filters ) { - switch( $select_id ) { + public function story_budget_filter_options( $select_id, $select_name, $filters ) { + switch ( $select_id ) { case 'post_status': - $post_stati = $this->get_budget_post_stati(); - ?> + $post_stati = $this->get_budget_post_stati(); + ?> - __( 'View all categories', 'edit-flow' ), 'hide_empty' => 0, 'hierarchical' => 1, 'show_count' => 0, 'orderby' => 'name', - 'selected' => $this->user_filters['cat'] - ); + 'selected' => $this->user_filters['cat'], + ); wp_dropdown_categories( $category_dropdown_args ); } - break; + break; case 'author': $users_dropdown_args = array( - 'show_option_all' => __( 'View all users', 'edit-flow' ), - 'name' => 'author', - 'selected' => $this->user_filters['author'], - 'who' => 'authors', - ); + 'show_option_all' => __( 'View all users', 'edit-flow' ), + 'name' => 'author', + 'selected' => $this->user_filters['author'], + 'who' => 'authors', + ); $users_dropdown_args = apply_filters( 'ef_story_budget_users_dropdown_args', $users_dropdown_args ); wp_dropdown_users( $users_dropdown_args ); - break; + break; default: - do_action( 'ef_story_budget_filter_display', $select_id, $select_name, $filters); - break; + do_action( 'ef_story_budget_filter_display', $select_id, $select_name, $filters ); + break; } } @@ -808,13 +837,12 @@ public function get_budget_post_stati() { $final_statuses = []; - foreach( $post_stati as $status ) { - if ( !empty( $custom_status_slug_keys[ $status->name ] ) ) { + foreach ( $post_stati as $status ) { + if ( ! empty( $custom_status_slug_keys[ $status->name ] ) ) { $final_statuses[] = $status; } } return apply_filters( 'ef_budget_post_stati', $final_statuses ); } - } diff --git a/modules/user-groups/user-groups.php b/modules/user-groups/user-groups.php index 1c81ddda4..fe4e33c97 100644 --- a/modules/user-groups/user-groups.php +++ b/modules/user-groups/user-groups.php @@ -1,7 +1,7 @@ module_url = $this->get_module_url( __FILE__ ); - - // Register the User Groups module with Edit Flow - $args = array( - 'title' => __( 'User Groups', 'edit-flow' ), - 'short_description' => __( 'Organize your users into groups to mimic your organizational structure.', 'edit-flow' ), - 'extended_description' => __( 'Configure user groups to organize all of the users on your site. Each user can be in many user groups and you can change them at any time.', 'edit-flow' ), - 'module_url' => $this->module_url, - 'img_url' => $this->module_url . 'lib/usergroups_s128.png', - 'slug' => 'user-groups', - 'default_options' => array( - 'enabled' => 'on', - 'post_types' => array( - 'post' => 'on', - 'page' => 'off', + /** + * Keys for storing data + * - taxonomy_key - used for custom taxonomy + * - term_prefix - Used for custom taxonomy terms + */ + // phpcs:ignore Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase + const taxonomy_key = 'ef_usergroup'; + // phpcs:ignore Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase + const term_prefix = 'ef-usergroup-'; + + public $manage_usergroups_cap = 'edit_usergroups'; + + /** + * Register the module with Edit Flow but don't do anything else + * + * @since 0.7 + */ + public function __construct() { + + $this->module_url = $this->get_module_url( __FILE__ ); + + // Register the User Groups module with Edit Flow + $args = array( + 'title' => __( 'User Groups', 'edit-flow' ), + 'short_description' => __( 'Organize your users into groups to mimic your organizational structure.', 'edit-flow' ), + 'extended_description' => __( 'Configure user groups to organize all of the users on your site. Each user can be in many user groups and you can change them at any time.', 'edit-flow' ), + 'module_url' => $this->module_url, + 'img_url' => $this->module_url . 'lib/usergroups_s128.png', + 'slug' => 'user-groups', + 'default_options' => array( + 'enabled' => 'on', + 'post_types' => array( + 'post' => 'on', + 'page' => 'off', + ), ), - ), - 'messages' => array( - 'usergroup-added' => __( "User group created. Feel free to add users to the usergroup.", 'edit-flow' ), - 'usergroup-updated' => __( "User group updated.", 'edit-flow' ), - 'usergroup-missing' => __( "User group doesn't exist.", 'edit-flow' ), - 'usergroup-deleted' => __( "User group deleted.", 'edit-flow' ), - ), - 'configure_page_cb' => 'print_configure_view', - 'configure_link_text' => __( 'Manage User Groups', 'edit-flow' ), - 'autoload' => false, - 'settings_help_tab' => array( - 'id' => 'ef-user-groups-overview', - 'title' => __('Overview', 'edit-flow'), - 'content' => __('

    For those with many people involved in the publishing process, user groups helps you keep them organized.

    Currently, user groups are primarily used for subscribing a set of users to a post for notifications.

    ', 'edit-flow'), + 'messages' => array( + 'usergroup-added' => __( 'User group created. Feel free to add users to the usergroup.', 'edit-flow' ), + 'usergroup-updated' => __( 'User group updated.', 'edit-flow' ), + 'usergroup-missing' => __( "User group doesn't exist.", 'edit-flow' ), + 'usergroup-deleted' => __( 'User group deleted.', 'edit-flow' ), ), - 'settings_help_sidebar' => __( '

    For more information:

    User Groups Documentation

    Edit Flow Forum

    Edit Flow on Github

    ', 'edit-flow' ), - ); - $this->module = EditFlow()->register_module( 'user_groups', $args ); - - } - - /** - * Module startup - */ - - /** - * Initialize the rest of the stuff in the class if the module is active - * - * @since 0.7 - */ - function init() { - - // Register the objects where we'll be storing data and relationships - $this->register_usergroup_objects(); - - $this->manage_usergroups_cap = apply_filters( 'ef_manage_usergroups_cap', $this->manage_usergroups_cap ); - - // Register our settings - add_action( 'admin_init', array( $this, 'register_settings' ) ); - - // Handle any adding, editing or saving - add_action( 'admin_init', array( $this, 'handle_add_usergroup' ) ); - add_action( 'admin_init', array( $this, 'handle_edit_usergroup' ) ); - add_action( 'admin_init', array( $this, 'handle_delete_usergroup' ) ); - add_action( 'wp_ajax_inline_save_usergroup', array( $this, 'handle_ajax_inline_save_usergroup' ) ); - - // Usergroups can be managed from the User profile view - add_action( 'show_user_profile', array( $this, 'user_profile_page' ) ); - add_action( 'edit_user_profile', array( $this, 'user_profile_page' ) ); - add_action( 'user_profile_update_errors', array( $this, 'user_profile_update' ), 10, 3 ); - - // Javascript and CSS if we need it - add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) ); - add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_styles' ) ); - - } - - /** - * Load the capabilities onto users the first time the module is run - * - * @since 0.7 - */ - function install() { - - // Add necessary capabilities to allow management of user groups - $usergroup_roles = array( - 'administrator' => array('edit_usergroups'), - ); - foreach( $usergroup_roles as $role => $caps ) { - $this->add_caps_to_role( $role, $caps ); - } - - // Create our default usergroups - $default_usergroups = array( - array( - 'name' => __( 'Copy Editors', 'edit-flow' ), - 'description' => __( 'Making sure the quality is top-notch.', 'edit-flow' ), - ), - array( - 'name' => __( 'Photographers', 'edit-flow' ), - 'description' => __( 'Capturing the story visually.', 'edit-flow' ), - ), - array( - 'name' => __( 'Reporters', 'edit-flow' ), - 'description' => __( 'Out in the field, writing stories.', 'edit-flow' ), - ), - array( - 'name' => __( 'Section Editors', 'edit-flow' ), - 'description' => __( 'Providing feedback and direction.', 'edit-flow' ), - ), - ); - foreach( $default_usergroups as $args ) { - $this->add_usergroup( $args ); + 'configure_page_cb' => 'print_configure_view', + 'configure_link_text' => __( 'Manage User Groups', 'edit-flow' ), + 'autoload' => false, + 'settings_help_tab' => array( + 'id' => 'ef-user-groups-overview', + 'title' => __( 'Overview', 'edit-flow' ), + 'content' => __( '

    For those with many people involved in the publishing process, user groups helps you keep them organized.

    Currently, user groups are primarily used for subscribing a set of users to a post for notifications.

    ', 'edit-flow' ), + ), + 'settings_help_sidebar' => __( '

    For more information:

    User Groups Documentation

    Edit Flow Forum

    Edit Flow on Github

    ', 'edit-flow' ), + ); + $this->module = EditFlow()->register_module( 'user_groups', $args ); } - - } - /** - * Upgrade our data in case we need to - * - * @since 0.7 - */ - function upgrade( $previous_version ) { - global $edit_flow; + /** + * Module startup + */ - // Upgrade path to v0.7 - if ( version_compare( $previous_version, '0.7' , '<' ) ) { - global $wpdb; + /** + * Initialize the rest of the stuff in the class if the module is active + * + * @since 0.7 + */ + public function init() { - // Set all of the user group terms to our new taxonomy - $wpdb->update( $wpdb->term_taxonomy, array( 'taxonomy' => self::taxonomy_key ), array( 'taxonomy' => 'following_usergroups' ) ); + // Register the objects where we'll be storing data and relationships + $this->register_usergroup_objects(); - // Get all of the users who are a part of user groups and assign them to their new user group values - $query = "SELECT * FROM $wpdb->usermeta WHERE meta_key='wp_ef_usergroups';"; - $usergroup_users = $wpdb->get_results( $query ); + $this->manage_usergroups_cap = apply_filters( 'ef_manage_usergroups_cap', $this->manage_usergroups_cap ); - // Sort all of the users based on their usergroup(s) - $users_to_add = array(); - foreach( (array)$usergroup_users as $usergroup_user ) { - if ( is_object( $usergroup_user ) ) - $users_to_add[$usergroup_user->meta_value][] = (int)$usergroup_user->user_id; - } - // Add user IDs to each usergroup - foreach( $users_to_add as $usergroup_slug => $users_array ) { - $usergroup = $this->get_usergroup_by( 'slug', $usergroup_slug ); - $this->add_users_to_usergroup( $users_array, $usergroup->term_id ); + // Register our settings + add_action( 'admin_init', array( $this, 'register_settings' ) ); + + // Handle any adding, editing or saving + add_action( 'admin_init', array( $this, 'handle_add_usergroup' ) ); + add_action( 'admin_init', array( $this, 'handle_edit_usergroup' ) ); + add_action( 'admin_init', array( $this, 'handle_delete_usergroup' ) ); + add_action( 'wp_ajax_inline_save_usergroup', array( $this, 'handle_ajax_inline_save_usergroup' ) ); + + // Usergroups can be managed from the User profile view + add_action( 'show_user_profile', array( $this, 'user_profile_page' ) ); + add_action( 'edit_user_profile', array( $this, 'user_profile_page' ) ); + add_action( 'user_profile_update_errors', array( $this, 'user_profile_update' ), 10, 3 ); + + // Javascript and CSS if we need it + add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) ); + add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_styles' ) ); + } + + /** + * Load the capabilities onto users the first time the module is run + * + * @since 0.7 + */ + public function install() { + + // Add necessary capabilities to allow management of user groups + $usergroup_roles = array( + 'administrator' => array( 'edit_usergroups' ), + ); + foreach ( $usergroup_roles as $role => $caps ) { + $this->add_caps_to_role( $role, $caps ); } - // Update the term slugs for each user group - $all_usergroups = $this->get_usergroups(); - foreach( $all_usergroups as $usergroup ) { - $new_slug = str_replace( 'ef_', self::term_prefix, $usergroup->slug ); - $this->update_usergroup( $usergroup->term_id, array( 'slug' => $new_slug ) ); + + // Create our default usergroups + $default_usergroups = array( + array( + 'name' => __( 'Copy Editors', 'edit-flow' ), + 'description' => __( 'Making sure the quality is top-notch.', 'edit-flow' ), + ), + array( + 'name' => __( 'Photographers', 'edit-flow' ), + 'description' => __( 'Capturing the story visually.', 'edit-flow' ), + ), + array( + 'name' => __( 'Reporters', 'edit-flow' ), + 'description' => __( 'Out in the field, writing stories.', 'edit-flow' ), + ), + array( + 'name' => __( 'Section Editors', 'edit-flow' ), + 'description' => __( 'Providing feedback and direction.', 'edit-flow' ), + ), + ); + foreach ( $default_usergroups as $args ) { + $this->add_usergroup( $args ); } + } - // Delete all of the previous usermeta values - $wpdb->query( "DELETE FROM $wpdb->usermeta WHERE meta_key='wp_ef_usergroups';" ); + /** + * Upgrade our data in case we need to + * + * @since 0.7 + */ + public function upgrade( $previous_version ) { + global $edit_flow; - // Technically we've run this code before so we don't want to auto-install new data - $edit_flow->update_module_option( $this->module->name, 'loaded_once', true ); + // Upgrade path to v0.7 + if ( version_compare( $previous_version, '0.7', '<' ) ) { + global $wpdb; - } - // Upgrade path to v0.7.4 - if ( version_compare( $previous_version, '0.7.4', '<' ) ) { - // Usergroup descriptions become base64_encoded, instead of maybe json_encoded. - $this->upgrade_074_term_descriptions( self::taxonomy_key ); + // Set all of the user group terms to our new taxonomy + $wpdb->update( $wpdb->term_taxonomy, array( 'taxonomy' => self::taxonomy_key ), array( 'taxonomy' => 'following_usergroups' ) ); + + // Get all of the users who are a part of user groups and assign them to their new user group values + $query = "SELECT * FROM $wpdb->usermeta WHERE meta_key='wp_ef_usergroups';"; + // There's no userdata here in this query and it's an upgrade query + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $usergroup_users = $wpdb->get_results( $query ); + + // Sort all of the users based on their usergroup(s) + $users_to_add = array(); + foreach ( (array) $usergroup_users as $usergroup_user ) { + if ( is_object( $usergroup_user ) ) { + $users_to_add[ $usergroup_user->meta_value ][] = (int) $usergroup_user->user_id; + } + } + // Add user IDs to each usergroup + foreach ( $users_to_add as $usergroup_slug => $users_array ) { + $usergroup = $this->get_usergroup_by( 'slug', $usergroup_slug ); + $this->add_users_to_usergroup( $users_array, $usergroup->term_id ); + } + // Update the term slugs for each user group + $all_usergroups = $this->get_usergroups(); + foreach ( $all_usergroups as $usergroup ) { + $new_slug = str_replace( 'ef_', self::term_prefix, $usergroup->slug ); + $this->update_usergroup( $usergroup->term_id, array( 'slug' => $new_slug ) ); + } + + // Delete all of the previous usermeta values + $wpdb->query( "DELETE FROM $wpdb->usermeta WHERE meta_key='wp_ef_usergroups';" ); + + // Technically we've run this code before so we don't want to auto-install new data + $edit_flow->update_module_option( $this->module->name, 'loaded_once', true ); + + } + // Upgrade path to v0.7.4 + if ( version_compare( $previous_version, '0.7.4', '<' ) ) { + // Usergroup descriptions become base64_encoded, instead of maybe json_encoded. + $this->upgrade_074_term_descriptions( self::taxonomy_key ); + } } - } - - /** - * Individual Usergroups are stored using a custom taxonomy - * Posts are associated with usergroups based on taxonomy relationship - * User associations are stored serialized in the term's description field - * - * @since 0.7 - * - * @uses register_taxonomy() - */ - function register_usergroup_objects() { - - // Load the currently supported post types so we only register against those - $supported_post_types = $this->get_post_types_for_module( $this->module ); - - // Use a taxonomy to manage relationships between posts and usergroups - $args = array( - 'public' => false, - 'rewrite' => false, - ); - register_taxonomy( self::taxonomy_key, $supported_post_types, $args ); - } - - /** - * Enqueue necessary admin scripts - * - * @since 0.7 - * - * @uses wp_enqueue_script() - */ - function enqueue_admin_scripts() { - - if ( $this->is_whitelisted_functional_view() || $this->is_whitelisted_settings_view( $this->module->name ) ) { - wp_enqueue_script( 'jquery-listfilterizer' ); - wp_enqueue_script( 'jquery-quicksearch' ); - wp_enqueue_script( 'edit-flow-user-groups-js', $this->module_url . 'lib/user-groups.js', array( 'jquery', 'jquery-listfilterizer', 'jquery-quicksearch' ), EDIT_FLOW_VERSION, true ); + /** + * Individual Usergroups are stored using a custom taxonomy + * Posts are associated with usergroups based on taxonomy relationship + * User associations are stored serialized in the term's description field + * + * @since 0.7 + * + * @uses register_taxonomy() + */ + public function register_usergroup_objects() { + + // Load the currently supported post types so we only register against those + $supported_post_types = $this->get_post_types_for_module( $this->module ); + + // Use a taxonomy to manage relationships between posts and usergroups + $args = array( + 'public' => false, + 'rewrite' => false, + ); + register_taxonomy( self::taxonomy_key, $supported_post_types, $args ); } - - if ( $this->is_whitelisted_settings_view( $this->module->name ) ) - wp_enqueue_script( 'edit-flow-user-groups-configure-js', $this->module_url . 'lib/user-groups-configure.js', array( 'jquery' ), EDIT_FLOW_VERSION, true ); - } - - /** - * Enqueue necessary admin styles, but only on the proper pages - * - * @since 0.7 - * - * @uses wp_enqueue_style() - */ - function enqueue_admin_styles() { - - if ( $this->is_whitelisted_functional_view() || $this->is_whitelisted_settings_view() ) { - wp_enqueue_style( 'jquery-listfilterizer' ); - wp_enqueue_style( 'edit-flow-user-groups-css', $this->module_url . 'lib/user-groups.css', false, EDIT_FLOW_VERSION ); + /** + * Enqueue necessary admin scripts + * + * @since 0.7 + * + * @uses wp_enqueue_script() + */ + public function enqueue_admin_scripts() { + + if ( $this->is_whitelisted_functional_view() || $this->is_whitelisted_settings_view( $this->module->name ) ) { + wp_enqueue_script( 'jquery-listfilterizer' ); + wp_enqueue_script( 'jquery-quicksearch' ); + wp_enqueue_script( 'edit-flow-user-groups-js', $this->module_url . 'lib/user-groups.js', array( 'jquery', 'jquery-listfilterizer', 'jquery-quicksearch' ), EDIT_FLOW_VERSION, true ); + } + + if ( $this->is_whitelisted_settings_view( $this->module->name ) ) { + wp_enqueue_script( 'edit-flow-user-groups-configure-js', $this->module_url . 'lib/user-groups-configure.js', array( 'jquery' ), EDIT_FLOW_VERSION, true ); + } } - } - - /** - * Module ??? - */ - - /** - * Handles a POST request to add a new Usergroup. Redirects to edit view after - * for admin to add users to usergroup - * Hooked into 'admin_init' and kicks out right away if no action - * - * @since 0.7 - */ - function handle_add_usergroup() { - if ( !isset( $_POST['submit'], $_POST['form-action'], $_GET['page'] ) - || $_GET['page'] != $this->module->settings_slug || $_POST['form-action'] != 'add-usergroup' ) - return; - - if ( !wp_verify_nonce( $_POST['_wpnonce'], 'add-usergroup' ) ) - wp_die( $this->module->messages['nonce-failed'] ); - - if ( !current_user_can( $this->manage_usergroups_cap ) ) - wp_die( $this->module->messages['invalid-permissions'] ); - - // Sanitize all of the user-entered values - $name = strip_tags( trim( $_POST['name'] ) ); - $description = stripslashes( strip_tags( trim( $_POST['description'] ) ) ); - - $_REQUEST['form-errors'] = array(); - /** - * Form validation for adding new Usergroup + * Enqueue necessary admin styles, but only on the proper pages + * + * @since 0.7 * - * Details - * - 'name' is a required field, but can't match an existing name or slug. Needs to be 40 characters or less - * - "description" can accept a limited amount of HTML, and is optional + * @uses wp_enqueue_style() */ - // Field is required - if ( empty( $name ) ) - $_REQUEST['form-errors']['name'] = __( 'Please enter a name for the user group.', 'edit-flow' ); - // Check to ensure a term with the same name doesn't exist - if ( $this->get_usergroup_by( 'name', $name ) ) - $_REQUEST['form-errors']['name'] = __( 'Name already in use. Please choose another.', 'edit-flow' ); - // Check to ensure a term with the same slug doesn't exist - if ( $this->get_usergroup_by( 'slug', sanitize_title( $name ) ) ) - $_REQUEST['form-errors']['name'] = __( 'Name conflicts with slug for another term. Please choose again.', 'edit-flow' ); - if ( strlen( $name ) > 40 ) - $_REQUEST['form-errors']['name'] = __( 'User group name cannot exceed 40 characters. Please try a shorter name.', 'edit-flow' ); - // Kick out if there are any errors - if ( count( $_REQUEST['form-errors'] ) ) { - $_REQUEST['error'] = 'form-error'; - return; + public function enqueue_admin_styles() { + + + if ( $this->is_whitelisted_functional_view() || $this->is_whitelisted_settings_view() ) { + wp_enqueue_style( 'jquery-listfilterizer' ); + wp_enqueue_style( 'edit-flow-user-groups-css', $this->module_url . 'lib/user-groups.css', false, EDIT_FLOW_VERSION ); + } } - // Try to add the Usergroup - $args = array( - 'name' => $name, - 'description' => $description, - ); - $usergroup = $this->add_usergroup( $args ); - if ( is_wp_error( $usergroup ) ) - wp_die( __( 'Error adding usergroup.', 'edit-flow' ) ); - - $args = array( - 'action' => 'edit-usergroup', - 'usergroup-id' => $usergroup->term_id, - 'message' => 'usergroup-added' - ); - $redirect_url = $this->get_link( $args ); - wp_redirect( $redirect_url ); - exit; - } - - /** - * Handles a POST request to edit a Usergroup - * Hooked into 'admin_init' and kicks out right away if no action - * - * @since 0.7 - */ - function handle_edit_usergroup() { - if ( !isset( $_POST['submit'], $_POST['form-action'], $_GET['page'] ) - || $_GET['page'] != $this->module->settings_slug || $_POST['form-action'] != 'edit-usergroup' ) - return; - - if ( !wp_verify_nonce( $_POST['_wpnonce'], 'edit-usergroup' ) ) - wp_die( $this->module->messages['nonce-failed'] ); - - if ( !current_user_can( $this->manage_usergroups_cap ) ) - wp_die( $this->module->messages['invalid-permissions'] ); - - if ( !$existing_usergroup = $this->get_usergroup_by( 'id', (int)$_POST['usergroup_id'] ) ) - wp_die( $this->module->messages['usergroup-missing'] ); - - // Sanitize all of the user-entered values - $name = strip_tags( trim( $_POST['name'] ) ); - $description = stripslashes( strip_tags( trim( $_POST['description'] ) ) ); - - $_REQUEST['form-errors'] = array(); - /** - * Form validation for editing a Usergroup + * Module ??? + */ + + /** + * Handles a POST request to add a new Usergroup. Redirects to edit view after + * for admin to add users to usergroup + * Hooked into 'admin_init' and kicks out right away if no action * - * Details - * - 'name' is a required field, but can't match an existing name or slug. Needs to be 40 characters or less - * - "description" can accept a limited amount of HTML, and is optional + * @since 0.7 */ - // Field is required - if ( empty( $name ) ) - $_REQUEST['form-errors']['name'] = __( 'Please enter a name for the user group.', 'edit-flow' ); - // Check to ensure a term with the same name doesn't exist - $search_term = $this->get_usergroup_by( 'name', $name ); - if ( is_object( $search_term ) && $search_term->term_id != $existing_usergroup->term_id ) - $_REQUEST['form-errors']['name'] = __( 'Name already in use. Please choose another.', 'edit-flow' ); - // Check to ensure a term with the same slug doesn't exist - $search_term = $this->get_usergroup_by( 'slug', sanitize_title( $name ) ); - if ( is_object( $search_term ) && $search_term->term_id != $existing_usergroup->term_id ) - $_REQUEST['form-errors']['name'] = __( 'Name conflicts with slug for another term. Please choose again.', 'edit-flow' ); - if ( strlen( $name ) > 40 ) - $_REQUEST['form-errors']['name'] = __( 'User group name cannot exceed 40 characters. Please try a shorter name.', 'edit-flow' ); - // Kick out if there are any errors - if ( count( $_REQUEST['form-errors'] ) ) { - $_REQUEST['error'] = 'form-error'; - return; + public function handle_add_usergroup() { + + if ( ! isset( $_POST['submit'], $_POST['form-action'], $_GET['page'] ) + || $_GET['page'] != $this->module->settings_slug || 'add-usergroup' != $_POST['form-action'] ) { + return; + } + + if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'add-usergroup' ) ) { + wp_die( esc_html( $this->module->messages['nonce-failed'] ) ); + } + + if ( ! current_user_can( $this->manage_usergroups_cap ) ) { + wp_die( esc_html( $this->module->messages['invalid-permissions'] ) ); + } + + // Sanitize all of the user-entered values + $name = ( isset( $_POST['name'] ) ) ? sanitize_text_field( trim( $_POST['name'] ) ) : ''; + $description = ( isset( $_POST['description'] ) ) ? stripslashes( wp_filter_nohtml_kses( trim( $_POST['description'] ) ) ) : ''; + + $_REQUEST['form-errors'] = array(); + + /** + * Form validation for adding new Usergroup + * + * Details + * - 'name' is a required field, but can't match an existing name or slug. Needs to be 40 characters or less + * - "description" can accept a limited amount of HTML, and is optional + */ + // Field is required + if ( empty( $name ) ) { + $_REQUEST['form-errors']['name'] = __( 'Please enter a name for the user group.', 'edit-flow' ); + } + // Check to ensure a term with the same name doesn't exist + if ( $this->get_usergroup_by( 'name', $name ) ) { + $_REQUEST['form-errors']['name'] = __( 'Name already in use. Please choose another.', 'edit-flow' ); + } + // Check to ensure a term with the same slug doesn't exist + if ( $this->get_usergroup_by( 'slug', sanitize_title( $name ) ) ) { + $_REQUEST['form-errors']['name'] = __( 'Name conflicts with slug for another term. Please choose again.', 'edit-flow' ); + } + if ( strlen( $name ) > 40 ) { + $_REQUEST['form-errors']['name'] = __( 'User group name cannot exceed 40 characters. Please try a shorter name.', 'edit-flow' ); + } + // Kick out if there are any errors + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated + if ( count( $_REQUEST['form-errors'] ) ) { + $_REQUEST['error'] = 'form-error'; + return; + } + + // Try to add the Usergroup + $args = array( + 'name' => $name, + 'description' => $description, + ); + $usergroup = $this->add_usergroup( $args ); + if ( is_wp_error( $usergroup ) ) { + wp_die( esc_html__( 'Error adding usergroup.', 'edit-flow' ) ); + } + + $args = array( + 'action' => 'edit-usergroup', + 'usergroup-id' => $usergroup->term_id, + 'message' => 'usergroup-added', + ); + $redirect_url = $this->get_link( $args ); + wp_redirect( $redirect_url ); + exit; } - // Try to edit the Usergroup - $args = array( - 'name' => $name, - 'description' => $description, - ); - // Gracefully handle the case where all users have been unsubscribed from the user group - $users = isset( $_POST['usergroup_users'] ) ? (array)$_POST['usergroup_users'] : array(); - $users = array_map( 'intval', $users ); - $usergroup = $this->update_usergroup( $existing_usergroup->term_id, $args, $users ); - if ( is_wp_error( $usergroup ) ) - wp_die( __( 'Error updating user group.', 'edit-flow' ) ); - - $args = array( - 'message' => 'usergroup-updated', - ); - $redirect_url = $this->get_link( $args ); - wp_redirect( $redirect_url ); - exit; - } - - /** - * Handles a request to delete a Usergroup. - * Hooked into 'admin_init' and kicks out right away if no action - * - * @since 0.7 - */ - function handle_delete_usergroup() { - if ( !isset( $_GET['page'], $_GET['action'], $_GET['usergroup-id'] ) - || $_GET['page'] != $this->module->settings_slug || $_GET['action'] != 'delete-usergroup' ) + /** + * Handles a POST request to edit a Usergroup + * Hooked into 'admin_init' and kicks out right away if no action + * + * @since 0.7 + */ + public function handle_edit_usergroup() { + if ( ! isset( $_POST['submit'], $_POST['form-action'], $_GET['page'] ) + || $_GET['page'] != $this->module->settings_slug || 'edit-usergroup' != $_POST['form-action'] ) { return; - - if ( !wp_verify_nonce( $_GET['nonce'], 'delete-usergroup' ) ) - wp_die( $this->module->messages['nonce-failed'] ); - - if ( !current_user_can( $this->manage_usergroups_cap ) ) - wp_die( $this->module->messages['invalid-permissions'] ); - - $result = $this->delete_usergroup( (int)$_GET['usergroup-id'] ); - if ( !$result || is_wp_error( $result ) ) - wp_die( __( 'Error deleting user group.', 'edit-flow' ) ); - - $redirect_url = $this->get_link( array( 'message' => 'usergroup-deleted' ) ); - wp_redirect( $redirect_url ); - exit; - } - - /** - * Handle the request to update a given Usergroup via inline edit - * - * @since 0.7 - */ - function handle_ajax_inline_save_usergroup() { - - if ( !wp_verify_nonce( $_POST['inline_edit'], 'usergroups-inline-edit-nonce' ) ) - die( $this->module->messages['nonce-failed'] ); - - if ( !current_user_can( $this->manage_usergroups_cap ) ) - die( $this->module->messages['invalid-permissions'] ); - - $usergroup_id = (int) $_POST['usergroup_id']; - if ( !$existing_term = $this->get_usergroup_by( 'id', $usergroup_id ) ) - die( $this->module->messages['usergroup-missing'] ); - - $name = strip_tags( trim( $_POST['name'] ) ); - $description = stripslashes( strip_tags( trim( $_POST['description'] ) ) ); - + } + + if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'edit-usergroup' ) ) { + wp_die( esc_html( $this->module->messages['nonce-failed'] ) ); + } + + if ( ! current_user_can( $this->manage_usergroups_cap ) ) { + wp_die( esc_html( $this->module->messages['invalid-permissions'] ) ); + } + + $usergroup_id = isset( $_POST['usergroup_id'] ) ? (int) $_POST['usergroup_id'] : 0; + $existing_usergroup = $this->get_usergroup_by( 'id', $usergroup_id ); + if ( ! $existing_usergroup ) { + wp_die( esc_html( $this->module->messages['usergroup-missing'] ) ); + } + + // Sanitize all of the user-entered values + $name = isset( $_POST['name'] ) ? sanitize_text_field( trim( $_POST['name'] ) ) : ''; + $description = isset( $_POST['description'] ) ? stripslashes( wp_filter_nohtml_kses( trim( $_POST['description'] ) ) ) : ''; + + $_REQUEST['form-errors'] = array(); + + /** + * Form validation for editing a Usergroup + * + * Details + * - 'name' is a required field, but can't match an existing name or slug. Needs to be 40 characters or less + * - "description" can accept a limited amount of HTML, and is optional + */ + // Field is required + if ( empty( $name ) ) { + $_REQUEST['form-errors']['name'] = __( 'Please enter a name for the user group.', 'edit-flow' ); + } + // Check to ensure a term with the same name doesn't exist + $search_term = $this->get_usergroup_by( 'name', $name ); + if ( is_object( $search_term ) && $search_term->term_id != $existing_usergroup->term_id ) { + $_REQUEST['form-errors']['name'] = __( 'Name already in use. Please choose another.', 'edit-flow' ); + } + // Check to ensure a term with the same slug doesn't exist + $search_term = $this->get_usergroup_by( 'slug', sanitize_title( $name ) ); + if ( is_object( $search_term ) && $search_term->term_id != $existing_usergroup->term_id ) { + $_REQUEST['form-errors']['name'] = __( 'Name conflicts with slug for another term. Please choose again.', 'edit-flow' ); + } + if ( strlen( $name ) > 40 ) { + $_REQUEST['form-errors']['name'] = __( 'User group name cannot exceed 40 characters. Please try a shorter name.', 'edit-flow' ); + } + // Kick out if there are any errors + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated + if ( count( $_REQUEST['form-errors'] ) ) { + $_REQUEST['error'] = 'form-error'; + return; + } + + // Try to edit the Usergroup + $args = array( + 'name' => $name, + 'description' => $description, + ); + // Gracefully handle the case where all users have been unsubscribed from the user group + $users = isset( $_POST['usergroup_users'] ) ? (array) $_POST['usergroup_users'] : array(); + $users = array_map( 'intval', $users ); + $usergroup = $this->update_usergroup( $existing_usergroup->term_id, $args, $users ); + if ( is_wp_error( $usergroup ) ) { + wp_die( esc_html__( 'Error updating user group.', 'edit-flow' ) ); + } + + $args = array( + 'message' => 'usergroup-updated', + ); + $redirect_url = $this->get_link( $args ); + wp_redirect( $redirect_url ); + exit; + } + /** - * Form validation for editing Usergroup - */ - // Check if name field was filled in - if ( empty( $name ) ) { - $change_error = new WP_Error( 'invalid', __( 'Please enter a name for the user group.', 'edit-flow' ) ); - die( $change_error->get_error_message() ); + * Handles a request to delete a Usergroup. + * Hooked into 'admin_init' and kicks out right away if no action + * + * @since 0.7 + */ + public function handle_delete_usergroup() { + if ( ! isset( $_GET['page'], $_GET['action'], $_GET['usergroup-id'] ) + || $_GET['page'] != $this->module->settings_slug || 'delete-usergroup' != $_GET['action'] ) { + return; + } + + if ( ! isset( $_GET['nonce'] ) || ! wp_verify_nonce( $_GET['nonce'], 'delete-usergroup' ) ) { + wp_die( esc_html( $this->module->messages['nonce-failed'] ) ); + } + + if ( ! current_user_can( $this->manage_usergroups_cap ) ) { + wp_die( esc_html( $this->module->messages['invalid-permissions'] ) ); + } + + $result = $this->delete_usergroup( (int) $_GET['usergroup-id'] ); + if ( ! $result || is_wp_error( $result ) ) { + wp_die( esc_html__( 'Error deleting user group.', 'edit-flow' ) ); + } + + $redirect_url = $this->get_link( array( 'message' => 'usergroup-deleted' ) ); + wp_redirect( $redirect_url ); + exit; } - // Check that the name doesn't exceed 40 chars - if ( strlen( $name ) > 40 ) { - $change_error = new WP_Error( 'invalid', __( 'User group name cannot exceed 40 characters. Please try a shorter name.' ) ); - die( $change_error->get_error_message() ); + + /** + * Handle the request to update a given Usergroup via inline edit + * + * @since 0.7 + */ + public function handle_ajax_inline_save_usergroup() { + + if ( ! isset( $_POST['inline_edit'] ) || ! wp_verify_nonce( $_POST['inline_edit'], 'usergroups-inline-edit-nonce' ) ) { + die( esc_html( $this->module->messages['nonce-failed'] ) ); + } + + if ( ! current_user_can( $this->manage_usergroups_cap ) ) { + die( esc_html( $this->module->messages['invalid-permissions'] ) ); + } + + $usergroup_id = isset( $_POST['usergroup_id'] ) ? (int) $_POST['usergroup_id'] : 0; + if ( ! $existing_term = $this->get_usergroup_by( 'id', $usergroup_id ) ) { + die( esc_html( $this->module->messages['usergroup-missing'] ) ); + } + + $name = isset( $_POST['name'] ) ? sanitize_text_field( trim( $_POST['name'] ) ) : ''; + $description = isset( $_POST['description'] ) ? stripslashes( wp_filter_nohtml_kses( trim( $_POST['description'] ) ) ) : ''; + + /** + * Form validation for editing Usergroup + */ + // Check if name field was filled in + if ( empty( $name ) ) { + $change_error = new WP_Error( 'invalid', esc_html__( 'Please enter a name for the user group.', 'edit-flow' ) ); + die( esc_html( $change_error->get_error_message() ) ); + } + // Check that the name doesn't exceed 40 chars + if ( strlen( $name ) > 40 ) { + $change_error = new WP_Error( 'invalid', esc_html__( 'User group name cannot exceed 40 characters. Please try a shorter name.' ) ); + die( esc_html( $change_error->get_error_message() ) ); + } + // Check to ensure a term with the same name doesn't exist + $search_term = $this->get_usergroup_by( 'name', $name ); + if ( is_object( $search_term ) && $search_term->term_id != $existing_term->term_id ) { + $change_error = new WP_Error( 'invalid', esc_html__( 'Name already in use. Please choose another.', 'edit-flow' ) ); + die( esc_html( $change_error->get_error_message() ) ); + } + // Check to ensure a term with the same slug doesn't exist + $search_term = $this->get_usergroup_by( 'slug', sanitize_title( $name ) ); + if ( is_object( $search_term ) && $search_term->term_id != $existing_term->term_id ) { + $change_error = new WP_Error( 'invalid', esc_html__( 'Name conflicts with slug for another term. Please choose again.', 'edit-flow' ) ); + die( esc_html( $change_error->get_error_message() ) ); + } + + // Prepare the term name and description for saving + $args = array( + 'name' => $name, + 'description' => $description, + ); + $return = $this->update_usergroup( $existing_term->term_id, $args ); + if ( ! is_wp_error( $return ) ) { + set_current_screen( 'edit-usergroup' ); + $wp_list_table = new EF_Usergroups_List_Table(); + $wp_list_table->prepare_items(); + echo wp_kses_post( $wp_list_table->single_row( $return ) ); + die(); + } else { + // translators: %s is the name of the user group + $change_error = new WP_Error( 'invalid', sprintf( __( 'Could not update the user group: %s', 'edit-flow' ), $name ) ); + die( wp_kses( $change_error->get_error_message(), 'strong' ) ); + } } - // Check to ensure a term with the same name doesn't exist - $search_term = $this->get_usergroup_by( 'name', $name ); - if ( is_object( $search_term ) && $search_term->term_id != $existing_term->term_id ) { - $change_error = new WP_Error( 'invalid', __( 'Name already in use. Please choose another.', 'edit-flow' ) ); - die( $change_error->get_error_message() ); + + /** + * Register settings for notifications so we can partially use the Settings API + * (We use the Settings API for form generation, but not saving) + * + * @since 0.7 + * @uses add_settings_section(), add_settings_field() + */ + public function register_settings() { + add_settings_section( $this->module->options_group_name . '_general', false, '__return_false', $this->module->options_group_name ); + 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' ); } - // Check to ensure a term with the same slug doesn't exist - $search_term = $this->get_usergroup_by( 'slug', sanitize_title( $name ) ); - if ( is_object( $search_term ) && $search_term->term_id != $existing_term->term_id ) { - $change_error = new WP_Error( 'invalid', __( 'Name conflicts with slug for another term. Please choose again.', 'edit-flow' ) ); - die( $change_error->get_error_message() ); + + /** + * Choose the post types for Usergroups + * + * @since 0.7 + */ + public function settings_post_types_option() { + global $edit_flow; + $edit_flow->settings->helper_option_custom_post_type( $this->module ); } - - // Prepare the term name and description for saving - $args = array( - 'name' => $name, - 'description' => $description, - ); - $return = $this->update_usergroup( $existing_term->term_id, $args ); - if( !is_wp_error( $return ) ) { - set_current_screen( 'edit-usergroup' ); - $wp_list_table = new EF_Usergroups_List_Table(); - $wp_list_table->prepare_items(); - echo $wp_list_table->single_row( $return ); - die(); - } else { - $change_error = new WP_Error( 'invalid', sprintf( __( 'Could not update the user group: %s', 'edit-flow' ), $usergroup_name ) ); - die( $change_error->get_error_message() ); + + /** + * Validate data entered by the user + * + * @since 0.7 + * + * @param array $new_options New values that have been entered by the user + * @return array $new_options Form values after they've been sanitized + */ + public function settings_validate( $new_options ) { + + + // Whitelist validation for the post type options + if ( ! isset( $new_options['post_types'] ) ) { + $new_options['post_types'] = array(); + } + $new_options['post_types'] = $this->clean_post_type_options( $new_options['post_types'], $this->module->post_type_support ); + + return $new_options; } - - } - - /** - * Register settings for notifications so we can partially use the Settings API - * (We use the Settings API for form generation, but not saving) - * - * @since 0.7 - * @uses add_settings_section(), add_settings_field() - */ - function register_settings() { - add_settings_section( $this->module->options_group_name . '_general', false, '__return_false', $this->module->options_group_name ); - 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' ); - } - - /** - * Choose the post types for Usergroups - * - * @since 0.7 - */ - function settings_post_types_option() { - global $edit_flow; - $edit_flow->settings->helper_option_custom_post_type( $this->module ); - } - /** - * Validate data entered by the user - * - * @since 0.7 - * - * @param array $new_options New values that have been entered by the user - * @return array $new_options Form values after they've been sanitized - */ - function settings_validate( $new_options ) { - - - // Whitelist validation for the post type options - if ( !isset( $new_options['post_types'] ) ) - $new_options['post_types'] = array(); - $new_options['post_types'] = $this->clean_post_type_options( $new_options['post_types'], $this->module->post_type_support ); - - return $new_options; - } - - /** - * Build a configuration view so we can manage our usergroups - * - * @since 0.7 - */ - function print_configure_view() { - global $edit_flow; - - if ( isset( $_GET['action'], $_GET['usergroup-id'] ) && $_GET['action'] == 'edit-usergroup' ) : - /** Full page width view for editing a given usergroup **/ - // Check whether the usergroup exists - $usergroup_id = (int)$_GET['usergroup-id']; - $usergroup = $this->get_usergroup_by( 'id', $usergroup_id ); - if ( !$usergroup ) { - echo '

    ' . $this->module->messages['usergroup-missing'] . '

    '; - return; - } - $name = ( isset( $_POST['name'] ) ) ? stripslashes( $_POST['name'] ) : $usergroup->name; - $description = ( isset( $_POST['description'] ) ) ? stripslashes( $_POST['description'] ) : $usergroup->description; - ?> -
    + /** + * Build a configuration view so we can manage our usergroups + * + * @since 0.7 + * 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. + * phpcs:disable:WordPress.Security.NonceVerification.Missing + */ + public function print_configure_view() { + global $edit_flow; + + if ( isset( $_GET['action'], $_GET['usergroup-id'] ) && 'edit-usergroup' == $_GET['action'] ) : + /** Full page width view for editing a given usergroup **/ + // Check whether the usergroup exists + $usergroup_id = (int) $_GET['usergroup-id']; + $usergroup = $this->get_usergroup_by( 'id', $usergroup_id ); + if ( ! $usergroup ) { + echo '

    ' . esc_html( $this->module->messages['usergroup-missing'] ) . '

    '; + return; + } + $name = ( isset( $_POST['name'] ) ) ? stripslashes( $_POST['name'] ) : $usergroup->name; + $description = ( isset( $_POST['description'] ) ) ? strip_tags( stripslashes( $_POST['description'] ) ) : $usergroup->description; + ?> +

    - 'ef-post_following_list', - 'input_id' => 'usergroup_users' + 'input_id' => 'usergroup_users', ); - ?> - users_select_form( $usergroup->user_ids , $select_form_args ); ?> + ?> + users_select_form( $usergroup->user_ids, $select_form_args ); ?>
    -
    +
    - + ?>
    @@ -579,83 +619,90 @@ function print_configure_view() { settings->helper_print_error_or_description( 'description', __( 'The description is primarily for administrative use, to give you some context on what the user group is to be used for.', 'edit-flow' ) ); ?>

    - +

    - - prepare_items(); - ?> -
    display(); ?>
    - +
    module->options_group_name ); ?> - module->options_group_name ); ?> + module->options_group_name ); ?> module->name ) . '" />'; ?>
    - - + +
    - + settings->helper_print_error_or_description( 'name', __( 'The name is used to identify the user group.', 'edit-flow' ) ); ?>
    - + settings->helper_print_error_or_description( 'description', __( 'The description is primarily for administrative use, to give you some context on what the user group is to be used for.', 'edit-flow' ) ); ?>
    '; ?> -

    +

    - inline_edit(); ?> - manage_usergroups_cap ) ) - return; - - //Don't allow display of user groups from network - if ( ( !is_null( get_current_screen() ) ) && ( get_current_screen()->is_network ) ) - return; - - // Assemble all necessary data - $usergroups = $this->get_usergroups(); - $selected_usergroups = $this->get_usergroups_for_user( $user_id ); - $usergroups_form_args = array( 'input_id' => 'ef_usergroups' ); - ?> + inline_edit(); ?> + manage_usergroups_cap ) ) { + return; + } + + //Don't allow display of user groups from network + if ( ( ! is_null( get_current_screen() ) ) && ( get_current_screen()->is_network ) ) { + return; + } + + // Assemble all necessary data + $usergroups = $this->get_usergroups(); + $selected_usergroups = $this->get_usergroups_for_user( $user_id ); + $usergroups_form_args = array( 'input_id' => 'ef_usergroups' ); + ?>
    -

    - ID ) : ?> -

    +

    + ID === $user_id ) : ?> +

    -

    +

    @@ -667,571 +714,599 @@ function user_profile_page() {
    - - + is_network ) { - return; + /** + * Function called when a user's profile is updated + * Adds user to specified usergroups + * + * @since 0.7 + * + * @param ??? + * @param ??? + * @param ??? + * @return ??? + */ + public function user_profile_update( $errors, $update, $user ) { + + if ( ! $update ) { + return array( &$errors, $update, &$user ); } - } - if ( current_user_can( $this->manage_usergroups_cap ) && wp_verify_nonce( $_POST['ef_edit_profile_usergroups_nonce'], 'ef_edit_profile_usergroups_nonce' ) ) { - // Sanitize the data and save - // Gracefully handle the case where the user was unsubscribed from all usergroups - $usergroups = isset( $_POST['ef_usergroups'] ) ? array_map( 'intval', (array)$_POST['ef_usergroups'] ) : array(); - $all_usergroups = $this->get_usergroups(); - foreach( $all_usergroups as $usergroup ) { - if ( in_array( $usergroup->term_id, $usergroups ) ) - $this->add_user_to_usergroup( $user->ID, $usergroup->term_id ); - else - $this->remove_user_from_usergroup( $user->ID, $usergroup->term_id ); + // `get_current_screen()` is defined on most admin pages, but not all. + if ( function_exists( 'get_current_screen' ) ) { + //Don't allow update of user groups from network + $screen = get_current_screen(); + if ( ! is_null( $screen ) && $screen->is_network ) { + return; + } + } + + if ( isset( $_POST['ef_edit_profile_usergroups_nonce'] ) && current_user_can( $this->manage_usergroups_cap ) && wp_verify_nonce( $_POST['ef_edit_profile_usergroups_nonce'], 'ef_edit_profile_usergroups_nonce' ) ) { + // Sanitize the data and save + // Gracefully handle the case where the user was unsubscribed from all usergroups + $usergroups = isset( $_POST['ef_usergroups'] ) ? array_map( 'intval', (array) $_POST['ef_usergroups'] ) : array(); + $all_usergroups = $this->get_usergroups(); + foreach ( $all_usergroups as $usergroup ) { + if ( in_array( $usergroup->term_id, $usergroups ) ) { + $this->add_user_to_usergroup( $user->ID, $usergroup->term_id ); + } else { + $this->remove_user_from_usergroup( $user->ID, $usergroup->term_id ); + } + } } + + return array( &$errors, $update, &$user ); } - - return array( &$errors, $update, &$user ); - } - - /** - * Generate a link to one of the usergroups actions - * - * @since 0.7 - * - * @param string $action Action we want the user to take - * @param array $args Any query args to add to the URL - * @return string $link Direct link to delete a usergroup - */ - function get_link( $args = array() ) { - if ( !isset( $args['action'] ) ) - $args['action'] = ''; - if ( !isset( $args['page'] ) ) - $args['page'] = $this->module->settings_slug; - // Add other things we may need depending on the action - switch( $args['action'] ) { - case 'delete-usergroup': - $args['nonce'] = wp_create_nonce( $args['action'] ); - break; - default: - break; + + /** + * Generate a link to one of the usergroups actions + * + * @since 0.7 + * + * @param string $action Action we want the user to take + * @param array $args Any query args to add to the URL + * @return string $link Direct link to delete a usergroup + */ + public function get_link( $args = array() ) { + if ( ! isset( $args['action'] ) ) { + $args['action'] = ''; + } + if ( ! isset( $args['page'] ) ) { + $args['page'] = $this->module->settings_slug; + } + // Add other things we may need depending on the action + switch ( $args['action'] ) { + case 'delete-usergroup': + $args['nonce'] = wp_create_nonce( $args['action'] ); + break; + default: + break; + } + return add_query_arg( $args, get_admin_url( null, 'admin.php' ) ); } - return add_query_arg( $args, get_admin_url( null, 'admin.php' ) ); - } - - /** - * Displays a list of usergroups with checkboxes - * - * @since 0.7 - * - * @param array $selected List of usergroup keys that should be checked - * @param array $args ??? - */ - function usergroups_select_form( $selected = array(), $args = null ) { - - // TODO add $args for additional options - // e.g. showing members assigned to group (John Smith, Jane Doe, and 9 others) - // before , after , class, id names? - $defaults = array( - 'list_class' => 'ef-post_following_list', - 'list_id' => 'ef-following_usergroups', - 'input_id' => 'following_usergroups' - ); - - $parsed_args = wp_parse_args( $args, $defaults ); - extract( $parsed_args, EXTR_SKIP ); - $usergroups = $this->get_usergroups(); - if ( empty($usergroups) ) { - ?> -

    - -
      - term_id, $selected ) ) ? ' checked="checked"' : ''; + /** + * Displays a list of usergroups with checkboxes + * + * @since 0.7 + * + * @param array $selected List of usergroup keys that should be checked + * @param array $args ??? + */ + public function usergroups_select_form( $selected = array(), $args = null ) { + + // TODO add $args for additional options + // e.g. showing members assigned to group (John Smith, Jane Doe, and 9 others) + // before , after , class, id names? + $defaults = array( + 'list_class' => 'ef-post_following_list', + 'list_id' => 'ef-following_usergroups', + 'input_id' => 'following_usergroups', + ); + + $parsed_args = wp_parse_args( $args, $defaults ); + extract( $parsed_args, EXTR_SKIP ); + $usergroups = $this->get_usergroups(); + if ( empty( $usergroups ) ) { ?> +

      + +
        +
      • -
      • + +
      -
    - get_usergroup_by( 'id', $usergroup_term->term_id ); } - return $usergroups; - } - - /** - * Get all of the data associated with a single usergroup - * Usergroup contains: - * - ID (key = term_id) - * - Slug (prefixed with our special key to avoid conflicts) - * - Name - * - Description - * - User IDs (array of IDs) - * - * @since 0.7 - * - * @param string $field 'id', 'name', or 'slug' - * @param int|string $value Value for the search field - * @return object|array|WP_Error $usergroup Usergroup information as specified by $output - */ - function get_usergroup_by( $field, $value ) { - - $usergroup = get_term_by( $field, $value, self::taxonomy_key ); - - if ( !$usergroup || is_wp_error( $usergroup ) ) - return $usergroup; - - // We're using an encoded description field to store extra values - // Declare $user_ids ahead of time just in case it's empty - $usergroup->user_ids = array(); - $unencoded_description = $this->get_unencoded_description( $usergroup->description ); - if ( is_array( $unencoded_description ) ) { - foreach( $unencoded_description as $key => $value ) { - $usergroup->$key = $value; + + /** + * Core Usergroups Module Functionality + */ + + /** + * Get all of the registered usergroups. Returns an array of objects + * + * @since 0.7 + * + * @param array $args Arguments to filter/sort by + * @return array|bool $usergroups Array of Usergroups with relevant data, false if none + */ + public function get_usergroups( $args = array() ) { + + // We want empty terms by default + $usergroup_terms = get_terms( array( + 'taxonomy' => self::taxonomy_key, + 'hide_empty' => isset( $args['hide_empty'] ), + )); + if ( ! $usergroup_terms ) { + return false; } + + // Run the usergroups through get_usergroup_by() so we load users too + $usergroups = array(); + foreach ( $usergroup_terms as $usergroup_term ) { + $usergroups[] = $this->get_usergroup_by( 'id', $usergroup_term->term_id ); + } + return $usergroups; } - $usergroup = apply_filters( 'ef_usergroup_object', $usergroup ); + /** + * Get all of the data associated with a single usergroup + * Usergroup contains: + * - ID (key = term_id) + * - Slug (prefixed with our special key to avoid conflicts) + * - Name + * - Description + * - User IDs (array of IDs) + * + * @since 0.7 + * + * @param string $field 'id', 'name', or 'slug' + * @param int|string $value Value for the search field + * @return object|array|WP_Error $usergroup Usergroup information as specified by $output + */ + public function get_usergroup_by( $field, $value ) { + + $usergroup = get_term_by( $field, $value, self::taxonomy_key ); + + if ( ! $usergroup || is_wp_error( $usergroup ) ) { + return $usergroup; + } + + // We're using an encoded description field to store extra values + // Declare $user_ids ahead of time just in case it's empty + $usergroup->user_ids = array(); + $unencoded_description = $this->get_unencoded_description( $usergroup->description ); + if ( is_array( $unencoded_description ) ) { + foreach ( $unencoded_description as $key => $value ) { + $usergroup->$key = $value; + } + } + + $usergroup = apply_filters( 'ef_usergroup_object', $usergroup ); - return $usergroup; - } - - /** - * Create a new usergroup containing: - * - Name - * - Slug (prefixed with our special key to avoid conflicts) - * - Description - * - Users - * - * @since 0.7 - * - * @param array $args Name (optional), slug and description for the usergroup - * @param array $user_ids IDs for the users to be added to the Usergroup - * @return object|WP_Error $usergroup Object for the new Usergroup on success, WP_Error otherwise - */ - function add_usergroup( $args = array(), $user_ids = array() ) { - - if ( !isset( $args['name'] ) ) - return new WP_Error( 'invalid', __( 'New user groups must have a name', 'edit-flow' ) ); - - $name = $args['name']; - $default = array( - 'name' => '', - 'slug' => self::term_prefix . sanitize_title( $name ), - 'description' => '', - ); - $args = array_merge( $default, $args ); - - // Encode our extra fields and then store them in the description field - $args_to_encode = array( - 'description' => $args['description'], - 'user_ids' => array_unique( $user_ids ), - ); - $encoded_description = $this->get_encoded_description( $args_to_encode ); - $args['description'] = $encoded_description; - $usergroup = wp_insert_term( $name, self::taxonomy_key, $args ); - if ( is_wp_error( $usergroup ) ) - return $usergroup; - - return $this->get_usergroup_by( 'id', $usergroup['term_id'] ); - } - - /** - * Update a usergroup with new data. - * Fields can include: - * - Name - * - Slug (prefixed with our special key, of course) - * - Description - * - Users - * - * @since 0.7 - * - * @param int $id Unique ID for the usergroup - * @param array $args Usergroup meta to update (name, slug, description) - * @param array $users Users to be added to the Usergroup. If set, removes existing users first. - * @return object|WP_Error $usergroup Object for the updated Usergroup on success, WP_Error otherwise - */ - function update_usergroup( $id, $args = array(), $users = null ) { - - $existing_usergroup = $this->get_usergroup_by( 'id', $id ); - if ( is_wp_error( $existing_usergroup ) ) - return new WP_Error( 'invalid', __( "User group doesn't exist.", 'edit-flow' ) ); - - // Encode our extra fields and then store them in the description field - $args_to_encode = array(); - $args_to_encode['description'] = ( isset( $args['description'] ) ) ? $args['description'] : $existing_usergroup->description; - $args_to_encode['user_ids'] = ( is_array( $users ) ) ? $users : $existing_usergroup->user_ids; - $args_to_encode['user_ids'] = array_unique( $args_to_encode['user_ids'] ); - $encoded_description = $this->get_encoded_description( $args_to_encode ); - $args['description'] = $encoded_description; - - $usergroup = wp_update_term( $id, self::taxonomy_key, $args ); - if ( is_wp_error( $usergroup ) ) return $usergroup; - - return $this->get_usergroup_by( 'id', $usergroup['term_id'] ); - } - - /** - * Delete a usergroup based on its term ID - * - * @since 0.7 - * - * @param int $id Unique ID for the Usergroup - * @param bool|WP_Error Returns true on success, WP_Error on failure - */ - function delete_usergroup( $id ) { - - $retval = wp_delete_term( $id, self::taxonomy_key ); - return $retval; - } - - /** - * Add an array of user logins or IDs to a given usergroup - * - * @since 0.7 - * - * @param array $user_ids_or_logins User IDs or logins to be added to the usergroup - * @param int $id Usergroup to perform the action on - * @param bool $reset Delete all of the relationships before adding - * @return bool $success Whether or not we were successful - */ - function add_users_to_usergroup( $user_ids_or_logins, $id, $reset = true ) { - - if ( !is_array( $user_ids_or_logins ) ) - return new WP_Error( 'invalid', __( "Invalid users variable. Should be array.", 'edit-flow' ) ); - - // To dump the existing users from a usergroup, we need to pass an empty array - $usergroup = $this->get_usergroup_by( 'id', $id ); - if ( $reset ) { - $retval = $this->update_usergroup( $id, null, array() ); - if ( is_wp_error( $retval ) ) - return $retval; } - - // Add the new users one by one to an array we'll pass back to the usergroup - $new_users = array(); - foreach ( (array)$user_ids_or_logins as $user_id_or_login ) { - if ( !is_numeric( $user_id_or_login ) ) - $new_users[] = get_user_by( 'login', $user_id_or_login )->ID; - else - $new_users[] = (int)$user_id_or_login; + + /** + * Create a new usergroup containing: + * - Name + * - Slug (prefixed with our special key to avoid conflicts) + * - Description + * - Users + * + * @since 0.7 + * + * @param array $args Name (optional), slug and description for the usergroup + * @param array $user_ids IDs for the users to be added to the Usergroup + * @return object|WP_Error $usergroup Object for the new Usergroup on success, WP_Error otherwise + */ + public function add_usergroup( $args = array(), $user_ids = array() ) { + + if ( ! isset( $args['name'] ) ) { + return new WP_Error( 'invalid', __( 'New user groups must have a name', 'edit-flow' ) ); + } + + $name = $args['name']; + $default = array( + 'name' => '', + 'slug' => self::term_prefix . sanitize_title( $name ), + 'description' => '', + ); + $args = array_merge( $default, $args ); + + // Encode our extra fields and then store them in the description field + $args_to_encode = array( + 'description' => $args['description'], + 'user_ids' => array_unique( $user_ids ), + ); + $encoded_description = $this->get_encoded_description( $args_to_encode ); + $args['description'] = $encoded_description; + $usergroup = wp_insert_term( $name, self::taxonomy_key, $args ); + if ( is_wp_error( $usergroup ) ) { + return $usergroup; + } + + return $this->get_usergroup_by( 'id', $usergroup['term_id'] ); + } + + /** + * Update a usergroup with new data. + * Fields can include: + * - Name + * - Slug (prefixed with our special key, of course) + * - Description + * - Users + * + * @since 0.7 + * + * @param int $id Unique ID for the usergroup + * @param array $args Usergroup meta to update (name, slug, description) + * @param array $users Users to be added to the Usergroup. If set, removes existing users first. + * @return object|WP_Error $usergroup Object for the updated Usergroup on success, WP_Error otherwise + */ + public function update_usergroup( $id, $args = array(), $users = null ) { + + $existing_usergroup = $this->get_usergroup_by( 'id', $id ); + if ( is_wp_error( $existing_usergroup ) ) { + return new WP_Error( 'invalid', __( "User group doesn't exist.", 'edit-flow' ) ); + } + + // Encode our extra fields and then store them in the description field + $args_to_encode = array(); + $args_to_encode['description'] = ( isset( $args['description'] ) ) ? $args['description'] : $existing_usergroup->description; + $args_to_encode['user_ids'] = ( is_array( $users ) ) ? $users : $existing_usergroup->user_ids; + $args_to_encode['user_ids'] = array_unique( $args_to_encode['user_ids'] ); + $encoded_description = $this->get_encoded_description( $args_to_encode ); + $args['description'] = $encoded_description; + + $usergroup = wp_update_term( $id, self::taxonomy_key, $args ); + if ( is_wp_error( $usergroup ) ) { + return $usergroup; + } + + return $this->get_usergroup_by( 'id', $usergroup['term_id'] ); } - $retval = $this->update_usergroup( $id, null, $new_users ); - if ( is_wp_error( $retval ) ) + + /** + * Delete a usergroup based on its term ID + * + * @since 0.7 + * + * @param int $id Unique ID for the Usergroup + * @param bool|WP_Error Returns true on success, WP_Error on failure + */ + public function delete_usergroup( $id ) { + + $retval = wp_delete_term( $id, self::taxonomy_key ); return $retval; - return true; - } - - /** - * Add a given user to a Usergroup. Can use User ID or user login - * - * @since 0.7 - * - * @param int|string $user_id_or_login User ID or login to be added to the Usergroups - * @param int|array $ids ID for the Usergroup(s) - * @return bool|WP_Error $retval Return true on success, WP_Error on error - */ - function add_user_to_usergroup( $user_id_or_login, $ids ) { - - if ( !is_numeric( $user_id_or_login ) ) - $user_id = get_user_by( 'login', $user_id_or_login )->ID; - else - $user_id = (int)$user_id_or_login; - - foreach( (array)$ids as $usergroup_id ) { - $usergroup = $this->get_usergroup_by( 'id', $usergroup_id ); - $usergroup->user_ids[] = $user_id; - $retval = $this->update_usergroup( $usergroup_id, null, $usergroup->user_ids ); - if ( is_wp_error( $retval ) ) - return $retval; } - return true; - } - - /** - * Remove a given user from one or more usergroups - * - * @since 0.7 - * - * @param int|string $user_id_or_login User ID or login to be removed from the Usergroups - * @param int|array $ids ID for the Usergroup(s) - * @return bool|WP_Error $retval Return true on success, WP_Error on error - */ - function remove_user_from_usergroup( $user_id_or_login, $ids ) { - - if ( !is_numeric( $user_id_or_login ) ) - $user_id = get_user_by( 'login', $user_id_or_login )->ID; - else - $user_id = (int)$user_id_or_login; - - // Remove the user from each usergroup specified - foreach( (array)$ids as $usergroup_id ) { - $usergroup = $this->get_usergroup_by( 'id', $usergroup_id ); - // @todo I bet there's a PHP function for this I couldn't look up at 35,000 over the Atlantic - foreach( $usergroup->user_ids as $key => $usergroup_user_id ) { - if ( $usergroup_user_id == $user_id ) - unset( $usergroup->user_ids[$key] ); - } - $retval = $this->update_usergroup( $usergroup_id, null, $usergroup->user_ids ); - if ( is_wp_error( $retval ) ) + + /** + * Add an array of user logins or IDs to a given usergroup + * + * @since 0.7 + * + * @param array $user_ids_or_logins User IDs or logins to be added to the usergroup + * @param int $id Usergroup to perform the action on + * @param bool $reset Delete all of the relationships before adding + * @return bool $success Whether or not we were successful + */ + public function add_users_to_usergroup( $user_ids_or_logins, $id, $reset = true ) { + + if ( ! is_array( $user_ids_or_logins ) ) { + return new WP_Error( 'invalid', __( 'Invalid users variable. Should be array.', 'edit-flow' ) ); + } + + // To dump the existing users from a usergroup, we need to pass an empty array + $usergroup = $this->get_usergroup_by( 'id', $id ); + if ( $reset ) { + $retval = $this->update_usergroup( $id, null, array() ); + if ( is_wp_error( $retval ) ) { + return $retval; + } + } + + // Add the new users one by one to an array we'll pass back to the usergroup + $new_users = array(); + foreach ( (array) $user_ids_or_logins as $user_id_or_login ) { + if ( ! is_numeric( $user_id_or_login ) ) { + $new_users[] = get_user_by( 'login', $user_id_or_login )->ID; + } else { + $new_users[] = (int) $user_id_or_login; + } + } + $retval = $this->update_usergroup( $id, null, $new_users ); + if ( is_wp_error( $retval ) ) { return $retval; + } + return true; } - return true; - - } - - /** - * Get all of the Usergroup ids or objects for a given user - * - * @since 0.7 - * - * @param int|string $user_id_or_login User ID or login to search against - * @param array $ids_or_objects Whether to retrieve an array of IDs or usergroup objects - * @param array|bool $usergroup_objects_or_ids Array of usergroup 'ids' or 'objects', false if none - */ - function get_usergroups_for_user( $user_id_or_login, $ids_or_objects = 'ids' ) { - - if ( !is_numeric( $user_id_or_login ) ) - $user_id = get_user_by( 'login', $user_id_or_login )->ID; - else - $user_id = (int)$user_id_or_login; - - // Unfortunately, the easiest way to do this is get all usergroups - // and then loop through each one to see if the user ID is stored - $all_usergroups = $this->get_usergroups(); - if ( !empty( $all_usergroups) ) { - $usergroup_objects_or_ids = array(); - foreach( $all_usergroups as $usergroup ) { - // Not in this usergroup, so keep going - if ( !in_array( $user_id, $usergroup->user_ids ) ) - continue; - if ( $ids_or_objects == 'ids' ) - $usergroup_objects_or_ids[] = (int)$usergroup->term_id; - else if ( $ids_or_objects == 'objects' ) - $usergroup_objects_or_ids[] = $usergroup; - } - return $usergroup_objects_or_ids; - } else { - return false; + + /** + * Add a given user to a Usergroup. Can use User ID or user login + * + * @since 0.7 + * + * @param int|string $user_id_or_login User ID or login to be added to the Usergroups + * @param int|array $ids ID for the Usergroup(s) + * @return bool|WP_Error $retval Return true on success, WP_Error on error + */ + public function add_user_to_usergroup( $user_id_or_login, $ids ) { + + if ( ! is_numeric( $user_id_or_login ) ) { + $user_id = get_user_by( 'login', $user_id_or_login )->ID; + } else { + $user_id = (int) $user_id_or_login; + } + + foreach ( (array) $ids as $usergroup_id ) { + $usergroup = $this->get_usergroup_by( 'id', $usergroup_id ); + $usergroup->user_ids[] = $user_id; + $retval = $this->update_usergroup( $usergroup_id, null, $usergroup->user_ids ); + if ( is_wp_error( $retval ) ) { + return $retval; + } + } + return true; + } + + /** + * Remove a given user from one or more usergroups + * + * @since 0.7 + * + * @param int|string $user_id_or_login User ID or login to be removed from the Usergroups + * @param int|array $ids ID for the Usergroup(s) + * @return bool|WP_Error $retval Return true on success, WP_Error on error + */ + public function remove_user_from_usergroup( $user_id_or_login, $ids ) { + + if ( ! is_numeric( $user_id_or_login ) ) { + $user_id = get_user_by( 'login', $user_id_or_login )->ID; + } else { + $user_id = (int) $user_id_or_login; + } + + // Remove the user from each usergroup specified + foreach ( (array) $ids as $usergroup_id ) { + $usergroup = $this->get_usergroup_by( 'id', $usergroup_id ); + // @todo I bet there's a PHP function for this I couldn't look up at 35,000 over the Atlantic + foreach ( $usergroup->user_ids as $key => $usergroup_user_id ) { + if ( $usergroup_user_id == $user_id ) { + unset( $usergroup->user_ids[ $key ] ); + } + } + $retval = $this->update_usergroup( $usergroup_id, null, $usergroup->user_ids ); + if ( is_wp_error( $retval ) ) { + return $retval; + } + } + return true; + } + + /** + * Get all of the Usergroup ids or objects for a given user + * + * @since 0.7 + * + * @param int|string $user_id_or_login User ID or login to search against + * @param array $ids_or_objects Whether to retrieve an array of IDs or usergroup objects + * @param array|bool $usergroup_objects_or_ids Array of usergroup 'ids' or 'objects', false if none + */ + public function get_usergroups_for_user( $user_id_or_login, $ids_or_objects = 'ids' ) { + + if ( ! is_numeric( $user_id_or_login ) ) { + $user_id = get_user_by( 'login', $user_id_or_login )->ID; + } else { + $user_id = (int) $user_id_or_login; + } + + // Unfortunately, the easiest way to do this is get all usergroups + // and then loop through each one to see if the user ID is stored + $all_usergroups = $this->get_usergroups(); + if ( ! empty( $all_usergroups ) ) { + $usergroup_objects_or_ids = array(); + foreach ( $all_usergroups as $usergroup ) { + // Not in this usergroup, so keep going + if ( ! in_array( $user_id, $usergroup->user_ids ) ) { + continue; + } + if ( 'ids' == $ids_or_objects ) { + $usergroup_objects_or_ids[] = (int) $usergroup->term_id; + } else if ( 'objects' == $ids_or_objects ) { + $usergroup_objects_or_ids[] = $usergroup; + } + } + return $usergroup_objects_or_ids; + } else { + return false; + } } } - -} } +// phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound -if ( !class_exists( 'EF_Usergroups_List_Table' ) ) { -/** - * Usergroups uses WordPress' List Table API for generating the Usergroup management table - * - * @since 0.7 - */ -class EF_Usergroups_List_Table extends WP_List_Table -{ - - var $callback_args; - - function __construct() { - - parent::__construct( array( - 'plural' => 'user groups', - 'singular' => 'user group', - 'ajax' => true - ) ); - - } - +if ( ! class_exists( 'EF_Usergroups_List_Table' ) ) { /** - * @todo Paginate if we have a lot of usergroups + * Usergroups uses WordPress' List Table API for generating the Usergroup management table * * @since 0.7 */ - function prepare_items() { - global $edit_flow; - - $columns = $this->get_columns(); - $hidden = array(); - $sortable = array(); - - $this->_column_headers = array( $columns, $hidden, $sortable ); - - $this->items = $edit_flow->user_groups->get_usergroups(); - - $this->set_pagination_args( array( - 'total_items' => count( $this->items ), - 'per_page' => count( $this->items ), - ) ); - } + class EF_Usergroups_List_Table extends WP_List_Table { - /** - * Message to be displayed when there are no usergroups - * - * @since 0.7 - */ - function no_items() { - _e( 'No user groups found.', 'edit-flow' ); - } - - /** - * Columns in our Usergroups table - * - * @since 0.7 - */ - function get_columns() { - - $columns = array( - 'name' => __( 'Name', 'edit-flow' ), - 'description' => __( 'Description', 'edit-flow' ), - 'users' => __( 'Users in Group', 'edit-flow' ), - ); - - return $columns; - } - - /** - * Process the Usergroup column value for all methods that aren't registered - * - * @since 0.7 - */ - function column_default( $usergroup, $column_name ) { - - } - - /** - * Process the Usergroup name column value. - * Displays the name of the Usergroup, and action links - * - * @since 0.7 - */ - function column_name( $usergroup ) { - global $edit_flow; - - // @todo direct edit link - $output = '' . esc_html( $usergroup->name ) . ''; - - $actions = array(); - $actions['edit edit-usergroup'] = sprintf( '' . __( 'Edit', 'edit-flow' ) . '', $edit_flow->user_groups->get_link( array( 'action' => 'edit-usergroup', 'usergroup-id' => $usergroup->term_id ) ) ); - $actions['inline hide-if-no-js'] = '' . __( 'Quick Edit' ) . ''; - $actions['delete delete-usergroup'] = sprintf( '' . __( 'Delete', 'edit-flow' ) . '', $edit_flow->user_groups->get_link( array( 'action' => 'delete-usergroup', 'usergroup-id' => $usergroup->term_id ) ) ); - - $output .= $this->row_actions( $actions, false ); - $output .= ''; - - return $output; - - } - - /** - * Handle the 'description' column for the table of Usergroups - * Don't need to unencode this because we already did when the usergroup was loaded - * - * @since 0.7 - */ - function column_description( $usergroup ) { - return esc_html( $usergroup->description ); - } - - /** - * Show the "Total Users" in a given usergroup - * - * @since 0.7 - */ - function column_users( $usergroup ) { - global $edit_flow; - return '' . count( $usergroup->user_ids ) . ''; - } - - /** - * Prepare a single row of information about a usergroup - * - * @since 0.7 - */ - function single_row( $usergroup ) { - static $row_class = ''; - $row_class = ( $row_class == '' ? ' class="alternate"' : '' ); + protected $callback_args; - echo ''; - echo $this->single_row_columns( $usergroup ); - echo ''; - } - - /** - * If we use this form, we can have inline editing! - * - * @since 0.7 - */ - function inline_edit() { - global $edit_flow; -?> + public function __construct() { + + parent::__construct( array( + 'plural' => 'user groups', + 'singular' => 'user group', + 'ajax' => true, + ) ); + } + + /** + * @todo Paginate if we have a lot of usergroups + * + * @since 0.7 + */ + public function prepare_items() { + global $edit_flow; + + $columns = $this->get_columns(); + $hidden = array(); + $sortable = array(); + + $this->_column_headers = array( $columns, $hidden, $sortable ); + + $this->items = $edit_flow->user_groups->get_usergroups(); + + $this->set_pagination_args( array( + 'total_items' => count( $this->items ), + 'per_page' => count( $this->items ), + ) ); + } + + /** + * Message to be displayed when there are no usergroups + * + * @since 0.7 + */ + public function no_items() { + _e( 'No user groups found.', 'edit-flow' ); + } + + /** + * Columns in our Usergroups table + * + * @since 0.7 + */ + public function get_columns() { + + $columns = array( + 'name' => __( 'Name', 'edit-flow' ), + 'description' => __( 'Description', 'edit-flow' ), + 'users' => __( 'Users in Group', 'edit-flow' ), + ); + + return $columns; + } + + /** + * Process the Usergroup column value for all methods that aren't registered + * + * @since 0.7 + */ + public function column_default( $usergroup, $column_name ) { + } + + /** + * Process the Usergroup name column value. + * Displays the name of the Usergroup, and action links + * + * @since 0.7 + */ + public function column_name( $usergroup ) { + global $edit_flow; + + // @todo direct edit link + $output = '' . esc_html( $usergroup->name ) . ''; + + $actions = array(); + $actions['edit edit-usergroup'] = sprintf( '' . __( 'Edit', 'edit-flow' ) . '', $edit_flow->user_groups->get_link( array( + 'action' => 'edit-usergroup', + 'usergroup-id' => $usergroup->term_id, + ) ) ); + $actions['inline hide-if-no-js'] = '' . __( 'Quick Edit' ) . ''; + $actions['delete delete-usergroup'] = sprintf( '' . __( 'Delete', 'edit-flow' ) . '', $edit_flow->user_groups->get_link( array( + 'action' => 'delete-usergroup', + 'usergroup-id' => $usergroup->term_id, + ) ) ); + + $output .= $this->row_actions( $actions, false ); + $output .= ''; + + return $output; + } + + /** + * Handle the 'description' column for the table of Usergroups + * Don't need to unencode this because we already did when the usergroup was loaded + * + * @since 0.7 + */ + public function column_description( $usergroup ) { + return esc_html( $usergroup->description ); + } + + /** + * Show the "Total Users" in a given usergroup + * + * @since 0.7 + */ + public function column_users( $usergroup ) { + global $edit_flow; + return '' . count( $usergroup->user_ids ) . ''; + } + + /** + * Prepare a single row of information about a usergroup + * + * @since 0.7 + */ + public function single_row( $usergroup ) { + static $row_class = ''; + $row_class = ( '' == $row_class ? ' class="alternate"' : '' ); + + echo wp_kses_post( '' ); + echo wp_kses_post( $this->single_row_columns( $usergroup ) ); + echo ''; + } + + /** + * If we use this form, we can have inline editing! + * + * @since 0.7 + */ + public function inline_edit() { + global $edit_flow; + ?>
    -
    - + + + + + + + + + + + + + + + + + + . */node_modules/* - */nodeapp/* */vendor/* - */wordpress/* + */tests/* diff --git a/vipgo-helper.php b/vipgo-helper.php index be0166473..5603fe950 100644 --- a/vipgo-helper.php +++ b/vipgo-helper.php @@ -9,10 +9,18 @@ * them via filters. */ add_filter( 'ef_kill_add_caps_to_role', '__return_true' ); -add_filter( 'ef_view_calendar_cap', function() {return 'edit_posts'; } ); -add_filter( 'ef_view_story_budget_cap', function() { return 'edit_posts'; } ); -add_filter( 'ef_edit_post_subscriptions_cap', function() { return 'edit_others_posts'; } ); -add_filter( 'ef_manage_usergroups_cap', function() { return 'manage_options'; } ); +add_filter( 'ef_view_calendar_cap', function () { + return 'edit_posts'; +} ); +add_filter( 'ef_view_story_budget_cap', function () { + return 'edit_posts'; +} ); +add_filter( 'ef_edit_post_subscriptions_cap', function () { + return 'edit_others_posts'; +} ); +add_filter( 'ef_manage_usergroups_cap', function () { + return 'manage_options'; +} ); /** * Edit Flow loads modules after plugins_loaded, which has already been fired when loading via wpcom_vip_load_plugins diff --git a/wpcom-helper.php b/wpcom-helper.php index 519b5eb93..7bd595d2c 100644 --- a/wpcom-helper.php +++ b/wpcom-helper.php @@ -9,10 +9,18 @@ * them with the WP.com + core caps approach */ add_filter( 'ef_kill_add_caps_to_role', '__return_true' ); -add_filter( 'ef_view_calendar_cap', function() { return 'edit_posts'; } ); -add_filter( 'ef_view_story_budget_cap', function() { return 'edit_posts'; } ); -add_filter( 'ef_edit_post_subscriptions_cap', function() { return 'edit_others_posts'; } ); -add_filter( 'ef_manage_usergroups_cap', function() { return 'manage_options'; } ); +add_filter( 'ef_view_calendar_cap', function () { + return 'edit_posts'; +} ); +add_filter( 'ef_view_story_budget_cap', function () { + return 'edit_posts'; +} ); +add_filter( 'ef_edit_post_subscriptions_cap', function () { + return 'edit_others_posts'; +} ); +add_filter( 'ef_manage_usergroups_cap', function () { + return 'manage_options'; +} ); /** * Edit Flow loads modules after plugins_loaded, which has already been fired on WP.com @@ -52,4 +60,4 @@ function edit_flow_fix_fix_post_name( $post ) { } return $post; -} \ No newline at end of file +}