Geen omschrijving

class-wp-site-health-auto-updates.php 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. <?php
  2. /**
  3. * Class for testing automatic updates in the WordPress code.
  4. *
  5. * @package WordPress
  6. * @subpackage Site_Health
  7. * @since 5.2.0
  8. */
  9. class WP_Site_Health_Auto_Updates {
  10. /**
  11. * WP_Site_Health_Auto_Updates constructor.
  12. *
  13. * @since 5.2.0
  14. */
  15. public function __construct() {
  16. require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
  17. }
  18. /**
  19. * Run tests to determine if auto-updates can run.
  20. *
  21. * @since 5.2.0
  22. *
  23. * @return array The test results.
  24. */
  25. public function run_tests() {
  26. $tests = array(
  27. $this->test_constants( 'WP_AUTO_UPDATE_CORE', array( true, 'beta', 'rc', 'development', 'branch-development', 'minor' ) ),
  28. $this->test_wp_version_check_attached(),
  29. $this->test_filters_automatic_updater_disabled(),
  30. $this->test_wp_automatic_updates_disabled(),
  31. $this->test_if_failed_update(),
  32. $this->test_vcs_abspath(),
  33. $this->test_check_wp_filesystem_method(),
  34. $this->test_all_files_writable(),
  35. $this->test_accepts_dev_updates(),
  36. $this->test_accepts_minor_updates(),
  37. );
  38. $tests = array_filter( $tests );
  39. $tests = array_map(
  40. static function( $test ) {
  41. $test = (object) $test;
  42. if ( empty( $test->severity ) ) {
  43. $test->severity = 'warning';
  44. }
  45. return $test;
  46. },
  47. $tests
  48. );
  49. return $tests;
  50. }
  51. /**
  52. * Test if auto-updates related constants are set correctly.
  53. *
  54. * @since 5.2.0
  55. * @since 5.5.1 The `$value` parameter can accept an array.
  56. *
  57. * @param string $constant The name of the constant to check.
  58. * @param bool|string|array $value The value that the constant should be, if set,
  59. * or an array of acceptable values.
  60. * @return array The test results.
  61. */
  62. public function test_constants( $constant, $value ) {
  63. $acceptable_values = (array) $value;
  64. if ( defined( $constant ) && ! in_array( constant( $constant ), $acceptable_values, true ) ) {
  65. return array(
  66. 'description' => sprintf(
  67. /* translators: %s: Name of the constant used. */
  68. __( 'The %s constant is defined and enabled.' ),
  69. "<code>$constant</code>"
  70. ),
  71. 'severity' => 'fail',
  72. );
  73. }
  74. }
  75. /**
  76. * Check if updates are intercepted by a filter.
  77. *
  78. * @since 5.2.0
  79. *
  80. * @return array The test results.
  81. */
  82. public function test_wp_version_check_attached() {
  83. if ( ( ! is_multisite() || is_main_site() && is_network_admin() )
  84. && ! has_filter( 'wp_version_check', 'wp_version_check' )
  85. ) {
  86. return array(
  87. 'description' => sprintf(
  88. /* translators: %s: Name of the filter used. */
  89. __( 'A plugin has prevented updates by disabling %s.' ),
  90. '<code>wp_version_check()</code>'
  91. ),
  92. 'severity' => 'fail',
  93. );
  94. }
  95. }
  96. /**
  97. * Check if automatic updates are disabled by a filter.
  98. *
  99. * @since 5.2.0
  100. *
  101. * @return array The test results.
  102. */
  103. public function test_filters_automatic_updater_disabled() {
  104. /** This filter is documented in wp-admin/includes/class-wp-automatic-updater.php */
  105. if ( apply_filters( 'automatic_updater_disabled', false ) ) {
  106. return array(
  107. 'description' => sprintf(
  108. /* translators: %s: Name of the filter used. */
  109. __( 'The %s filter is enabled.' ),
  110. '<code>automatic_updater_disabled</code>'
  111. ),
  112. 'severity' => 'fail',
  113. );
  114. }
  115. }
  116. /**
  117. * Check if automatic updates are disabled.
  118. *
  119. * @since 5.3.0
  120. *
  121. * @return array|false The test results. False if auto-updates are enabled.
  122. */
  123. public function test_wp_automatic_updates_disabled() {
  124. if ( ! class_exists( 'WP_Automatic_Updater' ) ) {
  125. require_once ABSPATH . 'wp-admin/includes/class-wp-automatic-updater.php';
  126. }
  127. $auto_updates = new WP_Automatic_Updater();
  128. if ( ! $auto_updates->is_disabled() ) {
  129. return false;
  130. }
  131. return array(
  132. 'description' => __( 'All automatic updates are disabled.' ),
  133. 'severity' => 'fail',
  134. );
  135. }
  136. /**
  137. * Check if automatic updates have tried to run, but failed, previously.
  138. *
  139. * @since 5.2.0
  140. *
  141. * @return array|false The test results. False if the auto-updates failed.
  142. */
  143. public function test_if_failed_update() {
  144. $failed = get_site_option( 'auto_core_update_failed' );
  145. if ( ! $failed ) {
  146. return false;
  147. }
  148. if ( ! empty( $failed['critical'] ) ) {
  149. $description = __( 'A previous automatic background update ended with a critical failure, so updates are now disabled.' );
  150. $description .= ' ' . __( 'You would have received an email because of this.' );
  151. $description .= ' ' . __( "When you've been able to update using the \"Update now\" button on Dashboard > Updates, we'll clear this error for future update attempts." );
  152. $description .= ' ' . sprintf(
  153. /* translators: %s: Code of error shown. */
  154. __( 'The error code was %s.' ),
  155. '<code>' . $failed['error_code'] . '</code>'
  156. );
  157. return array(
  158. 'description' => $description,
  159. 'severity' => 'warning',
  160. );
  161. }
  162. $description = __( 'A previous automatic background update could not occur.' );
  163. if ( empty( $failed['retry'] ) ) {
  164. $description .= ' ' . __( 'You would have received an email because of this.' );
  165. }
  166. $description .= ' ' . __( "We'll try again with the next release." );
  167. $description .= ' ' . sprintf(
  168. /* translators: %s: Code of error shown. */
  169. __( 'The error code was %s.' ),
  170. '<code>' . $failed['error_code'] . '</code>'
  171. );
  172. return array(
  173. 'description' => $description,
  174. 'severity' => 'warning',
  175. );
  176. }
  177. /**
  178. * Check if WordPress is controlled by a VCS (Git, Subversion etc).
  179. *
  180. * @since 5.2.0
  181. *
  182. * @return array The test results.
  183. */
  184. public function test_vcs_abspath() {
  185. $context_dirs = array( ABSPATH );
  186. $vcs_dirs = array( '.svn', '.git', '.hg', '.bzr' );
  187. $check_dirs = array();
  188. foreach ( $context_dirs as $context_dir ) {
  189. // Walk up from $context_dir to the root.
  190. do {
  191. $check_dirs[] = $context_dir;
  192. // Once we've hit '/' or 'C:\', we need to stop. dirname will keep returning the input here.
  193. if ( dirname( $context_dir ) === $context_dir ) {
  194. break;
  195. }
  196. // Continue one level at a time.
  197. } while ( $context_dir = dirname( $context_dir ) );
  198. }
  199. $check_dirs = array_unique( $check_dirs );
  200. // Search all directories we've found for evidence of version control.
  201. foreach ( $vcs_dirs as $vcs_dir ) {
  202. foreach ( $check_dirs as $check_dir ) {
  203. // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition,Squiz.PHP.DisallowMultipleAssignments
  204. if ( $checkout = @is_dir( rtrim( $check_dir, '\\/' ) . "/$vcs_dir" ) ) {
  205. break 2;
  206. }
  207. }
  208. }
  209. /** This filter is documented in wp-admin/includes/class-wp-automatic-updater.php */
  210. if ( $checkout && ! apply_filters( 'automatic_updates_is_vcs_checkout', true, ABSPATH ) ) {
  211. return array(
  212. 'description' => sprintf(
  213. /* translators: 1: Folder name. 2: Version control directory. 3: Filter name. */
  214. __( 'The folder %1$s was detected as being under version control (%2$s), but the %3$s filter is allowing updates.' ),
  215. '<code>' . $check_dir . '</code>',
  216. "<code>$vcs_dir</code>",
  217. '<code>automatic_updates_is_vcs_checkout</code>'
  218. ),
  219. 'severity' => 'info',
  220. );
  221. }
  222. if ( $checkout ) {
  223. return array(
  224. 'description' => sprintf(
  225. /* translators: 1: Folder name. 2: Version control directory. */
  226. __( 'The folder %1$s was detected as being under version control (%2$s).' ),
  227. '<code>' . $check_dir . '</code>',
  228. "<code>$vcs_dir</code>"
  229. ),
  230. 'severity' => 'warning',
  231. );
  232. }
  233. return array(
  234. 'description' => __( 'No version control systems were detected.' ),
  235. 'severity' => 'pass',
  236. );
  237. }
  238. /**
  239. * Check if we can access files without providing credentials.
  240. *
  241. * @since 5.2.0
  242. *
  243. * @return array The test results.
  244. */
  245. public function test_check_wp_filesystem_method() {
  246. // Make sure the `request_filesystem_credentials()` function is available during our REST API call.
  247. if ( ! function_exists( 'request_filesystem_credentials' ) ) {
  248. require_once ABSPATH . '/wp-admin/includes/file.php';
  249. }
  250. $skin = new Automatic_Upgrader_Skin;
  251. $success = $skin->request_filesystem_credentials( false, ABSPATH );
  252. if ( ! $success ) {
  253. $description = __( 'Your installation of WordPress prompts for FTP credentials to perform updates.' );
  254. $description .= ' ' . __( '(Your site is performing updates over FTP due to file ownership. Talk to your hosting company.)' );
  255. return array(
  256. 'description' => $description,
  257. 'severity' => 'fail',
  258. );
  259. }
  260. return array(
  261. 'description' => __( 'Your installation of WordPress does not require FTP credentials to perform updates.' ),
  262. 'severity' => 'pass',
  263. );
  264. }
  265. /**
  266. * Check if core files are writable by the web user/group.
  267. *
  268. * @since 5.2.0
  269. *
  270. * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
  271. *
  272. * @return array|false The test results. False if they're not writeable.
  273. */
  274. public function test_all_files_writable() {
  275. global $wp_filesystem;
  276. require ABSPATH . WPINC . '/version.php'; // $wp_version; // x.y.z
  277. $skin = new Automatic_Upgrader_Skin;
  278. $success = $skin->request_filesystem_credentials( false, ABSPATH );
  279. if ( ! $success ) {
  280. return false;
  281. }
  282. WP_Filesystem();
  283. if ( 'direct' !== $wp_filesystem->method ) {
  284. return false;
  285. }
  286. // Make sure the `get_core_checksums()` function is available during our REST API call.
  287. if ( ! function_exists( 'get_core_checksums' ) ) {
  288. require_once ABSPATH . '/wp-admin/includes/update.php';
  289. }
  290. $checksums = get_core_checksums( $wp_version, 'en_US' );
  291. $dev = ( false !== strpos( $wp_version, '-' ) );
  292. // Get the last stable version's files and test against that.
  293. if ( ! $checksums && $dev ) {
  294. $checksums = get_core_checksums( (float) $wp_version - 0.1, 'en_US' );
  295. }
  296. // There aren't always checksums for development releases, so just skip the test if we still can't find any.
  297. if ( ! $checksums && $dev ) {
  298. return false;
  299. }
  300. if ( ! $checksums ) {
  301. $description = sprintf(
  302. /* translators: %s: WordPress version. */
  303. __( "Couldn't retrieve a list of the checksums for WordPress %s." ),
  304. $wp_version
  305. );
  306. $description .= ' ' . __( 'This could mean that connections are failing to WordPress.org.' );
  307. return array(
  308. 'description' => $description,
  309. 'severity' => 'warning',
  310. );
  311. }
  312. $unwritable_files = array();
  313. foreach ( array_keys( $checksums ) as $file ) {
  314. if ( 'wp-content' === substr( $file, 0, 10 ) ) {
  315. continue;
  316. }
  317. if ( ! file_exists( ABSPATH . $file ) ) {
  318. continue;
  319. }
  320. if ( ! is_writable( ABSPATH . $file ) ) {
  321. $unwritable_files[] = $file;
  322. }
  323. }
  324. if ( $unwritable_files ) {
  325. if ( count( $unwritable_files ) > 20 ) {
  326. $unwritable_files = array_slice( $unwritable_files, 0, 20 );
  327. $unwritable_files[] = '...';
  328. }
  329. return array(
  330. 'description' => __( 'Some files are not writable by WordPress:' ) . ' <ul><li>' . implode( '</li><li>', $unwritable_files ) . '</li></ul>',
  331. 'severity' => 'fail',
  332. );
  333. } else {
  334. return array(
  335. 'description' => __( 'All of your WordPress files are writable.' ),
  336. 'severity' => 'pass',
  337. );
  338. }
  339. }
  340. /**
  341. * Check if the install is using a development branch and can use nightly packages.
  342. *
  343. * @since 5.2.0
  344. *
  345. * @return array|false The test results. False if it isn't a development version.
  346. */
  347. public function test_accepts_dev_updates() {
  348. require ABSPATH . WPINC . '/version.php'; // $wp_version; // x.y.z
  349. // Only for dev versions.
  350. if ( false === strpos( $wp_version, '-' ) ) {
  351. return false;
  352. }
  353. if ( defined( 'WP_AUTO_UPDATE_CORE' ) && ( 'minor' === WP_AUTO_UPDATE_CORE || false === WP_AUTO_UPDATE_CORE ) ) {
  354. return array(
  355. 'description' => sprintf(
  356. /* translators: %s: Name of the constant used. */
  357. __( 'WordPress development updates are blocked by the %s constant.' ),
  358. '<code>WP_AUTO_UPDATE_CORE</code>'
  359. ),
  360. 'severity' => 'fail',
  361. );
  362. }
  363. /** This filter is documented in wp-admin/includes/class-core-upgrader.php */
  364. if ( ! apply_filters( 'allow_dev_auto_core_updates', $wp_version ) ) {
  365. return array(
  366. 'description' => sprintf(
  367. /* translators: %s: Name of the filter used. */
  368. __( 'WordPress development updates are blocked by the %s filter.' ),
  369. '<code>allow_dev_auto_core_updates</code>'
  370. ),
  371. 'severity' => 'fail',
  372. );
  373. }
  374. }
  375. /**
  376. * Check if the site supports automatic minor updates.
  377. *
  378. * @since 5.2.0
  379. *
  380. * @return array The test results.
  381. */
  382. public function test_accepts_minor_updates() {
  383. if ( defined( 'WP_AUTO_UPDATE_CORE' ) && false === WP_AUTO_UPDATE_CORE ) {
  384. return array(
  385. 'description' => sprintf(
  386. /* translators: %s: Name of the constant used. */
  387. __( 'WordPress security and maintenance releases are blocked by %s.' ),
  388. "<code>define( 'WP_AUTO_UPDATE_CORE', false );</code>"
  389. ),
  390. 'severity' => 'fail',
  391. );
  392. }
  393. /** This filter is documented in wp-admin/includes/class-core-upgrader.php */
  394. if ( ! apply_filters( 'allow_minor_auto_core_updates', true ) ) {
  395. return array(
  396. 'description' => sprintf(
  397. /* translators: %s: Name of the filter used. */
  398. __( 'WordPress security and maintenance releases are blocked by the %s filter.' ),
  399. '<code>allow_minor_auto_core_updates</code>'
  400. ),
  401. 'severity' => 'fail',
  402. );
  403. }
  404. }
  405. }