{"id":376203,"date":"2026-10-16T12:19:50","date_gmt":"2026-10-16T12:19:50","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/photo-directory-importer\/"},"modified":"2026-10-16T13:05:46","modified_gmt":"2026-10-16T13:05:46","slug":"photofetch","status":"publish","type":"plugin","link":"https:\/\/wordpress.org\/plugins\/photofetch\/","author":6250429,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"2.0.0","stable_tag":"trunk","tested":"7.1.3","requires":"5.8","requires_php":"7.4","requires_plugins":null,"header_name":"PhotoFetch","header_author":"ekamran, veeeharris, mattgaldino, telizarose, topher1kenobe, gusteci, michelleames","header_description":"Search the WordPress Photo Directory (wordpress.org\/photos) and import CC0 photos straight into your Media Library \u2014 usable as featured images or anywhere else.","assets_banners_color":"b0aba2","last_updated":"2026-10-16 13:05:46","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"https:\/\/github.com\/topher1kenobe\/photofetch","header_author_uri":"https:\/\/github.com\/topher1kenobe\/PhotoFetch","rating":0,"author_block_rating":0,"active_installs":0,"downloads":74,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"2.0":{"tag":"2.0","author":"topher1kenobe","date":"2026-10-16 13:05:46","revision":3750464}},"upgrade_notice":{"2.0.0":"<p>Renamed to PhotoFetch. Breaking change: file names, text domain, and all internal identifiers changed. Deactivate and remove the old plugin, then install this one fresh.<\/p>","1.3.9":"<p>Corrects the trusted image-host allowlist added in 1.3.7, which could block all imports on some installs via a wrong default in 1.3.7 (and a partial fix in 1.3.8). Recommended if you&#039;re running 1.3.7 or 1.3.8.<\/p>"},"ratings":[],"assets_icons":{"icon-128x128.jpg":{"filename":"icon-128x128.jpg","revision":3750441,"resolution":"128x128","location":"assets","locale":"","width":128,"height":128},"icon-256x256.jpg":{"filename":"icon-256x256.jpg","revision":3750441,"resolution":"256x256","location":"assets","locale":"","width":256,"height":256},"icon.svg":{"filename":"icon.svg","revision":3750441,"resolution":false,"location":"assets","locale":false}},"assets_banners":{"banner-1544x500.jpg":{"filename":"banner-1544x500.jpg","revision":3750391,"resolution":"1544x500","location":"assets","locale":"","width":1544,"height":500},"banner-772x250.jpg":{"filename":"banner-772x250.jpg","revision":3750391,"resolution":"772x250","location":"assets","locale":"","width":772,"height":250}},"assets_blueprints":{},"all_blocks":[],"tagged_versions":["2.0"],"block_files":[],"assets_screenshots":[],"screenshots":[]},"plugin_section":[],"plugin_tags":[6606,84,274731,205],"plugin_category":[50,59],"plugin_contributors":[262067,287109,287107,191508,287108,82933,287106],"plugin_business_model":[],"class_list":["post-376203","plugin","type-plugin","status-publish","hentry","plugin_tags-importer","plugin_tags-media","plugin_tags-photo-directory","plugin_tags-photos","plugin_category-media","plugin_category-utilities-and-tools","plugin_contributors-ekamran","plugin_contributors-gusteci","plugin_contributors-mattgaldino","plugin_contributors-michelleames","plugin_contributors-telizarose","plugin_contributors-topher1kenobe","plugin_contributors-veeeharris","plugin_committers-topher1kenobe"],"banners":{"banner":"https:\/\/ps.w.org\/photofetch\/assets\/banner-772x250.jpg?rev=3750391","banner_2x":"https:\/\/ps.w.org\/photofetch\/assets\/banner-1544x500.jpg?rev=3750391","banner_rtl":false,"banner_2x_rtl":false},"icons":{"svg":"https:\/\/ps.w.org\/photofetch\/assets\/icon.svg?rev=3750441","icon":"https:\/\/ps.w.org\/photofetch\/assets\/icon.svg?rev=3750441","icon_2x":false,"generated":false},"screenshots":[],"raw_content":"<!--section=description-->\n<p>This plugin adds a \"Photo Directory\" search UI to wp-admin that talks to the\npublic Photo Directory REST API.<\/p>\n\n<p>You can search, preview, and import photos directly into your own Media\nLibrary, where the imported image behaves exactly like any regular upload\n\u2014 usable as a featured image, inside blocks, in a gallery, etc.<\/p>\n\n<p>Entry points:<\/p>\n\n<ul>\n<li><strong>Media &gt; Photo Directory<\/strong> \u2014 a dedicated search\/browse admin page.<\/li>\n<li><strong>\"Photo Directory\" button<\/strong> next to Add Media in the classic editor.<\/li>\n<li><strong>\"Photo Directory\" tab<\/strong> inside the native WordPress media picker itself\n(Set Featured Image, Add Media, an Image block's Media Library button,\netc.) \u2014 works in both the classic and block editor, since they share the\nsame underlying media picker.<\/li>\n<\/ul>\n\n<h3>Notes for developers<\/h3>\n\n<ul>\n<li>This is an unofficial integration with a public but not formally\ndocumented API. <code>includes\/class-photofetch-api.php::normalize_item()<\/code> is where\nthe raw JSON from the Photo Directory gets mapped into predictable\n  sizes\/<code>alt<\/code>\/<code>author<\/code> fields \u2014 it tries several plausible shapes\n(<code>media_details.sizes<\/code>, an embedded featured-media object, a bare\n  source_url, etc.). If wordpress.org changes the API's response shape,\nthis is the one function that should need updating.<\/li>\n<li>Title and alt text are sourced independently: alt text prefers a real\nalt-text field if the API exposes one, otherwise falls back to the same\ntext used for the Description field (see <code>PhotoFetch_API::normalize_item()<\/code> for\nthe exact priority order). It's never derived from the title. For title,\nknown upstream placeholder strings (currently \"Photo Detail\", \"Untitled\",\n\"Untitled Photo\" \u2014 see <code>photofetch_generic_title_placeholders<\/code> filter) are\ntreated as no title at all, and a fallback title is derived from the\nphoto's slug instead (e.g. <code>red-fox-in-snow<\/code> \u2192 \"Red fox in snow\"). If\nthe slug itself isn't usable, the title falls back to \"Untitled photo\".<\/li>\n<li>The attachment's Description field (post_content) and alt text both get\nthe photo's full description text; its Caption field (post_excerpt) gets\n<em>only<\/em> the photographer credit line (e.g. \"Photo by Jane Smith, via the\nWordPress Photo Directory.\"), or is left empty if no author name is\navailable.<\/li>\n<li>The search\/browse picker UI doesn't display each photo's title \u2014 many\nphotos on the Photo Directory only have a slug-derived fallback title\n(see above), which isn't meaningful to show while browsing. The title is\nstill set on the imported attachment; it's just not shown in the grid.<\/li>\n<li>Imports are deduplicated: each imported attachment gets a\n  _photofetch_source_id meta value, and re-importing the same photo returns the\nexisting attachment instead of downloading it again.<\/li>\n<li>Image format conversion (Settings &gt; Photo Directory,\n  PhotoFetch_Settings::get_format()) happens in\n  PhotoFetch_Importer::maybe_convert_image(), right after download and before\nsideloading. It re-checks <code>wp_image_editor_supports()<\/code> at import time\nrather than trusting the stored setting outright, and falls back to the\noriginal file on any failure.<\/li>\n<li>Search results are cached for 5 minutes via transients to avoid hammering\nthe upstream API on repeat searches.<\/li>\n<li>All photos on the Photo Directory are released CC0 (no attribution\nrequired), but the plugin stores the original photo URL and author name\n(when available) as <code>_photofetch_source_url<\/code> \/ <code>_photofetch_source_author<\/code> attachment\nmeta for your own reference, in addition to the caption credit above.<\/li>\n<\/ul>\n\n<!--section=installation-->\n<ol>\n<li>Upload the plugin files to the <code>\/wp-content\/plugins\/photofetch<\/code> directory, or install the plugin through the <strong>Plugins &gt; Add New<\/strong> screen in wp-admin directly.<\/li>\n<li>Activate the plugin through the <strong>Plugins<\/strong> screen in WordPress.<\/li>\n<li>That's it \u2014 no configuration is needed and no API key or account is required. Look under <strong>Media &gt; Photo Directory<\/strong>, 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.).<\/li>\n<\/ol>\n\n<!--section=faq-->\n<dl>\n<dt id=\"do%20i%20need%20an%20api%20key%20or%20account%20to%20use%20this%3F\"><h3>Do I need an API key or account to use this?<\/h3><\/dt>\n<dd><p>No. The Photo Directory REST API is public, so the plugin works as soon as it's activated.<\/p><\/dd>\n<dt id=\"do%20i%20need%20to%20credit%20the%20photographer%3F\"><h3>Do I need to credit the photographer?<\/h3><\/dt>\n<dd><p>No \u2014 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.<\/p><\/dd>\n<dt id=\"why%20does%20an%20imported%20photo%27s%20title%20look%20like%20a%20url%20slug%20instead%20of%20a%20real%20title%3F\"><h3>Why does an imported photo's title look like a URL slug instead of a real title?<\/h3><\/dt>\n<dd><p>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 \u2014 worth a glance before publishing.<\/p><\/dd>\n<dt id=\"why%20don%27t%20i%20see%20the%20%22photo%20directory%22%20button%20in%20the%20block%20editor%3F\"><h3>Why don't I see the \"Photo Directory\" button in the block editor?<\/h3><\/dt>\n<dd><p>That button is added next to Add Media via the <code>media_buttons<\/code> hook, which only the classic editor fires. In the block editor, use the \"Photo Directory\" tab inside the native media picker instead \u2014 it opens the same way from Set Featured Image, Add Media, or an Image block's Media Library button.<\/p><\/dd>\n<dt id=\"can%20i%20choose%20what%20size%20image%20gets%20imported%3F\"><h3>Can I choose what size image gets imported?<\/h3><\/dt>\n<dd><p>Yes. Both the Media &gt; Photo Directory browse screen and the media picker tab let you choose Full, Large, or Medium before importing.<\/p><\/dd>\n<dt id=\"can%20imported%20photos%20be%20converted%20to%20webp%20or%20avif%3F\"><h3>Can imported photos be converted to WebP or AVIF?<\/h3><\/dt>\n<dd><p>Yes, if your server's image editor supports it. Go to <strong>Settings &gt; Photo Directory<\/strong> to choose \u2014 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.<\/p><\/dd>\n<dt id=\"where%20does%20the%20plugin%20store%20which%20photos%20i%27ve%20already%20imported%3F\"><h3>Where does the plugin store which photos I've already imported?<\/h3><\/dt>\n<dd><p>Each imported attachment gets a <code>_photofetch_source_id<\/code> meta value matching its ID on the Photo Directory. Importing the same photo again returns the existing attachment instead of downloading a duplicate.<\/p><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>2.0.0<\/h4>\n\n<p>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:\n* Main plugin file renamed from <code>photo-directory-importer.php<\/code> to <code>photofetch.php<\/code>; every <code>includes\/class-pdi-*.php<\/code> file renamed to <code>includes\/class-photofetch-*.php<\/code>; the bundled <code>.pot<\/code> file renamed to <code>languages\/photofetch.pot<\/code>.\n* The <code>Text Domain<\/code> (and every translation call's domain argument) changed from <code>photo-directory-importer<\/code> to <code>photofetch<\/code>, matching the new slug \u2014 same requirement as the 1.3.14 and 1.3.16 fixes.\n* Unlike 1.3.16, this time the internal-only identifiers were deliberately included: all <code>PDI_*<\/code> class names (<code>PDI_API<\/code>, <code>PDI_Importer<\/code>, <code>PDI_Plugin<\/code>, <code>PDI_Settings<\/code>) are now <code>PhotoFetch_API<\/code>, <code>PhotoFetch_Importer<\/code>, <code>PhotoFetch_Plugin<\/code>, <code>PhotoFetch_Settings<\/code>; the <code>PDI_*<\/code> constants are now <code>PHOTOFETCH_*<\/code>; every <code>pdi_<\/code>-prefixed hook, filter, nonce, AJAX action, option, and meta key (including the <code>_pdi_source_id<\/code>\/<code>_pdi_source_url<\/code>\/<code>_pdi_source_author<\/code>\/<code>_pdi_imported<\/code> attachment meta) is now <code>photofetch_<\/code>-prefixed; and every <code>pdi-*<\/code> CSS class, script\/style handle, and HTML id is now <code>photofetch-*<\/code>.\n* 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 \u2014 only the identifiers above changed, not what they do.<\/p>\n\n<h4>1.3.17<\/h4>\n\n<ul>\n<li>Fixed the phpcs findings from a fresh run against the renamed plugin:\nan equals-sign alignment warning in <code>class-pdi-plugin.php<\/code>'s author-link\nrewriter, missing short descriptions in three doc comments (one each in\n  class-pdi-importer.php and <code>class-pdi-settings.php<\/code>, two total in the\nlatter), and <code>languages\/index.php<\/code>'s file comment switched from a <code>\/\/<\/code>\nline comment to the required <code>\/** ... *\/<\/code> docblock style.<\/li>\n<\/ul>\n\n<h4>1.3.16<\/h4>\n\n<ul>\n<li>Renamed the plugin from \"WP Photo Directory Importer\" to \"Photo\nDirectory Importer\" \u2014 WordPress.org's plugin guidelines prohibit \"WP\" or\n\"WordPress\" in a plugin's name, since it implies an official affiliation\nthe plugin doesn't have. This touched everything tied to the old name:\nthe <code>Plugin Name<\/code> header, the main plugin file\n(<code>wp-photo-directory-importer.php<\/code> \u2192 <code>photo-directory-importer.php<\/code>),\nthe <code>Text Domain<\/code> (and every translation call's domain argument, to\nmatch \u2014 same requirement as the 1.3.14 fix), the bundled <code>.pot<\/code> file's\nname and contents, and the <code>@package<\/code> docblock tag across every PHP\nfile. Internal-only identifiers (the <code>PDI_*<\/code> class names, <code>pdi_<\/code>-\nprefixed hooks\/filters\/nonces, <code>pdi-*<\/code> CSS\/JS handles) were left\nuntouched, since none of them spell out \"WP\" or \"WordPress\" and\nchanging them would be an unrelated, unnecessary breaking change.<\/li>\n<\/ul>\n\n<h4>1.3.15<\/h4>\n\n<ul>\n<li>Fixed incorrect plural handling in the browse screen and media-picker\ntab: several \"N selected\"\/\"N imported\" strings picked between exactly\ntwo hardcoded forms using a <code>count === 1<\/code> check in JavaScript \u2014 correct\nfor English, but impossible for a translator to adapt to a language\nwith three, four, or six plural forms (Russian, Polish, and Arabic,\namong others). Now uses <code>wp.i18n._n()<\/code>, which consults the current\nlocale's real plural-forms rule instead. Along the way this also fixed\na visible English-language bug: the single-photo \"quick import\" success\nmessage was using the plural template with the count hardcoded to 1,\nproducing \"1 photos imported.\"<\/li>\n<li>Added a bundled <code>.pot<\/code> file (<code>languages\/photo-directory-importer.pot<\/code>)\nand a <code>Domain Path<\/code> header, so translators have a starting point instead\nof needing to generate one themselves before they can begin.<\/li>\n<\/ul>\n\n<h4>1.3.14<\/h4>\n\n<ul>\n<li>Fixed a Plugin Check error: \"Mismatched text domain. Expected\n'photo-directory-importer' but got 'pdi'.\" WordPress.org requires the\n  Text Domain header, and every translation function's domain argument,\nto exactly match the plugin's own folder\/slug. Changed the domain from\n  pdi to <code>photo-directory-importer<\/code> everywhere: the plugin header\nand all translation function calls across every PHP and JS file.\nInternal-only identifiers that happen to start with <code>pdi_<\/code> (hooks,\nfilters, nonces, meta keys, transient keys) were deliberately left\nuntouched, since those aren't text domains.<\/li>\n<li>Reformatted a handful of PHP statements that became excessively long\npurely from the domain string's length.<\/li>\n<\/ul>\n\n<h4>1.3.12<\/h4>\n\n<ul>\n<li>On the Plugins list page, each contributor's name now links to their\nown wordpress.org profile (<code>https:\/\/profiles.wordpress.org\/username<\/code>)\ninstead of the whole comma-separated author list linking to one shared\nGitHub URL \u2014 the standard plugin header format only supports one link\nfor the entire \"Author\" string, so this rewrites the row directly via\nthe <code>all_plugins<\/code> filter.<\/li>\n<li>Added a \"Settings\" link to the plugin's row on the Plugins page,\nappearing first among the action links, right next to \"Deactivate\".<\/li>\n<\/ul>\n\n<h4>1.3.11<\/h4>\n\n<ul>\n<li>Added a quality field to <strong>Settings &gt; Photo Directory<\/strong>, shown only\nwhen converting to WebP or AVIF (hidden automatically when \"Keep\noriginal format\" is selected \u2014 quality has no meaning there, since\nnothing gets re-encoded). Defaults to 82, matching WordPress core's own\ndefault JPEG compression quality. Clamped to 1\u2013100 on save regardless\nof what's posted.<\/li>\n<\/ul>\n\n<h4>1.3.10<\/h4>\n\n<ul>\n<li>Added a <strong>Settings &gt; Photo Directory<\/strong> page. If this server's image\neditor (GD or Imagick, whichever core picked) can actually produce\nWebP \u2014 checked live via <code>wp_image_editor_supports()<\/code>, not assumed from\nPHP version or extension presence \u2014 a format picker appears: keep the\noriginal format, convert to WebP, or convert to AVIF (the latter only\noffered when supported too). WebP is the default whenever it's\navailable. If WebP isn't supported, the page simply says so and no\nphotos are converted, same as before this release.<\/li>\n<li>Conversion happens right after download, before the file is sideloaded,\nso every generated thumbnail\/medium\/large sub-size is produced from the\nconverted file directly rather than converted a second time after the\nfact.<\/li>\n<li>A conversion failure for any reason falls back to importing the\noriginal file untouched \u2014 this is meant to be a nice-to-have, never\nsomething that can turn a failed conversion into a failed import.<\/li>\n<li>The \"Import settings\" link in the Media &gt; Photo Directory page header,\npresent but hidden since 1.3.4, now points at the new settings page.<\/li>\n<\/ul>\n\n<h4>1.3.9<\/h4>\n\n<ul>\n<li>Added <code>w.org<\/code> to the default trusted image hosts (<code>pdi_allowed_image_hosts<\/code>).\nThe Photo Directory serves its images from <code>pd.w.org<\/code> specifically \u2014\n  w.org is WordPress.org's own short domain, the same family as\n  s.w.org, its well-known static-assets host. The 1.3.8 fix (adding\n  wp.com for Photon) turned out not to be where these images actually\nlive; this corrects it based on the real rejected host.<\/li>\n<\/ul>\n\n<h4>1.3.8<\/h4>\n\n<ul>\n<li><strong>Fixed a regression from 1.3.7 that broke every import.<\/strong> The trusted-host\ncheck added in 1.3.7 only allowed <code>wordpress.org<\/code> (and its subdomains),\nbut the Photo Directory serves uploaded images through Automattic's\nPhoton CDN (<code>i0.wp.com<\/code>, <code>i1.wp.com<\/code>, etc.) rather than from\nwordpress.org's own domain directly \u2014 so every single import was\nrejected as \"untrusted.\" <code>wp.com<\/code> (and its subdomains) is now included\nin the default allowed hosts.<\/li>\n<li>The \"not on a trusted host\" error message now includes the actual\nrejected hostname, so a similar mismatch in the future is immediately\ndiagnosable without digging through network requests, and can be\nresolved on the spot via the <code>pdi_allowed_image_hosts<\/code> filter.<\/li>\n<\/ul>\n\n<h4>1.3.7<\/h4>\n\n<p>Security review and hardening. No user-facing behavior changes; all four\nitems below were fixed as defense-in-depth rather than in response to an\nexploited vulnerability.<\/p>\n\n<ul>\n<li><strong>Sideloaded image URLs are now pinned to a trusted host.<\/strong>\n  PDI_Importer::import_photo() downloads whichever image URL the Photo\nDirectory API returns for a photo; that URL is data returned <em>by<\/em> the\nAPI, not something this plugin generates itself. Added\n  is_allowed_image_host(), checked before <code>download_url()<\/code> is ever\ncalled, requiring the URL's host to be <code>wordpress.org<\/code> or a subdomain of\nit (filterable via <code>pdi_allowed_image_hosts<\/code>). Nothing a site visitor or\nlower-privileged user supplies can influence which host this plugin\ntalks to for search or photo lookups \u2014 those always go to the hardcoded\n  REMOTE_BASE\/<code>TAXONOMY_BASE<\/code> constants \u2014 so this specifically guards\nagainst a compromised or malicious upstream API response causing this\nsite to download and store a file from an attacker-controlled server.<\/li>\n<li><strong>Removed an unused <code>innerHTML<\/code> code path.<\/strong> <code>assets\/js\/admin.js<\/code>'s <code>el()<\/code>\nhelper had a dead <code>'html'<\/code> attribute branch (<code>e.innerHTML = attrs.html<\/code>)\nthat nothing in the codebase called, but that would become an XSS sink\nthe moment anything did. Removed it entirely.<\/li>\n<li><strong>Added <code>rel=\"noopener noreferrer\"<\/code><\/strong> to the \"View in Media Library\"\nlink in <code>assets\/js\/admin.js<\/code>, which opens in a new tab\n(<code>target=\"_blank\"<\/code>). Its destination is fully server-generated today, so\nthis wasn't exploitable, but it's standard, zero-cost protection against\nreverse-tabnabbing regardless.<\/li>\n<li><strong>Added explicit <code>return<\/code> statements after every <code>wp_send_json_error()<\/code>\ncall<\/strong> in <code>class-pdi-api.php<\/code> (<code>ajax_search()<\/code>, <code>ajax_terms()<\/code>) and\n  class-pdi-importer.php (<code>ajax_import()<\/code>) that wasn't already the last\nstatement in its function. <code>wp_send_json_error()<\/code> calls <code>wp_die()<\/code>,\nwhich WordPress's AJAX handling turns into an immediate <code>die()<\/code>, so\nexecution already halted in normal operation \u2014 this wasn't currently\nexploitable. But code after those calls was relying on that implicit\nbehavior instead of stating it, which would fall through with\ninvalid\/stale state (e.g. continuing with a photo ID of 0, or treating a\n  WP_Error object as an array) if <code>wp_die()<\/code>'s behavior were ever\nfiltered, as some testing harnesses deliberately do.<\/li>\n<\/ul>\n\n<h4>1.3.6<\/h4>\n\n<ul>\n<li>Added a \"View full\" button to the \"Photo Directory\" tab inside the\nnative media picker (Set Featured Image, Add Media, etc.), next to the\n\"Import only\" button in the footer. Opens the currently-detailed\nphoto's full-size image in a plain, image-only overlay (closes on\nEscape, clicking outside the image, or the close button).<\/li>\n<\/ul>\n\n<h4>1.3.5<\/h4>\n\n<ul>\n<li>Added a \"View full\" button to every photo card on the Media &gt; Photo\nDirectory browse screen. Opens the photo's full-size image in a\nlightbox (closes on Escape or clicking outside the image), which\nincludes its own Import button (or a \"View in library\" link, once the\nphoto has been imported).<\/li>\n<\/ul>\n\n<h4>1.3.4<\/h4>\n\n<ul>\n<li>Rebuilt <strong>Media &gt; Photo Directory<\/strong> as a filterable browse screen\n(<code>assets\/js\/photo-browser.js<\/code>, React via <code>wp-element<\/code>): category,\norientation, and color filters populated live from the Photo\nDirectory's own taxonomies; sort by relevance or newest; multi-select\nwith a bulk-import tray; per-photo title\/alt\/caption edits and a\nphotographer-credit toggle before import.<\/li>\n<li>Rebuilt the \"Photo Directory\" tab inside the native media picker as a\ntwo-pane picker (thumbnail grid + detail sidebar) with the same\nfilters, matching the browse screen.<\/li>\n<li>Photos already in the Media Library are now flagged in bulk for a\nwhole result page at once (<code>PDI_Importer::find_existing_attachments()<\/code>)\ninstead of one lookup per card.<\/li>\n<li><code>_embed<\/code> now requests only the two relations the plugin reads\n(<code>author,wp:featuredmedia<\/code>) instead of every relation the API would\notherwise expand, avoiding an unnecessary <code>wp:term<\/code> expansion per\ntaxonomy per photo.<\/li>\n<li>Title fallback now prefers a short title derived from the photo's own\ndescription over a humanized slug, since most Photo Directory slugs are\nopaque hex strings (e.g. <code>6836a813f7<\/code>) with no human meaning to\nhumanize in the first place.<\/li>\n<li>Normalized photo data now includes width, height, MIME type, and file\nsize, and the computed credit line is available directly on the photo\nobject for the new UI to prefill.<\/li>\n<li>Added Michelle Frechette as a contributor.<\/li>\n<li>Removed the bundled <code>LICENSE<\/code> file in favor of linking the license\nonline.<\/li>\n<li>Fixed the search field's icon overlapping its placeholder text.<\/li>\n<\/ul>\n\n<h4>1.3.3<\/h4>\n\n<ul>\n<li>Sanitize untrusted fields from the Photo Directory API before use: the\nslug now goes through <code>sanitize_title()<\/code>, the author name through\n  wp_strip_all_tags()\/<code>trim()<\/code>, and every image URL through\n  esc_url_raw().<\/li>\n<li>Moved the <code>wp-admin\/includes\/{media,file,image}.php<\/code> requires from\nfile scope into <code>import_photo()<\/code> itself, so they're only parsed while\nan import is actually happening rather than on every page load\n(this file loads on every request via <code>plugins_loaded<\/code>, including the\nfront end).<\/li>\n<li>Fixed a phpcs suppression bug: the slow-query warning for\n  meta_key\/<code>meta_value<\/code> was suppressed with a single <code>phpcs:ignore<\/code>\ncomment, which only covers the next line and so didn't actually\nsuppress the <code>meta_value<\/code> warning. Switched to <code>phpcs:disable<\/code>\/\n  phpcs:enable spanning both lines.<\/li>\n<li>Fixed invalid XML in <code>phpcs.xml.dist<\/code> \u2014 an unescaped <code>--<\/code> inside an XML\ncomment (<code>--dev<\/code>) is illegal in XML and would cause strict parsers to\nreject the whole ruleset file.<\/li>\n<li>Added gusteci to the plugin's Author\/Contributors list.<\/li>\n<\/ul>\n\n<h4>1.3.2<\/h4>\n\n<ul>\n<li>Alt text now falls back to the same description text used for the\nDescription field, rather than a separate excerpt-only source that was\nfrequently empty even when the description itself had usable text.<\/li>\n<li>Reverted the 1.3.1 Caption\/Description swap: Description (post_content)\ngets the photo's full description text again, and Caption (post_excerpt)\ngoes back to containing only the photographer credit line.<\/li>\n<\/ul>\n\n<h4>1.3.1<\/h4>\n\n<ul>\n<li>Swapped the Caption\/Description field mapping: Caption (post_excerpt)\nnow gets the photo's full description text, and Description\n(post_content) gets only the photographer credit line.<\/li>\n<li>Removed the (often meaningless, slug-derived) title text from the\nsearch\/browse picker grid. The title is still set on import; it's just\nno longer shown while browsing.<\/li>\n<\/ul>\n\n<h4>1.3.0<\/h4>\n\n<ul>\n<li>Alt text now falls back to the photo's excerpt specifically, rather than\nits (potentially much longer) content\/description.<\/li>\n<li>The Caption field (post_excerpt) now contains only the photographer\ncredit line; description text no longer appears in both the Description\nand Caption fields.<\/li>\n<\/ul>\n\n<h4>1.2.3<\/h4>\n\n<ul>\n<li>Alt text now falls back to the photo's description when the upstream\nAPI doesn't expose a dedicated alt-text field of its own \u2014 that appears\nto be the closest thing to alt text the Photo Directory API offers.<\/li>\n<\/ul>\n\n<h4>1.2.2<\/h4>\n\n<ul>\n<li>Alt text was coming through empty for photos that, per direct testing,\ndo have alt text upstream \u2014 the embedded-featured-media lookup this\nplugin relied on apparently isn't where the Photo Directory's custom\npost type actually stores it. Added speculative fallback checks\n(<code>alt_text<\/code> and <code>meta.alt_text<\/code>\/<code>meta._wp_attachment_image_alt<\/code> on the\nitem itself) pending confirmation against a real API response.<\/li>\n<\/ul>\n\n<h4>1.2.1<\/h4>\n\n<ul>\n<li>The upstream Photo Directory substitutes a generic placeholder title\n(e.g. \"Photo Detail\") for photos the uploader never titled. That\nplaceholder is now recognized and treated as no title, with a fallback\ntitle derived from the photo's slug instead (e.g. <code>red-fox-in-snow<\/code> \u2192\n\"Red fox in snow\") rather than importing the literal placeholder text.\nFilterable via <code>pdi_generic_title_placeholders<\/code>.<\/li>\n<\/ul>\n\n<h4>1.2.0<\/h4>\n\n<ul>\n<li>Fixed a bug where the attachment title was copied into the alt text\nfield whenever the upstream photo had no alt text of its own, so the\ntitle and alt text would end up identical (and, for photos with a\ngeneric placeholder title, both fields would show that placeholder).\nAlt text is now only ever populated from the upstream photo's own alt\ntext, and left blank otherwise.<\/li>\n<li>Added a photographer credit line to the imported attachment's caption\nwhen the upstream API exposes an author name.<\/li>\n<li>Removed the block editor sidebar panel; the native media modal tab\n(added in 1.1.0) covers the same use case without a separate panel.<\/li>\n<\/ul>\n\n<h4>1.1.0<\/h4>\n\n<ul>\n<li>Added a \"Photo Directory\" tab inside the native WordPress media modal\n(Set Featured Image, Add Media, Image block, etc.), so photos can be\nsearched and imported without leaving the picker you're already in.<\/li>\n<\/ul>\n\n<h4>1.0.0<\/h4>\n\n<ul>\n<li>Initial release.<\/li>\n<\/ul>","raw_excerpt":"Search the WordPress Photo Directory (wordpress.org\/photos) and import CC0 photos straight into your Media Library.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/376203","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=376203"}],"author":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/topher1kenobe"}],"wp:attachment":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=376203"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=376203"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=376203"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=376203"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=376203"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=376203"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}