PluginProbe
FeedWordPress / 2008.1101
FeedWordPress v2008.1101
trunk 0.8 0.9 0.91 0.95 0.96 0.97 0.98 0.981 0.99 0.991 0.992 0.993 2008.1030 2008.1101 2008.1105 2008.1214 2009.0612 2009.0613 2009.0618 2009.0707 2009.1111 2009.1112 2010.0127 2010.0528 All 65 releases
feedwordpress / feedwordpress.php

feedwordpress.php in FeedWordPress 2008.1101, at feedwordpress.php

2,659 lines 92.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 Plugin Name: FeedWordPress
4 Plugin URI: http://projects.radgeek.com/feedwordpress
5 Description: simple and flexible Atom/RSS syndication for WordPress
6 Version: 2008.1101
7 Author: Charles Johnson
8 Author URI: http://radgeek.com/
9 License: GPL
10 Last modified: 2008-11-01 1:13pm PDT
11 */
12
13 # This uses code derived from:
14 # - wp-rss-aggregate.php by Kellan Elliot-McCrea <kellan@protest.net>
15 # - HTTP Navigator 2 by Keyvan Minoukadeh <keyvan@k1m.com>
16 # - Ultra-Liberal Feed Finder by Mark Pilgrim <mark@diveintomark.org>
17 # according to the terms of the GNU General Public License.
18 #
19 # INSTALLATION: see README.text or <http://projects.radgeek.com/install>
20 #
21 # USAGE: once FeedWordPress is installed, you manage just about everything from
22 # the WordPress Dashboard, under Options --> Syndication or Links --> Syndicated
23 # To ensure that fresh content is added as it becomes available, get your
24 # contributors to put your XML-RPC URI (if WordPress is installed at
25 # <http://www.zyx.com/blog>, XML-RPC requests should be sent to
26 # <http://www.zyx.com/blog/xmlrpc.php>), or see `update-feeds.php`
27
28 # -- Don't change these unless you know what you're doing...
29
30 define ('FEEDWORDPRESS_VERSION', '2008.1101');
31 define ('FEEDWORDPRESS_AUTHOR_CONTACT', 'http://radgeek.com/contact');
32 define ('DEFAULT_SYNDICATION_CATEGORY', 'Contributors');
33
34 define ('FEEDWORDPRESS_DEBUG', false);
35
36 define ('FEEDWORDPRESS_CAT_SEPARATOR_PATTERN', '/[:\n]/');
37 define ('FEEDWORDPRESS_CAT_SEPARATOR', "\n");
38
39 define ('FEEDVALIDATOR_URI', 'http://feedvalidator.org/check.cgi');
40
41 define ('FEEDWORDPRESS_FRESHNESS_INTERVAL', 10*60); // Every ten minutes
42
43 define ('FWP_SCHEMA_20', 3308); // Database schema # for WP 2.0
44 define ('FWP_SCHEMA_21', 4772); // Database schema # for WP 2.1
45 define ('FWP_SCHEMA_23', 5495); // Database schema # for WP 2.3
46 define ('FWP_SCHEMA_25', 7558); // Database schema # for WP 2.5
47 define ('FWP_SCHEMA_26', 8201); // Database schema # for WP 2.6
48
49 if (FEEDWORDPRESS_DEBUG) :
50 // Help us to pick out errors, if any.
51 ini_set('error_reporting', E_ALL & ~E_NOTICE);
52 ini_set('display_errors', true);
53 define('MAGPIE_DEBUG', true);
54
55 // When testing we don't want cache issues to interfere. But this is
56 // a VERY BAD SETTING for a production server. Webmasters will eat your
57 // face for breakfast if you use it, and the baby Jesus will cry. So
58 // make sure FEEDWORDPRESS_DEBUG is FALSE for any site that will be
59 // used for more than testing purposes!
60 define('MAGPIE_CACHE_AGE', 1);
61 else :
62 define('MAGPIE_DEBUG', false);
63 endif;
64
65 // Note that the rss-functions.php that comes prepackaged with WordPress is
66 // old & busted. For the new hotness, drop a copy of rss.php from
67 // this archive into wp-includes/rss.php
68
69 if (is_readable(ABSPATH . WPINC . '/rss.php')) :
70 require_once (ABSPATH . WPINC . '/rss.php');
71 else :
72 require_once (ABSPATH . WPINC . '/rss-functions.php');
73 endif;
74
75 if (isset($wp_db_version)) :
76 if ($wp_db_version >= FWP_SCHEMA_23) :
77 require_once (ABSPATH . WPINC . '/registration.php'); // for wp_insert_user
78 elseif ($wp_db_version >= FWP_SCHEMA_21) : // WordPress 2.1 and 2.2, but not 2.3
79 require_once (ABSPATH . WPINC . '/registration.php'); // for wp_insert_user
80 require_once (ABSPATH . 'wp-admin/admin-db.php'); // for wp_insert_category
81 elseif ($wp_db_version >= FWP_SCHEMA_20) : // WordPress 2.0
82 require_once (ABSPATH . WPINC . '/registration-functions.php'); // for wp_insert_user
83 require_once (ABSPATH . 'wp-admin/admin-db.php'); // for wp_insert_category
84 endif;
85 endif;
86
87 require_once(dirname(__FILE__) . '/compatability.php'); // LEGACY API: Replicate or mock up functions for legacy support purposes
88
89 // Magic quotes are just about the stupidest thing ever.
90 if (is_array($_POST)) :
91 $fwp_post = stripslashes_deep($_POST);
92 endif;
93
94 // Get the path relative to the plugins directory in which FWP is stored
95 preg_match (
96 '|/wp-content/plugins/(.+)$|',
97 dirname(__FILE__),
98 $ref
99 );
100
101 if (isset($ref[1])) :
102 $fwp_path = $ref[1];
103 else : // Something went wrong. Let's just guess.
104 $fwp_path = 'feedwordpress';
105 endif;
106
107 // If this is a FeedWordPress admin page, queue up scripts for AJAX functions that FWP uses
108 // If it is a display page or a non-FeedWordPress admin page, don't.
109 if (is_admin() and isset($_REQUEST['page']) and preg_match("|^{$fwp_path}/|", $_REQUEST['page'])) :
110 if (function_exists('wp_enqueue_script')) :
111 if (isset($wp_db_version) and $wp_db_version >= FWP_SCHEMA_25) :
112 wp_enqueue_script('post'); // for magic tag and category boxes
113 wp_enqueue_script('thickbox'); // for fold-up boxes
114 wp_enqueue_script('admin-forms'); // for checkbox selection
115 else :
116 wp_enqueue_script( 'ajaxcat' ); // Provides the handy-dandy new category text box
117 endif;
118 endif;
119 if (function_exists('wp_enqueue_style')) :
120 if (fwp_test_wp_version(FWP_SCHEMA_25)) :
121 wp_enqueue_style('dashboard');
122 endif;
123 endif;
124 if (function_exists('wp_admin_css')) :
125 if (fwp_test_wp_version(FWP_SCHEMA_25)) :
126 wp_admin_css('css/dashboard');
127 endif;
128 endif;
129 endif;
130
131 if (!FeedWordPress::needs_upgrade()) : // only work if the conditions are safe!
132
133 # Syndicated items are generally received in output-ready (X)HTML and
134 # should not be folded, crumpled, mutilated, or spindled by WordPress
135 # formatting filters. But we don't want to interfere with filters for
136 # any locally-authored posts, either.
137 #
138 # What WordPress should really have is a way for upstream filters to
139 # stop downstream filters from running at all. Since it doesn't, and
140 # since a downstream filter can't access the original copy of the text
141 # that is being filtered, what we will do here is (1) save a copy of the
142 # original text upstream, before any other filters run, and then (2)
143 # retrieve that copy downstream, after all the other filters run, *if*
144 # this is a syndicated post
145
146 add_filter('the_content', 'feedwordpress_preserve_syndicated_content', -10000);
147 add_filter('the_content', 'feedwordpress_restore_syndicated_content', 10000);
148
149 add_action('atom_entry', 'feedwordpress_item_feed_data');
150
151 # Filter in original permalinks if the user wants that
152 add_filter('post_link', 'syndication_permalink', 1);
153
154 # WTF? By default, wp_insert_link runs incoming link_url and link_rss
155 # URIs through default filters that include `wp_kses()`. But `wp_kses()`
156 # just happens to escape any occurrence of & to &amp; -- which just
157 # happens to fuck up any URI with a & to separate GET parameters.
158 remove_filter('pre_link_rss', 'wp_filter_kses');
159 remove_filter('pre_link_url', 'wp_filter_kses');
160
161 # Admin menu
162 add_action('admin_menu', 'fwp_add_pages');
163
164 # Inbound XML-RPC update methods
165 add_filter('xmlrpc_methods', 'feedwordpress_xmlrpc_hook');
166
167 # Outbound XML-RPC ping reform
168 remove_action('publish_post', 'generic_ping'); // WP 1.5.x
169 remove_action('do_pings', 'do_all_pings', 10, 1); // WP 2.1, 2.2
170 remove_action('publish_post', '_publish_post_hook', 5, 1); // WP 2.3
171
172 add_action('publish_post', 'fwp_publish_post_hook', 5, 1);
173 add_action('do_pings', 'fwp_do_pings', 10, 1);
174 add_action('feedwordpress_update', 'fwp_hold_pings');
175 add_action('feedwordpress_update_complete', 'fwp_release_pings');
176
177 # Hook in logging functions only if the logging option is ON
178 $update_logging = get_option('feedwordpress_update_logging');
179 if ($update_logging == 'yes') :
180 add_action('post_syndicated_item', 'log_feedwordpress_post', 100);
181 add_action('update_syndicated_item', 'log_feedwordpress_update_post', 100);
182 add_action('feedwordpress_update', 'log_feedwordpress_update_feeds', 100);
183 add_action('feedwordpress_check_feed', 'log_feedwordpress_check_feed', 100);
184 add_action('feedwordpress_update_complete', 'log_feedwordpress_update_complete', 100);
185 endif;
186
187 # Cron-less auto-update. Hooray!
188 add_action('init', 'feedwordpress_auto_update');
189
190 # Default sanitizers
191 add_filter('syndicated_item_content', array('SyndicatedPost', 'sanitize_content'), 0, 2);
192
193 else :
194 # Hook in the menus, which will just point to the upgrade interface
195 add_action('admin_menu', 'fwp_add_pages');
196 endif; // if (!FeedWordPress::needs_upgrade())
197
198 function feedwordpress_auto_update () {
199 if (FeedWordPress::stale()) :
200 $feedwordpress =& new FeedWordPress;
201 $feedwordpress->update();
202 endif;
203
204 if (FeedWordPress::update_requested()) :
205 exit;
206 endif;
207 } /* feedwordpress_auto_update () */
208
209 ################################################################################
210 ## LOGGING FUNCTIONS: log status updates to error_log if you want it ###########
211 ################################################################################
212
213 function log_feedwordpress_post ($id) {
214 $post = wp_get_single_post($id);
215 error_log("[".date('Y-m-d H:i:s')."][feedwordpress] posted "
216 ."'{$post->post_title}' ({$post->post_date})");
217 }
218
219 function log_feedwordpress_update_post ($id) {
220 $post = wp_get_single_post($id);
221 error_log("[".date('Y-m-d H:i:s')."][feedwordpress] updated "
222 ."'{$post->post_title}' ({$post->post_date})"
223 ." (as of {$post->post_modified})");
224 }
225
226 function log_feedwordpress_update_feeds ($uri) {
227 error_log("[".date('Y-m-d H:i:s')."][feedwordpress] update('$uri')");
228 }
229
230 function log_feedwordpress_check_feed ($feed) {
231 $uri = $feed['link/uri']; $name = $feed['link/name'];
232 error_log("[".date('Y-m-d H:i:s')."][feedwordpress] Examining $name <$uri>");
233 }
234
235 function log_feedwordpress_update_complete ($delta) {
236 $mesg = array();
237 if (isset($delta['new'])) $mesg[] = 'added '.$delta['new'].' new posts';
238 if (isset($delta['updated'])) $mesg[] = 'updated '.$delta['updated'].' existing posts';
239 if (empty($mesg)) $mesg[] = 'nothing changed';
240
241 error_log("[".date('Y-m-d H:i:s')."][feedwordpress] "
242 .(is_null($delta) ? "Error: I don't syndicate that URI"
243 : implode(' and ', $mesg)));
244 }
245
246 ################################################################################
247 ## TEMPLATE API: functions to make your templates syndication-aware ############
248 ################################################################################
249
250 function is_syndicated () { return (strlen(get_syndication_feed_id()) > 0); }
251
252 function the_syndication_source_link () { echo get_syndication_source_link(); }
253 function get_syndication_source_link () { list($n) = get_post_custom_values('syndication_source_uri'); return $n; }
254
255 function get_syndication_source () { list($n) = get_post_custom_values('syndication_source'); return $n; }
256 function the_syndication_source () { echo get_syndication_source(); }
257
258 function get_syndication_feed () { list($u) = get_post_custom_values('syndication_feed'); return $u; }
259 function the_syndication_feed () { echo get_syndication_feed (); }
260
261 function get_syndication_feed_id () { list($u) = get_post_custom_values('syndication_feed_id'); return $u; }
262 function the_syndication_feed_id () { echo get_syndication_feed_id(); }
263
264 $feedwordpress_linkcache = array (); // only load links from database once
265
266 function get_feed_meta ($key) {
267 global $wpdb, $feedwordpress_linkcache;
268 $feed_id = get_syndication_feed_id();
269
270 $ret = NULL;
271 if (strlen($feed_id) > 0):
272 if (isset($feedwordpress_linkcache[$feed_id])) :
273 $link = $feedwordpress_linkcache[$feed_id];
274 else :
275 $link =& new SyndicatedLink($feed_id);
276 $feedwordpress_linkcache[$feed_id] = $link;
277 endif;
278
279 $ret = $link->settings[$key];
280 endif;
281 return $ret;
282 } /* get_feed_meta() */
283
284 function get_syndication_permalink () {
285 list($u) = get_post_custom_values('syndication_permalink'); return $u;
286 }
287 function the_syndication_permalink () {
288 echo get_syndication_permalink();
289 }
290
291 ################################################################################
292 ## FILTERS: syndication-aware handling of post data for templates and feeds ####
293 ################################################################################
294
295 $feedwordpress_the_syndicated_content = NULL;
296
297 function feedwordpress_preserve_syndicated_content ($text) {
298 global $feedwordpress_the_syndicated_content;
299
300 if ( is_syndicated() and get_option('feedwordpress_formatting_filters') != 'yes' ) :
301 $feedwordpress_the_syndicated_content = $text;
302 else :
303 $feedwordpress_the_syndicated_content = NULL;
304 endif;
305 return $text;
306 }
307
308 function feedwordpress_restore_syndicated_content ($text) {
309 global $feedwordpress_the_syndicated_content;
310
311 if ( !is_null($feedwordpress_the_syndicated_content) ) :
312 $text = $feedwordpress_the_syndicated_content;
313 endif;
314
315 return $text;
316 }
317
318 function feedwordpress_item_feed_data () {
319 // In a post context....
320 if (is_syndicated()) :
321 ?>
322 <source>
323 <title><?php the_syndication_source(); ?></title>
324 <link rel="alternate" type="text/html" href="<?php the_syndication_source_link(); ?>" />
325 <link rel="self" href="<?php the_syndication_feed(); ?>" />
326 <?php
327 $id = get_feed_meta('feed/id');
328 if (strlen($id) > 0) :
329 ?>
330 <id><?php print $id; ?></id>
331 <?php
332 endif;
333 $updated = get_feed_meta('feed/updated');
334 if (strlen($updated) > 0) : ?>
335 <updated><?php print $updated; ?></updated>
336 <?php
337 endif;
338 ?>
339 </source>
340 <?php
341 endif;
342 }
343
344 function syndication_permalink ($permalink = '') {
345 if (get_option('feedwordpress_munge_permalink') != 'no'):
346 $uri = get_syndication_permalink();
347 return ((strlen($uri) > 0) ? $uri : $permalink);
348 else:
349 return $permalink;
350 endif;
351 } // function syndication_permalink ()
352
353 ################################################################################
354 ## ADMIN MENU ADD-ONS: register Dashboard management pages #####################
355 ################################################################################
356
357 function fwp_add_pages () {
358 global $fwp_capability;
359 global $fwp_path;
360
361 add_menu_page('Syndicated Sites', 'Syndication', $fwp_capability['manage_links'], $fwp_path.'/syndication.php');
362 add_submenu_page($fwp_path.'/syndication.php', 'Syndication Options', 'Options', $fwp_capability['manage_options'], $fwp_path.'/syndication-options.php');
363 add_options_page('Syndication Options', 'Syndication', $fwp_capability['manage_options'], $fwp_path.'/syndication-options.php');
364 } // function fwp_add_pages () */
365
366 ################################################################################
367 ## fwp_hold_pings() and fwp_release_pings(): Outbound XML-RPC ping reform ####
368 ## ... 'coz it's rude to send 500 pings the first time your aggregator runs ####
369 ################################################################################
370
371 $fwp_held_ping = NULL; // NULL: not holding pings yet
372
373 function fwp_hold_pings () {
374 global $fwp_held_ping;
375 if (is_null($fwp_held_ping)):
376 $fwp_held_ping = 0; // 0: ready to hold pings; none yet received
377 endif;
378 }
379
380 function fwp_release_pings () {
381 global $fwp_held_ping;
382 if ($fwp_held_ping):
383 if (function_exists('wp_schedule_single_event')) :
384 wp_schedule_single_event(time(), 'do_pings');
385 else :
386 generic_ping($fwp_held_ping);
387 endif;
388 endif;
389 $fwp_held_ping = NULL; // NULL: not holding pings anymore
390 }
391
392 function fwp_do_pings () {
393 if (!is_null($fwp_held_ping) and $post_id) : // Defer until we're done updating
394 $fwp_held_ping = $post_id;
395 elseif (function_exists('do_all_pings')) :
396 do_all_pings();
397 else :
398 generic_ping($fwp_held_ping);
399 endif;
400 }
401
402 function fwp_publish_post_hook ($post_id) {
403 global $fwp_held_ping;
404
405 if (!is_null($fwp_held_ping)) : // Syndicated post. Don't mark with _pingme
406 if ( defined('XMLRPC_REQUEST') )
407 do_action('xmlrpc_publish_post', $post_id);
408 if ( defined('APP_REQUEST') )
409 do_action('app_publish_post', $post_id);
410
411 if ( defined('WP_IMPORTING') )
412 return;
413
414 // Defer sending out pings until we finish updating
415 $fwp_held_ping = $post_id;
416 else :
417 if (function_exists('_publish_post_hook')) : // WordPress 2.3
418 _publish_post_hook($post_id);
419 endif;
420 endif;
421 }
422
423 ################################################################################
424 ## class FeedWordPress #########################################################
425 ################################################################################
426
427 // class FeedWordPress: handles feed updates and plugs in to the XML-RPC interface
428 class FeedWordPress {
429 var $strip_attrs = array (
430 array('[a-z]+', 'style'),
431 array('[a-z]+', 'target'),
432 );
433 var $uri_attrs = array (
434 array('a', 'href'),
435 array('applet', 'codebase'),
436 array('area', 'href'),
437 array('blockquote', 'cite'),
438 array('body', 'background'),
439 array('del', 'cite'),
440 array('form', 'action'),
441 array('frame', 'longdesc'),
442 array('frame', 'src'),
443 array('iframe', 'longdesc'),
444 array('iframe', 'src'),
445 array('head', 'profile'),
446 array('img', 'longdesc'),
447 array('img', 'src'),
448 array('img', 'usemap'),
449 array('input', 'src'),
450 array('input', 'usemap'),
451 array('ins', 'cite'),
452 array('link', 'href'),
453 array('object', 'classid'),
454 array('object', 'codebase'),
455 array('object', 'data'),
456 array('object', 'usemap'),
457 array('q', 'cite'),
458 array('script', 'src')
459 );
460
461 var $feeds = NULL;
462
463 # function FeedWordPress (): Contructor; retrieve a list of feeds
464 function FeedWordPress () {
465 $this->feeds = array ();
466 $links = FeedWordPress::syndicated_links();
467 if ($links): foreach ($links as $link):
468 $this->feeds[] =& new SyndicatedLink($link);
469 endforeach; endif;
470 } // FeedWordPress::FeedWordPress ()
471
472 # function update (): polls for updates on one or more Contributor feeds
473 #
474 # Arguments:
475 # ----------
476 # * $uri (string): either the URI of the feed to poll, the URI of the
477 # (human-readable) website whose feed you want to poll, or NULL.
478 #
479 # If $uri is NULL, then FeedWordPress will poll any feeds that are
480 # ready for polling. It will not poll feeds that are marked as
481 # "Invisible" Links (signifying that the subscription has been
482 # de-activated), or feeds that are not yet stale according to their
483 # TTL setting (which is either set in the feed, or else set
484 # randomly within a window of 30 minutes - 2 hours).
485 #
486 # Returns:
487 # --------
488 # * Normally returns an associative array, with 'new' => the number
489 # of new posts added during the update, and 'updated' => the number
490 # of old posts that were updated during the update. If both numbers
491 # are zero, there was no change since the last poll on that URI.
492 #
493 # * Returns NULL if URI it was passed was not a URI that this
494 # installation of FeedWordPress syndicates.
495 #
496 # Effects:
497 # --------
498 # * One or more feeds are polled for updates
499 #
500 # * If the feed Link does not have a hardcoded name set, its Link
501 # Name is synchronized with the feed's title element
502 #
503 # * If the feed Link does not have a hardcoded URI set, its Link URI
504 # is synchronized with the feed's human-readable link element
505 #
506 # * If the feed Link does not have a hardcoded description set, its
507 # Link Description is synchronized with the feed's description,
508 # tagline, or subtitle element.
509 #
510 # * The time of polling is recorded in the feed's settings, and the
511 # TTL (time until the feed is next available for polling) is set
512 # either from the feed (if it is supplied in the ttl or syndication
513 # module elements) or else from a randomly-generated time window
514 # (between 30 minutes and 2 hours).
515 #
516 # * New posts from the polled feed are added to the WordPress store.
517 #
518 # * Updates to existing posts since the last poll are mirrored in the
519 # WordPress store.
520 #
521 function update ($uri = null) {
522 global $wpdb;
523
524 if (FeedWordPress::needs_upgrade()) : // Will make duplicate posts if we don't hold off
525 return NULL;
526 endif;
527
528 if (!is_null($uri)) :
529 $uri = trim($uri);
530 else : // Update all
531 update_option('feedwordpress_last_update_all', time());
532 endif;
533
534 do_action('feedwordpress_update', $uri);
535
536 // Loop through and check for new posts
537 $delta = NULL;
538 foreach ($this->feeds as $feed) :
539 $pinged_that = (is_null($uri) or in_array($uri, array($feed->uri(), $feed->homepage())));
540
541 if (!is_null($uri)) : // A site-specific ping always updates
542 $timely = true;
543 else :
544 $timely = $feed->stale();
545 endif;
546
547 if ($pinged_that and is_null($delta)) : // If at least one feed was hit for updating...
548 $delta = array('new' => 0, 'updated' => 0); // ... don't return error condition
549 endif;
550
551 if ($pinged_that and $timely) :
552 do_action('feedwordpress_check_feed', $feed->settings);
553 $added = $feed->poll();
554 if (isset($added['new'])) : $delta['new'] += $added['new']; endif;
555 if (isset($added['updated'])) : $delta['updated'] += $added['updated']; endif;
556 endif;
557 endforeach;
558
559 do_action('feedwordpress_update_complete', $delta);
560
561 return $delta;
562 }
563
564 function stale () {
565 if (get_option('feedwordpress_automatic_updates')) :
566 $last = get_option('feedwordpress_last_update_all');
567
568 // If we haven't updated all yet, give it a time window
569 if (false === $last) :
570 $ret = false;
571 update_option('feedwordpress_last_update_all', time());
572
573 // Otherwise, check against freshness interval
574 elseif (is_numeric($last)) : // Expect a timestamp
575 $freshness = get_option('feedwordpress_freshness');
576 if (false === $freshness) : // Use default
577 $freshness = FEEDWORDPRESS_FRESHNESS_INTERVAL;
578 endif;
579 $ret = ( (time() - $last) > $freshness);
580
581 // This should never happen.
582 else :
583 FeedWordPress::critical_bug('FeedWordPress::stale::last', $last, __LINE__);
584 endif;
585
586 // Explicit request for an update (e.g. from a cron job).
587 elseif (FeedWordPress::update_requested()) :
588 $ret = true;
589
590 else :
591 $ret = false;
592 endif;
593 return $ret;
594 } // FeedWordPress::stale()
595
596 function update_requested () {
597 return (isset($_REQUEST['update_feedwordpress']) and $_REQUEST['update_feedwordpress']);
598 } // FeedWordPress::update_requested()
599
600 function syndicate_link ($name, $uri, $rss) {
601 global $wpdb;
602
603 // Get the category ID#
604 $cat_id = FeedWordPress::link_category_id();
605
606 // WordPress gets cranky if there's no homepage URI
607 if (!isset($uri) or strlen($uri)<1) : $uri = $rss; endif;
608
609 if (function_exists('wp_insert_link')) { // WordPress 2.x
610 $link_id = wp_insert_link(array(
611 "link_name" => $name,
612 "link_url" => $uri,
613 "link_category" => array($cat_id),
614 "link_rss" => $rss
615 ));
616 } else { // WordPress 1.5.x
617 $result = $wpdb->query("
618 INSERT INTO $wpdb->links
619 SET
620 link_name = '".$wpdb->escape($name)."',
621 link_url = '".$wpdb->escape($uri)."',
622 link_category = '".$wpdb->escape($cat_id)."',
623 link_rss = '".$wpdb->escape($rss)."'
624 ");
625 $link_id = $wpdb->insert_id;
626 } // if
627 return $link_id;
628 } // function FeedWordPress::syndicate_link()
629
630 function on_unfamiliar ($what = 'author', $override = NULL) {
631 $set = array(
632 'author' => array('create', 'default', 'filter'),
633 'category' => array('create', 'tag', 'default', 'filter'),
634 );
635
636 if (is_string($override)) :
637 $ret = strtolower($override);
638 endif;
639
640 if (!is_numeric($override) and !in_array($ret, $set[$what])) :
641 $ret = get_option('feedwordpress_unfamiliar_'.$what);
642 if (!is_numeric($ret) and !in_array($ret, $set[$what])) :
643 $ret = 'create';
644 endif;
645 endif;
646
647 return $ret;
648 } // function FeedWordPress::on_unfamiliar()
649
650 function syndicated_links () {
651 $contributors = FeedWordPress::link_category_id();
652 if (function_exists('get_bookmarks')) :
653 $links = get_bookmarks(array("category" => $contributors));
654 else:
655 $links = get_linkobjects($contributors); // deprecated as of WP 2.1
656 endif;
657 return $links;
658 } // function FeedWordPress::syndicated_links()
659
660 function link_category_id () {
661 global $wpdb, $wp_db_version;
662
663 $cat_id = get_option('feedwordpress_cat_id');
664
665 // If we don't yet *have* the category, we'll have to create it
666 if ($cat_id === false) :
667 $cat = $wpdb->escape(DEFAULT_SYNDICATION_CATEGORY);
668
669 // Look for something with the right name...
670 // -----------------------------------------
671
672 // WordPress 2.3 introduces a new taxonomy/term API
673 if (function_exists('is_term')) :
674 $cat_id = is_term($cat, 'link_category');
675 // WordPress 2.1 and 2.2 use a common table for both link and post categories
676 elseif (isset($wp_db_version) and ($wp_db_version >= FWP_SCHEMA_21 and $wp_db_version < FWP_SCHEMA_23) ) :
677 $cat_id = $wpdb->get_var("SELECT cat_id FROM {$wpdb->categories} WHERE cat_name='$cat'");
678 // WordPress 1.5 and 2.0.x have a separate table for link categories
679 elseif (!isset($wp_db_version) or $wp_db_version < FWP_SCHEMA_21) :
680 $cat_id = $wpdb->get_var("SELECT cat_id FROM {$wpdb->linkcategories} WHERE cat_name='$cat'");
681 // This should never happen.
682 else :
683 FeedWordPress::critical_bug('FeedWordPress::link_category_id::wp_db_version', $wp_db_version, __LINE__);
684 endif;
685
686 // If you still can't find anything, make it for yourself.
687 // -------------------------------------------------------
688 if (!$cat_id) :
689 // WordPress 2.3+ term/taxonomy API
690 if (function_exists('wp_insert_term')) :
691 $term = wp_insert_term($cat, 'link_category');
692 $cat_id = $term['term_id'];
693 // WordPress 2.1, 2.2 category API. By the way, why the fuck is this API function only available in a wp-admin module?
694 elseif (function_exists('wp_insert_category')) :
695 $cat_id = wp_insert_category(array('cat_name' => $cat));
696 // WordPress 1.5 and 2.0.x
697 elseif (!isset($wp_db_version) or $wp_db_version < FWP_SCHEMA_21) :
698 $result = $wpdb->query("
699 INSERT INTO $wpdb->linkcategories
700 SET
701 cat_id = 0,
702 cat_name='$cat',
703 show_images='N',
704 show_description='N',
705 show_rating='N',
706 show_updated='N',
707 sort_order='name'
708 ");
709 $cat_id = $wpdb->insert_id;
710 // This should never happen.
711 else :
712 FeedWordPress::critical_bug('FeedWordPress::link_category_id::wp_db_version', $wp_db_version, __LINE__);
713 endif;
714 endif;
715
716 update_option('feedwordpress_cat_id', $cat_id);
717 endif;
718 return $cat_id;
719 } // function FeedWordPress::link_category_id()
720
721 # Upgrades and maintenance...
722 function needs_upgrade () {
723 global $wpdb;
724 $fwp_db_version = get_option('feedwordpress_version');
725 $ret = false; // innocent until proven guilty
726 if (!$fwp_db_version or $fwp_db_version < FEEDWORDPRESS_VERSION) :
727 // This is an older version or a fresh install. Does it
728 // require a database upgrade or database initialization?
729 if ($fwp_db_version > 0.96) :
730 // No. Just brand it with the new version.
731 update_option('feedwordpress_version', FEEDWORDPRESS_VERSION);
732 else :
733 // Yes. Check to see whether this is a fresh install or an upgrade.
734 $syn = $wpdb->get_col("
735 SELECT post_id
736 FROM $wpdb->postmeta
737 WHERE meta_key = 'syndication_feed'
738 ");
739 if (count($syn) > 0) : // contains at least one syndicated post
740 $ret = true;
741 else : // fresh install; brand it as ours
742 update_option('feedwordpress_version', FEEDWORDPRESS_VERSION);
743 endif;
744 endif;
745 endif;
746 return $ret;
747 }
748
749 function upgrade_database ($from = NULL) {
750 global $wpdb;
751
752 if (is_null($from) or $from <= 0.96) : $from = 0.96; endif;
753
754 switch ($from) :
755 case 0.96: // account for changes to syndication custom values and guid
756 echo "<p>Upgrading database from {$from} to ".FEEDWORDPRESS_VERSION."...</p>\n";
757
758 $cat_id = FeedWordPress::link_category_id();
759
760 // Avoid duplicates
761 $wpdb->query("DELETE FROM `{$wpdb->postmeta}` WHERE meta_key = 'syndication_feed_id'");
762
763 // Look up all the link IDs
764 $wpdb->query("
765 CREATE TEMPORARY TABLE tmp_custom_values
766 SELECT
767 NULL AS meta_id,
768 post_id,
769 'syndication_feed_id' AS meta_key,
770 link_id AS meta_value
771 FROM `{$wpdb->postmeta}`, `{$wpdb->links}`
772 WHERE
773 meta_key='syndication_feed'
774 AND meta_value=link_rss
775 AND link_category = {$cat_id}
776 ");
777
778 // Now attach them to their posts
779 $wpdb->query("INSERT INTO `{$wpdb->postmeta}` SELECT * FROM tmp_custom_values");
780
781 // And clean up after ourselves.
782 $wpdb->query("DROP TABLE tmp_custom_values");
783
784 // Now fix the guids to avoid duplicate posts
785 echo "<ul>";
786 foreach ($this->feeds as $feed) :
787 echo "<li>Fixing post meta-data for <cite>".$feed['link/name']."</cite> &#8230; "; flush();
788 $rss = @fetch_rss($feed['link/uri']);
789 if (is_array($rss->items)) :
790 foreach ($rss->items as $item) :
791 $guid = $wpdb->escape(FeedWordPress::guid($item, $feed)); // new GUID algorithm
792 $link = $wpdb->escape($item['link']);
793
794 $wpdb->query("
795 UPDATE `{$wpdb->posts}` SET guid='{$guid}' WHERE guid='{$link}'
796 ");
797 endforeach;
798 endif;
799 echo "<strong>complete.</strong></li>\n";
800 endforeach;
801 echo "</ul>\n";
802
803 // Mark the upgrade as successful.
804 update_option('feedwordpress_version', FEEDWORDPRESS_VERSION);
805 endswitch;
806 echo "<p>Upgrade complete. FeedWordPress is now ready to use again.</p>";
807 } /* FeedWordPress::upgrade_database() */
808
809 # Utility functions for handling text settings
810 function negative ($f, $setting) {
811 $nego = array ('n', 'no', 'f', 'false');
812 return (isset($f[$setting]) and in_array(strtolower($f[$setting]), $nego));
813 }
814
815 function affirmative ($f, $setting) {
816 $affirmo = array ('y', 'yes', 't', 'true', 1);
817 return (isset($f[$setting]) and in_array(strtolower($f[$setting]), $affirmo));
818 }
819
820
821 # Internal debugging functions
822 function critical_bug ($varname, $var, $line) {
823 global $wp_version;
824
825 echo '<p>There may be a bug in FeedWordPress. Please <a href="'.FEEDWORDPRESS_AUTHOR_CONTACT.'">contact the author</a> and paste the following information into your e-mail:</p>';
826 echo "\n<plaintext>";
827 echo "Triggered at line # ".$line."\n";
828 echo "FeedWordPress version: ".FEEDWORDPRESS_VERSION."\n";
829 echo "WordPress version: $wp_version\n";
830 echo "PHP version: ".phpversion()."\n";
831 echo "\n";
832 echo $varname.": "; var_dump($var); echo "\n";
833 die;
834 }
835 } // class FeedWordPress
836
837 class SyndicatedPost {
838 var $item = null;
839
840 var $link = null;
841 var $feed = null;
842 var $feedmeta = null;
843
844 var $post = array ();
845 var $_base = null;
846
847 var $_freshness = null;
848 var $_wp_id = null;
849
850 function SyndicatedPost ($item, $link) {
851 global $wpdb;
852
853 $this->link = $link;
854 $feedmeta = $link->settings;
855 $feed = $link->magpie;
856
857 # This is ugly as all hell. I'd like to use apply_filters()'s
858 # alleged support for a variable argument count, but this seems
859 # to have been broken in WordPress 1.5. It'll be fixed somehow
860 # in WP 1.5.1, but I'm aiming at WP 1.5 compatibility across
861 # the board here.
862 #
863 # Cf.: <http://mosquito.wordpress.org/view.php?id=901>
864 global $fwp_channel, $fwp_feedmeta;
865 $fwp_channel = $feed; $fwp_feedmeta = $feedmeta;
866
867 $this->feed = $feed;
868 $this->feedmeta = $feedmeta;
869
870 $this->item = $item;
871 $this->item = apply_filters('syndicated_item', $this->item, $this);
872
873 # Filters can halt further processing by returning NULL
874 if (is_null($this->item)) :
875 $this->post = NULL;
876 else :
877 # Note that nothing is run through $wpdb->escape() here.
878 # That's deliberate. The escaping is done at the point
879 # of insertion, not here, to avoid double-escaping and
880 # to avoid screwing with syndicated_post filters
881
882 $this->post['post_title'] = apply_filters('syndicated_item_title', $this->item['title'], $this);
883
884 // This just gives us an alphanumeric representation of
885 // the author. We will look up (or create) the numeric
886 // ID for the author in SyndicatedPost::add()
887 $this->post['named']['author'] = apply_filters('syndicated_item_author', $this->author(), $this);
888
889 # Identify content and sanitize it.
890 # ---------------------------------
891 if (isset($this->item['xhtml']['body'])) :
892 $content = $this->item['xhtml']['body'];
893 elseif (isset($this->item['xhtml']['div'])) :
894 $content = $this->item['xhtml']['div'];
895 elseif (isset($this->item['content']['encoded']) and $this->item['content']['encoded']):
896 $content = $this->item['content']['encoded'];
897 else:
898 $content = $this->item['description'];
899 endif;
900 $this->post['post_content'] = apply_filters('syndicated_item_content', $content, $this);
901
902 # Identify and sanitize excerpt
903 $excerpt = NULL;
904 if ( isset($this->item['description']) and $this->item['description'] ) :
905 $excerpt = $this->item['description'];
906 elseif ( isset($content) and $content ) :
907 $excerpt = strip_tags($content);
908 if (strlen($excerpt) > 255) :
909 $excerpt = substr($excerpt,0,252).'...';
910 endif;
911 endif;
912 $excerpt = apply_filters('syndicated_item_excerpt', $excerpt, $this);
913
914 if (!is_null($excerpt)):
915 $this->post['post_excerpt'] = $excerpt;
916 endif;
917
918 // This is unnecessary if we use wp_insert_post
919 if (!$this->use_api('wp_insert_post')) :
920 $this->post['post_name'] = sanitize_title($this->post['post_title']);
921 endif;
922
923 $this->post['epoch']['issued'] = apply_filters('syndicated_item_published', $this->published(), $this);
924 $this->post['epoch']['created'] = apply_filters('syndicated_item_created', $this->created(), $this);
925 $this->post['epoch']['modified'] = apply_filters('syndicated_item_updated', $this->updated(), $this);
926
927 // Dealing with timestamps in WordPress is so fucking fucked.
928 $offset = (int) get_option('gmt_offset') * 60 * 60;
929 $this->post['post_date'] = gmdate('Y-m-d H:i:s', $this->published() + $offset);
930 $this->post['post_modified'] = gmdate('Y-m-d H:i:s', $this->updated() + $offset);
931 $this->post['post_date_gmt'] = gmdate('Y-m-d H:i:s', $this->published());
932 $this->post['post_modified_gmt'] = gmdate('Y-m-d H:i:s', $this->updated());
933
934 // Use feed-level preferences or the global default.
935 $this->post['post_status'] = $this->link->syndicated_status('post', 'publish');
936 $this->post['comment_status'] = $this->link->syndicated_status('comment', 'closed');
937 $this->post['ping_status'] = $this->link->syndicated_status('ping', 'closed');
938
939 // Unique ID (hopefully a unique tag: URI); failing that, the permalink
940 $this->post['guid'] = apply_filters('syndicated_item_guid', $this->guid(), $this);
941
942 // RSS 2.0 / Atom 1.0 enclosure support
943 if ( isset($this->item['enclosure#']) ) :
944 for ($i = 1; $i <= $this->item['enclosure#']; $i++) :
945 $eid = (($i > 1) ? "#{$id}" : "");
946 $this->post['meta']['enclosure'][] =
947 apply_filters('syndicated_item_enclosure_url', $this->item["enclosure{$eid}@url"], $this)."\n".
948 apply_filters('syndicated_item_enclosure_length', $this->item["enclosure{$eid}@length"], $this)."\n".
949 apply_filters('syndicated_item_enclosure_type', $this->item["enclosure{$eid}@type"], $this);
950 endfor;
951 endif;
952
953 // In case you want to point back to the blog this was syndicated from
954 if (isset($this->feed->channel['title'])) :
955 $this->post['meta']['syndication_source'] = apply_filters('syndicated_item_source_title', $this->feed->channel['title'], $this);
956 endif;
957 if (isset($this->feed->channel['link'])) :
958 $this->post['meta']['syndication_source_uri'] = apply_filters('syndicated_item_source_link', $this->feed->channel['link'], $this);
959 endif;
960
961 // Store information on human-readable and machine-readable comment URIs
962 if (isset($this->item['comments'])) :
963 $this->post['meta']['rss:comments'] = apply_filters('syndicated_item_comments', $this->item['comments']);
964 endif;
965 if (isset($this->item['wfw']['commentrss'])) :
966 $this->post['meta']['wfw:commentRSS'] = apply_filters('syndicated_item_commentrss', $this->item['wfw']['commentrss']);
967 endif;
968
969 // Store information to identify the feed that this came from
970 $this->post['meta']['syndication_feed'] = $this->feedmeta['link/uri'];
971 $this->post['meta']['syndication_feed_id'] = $this->feedmeta['link/id'];
972
973 // In case you want to know the external permalink...
974 $this->post['meta']['syndication_permalink'] = apply_filters('syndicated_item_link', $this->item['link']);
975
976 // Store a hash of the post content for checking whether something needs to be updated
977 $this->post['meta']['syndication_item_hash'] = $this->update_hash();
978
979 // Feed-by-feed options for author and category creation
980 $this->post['named']['unfamiliar']['author'] = $this->feedmeta['unfamiliar author'];
981 $this->post['named']['unfamiliar']['category'] = $this->feedmeta['unfamiliar category'];
982
983 // Categories: start with default categories, if any
984 $fc = get_option("feedwordpress_syndication_cats");
985 if ($fc) :
986 $this->post['named']['preset/category'] = explode("\n", $fc);
987 else :
988 $this->post['named']['preset/category'] = array();
989 endif;
990
991 if (is_array($this->feedmeta['cats'])) :
992 $this->post['named']['preset/category'] = array_merge($this->post['named']['preset/category'], $this->feedmeta['cats']);
993 endif;
994
995 // Now add categories from the post, if we have 'em
996 $this->post['named']['category'] = array();
997 if ( isset($this->item['category#']) ) :
998 for ($i = 1; $i <= $this->item['category#']; $i++) :
999 $cat_idx = (($i > 1) ? "#{$i}" : "");
1000 $cat = $this->item["category{$cat_idx}"];
1001
1002 if ( isset($this->feedmeta['cat_split']) and strlen($this->feedmeta['cat_split']) > 0) :
1003 $pcre = "\007".$this->feedmeta['cat_split']."\007";
1004 $this->post['named']['category'] = array_merge($this->post['named']['category'], preg_split($pcre, $cat, -1 /*=no limit*/, PREG_SPLIT_NO_EMPTY));
1005 else :
1006 $this->post['named']['category'][] = $cat;
1007 endif;
1008 endfor;
1009 endif;
1010 $this->post['named']['category'] = apply_filters('syndicated_item_categories', $this->post['named']['category'], $this);
1011
1012 // Tags: start with default tags, if any
1013 $ft = get_option("feedwordpress_syndication_tags");
1014 if ($ft) :
1015 $this->post['tags_input'] = explode('FEEDWORDPRESS_CAT_SEPARATOR', $ft);
1016 else :
1017 $this->post['tags_input'] = array();
1018 endif;
1019
1020 if (is_array($this->feedmeta['tags'])) :
1021 $this->post['tags_input'] = array_merge($this->post['tags_input'], $this->feedmeta['tags']);
1022 endif;
1023
1024 endif;
1025 } // SyndicatedPost::SyndicatedPost()
1026
1027 function filtered () {
1028 return is_null($this->post);
1029 }
1030
1031 function freshness () {
1032 global $wpdb;
1033
1034 if ($this->filtered()) : // This should never happen.
1035 FeedWordPress::critical_bug('SyndicatedPost', $this, __LINE__);
1036 endif;
1037
1038 if (is_null($this->_freshness)) :
1039 $guid = $wpdb->escape($this->guid());
1040
1041 $result = $wpdb->get_row("
1042 SELECT id, guid, post_modified_gmt
1043 FROM $wpdb->posts WHERE guid='$guid'
1044 ");
1045
1046 if (!$result) :
1047 $this->_freshness = 2; // New content
1048 else:
1049 $stored_update_hashes = get_post_custom_values('syndication_item_hash', $result->id);
1050 if (count($stored_update_hashes) > 0) :
1051 $stored_update_hash = $stored_update_hashes[0];
1052 $update_hash_changed = ($stored_update_hash != $this->update_hash());
1053 else :
1054 $update_hash_changed = false;
1055 endif;
1056
1057 preg_match('/([0-9]+)-([0-9]+)-([0-9]+) ([0-9]+):([0-9]+):([0-9]+)/', $result->post_modified_gmt, $backref);
1058
1059 $last_rev_ts = gmmktime($backref[4], $backref[5], $backref[6], $backref[2], $backref[3], $backref[1]);
1060 $updated_ts = $this->updated(/*fallback=*/ true, /*default=*/ NULL);
1061 $updated = ((
1062 !is_null($updated_ts)
1063 and ($updated_ts > $last_rev_ts)
1064 ) or $update_hash_changed);
1065
1066 if ($updated) :
1067 $this->_freshness = 1; // Updated content
1068 $this->_wp_id = $result->id;
1069 else :
1070 $this->_freshness = 0; // Same old, same old
1071 $this->_wp_id = $result->id;
1072 endif;
1073 endif;
1074 endif;
1075 return $this->_freshness;
1076 }
1077
1078 function wp_id () {
1079 if ($this->filtered()) : // This should never happen.
1080 FeedWordPress::critical_bug('SyndicatedPost', $this, __LINE__);
1081 endif;
1082
1083 if (is_null($this->_wp_id) and is_null($this->_freshness)) :
1084 $fresh = $this->freshness(); // sets WP DB id in the process
1085 endif;
1086 return $this->_wp_id;
1087 }
1088
1089 function store () {
1090 global $wpdb;
1091
1092 if ($this->filtered()) : // This should never happen.
1093 FeedWordPress::critical_bug('SyndicatedPost', $this, __LINE__);
1094 endif;
1095
1096 $freshness = $this->freshness();
1097 if ($freshness > 0) :
1098 # -- Look up, or create, numeric ID for author
1099 $this->post['post_author'] = $this->author_id (
1100 FeedWordPress::on_unfamiliar('author', $this->post['named']['unfamiliar']['author'])
1101 );
1102
1103 if (is_null($this->post['post_author'])) :
1104 $this->post = NULL;
1105 endif;
1106 endif;
1107
1108 if (!$this->filtered() and $freshness > 0) :
1109 # -- Look up, or create, numeric ID for categories
1110 list($pcats, $ptags) = $this->category_ids (
1111 $this->post['named']['category'],
1112 FeedWordPress::on_unfamiliar('category', $this->post['named']['unfamiliar']['category']),
1113 /*tags_too=*/ true
1114 );
1115
1116 $this->post['post_category'] = $pcats;
1117 $this->post['tags_input'] = array_merge($this->post['tags_input'], $ptags);
1118
1119 if (is_null($this->post['post_category'])) :
1120 // filter mode on, no matching categories; drop the post
1121 $this->post = NULL;
1122 else :
1123 // filter mode off or at least one match; now add on the feed and global presets
1124 $this->post['post_category'] = array_merge (
1125 $this->post['post_category'],
1126 $this->category_ids (
1127 $this->post['named']['preset/category'],
1128 'default'
1129 )
1130 );
1131
1132 if (count($this->post['post_category']) < 1) :
1133 $this->post['post_category'][] = 1; // Default to category 1 ("Uncategorized" / "General") if nothing else
1134 endif;
1135 endif;
1136 endif;
1137
1138 if (!$this->filtered() and $freshness > 0) :
1139 unset($this->post['named']);
1140 $this->post = apply_filters('syndicated_post', $this->post, $this);
1141 endif;
1142
1143 if (!$this->filtered() and $freshness == 2) :
1144 // The item has not yet been added. So let's add it.
1145 $this->insert_new();
1146 $this->add_rss_meta();
1147 do_action('post_syndicated_item', $this->wp_id());
1148
1149 $ret = 'new';
1150 elseif (!$this->filtered() and $freshness == 1) :
1151 $this->post['ID'] = $this->wp_id();
1152 $this->update_existing();
1153 $this->add_rss_meta();
1154 do_action('update_syndicated_item', $this->wp_id());
1155
1156 $ret = 'updated';
1157 else :
1158 $ret = false;
1159 endif;
1160
1161 return $ret;
1162 } // function SyndicatedPost::store ()
1163
1164 function insert_new () {
1165 global $wpdb, $wp_db_version;
1166
1167 // Why the fuck doesn't wp_insert_post already do this?
1168 foreach ($this->post as $key => $value) :
1169 if (is_string($value)) :
1170 $dbpost[$key] = $wpdb->escape($value);
1171 else :
1172 $dbpost[$key] = $value;
1173 endif;
1174 endforeach;
1175
1176 if (strlen($dbpost['post_title'].$dbpost['post_content'].$dbpost['post_excerpt']) == 0) :
1177 // FIXME: Option for filtering out empty posts
1178 endif;
1179 if (strlen($dbpost['post_title'])==0) :
1180 $dbpost['post_title'] = $this->post['meta']['syndication_source']
1181 .' '.gmdate('Y-m-d H:i:s', $this->published() + $offset);
1182 // FIXME: Option for what to fill a blank title with...
1183 endif;
1184
1185 if ($this->use_api('wp_insert_post')) :
1186 $dbpost['post_pingback'] = false; // Tell WP 2.1 and 2.2 not to process for pingbacks
1187
1188 // This is a ridiculous fucking kludge necessitated by WordPress 2.6 munging authorship meta-data
1189 add_action('_wp_put_post_revision', array($this, 'fix_revision_meta'));
1190
1191 // Kludge to prevent kses filters from stripping the
1192 // content of posts when updating without a logged in
1193 // user who has `unfiltered_html` capability.
1194 add_filter('content_save_pre', array($this, 'avoid_kses_munge'), 11);
1195
1196 $this->_wp_id = wp_insert_post($dbpost);
1197
1198 // Turn off ridiculous fucking kludges #1 and #2
1199 remove_action('_wp_put_post_revision', array($this, 'fix_revision_meta'));
1200 remove_filter('content_save_pre', array($this, 'avoid_kses_munge'), 11);
1201
1202 // This should never happen.
1203 if (!is_numeric($this->_wp_id) or ($this->_wp_id == 0)) :
1204 FeedWordPress::critical_bug('SyndicatedPost (_wp_id problem)', array("dbpost" => $dbpost, "this" => $this), __LINE__);
1205 endif;
1206
1207 // Unfortunately, as of WordPress 2.3, wp_insert_post()
1208 // *still* offers no way to use a guid of your choice,
1209 // and munges your post modified timestamp, too.
1210 $result = $wpdb->query("
1211 UPDATE $wpdb->posts
1212 SET
1213 guid='{$dbpost['guid']}',
1214 post_modified='{$dbpost['post_modified']}',
1215 post_modified_gmt='{$dbpost['post_modified_gmt']}'
1216 WHERE ID='{$this->_wp_id}'
1217 ");
1218 else :
1219 # The right way to do this is the above. But, alas,
1220 # in earlier versions of WordPress, wp_insert_post has
1221 # too much behavior (mainly related to pings) that can't
1222 # be overridden. In WordPress 1.5, it's enough of a
1223 # resource hog to make PHP segfault after inserting
1224 # 50-100 posts. This can get pretty annoying, especially
1225 # if you are trying to update your feeds for the first
1226 # time.
1227
1228 $result = $wpdb->query("
1229 INSERT INTO $wpdb->posts
1230 SET
1231 guid = '{$dbpost['guid']}',
1232 post_author = '{$dbpost['post_author']}',
1233 post_date = '{$dbpost['post_date']}',
1234 post_date_gmt = '{$dbpost['post_date_gmt']}',
1235 post_content = '{$dbpost['post_content']}',"
1236 .(isset($dbpost['post_excerpt']) ? "post_excerpt = '{$dbpost['post_excerpt']}'," : "")."
1237 post_title = '{$dbpost['post_title']}',
1238 post_name = '{$dbpost['post_name']}',
1239 post_modified = '{$dbpost['post_modified']}',
1240 post_modified_gmt = '{$dbpost['post_modified_gmt']}',
1241 comment_status = '{$dbpost['comment_status']}',
1242 ping_status = '{$dbpost['ping_status']}',
1243 post_status = '{$dbpost['post_status']}'
1244 ");
1245 $this->_wp_id = $wpdb->insert_id;
1246
1247 // This should never happen.
1248 if (!is_numeric($this->_wp_id) or ($this->_wp_id == 0)) :
1249 FeedWordPress::critical_bug('SyndicatedPost (_wp_id problem)', array("dbpost" => $dbpost, "this" => $this), __LINE__);
1250 endif;
1251
1252 // WordPress 1.5.x - 2.0.x
1253 wp_set_post_cats('1', $this->wp_id(), $this->post['post_category']);
1254
1255 // Since we are not going through official channels, we need to
1256 // manually tell WordPress that we've published a new post.
1257 // We need to make sure to do this in order for FeedWordPress
1258 // to play well with the staticize-reloaded plugin (something
1259 // that a large aggregator website is going to *want* to be
1260 // able to use).
1261 do_action('publish_post', $this->_wp_id);
1262 endif;
1263 } /* SyndicatedPost::insert_new() */
1264
1265 function update_existing () {
1266 global $wpdb;
1267
1268 // Why the fuck doesn't wp_insert_post already do this?
1269 $dbpost = array();
1270 foreach ($this->post as $key => $value) :
1271 if (is_string($value)) :
1272 $dbpost[$key] = $wpdb->escape($value);
1273 else :
1274 $dbpost[$key] = $value;
1275 endif;
1276 endforeach;
1277
1278 if ($this->use_api('wp_insert_post')) :
1279 $dbpost['post_pingback'] = false; // Tell WP 2.1 and 2.2 not to process for pingbacks
1280
1281 // This is a ridiculous fucking kludge necessitated by WordPress 2.6 munging authorship meta-data
1282 add_action('_wp_put_post_revision', array($this, 'fix_revision_meta'));
1283
1284 // Kludge to prevent kses filters from stripping the
1285 // content of posts when updating without a logged in
1286 // user who has `unfiltered_html` capability.
1287 add_filter('content_save_pre', array($this, 'avoid_kses_munge'), 11);
1288
1289 $this->_wp_id = wp_insert_post($dbpost);
1290
1291 // Turn off ridiculous fucking kludges #1 and #2
1292 remove_action('_wp_put_post_revision', array($this, 'fix_revision_meta'));
1293 remove_filter('content_save_pre', array($this, 'avoid_kses_munge'), 11);
1294
1295 // This should never happen.
1296 if (!is_numeric($this->_wp_id) or ($this->_wp_id == 0)) :
1297 FeedWordPress::critical_bug('SyndicatedPost (_wp_id problem)', array("dbpost" => $dbpost, "this" => $this), __LINE__);
1298 endif;
1299
1300 // Unfortunately, as of WordPress 2.3, wp_insert_post()
1301 // munges your post modified timestamp.
1302 $result = $wpdb->query("
1303 UPDATE $wpdb->posts
1304 SET
1305 post_modified='{$dbpost['post_modified']}',
1306 post_modified_gmt='{$dbpost['post_modified_gmt']}'
1307 WHERE ID='{$this->_wp_id}'
1308 ");
1309 else :
1310
1311 $result = $wpdb->query("
1312 UPDATE $wpdb->posts
1313 SET
1314 post_author = '{$dbpost['post_author']}',
1315 post_content = '{$dbpost['post_content']}',"
1316 .(isset($dbpost['post_excerpt']) ? "post_excerpt = '{$dbpost['post_excerpt']}'," : "")."
1317 post_title = '{$dbpost['post_title']}',
1318 post_name = '{$dbpost['post_name']}',
1319 post_modified = '{$dbpost['post_modified']}',
1320 post_modified_gmt = '{$dbpost['post_modified_gmt']}'
1321 WHERE guid='{$dbpost['guid']}'
1322 ");
1323
1324 // WordPress 2.1.x and up
1325 if (function_exists('wp_set_post_categories')) :
1326 wp_set_post_categories($this->wp_id(), $this->post['post_category']);
1327 // WordPress 1.5.x - 2.0.x
1328 elseif (function_exists('wp_set_post_cats')) :
1329 wp_set_post_cats('1', $this->wp_id(), $this->post['post_category']);
1330 // This should never happen.
1331 else :
1332 FeedWordPress::critical_bug('SyndicatedPost (_wp_id problem)', array("dbpost" => $dbpost, "this" => $this), __LINE__);
1333 endif;
1334
1335 // Since we are not going through official channels, we need to
1336 // manually tell WordPress that we've published a new post.
1337 // We need to make sure to do this in order for FeedWordPress
1338 // to play well with the staticize-reloaded plugin (something
1339 // that a large aggregator website is going to *want* to be
1340 // able to use).
1341 do_action('edit_post', $this->post['ID']);
1342 endif;
1343 } /* SyndicatedPost::update_existing() */
1344
1345 /**
1346 * SyndicatedPost::fix_revision_meta() - Fixes the way WP 2.6+ fucks up
1347 * meta-data (authorship, etc.) when storing revisions of an updated
1348 * syndicated post.
1349 *
1350 * In their infinite wisdom, the WordPress coders have made it completely
1351 * impossible for a plugin that uses wp_insert_post() to set certain
1352 * meta-data (such as the author) when you store an old revision of an
1353 * updated post. Instead, it uses the WordPress defaults (= currently
1354 * active user ID if the process is running with a user logged in, or
1355 * = #0 if there is no user logged in). This results in bogus authorship
1356 * data for revisions that are syndicated from off the feed, unless we
1357 * use a ridiculous kludge like this to end-run the munging of meta-data
1358 * by _wp_put_post_revision.
1359 *
1360 * @param int $revision_id The revision ID to fix up meta-data
1361 */
1362 function fix_revision_meta ($revision_id) {
1363 global $wpdb;
1364
1365 $post_author = (int) $this->post['post_author'];
1366
1367 $revision_id = (int) $revision_id;
1368 $wpdb->query("
1369 UPDATE $wpdb->posts
1370 SET post_author={$this->post['post_author']}
1371 WHERE post_type = 'revision' AND ID='$revision_id'
1372 ");
1373 } /* SyndicatedPost::fix_revision_meta () */
1374
1375 /**
1376 * SyndicatedPost::avoid_kses_munge() -- If FeedWordPress is processing
1377 * an automatic update, that generally means that wp_insert_post() is
1378 * being called under the user credentials of whoever is viewing the
1379 * blog at the time -- usually meaning no user at all. But if WordPress
1380 * gets a wp_insert_post() when current_user_can('unfiltered_html') is
1381 * false, it will run the content of the post through a kses function
1382 * that strips out lots of HTML tags -- notably <object> and some others.
1383 * This causes problems for syndicating (for example) feeds that contain
1384 * YouTube videos. It also produces an unexpected asymmetry between
1385 * automatically-initiated updates and updates initiated manually from
1386 * the WordPress Dashboard (which are usually initiated under the
1387 * credentials of a logged-in admin, and so don't get run through the
1388 * kses function). So, to avoid the whole mess, what we do here is
1389 * just forcibly disable the kses munging for a single syndicated post,
1390 * by restoring the contents of the `post_content` field.
1391 *
1392 * @param string $content The content of the post, after other filters have gotten to it
1393 * @return string The original content of the post, before other filters had a chance to munge it.
1394 */
1395 function avoid_kses_munge ($content) {
1396 global $wpdb;
1397 return $wpdb->escape($this->post['post_content']);
1398 }
1399
1400 // SyndicatedPost::add_rss_meta: adds interesting meta-data to each entry
1401 // using the space for custom keys. The set of keys and values to add is
1402 // specified by the keys and values of $post['meta']. This is used to
1403 // store anything that the WordPress user might want to access from a
1404 // template concerning the post's original source that isn't provided
1405 // for by standard WP meta-data (i.e., any interesting data about the
1406 // syndicated post other than author, title, timestamp, categories, and
1407 // guid). It's also used to hook into WordPress's support for
1408 // enclosures.
1409 function add_rss_meta () {
1410 global $wpdb;
1411 if ( is_array($this->post) and isset($this->post['meta']) and is_array($this->post['meta']) ) :
1412 $postId = $this->wp_id();
1413
1414 // Aggregated posts should NOT send out pingbacks.
1415 // WordPress 2.1-2.2 claim you can tell them not to
1416 // using $post_pingback, but they don't listen, so we
1417 // make sure here.
1418 $result = $wpdb->query("
1419 DELETE FROM $wpdb->postmeta
1420 WHERE post_id='$postId' AND meta_key='_pingme'
1421 ");
1422
1423 foreach ( $this->post['meta'] as $key => $values ) :
1424
1425 $key = $wpdb->escape($key);
1426
1427 // If this is an update, clear out the old
1428 // values to avoid duplication.
1429 $result = $wpdb->query("
1430 DELETE FROM $wpdb->postmeta
1431 WHERE post_id='$postId' AND meta_key='$key'
1432 ");
1433
1434 // Allow for either a single value or an array
1435 if (!is_array($values)) $values = array($values);
1436 foreach ( $values as $value ) :
1437 $value = $wpdb->escape($value);
1438 $result = $wpdb->query("
1439 INSERT INTO $wpdb->postmeta
1440 SET
1441 post_id='$postId',
1442 meta_key='$key',
1443 meta_value='$value'
1444 ");
1445 endforeach;
1446 endforeach;
1447 endif;
1448 } /* SyndicatedPost::add_rss_meta () */
1449
1450 // SyndicatedPost::author_id (): get the ID for an author name from
1451 // the feed. Create the author if necessary.
1452 function author_id ($unfamiliar_author = 'create') {
1453 global $wpdb, $wp_db_version; // test for WordPress 2.0 database schema
1454
1455 $a = $this->author();
1456 $author = $a['name'];
1457 $email = $a['email'];
1458 $url = $a['uri'];
1459
1460 // Never can be too careful...
1461 $login = sanitize_user($author, /*strict=*/ true);
1462 $login = apply_filters('pre_user_login', $login);
1463
1464 $nice_author = sanitize_title($author);
1465 $nice_author = apply_filters('pre_user_nicename', $nice_author);
1466
1467 $reg_author = $wpdb->escape(preg_quote($author));
1468 $author = $wpdb->escape($author);
1469 $email = $wpdb->escape($email);
1470 $url = $wpdb->escape($url);
1471
1472 // Check for an existing author rule....
1473 if (isset($this->link->settings['map authors']['name'][strtolower(trim($author))])) :
1474 $author_rule = $this->link->settings['map authors']['name'][strtolower(trim($author))];
1475 else :
1476 $author_rule = NULL;
1477 endif;
1478
1479 // User name is mapped to a particular author. If that author ID exists, use it.
1480 if (is_numeric($author_rule) and get_userdata((int) $author_rule)) :
1481 $id = (int) $author_rule;
1482
1483 // User name is filtered out
1484 elseif ('filter' == $author_rule) :
1485 $id = NULL;
1486
1487 else :
1488 // Check the database for an existing author record that might fit
1489
1490 #-- WordPress 1.5.x
1491 if (!isset($wp_db_version)) :
1492 $id = $wpdb->get_var(
1493 "SELECT ID from $wpdb->users
1494 WHERE
1495 TRIM(LCASE(user_login)) = TRIM(LCASE('$login')) OR
1496 (
1497 LENGTH(TRIM(LCASE(user_email))) > 0
1498 AND TRIM(LCASE(user_email)) = TRIM(LCASE('$email'))
1499 ) OR
1500 TRIM(LCASE(user_firstname)) = TRIM(LCASE('$author')) OR
1501 TRIM(LCASE(user_nickname)) = TRIM(LCASE('$author')) OR
1502 TRIM(LCASE(user_nicename)) = TRIM(LCASE('$nice_author')) OR
1503 TRIM(LCASE(user_description)) = TRIM(LCASE('$author')) OR
1504 (
1505 LOWER(user_description)
1506 RLIKE CONCAT(
1507 '(^|\\n)a\\.?k\\.?a\\.?( |\\t)*:?( |\\t)*',
1508 LCASE('$reg_author'),
1509 '( |\\t|\\r)*(\\n|\$)'
1510 )
1511 )
1512 ");
1513
1514 #-- WordPress 2.0+
1515 elseif ($wp_db_version >= 2966) :
1516
1517 // First try the user core data table.
1518 $id = $wpdb->get_var(
1519 "SELECT ID FROM $wpdb->users
1520 WHERE
1521 TRIM(LCASE(user_login)) = TRIM(LCASE('$login'))
1522 OR (
1523 LENGTH(TRIM(LCASE(user_email))) > 0
1524 AND TRIM(LCASE(user_email)) = TRIM(LCASE('$email'))
1525 )
1526 OR TRIM(LCASE(user_nicename)) = TRIM(LCASE('$nice_author'))
1527 ");
1528
1529 // If that fails, look for aliases in the user meta data table
1530 if (is_null($id)) :
1531 $id = $wpdb->get_var(
1532 "SELECT user_id FROM $wpdb->usermeta
1533 WHERE
1534 (meta_key = 'description' AND TRIM(LCASE(meta_value)) = TRIM(LCASE('$author')))
1535 OR (
1536 meta_key = 'description'
1537 AND TRIM(LCASE(meta_value))
1538 RLIKE CONCAT(
1539 '(^|\\n)a\\.?k\\.?a\\.?( |\\t)*:?( |\\t)*',
1540 TRIM(LCASE('$reg_author')),
1541 '( |\\t|\\r)*(\\n|\$)'
1542 )
1543 )
1544 ");
1545 endif;
1546 endif;
1547
1548 // ... if you don't find one, then do what you need to do
1549 if (is_null($id)) :
1550 if ($unfamiliar_author === 'create') :
1551 $userdata = array();
1552
1553 #-- user table data
1554 $userdata['ID'] = NULL; // new user
1555 $userdata['user_login'] = $login;
1556 $userdata['user_nicename'] = $nice_author;
1557 $userdata['user_pass'] = substr(md5(uniqid(microtime())), 0, 6); // just something random to lock it up
1558 $userdata['user_email'] = $email;
1559 $userdata['user_url'] = $url;
1560 $userdata['display_name'] = $author;
1561
1562 $id = wp_insert_user($userdata);
1563 elseif (is_numeric($unfamiliar_author) and get_userdata((int) $unfamiliar_author)) :
1564 $id = (int) $unfamiliar_author;
1565 elseif ($unfamiliar_author === 'default') :
1566 $id = 1;
1567 endif;
1568 endif;
1569 endif;
1570
1571 if ($id) :
1572 $this->link->settings['map authors']['name'][strtolower(trim($author))] = $id;
1573 endif;
1574 return $id;
1575 } // function SyndicatedPost::author_id ()
1576
1577 // look up (and create) category ids from a list of categories
1578 function category_ids ($cats, $unfamiliar_category = 'create', $tags_too = false) {
1579 global $wpdb;
1580
1581 // We need to normalize whitespace because (1) trailing
1582 // whitespace can cause PHP and MySQL not to see eye to eye on
1583 // VARCHAR comparisons for some versions of MySQL (cf.
1584 // <http://dev.mysql.com/doc/mysql/en/char.html>), and (2)
1585 // because I doubt most people want to make a semantic
1586 // distinction between 'Computers' and 'Computers '
1587 $cats = array_map('trim', $cats);
1588
1589 $tags = array();
1590
1591 $cat_ids = array ();
1592 foreach ($cats as $cat_name) :
1593 if (preg_match('/^{#([0-9]+)}$/', $cat_name, $backref)) :
1594 $cat_id = (int) $backref[1];
1595 if (function_exists('is_term') and is_term($cat_id, 'category')) :
1596 $cat_ids[] = $cat_id;
1597 elseif (get_category($cat_id)) :
1598 $cat_ids[] = $cat_id;
1599 endif;
1600 else :
1601 $esc = $wpdb->escape($cat_name);
1602 $resc = $wpdb->escape(preg_quote($cat_name));
1603
1604 // WordPress 2.3+
1605 if (function_exists('is_term')) :
1606 $cat_id = is_term($cat_name, 'category');
1607 if ($cat_id) :
1608 $cat_ids[] = $cat_id['term_id'];
1609 // There must be a better way to do this...
1610 elseif ($results = $wpdb->get_results(
1611 "SELECT term_id
1612 FROM $wpdb->term_taxonomy
1613 WHERE
1614 LOWER(description) RLIKE
1615 CONCAT('(^|\\n)a\\.?k\\.?a\\.?( |\\t)*:?( |\\t)*', LOWER('{$resc}'), '( |\\t|\\r)*(\\n|\$)')"
1616 )) :
1617 foreach ($results AS $term) :
1618 $cat_ids[] = (int) $term->term_id;
1619 endforeach;
1620 elseif ('tag'==$unfamiliar_category) :
1621 $tags[] = $cat_name;
1622 elseif ('create'===$unfamiliar_category) :
1623 $term = wp_insert_term($cat_name, 'category');
1624 $cat_ids[] = $term['term_id'];
1625 endif;
1626
1627 // WordPress 1.5.x - 2.2.x
1628 else :
1629 $results = $wpdb->get_results(
1630 "SELECT cat_ID
1631 FROM $wpdb->categories
1632 WHERE
1633 (LOWER(cat_name) = LOWER('$esc'))
1634 OR (LOWER(category_description)
1635 RLIKE CONCAT('(^|\\n)a\\.?k\\.?a\\.?( |\\t)*:?( |\\t)*', LOWER('{$resc}'), '( |\\t|\\r)*(\\n|\$)'))
1636 ");
1637 if ($results) :
1638 foreach ($results as $term) :
1639 $cat_ids[] = (int) $term->cat_ID;
1640 endforeach;
1641 elseif ('create'===$unfamiliar_category) :
1642 if (function_exists('wp_insert_category')) :
1643 $cat_id = wp_insert_category(array('cat_name' => $cat_name));
1644 // And into the database we go.
1645 else :
1646 $nice_kitty = sanitize_title($cat_name);
1647 $wpdb->query(sprintf("
1648 INSERT INTO $wpdb->categories
1649 SET
1650 cat_name='%s',
1651 category_nicename='%s'
1652 ", $wpdb->escape($cat_name), $nice_kitty
1653 ));
1654 $cat_id = $wpdb->insert_id;
1655 endif;
1656 $cat_ids[] = $cat_id;
1657 endif;
1658 endif;
1659 endif;
1660 endforeach;
1661
1662 if ((count($cat_ids) == 0) and ($unfamiliar_category === 'filter')) :
1663 $cat_ids = NULL; // Drop the post
1664 else :
1665 $cat_ids = array_unique($cat_ids);
1666 endif;
1667
1668 if ($tags_too) : $ret = array($cat_ids, $tags);
1669 else : $ret = $cat_ids;
1670 endif;
1671
1672 return $ret;
1673 } // function SyndicatedPost::category_ids ()
1674
1675 function use_api ($tag) {
1676 global $wp_db_version;
1677 switch ($tag) :
1678 case 'wp_insert_post':
1679 // Before 2.2, wp_insert_post does too much of the wrong stuff to use it
1680 // In 1.5 it was such a resource hog it would make PHP segfault on big updates
1681 $ret = (isset($wp_db_version) and $wp_db_version > FWP_SCHEMA_21);
1682 break;
1683 case 'post_status_pending':
1684 $ret = (isset($wp_db_version) and $wp_db_version > FWP_SCHEMA_23);
1685 break;
1686 endswitch;
1687 return $ret;
1688 } // function SyndicatedPost::use_api ()
1689
1690 #### EXTRACT DATA FROM FEED ITEM ####
1691
1692 function created () {
1693 $epoch = null;
1694 if (isset($this->item['dc']['created'])) :
1695 $epoch = @parse_w3cdtf($this->item['dc']['created']);
1696 elseif (isset($this->item['dcterms']['created'])) :
1697 $epoch = @parse_w3cdtf($this->item['dcterms']['created']);
1698 elseif (isset($this->item['created'])): // Atom 0.3
1699 $epoch = @parse_w3cdtf($this->item['created']);
1700 endif;
1701 return $epoch;
1702 }
1703 function published ($fallback = true) {
1704 $epoch = null;
1705
1706 # RSS is a fucking mess. Figure out whether we have a date in
1707 # <dc:date>, <issued>, <pubDate>, etc., and get it into Unix
1708 # epoch format for reformatting. If we can't find anything,
1709 # we'll use the last-updated time.
1710 if (isset($this->item['dc']['date'])): // Dublin Core
1711 $epoch = @parse_w3cdtf($this->item['dc']['date']);
1712 elseif (isset($this->item['dcterms']['issued'])) : // Dublin Core extensions
1713 $epoch = @parse_w3cdtf($this->item['dcterms']['issued']);
1714 elseif (isset($this->item['published'])) : // Atom 1.0
1715 $epoch = @parse_w3cdtf($this->item['published']);
1716 elseif (isset($this->item['issued'])): // Atom 0.3
1717 $epoch = @parse_w3cdtf($this->item['issued']);
1718 elseif (isset($this->item['pubdate'])): // RSS 2.0
1719 $epoch = strtotime($this->item['pubdate']);
1720 elseif ($fallback) : // Fall back to <updated> / <modified> if present
1721 $epoch = $this->updated(/*fallback=*/ false);
1722 endif;
1723
1724 # If everything failed, then default to the current time.
1725 if (is_null($epoch)) :
1726 if (-1 == $default) :
1727 $epoch = time();
1728 else :
1729 $epoch = $default;
1730 endif;
1731 endif;
1732
1733 return $epoch;
1734 }
1735 function updated ($fallback = true, $default = -1) {
1736 $epoch = null;
1737
1738 # As far as I know, only dcterms and Atom have reliable ways to
1739 # specify when something was *modified* last. If neither is
1740 # available, then we'll try to get the time of publication.
1741 if (isset($this->item['dc']['modified'])) : // Not really correct
1742 $epoch = @parse_w3cdtf($this->item['dc']['modified']);
1743 elseif (isset($this->item['dcterms']['modified'])) : // Dublin Core extensions
1744 $epoch = @parse_w3cdtf($this->item['dcterms']['modified']);
1745 elseif (isset($this->item['modified'])): // Atom 0.3
1746 $epoch = @parse_w3cdtf($this->item['modified']);
1747 elseif (isset($this->item['updated'])): // Atom 1.0
1748 $epoch = @parse_w3cdtf($this->item['updated']);
1749 elseif ($fallback) : // Fall back to issued / dc:date
1750 $epoch = $this->published(/*fallback=*/ false, /*default=*/ $default);
1751 endif;
1752
1753 # If everything failed, then default to the current time.
1754 if (is_null($epoch)) :
1755 if (-1 == $default) :
1756 $epoch = time();
1757 else :
1758 $epoch = $default;
1759 endif;
1760 endif;
1761
1762 return $epoch;
1763 }
1764
1765 function update_hash () {
1766 return md5(serialize($this->item));
1767 }
1768
1769 function guid () {
1770 $guid = null;
1771 if (isset($this->item['id'])): // Atom 0.3 / 1.0
1772 $guid = $this->item['id'];
1773 elseif (isset($this->item['atom']['id'])) : // Namespaced Atom
1774 $guid = $this->item['atom']['id'];
1775 elseif (isset($this->item['guid'])) : // RSS 2.0
1776 $guid = $this->item['guid'];
1777 elseif (isset($this->item['dc']['identifier'])) :// yeah, right
1778 $guid = $this->item['dc']['identifier'];
1779 else :
1780 // The feed does not seem to have provided us with a
1781 // unique identifier, so we'll have to cobble together
1782 // a tag: URI that might work for us. The base of the
1783 // URI will be the host name of the feed source ...
1784 $bits = parse_url($this->feedmeta['link/uri']);
1785 $guid = 'tag:'.$bits['host'];
1786
1787 // If we have a date of creation, then we can use that
1788 // to uniquely identify the item. (On the other hand, if
1789 // the feed producer was consicentious enough to
1790 // generate dates of creation, she probably also was
1791 // conscientious enough to generate unique identifiers.)
1792 if (!is_null($this->created())) :
1793 $guid .= '://post.'.date('YmdHis', $this->created());
1794
1795 // Otherwise, use both the URI of the item, *and* the
1796 // item's title. We have to use both because titles are
1797 // often not unique, and sometimes links aren't unique
1798 // either (e.g. Bitch (S)HITLIST, Mozilla Dot Org news,
1799 // some podcasts). But it's rare to have *both* the same
1800 // title *and* the same link for two different items. So
1801 // this is about the best we can do.
1802 else :
1803 $guid .= '://'.md5($this->item['link'].'/'.$this->item['title']);
1804 endif;
1805 endif;
1806 return $guid;
1807 }
1808
1809 function author () {
1810 $author = array ();
1811
1812 if (isset($this->item['author_name'])):
1813 $author['name'] = $this->item['author_name'];
1814 elseif (isset($this->item['dc']['creator'])):
1815 $author['name'] = $this->item['dc']['creator'];
1816 elseif (isset($this->item['dc']['contributor'])):
1817 $author['name'] = $this->item['dc']['contributor'];
1818 elseif (isset($this->feed->channel['dc']['creator'])) :
1819 $author['name'] = $this->feed->channel['dc']['creator'];
1820 elseif (isset($this->feed->channel['dc']['contributor'])) :
1821 $author['name'] = $this->feed->channel['dc']['contributor'];
1822 elseif (isset($this->feed->channel['author_name'])) :
1823 $author['name'] = $this->feed->channel['author_name'];
1824 elseif ($this->feed->is_rss() and isset($this->item['author'])) :
1825 // The author element in RSS is allegedly an
1826 // e-mail address, but lots of people don't use
1827 // it that way. So let's make of it what we can.
1828 $author = parse_email_with_realname($this->item['author']);
1829
1830 if (!isset($author['name'])) :
1831 if (isset($author['email'])) :
1832 $author['name'] = $author['email'];
1833 else :
1834 $author['name'] = $this->feed->channel['title'];
1835 endif;
1836 endif;
1837 else :
1838 $author['name'] = $this->feed->channel['title'];
1839 endif;
1840
1841 if (isset($this->item['author_email'])):
1842 $author['email'] = $this->item['author_email'];
1843 elseif (isset($this->feed->channel['author_email'])) :
1844 $author['email'] = $this->feed->channel['author_email'];
1845 endif;
1846
1847 if (isset($this->item['author_url'])):
1848 $author['uri'] = $this->item['author_url'];
1849 elseif (isset($this->feed->channel['author_url'])) :
1850 $author['uri'] = $this->item['author_url'];
1851 else:
1852 $author['uri'] = $this->feed->channel['link'];
1853 endif;
1854
1855 return $author;
1856 } // SyndicatedPost::author()
1857
1858 var $strip_attrs = array (
1859 array('[a-z]+', 'style'),
1860 array('[a-z]+', 'target'),
1861 );
1862 function sanitize_content ($content, $obj) {
1863 # FeedWordPress used to resolve URIs relative to the
1864 # feed URI. It now relies on the xml:base support
1865 # baked in to the MagpieRSS upgrade. So all we do here
1866 # now is to sanitize problematic attributes.
1867 #
1868 # This kind of sucks. I intend to replace it with
1869 # lib_filter sometime soon.
1870 foreach ($obj->strip_attrs as $pair):
1871 list($tag,$attr) = $pair;
1872 $content = preg_replace (
1873 ":(<$tag [^>]*)($attr=(\"[^\">]*\"|[^>\\s]+))([^>]*>):i",
1874 "\\1\\4",
1875 $content
1876 );
1877 endforeach;
1878 return $content;
1879 }
1880 } // class SyndicatedPost
1881
1882 # class SyndicatedLink: represents a syndication feed stored within the
1883 # WordPress database
1884 #
1885 # To keep things compact and editable from within WordPress, we use all the
1886 # links under a particular category in the WordPress "Blogroll" for the list of
1887 # feeds to syndicate. "Contributors" is the category used by default; you can
1888 # configure that under Options --> Syndication.
1889 #
1890 # Fields used are:
1891 #
1892 # * link_rss: the URI of the Atom/RSS feed to syndicate
1893 #
1894 # * link_notes: user-configurable options, with keys and values
1895 # like so:
1896 #
1897 # key: value
1898 # cats: computers\nweb
1899 # feed/key: value
1900 #
1901 # Keys that start with "feed/" are gleaned from the data supplied
1902 # by the feed itself, and will be overwritten with each update.
1903 #
1904 # Values have linebreak characters escaped with C-style
1905 # backslashes (so, for example, a newline becomes "\n").
1906 #
1907 # The value of `cats` is used as a newline-separated list of
1908 # default categories for any post coming from a particular feed.
1909 # (In the example above, any posts from this feed will be placed
1910 # in the "computers" and "web" categories--*in addition to* any
1911 # categories that may already be applied to the posts.)
1912 #
1913 # Values of keys in link_notes are accessible from templates using
1914 # the function `get_feed_meta($key)` if this plugin is activated.
1915
1916 class SyndicatedLink {
1917 var $id = null;
1918 var $link = null;
1919 var $settings = array ();
1920 var $magpie = null;
1921
1922 function SyndicatedLink ($link) {
1923 global $wpdb;
1924
1925 if (is_object($link)) :
1926 $this->link = $link;
1927 $this->id = $link->link_id;
1928 else :
1929 $this->id = $link;
1930 if (function_exists('get_bookmark')) : // WP 2.1+
1931 $this->link = get_bookmark($link);
1932 else :
1933 $this->link = $wpdb->get_row("
1934 SELECT * FROM $wpdb->links
1935 WHERE (link_id = '".$wpdb->escape($link)."')"
1936 );
1937 endif;
1938 endif;
1939
1940 if (strlen($this->link->link_rss) > 0) :
1941 // Read off feed settings from link_notes
1942 $notes = explode("\n", $this->link->link_notes);
1943 foreach ($notes as $note):
1944 list($key, $value) = explode(": ", $note, 2);
1945
1946 if (strlen($key) > 0) :
1947 // Unescape and trim() off the whitespace.
1948 // Thanks to Ray Lischner for pointing out the
1949 // need to trim off whitespace.
1950 $this->settings[$key] = stripcslashes (trim($value));
1951 endif;
1952 endforeach;
1953
1954 // "Magic" feed settings
1955 $this->settings['link/uri'] = $this->link->link_rss;
1956 $this->settings['link/name'] = $this->link->link_name;
1957 $this->settings['link/id'] = $this->link->link_id;
1958
1959 // `hardcode categories` and `unfamiliar categories` are deprecated in favor of `unfamiliar category`
1960 if (
1961 isset($this->settings['unfamiliar categories'])
1962 and !isset($this->settings['unfamiliar category'])
1963 ) :
1964 $this->settings['unfamiliar category'] = $this->settings['unfamiliar categories'];
1965 endif;
1966 if (
1967 FeedWordPress::affirmative($this->settings, 'hardcode categories')
1968 and !isset($this->settings['unfamiliar category'])
1969 ) :
1970 $this->settings['unfamiliar category'] = 'default';
1971 endif;
1972
1973 // Set this up automagically for del.icio.us
1974 $bits = parse_url($this->link->link_rss);
1975 $tagspacers = array('del.icio.us', 'feeds.delicious.com');
1976 if (!isset($this->settings['cat_split']) and in_array($bits['host'], $tagspacers)) :
1977 $this->settings['cat_split'] = '\s'; // Whitespace separates multiple tags in del.icio.us RSS feeds
1978 endif;
1979
1980 if (isset($this->settings['cats'])):
1981 $this->settings['cats'] = preg_split(FEEDWORDPRESS_CAT_SEPARATOR_PATTERN, $this->settings['cats']);
1982 endif;
1983 if (isset($this->settings['tags'])):
1984 $this->settings['tags'] = preg_split(FEEDWORDPRESS_CAT_SEPARATOR_PATTERN, $this->settings['tags']);
1985 endif;
1986
1987 if (isset($this->settings['map authors'])) :
1988 $author_rules = explode("\n\n", $this->settings['map authors']);
1989 $ma = array();
1990 foreach ($author_rules as $rule) :
1991 list($rule_type, $author_name, $author_action) = explode("\n", $rule);
1992
1993 // Normalize for case and whitespace
1994 $rule_type = strtolower(trim($rule_type));
1995 $author_name = strtolower(trim($author_name));
1996 $author_action = strtolower(trim($author_action));
1997
1998 $ma[$rule_type][$author_name] = $author_action;
1999 endforeach;
2000 $this->settings['map authors'] = $ma;
2001 endif;
2002 endif;
2003 } // SyndicatedLink::SyndicatedLink ()
2004
2005 function found () {
2006 return is_object($this->link);
2007 }
2008
2009 function stale () {
2010 $stale = true;
2011 if (isset($this->settings['update/hold']) and ($this->settings['update/hold']=='ping')) :
2012 $stale = false; // don't update on any timed updates; pings only
2013 elseif (isset($this->settings['update/hold']) and ($this->settings['update/hold']=='next')) :
2014 $stale = true; // update on the next timed update
2015 elseif (!isset($this->settings['update/ttl']) or !isset($this->settings['update/last'])) :
2016 $stale = true; // initial update
2017 else :
2018 $after = ((int) $this->settings['update/last'])
2019 +((int) $this->settings['update/ttl'] * 60);
2020 $stale = (time() >= $after);
2021 endif;
2022 return $stale;
2023 }
2024
2025 function poll () {
2026 global $wpdb;
2027
2028 $this->magpie = fetch_rss($this->link->link_rss);
2029 $new_count = NULL;
2030
2031 if (is_object($this->magpie)) :
2032 $new_count = array('new' => 0, 'updated' => 0);
2033
2034 # -- Update Link metadata live from feed
2035 $channel = $this->magpie->channel;
2036
2037 if (!isset($channel['id'])) :
2038 $channel['id'] = $this->link->link_rss;
2039 endif;
2040
2041 $update = array();
2042 if (!$this->hardcode('url') and isset($channel['link'])) :
2043 $update[] = "link_url = '".$wpdb->escape($channel['link'])."'";
2044 endif;
2045
2046 if (!$this->hardcode('name') and isset($channel['title'])) :
2047 $update[] = "link_name = '".$wpdb->escape($channel['title'])."'";
2048 endif;
2049
2050 if (!$this->hardcode('description')) :
2051 if (isset($channel['tagline'])) :
2052 $update[] = "link_description = '".$wpdb->escape($channel['tagline'])."'";
2053 elseif (isset($channel['description'])) :
2054 $update[] = "link_description = '".$wpdb->escape($channel['description'])."'";
2055 endif;
2056 endif;
2057
2058 $this->settings = array_merge($this->settings, $this->flatten_array($channel));
2059
2060 $this->settings['update/last'] = time(); $ttl = $this->ttl();
2061 if (!is_null($ttl)) :
2062 $this->settings['update/ttl'] = $ttl;
2063 $this->settings['update/timed'] = 'feed';
2064 else :
2065 $this->settings['update/ttl'] = rand(30, 120); // spread over time interval for staggered updates
2066 $this->settings['update/timed'] = 'automatically';
2067 endif;
2068
2069 if (!isset($this->settings['update/hold']) or $this->settings['update/hold']!='ping') :
2070 $this->settings['update/hold'] = 'scheduled';
2071 endif;
2072
2073 // Copy back without a few things that we don't want to save in the notes
2074 $to_notes = $this->settings;
2075
2076 if (is_array($to_notes['cats'])) :
2077 $to_notes['cats'] = implode(FEEDWORDPRESS_CAT_SEPARATOR, $to_notes['cats']);
2078 endif;
2079 if (is_array($to_notes['tags'])) :
2080 $to_notes['tags'] = implode(FEEDWORDPRESS_CAT_SEPARATOR, $to_notes['tags']);
2081 endif;
2082
2083 if (isset($to_notes['map authors'])) :
2084 $ma = array();
2085 foreach ($to_notes['map authors'] as $rule_type => $author_rules) :
2086 foreach ($author_rules as $author_name => $author_action) :
2087 $ma[] = $rule_type."\n".$author_name."\n".$author_action;
2088 endforeach;
2089 endforeach;
2090 $to_notes['map authors'] = implode("\n\n", $ma);
2091 endif;
2092
2093 unset($to_notes['link/id']); unset($to_notes['link/uri']);
2094 unset($to_notes['link/name']);
2095 unset($to_notes['hardcode categories']); // Deprecated
2096 unset($to_notes['unfamiliar categories']); // Deprecated
2097
2098 $notes = '';
2099 foreach ($to_notes as $key => $value) :
2100 $notes .= $key . ": ". addcslashes($value, "\0..\37".'\\') . "\n";
2101 endforeach;
2102 $update[] = "link_notes = '".$wpdb->escape($notes)."'";
2103
2104 $update_set = implode(',', $update);
2105
2106 // Update the properties of the link from the feed information
2107 $result = $wpdb->query("
2108 UPDATE $wpdb->links
2109 SET $update_set
2110 WHERE link_id='$this->id'
2111 ");
2112
2113 # -- Add new posts from feed and update any updated posts
2114 if (is_array($this->magpie->items)) :
2115 foreach ($this->magpie->items as $item) :
2116 $post =& new SyndicatedPost($item, $this);
2117 if (!$post->filtered()) :
2118 $new = $post->store();
2119 if ( $new !== false ) $new_count[$new]++;
2120 endif;
2121 endforeach;
2122 endif;
2123
2124 // Copy back any changes to feed settings made in the course of updating (e.g. new author rules)
2125 $to_notes = $this->settings;
2126
2127 if (is_array($to_notes['cats'])) :
2128 $to_notes['cats'] = implode(FEEDWORDPRESS_CAT_SEPARATOR, $to_notes['cats']);
2129 endif;
2130 if (is_array($to_notes['tags'])) :
2131 $to_notes['tags'] = implode(FEEDWORDPRESS_CAT_SEPARATOR, $to_notes['tags']);
2132 endif;
2133
2134 if (isset($to_notes['map authors'])) :
2135 $ma = array();
2136 foreach ($to_notes['map authors'] as $rule_type => $author_rules) :
2137 foreach ($author_rules as $author_name => $author_action) :
2138 $ma[] = $rule_type."\n".$author_name."\n".$author_action;
2139 endforeach;
2140 endforeach;
2141 $to_notes['map authors'] = implode("\n\n", $ma);
2142 endif;
2143
2144 unset($to_notes['link/id']); unset($to_notes['link/uri']);
2145 unset($to_notes['link/name']);
2146 unset($to_notes['hardcode categories']); // Deprecated
2147 unset($to_notes['unfamiliar categories']); // Deprecated
2148
2149 $notes = '';
2150 foreach ($to_notes as $key => $value) :
2151 $notes .= $key . ": ". addcslashes($value, "\0..\37".'\\') . "\n";
2152 endforeach;
2153
2154 $update_set = "link_notes = '".$wpdb->escape($notes)."'";
2155
2156 // Update the properties of the link from the feed information
2157 $result = $wpdb->query("
2158 UPDATE $wpdb->links
2159 SET $update_set
2160 WHERE link_id='$this->id'
2161 ");
2162 endif;
2163 return $new_count;
2164 } /* SyndicatedLink::poll() */
2165
2166 function uri () {
2167 return (is_object($this->link) ? $this->link->link_rss : NULL);
2168 }
2169 function homepage () {
2170 return (isset($this->settings['feed/link']) ? $this->settings['feed/link'] : NULL);
2171 }
2172
2173 function ttl () {
2174 if (is_object($this->magpie)) :
2175 $channel = $this->magpie->channel;
2176 else :
2177 $channel = array();
2178 endif;
2179
2180 if (isset($channel['ttl'])) :
2181 // "ttl stands for time to live. It's a number of
2182 // minutes that indicates how long a channel can be
2183 // cached before refreshing from the source."
2184 // <http://blogs.law.harvard.edu/tech/rss#ltttlgtSubelementOfLtchannelgt>
2185 $ret = $channel['ttl'];
2186 elseif (isset($channel['sy']['updatefrequency']) or isset($channel['sy']['updateperiod'])) :
2187 $period_minutes = array (
2188 'hourly' => 60, /* minutes in an hour */
2189 'daily' => 1440, /* minutes in a day */
2190 'weekly' => 10080, /* minutes in a week */
2191 'monthly' => 43200, /* minutes in a month */
2192 'yearly' => 525600, /* minutes in a year */
2193 );
2194
2195 // "sy:updatePeriod: Describes the period over which the
2196 // channel format is updated. Acceptable values are:
2197 // hourly, daily, weekly, monthly, yearly. If omitted,
2198 // daily is assumed." <http://web.resource.org/rss/1.0/modules/syndication/>
2199 if (isset($channel['sy']['updateperiod'])) : $period = $channel['sy']['updateperiod'];
2200 else : $period = 'daily';
2201 endif;
2202
2203 // "sy:updateFrequency: Used to describe the frequency
2204 // of updates in relation to the update period. A
2205 // positive integer indicates how many times in that
2206 // period the channel is updated. ... If omitted a value
2207 // of 1 is assumed." <http://web.resource.org/rss/1.0/modules/syndication/>
2208 if (isset($channel['sy']['updatefrequency'])) : $freq = (int) $channel['sy']['updatefrequency'];
2209 else : $freq = 1;
2210 endif;
2211
2212 $ret = (int) ($period_minutes[$period] / $freq);
2213 else :
2214 $ret = NULL;
2215 endif;
2216 return $ret;
2217 } /* SyndicatedLink::ttl() */
2218
2219 // SyndicatedLink::flatten_array (): flatten an array. Useful for
2220 // hierarchical and namespaced elements.
2221 //
2222 // Given an array which may contain array or object elements in it,
2223 // return a "flattened" array: a one-dimensional array of scalars
2224 // containing each of the scalar elements contained within the array
2225 // structure. Thus, for example, if $a['b']['c']['d'] == 'e', then the
2226 // returned array for FeedWordPress::flatten_array($a) will contain a key
2227 // $a['feed/b/c/d'] with value 'e'.
2228 function flatten_array ($arr, $prefix = 'feed/', $separator = '/') {
2229 $ret = array ();
2230 if (is_array($arr)) :
2231 foreach ($arr as $key => $value) :
2232 if (is_scalar($value)) :
2233 $ret[$prefix.$key] = $value;
2234 else :
2235 $ret = array_merge($ret, $this->flatten_array($value, $prefix.$key.$separator, $separator));
2236 endif;
2237 endforeach;
2238 endif;
2239 return $ret;
2240 } // function SyndicatedLink::flatten_array ()
2241
2242 function hardcode ($what) {
2243 $default = get_option("feedwordpress_hardcode_$what");
2244 if ( $default === 'yes' ) :
2245 // If the default is to hardcode, then we want the
2246 // negation of negative(): TRUE by default and FALSE if
2247 // the setting is explicitly "no"
2248 $ret = !FeedWordPress::negative($this->settings, "hardcode $what");
2249 else :
2250 // If the default is NOT to hardcode, then we want
2251 // affirmative(): FALSE by default and TRUE if the
2252 // setting is explicitly "yes"
2253 $ret = FeedWordPress::affirmative($this->settings, "hardcode $what");
2254 endif;
2255 return $ret;
2256 } // function SyndicatedLink::hardcode ()
2257
2258 function syndicated_status ($what, $default) {
2259 global $wpdb;
2260
2261 $ret = get_option("feedwordpress_syndicated_{$what}_status");
2262 if ( isset($this->settings["$what status"]) ) :
2263 $ret = $this->settings["$what status"];
2264 elseif (!$ret) :
2265 $ret = $default;
2266 endif;
2267 return $wpdb->escape(trim(strtolower($ret)));
2268 } // function SyndicatedLink:syndicated_status ()
2269 } // class SyndicatedLink
2270
2271 ################################################################################
2272 ## XML-RPC HOOKS: accept XML-RPC update pings from Contributors ################
2273 ################################################################################
2274
2275 function feedwordpress_xmlrpc_hook ($args = array ()) {
2276 $args['weblogUpdates.ping'] = 'feedwordpress_pong';
2277 return $args;
2278 }
2279
2280 function feedwordpress_pong ($args) {
2281 $feedwordpress =& new FeedWordPress;
2282 $delta = @$feedwordpress->update($args[1]);
2283 if (is_null($delta)):
2284 return array('flerror' => true, 'message' => "Sorry. I don't syndicate <$args[1]>.");
2285 else:
2286 $mesg = array();
2287 if (isset($delta['new'])) { $mesg[] = ' '.$delta['new'].' new posts were syndicated'; }
2288 if (isset($delta['updated'])) { $mesg[] = ' '.$delta['updated'].' existing posts were updated'; }
2289
2290 return array('flerror' => false, 'message' => "Thanks for the ping.".implode(' and', $mesg));
2291 endif;
2292 }
2293
2294 ################################################################################
2295 ## class FeedFinder: find likely feeds using autodetection and/or guesswork ####
2296 ################################################################################
2297
2298 class FeedFinder {
2299 var $uri = NULL;
2300 var $_cache_uri = NULL;
2301
2302 var $verify = FALSE;
2303
2304 var $_data = NULL;
2305 var $_head = NULL;
2306
2307 # -- Recognition patterns
2308 var $_feed_types = array(
2309 'application/rss+xml',
2310 'text/xml',
2311 'application/atom+xml',
2312 'application/x.atom+xml',
2313 'application/x-atom+xml'
2314 );
2315 var $_feed_markers = array('\\<feed', '\\<rss', 'xmlns="http://purl.org/rss/1.0');
2316 var $_html_markers = array('\\<html');
2317 var $_obvious_feed_url = array('[./]rss', '[./]rdf', '[./]atom', '[./]feed', '\.xml');
2318 var $_maybe_feed_url = array ('rss', 'rdf', 'atom', 'feed', 'xml');
2319
2320 function FeedFinder ($uri = NULL, $verify = TRUE) {
2321 $this->uri = $uri; $this->verify = $verify;
2322 } /* FeedFinder::FeedFinder () */
2323
2324 function find ($uri = NULL) {
2325 $ret = array ();
2326 if (!is_null($this->data($uri))) {
2327 if ($this->is_feed($uri)) {
2328 $ret = array($this->uri);
2329 } else {
2330 // Assume that we have HTML or XHTML (even if we don't, who's it gonna hurt?)
2331 // Autodiscovery is the preferred method
2332 $href = $this->_link_rel_feeds();
2333
2334 // ... but we'll also take the little orange buttons
2335 $href = array_merge($href, $this->_a_href_feeds(TRUE));
2336
2337 // If all that failed, look harder
2338 if (count($href) == 0) $href = $this->_a_href_feeds(FALSE);
2339
2340 // Verify feeds and resolve relative URIs
2341 foreach ($href as $u) {
2342 $the_uri = Relative_URI::resolve($u, $this->uri);
2343 if ($this->verify) {
2344 $feed =& new FeedFinder($the_uri);
2345 if ($feed->is_feed()) $ret[] = $the_uri;
2346 $feed = NULL;
2347 } else {
2348 $ret[] = $the_uri;
2349 }
2350 } /* foreach */
2351 } /* if */
2352 } /* if */
2353 return array_unique($ret);
2354 } /* FeedFinder::find () */
2355
2356 function data ($uri = NULL) {
2357 $this->_get($uri);
2358 return $this->_data;
2359 }
2360
2361 function is_feed ($uri = NULL) {
2362 $data = $this->data($uri);
2363
2364 return (
2365 preg_match (
2366 "\007(".implode('|',$this->_feed_markers).")\007i",
2367 $data
2368 ) and !preg_match (
2369 "\007(".implode('|',$this->_html_markers).")\007i",
2370 $data
2371 )
2372 );
2373 } /* FeedFinder::is_feed () */
2374
2375 # --- Private methods ---
2376 function _get ($uri = NULL) {
2377 if ($uri) $this->uri = $uri;
2378
2379 // Is the result not yet cached?
2380 if ($this->_cache_uri !== $this->uri) :
2381 // Snoopy is an HTTP client in PHP
2382 $client = new Snoopy();
2383
2384 // Prepare headers and internal settings
2385 $client->rawheaders['Connection'] = 'close';
2386 $client->accept = 'application/atom+xml application/rdf+xml application/rss+xml application/xml text/html */*';
2387 $client->agent = 'feedfinder/1.2 (compatible; PHP FeedFinder) +http://projects.radgeek.com/feedwordpress';
2388 $client->read_timeout = 25;
2389
2390 // Fetch the HTML or feed
2391 @$client->fetch($this->uri);
2392 $this->_data = $client->results;
2393
2394 // Kilroy was here
2395 $this->_cache_uri = $this->uri;
2396 endif;
2397 } /* FeedFinder::_get () */
2398
2399 function _link_rel_feeds () {
2400 $links = $this->_tags('link');
2401 $link_count = count($links);
2402
2403 // now figure out which one points to the RSS file
2404 $href = array ();
2405 for ($n=0; $n<$link_count; $n++) {
2406 if (strtolower($links[$n]['rel']) == 'alternate') {
2407 if (in_array(strtolower($links[$n]['type']), $this->_feed_types)) {
2408 $href[] = $links[$n]['href'];
2409 } /* if */
2410 } /* if */
2411 } /* for */
2412 return $href;
2413 }
2414
2415 function _a_href_feeds ($obvious = TRUE) {
2416 $pattern = ($obvious ? $this->_obvious_feed_url : $this->_maybe_feed_url);
2417
2418 $links = $this->_tags('a');
2419 $link_count = count($links);
2420
2421 // now figure out which one points to the RSS file
2422 $href = array ();
2423 for ($n=0; $n<$link_count; $n++) {
2424 if (preg_match("\007(".implode('|',$pattern).")\007i", $links[$n]['href'])) {
2425 $href[] = $links[$n]['href'];
2426 } /* if */
2427 } /* for */
2428 return $href;
2429 }
2430
2431 function _tags ($tag) {
2432 $html = $this->data();
2433
2434 // search through the HTML, save all <link> tags
2435 // and store each link's attributes in an associative array
2436 preg_match_all('/<'.$tag.'\s+(.*?)\s*\/?>/si', $html, $matches);
2437 $links = $matches[1];
2438 $ret = array();
2439 $link_count = count($links);
2440 for ($n=0; $n<$link_count; $n++) {
2441 $attributes = preg_split('/\s+/s', $links[$n]);
2442 foreach($attributes as $attribute) {
2443 $att = preg_split('/\s*=\s*/s', $attribute, 2);
2444 if (isset($att[1])) {
2445 $att[1] = preg_replace('/([\'"]?)(.*)\1/', '$2', $att[1]);
2446 $final_link[strtolower($att[0])] = $att[1];
2447 } /* if */
2448 } /* foreach */
2449 $ret[$n] = $final_link;
2450 } /* for */
2451 return $ret;
2452 }
2453 } /* class FeedFinder */
2454
2455 # Relative URI static class: PHP class for resolving relative URLs
2456 #
2457 # This class is derived (under the terms of the GPL) from URL Class 0.3 by
2458 # Keyvan Minoukadeh <keyvan@k1m.com>, which is great but more than we need
2459 # for FeedWordPress's purposes. The class has been stripped down to a single
2460 # public method: Relative_URI::resolve($url, $base), which resolves the URI in
2461 # $url relative to the URI in $base
2462 #
2463 # The upgraded MagpieRSS also uses this class. So if we have it loaded
2464 # in, don't load it again.
2465 if (!class_exists('Relative_URI')) {
2466
2467 class Relative_URI
2468 {
2469 // Resolve relative URI in $url against the base URI in $base. If $base
2470 // is not supplied, then we use the REQUEST_URI of this script.
2471 //
2472 // I'm hoping this method reflects RFC 2396 Section 5.2
2473 function resolve ($url, $base = NULL)
2474 {
2475 if (is_null($base)):
2476 $base = 'http://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];
2477 endif;
2478
2479 $base = Relative_URI::_encode(trim($base));
2480 $uri_parts = Relative_URI::_parse_url($base);
2481
2482 $url = Relative_URI::_encode(trim($url));
2483 $parts = Relative_URI::_parse_url($url);
2484
2485 $uri_parts['fragment'] = (isset($parts['fragment']) ? $parts['fragment'] : null);
2486 $uri_parts['query'] = (isset($parts['query']) ? $parts['query'] : null);
2487
2488 // if path is empty, and scheme, host, and query are undefined,
2489 // the URL is referring the base URL
2490
2491 if (($parts['path'] == '') && !isset($parts['scheme']) && !isset($parts['host']) && !isset($parts['query'])) {
2492 // If the URI is empty or only a fragment, return the base URI
2493 return $base . (isset($parts['fragment']) ? '#'.$parts['fragment'] : '');
2494 } elseif (isset($parts['scheme'])) {
2495 // If the scheme is set, then the URI is absolute.
2496 return $url;
2497 } elseif (isset($parts['host'])) {
2498 $uri_parts['host'] = $parts['host'];
2499 $uri_parts['path'] = $parts['path'];
2500 } else {
2501 // We have a relative path but not a host.
2502
2503 // start ugly fix:
2504 // prepend slash to path if base host is set, base path is not set, and url path is not absolute
2505 if ($uri_parts['host'] && ($uri_parts['path'] == '')
2506 && (strlen($parts['path']) > 0)
2507 && (substr($parts['path'], 0, 1) != '/')) {
2508 $parts['path'] = '/'.$parts['path'];
2509 } // end ugly fix
2510
2511 if (substr($parts['path'], 0, 1) == '/') {
2512 $uri_parts['path'] = $parts['path'];
2513 } else {
2514 // copy base path excluding any characters after the last (right-most) slash character
2515 $buffer = substr($uri_parts['path'], 0, (int)strrpos($uri_parts['path'], '/')+1);
2516 // append relative path
2517 $buffer .= $parts['path'];
2518 // remove "./" where "." is a complete path segment.
2519 $buffer = str_replace('/./', '/', $buffer);
2520 if (substr($buffer, 0, 2) == './') {
2521 $buffer = substr($buffer, 2);
2522 }
2523 // if buffer ends with "." as a complete path segment, remove it
2524 if (substr($buffer, -2) == '/.') {
2525 $buffer = substr($buffer, 0, -1);
2526 }
2527 // remove "<segment>/../" where <segment> is a complete path segment not equal to ".."
2528 $search_finished = false;
2529 $segment = explode('/', $buffer);
2530 while (!$search_finished) {
2531 for ($x=0; $x+1 < count($segment);) {
2532 if (($segment[$x] != '') && ($segment[$x] != '..') && ($segment[$x+1] == '..')) {
2533 if ($x+2 == count($segment)) $segment[] = '';
2534 unset($segment[$x], $segment[$x+1]);
2535 $segment = array_values($segment);
2536 continue 2;
2537 } else {
2538 $x++;
2539 }
2540 }
2541 $search_finished = true;
2542 }
2543 $buffer = (count($segment) == 1) ? '/' : implode('/', $segment);
2544 $uri_parts['path'] = $buffer;
2545
2546 }
2547 }
2548
2549 // If we've gotten to this point, we can try to put the pieces
2550 // back together.
2551 $ret = '';
2552 if (isset($uri_parts['scheme'])) $ret .= $uri_parts['scheme'].':';
2553 if (isset($uri_parts['user'])) {
2554 $ret .= $uri_parts['user'];
2555 if (isset($uri_parts['pass'])) $ret .= ':'.$uri_parts['parts'];
2556 $ret .= '@';
2557 }
2558 if (isset($uri_parts['host'])) {
2559 $ret .= '//'.$uri_parts['host'];
2560 if (isset($uri_parts['port'])) $ret .= ':'.$uri_parts['port'];
2561 }
2562 $ret .= $uri_parts['path'];
2563 if (isset($uri_parts['query'])) $ret .= '?'.$uri_parts['query'];
2564 if (isset($uri_parts['fragment'])) $ret .= '#'.$uri_parts['fragment'];
2565
2566 return $ret;
2567 }
2568
2569 /**
2570 * Parse URL
2571 *
2572 * Regular expression grabbed from RFC 2396 Appendix B.
2573 * This is a replacement for PHPs builtin parse_url().
2574 * @param string $url
2575 * @access private
2576 * @return array
2577 */
2578 function _parse_url($url)
2579 {
2580 // I'm using this pattern instead of parse_url() as there's a few strings where parse_url()
2581 // generates a warning.
2582 if (preg_match('!^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?!', $url, $match)) {
2583 $parts = array();
2584 if ($match[1] != '') $parts['scheme'] = $match[2];
2585 if ($match[3] != '') $parts['auth'] = $match[4];
2586 // parse auth
2587 if (isset($parts['auth'])) {
2588 // store user info
2589 if (($at_pos = strpos($parts['auth'], '@')) !== false) {
2590 $userinfo = explode(':', substr($parts['auth'], 0, $at_pos), 2);
2591 $parts['user'] = $userinfo[0];
2592 if (isset($userinfo[1])) $parts['pass'] = $userinfo[1];
2593 $parts['auth'] = substr($parts['auth'], $at_pos+1);
2594 }
2595 // get port number
2596 if ($port_pos = strrpos($parts['auth'], ':')) {
2597 $parts['host'] = substr($parts['auth'], 0, $port_pos);
2598 $parts['port'] = (int)substr($parts['auth'], $port_pos+1);
2599 if ($parts['port'] < 1) $parts['port'] = null;
2600 } else {
2601 $parts['host'] = $parts['auth'];
2602 }
2603 }
2604 unset($parts['auth']);
2605 $parts['path'] = $match[5];
2606 if (isset($match[6]) && ($match[6] != '')) $parts['query'] = $match[7];
2607 if (isset($match[8]) && ($match[8] != '')) $parts['fragment'] = $match[9];
2608 return $parts;
2609 }
2610 // shouldn't reach here
2611 return array('path'=>'');
2612 }
2613
2614 function _encode($string)
2615 {
2616 static $replace = array();
2617 if (!count($replace)) {
2618 $find = array(32, 34, 60, 62, 123, 124, 125, 91, 92, 93, 94, 96, 127);
2619 $find = array_merge(range(0, 31), $find);
2620 $find = array_map('chr', $find);
2621 foreach ($find as $char) {
2622 $replace[$char] = '%'.bin2hex($char);
2623 }
2624 }
2625 // escape control characters and a few other characters
2626 $encoded = strtr($string, $replace);
2627 // remove any character outside the hex range: 21 - 7E (see www.asciitable.com)
2628 return preg_replace('/[^\x21-\x7e]/', '', $encoded);
2629 }
2630 } // class Relative_URI
2631 }
2632
2633 // take your best guess at the realname and e-mail, given a string
2634 define('FWP_REGEX_EMAIL_ADDY', '([^@"(<\s]+@[^"@(<\s]+\.[^"@(<\s]+)');
2635 define('FWP_REGEX_EMAIL_NAME', '("([^"]*)"|([^"<(]+\S))');
2636 define('FWP_REGEX_EMAIL_POSTFIX_NAME', '/^\s*'.FWP_REGEX_EMAIL_ADDY."\s+\(".FWP_REGEX_EMAIL_NAME.'\)\s*$/');
2637 define('FWP_REGEX_EMAIL_PREFIX_NAME', '/^\s*'.FWP_REGEX_EMAIL_NAME.'\s*<'.FWP_REGEX_EMAIL_ADDY.'>\s*$/');
2638 define('FWP_REGEX_EMAIL_JUST_ADDY', '/^\s*'.FWP_REGEX_EMAIL_ADDY.'\s*$/');
2639 define('FWP_REGEX_EMAIL_JUST_NAME', '/^\s*'.FWP_REGEX_EMAIL_NAME.'\s*$/');
2640
2641 function parse_email_with_realname ($email) {
2642 if (preg_match(FWP_REGEX_EMAIL_POSTFIX_NAME, $email, $matches)) :
2643 ($ret['name'] = $matches[3]) or ($ret['name'] = $matches[2]);
2644 $ret['email'] = $matches[1];
2645 elseif (preg_match(FWP_REGEX_EMAIL_PREFIX_NAME, $email, $matches)) :
2646 ($ret['name'] = $matches[2]) or ($ret['name'] = $matches[3]);
2647 $ret['email'] = $matches[4];
2648 elseif (preg_match(FWP_REGEX_EMAIL_JUST_ADDY, $email, $matches)) :
2649 $ret['name'] = NULL; $ret['email'] = $matches[1];
2650 elseif (preg_match(FWP_REGEX_EMAIL_JUST_NAME, $email, $matches)) :
2651 $ret['email'] = NULL;
2652 ($ret['name'] = $matches[2]) or ($ret['name'] = $matches[3]);
2653 else :
2654 $ret['name'] = NULL; $ret['email'] = NULL;
2655 endif;
2656 return $ret;
2657 }
2658
2659