# Matomo Platform Changelog This is the Developer Changelog for Matomo platform developers. All changes in our HTTP APIs, Plugins, Themes, SDKs, etc. are listed below. The Product Changelog at **[matomo.org/changelog](https://matomo.org/changelog)** lets you see more details about any Matomo release, such as the list of new guides and FAQs, security fixes, and links to all closed issues. ## Matomo 6.0.0 ### Breaking Changes * API method parameters are now validated against their declared type. A parameter supplied with a value its declared type cannot accept, for example an array where a string is expected, is rejected with a bad request error. For a parameter declared with a `null` default this replaces the previous silent fallback to `null`; for a parameter declared without a default the error message changes from `General_PleaseSpecifyValue` to `General_InvalidValueForParameter`. Parameters declared with any other default still fall back to it. A parameter that is not supplied continues to use its default, as does one supplied without a value (`¶m=`) — except `string` parameters, which receive `''` as they always have. This applies to API classes that set `$autoSanitizeInputParams = false` or methods annotated `@unsanitized`; every other API method resolves its parameters through the sanitizing path and is unaffected. * Before upgrading a proxied installation that configures `[General] proxy_host_headers`, add both the public hostname and the hostname used to reach Matomo to `trusted_hosts`; otherwise the invalid-host warning replaces the login form. With trusted-host checking enabled, `Piwik\Url::isValidHost()` without an explicit hostname now validates the proxy-derived hostname, and `Piwik\Url::getCurrentHost()` returns that hostname only when it is accepted, falling back to the request/configuration-derived hostname otherwise. New installations record both hostnames automatically. * The deprecated method `Piwik\Archive::getBlob()` has been removed. Use one of the `Piwik\Archive::getDataTable*()` methods instead. * The deprecated method `Piwik\Archive::clearStaticCache()` has been removed. It was a no-op kept only for backwards compatibility. * The deprecated method `Piwik\ArchiveProcessor\Parameters::setIsPartialArchive()` has been removed. Use `Piwik\ArchiveProcessor\Parameters::setArchiveOnlyReport()` instead. * The deprecated method `Piwik\Db\Adapter::getDefaultPortForAdapter()` has been removed. Use `Piwik\Db\Schema::getDefaultPortForSchema()` instead. * The deprecated method `Piwik\Url::saveCORSHostnameInConfig()` has been removed. It was no longer in use. * The deprecated method `Piwik\Plugin\Report::getThirdLeveltableDimension()` has been removed. Use `Piwik\Plugin\Report::getNthLevelTableDimension(2)` instead. * The deprecated `API.getSettings` API method has been removed, along with the default `[APISettings]` section shipped in `config/global.ini.php` that it exposed. There is no replacement; integrations that fetched key/value pairs from that section over the REST API must migrate to another mechanism. Any `[APISettings]` entries in a local `config.ini.php` simply become inert. * The deprecated static method `getDefaultPort()` has been removed from `Piwik\Db\AdapterInterface` and its implementations (`Piwik\Db\Adapter\Mysqli`, `Piwik\Db\Adapter\Pdo\Mysql`). Use `Piwik\Db\Schema::getDefaultPortForSchema()` instead. * The deprecated method `Piwik\Plugins\Overlay\API::getExcludedQueryParameters()` has been removed. Use the `SitesManager.getExcludedQueryParameters` API method instead. * The deprecated method `Piwik\Db::optimizeTables()` has been removed. Use `Piwik\Db\Schema::getInstance()->optimizeTables()` instead. * The deprecated method `Piwik\Db::isOptimizeInnoDBSupported()` has been removed. Use `Piwik\Db\Schema::getInstance()->isOptimizeInnoDBSupported()` instead. * The deprecated method `Piwik\Db\TransactionLevel::setUncommitted()` has been removed. Use `Piwik\Db\TransactionLevel::setTransactionLevelForNonLockingReads()` instead. * The deprecated `SitesManager.setGlobalExcludedQueryParameters` API method has been removed. Use `SitesManager.setGlobalQueryParamExclusion` instead. * The deprecated method `Piwik\API\Request::isTokenAuthProvidedSecurely()` has been removed. * The API methods `Annotations.add`, `Annotations.save` and `Annotations.delete` now require `Write` permission. Previously `Annotations.add` required only `View` permission, and the author of an annotation could modify or delete it with only `View` permission. * Following the upgrade to psr/log 3, `Piwik\Log\LoggerInterface` (which extends `Psr\Log\LoggerInterface`) now requires the PSR-3 `void` return type on its logging methods (`log()`, `debug()`, `info()`, `notice()`, `warning()`, `error()`, `critical()`, `alert()`, `emergency()`). Plugins that implement this interface directly must add the `: void` return type to these methods. Plugins that obtain the logger through dependency injection or extend `Piwik\Log\Logger` are not affected. * The deprecated archiving script `./misc/cron/archive.sh` has been removed. Use the console command `core:archive` instead. * The `SEO` plugin has been removed, along with its `SEO` widget and the `SEO.getRank` API method. * The `TrackingSpamPrevention` plugin is now bundled with core and activated by default. It is no longer distributed on the Marketplace and can no longer be uninstalled, and updating to Matomo 6 activates it on installations where it was deactivated or was never installed. `Piwik\Plugin\Manager::isPluginBundledWithCore('TrackingSpamPrevention')` therefore returns `true` from now on. * The `TrackingSpamPrevention` setting `block_clouds` no longer controls GeoIP organisation blocking. It now only enables the daily cloud provider IP range sync, and is relabelled accordingly. A new `cloud_blocking_mode` setting decides whether organisation names are matched against nothing (`off`), Matomo's maintained default list (`default`), or the `organisation_block_list` setting (`custom`). Both are `[TrackingSpamPrevention]` `config.ini.php` keys as well as UI settings, and `Piwik\Plugins\TrackingSpamPrevention\SystemSettings::getBlockedOrganisations()` now returns the list the selected mode puts into effect rather than the stored list. Installations that already had the plugin are migrated to the state they were already in, and keep the blocking they had until the database update runs, so nothing changes between the new files being deployed and `core:update`. An installation where this plugin was installed but deactivated needs the update run twice, because Matomo 6 activates the plugin during the first pass and only picks up its update in the next one. Installations treated as new, which includes Matomo 5 installations where this plugin was never installed, start with both options enabled. * One Click Update now always downloads the update archive over HTTPS. The insecure "retry over HTTP" fallback screen and the `https` request parameter of the `CoreUpdater.oneClickUpdate` action have been removed, and the `$https` parameter of `Piwik\Plugins\CoreUpdater\Updater::updatePiwik()` and `Piwik\Plugins\CoreUpdater\Updater::getArchiveUrl()` has been removed. HTTP is only used when the `force_matomo_http_request` config option is enabled. * The global function `_glob()` has been removed. Use the native `glob()` instead. As a consequence `glob()` must no longer be listed in the `disable_functions` php.ini directive: Matomo now refuses to start with an explicit error message instead of emulating it. `glob()`, `fnmatch()` and `file_get_contents()` have also moved from the recommended to the required functions in the system check, which additionally catches a Suhosin function blacklist. * The global functions `safe_serialize()` and `_safe_serialize()` have been removed. Use the native `serialize()` instead. Note that the retained `safe_unserialize()` accepts only a strict subset of PHP's serialization format, so native `serialize()` is a drop-in replacement only for values that contain no objects, no references and no resources. A reference produces an `R:` token, which `safe_unserialize()` rejects; a resource is serialized as `i:0;`, which it silently reads back as the integer `0` where `safe_serialize()` used to refuse to serialize it at all. * The global function `_parse_ini_file()` has been removed. It was unused; use `Piwik\Config` for Matomo's configuration or `Matomo\Ini\IniReader` for other INI files. * The global functions `safe_unserialize()` and `_safe_unserialize()` in `libs/upgradephp/upgrade.php` are kept. Note that these are a different, stricter implementation than `Piwik\Common::safe_unserialize()`, which wraps the native `unserialize()` with `allowed_classes => false` and applies none of the `MAX_SERIALIZED_*` input limits. * The conditional fallback definitions for `mysqli_set_charset()`, `file_get_contents()`, `utf8_encode()`, `utf8_decode()`, `fnmatch()`, the `Error` class and the `PHP_INT_SIZE`/`PHP_INT_MAX` constants have been removed from `libs/upgradephp/upgrade.php`. All of these are provided natively by every supported PHP version; the fallbacks only ever activated on PHP versions that are no longer supported, or when the function had been turned off via `disable_functions`. * The `gzopen()` fallback has also been removed from `libs/upgradephp/upgrade.php`. It aliased `gzopen()` to `gzopen64()` on distribution builds where zlib exposes only the latter, which is a packaging issue rather than a PHP version or `disable_functions` one. On such a build `Piwik\Unzip` now falls back to `PclZip`. * Several core controller actions that return JSON now declare a native `string` return type as part of adopting the new `#[Piwik\Http\JsonResponse]` attribute. A plugin that extends one of these controllers and overrides such an action must declare a compatible `string` return type and re-declare `#[Piwik\Http\JsonResponse]` (attributes are not inherited). * The deprecated Piwik-era color aliases `@color-black-piwik`, `@color-blue-piwik`, `@color-red-piwik` and `@color-green-piwik` have been removed from `plugins/Morpheus/stylesheets/base/colors.less`. Use `@color-black-matomo`, `@color-blue-matomo`, `@color-red-matomo` and `@color-green-matomo` instead. * The third-party brand color variables `@color-orange-brand` (`#f57c00`), `@color-green-brandSocial` (`#009874`), `@color-blue-brandSocial` (`#3b5998`), `@color-blue-brandSocialLight` (`#1c87bd`) and `@color-blue-brandSocialVeryLight` (`#00aced`) have been removed. They described other companies' brands rather than Matomo's own palette; a plugin that still needs one of these colors should use the literal value. * The never-referenced palette tokens `@color-gray-light` (`#f0f0f0`), `@color-gray-bright` (`#EBF2EB`), `@color-gray-400` (`#BCBCBC`), `@color-jetstream` (`#c3d9c4`), `@color-silver-l14`, `@color-silver-l50`, `@color-silver-l70` and `@color-silver-l98` have been removed. Use one of the remaining `@color-silver-*` variables, a `@theme-color-*` variable or a literal value instead. * The stylesheet `plugins/Morpheus/stylesheets/base/mode-colors.less` has been removed together with the variables it defined, `@color-mode-black` and `@color-mode-white`. Use the `.inDarkMode()` mixin (still available from `plugins/Morpheus/stylesheets/base/mixins.less`) or the relevant `@theme-color-*` variable instead. * The stylesheet `plugins/Login/stylesheets/variables.less` has been removed together with the variable it defined, `@login-section-background`. A plugin that registers this file in `getStylesheetFiles()` must drop that line, otherwise stylesheet merging fails with `The ui asset with 'href' = .../plugins/Login/stylesheets/variables.less is not readable`. * The deprecated jQuery UI widget `$.fn.liveWidget` (`piwik.liveWidget`) has been removed together with the file `plugins/Live/javascripts/live.js` that defined it. Use the `Live.AutoRefreshWidget` Vue component instead. * Less variables that were only used within a single stylesheet have been inlined or renamed to private `@_`-prefixed names, and are therefore no longer visible to other stylesheets: `@top-menu-nav-color` (`plugins/CoreHome/stylesheets/layout.less`), `@color-period-selector`, `@color-period-selector-input-radio`, `@color-period-selector-options-hover-background`, `@color-period-selector-calendar-hover-background` (`PeriodSelector.less`), `@add-widget-padding`, `@add-widget-border`, `@add-widget-space-or-radius`, `@add-widget-categories`, `@add-widget-widgets`, `@add-widget-preview`, `@add-widget-height`, `@add-widget-item-height` (`AddWidgetModal.less`) and `@calendarHeaderBackground`, `@calendarHeaderColor`, `@calendarCurrentStateHover`, `@calendarBorder` (`plugins/Morpheus/stylesheets/ui/_components.less`). These were never theme variables; use the `@theme-color-*` variable they were derived from instead. * `CoreHome.EnrichedHeadline` no longer derives a report's inline help from the DOM. It used to look for a `.reportDocumentation[data-content]` element inside the next sibling of its headline and show that text behind a help icon; the text now comes from its `inline-help` attribute. The `piwik:reportChanged` DOM event, which told a headline to re-read that element, has been removed with it. Headlines rendered by `Piwik\View::singleReport()`, or by a template reproducing its shape, therefore lose their help icon unless `inline-help` is passed explicitly. * The development-only console commands `git:commit`, `git:pull` and `git:push` have been removed. They were thin wrappers around `git` that predate the current submodule workflow; use `git` directly instead. * Flat-first Actions archiving is enabled by default for new installations, as `datatable_archiving_maximum_rows_actions_flat` now defaults to `10000` instead of `0`. Installations updated from Matomo 5 keep the legacy hierarchical-only archiving, as the update writes a `0` to their `config.ini.php`, unless the setting is already configured there or in `common.config.ini.php`. While it is enabled, the page URL and page title reports (including their entry and exit variants) are capped by this setting instead of `datatable_archiving_maximum_rows_actions` and `datatable_archiving_maximum_rows_subtable_actions`, each of them is stored as one additional archive record, and their page categories are no longer truncated per category, so a category no longer ends in a summary row of its own. * Request parameters are no longer trimmed while a request is parsed. `Piwik\API\Request::getRequestArrayFromString()` used to apply `trim((string) $value)` to every non-array parameter, so leading and trailing whitespace in values such as a `label` or a segment operand is now preserved, and scalars keep their type. As a consequence a boolean passed through `Piwik\API\Request::processRequest()` no longer arrives as `'1'`/`''`: a parameter read with `Piwik\Common::getRequestVar($name, $default, 'string')` or `Piwik\Request::getStringParameter()` falls back to its default instead. Pass a string, or read it with `Piwik\Request::getBoolParameter()`, which accepts real booleans. * The `SitesManager.getImageTrackingCode` API method now requires view access to the given site, matching `SitesManager.getJavascriptTag`. Its parameters are typed as well, so `idSite` must be an integer and `forceMatomoEndpoint` a boolean — the latter is now read with `Piwik\Request::getBoolParameter()`, which understands `0`, `1`, `true` and `false` and silently falls back to `false` for anything else, where previously any truthy value enabled it. * The legacy Transitions renderer has been removed together with the JavaScript globals it defined, `Piwik_Transitions`, `Piwik_Transitions_Canvas` and `Piwik_Transitions_Util`. Use the `Transitions.TransitionsReport` Vue component instead, or `DataTable_RowActions_Transitions.launchForUrl()`, which is unchanged and still opens the report for a URL. `Piwik_Transitions_Model` and `Piwik_Transitions_Ajax` remain, without the renderer-only members `htmlLoaded()`, `getShareInGroupTooltip()` and `callTransitionsController()`. The controller action `index.php?module=Transitions&action=renderPopover`, which rendered the removed markup, and the `@internal` API method `Transitions.getTranslations`, which only supplied its labels, have been removed with it, along with the `Piwik_Transitions_Translations` global that template defined. `Piwik_Transitions_Ajax.callApi()` no longer falls back to `Piwik_Popover.showError()` when no `setErrorCallback()` was registered: an API error is dropped and the request's callback never runs, so a caller that wants errors surfaced must register one. * A plugin with no cover image of its own now falls back to the same generic `uncategorised` cover as every other plugin. `Piwik\Plugins\Marketplace\Plugins::addPluginCoverImage()` used to give a paid plugin owned by `piwik` or `matomo-org` the Matomo-branded `matomo.png` cover instead, which the redesigned cards mark with a Matomo chip rather than a whole cover image. The Marketplace's own category stand-ins now count as no cover image as well, so a plugin the Marketplace categorised also takes the generic fallback. * Several parameters of the `Goals` API methods now declare a native `bool` type: `caseSensitive`, `allowMultipleConversionsPerVisit` and `useEventValueAsRevenue` of `Goals.addGoal` and `Goals.updateGoal`, `abandonedCarts` of `Goals.getItemsSku`, `Goals.getItemsName` and `Goals.getItemsCategory`, and `showAllGoalSpecificMetrics` and `compare` of `Goals.get` and `Goals.getMetrics`. Over HTTP these parameters were previously passed through as raw strings, so any value other than `0` was truthy: `&abandonedCarts=false` returned abandoned carts rather than purchased products. They are now read like every other boolean parameter, which accepts `0`, `1`, `false` and `true` and falls back to the default for anything else. Plugins calling these methods directly in PHP must pass a boolean or a value PHP can coerce to one; `null` is no longer accepted. * The `idGoal` parameter of `Goals.deleteGoal` and `Goals.updateGoal` now declares a native `int` type, like `Goals.getGoal` already did. A value that is not a number, such as `idGoal=ecommerceOrder`, is rejected by the API layer instead of being coerced to `0`. Plugins calling these methods directly in PHP must pass an integer or a numeric string. * The Marketplace's `PluginList` Vue component has been removed along with the plugin list it rendered. The redesigned page is built from the `PluginGrid`, `PluginSection`, `PluginCard`, `CategoryTabs`, `MarketplaceHero` and `SortMenu` components the plugin now exports instead. The translation keys `Marketplace_CreatedBy`, `Marketplace_Intro`, `Marketplace_IntroSuperUser`, `Marketplace_NoThemesFound`, `Marketplace_PriceFromPerPeriod`, `Marketplace_Show`, `Marketplace_Sort` and `Marketplace_SortByPopular` have been removed with the markup that used them. The `pluginType` hash parameter is no longer written and only `themes` and `plugins` are still read back, for the links CorePluginsAdmin makes; `#?pluginType=premium` no longer opens a filtered list and is silently ignored, landing the reader on the full catalogue instead. * The site selector dropdown (`CoreHome.SiteSelector`) now uses the generic dropdown panel and search input markup, so the classes it used to expose have been removed: `custom_select_search`, `custom_select_container`, `custom_select_ul_list`, `custom_select_all`, `websiteSearch`, `inp`, `reset`, `noresult`, `autocompleteMatched`, and the `dropdown` class on the panel itself. Code that selected inside the site selector must target `.piwikSelector__dropdown` for the panel, `.mtm-dropdownPanel__menu` for the site list, `.mtm-dropdownPanel__menuItem` / `.mtm-dropdownPanel__menuLink` / `.mtm-dropdownPanel__menuLabel` for an entry, `.mtm-dropdownPanel__noResult` for the empty state, `.mtm-dropdownPanel__searchMatch` for the highlighted search term, and `.mtm-searchInput__input` / `.mtm-searchInput__clear` for the search field. The unused Less rules that styled `custom_select_search` inside `.segment-element` have been removed from `plugins/SegmentEditor/stylesheets/segmentation.less` and `plugins/Morpheus/stylesheets/ui/_components.less`. `plugins/CoreHome/vue/src/SiteSelector/SiteSelector.less` also drops the now unmatched global rules `.sites_selector_container`, `.custom_select_block_show`, `.custom_selector_container .ui-menu-item`, `.siteSelect a` and `.custom_select_main_link`; nothing in core or the bundled plugins carried that markup, but a third-party plugin reproducing it would lose the styling. The internal `AllSitesLink.vue` component has been removed: it was never exported from `CoreHome`, and its "All Websites" row is now rendered by `SiteSelector.vue` as a regular panel menu item. * The deprecated `$webSearchEnabled` parameter of the `Piwik\Plugins\AIProviders\AIProviderResponse` constructor has been removed. Pass a `Piwik\Plugins\AIProviders\WebSearchUsage` instead, which reports the searches, queries and citations a completion actually produced rather than a bare flag. It occupied position 8, so a caller that passes the execution time and stop reason positionally must drop the flag or those values now bind to the wrong parameters; the execution time, stop reason and `WebSearchUsage` each move up one position, so `WebSearchUsage` is now 10th rather than the 11th it held in Matomo 5.14.0. * The deprecated method `Piwik\Plugins\AIProviders\AIProviderResponse::isWebSearchEnabled()` has been removed, along with the `webSearchEnabled` key of `AIProviderResponse::toArray()`. Use `wasWebSearchUsed()` and the `webSearchUsed` key instead, which say what they mean; `Piwik\Plugins\AIProviders\AIRequest::isWebSearchEnabled()` is unaffected and keeps reporting what the request asked for. * The deprecated method `Piwik\Plugins\AIProviders\Provider\AIProvider::isWebSearchUsed()` has been removed and is no longer consulted. A provider that reported a search by overriding it must now override `supportsWebSearch()` to declare the capability and pass a `WebSearchUsage` to `buildResponse()`; an override left in place is simply ignored, so such a provider's answers report no search until it migrates. * Custom Dimension reports now credit a goal conversion to the value its visit ended with when the conversion recorded no value of its own, which happens when a visit-scoped dimension is only sent after the goal converted. Those conversions were previously left out of the report altogether, so `CustomDimensions.getCustomDimension` can return higher conversion counts for affected dates once they are re-archived. A conversion that did record a value is unaffected and still counts under that value. A visit that later sent the dimension as an empty value now credits its earlier conversions to "Value not defined". Segmented reports get the same credit, and a conversion is still counted once when the segment matches several actions of its visit. ### New Features * Added contextual recommendations for Premium products based on how Matomo is being used. ### New APIs * The new `Template.beforeDashboardWidgets` event is posted at the top of the dashboard, above the widgets, and allows a plugin to render its own content there. It is posted by `plugins/Dashboard/templates/embeddedIndex.twig`; like the other `Template.*` events, a listener takes the rendered output by reference (`function (&$out)`) and appends its markup to it. * The new `DragHandle` Vue component in CoreHome renders the standard 6-dot drag-handle icon, for use inside `DraggableList` rows. The `DraggableList` component's `handle` option now also works with real browser drags, which retarget `dragstart` to the draggable element (previously the handle was only recognised in synthetically dispatched events). * The new `closeTooltips()` helper in CoreHome closes the jQuery UI tooltips bound to a selector's elements, including pending delayed shows — for cases where no mouse event will fire that would close them, eg. once an HTML5 drag has started. * The generic dropdown panel (`plugins/Morpheus/stylesheets/ui/_dropdown-panel.less`) gained the elements `.mtm-dropdownPanel__search` (a nest element that hosts a search input inside a panel), `.mtm-dropdownPanel__searchMatch` (the part of a menu label matching the typed term) and `.mtm-dropdownPanel__noResult` (the row shown when a search yields nothing), plus the modifiers `.mtm-dropdownPanel__menu--scrollable` (caps the menu at `calc(80vh - 60px)` and scrolls) and `.mtm-dropdownPanel__menu--gutter` (an 8px horizontal gutter so the rows line up with a search input above them). The panel itself is now a fixed 254px wide rather than a 240px minimum. `.mtm-dropdownPanel__menuLabel` now truncates with an ellipsis instead of only allowing it. * The `SearchInput` clear button exported from `CoreHome` now carries a translated `title` (`General_Clear`), giving it an accessible name. * A new `#[Piwik\Http\JsonResponse]` attribute can be applied to a plugin controller action to declare that it returns a JSON response. When present, Matomo (re-)sends the `Content-Type: application/json` header after the action has returned, so it can no longer be overwritten by output produced while the action builds its response (for example a rendered `Piwik\View`, which sends `text/html`). An action using the attribute must return the JSON string, must not send the header itself, and must not emit output (`echo`/`print`/`flush`) or call `exit`/`die` before returning — otherwise the response headers are committed first and the JSON `Content-Type` cannot be applied. The attribute is not inherited: a subclass overriding a JSON action must re-declare it. These requirements are enforced by PHPStan rules. * The `PasswordConfirmation` component exported from `CorePluginsAdmin` gained a `requireDeleteConfirmation` prop, off by default. When it is set the confirmation dialog additionally asks the user to type `delete` - an untranslated literal, the same word in every language - and its Confirm button stays disabled until that matches exactly and, where re-authentication applies, a password has been entered. It is meant for actions that permanently remove data, and is used in four places in PrivacyManager: the settings for regularly deleting old raw data, for deleting old aggregated report data and for enforcing a raw data retention period through a compliance policy - the last of these only when the save being confirmed actually switches retention on - and the Purge DB Now link, which deletes straight away rather than scheduling a deletion for later. A plugin that supplies an alternative identity confirmation component through the `PasswordConfirmation.altIdComponent` event keeps that component on screen while the word is missing, greyed out and made inert inside a wrapper the dialog owns, so it can be neither clicked nor reached by keyboard until the word matches, and pressing Enter in the dialog activates that component rather than confirming without it. The component has to contain a single `` or `