PhotoFetch

Description

This plugin adds a “Photo Directory” search UI to wp-admin that talks to the
public Photo Directory REST API.

You can search, preview, and import photos directly into your own Media
Library, where the imported image behaves exactly like any regular upload
— usable as a featured image, inside blocks, in a gallery, etc.

Entry points:

  • Media > Photo Directory — a dedicated search/browse admin page.
  • “Photo Directory” button next to Add Media in the classic editor.
  • “Photo Directory” tab inside the native WordPress media picker itself
    (Set Featured Image, Add Media, an Image block’s Media Library button,
    etc.) — works in both the classic and block editor, since they share the
    same underlying media picker.

Notes for developers

  • This is an unofficial integration with a public but not formally
    documented API. includes/class-photofetch-api.php::normalize_item() is where
    the raw JSON from the Photo Directory gets mapped into predictable
    sizes/alt/author fields — it tries several plausible shapes
    (media_details.sizes, an embedded featured-media object, a bare
    source_url, etc.). If wordpress.org changes the API’s response shape,
    this is the one function that should need updating.
  • Title and alt text are sourced independently: alt text prefers a real
    alt-text field if the API exposes one, otherwise falls back to the same
    text used for the Description field (see PhotoFetch_API::normalize_item() for
    the exact priority order). It’s never derived from the title. For title,
    known upstream placeholder strings (currently “Photo Detail”, “Untitled”,
    “Untitled Photo” — see photofetch_generic_title_placeholders filter) are
    treated as no title at all, and a fallback title is derived from the
    photo’s slug instead (e.g. red-fox-in-snow “Red fox in snow”). If
    the slug itself isn’t usable, the title falls back to “Untitled photo”.
  • The attachment’s Description field (post_content) and alt text both get
    the photo’s full description text; its Caption field (post_excerpt) gets
    only the photographer credit line (e.g. “Photo by Jane Smith, via the
    WordPress Photo Directory.”), or is left empty if no author name is
    available.
  • The search/browse picker UI doesn’t display each photo’s title — many
    photos on the Photo Directory only have a slug-derived fallback title
    (see above), which isn’t meaningful to show while browsing. The title is
    still set on the imported attachment; it’s just not shown in the grid.
  • Imports are deduplicated: each imported attachment gets a
    _photofetch_source_id meta value, and re-importing the same photo returns the
    existing attachment instead of downloading it again.
  • Image format conversion (Settings > Photo Directory,
    PhotoFetch_Settings::get_format()) happens in
    PhotoFetch_Importer::maybe_convert_image(), right after download and before
    sideloading. It re-checks wp_image_editor_supports() at import time
    rather than trusting the stored setting outright, and falls back to the
    original file on any failure.
  • Search results are cached for 5 minutes via transients to avoid hammering
    the upstream API on repeat searches.
  • All photos on the Photo Directory are released CC0 (no attribution
    required), but the plugin stores the original photo URL and author name
    (when available) as _photofetch_source_url / _photofetch_source_author attachment
    meta for your own reference, in addition to the caption credit above.

Installation

  1. Upload the plugin files to the /wp-content/plugins/photofetch directory, or install the plugin through the Plugins > Add New screen in wp-admin directly.
  2. Activate the plugin through the Plugins screen in WordPress.
  3. That’s it — no configuration is needed and no API key or account is required. Look under Media > Photo Directory, use the “Photo Directory” button next to Add Media in the classic editor, or use the “Photo Directory” tab inside the native media picker (Set Featured Image, Add Media, an Image block’s Media Library button, etc.).

FAQ

Do I need an API key or account to use this?

No. The Photo Directory REST API is public, so the plugin works as soon as it’s activated.

Do I need to credit the photographer?

No — every photo on the Photo Directory is released under CC0, so attribution isn’t legally required. The plugin adds a photographer credit line to each imported photo’s caption anyway, when the API provides an author name, so credit is there if you choose to show it.

Why does an imported photo’s title look like a URL slug instead of a real title?

Some photos on the Photo Directory only have a generic placeholder title, because the original uploader never set a real one. The plugin recognizes those placeholders and falls back to a title built from the photo’s slug or description instead, rather than importing the placeholder text as-is. It’s a best-effort fallback, not guaranteed to read naturally — worth a glance before publishing.

Why don’t I see the “Photo Directory” button in the block editor?

That button is added next to Add Media via the media_buttons hook, which only the classic editor fires. In the block editor, use the “Photo Directory” tab inside the native media picker instead — it opens the same way from Set Featured Image, Add Media, or an Image block’s Media Library button.

Can I choose what size image gets imported?

Yes. Both the Media > Photo Directory browse screen and the media picker tab let you choose Full, Large, or Medium before importing.

Can imported photos be converted to WebP or AVIF?

Yes, if your server’s image editor supports it. Go to Settings > Photo Directory to choose — WebP is used automatically whenever your server can produce it. If it can’t, the page says so plainly and photos keep their original format. A quality field appears alongside the format choice whenever WebP or AVIF is selected; it has no effect when keeping the original format, so it’s hidden in that case.

Where does the plugin store which photos I’ve already imported?

Each imported attachment gets a _photofetch_source_id meta value matching its ID on the Photo Directory. Importing the same photo again returns the existing attachment instead of downloading a duplicate.

Reviews

There are no reviews for this plugin.

Contributors & Developers

“PhotoFetch” is open source software. The following people have contributed to this plugin.

Contributors

Translate “PhotoFetch” into your language.

Interested in development?

Browse the code, check out the SVN repository, or subscribe to the development log by RSS.

Changelog

2.0.0

Renamed the plugin from “Photo Directory Importer” to “PhotoFetch.” Unlike the 1.3.16 rename, this one is a full internal rebrand, not just the public-facing name:
* Main plugin file renamed from photo-directory-importer.php to photofetch.php; every includes/class-pdi-*.php file renamed to includes/class-photofetch-*.php; the bundled .pot file renamed to languages/photofetch.pot.
* The Text Domain (and every translation call’s domain argument) changed from photo-directory-importer to photofetch, matching the new slug — same requirement as the 1.3.14 and 1.3.16 fixes.
* Unlike 1.3.16, this time the internal-only identifiers were deliberately included: all PDI_* class names (PDI_API, PDI_Importer, PDI_Plugin, PDI_Settings) are now PhotoFetch_API, PhotoFetch_Importer, PhotoFetch_Plugin, PhotoFetch_Settings; the PDI_* constants are now PHOTOFETCH_*; every pdi_-prefixed hook, filter, nonce, AJAX action, option, and meta key (including the _pdi_source_id/_pdi_source_url/_pdi_source_author/_pdi_imported attachment meta) is now photofetch_-prefixed; and every pdi-* CSS class, script/style handle, and HTML id is now photofetch-*.
* This is a breaking change for existing installs: deactivate and remove the old plugin, then install this one fresh. Site functionality and every imported photo’s existing attachment meta are otherwise unaffected — only the identifiers above changed, not what they do.

1.3.17

  • Fixed the phpcs findings from a fresh run against the renamed plugin:
    an equals-sign alignment warning in class-pdi-plugin.php‘s author-link
    rewriter, missing short descriptions in three doc comments (one each in
    class-pdi-importer.php and class-pdi-settings.php, two total in the
    latter), and languages/index.php‘s file comment switched from a //
    line comment to the required /** ... */ docblock style.

1.3.16

  • Renamed the plugin from “WP Photo Directory Importer” to “Photo
    Directory Importer” — WordPress.org’s plugin guidelines prohibit “WP” or
    “WordPress” in a plugin’s name, since it implies an official affiliation
    the plugin doesn’t have. This touched everything tied to the old name:
    the Plugin Name header, the main plugin file
    (wp-photo-directory-importer.php photo-directory-importer.php),
    the Text Domain (and every translation call’s domain argument, to
    match — same requirement as the 1.3.14 fix), the bundled .pot file’s
    name and contents, and the @package docblock tag across every PHP
    file. Internal-only identifiers (the PDI_* class names, pdi_–
    prefixed hooks/filters/nonces, pdi-* CSS/JS handles) were left
    untouched, since none of them spell out “WP” or “WordPress” and
    changing them would be an unrelated, unnecessary breaking change.

1.3.15

  • Fixed incorrect plural handling in the browse screen and media-picker
    tab: several “N selected”/”N imported” strings picked between exactly
    two hardcoded forms using a count === 1 check in JavaScript — correct
    for English, but impossible for a translator to adapt to a language
    with three, four, or six plural forms (Russian, Polish, and Arabic,
    among others). Now uses wp.i18n._n(), which consults the current
    locale’s real plural-forms rule instead. Along the way this also fixed
    a visible English-language bug: the single-photo “quick import” success
    message was using the plural template with the count hardcoded to 1,
    producing “1 photos imported.”
  • Added a bundled .pot file (languages/photo-directory-importer.pot)
    and a Domain Path header, so translators have a starting point instead
    of needing to generate one themselves before they can begin.

1.3.14

  • Fixed a Plugin Check error: “Mismatched text domain. Expected
    ‘photo-directory-importer’ but got ‘pdi’.” WordPress.org requires the
    Text Domain header, and every translation function’s domain argument,
    to exactly match the plugin’s own folder/slug. Changed the domain from
    pdi to photo-directory-importer everywhere: the plugin header
    and all translation function calls across every PHP and JS file.
    Internal-only identifiers that happen to start with pdi_ (hooks,
    filters, nonces, meta keys, transient keys) were deliberately left
    untouched, since those aren’t text domains.
  • Reformatted a handful of PHP statements that became excessively long
    purely from the domain string’s length.

1.3.12

  • On the Plugins list page, each contributor’s name now links to their
    own wordpress.org profile (https://profiles.wordpress.org/username)
    instead of the whole comma-separated author list linking to one shared
    GitHub URL — the standard plugin header format only supports one link
    for the entire “Author” string, so this rewrites the row directly via
    the all_plugins filter.
  • Added a “Settings” link to the plugin’s row on the Plugins page,
    appearing first among the action links, right next to “Deactivate”.

1.3.11

  • Added a quality field to Settings > Photo Directory, shown only
    when converting to WebP or AVIF (hidden automatically when “Keep
    original format” is selected — quality has no meaning there, since
    nothing gets re-encoded). Defaults to 82, matching WordPress core’s own
    default JPEG compression quality. Clamped to 1–100 on save regardless
    of what’s posted.

1.3.10

  • Added a Settings > Photo Directory page. If this server’s image
    editor (GD or Imagick, whichever core picked) can actually produce
    WebP — checked live via wp_image_editor_supports(), not assumed from
    PHP version or extension presence — a format picker appears: keep the
    original format, convert to WebP, or convert to AVIF (the latter only
    offered when supported too). WebP is the default whenever it’s
    available. If WebP isn’t supported, the page simply says so and no
    photos are converted, same as before this release.
  • Conversion happens right after download, before the file is sideloaded,
    so every generated thumbnail/medium/large sub-size is produced from the
    converted file directly rather than converted a second time after the
    fact.
  • A conversion failure for any reason falls back to importing the
    original file untouched — this is meant to be a nice-to-have, never
    something that can turn a failed conversion into a failed import.
  • The “Import settings” link in the Media > Photo Directory page header,
    present but hidden since 1.3.4, now points at the new settings page.

1.3.9

  • Added w.org to the default trusted image hosts (pdi_allowed_image_hosts).
    The Photo Directory serves its images from pd.w.org specifically —
    w.org is WordPress.org’s own short domain, the same family as
    s.w.org, its well-known static-assets host. The 1.3.8 fix (adding
    wp.com for Photon) turned out not to be where these images actually
    live; this corrects it based on the real rejected host.

1.3.8

  • Fixed a regression from 1.3.7 that broke every import. The trusted-host
    check added in 1.3.7 only allowed wordpress.org (and its subdomains),
    but the Photo Directory serves uploaded images through Automattic’s
    Photon CDN (i0.wp.com, i1.wp.com, etc.) rather than from
    wordpress.org’s own domain directly — so every single import was
    rejected as “untrusted.” wp.com (and its subdomains) is now included
    in the default allowed hosts.
  • The “not on a trusted host” error message now includes the actual
    rejected hostname, so a similar mismatch in the future is immediately
    diagnosable without digging through network requests, and can be
    resolved on the spot via the pdi_allowed_image_hosts filter.

1.3.7

Security review and hardening. No user-facing behavior changes; all four
items below were fixed as defense-in-depth rather than in response to an
exploited vulnerability.

  • Sideloaded image URLs are now pinned to a trusted host.
    PDI_Importer::import_photo() downloads whichever image URL the Photo
    Directory API returns for a photo; that URL is data returned by the
    API, not something this plugin generates itself. Added
    is_allowed_image_host(), checked before download_url() is ever
    called, requiring the URL’s host to be wordpress.org or a subdomain of
    it (filterable via pdi_allowed_image_hosts). Nothing a site visitor or
    lower-privileged user supplies can influence which host this plugin
    talks to for search or photo lookups — those always go to the hardcoded
    REMOTE_BASE/TAXONOMY_BASE constants — so this specifically guards
    against a compromised or malicious upstream API response causing this
    site to download and store a file from an attacker-controlled server.
  • Removed an unused innerHTML code path. assets/js/admin.js‘s el()
    helper had a dead 'html' attribute branch (e.innerHTML = attrs.html)
    that nothing in the codebase called, but that would become an XSS sink
    the moment anything did. Removed it entirely.
  • Added rel="noopener noreferrer" to the “View in Media Library”
    link in assets/js/admin.js, which opens in a new tab
    (target="_blank"). Its destination is fully server-generated today, so
    this wasn’t exploitable, but it’s standard, zero-cost protection against
    reverse-tabnabbing regardless.
  • Added explicit return statements after every wp_send_json_error()
    call
    in class-pdi-api.php (ajax_search(), ajax_terms()) and
    class-pdi-importer.php (ajax_import()) that wasn’t already the last
    statement in its function. wp_send_json_error() calls wp_die(),
    which WordPress’s AJAX handling turns into an immediate die(), so
    execution already halted in normal operation — this wasn’t currently
    exploitable. But code after those calls was relying on that implicit
    behavior instead of stating it, which would fall through with
    invalid/stale state (e.g. continuing with a photo ID of 0, or treating a
    WP_Error object as an array) if wp_die()‘s behavior were ever
    filtered, as some testing harnesses deliberately do.

1.3.6

  • Added a “View full” button to the “Photo Directory” tab inside the
    native media picker (Set Featured Image, Add Media, etc.), next to the
    “Import only” button in the footer. Opens the currently-detailed
    photo’s full-size image in a plain, image-only overlay (closes on
    Escape, clicking outside the image, or the close button).

1.3.5

  • Added a “View full” button to every photo card on the Media > Photo
    Directory browse screen. Opens the photo’s full-size image in a
    lightbox (closes on Escape or clicking outside the image), which
    includes its own Import button (or a “View in library” link, once the
    photo has been imported).

1.3.4

  • Rebuilt Media > Photo Directory as a filterable browse screen
    (assets/js/photo-browser.js, React via wp-element): category,
    orientation, and color filters populated live from the Photo
    Directory’s own taxonomies; sort by relevance or newest; multi-select
    with a bulk-import tray; per-photo title/alt/caption edits and a
    photographer-credit toggle before import.
  • Rebuilt the “Photo Directory” tab inside the native media picker as a
    two-pane picker (thumbnail grid + detail sidebar) with the same
    filters, matching the browse screen.
  • Photos already in the Media Library are now flagged in bulk for a
    whole result page at once (PDI_Importer::find_existing_attachments())
    instead of one lookup per card.
  • _embed now requests only the two relations the plugin reads
    (author,wp:featuredmedia) instead of every relation the API would
    otherwise expand, avoiding an unnecessary wp:term expansion per
    taxonomy per photo.
  • Title fallback now prefers a short title derived from the photo’s own
    description over a humanized slug, since most Photo Directory slugs are
    opaque hex strings (e.g. 6836a813f7) with no human meaning to
    humanize in the first place.
  • Normalized photo data now includes width, height, MIME type, and file
    size, and the computed credit line is available directly on the photo
    object for the new UI to prefill.
  • Added Michelle Frechette as a contributor.
  • Removed the bundled LICENSE file in favor of linking the license
    online.
  • Fixed the search field’s icon overlapping its placeholder text.

1.3.3

  • Sanitize untrusted fields from the Photo Directory API before use: the
    slug now goes through sanitize_title(), the author name through
    wp_strip_all_tags()/trim(), and every image URL through
    esc_url_raw().
  • Moved the wp-admin/includes/{media,file,image}.php requires from
    file scope into import_photo() itself, so they’re only parsed while
    an import is actually happening rather than on every page load
    (this file loads on every request via plugins_loaded, including the
    front end).
  • Fixed a phpcs suppression bug: the slow-query warning for
    meta_key/meta_value was suppressed with a single phpcs:ignore
    comment, which only covers the next line and so didn’t actually
    suppress the meta_value warning. Switched to phpcs:disable/
    phpcs:enable spanning both lines.
  • Fixed invalid XML in phpcs.xml.dist — an unescaped -- inside an XML
    comment (--dev) is illegal in XML and would cause strict parsers to
    reject the whole ruleset file.
  • Added gusteci to the plugin’s Author/Contributors list.

1.3.2

  • Alt text now falls back to the same description text used for the
    Description field, rather than a separate excerpt-only source that was
    frequently empty even when the description itself had usable text.
  • Reverted the 1.3.1 Caption/Description swap: Description (post_content)
    gets the photo’s full description text again, and Caption (post_excerpt)
    goes back to containing only the photographer credit line.

1.3.1

  • Swapped the Caption/Description field mapping: Caption (post_excerpt)
    now gets the photo’s full description text, and Description
    (post_content) gets only the photographer credit line.
  • Removed the (often meaningless, slug-derived) title text from the
    search/browse picker grid. The title is still set on import; it’s just
    no longer shown while browsing.

1.3.0

  • Alt text now falls back to the photo’s excerpt specifically, rather than
    its (potentially much longer) content/description.
  • The Caption field (post_excerpt) now contains only the photographer
    credit line; description text no longer appears in both the Description
    and Caption fields.

1.2.3

  • Alt text now falls back to the photo’s description when the upstream
    API doesn’t expose a dedicated alt-text field of its own — that appears
    to be the closest thing to alt text the Photo Directory API offers.

1.2.2

  • Alt text was coming through empty for photos that, per direct testing,
    do have alt text upstream — the embedded-featured-media lookup this
    plugin relied on apparently isn’t where the Photo Directory’s custom
    post type actually stores it. Added speculative fallback checks
    (alt_text and meta.alt_text/meta._wp_attachment_image_alt on the
    item itself) pending confirmation against a real API response.

1.2.1

  • The upstream Photo Directory substitutes a generic placeholder title
    (e.g. “Photo Detail”) for photos the uploader never titled. That
    placeholder is now recognized and treated as no title, with a fallback
    title derived from the photo’s slug instead (e.g. red-fox-in-snow
    “Red fox in snow”) rather than importing the literal placeholder text.
    Filterable via pdi_generic_title_placeholders.

1.2.0

  • Fixed a bug where the attachment title was copied into the alt text
    field whenever the upstream photo had no alt text of its own, so the
    title and alt text would end up identical (and, for photos with a
    generic placeholder title, both fields would show that placeholder).
    Alt text is now only ever populated from the upstream photo’s own alt
    text, and left blank otherwise.
  • Added a photographer credit line to the imported attachment’s caption
    when the upstream API exposes an author name.
  • Removed the block editor sidebar panel; the native media modal tab
    (added in 1.1.0) covers the same use case without a separate panel.

1.1.0

  • Added a “Photo Directory” tab inside the native WordPress media modal
    (Set Featured Image, Add Media, Image block, etc.), so photos can be
    searched and imported without leaving the picker you’re already in.

1.0.0

  • Initial release.