| @@ -7,9 +7,9 @@ | ||
| 7 | 7 | private $is_gallery_used = true; // TODO: Would be nice to detect if the gallery is actually used on the current page. |
| 8 | 8 | private $skeleton_handler; |
| 9 | 9 | private $pro_module = false; |
| 10 | 10 | |
| 11 | - private $preview_cutoff = 12; // Limit the number of images to show in the preview (for performance reasons) | |
| 11 | + public $preview_cutoff = 12; // Limit the number of images to show in the preview (for performance reasons) | |
| 12 | 12 | |
| 13 | 13 | private static $plugin_option_name = 'mgl_options'; |
| 14 | 14 | private $pro; |
| 15 | 15 | private $option_name = 'mgl_options'; |
| @@ -47,9 +47,10 @@ | ||
| 47 | 47 | new Meow_MGL_Run( $this ); |
| 48 | 48 | } |
| 49 | 49 | } |
| 50 | 50 | |
| 51 | - // Load the Pro version *after* loading the Run class due to the JS file was gatherd into one file. | |
| 51 | + // Load the Pro version *after* the Run class: both share the same JS bundle, and Run is | |
| 52 | + // the one registering it (the Pro class only localizes extra data on it). | |
| 52 | 53 | |
| 53 | 54 | $this->pro_module = class_exists( 'MeowPro_MGL_Core' ); |
| 54 | 55 | if ( $this->pro_module ) { |
| 55 | 56 | $this->pro = new MeowPro_MGL_Core( $this ); |
| @@ -76,8 +77,34 @@ | ||
| 76 | 77 | function collection() { |
| 77 | 78 | return "<b>Meow Collection</b>: This is only available in the Pro version. Please <a href='https://meowapps.com/products/meow-gallery-pro/'>upgrade to Meow Gallery Pro</a> to use this feature."; |
| 78 | 79 | } |
| 79 | 80 | |
| 81 | + // Gallery and collection IDs are plain identifiers (generate_uniqid(), stored as varchar). | |
| 82 | + // Anything else is rejected, and rejected rather than stripped: stripping could turn a crafted | |
| 83 | + // ID into a different existing one. Security: esc_attr() does NOT escape "]", so an ID coming | |
| 84 | + // from a request and concatenated into a shortcode string could close the tag and run | |
| 85 | + // arbitrary shortcodes. Keep IDs on this charset and never build a shortcode string from them. | |
| 86 | + public static function sanitize_id( $id ) { | |
| 87 | + if ( !is_scalar( $id ) ) { | |
| 88 | + return ''; | |
| 89 | + } | |
| 90 | + $id = (string) $id; | |
| 91 | + return preg_match( '/^[A-Za-z0-9_-]+$/', $id ) ? $id : ''; | |
| 92 | + } | |
| 93 | + | |
| 94 | + // Renders a collection from its ID. Deliberately calls the handler directly instead of going | |
| 95 | + // through do_shortcode(): there is no shortcode string to inject into that way. | |
| 96 | + public function render_collection( $id, $is_preview = false ) { | |
| 97 | + $id = self::sanitize_id( $id ); | |
| 98 | + if ( $id === '' ) { | |
| 99 | + return "<p class='meow-error'><b>Meow Gallery:</b> This collection ID is not valid.</p>"; | |
| 100 | + } | |
| 101 | + if ( $this->pro_module && $this->pro ) { | |
| 102 | + return $this->pro->collection( array( 'id' => $id ), $is_preview ); | |
| 103 | + } | |
| 104 | + return $this->collection(); | |
| 105 | + } | |
| 106 | + | |
| 80 | 107 | public function can_access_settings() { |
| 81 | 108 | return apply_filters( 'mgl_allow_setup', current_user_can( 'manage_options' ) ); |
| 82 | 109 | } |
| 83 | 110 | |
| @@ -142,11 +169,11 @@ | ||
| 142 | 169 | if ( isset( $atts['meow'] ) && $atts['meow'] === 'false' ) { |
| 143 | 170 | return gallery_shortcode( $atts ); |
| 144 | 171 | } |
| 145 | 172 | |
| 146 | - // If the attributes contain "collection" then use the collection shortcode instead | |
| 173 | + // If the attributes contain "collection" then render that collection instead | |
| 147 | 174 | if ( isset( $atts['collection'] ) && !empty( $atts['collection'] ) ) { |
| 148 | - return do_shortcode( '[meow-collection id="' . $atts['collection'] . '"]' ); | |
| 175 | + return $this->render_collection( $atts['collection'] ); | |
| 149 | 176 | } |
| 150 | 177 | |
| 151 | 178 | $image_ids = array(); |
| 152 | 179 | $layout = ''; |
| @@ -173,9 +200,10 @@ | ||
| 173 | 200 | try { |
| 174 | 201 | $shortcode = $this->get_gallery_by_id( $shortcode_id ); |
| 175 | 202 | } |
| 176 | 203 | catch ( Exception $e ) { |
| 177 | - return "<p class='meow-error'><b>Meow Gallery:</b> This ID wasn't found in the Gallery Manager. (ID: $shortcode_id). " . $e->getMessage() . "</p>"; | |
| 204 | + $safe_id = esc_html( is_scalar( $shortcode_id ) ? $shortcode_id : '' ); | |
| 205 | + return "<p class='meow-error'><b>Meow Gallery:</b> This ID wasn't found in the Gallery Manager. (ID: $safe_id). " . esc_html( $e->getMessage() ) . "</p>"; | |
| 178 | 206 | } |
| 179 | 207 | |
| 180 | 208 | if ( !isset( $shortcode['medias'] ) || !isset( $shortcode['medias']['thumbnail_ids'])) { |
| 181 | 209 | return "<p class='meow-error'><b>Meow Gallery:</b> Thumbnail IDs not found.</p>"; |
| @@ -683,8 +711,26 @@ | ||
| 683 | 711 | static function get_plugin_option_name() { |
| 684 | 712 | return self::$plugin_option_name; |
| 685 | 713 | } |
| 686 | 714 | |
| 715 | + // Tiles density, from the shortcode attributes when they set one, from the options otherwise. | |
| 716 | + // Shared by the front-end settings (Meow_MGL_Run) and the tiles CSS (Meow_MGL_Builders_Tiles). | |
| 717 | + static function get_tiles_density( $atts = [] ) { | |
| 718 | + if ( isset( $atts['density'] ) ) { | |
| 719 | + return array( | |
| 720 | + 'desktop' => $atts['density'], | |
| 721 | + 'tablet' => $atts['density'], | |
| 722 | + 'mobile' => $atts['density'], | |
| 723 | + ); | |
| 724 | + } | |
| 725 | + $options = get_option( self::$plugin_option_name, [] ); | |
| 726 | + return array( | |
| 727 | + 'desktop' => $options['tiles_density'] ?? 'high', | |
| 728 | + 'tablet' => $options['tiles_density_tablet'] ?? 'medium', | |
| 729 | + 'mobile' => $options['tiles_density_mobile'] ?? 'low', | |
| 730 | + ); | |
| 731 | + } | |
| 732 | + | |
| 687 | 733 | static function get_plugin_option( $option_name, $default = null ) { |
| 688 | 734 | $options = get_option( self::$plugin_option_name, null ); |
| 689 | 735 | if ( !empty( $options ) && array_key_exists( $option_name, $options ) ) { |
| 690 | 736 | return $options[$option_name]; |
| @@ -1192,8 +1238,60 @@ | ||
| 1192 | 1238 | } |
| 1193 | 1239 | } |
| 1194 | 1240 | |
| 1195 | 1241 | |
| 1242 | + /** | |
| 1243 | + * Reads the "medias" of a gallery. Only the ordered attachment IDs are stored (older versions | |
| 1244 | + * also stored their URLs and mime types, which are ignored: they were a copy of the Media | |
| 1245 | + * Library that went stale). This is also the shape that gets written back, and all the | |
| 1246 | + * front-end needs. The Admin uses hydrate_medias() to get the URLs to display. | |
| 1247 | + */ | |
| 1248 | + public static function normalize_medias( $medias ) { | |
| 1249 | + $ids = ( is_array( $medias ) && isset( $medias['thumbnail_ids'] ) && is_array( $medias['thumbnail_ids'] ) ) | |
| 1250 | + ? array_values( $medias['thumbnail_ids'] ) : []; | |
| 1251 | + | |
| 1252 | + return [ 'thumbnail_ids' => $ids ]; | |
| 1253 | + } | |
| 1254 | + | |
| 1255 | + /** | |
| 1256 | + * Adds the 'thumbnails' the Admin displays: one entry per ID, with its URLs and mime type | |
| 1257 | + * resolved from the Media Library. Never stale, and a deleted attachment simply gets empty | |
| 1258 | + * URLs (the Admin then shows a placeholder). Not used on the front-end, which only needs | |
| 1259 | + * the IDs. | |
| 1260 | + */ | |
| 1261 | + public static function hydrate_medias( $medias ) { | |
| 1262 | + $ids = self::normalize_medias( $medias )['thumbnail_ids']; | |
| 1263 | + | |
| 1264 | + // One query for all the attachments instead of one per thumbnail. _prime_post_caches() | |
| 1265 | + // only caches the attachments which exist, so anything still absent from the cache | |
| 1266 | + // afterwards is gone: it's skipped instead of being queried on every request. | |
| 1267 | + if ( !empty( $ids ) ) { | |
| 1268 | + _prime_post_caches( array_values( array_unique( array_map( 'intval', $ids ) ) ), false, true ); | |
| 1269 | + } | |
| 1270 | + | |
| 1271 | + $thumbnails = []; | |
| 1272 | + foreach ( $ids as $id ) { | |
| 1273 | + $thumbnail = [ 'id' => $id, 'url' => '', 'zoom_url' => '', 'mime' => '' ]; | |
| 1274 | + | |
| 1275 | + if ( !empty( $id ) && wp_cache_get( (int)$id, 'posts' ) ) { | |
| 1276 | + $mime = get_post_mime_type( $id ) ?: ''; | |
| 1277 | + // Same rule as the latest_photos endpoint: videos have no image sizes, so their | |
| 1278 | + // own URL is used for both the thumbnail and the zoom. | |
| 1279 | + $is_video = strpos( $mime, 'video' ) !== false; | |
| 1280 | + $url = $is_video ? wp_get_attachment_url( $id ) : wp_get_attachment_image_url( $id, 'thumbnail' ); | |
| 1281 | + $zoom = $is_video ? $url : wp_get_attachment_image_url( $id, 'large' ); | |
| 1282 | + | |
| 1283 | + $thumbnail['url'] = $url ?: ''; | |
| 1284 | + $thumbnail['zoom_url'] = $zoom ?: $thumbnail['url']; | |
| 1285 | + $thumbnail['mime'] = $mime; | |
| 1286 | + } | |
| 1287 | + | |
| 1288 | + $thumbnails[] = $thumbnail; | |
| 1289 | + } | |
| 1290 | + | |
| 1291 | + return [ 'thumbnail_ids' => $ids, 'thumbnails' => $thumbnails ]; | |
| 1292 | + } | |
| 1293 | + | |
| 1196 | 1294 | public function get_gallery_by_id( $id ) { |
| 1197 | 1295 | global $wpdb; |
| 1198 | 1296 | $shortcodes_table = $wpdb->prefix . 'mgl_gallery_shortcodes'; |
| 1199 | 1297 | $gallery = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $shortcodes_table WHERE id = %s", $id ), ARRAY_A ); |
| @@ -1200,9 +1298,9 @@ | ||
| 1200 | 1298 | |
| 1201 | 1299 | if ( !$gallery ) { |
| 1202 | 1300 | throw new Exception( __( 'Gallery not found.', MGL_DOMAIN )); |
| 1203 | 1301 | } |
| 1204 | - $gallery['medias'] = maybe_unserialize( $gallery['medias'] ); | |
| 1302 | + $gallery['medias'] = self::normalize_medias( maybe_unserialize( $gallery['medias'] ) ); | |
| 1205 | 1303 | $gallery['posts'] = $gallery['posts'] ? maybe_unserialize( $gallery['posts'] ) : null; |
| 1206 | 1304 | $gallery['tags'] = $gallery['tags'] ? unserialize( $gallery['tags'] ) : null; |
| 1207 | 1305 | |
| 1208 | 1306 | return $gallery; |
| @@ -1219,9 +1317,9 @@ | ||
| 1219 | 1317 | $galleries[$gallery['id']] = [ |
| 1220 | 1318 | 'name' => $gallery['name'], |
| 1221 | 1319 | 'description' => $gallery['description'], |
| 1222 | 1320 | 'layout' => $gallery['layout'], |
| 1223 | - 'medias' => maybe_unserialize( $gallery['medias'] ), | |
| 1321 | + 'medias' => self::normalize_medias( maybe_unserialize( $gallery['medias'] ) ), | |
| 1224 | 1322 | 'lead_image_id' => $gallery['lead_image_id'], |
| 1225 | 1323 | 'order_by' => $gallery['order_by'], |
| 1226 | 1324 | 'is_post_mode' => ( bool )$gallery['is_post_mode'], |
| 1227 | 1325 | 'dynamic_source' => $gallery['dynamic_source'], |
| @@ -1279,9 +1377,9 @@ | ||
| 1279 | 1377 | $shortcodes[$gallery['id']] = [ |
| 1280 | 1378 | 'name' => $gallery['name'], |
| 1281 | 1379 | 'description' => $gallery['description'], |
| 1282 | 1380 | 'layout' => $gallery['layout'], |
| 1283 | - 'medias' => maybe_unserialize( $gallery['medias'] ), | |
| 1381 | + 'medias' => self::hydrate_medias( maybe_unserialize( $gallery['medias'] ) ), | |
| 1284 | 1382 | 'lead_image_id' => $gallery['lead_image_id'], |
| 1285 | 1383 | 'order_by' => $gallery['order_by'], |
| 1286 | 1384 | 'is_post_mode' => ( bool )$gallery['is_post_mode'], |
| 1287 | 1385 | 'hero' => ( bool )$gallery['is_hero_mode'], |
| @@ -1300,8 +1398,39 @@ | ||
| 1300 | 1398 | 'galleries' => $shortcodes |
| 1301 | 1399 | ]; |
| 1302 | 1400 | } |
| 1303 | 1401 | |
| 1402 | + /** | |
| 1403 | + * Just the id => name pairs, for the selectors (the Gutenberg block). They only need the | |
| 1404 | + * names, so this avoids shipping every gallery's medias in the page and, unlike | |
| 1405 | + * get_galleries(), it isn't paginated: the selectors used to be capped at 10 entries. | |
| 1406 | + */ | |
| 1407 | + public function get_gallery_names() { | |
| 1408 | + global $wpdb; | |
| 1409 | + $shortcodes_table = $wpdb->prefix . 'mgl_gallery_shortcodes'; | |
| 1410 | + Meow_MGL_Migrations::check_db(); | |
| 1411 | + | |
| 1412 | + $names = []; | |
| 1413 | + $results = $wpdb->get_results( "SELECT id, name FROM $shortcodes_table ORDER BY name ASC", ARRAY_A ); | |
| 1414 | + foreach ( $results as $gallery ) { | |
| 1415 | + $names[$gallery['id']] = [ 'name' => $gallery['name'] ]; | |
| 1416 | + } | |
| 1417 | + return [ 'galleries' => $names ]; | |
| 1418 | + } | |
| 1419 | + | |
| 1420 | + public function get_collection_names() { | |
| 1421 | + global $wpdb; | |
| 1422 | + $collections_table = $wpdb->prefix . 'mgl_collections'; | |
| 1423 | + Meow_MGL_Migrations::check_db(); | |
| 1424 | + | |
| 1425 | + $names = []; | |
| 1426 | + $results = $wpdb->get_results( "SELECT id, name FROM $collections_table ORDER BY name ASC", ARRAY_A ); | |
| 1427 | + foreach ( $results as $collection ) { | |
| 1428 | + $names[$collection['id']] = [ 'name' => $collection['name'] ]; | |
| 1429 | + } | |
| 1430 | + return [ 'collections' => $names ]; | |
| 1431 | + } | |
| 1432 | + | |
| 1304 | 1433 | public function get_collection_by_id( $id ) { |
| 1305 | 1434 | global $wpdb; |
| 1306 | 1435 | $collections_table = $wpdb->prefix . 'mgl_collections'; |
| 1307 | 1436 | $shortcodes_table = $wpdb->prefix . 'mgl_gallery_shortcodes'; |
| @@ -1326,9 +1455,9 @@ | ||
| 1326 | 1455 | 'id' => $gallery['id'], |
| 1327 | 1456 | 'name' => $gallery['name'], |
| 1328 | 1457 | 'description' => $gallery['description'], |
| 1329 | 1458 | 'layout' => $gallery['layout'], |
| 1330 | - 'medias' => unserialize( $gallery['medias'] ), | |
| 1459 | + 'medias' => self::normalize_medias( maybe_unserialize( $gallery['medias'] ) ), | |
| 1331 | 1460 | 'lead_image_id' => $gallery['lead_image_id'], |
| 1332 | 1461 | 'order_by' => $gallery['order_by'], |
| 1333 | 1462 | 'is_post_mode' => ( bool )$gallery['is_post_mode'], |
| 1334 | 1463 | 'hero' => ( bool )$gallery['is_hero_mode'], |
| @@ -1388,9 +1517,9 @@ | ||
| 1388 | 1517 | 'id' => $gallery['id'], |
| 1389 | 1518 | 'name' => $gallery['name'], |
| 1390 | 1519 | 'description' => $gallery['description'], |
| 1391 | 1520 | 'layout' => $gallery['layout'], |
| 1392 | - 'medias' => unserialize( $gallery['medias'] ), | |
| 1521 | + 'medias' => self::hydrate_medias( maybe_unserialize( $gallery['medias'] ) ), | |
| 1393 | 1522 | 'lead_image_id' => $gallery['lead_image_id'], |
| 1394 | 1523 | 'order_by' => $gallery['order_by'], |
| 1395 | 1524 | 'is_post_mode' => ( bool )$gallery['is_post_mode'], |
| 1396 | 1525 | 'hero' => ( bool )$gallery['is_hero_mode'], |