PageRenderTime 50ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/stats/plugins/CoreConsole/Commands/GeneratePluginBase.php

https://bitbucket.org/webstar1987923/mycampaignsio
PHP | 396 lines | 261 code | 81 blank | 54 comment | 43 complexity | db78e5e24d0be7ca55094391d3022e96 MD5 | raw file
Possible License(s): BSD-3-Clause, MPL-2.0-no-copyleft-exception, GPL-3.0, GPL-2.0, WTFPL, BSD-2-Clause, LGPL-2.1, Apache-2.0, MIT, AGPL-3.0
  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\Plugins\CoreConsole\Commands;
  10. use Piwik\Common;
  11. use Piwik\Development;
  12. use Piwik\Filesystem;
  13. use Piwik\Plugin\ConsoleCommand;
  14. use Piwik\Plugin\Dependency;
  15. use Piwik\Version;
  16. use Symfony\Component\Console\Input\InputInterface;
  17. use Symfony\Component\Console\Output\OutputInterface;
  18. abstract class GeneratePluginBase extends ConsoleCommand
  19. {
  20. public function isEnabled()
  21. {
  22. return Development::isEnabled();
  23. }
  24. public function getPluginPath($pluginName)
  25. {
  26. return PIWIK_INCLUDE_PATH . $this->getRelativePluginPath($pluginName);
  27. }
  28. private function getRelativePluginPath($pluginName)
  29. {
  30. return '/plugins/' . $pluginName;
  31. }
  32. private function createFolderWithinPluginIfNotExists($pluginNameOrCore, $folder)
  33. {
  34. if ($pluginNameOrCore === 'core') {
  35. $pluginPath = $this->getPathToCore();
  36. } else {
  37. $pluginPath = $this->getPluginPath($pluginNameOrCore);
  38. }
  39. if (!file_exists($pluginPath . $folder)) {
  40. Filesystem::mkdir($pluginPath . $folder);
  41. }
  42. }
  43. protected function createFileWithinPluginIfNotExists($pluginNameOrCore, $fileName, $content)
  44. {
  45. if ($pluginNameOrCore === 'core') {
  46. $pluginPath = $this->getPathToCore();
  47. } else {
  48. $pluginPath = $this->getPluginPath($pluginNameOrCore);
  49. }
  50. if (!file_exists($pluginPath . $fileName)) {
  51. file_put_contents($pluginPath . $fileName, $content);
  52. }
  53. }
  54. /**
  55. * Creates a lang/en.json within the plugin in case it does not exist yet and adds a translation for the given
  56. * text.
  57. *
  58. * @param $pluginName
  59. * @param $translatedText
  60. * @param string $translationKey Optional, by default the key will be generated automatically
  61. * @return string Either the generated translation key or the original text if a different translation for this
  62. * generated translation key already exists.
  63. */
  64. protected function makeTranslationIfPossible($pluginName, $translatedText, $translationKey = '')
  65. {
  66. $defaultLang = array($pluginName => array());
  67. $this->createFolderWithinPluginIfNotExists($pluginName, '/lang');
  68. $this->createFileWithinPluginIfNotExists($pluginName, '/lang/en.json', $this->toJson($defaultLang));
  69. $langJsonPath = $this->getPluginPath($pluginName) . '/lang/en.json';
  70. $translations = file_get_contents($langJsonPath);
  71. $translations = json_decode($translations, true);
  72. if (empty($translations[$pluginName])) {
  73. $translations[$pluginName] = array();
  74. }
  75. if (!empty($translationKey)) {
  76. $key = $translationKey;
  77. } else {
  78. $key = $this->buildTranslationKey($translatedText);
  79. }
  80. if (array_key_exists($key, $translations[$pluginName])) {
  81. // we do not want to overwrite any existing translations
  82. if ($translations[$pluginName][$key] === $translatedText) {
  83. return $pluginName . '_' . $key;
  84. }
  85. return $translatedText;
  86. }
  87. $translations[$pluginName][$key] = $this->removeNonJsonCompatibleCharacters($translatedText);
  88. file_put_contents($langJsonPath, $this->toJson($translations));
  89. return $pluginName . '_' . $key;
  90. }
  91. protected function checkAndUpdateRequiredPiwikVersion($pluginName, OutputInterface $output)
  92. {
  93. $pluginJsonPath = $this->getPluginPath($pluginName) . '/plugin.json';
  94. $relativePluginJson = $this->getRelativePluginPath($pluginName) . '/plugin.json';
  95. if (!file_exists($pluginJsonPath) || !is_writable($pluginJsonPath)) {
  96. return;
  97. }
  98. $pluginJson = file_get_contents($pluginJsonPath);
  99. $pluginJson = json_decode($pluginJson, true);
  100. if (empty($pluginJson)) {
  101. return;
  102. }
  103. if (empty($pluginJson['require'])) {
  104. $pluginJson['require'] = array();
  105. }
  106. $piwikVersion = Version::VERSION;
  107. $nextMajorVersion = (int) substr($piwikVersion, 0, strpos($piwikVersion, '.')) + 1;
  108. $secondPartPiwikVersionRequire = ',<' . $nextMajorVersion . '.0.0-b1';
  109. if (false === strpos($piwikVersion, '-')) {
  110. // see https://github.com/composer/composer/issues/4080 we need to specify -stable otherwise it would match
  111. // $piwikVersion-dev meaning it would also match all pre-released. However, we only want to match a stable
  112. // release
  113. $piwikVersion.= '-stable';
  114. }
  115. $newRequiredVersion = sprintf('>=%s,<%d.0.0-b1', $piwikVersion, $nextMajorVersion);
  116. if (!empty($pluginJson['require']['piwik'])) {
  117. $requiredVersion = trim($pluginJson['require']['piwik']);
  118. if ($requiredVersion === $newRequiredVersion) {
  119. // there is nothing to updated
  120. return;
  121. }
  122. // our generated versions look like ">=2.25.4,<3.0.0-b1".
  123. // We only updated the Piwik version in the first part if the piwik version looks like that or if it has only
  124. // one piwik version defined. In all other cases, eg user uses || etc we do not update it as user has customized
  125. // the piwik version.
  126. foreach (['<>','!=', '<=','==', '^'] as $comparison) {
  127. if (strpos($requiredVersion, $comparison) === 0) {
  128. // user is using custom piwik version require, we do not overwrite anything.
  129. return;
  130. }
  131. }
  132. if (strpos($requiredVersion, '||') !== false || strpos($requiredVersion, ' ') !== false) {
  133. // user is using custom piwik version require, we do not overwrite anything.
  134. return;
  135. }
  136. $requiredPiwikVersions = explode(',', (string) $requiredVersion);
  137. $numRequiredPiwikVersions = count($requiredPiwikVersions);
  138. if ($numRequiredPiwikVersions > 2) {
  139. // user is using custom piwik version require, we do not overwrite anything.
  140. return;
  141. }
  142. if ($numRequiredPiwikVersions === 2 &&
  143. !Common::stringEndsWith($requiredVersion, $secondPartPiwikVersionRequire)) {
  144. // user is using custom piwik version require, we do not overwrite anything
  145. return;
  146. }
  147. // if only one piwik version is defined we update it to make sure it does now specify an upper version limit
  148. $dependency = new Dependency();
  149. $missingVersion = $dependency->getMissingVersions($piwikVersion, $requiredVersion);
  150. if (!empty($missingVersion)) {
  151. $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);
  152. throw new \Exception($msg);
  153. }
  154. $output->writeln('');
  155. $output->writeln(sprintf('<comment>We have updated the required Piwik version from "%s" to "%s" in "%s".</comment>', $requiredVersion, $newRequiredVersion, $relativePluginJson));
  156. } else {
  157. $output->writeln('');
  158. $output->writeln(sprintf('<comment>We have updated your "%s" to require the Piwik version "%s".</comment>', $relativePluginJson, $newRequiredVersion));
  159. }
  160. $pluginJson['require']['piwik'] = $newRequiredVersion;
  161. file_put_contents($pluginJsonPath, $this->toJson($pluginJson));
  162. }
  163. private function toJson($value)
  164. {
  165. if (defined('JSON_PRETTY_PRINT')) {
  166. return json_encode($value, JSON_PRETTY_PRINT);
  167. }
  168. return json_encode($value);
  169. }
  170. private function buildTranslationKey($translatedText)
  171. {
  172. $translatedText = preg_replace('/(\s+)/', '', $translatedText);
  173. $translatedText = preg_replace("/[^A-Za-z0-9]/", '', $translatedText);
  174. $translatedText = trim($translatedText);
  175. return $this->removeNonJsonCompatibleCharacters($translatedText);
  176. }
  177. private function removeNonJsonCompatibleCharacters($text)
  178. {
  179. return preg_replace('/[^(\x00-\x7F)]*/', '', $text);
  180. }
  181. /**
  182. * Copies the given method and all needed use statements into an existing class. The target class name will be
  183. * built based on the given $replace argument.
  184. * @param string $sourceClassName
  185. * @param string $methodName
  186. * @param array $replace
  187. */
  188. protected function copyTemplateMethodToExisitingClass($sourceClassName, $methodName, $replace)
  189. {
  190. $targetClassName = $this->replaceContent($replace, $sourceClassName);
  191. if (Development::methodExists($targetClassName, $methodName)) {
  192. // we do not want to add the same method twice
  193. return;
  194. }
  195. Development::checkMethodExists($sourceClassName, $methodName, 'Cannot copy template method: ');
  196. $targetClass = new \ReflectionClass($targetClassName);
  197. $file = new \SplFileObject($targetClass->getFileName());
  198. $methodCode = Development::getMethodSourceCode($sourceClassName, $methodName);
  199. $methodCode = $this->replaceContent($replace, $methodCode);
  200. $methodLine = $targetClass->getEndLine() - 1;
  201. $sourceUses = Development::getUseStatements($sourceClassName);
  202. $targetUses = Development::getUseStatements($targetClassName);
  203. $usesToAdd = array_diff($sourceUses, $targetUses);
  204. if (empty($usesToAdd)) {
  205. $useCode = '';
  206. } else {
  207. $useCode = "\nuse " . implode("\nuse ", $usesToAdd) . "\n";
  208. }
  209. // search for namespace line before the class starts
  210. $useLine = 0;
  211. foreach (new \LimitIterator($file, 0, $targetClass->getStartLine()) as $index => $line) {
  212. if (0 === strpos(trim($line), 'namespace ')) {
  213. $useLine = $index + 1;
  214. break;
  215. }
  216. }
  217. $newClassCode = '';
  218. foreach(new \LimitIterator($file) as $index => $line) {
  219. if ($index == $methodLine) {
  220. $newClassCode .= $methodCode;
  221. }
  222. if (0 !== $useLine && $index == $useLine) {
  223. $newClassCode .= $useCode;
  224. }
  225. $newClassCode .= $line;
  226. }
  227. file_put_contents($targetClass->getFileName(), $newClassCode);
  228. }
  229. /**
  230. * @param string $templateFolder full path like /home/...
  231. * @param string $pluginName
  232. * @param array $replace array(key => value) $key will be replaced by $value in all templates
  233. * @param array $whitelistFiles If not empty, only given files/directories will be copied.
  234. * For instance array('/Controller.php', '/templates', '/templates/index.twig')
  235. */
  236. protected function copyTemplateToPlugin($templateFolder, $pluginName, array $replace = array(), $whitelistFiles = array())
  237. {
  238. $replace['PLUGINNAME'] = $pluginName;
  239. $files = array_merge(
  240. Filesystem::globr($templateFolder, '*'),
  241. // Also copy files starting with . such as .gitignore
  242. Filesystem::globr($templateFolder, '.*')
  243. );
  244. foreach ($files as $file) {
  245. $fileNamePlugin = str_replace($templateFolder, '', $file);
  246. if (!empty($whitelistFiles) && !in_array($fileNamePlugin, $whitelistFiles)) {
  247. continue;
  248. }
  249. if (is_dir($file)) {
  250. $fileNamePlugin = $this->replaceContent($replace, $fileNamePlugin);
  251. $this->createFolderWithinPluginIfNotExists($pluginName, $fileNamePlugin);
  252. } else {
  253. $template = file_get_contents($file);
  254. $template = $this->replaceContent($replace, $template);
  255. $fileNamePlugin = $this->replaceContent($replace, $fileNamePlugin);
  256. $this->createFileWithinPluginIfNotExists($pluginName, $fileNamePlugin, $template);
  257. }
  258. }
  259. }
  260. protected function getPluginNames()
  261. {
  262. $pluginDirs = \_glob(PIWIK_INCLUDE_PATH . '/plugins/*', GLOB_ONLYDIR);
  263. $pluginNames = array();
  264. foreach ($pluginDirs as $pluginDir) {
  265. $pluginNames[] = basename($pluginDir);
  266. }
  267. return $pluginNames;
  268. }
  269. protected function getPluginNamesHavingNotSpecificFile($filename)
  270. {
  271. $pluginDirs = \_glob(PIWIK_INCLUDE_PATH . '/plugins/*', GLOB_ONLYDIR);
  272. $pluginNames = array();
  273. foreach ($pluginDirs as $pluginDir) {
  274. if (!file_exists($pluginDir . '/' . $filename)) {
  275. $pluginNames[] = basename($pluginDir);
  276. }
  277. }
  278. return $pluginNames;
  279. }
  280. /**
  281. * @param InputInterface $input
  282. * @param OutputInterface $output
  283. * @return string
  284. * @throws \RuntimeException
  285. */
  286. protected function askPluginNameAndValidate(InputInterface $input, OutputInterface $output, $pluginNames, $invalidArgumentException)
  287. {
  288. $validate = function ($pluginName) use ($pluginNames, $invalidArgumentException) {
  289. if (!in_array($pluginName, $pluginNames)) {
  290. throw new \InvalidArgumentException($invalidArgumentException);
  291. }
  292. return $pluginName;
  293. };
  294. $pluginName = $input->getOption('pluginname');
  295. if (empty($pluginName)) {
  296. $dialog = $this->getHelperSet()->get('dialog');
  297. $pluginName = $dialog->askAndValidate($output, 'Enter the name of your plugin: ', $validate, false, null, $pluginNames);
  298. } else {
  299. $validate($pluginName);
  300. }
  301. return $pluginName;
  302. }
  303. private function getPathToCore()
  304. {
  305. $path = PIWIK_INCLUDE_PATH . '/core';
  306. return $path;
  307. }
  308. private function replaceContent($replace, $contentToReplace)
  309. {
  310. foreach ((array) $replace as $key => $value) {
  311. $contentToReplace = str_replace($key, $value, $contentToReplace);
  312. }
  313. return $contentToReplace;
  314. }
  315. }