capabilities ) ) ) || $force ) { fictioneer_setup_roles(); } // If this capability is missing, the roles need to be updated if ( $administrator && ! in_array( 'fcn_crosspost', array_keys( $administrator->capabilities ) ) ) { get_role( 'administrator' )->add_cap( 'fcn_custom_page_header' ); get_role( 'administrator' )->add_cap( 'fcn_custom_epub_upload' ); get_role( 'administrator' )->add_cap( 'fcn_unlock_posts' ); get_role( 'administrator' )->add_cap( 'fcn_expire_passwords' ); get_role( 'administrator' )->add_cap( 'fcn_crosspost' ); if ( $editor = get_role( 'editor' ) ) { $editor->add_cap( 'fcn_custom_page_header' ); $editor->add_cap( 'fcn_custom_epub_upload' ); } if ( $moderator = get_role( 'fcn_moderator' ) ) { $moderator->add_cap( 'fcn_only_moderate_comments' ); $moderator->add_cap( 'fcn_custom_epub_upload' ); } if ( $author = get_role( 'author' ) ) { $author->add_cap( 'fcn_custom_epub_upload' ); } } } add_action( 'admin_init', 'fictioneer_initialize_roles' ); /** * Build user roles with custom capabilities * * @since 5.6.0 */ function fictioneer_setup_roles() { // Capabilities $all = array_merge( FICTIONEER_BASE_CAPABILITIES, FICTIONEER_TAXONOMY_CAPABILITIES, FICTIONEER_STORY_CAPABILITIES, FICTIONEER_CHAPTER_CAPABILITIES, FICTIONEER_COLLECTION_CAPABILITIES, FICTIONEER_RECOMMENDATION_CAPABILITIES ); // Administrator $administrator = get_role( 'administrator' ); $administrator->remove_cap( 'fcn_only_moderate_comments' ); $administrator->remove_cap( 'fcn_reduced_profile' ); $administrator->remove_cap( 'fcn_allow_self_delete' ); $administrator->remove_cap( 'fcn_upload_limit' ); $administrator->remove_cap( 'fcn_upload_restrictions' ); foreach ( $all as $cap ) { $administrator->add_cap( $cap ); } // Editor $editor = get_role( 'editor' ); $editor_caps = array_merge( // Base array( 'fcn_read_others_files', 'fcn_edit_others_files', 'fcn_delete_others_files', 'fcn_admin_panel_access', 'fcn_adminbar_access', 'fcn_dashboard_access', 'fcn_seo_meta', 'fcn_make_sticky', 'fcn_edit_permalink', 'fcn_all_blocks', 'fcn_story_pages', 'fcn_edit_date', 'fcn_custom_page_header', 'fcn_custom_epub_upload', 'moderate_comments', // Legacy restore 'edit_comment', // Legacy restore 'edit_pages', // Legacy restore 'delete_pages', // Legacy restore 'delete_published_pages', // Legacy restore 'delete_published_posts', // Legacy restore 'delete_others_pages', // Legacy restore 'delete_others_posts', // Legacy restore 'publish_pages', // Legacy restore 'publish_posts', // Legacy restore 'manage_categories', // Legacy restore 'unfiltered_html', // Legacy restore 'manage_links', // Legacy restore ), FICTIONEER_TAXONOMY_CAPABILITIES, FICTIONEER_STORY_CAPABILITIES, FICTIONEER_CHAPTER_CAPABILITIES, FICTIONEER_COLLECTION_CAPABILITIES, FICTIONEER_RECOMMENDATION_CAPABILITIES ); foreach ( $editor_caps as $cap ) { $editor->add_cap( $cap ); } // Author $author = get_role( 'author' ); $author_caps = array( // Base 'fcn_admin_panel_access', 'fcn_adminbar_access', 'fcn_allow_self_delete', 'fcn_upload_limit', 'fcn_upload_restrictions', 'fcn_story_pages', 'fcn_custom_epub_upload', // Stories 'read_fcn_story', 'edit_fcn_stories', 'publish_fcn_stories', 'delete_fcn_stories', 'delete_published_fcn_stories', 'edit_published_fcn_stories', // Chapters 'read_fcn_chapter', 'edit_fcn_chapters', 'publish_fcn_chapters', 'delete_fcn_chapters', 'delete_published_fcn_chapters', 'edit_published_fcn_chapters', // Collections 'read_fcn_collection', 'edit_fcn_collections', 'publish_fcn_collections', 'delete_fcn_collections', 'delete_published_fcn_collections', 'edit_published_fcn_collections', // Recommendations 'read_fcn_recommendation', 'edit_fcn_recommendations', 'publish_fcn_recommendations', 'delete_fcn_recommendations', 'delete_published_fcn_recommendations', 'edit_published_fcn_recommendations', // Taxonomies 'manage_categories', 'manage_post_tags', 'manage_fcn_genres', 'manage_fcn_fandoms', 'manage_fcn_characters', 'manage_fcn_content_warnings', 'assign_categories', 'assign_post_tags', 'assign_fcn_genres', 'assign_fcn_fandoms', 'assign_fcn_characters', 'assign_fcn_content_warnings' ); $author->remove_cap( 'fcn_reduced_profile' ); foreach ( $author_caps as $cap ) { $author->add_cap( $cap ); } // Contributor $contributor = get_role( 'contributor' ); $contributor_caps = array( // Base 'fcn_admin_panel_access', 'fcn_adminbar_access', 'fcn_allow_self_delete', 'fcn_upload_limit', 'fcn_upload_restrictions', 'fcn_story_pages', // Stories 'read_fcn_story', 'edit_fcn_stories', 'delete_fcn_stories', 'edit_published_fcn_stories', // Chapters 'read_fcn_chapter', 'edit_fcn_chapters', 'delete_fcn_chapters', 'edit_published_fcn_chapters', // Collections 'read_fcn_collection', 'edit_fcn_collections', 'delete_fcn_collections', 'edit_published_fcn_collections', // Recommendations 'read_fcn_recommendation', 'edit_fcn_recommendations', 'delete_fcn_recommendations', 'edit_published_fcn_recommendations', // Taxonomies 'manage_categories', 'manage_post_tags', 'manage_fcn_genres', 'manage_fcn_fandoms', 'manage_fcn_characters', 'manage_fcn_content_warnings', 'assign_categories', 'assign_post_tags', 'assign_fcn_genres', 'assign_fcn_fandoms', 'assign_fcn_characters', 'assign_fcn_content_warnings' ); $contributor->remove_cap( 'fcn_reduced_profile' ); foreach ( $contributor_caps as $cap ) { $contributor->add_cap( $cap ); } // Moderator fictioneer_add_moderator_role(); // Subscriber $subscriber = get_role( 'subscriber' ); $subscriber_caps = array( // Base 'fcn_admin_panel_access', 'fcn_reduced_profile', 'fcn_allow_self_delete', 'fcn_upload_limit', 'fcn_upload_restrictions', // Stories 'read_fcn_story', // Chapters 'read_fcn_chapter', // Collections 'read_fcn_collection', // Recommendations 'read_fcn_recommendation' ); foreach ( $subscriber_caps as $cap ) { $subscriber->add_cap( $cap ); } } /** * Add custom moderator role * * @since 5.0.0 */ function fictioneer_add_moderator_role() { $moderator = get_role( 'fcn_moderator' ); $caps = array( // Base 'read' => true, 'edit_posts' => true, 'edit_others_posts' => true, 'edit_published_posts' => true, 'moderate_comments' => true, 'edit_comment' => true, 'delete_posts' => true, 'delete_others_posts' => true, 'fcn_admin_panel_access' => true, 'fcn_adminbar_access' => true, 'fcn_only_moderate_comments' => true, 'fcn_upload_limit' => true, 'fcn_upload_restrictions' => true, 'fcn_show_badge' => true, 'fcn_story_pages' => true, 'fcn_custom_epub_upload' => true, // Stories 'read_fcn_story' => true, 'edit_fcn_stories' => true, 'publish_fcn_stories' => true, 'delete_fcn_stories' => true, 'delete_published_fcn_stories' => true, 'edit_published_fcn_stories' => true, 'edit_others_fcn_stories' => true, // Chapters 'read_fcn_chapter' => true, 'edit_fcn_chapters' => true, 'publish_fcn_chapters' => true, 'delete_fcn_chapters' => true, 'delete_published_fcn_chapters' => true, 'edit_published_fcn_chapters' => true, 'edit_others_fcn_chapters' => true, // Collections 'read_fcn_collection' => true, 'edit_fcn_collections' => true, 'publish_fcn_collections' => true, 'delete_fcn_collections' => true, 'delete_published_fcn_collections' => true, 'edit_published_fcn_collections' => true, 'edit_others_fcn_collections' => true, // Recommendations 'read_fcn_recommendation' => true, 'edit_fcn_recommendations' => true, 'publish_fcn_recommendations' => true, 'delete_fcn_recommendations' => true, 'delete_published_fcn_recommendations' => true, 'edit_published_fcn_recommendations' => true, 'edit_others_fcn_recommendations' => true, // Taxonomies 'manage_categories' => true, 'manage_post_tags' => true, 'manage_fcn_genres' => true, 'manage_fcn_fandoms' => true, 'manage_fcn_characters' => true, 'manage_fcn_content_warnings', 'assign_categories' => true, 'assign_post_tags' => true, 'assign_fcn_genres' => true, 'assign_fcn_fandoms' => true, 'assign_fcn_characters' => true, 'assign_fcn_content_warnings' => true ); if ( $moderator ) { $caps = array_keys( $caps ); foreach ( $caps as $cap ) { $moderator->add_cap( $cap ); } // Already exists return null; } // Add return add_role( 'fcn_moderator', __( 'Moderator', 'fictioneer' ), $caps ); } // ============================================================================= // APPLY CAPABILITY RULES // ============================================================================= // Add templates ('name' => 'Display Name') to the array you want to allow if ( ! defined( 'FICTIONEER_ALLOWED_PAGE_TEMPLATES' ) ) { define( 'FICTIONEER_ALLOWED_PAGE_TEMPLATES', [] ); } /** * Exceptions for post passwords * * @since 5.12.3 * @since 5.15.0 - Add Patreon checks. * @since 5.16.0 - Add Patreon unlock checks and static variable cache. * * @param bool $required Whether the user needs to supply a password. * @param WP_Post $post Post object. * * @return bool True or false. */ function fictioneer_bypass_password( $required, $post ) { // Already unlocked if ( ! $required ) { return $required; } // Ensure to skip search, list pages, and nested loops if ( ( ! wp_doing_ajax() || ! ( $_GET['post_id'] ?? 0 ) ) && ( $_REQUEST['action'] ?? 0 ) !== 'fictioneer_ajax_submit_comment' && ! ( $_REQUEST['comment_post_ID'] ?? 0 ) ) { if ( ! is_singular() || get_queried_object_id() != $post->ID ) { return $required; } } // Static variable cache static $cache = []; $cache_key = $post->ID . '_' . get_current_user_id() . '_' . (int) $required; if ( isset( $cache[ $cache_key ] ) ) { return $cache[ $cache_key ]; } // Default remove_filter( 'post_password_required', 'fictioneer_bypass_password' ); $required = post_password_required( $post ); add_filter( 'post_password_required', 'fictioneer_bypass_password', 10, 2 ); // Notify cache plugins to NOT cache the page regardless of access if ( $required ) { // LiteSpeed Cache do_action( 'litespeed_control_set_nocache', 'nocache due to password protection bypass.' ); // WP Super Cache, W3 Total Cache, Hummingbird, and probably more if ( ! defined( 'DONOTCACHEPAGE' ) ) { define( 'DONOTCACHEPAGE', true ); } // Cache Enabler add_filter( 'cache_enabler_bypass_cache', '__return_true' ); // WP Rocket add_filter( 'do_rocket_generate_caching_files', '__return_false' ); } // Always allow admins if ( current_user_can( 'manage_options' ) ) { $cache[ $cache_key ] = false; return false; } // Setup $user = wp_get_current_user(); $patreon_user_data = fictioneer_get_user_patreon_data( $user->ID ); // Can be an empty array // Check capability per post type... switch ( $post->post_type ) { case 'post': $required = current_user_can( 'fcn_ignore_post_passwords' ) ? false : $required; break; case 'page': $required = current_user_can( 'fcn_ignore_page_passwords' ) ? false : $required; break; case 'fcn_story': $required = current_user_can( 'fcn_ignore_fcn_story_passwords' ) ? false : $required; break; case 'fcn_chapter': $required = current_user_can( 'fcn_ignore_fcn_chapter_passwords' ) ? false : $required; break; case 'fcn_collection': $required = current_user_can( 'fcn_ignore_fcn_collection_passwords' ) ? false : $required; break; } // Check Patreon tiers if ( $user && $required && get_option( 'fictioneer_enable_patreon_locks' ) && ( $patreon_user_data['valid'] ?? 0 ) ) { $patreon_post_data = fictioneer_get_post_patreon_data( $post ); // If there is anything to check... if ( $patreon_post_data['gated'] ) { $patreon_check_amount_cents = $patreon_post_data['gate_cents'] < 1 ? 999999999999 : $patreon_post_data['gate_cents']; $patreon_check_lifetime_amount_cents = $patreon_post_data['gate_lifetime_cents'] < 1 ? 999999999999 : $patreon_post_data['gate_lifetime_cents']; foreach ( $patreon_user_data['tiers'] as $tier ) { $required = ! ( in_array( $tier['id'], $patreon_post_data['gate_tiers'] ) || ( $tier['amount_cents'] ?? -1 ) >= $patreon_check_amount_cents || ( $patreon_user_data['lifetime_support_cents'] ?? -1 ) >= $patreon_check_lifetime_amount_cents ); $required = apply_filters( 'fictioneer_filter_patreon_tier_unlock', $required, $post, $user, $patreon_post_data ); if ( ! $required ) { break; } } } } // Check unlocked posts if ( $user && $required ) { $story_id = fictioneer_get_chapter_story_id( $post->ID ); $unlocks = get_user_meta( $user->ID, 'fictioneer_post_unlocks', true ) ?: []; $unlocks = is_array( $unlocks ) ? $unlocks : []; $lock_gate_amount = get_option( 'fictioneer_patreon_global_lock_unlock_amount', 0 ) ?: 0; $allow_unlocks = ! ( get_option( 'fictioneer_enable_patreon_locks' ) && $lock_gate_amount ); // Check Patreon unlock gate if ( ! $allow_unlocks && ( $patreon_user_data['valid'] ?? 0 ) ) { foreach ( $patreon_user_data['tiers'] as $tier ) { if ( ( $tier['amount_cents'] ?? -1 ) >= $lock_gate_amount ) { $allow_unlocks = true; break; } } } if ( $allow_unlocks && $unlocks && array_intersect( [ $post->ID, $story_id ], $unlocks ) ) { $required = false; } } // Cache $cache[ $cache_key ] = $required; // Continue filter return $required; } add_filter( 'post_password_required', 'fictioneer_bypass_password', 10, 2 ); // No restriction can be applied to administrators if ( ! current_user_can( 'manage_options' ) ) { // === FCN_ADMINBAR_ACCESS =================================================== /** * Admin bar */ if ( ! current_user_can( 'fcn_adminbar_access' ) ) { add_filter( 'show_admin_bar', '__return_false' ); } // === FCN_ADMIN_PANEL_ACCESS ================================================ /** * Prevent access to the admin panel * * @since 5.6.0 */ function fictioneer_restrict_admin_panel() { // Allow all AJAX requests if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) { return; } // Allow all admin_post actions if ( isset( $_REQUEST['action'] ) && ( $_SERVER['PHP_SELF'] == '/wp-admin/admin-post.php' || $_SERVER['PHP_SELF'] == '/wp-admin/admin-ajax.php' ) ) { return; } // Redirect back to Home if ( is_admin() && ! current_user_can( 'fcn_admin_panel_access' ) ) { wp_redirect( home_url() ); exit; } } add_filter( 'init', 'fictioneer_restrict_admin_panel' ); // === FCN_DASHBOARD_ACCESS ================================================== /** * Remove admin dashboard widgets * * @since 5.6.0 */ function fictioneer_remove_dashboard_widgets() { global $wp_meta_boxes; // Remove all $wp_meta_boxes['dashboard']['normal']['core'] = []; $wp_meta_boxes['dashboard']['side']['core'] = []; // Remove actions remove_action( 'welcome_panel', 'wp_welcome_panel' ); } /** * Remove the dashboard menu page * * @since 5.6.0 */ function fictioneer_remove_dashboard_menu() { remove_menu_page( 'index.php' ); remove_meta_box( 'dashboard_plugins', 'dashboard', 'normal' ); } /** * Redirect from dashboard to user profile * * @since 5.6.0 */ function fictioneer_skip_dashboard() { global $pagenow; // Unless it's AJAX or an administrator... if ( $pagenow == 'index.php' && ! ( defined( 'DOING_AJAX' ) && DOING_AJAX ) ) { // Skip dashboard, go to user profile wp_redirect( home_url( '/wp-admin/profile.php' ) ); exit; } } /** * Remove dashboard from admin bar dropdown * * @since 5.6.0 */ function fictioneer_remove_dashboard_from_admin_bar() { global $wp_admin_bar; $wp_admin_bar->remove_menu( 'dashboard' ); } if ( ! current_user_can( 'fcn_dashboard_access' ) ) { add_action( 'wp_before_admin_bar_render', 'fictioneer_remove_dashboard_from_admin_bar' ); add_action( 'wp_dashboard_setup', 'fictioneer_remove_dashboard_widgets' ); add_action( 'admin_menu', 'fictioneer_remove_dashboard_menu' ); add_action( 'admin_init', 'fictioneer_skip_dashboard' ); } // === FCN_SELECT_PAGE_TEMPLATE ============================================== /** * Prevents parent and order from being updated * * @param array $data An array of slashed, sanitized, and processed post data. * * @return array The potentially modified post data. */ function fictioneer_prevent_parent_and_order_update( $data ) { // Remove items unset( $data['post_parent'] ); unset( $data['menu_order'] ); // Continue filter return $data; } /** * Filters the page template selection * * @since 5.6.0 * * @param array $templates Array of templates ('name' => 'Display Name'). * * @return array Array of allowed templates. */ function fictioneer_disallow_page_template_select( $templates ) { // Trim default template selection $templates = array_intersect_key( $templates, FICTIONEER_ALLOWED_PAGE_TEMPLATES ) ?: []; // Continue filter return $templates; } /** * Prevents update of page template based on conditions * * If the user lacks permission and the page template is not listed * as allowed for everyone, the meta update is stopped. * * @since 5.6.2 * * @param null|mixed $check Whether to allow updating metadata for the given type. * @param int $object_id ID of the object metadata is for. * @param string $meta_key Metadata key. * @param mixed $meta_value Metadata value. Must be serializable if non-scalar. * * @return mixed Null if allowed (yes), literally anything else if not. */ function fictioneer_prevent_page_template_update( $check, $object_id, $meta_key, $meta_value ) { // Page template update? if ( $meta_key !== '_wp_page_template' ) { return $check; } // Permission to change the page template? if ( ! current_user_can( 'fcn_select_page_template' ) && ! in_array( $meta_value, array_keys( FICTIONEER_ALLOWED_PAGE_TEMPLATES ) ) ) { return false; } // Continue filter return $check; } // Run before other hooks or bad things will happen! if ( ! current_user_can( 'fcn_select_page_template' ) ) { add_filter( 'update_post_metadata', 'fictioneer_prevent_page_template_update', 1, 4 ); add_filter( 'theme_templates', 'fictioneer_disallow_page_template_select', 1 ); add_filter( 'wp_insert_post_data', 'fictioneer_prevent_parent_and_order_update', 1 ); } // === MODERATE_COMMENTS ===================================================== /** * Remove comment item from admin bar * * @since 5.6.0 */ function fictioneer_remove_comments_from_admin_bar() { global $wp_admin_bar; $wp_admin_bar->remove_node( 'comments' ); } /** * Remove comments menu * * @since 5.6.0 */ function fictioneer_remove_comments_menu_page() { remove_menu_page( 'edit-comments.php' ); } /** * Restrict menu access for non-administrators * * @since 5.6.0 */ function fictioneer_restrict_comment_edit() { // Setup $screen = get_current_screen(); if ( $screen->id === 'edit-comments' ) { wp_die( __( 'Access denied.', 'fictioneer' ) ); } } /** * Remove comments column * * @since 5.6.0 * * @param array $columns The table columns. * * @return array Modified table column. */ function fictioneer_remove_comments_column( $columns ) { if ( isset( $columns['comments'] ) ) { unset( $columns['comments'] ); } return $columns; } if ( ! current_user_can( 'moderate_comments' ) ) { add_action( 'admin_menu', 'fictioneer_remove_comments_menu_page' ); add_action( 'wp_before_admin_bar_render', 'fictioneer_remove_comments_from_admin_bar' ); add_action( 'current_screen', 'fictioneer_restrict_comment_edit' ); add_filter( 'manage_posts_columns', 'fictioneer_remove_comments_column' ); add_filter( 'manage_pages_columns', 'fictioneer_remove_comments_column' ); } /** * Only allow editing of comments * * @since 5.6.0 * * @param array $all_caps An array of all the user's capabilities. * @param array $cap Primitive capabilities that are being checked. * @param array $args Arguments passed to the capabilities check. * * @return array Modified capabilities array. */ function fictioneer_edit_only_comments( $all_caps, $cap, $args ) { if ( ( $args[0] ?? 0 ) == 'edit_post' && isset( $args[2] ) ) { $post = get_post( $args[2] ); $current_user_id = get_current_user_id(); // Remove capability if not a comment if ( ! empty( $post ) && $post->post_type !== 'comment' && $post->post_author != $current_user_id ) { $all_caps[ $cap[0] ] = false; } } return $all_caps; } if ( current_user_can( 'moderate_comments' ) && current_user_can( 'fcn_only_moderate_comments' ) ) { add_filter( 'user_has_cap', 'fictioneer_edit_only_comments', 10, 3 ); } /** * Restrict comment editing * * @since 5.7.3 * * @param array $caps Primitive capabilities required of the user. * @param string $cap Capability being checked. * @param int $user_id The user ID. * * @return array The still allowed primitive capabilities of the user. */ function fictioneer_edit_comments( $caps, $cap, $user_id ) { // Skip unrelated capabilities if ( $cap !== 'edit_comment' ) { return $caps; } // Get user $user = get_userdata( $user_id ); // Check capabilities if ( $user && $user->has_cap( 'moderate_comments' ) ) { return $caps; } // Disallow return ['do_not_allow']; } if ( ! current_user_can( 'moderate_comments' ) ) { add_filter( 'map_meta_cap', 'fictioneer_edit_comments', 10, 3 ); } // === MANAGE_OPTIONS ======================================================== /** * Reduce admin panel * * @since 5.6.0 */ function fictioneer_reduce_admin_panel() { // Remove menu pages remove_menu_page( 'tools.php' ); remove_menu_page( 'plugins.php' ); remove_menu_page( 'themes.php' ); } /** * Restrict menu access for non-administrators * * @since 5.6.0 */ function fictioneer_restrict_admin_only_pages() { // Setup $screen = get_current_screen(); // No access for non-administrators... if ( in_array( $screen->id, ['tools', 'export', 'import', 'site-health', 'export-personal-data', 'erase-personal-data', 'themes', 'customize', 'nav-menus', 'theme-editor', 'options-general'] ) ) { wp_die( __( 'Access denied.', 'fictioneer' ) ); } } if ( ! current_user_can( 'manage_options' ) ) { add_action( 'admin_menu', 'fictioneer_reduce_admin_panel' ); add_action( 'current_screen', 'fictioneer_restrict_admin_only_pages' ); } // === UPDATE_CORE =========================================================== /** * Remove update notice * * @since 5.6.0 */ function fictioneer_remove_update_notice(){ remove_action( 'admin_notices', 'update_nag', 3 ); } if ( ! current_user_can( 'update_core' ) ) { add_action( 'admin_head', 'fictioneer_remove_update_notice' ); } // === FCN_SHORTCODES ======================================================== /** * Strip shortcodes from content before saving to database * * Note: The user can still use shortcodes on pages that already have them. * This is not ideal, but an edge case. Someone who cannot use shortcodes * usually also cannot edit others posts. * * @since 5.6.0 * * @param array $data An array of slashed, sanitized, and processed post data. * * @return array Modified post data with shortcodes removed. */ function fictioneer_strip_shortcodes_on_save( $data ) { // Check permissions if ( current_user_can( 'fcn_shortcodes' ) || get_current_user_id() !== (int) $data['post_author'] ) { return $data; } // Exempt certain shortcodes add_filter( 'strip_shortcodes_tagnames', 'fictioneer_exempt_shortcodes_from_removal' ); // Strip the shortcodes $data['post_content'] = strip_shortcodes( $data['post_content'] ); // Only do this for the trigger post or bad things can happen! remove_filter( 'wp_insert_post_data', 'fictioneer_strip_shortcodes_on_save', 1 ); remove_filter( 'strip_shortcodes_tagnames', 'fictioneer_exempt_shortcodes_from_removal' ); // Continue filter return $data; } if ( ! current_user_can( 'fcn_shortcodes' ) ) { add_filter( 'wp_insert_post_data', 'fictioneer_strip_shortcodes_on_save', 1 ); } /** * Exempts shortcodes from being removed by strip_shortcodes() * * @since 5.14.0 * @since 5.25.0 - Allowed 'fcnt' shortcode to pass. * * @param array $tags_to_remove Tags to be removed. * * @return array Updated tags to be removed. */ function fictioneer_exempt_shortcodes_from_removal( $tags_to_remove ) { // Remove 'fictioneer_fa' shortcode from tags if ( ( $key = array_search( 'fictioneer_fa', $tags_to_remove ) ) !== false ) { unset( $tags_to_remove[ $key ] ); } // Remove 'fcnt' shortcode from tags if ( ( $key = array_search( 'fcnt', $tags_to_remove ) ) !== false ) { unset( $tags_to_remove[ $key ] ); } // Continue filter return $tags_to_remove; } // === FCN_EDIT_OTHERS_{POST_TYPE} =========================================== /** * Limit users to their own fiction posts * * @since 5.6.0 * * @param WP_Query $query The WP_Query instance (passed by reference). */ function fictioneer_edit_others_fictioneer_posts( $query ) { global $pagenow; // Abort conditions... if ( ! $query->is_admin || $pagenow != 'edit.php' ) { return; } // Setup $post_type = $query->get( 'post_type' ); // Abort if wrong post type if ( ! in_array( $post_type, ['fcn_story', 'fcn_chapter', 'fcn_collection', 'fcn_recommendation'] ) ) { return; } // Prepare $post_type_plural = $post_type == 'fcn_story' ? 'fcn_stories' : "{$post_type}s"; // Add author to query unless user can edit the fiction of others if ( ! current_user_can( "edit_others_{$post_type_plural}" ) ) { $query->set( 'author', get_current_user_id() ); } } add_action( 'pre_get_posts', 'fictioneer_edit_others_fictioneer_posts' ); // === FCN_READ_OTHERS_FILES ================================================= /** * Prevent users from seeing uploaded files of others * * @since 5.6.0 * * @param WP_Query $query The queried attachments. */ function fictioneer_read_others_files( $query ) { global $current_user, $pagenow; // Only affect media library on admin side if ( 'admin-ajax.php' !== $pagenow || ( $_REQUEST['action'] ?? 0 ) !== 'query-attachments' ) { return; } // Limit to author (uploader) $query->set( 'author', $current_user->ID ); } /** * Prevent users from seeing uploaded files of others in the list view * * @since 5.6.0 * * @param WP_Query $wp_query The current WP_Query. */ function fictioneer_read_others_files_list_view( $wp_query ) { // Safety check if ( ! function_exists( 'get_current_screen' ) ) { return; } $screen = get_current_screen(); if ( $wp_query->is_main_query() && $wp_query->query['post_type'] === 'attachment' && $screen && $screen->base == 'upload' && $screen->id == 'upload' && $screen->post_type == 'attachment' ) { $wp_query->set( 'author', get_current_user_id() ); } } if ( ! current_user_can( 'fcn_read_others_files' ) && is_admin() ) { add_action( 'pre_get_posts', 'fictioneer_read_others_files' ); add_action( 'pre_get_posts', 'fictioneer_read_others_files_list_view' ); } // === FCN_EDIT_OTHERS_FILES ================================================= /** * User cannot edit the files of others * * @since 5.6.0 * * @param array $caps Primitive capabilities required of the user. * @param string $cap Capability being checked. * @param int $user_id The user ID. * @param array $args Adds context to the capability check, typically * starting with an object ID. * * @return array The still allowed primitive capabilities of the user. */ function fictioneer_edit_others_files( $caps, $cap, $user_id, $args ) { // Skip unrelated capabilities if ( $cap != 'edit_post' ) { return $caps; } // Get the post in question. $post = get_post( $args[0] ?? 0 ); // Check if an attachment and whether the user is the author (uploader) if ( empty( $post ) || $post->post_type != 'attachment' || $post->post_author == $user_id ) { return $caps; } // Disallow return ['do_not_allow']; } if ( ! current_user_can( 'fcn_edit_others_files' ) ) { add_filter( 'map_meta_cap', 'fictioneer_edit_others_files', 10, 4 ); } // === FCN_DELETE_OTHERS_FILES =============================================== /** * User cannot delete the files of others * * @since 5.6.0 * * @param array $caps Primitive capabilities required of the user. * @param string $cap Capability being checked. * @param int $user_id The user ID. * @param array $args Adds context to the capability check, typically * starting with an object ID. * * @return array The still allowed primitive capabilities of the user. */ function fictioneer_delete_others_files( $caps, $cap, $user_id, $args ) { // Skip unrelated capabilities if ( $cap != 'delete_post' ) { return $caps; } // Get the post in question. $post = get_post( $args[0] ?? 0 ); // Check if an attachment and whether the user is the author (uploader) if ( empty( $post ) || $post->post_type != 'attachment' || $post->post_author == $user_id ) { return $caps; } // Disallow return ['do_not_allow']; } if ( ! current_user_can( 'fcn_delete_others_files' ) ) { add_filter( 'map_meta_cap', 'fictioneer_delete_others_files', 9999, 4 ); } // === FCN_PRIVACY_CLEARANCE ================================================= /** * Remove email and name columns from user table * * @since 4.7.0 * * @param array $column_headers Columns to show in the user table. * * @return array Reduced columns to show in the user table. */ function fictioneer_hide_users_columns( $column_headers ) { unset( $column_headers['email'] ); unset( $column_headers['name'] ); return $column_headers; } /** * Remove quick edit from comments table * * The quick edit form for comments shows unfortunately private data that * we want to hide if that setting is enabled. * * @since 4.7.0 * * @param array $actions Actions per row in the comments table. * * @return array Restricted actions per row in the comments table. */ function fictioneer_remove_quick_edit( $actions ) { unset( $actions['quickedit'] ); return $actions; } /** * Remove URL and email fields from comment edit page * * Since these are not normally accessible, we need to quickly hide them * with JavaScript. This is not a great solution but better than nothing. * * @since 4.7.0 */ function fictioneer_hide_private_data() { wp_add_inline_script( 'fictioneer-admin-script', "jQuery(function($) { $('.editcomment tr:nth-child(3)').remove(); $('.editcomment tr:nth-child(2)').remove(); });" ); } if ( ! current_user_can( 'fcn_privacy_clearance' ) ) { add_filter( 'comment_email', '__return_false' ); add_filter( 'get_comment_author_IP', '__return_empty_string' ); add_filter( 'manage_users_columns', 'fictioneer_hide_users_columns' ); add_filter( 'comment_row_actions', 'fictioneer_remove_quick_edit' ); add_action( 'admin_enqueue_scripts', 'fictioneer_hide_private_data', 20 ); } // === FCN_REDUCED_PROFILE =================================================== /** * Hide subscriber profile blocks in admin panel * * @since 5.6.0 * @since 5.26.1 - Use wp_print_inline_script_tag(). */ function fictioneer_remove_profile_blocks() { // Add CSS to hide blocks... echo ''; // Remove password options if ( ! get_option( 'fictioneer_show_wp_login_link' ) ) { wp_print_inline_script_tag( 'document.addEventListener("DOMContentLoaded", () => {document.querySelectorAll(".user-pass1-wrap, .user-pass2-wrap, .pw-weak, .user-generate-reset-link-wrap").forEach(element => {element.remove();});});', array( 'id' => 'fictioneer-iife-remove-admin-profile-blocks', 'type' => 'text/javascript', 'data-jetpack-boost' => 'ignore', 'data-no-optimize' => '1', 'data-no-defer' => '1', 'data-no-minify' => '1' ) ); } } if ( current_user_can( 'fcn_reduced_profile' ) ) { add_filter( 'wp_is_application_passwords_available', '__return_false' ); add_filter( 'user_contactmethods', '__return_empty_array' ); add_action( 'admin_head-profile.php', 'fictioneer_remove_profile_blocks' ); remove_action( 'admin_color_scheme_picker', 'admin_color_scheme_picker' ); } // === FCN_MAKE_STICKY ======================================================= /** * Unstick a post * * @since 5.6.0 * * @param int $post_id The post ID. */ function fictioneer_prevent_post_sticky( $post_id ) { unstick_post( $post_id ); // Only do this for the trigger post or bad things can happen! remove_action( 'post_stuck', 'fictioneer_prevent_post_sticky' ); } if ( ! current_user_can( 'fcn_make_sticky' ) ) { add_action( 'post_stuck', 'fictioneer_prevent_post_sticky' ); } // === FCN_UPLOAD_LIMIT ====================================================== /** * Limit the default upload size in MB (minimum 1 MB) * * @since 5.6.0 * * @param int $bytes Default limit value in bytes. * * @return int Modified maximum upload file size in bytes. */ function fictioneer_upload_size_limit( $bytes ) { // Setup $mb = absint( get_option( 'fictioneer_upload_size_limit', 5 ) ?: 5 ); $mb = max( $mb, 1 ); // 1 MB minimum // Return maximum upload file size return 1024 * 1024 * $mb; } if ( current_user_can( 'fcn_upload_limit' ) ) { add_filter( 'upload_size_limit', 'fictioneer_upload_size_limit' ); } // === FCN_UPLOAD_RESTRICTION ================================================ /** * Restrict uploaded file types based on allowed MIME types * * @since 5.6.0 * * @param array $file An array of data for a single uploaded file. Has keys * for 'name', 'type', 'tmp_name', 'error', and 'size'. * * @return array Modified array with error message if the MIME type is not allowed. */ function fictioneer_upload_restrictions( $file ) { // Setup $filetype = wp_check_filetype( $file['name'] ); $mime_type = $filetype['type']; $allowed = get_option( 'fictioneer_upload_mime_types', FICTIONEER_DEFAULT_UPLOAD_MIME_TYPE_RESTRICTIONS ) ?: FICTIONEER_DEFAULT_UPLOAD_MIME_TYPE_RESTRICTIONS; $allowed = fictioneer_explode_list( $allowed ); // Limit upload file types if ( ! in_array( $mime_type, $allowed ) ){ $file['error'] = __( 'You are not allowed to upload files of this type.', 'fictioneer' ); } // Continue filter return $file; } if ( current_user_can( 'fcn_upload_restrictions' ) ) { add_filter( 'wp_handle_upload_prefilter', 'fictioneer_upload_restrictions' ); } // === FCN_ALL_BLOCKS ======================================================== /** * Restrict the use of specific Gutenberg blocks * * @since 5.6.0 * * @param array $data An array of slashed, sanitized, and processed post data. * * @return array Modified post data with unwanted blocks removed. */ function fictioneer_remove_restricted_block_content( $data ) { // Regular expression to match forbidden blocks $forbidden_patterns = array( '/(.*?)/s', '/(.*?)/s', '/(.*?)/s', '/(.*?)/s', '/(.*?)/s', '/(.*?)/s' // Because it's common enough ); // Remove matching blocks foreach ( $forbidden_patterns as $pattern ) { $data['post_content'] = preg_replace( $pattern, '', $data['post_content'] ); } // Only do this for the trigger post or bad things can happen! remove_filter( 'wp_insert_post_data', 'fictioneer_remove_restricted_block_content', 1 ); // Continue with cleaned-up data return $data; } /** * Restricts the block types available in the Gutenberg editor * * @return string Array of allowed block types. */ function fictioneer_restrict_block_types() { $allowed = array( 'core/image', 'core/paragraph', 'core/heading', 'core/list', 'core/list-item', 'core/gallery', 'core/quote', 'core/pullquote', 'core/table', 'core/code', 'core/preformatted', 'core/html', 'core/separator', 'core/spacer', 'core/more', 'core/embed', 'core-embed/youtube', 'core-embed/soundcloud', 'core-embed/spotify', 'core-embed/vimeo', 'core-embed/twitter' ); if ( current_user_can( 'fcn_shortcodes' ) ) { $allowed[] = 'core/shortcode'; } return $allowed; } if ( ! current_user_can( 'fcn_all_blocks' ) ) { add_filter( 'allowed_block_types_all', 'fictioneer_restrict_block_types', 20 ); // Run after global filter add_filter( 'wp_insert_post_data', 'fictioneer_remove_restricted_block_content', 1 ); } // === FCN_EDIT_PERMALINK ==================================================== /** * Prevents user edit of permalink * * @since 5.6.0 * @since 5.8.6 - Fixed duplicate permalinks. * * @param array $data An array of slashed, sanitized, and processed post data. * @param array $postarr An array of sanitized (and slashed) but otherwise unmodified post data. * * @return array The post data with the permalink enforced. */ function fictioneer_prevent_permalink_edit( $data, $postarr ) { // Continue filter if empty or no change if ( empty( $data['post_name'] ) || $data['post_name'] === get_post_field( 'post_name', $postarr['ID'] ) ) { return $data; } // Generate slug from title $slug = sanitize_title( $data['post_title'] ); // Ensure unique slug $data['post_name'] = wp_unique_post_slug( $slug, $postarr['ID'], $data['post_status'], $data['post_type'], $data['post_parent'] ?? 0 ); // Continue filter return $data; } /** * Hide the permalink field with CSS * * @since 5.6.0 */ function fictioneer_hide_permalink_with_css() { echo ''; } /** * Hide the permalink field with JS * * @since 5.6.2 * @since 5.26.1 - Use wp_print_inline_script_tag(). * */ function fictioneer_hide_permalink_with_js() { wp_print_inline_script_tag( 'document.querySelectorAll("#edit-slug-buttons").forEach(element => {element.remove();});', array( 'id' => 'fictioneer-iife-hide-permalink-in-editor', 'type' => 'text/javascript', 'data-jetpack-boost' => 'ignore', 'data-no-optimize' => '1', 'data-no-defer' => '1', 'data-no-minify' => '1' ) ); } if ( ! current_user_can( 'fcn_edit_permalink' ) ) { add_action( 'admin_head-post.php', 'fictioneer_hide_permalink_with_css' ); add_action( 'admin_footer-post.php', 'fictioneer_hide_permalink_with_js' ); add_filter( 'wp_insert_post_data', 'fictioneer_prevent_permalink_edit', 99, 2 ); } // === FCN_EDIT_DATE ========================================================= /** * Prevents the update of the publish date * * Note: The date can be edited until the post has been published once, so you * can still schedule a post or change the target date. But once it is published, * the date cannot be changed. * * @param array $data An array of slashed, sanitized, and processed post data. * @param array $postarr An array of sanitized (and slashed) but otherwise unmodified post data. * * @return array The potentially modified post data. */ function fictioneer_prevent_publish_date_update( $data, $postarr ) { // New post? if ( empty( $postarr['ID'] ) || $postarr['post_status'] === 'auto-draft' || empty( $postarr['post_date_gmt'] ) ) { return $data; } // Setup $current_post_date_gmt = get_post_time( 'Y-m-d H:i:s', 1, $postarr['ID'] ); // Remove from update array if already published once if ( $current_post_date_gmt !== $data['post_date_gmt'] ) { unset( $data['post_date'] ); unset( $data['post_date_gmt'] ); } // Continue filter return $data; } if ( ! current_user_can( 'fcn_edit_date' ) ) { add_filter( 'wp_insert_post_data', 'fictioneer_prevent_publish_date_update', 1, 2 ); } // === FCN_CLASSIC_EDITOR ==================================================== /** * Inject CSS for the classic editor * * @since 5.6.2 */ function fictioneer_classic_editor_css_restrictions() { echo ''; } /** * Inject javascript for the classic editor * * @since 5.6.2 * @since 5.26.1 - Use wp_print_inline_script_tag(). */ function fictioneer_classic_editor_js_restrictions() { wp_print_inline_script_tag( 'document.querySelectorAll(".selectit[for=ping_status], #add-new-comment").forEach(element => {element.remove();});', array( 'id' => 'fictioneer-iife-classic-editor-restrictions', 'type' => 'text/javascript', 'data-jetpack-boost' => 'ignore', 'data-no-optimize' => '1', 'data-no-defer' => '1', 'data-no-minify' => '1' ) ); } /** * Restrict metaboxes in the classic editor * * @since 5.6.2 */ function fictioneer_restrict_classic_metaboxes() { $post_types = ['post', 'page', 'fcn_story', 'fcn_chapter', 'fcn_collection', 'fcn_recommendation']; // Trackbacks remove_meta_box( 'trackbacksdiv', $post_types, 'normal' ); // Tags if ( ! current_user_can( 'assign_post_tags' ) ) { remove_meta_box( 'tagsdiv-post_tag', $post_types, 'side' ); } // Categories if ( ! current_user_can( 'assign_categories' ) ) { remove_meta_box( 'categorydiv', $post_types, 'side' ); } // Genres if ( ! current_user_can( 'assign_fcn_genres' ) ) { remove_meta_box( 'fcn_genrediv', $post_types, 'side' ); } // Fandoms if ( ! current_user_can( 'assign_fcn_fandoms' ) ) { remove_meta_box( 'fcn_fandomdiv', $post_types, 'side' ); } // Characters if ( ! current_user_can( 'assign_fcn_characters' ) ) { remove_meta_box( 'fcn_characterdiv', $post_types, 'side' ); } // Content Warnings if ( ! current_user_can( 'assign_fcn_content_warnings' ) ) { remove_meta_box( 'fcn_content_warningdiv', $post_types, 'side' ); } // Permalink if ( ! current_user_can( 'fcn_edit_permalink' ) ) { remove_meta_box( 'slugdiv', $post_types, 'normal' ); } // Page template if ( ! current_user_can( 'fcn_select_page_template' ) ) { remove_meta_box( 'pageparentdiv', $post_types, 'side' ); } } if ( current_user_can( 'fcn_classic_editor' ) ) { add_filter( 'use_block_editor_for_post_type', '__return_false' ); add_action( 'add_meta_boxes', 'fictioneer_restrict_classic_metaboxes' ); add_action( 'admin_head-post.php', 'fictioneer_classic_editor_css_restrictions' ); add_action( 'admin_head-post-new.php', 'fictioneer_classic_editor_css_restrictions' ); add_action( 'admin_footer-post.php', 'fictioneer_classic_editor_js_restrictions' ); add_action( 'admin_footer-post-new.php', 'fictioneer_classic_editor_js_restrictions' ); } }