PluginProbe
FeedWordPress / 2020.0818
FeedWordPress v2020.0818
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 / syndicatedlink.class.php

syndicatedlink.class.php in FeedWordPress 2020.0818, at syndicatedlink.class.php

1,243 lines 38.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 # class SyndicatedLink: represents a syndication feed stored within the
3 # WordPress database
4 #
5 # To keep things compact and editable from within WordPress, we use all the
6 # links under a particular category in the WordPress "Blogroll" for the list of
7 # feeds to syndicate. "Contributors" is the category used by default; you can
8 # configure that under Options --> Syndication.
9 #
10 # Fields used are:
11 #
12 # * link_rss: the URI of the Atom/RSS feed to syndicate
13 #
14 # * link_notes: user-configurable options, with keys and values
15 # like so:
16 #
17 # key: value
18 # cats: computers\nweb
19 # feed/key: value
20 #
21 # Keys that start with "feed/" are gleaned from the data supplied
22 # by the feed itself, and will be overwritten with each update.
23 #
24 # Values have linebreak characters escaped with C-style
25 # backslashes (so, for example, a newline becomes "\n").
26 #
27 # The value of `cats` is used as a newline-separated list of
28 # default categories for any post coming from a particular feed.
29 # (In the example above, any posts from this feed will be placed
30 # in the "computers" and "web" categories--*in addition to* any
31 # categories that may already be applied to the posts.)
32 #
33 # Values of keys in link_notes are accessible from templates using
34 # the function `get_feed_meta($key)` if this plugin is activated.
35
36 require_once(dirname(__FILE__).'/magpiefromsimplepie.class.php');
37 require_once(dirname(__FILE__).'/feedwordpressparsedpostmeta.class.php');
38
39 class SyndicatedLink {
40 var $id = null;
41 var $link = null;
42 var $settings = array ();
43 public $simplepie = null;
44 var $magpie = null;
45
46 public function __construct( $link ) {
47 global $wpdb;
48
49 if (is_object($link)) :
50 $this->link = $link;
51 $this->id = $link->link_id;
52 else :
53 $this->id = $link;
54 $this->link = get_bookmark($link);
55 endif;
56
57 if (is_object($this->link)) :
58 if (strlen($this->link->link_rss) > 0) :
59 $this->get_settings_from_notes();
60 endif;
61 endif;
62
63 add_filter('feedwordpress_update_complete', array($this, 'process_retirements'), 1000, 1);
64 } /* SyndicatedLink::__construct () */
65
66 public function found () {
67 return is_object($this->link) and !is_wp_error($this->link);
68 } /* SyndicatedLink::found () */
69
70 public function id () {
71 return (is_object($this->link) ? $this->link->link_id : NULL);
72 }
73
74 public function stale () {
75 global $feedwordpress;
76
77 $stale = true;
78 if ($this->setting('update/hold')=='ping') :
79 $stale = false; // don't update on any timed updates; pings only
80 elseif ($this->setting('update/hold')=='next') :
81 $stale = true; // update on the next timed update
82 elseif ( !$this->setting('update/last') ) :
83 $stale = true; // initial update
84 elseif ($feedwordpress->force_update_all()) :
85 $stale = true; // forced general updating
86 else :
87 $after = (
88 (int) $this->setting('update/last')
89 + (int) $this->setting('update/fudge')
90 + ((int) $this->setting('update/ttl') * 60)
91 );
92 $stale = (time() >= $after);
93 endif;
94 return $stale;
95 } /* SyndicatedLink::stale () */
96
97 public function fetch () {
98 $timeout = $this->setting('fetch timeout', 'feedwordpress_fetch_timeout', FEEDWORDPRESS_FETCH_TIMEOUT_DEFAULT);
99
100 $this->simplepie = apply_filters(
101 'syndicated_feed',
102 FeedWordPress::fetch($this, array('timeout' => $timeout)),
103 $this
104 );
105
106 // Filter compatibility mode
107 if (is_wp_error($this->simplepie)) :
108 $this->magpie = $this->simplepie;
109 else :
110 $this->magpie = new MagpieFromSimplePie($this->simplepie, NULL);
111 endif;
112 } /* SyndicatedLink::fetch () */
113
114 public function live_posts () {
115 if (!is_object($this->simplepie)) :
116 $this->fetch();
117 endif;
118
119 if (is_object($this->simplepie) and method_exists($this->simplepie, 'get_items')) :
120 $ret = apply_filters(
121 'syndicated_feed_items',
122 $this->simplepie->get_items(),
123 $this
124 );
125 else :
126 $ret = $this->simplepie;
127 endif;
128 return $ret;
129 } /* SyndicatedLink::live_posts () */
130
131 protected function pause_updates () {
132 return ('yes'==$this->setting("update/pause", "update_pause", 'no'));
133 } /* SyndicatedLink::pause_updates () */
134
135 public function poll ($crash_ts = NULL) {
136 global $wpdb;
137
138 $url = $this->uri(array('add_params' => true, 'fetch' => true));
139 FeedWordPress::diagnostic('updated_feeds', 'Polling feed ['.$url.']');
140
141 $this->fetch();
142
143 $new_count = NULL;
144
145 $resume = ('yes'==$this->setting('update/unfinished'));
146 if ($resume) :
147 // pick up where we left off
148 $processed = array_map('trim', explode("\n", $this->setting('update/processed')));
149 else :
150 // begin at the beginning
151 $processed = array();
152 endif;
153
154 if (is_wp_error($this->simplepie)) :
155 $new_count = $this->simplepie;
156 // Error; establish an error setting.
157 $theError = array();
158 $theError['ts'] = time();
159 $theError['since'] = time();
160 $theError['object'] = $this->simplepie;
161
162 $oldError = $this->setting('update/error', NULL, NULL);
163 if (is_string($oldError)) :
164 $oldError = unserialize($oldError);
165 endif;
166
167 if (!is_null($oldError)) :
168 // Copy over the in-error-since timestamp
169 $theError['since'] = $oldError['since'];
170
171 // If this is a repeat error, then we should
172 // take a step back before we try to fetch it
173 // again.
174 $this->update_setting('update/last', time(), NULL);
175 $ttl = $this->automatic_ttl();
176 $ttl = apply_filters('syndicated_feed_ttl', $ttl, $this);
177 $ttl = apply_filters('syndicated_feed_ttl_from_error', $ttl, $this);
178 $this->update_setting('update/ttl', $ttl);
179 $this->update_setting('update/timed', 'automatically');
180 endif;
181
182 do_action('syndicated_feed_error', $theError, $oldError, $this);
183
184 $this->update_setting('update/error', serialize($theError));
185 $this->save_settings(/*reload=*/ true);
186
187 elseif (is_object($this->simplepie)) :
188
189 // Success; clear out error setting, if any.
190 $this->update_setting('update/error', NULL);
191
192 $new_count = array('new' => 0, 'updated' => 0, 'stored' => 0);
193
194 # -- Update Link metadata live from feed
195 $channel = $this->magpie->channel;
196
197 if (!isset($channel['id'])) :
198 $channel['id'] = $this->link->link_rss;
199 endif;
200
201 $update = array();
202 if (!$this->hardcode('url') and isset($channel['link'])) :
203 $update[] = "link_url = '".esc_sql($channel['link'])."'";
204 endif;
205
206 if (!$this->hardcode('name') and isset($channel['title'])) :
207 $update[] = "link_name = '".esc_sql($channel['title'])."'";
208 endif;
209
210 if (!$this->hardcode('description')) :
211 if (isset($channel['tagline'])) :
212 $update[] = "link_description = '".esc_sql($channel['tagline'])."'";
213 elseif (isset($channel['description'])) :
214 $update[] = "link_description = '".esc_sql($channel['description'])."'";
215 endif;
216 endif;
217
218 $this->update_setting('link/feed_type', $this->simplepie->get_type());
219
220 $this->merge_settings($channel, 'feed/');
221
222 $this->update_setting('update/last', time());
223 $this->do_update_ttl();
224
225 if (!$this->setting('update/hold') != 'ping') :
226 $this->update_setting('update/hold', 'scheduled');
227 endif;
228
229 $this->update_setting('update/unfinished', 'yes');
230
231 $update[] = "link_notes = '".esc_sql($this->settings_to_notes())."'";
232
233 $update_set = implode(',', $update);
234
235 // Update the properties of the link from the feed information
236 $result = $wpdb->query("
237 UPDATE $wpdb->links
238 SET $update_set
239 WHERE link_id='$this->id'
240 ");
241 do_action('update_syndicated_feed', $this->id, $this);
242
243 # -- Add new posts from feed and update any updated posts
244 $crashed = false;
245
246 $posts = $this->live_posts();
247
248 $this->magpie->originals = $posts;
249
250 // If this is a complete feed, rather than an incremental feed, we
251 // need to prepare to mark everything for presumptive retirement.
252 if ($this->is_non_incremental()) :
253 $q = new WP_Query(array(
254 'fields' => '_synfrom',
255 'post_status__not' => 'fwpretired',
256 'ignore_sticky_posts' => true,
257 'meta_key' => 'syndication_feed_id',
258 'meta_value' => $this->id,
259 ));
260 foreach ($q->posts as $p) :
261 update_post_meta($p->ID, '_feedwordpress_retire_me_'.$this->id, '1');
262 endforeach;
263 endif;
264
265 if (is_array($posts)) :
266 foreach ($posts as $key => $item) :
267 if (!$this->pause_updates()) :
268 $post = new SyndicatedPost($item, $this);
269
270 if (!$resume or !in_array(trim($post->guid()), $processed)) :
271 $processed[] = $post->guid();
272 if (!$post->filtered()) :
273 $new = $post->store();
274 if ( $new !== false ) $new_count[$new]++;
275 endif;
276
277 if (!is_null($crash_ts) and (time() > $crash_ts)) :
278 $crashed = true;
279 break;
280 endif;
281 endif;
282
283 unset($post);
284 endif;
285 endforeach;
286 endif;
287
288 if ('yes'==$this->setting('tombstones', 'tombstones', 'yes')) :
289 // Check for use of Atom tombstones. Spec:
290 // <http://tools.ietf.org/html/draft-snell-atompub-tombstones-18>
291 $tombstones = $this->simplepie->get_feed_tags('http://purl.org/atompub/tombstones/1.0', 'deleted-entry');
292 if (!is_null($tombstones) && count($tombstones) > 0) :
293 foreach ($tombstones as $tombstone) :
294 $ref = NULL;
295 foreach (array('', 'http://purl.org/atompub/tombstones/1.0') as $ns) :
296 if (isset($tombstone['attribs'][$ns])
297 and isset($tombstone['attribs'][$ns]['ref'])) :
298 $ref = $tombstone['attribs'][$ns]['ref'];
299 endif;
300 endforeach;
301
302 $q = new WP_Query(array(
303 'ignore_sticky_posts' => true,
304 'guid' => $ref,
305 'meta_key' => 'syndication_feed_id',
306 'meta_value' => $this->id, // Only allow a feed to tombstone its own entries.
307 ));
308
309 foreach ($q->posts as $p) :
310 $old_status = $p->post_status;
311 FeedWordPress::diagnostic('syndicated_posts', 'Retiring existing post # '.$p->ID.' "'.$p->post_title.'" due to Atom tombstone element in feed.');
312 set_post_field('post_status', 'fwpretired', $p->ID);
313 wp_transition_post_status('fwpretired', $old_status, $p);
314 endforeach;
315
316 endforeach;
317 endif;
318 endif;
319
320 $suffix = ($crashed ? 'crashed' : 'completed');
321 do_action('update_syndicated_feed_items', $this->id, $this);
322 do_action("update_syndicated_feed_items_${suffix}", $this->id, $this);
323
324 $this->update_setting('update/processed', $processed);
325 if (!$crashed) :
326 $this->update_setting('update/unfinished', 'no');
327 endif;
328 $this->update_setting('link/item count', count($posts));
329
330 // Copy back any changes to feed settings made in the
331 // course of updating (e.g. new author rules)
332 $update_set = "link_notes = '".esc_sql($this->settings_to_notes())."'";
333
334 // Update the properties of the link from the feed information
335 $result = $wpdb->query("
336 UPDATE $wpdb->links
337 SET $update_set
338 WHERE link_id='$this->id'
339 ");
340
341 do_action("update_syndicated_feed_completed", $this->id, $this);
342 endif;
343
344 // All done; let's clean up.
345 $this->magpie = NULL;
346
347 // Avoid circular-reference memory leak in PHP < 5.3.
348 // Cf. <http://simplepie.org/wiki/faq/i_m_getting_memory_leaks>
349 if (method_exists($this->simplepie, '__destruct')) :
350 $this->simplepie->__destruct();
351 endif;
352 $this->simplepie = NULL;
353
354 return $new_count;
355 } /* SyndicatedLink::poll() */
356
357 public function do_update_ttl () {
358 list($ttl, $xml) = $this->ttl(/*return element=*/ true);
359
360 if (!is_null($ttl)) :
361 $this->update_setting('update/ttl', $ttl);
362 $this->update_setting('update/xml', $xml);
363 $this->update_setting('update/timed', 'feed');
364 else :
365 $ttl = $this->automatic_ttl();
366 $this->update_setting('update/ttl', $ttl);
367 $this->update_setting('update/xml', NULL);
368 $this->update_setting('update/timed', 'automatically');
369 endif;
370
371 $this->update_setting('update/fudge', rand(0, ($ttl/3))*60);
372
373 $this->update_setting('update/ttl', apply_filters(
374 'syndicated_feed_ttl',
375 $this->setting('update/ttl'),
376 $this
377 ));
378
379 } /* SyndicatedLink::do_update_ttl () */
380
381 public function process_retirements ($delta) {
382 global $post;
383
384 $q = new WP_Query(array(
385 'fields' => '_synfrom',
386 'post_status__not' => 'fwpretired',
387 'ignore_sticky_posts' => true,
388 'meta_key' => '_feedwordpress_retire_me_'.$this->id,
389 'meta_value' => '1',
390 ));
391 if ($q->have_posts()) :
392 foreach ($q->posts as $p) :
393 $old_status = $p->post_status;
394 FeedWordPress::diagnostic('syndicated_posts', 'Retiring existing post # '.$p->ID.' "'.$p->post_title.'" due to absence from a non-incremental feed.');
395 set_post_field('post_status', 'fwpretired', $p->ID);
396 wp_transition_post_status('fwpretired', $old_status, $p);
397 delete_post_meta($p->ID, '_feedwordpress_retire_me_'.$this->id);
398 endforeach;
399 endif;
400
401 return $delta;
402 } /* SyndicatedLink::process_retirements () */
403
404 /**
405 * Updates the URL for the feed syndicated by this link.
406 *
407 * @param string $url The new feed URL to use for this source.
408 * @return bool TRUE on success, FALSE on failure.
409 */
410 public function set_uri ($url) {
411 global $wpdb;
412
413 if ($this->found()) :
414 // Update link_rss
415 $result = $wpdb->query("
416 UPDATE $wpdb->links
417 SET
418 link_rss = '".esc_sql($url)."'
419 WHERE link_id = '".esc_sql($this->id)."'
420 ");
421
422 $ret = ($result ? true : false);
423 else :
424 $ret = false;
425 endif;
426 return $ret;
427 } /* SyndicatedLink::set_uri () */
428
429 public function deactivate () {
430 global $wpdb;
431
432 $wpdb->query($wpdb->prepare("
433 UPDATE $wpdb->links SET link_visible = 'N' WHERE link_id = %d
434 ", (int) $this->id));
435 } /* SyndicatedLink::deactivate () */
436
437 /**
438 * SyndicatedLink::delete() deletes a subscription from the WordPress links
439 * table. Any posts that were syndicated through that subscription will still
440 * be present in the wp_posts table; but postmeta fields that refer to the
441 * syndication feed's numeric id (which will no longer be valid) will be
442 * deleted. For most purposes, the posts remaining will be treated as if they
443 * were locally authored posts rather than syndicated posts.
444 *
445 * @global $wpdb
446 * @uses wpdb::query
447 */
448 public function delete () {
449 global $wpdb;
450
451 $wpdb->query($wpdb->prepare("
452 DELETE FROM $wpdb->postmeta WHERE meta_key='syndication_feed_id'
453 AND meta_value = '%s'
454 ", $this->id));
455
456 $wpdb->query($wpdb->prepare("
457 DELETE FROM $wpdb->links WHERE link_id = %d
458 ", (int) $this->id));
459
460 $this->id = NULL;
461 } /* SyndicatedLink::delete () */
462
463 /**
464 * SyndicatedLink::nuke() deletes a subscription AND all of the
465 * posts syndicated through that subscription.
466 *
467 * @global $wpdb
468 * @uses wpdb::get_col
469 * @uses wp_delete_post
470 * @uses SyndicatedLink::delete
471 */
472 public function nuke () {
473 global $wpdb;
474
475 // Make a list of the items syndicated from this feed...
476 $post_ids = $wpdb->get_col($wpdb->prepare("
477 SELECT post_id FROM $wpdb->postmeta
478 WHERE meta_key = 'syndication_feed_id'
479 AND meta_value = '%s'
480 ", $this->id));
481
482 // ... and kill them all
483 if (count($post_ids) > 0) :
484 foreach ($post_ids as $post_id) :
485 // Force scrubbing of deleted post
486 // rather than sending to Trashcan
487 wp_delete_post(
488 /*postid=*/ $post_id,
489 /*force_delete=*/ true
490 );
491 endforeach;
492 endif;
493
494 $this->delete();
495 } /* SyndicatedLink::nuke () */
496
497 public function map_name_to_new_user ($name, $newuser_name) {
498 global $wpdb;
499
500 if (strlen($newuser_name) > 0) :
501 $newuser_id = fwp_insert_new_user($newuser_name);
502 if (is_numeric($newuser_id)) :
503 if (is_null($name)) : // Unfamiliar author
504 $this->update_setting('unfamiliar author', $newuser_id);
505 else :
506 $map = $this->setting('map authors');
507 $map['name'][$name] = $newuser_id;
508 $this->update_setting('map authors', $map);
509 endif;
510 else :
511 // TODO: Add some error detection and reporting
512 endif;
513 else :
514 // TODO: Add some error reporting
515 endif;
516 } /* SyndicatedLink::map_name_to_new_user () */
517
518 protected function imploded_settings () {
519 return array('cats', 'tags', 'match/cats', 'match/tags', 'match/filter');
520 } /* SyndicatedLink::imploded_settings () */
521
522 protected function get_settings_from_notes () {
523 // Read off feed settings from link_notes
524 $notes = explode("\n", $this->link->link_notes);
525 foreach ($notes as $note):
526 $pair = explode(": ", $note, 2);
527 $key = (isset($pair[0]) ? $pair[0] : null);
528 $value = (isset($pair[1]) ? $pair[1] : null);
529 if (!is_null($key) and !is_null($value)) :
530 // Unescape and trim() off the whitespace.
531 // Thanks to Ray Lischner for pointing out the
532 // need to trim off whitespace.
533 $this->settings[$key] = stripcslashes (trim($value));
534 endif;
535 endforeach;
536
537 // "Magic" feed settings
538 $this->settings['link/uri'] = $this->link->link_rss;
539 $this->settings['link/name'] = $this->link->link_name;
540 $this->settings['link/id'] = $this->link->link_id;
541
542 // `hardcode categories` and `unfamiliar categories` are
543 // deprecated in favor of `unfamiliar category`
544 if (
545 isset($this->settings['unfamiliar categories'])
546 and !isset($this->settings['unfamiliar category'])
547 ) :
548 $this->settings['unfamiliar category'] = $this->settings['unfamiliar categories'];
549 endif;
550 if (
551 FeedWordPress::affirmative($this->settings, 'hardcode categories')
552 and !isset($this->settings['unfamiliar category'])
553 ) :
554 $this->settings['unfamiliar category'] = 'default';
555 endif;
556
557 // Set this up automagically for del.icio.us
558 $bits = parse_url($this->link->link_rss);
559 $tagspacers = array('del.icio.us', 'feeds.delicious.com');
560 if (!isset($this->settings['cat_split']) and in_array($bits['host'], $tagspacers)) :
561 $this->settings['cat_split'] = '\s'; // Whitespace separates multiple tags in del.icio.us RSS feeds
562 endif;
563
564 // Simple lists
565 foreach ($this->imploded_settings() as $what) :
566 if (isset($this->settings[$what])):
567 $this->settings[$what] = explode(
568 FEEDWORDPRESS_CAT_SEPARATOR,
569 $this->settings[$what]
570 );
571 endif;
572 endforeach;
573
574 if (isset($this->settings['terms'])) :
575 // Look for new format
576 $this->settings['terms'] = maybe_unserialize($this->settings['terms']);
577
578 if (!is_array($this->settings['terms'])) :
579 // Deal with old format instead. Ugh.
580
581 // Split on two *or more* consecutive breaks
582 // because in the old format, a taxonomy
583 // without any associated terms would
584 // produce tax_name#1\n\n\ntax_name#2\nterm,
585 // and the naive split on the first \n\n
586 // would screw up the tax_name#2 list.
587 //
588 // Props to David Morris for pointing this
589 // out.
590
591 $this->settings['terms'] = preg_split(
592 "/".FEEDWORDPRESS_CAT_SEPARATOR."{2,}/",
593 $this->settings['terms']
594 );
595 $terms = array();
596 foreach ($this->settings['terms'] as $line) :
597 $line = explode(FEEDWORDPRESS_CAT_SEPARATOR, $line);
598 $tax = array_shift($line);
599 $terms[$tax] = $line;
600 endforeach;
601 $this->settings['terms'] = $terms;
602 endif;
603 endif;
604
605 if (isset($this->settings['map authors'])) :
606 $author_rules = explode("\n\n", $this->settings['map authors']);
607 $ma = array();
608 foreach ($author_rules as $rule) :
609 list($rule_type, $author_name, $author_action) = explode("\n", $rule);
610
611 // Normalize for case and whitespace
612 $rule_type = strtolower(trim($rule_type));
613 $author_name = strtolower(trim($author_name));
614 $author_action = strtolower(trim($author_action));
615
616 $ma[$rule_type][$author_name] = $author_action;
617 endforeach;
618 $this->settings['map authors'] = $ma;
619 endif;
620
621 } /* SyndicatedLink::get_settings_from_notes () */
622
623 protected function settings_to_notes () {
624 $to_notes = $this->settings;
625
626 unset($to_notes['link/id']); // Magic setting; don't save
627 unset($to_notes['link/uri']); // Magic setting; don't save
628 unset($to_notes['link/name']); // Magic setting; don't save
629 unset($to_notes['hardcode categories']); // Deprecated
630 unset($to_notes['unfamiliar categories']); // Deprecated
631
632 // Collapse array settings
633 if (isset($to_notes['update/processed']) and (is_array($to_notes['update/processed']))) :
634 $to_notes['update/processed'] = implode("\n", $to_notes['update/processed']);
635 endif;
636
637 foreach ($this->imploded_settings() as $what) :
638 if (isset($to_notes[$what]) and is_array($to_notes[$what])) :
639 $to_notes[$what] = implode(
640 FEEDWORDPRESS_CAT_SEPARATOR,
641 $to_notes[$what]
642 );
643 endif;
644 endforeach;
645
646 if (isset($to_notes['terms']) and is_array($to_notes['terms'])) :
647 // Serialize it.
648 $to_notes['terms'] = serialize($to_notes['terms']);
649 endif;
650
651 // Collapse the author mapping rule structure back into a flat string
652 if (isset($to_notes['map authors'])) :
653 $ma = array();
654 foreach ($to_notes['map authors'] as $rule_type => $author_rules) :
655 foreach ($author_rules as $author_name => $author_action) :
656 $ma[] = $rule_type."\n".$author_name."\n".$author_action;
657 endforeach;
658 endforeach;
659 $to_notes['map authors'] = implode("\n\n", $ma);
660 endif;
661
662 $notes = '';
663 foreach ($to_notes as $key => $value) :
664 $notes .= $key . ": ". addcslashes($value, "\0..\37".'\\') . "\n";
665 endforeach;
666 return $notes;
667 } /* SyndicatedLink::settings_to_notes () */
668
669 public function save_settings ($reload = false) {
670 global $wpdb;
671
672 // Save channel-level meta-data
673 foreach (array('link_name', 'link_description', 'link_url') as $what) :
674 $alter[] = "{$what} = '".esc_sql($this->link->{$what})."'";
675 endforeach;
676
677 // Save settings to the notes field
678 $alter[] = "link_notes = '".esc_sql($this->settings_to_notes())."'";
679
680 // Update the properties of the link from settings changes, etc.
681 $update_set = implode(", ", $alter);
682
683 $result = $wpdb->query("
684 UPDATE $wpdb->links
685 SET $update_set
686 WHERE link_id='$this->id'
687 ");
688
689 if ($reload) :
690 // force reload of link information from DB
691 if (function_exists('clean_bookmark_cache')) :
692 clean_bookmark_cache($this->id);
693 endif;
694 endif;
695 } /* SyndicatedLink::save_settings () */
696
697 /**
698 * Retrieves the value of a setting, allowing for a global setting to be
699 * used as a fallback, or a constant value, or both.
700 *
701 * @param string $name The link setting key
702 * @param mixed $fallback_global If the link setting is nonexistent or marked as a use-default value, fall back to the value of this global setting.
703 * @param mixed $fallback_value If the link setting and the global setting are nonexistent or marked as a use-default value, fall back to this constant value.
704 * @return bool TRUE on success, FALSE on failure.
705 */
706 public function setting ($name, $fallback_global = NULL, $fallback_value = NULL, $default = 'default') {
707 $ret = NULL;
708 if (isset($this->settings[$name])) :
709 $ret = $this->settings[$name];
710 endif;
711
712 $no_value = (
713 is_null($ret)
714 or (is_string($ret) and strtolower($ret)==$default)
715 );
716
717 if ($no_value and !is_null($fallback_global)) :
718 // Avoid duplication of this correction
719 $fallback_global = preg_replace('/^feedwordpress_/', '', $fallback_global);
720
721 // Occasionally we'll get an array back. Convert it to a string
722 if ( is_array($fallback_global) && sizeof($fallback_global) )
723 $fallback_global = reset($fallback_global);
724
725 if ( !empty($fallback_global) )
726 $ret = get_option('feedwordpress_'.$fallback_global, /*default=*/ NULL);
727 endif;
728
729 $no_value = (
730 is_null($ret)
731 or (is_string($ret) and strtolower($ret)==$default)
732 );
733
734 if ($no_value and !is_null($fallback_value)) :
735 $ret = $fallback_value;
736 endif;
737 return $ret;
738 } /* SyndicatedLink::setting () */
739
740 public function merge_settings ($data, $prefix, $separator = '/') {
741 $dd = $this->flatten_array($data, $prefix, $separator);
742 $this->settings = array_merge($this->settings, $dd);
743 } /* SyndicatedLink::merge_settings () */
744
745 public function update_setting ($name, $value, $default = 'default') {
746 if (!is_null($value) and $value != $default) :
747 $this->settings[$name] = $value;
748 else : // Zap it.
749 unset($this->settings[$name]);
750 endif;
751 } /* SyndicatedLink::update_setting () */
752
753 public function is_non_incremental () {
754 return ('complete'==$this->setting('update_incremental', 'update_incremental', 'incremental'));
755 } /* SyndicatedLink::is_non_incremental () */
756
757 public function get_feed_type () {
758 $type_code = $this->setting('link/feed_type');
759
760 // list derived from: <http://simplepie.org/api/class-SimplePie.html>, retrieved 2020/01/18
761 $bitmasks = array(
762 SIMPLEPIE_TYPE_RSS_090 => 'RSS 0.90',
763 SIMPLEPIE_TYPE_RSS_091_NETSCAPE => 'RSS 0.91 (Netscape)',
764 SIMPLEPIE_TYPE_RSS_091_USERLAND => 'RSS 0.91 (Userland)',
765 SIMPLEPIE_TYPE_RSS_091 => 'RSS 0.91',
766 SIMPLEPIE_TYPE_RSS_092 => 'RSS 0.92',
767 SIMPLEPIE_TYPE_RSS_093 => 'RSS 0.93',
768 SIMPLEPIE_TYPE_RSS_094 => 'RSS 0.94',
769 SIMPLEPIE_TYPE_RSS_10 => 'RSS 1.0',
770 SIMPLEPIE_TYPE_RSS_20 => 'RSS 2.0.x',
771 SIMPLEPIE_TYPE_RSS_RDF => 'RDF-based RSS',
772 SIMPLEPIE_TYPE_RSS_SYNDICATION => 'Non-RDF-based RSS',
773 SIMPLEPIE_TYPE_RSS_ALL => 'Any version of RSS',
774 SIMPLEPIE_TYPE_ATOM_03 => 'Atom 0.3',
775 SIMPLEPIE_TYPE_ATOM_10 => 'Atom 1.0',
776 SIMPLEPIE_TYPE_ATOM_ALL => 'Atom (any version)',
777 SIMPLEPIE_TYPE_ALL => 'Supported Feed (unspecified format)',
778 );
779
780 $type = "Unknown or unsupported format";
781 foreach ($bitmasks as $format_flag => $format_string) :
782 if (is_numeric($format_flag)) : // Guard against failure of constants to be defined.
783 if ($type_code & $format_flag) :
784 $type = $format_string;
785 break; // foreach
786 endif;
787 endif;
788 endforeach;
789
790 return $type;
791 } /* SyndicatedLink::get_feed_type () */
792
793 public function uri ($params = array()) {
794 $params = wp_parse_args($params, array(
795 'add_params' => false,
796 'fetch' => false,
797 ));
798
799 // Initialize $qp (= array for added query parameters, if any)
800 $qp = array();
801
802 $link_rss = (is_object($this->link) ? $this->link->link_rss : NULL);
803
804 // $link_rss stores the URI for the subscription as stored in the feed's record.
805 // $uri stores the effective URI of the request including any/all added query parameters
806 $uri = $link_rss;
807 if (!is_null($uri) and strlen($uri) > 0 and $params['add_params']) :
808 $qp = maybe_unserialize($this->setting('query parameters', array()));
809
810 // For high-tech HTTP feed request kung fu
811 $qp = apply_filters('syndicated_feed_parameters', $qp, $uri, $this);
812
813 // $qp is an array of key-value pairs stored as arrays of format [$key, $value]
814 $q = array();
815 if (is_array($qp) and count($qp) > 0) :
816 foreach ($qp as $pair) :
817 $q[] = urlencode($pair[0]).'='.urlencode($pair[1]);
818 endforeach;
819
820 // Are we appending to a URI that already has params?
821 $sep = ((strpos($uri, "?")===false) ? '?' : '&');
822
823 // Tack it on
824 $uri .= $sep . implode("&", $q);
825 endif;
826 endif;
827
828 // Do we have any filters that apply here?
829 $uri = apply_filters('syndicated_link_uri', $uri, $link_rss, $qp, $params, $this);
830
831 // Return the filtered link URI.
832 return $uri;
833 } /* SyndicatedLink::uri () */
834
835 public function username () {
836 return $this->setting('http username', 'http_username', NULL);
837 } /* SyndicatedLink::username () */
838
839 public function password () {
840 return $this->setting('http password', 'http_password', NULL);
841 } /* SyndicatedLink::password () */
842
843 public function authentication_method () {
844 $auth = $this->setting('http auth method', NULL);
845 if (('-' == $auth) or (strlen($auth)==0)) :
846 $auth = NULL;
847 endif;
848 return $auth;
849 } /* SyndicatedLink::authentication_method () */
850
851 var $postmeta = array();
852 public function postmeta ($params = array()) {
853 $params = wp_parse_args($params, /*defaults=*/ array(
854 "field" => NULL,
855 "parsed" => false,
856 "force" => false,
857 ));
858
859 if ($params['force'] or !isset($this->postmeta[/*parsed = */ false])) :
860 // First, get the global settings.
861 $default_custom_settings = get_option('feedwordpress_custom_settings');
862 if ($default_custom_settings and !is_array($default_custom_settings)) :
863 $default_custom_settings = unserialize($default_custom_settings);
864 endif;
865 if (!is_array($default_custom_settings)) :
866 $default_custom_settings = array();
867 endif;
868
869 // Next, get the settings for this particular feed.
870 $custom_settings = $this->setting('postmeta', NULL, NULL);
871 if ($custom_settings and !is_array($custom_settings)) :
872 $custom_settings = unserialize($custom_settings);
873 endif;
874 if (!is_array($custom_settings)) :
875 $custom_settings = array();
876 endif;
877
878 $this->postmeta[/*parsed=*/ false] = array_merge($default_custom_settings, $custom_settings);
879 $this->postmeta[/*parsed=*/ true] = array();
880
881 // Now, run through and parse them all.
882 foreach ($this->postmeta[/*parsed=*/ false] as $key => $meta) :
883 $meta = apply_filters("syndicated_link_post_meta_${key}_pre", $meta, $this);
884 $this->postmeta[/*parsed=*/ false][$key] = $meta;
885 $this->postmeta[/*parsed=*/ true][$key] = new FeedWordPressParsedPostMeta($meta);
886 endforeach;
887 endif;
888
889 $ret = $this->postmeta[!!$params['parsed']];
890 if (is_string($params['field'])) :
891 $ret = $ret[$params['field']];
892 endif;
893 return $ret;
894 } /* SyndicatedLink::postmeta () */
895
896 public function property_cascade ($fromFeed, $link_field, $setting, $method) {
897 $value = NULL;
898 if ($fromFeed) :
899 $value = $this->setting($setting, NULL, NULL, NULL);
900
901 $s = $this->simplepie;
902 $callable = (is_object($s) and method_exists($s, $method));
903 if (is_null($value) and $callable) :
904 $fallback = $s->{$method}();
905 endif;
906 else :
907 $value = $this->link->{$link_field};
908 endif;
909 return $value;
910 } /* SyndicatedLink::property_cascade () */
911
912 public function homepage ($fromFeed = true) {
913 return $this->property_cascade($fromFeed, 'link_url', 'feed/link', 'get_link');
914 } /* SyndicatedLink::homepage () */
915
916 public function name ($fromFeed = true) {
917 return $this->property_cascade($fromFeed, 'link_name', 'feed/title', 'get_title');
918 } /* SyndicatedLink::name () */
919
920 public function guid () {
921 $ret = $this->setting('feed/id', NULL, $this->uri());
922
923 // If we can get it live from the feed, do so.
924 if (is_object($this->simplepie)) :
925 $search = array(
926 array('', 'id'),
927 array(SIMPLEPIE_NAMESPACE_ATOM_10, 'id'),
928 array(SIMPLEPIE_NAMESPACE_ATOM_03, 'id'),
929 array(SIMPLEPIE_NAMESPACE_RSS_20, 'guid'),
930 array(SIMPLEPIE_NAMESPACE_DC_11, 'identifier'),
931 array(SIMPLEPIE_NAMESPACE_DC_10, 'identifier'),
932 );
933
934 foreach ($search as $pair) :
935 if ($id_tags = $this->simplepie->get_feed_tags($pair[0], $pair[1])) :
936 $ret = $id_tags[0]['data'];
937 break;
938 elseif ($id_tags = $this->simplepie->get_channel_tags($pair[0], $pair[1])) :
939 $ret = $id_tags[0]['data'];
940 break;
941 endif;
942 endforeach;
943 endif;
944 return $ret;
945 }
946
947 public function links ($params = array()) {
948 $params = wp_parse_args($params, array(
949 "rel" => NULL,
950 ));
951
952 $fLinks = array();
953 $search = array(
954 array('', 'link'),
955 array(SIMPLEPIE_NAMESPACE_ATOM_10, 'link'),
956 array(SIMPLEPIE_NAMESPACE_ATOM_03, 'link'),
957 );
958
959 foreach ($search as $pair) :
960 if ($link_tags = $this->simplepie->get_feed_tags($pair[0], $pair[1])) :
961 $fLinks = array_merge($fLinks, $link_tags);
962 endif;
963 if ($link_tags = $this->simplepie->get_channel_tags($pair[0], $pair[1])) :
964 $fLinks = array_merge($fLinks, $link_tags);
965 endif;
966 endforeach;
967
968 $ret = array();
969 foreach ($fLinks as $link) :
970 $filter = false;
971 if (!is_null($params['rel'])) :
972 $filter = true;
973
974 if (isset($link['attribs'])) :
975 // Get a list of NSes from the search
976 foreach ($search as $pair) :
977 $ns = $pair[0];
978
979 if (isset($link['attribs'][$ns])
980 and isset($link['attribs'][$ns]['rel'])
981 ) :
982 $rel = strtolower(trim($link['attribs'][$ns]['rel']));
983 $fRel = strtolower(trim($params['rel']));
984
985 if ($rel == $fRel) :
986 $filter = false;
987 endif;
988 endif;
989 endforeach;
990 endif;
991 endif;
992
993 if (!$filter) :
994 $ret[] = $link;
995 endif;
996 endforeach;
997
998 return $ret;
999 }
1000
1001 public function ttl ($return_element = false) {
1002 if (is_object($this->magpie)) :
1003 $channel = $this->magpie->channel;
1004 else :
1005 $channel = array();
1006 endif;
1007
1008 if (isset($channel['ttl'])) :
1009 // "ttl stands for time to live. It's a number of
1010 // minutes that indicates how long a channel can be
1011 // cached before refreshing from the source."
1012 // <http://blogs.law.harvard.edu/tech/rss#ltttlgtSubelementOfLtchannelgt>
1013 $xml = 'rss:ttl';
1014 $ret = $channel['ttl'];
1015 elseif (isset($channel['sy']['updatefrequency']) or isset($channel['sy']['updateperiod'])) :
1016 $period_minutes = array (
1017 'hourly' => 60, /* minutes in an hour */
1018 'daily' => 1440, /* minutes in a day */
1019 'weekly' => 10080, /* minutes in a week */
1020 'monthly' => 43200, /* minutes in a month */
1021 'yearly' => 525600, /* minutes in a year */
1022 );
1023
1024 // "sy:updatePeriod: Describes the period over which the
1025 // channel format is updated. Acceptable values are:
1026 // hourly, daily, weekly, monthly, yearly. If omitted,
1027 // daily is assumed." <http://web.resource.org/rss/1.0/modules/syndication/>
1028 if (isset($channel['sy']['updateperiod'])) : $period = trim($channel['sy']['updateperiod']);
1029 else : $period = 'daily';
1030 endif;
1031
1032 // "sy:updateFrequency: Used to describe the frequency
1033 // of updates in relation to the update period. A
1034 // positive integer indicates how many times in that
1035 // period the channel is updated. ... If omitted a value
1036 // of 1 is assumed." <http://web.resource.org/rss/1.0/modules/syndication/>
1037 if (isset($channel['sy']['updatefrequency'])) : $freq = (int) $channel['sy']['updatefrequency'];
1038 else : $freq = 1;
1039 endif;
1040
1041 // normalize the period name...
1042 $period = strtolower(trim($period));
1043
1044 // do we recognize the alphanumeric period name? if not, then guess
1045 // a responsible default, e.g. roughly hourly
1046 $mins = (isset($period_minutes[$period]) ? $period_minutes[$period] : 67);
1047
1048 $xml = 'sy:updateFrequency';
1049 $ret = (int) ($mins / $freq);
1050
1051 else :
1052 $xml = NULL;
1053 $ret = NULL;
1054 endif;
1055
1056 if ('yes'==$this->setting('update/minimum', 'update_minimum', 'no')) :
1057 $min = (int) $this->setting('update/window', 'update_window', DEFAULT_UPDATE_PERIOD);
1058
1059 if ($min > $ret) :
1060 $ret = NULL;
1061 endif;
1062 endif;
1063 return ($return_element ? array($ret, $xml) : $ret);
1064 } /* SyndicatedLink::ttl() */
1065
1066 public function automatic_ttl () {
1067 // spread out over a time interval for staggered updates
1068 $updateWindow = $this->setting('update/window', 'update_window', DEFAULT_UPDATE_PERIOD);
1069 if (!is_numeric($updateWindow) or ($updateWindow < 1)) :
1070 $updateWindow = DEFAULT_UPDATE_PERIOD;
1071 endif;
1072
1073 // We get a fudge of 1/3 of window from elsewhere. We'll do some more
1074 // fudging here.
1075 $fudgedInterval = $updateWindow+rand(-($updateWindow/6), 5*($updateWindow/12));
1076 return apply_filters('syndicated_feed_automatic_ttl', $fudgedInterval, $this);
1077 } /* SyndicatedLink::automatic_ttl () */
1078
1079 /**
1080 * SyndicatedLink::flatten_array (): flatten an array. Useful for
1081 * hierarchical and namespaced elements.
1082 *
1083 * Given an array which may contain array or object elements in it,
1084 * return a "flattened" array: a one-dimensional array of scalars
1085 * containing each of the scalar elements contained within the array
1086 * structure. Thus, for example, if $a['b']['c']['d'] == 'e', then the
1087 * returned array for FeedWordPress::flatten_array($a) will contain a key
1088 * $a['feed/b/c/d'] with value 'e'.
1089 *
1090 * @param array $arr
1091 * @param string $prefix
1092 * @param string $separator
1093 * @return array
1094 */
1095 public function flatten_array ($arr, $prefix = 'feed/', $separator = '/') {
1096 $ret = array ();
1097 if (is_array($arr)) :
1098 foreach ($arr as $key => $value) :
1099 if (is_scalar($value)) :
1100 $ret[$prefix.$key] = $value;
1101 else :
1102 $ret = array_merge($ret, $this->flatten_array($value, $prefix.$key.$separator, $separator));
1103 endif;
1104 endforeach;
1105 endif;
1106 return $ret;
1107 } /* SyndicatedLink::flatten_array () */
1108
1109 public function hardcode ($what) {
1110
1111 $ret = $this->setting('hardcode '.$what, 'hardcode_'.$what, NULL);
1112
1113 if ('yes' == $ret) :
1114 $ret = true;
1115 else :
1116 $ret = false;
1117 endif;
1118 return $ret;
1119 } /* SyndicatedLink::hardcode () */
1120
1121 public function syndicated_status ($what, $default, $fallback = true) {
1122 global $wpdb;
1123
1124 $g_set = ($fallback ? 'syndicated_' . $what . '_status' : NULL);
1125 $ret = $this->setting($what.' status', $g_set, $default);
1126
1127 return esc_sql(trim(strtolower($ret)));
1128 } /* SyndicatedLink:syndicated_status () */
1129
1130 public function taxonomies () {
1131 $post_type = $this->setting('syndicated post type', 'syndicated_post_type', 'post');
1132 return get_object_taxonomies(array('object_type' => $post_type), 'names');
1133 } /* SyndicatedLink::taxonomies () */
1134
1135 /**
1136 * category_ids: look up (and create) category ids from a list of
1137 * categories
1138 *
1139 * @param array $cats
1140 * @param string $unfamiliar_category
1141 * @param array|null $taxonomies
1142 * @return array
1143 */
1144 public function category_ids ($post, $cats, $unfamiliar_category = 'create', $taxonomies = NULL, $params = array()) {
1145 $singleton = (isset($params['singleton']) ? $params['singleton'] : true);
1146 $allowFilters = (isset($params['filters']) ? $params['filters'] : false);
1147
1148 $catTax = 'category';
1149
1150 if (is_null($taxonomies)) :
1151 $taxonomies = array('category');
1152 endif;
1153
1154 // We need to normalize whitespace because (1) trailing
1155 // whitespace can cause PHP and MySQL not to see eye to eye on
1156 // VARCHAR comparisons for some versions of MySQL (cf.
1157 // <http://dev.mysql.com/doc/mysql/en/char.html>), and (2)
1158 // because I doubt most people want to make a semantic
1159 // distinction between 'Computers' and 'Computers '
1160 $cats = array_map('trim', $cats);
1161
1162 $terms = array();
1163 foreach ($taxonomies as $tax) :
1164 $terms[$tax] = array();
1165 endforeach;
1166
1167 foreach ($cats as $cat_name) :
1168 if (strlen(trim($cat_name)) < 1) :
1169 continue;
1170 endif;
1171
1172 $oTerm = new SyndicatedPostTerm($cat_name, $taxonomies, $post);
1173
1174 if ($oTerm->is_familiar()) :
1175
1176 $tax = $oTerm->taxonomy();
1177 if (!isset($terms[$tax])) :
1178 $terms[$tax] = array();
1179 endif;
1180 $terms[$tax][] = $oTerm->id();
1181
1182 else :
1183
1184 if ('tag'==$unfamiliar_category) :
1185 $unfamiliar_category = 'create:post_tag';
1186 endif;
1187
1188 if (preg_match('/^create(:(.*))?$/i', $unfamiliar_category, $ref)) :
1189 $tax = $catTax; // Default
1190
1191 if (isset($ref[2])
1192 and strlen($ref[2]) > 2) :
1193 $tax = $ref[2];
1194 endif;
1195
1196 $inserted = $oTerm->insert($tax);
1197 if (!is_null($inserted)) :
1198 if (!isset($terms[$tax])) :
1199 $terms[$tax] = array();
1200 endif;
1201 $terms[$tax][] = $inserted;
1202 else :
1203
1204 endif; // !is_null($inserted)
1205 endif; // preg_match(...)
1206
1207 endif; /* ($oTerm->is_familiar()) */
1208 endforeach;
1209
1210 $filtersOn = $allowFilters;
1211 if ($allowFilters) :
1212 $filters = array_filter(
1213 $this->setting('match/filter', 'match_filter', array()),
1214 'remove_dummy_zero'
1215 );
1216 $filtersOn = ($filtersOn and is_array($filters) and (count($filters) > 0));
1217 endif;
1218
1219 // Check for filter conditions
1220 foreach ($terms as $tax => $term_ids) :
1221 if ($filtersOn
1222 and (count($term_ids)==0)
1223 and in_array($tax, $filters)) :
1224 $terms = NULL; // Drop the post
1225 break;
1226 else :
1227 $terms[$tax] = array_unique($term_ids);
1228 endif;
1229 endforeach;
1230
1231 if ($singleton and count($terms)==1) : // If we only searched one, just return the term IDs
1232 $terms = end($terms);
1233 endif;
1234
1235 FeedWordPress::diagnostic(
1236 'syndicated_posts:categories',
1237 'Category: MAPPED term names '.json_encode($cats).' to IDs: '.json_encode($terms)
1238 );
1239 return $terms;
1240 } /* SyndicatedLink::category_ids () */
1241 } /* class SyndicatedLink */
1242
1243