container = $container; $this->database = $database; $this->settings = $settings; add_action( 'parse_term_query', array( $this, 'parse_term_query' ) ); // add_action( 'parse_query', [$this, 'parse_query'], 1 ); add_action( 'pre_get_posts', array( $this, 'pre_get_posts' ), 1 ); add_filter( 'betterdocs_base_terms_args', array( $this, 'modify_terms_args_for_private_docs' ), 10, 1 ); /** * These below filters are hooked for navigation only. * * For old version of this portion. * @see `betterdocs_single_post_nav` filter * * For details: * @see https://developer.wordpress.org/reference/functions/get_next_post/ * @see https://developer.wordpress.org/reference/functions/get_previous_post/ * * @link https://developer.wordpress.org/reference/hooks/get_adjacent_post_where/ */ add_filter( 'get_next_post_where', array( $this, 'next_post_where' ), 99, 5 ); add_filter( 'get_previous_post_where', array( $this, 'previous_post_where' ), 99, 5 ); $this->init(); /** * Modify Popular Docs Query (For Shortcode & Widget) */ add_filter( 'posts_clauses', array( $this, 'mod_query_popular_docs' ), 10, 2 ); } public function mod_query_popular_docs( $clauses, $wp_query ) { if ( isset( $wp_query->query[ 'meta_key' ] ) ) { if ( '_betterdocs_meta_views' == $wp_query->query[ 'meta_key' ] ) { global $wpdb; $order = isset( $wp_query->query[ 'order' ] ) ? $wp_query->query[ 'order' ] : ''; $order_by_query = ( 'ASC' == $order || 'DESC' == $order ) ? "SUM({$wpdb->prefix}betterdocs_analytics.impressions) {$order}" : ( 'MODIFIED' == $order ? "{$wpdb->prefix}posts.post_modified_gmt DESC" : "{$wpdb->prefix}posts.post_date_gmt DESC" ); $clauses[ 'join' ] = "JOIN {$wpdb->prefix}betterdocs_analytics ON {$wpdb->prefix}posts.ID = {$wpdb->prefix}betterdocs_analytics.post_id"; $clauses[ 'where' ] = ! current_user_can( 'read_private_docs' ) ? "AND ( ( {$wpdb->prefix}posts.post_type = 'docs' ) AND ( {$wpdb->prefix}posts.post_status = 'publish' OR {$wpdb->prefix}posts.post_status = 'future' ) )" : "AND ( ( {$wpdb->prefix}posts.post_type = 'docs' ) AND ( {$wpdb->prefix}posts.post_status = 'publish' OR {$wpdb->prefix}posts.post_status = 'future' OR {$wpdb->prefix}posts.post_status = 'draft' OR {$wpdb->prefix}posts.post_status = 'pending' OR {$wpdb->prefix}posts.post_status = 'private' ) )"; $clauses[ 'orderby' ] = $order_by_query; $clauses[ 'groupby' ] = "{$wpdb->prefix}betterdocs_analytics.post_id"; if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) { global $sitepress; if ( $sitepress->is_setup_complete() ) { $constant_language_code = ICL_LANGUAGE_CODE; $clauses[ 'join' ] .= " JOIN {$wpdb->prefix}icl_translations ON {$wpdb->prefix}posts.ID = {$wpdb->prefix}icl_translations.element_id"; $clauses[ 'where' ] .= " AND ( {$wpdb->prefix}icl_translations.language_code = '{$constant_language_code}' ) AND ( {$wpdb->prefix}icl_translations.element_type = CONCAT('post_', {$wpdb->prefix}posts.post_type ) )"; } } } } return $clauses; } public function init() { } /** * Modify terms args to include terms with only private docs for users with read_private_docs capability * * @param array $args * @return array */ public function modify_terms_args_for_private_docs( $args ) { // Only modify for users with read_private_docs capability and supported taxonomies if ( ! current_user_can( 'read_private_docs' ) || ! isset( $args[ 'taxonomy' ] ) ) { return $args; } // Only support doc_category taxonomy for private docs filtering // knowledge_base terms don't have docs directly assigned to them $supported_taxonomies = array( 'doc_category' ); if ( ! in_array( $args[ 'taxonomy' ], $supported_taxonomies ) ) { return $args; } // If hide_empty is true, we need to modify the logic to include terms with private docs if ( isset( $args[ 'hide_empty' ] ) && $args[ 'hide_empty' ] ) { // Set hide_empty to false and we'll filter manually later $args[ 'hide_empty' ] = false; // Add a flag to indicate we need to filter manually $args[ '_betterdocs_filter_private' ] = true; } return $args; } public function parse_term_query( $term_query ) { if ( empty( $term_query->query_vars[ 'taxonomy' ] ) ) { return; } if ( ! in_array( 'doc_category', $term_query->query_vars[ 'taxonomy' ], true ) ) { return; } global $current_screen; if ( null == $current_screen ) { return; } if ( 'doc_category' !== $current_screen->taxonomy || 'edit-doc_category' != $current_screen->id ) { return; } // Use base meta key for admin listing - fallback logic is handled in set_tax_order $meta_key = 'doc_category_order'; $term_query->query_vars[ 'meta_query' ] = array( array( 'key' => $meta_key, 'type' => 'NUMERIC' ) ); $term_query->query_vars[ 'orderby' ] = 'meta_value_num'; } public function parse_query( &$query ) { // dump( is_single(), $query->query_vars ); // if ( is_single() && isset( $query->query_vars['post_type'] ) && $query->query_vars['post_type'] == 'docs' ) { // $query->is_single = false; // $query->is_archive = true; // $query->set( 'knowledge_base', $query->query_vars['docs'] ); // } } public function pre_get_posts( &$query ) { if ( is_admin() || ! $query->is_main_query() ) { return; } if ( is_tax( 'doc_category' ) ) { $query->set( 'post_type', 'docs' ); $query->set( 'posts_per_archive_page', -1 ); $term = get_term_by( 'slug', $query->get( 'doc_category', '' ), 'doc_category' ); if ( $term && isset( $term->term_id ) ) { $post__in = $this->get_docs_order_by_terms( $term->term_id ); } else { $post__in = array(); } if ( ! empty( $post__in ) ) { $query->set( 'orderby', 'post__in' ); $query->set( 'post__in', $post__in ); } // if ( ! empty( $query->query_vars['knowledge_base'] ) ) { // $query->betterdocs_terms = get_terms( [ // 'taxonomy' => 'doc_category', // 'parent' => 0, // 'hide_empty' => true, // 'meta_query' => [ // 'relation' => 'OR', // [ // 'key' => 'doc_category_knowledge_base', // 'value' => $query->query_vars['knowledge_base'], // 'compare' => 'LIKE' // ] // ] // ] ); // } } // dump( is_archive(), $query->query_vars ); } /** * Get docs orders by term_id. * * @since 2.5.0 * @param int $term_id * * @return array */ public function get_docs_order_by_terms( $term_id ) { global $wpdb; // Get language-specific meta key with fallback $meta_key = \WPDeveloper\BetterDocs\Utils\Helper::get_meta_key_with_fallback( '_docs_order', $term_id ); $_docs_order = get_term_meta( $term_id, $meta_key, true ); $query = $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}term_relationships WHERE term_taxonomy_id = %d", $term_id ); $query_key = "docs_order_by_terms_{$term_id}_{$meta_key}_" . md5( $query ); if ( ( $results = $this->database->get_cache( $query_key ) ) !== false ) { return $results; } if ( ! empty( $_docs_order ) ) { $_docs_order = explode( ',', $_docs_order ); $new_ids = array(); $results = $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared if ( is_array( $results ) && ! empty( $results ) ) { $object_ids = array_filter( $results, function ( $value ) use ( $_docs_order ) { return ! in_array( $value->object_id, $_docs_order ); } ); if ( ! empty( $object_ids ) ) { array_walk( $object_ids, function ( $value ) use ( &$new_ids ) { $new_ids[ ] = $value->object_id; } ); } } $_docs_order = array_merge( $new_ids, $_docs_order ); $this->database->set_cache( $query_key, $_docs_order, 1 ); return $_docs_order; } return array(); } /** * For determine the next post ID * * @link https://developer.wordpress.org/reference/hooks/get_adjacent_post_where/ * * @param mixed $where * @param mixed $in_same_term * @param mixed $excluded_terms * @param mixed $taxonomy * @param mixed $post * @return mixed */ public function next_post_where( $where, $in_same_term, $excluded_terms, $taxonomy, $post ) { return $this->get_adjacent_post_id( 'next', $where, $post, $taxonomy ); } /** * For determine the previous post ID * * @link https://developer.wordpress.org/reference/hooks/get_adjacent_post_where/ * * @param mixed $where * @param mixed $in_same_term * @param mixed $excluded_terms * @param mixed $taxonomy * @param mixed $post * @return mixed */ public function previous_post_where( $where, $in_same_term, $excluded_terms, $taxonomy, $post ) { return $this->get_adjacent_post_id( 'previous', $where, $post, $taxonomy ); } /** * Get where clause for next/previous post ID mainly used in post navigation. * * @see `previous_post_where` and `next_post_where` methods. * * @param mixed $adjacent * @param mixed $where * @param mixed $post * @param mixed $taxonomy * @return mixed */ private function get_adjacent_post_id( $adjacent, $where, $post, $taxonomy ) { if ( 'doc_category' !== $taxonomy ) { return $where; } $_id = null; $_terms = get_the_terms( $post->ID, 'doc_category' ); if ( empty( $_terms ) ) { return $where; } global $wp_query, $wpdb; $_docs_order = $this->get_docs_order_by_terms( $_terms[ 0 ]->term_id ); $_orderby = $this->settings->get( 'alphabetically_order_post', 'betterdocs_order' ); $_order = $this->settings->get( 'docs_order', 'ASC' ); $_docs_order = 'betterdocs_order' === $_orderby ? $_docs_order : array(); if ( empty( $_docs_order ) ) { $statuses = array( 'publish' ); if ( current_user_can( 'read_private_docs' ) ) { $statuses[ ] = 'private'; } $_args = array( 'post_status' => $statuses, 'term_id' => $_terms[ 0 ]->term_id ); if ( isset( $wp_query->query_vars[ 'doc_category' ] ) ) { $_args[ 'tax_query' ][ ] = array( 'taxonomy' => 'doc_category', 'field' => 'slug', 'terms' => $wp_query->query_vars[ 'doc_category' ], 'operator' => 'AND', 'include_children' => false ); } elseif ( isset( $wp_query->query_vars[ 'name' ] ) && isset( $wp_query->query_vars[ 'post_type' ] ) && 'docs' == $wp_query->query_vars[ 'post_type' ] ) { $_post = get_page_by_path( $wp_query->query_vars[ 'name' ], OBJECT, 'docs' ); $doc_terms = array(); if ( isset( $_post->ID ) ) { $terms = get_the_terms( $_post->ID, 'doc_category' ); if ( ! empty( $terms ) ) { $doc_terms = wp_list_pluck( $terms, 'term_id' ); } } if ( ! empty( $doc_terms ) ) { $_args[ 'tax_query' ][ ] = array( 'taxonomy' => 'doc_category', 'field' => 'term_id', 'terms' => $doc_terms, 'operator' => 'AND', 'include_children' => false ); $_args[ 'term_id' ] = $doc_terms[ 0 ]; } } else { // Fallback: use the term from current post $_args[ 'tax_query' ][ ] = array( 'taxonomy' => 'doc_category', 'field' => 'term_id', 'terms' => $_terms[ 0 ]->term_id, 'operator' => 'AND', 'include_children' => false ); } $_args[ 'orderby' ] = $_orderby; if ( 'betterdocs_order' != $_orderby ) { $_args[ 'order' ] = $_order; } /** * Before Query */ do_action_ref_array( 'betterdocs_navigation_docs_query', array( &$_args ) ); $docs = $this->get_posts( $this->docs_query_args( $_args ) ); $_docs = array(); if ( $docs->have_posts() ) { array_map( function ( $_post ) use ( &$_docs ) { $_docs[ ] = $_post->ID; }, $docs->posts ); } $_docs_order = $_docs; } $_docs_order = apply_filters( 'betterdocs_adjacent_docs_order', $_docs_order, $_terms ); // Ensure consistent type comparison by converting all IDs to integers $_docs_order = array_map( 'intval', $_docs_order ); $_docs_order = array_values( $_docs_order ); // Re-index array after type conversion $_id_index = array_search( (int) $post->ID, $_docs_order, true ); // If current post not found in order list, return original where clause if ( false === $_id_index ) { return $where; } $_id_index = 'next' === $adjacent ? $_id_index + 1 : $_id_index - 1; $_id = isset( $_docs_order[ $_id_index ] ) ? (int) $_docs_order[ $_id_index ] : null; // Fix: replace only ID comparison in where clause using regex if ( $_id ) { // This replaces any 'p.ID < N', 'p.ID > N', 'p.ID <= N', or 'p.ID >= N' etc, with 'p.ID = $_id' $where = preg_replace( '/p\.ID\s*[<>!=]+\s*\d+/', 'p.ID = ' . (int) $_id, $where ); } return $where; } public function parse_terms_args( $args = array() ) { $_default_args = array( 'hide_empty' => true, 'taxonomy' => 'doc_category' ); // OrderBy & Order $_orderby = ! empty( $args[ 'orderby' ] ) && 1 != $args[ 'orderby' ] ? $args[ 'orderby' ] : 'name'; $_order = ! empty( $args[ 'order' ] ) ? $args[ 'order' ] : ''; if ( 'betterdocs_order' == $_orderby ) { // Use base meta key - fallback logic will be handled in the terms_clauses filter $args[ 'meta_key' ] = 'doc_category_order'; $args[ 'orderby' ] = 'meta_value_num'; $args[ 'order' ] = 'ASC'; } else { $args[ 'orderby' ] = $_orderby; $args[ 'order' ] = $_order; } // Nested Sub Category // $args['parent'] = 0; // if ( $nested_subcategory == true ) { // } if ( ! isset( $args[ 'number' ] ) ) { global $wp_query; if ( null === $wp_query->query || ( isset( $wp_query->query[ 'post_type' ] ) && 'docs' != $wp_query->query[ 'post_type' ] ) ) { $args[ 'number' ] = 4; } } // Includes if ( isset( $args[ 'include' ] ) ) { $_include = ! is_array( $args[ 'include' ] ) ? explode( ',', $args[ 'include' ] ) : $args[ 'include' ]; $args[ 'include' ] = $_include; $args[ 'orderby' ] = 'include'; unset( $args[ 'parent' ] ); } $_meta_query = ! empty( $args[ 'meta_query' ] ) ? $args[ 'meta_query' ] : array(); $args[ 'meta_query' ] = apply_filters( 'betterdocs_taxonomy_object_meta_query', $_meta_query, $args ); return apply_filters( 'betterdocs_category_terms_object', wp_parse_args( $args, $_default_args ), $args ); } public function get_terms( $args ) { $parsed_args = $this->parse_terms_args( $args ); $terms = get_terms( $parsed_args ); // Filter terms manually if we need to consider private docs for users with read_private_docs capability if ( isset( $parsed_args[ '_betterdocs_filter_private' ] ) && $parsed_args[ '_betterdocs_filter_private' ] && current_user_can( 'read_private_docs' ) ) { $terms = array_filter( $terms, function ( $term ) { // Get the actual count including private docs for users with read_private_docs capability $actual_count = $this->get_docs_count( $term, false ); return $actual_count > 0; } ); } return $terms; } public function get_child_terms( $args ) { if ( ! isset( $args[ 'number' ] ) ) { global $wp_query; if ( null === $wp_query->query || ( isset( $wp_query->query[ 'post_type' ] ) && 'docs' != $wp_query->query[ 'post_type' ] ) ) { $args[ 'number' ] = 1; } } return $this->get_terms( $args ); } /** * Get POSTs of Docs type. * * @param mixed $args * @return WP_Query */ public function get_posts( $args, $ignore = false ) { if ( ! $ignore ) { $args = $this->docs_query_args( $args ); } return new WP_Query( $args ); } public function get_taxonomy( $tax = '' ) { global $wp_query; if ( is_tax( 'knowledge_base' ) ) { $_tax = $wp_query->tax_query->queried_terms; if ( array_key_exists( 'doc_category', $_tax ) ) { $tax = 'doc_category'; } else { $tax = 'knowledge_base'; } } elseif ( is_tax( 'doc_category' ) ) { $tax = 'doc_category'; } return $tax; } public function terms_query( $args = array() ) { global $wp_query; $_origin_args = $args; $default_args = array( 'hide_empty' => true, 'taxonomy' => 'doc_category', 'orderby' => 'name' ); /** * Number Set * * @FIX: If Built-in Docs page off and docs_page in use, then Terms Query Number 4|1 set. */ // if ( $wp_query->query === NULL || ( isset( $wp_query->query['post_type'] ) && $wp_query->query['post_type'] != 'docs' ) ) { // $default_args['number'] = 1; // } /** * Nested Sub Category */ if ( isset( $args[ 'nested_subcategory' ] ) && true == $args[ 'nested_subcategory' ] ) { $default_args[ 'parent' ] = 0; unset( $args[ 'nested_subcategory' ] ); // if ( $wp_query->query === NULL || ( isset( $wp_query->query['post_type'] ) && $wp_query->query['post_type'] != 'docs' ) ) { // $default_args['number'] = 4; // } } /** * OrderBy and Order */ if ( ! isset( $args[ 'orderby' ] ) ) { $_orderby = $this->settings->get( 'terms_orderby', 'name' ); $_order = $this->settings->get( 'terms_order', '' ); } else { $_orderby = $args[ 'orderby' ]; $_order = ! empty( $args[ 'order' ] ) ? $args[ 'order' ] : ''; } if ( 'betterdocs_order' === $_orderby ) { // Use different meta keys for different taxonomies if ( isset( $args[ 'taxonomy' ] ) && 'knowledge_base' === $args[ 'taxonomy' ] ) { $default_args[ 'meta_key' ] = 'kb_order'; } else { $default_args[ 'meta_key' ] = 'doc_category_order'; } $_orderby = 'meta_value_num'; $_order = 'ASC'; } elseif ( true === $_orderby ) { $_orderby = 'name'; } $args[ 'orderby' ] = $_orderby; if ( ! empty( $_order ) ) { $args[ 'order' ] = $_order; } /** * @todo old hook * hook: betterdocs_child_taxonomy_meta_query */ $_multiple_kb = apply_filters( 'betterdocs_query_args_multiple_kb_enabled', isset( $args[ 'multiple_kb' ] ) ? (bool) $args[ 'multiple_kb' ] : false, $_origin_args ); $_kb_slug = isset( $args[ 'kb_slug' ] ) ? trim( $args[ 'kb_slug' ] ) : ''; unset( $args[ 'multiple_kb' ] ); unset( $args[ 'kb_slug' ] ); $meta_query = ! empty( $args[ 'meta_query' ] ) ? $args[ 'meta_query' ] : array(); $meta_query = apply_filters( 'betterdocs_terms_meta_query_args', $meta_query, $_multiple_kb, $_kb_slug, $_origin_args ); if ( ! empty( $meta_query ) ) { $default_args[ 'meta_query' ] = $meta_query; } if ( ! empty( $args[ 'terms' ] ) ) { $args[ 'include' ] = explode( ',', $args[ 'terms' ] ); $args[ 'orderby' ] = 'include'; $args[ 'order' ] = 'ASC'; unset( $default_args[ 'parent' ] ); unset( $args[ 'parent' ] ); unset( $args[ 'terms' ] ); } $_query_args = wp_parse_args( $args, $default_args ); $_query_args = apply_filters( 'betterdocs_terms_query_args', $_query_args, $_origin_args ); // Apply private docs logic for logged-in users $_query_args = $this->modify_terms_args_for_private_docs( $_query_args ); return $_query_args; } public function get_term_parents( $term_id, $taxonomy = 'doc_category', $args = array() ) { $term = get_term( $term_id, $taxonomy ); if ( is_wp_error( $term ) ) { return $term; } if ( ! $term || ! isset( $term->term_id ) ) { return array(); } $_lists = array(); $origin_term = $term_id; $term_id = $term->term_id; $defaults = array( 'format' => 'name', 'inclusive' => true ); $args = wp_parse_args( $args, $defaults ); $args[ 'inclusive' ] = wp_validate_boolean( $args[ 'inclusive' ] ); $parents = get_ancestors( $term_id, $taxonomy ); if ( $args[ 'inclusive' ] ) { array_unshift( $parents, $term_id ); } foreach ( array_reverse( $parents ) as $term_id ) { $parent = get_term( $term_id, $taxonomy ); $name = ( 'slug' === $args[ 'format' ] ) ? $parent->slug : $parent->name; $term_permalink = get_term_link( $parent->term_id, $taxonomy ); $term_permalink = apply_filters( 'betterdocs_breadcrumb_term_permalink', $term_permalink, $term_id ); $_item = array( 'url' => $term_permalink, 'text' => $name ); $_lists[ ] = $_item; } return apply_filters( 'betterdocs_breadcrumb_archive_lists', $_lists, $origin_term ); } /** * Get all non-empty child term IDs recursively for a given taxonomy and parent term. * * This function retrieves all child terms for a specified taxonomy and parent term, * recursively fetching child terms of child terms, and returns only those with a non-zero post count. * * @param string $taxonomy The taxonomy name (e.g., 'doc_category'). * @param int $parent_id The ID of the parent term to start retrieving children from. * * @return array An array of non-empty child term IDs. */ public function get_all_child_term_ids( $taxonomy, $parent_id ) { // Set up the arguments for retrieving child terms $args = apply_filters( 'betterdocs_get_child_term_ids_args', array( 'taxonomy' => $taxonomy, 'parent' => $parent_id, 'hide_empty' => true, 'fields' => 'ids' ) ); // Get the terms based on the arguments $terms = get_terms( $args ); // Initialize an empty array to hold non-empty child term IDs $non_empty_children = array(); // Check if terms were retrieved without errors and that the result isn't empty if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) { $term_ids = array(); // Loop through each term ID foreach ( $terms as $term_id ) { // Add the current term ID to the term_ids array $term_ids[ ] = $term_id; // Recursively get child terms for the current term $child_term_ids = $this->get_all_child_term_ids( $taxonomy, $term_id ); // If there are child terms, merge them into the term_ids array if ( ! empty( $child_term_ids ) ) { $term_ids = array_merge( $term_ids, $child_term_ids ); } } // Loop through all retrieved term IDs to filter out empty ones foreach ( $term_ids as $term_id ) { // Get the term object for the current term ID $child_term = get_term( $term_id, $taxonomy ); // Only include terms that have a non-zero post count if ( $child_term && isset( $child_term->term_id ) && $child_term->count > 0 ) { $non_empty_children[ ] = $child_term->term_id; } } } // Return the final array of non-empty child term IDs return $non_empty_children; } /** * Get all nested child term IDs of a specific parent term in a taxonomy and return as a comma-separated string. * * @param string $taxonomy The taxonomy. * @param int $parent_id The parent term ID. * @return string The comma-separated term IDs. */ public function get_child_term_ids_by_parent_id( $taxonomy, $parent_id ) { $term_ids = $this->get_all_child_term_ids( $taxonomy, $parent_id ); return implode( ',', $term_ids ); } public function get_terms_children( $taxonomy, $parent_id ) { $children = get_term_children( $parent_id, $taxonomy ); $non_empty_children = array(); if ( ! is_wp_error( $children ) && ! empty( $children ) ) { foreach ( $children as $child_id ) { $child_term = get_term( $child_id, $taxonomy ); // Only include non-empty terms if ( $child_term && $child_term->count > 0 ) { $non_empty_children[ ] = $child_term; } } } return $non_empty_children; } public function get_terms_children_ids( $taxonomy, $parent_id ) { $children = $this->get_terms_children( $taxonomy, $parent_id ); $term_ids = wp_list_pluck( $children, 'term_id' ); return implode( ',', $term_ids ); } public function count_terms_children( $taxonomy, $parent_id ) { return count( $this->get_terms_children( $taxonomy, $parent_id ) ); } public function is_inner_templates() { if ( is_tax( 'knowledge_base' ) || is_tax( 'doc_category' ) || is_tax( 'doc_tag' ) || is_singular( 'docs' ) ) { return true; } return false; } /** * Summary of docs_query_args * @param mixed $args * @throws \Exception * @return mixed */ public function docs_query_args( $args, $filter = array() ) { $_origin_args = $args; $default_args = array( 'post_type' => 'docs' ); if ( ! empty( $args[ 'post_type' ] ) && trim( $args[ 'post_type' ] ) === 'docs_any' ) { $default_args[ 'post_type' ] = 'docs'; $default_args[ 'post_status' ] = 'any'; unset( $args[ 'post_type' ] ); } /** * OrderBy and Order */ if ( ! isset( $args[ 'orderby' ] ) ) { $_orderby = $this->settings->get( 'alphabetically_order_post' ); $_order = $this->settings->get( 'docs_order', 'ASC' ); } else { $_orderby = $args[ 'orderby' ]; $_order = ! empty( $args[ 'order' ] ) ? $args[ 'order' ] : 'ASC'; } if ( 'betterdocs_order' != $_orderby ) { if ( true === $_orderby ) { $args[ 'orderby' ] = 'title'; } else { $args[ 'orderby' ] = $_orderby; } $args[ 'order' ] = $_order; } elseif ( 'betterdocs_order' == $_orderby ) { unset( $args[ 'orderby' ] ); } if ( empty( $args[ 'orderby' ] ) ) { unset( $args[ 'order' ] ); } /** * Term ID */ $_term_id = null; if ( ! empty( $args[ 'term_id' ] ) ) { $_term_id = intval( $args[ 'term_id' ] ); unset( $args[ 'term_id' ] ); } // if ( $_term_id == null ) { // throw new \Exception( __( '$args["term_id"] cannot be null.', 'betterdocs' ) ); // } /** * Term Slug for tax_query */ $_term_slug = ''; if ( ! empty( $args[ 'term_slug' ] ) ) { $_term_slug = trim( $args[ 'term_slug' ] ); unset( $args[ 'term_slug' ] ); } /** * @todo old hook * hook: betterdocs_cat_template_multikb */ $_multiple_kb = apply_filters( 'betterdocs_enable_multiple_knowledge_base', isset( $args[ 'multiple_kb' ] ) ? (bool) $args[ 'multiple_kb' ] : false, $_origin_args ); $_kb_slug = isset( $args[ 'kb_slug' ] ) ? trim( $args[ 'kb_slug' ] ) : ''; unset( $args[ 'multiple_kb' ] ); unset( $args[ 'kb_slug' ] ); $tax_query = array( array( 'taxonomy' => 'doc_category', 'field' => 'slug', 'terms' => $_term_slug, 'operator' => 'AND', 'include_children' => false ) ); if ( ! isset( $args[ 'orderby' ] ) || 'betterdocs_order' == $args[ 'orderby' ] ) { $args[ 'orderby' ] = 'post__in'; $args[ 'post__in' ] = $this->get_docs_order_by_terms( $_term_id ); } if ( isset( $args[ 'tax_query' ] ) ) { $tax_query = $args[ 'tax_query' ]; } $args[ 'tax_query' ] = apply_filters( 'betterdocs_docs_tax_query_args', $tax_query, $_multiple_kb, $_term_slug, $_kb_slug, $_origin_args, $this->is_inner_templates() ); /** * Final parse args */ $args = wp_parse_args( $args, $default_args ); if ( ! empty( $filter ) ) { $filter = array_flip( $filter ); $args = array_filter( $args, function ( $item ) use ( $filter ) { return ! array_key_exists( $item, $filter ); }, ARRAY_FILTER_USE_KEY ); } $final_args = apply_filters( 'betterdocs_articles_args', $args, $_term_id, $_origin_args ); // Process betterdocs_order AFTER all filters have run if ( isset( $final_args[ 'orderby' ] ) && 'betterdocs_order' == $final_args[ 'orderby' ] ) { $docs_order = $this->get_docs_order_by_terms( $_term_id ); if ( ! empty( $docs_order ) ) { $final_args[ 'orderby' ] = 'post__in'; $final_args[ 'post__in' ] = $docs_order; } else { $final_args[ 'orderby' ] = 'menu_order'; $final_args[ 'order' ] = 'ASC'; } } return $final_args; } public function faq_terms_query_args( $includes = '', $excludes = '', $args = array() ) { $_args = array( 'taxonomy' => 'betterdocs_faq_category', 'meta_key' => 'order', 'orderby' => 'meta_value_num', 'order' => 'ASC', 'include' => $includes, 'exclude' => $excludes, 'meta_query' => array( array( 'key' => 'status', 'value' => 1, 'compare' => '==' ) ) ); if ( 'all' == $_args[ 'include' ] ) { unset( $_args[ 'include' ] ); unset( $_args[ 'exclude' ] ); } if ( empty( $_args[ 'exclude' ] ) ) { unset( $_args[ 'exclude' ] ); } if ( empty( $_args[ 'include' ] ) ) { unset( $_args[ 'include' ] ); } return wp_parse_args( $args, $_args ); } public function get_faq_by_term( $term_id ) { global $wpdb; $args = array( 'post_type' => 'betterdocs_faq', 'post_status' => 'publish', 'tax_query' => array( array( 'taxonomy' => 'betterdocs_faq_category', 'field' => 'term_id', 'terms' => $term_id, 'operator' => 'AND' ) ), 'posts_per_page' => -1 ); $args[ 'orderby' ] = 'post__in'; $args[ 'post__in' ] = $this->get_faq_orders( $term_id ); return new WP_Query( $args ); } public function get_faq_orders( $term_id = null ) { global $wpdb; $faq_order = get_term_meta( $term_id, '_betterdocs_faq_order', true ); $faq_order = explode( ',', $faq_order ); $query = $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}term_relationships WHERE term_taxonomy_id = %d", $term_id ); $query_key = 'betterdocs_faq_order_' . md5( $query ); if ( ( $results = $this->database->get_cache( $query_key ) ) !== false ) { return $results; } if ( ! empty( $faq_order ) ) { $new_ids = array(); $results = $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared if ( ! is_null( $results ) && ! empty( $results ) && is_array( $results ) ) { $object_ids = array_filter( $results, function ( $value ) use ( $faq_order ) { return ! in_array( $value->object_id, $faq_order ); } ); if ( ! empty( $object_ids ) ) { array_walk( $object_ids, function ( $value ) use ( &$new_ids ) { $new_ids[ ] = $value->object_id; } ); } } $faq_order = array_merge( $new_ids, $faq_order ); } $this->database->set_cache( $query_key, $faq_order, 1 ); return $faq_order; } public function get_faq_terms( $terms = array() ) { $_terms = get_terms( array( 'taxonomy' => 'betterdocs_faq_category', 'hide_empty' => true, 'orderby' => 'name', 'order' => 'ASC', 'meta_query' => array( array( 'key' => 'status', 'value' => 1, 'compare' => '==' ) ) ) ); if ( ! is_wp_error( $_terms ) ) { foreach ( $_terms as $term ) { $terms[ $term->term_id ] = $term->name; } } return $terms; } public function get_doc_terms( $terms = array() ) { $_terms = get_terms( array( 'taxonomy' => 'doc_category', 'hide_empty' => true, 'orderby' => 'name', 'order' => 'ASC' ) ); if ( ! is_wp_error( $_terms ) ) { foreach ( $_terms as $term ) { $terms[ $term->term_id ] = $term->name; } } return $terms; } public function get_docs_count( $term, $nested_subcategory = false, $args = array() ) { // Validate term object if ( ! is_object( $term ) ) { return 0; } $counts = isset( $term->count ) ? $term->count : 0; if ( false == $nested_subcategory ) { // For non-nested categories, we need to recalculate counts based on user capabilities // Only proceed if we have a valid term with required properties if ( isset( $term->term_id ) && isset( $term->taxonomy ) && is_numeric( $term->term_id ) ) { // Get all post IDs for this term $post_ids = get_objects_in_term( $term->term_id, $term->taxonomy ); if ( ! empty( $post_ids ) ) { if ( current_user_can( 'read_private_docs' ) ) { // For users with read_private_docs capability, include both private and public posts $filtered_post_ids = array_filter( $post_ids, function ( $post_id ) { $post_status = get_post_status( $post_id ); return 'private' === $post_status || is_post_publicly_viewable( $post_id ); } ); } else { // For users without read_private_docs capability, only include public posts $filtered_post_ids = array_filter( $post_ids, function ( $post_id ) { return is_post_publicly_viewable( $post_id ); } ); } $counts = count( $filtered_post_ids ); } else { $counts = 0; } } return apply_filters( 'betterdocs_docs_count', $counts, $term, $nested_subcategory, $args ); } $_child_terms_docs_ids = $this->get_doc_ids_by_term( $term, null, $nested_subcategory ); if ( is_array( $_child_terms_docs_ids ) ) { $counts = count( $_child_terms_docs_ids ); } return apply_filters( 'betterdocs_docs_count', $counts, $term, $nested_subcategory, $args ); } public function get_doc_ids_by_term( $term, $optional = null, $nested_subcategory = false ) { // Check if term has required properties and is a valid object if ( ! is_object( $term ) || ! isset( $term->term_id ) || ! isset( $term->taxonomy ) || ! is_numeric( $term->term_id ) ) { return false; } $args = array( 'include' => $term->term_id ); if ( $nested_subcategory ) { $args[ 'child_of' ] = $term->term_id; unset( $args[ 'include' ] ); } $_child_terms = get_terms( $term->taxonomy, $args ); if ( ! is_array( $_child_terms ) ) { return false; } array_unshift( $_child_terms, $term ); $_child_terms_ids = array_column( $_child_terms, 'term_id' ); $_child_terms_taxs = array_column( $_child_terms, 'taxonomy' ); $_child_terms_docs_ids = get_objects_in_term( $_child_terms_ids, $_child_terms_taxs ); if ( null !== $optional ) { $_optional_doc_ids = get_objects_in_term( $optional->term_id, $optional->taxonomy ); $_child_terms_docs_ids = array_intersect( $_child_terms_docs_ids, $_optional_doc_ids ); } return array_filter( $_child_terms_docs_ids, function ( $doc_id ) { if ( ! current_user_can( 'read_private_docs' ) ) { return is_post_publicly_viewable( $doc_id ); } $_status = get_post_status( $doc_id ); return 'private' == $_status || is_post_publicly_viewable( $doc_id ); } ); } /** * Get the common query arguments for WP_Query. * * @param string $terms The taxonomy. * @param string $term_slug The taxonomy term slug. * @param array $additional_args Additional arguments to merge with the common arguments. * @return array The query arguments. */ private function tax_query_args( $terms, $term_slug, $additional_args = array() ) { $common_args = array( 'post_type' => 'docs', 'post_status' => 'publish', 'tax_query' => array( array( 'taxonomy' => $terms, 'field' => 'slug', 'terms' => $term_slug ) ) ); return array_merge( $common_args, $additional_args ); } /** * Get the latest updated date for a specific taxonomy term. * * @param string $terms The taxonomy. * @param string $term_slug The taxonomy term slug. * @return string|null The latest modified date or null if no posts found. */ public function latest_updated_date( $terms, $term_slug ) { $args = $this->tax_query_args( $terms, $term_slug, array( 'posts_per_page' => 1, 'orderby' => 'modified', 'order' => 'DESC' ) ); $query = new WP_Query( $args ); if ( $query->have_posts() ) { while ( $query->have_posts() ) { $query->the_post(); $latest_post = get_the_modified_date(); wp_reset_postdata(); return $latest_post; } } else { return null; } } /** * Check if there are any new posts within the last 7 days for a specific taxonomy term. * * @param string $terms The taxonomy. * @param string $term_slug The taxonomy term slug. * @return bool True if there are new posts, false otherwise. */ public function check_new_posts( $terms, $term_slug ) { $date_7_days_ago = date( 'Y-m-d H:i:s', strtotime( '-7 days' ) ); $args = $this->tax_query_args( $terms, $term_slug, array( 'posts_per_page' => 1, 'orderby' => 'modified', 'order' => 'DESC', 'date_query' => array( array( 'after' => $date_7_days_ago, 'inclusive' => true ) ) ) ); $query = new WP_Query( $args ); $has_new_posts = $query->have_posts(); wp_reset_postdata(); return $has_new_posts; } public function insert_search_keyword( $search_input, $input_not_found ) { if ( empty( $search_input ) ) { return false; } global $wpdb; // Use BINARY comparison to avoid collation mismatch errors // This works across all character sets (latin1, utf8, utf8mb4, etc.) $search = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}betterdocs_search_keyword WHERE BINARY keyword = %s", $search_input ) ); if ( ! empty( $search ) ) { $search_log = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}betterdocs_search_log WHERE created_at = %s AND keyword_id = %d", date( 'Y-m-d' ), $search[ 0 ]->id ) ); if ( ! empty( $search_log ) ) { if ( ! empty( $input_not_found ) ) { $tbl_field = 'not_found_count'; $count = $search_log[ 0 ]->not_found_count + 1; } else { $tbl_field = 'count'; $count = $search_log[ 0 ]->count + 1; } // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $insert = $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->prefix}betterdocs_search_log SET " . $tbl_field . ' = ' . $count . ' WHERE created_at = %s AND keyword_id = %d', $search_log[ 0 ]->created_at, $search_log[ 0 ]->keyword_id ) ); } else { if ( ! empty( $input_not_found ) ) { $count = 0; $not_found_count = 1; } else { $count = 1; $not_found_count = 0; } $insert = $wpdb->query( $wpdb->prepare( "INSERT INTO {$wpdb->prefix}betterdocs_search_log ( keyword_id, count, not_found_count, created_at ) VALUES ( %d, %d, %d, %s )", array( $search[ 0 ]->id, $count, $not_found_count, date( 'Y-m-d' ) ) ) ); } } else { $insert = $wpdb->query( $wpdb->prepare( "INSERT INTO {$wpdb->prefix}betterdocs_search_keyword ( keyword ) VALUES ( %s )", array( $search_input ) ) ); if ( $insert ) { if ( ! empty( $input_not_found ) ) { $count = 0; $not_found_count = 1; } else { $count = 1; $not_found_count = 0; } $insert = $wpdb->query( $wpdb->prepare( "INSERT INTO {$wpdb->prefix}betterdocs_search_log ( keyword_id, count, not_found_count, created_at ) VALUES ( %d, %d, %d, %s )", array( $wpdb->insert_id, $count, $not_found_count, date( 'Y-m-d' ) ) ) ); } } return $insert; } /** * Counts the number of 'doc_category' terms assigned to a specific 'knowledge_base' by slug. * * This method retrieves all terms in the 'doc_category' taxonomy and checks the term meta * 'doc_category_knowledge_base' to determine how many 'doc_category' terms are associated * with a given 'knowledge_base' slug. * * @param string $knowledge_base_slug The slug of the 'knowledge_base' to count the assignments for. * * @return int The count of 'doc_category' terms assigned to the specified 'knowledge_base'. */ public function count_doc_categories_for_knowledge_base( $knowledge_base_slug ) { $count = 0; $doc_categories = get_terms( array( 'taxonomy' => 'doc_category', 'hide_empty' => true ) ); if ( ! empty( $doc_categories ) && ! is_wp_error( $doc_categories ) ) { foreach ( $doc_categories as $term ) { $meta_value = get_term_meta( $term->term_id, 'doc_category_knowledge_base', true ); if ( $meta_value ) { $knowledge_bases = maybe_unserialize( $meta_value ); // Check if the specific knowledge base slug is present in the meta value array if ( is_array( $knowledge_bases ) && in_array( $knowledge_base_slug, $knowledge_bases ) ) { ++$count; } } } } return $count; } }