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

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