4 * LiveJournal API Importer
10 // XML-RPC library for communicating with LiveJournal API
11 require_once( ABSPATH . WPINC . '/class-IXR.php' );
14 * LiveJournal API Importer class
16 * Imports your LiveJournal contents into WordPress using the LJ API
22 var $comments_url = 'http://www.livejournal.com/export_comments.bml';
23 var $ixr_url = 'http://www.livejournal.com/interface/xmlrpc';
32 var $pointers = array();
34 // This list taken from LJ, they don't appear to have an API for it
35 var $moods = array( '1' => 'aggravated',
38 '101' => 'contemplative',
66 '127' => 'distressed',
67 '128' => 'intimidated',
99 '36' => 'mischievous',
102 '39' => 'melancholy',
109 '45' => 'determined',
111 '47' => 'frustrated',
112 '48' => 'indescribable',
119 '55' => 'disappointed',
127 '62' => 'rejuvenated',
128 '63' => 'complacent',
130 '65' => 'indifferent',
136 '70' => 'optimistic',
137 '71' => 'pessimistic',
140 '74' => 'uncomfortable',
145 '79' => 'embarrassed',
148 '81' => 'sympathetic',
156 '89' => 'productive',
158 '90' => 'accomplished',
169 echo '<div class="wrap">';
171 echo '<h2>' . __( 'Import LiveJournal' ) . '</h2>';
181 <form action="admin.php?import=livejournal" method="post">
182 <?php wp_nonce_field( 'lj-api-import' ) ?>
183 <?php if ( get_option( 'ljapi_username' ) && get_option( 'ljapi_password' ) ) : ?>
184 <input type="hidden" name="step" value="<?php echo esc_attr( get_option( 'ljapi_step' ) ) ?>" />
185 <p><?php _e( 'It looks like you attempted to import your LiveJournal posts previously and got interrupted.' ) ?></p>
187 <input type="submit" class="button-primary" value="<?php esc_attr_e( 'Continue previous import' ) ?>" />
189 <p class="submitbox"><a href="<?php echo esc_url($_SERVER['PHP_SELF'] . '?import=livejournal&step=-1&_wpnonce=' . wp_create_nonce( 'lj-api-import' ) . '&_wp_http_referer=' . esc_attr( $_SERVER['REQUEST_URI'] )) ?>" class="deletion submitdelete"><?php _e( 'Cancel & start a new import' ) ?></a></p>
192 <input type="hidden" name="step" value="1" />
193 <input type="hidden" name="login" value="true" />
194 <p><?php _e( 'Howdy! This importer allows you to connect directly to LiveJournal and download all your entries and comments' ) ?></p>
195 <p><?php _e( 'Enter your LiveJournal username and password below so we can connect to your account:' ) ?></p>
197 <table class="form-table">
200 <th scope="row"><label for="lj_username"><?php _e( 'LiveJournal Username' ) ?></label></th>
201 <td><input type="text" name="lj_username" id="lj_username" class="regular-text" /></td>
205 <th scope="row"><label for="lj_password"><?php _e( 'LiveJournal Password' ) ?></label></th>
206 <td><input type="password" name="lj_password" id="lj_password" class="regular-text" /></td>
211 <p><?php _e( 'If you have any entries on LiveJournal which are marked as private, they will be password-protected when they are imported so that only people who know the password can see them.' ) ?></p>
212 <p><?php _e( 'If you don’t enter a password, ALL ENTRIES from your LiveJournal will be imported as public posts in WordPress.' ) ?></p>
213 <p><?php _e( 'Enter the password you would like to use for all protected entries here:' ) ?></p>
214 <table class="form-table">
217 <th scope="row"><label for="protected_password"><?php _e( 'Protected Post Password' ) ?></label></th>
218 <td><input type="text" name="protected_password" id="protected_password" class="regular-text" /></td>
223 <p><?php _e( "<strong>WARNING:</strong> This can take a really long time if you have a lot of entries in your LiveJournal, or a lot of comments. Ideally, you should only start this process if you can leave your computer alone while it finishes the import." ) ?></p>
226 <input type="submit" class="button-primary" value="<?php esc_attr_e( 'Connect to LiveJournal and Import' ) ?>" />
229 <p><?php _e( '<strong>NOTE:</strong> If the import process is interrupted for <em>any</em> reason, come back to this page and it will continue from where it stopped automatically.' ) ?></p>
232 <p><?php _e( '<strong>NOTE:</strong> You appear to have JavaScript disabled, so you will need to manually click through each step of this importer. If you enable JavaScript, it will step through automatically.' ) ?></p>
240 function download_post_meta() {
241 $total = (int) get_option( 'ljapi_total' );
242 $count = (int) get_option( 'ljapi_count' );
243 $lastsync = get_option( 'ljapi_lastsync' );
245 update_option( 'ljapi_lastsync', '1900-01-01 00:00:00' );
247 $sync_item_times = get_option( 'ljapi_sync_item_times' );
248 if ( !is_array( $sync_item_times ) )
249 $sync_item_times = array();
252 $lastsync = date( 'Y-m-d H:i:s', strtotime( get_option( 'ljapi_lastsync' ) ) );
253 $synclist = $this->lj_ixr( 'syncitems', array( 'ver' => 1, 'lastsync' => $lastsync ) );
254 if ( is_wp_error( $synclist ) )
257 // Keep track of if we've downloaded everything
258 $total = $synclist['total'];
259 $count = $synclist['count'];
261 foreach ( $synclist['syncitems'] as $event ) {
262 if ( substr( $event['item'], 0, 2 ) == 'L-' ) {
263 $sync_item_times[ str_replace( 'L-', '', $event['item'] ) ] = $event['time'];
264 if ( $event['time'] > $lastsync ) {
265 $lastsync = $event['time'];
266 update_option( 'ljapi_lastsync', $lastsync );
270 } while ( $total > $count );
271 // endwhile - all post meta is cached locally
273 update_option( 'ljapi_sync_item_times', $sync_item_times );
274 update_option( 'ljapi_total', $total );
275 update_option( 'ljapi_count', $count );
277 echo '<p>' . __( 'Post metadata has been downloaded, proceeding with posts...' ) . '</p>';
280 function download_post_bodies() {
281 $imported_count = (int) get_option( 'ljapi_imported_count' );
282 $sync_item_times = get_option( 'ljapi_sync_item_times' );
283 $lastsync = get_option( 'ljapi_lastsync_posts' );
285 update_option( 'ljapi_lastsync_posts', date( 'Y-m-d H:i:s', 0 ) );
290 $lastsync = date( 'Y-m-d H:i:s', strtotime( get_option( 'ljapi_lastsync_posts' ) ) );
292 // Get the batch of items that match up with the syncitems list
293 $itemlist = $this->lj_ixr( 'getevents', array( 'ver' => 1,
294 'selecttype' => 'syncitems',
295 'lineendings' => 'pc',
296 'lastsync' => $lastsync ) );
297 if ( is_wp_error( $itemlist ) )
300 if ( $num = count( $itemlist['events'] ) ) {
301 for ( $e = 0; $e < count( $itemlist['events'] ); $e++ ) {
302 $event = $itemlist['events'][$e];
304 $inserted = $this->import_post( $event );
305 if ( is_wp_error( $inserted ) )
307 if ( $sync_item_times[ $event['itemid'] ] > $lastsync )
308 $lastsync = $sync_item_times[ $event['itemid'] ];
311 update_option( 'ljapi_lastsync_posts', $lastsync );
312 update_option( 'ljapi_imported_count', $imported_count );
313 update_option( 'ljapi_last_sync_count', $num );
316 } while ( $num > 0 && $count < 3 ); // Doing up to 3 requests at a time to avoid memory problems
318 // Used so that step1 knows when to stop posting back on itself
319 update_option( 'ljapi_last_sync_count', $num );
321 // Counter just used to show progress to user
322 update_option( 'ljapi_post_batch', ( (int) get_option( 'ljapi_post_batch' ) + 1 ) );
327 function _normalize_tag( $matches ) {
328 return '<' . strtolower( $matches[1] );
331 function import_post( $post ) {
334 // Make sure we haven't already imported this one
335 if ( $this->get_wp_post_ID( $post['itemid'] ) )
338 $user = wp_get_current_user();
339 $post_author = $user->ID;
340 $post['security'] = !empty( $post['security'] ) ? $post['security'] : '';
341 $post_status = ( 'private' == trim( $post['security'] ) ) ? 'private' : 'publish'; // Only me
342 $post_password = ( 'usemask' == trim( $post['security'] ) ) ? $this->protected_password : ''; // "Friends" via password
344 // For some reason, LJ sometimes sends a date as "2004-04-1408:38:00" (no space btwn date/time)
345 $post_date = $post['eventtime'];
346 if ( 18 == strlen( $post_date ) )
347 $post_date = substr( $post_date, 0, 10 ) . ' ' . substr( $post_date, 10 );
349 // Cleaning up and linking the title
350 $post_title = isset( $post['subject'] ) ? trim( $post['subject'] ) : '';
351 $post_title = $this->translate_lj_user( $post_title ); // Translate it, but then we'll strip the link
352 $post_title = strip_tags( $post_title ); // Can't have tags in the title in WP
353 $post_title = $wpdb->escape( $post_title );
356 $post_content = $post['event'];
357 $post_content = preg_replace_callback( '|<(/?[A-Z]+)|', array( &$this, '_normalize_tag' ), $post_content );
358 // XHTMLize some tags
359 $post_content = str_replace( '<br>', '<br />', $post_content );
360 $post_content = str_replace( '<hr>', '<hr />', $post_content );
361 // lj-cut ==> <!--more-->
362 $post_content = preg_replace( '|<lj-cut text="([^"]*)">|is', '<!--more $1-->', $post_content );
363 $post_content = str_replace( array( '<lj-cut>', '</lj-cut>' ), array( '<!--more-->', '' ), $post_content );
364 $first = strpos( $post_content, '<!--more' );
365 $post_content = substr( $post_content, 0, $first + 1 ) . preg_replace( '|<!--more(.*)?-->|sUi', '', substr( $post_content, $first + 1 ) );
366 // lj-user ==> a href
367 $post_content = $this->translate_lj_user( $post_content );
368 //$post_content = force_balance_tags( $post_content );
369 $post_content = $wpdb->escape( $post_content );
371 // Handle any tags associated with the post
372 $tags_input = !empty( $post['props']['taglist'] ) ? $post['props']['taglist'] : '';
374 // Check if comments are closed on this post
375 $comment_status = !empty( $post['props']['opt_nocomments'] ) ? 'closed' : 'open';
378 if ( $post_id = post_exists( $post_title, $post_content, $post_date ) ) {
379 printf( __( 'Post <strong>%s</strong> already exists.' ), stripslashes( $post_title ) );
381 printf( __( 'Imported post <strong>%s</strong>...' ), stripslashes( $post_title ) );
382 $postdata = compact( 'post_author', 'post_date', 'post_content', 'post_title', 'post_status', 'post_password', 'tags_input', 'comment_status' );
383 $post_id = wp_insert_post( $postdata, true );
384 if ( is_wp_error( $post_id ) ) {
385 if ( 'empty_content' == $post_id->get_error_code() )
386 return; // Silent skip on "empty" posts
390 _e( 'Couldn’t get post ID (creating post failed!)' );
392 return new WP_Error( 'insert_post_failed', __( 'Failed to create post.' ) );
395 // Handle all the metadata for this post
396 $this->insert_postmeta( $post_id, $post );
401 // Convert lj-user tags to links to that user
402 function translate_lj_user( $str ) {
403 return preg_replace( '|<lj\s+user\s*=\s*["\']([\w-]+)["\']>|', '<a href="http://$1.livejournal.com/" class="lj-user">$1</a>', $str );
406 function insert_postmeta( $post_id, $post ) {
407 // Need the original LJ id for comments
408 add_post_meta( $post_id, 'lj_itemid', $post['itemid'] );
410 // And save the permalink on LJ in case we want to link back or something
411 add_post_meta( $post_id, 'lj_permalink', $post['url'] );
413 // Supports the following "props" from LJ, saved as lj_<prop_name> in wp_postmeta
414 // Adult Content - adult_content
415 // Location - current_coords + current_location
416 // Mood - current_mood (translated from current_moodid)
417 // Music - current_music
418 // Userpic - picture_keyword
419 foreach ( array( 'adult_content', 'current_coords', 'current_location', 'current_moodid', 'current_music', 'picture_keyword' ) as $prop ) {
420 if ( !empty( $post['props'][$prop] ) ) {
421 if ( 'current_moodid' == $prop ) {
422 $prop = 'current_mood';
423 $val = $this->moods[ $post['props']['current_moodid'] ];
425 $val = $post['props'][$prop];
427 add_post_meta( $post_id, 'lj_' . $prop, $val );
432 // Set up a session (authenticate) with LJ
433 function get_session() {
434 // Get a session via XMLRPC
435 $cookie = $this->lj_ixr( 'sessiongenerate', array( 'ver' => 1, 'expiration' => 'short' ) );
436 if ( is_wp_error( $cookie ) )
437 return new WP_Error( 'cookie', __( 'Could not get a cookie from LiveJournal. Please try again soon.' ) );
438 return new WP_Http_Cookie( array( 'name' => 'ljsession', 'value' => $cookie['ljsession'] ) );
441 // Loops through and gets comment meta from LJ in batches
442 function download_comment_meta() {
443 $cookie = $this->get_session();
444 if ( is_wp_error( $cookie ) )
447 // Load previous state (if any)
448 $this->usermap = (array) get_option( 'ljapi_usermap' );
449 $maxid = get_option( 'ljapi_maxid' ) ? get_option( 'ljapi_maxid' ) : 1;
450 $highest_id = get_option( 'ljapi_highest_id' ) ? get_option( 'ljapi_highest_id' ) : 0;
452 // We need to loop over the metadata request until we have it all
453 while ( $maxid > $highest_id ) {
454 // Now get the meta listing
455 $results = wp_remote_get( $this->comments_url . '?get=comment_meta&startid=' . ( $highest_id + 1 ),
456 array( 'cookies' => array( $cookie ), 'timeout' => 20 ) );
457 if ( is_wp_error( $results ) )
458 return new WP_Error( 'comment_meta', __( 'Failed to retrieve comment meta information from LiveJournal. Please try again soon.' ) );
460 $results = wp_remote_retrieve_body( $results );
462 // Get the maxid so we know if we have them all yet
463 preg_match( '|<maxid>(\d+)</maxid>|', $results, $matches );
464 if ( 0 == $matches[1] ) {
465 // No comment meta = no comments for this journal
466 echo '<p>' . __( 'You have no comments to import!' ) . '</p>';
467 update_option( 'ljapi_highest_id', 1 );
468 update_option( 'ljapi_highest_comment_id', 1 );
469 return false; // Bail out of comment importing entirely
471 $maxid = !empty( $matches[1] ) ? $matches[1] : $maxid;
473 // Parse comments and get highest id available
474 preg_match_all( '|<comment id=\'(\d+)\'|is', $results, $matches );
475 foreach ( $matches[1] as $id ) {
476 if ( $id > $highest_id )
480 // Parse out the list of user mappings, and add it to the known list
481 preg_match_all( '|<usermap id=\'(\d+)\' user=\'([^\']+)\' />|', $results, $matches );
482 foreach ( $matches[1] as $count => $userid )
483 $this->usermap[$userid] = $matches[2][$count]; // need this in memory for translating ids => names
487 // endwhile - should have seen all comment meta at this point
489 update_option( 'ljapi_usermap', $this->usermap );
490 update_option( 'ljapi_maxid', $maxid );
491 update_option( 'ljapi_highest_id', $highest_id );
493 echo '<p>' . __( ' Comment metadata downloaded successfully, proceeding with comment bodies...' ) . '</p>';
498 // Downloads actual comment bodies from LJ
499 // Inserts them all directly to the DB, with additional info stored in "spare" fields
500 function download_comment_bodies() {
502 $cookie = $this->get_session();
503 if ( is_wp_error( $cookie ) )
506 // Load previous state (if any)
507 $this->usermap = (array) get_option( 'ljapi_usermap' );
508 $maxid = get_option( 'ljapi_maxid' ) ? (int) get_option( 'ljapi_maxid' ) : 1;
509 $highest_id = (int) get_option( 'ljapi_highest_comment_id' );
511 while ( $maxid > $highest_id && $loop < 5 ) { // We do 5 loops per call to avoid memory limits
514 // Get a batch of comments, using the highest_id we've already got as a starting point
515 $results = wp_remote_get( $this->comments_url . '?get=comment_body&startid=' . ( $highest_id + 1 ),
516 array( 'cookies' => array( $cookie ), 'timeout' => 20 ) );
517 if ( is_wp_error( $results ) )
518 return new WP_Error( 'comment_bodies', __( 'Failed to retrieve comment bodies from LiveJournal. Please try again soon.' ) );
520 $results = wp_remote_retrieve_body( $results );
522 // Parse out each comment and insert directly
523 preg_match_all( '|<comment id=\'(\d+)\'.*</comment>|iUs', $results, $matches );
524 for ( $c = 0; $c < count( $matches[0] ); $c++ ) {
525 // Keep track of highest id seen
526 if ( $matches[1][$c] > $highest_id ) {
527 $highest_id = $matches[1][$c];
528 update_option( 'ljapi_highest_comment_id', $highest_id );
531 $comment = $matches[0][$c];
533 // Filter out any captured, deleted comments (nothing useful to import)
534 $comment = preg_replace( '|<comment id=\'\d+\' jitemid=\'\d+\' posterid=\'\d+\' state=\'D\'[^/]*/>|is', '', $comment );
536 // Parse this comment into an array and insert
537 $comment = $this->parse_comment( $comment );
538 $comment = wp_filter_comment( $comment );
539 $id = wp_insert_comment( $comment );
542 clean_comment_cache( $id );
545 // Clear cache to preseve memory
548 // endwhile - all comments downloaded and ready for bulk processing
550 // Counter just used to show progress to user
551 update_option( 'ljapi_comment_batch', ( (int) get_option( 'ljapi_comment_batch' ) + 1 ) );
556 // Takes a block of XML and parses out all the elements of the comment
557 function parse_comment( $comment ) {
560 // Get the top-level attributes
561 preg_match( '|<comment([^>]+)>|i', $comment, $attribs );
562 preg_match( '| id=\'(\d+)\'|i', $attribs[1], $matches );
563 $lj_comment_ID = $matches[1];
564 preg_match( '| jitemid=\'(\d+)\'|i', $attribs[1], $matches );
565 $lj_comment_post_ID = $matches[1];
566 preg_match( '| posterid=\'(\d+)\'|i', $attribs[1], $matches );
567 $comment_author_ID = isset( $matches[1] ) ? $matches[1] : 0;
568 preg_match( '| parentid=\'(\d+)\'|i', $attribs[1], $matches ); // optional
569 $lj_comment_parent = isset( $matches[1] ) ? $matches[1] : 0;
570 preg_match( '| state=\'([SDFA])\'|i', $attribs[1], $matches ); // optional
571 $lj_comment_state = isset( $matches[1] ) ? $matches[1] : 'A';
573 // Clean up "subject" - this will become the first line of the comment in WP
574 preg_match( '|<subject>(.*)</subject>|is', $comment, $matches );
575 if ( isset( $matches[1] ) ) {
576 $comment_subject = $wpdb->escape( trim( $matches[1] ) );
577 if ( 'Re:' == $comment_subject )
578 $comment_subject = '';
581 // Get the body and HTMLize it
582 preg_match( '|<body>(.*)</body>|is', $comment, $matches );
583 $comment_content = !empty( $comment_subject ) ? $comment_subject . "\n\n" . $matches[1] : $matches[1];
584 $comment_content = @html_entity_decode( $comment_content, ENT_COMPAT, get_option('blog_charset') );
585 $comment_content = str_replace( ''', "'", $comment_content );
586 $comment_content = wpautop( $comment_content );
587 $comment_content = str_replace( '<br>', '<br />', $comment_content );
588 $comment_content = str_replace( '<hr>', '<hr />', $comment_content );
589 $comment_content = preg_replace_callback( '|<(/?[A-Z]+)|', array( &$this, '_normalize_tag' ), $comment_content );
590 $comment_content = $wpdb->escape( trim( $comment_content ) );
592 // Get and convert the date
593 preg_match( '|<date>(.*)</date>|i', $comment, $matches );
594 $comment_date = trim( str_replace( array( 'T', 'Z' ), ' ', $matches[1] ) );
596 // Grab IP if available
597 preg_match( '|<property name=\'poster_ip\'>(.*)</property>|i', $comment, $matches ); // optional
598 $comment_author_IP = isset( $matches[1] ) ? $matches[1] : '';
600 // Try to get something useful for the comment author, especially if it was "my" comment
601 $author = ( empty( $comment_author_ID ) || empty( $this->usermap[$comment_author_ID] ) || substr( $this->usermap[$comment_author_ID], 0, 4 ) == 'ext_' ) ? __( 'Anonymous' ) : $this->usermap[$comment_author_ID];
602 if ( get_option( 'ljapi_username' ) == $author ) {
603 $user = wp_get_current_user();
604 $user_id = $user->ID;
605 $author = $user->display_name;
606 $url = trailingslashit( get_option( 'home' ) );
609 $url = ( __( 'Anonymous' ) == $author ) ? '' : 'http://' . $author . '.livejournal.com/';
612 // Send back the array of details
613 return array( 'lj_comment_ID' => $lj_comment_ID,
614 'lj_comment_post_ID' => $lj_comment_post_ID,
615 'lj_comment_parent' => ( !empty( $lj_comment_parent ) ? $lj_comment_parent : 0 ),
616 'lj_comment_state' => $lj_comment_state,
617 'comment_post_ID' => $this->get_wp_post_ID( $lj_comment_post_ID ),
618 'comment_author' => $author,
619 'comment_author_url' => $url,
620 'comment_author_email' => '',
621 'comment_content' => $comment_content,
622 'comment_date' => $comment_date,
623 'comment_author_IP' => ( !empty( $comment_author_IP ) ? $comment_author_IP : '' ),
624 'comment_approved' => ( in_array( $lj_comment_state, array( 'A', 'F' ) ) ? 1 : 0 ),
625 'comment_karma' => $lj_comment_ID, // Need this and next value until rethreading is done
626 'comment_agent' => $lj_comment_parent,
627 'comment_type' => 'livejournal', // Custom type, so we can find it later for processing
628 'user_ID' => $user_id
633 // Gets the post_ID that a LJ post has been saved as within WP
634 function get_wp_post_ID( $post ) {
637 if ( empty( $this->postmap[$post] ) )
638 $this->postmap[$post] = (int) $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = 'lj_itemid' AND meta_value = %d", $post ) );
640 return $this->postmap[$post];
643 // Gets the comment_ID that a LJ comment has been saved as within WP
644 function get_wp_comment_ID( $comment ) {
646 if ( empty( $this->commentmap[$comment] ) )
647 $this->commentmap[$comment] = $wpdb->get_var( $wpdb->prepare( "SELECT comment_ID FROM $wpdb->comments WHERE comment_karma = %d", $comment ) );
648 return $this->commentmap[$comment];
652 if ( $challenge = $this->ixr->query( 'LJ.XMLRPC.getchallenge' ) ) {
653 $challenge = $this->ixr->getResponse();
655 if ( isset( $challenge['challenge'] ) ) {
656 $params = array( 'username' => $this->username,
657 'auth_method' => 'challenge',
658 'auth_challenge' => $challenge['challenge'],
659 'auth_response' => md5( $challenge['challenge'] . md5( $this->password ) ) );
661 return new WP_Error( 'IXR', __( 'LiveJournal is not responding to authentication requests. Please wait a while and then try again.' ) );
664 $args = func_get_args();
665 $method = array_shift( $args );
666 if ( isset( $args[0] ) )
667 $params = array_merge( $params, $args[0] );
668 if ( $this->ixr->query( 'LJ.XMLRPC.' . $method, $params ) ) {
669 return $this->ixr->getResponse();
671 return new WP_Error( 'IXR', __( 'XML-RPC Request Failed -- ' ) . $this->ixr->getErrorCode() . ': ' . $this->ixr->getErrorMessage() );
675 function dispatch() {
676 if ( empty( $_REQUEST['step'] ) )
679 $step = (int) $_REQUEST['step'];
686 // Intentional no break
693 check_admin_referer( 'lj-api-import' );
694 $result = $this->{ 'step' . $step }();
695 if ( is_wp_error( $result ) ) {
696 $this->throw_error( $result, $step );
704 // Technically the first half of step 1, this is separated to allow for AJAX
705 // calls. Sets up some variables and options and confirms authentication.
708 // Get details from form or from DB
709 if ( !empty( $_POST['lj_username'] ) && !empty( $_POST['lj_password'] ) ) {
710 // Store details for later
711 $this->username = $_POST['lj_username'];
712 $this->password = $_POST['lj_password'];
713 update_option( 'ljapi_username', $this->username );
714 update_option( 'ljapi_password', $this->password );
716 $this->username = get_option( 'ljapi_username' );
717 $this->password = get_option( 'ljapi_password' );
720 // This is the password to set on protected posts
721 if ( !empty( $_POST['protected_password'] ) ) {
722 $this->protected_password = $_POST['protected_password'];
723 update_option( 'ljapi_protected_password', $this->protected_password );
725 $this->protected_password = get_option( 'ljapi_protected_password' );
728 // Log in to confirm the details are correct
729 if ( empty( $this->username ) || empty( $this->password ) ) {
731 <p><?php _e( 'Please enter your LiveJournal username <em>and</em> password so we can download your posts and comments.' ) ?></p>
732 <p><a href="<?php echo esc_url($_SERVER['PHP_SELF'] . '?import=livejournal&step=-1&_wpnonce=' . wp_create_nonce( 'lj-api-import' ) . '&_wp_http_referer=' . esc_attr( str_replace( '&step=1', '', $_SERVER['REQUEST_URI'] ) ) ) ?>"><?php _e( 'Start again' ) ?></a></p>
736 $verified = $this->lj_ixr( 'login' );
737 if ( is_wp_error( $verified ) ) {
738 if ( 100 == $this->ixr->getErrorCode() || 101 == $this->ixr->getErrorCode() ) {
739 delete_option( 'ljapi_username' );
740 delete_option( 'ljapi_password' );
741 delete_option( 'ljapi_protected_password' );
743 <p><?php _e( 'Logging in to LiveJournal failed. Check your username and password and try again.' ) ?></p>
744 <p><a href="<?php echo esc_url($_SERVER['PHP_SELF'] . '?import=livejournal&step=-1&_wpnonce=' . wp_create_nonce( 'lj-api-import' ) . '&_wp_http_referer=' . esc_attr( str_replace( '&step=1', '', $_SERVER['REQUEST_URI'] ) ) ) ?>"><?php _e( 'Start again' ) ?></a></p>
751 update_option( 'ljapi_verified', 'yes' );
754 // Set up some options to avoid them autoloading (these ones get big)
755 add_option( 'ljapi_sync_item_times', '', '', 'no' );
756 add_option( 'ljapi_usermap', '', '', 'no' );
757 update_option( 'ljapi_comment_batch', 0 );
762 // Check form inputs and start importing posts
766 update_option( 'ljapi_step', 1 );
767 if ( !$this->ixr ) $this->ixr = new IXR_Client( $this->ixr_url, false, 80, 30 );
768 if ( empty( $_POST['login'] ) ) {
769 // We're looping -- load some details from DB
770 $this->username = get_option( 'ljapi_username' );
771 $this->password = get_option( 'ljapi_password' );
772 $this->protected_password = get_option( 'ljapi_protected_password' );
774 // First run (non-AJAX)
775 $setup = $this->setup();
778 } else if ( is_wp_error( $setup ) ) {
779 $this->throw_error( $setup, 1 );
784 echo '<div id="ljapi-status">';
785 echo '<h3>' . __( 'Importing Posts' ) . '</h3>';
786 echo '<p>' . __( 'We’re downloading and importing your LiveJournal posts...' ) . '</p>';
787 if ( get_option( 'ljapi_post_batch' ) && count( get_option( 'ljapi_sync_item_times' ) ) ) {
788 $batch = count( get_option( 'ljapi_sync_item_times' ) );
789 $batch = $count > 300 ? ceil( $batch / 300 ) : 1;
790 echo '<p><strong>' . sprintf( __( 'Imported post batch %d of <strong>approximately</strong> %d' ), ( get_option( 'ljapi_post_batch' ) + 1 ), $batch ) . '</strong></p>';
794 if ( !get_option( 'ljapi_lastsync' ) || '1900-01-01 00:00:00' == get_option( 'ljapi_lastsync' ) ) {
795 // We haven't downloaded meta yet, so do that first
796 $result = $this->download_post_meta();
797 if ( is_wp_error( $result ) ) {
798 $this->throw_error( $result, 1 );
803 // Download a batch of actual posts
804 $result = $this->download_post_bodies();
805 if ( is_wp_error( $result ) ) {
806 if ( 406 == $this->ixr->getErrorCode() ) {
808 <p><strong><?php _e( 'Uh oh – LiveJournal has disconnected us because we made too many requests to their servers too quickly.' ) ?></strong></p>
809 <p><strong><?php _e( 'We’ve saved where you were up to though, so if you come back to this importer in about 30 minutes, you should be able to continue from where you were.' ) ?></strong></p>
811 echo $this->next_step( 1, __( 'Try Again' ) );
814 $this->throw_error( $result, 1 );
819 if ( get_option( 'ljapi_last_sync_count' ) > 0 ) {
821 <form action="admin.php?import=livejournal" method="post" id="ljapi-auto-repost">
822 <?php wp_nonce_field( 'lj-api-import' ) ?>
823 <input type="hidden" name="step" id="step" value="1" />
824 <p><input type="submit" class="button-primary" value="<?php esc_attr_e( 'Import the next batch' ) ?>" /> <span id="auto-message"></span></p>
826 <?php $this->auto_ajax( 'ljapi-auto-repost', 'auto-message', 0 ); ?>
829 echo '<p>' . __( 'Your posts have all been imported, but wait – there’s more! Now we need to download & import your comments.' ) . '</p>';
830 echo $this->next_step( 2, __( 'Download my comments »' ) );
831 $this->auto_submit();
836 // Download comments to local XML
839 update_option( 'ljapi_step', 2 );
840 $this->username = get_option( 'ljapi_username' );
841 $this->password = get_option( 'ljapi_password' );
842 $this->ixr = new IXR_Client( $this->ixr_url, false, 80, 30 );
844 echo '<div id="ljapi-status">';
845 echo '<h3>' . __( 'Downloading Comments' ) . '</h3>';
846 echo '<p>' . __( 'Now we will download your comments so we can import them (this could take a <strong>long</strong> time if you have lots of comments)...' ) . '</p>';
849 if ( !get_option( 'ljapi_usermap' ) ) {
850 // We haven't downloaded meta yet, so do that first
851 $result = $this->download_comment_meta();
852 if ( is_wp_error( $result ) ) {
853 $this->throw_error( $result, 2 );
858 // Download a batch of actual comments
859 $result = $this->download_comment_bodies();
860 if ( is_wp_error( $result ) ) {
861 $this->throw_error( $result, 2 );
865 $maxid = get_option( 'ljapi_maxid' ) ? (int) get_option( 'ljapi_maxid' ) : 1;
866 $highest_id = (int) get_option( 'ljapi_highest_comment_id' );
867 if ( $maxid > $highest_id ) {
868 $batch = $maxid > 5000 ? ceil( $maxid / 5000 ) : 1;
870 <form action="admin.php?import=livejournal" method="post" id="ljapi-auto-repost">
871 <p><strong><?php printf( __( 'Imported comment batch %d of <strong>approximately</strong> %d' ), get_option( 'ljapi_comment_batch' ), $batch ) ?></strong></p>
872 <?php wp_nonce_field( 'lj-api-import' ) ?>
873 <input type="hidden" name="step" id="step" value="2" />
874 <p><input type="submit" class="button-primary" value="<?php esc_attr_e( 'Import the next batch' ) ?>" /> <span id="auto-message"></span></p>
876 <?php $this->auto_ajax( 'ljapi-auto-repost', 'auto-message', 0 ); ?>
879 echo '<p>' . __( 'Your comments have all been imported now, but we still need to rebuild your conversation threads.' ) . '</p>';
880 echo $this->next_step( 3, __( 'Rebuild my comment threads »' ) );
881 $this->auto_submit();
886 // Re-thread comments already in the DB
890 update_option( 'ljapi_step', 3 );
892 echo '<div id="ljapi-status">';
893 echo '<h3>' . __( 'Threading Comments' ) . '</h3>';
894 echo '<p>' . __( 'We are now re-building the threading of your comments (this can also take a while if you have lots of comments)...' ) . '</p>';
897 // Only bother adding indexes if they have over 5000 comments (arbitrary number)
898 $imported_comments = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->comments} WHERE comment_type = 'livejournal'" );
899 $added_indices = false;
900 if ( 5000 < $imported_comments ) {
901 include_once(ABSPATH . 'wp-admin/includes/upgrade.php');
902 $added_indices = true;
903 add_clean_index( $wpdb->comments, 'comment_type' );
904 add_clean_index( $wpdb->comments, 'comment_karma' );
905 add_clean_index( $wpdb->comments, 'comment_agent' );
908 // Get LJ comments, which haven't been threaded yet, 5000 at a time and thread them
909 while ( $comments = $wpdb->get_results( "SELECT comment_ID, comment_agent FROM {$wpdb->comments} WHERE comment_type = 'livejournal' AND comment_agent != '0' LIMIT 5000", OBJECT ) ) {
910 foreach ( $comments as $comment ) {
911 $wpdb->update( $wpdb->comments,
912 array( 'comment_parent' => $this->get_wp_comment_ID( $comment->comment_agent ), 'comment_type' => 'livejournal-done' ),
913 array( 'comment_ID' => $comment->comment_ID ) );
919 // Revert the comments table back to normal and optimize it to reclaim space
920 if ( $added_indices ) {
921 drop_index( $wpdb->comments, 'comment_type' );
922 drop_index( $wpdb->comments, 'comment_karma' );
923 drop_index( $wpdb->comments, 'comment_agent' );
924 $wpdb->query( "OPTIMIZE TABLE {$wpdb->comments}" );
927 // Clean up database and we're out
929 do_action( 'import_done', 'livejournal' );
930 if ( $imported_comments > 1 )
931 echo '<p>' . sprintf( __( "Successfully re-threaded %s comments." ), number_format( $imported_comments ) ) . '</p>';
933 printf( __( 'All done. <a href="%s">Have fun!</a>' ), get_option( 'home' ) );
938 // Output an error message with a button to try again.
939 function throw_error( $error, $step ) {
940 echo '<p><strong>' . $error->get_error_message() . '</strong></p>';
941 echo $this->next_step( $step, __( 'Try Again' ) );
944 // Returns the HTML for a link to the next page
945 function next_step( $next_step, $label, $id = 'ljapi-next-form' ) {
946 $str = '<form action="admin.php?import=livejournal" method="post" id="' . $id . '">';
947 $str .= wp_nonce_field( 'lj-api-import', '_wpnonce', true, false );
948 $str .= wp_referer_field( false );
949 $str .= '<input type="hidden" name="step" id="step" value="' . esc_attr($next_step) . '" />';
950 $str .= '<p><input type="submit" class="button-primary" value="' . esc_attr( $label ) . '" /> <span id="auto-message"></span></p>';
956 // Automatically submit the specified form after $seconds
957 // Include a friendly countdown in the element with id=$msg
958 function auto_submit( $id = 'ljapi-next-form', $msg = 'auto-message', $seconds = 10 ) {
959 ?><script type="text/javascript">
960 next_counter = <?php echo $seconds ?>;
961 jQuery(document).ready(function(){
965 function ljapi_msg() {
966 str = '<?php _e( "Continuing in %d" ) ?>';
967 jQuery( '#<?php echo $msg ?>' ).text( str.replace( /%d/, next_counter ) );
968 if ( next_counter <= 0 ) {
969 if ( jQuery( '#<?php echo $id ?>' ).length ) {
970 jQuery( "#<?php echo $id ?> input[type='submit']" ).hide();
971 str = '<?php _e( "Continuing" ) ?> <img src="images/wpspin_light.gif" alt="" id="processing" align="top" />';
972 jQuery( '#<?php echo $msg ?>' ).html( str );
973 jQuery( '#<?php echo $id ?>' ).submit();
977 next_counter = next_counter - 1;
978 setTimeout('ljapi_msg()', 1000);
983 // Automatically submit the form with #id to continue the process
984 // Hide any submit buttons to avoid people clicking them
985 // Display a countdown in the element indicated by $msg for "Continuing in x"
986 function auto_ajax( $id = 'ljapi-next-form', $msg = 'auto-message', $seconds = 5 ) {
987 ?><script type="text/javascript">
988 next_counter = <?php echo $seconds ?>;
989 jQuery(document).ready(function(){
993 function ljapi_msg() {
994 str = '<?php _e( "Continuing in %d" ) ?>';
995 jQuery( '#<?php echo $msg ?>' ).text( str.replace( /%d/, next_counter ) );
996 if ( next_counter <= 0 ) {
997 if ( jQuery( '#<?php echo $id ?>' ).length ) {
998 jQuery( "#<?php echo $id ?> input[type='submit']" ).hide();
999 jQuery.ajaxSetup({'timeout':3600000});
1000 str = '<?php _e( "Processing next batch." ) ?> <img src="images/wpspin_light.gif" alt="" id="processing" align="top" />';
1001 jQuery( '#<?php echo $msg ?>' ).html( str );
1002 jQuery('#ljapi-status').load(ajaxurl, {'action':'lj-importer',
1003 'step':jQuery('#step').val(),
1004 '_wpnonce':'<?php echo wp_create_nonce( 'lj-api-import' ) ?>',
1005 '_wp_http_referer':'<?php echo $_SERVER['REQUEST_URI'] ?>'});
1009 next_counter = next_counter - 1;
1010 setTimeout('ljapi_msg()', 1000);
1015 // Remove all options used during import process and
1016 // set wp_comments entries back to "normal" values
1017 function cleanup() {
1020 delete_option( 'ljapi_username' );
1021 delete_option( 'ljapi_password' );
1022 delete_option( 'ljapi_protected_password' );
1023 delete_option( 'ljapi_verified' );
1024 delete_option( 'ljapi_total' );
1025 delete_option( 'ljapi_count' );
1026 delete_option( 'ljapi_lastsync' );
1027 delete_option( 'ljapi_last_sync_count' );
1028 delete_option( 'ljapi_sync_item_times' );
1029 delete_option( 'ljapi_lastsync_posts' );
1030 delete_option( 'ljapi_post_batch' );
1031 delete_option( 'ljapi_imported_count' );
1032 delete_option( 'ljapi_maxid' );
1033 delete_option( 'ljapi_usermap' );
1034 delete_option( 'ljapi_highest_id' );
1035 delete_option( 'ljapi_highest_comment_id' );
1036 delete_option( 'ljapi_comment_batch' );
1037 delete_option( 'ljapi_step' );
1039 $wpdb->update( $wpdb->comments,
1040 array( 'comment_karma' => 0, 'comment_agent' => 'WP LJ Importer', 'comment_type' => '' ),
1041 array( 'comment_type' => 'livejournal-done' ) );
1042 $wpdb->update( $wpdb->comments,
1043 array( 'comment_karma' => 0, 'comment_agent' => 'WP LJ Importer', 'comment_type' => '' ),
1044 array( 'comment_type' => 'livejournal' ) );
1047 function LJ_API_Import() {
1048 $this->__construct();
1051 function __construct() {
1056 $lj_api_import = new LJ_API_Import();
1058 register_importer( 'livejournal', __( 'LiveJournal' ), __( 'Import posts from LiveJournal using their API.' ), array( $lj_api_import, 'dispatch' ) );