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
← All changes | classes/rest.php +698 -15 4.3.05.5.5 View file →
@@ -4,25 +4,32 @@
4 4 {
5 5 private $core;
6 6 private $namespace = 'meow-gallery/v1';
7 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 +
8 15 public function __construct( $core ) {
9 16 $this->core = $core;
10 17
11 18 // FOR DEBUG
12 19 // For experiencing the UI behavior on a slower install.
13 - // sleep(1);
20 + // sleep( 1 );
14 21 // For experiencing the UI behavior on a buggy install.
15 - // trigger_error( "Error", E_USER_ERROR);
16 - // trigger_error( "Warning", E_USER_WARNING);
17 - // trigger_error( "Notice", E_USER_NOTICE);
18 - // trigger_error( "Deprecated", E_USER_DEPRECATED);
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 );
19 26
20 27 add_action( 'rest_api_init', array( $this, 'rest_api_init' ) );
21 28 }
22 29
23 30
24 - function rest_api_init() {
31 + function rest_api_init( ) {
25 32
26 33 // Settings
27 34 register_rest_route( $this->namespace, '/update_option/', array(
28 35 'methods' => 'POST',
@@ -33,9 +40,85 @@
33 40 'methods' => 'GET',
34 41 'permission_callback' => array( $this->core, 'can_access_settings' ),
35 42 'callback' => array( $this, 'rest_all_settings' )
36 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 + ) );
37 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 +
38 121 // Gutenberg Block
39 122 register_rest_route( $this->namespace, '/preview', array(
40 123 'methods' => 'POST',
41 124 'permission_callback' => array( $this->core, 'can_access_features' ),
@@ -40,34 +123,634 @@
40 123 'methods' => 'POST',
41 124 'permission_callback' => array( $this->core, 'can_access_features' ),
42 125 'callback' => array( $this, 'preview' ),
43 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 + ) );
44 145 }
45 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 +
46 157 function preview( WP_REST_Request $request ) {
47 - $params = $request->get_body();
158 + $params = $request->get_body( );
48 159 $params = json_decode( $params );
49 160 $params->ids = implode( ',', $params->ids );
50 - $atts = (array) $params;
51 - $html = $this->core->gallery( $atts, true );
52 - return new WP_REST_Response( [ 'success' => true, 'data' => $html ], 200 );
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 );
53 186 }
54 187
55 - function rest_all_settings() {
56 - return new WP_REST_Response( [ 'success' => true, 'data' => $this->core->get_all_options() ], 200 );
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 + }
57 231 }
58 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 +
59 638 function rest_update_option( $request ) {
60 639 try {
61 - $params = $request->get_json_params();
640 + $params = $request->get_json_params( );
62 641 $value = $params['options'];
63 642 $options = $this->core->update_options( $value );
64 643 $success = !!$options;
65 644 $message = __( $success ? 'OK' : "Could not update options.", MGL_DOMAIN );
66 - return new WP_REST_Response([ 'success' => $success, 'message' => $message, 'options' => $success ? $options : null ], 200 );
645 + return new WP_REST_Response( [ 'success' => $success, 'message' => $message, 'options' => $success ? $options : null ], 200 );
67 646 }
68 647 catch ( Exception $e ) {
69 - return new WP_REST_Response([ 'success' => false, 'message' => $e->getMessage() ], 500 );
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);
70 753 }
71 754 }
72 755
73 756 }