PluginProbe
FeedWordPress / 2011.0512
FeedWordPress v2011.0512
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
← All changes | syndicatedlink.class.php +401 -41 2009.06132011.0512 View file →
@@ -32,12 +32,15 @@
32 32 #
33 33 # Values of keys in link_notes are accessible from templates using
34 34 # the function `get_feed_meta($key)` if this plugin is activated.
35 35
36 +require_once(dirname(__FILE__).'/magpiefromsimplepie.class.php');
37 +
36 38 class SyndicatedLink {
37 39 var $id = null;
38 40 var $link = null;
39 41 var $settings = array ();
42 + var $simplepie = null;
40 43 var $magpie = null;
41 44
42 45 function SyndicatedLink ($link) {
43 46 global $wpdb;
@@ -60,11 +63,12 @@
60 63 if (strlen($this->link->link_rss) > 0) :
61 64 // Read off feed settings from link_notes
62 65 $notes = explode("\n", $this->link->link_notes);
63 66 foreach ($notes as $note):
64 - list($key, $value) = explode(": ", $note, 2);
65 -
66 - if (strlen($key) > 0) :
67 + $pair = explode(": ", $note, 2);
68 + $key = (isset($pair[0]) ? $pair[0] : null);
69 + $value = (isset($pair[1]) ? $pair[1] : null);
70 + if (!is_null($key) and !is_null($value)) :
67 71 // Unescape and trim() off the whitespace.
68 72 // Thanks to Ray Lischner for pointing out the
69 73 // need to trim off whitespace.
70 74 $this->settings[$key] = stripcslashes (trim($value));
@@ -96,14 +100,48 @@
96 100 if (!isset($this->settings['cat_split']) and in_array($bits['host'], $tagspacers)) :
97 101 $this->settings['cat_split'] = '\s'; // Whitespace separates multiple tags in del.icio.us RSS feeds
98 102 endif;
99 103
100 - if (isset($this->settings['cats'])):
101 - $this->settings['cats'] = preg_split(FEEDWORDPRESS_CAT_SEPARATOR_PATTERN, $this->settings['cats']);
104 + // Simple lists
105 + foreach ($this->imploded_settings() as $what) :
106 + if (isset($this->settings[$what])):
107 + $this->settings[$what] = explode(
108 + FEEDWORDPRESS_CAT_SEPARATOR,
109 + $this->settings[$what]
110 + );
111 + endif;
112 + endforeach;
113 +
114 + if (isset($this->settings['terms'])) :
115 + // Look for new format
116 + $this->settings['terms'] = maybe_unserialize($this->settings['terms']);
117 +
118 + if (!is_array($this->settings['terms'])) :
119 + // Deal with old format instead. Ugh.
120 +
121 + // Split on two *or more* consecutive breaks
122 + // because in the old format, a taxonomy
123 + // without any associated terms would
124 + // produce tax_name#1\n\n\ntax_name#2\nterm,
125 + // and the naive split on the first \n\n
126 + // would screw up the tax_name#2 list.
127 + //
128 + // Props to David Morris for pointing this
129 + // out.
130 +
131 + $this->settings['terms'] = preg_split(
132 + "/".FEEDWORDPRESS_CAT_SEPARATOR."{2,}/",
133 + $this->settings['terms']
134 + );
135 + $terms = array();
136 + foreach ($this->settings['terms'] as $line) :
137 + $line = explode(FEEDWORDPRESS_CAT_SEPARATOR, $line);
138 + $tax = array_shift($line);
139 + $terms[$tax] = $line;
140 + endforeach;
141 + $this->settings['terms'] = $terms;
142 + endif;
102 143 endif;
103 - if (isset($this->settings['tags'])):
104 - $this->settings['tags'] = preg_split(FEEDWORDPRESS_CAT_SEPARATOR_PATTERN, $this->settings['tags']);
105 - endif;
106 144
107 145 if (isset($this->settings['map authors'])) :
108 146 $author_rules = explode("\n\n", $this->settings['map authors']);
109 147 $ma = array();
@@ -122,9 +160,9 @@
122 160 endif;
123 161 } /* SyndicatedLink::SyndicatedLink () */
124 162
125 163 function found () {
126 - return is_object($this->link);
164 + return is_object($this->link) and !is_wp_error($this->link);
127 165 } /* SyndicatedLink::found () */
128 166
129 167 function stale () {
130 168 $stale = true;
@@ -144,9 +182,26 @@
144 182
145 183 function poll ($crash_ts = NULL) {
146 184 global $wpdb;
147 185
148 - $this->magpie = fetch_rss($this->link->link_rss);
186 + $url = $this->uri(array('add_params' => true));
187 + FeedWordPress::diagnostic('updated_feeds', 'Polling feed ['.$url.']');
188 +
189 + $timeout = $this->setting('fetch timeout', 'feedwordpress_fetch_timeout', FEEDWORDPRESS_FETCH_TIMEOUT_DEFAULT);
190 +
191 + $this->simplepie = apply_filters(
192 + 'syndicated_feed',
193 + FeedWordPress::fetch($url, array('timeout' => $timeout)),
194 + $this
195 + );
196 +
197 + // Filter compatibility mode
198 + if (is_wp_error($this->simplepie)) :
199 + $this->magpie = $this->simplepie;
200 + else :
201 + $this->magpie = new MagpieFromSimplePie($this->simplepie, NULL);
202 + endif;
203 +
149 204 $new_count = NULL;
150 205
151 206 $resume = FeedWordPress::affirmative($this->settings, 'update/unfinished');
152 207 if ($resume) :
@@ -156,9 +211,46 @@
156 211 // begin at the beginning
157 212 $processed = array();
158 213 endif;
159 214
160 - if (is_object($this->magpie)) :
215 + if (is_wp_error($this->simplepie)) :
216 + $new_count = $this->simplepie;
217 + // Error; establish an error setting.
218 + $theError = array();
219 + $theError['ts'] = time();
220 + $theError['since'] = time();
221 + $theError['object'] = $this->simplepie;
222 +
223 + $oldError = $this->setting('update/error', NULL, NULL);
224 + if (is_string($oldError)) :
225 + $oldError = unserialize($oldError);
226 + endif;
227 +
228 + if (!is_null($oldError)) :
229 + // Copy over the in-error-since timestamp
230 + $theError['since'] = $oldError['since'];
231 +
232 + // If this is a repeat error, then we should take
233 + // a step back before we try to fetch it again.
234 + $this->settings['update/last'] = time();
235 + $this->settings['update/ttl'] = $this->automatic_ttl();
236 + $this->settings['update/ttl'] = apply_filters('syndicated_feed_ttl', $this->settings['update/ttl'], $this);
237 + $this->settings['update/ttl'] = apply_filters('syndicated_feed_ttl_from_error', $this->settings['update/ttl'], $this);
238 +
239 + $this->settings['update/timed'] = 'automatically';
240 + endif;
241 +
242 + do_action('syndicated_feed_error', $theError, $oldError, $this);
243 +
244 + $this->settings['update/error'] = serialize($theError);
245 + $this->save_settings(/*reload=*/ true);
246 +
247 + elseif (is_object($this->simplepie)) :
248 + // Success; clear out error setting, if any.
249 + if (isset($this->settings['update/error'])) :
250 + unset($this->settings['update/error']);
251 + endif;
252 +
161 253 $new_count = array('new' => 0, 'updated' => 0);
162 254
163 255 # -- Update Link metadata live from feed
164 256 $channel = $this->magpie->channel;
@@ -190,11 +282,12 @@
190 282 if (!is_null($ttl)) :
191 283 $this->settings['update/ttl'] = $ttl;
192 284 $this->settings['update/timed'] = 'feed';
193 285 else :
194 - $this->settings['update/ttl'] = rand(30, 120); // spread over time interval for staggered updates
286 + $this->settings['update/ttl'] = $this->automatic_ttl();
195 287 $this->settings['update/timed'] = 'automatically';
196 288 endif;
289 + $this->settings['update/ttl'] = apply_filters('syndicated_feed_ttl', $this->settings['update/ttl'], $this);
197 290
198 291 if (!isset($this->settings['update/hold']) or $this->settings['update/hold']!='ping') :
199 292 $this->settings['update/hold'] = 'scheduled';
200 293 endif;
@@ -210,15 +303,25 @@
210 303 UPDATE $wpdb->links
211 304 SET $update_set
212 305 WHERE link_id='$this->id'
213 306 ");
307 + do_action('update_syndicated_feed', $this->id, $this);
214 308
215 309 # -- Add new posts from feed and update any updated posts
216 310 $crashed = false;
217 311
218 - if (is_array($this->magpie->items)) :
219 - foreach ($this->magpie->items as $item) :
220 - $post =& new SyndicatedPost($item, $this);
312 + $posts = apply_filters(
313 + 'syndicated_feed_items',
314 + $this->simplepie->get_items(),
315 + &$this
316 + );
317 +
318 + $this->magpie->originals = $posts;
319 +
320 + if (is_array($posts)) :
321 + foreach ($posts as $key => $item) :
322 + $post = new SyndicatedPost($item, $this);
323 +
221 324 if (!$resume or !in_array(trim($post->guid()), $processed)) :
222 325 $processed[] = $post->guid();
223 326 if (!$post->filtered()) :
224 327 $new = $post->store();
@@ -229,11 +332,15 @@
229 332 $crashed = true;
230 333 break;
231 334 endif;
232 335 endif;
336 + unset($post);
233 337 endforeach;
234 338 endif;
235 -
339 + $suffix = ($crashed ? 'crashed' : 'completed');
340 + do_action('update_syndicated_feed_items', $this->id, $this);
341 + do_action("update_syndicated_feed_items_${suffix}", $this->id, $this);
342 +
236 343 // Copy back any changes to feed settings made in the course of updating (e.g. new author rules)
237 344 $to_notes = $this->settings;
238 345
239 346 $this->settings['update/processed'] = $processed;
@@ -248,29 +355,103 @@
248 355 UPDATE $wpdb->links
249 356 SET $update_set
250 357 WHERE link_id='$this->id'
251 358 ");
359 +
360 + do_action("update_syndicated_feed_completed", $this->id, $this);
252 361 endif;
253 362
363 + // All done; let's clean up.
364 + $this->magpie = NULL;
365 +
366 + // Avoid circular-reference memory leak in PHP < 5.3.
367 + // Cf. <http://simplepie.org/wiki/faq/i_m_getting_memory_leaks>
368 + if (method_exists($this->simplepie, '__destruct')) :
369 + $this->simplepie->__destruct();
370 + endif;
371 + $this->simplepie = NULL;
372 +
254 373 return $new_count;
255 374 } /* SyndicatedLink::poll() */
256 375
376 + /**
377 + * Updates the URL for the feed syndicated by this link.
378 + *
379 + * @param string $url The new feed URL to use for this source.
380 + * @return bool TRUE on success, FALSE on failure.
381 + */
382 + function set_uri ($url) {
383 + global $wpdb;
384 +
385 + if ($this->found()) :
386 + // Update link_rss
387 + $result = $wpdb->query("
388 + UPDATE $wpdb->links
389 + SET
390 + link_rss = '".$wpdb->escape($url)."'
391 + WHERE link_id = '".$wpdb->escape($this->id)."'
392 + ");
393 +
394 + $ret = ($result ? true : false);
395 + else :
396 + $ret = false;
397 + endif;
398 + return $ret;
399 + } /* SyndicatedLink::set_uri () */
400 +
401 + function deactivate () {
402 + global $wpdb;
403 +
404 + $wpdb->query($wpdb->prepare("
405 + UPDATE $wpdb->links SET link_visible = 'N' WHERE link_id = %d
406 + ", (int) $this->id));
407 + } /* SyndicatedLink::deactivate () */
408 +
409 + function delete () {
410 + global $wpdb;
411 +
412 + $wpdb->query($wpdb->prepare("
413 + DELETE FROM $wpdb->postmeta WHERE meta_key='syndication_feed_id'
414 + AND meta_value = '%s'
415 + ", $this->id));
416 +
417 + $wpdb->query($wpdb->prepare("
418 + DELETE FROM $wpdb->links WHERE link_id = %d
419 + ", (int) $this->id));
420 +
421 + $this->id = NULL;
422 + } /* SyndicatedLink::delete () */
423 +
424 + function nuke () {
425 + global $wpdb;
426 +
427 + // Make a list of the items syndicated from this feed...
428 + $post_ids = $wpdb->get_col($wpdb->prepare("
429 + SELECT post_id FROM $wpdb->postmeta
430 + WHERE meta_key = 'syndication_feed_id'
431 + AND meta_value = '%s'
432 + ", $this->id));
433 +
434 + // ... and kill them all
435 + if (count($post_ids) > 0) :
436 + foreach ($post_ids as $post_id) :
437 + // Force scrubbing of deleted post
438 + // rather than sending to Trashcan
439 + wp_delete_post(
440 + /*postid=*/ $post_id,
441 + /*force_delete=*/ true
442 + );
443 + endforeach;
444 + endif;
445 +
446 + $this->delete();
447 + } /* SyndicatedLink::nuke () */
448 +
257 449 function map_name_to_new_user ($name, $newuser_name) {
258 450 global $wpdb;
259 451
260 452 if (strlen($newuser_name) > 0) :
261 - $userdata = array();
262 - $userdata['ID'] = NULL;
263 -
264 - $userdata['user_login'] = sanitize_user($newuser_name);
265 - $userdata['user_login'] = apply_filters('pre_user_login', $userdata['user_login']);
266 -
267 - $userdata['user_nicename'] = sanitize_title($newuser_name);
268 - $userdata['user_nicename'] = apply_filters('pre_user_nicename', $userdata['user_nicename']);
269 -
270 - $userdata['display_name'] = $wpdb->escape($newuser_name);
271 -
272 - $newuser_id = wp_insert_user($userdata);
453 + $newuser_id = fwp_insert_new_user($newuser_name);
273 454 if (is_numeric($newuser_id)) :
274 455 if (is_null($name)) : // Unfamiliar author
275 456 $this->settings['unfamiliar author'] = $newuser_id;
276 457 else :
@@ -283,8 +464,11 @@
283 464 // TODO: Add some error reporting
284 465 endif;
285 466 } /* SyndicatedLink::map_name_to_new_user () */
286 467
468 + function imploded_settings () {
469 + return array('cats', 'tags', 'match/cats', 'match/tags', 'match/filter');
470 + }
287 471 function settings_to_notes () {
288 472 $to_notes = $this->settings;
289 473
290 474 unset($to_notes['link/id']); // Magic setting; don't save
@@ -297,15 +481,22 @@
297 481 if (isset($to_notes['update/processed']) and (is_array($to_notes['update/processed']))) :
298 482 $to_notes['update/processed'] = implode("\n", $to_notes['update/processed']);
299 483 endif;
300 484
301 - if (is_array($to_notes['cats'])) :
302 - $to_notes['cats'] = implode(FEEDWORDPRESS_CAT_SEPARATOR, $to_notes['cats']);
485 + foreach ($this->imploded_settings() as $what) :
486 + if (isset($to_notes[$what]) and is_array($to_notes[$what])) :
487 + $to_notes[$what] = implode(
488 + FEEDWORDPRESS_CAT_SEPARATOR,
489 + $to_notes[$what]
490 + );
491 + endif;
492 + endforeach;
493 +
494 + if (isset($to_notes['terms']) and is_array($to_notes['terms'])) :
495 + // Serialize it.
496 + $to_notes['terms'] = serialize($to_notes['terms']);
303 497 endif;
304 - if (is_array($to_notes['tags'])) :
305 - $to_notes['tags'] = implode(FEEDWORDPRESS_CAT_SEPARATOR, $to_notes['tags']);
306 - endif;
307 -
498 +
308 499 // Collapse the author mapping rule structure back into a flat string
309 500 if (isset($to_notes['map authors'])) :
310 501 $ma = array();
311 502 foreach ($to_notes['map authors'] as $rule_type => $author_rules) :
@@ -322,16 +513,160 @@
322 513 endforeach;
323 514 return $notes;
324 515 } /* SyndicatedLink::settings_to_notes () */
325 516
326 - function uri () {
327 - return (is_object($this->link) ? $this->link->link_rss : NULL);
517 + function save_settings ($reload = false) {
518 + global $wpdb;
519 +
520 + // Save channel-level meta-data
521 + foreach (array('link_name', 'link_description', 'link_url') as $what) :
522 + $alter[] = "{$what} = '".$wpdb->escape($this->link->{$what})."'";
523 + endforeach;
524 +
525 + // Save settings to the notes field
526 + $alter[] = "link_notes = '".$wpdb->escape($this->settings_to_notes())."'";
527 +
528 + // Update the properties of the link from settings changes, etc.
529 + $update_set = implode(", ", $alter);
530 +
531 + $result = $wpdb->query("
532 + UPDATE $wpdb->links
533 + SET $update_set
534 + WHERE link_id='$this->id'
535 + ");
536 +
537 + if ($reload) :
538 + // force reload of link information from DB
539 + if (function_exists('clean_bookmark_cache')) :
540 + clean_bookmark_cache($this->id);
541 + endif;
542 + endif;
543 + } /* SyndicatedLink::save_settings () */
544 +
545 + /**
546 + * Retrieves the value of a setting, allowing for a global setting to be
547 + * used as a fallback, or a constant value, or both.
548 + *
549 + * @param string $name The link setting key
550 + * @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.
551 + * @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.
552 + * @return bool TRUE on success, FALSE on failure.
553 + */
554 + function setting ($name, $fallback_global = NULL, $fallback_value = NULL, $default = 'default') {
555 + $ret = NULL;
556 + if (isset($this->settings[$name])) :
557 + $ret = $this->settings[$name];
558 + endif;
559 +
560 + $no_value = (
561 + is_null($ret)
562 + or (is_string($ret) and strtolower($ret)==$default)
563 + );
564 +
565 + if ($no_value and !is_null($fallback_global)) :
566 + // Avoid duplication of this correction
567 + $fallback_global = preg_replace('/^feedwordpress_/', '', $fallback_global);
568 +
569 + $ret = get_option('feedwordpress_'.$fallback_global, /*default=*/ NULL);
570 + endif;
571 +
572 + $no_value = (
573 + is_null($ret)
574 + or (is_string($ret) and strtolower($ret)==$default)
575 + );
576 +
577 + if ($no_value and !is_null($fallback_value)) :
578 + $ret = $fallback_value;
579 + endif;
580 + return $ret;
581 + } /* SyndicatedLink::setting () */
582 +
583 + function update_setting ($name, $value, $default = 'default') {
584 + if (!is_null($value) and $value != $default) :
585 + $this->settings[$name] = $value;
586 + else : // Zap it.
587 + unset($this->settings[$name]);
588 + endif;
589 + } /* SyndicatedLink::update_setting () */
590 +
591 + function uri ($params = array()) {
592 + $params = shortcode_atts(array(
593 + 'add_params' => false,
594 + ), $params);
595 +
596 + $uri = (is_object($this->link) ? $this->link->link_rss : NULL);
597 + if (!is_null($uri) and strlen($uri) > 0 and $params['add_params']) :
598 + $qp = maybe_unserialize($this->setting('query parameters', array()));
599 +
600 + // For high-tech HTTP feed request kung fu
601 + $qp = apply_filters('syndicated_feed_parameters', $qp, $uri, $this);
602 +
603 + $q = array();
604 + if (is_array($qp) and count($qp) > 0) :
605 + foreach ($qp as $pair) :
606 + $q[] = urlencode($pair[0]).'='.urlencode($pair[1]);
607 + endforeach;
608 +
609 + // Are we appending to a URI that already has params?
610 + $sep = ((strpos('?', $uri)===false) ? '?' : '&');
611 +
612 + // Tack it on
613 + $uri .= $sep . implode("&", $q);
614 + endif;
615 + endif;
616 +
617 + return $uri;
328 618 } /* SyndicatedLink::uri () */
329 619
330 - function homepage () {
331 - return (isset($this->settings['feed/link']) ? $this->settings['feed/link'] : NULL);
620 + function property_cascade ($fromFeed, $link_field, $setting, $simplepie_method) {
621 + $value = NULL;
622 + if ($fromFeed) :
623 + if (isset($this->settings[$setting])) :
624 + $value = $this->settings[$setting];
625 + elseif (is_object($this->simplepie)
626 + and method_exists($this->simplepie, $simplepie_method)) :
627 + $value = $this->simplepie->{$simplepie_method}();
628 + endif;
629 + else :
630 + $value = $this->link->{$link_field};
631 + endif;
632 + return $value;
633 + } /* SyndicatedLink::property_cascade () */
634 +
635 + function homepage ($fromFeed = true) {
636 + return $this->property_cascade($fromFeed, 'link_url', 'feed/link', 'get_link');
332 637 } /* SyndicatedLink::homepage () */
333 638
639 + function name ($fromFeed = true) {
640 + return $this->property_cascade($fromFeed, 'link_name', 'feed/title', 'get_title');
641 + } /* SyndicatedLink::name () */
642 +
643 + function guid () {
644 + $ret = $this->setting('feed/id', NULL, $this->uri());
645 +
646 + // If we can get it live from the feed, do so.
647 + if (is_object($this->simplepie)) :
648 + $search = array(
649 + array(SIMPLEPIE_NAMESPACE_ATOM_10, 'id'),
650 + array(SIMPLEPIE_NAMESPACE_ATOM_03, 'id'),
651 + array(SIMPLEPIE_NAMESPACE_RSS_20, 'guid'),
652 + array(SIMPLEPIE_NAMESPACE_DC_11, 'identifier'),
653 + array(SIMPLEPIE_NAMESPACE_DC_10, 'identifier'),
654 + );
655 +
656 + foreach ($search as $pair) :
657 + if ($id_tags = $this->simplepie->get_feed_tags($pair[0], $pair[1])) :
658 + $ret = $id_tags[0]['data'];
659 + break;
660 + elseif ($id_tags = $this->simplepie->get_channel_tags($pair[0], $pair[1])) :
661 + $ret = $id_tags[0]['data'];
662 + break;
663 + endif;
664 + endforeach;
665 + endif;
666 + return $ret;
667 + }
668 +
334 669 function ttl () {
335 670 if (is_object($this->magpie)) :
336 671 $channel = $this->magpie->channel;
337 672 else :
@@ -376,8 +711,19 @@
376 711 endif;
377 712 return $ret;
378 713 } /* SyndicatedLink::ttl() */
379 714
715 + function automatic_ttl () {
716 + // spread out over a time interval for staggered updates
717 + $updateWindow = $this->setting('update/window', 'update_window', DEFAULT_UPDATE_PERIOD);
718 + if (!is_numeric($updateWindow) or ($updateWindow < 1)) :
719 + $updateWindow = DEFAULT_UPDATE_PERIOD;
720 + endif;
721 +
722 + $fudgedInterval = $updateWindow+rand(0, 2*($updateWindow/3));
723 + return apply_filters('syndicated_feed_automatic_ttl', $fudgedInterval, $this);
724 + } /* SyndicatedLink::automatic_ttl () */
725 +
380 726 // SyndicatedLink::flatten_array (): flatten an array. Useful for
381 727 // hierarchical and namespaced elements.
382 728 //
383 729 // Given an array which may contain array or object elements in it,
@@ -415,17 +761,31 @@
415 761 endif;
416 762 return $ret;
417 763 } /* SyndicatedLink::hardcode () */
418 764
419 - function syndicated_status ($what, $default) {
765 + function syndicated_status ($what, $default, $fallback = true) {
420 766 global $wpdb;
421 767
422 - $ret = get_option("feedwordpress_syndicated_{$what}_status");
768 + // Use local setting if we have it
423 769 if ( isset($this->settings["$what status"]) ) :
424 770 $ret = $this->settings["$what status"];
425 - elseif (!$ret) :
771 +
772 + // Or fall back to global default if we can
773 + elseif ($fallback) :
774 + $ret = FeedWordPress::syndicated_status($what, $default);
775 +
776 + // Or use default value if we can't.
777 + else :
426 778 $ret = $default;
779 +
427 780 endif;
781 +
428 782 return $wpdb->escape(trim(strtolower($ret)));
429 783 } /* SyndicatedLink:syndicated_status () */
784 +
785 + function taxonomies () {
786 + $post_type = $this->setting('syndicated post type', 'syndicated_post_type', 'post');
787 + return get_object_taxonomies(array('object_type' => $post_type), 'names');
788 + } /* SyndicatedLink::taxonomies () */
789 +
430 790 } // class SyndicatedLink
431 791