PageRenderTime 45ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/wp-content/plugins/versionpress/src/Utils/RequirementsChecker.php

https://gitlab.com/vanafroo/landingpage
PHP | 334 lines | 231 code | 52 blank | 51 comment | 11 complexity | 967da9394f8e9a560e079f40019eadd4 MD5 | raw file
  1. <?php
  2. namespace VersionPress\Utils;
  3. use Exception;
  4. use Nette\Utils\Strings;
  5. use Symfony\Component\Filesystem\Exception\IOException;
  6. use VersionPress\Database\Database;
  7. use VersionPress\Database\DbSchemaInfo;
  8. use wpdb;
  9. class RequirementsChecker
  10. {
  11. private $requirements = [];
  12. /**
  13. * @var Database
  14. */
  15. private $database;
  16. /**
  17. * @var DbSchemaInfo
  18. */
  19. private $schema;
  20. const SITE = 'site';
  21. const ENVIRONMENT = 'environment';
  22. /** @var string[] */
  23. public static $compatiblePlugins = [
  24. 'akismet' => 'akismet/akismet.php',
  25. 'advanced-custom-fields' => 'advanced-custom-fields/acf.php',
  26. 'hello-dolly' => 'hello-dolly/hello.php',
  27. '_hello-dolly' => 'hello.php',
  28. 'versionpress' => 'versionpress/versionpress.php',
  29. ];
  30. public static $incompatiblePlugins = [
  31. 'wp-super-cache' => 'wp-super-cache/wp-cache.php'
  32. ];
  33. /** @var bool */
  34. private $isWithoutCriticalErrors;
  35. /** @var bool */
  36. private $isEverythingFulfilled;
  37. /**
  38. * RequirementsChecker constructor.
  39. * @param Database $database
  40. * @param DbSchemaInfo $schema
  41. * @param string $checkScope determines if all VersionPress requirements need to be fullfilled or just some of them.
  42. * Possible values are RequirementsChecker::SITE or RequirementsChecker::ENVIRONMENT
  43. * Default value is RequirementsChecker::SITE which means that all requirements need to be matched.
  44. * RequirementsChecker::ENVIRONMENT checks only requirements related to "runtime" environment.
  45. */
  46. public function __construct($database, DbSchemaInfo $schema, $checkScope = RequirementsChecker::SITE)
  47. {
  48. $this->database = $database;
  49. $this->schema = $schema;
  50. // Markdown can be used in the 'help' field
  51. $this->requirements[] = [
  52. 'name' => 'PHP 5.6+',
  53. 'level' => 'critical',
  54. 'fulfilled' => version_compare(PHP_VERSION, '5.6.0', '>='),
  55. 'help' => 'PHP 5.6+ is required.'
  56. ];
  57. $this->requirements[] = [
  58. 'name' => "'mbstring' extension",
  59. 'level' => 'critical',
  60. 'fulfilled' => extension_loaded('mbstring'),
  61. 'help' => 'Extension `mbstring` is required.'
  62. ];
  63. $this->requirements[] = [
  64. 'name' => 'Execute external commands',
  65. 'level' => 'critical',
  66. 'fulfilled' => $this->tryRunProcess(),
  67. 'help' => 'PHP function `proc_open()` must be enabled as VersionPress uses it to execute Git commands. ' .
  68. 'Please update your php.ini.'
  69. ];
  70. $gitCheckResult = $this->tryGit();
  71. switch ($gitCheckResult) {
  72. case "no-git":
  73. // @codingStandardsIgnoreLine
  74. $gitHelpMessage = '[Git](http://git-scm.com/) must be installed on the server. If you think it is then it\'s probably not visible to the web server user – please update its PATH. Alternatively, [configure VersionPress](http://docs.versionpress.net/en/getting-started/configuration#git-binary) to use specific Git binary. [Learn more](http://docs.versionpress.net/en/getting-started/installation-uninstallation#git).';
  75. break;
  76. case "wrong-version":
  77. // @codingStandardsIgnoreLine
  78. $gitHelpMessage = 'Git version ' . SystemInfo::getGitVersion() . ' detected with which there are known issues. Please install at least version ' . self::GIT_MINIMUM_REQUIRED_VERSION . ' (this can be done side-by-side and VersionPress can be [configured](http://docs.versionpress.net/en/getting-started/configuration#git-binary) to use that specific Git version). [Learn more](http://docs.versionpress.net/en/getting-started/installation-uninstallation#git).';
  79. break;
  80. default:
  81. $gitHelpMessage = "";
  82. }
  83. $this->requirements[] = [
  84. 'name' => 'Git ' . self::GIT_MINIMUM_REQUIRED_VERSION . '+ installed',
  85. 'level' => 'critical',
  86. 'fulfilled' => $gitCheckResult == "ok",
  87. 'help' => $gitHelpMessage
  88. ];
  89. $this->requirements[] = [
  90. 'name' => 'Write access on the filesystem',
  91. 'level' => 'critical',
  92. 'fulfilled' => $this->tryWrite(),
  93. // @codingStandardsIgnoreLine
  94. 'help' => 'VersionPress needs write access in the site root, its nested directories and the <abbr title="' . sys_get_temp_dir() . '" style="border-bottom: 1px dotted; border-color: inherit;">system temp directory</abbr>. Please update the permissions.'
  95. ];
  96. $this->requirements[] = [
  97. 'name' => 'Access rules can be installed',
  98. 'level' => 'warning',
  99. 'fulfilled' => $this->tryAccessControlFiles(),
  100. // @codingStandardsIgnoreLine
  101. 'help' => 'VersionPress automatically tries to secure certain locations, like `wp-content/vpdb`. You either don\'t have a supported web server or rules cannot be enforced. [Learn more](http://docs.versionpress.net/en/getting-started/installation-uninstallation#supported-web-servers).'
  102. ];
  103. if ($checkScope === RequirementsChecker::SITE) {
  104. $this->requirements[] = [
  105. 'name' => 'wpdb hook',
  106. 'level' => 'critical',
  107. 'fulfilled' => is_writable(ABSPATH . WPINC . '/wp-db.php'),
  108. // @codingStandardsIgnoreLine
  109. 'help' => 'For VersionPress to do its magic, it needs to change the `wpdb` class and put some code there. ' .
  110. 'To do so it needs write access to the `wp-includes/wp-db.php` file. Please update the permissions.'
  111. ];
  112. $this->requirements[] = [
  113. 'name' => 'Not multisite',
  114. 'level' => 'critical',
  115. 'fulfilled' => !is_multisite(),
  116. 'help' => 'Currently VersionPress does not support multisites. Stay tuned!'
  117. ];
  118. $this->requirements[] = [
  119. 'name' => 'Standard directory layout',
  120. 'level' => 'warning',
  121. 'fulfilled' => $this->testDirectoryLayout(),
  122. // @codingStandardsIgnoreLine
  123. 'help' => 'It seems like you use customized project structure. VersionPress supports only some scenarios. [Learn more](http://docs.versionpress.net/en/feature-focus/custom-project-structure).'
  124. ];
  125. $setTimeLimitEnabled = (false === strpos(ini_get("disable_functions"), "set_time_limit"));
  126. $countOfEntities = $this->countEntities();
  127. if ($setTimeLimitEnabled) {
  128. // @codingStandardsIgnoreLine
  129. $help = "The initialization will take a little longer. This website contains $countOfEntities entities.";
  130. } else {
  131. $help = "The initialization may not finish. This website contains $countOfEntities entities.";
  132. }
  133. $this->requirements[] = [
  134. 'name' => 'Web size',
  135. 'level' => 'warning',
  136. 'fulfilled' => $countOfEntities < 500,
  137. 'help' => $help
  138. ];
  139. $unsupportedPluginsCount = $this->testExternalPlugins();
  140. $externalPluginsHelp = "You run $unsupportedPluginsCount external " .
  141. ($unsupportedPluginsCount == 1 ? "plugin" : "plugins") .
  142. ' we have not tested yet. <a href="http://docs.versionpress.net/en/feature-focus/external-plugins">' .
  143. 'Read more about 3rd party plugins support.</a>';
  144. $this->requirements[] = [
  145. 'name' => 'External plugins',
  146. 'level' => 'warning',
  147. 'fulfilled' => $unsupportedPluginsCount == 0,
  148. 'help' => $externalPluginsHelp
  149. ];
  150. }
  151. $this->isWithoutCriticalErrors = array_reduce($this->requirements, function ($carry, $requirement) {
  152. return $carry && ($requirement['fulfilled'] || $requirement['level'] === 'warning');
  153. }, true);
  154. $this->isEverythingFulfilled = array_reduce($this->requirements, function ($carry, $requirement) {
  155. return $carry && $requirement['fulfilled'];
  156. }, true);
  157. }
  158. /**
  159. * Returns list of requirements and their fulfillment
  160. *
  161. * @return array
  162. */
  163. public function getRequirements()
  164. {
  165. return $this->requirements;
  166. }
  167. public function isWithoutCriticalErrors()
  168. {
  169. return $this->isWithoutCriticalErrors;
  170. }
  171. public function isEverythingFulfilled()
  172. {
  173. return $this->isEverythingFulfilled;
  174. }
  175. private function tryRunProcess()
  176. {
  177. try {
  178. $process = new Process("echo test");
  179. $process->run();
  180. return true;
  181. } catch (Exception $e) {
  182. return false;
  183. }
  184. }
  185. /**
  186. * @return string "ok", "no-git" or "wrong-version"
  187. */
  188. private function tryGit()
  189. {
  190. try {
  191. $gitVersion = SystemInfo::getGitVersion();
  192. return self::gitMatchesMinimumRequiredVersion($gitVersion) ? "ok" : "wrong-version";
  193. } catch (Exception $e) {
  194. return "no-git";
  195. }
  196. }
  197. private function tryWrite()
  198. {
  199. $filename = ".vp-try-write";
  200. $testPaths = [
  201. ABSPATH,
  202. WP_CONTENT_DIR,
  203. sys_get_temp_dir()
  204. ];
  205. $writable = true;
  206. foreach ($testPaths as $directory) {
  207. $filePath = $directory . '/' . $filename;
  208. /** @noinspection PhpUsageOfSilenceOperatorInspection */
  209. @file_put_contents($filePath, "");
  210. $writable &= is_file($filePath);
  211. FileSystem::remove($filePath);
  212. // Trying to create file from process (issue #522)
  213. $process = new Process(sprintf("echo test > %s", escapeshellarg($filePath)));
  214. $process->run();
  215. $writable &= is_file($filePath);
  216. try {
  217. FileSystem::remove($filePath);
  218. } catch (IOException $ex) {
  219. $writable = false; // the file could not be deleted - the permissions are wrong
  220. }
  221. }
  222. return $writable;
  223. }
  224. private function tryAccessControlFiles()
  225. {
  226. $securedUrl = site_url() . '/wp-content/plugins/versionpress/temp/security-check.txt';
  227. /** @noinspection PhpUsageOfSilenceOperatorInspection */
  228. return @file_get_contents($securedUrl) === false; // intentionally @
  229. }
  230. private function testDirectoryLayout()
  231. {
  232. $uploadDirInfo = wp_upload_dir();
  233. $isStandardLayout = true;
  234. $isStandardLayout &= ABSPATH . 'wp-content' === WP_CONTENT_DIR;
  235. $isStandardLayout &= WP_CONTENT_DIR . '/plugins' === WP_PLUGIN_DIR;
  236. $isStandardLayout &= WP_CONTENT_DIR . '/themes' === get_theme_root();
  237. $isStandardLayout &= WP_CONTENT_DIR . '/uploads' === $uploadDirInfo['basedir'];
  238. $isStandardLayout &= is_file(ABSPATH . 'wp-config.php');
  239. return $isStandardLayout;
  240. }
  241. /**
  242. * Minimum required Git version
  243. */
  244. const GIT_MINIMUM_REQUIRED_VERSION = "1.9";
  245. /**
  246. * Returns true if git version matches the minimum required version. If minimum required version
  247. * is not given, RequirementsChecker::GIT_MINIMUM_REQUIRED_VERSION is used by default.
  248. *
  249. * @param string $gitVersion
  250. * @param string $minimumRequiredVersion
  251. * @return bool
  252. */
  253. public static function gitMatchesMinimumRequiredVersion($gitVersion, $minimumRequiredVersion = null)
  254. {
  255. $minimumRequiredVersion = $minimumRequiredVersion ?: self::GIT_MINIMUM_REQUIRED_VERSION;
  256. return version_compare($gitVersion, $minimumRequiredVersion, ">=");
  257. }
  258. private function countEntities()
  259. {
  260. $entities = $this->schema->getAllEntityNames();
  261. $totalEntitiesCount = 0;
  262. foreach ($entities as $entity) {
  263. $table = $this->schema->getPrefixedTableName($entity);
  264. $totalEntitiesCount += $this->database->get_var("SELECT COUNT(*) FROM $table");
  265. }
  266. return $totalEntitiesCount;
  267. }
  268. /**
  269. * @return int Number of unsupported plugins.
  270. */
  271. private function testExternalPlugins()
  272. {
  273. $plugins = get_option('active_plugins');
  274. $unsupportedPluginsCount = 0;
  275. foreach ($plugins as $plugin) {
  276. if (!in_array($plugin, self::$compatiblePlugins)) {
  277. $unsupportedPluginsCount++;
  278. }
  279. }
  280. return $unsupportedPluginsCount;
  281. }
  282. }