WP Manifestindependent plugin directory
manifest / media / mono-gallery

Mono Gallery releases

Minimal monochrome photography gallery plugin for WordPress — EXIF metadata, smart albums, GPS map, image protection, watermarking, Google Photos & Apple Photos import. Optimised for Cloudways.

by Mike Richardson · github.com/mike-richardson-uk/mono-gallery · website

0stars
0forks

Install

No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:

wp plugin install https://github.com/mike-richardson-uk/mono-gallery/archive/refs/heads/main.zip

Readme

Mono Gallery

A minimal monochrome photography gallery plugin for WordPress, built for Cloudways hosting. Designed for abstract and fine-art photography — clean grid/masonry layouts, full EXIF display, a world map view, smart albums, and multi-layer image protection.


Requirements

  • WordPress 6.0+
  • PHP 8.1+ with GD extension (standard on Cloudways)
  • MySQL 5.7+ / MariaDB 10.3+
  • EXIF support in PHP (exif_read_data — enabled by default on Cloudways)

Installation

  1. Upload the mono-gallery folder to wp-content/plugins/.
  2. Activate via Plugins → Installed Plugins.
  3. On activation the plugin:
    • Creates five database tables (wp_mono_photos, wp_mono_albums, wp_mono_photo_albums, wp_mono_tags, wp_mono_photo_tags)
    • Creates the upload directory structure under wp-content/uploads/mono-gallery/
    • Writes .htaccess rules blocking direct access to image files
    • Generates a Cloudways Nginx config snippet at mono-gallery/cloudways-nginx.conf
  4. Follow the Cloudways setup steps under Mono Gallery → Settings → Setup & Help.

Shortcodes

Shortcode Description
[mono_gallery] All photos, paginated grid
[mono_gallery album="slug"] Photos from a named album
[mono_gallery tag="architecture"] Photos with a specific tag
[mono_gallery layout="masonry" columns="4"] Masonry layout, 4 columns
[mono_gallery per_page="36"] Custom page size
[mono_gallery show_map="true"] Gallery + GPS map below
[mono_album slug="japan-2024"] Album by slug (alias for album="")
[mono_map] Standalone world map of all geotagged photos
[mono_map album="japan-2024" height="600px"] Map for one album

Features

Gallery display

  • CSS grid and CSS masonry layouts (no JavaScript layout library)
  • Lightbox with swipe support and keyboard navigation (← → Esc)
  • EXIF panel in lightbox: camera, lens, aperture, shutter, ISO, focal length, date
  • Leaflet.js world map (OpenStreetMap tiles, no API key required) — grayscale filtered to match the monochrome aesthetic
  • Lazy loading on all images
  • Search and album/tag filters on the front end

Photo management

  • Drag-and-drop bulk uploader in the admin (JPEG, PNG, WebP)
  • EXIF extraction on upload: camera make/model, lens, aperture, shutter speed, ISO, focal length, GPS coordinates, date taken
  • Tags (free-form, comma-separated)
  • Manual albums — assign photos individually
  • Smart albums — auto-populated by rules (camera model, lens, ISO, aperture, date year/month, GPS presence, tag, import source). All rules are AND-combined.

Import

  • Google Photos — full OAuth2 integration. Imports media items from any album or your entire library. Skips duplicates and videos automatically.
  • Apple Photos — iCloud Shared Album import. Create a shared album in Photos.app, copy the iCloud link, paste it into the import page. No Apple developer account required.

Image protection

Layer How it works
Signed proxy URLs Every image URL contains an HMAC-SHA256 token. Cannot be guessed or bulk-enumerated.
Referrer check Server-side: requests without a matching referrer are rejected with 403.
.htaccess block Direct HTTP access to the originals, thumbs, and watermarked directories is denied.
Nginx rules Provided config snippet blocks direct access at the Nginx layer (faster than .htaccess).
Transparent overlay A <div> covers every image, intercepting right-click, drag, and iOS touch-hold.
CSS/JS pointer-events: none, user-select: none, -webkit-user-drag: none on all <img> elements.
Cache headers Cache-Control: private, no-store — bypasses Varnish, Breeze, and browser cache.
Keyboard shortcuts Ctrl+S, Ctrl+U, Ctrl+Shift+I, PrintScreen blocked on gallery pages (opt-in).

Watermark

  • Server-side GD watermarking — burned into served copies, originals untouched
  • Configurable text (supports {year} placeholder), position (9 positions), opacity, and size
  • Drop a mono.ttf TrueType font into assets/fonts/ for crisp text rendering; falls back to GD's built-in bitmap font
  • Watermark cache invalidated automatically when settings change

Performance (Cloudways-optimised)

  • Redis object cache — photo queries, EXIF lookups, and thumbnail existence checks are cached via wp_cache_*. When the Redis Object Cache plugin is active on Cloudways, all cache calls hit Redis automatically.
  • WebP conversion — opt-in. JPEG/PNG thumbnails are converted to WebP on first request and cached. Browsers that send Accept: image/webp receive WebP automatically (~30% smaller).
  • 304 Not Modified — ETag headers on all image responses. Browsers skip re-downloading images they've seen before.
  • On-demand thumbnails — thumbnails are generated on first request, not on upload. Large imports don't block.
  • X-Accel-Buffering: no — tells Nginx not to buffer the full image in memory before sending.
  • X-Cache-Bypass: 1 — tells Cloudways Breeze to skip its cache for image proxy requests.

Cloudways Setup

1. Enable Redis

  1. Cloudways Platform → Application → Packages → ensure Redis is installed.
  2. In WordPress, install the Redis Object Cache plugin.
  3. Settings → Redis → Enable Object Cache.

2. Apply Nginx Rules

  1. Cloudways Platform → Application → Settings & Packages → Nginx Settings → Custom Vhost.
  2. Copy the contents of mono-gallery/cloudways-nginx.conf and paste them in.
  3. Save and restart Nginx.

The snippet:

  • Blocks direct access to gallery image directories at the Nginx layer
  • Bypasses Varnish/Breeze for the image proxy and REST API endpoints
  • Sets generous FastCGI timeouts for large image handling

3. Breeze Cache Exclusions

Settings → Breeze → CDN / Advanced — add these to the exclusion list:

/mono-gallery/img/
/wp-json/mono-gallery/

4. Cloudflare (if used)

Add a Cache Rule in Cloudflare to bypass cache for */mono-gallery/img/*. Signed image URLs are per-user and must never be served from a shared CDN cache.

5. PHP Settings

In Cloudways → Application → Settings & Packages → PHP FPM Settings:

Setting Recommended
memory_limit 256M
max_execution_time 120
upload_max_filesize 50M
post_max_size 50M
max_file_uploads 50

REST API

All endpoints are under /wp-json/mono-gallery/v1/.

Method Endpoint Auth Description
GET /photos Public List photos. Params: album_id, tag, search, has_gps, order, orderby, per_page, page
GET /photos/{id} Public Single photo with full EXIF
PATCH /photos/{id} Admin Update title, description, tags, location name
DELETE /photos/{id} Admin Delete photo and all cached files
POST /upload Admin Upload a photo file. Params: file, title, tags, album_id
GET /albums Public List all albums
POST /albums Admin Create album
PATCH /albums/{id} Admin Update album
DELETE /albums/{id} Admin Delete album
POST /albums/{id}/photos Admin Set album photo membership
GET /tags Public List all tags with counts
GET /google/auth Admin Get Google OAuth URL
POST /google/import Admin Import from Google Photos
POST /google/disconnect Admin Disconnect Google account
GET /apple/links Admin List iCloud shared album links
POST /apple/links Admin Add an iCloud link
DELETE /apple/links/{index} Admin Remove a link
POST /apple/import Admin Import from an iCloud link
POST /smart-albums/rebuild Admin Rebuild all smart album memberships

File Structure

mono-gallery/
├── mono-gallery.php              Main plugin file
├── cloudways-nginx.conf          Generated Nginx config snippet (copy to Cloudways)
├── assets/
│   └── fonts/
│       └── mono.ttf              (drop your .ttf here for watermark text)
├── includes/
│   ├── class-activator.php       DB setup, upload dirs, .htaccess, Nginx config
│   ├── class-photo.php           Photo CRUD and tag management
│   ├── class-album.php           Album CRUD and photo membership
│   ├── class-exif.php            EXIF extraction (GD/exif_read_data)
│   ├── class-image-server.php    Protected image proxy, watermarking, WebP serving
│   ├── class-cache.php           Redis/object cache helpers
│   ├── class-webp.php            WebP conversion and stats
│   ├── class-google-photos.php   Google Photos OAuth2 and import
│   ├── class-apple-photos.php    iCloud Shared Albums import
│   ├── class-smart-albums.php    Smart album rule engine
│   └── class-rest-api.php        REST API endpoints
├── admin/
│   ├── class-admin.php           Admin menus and settings pages
│   ├── css/admin.css
│   └── js/admin.js               Library grid, uploader, album builder
└── public/
    ├── class-shortcode.php       Shortcode registration
    ├── css/gallery.css           Minimal monochrome front-end CSS
    └── js/gallery.js             Grid, masonry, lightbox, map, protection

Changelog

1.0.1 — Security patch

  • [Critical] Fixed SSRF in Apple Photos import: strict regex allowlist now enforces that the iCloud API host matches p{N}-sharedstreams.icloud.com exactly — subdomain confusion attacks (e.g. icloud.com.evil.com) are rejected
  • [Critical] Fixed SSRF via unvalidated download URLs in Apple Photos import: download URLs must be https:// and resolve to *.icloud.com or *.apple.com before download_url() is called
  • [High] Fixed broken Google Photos OAuth state verification: wp_verify_nonce() arguments were swapped, meaning the incoming state was never actually compared — corrected argument order
  • [High] Added require_admin permission check to the Google OAuth callback REST endpoint, which was previously publicly accessible
  • [High] Upload MIME type validation now uses wp_check_filetype_and_ext() (validates file bytes, not just extension) plus getimagesize() confirmation — prevents extension-spoofed uploads
  • [High] Tab labels in admin settings page escaped with esc_html() (XSS hardening)
  • [Medium] Image proxy tokens are now time-windowed (24-hour expiry, previous window also accepted) — leaked URLs from logs/referrers eventually expire
  • [Medium] Watermark cache file written atomically via temp-file-then-rename, eliminating TOCTOU race condition under concurrent requests
  • [Medium] Watermark cache key now derived from watermark-specific settings only, not the full options array (which included Google OAuth secrets)
  • [Medium] Google Photos OAuth tokens stored in a non-autoloaded wp_options row, reducing exposure in serialized dumps and per-request memory
  • [Medium] sort_order album field validated against an explicit allowlist before storage
  • [Low] Plugin directory protected by .htaccess blocking web access to .conf, .md, .txt files (prevents the generated Nginx config from being read publicly)
  • [Low] Leaflet map popup built via DOM API instead of innerHTML template interpolation (eliminates XSS surface on photo.urls.thumb)
  • [Low] Leaflet 1.9.4 loaded from unpkg with verified SHA-256 Subresource Integrity hashes and crossorigin="anonymous" — compromised CDN cannot inject code
  • [Low] Apple Photos import error messages sanitised — raw WP_Error messages (which can contain filesystem paths from cURL/TLS errors) are no longer returned to the browser

1.0.0

  • Initial release
  • Grid and masonry layouts
  • EXIF extraction and display
  • Leaflet.js GPS map
  • Manual and smart albums
  • Google Photos OAuth2 import
  • Apple Photos iCloud Shared Albums import
  • Multi-layer image protection (signed URLs, overlays, .htaccess, Nginx rules)
  • Server-side GD watermarking
  • WebP conversion
  • Redis object caching
  • Cloudways Nginx config generator
  • Tabbed settings page with inline setup guide

Licence

GPL-2.0+

Read the full README on GitHub →

Releases

TagPublished
v1.0.1 Jun 13, 2026
v1.0.0 Jun 13, 2026

These releases are tags only. The author does not attach a packaged zip, so there are no download counts to report.