Skip to content

Commit 6b8a1c8

Browse files
zackkatzdoekenorg
andauthored
Add support for WP_Query data source (#14)
* Add support for WP_Query data source This allows for directly querying posts. ``` $wp_query_data_source = new \DataKit\Plugin\Data\WPQueryDataSource( [ 'post_type' => 'page', ] ); ``` * Refactor code and translation domain --------- Co-authored-by: Doeke Norg <[email protected]>
1 parent 8309b1b commit 6b8a1c8

File tree

1 file changed

+323
-0
lines changed

1 file changed

+323
-0
lines changed

src/Data/WPQueryDataSource.php

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
<?php
2+
3+
namespace DataKit\Plugin\Data;
4+
5+
use DataKit\DataViews\Data\BaseDataSource;
6+
use DataKit\DataViews\Data\MutableDataSource;
7+
use DataKit\DataViews\Data\Exception\DataNotFoundException;
8+
use DataKit\DataViews\Data\Exception\ActionForbiddenException;
9+
10+
/**
11+
* A data source backed by WordPress' WP_Query.
12+
*
13+
* Provides an interface to retrieve and manipulate posts, pages, and custom post types.
14+
*
15+
* @since $ver$
16+
*/
17+
final class WPQueryDataSource extends BaseDataSource implements MutableDataSource {
18+
/**
19+
* The base WP_Query instance that queries will use as a starting point (except for count queries).
20+
*
21+
* @since $ver$
22+
*
23+
* @var \WP_Query
24+
*/
25+
private \WP_Query $base_query;
26+
27+
/**
28+
* Stores the query arguments for later execution.
29+
*
30+
* @since $ver$
31+
*
32+
* @var array
33+
*/
34+
private array $query_args = [];
35+
36+
/**
37+
* Default query arguments.
38+
*
39+
* @since $ver$
40+
*
41+
* @var array
42+
*/
43+
private array $default_args = [
44+
'post_type' => 'any',
45+
'post_status' => 'publish',
46+
'ignore_sticky_posts' => true,
47+
'no_found_rows' => true,
48+
'suppress_filters' => false,
49+
];
50+
51+
/**
52+
* Constructor for WPQueryDataSource.
53+
*
54+
* @since $ver$
55+
*
56+
* @param \WP_Query|string|array|null $query Optional query parameter.
57+
*/
58+
public function __construct( $query = null ) {
59+
$this->base_query = new \WP_Query();
60+
$this->store_query_args( $query );
61+
}
62+
63+
/**
64+
* Stores the query arguments instead of immediately executing them.
65+
*
66+
* @since $ver$
67+
*
68+
* @param \WP_Query|string|array|null $query Optional query parameter.
69+
*/
70+
private function store_query_args( $query ): void {
71+
if ( $query instanceof \WP_Query ) {
72+
$this->query_args = $query->query_vars;
73+
} elseif ( is_string( $query ) || is_array( $query ) ) {
74+
$this->query_args = wp_parse_args( $query );
75+
}
76+
77+
// Merge with default arguments.
78+
$this->query_args = wp_parse_args( $this->query_args, $this->default_args );
79+
80+
// Ensure security-related arguments are properly set.
81+
$this->sanitize_query_args();
82+
}
83+
84+
/**
85+
* Sanitize and set security-related query arguments.
86+
*
87+
* @since $ver$
88+
*/
89+
private function sanitize_query_args(): void {
90+
// Ensure 'suppress_filters' is always false for security.
91+
$this->query_args['suppress_filters'] = false;
92+
93+
// Limit 'post_status' to viewable statuses if not explicitly set.
94+
if ( ! isset( $this->query_args['post_status'] ) ) {
95+
$this->query_args['post_status'] = array_values( get_post_stati( [ 'public' => true ] ) );
96+
}
97+
98+
// Set 'post_type' to 'any' if not explicitly set.
99+
if ( ! isset( $this->query_args['post_type'] ) ) {
100+
$this->query_args['post_type'] = 'any';
101+
}
102+
103+
// Ensure 'perm' is set to 'readable' if not explicitly set.
104+
if ( ! isset( $this->query_args['perm'] ) ) {
105+
$this->query_args['perm'] = 'readable';
106+
}
107+
108+
// Remove any attempts to modify SQL directly.
109+
unset(
110+
$this->query_args['posts_where'],
111+
$this->query_args['posts_groupby'],
112+
$this->query_args['posts_join'],
113+
$this->query_args['posts_orderby']
114+
);
115+
}
116+
117+
/**
118+
* Run the query to fetch post IDs based on the limit, offset, and sorting criteria.
119+
*
120+
* @since $ver$
121+
*
122+
* @param int $limit The number of posts to retrieve.
123+
* @param int $offset The number of posts to skip.
124+
*
125+
* @return string[] Array of post IDs.
126+
*/
127+
public function get_data_ids( int $limit = 100, int $offset = 0 ): array {
128+
$query_args = array_merge(
129+
$this->query_args,
130+
[
131+
'fields' => 'ids',
132+
's' => $this->get_search_string(),
133+
'posts_per_page' => $limit,
134+
'offset' => $offset,
135+
],
136+
$this->get_sorting()
137+
);
138+
139+
$this->base_query->query( $query_args );
140+
141+
return array_map( 'strval', $this->base_query->posts );
142+
}
143+
144+
/**
145+
* Returns the search string.
146+
*
147+
* @since $ver$
148+
*
149+
* @return string
150+
*/
151+
private function get_search_string(): string {
152+
return trim( ( $this->query_args['s'] ?? '' ) . ' ' . $this->search );
153+
}
154+
155+
/**
156+
* @inheritDoc
157+
*
158+
* @since $ver$
159+
*/
160+
public function id(): string {
161+
return sprintf( 'wpquery-%s', wp_hash( wp_json_encode( $this->query_args ) ) );
162+
}
163+
164+
/**
165+
* Returns the sorting criteria based on the sort object.
166+
*
167+
* @since $ver$
168+
*
169+
* @return array The sorting criteria.
170+
*/
171+
private function get_sorting(): array {
172+
if ( ! $this->sort ) {
173+
return [];
174+
}
175+
176+
$sort = $this->sort->to_array();
177+
178+
return [
179+
'orderby' => $sort['field'],
180+
'order' => strtoupper( $sort['direction'] ),
181+
];
182+
}
183+
184+
/**
185+
* {@inheritDoc}
186+
*
187+
* @since $ver$
188+
*
189+
* @return array The provided data.
190+
*
191+
* @throws DataNotFoundException If the post does not exist.
192+
* @throws ActionForbiddenException If the user doesn't have permission to read the post.
193+
*/
194+
public function get_data_by_id( string $id ): array {
195+
$post = get_post( (int) $id );
196+
197+
if ( ! $post ) {
198+
throw DataNotFoundException::with_id( $this, $id );
199+
}
200+
201+
// Check if the current user has permission to read this post.
202+
if ( ! current_user_can( 'read_post', $post->ID ) ) {
203+
throw new ActionForbiddenException(
204+
$this,
205+
// translators: %d is the post ID.
206+
sprintf( esc_html__( 'You do not have permission to read post #%d.', 'datakit' ), $post->ID )
207+
);
208+
}
209+
210+
return [
211+
'ID' => $post->ID,
212+
'post_author' => $post->post_author,
213+
'post_date' => $post->post_date,
214+
'post_date_gmt' => $post->post_date_gmt,
215+
'post_content' => $post->post_content,
216+
'post_title' => $post->post_title,
217+
'post_excerpt' => $post->post_excerpt,
218+
'post_status' => $post->post_status,
219+
'comment_status' => $post->comment_status,
220+
'ping_status' => $post->ping_status,
221+
'post_password' => $post->post_password,
222+
'post_name' => $post->post_name,
223+
'to_ping' => $post->to_ping,
224+
'pinged' => $post->pinged,
225+
'post_modified' => $post->post_modified,
226+
'post_modified_gmt' => $post->post_modified_gmt,
227+
'post_content_filtered' => $post->post_content_filtered,
228+
'post_parent' => $post->post_parent,
229+
'guid' => $post->guid,
230+
'menu_order' => $post->menu_order,
231+
'post_type' => $post->post_type,
232+
'post_mime_type' => $post->post_mime_type,
233+
'comment_count' => $post->comment_count,
234+
'permalink' => get_permalink( $post->ID ),
235+
];
236+
}
237+
238+
/**
239+
* @inheritDoc
240+
*
241+
* @since $ver$
242+
*/
243+
public function count(): int {
244+
$query = new \WP_Query(
245+
array_merge(
246+
$this->query_args,
247+
[
248+
'fields' => 'ids',
249+
'no_found_rows' => false,
250+
's' => $this->get_search_string(),
251+
]
252+
)
253+
);
254+
255+
return (int) $query->found_posts;
256+
}
257+
258+
/**
259+
* @inheritDoc
260+
*
261+
* @since $ver$
262+
*/
263+
public function can_delete(): bool {
264+
return current_user_can( 'delete_posts' );
265+
}
266+
267+
/**
268+
* @inheritDoc
269+
*
270+
* @since $ver$
271+
*
272+
* @throws DataNotFoundException If the post does not exist before deletion.
273+
* @throws ActionForbiddenException If the user doesn't have permission to delete the post.
274+
*/
275+
public function delete_data_by_id( string ...$ids ): void {
276+
foreach ( $ids as $id ) {
277+
$post = get_post( (int) $id );
278+
279+
if ( ! $post ) {
280+
throw DataNotFoundException::with_id( $this, $id );
281+
}
282+
283+
if ( ! current_user_can( 'delete_post', $post->ID ) ) {
284+
throw new ActionForbiddenException(
285+
$this,
286+
// translators: %d is the post ID.
287+
sprintf( esc_html__( 'You do not have permission to delete post #%d.', 'datakit' ), $post->ID )
288+
);
289+
}
290+
291+
wp_delete_post( $post->ID, true );
292+
}
293+
}
294+
295+
/**
296+
* @inheritDoc
297+
*
298+
* @since $ver$
299+
*/
300+
public function get_fields(): array {
301+
return [
302+
'ID' => __( 'ID', 'datakit' ),
303+
'post_author' => __( 'Author', 'datakit' ),
304+
'post_date' => __( 'Date', 'datakit' ),
305+
'post_content' => __( 'Content', 'datakit' ),
306+
'post_title' => __( 'Title', 'datakit' ),
307+
'post_excerpt' => __( 'Excerpt', 'datakit' ),
308+
'post_status' => __( 'Status', 'datakit' ),
309+
'comment_status' => __( 'Comment Status', 'datakit' ),
310+
'ping_status' => __( 'Ping Status', 'datakit' ),
311+
'post_password' => __( 'Password', 'datakit' ),
312+
'post_name' => __( 'Slug', 'datakit' ),
313+
'post_modified' => __( 'Modified Date', 'datakit' ),
314+
'post_parent' => __( 'Parent', 'datakit' ),
315+
'guid' => __( 'GUID', 'datakit' ),
316+
'menu_order' => __( 'Menu Order', 'datakit' ),
317+
'post_type' => __( 'Post Type', 'datakit' ),
318+
'post_mime_type' => __( 'MIME Type', 'datakit' ),
319+
'comment_count' => __( 'Comment Count', 'datakit' ),
320+
'permalink' => __( 'Permalink', 'datakit' ),
321+
];
322+
}
323+
}

0 commit comments

Comments
 (0)