SyncCode I4 synchronizer.php
From MWWiki
Back to WordPress Synchronizer
<?php /* Plugin Name: WP Synchronizer Plugin URI: http://www.magsolweb.net/synchronizer Description: Allows remote sychronization of two WordPress blogs. Version: 0.1 Author: Shannon Quinn Author URI: http://www.magsolweb.net */ /* The software is provided "as is" and the author disclaims all warranties with regard to this software including all implied warranties of merchantability and fitness. In no event shall the author be liable for any special, direct, indirect, or consequential damages or any damages whatosever resulting from loss of use, data or profits, whether in an actino of contract, negligence or other tortious action, arising out of or in connection with the use of performance of this software. */ define('WS', 1); include_once('class.WordRPC.inc.php'); include_once('lib/FirePHPCore/fb.php'); include_once('lib/FirePHPCore/FirePHP.class.php'); /** * Handles some nice debugging. Makes use of the standard FirePHP mechanism * (so you better have it installed if you want to see any meaningful output), * using its "log" function. Furthermore, depending on the global debug level, * and what is provided to the function, this may not print anything at all. * * @param string $output The output to be printed to the FirePHP console. * @param int $debuglevel Specificity of debug output (1 is most basic, higher numbers more detailed) */ function sync_debug($output, $debuglevel) { global $firephp, $debug; if ($debuglevel <= $debug) { $firephp->log($output); } } /** * This creates and initializes a few basic options within the database * whenever this plugin is activated. */ function sync_activation() { global $levelstr, $wpdb, $tablename; // set a few defaults add_option($levelstr, 8); // create the table, if it doesn't already exist if ($wpdb->get_var('SHOW TABLES LIKE "' . $tablename . '"') != $tablename) { // here's da query $sql = 'CREATE TABLE `' . $tablename . '` (' . '`localid` INT UNSIGNED NOT NULL, ' . '`remoteid` INT UNSIGNED NOT NULL, ' . '`syncdate` DATETIME NOT NULL, ' . 'PRIMARY KEY (`localid`) ' . ');'; $wpdb->query($sql); } } /** * When the plugin is deactivated, any options that are set in the database * during this plugin's lifetime are cleared out here. */ function sync_deactivation() { global $levelstr, $userstr, $passstr, $urlstr, $wpdb, $tablename; // delete the values from the database delete_option($levelstr); delete_option($userstr); delete_option($passstr); delete_option($urlstr); if ($wpdb->get_var('SHOW TABLES LIKE "' . $tablename . '"') == $tablename) { $wpdb->query('DROP TABLE `' . $tablename . '`'); } } /** * This sets up the parts of the plugin items, creating the menu in the * administrator panel with the submenu items, as well as registering a few * more database options. */ function sync_registerMenu() { global $fullname, $page, $levelstr; $level = get_option($levelstr); add_menu_page($fullname, 'Synchronizer', $level, $page); add_submenu_page($page, $fullname, 'Setup Options', $level, $page, 'sync_setupOptions'); add_submenu_page($page, $fullname, 'Sync Entries', $level, 'syncentries', 'sync_syncSpecificEntries'); } /** * This functions is called whenever the user clicks the menu option to * adjust the settings of the XML-RPC connection. This function also * handles when the user updates these options. */ function sync_setupOptions() { global $urlstr, $userstr, $passstr, $levelstr; // an update was made if (wp_verify_nonce($_POST['sync_updateOptions'], 'update-options')) { // update all the options update_option($urlstr, $_POST[$urlstr]); update_option($userstr, $_POST[$userstr]); update_option($passstr, $_POST[$passstr]); update_option($levelstr, $_POST[$levelstr]); ?> <div class="updated"> <p><strong> <?php $rpc = sync_getRPC(true); if ($rpc->error()) { // login failed _e('Connection failed: ' . $rpc->errorMsg()); } else { // login successful _e('Credentials saved and successfully tested.'); } ?></strong></p> </div> <?php } // this is how we do $level = get_option($levelstr); ?> <div id="icon-options-general" class="icon32"><br /></div> <div class="wrap"> <h2>Setup Options</h2> <form method="post" action="<?php echo str_replace('%7E', '~', $_SERVER['REQUEST_URI']); ?>"> <?php wp_nonce_field('update-options', 'sync_updateOptions'); ?> <h3>Access Level</h3> <p><?php _e("Controls what account level has access to this plugin.") ?></p> <label><input type="radio" name="<?php echo $levelstr; ?>" value="0"<?php echo ($level == 0 ? ' checked="checked" ' : '') ?>>Subscriber</label><br /> <label><input type="radio" name="<?php echo $levelstr; ?>" value="1"<?php echo ($level == 1 ? ' checked="checked" ' : '') ?>>Contributor</label><br /> <label><input type="radio" name="<?php echo $levelstr; ?>" value="2"<?php echo ($level == 2 ? ' checked="checked" ' : '') ?>>Author</label><br /> <label><input type="radio" name="<?php echo $levelstr; ?>" value="3"<?php echo ($level == 3 ? ' checked="checked" ' : '') ?>>Editor</label><br /> <label><input type="radio" name="<?php echo $levelstr; ?>" value="8"<?php echo ($level == 8 ? ' checked="checked" ' : '') ?>>Administrator</label><br /> <h3>Remote Website</h3> <p><?php _e("Please enter the full URL (including the xmlrpc.php file) to your remote blog."); ?></p> <input type="text" name="<?php echo $urlstr; ?>" size="50" value="<?php echo get_option($urlstr); ?>" /> <br /> <h3>Username</h3> <p><?php _e("This should be a valid username for the blog referenced by the above URL."); ?></p> <input type="text" name="<?php echo $userstr; ?>" size="25" value="<?php echo get_option($userstr); ?>" /> <br /> <h3>Password</h3> <p><?php _e("The password for the above user account."); ?></p> <input type="password" name="<?php echo $passstr; ?>" size="25" value="<?php echo get_option($passstr); ?>" /> <br /> <p class="submit"> <input type="submit" name="Submit" value="<?php _e("Save and test"); ?>" /> </p> </form> </div> <?php } /** * This generates the page where the user can selectively sync and un-sync * specific entries in the local database. Full re-synchronizations can * also be performed, though this is costly in terms of time. * * If partial synchronizations are performed, data is cached in such a way * that only the changes incur any additional processing time. */ function sync_syncSpecificEntries() { global $wpdb, $tablename; // has this form already been submitted? if so, process the udpates // un-sync the deselected entries (if currently sync'd), and sync up // the selected ones (if currently un-sync'd) if (wp_verify_nonce($_POST['sync_syncsel'], 'update-options')) { // determine all the local entries $sql = 'SELECT * FROM `' . $wpdb->posts . '`'; $posts = $wpdb->get_results($sql, ARRAY_A); // loop through each entry, accessing its POST information foreach ($posts as $post) { // first, check on the POST value if (isset($_POST['entry' . $post['ID']])) { // this entry was selected to be synchronized...is it already? sync_debug('Entry ' . $post['ID'] . ' selected for synchronization.', 2); if (($remote = sync_getRemoteID($post['ID'])) === false) { // nope, sync it up sync_debug('Entry ' . $post['ID'] . ' now synchronized.', 2); sync_updateEntry($post['ID']); } // FIXME } else { // this entry was selected for desynchronization...is it already? sync_debug('Entry ' . $post['ID'] . ' selected for un-synchronization.', 2); if (($remote = sync_getRemoteID($post['ID'])) !== false) { // nope, delete it sync_debug('Entry ' . $post['ID'] . ' un-synchronized.', 2); sync_removeEntry($post['ID'], $remote); } } } } // now print out the page regardless of pre-submission of post-submission ?> <div id="icon-options-general" class="icon32"><br /></div> <div class="wrap"> <h2>Synchronize Specific Entries</h2> <?php $rpc = sync_getRPC(); if ($rpc->error()) { ?> No connection exists with the remote server: <?php echo $rpc->errorMsg(); ?> Please return to the Setup page and properly configure your remote blog. <?php return; } ?> <p> This page lists all the entries found in the local blog. Any entries without a check do not appear in the remote blog, while any checked entries are synchronized. Unchecking entries will delete them from the remote blog, and checking the entries will synchronize them. </p> <p>Click the following button to delete <strong>all</strong> entries from the remote blog that also appear in this one.</p> <p class="submit"> <input type="button" id="sync_button" value="<?php _e("Full Re-synchronization"); ?>" onClick="document.getElementById('sync_results').innerHTML = 'DISABLED FOR DEVELOPMENT';" /> <!-- UNCOMMENT ONCE DEVELOPMENT IS COMPLETE --> <!-- onClick="document.getElementById('sync_button').disabled = true; document.getElementById('sync_results').innerHTML = 'Starting...<br />'; sync_ajaxSyncAll('sync_results');" /> --> </p> <div id="sync_results"> </div> <hr /> <form method="post" action="<?php echo str_replace('%7E', '~', $_SERVER['REQUEST_URI']); ?>"> <?php wp_nonce_field('update-options', 'sync_syncsel'); ?> <p class="submit"> <input type="submit" value="<?php _e("Sync selected entries"); ?>" /> </p> <?php // grab all the entries, joining them on the syncs table $sql = 'SELECT * FROM `' . $wpdb->posts . '` LEFT JOIN `' . $tablename . '` ON `' . $wpdb->posts . '`.`ID` = `' . $tablename . '`.`localid` ' . 'WHERE `' . $wpdb->posts . '`.`post_status` = "publish" ORDER BY `' . $wpdb->posts . '`.`ID` DESC'; $posts = $wpdb->get_results($sql, ARRAY_A); // loop through all the results, listing each post and adding a checkbox // if the post is sync'd, fill the checkbox; otherwise, leave it empty foreach ($posts as $post) { ?> <p> <label title="test"><input type="checkbox" name="entry<?php echo $post['ID']; ?>" <?php echo (isset($post['remoteid']) ? 'checked="checked" ' : ''); ?>/><?php echo $post['post_title']; ?></label> </p> <?php } ?> <p class="submit"> <input type="submit" value="<?php _e("Sync selected entries"); ?>" /> </p> </form> </div> <?php } /** * This function is responsible for including some custom JavaScript in this * plugin. It will print the defined JS functions to all the admin screens. */ function sync_addJS() { // include WP's SACK JavaScript library wp_print_scripts(array('sack')); // define all the JS functions we'll need ?> <script type="text/javascript"> //<![CDATA[ function sync_ajaxSyncAll(resultfield) { // define the sack...so juvenile var myballsack = new sack("<?php bloginfo("wpurl"); ?>/wp-admin/admin-ajax.php"); // set a few variables myballsack.execute = 1; myballsack.method = "POST"; myballsack.setVar("action", "sync_syncAll"); myballsack.setVar("resultid", document.getElementById(resultfield)); myballsack.setVar("cookie", document.cookie, false); myballsack.onError = function() { alert('ERROR! Unable to complete sync request!'); }; myballsack.runAJAX(); return true; } //]]> </script> <?php // all done! } /** * The end-all, be-all. This wipes the slate clean and re-syncs everything. * The synchronization table is emptied, and all local published posts are * synchronized with the remote server. As such, this can take awhile. */ function sync_syncAllEntries() { global $wpdb, $tablename; $rpc = sync_getRPC(); // first, delete all the synchronized remote posts $sql = 'SELECT remoteid FROM `' . $tablename . '`'; $ids = $wpdb->get_results($sql, ARRAY_A); for ($i = 0; $i < count($ids); $i++) { $rpc->deletePost($ids[$i]['remoteid']); } $wpdb->query('TRUNCATE TABLE `' . $tablename . '`'); // second, start over and get all published posts $sql = 'SELECT `ID` FROM `' . $wpdb->posts . '` WHERE `post_status` = "publish" ' . 'ORDER BY `ID` ASC'; $posts = $wpdb->get_results($sql, ARRAY_A); foreach ($posts as $post) { if (!sync_updateEntry($post['ID'])) { echo "document.getElementById('" . $_POST['resultid'] . "').innerHTML += '" . "<b>Error with entry " . $post['ID'] . ": " . $rpc->errorMsg() . "</b><br />';"; } } // finish up die("document.getElementById('" . $_POST['resultid'] . "').innerHTML = " . '\'<div class="updated"><p><strong>Full synchronization successful!' . '</stront></p></div>\''); } /** * Registers the specific data boxes with constructing a post as well * as a page. This will make the new page/post screen render all the * additional data necessary for this plugin. */ function sync_customPostData() { add_meta_box('sync_sectionID', 'Post Synchronization', 'sync_setupPostBox', 'post', 'advanced'); add_meta_box('sync_sectionID', 'Page Synchronization', 'sync_setupPostBox', 'page', 'advanced'); } /** * Prints form data to the page/post creation page so that entries can * be synchronized on the fly, rather than through the manual interface. */ function sync_setupPostBox() { $rpc = sync_getRPC(); if (!$rpc->error()) { // make sure this option is only available when the credentials are correct wp_nonce_field(__FILE__, 'sync_syncEntryConfirm'); echo '<label for="sync_syncThisEntry">' . '<input type="checkbox" id="sync_syncThisEntry" name="sync_syncThisEntry" checked="checked" /> ' . 'Sync this entry to remote blog' . '</label>'; } else { echo '<strong>Please visit the setup page to configure this option.</strong><br />' . $rpc->errorMsg(); } } /** * This function is invoked whenever the save_post action is triggered. This * will ensure that an individual synchronization request is carried out for * the entry with the correct post_id. * * @param int $post_id The ID of the post to be synchronized on the fly. * @return int $post_id */ function sync_savePostData($post_id) { // first, perform a few security checks, since this event can be // triggered in multiple spots if (!wp_verify_nonce($_POST['sync_syncEntryConfirm'], __FILE__)) { return $post_id; } // test user permissions for enacting this action if (!current_user_can('edit_' . ($_POST['post_type'] == 'page' ? 'page' : 'post'), $post_id)) { return $post_id; } // we're good to go if (isset($_POST['sync_syncThisEntry'])) { // sync it, bitches! if (!sync_updateEntry($post_id)) { $rpc = sync_getRPC(); sync_debug('RPC Error: ' . $rpc->errorMsg(), 1); } } return $post_id; } /** * Returns the remoteid for an entry from the database table tracking * synchronizations. This function makes use of some interesting caching * while the script is in execution so the whole list of remote entries * does not have to be retrieved repeatedly. Furthermore, since the entries * are sorted, binary search is used to further speed things up. * * @param int $localid The local post's ID. * @param bool $forceupdate If true, the list of remotes will be updated * @return The remote ID of the entry, or false if none is found. */ function sync_getRemoteID($localid, $forceupdate = false) { global $wpdb, $tablename; static $remotes; // first, set our list if (!isset($remotes) || $forceupdate) { $sql = 'SELECT * FROM `' . $tablename . '` ORDER BY `localid` ASC'; $remotes = $wpdb->get_results($sql, ARRAY_A); } // now perform an iterative search for the localid /* LINEAR SEARCH $length = count($remotes); for ($i = 0; $i < $length; $i++) { if ($remotes[$i]['localid'] == $localid) { return $remotes[$i]['remoteid']; } } return false; */ /* BINARY SEARCH */ $left = 0; $right = count($remotes) - 1; $mid = intval(($left + $right) / 2); do { if ($remotes[$mid]['localid'] < $localid) { $left = $mid + 1; } else { $right = $mid - 1; } $mid = intval(($left + $right) / 2); } while ($remotes[$mid]['localid'] != $localid && $left < $right); // finally, return our remote id (if it exists) return ($remotes[$mid]['localid'] == $localid ? $remotes[$mid]['remoteid'] : false); } /** * Handles all the dirty work involved in actually sending the entry over * the wire. If the entry is brand new, appropriate rows are created in the * database and sync'ed up with whatever the remote server returns. If the * entry is an edit of a previously sync'd entry, that is handled here as well. * * @param int $postid The local ID of the post. * @return bool True on success, false on failure. */ function sync_updateEntry($postid) { global $wpdb, $tablename, $urlstr, $userstr, $passstr; // first, get all the post information $sql = 'SELECT * FROM `' . $wpdb->posts . '` WHERE `ID` = ' . $postid; $post = $wpdb->get_row($sql); // now, get all the categories $categories = sync_getLocalCategories($postid); // set up the annoying timestamp-itude $date = new stdClass(); $timestamp = strtotime($post->post_date); $date->scalar = date('Ymd', $timestamp) . 'T' . date('H:i:s', $timestamp); $date->xmlrpc_type = 'datetime'; $date->timestamp = $timestamp; $struct = array('dateCreated' => $date, 'description' => $post->post_content, 'title' => $post->post_title); // initialize the remoteid variable and the RPC mechanism $remoteid = ""; $newpost = false; $rpc = sync_getRPC(); if ($rpc->error()) { sync_debug('RPC Error: ' . $rpc->errorMsg(), 1); return false; } // first, create or edit the post itself if (($remoteid = sync_getRemoteID($postid)) !== false) { // the post already exists remotely, so let's simply update it $rpc->editPost($remoteid, $struct); sync_debug('Post is being updated', 2); } else { // make sure this isn't a draft if ($post->post_status != 'publish') { return true; } // this post has not been sync'd, so create it $remoteid = $rpc->newPost($struct); $newpost = true; sync_debug('New post created', 2); } // check for errors? if ($rpc->error()) { sync_debug('RPC Error: ' . $rpc->errorMsg(), 1); return false; } // sweet, now post the categories $rpc->setCategories($remoteid, $categories); // any errors? if ($rpc->error()) { sync_debug('RPC Error: ' . $rpc->errorMsg(), 1); return false; } // excellent, now update the synchronization table $sql = ""; if ($newpost) { $sql = 'INSERT INTO `' . $tablename . '` VALUES (' . $post->ID . ', ' . $remoteid . ', "' . date('Y-m-d H:i:s', time()) . '")'; } else { $sql = 'UPDATE `' . $tablename . '` SET `syncdate` = "' . date('Y-m-d H:i:s', time()) . '" WHERE `localid` = ' . $post->ID . ' AND `remoteid` = ' . $remoteid; } $wpdb->query($sql); // done! return true; } /** * Helper function to process the query result of grabbing all the * categories in the current post. * * @param array $postid The ID of the post to edit. * @return array An array of arrays. */ function sync_getLocalCategories($postid) { global $wpdb; // define some constants to make DB query easier $terms = '`' . $wpdb->prefix . 'terms`'; $termtax = '`' . $wpdb->prefix . 'term_taxonomy`'; $termrel = '`' . $wpdb->prefix . 'term_relationships`'; // hit the database for the categories $sql = 'SELECT ' . $terms . '.`name`, ' . $terms . '.`term_id`' . ' FROM ' . $terms . ', ' . $termtax . ', ' . $termrel . ' WHERE ' . $terms . '.`term_id` = ' . $termtax . '.`term_id` AND ' . $termtax . '.`term_taxonomy_id` = ' . $termrel . '.`term_taxonomy_id` AND ' . $termrel . '.`object_id` = ' . $postid; $cats = $wpdb->get_results($sql, ARRAY_A); // process them into a coherent array that mt.setPostCategories is looking for $retval = array(); $numelems = 0; foreach($cats as $cat) { $id = sync_getRemoteCategoryId($cat['name']); $retval[$numelems++] = array('categoryId' => $id); } return $retval; } /** * This function matches the local category with its corresponding identical * category in the remote blog. If the category does not exist remotely, * it will be created. * * @param string $category The name of the category. * @return int The remote ID of the category. */ function sync_getRemoteCategoryId($category) { global $wpdb; static $categories; $rpc = sync_getRPC(); if ($rpc->error()) { return -1; } if (!isset($categories)) { $categores = $rpc->getCategories(); if ($rpc->error()) { return -1; } } foreach ($categories as $cat) { if ($cat['categoryName'] == $category) { return $cat['categoryId']; } } // no match found, create it $id = $rpc->addCategory(array('name' => $category, 'slug' => strtolower($category))); if ($rpc->error()) { return -1; } // update the $categories array $categories = $rpc->getCategories(); if ($rpc->error()) { return -1; } // return the id return $id; } /** * This function is executed whenever a post or page is deleted from the * local blog. If this entry has been sync'd, its entry in the sync table * will be deleted, and its remote post will also be removed. * * @param int $postid The local ID of the post. * @return int $postid */ function sync_deletePostData($postid) { global $wpdb, $tablename; // first, was the local post synchronized remotely? $remoteid = sync_getRemoteID($postid); if ($remoteid === false) { // no post exists remotely sync_debug('No post found to be deleted', 2); return $postid; } // do the deed and GTFO sync_removeEntry($postid, $remoteid); return $postid; } /** * This handles explicitly deleting the remote entry and all local synchronization * data associated with it. * * @param int $localid The local entry ID. * @param int $remoteid The corresponding remote entry ID. * @return bool True on success, false on failure. */ function sync_removeEntry($localid, $remoteid) { global $wpdb, $tablename; // first, get the RPC $rpc = sync_getRPC(); if ($rpc->error()) { sync_debug('RPC Error: ' . $rpc->errorMsg(), 1); return false; } // now, delete the post $rpc->deletePost($remoteid); if ($rpc->error()) { sync_debug('RPC Error: ' . $rpc->errorMsg(), 1); return false; } // finally, delete the local sync data $sql = 'DELETE FROM `' . $tablename . '` WHERE `localid` = ' . $localid; $wpdb->query($sql); // done return true; } /** * Initializes and returns the RPC handle, and handles any errors that arise * from its initialization. * * @param forceupdate bool Set to true if we want to force reinitialization of the RPC handle * @return object The RPC handle. */ function sync_getRPC($forceupdate = false) { global $userstr, $urlstr, $passstr; static $rpc; if (!isset($rpc) || $forceupdate) { $rpc = new WordRPC(get_option($urlstr), get_option($userstr), get_option($passstr)); } return $rpc; } /* a few important global variables */ global $fullname, $page, $levelstr, $urlstr, $userstr, $passstr, $tablename, $wpdb, $debug, $firephp; $fullname = 'WordPress Synchronizer'; $page = 'synchronizer'; $levelstr = 'sync_level'; $urlstr = 'sync_url'; $userstr = 'sync_username'; $passstr = 'sync_password'; $tablename = $wpdb->prefix . 'syncs'; $firephp = FirePHP::getInstance(true); $debug = 1; // 0 is off; 1 is basic; 2 is detailed; 3 is verbose ob_start(); /* here's what starts all this crap */ // plugin activation/deactivation hooks (setup and cleanup) register_activation_hook(__FILE__, 'sync_activation'); register_deactivation_hook(__FILE__, 'sync_deactivation'); // basics add_action('admin_menu', 'sync_registerMenu'); add_action('admin_menu', 'sync_customPostData'); add_action('save_post', 'sync_savePostData'); add_action('delete_post', 'sync_deletePostData'); // these two actions are required for AJAX add_action('admin_print_scripts', 'sync_addJS'); add_action('wp_ajax_sync_syncAll', 'sync_syncAllEntries'); ?>
