Mono Gallery WP Registry grade A. No security issues foundWP RegistryNo security issues foundOpen the reportA
Minimal monochrome photography gallery plugin for WordPress — EXIF metadata, smart albums, GPS map, image protection, watermarking, Google Photos & Apple Photos import. Optimised for Cloudways.
Install
This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.
Install straight from GitHub with WP-CLI:
wp plugin install https://github.com/mike-richardson-uk/mono-gallery/archive/refs/heads/main.zipA 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
- Upload the
mono-galleryfolder towp-content/plugins/. - Activate via Plugins → Installed Plugins.
- 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
.htaccessrules blocking direct access to image files - Generates a Cloudways Nginx config snippet at
mono-gallery/cloudways-nginx.conf
- Creates five database tables (
- 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.ttfTrueType font intoassets/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/webpreceive 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
- Cloudways Platform → Application → Packages → ensure Redis is installed.
- In WordPress, install the Redis Object Cache plugin.
- Settings → Redis → Enable Object Cache.
2. Apply Nginx Rules
- Cloudways Platform → Application → Settings & Packages → Nginx Settings → Custom Vhost.
- Copy the contents of
mono-gallery/cloudways-nginx.confand paste them in. - 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.comexactly — 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.comor*.apple.combeforedownload_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_adminpermission 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) plusgetimagesize()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_optionsrow, reducing exposure in serialized dumps and per-request memory - [Medium]
sort_orderalbum field validated against an explicit allowlist before storage - [Low] Plugin directory protected by
.htaccessblocking web access to.conf,.md,.txtfiles (prevents the generated Nginx config from being read publicly) - [Low] Leaflet map popup built via DOM API instead of
innerHTMLtemplate interpolation (eliminates XSS surface onphoto.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_Errormessages (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+
Releases
2 releases.
These releases are tags only. The author does not attach a packaged zip, so there are no download counts to report.