web/wp-admin/import/blogger.php
changeset 136 bde1974c263b
equal deleted inserted replaced
135:53cff4b4a802 136:bde1974c263b
       
     1 <?php
       
     2 /**
       
     3  * Blogger Importer
       
     4  *
       
     5  * @package WordPress
       
     6  * @subpackage Importer
       
     7  */
       
     8 
       
     9 /**
       
    10  * How many records per GData query
       
    11  *
       
    12  * @package WordPress
       
    13  * @subpackage Blogger_Import
       
    14  * @var int
       
    15  * @since unknown
       
    16  */
       
    17 define( 'MAX_RESULTS',        50 );
       
    18 
       
    19 /**
       
    20  * How many seconds to let the script run
       
    21  *
       
    22  * @package WordPress
       
    23  * @subpackage Blogger_Import
       
    24  * @var int
       
    25  * @since unknown
       
    26  */
       
    27 define( 'MAX_EXECUTION_TIME', 20 );
       
    28 
       
    29 /**
       
    30  * How many seconds between status bar updates
       
    31  *
       
    32  * @package WordPress
       
    33  * @subpackage Blogger_Import
       
    34  * @var int
       
    35  * @since unknown
       
    36  */
       
    37 define( 'STATUS_INTERVAL',     3 );
       
    38 
       
    39 /**
       
    40  * Blogger Importer class
       
    41  *
       
    42  * @since unknown
       
    43  */
       
    44 class Blogger_Import {
       
    45 
       
    46 	// Shows the welcome screen and the magic auth link.
       
    47 	function greet() {
       
    48 		$next_url = get_option('siteurl') . '/wp-admin/index.php?import=blogger&amp;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');
       
    55 
       
    56 		echo "
       
    57 		<div class='wrap'>
       
    58 		".screen_icon()."
       
    59 		<h2>$title</h2>
       
    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' />
       
    68 				</p>
       
    69 			</form>
       
    70 		</div>\n";
       
    71 	}
       
    72 
       
    73 	function uh_oh($title, $message, $info) {
       
    74 		echo "<div class='wrap'>";
       
    75 		screen_icon();
       
    76 		echo "<h2>$title</h2><p>$message</p><pre>$info</pre></div>";
       
    77 	}
       
    78 
       
    79 	function auth() {
       
    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'] );
       
    82 		$headers = array(
       
    83 			"GET /accounts/AuthSubSessionToken HTTP/1.0",
       
    84 			"Authorization: AuthSub token=\"$token\""
       
    85 		);
       
    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] ) ) {
       
    92 			$this->uh_oh(
       
    93 				__( 'Authorization failed' ),
       
    94 				__( 'Something went wrong. If the problem persists, send this info to support:' ),
       
    95 				htmlspecialchars($response)
       
    96 			);
       
    97 			return false;
       
    98 		}
       
    99 		$this->token = $matches[1];
       
   100 
       
   101 		wp_redirect( remove_query_arg( array( 'token', 'noheader' ) ) );
       
   102 	}
       
   103 
       
   104 	function get_token_info() {
       
   105 		$headers = array(
       
   106 			"GET /accounts/AuthSubTokenInfo  HTTP/1.0",
       
   107 			"Authorization: AuthSub token=\"$this->token\""
       
   108 		);
       
   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);
       
   114 	}
       
   115 
       
   116 	function token_is_valid() {
       
   117 		$info = $this->get_token_info();
       
   118 
       
   119 		if ( $info['code'] == 200 )
       
   120 			return true;
       
   121 
       
   122 		return false;
       
   123 	}
       
   124 
       
   125 	function show_blogs($iter = 0) {
       
   126 		if ( empty($this->blogs) ) {
       
   127 			$headers = array(
       
   128 				"GET /feeds/default/blogs HTTP/1.0",
       
   129 				"Host: www.blogger.com",
       
   130 				"Authorization: AuthSub token=\"$this->token\""
       
   131 			);
       
   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 );
       
   136 
       
   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);
       
   141 			xml_parser_free($p);
       
   142 
       
   143 			$this->title = $vals[$index['TITLE'][0]]['value'];
       
   144 
       
   145 			// Give it a few retries... this step often flakes out the first time.
       
   146 			if ( empty( $index['ENTRY'] ) ) {
       
   147 				if ( $iter < 3 ) {
       
   148 					return $this->show_blogs($iter + 1);
       
   149 				} else {
       
   150 					$this->uh_oh(
       
   151 						__('Trouble signing in'),
       
   152 						__('We were not able to gain access to your account. Try starting over.'),
       
   153 						''
       
   154 					);
       
   155 					return false;
       
   156 				}
       
   157 			}
       
   158 
       
   159 			foreach ( $index['ENTRY'] as $i ) {
       
   160 				$blog = array();
       
   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'];
       
   172 					}
       
   173 					++$i;
       
   174 				}
       
   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;
       
   180 				}
       
   181 			}
       
   182 
       
   183 			if ( empty( $this->blogs ) ) {
       
   184 				$this->uh_oh(
       
   185 					__('No blogs found'),
       
   186 					__('We were able to log in but there were no blogs. Try a different account next time.'),
       
   187 					''
       
   188 				);
       
   189 				return false;
       
   190 			}
       
   191 		}
       
   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&#8217;t worry, you can turn it back off when you&#8217;re done.');
       
   208 
       
   209 		$interval = STATUS_INTERVAL * 1000;
       
   210 
       
   211 		foreach ( $this->blogs as $i => $blog ) {
       
   212 			if ( $blog['mode'] == 'init' )
       
   213 				$value = $start;
       
   214 			elseif ( $blog['mode'] == 'posts' || $blog['mode'] == 'comments' )
       
   215 				$value = $continue;
       
   216 			else
       
   217 				$value = $authors;
       
   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'>&nbsp;</div><div id='pstat$i' class='stat'>$pdone/{$blog['total_posts']}</div>";
       
   224 			$cstat = "<div class='ind' id='cind$i'>&nbsp;</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";
       
   226 		}
       
   227 
       
   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>";
       
   229 		echo "
       
   230 		<script type='text/javascript'>
       
   231 		/* <![CDATA[ */
       
   232 			var strings = {cont:'$continue',stop:'$stop',stopping:'$stopping',authors:'$authors',nothing:'$nothing'};
       
   233 			var blogs = {};
       
   234 			function blog(i, title, mode, status){
       
   235 				this.blog   = i;
       
   236 				this.mode   = mode;
       
   237 				this.title  = title;
       
   238 				this.status = status;
       
   239 				this.button = document.getElementById('submit'+this.blog);
       
   240 			};
       
   241 			blog.prototype = {
       
   242 				start: function() {
       
   243 					this.cont = true;
       
   244 					this.kick();
       
   245 					this.check();
       
   246 				},
       
   247 				kick: function() {
       
   248 					++this.kicks;
       
   249 					var i = this.blog;
       
   250 					jQuery.post('admin.php?import=blogger&noheader=true',{blog:this.blog},function(text,result){blogs[i].kickd(text,result)});
       
   251 				},
       
   252 				check: function() {
       
   253 					++this.checks;
       
   254 					var i = this.blog;
       
   255 					jQuery.post('admin.php?import=blogger&noheader=true&status=true',{blog:this.blog},function(text,result){blogs[i].checkd(text,result)});
       
   256 				},
       
   257 				kickd: function(text, result) {
       
   258 					if ( result == 'error' ) {
       
   259 						// TODO: exception handling
       
   260 						if ( this.cont )
       
   261 							setTimeout('blogs['+this.blog+'].kick()', 1000);
       
   262 					} else {
       
   263 						if ( text == 'done' ) {
       
   264 							this.stop();
       
   265 							this.done();
       
   266 						} else if ( text == 'nothing' ) {
       
   267 							this.stop();
       
   268 							this.nothing();
       
   269 						} else if ( text == 'continue' ) {
       
   270 							this.kick();
       
   271 						} else if ( this.mode = 'stopped' )
       
   272 							jQuery(this.button).attr('value', strings.cont);
       
   273 					}
       
   274 					--this.kicks;
       
   275 				},
       
   276 				checkd: function(text, result) {
       
   277 					if ( result == 'error' ) {
       
   278 						// TODO: exception handling
       
   279 					} else {
       
   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);
       
   283 						this.update();
       
   284 						if ( this.cont || this.kicks > 0 )
       
   285 							setTimeout('blogs['+this.blog+'].check()', $interval);
       
   286 					}
       
   287 					--this.checks;
       
   288 				},
       
   289 				update: function() {
       
   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');
       
   292 				},
       
   293 				stop: function() {
       
   294 					this.cont = false;
       
   295 				},
       
   296 				done: function() {
       
   297 					this.mode = 'authors';
       
   298 					jQuery(this.button).attr('value', strings.authors);
       
   299 				},
       
   300 				nothing: function() {
       
   301 					this.mode = 'nothing';
       
   302 					jQuery(this.button).remove();
       
   303 					alert(strings.nothing);
       
   304 				},
       
   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});
       
   311 				},
       
   312 				init: function() {
       
   313 					this.update();
       
   314 					var i = this.blog;
       
   315 					jQuery(this.button).bind('click', function(){return blogs[i].click();});
       
   316 					this.kicks = 0;
       
   317 					this.checks = 0;
       
   318 				},
       
   319 				click: function() {
       
   320 					if ( this.mode == 'init' || this.mode == 'stopped' || this.mode == 'posts' || this.mode == 'comments' ) {
       
   321 						this.mode = 'started';
       
   322 						this.start();
       
   323 						jQuery(this.button).attr('value', strings.stop);
       
   324 					} else if ( this.mode == 'started' ) {
       
   325 						return false; // let it run...
       
   326 						this.mode = 'stopped';
       
   327 						this.stop();
       
   328 						if ( this.checks > 0 || this.kicks > 0 ) {
       
   329 							this.mode = 'stopping';
       
   330 							jQuery(this.button).attr('value', strings.stopping);
       
   331 						} else {
       
   332 							jQuery(this.button).attr('value', strings.cont);
       
   333 						}
       
   334 					} else if ( this.mode == 'authors' ) {
       
   335 						document.location = 'index.php?import=blogger&authors=1&blog='+this.blog;
       
   336 						//this.mode = 'authors2';
       
   337 						//this.getauthors();
       
   338 					}
       
   339 					return false;
       
   340 				}
       
   341 			};
       
   342 			$init
       
   343 			jQuery.each(blogs, function(i, me){me.init();});
       
   344 		/* ]]> */
       
   345 		</script>\n";
       
   346 	}
       
   347 
       
   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 )
       
   352 			die('continue');
       
   353 		return true;
       
   354 	}
       
   355 
       
   356 	function get_total_results($type, $host) {
       
   357 		$headers = array(
       
   358 			"GET /feeds/$type/default?max-results=1&start-index=2 HTTP/1.0",
       
   359 			"Host: $host",
       
   360 			"Authorization: AuthSub token=\"$this->token\""
       
   361 		);
       
   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;
       
   372 	}
       
   373 
       
   374 	function import_blog($blogID) {
       
   375 		global $importing_blog;
       
   376 		$importing_blog = $blogID;
       
   377 
       
   378 		if ( isset($_GET['authors']) )
       
   379 			return print($this->get_author_form());
       
   380 
       
   381 		header('Content-Type: text/plain');
       
   382 
       
   383 		if ( isset($_GET['status']) )
       
   384 			die($this->get_js_status());
       
   385 
       
   386 		if ( isset($_GET['saveauthors']) )
       
   387 			die($this->save_authors());
       
   388 
       
   389 		$blog = $this->blogs[$blogID];
       
   390 		$total_results = $this->get_total_results('posts', $blog['host']);
       
   391 		$this->blogs[$importing_blog]['total_posts'] = $total_results;
       
   392 
       
   393 		$start_index = $total_results - MAX_RESULTS + 1;
       
   394 
       
   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;
       
   399 		else
       
   400 			$start_index = 1;
       
   401 
       
   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;
       
   407 			do {
       
   408 				$index = $struct = $entries = array();
       
   409 				$headers = array(
       
   410 					"GET /feeds/posts/default?$query HTTP/1.0",
       
   411 					"Host: {$blog['host']}",
       
   412 					"Authorization: AuthSub token=\"$this->token\""
       
   413 				);
       
   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 );
       
   418 
       
   419 				$response = $this->parse_response( $response );
       
   420 
       
   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 ) )
       
   431 							return $result;
       
   432 						unset($AtomParser);
       
   433 					}
       
   434 				} else break;
       
   435 
       
   436 				// Get the 'previous' query string which we'll use on the next iteration
       
   437 				$query = '';
       
   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') );
       
   443 
       
   444 				if ( $query ) {
       
   445 					parse_str($query, $q);
       
   446 					$this->blogs[$importing_blog]['posts_start_index'] = (int) $q['start-index'];
       
   447 				} else
       
   448 					$this->blogs[$importing_blog]['posts_start_index'] = 0;
       
   449 				$this->save_vars();
       
   450 			} while ( !empty( $query ) && $this->have_time() );
       
   451 		}
       
   452 
       
   453 		$total_results = $this->get_total_results( 'comments', $blog['host'] );
       
   454 		$this->blogs[$importing_blog]['total_comments'] = $total_results;
       
   455 
       
   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;
       
   460 		else
       
   461 			$start_index = 1;
       
   462 
       
   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;
       
   467 			do {
       
   468 				$index = $struct = $entries = array();
       
   469 				$headers = array(
       
   470 					"GET /feeds/comments/default?$query HTTP/1.0",
       
   471 					"Host: {$blog['host']}",
       
   472 					"Authorization: AuthSub token=\"$this->token\""
       
   473 				);
       
   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 );
       
   478 
       
   479 				$response = $this->parse_response( $response );
       
   480 
       
   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);
       
   490 						unset($AtomParser);
       
   491 					}
       
   492 				}
       
   493 
       
   494 				// Get the 'previous' query string which we'll use on the next iteration
       
   495 				$query = '';
       
   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') );
       
   501 
       
   502 				parse_str($query, $q);
       
   503 
       
   504 				$this->blogs[$importing_blog]['comments_start_index'] = (int) $q['start-index'];
       
   505 				$this->save_vars();
       
   506 			} while ( !empty( $query ) && $this->have_time() );
       
   507 		}
       
   508 		$this->blogs[$importing_blog]['mode'] = 'authors';
       
   509 		$this->save_vars();
       
   510 		if ( !$this->blogs[$importing_blog]['posts_done'] && !$this->blogs[$importing_blog]['comments_done'] )
       
   511 			die('nothing');
       
   512 		do_action('import_done', 'blogger');
       
   513 		die('done');
       
   514 	}
       
   515 
       
   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);
       
   523 	}
       
   524 
       
   525 	function no_apos( $string ) {
       
   526 		return str_replace( '&apos;', "'", $string);
       
   527 	}
       
   528 
       
   529 	function min_whitespace( $string ) {
       
   530 		return preg_replace( '|\s+|', ' ', $string );
       
   531 	}
       
   532 
       
   533 	function _normalize_tag( $matches ) {
       
   534 		return '<' . strtolower( $matches[1] );
       
   535 	}
       
   536 
       
   537 	function import_post( $entry ) {
       
   538 		global $importing_blog;
       
   539 
       
   540 		// The old permalink is all Blogger gives us to link comments to their posts.
       
   541 		if ( isset( $entry->draft ) )
       
   542 			$rel = 'self';
       
   543 		else
       
   544 			$rel = 'alternate';
       
   545 		foreach ( $entry->links as $link ) {
       
   546 			if ( $link['rel'] == $rel ) {
       
   547 				$parts = parse_url( $link['href'] );
       
   548 				$entry->old_permalink = $parts['path'];
       
   549 				break;
       
   550 			}
       
   551 		}
       
   552 
       
   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';
       
   557 
       
   558 		// Clean up content
       
   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);
       
   562 
       
   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'];
       
   569 		} else {
       
   570 			$post = compact('post_date', 'post_content', 'post_title', 'post_status');
       
   571 
       
   572 			$post_id = wp_insert_post($post);
       
   573 			if ( is_wp_error( $post_id ) )
       
   574 				return $post_id;
       
   575 
       
   576 			wp_create_categories( array_map( 'addslashes', $entry->categories ), $post_id );
       
   577 
       
   578 			$author = $this->no_apos( strip_tags( $entry->author ) );
       
   579 
       
   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 );
       
   583 
       
   584 			$this->blogs[$importing_blog]['posts'][$entry->old_permalink] = $post_id;
       
   585 			++$this->blogs[$importing_blog]['posts_done'];
       
   586 		}
       
   587 		$this->save_vars();
       
   588 		return;
       
   589 	}
       
   590 
       
   591 	function import_comment( $entry ) {
       
   592 		global $importing_blog;
       
   593 
       
   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'];
       
   600 				break;
       
   601 			}
       
   602 		}
       
   603 
       
   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') ) ) );
       
   610 
       
   611 		// Clean up content
       
   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);
       
   615 
       
   616 		// Checks for duplicates
       
   617 		if (
       
   618 			isset( $this->blogs[$importing_blog]['comments'][$entry->old_permalink] ) ||
       
   619 			comment_exists( $comment_author, $comment_date )
       
   620 		) {
       
   621 			++$this->blogs[$importing_blog]['comments_skipped'];
       
   622 		} else {
       
   623 			$comment = compact('comment_post_ID', 'comment_author', 'comment_author_url', 'comment_date', 'comment_content');
       
   624 
       
   625 			$comment = wp_filter_comment($comment);
       
   626 			$comment_id = wp_insert_comment($comment);
       
   627 
       
   628 			$this->blogs[$importing_blog]['comments'][$entry->old_permalink] = $comment_id;
       
   629 
       
   630 			++$this->blogs[$importing_blog]['comments_done'];
       
   631 		}
       
   632 		$this->save_vars();
       
   633 	}
       
   634 
       
   635 	function get_js_status($blog = false) {
       
   636 		global $importing_blog;
       
   637 		if ( $blog === false )
       
   638 			$blog = $this->blogs[$importing_blog];
       
   639 		else
       
   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}";
       
   646 	}
       
   647 
       
   648 	function get_author_form($blog = false) {
       
   649 		global $importing_blog, $wpdb, $current_user;
       
   650 		if ( $blog === false )
       
   651 			$blog = & $this->blogs[$importing_blog];
       
   652 		else
       
   653 			$blog = & $this->blogs[$blog];
       
   654 
       
   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));
       
   659 			$this->save_vars();
       
   660 		}
       
   661 
       
   662 		$directions = __('All posts were imported with the current user as author. Use this form to move each Blogger user&#8217;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 &#8220;Restart&#8221; 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') );
       
   668 
       
   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>";
       
   671 
       
   672 		return "<div class='wrap'><h2>$heading</h2><h3>$blogtitle</h3><p>$directions</p><form action='index.php?import=blogger&amp;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>";
       
   673 	}
       
   674 
       
   675 	function get_user_options($current) {
       
   676 		global $importer_users;
       
   677 		if ( ! isset( $importer_users ) )
       
   678 			$importer_users = (array) get_users_of_blog();
       
   679 
       
   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>";
       
   683 		}
       
   684 
       
   685 		return $options;
       
   686 	}
       
   687 
       
   688 	function save_authors() {
       
   689 		global $importing_blog, $wpdb;
       
   690 		$authors = (array) $_POST['authors'];
       
   691 
       
   692 		$host = $this->blogs[$importing_blog]['host'];
       
   693 
       
   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;
       
   700 
       
   701 		foreach ( $authors as $author => $user_id ) {
       
   702 			$user_id = (int) $user_id;
       
   703 
       
   704 			// Skip authors that haven't been changed
       
   705 			if ( $user_id == $this->blogs[$importing_blog]['authors'][$author][1] )
       
   706 				continue;
       
   707 
       
   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);
       
   711 
       
   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;
       
   714 		}
       
   715 		$this->save_vars();
       
   716 
       
   717 		wp_redirect('edit.php');
       
   718 	}
       
   719 
       
   720 	function _get_auth_sock() {
       
   721 		// Connect to https://www.google.com
       
   722 		if ( !$sock = @ fsockopen('ssl://www.google.com', 443, $errno, $errstr) ) {
       
   723 			$this->uh_oh(
       
   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:'),
       
   726 				"$errstr ($errno)"
       
   727 			);
       
   728 			return false;
       
   729 		}
       
   730 		return $sock;
       
   731 	}
       
   732 
       
   733 	function _get_blogger_sock($host = 'www2.blogger.com') {
       
   734 		if ( !$sock = @ fsockopen($host, 80, $errno, $errstr) ) {
       
   735 			$this->uh_oh(
       
   736 				sprintf( __('Could not connect to %s'), $host ),
       
   737 				__('There was a problem opening a connection to Blogger. This is what went wrong:'),
       
   738 				"$errstr ($errno)"
       
   739 			);
       
   740 			return false;
       
   741 		}
       
   742 		return $sock;
       
   743 	}
       
   744 
       
   745 	function _txrx( $sock, $request ) {
       
   746 		fwrite( $sock, $request );
       
   747 		while ( ! feof( $sock ) )
       
   748 			$response .= @ fread ( $sock, 8192 );
       
   749 		fclose( $sock );
       
   750 		return $response;
       
   751 	}
       
   752 
       
   753 	function revoke($token) {
       
   754 		$headers = array(
       
   755 			"GET /accounts/AuthSubRevokeToken HTTP/1.0",
       
   756 			"Authorization: AuthSub token=\"$token\""
       
   757 		);
       
   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 );
       
   762 	}
       
   763 
       
   764 	function restart() {
       
   765 		global $wpdb;
       
   766 		$options = get_option( 'blogger_importer' );
       
   767 
       
   768 		if ( isset( $options['token'] ) )
       
   769 			$this->revoke( $options['token'] );
       
   770 
       
   771 		delete_option('blogger_importer');
       
   772 		$wpdb->query("DELETE FROM $wpdb->postmeta WHERE meta_key = 'blogger_author'");
       
   773 		wp_redirect('?import=blogger');
       
   774 	}
       
   775 
       
   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);
       
   781 
       
   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]; }
       
   785 
       
   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";
       
   791 		}
       
   792 
       
   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); }
       
   796 
       
   797 		return array("code" => $response_code, "header" => $response_header_array, "cookies" => $cookie_array, "body" => $response_body);
       
   798 	}
       
   799 
       
   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&#8217;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 &amp; 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>';
       
   809 		echo '</ul>';
       
   810 	}
       
   811 
       
   812 	// Figures out what to do, then does it.
       
   813 	function start() {
       
   814 		if ( isset($_POST['restart']) )
       
   815 			$this->restart();
       
   816 
       
   817 		$options = get_option('blogger_importer');
       
   818 
       
   819 		if ( is_array($options) )
       
   820 			foreach ( $options as $key => $value )
       
   821 				$this->$key = $value;
       
   822 
       
   823 		if ( isset( $_REQUEST['blog'] ) ) {
       
   824 			$blog = is_array($_REQUEST['blog']) ? array_shift( $keys = array_keys( $_REQUEST['blog'] ) ) : $_REQUEST['blog'];
       
   825 			$blog = (int) $blog;
       
   826 			$result = $this->import_blog( $blog );
       
   827 			if ( is_wp_error( $result ) )
       
   828 				echo $result->get_error_message();
       
   829 		} elseif ( isset($_GET['token']) )
       
   830 			$this->auth();
       
   831 		elseif ( isset($this->token) && $this->token_is_valid() )
       
   832 			$this->show_blogs();
       
   833 		else
       
   834 			$this->greet();
       
   835 
       
   836 		$saved = $this->save_vars();
       
   837 
       
   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&amp;noheader=true'><p class='submit' style='text-align:left;'><input type='submit' class='button' value='$submit' name='restart' /></p></form></div>";
       
   843 		}
       
   844 	}
       
   845 
       
   846 	function save_vars() {
       
   847 		$vars = get_object_vars($this);
       
   848 		update_option( 'blogger_importer', $vars );
       
   849 
       
   850 		return !empty($vars);
       
   851 	}
       
   852 
       
   853 	function admin_head() {
       
   854 ?>
       
   855 <style type="text/css">
       
   856 td { text-align: center; line-height: 2em;}
       
   857 thead td { font-weight: bold; }
       
   858 .bar {
       
   859 	width: 200px;
       
   860 	text-align: left;
       
   861 	line-height: 2em;
       
   862 	padding: 0px;
       
   863 }
       
   864 .ind {
       
   865 	position: absolute;
       
   866 	background-color: #83B4D8;
       
   867 	width: 1px;
       
   868 	z-index: 9;
       
   869 }
       
   870 .stat {
       
   871 	z-index: 10;
       
   872 	position: relative;
       
   873 	text-align: center;
       
   874 }
       
   875 </style>
       
   876 <?php
       
   877 	}
       
   878 
       
   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'));
       
   885 		}
       
   886 	}
       
   887 }
       
   888 
       
   889 $blogger_import = new Blogger_Import();
       
   890 
       
   891 register_importer('blogger', __('Blogger'), __('Import posts, comments, and users from a Blogger blog.'), array ($blogger_import, 'start'));
       
   892 
       
   893 class AtomEntry {
       
   894 	var $links = array();
       
   895 	var $categories = array();
       
   896 }
       
   897 
       
   898 class AtomParser {
       
   899 
       
   900 	var $ATOM_CONTENT_ELEMENTS = array('content','summary','title','subtitle','rights');
       
   901 	var $ATOM_SIMPLE_ELEMENTS = array('id','updated','published','draft','author');
       
   902 
       
   903 	var $depth = 0;
       
   904 	var $indent = 2;
       
   905 	var $in_content;
       
   906 	var $ns_contexts = array();
       
   907 	var $ns_decls = array();
       
   908 	var $is_xhtml = false;
       
   909 	var $skipped_div = false;
       
   910 
       
   911 	var $entry;
       
   912 
       
   913 	function AtomParser() {
       
   914 		$this->entry = new AtomEntry();
       
   915 	}
       
   916 
       
   917 	function _map_attrs_func( $k, $v ) {
       
   918 		return "$k=\"$v\"";
       
   919 	}
       
   920 
       
   921 	function _map_xmlns_func( $p, $n ) {
       
   922 		$xd = "xmlns";
       
   923 		if ( strlen( $n[0] ) > 0 )
       
   924 			$xd .= ":{$n[0]}";
       
   925 
       
   926 		return "{$xd}=\"{$n[1]}\"";
       
   927 	}
       
   928 
       
   929 	function parse($xml) {
       
   930 
       
   931 		global $app_logging;
       
   932 		array_unshift($this->ns_contexts, array());
       
   933 
       
   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");
       
   943 
       
   944 		$contents = "";
       
   945 
       
   946 		xml_parse($parser, $xml);
       
   947 
       
   948 		xml_parser_free($parser);
       
   949 
       
   950 		return true;
       
   951 	}
       
   952 
       
   953 	function start_element($parser, $name, $attrs) {
       
   954 
       
   955 		$tag = array_pop(split(":", $name));
       
   956 
       
   957 		array_unshift($this->ns_contexts, $this->ns_decls);
       
   958 
       
   959 		$this->depth++;
       
   960 
       
   961 		if(!empty($this->in_content)) {
       
   962 			$attrs_prefix = array();
       
   963 
       
   964 			// resolve prefixes for attributes
       
   965 			foreach($attrs as $key => $value) {
       
   966 				$attrs_prefix[$this->ns_to_prefix($key)] = $this->xml_escape($value);
       
   967 			}
       
   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;
       
   971 			}
       
   972 
       
   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;
       
   976 			}
       
   977 
       
   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, ">");
       
   981 			}
       
   982 
       
   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']);
       
   992 		}
       
   993 
       
   994 		$this->ns_decls = array();
       
   995 	}
       
   996 
       
   997 	function end_element($parser, $name) {
       
   998 
       
   999 		$tag = array_pop(split(":", $name));
       
  1000 
       
  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);
       
  1007 				}
       
  1008 				$this->entry->$tag = join('',$this->in_content);
       
  1009 				$this->in_content = array();
       
  1010 			} else {
       
  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, "/>");
       
  1014 				} else {
       
  1015 					array_push($this->in_content, "</$endtag>");
       
  1016 				}
       
  1017 			}
       
  1018 		}
       
  1019 
       
  1020 		array_shift($this->ns_contexts);
       
  1021 
       
  1022 		#print str_repeat(" ", $this->depth * $this->indent) . "end_element('$name')" ."\n";
       
  1023 
       
  1024 		$this->depth--;
       
  1025 	}
       
  1026 
       
  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));
       
  1030 	}
       
  1031 
       
  1032 	function end_ns($parser, $prefix) {
       
  1033 		#print str_repeat(" ", $this->depth * $this->indent) . "ending: #" . $prefix . "#\n";
       
  1034 	}
       
  1035 
       
  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, ">");
       
  1042 			}
       
  1043 			array_push($this->in_content, $this->xml_escape($data));
       
  1044 		}
       
  1045 	}
       
  1046 
       
  1047 	function _default($parser, $data) {
       
  1048 		# when does this gets called?
       
  1049 	}
       
  1050 
       
  1051 
       
  1052 	function ns_to_prefix($qname) {
       
  1053 		$components = split(":", $qname);
       
  1054 		$name = array_pop($components);
       
  1055 
       
  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";
       
  1062 					}
       
  1063 				}
       
  1064 			}
       
  1065 		}
       
  1066 		return $name;
       
  1067 	}
       
  1068 
       
  1069 	function xml_escape($string)
       
  1070 	{
       
  1071 			 return str_replace(array('&','"',"'",'<','>'),
       
  1072 				array('&amp;','&quot;','&apos;','&lt;','&gt;'),
       
  1073 				$string );
       
  1074 	}
       
  1075 }
       
  1076 
       
  1077 ?>