PageRenderTime 84ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/core/API/Proxy.php

https://github.com/CodeYellowBV/piwik
PHP | 514 lines | 276 code | 31 blank | 207 comment | 2 complexity | f954a02f09fe057a1a396c057b63b1a1 MD5 | raw file
Possible License(s): LGPL-3.0, JSON, MIT, GPL-3.0, LGPL-2.1, GPL-2.0, AGPL-1.0, BSD-2-Clause, BSD-3-Clause
  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\Common;
  12. use Piwik\Piwik;
  13. use Piwik\Singleton;
  14. use ReflectionClass;
  15. use ReflectionMethod;
  16. /**
  17. * Proxy is a singleton that has the knowledge of every method available, their parameters
  18. * and default values.
  19. * Proxy receives all the API calls requests via call() and forwards them to the right
  20. * object, with the parameters in the right order.
  21. *
  22. * It will also log the performance of API calls (time spent, parameter values, etc.) if logger available
  23. *
  24. * @method static \Piwik\API\Proxy getInstance()
  25. */
  26. class Proxy extends Singleton
  27. {
  28. // array of already registered plugins names
  29. protected $alreadyRegistered = array();
  30. private $metadataArray = array();
  31. private $hideIgnoredFunctions = true;
  32. // when a parameter doesn't have a default value we use this
  33. private $noDefaultValue;
  34. /**
  35. * protected constructor
  36. */
  37. protected function __construct()
  38. {
  39. $this->noDefaultValue = new NoDefaultValue();
  40. }
  41. /**
  42. * Returns array containing reflection meta data for all the loaded classes
  43. * eg. number of parameters, method names, etc.
  44. *
  45. * @return array
  46. */
  47. public function getMetadata()
  48. {
  49. ksort($this->metadataArray);
  50. return $this->metadataArray;
  51. }
  52. /**
  53. * Registers the API information of a given module.
  54. *
  55. * The module to be registered must be
  56. * - a singleton (providing a getInstance() method)
  57. * - the API file must be located in plugins/ModuleName/API.php
  58. * for example plugins/Referrers/API.php
  59. *
  60. * The method will introspect the methods, their parameters, etc.
  61. *
  62. * @param string $className ModuleName eg. "API"
  63. */
  64. public function registerClass($className)
  65. {
  66. if (isset($this->alreadyRegistered[$className])) {
  67. return;
  68. }
  69. $this->includeApiFile($className);
  70. $this->checkClassIsSingleton($className);
  71. $rClass = new ReflectionClass($className);
  72. foreach ($rClass->getMethods() as $method) {
  73. $this->loadMethodMetadata($className, $method);
  74. }
  75. $this->setDocumentation($rClass, $className);
  76. $this->alreadyRegistered[$className] = true;
  77. }
  78. /**
  79. * Will be displayed in the API page
  80. *
  81. * @param ReflectionClass $rClass Instance of ReflectionClass
  82. * @param string $className Name of the class
  83. */
  84. private function setDocumentation($rClass, $className)
  85. {
  86. // Doc comment
  87. $doc = $rClass->getDocComment();
  88. $doc = str_replace(" * " . PHP_EOL, "<br>", $doc);
  89. // boldify the first line only if there is more than one line, otherwise too much bold
  90. if (substr_count($doc, '<br>') > 1) {
  91. $firstLineBreak = strpos($doc, "<br>");
  92. $doc = "<div class='apiFirstLine'>" . substr($doc, 0, $firstLineBreak) . "</div>" . substr($doc, $firstLineBreak + strlen("<br>"));
  93. }
  94. $doc = preg_replace("/(@package)[a-z _A-Z]*/", "", $doc);
  95. $doc = preg_replace("/(@method).*/", "", $doc);
  96. $doc = str_replace(array("\t", "\n", "/**", "*/", " * ", " *", " ", "\t*", " * @package"), " ", $doc);
  97. $this->metadataArray[$className]['__documentation'] = $doc;
  98. }
  99. /**
  100. * Returns number of classes already loaded
  101. * @return int
  102. */
  103. public function getCountRegisteredClasses()
  104. {
  105. return count($this->alreadyRegistered);
  106. }
  107. /**
  108. * Will execute $className->$methodName($parametersValues)
  109. * If any error is detected (wrong number of parameters, method not found, class not found, etc.)
  110. * it will throw an exception
  111. *
  112. * It also logs the API calls, with the parameters values, the returned value, the performance, etc.
  113. * You can enable logging in config/global.ini.php (log_api_call)
  114. *
  115. * @param string $className The class name (eg. API)
  116. * @param string $methodName The method name
  117. * @param array $parametersRequest The parameters pairs (name=>value)
  118. *
  119. * @return mixed|null
  120. * @throws Exception|\Piwik\NoAccessException
  121. */
  122. public function call($className, $methodName, $parametersRequest)
  123. {
  124. $returnedValue = null;
  125. // Temporarily sets the Request array to this API call context
  126. $saveGET = $_GET;
  127. $saveQUERY_STRING = @$_SERVER['QUERY_STRING'];
  128. foreach ($parametersRequest as $param => $value) {
  129. $_GET[$param] = $value;
  130. }
  131. try {
  132. $this->registerClass($className);
  133. // instanciate the object
  134. $object = $className::getInstance();
  135. // check method exists
  136. $this->checkMethodExists($className, $methodName);
  137. // get the list of parameters required by the method
  138. $parameterNamesDefaultValues = $this->getParametersList($className, $methodName);
  139. // load parameters in the right order, etc.
  140. $finalParameters = $this->getRequestParametersArray($parameterNamesDefaultValues, $parametersRequest);
  141. // allow plugins to manipulate the value
  142. $pluginName = $this->getModuleNameFromClassName($className);
  143. /**
  144. * Triggered before an API request is dispatched.
  145. *
  146. * This event can be used to modify the arguments passed to one or more API methods.
  147. *
  148. * **Example**
  149. *
  150. * Piwik::addAction('API.Request.dispatch', function (&$parameters, $pluginName, $methodName) {
  151. * if ($pluginName == 'Actions') {
  152. * if ($methodName == 'getPageUrls') {
  153. * // ... do something ...
  154. * } else {
  155. * // ... do something else ...
  156. * }
  157. * }
  158. * });
  159. *
  160. * @param array &$finalParameters List of parameters that will be passed to the API method.
  161. * @param string $pluginName The name of the plugin the API method belongs to.
  162. * @param string $methodName The name of the API method that will be called.
  163. */
  164. Piwik::postEvent('API.Request.dispatch', array(&$finalParameters, $pluginName, $methodName));
  165. /**
  166. * Triggered before an API request is dispatched.
  167. *
  168. * This event exists for convenience and is triggered directly after the {@hook API.Request.dispatch}
  169. * event is triggered. It can be used to modify the arguments passed to a **single** API method.
  170. *
  171. * _Note: This is can be accomplished with the {@hook API.Request.dispatch} event as well, however
  172. * event handlers for that event will have to do more work._
  173. *
  174. * **Example**
  175. *
  176. * Piwik::addAction('API.Actions.getPageUrls', function (&$parameters) {
  177. * // force use of a single website. for some reason.
  178. * $parameters['idSite'] = 1;
  179. * });
  180. *
  181. * @param array &$finalParameters List of parameters that will be passed to the API method.
  182. */
  183. Piwik::postEvent(sprintf('API.%s.%s', $pluginName, $methodName), array(&$finalParameters));
  184. // call the method
  185. $returnedValue = call_user_func_array(array($object, $methodName), $finalParameters);
  186. $endHookParams = array(
  187. &$returnedValue,
  188. array('className' => $className,
  189. 'module' => $pluginName,
  190. 'action' => $methodName,
  191. 'parameters' => $finalParameters)
  192. );
  193. /**
  194. * Triggered directly after an API request is dispatched.
  195. *
  196. * This event exists for convenience and is triggered immediately before the
  197. * {@hook API.Request.dispatch.end} event. It can be used to modify the output of a **single**
  198. * API method.
  199. *
  200. * _Note: This can be accomplished with the {@hook API.Request.dispatch.end} event as well,
  201. * however event handlers for that event will have to do more work._
  202. *
  203. * **Example**
  204. *
  205. * // append (0 hits) to the end of row labels whose row has 0 hits
  206. * Piwik::addAction('API.Actions.getPageUrls', function (&$returnValue, $info)) {
  207. * $returnValue->filter('ColumnCallbackReplace', 'label', function ($label, $hits) {
  208. * if ($hits === 0) {
  209. * return $label . " (0 hits)";
  210. * } else {
  211. * return $label;
  212. * }
  213. * }, null, array('nb_hits'));
  214. * }
  215. *
  216. * @param mixed &$returnedValue The API method's return value. Can be an object, such as a
  217. * {@link Piwik\DataTable DataTable} instance.
  218. * could be a {@link Piwik\DataTable DataTable}.
  219. * @param array $extraInfo An array holding information regarding the API request. Will
  220. * contain the following data:
  221. *
  222. * - **className**: The namespace-d class name of the API instance
  223. * that's being called.
  224. * - **module**: The name of the plugin the API request was
  225. * dispatched to.
  226. * - **action**: The name of the API method that was executed.
  227. * - **parameters**: The array of parameters passed to the API
  228. * method.
  229. */
  230. Piwik::postEvent(sprintf('API.%s.%s.end', $pluginName, $methodName), $endHookParams);
  231. /**
  232. * Triggered directly after an API request is dispatched.
  233. *
  234. * This event can be used to modify the output of any API method.
  235. *
  236. * **Example**
  237. *
  238. * // append (0 hits) to the end of row labels whose row has 0 hits for any report that has the 'nb_hits' metric
  239. * Piwik::addAction('API.Actions.getPageUrls', function (&$returnValue, $info)) {
  240. * // don't process non-DataTable reports and reports that don't have the nb_hits column
  241. * if (!($returnValue instanceof DataTableInterface)
  242. * || in_array('nb_hits', $returnValue->getColumns())
  243. * ) {
  244. * return;
  245. * }
  246. *
  247. * $returnValue->filter('ColumnCallbackReplace', 'label', function ($label, $hits) {
  248. * if ($hits === 0) {
  249. * return $label . " (0 hits)";
  250. * } else {
  251. * return $label;
  252. * }
  253. * }, null, array('nb_hits'));
  254. * }
  255. *
  256. * @param mixed &$returnedValue The API method's return value. Can be an object, such as a
  257. * {@link Piwik\DataTable DataTable} instance.
  258. * @param array $extraInfo An array holding information regarding the API request. Will
  259. * contain the following data:
  260. *
  261. * - **className**: The namespace-d class name of the API instance
  262. * that's being called.
  263. * - **module**: The name of the plugin the API request was
  264. * dispatched to.
  265. * - **action**: The name of the API method that was executed.
  266. * - **parameters**: The array of parameters passed to the API
  267. * method.
  268. */
  269. Piwik::postEvent('API.Request.dispatch.end', $endHookParams);
  270. // Restore the request
  271. $_GET = $saveGET;
  272. $_SERVER['QUERY_STRING'] = $saveQUERY_STRING;
  273. } catch (Exception $e) {
  274. $_GET = $saveGET;
  275. throw $e;
  276. }
  277. return $returnedValue;
  278. }
  279. /**
  280. * Returns the parameters names and default values for the method $name
  281. * of the class $class
  282. *
  283. * @param string $class The class name
  284. * @param string $name The method name
  285. * @return array Format array(
  286. * 'testParameter' => null, // no default value
  287. * 'life' => 42, // default value = 42
  288. * 'date' => 'yesterday',
  289. * );
  290. */
  291. public function getParametersList($class, $name)
  292. {
  293. return $this->metadataArray[$class][$name]['parameters'];
  294. }
  295. /**
  296. * Returns the 'moduleName' part of '\\Piwik\\Plugins\\moduleName\\API'
  297. *
  298. * @param string $className "API"
  299. * @return string "Referrers"
  300. */
  301. public function getModuleNameFromClassName($className)
  302. {
  303. return str_replace(array('\\Piwik\\Plugins\\', '\\API'), '', $className);
  304. }
  305. public function isExistingApiAction($pluginName, $apiAction)
  306. {
  307. $namespacedApiClassName = "\\Piwik\\Plugins\\$pluginName\\API";
  308. $api = $namespacedApiClassName::getInstance();
  309. return method_exists($api, $apiAction);
  310. }
  311. public function buildApiActionName($pluginName, $apiAction)
  312. {
  313. return sprintf("%s.%s", $pluginName, $apiAction);
  314. }
  315. /**
  316. * Sets whether to hide '@ignore'd functions from method metadata or not.
  317. *
  318. * @param bool $hideIgnoredFunctions
  319. */
  320. public function setHideIgnoredFunctions($hideIgnoredFunctions)
  321. {
  322. $this->hideIgnoredFunctions = $hideIgnoredFunctions;
  323. // make sure metadata gets reloaded
  324. $this->alreadyRegistered = array();
  325. $this->metadataArray = array();
  326. }
  327. /**
  328. * Returns an array containing the values of the parameters to pass to the method to call
  329. *
  330. * @param array $requiredParameters array of (parameter name, default value)
  331. * @param array $parametersRequest
  332. * @throws Exception
  333. * @return array values to pass to the function call
  334. */
  335. private function getRequestParametersArray($requiredParameters, $parametersRequest)
  336. {
  337. $finalParameters = array();
  338. foreach ($requiredParameters as $name => $defaultValue) {
  339. try {
  340. if ($defaultValue instanceof NoDefaultValue) {
  341. $requestValue = Common::getRequestVar($name, null, null, $parametersRequest);
  342. } else {
  343. try {
  344. if ($name == 'segment' && !empty($parametersRequest['segment'])) {
  345. // segment parameter is an exception: we do not want to sanitize user input or it would break the segment encoding
  346. $requestValue = ($parametersRequest['segment']);
  347. } else {
  348. $requestValue = Common::getRequestVar($name, $defaultValue, null, $parametersRequest);
  349. }
  350. } catch (Exception $e) {
  351. // Special case: empty parameter in the URL, should return the empty string
  352. if (isset($parametersRequest[$name])
  353. && $parametersRequest[$name] === ''
  354. ) {
  355. $requestValue = '';
  356. } else {
  357. $requestValue = $defaultValue;
  358. }
  359. }
  360. }
  361. } catch (Exception $e) {
  362. throw new Exception(Piwik::translate('General_PleaseSpecifyValue', array($name)));
  363. }
  364. $finalParameters[] = $requestValue;
  365. }
  366. return $finalParameters;
  367. }
  368. /**
  369. * Includes the class API by looking up plugins/UserSettings/API.php
  370. *
  371. * @param string $fileName api class name eg. "API"
  372. * @throws Exception
  373. */
  374. private function includeApiFile($fileName)
  375. {
  376. $module = self::getModuleNameFromClassName($fileName);
  377. $path = PIWIK_INCLUDE_PATH . '/plugins/' . $module . '/API.php';
  378. if (is_readable($path)) {
  379. require_once $path; // prefixed by PIWIK_INCLUDE_PATH
  380. } else {
  381. throw new Exception("API module $module not found.");
  382. }
  383. }
  384. /**
  385. * @param string $class name of a class
  386. * @param ReflectionMethod $method instance of ReflectionMethod
  387. */
  388. private function loadMethodMetadata($class, $method)
  389. {
  390. if ($method->isPublic()
  391. && !$method->isConstructor()
  392. && $method->getName() != 'getInstance'
  393. && false === strstr($method->getDocComment(), '@deprecated')
  394. && (!$this->hideIgnoredFunctions || false === strstr($method->getDocComment(), '@ignore'))
  395. ) {
  396. $name = $method->getName();
  397. $parameters = $method->getParameters();
  398. $aParameters = array();
  399. foreach ($parameters as $parameter) {
  400. $nameVariable = $parameter->getName();
  401. $defaultValue = $this->noDefaultValue;
  402. if ($parameter->isDefaultValueAvailable()) {
  403. $defaultValue = $parameter->getDefaultValue();
  404. }
  405. $aParameters[$nameVariable] = $defaultValue;
  406. }
  407. $this->metadataArray[$class][$name]['parameters'] = $aParameters;
  408. $this->metadataArray[$class][$name]['numberOfRequiredParameters'] = $method->getNumberOfRequiredParameters();
  409. }
  410. }
  411. /**
  412. * Checks that the method exists in the class
  413. *
  414. * @param string $className The class name
  415. * @param string $methodName The method name
  416. * @throws Exception If the method is not found
  417. */
  418. private function checkMethodExists($className, $methodName)
  419. {
  420. if (!$this->isMethodAvailable($className, $methodName)) {
  421. throw new Exception(Piwik::translate('General_ExceptionMethodNotFound', array($methodName, $className)));
  422. }
  423. }
  424. /**
  425. * Returns the number of required parameters (parameters without default values).
  426. *
  427. * @param string $class The class name
  428. * @param string $name The method name
  429. * @return int The number of required parameters
  430. */
  431. private function getNumberOfRequiredParameters($class, $name)
  432. {
  433. return $this->metadataArray[$class][$name]['numberOfRequiredParameters'];
  434. }
  435. /**
  436. * Returns true if the method is found in the API of the given class name.
  437. *
  438. * @param string $className The class name
  439. * @param string $methodName The method name
  440. * @return bool
  441. */
  442. private function isMethodAvailable($className, $methodName)
  443. {
  444. return isset($this->metadataArray[$className][$methodName]);
  445. }
  446. /**
  447. * Checks that the class is a Singleton (presence of the getInstance() method)
  448. *
  449. * @param string $className The class name
  450. * @throws Exception If the class is not a Singleton
  451. */
  452. private function checkClassIsSingleton($className)
  453. {
  454. if (!method_exists($className, "getInstance")) {
  455. throw new Exception("$className that provide an API must be Singleton and have a 'static public function getInstance()' method.");
  456. }
  457. }
  458. }
  459. /**
  460. * To differentiate between "no value" and default value of null
  461. *
  462. */
  463. class NoDefaultValue
  464. {
  465. }