Make WordPress Core

Ticket #14525: 14525.combined.patch

File 14525.combined.patch, 96.9 KB (added by SergeyBiryukov, 13 years ago)
  • blogger-importer-blogitem.php

     
     1<?php
     2
     3/**
     4* Based on WP_SimplePieAtomPub_Item
     5* Expect this to become part of core wordpress at some point.
     6* See http://core.trac.wordpress.org/ticket/7652
     7*
     8* Todo GeoTag parsing
     9* http://codex.wordpress.org/Geodata
     10*
     11 */
     12
     13define('SIMPLEPIE_NAMESPACE_ATOMPUB', 'http://www.w3.org/2007/app');
     14         
     15        /**
     16         * SimplePie Helper for AtomPub
     17         *
     18         * @package WordPress
     19         * @subpackage Publishing
     20         * @since 3.1
     21         */
     22if ( !class_exists( 'WP_SimplePie_Blog_Item' ) ) {   
     23        class WP_SimplePie_Blog_Item extends SimplePie_Item {
     24                /**
     25                 * Constructor
     26                 */
     27                function WP_SimplePieAtomPub_Item($feed, $data) {
     28                        parent::SimplePie_Item($feed, $data);
     29                }
     30         
     31                /**
     32                 * Get the status of the entry
     33                 *
     34                 * @return bool True if the item is a draft, false otherwise
     35                 */
     36                function get_draft_status() {
     37                        $draft = false;
     38                        if (($control = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOMPUB, 'control')) && !empty($control[0]['child'][SIMPLEPIE_NAMESPACE_ATOMPUB]['draft'][0]['data'])) {
     39                                $draft = ('yes' == $control[0]['child'][SIMPLEPIE_NAMESPACE_ATOMPUB]['draft'][0]['data']);
     40                        }
     41                        return $draft;
     42                }
     43 
     44            //Tried using date functions from http://core.trac.wordpress.org/attachment/ticket/7652/7652-separate.diff
     45            //but ended up with 1970s dates so returned to Otto's version which is much simplified
     46                function get_updated() {
     47                $temparray = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'updated');
     48                if ( isset( $temparray[0]['data'] ) ) return $this->convert_date($temparray[0]['data']);
     49                else return NULL;
     50            }
     51         
     52            function get_published() {
     53                $temparray = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'published');
     54                if ( isset( $temparray[0]['data'] ) ) return $this->convert_date($temparray[0]['data']);
     55                else return NULL;
     56            }
     57           
     58            function convert_date( $date ) {
     59                    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);
     60                    $offset = iso8601_timezone_to_offset( $date_bits[7] );
     61                        $timestamp = gmmktime($date_bits[4], $date_bits[5], $date_bits[6], $date_bits[2], $date_bits[3], $date_bits[1]);
     62                        $timestamp -= $offset; // Convert from Blogger local time to GMT
     63                        $timestamp += get_option('gmt_offset') * 3600; // Convert from GMT to WP local time
     64                        return gmdate('Y-m-d H:i:s', $timestamp);
     65                }   
     66     
     67            //Don't Sanitize the ID, the default get_id was cleaning our IDs and that meant that nested comments did not work     
     68                function get_id()
     69                {
     70                                if ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'id'))
     71                                {
     72                                        return $return[0]['data'];
     73                                }
     74             }
     75       
     76    }
     77
     78}
     79
     80?>
     81 No newline at end of file
  • blogger-importer-sanitize.php

     
     1<?php
     2/**
     3 * New class to sanitize trusted content from blogger import
     4 * Based on the SimplePie_Sanitize class by Ryan Parman and Geoffrey Sneddon
     5 *
     6 */
     7
     8class Blogger_Importer_Sanitize extends Simplepie_Sanitize
     9{
     10    // Private vars
     11    var $base;
     12
     13    // Options
     14    var $image_handler = '';
     15    var $strip_htmltags = array('base', 'blink', 'body', 'doctype', 'font', 'form',
     16        'frame', 'frameset', 'html', 'input', 'marquee', 'meta', 'script', 'style');
     17    //Allow iframe (new style) and embed, param and object(old style) so that we get youtube videos transferred
     18    //Allow object and noscript for Amazon widgets
     19    var $encode_instead_of_strip = false;
     20    var $strip_attributes = array('bgsound','class', 'expr', 'id', 'imageanchor', 'onclick', 'onerror',
     21        'onfinish', 'onmouseover', 'onmouseout', 'onfocus', 'onblur', 'lowsrc', 'dynsrc');
     22    //Allow styles so we don't have to redo in Wordpress
     23    //Brett Morgan from Google has confirmed that imageanchor is a made up attribute that is just used in the blogger editor so we can remove that
     24    var $output_encoding = 'UTF-8';
     25    var $enable_cache = true;
     26    var $cache_location = './cache';
     27    var $cache_name_function = 'md5';
     28    var $cache_class = 'SimplePie_Cache';
     29    var $file_class = 'SimplePie_File';
     30    var $timeout = 10;
     31    var $useragent = '';
     32    var $force_fsockopen = false;
     33
     34    var $replace_url_attributes = array('a' => 'href', 'area' => 'href',
     35        'blockquote' => 'cite', 'del' => 'cite', 'form' => 'action', 'img' => array('longdesc',
     36        'src'), 'input' => 'src', 'ins' => 'cite', 'q' => 'cite');
     37
     38        function _normalize_tag( $matches ) {
     39                return '<' . strtolower( $matches[1] );
     40        }
     41
     42    function sanitize($data, $type, $base = '')
     43    {
     44        //Simplified function
     45        $data = trim($data);
     46
     47        // Remappings
     48                $data = str_replace('<br>', '<br />', $data);
     49                $data = str_replace('<hr>', '<hr />', $data);
     50        //<span style="font-weight:bold;">Workshopshed:</span> > <b>Workshopshed:</b>
     51        $data =  preg_replace('|(<span style="font-weight:bold;">)(?<!<span style="font-weight:bold;">).*(.*)(</span>)|', '<strong>$2</strong>', $data);
     52       
     53        //N.B. Don't strip comments as blogger uses <!--more--> which is the same as Wordpress
     54
     55        //Now clean up
     56        foreach ($this->strip_htmltags as $tag) {
     57            $pcre = "/<($tag)" . SIMPLEPIE_PCRE_HTML_ATTRIBUTE . "(>(.*)<\/$tag" .
     58                SIMPLEPIE_PCRE_HTML_ATTRIBUTE . '>|(\/)?>)/siU';
     59            while (preg_match($pcre, $data)) {
     60                $data = preg_replace_callback($pcre, array(&$this, 'do_strip_htmltags'), $data);
     61            }
     62        }
     63
     64        foreach ($this->strip_attributes as $attrib) {
     65            $data = preg_replace('/(<[A-Za-z][^\x09\x0A\x0B\x0C\x0D\x20\x2F\x3E]*)' .
     66                SIMPLEPIE_PCRE_HTML_ATTRIBUTE . trim($attrib) . '(?:\s*=\s*(?:"(?:[^"]*)"|\'(?:[^\']*)\'|(?:[^\x09\x0A\x0B\x0C\x0D\x20\x22\x27\x3E][^\x09\x0A\x0B\x0C\x0D\x20\x3E]*)?))?' .
     67                SIMPLEPIE_PCRE_HTML_ATTRIBUTE . '>/', '\1\2\3>', $data);
     68        }
     69
     70        // Replace relative URLs
     71        $this->base = $base;
     72        foreach ($this->replace_url_attributes as $element => $attributes) {
     73            $data = $this->replace_urls($data, $element, $attributes);
     74        }
     75
     76        // If image handling (caching, etc.) is enabled, cache and rewrite all the image tags.
     77        if (isset($this->image_handler) && ((string )$this->image_handler) !== '' && $this->
     78            enable_cache) {
     79            $images = SimplePie_Misc::get_element('img', $data);
     80            foreach ($images as $img) {
     81                if (isset($img['attribs']['src']['data'])) {
     82                    $image_url = call_user_func($this->cache_name_function, $img['attribs']['src']['data']);
     83                    $cache = call_user_func(array($this->cache_class, 'create'), $this->
     84                        cache_location, $image_url, 'spi');
     85                    if ($cache->load()) {
     86                        $img['attribs']['src']['data'] = $this->image_handler . $image_url;
     87                        $data = str_replace($img['full'], SimplePie_Misc::element_implode($img), $data);
     88                    } else {
     89                        $file = &new $this->file_class($img['attribs']['src']['data'], $this->timeout, 5,
     90                            array('X-FORWARDED-FOR' => $_SERVER['REMOTE_ADDR']), $this->useragent, $this->
     91                            force_fsockopen);
     92                        $headers = $file->headers;
     93                        if ($file->success && ($file->method & SIMPLEPIE_FILE_SOURCE_REMOTE === 0 || ($file->
     94                            status_code === 200 || $file->status_code > 206 && $file->status_code < 300))) {
     95                            if ($cache->save(array('headers' => $file->headers, 'body' => $file->body))) {
     96                                $img['attribs']['src']['data'] = $this->image_handler . $image_url;
     97                                $data = str_replace($img['full'], SimplePie_Misc::element_implode($img), $data);
     98                            } else {
     99                                trigger_error("$this->cache_location is not writeable", E_USER_WARNING);
     100                            }
     101                        }
     102                    }
     103                }
     104            }
     105        }
     106
     107        // Having (possibly) taken stuff out, there may now be whitespace at the beginning/end of the data
     108        $data = trim($data);
     109       
     110        // Normalise tags
     111        $data = preg_replace_callback('|<(/?[A-Z]+)|', array( &$this, '_normalize_tag' ), $data);
     112
     113        return $data;
     114    }
     115
     116    function replace_urls($data, $tag, $attributes)
     117    {
     118        if (!is_array($this->strip_htmltags) || !in_array($tag, $this->strip_htmltags)) {
     119            $elements = SimplePie_Misc::get_element($tag, $data);
     120            foreach ($elements as $element) {
     121                if (is_array($attributes)) {
     122                    foreach ($attributes as $attribute) {
     123                        if (isset($element['attribs'][$attribute]['data'])) {
     124                            $element['attribs'][$attribute]['data'] = SimplePie_Misc::absolutize_url($element['attribs'][$attribute]['data'],
     125                                $this->base);
     126                            $new_element = SimplePie_Misc::element_implode($element);
     127                            $data = str_replace($element['full'], $new_element, $data);
     128                            $element['full'] = $new_element;
     129                        }
     130                    }
     131                } elseif (isset($element['attribs'][$attributes]['data'])) {
     132                    $element['attribs'][$attributes]['data'] = SimplePie_Misc::absolutize_url($element['attribs'][$attributes]['data'],
     133                        $this->base);
     134                    $data = str_replace($element['full'], SimplePie_Misc::element_implode($element),
     135                        $data);
     136                }
     137            }
     138        }
     139        return $data;
     140    }
     141
     142
     143}
     144?>
  • blogger-importer.css

     
     1
     2td { text-align: center; line-height: 2em;}
     3thead td { font-weight: bold; }
     4.bar {
     5        width: 200px;
     6        text-align: left;
     7        line-height: 2em;
     8        padding: 0px;
     9}
     10.ind {
     11        position: absolute;
     12        background-color: #83B4D8;
     13        width: 1px;
     14        z-index: 9;
     15}
     16.stat {
     17        z-index: 10;
     18        position: relative;
     19        text-align: center;
     20}
     21td.submit {
     22        margin:0;
     23        padding:0;
     24}
     25
     26td {
     27        padding-left:10px;
     28        padding-right:10px;
     29}
  • blogger-importer.php

     
    22/*
    33Plugin Name: Blogger Importer
    44Plugin URI: http://wordpress.org/extend/plugins/blogger-importer/
    5 Description: Import posts, comments, tags, and attachments from a Blogger blog.
     5Description: Imports posts, comments and tags from a Blogger blog then migrates authors to Wordpress users.
    66Author: wordpressdotorg
    77Author URI: http://wordpress.org/
    8 Version: 0.4
    9 Stable tag: 0.4
    10 License: GPL version 2 or later - http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
     8Version: 0.6
     9License: GPL v2 - http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
    1110*/
    1211
    1312if ( !defined('WP_LOAD_IMPORTERS') )
     
    1615// Load Importer API
    1716require_once ABSPATH . 'wp-admin/includes/import.php';
    1817
     18// Load Simple Pie
     19require_once ABSPATH . WPINC . '/class-feed.php';
     20require_once 'blogger-importer-sanitize.php';
     21require_once 'blogger-importer-blogitem.php';
     22
     23// Load OAuth library
     24require_once 'oauth.php';
     25
    1926if ( !class_exists( 'WP_Importer' ) ) {
    2027        $class_wp_importer = ABSPATH . 'wp-admin/includes/class-wp-importer.php';
    2128        if ( file_exists( $class_wp_importer ) )
     
    3037 * @var int
    3138 * @since unknown
    3239 */
    33 define( 'MAX_RESULTS',        50 );
     40define( 'MAX_RESULTS', 25 );
    3441
    3542/**
    3643 * How many seconds to let the script run
     
    6168if ( class_exists( 'WP_Importer' ) ) {
    6269class Blogger_Import extends WP_Importer {
    6370
     71        function Blogger_Import() {
     72                global $importer_started;
     73                $importer_started = time();
     74                if ( isset( $_GET['import'] ) && $_GET['import'] == 'blogger' ) {
     75                        add_action('admin_print_scripts', array(&$this, 'queue_scripts'));
     76            add_action('admin_print_styles', array(&$this, 'queue_style'));
     77                }
     78        }
     79   
     80    function queue_scripts($hook) {
     81        wp_enqueue_script('jquery');
     82    }
     83   
     84    function queue_style() {
     85        wp_enqueue_style('BloggerImporter',plugins_url('/blogger-importer.css', __FILE__));
     86    }
     87
    6488        // Shows the welcome screen and the magic auth link.
    6589        function greet() {
    6690                $next_url = get_option('siteurl') . '/wp-admin/index.php?import=blogger&amp;noheader=true';
    67                 $auth_url = "https://www.google.com/accounts/AuthSubRequest";
     91                $auth_url = $this->get_oauth_link();
    6892                $title = __('Import Blogger', 'blogger-importer');
    6993                $welcome = __('Howdy! This importer allows you to import posts and comments from your Blogger account into your WordPress site.', 'blogger-importer');
    7094                $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).', 'blogger-importer');
     
    76100                ".screen_icon()."
    77101                <h2>$title</h2>
    78102                <p>$welcome</p><p>$prereqs</p><p>$stepone</p>
    79                         <form action='$auth_url' method='get'>
     103                        <form action='{$auth_url['url']}' method='get'>
    80104                                <p class='submit' style='text-align:left;'>
    81105                                        <input type='submit' class='button' value='$auth' />
    82                                         <input type='hidden' name='scope' value='http://www.blogger.com/feeds/' />
    83                                         <input type='hidden' name='session' value='1' />
    84                                         <input type='hidden' name='secure' value='0' />
    85                                         <input type='hidden' name='next' value='$next_url' />
     106                                        <input type='hidden' name='oauth_token' value='{$auth_url['oauth_token']}' />
     107                                        <input type='hidden' name='oauth_callback' value='{$auth_url['oauth_callback']}' />
    86108                                </p>
    87109                        </form>
    88110                </div>\n";
    89111        }
     112       
     113        function get_oauth_link() {
     114                // Establish an Blogger_OAuth consumer
     115                $base_url = get_option('siteurl') . '/wp-admin';
     116                $request_token_endpoint = 'https://www.google.com/accounts/OAuthGetRequestToken';
     117                $authorize_endpoint = 'https://www.google.com/accounts/OAuthAuthorizeToken';
    90118
     119                $test_consumer = new Blogger_OAuthConsumer('anonymous', 'anonymous', NULL); // anonymous is a google thing to allow non-registered apps to work
     120
     121                //prepare to get request token
     122                $sig_method = new Blogger_OAuthSignatureMethod_HMAC_SHA1();
     123                $parsed = parse_url($request_token_endpoint);
     124                $params = array('callback' => $base_url, 'scope'=>'http://www.blogger.com/feeds/', 'xoauth_displayname'=>'WordPress');
     125
     126                $req_req = Blogger_OAuthRequest::from_consumer_and_token($test_consumer, NULL, "GET", $request_token_endpoint, $params);
     127                $req_req->sign_request($sig_method, $test_consumer, NULL);
     128
     129                // go get the request tokens from Google
     130                $req_token = wp_remote_retrieve_body(wp_remote_get($req_req->to_url(), array('sslverify'=>false) ) );
     131
     132                // parse the tokens
     133                parse_str ($req_token,$tokens);
     134
     135                $oauth_token = $tokens['oauth_token'];
     136                $oauth_token_secret = $tokens['oauth_token_secret'];
     137
     138                $callback_url = "$base_url/index.php?import=blogger&noheader=true&token=$oauth_token&token_secret=$oauth_token_secret";
     139
     140                return array('url'=>$authorize_endpoint, 'oauth_token'=>$oauth_token, 'oauth_callback'=>$callback_url );       
     141        }
     142
    91143        function uh_oh($title, $message, $info) {
    92144                echo "<div class='wrap'>";
    93145                screen_icon();
     
    95147        }
    96148
    97149        function auth() {
    98                 // We have a single-use token that must be upgraded to a session token.
    99                 $token = urldecode( preg_replace( '/[^%-_0-9a-zA-Z]/', '', $_GET['token'] ) );
    100                 $headers = array(
    101                         "GET /accounts/AuthSubSessionToken HTTP/1.0",
    102                         "Authorization: AuthSub token=\"$token\""
    103                 );
    104                 $request = join( "\r\n", $headers ) . "\r\n\r\n";
    105                 $sock = $this->_get_auth_sock( );
    106                 if ( ! $sock ) return false;
    107                 $response = $this->_txrx( $sock, $request );
    108                 preg_match( '/token=([%-_0-9a-z]+)/i', $response, $matches );
    109                 if ( empty( $matches[1] ) ) {
    110                         $this->uh_oh(
    111                                 __( 'Authorization failed' , 'blogger-importer'),
    112                                 __( 'Something went wrong. If the problem persists, send this info to support:' , 'blogger-importer'),
    113                                 htmlspecialchars($response)
    114                         );
    115                         return false;
    116                 }
    117                 $this->token = urldecode( $matches[1] );
    118 
     150                // we have a authorized request token now, so upgrade it to an access token             
     151                $token = $_GET['token'];
     152                $token_secret = $_GET['token_secret'];
     153               
     154                $oauth_access_token_endpoint  = 'https://www.google.com/accounts/OAuthGetAccessToken';
     155               
     156                // auth the token
     157                $test_consumer = new Blogger_OAuthConsumer('anonymous', 'anonymous', NULL);
     158                $auth_token = new Blogger_OAuthConsumer($token, $token_secret);
     159                $access_token_req = new Blogger_OAuthRequest("GET", $oauth_access_token_endpoint);
     160                $access_token_req = $access_token_req->from_consumer_and_token($test_consumer, $auth_token, "GET", $oauth_access_token_endpoint);
     161               
     162                $access_token_req->sign_request(new Blogger_OAuthSignatureMethod_HMAC_SHA1(),$test_consumer, $auth_token);
     163               
     164                $after_access_request = wp_remote_retrieve_body(wp_remote_get($access_token_req->to_url(), array('sslverify'=>false) ) );
     165               
     166                parse_str($after_access_request,$access_tokens);
     167               
     168                $this->token = $access_tokens['oauth_token'];
     169                $this->token_secret = $access_tokens['oauth_token_secret'];
     170       
    119171                wp_redirect( remove_query_arg( array( 'token', 'noheader' ) ) );
    120172        }
     173       
     174        // get a URL using the oauth token for authentication (returns false on failure)
     175        function oauth_get($url, $params=NULL) {
     176                $test_consumer = new Blogger_OAuthConsumer('anonymous', 'anonymous', NULL);
     177                $goog = new Blogger_OAuthConsumer($this->token, $this->token_secret, NULL);
     178                $request = new Blogger_OAuthRequest("GET", $url, $params);
    121179
    122         function get_token_info() {
    123                 $headers = array(
    124                         "GET /accounts/AuthSubTokenInfo  HTTP/1.0",
    125                         "Authorization: AuthSub token=\"$this->token\""
    126                 );
    127                 $request = join( "\r\n", $headers ) . "\r\n\r\n";
    128                 $sock = $this->_get_auth_sock( );
    129                 if ( ! $sock ) return;
    130                 $response = $this->_txrx( $sock, $request );
    131                 return $this->parse_response($response);
    132         }
     180        //Ref: Not importing properly http://core.trac.wordpress.org/ticket/19096 
     181                $blog_req = $request->from_consumer_and_token($test_consumer, $goog, 'GET', $url,$params);
    133182
    134         function token_is_valid() {
    135                 $info = $this->get_token_info();
     183                $blog_req->sign_request(new Blogger_OAuthSignatureMethod_HMAC_SHA1(),$test_consumer,$goog);
    136184
    137                 if ( $info['code'] == 200 )
    138                         return true;
    139 
    140                 return false;
     185                $data = wp_remote_get($blog_req->to_url(), array('sslverify'=>false) );
     186               
     187                if ( wp_remote_retrieve_response_code( $data ) == 200 ) {
     188                        $response = wp_remote_retrieve_body( $data );
     189                } else {
     190                        $response = false;
     191                }
     192               
     193                return $response;
    141194        }
    142 
     195       
    143196        function show_blogs($iter = 0) {
    144197                if ( empty($this->blogs) ) {
    145                         $headers = array(
    146                                 "GET /feeds/default/blogs HTTP/1.0",
    147                                 "Host: www.blogger.com",
    148                                 "Authorization: AuthSub token=\"$this->token\""
    149                         );
    150                         $request = join( "\r\n", $headers ) . "\r\n\r\n";
    151                         $sock = $this->_get_blogger_sock( );
    152                         if ( ! $sock ) return;
    153                         $response = $this->_txrx( $sock, $request );
     198                        $xml = $this->oauth_get('https://www.blogger.com/feeds/default/blogs');
    154199
    155                         // Quick and dirty XML mining.
    156                         list( $headers, $xml ) = explode( "\r\n\r\n", $response );
    157                         $p = xml_parser_create();
    158                         xml_parse_into_struct($p, $xml, $vals, $index);
    159                         xml_parser_free($p);
    160 
    161                         $this->title = $vals[$index['TITLE'][0]]['value'];
    162 
    163200                        // Give it a few retries... this step often flakes out the first time.
    164                         if ( empty( $index['ENTRY'] ) ) {
     201                        if ( empty( $xml ) ) {
    165202                                if ( $iter < 3 ) {
    166203                                        return $this->show_blogs($iter + 1);
    167204                                } else {
     
    173210                                        return false;
    174211                                }
    175212                        }
    176 
    177                         foreach ( $index['ENTRY'] as $i ) {
    178                                 $blog = array();
    179                                 while ( ( $tag = $vals[$i] ) && ! ( $tag['tag'] == 'ENTRY' && $tag['type'] == 'close' ) ) {
    180                                         if ( $tag['tag'] == 'TITLE' ) {
    181                                                 $blog['title'] = $tag['value'];
    182                                         } elseif ( $tag['tag'] == 'SUMMARY' ) {
    183                                                 $blog['summary'] = $tag['value'];
    184                                         } elseif ( $tag['tag'] == 'LINK' ) {
    185                                                 if ( $tag['attributes']['REL'] == 'alternate' && $tag['attributes']['TYPE'] == 'text/html' ) {
    186                                                         $parts = parse_url( $tag['attributes']['HREF'] );
    187                                                         $blog['host'] = $parts['host'];
    188                                                 } elseif ( $tag['attributes']['REL'] == 'edit' ) {
    189                                                         $blog['gateway'] = $tag['attributes']['HREF'];
    190                                                 } elseif ( $tag['attributes']['REL'] == 'http://schemas.google.com/g/2005#post' ) {
    191                                                         $parts = parse_url( $tag['attributes']['HREF'] );
    192                                                         $blog['posts_host'] = $parts['host'];
    193                                                         $blog['posts_path'] = $parts['path'];
    194                                                 }
    195                                         }
    196                                         ++$i;
    197                                 }
     213                       
     214                        $feed = new SimplePie();
     215                        $feed->set_raw_data($xml);
     216                        $feed->init();
     217                       
     218                        foreach ($feed->get_items() as $item) {
     219                $blog = array(); //reset
     220                                $blog['title'] = $item->get_title();
     221                                $blog['summary'] = $item->get_description();
     222               
     223                //ID is of the form tag:blogger.com,1999:blog-417730729915399755
     224                //We need that number from the end
     225                $rawid = explode('-',$item->get_id());
     226                $blog['id'] = $rawid[count($rawid)-1];
     227                       
     228                                $parts = parse_url( $item->get_link( 0, 'alternate' ) );
     229                                $blog['host'] = $parts['host'];
     230                                $blog['gateway'] = $item->get_link( 0, 'edit' );
     231                                $blog['posts_url'] = $item->get_link( 0, 'http://schemas.google.com/g/2005#post' );
     232               
     233                //AGC:20/4/2012 Developers guide suggests that the correct feed is located as follows
     234                //See https://developers.google.com/blogger/docs/1.0/developers_guide_php
     235                $blog['comments_url'] =  "http://www.blogger.com/feeds/{$blog['id']}/comments/default";
     236                               
    198237                                if ( ! empty ( $blog ) ) {
    199                                         $blog['total_posts'] = $this->get_total_results('posts', $blog['host']);
    200                                         $blog['total_comments'] = $this->get_total_results('comments', $blog['host']);
     238                                        $blog['total_posts'] = $this->get_total_results( $blog['posts_url'] );
     239                    $blog['total_comments'] = $this->get_total_results($blog['comments_url']);
     240                   
    201241                                        $blog['mode'] = 'init';
    202242                                        $this->blogs[] = $blog;
    203243                                }
     244
    204245                        }
    205 
     246                       
    206247                        if ( empty( $this->blogs ) ) {
    207248                                $this->uh_oh(
    208249                                        __('No blogs found', 'blogger-importer'),
     
    210251                                        ''
    211252                                );
    212253                                return false;
    213                         }
     254                        }                       
    214255                }
     256       
     257//Should probably be using WP_LIST_TABLE here rather than manually rendering a table in html
     258//http://wpengineer.com/2426/wp_list_table-a-step-by-step-guide/       
    215259//echo '<pre>'.print_r($this,1).'</pre>';
    216260                $start    = esc_js( __('Import', 'blogger-importer') );
    217261                $continue = esc_js( __('Continue', 'blogger-importer') );
     
    230274                $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.', 'blogger-importer');
    231275
    232276                $interval = STATUS_INTERVAL * 1000;
     277        $init = '';
     278        $rows = '';
    233279
    234280                foreach ( $this->blogs as $i => $blog ) {
    235281                        if ( $blog['mode'] == 'init' )
     
    242288                        $blogtitle = esc_js( $blog['title'] );
    243289                        $pdone = isset($blog['posts_done']) ? (int) $blog['posts_done'] : 0;
    244290                        $cdone = isset($blog['comments_done']) ? (int) $blog['comments_done'] : 0;
    245                         $init .= "blogs[$i]=new blog($i,'$blogtitle','{$blog['mode']}'," . $this->get_js_status($i) . ');';
     291                        $init .= "blogs[$i]=new blog($i,'$blogtitle','{$blog['mode']}','" . $this->get_js_status($i) . '\');';
    246292                        $pstat = "<div class='ind' id='pind$i'>&nbsp;</div><div id='pstat$i' class='stat'>$pdone/{$blog['total_posts']}</div>";
    247293                        $cstat = "<div class='ind' id='cind$i'>&nbsp;</div><div id='cstat$i' class='stat'>$cdone/{$blog['total_comments']}</div>";
    248294                        $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";
     
    258304                                this.blog   = i;
    259305                                this.mode   = mode;
    260306                                this.title  = title;
    261                                 this.status = status;
     307                eval('this.status='+status);
    262308                                this.button = document.getElementById('submit'+this.blog);
    263309                        };
    264310                        blog.prototype = {
     
    311357                                },
    312358                                update: function() {
    313359                                        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');
     360                    jQuery('#pstat'+this.blog).attr('title', 'Posts skipped '+this.status.p3);
    314361                                        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');
     362                    jQuery('#cstat'+this.blog).attr('title', 'Comments skipped '+this.status.c3);
     363
    315364                                },
    316365                                stop: function() {
    317366                                        this.cont = false;
     
    372421        function have_time() {
    373422                global $importer_started;
    374423                if ( time() - $importer_started > MAX_EXECUTION_TIME )
    375                         die('continue');
     424                        self::ajax_die('continue');
    376425                return true;
    377426        }
    378 
    379         function get_total_results($type, $host) {
    380                 $headers = array(
    381                         "GET /feeds/$type/default?max-results=1&start-index=2 HTTP/1.0",
    382                         "Host: $host",
    383                         "Authorization: AuthSub token=\"$this->token\""
    384                 );
    385                 $request = join( "\r\n", $headers ) . "\r\n\r\n";
    386                 $sock = $this->_get_blogger_sock( $host );
    387                 if ( ! $sock ) return;
    388                 $response = $this->_txrx( $sock, $request );
    389                 $response = $this->parse_response( $response );
    390                 $parser = xml_parser_create();
    391                 xml_parse_into_struct($parser, $response['body'], $struct, $index);
    392                 xml_parser_free($parser);
    393                 $total_results = $struct[$index['OPENSEARCH:TOTALRESULTS'][0]]['value'];
    394                 return (int) $total_results;
     427       
     428        function get_total_results($url) {             
     429                $response = $this->oauth_get( $url, array('max-results'=>1, 'start-index'=>2) );
     430       
     431        $feed = new SimplePie();
     432        $feed->set_raw_data($response);
     433        $feed->init();
     434        $results = $feed->get_channel_tags('http://a9.com/-/spec/opensearchrss/1.0/', 'totalResults');
     435       
     436        $total_results = $results[0]['data'];
     437        unset($feed);
     438        return (int) $total_results;
    395439        }
    396440
    397441        function import_blog($blogID) {
    398                 global $importing_blog;
     442                global $importing_blog;
    399443                $importing_blog = $blogID;
    400444
    401445                if ( isset($_GET['authors']) )
    402446                        return print($this->get_author_form());
    403447
    404                 header('Content-Type: text/plain');
    405 
    406448                if ( isset($_GET['status']) )
    407                         die($this->get_js_status());
     449                        self::ajax_die($this->get_js_status());
    408450
    409451                if ( isset($_GET['saveauthors']) )
    410                         die($this->save_authors());
     452                        self::ajax_die($this->save_authors());
    411453
    412                 $blog = $this->blogs[$blogID];
    413                 $total_results = $this->get_total_results('posts', $blog['host']);
    414                 $this->blogs[$importing_blog]['total_posts'] = $total_results;
    415 
    416                 $start_index = $total_results - MAX_RESULTS + 1;
    417 
     454        //Simpler counting for posts as we load them forwards
    418455                if ( isset( $this->blogs[$importing_blog]['posts_start_index'] ) )
    419456                        $start_index = (int) $this->blogs[$importing_blog]['posts_start_index'];
    420                 elseif ( $total_results > MAX_RESULTS )
    421                         $start_index = $total_results - MAX_RESULTS + 1;
    422457                else
    423458                        $start_index = 1;
    424459
     
    426461                if ( $start_index > 0 ) {
    427462                        // Grab all the posts
    428463                        $this->blogs[$importing_blog]['mode'] = 'posts';
    429                         $query = "start-index=$start_index&max-results=" . MAX_RESULTS;
    430464                        do {
     465                         
    431466                                $index = $struct = $entries = array();
    432                                 $headers = array(
    433                                         "GET {$blog['posts_path']}?$query HTTP/1.0",
    434                                         "Host: {$blog['posts_host']}",
    435                                         "Authorization: AuthSub token=\"$this->token\""
    436                                 );
    437                                 $request = join( "\r\n", $headers ) . "\r\n\r\n";
    438                                 $sock = $this->_get_blogger_sock( $blog['posts_host'] );
    439                                 if ( ! $sock ) return; // TODO: Error handling
    440                                 $response = $this->_txrx( $sock, $request );
    441467
    442                                 $response = $this->parse_response( $response );
     468                                $url = $this->blogs[$importing_blog]['posts_url'];
    443469
    444                                 // Extract the entries and send for insertion
    445                                 preg_match_all( '/<entry[^>]*>.*?<\/entry>/s', $response['body'], $matches );
    446                                 if ( count( $matches[0] ) ) {
    447                                         $entries = array_reverse($matches[0]);
    448                                         foreach ( $entries as $entry ) {
    449                                                 $entry = "<feed>$entry</feed>";
    450                                                 $AtomParser = new AtomParser();
    451                                                 $AtomParser->parse( $entry );
    452                                                 $result = $this->import_post($AtomParser->entry);
    453                                                 if ( is_wp_error( $result ) )
    454                                                         return $result;
    455                                                 unset($AtomParser);
     470                                $response = $this->oauth_get( $url, array('max-results'=>MAX_RESULTS, 'start-index'=>$start_index) );
     471                               
     472                                if ($response == false) break;
     473                                                               
     474                                // parse the feed
     475                                $feed = new SimplePie();
     476                $feed->set_item_class('WP_SimplePie_Blog_Item');
     477                $feed->set_sanitize_class('Blogger_Importer_Sanitize');
     478                $feed->set_raw_data($response);
     479                                $feed->init();
     480               
     481                                foreach ( $feed->get_items() as $item ) {
     482                                   
     483                                        $blogentry = new BloggerEntry();
     484                                       
     485                                        $blogentry->id = $item->get_id();
     486                                        $blogentry->published = $item->get_published();
     487                                        $blogentry->updated = $item->get_updated();
     488                    $blogentry->isDraft = $item->get_draft_status($item);
     489                                        $blogentry->title = $item->get_title();
     490                                        $blogentry->content = $item->get_content();
     491                                        $blogentry->author = $item->get_author()->get_name();
     492                                       
     493                                        $linktypes = array('replies','edit','self','alternate');
     494                                        foreach ($linktypes as $type) {
     495                                                $links = $item->get_links($type);
     496                       
     497                        if (!is_null($links)) {
     498                                                foreach ($links as $link) {
     499                                                        $blogentry->links[] = array( 'rel' => $type, 'href' => $link );
     500                                                }
     501                        }
    456502                                        }
    457                                 } else break;
    458 
    459                                 // Get the 'previous' query string which we'll use on the next iteration
    460                                 $query = '';
    461                                 $links = preg_match_all('/<link([^>]*)>/', $response['body'], $matches);
    462                                 if ( count( $matches[1] ) )
    463                                         foreach ( $matches[1] as $match )
    464                                                 if ( preg_match('/rel=.previous./', $match) )
    465                                                         $query = @html_entity_decode( preg_replace('/^.*href=[\'"].*\?(.+)[\'"].*$/', '$1', $match), ENT_COMPAT, get_option('blog_charset') );
    466 
    467                                 if ( $query ) {
    468                                         parse_str($query, $q);
    469                                         $this->blogs[$importing_blog]['posts_start_index'] = (int) $q['start-index'];
    470                                 } else
    471                                         $this->blogs[$importing_blog]['posts_start_index'] = 0;
     503                                       
     504                                        $cats = $item->get_categories();
     505                   
     506                    if (!is_null($cats)) {
     507                                        foreach ($cats as $cat) {
     508                                                $blogentry->categories[] = $cat->term;
     509                                        }
     510                    }
     511                                       
     512                                        $result = $this->import_post($blogentry);
     513                   
     514                    //Ref: Not importing properly http://core.trac.wordpress.org/ticket/19096 
     515                    //Simplified this section to count what is loaded rather than parsing the results again
     516                    $start_index++;
     517                                }
     518                               
     519                $this->blogs[$importing_blog]['posts_start_index'] = $start_index;
     520               
    472521                                $this->save_vars();
    473                         } while ( !empty( $query ) && $this->have_time() );
     522               
     523                        } while ( $this->blogs[$importing_blog]['total_posts'] >$start_index && $this->have_time() ); //have time function will "die" if it's out of time
    474524                }
    475525
    476                 $total_results = $this->get_total_results( 'comments', $blog['host'] );
    477                 $this->blogs[$importing_blog]['total_comments'] = $total_results;
    478 
    479                 if ( isset( $this->blogs[$importing_blog]['comments_start_index'] ) )
     526       
     527        if ( isset( $this->blogs[$importing_blog]['comments_start_index'] ) )
    480528                        $start_index = (int) $this->blogs[$importing_blog]['comments_start_index'];
    481                 elseif ( $total_results > MAX_RESULTS )
    482                         $start_index = $total_results - MAX_RESULTS + 1;
    483                 else
    484                         $start_index = 1;
     529        else
     530            $start_index = 1;
     531       
     532                if ( $start_index > 0 && $this->blogs[$importing_blog]['total_comments'] > 0) {
    485533
    486                 if ( $start_index > 0 ) {
    487                         // Grab all the comments
    488534                        $this->blogs[$importing_blog]['mode'] = 'comments';
    489                         $query = "start-index=$start_index&max-results=" . MAX_RESULTS;
    490535                        do {
    491536                                $index = $struct = $entries = array();
    492                                 $headers = array(
    493                                         "GET /feeds/comments/default?$query HTTP/1.0",
    494                                         "Host: {$blog['host']}",
    495                                         "Authorization: AuthSub token=\"$this->token\""
    496                                 );
    497                                 $request = join( "\r\n", $headers ) . "\r\n\r\n";
    498                                 $sock = $this->_get_blogger_sock( $blog['host'] );
    499                                 if ( ! $sock ) return; // TODO: Error handling
    500                                 $response = $this->_txrx( $sock, $request );
    501537
    502                                 $response = $this->parse_response( $response );
    503 
    504                                 // Extract the comments and send for insertion
    505                                 preg_match_all( '/<entry[^>]*>.*?<\/entry>/s', $response['body'], $matches );
    506                                 if ( count( $matches[0] ) ) {
    507                                         $entries = array_reverse( $matches[0] );
    508                                         foreach ( $entries as $entry ) {
    509                                                 $entry = "<feed>$entry</feed>";
    510                                                 $AtomParser = new AtomParser();
    511                                                 $AtomParser->parse( $entry );
    512                                                 $this->import_comment($AtomParser->entry);
    513                                                 unset($AtomParser);
    514                                         }
    515                                 }
    516 
    517                                 // Get the 'previous' query string which we'll use on the next iteration
    518                                 $query = '';
    519                                 $links = preg_match_all('/<link([^>]*)>/', $response['body'], $matches);
    520                                 if ( count( $matches[1] ) )
    521                                         foreach ( $matches[1] as $match )
    522                                                 if ( preg_match('/rel=.previous./', $match) )
    523                                                         $query = @html_entity_decode( preg_replace('/^.*href=[\'"].*\?(.+)[\'"].*$/', '$1', $match), ENT_COMPAT, get_option('blog_charset') );
    524 
    525                                 parse_str($query, $q);
    526 
    527                                 $this->blogs[$importing_blog]['comments_start_index'] = (int) $q['start-index'];
     538                //So we can link up the comments as we go we need to load them in reverse order
     539                //Reverse the start index as the GData Blogger feed can't be sorted
     540                $batch = ((floor(($this->blogs[$importing_blog]['total_comments'] - $start_index)/MAX_RESULTS)*MAX_RESULTS)+1);
     541                                             
     542                                $response = $this->oauth_get( $this->blogs[$importing_blog]['comments_url'], array('max-results'=>MAX_RESULTS, 'start-index'=>$batch) );
     543                               
     544                                // parse the feed
     545                                $feed = new SimplePie();
     546                $feed->set_item_class('WP_SimplePie_Blog_Item');
     547                // Use the standard "stricter" sanitize class for comments
     548                                $feed->set_raw_data($response);
     549                                $feed->init();
     550               
     551                //Reverse the batch so we load the oldest comments first and hence can link up nested comments
     552                $comments = array_reverse($feed->get_items());
     553               
     554                if (!is_null($comments)) {
     555                                foreach ($comments  as $item ) {
     556 
     557                                        $blogentry = new BloggerEntry();
     558                        $blogentry->id = $item->get_id();
     559                                        $blogentry->updated = $item->get_updated();
     560                                        $blogentry->content = $item->get_content();
     561                                        $blogentry->author = $item->get_author()->get_name();
     562                                        $blogentry->authoruri = $item->get_author()->get_link();
     563                                        $blogentry->authoremail = $item->get_author()->get_email();
     564   
     565                                        $temp = $item->get_item_tags('http://purl.org/syndication/thread/1.0','in-reply-to');
     566   
     567                                        foreach ($temp as $t) {
     568                                                if ( isset( $t['attribs']['']['source'] ) ) {
     569                                                        $blogentry->source = $t['attribs']['']['source'];
     570                                                }
     571                                        }
     572                       
     573                        //Get the links
     574                        $linktypes = array('edit','self','alternate','related');
     575                                        foreach ($linktypes as $type) {
     576                                                $links = $item->get_links($type);
     577                            if (!is_null($links)) {
     578                                                        foreach ($links as $link) {
     579                                                                $blogentry->links[] = array( 'rel' => $type, 'href' => $link );
     580                                                        }
     581                            }
     582                                        }
     583                                       
     584                                        $this->import_comment($blogentry);
     585                        $start_index++;
     586                                }
     587                }
     588                               
     589                                $this->blogs[$importing_blog]['comments_start_index'] = $start_index;
    528590                                $this->save_vars();
    529                         } while ( !empty( $query ) && $this->have_time() );
     591                        } while ( $this->blogs[$importing_blog]['total_comments'] > $start_index && $this->have_time() );
    530592                }
     593
    531594                $this->blogs[$importing_blog]['mode'] = 'authors';
    532595                $this->save_vars();
     596
    533597                if ( !$this->blogs[$importing_blog]['posts_done'] && !$this->blogs[$importing_blog]['comments_done'] )
    534                         die('nothing');
     598                        self::ajax_die('nothing');
     599
    535600                do_action('import_done', 'blogger');
    536                 die('done');
     601                self::ajax_die('done');
    537602        }
    538603
    539         function convert_date( $date ) {
    540             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);
    541             $offset = iso8601_timezone_to_offset( $date_bits[7] );
    542                 $timestamp = gmmktime($date_bits[4], $date_bits[5], $date_bits[6], $date_bits[2], $date_bits[3], $date_bits[1]);
    543                 $timestamp -= $offset; // Convert from Blogger local time to GMT
    544                 $timestamp += get_option('gmt_offset') * 3600; // Convert from GMT to WP local time
    545                 return gmdate('Y-m-d H:i:s', $timestamp);
    546         }
    547 
    548604        function no_apos( $string ) {
    549605                return str_replace( '&apos;', "'", $string);
    550606        }
     
    559615
    560616        function import_post( $entry ) {
    561617                global $importing_blog;
    562 
    563                 // The old permalink is all Blogger gives us to link comments to their posts.
    564                 if ( isset( $entry->draft ) )
    565                         $rel = 'self';
    566                 else
    567                         $rel = 'alternate';
    568                 foreach ( $entry->links as $link ) {
    569                         if ( $link['rel'] == $rel ) {
     618       
     619                foreach ( $entry->links as $link ) {                   
     620                        // save the self link as meta
     621                        if ( $link['rel'] == 'self' ) {
     622                                $postself = $link['href'];
    570623                                $parts = parse_url( $link['href'] );
    571624                                $entry->old_permalink = $parts['path'];
    572                                 break;
    573625                        }
     626
     627            // get the old URI for the page when available
     628            if ( $link['rel'] == 'alternate' ) {
     629                                $parts = parse_url( $link['href'] );
     630                                $entry->bookmark = $parts['path'];
     631                        }
     632           
     633                        // save the replies feed link as meta (ignore the comment form one)
     634                        if ( $link['rel'] == 'replies' && false === strpos($link['href'], '#comment-form') ) {
     635                                $postreplies = $link['href'];
     636                        }
    574637                }
    575 
    576                 $post_date    = $this->convert_date( $entry->published );
     638               
     639        //Check if we are double cleaning here? Does the Simplepie already do all this?
     640                $post_date    =  $entry->published;
    577641                $post_content = trim( addslashes( $this->no_apos( @html_entity_decode( $entry->content, ENT_COMPAT, get_option('blog_charset') ) ) ) );
    578642                $post_title   = trim( addslashes( $this->no_apos( $this->min_whitespace( $entry->title ) ) ) );
    579                 $post_status  = isset( $entry->draft ) ? 'draft' : 'publish';
     643       
     644                $post_status  = $entry->isDraft ? 'draft' : 'publish';
    580645
    581                 // Clean up content
    582                 $post_content = preg_replace_callback('|<(/?[A-Z]+)|', array( &$this, '_normalize_tag' ), $post_content);
    583                 $post_content = str_replace('<br>', '<br />', $post_content);
    584                 $post_content = str_replace('<hr>', '<hr />', $post_content);
     646                // N.B. Clean up of $post_content is now part of the sanitize class
    585647
    586648                // Checks for duplicates
    587649                if ( isset( $this->blogs[$importing_blog]['posts'][$entry->old_permalink] ) ) {
    588                         ++$this->blogs[$importing_blog]['posts_skipped'];
     650                        $this->blogs[$importing_blog]['posts_skipped']++;
    589651                } elseif ( $post_id = post_exists( $post_title, $post_content, $post_date ) ) {
    590652                        $this->blogs[$importing_blog]['posts'][$entry->old_permalink] = $post_id;
    591                         ++$this->blogs[$importing_blog]['posts_skipped'];
     653                        $this->blogs[$importing_blog]['posts_skipped']++;
    592654                } else {
    593655                        $post = compact('post_date', 'post_content', 'post_title', 'post_status');
    594656
     
    602664
    603665                        add_post_meta( $post_id, 'blogger_blog', $this->blogs[$importing_blog]['host'], true );
    604666                        add_post_meta( $post_id, 'blogger_author', $author, true );
    605                         add_post_meta( $post_id, 'blogger_permalink', $entry->old_permalink, true );
     667           
     668            //Use the page id if available or the blogger internal id if it's a draft
     669                if ( $entry->isDraft | !isset($entry->bookmark) )
     670                             add_post_meta( $post_id, 'blogger_permalink', $entry->old_permalink, true );
     671            else
     672                             add_post_meta( $post_id, 'blogger_permalink', $entry->bookmark, true );
     673                 
     674                        add_post_meta( $post_id, '_blogger_self', $postself, true );
     675            //Undefined variable: postreplies
     676                        //add_post_meta( $post_id, '_blogger_replies', $postreplies, true );
    606677
    607678                        $this->blogs[$importing_blog]['posts'][$entry->old_permalink] = $post_id;
    608                         ++$this->blogs[$importing_blog]['posts_done'];
     679           
     680                        $this->blogs[$importing_blog]['posts_done']++;
    609681                }
    610682                $this->save_vars();
    611683                return;
     
    613685
    614686        function import_comment( $entry ) {
    615687                global $importing_blog;
    616 
     688               
     689                $parts = parse_url( $entry->source );
     690                $entry->old_post_permalink = $parts['path'];          //Will be something like this '/feeds/417730729915399755/posts/default/8397846992898424746'
     691               
    617692                // Drop the #fragment and we have the comment's old post permalink.
    618693                foreach ( $entry->links as $link ) {
    619694                        if ( $link['rel'] == 'alternate' ) {
    620695                                $parts = parse_url( $link['href'] );
    621696                                $entry->old_permalink = $parts['fragment'];
    622                                 $entry->old_post_permalink = $parts['path'];
    623                                 break;
    624697                        }
     698            //Parent post for nested links
     699            if ( $link['rel'] == 'related' ) {
     700                $parts = parse_url( $link['href'] );
     701                                $entry->related = $parts['path'];
     702            }
     703            if ( $link['rel'] == 'self' ) {
     704                $parts = parse_url( $link['href'] );
     705                                $entry->self = $parts['path'];
     706            }           
    625707                }
     708       
     709        //Check for duplicated cleanup here
     710        $comment_post_ID = (int) $this->blogs[$importing_blog]['posts'][$entry->old_post_permalink];
     711                $comment_author  = addslashes( $this->no_apos( strip_tags( $entry->author ) ) );
     712                $comment_author_url = addslashes( $this->no_apos( strip_tags( $entry->authoruri ) ) );
     713                $comment_author_email = addslashes( $this->no_apos( strip_tags( $entry->authoremail ) ) );
     714                $comment_date    = $entry->updated;
    626715
    627                 $comment_post_ID = (int) $this->blogs[$importing_blog]['posts'][$entry->old_post_permalink];
    628                 preg_match('#<name>(.+?)</name>.*(?:\<uri>(.+?)</uri>)?#', $entry->author, $matches);
    629                 $comment_author  = addslashes( $this->no_apos( strip_tags( (string) $matches[1] ) ) );
    630                 $comment_author_url = addslashes( $this->no_apos( strip_tags( (string) $matches[2] ) ) );
    631                 $comment_date    = $this->convert_date( $entry->updated );
     716                // Clean up content
     717        // Again, check if the Simplepie is already handling all this stuff
    632718                $comment_content = addslashes( $this->no_apos( @html_entity_decode( $entry->content, ENT_COMPAT, get_option('blog_charset') ) ) );
    633 
    634                 // Clean up content
    635719                $comment_content = preg_replace_callback('|<(/?[A-Z]+)|', array( &$this, '_normalize_tag' ), $comment_content);
    636720                $comment_content = str_replace('<br>', '<br />', $comment_content);
    637721                $comment_content = str_replace('<hr>', '<hr />', $comment_content);
    638 
    639                 // Checks for duplicates
    640                 if (
    641                         isset( $this->blogs[$importing_blog]['comments'][$entry->old_permalink] ) ||
    642                         comment_exists( $comment_author, $comment_date )
    643                 ) {
    644                         ++$this->blogs[$importing_blog]['comments_skipped'];
    645                 } else {
    646                         $comment = compact('comment_post_ID', 'comment_author', 'comment_author_url', 'comment_date', 'comment_content');
    647 
    648                         $comment = wp_filter_comment($comment);
    649                         $comment_id = wp_insert_comment($comment);
    650 
    651                         $this->blogs[$importing_blog]['comments'][$entry->old_permalink] = $comment_id;
    652 
    653                         ++$this->blogs[$importing_blog]['comments_done'];
    654                 }
     722       
     723        // Nested comment?
     724        if (!is_null($entry->related)) {
     725            $comment_parent = $this->blogs[$importing_blog]['comments'][$entry->related];
     726        }
     727       
     728        // if the post does not exist then we need stop and not add the comment
     729        if ($comment_post_ID != 0) {
     730                // Checks for duplicates
     731                if (
     732                        isset( $this->blogs[$importing_blog][$entry->id] ) ||
     733                        $this->comment_exists($comment_post_ID, $comment_author, $comment_date )
     734                ) {               
     735                        $this->blogs[$importing_blog]['comments_skipped']++;
     736                } else {
     737                        $comment = compact('comment_post_ID', 'comment_author', 'comment_author_url','comment_author_email', 'comment_date', 'comment_content','comment_parent');
     738   
     739                        $comment = wp_filter_comment($comment);
     740                        $comment_id = wp_insert_comment($comment);
     741               
     742                        $this->blogs[$importing_blog]['comments'][$entry->id] = $comment_id;
     743                $this->blogs[$importing_blog]['comments'][$entry->self] = $comment_id; //For nested comments
     744   
     745                        $this->blogs[$importing_blog]['comments_done']++;
     746                }
     747        }
     748        else {
     749            $this->blogs[$importing_blog]['comments_skipped']++;
     750        }
    655751                $this->save_vars();
    656752        }
     753   
     754    function ajax_die($data){
     755        ob_clean(); //Discard any debug messages or other fluff already sent
     756        header('Content-Type: text/plain');
     757                die($data);
     758    }
    657759
     760
    658761        function get_js_status($blog = false) {
    659762                global $importing_blog;
    660763                if ( $blog === false )
    661764                        $blog = $this->blogs[$importing_blog];
    662765                else
    663766                        $blog = $this->blogs[$blog];
     767           
    664768                $p1 = isset( $blog['posts_done'] ) ? (int) $blog['posts_done'] : 0;
    665769                $p2 = isset( $blog['total_posts'] ) ? (int) $blog['total_posts'] : 0;
     770        $p3 = isset( $blog['posts_skipped'] ) ? (int) $blog['posts_skipped'] : 0;
    666771                $c1 = isset( $blog['comments_done'] ) ? (int) $blog['comments_done'] : 0;
    667772                $c2 = isset( $blog['total_comments'] ) ? (int) $blog['total_comments'] : 0;
    668                 return "{p1:$p1,p2:$p2,c1:$c1,c2:$c2}";
     773        $c3 = isset( $blog['comments_skipped'] ) ? (int) $blog['comments_skipped'] : 0;
     774        return "{p1:$p1,p2:$p2,p3:$p3,c1:$c1,c2:$c2,c3:$c3}";
    669775        }
    670776
    671777        function get_author_form($blog = false) {
     
    688794                $mapthis = __('Blogger username', 'blogger-importer');
    689795                $tothis = __('WordPress login', 'blogger-importer');
    690796                $submit = esc_js( __('Save Changes', 'blogger-importer') );
     797        $rows = '';
    691798
    692799                foreach ( $blog['authors'] as $i => $author )
    693800                        $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>";
     
    698805        function get_user_options($current) {
    699806                global $importer_users;
    700807                if ( ! isset( $importer_users ) )
    701                         $importer_users = (array) get_users_of_blog();
     808            $importer_users = (array) get_users();  //Function: get_users_of_blog() Deprecated in version 3.1. Use get_users() instead.
    702809
     810        $options = '';
     811
    703812                foreach ( $importer_users as $user ) {
    704                         $sel = ( $user->user_id == $current ) ? " selected='selected'" : '';
    705                         $options .= "<option value='$user->user_id'$sel>$user->display_name</option>";
     813                        $sel = ( $user->ID == $current ) ? " selected='selected'" : '';
     814                        $options .= "<option value='$user->ID'$sel>$user->display_name</option>";
    706815                }
    707816
    708817                return $options;
     
    710819
    711820        function save_authors() {
    712821                global $importing_blog, $wpdb;
     822        $blog =& $this->blogs[$importing_blog]; //Get a reference to blogs so we don't have to write it longhand
     823                       
    713824                $authors = (array) $_POST['authors'];
    714825
    715                 $host = $this->blogs[$importing_blog]['host'];
     826                $host = $blog['host'];
    716827
    717828                // Get an array of posts => authors
    718829                $post_ids = (array) $wpdb->get_col( $wpdb->prepare("SELECT post_id FROM $wpdb->postmeta WHERE meta_key = 'blogger_blog' AND meta_value = %s", $host) );
     
    725836                        $user_id = (int) $user_id;
    726837
    727838                        // Skip authors that haven't been changed
    728                         if ( $user_id == $this->blogs[$importing_blog]['authors'][$author][1] )
     839                        if ( $user_id == $blog['authors'][$author][1] )
    729840                                continue;
    730841
    731842                        // Get a list of the selected author's posts
    732                         $post_ids = (array) array_keys( $authors_posts, $this->blogs[$importing_blog]['authors'][$author][0] );
     843                        $post_ids = (array) array_keys( $authors_posts, $blog['authors'][$author][0] );
    733844                        $post_ids = join( ',', $post_ids);
    734845
    735846                        $wpdb->query( $wpdb->prepare("UPDATE $wpdb->posts SET post_author = %d WHERE id IN ($post_ids)", $user_id) );
    736                         $this->blogs[$importing_blog]['authors'][$author][1] = $user_id;
     847                        $blog['authors'][$author][1] = $user_id;
    737848                }
    738849                $this->save_vars();
    739850
    740851                wp_redirect('edit.php');
    741852        }
    742853
    743         function _get_auth_sock() {
    744                 // Connect to https://www.google.com
    745                 if ( !$sock = @ fsockopen('ssl://www.google.com', 443, $errno, $errstr) ) {
    746                         $this->uh_oh(
    747                                 __('Could not connect to https://www.google.com', 'blogger-importer'),
    748                                 __('There was a problem opening a secure connection to Google. This is what went wrong:', 'blogger-importer'),
    749                                 "$errstr ($errno)"
    750                         );
    751                         return false;
    752                 }
    753                 return $sock;
    754         }
    755 
    756         function _get_blogger_sock($host = 'www2.blogger.com') {
    757                 if ( !$sock = @ fsockopen($host, 80, $errno, $errstr) ) {
    758                         $this->uh_oh(
    759                                 sprintf( __('Could not connect to %s', 'blogger-importer'), $host ),
    760                                 __('There was a problem opening a connection to Blogger. This is what went wrong:', 'blogger-importer'),
    761                                 "$errstr ($errno)"
    762                         );
    763                         return false;
    764                 }
    765                 return $sock;
    766         }
    767 
    768         function _txrx( $sock, $request ) {
    769                 fwrite( $sock, $request );
    770                 while ( ! feof( $sock ) )
    771                         $response .= @ fread ( $sock, 8192 );
    772                 fclose( $sock );
    773                 return $response;
    774         }
    775 
    776         function revoke($token) {
    777                 $headers = array(
    778                         "GET /accounts/AuthSubRevokeToken HTTP/1.0",
    779                         "Authorization: AuthSub token=\"$token\""
    780                 );
    781                 $request = join( "\r\n", $headers ) . "\r\n\r\n";
    782                 $sock = $this->_get_auth_sock( );
    783                 if ( ! $sock ) return false;
    784                 $this->_txrx( $sock, $request );
    785         }
    786 
    787854        function restart() {
    788855                global $wpdb;
    789856                $options = get_option( 'blogger_importer' );
    790857
    791                 if ( isset( $options['token'] ) )
    792                         $this->revoke( $options['token'] );
    793 
    794858                delete_option('blogger_importer');
    795859                $wpdb->query("DELETE FROM $wpdb->postmeta WHERE meta_key = 'blogger_author'");
    796860                wp_redirect('?import=blogger');
    797861        }
    798862
    799         // Returns associative array of code, header, cookies, body. Based on code from php.net.
    800         function parse_response($this_response) {
    801                 // Split response into header and body sections
    802                 list($response_headers, $response_body) = explode("\r\n\r\n", $this_response, 2);
    803                 $response_header_lines = explode("\r\n", $response_headers);
    804 
    805                 // First line of headers is the HTTP response code
    806                 $http_response_line = array_shift($response_header_lines);
    807                 if (preg_match('@^HTTP/[0-9]\.[0-9] ([0-9]{3})@',$http_response_line, $matches)) { $response_code = $matches[1]; }
    808 
    809                 // put the rest of the headers in an array
    810                 $response_header_array = array();
    811                 foreach($response_header_lines as $header_line) {
    812                         list($header,$value) = explode(': ', $header_line, 2);
    813                         $response_header_array[$header] .= $value."\n";
    814                 }
    815 
    816                 $cookie_array = array();
    817                 $cookies = explode("\n", $response_header_array["Set-Cookie"]);
    818                 foreach($cookies as $this_cookie) { array_push($cookie_array, "Cookie: ".$this_cookie); }
    819 
    820                 return array("code" => $response_code, "header" => $response_header_array, "cookies" => $cookie_array, "body" => $response_body);
    821         }
    822 
    823863        // Step 9: Congratulate the user
    824864        function congrats() {
    825865                $blog = (int) $_GET['blog'];
     
    836876        function start() {
    837877                if ( isset($_POST['restart']) )
    838878                        $this->restart();
    839 
     879                       
    840880                $options = get_option('blogger_importer');
    841881
    842882                if ( is_array($options) )
     
    849889                        $result = $this->import_blog( $blog );
    850890                        if ( is_wp_error( $result ) )
    851891                                echo $result->get_error_message();
    852                 } elseif ( isset($_GET['token']) )
     892                } elseif ( isset($_GET['token']) && isset($_GET['token_secret']) )
    853893                        $this->auth();
    854                 elseif ( isset($this->token) && $this->token_is_valid() )
     894                elseif ( isset($this->token) && isset($this->token_secret) )
    855895                        $this->show_blogs();
    856896                else
    857897                        $this->greet();
     
    873913                return !empty($vars);
    874914        }
    875915
    876         function admin_head() {
    877 ?>
    878 <style type="text/css">
    879 td { text-align: center; line-height: 2em;}
    880 thead td { font-weight: bold; }
    881 .bar {
    882         width: 200px;
    883         text-align: left;
    884         line-height: 2em;
    885         padding: 0px;
    886 }
    887 .ind {
    888         position: absolute;
    889         background-color: #83B4D8;
    890         width: 1px;
    891         z-index: 9;
    892 }
    893 .stat {
    894         z-index: 10;
    895         position: relative;
    896         text-align: center;
    897 }
    898 </style>
    899 <?php
    900         }
     916    function comment_exists($post_id, $comment_author, $comment_date) {
     917    //Do we have 2 comments for the same author at the same time, on the same post?
     918    //returns comment id
     919        global $wpdb;
    901920
    902         function Blogger_Import() {
    903                 global $importer_started;
    904                 $importer_started = time();
    905                 if ( isset( $_GET['import'] ) && $_GET['import'] == 'blogger' ) {
    906                         wp_enqueue_script('jquery');
    907                         add_action('admin_head', array(&$this, 'admin_head'));
    908                 }
    909         }
    910 }
     921        $comment_author = stripslashes($comment_author);
     922        $comment_date = stripslashes($comment_date);
    911923
    912 $blogger_import = new Blogger_Import();
     924        return $wpdb->get_var( $wpdb->prepare("SELECT comment_ID FROM $wpdb->comments
     925                        WHERE comment_post_ID = %s and comment_author = %s AND comment_date = %s", $post_id, $comment_author, $comment_date) );
     926    }
    913927
    914 register_importer('blogger', __('Blogger', 'blogger-importer'), __('Import posts, comments, and users from a Blogger blog.', 'blogger-importer'), array ($blogger_import, 'start'));
     928}
    915929
    916 class AtomEntry {
     930class BloggerEntry {
    917931        var $links = array();
    918932        var $categories = array();
    919933}
    920934
    921 class AtomParser {
    922 
    923         var $ATOM_CONTENT_ELEMENTS = array('content','summary','title','subtitle','rights');
    924         var $ATOM_SIMPLE_ELEMENTS = array('id','updated','published','draft','author');
    925 
    926         var $depth = 0;
    927         var $indent = 2;
    928         var $in_content;
    929         var $ns_contexts = array();
    930         var $ns_decls = array();
    931         var $is_xhtml = false;
    932         var $skipped_div = false;
    933 
    934         var $entry;
    935 
    936         function AtomParser() {
    937                 $this->entry = new AtomEntry();
    938         }
    939 
    940         function _map_attrs_func( $k, $v ) {
    941                 return "$k=\"$v\"";
    942         }
    943 
    944         function _map_xmlns_func( $p, $n ) {
    945                 $xd = "xmlns";
    946                 if ( strlen( $n[0] ) > 0 )
    947                         $xd .= ":{$n[0]}";
    948 
    949                 return "{$xd}=\"{$n[1]}\"";
    950         }
    951 
    952         function parse($xml) {
    953 
    954                 global $app_logging;
    955                 array_unshift($this->ns_contexts, array());
    956 
    957                 $parser = xml_parser_create_ns();
    958                 xml_set_object($parser, $this);
    959                 xml_set_element_handler($parser, "start_element", "end_element");
    960                 xml_parser_set_option($parser,XML_OPTION_CASE_FOLDING,0);
    961                 xml_parser_set_option($parser,XML_OPTION_SKIP_WHITE,0);
    962                 xml_set_character_data_handler($parser, "cdata");
    963                 xml_set_default_handler($parser, "_default");
    964                 xml_set_start_namespace_decl_handler($parser, "start_ns");
    965                 xml_set_end_namespace_decl_handler($parser, "end_ns");
    966 
    967                 $contents = "";
    968 
    969                 xml_parse($parser, $xml);
    970 
    971                 xml_parser_free($parser);
    972 
    973                 return true;
    974         }
    975 
    976         function start_element($parser, $name, $attrs) {
    977 
    978                 $tag = array_pop(split(":", $name));
    979 
    980                 array_unshift($this->ns_contexts, $this->ns_decls);
    981 
    982                 $this->depth++;
    983 
    984                 if (!empty($this->in_content)) {
    985                         $attrs_prefix = array();
    986 
    987                         // resolve prefixes for attributes
    988                         foreach($attrs as $key => $value) {
    989                                 $attrs_prefix[$this->ns_to_prefix($key)] = $this->xml_escape($value);
    990                         }
    991                         $attrs_str = join(' ', array_map( array( &$this, '_map_attrs_func' ), array_keys($attrs_prefix), array_values($attrs_prefix)));
    992                         if (strlen($attrs_str) > 0) {
    993                                 $attrs_str = " " . $attrs_str;
    994                         }
    995 
    996                         $xmlns_str = join(' ', array_map( array( &$this, '_map_xmlns_func' ), array_keys($this->ns_contexts[0]), array_values($this->ns_contexts[0])));
    997                         if (strlen($xmlns_str) > 0) {
    998                                 $xmlns_str = " " . $xmlns_str;
    999                         }
    1000 
    1001                         // handle self-closing tags (case: a new child found right-away, no text node)
    1002                         if (count($this->in_content) == 2) {
    1003                                 array_push($this->in_content, ">");
    1004                         }
    1005 
    1006                         array_push($this->in_content, "<". $this->ns_to_prefix($name) ."{$xmlns_str}{$attrs_str}");
    1007                 } else if (in_array($tag, $this->ATOM_CONTENT_ELEMENTS) || in_array($tag, $this->ATOM_SIMPLE_ELEMENTS)) {
    1008                         $this->in_content = array();
    1009                         $this->is_xhtml = $attrs['type'] == 'xhtml';
    1010                         array_push($this->in_content, array($tag,$this->depth));
    1011                 } else if ($tag == 'link') {
    1012                         array_push($this->entry->links, $attrs);
    1013                 } else if ($tag == 'category') {
    1014                         array_push($this->entry->categories, $attrs['term']);
    1015                 }
    1016 
    1017                 $this->ns_decls = array();
    1018         }
    1019 
    1020         function end_element($parser, $name) {
    1021 
    1022                 $tag = array_pop(split(":", $name));
    1023 
    1024                 if (!empty($this->in_content)) {
    1025                         if ($this->in_content[0][0] == $tag &&
    1026                         $this->in_content[0][1] == $this->depth) {
    1027                                 array_shift($this->in_content);
    1028                                 if ($this->is_xhtml) {
    1029                                         $this->in_content = array_slice($this->in_content, 2, count($this->in_content)-3);
    1030                                 }
    1031                                 $this->entry->$tag = join('',$this->in_content);
    1032                                 $this->in_content = array();
    1033                         } else {
    1034                                 $endtag = $this->ns_to_prefix($name);
    1035                                 if (strpos($this->in_content[count($this->in_content)-1], '<' . $endtag) !== false) {
    1036                                         array_push($this->in_content, "/>");
    1037                                 } else {
    1038                                         array_push($this->in_content, "</$endtag>");
    1039                                 }
    1040                         }
    1041                 }
    1042 
    1043                 array_shift($this->ns_contexts);
    1044 
    1045                 #print str_repeat(" ", $this->depth * $this->indent) . "end_element('$name')" ."\n";
    1046 
    1047                 $this->depth--;
    1048         }
    1049 
    1050         function start_ns($parser, $prefix, $uri) {
    1051                 #print str_repeat(" ", $this->depth * $this->indent) . "starting: " . $prefix . ":" . $uri . "\n";
    1052                 array_push($this->ns_decls, array($prefix,$uri));
    1053         }
    1054 
    1055         function end_ns($parser, $prefix) {
    1056                 #print str_repeat(" ", $this->depth * $this->indent) . "ending: #" . $prefix . "#\n";
    1057         }
    1058 
    1059         function cdata($parser, $data) {
    1060                 #print str_repeat(" ", $this->depth * $this->indent) . "data: #" . $data . "#\n";
    1061                 if (!empty($this->in_content)) {
    1062                         // handle self-closing tags (case: text node found, need to close element started)
    1063                         if (strpos($this->in_content[count($this->in_content)-1], '<') !== false) {
    1064                                 array_push($this->in_content, ">");
    1065                         }
    1066                         array_push($this->in_content, $this->xml_escape($data));
    1067                 }
    1068         }
    1069 
    1070         function _default($parser, $data) {
    1071                 # when does this gets called?
    1072         }
    1073 
    1074 
    1075         function ns_to_prefix($qname) {
    1076                 $components = split(":", $qname);
    1077                 $name = array_pop($components);
    1078 
    1079                 if (!empty($components)) {
    1080                         $ns = join(":",$components);
    1081                         foreach ($this->ns_contexts as $context) {
    1082                                 foreach ($context as $mapping) {
    1083                                         if ($mapping[1] == $ns && strlen($mapping[0]) > 0) {
    1084                                                 return "$mapping[0]:$name";
    1085                                         }
    1086                                 }
    1087                         }
    1088                 }
    1089                 return $name;
    1090         }
    1091 
    1092         function xml_escape($string)
    1093         {
    1094                          return str_replace(array('&','"',"'",'<','>'),
    1095                                 array('&amp;','&quot;','&apos;','&lt;','&gt;'),
    1096                                 $string );
    1097         }
    1098 }
    1099935} // class_exists( 'WP_Importer' )
    1100936
    1101937function blogger_importer_init() {
    1102938    load_plugin_textdomain( 'blogger-importer', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
     939   
     940    $blogger_import = new Blogger_Import();
     941    register_importer('blogger', __('Blogger', 'blogger-importer'), __('Import categories, posts and comments then maps users from a Blogger blog.', 'blogger-importer'), array ($blogger_import, 'start'));
     942
    1103943}
    1104 add_action( 'init', 'blogger_importer_init' );
     944add_action( 'admin_init', 'blogger_importer_init' );
  • oauth.php

     
     1<?php
     2/*
     3Original File from: http://oauth.googlecode.com/svn/code/php/
     4License: MIT License:
     5
     6The MIT License
     7
     8Copyright (c) 2007 Andy Smith
     9
     10Permission is hereby granted, free of charge, to any person obtaining a copy
     11of this software and associated documentation files (the "Software"), to deal
     12in the Software without restriction, including without limitation the rights
     13to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     14copies of the Software, and to permit persons to whom the Software is
     15furnished to do so, subject to the following conditions:
     16
     17The above copyright notice and this permission notice shall be included in
     18all copies or substantial portions of the Software.
     19
     20THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
     21IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
     22FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
     23AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
     24LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
     25OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
     26THE SOFTWARE.
     27
     28
     29Modified for use in WordPress by Otto
     30
     31Changes: All classes renamed to Blogger_* to prevent conflicts with other plugins/code using the same OAuth library.
     32
     33*/
     34// vim: foldmethod=marker
     35
     36/* Generic exception class
     37 */
     38class Blogger_OAuthException extends Exception {
     39  // pass
     40}
     41
     42class Blogger_OAuthConsumer {
     43  public $key;
     44  public $secret;
     45
     46  function __construct($key, $secret, $callback_url=NULL) {
     47    $this->key = $key;
     48    $this->secret = $secret;
     49    $this->callback_url = $callback_url;
     50  }
     51
     52  function __toString() {
     53    return "Blogger_OAuthConsumer[key=$this->key,secret=$this->secret]";
     54  }
     55}
     56
     57class Blogger_OAuthToken {
     58  // access tokens and request tokens
     59  public $key;
     60  public $secret;
     61
     62  /**
     63   * key = the token
     64   * secret = the token secret
     65   */
     66  function __construct($key, $secret) {
     67    $this->key = $key;
     68    $this->secret = $secret;
     69  }
     70
     71  /**
     72   * generates the basic string serialization of a token that a server
     73   * would respond to request_token and access_token calls with
     74   */
     75  function to_string() {
     76    return "oauth_token=" .
     77           Blogger_OAuthUtil::urlencode_rfc3986($this->key) .
     78           "&oauth_token_secret=" .
     79           Blogger_OAuthUtil::urlencode_rfc3986($this->secret);
     80  }
     81
     82  function __toString() {
     83    return $this->to_string();
     84  }
     85}
     86
     87/**
     88 * A class for implementing a Signature Method
     89 * See section 9 ("Signing Requests") in the spec
     90 */
     91abstract class Blogger_OAuthSignatureMethod {
     92  /**
     93   * Needs to return the name of the Signature Method (ie HMAC-SHA1)
     94   * @return string
     95   */
     96  abstract public function get_name();
     97
     98  /**
     99   * Build up the signature
     100   * NOTE: The output of this function MUST NOT be urlencoded.
     101   * the encoding is handled in Blogger_OAuthRequest when the final
     102   * request is serialized
     103   * @param Blogger_OAuthRequest $request
     104   * @param Blogger_OAuthConsumer $consumer
     105   * @param Blogger_OAuthToken $token
     106   * @return string
     107   */
     108  abstract public function build_signature($request, $consumer, $token);
     109
     110  /**
     111   * Verifies that a given signature is correct
     112   * @param Blogger_OAuthRequest $request
     113   * @param Blogger_OAuthConsumer $consumer
     114   * @param Blogger_OAuthToken $token
     115   * @param string $signature
     116   * @return bool
     117   */
     118  public function check_signature($request, $consumer, $token, $signature) {
     119    $built = $this->build_signature($request, $consumer, $token);
     120
     121    // Check for zero length, although unlikely here
     122    if (strlen($built) == 0 || strlen($signature) == 0) {
     123      return false;
     124    }
     125
     126    if (strlen($built) != strlen($signature)) {
     127      return false;
     128    }
     129
     130    // Avoid a timing leak with a (hopefully) time insensitive compare
     131    $result = 0;
     132    for ($i = 0; $i < strlen($signature); $i++) {
     133      $result |= ord($built{$i}) ^ ord($signature{$i});
     134    }
     135
     136    return $result == 0;
     137  }
     138}
     139
     140/**
     141 * The HMAC-SHA1 signature method uses the HMAC-SHA1 signature algorithm as defined in [RFC2104]
     142 * where the Signature Base String is the text and the key is the concatenated values (each first
     143 * encoded per Parameter Encoding) of the Consumer Secret and Token Secret, separated by an '&'
     144 * character (ASCII code 38) even if empty.
     145 *   - Chapter 9.2 ("HMAC-SHA1")
     146 */
     147class Blogger_OAuthSignatureMethod_HMAC_SHA1 extends Blogger_OAuthSignatureMethod {
     148  function get_name() {
     149    return "HMAC-SHA1";
     150  }
     151
     152  public function build_signature($request, $consumer, $token) {
     153    $base_string = $request->get_signature_base_string();
     154    $request->base_string = $base_string;
     155
     156    $key_parts = array(
     157      $consumer->secret,
     158      ($token) ? $token->secret : ""
     159    );
     160
     161    $key_parts = Blogger_OAuthUtil::urlencode_rfc3986($key_parts);
     162    $key = implode('&', $key_parts);
     163
     164    return base64_encode(hash_hmac('sha1', $base_string, $key, true));
     165  }
     166}
     167
     168/**
     169 * The PLAINTEXT method does not provide any security protection and SHOULD only be used
     170 * over a secure channel such as HTTPS. It does not use the Signature Base String.
     171 *   - Chapter 9.4 ("PLAINTEXT")
     172 */
     173class Blogger_OAuthSignatureMethod_PLAINTEXT extends Blogger_OAuthSignatureMethod {
     174  public function get_name() {
     175    return "PLAINTEXT";
     176  }
     177
     178  /**
     179   * oauth_signature is set to the concatenated encoded values of the Consumer Secret and
     180   * Token Secret, separated by a '&' character (ASCII code 38), even if either secret is
     181   * empty. The result MUST be encoded again.
     182   *   - Chapter 9.4.1 ("Generating Signatures")
     183   *
     184   * Please note that the second encoding MUST NOT happen in the SignatureMethod, as
     185   * Blogger_OAuthRequest handles this!
     186   */
     187  public function build_signature($request, $consumer, $token) {
     188    $key_parts = array(
     189      $consumer->secret,
     190      ($token) ? $token->secret : ""
     191    );
     192
     193    $key_parts = Blogger_OAuthUtil::urlencode_rfc3986($key_parts);
     194    $key = implode('&', $key_parts);
     195    $request->base_string = $key;
     196
     197    return $key;
     198  }
     199}
     200
     201/**
     202 * The RSA-SHA1 signature method uses the RSASSA-PKCS1-v1_5 signature algorithm as defined in
     203 * [RFC3447] section 8.2 (more simply known as PKCS#1), using SHA-1 as the hash function for
     204 * EMSA-PKCS1-v1_5. It is assumed that the Consumer has provided its RSA public key in a
     205 * verified way to the Service Provider, in a manner which is beyond the scope of this
     206 * specification.
     207 *   - Chapter 9.3 ("RSA-SHA1")
     208 */
     209abstract class Blogger_OAuthSignatureMethod_RSA_SHA1 extends Blogger_OAuthSignatureMethod {
     210  public function get_name() {
     211    return "RSA-SHA1";
     212  }
     213
     214  // Up to the SP to implement this lookup of keys. Possible ideas are:
     215  // (1) do a lookup in a table of trusted certs keyed off of consumer
     216  // (2) fetch via http using a url provided by the requester
     217  // (3) some sort of specific discovery code based on request
     218  //
     219  // Either way should return a string representation of the certificate
     220  protected abstract function fetch_public_cert(&$request);
     221
     222  // Up to the SP to implement this lookup of keys. Possible ideas are:
     223  // (1) do a lookup in a table of trusted certs keyed off of consumer
     224  //
     225  // Either way should return a string representation of the certificate
     226  protected abstract function fetch_private_cert(&$request);
     227
     228  public function build_signature($request, $consumer, $token) {
     229    $base_string = $request->get_signature_base_string();
     230    $request->base_string = $base_string;
     231
     232    // Fetch the private key cert based on the request
     233    $cert = $this->fetch_private_cert($request);
     234
     235    // Pull the private key ID from the certificate
     236    $privatekeyid = openssl_get_privatekey($cert);
     237
     238    // Sign using the key
     239    $ok = openssl_sign($base_string, $signature, $privatekeyid);
     240
     241    // Release the key resource
     242    openssl_free_key($privatekeyid);
     243
     244    return base64_encode($signature);
     245  }
     246
     247  public function check_signature($request, $consumer, $token, $signature) {
     248    $decoded_sig = base64_decode($signature);
     249
     250    $base_string = $request->get_signature_base_string();
     251
     252    // Fetch the public key cert based on the request
     253    $cert = $this->fetch_public_cert($request);
     254
     255    // Pull the public key ID from the certificate
     256    $publickeyid = openssl_get_publickey($cert);
     257
     258    // Check the computed signature against the one passed in the query
     259    $ok = openssl_verify($base_string, $decoded_sig, $publickeyid);
     260
     261    // Release the key resource
     262    openssl_free_key($publickeyid);
     263
     264    return $ok == 1;
     265  }
     266}
     267
     268class Blogger_OAuthRequest {
     269  protected $parameters;
     270  protected $http_method;
     271  protected $http_url;
     272  // for debug purposes
     273  public $base_string;
     274  public static $version = '1.0';
     275  public static $POST_INPUT = 'php://input';
     276
     277  function __construct($http_method, $http_url, $parameters=NULL) {
     278    $parameters = ($parameters) ? $parameters : array();
     279    $parameters = array_merge( Blogger_OAuthUtil::parse_parameters(parse_url($http_url, PHP_URL_QUERY)), $parameters);
     280    $this->parameters = $parameters;
     281    $this->http_method = $http_method;
     282    $this->http_url = $http_url;
     283  }
     284
     285
     286  /**
     287   * attempt to build up a request from what was passed to the server
     288   */
     289  public static function from_request($http_method=NULL, $http_url=NULL, $parameters=NULL) {
     290    $scheme = (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != "on")
     291              ? 'http'
     292              : 'https';
     293    $http_url = ($http_url) ? $http_url : $scheme .
     294                              '://' . $_SERVER['SERVER_NAME'] .
     295                              ':' .
     296                              $_SERVER['SERVER_PORT'] .
     297                              $_SERVER['REQUEST_URI'];
     298    $http_method = ($http_method) ? $http_method : $_SERVER['REQUEST_METHOD'];
     299
     300    // We weren't handed any parameters, so let's find the ones relevant to
     301    // this request.
     302    // If you run XML-RPC or similar you should use this to provide your own
     303    // parsed parameter-list
     304    if (!$parameters) {
     305      // Find request headers
     306      $request_headers = Blogger_OAuthUtil::get_headers();
     307
     308      // Parse the query-string to find GET parameters
     309      $parameters = Blogger_OAuthUtil::parse_parameters($_SERVER['QUERY_STRING']);
     310
     311      // It's a POST request of the proper content-type, so parse POST
     312      // parameters and add those overriding any duplicates from GET
     313      if ($http_method == "POST"
     314          &&  isset($request_headers['Content-Type'])
     315          && strstr($request_headers['Content-Type'],
     316                     'application/x-www-form-urlencoded')
     317          ) {
     318        $post_data = Blogger_OAuthUtil::parse_parameters(
     319          file_get_contents(self::$POST_INPUT)
     320        );
     321        $parameters = array_merge($parameters, $post_data);
     322      }
     323
     324      // We have a Authorization-header with Blogger_OAuth data. Parse the header
     325      // and add those overriding any duplicates from GET or POST
     326      if (isset($request_headers['Authorization']) && substr($request_headers['Authorization'], 0, 6) == 'Blogger_OAuth ') {
     327        $header_parameters = Blogger_OAuthUtil::split_header(
     328          $request_headers['Authorization']
     329        );
     330        $parameters = array_merge($parameters, $header_parameters);
     331      }
     332
     333    }
     334
     335    return new Blogger_OAuthRequest($http_method, $http_url, $parameters);
     336  }
     337
     338  /**
     339   * pretty much a helper function to set up the request
     340   */
     341  public static function from_consumer_and_token($consumer, $token, $http_method, $http_url, $parameters=NULL) {
     342    $parameters = ($parameters) ?  $parameters : array();
     343    $defaults = array("oauth_version" => Blogger_OAuthRequest::$version,
     344                      "oauth_nonce" => Blogger_OAuthRequest::generate_nonce(),
     345                      "oauth_timestamp" => Blogger_OAuthRequest::generate_timestamp(),
     346                      "oauth_consumer_key" => $consumer->key);
     347    if ($token)
     348      $defaults['oauth_token'] = $token->key;
     349
     350    $parameters = array_merge($defaults, $parameters);
     351
     352    return new Blogger_OAuthRequest($http_method, $http_url, $parameters);
     353  }
     354
     355  public function set_parameter($name, $value, $allow_duplicates = true) {
     356    if ($allow_duplicates && isset($this->parameters[$name])) {
     357      // We have already added parameter(s) with this name, so add to the list
     358      if (is_scalar($this->parameters[$name])) {
     359        // This is the first duplicate, so transform scalar (string)
     360        // into an array so we can add the duplicates
     361        $this->parameters[$name] = array($this->parameters[$name]);
     362      }
     363
     364      $this->parameters[$name][] = $value;
     365    } else {
     366      $this->parameters[$name] = $value;
     367    }
     368  }
     369
     370  public function get_parameter($name) {
     371    return isset($this->parameters[$name]) ? $this->parameters[$name] : null;
     372  }
     373
     374  public function get_parameters() {
     375    return $this->parameters;
     376  }
     377
     378  public function unset_parameter($name) {
     379    unset($this->parameters[$name]);
     380  }
     381
     382  /**
     383   * The request parameters, sorted and concatenated into a normalized string.
     384   * @return string
     385   */
     386  public function get_signable_parameters() {
     387    // Grab all parameters
     388    $params = $this->parameters;
     389
     390    // Remove oauth_signature if present
     391    // Ref: Spec: 9.1.1 ("The oauth_signature parameter MUST be excluded.")
     392    if (isset($params['oauth_signature'])) {
     393      unset($params['oauth_signature']);
     394    }
     395
     396    return Blogger_OAuthUtil::build_http_query($params);
     397  }
     398
     399  /**
     400   * Returns the base string of this request
     401   *
     402   * The base string defined as the method, the url
     403   * and the parameters (normalized), each urlencoded
     404   * and the concated with &.
     405   */
     406  public function get_signature_base_string() {
     407    $parts = array(
     408      $this->get_normalized_http_method(),
     409      $this->get_normalized_http_url(),
     410      $this->get_signable_parameters()
     411    );
     412
     413    $parts = Blogger_OAuthUtil::urlencode_rfc3986($parts);
     414
     415    return implode('&', $parts);
     416  }
     417
     418  /**
     419   * just uppercases the http method
     420   */
     421  public function get_normalized_http_method() {
     422    return strtoupper($this->http_method);
     423  }
     424
     425  /**
     426   * parses the url and rebuilds it to be
     427   * scheme://host/path
     428   */
     429  public function get_normalized_http_url() {
     430    $parts = parse_url($this->http_url);
     431
     432    $scheme = (isset($parts['scheme'])) ? $parts['scheme'] : 'http';
     433    $port = (isset($parts['port'])) ? $parts['port'] : (($scheme == 'https') ? '443' : '80');
     434    $host = (isset($parts['host'])) ? strtolower($parts['host']) : '';
     435    $path = (isset($parts['path'])) ? $parts['path'] : '';
     436
     437    if (($scheme == 'https' && $port != '443')
     438        || ($scheme == 'http' && $port != '80')) {
     439      $host = "$host:$port";
     440    }
     441    return "$scheme://$host$path";
     442  }
     443
     444  /**
     445   * builds a url usable for a GET request
     446   */
     447  public function to_url() {
     448    $post_data = $this->to_postdata();
     449    $out = $this->get_normalized_http_url();
     450    if ($post_data) {
     451      $out .= '?'.$post_data;
     452    }
     453    return $out;
     454  }
     455
     456  /**
     457   * builds the data one would send in a POST request
     458   */
     459  public function to_postdata() {
     460    return Blogger_OAuthUtil::build_http_query($this->parameters);
     461  }
     462
     463  /**
     464   * builds the Authorization: header
     465   */
     466  public function to_header($realm=null) {
     467    $first = true;
     468        if($realm) {
     469      $out = 'Authorization: Blogger_OAuth realm="' . Blogger_OAuthUtil::urlencode_rfc3986($realm) . '"';
     470      $first = false;
     471    } else
     472      $out = 'Authorization: Blogger_OAuth';
     473
     474    $total = array();
     475    foreach ($this->parameters as $k => $v) {
     476      if (substr($k, 0, 5) != "oauth") continue;
     477      if (is_array($v)) {
     478        throw new Blogger_OAuthException('Arrays not supported in headers');
     479      }
     480      $out .= ($first) ? ' ' : ',';
     481      $out .= Blogger_OAuthUtil::urlencode_rfc3986($k) .
     482              '="' .
     483              Blogger_OAuthUtil::urlencode_rfc3986($v) .
     484              '"';
     485      $first = false;
     486    }
     487    return $out;
     488  }
     489
     490  public function __toString() {
     491    return $this->to_url();
     492  }
     493
     494
     495  public function sign_request($signature_method, $consumer, $token) {
     496    $this->set_parameter(
     497      "oauth_signature_method",
     498      $signature_method->get_name(),
     499      false
     500    );
     501    $signature = $this->build_signature($signature_method, $consumer, $token);
     502    $this->set_parameter("oauth_signature", $signature, false);
     503  }
     504
     505  public function build_signature($signature_method, $consumer, $token) {
     506    $signature = $signature_method->build_signature($this, $consumer, $token);
     507    return $signature;
     508  }
     509
     510  /**
     511   * util function: current timestamp
     512   */
     513  private static function generate_timestamp() {
     514    return time();
     515  }
     516
     517  /**
     518   * util function: current nonce
     519   */
     520  private static function generate_nonce() {
     521    $mt = microtime();
     522    $rand = mt_rand();
     523
     524    return md5($mt . $rand); // md5s look nicer than numbers
     525  }
     526}
     527
     528class Blogger_OAuthServer {
     529  protected $timestamp_threshold = 300; // in seconds, five minutes
     530  protected $version = '1.0';             // hi blaine
     531  protected $signature_methods = array();
     532
     533  protected $data_store;
     534
     535  function __construct($data_store) {
     536    $this->data_store = $data_store;
     537  }
     538
     539  public function add_signature_method($signature_method) {
     540    $this->signature_methods[$signature_method->get_name()] =
     541      $signature_method;
     542  }
     543
     544  // high level functions
     545
     546  /**
     547   * process a request_token request
     548   * returns the request token on success
     549   */
     550  public function fetch_request_token(&$request) {
     551    $this->get_version($request);
     552
     553    $consumer = $this->get_consumer($request);
     554
     555    // no token required for the initial token request
     556    $token = NULL;
     557
     558    $this->check_signature($request, $consumer, $token);
     559
     560    // Rev A change
     561    $callback = $request->get_parameter('oauth_callback');
     562    $new_token = $this->data_store->new_request_token($consumer, $callback);
     563
     564    return $new_token;
     565  }
     566
     567  /**
     568   * process an access_token request
     569   * returns the access token on success
     570   */
     571  public function fetch_access_token(&$request) {
     572    $this->get_version($request);
     573
     574    $consumer = $this->get_consumer($request);
     575
     576    // requires authorized request token
     577    $token = $this->get_token($request, $consumer, "request");
     578
     579    $this->check_signature($request, $consumer, $token);
     580
     581    // Rev A change
     582    $verifier = $request->get_parameter('oauth_verifier');
     583    $new_token = $this->data_store->new_access_token($token, $consumer, $verifier);
     584
     585    return $new_token;
     586  }
     587
     588  /**
     589   * verify an api call, checks all the parameters
     590   */
     591  public function verify_request(&$request) {
     592    $this->get_version($request);
     593    $consumer = $this->get_consumer($request);
     594    $token = $this->get_token($request, $consumer, "access");
     595    $this->check_signature($request, $consumer, $token);
     596    return array($consumer, $token);
     597  }
     598
     599  // Internals from here
     600  /**
     601   * version 1
     602   */
     603  private function get_version(&$request) {
     604    $version = $request->get_parameter("oauth_version");
     605    if (!$version) {
     606      // Service Providers MUST assume the protocol version to be 1.0 if this parameter is not present.
     607      // Chapter 7.0 ("Accessing Protected Ressources")
     608      $version = '1.0';
     609    }
     610    if ($version !== $this->version) {
     611      throw new Blogger_OAuthException("Blogger_OAuth version '$version' not supported");
     612    }
     613    return $version;
     614  }
     615
     616  /**
     617   * figure out the signature with some defaults
     618   */
     619  private function get_signature_method($request) {
     620    $signature_method = $request instanceof Blogger_OAuthRequest
     621        ? $request->get_parameter("oauth_signature_method")
     622        : NULL;
     623
     624    if (!$signature_method) {
     625      // According to chapter 7 ("Accessing Protected Ressources") the signature-method
     626      // parameter is required, and we can't just fallback to PLAINTEXT
     627      throw new Blogger_OAuthException('No signature method parameter. This parameter is required');
     628    }
     629
     630    if (!in_array($signature_method,
     631                  array_keys($this->signature_methods))) {
     632      throw new Blogger_OAuthException(
     633        "Signature method '$signature_method' not supported " .
     634        "try one of the following: " .
     635        implode(", ", array_keys($this->signature_methods))
     636      );
     637    }
     638    return $this->signature_methods[$signature_method];
     639  }
     640
     641  /**
     642   * try to find the consumer for the provided request's consumer key
     643   */
     644  private function get_consumer($request) {
     645    $consumer_key = $request instanceof Blogger_OAuthRequest
     646        ? $request->get_parameter("oauth_consumer_key")
     647        : NULL;
     648
     649    if (!$consumer_key) {
     650      throw new Blogger_OAuthException("Invalid consumer key");
     651    }
     652
     653    $consumer = $this->data_store->lookup_consumer($consumer_key);
     654    if (!$consumer) {
     655      throw new Blogger_OAuthException("Invalid consumer");
     656    }
     657
     658    return $consumer;
     659  }
     660
     661  /**
     662   * try to find the token for the provided request's token key
     663   */
     664  private function get_token($request, $consumer, $token_type="access") {
     665    $token_field = $request instanceof Blogger_OAuthRequest
     666         ? $request->get_parameter('oauth_token')
     667         : NULL;
     668
     669    $token = $this->data_store->lookup_token(
     670      $consumer, $token_type, $token_field
     671    );
     672    if (!$token) {
     673      throw new Blogger_OAuthException("Invalid $token_type token: $token_field");
     674    }
     675    return $token;
     676  }
     677
     678  /**
     679   * all-in-one function to check the signature on a request
     680   * should guess the signature method appropriately
     681   */
     682  private function check_signature($request, $consumer, $token) {
     683    // this should probably be in a different method
     684    $timestamp = $request instanceof Blogger_OAuthRequest
     685        ? $request->get_parameter('oauth_timestamp')
     686        : NULL;
     687    $nonce = $request instanceof Blogger_OAuthRequest
     688        ? $request->get_parameter('oauth_nonce')
     689        : NULL;
     690
     691    $this->check_timestamp($timestamp);
     692    $this->check_nonce($consumer, $token, $nonce, $timestamp);
     693
     694    $signature_method = $this->get_signature_method($request);
     695
     696    $signature = $request->get_parameter('oauth_signature');
     697    $valid_sig = $signature_method->check_signature(
     698      $request,
     699      $consumer,
     700      $token,
     701      $signature
     702    );
     703
     704    if (!$valid_sig) {
     705      throw new Blogger_OAuthException("Invalid signature");
     706    }
     707  }
     708
     709  /**
     710   * check that the timestamp is new enough
     711   */
     712  private function check_timestamp($timestamp) {
     713    if( ! $timestamp )
     714      throw new Blogger_OAuthException(
     715        'Missing timestamp parameter. The parameter is required'
     716      );
     717   
     718    // verify that timestamp is recentish
     719    $now = time();
     720    if (abs($now - $timestamp) > $this->timestamp_threshold) {
     721      throw new Blogger_OAuthException(
     722        "Expired timestamp, yours $timestamp, ours $now"
     723      );
     724    }
     725  }
     726
     727  /**
     728   * check that the nonce is not repeated
     729   */
     730  private function check_nonce($consumer, $token, $nonce, $timestamp) {
     731    if( ! $nonce )
     732      throw new Blogger_OAuthException(
     733        'Missing nonce parameter. The parameter is required'
     734      );
     735
     736    // verify that the nonce is uniqueish
     737    $found = $this->data_store->lookup_nonce(
     738      $consumer,
     739      $token,
     740      $nonce,
     741      $timestamp
     742    );
     743    if ($found) {
     744      throw new Blogger_OAuthException("Nonce already used: $nonce");
     745    }
     746  }
     747
     748}
     749
     750class Blogger_OAuthDataStore {
     751  function lookup_consumer($consumer_key) {
     752    // implement me
     753  }
     754
     755  function lookup_token($consumer, $token_type, $token) {
     756    // implement me
     757  }
     758
     759  function lookup_nonce($consumer, $token, $nonce, $timestamp) {
     760    // implement me
     761  }
     762
     763  function new_request_token($consumer, $callback = null) {
     764    // return a new token attached to this consumer
     765  }
     766
     767  function new_access_token($token, $consumer, $verifier = null) {
     768    // return a new access token attached to this consumer
     769    // for the user associated with this token if the request token
     770    // is authorized
     771    // should also invalidate the request token
     772  }
     773
     774}
     775
     776class Blogger_OAuthUtil {
     777  public static function urlencode_rfc3986($input) {
     778  if (is_array($input)) {
     779    return array_map(array('Blogger_OAuthUtil', 'urlencode_rfc3986'), $input);
     780  } else if (is_scalar($input)) {
     781    return str_replace(
     782      '+',
     783      ' ',
     784      str_replace('%7E', '~', rawurlencode($input))
     785    );
     786  } else {
     787    return '';
     788  }
     789}
     790
     791
     792  // This decode function isn't taking into consideration the above
     793  // modifications to the encoding process. However, this method doesn't
     794  // seem to be used anywhere so leaving it as is.
     795  public static function urldecode_rfc3986($string) {
     796    return urldecode($string);
     797  }
     798
     799  // Utility function for turning the Authorization: header into
     800  // parameters, has to do some unescaping
     801  // Can filter out any non-oauth parameters if needed (default behaviour)
     802  // May 28th, 2010 - method updated to tjerk.meesters for a speed improvement.
     803  //                  see http://code.google.com/p/oauth/issues/detail?id=163
     804  public static function split_header($header, $only_allow_oauth_parameters = true) {
     805    $params = array();
     806    if (preg_match_all('/('.($only_allow_oauth_parameters ? 'oauth_' : '').'[a-z_-]*)=(:?"([^"]*)"|([^,]*))/', $header, $matches)) {
     807      foreach ($matches[1] as $i => $h) {
     808        $params[$h] = Blogger_OAuthUtil::urldecode_rfc3986(empty($matches[3][$i]) ? $matches[4][$i] : $matches[3][$i]);
     809      }
     810      if (isset($params['realm'])) {
     811        unset($params['realm']);
     812      }
     813    }
     814    return $params;
     815  }
     816
     817  // helper to try to sort out headers for people who aren't running apache
     818  public static function get_headers() {
     819    if (function_exists('apache_request_headers')) {
     820      // we need this to get the actual Authorization: header
     821      // because apache tends to tell us it doesn't exist
     822      $headers = apache_request_headers();
     823
     824      // sanitize the output of apache_request_headers because
     825      // we always want the keys to be Cased-Like-This and arh()
     826      // returns the headers in the same case as they are in the
     827      // request
     828      $out = array();
     829      foreach ($headers AS $key => $value) {
     830        $key = str_replace(
     831            " ",
     832            "-",
     833            ucwords(strtolower(str_replace("-", " ", $key)))
     834          );
     835        $out[$key] = $value;
     836      }
     837    } else {
     838      // otherwise we don't have apache and are just going to have to hope
     839      // that $_SERVER actually contains what we need
     840      $out = array();
     841      if( isset($_SERVER['CONTENT_TYPE']) )
     842        $out['Content-Type'] = $_SERVER['CONTENT_TYPE'];
     843      if( isset($_ENV['CONTENT_TYPE']) )
     844        $out['Content-Type'] = $_ENV['CONTENT_TYPE'];
     845
     846      foreach ($_SERVER as $key => $value) {
     847        if (substr($key, 0, 5) == "HTTP_") {
     848          // this is chaos, basically it is just there to capitalize the first
     849          // letter of every word that is not an initial HTTP and strip HTTP
     850          // code from przemek
     851          $key = str_replace(
     852            " ",
     853            "-",
     854            ucwords(strtolower(str_replace("_", " ", substr($key, 5))))
     855          );
     856          $out[$key] = $value;
     857        }
     858      }
     859    }
     860    return $out;
     861  }
     862
     863  // This function takes a input like a=b&a=c&d=e and returns the parsed
     864  // parameters like this
     865  // array('a' => array('b','c'), 'd' => 'e')
     866  public static function parse_parameters( $input ) {
     867    if (!isset($input) || !$input) return array();
     868
     869    $pairs = explode('&', $input);
     870
     871    $parsed_parameters = array();
     872    foreach ($pairs as $pair) {
     873      $split = explode('=', $pair, 2);
     874      $parameter = Blogger_OAuthUtil::urldecode_rfc3986($split[0]);
     875      $value = isset($split[1]) ? Blogger_OAuthUtil::urldecode_rfc3986($split[1]) : '';
     876
     877      if (isset($parsed_parameters[$parameter])) {
     878        // We have already recieved parameter(s) with this name, so add to the list
     879        // of parameters with this name
     880
     881        if (is_scalar($parsed_parameters[$parameter])) {
     882          // This is the first duplicate, so transform scalar (string) into an array
     883          // so we can add the duplicates
     884          $parsed_parameters[$parameter] = array($parsed_parameters[$parameter]);
     885        }
     886
     887        $parsed_parameters[$parameter][] = $value;
     888      } else {
     889        $parsed_parameters[$parameter] = $value;
     890      }
     891    }
     892    return $parsed_parameters;
     893  }
     894
     895  public static function build_http_query($params) {
     896    if (!$params) return '';
     897
     898    // Urlencode both keys and values
     899    $keys = Blogger_OAuthUtil::urlencode_rfc3986(array_keys($params));
     900    $values = Blogger_OAuthUtil::urlencode_rfc3986(array_values($params));
     901    $params = array_combine($keys, $values);
     902
     903    // Parameters are sorted by name, using lexicographical byte value ordering.
     904    // Ref: Spec: 9.1.1 (1)
     905    uksort($params, 'strcmp');
     906
     907    $pairs = array();
     908    foreach ($params as $parameter => $value) {
     909      if (is_array($value)) {
     910        // If two or more parameters share the same name, they are sorted by their value
     911        // Ref: Spec: 9.1.1 (1)
     912        // June 12th, 2010 - changed to sort because of issue 164 by hidetaka
     913        sort($value, SORT_STRING);
     914        foreach ($value as $duplicate_value) {
     915          $pairs[] = $parameter . '=' . $duplicate_value;
     916        }
     917      } else {
     918        $pairs[] = $parameter . '=' . $value;
     919      }
     920    }
     921    // For each parameter, the name is separated from the corresponding value by an '=' character (ASCII code 61)
     922    // Each name-value pair is separated by an '&' character (ASCII code 38)
     923    return implode('&', $pairs);
     924  }
     925}
     926
  • readme.txt

     
    1 === Blogger Importer ===
    2 Contributors: wordpressdotorg
     1=== Plugin Name ===
     2Contributors: wordpressdotorg, otto42, workshopshed, SergeyBiryukov, rmccue
    33Donate link:
    44Tags: importer, blogger
    55Requires at least: 3.0
    66Tested up to: 3.2
    7 Stable tag: 0.4
     7Stable tag: 0.6
     8License: GPLv2 or later
    89
    9 Import posts, comments, and users from a Blogger blog.
     10Imports posts, comments and categories (blogger tags) from a Blogger blog then migrates authors to Wordpress users.
    1011
    1112== Description ==
    1213
    13 This is the Blogger Importer available from the WordPress Tools->Import screen. It imports posts, comments, and users from a Blogger site into a WordPress installation.
     14The Blogger Importer imports your blog data from a Blogger site into a WordPress.org installation.
    1415
     16= Items imported =
     17
     18* Categories
     19* Posts (published, scheduled and draft)
     20* Comments (not spam)
     21
     22= Items not imported =
     23
     24* Pages
     25* Images (the images will appear in your new blog but will link to the old blogspot or picassa web locations)
     26
    1527== Installation ==
    1628
    17291. Upload the `blogger-importer` folder to the `/wp-content/plugins/` directory
    18301. Activate the plugin through the 'Plugins' menu in WordPress
    19 1. Go to the Tools -> Import screen, Click on Blogger
    2031
     32= How to use =
     33
     341. Blogger Importer is available from the WordPress Tools->Import screen.
     351. Press Authorise
     361. If you are not already logged into Google you will be asked to login
     371. You will be asked to grant Wordpress access to your Blogger information, to continue press Grant Access
     381. You will be presented with a list of all your blogs
     391. Select the appropriate blog and press the import button
     401. Wait whilst the posts and comments are imported
     411. Press the Set Authors button
     421. Select the appropriate mapping for the authors
     431. Review categories, posts and comments
     44
     45You can now remove the importer plugin if you no longer need to use it.
     46
    2147== Frequently Asked Questions ==
    2248
     49= How do I re-import? =
     50
     51Press the clear account information button, then re-connect to blogger and re-import, the importer is designed not to re-import the same posts. If you need to do a full re-import then delete the posts and then empty the trash before re-importing.
     52
     53= How do I know which posts were imported? =
     54
     55Each of the posts loaded is tagged with a meta tags indicating where the posts were loaded from. The permalink will be set to the visible URL if the post was published or the internal ID if it was still a draft or scheduled post
     56
     57* blogger_author
     58* blogger_blog
     59* blogger_permalink
     60
     61= Why does it keep stopping? =
     62
     63The importer is designed not to overload blogger or your site so only imports in batches and will run for a fixed number of seconds before pausing, the admin screen will refresh every few seconds to show how many it has done so far. Press continue to continue importing posts and comments.
     64
     65= After importing there are a lot of categories =
     66
     67Blogger does not distinguish between tags and categories so you will likely want to review what was imported and then use the categories to tags converter
     68
     69= What about pages? =
     70
     71This importer does not handle blogger pages, you will need to manually transfer them.
     72
     73= What about images? =
     74
     75The importer will simply load the tags for the images as they appear in your source data, so you will have references to blogspot and picassa based images. If you do not migrate these with a separate tool then these will be lost when you delete your old blogger blog.
     76
     77= Are the permalinks the same? =
     78
     79No, Wordpress and Blogger handle the permalinks differently. However, it is possible to use the redirection plugin to map the old URLs across to the new URLs.
     80
     81= What about future posts? =
     82
     83The scheduled posts will be transferred and will be published as specified. However, Blogger and Wordpress handle drafts differently, Wordpress does not support dates on draft posts so you will need to use a plugin if you wish to plan your writing schedule.
     84
     85= My posts and comments moved across but some things are stripped out =
     86
     87The importer uses the SimplePie classes to process the data, these in turn use a Simplepie_Sanitize class to remove potentially malicious code from the source data.
     88
    2389== Screenshots ==
    2490
     91== Reference ==
     92
     93* https://developers.google.com/blogger/docs/1.0/developers_guide_php
     94* https://developers.google.com/gdata/articles/oauth
     95
     96== Other changes to review ==
     97
     98Review the santisation processes, are these being done twice?
     99
     100http://core.trac.wordpress.org/attachment/ticket/15737/blogger-importer.patch
     101http://core.trac.wordpress.org/ticket/15737
     102
     103
     104
     105http://core.trac.wordpress.org/ticket/7652
     106http://core.trac.wordpress.org/attachment/ticket/7652/7652-blogger.diff
     107http://core.trac.wordpress.org/attachment/ticket/7652/7652-separate.diff
     108
     109Move the Javascript into a separate file, pass the parameters and localised strings using wp_localize_script
     110
     111Inconsistent UI (sometimes you get a set of tools graphic, sometimes not)
     112
     113Handle geotags See http://codex.wordpress.org/Geodata
     114
     115
     116
    25117== Changelog ==
    26118
     119= 0.6 =
     120* Merged in fix by SergeyBiryukov http://core.trac.wordpress.org/ticket/16012
     121* Merged in rmccue change to get_total_results to also use SimplePie from http://core.trac.wordpress.org/attachment/ticket/7652/7652-blogger.diff
     122* Reviewed in rmccue's changes in http://core.trac.wordpress.org/attachment/ticket/7652/7652-separate.diff issues with date handling functions so skipped those
     123* Moved SimplePie functions in  new class WP_SimplePie_Blog_Item incorporating get_draft_status and get_updated and convert date
     124* Andy from Workshopshed tested comments from source blog GMT-8, destination London (currently GMT-1), comment dates transferred correctly.
     125* Andy from Workshopshed Fixed typo in oauth_get
     126
     127= 0.5 =
     128* Change by Otto42, rmccue to use Simplepie XML processing rather than Atomparser, http://core.trac.wordpress.org/ticket/14525 ref: http://core.trac.wordpress.org/attachment/ticket/7652/7652-blogger.diff
     129  this also fixes http://core.trac.wordpress.org/ticket/15560
     130* Change by Otto42 to use OAuth rather than AuthSub authentication, should make authentication more reliable
     131* Fix by Andy from Workshopshed to load comments correctly
     132* Fix by Andy from Workshopshed to correctly pass the blogger start-index and max-results parameters to oAuth functions and to process more than one batch http://core.trac.wordpress.org/ticket/19096
     133* Fix by Andy from Workshopshed error about incorrect enqueuing of scripts also changed styles to work the same
     134* Change by Andy from Workshopshed testing in debug mode and wrapped ajax return into a function to suppress debug messages
     135* Fix by Andy from Workshopshed notices for undefined variables.
     136* Change by Andy from Workshopshed Added tooltip to results table to show numbers of posts and comments skipped (duplicates / missing key)
     137* Fix by Andy from Workshopshed incorrectly checking for duplicates based on only the date and username, this gave false positives when large numbers of comments, particularly anonymous ones.
     138
    27139= 0.4 =
    28140* Fix for tracking images being added by Blogger to non-authenticated feeds http://core.trac.wordpress.org/ticket/17623
    29141
     
    35147
    36148== Upgrade Notice ==
    37149
    38 = 0.4 =
    39 * Fix for tracking images being added by Blogger to non-authenticated feeds http://core.trac.wordpress.org/ticket/17623
     150= 0.6 =
    40151
    41 = 0.3 =
    42 * Bugfix for 403 Invalid AuthSub Token
     152Merged in fixes found in Trac
    43153
     154= 0.5 =
     155
     156This version is a significant re-write based on previous versions.
     157