PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 4.0.2
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v4.0.2
5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / core / API / Proxy.php
matomo / app / core / API Last commit date
DataTableManipulator 5 years ago ApiRenderer.php 5 years ago CORSHandler.php 5 years ago DataTableGenericFilter.php 5 years ago DataTableManipulator.php 5 years ago DataTablePostProcessor.php 5 years ago DocumentationGenerator.php 5 years ago Inconsistencies.php 5 years ago NoDefaultValue.php 5 years ago Proxy.php 5 years ago Request.php 5 years ago ResponseBuilder.php 5 years ago
Proxy.php
582 lines
1 <?php
2 /**
3 * Matomo - free/libre analytics platform
4 *
5 * @link https://matomo.org
6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7 *
8 */
9
10 namespace Piwik\API;
11
12 use Exception;
13 use Piwik\Common;
14 use Piwik\Container\StaticContainer;
15 use Piwik\Context;
16 use Piwik\Piwik;
17 use Piwik\Plugin\Manager;
18 use ReflectionClass;
19 use ReflectionMethod;
20
21 // prevent upgrade error eg from Matomo 3.x to Matomo 4.x. Refs https://github.com/matomo-org/matomo/pull/16468
22 // the `false` is important otherwise it would fail and try to load the proxy.php file again.
23 if (!class_exists('Piwik\API\NoDefaultValue', false)) {
24 class NoDefaultValue
25 {
26 }
27 }
28
29 /**
30 * Proxy is a singleton that has the knowledge of every method available, their parameters
31 * and default values.
32 * Proxy receives all the API calls requests via call() and forwards them to the right
33 * object, with the parameters in the right order.
34 *
35 * It will also log the performance of API calls (time spent, parameter values, etc.) if logger available
36 */
37 class Proxy
38 {
39 // array of already registered plugins names
40 protected $alreadyRegistered = array();
41
42 protected $metadataArray = array();
43 private $hideIgnoredFunctions = true;
44
45 // when a parameter doesn't have a default value we use this
46 private $noDefaultValue;
47
48 public function __construct()
49 {
50 $this->noDefaultValue = new NoDefaultValue();
51 }
52
53 public static function getInstance()
54 {
55 return StaticContainer::get(self::class);
56 }
57
58 /**
59 * Returns array containing reflection meta data for all the loaded classes
60 * eg. number of parameters, method names, etc.
61 *
62 * @return array
63 */
64 public function getMetadata()
65 {
66 ksort($this->metadataArray);
67 return $this->metadataArray;
68 }
69
70 /**
71 * Registers the API information of a given module.
72 *
73 * The module to be registered must be
74 * - a singleton (providing a getInstance() method)
75 * - the API file must be located in plugins/ModuleName/API.php
76 * for example plugins/Referrers/API.php
77 *
78 * The method will introspect the methods, their parameters, etc.
79 *
80 * @param string $className ModuleName eg. "API"
81 */
82 public function registerClass($className)
83 {
84 if (isset($this->alreadyRegistered[$className])) {
85 return;
86 }
87 $this->includeApiFile($className);
88 $this->checkClassIsSingleton($className);
89
90 $rClass = new ReflectionClass($className);
91 if (!$this->shouldHideAPIMethod($rClass->getDocComment())) {
92 foreach ($rClass->getMethods() as $method) {
93 $this->loadMethodMetadata($className, $method);
94 }
95
96 $this->setDocumentation($rClass, $className);
97 $this->alreadyRegistered[$className] = true;
98 }
99 }
100
101 /**
102 * Will be displayed in the API page
103 *
104 * @param ReflectionClass $rClass Instance of ReflectionClass
105 * @param string $className Name of the class
106 */
107 private function setDocumentation($rClass, $className)
108 {
109 // Doc comment
110 $doc = $rClass->getDocComment();
111 $doc = str_replace(" * " . PHP_EOL, "<br>", $doc);
112
113 // boldify the first line only if there is more than one line, otherwise too much bold
114 if (substr_count($doc, '<br>') > 1) {
115 $firstLineBreak = strpos($doc, "<br>");
116 $doc = "<div class='apiFirstLine'>" . substr($doc, 0, $firstLineBreak) . "</div>" . substr($doc, $firstLineBreak + strlen("<br>"));
117 }
118 $doc = preg_replace("/(@package)[a-z _A-Z]*/", "", $doc);
119 $doc = preg_replace("/(@method).*/", "", $doc);
120 $doc = str_replace(array("\t", "\n", "/**", "*/", " * ", " *", " ", "\t*", " * @package"), " ", $doc);
121
122 // replace 'foo' and `bar` and "foobar" with code blocks... much magic
123 $doc = preg_replace('/`(.*?)`/', '<code>$1</code>', $doc);
124 $this->metadataArray[$className]['__documentation'] = $doc;
125 }
126
127 /**
128 * Returns number of classes already loaded
129 * @return int
130 */
131 public function getCountRegisteredClasses()
132 {
133 return count($this->alreadyRegistered);
134 }
135
136 /**
137 * Will execute $className->$methodName($parametersValues)
138 * If any error is detected (wrong number of parameters, method not found, class not found, etc.)
139 * it will throw an exception
140 *
141 * It also logs the API calls, with the parameters values, the returned value, the performance, etc.
142 * You can enable logging in config/global.ini.php (log_api_call)
143 *
144 * @param string $className The class name (eg. API)
145 * @param string $methodName The method name
146 * @param array $parametersRequest The parameters pairs (name=>value)
147 *
148 * @return mixed|null
149 * @throws Exception|\Piwik\NoAccessException
150 */
151 public function call($className, $methodName, $parametersRequest)
152 {
153 // Temporarily sets the Request array to this API call context
154 return Context::executeWithQueryParameters($parametersRequest, function () use ($className, $methodName, $parametersRequest) {
155 $returnedValue = null;
156
157 $this->registerClass($className);
158
159 // instantiate the object
160 $object = $className::getInstance();
161
162 // check method exists
163 $this->checkMethodExists($className, $methodName);
164
165 // get the list of parameters required by the method
166 $parameterNamesDefaultValues = $this->getParametersList($className, $methodName);
167
168 // load parameters in the right order, etc.
169 $finalParameters = $this->getRequestParametersArray($parameterNamesDefaultValues, $parametersRequest);
170
171 // allow plugins to manipulate the value
172 $pluginName = $this->getModuleNameFromClassName($className);
173
174 $returnedValue = null;
175
176 /**
177 * Triggered before an API request is dispatched.
178 *
179 * This event can be used to modify the arguments passed to one or more API methods.
180 *
181 * **Example**
182 *
183 * Piwik::addAction('API.Request.dispatch', function (&$parameters, $pluginName, $methodName) {
184 * if ($pluginName == 'Actions') {
185 * if ($methodName == 'getPageUrls') {
186 * // ... do something ...
187 * } else {
188 * // ... do something else ...
189 * }
190 * }
191 * });
192 *
193 * @param array &$finalParameters List of parameters that will be passed to the API method.
194 * @param string $pluginName The name of the plugin the API method belongs to.
195 * @param string $methodName The name of the API method that will be called.
196 */
197 Piwik::postEvent('API.Request.dispatch', array(&$finalParameters, $pluginName, $methodName));
198
199 /**
200 * Triggered before an API request is dispatched.
201 *
202 * This event exists for convenience and is triggered directly after the {@hook API.Request.dispatch}
203 * event is triggered. It can be used to modify the arguments passed to a **single** API method.
204 *
205 * _Note: This is can be accomplished with the {@hook API.Request.dispatch} event as well, however
206 * event handlers for that event will have to do more work._
207 *
208 * **Example**
209 *
210 * Piwik::addAction('API.Actions.getPageUrls', function (&$parameters) {
211 * // force use of a single website. for some reason.
212 * $parameters['idSite'] = 1;
213 * });
214 *
215 * @param array &$finalParameters List of parameters that will be passed to the API method.
216 */
217 Piwik::postEvent(sprintf('API.%s.%s', $pluginName, $methodName), array(&$finalParameters));
218
219 /**
220 * Triggered before an API request is dispatched.
221 *
222 * Use this event to intercept an API request and execute your own code instead. If you set
223 * `$returnedValue` in a handler for this event, the original API method will not be executed,
224 * and the result will be what you set in the event handler.
225 *
226 * @param mixed &$returnedValue Set this to set the result and preempt normal API invocation.
227 * @param array &$finalParameters List of parameters that will be passed to the API method.
228 * @param string $pluginName The name of the plugin the API method belongs to.
229 * @param string $methodName The name of the API method that will be called.
230 * @param array $parametersRequest The query parameters for this request.
231 */
232 Piwik::postEvent('API.Request.intercept', [&$returnedValue, $finalParameters, $pluginName, $methodName, $parametersRequest]);
233
234 $apiParametersInCorrectOrder = array();
235
236 foreach ($parameterNamesDefaultValues as $name => $defaultValue) {
237 if (isset($finalParameters[$name]) || array_key_exists($name, $finalParameters)) {
238 $apiParametersInCorrectOrder[] = $finalParameters[$name];
239 }
240 }
241
242 // call the method if a hook hasn't already set an output variable
243 if ($returnedValue === null) {
244 $returnedValue = call_user_func_array(array($object, $methodName), $apiParametersInCorrectOrder);
245 }
246
247 $endHookParams = array(
248 &$returnedValue,
249 array('className' => $className,
250 'module' => $pluginName,
251 'action' => $methodName,
252 'parameters' => $finalParameters)
253 );
254
255 /**
256 * Triggered directly after an API request is dispatched.
257 *
258 * This event exists for convenience and is triggered immediately before the
259 * {@hook API.Request.dispatch.end} event. It can be used to modify the output of a **single**
260 * API method.
261 *
262 * _Note: This can be accomplished with the {@hook API.Request.dispatch.end} event as well,
263 * however event handlers for that event will have to do more work._
264 *
265 * **Example**
266 *
267 * // append (0 hits) to the end of row labels whose row has 0 hits
268 * Piwik::addAction('API.Actions.getPageUrls', function (&$returnValue, $info)) {
269 * $returnValue->filter('ColumnCallbackReplace', 'label', function ($label, $hits) {
270 * if ($hits === 0) {
271 * return $label . " (0 hits)";
272 * } else {
273 * return $label;
274 * }
275 * }, null, array('nb_hits'));
276 * }
277 *
278 * @param mixed &$returnedValue The API method's return value. Can be an object, such as a
279 * {@link Piwik\DataTable DataTable} instance.
280 * could be a {@link Piwik\DataTable DataTable}.
281 * @param array $extraInfo An array holding information regarding the API request. Will
282 * contain the following data:
283 *
284 * - **className**: The namespace-d class name of the API instance
285 * that's being called.
286 * - **module**: The name of the plugin the API request was
287 * dispatched to.
288 * - **action**: The name of the API method that was executed.
289 * - **parameters**: The array of parameters passed to the API
290 * method.
291 */
292 Piwik::postEvent(sprintf('API.%s.%s.end', $pluginName, $methodName), $endHookParams);
293
294 /**
295 * Triggered directly after an API request is dispatched.
296 *
297 * This event can be used to modify the output of any API method.
298 *
299 * **Example**
300 *
301 * // append (0 hits) to the end of row labels whose row has 0 hits for any report that has the 'nb_hits' metric
302 * Piwik::addAction('API.Actions.getPageUrls.end', function (&$returnValue, $info)) {
303 * // don't process non-DataTable reports and reports that don't have the nb_hits column
304 * if (!($returnValue instanceof DataTableInterface)
305 * || in_array('nb_hits', $returnValue->getColumns())
306 * ) {
307 * return;
308 * }
309 *
310 * $returnValue->filter('ColumnCallbackReplace', 'label', function ($label, $hits) {
311 * if ($hits === 0) {
312 * return $label . " (0 hits)";
313 * } else {
314 * return $label;
315 * }
316 * }, null, array('nb_hits'));
317 * }
318 *
319 * @param mixed &$returnedValue The API method's return value. Can be an object, such as a
320 * {@link Piwik\DataTable DataTable} instance.
321 * @param array $extraInfo An array holding information regarding the API request. Will
322 * contain the following data:
323 *
324 * - **className**: The namespace-d class name of the API instance
325 * that's being called.
326 * - **module**: The name of the plugin the API request was
327 * dispatched to.
328 * - **action**: The name of the API method that was executed.
329 * - **parameters**: The array of parameters passed to the API
330 * method.
331 */
332 Piwik::postEvent('API.Request.dispatch.end', $endHookParams);
333
334 return $returnedValue;
335 });
336 }
337
338 /**
339 * Returns the parameters names and default values for the method $name
340 * of the class $class
341 *
342 * @param string $class The class name
343 * @param string $name The method name
344 * @return array Format array(
345 * 'testParameter' => null, // no default value
346 * 'life' => 42, // default value = 42
347 * 'date' => 'yesterday',
348 * );
349 */
350 public function getParametersList($class, $name)
351 {
352 return $this->metadataArray[$class][$name]['parameters'];
353 }
354
355 /**
356 * Check if given method name is deprecated or not.
357 */
358 public function isDeprecatedMethod($class, $methodName)
359 {
360 return $this->metadataArray[$class][$methodName]['isDeprecated'];
361 }
362
363 /**
364 * Returns the 'moduleName' part of '\\Piwik\\Plugins\\moduleName\\API'
365 *
366 * @param string $className "API"
367 * @return string "Referrers"
368 */
369 public function getModuleNameFromClassName($className)
370 {
371 return str_replace(array('\\Piwik\\Plugins\\', '\\API'), '', $className);
372 }
373
374 public function isExistingApiAction($pluginName, $apiAction)
375 {
376 $namespacedApiClassName = "\\Piwik\\Plugins\\$pluginName\\API";
377 $api = $namespacedApiClassName::getInstance();
378
379 return method_exists($api, $apiAction);
380 }
381
382 public function buildApiActionName($pluginName, $apiAction)
383 {
384 return sprintf("%s.%s", $pluginName, $apiAction);
385 }
386
387 /**
388 * Sets whether to hide '@ignore'd functions from method metadata or not.
389 *
390 * @param bool $hideIgnoredFunctions
391 */
392 public function setHideIgnoredFunctions($hideIgnoredFunctions)
393 {
394 $this->hideIgnoredFunctions = $hideIgnoredFunctions;
395
396 // make sure metadata gets reloaded
397 $this->alreadyRegistered = array();
398 $this->metadataArray = array();
399 }
400
401 /**
402 * Returns an array containing the values of the parameters to pass to the method to call
403 *
404 * @param array $requiredParameters array of (parameter name, default value)
405 * @param array $parametersRequest
406 * @throws Exception
407 * @return array values to pass to the function call
408 */
409 private function getRequestParametersArray($requiredParameters, $parametersRequest)
410 {
411 $finalParameters = array();
412 foreach ($requiredParameters as $name => $defaultValue) {
413 try {
414 if ($defaultValue instanceof NoDefaultValue) {
415 $requestValue = Common::getRequestVar($name, null, null, $parametersRequest);
416 } else {
417 try {
418 if ($name == 'segment' && !empty($parametersRequest['segment'])) {
419 // segment parameter is an exception: we do not want to sanitize user input or it would break the segment encoding
420 $requestValue = ($parametersRequest['segment']);
421 } else {
422 $requestValue = Common::getRequestVar($name, $defaultValue, null, $parametersRequest);
423 }
424 } catch (Exception $e) {
425 // Special case: empty parameter in the URL, should return the empty string
426 if (isset($parametersRequest[$name])
427 && $parametersRequest[$name] === ''
428 ) {
429 $requestValue = '';
430 } else {
431 $requestValue = $defaultValue;
432 }
433 }
434 }
435 } catch (Exception $e) {
436 throw new Exception(Piwik::translate('General_PleaseSpecifyValue', array($name)));
437 }
438 $finalParameters[$name] = $requestValue;
439 }
440 return $finalParameters;
441 }
442
443 /**
444 * Includes the class API by looking up plugins/xxx/API.php
445 *
446 * @param string $fileName api class name eg. "API"
447 * @throws Exception
448 */
449 private function includeApiFile($fileName)
450 {
451 $module = self::getModuleNameFromClassName($fileName);
452 $path = Manager::getPluginDirectory($module) . '/API.php';
453
454 if (is_readable($path)) {
455 require_once $path; // prefixed by PIWIK_INCLUDE_PATH
456 } else {
457 throw new Exception("API module $module not found.");
458 }
459 }
460
461 /**
462 * @param string $class name of a class
463 * @param ReflectionMethod $method instance of ReflectionMethod
464 */
465 private function loadMethodMetadata($class, $method)
466 {
467 if (!$this->checkIfMethodIsAvailable($method)) {
468 return;
469 }
470 $name = $method->getName();
471 $parameters = $method->getParameters();
472 $docComment = $method->getDocComment();
473
474 $aParameters = array();
475 foreach ($parameters as $parameter) {
476 $nameVariable = $parameter->getName();
477
478 $defaultValue = $this->noDefaultValue;
479 if ($parameter->isDefaultValueAvailable()) {
480 $defaultValue = $parameter->getDefaultValue();
481 }
482
483 $aParameters[$nameVariable] = $defaultValue;
484 }
485 $this->metadataArray[$class][$name]['parameters'] = $aParameters;
486 $this->metadataArray[$class][$name]['numberOfRequiredParameters'] = $method->getNumberOfRequiredParameters();
487 $this->metadataArray[$class][$name]['isDeprecated'] = false !== strstr($docComment, '@deprecated');
488 }
489
490 /**
491 * Checks that the method exists in the class
492 *
493 * @param string $className The class name
494 * @param string $methodName The method name
495 * @throws Exception If the method is not found
496 */
497 private function checkMethodExists($className, $methodName)
498 {
499 if (!$this->isMethodAvailable($className, $methodName)) {
500 throw new Exception(Piwik::translate('General_ExceptionMethodNotFound', array($methodName, $className)));
501 }
502 }
503
504 /**
505 * @param $docComment
506 * @return bool
507 */
508 public function shouldHideAPIMethod($docComment)
509 {
510 $hideLine = strstr($docComment, '@hide');
511
512 if ($hideLine === false) {
513 return false;
514 }
515
516 $hideLine = trim($hideLine);
517 $hideLine .= ' ';
518
519 $token = trim(strtok($hideLine, " "), "\n");
520
521 $hide = false;
522
523 if (!empty($token)) {
524 /**
525 * This event exists for checking whether a Plugin API class or a Plugin API method tagged
526 * with a `@hideXYZ` should be hidden in the API listing.
527 *
528 * @param bool &$hide whether to hide APIs tagged with $token should be displayed.
529 */
530 Piwik::postEvent(sprintf('API.DocumentationGenerator.%s', $token), array(&$hide));
531 }
532
533 return $hide;
534 }
535
536 /**
537 * @param ReflectionMethod $method
538 * @return bool
539 */
540 protected function checkIfMethodIsAvailable(ReflectionMethod $method)
541 {
542 if (!$method->isPublic() || $method->isConstructor() || $method->getName() === 'getInstance') {
543 return false;
544 }
545
546 if ($this->hideIgnoredFunctions && false !== strstr($method->getDocComment(), '@ignore')) {
547 return false;
548 }
549
550 if ($this->shouldHideAPIMethod($method->getDocComment())) {
551 return false;
552 }
553
554 return true;
555 }
556
557 /**
558 * Returns true if the method is found in the API of the given class name.
559 *
560 * @param string $className The class name
561 * @param string $methodName The method name
562 * @return bool
563 */
564 private function isMethodAvailable($className, $methodName)
565 {
566 return isset($this->metadataArray[$className][$methodName]);
567 }
568
569 /**
570 * Checks that the class is a Singleton (presence of the getInstance() method)
571 *
572 * @param string $className The class name
573 * @throws Exception If the class is not a Singleton
574 */
575 private function checkClassIsSingleton($className)
576 {
577 if (!method_exists($className, "getInstance")) {
578 throw new Exception("$className that provide an API must be Singleton and have a 'public static function getInstance()' method.");
579 }
580 }
581 }
582