PluginProbe
FeedWordPress / 2008.1105
FeedWordPress v2008.1105
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.1105, at feedwordpress.php

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