| @@ -1,28 +1,35 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | |
| 3 | 3 | class Meow_MGL_Rest |
| 4 | 4 | { |
| 5 | - private $core; | |
| 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,114 +40,717 @@ | ||
| 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 | - register_rest_route( $this->namespace, '/preview', array( | |
| 122 | + register_rest_route( $this->namespace, '/preview', array( | |
| 40 | 123 | 'methods' => 'POST', |
| 41 | 124 | 'permission_callback' => array( $this->core, 'can_access_features' ), |
| 42 | 125 | 'callback' => array( $this, 'preview' ), |
| 43 | 126 | ) ); |
| 44 | - } | |
| 45 | - | |
| 46 | - function preview( WP_REST_Request $request ) { | |
| 47 | - $params = $request->get_body(); | |
| 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( ); | |
| 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->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 | |
| 59 | - function create_default_googlemaps_style( $force = false ) { | |
| 60 | - $style = get_option( 'mgl_googlemaps_style', "" ); | |
| 61 | - if ( $force || empty( $style ) ) { | |
| 62 | - $style = '[]'; | |
| 63 | - update_option( 'mgl_googlemaps_style', $style ); | |
| 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 ); | |
| 64 | 240 | } |
| 65 | - return $style; | |
| 241 | + return new WP_REST_Response( [ 'success' => true, 'available' => true, 'data' => Meow_MGL_RML::get_all_folders() ], 200 ); | |
| 66 | 242 | } |
| 67 | 243 | |
| 68 | - function create_default_mapbox_style( $force = false ) { | |
| 69 | - $style = get_option( 'mgl_mapbox_style', "" ); | |
| 70 | - if ( $force || empty( $style ) ) { | |
| 71 | - $style = '{"username":"", "style_id":""}'; | |
| 72 | - update_option( 'mgl_mapbox_style', $style ); | |
| 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 ); | |
| 73 | 336 | } |
| 74 | - return $style; | |
| 75 | 337 | } |
| 76 | 338 | |
| 77 | - function get_all_options() { | |
| 78 | - $options = $this->list_options(); | |
| 79 | - $current_options = array(); | |
| 80 | - foreach ( $options as $option => $default ) { | |
| 81 | - $current_options[$option] = get_option( $option, $default ); | |
| 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 ); | |
| 82 | 354 | } |
| 83 | - return $current_options; | |
| 84 | 355 | } |
| 85 | 356 | |
| 86 | - // List all the options with their default values. | |
| 87 | - function list_options() { | |
| 88 | - return array( | |
| 89 | - 'mgl_layout' => 'tiles', | |
| 90 | - 'mgl_captions' => 'none', | |
| 91 | - 'mgl_animation' => false, | |
| 92 | - 'mgl_image_size' => 'srcset', | |
| 93 | - 'mgl_infinite' => false, | |
| 94 | - 'mgl_infinite_buffer' => 0, | |
| 95 | - 'mgl_tiles_gutter' => 5, | |
| 96 | - 'mgl_tiles_gutter_tablet' => 5, | |
| 97 | - 'mgl_tiles_gutter_mobile' => 5, | |
| 98 | - 'mgl_tiles_density' => 'high', | |
| 99 | - 'mgl_tiles_density_tablet' => 'medium', | |
| 100 | - 'mgl_tiles_density_mobile' => 'low', | |
| 101 | - 'mgl_masonry_gutter' => 5, | |
| 102 | - 'mgl_masonry_columns' => 3, | |
| 103 | - 'mgl_justified_gutter' => 5, | |
| 104 | - 'mgl_justified_row_height' => 200, | |
| 105 | - 'mgl_square_gutter' => 5, | |
| 106 | - 'mgl_square_columns' => 5, | |
| 107 | - 'mgl_cascade_gutter' => 5, | |
| 108 | - 'mgl_horizontal_gutter' => 5, | |
| 109 | - 'mgl_horizontal_image_height' => 500, | |
| 110 | - 'mgl_horizontal_hide_scrollbar' => false, | |
| 111 | - 'mgl_carousel_gutter' => 5, | |
| 112 | - 'mgl_carousel_image_height' => 500, | |
| 113 | - 'mgl_carousel_arrow_nav_enabled' => true, | |
| 114 | - 'mgl_carousel_dot_nav_enabled' => true, | |
| 115 | - 'mgl_map_engine' => '', | |
| 116 | - 'mgl_map_height' => 400, | |
| 117 | - 'mgl_googlemaps_token' => '', | |
| 118 | - 'mgl_googlemaps_style' => $this->create_default_googlemaps_style(), | |
| 119 | - 'mgl_mapbox_token' => '', | |
| 120 | - 'mgl_mapbox_style' => $this->create_default_mapbox_style(), | |
| 121 | - 'mgl_maptiler_token' => '', | |
| 122 | - 'mgl_right_click' => false | |
| 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 | |
| 123 | 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 ); | |
| 124 | 636 | } |
| 125 | 637 | |
| 126 | 638 | function rest_update_option( $request ) { |
| 127 | - $params = $request->get_json_params(); | |
| 128 | 639 | try { |
| 129 | - $name = $params['name']; | |
| 130 | - $options = $this->list_options(); | |
| 131 | - if ( !array_key_exists( $name, $options ) ) { | |
| 132 | - return new WP_REST_Response([ 'success' => false, 'message' => 'This option does not exist.' ], 200 ); | |
| 133 | - } | |
| 134 | - $value = is_bool( $params['value'] ) ? ( $params['value'] ? '1' : '' ) : $params['value']; | |
| 135 | - $success = update_option( $name, $value ); | |
| 136 | - if ( !$success ) { | |
| 137 | - return new WP_REST_Response([ 'success' => false, 'message' => 'Could not update option.' ], 200 ); | |
| 138 | - } | |
| 139 | - return new WP_REST_Response([ 'success' => true, 'data' => $value ], 200 ); | |
| 140 | - } | |
| 141 | - catch (Exception $e) { | |
| 142 | - return new WP_REST_Response([ 'success' => false, 'message' => $e->getMessage() ], 500 ); | |
| 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); | |
| 143 | 753 | } |
| 144 | 754 | } |
| 145 | 755 | |
| 146 | 756 | } |