PluginProbe
Meow Gallery / 5.5.5
Meow Gallery v5.5.5
5.5.5 5.5.4 5.5.3 5.5.2 5.5.1 5.5.0 5.4.9 5.4.8 5.4.7 4.1.5 4.1.6 4.1.7 4.1.8 4.1.9 4.2.0 4.2.1 4.2.2 4.2.3 4.2.4 4.2.5 4.2.6 4.2.7 4.2.8 4.2.9 4.3.0 All 157 releases
meow-gallery / classes / rest.php

rest.php in Meow Gallery 5.5.5, at classes/rest.php

758 lines 25.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 class Meow_MGL_Rest
4 {
5 private $core;
6 private $namespace = 'meow-gallery/v1';
7
8 // Gallery attributes that decide *which* media a gallery shows. They must never be taken from
9 // an untrusted request: see rest_load_gallery_collection().
10 private static $source_atts = [
11 'collection', 'id', 'ids', 'include', 'tags', 'posts', 'latest_posts', 'attachments',
12 'rml', 'wplr-collection', 'meow',
13 ];
14
15 public function __construct( $core ) {
16 $this->core = $core;
17
18 // FOR DEBUG
19 // For experiencing the UI behavior on a slower install.
20 // sleep( 1 );
21 // For experiencing the UI behavior on a buggy install.
22 // trigger_error( "Error", E_USER_ERROR );
23 // trigger_error( "Warning", E_USER_WARNING );
24 // trigger_error( "Notice", E_USER_NOTICE );
25 // trigger_error( "Deprecated", E_USER_DEPRECATED );
26
27 add_action( 'rest_api_init', array( $this, 'rest_api_init' ) );
28 }
29
30
31 function rest_api_init( ) {
32
33 // Settings
34 register_rest_route( $this->namespace, '/update_option/', array(
35 'methods' => 'POST',
36 'permission_callback' => array( $this->core, 'can_access_settings' ),
37 'callback' => array( $this, 'rest_update_option' )
38 ) );
39 register_rest_route( $this->namespace, '/all_settings/', array(
40 'methods' => 'GET',
41 'permission_callback' => array( $this->core, 'can_access_settings' ),
42 'callback' => array( $this, 'rest_all_settings' )
43 ) );
44 register_rest_route( $this->namespace, '/reset_options', array(
45 'methods' => 'POST',
46 'permission_callback' => array( $this->core, 'can_access_settings' ),
47 'callback' => array( $this, 'rest_reset_options' )
48 ) );
49
50
51 // Gallery Manager
52 register_rest_route( $this->namespace, '/latest_photos', array(
53 'methods' => 'GET',
54 'permission_callback' => array( $this->core, 'can_access_settings' ),
55 'callback' => array( $this, 'rest_latest_photos' ),
56 'args' => array(
57 'search' => array( 'required' => false ),
58 'offset' => array( 'required' => false, 'default' => 0 ),
59 'except' => array( 'required' => false ),
60 )
61 ) );
62 register_rest_route( $this->namespace, '/save_shortcode', array(
63 'methods' => 'POST',
64 'permission_callback' => array( $this->core, 'can_access_settings' ),
65 'callback' => array( $this, 'rest_save_shortcode' ),
66 ) );
67 register_rest_route( $this->namespace, '/remove_shortcode', array(
68 'methods' => 'POST',
69 'permission_callback' => array( $this->core, 'can_access_settings' ),
70 'callback' => array( $this, 'rest_remove_shortcode' ),
71 ) );
72 register_rest_route( $this->namespace, '/update_gallery_rank', array(
73 'methods' => 'POST',
74 'permission_callback' => array( $this->core, 'can_access_settings' ),
75 'callback' => array( $this, 'rest_update_gallery_rank' ),
76 ) );
77 register_rest_route( $this->namespace, '/rml_folders', array(
78 'methods' => 'GET',
79 'permission_callback' => array( $this->core, 'can_access_settings' ),
80 'callback' => array( $this, 'rest_rml_folders' ),
81 ) );
82
83
84 register_rest_route( $this->namespace, '/fetch_shortcodes', array(
85 'methods' => 'POST',
86 'permission_callback' => array( $this->core, 'can_access_features' ),
87 'callback' => array( $this, 'rest_fetch_shortcodes' ),
88 ) );
89 register_rest_route( $this->namespace, '/fetch_gallery_items', array(
90 'methods' => 'POST',
91 'permission_callback' => array( $this->core, 'can_access_features' ),
92 'callback' => array( $this, 'rest_fetch_gallery_items' ),
93 ) );
94
95
96
97 //Collection Manager
98 register_rest_route( $this->namespace, '/save_collection', array(
99 'methods' => 'POST',
100 'permission_callback' => array( $this->core, 'can_access_settings' ),
101 'callback' => array( $this, 'rest_save_collection' ),
102 ) );
103 register_rest_route( $this->namespace, '/remove_collection', array(
104 'methods' => 'POST',
105 'permission_callback' => array( $this->core, 'can_access_settings' ),
106 'callback' => array( $this, 'rest_remove_collection' ),
107 ) );
108
109 register_rest_route( $this->namespace, '/fetch_collections', array(
110 'methods' => 'POST',
111 'permission_callback' => array( $this->core, 'can_access_features' ),
112 'callback' => array( $this, 'rest_fetch_collections' ),
113 ) );
114
115 register_rest_route( $this->namespace, '/load_gallery_collection', array(
116 'methods' => 'POST',
117 'permission_callback' => '__return_true',
118 'callback' => array( $this, 'rest_load_gallery_collection' ),
119 ) );
120
121 // Gutenberg Block
122 register_rest_route( $this->namespace, '/preview', array(
123 'methods' => 'POST',
124 'permission_callback' => array( $this->core, 'can_access_features' ),
125 'callback' => array( $this, 'preview' ),
126 ) );
127
128 // Gallery
129 register_rest_route( $this->namespace, '/images/', array(
130 'methods' => 'POST',
131 'permission_callback' => array( $this, 'can_load_images' ),
132 'callback' => array( $this, 'rest_images' )
133 ) );
134
135 register_rest_route( $this->namespace, '/fetch_posts', array(
136 'methods' => 'POST',
137 'permission_callback' => array( $this->core, 'can_access_features' ),
138 'callback' => array( $this, 'rest_fetch_posts' ),
139 'args' => array(
140 'search' => array( 'required' => false ),
141 'offset' => array( 'required' => false, 'default' => 0 ),
142 'limit' => array( 'required' => false, 'default' => 10 ),
143 )
144 ) );
145 }
146
147 // The /images/ route feeds the infinite scroll and nothing else: when it is off (the default,
148 // and always in the free version) the gallery is rendered whole and the front-end never calls
149 // this. It has to stay open to visitors when infinite scroll IS on, but leaving it open
150 // everywhere exposed the title, caption and URL of any attachment ID, including attachments of
151 // posts that are not published.
152 public function can_load_images() {
153 $infinite = class_exists( 'MeowPro_MGL_Core' ) && Meow_MGL_Core::get_plugin_option( 'infinite', false );
154 return apply_filters( 'mgl_allow_load_images', (bool) $infinite );
155 }
156
157 function preview( WP_REST_Request $request ) {
158 $params = $request->get_body( );
159 $params = json_decode( $params );
160 $params->ids = implode( ',', $params->ids );
161 $atts = ( array ) $params;
162
163 $full = !empty( $atts['full'] );
164 unset( $atts['full'] );
165
166 $is_collection = isset( $atts['collection'] ) && !empty( $atts['collection'] );
167 if ( $is_collection ) {
168 $html = $this->core->render_collection( $atts['collection'] );
169 $counts = [ 'total' => 0, 'shown' => 0 ];
170 } else {
171 $this->core->last_preview_counts = [ 'total' => 0, 'shown' => 0 ];
172 if ( $full ) {
173 $this->core->preview_cutoff = PHP_INT_MAX;
174 }
175 $html = $this->core->gallery( $atts, [ 'isPreview' => true ] );
176 $counts = $this->core->last_preview_counts;
177 }
178
179
180 return new WP_REST_Response( [
181 'success' => true,
182 'data' => $html,
183 'total' => intval( $counts['total'] ),
184 'shown' => intval( $counts['shown'] ),
185 ], 200 );
186 }
187
188 function rest_load_gallery_collection( $request ) {
189 try {
190 $params = $request->get_json_params( );
191 $gallery_id = $params['id'] ?? '';
192 $search_slug = $params['search_slug'] ?? '';
193 $gallery_atts = $params['gallery_atts'] ?? array();
194 $gallery_atts = is_array( $gallery_atts ) ? $gallery_atts : array();
195
196 $key = [
197 'gallery_id' => 'id',
198 'wplr_collection_id' => 'wplr-collection',
199 'rml' => 'rml',
200 ];
201
202 // This route is public (visitors open galleries from a collection), so everything it
203 // receives is untrusted. The gallery to render is decided by 'search_slug' + 'id'
204 // only: the caller-supplied attributes are stripped of anything that could point the
205 // gallery at other content. Without this, 'collection' could be used to inject
206 // arbitrary shortcodes (reported by JunHee CHO, 2026-09).
207 if ( !isset( $key[ $search_slug ] ) ) {
208 return new WP_REST_Response( [ 'success' => false, 'message' => __( 'Unknown gallery source.', MGL_DOMAIN ) ], 400 );
209 }
210 $gallery_atts = array_diff_key( $gallery_atts, array_flip( self::$source_atts ) );
211
212 // The RML source is a folder path, the others are identifiers.
213 if ( $search_slug !== 'rml' ) {
214 $gallery_id = Meow_MGL_Core::sanitize_id( $gallery_id );
215 if ( $gallery_id === '' ) {
216 return new WP_REST_Response( [ 'success' => false, 'message' => __( 'Invalid gallery ID.', MGL_DOMAIN ) ], 400 );
217 }
218 }
219
220 $shortcode_atts = array();
221 $shortcode_atts[ $key[$search_slug] ] = $gallery_id;
222 $shortcode_atts = [...$shortcode_atts, ...$gallery_atts];
223
224 $html = $this->core->gallery( $shortcode_atts, [ 'isPreview' => false, 'isRest' => true ] );
225 $mwlData = json_encode( $this->core->get_rewritten_mwl_data( ) );
226 return new WP_REST_Response( [ 'success' => true, 'data' => $html, 'mwl_data' => $mwlData ], 200 );
227 }
228 catch ( Exception $e ) {
229 return new WP_REST_Response( [ 'success' => false, 'message' => $e->getMessage( ) ], 500 );
230 }
231 }
232
233 function rest_all_settings( ) {
234 return new WP_REST_Response( [ 'success' => true, 'data' => $this->core->get_all_options( ) ], 200 );
235 }
236
237 function rest_rml_folders( ) {
238 if ( ! Meow_MGL_RML::is_available() ) {
239 return new WP_REST_Response( [ 'success' => true, 'available' => false, 'data' => [] ], 200 );
240 }
241 return new WP_REST_Response( [ 'success' => true, 'available' => true, 'data' => Meow_MGL_RML::get_all_folders() ], 200 );
242 }
243
244 function rest_reset_options( ) {
245 $this->core->reset_options( );
246 return new WP_REST_Response( [ 'success' => true, 'options' => $this->core->get_all_options( ) ], 200 );
247 }
248
249 function rest_save_shortcode( $request ) {
250 try {
251 global $wpdb;
252 $params = $request->get_json_params( );
253
254 $id = $params['id'];
255 $medias = Meow_MGL_Core::normalize_medias( $params['medias'] ?? null );
256 $name = $params['name'];
257 $layout = $params['layout'];
258 $description = $params['description'];
259 $posts = $params['posts'];
260 $latest_posts = $params['latest_posts'];
261 $tags = $params['tags'];
262 $dynamic_source = $params['dynamic_source'];
263 $lead_image_id = $params['lead_image_id'];
264 $order_by = $params['order_by'];
265 $is_post_mode = $params['is_post_mode'];
266 $is_hero_mode = $params['is_hero_mode'];
267 $rml = $params['rml'] ?? null;
268
269 if ( !$name ) {
270 throw new Exception( __( 'Please enter a name for your shortcode.', MGL_DOMAIN ));
271 }
272
273 if ( !$is_post_mode && empty( $medias['thumbnail_ids'] ) ) {
274 throw new Exception( __( 'Please select at least one image.', MGL_DOMAIN ));
275 }
276
277 if ( $is_post_mode && $dynamic_source === 'posts' && ( !$posts && !$latest_posts )) {
278 throw new Exception( __( 'Please select at least one post.', MGL_DOMAIN ));
279 }
280
281 if ( $is_post_mode && $dynamic_source === 'tags' && !$tags ) {
282 throw new Exception( __( 'Please enter at least one tag.', MGL_DOMAIN ));
283 }
284
285 if ( $is_post_mode && $dynamic_source === 'rml' && empty( $rml ) ) {
286 throw new Exception( __( 'Please select a Real Media Library folder.', MGL_DOMAIN ));
287 }
288
289 if ( $is_hero_mode && !$is_post_mode ) {
290 throw new Exception( __( 'Hero mode is only available for post mode.', MGL_DOMAIN ));
291 }
292
293 if ( !$id || $id == '' ) {
294 $id = $this->core->generate_uniqid( 10 );
295 }
296
297 $shortcodes_table = $wpdb->prefix . 'mgl_gallery_shortcodes';
298 Meow_MGL_Migrations::check_db();
299
300 // Check if the record exists
301 $exists = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( * ) FROM $shortcodes_table WHERE id = %s", $id ));
302
303 $data = [
304 'name' => $name,
305 'description' => $description,
306 'layout' => $layout,
307 'medias' => serialize( $medias ),
308 'lead_image_id' => $lead_image_id,
309 'order_by' => $order_by,
310 'is_post_mode' => $is_post_mode ? 1 : 0,
311 'is_hero_mode' => $is_hero_mode ? 1 : 0,
312 'posts' => $posts ? serialize( $posts ) : null,
313 'latest_posts' => $latest_posts,
314 'tags' => serialize( $tags ),
315 'dynamic_source' => $dynamic_source,
316 'rml' => $rml
317 ];
318
319 if ( $exists ) {
320 // Update existing record
321 $wpdb->update(
322 $shortcodes_table,
323 $data,
324 ['id' => $id]
325 );
326 } else {
327 // Insert new record
328 $data['id'] = $id;
329 $wpdb->insert( $shortcodes_table, $data );
330 }
331
332
333 return new WP_REST_Response( ['success' => true, 'message' => 'Shortcode created.'], 200 );
334 } catch ( Exception $e ) {
335 return new WP_REST_Response( ['success' => false, 'message' => $e->getMessage( )], 500 );
336 }
337 }
338
339 function rest_remove_collection( $request ) {
340 try {
341 global $wpdb;
342 $params = $request->get_json_params( );
343 $id = $params['id'];
344
345 $collections_table = $wpdb->prefix . 'mgl_collections';
346 Meow_MGL_Migrations::check_db();
347
348 $wpdb->delete( $collections_table, ['id' => $id] );
349
350
351 return new WP_REST_Response( ['success' => true, 'message' => 'Collection removed.'], 200 );
352 } catch ( Exception $e ) {
353 return new WP_REST_Response( ['success' => false, 'message' => $e->getMessage( )], 500 );
354 }
355 }
356
357 function rest_save_collection( $request ) {
358 try {
359 global $wpdb;
360 $params = $request->get_json_params( );
361
362 $id = $params['id'];
363 $name = $params['name'];
364 $layout = $params['layout'];
365 $galleries_ids = $params['galleries_ids'];
366 $description = $params['description'];
367
368 if ( !$name ) {
369 throw new Exception( __( 'Please enter a name for your collection.', MGL_DOMAIN ));
370 }
371
372 if ( !$galleries_ids || !count( $galleries_ids )) {
373 throw new Exception( __( 'Please select at least one gallery.', MGL_DOMAIN ));
374 }
375
376 if ( !$id || $id == '' ) {
377 $id = $this->core->generate_uniqid( 10 );
378 }
379
380 $collections_table = $wpdb->prefix . 'mgl_collections';
381 Meow_MGL_Migrations::check_db();
382
383 // Check if the record exists
384 $exists = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( * ) FROM $collections_table WHERE id = %s", $id ));
385
386 $data = [
387 'name' => $name,
388 'description' => $description,
389 'layout' => $layout,
390 'galleries_ids' => serialize( $galleries_ids )
391 ];
392
393 if ( $exists ) {
394 // Update existing record
395 $wpdb->update(
396 $collections_table,
397 $data,
398 ['id' => $id]
399 );
400 } else {
401 // Insert new record
402 $data['id'] = $id;
403 $wpdb->insert( $collections_table, $data );
404 }
405
406
407 return new WP_REST_Response( ['success' => true, 'message' => 'Collection created.'], 200 );
408 } catch ( Exception $e ) {
409 return new WP_REST_Response( ['success' => false, 'message' => $e->getMessage( )], 500 );
410 }
411 }
412
413
414 function rest_fetch_collections( $request ) {
415 try {
416 $params = $request->get_json_params( );
417
418 $offset = isset( $params['offset'] ) ? $params['offset'] : 0;
419 $limit = isset( $params['limit'] ) ? $params['limit'] : 10;
420 $sort_updated = $params['sort']['by']; // desc, asc
421 $page = isset( $params['page'] ) ? $params['page'] : 1;
422 $order = $sort_updated === 'desc' ? 'DESC' : 'ASC';
423 $search = isset( $params['search'] ) ? $params['search'] : '';
424
425 $res = $this->core->get_collections( $offset, $limit, $order, $page, $search );
426 $collections = $res['collections'];
427 $total = $res['total'];
428
429 return new WP_REST_Response( ['success' => true, 'data' => $collections, 'total' => $total], 200 );
430 }
431 catch ( Exception $e ) {
432 return new WP_REST_Response( ['success' => false, 'message' => $e->getMessage( )], 500 );
433 }
434 }
435
436 function rest_fetch_gallery_items( $request ) {
437 try {
438 global $wpdb;
439 $params = $request->get_json_params( );
440 $galleryIds = $params['galleryIds'];
441
442 $shortcodes_table = $wpdb->prefix . 'mgl_gallery_shortcodes';
443 Meow_MGL_Migrations::check_db();
444
445 $galleries = [];
446 if ( !empty( $galleryIds )) {
447 $ids_str = "'" . implode( "','", array_map( 'esc_sql', $galleryIds )) . "'";
448 $query = "SELECT * FROM $shortcodes_table WHERE id IN ( $ids_str )";
449 $results = $wpdb->get_results( $query, ARRAY_A );
450
451 foreach ( $results as $gallery ) {
452 // Transform database format to match expected format
453 $galleries[$gallery['id']] = [
454 'name' => $gallery['name'],
455 'description' => $gallery['description'],
456 'layout' => $gallery['layout'],
457 'medias' => Meow_MGL_Core::hydrate_medias( maybe_unserialize( $gallery['medias'] ) ),
458 'is_post_mode' => ( bool )$gallery['is_post_mode'],
459 'hero' => ( bool )$gallery['is_hero_mode'],
460 'posts' => $gallery['posts'] ? unserialize( $gallery['posts'] ) : null,
461 'latest_posts' => $gallery['latest_posts'],
462 'tags' => unserialize( $gallery['tags'] ),
463 'dynamic_source' => $gallery['dynamic_source'],
464 'updated' => strtotime( $gallery['updated_at'] )
465 ];
466 }
467 }
468
469 return new WP_REST_Response( ['success' => true, 'data' => $galleries], 200 );
470 } catch ( Exception $e ) {
471 return new WP_REST_Response( ['success' => false, 'message' => $e->getMessage( )], 500 );
472 }
473 }
474
475 function rest_fetch_shortcodes( $request ) {
476 try {
477 $params = $request->get_json_params( );
478
479 $offset = isset( $params['offset'] ) ? $params['offset'] : 0;
480 $limit = isset( $params['limit'] ) ? $params['limit'] : 10;
481 $page = isset( $params['page'] ) ? $params['page'] : 1;
482
483 $search = isset( $params['search'] ) ? $params['search'] : '';
484
485 $sort_by = $params['sort']['accessor'] ?? null;
486 $order_by = strtoupper( $params['sort']['by'] ); // desc, asc
487
488 $res = $this->core->get_galleries( $offset, $limit, $order_by, $sort_by, $page, $search );
489 $shortcodes = $res['galleries'];
490 $total = $res['total'];
491
492 return new WP_REST_Response( ['success' => true, 'data' => $shortcodes, 'total' => $total], 200 );
493 }
494 catch ( Exception $e ) {
495 return new WP_REST_Response( ['success' => false, 'message' => $e->getMessage( )], 500 );
496 }
497 }
498
499 function rest_remove_shortcode( $request ) {
500 try {
501 global $wpdb;
502 $params = $request->get_json_params( );
503 $id = $params['id'];
504
505 $shortcodes_table = $wpdb->prefix . 'mgl_gallery_shortcodes';
506 Meow_MGL_Migrations::check_db();
507
508 $wpdb->delete( $shortcodes_table, ['id' => $id] );
509
510
511 return new WP_REST_Response( ['success' => true, 'message' => 'Shortcode removed.'], 200 );
512 } catch ( Exception $e ) {
513 return new WP_REST_Response( ['success' => false, 'message' => $e->getMessage( )], 500 );
514 }
515 }
516
517 function rest_update_gallery_rank( $request ) {
518 try {
519 global $wpdb;
520 $params = $request->get_json_params();
521 $id = $params['id'];
522 $direction = $params['direction']; // 'up' or 'down'
523
524 $shortcodes_table = $wpdb->prefix . 'mgl_gallery_shortcodes';
525 Meow_MGL_Migrations::check_db();
526
527 // Get current rank
528 $current_rank = $wpdb->get_var( $wpdb->prepare( "SELECT pref_rank FROM $shortcodes_table WHERE id = %s", $id ) );
529 $current_rank = intval( $current_rank );
530
531 // Calculate new rank (up = higher priority = higher number, down = lower priority = lower number)
532 $new_rank = $direction === 'up' ? $current_rank + 1 : $current_rank - 1;
533
534 // Update the rank
535 $wpdb->update(
536 $shortcodes_table,
537 ['pref_rank' => $new_rank],
538 ['id' => $id]
539 );
540
541 return new WP_REST_Response( ['success' => true, 'message' => 'Gallery rank updated.', 'new_rank' => $new_rank], 200 );
542 } catch ( Exception $e ) {
543 return new WP_REST_Response( ['success' => false, 'message' => $e->getMessage()], 500 );
544 }
545 }
546
547 function rest_latest_photos( $request ) {
548
549 $search = trim( $request->get_param( 'search' ) );
550 $offset = trim( $request->get_param( 'offset' ) );
551 $limit = trim( $request->get_param( 'limit' ) );
552
553 $except = json_decode( trim( $request->get_param( 'except' ) ), true );
554 $unusedImages = trim( $request->get_param( 'unusedImages' ) );
555
556 global $wpdb;
557 $searchPlaceholder = $search ? '%' . $search . '%' : '';
558 $where_search_clause = $search ? $wpdb->prepare(
559 "AND ( p.post_title LIKE %s OR p.post_content LIKE %s OR p.post_name LIKE %s ) ",
560 $searchPlaceholder,
561 $searchPlaceholder,
562 $searchPlaceholder
563 ) : '';
564 $where_search_clause .= $except && count( $except ) ? $wpdb->prepare(
565 "AND p.ID NOT IN ( " . implode( ', ', array_fill( 0, count( $except ), '%s' )) . " )", $except
566 ) : '';
567 $join_clause = '';
568 if ( $unusedImages ) {
569 // Every image used by a gallery, read from the galleries table (this used to read the
570 // old 'mgl_shortcodes' option, which isn't written anymore since the migration).
571 $shortcodes_table = $wpdb->prefix . 'mgl_gallery_shortcodes';
572 Meow_MGL_Migrations::check_db();
573
574 $used_thumbnail_ids = [];
575 foreach ( $wpdb->get_col( "SELECT medias FROM $shortcodes_table" ) as $medias ) {
576 $medias = Meow_MGL_Core::normalize_medias( maybe_unserialize( $medias ) );
577 $used_thumbnail_ids = array_merge( $used_thumbnail_ids, $medias['thumbnail_ids'] );
578 }
579
580 // Make sure the IDs are integers
581 $used_thumbnail_ids = array_unique( array_map( 'intval', $used_thumbnail_ids ) );
582
583 // Include the NOT IN clause to exclude used thumbnail IDs
584 if ( !empty( $used_thumbnail_ids ) ) {
585 $placeholders = implode( ',', array_fill( 0, count( $used_thumbnail_ids ), '%d' ) );
586 $where_search_clause .= $wpdb->prepare( " AND p.ID NOT IN ( $placeholders ) ", $used_thumbnail_ids );
587 }
588 }
589 $posts = $wpdb->get_results(
590 $wpdb->prepare(
591 "SELECT p.ID, p.post_title, p.post_mime_type
592 FROM $wpdb->posts p
593 $join_clause
594 WHERE p.post_type='attachment'
595 AND p.post_status='inherit'
596 $where_search_clause
597 ORDER BY p.post_modified DESC
598 LIMIT %d, $limit", $offset
599 ), OBJECT
600 );
601 $posts_count = ( int )$wpdb->get_var(
602 "SELECT COUNT( * )
603 FROM $wpdb->posts p
604 $join_clause
605 WHERE p.post_type='attachment'
606 AND p.post_status='inherit'
607 $where_search_clause"
608 );
609
610 $data = [];
611 foreach ( $posts as $post ) {
612 $file_url = get_attached_file( $post->ID );
613
614 $mime = $post->post_mime_type;
615 $is_video = ( strpos( $mime, 'video' ) !== false );
616
617 $thumbnail_url = $is_video ? wp_get_attachment_url( $post->ID ) : wp_get_attachment_image_url( $post->ID, 'thumbnail' );
618
619 if ( file_exists( $file_url ) ) {
620 $data[] = [
621 'id' => $post->ID,
622 'thumbnail_url' => $thumbnail_url,
623 'zoom_url' => wp_get_attachment_image_url( $post->ID, 'large' ),
624 'title' => $post->post_title,
625 'filename' => basename( $file_url ),
626 'size' => size_format( filesize( $file_url ) ),
627 'mime' => $mime,
628 ];
629 }
630 }
631 return new WP_REST_Response( [
632 'success' => true,
633 'data' => $data,
634 'total' => $posts_count
635 ], 200 );
636 }
637
638 function rest_update_option( $request ) {
639 try {
640 $params = $request->get_json_params( );
641 $value = $params['options'];
642 $options = $this->core->update_options( $value );
643 $success = !!$options;
644 $message = __( $success ? 'OK' : "Could not update options.", MGL_DOMAIN );
645 return new WP_REST_Response( [ 'success' => $success, 'message' => $message, 'options' => $success ? $options : null ], 200 );
646 }
647 catch ( Exception $e ) {
648 return new WP_REST_Response( [ 'success' => false, 'message' => $e->getMessage( ) ], 500 );
649 }
650 }
651
652 function rest_images( $request ) {
653 $params = $request->get_json_params( );
654
655 $image_ids = $params['imageIds'];
656 $atts = $params['atts'];
657 $layout = trim( $params['layout'] );
658 $size = trim( $params['size'] );
659
660 return new WP_REST_Response( [
661 'success' => true,
662 'data' => $this->core->get_gallery_images( $image_ids, $atts, $layout, $size )
663 ], 200 );
664 }
665
666 // Applies WordPress's own visibility rules to a raw posts query: published posts for everyone,
667 // other people's drafts only with edit_others_posts, other people's private posts only with
668 // read_private_posts, and your own in both cases. 'upload_files' (the capability gating this
669 // REST controller) is held by Authors, who must not see the whole site's unpublished content.
670 private function get_post_status_clause( $alias = 'p', $post_type = 'post' ) {
671 global $wpdb;
672
673 $post_type_object = get_post_type_object( $post_type );
674 $read_private_cap = $post_type_object ? $post_type_object->cap->read_private_posts : 'read_private_posts';
675 $edit_others_cap = $post_type_object ? $post_type_object->cap->edit_others_posts : 'edit_others_posts';
676
677 $user_id = get_current_user_id();
678 $clause = "AND ( $alias.post_status = 'publish'";
679
680 $clause .= current_user_can( $read_private_cap )
681 ? " OR $alias.post_status = 'private'"
682 : $wpdb->prepare( " OR ( $alias.post_status = 'private' AND $alias.post_author = %d )", $user_id );
683
684 $clause .= current_user_can( $edit_others_cap )
685 ? " OR $alias.post_status = 'draft'"
686 : $wpdb->prepare( " OR ( $alias.post_status = 'draft' AND $alias.post_author = %d )", $user_id );
687
688 return $clause . " ) ";
689 }
690
691 function rest_fetch_posts( $request ) {
692 try {
693 $params = $request->get_json_params();
694 $search = isset($params['search']) ? $params['search'] : '';
695 $offset = isset($params['offset']) ? intval($params['offset']) : 0;
696 $limit = isset($params['limit']) ? intval($params['limit']) : 10;
697
698 global $wpdb;
699 $searchPlaceholder = $search ? '%' . $search . '%' : '';
700 $where_search_clause = $search ? $wpdb->prepare(
701 "AND ( p.post_title LIKE %s OR p.post_content LIKE %s OR p.post_name LIKE %s ) ",
702 $searchPlaceholder,
703 $searchPlaceholder,
704 $searchPlaceholder
705 ) : '';
706
707 // The same clause is used by both queries on purpose: the search also matches
708 // post_content, so a count taken over a wider set than the rows would let a user probe
709 // the body of posts they cannot read (reported by Kaan Ă–zbek, 2026-09).
710 $where_status_clause = $this->get_post_status_clause( 'p' );
711
712 $posts = $wpdb->get_results(
713 $wpdb->prepare(
714 "SELECT p.ID, p.post_title, p.post_date, p.post_status, u.display_name as author
715 FROM $wpdb->posts p
716 LEFT JOIN $wpdb->users u ON p.post_author = u.ID
717 WHERE p.post_type = 'post'
718 $where_status_clause
719 $where_search_clause
720 ORDER BY p.post_date DESC
721 LIMIT %d, %d",
722 $offset,
723 $limit
724 ),
725 OBJECT
726 );
727
728 $posts_count = (int)$wpdb->get_var(
729 "SELECT COUNT(*)
730 FROM $wpdb->posts p
731 WHERE p.post_type = 'post'
732 $where_status_clause
733 $where_search_clause"
734 );
735
736 $data = array_map(function($post) {
737 return [
738 'id' => $post->ID,
739 'title' => $post->post_title,
740 'date' => $post->post_date,
741 'author' => $post->author,
742 'status' => $post->post_status
743 ];
744 }, $posts);
745
746 return new WP_REST_Response([
747 'success' => true,
748 'data' => $data,
749 'total' => $posts_count
750 ], 200);
751 } catch (Exception $e) {
752 return new WP_REST_Response(['success' => false, 'message' => $e->getMessage()], 500);
753 }
754 }
755
756 }
757
758 ?>