Version 3.3.0

September 16, 2026

Fixed

  • Metadata sync works on Craft 5. The sync was built on Craft 4's content table: it read Field::columnPrefix (removed in Craft 5) and ran raw SQL against craft\db\Table::CONTENT (undefined in Craft 5), so pressing Sync metadata under Utilities threw immediately. The sync now finds Imageshop values through Craft's field layouts and element queries (elements.fieldLayoutId), which works identically on Craft 4 per-field columns and Craft 5 elements_sites.content JSON, and covers nested entries and Matrix blocks because they are elements with their own layouts. Fixes #8.
  • An Imageshop outage no longer advances the sync window. _request() used to return null for "no token", a transport error and any non-200 alike, so a sync run during an outage was recorded as a successful empty run and the next run only asked for documents changed after it, silently skipping everything changed in between. Failures now surface as false, getRecentlyUpdatedDocumentIds() returns null when Imageshop could not be asked, and such a run is logged as failed without touching the watermark. When only some document requests fail, the fetched documents are applied and the run is logged as partial with the watermark kept, so the rest is retried next time. The watermark is also taken before the changed-ids request rather than after all fetches, so a document changed mid-run is reported to the next run.
  • Queued sync jobs can no longer be starved by a later run. Jobs read the document cache at execution time from a single row that every run overwrote, so a second sync (even one finding no changes) started before the queue drained made the older jobs no-ops and those changes were never retried. Each run now stores its own snapshot (imageshop-dam_sync keeps the last 100) and every job carries the id of the run that queued it plus its document ids; a job whose snapshot has since been pruned refetches those documents from the API instead of doing nothing.
  • Sync runs applied out of order cannot roll metadata back. With two queue workers or a retried job, a job from an earlier run could apply its older snapshot after a later run had already written newer text. The stored image JSON now records the document's Changed timestamp it was synced from (sync.changed), and mapDocumentFields() ignores a snapshot older than that.
  • Sync jobs queued by an older version no longer fail to unserialize. jobs\Sync used to declare documentIds as a string; reusing the name with an array type made PHP reject legacy payloads before the no-op guard could run. The job now uses different property names for its new payload and allows legacy dynamic properties, so an old job unserializes and finishes as a no-op.
  • array_is_list() (PHP 8.1) replaced with ArrayHelper::isIndexed() in the field normalizer and the API client, so Craft 4 sites on PHP 8.0 are not broken by the sync.
  • A queue-mode run only advances the sync window once every job it queued has succeeded. The run row now records the watermark it will vouch for and how many jobs it queued; each job reports completion or failure, and the last successful job moves the watermark. A job that fails leaves the run open, so the next sync run asks Imageshop for the same changes again and queues the element afresh, instead of the element only being recoverable by a manual retry from the queue. Requires the new m260916_000000_add_sync_run_columns migration; run php craft migrate/all after updating (plugin schemaVersion is now 2.2.0).
  • Discovery works on Craft 4 as well as Craft 5. Fields::getAllLayouts() only exists in Craft 5, so layouts are now collected per element type via getLayoutsByType(). Craft 4 leaves elements.fieldLayoutId NULL for entries and only fills it for Matrix blocks, so entries are narrowed by entry type instead, Craft 4 Matrix blocks are scanned one Matrix field at a time (their query only joins the per-field content table when it knows the field), and other element types fall back to a full scan where the column is NULL. Verified on Craft 4.18.8 with a Matrix field holding an Imageshop sub-field, and on Craft 5.10.2.
  • jobs\UpdateCache now runs a full sync instead of only refreshing the document cache, since a fetch on its own would never advance the window. The deprecated updateRecentlyUpdatedCache() + updateImages() pair still works: the watermark advances when the queued jobs complete.
  • Failure wording no longer claims the API was unreachable: a 401, 403 or 500 is reported as a failed request, with the detail in the Craft logs.
  • A crash while queueing sync jobs can no longer skip documents. The snapshot and its watermark were written before jobs were pushed, so a process dying mid-enqueue left the remaining elements without jobs while the next run only asked for later changes. A run now stores its snapshot with the previous watermark, queues its jobs (or saves its elements inline), and advances the watermark as the last step. A retried run may queue a duplicate job for an element already updated; that job finds nothing to change and skips the save.
  • A sync save that fails is reported, not swallowed. syncElement() returned false both for "nothing to change" and for a save a hook vetoed, so a failed update completed as a successful job. It now throws on a failed save: a queue job shows as failed with the reason and can be retried from the queue, and an inline run counts it, is logged as partial and holds the watermark so the element is retried next run.
  • The sync cannot overwrite an override saved while it runs. Each element is updated inside a transaction holding a row lock on the element, so an editor's save either lands before the sync reads the element (and is preserved) or waits until the sync has written.
  • A 200 response whose body is not JSON is now a retryable failure rather than "no such document"; a literal null body (what the API returns for a nonexistent document) is still treated as no document. The Guzzle client no longer throws on HTTP error statuses, so they are classified explicitly.
  • Language codes are canonicalized in one place. A per-site mapping such as en-US was passed raw to the CP (so overrides were stored under en-US) while explicit getter arguments were normalized to en, so the override could not be read back. getImageshopLanguageForSite() and setSiteLanguage() now run every code through sanitizeLanguage(), matching what explicit arguments already did. The global-language fallback is normalized too, so a legacy nb-NO setting resolves no text instead of nothing.
  • The sync goes through the element lifecycle instead of a raw UPDATE. Each affected element/site is loaded, given the refreshed field value and saved with Elements::saveElement(), so element caches are invalidated (Craft tags the owner of a nested element too), the search index is updated and afterSave listeners such as Blitz react. Elements are saved with resaving = true, so dateUpdated is not bumped, and validation is skipped so an unrelated invalid field cannot block a metadata refresh. Drafts and provisional drafts are synced; revisions are skipped. Fixes #9.

Changed

  • Alt text and descriptions entered in Craft survive a sync. Editor text is now stored in an overrides block in the image JSON, separate from the text block the sync writes. getAltText() / getDescription() return the override when one exists and the synced text otherwise; the CP shows the synced text as the field placeholder and an empty field means "use the Imageshop text". Text entered before this version lives in the synced slot and is replaced on the next sync, exactly as before; re-enter it to make it an override. An override cannot blank out a synced value. Fixes the second half of #11.
  • The sync now also fetches text for the Imageshop language of every Craft site, and mapDocumentFields() adds a language block that was missing from a stored value. In-use documents missing a text block for a site language are fetched even if they did not change in Imageshop, so a site added after an image was picked receives its text on the next sync.
  • The sync history column "Jobs queued" is now "Elements": the number of element/site combinations queued, or updated for --inline runs. Runs can also show Partial and Failed.
  • services\ImageShop::getImageShopFields(), getAllImageShopContentRows() and updateContentRow() are removed. updateRecentlyUpdatedCache() and updateImages() remain as deprecated wrappers around the new sync component. jobs\Sync now carries elementType / elementId / siteId / fieldHandles instead of content-row ids.

Added

  • php craft imageshop-dam/sync/run [--inline] and php craft imageshop-dam/sync/status console commands, so the sync can be scheduled with cron. --inline updates elements immediately instead of queueing jobs, for hosts without a queue runner. Fixes the first half of #11.
  • ImageShop::getInstance()->sync (services\Sync) with findUsages(), updateRecentlyUpdatedCache(), queueSyncJobs(), syncElement(), applyDocumentCache() and run().
  • Model getters getSyncedAltText(), getSyncedDescription(), getOverride(), hasOverride() and getOverrides().
  • test-sync.php in the dev harness, covering override semantics, mapDocumentFields(), usage discovery and an end-to-end sync against the API when a token is configured.

Version 3.2.1

September 16, 2026

Fixed

  • Alt text and description edits are stored under the correct site language for Matrix blocks and slideout editors. The CP field resolved its language from the site query param, which is only present on full page loads. Craft 5 renders newly added Matrix blocks (and element slideouts) over AJAX with just a siteId body param, so a block added on a non-primary site rendered its "Alternative text" / "Description" editors labelled with the primary site's language, and whatever the editor typed was written into that language's text block. On reload the labels switched to the correct language and the text appeared to have moved to another site. ImageShopField::getCurrentAdminLanguage() now takes the element being edited and resolves the language from its siteId first, falling back to the site / siteId request params and finally the primary site. getInputHtml() passes the resolved language to the template and the picker culture option instead of re-deriving it from the request. Request params are only read on web requests, so the method is safe to call from console commands and tests.

Version 3.2.0

August 11, 2026

Fixed

  • Permalinks are no longer regenerated when Craft's cache is cleared. /Permalink/CreatePermaLinkFromDocumentId is not idempotent — every call mints a brand new permalink. Because getCachedPermalink() stored results in Craft's data cache, any cache flush (a deploy running clear-caches, a container restart, or simply a second app node with its own file cache) created a fresh URL that the CDN had never seen, costing several seconds on the next load. Permalinks now live in a new imageshop-dam_permalinks table keyed on documentId + width + height, so the create call happens once per derivative and survives deploys, restarts and multi-node setups. A cache miss can no longer reach the API at all.
  • Failed permalink lookups are no longer retried on every request. The previous code cached false on failure, but Yii's getOrSet() treats a stored false as a cache miss, so a failing derivative was re-requested on every single page render. Failures are now held in a short back-off (5 minutes) so an API outage cannot turn one page into dozens of failing round trips.
  • Added connect (5s) and total (10s) timeouts to the API client. Guzzle previously fell back to PHP's default_socket_timeout, so an unresponsive API could stall a page render for up to a minute per image.
  • getResizedUrl() / getSrcset() no longer emit the string "Access denied" into src / srcset. For a deleted or inaccessible document the Permalink API answers 200 with a JSON body whose url is the literal string "Access denied", which passed the isJsonObject() guard and was returned as if it were a URL — rendering <img src="Access denied"> on the front end and in the CP field preview. Permalink values are now validated as http(s) URLs before being returned or stored, so the existing ?? $this->getUrl() fallback and the getSrcset() null-skip behave correctly. Combined with the failure back-off above, a restored image now recovers in minutes rather than being broken for 30 days. Fixes #14.
  • The settings cog now reliably opens the alt text / description panel. The panel is hidden in markup with the hidden attribute, but the cog handler toggled inline display via jQuery's .toggle(). That only wins while no stylesheet declares [hidden] { display: none !important } — under such a rule the handler still sets display: block, the panel stays invisible, and because jQuery decides show-vs-hide by measuring the element it then reads as hidden on every subsequent click and can never be opened. Nothing is logged and no request is made, so the cog simply appears dead. The handler now toggles the hidden attribute itself, so a single mechanism owns visibility. The click binding is also namespaced and unbound before rebinding, so repeated preview re-renders cannot stack duplicate handlers. Fixes #7.
  • Picker field settings are honoured again. Show size dialogue, Show crop dialogue, Edit description before insert and Allow multiple were always sent to the picker as true. The field JS posts these form-encoded, so a JavaScript false arrives as the string "false" — and (bool) "false" is true, forcing every option on. PickerController::actionGetUrl() now reads them with FILTER_VALIDATE_BOOLEAN. Fixes #10.

Added

  • services\ImageShop::clearPermalinks(?int $documentId = null) for invalidating stored permalinks, either for one document or all of them.
  • php craft imageshop-dam/permalinks/stats and php craft imageshop-dam/permalinks/clear console commands. Stored permalinks never expire — Imageshop have confirmed a permalink keeps resolving to the current image when that image is replaced under the same document id, so there is nothing to invalidate on a schedule. clear is the manual escape hatch for the exceptions; it takes --document-id to limit the blast radius, and clearing everything requires --force when run non-interactively, since every deleted permalink is recreated cold on the next request.
  • test-permalinks.php covering permalink durability, including an explicit regression test that a full cache flush does not mint a new permalink.

Changed

  • Requested dimensions are clamped to 5000px. The permalink/get-hq-url action is allowAnonymous and takes width/height straight from query params, which now write durable rows rather than cache entries.
  • Multi-width getSrcset() calls resolve from a single database read per document instead of one lookup per width.
  • Plugin schemaVersion bumped to 2.1.0; run php craft migrate/all after updating.

Version 3.1.4

June 17, 2026

Fixed

  • Private key is no longer required. The Imageshop API only needs the persistent token (sent as the Token: request header) to mint a temporary picker token — privateKey is documented as optional. Previously the plugin enforced key as required at three layers (settings UI red asterisk, Settings model validation rule, and the _request() / getTemporaryToken() guard rails), so token-only accounts could not save settings or open the image picker. Now: the Key field is optional in the settings form, the model only requires token + language, and the service includes privateKey in the temporary-token request only when it is set.

Version 3.1.3

June 16, 2026

Fixed

  • Installation instructions now work end-to-end. The Composer package name, the Craft plugin handle, and the README all consistently reference imageshop-dam. Previously, 3.1.1 changed the plugin's handle to imageshop-plugin and 3.1.2 also renamed the Composer name field, but the package remained published on Packagist as imageshop/imageshop-dam — leaving three mismatching identifiers and breaking both composer require imageshop/imageshop-plugin (404 on Packagist) and php craft plugin/install imageshop-dam (handle not found). Fixes #6.

Version 3.1.2

May 28, 2026

Changed

  • Renamed the plugin's composer.json name field to imageshop/imageshop-plugin. Packagist continued to publish the package as imageshop/imageshop-dam, so this rename was effectively invisible to installers — and contributed to the installation mismatch reported in #6. Reverted in 3.1.3.

Version 3.1.1

May 28, 2026

Changed

  • Changed the Craft plugin handle from imageshop-dam to imageshop-plugin (also fixing the unreleased imageshop-da typo from 3.1.0 prep). This caused the Craft Plugin Store's "Install" button — which calls php craft plugin/install imageshop-dam — to fail with "No plugin exists with the handle imageshop-dam". Reverted in 3.1.3.

Version 3.1.0

May 26, 2026

Changed

  • Rebranded from webdna/imageshop-dam to imageshop/imageshop-dam. PHP namespace changed to Imageshop\Imageshop\…; display name changed to "Imageshop DAM". Plugin handle remained imageshop-dam.

Version 3.0.0

May 21, 2026

Added

Updating packagist setup and release for main to support craft 4 and craft 5.

Version 2.6.0

April 20, 2026

Added

  • Per-site language mapping. New plugin setting "Site language mappings" lets editors override which Imageshop language code is used for each Craft site. Example: a Norwegian Bokmål (nb-NO) Craft site can be mapped to pull Nynorsk (nn) texts from Imageshop. Empty entries fall back to the auto-derived code (existing behavior). Applies to the picker CULTURE, admin field labels (data-current-language), ImageShop model text getters (getAltText(), getDescription(), getTitle(), getCredits(), getRights(), getTags()), and all GraphQL text resolvers.
  • services\ImageShop::getImageshopLanguageForSite(?Site $site) resolver: checks the per-site mapping → falls back to sanitizeLanguage($site->language) → falls back to the global $settings->language.
  • models\Settings::$siteLanguages array property (keyed by Craft site handle), exposed in the settings UI as one text input per site with the auto-derived code as placeholder.
  • Optional $lang parameter on models\ImageShop::getAdminLabel($lang = null).

Fixed

  • Admin field card title now respects the site's resolved Imageshop language. Previously, after picking an image, the AJAX re-render path (ContentController::actionGetImageList) built fresh ImageShop models without a site language, so getAdminLabel() fell back to the CP's current-site language (often English) instead of the entry site's mapped Imageshop language. The controller now calls setSiteLanguage() on each model and input-list.twig passes the template's language variable explicitly to getAdminLabel().

Changed

  • ImageShopField::getCurrentAdminLanguage() and ImageShopField::normalizeValue() now route through the new getImageshopLanguageForSite() resolver instead of calling sanitizeLanguage() directly on the site language.
  • models\ImageShop::getLang() defaults to the resolver when neither an explicit $lang nor a pre-set _siteLanguage is available, so models constructed outside normalizeValue() still honour the per-site mapping.

Version 2.5.1

April 16, 2026

Security

  • The persistent Imageshop API token is no longer exposed in the picker popup URL. Previously IMAGESHOPTOKEN was rendered server-side into the entry edit page, leaking the long-lived token to browser history, devtools, referer headers and proxy logs. The field now requests a fresh short-lived token from a new CSRF-protected CP action (imageshop-dam/picker/get-url) just before the popup opens, so the long-lived token never leaves the server. Requires no configuration changes.

Added

  • PickerController::actionGetUrl CP action that mints a picker URL with a short-lived token. Requires accessCp permission and a valid CSRF token.
  • services\ImageShop::getPickerUrl($options) helper that whitelists picker options and builds the popup URL with a fresh temporary token.

Changed

  • services\ImageShop::getTemporaryToken() now explicitly returns ?string and parses the API response (handles both raw string and JSON-encoded string shapes).
  • ImageShopField::getInputHtml() no longer builds the picker URL; it passes non-sensitive pickerOptions to the field JS instead.
  • Field JS (showPopup) now fetches the picker URL on click via AJAX and displays an error notification if the token cannot be obtained.

Version 2.5.0

April 7, 2026

Added

  • Craft CMS 5 support. The plugin now works on both Craft 4 and Craft 5. Composer requirement updated to ^4.0.0 || ^5.0.0.
  • Restored "Edit description before insert?" (showDescription) field setting and SHOWDESCRIPTION picker popup parameter. When enabled, the Imageshop popup shows a description field that editors can fill in before inserting the image. The entered description is passed through to Craft and pre-populates the description field on the entry.
  • Site-aware picker language. The Imageshop popup (CULTURE parameter) now uses the current site's language instead of the plugin's global language setting. Editors see the popup in the correct language when editing entries on different sites.
  • Smaller admin thumbnails. Field thumbnails in the control panel now load 400px resized images via getResizedUrl() instead of full-size originals, reducing bandwidth and improving editor performance.
  • MEDIUMTEXT content column type for ImageShop fields (was TEXT), supporting larger JSON payloads for gallery fields with many images. Includes a migration to upgrade existing field columns.
  • refreshMetadata controller action for re-fetching document metadata from the Imageshop API after popup selection.

Fixed

  • Fixed Craft 5 compatibility: updated normalizeValue(), serializeValue(), and getInputHtml() method signatures. Added static dbType() method alongside getContentColumnType().
  • Fixed Craft 5 utility registration: EVENT_REGISTER_UTILITY_TYPESEVENT_REGISTER_UTILITIES (runtime detection for dual Craft 4/5 support).
  • Fixed Craft 5 service registration: added config() static method for component registration.
  • Fixed normalizeValue() to handle arrays of JSON strings and decoded associative arrays (Craft 5 content storage path).
  • Added public visibility modifier to utility methods (id(), contentHtml()) and queue job execute() methods.

Changed

  • Composer requirement updated from ^4.0.0 to ^4.0.0 || ^5.0.0.

Version 2.4.0

February 20, 2026

Added

  • Focal point support: getFocalPoint() on the image model returns CSS-ready x/y percentages from the Imageshop picker's focal point data.
  • focalPoint field in GraphQL type, returning JSON with x/y percentages.
  • altText and tags fields in GraphQL type.
  • Explicit GraphQL resolvers for all language-dependent text fields (credits, description, title, altText, rights, tags), ensuring correct per-site language resolution.
  • Documentation for focal point usage in Twig templates and GraphQL.
  • High-quality image permalink endpoint (/actions/imageshop-dam/permalink/get-hq-url) for fetching larger resolution images on-demand via the Imageshop Permalink API. Useful for lightbox/modal popups.
  • getPermalink() service method for generating permanent CDN URLs at any resolution.
  • Documentation for the permalink endpoint and gallery lightbox usage pattern.
  • Full Norwegian (Bokmål) translations for all plugin UI: settings page, field settings, field input labels, utility page, and queue job descriptions.
  • Added |t('imageshop-dam') translation filters to all plugin templates (settings, field settings, field input) to enable localization.
  • Sync log table (imageshop-dam_sync_log) records each sync run with documents changed, jobs queued, and status.
  • Sync history displayed on the Utilities → Imageshop page.

Fixed

  • SEOmatic OpenGraph/Twitter image integration now works correctly. The matched element is resolved inside the event handler instead of at plugin init time, where routing hasn't completed yet. Image dimensions and alt text are now also set on the meta tags.
  • Fixed "Attempt to assign property on null" error when SEOmatic meta object is not initialized on pages without a matched element (e.g. listing pages).
  • SEOmatic CP sidebar preview (SEO Preview) now shows the Imageshop image in the Twitter and Facebook card previews.
  • GraphQL queries now return text in the correct site language instead of always using the current site's language.
  • allowMultiple setting is now enforced — single-image fields no longer accumulate extra images when the picker is opened repeatedly.
  • Sync now fetches metadata for all languages found in content, not just the plugin's configured language.
  • Fixed mapDocumentFields reading text fields from wrong location in API response.
  • Sync job uses correct field column names.
  • Fixed swapped descriptions for "Show Crop Dialogue" and "Show Size Dialogue" field settings.
  • Fixed duplicate title attribute on reorder icon in field input.
  • Fixed typos in OpenGraph global settings instructions.

Changed

  • Added CSRF validation and POST method enforcement on controller actions.
  • Hardened API service layer: validate responses, prevent cache poisoning via document ID validation, use environment variable for API base URL.
  • Added primary key to sync table via migration.
  • Updated plugin logo/branding.
  • Removed unused UpdateFields job and orphaned migration.
  • Removed empty EVENT_AFTER_INSTALL_PLUGIN handler.
  • Standardized branding from "ImageShop" to "Imageshop" across all display text, documentation, and translations.
  • Reduced default permalink image width from 3840px to 1920px for faster loading.
  • Removed showDescription and showCredits field settings and their corresponding picker popup parameters (SHOWDESCRIPTION, SHOWCREDITS). Existing project configs with these keys are handled gracefully during upgrade.

Version 2.3.0

November 11, 2025

Added

  • Added an option to override image alt text and description.
  • Added the functionality of sorting images in the field.
  • Fixed "Sync metadata" utility.
  • Added the option to use imageshop images foropengraph, with SEOmatic plugin.

Version 2.1.0

December 19, 2024

Added

  • Sync utility to update existing fields.

Version 2.0.7

November 1, 2024

Fixed

  • GraphQL now returns an array of imageShop images.

Version 2.0.6

September 5, 2024

Added

  • Multi image support

Version 2.0.5

January 25, 2024

Added

  • Data to GraphQL

Version 2.0.4

December 19, 2023

Fixed

  • GraphQL bug fix

Version 2.0.3

December 3, 2023

Added

  • GraphQL support

Version 2.0.2

March 7, 2023

Added

  • Show Description option
  • Show Credits option

Changed

  • default lang based off plugin settings

Version 2.0.1

December 12, 2022

Fixed

  • Styling issues
  • Fieldtype fixes

Version 2.0.0

October 11, 2022

Added

  • Initial release