3854 lines
122 KiB
PHP
Raw Normal View History

2023-01-21 01:31:34 +01:00
<?php
// =============================================================================
// CHECK FOR LOCAL DEV ENVIRONMENT
// =============================================================================
/**
* Checks whether the site runs on a local environment
*
* @since 5.25.0
*
* @return bool True or false.
*/
function fictioneer_is_local_environment() {
$local_hosts = ['localhost', '127.0.0.1', '::1'];
if ( in_array( $_SERVER['HTTP_HOST'], $local_hosts ) || in_array( $_SERVER['SERVER_ADDR'], $local_hosts ) ) {
return true;
}
return false;
}
2023-01-21 01:31:34 +01:00
// =============================================================================
// CHECK IF URL EXISTS
// =============================================================================
if ( ! function_exists( 'fictioneer_url_exists' ) ) {
/**
2023-08-28 10:43:04 +02:00
* Checks whether an URL exists
2023-01-21 01:31:34 +01:00
*
* @since 4.0.0
2023-08-28 10:43:04 +02:00
* @link https://www.geeksforgeeks.org/how-to-check-the-existence-of-url-in-php/
2023-01-21 01:31:34 +01:00
*
* @param string $url The URL to check.
2023-01-21 01:31:34 +01:00
*
2023-08-28 10:43:04 +02:00
* @return boolean True if the URL exists and false otherwise. Probably.
2023-01-21 01:31:34 +01:00
*/
function fictioneer_url_exists( $url ) {
2023-10-24 01:21:10 +02:00
if ( empty( $url ) ) {
2023-08-28 10:43:04 +02:00
return false;
}
2023-01-21 01:31:34 +01:00
2023-10-24 01:21:10 +02:00
$response = wp_remote_head( $url );
2023-01-21 01:31:34 +01:00
2023-10-24 01:21:10 +02:00
if ( is_wp_error( $response ) ) {
return false;
2023-01-21 01:31:34 +01:00
}
2023-10-24 01:21:10 +02:00
$statusCode = wp_remote_retrieve_response_code( $response );
// Check for 2xx status codes which indicate success
return ( $statusCode >= 200 && $statusCode < 300 );
2023-01-21 01:31:34 +01:00
}
}
// =============================================================================
// CHECK WHETHER VALID JSON
// =============================================================================
2024-07-10 20:41:15 +02:00
/**
* Check whether a JSON is valid
*
* @since 4.0.0
* @since 5.21.1 - Use json_validate() if on PHP 8.3 or higher.
*
* @param string $data JSON string hopeful.
*
* @return boolean True if the JSON is valid, false if not.
*/
2023-08-22 10:28:40 +02:00
2024-07-10 20:41:15 +02:00
function fictioneer_is_valid_json( $data = null ) {
if ( empty( $data ) ) {
return false;
}
2023-08-22 10:28:40 +02:00
2024-07-10 20:41:15 +02:00
// PHP 8.3 or higher
if ( function_exists( 'json_validate' ) ) {
return json_validate( $data );
2023-01-21 01:31:34 +01:00
}
2024-07-10 20:41:15 +02:00
$data = @json_decode( $data, true );
return ( json_last_error() === JSON_ERROR_NONE );
2023-01-21 01:31:34 +01:00
}
// =============================================================================
// CHECK FOR ACTIVE PLUGINS
// =============================================================================
2024-06-19 22:55:12 +02:00
/**
* Checks whether a plugin is active for the entire network
*
* @since 5.20.2
* @link https://developer.wordpress.org/reference/functions/is_plugin_active_for_network/
*
* @param string $path Relative path to the plugin.
*
* @return boolean True if the plugin is active, otherwise false.
*/
function fictioneer_is_network_plugin_active( $path ) {
if ( ! is_multisite() ) {
return false;
}
$plugins = get_site_option( 'active_sitewide_plugins' );
2023-01-21 01:31:34 +01:00
2024-06-19 22:55:12 +02:00
if ( isset( $plugins[ $path ] ) ) {
return true;
2023-01-21 01:31:34 +01:00
}
2024-06-19 22:55:12 +02:00
return false;
}
/**
* Checks whether a plugin is active
*
* @since 4.0.0
* @since 5.20.2 - Changed to copy of is_plugin_active().
* @link https://developer.wordpress.org/reference/functions/is_plugin_active/
*
* @param string $path Relative path to the plugin.
*
* @return boolean True if the plugin is active, otherwise false.
*/
function fictioneer_is_plugin_active( $path ) {
return in_array( $path, (array) get_option( 'active_plugins', [] ), true ) || fictioneer_is_network_plugin_active( $path );
2023-01-21 01:31:34 +01:00
}
if ( ! function_exists( 'fictioneer_seo_plugin_active' ) ) {
/**
* Checks whether any SEO plugin known to the theme is active
*
* The theme's SEO features are inherently incompatible with SEO plugins, which
* may be more sophisticated but do not understand the theme's content structure.
* At all. Regardless, if the user want to use a SEO plugin, it's better to turn
* off the theme's own SEO features. This function detects some of them.
*
* @since 4.0.0
2023-01-21 01:31:34 +01:00
*
* @return boolean True if a known SEO is active, otherwise false.
*/
function fictioneer_seo_plugin_active() {
return fictioneer_is_plugin_active( 'wordpress-seo/wp-seo.php' ) ||
2023-08-22 10:28:40 +02:00
fictioneer_is_plugin_active( 'wordpress-seo-premium/wp-seo-premium.php' ) ||
function_exists( 'aioseo' );
2023-01-21 01:31:34 +01:00
}
}
// =============================================================================
// GET USER BY ID OR EMAIL
// =============================================================================
if ( ! function_exists( 'fictioneer_get_user_by_id_or_email' ) ) {
/**
* Get user by ID or email
*
* @since 4.6.0
2023-01-21 01:31:34 +01:00
*
* @param int|string $id_or_email User ID or email address.
2023-01-21 01:31:34 +01:00
*
* @return WP_User|boolean Returns the user or false if not found.
*/
function fictioneer_get_user_by_id_or_email( $id_or_email ) {
$user = false;
if ( is_numeric( $id_or_email ) ) {
$id = (int) $id_or_email;
$user = get_user_by( 'id' , $id );
} elseif ( is_object( $id_or_email ) ) {
if ( ! empty( $id_or_email->user_id ) ) {
$id = (int) $id_or_email->user_id;
$user = get_user_by( 'id' , $id );
}
} else {
$user = get_user_by( 'email', $id_or_email );
}
return $user;
}
}
2023-08-17 18:27:34 +02:00
// =============================================================================
// GET LAST CHAPTER/STORY UPDATE
// =============================================================================
if ( ! function_exists( 'fictioneer_get_last_fiction_update' ) ) {
2023-08-17 18:27:34 +02:00
/**
* Get Unix timestamp for last story or chapter update
*
* @since 5.0.0
2023-08-17 18:27:34 +02:00
*
* @return int The timestamp in milliseconds.
*/
function fictioneer_get_last_fiction_update() {
$last_update = get_option( 'fictioneer_story_or_chapter_updated_timestamp' );
if ( empty( $last_update ) ) {
$last_update = time() * 1000;
update_option( 'fictioneer_story_or_chapter_updated_timestamp', $last_update );
}
return $last_update;
}
}
// =============================================================================
// GET STORY CHAPTERS
// =============================================================================
/**
* Returns array of chapter posts for a story
*
* @since 5.9.2
* @since 5.22.3 - Refactored.
*
* @param int $story_id ID of the story.
* @param array $args Optional. Additional query arguments.
2024-08-16 15:56:17 +02:00
* @param bool $full Optional. Whether to not reduce the posts. Default false.
*
* @return array Array of chapter posts or empty.
*/
2024-08-16 15:56:17 +02:00
function fictioneer_get_story_chapter_posts( $story_id, $args = [], $full = false ) {
// Static variable cache
static $cached_results = [];
// Setup
$chapter_ids = fictioneer_get_story_chapter_ids( $story_id );
// No chapters?
if ( empty( $chapter_ids ) ) {
return [];
}
// Query arguments
$query_args = array(
'fictioneer_query_name' => 'get_story_chapter_posts',
'post_type' => 'fcn_chapter',
'post_status' => 'publish',
'ignore_sticky_posts' => true,
'posts_per_page' => -1,
'no_found_rows' => true, // Improve performance
'update_post_term_cache' => false // Improve performance
);
// Apply filters and custom arguments
$query_args = array_merge( $query_args, $args );
2024-08-15 12:30:38 +02:00
$query_args = apply_filters( 'fictioneer_filter_story_chapter_posts_query', $query_args, $story_id, $chapter_ids );
// Static cache key
$cache_key = $story_id . '_' . md5( serialize( $query_args ) );
// Static cache hit?
if ( isset( $cached_results[ $cache_key ] ) ) {
return $cached_results[ $cache_key ];
}
// Batched or one go?
if ( count( $chapter_ids ) <= FICTIONEER_QUERY_ID_ARRAY_LIMIT ) {
$query_args['post__in'] = $chapter_ids ?: [0];
$chapter_query = new WP_Query( $query_args );
$chapter_posts = $chapter_query->posts;
} else {
$chapter_posts = [];
$batches = array_chunk( $chapter_ids, FICTIONEER_QUERY_ID_ARRAY_LIMIT );
foreach ( $batches as $batch ) {
$query_args['post__in'] = $batch ?: [0];
$chapter_query = new WP_Query( $query_args );
$chapter_posts = array_merge( $chapter_posts, $chapter_query->posts );
}
}
// Restore order
$chapter_positions = array_flip( $chapter_ids );
usort( $chapter_posts, function( $a, $b ) use ( $chapter_positions ) {
return $chapter_positions[ $a->ID ] - $chapter_positions[ $b->ID ];
});
// Return chapters selected in story
return $chapter_posts;
}
// =============================================================================
// GROUP CHAPTERS
// =============================================================================
/**
* Groups and prepares chapters for a specific story
*
* Note: If chapter groups are disabled, all chapters will be
2024-10-12 11:17:02 +02:00
* within the 'all_chapters' group.
*
* @since 5.25.0
*
* @param int $story_id ID of the story.
* @param array $chapters Array of (reduced) WP_Post objects.
*
* @return array The grouped and prepared chapters.
*/
function fictioneer_prepare_chapter_groups( $story_id, $chapters ) {
// Any chapters?
if ( empty( $chapters ) ) {
return [];
}
// Setup
$chapter_groups = [];
$allowed_permalinks = apply_filters( 'fictioneer_filter_allowed_chapter_permalinks', ['publish'] );
$enable_groups = get_option( 'fictioneer_enable_chapter_groups' ) &&
! get_post_meta( $story_id, 'fictioneer_story_disable_groups', true );
// Loop chapters...
foreach ( $chapters as $post ) {
$chapter_id = $post->ID;
// Skip missing or not visible chapters
if ( ! $post || get_post_meta( $chapter_id, 'fictioneer_chapter_hidden', true ) ) {
continue;
}
// Data
$group = get_post_meta( $chapter_id, 'fictioneer_chapter_group', true );
$group = empty( $group ) ? fcntr( 'unassigned_group' ) : $group;
$group = $enable_groups ? $group : 'all_chapters';
$group_key = sanitize_title( $group );
if ( ! array_key_exists( $group_key, $chapter_groups ) ) {
$chapter_groups[ $group_key ] = array(
'group' => $group,
'toggle_icon' => 'fa-solid fa-chevron-down',
'data' => [],
'count' => 0,
'classes' => array(
'_group-' . sanitize_title( $group ),
"_story-{$story_id}"
)
);
}
$chapter_groups[ $group_key ]['data'][] = array(
'id' => $chapter_id,
'story_id' => $story_id,
'status' => $post->post_status,
'link' => in_array( $post->post_status, $allowed_permalinks ) ? get_permalink( $post->ID ) : '',
'timestamp' => get_the_time( 'U', $post ),
'password' => ! empty( $post->post_password ),
'list_date' => get_the_date( '', $post ),
'grid_date' => get_the_time( get_option( 'fictioneer_subitem_date_format', "M j, 'y" ) ?: "M j, 'y", $post ),
'icon' => fictioneer_get_icon_field( 'fictioneer_chapter_icon', $chapter_id ),
'text_icon' => get_post_meta( $chapter_id, 'fictioneer_chapter_text_icon', true ),
'prefix' => get_post_meta( $chapter_id, 'fictioneer_chapter_prefix', true ),
'title' => fictioneer_get_safe_title( $chapter_id, 'story-chapter-list' ),
'list_title' => get_post_meta( $chapter_id, 'fictioneer_chapter_list_title', true ),
'words' => fictioneer_get_word_count( $chapter_id ),
'warning' => get_post_meta( $chapter_id, 'fictioneer_chapter_warning', true )
);
$chapter_groups[ $group_key ]['count'] += 1;
}
return $chapter_groups;
}
2023-01-21 01:31:34 +01:00
// =============================================================================
// GET STORY DATA
// =============================================================================
if ( ! function_exists( 'fictioneer_get_story_data' ) ) {
/**
* Get collection of a story's data
*
* @since 4.3.0
* @since 5.25.0 - Refactored with custom SQL query.
2023-01-21 01:31:34 +01:00
*
* @param int $story_id ID of the story.
* @param boolean $show_comments Optional. Whether the comment count is needed.
* Default true.
2024-01-27 00:30:20 +01:00
* @param array $args Optional array of arguments.
2023-01-21 01:31:34 +01:00
*
* @return array|boolean Data of the story or false if invalid.
2023-01-21 01:31:34 +01:00
*/
function fictioneer_get_story_data( $story_id, $show_comments = true, $args = [] ) {
global $wpdb;
2023-01-21 01:31:34 +01:00
$story_id = fictioneer_validate_id( $story_id, 'fcn_story' );
$meta_cache = null;
2023-01-21 01:31:34 +01:00
if ( empty( $story_id ) ) {
return false;
}
2023-01-21 01:31:34 +01:00
// Meta cache (purged on update)?
if ( FICTIONEER_ENABLE_STORY_DATA_META_CACHE ) {
$meta_cache = get_post_meta( $story_id, 'fictioneer_story_data_collection', true );
}
2023-01-21 01:31:34 +01:00
2024-04-11 01:41:45 +02:00
if ( $meta_cache && ( $meta_cache['last_modified'] ?? 0 ) >= get_the_modified_time( 'U', $story_id ) ) {
// Return cached data without refreshing the comment count
if ( ! $show_comments ) {
return $meta_cache;
}
// Time to refresh comment count?
$comment_count_delay = ( $meta_cache['comment_count_timestamp'] ?? 0 ) + FICTIONEER_STORY_COMMENT_COUNT_TIMEOUT;
2024-01-27 00:30:20 +01:00
$refresh_comments = $comment_count_delay < time() ||
( $args['refresh_comment_count'] ?? 0 ) || fictioneer_caching_active( 'story_data_refresh_comment_count' );
// Refresh comment count
if ( $refresh_comments ) {
// Use old count as fallback
$comment_count = $meta_cache['comment_count'];
if ( count( $meta_cache['chapter_ids'] ) > 0 ) {
$comment_count = fictioneer_get_story_comment_count( $story_id, $meta_cache['chapter_ids'] );
}
$meta_cache['comment_count'] = $comment_count;
$meta_cache['comment_count_timestamp'] = time();
// Update post database comment count
$story_comment_count = get_approved_comments( $story_id, array( 'count' => true ) ) ?: 0;
fictioneer_sql_update_comment_count( $story_id, $comment_count + $story_comment_count );
2024-01-26 23:30:58 +01:00
// Update meta cache and purge
update_post_meta( $story_id, 'fictioneer_story_data_collection', $meta_cache );
if ( function_exists( 'fictioneer_purge_post_cache' ) ) {
fictioneer_purge_post_cache( $story_id );
}
}
2023-01-21 01:31:34 +01:00
// Return cached data
return $meta_cache;
2023-01-21 01:31:34 +01:00
}
// Setup
$tags = get_the_tags( $story_id );
$fandoms = get_the_terms( $story_id, 'fcn_fandom' );
$characters = get_the_terms( $story_id, 'fcn_character' );
$warnings = get_the_terms( $story_id, 'fcn_content_warning' );
$genres = get_the_terms( $story_id, 'fcn_genre' );
$status = get_post_meta( $story_id, 'fictioneer_story_status', true );
2023-01-21 01:31:34 +01:00
$icon = 'fa-solid fa-circle';
$chapter_count = 0;
$word_count = 0;
$comment_count = 0;
$visible_chapter_ids = [];
$indexed_chapter_ids = [];
2023-01-21 01:31:34 +01:00
// Assign correct icon
if ( $status != 'Ongoing' ) {
switch ( $status ) {
case 'Completed':
$icon = 'fa-solid fa-circle-check';
break;
case 'Oneshot':
$icon = 'fa-solid fa-circle-check';
break;
case 'Hiatus':
$icon = 'fa-solid fa-circle-pause';
break;
case 'Canceled':
$icon = 'fa-solid fa-ban';
break;
}
}
// Custom SQL query to count chapters, words, comments, etc.
// This significantly faster than WP_Query (up to 15 times with 500 chapters)
$queried_statuses = apply_filters( 'fictioneer_filter_get_story_data_queried_chapter_statuses', ['publish'], $story_id );
$indexed_statuses = apply_filters( 'fictioneer_filter_get_story_data_indexed_chapter_statuses', ['publish'], $story_id );
$chapter_ids = fictioneer_get_story_chapter_ids( $story_id );
$chapters = [];
if ( ! empty( $chapter_ids ) ) {
$chapter_ids_placeholder = implode( ',', array_fill( 0, count( $chapter_ids ), '%d' ) );
$status_list = array_map( function( $status ) use ( $wpdb ) {
return $wpdb->prepare( '%s', $status );
}, $queried_statuses );
$status_list = implode( ',', $status_list );
$query = $wpdb->prepare(
"SELECT
c.ID as chapter_id,
c.comment_count,
SUM(CASE WHEN pm.meta_key = '_word_count' THEN CAST(pm.meta_value AS UNSIGNED) ELSE 0 END) AS word_count,
MAX(CASE WHEN pm.meta_key = 'fictioneer_chapter_hidden' THEN pm.meta_value ELSE '' END) AS is_hidden,
MAX(CASE WHEN pm.meta_key = 'fictioneer_chapter_no_chapter' THEN pm.meta_value ELSE '' END) AS is_no_chapter,
c.post_status
FROM {$wpdb->posts} c
LEFT JOIN {$wpdb->postmeta} pm ON pm.post_id = c.ID
WHERE c.ID IN ($chapter_ids_placeholder)
AND c.post_status IN ($status_list)
GROUP BY c.ID",
...$chapter_ids // WHERE clause
);
$query = apply_filters( 'fictioneer_filter_get_story_data_sql', $query, $story_id, $chapter_ids, $queried_statuses );
$chapters = $wpdb->get_results( $query );
usort( $chapters, function( $a, $b ) use ( $chapter_ids ) {
$position_a = array_search( $a->chapter_id, $chapter_ids );
$position_b = array_search( $b->chapter_id, $chapter_ids );
return $position_a - $position_b;
});
}
foreach ( $chapters as $chapter ) {
if ( empty( $chapter->is_hidden ) ) {
// Do not count non-chapters...
if ( empty( $chapter->is_no_chapter ) ) {
$chapter_count++;
$word_count += intval( $chapter->word_count );
2023-01-21 01:31:34 +01:00
}
// ... but they are still listed!
$visible_chapter_ids[] = $chapter->chapter_id;
// Indexed chapters (accounts for custom filters)
if ( in_array( $chapter->post_status, $indexed_statuses ) ) {
$indexed_chapter_ids[] = $chapter->chapter_id;
}
2023-01-21 01:31:34 +01:00
}
// Count ALL comments
$comment_count += intval( $chapter->comment_count );
2023-01-21 01:31:34 +01:00
}
// Add story word count
$word_count += get_post_meta( $story_id, '_word_count', true );
// Customize word count
$modified_word_count = fictioneer_multiply_word_count( $word_count );
2024-08-26 20:27:15 +02:00
// Rating
$rating = get_post_meta( $story_id, 'fictioneer_story_rating', true ) ?: 'Everyone';
// Prepare result
2023-01-21 01:31:34 +01:00
$result = array(
'id' => $story_id,
'chapter_count' => $chapter_count,
'word_count' => $modified_word_count,
'word_count_short' => fictioneer_shorten_number( $modified_word_count ),
2023-01-21 01:31:34 +01:00
'status' => $status,
'icon' => $icon,
'has_taxonomies' => $fandoms || $characters || $genres,
'tags' => $tags,
'characters' => $characters,
'fandoms' => $fandoms,
'warnings' => $warnings,
'genres' => $genres,
'title' => fictioneer_get_safe_title( $story_id, 'utility-get-story-data' ),
2024-08-26 20:27:15 +02:00
'rating' => $rating,
'rating_letter' => $rating[0],
'chapter_ids' => $visible_chapter_ids,
'indexed_chapter_ids' => $indexed_chapter_ids,
2023-01-21 01:31:34 +01:00
'last_modified' => get_the_modified_time( 'U', $story_id ),
'comment_count' => $comment_count,
2024-04-14 22:43:25 +02:00
'comment_count_timestamp' => time(),
'redirect' => get_post_meta( $story_id, 'fictioneer_story_redirect_link', true )
2023-01-21 01:31:34 +01:00
);
// Update meta cache if enabled
if ( FICTIONEER_ENABLE_STORY_DATA_META_CACHE ) {
update_post_meta( $story_id, 'fictioneer_story_data_collection', $result );
}
2023-01-21 01:31:34 +01:00
2024-04-11 01:41:45 +02:00
// Update story total word count
update_post_meta( $story_id, 'fictioneer_story_total_word_count', $word_count );
// Update post database comment count
$story_comment_count = get_approved_comments( $story_id, array( 'count' => true ) ) ?: 0;
fictioneer_sql_update_comment_count( $story_id, $comment_count + $story_comment_count );
// Done
2023-01-21 01:31:34 +01:00
return $result;
}
}
/**
* Returns the comment count of all story chapters
*
* Note: Includes hidden and non-chapter posts.
*
* @since 5.22.2
* @since 5.22.3 - Switched to SQL query.
*
* @param int $story_id ID of the story.
* @param array|null $chapter_ids Optional. Array of chapter IDs.
*
* @return int Number of comments.
*/
function fictioneer_get_story_comment_count( $story_id, $chapter_ids = null ) {
// Setup
$comment_count = 0;
$chapter_ids = $chapter_ids ?? fictioneer_get_story_chapter_ids( $story_id );
// No chapters?
if ( empty( $chapter_ids ) ) {
return 0;
}
// SQL
global $wpdb;
$chunks = array_chunk( $chapter_ids, FICTIONEER_QUERY_ID_ARRAY_LIMIT );
2024-09-19 02:24:49 +02:00
foreach ( $chunks as $chunk ) {
$placeholders = implode( ',', array_fill( 0, count( $chunk ), '%d' ) );
$query = $wpdb->prepare("
SELECT COUNT(comment_ID)
FROM {$wpdb->comments} c
INNER JOIN {$wpdb->posts} p ON c.comment_post_ID = p.ID
WHERE p.post_type = 'fcn_chapter'
AND p.ID IN ($placeholders)
AND c.comment_approved = '1'
", ...$chunk );
$comment_count += $wpdb->get_var( $query );
}
// Return result
return $comment_count;
}
2023-01-21 01:31:34 +01:00
// =============================================================================
// GET AUTHOR STATISTICS
// =============================================================================
if ( ! function_exists( 'fictioneer_get_author_statistics' ) ) {
/**
* Returns an author's statistics
2023-01-21 01:31:34 +01:00
*
* Note: Cached as meta field for an hour.
*
* @since 4.6.0
2023-01-21 01:31:34 +01:00
*
* @param int $author_id User ID of the author.
2023-01-21 01:31:34 +01:00
*
* @return array|boolean Array of statistics or false if user does not exist.
2023-01-21 01:31:34 +01:00
*/
function fictioneer_get_author_statistics( $author_id ) {
// Setup
$author_id = fictioneer_validate_id( $author_id );
2023-08-22 10:28:40 +02:00
if ( ! $author_id ) {
return false;
}
2023-01-21 01:31:34 +01:00
$author = get_user_by( 'id', $author_id );
2023-08-22 10:28:40 +02:00
if ( ! $author ) {
return false;
}
2023-01-21 01:31:34 +01:00
$cache_plugin_active = fictioneer_caching_active( 'author_statistics' );
// Meta cache?
if ( ! $cache_plugin_active ) {
$meta_cache = $author->fictioneer_author_statistics;
2023-01-21 01:31:34 +01:00
if ( $meta_cache && ( $meta_cache['valid_until'] ?? 0 ) > time() ) {
return $meta_cache;
}
2023-01-21 01:31:34 +01:00
}
// Get stories
2023-01-21 01:31:34 +01:00
$stories = get_posts(
array(
2023-11-12 12:42:25 +01:00
'post_type' => 'fcn_story',
'post_status' => 'publish',
2023-01-21 01:31:34 +01:00
'author' => $author_id,
'numberposts' => -1,
2023-11-12 15:11:56 +01:00
'update_post_meta_cache' => true,
2023-11-15 10:32:05 +01:00
'update_post_term_cache' => false,
'no_found_rows' => true
2023-01-21 01:31:34 +01:00
)
);
// Filter out unwanted stories (faster than meta query)
$stories = array_filter( $stories, function ( $post ) {
// Story hidden?
$story_hidden = get_post_meta( $post->ID, 'fictioneer_story_hidden', true );
2023-11-12 15:09:43 +01:00
return empty( $story_hidden ) || $story_hidden === '0';
});
// Get chapters
2023-01-21 01:31:34 +01:00
$chapters = get_posts(
array(
2023-11-12 12:42:25 +01:00
'post_type' => 'fcn_chapter',
'post_status' => 'publish',
2023-01-21 01:31:34 +01:00
'author' => $author_id,
'numberposts' => -1,
2023-11-12 15:11:56 +01:00
'update_post_meta_cache' => true,
2023-11-15 10:32:05 +01:00
'update_post_term_cache' => false,
'no_found_rows' => true
2023-01-21 01:31:34 +01:00
)
);
// Filter out unwanted chapters (faster than meta query)
$chapters = array_filter( $chapters, function ( $post ) {
// Chapter hidden?
$chapter_hidden = get_post_meta( $post->ID, 'fictioneer_chapter_hidden', true );
$not_hidden = empty( $chapter_hidden ) || $chapter_hidden === '0';
// Not a chapter?
$no_chapter = get_post_meta( $post->ID, 'fictioneer_chapter_no_chapter', true );
$is_chapter = empty( $no_chapter ) || $no_chapter === '0';
// Only keep if both conditions are met
return $not_hidden && $is_chapter;
});
// Count words and comments
2023-01-21 01:31:34 +01:00
$word_count = 0;
$comment_count = 0;
foreach ( $stories as $story ) {
$word_count += fictioneer_get_word_count( $story->ID );
}
2023-01-21 01:31:34 +01:00
foreach ( $chapters as $chapter ) {
2023-11-30 17:10:19 +01:00
$word_count += fictioneer_get_word_count( $chapter->ID );
$comment_count += $chapter->comment_count;
2023-01-21 01:31:34 +01:00
}
// Prepare results
2023-01-21 01:31:34 +01:00
$result = array(
'story_count' => count( $stories ),
'chapter_count' => count( $chapters ),
'word_count' => $word_count,
'word_count_short' => fictioneer_shorten_number( $word_count ),
'valid_until' => time() + HOUR_IN_SECONDS,
2023-01-21 01:31:34 +01:00
'comment_count' => $comment_count
);
// Update meta cache
if ( ! $cache_plugin_active ) {
fictioneer_update_user_meta( $author_id, 'fictioneer_author_statistics', $result );
}
2023-01-21 01:31:34 +01:00
// Done
2023-01-21 01:31:34 +01:00
return $result;
}
}
// =============================================================================
// GET COLLECTION STATISTICS
// =============================================================================
if ( ! function_exists( 'fictioneer_get_collection_statistics' ) ) {
/**
* Returns a collection's statistics
*
* @since 5.9.2
* @since 5.26.0 - Refactored with custom SQL.
*
* @global wpdb $wpdb WordPress database object.
*
* @param int $collection_id ID of the collection.
*
* @return array Array of statistics.
*/
function fictioneer_get_collection_statistics( $collection_id ) {
global $wpdb;
// Meta cache?
$cache_plugin_active = fictioneer_caching_active( 'collection_statistics' );
2024-01-26 23:01:20 +01:00
if ( ! $cache_plugin_active ) {
$meta_cache = get_post_meta( $collection_id, 'fictioneer_collection_statistics', true );
if ( $meta_cache && ( $meta_cache['valid_until'] ?? 0 ) > time() ) {
return $meta_cache;
}
2024-01-26 23:01:20 +01:00
}
// Setup
$featured = get_post_meta( $collection_id, 'fictioneer_collection_items', true );
$story_count = 0;
$word_count = 0;
$chapter_count = 0;
$comment_count = 0;
$found_chapter_ids = []; // Chapters that were already counted
$query_chapter_ids = []; // Chapters that need to be queried
// Empty collection?
if ( empty( $featured ) ) {
return array(
'story_count' => 0,
'word_count' => 0,
'chapter_count' => 0,
'comment_count' => 0
);
}
// SQL query to analyze collection posts
$placeholders = implode( ',', array_fill( 0, count( $featured ), '%d' ) );
$sql =
"SELECT p.ID, p.post_type
FROM {$wpdb->posts} p
WHERE p.ID IN ($placeholders)";
$posts = $wpdb->get_results( $wpdb->prepare( $sql, ...$featured ) );
foreach ( $posts as $post ) {
// Only look at stories and chapters...
if ( $post->post_type === 'fcn_chapter' ) {
// ... single chapters need to be looked up separately
$query_chapter_ids[] = $post->ID;
} elseif ( $post->post_type === 'fcn_story' ) {
// ... stories have pre-processed data
$story = fictioneer_get_story_data( $post->ID, false );
$found_chapter_ids = array_merge( $found_chapter_ids, $story['chapter_ids'] );
$word_count += $story['word_count'];
$chapter_count += $story['chapter_count'];
$comment_count += $story['comment_count'];
$story_count += 1;
}
}
// Remove duplicate chapters
$found_chapter_ids = array_unique( $found_chapter_ids );
$query_chapter_ids = array_unique( $query_chapter_ids );
// Do not query already counted chapters
$query_chapter_ids = array_diff( $query_chapter_ids, $found_chapter_ids );
// SQL query for lone chapters not belong to featured stories...
if ( ! empty( $query_chapter_ids ) ) {
$placeholders = implode( ',', array_fill( 0, count( $query_chapter_ids ), '%d' ) );
$sql =
"SELECT p.ID, p.comment_count, COALESCE(pm_word_count.meta_value, 0) AS word_count
FROM {$wpdb->posts} p
LEFT JOIN {$wpdb->postmeta} pm_hidden
ON (p.ID = pm_hidden.post_id AND pm_hidden.meta_key = 'fictioneer_chapter_hidden')
LEFT JOIN {$wpdb->postmeta} pm_no_chapter
ON (p.ID = pm_no_chapter.post_id AND pm_no_chapter.meta_key = 'fictioneer_chapter_no_chapter')
LEFT JOIN {$wpdb->postmeta} pm_word_count
ON (p.ID = pm_word_count.post_id AND pm_word_count.meta_key = '_word_count')
WHERE p.ID IN ($placeholders)
AND p.post_type = 'fcn_chapter'
AND p.post_status = 'publish'
AND (pm_hidden.meta_value IS NULL OR pm_hidden.meta_value = '' OR pm_hidden.meta_value = '0')
AND (pm_no_chapter.meta_value IS NULL OR pm_no_chapter.meta_value = '' OR pm_no_chapter.meta_value = '0')";
$chapters = $wpdb->get_results( $wpdb->prepare( $sql, ...$query_chapter_ids ) );
foreach ( $chapters as $chapter ) {
$comment_count += $chapter->comment_count;
$chapter_count += 1;
$words = (int) $chapter->word_count;
$words = max( 0, $words );
$words = apply_filters( 'fictioneer_filter_word_count', $words, $chapter->ID );
$words = fictioneer_multiply_word_count( $words );
$word_count += $words;
}
}
2024-01-26 23:01:20 +01:00
// Statistics
$statistics = array(
'story_count' => $story_count,
'word_count' => $word_count,
'chapter_count' => $chapter_count,
2024-01-26 23:01:20 +01:00
'comment_count' => $comment_count,
'valid_until' => time() + 900 // 15 minutes
);
2024-01-26 23:01:20 +01:00
// Update meta cache
if ( ! $cache_plugin_active ) {
update_post_meta( $collection_id, 'fictioneer_collection_statistics', $statistics );
}
2024-01-26 23:01:20 +01:00
// Done
2024-01-26 23:01:20 +01:00
return $statistics;
}
}
2023-01-21 01:31:34 +01:00
// =============================================================================
// SHORTEN NUMBER WITH LETTER
// =============================================================================
if ( ! function_exists( 'fictioneer_shorten_number' ) ) {
/**
* Shortens a number to a fractional with a letter
*
* @since 4.5.0
2023-01-21 01:31:34 +01:00
*
* @param int $number The number to be shortened.
* @param int $precision Precision of the fraction. Default 1.
2023-01-21 01:31:34 +01:00
*
* @return string The minified number string.
*/
function fictioneer_shorten_number( $number, $precision = 1 ) {
$number = intval( $number );
2023-01-21 01:31:34 +01:00
// The letters are prefixed by a HAIR SPACE (&hairsp;)
if ( $number < 1000 ) {
2023-01-26 01:50:06 +01:00
return strval( $number );
2023-01-21 01:31:34 +01:00
} else if ( $number < 1000000 ) {
return number_format( $number / 1000, $precision ) . 'K';
} else if ( $number < 1000000000 ) {
return number_format( $number / 1000000, $precision ) . 'M';
} else {
return number_format( $number / 1000000000, $precision ) . 'B';
}
}
}
// =============================================================================
// VALIDATE ID
// =============================================================================
if ( ! function_exists( 'fictioneer_validate_id' ) ) {
/**
* Ensures an ID is a valid integer and positive; optionally checks whether the
2023-08-28 10:43:04 +02:00
* associated post is of a certain types (or among an array of types)
2023-01-21 01:31:34 +01:00
*
* @since 4.7.0
2023-01-21 01:31:34 +01:00
*
* @param int $id The ID to validate.
* @param string|array $for_type Optional. The expected post type(s).
2023-01-21 01:31:34 +01:00
*
2023-08-05 01:59:55 +02:00
* @return int|boolean The validated ID or false if invalid.
2023-01-21 01:31:34 +01:00
*/
2023-01-26 01:52:15 +01:00
function fictioneer_validate_id( $id, $for_type = [] ) {
2023-01-21 01:31:34 +01:00
$safe_id = intval( $id );
$types = is_array( $for_type ) ? $for_type : [ $for_type ];
2023-01-21 01:31:34 +01:00
if ( empty( $safe_id ) || $safe_id < 0 ) {
return false;
}
2023-08-05 01:59:55 +02:00
if ( ! empty( $for_type ) && ! in_array( get_post_type( $safe_id ), $types ) ) {
return false;
}
2023-01-21 01:31:34 +01:00
return $safe_id;
}
}
// =============================================================================
// VALIDATE NONCE PLAUSIBILITY
// =============================================================================
if ( ! function_exists( 'fictioneer_nonce_plausibility' ) ) {
/**
* Checks nonce to be plausible
*
* This helps to evaluate whether a nonce has been malformed, for example
* through a dynamic update from a cache plugin not working properly.
*
* @since 5.9.4
*
* @param string $nonce The nonce to check.
*
* @return boolean Whether the nonce is plausible.
*/
function fictioneer_nonce_plausibility( $nonce ) {
if ( preg_match( '/^[a-f0-9]{10}$/i', $nonce ) === 1 ) {
return true;
}
return false;
}
}
2023-01-21 01:31:34 +01:00
// =============================================================================
// GET VALIDATED AJAX USER
// =============================================================================
if ( ! function_exists( 'fictioneer_get_validated_ajax_user' ) ) {
/**
* Get the current user after performing AJAX validations
*
* @since 5.0.0
2023-01-21 01:31:34 +01:00
*
* @param string $nonce_name Optional. The name of the nonce. Default 'nonce'.
* @param string $nonce_value Optional. The value of the nonce. Default 'fictioneer_nonce'.
2023-01-21 01:31:34 +01:00
*
* @return boolean|WP_User False if not valid, the current user object otherwise.
*/
function fictioneer_get_validated_ajax_user( $nonce_name = 'nonce', $nonce_value = 'fictioneer_nonce' ) {
// Setup
$user = wp_get_current_user();
// Validate
if (
! $user->exists() ||
2023-01-21 01:31:34 +01:00
! check_ajax_referer( $nonce_value, $nonce_name, false )
) {
return false;
}
return $user;
}
}
// =============================================================================
2023-09-01 21:36:18 +02:00
// RATE LIMIT
// =============================================================================
/**
* Checks rate limit globally or for an action via the session
*
* @since 5.7.1
2023-09-01 21:36:18 +02:00
*
2023-09-01 21:41:52 +02:00
* @param string $action The action to check for rate-limiting.
* Defaults to 'fictioneer_global'.
* @param int|null $max Optional. Maximum number of requests.
* Defaults to FICTIONEER_REQUESTS_PER_MINUTE.
2023-09-01 21:36:18 +02:00
*/
2023-09-01 21:41:52 +02:00
function fictioneer_check_rate_limit( $action = 'fictioneer_global', $max = null ) {
2023-09-01 21:36:18 +02:00
if ( ! get_option( 'fictioneer_enable_rate_limits' ) ) {
return;
}
// Start session if not already done
if ( session_status() == PHP_SESSION_NONE ) {
session_start();
}
// Initialize if not set
if ( ! isset( $_SESSION[ $action ]['request_times'] ) ) {
$_SESSION[ $action ]['request_times'] = [];
}
// Setup
$current_time = microtime( true );
$time_window = 60;
2023-09-01 21:41:52 +02:00
$max = $max ? absint( $max ) : FICTIONEER_REQUESTS_PER_MINUTE;
$max = max( 1, $max );
2023-09-01 21:36:18 +02:00
// Filter out old timestamps
$_SESSION[ $action ]['request_times'] = array_filter(
$_SESSION[ $action ]['request_times'],
function ( $time ) use ( $current_time, $time_window ) {
return ( $current_time - $time ) < $time_window;
}
);
// Limit exceeded?
2023-09-01 21:41:52 +02:00
if ( count( $_SESSION[ $action ]['request_times'] ) >= $max ) {
2023-09-01 21:36:18 +02:00
http_response_code( 429 ); // Too many requests
exit;
}
// Record the current request time
$_SESSION[ $action ]['request_times'][] = $current_time;
}
// =============================================================================
2023-01-21 01:31:34 +01:00
// KEY/VALUE STRING REPLACEMENT
// =============================================================================
if ( ! function_exists( 'fictioneer_replace_key_value' ) ) {
/**
* Replaces key/value pairs in a string
*
* @since 5.0.0
2023-01-21 01:31:34 +01:00
*
* @param string $text Text that has key/value pairs to be replaced.
* @param array $args The key/value pairs.
* @param string $default Optional. To be used if the text is empty.
* Default is an empty string.
2023-01-21 01:31:34 +01:00
*
* @return string The modified text.
*/
function fictioneer_replace_key_value( $text, $args, $default = '' ) {
// Check if text exists
2023-08-22 10:28:40 +02:00
if ( empty( $text ) ) {
$text = $default;
}
2023-01-21 01:31:34 +01:00
// Check args
$args = is_array( $args ) ? $args : [];
2023-01-21 01:31:34 +01:00
// Filter args
$args = array_filter( $args, 'is_scalar' );
2023-01-21 01:31:34 +01:00
// Return modified text
return trim( strtr( $text, $args ) );
2023-01-21 01:31:34 +01:00
}
}
// =============================================================================
// CHECK USER CAPABILITIES
// =============================================================================
if ( ! function_exists( 'fictioneer_is_admin' ) ) {
/**
* Checks if an user is an administrator
*
* @since 5.0.0
2023-01-21 01:31:34 +01:00
*
2023-10-06 02:44:44 +02:00
* @param int $user_id The user ID to check.
2023-01-21 01:31:34 +01:00
*
* @return boolean To be or not to be.
*/
2023-10-06 02:44:44 +02:00
function fictioneer_is_admin( $user_id ) {
2023-01-21 01:31:34 +01:00
// Abort conditions
2023-08-22 10:28:40 +02:00
if ( ! $user_id ) {
return false;
}
2023-01-21 01:31:34 +01:00
// Check capabilities
$check = user_can( $user_id, 'administrator' );
// Filter
$check = apply_filters( 'fictioneer_filter_is_admin', $check, $user_id );
// Return result
return $check;
}
}
if ( ! function_exists( 'fictioneer_is_author' ) ) {
/**
* Checks if an user is an author
*
* @since 5.0.0
2023-01-21 01:31:34 +01:00
*
2023-10-06 02:44:44 +02:00
* @param int $user_id The user ID to check.
2023-01-21 01:31:34 +01:00
*
* @return boolean To be or not to be.
*/
2023-10-06 02:44:44 +02:00
function fictioneer_is_author( $user_id ) {
2023-01-21 01:31:34 +01:00
// Abort conditions
2023-08-22 10:28:40 +02:00
if ( ! $user_id ) {
return false;
}
2023-01-21 01:31:34 +01:00
// Check capabilities
2024-08-17 18:37:50 +02:00
$check = user_can( $user_id, 'publish_posts' ) ||
user_can( $user_id, 'publish_fcn_stories' ) ||
user_can( $user_id, 'publish_fcn_chapters' ) ||
user_can( $user_id, 'publish_fcn_collections' );
2023-01-21 01:31:34 +01:00
// Filter
$check = apply_filters( 'fictioneer_filter_is_author', $check, $user_id );
// Return result
return $check;
}
}
if ( ! function_exists( 'fictioneer_is_moderator' ) ) {
/**
* Checks if an user is a moderator
*
* @since 5.0.0
2023-01-21 01:31:34 +01:00
*
2023-10-06 02:44:44 +02:00
* @param int $user_id The user ID to check.
2023-01-21 01:31:34 +01:00
*
* @return boolean To be or not to be.
*/
2023-10-06 02:44:44 +02:00
function fictioneer_is_moderator( $user_id ) {
2023-01-21 01:31:34 +01:00
// Abort conditions
2023-08-22 10:28:40 +02:00
if ( ! $user_id ) {
return false;
}
2023-01-21 01:31:34 +01:00
// Check capabilities
$check = user_can( $user_id, 'moderate_comments' );
// Filter
$check = apply_filters( 'fictioneer_filter_is_moderator', $check, $user_id );
// Return result
return $check;
}
}
if ( ! function_exists( 'fictioneer_is_editor' ) ) {
/**
* Checks if an user is an editor
*
* @since 5.0.0
2023-01-21 01:31:34 +01:00
*
2023-10-06 02:44:44 +02:00
* @param int $user_id The user ID to check.
2023-01-21 01:31:34 +01:00
*
* @return boolean To be or not to be.
*/
2023-10-06 02:44:44 +02:00
function fictioneer_is_editor( $user_id ) {
2023-01-21 01:31:34 +01:00
// Abort conditions
2023-08-22 10:28:40 +02:00
if ( ! $user_id ) {
return false;
}
2023-01-21 01:31:34 +01:00
// Check capabilities
$check = user_can( $user_id, 'editor' ) || user_can( $user_id, 'administrator' );
// Filter
$check = apply_filters( 'fictioneer_filter_is_editor', $check, $user_id );
// Return result
return $check;
}
}
// =============================================================================
// GET META FIELDS
// =============================================================================
2024-10-27 13:03:27 +01:00
if ( ! function_exists( 'fictioneer_get_story_chapter_ids' ) ) {
/**
* Wrapper for get_post_meta() to get story chapter IDs
*
* @since 5.8.2
*
* @param int $post_id Optional. The ID of the post the field belongs to.
* Defaults to current post ID.
*
* @return array Array of chapter post IDs or an empty array.
*/
function fictioneer_get_story_chapter_ids( $post_id = null ) {
// Setup
$chapter_ids = get_post_meta( $post_id ?? get_the_ID(), 'fictioneer_story_chapters', true );
// Always return an array
return is_array( $chapter_ids ) ? $chapter_ids : [];
}
}
if ( ! function_exists( 'fictioneer_count_words' ) ) {
/**
* Returns word count of a post
*
* @since 5.25.0
*
* @param int $post_id ID of the post to count the words of.
* @param string|null $content Optional. The post content. Queries the field by default.
*
* @return int The word count.
*/
function fictioneer_count_words( $post_id, $content = null ) {
// Prepare
$content = $content ?? get_post_field( 'post_content', $post_id );
$content = strip_shortcodes( $content );
$content = strip_tags( $content );
$content = preg_replace( ['/--/', "/['-]/"], ['—', ''], $content );
// Count and return result
return count( preg_split( '/\s+/', $content ) ?: [] );
}
}
2023-11-30 17:10:19 +01:00
if ( ! function_exists( 'fictioneer_get_word_count' ) ) {
/**
* Wrapper for get_post_meta() to get word count
*
* @since 5.8.2
* @since 5.9.4 - Add logic for optional multiplier.
* @since 5.22.3 - Moved multiplier to fictioneer_multiply_word_count()
2024-08-28 19:03:43 +02:00
* @since 5.23.1 - Added filter and additional sanitization.
2023-11-30 17:10:19 +01:00
*
* @param int $post_id Optional. The ID of the post the field belongs to.
* Defaults to current post ID.
*
* @return int The word count or 0.
*/
function fictioneer_get_word_count( $post_id = null ) {
2024-08-28 19:03:43 +02:00
// Get word count
$words = get_post_meta( $post_id ?? get_the_ID(), '_word_count', true ) ?: 0;
$words = max( 0, intval( $words ) );
// Filter
$words = apply_filters( 'fictioneer_filter_word_count', $words, $post_id );
// Apply multiplier and return
return fictioneer_multiply_word_count( $words );
}
}
if ( ! function_exists( 'fictioneer_get_story_word_count' ) ) {
/**
* Returns word count for the whole story
*
* @since 5.22.3
2024-08-28 19:03:43 +02:00
* @since 5.23.1 - Added filter and additional sanitization.
*
* @param int $post_id Post ID of the story.
*
* @return int The word count or 0.
*/
function fictioneer_get_story_word_count( $post_id = null ) {
2024-08-28 19:03:43 +02:00
// Get word count
$words = get_post_meta( $post_id, 'fictioneer_story_total_word_count', true ) ?: 0;
2024-08-28 19:03:43 +02:00
$words = max( 0, intval( $words ) );
// Filter
$words = apply_filters( 'fictioneer_filter_story_word_count', $words, $post_id );
// Apply multiplier and return
return fictioneer_multiply_word_count( $words );
}
}
if ( ! function_exists( 'fictioneer_multiply_word_count' ) ) {
/**
* Multiplies word count with factor from options
*
* @since 5.22.3
*
* @param int $words Word count.
*
* @return int The updated word count.
*/
function fictioneer_multiply_word_count( $words ) {
// Setup
$multiplier = floatval( get_option( 'fictioneer_word_count_multiplier', 1.0 ) );
// Multiply
if ( $multiplier !== 1.0 ) {
$words = intval( $words * $multiplier );
}
2023-11-30 17:10:19 +01:00
// Always return an integer greater or equal 0
2024-08-28 19:03:43 +02:00
return max( 0, $words );
2023-11-30 17:10:19 +01:00
}
}
2023-01-21 01:31:34 +01:00
if ( ! function_exists( 'fictioneer_get_content_field' ) ) {
/**
2024-01-28 00:55:23 +01:00
* Wrapper for get_post_meta() with content filters applied
2023-01-21 01:31:34 +01:00
*
* @since 5.0.0
2023-01-21 01:31:34 +01:00
*
* @param string $field Name of the meta field to retrieve.
* @param int $post_id Optional. The ID of the post the field belongs to.
* Defaults to current post ID.
2023-01-21 01:31:34 +01:00
*
* @return string The single field value formatted as content.
*/
function fictioneer_get_content_field( $field, $post_id = null ) {
// Setup
$content = get_post_meta( $post_id ?? get_the_ID(), $field, true );
2023-01-21 01:31:34 +01:00
// Apply default filter functions from the_content (but nothing else)
$content = do_blocks( $content );
2023-01-21 01:31:34 +01:00
$content = wptexturize( $content );
$content = convert_chars( $content );
$content = wpautop( $content );
$content = shortcode_unautop( $content );
$content = prepend_attachment( $content );
$content = wp_replace_insecure_home_url( $content );
$content = wp_filter_content_tags( $content );
$content = convert_smilies( $content );
2023-01-21 01:31:34 +01:00
// Return formatted/filtered content
return $content;
}
}
if ( ! function_exists( 'fictioneer_get_icon_field' ) ) {
/**
* Wrapper for get_post_meta() to get Font Awesome icon class
2023-01-21 01:31:34 +01:00
*
* @since 5.0.0
* @since 5.26.0 - Add $icon parameter as override.
2023-01-21 01:31:34 +01:00
*
* @param string $field Name of the meta field to retrieve.
* @param int|null $post_id Optional. The ID of the post the field belongs to.
* Defaults to current post ID.
* @param string|null $icon Optional. Pre-retrieved icon string as override. Default null.
2023-01-21 01:31:34 +01:00
*
* @return string The Font Awesome class.
*/
function fictioneer_get_icon_field( $field, $post_id = null, $icon = null ) {
2023-01-21 01:31:34 +01:00
// Setup
$icon = $icon ?? get_post_meta( $post_id ?? get_the_ID(), $field, true );
2023-11-20 14:16:05 +01:00
$icon_object = json_decode( $icon ); // Check for ACF Font Awesome plugin
2023-01-21 01:31:34 +01:00
// Valid?
if ( ! $icon_object && ( empty( $icon ) || strpos( $icon, 'fa-' ) !== 0 ) ) {
2024-05-23 20:30:08 +02:00
return FICTIONEER_DEFAULT_CHAPTER_ICON;
}
if ( $icon_object && ( ! property_exists( $icon_object, 'style' ) || ! property_exists( $icon_object, 'id' ) ) ) {
2024-05-23 20:30:08 +02:00
return FICTIONEER_DEFAULT_CHAPTER_ICON;
2023-01-21 01:31:34 +01:00
}
// Return
if ( $icon_object && property_exists( $icon_object, 'style' ) && property_exists( $icon_object, 'id' ) ) {
return 'fa-' . $icon_object->style . ' fa-' . $icon_object->id;
} else {
return esc_attr( $icon );
}
}
}
2023-09-18 14:49:36 +02:00
// =============================================================================
// UPDATE META FIELDS
// =============================================================================
2023-09-18 18:06:46 +02:00
if ( ! function_exists( 'fictioneer_update_user_meta' ) ) {
/**
* Wrapper to update user meta
*
* If the meta value is truthy, the meta field is updated as normal.
* If not, the meta field is deleted instead to keep the database tidy.
*
* @since 5.7.3
*
* @param int $user_id The ID of the user.
* @param string $meta_key The meta key to update.
* @param mixed $meta_value The new meta value. If empty, the meta key will be deleted.
* @param mixed $prev_value Optional. If specified, only updates existing metadata with this value.
* Otherwise, update all entries. Default empty.
*
* @return int|bool Meta ID if the key didn't exist on update, true on successful update or delete,
* false on failure or if the value passed to the function is the same as the one
* that is already in the database.
*/
function fictioneer_update_user_meta( $user_id, $meta_key, $meta_value, $prev_value = '' ) {
2023-10-03 23:42:37 +02:00
if ( empty( $meta_value ) && ! in_array( $meta_key, fictioneer_get_falsy_meta_allow_list() ) ) {
2023-09-18 18:06:46 +02:00
return delete_user_meta( $user_id, $meta_key );
} else {
return update_user_meta( $user_id, $meta_key, $meta_value, $prev_value );
}
}
}
2023-09-18 14:49:36 +02:00
if ( ! function_exists( 'fictioneer_update_comment_meta' ) ) {
/**
* Wrapper to update comment meta
*
* If the meta value is truthy, the meta field is updated as normal.
* If not, the meta field is deleted instead to keep the database tidy.
*
* @since 5.7.3
*
* @param int $comment_id The ID of the comment.
* @param string $meta_key The meta key to update.
* @param mixed $meta_value The new meta value. If empty, the meta key will be deleted.
* @param mixed $prev_value Optional. If specified, only updates existing metadata with this value.
* Otherwise, update all entries. Default empty.
*
* @return int|bool Meta ID if the key didn't exist on update, true on successful update or delete,
* false on failure or if the value passed to the function is the same as the one
* that is already in the database.
2023-09-18 14:49:36 +02:00
*/
function fictioneer_update_comment_meta( $comment_id, $meta_key, $meta_value, $prev_value = '' ) {
2023-10-03 23:42:37 +02:00
if ( empty( $meta_value ) && ! in_array( $meta_key, fictioneer_get_falsy_meta_allow_list() ) ) {
return delete_comment_meta( $comment_id, $meta_key );
2023-09-18 14:49:36 +02:00
} else {
return update_comment_meta( $comment_id, $meta_key, $meta_value, $prev_value );
2023-09-18 14:49:36 +02:00
}
}
}
if ( ! function_exists( 'fictioneer_update_post_meta' ) ) {
/**
* Wrapper to update post meta
*
* If the meta value is truthy, the meta field is updated as normal.
* If not, the meta field is deleted instead to keep the database tidy.
*
* @since 5.7.4
*
* @param int $post_id The ID of the post.
* @param string $meta_key The meta key to update.
* @param mixed $meta_value The new meta value. If empty, the meta key will be deleted.
* @param mixed $prev_value Optional. If specified, only updates existing metadata with this value.
* Otherwise, update all entries. Default empty.
*
* @return int|bool Meta ID if the key didn't exist on update, true on successful update or delete,
* false on failure or if the value passed to the function is the same as the one
* that is already in the database.
*/
function fictioneer_update_post_meta( $post_id, $meta_key, $meta_value, $prev_value = '' ) {
2023-10-03 23:42:37 +02:00
if ( empty( $meta_value ) && ! in_array( $meta_key, fictioneer_get_falsy_meta_allow_list() ) ) {
return delete_post_meta( $post_id, $meta_key );
} else {
return update_post_meta( $post_id, $meta_key, $meta_value, $prev_value );
}
}
}
2023-10-03 23:42:37 +02:00
/**
* Return allow list for falsy meta fields
*
* @since 5.7.4
*
* @return array Meta fields allowed to be saved falsy and not be deleted.
*/
function fictioneer_get_falsy_meta_allow_list() {
2024-05-10 23:01:01 +02:00
return apply_filters( 'fictioneer_filter_falsy_meta_allow_list', [] );
2023-10-03 23:42:37 +02:00
}
// =============================================================================
// APPEND CHAPTER
// =============================================================================
/**
* Appends new chapters to story list
*
* @since 5.4.9
* @since 5.7.4 - Updated
* @since 5.8.6 - Added $force param and moved function.
2024-06-10 02:25:46 +02:00
* @since 5.19.1 - Always append chapter to story.
*
* @param int $post_id The chapter post ID.
* @param int $story_id The story post ID.
* @param bool $force Optional. Whether to skip some guard clauses. Default false.
*/
function fictioneer_append_chapter_to_story( $post_id, $story_id, $force = false ) {
2024-09-25 22:29:18 +02:00
$allowed_statuses = apply_filters(
'fictioneer_filter_append_chapter_to_story_statuses',
['publish', 'future'],
2024-09-25 22:29:18 +02:00
$post_id,
$story_id,
$force
);
2024-09-25 22:29:18 +02:00
// Abort if chapter status is not allowed
if ( ! in_array( get_post_status( $post_id ), $allowed_statuses ) ) {
return;
}
// Setup
$story = get_post( $story_id );
// Abort if story not found or not a story
if ( ! $story || $story->post_type !== 'fcn_story' ) {
return;
}
// Setup, continued
$chapter_author_id = get_post_field( 'post_author', $post_id );
$story_author_id = get_post_field( 'post_author', $story_id );
$co_authored_story_ids = fictioneer_sql_get_co_authored_story_ids( $chapter_author_id );
// Abort if the author IDs do not match
2024-10-29 16:32:32 +01:00
if (
$chapter_author_id != $story_author_id &&
! in_array( $story_id, $co_authored_story_ids ) &&
! $force
) {
return;
}
// Get current story chapters
$story_chapters = fictioneer_get_story_chapter_ids( $story_id );
// Append chapter (if not already included) and save to database
if ( ! in_array( $post_id, $story_chapters ) ) {
$previous_chapters = $story_chapters;
$story_chapters[] = $post_id;
$story_chapters = array_unique( $story_chapters );
2024-01-03 08:36:42 +01:00
// Save updated list
update_post_meta( $story_id, 'fictioneer_story_chapters', $story_chapters );
// Remember when chapters have been changed
update_post_meta( $story_id, 'fictioneer_chapters_modified', current_time( 'mysql', true ) );
// Remember when chapters have been added
$allowed_statuses = apply_filters(
'fictioneer_filter_chapters_added_statuses',
['publish'],
$post_id
);
if ( in_array( get_post_status( $post_id ), $allowed_statuses ) ) {
if ( ! get_post_meta( $post_id, 'fictioneer_chapter_hidden', true ) ) {
update_post_meta( $story_id, 'fictioneer_chapters_added', current_time( 'mysql', true ) );
}
}
// Log changes
fictioneer_log_story_chapter_changes( $story_id, $story_chapters, $previous_chapters );
2024-01-29 18:31:01 +01:00
// Clear meta caches to ensure they get refreshed
delete_post_meta( $story_id, 'fictioneer_story_data_collection' );
2024-01-29 18:31:01 +01:00
delete_post_meta( $story_id, 'fictioneer_story_chapter_index_html' );
} else {
// Nothing to do
return;
}
// Update story post to fire associated actions
wp_update_post( array( 'ID' => $story_id ) );
}
2023-01-21 01:31:34 +01:00
// =============================================================================
// GET COOKIE CONSENT
// =============================================================================
if ( ! function_exists( 'fictioneer_get_consent' ) && get_option( 'fictioneer_cookie_banner' ) ) {
/**
* Get cookie consent
*
* Checks the current users consent cookie for their preferences, either 'full'
* or 'necessary' by default. Returns false if no consent cookie is set.
*
* @since 4.7.0
2023-01-21 01:31:34 +01:00
*
* @return boolean|string Either false or a string describing the level of consent.
*/
function fictioneer_get_consent() {
2023-08-28 10:43:04 +02:00
if ( ! isset( $_COOKIE['fcn_cookie_consent'] ) || $_COOKIE['fcn_cookie_consent'] === '' ) {
return false;
}
2023-01-21 01:31:34 +01:00
return strval( $_COOKIE['fcn_cookie_consent'] );
}
}
// =============================================================================
// SANITIZE INTEGER
// =============================================================================
/**
* Sanitizes an integer with options for default, minimum, and maximum
2023-01-21 01:31:34 +01:00
*
* @since 4.0.0
2023-01-21 01:31:34 +01:00
*
2023-08-28 10:43:04 +02:00
* @param mixed $value The value to be sanitized.
* @param int $default Default value if an invalid integer is provided. Default 0.
* @param int $min Optional. Minimum value for the integer. Default is no minimum.
* @param int $max Optional. Maximum value for the integer. Default is no maximum.
2023-01-21 01:31:34 +01:00
*
* @return int The sanitized integer.
*/
2023-08-28 10:43:04 +02:00
function fictioneer_sanitize_integer( $value, $default = 0, $min = null, $max = null ) {
// Ensure $value is numeric in the first place
if ( ! is_numeric( $value ) ) {
return $default;
}
// Cast to integer
2023-01-21 01:31:34 +01:00
$value = (int) $value;
2023-08-28 10:43:04 +02:00
// Apply minimum limit if specified
if ( $min !== null && $value < $min ) {
return $min;
}
// Apply maximum limit if specified
if ( $max !== null && $value > $max ) {
return $max;
}
2023-01-21 01:31:34 +01:00
return $value;
}
// =============================================================================
// SANITIZE POSITIVE FLOAT
// =============================================================================
/**
* Sanitizes a float as positive number
*
* @since 5.9.4
*
* @param mixed $value The value to be sanitized.
* @param int $default Default value if an invalid float is provided. Default 0.0.
*
* @return float The sanitized float.
*/
function fictioneer_sanitize_positive_float( $value, $default = 0.0 ) {
// Ensure $value is numeric in the first place
if ( ! is_numeric( $value ) ) {
return $default;
}
// Cast to float
$value = (float) $value;
// Return positive float
return $value < 0 ? $default : $value;
}
/**
2024-02-12 13:58:11 +01:00
* Sanitize callback with positive float or default 1.0
*
2024-02-12 13:58:11 +01:00
* @since 5.10.1
*
* @param mixed $value The value to be sanitized.
*
2024-02-12 13:58:11 +01:00
* @return float The sanitized positive float.
*/
function fictioneer_sanitize_positive_float_def1( $value ) {
// Ensure $value is numeric in the first place
if ( ! is_numeric( $value ) ) {
return 1.0;
}
// Call general sanitizer with params
return fictioneer_sanitize_positive_float( $value, 1.0 );
}
/**
* Sanitize callback with float or default 0
*
* @since 5.19.0
*
* @param mixed $value The value to be sanitized.
*
* @return float The sanitized float.
*/
function fictioneer_sanitize_float( $value ) {
// Ensure $value is numeric in the first place
if ( ! is_numeric( $value ) ) {
return 0.0;
}
// Cast to float
return (float) $value;
}
2023-01-21 01:31:34 +01:00
// =============================================================================
// SANITIZE CHECKBOX
// =============================================================================
/**
* Sanitizes a checkbox value into true or false
*
* @since 4.7.0
* @link https://www.php.net/manual/en/function.filter-var.php
2023-01-21 01:31:34 +01:00
*
* @param string|boolean $value The checkbox value to be sanitized.
2023-01-21 01:31:34 +01:00
*
* @return boolean True or false.
*/
function fictioneer_sanitize_checkbox( $value ) {
$value = filter_var( $value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE );
return empty( $value ) ? 0 : 1;
2023-01-21 01:31:34 +01:00
}
2023-09-19 23:46:31 +02:00
// =============================================================================
// SANITIZE SELECT OPTION
// =============================================================================
/**
* Sanitizes a selected option
*
* @since 5.7.4
*
* @param mixed $value The selected value to be sanitized.
* @param array $allowed_options The allowed values to be checked against.
* @param mixed $default Optional. The default value as fallback.
*
* @return mixed The sanitized value or default, null if not provided.
*/
function fictioneer_sanitize_selection( $value, $allowed_options, $default = null ) {
$value = sanitize_text_field( $value );
$value = is_numeric( $value ) ? intval( $value ) : $value;
return in_array( $value, $allowed_options ) ? $value : $default;
}
2023-09-29 19:41:28 +02:00
// =============================================================================
// SANITIZE CSS
// =============================================================================
/**
* Sanitizes a CSS string
*
* @since 5.7.4
*
* @param string $css The CSS string to be sanitized.
*
* @return string The sanitized string.
*/
function fictioneer_sanitize_css( $css ) {
$css = sanitize_textarea_field( $css );
$css = preg_match( '/<\/?\w+/', $css ) ? '' : $css;
$opening_braces = substr_count( $css, '{' );
$closing_braces = substr_count( $css, '}' );
if ( $opening_braces < 1 || $opening_braces !== $closing_braces ) {
$css = '';
}
return $css;
}
// =============================================================================
// SANITIZE LIST INTO ARRAY
// =============================================================================
/**
* Sanitizes (and transforms) a comma-separated list into array
*
* @since 5.15.0
*
* @param string $input The comma-separated list.
* @param array $args {
* Optional. An array of additional arguments.
*
* @type bool $unique Run array through array_unique(). Default false.
* @type bool $absint Run all elements through absint(). Default false.
* }
*
* @return array The comma-separated list turned array.
*/
function fictioneer_sanitize_list_into_array( $input, $args = [] ) {
$input = fictioneer_explode_list( sanitize_textarea_field( $input ) );
if ( $args['absint'] ?? 0 ) {
$input = array_map( 'absint', $input );
}
if ( $args['unique'] ?? 0 ) {
$input = array_unique( $input );
}
return $input;
}
// =============================================================================
// SANITIZE QUERY VARIABLE
// =============================================================================
/**
* Sanitizes a query variable
*
* @since 5.14.0
*
* @param string $var Query variable to sanitize.
* @param array $allowed Array of allowed string (lowercase).
* @param string|null $default Optional default value.
* @param array $args {
* Optional. An array of additional arguments.
*
* @type bool $keep_case Whether to transform the variable to lowercase. Default false.
* }
*
*
* @return string The sanitized (lowercase) query variable.
*/
function fictioneer_sanitize_query_var( $var, $allowed, $default = null, $args = [] ) {
2024-05-17 15:26:51 +02:00
if ( isset( $args['keep_case'] ) ) {
$sanitized = array_intersect( [ $var ?? 0 ], $allowed );
} else {
$sanitized = array_intersect( [ strtolower( $var ?? 0 ) ], $allowed );
}
return reset( $sanitized ) ?: $default;
}
// =============================================================================
// SANITIZE URL
// =============================================================================
/**
* Sanitizes an URL
*
* @since 5.19.1
*
* @param string $url The URL entered.
* @param string|null $match Optional. URL must start with this string.
* @param string|null $preg_match Optional. String for a preg_match() test.
*
* @return string The sanitized URL or an empty string if invalid.
*/
function fictioneer_sanitize_url( $url, $match = null, $preg_match = null ) {
$url = sanitize_url( $url );
$url = filter_var( $url, FILTER_VALIDATE_URL ) ? $url : '';
if ( $match && is_string( $match ) ) {
$url = strpos( $url, $match ) === 0 ? $url : '';
}
if ( $preg_match && is_string( $preg_match ) ) {
$url = preg_match( $preg_match, $url ) ? $url : '';
}
return $url;
}
2024-04-14 14:25:18 +02:00
// =============================================================================
2024-04-14 15:13:18 +02:00
// ASPECT RATIO CSS
2024-04-14 14:25:18 +02:00
// =============================================================================
/**
* Sanitizes a CSS aspect ratio value
*
* @since 5.14.0
* @since 5.23.0 - Refactored to accept fractional values.
2024-04-14 14:25:18 +02:00
*
* @param string $css The CSS value to be sanitized.
* @param string $default Optional default value.
*
* @return string|bool The sanitized value or default if invalid.
*/
function sanitize_css_aspect_ratio( $css, $default = false ) {
// Remove unwanted white spaces
$css = trim( $css );
// Validate
if ( preg_match( '/^\d+(\.\d+)?\/\d+(\.\d+)?$/', $css ) ) {
2024-04-14 14:25:18 +02:00
// Split based on the slash '/'
list( $numerator, $denominator ) = explode( '/', $css, 2 );
// Sanitize parts
$numerator = max( 0, floatval( $numerator ) );
$denominator = max( 0, floatval( $denominator ) );
// Nonsense?
if ( $numerator == 0 || $denominator == 0 ) {
return $default;
}
2024-04-14 14:25:18 +02:00
// Combine and return
return $numerator . '/' . $denominator;
}
// Default if invalid
return $default;
}
2024-04-14 15:13:18 +02:00
/**
* Returns aspect ratio values as tuple
*
* @since 5.14.0
*
* @param string $css The aspect-ratio CSS value.
*
* @return array Tuple of aspect-ratio values.
*/
function fictioneer_get_split_aspect_ratio( $css ) {
// Split based on the slash '/'
list( $numerator, $denominator ) = explode( '/', $css, 2 );
// Return tuple
return array( (int) ( $numerator ?? 1 ), (int) ( $denominator ?? 1 ) );
}
2024-05-16 14:26:59 +02:00
// =============================================================================
// SHOW LOGIN
// =============================================================================
/**
* Checks whether the login should be rendered
*
* @since 5.18.1
*
* @return boolean True or false.
*/
function fictioneer_show_login() {
$enabled = get_option( 'fictioneer_enable_oauth' ) || get_option( 'fictioneer_show_wp_login_link' );
return ( $enabled && ! is_user_logged_in() ) || get_option( 'fictioneer_enable_public_cache_compatibility' );
2024-05-16 14:26:59 +02:00
}
2023-01-21 01:31:34 +01:00
// =============================================================================
// SHOW NON-PUBLIC CONTENT
// =============================================================================
/**
* Wrapper for is_user_logged_in() with global public cache consideration
*
* If public caches are served to all users, including logged-in users, it is
* necessary to render items that would normally be skipped for logged-out
* users, such as the profile link. They are hidden via CSS, which works as
* long as the 'logged-in' class is still set on the <body>.
*
* @since 5.0.0
2023-01-21 01:31:34 +01:00
*
* @return boolean True or false.
*/
function fictioneer_show_auth_content() {
2024-01-16 08:00:17 +01:00
return is_user_logged_in() ||
get_option( 'fictioneer_enable_public_cache_compatibility' ) ||
get_option( 'fictioneer_enable_ajax_authentication' );
2023-01-21 01:31:34 +01:00
}
// =============================================================================
// FICTIONEER TRANSLATIONS
// =============================================================================
/**
* Returns selected translations
*
* Adding theme options for all possible translations would be a pain,
* so this is done with a function that can be filtered as needed. For
* giving your site a personal touch.
*
* @since 5.0.0
2023-01-21 01:31:34 +01:00
*
* @param string $key Key for requested translation.
* @param boolean $escape Optional. Escape the string for safe use in
* attributes. Default false.
2023-01-21 01:31:34 +01:00
*
* @return string The translation or an empty string if not found.
*/
function fcntr( $key, $escape = false ) {
2024-04-08 12:32:09 +02:00
static $strings = null;
2023-01-21 01:31:34 +01:00
// Define default translations
2024-04-08 12:32:09 +02:00
if ( $strings === null ) {
$strings = array(
'account' => __( 'Account', 'fictioneer' ),
'bookshelf' => __( 'Bookshelf', 'fictioneer' ),
'admin' => _x( 'Admin', 'Caption for administrator badge label.', 'fictioneer' ),
'anonymous_guest' => __( 'Anonymous Guest', 'fictioneer' ),
'author' => _x( 'Author', 'Caption for author badge label.', 'fictioneer' ),
'bbcodes_modal' => _x( 'BBCodes', 'Heading for BBCodes tutorial modal.', 'fictioneer' ),
'blog' => _x( 'Blog', 'Blog page name, mainly used in breadcrumbs.', 'fictioneer' ),
'bookmark' => __( 'Bookmark', 'fictioneer' ),
'bookmarks' => __( 'Bookmarks', 'fictioneer' ),
'warning_notes' => __( 'Warning Notes', 'fictioneer' ),
'deleted_user' => __( 'Deleted User', 'fictioneer' ),
'follow' => _x( 'Follow', 'Follow a story.', 'fictioneer' ),
'follows' => _x( 'Follows', 'List of followed stories.', 'fictioneer' ),
'forget' => _x( 'Forget', 'Forget story set to be read later.', 'fictioneer' ),
'formatting' => _x( 'Formatting', 'Toggle for chapter formatting modal.', 'fictioneer' ),
'formatting_modal' => _x( 'Formatting', 'Chapter formatting modal heading.', 'fictioneer' ),
'frontpage' => _x( 'Home', 'Frontpage page name, mainly used in breadcrumbs.', 'fictioneer' ),
'is_followed' => _x( 'Followed', 'Story is followed.', 'fictioneer' ),
'is_read' => _x( 'Read', 'Story or chapter is marked as read.', 'fictioneer' ),
'is_read_later' => _x( 'Read later', 'Story is marked to be read later.', 'fictioneer' ),
'jump_to_comments' => __( 'Jump: Comments', 'fictioneer' ),
'jump_to_bookmark' => __( 'Jump: Bookmark', 'fictioneer' ),
'login' => __( 'Login', 'fictioneer' ),
'login_modal' => _x( 'Login', 'Login modal heading.', 'fictioneer' ),
'login_with' => _x( 'Log in with', 'OAuth 2.0 login option plus appended icon.', 'fictioneer' ),
'logout' => __( 'Logout', 'fictioneer' ),
'mark_read' => _x( 'Mark Read', 'Mark story as read.', 'fictioneer' ),
'mark_unread' => _x( 'Mark Unread', 'Mark story as unread.', 'fictioneer' ),
'moderator' => _x( 'Mod', 'Caption for moderator badge label', 'fictioneer' ),
'next' => __( '<span class="on">Next</span><span class="off"><i class="fa-solid fa-caret-right"></i></span>', 'fictioneer' ),
'no_bookmarks' => __( 'No bookmarks.', 'fictioneer' ),
'password' => __( 'Password', 'fictioneer' ),
'previous' => __( '<span class="off"><i class="fa-solid fa-caret-left"></i></span><span class="on">Previous</span>', 'fictioneer' ),
'read_later' => _x( 'Read Later', 'Remember a story to be read later.', 'fictioneer' ),
'read_more' => _x( 'Read More', 'Read more of a post.', 'fictioneer' ),
'reminders' => _x( 'Reminders', 'List of stories to read later.', 'fictioneer' ),
'site_settings' => __( 'Site Settings', 'fictioneer' ),
'story_blog' => _x( 'Blog', 'Blog tab of the story.', 'fictioneer' ),
'subscribe' => _x( 'Subscribe', 'Subscribe to a story.', 'fictioneer' ),
'unassigned_group' => _x( 'Unassigned', 'Chapters not assigned to group.', 'fictioneer' ),
'unfollow' => _x( 'Unfollow', 'Stop following a story.', 'fictioneer' ),
'E' => _x( 'E', 'Age rating E for Everyone.', 'fictioneer' ),
'T' => _x( 'T', 'Age rating T for Teen.', 'fictioneer' ),
'M' => _x( 'M', 'Age rating M for Mature.', 'fictioneer' ),
'A' => _x( 'A', 'Age rating A for Adult.', 'fictioneer' ),
'Everyone' => _x( 'Everyone', 'Age rating Everyone.', 'fictioneer' ),
'Teen' => _x( 'Teen', 'Age rating Teen.', 'fictioneer' ),
'Mature' => _x( 'Mature', 'Age rating Mature.', 'fictioneer' ),
'Adult' => _x( 'Adult', 'Age rating Adult.', 'fictioneer' ),
'Completed' => _x( 'Completed', 'Completed story status.', 'fictioneer' ),
'Ongoing' => _x( 'Ongoing', 'Ongoing story status', 'fictioneer' ),
'Hiatus' => _x( 'Hiatus', 'Hiatus story status', 'fictioneer' ),
'Oneshot' => _x( 'Oneshot', 'Oneshot story status', 'fictioneer' ),
'Canceled' => _x( 'Canceled', 'Canceled story status', 'fictioneer' ),
'comment_anchor' => _x( '<i class="fa-solid fa-link"></i>', 'Text or icon for paragraph anchor in comments.', 'fictioneer' ),
'bbcode_b' => _x( '<code>[b]</code><strong>Bold</strong><code>[/b]</code> of you to assume I have a plan.', 'fictioneer' ),
'bbcode_i' => _x( 'Deathbringer, emphasis on <code>[i]</code><em>death</em><code>[/i]</code>.', 'fictioneer' ),
'bbcode_s' => _x( 'Im totally <code>[s]</code><strike>crossed out</strike><code>[/s]</code> by this.', 'fictioneer' ),
'bbcode_li' => _x( '<ul><li class="comment-list-item">Listless Im counting my <code>[li]</code>bullets<code>[/li]</code>.</li></ul>', 'fictioneer' ),
'bbcode_img' => _x( '<code>[img]</code>https://www.agine.this<code>[/img]</code> %s', 'BBCode example.', 'fictioneer' ),
'bbcode_link' => _x( '<code>[link]</code><a href="http://topwebfiction.com/" target="_blank" class="link">http://topwebfiction.com</a><code>[/link]</code>.', 'BBCode example.', 'fictioneer' ),
'bbcode_link_name' => _x( '<code>[link=https://www.n.ot]</code><a href="http://topwebfiction.com/" class="link">clickbait</a><code>[/link]</code>.', 'BBCode example.', 'fictioneer' ),
'bbcode_quote' => _x( '<blockquote><code>[quote]</code>… me like my landlord!<code>[/quote]</code></blockquote>', 'BBCode example.', 'fictioneer' ),
'bbcode_spoiler' => _x( '<code>[spoiler]</code><span class="spoiler">Spanish Inquisition!</span><code>[/spoiler]</code>', 'BBCode example.', 'fictioneer' ),
'bbcode_ins' => _x( '<code>[ins]</code><ins>Insert</ins><code>[/ins]</code> more bad puns!', 'BBCode example.', 'fictioneer' ),
'bbcode_del' => _x( '<code>[del]</code><del>Delete</del><code>[/del]</code> your browser history!', 'BBCode example.', 'fictioneer' ),
'log_in_with' => _x( 'Enter your details or log in with:', 'Comment form login note.', 'fictioneer' ),
2024-12-06 17:38:32 +01:00
'logged_in_as' => _x( '<span>Logged in as <strong><a href="%1$s">%2$s</a></strong>. <a class="logout-link" href="%3$s" data-action="click->fictioneer#logout">Log out?</a></span>', 'Comment form logged-in note.', 'fictioneer' ),
2024-04-08 12:32:09 +02:00
'accept_privacy_policy' => _x( 'I accept the <b><a class="link" href="%s" target="_blank">privacy policy</a></b>.', 'Comment form privacy checkbox.', 'fictioneer' ),
'save_in_cookie' => _x( 'Save in cookie for next time.', 'Comment form cookie checkbox.', 'fictioneer' ),
'future_prefix' => _x( 'Scheduled:', 'Chapter list status prefix.', 'fictioneer' ),
'trashed_prefix' => _x( 'Trashed:', 'Chapter list status prefix.', 'fictioneer' ),
'private_prefix' => _x( 'Private:', 'Chapter list status prefix.', 'fictioneer' ),
'free_patreon_tier' => _x( 'Follower (Free)', 'Free Patreon tier (follower).', 'fictioneer' ),
'private_comment' => _x( 'Private Comment', 'Comment type translation.', 'fictioneer' ),
'comment_comment' => _x( 'Public Comment', 'Comment type translation.', 'fictioneer' ),
'approved_comment_status' => _x( 'Approved', 'Comment status translation.', 'fictioneer' ),
'hold_comment_status' => _x( 'Hold', 'Comment status translation.', 'fictioneer' ),
'unapproved_comment_status' => _x( 'Unapproved', 'Comment status translation.', 'fictioneer' ),
'spam_comment_status' => _x( 'Spam', 'Comment status translation.', 'fictioneer' ),
'trash_comment_status' => _x( 'Trash', 'Comment status translation.', 'fictioneer' ),
2024-04-08 12:32:09 +02:00
);
// Filter static translations
$strings = apply_filters( 'fictioneer_filter_translations_static', $strings );
2024-04-08 12:32:09 +02:00
}
2023-01-21 01:31:34 +01:00
// Filter translations
$strings = apply_filters( 'fictioneer_filter_translations', $strings );
// Return requested translation if defined...
if ( array_key_exists( $key, $strings ) ) {
return $escape ? esc_attr( $strings[ $key ] ) : $strings[ $key ];
}
// ... otherwise return empty string
return '';
}
// =============================================================================
// BALANCE PAGINATION ARRAY
// =============================================================================
/**
* Balances pagination array
*
* Takes an number array of pagination pages and balances the items around the
* current page number, replacing anything above the keep threshold with ellipses.
* E.g. 1 7, 8, [9], 10, 11 20.
*
* @since 5.0.0
2023-01-21 01:31:34 +01:00
*
* @param array|int $pages Array of pages to balance. If an integer is provided,
* it is converted to a number array.
* @param int $current Current page number.
* @param int $keep Optional. Balancing factor to each side. Default 2.
* @param string $ellipses Optional. String for skipped numbers. Default '…'.
2023-01-21 01:31:34 +01:00
*
* @return array The balanced array.
*/
function fictioneer_balance_pagination_array( $pages, $current, $keep = 2, $ellipses = '…' ) {
// Setup
$max_pages = is_array( $pages ) ? count( $pages ) : $pages;
$steps = is_array( $pages ) ? $pages : [];
if ( ! is_array( $pages ) ) {
for ( $i = 1; $i <= $max_pages; $i++ ) {
$steps[] = $i;
}
}
// You know, I wrote this but don't really get it myself...
if ( $max_pages - $keep * 2 > $current ) {
$start = $current + $keep;
$end = $max_pages - $keep + 1;
for ( $i = $start; $i < $end; $i++ ) {
unset( $steps[ $i ] );
}
array_splice( $steps, count( $steps ) - $keep + 1, 0, $ellipses );
}
// It certainly does math...
if ( $current - $keep * 2 >= $keep ) {
$start = $keep - 1;
$end = $current - $keep - 1;
for ( $i = $start; $i < $end; $i++ ) {
unset( $steps[ $i ] );
}
array_splice( $steps, $keep - 1, 0, $ellipses );
}
return $steps;
}
// =============================================================================
// CHECK WHETHER COMMENTING IS DISABLED
// =============================================================================
if ( ! function_exists( 'fictioneer_is_commenting_disabled' ) ) {
/**
* Check whether commenting is disabled
*
* Differs from comments_open() in the regard that it does not hide the whole
* comment section but does not allow new comments to be posted.
*
* @since 5.0.0
2023-01-21 01:31:34 +01:00
*
* @param int|null $post_id Post ID the comments are for. Defaults to current post ID.
2023-01-21 01:31:34 +01:00
*
* @return boolean True or false.
*/
function fictioneer_is_commenting_disabled( $post_id = null ) {
// Setup
$post_id = $post_id ?? get_the_ID();
// Return immediately if...
if (
get_option( 'fictioneer_disable_commenting' ) ||
get_post_meta( $post_id, 'fictioneer_disable_commenting', true )
) {
return true;
}
// Check parent story if chapter...
if ( get_post_type( $post_id ) === 'fcn_chapter' ) {
$story_id = fictioneer_get_chapter_story_id( $post_id );
if ( $story_id ) {
return get_post_meta( $story_id, 'fictioneer_disable_commenting', true ) == true;
}
}
return false;
2023-01-21 01:31:34 +01:00
}
}
// =============================================================================
// CHECK DISALLOWED KEYS WITH OFFENSES RETURNED
// =============================================================================
if ( ! function_exists( 'fictioneer_check_comment_disallowed_list' ) ) {
/**
* Checks whether a comment contains disallowed characters or words and
* returns the offenders within the comment content
*
* @since 5.0.0
2023-01-21 01:31:34 +01:00
*
* @param string $author The author of the comment.
* @param string $email The email of the comment.
* @param string $url The url used in the comment.
* @param string $comment The comment content
* @param string $user_ip The comment author's IP address.
* @param string $user_agent The author's browser user agent.
2023-01-21 01:31:34 +01:00
*
* @return array Tuple of true/false [0] and offenders [1] as array.
*/
function fictioneer_check_comment_disallowed_list( $author, $email, $url, $comment, $user_ip, $user_agent ) {
// Implementation is the same as wp_check_comment_disallowed_list(...)
$mod_keys = trim( get_option( 'disallowed_keys' ) );
if ( '' === $mod_keys ) {
2023-01-26 01:58:36 +01:00
return [false, []]; // If moderation keys are empty.
2023-01-21 01:31:34 +01:00
}
// Ensure HTML tags are not being used to bypass the list of disallowed characters and words.
$comment_without_html = wp_strip_all_tags( $comment );
$words = explode( "\n", $mod_keys );
foreach ( (array) $words as $word ) {
$word = trim( $word );
// Skip empty lines.
if ( empty( $word ) ) {
continue;
}
2023-01-21 01:31:34 +01:00
// Do some escaping magic so that '#' chars in the spam words don't break things:
$word = preg_quote( $word, '#' );
$matches = false;
$pattern = "#$word#i";
if ( preg_match( $pattern, $author )
|| preg_match( $pattern, $email )
|| preg_match( $pattern, $url )
|| preg_match( $pattern, $comment, $matches )
|| preg_match( $pattern, $comment_without_html, $matches )
|| preg_match( $pattern, $user_ip )
|| preg_match( $pattern, $user_agent )
) {
return [true, $matches];
}
}
return [false, []];
}
}
// =============================================================================
// BBCODES
// =============================================================================
if ( ! function_exists( 'fictioneer_bbcodes' ) ) {
/**
* Interprets BBCodes into HTML
*
* Note: Spoilers do not work properly if wrapping multiple lines or other codes.
*
* @since 4.0.0
* @link https://stackoverflow.com/a/17508056/17140970
2023-01-21 01:31:34 +01:00
*
* @param string $content The content.
*
* @return string The content with interpreted BBCodes.
2023-01-21 01:31:34 +01:00
*/
function fictioneer_bbcodes( $content ) {
// Setup
$img_search = '\s*https:[^\"\'|;<>\[\]]+?\.(?:png|jpg|jpeg|gif|webp|svg|avif|tiff).*?\s*';
$url_search = '\s*(http|ftp|https):\/\/[\w-]+(\.[\w-]+)+([\w.,@?^=%&amp;:\/~+#-]*[\w@?^=%&amp;\/~+#-])?\s*';
2023-01-21 01:31:34 +01:00
// Deal with some multi-line spoiler issues
2023-06-29 22:15:03 +02:00
if ( preg_match_all( '/\[spoiler](.+?)\[\/spoiler]/is', $content, $spoilers, PREG_PATTERN_ORDER ) ) {
2023-01-21 01:31:34 +01:00
foreach ( $spoilers[0] as $spoiler ) {
$replace = str_replace( '<p></p>', ' ', $spoiler );
2023-06-29 22:15:03 +02:00
$replace = preg_replace( '/\[quote](.+?)\[\/quote]/is', '<blockquote class="spoiler">$1</blockquote>', $replace );
2023-01-21 01:31:34 +01:00
$content = str_replace( $spoiler, $replace, $content );
}
}
// Possible patterns
$patterns = array(
2023-06-29 22:15:03 +02:00
'/\[spoiler]\[quote](.+?)\[\/quote]\[\/spoiler]/is',
2023-01-21 01:31:34 +01:00
'/\[spoiler](.+?)\[\/spoiler]/i',
2023-06-29 22:15:03 +02:00
'/\[spoiler](.+?)\[\/spoiler]/is',
2023-01-21 01:31:34 +01:00
'/\[b](.+?)\[\/b]/i',
'/\[i](.+?)\[\/i]/i',
'/\[s](.+?)\[\/s]/i',
2023-06-29 22:15:03 +02:00
'/\[quote](.+?)\[\/quote]/is',
'/\[ins](.+?)\[\/ins]/is',
'/\[del](.+?)\[\/del]/is',
2023-01-21 01:31:34 +01:00
'/\[li](.+?)\[\/li]/i',
"/\[link.*]\[img]\s*($img_search)\s*\[\/img]\[\/link]/i",
"/\[img]\s*($img_search)\s*\[\/img]/i",
"/\[link\]\s*($url_search)\s*\[\/link\]/i",
"/\[link=\s*[\"']?\s*($url_search)\s*[\"']?\s*\](.+?)\[\/link\]/i",
'/\[anchor]\s*([^\"\'|;<>\[\]]+?)\s*\[\/anchor]/i'
2023-01-21 01:31:34 +01:00
);
// HTML replacements
$replacements = array(
'<blockquote class="spoiler">$1</blockquote>',
'<span class="spoiler">$1</span>',
2023-06-29 22:15:03 +02:00
'<div class="spoiler">$1</div>',
2023-01-21 01:31:34 +01:00
'<strong>$1</strong>',
'<em>$1</em>',
'<strike>$1</strike>',
'<blockquote>$1</blockquote>',
'<ins>$1</ins>',
'<del>$1</del>',
'<div class="comment-list-item">$1</div>',
'<span class="comment-image-consent-wrapper"><button type="button" class="button _secondary consent-button" title="$1">' . _x( '<i class="fa-solid fa-image"></i> Show Image', 'Comment image consent wrapper button.', 'fictioneer' ) . '</button><a href="$1" class="comment-image-link" rel="noreferrer noopener nofollow" target="_blank"><img class="comment-image" data-src="$1"></a></span>',
'<span class="comment-image-consent-wrapper"><button type="button" class="button _secondary consent-button" title="$1">' . _x( '<i class="fa-solid fa-image"></i> Show Image', 'Comment image consent wrapper button.', 'fictioneer' ) . '</button><img class="comment-image" data-src="$1"></span>',
2023-01-21 01:31:34 +01:00
"<a href=\"$1\" rel=\"noreferrer noopener nofollow\">$1</a>",
"<a href=\"$1\" rel=\"noreferrer noopener nofollow\">$5</a>",
'<a href="#$1" rel="noreferrer noopener nofollow" data-block="start" class="comment-anchor">:anchor:</a>'
2023-01-21 01:31:34 +01:00
);
// Pattern replace
$content = preg_replace( $patterns, $replacements, $content );
// Icons
$content = str_replace( ':anchor:', fcntr( 'comment_anchor' ), $content );
return $content;
}
}
// =============================================================================
// GET TAXONOMY NAMES
// =============================================================================
if ( ! function_exists( 'fictioneer_get_taxonomy_names' ) ) {
/**
* Get all taxonomies of a post
*
2023-02-15 01:45:14 +01:00
* @since 5.0.20
*
* @param int $post_id ID of the post to get the taxonomies of.
* @param boolean $flatten Whether to flatten the result. Default false.
*
* @return array Array with all taxonomies.
*/
function fictioneer_get_taxonomy_names( $post_id, $flatten = false ) {
// Setup
$t = [];
$t['tags'] = get_the_tags( $post_id );
$t['fandoms'] = get_the_terms( $post_id, 'fcn_fandom' );
$t['characters'] = get_the_terms( $post_id, 'fcn_character' );
$t['warnings'] = get_the_terms( $post_id, 'fcn_content_warning' );
$t['genres'] = get_the_terms( $post_id, 'fcn_genre' );
// Validate
2023-02-15 01:44:58 +01:00
foreach ( $t as $key => $tax ) {
$t[ $key ] = is_array( $t[ $key ] ) ? $t[ $key ] : [];
}
// Extract
2023-02-15 01:44:58 +01:00
foreach ( $t as $key => $tax ) {
$t[ $key ] = array_map( function( $a ) { return $a->name; }, $t[ $key ] );
}
// Return flattened
if ( $flatten ) {
return array_merge( $t['tags'], $t['fandoms'], $t['characters'], $t['warnings'], $t['genres'] );
}
// Return without empty arrays
return array_filter( $t, 'count' );
}
}
2023-02-23 17:03:41 +01:00
// =============================================================================
// GET FONTS
// =============================================================================
if ( ! function_exists( 'fictioneer_get_fonts' ) ) {
/**
* Returns array of font items
*
2024-04-10 12:05:41 +02:00
* Note: The css string can contain quotes in case of multiple words,
* such as "Roboto Mono".
*
2023-02-23 17:03:41 +01:00
* @since 5.1.1
2024-02-07 16:40:14 +01:00
* @since 5.10.0 - Refactor for font manager.
2024-04-10 12:05:41 +02:00
* @since 5.12.5 - Add theme mod for chapter body font.
2023-02-23 17:03:41 +01:00
*
* @return array Font items (css, name, and alt).
*/
function fictioneer_get_fonts() {
2024-02-07 16:40:14 +01:00
// Make sure fonts are set up!
if (
! get_option( 'fictioneer_chapter_fonts' ) ||
! is_array( get_option( 'fictioneer_chapter_fonts' ) )
) {
fictioneer_build_bundled_fonts();
2023-08-25 21:56:36 +02:00
}
2024-02-07 16:40:14 +01:00
// Setup
$custom_fonts = get_option( 'fictioneer_chapter_fonts' );
2024-04-10 12:05:41 +02:00
$primary_chapter_font = get_theme_mod( 'chapter_chapter_body_font_family_value', 'default' );
2024-02-07 16:40:14 +01:00
$fonts = array(
2024-02-07 22:14:08 +01:00
array( 'css' => fictioneer_font_family_value( FICTIONEER_PRIMARY_FONT_CSS ), 'name' => FICTIONEER_PRIMARY_FONT_NAME ),
2024-02-07 16:40:14 +01:00
array( 'css' => '', 'name' => _x( 'System Font', 'Font name.', 'fictioneer' ) )
2023-02-23 17:03:41 +01:00
);
2024-02-07 16:40:14 +01:00
// Build final font array
foreach ( $custom_fonts as $custom_font ) {
if (
! in_array( $custom_font, $fonts ) &&
$custom_font['name'] !== FICTIONEER_PRIMARY_FONT_NAME &&
$custom_font['css'] !== FICTIONEER_PRIMARY_FONT_CSS
) {
2024-04-10 12:05:41 +02:00
if (
$primary_chapter_font !== 'default' &&
2024-04-13 04:20:22 +02:00
strpos( $custom_font['css'], $primary_chapter_font ) !== false
2024-04-10 12:05:41 +02:00
) {
array_unshift( $fonts, $custom_font );
} else {
$fonts[] = $custom_font;
}
2024-02-07 16:40:14 +01:00
}
}
2023-02-23 17:03:41 +01:00
// Apply filters and return
return apply_filters( 'fictioneer_filter_fonts', $fonts );
}
}
2024-02-07 22:14:08 +01:00
// =============================================================================
// WRAP MULTI-WORD FONTS INTO QUOTES
// =============================================================================
/**
* Returns font family value with quotes if required
*
* @since 5.10.0
*
* @param string $font_value The font family value.
* @param string $quote Optional. The wrapping character. Default '"'.
*
* @return string Ready to use font family value.
*/
function fictioneer_font_family_value( $font_value, $quote = '"' ) {
if ( preg_match( '/\s/', $font_value ) ) {
return $quote . $font_value . $quote;
} else {
return $font_value;
}
}
2023-02-23 18:41:14 +01:00
// =============================================================================
// GET FONT COLORS
// =============================================================================
if ( ! function_exists( 'fictioneer_get_font_colors' ) ) {
/**
* Returns array of font color items
*
* @since 5.1.1
*
* @return array Font items (css and name).
*/
function fictioneer_get_font_colors() {
// Setup default font colors
$colors = array(
array( 'css' => 'var(--fg-tinted)', 'name' => _x( 'Tinted', 'Chapter font color name.', 'fictioneer' ) ),
array( 'css' => 'var(--fg-500)', 'name' => _x( 'Baseline', 'Chapter font color name.', 'fictioneer' ) ),
array( 'css' => 'var(--fg-600)', 'name' => _x( 'Low', 'Chapter font color name.', 'fictioneer' ) ),
array( 'css' => 'var(--fg-700)', 'name' => _x( 'Lower', 'Chapter font color name.', 'fictioneer' ) ),
array( 'css' => 'var(--fg-800)', 'name' => _x( 'Lowest', 'Chapter font color name.', 'fictioneer' ) ),
array( 'css' => 'var(--fg-400)', 'name' => _x( 'High', 'Chapter font color name.', 'fictioneer' ) ),
array( 'css' => 'var(--fg-300)', 'name' => _x( 'Higher', 'Chapter font color name.', 'fictioneer' ) ),
array( 'css' => 'var(--fg-200)', 'name' => _x( 'Highest', 'Chapter font color name.', 'fictioneer' ) ),
array( 'css' => '#fff', 'name' => _x( 'White', 'Chapter font color name.', 'fictioneer' ) ),
array( 'css' => '#999', 'name' => _x( 'Gray', 'Chapter font color name.', 'fictioneer' ) ),
array( 'css' => '#000', 'name' => _x( 'Black', 'Chapter font color name.', 'fictioneer' ) )
2023-02-23 18:41:14 +01:00
);
// Apply filters and return
return apply_filters( 'fictioneer_filter_font_colors', $colors );
}
}
// =============================================================================
// ARRAY FROM COMMA SEPARATED STRING
// =============================================================================
/**
* Explodes string into an array
*
* Strips lines breaks, trims whitespaces, and removes empty elements.
2023-08-13 13:22:22 +02:00
* Values might not be unique.
*
* @since 5.1.3
*
* @param string $string The string to explode.
*
* @return array The string content as array.
*/
function fictioneer_explode_list( $string ) {
2023-08-13 13:22:22 +02:00
if ( empty( $string ) ) {
return [];
}
2023-03-10 14:40:30 +01:00
2023-08-13 13:22:22 +02:00
$string = str_replace( ["\n", "\r"], '', $string ); // Remove line breaks
$array = explode( ',', $string );
$array = array_map( 'trim', $array ); // Remove extra whitespaces
$array = array_filter( $array, 'strlen' ); // Remove empty elements
$array = is_array( $array ) ? $array : [];
return $array;
}
2023-05-31 15:43:42 +02:00
// =============================================================================
2024-05-17 13:52:08 +02:00
// BUILD FRONTEND NOTICE
2023-05-31 15:43:42 +02:00
// =============================================================================
if ( ! function_exists( 'fictioneer_notice' ) ) {
/**
* Render or return a frontend notice element
2023-05-31 15:43:42 +02:00
*
* @since 5.2.5
*
* @param string $message The notice to show.
* @param string $type Optional. The notice type. Default 'warning'.
* @param bool $display Optional. Whether to render or return. Default true.
2023-05-31 15:43:42 +02:00
*
* @return void|string The build HTML or nothing if rendered.
*/
function fictioneer_notice( $message, $type = 'warning', $display = true ) {
$output = '<div class="notice _' . esc_attr( $type ) . '">';
if ( $type === 'warning' ) {
$output .= '<i class="fa-solid fa-triangle-exclamation"></i>';
}
$output .= "<div>{$message}</div></div>";
2023-05-31 15:43:42 +02:00
if ( $display ) {
echo $output;
} else {
return $output;
}
}
}
// =============================================================================
// MINIFY HTML
// =============================================================================
if ( ! function_exists( 'fictioneer_minify_html' ) ) {
/**
* Minifies a HTML string
*
* This is not safe for `<pre>` or `<code>` tags!
*
* @since 5.4.0
*
* @param string $html The HTML string to be minified.
*
* @return string The minified HTML string.
*/
function fictioneer_minify_html( $html ) {
return preg_replace( '/\s+/', ' ', trim( $html ) );
}
}
2024-10-07 10:59:58 +02:00
// =============================================================================
// MINIFY CSS
// =============================================================================
if ( ! function_exists( 'fictioneer_minify_css' ) ) {
/**
* Minify CSS.
*
* @license CC BY-SA 4.0
* @author Qtax https://stackoverflow.com/users/107152/qtax
* @author lots0logs https://stackoverflow.com/users/2639936/lots0logs
*
* @since 4.7.0
* @link https://stackoverflow.com/a/15195752/17140970
* @link https://stackoverflow.com/a/44350195/17140970
*
* @param string $string The to be minified CSS string.
*
* @return string The minified CSS string.
*/
function fictioneer_minify_css( $string = '' ) {
$comments = <<<'EOS'
(?sx)
# don't change anything inside of quotes
( "(?:[^"\\]++|\\.)*+" | '(?:[^'\\]++|\\.)*+' )
|
# comments
/\* (?> .*? \*/ )
EOS;
$everything_else = <<<'EOS'
(?six)
# don't change anything inside of quotes
( "(?:[^"\\]++|\\.)*+" | '(?:[^'\\]++|\\.)*+' )
|
# spaces before and after ; and }
\s*+ ; \s*+ ( } ) \s*+
|
# all spaces around meta chars/operators (excluding + and -)
\s*+ ( [*$~^|]?+= | [{};,>~] | !important\b ) \s*+
|
# all spaces around + and - (in selectors only!)
\s*([+-])\s*(?=[^}]*{)
|
# spaces right of ( [ :
( [[(:] ) \s++
|
# spaces left of ) ]
\s++ ( [])] )
|
# spaces left (and right) of : (but not in selectors)!
\s+(:)(?![^\}]*\{)
|
# spaces at beginning/end of string
^ \s++ | \s++ \z
|
# double spaces to single
(\s)\s+
EOS;
$search_patterns = array( "%{$comments}%", "%{$everything_else}%" );
$replace_patterns = array( '$1', '$1$2$3$4$5$6$7$8' );
return preg_replace( $search_patterns, $replace_patterns, $string );
}
}
// =============================================================================
// GET CLEAN CURRENT URL
// =============================================================================
if ( ! function_exists( 'fictioneer_get_clean_url' ) ) {
/**
* Returns URL without query arguments or page number
*
* @since 5.4.0
*
* @return string The clean URL.
*/
function fictioneer_get_clean_url() {
global $wp;
// Setup
$url = home_url( $wp->request );
// Remove page (if any)
$url = preg_replace( '/\/page\/\d+\/$/', '', $url );
$url = preg_replace( '/\/page\/\d+$/', '', $url );
// Return cleaned URL
return $url;
}
}
2023-07-28 12:05:22 +02:00
// =============================================================================
// GET AUTHOR IDS OF POST
// =============================================================================
if ( ! function_exists( 'fictioneer_get_post_author_ids' ) ) {
/**
* Returns array of author IDs for a post ID
*
* @since 5.4.8
*
* @param int $post_id The post ID.
*
* @return array The author IDs.
*/
function fictioneer_get_post_author_ids( $post_id ) {
$author_ids = get_post_meta( $post_id, 'fictioneer_story_co_authors', true ) ?: [];
$author_ids = is_array( $author_ids ) ? $author_ids : [];
array_unshift( $author_ids, get_post_field( 'post_author', $post_id ) );
2023-07-28 12:05:22 +02:00
return array_unique( $author_ids );
}
}
// =============================================================================
// DELETE TRANSIENTS THAT INCLUDE A STRING
// =============================================================================
if ( ! function_exists( 'fictioneer_delete_transients_like' ) ) {
/**
* Delete Transients with a like key and return the count deleted
*
* Note: The fast variant of this function is not compatible with
* external object caches such as Memcached, because without the
* help of delete_transient(), it won't know about the change.
*
* @since 5.4.9
*
* @param string $partial_key String that is part of the key.
* @param boolean $fast Optional. Whether to delete with a single SQL query or
* loop each Transient with delete_transient(), which can
* trigger hooked actions (if any). Default false.
*
* @return int Count of deleted Transients.
*/
function fictioneer_delete_transients_like( $partial_key, $fast = false ) {
// Globals
global $wpdb;
// Setup
$count = 0;
// Fast?
if ( $fast ) {
// Prepare SQL
$sql = $wpdb->prepare(
"DELETE FROM $wpdb->options WHERE `option_name` LIKE %s OR `option_name` LIKE %s",
"%_transient%{$partial_key}%",
"%_transient_timeout%{$partial_key}%"
);
// Query
$count = $wpdb->query( $sql );
} else {
// Prepare SQL
$sql = $wpdb->prepare(
"SELECT `option_name` AS `name` FROM $wpdb->options WHERE `option_name` LIKE %s",
"_transient%{$partial_key}%"
);
// Query
$transients = $wpdb->get_col( $sql );
// Build full keys and delete
foreach ( $transients as $transient ) {
$key = str_replace( '_transient_', '', $transient );
$count += delete_transient( $key ) ? 1 : 0;
}
}
// Return count
return $count;
}
}
// =============================================================================
// GET OPTION PAGE LINK
// =============================================================================
if ( ! function_exists( 'fictioneer_get_assigned_page_link' ) ) {
/**
* Returns permalink for an assigned page or null
*
* @since 5.4.9
*
* @param string $option The option name of the page assignment.
*
* @return string|null The permalink or null.
*/
function fictioneer_get_assigned_page_link( $option ) {
// Setup
$page_id = get_option( $option );
// Null if no page has been selected (null or -1)
if ( empty( $page_id ) || $page_id < 0 ) {
return null;
}
// Get permalink from options or post
$link = get_option( "{$option}_link" );
if ( empty( $link ) ) {
$link = get_permalink( $page_id );
update_option( "{$option}_link", $link, true ); // Save for next time
}
// Return
return $link;
}
}
2023-08-05 12:12:07 +02:00
// =============================================================================
// MULTI SAVE GUARD
// =============================================================================
if ( ! function_exists( 'fictioneer_multi_save_guard' ) ) {
/**
* Prevents multi-fire in update hooks
*
2023-08-18 10:17:18 +02:00
* Unfortunately, the block editor always fires twice: once as REST request and
* followed by WP_POST. Only the first will have the correct parameters such as
* $update set, the second is technically no longer an update. Since blocking
* the follow-up WP_POST would block programmatically triggered actions, there
* is no other choice but to block the REST request and live with it.
*
2023-08-05 12:12:07 +02:00
* @since 5.5.2
*
* @param int $post_id The ID of the updated post.
*
2023-08-05 13:27:00 +02:00
* @return boolean True if NOT allowed, false otherwise.
2023-08-05 12:12:07 +02:00
*/
function fictioneer_multi_save_guard( $post_id ) {
// Always allow trash action to pass
if ( get_post_status( $post_id ) === 'trash' ) {
return false;
}
// Block REST requests and unnecessary triggers
2023-08-05 12:12:07 +02:00
if (
( defined( 'REST_REQUEST' ) && REST_REQUEST && ! get_option( 'fictioneer_allow_rest_save_actions' ) ) ||
2023-08-05 12:12:07 +02:00
wp_is_post_autosave( $post_id ) ||
wp_is_post_revision( $post_id ) ||
get_post_status( $post_id ) === 'auto-draft'
2023-08-05 12:12:07 +02:00
) {
return true;
}
// Pass
2023-08-05 12:12:07 +02:00
return false;
}
}
// =============================================================================
// GET TOTAL WORD COUNT FOR ALL STORIES
// =============================================================================
if ( ! function_exists( 'fictioneer_get_stories_total_word_count' ) ) {
/**
* Returns the total word count of all published stories
*
* Note: Does not include standalone chapters for performance reasons.
*
* @since 4.0.0
* @since 5.22.3 - Refactored with SQL query for better performance.
*
* @return int The word count of all published stories.
*/
function fictioneer_get_stories_total_word_count() {
// Look for cached value (purged after each update, should never be stale)
$transient_word_count_cache = get_transient( 'fictioneer_stories_total_word_count' );
// Return cached value if found
if ( $transient_word_count_cache ) {
return $transient_word_count_cache;
2023-08-19 13:33:25 +02:00
}
global $wpdb;
// Setup
$words = 0;
// Sum of all word counts
$words = $wpdb->get_var(
$wpdb->prepare(
"
SELECT SUM(CAST(pm.meta_value AS UNSIGNED))
FROM $wpdb->postmeta AS pm
INNER JOIN $wpdb->posts AS p ON pm.post_id = p.ID
WHERE pm.meta_key = %s
AND p.post_type = %s
AND p.post_status = %s
",
'fictioneer_story_total_word_count',
'fcn_story',
'publish'
)
);
// Customize
$words = fictioneer_multiply_word_count( $words );
2023-11-30 17:12:48 +01:00
// Cache for next time
set_transient( 'fictioneer_stories_total_word_count', $words, DAY_IN_SECONDS );
// Return newly calculated value
return $words;
}
}
// =============================================================================
// REDIRECT TO 404
// =============================================================================
if ( ! function_exists( 'fictioneer_redirect_to_404' ) ) {
/**
* Redirects the current request to the WordPress 404 page
*
* @since 5.6.0
2023-08-20 16:14:19 +02:00
*
* @global WP_Query $wp_query The main WP_Query instance.
*/
function fictioneer_redirect_to_404() {
global $wp_query;
// Remove scripts to avoid errors
add_action( 'wp_print_scripts', function() {
wp_dequeue_script( 'fictioneer-chapter-scripts' );
wp_dequeue_script( 'fictioneer-suggestion-scripts' );
wp_dequeue_script( 'fictioneer-tts-scripts' );
wp_dequeue_script( 'fictioneer-story-scripts' );
}, 99 );
// Set query to 404
$wp_query->set_404();
status_header( 404 );
nocache_headers();
get_template_part( 404 );
// Terminate
exit();
}
}
// =============================================================================
// PREVIEW ACCESS VERIFICATION
// =============================================================================
if ( ! function_exists( 'fictioneer_verify_unpublish_access' ) ) {
/**
* Verifies access to unpublished posts (not drafts)
*
* @since 5.6.0
2023-08-20 16:14:19 +02:00
*
* @return boolean True if access granted, false otherwise.
*/
function fictioneer_verify_unpublish_access( $post_id ) {
// Setup
$post = get_post( $post_id );
$authorized = false;
// Always let owner to pass
if ( get_current_user_id() === absint( $post->post_author ) ) {
$authorized = true;
}
// Always let administrators pass
if ( current_user_can( 'manage_options' ) ) {
$authorized = true;
}
// Check capability for post type
if ( $post->post_status === 'private' ) {
switch ( $post->post_type ) {
case 'post':
$authorized = current_user_can( 'edit_private_posts' );
break;
case 'page':
$authorized = current_user_can( 'edit_private_pages' );
break;
case 'fcn_chapter':
$authorized = current_user_can( 'read_private_fcn_chapters' );
break;
case 'fcn_story':
$authorized = current_user_can( 'read_private_fcn_stories' );
break;
case 'fcn_recommendation':
$authorized = current_user_can( 'read_private_fcn_recommendations' );
break;
case 'fcn_collection':
$authorized = current_user_can( 'read_private_fcn_collections' );
break;
default:
$authorized = current_user_can( 'edit_others_posts' );
}
}
// Drafts are handled by WordPress
return apply_filters( 'fictioneer_filter_verify_unpublish_access', $authorized, $post_id, $post );
2023-08-17 18:55:36 +02:00
}
}
// =============================================================================
// ADD ACTION TO SAVE/TRASH/UNTRASH/DELETE HOOKS WITH POST ID
// =============================================================================
/**
* Adds callback to save, trash, untrash, and delete hooks (1 argument)
*
* This helper saves some time/space adding a callback action to all four
* default post operations. But only with the first argument: post_id.
*
* @since 5.6.3
*
* @param callable $function The callback function to be added.
* @param int $priority Optional. Used to specify the order in which the
* functions associated with a particular action are
* executed. Default 10. Lower numbers correspond with
* earlier execution, and functions with the same
* priority are executed in the order in which they
* were added to the action.
*
* @return true Will always return true.
*/
function fictioneer_add_stud_post_actions( $function, $priority = 10 ) {
$hooks = ['save_post', 'trashed_post', 'delete_post', 'untrash_post'];
foreach ( $hooks as $hook ) {
add_action( $hook, $function, $priority );
}
return true;
}
2023-10-02 00:02:12 +02:00
// =============================================================================
// CONVERT URL LIST TO ARRAY
// =============================================================================
/**
* Turn line-break separated list into array of links
*
* @since 5.7.4
2023-10-02 00:02:12 +02:00
*
* @param string $list The list of links
*
* @return array The array of links.
*/
function fictioneer_url_list_to_array( $list ) {
// Already array?
if ( is_array( $list ) ) {
return $list;
}
// Catch falsy values
if ( empty( $list ) ) {
return [];
}
// Prepare URLs
$urls = [];
$lines = explode( "\n", $list );
// Extract
foreach ( $lines as $line ) {
$tuple = explode( '|', $line );
$tuple = array_map( 'trim', $tuple );
$urls[] = array(
'name' => wp_strip_all_tags( $tuple[0] ),
2024-06-10 10:32:23 +02:00
'url' => fictioneer_sanitize_url( $tuple[1] )
2023-10-02 00:02:12 +02:00
);
}
// Return
return $urls;
}
// =============================================================================
2023-11-08 19:59:14 +01:00
// ARRAY OPERATIONS
// =============================================================================
/**
* Unset array element by value
*
* @since 5.7.5
*
* @param mixed $value The value to look for.
* @param array $array The array to be modified.
*
* @return array The modified array.
*/
function fictioneer_unset_by_value( $value, $array ) {
if ( ( $key = array_search( $value, $array ) ) !== false ) {
unset( $array[ $key ] );
}
return $array;
}
2023-11-08 19:59:14 +01:00
// =============================================================================
// RETURN NO FORMAT STRING
// =============================================================================
/**
* Returns an unformatted replacement string
2023-11-08 19:59:14 +01:00
*
* @since 5.7.5
2023-11-08 19:59:14 +01:00
*
* @return string Just a simple '%s'.
*/
function fictioneer__return_no_format() {
return '%s';
}
// =============================================================================
// TRUNCATE STRING
// =============================================================================
/**
* Returns a truncated string without tags
*
* @since 5.9.0
*
* @param string $string The string to truncate.
* @param int $length Maximum length in characters.
* @param string|null $ellipsis Optional. Truncation indicator suffix.
*
* @return string The truncated string without tags.
*/
function fictioneer_truncate( string $string, int $length, string $ellipsis = null ) {
// Setup
$string = wp_strip_all_tags( $string ); // Prevent tags from being cut off
$ellipsis = $ellipsis ?? FICTIONEER_TRUNCATION_ELLIPSIS;
// Return truncated string
2024-08-21 21:06:14 +02:00
if ( function_exists( 'mb_strimwidth' ) ) {
return mb_strimwidth( $string, 0, $length, $ellipsis );
} else {
return strlen( $string ) > $length ? substr( $string, 0, $length ) . $ellipsis : $string;
}
}
// =============================================================================
2024-03-06 22:06:09 +01:00
// COMPARE WORDPRESS VERSION
// =============================================================================
/**
* Compare installed WordPress version against version string
*
* @since 5.12.2
* @global wpdb $wp_version Current WordPress version string.
*
* @param string $version The version string to test against.
* @param string $operator Optional. How to compare. Default '>='.
*
* @return boolean True or false.
*/
function fictioneer_compare_wp_version( $version, $operator = '>=' ) {
global $wp_version;
return version_compare( $wp_version, $version, $operator );
}
// =============================================================================
// CSS LOADING PATTERN
// =============================================================================
if ( ! function_exists( 'fictioneer_get_async_css_loading_pattern' ) ) {
/**
* Returns the media attribute and loading strategy for stylesheets
*
* @since 5.12.2
*
* @return string Media attribute script for stylesheet links.
*/
function fictioneer_get_async_css_loading_pattern() {
if ( FICTIONEER_ENABLE_ASYNC_ONLOAD_PATTERN ) {
return 'media="print" onload="this.media=\'all\'; this.onload=null;"';
}
return 'media="all"';
}
}
// =============================================================================
// GENERATE PLACEHOLDER
// =============================================================================
if ( ! function_exists( 'fictioneer_generate_placeholder' ) ) {
/**
* Dummy implementation (currently used a CSS background)
*
* @since 5.14.0
*
* @param array|null $args Optional arguments to generate a placeholder.
*
* @return string The placeholder URL or data URI.
*/
function fictioneer_generate_placeholder( $args = [] ) {
return '';
}
}
// =============================================================================
// FIND USER(S)
// =============================================================================
/**
* Find (first) user by display name
*
* @since 5.14.1
*
* @param string $display_name The display name to search for.
*
* @return WP_User|null The first matching user or null if not found.
*/
function fictioneer_find_user_by_display_name( $display_name ) {
// No choice but to query all because the display name is not unique
$users = get_users(
array(
'search' => sanitize_user( $display_name ),
'search_columns' => ['display_name'],
'number' => 1
)
);
// If found, return first
if ( ! empty( $users ) ) {
return $users[0];
}
// Not found
return null;
}
// =============================================================================
// JOIN ARRAYS IN SPECIAL WAYS
// =============================================================================
if ( ! function_exists( 'fictioneer_get_human_readable_list' ) ) {
/**
* Join string in an array as human readable list.
*
* @since 5.15.0
* @link https://gist.github.com/SleeplessByte/4514697
*
* @param array $array Array of strings.
*
* @return string The human readable list.
*/
function fictioneer_get_human_readable_list( $array ) {
// Setup
$comma = _x( ', ', 'Human readable list joining three or more items except the last two.', 'fictioneer' );
$double = _x( ' or ', 'Human readable list joining two items.', 'fictioneer' );
$final = _x( ', or ', 'Human readable list joining the last two of three or more items.', 'fictioneer' );
// One or two items
if ( count( $array ) < 3 ) {
return implode( $double, $array );
}
// Three or more items
array_splice( $array, -2, 2, implode( $final, array_slice( $array, -2, 2 ) ) );
// Finish
return implode( $comma , $array );
}
}
// =============================================================================
// PATREON UTILITIES
// =============================================================================
/**
* Get Patreon data for a post
*
* Note: Considers both the post meta and global settings.
*
* @since 5.15.0
*
* @param WP_Post|null $post The post to check. Default to global $post.
*
2024-04-29 22:39:33 +02:00
* @return array|null Array with 'gated' (bool), 'gate_tiers' (array), 'gate_cents' (int),
* and 'gate_lifetime_cents' (int). Null if the post could not be found.
*/
function fictioneer_get_post_patreon_data( $post = null ) {
// Static cache
static $cache = [];
// Post?
$post = $post ?? get_post();
2024-05-17 17:46:54 +02:00
$post_id = $post->ID;
if ( ! $post ) {
return null;
}
// Check cache
2024-05-17 17:46:54 +02:00
if ( isset( $cache[ $post_id ] ) ) {
return $cache[ $post_id ];
}
// Setup
2024-04-28 14:16:24 +02:00
$global_tiers = get_option( 'fictioneer_patreon_global_lock_tiers', [] ) ?: [];
$global_amount_cents = get_option( 'fictioneer_patreon_global_lock_amount', 0 ) ?: 0;
$global_lifetime_amount_cents = get_option( 'fictioneer_patreon_global_lock_lifetime_amount', 0 ) ?: 0;
2024-05-17 17:46:54 +02:00
$post_tiers = get_post_meta( $post_id, 'fictioneer_patreon_lock_tiers', true );
2024-04-28 14:16:24 +02:00
$post_tiers = is_array( $post_tiers ) ? $post_tiers : [];
2024-05-17 17:46:54 +02:00
$post_amount_cents = absint( get_post_meta( $post_id, 'fictioneer_patreon_lock_amount', true ) );
2024-04-28 14:16:24 +02:00
$check_tiers = array_merge( $global_tiers, $post_tiers );
$check_tiers = array_unique( $check_tiers );
2024-04-28 14:16:24 +02:00
$check_amount_cents = $post_amount_cents > 0 ? $post_amount_cents : $global_amount_cents;
$check_amount_cents = $post_amount_cents > 0 && $global_amount_cents > 0
? min( $post_amount_cents, $global_amount_cents ) : $check_amount_cents;
$check_amount_cents = max( $check_amount_cents, 0 );
// Compile
$data = array(
2024-04-28 14:16:24 +02:00
'gated' => $check_tiers || $check_amount_cents > 0,
'gate_tiers' => $check_tiers,
'gate_cents' => $check_amount_cents,
'gate_lifetime_cents' => $global_lifetime_amount_cents
);
// Cache
2024-05-17 17:46:54 +02:00
$cache[ $post_id ] = $data;
// Return
return $data;
}
2024-05-21 00:00:37 +02:00
// =============================================================================
// ENCRYPTION/DECRYPTION
// =============================================================================
/**
* Encrypt data
*
* @since 5.19.0
*
* @param mixed $data The data to encrypt.
*
* @return string|false The encrypted data or false on failure.
*/
function fictioneer_encrypt( $data ) {
$key = wp_salt();
$encrypted = openssl_encrypt(
json_encode( $data ),
'aes-256-cbc',
$key,
0,
substr( hash( 'sha256', $key ), 0, 16 )
);
if ( $encrypted === false ) {
return false;
}
return base64_encode( $encrypted );
}
/**
* Decrypt data
*
* @since 5.19.0
*
* @param string $data The data to decrypt.
*
* @return mixed The decrypted data.
*/
function fictioneer_decrypt( $data ) {
$key = wp_salt();
$decrypted = openssl_decrypt(
base64_decode( $data ),
'aes-256-cbc',
$key,
0,
substr( hash( 'sha256', $key ), 0, 16 )
);
return json_decode( $decrypted, true );
}
// =============================================================================
// RANDOM USERNAME
// =============================================================================
/**
* Returns array of adjectives for randomized username generation
*
* @since 5.19.0
*
* @return array Array of nouns.
*/
function fictioneer_get_username_adjectives() {
$adjectives = array(
'Radical', 'Tubular', 'Gnarly', 'Epic', 'Electric', 'Neon', 'Bodacious', 'Rad',
'Totally', 'Funky', 'Wicked', 'Fresh', 'Chill', 'Groovy', 'Vibrant', 'Flashy',
'Buff', 'Hella', 'Motor', 'Cyber', 'Pixel', 'Holo', 'Stealth', 'Synthetic',
'Enhanced', 'Synth', 'Bio', 'Laser', 'Virtual', 'Analog', 'Mega', 'Wave', 'Solo',
'Retro', 'Quantum', 'Robotic', 'Digital', 'Hyper', 'Punk', 'Giga', 'Electro',
'Chrome', 'Fusion', 'Vivid', 'Stellar', 'Galactic', 'Turbo', 'Atomic', 'Cosmic',
'Artificial', 'Kinetic', 'Binary', 'Hypersonic', 'Runic', 'Data', 'Knightly',
'Cryonic', 'Nebular', 'Golden', 'Silver', 'Red', 'Crimson', 'Augmented', 'Vorpal',
'Ascended', 'Serious', 'Solid', 'Master', 'Prism', 'Spinning', 'Masked', 'Hardcore',
'Somber', 'Celestial', 'Arcane', 'Luminous', 'Ionized', 'Lunar', 'Uncanny', 'Subatomic',
'Luminary', 'Radiant', 'Ultra', 'Starship', 'Space', 'Starlight', 'Interstellar', 'Metal',
2024-05-21 17:23:21 +02:00
'Bionic', 'Machine', 'Isekai', 'Warp', 'Neo', 'Alpha', 'Power', 'Unhinged', 'Ash',
'Savage', 'Silent', 'Screaming', 'Misty', 'Rending', 'Horny', 'Dreadful', 'Bizarre',
'Chaotic', 'Wacky', 'Twisted', 'Manic', 'Crystal', 'Infernal', 'Ruthless', 'Grim',
'Mortal', 'Forsaken', 'Heretical', 'Cursed', 'Blighted', 'Scarlet', 'Delightful',
'Nuclear', 'Azure', 'Emerald', 'Amber', 'Mystic', 'Ethereal', 'Enchanted', 'Valiant',
'Fierce', 'Obscure', 'Enigmatic'
);
return apply_filters( 'fictioneer_random_username_adjectives', $adjectives );
}
/**
* Returns array of nouns for randomized username generation
*
* @since 5.19.0
*
* @return array Array of nouns.
*/
function fictioneer_get_username_nouns() {
$nouns = array(
'Avatar', 'Cassette', 'Rubiks', 'Gizmo', 'Synthwave', 'Tron', 'Replicant', 'Warrior',
'Hacker', 'Samurai', 'Cyborg', 'Runner', 'Mercenary', 'Shogun', 'Maverick', 'Glitch',
'Byte', 'Matrix', 'Motion', 'Shinobi', 'Circuit', 'Droid', 'Virus', 'Vortex', 'Mech',
'Codex', 'Hologram', 'Specter', 'Intelligence', 'Technomancer', 'Rider', 'Ghost',
'Hunter', 'Hound', 'Wizard', 'Knight', 'Rogue', 'Scout', 'Ranger', 'Paladin', 'Sorcerer',
'Mage', 'Artificer', 'Cleric', 'Tank', 'Fighter', 'Pilot', 'Necromancer', 'Neuromancer',
'Barbarian', 'Streetpunk', 'Phantom', 'Shaman', 'Druid', 'Dragon', 'Dancer', 'Captain',
'Pirate', 'Snake', 'Rebel', 'Kraken', 'Spark', 'Blitz', 'Alchemist', 'Dragoon', 'Geomancer',
'Neophyte', 'Terminator', 'Tempest', 'Enigma', 'Automaton', 'Daemon', 'Juggernaut',
'Paragon', 'Sentinel', 'Viper', 'Velociraptor', 'Spirit', 'Punk', 'Synth', 'Biomech',
'Engineer', 'Pentagoose', 'Vampire', 'Soldier', 'Chimera', 'Lobotomy', 'Mutant',
2024-05-21 17:23:21 +02:00
'Revenant', 'Wraith', 'Chupacabra', 'Banshee', 'Fae', 'Leviathan', 'Cenobite', 'Bob',
'Ketchum', 'Collector', 'Student', 'Lover', 'Chicken', 'Alien', 'Titan', 'Sinner',
'Nightmare', 'Bioplague', 'Annihilation', 'Elder', 'Priest', 'Guardian', 'Quagmire',
'Berserker', 'Oblivion', 'Decimator', 'Devastation', 'Calamity', 'Doom', 'Ruin', 'Abyss',
'Heretic', 'Armageddon', 'Obliteration', 'Inferno', 'Torment', 'Carnage', 'Purgatory',
'Chastity', 'Angel', 'Raven', 'Star', 'Trinity', 'Idol', 'Eidolon', 'Havoc', 'Nirvana',
'Digitron', 'Phoenix', 'Lantern', 'Warden', 'Falcon'
);
return apply_filters( 'fictioneer_random_username_nouns', $nouns );
}
/**
* Returns randomized username
*
* @since 5.19.0
*
* @param bool $unique Optional. Whether the username must be unique. Default true.
*
* @return string Sanitized random username.
*/
function fictioneer_get_random_username( $unique = true ) {
// Setup
$adjectives = fictioneer_get_username_adjectives();
$nouns = fictioneer_get_username_nouns();
2024-05-21 17:23:21 +02:00
// Shuffle the arrays to ensure more randomness
shuffle( $adjectives );
shuffle( $nouns );
// Build username
do {
$username = $adjectives[ array_rand( $adjectives ) ] . $nouns[ array_rand( $nouns ) ] . rand( 1000, 9999 );
$username = sanitize_user( $username, true );
} while ( username_exists( $username ) && $unique );
// Return username
return $username;
}
2024-07-30 09:30:40 +02:00
// =============================================================================
// STRING LENGTH
// =============================================================================
if ( ! function_exists( 'mb_strlen' ) ) {
/**
* Fallback function for mb_strlen
*
* @param string $string The string to being measured.
* @param string $encoding The character encoding. Default UTF-8.
*
* @return int The number of characters in the string.
*/
function mb_strlen( $string, $encoding = 'UTF-8' ) {
if ( $encoding !== 'UTF-8' ) {
return strlen( $string );
}
$converted_string = iconv( $encoding, 'UTF-16', $string );
if ( $converted_string === false ) {
return strlen( $string );
} else {
return strlen( $converted_string ) / 2; // Each character is 2 bytes in UTF-16
}
}
}
// =============================================================================
// GET ALL PUBLISHING AUTHORS
// =============================================================================
/**
* Returns all authors with published posts
*
* Note: Qualified post types are fcn_story, fcn_chapter, fcn_recommendation,
* and post. The result is cached for 12 hours as Transient.
*
* @since 5.24.0
* @link https://developer.wordpress.org/reference/functions/get_users/
*
* @param array $args Optional. Array of additional query arguments.
*
* @return array Array of WP_User object, stdClass objects, or IDs.
*/
function fictioneer_get_publishing_authors( $args = [] ) {
static $authors = null;
$key = 'fictioneer_publishing_authors_' . md5( serialize( $args ) );
if ( ! $authors && $transient = get_transient( $key ) ) {
$authors = $transient;
}
if ( $authors ) {
return $authors;
}
$authors = get_users(
array_merge(
array( 'has_published_posts' => ['fcn_story', 'fcn_chapter', 'fcn_recommendation', 'post'] ),
$args
)
);
set_transient( $key, $authors, 12 * HOUR_IN_SECONDS );
return $authors;
}
// =============================================================================
2024-10-04 16:28:08 +02:00
// GET POST LABELS
// =============================================================================
/**
* Returns the translated label of the post status
*
* @since 5.24.5
*
* @param string $status Post status.
*
* @return string Translated label of the post status or the post status if custom.
*/
function fictioneer_get_post_status_label( $status ) {
static $labels = null;
if ( ! $labels ) {
$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
);
}
return $labels[ $status ] ?? $status;
}
2024-10-04 16:28:08 +02:00
/**
* Returns the translated label of the post type
*
* @since 5.25.0
*
* @param string $type Post type.
*
* @return string Translated label of the post type or the post type if custom.
*/
function fictioneer_get_post_type_label( $type ) {
static $labels = null;
if ( ! $labels ) {
$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' )
);
}
return $labels[ $type ] ?? $type;
}
2024-10-11 13:44:59 +02:00
2024-10-12 20:23:13 +02:00
// =============================================================================
// LOGS
// =============================================================================
/**
* Returns (or creates) secret log hash used to obscure the log file name
*
* @since 5.24.1
*
* @return string The log hash.
*/
function fictioneer_get_log_hash() {
$hash = strval( get_option( 'fictioneer_log_hash' ) );
if ( ! empty( $hash ) ) {
return $hash;
}
$hash = wp_generate_password( 32, false );
update_option( 'fictioneer_log_hash', $hash, 'no' );
return $hash;
}
/**
* Logs a message to the theme log file
*
* @since 5.0.0
*
* @param string $message What has been updated
* @param WP_User|null $user The user who did it. Defaults to current user.
*/
function fictioneer_log( $message, $current_user = null ) {
// Setup
$current_user = $current_user ?? wp_get_current_user();
$username = _x( 'System', 'Default name in logs.', 'fictioneer' );
$log_hash = fictioneer_get_log_hash();
$log_file = WP_CONTENT_DIR . "/fictioneer-{$log_hash}-log.log";
$log_limit = 5000;
$date = current_time( 'mysql', true );
if ( is_object( $current_user ) && $current_user->ID > 0 ) {
$username = $current_user->user_login . ' #' . $current_user->ID;
}
if ( empty( $current_user ) && wp_doing_cron() ) {
$username = 'WP Cron';
}
if ( empty( $current_user ) && wp_doing_ajax() ) {
$username = 'AJAX';
}
$username = empty( $username ) ? __( 'Anonymous', 'fictioneer' ) : $username;
// Make sure the log file exists
if ( ! file_exists( $log_file ) ) {
file_put_contents( $log_file, '' );
}
// Read
$log_contents = file_get_contents( $log_file );
// Parse
$log_entries = explode( "\n", $log_contents );
// Limit (if too large)
$log_entries = array_slice( $log_entries, -($log_limit + 1) );
// Add new entry
$log_entries[] = "[{$date} UTC] [{$username}] $message";
// Concatenate and save
file_put_contents( $log_file, implode( "\n", $log_entries ) );
// Set file permissions
chmod( $log_file, 0600 );
// Security
$silence = WP_CONTENT_DIR . '/index.php';
if ( ! file_exists( $silence ) ) {
file_put_contents( $silence, "<?php\n// Silence is golden.\n" );
chmod( $silence, 0600 );
}
}
/**
* Retrieves the log entries and returns an HTML representation
*
* @return string The HTML representation of the log entries.
*/
function fictioneer_get_log() {
// Setup
$log_hash = fictioneer_get_log_hash();
$log_file = WP_CONTENT_DIR . "/fictioneer-{$log_hash}-log.log";
$output = '';
// Check whether log file exists
if ( ! file_exists( $log_file ) ) {
return '<ul class="fictioneer-log"><li class="fictioneer-log__item">No log entries yet.</li></ul>';
}
// Read
$log_contents = file_get_contents( $log_file );
// Parse
$log_entries = explode( "\n", $log_contents );
// Limit display to 250
$log_entries = array_slice( $log_entries, -250 );
// Reverse
$log_entries = array_reverse( $log_entries );
// Build list items
foreach ( $log_entries as $entry ) {
$output .= '<li class="fictioneer-log__item">' . esc_html( $entry ) . '</li>';
}
// Return HTML
return '<ul class="fictioneer-log">' . $output . '</ul>';
}
/**
* Retrieves the debug log entries and returns an HTML representation
*
* @return string The HTML representation of the log entries.
*/
function fictioneer_get_wp_debug_log() {
// Setup
$log_file = WP_CONTENT_DIR . '/debug.log';
$output = '';
// Check whether log file exists
if ( ! file_exists( $log_file ) ) {
return '<ul class="fictioneer-log _wp-debug-log"><li class="fictioneer-log__item">No log entries yet.</li></ul>';
}
// Read
$log_contents = file_get_contents( $log_file );
// Parse
$log_entries = explode( "\n", $log_contents );
// Limit display to 250
$log_entries = array_slice( $log_entries, -250 );
// Reverse
$log_entries = array_reverse( $log_entries );
// Build list items
foreach ( $log_entries as $entry ) {
$output .= '<li class="fictioneer-log__item _wp-debug-log">' . esc_html( $entry ) . '</li>';
}
// Return HTML
return '<ul class="fictioneer-log _wp-debug-log">' . $output . '</ul>';
}