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 = esc_attr__('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 = esc_js( __('Import') );
194 $continue = esc_js( __('Continue') );
195 $stop = esc_js( __('Importing...') );
196 $authors = esc_js( __('Set Authors') );
197 $loadauth = esc_js( __('Preparing author mapping form...') );
198 $authhead = esc_js( __('Final Step: Author Mapping') );
199 $nothing = esc_js( __('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 $value = esc_attr($value);
218 $blogtitle = esc_js( $blog['title'] );
219 $pdone = isset($blog['posts_done']) ? (int) $blog['posts_done'] : 0;
220 $cdone = isset($blog['comments_done']) ? (int) $blog['comments_done'] : 0;
221 $init .= "blogs[$i]=new blog($i,'$blogtitle','{$blog['mode']}'," . $this->get_js_status($i) . ');';
222 $pstat = "<div class='ind' id='pind$i'> </div><div id='pstat$i' class='stat'>$pdone/{$blog['total_posts']}</div>";
223 $cstat = "<div class='ind' id='cind$i'> </div><div id='cstat$i' class='stat'>$cdone/{$blog['total_comments']}</div>";
224 $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";
227 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>";
229 <script type='text/javascript'>
231 var strings = {cont:'$continue',stop:'$stop',stopping:'$stopping',authors:'$authors',nothing:'$nothing'};
233 function blog(i, title, mode, status){
237 this.status = status;
238 this.button = document.getElementById('submit'+this.blog);
249 jQuery.post('admin.php?import=blogger&noheader=true',{blog:this.blog},function(text,result){blogs[i].kickd(text,result)});
254 jQuery.post('admin.php?import=blogger&noheader=true&status=true',{blog:this.blog},function(text,result){blogs[i].checkd(text,result)});
256 kickd: function(text, result) {
257 if ( result == 'error' ) {
258 // TODO: exception handling
260 setTimeout('blogs['+this.blog+'].kick()', 1000);
262 if ( text == 'done' ) {
265 } else if ( text == 'nothing' ) {
268 } else if ( text == 'continue' ) {
270 } else if ( this.mode = 'stopped' )
271 jQuery(this.button).attr('value', strings.cont);
275 checkd: function(text, result) {
276 if ( result == 'error' ) {
277 // TODO: exception handling
279 eval('this.status='+text);
280 jQuery('#pstat'+this.blog).empty().append(this.status.p1+'/'+this.status.p2);
281 jQuery('#cstat'+this.blog).empty().append(this.status.c1+'/'+this.status.c2);
283 if ( this.cont || this.kicks > 0 )
284 setTimeout('blogs['+this.blog+'].check()', $interval);
289 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');
290 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');
296 this.mode = 'authors';
297 jQuery(this.button).attr('value', strings.authors);
299 nothing: function() {
300 this.mode = 'nothing';
301 jQuery(this.button).remove();
302 alert(strings.nothing);
304 getauthors: function() {
305 if ( jQuery('div.wrap').length > 1 )
306 jQuery('div.wrap').gt(0).remove();
307 jQuery('div.wrap').empty().append('<h2>$authhead</h2><h3>' + this.title + '</h3>');
308 jQuery('div.wrap').append('<p id=\"auth\">$loadauth</p>');
309 jQuery('p#auth').load('index.php?import=blogger&noheader=true&authors=1',{blog:this.blog});
314 jQuery(this.button).bind('click', function(){return blogs[i].click();});
319 if ( this.mode == 'init' || this.mode == 'stopped' || this.mode == 'posts' || this.mode == 'comments' ) {
320 this.mode = 'started';
322 jQuery(this.button).attr('value', strings.stop);
323 } else if ( this.mode == 'started' ) {
324 return false; // let it run...
325 this.mode = 'stopped';
327 if ( this.checks > 0 || this.kicks > 0 ) {
328 this.mode = 'stopping';
329 jQuery(this.button).attr('value', strings.stopping);
331 jQuery(this.button).attr('value', strings.cont);
333 } else if ( this.mode == 'authors' ) {
334 document.location = 'index.php?import=blogger&authors=1&blog='+this.blog;
335 //this.mode = 'authors2';
342 jQuery.each(blogs, function(i, me){me.init();});
347 // Handy function for stopping the script after a number of seconds.
348 function have_time() {
349 global $importer_started;
350 if ( time() - $importer_started > MAX_EXECUTION_TIME )
355 function get_total_results($type, $host) {
357 "GET /feeds/$type/default?max-results=1&start-index=2 HTTP/1.0",
359 "Authorization: AuthSub token=\"$this->token\""
361 $request = join( "\r\n", $headers ) . "\r\n\r\n";
362 $sock = $this->_get_blogger_sock( $host );
363 if ( ! $sock ) return;
364 $response = $this->_txrx( $sock, $request );
365 $response = $this->parse_response( $response );
366 $parser = xml_parser_create();
367 xml_parse_into_struct($parser, $response['body'], $struct, $index);
368 xml_parser_free($parser);
369 $total_results = $struct[$index['OPENSEARCH:TOTALRESULTS'][0]]['value'];
370 return (int) $total_results;
373 function import_blog($blogID) {
374 global $importing_blog;
375 $importing_blog = $blogID;
377 if ( isset($_GET['authors']) )
378 return print($this->get_author_form());
380 header('Content-Type: text/plain');
382 if ( isset($_GET['status']) )
383 die($this->get_js_status());
385 if ( isset($_GET['saveauthors']) )
386 die($this->save_authors());
388 $blog = $this->blogs[$blogID];
389 $total_results = $this->get_total_results('posts', $blog['host']);
390 $this->blogs[$importing_blog]['total_posts'] = $total_results;
392 $start_index = $total_results - MAX_RESULTS + 1;
394 if ( isset( $this->blogs[$importing_blog]['posts_start_index'] ) )
395 $start_index = (int) $this->blogs[$importing_blog]['posts_start_index'];
396 elseif ( $total_results > MAX_RESULTS )
397 $start_index = $total_results - MAX_RESULTS + 1;
401 // This will be positive until we have finished importing posts
402 if ( $start_index > 0 ) {
403 // Grab all the posts
404 $this->blogs[$importing_blog]['mode'] = 'posts';
405 $query = "start-index=$start_index&max-results=" . MAX_RESULTS;
407 $index = $struct = $entries = array();
409 "GET /feeds/posts/default?$query HTTP/1.0",
410 "Host: {$blog['host']}",
411 "Authorization: AuthSub token=\"$this->token\""
413 $request = join( "\r\n", $headers ) . "\r\n\r\n";
414 $sock = $this->_get_blogger_sock( $blog['host'] );
415 if ( ! $sock ) return; // TODO: Error handling
416 $response = $this->_txrx( $sock, $request );
418 $response = $this->parse_response( $response );
420 // Extract the entries and send for insertion
421 preg_match_all( '/<entry[^>]*>.*?<\/entry>/s', $response['body'], $matches );
422 if ( count( $matches[0] ) ) {
423 $entries = array_reverse($matches[0]);
424 foreach ( $entries as $entry ) {
425 $entry = "<feed>$entry</feed>";
426 $AtomParser = new AtomParser();
427 $AtomParser->parse( $entry );
428 $result = $this->import_post($AtomParser->entry);
429 if ( is_wp_error( $result ) )
435 // Get the 'previous' query string which we'll use on the next iteration
437 $links = preg_match_all('/<link([^>]*)>/', $response['body'], $matches);
438 if ( count( $matches[1] ) )
439 foreach ( $matches[1] as $match )
440 if ( preg_match('/rel=.previous./', $match) )
441 $query = @html_entity_decode( preg_replace('/^.*href=[\'"].*\?(.+)[\'"].*$/', '$1', $match), ENT_COMPAT, get_option('blog_charset') );
444 parse_str($query, $q);
445 $this->blogs[$importing_blog]['posts_start_index'] = (int) $q['start-index'];
447 $this->blogs[$importing_blog]['posts_start_index'] = 0;
449 } while ( !empty( $query ) && $this->have_time() );
452 $total_results = $this->get_total_results( 'comments', $blog['host'] );
453 $this->blogs[$importing_blog]['total_comments'] = $total_results;
455 if ( isset( $this->blogs[$importing_blog]['comments_start_index'] ) )
456 $start_index = (int) $this->blogs[$importing_blog]['comments_start_index'];
457 elseif ( $total_results > MAX_RESULTS )
458 $start_index = $total_results - MAX_RESULTS + 1;
462 if ( $start_index > 0 ) {
463 // Grab all the comments
464 $this->blogs[$importing_blog]['mode'] = 'comments';
465 $query = "start-index=$start_index&max-results=" . MAX_RESULTS;
467 $index = $struct = $entries = array();
469 "GET /feeds/comments/default?$query HTTP/1.0",
470 "Host: {$blog['host']}",
471 "Authorization: AuthSub token=\"$this->token\""
473 $request = join( "\r\n", $headers ) . "\r\n\r\n";
474 $sock = $this->_get_blogger_sock( $blog['host'] );
475 if ( ! $sock ) return; // TODO: Error handling
476 $response = $this->_txrx( $sock, $request );
478 $response = $this->parse_response( $response );
480 // Extract the comments and send for insertion
481 preg_match_all( '/<entry[^>]*>.*?<\/entry>/s', $response['body'], $matches );
482 if ( count( $matches[0] ) ) {
483 $entries = array_reverse( $matches[0] );
484 foreach ( $entries as $entry ) {
485 $entry = "<feed>$entry</feed>";
486 $AtomParser = new AtomParser();
487 $AtomParser->parse( $entry );
488 $this->import_comment($AtomParser->entry);
493 // Get the 'previous' query string which we'll use on the next iteration
495 $links = preg_match_all('/<link([^>]*)>/', $response['body'], $matches);
496 if ( count( $matches[1] ) )
497 foreach ( $matches[1] as $match )
498 if ( preg_match('/rel=.previous./', $match) )
499 $query = @html_entity_decode( preg_replace('/^.*href=[\'"].*\?(.+)[\'"].*$/', '$1', $match), ENT_COMPAT, get_option('blog_charset') );
501 parse_str($query, $q);
503 $this->blogs[$importing_blog]['comments_start_index'] = (int) $q['start-index'];
505 } while ( !empty( $query ) && $this->have_time() );
507 $this->blogs[$importing_blog]['mode'] = 'authors';
509 if ( !$this->blogs[$importing_blog]['posts_done'] && !$this->blogs[$importing_blog]['comments_done'] )
511 do_action('import_done', 'blogger');
515 function convert_date( $date ) {
516 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);
517 $offset = iso8601_timezone_to_offset( $date_bits[7] );
518 $timestamp = gmmktime($date_bits[4], $date_bits[5], $date_bits[6], $date_bits[2], $date_bits[3], $date_bits[1]);
519 $timestamp -= $offset; // Convert from Blogger local time to GMT
520 $timestamp += get_option('gmt_offset') * 3600; // Convert from GMT to WP local time
521 return gmdate('Y-m-d H:i:s', $timestamp);
524 function no_apos( $string ) {
525 return str_replace( ''', "'", $string);
528 function min_whitespace( $string ) {
529 return preg_replace( '|\s+|', ' ', $string );
532 function import_post( $entry ) {
533 global $importing_blog;
535 // The old permalink is all Blogger gives us to link comments to their posts.
536 if ( isset( $entry->draft ) )
540 foreach ( $entry->links as $link ) {
541 if ( $link['rel'] == $rel ) {
542 $parts = parse_url( $link['href'] );
543 $entry->old_permalink = $parts['path'];
548 $post_date = $this->convert_date( $entry->published );
549 $post_content = trim( addslashes( $this->no_apos( @html_entity_decode( $entry->content, ENT_COMPAT, get_option('blog_charset') ) ) ) );
550 $post_title = trim( addslashes( $this->no_apos( $this->min_whitespace( $entry->title ) ) ) );
551 $post_status = isset( $entry->draft ) ? 'draft' : 'publish';
554 $post_content = preg_replace_callback('|<(/?[A-Z]+)|', create_function('$match', 'return "<" . strtolower($match[1]);'), $post_content);
555 $post_content = str_replace('<br>', '<br />', $post_content);
556 $post_content = str_replace('<hr>', '<hr />', $post_content);
558 // Checks for duplicates
559 if ( isset( $this->blogs[$importing_blog]['posts'][$entry->old_permalink] ) ) {
560 ++$this->blogs[$importing_blog]['posts_skipped'];
561 } elseif ( $post_id = post_exists( $post_title, $post_content, $post_date ) ) {
562 $this->blogs[$importing_blog]['posts'][$entry->old_permalink] = $post_id;
563 ++$this->blogs[$importing_blog]['posts_skipped'];
565 $post = compact('post_date', 'post_content', 'post_title', 'post_status');
567 $post_id = wp_insert_post($post);
568 if ( is_wp_error( $post_id ) )
571 wp_create_categories( array_map( 'addslashes', $entry->categories ), $post_id );
573 $author = $this->no_apos( strip_tags( $entry->author ) );
575 add_post_meta( $post_id, 'blogger_blog', $this->blogs[$importing_blog]['host'], true );
576 add_post_meta( $post_id, 'blogger_author', $author, true );
577 add_post_meta( $post_id, 'blogger_permalink', $entry->old_permalink, true );
579 $this->blogs[$importing_blog]['posts'][$entry->old_permalink] = $post_id;
580 ++$this->blogs[$importing_blog]['posts_done'];
586 function import_comment( $entry ) {
587 global $importing_blog;
589 // Drop the #fragment and we have the comment's old post permalink.
590 foreach ( $entry->links as $link ) {
591 if ( $link['rel'] == 'alternate' ) {
592 $parts = parse_url( $link['href'] );
593 $entry->old_permalink = $parts['fragment'];
594 $entry->old_post_permalink = $parts['path'];
599 $comment_post_ID = (int) $this->blogs[$importing_blog]['posts'][$entry->old_post_permalink];
600 preg_match('#<name>(.+?)</name>.*(?:\<uri>(.+?)</uri>)?#', $entry->author, $matches);
601 $comment_author = addslashes( $this->no_apos( strip_tags( (string) $matches[1] ) ) );
602 $comment_author_url = addslashes( $this->no_apos( strip_tags( (string) $matches[2] ) ) );
603 $comment_date = $this->convert_date( $entry->updated );
604 $comment_content = addslashes( $this->no_apos( @html_entity_decode( $entry->content, ENT_COMPAT, get_option('blog_charset') ) ) );
607 $comment_content = preg_replace_callback('|<(/?[A-Z]+)|', create_function('$match', 'return "<" . strtolower($match[1]);'), $comment_content);
608 $comment_content = str_replace('<br>', '<br />', $comment_content);
609 $comment_content = str_replace('<hr>', '<hr />', $comment_content);
611 // Checks for duplicates
613 isset( $this->blogs[$importing_blog]['comments'][$entry->old_permalink] ) ||
614 comment_exists( $comment_author, $comment_date )
616 ++$this->blogs[$importing_blog]['comments_skipped'];
618 $comment = compact('comment_post_ID', 'comment_author', 'comment_author_url', 'comment_date', 'comment_content');
620 $comment_id = wp_insert_comment($comment);
622 $this->blogs[$importing_blog]['comments'][$entry->old_permalink] = $comment_id;
624 ++$this->blogs[$importing_blog]['comments_done'];
629 function get_js_status($blog = false) {
630 global $importing_blog;
631 if ( $blog === false )
632 $blog = $this->blogs[$importing_blog];
634 $blog = $this->blogs[$blog];
635 $p1 = isset( $blog['posts_done'] ) ? (int) $blog['posts_done'] : 0;
636 $p2 = isset( $blog['total_posts'] ) ? (int) $blog['total_posts'] : 0;
637 $c1 = isset( $blog['comments_done'] ) ? (int) $blog['comments_done'] : 0;
638 $c2 = isset( $blog['total_comments'] ) ? (int) $blog['total_comments'] : 0;
639 return "{p1:$p1,p2:$p2,c1:$c1,c2:$c2}";
642 function get_author_form($blog = false) {
643 global $importing_blog, $wpdb, $current_user;
644 if ( $blog === false )
645 $blog = & $this->blogs[$importing_blog];
647 $blog = & $this->blogs[$blog];
649 if ( !isset( $blog['authors'] ) ) {
650 $post_ids = array_values($blog['posts']);
651 $authors = (array) $wpdb->get_col("SELECT DISTINCT meta_value FROM $wpdb->postmeta WHERE meta_key = 'blogger_author' AND post_id IN (" . join( ',', $post_ids ) . ")");
652 $blog['authors'] = array_map(null, $authors, array_fill(0, count($authors), $current_user->ID));
656 $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.');
657 $heading = __('Author mapping');
658 $blogtitle = "{$blog['title']} ({$blog['host']})";
659 $mapthis = __('Blogger username');
660 $tothis = __('WordPress login');
661 $submit = esc_js( __('Save Changes') );
663 foreach ( $blog['authors'] as $i => $author )
664 $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>";
666 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='" . esc_attr($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>";
669 function get_user_options($current) {
670 global $importer_users;
671 if ( ! isset( $importer_users ) )
672 $importer_users = (array) get_users_of_blog();
674 foreach ( $importer_users as $user ) {
675 $sel = ( $user->user_id == $current ) ? " selected='selected'" : '';
676 $options .= "<option value='$user->user_id'$sel>$user->display_name</option>";
682 function save_authors() {
683 global $importing_blog, $wpdb;
684 $authors = (array) $_POST['authors'];
686 $host = $this->blogs[$importing_blog]['host'];
688 // Get an array of posts => authors
689 $post_ids = (array) $wpdb->get_col( $wpdb->prepare("SELECT post_id FROM $wpdb->postmeta WHERE meta_key = 'blogger_blog' AND meta_value = %s", $host) );
690 $post_ids = join( ',', $post_ids );
691 $results = (array) $wpdb->get_results("SELECT post_id, meta_value FROM $wpdb->postmeta WHERE meta_key = 'blogger_author' AND post_id IN ($post_ids)");
692 foreach ( $results as $row )
693 $authors_posts[$row->post_id] = $row->meta_value;
695 foreach ( $authors as $author => $user_id ) {
696 $user_id = (int) $user_id;
698 // Skip authors that haven't been changed
699 if ( $user_id == $this->blogs[$importing_blog]['authors'][$author][1] )
702 // Get a list of the selected author's posts
703 $post_ids = (array) array_keys( $authors_posts, $this->blogs[$importing_blog]['authors'][$author][0] );
704 $post_ids = join( ',', $post_ids);
706 $wpdb->query( $wpdb->prepare("UPDATE $wpdb->posts SET post_author = %d WHERE id IN ($post_ids)", $user_id) );
707 $this->blogs[$importing_blog]['authors'][$author][1] = $user_id;
711 wp_redirect('edit.php');
714 function _get_auth_sock() {
715 // Connect to https://www.google.com
716 if ( !$sock = @ fsockopen('ssl://www.google.com', 443, $errno, $errstr) ) {
718 __('Could not connect to https://www.google.com'),
719 __('There was a problem opening a secure connection to Google. This is what went wrong:'),
727 function _get_blogger_sock($host = 'www2.blogger.com') {
728 if ( !$sock = @ fsockopen($host, 80, $errno, $errstr) ) {
730 sprintf( __('Could not connect to %s'), $host ),
731 __('There was a problem opening a connection to Blogger. This is what went wrong:'),
739 function _txrx( $sock, $request ) {
740 fwrite( $sock, $request );
741 while ( ! feof( $sock ) )
742 $response .= @ fread ( $sock, 8192 );
747 function revoke($token) {
749 "GET /accounts/AuthSubRevokeToken HTTP/1.0",
750 "Authorization: AuthSub token=\"$token\""
752 $request = join( "\r\n", $headers ) . "\r\n\r\n";
753 $sock = $this->_get_auth_sock( );
754 if ( ! $sock ) return false;
755 $this->_txrx( $sock, $request );
760 $options = get_option( 'blogger_importer' );
762 if ( isset( $options['token'] ) )
763 $this->revoke( $options['token'] );
765 delete_option('blogger_importer');
766 $wpdb->query("DELETE FROM $wpdb->postmeta WHERE meta_key = 'blogger_author'");
767 wp_redirect('?import=blogger');
770 // Returns associative array of code, header, cookies, body. Based on code from php.net.
771 function parse_response($this_response) {
772 // Split response into header and body sections
773 list($response_headers, $response_body) = explode("\r\n\r\n", $this_response, 2);
774 $response_header_lines = explode("\r\n", $response_headers);
776 // First line of headers is the HTTP response code
777 $http_response_line = array_shift($response_header_lines);
778 if(preg_match('@^HTTP/[0-9]\.[0-9] ([0-9]{3})@',$http_response_line, $matches)) { $response_code = $matches[1]; }
780 // put the rest of the headers in an array
781 $response_header_array = array();
782 foreach($response_header_lines as $header_line) {
783 list($header,$value) = explode(': ', $header_line, 2);
784 $response_header_array[$header] .= $value."\n";
787 $cookie_array = array();
788 $cookies = explode("\n", $response_header_array["Set-Cookie"]);
789 foreach($cookies as $this_cookie) { array_push($cookie_array, "Cookie: ".$this_cookie); }
791 return array("code" => $response_code, "header" => $response_header_array, "cookies" => $cookie_array, "body" => $response_body);
794 // Step 9: Congratulate the user
795 function congrats() {
796 $blog = (int) $_GET['blog'];
797 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>';
798 if ( count($this->import['blogs']) > 1 )
799 echo '<li>'.__('In case you haven’t done it already, you can import the posts from your other blogs:'). $this->show_blogs() . '</li>';
800 if ( $n = count($this->import['blogs'][$blog]['newusers']) )
801 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>';
802 echo '<li>'.__('For security, click the link below to reset this importer.').'</li>';
806 // Figures out what to do, then does it.
808 if ( isset($_POST['restart']) )
811 $options = get_option('blogger_importer');
813 if ( is_array($options) )
814 foreach ( $options as $key => $value )
815 $this->$key = $value;
817 if ( isset( $_REQUEST['blog'] ) ) {
818 $blog = is_array($_REQUEST['blog']) ? array_shift( $keys = array_keys( $_REQUEST['blog'] ) ) : $_REQUEST['blog'];
820 $result = $this->import_blog( $blog );
821 if ( is_wp_error( $result ) )
822 echo $result->get_error_message();
823 } elseif ( isset($_GET['token']) )
825 elseif ( isset($this->token) && $this->token_is_valid() )
830 $saved = $this->save_vars();
832 if ( $saved && !isset($_GET['noheader']) ) {
833 $restart = __('Restart');
834 $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.');
835 $submit = esc_attr__('Clear account information');
836 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>";
840 function save_vars() {
841 $vars = get_object_vars($this);
842 update_option( 'blogger_importer', $vars );
844 return !empty($vars);
847 function admin_head() {
849 <style type="text/css">
850 td { text-align: center; line-height: 2em;}
851 thead td { font-weight: bold; }
860 background-color: #83B4D8;
873 function Blogger_Import() {
874 global $importer_started;
875 $importer_started = time();
876 if ( isset( $_GET['import'] ) && $_GET['import'] == 'blogger' ) {
877 wp_enqueue_script('jquery');
878 add_action('admin_head', array(&$this, 'admin_head'));
883 $blogger_import = new Blogger_Import();
885 register_importer('blogger', __('Blogger'), __('Import posts, comments, and users from a Blogger blog.'), array ($blogger_import, 'start'));
888 var $links = array();
889 var $categories = array();
894 var $ATOM_CONTENT_ELEMENTS = array('content','summary','title','subtitle','rights');
895 var $ATOM_SIMPLE_ELEMENTS = array('id','updated','published','draft','author');
900 var $ns_contexts = array();
901 var $ns_decls = array();
902 var $is_xhtml = false;
903 var $skipped_div = false;
907 function AtomParser() {
909 $this->entry = new AtomEntry();
910 $this->map_attrs_func = create_function('$k,$v', 'return "$k=\"$v\"";');
911 $this->map_xmlns_func = create_function('$p,$n', '$xd = "xmlns"; if(strlen($n[0])>0) $xd .= ":{$n[0]}"; return "{$xd}=\"{$n[1]}\"";');
914 function parse($xml) {
917 array_unshift($this->ns_contexts, array());
919 $parser = xml_parser_create_ns();
920 xml_set_object($parser, $this);
921 xml_set_element_handler($parser, "start_element", "end_element");
922 xml_parser_set_option($parser,XML_OPTION_CASE_FOLDING,0);
923 xml_parser_set_option($parser,XML_OPTION_SKIP_WHITE,0);
924 xml_set_character_data_handler($parser, "cdata");
925 xml_set_default_handler($parser, "_default");
926 xml_set_start_namespace_decl_handler($parser, "start_ns");
927 xml_set_end_namespace_decl_handler($parser, "end_ns");
931 xml_parse($parser, $xml);
933 xml_parser_free($parser);
938 function start_element($parser, $name, $attrs) {
940 $tag = array_pop(split(":", $name));
942 array_unshift($this->ns_contexts, $this->ns_decls);
946 if(!empty($this->in_content)) {
947 $attrs_prefix = array();
949 // resolve prefixes for attributes
950 foreach($attrs as $key => $value) {
951 $attrs_prefix[$this->ns_to_prefix($key)] = $this->xml_escape($value);
953 $attrs_str = join(' ', array_map($this->map_attrs_func, array_keys($attrs_prefix), array_values($attrs_prefix)));
954 if(strlen($attrs_str) > 0) {
955 $attrs_str = " " . $attrs_str;
958 $xmlns_str = join(' ', array_map($this->map_xmlns_func, array_keys($this->ns_contexts[0]), array_values($this->ns_contexts[0])));
959 if(strlen($xmlns_str) > 0) {
960 $xmlns_str = " " . $xmlns_str;
963 // handle self-closing tags (case: a new child found right-away, no text node)
964 if(count($this->in_content) == 2) {
965 array_push($this->in_content, ">");
968 array_push($this->in_content, "<". $this->ns_to_prefix($name) ."{$xmlns_str}{$attrs_str}");
969 } else if(in_array($tag, $this->ATOM_CONTENT_ELEMENTS) || in_array($tag, $this->ATOM_SIMPLE_ELEMENTS)) {
970 $this->in_content = array();
971 $this->is_xhtml = $attrs['type'] == 'xhtml';
972 array_push($this->in_content, array($tag,$this->depth));
973 } else if($tag == 'link') {
974 array_push($this->entry->links, $attrs);
975 } else if($tag == 'category') {
976 array_push($this->entry->categories, $attrs['term']);
979 $this->ns_decls = array();
982 function end_element($parser, $name) {
984 $tag = array_pop(split(":", $name));
986 if(!empty($this->in_content)) {
987 if($this->in_content[0][0] == $tag &&
988 $this->in_content[0][1] == $this->depth) {
989 array_shift($this->in_content);
990 if($this->is_xhtml) {
991 $this->in_content = array_slice($this->in_content, 2, count($this->in_content)-3);
993 $this->entry->$tag = join('',$this->in_content);
994 $this->in_content = array();
996 $endtag = $this->ns_to_prefix($name);
997 if (strpos($this->in_content[count($this->in_content)-1], '<' . $endtag) !== false) {
998 array_push($this->in_content, "/>");
1000 array_push($this->in_content, "</$endtag>");
1005 array_shift($this->ns_contexts);
1007 #print str_repeat(" ", $this->depth * $this->indent) . "end_element('$name')" ."\n";
1012 function start_ns($parser, $prefix, $uri) {
1013 #print str_repeat(" ", $this->depth * $this->indent) . "starting: " . $prefix . ":" . $uri . "\n";
1014 array_push($this->ns_decls, array($prefix,$uri));
1017 function end_ns($parser, $prefix) {
1018 #print str_repeat(" ", $this->depth * $this->indent) . "ending: #" . $prefix . "#\n";
1021 function cdata($parser, $data) {
1022 #print str_repeat(" ", $this->depth * $this->indent) . "data: #" . $data . "#\n";
1023 if(!empty($this->in_content)) {
1024 // handle self-closing tags (case: text node found, need to close element started)
1025 if (strpos($this->in_content[count($this->in_content)-1], '<') !== false) {
1026 array_push($this->in_content, ">");
1028 array_push($this->in_content, $this->xml_escape($data));
1032 function _default($parser, $data) {
1033 # when does this gets called?
1037 function ns_to_prefix($qname) {
1038 $components = split(":", $qname);
1039 $name = array_pop($components);
1041 if(!empty($components)) {
1042 $ns = join(":",$components);
1043 foreach($this->ns_contexts as $context) {
1044 foreach($context as $mapping) {
1045 if($mapping[1] == $ns && strlen($mapping[0]) > 0) {
1046 return "$mapping[0]:$name";
1054 function xml_escape($string)
1056 return str_replace(array('&','"',"'",'<','>'),
1057 array('&','"',''','<','>'),