| 1 |
<?php |
| 2 |
|
| 3 |
/** |
| 4 |
* Matomo - free/libre analytics platform |
| 5 |
* |
| 6 |
* @link https://matomo.org |
| 7 |
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later |
| 8 |
*/ |
| 9 |
namespace Piwik\API; |
| 10 |
|
| 11 |
use Exception; |
| 12 |
use Piwik\Access; |
| 13 |
use Piwik\Http\HttpCodeException; |
| 14 |
use Piwik\Request\AuthenticationToken; |
| 15 |
use Piwik\Cache; |
| 16 |
use Piwik\Common; |
| 17 |
use Piwik\Config\GeneralConfig; |
| 18 |
use Piwik\Container\StaticContainer; |
| 19 |
use Piwik\Context; |
| 20 |
use Piwik\DataTable; |
| 21 |
use Piwik\Exception\PluginDeactivatedException; |
| 22 |
use Piwik\IP; |
| 23 |
use Piwik\Piwik; |
| 24 |
use Piwik\Plugin\Manager as PluginManager; |
| 25 |
use Piwik\Plugins\CoreHome\LoginAllowlist; |
| 26 |
use Piwik\SettingsServer; |
| 27 |
use Piwik\Url; |
| 28 |
use Piwik\UrlHelper; |
| 29 |
use Piwik\Log\LoggerInterface; |
| 30 |
/** |
| 31 |
* Dispatches API requests to the appropriate API method. |
| 32 |
* |
| 33 |
* The Request class is used throughout Piwik to call API methods. The difference |
| 34 |
* between using Request and calling API methods directly is that Request |
| 35 |
* will do more after calling the API including: applying generic filters, applying queued filters, |
| 36 |
* and handling the **flat** and **label** query parameters. |
| 37 |
* |
| 38 |
* Additionally, the Request class will **forward current query parameters** to the request |
| 39 |
* which is more convenient than calling {@link Piwik\Common::getRequestVar()} many times over. |
| 40 |
* |
| 41 |
* In most cases, using a Request object to query the API is the correct approach. |
| 42 |
* |
| 43 |
* ### Post-processing |
| 44 |
* |
| 45 |
* The return value of API methods undergo some extra processing before being returned by Request. |
| 46 |
* |
| 47 |
* ### Output Formats |
| 48 |
* |
| 49 |
* The value returned by Request will be serialized to a certain format before being returned. |
| 50 |
* |
| 51 |
* ### Examples |
| 52 |
* |
| 53 |
* **Basic Usage** |
| 54 |
* |
| 55 |
* $request = new Request([ |
| 56 |
* 'method' => 'UserLanguage.getLanguage', |
| 57 |
* 'idSite' => 1, |
| 58 |
* 'date' => 'yesterday', |
| 59 |
* 'period' => 'week', |
| 60 |
* 'format' => 'xml', |
| 61 |
* 'filter_limit' => 5, |
| 62 |
* 'filter_offset' => 0, |
| 63 |
* ]) |
| 64 |
* $result = $request->process(); |
| 65 |
* echo $result; |
| 66 |
* |
| 67 |
* **Getting a unrendered DataTable** |
| 68 |
* |
| 69 |
* // use the convenience method 'processRequest' |
| 70 |
* $dataTable = Request::processRequest('UserLanguage.getLanguage', array( |
| 71 |
* 'idSite' => 1, |
| 72 |
* 'date' => 'yesterday', |
| 73 |
* 'period' => 'week', |
| 74 |
* 'filter_limit' => 5, |
| 75 |
* 'filter_offset' => 0 |
| 76 |
* |
| 77 |
* 'format' => 'original', // this is the important bit |
| 78 |
* )); |
| 79 |
* echo "This DataTable has " . $dataTable->getRowsCount() . " rows."; |
| 80 |
* |
| 81 |
* @see https://piwik.org/docs/analytics-api |
| 82 |
* @api |
| 83 |
*/ |
| 84 |
class Request |
| 85 |
{ |
| 86 |
/** |
| 87 |
* The count of nested API request invocations. Used to determine if the currently executing request is the root or not. |
| 88 |
* |
| 89 |
* @var int |
| 90 |
*/ |
| 91 |
private static $nestedApiInvocationCount = 0; |
| 92 |
private $request = null; |
| 93 |
/** |
| 94 |
* Converts the supplied request string into an array of query parameter name/value |
| 95 |
* mappings. The current query parameters (everything in `$_GET` and `$_POST`) are |
| 96 |
* forwarded to request array before it is returned. |
| 97 |
* |
| 98 |
* @param string|array|null $request The base request string or array, eg, |
| 99 |
* `'module=UserLanguage&action=getLanguage'`. |
| 100 |
* @param array $defaultRequest Default query parameters. If a query parameter is absent in `$request`, it will be loaded |
| 101 |
* from this. Defaults to `$_GET + $_POST`. |
| 102 |
* @return array |
| 103 |
*/ |
| 104 |
public static function getRequestArrayFromString($request, $defaultRequest = null) |
| 105 |
{ |
| 106 |
if ($defaultRequest === null) { |
| 107 |
$defaultRequest = self::getDefaultRequest(); |
| 108 |
$requestRaw = self::getRequestParametersGET(); |
| 109 |
if (!empty($requestRaw['segment'])) { |
| 110 |
$defaultRequest['segment'] = $requestRaw['segment']; |
| 111 |
} |
| 112 |
// Only default to formatting metrics if the request doesn't already contain the format metrics parameter |
| 113 |
if (!isset($defaultRequest['format_metrics']) && !isset($request['format_metrics'])) { |
| 114 |
$defaultRequest['format_metrics'] = 'bc'; |
| 115 |
} |
| 116 |
} |
| 117 |
$requestArray = $defaultRequest; |
| 118 |
if (!is_null($request)) { |
| 119 |
if (is_array($request)) { |
| 120 |
$requestParsed = $request; |
| 121 |
} else { |
| 122 |
$request = trim($request); |
| 123 |
$request = str_replace(array("\n", "\t"), '', $request); |
| 124 |
$requestParsed = UrlHelper::getArrayFromQueryString($request); |
| 125 |
} |
| 126 |
$requestArray = $requestParsed + $defaultRequest; |
| 127 |
} |
| 128 |
foreach ($requestArray as &$element) { |
| 129 |
if (!is_array($element)) { |
| 130 |
$element = trim((string) $element); |
| 131 |
} |
| 132 |
} |
| 133 |
return $requestArray; |
| 134 |
} |
| 135 |
/** |
| 136 |
* @param string|array $request Query string that defines the API call (must at least contain a **method** parameter), |
| 137 |
* eg, `'method=UserLanguage.getLanguage&idSite=1&date=yesterday&period=week&format=xml'` |
| 138 |
* If a request is not provided, then we use the values in the `$_GET` and `$_POST` |
| 139 |
* superglobals. |
| 140 |
* @param array $defaultRequest Default query parameters. If a query parameter is absent in `$request`, it will be loaded |
| 141 |
* from this. Defaults to `$_GET + $_POST`. |
| 142 |
*/ |
| 143 |
public function __construct($request = null, $defaultRequest = null) |
| 144 |
{ |
| 145 |
$this->request = self::getRequestArrayFromString($request, $defaultRequest); |
| 146 |
$this->sanitizeRequest(); |
| 147 |
$this->renameModuleAndActionInRequest(); |
| 148 |
} |
| 149 |
/** |
| 150 |
* For backward compatibility: Piwik API still works if module=Referers, |
| 151 |
* we rewrite to correct renamed plugin: Referrers |
| 152 |
* |
| 153 |
* @param $module |
| 154 |
* @param $action |
| 155 |
* @return array( $module, $action ) |
| 156 |
* @ignore |
| 157 |
*/ |
| 158 |
public static function getRenamedModuleAndAction($module, $action) |
| 159 |
{ |
| 160 |
/** |
| 161 |
* This event is posted in the Request dispatcher and can be used |
| 162 |
* to overwrite the Module and Action to dispatch. |
| 163 |
* This is useful when some Controller methods or API methods have been renamed or moved to another plugin. |
| 164 |
* |
| 165 |
* @param $module string |
| 166 |
* @param $action string |
| 167 |
*/ |
| 168 |
Piwik::postEvent('Request.getRenamedModuleAndAction', array(&$module, &$action)); |
| 169 |
return array($module, $action); |
| 170 |
} |
| 171 |
/** |
| 172 |
* Make sure that the request contains no logical errors |
| 173 |
*/ |
| 174 |
private function sanitizeRequest() |
| 175 |
{ |
| 176 |
// The label filter does not work with expanded=1 because the data table IDs have a different meaning |
| 177 |
// depending on whether the table has been loaded yet. expanded=1 causes all tables to be loaded, which |
| 178 |
// is why the label filter can't descend when a recursive label has been requested. |
| 179 |
// To fix this, we remove the expanded parameter if a label parameter is set. |
| 180 |
if (isset($this->request['label']) && !empty($this->request['label']) && isset($this->request['expanded']) && $this->request['expanded']) { |
| 181 |
unset($this->request['expanded']); |
| 182 |
} |
| 183 |
} |
| 184 |
/** |
| 185 |
* Dispatches the API request to the appropriate API method and returns the result |
| 186 |
* after post-processing. |
| 187 |
* |
| 188 |
* Post-processing includes: |
| 189 |
* |
| 190 |
* - flattening if **flat** is 0 |
| 191 |
* - running generic filters unless **disable_generic_filters** is set to 1 |
| 192 |
* - URL decoding label column values |
| 193 |
* - running queued filters unless **disable_queued_filters** is set to 1 |
| 194 |
* - removing columns based on the values of the **hideColumns** and **showColumns** query parameters |
| 195 |
* - filtering rows if the **label** query parameter is set |
| 196 |
* - converting the result to the appropriate format (ie, XML, JSON, etc.) |
| 197 |
* |
| 198 |
* If `'original'` is supplied for the output format, the result is returned as a PHP |
| 199 |
* object. |
| 200 |
* |
| 201 |
* @return DataTable|DataTable\Map|scalar|array|object|resource|null The data resulting from the API call. |
| 202 |
* @throws Exception if the requested API method cannot be called, if required parameters for the |
| 203 |
* API method are missing or if the API method throws an exception and the **format** |
| 204 |
* query parameter is **original**. |
| 205 |
* @throws PluginDeactivatedException if the module plugin is not activated. |
| 206 |
*/ |
| 207 |
public function process() |
| 208 |
{ |
| 209 |
$shouldReloadAuth = \false; |
| 210 |
$hadSuperUserAccess = \false; |
| 211 |
$tokenAuthToRestore = null; |
| 212 |
try { |
| 213 |
++self::$nestedApiInvocationCount; |
| 214 |
// read the format requested for the output data |
| 215 |
$outputFormat = strtolower(Common::getRequestVar('format', 'xml', 'string', $this->request)); |
| 216 |
$disablePostProcessing = $this->shouldDisablePostProcessing(); |
| 217 |
// create the response |
| 218 |
$response = new \Piwik\API\ResponseBuilder($outputFormat, $this->request); |
| 219 |
// do not send any header when processing a nested API request, |
| 220 |
// as the headers might remain for to the original response |
| 221 |
if (!self::isCurrentApiRequestTheRootApiRequest()) { |
| 222 |
$response->disableSendHeader(); |
| 223 |
} |
| 224 |
if ($disablePostProcessing) { |
| 225 |
$response->disableDataTablePostProcessor(); |
| 226 |
} |
| 227 |
$corsHandler = new \Piwik\API\CORSHandler(); |
| 228 |
$corsHandler->handle(); |
| 229 |
$tokenAuth = StaticContainer::get(AuthenticationToken::class)->getAuthToken($this->request); |
| 230 |
// IP check is needed here as we cannot listen to API.Request.authenticate as it would then not return proper API format response. |
| 231 |
// We can also not do it by listening to API.Request.dispatch as by then the user is already authenticated and we want to make sure |
| 232 |
// to not expose any information in case the IP is not allowed. |
| 233 |
$list = new LoginAllowlist(); |
| 234 |
if ($list->shouldCheckAllowlist() && $list->shouldAllowlistApplyToAPI()) { |
| 235 |
$ip = IP::getIpFromHeader(); |
| 236 |
$list->checkIsAllowed($ip); |
| 237 |
} |
| 238 |
// read parameters |
| 239 |
$moduleMethod = Common::getRequestVar('method', null, 'string', $this->request); |
| 240 |
[$module, $method] = $this->extractModuleAndMethod($moduleMethod); |
| 241 |
[$module, $method] = self::getRenamedModuleAndAction($module, $method); |
| 242 |
PluginManager::getInstance()->checkIsPluginActivated($module); |
| 243 |
$apiClassName = self::getClassNameAPI($module); |
| 244 |
if ($shouldReloadAuth = self::shouldReloadAuthUsingTokenAuth($this->request)) { |
| 245 |
$access = Access::getInstance(); |
| 246 |
$tokenAuthToRestore = $access->getTokenAuth(); |
| 247 |
$hadSuperUserAccess = $access->hasSuperUserAccess(); |
| 248 |
self::forceReloadAuthUsingTokenAuth($tokenAuth); |
| 249 |
} |
| 250 |
// call the method |
| 251 |
$returnedValue = \Piwik\API\Proxy::getInstance()->call($apiClassName, $method, $this->request); |
| 252 |
// get the response with the request query parameters loaded, since DataTablePost processor will use the Report |
| 253 |
// class instance, which may inspect the query parameters. (eg, it may look for the idCustomReport parameters |
| 254 |
// which may only exist in $this->request, if the request was called programmatically) |
| 255 |
$toReturn = Context::executeWithQueryParameters($this->request, function () use($response, $returnedValue, $module, $method) { |
| 256 |
return $response->getResponse($returnedValue, $module, $method); |
| 257 |
}); |
| 258 |
} catch (Exception $e) { |
| 259 |
if ($e instanceof HttpCodeException && $e->getCode() >= 400 && $e->getCode() < 500) { |
| 260 |
StaticContainer::get(LoggerInterface::class)->debug('Uncaught client error in API: {exception}', ['exception' => $e, 'ignoreInScreenWriter' => \true]); |
| 261 |
} else { |
| 262 |
StaticContainer::get(LoggerInterface::class)->error('Uncaught exception in API: {exception}', ['exception' => $e, 'ignoreInScreenWriter' => \true]); |
| 263 |
} |
| 264 |
if (empty($response)) { |
| 265 |
$response = new \Piwik\API\ResponseBuilder('console', $this->request); |
| 266 |
} |
| 267 |
$toReturn = $response->getResponseException($e); |
| 268 |
} finally { |
| 269 |
--self::$nestedApiInvocationCount; |
| 270 |
} |
| 271 |
if ($shouldReloadAuth) { |
| 272 |
$this->restoreAuthUsingTokenAuth($tokenAuthToRestore, $hadSuperUserAccess); |
| 273 |
} |
| 274 |
return $toReturn; |
| 275 |
} |
| 276 |
private function restoreAuthUsingTokenAuth( |
| 277 |
#[\SensitiveParameter] |
| 278 |
$tokenToRestore, $hadSuperUserAccess) |
| 279 |
{ |
| 280 |
// if we would not make sure to unset super user access, the tokenAuth would be not authenticated and any |
| 281 |
// token would just keep super user access (eg if the token that was reloaded before had super user access) |
| 282 |
Access::getInstance()->setSuperUserAccess(\false); |
| 283 |
// we need to restore by reloading the tokenAuth as some permissions could have been removed in the API |
| 284 |
// request etc. Otherwise we could just store a clone of Access::getInstance() and restore here |
| 285 |
self::forceReloadAuthUsingTokenAuth($tokenToRestore); |
| 286 |
if ($hadSuperUserAccess && !Access::getInstance()->hasSuperUserAccess()) { |
| 287 |
// we are in context of `doAsSuperUser()` and need to restore this behaviour |
| 288 |
Access::getInstance()->setSuperUserAccess(\true); |
| 289 |
} |
| 290 |
} |
| 291 |
/** |
| 292 |
* Returns the name of a plugin's API class by plugin name. |
| 293 |
* |
| 294 |
* @param string $plugin The plugin name, eg, `'Referrers'`. |
| 295 |
* @return string The fully qualified API class name, eg, `'\Piwik\Plugins\Referrers\API'`. |
| 296 |
*/ |
| 297 |
public static function getClassNameAPI($plugin) |
| 298 |
{ |
| 299 |
return sprintf('\\Piwik\\Plugins\\%s\\API', $plugin); |
| 300 |
} |
| 301 |
/** |
| 302 |
* @ignore |
| 303 |
* @internal |
| 304 |
* @param string $currentApiMethod |
| 305 |
*/ |
| 306 |
public static function setIsRootRequestApiRequest($currentApiMethod) |
| 307 |
{ |
| 308 |
Cache::getTransientCache()->save('API.setIsRootRequestApiRequest', $currentApiMethod); |
| 309 |
} |
| 310 |
/** |
| 311 |
* @ignore |
| 312 |
* @internal |
| 313 |
* @return string current Api Method if it is an api request |
| 314 |
*/ |
| 315 |
public static function getRootApiRequestMethod() |
| 316 |
{ |
| 317 |
return Cache::getTransientCache()->fetch('API.setIsRootRequestApiRequest'); |
| 318 |
} |
| 319 |
/** |
| 320 |
* Detect if the root request (the actual request) is an API request or not. To detect whether an API is currently |
| 321 |
* request within any request, have a look at {@link isApiRequest()}. |
| 322 |
* |
| 323 |
* @return bool |
| 324 |
* @throws Exception |
| 325 |
*/ |
| 326 |
public static function isRootRequestApiRequest() |
| 327 |
{ |
| 328 |
$apiMethod = Cache::getTransientCache()->fetch('API.setIsRootRequestApiRequest'); |
| 329 |
return !empty($apiMethod); |
| 330 |
} |
| 331 |
/** |
| 332 |
* Checks if the currently executing API request is the root API request or not. |
| 333 |
* |
| 334 |
* Note: the "root" API request is the first request made. Within that request, further API methods |
| 335 |
* can be called programmatically. These requests are considered "child" API requests. |
| 336 |
* |
| 337 |
* @return bool |
| 338 |
* @throws Exception |
| 339 |
*/ |
| 340 |
public static function isCurrentApiRequestTheRootApiRequest() |
| 341 |
{ |
| 342 |
return self::$nestedApiInvocationCount == 1; |
| 343 |
} |
| 344 |
/** |
| 345 |
* Checks if the currently executing API request is running inside another API request. |
| 346 |
* |
| 347 |
* This is true only for "child" API requests, i.e. requests that were dispatched |
| 348 |
* programmatically from within another API method (for example the sub-requests run by |
| 349 |
* {@link \Piwik\Plugins\API\API::getBulkRequest()}). It is false for the root request and |
| 350 |
* when no API request is currently being processed. |
| 351 |
*/ |
| 352 |
public static function isCurrentApiRequestNestedInAnotherApiRequest() : bool |
| 353 |
{ |
| 354 |
return self::$nestedApiInvocationCount > 1; |
| 355 |
} |
| 356 |
/** |
| 357 |
* Detect if request is an API request. Meaning the module is 'API' and an API method having a valid format was |
| 358 |
* specified. Note that this method will return true even if the actual request is for example a regular UI |
| 359 |
* reporting page request but within this request we are currently processing an API request (eg a |
| 360 |
* controller calls Request::processRequest('API.getMatomoVersion')). To find out if the root request is an API |
| 361 |
* request or not, call {@link isRootRequestApiRequest()} |
| 362 |
* |
| 363 |
* @param array $request eg array('module' => 'API', 'method' => 'Test.getMethod') |
| 364 |
* @return bool |
| 365 |
* @throws Exception |
| 366 |
*/ |
| 367 |
public static function isApiRequest($request) |
| 368 |
{ |
| 369 |
$method = self::getMethodIfApiRequest($request); |
| 370 |
return !empty($method); |
| 371 |
} |
| 372 |
/** |
| 373 |
* Returns the current API method being executed, if the current request is an API request. |
| 374 |
* |
| 375 |
* @param array|null $request eg array('module' => 'API', 'method' => 'Test.getMethod') |
| 376 |
* @return string|null |
| 377 |
* @throws Exception |
| 378 |
*/ |
| 379 |
public static function getMethodIfApiRequest($request) |
| 380 |
{ |
| 381 |
$module = Common::getRequestVar('module', '', 'string', $request); |
| 382 |
$method = Common::getRequestVar('method', '', 'string', $request); |
| 383 |
$isApi = $module === 'API' && !empty($method) && count(explode('.', $method)) === 2; |
| 384 |
return $isApi ? $method : null; |
| 385 |
} |
| 386 |
/** |
| 387 |
* If the token_auth is found in the $request parameter, |
| 388 |
* the current session will be authenticated using this token_auth. |
| 389 |
* It will overwrite the previous Auth object. |
| 390 |
* |
| 391 |
* @param array $request If null, uses the default request ($_GET) |
| 392 |
* @return void |
| 393 |
* @ignore |
| 394 |
*/ |
| 395 |
public static function reloadAuthUsingTokenAuth($request = null) |
| 396 |
{ |
| 397 |
// if a token_auth is specified in the API request, we load the right permissions |
| 398 |
$token_auth = StaticContainer::get(AuthenticationToken::class)->getAuthToken($request); |
| 399 |
if (self::shouldReloadAuthUsingTokenAuth($request)) { |
| 400 |
self::forceReloadAuthUsingTokenAuth($token_auth); |
| 401 |
} |
| 402 |
} |
| 403 |
/** |
| 404 |
* The current session will be authenticated using this token_auth. |
| 405 |
* It will overwrite the previous Auth object. |
| 406 |
* |
| 407 |
* @param string $tokenAuth |
| 408 |
* @return void |
| 409 |
*/ |
| 410 |
private static function forceReloadAuthUsingTokenAuth( |
| 411 |
#[\SensitiveParameter] |
| 412 |
$tokenAuth) |
| 413 |
{ |
| 414 |
/** |
| 415 |
* Triggered when authenticating an API request, but only if the **token_auth** |
| 416 |
* query parameter is found in the request. |
| 417 |
* |
| 418 |
* Plugins that provide authentication capabilities should subscribe to this event |
| 419 |
* and make sure the global authentication object (the object returned by `StaticContainer::get('Piwik\Auth')`) |
| 420 |
* is setup to use `$token_auth` when its `authenticate()` method is executed. |
| 421 |
* |
| 422 |
* @param string $token_auth The value of the **token_auth** query parameter. |
| 423 |
*/ |
| 424 |
Piwik::postEvent('API.Request.authenticate', array($tokenAuth)); |
| 425 |
if (!Access::getInstance()->reloadAccess() && $tokenAuth && $tokenAuth !== 'anonymous') { |
| 426 |
/** |
| 427 |
* @ignore |
| 428 |
* @internal |
| 429 |
*/ |
| 430 |
Piwik::postEvent('API.Request.authenticate.failed'); |
| 431 |
} |
| 432 |
SettingsServer::raiseMemoryLimitIfNecessary(); |
| 433 |
} |
| 434 |
/** |
| 435 |
* Needs to be called AFTER the user has been authenticated using a token. |
| 436 |
* |
| 437 |
* @internal |
| 438 |
* @ignore |
| 439 |
* @param string $module |
| 440 |
* @param string $action |
| 441 |
* @throws Exception |
| 442 |
*/ |
| 443 |
public static function checkTokenAuthIsNotLimited($module, $action) |
| 444 |
{ |
| 445 |
$isApi = $module === 'API' && (empty($action) || $action === 'index'); |
| 446 |
if ($isApi || Common::isPhpCliMode()) { |
| 447 |
return; |
| 448 |
} |
| 449 |
if (Access::getInstance()->hasSuperUserAccess()) { |
| 450 |
$ex = new \Piwik\Exception\Exception(Piwik::translate('Widgetize_TooHighAccessLevel', [Url::getExternalLinkTag('https://matomo.org/faq/troubleshooting/faq_147/'), '</a>'])); |
| 451 |
$ex->setIsHtmlMessage(); |
| 452 |
throw $ex; |
| 453 |
} |
| 454 |
$allowWriteAdminModuleActionConfig = StaticContainer::get('token_auth.write_admin_allowed_module_actions'); |
| 455 |
if (!is_array($allowWriteAdminModuleActionConfig)) { |
| 456 |
$allowWriteAdminModuleActionConfig = []; |
| 457 |
} |
| 458 |
$allowWriteAdmin = GeneralConfig::getConfigValue('enable_framed_allow_write_admin_token_auth') == 1; |
| 459 |
$allowWriteAdminModuleAction = in_array($module . '.' . $action, $allowWriteAdminModuleActionConfig, \true); |
| 460 |
if (Piwik::isUserHasSomeWriteAccess() && !$allowWriteAdmin && !$allowWriteAdminModuleAction) { |
| 461 |
// we allow UI authentication/ embedding widgets / reports etc only for users that have only view |
| 462 |
// access. it's mostly there to get users to use auth tokens of view users when embedding reports |
| 463 |
// token_auth is fine for API calls since they would be always authenticated later anyway |
| 464 |
// token_auth is also fine in CLI mode as eg doAsSuperUser might be used etc |
| 465 |
// |
| 466 |
// NOTE: this does not apply if the [General] enable_framed_allow_write_admin_token_auth INI |
| 467 |
// option is set, or if the current module/action is allowlisted in the |
| 468 |
// token_auth.write_admin_allowed_module_actions DI entry. |
| 469 |
$ex = new \Piwik\Exception\Exception(Piwik::translate('Widgetize_ViewAccessRequired', [Url::getExternalLinkTag('https://matomo.org/faq/troubleshooting/faq_147/') . 'https://matomo.org/faq/troubleshooting/faq_147/</a>'])); |
| 470 |
$ex->setIsHtmlMessage(); |
| 471 |
throw $ex; |
| 472 |
} |
| 473 |
} |
| 474 |
/** |
| 475 |
* @internal |
| 476 |
* @ignore |
| 477 |
* @param $request |
| 478 |
* @return bool |
| 479 |
* @throws Exception |
| 480 |
*/ |
| 481 |
public static function shouldReloadAuthUsingTokenAuth($request) |
| 482 |
{ |
| 483 |
if (is_null($request)) { |
| 484 |
return StaticContainer::get(AuthenticationToken::class)->getAuthToken() != Access::getInstance()->getTokenAuth(); |
| 485 |
} |
| 486 |
if (!isset($request['token_auth'])) { |
| 487 |
// no token is given so we just keep the current loaded user |
| 488 |
return \false; |
| 489 |
} |
| 490 |
// a token is specified, we need to reload auth in case it is different than the current one, even if it is empty |
| 491 |
$tokenAuth = Common::getRequestVar('token_auth', '', 'string', $request); |
| 492 |
// not using !== is on purpose as getTokenAuth() might return null whereas $tokenAuth is '' . In this case |
| 493 |
// we do not need to reload. |
| 494 |
return $tokenAuth != Access::getInstance()->getTokenAuth(); |
| 495 |
} |
| 496 |
/** |
| 497 |
* Returns true if a token_auth parameter was supplied via a secure mechanism and is not present as a URL parameter |
| 498 |
* At the moment POST requests are checked, but in future other mechanism such as Authorisation HTTP header |
| 499 |
* and bearer tokens might be used as well. |
| 500 |
* |
| 501 |
* @return bool True if token was supplied in a secure way |
| 502 |
* @deprecated will be removed in Matomo 6 |
| 503 |
*/ |
| 504 |
public static function isTokenAuthProvidedSecurely() : bool |
| 505 |
{ |
| 506 |
return StaticContainer::get(AuthenticationToken::class)->wasTokenAuthProvidedSecurely(); |
| 507 |
} |
| 508 |
/** |
| 509 |
* Returns array($class, $method) from the given string $class.$method |
| 510 |
* |
| 511 |
* @param string $parameter |
| 512 |
* @throws Exception |
| 513 |
* @return array |
| 514 |
*/ |
| 515 |
private function extractModuleAndMethod($parameter) |
| 516 |
{ |
| 517 |
$a = explode('.', $parameter); |
| 518 |
if (count($a) != 2) { |
| 519 |
throw new Exception("The method name is invalid. Expected 'module.methodName'"); |
| 520 |
} |
| 521 |
return $a; |
| 522 |
} |
| 523 |
/** |
| 524 |
* Helper method that processes an API request in one line using the variables in `$_GET` |
| 525 |
* and `$_POST`. |
| 526 |
* |
| 527 |
* @param string $method The API method to call, ie, `'Actions.getPageTitles'`. |
| 528 |
* @param array $paramOverride The parameter name-value pairs to use instead of what's |
| 529 |
* in `$_GET` & `$_POST`. |
| 530 |
* @param array $defaultRequest Default query parameters. If a query parameter is absent in `$request`, it will be loaded |
| 531 |
* from this. Defaults to `$_GET + $_POST`. |
| 532 |
* |
| 533 |
* To avoid using any parameters from $_GET or $_POST, set this to an empty `array()`. |
| 534 |
* @return mixed The result of the API request. See {@link process()}. |
| 535 |
*/ |
| 536 |
public static function processRequest($method, $paramOverride = array(), $defaultRequest = null) |
| 537 |
{ |
| 538 |
$params = array(); |
| 539 |
$params['format'] = 'original'; |
| 540 |
$params['serialize'] = '0'; |
| 541 |
$params['module'] = 'API'; |
| 542 |
$params['method'] = $method; |
| 543 |
$params['compare'] = '0'; |
| 544 |
$params = $paramOverride + $params; |
| 545 |
// process request |
| 546 |
$request = new \Piwik\API\Request($params, $defaultRequest); |
| 547 |
return $request->process(); |
| 548 |
} |
| 549 |
/** |
| 550 |
* Returns the original request parameters in the current query string as an array mapping |
| 551 |
* query parameter names with values. The result of this function will not be affected |
| 552 |
* by any modifications to `$_GET` and will not include parameters in `$_POST`. |
| 553 |
* |
| 554 |
* @return array |
| 555 |
*/ |
| 556 |
public static function getRequestParametersGET() |
| 557 |
{ |
| 558 |
if (empty($_SERVER['QUERY_STRING'])) { |
| 559 |
return array(); |
| 560 |
} |
| 561 |
$GET = UrlHelper::getArrayFromQueryString($_SERVER['QUERY_STRING']); |
| 562 |
return $GET; |
| 563 |
} |
| 564 |
/** |
| 565 |
* Returns the URL for the current requested report w/o any filter parameters. |
| 566 |
* |
| 567 |
* @param string $module The API module. |
| 568 |
* @param string $action The API action. |
| 569 |
* @param array $queryParams Query parameter overrides. |
| 570 |
* @return string |
| 571 |
*/ |
| 572 |
public static function getBaseReportUrl($module, $action, $queryParams = array()) |
| 573 |
{ |
| 574 |
$params = array_merge($queryParams, array('module' => $module, 'action' => $action)); |
| 575 |
return \Piwik\API\Request::getCurrentUrlWithoutGenericFilters($params); |
| 576 |
} |
| 577 |
/** |
| 578 |
* Returns the current URL without generic filter query parameters. |
| 579 |
* |
| 580 |
* @param array $params Query parameter values to override in the new URL. |
| 581 |
* @return string |
| 582 |
*/ |
| 583 |
public static function getCurrentUrlWithoutGenericFilters($params) |
| 584 |
{ |
| 585 |
// unset all filter query params so the related report will show up in its default state, |
| 586 |
// unless the filter param was in $queryParams |
| 587 |
$genericFiltersInfo = \Piwik\API\DataTableGenericFilter::getGenericFiltersInformation(); |
| 588 |
foreach ($genericFiltersInfo as $filter) { |
| 589 |
foreach ($filter[1] as $queryParamName => $queryParamInfo) { |
| 590 |
if (!isset($params[$queryParamName])) { |
| 591 |
$params[$queryParamName] = null; |
| 592 |
} |
| 593 |
} |
| 594 |
} |
| 595 |
$params['compareDates'] = null; |
| 596 |
$params['comparePeriods'] = null; |
| 597 |
$params['compareSegments'] = null; |
| 598 |
return Url::getCurrentQueryStringWithParametersModified($params); |
| 599 |
} |
| 600 |
/** |
| 601 |
* Returns whether the DataTable result will have to be expanded for the |
| 602 |
* current request before rendering. |
| 603 |
* |
| 604 |
* @return bool |
| 605 |
* @ignore |
| 606 |
*/ |
| 607 |
public static function shouldLoadExpanded() |
| 608 |
{ |
| 609 |
// if filter_column_recursive & filter_pattern_recursive are supplied, and flat isn't supplied |
| 610 |
// we have to load all the child subtables. |
| 611 |
return Common::getRequestVar('filter_column_recursive', \false) !== \false && Common::getRequestVar('filter_pattern_recursive', \false) !== \false && !self::shouldLoadFlatten(); |
| 612 |
} |
| 613 |
/** |
| 614 |
* @return bool |
| 615 |
*/ |
| 616 |
public static function shouldLoadFlatten() |
| 617 |
{ |
| 618 |
return Common::getRequestVar('flat', \false) == 1; |
| 619 |
} |
| 620 |
/** |
| 621 |
* Returns the segment query parameter from the original request, without modifications. |
| 622 |
* |
| 623 |
* @return string|false |
| 624 |
*/ |
| 625 |
public static function getRawSegmentFromRequest() |
| 626 |
{ |
| 627 |
// we need the URL encoded segment parameter, we fetch it from _SERVER['QUERY_STRING'] instead of default URL decoded _GET |
| 628 |
$segmentRaw = \false; |
| 629 |
$segment = Common::getRequestVar('segment', '', 'string'); |
| 630 |
if (!empty($segment)) { |
| 631 |
$request = \Piwik\API\Request::getRequestParametersGET(); |
| 632 |
if (!empty($request['segment'])) { |
| 633 |
$segmentRaw = $request['segment']; |
| 634 |
} |
| 635 |
} |
| 636 |
return $segmentRaw; |
| 637 |
} |
| 638 |
private function renameModuleAndActionInRequest() |
| 639 |
{ |
| 640 |
if (empty($this->request['apiModule'])) { |
| 641 |
return; |
| 642 |
} |
| 643 |
if (empty($this->request['apiAction'])) { |
| 644 |
$this->request['apiAction'] = null; |
| 645 |
} |
| 646 |
[$this->request['apiModule'], $this->request['apiAction']] = $this->getRenamedModuleAndAction($this->request['apiModule'], $this->request['apiAction']); |
| 647 |
} |
| 648 |
/** |
| 649 |
* @return array |
| 650 |
*/ |
| 651 |
private static function getDefaultRequest() |
| 652 |
{ |
| 653 |
return $_GET + $_POST; |
| 654 |
} |
| 655 |
private function shouldDisablePostProcessing() |
| 656 |
{ |
| 657 |
$shouldDisable = \false; |
| 658 |
/** |
| 659 |
* After an API method returns a value, the value is post processed (eg, rows are sorted |
| 660 |
* based on the `filter_sort_column` query parameter, rows are truncated based on the |
| 661 |
* `filter_limit`/`filter_offset` parameters, amongst other things). |
| 662 |
* |
| 663 |
* If you're creating a plugin that needs to disable post processing entirely for |
| 664 |
* certain requests, use this event. |
| 665 |
* |
| 666 |
* @param bool &$shouldDisable Set this to true to disable datatable post processing for a request. |
| 667 |
* @param array $request The request parameters. |
| 668 |
*/ |
| 669 |
Piwik::postEvent('Request.shouldDisablePostProcessing', [&$shouldDisable, $this->request]); |
| 670 |
if (!$shouldDisable) { |
| 671 |
$shouldDisable = self::isCurrentApiRequestTheRootApiRequest() && Common::getRequestVar('disable_root_datatable_post_processor', 0, 'int', $this->request) == 1; |
| 672 |
} |
| 673 |
return $shouldDisable; |
| 674 |
} |
| 675 |
} |
| 676 |
|