HM Media PII Cleaner releases
Cleans up personally identifiable information from WordPress media file metadata
by Human Made Limited · github.com/humanmade/hm-media-pii-cleaner · website
Install
The author publishes release zips, so WP-CLI can install straight from GitHub:
wp plugin install https://github.com/humanmade/hm-media-pii-cleaner/releases/download/v0.2.0/hm-media-pii-cleaner-v0.2.0.zipWordPress plugin that strips hidden metadata (EXIF/IPTC/XMP, PDF Info/XMP, video udta, OOXML docProps) from Media Library uploads. By default title, organisation, copyright, created date, version and language are kept (see Allowed fields); author, software, camera, GPS and similar fields are removed.
Files that can't be proven clean are flagged in the Media Library and moved out of the document root.
Covered: JPEG, PNG, WebP, GIF, SVG, PDF, MP4/QuickTime, DOCX/XLSX/PPTX. No shell access, exiftool, Imagick or GD required.
See LIMITATIONS.md for per-format behaviour, what isn't covered and the remediation workflow.
Try it in WordPress Playground (latest main, with sample media already imported).
Requirements
- WordPress 6.2+
- PHP 8.3+
Installation
composer install --no-dev
Or require humanmade/hm-media-pii-cleaner from a site's composer.json. If the site already autoloads setasign/fpdi, the plugin uses that instead of its own vendor/.
WP-CLI
wp hm-media-pii-cleaner sanitize-all [--include-videos] [--force] [--dry-run] [--report=<path>]
wp hm-media-pii-cleaner sanitize-pdfs|sanitize-gifs|sanitize-svgs|sanitize-videos|sanitize-ooxml|verify-images [<id>...] [--all] [--force] [--dry-run] [--report=<path>]
wp hm-media-pii-cleaner set-status <id> <status> [--detail=<text>]
wp hm-media-pii-cleaner status-report [--format=<all|pdf|image|video|ooxml>] [--output=<path>]
Checking an attachment before exposing a link
if (
function_exists( '\HM\MediaPiiCleaner\RenderGuard\is_attachment_sanitized' )
&& ! \HM\MediaPiiCleaner\RenderGuard\is_attachment_sanitized( $attachment_id )
) {
return null;
}
Allowed fields
The fields that survive sanitization are defined in inc/fields.php and can be changed with the hm_media_pii_cleaner_allowed_fields filter. Each field maps onto the metadata formats it applies to:
| Key | Value | Used for |
|---|---|---|
xmp |
[ namespace, local name, prefix ] list |
XMP in JPEG, WebP, PNG, SVG and PDF. The first entry also receives converted EXIF/IPTC values. |
exif |
EXIF tag names, in precedence order | Converted to the field's XMP property before binary EXIF is removed |
iptc |
IPTC dataset codes, joined in order | Converted to the field's XMP property (JPEG) |
png |
PNG text keywords | tEXt/zTXt/iTXt chunks kept as-is |
pdf_info |
Title, Author, Subject, Keywords, Creator, CreationDate |
PDF /Info dictionary |
ooxml_core |
[ namespace, local name ] list (dc, dcterms or cp) |
docProps/core.xml |
ooxml_app |
Element names | docProps/app.xml |
image_meta |
WordPress image_meta keys |
Stored attachment metadata and the REST API |
date |
true |
Normalizes converted EXIF/IPTC values to ISO 8601 |
Remove a field:
add_filter( 'hm_media_pii_cleaner_allowed_fields', function ( array $fields ) : array {
unset( $fields['organisation'] );
return $fields;
} );
Add a field, or change an existing one:
add_filter( 'hm_media_pii_cleaner_allowed_fields', function ( array $fields ) : array {
$fields['credit'] = [
'xmp' => [ [ 'http://ns.adobe.com/photoshop/1.0/', 'Credit', 'photoshop' ] ],
'iptc' => [ '2#110' ],
];
$fields['title']['png'][] = 'Description';
return $fields;
} );
Notes:
- Invalid mappings are dropped and reported through
_doing_it_wrong(). A filter that doesn't return an array keeps nothing. - Reserved XMP namespaces (
rdf,x,xml), PNG's raw XMP keyword and the PDFProducerkey can't be allowed.orientationis always kept inimage_meta, since the image editor needs it. - Anything you add is published with every matching file. Author, creator and software fields usually contain usernames and version numbers.
- The PDF verifier follows the same rules, so an allowed field doesn't cause a PDF to be flagged.
- Changes only apply to files sanitized afterwards. Run
wp hm-media-pii-cleaner sanitize-all --forceto re-process existing files. Fields that were already stripped can't be restored.
Configuration
Optional constants for wp-config.php:
HM_MEDIA_PII_CLEANER_QUARANTINE_DIR: private directory for flagged files.HM_MEDIA_PII_CLEANER_MAX_{IMAGE,PDF,VIDEO,OOXML}_BYTES,HM_MEDIA_PII_CLEANER_MEMORY_RESERVE_BYTES: size and memory ceilings.
Development
composer install
composer test
composer lint
bin/build-zip.sh builds the installable zip into build/, with production vendor/ included.
Releasing
The plugin header keeps Version: __VERSION__ in the repo. To release:
- Go to Releases > Draft a new release and create a new tag such as
v1.2.0onmain. - Write the notes (or use "Generate release notes") and publish.
release.yml then writes 1.2.0 into the plugin header, commits it onto the tag, and attaches hm-media-pii-cleaner-v1.2.0.zip to the release. That zip is the one to install. GitHub's source archives don't include vendor/, and without it every PDF is flagged and quarantined instead of cleaned.
WordPress Playground
composer install
composer playground
This starts a disposable WordPress Playground site on port 9400, or the next free port (Node.js 20.18+ required), and mounts this directory as the plugin, including vendor/. The site boots from blueprint.json: PHP 8.3, latest WordPress, WP_DEBUG_LOG on, plugin active, logged in as admin.
It also imports a sample JPEG, PNG and PDF from tests/fixtures/playground/. Each one has fake personal metadata (author, software, camera) as well as a title and copyright, and opens in the Media Library list view, where the Metadata column shows the result. Pass extra CLI flags after --, e.g. composer playground -- --port=9500 --php=8.4. To rebuild the sample files, run php tests/fixtures/playground/generate.php.
Online previews use the same blueprint.json, with the plugin installed from a zip built by bin/build-playground.sh (production vendor/ plus the sample files) instead of mounted:
- Pull requests:
playground-preview-build.ymlbuilds the zip from the PR with read-only permissions.playground-preview-publish.ymlthen uploads it to theci-artifactsprerelease and adds an "Open in Playground" button to the PR description. main:playground-main.ymluploads the zip to theplayground-mainprerelease on every push. The README link above installs from there.
The README link carries the blueprint inline, because playground.wordpress.net can't fetch a blueprint from a GitHub release (no CORS headers). After changing blueprint.json, regenerate it with the command below; composer test fails while the link is out of date.
php bin/playground-blueprint.php --link https://github.com/humanmade/hm-media-pii-cleaner/releases/download/playground-main/hm-media-pii-cleaner.zipReleases
3 releases. Each count is every asset in that release; expand a row for the breakdown.