diff --git a/app/Config/SettingsRepository.php b/app/Config/SettingsRepository.php index 1070d85..c87b748 100644 --- a/app/Config/SettingsRepository.php +++ b/app/Config/SettingsRepository.php @@ -88,6 +88,25 @@ public function defaultHideInNav() return false; } + + /** + * Number of available page groups before selection is required + * @return int + */ + public function maxNonSelectedPageGroups() + { + $value = 3; + $option = get_option('nestedpages_ui', false); + if ( is_array($option) ) + if ( array_key_exists('max_nonselected_pagegroups', $option) ) { + if ( is_numeric($option['max_nonselected_pagegroups']) ) { + if ( $option['max_nonselected_pagegroups'] >= -1 ) $value = $option['max_nonselected_pagegroups']; + } + } + if ( $value < -1 ) $value = -1; + return $value; + } + /** * Are menus completely disabled? * @return boolean @@ -262,4 +281,5 @@ public function sortViewEnabled() endif; return $roles; } -} \ No newline at end of file + +} diff --git a/app/Entities/AdminMenu/BlockEditorLink.php b/app/Entities/AdminMenu/BlockEditorLink.php index 9816d13..340ffb8 100644 --- a/app/Entities/AdminMenu/BlockEditorLink.php +++ b/app/Entities/AdminMenu/BlockEditorLink.php @@ -55,6 +55,8 @@ private function fullPageEditorLink($type) if ( !$user_can_view ) return; $link = ( $type->name == 'page' ) ? 'admin.php?page=nestedpages' : 'admin.php?page=' . $this->post_type_repo->getMenuSlug($type); $url = admin_url($link); + $pagegroup = $this->post_repo->getPageGroup(get_post()->ID); + if ( $pagegroup !== false ) $url .= '&np_page_group=' . $pagegroup; echo ''; } -} \ No newline at end of file +} diff --git a/app/Entities/Listing/Listing.php b/app/Entities/Listing/Listing.php index 395ecc0..a833117 100644 --- a/app/Entities/Listing/Listing.php +++ b/app/Entities/Listing/Listing.php @@ -127,6 +127,18 @@ class Listing */ private $status_preference; + /** + * Current page group + * @var ?int + */ + private $page_group_id; + + /** + * Available page groups + * @var array + */ + private $page_groups; + /** * Enabled Custom Fields */ @@ -150,6 +162,8 @@ public function __construct($post_type) $this->setPostTypeSettings(); $this->setStandardFields(); $this->setStatusPreference(); + $this->loadPageGroups(); + $this->setPageGroup(); } /** @@ -359,15 +373,42 @@ private function publishedChildrenCount($post) return $publish_count; } + /** + * Load all the available page groups + */ + private function loadPageGroups() { + $this->page_groups = $this->listing_query->getPosts($this->post_type, $this->h_taxonomies, $this->f_taxonomies, null, true); + } + + /** + * Set the page group from the HTTP request + */ + private function setPageGroup() { + $this->page_group_id = null; + if ( array_key_exists( 'np_page_group', $_REQUEST ) ) { + if ( is_numeric( $_REQUEST['np_page_group'] ) ) + $this->page_group_id = $_REQUEST['np_page_group']; + } + $this->all_posts = []; + } + /** * Loop through all the pages and create the nested / sortable list * Called in listing.php view */ - private function getPosts() + private function printPostsList() { - $this->all_posts = $this->listing_query->getPosts($this->post_type, $this->h_taxonomies, $this->f_taxonomies); - $this->listPostLevel(); - return; + $max_nonselected_pagegroups = $this->settings->maxNonSelectedPageGroups(); + if ( $max_nonselected_pagegroups == -1 ) + $list_due_to_pagegroups = true; + else if ( $max_nonselected_pagegroups == 0 ) + $list_due_to_pagegroups = false; + else + $list_due_to_pagegroups = count($this->page_groups) <= $max_nonselected_pagegroups; + if ( $list_due_to_pagegroups || $this->page_group_id !== null || $this->listing_repo->isSearch() ) { + $this->all_posts = $this->listing_query->getPosts($this->post_type, $this->h_taxonomies, $this->f_taxonomies, $this->page_group_id, false); + $this->listPostLevel(); + } } /** diff --git a/app/Entities/Listing/ListingQuery.php b/app/Entities/Listing/ListingQuery.php index 7ec9f9a..fe64e05 100644 --- a/app/Entities/Listing/ListingQuery.php +++ b/app/Entities/Listing/ListingQuery.php @@ -5,6 +5,7 @@ use NestedPages\Entities\PostType\PostTypeRepository; use NestedPages\Config\SettingsRepository; use NestedPages\Entities\PluginIntegration\IntegrationFactory; +use NestedPages\Entities\Post\PostRepository; /** * Performs the query for the page listing and formats into a multidemensional array @@ -73,7 +74,7 @@ private function setOrder() /** * Get the Posts */ - public function getPosts($post_type, $h_taxonomies = [], $f_taxonomies = []) + public function getPosts($post_type, $h_taxonomies = [], $f_taxonomies = [], $page_group_id = null, $only_page_groups = false) { $this->post_type = $post_type; @@ -91,7 +92,8 @@ public function getPosts($post_type, $h_taxonomies = [], $f_taxonomies = []) $post_type = [$post_type->name]; } - $statuses = ['publish', 'pending', 'draft', 'private', 'future', 'trash']; + $statuses = ['publish', 'pending', 'draft', 'private', 'future']; + if ( !$only_page_groups ) $statuses[] = 'trash'; $post_type_settings = $this->post_type_repo->getSinglePostType($post_type[0]); if ( isset($post_type_settings->custom_statuses) ) $statuses = array_merge($statuses, $post_type_settings->custom_statuses); @@ -101,23 +103,33 @@ public function getPosts($post_type, $h_taxonomies = [], $f_taxonomies = []) 'author' => $this->sort_options->author, 'orderby' => $this->sort_options->orderby, 'post_status' => apply_filters('nestedpages_listing_statuses', $statuses, $this->post_type), - 'order' => $this->sort_options->order + 'order' => $this->sort_options->order, + 'suppress_filters' => false, + 'nested_pages_page_group' => $page_group_id, ]; if ( $this->listing_repo->isSearch() ) $query_args = $this->searchParams($query_args); if ( $this->listing_repo->isFiltered() ) $query_args = $this->filterParams($query_args); if ( $this->sort_options->tax_query ) $query_args['tax_query'] = $this->sort_options->tax_query; + if ( $page_group_id !== null ) + $query_args['post__in'] = PostRepository::getPostsOfPageGroup( + $page_group_id, + [ 'post_type' => + [ + 'include' => ['page'] + ], + ] + ); + if ( $only_page_groups ) $query_args['post_parent'] = 0; $query_args = apply_filters('nestedpages_page_listing', $query_args, $this->post_type); - add_filter( 'posts_clauses', [$this, 'queryFilter']); + add_filter( 'posts_clauses', [$this, 'queryFilter'] ); $all_posts = new \WP_Query($query_args); - remove_filter( 'posts_clauses', [$this, 'queryFilter']); - - if ( $all_posts->have_posts() ) : - return $all_posts->posts; - endif; wp_reset_postdata(); - return null; + remove_filter( 'posts_clauses', [$this, 'queryFilter'] ); + + if ( ! $all_posts->have_posts() ) wp_reset_postdata(); + return $all_posts->posts; } /** @@ -166,7 +178,12 @@ private function setTaxonomyFilters() public function queryFilter($pieces) { global $wpdb; - + + // Restrict fields to avoid loading the post contents + $pieces['fields'] = 'ID, post_author, post_date, post_date_gmt, post_title, post_status,' + . ' post_name, post_modified, post_modified_gmt, post_parent, guid, menu_order,' + . ' post_type, post_mime_type, comment_count'; + // Add Hierarchical Categories $c = 0; foreach($this->h_taxonomies as $tax){ @@ -209,11 +226,12 @@ public function queryFilter($pieces) LEFT JOIN `$wpdb->term_taxonomy` AS $tt ON $tt.term_taxonomy_id = $tr.term_taxonomy_id AND $tt.taxonomy = '$name' LEFT JOIN `$wpdb->terms` AS $t ON $t.term_id = $tt.term_id"; endif ; - $pieces['fields'] .= ",GROUP_CONCAT(DISTINCT $t.term_id SEPARATOR ',') AS '$name'"; + $pieces['fields'] .= ", GROUP_CONCAT(DISTINCT $t.term_id SEPARATOR ',') AS '$name'"; $c++; } $pieces['groupby'] = "$wpdb->posts.ID"; return $pieces; } -} \ No newline at end of file + +} diff --git a/app/Entities/PluginIntegration/WPML.php b/app/Entities/PluginIntegration/WPML.php index 323b4b2..ef016e6 100644 --- a/app/Entities/PluginIntegration/WPML.php +++ b/app/Entities/PluginIntegration/WPML.php @@ -344,4 +344,15 @@ public function getPostTypeCountByLanguage($post_type, $language = null) $query = $wpdb->prepare("SELECT COUNT(*) FROM {$wpdb->prefix}posts AS p LEFT JOIN {$wpdb->prefix}icl_translations AS trans ON p.ID = trans.element_id WHERE post_type = %s AND p.post_status = 'publish' AND trans.language_code = %s", $post_type, $language); return $wpdb->get_var($query); } + + /** + * Get language of given post ID + * @param $post_id int - the post ID + * @return string or null - language code + */ + public function getPostLanguage($post_id) + { + $langauge_details = apply_filters('wpml_post_language_details', null, $post_id); + return $langauge_details['language_code'] ?? null; + } } diff --git a/app/Entities/Post/PostRepository.php b/app/Entities/Post/PostRepository.php index 19e0fa7..9631984 100644 --- a/app/Entities/Post/PostRepository.php +++ b/app/Entities/Post/PostRepository.php @@ -222,4 +222,71 @@ public function getChildren($parent_id, $post_type, $posts = []) } return $posts; } -} \ No newline at end of file + + /** + * Return posts of a page group (recursion function) + */ + private static function getChildPosts_recurse(int $toplevel_id, array &$pages, string &$sqlWhere) { + global $wpdb; + $sqlQuery = "select id from {$wpdb->posts} where post_parent = {$toplevel_id} $sqlWhere"; + $rows_sub_ids = $wpdb->get_results($sqlQuery, ARRAY_A); + foreach ( $rows_sub_ids as &$row_sub_id ) { + $sub_id = $row_sub_id['id']; + $pages[] = $sub_id; + self::getChildPosts_recurse($sub_id, $pages, $sqlWhere); + } + unset($row_sub_id); + } + + /** + * Get child posts of a post + */ + public static function getChildPosts(int $toplevel_id, array $args = []) { + $sqlWhere = ''; + foreach ( ['post_type', 'post_status'] as $field ) { + if ( array_key_exists($field, $args) ) { + foreach ( ['include', 'exclude'] as $rule ) { + if ( array_key_exists( $rule, $args[$field] ) ) { + $sqlWhere .= \NestedPages\Helpers::getSQLWhere($rule == 'include', $field, (array)$args[$field][$rule]); + } + } + } + } + $page_ids = [ $toplevel_id ]; + self::getChildPosts_recurse($toplevel_id, $page_ids, $sqlWhere); + return $page_ids; + } + + /** + * Get posts of a page group. + * By default, it returns all related posts. + */ + public static function getPostsOfPageGroup(int $post_id, ?array $postTypes = null) { + $page_ids = self::getChildPosts( self::getPageGroup($post_id), $postTypes ); + return $page_ids; + } + + /** + * Return page group of the given post + */ + public static function getPageGroup(int $post_id) { + global $wpdb; + $pagegroup = false; + $has_error = false; + $p_id = $post_id; + do { + $rows = $wpdb->get_results("select id, post_parent from {$wpdb->posts} where post_type = 'page' and id = " . $p_id, ARRAY_A); + if ( count( $rows ) == 1 ) { + if ( $rows[0]['post_parent'] ) { + $p_id = $rows[0]['post_parent']; + } else { + $pagegroup = $rows[0]['id']; + } + } else + $has_error = true; + } while ( ! $has_error && $pagegroup === false ); + return $has_error ? false : $pagegroup; + } + + +} diff --git a/app/Form/Listeners/ListingSort.php b/app/Form/Listeners/ListingSort.php index 77c985f..50e8b3d 100644 --- a/app/Form/Listeners/ListingSort.php +++ b/app/Form/Listeners/ListingSort.php @@ -39,6 +39,8 @@ private function setURL() $this->setOrder(); $this->setAuthor(); $this->setTaxonomies(); + $this->setPageGroup(); + $this->setLanguage(); } /** @@ -81,4 +83,22 @@ private function setTaxonomies() endif; endforeach; } -} \ No newline at end of file + + /** + * Set page group + */ + private function setPageGroup() + { + if ( isset($_POST['np_page_group']) && $_POST['np_page_group'] !== '' ) $this->url .= '&np_page_group=' . sanitize_text_field($_POST['np_page_group']); + } + + /** + * Set language + */ + private function setLanguage() + { + if ( isset($_POST['lang']) ) $this->url .= '&lang=' . sanitize_text_field($_POST['lang']); + } + + +} diff --git a/app/Helpers.php b/app/Helpers.php index 2f7d9d8..8b1f4be 100644 --- a/app/Helpers.php +++ b/app/Helpers.php @@ -41,4 +41,22 @@ public static function defaultPagesLink($type = 'page') $link = esc_url( admin_url('edit.php?post_type=' . $type ) ); return $link; } -} \ No newline at end of file + + /** + * Return a part of an SQL where clause. + * Since this function is used internally and possibly by theme and plugin developers only, + * it is expected that the field name is not vulnerable to SQL injection. + */ + public static function getSQLWhere(bool $include, string $field, array $values) { + $sqlWhere = ' and ' . $field; + if ( !$include ) $sqlWhere .= ' not'; + $sqlWhere .= ' in ('; + $sep = ''; + foreach ($values as $value) { + $sqlWhere .= $sep . "'" . esc_sql($value) . "'"; + $sep = ', '; + } + $sqlWhere .= ')'; + } + +} diff --git a/app/Views/listing.php b/app/Views/listing.php index 31e0c5c..b815427 100644 --- a/app/Views/listing.php +++ b/app/Views/listing.php @@ -89,14 +89,17 @@
- +