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

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