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 $stopping = ''; //Missing String used below.
201 $title = __('Blogger Blogs');
202 $name = __('Blog Name');
203 $url = __('Blog URL');
204 $action = __('The Magic Button');
205 $posts = __('Posts');
206 $comments = __('Comments');
207 $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.');
209 $interval = STATUS_INTERVAL * 1000;
211 foreach ( $this->blogs as $i => $blog ) {
212 if ( $blog['mode'] == 'init' )
214 elseif ( $blog['mode'] == 'posts' || $blog['mode'] == 'comments' )
218 $value = esc_attr($value);
219 $blogtitle = esc_js( $blog['title'] );
220 $pdone = isset($blog['posts_done']) ? (int) $blog['posts_done'] : 0;
221 $cdone = isset($blog['comments_done']) ? (int) $blog['comments_done'] : 0;
222 $init .= "blogs[$i]=new blog($i,'$blogtitle','{$blog['mode']}'," . $this->get_js_status($i) . ');';
223 $pstat = "<div class='ind' id='pind$i'> </div><div id='pstat$i' class='stat'>$pdone/{$blog['total_posts']}</div>";
224 $cstat = "<div class='ind' id='cind$i'> </div><div id='cstat$i' class='stat'>$cdone/{$blog['total_comments']}</div>";
225 $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";
228 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>";
230 <script type='text/javascript'>
232 var strings = {cont:'$continue',stop:'$stop',stopping:'$stopping',authors:'$authors',nothing:'$nothing'};
234 function blog(i, title, mode, status){
238 this.status = status;
239 this.button = document.getElementById('submit'+this.blog);
250 jQuery.post('admin.php?import=blogger&noheader=true',{blog:this.blog},function(text,result){blogs[i].kickd(text,result)});
255 jQuery.post('admin.php?import=blogger&noheader=true&status=true',{blog:this.blog},function(text,result){blogs[i].checkd(text,result)});
257 kickd: function(text, result) {
258 if ( result == 'error' ) {
259 // TODO: exception handling
261 setTimeout('blogs['+this.blog+'].kick()', 1000);
263 if ( text == 'done' ) {
266 } else if ( text == 'nothing' ) {
269 } else if ( text == 'continue' ) {
271 } else if ( this.mode = 'stopped' )
272 jQuery(this.button).attr('value', strings.cont);
276 checkd: function(text, result) {
277 if ( result == 'error' ) {
278 // TODO: exception handling
280 eval('this.status='+text);
281 jQuery('#pstat'+this.blog).empty().append(this.status.p1+'/'+this.status.p2);
282 jQuery('#cstat'+this.blog).empty().append(this.status.c1+'/'+this.status.c2);
284 if ( this.cont || this.kicks > 0 )
285 setTimeout('blogs['+this.blog+'].check()', $interval);
290 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');
291 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');
297 this.mode = 'authors';
298 jQuery(this.button).attr('value', strings.authors);
300 nothing: function() {
301 this.mode = 'nothing';
302 jQuery(this.button).remove();
303 alert(strings.nothing);
305 getauthors: function() {
306 if ( jQuery('div.wrap').length > 1 )
307 jQuery('div.wrap').gt(0).remove();
308 jQuery('div.wrap').empty().append('<h2>$authhead</h2><h3>' + this.title + '</h3>');
309 jQuery('div.wrap').append('<p id=\"auth\">$loadauth</p>');
310 jQuery('p#auth').load('index.php?import=blogger&noheader=true&authors=1',{blog:this.blog});
315 jQuery(this.button).bind('click', function(){return blogs[i].click();});
320 if ( this.mode == 'init' || this.mode == 'stopped' || this.mode == 'posts' || this.mode == 'comments' ) {
321 this.mode = 'started';
323 jQuery(this.button).attr('value', strings.stop);
324 } else if ( this.mode == 'started' ) {
325 return false; // let it run...
326 this.mode = 'stopped';
328 if ( this.checks > 0 || this.kicks > 0 ) {
329 this.mode = 'stopping';
330 jQuery(this.button).attr('value', strings.stopping);
332 jQuery(this.button).attr('value', strings.cont);
334 } else if ( this.mode == 'authors' ) {
335 document.location = 'index.php?import=blogger&authors=1&blog='+this.blog;
336 //this.mode = 'authors2';
343 jQuery.each(blogs, function(i, me){me.init();});
348 // Handy function for stopping the script after a number of seconds.
349 function have_time() {
350 global $importer_started;
351 if ( time() - $importer_started > MAX_EXECUTION_TIME )
356 function get_total_results($type, $host) {
358 "GET /feeds/$type/default?max-results=1&start-index=2 HTTP/1.0",
360 "Authorization: AuthSub token=\"$this->token\""
362 $request = join( "\r\n", $headers ) . "\r\n\r\n";
363 $sock = $this->_get_blogger_sock( $host );
364 if ( ! $sock ) return;
365 $response = $this->_txrx( $sock, $request );
366 $response = $this->parse_response( $response );
367 $parser = xml_parser_create();
368 xml_parse_into_struct($parser, $response['body'], $struct, $index);
369 xml_parser_free($parser);
370 $total_results = $struct[$index['OPENSEARCH:TOTALRESULTS'][0]]['value'];
371 return (int) $total_results;
374 function import_blog($blogID) {
375 global $importing_blog;
376 $importing_blog = $blogID;
378 if ( isset($_GET['authors']) )
379 return print($this->get_author_form());
381 header('Content-Type: text/plain');
383 if ( isset($_GET['status']) )
384 die($this->get_js_status());
386 if ( isset($_GET['saveauthors']) )
387 die($this->save_authors());
389 $blog = $this->blogs[$blogID];
390 $total_results = $this->get_total_results('posts', $blog['host']);
391 $this->blogs[$importing_blog]['total_posts'] = $total_results;
393 $start_index = $total_results - MAX_RESULTS + 1;
395 if ( isset( $this->blogs[$importing_blog]['posts_start_index'] ) )
396 $start_index = (int) $this->blogs[$importing_blog]['posts_start_index'];
397 elseif ( $total_results > MAX_RESULTS )
398 $start_index = $total_results - MAX_RESULTS + 1;
402 // This will be positive until we have finished importing posts
403 if ( $start_index > 0 ) {
404 // Grab all the posts
405 $this->blogs[$importing_blog]['mode'] = 'posts';
406 $query = "start-index=$start_index&max-results=" . MAX_RESULTS;
408 $index = $struct = $entries = array();
410 "GET /feeds/posts/default?$query HTTP/1.0",
411 "Host: {$blog['host']}",
412 "Authorization: AuthSub token=\"$this->token\""
414 $request = join( "\r\n", $headers ) . "\r\n\r\n";
415 $sock = $this->_get_blogger_sock( $blog['host'] );
416 if ( ! $sock ) return; // TODO: Error handling
417 $response = $this->_txrx( $sock, $request );
419 $response = $this->parse_response( $response );
421 // Extract the entries and send for insertion
422 preg_match_all( '/<entry[^>]*>.*?<\/entry>/s', $response['body'], $matches );
423 if ( count( $matches[0] ) ) {
424 $entries = array_reverse($matches[0]);
425 foreach ( $entries as $entry ) {
426 $entry = "<feed>$entry</feed>";
427 $AtomParser = new AtomParser();
428 $AtomParser->parse( $entry );
429 $result = $this->import_post($AtomParser->entry);
430 if ( is_wp_error( $result ) )
436 // Get the 'previous' query string which we'll use on the next iteration
438 $links = preg_match_all('/<link([^>]*)>/', $response['body'], $matches);
439 if ( count( $matches[1] ) )
440 foreach ( $matches[1] as $match )
441 if ( preg_match('/rel=.previous./', $match) )
442 $query = @html_entity_decode( preg_replace('/^.*href=[\'"].*\?(.+)[\'"].*$/', '$1', $match), ENT_COMPAT, get_option('blog_charset') );
445 parse_str($query, $q);
446 $this->blogs[$importing_blog]['posts_start_index'] = (int) $q['start-index'];
448 $this->blogs[$importing_blog]['posts_start_index'] = 0;
450 } while ( !empty( $query ) && $this->have_time() );
453 $total_results = $this->get_total_results( 'comments', $blog['host'] );
454 $this->blogs[$importing_blog]['total_comments'] = $total_results;
456 if ( isset( $this->blogs[$importing_blog]['comments_start_index'] ) )
457 $start_index = (int) $this->blogs[$importing_blog]['comments_start_index'];
458 elseif ( $total_results > MAX_RESULTS )
459 $start_index = $total_results - MAX_RESULTS + 1;
463 if ( $start_index > 0 ) {
464 // Grab all the comments
465 $this->blogs[$importing_blog]['mode'] = 'comments';
466 $query = "start-index=$start_index&max-results=" . MAX_RESULTS;
468 $index = $struct = $entries = array();
470 "GET /feeds/comments/default?$query HTTP/1.0",
471 "Host: {$blog['host']}",
472 "Authorization: AuthSub token=\"$this->token\""
474 $request = join( "\r\n", $headers ) . "\r\n\r\n";
475 $sock = $this->_get_blogger_sock( $blog['host'] );
476 if ( ! $sock ) return; // TODO: Error handling
477 $response = $this->_txrx( $sock, $request );
479 $response = $this->parse_response( $response );
481 // Extract the comments and send for insertion
482 preg_match_all( '/<entry[^>]*>.*?<\/entry>/s', $response['body'], $matches );
483 if ( count( $matches[0] ) ) {
484 $entries = array_reverse( $matches[0] );
485 foreach ( $entries as $entry ) {
486 $entry = "<feed>$entry</feed>";
487 $AtomParser = new AtomParser();
488 $AtomParser->parse( $entry );
489 $this->import_comment($AtomParser->entry);
494 // Get the 'previous' query string which we'll use on the next iteration
496 $links = preg_match_all('/<link([^>]*)>/', $response['body'], $matches);
497 if ( count( $matches[1] ) )
498 foreach ( $matches[1] as $match )
499 if ( preg_match('/rel=.previous./', $match) )
500 $query = @html_entity_decode( preg_replace('/^.*href=[\'"].*\?(.+)[\'"].*$/', '$1', $match), ENT_COMPAT, get_option('blog_charset') );
502 parse_str($query, $q);
504 $this->blogs[$importing_blog]['comments_start_index'] = (int) $q['start-index'];
506 } while ( !empty( $query ) && $this->have_time() );
508 $this->blogs[$importing_blog]['mode'] = 'authors';
510 if ( !$this->blogs[$importing_blog]['posts_done'] && !$this->blogs[$importing_blog]['comments_done'] )
512 do_action('import_done', 'blogger');
516 function convert_date( $date ) {
517 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);
518 $offset = iso8601_timezone_to_offset( $date_bits[7] );
519 $timestamp = gmmktime($date_bits[4], $date_bits[5], $date_bits[6], $date_bits[2], $date_bits[3], $date_bits[1]);
520 $timestamp -= $offset; // Convert from Blogger local time to GMT
521 $timestamp += get_option('gmt_offset') * 3600; // Convert from GMT to WP local time
522 return gmdate('Y-m-d H:i:s', $timestamp);
525 function no_apos( $string ) {
526 return str_replace( ''', "'", $string);
529 function min_whitespace( $string ) {
530 return preg_replace( '|\s+|', ' ', $string );
533 function _normalize_tag( $matches ) {
534 return '<' . strtolower( $matches[1] );
537 function import_post( $entry ) {
538 global $importing_blog;
540 // The old permalink is all Blogger gives us to link comments to their posts.
541 if ( isset( $entry->draft ) )
545 foreach ( $entry->links as $link ) {
546 if ( $link['rel'] == $rel ) {
547 $parts = parse_url( $link['href'] );
548 $entry->old_permalink = $parts['path'];
553 $post_date = $this->convert_date( $entry->published );
554 $post_content = trim( addslashes( $this->no_apos( @html_entity_decode( $entry->content, ENT_COMPAT, get_option('blog_charset') ) ) ) );
555 $post_title = trim( addslashes( $this->no_apos( $this->min_whitespace( $entry->title ) ) ) );
556 $post_status = isset( $entry->draft ) ? 'draft' : 'publish';
559 $post_content = preg_replace_callback('|<(/?[A-Z]+)|', array( &$this, '_normalize_tag' ), $post_content);
560 $post_content = str_replace('<br>', '<br />', $post_content);
561 $post_content = str_replace('<hr>', '<hr />', $post_content);
563 // Checks for duplicates
564 if ( isset( $this->blogs[$importing_blog]['posts'][$entry->old_permalink] ) ) {
565 ++$this->blogs[$importing_blog]['posts_skipped'];
566 } elseif ( $post_id = post_exists( $post_title, $post_content, $post_date ) ) {
567 $this->blogs[$importing_blog]['posts'][$entry->old_permalink] = $post_id;
568 ++$this->blogs[$importing_blog]['posts_skipped'];
570 $post = compact('post_date', 'post_content', 'post_title', 'post_status');
572 $post_id = wp_insert_post($post);
573 if ( is_wp_error( $post_id ) )
576 wp_create_categories( array_map( 'addslashes', $entry->categories ), $post_id );
578 $author = $this->no_apos( strip_tags( $entry->author ) );
580 add_post_meta( $post_id, 'blogger_blog', $this->blogs[$importing_blog]['host'], true );
581 add_post_meta( $post_id, 'blogger_author', $author, true );
582 add_post_meta( $post_id, 'blogger_permalink', $entry->old_permalink, true );
584 $this->blogs[$importing_blog]['posts'][$entry->old_permalink] = $post_id;
585 ++$this->blogs[$importing_blog]['posts_done'];
591 function import_comment( $entry ) {
592 global $importing_blog;
594 // Drop the #fragment and we have the comment's old post permalink.
595 foreach ( $entry->links as $link ) {
596 if ( $link['rel'] == 'alternate' ) {
597 $parts = parse_url( $link['href'] );
598 $entry->old_permalink = $parts['fragment'];
599 $entry->old_post_permalink = $parts['path'];
604 $comment_post_ID = (int) $this->blogs[$importing_blog]['posts'][$entry->old_post_permalink];
605 preg_match('#<name>(.+?)</name>.*(?:\<uri>(.+?)</uri>)?#', $entry->author, $matches);
606 $comment_author = addslashes( $this->no_apos( strip_tags( (string) $matches[1] ) ) );
607 $comment_author_url = addslashes( $this->no_apos( strip_tags( (string) $matches[2] ) ) );
608 $comment_date = $this->convert_date( $entry->updated );
609 $comment_content = addslashes( $this->no_apos( @html_entity_decode( $entry->content, ENT_COMPAT, get_option('blog_charset') ) ) );
612 $comment_content = preg_replace_callback('|<(/?[A-Z]+)|', array( &$this, '_normalize_tag' ), $comment_content);
613 $comment_content = str_replace('<br>', '<br />', $comment_content);
614 $comment_content = str_replace('<hr>', '<hr />', $comment_content);
616 // Checks for duplicates
618 isset( $this->blogs[$importing_blog]['comments'][$entry->old_permalink] ) ||
619 comment_exists( $comment_author, $comment_date )
621 ++$this->blogs[$importing_blog]['comments_skipped'];
623 $comment = compact('comment_post_ID', 'comment_author', 'comment_author_url', 'comment_date', 'comment_content');
625 $comment = wp_filter_comment($comment);
626 $comment_id = wp_insert_comment($comment);
628 $this->blogs[$importing_blog]['comments'][$entry->old_permalink] = $comment_id;
630 ++$this->blogs[$importing_blog]['comments_done'];
635 function get_js_status($blog = false) {
636 global $importing_blog;
637 if ( $blog === false )
638 $blog = $this->blogs[$importing_blog];
640 $blog = $this->blogs[$blog];
641 $p1 = isset( $blog['posts_done'] ) ? (int) $blog['posts_done'] : 0;
642 $p2 = isset( $blog['total_posts'] ) ? (int) $blog['total_posts'] : 0;
643 $c1 = isset( $blog['comments_done'] ) ? (int) $blog['comments_done'] : 0;
644 $c2 = isset( $blog['total_comments'] ) ? (int) $blog['total_comments'] : 0;
645 return "{p1:$p1,p2:$p2,c1:$c1,c2:$c2}";
648 function get_author_form($blog = false) {
649 global $importing_blog, $wpdb, $current_user;
650 if ( $blog === false )
651 $blog = & $this->blogs[$importing_blog];
653 $blog = & $this->blogs[$blog];
655 if ( !isset( $blog['authors'] ) ) {
656 $post_ids = array_values($blog['posts']);
657 $authors = (array) $wpdb->get_col("SELECT DISTINCT meta_value FROM $wpdb->postmeta WHERE meta_key = 'blogger_author' AND post_id IN (" . join( ',', $post_ids ) . ")");
658 $blog['authors'] = array_map(null, $authors, array_fill(0, count($authors), $current_user->ID));
662 $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.');
663 $heading = __('Author mapping');
664 $blogtitle = "{$blog['title']} ({$blog['host']})";
665 $mapthis = __('Blogger username');
666 $tothis = __('WordPress login');
667 $submit = esc_js( __('Save Changes') );
669 foreach ( $blog['authors'] as $i => $author )
670 $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>";
672 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>";
675 function get_user_options($current) {
676 global $importer_users;
677 if ( ! isset( $importer_users ) )
678 $importer_users = (array) get_users_of_blog();
680 foreach ( $importer_users as $user ) {
681 $sel = ( $user->user_id == $current ) ? " selected='selected'" : '';
682 $options .= "<option value='$user->user_id'$sel>$user->display_name</option>";
688 function save_authors() {
689 global $importing_blog, $wpdb;
690 $authors = (array) $_POST['authors'];
692 $host = $this->blogs[$importing_blog]['host'];
694 // Get an array of posts => authors
695 $post_ids = (array) $wpdb->get_col( $wpdb->prepare("SELECT post_id FROM $wpdb->postmeta WHERE meta_key = 'blogger_blog' AND meta_value = %s", $host) );
696 $post_ids = join( ',', $post_ids );
697 $results = (array) $wpdb->get_results("SELECT post_id, meta_value FROM $wpdb->postmeta WHERE meta_key = 'blogger_author' AND post_id IN ($post_ids)");
698 foreach ( $results as $row )
699 $authors_posts[$row->post_id] = $row->meta_value;
701 foreach ( $authors as $author => $user_id ) {
702 $user_id = (int) $user_id;
704 // Skip authors that haven't been changed
705 if ( $user_id == $this->blogs[$importing_blog]['authors'][$author][1] )
708 // Get a list of the selected author's posts
709 $post_ids = (array) array_keys( $authors_posts, $this->blogs[$importing_blog]['authors'][$author][0] );
710 $post_ids = join( ',', $post_ids);
712 $wpdb->query( $wpdb->prepare("UPDATE $wpdb->posts SET post_author = %d WHERE id IN ($post_ids)", $user_id) );
713 $this->blogs[$importing_blog]['authors'][$author][1] = $user_id;
717 wp_redirect('edit.php');
720 function _get_auth_sock() {
721 // Connect to https://www.google.com
722 if ( !$sock = @ fsockopen('ssl://www.google.com', 443, $errno, $errstr) ) {
724 __('Could not connect to https://www.google.com'),
725 __('There was a problem opening a secure connection to Google. This is what went wrong:'),
733 function _get_blogger_sock($host = 'www2.blogger.com') {
734 if ( !$sock = @ fsockopen($host, 80, $errno, $errstr) ) {
736 sprintf( __('Could not connect to %s'), $host ),
737 __('There was a problem opening a connection to Blogger. This is what went wrong:'),
745 function _txrx( $sock, $request ) {
746 fwrite( $sock, $request );
747 while ( ! feof( $sock ) )
748 $response .= @ fread ( $sock, 8192 );
753 function revoke($token) {
755 "GET /accounts/AuthSubRevokeToken HTTP/1.0",
756 "Authorization: AuthSub token=\"$token\""
758 $request = join( "\r\n", $headers ) . "\r\n\r\n";
759 $sock = $this->_get_auth_sock( );
760 if ( ! $sock ) return false;
761 $this->_txrx( $sock, $request );
766 $options = get_option( 'blogger_importer' );
768 if ( isset( $options['token'] ) )
769 $this->revoke( $options['token'] );
771 delete_option('blogger_importer');
772 $wpdb->query("DELETE FROM $wpdb->postmeta WHERE meta_key = 'blogger_author'");
773 wp_redirect('?import=blogger');
776 // Returns associative array of code, header, cookies, body. Based on code from php.net.
777 function parse_response($this_response) {
778 // Split response into header and body sections
779 list($response_headers, $response_body) = explode("\r\n\r\n", $this_response, 2);
780 $response_header_lines = explode("\r\n", $response_headers);
782 // First line of headers is the HTTP response code
783 $http_response_line = array_shift($response_header_lines);
784 if(preg_match('@^HTTP/[0-9]\.[0-9] ([0-9]{3})@',$http_response_line, $matches)) { $response_code = $matches[1]; }
786 // put the rest of the headers in an array
787 $response_header_array = array();
788 foreach($response_header_lines as $header_line) {
789 list($header,$value) = explode(': ', $header_line, 2);
790 $response_header_array[$header] .= $value."\n";
793 $cookie_array = array();
794 $cookies = explode("\n", $response_header_array["Set-Cookie"]);
795 foreach($cookies as $this_cookie) { array_push($cookie_array, "Cookie: ".$this_cookie); }
797 return array("code" => $response_code, "header" => $response_header_array, "cookies" => $cookie_array, "body" => $response_body);
800 // Step 9: Congratulate the user
801 function congrats() {
802 $blog = (int) $_GET['blog'];
803 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>';
804 if ( count($this->import['blogs']) > 1 )
805 echo '<li>'.__('In case you haven’t done it already, you can import the posts from your other blogs:'). $this->show_blogs() . '</li>';
806 if ( $n = count($this->import['blogs'][$blog]['newusers']) )
807 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>';
808 echo '<li>'.__('For security, click the link below to reset this importer.').'</li>';
812 // Figures out what to do, then does it.
814 if ( isset($_POST['restart']) )
817 $options = get_option('blogger_importer');
819 if ( is_array($options) )
820 foreach ( $options as $key => $value )
821 $this->$key = $value;
823 if ( isset( $_REQUEST['blog'] ) ) {
824 $blog = is_array($_REQUEST['blog']) ? array_shift( $keys = array_keys( $_REQUEST['blog'] ) ) : $_REQUEST['blog'];
826 $result = $this->import_blog( $blog );
827 if ( is_wp_error( $result ) )
828 echo $result->get_error_message();
829 } elseif ( isset($_GET['token']) )
831 elseif ( isset($this->token) && $this->token_is_valid() )
836 $saved = $this->save_vars();
838 if ( $saved && !isset($_GET['noheader']) ) {
839 $restart = __('Restart');
840 $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.');
841 $submit = esc_attr__('Clear account information');
842 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>";
846 function save_vars() {
847 $vars = get_object_vars($this);
848 update_option( 'blogger_importer', $vars );
850 return !empty($vars);
853 function admin_head() {
855 <style type="text/css">
856 td { text-align: center; line-height: 2em;}
857 thead td { font-weight: bold; }
866 background-color: #83B4D8;
879 function Blogger_Import() {
880 global $importer_started;
881 $importer_started = time();
882 if ( isset( $_GET['import'] ) && $_GET['import'] == 'blogger' ) {
883 wp_enqueue_script('jquery');
884 add_action('admin_head', array(&$this, 'admin_head'));
889 $blogger_import = new Blogger_Import();
891 register_importer('blogger', __('Blogger'), __('Import posts, comments, and users from a Blogger blog.'), array ($blogger_import, 'start'));
894 var $links = array();
895 var $categories = array();
900 var $ATOM_CONTENT_ELEMENTS = array('content','summary','title','subtitle','rights');
901 var $ATOM_SIMPLE_ELEMENTS = array('id','updated','published','draft','author');
906 var $ns_contexts = array();
907 var $ns_decls = array();
908 var $is_xhtml = false;
909 var $skipped_div = false;
913 function AtomParser() {
914 $this->entry = new AtomEntry();
917 function _map_attrs_func( $k, $v ) {
921 function _map_xmlns_func( $p, $n ) {
923 if ( strlen( $n[0] ) > 0 )
926 return "{$xd}=\"{$n[1]}\"";
929 function parse($xml) {
932 array_unshift($this->ns_contexts, array());
934 $parser = xml_parser_create_ns();
935 xml_set_object($parser, $this);
936 xml_set_element_handler($parser, "start_element", "end_element");
937 xml_parser_set_option($parser,XML_OPTION_CASE_FOLDING,0);
938 xml_parser_set_option($parser,XML_OPTION_SKIP_WHITE,0);
939 xml_set_character_data_handler($parser, "cdata");
940 xml_set_default_handler($parser, "_default");
941 xml_set_start_namespace_decl_handler($parser, "start_ns");
942 xml_set_end_namespace_decl_handler($parser, "end_ns");
946 xml_parse($parser, $xml);
948 xml_parser_free($parser);
953 function start_element($parser, $name, $attrs) {
955 $tag = array_pop(split(":", $name));
957 array_unshift($this->ns_contexts, $this->ns_decls);
961 if(!empty($this->in_content)) {
962 $attrs_prefix = array();
964 // resolve prefixes for attributes
965 foreach($attrs as $key => $value) {
966 $attrs_prefix[$this->ns_to_prefix($key)] = $this->xml_escape($value);
968 $attrs_str = join(' ', array_map( array( &$this, '_map_attrs_func' ), array_keys($attrs_prefix), array_values($attrs_prefix)));
969 if(strlen($attrs_str) > 0) {
970 $attrs_str = " " . $attrs_str;
973 $xmlns_str = join(' ', array_map( array( &$this, '_map_xmlns_func' ), array_keys($this->ns_contexts[0]), array_values($this->ns_contexts[0])));
974 if(strlen($xmlns_str) > 0) {
975 $xmlns_str = " " . $xmlns_str;
978 // handle self-closing tags (case: a new child found right-away, no text node)
979 if(count($this->in_content) == 2) {
980 array_push($this->in_content, ">");
983 array_push($this->in_content, "<". $this->ns_to_prefix($name) ."{$xmlns_str}{$attrs_str}");
984 } else if(in_array($tag, $this->ATOM_CONTENT_ELEMENTS) || in_array($tag, $this->ATOM_SIMPLE_ELEMENTS)) {
985 $this->in_content = array();
986 $this->is_xhtml = $attrs['type'] == 'xhtml';
987 array_push($this->in_content, array($tag,$this->depth));
988 } else if($tag == 'link') {
989 array_push($this->entry->links, $attrs);
990 } else if($tag == 'category') {
991 array_push($this->entry->categories, $attrs['term']);
994 $this->ns_decls = array();
997 function end_element($parser, $name) {
999 $tag = array_pop(split(":", $name));
1001 if(!empty($this->in_content)) {
1002 if($this->in_content[0][0] == $tag &&
1003 $this->in_content[0][1] == $this->depth) {
1004 array_shift($this->in_content);
1005 if($this->is_xhtml) {
1006 $this->in_content = array_slice($this->in_content, 2, count($this->in_content)-3);
1008 $this->entry->$tag = join('',$this->in_content);
1009 $this->in_content = array();
1011 $endtag = $this->ns_to_prefix($name);
1012 if (strpos($this->in_content[count($this->in_content)-1], '<' . $endtag) !== false) {
1013 array_push($this->in_content, "/>");
1015 array_push($this->in_content, "</$endtag>");
1020 array_shift($this->ns_contexts);
1022 #print str_repeat(" ", $this->depth * $this->indent) . "end_element('$name')" ."\n";
1027 function start_ns($parser, $prefix, $uri) {
1028 #print str_repeat(" ", $this->depth * $this->indent) . "starting: " . $prefix . ":" . $uri . "\n";
1029 array_push($this->ns_decls, array($prefix,$uri));
1032 function end_ns($parser, $prefix) {
1033 #print str_repeat(" ", $this->depth * $this->indent) . "ending: #" . $prefix . "#\n";
1036 function cdata($parser, $data) {
1037 #print str_repeat(" ", $this->depth * $this->indent) . "data: #" . $data . "#\n";
1038 if(!empty($this->in_content)) {
1039 // handle self-closing tags (case: text node found, need to close element started)
1040 if (strpos($this->in_content[count($this->in_content)-1], '<') !== false) {
1041 array_push($this->in_content, ">");
1043 array_push($this->in_content, $this->xml_escape($data));
1047 function _default($parser, $data) {
1048 # when does this gets called?
1052 function ns_to_prefix($qname) {
1053 $components = split(":", $qname);
1054 $name = array_pop($components);
1056 if(!empty($components)) {
1057 $ns = join(":",$components);
1058 foreach($this->ns_contexts as $context) {
1059 foreach($context as $mapping) {
1060 if($mapping[1] == $ns && strlen($mapping[0]) > 0) {
1061 return "$mapping[0]:$name";
1069 function xml_escape($string)
1071 return str_replace(array('&','"',"'",'<','>'),
1072 array('&','"',''','<','>'),