Ticket #46130: 46130.1.diff
File 46130.1.diff, 26.5 KB (added by , 6 years ago) |
---|
-
src/wp-admin/includes/class-wp-plugins-list-table.php
diff --git a/src/wp-admin/includes/class-wp-plugins-list-table.php b/src/wp-admin/includes/class-wp-plugins-list-table.php index a07945a973..ff471650ea 100644
a b class WP_Plugins_List_Table extends WP_List_Table { 791 791 $class .= ' update'; 792 792 } 793 793 794 $paused 795 $paused_on_network_sites_count = $screen->in_admin( 'network' ) ? count_paused_plugin_sites_for_network( $plugin_file ) : 0; 796 if ( $paused || $paused_on_network_sites_count) {794 $paused = is_plugin_paused( $plugin_file ); 795 796 if ( $paused ) { 797 797 $class .= ' paused'; 798 798 } 799 799 … … class WP_Plugins_List_Table extends WP_List_Table { 885 885 886 886 echo '</div>'; 887 887 888 if ( $paused || $paused_on_network_sites_count ) { 889 $notice_text = __( 'This plugin failed to load properly and was paused within the admin backend.' ); 890 if ( $screen->in_admin( 'network' ) && $paused_on_network_sites_count ) { 891 $notice_text = sprintf( 892 /* translators: %s: number of sites */ 893 _n( 'This plugin failed to load properly and was paused within the admin backend for %s site.', 'This plugin failed to load properly and was paused within the admin backend for %s sites.', $paused_on_network_sites_count ), 894 number_format_i18n( $paused_on_network_sites_count ) 895 ); 896 } 888 if ( $paused ) { 889 $notice_text = __( 'This plugin failed to load properly and is paused during recovery mode.' ); 897 890 898 891 printf( '<p><span class="dashicons dashicons-warning"></span> <strong>%s</strong></p>', $notice_text ); 899 892 … … class WP_Plugins_List_Table extends WP_List_Table { 911 904 912 905 $error['type'] = $core_errors[ $error['type'] ]; 913 906 907 if ( empty( $error['wp_is_protected'] ) ) { 908 /* translators: 1: error type, 2: error line number, 3: error file name, 4: error message */ 909 $error_message = __( 'The plugin caused an error of type %1$s in line %2$s of the file %3$s. Error message: %4$s' ); 910 } else { 911 /* translators: 1: error type, 2: error line number, 3: error file name, 4: error message */ 912 $error_message = __( 'The plugin caused an error in the <strong>admin backend</strong> of type %1$s in line %2$s of the file %3$s. Error message: %4$s' ); 913 } 914 914 915 printf( 915 916 '<div class="error-display"><p>%s</p></div>', 916 917 sprintf( 917 /* translators: 1: error type, 2: error line number, 3: error file name, 4: error message */ 918 __( 'The plugin caused an error of type %1$s in line %2$s of the file %3$s. Error message: %4$s' ), 918 $error_message, 919 919 "<code>{$error['type']}</code>", 920 920 "<code>{$error['line']}</code>", 921 921 "<code>{$error['file']}</code>", -
src/wp-includes/class-wp-fatal-error-handler.php
diff --git a/src/wp-includes/class-wp-fatal-error-handler.php b/src/wp-includes/class-wp-fatal-error-handler.php index 0299b67a48..45abde34a3 100644
a b class WP_Fatal_Error_Handler { 40 40 41 41 // If the error was stored and thus the extension paused, 42 42 // redirect the request to catch multiple errors in one go. 43 if ( $this->store_error( $error ) ) {43 if ( $this->store_error( $error ) && wp_is_recovery_mode() ) { 44 44 $this->redirect_protected(); 45 45 } 46 46 47 maybe_send_recovery_mode_email(); 48 47 49 // Display the PHP error template. 48 50 $this->display_error_template(); 49 51 } catch ( Exception $e ) { … … class WP_Fatal_Error_Handler { 83 85 * @return bool True if the error was stored successfully, false otherwise. 84 86 */ 85 87 protected function store_error( $error ) { 86 // Do not pause extensions if they only crash on a non-protected endpoint.87 if ( ! is_protected_endpoint() ) {88 return false;89 }90 91 88 return wp_record_extension_error( $error ); 92 89 } 93 90 … … class WP_Fatal_Error_Handler { 102 99 * @since 5.1.0 103 100 */ 104 101 protected function redirect_protected() { 105 // Do not redirect requests on non-protected endpoints.106 if ( ! is_protected_endpoint() ) {107 return;108 }109 110 102 // Pluggable is usually loaded after plugins, so we manually include it here for redirection functionality. 111 103 if ( ! function_exists( 'wp_redirect' ) ) { 112 104 include ABSPATH . WPINC . '/pluggable.php'; … … class WP_Fatal_Error_Handler { 171 163 'response' => 500, 172 164 'exit' => false, 173 165 ); 174 if ( function_exists( ' admin_url' ) ) {175 $args['link_url'] = admin_url();176 $args['link_text'] = __( ' Log into the admin backendto fix this.' );166 if ( function_exists( 'wp_login_url' ) ) { 167 $args['link_url'] = get_recovery_mode_request_url(); 168 $args['link_text'] = __( 'Request a Recovery Mode email to fix this.' ); 177 169 } 178 170 179 171 /** -
src/wp-includes/default-constants.php
diff --git a/src/wp-includes/default-constants.php b/src/wp-includes/default-constants.php index 1d3fd5df98..0a71a91f26 100644
a b function wp_cookie_constants() { 302 302 if ( ! defined( 'COOKIE_DOMAIN' ) ) { 303 303 define( 'COOKIE_DOMAIN', false ); 304 304 } 305 306 /** 307 * @since 5.1.0 308 */ 309 define( 'RECOVERY_MODE_COOKIE', 'wordpress_rec_' . COOKIEHASH ); 305 310 } 306 311 307 312 /** -
src/wp-includes/error-protection.php
diff --git a/src/wp-includes/error-protection.php b/src/wp-includes/error-protection.php index c9b5740960..e045efde57 100644
a b function wp_record_extension_error( $error ) { 88 88 $parts = explode( '/', $path ); 89 89 $extension = array_shift( $parts ); 90 90 91 $error['wp_is_protected'] = is_protected_endpoint(); 92 91 93 return call_user_func( $callback )->record( $extension, $error ); 92 94 } 93 95 -
src/wp-includes/load.php
diff --git a/src/wp-includes/load.php b/src/wp-includes/load.php index 7fe5566e7e..6951845f2a 100644
a b function wp_get_active_and_valid_plugins() { 701 701 * Remove plugins from the list of active plugins when we're on an endpoint 702 702 * that should be protected against WSODs and the plugin is paused. 703 703 */ 704 if ( is_protected_endpoint() ) {704 if ( wp_is_recovery_mode() ) { 705 705 $plugins = wp_skip_paused_plugins( $plugins ); 706 706 } 707 707 … … function wp_get_active_and_valid_themes() { 766 766 * Remove themes from the list of active themes when we're on an endpoint 767 767 * that should be protected against WSODs and the theme is paused. 768 768 */ 769 if ( is_protected_endpoint() ) {769 if ( wp_is_recovery_mode() ) { 770 770 $themes = wp_skip_paused_themes( $themes ); 771 771 772 772 // If no active and valid themes exist, skip loading themes. … … function wp_using_themes() { 1289 1289 return apply_filters( 'wp_using_themes', defined( 'WP_USE_THEMES' ) && WP_USE_THEMES ); 1290 1290 } 1291 1291 1292 /** 1293 * Is WordPress in Recovery Mode. 1294 * 1295 * In this mode, plugins or themes that cause WSODs will be paused. 1296 * 1297 * @since 5.1.0 1298 * 1299 * @return bool 1300 */ 1301 function wp_is_recovery_mode() { 1302 /** 1303 * Filters whether WordPress is in Recovery Mode. 1304 * 1305 * @since 5.1.0 1306 * 1307 * @param bool $wp_is_recovery_mode Whether WordPress is in recovery mode. 1308 */ 1309 return apply_filters( 'wp_is_recovery_mode', defined( 'WP_RECOVERY_MODE' ) && WP_RECOVERY_MODE ); 1310 } 1311 1312 /** 1313 * Create a recovery mode key for a user. 1314 * 1315 * @since 5.1.0 1316 * 1317 * @global PasswordHash $wp_hasher 1318 * 1319 * @return string Recovery mode key. 1320 */ 1321 function generate_and_store_recovery_mode_key() { 1322 1323 global $wp_hasher; 1324 1325 if ( ! function_exists( 'wp_generate_password' ) ) { 1326 require_once ABSPATH . WPINC . '/pluggable.php'; 1327 } 1328 1329 $key = wp_generate_password( 20, false ); 1330 1331 /** 1332 * Fires when a recovery mode key is generated for a user. 1333 * 1334 * @since 5.1.0 1335 * 1336 * @param string $key The recovery mode key. 1337 */ 1338 do_action( 'generate_recovery_mode_key', $key ); 1339 1340 if ( empty( $wp_hasher ) ) { 1341 require_once ABSPATH . WPINC . '/class-phpass.php'; 1342 $wp_hasher = new PasswordHash( 8, true ); 1343 } 1344 1345 $hashed = $wp_hasher->HashPassword( $key ); 1346 1347 update_site_option( 'recovery_key', array( 1348 'hashed_key' => $hashed, 1349 'created_at' => time(), 1350 ) ); 1351 1352 return $key; 1353 } 1354 1355 /** 1356 * Verify if the recovery mode key is correct. 1357 * 1358 * @since 5.1.0 1359 * 1360 * @param string $key The unhashed key. 1361 * 1362 * @return true|WP_Error 1363 */ 1364 function validate_recovery_mode_key( $key ) { 1365 1366 $record = get_site_option( 'recovery_key' ); 1367 1368 if ( ! $record ) { 1369 return new WP_Error( 'no_recovery_key_set', __( 'Recovery Mode not initialized.' ) ); 1370 } 1371 1372 if ( ! is_array( $record ) || ! isset( $record['hashed_key'], $record['created_at'] ) ) { 1373 return new WP_Error( 'invalid_recovery_key_format', __( 'Invalid recovery key format.' ) ); 1374 } 1375 1376 if ( ! function_exists( 'wp_check_password' ) ) { 1377 require_once ABSPATH . WPINC . '/pluggable.php'; 1378 } 1379 1380 if ( ! wp_check_password( $key, $record['hashed_key'] ) ) { 1381 return new WP_Error( 'hash_mismatch', __( 'Invalid recovery key.' ) ); 1382 } 1383 1384 $valid_for = HOUR_IN_SECONDS; 1385 1386 if ( time() > $record['created_at'] + $valid_for ) { 1387 return new WP_Error( 'key_expired', __( 'Recovery key expired.' ) ); 1388 } 1389 1390 return true; 1391 } 1392 1393 /** 1394 * A form of `wp_hash()` specific to Recovery Mode. 1395 * 1396 * We cannot use `wp_hash()` because it is defined in `pluggable.php` which is not loaded until after plugins are loaded, 1397 * which is too late to verify the recovery mode cookie. 1398 * 1399 * This tries to use the `AUTH` salts first, but if they aren't valid specific salts will be generated and stored. 1400 * 1401 * @param string $data 1402 * 1403 * @return string|false 1404 */ 1405 function recovery_mode_hash( $data ) { 1406 1407 if ( ! defined( 'AUTH_KEY' ) || AUTH_KEY === 'put your unique phrase here' ) { 1408 $auth_key = get_site_option( 'recovery_mode_auth_key' ); 1409 1410 if ( ! $auth_key ) { 1411 if ( ! function_exists( 'wp_generate_password' ) ) { 1412 require_once ABSPATH . WPINC . '/pluggable.php'; 1413 } 1414 1415 $auth_key = wp_generate_password( 64, true, true ); 1416 update_site_option( 'recovery_mode_auth_key', $auth_key ); 1417 } 1418 } else { 1419 $auth_key = AUTH_KEY; 1420 } 1421 1422 if ( ! defined( 'AUTH_SALT' ) || 'put your unique phrase here' === AUTH_SALT || $auth_key === AUTH_SALT ) { 1423 $auth_salt = get_site_option( 'recovery_mode_auth_salt' ); 1424 1425 if ( ! $auth_salt ) { 1426 if ( ! function_exists( 'wp_generate_password' ) ) { 1427 require_once ABSPATH . WPINC . '/pluggable.php'; 1428 } 1429 1430 $auth_salt = wp_generate_password( 64, true, true ); 1431 update_site_option( 'recovery_mode_auth_salt', $auth_salt ); 1432 } 1433 } else { 1434 $auth_salt = AUTH_SALT; 1435 } 1436 1437 $secret = $auth_key . $auth_salt; 1438 1439 return hash_hmac( 'sha1', $data, $secret ); 1440 } 1441 1442 /** 1443 * Generate the recovery mode cookie value. 1444 * 1445 * @since 5.1.0 1446 * 1447 * @return string 1448 */ 1449 function generate_recovery_mode_cookie() { 1450 1451 if ( ! function_exists( 'wp_generate_password' ) ) { 1452 require_once ABSPATH . WPINC . '/pluggable.php'; 1453 } 1454 1455 $to_sign = sprintf( 'recovery_mode|%s|%s', time(), wp_generate_password( 20, false ) ); 1456 $signed = recovery_mode_hash( $to_sign ); 1457 1458 return base64_encode( sprintf( '%s|%s', $to_sign, $signed ) ); 1459 } 1460 1461 /** 1462 * Set the recovery mode cookie. 1463 * 1464 * @since 5.1.0 1465 */ 1466 function set_recovery_mode_cookie() { 1467 1468 $value = generate_recovery_mode_cookie(); 1469 1470 setcookie( RECOVERY_MODE_COOKIE, $value, 0, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true ); 1471 1472 if ( COOKIEPATH !== SITECOOKIEPATH ) { 1473 setcookie( RECOVERY_MODE_COOKIE, $value, 0, SITECOOKIEPATH, COOKIE_DOMAIN, is_ssl(), true ); 1474 } 1475 } 1476 1477 /** 1478 * Clear the recovery mode cookie. 1479 * 1480 * @sicne 5.1.0 1481 */ 1482 function clear_recovery_mode_cookie() { 1483 setcookie( RECOVERY_MODE_COOKIE, ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN ); 1484 setcookie( RECOVERY_MODE_COOKIE, ' ', time() - YEAR_IN_SECONDS, SITECOOKIEPATH, COOKIE_DOMAIN ); 1485 } 1486 1487 /** 1488 * Validate the recovery mode cookie. 1489 * 1490 * @since 5.1.0 1491 * 1492 * @param string $cookie Optionally, specify the cookie value instead of fetching from the super global. 1493 * 1494 * @return true|WP_Error 1495 */ 1496 function validate_recovery_mode_cookie( $cookie = '' ) { 1497 1498 if ( ! $cookie ) { 1499 if ( empty( $_COOKIE[ RECOVERY_MODE_COOKIE ] ) ) { 1500 return new WP_Error( 'no_cookie' ); 1501 } 1502 1503 $cookie = $_COOKIE[ RECOVERY_MODE_COOKIE ]; 1504 } 1505 1506 $cookie = base64_decode( $cookie ); 1507 $parts = explode( '|', $cookie ); 1508 1509 if ( 4 !== count( $parts ) ) { 1510 return new WP_Error( 'invalid_format', __( 'Invalid cookie format.' ) ); 1511 } 1512 1513 list( , $created_at, $random, $signature ) = $parts; 1514 1515 if ( ! ctype_digit( $created_at ) ) { 1516 return new WP_Error( 'invalid_created_at', __( 'Invalid cookie format.' ) ); 1517 } 1518 1519 /** 1520 * Filter the length of time a Recovery Mode cookie is valid for. 1521 * 1522 * @since 5.1.0 1523 * 1524 * @param int $length Length in seconds. 1525 */ 1526 $length = apply_filters( 'recovery_mode_cookie_length', WEEK_IN_SECONDS ); 1527 1528 if ( time() > $created_at + $length ) { 1529 return new WP_Error( 'expired', __( 'Cookie expired.' ) ); 1530 } 1531 1532 $to_sign = sprintf( 'recovery_mode|%s|%s', $created_at, $random ); 1533 $hashed = recovery_mode_hash( $to_sign ); 1534 1535 if ( ! hash_equals( $signature, $hashed ) ) { 1536 return new WP_Error( 'signature_mismatch', __( 'Invalid cookie.' ) ); 1537 } 1538 1539 return true; 1540 } 1541 1542 /** 1543 * Handle initializing Recovery Mode and sending a Recovery Mode link. 1544 * 1545 * @since 5.1.0 1546 */ 1547 function handle_recovery_mode_actions() { 1548 1549 if ( isset( $_COOKIE[ RECOVERY_MODE_COOKIE ] ) ) { 1550 $validated = validate_recovery_mode_cookie(); 1551 1552 if ( is_wp_error( $validated ) ) { 1553 clear_recovery_mode_cookie(); 1554 1555 wp_die( $validated, '', array( 1556 'link_url' => get_recovery_mode_request_url(), 1557 'link_text' => __( 'Send a new email.' ), 1558 ) ); 1559 } 1560 1561 if ( ! defined( 'WP_RECOVERY_MODE' ) ) { 1562 define( 'WP_RECOVERY_MODE', true ); 1563 } 1564 1565 return; 1566 } 1567 1568 if ( ! isset( $GLOBALS['pagenow'] ) || 'wp-login.php' !== $GLOBALS['pagenow'] ) { 1569 return; 1570 } 1571 1572 if ( isset( $_GET['action'], $_GET['rm_key'] ) && 'begin_recovery_mode' === $_GET['action'] ) { 1573 $validated = validate_recovery_mode_key( $_GET['rm_key'] ); 1574 1575 if ( is_wp_error( $validated ) ) { 1576 wp_die( $validated, '', array( 1577 'link_url' => get_recovery_mode_request_url(), 1578 'link_text' => __( 'Send a new email.' ), 1579 ) ); 1580 } 1581 1582 set_recovery_mode_cookie(); 1583 1584 // This should be loaded by set_recovery_mode_cookie() but load it again to be safe. 1585 if ( ! function_exists( 'wp_redirect' ) ) { 1586 require_once ABSPATH . WPINC . '/pluggable.php'; 1587 } 1588 1589 $url = add_query_arg( 'action', 'begun_recovery_mode', wp_login_url() ); 1590 $message = '<script type="application/javascript">window.location = \'' . esc_js( $url ) . '\';</script>'; 1591 1592 wp_die( $message, '', array( 1593 'response' => 200, 1594 'link_url' => $url, 1595 'link_text' => __( 'Continue to Login' ), 1596 ) ); 1597 } 1598 1599 if ( isset( $_GET['action'] ) && 'request_recovery_mode' === $_GET['action'] ) { 1600 $sent = maybe_send_recovery_mode_email(); 1601 1602 if ( ! function_exists( 'wp_redirect' ) ) { 1603 require_once ABSPATH . WPINC . '/pluggable.php'; 1604 } 1605 1606 if ( is_wp_error( $sent ) ) { 1607 $message = $sent; 1608 $args = array(); 1609 } else { 1610 $message = __( 'Recovery Link sent to the Site Admin email address.' ); 1611 $args = array( 'response' => 200 ); 1612 } 1613 1614 wp_die( $message, '', $args ); 1615 exit; 1616 } 1617 } 1618 1619 /** 1620 * Get a URL to request a recovery mode link be emailed to the user. 1621 * 1622 * @since 5.1.0 1623 * 1624 * @return string 1625 */ 1626 function get_recovery_mode_request_url() { 1627 $url = add_query_arg( 'action', 'request_recovery_mode', wp_login_url() ); 1628 1629 /** 1630 * Filter the URL to request a recovery mode link be emailed to the user. 1631 * 1632 * @since 5.1.0 1633 * 1634 * @param string $url 1635 */ 1636 return apply_filters( 'recovery_mode_request_url', $url ); 1637 } 1638 1639 /** 1640 * Get a URL to begin recovery mode. 1641 * 1642 * @since 5.1.0 1643 * 1644 * @param string $key Recovery Mode key created by {@see generate_and_store_recovery_mode_key()} 1645 * 1646 * @return string 1647 */ 1648 function get_recovery_mode_begin_url( $key ) { 1649 1650 $url = add_query_arg( array( 1651 'action' => 'begin_recovery_mode', 1652 'rm_key' => $key, 1653 ), wp_login_url() ); 1654 1655 /** 1656 * Filter the URL to begin recovery mode. 1657 * 1658 * @since 5.1.0 1659 * 1660 * @param string $url 1661 * @param string $key 1662 */ 1663 return apply_filters( 'recovery_mode_begin_url', $url, $key ); 1664 } 1665 1666 /** 1667 * Send the recovery mode email if the rate limit has not been sent. 1668 * 1669 * @since 5.1.0 1670 * 1671 * @return true|WP_Error True if email sent, WP_Error otherwise. 1672 */ 1673 function maybe_send_recovery_mode_email() { 1674 1675 /** 1676 * Filter the rate limit between sending new recovery mode email links. 1677 * 1678 * @since 5.1.0 1679 * 1680 * @param int $rate_limit Time to wait in seconds. 1681 */ 1682 $rate_limit = apply_filters( 'recovery_mode_email_rate_limit', HOUR_IN_SECONDS ); 1683 1684 $last_sent = get_site_option( 'recovery_mode_email_last_sent' ); 1685 1686 if ( ! $last_sent || time() > $last_sent + $rate_limit ) { 1687 $sent = send_recovery_mode_email(); 1688 update_site_option( 'recovery_mode_email_last_sent', time() ); 1689 1690 if ( $sent ) { 1691 return true; 1692 } 1693 1694 return new WP_Error( 'email_failed', __( 'The email could not be sent. Possible reason: your host may have disabled the mail() function.' ) ); 1695 } 1696 1697 $error = sprintf( 1698 /* translators: 1. Last sent as a human time diff 2. Wait time as a human time diff. */ 1699 __( 'A recovery link was already sent %1$s ago. Please wait another %2$s before requesting a new email.' ), 1700 human_time_diff( $last_sent ), 1701 human_time_diff( $last_sent + $rate_limit ) 1702 ); 1703 1704 return new WP_Error( 'email_sent_already', $error ); 1705 } 1706 1707 /** 1708 * Send the Recovery Mode email to the site admin email address. 1709 * 1710 * @since 5.1.0 1711 * 1712 * @return bool Whether the email was sent successfully. 1713 */ 1714 function send_recovery_mode_email() { 1715 1716 $key = generate_and_store_recovery_mode_key(); 1717 $url = get_recovery_mode_begin_url( $key ); 1718 $blogname = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ); 1719 1720 $switched_locale = false; 1721 1722 // The switch_to_locale() function is loaded before it can actually be used. 1723 if ( function_exists( 'switch_to_locale' ) && isset( $GLOBALS['wp_locale_switcher'] ) ) { 1724 $switched_locale = switch_to_locale( get_locale() ); 1725 } 1726 1727 $message = __( 1728 'Howdy, 1729 1730 Your site recently experienced a fatal error. Click the link below to initiate recovery mode to fix the problem. 1731 1732 This link expires in one hour. 1733 1734 ###LINK###' 1735 ); 1736 $message = str_replace( '###LINK###', $url, $message ); 1737 1738 $email = array( 1739 'to' => get_option( 'admin_email' ), 1740 'subject' => __( '[%s] Recovery Mode' ), 1741 'message' => $message, 1742 'headers' => '', 1743 ); 1744 1745 /** 1746 * Filter the contents of the Recovery Mode email. 1747 * 1748 * @since 5.1.0 1749 * 1750 * @param array $email Used to build wp_mail(). 1751 * @param string $key Recovery mode key. 1752 */ 1753 $email = apply_filters( 'recovery_mode_email', $email, $key ); 1754 1755 $sent = wp_mail( 1756 $email['to'], 1757 wp_specialchars_decode( sprintf( $email['subject'], $blogname ) ), 1758 $email['message'], 1759 $email['headers'] 1760 ); 1761 1762 if ( $switched_locale ) { 1763 restore_previous_locale(); 1764 } 1765 1766 return $sent; 1767 } 1768 1292 1769 /** 1293 1770 * Determines whether we are currently on an endpoint that should be protected against WSODs. 1294 1771 * -
src/wp-includes/ms-load.php
diff --git a/src/wp-includes/ms-load.php b/src/wp-includes/ms-load.php index 4f630cce27..91c9c8c301 100644
a b function wp_get_active_network_plugins() { 53 53 } 54 54 } 55 55 56 /*57 * Remove plugins from the list of active plugins when we're on an endpoint58 * that should be protected against WSODs and the plugin is paused.59 */60 if ( is_protected_endpoint() ) {61 $plugins = wp_skip_paused_plugins( $plugins );62 }63 64 56 return $plugins; 65 57 } 66 58 -
src/wp-includes/pluggable.php
diff --git a/src/wp-includes/pluggable.php b/src/wp-includes/pluggable.php index 0e9d4ad2f0..90b9dba6df 100644
a b if ( ! function_exists( 'wp_clear_auth_cookie' ) ) : 979 979 980 980 // Post password cookie 981 981 setcookie( 'wp-postpass_' . COOKIEHASH, ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN ); 982 983 clear_recovery_mode_cookie(); 982 984 } 983 985 endif; 984 986 -
src/wp-login.php
diff --git a/src/wp-login.php b/src/wp-login.php index 1a302a35f1..4171e9065b 100644
a b if ( isset( $_GET['key'] ) ) { 438 438 } 439 439 440 440 // Validate action so as to default to the login screen. 441 if ( ! in_array( $action, array( 'postpass', 'logout', 'lostpassword', 'retrievepassword', 'resetpass', 'rp', 'register', 'login', 'confirmaction' ), true ) && false === has_filter( 'login_form_' . $action ) ) {441 if ( ! in_array( $action, array( 'postpass', 'logout', 'lostpassword', 'retrievepassword', 'resetpass', 'rp', 'register', 'login', 'confirmaction', 'begun_recovery_mode' ), true ) && false === has_filter( 'login_form_' . $action ) ) { 442 442 $action = 'login'; 443 443 } 444 444 … … switch ( $action ) { 1024 1024 $errors->add( 'registered', __( 'Registration complete. Please check your email.' ), 'message' ); 1025 1025 } elseif ( strpos( $redirect_to, 'about.php?updated' ) ) { 1026 1026 $errors->add( 'updated', __( '<strong>You have successfully updated WordPress!</strong> Please log back in to see what’s new.' ), 'message' ); 1027 } elseif ( 'begun_recovery_mode' === $action ) { 1028 $errors->add( 'begun_recovery_mode', __( 'Recovery Mode Initialized. Please login to continue.' ), 'message' ); 1027 1029 } 1028 1030 } 1029 1031 -
src/wp-settings.php
diff --git a/src/wp-settings.php b/src/wp-settings.php index e48208beb6..60f1e50485 100644
a b wp_start_scraping_edited_file_errors(); 342 342 // Register the default theme directory root 343 343 register_theme_directory( get_theme_root() ); 344 344 345 // Handle users requesting a recovery mode link and initiating recovery mode. 346 handle_recovery_mode_actions(); 347 345 348 // Load active plugins. 346 349 foreach ( wp_get_active_and_valid_plugins() as $plugin ) { 347 350 wp_register_plugin_realpath( $plugin ); -
new file tests/phpunit/tests/recovery-mode.php
diff --git a/tests/phpunit/tests/recovery-mode.php b/tests/phpunit/tests/recovery-mode.php new file mode 100644 index 0000000000..a7d2a69781
- + 1 <?php 2 3 class Tests_Recovery_Mode extends WP_UnitTestCase { 4 5 private static $subscriber; 6 private static $administrator; 7 8 public static function setUpBeforeClass() { 9 self::$subscriber = self::factory()->user->create( array( 'role' => 'subscriber' ) ); 10 self::$administrator = self::factory()->user->create( array( 'role' => 'administrator' ) ); 11 12 return parent::setUpBeforeClass(); 13 } 14 15 public static function tearDownAfterClass() { 16 wp_delete_user( self::$subscriber ); 17 wp_delete_user( self::$administrator ); 18 19 return parent::tearDownAfterClass(); 20 } 21 22 public function test_generate_and_store_returns_recovery_key() { 23 $key = generate_and_store_recovery_mode_key(); 24 25 $this->assertNotWPError( $key ); 26 } 27 28 public function test_verify_recovery_mode_key_returns_wp_error_if_no_key_set() { 29 $error = validate_recovery_mode_key( 'abcd' ); 30 31 $this->assertWPError( $error ); 32 $this->assertEquals( 'no_recovery_key_set', $error->get_error_code() ); 33 } 34 35 public function test_verify_recovery_mode_key_returns_wp_error_if_stored_format_is_invalid() { 36 update_site_option( 'recovery_key', 'gibberish' ); 37 $error = validate_recovery_mode_key( 'abcd' ); 38 39 $this->assertWPError( $error ); 40 $this->assertEquals( 'invalid_recovery_key_format', $error->get_error_code() ); 41 } 42 43 public function test_verify_recovery_mode_key_returns_wp_error_if_empty_key() { 44 generate_and_store_recovery_mode_key(); 45 $error = validate_recovery_mode_key( '' ); 46 47 $this->assertWPError( $error ); 48 $this->assertEquals( 'hash_mismatch', $error->get_error_code() ); 49 } 50 51 public function test_verify_recovery_mode_key_returns_wp_error_if_hash_mismatch() { 52 generate_and_store_recovery_mode_key(); 53 $error = validate_recovery_mode_key( 'abcd' ); 54 55 $this->assertWPError( $error ); 56 $this->assertEquals( 'hash_mismatch', $error->get_error_code() ); 57 } 58 59 public function test_verify_recovery_mode_key_returns_wp_error_if_expired() { 60 $key = generate_and_store_recovery_mode_key(); 61 62 $record = get_site_option( 'recovery_key' ); 63 $record['created_at'] = time() - HOUR_IN_SECONDS - 30; 64 update_site_option( 'recovery_key', $record ); 65 66 $error = validate_recovery_mode_key( $key ); 67 68 $this->assertWPError( $error ); 69 $this->assertEquals( 'key_expired', $error->get_error_code() ); 70 } 71 72 public function test_verify_recovery_mode_key_returns_true_for_valid_key() { 73 74 $key = generate_and_store_recovery_mode_key(); 75 $this->assertTrue( validate_recovery_mode_key( $key ) ); 76 } 77 78 public function test_validate_recovery_mode_cookie_returns_wp_error_if_invalid_format() { 79 80 $error = validate_recovery_mode_cookie( 'gibbersih' ); 81 $this->assertWPError( $error ); 82 $this->assertEquals( 'invalid_format', $error->get_error_code() ); 83 84 $error = validate_recovery_mode_cookie( base64_encode( 'test|data|format' ) ); 85 $this->assertWPError( $error ); 86 $this->assertEquals( 'invalid_format', $error->get_error_code() ); 87 88 $error = validate_recovery_mode_cookie( base64_encode( 'test|data|format|to|long' ) ); 89 $this->assertWPError( $error ); 90 $this->assertEquals( 'invalid_format', $error->get_error_code() ); 91 } 92 93 public function test_validate_recovery_mode_cookie_returns_wp_error_if_expired() { 94 95 $to_sign = sprintf( 'recovery_mode|%s|%s', time() - WEEK_IN_SECONDS - 30, wp_generate_password( 20, false ) ); 96 $signed = recovery_mode_hash( $to_sign ); 97 $cookie = base64_encode( sprintf( '%s|%s', $to_sign, $signed ) ); 98 99 $error = validate_recovery_mode_cookie( $cookie ); 100 $this->assertWPError( $error ); 101 $this->assertEquals( 'expired', $error->get_error_code() ); 102 } 103 104 public function test_validate_recovery_mode_cookie_returns_wp_error_if_signature_mismatch() { 105 106 $cookie = generate_recovery_mode_cookie(); 107 $cookie .= 'gibbersih'; 108 109 $error = validate_recovery_mode_cookie( $cookie ); 110 $this->assertWPError( $error ); 111 $this->assertEquals( 'signature_mismatch', $error->get_error_code() ); 112 } 113 114 public function test_validate_recovery_mode_cookie_returns_wp_error_if_created_at_is_invalid_format() { 115 116 $to_sign = sprintf( 'recovery_mode|%s|%s', 'month', wp_generate_password( 20, false ) ); 117 $signed = recovery_mode_hash( $to_sign ); 118 $cookie = base64_encode( sprintf( '%s|%s', $to_sign, $signed ) ); 119 120 $error = validate_recovery_mode_cookie( $cookie ); 121 $this->assertWPError( $error ); 122 $this->assertEquals( 'invalid_created_at', $error->get_error_code() ); 123 } 124 125 public function test_generate_and_validate_recovery_mode_cookie_returns_true_for_valid_cookie() { 126 127 $cookie = generate_recovery_mode_cookie(); 128 $this->assertTrue( validate_recovery_mode_cookie( $cookie ) ); 129 } 130 }