diff --git a/.gitmodules b/.gitmodules index fefdf57df8f..71eed63ea23 100644 --- a/.gitmodules +++ b/.gitmodules @@ -57,15 +57,6 @@ [submodule "plugins/DeviceDetectorCache"] path = plugins/DeviceDetectorCache url = https://github.com/matomo-org/plugin-DeviceDetectorCache.git -[submodule "plugins/Provider"] - path = plugins/Provider - url = https://github.com/matomo-org/plugin-Provider.git - -# Add new Plugin submodule above this line ^^ -# -# Note: when you add a submodule that SHOULD be left in the packaged release such as the few submodules below, -# then you MUST add these submodules names in the SUBMODULES_PACKAGED_WITH_CORE variable in: -# https://github.com/matomo-org/matomo-package/blob/master/scripts/build-package.sh [submodule "misc/log-analytics"] path = misc/log-analytics url = https://github.com/matomo-org/matomo-log-analytics.git diff --git a/CHANGELOG.md b/CHANGELOG.md index a3741a87bea..a0efe87536d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,12 @@ The Product Changelog at **[matomo.org/changelog](https://matomo.org/changelog)* ## Matomo 4.0.0 -### Breaking Changes +### New API +* A new API `UsersManager.createAppSpecificTokenAuth` has been added to create an app specific token for a user. +### Breaking changes +* The API `UsersManager.getTokenAuth` has been removed. Instead you need to use `UsersManager.createAppSpecificTokenAuth` and store this token in your application. +* The API `UsersManager.createTokenAuth` has been removed. Instead you need to use `UsersManager.createAppSpecificTokenAuth` and store this token in your application. * Deprecated `piwik` font was removed. Use `matomo` font instead * The JavaScript AjaxHelper does not longer support synchronous requests. All requests will be sent async instead. * The deprecated Platform API method `\Piwik\Plugin::getListHooksRegistered()` has been removed. Use `\Piwik\Plugin::registerEvents()` instead diff --git a/core/Access.php b/core/Access.php index d99551c219c..7e4bcd59d85 100644 --- a/core/Access.php +++ b/core/Access.php @@ -15,6 +15,7 @@ use Piwik\Container\StaticContainer; use Piwik\Exception\InvalidRequestParameterException; use Piwik\Plugins\SitesManager\API as SitesManagerApi; +use Piwik\Session\SessionAuth; /** * Singleton that manages user access to Piwik resources. @@ -155,8 +156,32 @@ public function reloadAccess(Auth $auth = null) return false; } + $result = null; + + $forceApiSession = Common::getRequestVar('force_api_session', 0, 'int', $_POST); + if ($forceApiSession && Piwik::getModule() === 'API' && (Piwik::getAction() === 'index' || !Piwik::getAction())) { + $tokenAuth = Common::getRequestVar('token_auth', '', 'string', $_POST); + if (!empty($tokenAuth)) { + Session::start(); + $auth = StaticContainer::get(SessionAuth::class); + $auth->setTokenAuth($tokenAuth); + $result = $auth->authenticate(); + if (!$result->wasAuthenticationSuccessful()) { + /** + * Ensures brute force logic to be executed + * @ignore + * @internal + */ + Piwik::postEvent('API.Request.authenticate.failed'); + } + // if not successful, we will fallback to regular auth + } + } + // access = array ( idsite => accessIdSite, idsite2 => accessIdSite2) - $result = $this->auth->authenticate(); + if (!$result || !$result->wasAuthenticationSuccessful()) { + $result = $this->auth->authenticate(); + } if (!$result->wasAuthenticationSuccessful()) { return false; diff --git a/core/CliMulti.php b/core/CliMulti.php index 863bea1b3b9..751c5fbd3ee 100644 --- a/core/CliMulti.php +++ b/core/CliMulti.php @@ -365,8 +365,7 @@ private function executeNotAsyncHttp($url, Output $output) } if ($this->runAsSuperUser) { - $tokenAuths = self::getSuperUserTokenAuths(); - $tokenAuth = reset($tokenAuths); + $tokenAuth = self::getSuperUserTokenAuth(); if (strpos($url, '?') === false) { $url .= '?'; @@ -433,18 +432,9 @@ private function requestUrls(array $piwikUrls) return $results; } - private static function getSuperUserTokenAuths() + private static function getSuperUserTokenAuth() { - $tokens = array(); - - /** - * Used to be in CronArchive, moved to CliMulti. - * - * @ignore - */ - Piwik::postEvent('CronArchive.getTokenAuth', array(&$tokens)); - - return $tokens; + return Piwik::requestTemporarySystemAuthToken('CliMultiNonAsyncArchive', 36); } public function setUrlToPiwik($urlToPiwik) diff --git a/core/Db/Schema/Mysql.php b/core/Db/Schema/Mysql.php index ffe4aa5b655..01ea82492ad 100644 --- a/core/Db/Schema/Mysql.php +++ b/core/Db/Schema/Mysql.php @@ -16,6 +16,7 @@ use Piwik\Db; use Piwik\DbHelper; use Piwik\Option; +use Piwik\Plugins\UsersManager\Model; use Piwik\Version; /** @@ -45,12 +46,24 @@ public function getTablesCreateSql() alias VARCHAR(45) NOT NULL, email VARCHAR(100) NOT NULL, twofactor_secret VARCHAR(40) NOT NULL DEFAULT '', - token_auth CHAR(32) NOT NULL, superuser_access TINYINT(2) unsigned NOT NULL DEFAULT '0', date_registered TIMESTAMP NULL, ts_password_modified TIMESTAMP NULL, - PRIMARY KEY(login), - UNIQUE KEY uniq_keytoken(token_auth) + PRIMARY KEY(login) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", + 'user_token_auth' => "CREATE TABLE {$prefixTables}user_token_auth ( + idusertokenauth BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + login VARCHAR(100) NOT NULL, + description VARCHAR(".Model::MAX_LENGTH_TOKEN_DESCRIPTION.") NOT NULL, + password VARCHAR(255) NOT NULL, + hash_algo VARCHAR(30) NOT NULL, + system_token TINYINT(1) NOT NULL DEFAULT 0, + last_used DATETIME NULL, + date_created DATETIME NOT NULL, + date_expired DATETIME NULL, + PRIMARY KEY(idusertokenauth), + UNIQUE KEY uniq_password(password) ) ENGINE=$engine DEFAULT CHARSET=utf8 ", @@ -504,12 +517,15 @@ public function createTables() public function createAnonymousUser() { $now = Date::factory('now')->getDatetime(); - // The anonymous user is the user that is assigned by default // note that the token_auth value is anonymous, which is assigned by default as well in the Login plugin $db = $this->getDb(); $db->query("INSERT IGNORE INTO " . Common::prefixTable("user") . " - VALUES ( 'anonymous', '', 'anonymous', 'anonymous@example.org', '', 'anonymous', 0, '$now', '$now' );"); + (`login`, `password`, `alias`, `email`, `twofactor_secret`, `superuser_access`, `date_registered`, `ts_password_modified`) + VALUES ( 'anonymous', '', 'anonymous', 'anonymous@example.org', '', 0, '$now', '$now' );"); + + $model = new Model(); + $model->addTokenAuth('anonymous', 'anonymous', 'anonymous default token', $now); } /** diff --git a/core/Piwik.php b/core/Piwik.php index 32520ffec40..d16ffca97e3 100644 --- a/core/Piwik.php +++ b/core/Piwik.php @@ -16,6 +16,7 @@ use Piwik\Period\Week; use Piwik\Period\Year; use Piwik\Plugins\UsersManager\API as APIUsersManager; +use Piwik\Plugins\UsersManager\Model; use Piwik\Translation\Translator; /** @@ -257,6 +258,51 @@ public static function checkUserHasSuperUserAccessOrIsTheUser($theUser) } } + /** + * Request a token auth to authenticate in a request. + * + * Note: During one request the token is only being requested once and used throughout the request. So you want to make + * sure the token is valid for enough time for the whole request to finish. + * + * @param string $reason some short string/text explaining the reason for the token generation, eg "CliMultiAsyncHttpArchiving" + * @param int $validForHours For how many hours the token should be valid. Should not be valid for more than 14 days. + * @return mixed + */ + public static function requestTemporarySystemAuthToken($reason, $validForHours) + { + static $token = array(); + + if (isset($token[$reason])) { + // note: For now we do not increase the expire time when it is already requested + return $token[$reason]; + } + + $twoWeeksInHours = 14 * 24; + if ($validForHours > $twoWeeksInHours) { + throw new Exception('The token cannot be valid for so many hours: ' . $validForHours); + } + + $model = new Model(); + $users = $model->getUsersHavingSuperUserAccess(); + if (!empty($users)) { + $user = reset($users); + $expireDate = Date::now()->addHour($validForHours)->getDatetime(); + + $token[$reason] = $model->generateRandomTokenAuth(); + + $model->addTokenAuth( + $user['login'], + $token[$reason], + 'System generated ' . $reason, + Date::now()->getDatetime(), + $expireDate, + true); + + return $token[$reason]; + } + + } + /** * Check whether the given user has superuser access. * diff --git a/core/Session/SessionAuth.php b/core/Session/SessionAuth.php index 001b3d2bb09..da896b991eb 100644 --- a/core/Session/SessionAuth.php +++ b/core/Session/SessionAuth.php @@ -11,8 +11,10 @@ use Piwik\Auth; use Piwik\AuthResult; +use Piwik\Common; use Piwik\Config; use Piwik\Date; +use Piwik\Plugins\UsersManager\Model; use Piwik\Plugins\UsersManager\Model as UsersModel; use Piwik\Session; @@ -42,6 +44,8 @@ class SessionAuth implements Auth */ private $user; + private $tokenAuth; + public function __construct(UsersModel $userModel = null, $shouldDestroySession = true) { $this->userModel = $userModel ?: new UsersModel(); @@ -55,7 +59,7 @@ public function getName() public function setTokenAuth($token_auth) { - // empty + $this->tokenAuth = $token_auth; } public function getLogin() @@ -113,7 +117,17 @@ public function authenticate() $this->updateSessionExpireTime($sessionFingerprint); - return $this->makeAuthSuccess($user); + if (!empty($this->tokenAuth) && $this->tokenAuth !== $sessionFingerprint->getSessionTokenAuth()) { + return $this->makeAuthFailure(); + } + + if ($sessionFingerprint->getSessionTokenAuth()) { + $tokenAuth = $sessionFingerprint->getSessionTokenAuth(); + } else { + $tokenAuth = $this->userModel->generateRandomTokenAuth(); + } + + return $this->makeAuthSuccess($user, $tokenAuth); } private function isSessionStartedBeforePasswordChange(SessionFingerprint $sessionFingerprint, $tsPasswordModified) @@ -137,14 +151,15 @@ private function makeAuthFailure() return new AuthResult(AuthResult::FAILURE, null, null); } - private function makeAuthSuccess($user) + private function makeAuthSuccess($user, $tokenAuth) { $this->user = $user; + $this->tokenAuth = $tokenAuth; $isSuperUser = (int) $user['superuser_access']; $code = $isSuperUser ? AuthResult::SUCCESS_SUPERUSER_AUTH_CODE : AuthResult::SUCCESS; - return new AuthResult($code, $user['login'], $user['token_auth']); + return new AuthResult($code, $user['login'], $tokenAuth); } protected function initNewBlankSession(SessionFingerprint $sessionFingerprint) @@ -178,7 +193,7 @@ protected function destroyCurrentSession(SessionFingerprint $sessionFingerprint) public function getTokenAuth() { - return $this->user['token_auth']; + return $this->tokenAuth; } private function updateSessionExpireTime(SessionFingerprint $sessionFingerprint) diff --git a/core/Session/SessionFingerprint.php b/core/Session/SessionFingerprint.php index df8a519e5e3..3399fa7a0c0 100644 --- a/core/Session/SessionFingerprint.php +++ b/core/Session/SessionFingerprint.php @@ -9,6 +9,7 @@ namespace Piwik\Session; +use Piwik\Common; use Piwik\Config; use Piwik\Date; @@ -42,6 +43,7 @@ class SessionFingerprint const USER_NAME_SESSION_VAR_NAME = 'user.name'; const SESSION_INFO_SESSION_VAR_NAME = 'session.info'; const SESSION_INFO_TWO_FACTOR_AUTH_VERIFIED = 'twofactorauth.verified'; + const SESSION_INFO_TEMP_TOKEN_AUTH = 'user.token_auth_temp'; public function getUser() { @@ -61,6 +63,15 @@ public function getUserInfo() return null; } + public function getSessionTokenAuth() + { + if (!empty($_SESSION[self::SESSION_INFO_TEMP_TOKEN_AUTH])) { + return $_SESSION[self::SESSION_INFO_TEMP_TOKEN_AUTH]; + } + + return null; + } + public function hasVerifiedTwoFactor() { if (isset($_SESSION[self::SESSION_INFO_TWO_FACTOR_AUTH_VERIFIED])) { @@ -75,11 +86,12 @@ public function setTwoFactorAuthenticationVerified() $_SESSION[self::SESSION_INFO_TWO_FACTOR_AUTH_VERIFIED] = 1; } - public function initialize($userName, $isRemembered = false, $time = null) + public function initialize($userName, $tokenAuth, $isRemembered = false, $time = null) { $time = $time ?: Date::now()->getTimestampUTC(); $_SESSION[self::USER_NAME_SESSION_VAR_NAME] = $userName; $_SESSION[self::SESSION_INFO_TWO_FACTOR_AUTH_VERIFIED] = 0; + $_SESSION[self::SESSION_INFO_TEMP_TOKEN_AUTH] = $tokenAuth; $_SESSION[self::SESSION_INFO_SESSION_VAR_NAME] = [ 'ts' => $time, 'remembered' => $isRemembered, @@ -100,6 +112,10 @@ public function clear() if (isset($_SESSION[self::SESSION_INFO_TWO_FACTOR_AUTH_VERIFIED])) { // may not be available during tests unset($_SESSION[self::SESSION_INFO_TWO_FACTOR_AUTH_VERIFIED]); } + + if (isset($_SESSION[self::SESSION_INFO_TEMP_TOKEN_AUTH])) { // may not be available during tests + unset($_SESSION[self::SESSION_INFO_TEMP_TOKEN_AUTH]); + } } public function getSessionStartTime() diff --git a/core/Session/SessionInitializer.php b/core/Session/SessionInitializer.php index 40218901671..11d753395d0 100644 --- a/core/Session/SessionInitializer.php +++ b/core/Session/SessionInitializer.php @@ -83,7 +83,7 @@ protected function processFailedSession() protected function processSuccessfulSession(AuthResult $authResult) { $sessionIdentifier = new SessionFingerprint(); - $sessionIdentifier->initialize($authResult->getIdentity(), $this->isRemembered()); + $sessionIdentifier->initialize($authResult->getIdentity(), $authResult->getTokenAuth(), $this->isRemembered()); /** * @ignore diff --git a/core/Tracker/Request.php b/core/Tracker/Request.php index 7da7f122876..3b9bae24d3f 100644 --- a/core/Tracker/Request.php +++ b/core/Tracker/Request.php @@ -208,6 +208,8 @@ public static function authenticateSuperUserOrAdminOrWrite($tokenAuth, $idSite) // Now checking the list of admin token_auth cached in the Tracker config file if (!empty($idSite) && $idSite > 0) { $website = Cache::getCacheWebsiteAttributes($idSite); + $userModel = new \Piwik\Plugins\UsersManager\Model(); + $tokenAuth = $userModel->hashTokenAuth($tokenAuth); $hashedToken = UsersManager::hashTrackingToken((string) $tokenAuth, $idSite); if (array_key_exists('tracking_token_auth', $website) diff --git a/core/Updater/Migration/Plugin/Uninstall.php b/core/Updater/Migration/Plugin/Uninstall.php index 77e95d2f24e..ed4b29a08f4 100644 --- a/core/Updater/Migration/Plugin/Uninstall.php +++ b/core/Updater/Migration/Plugin/Uninstall.php @@ -48,6 +48,10 @@ public function shouldIgnoreError($exception) public function exec() { $this->pluginManager->uninstallPlugin($this->pluginName); + + // uninstallPlugin() loads all plugins in the filesystem, which we don't want for the rest of the updates + $this->pluginManager->unloadPlugins(); + $this->pluginManager->loadActivatedPlugins(); } } diff --git a/core/Updates/2.0.4-b5.php b/core/Updates/2.0.4-b5.php index 2282ca1dae9..a96d9e4cc35 100644 --- a/core/Updates/2.0.4-b5.php +++ b/core/Updates/2.0.4-b5.php @@ -36,7 +36,7 @@ public function __construct(MigrationFactory $factory) public function getMigrations(Updater $updater) { return array( - $this->migration->db->addColumn('user', 'superuser_access', "TINYINT(2) UNSIGNED NOT NULL DEFAULT '0'", 'token_auth') + $this->migration->db->addColumn('user', 'superuser_access', "TINYINT(2) UNSIGNED NOT NULL DEFAULT '0'") ); } @@ -82,7 +82,7 @@ private static function migrateConfigSuperUserToDb() 'password' => $superUser['password'], 'alias' => $superUser['login'], 'email' => $superUser['email'], - 'token_auth' => $userApi->getTokenAuth($superUser['login'], $superUser['password']), + 'token_auth' => md5(Common::getRandomString(32)), 'date_registered' => Date::now()->getDatetime(), 'superuser_access' => 1 ) diff --git a/core/Updates/4.0.0-b1.php b/core/Updates/4.0.0-b1.php index c38b1a1eb5b..2b289f6a449 100644 --- a/core/Updates/4.0.0-b1.php +++ b/core/Updates/4.0.0-b1.php @@ -9,6 +9,9 @@ namespace Piwik\Updates; +use Piwik\Date; +use Piwik\DbHelper; +use Piwik\Plugins\UsersManager\Model; use Piwik\Common; use Piwik\Config; use Piwik\Plugins\UserCountry\LocationProvider; @@ -33,10 +36,49 @@ public function __construct(MigrationFactory $factory) public function getMigrations(Updater $updater) { - $migrations = []; + $migrations = array(); $migrations[] = $this->migration->db->changeColumnType('log_action', 'name', 'VARCHAR(4096)'); $migrations[] = $this->migration->db->changeColumnType('log_conversion', 'url', 'VARCHAR(4096)'); + /** APP SPECIFIC TOKEN START */ + $migrations[] = $this->migration->db->createTable('user_token_auth', array( + 'idusertokenauth' => 'BIGINT UNSIGNED NOT NULL AUTO_INCREMENT', + 'login' => 'VARCHAR(100) NOT NULL', + 'description' => 'VARCHAR('.Model::MAX_LENGTH_TOKEN_DESCRIPTION.') NOT NULL', + 'password' => 'VARCHAR(255) NOT NULL', + 'system_token' => 'TINYINT(1) NOT NULL DEFAULT 0', + 'hash_algo' => 'VARCHAR(30) NOT NULL', + 'last_used' => 'DATETIME NULL', + 'date_created' => ' DATETIME NOT NULL', + 'date_expired' => ' DATETIME NULL', + ), 'idusertokenauth'); + $migrations[] = $this->migration->db->addUniqueKey('user_token_auth', 'password', 'uniq_password'); + + $migrations[] = $this->migration->db->dropIndex('user', 'uniq_keytoken'); + + $userModel = new Model(); + foreach ($userModel->getUsers(array()) as $user) { + if (!empty($user['token_auth'])) { + $migrations[] = $this->migration->db->insert('user_token_auth', array( + 'login' => $user['login'], + 'description' => 'Created by Matomo 4 migration', + 'password' => $userModel->hashTokenAuth($user['token_auth']), + 'date_created' => Date::now()->getDatetime() + )); + } + } + + // we don't delete the token_auth column so users can still downgrade to 3.X if they want to. However, the original + // token_auth will be regenerated for security reasons to no longer have it in plain text. So this column will be no longer used + // unless someone downgrades to 3.x + $columns = DbHelper::getTableColumns(Common::prefixTable('user')); + if (isset($columns['token_auth'])) { + $sql = sprintf('UPDATE %s set token_auth = MD5(CONCAT(NOW(), UUID()))', Common::prefixTable('user')); + $migrations[] = $this->migration->db->sql($sql, Updater\Migration\Db::ERROR_CODE_UNKNOWN_COLUMN); + } + + /** APP SPECIFIC TOKEN END */ + $customTrackerPluginActive = false; if (in_array('CustomPiwikJs', Config::getInstance()->Plugins['Plugins'])) { $customTrackerPluginActive = true; diff --git a/lang/en.json b/lang/en.json index c3c715ea232..cda996a2a26 100644 --- a/lang/en.json +++ b/lang/en.json @@ -112,6 +112,7 @@ "Continue": "Continue", "ContinueToPiwik": "Continue to Matomo", "CurrentlyUsingUnsecureHttp": "You are currently using Matomo over unsecure HTTP. This can make your Matomo vulnerable to security exploits. You may also be in breach of privacy laws, as some features including opt-out cookies will not work. We recommend you set up Matomo to use SSL (HTTPS) for improved security.", + "CreationDate": "Creation date", "CreatedByUser": "created by %s", "CurrentMonth": "Current Month", "CurrentWeek": "Current Week", @@ -389,6 +390,7 @@ "Search": "Search", "Clear": "Clear", "SearchNoResults": "No results", + "Security": "Security", "SeeAll": "see all", "SeeTheOfficialDocumentationForMoreInformation": "See the %1$sofficial documentation%2$s for more information.", "SeeThisFaq": "See %1$sthis faq%2$s.", diff --git a/misc/cron/updatetoken.php b/misc/cron/updatetoken.php index b10a21d3471..f1c154705b2 100644 --- a/misc/cron/updatetoken.php +++ b/misc/cron/updatetoken.php @@ -56,10 +56,7 @@ function getPiwikDomain() $environment = new Environment('cli'); $environment->init(); -$token = Db::get()->fetchOne("SELECT token_auth - FROM " . Common::prefixTable("user") . " - WHERE superuser_access = 1 - ORDER BY date_registered ASC"); +$token = Piwik::requestTemporarySystemAuthToken('LogImporter', 48); $filename = $environment->getContainer()->get('path.tmp') . '/cache/token.php'; diff --git a/misc/log-analytics b/misc/log-analytics index b3973f3cd0c..4794df04cb9 160000 --- a/misc/log-analytics +++ b/misc/log-analytics @@ -1 +1 @@ -Subproject commit b3973f3cd0cb773eb9cf3bfff439cc43640c5f15 +Subproject commit 4794df04cb9187031a044e79b77351c4f8e8ef08 diff --git a/plugins/API/Controller.php b/plugins/API/Controller.php index 326b048926c..d46bdf158a6 100644 --- a/plugins/API/Controller.php +++ b/plugins/API/Controller.php @@ -26,7 +26,9 @@ class Controller extends \Piwik\Plugin\Controller { function index() { - $token = 'token_auth=' . Common::getRequestVar('token_auth', 'anonymous', 'string'); + $tokenAuth = Common::getRequestVar('token_auth', 'anonymous', 'string'); + + $token = 'token_auth=' . $tokenAuth; // when calling the API through http, we limit the number of returned results if (!isset($_GET['filter_limit'])) { diff --git a/plugins/API/css/styles.css b/plugins/API/css/styles.css new file mode 100644 index 00000000000..3b52ba995a0 --- /dev/null +++ b/plugins/API/css/styles.css @@ -0,0 +1,29 @@ +#token_auth { + background-color:#E8FFE9; + border-color:#00CC3A; + border-style: solid; + border-width: 1px; + margin: 0pt 0pt 16px 8px; + padding: 12px; + line-height:4em; +} +.example, .example A { + color:#9E9E9E; +} + +.page_api{ + padding:0px 15px 0 15px; + font-size:13px; +} + +.page_api h2 { + border-bottom:1px solid #DADADA; + margin:10px -15px 15px 0px; + padding:0pt 0px 5px 0pt; + font-size:24px; +} + +.page_api p { + line-height:140%; + padding-bottom:20px; +} diff --git a/plugins/API/lang/en.json b/plugins/API/lang/en.json index 72dd837fae9..cc43b92313f 100644 --- a/plugins/API/lang/en.json +++ b/plugins/API/lang/en.json @@ -9,7 +9,7 @@ "ReportingApiReference": "Reporting API Reference", "TopLinkTooltip": "Access your Web Analytics data programmatically through a simple API in json, xml, etc.", "UserAuthentication": "User authentication", - "UsingTokenAuth": "If you want to %1$s request data within a script, a crontab, etc. %2$s you need to add the parameter %3$s to the API calls URLs that require authentication.", + "UsingTokenAuth": "If you want to %1$s request data within a script, a crontab, etc. %2$s you need to add the URL parameter %3$s to the API calls URLs that require authentication.", "Glossary": "Glossary", "LearnAboutCommonlyUsedTerms2": "Learn about the commonly used terms to make the most of Matomo Analytics.", "EvolutionMetricName": "%s Evolution" diff --git a/plugins/API/templates/listAllAPI.twig b/plugins/API/templates/listAllAPI.twig index 17da68de1d6..9c9c2ea70f5 100644 --- a/plugins/API/templates/listAllAPI.twig +++ b/plugins/API/templates/listAllAPI.twig @@ -19,13 +19,12 @@
- {{ 'API_UsingTokenAuth'|translate('','',"")|raw }}
-
&token_auth=