(.*?)/s', '', $content );
return $content;
}
// =============================================================================
// META FIELD HELPERS
// =============================================================================
/**
* Returns HTML for a checkbox meta field
*
* @since 5.7.4
*
* @param WP_Post $post The post.
* @param string $meta_key The meta key.
* @param string $label The checkbox label.
* @param array $args {
* Optional. An array of additional arguments.
*
* @type bool $required Whether the field is required. Default false.
* }
*
* @return string The HTML markup for the field.
*/
function fictioneer_get_metabox_checkbox( $post, $meta_key, $label, $args = [] ) {
// Setup
$required = ( $args['required'] ?? 0 ) ? 'required' : '';
$data_required = $required ? 'data-required="true"' : '';
$attributes = implode( ' ', $args['attributes'] ?? [] );
ob_start();
// Start HTML ---> ?>
ID, $meta_key, true ) );
$label = strval( $args['label'] ?? '' );
$description = strval( $args['description'] ?? '' );
$placeholder = strval( $args['placeholder'] ?? '' );
$type = $args['type'] ?? 'text';
$required = ( $args['required'] ?? 0 ) ? 'required' : '';
$data_required = $required ? 'data-required="true"' : '';
$maxlength = isset( $args['maxlength'] ) ? 'maxlength="' . $args['maxlength'] . '"' : '';
$attributes = implode( ' ', $args['attributes'] ?? [] );
ob_start();
// Start HTML ---> ?>
ID, $meta_key, true ) );
$label = strval( $args['label'] ?? '' );
$description = strval( $args['description'] ?? '' );
$attributes = implode( ' ', $args['attributes'] ?? [] );
if ( $meta_value ) {
$utc_datetime = new DateTime( $meta_value, new DateTimeZone( 'UTC' ) );
$utc_datetime->setTimezone( wp_timezone() );
$meta_value = $utc_datetime->format( 'Y-m-d\TH:i:s' );
}
ob_start();
// Start HTML ---> ?>
ID, $meta_key, true ) );
$label = strval( $args['label'] ?? '' );
$description = strval( $args['description'] ?? '' );
$placeholder = strval( $args['placeholder'] ?? '' );
$required = ( $args['required'] ?? 0 ) ? 'required' : '';
$data_required = $required ? 'data-required="true"' : '';
$attributes = implode( ' ', $args['attributes'] ?? [] );
ob_start();
// Start HTML ---> ?>
ID, $meta_key, true );
$array = is_array( $array ) ? $array : [];
$list = esc_attr( implode( ', ', $array ) );
$label = strval( $args['label'] ?? '' );
$description = strval( $args['description'] ?? '' );
$placeholder = strval( $args['placeholder'] ?? '' );
$required = ( $args['required'] ?? 0 ) ? 'required' : '';
$data_required = $required ? 'data-required="true"' : '';
$attributes = implode( ' ', $args['attributes'] ?? [] );
ob_start();
// Start HTML ---> ?>
Name).
* @param array $args {
* Optional. An array of additional arguments.
*
* @type string $label Label above the field.
* @type string $description Description below the field.
* @type bool $required Whether the field is required. Default false.
* @type string $query_var GET variable for current selection if empty.
* }
*
* @return string The HTML markup for the field.
*/
function fictioneer_get_metabox_select( $post, $meta_key, $options, $args = [] ) {
// Setup
$selected = get_post_meta( $post->ID, $meta_key, true );
$label = strval( $args['label'] ?? '' );
$description = strval( $args['description'] ?? '' );
$required = ( $args['required'] ?? 0 ) ? 'required' : '';
$data_required = $required ? 'data-required="true"' : '';
$attributes = implode( ' ', $args['attributes'] ?? [] );
// Current selection
if ( empty( $selected ) ) {
$query_var = sanitize_key( $_GET[ $args['query_var'] ?? '' ] ?? '' );
if ( $query_var && array_key_exists( $query_var, $options ) ) {
$selected = $query_var;
} else {
$selected = empty( $selected ) ? array_keys( $options )[0] : $selected;
}
}
// Sort alphabetically
if ( $args['asort'] ?? 0 ) {
$none_item = isset( $options['0'] ) ? array( '0' => $options['0'] ) : [];
unset( $options['0'] );
asort( $options );
$options = $none_item + $options;
}
ob_start();
// Start HTML ---> ?>
ID, $meta_key, true );
$label = strval( $args['label'] ?? '' );
$description = strval( $args['description'] ?? '' );
$placeholder = strval( $args['placeholder'] ?? '' );
$required = ( $args['required'] ?? 0 ) ? 'required' : '';
$data_required = $required ? 'data-required="true"' : '';
$input_classes = strval( $args['input_classes'] ?? '' );
$attributes = implode( ' ', $args['attributes'] ?? [] );
ob_start();
// Start HTML ---> ?>
ID, $meta_key, true );
$label = strval( $args['label'] ?? '' );
$description = strval( $args['description'] ?? '' );
$upload = strval( $args['button'] ?? _x( 'Set image', 'Metabox image upload button.', 'fictioneer' ) );
$replace = _x( 'Replace', 'Metabox image upload button.', 'fictioneer' );
$remove = _x( 'Remove', 'Metabox image remove button.', 'fictioneer' );
$image_url = wp_get_attachment_url( $meta_value );
$image_css = $image_url ? "style='background-image: url(\"{$image_url}\");'" : '';
ob_start();
// Start HTML ---> ?>
ID, $meta_key, true );
$file_path = get_attached_file( $meta_value );
$label = strval( $args['label'] ?? '' );
$description = strval( $args['description'] ?? '' );
$button_upload = strval( $args['button'] ?? _x( 'Add eBook', 'Metabox ebook upload button.', 'fictioneer' ) );
$button_replace = _x( 'Replace', 'Metabox replace button.', 'fictioneer' );
$button_remove = _x( 'Remove', 'Metabox remove button.', 'fictioneer' );
$title = null;
$filename = null;
$filesize = null;
$url = null;
$is_set = false;
if ( file_exists( $file_path ) ) {
$title = get_the_title( $meta_value );
$filename = wp_basename( $file_path );
$filesize = size_format( filesize( $file_path ) );
$url = wp_get_attachment_url( $meta_value );
$is_set = true;
} else {
$meta_value = null;
}
ob_start();
// Start HTML ---> ?>
ID, $meta_key, true );
$array = is_array( $array ) ? $array : [];
$list = esc_attr( implode( ', ', $array ) );
$label = strval( $args['label'] ?? '' );
$description = strval( $args['description'] ?? '' );
ob_start();
// Start HTML ---> ?>
ID, $meta_key, true ) );
$label = strval( $args['label'] ?? '' );
$description = strval( $args['description'] ?? '' );
$placeholder = strval( $args['placeholder'] ?? '' );
$required = ( $args['required'] ?? 0 ) ? 'required' : '';
$data_required = $required ? 'data-required="true"' : '';
$current_icon_class = fictioneer_get_icon_field( $meta_key, $post->ID );
ob_start();
// Start HTML ---> ?>
ID, $meta_key, true );
$label = strval( $args['label'] ?? '' );
$description = strval( $args['description'] ?? '' );
$required = ( $args['required'] ?? 0 ) ? 'required' : '';
$data_required = $required ? 'data-required="true"' : '';
$settings = array(
'wpautop' => true,
'media_buttons' => false,
'textarea_name' => $meta_key,
'textarea_rows' => 10,
'default_editor' => 'html',
'tinymce' => array(
'toolbar1' => 'bold,italic,bullist,numlist,blockquote,alignleft,aligncenter,alignright,link',
'toolbar2' => '',
'paste_as_text' => true
),
'quicktags' => array(
'buttons' => 'strong,em,link,block,img,ul,ol,li,close'
)
);
ob_start();
// Start HTML ---> ?>
ID}" );
$label = strval( $args['label'] ?? '' );
$description = strval( $args['description'] ?? '' );
// Build and return HTML
ob_start();
// Start HTML ---> ?>
true ), $args['type_args'] ?? [] );
$post_types = get_post_types( $type_args, 'objects' );
// HTML
echo '';
}
/**
* AJAX: Delegate query and send posts for relationship fields
*
* @since 5.8.0
*/
function fictioneer_ajax_query_relationship_posts() {
// Validate
$action = sanitize_text_field( $_REQUEST['action'] ?? '' );
$nonce = sanitize_text_field( $_REQUEST['nonce'] ?? '' );
$meta_key = sanitize_text_field( $_REQUEST['key'] ?? '' );
$post_id = absint( $_REQUEST['post_id'] ?? 0 );
$user = wp_get_current_user();
// Abort if...
if ( empty( $action ) ) {
wp_send_json_error( array( 'error' => __( 'Error: Action parameter missing.', 'fictioneer' ) ) );
}
if ( empty( $nonce ) ) {
wp_send_json_error( array( 'error' => __( 'Error: Nonce parameter missing.', 'fictioneer' ) ) );
}
if ( empty( $meta_key ) ) {
wp_send_json_error( array( 'error' => __( 'Error: Key parameter missing.', 'fictioneer' ) ) );
}
if ( ! $user->exists() ) {
wp_send_json_error( array( 'error' => __( 'Error: Not logged in.', 'fictioneer' ) ) );
}
// Story chapter assignment?
if ( check_ajax_referer( "relationship_posts_fictioneer_story_chapters_{$post_id}", 'nonce', false ) && $post_id > 0 ) {
fictioneer_ajax_get_relationship_chapters( $post_id, $meta_key );
}
// Story page assignment?
if ( check_ajax_referer( "relationship_posts_fictioneer_story_custom_pages_{$post_id}", 'nonce', false ) && $post_id > 0 ) {
fictioneer_ajax_get_relationship_story_pages( $post_id, $meta_key );
}
// Collection item assignment?
if ( check_ajax_referer( "relationship_posts_fictioneer_collection_items_{$post_id}", 'nonce', false ) && $post_id > 0 ) {
fictioneer_ajax_get_relationship_collection( $post_id, $meta_key );
}
// Post featured item assignment?
if ( check_ajax_referer( "relationship_posts_fictioneer_post_featured_{$post_id}", 'nonce', false ) && $post_id > 0 ) {
fictioneer_ajax_get_relationship_featured( $post_id, $meta_key );
}
// Nothing worked...
wp_send_json_error( array( 'error' => __( 'Error: Invalid request.', 'fictioneer' ) ) );
}
add_action( 'wp_ajax_fictioneer_ajax_query_relationship_posts', 'fictioneer_ajax_query_relationship_posts' );
/**
* Render HTML for selected story chapters
*
* @since 5.8.0
*
* @param array $selected Currently selected chapters.
* @param string $meta_key The meta key.
* @param array $args Optional. An array of additional arguments.
*/
function fictioneer_callback_relationship_chapters( $selected, $meta_key, $args = [] ) {
// Setup
$status_labels = array(
'draft' => get_post_status_object( 'draft' )->label,
'pending' => get_post_status_object( 'pending' )->label,
'publish' => get_post_status_object( 'publish' )->label,
'private' => get_post_status_object( 'private' )->label,
'future' => get_post_status_object( 'future' )->label,
'trash' => get_post_status_object( 'trash' )->label,
);
// Build HTML
foreach ( $selected as $chapter ) {
$title = fictioneer_get_safe_title( $chapter, 'admin-callback-relationship-chapters' );
$classes = ['fictioneer-meta-field__relationships-item', 'fictioneer-meta-field__relationships-values-item'];
if ( get_post_meta( $chapter->ID, 'fictioneer_chapter_hidden', true ) ) {
$title = "{$title} (" . _x( 'Unlisted', 'Chapter assignment flag.', 'fictioneer' ) . ")";
}
if ( $chapter->post_status !== 'publish' ) {
$title = "{$title} ({$status_labels[ $chapter->post_status ]})";
}
// Start HTML ---> ?>
post_type !== 'fcn_story' ) {
wp_send_json_error( array( 'error' => __( 'Error: Wrong post type.', 'fictioneer' ) ) );
}
if (
! current_user_can( 'edit_fcn_stories' ) ||
(
$user->ID != $post->post_author &&
! current_user_can( 'edit_others_fcn_stories' ) &&
! current_user_can( 'manage_options' )
)
) {
wp_send_json_error( array( 'error' => __( 'Error: Insufficient permissions.', 'fictioneer' ) ) );
}
// Query
$query_args = array(
'post_type' => 'fcn_chapter',
'post_status' => ['publish', 'private', 'future'],
'orderby' => 'date',
'order' => 'desc',
'posts_per_page' => 10,
'paged' => $page,
's' => $search,
'update_post_meta_cache' => true,
'update_post_term_cache' => false // Improve performance
);
if ( FICTIONEER_FILTER_STORY_CHAPTERS ) {
$query_args['meta_key'] = 'fictioneer_chapter_story';
$query_args['meta_value'] = $post_id;
}
$query = new WP_Query( $query_args );
// Setup
$nonce = wp_create_nonce( "relationship_posts_{$meta_key}_{$post_id}" );
$output = [];
$status_labels = array(
'draft' => get_post_status_object( 'draft' )->label,
'pending' => get_post_status_object( 'pending' )->label,
'publish' => get_post_status_object( 'publish' )->label,
'private' => get_post_status_object( 'private' )->label,
'future' => get_post_status_object( 'future' )->label,
'trash' => get_post_status_object( 'trash' )->label,
);
// Build HTML for items
foreach ( $query->posts as $chapter ) {
// Chapter setup
$title = fictioneer_get_safe_title( $chapter, 'admin-ajax-get-relationship-chapters' );
$classes = ['fictioneer-meta-field__relationships-item', 'fictioneer-meta-field__relationships-source-item'];
// Update title if necessary
if ( get_post_meta( $chapter->ID, 'fictioneer_chapter_hidden', true ) ) {
$title = "{$title} (" . _x( 'Unlisted', 'Chapter assignment flag.', 'fictioneer' ) . ")";
}
if ( $chapter->post_status !== 'publish' ) {
$title = "{$title} ({$status_labels[ $chapter->post_status ]})";
}
// Build and append item
$item = "{$title}";
$output[] = $item;
}
// Build HTML for observer
if ( $page < $query->max_num_pages ) {
$page++;
$observer = "';
$observer .= '' . _x( 'Loading', 'AJAX relationship posts loading.', 'fictioneer' ) . '';
$output[] = $observer;
}
// No results
if ( empty( $output ) ) {
$no_matches = "";
$no_matches .= _x( 'No matches found.', 'AJAX relationship posts loading.', 'fictioneer' );
$no_matches .= '';
$output[] = $no_matches;
}
// Response
wp_send_json_success(
array(
'html' => implode( '', $output )
)
);
}
/**
* Return HTML story chapter info
*
* @since 5.8.0
*
* @param WP_Post $chapter The chapter post.
*
* @return string HTML for the chapter info.
*/
function fictioneer_get_relationship_chapter_details( $chapter ) {
// Setup
$text_icon = get_post_meta( $chapter->ID, 'fictioneer_chapter_text_icon', true );
$icon = fictioneer_get_icon_field( 'fictioneer_chapter_icon', $chapter->ID );
$rating = get_post_meta( $chapter->ID, 'fictioneer_chapter_rating', true );
$warning = get_post_meta( $chapter->ID, 'fictioneer_chapter_warning', true );
$group = get_post_meta( $chapter->ID, 'fictioneer_chapter_group', true );
$flags = [];
$status_labels = array(
'draft' => get_post_status_object( 'draft' )->label,
'pending' => get_post_status_object( 'pending' )->label,
'publish' => get_post_status_object( 'publish' )->label,
'private' => get_post_status_object( 'private' )->label,
'future' => get_post_status_object( 'future' )->label,
'trash' => get_post_status_object( 'trash' )->label,
);
$status = $status_labels[ $chapter->post_status ];
$info = [];
// Build
$info[] = empty( $text_icon ) ? sprintf( '', $icon ) : "{$text_icon}";
if ( get_post_meta( $chapter->ID, 'fictioneer_chapter_hidden', true ) ) {
$flags[] = _x( 'Unlisted', 'Chapter assignment flag.', 'fictioneer' );
}
if ( get_post_meta( $chapter->ID, 'fictioneer_chapter_no_chapter', true ) ) {
$flags[] = _x( 'No Chapter', 'Chapter assignment flag.', 'fictioneer' );
}
$info[] = sprintf(
_x( 'Status: %s', 'Chapter assignment info.', 'fictioneer' ),
$status
);
$info[] = sprintf(
_x( 'Date: %1$s at %2$s', 'Chapter assignment info.', 'fictioneer' ),
get_the_date( '', $chapter->ID ),
get_the_time( '', $chapter->ID )
);
if ( ! empty( $group ) ) {
$info[] = sprintf(
_x( 'Group: %s', 'Chapter assignment info.', 'fictioneer' ),
$group
);
}
if ( ! empty( $rating ) ) {
$info[] = sprintf(
_x( 'Age Rating: %s', 'Chapter assignment info.', 'fictioneer' ),
$rating
);
}
if ( ! empty( $flags ) ) {
$info[] = sprintf(
_x( 'Flags: %s', 'Chapter assignment info.', 'fictioneer' ),
implode( ', ', $flags )
);
}
if ( ! empty( $warning ) ) {
$info[] = sprintf(
_x( 'Warning: %s', 'Chapter assignment info.', 'fictioneer' ),
$warning
);
}
// Implode and return
return implode( ' • ', $info );
}
/**
* Render HTML for selected story pages
*
* @since 5.8.0
*
* @param array $selected Currently selected pages.
* @param string $meta_key The meta key.
* @param array $args Optional. An array of additional arguments.
*/
function fictioneer_callback_relationship_story_pages( $selected, $meta_key, $args = [] ) {
// Build HTML
foreach ( $selected as $page ) {
$title = fictioneer_get_safe_title( $page, 'admin-callback-relationship-story-pages' );
$classes = ['fictioneer-meta-field__relationships-item', 'fictioneer-meta-field__relationships-values-item'];
// Start HTML ---> ?>
post_type !== 'fcn_story' ) {
wp_send_json_error( array( 'error' => __( 'Error: Wrong post type.', 'fictioneer' ) ) );
}
if (
! current_user_can( 'edit_fcn_stories' ) ||
(
$user->ID != $post->post_author &&
! current_user_can( 'edit_others_fcn_stories' ) &&
! current_user_can( 'manage_options' )
)
) {
wp_send_json_error( array( 'error' => __( 'Error: Insufficient permissions.', 'fictioneer' ) ) );
}
$forbidden = array_unique(
array(
get_option( 'fictioneer_user_profile_page', 0 ),
get_option( 'fictioneer_bookmarks_page', 0 ),
get_option( 'fictioneer_stories_page', 0 ),
get_option( 'fictioneer_chapters_page', 0 ),
get_option( 'fictioneer_recommendations_page', 0 ),
get_option( 'fictioneer_collections_page', 0 ),
get_option( 'fictioneer_bookshelf_page', 0 ),
get_option( 'fictioneer_404_page', 0 ),
get_option( 'page_on_front', 0 ),
get_option( 'page_for_posts', 0 )
)
);
// Query
$query = new WP_Query(
array(
'post_type' => 'page',
'post_status' => 'publish',
'orderby' => 'date',
'order' => 'desc',
'author' => get_post_field( 'post_author', $post_id ),
'post__not_in' => array_map( 'strval', $forbidden ),
'posts_per_page' => 10,
'paged' => $page,
's' => $search,
'update_post_meta_cache' => false, // Improve performance
'update_post_term_cache' => false // Improve performance
)
);
// Setup
$nonce = wp_create_nonce( "relationship_posts_{$meta_key}_{$post_id}" );
$output = [];
// Build HTML for items
foreach ( $query->posts as $item ) {
// Chapter setup
$title = fictioneer_get_safe_title( $item, 'admin-ajax-get-relationship-story-pages' );
$classes = ['fictioneer-meta-field__relationships-item', 'fictioneer-meta-field__relationships-source-item'];
// Build and append item
$item = "";
$item .= "{$title}";
$output[] = $item;
}
// Build HTML for observer
if ( $page < $query->max_num_pages ) {
$page++;
$observer = "';
$observer .= '' . _x( 'Loading', 'AJAX relationship posts loading.', 'fictioneer' ) . '';
$output[] = $observer;
}
// No results
if ( empty( $output ) ) {
$no_matches = "";
$no_matches .= _x( 'No matches found.', 'AJAX relationship posts loading.', 'fictioneer' );
$no_matches .= '';
$output[] = $no_matches;
}
// Response
wp_send_json_success(
array(
'html' => implode( '', $output )
)
);
}
/**
* Render HTML for selected collection items
*
* @since 5.8.0
*
* @param array $selected Currently selected items.
* @param string $meta_key The meta key.
* @param array $args Optional. An array of additional arguments.
*/
function fictioneer_callback_relationship_collection( $selected, $meta_key, $args = [] ) {
// Setup
$post_type_labels = array(
'post' => _x( 'Post', 'Post type label.', 'fictioneer' ),
'page' => _x( 'Page', 'Post type label.', 'fictioneer' ),
'fcn_story' => _x( 'Story', 'Post type label.', 'fictioneer' ),
'fcn_chapter' => _x( 'Chapter', 'Post type label.', 'fictioneer' ),
'fcn_collection' => _x( 'Collection', 'Post type label.', 'fictioneer' ),
'fcn_recommendation' => _x( 'Rec', 'Post type label.', 'fictioneer' )
);
// Build HTML
foreach ( $selected as $item ) {
$title = fictioneer_get_safe_title( $item, 'admin-callback-relationship-collection' );
$label = esc_html( $post_type_labels[ $item->post_type ] ?? _x( '?', 'Relationship item label.', 'fictioneer' ) );
$classes = ['fictioneer-meta-field__relationships-item', 'fictioneer-meta-field__relationships-values-item'];
// Start HTML ---> ?>
_x( 'Post', 'Post type label.', 'fictioneer' ),
'page' => _x( 'Page', 'Post type label.', 'fictioneer' ),
'fcn_story' => _x( 'Story', 'Post type label.', 'fictioneer' ),
'fcn_chapter' => _x( 'Chapter', 'Post type label.', 'fictioneer' ),
'fcn_collection' => _x( 'Collection', 'Post type label.', 'fictioneer' ),
'fcn_recommendation' => _x( 'Rec', 'Post type label.', 'fictioneer' )
);
$forbidden = array_unique(
array(
get_option( 'fictioneer_user_profile_page', 0 ),
get_option( 'fictioneer_bookmarks_page', 0 ),
get_option( 'fictioneer_stories_page', 0 ),
get_option( 'fictioneer_chapters_page', 0 ),
get_option( 'fictioneer_recommendations_page', 0 ),
get_option( 'fictioneer_collections_page', 0 ),
get_option( 'fictioneer_bookshelf_page', 0 ),
get_option( 'fictioneer_404_page', 0 ),
get_option( 'page_on_front', 0 ),
get_option( 'page_for_posts', 0 )
)
);
// Validations
if ( $post->post_type !== 'fcn_collection' ) {
wp_send_json_error( array( 'error' => __( 'Error: Wrong post type.', 'fictioneer' ) ) );
}
if (
! current_user_can( 'edit_fcn_collections' ) ||
(
$user->ID != $post->post_author &&
! current_user_can( 'edit_others_fcn_collections' ) &&
! current_user_can( 'manage_options' )
)
) {
wp_send_json_error( array( 'error' => __( 'Error: Insufficient permissions.', 'fictioneer' ) ) );
}
// Query
$query = new WP_Query(
array(
'post_type' => $post_type ?: $allowed_post_types,
'post_status' => 'publish',
'orderby' => 'date',
'order' => 'desc',
'post__not_in' => array_map( 'strval', $forbidden ),
'posts_per_page' => 10,
'paged' => $page,
's' => $search,
'update_post_meta_cache' => false, // Improve performance
'update_post_term_cache' => false // Improve performance
)
);
// Setup
$nonce = wp_create_nonce( "relationship_posts_{$meta_key}_{$post_id}" );
$output = [];
// Build HTML for items
foreach ( $query->posts as $item ) {
// Chapter setup
$title = fictioneer_get_safe_title( $item, 'admin-ajax-get-relationship-collection' );
$label = esc_html( $post_type_labels[ $item->post_type ] ?? _x( '?', 'Relationship item label.', 'fictioneer' ) );
$classes = ['fictioneer-meta-field__relationships-item', 'fictioneer-meta-field__relationships-source-item'];
// Build and append item
$item = "";
$item .= "{$label} {$title}";
$output[] = $item;
}
// Build HTML for observer
if ( $page < $query->max_num_pages ) {
$page++;
$observer = "';
$observer .= '' . _x( 'Loading', 'AJAX relationship posts loading.', 'fictioneer' ) . '';
$output[] = $observer;
}
// No results
if ( empty( $output ) ) {
$no_matches = "";
$no_matches .= _x( 'No matches found.', 'AJAX relationship posts loading.', 'fictioneer' );
$no_matches .= '';
$output[] = $no_matches;
}
// Response
wp_send_json_success(
array(
'html' => implode( '', $output )
)
);
}
/**
* Render HTML for selected featured items
*
* @since 5.8.0
*
* @param array $selected Currently selected items.
* @param string $meta_key The meta key.
* @param array $args Optional. An array of additional arguments.
*/
function fictioneer_callback_relationship_featured( $selected, $meta_key, $args = [] ) {
// Setup
$post_type_labels = array(
'post' => _x( 'Post', 'Post type label.', 'fictioneer' ),
'fcn_story' => _x( 'Story', 'Post type label.', 'fictioneer' ),
'fcn_chapter' => _x( 'Chapter', 'Post type label.', 'fictioneer' ),
'fcn_collection' => _x( 'Collection', 'Post type label.', 'fictioneer' ),
'fcn_recommendation' => _x( 'Rec', 'Post type label.', 'fictioneer' )
);
// Build HTML
foreach ( $selected as $item ) {
$title = fictioneer_get_safe_title( $item, 'admin-callback-relationship-featured' );
$label = esc_html( $post_type_labels[ $item->post_type ] ?? _x( '?', 'Relationship item label.', 'fictioneer' ) );
$classes = ['fictioneer-meta-field__relationships-item', 'fictioneer-meta-field__relationships-values-item'];
// Start HTML ---> ?>
_x( 'Post', 'Post type label.', 'fictioneer' ),
'fcn_story' => _x( 'Story', 'Post type label.', 'fictioneer' ),
'fcn_chapter' => _x( 'Chapter', 'Post type label.', 'fictioneer' ),
'fcn_collection' => _x( 'Collection', 'Post type label.', 'fictioneer' ),
'fcn_recommendation' => _x( 'Rec', 'Post type label.', 'fictioneer' )
);
// Validations
if ( $post->post_type !== 'post' ) {
wp_send_json_error( array( 'error' => __( 'Error: Wrong post type.', 'fictioneer' ) ) );
}
if (
! current_user_can( 'edit_posts' ) ||
(
$user->ID != $post->post_author &&
! current_user_can( 'edit_others_posts' ) &&
! current_user_can( 'manage_options' )
)
) {
wp_send_json_error( array( 'error' => __( 'Error: Insufficient permissions.', 'fictioneer' ) ) );
}
// Query
$query = new WP_Query(
array(
'post_type' => $post_type ?: $allowed_post_types,
'post_status' => 'publish',
'orderby' => 'date',
'order' => 'desc',
'posts_per_page' => 10,
'paged' => $page,
's' => $search,
'update_post_meta_cache' => false, // Improve performance
'update_post_term_cache' => false // Improve performance
)
);
// Setup
$nonce = wp_create_nonce( "relationship_posts_{$meta_key}_{$post_id}" );
$output = [];
// Build HTML for items
foreach ( $query->posts as $item ) {
// Chapter setup
$title = fictioneer_get_safe_title( $item, 'admin-ajax-get-relationship-featured' );
$label = esc_html( $post_type_labels[ $item->post_type ] ?? _x( '?', 'Relationship item label.', 'fictioneer' ) );
$classes = ['fictioneer-meta-field__relationships-item', 'fictioneer-meta-field__relationships-source-item'];
// Build and append item
$item = "";
$item .= "{$label} {$title}";
$output[] = $item;
}
// Build HTML for observer
if ( $page < $query->max_num_pages ) {
$page++;
$observer = "';
$observer .= '' . _x( 'Loading', 'AJAX relationship posts loading.', 'fictioneer' ) . '';
$output[] = $observer;
}
// No results
if ( empty( $output ) ) {
$no_matches = "";
$no_matches .= _x( 'No matches found.', 'AJAX relationship posts loading.', 'fictioneer' );
$no_matches .= '';
$output[] = $no_matches;
}
// Response
wp_send_json_success(
array(
'html' => implode( '', $output )
)
);
}
// =============================================================================
// METABOX CLASSES
// =============================================================================
/**
* Append classes to the metabox
*
* @since 5.7.4
*
* @param array $classes An array of postbox classes.
*
* @return array The modified array of postbox classes.
*/
function fictioneer_append_metabox_classes( $classes ) {
global $post;
// Add class
$classes[] = 'fictioneer-side-metabox';
if ( ! use_block_editor_for_post_type( $post->post_type ) ) {
$classes[] = 'fictioneer-side-metabox--classic';
}
// Return with added class
return $classes;
}
add_filter( 'postbox_classes_fcn_story_fictioneer-story-meta', 'fictioneer_append_metabox_classes' );
add_filter( 'postbox_classes_fcn_story_fictioneer-story-data', 'fictioneer_append_metabox_classes' );
add_filter( 'postbox_classes_fcn_story_fictioneer-story-epub', 'fictioneer_append_metabox_classes' );
add_filter( 'postbox_classes_fcn_chapter_fictioneer-chapter-meta', 'fictioneer_append_metabox_classes' );
add_filter( 'postbox_classes_fcn_chapter_fictioneer-chapter-data', 'fictioneer_append_metabox_classes' );
add_filter( 'postbox_classes_post_fictioneer-featured-content', 'fictioneer_append_metabox_classes' );
add_filter( 'postbox_classes_fcn_collection_fictioneer-collection-data', 'fictioneer_append_metabox_classes' );
add_filter( 'postbox_classes_fcn_recommendation_fictioneer-recommendation-data', 'fictioneer_append_metabox_classes' );
foreach ( ['post', 'page', 'fcn_story', 'fcn_chapter', 'fcn_recommendation', 'fcn_collection'] as $type ) {
add_filter( "postbox_classes_{$type}_fictioneer-extra", 'fictioneer_append_metabox_classes' );
}
foreach ( ['post', 'fcn_story', 'fcn_chapter'] as $type ) {
add_filter( "postbox_classes_{$type}_fictioneer-support-links", 'fictioneer_append_metabox_classes' );
}
// =============================================================================
// STORY META FIELDS
// =============================================================================
/**
* Adds story meta metabox
*
* @since 5.7.4
*/
function fictioneer_add_story_meta_metabox() {
add_meta_box(
'fictioneer-story-meta',
__( 'Story Meta', 'fictioneer' ),
'fictioneer_render_story_meta_metabox',
['fcn_story'],
'side',
'default'
);
}
add_action( 'add_meta_boxes', 'fictioneer_add_story_meta_metabox' );
/**
* Render story meta metabox
*
* @since 5.7.4
*
* @param WP_Post $post The current post object.
*/
function fictioneer_render_story_meta_metabox( $post ) {
// --- Setup -----------------------------------------------------------------
$nonce = wp_create_nonce( "story_meta_data_{$post->ID}" ); // Accounts for manual wp_update_post() calls!
$advanced = get_option( 'fictioneer_enable_advanced_meta_fields' );
$output = [];
// --- Add fields ------------------------------------------------------------
$output['fictioneer_story_status'] = fictioneer_get_metabox_select(
$post,
'fictioneer_story_status',
array(
'Ongoing' => _x( 'Ongoing', 'Story status select option.', 'fictioneer' ),
'Completed' => _x( 'Completed', 'Story status select option.', 'fictioneer' ),
'Oneshot' => _x( 'Oneshot', 'Story status select option.', 'fictioneer' ),
'Hiatus' => _x( 'Hiatus', 'Story status select option.', 'fictioneer' ),
'Canceled' => _x( 'Canceled', 'Story status select option.', 'fictioneer' )
),
array(
'label' => _x( 'Status', 'Story status meta field label.', 'fictioneer' ),
'description' => __( 'Current status of the story.', 'fictioneer' ),
'required' => 1
)
);
$output['fictioneer_story_rating'] = fictioneer_get_metabox_select(
$post,
'fictioneer_story_rating',
array(
'Everyone' => _x( 'Everyone', 'Story age rating select option.', 'fictioneer' ),
'Teen' => _x( 'Teen', 'Story age rating select option.', 'fictioneer' ),
'Mature' => _x( 'Mature', 'Story age rating select option.', 'fictioneer' ),
'Adult' => _x( 'Adult', 'Story age rating select option.', 'fictioneer' )
),
array(
'label' => _x( 'Age Rating', 'Story age rating meta field label.', 'fictioneer' ),
'description' => __( 'Select an overall age rating.', 'fictioneer' ),
'required' => 1
)
);
if ( $advanced ) {
$output['fictioneer_story_co_authors'] = fictioneer_get_metabox_array(
$post,
'fictioneer_story_co_authors',
array(
'label' => _x( 'Co-Authors', 'Story co-authors meta field label.', 'fictioneer' ),
'description' => __( 'Comma-separated list of author IDs.', 'fictioneer' )
)
);
}
$output['fictioneer_story_copyright_notice'] = fictioneer_get_metabox_text(
$post,
'fictioneer_story_copyright_notice',
array(
'label' => _x( 'Copyright Notice', 'Story copyright notice meta field label.', 'fictioneer' ),
'description' => __( 'Optional, leave empty to hide.', 'fictioneer' )
)
);
$output['fictioneer_story_topwebfiction_link'] = fictioneer_get_metabox_url(
$post,
'fictioneer_story_topwebfiction_link',
array(
'label' => _x( 'Top Web Fiction Link', 'Story top web fiction link meta field label.', 'fictioneer' ),
'description' => __( 'URL to Top Web Fiction page.', 'fictioneer' )
)
);
$output['flags_heading'] = '' .
__( 'Flags', 'Metabox checkbox heading.', 'fictioneer' ) . '
';
if ( current_user_can( 'fcn_make_sticky', $post->ID ) && FICTIONEER_ENABLE_STICKY_CARDS ) {
$output['fictioneer_story_sticky'] = fictioneer_get_metabox_checkbox(
$post,
'fictioneer_story_sticky',
__( 'Sticky in lists', 'fictioneer' )
);
}
$output['fictioneer_story_hidden'] = fictioneer_get_metabox_checkbox(
$post,
'fictioneer_story_hidden',
__( 'Hide story in lists', 'fictioneer' )
);
$output['fictioneer_story_no_thumbnail'] = fictioneer_get_metabox_checkbox(
$post,
'fictioneer_story_no_thumbnail',
__( 'Hide thumbnail on story page', 'fictioneer' )
);
$output['fictioneer_story_no_tags'] = fictioneer_get_metabox_checkbox(
$post,
'fictioneer_story_no_tags',
__( 'Hide tags on story page', 'fictioneer' )
);
if ( ! get_option( 'fictioneer_hide_chapter_icons' ) ) {
$output['fictioneer_story_hide_chapter_icons'] = fictioneer_get_metabox_checkbox(
$post,
'fictioneer_story_hide_chapter_icons',
__( 'Hide chapter icons', 'fictioneer' )
);
}
if ( ! get_option( 'fictioneer_disable_chapter_collapsing' ) ) {
$output['fictioneer_story_disable_collapse'] = fictioneer_get_metabox_checkbox(
$post,
'fictioneer_story_disable_collapse',
__( 'Disable collapsing of chapters', 'fictioneer' )
);
}
if ( get_option( 'fictioneer_enable_chapter_groups' ) ) {
$output['fictioneer_story_disable_groups'] = fictioneer_get_metabox_checkbox(
$post,
'fictioneer_story_disable_groups',
__( 'Disable chapter groups', 'fictioneer' )
);
}
if ( get_option( 'fictioneer_enable_epubs' ) ) {
$output['fictioneer_story_no_epub'] = fictioneer_get_metabox_checkbox(
$post,
'fictioneer_story_no_epub',
__( 'Disable ePUB download', 'fictioneer' )
);
}
if ( current_user_can( 'fcn_custom_page_css', $post->ID ) ) {
$output['fictioneer_story_css'] = fictioneer_get_metabox_textarea(
$post,
'fictioneer_story_css',
array(
'label' => __( 'Custom Story CSS', 'fictioneer' ),
'description' => __( 'Applied to the story and all chapters.', 'fictioneer' ),
'placeholder' => __( '.selector { ... }', 'fictioneer' ),
'input_classes' => 'fictioneer-meta-field__textarea--code'
)
);
}
if ( $advanced && current_user_can( 'manage_options' ) ) {
$output['fictioneer_story_redirect_link'] = fictioneer_get_metabox_url(
$post,
'fictioneer_story_redirect_link',
array(
'label' => _x( 'Redirect Link (Beware!)', 'Story redirect link meta field label.', 'fictioneer' ),
'description' => __( 'URL to redirect to when story is opened.', 'fictioneer' )
)
);
}
// --- Filters ---------------------------------------------------------------
$output = apply_filters( 'fictioneer_filter_metabox_story_meta', $output, $post );
// --- Render ----------------------------------------------------------------
echo implode( '', $output );
// Start HTML ---> ?>
ID}" ); // Accounts for manual wp_update_post() calls!
$output = [];
// --- Add fields ------------------------------------------------------------
// Short description
$output['fictioneer_story_short_description'] = fictioneer_get_metabox_editor(
$post,
'fictioneer_story_short_description',
array(
'label' => _x( 'Short Description', 'Story short description meta field label.', 'fictioneer' ),
'description' => __( 'The first paragraph is used on cards, so keep it nice and concise. The full short description is passed to the Storygraph API.', 'fictioneer' ),
'required' => 1
)
);
// Chapters
$chapter_ids = fictioneer_get_story_chapter_ids( $post->ID );
$chapters = empty( $chapter_ids ) ? [] : get_posts(
array(
'post_type' => 'fcn_chapter',
'post_status' => 'any',
'post__in' => $chapter_ids ?: [0], // Must not be empty!
'orderby' => 'post__in',
'posts_per_page' => -1,
'meta_key' => 'fictioneer_chapter_story',
'meta_value' => $post->ID,
'update_post_meta_cache' => true,
'update_post_term_cache' => false, // Improve performance
'no_found_rows' => true // Improve performance
)
);
$output['fictioneer_story_chapters'] = fictioneer_get_metabox_relationships(
$post,
'fictioneer_story_chapters',
$chapters,
'fictioneer_callback_relationship_chapters',
array(
'label' => _x( 'Chapters', 'Story chapters meta field label.', 'fictioneer' ),
'description' => __( 'Select and order chapters assigned to the story (set in the chapter).', 'fictioneer' ),
'show_info' => 1
)
);
// Custom pages
if ( current_user_can( 'fcn_story_pages', $post->ID ) && FICTIONEER_MAX_CUSTOM_PAGES_PER_STORY > 0 ) {
$page_ids = get_post_meta( $post->ID, 'fictioneer_story_custom_pages', true ) ?: [];
$page_ids = is_array( $page_ids ) ? $page_ids : [];
$pages = empty( $page_ids ) ? [] : get_posts(
array(
'post_type' => 'page',
'post_status' => 'any',
'post__in' => $page_ids ?: [0], // Must not be empty!
'orderby' => 'post__in',
'posts_per_page' => -1,
'author' => get_post_field( 'post_author', $post->ID ),
'update_post_meta_cache' => false, // Improve performance
'update_post_term_cache' => false, // Improve performance
'no_found_rows' => true // Improve performance
)
);
$output['fictioneer_story_custom_pages'] = fictioneer_get_metabox_relationships(
$post,
'fictioneer_story_custom_pages',
$pages,
'fictioneer_callback_relationship_story_pages',
array(
'label' => _x( 'Custom Pages', 'Story pages meta field label.', 'fictioneer' ),
'description' => sprintf(
__( 'Add up to %s pages as tabs to stories. Pages must have a short name or will not be shown.', 'fictioneer' ),
FICTIONEER_MAX_CUSTOM_PAGES_PER_STORY
)
)
);
}
// Global note
$output['fictioneer_story_global_note'] = fictioneer_get_metabox_editor(
$post,
'fictioneer_story_global_note',
array(
'label' => _x( 'Global Note', 'Story global note meta field label.', 'fictioneer' ),
'description' => __( 'Displayed in a box above all chapters; start with "[!password]" to hide in protected chapters. Limited HTML allowed.', 'fictioneer' )
)
);
// Password note
$output['fictioneer_story_password_note'] = fictioneer_get_metabox_editor(
$post,
'fictioneer_story_password_note',
array(
'label' => _x( 'Password Note', 'Story password note meta field label.', 'fictioneer' ),
'description' => __( 'Displayed for password protected content; start with "[!global]" to show on all protected chapters without note. Limited HTML allowed.', 'fictioneer' )
)
);
// --- Filters ---------------------------------------------------------------
$output = apply_filters( 'fictioneer_filter_metabox_story_data', $output, $post );
// --- Render ----------------------------------------------------------------
echo implode( '', $output );
// Start HTML ---> ?>
ID}" ); // Accounts for manual wp_update_post() calls!
$output = [];
// --- Add fields ------------------------------------------------------------
// Custom upload
if ( current_user_can( 'fcn_custom_epub_upload', $post->ID ) ) {
$output['fictioneer_story_ebook_upload_one'] = fictioneer_get_metabox_ebook(
$post,
'fictioneer_story_ebook_upload_one',
array(
'label' => _x( 'Custom Upload', 'Story ebook upload meta field label.', 'fictioneer' ),
'description' => __( 'If you do not want to rely on the ePUB converter, you can upload your own ebook. Allowed formats are epub, mobi, pdf, rtf, and txt.', 'fictioneer' )
)
);
}
// Preface
$output['fictioneer_story_epub_preface'] = fictioneer_get_metabox_editor(
$post,
'fictioneer_story_epub_preface',
array(
'label' => _x( 'ePUB Preface', 'Story ePUB preface meta field label.', 'fictioneer' ),
'description' => __( 'Required for the download. Inserted as separate page between cover and table of contents. Disclaimers, copyright notes, tags, and other front matter goes here.', 'fictioneer' )
)
);
// Afterword
$output['fictioneer_story_epub_afterword'] = fictioneer_get_metabox_editor(
$post,
'fictioneer_story_epub_afterword',
array(
'label' => _x( 'ePUB Afterword', 'Story ePUB afterword meta field label.', 'fictioneer' ),
'description' => __( 'Inserted after the last chapter. Thanks and personal thoughts on the story go here, for example.', 'fictioneer' )
)
);
// CSS
if ( current_user_can( 'fcn_custom_epub_css', $post->ID ) ) {
$output['fictioneer_story_epub_custom_css'] = fictioneer_get_metabox_textarea(
$post,
'fictioneer_story_epub_custom_css',
array(
'label' => _x( 'ePUB CSS', 'Story ePUB CSS meta field label.', 'fictioneer' ),
'description' => __( 'Inject CSS into the ePUB to customize the style for your story. Dangerous.', 'fictioneer' ),
'input_classes' => 'fictioneer-meta-field__textarea--code',
'placeholder' => '.selector { ... }'
)
);
}
// --- Filters ---------------------------------------------------------------
$output = apply_filters( 'fictioneer_filter_metabox_story_epub', $output, $post );
// --- Render ----------------------------------------------------------------
echo implode( '', $output );
// Start HTML ---> ?>
0; });
$chapter_ids = array_unique( $chapter_ids );
if ( empty( $chapter_ids ) ) {
$fields['fictioneer_story_chapters'] = []; // Ensure empty meta is removed
} else {
// Make sure only allowed posts are in
$chapter_query_args = array(
'post_type' => 'fcn_chapter',
'post__in' => $chapter_ids ?: [0], // Must not be empty!
'orderby' => 'post__in',
'fields' => 'ids',
'posts_per_page' => -1,
'update_post_meta_cache' => false, // Improve performance
'update_post_term_cache' => false, // Improve performance
'no_found_rows' => true // Improve performance
);
if ( FICTIONEER_FILTER_STORY_CHAPTERS ) {
$chapter_query_args['meta_key'] = 'fictioneer_chapter_story';
$chapter_query_args['meta_value'] = $post_id;
}
$chapters_query = new WP_Query( $chapter_query_args );
$chapter_ids = array_map( 'strval', $chapters_query->posts );
$fields['fictioneer_story_chapters'] = $chapter_ids;
}
// Remember when chapters have been changed
if ( $previous_chapter_ids !== $chapter_ids ) {
update_post_meta( $post_id, 'fictioneer_chapters_modified', current_time( 'mysql' ) );
}
if ( count( $previous_chapter_ids ) < count( $chapter_ids ) ) {
update_post_meta( $post_id, 'fictioneer_chapters_added', current_time( 'mysql' ) );
}
// Log changes
fictioneer_log_story_chapter_changes( $post_id, $chapter_ids, $previous_chapter_ids );
}
// Custom pages
if (
isset( $_POST['fictioneer_story_custom_pages'] ) &&
current_user_can( 'fcn_story_pages', $post_id )
) {
$page_ids = $_POST['fictioneer_story_custom_pages'];
$page_ids = is_array( $page_ids ) ? $page_ids : [ $page_ids ];
$page_ids = array_map( 'intval', $page_ids );
$page_ids = array_filter( $page_ids, function( $value ) { return $value > 0; });
if ( empty( $page_ids ) || FICTIONEER_MAX_CUSTOM_PAGES_PER_STORY < 1 ) {
$fields['fictioneer_story_custom_pages'] = []; // Ensure empty meta is removed
} else {
$page_ids = array_unique( $page_ids );
$page_ids = array_slice( $page_ids, 0, FICTIONEER_MAX_CUSTOM_PAGES_PER_STORY );
$pages_query = new WP_Query(
array(
'post_type' => 'page',
'post__in' => $page_ids ?: [0], // Must not be empty!
'author' => $post_author_id, // Only allow author's pages
'orderby' => 'post__in',
'fields' => 'ids',
'posts_per_page' => FICTIONEER_MAX_CUSTOM_PAGES_PER_STORY,
'update_post_meta_cache' => false, // Improve performance
'update_post_term_cache' => false, // Improve performance
'no_found_rows' => true // Improve performance
)
);
$fields['fictioneer_story_custom_pages'] = array_map( 'strval', $pages_query->posts );
}
}
// Global note
if ( isset( $_POST['fictioneer_story_global_note'] ) ) {
$fields['fictioneer_story_global_note'] =
fictioneer_sanitize_editor( $_POST['fictioneer_story_global_note'] );
}
// Password note
if ( isset( $_POST['fictioneer_story_password_note'] ) ) {
$fields['fictioneer_story_password_note'] =
fictioneer_sanitize_editor( $_POST['fictioneer_story_password_note'] );
}
// ePUBs...
if ( get_option( 'fictioneer_enable_epubs' ) ) {
// Custom upload
if ( isset( $_POST['fictioneer_story_ebook_upload_one'] ) && current_user_can( 'fcn_custom_epub_upload', $post_id ) ) {
$ebook_id = absint( $_POST['fictioneer_story_ebook_upload_one'] );
if ( $ebook_id > 0 && wp_get_attachment_url( $ebook_id ) ) {
$fields['fictioneer_story_ebook_upload_one'] = $ebook_id;
} else {
$fields['fictioneer_story_ebook_upload_one'] = 0;
}
}
// ePUB preface
if ( isset( $_POST['fictioneer_story_epub_preface'] ) ) {
$fields['fictioneer_story_epub_preface'] = fictioneer_sanitize_editor( $_POST['fictioneer_story_epub_preface'] );
}
// ePUB afterword
if ( isset( $_POST['fictioneer_story_epub_afterword'] ) ) {
$fields['fictioneer_story_epub_afterword'] = fictioneer_sanitize_editor( $_POST['fictioneer_story_epub_afterword'] );
}
// Custom ePUB CSS
if ( isset( $_POST['fictioneer_story_epub_custom_css'] ) && current_user_can( 'fcn_custom_epub_css', $post_id ) ) {
$fields['fictioneer_story_epub_custom_css'] = fictioneer_sanitize_css( $_POST['fictioneer_story_epub_custom_css'] );
}
}
// --- Cleanup -----------------------------------------------------------------
global $wpdb;
// Orphaned values
$wpdb->query(
$wpdb->prepare("
DELETE FROM $wpdb->postmeta
WHERE meta_key = %s
AND (meta_value = '' OR meta_value IS NULL OR meta_value = '0')",
'fictioneer_story_ebook_upload_one'
)
);
// --- Filters ---------------------------------------------------------------
$fields = apply_filters( 'fictioneer_filter_metabox_updates_story', $fields, $post_id );
// --- Save ------------------------------------------------------------------
foreach ( $fields as $key => $value ) {
fictioneer_update_post_meta( $post_id, $key, $value ?? 0 ); // Add, update, or delete (if falsy)
}
}
add_action( 'save_post', 'fictioneer_save_story_metaboxes' );
// =============================================================================
// CHAPTER META FIELDS
// =============================================================================
/**
* Adds chapter meta metabox
*
* @since 5.7.4
*/
function fictioneer_add_chapter_meta_metabox() {
add_meta_box(
'fictioneer-chapter-meta',
__( 'Chapter Meta', 'fictioneer' ),
'fictioneer_render_chapter_meta_metabox',
['fcn_chapter'],
'side',
'default'
);
}
add_action( 'add_meta_boxes', 'fictioneer_add_chapter_meta_metabox' );
/**
* Render chapter meta metabox
*
* @since 5.7.4
*
* @param WP_Post $post The current post object.
*/
function fictioneer_render_chapter_meta_metabox( $post ) {
// --- Setup -----------------------------------------------------------------
$nonce = wp_create_nonce( "chapter_meta_data_{$post->ID}" ); // Accounts for manual wp_update_post() calls!
$output = [];
// --- Add fields ------------------------------------------------------------
if ( ! get_option( 'fictioneer_hide_chapter_icons' ) ) {
$output['fictioneer_chapter_icon'] = fictioneer_get_metabox_icons(
$post,
'fictioneer_chapter_icon',
array(
'label' => _x( 'Icon', 'Chapter icon meta field label.', 'fictioneer' ),
'description' => sprintf(
__( 'You can use all free Font Awesome icons.', 'fictioneer' ),
'https://fontawesome.com/search'
),
'placeholder' => 'fa-solid fa-book'
)
);
}
if ( get_option( 'fictioneer_enable_advanced_meta_fields' ) ) {
$output['fictioneer_chapter_text_icon'] = fictioneer_get_metabox_text(
$post,
'fictioneer_chapter_text_icon',
array(
'label' => _x( 'Text Icon', 'Chapter text icon meta field label.', 'fictioneer' ),
'description' => __( 'Text string as icon; mind the limited space.', 'fictioneer' )
)
);
}
if ( get_option( 'fictioneer_enable_advanced_meta_fields' ) ) {
$output['fictioneer_chapter_short_title'] = fictioneer_get_metabox_text(
$post,
'fictioneer_chapter_short_title',
array(
'label' => _x( 'Short Title', 'Chapter short title meta field label.', 'fictioneer' ),
'description' => __( 'Shorter title, such as "Arc 15, Ch. 17". Not used by default, intended for child themes.', 'fictioneer' )
)
);
}
if ( get_option( 'fictioneer_enable_advanced_meta_fields' ) ) {
$output['fictioneer_chapter_prefix'] = fictioneer_get_metabox_text(
$post,
'fictioneer_chapter_prefix',
array(
'label' => _x( 'Prefix', 'Chapter prefix meta field label.', 'fictioneer' ),
'description' => __( 'Prefix, such as "Prologue" or "Act I ~ 01".', 'fictioneer' )
)
);
}
if ( get_option( 'fictioneer_enable_advanced_meta_fields' ) ) {
$output['fictioneer_chapter_co_authors'] = fictioneer_get_metabox_array(
$post,
'fictioneer_chapter_co_authors',
array(
'label' => _x( 'Co-Authors', 'Chapter co-authors meta field label.', 'fictioneer' ),
'description' => __( 'Comma-separated list of author IDs.', 'fictioneer' )
)
);
}
$output['fictioneer_chapter_rating'] = fictioneer_get_metabox_select(
$post,
'fictioneer_chapter_rating',
array(
'0' => __( '— Unset —', 'fictioneer' ),
'Everyone' => _x( 'Everyone', 'Chapter age rating select option.', 'fictioneer' ),
'Teen' => _x( 'Teen', 'Chapter age rating select option.', 'fictioneer' ),
'Mature' => _x( 'Mature', 'Chapter age rating select option.', 'fictioneer' ),
'Adult' => _x( 'Adult', 'Chapter age rating select option.', 'fictioneer' )
),
array(
'label' => _x( 'Age Rating', 'Chapter age rating meta field label.', 'fictioneer' ),
'description' => __( 'Optional age rating only for the chapter.', 'fictioneer' )
)
);
$output['fictioneer_chapter_warning'] = fictioneer_get_metabox_text(
$post,
'fictioneer_chapter_warning',
array(
'label' => _x( 'Warning', 'Chapter warning meta field label.', 'fictioneer' ),
'description' => __( 'Warning shown in chapter lists.', 'fictioneer' ),
'maxlength' => 48
)
);
$output['fictioneer_chapter_warning_notes'] = fictioneer_get_metabox_textarea(
$post,
'fictioneer_chapter_warning_notes',
array(
'label' => __( 'Warning Notes', 'fictioneer' ),
'description' => __( 'Warning note shown above chapters.', 'fictioneer' )
)
);
$output['flags_heading'] = '' .
__( 'Flags', 'Metabox checkbox heading.', 'fictioneer' ) . '
';
$output['fictioneer_chapter_hidden'] = fictioneer_get_metabox_checkbox(
$post,
'fictioneer_chapter_hidden',
__( 'Unlisted (but accessible with link)', 'fictioneer' )
);
$output['fictioneer_chapter_no_chapter'] = fictioneer_get_metabox_checkbox(
$post,
'fictioneer_chapter_no_chapter',
__( 'Do not count as chapter', 'fictioneer' )
);
$output['fictioneer_chapter_hide_title'] = fictioneer_get_metabox_checkbox(
$post,
'fictioneer_chapter_hide_title',
__( 'Hide title in chapter', 'fictioneer' )
);
$output['fictioneer_chapter_hide_support_links'] = fictioneer_get_metabox_checkbox(
$post,
'fictioneer_chapter_hide_support_links',
__( 'Hide support links', 'fictioneer' )
);
if ( FICTIONEER_ENABLE_PARTIAL_CACHING && current_user_can( 'manage_options' ) ) {
$output['fictioneer_chapter_disable_partial_caching'] = fictioneer_get_metabox_checkbox(
$post,
'fictioneer_chapter_disable_partial_caching',
__( 'Disable partial caching of content', 'fictioneer' )
);
}
// --- Filters ---------------------------------------------------------------
$output = apply_filters( 'fictioneer_filter_metabox_chapter_meta', $output, $post );
// --- Render ----------------------------------------------------------------
echo implode( '', $output );
// Start HTML ---> ?>
ID}" ); // Accounts for manual wp_update_post() calls!
$post_author_id = get_post_field( 'post_author', $post->ID );
$current_story_id = get_post_meta( $post->ID, 'fictioneer_chapter_story', true );
$output = [];
// --- Add fields ------------------------------------------------------------
// Story
$stories = array( '0' => _x( '— Unassigned —', 'Chapter story select option.', 'fictioneer' ) );
$description = __( 'Select the story this chapter belongs to; assign and order in the story.', 'fictioneer' );
$author_warning = '';
$selectable_stories_args = array(
'post_type' => 'fcn_story',
'post_status' => ['publish', 'private'],
'posts_per_page'=> -1,
'update_post_meta_cache' => false, // Improve performance
'update_post_term_cache' => false, // Improve performance
'no_found_rows' => true // Improve performance
);
if ( get_option( 'fictioneer_limit_chapter_stories_by_author' ) ) {
$selectable_stories_args['author'] = $post_author_id;
}
$selectable_stories = new WP_Query( $selectable_stories_args );
if ( $selectable_stories->have_posts() ) {
foreach( $selectable_stories->posts as $story ) {
if ( $story->post_status !== 'publish' ) {
$status_object = get_post_status_object( $story->post_status );
$status_label = $status_object ? $status_object->label : $story->post_status;
$stories[ $story->ID ] = sprintf(
_x( '%s (%s)', 'Chapter story meta field option with status label.', 'fictioneer' ),
fictioneer_get_safe_title( $story->ID, 'admin-render-chapter-data-metabox-selectable-suffix' ),
$status_label
);
} else {
$stories[ $story->ID ] = fictioneer_get_safe_title( $story->ID, 'admin-render-chapter-data-metabox-selectable' );
}
}
}
if ( $current_story_id ) {
// Check unmatched story assignment...
if ( ! array_key_exists( $current_story_id, $stories ) ) {
$other_author_id = get_post_field( 'post_author', $current_story_id );
$suffix = [];
// Other author
if ( $other_author_id != $post_author_id ) {
$suffix['author'] = get_the_author_meta( 'display_name', $other_author_id );
$author_warning = __( 'Warning: The selected story belongs to another author. If you change the selection, you cannot go back.', 'fictioneer' );
}
// Other status
if ( get_post_status( $current_story_id ) === 'publish' ) {
$status_object = get_post_status_object( $story->post_status );
$status_label = $status_object ? $status_object->label : $story->post_status;
$suffix['status'] = $status_label;
}
// Prepare suffix
if ( ! empty( $suffix ) ) {
$suffix = ' (' . implode( ' | ', $suffix ) . ')';
}
$stories[ $current_story_id ] = sprintf(
_x( '%s %s', 'Chapter story meta field mismatched option with author and/or status label.', 'fictioneer' ),
fictioneer_get_safe_title( $current_story_id, 'admin-render-chapter-data-metabox-current-suffix' ),
$suffix
);
}
}
if ( get_option( 'fictioneer_enable_chapter_appending' ) ) {
$description = __( 'Select the story this chapter belongs to; new published chapters are automatically appended to the story.', 'fictioneer' );
}
$output['fictioneer_chapter_story'] = fictioneer_get_metabox_select(
$post,
'fictioneer_chapter_story',
$stories,
array(
'label' => _x( 'Story', 'Chapter story meta field label.', 'fictioneer' ),
'description' => $description . ' ' . $author_warning,
'attributes' => array(
'data-action="select-story"',
"data-target='chapter_groups_for_{$post->ID}'"
),
'asort' => 1,
'query_var' => 'story_id'
)
);
// Card/List title
$output['fictioneer_chapter_list_title'] = fictioneer_get_metabox_text(
$post,
'fictioneer_chapter_list_title',
array(
'label' => _x( 'Card/List Title', 'Chapter card/list title meta field label.', 'fictioneer' ),
'description' => __( 'Alternative title used in cards and lists. Useful for long titles that get truncated on small screens.', 'fictioneer' )
)
);
// Group
$output['fictioneer_chapter_group'] = fictioneer_get_metabox_text(
$post,
'fictioneer_chapter_group',
array(
'label' => _x( 'Group', 'Chapter group meta field label.', 'fictioneer' ),
'description' => __( 'Organize chapters into groups; mind the order and spelling (case-sensitive). Only rendered if there are at least two, unassigned chapters are grouped under "Unassigned".', 'fictioneer' ),
'attributes' => array(
"list='chapter_groups_for_{$post->ID}'"
)
)
);
// Foreword
$output['fictioneer_chapter_foreword'] = fictioneer_get_metabox_editor(
$post,
'fictioneer_chapter_foreword',
array(
'label' => _x( 'Foreword', 'Chapter foreword meta field label.', 'fictioneer' ),
'description' => __( 'Displayed in a box above the chapter; start with "[!show]" to show in protected chapters. Limited HTML allowed.', 'fictioneer' )
)
);
// Afterword
$output['fictioneer_chapter_afterword'] = fictioneer_get_metabox_editor(
$post,
'fictioneer_chapter_afterword',
array(
'label' => _x( 'Afterword', 'Chapter afterword meta field label.', 'fictioneer' ),
'description' => __( 'Displayed in a box below the chapter; start with "[!show]" to show in protected chapters. Limited HTML allowed.', 'fictioneer' )
)
);
// Password note
$output['fictioneer_chapter_password_note'] = fictioneer_get_metabox_editor(
$post,
'fictioneer_chapter_password_note',
array(
'label' => _x( 'Password Note', 'Chapter password note meta field label.', 'fictioneer' ),
'description' => __( 'Displayed for password protected content. Limited HTML allowed.', 'fictioneer' )
)
);
// --- Filters ---------------------------------------------------------------
$output = apply_filters( 'fictioneer_filter_metabox_chapter_data', $output, $post );
// --- Render ----------------------------------------------------------------
echo implode( '', $output );
// Start HTML ---> ?>
$value ) {
fictioneer_update_post_meta( $post_id, $key, $value ?? 0 ); // Add, update, or delete (if falsy)
}
}
add_action( 'save_post', 'fictioneer_save_chapter_metaboxes' );
// =============================================================================
// EXTRA META FIELDS
// =============================================================================
/**
* Adds extra metabox
*
* @since 5.7.4
*/
function fictioneer_add_extra_metabox() {
add_meta_box(
'fictioneer-extra',
__( 'Extra Meta', 'fictioneer' ),
'fictioneer_render_extra_metabox',
['post', 'page', 'fcn_story', 'fcn_chapter', 'fcn_recommendation', 'fcn_collection'],
'side',
'default'
);
}
add_action( 'add_meta_boxes', 'fictioneer_add_extra_metabox' );
/**
* Render extra metabox
*
* @since 5.7.4
*
* @param WP_Post $post The current post object.
*/
function fictioneer_render_extra_metabox( $post ) {
// --- Setup -----------------------------------------------------------------
$nonce = wp_create_nonce( "advanced_meta_{$post->ID}" ); // Accounts for manual wp_update_post() calls!
$output = [];
$author_id = $post->post_author ?: get_current_user_id();
// --- Add fields ------------------------------------------------------------
// Landscape image
$output['fictioneer_landscape_image'] = fictioneer_get_metabox_image(
$post,
'fictioneer_landscape_image',
array(
'label' => __( 'Landscape Image', 'fictioneer' ),
'description' => __( 'Used where the image is wider than high.', 'fictioneer' ),
'button' => __( 'Set landscape image', 'fictioneer' )
)
);
// Custom page header
if ( current_user_can( 'fcn_custom_page_header', $post->ID ) ) {
$output['fictioneer_custom_header_image'] = fictioneer_get_metabox_image(
$post,
'fictioneer_custom_header_image',
array(
'label' => __( 'Header Image', 'fictioneer' ),
'description' => __( 'Replaces the default header image.', 'fictioneer' ),
'button' => __( 'Set header image', 'fictioneer' ),
)
);
}
// Custom page CSS
if ( current_user_can( 'fcn_custom_page_css', $post->ID ) ) {
$output['fictioneer_custom_css'] = fictioneer_get_metabox_textarea(
$post,
'fictioneer_custom_css',
array(
'label' => __( 'Custom Page CSS', 'fictioneer' ),
'description' => __( 'Only applied to the page.', 'fictioneer' ),
'placeholder' => __( '.selector { ... }', 'fictioneer' ),
'input_classes' => 'fictioneer-meta-field__textarea--code'
)
);
}
if ( $post->post_type === 'page' ) {
// Short name
$output['fictioneer_short_name'] = fictioneer_get_metabox_text(
$post,
'fictioneer_short_name',
array(
'label' => _x( 'Short Name', 'Page short name meta field label.', 'fictioneer' ),
'description' => __( 'Required for the tab view in stories.', 'fictioneer' )
)
);
// Filter & Search ID
if ( current_user_can( 'install_plugins' ) ) {
$output['fictioneer_filter_and_search_id'] = fictioneer_get_metabox_text(
$post,
'fictioneer_filter_and_search_id',
array(
'label' => _x( 'Filter & Search ID', 'Page filter ans search meta field label.', 'fictioneer' ),
'description' => __( 'ID for a filter and/or search plugins.', 'fictioneer' )
)
);
}
// Story ID
if ( current_user_can( 'manage_options' ) ) {
$output['fictioneer_template_story_id'] = fictioneer_get_metabox_text(
$post,
'fictioneer_template_story_id',
array(
'label' => _x( 'Story Id', 'Page story ID meta field label.', 'fictioneer' ),
'description' => __( 'Used by the "Story Page/Mirror" templates.', 'fictioneer' )
)
);
}
// Checkbox: Show story header
if ( current_user_can( 'manage_options' ) ) {
$output['fictioneer_template_show_story_header'] = fictioneer_get_metabox_checkbox(
$post,
'fictioneer_template_show_story_header',
__( 'Show story header', 'fictioneer' )
);
}
}
// Story blogs
if ( $post->post_type === 'post' ) {
$user_stories = get_posts(
array(
'post_type' => 'fcn_story',
'post_status' => ['publish', 'private'],
'author' => $author_id,
'posts_per_page' => -1,
'update_post_meta_cache' => false, // Improve performance
'update_post_term_cache' => false, // Improve performance
'no_found_rows' => true // Improve performance
)
);
if ( ! empty( $user_stories ) ) {
$blog_options = array( '0' => __( '— Story —', 'fictioneer' ) );
foreach ( $user_stories as $story ) {
$blog_options[ $story->ID ] = $story->post_title;
}
$output['fictioneer_post_story_blogs'] = fictioneer_get_metabox_tokens(
$post,
'fictioneer_post_story_blogs',
$blog_options,
array(
'label' => _x( 'Story Blogs', 'Story blogs meta field label.', 'fictioneer' ),
'description' => __( 'Select where the post should appear.', 'fictioneer' )
)
);
}
}
// Patreon
$can_patreon = current_user_can( 'fcn_assign_patreon_tiers' ) || current_user_can( 'manage_options' );
if ( $can_patreon && get_option( 'fictioneer_enable_patreon_locks' ) ) {
$patreon_client_id = fictioneer_get_oauth_client_credentials( 'patreon' );
$patreon_client_secret = fictioneer_get_oauth_client_credentials( 'patreon', 'secret' );
if ( get_option( 'fictioneer_enable_oauth' ) && $patreon_client_id && $patreon_client_secret ) {
$patreon_tiers = get_option( 'fictioneer_connection_patreon_tiers' );
$patreon_tiers = is_array( $patreon_tiers ) ? $patreon_tiers : [];
if ( ! empty( $patreon_tiers ) ) {
$tier_options = array( '0' => __( '— Tier —', 'fictioneer' ) );
foreach ( $patreon_tiers as $tier ) {
$tier_options[ $tier['id'] ] = sprintf(
_x( '%s (%s)', 'Patreon tier meta field token (title and amount_cents).', 'fictioneer' ),
$tier['title'],
$tier['amount_cents']
);
}
// Tiers
$output['fictioneer_patreon_lock_tiers'] = fictioneer_get_metabox_tokens(
$post,
'fictioneer_patreon_lock_tiers',
$tier_options,
array(
'label' => _x( 'Patreon Tiers', 'Patreon tiers meta field label.', 'fictioneer' ),
'description' => __( 'Select which tiers ignore the password.', 'fictioneer' ),
'names' => $tier_options
)
);
// Threshold
$output['fictioneer_patreon_lock_amount'] = fictioneer_get_metabox_number(
$post,
'fictioneer_patreon_lock_amount',
array(
'label' => _x( 'Patreon Amount Cents', 'Patreon amount cents meta field label.', 'fictioneer' ),
'description' => __( 'Pledge threshold to ignore the password.', 'fictioneer' ),
'attributes' => array(
'min="0"'
)
)
);
}
}
}
// Password expiration datetime
if ( current_user_can( 'manage_options' ) || current_user_can( 'fcn_expire_passwords' ) ) {
$output['fictioneer_post_password_expiration_date'] = fictioneer_get_metabox_datetime(
$post,
'fictioneer_post_password_expiration_date',
array(
'label' => _x( 'Expire Post Password', 'Password expiration meta field label.', 'fictioneer' ),
'description' => __( 'Removes the password after the date.', 'fictioneer' ),
)
);
}
// Checkbox: Disable new comments
if ( in_array( $post->post_type, ['post', 'page', 'fcn_story', 'fcn_chapter'] ) ) {
$output['flags_heading'] = '' .
_x( 'Flags', 'Metabox checkbox heading.', 'fictioneer' ) . '
';
$output['fictioneer_disable_commenting'] = fictioneer_get_metabox_checkbox(
$post,
'fictioneer_disable_commenting',
__( 'Disable new comments', 'fictioneer' )
);
}
// --- Filters ---------------------------------------------------------------
$output = apply_filters( 'fictioneer_filter_metabox_advanced', $output, $post );
// --- Render ----------------------------------------------------------------
echo implode( '', $output );
// Start HTML ---> ?>
['edit_posts', 'edit_published_posts'],
'page' => ['edit_pages', 'edit_published_pages'],
'fcn_story' => ['edit_fcn_stories', 'edit_published_fcn_stories'],
'fcn_chapter' => ['edit_fcn_chapters', 'edit_published_fcn_chapters'],
'fcn_collection' => ['edit_fcn_collections', 'edit_published_fcn_collections'],
'fcn_recommendation' => ['edit_fcn_recommendations', 'edit_published_fcn_recommendations']
);
if (
! current_user_can( $permission_list[ $post_type ][0], $post_id ) ||
( get_post_status( $post_id ) === 'publish' && ! current_user_can( $permission_list[ $post_type ][1], $post_id ) )
) {
return;
}
// --- Sanitize and add data ---------------------------------------------------
$post_author_id = get_post_field( 'post_author', $post_id );
$fields = [];
// Landscape image
if ( isset( $_POST['fictioneer_landscape_image'] ) ) {
$fields['fictioneer_landscape_image'] = absint( $_POST['fictioneer_landscape_image'] );
}
// Custom page header
if ( isset( $_POST['fictioneer_custom_header_image'] ) && current_user_can( 'fcn_custom_page_header', $post_id ) ) {
$fields['fictioneer_custom_header_image'] = absint( $_POST['fictioneer_custom_header_image'] );
}
// Custom page CSS
if ( isset( $_POST['fictioneer_custom_css'] ) && current_user_can( 'fcn_custom_page_css', $post_id ) ) {
$fields['fictioneer_custom_css'] = fictioneer_sanitize_css( $_POST['fictioneer_custom_css'] );
}
// Short name
if ( isset( $_POST['fictioneer_short_name'] ) && $post_type === 'page' ) {
$fields['fictioneer_short_name'] = sanitize_text_field( $_POST['fictioneer_short_name'] );
if ( empty( $fields['fictioneer_short_name'] ) ) {
$fields['fictioneer_short_name'] = fictioneer_truncate( get_the_title( $post_id ), 24 );
}
}
// Search & Filter ID
if ( isset( $_POST['fictioneer_filter_and_search_id'] ) && $post_type === 'page' ) {
if ( current_user_can( 'install_plugins' ) ) {
$fields['fictioneer_filter_and_search_id'] = absint( $_POST['fictioneer_filter_and_search_id'] );
}
}
// Story ID
if ( isset( $_POST['fictioneer_template_story_id'] ) && $post_type === 'page' ) {
$template_story_id = absint( $_POST['fictioneer_template_story_id'] );
if ( current_user_can( 'manage_options' ) && fictioneer_validate_id( $template_story_id, 'fcn_story' ) ) {
$fields['fictioneer_template_story_id'] = $template_story_id;
} else {
$fields['fictioneer_template_story_id'] = '';
}
}
// Checkbox: Show story template header
if (
isset( $_POST['fictioneer_template_show_story_header'] ) &&
$post_type === 'page' &&
current_user_can( 'manage_options' )
) {
$fields['fictioneer_template_show_story_header'] =
fictioneer_sanitize_checkbox( $_POST['fictioneer_template_show_story_header'] );
}
// Story blogs
if ( isset( $_POST['fictioneer_post_story_blogs'] ) && $post_type === 'post' ) {
$story_blogs = fictioneer_explode_list( $_POST['fictioneer_post_story_blogs'] );
if ( ! empty( $story_blogs ) ) {
$story_blogs = array_map( 'absint', $story_blogs );
$story_blogs = array_unique( $story_blogs );
// Ensure the stories belong to the post author
$blog_story_query = new WP_Query(
array(
'author' => $post_author_id,
'post_type' => 'fcn_story',
'post_status' => ['publish', 'private'],
'post__in' => $story_blogs ?: [0], // Must not be empty!
'fields' => 'ids',
'posts_per_page'=> -1,
'update_post_meta_cache' => false, // Improve performance
'update_post_term_cache' => false, // Improve performance
'no_found_rows' => true // Improve performance
)
);
$story_blogs = $blog_story_query->posts;
$story_blogs = array_map( 'strval', $story_blogs ); // Safer to match with LIKE in SQL
}
$fields['fictioneer_post_story_blogs'] = $story_blogs;
}
// Patreon
$can_patreon = current_user_can( 'fcn_assign_patreon_tiers' ) || current_user_can( 'manage_options' );
if ( $can_patreon && get_option( 'fictioneer_enable_patreon_locks' ) ) {
$patreon_client_id = fictioneer_get_oauth_client_credentials( 'patreon' );
$patreon_client_secret = fictioneer_get_oauth_client_credentials( 'patreon', 'secret' );
$patreon_tiers = get_option( 'fictioneer_connection_patreon_tiers' );
$patreon_tiers = is_array( $patreon_tiers ) ? $patreon_tiers : [];
if ( get_option( 'fictioneer_enable_oauth' ) && $patreon_client_id && $patreon_client_secret && $patreon_tiers ) {
// Tiers
if ( isset( $_POST['fictioneer_patreon_lock_tiers'] ) ) {
$selected_patreon_tiers = fictioneer_explode_list( $_POST['fictioneer_patreon_lock_tiers'] );
if ( ! empty( $patreon_tiers ) ) {
$selected_patreon_tiers = array_intersect( $selected_patreon_tiers, array_keys( $patreon_tiers ) );
$selected_patreon_tiers = array_map( 'absint', $selected_patreon_tiers );
$selected_patreon_tiers = array_unique( $selected_patreon_tiers );
$selected_patreon_tiers = array_map( 'strval', $selected_patreon_tiers ); // Safer to match with LIKE in SQL
}
$fields['fictioneer_patreon_lock_tiers'] = $selected_patreon_tiers;
}
// Threshold
if ( isset( $_POST['fictioneer_patreon_lock_amount'] ) ) {
$fields['fictioneer_patreon_lock_amount'] = absint( $_POST['fictioneer_patreon_lock_amount'] );
}
}
}
// Password expiration datetime
$can_expire_passwords = current_user_can( 'manage_options' ) || current_user_can( 'fcn_expire_passwords' );
if ( $can_expire_passwords && isset( $_POST['fictioneer_post_password_expiration_date'] ) ) {
$expiration_date = $_POST['fictioneer_post_password_expiration_date'];
if ( ! empty( $expiration_date ) ) {
$local_datetime = new DateTime( $expiration_date, wp_timezone() );
$local_datetime->setTimezone( new DateTimeZone( 'UTC' ) );
$fields['fictioneer_post_password_expiration_date'] = $local_datetime->format( 'Y-m-d H:i:s' );
} else {
$fields['fictioneer_post_password_expiration_date'] = 0;
}
}
// Checkbox: Disable new comments
if (
isset( $_POST['fictioneer_disable_commenting'] ) &&
in_array( $post_type, ['post', 'page', 'fcn_story', 'fcn_chapter'] )
) {
$fields['fictioneer_disable_commenting'] = fictioneer_sanitize_checkbox( $_POST['fictioneer_disable_commenting'] );
}
// --- Filters -----------------------------------------------------------------
$fields = apply_filters( 'fictioneer_filter_metabox_updates_advanced', $fields, $post_id );
// --- Save --------------------------------------------------------------------
foreach ( $fields as $key => $value ) {
fictioneer_update_post_meta( $post_id, $key, $value ?? 0 ); // Add, update, or delete (if falsy)
}
}
add_action( 'save_post', 'fictioneer_save_extra_metabox' );
// =============================================================================
// SUPPORT LINKS META FIELDS
// =============================================================================
/**
* Adds support links metabox
*
* @since 5.7.4
*/
function fictioneer_add_support_links_metabox() {
add_meta_box(
'fictioneer-support-links',
__( 'Support Links', 'fictioneer' ),
'fictioneer_render_support_links_metabox',
['post', 'fcn_story', 'fcn_chapter'],
'side',
'low'
);
}
add_action( 'add_meta_boxes', 'fictioneer_add_support_links_metabox' );
/**
* Render support links metabox
*
* @since 5.7.4
*
* @param WP_Post $post The current post object.
*/
function fictioneer_render_support_links_metabox( $post ) {
// --- Setup -------------------------------------------------------------------
$nonce = wp_create_nonce( "support_links_{$post->ID}" ); // Accounts for manual wp_update_post() calls!
$output = [];
// --- Add fields --------------------------------------------------------------
$output['fictioneer_patreon_link'] = fictioneer_get_metabox_url(
$post,
'fictioneer_patreon_link',
array(
'label' => _x( 'Patreon Link', 'Patreon link meta field label.', 'fictioneer' )
)
);
$output['fictioneer_kofi_link'] = fictioneer_get_metabox_url(
$post,
'fictioneer_kofi_link',
array(
'label' => _x( 'Ko-fi Link', 'Ko-fi link meta field label.', 'fictioneer' )
)
);
$output['fictioneer_subscribestar_link'] = fictioneer_get_metabox_url(
$post,
'fictioneer_subscribestar_link',
array(
'label' => _x( 'SubscribeStar Link', 'SubscribeStar link meta field label.', 'fictioneer' )
)
);
$output['fictioneer_paypal_link'] = fictioneer_get_metabox_url(
$post,
'fictioneer_paypal_link',
array(
'label' => _x( 'Paypal Link', 'Paypal link meta field label.', 'fictioneer' )
)
);
$output['fictioneer_donation_link'] = fictioneer_get_metabox_url(
$post,
'fictioneer_donation_link',
array(
'label' => _x( 'Donation Link', 'Donation link meta field label.', 'fictioneer' )
)
);
// --- Filters -----------------------------------------------------------------
$output = apply_filters( 'fictioneer_filter_metabox_support_links', $output, $post );
// --- Render ------------------------------------------------------------------
echo implode( '', $output );
// Start HTML ---> ?>
['edit_posts', 'edit_published_posts'],
'fcn_story' => ['edit_fcn_stories', 'edit_published_fcn_stories'],
'fcn_chapter' => ['edit_fcn_chapters', 'edit_published_fcn_chapters']
);
if (
! current_user_can( $permission_list[ $post_type ][0], $post_id ) ||
( get_post_status( $post_id ) === 'publish' && ! current_user_can( $permission_list[ $post_type ][1], $post_id ) )
) {
return;
}
// --- Sanitize and add data ---------------------------------------------------
$fields = [];
// Patreon link
if ( isset( $_POST['fictioneer_patreon_link'] ) ) {
$patreon = sanitize_url( $_POST['fictioneer_patreon_link'] );
$patreon = filter_var( $patreon, FILTER_VALIDATE_URL ) ? $patreon : '';
$patreon = preg_match( '#^https://(www\.)?patreon#', $patreon ) ? $patreon : '';
$fields['fictioneer_patreon_link'] = $patreon;
}
// Ko-fi link
if ( isset( $_POST['fictioneer_kofi_link'] ) ) {
$kofi = sanitize_url( $_POST['fictioneer_kofi_link'] );
$kofi = filter_var( $kofi, FILTER_VALIDATE_URL ) ? $kofi : '';
$kofi = preg_match( '#^https://(www\.)?ko-fi#', $kofi ) ? $kofi : '';
$fields['fictioneer_kofi_link'] = $kofi;
}
// SubscribeStar link
if ( isset( $_POST['fictioneer_subscribestar_link'] ) ) {
$subscribe_star = sanitize_url( $_POST['fictioneer_subscribestar_link'] );
$subscribe_star = filter_var( $subscribe_star, FILTER_VALIDATE_URL ) ? $subscribe_star : '';
$subscribe_star = preg_match( '#^https://(www\.)?subscribestar#', $subscribe_star ) ? $subscribe_star : '';
$fields['fictioneer_subscribestar_link'] = $subscribe_star;
}
// Paypal link
if ( isset( $_POST['fictioneer_paypal_link'] ) ) {
$paypal = sanitize_url( $_POST['fictioneer_paypal_link'] );
$paypal = filter_var( $paypal, FILTER_VALIDATE_URL ) ? $paypal : '';
$paypal = preg_match( '#^https://(www\.)?paypal#', $paypal ) ? $paypal : '';
$fields['fictioneer_paypal_link'] = $paypal;
}
// Donation link
if ( isset( $_POST['fictioneer_donation_link'] ) ) {
$donation = sanitize_url( $_POST['fictioneer_donation_link'] );
$donation = filter_var( $donation, FILTER_VALIDATE_URL ) ? $donation : '';
$donation = strpos( $donation, 'https://' ) === 0 ? $donation : '';
$fields['fictioneer_donation_link'] = $donation;
}
// --- Filters -----------------------------------------------------------------
$fields = apply_filters( 'fictioneer_filter_metabox_updates_support_links', $fields, $post_id );
// --- Save --------------------------------------------------------------------
foreach ( $fields as $key => $value ) {
fictioneer_update_post_meta( $post_id, $key, $value ?? 0 ); // Add, update, or delete (if falsy)
}
}
add_action( 'save_post', 'fictioneer_save_support_links_metabox' );
// =============================================================================
// BLOG POST META FIELDS
// =============================================================================
/**
* Adds featured content side metabox
*
* @since 5.7.4
*/
function fictioneer_add_featured_content_metabox() {
add_meta_box(
'fictioneer-featured-content',
__( 'Featured Content', 'fictioneer' ),
'fictioneer_render_featured_content_metabox',
'post',
'normal',
'default'
);
}
add_action( 'add_meta_boxes', 'fictioneer_add_featured_content_metabox' );
/**
* Render featured content metabox
*
* @since 5.7.4
*
* @param WP_Post $post The current post object.
*/
function fictioneer_render_featured_content_metabox( $post ) {
// --- Setup -------------------------------------------------------------------
$nonce = wp_create_nonce( "post_data_{$post->ID}" ); // Accounts for manual wp_update_post() calls!
$output = [];
// --- Add fields --------------------------------------------------------------
// Featured items
$item_ids = get_post_meta( $post->ID, 'fictioneer_post_featured', true ) ?: [];
$item_ids = is_array( $item_ids ) ? $item_ids : [];
$items = empty( $item_ids ) ? [] : get_posts(
array(
'post_type' => ['post', 'fcn_story', 'fcn_chapter', 'fcn_collection', 'fcn_recommendation'],
'post_status' => 'any',
'post__in' => $item_ids ?: [0], // Must not be empty!
'orderby' => 'post__in',
'posts_per_page' => -1,
'update_post_meta_cache' => false, // Improve performance
'update_post_term_cache' => false, // Improve performance
'no_found_rows' => true // Improve performance
)
);
$output['fictioneer_post_featured'] = fictioneer_get_metabox_relationships(
$post,
'fictioneer_post_featured',
$items,
'fictioneer_callback_relationship_featured',
array(
'description' => __( 'Select posts to be shown as cards below the content.', 'fictioneer' ),
'post_types' => array(
'excluded' => ['attachment', 'page']
)
)
);
// --- Filters -----------------------------------------------------------------
$output = apply_filters( 'fictioneer_filter_metabox_post_data', $output, $post );
// --- Render ------------------------------------------------------------------
echo implode( '', $output );
// Start HTML ---> ?>
0; });
$item_ids = array_unique( $item_ids );
if ( empty( $item_ids ) ) {
$fields['fictioneer_post_featured'] = []; // Ensure empty meta is removed
} else {
// Make sure only allowed posts are in
$items_query = new WP_Query(
array(
'post_type' => ['post', 'fcn_story', 'fcn_chapter', 'fcn_collection', 'fcn_recommendation'],
'post_status' => 'publish',
'post__in' => $item_ids ?: [0], // Must not be empty!
'orderby' => 'post__in',
'fields' => 'ids',
'posts_per_page' => -1,
'update_post_meta_cache' => false, // Improve performance
'update_post_term_cache' => false, // Improve performance
'no_found_rows' => true // Improve performance
)
);
$fields['fictioneer_post_featured'] = array_map( 'strval', $items_query->posts );
}
}
// --- Filters -----------------------------------------------------------------
$fields = apply_filters( 'fictioneer_filter_metabox_updates_post', $fields, $post_id );
// --- Save --------------------------------------------------------------------
foreach ( $fields as $key => $value ) {
fictioneer_update_post_meta( $post_id, $key, $value ?? 0 ); // Add, update, or delete (if falsy)
}
// --- After update ------------------------------------------------------------
// Update relationship registry
if ( isset( $_POST['fictioneer_post_featured'] ) ) {
fictioneer_update_post_relationship_registry( $post_id );
}
}
add_action( 'save_post', 'fictioneer_save_post_metaboxes' );
/**
* Update relationship registry for 'post' post types
*
* @since 5.0.0
*
* @param int $post_id The post ID.
*/
function fictioneer_update_post_relationship_registry( $post_id ) {
// Setup
$registry = fictioneer_get_relationship_registry();
$featured = get_post_meta( $post_id, 'fictioneer_post_featured', true );
// Update relationships
$registry[ $post_id ] = [];
if ( is_array( $featured ) && ! empty( $featured ) ) {
foreach ( $featured as $featured_id ) {
$registry[ $post_id ][ $featured_id ] = 'is_featured';
if ( ! isset( $registry[ $featured_id ] ) ) {
$registry[ $featured_id ] = [];
}
$registry[ $featured_id ][ $post_id ] = 'featured_by';
}
} else {
$featured = [];
}
// Check for and remove outdated direct references
foreach ( $registry as $key => $entry ) {
// Skip if...
if ( absint( $key ) < 1 || ! is_array( $entry ) || in_array( $key, $featured ) ) {
continue;
}
// Unset if in array
unset( $registry[ $key ][ $post_id ] );
// Remove node if empty
if ( empty( $registry[ $key ] ) ) {
unset( $registry[ $key ] );
}
}
// Update database
fictioneer_save_relationship_registry( $registry );
}
// =============================================================================
// COLLECTION META FIELDS
// =============================================================================
/**
* Adds collection data metabox
*
* @since 5.7.4
*/
function fictioneer_add_collection_data_metabox() {
add_meta_box(
'fictioneer-collection-data',
__( 'Collection Data', 'fictioneer' ),
'fictioneer_render_collection_data_metabox',
'fcn_collection',
'normal',
'default'
);
}
add_action( 'add_meta_boxes', 'fictioneer_add_collection_data_metabox' );
/**
* Render collection data metabox
*
* @since 5.7.4
*
* @param WP_Post $post The current post object.
*/
function fictioneer_render_collection_data_metabox( $post ) {
// --- Setup -------------------------------------------------------------------
$nonce = wp_create_nonce( "collection_data_{$post->ID}" ); // Accounts for manual wp_update_post() calls!
$output = [];
// --- Add fields --------------------------------------------------------------
// Card/List title
$output['fictioneer_collection_list_title'] = fictioneer_get_metabox_text(
$post,
'fictioneer_collection_list_title',
array(
'label' => _x( 'Card/List Title', 'Collection card/list title meta field label.', 'fictioneer' ),
'description' => __( 'Alternative title used in cards and lists. Useful for long titles that get truncated on small screens.', 'fictioneer' )
)
);
// Short description
$output['fictioneer_collection_description'] = fictioneer_get_metabox_editor(
$post,
'fictioneer_collection_description',
array(
'label' => _x( 'Short Description', 'Collection short description meta field label.', 'fictioneer' ),
'description' => __( 'The short description is used on cards, so best keep it under 100 words or 400 characters.', 'fictioneer' ),
'required' => 1
)
);
// Collection items
$item_ids = get_post_meta( $post->ID, 'fictioneer_collection_items', true ) ?: [];
$item_ids = is_array( $item_ids ) ? $item_ids : [];
$items = empty( $item_ids ) ? [] : get_posts(
array(
'post_type' => ['post', 'page', 'fcn_story', 'fcn_chapter', 'fcn_collection', 'fcn_recommendation'],
'post_status' => 'any',
'post__in' => $item_ids ?: [0], // Must not be empty!
'orderby' => 'post__in',
'posts_per_page' => -1,
'update_post_meta_cache' => false, // Improve performance
'update_post_term_cache' => false, // Improve performance
'no_found_rows' => true // Improve performance
)
);
$output['fictioneer_collection_items'] = fictioneer_get_metabox_relationships(
$post,
'fictioneer_collection_items',
$items,
'fictioneer_callback_relationship_collection',
array(
'label' => _x( 'Collection Items', 'Collection Items meta field label.', 'fictioneer' ),
'description' => __(
'Select the posts you want to be featured. If you add a story, do not add its chapters separately.', 'fictioneer'
),
'post_types' => array(
'excluded' => ['attachment']
)
)
);
// --- Filters -----------------------------------------------------------------
$output = apply_filters( 'fictioneer_filter_metabox_collection_data', $output, $post );
// --- Render ------------------------------------------------------------------
echo implode( '', $output );
// Start HTML ---> ?>
0; });
$item_ids = array_unique( $item_ids );
$forbidden = array_unique(
array(
get_option( 'fictioneer_user_profile_page', 0 ),
get_option( 'fictioneer_bookmarks_page', 0 ),
get_option( 'fictioneer_stories_page', 0 ),
get_option( 'fictioneer_chapters_page', 0 ),
get_option( 'fictioneer_recommendations_page', 0 ),
get_option( 'fictioneer_collections_page', 0 ),
get_option( 'fictioneer_bookshelf_page', 0 ),
get_option( 'fictioneer_404_page', 0 ),
get_option( 'page_on_front', 0 ),
get_option( 'page_for_posts', 0 )
)
);
$item_ids = array_diff( $item_ids, array_map( 'strval', $forbidden ) );
if ( empty( $item_ids ) ) {
$fields['fictioneer_collection_items'] = []; // Ensure empty meta is removed
} else {
// Make sure only allowed posts are in
$items_query = new WP_Query(
array(
'post_type' => ['post', 'page', 'fcn_story', 'fcn_chapter', 'fcn_collection', 'fcn_recommendation'],
'post_status' => 'publish',
'post__in' => $item_ids ?: [0], // Must not be empty!
'orderby' => 'post__in',
'fields' => 'ids',
'posts_per_page' => -1,
'update_post_meta_cache' => false, // Improve performance
'update_post_term_cache' => false, // Improve performance
'no_found_rows' => true // Improve performance
)
);
$fields['fictioneer_collection_items'] = array_map( 'strval', $items_query->posts );
}
}
// --- Filters -----------------------------------------------------------------
$fields = apply_filters( 'fictioneer_filter_metabox_updates_collection', $fields, $post_id );
// --- Save --------------------------------------------------------------------
foreach ( $fields as $key => $value ) {
fictioneer_update_post_meta( $post_id, $key, $value ?? 0 ); // Add, update, or delete (if falsy)
}
}
add_action( 'save_post', 'fictioneer_save_collection_metaboxes' );
// =============================================================================
// RECOMMENDATION META FIELDS
// =============================================================================
/**
* Adds recommendation data metabox
*
* @since 5.7.4
*/
function fictioneer_add_recommendation_data_metabox() {
add_meta_box(
'fictioneer-recommendation-data',
__( 'Recommendation Data', 'fictioneer' ),
'fictioneer_render_recommendation_data_metabox',
'fcn_recommendation',
'normal',
'default'
);
}
add_action( 'add_meta_boxes', 'fictioneer_add_recommendation_data_metabox' );
/**
* Render recommendation data metabox
*
* @since 5.7.4
*
* @param WP_Post $post The current post object.
*/
function fictioneer_render_recommendation_data_metabox( $post ) {
// --- Setup -------------------------------------------------------------------
$nonce = wp_create_nonce( "recommendation_data_{$post->ID}" ); // Accounts for manual wp_update_post() calls!
$output = [];
// --- Add fields --------------------------------------------------------------
// One sentence
$output['fictioneer_recommendation_one_sentence'] = fictioneer_get_metabox_text(
$post,
'fictioneer_recommendation_one_sentence',
array(
'label' => _x( 'One Sentence', 'Recommendation one sentence meta field label.', 'fictioneer' ),
'description' => __( 'Elevator pitch with 150 characters are less. For example: "Rebellious corporate heiress and her genius friend commit high-tech heists in a doomed city."', 'fictioneer' ),
'required' => 1
)
);
// Author
$output['fictioneer_recommendation_author'] = fictioneer_get_metabox_text(
$post,
'fictioneer_recommendation_author',
array(
'label' => _x( 'Author', 'Recommendation author meta field label.', 'fictioneer' ),
'description' => __( 'The name of the author. Separate multiple authors with commas.', 'fictioneer' ),
'required' => 1
)
);
// Primary URL
$output['fictioneer_recommendation_primary_url'] = fictioneer_get_metabox_url(
$post,
'fictioneer_recommendation_primary_url',
array(
'label' => _x( 'Primary Link', 'Recommendation primary link meta field label.', 'fictioneer' ),
'description' => __( "Primary link to the recommendation or author's website.", 'fictioneer' ),
'required' => 1
)
);
// URLs
$output['fictioneer_recommendation_urls'] = fictioneer_get_metabox_textarea(
$post,
'fictioneer_recommendation_urls',
array(
'label' => _x( 'Additional Links', 'Recommendation additional links meta field label.', 'fictioneer' ),
'description' => __( 'Links to the story, one link per line and mind the whitespaces. Format: "Link Name | https://www.address.abc" (without quotes).', 'fictioneer' )
)
);
// Support
$output['fictioneer_recommendation_support'] = fictioneer_get_metabox_textarea(
$post,
'fictioneer_recommendation_support',
array(
'label' => _x( 'Support', 'Recommendation support meta field label.', 'fictioneer' ),
'description' => __( 'Link to the author\'s Patreon, Ko-Fi, etc. One link per line and mind the whitespaces. Format: "Link Name | https://www.address.abc" (without quotes).', 'fictioneer' )
)
);
// --- Filters -----------------------------------------------------------------
$output = apply_filters( 'fictioneer_filter_metabox_recommendation_data', $output, $post );
// --- Render ------------------------------------------------------------------
echo implode( '', $output );
// Start HTML ---> ?>
$value ) {
fictioneer_update_post_meta( $post_id, $key, $value ?? 0 ); // Add, update, or delete (if falsy)
}
}
add_action( 'save_post', 'fictioneer_save_recommendation_metaboxes' );
// =============================================================================
// ADD META FIELDS TO BULK EDIT
// =============================================================================
/**
* Hide Patreon columns in list table views by default
*
* @since 5.17.0
*
* @param array $hidden Array of IDs of columns hidden by default.
* @param WP_Screen $screen WP_Screen object of the current screen.
*
* @return array Updated array of IDs.
*/
function fictioneer_default_hide_patreon_posts_columns( $hidden, $screen ) {
if (
in_array(
$screen->post_type,
['post', 'page', 'fcn_story', 'fcn_chapter', 'fcn_collection', 'fcn_recommendation']
)
) {
$hidden[] = 'fictioneer_patreon_lock_tiers';
$hidden[] = 'fictioneer_patreon_lock_amount';
}
return $hidden;
}
add_filter( 'default_hidden_columns', 'fictioneer_default_hide_patreon_posts_columns', 10, 2 );
/**
* Add Patreon columns to list table views
*
* @since 5.17.0
*
* @param array $post_columns An associative array of column headings.
*
* @return array Updated associative array of column headings.
*/
function fictioneer_add_patreon_posts_columns( $post_columns ) {
$post_columns[ 'fictioneer_patreon_lock_tiers' ] =
_x( 'Patreon Tiers', 'Patreon tiers list table column title.', 'fictioneer' );
$post_columns[ 'fictioneer_patreon_lock_amount' ] =
_x( 'Patreon Cents', 'Patreon amount cents list table column title.', 'fictioneer' );
return $post_columns;
}
/**
* Output Patreon values in list table views
*
* @since 5.17.0
*
* @param string $column_name The name of the column to display.
* @param int $post_id The current post ID.
*/
function fictioneer_manage_posts_custom_column( $column_name, $post_id ) {
// Setup
$post = get_post( $post_id );
$class = $post->post_password ? 'has-password' : 'no-password';
// Output
switch( $column_name ) {
case 'fictioneer_patreon_lock_tiers':
static $patreon_tiers = null;
if ( ! $patreon_tiers ) {
$patreon_tiers = get_option( 'fictioneer_connection_patreon_tiers' );
$patreon_tiers = is_array( $patreon_tiers ) ? $patreon_tiers : [];
}
$post_tiers = get_post_meta( $post_id, 'fictioneer_patreon_lock_tiers', true ) ?: [];
$post_tiers = is_array( $post_tiers ) ? $post_tiers : [];
foreach ( $post_tiers as $key => $tier ) {
$tier_data = $patreon_tiers[ $tier ] ?? 0;
if ( $tier_data ) {
$post_tiers[ $key ] = $tier_data['title'] === 'Free' ? fcntr( 'free_patreon_tier' ) : $tier_data['title'];
}
}
echo $post_tiers ? '' . implode( ', ', $post_tiers ) . '' : '—';
break;
case 'fictioneer_patreon_lock_amount':
$amount = get_post_meta( $post_id, 'fictioneer_patreon_lock_amount', true );
echo $amount ? '' . $amount . '' : '—';
break;
}
}
/**
* Add Patreon tiers to bulk edit
*
* @since 5.17.0
*
* @param string $column_name Name of the column to edit.
* @param string $post_type The post type slug.
*/
function fictioneer_add_patreon_bulk_edit_tiers( $column_name, $post_type ) {
// Check column and post type
if (
$column_name !== 'fictioneer_patreon_lock_tiers' ||
! in_array( $post_type, ['post', 'page', 'fcn_story', 'fcn_chapter', 'fcn_collection', 'fcn_recommendation'] )
) {
return;
}
// Setup
$patreon_tiers = get_option( 'fictioneer_connection_patreon_tiers' );
$patreon_tiers = is_array( $patreon_tiers ) ? $patreon_tiers : [];
// Tiers?
if ( ! $patreon_tiers ) {
return;
}
// Prepare options
$tier_options = [];
foreach ( $patreon_tiers as $tier ) {
$tier_options[ $tier['id'] ] = sprintf(
_x( '%s (%s)', 'Patreon tier meta field token (title and amount_cents).', 'fictioneer' ),
$tier['title'],
$tier['amount_cents']
);
}
// Start HTML ---> ?>
?>