| <?php |
|
|
| |
| |
| |
| |
| |
| |
|
|
| namespace Piwik\API; |
|
|
| use Exception; |
| use Piwik\Http\BadRequestException; |
| use Piwik\Common; |
| use Piwik\Container\StaticContainer; |
| use Piwik\Context; |
| use Piwik\Piwik; |
| use Piwik\Plugin\API; |
| use Piwik\Plugin\Manager; |
| use ReflectionClass; |
| use ReflectionMethod; |
|
|
| |
| |
| if (!class_exists('Piwik\API\NoDefaultValue', false)) { |
|
|
| |
| class NoDefaultValue |
| { |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| class Proxy |
| { |
| |
| protected $alreadyRegistered = array(); |
|
|
| protected $metadataArray = array(); |
| private $hideIgnoredFunctions = true; |
|
|
| |
| private $noDefaultValue; |
|
|
| public function __construct() |
| { |
| $this->noDefaultValue = new NoDefaultValue(); |
| } |
|
|
| public static function getInstance() |
| { |
| return StaticContainer::get(self::class); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public function getMetadata() |
| { |
| ksort($this->metadataArray); |
| return $this->metadataArray; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| public function registerClass($className) |
| { |
| if (isset($this->alreadyRegistered[$className])) { |
| return; |
| } |
|
|
| $this->includeApiFile($className); |
| $this->checkClassIsSingleton($className); |
|
|
| $rClass = new ReflectionClass($className); |
| if (!$this->shouldHideAPIMethod($rClass->getDocComment())) { |
| foreach ($rClass->getMethods() as $method) { |
| $this->loadMethodMetadata($className, $method); |
| } |
|
|
| $this->setDocumentation($rClass, $className); |
| $this->alreadyRegistered[$className] = true; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| private function setDocumentation($rClass, $className) |
| { |
| |
| $doc = $rClass->getDocComment(); |
| $doc = $this->removeDocblockAnnotationBlocks($doc, 'phpstan'); |
| $doc = str_replace(" * " . PHP_EOL, "<br>", $doc); |
|
|
| |
| if (substr_count($doc, '<br>') > 1) { |
| $firstLineBreak = strpos($doc, "<br>"); |
| $doc = "<div class='apiFirstLine'>" . substr($doc, 0, $firstLineBreak) . "</div>" . substr($doc, $firstLineBreak + strlen("<br>")); |
| } |
| $doc = preg_replace("/(@package)[a-z _A-Z]*/", "", $doc); |
| $doc = preg_replace("/(@method).*/", "", $doc); |
| $doc = str_replace(array("\t", "\n", "/**", "*/", " * ", " *", " ", "\t*", " * @package"), " ", $doc); |
|
|
| |
| $doc = preg_replace('/`(.*?)`/', '<code>$1</code>', $doc); |
| $this->metadataArray[$className]['__documentation'] = $doc; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| private function removeDocblockAnnotationBlocks($doc, $annotationPrefix) |
| { |
| if (!is_string($doc) || $doc === '') { |
| return $doc; |
| } |
|
|
| $lines = preg_split('/\R/', $doc); |
| $result = []; |
| $isSkipping = false; |
|
|
| foreach ($lines as $line) { |
| if (preg_match('/^\s*\*\s*@' . preg_quote($annotationPrefix, '/') . '\S*/', $line)) { |
| $isSkipping = true; |
| continue; |
| } |
|
|
| if ($isSkipping) { |
| |
| if (preg_match('/^\s*\*\s*@\S+/', $line)) { |
| $isSkipping = false; |
| } elseif (preg_match('/^\s*\*\/\s*$/', $line)) { |
| $isSkipping = false; |
| } else { |
| continue; |
| } |
| } |
|
|
| $result[] = $line; |
| } |
|
|
| return implode(PHP_EOL, $result); |
| } |
|
|
| |
| |
| |
| |
| public function getCountRegisteredClasses() |
| { |
| return count($this->alreadyRegistered); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| public function call($className, $methodName, $parametersRequest) |
| { |
| |
| return Context::executeWithQueryParameters($parametersRequest, function () use ($className, $methodName, $parametersRequest) { |
| $this->registerClass($className); |
|
|
| $request = new \Piwik\Request($parametersRequest); |
|
|
| |
| |
| |
| |
| $object = $className::getInstance(); |
|
|
| |
| $this->checkMethodExists($className, $methodName); |
|
|
| |
| $parameterNamesDefaultValuesAndTypes = $this->getParametersListWithTypes($className, $methodName); |
|
|
| |
| if ($object->usesAutoSanitizeInputParams() && !$this->usesUnsanitizedInputParams($className, $methodName)) { |
| $finalParameters = $this->getSanitizedRequestParametersArray($parameterNamesDefaultValuesAndTypes, $request->getParameters()); |
| } else { |
| $finalParameters = $this->getRequestParametersArray($parameterNamesDefaultValuesAndTypes, $request); |
| } |
|
|
| |
| $pluginName = $this->getModuleNameFromClassName($className); |
|
|
| $returnedValue = null; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| Piwik::postEvent('API.Request.dispatch', array(&$finalParameters, $pluginName, $methodName)); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| Piwik::postEvent(sprintf('API.%s.%s', $pluginName, $methodName), array(&$finalParameters)); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| Piwik::postEvent('API.Request.intercept', [&$returnedValue, $finalParameters, $pluginName, $methodName, $parametersRequest]); |
|
|
| $apiParametersInCorrectOrder = array(); |
|
|
| foreach ($parameterNamesDefaultValuesAndTypes as $name => $parameter) { |
| if (isset($finalParameters[$name]) || array_key_exists($name, $finalParameters)) { |
| $apiParametersInCorrectOrder[] = $finalParameters[$name]; |
| } |
| } |
|
|
| |
| if ($returnedValue === null) { |
| $returnedValue = call_user_func_array(array($object, $methodName), $apiParametersInCorrectOrder); |
| } |
|
|
| $endHookParams = array( |
| &$returnedValue, |
| array('className' => $className, |
| 'module' => $pluginName, |
| 'action' => $methodName, |
| 'parameters' => $finalParameters) |
| ); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| Piwik::postEvent(sprintf('API.%s.%s.end', $pluginName, $methodName), $endHookParams); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| Piwik::postEvent('API.Request.dispatch.end', $endHookParams); |
|
|
| return $returnedValue; |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| public function getParametersList($class, $name) |
| { |
| return array_combine( |
| array_keys($this->metadataArray[$class][$name]['parameters']), |
| array_column($this->metadataArray[$class][$name]['parameters'], 'default') |
| ); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| public function getParametersListWithTypes($class, $name) |
| { |
| return $this->metadataArray[$class][$name]['parameters']; |
| } |
|
|
| |
| |
| |
| public function isDeprecatedMethod($class, $methodName) |
| { |
| return $this->metadataArray[$class][$methodName]['isDeprecated'] ?? false; |
| } |
|
|
| |
| |
| |
| public function usesUnsanitizedInputParams($class, $methodName) |
| { |
| return $this->metadataArray[$class][$methodName]['unsanitizedInputParams'] ?? false; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public function getModuleNameFromClassName($className) |
| { |
| return str_replace(array('\\Piwik\\Plugins\\', '\\API'), '', $className); |
| } |
|
|
| public function isExistingApiAction($pluginName, $apiAction) |
| { |
| $namespacedApiClassName = "\\Piwik\\Plugins\\$pluginName\\API"; |
| $api = $namespacedApiClassName::getInstance(); |
|
|
| return method_exists($api, $apiAction); |
| } |
|
|
| public function buildApiActionName($pluginName, $apiAction) |
| { |
| return sprintf("%s.%s", $pluginName, $apiAction); |
| } |
|
|
| |
| |
| |
| |
| |
| public function setHideIgnoredFunctions($hideIgnoredFunctions) |
| { |
| $this->hideIgnoredFunctions = $hideIgnoredFunctions; |
|
|
| |
| $this->alreadyRegistered = array(); |
| $this->metadataArray = array(); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| private function getSanitizedRequestParametersArray($requiredParameters, $parametersRequest) |
| { |
| $finalParameters = []; |
| foreach ($requiredParameters as $name => $parameter) { |
| try { |
| $defaultValue = $parameter['default']; |
| $type = $parameter['type']; |
| $request = new \Piwik\Request($parametersRequest); |
|
|
| if (in_array($name, ['segment', 'password', 'passwordConfirmation']) && !empty($parametersRequest[$name])) { |
| |
| |
| |
| $requestValue = ($parametersRequest[$name]); |
| } elseif ($defaultValue instanceof NoDefaultValue) { |
| if ($type === 'bool') { |
| $requestValue = $request->getBoolParameter($name); |
| } else { |
| $requestValue = Common::getRequestVar($name, null, $type, $parametersRequest); |
| } |
| } else { |
| try { |
| if ($type === 'bool') { |
| $requestValue = $request->getBoolParameter($name, $defaultValue); |
| } else { |
| $requestValue = Common::getRequestVar($name, $defaultValue, $type, $parametersRequest); |
| } |
| } catch (Exception $e) { |
| |
| if (isset($parametersRequest[$name]) |
| && $parametersRequest[$name] === '' |
| && (empty($type) || $type === 'string') |
| ) { |
| $requestValue = ''; |
| } else { |
| $requestValue = $defaultValue; |
| } |
| } |
| } |
| } catch (Exception $e) { |
| throw new BadRequestException(Piwik::translate('General_PleaseSpecifyValue', [$name])); |
| } |
| $finalParameters[$name] = $requestValue; |
| } |
| return $finalParameters; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| private function getRequestParametersArray($requiredParameters, \Piwik\Request $request): array |
| { |
| $finalParameters = []; |
| foreach ($requiredParameters as $name => $parameter) { |
| try { |
| $defaultValue = $parameter['default']; |
| $type = $parameter['type'] ?? ''; |
| $requestValue = null; |
|
|
| switch (strtolower($type)) { |
| case 'bool': |
| $method = 'getBoolParameter'; |
| break; |
| case 'int': |
| $method = 'getIntegerParameter'; |
| break; |
| case 'string': |
| $method = 'getStringParameter'; |
| break; |
| case 'float': |
| $method = 'getFloatParameter'; |
| break; |
| case 'array': |
| $method = 'getArrayParameter'; |
| break; |
| default: |
| $method = 'getParameter'; |
| } |
|
|
| if ($defaultValue instanceof NoDefaultValue) { |
| $requestValue = $request->$method($name); |
| } elseif ($defaultValue === null) { |
| try { |
| $requestValue = $request->$method($name); |
| } catch (\InvalidArgumentException $e) { |
| $requestValue = null; |
| } |
| } else { |
| $requestValue = $request->$method($name, $defaultValue); |
| } |
| } catch (Exception $e) { |
| throw new BadRequestException(Piwik::translate('General_PleaseSpecifyValue', [$name])); |
| } |
| $finalParameters[$name] = $requestValue; |
| } |
| return $finalParameters; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| private function includeApiFile($fileName) |
| { |
| $module = self::getModuleNameFromClassName($fileName); |
| $path = Manager::getPluginDirectory($module) . '/API.php'; |
|
|
| if (is_readable($path)) { |
| require_once $path; |
| } else { |
| throw new Exception("API module $module not found."); |
| } |
| } |
|
|
| |
| |
| |
| |
| private function loadMethodMetadata($class, $method) |
| { |
| if (!$this->checkIfMethodIsAvailable($method)) { |
| return; |
| } |
| $name = $method->getName(); |
| $parameters = $method->getParameters(); |
| $docComment = $method->getDocComment(); |
|
|
| $aParameters = array(); |
| foreach ($parameters as $parameter) { |
| $nameVariable = $parameter->getName(); |
|
|
| $defaultValue = $this->noDefaultValue; |
| if ($parameter->isDefaultValueAvailable()) { |
| $defaultValue = $parameter->getDefaultValue(); |
| } |
|
|
| $type = $parameter->getType(); |
|
|
| |
| if ($type && $type->allowsNull() && $defaultValue === $this->noDefaultValue) { |
| $defaultValue = null; |
| } |
|
|
| $aParameters[$nameVariable] = [ |
| 'default' => $defaultValue, |
| 'type' => ($type && $type->isBuiltin()) ? $type->getName() : null, |
| 'allowsNull' => $type ? $type->allowsNull() : $defaultValue === null, |
| ]; |
| } |
| $this->metadataArray[$class][$name]['parameters'] = $aParameters; |
| $this->metadataArray[$class][$name]['numberOfRequiredParameters'] = $method->getNumberOfRequiredParameters(); |
| $this->metadataArray[$class][$name]['isDeprecated'] = false !== strstr($docComment, '@deprecated'); |
| $this->metadataArray[$class][$name]['unsanitizedInputParams'] = false !== strstr($docComment, '@unsanitized'); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| private function checkMethodExists($className, $methodName) |
| { |
| if (!$this->isMethodAvailable($className, $methodName)) { |
| throw new BadRequestException(Piwik::translate('General_ExceptionMethodNotFound', [$methodName, $className])); |
| } |
| } |
|
|
| |
| |
| |
| |
| public function shouldHideAPIMethod($docComment) |
| { |
| $hideLine = strstr($docComment, '@hide'); |
|
|
| if ($hideLine === false) { |
| return false; |
| } |
|
|
| $hideLine = trim($hideLine); |
| $hideLine .= ' '; |
|
|
| $token = trim(strtok($hideLine, " "), "\n"); |
|
|
| $hide = false; |
|
|
| if (!empty($token)) { |
| |
| |
| |
| |
| |
| |
| Piwik::postEvent(sprintf('API.DocumentationGenerator.%s', $token), array(&$hide)); |
| } |
|
|
| return $hide; |
| } |
|
|
| |
| |
| |
| |
| protected function checkIfMethodIsAvailable(ReflectionMethod $method) |
| { |
| if (!$method->isPublic() || $method->isConstructor() || $method->getName() === 'getInstance') { |
| return false; |
| } |
|
|
| if ($this->hideIgnoredFunctions && false !== strstr($method->getDocComment(), '@ignore')) { |
| return false; |
| } |
|
|
| if ($this->shouldHideAPIMethod($method->getDocComment())) { |
| return false; |
| } |
|
|
| return true; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| private function isMethodAvailable($className, $methodName) |
| { |
| return isset($this->metadataArray[$className][$methodName]); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| private function checkClassIsSingleton($className) |
| { |
| if (!method_exists($className, "getInstance")) { |
| throw new Exception("$className that provide an API must be Singleton and have a 'public static function getInstance()' method."); |
| } |
| } |
| } |
|
|