WordPress.org

Make WordPress Core

Ticket #47954: 47954.2.diff

File 47954.2.diff, 17.0 KB (added by donmhico, 2 years ago)

Make the expiry time filterable.

  • new file src/wp-includes/class-wp-restore-siteurl.php

    diff --git src/wp-includes/class-wp-restore-siteurl.php src/wp-includes/class-wp-restore-siteurl.php
    new file mode 100644
    index 0000000000..e007271599
    - +  
     1<?php
     2/**
     3 * Handles the restore siteurl operation when the user updates either 'home' or 'siteurl' options
     4 * and wants to revert it back.
     5 *
     6 * @since 5.3.0
     7 *
     8 * @link https://core.trac.wordpress.org/ticket/47954
     9 */
     10class WP_Restore_Siteurl {
     11    /**
     12     * Restore key to authenticate the restore siteurl request.
     13     *
     14     * @since 5.3.0
     15     * @var string
     16     */
     17    private $restore_key = null;
     18
     19    /**
     20     * Is the restore siteurl email sent to the user.
     21     *
     22     * @since 5.3.0
     23     * @var boolean
     24     */
     25    private $is_restore_siteurl_email_sent = false;
     26
     27    /**
     28     * Old 'home' option value.
     29     *
     30     * @since 5.3.0
     31     * @var string
     32     */
     33    private $old_home_value = null;
     34
     35    /**
     36     * Hook the functions.
     37     *
     38     * @since 5.3.0
     39     */
     40    public function __construct() {
     41        add_action( 'registered_taxonomy', array( $this, 'perform_siteurl_restore' ) );
     42        add_action( 'updated_option', array( $this, 'setup_siteurl_restore' ), 10, 3 );
     43        add_action( 'admin_init', array( $this, 'restore_siteurl_success_notice' ) );
     44    }
     45
     46    /**
     47     * Generates a restore key.
     48     *
     49     * @since 5.3.0
     50     *
     51     * @return string $restore_key A random string to authenticate the restore siteurl request.
     52     */
     53    private function generate_restore_key() {
     54        if ( null === $this->restore_key ) {
     55            $this->restore_key = wp_generate_password( 16, false );
     56        }
     57
     58        return $this->restore_key;
     59    }
     60
     61    /**
     62     * Listen to updates for 'home' and 'siteurl' options.
     63     *
     64     * This function performs the following tasks when either 'home' and 'siteurl' options are updated:
     65     * 1. Caches the old 'home' value if it's the option updated.
     66     * 2. Creates a 'siteurl_restore_key' transient that will be used to authenticate the restore operation.
     67     * 3. Backups the old 'home' or 'siteurl' values.
     68     * 4. Sends an email to the admin with the restore link.
     69     *
     70     * @since 5.3.0
     71     *
     72     * @param string $option    Name of the updated option.
     73     * @param string $old_value The old option value.
     74     * @param string $value     The new option value.
     75     */
     76    public function setup_siteurl_restore( $option, $old_value, $value ) {
     77        if ( 'siteurl' === $option || 'home' === $option ) {
     78            // Prevent Restore Siteurl protocol if the update is the restoration.
     79            $backup_value = get_transient( "old_{$option}" );
     80            if ( $value === $backup_value ) {
     81                return;
     82            }
     83
     84            // Backup the old 'home' option value.
     85            if ( 'home' === $option ) {
     86                $this->old_home_value = $old_value;
     87            }
     88
     89            $default_expiry_time = 30 * MINUTE_IN_SECONDS;
     90            /**
     91             * Filters the expiry time of the restore siteurl link.
     92             *
     93             * @since 5.3.0
     94             *
     95             * @param string $expiry_time Expiry time of the restore siteurl link.
     96             */
     97            $expiry_time = apply_filters( 'restore_siteurl_expiry_time', $default_expiry_time );
     98
     99            $restore_key_transient = get_transient( 'siteurl_restore_key' );
     100            $set_restore_key_transient = false;
     101            if ( $this->generate_restore_key() != $restore_key_transient ) {
     102                // Create a restore key transient.
     103                $set_restore_key_transient = set_transient( 'siteurl_restore_key', $this->generate_restore_key(), $expiry_time );
     104            }
     105
     106            // Keep a backup of the old `siteurl` and `home`.
     107            delete_transient( "old_{$option}" );
     108            $set_backup_transient = set_transient( "old_{$option}", $old_value, $expiry_time );
     109           
     110            if ( $set_restore_key_transient && $set_backup_transient ) {
     111                $this->send_restore_link_email_to_admin();
     112            }
     113        }
     114    }
     115
     116    /**
     117     * Send restore link email to admin.
     118     *
     119     * @since 5.3.0
     120     */
     121    private function send_restore_link_email_to_admin() {
     122        if ( ! $this->is_restore_siteurl_email_sent ) {
     123            $restore_link = add_query_arg( [
     124                'srk' => $this->generate_restore_key(),
     125            ], $this->get_old_home_value() );
     126
     127            $admin_email = get_option( 'admin_email' );
     128            /**
     129             * Filters the subject of the restore siteurl email.
     130             *
     131             * @since 5.3.0
     132             *
     133             * @param string $email_subject The subject of the restore siteurl email.
     134             */
     135            $email_subject = apply_filters( 'restore_siteurl_email_subject', __( 'Your WordPress site url was changed.' ) );
     136            /**
     137             * Filters the content body of the restore siteurl email.
     138             *
     139             * @since 5.3.0
     140             *
     141             * @param string $email_content The content body of the restore siteurl email.
     142             */
     143            $email_content = apply_filters(
     144                'restore_siteurl_email_message',
     145                __( 'You can undo this change by clicking this link ###restore_link###' )
     146            );
     147
     148            // Replace placeholder with actual restore link.
     149            $email_content = str_replace( '###restore_link###', $restore_link, $email_content );
     150
     151            $email = wp_mail( $admin_email, $email_subject, $email_content );
     152
     153            if ( $email ) {
     154                $this->is_restore_siteurl_email_sent = true;
     155            }
     156
     157            /**
     158             * Fires after the attempt to send restore link email to admin.
     159             *
     160             * @since 5.3.0
     161             *
     162             * @param boolean $email Whether the email is sent successfully.
     163             */
     164            do_action( 'send_restore_link_email', $email );
     165        }
     166    }
     167
     168    /**
     169     * Get the old 'home' option value.
     170     *
     171     * @since 5.3.0
     172     *
     173     * @return string $old_home_value Old 'home' value.
     174     */
     175    public function get_old_home_value() {
     176        if ( null !== $this->old_home_value ) {
     177            return esc_url( $this->old_home_value );
     178        }
     179
     180        $old_home_transient = get_transient( 'old_home' );
     181        if ( $old_home_transient !== false ) {
     182            $this->old_home_value = $old_home_transient;
     183        }
     184        else {
     185            $this->old_home_value = get_option( 'home' );
     186        }
     187       
     188        return esc_url( $this->old_home_value );
     189    }
     190
     191    /**
     192     * Perform the siteurl restore operation.
     193     *
     194     * If the provided restore key is invalid, the process is terminated using `wp_die()`.
     195     * Else if the restore key is valid, then it will perform the restore siteurl operation.
     196     *
     197     * If the restore siteurl operation is a success, it will perform the following.
     198     * 1. Delete the backup transients.
     199     * 2. Create a success-tracker transient named 'siteurl_restore_success'.
     200     * 3. Redirect the user to the old admin dashboard url.
     201     *
     202     * @since 5.3.0
     203     */
     204    public function perform_siteurl_restore() {
     205        if ( isset( $_GET['srk'] ) && ! empty( $_GET['srk'] ) ) {
     206            $restore_key = get_transient( 'siteurl_restore_key' );
     207
     208            if ( $_GET['srk'] === $restore_key ) {
     209                $old_siteurl = get_transient( 'old_siteurl' );
     210                $old_home        = get_transient( 'old_home' );
     211
     212                $is_restore_success = false;
     213
     214                if ( $old_siteurl ) {
     215                    $update_siteurl = update_option( 'siteurl', $old_siteurl );
     216
     217                    if ( $update_siteurl ) {
     218                        delete_transient( 'old_siteurl' );
     219
     220                        $is_restore_success = true;
     221                    }
     222                }
     223
     224                if ( $old_home ) {
     225                    $update_home = update_option( 'home', $old_home );
     226
     227                    if ( $update_home ) {
     228                        delete_transient( 'old_home' );
     229
     230                        $is_restore_success = true;
     231                    }
     232                }
     233
     234                if ( $is_restore_success ) {
     235                    delete_transient( 'siteurl_restore_key' );
     236
     237                    // Track success.
     238                    set_transient( 'siteurl_restore_success', '1', 300 );
     239
     240                    $this->success_redirect();
     241                }
     242            }
     243            else {
     244                wp_die( __( 'Restore key is invalid.' ) );
     245                exit;
     246            }
     247        }
     248    }
     249
     250    /**
     251     * Redirect the user to admin dashboard with restore siteurl success $_GET param.
     252     *
     253     * @since 5.3.0
     254     */
     255    protected function success_redirect() {
     256        $restored_site_admin_url = trailingslashit( get_option( 'home' ) ) . 'wp-admin';
     257
     258        // Success redirect url.
     259        $success_redirect_url = add_query_arg( 'srsuccess', '1', $restored_site_admin_url );
     260
     261        wp_redirect( $success_redirect_url );
     262        exit;
     263    }
     264
     265    /**
     266     * Display success notice if the restore siteurl operation is a success.
     267     *
     268     * @since 5.3.0
     269     */
     270    public function restore_siteurl_success_notice() {
     271        if ( isset( $_GET['srsuccess'] ) && '1' === $_GET['srsuccess'] ) {
     272            $restore_success = get_transient( 'siteurl_restore_success' );
     273
     274            if ( '1' === $restore_success ) {
     275                add_action( 'admin_notices', [ $this, 'restore_siteurl_success_notice__success' ] );
     276
     277                delete_transient( 'siteurl_restore_success' );
     278            }
     279        }
     280    }
     281
     282    /**
     283     * Restore siteurl success notice.
     284     *
     285     * @since 5.3.0
     286     */
     287    public function restore_siteurl_success_notice__success() {
     288        $class = 'notice notice-success';
     289        $message = __( 'Your WordPress site url was successfully restored.' );
     290
     291        printf( '<div class="%1$s"><p>%2$s</p></div>', esc_attr( $class ), esc_html( $message ) );
     292    }
     293}
     294 No newline at end of file
  • src/wp-includes/functions.php

    diff --git src/wp-includes/functions.php src/wp-includes/functions.php
    index f32e236778..6be7a39db7 100644
    function is_wp_version_compatible( $required ) { 
    73167316function is_php_version_compatible( $required ) {
    73177317        return empty( $required ) || version_compare( phpversion(), $required, '>=' );
    73187318}
     7319
     7320/**
     7321 * Access the WP Restore Siteurl instance.
     7322 *
     7323 * @since 5.3.0
     7324 *
     7325 * @return WP_Restore_Siteurl
     7326 */
     7327function wp_restore_siteurl() {
     7328        static $wp_restore_siteurl;
     7329
     7330        if ( ! $wp_restore_siteurl ) {
     7331                $wp_restore_siteurl = new WP_Restore_Siteurl();
     7332        }
     7333
     7334        return $wp_restore_siteurl;
     7335}
     7336 No newline at end of file
  • src/wp-settings.php

    diff --git src/wp-settings.php src/wp-settings.php
    index 681053a0d4..70e0965d89 100644
    require_once( ABSPATH . WPINC . '/class-wp-locale-switcher.php' ); 
    151151wp_not_installed();
    152152
    153153// Load most of WordPress.
     154require( ABSPATH . WPINC . '/class-wp-restore-siteurl.php' );
    154155require( ABSPATH . WPINC . '/class-wp-walker.php' );
    155156require( ABSPATH . WPINC . '/class-wp-ajax-response.php' );
    156157require( ABSPATH . WPINC . '/capabilities.php' );
    unset( $plugin ); 
    373374require( ABSPATH . WPINC . '/pluggable.php' );
    374375require( ABSPATH . WPINC . '/pluggable-deprecated.php' );
    375376
     377// Load the WP Restore Siteurl instance.
     378wp_restore_siteurl();
     379
    376380// Set internal encoding.
    377381wp_set_internal_encoding();
    378382
  • new file tests/phpunit/tests/restore-siteurl/restore-siteurl.php

    diff --git tests/phpunit/tests/restore-siteurl/restore-siteurl.php tests/phpunit/tests/restore-siteurl/restore-siteurl.php
    new file mode 100644
    index 0000000000..f151037c3e
    - +  
     1<?php
     2/**
     3 * @group restore-siteurl
     4 */
     5class Tests_Restore_Siteurl extends WP_UnitTestCase {
     6
     7    /**
     8     * Tracker of how many times the action `send_restore_link_email` is invoked.
     9     *
     10     * @var integer
     11     */
     12    private $test_opt_ctr = 1;
     13
     14    /**
     15     * Restore siteurl email should only be sent once.
     16     *
     17     * In this test, 'test_opt' option is added with the value of `$this->test_opt_ctr`.
     18     * Then function `$this->change_test_opt_value()` that increments the value of `test_opt`
     19     * is hooked in `send_restore_link_email` action.
     20     *
     21     * Since it is expected that the email will only be sent once, then `send_restore_link_email`
     22     * action should only be invoked once. Hence the expected value of `test_opt` value should
     23     * be 1.
     24     */
     25    public function test_restore_link_email_should_only_be_sent_once() {
     26        update_option( 'test_opt', $this->test_opt_ctr );
     27
     28        add_action( 'send_restore_link_email', array( $this, 'change_test_opt_value' ) );
     29
     30        update_option( 'home', 'http://wp.org' );
     31        update_option( 'site_url', 'http://wp.org' );
     32
     33        $test_opt = get_option( 'test_opt' );
     34
     35        $this->assertEquals( '1', $test_opt );
     36    }
     37
     38    public function change_test_opt_value( $email ) {
     39        update_option( 'test_opt', $this->test_opt_ctr );
     40        $this->test_opt_ctr += 1;
     41    }
     42
     43    public function test_updating_home_option_should_create_backup_transient() {
     44        update_option( 'home', 'http://wp.org' );
     45        $a = get_option( 'home' );
     46        $actual = get_transient( 'old_home' );
     47        $this->assertNotFalse( $actual );
     48    }
     49
     50    public function test_updating_siteurl_option_should_create_backup_transient() {
     51        update_option( 'siteurl', 'http://wp.org' );
     52        $a = get_option( 'siteurl' );
     53        $actual = get_transient( 'old_siteurl' );
     54        $this->assertNotFalse( $actual );
     55    }
     56
     57    public function test_restore_key_should_not_change_when_both_options_are_updated() {
     58        update_option( 'home', 'http://wp.org' );
     59
     60        $wp_restore_siteurl = wp_restore_siteurl();
     61
     62        $reflection = new ReflectionClass( $wp_restore_siteurl );
     63        $method     = $reflection->getMethod( 'generate_restore_key' );
     64        $method->setAccessible( true );
     65
     66        $restore_key           = $method->invokeArgs( $wp_restore_siteurl, array() );
     67        $transient_restore_key = get_transient( 'siteurl_restore_key' );
     68
     69        update_option( 'siteurl', 'http://wp.org' );
     70
     71        $this->assertEquals( $restore_key, $transient_restore_key );
     72    }
     73
     74    /**
     75     * @expectedException        WPDieException
     76     * @expectedExceptionMessage Restore key is invalid.
     77     */
     78    public function test_invalid_restore_key_should_be_blocked() {
     79        update_option( 'home', 'http://wp.org' );
     80        update_option( 'siteurl', 'http://wp.org' );
     81
     82        // Use invalid restore key.
     83        $_GET['srk'] = 'this-is-invalid.';
     84
     85        $wp_restore_siteurl = wp_restore_siteurl();
     86        $wp_restore_siteurl->perform_siteurl_restore();
     87    }
     88
     89    public function test_success_perform_siteurl_restore() {
     90        update_option( 'home', 'http://wp.org' );
     91        update_option( 'siteurl', 'http://wp.org' );
     92
     93        // Use the correct restore keys.
     94        $_GET = array();
     95        $_GET['srk'] = get_transient( 'siteurl_restore_key' );
     96
     97        // Test that success restore will perform redirection.
     98        $wp_restore_siteurl = $this->getMockBuilder( 'WP_Restore_Siteurl' )
     99                                ->setMethods( array( 'success_redirect' ) )
     100                                ->getMock();
     101        $wp_restore_siteurl->expects( $this->once() )
     102            ->method( 'success_redirect' );
     103
     104        $wp_restore_siteurl->perform_siteurl_restore();
     105
     106        $this->assertFalse( get_transient( 'old_home' ) );
     107        $this->assertFalse( get_transient( 'old_siteurl' ) );
     108        $this->assertFalse( get_transient( 'siteurl_restore_key' ) );
     109       
     110        $this->assertEquals( '1', get_transient( 'siteurl_restore_success' ) );
     111    }
     112
     113    public function test_restore_siteurl_success_notice() {
     114        update_option( 'home', 'http://wp.org' );
     115        update_option( 'siteurl', 'http://wp.org' );
     116
     117        // Use the correct restore keys.
     118        $_GET = array();
     119        $_GET['srk'] = get_transient( 'siteurl_restore_key' );
     120
     121        // Perform the siteurl restore.
     122        $wp_restore_siteurl = $this->getMockBuilder( 'WP_Restore_Siteurl' )
     123                                ->setMethods( array( 'success_redirect' ) )
     124                                ->getMock();
     125        $wp_restore_siteurl->perform_siteurl_restore();
     126
     127        // Simulate click to success url.
     128        $_GET = array();
     129        $_GET['srsuccess'] = '1';
     130        $wp_restore_siteurl->restore_siteurl_success_notice();
     131
     132        $is_success_notice_hooked = has_action( 'admin_notices', [ $wp_restore_siteurl, 'restore_siteurl_success_notice__success'] );
     133
     134        $this->assertEquals( 10, $is_success_notice_hooked );
     135
     136        $success_transient = get_transient( 'siteurl_restore_success' );
     137
     138        $this->assertFalse( $success_transient );
     139    }
     140}
     141 No newline at end of file