Teknik is a suite of services with attractive and functional interfaces. https://www.teknik.io/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Request.php 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. <?php
  2. /**
  3. * Piwik - free/libre analytics platform
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. *
  8. */
  9. namespace Piwik\API;
  10. use Exception;
  11. use Piwik\Access;
  12. use Piwik\Common;
  13. use Piwik\DataTable;
  14. use Piwik\Piwik;
  15. use Piwik\PluginDeactivatedException;
  16. use Piwik\SettingsServer;
  17. use Piwik\Url;
  18. use Piwik\UrlHelper;
  19. use Piwik\Log;
  20. /**
  21. * Dispatches API requests to the appropriate API method.
  22. *
  23. * The Request class is used throughout Piwik to call API methods. The difference
  24. * between using Request and calling API methods directly is that Request
  25. * will do more after calling the API including: applying generic filters, applying queued filters,
  26. * and handling the **flat** and **label** query parameters.
  27. *
  28. * Additionally, the Request class will **forward current query parameters** to the request
  29. * which is more convenient than calling {@link Piwik\Common::getRequestVar()} many times over.
  30. *
  31. * In most cases, using a Request object to query the API is the correct approach.
  32. *
  33. * ### Post-processing
  34. *
  35. * The return value of API methods undergo some extra processing before being returned by Request.
  36. * To learn more about what happens to API results, read [this](/guides/piwiks-web-api#extra-report-processing).
  37. *
  38. * ### Output Formats
  39. *
  40. * The value returned by Request will be serialized to a certain format before being returned.
  41. * To see the list of supported output formats, read [this](/guides/piwiks-web-api#output-formats).
  42. *
  43. * ### Examples
  44. *
  45. * **Basic Usage**
  46. *
  47. * $request = new Request('method=UserSettings.getWideScreen&idSite=1&date=yesterday&period=week'
  48. * . '&format=xml&filter_limit=5&filter_offset=0')
  49. * $result = $request->process();
  50. * echo $result;
  51. *
  52. * **Getting a unrendered DataTable**
  53. *
  54. * // use the convenience method 'processRequest'
  55. * $dataTable = Request::processRequest('UserSettings.getWideScreen', array(
  56. * 'idSite' => 1,
  57. * 'date' => 'yesterday',
  58. * 'period' => 'week',
  59. * 'filter_limit' => 5,
  60. * 'filter_offset' => 0
  61. *
  62. * 'format' => 'original', // this is the important bit
  63. * ));
  64. * echo "This DataTable has " . $dataTable->getRowsCount() . " rows.";
  65. *
  66. * @see http://piwik.org/docs/analytics-api
  67. * @api
  68. */
  69. class Request
  70. {
  71. protected $request = null;
  72. /**
  73. * Converts the supplied request string into an array of query paramater name/value
  74. * mappings. The current query parameters (everything in `$_GET` and `$_POST`) are
  75. * forwarded to request array before it is returned.
  76. *
  77. * @param string|array $request The base request string or array, eg,
  78. * `'module=UserSettings&action=getWidescreen'`.
  79. * @return array
  80. */
  81. public static function getRequestArrayFromString($request)
  82. {
  83. $defaultRequest = $_GET + $_POST;
  84. $requestRaw = self::getRequestParametersGET();
  85. if (!empty($requestRaw['segment'])) {
  86. $defaultRequest['segment'] = $requestRaw['segment'];
  87. }
  88. $requestArray = $defaultRequest;
  89. if (!is_null($request)) {
  90. if (is_array($request)) {
  91. $requestParsed = $request;
  92. } else {
  93. $request = trim($request);
  94. $request = str_replace(array("\n", "\t"), '', $request);
  95. $requestParsed = UrlHelper::getArrayFromQueryString($request);
  96. }
  97. $requestArray = $requestParsed + $defaultRequest;
  98. }
  99. foreach ($requestArray as &$element) {
  100. if (!is_array($element)) {
  101. $element = trim($element);
  102. }
  103. }
  104. return $requestArray;
  105. }
  106. /**
  107. * Constructor.
  108. *
  109. * @param string|array $request Query string that defines the API call (must at least contain a **method** parameter),
  110. * eg, `'method=UserSettings.getWideScreen&idSite=1&date=yesterday&period=week&format=xml'`
  111. * If a request is not provided, then we use the values in the `$_GET` and `$_POST`
  112. * superglobals.
  113. */
  114. public function __construct($request = null)
  115. {
  116. $this->request = self::getRequestArrayFromString($request);
  117. $this->sanitizeRequest();
  118. }
  119. /**
  120. * For backward compatibility: Piwik API still works if module=Referers,
  121. * we rewrite to correct renamed plugin: Referrers
  122. *
  123. * @param $module
  124. * @return string
  125. * @ignore
  126. */
  127. public static function renameModule($module)
  128. {
  129. $moduleToRedirect = array(
  130. 'Referers' => 'Referrers',
  131. 'PDFReports' => 'ScheduledReports',
  132. );
  133. if (isset($moduleToRedirect[$module])) {
  134. return $moduleToRedirect[$module];
  135. }
  136. return $module;
  137. }
  138. /**
  139. * Make sure that the request contains no logical errors
  140. */
  141. private function sanitizeRequest()
  142. {
  143. // The label filter does not work with expanded=1 because the data table IDs have a different meaning
  144. // depending on whether the table has been loaded yet. expanded=1 causes all tables to be loaded, which
  145. // is why the label filter can't descend when a recursive label has been requested.
  146. // To fix this, we remove the expanded parameter if a label parameter is set.
  147. if (isset($this->request['label']) && !empty($this->request['label'])
  148. && isset($this->request['expanded']) && $this->request['expanded']
  149. ) {
  150. unset($this->request['expanded']);
  151. }
  152. }
  153. /**
  154. * Dispatches the API request to the appropriate API method and returns the result
  155. * after post-processing.
  156. *
  157. * Post-processing includes:
  158. *
  159. * - flattening if **flat** is 0
  160. * - running generic filters unless **disable_generic_filters** is set to 1
  161. * - URL decoding label column values
  162. * - running queued filters unless **disable_queued_filters** is set to 1
  163. * - removing columns based on the values of the **hideColumns** and **showColumns** query parameters
  164. * - filtering rows if the **label** query parameter is set
  165. * - converting the result to the appropriate format (ie, XML, JSON, etc.)
  166. *
  167. * If `'original'` is supplied for the output format, the result is returned as a PHP
  168. * object.
  169. *
  170. * @throws PluginDeactivatedException if the module plugin is not activated.
  171. * @throws Exception if the requested API method cannot be called, if required parameters for the
  172. * API method are missing or if the API method throws an exception and the **format**
  173. * query parameter is **original**.
  174. * @return DataTable|Map|string The data resulting from the API call.
  175. */
  176. public function process()
  177. {
  178. // read the format requested for the output data
  179. $outputFormat = strtolower(Common::getRequestVar('format', 'xml', 'string', $this->request));
  180. // create the response
  181. $response = new ResponseBuilder($outputFormat, $this->request);
  182. $corsHandler = new CORSHandler();
  183. $corsHandler->handle();
  184. try {
  185. // read parameters
  186. $moduleMethod = Common::getRequestVar('method', null, 'string', $this->request);
  187. list($module, $method) = $this->extractModuleAndMethod($moduleMethod);
  188. $module = $this->renameModule($module);
  189. if (!\Piwik\Plugin\Manager::getInstance()->isPluginActivated($module)) {
  190. throw new PluginDeactivatedException($module);
  191. }
  192. $apiClassName = $this->getClassNameAPI($module);
  193. self::reloadAuthUsingTokenAuth($this->request);
  194. // call the method
  195. $returnedValue = Proxy::getInstance()->call($apiClassName, $method, $this->request);
  196. $toReturn = $response->getResponse($returnedValue, $module, $method);
  197. } catch (Exception $e) {
  198. Log::debug($e);
  199. $toReturn = $response->getResponseException($e);
  200. }
  201. return $toReturn;
  202. }
  203. /**
  204. * Returns the name of a plugin's API class by plugin name.
  205. *
  206. * @param string $plugin The plugin name, eg, `'Referrers'`.
  207. * @return string The fully qualified API class name, eg, `'\Piwik\Plugins\Referrers\API'`.
  208. */
  209. public static function getClassNameAPI($plugin)
  210. {
  211. return sprintf('\Piwik\Plugins\%s\API', $plugin);
  212. }
  213. /**
  214. * If the token_auth is found in the $request parameter,
  215. * the current session will be authenticated using this token_auth.
  216. * It will overwrite the previous Auth object.
  217. *
  218. * @param array $request If null, uses the default request ($_GET)
  219. * @return void
  220. * @ignore
  221. */
  222. public static function reloadAuthUsingTokenAuth($request = null)
  223. {
  224. // if a token_auth is specified in the API request, we load the right permissions
  225. $token_auth = Common::getRequestVar('token_auth', '', 'string', $request);
  226. if ($token_auth) {
  227. /**
  228. * Triggered when authenticating an API request, but only if the **token_auth**
  229. * query parameter is found in the request.
  230. *
  231. * Plugins that provide authentication capabilities should subscribe to this event
  232. * and make sure the global authentication object (the object returned by `Registry::get('auth')`)
  233. * is setup to use `$token_auth` when its `authenticate()` method is executed.
  234. *
  235. * @param string $token_auth The value of the **token_auth** query parameter.
  236. */
  237. Piwik::postEvent('API.Request.authenticate', array($token_auth));
  238. Access::getInstance()->reloadAccess();
  239. SettingsServer::raiseMemoryLimitIfNecessary();
  240. }
  241. }
  242. /**
  243. * Returns array($class, $method) from the given string $class.$method
  244. *
  245. * @param string $parameter
  246. * @throws Exception
  247. * @return array
  248. */
  249. private function extractModuleAndMethod($parameter)
  250. {
  251. $a = explode('.', $parameter);
  252. if (count($a) != 2) {
  253. throw new Exception("The method name is invalid. Expected 'module.methodName'");
  254. }
  255. return $a;
  256. }
  257. /**
  258. * Helper method that processes an API request in one line using the variables in `$_GET`
  259. * and `$_POST`.
  260. *
  261. * @param string $method The API method to call, ie, `'Actions.getPageTitles'`.
  262. * @param array $paramOverride The parameter name-value pairs to use instead of what's
  263. * in `$_GET` & `$_POST`.
  264. * @return mixed The result of the API request. See {@link process()}.
  265. */
  266. public static function processRequest($method, $paramOverride = array())
  267. {
  268. $params = array();
  269. $params['format'] = 'original';
  270. $params['module'] = 'API';
  271. $params['method'] = $method;
  272. $params = $paramOverride + $params;
  273. // process request
  274. $request = new Request($params);
  275. return $request->process();
  276. }
  277. /**
  278. * Returns the original request parameters in the current query string as an array mapping
  279. * query parameter names with values. The result of this function will not be affected
  280. * by any modifications to `$_GET` and will not include parameters in `$_POST`.
  281. *
  282. * @return array
  283. */
  284. public static function getRequestParametersGET()
  285. {
  286. if (empty($_SERVER['QUERY_STRING'])) {
  287. return array();
  288. }
  289. $GET = UrlHelper::getArrayFromQueryString($_SERVER['QUERY_STRING']);
  290. return $GET;
  291. }
  292. /**
  293. * Returns the URL for the current requested report w/o any filter parameters.
  294. *
  295. * @param string $module The API module.
  296. * @param string $action The API action.
  297. * @param array $queryParams Query parameter overrides.
  298. * @return string
  299. */
  300. public static function getBaseReportUrl($module, $action, $queryParams = array())
  301. {
  302. $params = array_merge($queryParams, array('module' => $module, 'action' => $action));
  303. return Request::getCurrentUrlWithoutGenericFilters($params);
  304. }
  305. /**
  306. * Returns the current URL without generic filter query parameters.
  307. *
  308. * @param array $params Query parameter values to override in the new URL.
  309. * @return string
  310. */
  311. public static function getCurrentUrlWithoutGenericFilters($params)
  312. {
  313. // unset all filter query params so the related report will show up in its default state,
  314. // unless the filter param was in $queryParams
  315. $genericFiltersInfo = DataTableGenericFilter::getGenericFiltersInformation();
  316. foreach ($genericFiltersInfo as $filter) {
  317. foreach ($filter[1] as $queryParamName => $queryParamInfo) {
  318. if (!isset($params[$queryParamName])) {
  319. $params[$queryParamName] = null;
  320. }
  321. }
  322. }
  323. return Url::getCurrentQueryStringWithParametersModified($params);
  324. }
  325. /**
  326. * Returns whether the DataTable result will have to be expanded for the
  327. * current request before rendering.
  328. *
  329. * @return bool
  330. * @ignore
  331. */
  332. public static function shouldLoadExpanded()
  333. {
  334. // if filter_column_recursive & filter_pattern_recursive are supplied, and flat isn't supplied
  335. // we have to load all the child subtables.
  336. return Common::getRequestVar('filter_column_recursive', false) !== false
  337. && Common::getRequestVar('filter_pattern_recursive', false) !== false
  338. && !self::shouldLoadFlatten();
  339. }
  340. /**
  341. * @return bool
  342. */
  343. public static function shouldLoadFlatten()
  344. {
  345. return Common::getRequestVar('flat', false) == 1;
  346. }
  347. /**
  348. * Returns the segment query parameter from the original request, without modifications.
  349. *
  350. * @return array|bool
  351. */
  352. public static function getRawSegmentFromRequest()
  353. {
  354. // we need the URL encoded segment parameter, we fetch it from _SERVER['QUERY_STRING'] instead of default URL decoded _GET
  355. $segmentRaw = false;
  356. $segment = Common::getRequestVar('segment', '', 'string');
  357. if (!empty($segment)) {
  358. $request = Request::getRequestParametersGET();
  359. if (!empty($request['segment'])) {
  360. $segmentRaw = $request['segment'];
  361. }
  362. }
  363. return $segmentRaw;
  364. }
  365. }