diff --git a/app/functions.php b/app/functions.php index ddc1ed6..4de91dd 100644 --- a/app/functions.php +++ b/app/functions.php @@ -13,6 +13,7 @@ new \Sakura\Helpers\WhoopsHelper(); new \Sakura\Helpers\ViteRequireHelper(); new \Sakura\Helpers\CustomMenuMetaFieldsHelper(); new \Sakura\Helpers\CommentHelper(); +new \Sakura\Helpers\PostQueryHelper('post'); new \Sakura\Routers\ApiRouter(); new \Sakura\Routers\PagesRouter(); diff --git a/app/helpers/post-query-helper.php b/app/helpers/post-query-helper.php new file mode 100644 index 0000000..9ba730e --- /dev/null +++ b/app/helpers/post-query-helper.php @@ -0,0 +1,51 @@ +post_type = $post_type; + add_filter("rest_{$post_type}_query", [$this, 'filter_rest_post_query'], 10, 2); + } + + /** + * Filter rest posts by category slug + * + * @param array $args + * @param WP_Rest_Rquest $request + * @return array $args + */ + public function filter_rest_post_query($args, $request) + { + $args['tax_query'] = []; + + $taxonomies = wp_list_filter(get_object_taxonomies($this->post_type, 'objects'), array('show_in_rest' => true)); + + foreach ($taxonomies as $taxonomy) { + $base = !empty($taxonomy->rest_base) ? $taxonomy->rest_base : $taxonomy->name; + + if (!isset($request["{$base}_slug"])) { + continue; + } + + array_push($args['tax_query'], [ + 'taxonomy' => $taxonomy->name, + 'field' => 'slug', + 'terms' => explode(',', $request["{$base}_slug"]), + 'include_children' => true, + 'operator' => 'IN', + ]); + } + return $args; + } +} diff --git a/app/helpers/rest-api-filter-helper.php b/app/helpers/rest-api-filter-helper.php new file mode 100644 index 0000000..a941a77 --- /dev/null +++ b/app/helpers/rest-api-filter-helper.php @@ -0,0 +1,58 @@ +post_type}_query", array $args, WP_REST_Request $request ) or filter instead + */ +class RestApiFilterHelper +{ + public function __construct() + { + add_action('rest_api_init', [$this, 'rest_api_filter_add_filters']); + } + + /** + * Add the necessary filter to each post type + **/ + public function rest_api_filter_add_filters() + { + foreach (get_post_types(array('show_in_rest' => true), 'objects') as $post_type) { + add_filter('rest_' . $post_type->name . '_query', [$this, 'rest_api_filter_add_filter_param'], 10, 2); + } + } + + /** + * Add the filter parameter + * + * @param array $args The query arguments. + * @param WP_REST_Request $request Full details about the request. + * @return array $args. + **/ + public function rest_api_filter_add_filter_param($args, $request) + { + // Bail out if no filter parameter is set. + if (empty($request['filter']) || !is_array($request['filter'])) { + return $args; + } + + $filter = $request['filter']; + + if (isset($filter['posts_per_page']) && ((int) $filter['posts_per_page'] >= 1 && (int) $filter['posts_per_page'] <= 100)) { + $args['posts_per_page'] = $filter['posts_per_page']; + } + + global $wp; + $vars = apply_filters('rest_query_vars', $wp->public_query_vars); + + // Allow valid meta query vars. + $vars = array_unique(array_merge($vars, array('meta_query', 'meta_key', 'meta_value', 'meta_compare'))); + + foreach ($vars as $var) { + if (isset($filter[$var])) { + $args[$var] = $filter[$var]; + } + } + return $args; + } +} diff --git a/app/lib/class-wp-rest-posts-controller.php b/app/lib/class-wp-rest-posts-controller.php new file mode 100644 index 0000000..8a17176 --- /dev/null +++ b/app/lib/class-wp-rest-posts-controller.php @@ -0,0 +1,357 @@ +post_type}_query", array $args, WP_REST_Request $request ) instead + */ +class ClassWpRestPostsController extends WP_REST_Posts_Controller +{ + /** + * Retrieves a collection of posts. + * Source: https://github.com/WordPress/wordpress-develop/blob/master/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php + * Memo: only change $registered = $this->get_collection_params_mod(); + * Based on commit 5383af8 + * + * @since 4.7.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_items($request) + { + // Ensure a search string is set in case the orderby is set to 'relevance'. + if (!empty($request['orderby']) && 'relevance' === $request['orderby'] && empty($request['search'])) { + return new WP_Error( + 'rest_no_search_term_defined', + __('You need to define a search term to order by relevance.'), + array('status' => 400) + ); + } + + // Ensure an include parameter is set in case the orderby is set to 'include'. + if (!empty($request['orderby']) && 'include' === $request['orderby'] && empty($request['include'])) { + return new WP_Error( + 'rest_orderby_include_missing_include', + __('You need to define an include parameter to order by include.'), + array('status' => 400) + ); + } + + // Retrieve the list of registered collection query parameters. + $registered = $this->get_collection_params_mod(); + $args = array(); + + /* + * This array defines mappings between public API query parameters whose + * values are accepted as-passed, and their internal WP_Query parameter + * name equivalents (some are the same). Only values which are also + * present in $registered will be set. + */ + $parameter_mappings = array( + 'author' => 'author__in', + 'author_exclude' => 'author__not_in', + 'exclude' => 'post__not_in', + 'include' => 'post__in', + 'menu_order' => 'menu_order', + 'offset' => 'offset', + 'order' => 'order', + 'orderby' => 'orderby', + 'page' => 'paged', + 'parent' => 'post_parent__in', + 'parent_exclude' => 'post_parent__not_in', + 'search' => 's', + 'slug' => 'post_name__in', + 'status' => 'post_status', + ); + + /* + * For each known parameter which is both registered and present in the request, + * set the parameter's value on the query $args. + */ + foreach ($parameter_mappings as $api_param => $wp_param) { + if (isset($registered[$api_param], $request[$api_param])) { + $args[$wp_param] = $request[$api_param]; + } + } + + // Check for & assign any parameters which require special handling or setting. + $args['date_query'] = array(); + + if (isset($registered['before'], $request['before'])) { + $args['date_query'][] = array( + 'before' => $request['before'], + 'column' => 'post_date', + ); + } + + if (isset($registered['modified_before'], $request['modified_before'])) { + $args['date_query'][] = array( + 'before' => $request['modified_before'], + 'column' => 'post_modified', + ); + } + + if (isset($registered['after'], $request['after'])) { + $args['date_query'][] = array( + 'after' => $request['after'], + 'column' => 'post_date', + ); + } + + if (isset($registered['modified_after'], $request['modified_after'])) { + $args['date_query'][] = array( + 'after' => $request['modified_after'], + 'column' => 'post_modified', + ); + } + + // Ensure our per_page parameter overrides any provided posts_per_page filter. + if (isset($registered['per_page'])) { + $args['posts_per_page'] = $request['per_page']; + } + + if (isset($registered['sticky'], $request['sticky'])) { + $sticky_posts = get_option('sticky_posts', array()); + if (!is_array($sticky_posts)) { + $sticky_posts = array(); + } + if ($request['sticky']) { + /* + * As post__in will be used to only get sticky posts, + * we have to support the case where post__in was already + * specified. + */ + $args['post__in'] = $args['post__in'] ? array_intersect($sticky_posts, $args['post__in']) : $sticky_posts; + + /* + * If we intersected, but there are no post IDs in common, + * WP_Query won't return "no posts" for post__in = array() + * so we have to fake it a bit. + */ + if (!$args['post__in']) { + $args['post__in'] = array(0); + } + } elseif ($sticky_posts) { + /* + * As post___not_in will be used to only get posts that + * are not sticky, we have to support the case where post__not_in + * was already specified. + */ + $args['post__not_in'] = array_merge($args['post__not_in'], $sticky_posts); + } + } + + $args = $this->prepare_tax_query($args, $request); + + // Force the post_type argument, since it's not a user input variable. + $args['post_type'] = $this->post_type; + + /** + * Filters WP_Query arguments when querying posts via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. + * + * Possible hook names include: + * + * - `rest_post_query` + * - `rest_page_query` + * - `rest_attachment_query` + * + * Enables adding extra arguments or setting defaults for a post collection request. + * + * @since 4.7.0 + * @since 5.7.0 Moved after the `tax_query` query arg is generated. + * + * @link https://developer.wordpress.org/reference/classes/wp_query/ + * + * @param array $args Array of arguments for WP_Query. + * @param WP_REST_Request $request The REST API request. + */ + $args = apply_filters("rest_{$this->post_type}_query", $args, $request); + $query_args = $this->prepare_items_query($args, $request); + $posts_query = new WP_Query(); + $query_result = $posts_query->query($query_args); + + // Allow access to all password protected posts if the context is edit. + if ('edit' === $request['context']) { + add_filter('post_password_required', array($this, 'check_password_required'), 10, 2); + } + + $posts = array(); + + foreach ($query_result as $post) { + if (!$this->check_read_permission($post)) { + continue; + } + + $data = $this->prepare_item_for_response($post, $request); + $posts[] = $this->prepare_response_for_collection($data); + } + + // Reset filter. + if ('edit' === $request['context']) { + remove_filter('post_password_required', array($this, 'check_password_required')); + } + + $page = (int) $query_args['paged']; + $total_posts = $posts_query->found_posts; + + if ($total_posts < 1) { + // Out-of-bounds, run the query again without LIMIT for total count. + unset($query_args['paged']); + + $count_query = new WP_Query(); + $count_query->query($query_args); + $total_posts = $count_query->found_posts; + } + + $max_pages = ceil($total_posts / (int) $posts_query->query_vars['posts_per_page']); + + if ($page > $max_pages && $total_posts > 0) { + return new WP_Error( + 'rest_post_invalid_page_number', + __('The page number requested is larger than the number of pages available.'), + array('status' => 400) + ); + } + + $response = rest_ensure_response($posts); + + $response->header('X-WP-Total', (int) $total_posts); + $response->header('X-WP-TotalPages', (int) $max_pages); + + $request_params = $request->get_query_params(); + $base = add_query_arg(urlencode_deep($request_params), rest_url(sprintf('%s/%s', $this->namespace, $this->rest_base))); + + if ($page > 1) { + $prev_page = $page - 1; + + if ($prev_page > $max_pages) { + $prev_page = $max_pages; + } + + $prev_link = add_query_arg('page', $prev_page, $base); + $response->link_header('prev', $prev_link); + } + if ($max_pages > $page) { + $next_page = $page + 1; + $next_link = add_query_arg('page', $next_page, $base); + + $response->link_header('next', $next_link); + } + + return $response; + } + + /** + * Prepares the 'tax_query' for a collection of posts. + * + * @since 5.7.0 + * + * @param array $args WP_Query arguments. + * @param WP_REST_Request $request Full details about the request. + * @return array Updated query arguments. + */ + private function prepare_tax_query(array $args, WP_REST_Request $request) + { + $relation = $request['tax_relation']; + + if ($relation) { + $args['tax_query'] = array('relation' => $relation); + } + + $taxonomies = wp_list_filter( + get_object_taxonomies($this->post_type, 'objects'), + array('show_in_rest' => true) + ); + + foreach ($taxonomies as $taxonomy) { + $base = !empty($taxonomy->rest_base) ? $taxonomy->rest_base : $taxonomy->name; + + $tax_include = $request[$base]; + $tax_exclude = $request[$base . '_exclude']; + + if ($tax_include) { + $terms = array(); + $include_children = false; + $operator = 'IN'; + + if (rest_is_array($tax_include)) { + $terms = $tax_include; + } elseif (rest_is_object($tax_include)) { + $terms = empty($tax_include['terms']) ? array() : $tax_include['terms']; + $include_children = !empty($tax_include['include_children']); + + if (isset($tax_include['operator']) && 'AND' === $tax_include['operator']) { + $operator = 'AND'; + } + } + + if ($terms) { + $args['tax_query'][] = array( + 'taxonomy' => $taxonomy->name, + 'field' => 'slug', // 'term_id', + 'terms' => $terms, + 'include_children' => $include_children, + 'operator' => $operator, + ); + } + } + + if ($tax_exclude) { + $terms = array(); + $include_children = false; + + if (rest_is_array($tax_exclude)) { + $terms = $tax_exclude; + } elseif (rest_is_object($tax_exclude)) { + $terms = empty($tax_exclude['terms']) ? array() : $tax_exclude['terms']; + $include_children = !empty($tax_exclude['include_children']); + } + + if ($terms) { + $args['tax_query'][] = array( + 'taxonomy' => $taxonomy->name, + 'field' => 'term_id', + 'terms' => $terms, + 'include_children' => $include_children, + 'operator' => 'NOT IN', + ); + } + } + } + + return $args; + } + + public function get_public_item_schema_mod() + { + $schema = $this->get_public_item_schema(); + $schema['properties']['categories']['items']['type'] = [ + "string", + "integer" + ]; + return $schema; + } + + public function get_collection_params_mod() + { + $params = $this->get_collection_params(); + $new = [ + "title" => "Term Slug", + "description" => "Match terms with the listed Slug.", + "type" => "array", + "items" => [ + "type" => "string" + ] + ]; + $params['categories']['oneOf'][0] = $new; + return $params; + } +} diff --git a/app/routers/api-router.php b/app/routers/api-router.php index bb7b32f..6d588ba 100644 --- a/app/routers/api-router.php +++ b/app/routers/api-router.php @@ -13,6 +13,7 @@ use Sakura\Controllers\CategoryController; use Sakura\Controllers\TagController; use Sakura\Controllers\CommentController; use Sakura\Lib\ClassWpRestCommentsController; +use Sakura\Lib\ClassWpRestPostsController; class ApiRouter extends WP_REST_Controller { @@ -135,6 +136,29 @@ class ApiRouter extends WP_REST_Controller 'schema' => array($this, 'get_public_item_schema'), ) ); + + // @deprecated using PostQueryHelper instaed + // $rest_posts_controller = new ClassWpRestPostsController('post'); + // custom get posts by category slugs + // register_rest_route( + // $this->namespace, + // '/posts', + // array( + // array( + // 'methods' => WP_REST_Server::READABLE, + // 'callback' => array($rest_posts_controller, 'get_items'), + // 'permission_callback' => array($rest_posts_controller, 'get_items_permissions_check'), + // 'args' => $rest_posts_controller->get_collection_params_mod(), + // ), + // array( + // 'methods' => WP_REST_Server::CREATABLE, + // 'callback' => array($rest_posts_controller, 'create_item'), + // 'permission_callback' => array($rest_posts_controller, 'create_item_permissions_check'), + // 'args' => $rest_posts_controller->get_endpoint_args_for_item_schema(WP_REST_Server::CREATABLE), + // ), + // 'schema' => array($rest_posts_controller, 'get_public_item_schema_mod'), + // ) + // ); }); } diff --git a/src/api/Wp/v2/index.ts b/src/api/Wp/v2/index.ts index 46ae761..3ff5370 100644 --- a/src/api/Wp/v2/index.ts +++ b/src/api/Wp/v2/index.ts @@ -1,8 +1,6 @@ import request, { AxiosPromise } from '@/utils/http' import snakecaseKeys from 'snakecase-keys' -type WpPostObjectFilter = number | string | number[] | string[] - /** * GET /wp/v2/posts * https://developer.wordpress.org/rest-api/reference/posts/#arguments @@ -16,8 +14,8 @@ export interface GetPostParams { author?: string | number authorExclude?: string | number before?: string // ISO8601 - exclude?: WpPostObjectFilter // TODO: check this - include?: WpPostObjectFilter // TODO: check this + exclude?: number | number[] + include?: number | number[] offset?: number order?: 'asc' | 'desc' // default: desc orderby?: @@ -31,13 +29,15 @@ export interface GetPostParams { | 'slug' | 'include_slugs' | 'title' // default: date - slug?: WpPostObjectFilter // TODO: check this + slug?: string status?: string // default: 'publish' taxRelation?: 'AND' | 'OR' - categories?: WpPostObjectFilter // TODO: check this - categoriesExclude?: WpPostObjectFilter // TODO: check this - tags?: WpPostObjectFilter // TODO: check this - tagsExclude?: WpPostObjectFilter // TODO: check this + categories?: number | number[] + categoriesExclude?: number | number[] + categoriesSlug?: string + tags?: number | number[] + tagsExclude?: number | number[] + tagsSlug?: string sticky?: boolean // TODO: check this } @@ -48,8 +48,10 @@ export interface GetPageParams | 'taxRelation' | 'categories' | 'categoriesExclude' + | 'categoriesSlug' | 'tags' | 'tagsExclude' + | 'tagsSlug' | 'sticky' > { menuOrder: any // TODO: check this @@ -79,8 +81,8 @@ export interface GetCommentParams { authorExclude?: string | number author_email?: string before?: string // ISO8601 - exclude?: WpPostObjectFilter // TODO: check this - include?: WpPostObjectFilter // TODO: check this + exclude?: number | number[] // TODO: check this + include?: number | number[] // TODO: check this offset?: number order?: 'asc' | 'desc' // default: desc orderby?: 'date' | 'date_gmt' | 'id' | 'include' | 'post' | 'parent' | 'type' // default: date diff --git a/src/components/image/Image.vue b/src/components/image/Image.vue index 12ce17f..9169f75 100644 --- a/src/components/image/Image.vue +++ b/src/components/image/Image.vue @@ -8,7 +8,13 @@ @error="handleError" @load="handleLoad" /> - + diff --git a/src/components/lists/postThembList/PostThumbList.vue b/src/components/lists/postThumbList/PostThumbList.vue similarity index 93% rename from src/components/lists/postThembList/PostThumbList.vue rename to src/components/lists/postThumbList/PostThumbList.vue index 99c589c..217ee05 100644 --- a/src/components/lists/postThembList/PostThumbList.vue +++ b/src/components/lists/postThumbList/PostThumbList.vue @@ -29,6 +29,12 @@ export default defineComponent({ page: { type: Number, default: 1 }, perPage: { type: Number, default: 10 }, autoLoad: { type: Boolean, default: true }, + fetchParameters: { + type: Object, + default: () => { + return {} + }, + }, }, setup(props) { const [listContainerRef, setListContainerRef] = useElementRef() @@ -64,7 +70,12 @@ export default defineComponent({ fetchPost({ state: postsStore, namespace: props.namespace, - opts: { page: currentPage.value, perPage: props.perPage, context: 'embed' }, + opts: { + page: currentPage.value, + perPage: props.perPage, + context: 'embed', + ...props.fetchParameters, + }, }).then(() => { get() setFetchStatus('done') diff --git a/src/views/Category.vue b/src/views/Category.vue index 6614b01..e67b42b 100644 --- a/src/views/Category.vue +++ b/src/views/Category.vue @@ -1,11 +1,31 @@ diff --git a/src/views/Home.vue b/src/views/Home.vue index a05ebc7..985e932 100644 --- a/src/views/Home.vue +++ b/src/views/Home.vue @@ -3,7 +3,7 @@
- +
@@ -11,21 +11,11 @@ diff --git a/src/views/Tag.vue b/src/views/Tag.vue index 0dfe28a..ddde4ee 100644 --- a/src/views/Tag.vue +++ b/src/views/Tag.vue @@ -1,11 +1,32 @@