| <?php |
|
|
| |
| |
| |
| |
| |
| |
|
|
| namespace Piwik\Plugins\CoreConsole\Commands; |
|
|
| use Piwik\Common; |
| use Piwik\Development; |
| use Piwik\Filesystem; |
| use Piwik\Plugin\ConsoleCommand; |
| use Piwik\Plugin\Dependency; |
| use Piwik\Plugin\Manager; |
| use Piwik\Version; |
| use Piwik\SettingsPiwik; |
| use Piwik\Exception\NotGitInstalledException; |
|
|
| abstract class GeneratePluginBase extends ConsoleCommand |
| { |
| protected function doInitialize(): void |
| { |
| $this->throwErrorIfNotGitInstalled(); |
| } |
|
|
| public function isEnabled() |
| { |
| return Development::isEnabled(); |
| } |
|
|
| public function getPluginPath($pluginName) |
| { |
| return Manager::getPluginDirectory($pluginName); |
| } |
|
|
| private function createFolderWithinPluginIfNotExists($pluginNameOrCore, $folder) |
| { |
| if ($pluginNameOrCore === 'core') { |
| $pluginPath = $this->getPathToCore(); |
| } else { |
| $pluginPath = $this->getPluginPath($pluginNameOrCore); |
| } |
|
|
| if (!file_exists($pluginPath . $folder)) { |
| Filesystem::mkdir($pluginPath . $folder); |
| } |
| } |
|
|
| protected function createFileWithinPluginIfNotExists($pluginNameOrCore, $fileName, $content) |
| { |
| if ($pluginNameOrCore === 'core') { |
| $pluginPath = $this->getPathToCore(); |
| } else { |
| $pluginPath = $this->getPluginPath($pluginNameOrCore); |
| } |
|
|
| if (!file_exists($pluginPath . $fileName)) { |
| file_put_contents($pluginPath . $fileName, $content); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| protected function makeTranslationIfPossible($pluginName, $translatedText, $translationKey = '') |
| { |
| $defaultLang = array($pluginName => array()); |
|
|
| $this->createFolderWithinPluginIfNotExists($pluginName, '/lang'); |
| $this->createFileWithinPluginIfNotExists($pluginName, '/lang/en.json', $this->toJson($defaultLang)); |
|
|
| $langJsonPath = $this->getPluginPath($pluginName) . '/lang/en.json'; |
| $translations = file_get_contents($langJsonPath); |
| $translations = json_decode($translations, true); |
|
|
| if (empty($translations[$pluginName])) { |
| $translations[$pluginName] = array(); |
| } |
|
|
| if (!empty($translationKey)) { |
| $key = $translationKey; |
| } else { |
| $key = $this->buildTranslationKey($translatedText); |
| } |
|
|
| if (array_key_exists($key, $translations[$pluginName])) { |
| |
| if ($translations[$pluginName][$key] === $translatedText) { |
| return $pluginName . '_' . $key; |
| } |
|
|
| return $translatedText; |
| } |
|
|
| $translations[$pluginName][$key] = $this->removeNonJsonCompatibleCharacters($translatedText); |
|
|
| file_put_contents($langJsonPath, $this->toJson($translations)); |
|
|
| return $pluginName . '_' . $key; |
| } |
|
|
| protected function checkAndUpdateRequiredPiwikVersion($pluginName) |
| { |
| $output = $this->getOutput(); |
| $pluginJsonPath = $this->getPluginPath($pluginName) . '/plugin.json'; |
| $relativePluginJson = Manager::getPluginDirectory($pluginName) . '/plugin.json'; |
|
|
| if (!file_exists($pluginJsonPath) || !is_writable($pluginJsonPath)) { |
| return; |
| } |
|
|
| $pluginJson = file_get_contents($pluginJsonPath); |
| $pluginJson = json_decode($pluginJson, true); |
|
|
| if (empty($pluginJson)) { |
| return; |
| } |
|
|
| if (empty($pluginJson['require'])) { |
| $pluginJson['require'] = array(); |
| } |
|
|
| $piwikVersion = Version::VERSION; |
| $nextMajorVersion = (int) substr($piwikVersion, 0, strpos($piwikVersion, '.')) + 1; |
| $secondPartPiwikVersionRequire = ',<' . $nextMajorVersion . '.0.0-b1'; |
| if (false === strpos($piwikVersion, '-')) { |
| |
| |
| |
| $piwikVersion .= '-stable'; |
| } |
|
|
| $newRequiredVersion = sprintf('>=%s,<%d.0.0-b1', $piwikVersion, $nextMajorVersion); |
|
|
| if (!empty($pluginJson['require']['piwik'])) { |
| $pluginJson['require']['matomo'] = $pluginJson['require']['piwik']; |
| unset($pluginJson['require']['piwik']); |
| } |
|
|
| if (!empty($pluginJson['require']['matomo'])) { |
| $requiredVersion = trim($pluginJson['require']['matomo']); |
|
|
| if ($requiredVersion === $newRequiredVersion) { |
| |
| return; |
| } |
|
|
| |
| |
| |
| |
|
|
| foreach (['<>','!=', '<=','==', '^'] as $comparison) { |
| if (strpos($requiredVersion, $comparison) === 0) { |
| |
| return; |
| } |
| } |
|
|
| if (strpos($requiredVersion, '||') !== false || strpos($requiredVersion, ' ') !== false) { |
| |
| return; |
| } |
|
|
| $requiredPiwikVersions = explode(',', (string) $requiredVersion); |
| $numRequiredPiwikVersions = count($requiredPiwikVersions); |
|
|
| if ($numRequiredPiwikVersions > 2) { |
| |
| return; |
| } |
|
|
| if ( |
| $numRequiredPiwikVersions === 2 && |
| !Common::stringEndsWith($requiredVersion, $secondPartPiwikVersionRequire) |
| ) { |
| |
| return; |
| } |
|
|
| |
|
|
| $dependency = new Dependency(); |
| $missingVersion = $dependency->getMissingVersions($piwikVersion, $requiredVersion); |
|
|
| if (!empty($missingVersion)) { |
| $msg = sprintf( |
| 'We cannot generate this component as the plugin "%s" requires the Piwik version "%s" in the ' . |
| 'file "%s". Generating this component requires "%s". If you know your plugin is compatible with your ' . |
| 'Piwik version remove the required Piwik version in "%s" and try to execute this command again.', |
| $pluginName, |
| $requiredVersion, |
| $relativePluginJson, |
| $newRequiredVersion, |
| $relativePluginJson |
| ); |
|
|
| throw new \Exception($msg); |
| } |
|
|
| $output->writeln(''); |
| $output->writeln(sprintf( |
| '<comment>We have updated the required Piwik version from "%s" to "%s" in "%s".</comment>', |
| $requiredVersion, |
| $newRequiredVersion, |
| $relativePluginJson |
| )); |
| } else { |
| $output->writeln(''); |
| $output->writeln(sprintf( |
| '<comment>We have updated your "%s" to require the Piwik version "%s".</comment>', |
| $relativePluginJson, |
| $newRequiredVersion |
| )); |
| } |
|
|
| $pluginJson['require']['matomo'] = $newRequiredVersion; |
| file_put_contents($pluginJsonPath, $this->toJson($pluginJson)); |
| } |
|
|
| private function toJson($value) |
| { |
| if (defined('JSON_PRETTY_PRINT')) { |
| return json_encode($value, JSON_PRETTY_PRINT); |
| } |
|
|
| return json_encode($value); |
| } |
|
|
| private function buildTranslationKey($translatedText) |
| { |
| $translatedText = preg_replace('/(\s+)/', '', $translatedText); |
| $translatedText = preg_replace("/[^A-Za-z0-9]/", '', $translatedText); |
| $translatedText = trim($translatedText); |
|
|
| return $this->removeNonJsonCompatibleCharacters($translatedText); |
| } |
|
|
| private function removeNonJsonCompatibleCharacters($text) |
| { |
| return preg_replace('/[^(\x00-\x7F)]*/', '', $text); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| protected function copyTemplateMethodToExisitingClass($sourceClassName, $methodName, $replace) |
| { |
| $targetClassName = $this->replaceContent($replace, $sourceClassName); |
|
|
| if (Development::methodExists($targetClassName, $methodName)) { |
| |
| return; |
| } |
|
|
| Development::checkMethodExists($sourceClassName, $methodName, 'Cannot copy template method: '); |
|
|
| $targetClass = new \ReflectionClass($targetClassName); |
| $file = new \SplFileObject($targetClass->getFileName()); |
|
|
| $methodCode = Development::getMethodSourceCode($sourceClassName, $methodName); |
| $methodCode = $this->replaceContent($replace, $methodCode); |
| $methodLine = $targetClass->getEndLine() - 1; |
|
|
| $sourceUses = Development::getUseStatements($sourceClassName); |
| $targetUses = Development::getUseStatements($targetClassName); |
| $usesToAdd = array_diff($sourceUses, $targetUses); |
| if (empty($usesToAdd)) { |
| $useCode = ''; |
| } else { |
| $useCode = "\nuse " . implode("\nuse ", $usesToAdd) . "\n"; |
| } |
|
|
| |
| $useLine = 0; |
| foreach (new \LimitIterator($file, 0, $targetClass->getStartLine()) as $index => $line) { |
| if (0 === strpos(trim($line), 'namespace ')) { |
| $useLine = $index + 1; |
| break; |
| } |
| } |
|
|
| $newClassCode = ''; |
| foreach (new \LimitIterator($file) as $index => $line) { |
| if ($index == $methodLine) { |
| $newClassCode .= $methodCode; |
| } |
|
|
| if (0 !== $useLine && $index == $useLine) { |
| $newClassCode .= $useCode; |
| } |
|
|
| $newClassCode .= $line; |
| } |
|
|
| file_put_contents($targetClass->getFileName(), $newClassCode); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| protected function copyTemplateToPlugin($templateFolder, $pluginName, array $replace = [], $whitelistFiles = []) |
| { |
| $replace['PLUGINNAME'] = $pluginName; |
|
|
| $files = array_merge( |
| Filesystem::globr($templateFolder, '*'), |
| // Also copy files starting with . such as .gitignore |
| Filesystem::globr($templateFolder, '.*') |
| ); |
|
|
| foreach ($files as $file) { |
| $fileNamePlugin = str_replace($templateFolder, '', $file); |
|
|
| if (!empty($whitelistFiles) && !in_array($fileNamePlugin, $whitelistFiles)) { |
| continue; |
| } |
|
|
| if (is_dir($file)) { |
| $fileNamePlugin = $this->replaceContent($replace, $fileNamePlugin); |
| $this->createFolderWithinPluginIfNotExists($pluginName, $fileNamePlugin); |
| } else { |
| $template = file_get_contents($file); |
| $template = $this->replaceContent($replace, $template); |
|
|
| $fileNamePlugin = $this->replaceContent($replace, $fileNamePlugin); |
|
|
| $this->createFileWithinPluginIfNotExists($pluginName, $fileNamePlugin, $template); |
| } |
| } |
| } |
|
|
| protected function getPluginNames() |
| { |
| $pluginNames = array(); |
| foreach (Manager::getPluginsDirectories() as $pluginsDir) { |
| $pluginDirs = \_glob($pluginsDir . '*', GLOB_ONLYDIR); |
|
|
| foreach ($pluginDirs as $pluginDir) { |
| $pluginNames[] = basename($pluginDir); |
| } |
| } |
|
|
| return $pluginNames; |
| } |
|
|
| protected function getPluginNamesHavingNotSpecificFile($filename) |
| { |
| $pluginNames = array(); |
| foreach (Manager::getPluginsDirectories() as $pluginsDir) { |
| $pluginDirs = \_glob($pluginsDir . '*', GLOB_ONLYDIR); |
|
|
| foreach ($pluginDirs as $pluginDir) { |
| if (!file_exists($pluginDir . '/' . $filename)) { |
| $pluginNames[] = basename($pluginDir); |
| } |
| } |
| } |
| return $pluginNames; |
| } |
|
|
| |
| |
| |
| |
| protected function askPluginNameAndValidate($pluginNames, $invalidArgumentException) |
| { |
| $input = $this->getInput(); |
|
|
| $validate = function ($pluginName) use ($pluginNames, $invalidArgumentException) { |
| if (!in_array($pluginName, $pluginNames)) { |
| throw new \InvalidArgumentException($invalidArgumentException); |
| } |
|
|
| return $pluginName; |
| }; |
|
|
| $pluginName = $input->getOption('pluginname'); |
|
|
| if (empty($pluginName)) { |
| $pluginName = $this->askAndValidate('Enter the name of your plugin: ', $validate, false, $pluginNames); |
| } else { |
| $validate($pluginName); |
| } |
|
|
| return $pluginName; |
| } |
|
|
| private function getPathToCore() |
| { |
| $path = PIWIK_INCLUDE_PATH . '/core'; |
| return $path; |
| } |
|
|
| private function replaceContent($replace, $contentToReplace) |
| { |
| foreach ((array) $replace as $key => $value) { |
| $contentToReplace = str_replace($key, $value, $contentToReplace); |
| } |
|
|
| return $contentToReplace; |
| } |
|
|
| protected function throwErrorIfNotGitInstalled() |
| { |
| if (!SettingsPiwik::isGitDeployment()) { |
| $url = 'https://developer.matomo.org/guides/getting-started-part-1'; |
| throw new NotGitInstalledException( |
| "This development feature requires Matomo to be checked out from git. For more information please visit {$url}." |
| ); |
| } |
| } |
| } |
|
|