PluginProbe
FeedWordPress / 2022.0222
FeedWordPress v2022.0222
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 2022.0222, at feedwordpress.php

2,367 lines 79.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 Plugin Name: FeedWordPress
4 Plugin URI: http://feedwordpress.radgeek.com/
5 Description: simple and flexible Atom/RSS syndication for WordPress
6 Version: 2022.0222
7 Author: C. Johnson
8 Author URI: https://feedwordpress.radgeek.com/contact/
9 License: GPL
10 */
11
12 /**
13 * @package FeedWordPress
14 * @version 2022.0222
15 */
16
17 # This plugin uses code derived from:
18 # - wp-rss-aggregate.php by Kellan Elliot-McCrea <kellan@protest.net>
19 # - SimplePie feed parser by Ryan Parman, Geoffrey Sneddon, Ryan McCue, et al.
20 # - MagpieRSS feed parser by Kellan Elliot-McCrea <kellan@protest.net>
21 # - Ultra-Liberal Feed Finder by Mark Pilgrim <mark@diveintomark.org>
22 # - WordPress Blog Tool and Publishing Platform <http://wordpress.org/>
23 # - Github contributors @Flynsarmy, @BandonRandon, @david-robinson-practiceweb,
24 # @daidais, @thegreatmichael, @stedaniels, @alexiskulash, @quassy, @zoul0813,
25 # @timmmmyboy, @vobornik, @inanimatt, @tristanleboss, @martinburchell,
26 # @bigalownz, and @oppiansteve
27 # according to the terms of the GNU General Public License.
28
29 ####################################################################################
30 ## CONSTANTS & DEFAULTS ############################################################
31 ####################################################################################
32
33 define ('FEEDWORDPRESS_VERSION', '2022.0222');
34 define ('FEEDWORDPRESS_AUTHOR_CONTACT', 'http://feedwordpress.radgeek.com/contact');
35
36 if (!defined('FEEDWORDPRESS_BLEG')) :
37 define ('FEEDWORDPRESS_BLEG', true);
38 endif;
39
40 define('FEEDWORDPRESS_BLEG_BTC_pre_2020', '15EsQ9QMZtLytsaVYZUaUCmrkSMaxZBTso');
41 define('FEEDWORDPRESS_BLEG_BTC_2020', '1NB1ebYVb68Har4WijmE8gKnZ47NptCqtB'); // 2020.0201
42 define('FEEDWORDPRESS_BLEG_BTC', '1HCDdeGcR66EPxkPT2rbdTd1ezh27pmjPR'); // 2021.0713
43
44 define('FEEDWORDPRESS_BLEG_PAYPAL', '22PAJZZCK5Z3W');
45
46 // Defaults
47 define ('DEFAULT_SYNDICATION_CATEGORY', 'Contributors');
48 define ('DEFAULT_UPDATE_PERIOD', 60); // value in minutes
49 define ('FEEDWORDPRESS_DEFAULT_CHECKIN_INTERVAL', DEFAULT_UPDATE_PERIOD/10);
50
51 // Dependencies: modules packaged with FeedWordPress plugin
52 $dir = dirname( __FILE__ );
53 require_once("${dir}/externals/myphp/myphp.class.php");
54
55 $feedwordpress_debug = FeedWordPress::param( 'feedwordpress_debug', get_option('feedwordpress_debug') );
56
57 if (is_string($feedwordpress_debug)) :
58 $feedwordpress_debug = ($feedwordpress_debug == 'yes');
59 endif;
60
61 define ('FEEDWORDPRESS_DEBUG', $feedwordpress_debug);
62 $feedwordpress_compatibility = true;
63 define ('FEEDWORDPRESS_COMPATIBILITY', $feedwordpress_compatibility);
64
65 define ('FEEDWORDPRESS_CAT_SEPARATOR_PATTERN', '/[:\n]/');
66 define ('FEEDWORDPRESS_CAT_SEPARATOR', "\n");
67
68 define ('FEEDVALIDATOR_URI', 'http://feedvalidator.org/check.cgi');
69
70 define ('FEEDWORDPRESS_FRESHNESS_INTERVAL', 10*60); // Every ten minutes
71
72 define('FEEDWORDPRESS_BOILERPLATE_DEFAULT_HOOK_ORDER', 11000); // at the tail end of the filtering process
73
74 if (FEEDWORDPRESS_DEBUG) :
75 // Help us to pick out errors, if any.
76 ini_set('error_reporting', E_ALL & ~E_NOTICE);
77 ini_set('display_errors', true);
78
79 // When testing we don't want cache issues to interfere. But this is
80 // a VERY BAD SETTING for a production server. Webmasters will eat your
81 // face for breakfast if you use it, and the baby Jesus will cry. So
82 // make sure FEEDWORDPRESS_DEBUG is FALSE for any site that will be
83 // used for more than testing purposes!
84 define('FEEDWORDPRESS_CACHE_AGE', 1);
85 define('FEEDWORDPRESS_CACHE_LIFETIME', 1);
86 define('FEEDWORDPRESS_FETCH_TIMEOUT_DEFAULT', 60);
87 else :
88 // Hold onto data all day for conditional GET purposes,
89 // but consider it stale after 1 min (requiring a conditional GET)
90 define('FEEDWORDPRESS_CACHE_LIFETIME', 24*60*60);
91 define('FEEDWORDPRESS_CACHE_AGE', 1*60);
92 define('FEEDWORDPRESS_FETCH_TIMEOUT_DEFAULT', 20);
93 endif;
94
95 ####################################################################################
96 ## CORE DEPENDENCIES & PLUGIN MODULES ##############################################
97 ####################################################################################
98
99 // Dependencies: modules packaged with WordPress core
100 $wpCoreDependencies = array(
101 "class:SimplePie" => "class-simplepie",
102 "class:WP_SimplePie_File" => "class-wp-simplepie-file",
103 "class:WP_SimplePie_Sanitize_KSES" => "class-wp-simplepie-sanitize-kses",
104 "function:wp_insert_user" => "registration",
105 );
106
107 // Ensure that we have SimplePie loaded up and ready to go
108 // along with the WordPress auxiliary classes.
109 $unmetCoreDependencies = array();
110 foreach ($wpCoreDependencies as $unit => $fileSlug) :
111 list($unitType, $unitName) = explode(":", $unit, 2);
112
113 $dependencyMet = (('class'==$unitType) ? class_exists($unitName) : function_exists($unitName));
114 if (!$dependencyMet) :
115 $phpFileName = ABSPATH . WPINC . "/${fileSlug}.php";
116 if (file_exists($phpFileName)) :
117 require_once($phpFileName);
118 else :
119 $unmetCoreDependencies[] = $unitName;
120 endif;
121 endif;
122 endforeach;
123
124 // Fallback garbage-pail module used in WP < 4.7 which may meet our dependencies
125 if (count($unmetCoreDependencies) > 0) :
126 require_once(ABSPATH . WPINC . "/class-feed.php");
127 endif;
128
129 // Dependencies: modules packaged with FeedWordPress plugin
130 $dir = dirname( __FILE__ );
131 require_once("${dir}/feedwordpressadminpage.class.php");
132 require_once("${dir}/feedwordpresssettingsui.class.php");
133 require_once("${dir}/feedwordpressdiagnostic.class.php");
134 require_once("${dir}/admin-ui.php");
135 require_once("${dir}/template-functions.php");
136 require_once("${dir}/feedwordpresssyndicationpage.class.php");
137 require_once("${dir}/compatability.php"); // Legacy API
138 require_once("${dir}/syndicatedpost.class.php");
139 require_once("${dir}/syndicatedlink.class.php");
140 require_once("${dir}/feedwordpresshtml.class.php");
141 require_once("${dir}/inspectpostmeta.class.php");
142 require_once("${dir}/syndicationdataqueries.class.php");
143 require_once("${dir}/extend/SimplePie/feedwordpie.class.php");
144 require_once("${dir}/extend/SimplePie/feedwordpie_cache.class.php");
145 require_once("${dir}/extend/SimplePie/feedwordpie_item.class.php");
146 require_once("${dir}/extend/SimplePie/feedwordpie_file.class.php");
147 require_once("${dir}/extend/SimplePie/feedwordpie_parser.class.php");
148 require_once("${dir}/extend/SimplePie/feedwordpie_content_type_sniffer.class.php");
149 require_once("${dir}/feedwordpressrpc.class.php");
150 require_once("${dir}/feedwordpresshttpauthenticator.class.php");
151 require_once("${dir}/feedwordpresslocalpost.class.php");
152
153 ####################################################################################
154 ## GLOBAL PARAMETERS ###############################################################
155 ####################################################################################
156
157 // Get the path relative to the plugins directory in which FWP is stored
158 preg_match (
159 '|'.preg_quote(WP_PLUGIN_DIR).'/(.+)$|',
160 dirname(__FILE__),
161 $ref
162 );
163
164 if (isset($ref[1])) :
165 $fwp_path = $ref[1];
166 else : // Something went wrong. Let's just guess.
167 $fwp_path = 'feedwordpress';
168 endif;
169
170 ####################################################################################
171 ## FEEDWORDPRESS: INITIALIZE OBJECT AND FILTERS ####################################
172 ####################################################################################
173
174 $feedwordpress = new FeedWordPress;
175 if (!$feedwordpress->needs_upgrade()) : // only work if the conditions are safe!
176 $feedwordpress->add_filters();
177
178 # Inbound XML-RPC update methods
179 $feedwordpressRPC = new FeedWordPressRPC;
180
181 # Outbound XML-RPC ping reform
182 remove_action('publish_post', 'generic_ping'); // WP 1.5.x
183 remove_action('do_pings', 'do_all_pings', 10, 1); // WP 2.1, 2.2
184 remove_action('publish_post', '_publish_post_hook', 5, 1); // WP 2.3
185
186 add_action('publish_post', 'fwp_publish_post_hook', 5, 1);
187 add_action('do_pings', 'fwp_do_pings', 10, 1);
188 add_action('feedwordpress_update', 'fwp_hold_pings');
189 add_action('feedwordpress_update_complete', 'fwp_release_pings');
190
191 else :
192 # Hook in the menus, which will just point to the upgrade interface
193 add_action('admin_menu', array($feedwordpress, 'add_pages'));
194 endif; // if (!FeedWordPress::needs_upgrade())
195
196 register_deactivation_hook(__FILE__, 'feedwordpress_deactivate');
197 function feedwordpress_deactivate () {
198 wp_clear_scheduled_hook('fwp_scheduled_update_checkin');
199 } /* feedwordpress_deactivate () */
200
201 ################################################################################
202 ## LOGGING FUNCTIONS: log status updates to error_log if you want it ###########
203 ################################################################################
204
205 function debug_out_human_readable_bytes ($quantity) {
206 $quantity = (int) $quantity;
207 $magnitude = 'B';
208 $orders = array('KB', 'MB', 'GB', 'TB');
209 while (($quantity > (1024*100)) and (count($orders) > 0)) :
210 $quantity = floor($quantity / 1024);
211 $magnitude = array_shift($orders);
212 endwhile;
213 return "${quantity} ${magnitude}";
214 }
215
216 function debug_out_feedwordpress_footer () {
217 if (FeedWordPressDiagnostic::is_on('memory_usage')) :
218 if (function_exists('memory_get_usage')) :
219 FeedWordPress::diagnostic ('memory_usage', "Memory: Current usage: ".debug_out_human_readable_bytes(memory_get_usage()));
220 endif;
221 if (function_exists('memory_get_peak_usage')) :
222 FeedWordPress::diagnostic ('memory_usage', "Memory: Peak usage: ".debug_out_human_readable_bytes(memory_get_peak_usage()));
223 endif;
224 endif;
225 } /* debug_out_feedwordpress_footer() */
226
227 ################################################################################
228 ## FILTERS: syndication-aware handling of post data for templates and feeds ####
229 ################################################################################
230
231 $feedwordpress_the_syndicated_content = null;
232 $feedwordpress_the_original_permalink = null;
233
234 function feedwordpress_preserve_syndicated_content ($text) {
235 global $feedwordpress_the_syndicated_content;
236
237 $p = new FeedWordPressLocalPost;
238 if (!$p->is_exposed_to_formatting_filters()) :
239 $feedwordpress_the_syndicated_content = $text;
240 else :
241 $feedwordpress_the_syndicated_content = null;
242 endif;
243 return $text;
244 }
245
246 function feedwordpress_restore_syndicated_content ($text) {
247 global $feedwordpress_the_syndicated_content;
248
249 if ( !is_null($feedwordpress_the_syndicated_content) ) :
250 $text = $feedwordpress_the_syndicated_content;
251 endif;
252
253 return $text;
254 }
255
256 function feedwordpress_item_feed_data () {
257 // In a post context....
258 if (is_syndicated()) :
259 ?>
260 <source>
261 <title><?php print htmlspecialchars(get_syndication_source()); ?></title>
262 <link rel="alternate" type="text/html" href="<?php print htmlspecialchars(get_syndication_source_link()); ?>" />
263 <link rel="self" href="<?php print htmlspecialchars(get_syndication_feed()); ?>" />
264 <?php
265 $id = get_syndication_feed_guid();
266 if (strlen($id) > 0) :
267 ?>
268 <id><?php print htmlspecialchars($id); ?></id>
269 <?php
270 endif;
271 $updated = get_feed_meta('feed/updated');
272 if (strlen($updated) > 0) : ?>
273 <updated><?php print esc_xml($updated); ?></updated>
274 <?php
275 endif;
276 ?>
277 </source>
278 <?php
279 endif;
280 }
281
282 /**
283 * syndication_permalink: Allow WordPress to use the original remote URL of
284 * syndicated posts as their permalink. Can be turned on or off by by setting in
285 * Syndication => Posts & Links. Saves the old internal permalink in a global
286 * variable for later use.
287 *
288 * @param string $permalink The internal permalink
289 * @return string The new permalink. Same as the old if the post is not
290 * syndicated, or if FWP is set to use internal permalinks, or if the post
291 * was syndicated, but didn't have a proper permalink recorded.
292 *
293 * @uses SyndicatedLink::setting()
294 * @uses get_syndication_permalink()
295 * @uses get_syndication_feed_object()
296 * @uses url_to_postid()
297 * @global $id
298 * @global $feedwordpress_the_original_permalink
299 */
300 function syndication_permalink ($permalink = '', $post = null, $leavename = false, $sample = false) {
301 global $id;
302 global $feedwordpress_the_original_permalink;
303
304 // Save the local permalink in case we need to retrieve it later.
305 $feedwordpress_the_original_permalink = $permalink;
306
307 if (is_object($post) and isset($post->ID) and !empty($post->ID)) :
308 // Use the post ID we've been provided with.
309 $postId = $post->ID;
310 elseif (is_string($permalink) and strlen($permalink) > 0) :
311 // Map this permalink to a post ID so we can get the correct
312 // permalink even outside of the Post Loop. Props Björn.
313 $postId = url_to_postid($permalink);
314 else :
315 // If the permalink string is empty but Post Loop context
316 // provides an id.
317 $postId = $id;
318 endif;
319
320 $munge = false;
321 $link = get_syndication_feed_object($postId);
322 if (is_object($link)) :
323 $munge = ($link->setting('munge permalink', 'munge_permalink', 'yes') != 'no');
324 endif;
325
326 if ($munge):
327 $uri = get_syndication_permalink($postId);
328 $permalink = ((strlen($uri) > 0) ? $uri : $permalink);
329 endif;
330 return $permalink;
331 } /* function syndication_permalink () */
332
333 /**
334 * syndication_permalink_escaped: Escape XML special characters in syndicated
335 * permalinks when used in feed contexts and HTML display contexts.
336 *
337 * @param string $permalink
338 * @return string
339 *
340 * @uses is_syndicated()
341 * @uses FeedWordPress::munge_permalinks()
342 *
343 */
344 function syndication_permalink_escaped ($permalink) {
345 /* FIXME: This should review link settings not just global settings */
346 if (is_syndicated() and FeedWordPress::munge_permalinks()) :
347 // This is a foreign link; WordPress can't vouch for its not
348 // having any entities that need to be &-escaped. So we'll do
349 // it here.
350 $permalink = esc_html($permalink);
351 endif;
352 return $permalink;
353 } /* function syndication_permalink_escaped() */
354
355 /**
356 * syndication_comments_feed_link: Escape XML special characters in comments
357 * feed links
358 *
359 * @param string $link
360 * @return string
361 *
362 * @uses is_syndicated()
363 * @uses FeedWordPress::munge_permalinks()
364 */
365 function syndication_comments_feed_link ($link) {
366 global $feedwordpress_the_original_permalink, $id;
367
368 if (is_syndicated() and FeedWordPress::munge_permalinks()) :
369 // If the source post provided a comment feed URL using
370 // wfw:commentRss or atom:link/@rel="replies" we can make use of
371 // that value here.
372 $source = get_syndication_feed_object();
373 $replacement = null;
374
375 if (is_object($source) && $source->setting('munge comments feed links', 'munge_comments_feed_links', 'yes') != 'no') :
376 $commentFeeds = get_post_custom_values('wfw:commentRSS');
377 if (
378 is_array($commentFeeds)
379 and (count($commentFeeds) > 0)
380 and (strlen($commentFeeds[0]) > 0)
381 ) :
382 $replacement = $commentFeeds[0];
383
384 // This is a foreign link; WordPress can't vouch for its not
385 // having any entities that need to be &-escaped. So we'll do it
386 // here.
387 $replacement = esc_html($replacement);
388 endif;
389 endif;
390
391 if (is_null($replacement)) :
392 // Q: How can we get the proper feed format, since the
393 // format is, stupidly, not passed to the filter?
394 // A: Kludge kludge kludge kludge!
395 $fancy_permalinks = ('' != get_option('permalink_structure'));
396 if ($fancy_permalinks) :
397 preg_match('|/feed(/([^/]+))?/?$|', $link, $ref);
398
399 $format = (isset($ref[2]) ? $ref[2] : '');
400 if (strlen($format) == 0) : $format = get_default_feed(); endif;
401
402 $replacement = trailingslashit($feedwordpress_the_original_permalink) . 'feed';
403 if ($format != get_default_feed()) :
404 $replacement .= '/'.$format;
405 endif;
406 $replacement = user_trailingslashit($replacement, 'single_feed');
407 else :
408 // No fancy permalinks = no problem
409 // WordPress doesn't call get_permalink() to
410 // generate the comment feed URL, so the
411 // comments feed link is never munged by FWP.
412 endif;
413 endif;
414
415 if (!is_null($replacement)) : $link = $replacement; endif;
416 endif;
417 return $link;
418 } /* function syndication_comments_feed_link() */
419
420 require_once("${dir}/feedwordpress.pings.functions.php");
421
422 require_once("${dir}/feedwordpress.wp-admin.post-edit.functions.php");
423
424 ################################################################################
425 ## class FeedWordPressBoilerplateReformatter ###################################
426 ################################################################################
427
428 require_once("${dir}/feedwordpressboilerplatereformatter.class.php");
429 require_once("${dir}/feedwordpressboilerplatereformatter.shortcode.functions.php");
430
431 ################################################################################
432 ## class FeedWordPress #########################################################
433 ################################################################################
434
435 // class FeedWordPress: handles feed updates and plugs in to the XML-RPC interface
436 class FeedWordPress {
437 var $strip_attrs = array (
438 array('[a-z]+', 'style'),
439 array('[a-z]+', 'target'),
440 );
441 var $uri_attrs = array (
442 array('a', 'href'),
443 array('applet', 'codebase'),
444 array('area', 'href'),
445 array('blockquote', 'cite'),
446 array('body', 'background'),
447 array('del', 'cite'),
448 array('form', 'action'),
449 array('frame', 'longdesc'),
450 array('frame', 'src'),
451 array('iframe', 'longdesc'),
452 array('iframe', 'src'),
453 array('head', 'profile'),
454 array('img', 'longdesc'),
455 array('img', 'src'),
456 array('img', 'usemap'),
457 array('input', 'src'),
458 array('input', 'usemap'),
459 array('ins', 'cite'),
460 array('link', 'href'),
461 array('object', 'classid'),
462 array('object', 'codebase'),
463 array('object', 'data'),
464 array('object', 'usemap'),
465 array('q', 'cite'),
466 array('script', 'src')
467 );
468
469 var $feeds = null;
470 var $feedurls = null;
471 var $httpauth = null;
472
473 /**
474 * FeedWordPress::__construct (): Construct FeedWordPress singleton object
475 * and retrieves a list of feeds for later reference.
476 */
477 public function __construct () {
478 $this->feeds = array ();
479 $this->feedurls = array();
480 $links = FeedWordPress::syndicated_links();
481
482 if ($links): foreach ($links as $link):
483 $id = intval($link->link_id);
484 $url = $link->link_rss;
485
486 // Store for later reference.
487 $this->feeds[$id] = $id;
488 if (strlen($url) > 0) :
489 $this->feedurls[$url] = $id;
490 endif;
491 endforeach; endif;
492
493 // System-related event hooks
494 add_filter('cron_schedules', array($this, 'cron_schedules'), 10, 1);
495
496 // FeedWordPress-related event hooks
497 add_filter('feedwordpress_update_complete', array($this, 'process_retirements'), 1000, 1);
498
499 $this->httpauth = new FeedWordPressHTTPAuthenticator;
500 } /* FeedWordPress::__construct () */
501
502 /**
503 * FeedWordPress::add_filters() connects FeedWordPress to WordPress lifecycle
504 * events by setting up action and filter hooks.
505 *
506 * @uses get_option()
507 * @uses add_filter()
508 * @uses add_action()
509 * @uses remove_filter()
510 */
511 public function add_filters () {
512 # Syndicated items are generally received in output-ready (X)HTML and
513 # should not be folded, crumpled, mutilated, or spindled by WordPress
514 # formatting filters. But we don't want to interfere with filters for
515 # any locally-authored posts, either.
516 #
517 # What WordPress should really have is a way for upstream filters to
518 # stop downstream filters from running at all. Since it doesn't, and
519 # since a downstream filter can't access the original copy of the text
520 # that is being filtered, what we will do here is (1) save a copy of the
521 # original text upstream, before any other filters run, and then (2)
522 # retrieve that copy downstream, after all the other filters run, *if*
523 # this is a syndicated post
524
525 add_filter('the_content', 'feedwordpress_preserve_syndicated_content', -10000);
526 add_filter('the_content', 'feedwordpress_restore_syndicated_content', 10000);
527
528 add_action('atom_entry', 'feedwordpress_item_feed_data');
529
530 # Filter in original permalinks if the user wants that
531 add_filter('post_link', 'syndication_permalink', /*priority=*/ 1, /*arguments=*/ 3);
532 add_filter('post_type_link', 'syndication_permalink', /*priority=*/ 1, /*arguments=*/ 4);
533
534 # When foreign URLs are used for permalinks in feeds or display
535 # contexts, they need to be escaped properly.
536 add_filter('the_permalink', 'syndication_permalink_escaped');
537 add_filter('the_permalink_rss', 'syndication_permalink_escaped');
538
539 add_filter('post_comments_feed_link', 'syndication_comments_feed_link');
540
541 # WTF? By default, wp_insert_link runs incoming link_url and link_rss
542 # URIs through default filters that include `wp_kses()`. But `wp_kses()`
543 # just happens to escape any occurrence of & to &amp; -- which just
544 # happens to fuck up any URI with a & to separate GET parameters.
545 remove_filter('pre_link_rss', 'wp_filter_kses');
546 remove_filter('pre_link_url', 'wp_filter_kses');
547
548 # Boilerplate functionality: hook in to title, excerpt, and content to add boilerplate text
549 $hookOrder = get_option('feedwordpress_boilerplate_hook_order', FEEDWORDPRESS_BOILERPLATE_DEFAULT_HOOK_ORDER);
550 add_filter(
551 /*hook=*/ 'the_title',
552 /*function=*/ 'add_boilerplate_title',
553 /*priority=*/ $hookOrder,
554 /*arguments=*/ 2
555 );
556 add_filter(
557 /*hook=*/ 'get_the_excerpt',
558 /*function=*/ 'add_boilerplate_excerpt',
559 /*priority=*/ $hookOrder,
560 /*arguments=*/ 1
561 );
562 add_filter(
563 /*hook=*/ 'the_content',
564 /*function=*/ 'add_boilerplate_content',
565 /*priority=*/ $hookOrder,
566 /*arguments=*/ 1
567 );
568 add_filter(
569 /*hook=*/ 'the_content_rss',
570 /*function=*/ 'add_boilerplate_content',
571 /*priority=*/ $hookOrder,
572 /*arguments=*/ 1
573 );
574
575 # Admin menu
576 add_action('admin_init', array($this, 'admin_init'));
577 add_action('admin_menu', array($this, 'add_pages'));
578 add_action('admin_notices', array($this, 'check_debug'));
579
580 add_action('admin_menu', 'feedwordpress_add_post_edit_controls');
581 add_action('save_post', 'feedwordpress_save_post_edit_controls');
582
583 add_action('admin_footer', array($this, 'admin_footer'));
584
585 add_action('syndicated_feed_error', array('FeedWordPressDiagnostic', 'feed_error'), 100, 3);
586
587 add_action('wp_footer', 'debug_out_feedwordpress_footer', -100);
588 add_action('admin_footer', 'debug_out_feedwordpress_footer', -100);
589
590 # Cron-less auto-update. Hooray!
591 $autoUpdateHook = $this->automatic_update_hook();
592 if (!is_null($autoUpdateHook)) :
593 add_action($autoUpdateHook, array($this, 'auto_update'));
594 endif;
595
596 add_action('init', array($this, 'init'));
597 add_action('wp_loaded', array($this, 'wp_loaded'));
598
599 add_action('shutdown', array($this, 'email_diagnostic_log'));
600 add_action('shutdown', array($this, 'feedwordpress_cleanup'));
601 add_action('wp_dashboard_setup', array($this, 'dashboard_setup'));
602
603 # Default sanitizers
604 add_filter('syndicated_item_content', array('SyndicatedPost', 'resolve_relative_uris'), 0, 2);
605 add_filter('syndicated_item_content', array('SyndicatedPost', 'sanitize_content'), 0, 2);
606
607 add_action('plugins_loaded', array($this, 'admin_api'));
608 add_action('all_admin_notices', array($this, 'all_admin_notices'));
609
610 // Use the cache settings that we want, from a static method
611 add_filter('wp_feed_cache_transient_lifetime', array(get_class($this), 'cache_lifetime'));
612
613 } /* FeedWordPress::add_filters () */
614
615 ################################################################################
616 ## ADMIN MENU ADD-ONS: register Dashboard management pages #####################
617 ################################################################################
618
619 /**
620 * FeedWordPress::add_pages(): set up WordPress admin interface pages thru
621 * hooking in Syndication menu and submenus
622 *
623 * @uses FeedWordPress::menu_cap()
624 * @uses FeedWordPress::path()
625 * @uses add_menu_page()
626 * @uses add_submenu_page()
627 * @uses do_action()
628 * @uses add_filter()
629 */
630 public function add_pages () {
631
632 $menu_cap = FeedWordPress::menu_cap();
633 $settings_cap = FeedWordPress::menu_cap(/*sub=*/ true);
634 $syndicationMenu = FeedWordPress::path('syndication.php');
635
636 add_menu_page(
637 'Syndicated Sites', 'Syndication',
638 $menu_cap,
639 $syndicationMenu,
640 null,
641 $this->plugin_dir_url( 'assets/images/feedwordpress-tiny.png' )
642 );
643
644 do_action('feedwordpress_admin_menu_pre_feeds', $menu_cap, $settings_cap);
645 add_submenu_page(
646 $syndicationMenu, 'Syndicated Sites', 'Syndicated Sites',
647 $settings_cap, $syndicationMenu
648 );
649
650 do_action('feedwordpress_admin_menu_pre_feeds', $menu_cap, $settings_cap);
651 add_submenu_page(
652 $syndicationMenu, 'Syndicated Feeds & Updates', 'Feeds & Updates',
653 $settings_cap, FeedWordPress::path('feeds-page.php')
654 );
655
656 do_action('feedwordpress_admin_menu_pre_posts', $menu_cap, $settings_cap);
657 add_submenu_page(
658 $syndicationMenu, 'Syndicated Posts & Links', 'Posts & Links',
659 $settings_cap, FeedWordPress::path('posts-page.php')
660 );
661
662 do_action('feedwordpress_admin_menu_pre_authors', $menu_cap, $settings_cap);
663 add_submenu_page(
664 $syndicationMenu, 'Syndicated Authors', 'Authors',
665 $settings_cap, FeedWordPress::path('authors-page.php')
666 );
667
668 do_action('feedwordpress_admin_menu_pre_categories', $menu_cap, $settings_cap);
669 add_submenu_page(
670 $syndicationMenu, 'Categories & Tags', 'Categories & Tags',
671 $settings_cap, FeedWordPress::path('categories-page.php')
672 );
673
674 do_action('feedwordpress_admin_menu_pre_performance', $menu_cap, $settings_cap);
675 add_submenu_page(
676 $syndicationMenu, 'FeedWordPress Performance', 'Performance',
677 $settings_cap, FeedWordPress::path('performance-page.php')
678 );
679
680 do_action('feedwordpress_admin_menu_pre_diagnostics', $menu_cap, $settings_cap);
681 add_submenu_page(
682 $syndicationMenu, 'FeedWordPress Diagnostics', 'Diagnostics',
683 $settings_cap, FeedWordPress::path('diagnostics-page.php')
684 );
685
686 add_filter('page_row_actions', array($this, 'row_actions'), 10, 2);
687 add_filter('post_row_actions', array($this, 'row_actions'), 10, 2);
688 } /* function FeedWordPress::add_pages () */
689
690 public function check_debug () {
691 // This is a horrible fucking kludge that I have to do because the
692 // admin notice code is triggered before the code that updates the
693 // setting.
694 $feedwordpress_debug = FeedWordPress::param( 'feedwordpress_debug', get_option( 'feedwordpress_debug' ) );
695
696 FeedWordPressSettingsUI::get_template_part('notice-debug-mode', $feedwordpress_debug, 'html');
697 } /* function FeedWordPress::check_debug () */
698
699 /**
700 * FeedWordPress::subscribed (): Check whether a feed is currently in the
701 * subscription list for FeedWordPress.
702 *
703 * @param mixed $id Numeric ID of a WordPress link object or URL of a feed
704 * @return bool TRUE if currently subscribed; FALSE otherwise.
705 */
706 public function subscribed ($id) {
707 return (isset($this->feedurls[$id]) or isset($this->feeds[$id]));
708 } /* FeedWordPress::subscribed () */
709
710 /**
711 * FeedWordPress::subscription (): Get the SyndicatedLink object for a
712 * given URL or numeric ID, if we have either an active subscription to
713 * it; or a de-activated subscription.
714 *
715 * @param mixed $which Numeric ID for a WordPress link object or string URL for a feed
716 * @return mixed SyndicatedLink object if subscription is found; null if not
717 */
718 public function subscription ($which) {
719 $sub = null;
720
721 if (is_string($which) and isset($this->feedurls[$which])) :
722 $which = $this->feedurls[$which];
723 endif;
724
725 if (isset($this->feeds[$which])) :
726 $sub = $this->feeds[$which];
727 endif;
728
729 // If it's not in the in-memory cache already, try to load it from DB.
730 // This is necessary to fill requests for subscriptions that we don't
731 // cache in memory, e.g. for deactivated feeds.
732 if (is_null($sub)) :
733 $sub = get_bookmark($which);
734 endif;
735
736 // Load 'er up if you haven't already.
737 if (!is_null($sub) and !($sub InstanceOf SyndicatedLink)) :
738 $link = new SyndicatedLink($sub);
739 $this->feeds[$which] = $link;
740 $sub = $link;
741 endif;
742
743 return $sub;
744 } /* FeedWordPress::subscription () */
745
746 # function update (): polls for updates on one or more Contributor feeds
747 #
748 # Arguments:
749 # ----------
750 # * $uri (string): either the URI of the feed to poll, the URI of the
751 # (human-readable) website whose feed you want to poll, or null.
752 #
753 # If $uri is null, then FeedWordPress will poll any feeds that are
754 # ready for polling. It will not poll feeds that are marked as
755 # "Invisible" Links (signifying that the subscription has been
756 # de-activated), or feeds that are not yet stale according to their
757 # TTL setting (which is either set in the feed, or else set
758 # randomly within a window of 30 minutes - 2 hours).
759 #
760 # Returns:
761 # --------
762 # * Normally returns an associative array, with 'new' => the number
763 # of new posts added during the update, and 'updated' => the number
764 # of old posts that were updated during the update. If both numbers
765 # are zero, there was no change since the last poll on that URI.
766 #
767 # * Returns null if URI it was passed was not a URI that this
768 # installation of FeedWordPress syndicates.
769 #
770 # Effects:
771 # --------
772 # * One or more feeds are polled for updates
773 #
774 # * If the feed Link does not have a hardcoded name set, its Link
775 # Name is synchronized with the feed's title element
776 #
777 # * If the feed Link does not have a hardcoded URI set, its Link URI
778 # is synchronized with the feed's human-readable link element
779 #
780 # * If the feed Link does not have a hardcoded description set, its
781 # Link Description is synchronized with the feed's description,
782 # tagline, or subtitle element.
783 #
784 # * The time of polling is recorded in the feed's settings, and the
785 # TTL (time until the feed is next available for polling) is set
786 # either from the feed (if it is supplied in the ttl or syndication
787 # module elements) or else from a randomly-generated time window
788 # (between 30 minutes and 2 hours).
789 #
790 # * New posts from the polled feed are added to the WordPress store.
791 #
792 # * Updates to existing posts since the last poll are mirrored in the
793 # WordPress store.
794 #
795 public function update ($uri = null, $crash_ts = null) {
796 global $wpdb;
797
798 if (FeedWordPress::needs_upgrade()) : // Will make duplicate posts if we don't hold off
799 return null;
800 endif;
801
802 if (!is_null($uri) and $uri != '*') :
803 $uri = trim($uri);
804 else : // Update all
805 if ($this->update_hooked) :
806 $diag = $this->update_hooked;
807 else :
808 $diag = 'Initiating a MANUAL check-in on the update schedule at '.date('r', time());
809 endif;
810 $this->diagnostic('update_schedule:check', $diag);
811
812 update_option('feedwordpress_last_update_all', time());
813 endif;
814
815 do_action('feedwordpress_update', $uri);
816
817 if (is_null($crash_ts)) :
818 $crash_ts = $this->crash_ts();
819 endif;
820
821 // Randomize order for load balancing purposes
822 $feed_set = array_keys($this->feeds);
823 shuffle($feed_set);
824
825 $updateWindow = (int) get_option('feedwordpress_update_window', DEFAULT_UPDATE_PERIOD) * 60 /* sec/min */;
826 $interval = (int) get_option('feedwordpress_freshness', FEEDWORDPRESS_FRESHNESS_INTERVAL);
827 $portion = max(
828 (int) ceil(count($feed_set) / ($updateWindow / $interval)),
829 10
830 );
831
832 $max_polls = apply_filters('feedwordpress_polls_per_update', get_option(
833 'feedwordpress_polls_per_update', $portion
834 ), $uri);
835
836 $feed_set = apply_filters('feedwordpress_update_feeds', $feed_set, $uri);
837
838
839 // Loop through and check for new posts
840 $delta = null; $remaining = $max_polls;
841 foreach ($feed_set as $feed_id) :
842
843 $feed = $this->subscription($feed_id);
844
845 // Has this process overstayed its welcome?
846 if (
847 // Over time limit?
848 (!is_null($crash_ts) and (time() > $crash_ts))
849
850 // Over feed count?
851 or ($remaining == 0)
852 ) :
853 break;
854 endif;
855
856 $pinged_that = (is_null($uri) or ($uri=='*') or in_array($uri, array($feed->uri(), $feed->homepage())));
857
858 if (!is_null($uri)) : // A site-specific ping always updates
859 $timely = true;
860 else :
861 $timely = $feed->stale();
862 endif;
863
864 // If at least one feed was hit for updating...
865 if ($pinged_that and is_null($delta)) :
866 // ... don't return error condition
867 $delta = array('new' => 0, 'updated' => 0, 'stored' => 0);
868 endif;
869
870 if ($pinged_that and $timely) :
871 $remaining = $remaining - 1;
872
873 do_action('feedwordpress_check_feed', $feed->settings);
874 $start_ts = time();
875 $added = $feed->poll($crash_ts);
876 do_action('feedwordpress_check_feed_complete', $feed->settings, $added, time() - $start_ts);
877
878 if (is_array($added)) : // Success
879 foreach ($added as $key => $count) :
880 $delta[$key] += $added[$key];
881 endforeach;
882 endif;
883 endif;
884 endforeach;
885
886 do_action('feedwordpress_update_complete', $delta);
887 return $delta;
888 } /* FeedWordPress::update () */
889
890 public function crash_ts ($default = null) {
891 $crash_dt = (int) get_option('feedwordpress_update_time_limit', 0);
892 if ($crash_dt > 0) :
893 $crash_ts = time() + $crash_dt;
894 else :
895 $crash_ts = $default;
896 endif;
897 return $crash_ts;
898 } /* FeedWordPress::crash_ts () */
899
900 public function secret_key () {
901 $secret = get_option('feedwordpress_secret_key', false);
902 if (!$secret) : // Generate random key.
903 $secret = substr(md5(uniqid(microtime())), 0, 6);
904 update_option('feedwordpress_secret_key', $secret);
905 endif;
906 return $secret;
907 } /* FeedWordPress::secret_key () */
908
909 public function has_secret () {
910 return (MyPHP::request('feedwordpress_key')==$this->secret_key());
911 } /* FeedWordPress::has_secret () */
912
913 var $update_hooked = null;
914 public function automatic_update_hook ($params = array()) {
915 $params = wp_parse_args($params, array( // Defaults
916 'setting only' => false,
917 ));
918 $hook = get_option('feedwordpress_automatic_updates', null);
919 $method = 'FeedWordPress option';
920
921 // Allow for forced behavior in testing.
922 if (
923 !$params['setting only']
924 and $this->has_secret()
925 and MyPHP::request('automatic_update')
926 ) :
927 $hook = MyPHP::request('automatic_update');
928 $method = 'URL parameter';
929 endif;
930
931 $exact = $hook; // Before munging
932
933 if (!!$hook) :
934 if ($hook == 'init' or $hook == 'wp_loaded') : // Re-map init to wp_loaded
935 $hook = ($params['setting only'] ? 'init' : 'wp_loaded');
936
937 // Constrain possible values. If it's not an init or wp_loaded, it's a shutdown
938 else :
939 $hook = 'shutdown';
940 endif;
941 endif;
942
943 if ($hook) :
944 $this->update_hooked = "Initiating an AUTOMATIC CHECK FOR UPDATES ON PAGE LOAD ".$hook." due to ".$method." = ".trim($this->val($exact));
945 endif;
946
947 return $hook;
948 } /* FeedWordPress::automatic_update_hook () */
949
950 public function last_update_all () {
951 $last = get_option('feedwordpress_last_update_all');
952 if ( $this->has_secret() and FeedWordPress::param('automatic_update') ) :
953 $last = 1; // A long, long time ago.
954 elseif ( $this->has_secret() and FeedWordPress::param('last_update_all') ) :
955 $last = FeedWordPress::param( 'last_update_all' );
956 endif;
957 return $last;
958 } /* FeedWordPress::last_update_all () */
959
960 public function force_update_all () {
961 return ($this->has_secret() and FeedWordPress::param( 'force_update_feeds' ));
962 } /* FeedWordPress::force_update_all () */
963
964 public function stale () {
965 if (!is_null($this->automatic_update_hook())) :
966 // Do our best to avoid possible simultaneous
967 // updates by getting up-to-the-minute settings.
968
969 $last = $this->last_update_all();
970
971 // If we haven't updated all yet, give it a time window
972 if (false === $last) :
973 $ret = false;
974 update_option('feedwordpress_last_update_all', time());
975
976 // Otherwise, check against freshness interval
977 elseif (is_numeric($last)) : // Expect a timestamp
978 $freshness = get_option('feedwordpress_freshness');
979 if (false === $freshness) : // Use default
980 $freshness = FEEDWORDPRESS_FRESHNESS_INTERVAL;
981 endif;
982 $ret = ( (time() - $last) > $freshness );
983
984 // This should never happen.
985 else :
986 FeedWordPressDiagnostic::critical_bug('FeedWordPress::stale::last', $last, __LINE__, __FILE__);
987 endif;
988
989 else :
990 $ret = false;
991 endif;
992 return $ret;
993 } /* FeedWordPress::stale() */
994
995 static function admin_init () {
996 // WordPress 3.5+ compat: the WP devs are in the midst of removing Links from the WordPress core. Eventually we'll have to deal
997 // with the possible disappearance of the wp_links table as a whole; but in the meantime, we just need to turn on the interface
998 // element to avoid problems with user capabilities that presume the presence of the Links Manager in the admin UI.
999 if (!intval(get_option('link_manager_enabled', false))) :
1000 update_option('link_manager_enabled', true);
1001 endif;
1002
1003 if (defined('FEEDWORDPRESS_PREPARE_TO_ZAP') and FEEDWORDPRESS_PREPARE_TO_ZAP) :
1004 $post_id = FEEDWORDPRESS_PREPARE_TO_ZAP;
1005 $sendback = wp_get_referer();
1006 if (
1007 ! $sendback
1008 or strpos( $sendback, 'post.php' ) !== false
1009 or strpos( $sendback, 'post-new.php' ) !== false
1010 ) :
1011 if ( 'attachment' == $post_type ) :
1012 $sendback = admin_url( 'upload.php' );
1013 else :
1014 $sendback = admin_url( 'edit.php' );
1015 $sendback .= ( ! empty( $post_type ) ) ? '?post_type=' . $post_type : '';
1016 endif;
1017 else :
1018 $sendback = esc_url( remove_query_arg( array('trashed', 'untrashed', 'deleted', 'zapped', 'unzapped', 'ids'), $sendback ) );
1019 endif;
1020
1021 // Make sure we have a post corresponding to this ID.
1022 $post = $post_type = $post_type_object = null;
1023 if ( $post_id ) :
1024 $post = get_post( $post_id );
1025 endif;
1026
1027 if ( $post ) :
1028 $post_type = $post->post_type;
1029 endif;
1030 $p = get_post($post_id);
1031
1032 if ( ! $post ) :
1033 wp_die( __( 'The item you are trying to zap no longer exists.' ) );
1034 endif;
1035
1036 if ( ! current_user_can( 'delete_post', $post_id ) ) :
1037 wp_die( __( 'You are not allowed to zap this item.' ) );
1038 endif;
1039
1040 if ( $user_id = wp_check_post_lock( $post_id ) ) :
1041 $user = get_userdata( $user_id );
1042 wp_die( sprintf( __( 'You cannot retire this item. %s is currently editing.' ), $user->display_name ) );
1043 endif;
1044
1045 if (FeedWordPress::param( 'fwp_post_delete' ) == 'zap') :
1046 FeedWordPress::diagnostic('syndicated_posts', 'Zapping existing post # '.$p->ID.' "'.$p->post_title.'" due to user request.');
1047
1048 $old_status = $post->post_status;
1049
1050 set_post_field('post_status', 'fwpzapped', $post_id);
1051 wp_transition_post_status('fwpzapped', $old_status, $post);
1052
1053 # Set up the post to have its content blanked on
1054 # next update if you do not undo the zapping.
1055 add_post_meta($post_id, '_feedwordpress_zapped_blank_me', 1, /*unique=*/ true);
1056 add_post_meta($post_id, '_feedwordpress_zapped_blank_old_status', $old_status, /*unique=*/ true);
1057
1058 wp_redirect( esc_url_raw( add_query_arg( array('zapped' => 1, 'ids' => $post_id), $sendback ) ) );
1059
1060 else :
1061 $old_status = get_post_meta($post_id, '_feedwordpress_zapped_blank_old_status', /*single=*/ true);
1062
1063 set_post_field('post_status', $old_status, $post_id);
1064 wp_transition_post_status($old_status, 'fwpzapped', $post);
1065
1066 # O.K., make sure this post does not get blanked
1067 delete_post_meta($post_id, '_feedwordpress_zapped_blank_me');
1068 delete_post_meta($post_id, '_feedwordpress_zapped_blank_old_status');
1069
1070 wp_redirect( esc_url_raw( add_query_arg( array('unzapped' => 1, 'ids' => $post_id), $sendback ) ) );
1071
1072 endif;
1073
1074 // Intercept, don't pass on.
1075 exit;
1076 endif;
1077 } /* FeedWordPress::admin_init() */
1078
1079 public function admin_api () {
1080 // This sucks, but WordPress doesn't give us any other way to
1081 // easily invoke a permanent-delete from a plugged in post
1082 // actions link. So we create a magic parameter, and when this
1083 // magic parameter is activated, the WordPress trashcan is
1084 // temporarily de-activated.
1085
1086 if (FeedWordPress::param( 'fwp_post_delete' ) == 'nuke') :
1087 // Get post ID #
1088 $post_id = FeedWordPress::param( 'post' );
1089 if (!$post_id) :
1090 $post_id = FeedWordPress::param( 'post_ID' );
1091 endif;
1092
1093 // Make sure we've got the right nonce and all that.
1094 check_admin_referer('delete-post_' . $post_id);
1095
1096 // If so, disable the trashcan.
1097 define('EMPTY_TRASH_DAYS', 0);
1098
1099 elseif ( FeedWordPress::param( 'fwp_post_delete' ) == 'zap' || FeedWordPress::param( 'fwp_post_delete' ) == 'unzap' ) :
1100 // Get post ID #
1101 $post_id = FeedWordPress::param( 'post' );
1102 if (!$post_id) :
1103 $post_id = FeedWordPress::param( 'post_ID' );
1104 endif;
1105
1106 // Make sure we've got the right nonce and all that
1107 check_admin_referer('delete-post_' . $post_id);
1108
1109 // If so, get ready to intercept the call a little
1110 // further down the line.
1111
1112 define('FEEDWORDPRESS_PREPARE_TO_ZAP', $post_id);
1113
1114 endif;
1115
1116 } /* FeedWordPress::admin_api () */
1117
1118 public function all_admin_notices () {
1119 if (FeedWordPress::param( 'zapped' )) :
1120 $n = intval( FeedWordPress::param( 'zapped' ) );
1121 ?>
1122 <div id="message" class="updated"><p><?php print esc_html( $n ); ?> syndicated item<?php print esc_html( $n != 1 ? 's' : '' ); ?> zapped. <strong>These items will not be re-syndicated.</strong> If this was a mistake, you must <strong>immediately</strong> Un-Zap them in the Zapped items section to avoid losing the data.</p></div>
1123 <?php
1124 endif;
1125
1126 if ( FeedWordPress::param( 'unzapped' ) ) :
1127 $n = intval( FeedWordPress::param( 'unzapped' ) );
1128 ?>
1129 <div id="message" class="updated"><p><?php print esc-html( $n ); ?> syndicated item<?php print esc_html( $n != 1 ? 's' : '' ) ?> un-zapped and restored to normal.</p></div>
1130 <?php
1131 endif;
1132 } /* FeedWordPress::all_admin_notices () */
1133
1134 public function process_retirements ($delta) {
1135 update_option('feedwordpress_process_zaps', 1);
1136
1137 return $delta;
1138 }
1139
1140 public function feedwordpress_cleanup () {
1141 if (get_option('feedwordpress_process_zaps', null)) :
1142 $q = new WP_Query(array(
1143 'fields' => '_synfrom',
1144 'post_status' => 'fwpzapped',
1145 'ignore_sticky_posts' => true,
1146 'meta_key' => '_feedwordpress_zapped_blank_me',
1147 'meta_value' => 1,
1148 ));
1149
1150 if ($q->have_posts()) :
1151 foreach ($q->posts as $p) :
1152
1153 $post_id = $p->ID;
1154 $revisions = wp_get_post_revisions($post_id, array("check_enabled" => false));
1155
1156 # Now nuke the content of the post & its revisions
1157 set_post_field('post_content', '', $post_id);
1158 set_post_field('post_excerpt', '', $post_id);
1159
1160 foreach ($revisions as $rev) :
1161 set_post_field('post_content', '', $rev->ID);
1162 set_post_field('post_excerpt', '', $rev->ID);
1163 endforeach;
1164
1165 # Un-tag it for blanking.
1166 delete_post_meta($p->ID, '_feedwordpress_zapped_blank_me');
1167
1168 # Don't remove old_status indicator. A later
1169 # update from the feed may cause us to once
1170 # again have some content so we can un-zap.
1171
1172 endforeach;
1173 endif;
1174
1175 $q = new WP_Query(array(
1176 'fields' => '_synfrom',
1177 'post_status' => 'fwpzapped',
1178 'ignore_sticky_posts' => true,
1179 'meta_key' => '_feedwordpress_zapped_blank_me',
1180 'meta_value' => 2,
1181 ));
1182
1183 if ($q->have_posts()) :
1184 foreach ($q->posts as $p) :
1185 update_post_meta($p->ID, '_feedwordpress_zapped_blank_me', 1);
1186 endforeach;
1187 endif;
1188
1189 update_option('feedwordpress_process_zaps', 0);
1190 endif;
1191 } /* FeedWordPress::feedwordpress_cleanup () */
1192
1193 public function init () {
1194
1195 // If this is a FeedWordPress admin page, queue up scripts for AJAX
1196 // functions that FWP uses. If it is a display page or a non-FWP admin
1197 // page, don't.
1198 wp_register_style('feedwordpress-elements', $this->plugin_dir_url( 'assets/css/feedwordpress-elements.css') );
1199 if (FeedWordPressSettingsUI::is_admin()) :
1200 // For JavaScript that needs to be generated dynamically
1201 add_action('admin_print_scripts', array('FeedWordPressSettingsUI', 'admin_scripts'));
1202
1203 // For CSS that needs to be generated dynamically.
1204 add_action('admin_print_styles', array('FeedWordPressSettingsUI', 'admin_styles'));
1205
1206 wp_enqueue_style('dashboard');
1207 wp_enqueue_style('feedwordpress-elements');
1208 endif;
1209
1210 // These are a special post statuses for hiding posts that have
1211 // expired from the feed or been marked for permanent zapping by
1212 // the FWP admin.
1213 register_post_status('fwpretired', array(
1214 'label' => _x('Retired', 'post'),
1215 'label_count' => _n_noop('Retired <span class="count">(%s)</span>', 'Retired <span class="count">(%s)</span>'),
1216 'exclude_from_search' => true,
1217 'public' => false,
1218 'publicly_queryable' => false,
1219 'show_in_admin_all_list' => false,
1220 'show_in_admin_status_list' => true,
1221 ));
1222 register_post_status('fwpzapped', array(
1223 'label' => _x('Zapped', 'post'),
1224 'label_count' => _n_noop('Zapped <span class="count">(%s)</span>', 'Retired <span class="count">(%s)</span>'),
1225 'exclude_from_search' => true,
1226 'public' => false,
1227 'publicly_queryable' => false,
1228 'show_in_admin_all_list' => false,
1229 'show_in_admin_status_list' => true,
1230 ));
1231 add_action(
1232 /*hook=*/ 'template_redirect',
1233 /*function=*/ array($this, 'redirect_retired'),
1234 /*priority=*/ -100
1235 );
1236
1237 add_action('wp_ajax_fwp_feeds', array($this, 'fwp_feeds'));
1238 add_action('wp_ajax_fwp_feedcontents', array($this, 'fwp_feedcontents'));
1239 add_action('wp_ajax_fwp_xpathtest', array($this, 'fwp_xpathtest'));
1240
1241 } /* FeedWordPress::init() */
1242
1243 /**
1244 * FeedWordPress::wp_loaded (): Once all plugin and theme modules have been
1245 * loaded and initialized (by actions on the init hook, etc.), check the HTTP
1246 * request to see if we need to perform any special FeedWordPress-related
1247 * actions.
1248 *
1249 * @since 2016.0614
1250 */
1251 public function wp_loaded () {
1252 $this->clear_cache_magic_url();
1253 $this->update_magic_url();
1254 } /* FeedWordPress::wp_loaded () */
1255
1256 /**
1257 * FeedWordPress::cron_schedules(): Filter hook to add WP-Cron schedules
1258 * that plugins like FeedWordPress can use to carry out scheduled events.
1259 *
1260 * @param array $schedules An associative array containing the current set of cron schedules (hourly, daily, etc.)
1261 * @return array The same array, with a new entry ('fwp_checkin_interval') added to the list.
1262 *
1263 * @since 2017.1021
1264 */
1265 public function cron_schedules ($schedules) {
1266 $interval = FEEDWORDPRESS_DEFAULT_CHECKIN_INTERVAL*60 /*sec/min*/;
1267
1268 // add 'fwp_checkin_interval' to the existing set
1269 $schedules['fwp_checkin_interval'] = array(
1270 'interval' => $interval,
1271 'display' => 'FeedWordPress update schedule check-in',
1272 );
1273
1274 return $schedules;
1275 } /* FeedWordPress::cron_schedules () */
1276
1277 public function fwp_feeds () {
1278 $feeds = array();
1279 $feed_ids = $this->feeds;
1280
1281 foreach ($feed_ids as $id) :
1282 $sub = $this->subscription($id);
1283 $feeds[] = array(
1284 "id" => $id,
1285 "url" => $sub->uri(),
1286 "name" => $sub->name(/*fromFeed=*/ false),
1287 );
1288 endforeach;
1289
1290 header("Content-Type: application/json");
1291 echo json_encode($feeds);
1292 exit;
1293 } /* FeedWordPress::fwp_feeds () */
1294
1295 public function fwp_feedcontents () {
1296 $feed_id = FeedWordPress::param( 'feed_id' );
1297
1298 // Let's load up some data from the feed . . .
1299 $feed = $this->subscription($feed_id);
1300 $posts = $feed->live_posts();
1301
1302 if (is_wp_error($posts)) :
1303 header("HTTP/1.1 502 Bad Gateway");
1304 $result = $posts;
1305 else :
1306 $result = array();
1307
1308 foreach ($posts as $post) :
1309 $p = new SyndicatedPost($post, $feed);
1310
1311 $result[] = array(
1312 "post_title" => $p->entry->get_title(),
1313 "post_link" => $p->permalink(),
1314 "guid" => $p->guid(),
1315 "post_date" => $p->published(),
1316 );
1317 endforeach;
1318 endif;
1319
1320 header("Content-Type: application/json");
1321
1322 echo json_encode($result);
1323
1324 // This is an AJAX request, so close it out thus.
1325 die;
1326 } /* FeedWordPress::fwp_feedcontents () */
1327
1328 public function fwp_xpathtest () {
1329 $xpath = FeedWordPress::param( 'xpath' );
1330 $feed_id = FeedWordPress::param( 'feed_id' );
1331 $post_id = FeedWordPress::param( 'post_id' );
1332
1333 $expr = new FeedWordPressParsedPostMeta($xpath);
1334
1335 // Let's load up some data from the feed . . .
1336 $feed = $this->subscription($feed_id);
1337 $posts = $feed->live_posts();
1338
1339 if (!is_wp_error($posts)) :
1340 if (strlen($post_id) == 0) :
1341 $post = $posts[0];
1342 else :
1343 $post = null;
1344
1345 foreach ($posts as $p) :
1346 if ($p->get_id() == $post_id) :
1347 $post = $p;
1348 endif;
1349 endforeach;
1350 endif;
1351
1352 $post = new SyndicatedPost($post, $feed);
1353 $meta = $expr->do_substitutions($post);
1354
1355 $result = array(
1356 "post_title" => $post->entry->get_title(),
1357 "post_link" => $post->permalink(),
1358 "guid" => $post->guid(),
1359 "expression" => $xpath,
1360 "results" => $meta
1361 );
1362 else :
1363 $result = array(
1364 "expression" => $xpath,
1365 "feed_id" => $feed_id,
1366 "post_id" => $post_id,
1367 "results" => $posts
1368 );
1369
1370 header("HTTP/1.1 503 Bad Gateway");
1371 endif;
1372
1373 header("Content-Type: application/json");
1374
1375 echo json_encode($result);
1376
1377 // This is an AJAX request, so close it out thus.
1378 die;
1379 } /* FeedWordPress::fwp_xpathtest () */
1380
1381 public function redirect_retired () {
1382 global $wp_query;
1383 if (is_singular()) :
1384 if (
1385 'fwpretired'==$wp_query->post->post_status
1386 or 'fwpzapped'==$wp_query->post->post_status
1387 ) :
1388 do_action('feedwordpress_redirect_retired', $wp_query->post);
1389
1390 if (!($template = get_404_template())) :
1391 $template = get_index_template();
1392 endif;
1393 if ($template = apply_filters('template_include', $template)) :
1394 header("HTTP/1.1 410 Gone");
1395 include($template);
1396 endif;
1397 exit;
1398 endif;
1399 endif;
1400 } /* FeedWordPress::redirect_retired () */
1401
1402 public function row_actions ($actions, $post) {
1403 if (is_syndicated($post->ID) && current_user_can('edit_post', $post->ID)) :
1404 $link = get_delete_post_link($post->ID, '', true);
1405 $eraseLink = MyPHP::url($link, array("fwp_post_delete" => "nuke"));
1406
1407 $caption = apply_filters('feedwordpress_ui_erase_link_caption', __('Erase the record of this post (will be re-syndicated if it still appears on the feed).'));
1408 $linktext = apply_filters('feedwordpress_ui_erase_link_text', __('Erase/Resyndicate'));
1409
1410 $retireClass = null;
1411 if ($post->post_status == 'fwpzapped') :
1412 if (count(get_post_meta($post->ID, '_feedwordpress_zapped_blank_me')) > 0) :
1413 $retireCap = 'Un-Zap this syndicated post (so it will appear on the site again)';
1414 $retireText = 'Un-Zap &amp; Restore';
1415 $retireLink = MyPHP::url($link, array("fwp_post_delete" => "unzap"));
1416 else :
1417 // No Un-Zap link for posts that have
1418 // been blanked. You'll just have to
1419 // Erase and hope you can resyndicate...
1420 $retireLink = null;
1421 endif;
1422 else :
1423 $retireCap = apply_filters('feedwordpress_ui_zap_link_caption', __('Zap this syndicated post (so it will not be re-syndicated if it still appears on the feed).'));
1424 $retireText = apply_filters('feedwordpress_ui_zap_link_text', __('Zap/Don&#8217;t Resyndicate'));
1425 $retireLink = MyPHP::url($link, array("fwp_post_delete" => "zap"));
1426 $retireClass = 'submitdelete';
1427 endif;
1428
1429 $keys = array_keys($actions);
1430 $links = array();
1431 foreach ($keys as $key) :
1432 $links[$key] = $actions[$key];
1433
1434 if ('trash'==$key) :
1435 #$links[$key] = "<a class='submitdelete' title='" . esc_attr( __( 'Move this item to the Trash (will NOT be re-syndicated)' ) ) . "' href='" . get_delete_post_link( $post->ID ) . "'>" . __( 'Trash' ) . "</a>";
1436
1437 // Placeholder.
1438 if (!is_null($retireLink)) :
1439 $links['zap trash'] = '';
1440 endif;
1441 $links['delete'] = '';
1442 endif;
1443 endforeach;
1444
1445 if (!is_null($retireLink)) :
1446 $links['zap trash'] = '<a class="'.esc_attr($retireClass).'" title="'.esc_attr(__($retireCap)).'" href="' . $retireLink . '">' . __($retireText) . '</a>';
1447 endif;
1448 $links['delete'] = '<a class="submitdelete" title="'.esc_attr(__($caption)).'" href="' . $eraseLink . '">' . __($linktext) . '</a>';
1449
1450 $actions = $links;
1451 endif;
1452 return $actions;
1453 } /* FeedWordPress::row_actions () */
1454
1455 public function dashboard_setup () {
1456 $see_it = FeedWordPress::menu_cap();
1457
1458 if (current_user_can($see_it)) :
1459 // Get the stylesheet
1460 wp_enqueue_style('feedwordpress-elements');
1461
1462 $widget_id = 'feedwordpress_dashboard';
1463 $widget_name = __('Syndicated Sources');
1464 $column = 'side';
1465 $priority = 'core';
1466
1467 // I would love to use wp_add_dashboard_widget() here and save
1468 // myself some trouble. But WP 3 does not yet have any way to
1469 // push a dashboard widget onto the side, or to give it a default
1470 // location.
1471 add_meta_box(
1472 /*id=*/ $widget_id,
1473 /*title=*/ $widget_name,
1474 /*callback=*/ array($this, 'dashboard'),
1475 /*page=*/ 'dashboard',
1476 /*context=*/ $column,
1477 /*priority=*/ $priority
1478 );
1479 /*control_callback= array($this, 'dashboard_control') */
1480
1481 // This is kind of rude, I know, but the dashboard widget isn't
1482 // worth much if users don't know that it exists, and I don't
1483 // know of any better way to reorder the boxen.
1484 //
1485 // Gleefully ripped off of codex.wordpress.org/Dashboard_Widgets_API
1486
1487 // Globalize the metaboxes array, this holds all the widgets for wp-admin
1488 global $wp_meta_boxes;
1489
1490 // Get the regular dashboard widgets array
1491 // (which has our new widget already but at the end)
1492
1493 $normal_dashboard = $wp_meta_boxes['dashboard'][$column][$priority];
1494
1495 // Backup and delete our new dashbaord widget from the end of the array
1496 if (isset($normal_dashboard[$widget_id])) :
1497 $backup = array();
1498 $backup[$widget_id] = $normal_dashboard[$widget_id];
1499 unset($normal_dashboard[$widget_id]);
1500
1501 // Merge the two arrays together so our widget is at the
1502 // beginning
1503 $sorted_dashboard = array_merge($backup, $normal_dashboard);
1504
1505 // Save the sorted array back into the original metaboxes
1506 $wp_meta_boxes['dashboard'][$column][$priority] = $sorted_dashboard;
1507 endif;
1508 endif;
1509 } /* FeedWordPress::dashboard_setup () */
1510
1511 public function dashboard () {
1512 $syndicationPage = new FeedWordPressSyndicationPage(dirname(__FILE__).'/syndication.php');
1513 $syndicationPage->dashboard_box($syndicationPage);
1514 } /* FeedWordPress::dashboard () */
1515
1516 public function user_can_richedit ($rich_edit) {
1517
1518 $post = new FeedWordPressLocalPost;
1519
1520 if (!$post->is_exposed_to_formatting_filters()) :
1521 // Disable visual editor and only allow operations
1522 // directly on HTML if post is bypassing fmt filters
1523 # $rich_edit = false;
1524 endif;
1525
1526 return $rich_edit;
1527
1528 } /* FeedWordPress::user_can_richedit () */
1529
1530 public function clear_cache_magic_url () {
1531 if ($this->clear_cache_requested()) :
1532 $this->clear_cache();
1533 endif;
1534 } /* FeedWordPress::clear_cache_magic_url() */
1535
1536 public function clear_cache_requested () {
1537 return FeedWordPress::param( 'clear_cache' );
1538 } /* FeedWordPress::clear_cache_requested() */
1539
1540 public function update_magic_url () {
1541 global $wpdb;
1542
1543 // Explicit update request in the HTTP request (e.g. from a cron job)
1544 if (self::update_requested()) :
1545 /*DBG*/ header("Content-Type: text/plain");
1546
1547 $this->update_hooked = "Initiating a CRON JOB CHECK-IN ON UPDATE SCHEDULE due to URL parameter = " . trim( $this->val( FeedWordPress::param('update_feedwordpress' ) ) );
1548
1549 $this->update($this->update_requested_url());
1550
1551 if (FEEDWORDPRESS_DEBUG and is_array($wpdb->queries) and count($wpdb->queries) > 0) :
1552 $mysqlTime = 0.0;
1553 $byTime = array();
1554 foreach ($wpdb->queries as $query) :
1555 $time = $query[1] * 1000000.0;
1556 $mysqlTime += $query[1];
1557 if (!isset($byTime[$time])) : $byTime[$time] = array(); endif;
1558 $byTime[$time][] = $query[0]. ' // STACK: ' . $query[2];
1559 endforeach;
1560 krsort($byTime);
1561
1562 foreach ($byTime as $time => $querySet) :
1563 foreach ($querySet as $query) :
1564 print "[".(sprintf('%4.4f', $time/1000.0)) . "ms] $query\n";
1565 endforeach;
1566 endforeach;
1567 echo self::log_prefix()."$wpdb->num_queries queries. $mysqlTime seconds in MySQL. Total of "; timer_stop(1); print " seconds.";
1568 endif;
1569
1570 debug_out_feedwordpress_footer();
1571
1572 // Magic URL should return nothing but a 200 OK header packet
1573 // when successful.
1574 exit;
1575 endif;
1576 } /* FeedWordPress::update_magic_url () */
1577
1578 public static function update_requested () {
1579 return FeedWordPress::param( 'update_feedwordpress' );
1580 } /* FeedWordPress::update_requested() */
1581
1582 public function update_requested_url () {
1583 $ret = null;
1584
1585 $uf = FeedWordPress::update_requested();
1586 if (
1587 ( '*' == $uf )
1588 || ( preg_match( '|^[a-z]+://.*|i', $uf ) )
1589 ) :
1590 $ret = $uf;
1591 endif;
1592
1593 return $ret;
1594 } /* FeedWordPress::update_requested_url() */
1595
1596 public function auto_update () {
1597 if ($this->stale()) :
1598 $this->update();
1599 endif;
1600 } /* FeedWordPress::auto_update () */
1601
1602 public static function find_link ($uri, $field = 'link_rss') {
1603 global $wpdb;
1604
1605 $unslashed = untrailingslashit($uri);
1606 $slashed = trailingslashit($uri);
1607 $link_id = $wpdb->get_var($wpdb->prepare("
1608 SELECT link_id FROM $wpdb->links WHERE $field IN ('%s', '%s')
1609 LIMIT 1", $unslashed, $slashed
1610 ));
1611
1612 return $link_id;
1613 } /* FeedWordPress::find_link () */
1614
1615 /**
1616 * FeedWordPress:syndicate_link(): add or update a feed subscription
1617 *
1618 * Add a new subscription to, or update an existing subscription in,
1619 * FWP's list of subscribed feeds.
1620 *
1621 * Postcondition: If $rss is the URL of a feed not yet on FWP's list of
1622 * subscribed feeds, then a new subscription will be added, using $name
1623 * as its initial title and $uri as its initial homepage URL (normally
1624 * these will be updated with new values taken from the feed, the first
1625 * time the new feed is checked for syndicated content, unless feed
1626 * settings prevent this). If $rss is the URL of a feed that is already
1627 * on FWP's list of subscribed feeds, then that feed will be updated to
1628 * use the title provided in $name and the homepage URL in $uri
1629 *
1630 * @param string $name The human-readable title of the feed (for example, "Rad Geek People's Daily")
1631 * @param string $uri The URI for the human-readable homepage associated with the feed (for example, <http://radgeek.com/>)
1632 * @param string $rss The URI for the feed itself (for example, <http://radgeek.com/feed/>)
1633 *
1634 * @return mixed Returns an int with the numeric ID of the new
1635 * subscription's wp_links record if successful or a WP_Error object
1636 * if wp_insert_link() failed.
1637 *
1638 * @uses FeedWordPress::link_category_id()
1639 * @uses FeedWordPress::find_link()
1640 * @uses is_wp_error()
1641 * @uses wp_insert_link()
1642 *
1643 */
1644 public static function syndicate_link ($name, $uri, $rss) {
1645 // Get the category ID#
1646 $cat_id = FeedWordPress::link_category_id();
1647 if (!is_wp_error($cat_id)) :
1648 $link_category = array($cat_id);
1649 else :
1650 $link_category = array();
1651 endif;
1652
1653 // WordPress gets cranky if there's no homepage URI
1654 if (!is_string($uri) or strlen($uri)<1) : $uri = $rss; endif;
1655
1656 // Check if this feed URL is already being syndicated.
1657 $link_id = wp_insert_link(/*linkdata=*/ array(
1658 "link_id" => FeedWordPress::find_link($rss), // insert if nothing was found; else update
1659 "link_rss" => $rss,
1660 "link_name" => $name,
1661 "link_url" => $uri,
1662 "link_category" => $link_category,
1663 "link_visible" => 'Y', // reactivate if inactivated
1664 ), /*wp_error=*/ true);
1665
1666 return $link_id;
1667 } /* function FeedWordPress::syndicate_link() */
1668
1669 static function syndicated_status ($what, $default) {
1670 $ret = get_option("feedwordpress_syndicated_{$what}_status");
1671 if (!$ret) :
1672 $ret = $default;
1673 endif;
1674 return $ret;
1675 } /* FeedWordPress::syndicated_status() */
1676
1677 public static function on_unfamiliar ($what = 'author') {
1678 switch ($what) :
1679 case 'category' : $suffix = ':category'; break;
1680 case 'post_tag' : $suffix = ':post_tag'; break;
1681 default: $suffix = '';
1682 endswitch;
1683
1684 return get_option('feedwordpress_unfamiliar_'.$what, 'create'.$suffix);
1685 } // function FeedWordPress::on_unfamiliar()
1686
1687 public static function null_email_set () {
1688 $base = get_option('feedwordpress_null_email_set');
1689
1690 if ($base===false) :
1691 $ret = array('noreply@blogger.com'); // default
1692 else :
1693 $ret = array_map('strtolower',
1694 array_map('trim', explode("\n", $base)));
1695 endif;
1696 $ret = apply_filters('syndicated_item_author_null_email_set', $ret);
1697 return $ret;
1698
1699 } /* FeedWordPress::null_email_set () */
1700
1701 public static function is_null_email( $email ) {
1702 $ret = in_array( strtolower( trim( $email ) ), FeedWordPress::null_email_set() );
1703 $ret = apply_filters( 'syndicated_item_author_is_null_email', $ret, $email );
1704 return $ret;
1705 } /* FeedWordPress::is_null_email () */
1706
1707 public static function use_aggregator_source_data () {
1708 $ret = get_option('feedwordpress_use_aggregator_source_data');
1709 return apply_filters('syndicated_post_use_aggregator_source_data', ($ret=='yes'));
1710 } /* FeedWordPress::use_aggregator_source_data () */
1711
1712 /**
1713 * FeedWordPress::munge_permalinks: check whether or not FeedWordPress
1714 * should rewrite permalinks for syndicated items to reflect their
1715 * original location.
1716 *
1717 * @return bool TRUE if FeedWordPress SHOULD rewrite permalinks; FALSE otherwise
1718 */
1719 static function munge_permalinks () {
1720 return (get_option('feedwordpress_munge_permalink', /*default=*/ 'yes') != 'no');
1721 } /* FeedWordPress::munge_permalinks() */
1722
1723 public static function syndicated_links ($args = array()) {
1724 $contributors = FeedWordPress::link_category_id();
1725 if (!is_wp_error($contributors)) :
1726 $links = get_bookmarks(array_merge(
1727 array("category" => $contributors),
1728 $args
1729 ));
1730 else :
1731 $links = array();
1732 endif;
1733
1734 return $links;
1735 } /* FeedWordPress::syndicated_links() */
1736
1737 public static function link_category_id () {
1738 global $wpdb, $wp_db_version;
1739
1740 $cat_id = get_option('feedwordpress_cat_id');
1741
1742 // If we don't yet have the category ID stored, search by name
1743 if (!$cat_id) :
1744 $cat_id = FeedWordPressCompatibility::link_category_id(DEFAULT_SYNDICATION_CATEGORY);
1745
1746 if ($cat_id) :
1747 // We found it; let's stamp it.
1748 update_option('feedwordpress_cat_id', $cat_id);
1749 endif;
1750
1751 // If we *do* have the category ID stored, verify that it exists
1752 else :
1753 $cat_id = FeedWordPressCompatibility::link_category_id((int) $cat_id, 'cat_id');
1754 endif;
1755
1756 // If we could not find an appropriate link category,
1757 // make a new one for ourselves.
1758 if (!$cat_id) :
1759 $cat_id = FeedWordPressCompatibility::insert_link_category(DEFAULT_SYNDICATION_CATEGORY);
1760 if (!is_wp_error($cat_id)) :
1761 // Stamp it
1762 update_option('feedwordpress_cat_id', $cat_id);
1763 endif;
1764 endif;
1765
1766 return $cat_id;
1767 } /* FeedWordPress::link_category_id() */
1768
1769 # Upgrades and maintenance...
1770 static function needs_upgrade () {
1771
1772 global $wpdb;
1773 $fwp_db_version = get_option('feedwordpress_version', null);
1774 $ret = false; // innocent until proven guilty
1775 if (is_null($fwp_db_version) or ($fwp_db_version < FEEDWORDPRESS_VERSION)) :
1776
1777 // This is an older version or a fresh install. Does it require a database
1778 // upgrade or database initialization?
1779
1780 if (is_null($fwp_db_version)) :
1781
1782 // Fresh install; brand it as ours. Or possibly a version of FWP
1783 // from before 0.96. But I'm no longer supporting upgrade paths
1784 // for versions from the previous decade. Sorry.
1785 update_option('feedwordpress_version', FEEDWORDPRESS_VERSION);
1786
1787 elseif ($fwp_db_version < 2010.0814) :
1788
1789 // Change in terminology.
1790 if (get_option('feedwordpress_unfamiliar_category', 'create')=='default') :
1791 update_option('feedwordpress_unfamiliar_category', 'null');
1792 endif;
1793 foreach (FeedWordPress::syndicated_links() as $link) :
1794 $sub = new SyndicatedLink($link);
1795
1796 $remap_uf = array(
1797 'default' => 'null',
1798 'filter' => 'null',
1799 'create' => 'create:category',
1800 'tag' => 'create:post_tag'
1801 );
1802 if (isset($sub->settings['unfamiliar category'])) :
1803 if ($sub->settings['unfamiliar category']=='filter') :
1804 $sub->settings['match/filter'] = array('category');
1805 endif;
1806 foreach ($remap_uf as $from => $to) :
1807 if ($sub->settings['unfamiliar category']==$from) :
1808 $sub->settings['unfamiliar category'] = $to;
1809 endif;
1810 endforeach;
1811 endif;
1812
1813 if (isset($sub->settings['add global categories'])) :
1814 $sub->settings['add/category'] = $sub->settings['add global categories'];
1815 unset($sub->settings['add global categories']);
1816 endif;
1817
1818 $sub->save_settings(/*reload=*/ true);
1819 endforeach;
1820 update_option('feedwordpress_version', FEEDWORDPRESS_VERSION);
1821
1822 else :
1823
1824 // No upgrade needed. Just brand it with the new version.
1825 update_option('feedwordpress_version', FEEDWORDPRESS_VERSION);
1826
1827 endif;
1828
1829 endif;
1830 return $ret;
1831 } /* FeedWordPress::needs_upgrade () */
1832
1833 static function upgrade_database ($from = null) {
1834 global $wpdb;
1835
1836 if (is_null($from) or $from <= 0.96) : $from = 0.96; endif;
1837
1838 switch ($from) :
1839 case 0.96:
1840 // Dropping legacy upgrade code. If anyone is still
1841 // using 0.96 and just now decided to upgrade, well, I'm
1842 // sorry about that. You'll just have to cope with a few
1843 // duplicate posts.
1844
1845 // Mark the upgrade as successful.
1846 update_option('feedwordpress_version', FEEDWORDPRESS_VERSION);
1847 endswitch;
1848 echo "<p>Upgrade complete. FeedWordPress is now ready to use again.</p>";
1849 } /* FeedWordPress::upgrade_database() */
1850
1851 public static function has_guid_index () {
1852 global $wpdb;
1853
1854 $found = false; // Guilty until proven innocent.
1855
1856 $results = $wpdb->get_results("
1857 SHOW INDEXES FROM {$wpdb->posts}
1858 ");
1859 if ($results) :
1860 foreach ($results as $index) :
1861 if (isset($index->Column_name)
1862 and ('guid' == $index->Column_name)) :
1863 $found = true;
1864 endif;
1865 endforeach;
1866 endif;
1867 return $found;
1868 } /* FeedWordPress::has_guid_index () */
1869
1870 public static function create_guid_index () {
1871 global $wpdb;
1872
1873 $wpdb->query("
1874 CREATE INDEX {$wpdb->posts}_guid_idx ON {$wpdb->posts}(guid)
1875 ");
1876 } /* FeedWordPress::create_guid_index () */
1877
1878 public static function remove_guid_index () {
1879 global $wpdb;
1880
1881 $wpdb->query("
1882 DROP INDEX {$wpdb->posts}_guid_idx ON {$wpdb->posts}
1883 ");
1884 }
1885
1886 static function fetch_timeout () {
1887 return apply_filters(
1888 'feedwordpress_fetch_timeout',
1889 intval(get_option('feedwordpress_fetch_timeout', FEEDWORDPRESS_FETCH_TIMEOUT_DEFAULT))
1890 );
1891 }
1892
1893 static function fetch ($url, $params = array()) {
1894 if (is_wp_error($url)) :
1895 // Let's bounce.
1896 return $url;
1897 endif;
1898
1899 $force_feed = true; // Default
1900
1901 // Allow user to change default feed-fetch timeout with a global setting.
1902 // Props Erigami Scholey-Fuller <http://www.piepalace.ca/blog/2010/11/feedwordpress-broke-my-heart.html>
1903 $timeout = FeedWordPress::fetch_timeout();
1904
1905 if (!is_array($params)) :
1906 $force_feed = $params;
1907 else : // Parameter array
1908 $args = wp_parse_args(array(
1909 'force_feed' => $force_feed,
1910 'timeout' => $timeout
1911 ), $params);
1912
1913 extract($args);
1914 endif;
1915 $timeout = intval($timeout);
1916
1917 $pie_class = apply_filters('feedwordpress_simplepie_class', 'FeedWordPie');
1918 $cache_class = apply_filters('feedwordpress_cache_class', 'FeedWordPie_Cache');
1919 $file_class = apply_filters('feedwordpress_file_class', 'FeedWordPie_File');
1920 $parser_class = apply_filters('feedwordpress_parser_class', 'FeedWordPie_Parser');
1921 $item_class = apply_filters('feedwordpress_item_class', 'FeedWordPie_Item');
1922 $sniffer_class = apply_filters('feedwordpress_sniffer_class', 'FeedWordPie_Content_Type_Sniffer');
1923
1924 $feed = new $pie_class;
1925 $feed->set_feed_url($url);
1926 $feed->registry->register('Cache', $cache_class);
1927 $feed->set_timeout($timeout);
1928
1929 $feed->set_content_type_sniffer_class($sniffer_class);
1930 $feed->set_file_class($file_class);
1931 $feed->set_parser_class($parser_class);
1932 $feed->set_item_class($item_class);
1933 $feed->force_feed($force_feed);
1934 $feed->set_cache_duration(FeedWordPress::cache_duration($params));
1935 $feed->init();
1936 $feed->handle_content_type();
1937
1938 if ($feed->error()) :
1939 $ret = new WP_Error('simplepie-error', $feed->error());
1940 else :
1941 $ret = $feed;
1942 endif;
1943 return $ret;
1944 } /* FeedWordPress::fetch () */
1945
1946 public function clear_cache () {
1947 global $wpdb;
1948
1949 // Just in case, clear out any old MagpieRSS cache records.
1950 $magpies = $wpdb->query("
1951 DELETE FROM {$wpdb->options}
1952 WHERE option_name LIKE 'rss_%' AND LENGTH(option_name) > 32
1953 ");
1954
1955 // The WordPress SimplePie module stores its cached feeds as
1956 // transient records in the options table. The data itself is
1957 // stored in `_transient_feed_{md5 of url}` and the last-modified
1958 // timestamp in `_transient_feed_mod_{md5 of url}`. Timeouts for
1959 // these records are stored in `_transient_timeout_feed_{md5}`.
1960 // Since the md5 is always 32 characters in length, the
1961 // option_name is always over 32 characters.
1962 $simplepies = $wpdb->query("
1963 DELETE FROM {$wpdb->options}
1964 WHERE option_name LIKE '_transient%_feed_%' AND LENGTH(option_name) > 32
1965 ");
1966 $simplepies = (int) ($simplepies / 4); // Each transient has 4 rows: the data, the modified timestamp; and the timeouts for each
1967
1968 return ($magpies + $simplepies);
1969 } /* FeedWordPress::clear_cache () */
1970
1971 static public function cache_duration ($params = array()) {
1972 $params = wp_parse_args($params, array(
1973 "cache" => true,
1974 ));
1975
1976 $duration = null;
1977 if (!$params['cache']) :
1978 $duration = 0;
1979 elseif (defined('FEEDWORDPRESS_CACHE_AGE')) :
1980 $duration = FEEDWORDPRESS_CACHE_AGE;
1981 endif;
1982 return $duration;
1983 }
1984
1985 static public function cache_lifetime ($duration) {
1986 // Check for explicit setting of a lifetime duration
1987 if (defined('FEEDWORDPRESS_CACHE_LIFETIME')) :
1988 $duration = FEEDWORDPRESS_CACHE_LIFETIME;
1989
1990 // Fall back to the cache freshness duration
1991 elseif (defined('FEEDWORDPRESS_CACHE_AGE')) :
1992 $duration = FEEDWORDPRESS_CACHE_AGE;
1993 endif;
1994
1995 // Fall back to WordPress default
1996 return $duration;
1997 } /* FeedWordPress::cache_lifetime () */
1998
1999 # Utility functions for handling text settings
2000 static function get_field( $f, $setting = null ) {
2001
2002 $ret = $f;
2003 if ( ! is_null( $setting ) ) :
2004 $ret = null;
2005 if ( array_key_exists( $setting, $f ) ) :
2006 $ret = $f[ $setting ];
2007 endif;
2008 endif;
2009 return $ret;
2010
2011 } /* FeedWordPress::get_field () */
2012
2013 static function negative ($f, $setting = null) {
2014 $nego = array ('n', 'no', 'f', 'false');
2015 $q = self::get_field( $f, $setting );
2016 return in_array( strtolower( trim( $q ) ), $nego );
2017 } /* FeedWordPress::negative () */
2018
2019 static function affirmative ($f, $setting = null) {
2020 $affirmo = array ('y', 'yes', 't', 'true', 1);
2021 $q = self::get_field( $f, $setting );
2022 return in_array( strtolower( trim( $q ) ), $affirmo );
2023 } /* FeedWordPress::affirmative () */
2024
2025 # Internal debugging functions
2026
2027 static function diagnostic ($level, $out, $persist = null, $since = null, $mostRecent = null) {
2028 global $feedwordpress_admin_footer;
2029
2030 $output = get_option('feedwordpress_diagnostics_output', array());
2031 $dlog = get_option('feedwordpress_diagnostics_log', array());
2032
2033 $diagnostic_nesting = count( explode( ":", $level ) );
2034
2035 if (FeedWordPressDiagnostic::is_on($level)) :
2036 foreach ($output as $method) :
2037 switch ($method) :
2038 case 'echo' :
2039 if (!self::update_requested()) :
2040 echo "<div><pre><strong>Diag".str_repeat('====', $diagnostic_nesting-1).'|</strong> '.$out."</pre></div>\n";
2041 endif;
2042 break;
2043 case 'echo_in_cronjob' :
2044 if (self::update_requested()) :
2045 echo self::log_prefix() . ' ' . esc_html( $out ) . "\n";
2046 endif;
2047 break;
2048 case 'admin_footer' :
2049 $feedwordpress_admin_footer[] = $out;
2050 break;
2051 case 'error_log' :
2052 error_log(self::log_prefix() . ' ' . $out);
2053 break;
2054 case 'email' :
2055
2056 if (is_null($persist)) :
2057 $sect = 'occurrent';
2058 $hook = (isset($dlog['mesg'][$sect]) ? count($dlog['mesg'][$sect]) : 0);
2059 $line = array("Time" => time(), "Message" => $out);
2060 else :
2061 $sect = 'persistent';
2062 $hook = md5($level."\n".$persist);
2063 $line = array("Since" => $since, "Message" => $out, "Most Recent" => $mostRecent);
2064 endif;
2065
2066 if (!isset($dlog['mesg'])) : $dlog['mesg'] = array(); endif;
2067 if (!isset($dlog['mesg'][$sect])) : $dlog['mesg'][$sect] = array(); endif;
2068
2069 $dlog['mesg'][$sect][$hook] = $line;
2070 endswitch;
2071 endforeach;
2072 endif;
2073
2074 update_option('feedwordpress_diagnostics_log', $dlog);
2075 } /* FeedWordPress::diagnostic () */
2076
2077 public function email_diagnostics_override () {
2078 return ( $this->has_secret() and ! ! FeedWordPress::param( 'feedwordpress_email_diagnostics' ) );
2079 } /* FeedWordPress::email_diagnostics_override () */
2080
2081 public function has_emailed_diagnostics ($dlog) {
2082 $ret = false;
2083 if ($this->email_diagnostics_override()
2084 or (isset($dlog['schedule']) and isset($dlog['schedule']['last']))) :
2085 $ret = true;
2086 endif;
2087 return $ret;
2088 } /* FeedWordPress::has_emailed_diagnostics () */
2089
2090 public function ready_to_email_diagnostics ($dlog) {
2091 $ret = false;
2092 if ($this->email_diagnostics_override()
2093 or (time() > ($dlog['schedule']['last'] + $dlog['schedule']['freq']))) :
2094 $ret = true;
2095 endif;
2096 return $ret;
2097 } /* FeedWordPress::ready_to_email_diagnostics () */
2098
2099 public function email_diagnostic_log ($params = array()) {
2100 $params = wp_parse_args($params, array(
2101 "force" => false,
2102 ));
2103
2104 $dlog = get_option('feedwordpress_diagnostics_log', array());
2105
2106 if ($this->has_emailed_diagnostics($dlog)) :
2107 if ($this->ready_to_email_diagnostics($dlog)) :
2108 // No news is good news; only send if
2109 // there are some messages to send.
2110 $body = null;
2111 if (!isset($dlog['mesg'])) : $dlog['mesg'] = array(); endif;
2112
2113 foreach ($dlog['mesg'] as $sect => $mesgs) :
2114 if (count($mesgs) > 0) :
2115 if (is_null($body)) : $body = ''; endif;
2116
2117 $paradigm = reset($mesgs);
2118 $body .= "<h2>".ucfirst($sect)." issues</h2>\n"
2119 ."<table>\n"
2120 ."<thead><tr>\n";
2121 foreach ($paradigm as $col => $value) :
2122 $body .= '<th scope="col">'.$col."</th>\n";
2123 endforeach;
2124 $body .= "</tr></thead>\n"
2125 ."<tbody>\n";
2126
2127 foreach ($mesgs as $line) :
2128 $body .= "<tr>\n";
2129 foreach ($line as $col => $cell) :
2130 if (is_numeric($cell)) :
2131 $cell = date('j-M-y, h:i a', $cell);
2132 endif;
2133 $class = strtolower(preg_replace('/\s+/', '-', $col));
2134 $body .= "<td class=\"$class\">${cell}</td>";
2135 endforeach;
2136 $body .= "</tr>\n";
2137 endforeach;
2138
2139 $body .= "</tbody>\n</table>\n\n";
2140 endif;
2141 endforeach;
2142
2143 $body = apply_filters('feedwordpress_diagnostic_email_body', $body, $dlog);
2144 if (!is_null($body)) :
2145 $home = feedwordpress_display_url(get_bloginfo('url'));
2146 $subj = apply_filters('feedwordpress_diagnostic_email_subject', $home . " syndication issues", $dlog);
2147 $agent = 'FeedWordPress '.FEEDWORDPRESS_VERSION;
2148 $body = <<<EOMAIL
2149 <html>
2150 <head>
2151 <title>$subj</title>
2152 <style type="text/css">
2153 body { background-color: white; color: black; }
2154 table { width: 100%; border: 1px solid black; }
2155 table thead tr th { background-color: #ff7700; color: white; border-bottom: 1px solid black; }
2156 table thead tr { border-bottom: 1px solid black; }
2157 table tr { vertical-align: top; }
2158 table .since { width: 20%; }
2159 table .time { width: 20%; }
2160 table .most-recently { width: 20%; }
2161 table .message { width: auto; }
2162 </style>
2163 </head>
2164 <body>
2165 <h1>Syndication Issues encountered by $agent on $home</h1>
2166 $body
2167 </body>
2168 </html>
2169
2170 EOMAIL;
2171
2172 $ded = get_option('feedwordpress_diagnostics_email_destination', 'admins');
2173
2174 // e-mail address
2175 if (preg_match('/^mailto:(.*)$/', $ded, $ref)) :
2176 $recipients = array($ref[1]);
2177
2178 // userid
2179 elseif (preg_match('/^user:(.*)$/', $ded, $ref)) :
2180 $userdata = get_userdata((int) $ref[1]);
2181 $recipients = array($userdata->user_email);
2182
2183 // admins
2184 else :
2185 $recipients = FeedWordPressDiagnostic::admin_emails();
2186 endif;
2187
2188 $mesgId = 'feedwordpress+'.time().'@'.$home;
2189 $parentId = get_option('feedwordpress_diagnostics_email_root_message_id', null);
2190
2191 $head = array("Message-ID: <$mesgId>");
2192 if (!is_null($parentId)) :
2193 // We've already sent off a diagnostic message in the past. Let's do some
2194 // magic to help with threading, in the hopes that all diagnostic messages
2195 // get threaded together.
2196 $head[] = "References: <$parentId>";
2197 $head[] = "In-Reply-To: <$parentId>";
2198 $subj = "Re: ".$subj;
2199 else :
2200 // This is the first of its kind. Let's mark it as such.
2201 update_option('feedwordpress_diagnostics_email_root_message_id', $mesgId);
2202 endif;
2203 $head = apply_filters('feedwordpress_diagnostic_email_headers', $head);
2204
2205 foreach ($recipients as $email) :
2206 add_filter('wp_mail_content_type', array('FeedWordPress', 'allow_html_mail'));
2207 wp_mail($email, $subj, $body, $head);
2208 remove_filter('wp_mail_content_type', array('FeedWordPress', 'allow_html_mail'));
2209 endforeach;
2210 endif;
2211
2212 // Clear the logs
2213 $dlog['mesg']['persistent'] = array();
2214 $dlog['mesg']['occurrent'] = array();
2215
2216 // Set schedule for next update
2217 $dlog['schedule']['last'] = time();
2218 endif;
2219 else :
2220 $dlog['schedule'] = array(
2221 'freq' => 24 /*hr*/ * 60 /*min*/ * 60 /*s*/,
2222 'last' => time(),
2223 );
2224 endif;
2225
2226 update_option('feedwordpress_diagnostics_log', $dlog);
2227 } /* FeedWordPress::email_diagnostic_log () */
2228
2229 static function allow_html_mail () {
2230 return 'text/html';
2231 } /* FeedWordPress::allow_html_mail () */
2232
2233 static function admin_footer () {
2234 global $feedwordpress_admin_footer;
2235 foreach ($feedwordpress_admin_footer as $line) :
2236 echo '<div><pre>' . esc_html( $line ) . '</pre></div>';
2237 endforeach;
2238 } /* FeedWordPress::admin_footer () */
2239
2240 static function log_prefix ($date = false) {
2241 $home = get_bloginfo('url');
2242 $prefix = '['.feedwordpress_display_url($home).'] [feedwordpress] ';
2243 if ($date) :
2244 $prefix = "[".date('Y-m-d H:i:s')."]".$prefix;
2245 endif;
2246 return $prefix;
2247 } /* FeedWordPress::log_prefix () */
2248
2249 static function menu_cap ($sub = false) {
2250 if ($sub) :
2251 $cap = apply_filters('feedwordpress_menu_settings_capacity', 'manage_options');
2252 else :
2253 $cap = apply_filters('feedwordpress_menu_main_capacity', 'manage_links');
2254 endif;
2255 return $cap;
2256 } /* FeedWordPress::menu_cap () */
2257
2258 public function plugin_dir_path ($path = '') {
2259 $dir = plugin_dir_path( __FILE__ );
2260 $file_path = "${dir}${path}";
2261 return apply_filters( "feedwordpress_plugin_dir_path", $file_path );
2262 } /* FeedWordPress::plugin_dir_path () */
2263
2264 public function plugin_dir_url ($path = '') {
2265 $url_path = plugins_url( $path, __FILE__ );
2266 return apply_filters( "feedwordpress_plugin_dir_url", $url_path );
2267 } /* FeedWordPRess::plugin_dir_url () */
2268
2269 static function path ($filename = '') {
2270 global $fwp_path;
2271
2272 $path = $fwp_path;
2273 if (strlen($filename) > 0) :
2274 $path .= '/'.$filename;
2275 endif;
2276
2277 return $path;
2278 } /* FeedWordPress::path () */
2279
2280 // -- DEPRCATED UTILITY FUNCTIONS -------------------------------------
2281 // These are superceded by later code re-organization, (for example
2282 // MyPHP::param/post/get/request, or FeedWordPressDiagnostic methods),
2283 // but for the last several versions have been kept here for backward
2284 // compatibility with add-ons, older code, etc. Maybe someday they
2285 // will go away.
2286 // -------------------------------------------------------------------
2287 static function param( $key, $type = 'REQUEST', $default = null, $sanitizer = null ) {
2288 return self::sanitized_parameter( MyPHP::param( $key, $default, $type ), $sanitizer );
2289 } /* FeedWordPress::param () */
2290
2291 static function post( $key, $default = null, $sanitizer = null ) {
2292 return self::sanitized_parameter( MyPHP::post( $key, $default, $type ), $sanitizer );
2293 } /* FeedWordPress::post () */
2294
2295 static function shallow_sanitize( $item, $sanitizer = null ) {
2296 if ( $sanitizer == 'raw' ) :
2297 $value = $item;
2298 elseif ( $sanitizer == 'textarea' ) :
2299 $value = sanitize_textarea_field( $item );
2300 else :
2301 $value = sanitize_text_field( $item );
2302 endif;
2303 return $value;
2304 }
2305
2306 static function sanitized_parameter( $value, $sanitizer = null ) {
2307 if ( $sanitizer === 'raw' || is_scalar( $value ) ) :
2308 $ret = self::shallow_sanitize( $value, $sanitizer );
2309 elseif ( is_array( $value ) ) :
2310 $ret = array();
2311 foreach ( $value as $key => $item ) :
2312 $ret[ $key ] = self::sanitized_parameter( $item, $sanitizer );
2313 endforeach;
2314 else :
2315 $ret = null; // "Sanitized" objects or resources currently get nothing.
2316 endif;
2317 return $ret;
2318 }
2319
2320 static function val ($v, $no_newlines = false) {
2321 return MyPHP::val($v, $no_newlines);
2322 } /* FeedWordPress::val () */
2323
2324 static function critical_bug ($varname, $var, $line, $file = null) {
2325 FeedWordPressDiagnostic::critical_bug($varname, $var, $line, $file);
2326 } /* FeedWordPress::critical_bug () */
2327
2328 static function noncritical_bug ($varname, $var, $line, $file = null) {
2329 FeedWordPressDiagnostic::noncritical_bug($varname, $var, $line, $file);
2330 } /* FeedWordPress::noncritical_bug () */
2331
2332 static function diagnostic_on ($level) {
2333 return FeedWordPressDiagnostic::is_on($level);
2334 } /* FeedWordPress::diagnostic_on () */
2335
2336 } /* class FeedWordPress */
2337
2338 $feedwordpress_admin_footer = array();
2339
2340 // take your best guess at the realname and e-mail, given a string
2341 define( 'FWP_REGEX_EMAIL_ADDY', '([^@"(<\s]+@[^"@(<\s]+\.[^"@(<\s]+)' );
2342 define( 'FWP_REGEX_EMAIL_NAME', '("([^"]*)"|([^"<(]+\S))' );
2343 define( 'FWP_REGEX_EMAIL_POSTFIX_NAME', '/^\s*' . FWP_REGEX_EMAIL_ADDY . '\s+\(' . FWP_REGEX_EMAIL_NAME . '\)\s*$/' );
2344 define( 'FWP_REGEX_EMAIL_PREFIX_NAME', '/^\s*' . FWP_REGEX_EMAIL_NAME . '\s*<' . FWP_REGEX_EMAIL_ADDY . '>\s*$/' );
2345 define( 'FWP_REGEX_EMAIL_JUST_ADDY', '/^\s*' . FWP_REGEX_EMAIL_ADDY . '\s*$/' );
2346 define( 'FWP_REGEX_EMAIL_JUST_NAME', '/^\s*' . FWP_REGEX_EMAIL_NAME . '\s*$/' );
2347
2348 function parse_email_with_realname( $email ) {
2349 if ( preg_match( FWP_REGEX_EMAIL_POSTFIX_NAME, $email, $matches ) ) :
2350 ( $ret['name'] = $matches[3] ) || ( $ret['name'] = $matches[2] );
2351 $ret['email'] = $matches[1];
2352 elseif ( preg_match( FWP_REGEX_EMAIL_PREFIX_NAME, $email, $matches ) ) :
2353 ( $ret['name'] = $matches[2] ) || ( $ret['name'] = $matches[3] );
2354 $ret['email'] = $matches[4];
2355 elseif ( preg_match( FWP_REGEX_EMAIL_JUST_ADDY, $email, $matches ) ) :
2356 $ret['name'] = null;
2357 $ret['email'] = $matches[1];
2358 elseif ( preg_match( FWP_REGEX_EMAIL_JUST_NAME, $email, $matches ) ) :
2359 $ret['email'] = null;
2360 ( $ret['name'] = $matches[2] ) || ( $ret['name'] = $matches[3] );
2361 else :
2362 $ret['name'] = null;
2363 $ret['email'] = null;
2364 endif;
2365 return $ret;
2366 }
2367