PluginProbe
FeedWordPress / 2009.0612
FeedWordPress v2009.0612
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 2009.0612, at feedwordpress.php

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