PluginProbe
FeedWordPress / 2025.1211
FeedWordPress v2025.1211
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 2025.1211, at syndicatedlink.class.php

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