File: /var/www/intranet.kauko.lt/wp-content/plugins/document-library-pro/src/Folder_Tree.php
<?php
namespace Barn2\Plugin\Document_Library_Pro;
use Barn2\Plugin\Document_Library_Pro\Posts_Table_Pro\Table_Args;
use Barn2\Plugin\Document_Library_Pro\Posts_Table_Pro\Table_Query;
use Barn2\Plugin\Document_Library_Pro\Frontend_Scripts;
use Barn2\Plugin\Document_Library_Pro\Util\SVG_Icon;
use Barn2\Plugin\Document_Library_Pro\Util\Options;
use Barn2\Plugin\Document_Library_Pro\Taxonomies;
defined( 'ABSPATH' ) || exit;
/**
* This class handles the folders HTML output
*
* @package Barn2\document-library-pro
* @author Barn2 Plugins <support@barn2.com>
* @license GPL-3.0
* @copyright Barn2 Media Ltd
*/
class Folder_Tree {
private $allowed_terms = [];
private $exclude_categories = [];
private $include_categories = [];
private $exclude_terms = [];
private $include_terms = [];
private $args;
private $query;
private $shortcode_atts;
/**
* Constructor.
*
* @param array $args
* @param array $atts
*/
public function __construct( $args = [], $atts = [] ) {
if ( (int) $args['post_limit'] < 500 && (int) $args['post_limit'] !== -1 ) {
$args['post_limit'] = 500;
}
$args['lazy_load'] = false;
$this->args = new Table_Args( $args );
$this->query = new Table_Query( $this->args );
$this->shortcode_atts = $atts;
$this->init_category_options();
}
/**
* Get any terms options associated to or excluded from the folder structure
*/
private function init_category_options() {
$this->exclude_categories = array_filter( explode( ',', $this->args->get_args()['exclude_doc_category'] ) );
$this->include_categories = array_filter( explode( ',', $this->args->get_args()['doc_category'] ) );
$include_args = $this->args->get_args()['term'];
$this->include_terms = false !== strpos( $include_args, '+' ) ? array_filter( explode( '+', $include_args ) ) : array_filter( explode( ',', $include_args ) );
$this->include_terms = array_filter(
array_map(
function ( $term ) {
$term_split = explode( ':', $term, 2 );
if ( $term_split[0] === 'doc_categories' ) {
return $term_split[1];
}
},
$this->include_terms
)
);
$exclude_args = $this->args->get_args()['exclude_term'];
$this->exclude_terms = false !== strpos( $exclude_args, '+' ) ? array_filter( explode( '+', $exclude_args ) ) : array_filter( explode( ',', $exclude_args ) );
$this->exclude_terms = array_filter(
array_map(
function ( $term ) {
$term_split = explode( ':', $term, 2 );
if ( $term_split[0] === 'doc_categories' ) {
return $term_split[1];
}
},
$this->exclude_terms
)
);
add_filter( 'document_library_pro_icon_svg', [ $this, 'icon_svgs' ] );
}
/**
* Retrieves the folder tree HTML
*
* @return string $folder_html
*/
public function get_html() {
Frontend_Scripts::load_folder_scripts( $this->args );
if ( ! $this->query->get_posts() ) {
return __( 'No documents found.', 'document-library-pro' );
}
$category_tree = $this->generate_category_tree();
if ( empty( $category_tree ) ) {
$args = $this->args->get_args();
$args['folders'] = false;
return dlp_get_doc_library( $args );
}
ob_start();
$this->generate_category_folder_html( $category_tree, true, $this->shortcode_atts );
return ob_get_clean();
}
/**
* Generates the full category tree.
*
* @return array
*/
private function generate_category_tree() {
$categories = $this->get_attached_categories();
$tree_root = $this->generate_categories_tree_root( $categories );
$tree = $this->populate_missing_categories_into_tree( $tree_root );
return $tree;
}
/**
* Retrieves all categories currently attached to the current table query
*
* @return \WP_Term[] $categories
*/
private function get_attached_categories() {
$post_ids = array_map(
function ( \WP_Post $post ) {
return $post->ID;
},
$this->query->get_posts()
);
// We need to fetch all attached terms to match agains.
// Query options provided to the shortcode could exclude certain categories (e.g. include, exclude, status etc..)
$categories = wp_get_object_terms(
$post_ids,
[ Taxonomies::CATEGORY_SLUG ],
[
'orderby' => apply_filters( 'document_library_pro_folder_orderby', $this->args->get_args()['folders_order_by'] ),
'order' => $this->get_order(),
]
);
// Fetch empty parents not assigned to any documents in order to display the full hierarchy
$category_parents_objects = [];
foreach ( $categories as $category ) {
$category_parents_ids = get_ancestors( $category->term_id, Taxonomies::CATEGORY_SLUG );
if ( ! empty( $category_parents_ids ) ) {
$term_objects = array_map(
function ( $parent_id ) {
$parent_term = get_term( $parent_id, Taxonomies::CATEGORY_SLUG );
return $parent_term;
},
$category_parents_ids
);
$category_parents_objects = array_merge( $category_parents_objects, $term_objects );
}
}
$categories = array_merge( $categories, $category_parents_objects );
// We store all the allowed terms from the main query above. This is used to filter the categories in lower levels of the tree.
$this->allowed_terms = array_map(
function ( $term ) {
return $term->term_id;
},
$categories
);
// Adjust for shortcode provided category query changes
if ( ! empty( $this->include_terms ) ) {
$using_term_ids = count( $this->include_terms ) === count( array_filter( $this->include_terms, 'is_numeric' ) );
$field = $using_term_ids ? 'term_id' : 'slug';
$include_terms = $this->include_terms;
$categories = array_filter(
$categories,
function ( \WP_Term $item ) use ( $include_terms, $field ) {
return in_array( (string) $item->{$field}, $include_terms, true );
}
);
}
if ( ! empty( $this->exclude_terms ) ) {
$using_term_ids = count( $this->exclude_terms ) === count( array_filter( $this->exclude_terms, 'is_numeric' ) );
$field = $using_term_ids ? 'term_id' : 'slug';
$exclude_terms = $this->exclude_terms;
$categories = array_filter(
$categories,
function ( \WP_Term $item ) use ( $exclude_terms, $field ) {
return ! in_array( (string) $item->{$field}, $exclude_terms, true );
}
);
}
if ( ! empty( $this->include_categories ) ) {
$using_term_ids = count( $this->include_categories ) === count( array_filter( $this->include_categories, 'is_numeric' ) );
$field = $using_term_ids ? 'term_id' : 'slug';
$categories = array_filter(
$categories,
function ( \WP_Term $item ) use ( $field ) {
return in_array( (string) $item->{$field}, $this->include_categories, true );
}
);
}
if ( ! empty( $this->exclude_categories ) ) {
$using_term_ids = count( $this->exclude_categories ) === count( array_filter( $this->exclude_categories, 'is_numeric' ) );
$field = $using_term_ids ? 'term_id' : 'slug';
$categories = array_filter(
$categories,
function ( \WP_Term $item ) use ( $field ) {
return ! in_array( (string) $item->{$field}, $this->exclude_categories, true );
}
);
}
/**
* Filter the categories to hide locked categories
*
* @since 4.9.0
*
* @param array $categories An array of categories for retrieving terms for the given object(s).
*/
$categories = apply_filters( 'document_library_pro_folder_categories', $categories );
return $categories;
}
/**
* Generate the top level of the tree array
*
* @param \WP_Term[] $categories
*/
private function generate_categories_tree_root( $categories ) {
$category_tree = [];
// Get all term ids
$term_ids = array_map(
function ( $term ) {
return $term->term_id;
},
$categories
);
// Check for those without included parents
$top_level_categories = array_filter(
$categories,
function ( \WP_Term $category ) use ( $term_ids ) {
return ! in_array( $category->parent, $term_ids, true );
}
);
// Get top level term ids
$top_level_ids = array_map(
function ( $category ) {
return $category->term_id;
},
$top_level_categories
);
// Get the remaining sub categories
$remaining_sub_categories = array_filter(
$categories,
function ( \WP_Term $category ) use ( $term_ids ) {
return in_array( $category->parent, $term_ids, true );
}
);
// Get the remaining sub categories ids
$remaining_sub_categories_ids = array_map(
function ( $category ) {
return $category->term_id;
},
$remaining_sub_categories
);
// Sort the remaining sub categories
$remaining_sub_categories = get_terms(
[
'taxonomy' => Taxonomies::CATEGORY_SLUG,
'include' => $remaining_sub_categories_ids,
'orderby' => apply_filters( 'document_library_pro_folder_orderby', $this->args->get_args()['folders_order_by'] ),
'order' => $this->get_order(),
'hide_empty' => true,
]
);
// Check if any of the top root categories are children of each other
foreach ( $top_level_categories as $index => $category ) {
if ( $category->parent === 0 ) {
continue;
}
$ancestors = get_ancestors( $category->term_id, Taxonomies::CATEGORY_SLUG, 'taxonomy' );
if ( ! empty( array_intersect( $ancestors, $top_level_ids ) ) ) {
$remaining_sub_categories[] = $category;
unset( $top_level_categories[ $index ] );
}
}
// Get top level term ids so we can sort by menu order
$top_level_ids = array_map(
function ( $category ) {
return $category->term_id;
},
$top_level_categories
);
$top_level_categories = get_terms(
[
'taxonomy' => Taxonomies::CATEGORY_SLUG,
'include' => $top_level_ids,
'orderby' => apply_filters( 'document_library_pro_folder_orderby', $this->args->get_args()['folders_order_by'] ),
'order' => $this->get_order(),
'hide_empty' => true,
]
);
// Create the tree root (top level)
foreach ( $top_level_categories as $category ) {
$category_tree[ $category->term_id ] = $this->fill_in_subcategories( $remaining_sub_categories, $category->term_id );
}
return $category_tree;
}
/**
* Recursively fills in the attached subcategories of a tree root
*
* @param \WP_Term[] $categories
* @param int $parent
* @return array $category_tree
*/
private function fill_in_subcategories( $categories, $parent ) {
$terms = array_filter(
$categories,
function ( \WP_Term $category ) use ( $parent ) {
return $category->parent === $parent;
}
);
$category_tree = [];
foreach ( $terms as $term ) {
$category_tree[ $term->term_id ] = $this->fill_in_subcategories( $categories, $term->term_id );
}
return $category_tree;
}
/**
* Populates in missing category hierarchy of included terms
*
* @param array $category_tree
* @return array $category_tree
*/
private function populate_missing_categories_into_tree( $category_tree ) {
foreach ( $category_tree as $category_id => $sub_categories ) {
$term_children = get_terms(
[
'taxonomy' => Taxonomies::CATEGORY_SLUG,
'parent' => $category_id,
'orderby' => apply_filters( 'document_library_pro_folder_orderby', $this->args->get_args()['folders_order_by'] ),
'order' => $this->get_order(),
'hide_empty' => true,
]
);
// Check that the term children are in the allowed categories from the generated IDs of the whole query.
if ( ! empty( $this->allowed_terms ) ) {
$term_children = array_filter(
$term_children,
function ( \WP_Term $item ) {
return in_array( $item->term_id, $this->allowed_terms, true );
}
);
}
if ( ! empty( $this->exclude_categories ) ) {
$using_term_ids = count( $this->exclude_categories ) === count( array_filter( $this->exclude_categories, 'is_numeric' ) );
$field = $using_term_ids ? 'term_id' : 'slug';
$term_children = array_filter(
$term_children,
function ( \WP_Term $item ) use ( $field ) {
return ! in_array( (string) $item->{$field}, $this->exclude_categories, true );
}
);
}
if ( ! empty( $this->exclude_terms ) ) {
$using_term_ids = count( $this->exclude_terms ) === count( array_filter( $this->exclude_terms, 'is_numeric' ) );
$field = $using_term_ids ? 'term_id' : 'slug';
$exclude_terms = $this->exclude_terms;
$term_children = array_filter(
$term_children,
function ( \WP_Term $item ) use ( $exclude_terms, $field ) {
return ! in_array( (string) $item->{$field}, $exclude_terms, true );
}
);
}
foreach ( $term_children as $term ) {
$term_descendants = get_term_children( $term->term_id, Taxonomies::CATEGORY_SLUG );
if ( empty( $term_descendants ) && $term->count === 0 ) {
continue;
}
$category_tree[ $category_id ][ $term->term_id ] = [];
}
$category_tree[ $category_id ] = $this->populate_missing_categories_into_tree( $category_tree[ $category_id ] );
}
return $category_tree;
}
/**
* Generates the folder HTML.
*
* @param mixed $categories
* @param bool $root
* @param array $shortcode_atts
*/
private function generate_category_folder_html( $categories, $root = false, $shortcode_atts = [] ) {
?>
<div class="dlp-folders-container" style="display:none;">
<?php
if ( $root ) {
$this->maybe_output_search_box( 'top' );
$this->maybe_output_search_results_container();
}
if ( isset( Options::get_settings()['folder_design'] ) && Options::get_settings()['folder_design'] === 'default' ) {
$top_folder_color = '#f6b900';
$sub_folder_color = '#f6b900';
} else {
$settings = Options::get_settings();
$top_folder_color = isset($settings['folder_icon_color']) ? $settings['folder_icon_color'] : '#f6b900';
$sub_folder_color = isset($settings['folder_icon_subcolor']) ? $settings['folder_icon_subcolor'] : '#f6b900';
}
?>
<ul class="dlp-folders <?php echo $root ? 'dlp-folders-root' : ''; ?>" <?php echo $root ? 'data-shortcode-atts=\'' . wp_json_encode( $shortcode_atts ) . '\'' : ''; ?>>
<?php
foreach ( $categories as $category_id => $subcategories ) :
$term = get_term( $category_id );
$status = $this->get_term_status( $term );
?>
<li class="dlp-folder dlp-folder <?php echo esc_attr( $status ); ?>" data-category-id="<?php echo esc_attr( $category_id ); ?>">
<span class="dlp-category" tabindex="0" role="button" aria-expanded="false">
<?php
$is_top_level = $term->parent === 0;
$folder_color = $is_top_level ? $top_folder_color : $sub_folder_color;
SVG_Icon::render( 'folder', [], $folder_color );
SVG_Icon::render( 'folder_open', [], $folder_color );
?>
<span class="dlp-folder-label">
<span class="dlp-category-name"><?php echo esc_html( $term->name ); ?></span>
<?php
$description = $term->description;
if ( $description !== '' && apply_filters( 'document_library_pro_should_display_category_description_in_folder', false ) ) {
if ( isset( Options::get_user_shortcode_options()['shortcodes'] ) && Options::get_user_shortcode_options()['shortcodes'] ) {
$description = do_shortcode( $description );
}
?>
<span class="dlp-category-description"><?php echo wp_kses_post( $description ); ?></span>
<?php } ?>
</span>
</span>
<div class="dlp-folder-inner">
<div class="dlp-category-table">
</div>
<div class="dlp-category-subcategories">
<?php
if ( ! empty( $subcategories ) ) {
$this->generate_category_folder_html( $subcategories );
}
?>
</div>
</div>
</li>
<?php endforeach; ?>
</ul>
<?php
if ( $root ) {
$this->maybe_output_search_box( 'bottom' );
}
?>
</div>
<?php
}
/**
* Determines the initial status (open or closed) of a term folder.
*
* @param \WP_Term $term The term object.
* @return string 'closed' if the folder should be closed, empty string if it should be open.
*/
private function get_term_status( $term ) {
if ( 'custom' !== $this->args->folder_status ) {
return 'closed' === $this->args->folder_status ? 'closed' : '';
}
$open_folders = array_filter( explode( ',', $this->args->folder_status_custom ) );
// the term ID or the term slug is listed in the open folders field
$is_open = in_array( (string) $term->term_id, $open_folders, true ) || in_array( $term->slug, $open_folders, true );
if ( $is_open ) {
return '';
}
foreach ( get_term_children( $term->term_id, Taxonomies::CATEGORY_SLUG ) as $child ) {
$child = get_term( $child );
$is_open = ( in_array( (string) $child->term_id, $open_folders, true ) || in_array( $child->slug, $open_folders, true ) );
if ( $is_open ) {
return '';
}
}
return 'closed';
}
/**
* Output the folder search box.
*
* @param string $context
*/
private function maybe_output_search_box( $context = 'top' ) {
$search_position = $this->args->get_args()['search_box'];
$search_html = '';
if ( $search_position === 'true' || $search_position === true ) {
$search_position = 'top';
}
// Search Input
if ( in_array( $search_position, [ $context, 'both' ], true ) ) {
$search_label = apply_filters( 'document_library_pro_search_label', __( 'Search:', 'document-library-pro' ) );
$search_placeholder = apply_filters( 'document_library_pro_search_placeholder', '' );
$search_query = filter_input( INPUT_GET, 'dlp_search', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) ?? '';
$search_html = sprintf(
'<div class="dlp-folders-search"><label>%1$s<input type="search" placeholder="%2$s" value="%3$s"></label><div class="dlp-folders-reset"><a class="reset" href="#">%4$s %5$s</a></div></div>',
esc_html( $search_label ),
esc_html( $search_placeholder ),
$search_query,
SVG_Icon::get( 'reset' ),
esc_html__( 'Reset', 'document-library-pro' )
);
}
/* phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped */
echo $search_html;
}
/**
* Output the search results container DIV.
*/
private function maybe_output_search_results_container() {
$search_position = $this->args->get_args()['search_box'];
if ( ! in_array( $search_position, [ 'top', 'bottom', 'both', 'true', true ], true ) ) {
return;
}
echo '<div class="dlp-folders-search-results"></div>';
}
/**
* Determines the order based on the orderby parameter
*
* @return string
*/
private function get_order() {
$orderby = apply_filters( 'document_library_pro_folder_orderby', $this->args->get_args()['folders_order_by'] );
if ( $orderby === 'count' ) {
return 'DESC';
} else {
return 'ASC';
}
}
/**
* Filters the SVG icons used for folders.
*
* @param array $icons An array of SVG icon HTML strings, keyed by icon name.
* @return array|void The modified array of icons, or void if default design is used.
*/
public function icon_svgs( $icons ) {
if ( get_option( 'folder_design' ) === 'default' ) {
return;
}
$folder_closed = get_option( Options::FOLDER_CLOSE_SVG_OPTION_KEY );
if ( $folder_closed ) {
$icons['folder'] = html_entity_decode( $folder_closed );
}
$folder_open = get_option( Options::FOLDER_OPEN_SVG_OPTION_KEY );
if ( $folder_open ) {
$icons['folder_open'] = html_entity_decode( $folder_open );
}
return $icons;
}
}