Google Reader list in WordPress

I don't use WordPress and I don't use Google Reader. But someone who does found Mike Crute's code which incorporates Google Reader into a WordPress sidebar.

Because our web host has disabled url_fopen for security reasons, I added support for cURL. I also added a few other features and error checking to it.

Update: The plugin now purports to support "Share with note". If you have enabled excerpts, your note will appear before the excerpt inside a blockquote element.

Here it is (or download it):

<?php
/*
    Plugin Name: Google Reader Shared
    Plugin URI: http://mike.crute.org/
    Description: A sidebar add-in for Google Reader shared items.
    Version: 1.0
    Author: Mike Crute
    Author URI: http://mike.crute.org/
*/
/* Change log
   Author: Jeremy Michelson
   Author URI: http://jeremy.smallinfinity.net/

   July 13, 2008
   Put shared comments into a blockquote, with the "Shared by <user> comment
   removed.

   March 21, 2007
   Apparently--at least this is what I think happened--Google altered the
   XML format of their feed, thereby destroying the link targets of the
   plugin.  This is now fixed, at least until the XML format changes again.
   Thanks to Amish for alerting me to the problem!
   
   BUG FIXES: Fixed year in previous update timestamps!

   TO DO: The things I previously assigned myself to do.  But I've been busy...

   February 3, 2007
   BUG FIXES
   1. Fixed stupid javascript bug (it's getElementById, not getDocumentById!)
      Now the Content Length textbox is only enabled if you want content
      excerpts.

   January 28, 2007
   BUG FIXES
   1. If you are only listing titles, then the close tag is </ul>
      not </dl>!  (Thanks Amish!)

   January 27, 2007
   NEW FEATURES
   1. "Silent" failures can be made verbose by changing the below
      "define('READER_SHARED_DEBUG', false)" to "..., true)".  It is not
      recommended that this be done except for debugging.
   2. Added optional argument to the readerShared so that
      <?php readerShared('tag'); ?> extracts only those shared items that
      have been tagged, or given the label, 'tag'.  Conversely, if you
      provided a specific URL for a label, then <?php readerShared('') ?>
      will show all items, regardless of tag.

   TO DO
   1. Allow additional optional arguments to readerShared.  These will
      likely have the following format: 
           readerShared('tag1', 'tag2', '!tag3', 'tag4', '!tag5', ...)
      will display those shared items that match tag1, tag2, and tag4
      while not matching tag3 or tag5.  Similarly,
           readerShared('!tag')
      removes items tagged with 'tag' from what would otherwise be displayed;
       this is backwards compatible with the current
           readherShared('tag')
      which displays those items with that tag; etc..  Be warned that this is
      incompatible with having a tag which begins with a '!'.

   Posted 2007-01-20
   BUG FIXES
   1. removed the "class=..." from the dd tag.  This is unnecessary
      provided the style sheet contains a ".classname dd" rule; and
      any "dd.classname" rule should be converted to ".classname dd".  Moreover
      the way it was written, if there was no class, then '<dd class="">'
      would appear, which is just dumb.

   Posted 2007-01-19
   NEW FEATURES
   1. added support for cURL if allow_url_fopen is off
   2. moved the fprintf("<ul>...") until after the xmlsimple parsing
      so that we can return on failure with no side effects
   3. if neither cURL is enabled, nor allow_url_fopen, then this is now a NOP.
      Similarly if there is no SimpleXML, then this is a NOP.
      Added an option to determine timeout length for cURL.
   4. Options to excerpt the content and if so, how much
   5. Silently but cleanly failing error checking for failure of 
      simplexml_load_(file|string).  Similarly, supressed errors therefrom.
   6. Added error checking of $_POST's.
   7. Similarly, silently NOP if configuration has not occurred, thereby
      avoiding errors trying to obtain the xml from "".  Similarly override
      an empty numdisp to the maximum while rendering.

   BUG FIXES
   1. There was an "function readerShared_Menu redefined on line (just after
      where it is defined " fatal error.  This was fixed by surrounding the
      definition with an "if ( ! function_exists(...) ) { ... }"
   2. "&"'s in href's were not written as "&amp;" resulting in invalid HTML.
      On closer examination, this is the effect of SimpleXML's translating
      character entities to their corresponding characters.  So, we should
      selectively translate back.  We need to do it selectively, so that e.g.
      "?" doesn't become "%3F" but so that the obviously bad characters
      '"', '&', '<', '>' get properly reencoded.  htmlspecialchars appears to be
      precisely designed for this.

   TO DO/CHECK
   1. Ensure that if get_magic_quotes_runtime(), there isn't a problem
      with the data that SimpleXML receives.
   2. Wonder if $_POST data needs to be stripslashed if magic_quotes_gpc,
      or if update_option does this automatically.
   3. If the order of preference (url_fopen vs cURL) is changed, update
      the form (function_exists instead of ini_get) as well.  Also,
      the form could detect whether this plugin is guaranteed to fail for
      reasons described in Features 3&5 and give feedback accordingly.
*/

define('READER_SHARED_DEBUG', false);

// checks to make sure the string is a good integer within valid bounds
// NULL means unbounded.  defaults to a min of 0 and a max of NULL
// Return value codes:
//   FALSE: not an integer; -1: less than $min; +1: bigger than $min; 0: ok
// so use === to test the return value!
if ( !function_exists('readerShared_echo') ) {
  function readerShared_echo($s) {
    if ( READER_SHARED_DEBUG ) echo "<p class=\"error\">$s</p>\n";
  }
}

if ( !function_exists('readerShared_goodInt') ) {
  function readerShared_goodInt($s, $min=0, $max=NULL) {
    $s = trim($s);
    if ( !ctype_digit($s) && 
	 ( ($min !== NULL && $min >=0) || $s{0} != '-' 
	   || !ctype_digits(substr($s, 1)) ) ) {
      return FALSE;
    }
    $i = (int)$s;
    if ( $min !== NULL && $i < $min ) {
      return -1;
    }
    if ( $max !== NULL && $i > $max ) {
      return 1;
    }

    return 0;
  }
}

// Format the plugin page
if (is_plugin_page()) { 
    // sanity check: don't want our "url" to e.g. be "file:///etc/passwd"
    // even if only an admin user is ostensibly able to get to this page
    // on the other hand, we should be as liberal as possible...so allow ftp
    $readerSharedErrors = array();
    if ( isset($_POST['update_reader']) ) {
        if ( isset($_POST['url'])
  	     && preg_match('!^\s*(?:https?|ftp)://!', $_POST['url']) ) {
          if ( !preg_match('!^https?://www\.google\.com/reader/public/atom/user/\d+/state/com\.google/broadcast$!', $_POST['url']) ) {
	    if ( preg_match('!^https?://www\.google\.com/reader/public/atom/user/\d+/label/[^/]+$!', $_POST['url']) ) {
		  // could do something like a warning
	    } else {
	      $readerSharedErrors[] = "I will use the provided URL, <code>{$_POST['url']}</code>, even though it does not look like a Google Reader URL";
	    }
	  } // finished checking type of google reader url

	  update_option('GoogleReader_FeedURL', $_POST['url']);
        } else {
            $readerSharedErrors[] = 'Please '.( isset($_POST['update_reader'])
      					        ? 'provide' : 'correct' )
                                    .' your <a href="#url">feed URL</a>.';
	}

        // make sure numdisp is a number within the advertised bounds
        if ( isset($_POST['numdisp']) 
	     && ($readerSharedITest 
                 = readerShared_goodInt($_POST['numdisp'], 1, 20)) === 0 ) {
	    update_option('GoogleReader_NumDisplay', $_POST['numdisp']);
	} else {
          $readerSharedErrorPre = '<a href="#numdisp">\"Number to Display\"</a>';
	  if ( $readerSharedITest === FALSE ) {
	    $readerSharedErrors[] = $readerSharedErrorPre." must be an integer!";
	  } else if ( $readerSharedITest === 1 ) {
	    $readerSharedErrors[] = $readerSharedErrorPre." must be positive.";
	  } else { // must be -1
	    $readerSharedErrors[] = $readerSharedErrorPre." must be less than 20.";
	  }
	} // done with numdisp

        // nothing to check for the class
        update_option('GoogleReader_CSSClass', $_POST['cssclass']);

	// excerpt is a checkbox
        update_option('GoogleReader_IncludeExcerpts',
		      (isset($_POST['excerpt']) ? "1" : "" ));

        // even if !excerpt, update the content length, if there.  Never an
        // error to omit; defaults to unlimited
	// similarly curl time out length
        foreach ( array('contentlen'=>array('GoogleReader_ContentLength', 'amount to excerpt'),
                        'curltime' => array('GoogleReader_curlTimeOut', 'cURL time out'))
                  as $readerShared_id => $readerShared_stuff ) {
	  if ( isset($_POST[$readerShared_id]) ) {
	    $readerSharedErrorPre = "<a href=\"#$readerShared_id\">{$readerShared_stuff[1]}</a>";
	    if ( ($readerSharedITest
		  = readerShared_goodInt($_POST[$readerShared_id], 0, NULL)) === 0 ) {
	      update_option($readerShared_stuff[0], $_POST[$readerShared_id]);
	    } else if ( trim($_POST[$readerShared_id]) === "" ) { // treat as 0
	      update_option($readerShared_stuff[0], '0');
	    } else if ( $readerSharedITest !== 1 ) { // wasn't empty
	      $readerSharedErrors[] = $readerSharedErrorPre.' must be a nonnegative integer.';
	    }
	  } // end of if isset
	} // end of foreach similar integer

        echo('<div class="updated"><p>'
             .(!empty($readerSharedErrors) ? 'Valid o' : 'O')
             .'ptions changes saved.</p></div>');
        if ( !empty($readerSharedErrors) ) {
	    echo '<div class="error"><ul>';
	    foreach ( $readerSharedErrors as $readerSharedError ) {
	      echo "\n<li>$readerSharedError</li>";
	    }
	    echo "\n</ul></div>\n";
	}

    }
?>
    <div class="wrap">
        <h2>Google Reader Shared Items Options</h2>
        
        <form method="post">        
            <fieldset class="options">
                <p><strong>Google Reader</strong></p>
                <p>
                <label for="url">Feed URL:</label>
                <input name="url" type="text" id="url" value="<?php echo get_option('GoogleReader_FeedURL'); ?>" />
                
                <label for="numdisp">Number to Display (max is 20):</label>
                <input name="numdisp" type="text" id="numdisp" value="<?php echo get_option('GoogleReader_NumDisplay'); ?>" />
                
                <label for="cssclass">CSS Class:</label>
                <input name="cssclass" type="text" id="cssclass" value="<?php echo get_option('GoogleReader_CSSClass'); ?>" />
                </p>
                </p><p>
		<input name="excerpt" type="checkbox" id="excerpt"
                   <?php if ( get_option('GoogleReader_IncludeExcerpts') == "1" ) echo ' checked="checked"'; ?>          
                   onclick="getElementById('contentlen').disabled = !this.checked" />&nbsp;<label for="excerpt">Include Content Excerpt</label>
		<label for="contentlen">Number of characters to excerpt (use 0 for no limit):</label>
                <input name="contentlen" type="text" id="contentlen" value="<?php echo get_option('GoogleReader_ContentLength'); ?>" />
                <script type="text/javascript">
                   <!--
		   getElementById('contentlen').disabled = !getElementById('excerpt').checked;
		   //-->
                </script>
                </p><p>
		<label for="curltime">If using cURL,
		(you probably are<?php if ( ini_get('allow_url_fopen') ) echo  ' not'; ?>)
                number of seconds to wait before timing out (0 means unspecified default behavior):</label>
                <input name="curltime" type="text" id="curltime" value="<?php echo get_option('GoogleReader_curlTimeOut'); ?>" />
                </p>
            </fieldset>

              <p><div class="submit"><input type="submit" name="update_reader" value="Save Settings" style="font-weight:bold;" /></div></p>
        </form>
	<p>If you excerpt, the Google Reader list will be inside a &lt;dl&gt; (descriptive list) element;
	   the headlines will be inside a &lt;dt&gt; element and
           the excerpted content will be inside a &lt;dd&gt; element.
	   Otherwise, &lt;ul&gt; and &lt;li&gt; tags will be used as usual.
           Please ensure your style sheet is written accordingly.
        </p>
        <p>Make sure your <code>sidebar.php</code> contains lines functionally
           equivalent to</p>
        <pre>
	   &lt;h2&gt;<em>Google Reader</em>&lt;/h2&gt;
           &lt;?php readerShared(); ?&gt;
        </pre>
    </div>
<?php
}

else {
    function readerShared($tag = NULL) {
        if ( ! extension_loaded('SimpleXML') ) {
	  readerShared_echo("SimpleXML is required and not installed.");
            if ( !version_compare(phpversion(), '5.0', '>=') ) {
	      readerShared_echo("Moreover, SimpleXML requires PHP 5 or better.");
            }
            return; // no SimpleXML support!
        }
        $feedurl = trim(get_option('GoogleReader_FeedURL'));
        $display = trim(get_option('GoogleReader_NumDisplay'));
        $class = trim(get_option('GoogleReader_CSSClass'));

        if ( empty($feedurl) ) { // user hasn't configured yet!
          readerShared_echo('Reader_Shared plugin has not yet been configured.');
	  return;
	} else { // deal with tag
          if ( preg_match('!^http(s?)://www\.google\.com/reader/public/atom/user/(\d+)/state/com\.google/broadcast$!', $feedurl, $nonunique) 
	       && !empty($tag) ) { // turn it into a tag url
	    $feedurl = "http{$nonunique[1]}://www.google.com/reader/public/atom/user/{$nonunique[2]}/label/".urlencode($tag);
	  } else if ( preg_match('!^http(s?)://www\.google\.com/reader/public/atom/user/(\d+)/label/([^/]+)$!', $feedurl, $nonunique) ) { // might need to change it
	    if ( $tag === '' ) { // need to turn it into a nontag url
	      $feedurl = "http{$nonunique[1]}://www.google.com/reader/public/atom/user/{$nonunique[2]}/state/com.google/broadcast";
	    } else if ( $tag !== NULL && $tag !== $nonunique[3] ) { // replace the tag
	      $feedurl = "http{$nonunique[1]}://www.google.com/reader/public/atom/user/{$nonunique[2]}/label/".urlencode($tag);
	    } // end of fixing tag url
	  } // end of fixing url
	} // end of checking/fixing url

        if ( empty($display) ) { // use max
	  $display = 20;
        }

        if ( ini_get('allow_url_fopen') ) { // ok
	    $feed = @simplexml_load_file($feedurl);
        } else if ( function_exists("curl_init") ) {
	    $curlHandle = curl_init($feedurl);
            $curlTO =  get_option('GoogleReader_curlTimeOut');
            if ( !empty($curlTO) ) { //  "0" is empty
	      curl_setopt($curlHandle, CURLOPT_TIMEOUT, $curlTO);
	    } // otherwise whatever the default is
	    // make sure cURL returns a string that we can give to simplexml
	    curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, true);
	    // make sure there is no infinite redirect loop
	    curl_setopt($curlHandle, CURLOPT_MAXREDIRS, 10);
	    // make sure the blog doesn't block unknown user agents
	    curl_setopt($curlHandle, CURLOPT_USERAGENT,
	    	      $_SERVER['HTTP_USER_AGENT']);
	    // this is probably unnecessary but prevents broadcasting passwords
	    curl_setopt($curlHandle, CURLOPT_UNRESTRICTED_AUTH, FALSE);
	    $feedstring = curl_exec($curlHandle);
	    if ( curl_errno($curlHandle) ) { // fail silently
	      readerShared_echo("Error obtaining the Google Reader feed $feedurl.");
	        return;
	    }
	    curl_close($curlHandle);
	    $feed = @simplexml_load_string($feedstring);
	} else { // no curl, no url_fopen, I give up
	  readerShared_echo('This plugin requires at least one of <code>url_fopen</code> or <code>cURL</code>.');
	    return;
	}

        if ( !is_a($feed, 'SimpleXMLElement') ) { // problem occurred
	    readerShared_echo('SimpleXML failed somehow.');
            return;
	}
        $loopcount = 0;
        if ( $excerpt = (get_option('GoogleReader_IncludeExcerpts') == '1') ) {
	  $listType = 'dl';
	  $listElement = 'dt';
          $excerptLen = trim(get_option('GoogleReader_ContentLength'));
          if ( empty($excerptLen) ) {
	    $excerptLen = 0;
	  } else {
	    $excerptLen = (int)$excerptLen;
	  }
	} else {
	  $listType = 'ul';
	  $listElement = 'li';
	}
	printf('<%s%s>', $listType, ($class != '') ? " class=\"$class\"" : '');
        foreach ($feed->entry as $item) {
            if ($loopcount < $display) {
                printf('<%s><a href="%s" rel="nofollow">%s - %s</a></%1$s>',
                       $listElement,
                       htmlspecialchars($item->link['href']),
                       $item->title,
                       $item->source->title);
                $loopcount++;
                if ( !$excerpt ) continue;
                $content = ( !empty($item->summary) ? $item->summary : $item->content );
		$content = readerShared_excerptContent($content, $excerptLen);//defined below
                echo "<dd>$content...</dd>\n";
            }
        }
        echo("</$listType>");
    }
}    

if ( !function_exists('readerShared_excerptContent') ) {
    function readerShared_excerptContent($s, $len=0) {
        /* check if there is a quote */
      if ( preg_match('!^<blockquote>Shared by\\s*\\w+\\s*$\\s*<br>\\s*$\\s*(.*)</blockquote>\\s*$!msu', // m allows $ to match eol; s allows . to match eol; and u is for ungreedy.
		      $s, $matches) ) {
	$s = substr($s, strlen($matches[0]));
	/*$comment = strip_tags($matches[1]);*/ // unnecessary since complete
        $comment = $matches[1];
      } else {
	$comment = '';
      }
         
        // easiest to strip tags and then truncate then to truncate, strip tags,
        // and worry about whether there is an unfinished tag "<p", a "<" inside
        // an attribute (<foo onmouseover="this.innerHTML += '<p>foo</p)
        // and other such worries.  Even though stripping all tags before
        // truncating is more work than strip_tags should have to do
        $s = strip_tags($s);
        if ( $len > 0 ) {
	  $s = substr($s, 0, $len);
	}
      return strlen($comment) ?"<blockquote>$comment</blockquote>\n$s" : $s;
    }
}

if ( !function_exists('readerShared_Menu') ) {
    function readerShared_Menu() {
        add_options_page('Google Reader Shared Options', 'Google Reader', 9, basename(__FILE__));
    }
}

add_action('admin_menu', 'readerShared_Menu');
?>

Name it reader_shared.php and install it as one installs plugins, then go to the options page to configure it. Also, if you copy and paste it, make sure there are no extra spaces or empty lines at the end of the file. I have attempted to make it self-explanatory.

Unfortunately--I hope I can blame this on a style sheet over which I have no control--the options page is now quite ugly: the check box is too big and is not obviously associated with its "Include Content Excerpt" label. This seems to have gone away. But I'll let you see an old screen shot anyway.


This page was last updated Sunday, July 13, 2008.

Valid XHTML 1.0! Valid CSS!