schoenwaldnils
7/5/2015 - 1:48 AM

Create a WordPress staging area from your live wordpress setup with a Click-of-a-button [TM] ..

Create a WordPress staging area from your live wordpress setup with a Click-of-a-button [TM] ..

<?php
/**
 *
 * This script will copy your wordpress from public_html (or wherever)
 * and place it in a staging folder.
 * It will then clone the database, reconfigure the config file
 * and replace URL's from the original URL to your staging URL.
 * It will then make sure to NOT allow search engines to index the page.
 *
 * Use this script to clone your main wp in order to test maintenance work
 * like updating the site, removing plugins, adding new plugins
 * and so on..
 *
 * If the staging area already exists, this script will remove the staging
 * area and install a fresh copy.
 *
 * There really should be some error handling and testing .. I may add that in later
 * but for now, this works for me so I'm not adding any safeguards unless YOU let me
 * know of something that broke.
 *
 * Feel free to hack this code, fix it or jazz it.
 * If you find a bug - let me know .. maybe I'll turn this into a repo and accept PR's if I'm up for it.
 *
 * If you need to contact me, you can reach me at github/twitter @sigginet or siggy@quickfalcon.com
 *
 * If this helped you and you feel the need to repay me - buy me a book! I love to read! :)
 * http://www.amazon.com/gp/registry/wishlist/1HSVVQQ3YGNGY/
 *
 *
 *
 ****************
 * INSTRUCTIONS *
 ****************
 *
 * ASSUMPTIONS/REQUIREMENTS: (skip this if you want to fail)
 * 1. You've created a DNS record (or modified your hosts file) for the staging URL.
 * 2. You've created an empty folder for your staging area.
 * 3. You've mapped your staging URL to the staging area on your server.
 * 4. You've created an empty staging database that the live database user has full control over.
 * 5. You're running PHP 5.3+. If not - BE ASHAMED OF YOURSELF AND UPDATE! Use HHVM if you want better performance.
 *  a. If you're using 5.3 then you REALLY need to update - it went out of date 14 Aug 2014 but 
 *     major hosting companies STILL use this php version on their hosts.
 * 6. You've either got PDO mysql or mysqli. The script will die otherwise.
 * 
 * INSTRUCTIONS:
 * 1. Read the assumptions part above and make sure you've met the minimum requirements
 * 2. Create a staging area folder (see my example structure below)
 * 3. Make sure that the HTTP server can read and write to that folder
 * 4. Create a staging database
 * 5. Make sure the LIVE database user has full access to the staging database
 * 6. Create a staging URL (it can be staging.example.com)
 * 7. Make sure the staging URL points to the staging folder
 * 8. Create a "staging" subfolder under your LIVE website
 * 9. Configure the variables below
 * 10. Save this file as index.php in the "staging" subfolder.
 * 11. Visit http://yourlivesite.com/staging/ and press the button .. This might take a minute.
 * 12. Enjoy your new staging area.
 * 13. Feel free to visit http://yourlivesite.com/staging/ every time you need to refresh the staging area.
 * 
 * 
 *
 * MY EXAMPLE FOLDER STRUCTURE:
 * - home
 *  '- public_html    (WP is here)
 *    '- staging      (Put the staging script here, call it index.php)
 *  '- staging_area   (Staging area for WP)
 *
 * MY EXAMPLE DATABASE STRUCTURE:
 *  - live_db         (the live WP database)
 *  - staging_db      (the staging WP database - will be truncated and overwritten)
 *
 *
 **/

//
// Set the variables
//

// $protocol - the transfer protocol used for both staging and the live site
// $livesite - the live website URL
// $stagingsite - the staging site URL
// $livedb - the database name of the live site - grabbed from wp-config.php's DB_NAME definition
// $stagingdb - the database name of the staging site - the one you created. We assume the same DB user.
// $homedir - currently points to the parent folder (expects to be 1st level subfolder inside the WP directory)
// $subfolder - the subfolder of the live site where the staging php application sits (the folder containing this file)
// $stagingdir - the folder that should contain staging area - usually a subfolder or sidefolder of public_html

$protocol = 'http';
$livesite = 'www.example.com';
$livesiteproto = 'http://';
$stagingsite = 'staging.example.com';
$stagingsiteproto = 'http://';
$livedb = DB_NAME;
$stagingdb = 'example_staging';
$homedir = dirname(__DIR__);
$subfolder = 'staging';
$stagingdir = dirname($homedir) . '/staging_area';



//
// DO NOT EDIT THE CODE BELOW (unless you know what you're doing)
//

$mysqli_available = class_exists( 'mysqli' );
$pdo_available    = class_exists( 'PDO'    );
$connection_type = '';

if ( $mysqli_available ) {
    $connection_type = 'mysqli';
}

if ( $pdo_available ) {
    $mysql_driver_present = in_array( 'mysql', pdo_drivers() );
    if ( $mysql_driver_present ) {
        $connection_type = 'pdo';
    }
}

// Abort if mysqli and PDO are both broken.
if ( '' === $connection_type ) {
    die("Could not find any MySQL database drivers. (MySQLi or PDO required.)");
}

include('../wp-load.php');


//
//  Re-direct not-logged-in users to holding page
//
if(!is_user_logged_in()) {
    wp_redirect( $protocol.'://'.$livesite.'/wp-login.php?redirect_to='.$protocol.'://'.$livesite.'/'.$subfolder.'/', 302 );
    exit;
}

//
// Check if the user is an administrator
//
if ( ! current_user_can( 'manage_options' )) {
    die("Sorry buddy, you don't have permission to be here..");
}


//
// Great! Now, let's check if we're asked to create the staging area
//

if ( !empty($_POST['createstagingarea']) ) {
    // Create the staging area
    echo_immediately("<html><body>");

    // Set up the database
    if ($connection_type == 'pdo') {
        $dbconn = new PDO("mysql:host=".DB_HOST.";dbname=".$stagingdb, DB_USER, DB_PASSWORD);
    } elseif ($connection_type == 'mysqli') {
        $dbconn = new mysqli(DB_HOST, DB_USER, DB_PASSWORD, $stagingdb);
    }
    
    global $dbconn;
    global $stagingdb;
    global $livedb;


    // Remove the staging folder contents
    echo_immediately("Removing the staging area files<br>");
    rrmdir($stagingdir);
    
    // Drop the database
    echo_immediately("Removing the old database<br>");
    cleandb();
    
    // Copy the new database
    echo_immediately("Copying the database to the staging database<br>");
    clonedb($livedb, $stagingdb);
    
    // Copy the files
    echo_immediately("Copying the files to the staging area<br>");
    rcopy($homedir, $stagingdir);
    
    // Modify the config file, change the database name
    echo_immediately("Modifying the config file, pointing it to the new database<br>");
    $configfile = $stagingdir.DIRECTORY_SEPARATOR.'wp-config.php';
    $config_data = file($configfile);
    $config_data = array_map('replace_a_line', $config_data);
    file_put_contents($configfile, implode('', $config_data));
    
    // Modify the database, replace the old URL with the new URL
    echo_immediately("Modifying the database - replacing the live URL with the staging URL.<br>");
    // Start by downloading and including Search-Replace-DB
    if (file_exists(dirname(__FILE__).DIRECTORY_SEPARATOR.'srdb.class.php') === FALSE) {
        file_put_contents(dirname(__FILE__).DIRECTORY_SEPARATOR.'srdb.class.php', file_get_contents('https://raw.githubusercontent.com/interconnectit/Search-Replace-DB/master/srdb.class.php'));
    }
    include 'srdb.class.php';
    $srdb_args = array(
        'name'    => $stagingdb,
        'user'    => DB_USER,
        'pass'    => DB_PASSWORD,
        'host'    => DB_HOST,
        'search'  => $livesiteproto . $livesite,
        'replace' => $stagingsiteproto . $stagingsite,
        'dry_run' => FALSE);
    $search_replace_results = new icit_srdb($srdb_args);
    
    
    // Awesomesauce .. let's link to the new site
    echo "<br><br>AWESOME! We're done here. Go <a href='".$protocol."://".$stagingsite."/'>check out the staging area</a> or <a href='".$protocol."://".$stagingsite."/wp-admin/'>visit the admin area</a> and do your modifications.<br></body></html>";
    
    // We're done here. Kill the php process and end everything.
    die();
}

//
// Alright, no proper _POST data .. so let's offer the user to create a new staging area.
//

?>
<html>
<body>
<h1>Staging Area Creator</h1>
<h2>If you want to create a new staging area for this website, then press the button below.</h2>
<p>Please note, the existing staging area files and database will be removed and the live website will be copied when you press the button.</p>
<form method="POST">
<input type="hidden" name="createstagingarea" value="goforit">
<input type="checkbox" name="symlinkuploads" value="yes" checked> Do not copy the wp-content/uploads directory, only create a symlink to the original. (unchecked = copy everything) <br>
<input type="submit" value="Create a new staging area">
</form>
</body>
</html><?php

//
// Alright .. now we can insert our FUNCTIONS
// Thankfully, you can call a function before you declare it in PHP, making for such a nicely readable code
//

/**
 * function rrmdir($dir)
 *
 * Deletes a directory's content recursively except for the directory itself
 *
 **/
function rrmdir($dir)
{ 
    $files = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
        RecursiveIteratorIterator::CHILD_FIRST
    );

    foreach ($files as $fileinfo) {
        $todo = ($fileinfo->isDir() ? 'rmdir' : 'unlink');
        $todo($fileinfo->getRealPath());
    }
}

/**
 * function rcopy($dir)
 *
 * Copies a directory's content recursively except for the directory itself
 *
 **/
function rcopy($origdir, $destdir)
{
    $symlink_upload = false;
    if (!empty($_POST["symlinkuploads"])) {
        $symlink_upload = true;
        $files = new RecursiveIteratorIterator(
            new MyRecursiveFilterIterator(
                new RecursiveDirectoryIterator($origdir, RecursiveDirectoryIterator::SKIP_DOTS)),
                RecursiveIteratorIterator::SELF_FIRST
        );
    } else {
        $files = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($origdir, RecursiveDirectoryIterator::SKIP_DOTS),
            RecursiveIteratorIterator::SELF_FIRST
        );
    }

    foreach ($files as $fileinfo) {
        if ($fileinfo->isDir()) {
            mkdir($destdir . DIRECTORY_SEPARATOR . $files->getSubPathName());
        } else {
            copy($fileinfo, $destdir . DIRECTORY_SEPARATOR . $files->getSubPathName());
        }
    }
    if ($symlink_upload) {
        symlink($origdir.'/wp-content/uploads', $destdir.'/wp-content/uploads');
    }
}

/**
 * function cleandb()
 *
 * Drops all the tables in a database
 *
 **/
function cleandb()
{
    global $dbconn, $connection_type;
    $dbconn->query('SET foreign_key_checks = 0');
    if ($result = $dbconn->query("SHOW TABLES"))
    {
        if ($connection_type == 'pdo') {
            echo_immediately("DB Connection is PDO<br>");
            while($row = $result->fetch(PDO::FETCH_ASSOC))
            {
                $dbconn->query('DROP TABLE IF EXISTS '.array_values($row)[0]);
            }
        } elseif ($connection_type == 'mysqli') {
            echo_immediately("DB Connection is MySQLi<br>");
            while($row = $result->fetch_array(MYSQLI_NUM))
            {
                $dbconn->query('DROP TABLE IF EXISTS '.$row[0]);
            }
        } else {
	    echo_immediately("DB Connection is not available<br>");
	}
    }
    $dbconn->query('SET foreign_key_checks = 1');
}


/**
 * function clonedb($origdb, $destdb)
 *
 * Clones all tables from one database to another
 *
 **/
function clonedb($origdb, $destdb)
{
    global $dbconn;
    global $table_prefix;
    global $connection_type;
    if ($result = $dbconn->query("SHOW TABLES FROM ".$origdb))
    {
        if ($connection_type == 'pdo') {
            while($row = $result->fetch(PDO::FETCH_ASSOC))
            {
                $dbconn->query('CREATE TABLE '.array_values($row)[0].' LIKE '.$origdb.'.'.array_values($row)[0]);
                $dbconn->query('INSERT INTO '.array_values($row)[0].' SELECT * FROM '.$origdb.'.'.array_values($row)[0]);
            }
        } elseif ($connection_type == 'mysqli') {
            while($row = $result->fetch_array(MYSQLI_NUM))
            {
                $dbconn->query('CREATE TABLE '.$row[0].' LIKE '.$origdb.'.'.$row[0]);
                $dbconn->query('INSERT INTO '.$row[0].' SELECT * FROM '.$origdb.'.'.$row[0]);
            }
        }
        // Make sure to discourage indexing
        $dbconn->query('UPDATE '.$table_prefix.'options SET option_value=\'0\' WHERE option_name=\'blog_public\'');
    }
}


/**
 * function replace_a_line($data)
 *
 * Makes sure to only replace the line we want .. the DB_NAME line
 * not DB_USER DB_HOST or any other configuration line.
 *
 **/
function replace_a_line($data) {
    global $stagingdb;
    global $livedb;
    if (stristr($data, 'DB_NAME')) {
        return str_replace($livedb, $stagingdb, $data);
    }
    return $data;
}

/**
 * Prints the output immediately
 */
function echo_immediately($string) {
    echo $string;
    ob_flush();
    flush();
    return;
}

/**
 * Filter for the uploads directory.
 * Only if you want to symlink the uploads directory (multiple files, large files, etc.)
 * 
 */
class MyRecursiveFilterIterator extends RecursiveFilterIterator {

    public static $FILTERS = array(
        'uploads',
    );

    public function accept() {
        return !in_array(
            $this->current()->getFilename(),
            self::$FILTERS,
            true
        );
    }
}