# - HTTP Navigator 2 by Keyvan Minoukadeh # - Ultra-Liberal Feed Finder by Mark Pilgrim # according to the terms of the GNU General Public License. # # INSTALLATION: see README.text or # # USAGE: once FeedWordPress is installed, you manage just about everything from # the WordPress Dashboard, under Options --> Syndication or Links --> Syndicated # To ensure that fresh content is added as it becomes available, get your # contributors to put your XML-RPC URI (if WordPress is installed at # , XML-RPC requests should be sent to # ), or see `update-feeds.php` # -- Don't change these unless you know what you're doing... define ('RPC_MAGIC', 'tag:radgeek.com/projects/feedwordpress/'); define ('FEEDWORDPRESS_VERSION', '0.98'); define ('DEFAULT_SYNDICATION_CATEGORY', 'Contributors'); define ('FEEDWORDPRESS_CAT_SEPARATOR_PATTERN', '/[:\n]/'); define ('FEEDWORDPRESS_CAT_SEPARATOR', "\n"); define ('FEEDVALIDATOR_URI', 'http://feedvalidator.org/check.cgi'); // Note that the rss-functions.php that comes prepackaged with WordPress is // old & busted. For the new hotness, drop a copy of rss-functions.php from // this archive into wp-includes/rss-functions.php require_once (ABSPATH . WPINC . '/rss-functions.php'); if (isset($wp_db_version) and $wp_db_version >= 2966) : require_once (ABSPATH . WPINC . '/registration-functions.php'); endif; // Is this being loaded from within WordPress 1.5 or later? if (isset($wp_version) and $wp_version >= 1.5): $fwp_db_version = get_settings('feedwordpress_version'); $feedwordpress_needs_upgrade = false; // innocent until proven guilty if (!$fwp_db_version or $fwp_db_version < FEEDWORDPRESS_VERSION) : // check to see whether this is a fresh install or an upgrade $syn = $wpdb->get_col(" SELECT post_id FROM $wpdb->postmeta WHERE meta_key = 'syndication_feed' "); if (count($syn) > 0) : // contains at least one syndicated post $feedwordpress_needs_upgrade = true; else : // fresh install; brand it as ours update_option('feedwordpress_version', FEEDWORDPRESS_VERSION); if (!get_settings('feedwordpress_rpc_secret')) : update_option('feedwordpress_rpc_secret', substr(md5(uniqid(microtime())), 0, 6)); endif; endif; endif; if (!$feedwordpress_needs_upgrade) : // only work if the conditions are safe! # Syndicated items are generally received in output-ready (X)HTML and # should not be folded, crumpled, mutilated, or spindled by WordPress # formatting filters. But we don't want to interfere with filters for # any locally-authored posts, either. # # What WordPress should really have is a way for upstream filters to # stop downstream filters from running at all. Since it doesn't, and # since a downstream filter can't access the original copy of the text # that is being filtered, what we will do here is (1) save a copy of the # original text upstream, before any other filters run, and then (2) # retrieve that copy downstream, after all the other filters run, *if* # this is a syndicated post add_filter('the_content', 'feedwordpress_preserve_syndicated_content', -10000); add_filter('the_content', 'feedwordpress_restore_syndicated_content', 10000); # Filter in original permalinks if the user wants that add_filter('post_link', 'syndication_permalink', 1); # Admin menu add_action('admin_menu', 'fwp_add_pages'); # Inbound XML-RPC update methods add_filter('xmlrpc_methods', 'feedwordpress_xmlrpc_hook'); # Outbound XML-RPC ping reform remove_action('publish_post', 'generic_ping'); add_action('publish_post', 'fwp_catch_ping'); # Hook in logging functions only if the logging option is ON $update_logging = get_settings('feedwordpress_update_logging'); if ($update_logging == 'yes') : add_action('post_syndicated_item', 'log_feedwordpress_post', 100); add_action('update_syndicated_item', 'log_feedwordpress_update_post', 100); add_action('feedwordpress_update', 'log_feedwordpress_update_feeds', 100); add_action('feedwordpress_check_feed', 'log_feedwordpress_check_feed', 100); add_action('feedwordpress_update_complete', 'log_feedwordpress_update_complete', 100); endif; else : # Hook in the menus, which will just point to the upgrade interface add_action('admin_menu', 'fwp_add_pages'); endif; // if (!$feedwordpress_needs_upgrade) endif; ################################################################################ ## LOGGING FUNCTIONS: log status updates to error_log if you want it ########### ################################################################################ function log_feedwordpress_post ($id) { $post = wp_get_single_post($id); error_log("[".date('Y-m-d H:i:s')."][feedwordpress] posted " ."'{$post->post_title}' ({$post->post_date})"); } function log_feedwordpress_update_post ($id) { $post = wp_get_single_post($id); error_log("[".date('Y-m-d H:i:s')."][feedwordpress] updated " ."'{$post->post_title}' ({$post->post_date})" ." (as of {$post->post_modified})"); } function log_feedwordpress_update_feeds ($uri) { error_log("[".date('Y-m-d H:i:s')."][feedwordpress] update('$uri')"); } function log_feedwordpress_check_feed ($feed) { $uri = $feed['link/uri']; $name = $feed['link/name']; error_log("[".date('Y-m-d H:i:s')."][feedwordpress] Examining $name <$uri>"); } function log_feedwordpress_update_complete ($delta) { $mesg = array(); if (isset($delta['new'])) $mesg[] = 'added '.$delta['new'].' new posts'; if (isset($delta['updated'])) $mesg[] = 'updated '.$delta['updated'].' existing posts'; if (empty($mesg)) $mesg[] = 'nothing changed'; error_log("[".date('Y-m-d H:i:s')."][feedwordpress] " .(is_null($delta) ? "Error: I don't syndicate that URI" : implode(' and ', $mesg))); } ################################################################################ ## TEMPLATE API: functions to make your templates syndication-aware ############ ################################################################################ function is_syndicated () { return (strlen(get_syndication_feed_id()) > 0); } function the_syndication_source_link () { echo get_syndication_source_link(); } function get_syndication_source_link () { list($n) = get_post_custom_values('syndication_source_uri'); return $n; } function get_syndication_source () { list($n) = get_post_custom_values('syndication_source'); return $n; } function the_syndication_source () { echo get_syndication_source(); } function get_syndication_feed () { list($u) = get_post_custom_values('syndication_feed'); return $u; } function the_syndication_feed () { echo get_syndication_feed (); } function get_syndication_feed_id () { list($u) = get_post_custom_values('syndication_feed_id'); return $u; } function the_syndication_feed_id () { echo get_syndication_feed_id(); } $feedwordpress_linkcache = array (); // only load links from database once function get_feed_meta ($key) { global $wpdb, $feedwordpress_linkcache; $feed_id = get_syndication_feed_id(); $ret = NULL; if (strlen($feed_id) > 0): if (isset($feedwordpress_linkcache[$feed_id])) : $result = $feedwordpress_linkcache[$feed_id]; else : $result = $wpdb->get_row(" SELECT * FROM $wpdb->links WHERE (link_id = '".$wpdb->escape($feed_id)."')" ); $feedwordpress_linkcache[$feed_id] = $result; endif; $meta = FeedWordPress::notes_to_settings($result->link_notes); $ret = $meta[$key]; endif; /* if */ return $ret; } function get_syndication_permalink () { list($u) = get_post_custom_values('syndication_permalink'); return $u; } function the_syndication_permalink () { echo get_syndication_permalink(); } ################################################################################ ## FILTERS: syndication-aware handling of post data for templates and feeds #### ################################################################################ $feedwordpress_the_syndicated_content = NULL; function feedwordpress_preserve_syndicated_content ($text) { global $feedwordpress_the_syndicated_content; if ( is_syndicated() ) : $feedwordpress_the_syndicated_content = $text; else : $feedwordpress_the_syndicated_content = NULL; endif; return $text; } function feedwordpress_restore_syndicated_content ($text) { global $feedwordpress_the_syndicated_content; if ( !is_null($feedwordpress_the_syndicated_content) ) : $text = $feedwordpress_the_syndicated_content; endif; return $text; } function syndication_permalink ($permalink = '') { if (get_settings('feedwordpress_munge_permalink') != 'no'): $uri = get_syndication_permalink(); return ((strlen($uri) > 0) ? $uri : $permalink); else: return $permalink; endif; } // function syndication_permalink () ################################################################################ ## UPGRADE INTERFACE: Have users upgrade DB from older versions of FWP ######### ################################################################################ function fwp_upgrade_page () { if (isset($_POST['action']) and $_POST['action']=='Upgrade') : $ver = get_settings('feedwordpress_version'); if (get_settings('feedwordpress_version') != FEEDWORDPRESS_VERSION) : echo "
\n"; echo "

Upgrading FeedWordPress...

"; $feedwordpress =& new FeedWordPress; $feedwordpress->upgrade_database(); echo "

Done! Upgraded database to version ".FEEDWORDPRESS_VERSION.".

\n"; echo "
\n"; echo "
"; echo "
\n"; echo "
\n"; return; else : echo "

Already at version ".FEEDWORDPRESS_VERSION."!

"; endif; endif; ?>

Upgrade FeedWordPress

It appears that you have installed FeedWordPress as an upgrade to an existing installation of FeedWordPress. That's no problem, but you will need to take a minute out first to upgrade your database: some necessarily changes in how the software keeps track of posts and feeds will cause problems such as duplicate posts and broken templates if we were to continue without the upgrade.

Note that most of FeedWordPress's functionality is temporarily disabled until we have successfully completed the upgrade. Everything should begin working as normal again once the upgrade is complete. There's extraordinarily little chance of any damage as the result of the upgrade, but if you're paranoid like me you may want to back up your database before you proceed.

This may take several minutes for a large installation.

get_col( "SELECT cat_name FROM $wpdb->categories WHERE cat_ID IN {$cat_set} "); endif; if (!empty($cats)) : update_option('feedwordpress_syndication_cats', implode("\n", $cats)); else : delete_option('feedwordpress_syndication_cats'); endif; if (isset($_REQUEST['comment_status']) and ($_REQUEST['comment_status'] == 'open')) : update_option('feedwordpress_syndicated_comment_status', 'open'); else : update_option('feedwordpress_syndicated_comment_status', 'closed'); endif; if (isset($_REQUEST['ping_status']) and ($_REQUEST['ping_status'] == 'open')) : update_option('feedwordpress_syndicated_ping_status', 'open'); else : update_option('feedwordpress_syndicated_ping_status', 'closed'); endif; if (isset($_REQUEST['hardcode_name']) and ($_REQUEST['hardcode_name'] == 'no')) : update_option('feedwordpress_hardcode_name', 'no'); else : update_option('feedwordpress_hardcode_name', 'yes'); endif; if (isset($_REQUEST['hardcode_description']) and ($_REQUEST['hardcode_description'] == 'no')) : update_option('feedwordpress_hardcode_description', 'no'); else : update_option('feedwordpress_hardcode_description', 'yes'); endif; if (isset($_REQUEST['hardcode_url']) and ($_REQUEST['hardcode_url'] == 'no')) : update_option('feedwordpress_hardcode_url', 'no'); else : update_option('feedwordpress_hardcode_url', 'yes'); endif; ?>

'','default' => '','filter' => ''); $ua = FeedWordPress::on_unfamiliar('author'); if (is_string($ua) and array_key_exists($ua, $unfamiliar_author)) : $unfamiliar_author[$ua] = ' checked="checked"'; endif; $unfamiliar_category = array ('create'=>'','default'=>'','filter'=>''); $uc = FeedWordPress::on_unfamiliar('category'); if (is_string($uc) and array_key_exists($uc, $unfamiliar_category)) : $unfamiliar_category[$uc] = ' checked="checked"'; endif; $results = $wpdb->get_results("SELECT cat_id, cat_name, auto_toggle FROM $wpdb->linkcategories ORDER BY cat_id"); $cats = get_settings('feedwordpress_syndication_cats'); $dogs = get_nested_categories(-1, 0); $cats = array_map('strtolower', array_map('trim', preg_split(FEEDWORDPRESS_CAT_SEPARATOR_PATTERN, $cats) )); foreach ($dogs as $tag => $dog) : if (in_array(strtolower(trim($dog['cat_name'])), $cats)) : $dogs[$tag]['checked'] = true; endif; endforeach; ?>

Syndication Options

Syndicated Feeds
Syndicate links in category: "; foreach ($results as $row) { echo "\n\t\n"; } echo "\n\n"; ?>
Update live from feed:
  • /> Contributor name (feed title)
  • /> Contributor description (feed tagline)
  • /> Homepage (feed link)
Syndicated Posts
Categories

Place all syndicated posts under...

Publication:
Comments:
Trackback and Pingback:
Unfamiliar authors:
Unfamiliar categories:
Permalinks point to:
Back-end Options
XML-RPC update secret word:
Write update notices to PHP logs:

Syndicate a new site:

Syndicated Sites

> link_rss) > 0): $caption='Switch Feed'; $uri_bits = parse_url($link->link_rss); $uri_bits['host'] = preg_replace('/^www\./i', '', $uri_bits['host']); $display_uri = (isset($uri_bits['user'])?$uri_bits['user'].'@':'') .(isset($uri_bits['host'])?$uri_bits['host']:'') .(isset($uri_bits['port'])?':'.$uri_bits['port']:'') .(isset($uri_bits['path'])?$uri_bits['path']:'') .(isset($uri_bits['query'])?'?'.$uri_bits['query']:''); if (strlen($display_uri) > 32) : $display_uri = substr($display_uri, 0, 32).'…'; endif; ?> user_level <= $user_level)): ?>  \n"; endif; echo "\n\t"; endforeach; else: ?>

There are no websites currently listed for syndication.

link_name)?>

no feed assigned

   

Manage Multiple Links

get_row("SELECT * FROM $wpdb->links WHERE link_id='".$wpdb->escape($link_id)."'"); if (is_object($link)): if (is_null($lookup)) $lookup = $link->link_url; $name = wp_specialchars($link->link_name); else: die (__("Cheatin' uh ?")); endif; else: $name = "New Syndicated Feed"; $link_id = 0; endif; ?>

Feed Finder:

find(); if (count($feeds) > 0): foreach ($feeds as $key => $f): $rss = fetch_rss($f); if ($rss): $feed_title = isset($rss->channel['title'])?$rss->channel['title']:$rss->channel['link']; $feed_link = isset($rss->channel['link'])?$rss->channel['link']:''; ?>
feed_type?> feed_version?> feed
items) > 0): ?> items[0]; ?>

Sample Item

  • Title:
  • Date:

No Items

FeedWordPress found no posts on this feed.

Feed Information

  • Website: Unknown':$feed_title?>
  • Feed URI: (→)
  • Encoding: encoding)?wp_specialchars($rss->encoding):"Unknown"?>
  • Description: channel['description'])?wp_specialchars($rss->channel['description']):"Unknown"?>
no feed found

"; endif; ?>

Use another feed

query(" INSERT INTO $wpdb->links SET link_name = '".$wpdb->escape($_REQUEST['feed_title'])."', link_url = '".$wpdb->escape($_REQUEST['feed_link'])."', link_category = '".$wpdb->escape($cat_id)."', link_rss = '".$wpdb->escape($_REQUEST['feed'])."' "); if ($result): ?>

has been added as a contributing site, using the newsfeed at <>.

There was a problem adding the newsfeed. [SQL: ]

query(" UPDATE $wpdb->links SET link_rss = '".$wpdb->escape($_REQUEST['feed'])."' WHERE link_id = '".$wpdb->escape($_REQUEST['link_id'])."' "); if ($result): $result = $wpdb->get_row(" SELECT link_name, link_url FROM $wpdb->links WHERE link_id = '".$wpdb->escape($_REQUEST['link_id'])."' "); ?>

Feed for link_name)?> updated to <>.

Nothing was changed.

get_row(" SELECT * FROM $wpdb->links WHERE link_id = $link_id "); if ($row) : if (isset($_POST['save'])) : $alter = array (); $meta = FeedWordPress::notes_to_settings($row->link_notes); if (isset($meta['cats'])): $meta['cats'] = preg_split(FEEDWORDPRESS_CAT_SEPARATOR_PATTERN, $meta['cats']); endif; // custom feed settings first foreach ($_POST['notes'] as $mn) : $mn['key0'] = trim($mn['key0']); $mn['key1'] = trim($mn['key1']); if (preg_match("\007^((" .implode(')|(',$special_settings) ."))$\007i", $mn['key1'])) : $mn['key1'] = 'user/'.$mn['key1']; endif; if (strlen($mn['key0']) > 0) : unset($meta[$mn['key0']]); // out with the old endif; if (($mn['action']=='update') and (strlen($mn['key1']) > 0)) : $meta[$mn['key1']] = $mn['value']; // in with the new endif; endforeach; // now stuff through the web form // hardcoded feed info if (isset($_POST['hardcode_name'])) : $meta['hardcode name'] = $_POST['hardcode_name']; if (FeedWordPress::affirmative($meta, 'hardcode name')) : $alter[] = "link_name = '".$wpdb->escape($_POST['name'])."'"; endif; endif; if (isset($_POST['hardcode_description'])) : $meta['hardcode description'] = $_POST['hardcode_description']; if (FeedWordPress::affirmative($meta, 'hardcode description')) : $alter[] = "link_description = '".$wpdb->escape($_POST['description'])."'"; endif; endif; if (isset($_POST['hardcode_url'])) : $meta['hardcode url'] = $_POST['hardcode_url']; if (FeedWordPress::affirmative($meta, 'hardcode url')) : $alter[] = "link_url = '".$wpdb->escape($_POST['linkurl'])."'"; endif; endif; // Update scheduling if (isset($_POST['update_schedule'])) : $meta['update/hold'] = $_POST['update_schedule']; endif; // Categories if (isset($_POST['post_category'])) : $cat_set = "(".implode(",", $_POST['post_category']).")"; $meta['cats'] = $wpdb->get_col( "SELECT cat_name FROM $wpdb->categories WHERE cat_ID IN {$cat_set} "); if (count($meta['cats']) == 0) : unset($meta['cats']); endif; else : unset($meta['cats']); endif; // Post status, comment status, ping status foreach (array('post', 'comment', 'ping') as $what) : $sfield = "feed_{$what}_status"; if (isset($_POST[$sfield])) : if ($_POST[$sfield]=='site-default') : unset($meta["{$what} status"]); else : $meta["{$what} status"] = $_POST[$sfield]; endif; endif; endforeach; // Unfamiliar author, unfamiliar categories foreach (array("author", "category") as $what) : $sfield = "unfamiliar_{$what}"; if (isset($_POST[$sfield])) : if ($_POST[$sfield]=='site-default') : unset($meta["unfamiliar {$what}"]); else : $meta["unfamiliar {$what}"] = $_POST[$sfield]; endif; endif; endforeach; if (is_array($meta['cats'])) : $meta['cats'] = implode(FEEDWORDPRESS_CAT_SEPARATOR, $meta['cats']); endif; $notes = ''; foreach ($meta as $key => $value) : $notes .= $key . ": ". addcslashes($value, "\0..\37") . "\n"; endforeach; $alter[] = "link_notes = '".$wpdb->escape($notes)."'"; $alter_set = implode(", ", $alter); // issue update query $result = $wpdb->query(" UPDATE $wpdb->links SET $alter_set WHERE link_id='$link_id' "); $updated_link = true; // reload link information from DB $row = $wpdb->get_row(" SELECT * FROM $wpdb->links WHERE link_id = $link_id "); else : $updated_link = false; endif; $link_url = wp_specialchars($row->link_url, 1); $link_name = wp_specialchars($row->link_name, 1); $link_image = $row->link_image; $link_target = $row->link_target; $link_category = $row->link_category; $link_description = wp_specialchars($row->link_description); $link_visible = $row->link_visible; $link_rating = $row->link_rating; $link_rel = $row->link_rel; $link_notes = wp_specialchars($row->link_notes); $link_rss_uri = wp_specialchars($row->link_rss); $meta = FeedWordPress::notes_to_settings($row->link_notes); $status['post'] = array('publish' => '', 'private' => '', 'draft' => '', 'site-default' => ''); $status['comment'] = array('open' => '', 'closed' => '', 'site-default' => ''); $status['ping'] = array('open' => '', 'closed' => '', 'site-default' => ''); foreach (array('post', 'comment', 'ping') as $what) : if (isset($meta["{$what} status"])) : $status[$what][$meta["{$what} status"]] = ' checked="checked"'; else : $status[$what]['site-default'] = ' checked="checked"'; endif; endforeach; $unfamiliar['author'] = array ('create' => '','default' => '','filter' => ''); $unfamiliar['category'] = array ('create'=>'','default'=>'','filter'=>''); foreach (array('author', 'category') as $what) : if (is_string($meta["unfamiliar {$what}"]) and array_key_exists($meta["unfamiliar {$what}"], $unfamiliar[$what])) : $key = $meta["unfamiliar {$what}"]; else: $key = 'site-default'; endif; $unfamiliar[$what][$key] = ' checked="checked"'; endforeach; $dogs = get_nested_categories(-1, 0); $cats = array_map('strtolower', array_map('trim', preg_split(FEEDWORDPRESS_CAT_SEPARATOR_PATTERN, $meta['cats']) )); foreach ($dogs as $tag => $dog) : if (in_array(strtolower(trim($dog['cat_name'])), $cats)) : $dogs[$tag]['checked'] = true; endif; endforeach; else : die( __('Link not found.') ); endif; ?>

Syndicated feed settings updated.

Edit a syndicated feed:

Basics
→
:
Next update:

Syndicated Posts
Categories

Place all syndicated posts from this feed under...

Publication:
Comments:
Trackback and Pingback:

Advanced Feed Options
Unfamiliar authors:
Unfamiliar categories:

Custom Settings (for use in templates)
$value) : if (!preg_match("\007^((".implode(')|(', $special_settings)."))$\007i", $key)) : ?>
Key Value Action
add new setting...

$what) : $do_it[$what][] = $link_id; endforeach; $alter = array(); if (count($do_it['hide']) > 0) : $hidem = "(".implode(', ', $do_it['hide']).")"; $alter[] = " UPDATE $wpdb->links SET link_visible = 'N' WHERE link_id IN {$hidem} "; endif; if (count($do_it['nuke']) > 0) : $nukem = "(".implode(', ', $do_it['nuke']).")"; // Make a list of the items syndicated from this feed... $post_ids = $wpdb->get_col(" SELECT post_id FROM $wpdb->postmeta WHERE meta_key = 'syndication_feed_id' AND meta_value IN {$nukem} "); // ... and kill them all if (count($post_ids) > 0) : foreach ($post_ids as $post_id) : wp_delete_post($post_id); endforeach; endif; $alter[] = " DELETE FROM $wpdb->links WHERE link_id IN {$nukem} "; endif; if (count($do_it['delete']) > 0) : $deletem = "(".implode(', ', $do_it['delete']).")"; // Make the items syndicated from this feed appear to be locally-authored $alter[] = " DELETE FROM $wpdb->postmeta WHERE meta_key = 'syndication_feed_id' AND meta_value IN {$deletem} "; // ... and delete the links themselves. $alter[] = " DELETE FROM $wpdb->links WHERE link_id IN {$deletem} "; endif; $errs = array(); $success = array (); foreach ($alter as $sql) : $result = $wpdb->query($sql); if (!$result): $errs[] = mysql_error(); endif; endforeach; if (count($alter) > 0) : echo "
\n"; if (count($errs) > 0) : echo "There were some problems processing your "; echo "unsubscribe request. [SQL: ".implode('; ', $errs)."]"; else : echo "Your unsubscribe request(s) have been processed."; endif; echo "
\n"; endif; return true; // Continue on to Syndicated Sites listing else : $targets = $wpdb->get_results(" SELECT * FROM $wpdb->links WHERE link_id IN (".implode(",",$link_ids).") "); ?>

Unsubscribe from Syndicated Links:

link_url, 1); $link_name = wp_specialchars($link->link_name, 1); $link_description = wp_specialchars($link->link_description); $link_rss = wp_specialchars($link->link_rss); $meta = FeedWordPress::notes_to_settings($link->link_notes); ?>
Subscription :
link_rss) > 0): $sec = FeedWordPress::notes_to_settings($link->link_notes); $sec['link/uri'] = $link->link_rss; $sec['link/name'] = $link->link_name; $sec['link/id'] = $link->link_id; // `hardcode categories` is deprecated in favor // of `unfamiliar categories` if ( FeedWordPress::affirmative($sec, 'hardcode categories') and !isset($sec['unfamiliar categories']) ) : $sec['unfamiliar categories'] = 'default'; endif; if (isset($sec['cats'])): $sec['cats'] = preg_split(FEEDWORDPRESS_CAT_SEPARATOR_PATTERN, $sec['cats']); endif; $feeds[] = $sec; endif; endforeach; endif; $this->feeds = $feeds; } // FeedWordPress::FeedWordPress () # function notes_to_settings (): Convert WordPress Link Notes to array # of feed-level settings # # Arguments: # ---------- # * $link_notes (string): the text from the Link Notes section of a link # # Returns: # -------- # An associative array of settings stored in the Link Notes field. (For # the `unfamiliar authors` setting, for example, simply look up the # value of $meta['unfamiliar authors'], if $meta contains the value # returned by `notes_to_settings()`. # # Values in FeedWordPress feed settings are escaped using C-style # slashes. The escaped characters will already have been processed and # converted in the returned array. function notes_to_settings ($link_notes) { $notes = explode("\n", $link_notes); $sec = array (); foreach ($notes as $note): list($key, $value) = explode(": ", $note, 2); if (strlen($key) > 0) : // Unescape and trim() off the whitespace. // Thanks to Ray Lischner for pointing out the // need to trim off whitespace. $sec[$key] = stripcslashes (trim($value)); endif; endforeach; return $sec; } // FeedWordPress::notes_to_settings () # function update (): polls for updates on one or more Contributor feeds # # Arguments: # ---------- # * $uri (string): either the URI of the feed to poll, the URI of the # website (human-readable link) whose feed you want to poll, or a # "magic" tag: URI composed of the URI in the constant `RPC_MAGIC` # and a "secret word" set in the FeedWordPress Options. # # If the "magic" URI is used, then FeedWordPress will poll any # feeds that are ready for polling. It will not poll feeds that are # marked as "Invisible" Links (signifying that the subscription has # been de-activated), or feeds that are not yet stale according to # their TTL setting (which is either set in the feed, or else # set randomly within a window of 30 minutes - 2 hours). # # Returns: # -------- # * Normally returns an associative array, with 'new' => the number # of new posts added during the update, and 'updated' => the number # of old posts that were updated during the update. If both numbers # are zero, there was no change since the last poll on that URI. # # * Returns NULL if URI it was passed was not a URI that this # installation of FeedWordPress syndicates (the most common cause # of this error is attempts to poll all feeds, lacking, or using # an incorrect, "secret word." # # Effects: # -------- # * One or more feeds are polled for updates # # * If the feed Link does not have a hardcoded name set, its Link # Name is synchronized with the feed's title element # # * If the feed Link does not have a hardcoded URI set, its Link URI # is synchronized with the feed's human-readable link element # # * If the feed Link does not have a hardcoded description set, its # Link Description is synchronized with the feed's description, # tagline, or subtitle element. # # * The time of polling is recorded in the feed's settings, and the # TTL (time until the feed is next available for polling) is set # either from the feed (if it is supplied in the ttl or syndication # module elements) or else from a randomly-generated time window # (between 30 minutes and 2 hours). # # * New posts from the polled feed are added to the WordPress store. # # * Updates to existing posts since the last poll are mirrored in the # WordPress store. # function update ($uri) { global $wpdb; $uri = trim($uri); if ($GLOBALS['feedwordpress_needs_upgrade']) : // Will make duplicate posts if we don't hold off return NULL; endif; do_action('feedwordpress_update', $uri); // Secret voodoo tag: URI for updating *everything*. $secret = RPC_MAGIC.FeedWordPress::rpc_secret(); fwp_hold_pings(); // Only send out one ping for the whole to-do // Loop through and check for new posts $delta = NULL; foreach ($this->feeds as $feed) : $pinged_that = in_array($uri, array($secret, $feed['link/uri'], $feed['feed/link'])); if ($uri != $secret) : // A site-specific ping always updates $timely = true; elseif (isset($feed['update/hold']) and ($feed['update/hold']=='ping')) : $timely = false; elseif (isset($feed['update/hold']) and ($feed['update/hold']=='next')) : $timely = true; elseif (!isset($feed['update/ttl']) or !isset($feed['update/last'])) : $timely = true; else : $after = ((int) $feed['update/last']) +((int) $feed['update/ttl'] * 60); $timely = (time() >= $after); endif; if ($pinged_that and is_null($delta)) : // If at least one feed was hit for updating... $delta = array('new' => 0, 'updated' => 0); // ... don't return error condition endif; if ($pinged_that and $timely) : do_action('feedwordpress_check_feed', array($feed)); $added = $this->feed2wp($wpdb, $feed); if (isset($added['new'])) : $delta['new'] += $added['new']; endif; if (isset($added['updated'])) : $delta['updated'] += $added['updated']; endif; endif; endforeach; do_action('feedwordpress_update_complete', array($delta)); fwp_release_pings(); // Now that we're done, send the one ping return $delta; } function feed2wp ($wpdb, $f) { $feed = fetch_rss($f['link/uri']); $new_count = array('new' => 0, 'updated' => 0); $this->update_feed($wpdb, $feed->channel, $f); if (is_array($feed->items)) : foreach ($feed->items as $item) : $post = $this->item_to_post($wpdb, $item, $feed, $f); if (!is_null($post)) : $new = $this->add_post($wpdb, $post); if ( $new !== false ) $new_count[$new]++; endif; endforeach; endif; return $new_count; } // function feed2wp () // FeedWordPress::flatten_array (): flatten an array. Useful for // hierarchical and namespaced elements. // // Given an array which may contain array or object elements in it, // return a "flattened" array: a one-dimensional array of scalars // containing each of the scalar elements contained within the array // structure. Thus, for example, if $a['b']['c']['d'] == 'e', then the // returned array for FeedWordPress::flatten_array($a) will contain a key // $a['feed/b/c/d'] with value 'e'. function flatten_array ($arr, $prefix = 'feed/', $separator = '/') { $ret = array (); if (is_array($arr)) : foreach ($arr as $key => $value) : if (is_scalar($value)) : $ret[$prefix.$key] = $value; else : $ret = array_merge($ret, $this->flatten_array($value, $prefix.$key.$separator, $separator)); endif; endforeach; endif; return $ret; } // function FeedWordPress::flatten_array () function resolve_relative_uri ($matches) { return $matches[1].Relative_URI::resolve($matches[2], $this->_base).$matches[3]; } // function FeedWordPress::resolve_relative_uri () function hardcode ($what, $f) { $default = get_settings("feedwordpress_hardcode_$what"); if ( $default === 'yes' ) : // If the default is to hardcode, then we want the // negation of negative(): TRUE by default and FALSE if // the setting is explicitly "no" $ret = !FeedWordPress::negative($f, "hardcode $what"); else : // If the default is NOT to hardcode, then we want // affirmative(): FALSE by default and TRUE if the // setting is explicitly "yes" $ret = FeedWordPress::affirmative($f, "hardcode $what"); endif; return $ret; } function syndicated_status ($what, $f, $default) { global $wpdb; $ret = get_settings("feedwordpress_syndicated_{$what}_status"); if ( isset($f["$what status"]) ) : $ret = $f["$what status"]; elseif (!$ret) : $ret = $default; endif; return $wpdb->escape(trim(strtolower($ret))); } function negative ($f, $setting) { $nego = array ('n', 'no', 'f', 'false'); return (isset($f[$setting]) and in_array(strtolower($f[$setting]), $nego)); } function affirmative ($f, $setting) { $affirmo = array ('y', 'yes', 't', 'true', 1); return (isset($f[$setting]) and in_array(strtolower($f[$setting]), $affirmo)); } function feed_ttl ($channel) { if (isset($channel['ttl'])) : // "ttl stands for time to live. It's a number of // minutes that indicates how long a channel can be // cached before refreshing from the source." // $ret = $channel['ttl']; elseif (isset($channel['sy']['updatefrequency']) or isset($channel['sy']['updateperiod'])) : $period_minutes = array ( 'hourly' => 60, /* minutes in an hour */ 'daily' => 1440, /* minutes in a day */ 'weekly' => 10080, /* minutes in a week */ 'monthly' => 43200, /* minutes in a month */ 'yearly' => 525600, /* minutes in a year */ ); // "sy:updatePeriod: Describes the period over which the // channel format is updated. Acceptable values are: // hourly, daily, weekly, monthly, yearly. If omitted, // daily is assumed." if (isset($channel['sy']['updateperiod'])) : $period = $channel['sy']['updateperiod']; else : $period = 'daily'; endif; // "sy:updateFrequency: Used to describe the frequency // of updates in relation to the update period. A // positive integer indicates how many times in that // period the channel is updated. ... If omitted a value // of 1 is assumed." if (isset($channel['sy']['updatefrequency'])) : $freq = (int) $channel['sy']['updatefrequency']; else : $freq = 1; endif; $ret = (int) ($period_minutes[$period] / $freq); else : $ret = NULL; endif; return $ret; } function update_feed ($wpdb, $channel, $f) { $link_id = $f['link/id']; if (!isset($channel['id'])) : $channel['id'] = $f['link/uri']; endif; $update = array(); if (!FeedWordPress::hardcode('url', $f) and isset($channel['link'])) : $update[] = "link_url = '".$wpdb->escape($channel['link'])."'"; endif; if (!FeedWordPress::hardcode('name', $f) and isset($channel['title'])) : $update[] = "link_name = '".$wpdb->escape($channel['title'])."'"; endif; if (!FeedWordPress::hardcode('description', $f)) : if (isset($channel['tagline'])) : $update[] = "link_description = '".$wpdb->escape($channel['tagline'])."'"; elseif (isset($channel['description'])) : $update[] = "link_description = '".$wpdb->escape($channel['description'])."'"; endif; endif; if (is_array($f['cats'])) : $f['cats'] = implode(FEEDWORDPRESS_CAT_SEPARATOR, $f['cats']); endif; $f = array_merge($f, $this->flatten_array($channel)); $f['update/last'] = time(); $ttl = $this->feed_ttl($channel); if (!is_null($ttl)) : $f['update/ttl'] = $ttl; $f['update/timed'] = 'feed'; else : $f['update/ttl'] = rand(30, 120); // spread over time interval for staggered updates $f['update/timed'] = 'automatically'; endif; if (!isset($f['update/hold']) or $f['update/hold']!='ping') : $f['update/hold'] = 'scheduled'; endif; # -- A few things we don't want to save in the notes unset($f['link/id']); unset($f['link/uri']); unset($f['link/name']); unset($f['hardcode categories']); // Deprecated $notes = ''; foreach ($f as $key => $value) : $notes .= $key . ": ". addcslashes($value, "\0..\37") . "\n"; endforeach; $update[] = "link_notes = '".$wpdb->escape($notes)."'"; $update_set = implode(',', $update); // Update the properties of the link from the feed information $result = $wpdb->query(" UPDATE $wpdb->links SET $update_set WHERE link_id='$link_id' "); } // function FeedWordPress::update_feed () function date_created ($item) { if (isset($item['dc']['created'])) : $epoch = @parse_w3cdtf($item['dc']['created']); elseif (isset($item['dcterms']['created'])) : $epoch = @parse_w3cdtf($item['dcterms']['created']); elseif (isset($item['created'])): // Atom 0.3 $epoch = @parse_w3cdtf($item['created']); endif; return $epoch; } function guid ($item, $feed) { if (isset($item['id'])): // Atom 0.3 / 1.0 $guid = $item['id']; elseif (isset($item['atom']['id'])) : // Namespaced Atom $guid = $item['atom']['id']; elseif (isset($item['guid'])) : // RSS 2.0 $guid = $item['guid']; elseif (isset($item['dc']['identifier'])) : // yeah, right $guid = $item['dc']['identifier']; else : // The feed does not seem to have provided us with a // unique identifier, so we'll have to cobble together // a tag: URI that might work for us. The base of the // URI will be the host name of the feed source ... $bits = parse_url($feed['link/uri']); $guid = 'tag:'.$bits['host']; // If we have a date of creation, then we can use that // to uniquely identify the item. (On the other hand, if // the feed producer was consicentious enough to // generate dates of creation, she probably also was // conscientious enough to generate unique identifiers.) if (!is_null(FeedWordPress::date_created($item))) : $guid .= '://post.'.date('YmdHis', FeedWordPress::date_created($item)); // Otherwise, use both the URI of the item, *and* the // item's title. We have to use both because titles are // often not unique, and sometimes links aren't unique // either (e.g. Bitch (S)HITLIST, Mozilla Dot Org news, // some podcasts). But it's rare to have *both* the same // title *and* the same link for two different items. So // this is about the best we can do. else : $guid .= '://'.md5($item['link'].'/'.$item['title']); endif; endif; return $guid; } // item_to_post(): convert information from a single item from an // Atom/RSS feed to a post for WordPress's database. // // item_to_post() invokes the syndicated_item filter on each item it // receives. Filters should return either (a) the item unmodified, // (b) the item modified according to the rules of the filter, or // (c) NULL. A NULL item will not be posted into the database. // // N.B.: item_to_post and the syndicate_item filter really ought to have // *no* side effects on the WordPress database (that's why, for example, // we handle lookup/creation of numeric author and category IDs in // add_post()). If you want plugins that have side effects on the posts // database, you should probably hook into the action // post_syndicated_item function item_to_post($wpdb, $item, $rss, $f) { $channel = $rss->channel; $post = array(); // This is ugly as all hell. I'd like to use apply_filters()'s // alleged support for a variable argument count, but this seems // to have been broken in WordPress 1.5. It'll be fixed somehow // in WP 1.5.1, but I'm aiming at WP 1.5 compatibility across // the board here. // // Cf.: global $fwp_channel, $fwp_feedmeta; $fwp_channel = $channel; $fwp_feedmeta = $f; $item = apply_filters('syndicated_item', $item); // Filters can halt further processing by returning NULL if (is_null($item)) : $post = NULL; else : $post['post_title'] = $wpdb->escape($item['title']); $post['named']['author'] = array (); if (isset($item['author_name'])): $post['named']['author']['name'] = $item['author_name']; elseif (isset($item['dc']['creator'])): $post['named']['author']['name'] = $item['dc']['creator']; elseif (isset($item['dc']['contributor'])): $post['named']['author']['name'] = $item['dc']['contributor']; elseif (isset($channel['dc']['creator'])) : $post['named']['author']['name'] = $channel['dc']['creator']; elseif (isset($channel['dc']['contributor'])) : $post['named']['author']['name'] = $channel['dc']['contributor']; elseif (isset($channel['author_name'])) : $post['named']['author']['name'] = $channel['author_name']; elseif ($rss->is_rss() and isset($item['author'])) : // The author element in RSS is allegedly an // e-mail address, but lots of people don't use // it that way. So let's make of it what we can. $post['named']['author'] = parse_email_with_realname($item['author']); if (!isset($post['named']['author']['name'])) : if (isset($post['named']['author']['email'])) : $post['named']['author']['name'] = $post['named']['author']['email']; else : $post['named']['author']['name'] = $channel['title']; endif; endif; else : $post['named']['author']['name'] = $channel['title']; endif; if (isset($item['author_email'])): $post['named']['author']['email'] = $item['author_email']; elseif (isset($channel['author_email'])) : $post['named']['author']['email'] = $channel['author_email']; endif; if (isset($item['author_url'])): $post['named']['author']['uri'] = $item['author_url']; elseif (isset($channel['author_url'])) : $post['named']['author']['uri'] = $item['author_url']; else: $post['named']['author']['uri'] = $channel['link']; endif; // ... So far we just have an alphanumeric // representation of the author. We will look up (or // create) the numeric ID for the author in // FeedWordPress::add_post() # Identify content and sanitize it. # --------------------------------- if (isset($item['xhtml']['body'])) : $content = $item['xhtml']['body']; elseif (isset($item['xhtml']['div'])) : $content = $item['xhtml']['div']; elseif (isset($item['content']['encoded']) and $item['content']['encoded']): $content = $item['content']['encoded']; else: $content = $item['description']; endif; # Resolve relative URIs in post content # # N.B.: We *might* get screwed over by xml:base. But I don't see # any way to get that information out of MagpieRSS if it's # in the feed, and if it's in the content itself we'd have # to do yet more XML parsing to do things right. For now # this will have to do. $this->_base = $item['link']; // Reset the base for resolving relative URIs foreach ($this->uri_attrs as $pair): list($tag,$attr) = $pair; $content = preg_replace_callback ( ":(<$tag [^>]*$attr=\")([^\">]*)(\"[^>]*>):i", array(&$this,'resolve_relative_uri'), $content ); endforeach; # Sanitize problematic attributes foreach ($this->strip_attrs as $pair): list($tag,$attr) = $pair; $content = preg_replace ( ":(<$tag [^>]*)($attr=(\"[^\">]*\"|[^>\\s]+))([^>]*>):i", "\\1\\4", $content ); endforeach; # Identify and sanitize excerpt $excerpt = NULL; if ( isset($item['description']) and $item['description'] ) : $excerpt = $item['description']; elseif ( isset($content) and $content ) : $excerpt = strip_tags($content); if (strlen($excerpt) > 255) : $excerpt = substr($excerpt,0,252).'...'; endif; endif; $post['post_content'] = $wpdb->escape($content); if (!is_null($excerpt)): $post['post_excerpt'] = $wpdb->escape($excerpt); endif; # This is unneeded if wp_insert_post can be used. # --- cut here --- $post['post_name'] = sanitize_title($post['post_title']); # --- cut here --- # RSS is a fucking mess. Figure out whether we have a date in # dc:date, , , etc., and get it into Unix epoch # format for reformatting. If you can't find anything, use the # current time. if (isset($item['dc']['date'])): $post['epoch']['issued'] = parse_w3cdtf($item['dc']['date']); elseif (isset($item['dcterms']['issued'])) : $post['epoch']['issued'] = parse_w3cdtf($item['dcterms']['issued']); elseif (isset($item['published'])) : // Atom 1.0 $post['epoch']['issued'] = parse_w3cdtf($item['published']); elseif (isset($item['issued'])): // Atom 0.3 $post['epoch']['issued'] = parse_w3cdtf($item['issued']); elseif (isset($item['pubdate'])): // RSS 2.0 $post['epoch']['issued'] = strtotime($item['pubdate']); else: $post['epoch']['issued'] = time(); endif; # And again, for the created date $post['epoch']['created'] = FeedWordPress::date_created($item); # As far as I know, only atom currently has a reliable way to # specify when something was *modified* last if (isset($item['dc']['modified'])) : // Not really correct $post['epoch']['modified'] = @parse_w3cdtf($item['dc']['modified']); elseif (isset($item['dcterms']['modified'])) : // Dublin Core extensions $post['epoch']['modified'] = @parse_w3cdtf($item['dcterms']['modified']); elseif (isset($item['modified'])): // Atom 0.3 $post['epoch']['modified'] = @parse_w3cdtf($item['modified']); elseif (isset($item['updated'])): // Atom 1.0 $post['epoch']['modified'] = @parse_w3cdtf($item['updated']); else : // Fall back to issued / dc:date $post['epoch']['modified'] = $post['epoch']['issued']; endif; $post['post_date'] = date('Y-m-d H:i:s', $post['epoch']['issued']); $post['post_modified'] = date('Y-m-d H:i:s', $post['epoch']['modified']); $post['post_date_gmt'] = gmdate('Y-m-d H:i:s', $post['epoch']['issued']); $post['post_modified_gmt'] = gmdate('Y-m-d H:i:s', $post['epoch']['modified']); # Use feed-level preferences or the global default. $post['post_status'] = FeedWordPress::syndicated_status('post', $f, 'publish'); $post['comment_status'] = FeedWordPress::syndicated_status('comment', $f, 'closed'); $post['ping_status'] = FeedWordPress::syndicated_status('ping', $f, 'closed'); // Unique ID (hopefully a unique tag: URI); failing that, the permalink $post['guid'] = $wpdb->escape(FeedWordPress::guid($item, $f)); // RSS 2.0 / Atom 1.0 enclosure support if ( isset($item['enclosure#']) ) : for ($i = 1; $i <= $item['enclosure#']; $i++) : $eid = (($i > 1) ? "#{$id}" : ""); $post['meta']['enclosure'][] = $item["enclosure{$eid}@url"]."\n". $item["enclosure{$eid}@length"]."\n". $item["enclosure{$eid}@type"]; endfor; endif; // In case you want to point back to the blog this was syndicated from if (isset($channel['title'])) $post['meta']['syndication_source'] = $channel['title']; if (isset($channel['link'])) $post['meta']['syndication_source_uri'] = $channel['link']; // Store information on human-readable and machine-readable comment URIs if (isset($item['comments'])) : $post['meta']['rss:comments'] = $item['comments']; endif; if (isset($item['wfw']['commentrss'])) : $post['meta']['wfw:commentRSS'] = $item['wfw']['commentrss']; endif; // Store information to identify the feed that this came from $post['meta']['syndication_feed'] = $f['link/uri']; $post['meta']['syndication_feed_id'] = $f['link/id']; // In case you want to know the external permalink... $post['meta']['syndication_permalink'] = $item['link']; // Feed-by-feed options for author and category creation $post['named']['unfamiliar']['author'] = $f['unfamiliar author']; $post['named']['unfamiliar']['category'] = $f['unfamiliar categories']; // Categories: start with default categories $fc = get_settings("feedwordpress_syndication_cats"); if ($fc) : $post['named']['preset/category'] = explode("\n", $fc); else : $post['named']['preset/category'] = array(); endif; $post['named']['preset/category'] = array_merge($post['named']['preset/category'], $f['cats']); // Now add categories from the post, if we have 'em $post['named']['category'] = array(); if ( isset($item['category#']) ) : for ($i = 1; $i <= $item['category#']; $i++) : $cat_idx = (($i > 1) ? "#{$i}" : ""); $cat = $item["category{$cat_idx}"]; if ( strpos($f['link/uri'], 'del.icio.us') !== false ): $post['named']['category'] = array_merge($post['named']['category'], explode(' ', $cat)); else: $post['named']['category'][] = $cat; endif; endfor; endif; endif; return $post; } // function FeedWordPress::item_to_post () function add_post ($wpdb, $post) { $guid = $post['guid']; $result = $wpdb->get_row(" SELECT id, guid, UNIX_TIMESTAMP(post_modified) AS modified FROM $wpdb->posts WHERE guid='$guid' "); if (!$result) : $freshness = 2; // New content elseif ($post['epoch']['modified'] > $result->modified) : $freshness = 1; // Updated content else : $freshness = 0; endif; if ($freshness > 0) : # -- Look up, or create, numeric ID for author $post['post_author'] = $this->author_to_id ( $wpdb, $post['named']['author']['name'], $post['named']['author']['email'], $post['named']['author']['uri'], FeedWordPress::on_unfamiliar('author', $post['named']['unfamiliar']['author']) ); if (is_null($post['post_author'])) : $freshness = 0; else : # -- Look up, or create, numeric ID for categories $post['post_category'] = $this->lookup_categories ( $wpdb, $post['named']['category'], FeedWordPress::on_unfamiliar('category', $post['named']['unfamiliar']['category']) ); if (is_null($post['post_category'])) : // filter mode on, no matching categories; drop the post $freshness = 0; else : // filter mode off or at least one match; now add on the feed and global presets $post['post_category'] = array_merge ( $post['post_category'], $this->lookup_categories ( $wpdb, $post['named']['preset/category'], 'default' ) ); endif; endif; unset($post['named']); endif; if ($freshness > 0) : $post = apply_filters('syndicated_post', $post); if (is_null($post)) $freshness = 0; endif; if ($freshness == 2) : // The item has not yet been added. So let's add it. $postId = $this->insert_new_post($post); $this->add_rss_meta($wpdb, $postId, $post); do_action('post_syndicated_item', $postId); $ret = 'new'; elseif ($freshness == 1) : $post['ID'] = $result->id; $modified = $result->modified; $this->update_existing_post($post); $this->add_rss_meta($wpdb, $post['ID'], $post); do_action('update_syndicated_item', $post['ID']); $ret = 'updated'; else : $ret = false; endif; return $ret; } // function FeedWordPress::add_post () function insert_new_post ($post) { global $wpdb; $guid = $post['guid']; # The right way to do this would be to use: # # $postId = wp_insert_post($post); # $result = $wpdb->query(" # UPDATE $wpdb->posts # SET # guid='$guid' # WHERE post_id='$postId' # "); # # in place of everything in the cut below. Alas, # wp_insert_post seems to be a memory hog; using it # to insert several posts in one session makes php # segfault after inserting 50-100 posts. This can get # pretty annoying, especially if you are trying to # update your feeds for the first time. # # --- cut here --- $result = $wpdb->query(" INSERT INTO $wpdb->posts SET guid = '$guid', post_author = '".$post['post_author']."', post_date = '".$post['post_date']."', post_date_gmt = '".$post['post_date_gmt']."', post_content = '".$post['post_content']."'," .(isset($post['post_excerpt']) ? "post_excerpt = '".$post['post_excerpt']."'," : "")." post_title = '".$post['post_title']."', post_name = '".$post['post_name']."', post_modified = '".$post['post_modified']."', post_modified_gmt = '".$post['post_modified_gmt']."', comment_status = '".$post['comment_status']."', ping_status = '".$post['ping_status']."', post_status = '".$post['post_status']."' "); $postId = $wpdb->insert_id; $this->add_to_category($wpdb, $postId, $post['post_category']); // Since we are not going through official channels, we need to // manually tell WordPress that we've published a new post. // We need to make sure to do this in order for FeedWordPress // to play well with the staticize-reloaded plugin (something // that a large aggregator website is going to *want* to be // able to use). do_action('publish_post', $postId); # --- cut here --- return $postId; } /* FeedWordPress::insert_new_post() */ function update_existing_post ($post) { global $wpdb; $guid = $post['guid']; $postId = $post['ID']; $result = $wpdb->query(" UPDATE $wpdb->posts SET post_author = '".$post['post_author']."', post_content = '".$post['post_content']."', post_title = '".$post['post_title']."', post_name = '".$post['post_name']."', post_modified = '".$post['post_modified']."', post_modified_gmt = '".$post['post_modified_gmt']."' WHERE guid='$guid' "); $this->add_to_category($wpdb, $postId, $post['post_category']); // Since we are not going through official channels, we need to // manually tell WordPress that we've published a new post. // We need to make sure to do this in order for FeedWordPress // to play well with the staticize-reloaded plugin (something // that a large aggregator website is going to *want* to be // able to use). do_action('edit_post', $postId); } /* FeedWordPress::update_existing_post() */ # function FeedWordPress::add_to_category () # # If there is a way to properly hook in to wp_insert_post, then this # function will no longer be needed. In the meantime, here it is. # --- cut here --- function add_to_category($wpdb, $postId, $post_categories) { global $wp_db_version; // test for WordPress 2.0 database schema // Default to category 1 ("Uncategorized"), if nothing else if (!$post_categories) $post_categories[] = 1; // Now pass the buck to the WordPress API... wp_set_post_cats('', $postId, $post_categories); } // function FeedWordPress::add_to_category () # --- cut here --- // FeedWordPress::add_rss_meta: adds interesting meta-data to each entry // using the space for custom keys. The set of keys and values to add is // specified by the keys and values of $post['meta']. This is used to // store anything that the WordPress user might want to access from a // template concerning the post's original source that isn't provided // for by standard WP meta-data (i.e., any interesting data about the // syndicated post other than author, title, timestamp, categories, and // guid). It's also used to hook into WordPress's support for // enclosures. function add_rss_meta ($wpdb, $postId, $post) { if ( is_array($post) and isset($post['meta']) and is_array($post['meta']) ) : foreach ( $post['meta'] as $key => $values ) : $key = $wpdb->escape($key); // If this is an update, clear out the old // values to avoid duplication. $result = $wpdb->query(" DELETE FROM $wpdb->postmeta WHERE post_id='$postId' AND meta_key='$key' "); // Allow for either a single value or an array if (!is_array($values)) $values = array($values); foreach ( $values as $value ) : $value = $wpdb->escape($value); $result = $wpdb->query(" INSERT INTO $wpdb->postmeta SET post_id='$postId', meta_key='$key', meta_value='$value' "); endforeach; endforeach; endif; } /* FeedWordPress::add_rss_meta () */ // FeedWordPress::author_to_id (): get the ID for an author name from // the feed. Create the author if necessary. function author_to_id ($wpdb, $author, $email, $url, $unfamiliar_author = 'create') { global $wp_db_version; // test for WordPress 2.0 database schema // Never can be too careful... $nice_author = sanitize_title($author); $reg_author = $wpdb->escape(preg_quote($author)); $author = $wpdb->escape($author); $email = $wpdb->escape($email); $url = $wpdb->escape($url); // Look for an existing author record that fits... if (!isset($wp_db_version)) : #-- WordPress 1.5.x $id = $wpdb->get_var( "SELECT ID from $wpdb->users WHERE TRIM(LCASE(user_login)) = TRIM(LCASE('$author')) OR ( LENGTH(TRIM(LCASE(user_email))) > 0 AND TRIM(LCASE(user_email)) = TRIM(LCASE('$email')) ) OR TRIM(LCASE(user_firstname)) = TRIM(LCASE('$author')) OR TRIM(LCASE(user_nickname)) = TRIM(LCASE('$author')) OR TRIM(LCASE(user_nicename)) = TRIM(LCASE('$nice_author')) OR TRIM(LCASE(user_description)) = TRIM(LCASE('$author')) OR ( LOWER(user_description) RLIKE CONCAT( '(^|\\n)a.k.a.( |\\t)*:?( |\\t)*', LCASE('$reg_author'), '( |\\t|\\r)*(\\n|\$)' ) ) "); elseif ($wp_db_version >= 2966) : #-- WordPress 2.0+ # First try the user core data table. $id = $wpdb->get_var( "SELECT ID FROM $wpdb->users WHERE TRIM(LCASE(user_login)) = TRIM(LCASE('$author')) OR ( LENGTH(TRIM(LCASE(user_email))) > 0 AND TRIM(LCASE(user_email)) = TRIM(LCASE('$email')) ) OR TRIM(LCASE(user_nicename)) = TRIM(LCASE('$nice_author')) "); if (is_null($id)) : # Then look for aliases in the user meta data table $id = $wpdb->get_var( "SELECT user_id FROM $wpdb->usermeta WHERE (meta_key = 'description' AND TRIM(LCASE(meta_value)) = TRIM(LCASE('$author'))) OR ( meta_key = 'description' AND LCASE(meta_value) RLIKE CONCAT( '(^|\\n)a.k.a.( |\\t)*:?( |\\t)*', LCASE('$reg_author'), '( |\\t|\\r)*(\\n|\$)' ) ) "); endif; endif; // ... if you don't find one, then do what you need to do if (is_null($id)) : if ($unfamiliar_author === 'create') : if (!isset($wp_db_version)) : #-- WordPress 1.5.x $wpdb->query ( "INSERT INTO $wpdb->users SET ID='0', user_login='$author', user_firstname='$author', user_nickname='$author', user_nicename='$nice_author', user_description='$author', user_email='$email', user_url='$url'"); $id = $wpdb->insert_id; elseif ($wp_db_version >= 2966) : #-- WordPress 2.0+ $userdata = array(); #-- user table data $userdata['ID'] = NULL; // new user $userdata['user_login'] = $author; $userdata['user_pass'] = FeedWordPress::rpc_secret(); $userdata['user_email'] = $email; $userdata['user_url'] = $url; $userdata['display_name'] = $author; $id = wp_insert_user($userdata); endif; elseif ($unfamiliar_author === 'default') : $id = 1; endif; endif; return $id; } // function FeedWordPress::author_to_id () // look up (and create) category ids from a list of categories function lookup_categories ($wpdb, $cats, $unfamiliar_category = 'create') { // Normalize whitespace because (1) trailing whitespace can // cause PHP and MySQL not to see eye to eye on VARCHAR // comparisons for some versions of MySQL (cf. // ), and (2) // because I doubt most people want to make a semantic // distinction between 'Computers' and 'Computers ' $cats = array_map('trim', $cats); $cat_ids = array (); if ( count($cats) > 0 ) : # i'd kill for a decent map function in PHP # but that would require functions to be first class object, # or at least coderef support $cat_str = array (); $cat_aka = array (); foreach ( $cats as $c ) : $resc = $wpdb->escape(preg_quote($c)); $esc = $wpdb->escape($c); $cat_str[] = "'$esc'"; $cat_aka[] = "(LOWER(category_description) RLIKE CONCAT('(^|\n)a.k.a.( |\t)*:?( |\t)*', LOWER('{$resc}'), '( |\t|\r)*(\n|\$)'))"; endforeach; $match_cat_name = 'cat_name IN ('.join(',', $cat_str).')'; $match_cat_alias = join(' OR ', $cat_aka); $results = $wpdb->get_results( "SELECT cat_ID, cat_name, category_description FROM $wpdb->categories WHERE ($match_cat_name) OR ($match_cat_alias)" ); $cat_ids = array(); $found = array(); if (!is_null($results)): foreach ( $results as $row ) : // Add existing ID to list of numerical // IDs to eventually place post in $cat_ids[] = $row->cat_ID; // Add name to list of categories not to // create afresh. Normalizing case with // strtolower() avoids mismatches in // VARCHAR comparison between PHP (which // has case-sensitive comparisons) and // MySQL (which has case-insensitive // comparisons for the field types used // by WordPress) $found[] = strtolower(trim($row->cat_name)); // Add name of any aliases to list of // categories not to create afresh. if (preg_match_all('/^a.k.a. \s* :? \s* (.*\S) \s*$/mx', $row->category_description, $aka, PREG_PATTERN_ORDER)) : $found = array_merge ( $found, array_map('strtolower', array_map('trim', $aka[1] )) ); endif; endforeach; endif; foreach ($cats as $new_cat) : if (($unfamiliar_category==='create') and !in_array(strtolower($new_cat), $found)) : $nice_cat = sanitize_title($new_cat); $wpdb->query(sprintf(" INSERT INTO $wpdb->categories SET cat_name='%s', category_nicename='%s' ", $wpdb->escape($new_cat), $nice_cat)); $cat_ids[] = $wpdb->insert_id; endif; endforeach; if ((count($cat_ids) == 0) and ($unfamiliar_category === 'filter')) : $cat_ids = NULL; // Drop the post endif; endif; return $cat_ids; } // function FeedWordPress::lookup_categories () function rpc_secret () { return get_settings('feedwordpress_rpc_secret'); } // function FeedWordPress::rpc_secret () function on_unfamiliar ($what = 'author', $override = NULL) { $set = array('create', 'default', 'filter'); $ret = strtolower($override); if (!in_array($ret, $set)) : $ret = get_settings('feedwordpress_unfamiliar_'.$what); if (!in_array($ret, $set)) : $ret = 'create'; endif; endif; return $ret; } // function FeedWordPress::on_unfamiliar() function link_category_id () { global $wpdb; $cat_id = get_settings('feedwordpress_cat_id'); // If we don't yet *have* the category, we'll have to create it if ($cat_id === false) { $cat = $wpdb->escape(DEFAULT_SYNDICATION_CATEGORY); // Look for something with the right name... $cat_id = $wpdb->get_var(" SELECT cat_id FROM $wpdb->linkcategories WHERE cat_name='$cat' "); // If you still can't find anything, make it for yourself. if (!$cat_id) { $result = $wpdb->query(" INSERT INTO $wpdb->linkcategories SET cat_id = 0, cat_name='$cat', show_images='N', show_description='N', show_rating='N', show_updated='N', sort_order='name' "); $cat_id = $wpdb->insert_id; } update_option('feedwordpress_cat_id', $cat_id); } return $cat_id; } function link_category () { global $wpdb; $cat_id = FeedWordPress::link_category_id(); // Get the ID# for the category name... $cat_name = $wpdb->get_var(" SELECT cat_name FROM $wpdb->linkcategories WHERE cat_id='$cat_id' "); return $cat_name; } function upgrade_database ($from = NULL) { global $wpdb; if (is_null($from) or $from <= 0.96) : $from = 0.96; endif; switch ($from) : case 0.96: // account for changes to syndication custom values and guid echo "

Upgrading database from {$from} to 0.97+...

\n"; $cat_id = FeedWordPress::link_category_id(); // Avoid duplicates $wpdb->query("DELETE FROM `{$wpdb->postmeta}` WHERE meta_key = 'syndication_feed_id'"); // Look up all the link IDs $wpdb->query(" CREATE TEMPORARY TABLE tmp_custom_values SELECT NULL AS meta_id, post_id, 'syndication_feed_id' AS meta_key, link_id AS meta_value FROM `{$wpdb->postmeta}`, `{$wpdb->links}` WHERE meta_key='syndication_feed' AND meta_value=link_rss AND link_category = {$cat_id} "); // Now attach them to their posts $wpdb->query("INSERT INTO `{$wpdb->postmeta}` SELECT * FROM tmp_custom_values"); // And clean up after ourselves. $wpdb->query("DROP TABLE tmp_custom_values"); // Now fix the guids to avoid duplicate posts echo "
    "; foreach ($this->feeds as $feed) : echo "
  • Fixing post meta-data for ".$feed['link/name']." … "; flush(); $rss = @fetch_rss($feed['link/uri']); if (is_array($rss->items)) : foreach ($rss->items as $item) : $guid = $wpdb->escape(FeedWordPress::guid($item, $feed)); // new GUID algorithm $link = $wpdb->escape($item['link']); $wpdb->query(" UPDATE `{$wpdb->posts}` SET guid='{$guid}' WHERE guid='{$link}' "); endforeach; endif; echo "complete.
  • \n"; endforeach; echo "
\n"; // Mark the upgrade as successful. update_option('feedwordpress_version', FEEDWORDPRESS_VERSION); endswitch; echo "

Upgrade complete. FeedWordPress is now ready to use again.

"; } /* FeedWordPress::upgrade_database() */ } // class FeedWordPress ################################################################################ ## XML-RPC HOOKS: accept XML-RPC update pings from Contributors ################ ################################################################################ function feedwordpress_xmlrpc_hook ($args = array ()) { $args['weblogUpdates.ping'] = 'feedwordpress_pong'; return $args; } function feedwordpress_pong ($args) { $feedwordpress =& new FeedWordPress; $delta = @$feedwordpress->update($args[1]); if (is_null($delta)): return array('flerror' => true, 'message' => "Sorry. I don't syndicate <$args[1]>."); else: $mesg = array(); if (isset($delta['new'])) { $mesg[] = ' '.$delta['new'].' new posts were syndicated'; } if (isset($delta['updated'])) { $mesg[] = ' '.$delta['updated'].' existing posts were updated'; } return array('flerror' => false, 'message' => "Thanks for the ping.".implode(' and', $mesg)); endif; } ################################################################################ ## class FeedFinder: find likely feeds using autodetection and/or guesswork #### ################################################################################ class FeedFinder { var $uri = NULL; var $_cache_uri = NULL; var $verify = FALSE; var $_data = NULL; var $_head = NULL; # -- Recognition patterns var $_feed_types = array( 'application/rss+xml', 'text/xml', 'application/atom+xml', 'application/x.atom+xml', 'application/x-atom+xml' ); var $_feed_markers = array('\\_feed_markers).")\007i", $data ) and !preg_match ( "\007(".implode('|',$this->_html_markers).")\007i", $data ) ); } /* FeedFinder::is_feed () */ # --- Private methods --- function _get ($uri = NULL) { if ($uri) $this->uri = $uri; // Is the result not yet cached? if ($this->_cache_uri !== $this->uri) : // Snoopy is an HTTP client in PHP $client = new Snoopy(); // Prepare headers and internal settings $client->rawheaders['Connection'] = 'close'; $client->accept = 'application/atom+xml application/rdf+xml application/rss+xml application/xml text/html */*'; $client->agent = 'feedfinder/1.2 (compatible; PHP FeedFinder) +http://projects.radgeek.com/feedwordpress'; $client->read_timeout = 5; // Fetch the HTML or feed @$client->fetch($this->uri); $this->_data = $client->results; // Kilroy was here $this->_cache_uri = $this->uri; endif; } /* FeedFinder::_get () */ function _link_rel_feeds () { $links = $this->_tags('link'); $link_count = count($links); // now figure out which one points to the RSS file $href = array (); for ($n=0; $n<$link_count; $n++) { if (strtolower($links[$n]['rel']) == 'alternate') { if (in_array(strtolower($links[$n]['type']), $this->_feed_types)) { $href[] = $links[$n]['href']; } /* if */ } /* if */ } /* for */ return $href; } function _a_href_feeds ($obvious = TRUE) { $pattern = ($obvious ? $this->_obvious_feed_url : $this->_maybe_feed_url); $links = $this->_tags('a'); $link_count = count($links); // now figure out which one points to the RSS file $href = array (); for ($n=0; $n<$link_count; $n++) { if (preg_match("\007(".implode('|',$pattern).")\007i", $links[$n]['href'])) { $href[] = $links[$n]['href']; } /* if */ } /* for */ return $href; } function _tags ($tag) { $html = $this->data(); // search through the HTML, save all tags // and store each link's attributes in an associative array preg_match_all('/<'.$tag.'\s+(.*?)\s*\/?>/si', $html, $matches); $links = $matches[1]; $ret = array(); $link_count = count($links); for ($n=0; $n<$link_count; $n++) { $attributes = preg_split('/\s+/s', $links[$n]); foreach($attributes as $attribute) { $att = preg_split('/\s*=\s*/s', $attribute, 2); if (isset($att[1])) { $att[1] = preg_replace('/([\'"]?)(.*)\1/', '$2', $att[1]); $final_link[strtolower($att[0])] = $att[1]; } /* if */ } /* foreach */ $ret[$n] = $final_link; } /* for */ return $ret; } } /* class FeedFinder */ # Relative URI static class: PHP class for resolving relative URLs # # This class is derived (under the terms of the GPL) from URL Class 0.3 by # Keyvan Minoukadeh , which is great but more than we need # for FeedWordPress's purposes. The class has been stripped down to a single # public method: Relative_URI::resolve($url, $base), which resolves the URI in # $url relative to the URI in $base class Relative_URI { // Resolve relative URI in $url against the base URI in $base. If $base // is not supplied, then we use the REQUEST_URI of this script. // // I'm hoping this method reflects RFC 2396 Section 5.2 function resolve ($url, $base = NULL) { if (is_null($base)): $base = 'http://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']; endif; $base = Relative_URI::_encode(trim($base)); $uri_parts = Relative_URI::_parse_url($base); $url = Relative_URI::_encode(trim($url)); $parts = Relative_URI::_parse_url($url); $uri_parts['fragment'] = (isset($parts['fragment']) ? $parts['fragment'] : null); $uri_parts['query'] = $parts['query']; // if path is empty, and scheme, host, and query are undefined, // the URL is referring the base URL if (($parts['path'] == '') && !isset($parts['scheme']) && !isset($parts['host']) && !isset($parts['query'])) { // If the URI is empty or only a fragment, return the base URI return $base . (isset($parts['fragment']) ? '#'.$parts['fragment'] : ''); } elseif (isset($parts['scheme'])) { // If the scheme is set, then the URI is absolute. return $url; } elseif (isset($parts['host'])) { $uri_parts['host'] = $parts['host']; $uri_parts['path'] = $parts['path']; } else { // We have a relative path but not a host. // start ugly fix: // prepend slash to path if base host is set, base path is not set, and url path is not absolute if ($uri_parts['host'] && ($uri_parts['path'] == '') && (strlen($parts['path']) > 0) && (substr($parts['path'], 0, 1) != '/')) { $parts['path'] = '/'.$parts['path']; } // end ugly fix if (substr($parts['path'], 0, 1) == '/') { $uri_parts['path'] = $parts['path']; } else { // copy base path excluding any characters after the last (right-most) slash character $buffer = substr($uri_parts['path'], 0, (int)strrpos($uri_parts['path'], '/')+1); // append relative path $buffer .= $parts['path']; // remove "./" where "." is a complete path segment. $buffer = str_replace('/./', '/', $buffer); if (substr($buffer, 0, 2) == './') { $buffer = substr($buffer, 2); } // if buffer ends with "." as a complete path segment, remove it if (substr($buffer, -2) == '/.') { $buffer = substr($buffer, 0, -1); } // remove "/../" where is a complete path segment not equal to ".." $search_finished = false; $segment = explode('/', $buffer); while (!$search_finished) { for ($x=0; $x+1 < count($segment);) { if (($segment[$x] != '') && ($segment[$x] != '..') && ($segment[$x+1] == '..')) { if ($x+2 == count($segment)) $segment[] = ''; unset($segment[$x], $segment[$x+1]); $segment = array_values($segment); continue 2; } else { $x++; } } $search_finished = true; } $buffer = (count($segment) == 1) ? '/' : implode('/', $segment); $uri_parts['path'] = $buffer; } } // If we've gotten to this point, we can try to put the pieces // back together. $ret = ''; if (isset($uri_parts['scheme'])) $ret .= $uri_parts['scheme'].':'; if (isset($uri_parts['user'])) { $ret .= $uri_parts['user']; if (isset($uri_parts['pass'])) $ret .= ':'.$uri_parts['parts']; $ret .= '@'; } if (isset($uri_parts['host'])) { $ret .= '//'.$uri_parts['host']; if (isset($uri_parts['port'])) $ret .= ':'.$uri_parts['port']; } $ret .= $uri_parts['path']; if (isset($uri_parts['query'])) $ret .= '?'.$uri_parts['query']; if (isset($uri_parts['fragment'])) $ret .= '#'.$uri_parts['fragment']; return $ret; } /** * Parse URL * * Regular expression grabbed from RFC 2396 Appendix B. * This is a replacement for PHPs builtin parse_url(). * @param string $url * @access private * @return array */ function _parse_url($url) { // I'm using this pattern instead of parse_url() as there's a few strings where parse_url() // generates a warning. if (preg_match('!^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?!', $url, $match)) { $parts = array(); if ($match[1] != '') $parts['scheme'] = $match[2]; if ($match[3] != '') $parts['auth'] = $match[4]; // parse auth if (isset($parts['auth'])) { // store user info if (($at_pos = strpos($parts['auth'], '@')) !== false) { $userinfo = explode(':', substr($parts['auth'], 0, $at_pos), 2); $parts['user'] = $userinfo[0]; if (isset($userinfo[1])) $parts['pass'] = $userinfo[1]; $parts['auth'] = substr($parts['auth'], $at_pos+1); } // get port number if ($port_pos = strrpos($parts['auth'], ':')) { $parts['host'] = substr($parts['auth'], 0, $port_pos); $parts['port'] = (int)substr($parts['auth'], $port_pos+1); if ($parts['port'] < 1) $parts['port'] = null; } else { $parts['host'] = $parts['auth']; } } unset($parts['auth']); $parts['path'] = $match[5]; if (isset($match[6]) && ($match[6] != '')) $parts['query'] = $match[7]; if (isset($match[8]) && ($match[8] != '')) $parts['fragment'] = $match[9]; return $parts; } // shouldn't reach here return array('path'=>''); } function _encode($string) { static $replace = array(); if (!count($replace)) { $find = array(32, 34, 60, 62, 123, 124, 125, 91, 92, 93, 94, 96, 127); $find = array_merge(range(0, 31), $find); $find = array_map('chr', $find); foreach ($find as $char) { $replace[$char] = '%'.bin2hex($char); } } // escape control characters and a few other characters $encoded = strtr($string, $replace); // remove any character outside the hex range: 21 - 7E (see www.asciitable.com) return preg_replace('/[^\x21-\x7e]/', '', $encoded); } } // take your best guess at the realname and e-mail, given a string define('FWP_REGEX_EMAIL_ADDY', '([^@"(<\s]+@[^"@(<\s]+\.[^"@(<\s]+)'); define('FWP_REGEX_EMAIL_NAME', '("([^"]*)"|([^"<(]+\S))'); define('FWP_REGEX_EMAIL_POSTFIX_NAME', "/^\s*".FWP_REGEX_EMAIL_ADDY."\s+\(".FWP_REGEX_EMAIL_NAME."\)\s*$/"); define('FWP_REGEX_EMAIL_PREFIX_NAME', "/^\s*".FWP_REGEX_EMAIL_NAME."\s*<".FWP_REGEX_EMAIL_ADDY.">\s*$/"); define('FWP_REGEX_EMAIL_JUST_ADDY', "/^\s*".FWP_REGEX_EMAIL_ADDY."\s*$/"); define('FWP_REGEX_EMAIL_JUST_NAME', "/^\s*".FWP_REGEX_EMAIL_NAME."\s*$/"); function parse_email_with_realname ($email) { if (preg_match(FWP_REGEX_EMAIL_POSTFIX_NAME, $email, $matches)) : ($ret['name'] = $matches[3]) or ($ret['name'] = $matches[2]); $ret['email'] = $matches[1]; elseif (preg_match(FWP_REGEX_EMAIL_PREFIX_NAME, $email, $matches)) : ($ret['name'] = $matches[2]) or ($ret['name'] = $matches[3]); $ret['email'] = $matches[4]; elseif (preg_match(FWP_REGEX_EMAIL_JUST_ADDY, $email, $matches)) : $ret['name'] = NULL; $ret['email'] = $matches[1]; elseif (preg_match(FWP_REGEX_EMAIL_JUST_NAME, $email, $matches)) : $ret['email'] = NULL; ($ret['name'] = $matches[2]) or ($ret['name'] = $matches[3]); else : $ret['name'] = NULL; $ret['email'] = NULL; endif; return $ret; } ?>