diff --git a/assets/js/press-sync.js b/assets/js/press-sync.js index 080e435..896bcbd 100644 --- a/assets/js/press-sync.js +++ b/assets/js/press-sync.js @@ -1,17 +1,83 @@ window.PressSync = ( function( window, document, $ ) { - var app = {}; - - app.PAGE_SIZE = 1; + var app = { + PAGE_SIZE: 1, + LOG_LIMIT: 20, // Limit how many log entries to show, previous logs will be discarded. + log_index: 0, // Count how many logs have processed. + times: [], // Array of times used to calculate time remaining average more accurately. + elCache: {}, + logFileURL: null + }; + /** + * Initialize the JS app for Press Sync. + * + * Handles registering elements to the element Cache and binding inital listeners. + * + * @since 0.1.0 + */ app.init = function() { - $(document).on( 'click', '.press-sync-button', app.pressSyncButton ); + app.elCache.syncButton = $('#press-sync-button'); + app.elCache.cancelButton = $('#press-sync-cancel-button'); + app.elCache.status = $('.progress-stats'); + app.elCache.bulkSettings = $('#press-sync-bulk-settings'); + app.elCache.logView = $('#press-sync-log-view' ); + app.elCache.logs = $('#press-sync-logs'); + app.elCache.downloadLog = $('#press-sync-download-log'); + + app.elCache.syncButton.on( 'click', app.pressSyncButton ); + app.elCache.cancelButton.on( 'click', app.cancelSync ); + app.elCache.downloadLog.on( 'click', app.downloadLog ); }; + /** + * Handles the Press Sync "Sync" button. + * + * @since NEXT + * @param Object click_event The click event from the listener. + */ app.pressSyncButton = function( click_event ) { + app.running = true; + + // @TODO probably should have this in a method similar to app.cleanup. + app.elCache.syncButton.hide(); + app.elCache.cancelButton.show(); + app.elCache.bulkSettings.hide(); + app.elCache.logView.show(); + app.elCache.logs.val(''); app.loadProgressBar(); return; - } + }; + + /** + * Set the app.running flag to false to stop the current sync process. + * + * @since NEXT + */ + app.cancelSync = function() { + app.running = false; + }; + + /** + * Update the timing array and smooth out timing samples. + * + * @since NEXT + * @param Number remaining_time The most recent remaining time estimate. + */ + app.updateTiming = function( remaining_time ) { + app.times.push( remaining_time ); + + // Limit sample size. + if ( 100 < app.times.length ) { + app.times.shift(); + } + + app.times.map( function ( e ) { + return e += e; + } ); + + app.times = smooth( app.times, 0.85 ); + }; app.loadProgressBar = function() { $('.press-sync-button').hide(); @@ -72,24 +138,25 @@ window.PressSync = ( function( window, document, $ ) { if ( request_time ) { // Estimate time remaining. - var remaining_time = ( ( ( total_objects - total_objects_processed ) / app.PAGE_SIZE ) * request_time ); - remaining_time = remaining_time / 60 / 60; var time_left_suffix = 'hours'; + var remaining_time = ( ( ( total_objects - total_objects_processed ) / app.PAGE_SIZE ) * request_time ); + app.updateTiming( remaining_time ); + remaining_time = remaining_time / app.times.length; + remaining_time = remaining_time / 60 / 60; // Shift to minutes. if ( 1 > remaining_time ) { - remaining_time = remaining_time * 60; + remaining_time = remaining_time * 60; time_left_suffix = 'minutes'; } // Round to two decimal places, mostly. - remaining_time = Math.round( remaining_time * 100 ) / 100; - - progress_string += ' (' + [ 'Estimated time remaining:', remaining_time, time_left_suffix ].join(' ') + ')'; + remaining_time = Math.round( remaining_time * 100 ) / 100; + progress_string += ' (' + [ 'Estimated time remaining:', remaining_time, time_left_suffix ].join(' ') + ' )'; } $('.progress-stats').text( progress_string ); - } + }; /** * Syncs data to the remote site. @@ -117,29 +184,140 @@ window.PressSync = ( function( window, document, $ ) { objects_to_sync: objects_to_sync } }).done(function( response ) { - // Convert request time from milliseconds to seconds. - var request_time = ( new Date().getTime() - start_time ) / 1000; + if ( ! app.running ) { + app.cleanup( 'Sync canceled by user.' ); + return; + } - app.updateProgressBar( response.data.objects_to_sync, response.data.total_objects_processed, response.data.total_objects, request_time ); + app.updateProgressBar( + response.data.ps_objects_to_sync, + response.data.total_objects_processed, + response.data.total_objects, + ( new Date().getTime() - start_time ) / 1000 // Convert request time from milliseconds to seconds. + ); + app.Log( response ); + + // Bail if we're done. if ( response.data.total_objects_processed >= response.data.total_objects ) { // Start the next batch at page 1. if ( next_args && next_args.order_to_sync_all && next_args.order_to_sync_all.length ) { return app.syncData( 1, null, next_args ); } - $('.press-sync-button').show(); - $('.progress-stats').text('Sync completed!'); + app.cleanup( 'Sync completed!' ); return; } - app.syncData( response.data.next_page, response.data.objects_to_sync, next_args ); + app.syncData( response.data.next_page, response.data.ps_objects_to_sync, next_args ); }); - } + /** + * Logs messages from the remote server to the log window. + * + * @since NEXT + * @param Object response The response from the AJAX request. + */ + app.Log = function( response ) { + if ( ! response.data ) { + return; + } + + var loglines = []; + + try { + var logs = response.data.log; + for ( var i = 0; i < logs.length; i++) { + loglines.push( logs[i] ); + } + + loglines.push("\n---BATCH END ---\n"); + app.elCache.logs.val( app.elCache.logs.val() + loglines.join("\n") ); + } catch ( e ) { + console.warn( "Could not log data, response: " + e ); + console.warn( response.data ); + } + }; + + /** + * Cleanup the view so it's back to a state similar to when we first + * visit the page. + * + * @since NEXT + * @param string message The message to display under the progress bar. + */ + app.cleanup = function( message ){ + app.elCache.syncButton.show(); + app.elCache.cancelButton.hide(); + app.elCache.bulkSettings.show(); + app.elCache.status.text( message ); + createLogFile(); + app.elCache.downloadLog.show(); + }; + + /** + * Click handler to download the log file. + * + * @since NEXT + */ + app.downloadLog = function() { + app.elCache.downloadLog.attr( 'href', app.logFileURL ); + }; + $( document ).ready( app.init ); return app; + // Private methods. + + /** + * Smooth a set of values. + * + * We use this to smooth out the timings in the array of request times to give a more accurate + * time estimation. + * + * Source: https://stackoverflow.com/q/32788836/1169389 + */ + function smooth(values, alpha) { + var weighted = average(values) * alpha; + var smoothed = []; + for (var i in values) { + var curr = values[i]; + var prev = smoothed[i - 1] || values[values.length - 1]; + var next = curr || values[0]; + var improved = Number(average([weighted, prev, curr, next]).toFixed(2)); + smoothed.push(improved); + } + return smoothed; + } + + /** + * Gets the averate of a set of data. + * + * Source: https://stackoverflow.com/q/32788836/1169389 + */ + function average(data) { + var sum = data.reduce(function(sum, value) { + return sum + value; + }, 0); + var avg = sum / data.length; + return avg; + } + + /** + * Creates a downloadable file using the Javascript Blob object. + * + * Source: https://stackoverflow.com/a/21016088/1169389 + */ + function createLogFile() { + var text = app.elCache.logs.val(); + var data = new Blob([text], {type: 'text/plain'}); + + if ( null !== app.logFileURL ) { + window.URL.revokeObjectURL( app.logFileURL ); + } + + app.logFileURL = window.URL.createObjectURL(data); + } } )( window, document, jQuery ); diff --git a/includes/class-api.php b/includes/class-api.php index a246325..d8c914f 100644 --- a/includes/class-api.php +++ b/includes/class-api.php @@ -31,6 +31,14 @@ class API extends \WP_REST_Controller { */ private $fix_terms = false; + /** + * Array of log messages to return with API responses. + * + * @var array + * @since NEXT + */ + private $logs = array(); + /** * Constructor. * @@ -158,7 +166,9 @@ public function validate_sync_key() { $press_sync_key = get_option( 'ps_key' ); if ( ! $press_sync_key || ( $press_sync_key_from_remote !== $press_sync_key ) ) { - return false; + wp_send_json_error( array( + 'message' => __( 'Invalid Press Sync key.', 'press-sync' ), + ) ); } return true; @@ -216,18 +226,24 @@ public function sync_objects( $request ) { remove_filter( 'content_save_pre', 'wp_filter_post_kses' ); $responses = array(); + $this->log( date( DATE_RFC2822, time() ) . '(' . time() . ')' ); $objects_to_sync = in_array( $objects_to_sync, array( 'attachment', 'comment', 'user', 'option', 'taxonomy_term' ), true ) ? $objects_to_sync : 'post'; foreach ( $objects as $object ) { $sync_method = "sync_{$objects_to_sync}"; + $this->log( '=====================================' ); + $this->log( sprintf( __( 'Syncing next object with method "%s".', 'press-sync' ), $sync_method ) ); + $this->log( '-------------------------------------' ); $responses[] = $this->$sync_method( $object, $duplicate_action, $force_update ); } add_filter( 'content_save_pre', 'wp_filter_post_kses' ); - return $responses; + // Remove empty lines and reset array indexes. + $log = array_values( array_filter( $this->get_log() ) ); + return compact( 'responses', 'log' ); } /** @@ -260,67 +276,68 @@ public function sync_post( $post_args, $duplicate_action, $force_update = false if ( $this->fix_terms ) { if ( ! $local_post ) { - return array( - 'debug' => array( - 'message' => __( 'Could not find a local post to attach the terms to.', 'press-sync' ), - ), - ); + return $this->error_log( __( 'Could not find a local post to attach the terms to.', 'press-sync' ) ); } + return $this->fix_term_relationships( $local_post['ID'], $post_args ); } $post_args['ID'] = isset( $local_post['ID'] ) ? $local_post['ID'] : 0; + $this->log( sprintf( __( 'Final post_args["ID"] = %d', 'press-sync' ), $post_args['ID'] ) ); // Replace embedded media. if ( isset( $post_args['embedded_media'] ) ) { - foreach ( $post_args['embedded_media'] as $attachment_args ) { + $this->log( __( 'Attempting addition of embedded media.', 'press-sync' ) ); - $attachment_id = $this->sync_attachment( $attachment_args ); - - if ( abinst( $attachment_id ) ) { - - $sync_source = $post_args['meta_input']['press_sync_source']; - $attachment_url = str_ireplace( $sync_source, home_url(), $attachment_args['attachment_url'] ); - + if ( abinst( $attachment_id = $this->sync_attachment( $attachment_args ) ) ) { + $sync_source = $post_args['meta_input']['press_sync_source']; + $attachment_url = str_ireplace( $sync_source, home_url(), $attachment_args['attachment_url'] ); $post_args['post_content'] = str_ireplace( $attachment_args['attachment_url'], $attachment_url, $post_args['post_content'] ); } } } + $this->log( sprintf( __( 'Incoming author: %s', 'press-sync' ), $post_args['post_author'] ) ); + // Set the correct post author. $post_args['post_author'] = $this->get_press_sync_author_id( $post_args['post_author'] ); + $this->log( sprintf( __( 'Matched author: %s', 'press-sync' ), $post_args['post_author'] ) ); + // Check for post parent and update IDs accordingly. if ( ! $this->preserve_ids && isset( $post_args['post_parent'] ) && $post_parent_id = $post_args['post_parent'] ) { - $post_parent_args['post_type'] = $post_args['post_type']; - $post_parent_args['meta_input']['press_sync_post_id'] = $post_parent_id; + $this->log( __( 'Attempting connection to parent post.', 'press-sync' ) ); + $post_parent_args = array( + 'post_type' => $post_args['post_type'], + 'meta_input' => $post_parent_id, + ); $parent_post = $this->get_synced_post( $post_parent_args, false ); + $found = $parent_post ? __( 'Found', 'press-sync' ) : __( 'Not found.', 'press-sync' ); + $this->log( sprintf( __( 'Result of search for valid post_parent: %s', 'press-sync' ), $found ) ); $post_args['post_parent'] = ( $parent_post ) ? $parent_post['ID'] : 0; } // Keep the ID because we found a regular ol' duplicate. if ( $this->preserve_ids && ! $local_post && ! empty( $post_args['ID'] ) ) { + $this->log( __( 'Preserving original post-type object ID.', 'press-sync' ) ); $post_args['import_id'] = $post_args['ID']; unset( $post_args['ID'] ); } // Determine which content is newer, local or remote. if ( ! $force_update && $local_post && ( strtotime( $local_post['post_modified'] ) >= strtotime( $post_args['post_modified'] ) ) ) { - // If we're here, then we need to keep our local version. $response['remote_post_id'] = $post_args['meta_input']['press_sync_post_id']; $response['local_post_id'] = $local_post['ID']; - $response['message'] = __( 'Local version is newer than remote version', 'press-sync' ); + $this->log( __( 'Local version is newer than remote version.', 'press-sync' ) ); // Assign a press sync ID. $this->add_press_sync_id( $local_post['ID'], $post_args ); - - return array( 'debug' => $response ); - + return $response; } // Add categories. @@ -329,6 +346,7 @@ public function sync_post( $post_args, $duplicate_action, $force_update = false require_once( ABSPATH . '/wp-admin/includes/taxonomy.php' ); foreach ( $post_args['tax_input']['category'] as $category ) { + $this->log( sprintf( __( 'Creating new category "%s".', 'press-sync' ), $category['name'] ) ); wp_insert_category( array( 'cat_name' => $category['name'], 'category_description' => $category['description'], @@ -342,31 +360,33 @@ public function sync_post( $post_args, $duplicate_action, $force_update = false // Insert/update the post. $local_post_id = wp_insert_post( $post_args, true ); + $this->log( sprintf( __( 'Inserted new post with ID "%d", post type "%s", title "%s".', 'press-sync' ), + $local_post_id, + $post_args['post_type'], + $post_args['post_title'] + ) ); + // Bail if the insert didn't work. if ( is_wp_error( $local_post_id ) ) { - trigger_error( sprintf( 'Error inserting post: ', $local_post_id->get_error_message() ) ); - return array( 'debug' => $local_post_id ); + return $this->error_log( sprintf( __( 'Error inserting post: %s', 'press-sync' ), $local_post_id->get_error_message() ) ); } // Attach featured image. - $featured_result = $this->attach_featured_image( $local_post_id, $post_args ); + $this->attach_featured_image( $local_post_id, $post_args ); // Attach any comments. $comments = isset( $post_args['comments'] ) ? $post_args['comments'] : array(); $this->attach_comments( $local_post_id, $comments ); - $this->attach_terms( $local_post_id, $post_args ); // Run any secondary commands. do_action( 'press_sync_sync_post', $local_post_id, $post_args ); + $this->log( __( 'The post has been synced with the remote site', 'press-sync' ) ); return array( - 'debug' => array( - 'remote_post_id' => $post_args['meta_input']['press_sync_post_id'], - 'local_post_id' => $local_post_id, - 'message' => __( 'The post has been synced with the remote site', 'press-sync' ), - 'featured_result' => $featured_result, - ), + 'remote_post_id' => $post_args['meta_input']['press_sync_post_id'], + 'local_post_id' => $local_post_id, + 'featured_result' => $featured_result, ); } @@ -385,10 +405,11 @@ public function sync_attachment( $attachment_args, $duplicate_action = 'skip', $ $attachment_id = false; $import_id = false; $attachment_args = $this->clean_post_object_args( $attachment_args ); + $error_message = null; // Attachment URL does not exist so bail early. if ( ! $this->skip_assets && ! array_key_exists( 'attachment_url', $attachment_args ) ) { - return false; + return $this->error_log( __( 'Attachment data missing "attachment_url" parameter!', 'press-sync' ) ); } if ( isset( $attachment_args['ID'] ) ) { @@ -401,6 +422,7 @@ public function sync_attachment( $attachment_args, $duplicate_action = 'skip', $ // ID will only be set if we find the attachment is already here. if ( ! isset( $attachment['ID'] ) ) { + $this->log( __( 'Inserting a new attachment.', 'press-sync' ) ); $filename = $attachment['filename']; unset( $attachment['filename'] ); @@ -434,10 +456,10 @@ public function sync_attachment( $attachment_args, $duplicate_action = 'skip', $ if ( $attachment_id && $import_id ) { update_post_meta( $attachment_id, 'press_sync_post_id', $import_id ); } - } - catch( \Exception $e ) { - // @TODO log it more! - error_log( $e->getMessage() ); + + $this->log( sprintf( __( 'Successfully uploaded a new attachment with ID "%d".', 'press-sync' ), $attachment_id ) ); + } catch( \Exception $e ) { + $this->error_log( $e->getMessage() ); } return $attachment_id; @@ -455,27 +477,30 @@ public function sync_attachment( $attachment_args, $duplicate_action = 'skip', $ * @return array */ public function sync_user( $user_args, $duplicate_action, $force_update = false ) { - $username = isset( $user_args['user_login'] ) ? $user_args['user_login'] : ''; + $this->log( sprintf( __( 'Attempting sync of user with login "%s"', 'press-sync' ), $username ) ); + // Check to see if the user exists. $user = get_user_by( 'login', $username ); if ( ! $user ) { - + $this->log( __( 'Creating new user.', 'press-sync' ) ); $user_id = wp_insert_user( $user_args ); if ( is_wp_error( $user_id ) ) { - return wp_send_json_error(); + return $this->error_log( sprintf( __( 'There was an error creating this user: "%s".', 'press-sync' ), $user_id->get_error_message() ) ); } $user = get_user_by( 'id', $user_id ); - + $this->log( sprintf( __( 'New user created with ID "%d".', 'press-sync' ), $user_id ) ); } else { $user_id = $user->ID; + $this->log( sprintf( __( 'Found existing user with ID "%d".', 'press-sync' ), $user_id ) ); } // Update the meta. + $this->debug_log( __( 'Attaching user metadata.', 'press-sync' ), $user_args['meta_input'] ); foreach ( $user_args['meta_input'] as $usermeta_key => $usermeta_value ) { if ( 0 === strpos( $usermeta_key, 'press_sync_' ) ) { $usermeta_key = $this->maybe_make_multisite_key( $usermeta_key ); @@ -486,13 +511,13 @@ public function sync_user( $user_args, $duplicate_action, $force_update = false // Asign user role. $user->add_role( $user_args['role'] ); + $this->log( sprintf( __( 'Assigning user role of "%s".', 'press-sync' ), $user_args['role'] ) ); // Prepare response. $response['user_id'] = $user_id; $response['blog_id'] = get_current_blog_id(); return $response; - } /** @@ -510,11 +535,18 @@ public function sync_option( $option_args ) { $option_value = isset( $option_args['option_value'] ) ? $option_args['option_value'] : ''; if ( empty( $option_value ) || empty( $option_name ) ) { - return false; + return $this->error_log( __( 'Attempted syncing option, but missing arg of "option_name" or "option_value"!', 'press-sync' ) ); } + $this->log( sprintf( __( 'Syncing option "%s" with value "%s".', 'press-sync' ), $option_name, $option_value ) ); $response['option_id'] = update_option( $option_name, $option_value, $option_args['autoload'] ); + $result = __( 'Updated', 'press-sync' ); + if ( false === $response['option_id'] ) { + $result = __( 'Not updated.', 'press-sync' ); + } + + $this->log( sprintf( __( 'Update result: "%s".', 'press-sync' ), $result ) ); return $response; } @@ -549,9 +581,13 @@ public function get_synced_post( $post_args, $respect_post_type = true ) { } $sql .= ' LIMIT 1'; - $prepared_sql = $wpdb->prepare( $sql, $prepare_args ); - $post = $wpdb->get_row( $prepared_sql, ARRAY_A ); + + $this->debug_log( __( 'Attempting to find a synced post with SQL: ', 'press-sync' ), $prepared_sql ); + + $post = $wpdb->get_row( $prepared_sql, ARRAY_A ); + + $this->log( sprintf( __( 'Synced post search result: %s.', 'press-sync' ), ( $post ? 'Found' : 'Not found' ) ) ); return ( $post ) ? $post : false; @@ -594,7 +630,6 @@ public function media_exists( $media_url ) { * @return boolean */ public function comment_exists( $comment_args = array() ) { - $press_sync_comment_id = isset( $comment_args['meta_input']['press_sync_comment_id'] ) ? $comment_args['meta_input']['press_sync_comment_id'] : 0; $press_sync_source = isset( $comment_args['meta_input']['press_sync_source'] ) ? $comment_args['meta_input']['press_sync_source'] : 0; @@ -617,6 +652,7 @@ public function comment_exists( $comment_args = array() ) { $comment = get_comments( $query_args ); if ( $comment ) { + $this->log( __( 'Comment already exists, skipping.', 'press-sync' ) ); return (array) $comment[0]; } @@ -669,12 +705,13 @@ public function get_press_sync_author_id( $user_id ) { global $wpdb; - $usermeta_key = $this->maybe_make_multisite_key( 'press_sync_user_id' ); - $sql = "SELECT user_id AS ID FROM {$wpdb->usermeta} WHERE meta_key = '{$usermeta_key}' AND meta_value = %d"; - $prepared_sql = $wpdb->prepare( $sql, $user_id ); - + $usermeta_key = $this->maybe_make_multisite_key( 'press_sync_user_id' ); + $sql = "SELECT user_id AS ID FROM {$wpdb->usermeta} WHERE meta_key = '{$usermeta_key}' AND meta_value = %d"; + $prepared_sql = $wpdb->prepare( $sql, $user_id ); $press_sync_user_id = $wpdb->get_var( $prepared_sql ); + $this->debug_log( __( 'Attempted finding an author for this object with SQL: ', 'press-sync' ), $prepared_sql ); + return ( $press_sync_user_id ) ? $press_sync_user_id : 1; } @@ -691,7 +728,7 @@ public function attach_featured_image( $post_id, $post_args ) { // Post does not have a featured image so bail early. if ( empty( $post_args['featured_image'] ) ) { - return false; + return $this->log( __( 'No featured image attached.', 'press-sync' ) ); } // Allow download_url() to use an external request to retrieve featured images. @@ -705,7 +742,11 @@ public function attach_featured_image( $post_id, $post_args ) { // Remove filter that allowed an external request to be made via download_url(). remove_filter( 'http_request_host_is_external', array( $this, 'allow_sync_external_host' ) ); - return $response ? '' : "Error attaching thumbnail {$thumbnail_id} to post {$post_id}"; + if ( false === $response ) { + return $this->error_log( sprintf( __( 'Error attaching thumbnail "%d" to post "%d"', 'press-sync'), $thumbnail_id, $post_id ) ); + } + + return $this->log( __( 'Thumbnail meta attached.', 'press-sync' ) ); } /** @@ -733,18 +774,18 @@ public function allow_sync_external_host( $allow, $host, $url ) { * @param array $comments The WP Comments. */ public function attach_comments( $post_id, $comments ) { - if ( empty( $post_id ) || ! $comments ) { + $this->log( __( 'No comments to attach.', 'press-sync' ) ); return; } foreach ( $comments as $comment_args ) { - // Check to see if the comment already exists. if ( $comment = $this->comment_exists( $comment_args ) ) { continue; } + $this->log( __( 'Attaching new comment.', 'press-sync' ) ); // Set Comment Post ID to correct local Post ID. $comment_args['comment_post_ID'] = $post_id; @@ -753,11 +794,14 @@ public function attach_comments( $post_id, $comments ) { $comment_id = wp_insert_comment( $comment_args ); - if ( ! is_wp_error( $comment_id ) ) { + if ( is_wp_error( $comment_id ) ) { + $this->error_log( sprintf( __( 'Error inserting comment: "%s"', 'press-sync' ), $comment_id->get_error_message() ) ); + continue; + } - foreach ( $comment_args['meta_input'] as $meta_key => $meta_value ) { - update_comment_meta( $comment_id, $meta_key, $meta_value ); - } + $this->debug_log( __( 'Updating comment metadata.', 'press-sync' ), $comment_args['meta_input'] ); + foreach ( $comment_args['meta_input'] as $meta_key => $meta_value ) { + update_comment_meta( $comment_id, $meta_key, $meta_value ); } } } @@ -854,6 +898,8 @@ public function get_non_synced_duplicate( $post_args ) { if ( ! empty( $post_args['post_name'] ) ) { global $wpdb; + $this->log( sprintf( __( 'Looking for non-synced duplicate using post_name of "%s".', 'press-sync' ), $post_args['post_name'] ) ); + $sql = "SELECT ID, post_title, post_content, post_type, post_modified FROM {$wpdb->posts} WHERE post_name = %s AND post_type = %s"; $prepared_sql = $wpdb->prepare( $sql, $post_args['post_name'], $post_args['post_type'] ); @@ -865,6 +911,7 @@ public function get_non_synced_duplicate( $post_args ) { $content_threshold = $this->content_threshold; if ( $duplicate_post && false !== $content_threshold && 0 !== absint( $content_threshold ) ) { + $this->log( sprintf( __( 'Looking for non-synced duplicate using post_content comparison threshold of %d%%.', 'press-sync' ), $content_threshold ) ); $content_threshold = absint( $content_threshold ); // Calculate how similar the post contents are (is?). @@ -875,6 +922,8 @@ public function get_non_synced_duplicate( $post_args ) { } } + $this->log( sprintf( __( 'Result of search for non-synced duplicate: %s.', 'press-sync' ), ( $duplicate_post ? 'Found' : 'Not found' ) ) ); + return $duplicate_post; } @@ -961,18 +1010,22 @@ public function get_sync_progress( $request ) { } private function maybe_upload_remote_attachment( $attachment_args ) { + $this->log( __( 'Attempting to upload remote attachment.', 'press-sync' ) ); + $attachment_url = isset( $attachment_args['details']['url'] ) ? $attachment_args['details']['url'] : $attachment_args['attachment_url']; $attachment_post_date = isset( $attachment_args['details']['post_date'] ) ? $attachment_args['details']['post_date'] : $attachment_args['post_date']; $attachment_title = isset( $attachment_args['post_title'] ) ? $attachment_args['post_title'] : ''; $attachment_name = isset( $attachment_args['post_name'] ) ? $attachment_args['post_name'] : ''; + $this->debug_log( __( 'Incoming attachment arguments: ', 'press-sync' ), $attachment_args ); + // Check to see if the file already exists. if ( $attachment_id = $this->plugin->file_exists( $attachment_url, $attachment_post_date ) ) { + $this->log( sprintf( __( 'Found existing attachment with ID "%d"', 'press-sync' ), $attachment_id ) ); return array( 'ID' => $attachment_id ); } - $attachment_metadata = $this->get_attachment_metadata_from_request( $attachment_args ); - $temp_file = false; + $temp_file = false; require_once( ABSPATH . '/wp-admin/includes/image.php' ); require_once( ABSPATH . '/wp-admin/includes/file.php' ); @@ -1041,22 +1094,14 @@ private function maybe_upload_remote_attachment( $attachment_args ) { private function update_post_meta_array( $post_id, $meta_data = array() ) { foreach ( $meta_data as $field => $values ) { if ( is_array( $values ) ) { - update_post_meta( $post_id, $field, current( $values ) ); - continue; - - // Handle $values as an array. - if ( 1 === count( $values ) ) { - update_post_meta( $post_id, $field, maybe_unserialize( current( $values ) ) ); - } else { - // Also handle multiple keys by removing and re-adding. - delete_post_meta( $post_id, $field ); - foreach ( $values as $value ) { - add_post_meta( $post_id, $field, maybe_unserialize( $value ) ); - } - } + $value = current( $values ); } else { - update_post_meta( $post_id, $field, maybe_unserialize( $values ) ); + $value = maybe_unserialize( $values ); } + + update_post_meta( $post_id, $field, $value ); + + $this->log( sprintf( __( 'Attempted update of post meta field "%s" with value "%s".', 'press-sync' ), $field, $values ) ); } } @@ -1091,21 +1136,14 @@ public function sync_taxonomy_term( $object_args ) { } } + $this->log( sprintf( __( 'Added term "%s" to taxonomy "%s".', 'press-sync' ), $object_args['name'], $object_args['taxonomy'] ) ); + if ( ! empty( $object_args['meta_input'] ) ) { $this->maybe_update_term_meta( $term_ids['term_id'], $object_args['meta_input'] ); } } catch ( \Exception $e ) { - trigger_error( $e->getMessage() ); - return array( - 'debug' => $e->getMessage(), - ); + $this->error_log( $e->getMessage() ); } - - return array( - 'debug' => array( - 'message' => __( 'The taxonomy term was succesfully added.', 'press-sync' ), - ), - ); } /** @@ -1120,10 +1158,12 @@ private function attach_terms( $post_id, $post_args ) { if ( isset( $post_args['tax_input'] ) ) { foreach ( $post_args['tax_input'] as $taxonomy => $terms ) { if ( $this->is_partial_term_sync( $terms ) ) { + $this->log( sprintf( __( 'Doing partial term sync for term "%s" in taxonomy "%s".' , 'press-sync' ), $terms['slug'], $terms['taxonomy'] ) ); wp_set_object_terms( $post_id, $terms['slug'], $terms['taxonomy'], true ); wp_remove_object_terms( $post_id, 'uncategorized', 'category' ); continue; } + $this->maybe_create_new_terms( $taxonomy, $terms ); wp_set_object_terms( $post_id, wp_list_pluck( $terms, 'slug' ), $taxonomy, false ); } @@ -1140,11 +1180,7 @@ private function attach_terms( $post_id, $post_args ) { */ private function fix_term_relationships( $post_id, $post_args ) { $this->attach_terms( $post_id, $post_args ); - return array( - 'debug' => array( - 'message' => __( 'Fixed term relationships.', 'press-sync' ), - ), - ); + return $this->log( __( 'Fixed term relationships.', 'press-sync' ) ); } /** @@ -1186,6 +1222,7 @@ private function maybe_make_multisite_key( $key ) { private function maybe_create_new_terms( $taxonomy, $terms ) { foreach ( $terms as $term ) { if ( term_exists( $term['slug'], $taxonomy ) ) { + $this->log( sprintf( __( 'Term "%s" already exists for taxonomy "%s".', 'press-sync' ), $term['name'], $taxonomy ) ); continue; } @@ -1206,13 +1243,14 @@ private function maybe_create_new_terms( $taxonomy, $terms ) { * @param string $taxonomy The taxonomy to attach the term to. */ private function create_term( $term, $taxonomy ) { + $this->log( sprintf( __( 'Creating new term "%s" for taxonomy "%s".', 'press-sync' ), $term['name'], $taxonomy ) ); $term_result = wp_insert_term( $term['name'], $taxonomy, array( 'slug' => $term['slug'], 'description' => $term['description'], ) ); if ( is_wp_error( $term_result ) ) { - trigger_error( sprintf( __( 'Could not insert new term "%s": %s.', 'press-sync' ), $term['name'], $term_result->get_error_message() ) ); + $this->error_log( sprintf( __( 'Could not insert new term "%s": %s.', 'press-sync' ), $term['name'], $term_result->get_error_message() ) ); } return $term_result['term_id']; @@ -1228,14 +1266,21 @@ private function create_term( $term, $taxonomy ) { private function maybe_update_term_meta( $term_id, $term_meta ) { foreach ( $term_meta as $meta_key => $meta_value ) { $meta_value = is_array( $meta_value ) ? current( $meta_value ) : $meta_value; + + delete_term_meta( $term_id, $meta_key ); $meta_result = update_term_meta( $term_id, $meta_key, $meta_value ); - if ( is_wp_error( $meta_result ) ) { - trigger_error( sprintf( __( 'Error updating term meta, ambiguous term ID: %s', 'press-sync' ), $meta_result->get_error_message() ) ); - } + switch( true ) { + case is_wp_error( $meta_result ): + $this->error_log( sprintf( __( 'Error updating term meta, ambiguous term ID: %s', 'press-sync' ), $meta_result->get_error_message() ) ); + break; - if ( false === $meta_result ) { - trigger_error( sprintf( __( 'Could not add term meta for term %d.', 'press-sync' ), $term_id ) ); + case false === $meta_result: + $this->error_log( sprintf( __( 'Could not add term meta for term %d, it is possible the term meta has not changed.', 'press-sync' ), $term_id ) ); + break; + + default: + $this->log( sprintf( __( 'Updated term meta, added "%s" with value "%s".', 'press-sync' ), $meta_key, var_export( $meta_value, 1 ) ) ); } } } @@ -1248,7 +1293,7 @@ private function maybe_update_term_meta( $term_id, $term_meta ) { * @return bool */ private function is_partial_term_sync( $terms ) { - return 2 == count( $terms ); + return 2 == count( $terms ) && ! is_array( current( $terms ) ); } /** @@ -1265,9 +1310,62 @@ private function clean_post_object_args( $args ) { && '0000-00-00 00:00:00' === $args['post_date'] && ! in_array( $args['post_status'], array( 'draft', 'pending' ) ) ) { + $this->log( __( 'Giving post a date of the Unix Epoch (incoming date was (0000-00-00 00:00:00").', 'press-sync' ) ); $args['post_date'] = date( 'Y-m-d H:i:s', 0 ); } return $args; } + + /** + * Log a message. + * + * @since NEXT + * @param string $message The message to add to the logs. + * @param string $prefix This prefix will be prepended to the log message to denote the log type. + */ + public function log( $message, $prefix = 'i' ) { + $this->logs[] = "[{$prefix}] {$message}"; + } + + /** + * Log an error message with a severity level. + * + * @since NEXT + * @param string $message The error message to add to the logs. + * @param int $level The log level to use (must be one of E_USER_NOTICE, E_USER_WARNING, or E_USER_ERROR). + */ + public function error_log( $message, $level = E_USER_NOTICE ) { + trigger_error( $message, $level ); + $this->log( $message, 'e' ); + } + + /** + * Log a debugging message and debugging information. + * + * @since NEXT + * @param string $message The debugging message to add to the logs. + * @param mixed $extra If supplied, this variable will be cleaned up and printed as a string in the logs. + */ + public function debug_log( $message, $extra = null ) { + $this->log( $message, 'd' ); + + if ( $extra ) { + $extra = print_r( $extra, 1 ); + $extra = trim( $extra ); + $extra = strtr( $extra, [ "\n" => ' ' ] ); + $extra = preg_replace( '/\s+/', ' ', $extra ); + $this->log( $extra, 'd' ); + } + } + + /** + * Get the internal $logs property. + * + * @since NEXT + * @return array + */ + public function get_log() { + return $this->logs; + } } diff --git a/includes/class-dashboard.php b/includes/class-dashboard.php index 00a1690..3618062 100644 --- a/includes/class-dashboard.php +++ b/includes/class-dashboard.php @@ -128,6 +128,13 @@ public function load_scripts() { wp_enqueue_script( 'press-sync', $press_sync_js, true ); wp_localize_script( 'press-sync', 'press_sync', array( 'ajax_url' => admin_url( 'admin-ajax.php' ) ) ); + + // jQuery UI datepicker support. + wp_enqueue_script( 'jquery-ui-datepicker' ); + + // Get the styles for the datepicker. + wp_register_style('jquery-ui', 'http://ajax.googleapis.com/ajax/libs/jqueryui/1.8/themes/base/jquery-ui.css'); + wp_enqueue_style( 'jquery-ui' ); } /** diff --git a/includes/class-press-sync.php b/includes/class-press-sync.php index 93fc097..38031f3 100644 --- a/includes/class-press-sync.php +++ b/includes/class-press-sync.php @@ -45,6 +45,22 @@ class Press_Sync { */ public $remote_domain = null; + /** + * Sync posts modified after this date. + * + * @var string + * @since 0.8.0 + */ + private $delta_date = false; + + /** + * The last connection error when attempting to contact the remote site. + * + * @var string + * @since NEXT + */ + private $last_connection_error = ''; + /** * Initialize the class instance. * @@ -185,7 +201,6 @@ public function init_connection( $remote_domain = '' ) { * @return boolean */ public function check_connection( $url = '' ) { - $url = $this->get_remote_url( $url ); $remote_get_args = array( @@ -194,15 +209,36 @@ public function check_connection( $url = '' ) { $response = wp_remote_get( $url, $remote_get_args ); $response_code = wp_remote_retrieve_response_code( $response ); + $response_body = null; + $success = false; - if ( 200 === $response_code ) { - $response_body = json_decode( wp_remote_retrieve_body( $response ), true ); + try { + if ( 200 === absint( $response_code ) ) { + $response_body = json_decode( wp_remote_retrieve_body( $response ), true ); + $success = isset( $response_body['success'] ) ? $response_body['success'] : false; - return isset( $response_body['success'] ) ? $response_body['success'] : false; - } + if ( ! $success ) { + $message = var_export( $response, 1 ); - return false; + if ( ! empty( $response_body['data']['message'] ) ) { + $message = $response_body['data']['message']; + } + + throw new \Exception( $message ); + } + } + if ( is_wp_error( $response ) ) { + throw new \Exception( $response->get_error_message() ); + } + + } catch ( \Exception $e ) { + $error = __( '
Got response code: "%d".
Got response: "%s"', 'press-sync' ); + $message = $e->getMessage(); + $this->last_connection_error = sprintf( $error, $response_code, $message ); + } + + return $success; } /** @@ -405,7 +441,17 @@ public function get_relationships( $object_id, $taxonomies ) { $sql = $GLOBALS['wpdb']->prepare( $sql, $object_id ); $terms = $GLOBALS['wpdb']->get_results( $sql, ARRAY_A ); - return $terms; + $final_terms = []; + + foreach ( $terms as $term ) { + if ( ! isset( $final_terms[ $term['taxonomy'] ] ) ) { + $final_terms[ $term['taxonomy'] ] = []; + } + + $final_terms[ $term['taxonomy'] ][] = $term['slug']; + } + + return $final_terms; } foreach ( $taxonomies as $key => $taxonomy ) { @@ -1018,8 +1064,19 @@ public function sync_batch( $content_type = 'post', $settings = array(), $next_p $this->init_connection( $settings['ps_remote_domain'] ); // Build out the url and send the data to the remote site. - $url = $this->get_remote_url( '', 'sync' ); - $logs = $this->send_data_to_remote_site( $url, $objects_args ); + $url = $this->get_remote_url( '', 'sync' ); + $response = $this->send_data_to_remote_site( $url, $objects_args ); + + $response = json_decode( $response, true ); + $object_status = $logs = array(); + + if ( isset( $response['responses'] ) ) { + $object_status = $response['responses']; + } + + if ( isset( $response['log'] ) ) { + $logs = $response['log']; + } return array( 'ps_objects_to_sync' => $content_type, @@ -1027,6 +1084,7 @@ public function sync_batch( $content_type = 'post', $settings = array(), $next_p 'total_objects_processed' => ( $next_page * $this->settings['ps_page_size'] ) - ( $this->settings['ps_page_size'] - count( $objects ) ), 'next_page' => $next_page + 1, 'log' => $logs, + 'status' => $object_status, ); } @@ -1386,7 +1444,7 @@ public function change_the_next_page( $next_page = 1 ) { if ( 0 < $page_offset && 1 === absint( $next_page ) ) { $page_offset = floor( $page_offset / $this->settings['ps_page_size'] ); - $next_page += ( $page_offset - 1); + $next_page += ( $page_offset - 1 ); error_log( '----NP: ' . $next_page ); } @@ -1437,7 +1495,7 @@ public function get_synced_object_ids( $objects_to_sync ) { update_option( $option_name, $response_body['data']['synced'] ); return $response_body['data']['synced']; - } + } /** * Removes post IDs if the option to preserve them isn't active. @@ -1500,7 +1558,7 @@ public function count_taxonomy_term_to_sync() { * Gets the next set of taxonomies/terms to sync. * * @since 0.7.0 - * @param int $next_page The page of results to get. + * @param int $next_page The page of results to get. * @return array */ public function get_taxonomy_term_to_sync( $next_page ) { @@ -1597,6 +1655,7 @@ public function maybe_get_terms_for_post( $where ) { object_id = %d ) SQL; + $where = $GLOBALS['wpdb']->prepare( $where, $this->settings['ps_testing_post'] ); return $where; } @@ -1638,4 +1697,14 @@ public function prepare_post_delta_date( $settings ) { $settings['ps_delta_date'] = date( 'y-m-d 00:00:00', strtotime( $settings['ps_delta_date'] ) ) ?: false; return $settings; } + + /** + * Gets the last connection error. + * + * @since NEXT + * @return string + */ + public function get_connection_error() { + return $this->last_connection_error; + } } diff --git a/views/dashboard/html-bulk-sync.php b/views/dashboard/html-bulk-sync.php index e6be773..0cbe404 100644 --- a/views/dashboard/html-bulk-sync.php +++ b/views/dashboard/html-bulk-sync.php @@ -5,7 +5,10 @@

This tool allows you to synchronize this entire site (or a portion of it) with another WordPress site.

Enter your settings below. Save the changes. Press "Sync" to trigger Press Sync.

-

+

+ + +

Check your remote Press Sync key. You are not connected to the remote site.

@@ -18,88 +21,101 @@
-

Bulk Sync Settings

-
- - - - - - - - + + + + + + + + + + + + + + + + + + + + + + +
Sync Method - - - Advanced Options -
    - - -
  • - -
- - There are no advanced options configured. - + +
+

Bulk Sync Settings

+ + + + + + + - - - - - - - - - - - - - - - - - - - - - - -
Sync Method +
Objects to Sync - -

Define the WP objects you want to synchronize with the remote site.

-
WP Options to Sync - -

The comma-separated list of WP options you want to synchronize when "Objects To Sync" is "Options".

-
Duplicate Action - -

How do you want to handle non-synced duplicates? The "sync" option will give a non-synced duplicate a press sync ID to be synced for the future.

-
Force Update? - -

Force the content on the remote site to be overwritten when the sync method is "push".

-
Ignore Comments? - -

Checking this box ommits comments from being synced to the remote site.

-
- - -
+ +
+ Advanced Options +
    + + +
  • + +
+ + There are no advanced options configured. + +
Objects to Sync + +

Define the WP objects you want to synchronize with the remote site.

+
WP Options to Sync + +

The comma-separated list of WP options you want to synchronize when "Objects To Sync" is "Options".

+
Duplicate Action + +

How do you want to handle non-synced duplicates? The "sync" option will give a non-synced duplicate a press sync ID to be synced for the future.

+
Force Update? + +

Force the content on the remote site to be overwritten when the sync method is "push".

+
Ignore Comments? + +

Checking this box ommits comments from being synced to the remote site.

+
+ + + diff --git a/views/dashboard/html-credentials.php b/views/dashboard/html-credentials.php index c25a6d6..a89b721 100644 --- a/views/dashboard/html-credentials.php +++ b/views/dashboard/html-credentials.php @@ -39,6 +39,7 @@ Connected Not connected. Please check your remote secret key and domain for incorrect spellings. +