10 * How many records per GData query
13 * @subpackage Blogger_Import
17 define( 'MAX_RESULTS', 50 );
20 * How many seconds to let the script run
23 * @subpackage Blogger_Import
27 define( 'MAX_EXECUTION_TIME', 20 );
30 * How many seconds between status bar updates
33 * @subpackage Blogger_Import
37 define( 'STATUS_INTERVAL', 3 );
40 * Blogger Importer class
44 class Blogger_Import {
46 // Shows the welcome screen and the magic auth link.
48 $next_url = get_option('siteurl') . '/wp-admin/index.php?import=blogger&noheader=true';
49 $auth_url = "https://www.google.com/accounts/AuthSubRequest";
50 $title = __('Import Blogger');
51 $welcome = __('Howdy! This importer allows you to import posts and comments from your Blogger account into your WordPress blog.');
52 $prereqs = __('To use this importer, you must have a Google account and an upgraded (New, was Beta) blog hosted on blogspot.com or a custom domain (not FTP).');
53 $stepone = __('The first thing you need to do is tell Blogger to let WordPress access your account. You will be sent back here after providing authorization.');
54 $auth = __('Authorize');
60 <p>$welcome</p><p>$prereqs</p><p>$stepone</p>
61 <form action='$auth_url' method='get'>
62 <p class='submit' style='text-align:left;'>
63 <input type='submit' class='button' value='$auth' />
64 <input type='hidden' name='scope' value='http://www.blogger.com/feeds/' />
65 <input type='hidden' name='session' value='1' />
66 <input type='hidden' name='secure' value='0' />
67 <input type='hidden' name='next' value='$next_url' />
73 function uh_oh($title, $message, $info) {
74 echo "<div class='wrap'>";
76 echo "<h2>$title</h2><p>$message</p><pre>$info</pre></div>";
80 // We have a single-use token that must be upgraded to a session token.
81 $token = preg_replace( '/[^-_0-9a-zA-Z]/', '', $_GET['token'] );
83 "GET /accounts/AuthSubSessionToken HTTP/1.0",
84 "Authorization: AuthSub token=\"$token\""
86 $request = join( "\r\n", $headers ) . "\r\n\r\n";
87 $sock = $this->_get_auth_sock( );
88 if ( ! $sock ) return false;
89 $response = $this->_txrx( $sock, $request );
90 preg_match( '/token=([-_0-9a-z]+)/i', $response, $matches );
91 if ( empty( $matches[1] ) ) {
93 __( 'Authorization failed' ),
94 __( 'Something went wrong. If the problem persists, send this info to support:' ),
95 htmlspecialchars($response)
99 $this->token = $matches[1];
101 wp_redirect( remove_query_arg( array( 'token', 'noheader' ) ) );
104 function get_token_info() {
106 "GET /accounts/AuthSubTokenInfo HTTP/1.0",
107 "Authorization: AuthSub token=\"$this->token\""
109 $request = join( "\r\n", $headers ) . "\r\n\r\n";
110 $sock = $this->_get_auth_sock( );
111 if ( ! $sock ) return;
112 $response = $this->_txrx( $sock, $request );
113 return $this->parse_response($response);
116 function token_is_valid() {
117 $info = $this->get_token_info();
119 if ( $info['code'] == 200 )
125 function show_blogs($iter = 0) {
126 if ( empty($this->blogs) ) {
128 "GET /feeds/default/blogs HTTP/1.0",
129 "Host: www.blogger.com",
130 "Authorization: AuthSub token=\"$this->token\""
132 $request = join( "\r\n", $headers ) . "\r\n\r\n";
133 $sock = $this->_get_blogger_sock( );
134 if ( ! $sock ) return;
135 $response = $this->_txrx( $sock, $request );
137 // Quick and dirty XML mining.
138 list( $headers, $xml ) = explode( "\r\n\r\n", $response );
139 $p = xml_parser_create();
140 xml_parse_into_struct($p, $xml, $vals, $index);
143 $this->title = $vals[$index['TITLE'][0]]['value'];
145 // Give it a few retries... this step often flakes out the first time.
146 if ( empty( $index['ENTRY'] ) ) {
148 return $this->show_blogs($iter + 1);
151 __('Trouble signing in'),
152 __('We were not able to gain access to your account. Try starting over.'),
159 foreach ( $index['ENTRY'] as $i ) {
161 while ( ( $tag = $vals[$i] ) && ! ( $tag['tag'] == 'ENTRY' && $tag['type'] == 'close' ) ) {
162 if ( $tag['tag'] == 'TITLE' ) {
163 $blog['title'] = $tag['value'];
164 } elseif ( $tag['tag'] == 'SUMMARY' ) {
165 $blog['summary'] == $tag['value'];
166 } elseif ( $tag['tag'] == 'LINK' ) {
167 if ( $tag['attributes']['REL'] == 'alternate' && $tag['attributes']['TYPE'] == 'text/html' ) {
168 $parts = parse_url( $tag['attributes']['HREF'] );
169 $blog['host'] = $parts['host'];
170 } elseif ( $tag['attributes']['REL'] == 'edit' )
171 $blog['gateway'] = $tag['attributes']['HREF'];
175 if ( ! empty ( $blog ) ) {
176 $blog['total_posts'] = $this->get_total_results('posts', $blog['host']);
177 $blog['total_comments'] = $this->get_total_results('comments', $blog['host']);
178 $blog['mode'] = 'init';
179 $this->blogs[] = $blog;
183 if ( empty( $this->blogs ) ) {
185 __('No blogs found'),
186 __('We were able to log in but there were no blogs. Try a different account next time.'),
192 //echo '<pre>'.print_r($this,1).'</pre>';
193 $start = js_escape( __('Import') );
194 $continue = js_escape( __('Continue') );
195 $stop = js_escape( __('Importing...') );
196 $authors = js_escape( __('Set Authors') );
197 $loadauth = js_escape( __('Preparing author mapping form...') );
198 $authhead = js_escape( __('Final Step: Author Mapping') );
199 $nothing = js_escape( __('Nothing was imported. Had you already imported this blog?') );
200 $title = __('Blogger Blogs');
201 $name = __('Blog Name');
202 $url = __('Blog URL');
203 $action = __('The Magic Button');
204 $posts = __('Posts');
205 $comments = __('Comments');
206 $noscript = __('This feature requires Javascript but it seems to be disabled. Please enable Javascript and then reload this page. Don\'t worry, you can turn it back off when you\'re done.');
208 $interval = STATUS_INTERVAL * 1000;
210 foreach ( $this->blogs as $i => $blog ) {
211 if ( $blog['mode'] == 'init' )
213 elseif ( $blog['mode'] == 'posts' || $blog['mode'] == 'comments' )
217 $blogtitle = js_escape( $blog['title'] );
218 $pdone = isset($blog['posts_done']) ? (int) $blog['posts_done'] : 0;
219 $cdone = isset($blog['comments_done']) ? (int) $blog['comments_done'] : 0;
220 $init .= "blogs[$i]=new blog($i,'$blogtitle','{$blog['mode']}'," . $this->get_js_status($i) . ');';
221 $pstat = "<div class='ind' id='pind$i'> </div><div id='pstat$i' class='stat'>$pdone/{$blog['total_posts']}</div>";
222 $cstat = "<div class='ind' id='cind$i'> </div><div id='cstat$i' class='stat'>$cdone/{$blog['total_comments']}</div>";
223 $rows .= "<tr id='blog$i'><td class='blogtitle'>$blogtitle</td><td class='bloghost'>{$blog['host']}</td><td class='bar'>$pstat</td><td class='bar'>$cstat</td><td class='submit'><input type='submit' class='button' id='submit$i' value='$value' /><input type='hidden' name='blog' value='$i' /></td></tr>\n";
226 echo "<div class='wrap'><h2>$title</h2><noscript>$noscript</noscript><table cellpadding='5px'><thead><tr><td>$name</td><td>$url</td><td>$posts</td><td>$comments</td><td>$action</td></tr></thead>\n$rows</table></div>";
228 <script type='text/javascript'>
230 var strings = {cont:'$continue',stop:'$stop',stopping:'$stopping',authors:'$authors',nothing:'$nothing'};
232 function blog(i, title, mode, status){
236 this.status = status;
237 this.button = document.getElementById('submit'+this.blog);
248 jQuery.post('admin.php?import=blogger&noheader=true',{blog:this.blog},function(text,result){blogs[i].kickd(text,result)});
253 jQuery.post('admin.php?import=blogger&noheader=true&status=true',{blog:this.blog},function(text,result){blogs[i].checkd(text,result)});
255 kickd: function(text, result) {
256 if ( result == 'error' ) {
257 // TODO: exception handling
259 setTimeout('blogs['+this.blog+'].kick()', 1000);
261 if ( text == 'done' ) {
264 } else if ( text == 'nothing' ) {
267 } else if ( text == 'continue' ) {
269 } else if ( this.mode = 'stopped' )
270 jQuery(this.button).attr('value', strings.cont);
274 checkd: function(text, result) {
275 if ( result == 'error' ) {
276 // TODO: exception handling
278 eval('this.status='+text);
279 jQuery('#pstat'+this.blog).empty().append(this.status.p1+'/'+this.status.p2);
280 jQuery('#cstat'+this.blog).empty().append(this.status.c1+'/'+this.status.c2);
282 if ( this.cont || this.kicks > 0 )
283 setTimeout('blogs['+this.blog+'].check()', $interval);
288 jQuery('#pind'+this.blog).width(((this.status.p1>0&&this.status.p2>0)?(this.status.p1/this.status.p2*jQuery('#pind'+this.blog).parent().width()):1)+'px');
289 jQuery('#cind'+this.blog).width(((this.status.c1>0&&this.status.c2>0)?(this.status.c1/this.status.c2*jQuery('#cind'+this.blog).parent().width()):1)+'px');
295 this.mode = 'authors';
296 jQuery(this.button).attr('value', strings.authors);
298 nothing: function() {
299 this.mode = 'nothing';
300 jQuery(this.button).remove();
301 alert(strings.nothing);
303 getauthors: function() {
304 if ( jQuery('div.wrap').length > 1 )
305 jQuery('div.wrap').gt(0).remove();
306 jQuery('div.wrap').empty().append('<h2>$authhead</h2><h3>' + this.title + '</h3>');
307 jQuery('div.wrap').append('<p id=\"auth\">$loadauth</p>');
308 jQuery('p#auth').load('index.php?import=blogger&noheader=true&authors=1',{blog:this.blog});
313 jQuery(this.button).bind('click', function(){return blogs[i].click();});
318 if ( this.mode == 'init' || this.mode == 'stopped' || this.mode == 'posts' || this.mode == 'comments' ) {
319 this.mode = 'started';
321 jQuery(this.button).attr('value', strings.stop);
322 } else if ( this.mode == 'started' ) {
323 return false; // let it run...
324 this.mode = 'stopped';
326 if ( this.checks > 0 || this.kicks > 0 ) {
327 this.mode = 'stopping';
328 jQuery(this.button).attr('value', strings.stopping);
330 jQuery(this.button).attr('value', strings.cont);
332 } else if ( this.mode == 'authors' ) {
333 document.location = 'index.php?import=blogger&authors=1&blog='+this.blog;
334 //this.mode = 'authors2';
341 jQuery.each(blogs, function(i, me){me.init();});
346 // Handy function for stopping the script after a number of seconds.
347 function have_time() {
348 global $importer_started;
349 if ( time() - $importer_started > MAX_EXECUTION_TIME )
354 function get_total_results($type, $host) {
356 "GET /feeds/$type/default?max-results=1&start-index=2 HTTP/1.0",
358 "Authorization: AuthSub token=\"$this->token\""
360 $request = join( "\r\n", $headers ) . "\r\n\r\n";
361 $sock = $this->_get_blogger_sock( $host );
362 if ( ! $sock ) return;
363 $response = $this->_txrx( $sock, $request );
364 $response = $this->parse_response( $response );
365 $parser = xml_parser_create();
366 xml_parse_into_struct($parser, $response['body'], $struct, $index);
367 xml_parser_free($parser);
368 $total_results = $struct[$index['OPENSEARCH:TOTALRESULTS'][0]]['value'];
369 return (int) $total_results;
372 function import_blog($blogID) {
373 global $importing_blog;
374 $importing_blog = $blogID;
376 if ( isset($_GET['authors']) )
377 return print($this->get_author_form());
379 header('Content-Type: text/plain');
381 if ( isset($_GET['status']) )
382 die($this->get_js_status());
384 if ( isset($_GET['saveauthors']) )
385 die($this->save_authors());
387 $blog = $this->blogs[$blogID];
388 $total_results = $this->get_total_results('posts', $blog['host']);
389 $this->blogs[$importing_blog]['total_posts'] = $total_results;
391 $start_index = $total_results - MAX_RESULTS + 1;
393 if ( isset( $this->blogs[$importing_blog]['posts_start_index'] ) )
394 $start_index = (int) $this->blogs[$importing_blog]['posts_start_index'];
395 elseif ( $total_results > MAX_RESULTS )
396 $start_index = $total_results - MAX_RESULTS + 1;
400 // This will be positive until we have finished importing posts
401 if ( $start_index > 0 ) {
402 // Grab all the posts
403 $this->blogs[$importing_blog]['mode'] = 'posts';
404 $query = "start-index=$start_index&max-results=" . MAX_RESULTS;
406 $index = $struct = $entries = array();
408 "GET /feeds/posts/default?$query HTTP/1.0",
409 "Host: {$blog['host']}",
410 "Authorization: AuthSub token=\"$this->token\""
412 $request = join( "\r\n", $headers ) . "\r\n\r\n";
413 $sock = $this->_get_blogger_sock( $blog['host'] );
414 if ( ! $sock ) return; // TODO: Error handling
415 $response = $this->_txrx( $sock, $request );
417 $response = $this->parse_response( $response );
419 // Extract the entries and send for insertion
420 preg_match_all( '/<entry[^>]*>.*?<\/entry>/s', $response['body'], $matches );
421 if ( count( $matches[0] ) ) {
422 $entries = array_reverse($matches[0]);
423 foreach ( $entries as $entry ) {
424 $entry = "<feed>$entry</feed>";
425 $AtomParser = new AtomParser();
426 $AtomParser->parse( $entry );
427 $result = $this->import_post($AtomParser->entry);
428 if ( is_wp_error( $result ) )
434 // Get the 'previous' query string which we'll use on the next iteration
436 $links = preg_match_all('/<link([^>]*)>/', $response['body'], $matches);
437 if ( count( $matches[1] ) )
438 foreach ( $matches[1] as $match )
439 if ( preg_match('/rel=.previous./', $match) )
440 $query = html_entity_decode( preg_replace('/^.*href=[\'"].*\?(.+)[\'"].*$/', '$1', $match) );
443 parse_str($query, $q);
444 $this->blogs[$importing_blog]['posts_start_index'] = (int) $q['start-index'];
446 $this->blogs[$importing_blog]['posts_start_index'] = 0;
448 } while ( !empty( $query ) && $this->have_time() );
451 $total_results = $this->get_total_results( 'comments', $blog['host'] );
452 $this->blogs[$importing_blog]['total_comments'] = $total_results;
454 if ( isset( $this->blogs[$importing_blog]['comments_start_index'] ) )
455 $start_index = (int) $this->blogs[$importing_blog]['comments_start_index'];
456 elseif ( $total_results > MAX_RESULTS )
457 $start_index = $total_results - MAX_RESULTS + 1;
461 if ( $start_index > 0 ) {
462 // Grab all the comments
463 $this->blogs[$importing_blog]['mode'] = 'comments';
464 $query = "start-index=$start_index&max-results=" . MAX_RESULTS;
466 $index = $struct = $entries = array();
468 "GET /feeds/comments/default?$query HTTP/1.0",
469 "Host: {$blog['host']}",
470 "Authorization: AuthSub token=\"$this->token\""
472 $request = join( "\r\n", $headers ) . "\r\n\r\n";
473 $sock = $this->_get_blogger_sock( $blog['host'] );
474 if ( ! $sock ) return; // TODO: Error handling
475 $response = $this->_txrx( $sock, $request );
477 $response = $this->parse_response( $response );
479 // Extract the comments and send for insertion
480 preg_match_all( '/<entry[^>]*>.*?<\/entry>/s', $response['body'], $matches );
481 if ( count( $matches[0] ) ) {
482 $entries = array_reverse( $matches[0] );
483 foreach ( $entries as $entry ) {
484 $entry = "<feed>$entry</feed>";
485 $AtomParser = new AtomParser();
486 $AtomParser->parse( $entry );
487 $this->import_comment($AtomParser->entry);
492 // Get the 'previous' query string which we'll use on the next iteration
494 $links = preg_match_all('/<link([^>]*)>/', $response['body'], $matches);
495 if ( count( $matches[1] ) )
496 foreach ( $matches[1] as $match )
497 if ( preg_match('/rel=.previous./', $match) )
498 $query = html_entity_decode( preg_replace('/^.*href=[\'"].*\?(.+)[\'"].*$/', '$1', $match) );
500 parse_str($query, $q);
502 $this->blogs[$importing_blog]['comments_start_index'] = (int) $q['start-index'];
504 } while ( !empty( $query ) && $this->have_time() );
506 $this->blogs[$importing_blog]['mode'] = 'authors';
508 if ( !$this->blogs[$importing_blog]['posts_done'] && !$this->blogs[$importing_blog]['comments_done'] )
510 do_action('import_done', 'blogger');
514 function convert_date( $date ) {
515 preg_match('#([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(?:\.[0-9]+)?(Z|[\+|\-][0-9]{2,4}){0,1}#', $date, $date_bits);
516 $offset = iso8601_timezone_to_offset( $date_bits[7] );
517 $timestamp = gmmktime($date_bits[4], $date_bits[5], $date_bits[6], $date_bits[2], $date_bits[3], $date_bits[1]);
518 $timestamp -= $offset; // Convert from Blogger local time to GMT
519 $timestamp += get_option('gmt_offset') * 3600; // Convert from GMT to WP local time
520 return gmdate('Y-m-d H:i:s', $timestamp);
523 function no_apos( $string ) {
524 return str_replace( ''', "'", $string);
527 function min_whitespace( $string ) {
528 return preg_replace( '|\s+|', ' ', $string );
531 function import_post( $entry ) {
532 global $importing_blog;
534 // The old permalink is all Blogger gives us to link comments to their posts.
535 if ( isset( $entry->draft ) )
539 foreach ( $entry->links as $link ) {
540 if ( $link['rel'] == $rel ) {
541 $parts = parse_url( $link['href'] );
542 $entry->old_permalink = $parts['path'];
547 $post_date = $this->convert_date( $entry->published );
548 $post_content = trim( addslashes( $this->no_apos( html_entity_decode( $entry->content ) ) ) );
549 $post_title = trim( addslashes( $this->no_apos( $this->min_whitespace( $entry->title ) ) ) );
550 $post_status = isset( $entry->draft ) ? 'draft' : 'publish';
553 $post_content = preg_replace_callback('|<(/?[A-Z]+)|', create_function('$match', 'return "<" . strtolower($match[1]);'), $post_content);
554 $post_content = str_replace('<br>', '<br />', $post_content);
555 $post_content = str_replace('<hr>', '<hr />', $post_content);
557 // Checks for duplicates
558 if ( isset( $this->blogs[$importing_blog]['posts'][$entry->old_permalink] ) ) {
559 ++$this->blogs[$importing_blog]['posts_skipped'];
560 } elseif ( $post_id = post_exists( $post_title, $post_content, $post_date ) ) {
561 $this->blogs[$importing_blog]['posts'][$entry->old_permalink] = $post_id;
562 ++$this->blogs[$importing_blog]['posts_skipped'];
564 $post = compact('post_date', 'post_content', 'post_title', 'post_status');
566 $post_id = wp_insert_post($post);
567 if ( is_wp_error( $post_id ) )
570 wp_create_categories( array_map( 'addslashes', $entry->categories ), $post_id );
572 $author = $this->no_apos( strip_tags( $entry->author ) );
574 add_post_meta( $post_id, 'blogger_blog', $this->blogs[$importing_blog]['host'], true );
575 add_post_meta( $post_id, 'blogger_author', $author, true );
576 add_post_meta( $post_id, 'blogger_permalink', $entry->old_permalink, true );
578 $this->blogs[$importing_blog]['posts'][$entry->old_permalink] = $post_id;
579 ++$this->blogs[$importing_blog]['posts_done'];
585 function import_comment( $entry ) {
586 global $importing_blog;
588 // Drop the #fragment and we have the comment's old post permalink.
589 foreach ( $entry->links as $link ) {
590 if ( $link['rel'] == 'alternate' ) {
591 $parts = parse_url( $link['href'] );
592 $entry->old_permalink = $parts['fragment'];
593 $entry->old_post_permalink = $parts['path'];
598 $comment_post_ID = (int) $this->blogs[$importing_blog]['posts'][$entry->old_post_permalink];
599 preg_match('#<name>(.+?)</name>.*(?:\<uri>(.+?)</uri>)?#', $entry->author, $matches);
600 $comment_author = addslashes( $this->no_apos( strip_tags( (string) $matches[1] ) ) );
601 $comment_author_url = addslashes( $this->no_apos( strip_tags( (string) $matches[2] ) ) );
602 $comment_date = $this->convert_date( $entry->updated );
603 $comment_content = addslashes( $this->no_apos( html_entity_decode( $entry->content ) ) );
606 $comment_content = preg_replace_callback('|<(/?[A-Z]+)|', create_function('$match', 'return "<" . strtolower($match[1]);'), $comment_content);
607 $comment_content = str_replace('<br>', '<br />', $comment_content);
608 $comment_content = str_replace('<hr>', '<hr />', $comment_content);
610 // Checks for duplicates
612 isset( $this->blogs[$importing_blog]['comments'][$entry->old_permalink] ) ||
613 comment_exists( $comment_author, $comment_date )
615 ++$this->blogs[$importing_blog]['comments_skipped'];
617 $comment = compact('comment_post_ID', 'comment_author', 'comment_author_url', 'comment_date', 'comment_content');
619 $comment_id = wp_insert_comment($comment);
621 $this->blogs[$importing_blog]['comments'][$entry->old_permalink] = $comment_id;
623 ++$this->blogs[$importing_blog]['comments_done'];
628 function get_js_status($blog = false) {
629 global $importing_blog;
630 if ( $blog === false )
631 $blog = $this->blogs[$importing_blog];
633 $blog = $this->blogs[$blog];
634 $p1 = isset( $blog['posts_done'] ) ? (int) $blog['posts_done'] : 0;
635 $p2 = isset( $blog['total_posts'] ) ? (int) $blog['total_posts'] : 0;
636 $c1 = isset( $blog['comments_done'] ) ? (int) $blog['comments_done'] : 0;
637 $c2 = isset( $blog['total_comments'] ) ? (int) $blog['total_comments'] : 0;
638 return "{p1:$p1,p2:$p2,c1:$c1,c2:$c2}";
641 function get_author_form($blog = false) {
642 global $importing_blog, $wpdb, $current_user;
643 if ( $blog === false )
644 $blog = & $this->blogs[$importing_blog];
646 $blog = & $this->blogs[$blog];
648 if ( !isset( $blog['authors'] ) ) {
649 $post_ids = array_values($blog['posts']);
650 $authors = (array) $wpdb->get_col("SELECT DISTINCT meta_value FROM $wpdb->postmeta WHERE meta_key = 'blogger_author' AND post_id IN (" . join( ',', $post_ids ) . ")");
651 $blog['authors'] = array_map(null, $authors, array_fill(0, count($authors), $current_user->ID));
655 $directions = __('All posts were imported with the current user as author. Use this form to move each Blogger user\'s posts to a different WordPress user. You may <a href="users.php">add users</a> and then return to this page and complete the user mapping. This form may be used as many times as you like until you activate the "Restart" function below.');
656 $heading = __('Author mapping');
657 $blogtitle = "{$blog['title']} ({$blog['host']})";
658 $mapthis = __('Blogger username');
659 $tothis = __('WordPress login');
660 $submit = js_escape( __('Save Changes') );
662 foreach ( $blog['authors'] as $i => $author )
663 $rows .= "<tr><td><label for='authors[$i]'>{$author[0]}</label></td><td><select name='authors[$i]' id='authors[$i]'>" . $this->get_user_options($author[1]) . "</select></td></tr>";
665 return "<div class='wrap'><h2>$heading</h2><h3>$blogtitle</h3><p>$directions</p><form action='index.php?import=blogger&noheader=true&saveauthors=1' method='post'><input type='hidden' name='blog' value='$importing_blog' /><table cellpadding='5'><thead><td>$mapthis</td><td>$tothis</td></thead>$rows<tr><td></td><td class='submit'><input type='submit' class='button authorsubmit' value='$submit' /></td></tr></table></form></div>";
668 function get_user_options($current) {
669 global $importer_users;
670 if ( ! isset( $importer_users ) )
671 $importer_users = (array) get_users_of_blog();
673 foreach ( $importer_users as $user ) {
674 $sel = ( $user->user_id == $current ) ? " selected='selected'" : '';
675 $options .= "<option value='$user->user_id'$sel>$user->display_name</option>";
681 function save_authors() {
682 global $importing_blog, $wpdb;
683 $authors = (array) $_POST['authors'];
685 $host = $this->blogs[$importing_blog]['host'];
687 // Get an array of posts => authors
688 $post_ids = (array) $wpdb->get_col( $wpdb->prepare("SELECT post_id FROM $wpdb->postmeta WHERE meta_key = 'blogger_blog' AND meta_value = %s", $host) );
689 $post_ids = join( ',', $post_ids );
690 $results = (array) $wpdb->get_results("SELECT post_id, meta_value FROM $wpdb->postmeta WHERE meta_key = 'blogger_author' AND post_id IN ($post_ids)");
691 foreach ( $results as $row )
692 $authors_posts[$row->post_id] = $row->meta_value;
694 foreach ( $authors as $author => $user_id ) {
695 $user_id = (int) $user_id;
697 // Skip authors that haven't been changed
698 if ( $user_id == $this->blogs[$importing_blog]['authors'][$author][1] )
701 // Get a list of the selected author's posts
702 $post_ids = (array) array_keys( $authors_posts, $this->blogs[$importing_blog]['authors'][$author][0] );
703 $post_ids = join( ',', $post_ids);
705 $wpdb->query( $wpdb->prepare("UPDATE $wpdb->posts SET post_author = %d WHERE id IN ($post_ids)", $user_id) );
706 $this->blogs[$importing_blog]['authors'][$author][1] = $user_id;
710 wp_redirect('edit.php');
713 function _get_auth_sock() {
714 // Connect to https://www.google.com
715 if ( !$sock = @ fsockopen('ssl://www.google.com', 443, $errno, $errstr) ) {
717 __('Could not connect to https://www.google.com'),
718 __('There was a problem opening a secure connection to Google. This is what went wrong:'),
726 function _get_blogger_sock($host = 'www2.blogger.com') {
727 if ( !$sock = @ fsockopen($host, 80, $errno, $errstr) ) {
729 sprintf( __('Could not connect to %s'), $host ),
730 __('There was a problem opening a connection to Blogger. This is what went wrong:'),
738 function _txrx( $sock, $request ) {
739 fwrite( $sock, $request );
740 while ( ! feof( $sock ) )
741 $response .= @ fread ( $sock, 8192 );
746 function revoke($token) {
748 "GET /accounts/AuthSubRevokeToken HTTP/1.0",
749 "Authorization: AuthSub token=\"$token\""
751 $request = join( "\r\n", $headers ) . "\r\n\r\n";
752 $sock = $this->_get_auth_sock( );
753 if ( ! $sock ) return false;
754 $this->_txrx( $sock, $request );
759 $options = get_option( 'blogger_importer' );
761 if ( isset( $options['token'] ) )
762 $this->revoke( $options['token'] );
764 delete_option('blogger_importer');
765 $wpdb->query("DELETE FROM $wpdb->postmeta WHERE meta_key = 'blogger_author'");
766 wp_redirect('?import=blogger');
769 // Returns associative array of code, header, cookies, body. Based on code from php.net.
770 function parse_response($this_response) {
771 // Split response into header and body sections
772 list($response_headers, $response_body) = explode("\r\n\r\n", $this_response, 2);
773 $response_header_lines = explode("\r\n", $response_headers);
775 // First line of headers is the HTTP response code
776 $http_response_line = array_shift($response_header_lines);
777 if(preg_match('@^HTTP/[0-9]\.[0-9] ([0-9]{3})@',$http_response_line, $matches)) { $response_code = $matches[1]; }
779 // put the rest of the headers in an array
780 $response_header_array = array();
781 foreach($response_header_lines as $header_line) {
782 list($header,$value) = explode(': ', $header_line, 2);
783 $response_header_array[$header] .= $value."\n";
786 $cookie_array = array();
787 $cookies = explode("\n", $response_header_array["Set-Cookie"]);
788 foreach($cookies as $this_cookie) { array_push($cookie_array, "Cookie: ".$this_cookie); }
790 return array("code" => $response_code, "header" => $response_header_array, "cookies" => $cookie_array, "body" => $response_body);
793 // Step 9: Congratulate the user
794 function congrats() {
795 $blog = (int) $_GET['blog'];
796 echo '<h1>'.__('Congratulations!').'</h1><p>'.__('Now that you have imported your Blogger blog into WordPress, what are you going to do? Here are some suggestions:').'</p><ul><li>'.__('That was hard work! Take a break.').'</li>';
797 if ( count($this->import['blogs']) > 1 )
798 echo '<li>'.__('In case you haven\'t done it already, you can import the posts from your other blogs:'). $this->show_blogs() . '</li>';
799 if ( $n = count($this->import['blogs'][$blog]['newusers']) )
800 echo '<li>'.sprintf(__('Go to <a href="%s" target="%s">Authors & Users</a>, where you can modify the new user(s) or delete them. If you want to make all of the imported posts yours, you will be given that option when you delete the new authors.'), 'users.php', '_parent').'</li>';
801 echo '<li>'.__('For security, click the link below to reset this importer.').'</li>';
805 // Figures out what to do, then does it.
807 if ( isset($_POST['restart']) )
810 $options = get_option('blogger_importer');
812 if ( is_array($options) )
813 foreach ( $options as $key => $value )
814 $this->$key = $value;
816 if ( isset( $_REQUEST['blog'] ) ) {
817 $blog = is_array($_REQUEST['blog']) ? array_shift( $keys = array_keys( $_REQUEST['blog'] ) ) : $_REQUEST['blog'];
819 $result = $this->import_blog( $blog );
820 if ( is_wp_error( $result ) )
821 echo $result->get_error_message();
822 } elseif ( isset($_GET['token']) )
824 elseif ( isset($this->token) && $this->token_is_valid() )
829 $saved = $this->save_vars();
831 if ( $saved && !isset($_GET['noheader']) ) {
832 $restart = __('Restart');
833 $message = __('We have saved some information about your Blogger account in your WordPress database. Clearing this information will allow you to start over. Restarting will not affect any posts you have already imported. If you attempt to re-import a blog, duplicate posts and comments will be skipped.');
834 $submit = __('Clear account information');
835 echo "<div class='wrap'><h2>$restart</h2><p>$message</p><form method='post' action='?import=blogger&noheader=true'><p class='submit' style='text-align:left;'><input type='submit' class='button' value='$submit' name='restart' /></p></form></div>";
839 function save_vars() {
840 $vars = get_object_vars($this);
841 update_option( 'blogger_importer', $vars );
843 return !empty($vars);
846 function admin_head() {
848 <style type="text/css">
849 td { text-align: center; line-height: 2em;}
850 thead td { font-weight: bold; }
859 background-color: #83B4D8;
872 function Blogger_Import() {
873 global $importer_started;
874 $importer_started = time();
875 if ( isset( $_GET['import'] ) && $_GET['import'] == 'blogger' ) {
876 wp_enqueue_script('jquery');
877 add_action('admin_head', array(&$this, 'admin_head'));
882 $blogger_import = new Blogger_Import();
884 register_importer('blogger', __('Blogger'), __('Import posts, comments, and users from a Blogger blog.'), array ($blogger_import, 'start'));
887 var $links = array();
888 var $categories = array();
893 var $ATOM_CONTENT_ELEMENTS = array('content','summary','title','subtitle','rights');
894 var $ATOM_SIMPLE_ELEMENTS = array('id','updated','published','draft','author');
899 var $ns_contexts = array();
900 var $ns_decls = array();
901 var $is_xhtml = false;
902 var $skipped_div = false;
906 function AtomParser() {
908 $this->entry = new AtomEntry();
909 $this->map_attrs_func = create_function('$k,$v', 'return "$k=\"$v\"";');
910 $this->map_xmlns_func = create_function('$p,$n', '$xd = "xmlns"; if(strlen($n[0])>0) $xd .= ":{$n[0]}"; return "{$xd}=\"{$n[1]}\"";');
913 function parse($xml) {
916 array_unshift($this->ns_contexts, array());
918 $parser = xml_parser_create_ns();
919 xml_set_object($parser, $this);
920 xml_set_element_handler($parser, "start_element", "end_element");
921 xml_parser_set_option($parser,XML_OPTION_CASE_FOLDING,0);
922 xml_parser_set_option($parser,XML_OPTION_SKIP_WHITE,0);
923 xml_set_character_data_handler($parser, "cdata");
924 xml_set_default_handler($parser, "_default");
925 xml_set_start_namespace_decl_handler($parser, "start_ns");
926 xml_set_end_namespace_decl_handler($parser, "end_ns");
930 xml_parse($parser, $xml);
932 xml_parser_free($parser);
937 function start_element($parser, $name, $attrs) {
939 $tag = array_pop(split(":", $name));
941 array_unshift($this->ns_contexts, $this->ns_decls);
945 if(!empty($this->in_content)) {
946 $attrs_prefix = array();
948 // resolve prefixes for attributes
949 foreach($attrs as $key => $value) {
950 $attrs_prefix[$this->ns_to_prefix($key)] = $this->xml_escape($value);
952 $attrs_str = join(' ', array_map($this->map_attrs_func, array_keys($attrs_prefix), array_values($attrs_prefix)));
953 if(strlen($attrs_str) > 0) {
954 $attrs_str = " " . $attrs_str;
957 $xmlns_str = join(' ', array_map($this->map_xmlns_func, array_keys($this->ns_contexts[0]), array_values($this->ns_contexts[0])));
958 if(strlen($xmlns_str) > 0) {
959 $xmlns_str = " " . $xmlns_str;
962 // handle self-closing tags (case: a new child found right-away, no text node)
963 if(count($this->in_content) == 2) {
964 array_push($this->in_content, ">");
967 array_push($this->in_content, "<". $this->ns_to_prefix($name) ."{$xmlns_str}{$attrs_str}");
968 } else if(in_array($tag, $this->ATOM_CONTENT_ELEMENTS) || in_array($tag, $this->ATOM_SIMPLE_ELEMENTS)) {
969 $this->in_content = array();
970 $this->is_xhtml = $attrs['type'] == 'xhtml';
971 array_push($this->in_content, array($tag,$this->depth));
972 } else if($tag == 'link') {
973 array_push($this->entry->links, $attrs);
974 } else if($tag == 'category') {
975 array_push($this->entry->categories, $attrs['term']);
978 $this->ns_decls = array();
981 function end_element($parser, $name) {
983 $tag = array_pop(split(":", $name));
985 if(!empty($this->in_content)) {
986 if($this->in_content[0][0] == $tag &&
987 $this->in_content[0][1] == $this->depth) {
988 array_shift($this->in_content);
989 if($this->is_xhtml) {
990 $this->in_content = array_slice($this->in_content, 2, count($this->in_content)-3);
992 $this->entry->$tag = join('',$this->in_content);
993 $this->in_content = array();
995 $endtag = $this->ns_to_prefix($name);
996 if (strpos($this->in_content[count($this->in_content)-1], '<' . $endtag) !== false) {
997 array_push($this->in_content, "/>");
999 array_push($this->in_content, "</$endtag>");
1004 array_shift($this->ns_contexts);
1006 #print str_repeat(" ", $this->depth * $this->indent) . "end_element('$name')" ."\n";
1011 function start_ns($parser, $prefix, $uri) {
1012 #print str_repeat(" ", $this->depth * $this->indent) . "starting: " . $prefix . ":" . $uri . "\n";
1013 array_push($this->ns_decls, array($prefix,$uri));
1016 function end_ns($parser, $prefix) {
1017 #print str_repeat(" ", $this->depth * $this->indent) . "ending: #" . $prefix . "#\n";
1020 function cdata($parser, $data) {
1021 #print str_repeat(" ", $this->depth * $this->indent) . "data: #" . $data . "#\n";
1022 if(!empty($this->in_content)) {
1023 // handle self-closing tags (case: text node found, need to close element started)
1024 if (strpos($this->in_content[count($this->in_content)-1], '<') !== false) {
1025 array_push($this->in_content, ">");
1027 array_push($this->in_content, $this->xml_escape($data));
1031 function _default($parser, $data) {
1032 # when does this gets called?
1036 function ns_to_prefix($qname) {
1037 $components = split(":", $qname);
1038 $name = array_pop($components);
1040 if(!empty($components)) {
1041 $ns = join(":",$components);
1042 foreach($this->ns_contexts as $context) {
1043 foreach($context as $mapping) {
1044 if($mapping[1] == $ns && strlen($mapping[0]) > 0) {
1045 return "$mapping[0]:$name";
1053 function xml_escape($string)
1055 return str_replace(array('&','"',"'",'<','>'),
1056 array('&','"',''','<','>'),