PluginProbe
FeedWordPress / 2024.0511
FeedWordPress v2024.0511
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 2024.0511, at syndicatedlink.class.php

1,284 lines 40.3 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() {
365 list( $ttl, $xml ) = $this->ttl( /*return element=*/ true );
366
367 if ( ! is_null( $ttl ) ) :
368 $this->update_setting( 'update/ttl', $ttl);
369 $this->update_setting( 'update/xml', $xml);
370 $this->update_setting( 'update/timed', 'feed');
371 else :
372 $ttl = $this->automatic_ttl();
373 $this->update_setting( 'update/ttl', $ttl);
374 $this->update_setting( 'update/xml', NULL);
375 $this->update_setting( 'update/timed', 'automatically');
376 endif;
377
378 $this->update_setting( 'update/fudge', rand( 0, ( $ttl / 3 ) ) * 60 );
379
380 $this->update_setting(
381 'update/ttl',
382 apply_filters(
383 'syndicated_feed_ttl',
384 $this->setting( 'update/ttl' ),
385 $this
386 )
387 );
388 } /* SyndicatedLink::do_update_ttl () */
389
390 public function process_retirements ($delta) {
391 $q = new WP_Query(array(
392 'fields' => '_synfrom',
393 'post_status__not' => 'fwpretired',
394 'ignore_sticky_posts' => true,
395 'meta_key' => '_feedwordpress_retire_me_'.$this->id,
396 'meta_value' => '1',
397 ));
398 if ($q->have_posts()) :
399 foreach ($q->posts as $p) :
400 $old_status = $p->post_status;
401 FeedWordPress::diagnostic('syndicated_posts', 'Retiring existing post # '.$p->ID.' "'.$p->post_title.'" due to absence from a non-incremental feed.');
402 set_post_field('post_status', 'fwpretired', $p->ID);
403 wp_transition_post_status('fwpretired', $old_status, $p);
404 delete_post_meta($p->ID, '_feedwordpress_retire_me_'.$this->id);
405 endforeach;
406 endif;
407
408 return $delta;
409 } /* SyndicatedLink::process_retirements () */
410
411 /**
412 * Updates the URL for the feed syndicated by this link.
413 *
414 * @param string $url The new feed URL to use for this source.
415 * @return bool TRUE on success, FALSE on failure.
416 */
417 public function set_uri ($url) {
418 global $wpdb;
419
420 if ($this->found()) :
421 // Update link_rss
422 $result = $wpdb->query("
423 UPDATE $wpdb->links
424 SET
425 link_rss = '".esc_sql($url)."'
426 WHERE link_id = '".esc_sql($this->id)."'
427 ");
428
429 $ret = ($result ? true : false);
430 else :
431 $ret = false;
432 endif;
433 return $ret;
434 } /* SyndicatedLink::set_uri () */
435
436 public function deactivate () {
437 global $wpdb;
438
439 $wpdb->query($wpdb->prepare("
440 UPDATE $wpdb->links SET link_visible = 'N' WHERE link_id = %d
441 ", (int) $this->id));
442 } /* SyndicatedLink::deactivate () */
443
444 /**
445 * SyndicatedLink::delete() deletes a subscription from the WordPress links
446 * table. Any posts that were syndicated through that subscription will still
447 * be present in the wp_posts table; but postmeta fields that refer to the
448 * syndication feed's numeric id (which will no longer be valid) will be
449 * deleted. For most purposes, the posts remaining will be treated as if they
450 * were locally authored posts rather than syndicated posts.
451 *
452 * @global $wpdb
453 * @uses wpdb::query
454 */
455 public function delete () {
456 global $wpdb;
457
458 $wpdb->query($wpdb->prepare("
459 DELETE FROM $wpdb->postmeta WHERE meta_key='syndication_feed_id'
460 AND meta_value = '%s'
461 ", $this->id));
462
463 $wpdb->query($wpdb->prepare("
464 DELETE FROM $wpdb->links WHERE link_id = %d
465 ", (int) $this->id));
466
467 $this->id = NULL;
468 } /* SyndicatedLink::delete () */
469
470 /**
471 * SyndicatedLink::nuke() deletes a subscription AND all of the
472 * posts syndicated through that subscription.
473 *
474 * @global $wpdb
475 * @uses wpdb::get_col
476 * @uses wp_delete_post
477 * @uses SyndicatedLink::delete
478 */
479 public function nuke () {
480 global $wpdb;
481
482 // Make a list of the items syndicated from this feed...
483 $post_ids = $wpdb->get_col($wpdb->prepare("
484 SELECT post_id FROM $wpdb->postmeta
485 WHERE meta_key = 'syndication_feed_id'
486 AND meta_value = '%s'
487 ", $this->id));
488
489 // ... and kill them all
490 if (count($post_ids) > 0) :
491 foreach ($post_ids as $post_id) :
492 // Force scrubbing of deleted post
493 // rather than sending to Trashcan
494 wp_delete_post(
495 /*postid=*/ $post_id,
496 /*force_delete=*/ true
497 );
498 endforeach;
499 endif;
500
501 $this->delete();
502 } /* SyndicatedLink::nuke () */
503
504 public function map_name_to_new_user ($name, $newuser_name) {
505 global $wpdb;
506
507 if (strlen($newuser_name) > 0) :
508 $newuser_id = fwp_insert_new_user($newuser_name);
509 if (is_numeric($newuser_id)) :
510 if (is_null($name)) : // Unfamiliar author
511 $this->update_setting('unfamiliar author', $newuser_id);
512 else :
513 $map = $this->setting('map authors');
514 $map['name'][$name] = $newuser_id;
515 $this->update_setting('map authors', $map);
516 endif;
517 else :
518 // TODO: Add some error detection and reporting
519 endif;
520 else :
521 // TODO: Add some error reporting
522 endif;
523 } /* SyndicatedLink::map_name_to_new_user () */
524
525 protected function imploded_settings () {
526 return array('cats', 'tags', 'match/cats', 'match/tags', 'match/filter');
527 } /* SyndicatedLink::imploded_settings () */
528
529 protected function get_settings_from_notes () {
530 // Read off feed settings from link_notes
531 $notes = explode("\n", $this->link->link_notes);
532 foreach ($notes as $note):
533 $pair = explode(": ", $note, 2);
534 $key = (isset($pair[0]) ? $pair[0] : null);
535 $value = (isset($pair[1]) ? $pair[1] : null);
536 if ( !is_null($key) and !is_null($value)) :
537 // Unescape and trim() off the whitespace.
538 // Thanks to Ray Lischner for pointing out the
539 // need to trim off whitespace.
540 $this->settings[$key] = stripcslashes (trim($value));
541 endif;
542 endforeach;
543
544 // "Magic" feed settings
545 $this->settings['link/uri'] = $this->link->link_rss;
546 $this->settings['link/name'] = $this->link->link_name;
547 $this->settings['link/id'] = $this->link->link_id;
548
549 // `hardcode categories` and `unfamiliar categories` are
550 // deprecated in favor of `unfamiliar category`
551 if (
552 isset($this->settings['unfamiliar categories'])
553 and !isset($this->settings['unfamiliar category'])
554 ) :
555 $this->settings['unfamiliar category'] = $this->settings['unfamiliar categories'];
556 endif;
557 if (
558 FeedWordPress::affirmative($this->settings, 'hardcode categories')
559 and !isset($this->settings['unfamiliar category'])
560 ) :
561 $this->settings['unfamiliar category'] = 'default';
562 endif;
563
564 // Set this up automagically for del.icio.us
565 $bits = parse_url($this->link->link_rss);
566 $tagspacers = array('del.icio.us', 'feeds.delicious.com');
567 if ( !isset($this->settings['cat_split']) and in_array($bits['host'], $tagspacers)) :
568 $this->settings['cat_split'] = '\s'; // Whitespace separates multiple tags in del.icio.us RSS feeds
569 endif;
570
571 // Simple lists
572 foreach ($this->imploded_settings() as $what) :
573 if (isset($this->settings[$what])):
574 $this->settings[$what] = explode(
575 FEEDWORDPRESS_CAT_SEPARATOR,
576 $this->settings[$what]
577 );
578 endif;
579 endforeach;
580
581 if (isset($this->settings['terms'])) :
582 // Look for new format
583 $this->settings['terms'] = maybe_unserialize($this->settings['terms']);
584
585 if ( !is_array($this->settings['terms'])) :
586 // Deal with old format instead. Ugh.
587
588 // Split on two *or more* consecutive breaks
589 // because in the old format, a taxonomy
590 // without any associated terms would
591 // produce tax_name#1\n\n\ntax_name#2\nterm,
592 // and the naive split on the first \n\n
593 // would screw up the tax_name#2 list.
594 //
595 // Props to David Morris for pointing this
596 // out.
597
598 $this->settings['terms'] = preg_split(
599 "/".FEEDWORDPRESS_CAT_SEPARATOR."{2,}/",
600 $this->settings['terms']
601 );
602 $terms = array();
603 foreach ($this->settings['terms'] as $line) :
604 $line = explode(FEEDWORDPRESS_CAT_SEPARATOR, $line);
605 $tax = array_shift($line);
606 $terms[$tax] = $line;
607 endforeach;
608 $this->settings['terms'] = $terms;
609 endif;
610 endif;
611
612 if (isset($this->settings['map authors'])) :
613 $author_rules = explode("\n\n", $this->settings['map authors']);
614 $ma = array();
615 foreach ($author_rules as $rule) :
616 list($rule_type, $author_name, $author_action) = explode("\n", $rule);
617
618 // Normalize for case and whitespace
619 $rule_type = strtolower(trim($rule_type));
620 $author_name = strtolower(trim($author_name));
621 $author_action = strtolower(trim($author_action));
622
623 $ma[$rule_type][$author_name] = $author_action;
624 endforeach;
625 $this->settings['map authors'] = $ma;
626 endif;
627
628 } /* SyndicatedLink::get_settings_from_notes () */
629
630 protected function settings_to_notes () {
631 $to_notes = $this->settings;
632
633 unset($to_notes['link/id']); // Magic setting; don't save
634 unset($to_notes['link/uri']); // Magic setting; don't save
635 unset($to_notes['link/name']); // Magic setting; don't save
636 unset($to_notes['hardcode categories']); // Deprecated
637 unset($to_notes['unfamiliar categories']); // Deprecated
638
639 // Collapse array settings
640 if (isset($to_notes['update/processed']) and (is_array($to_notes['update/processed']))) :
641 $to_notes['update/processed'] = implode("\n", $to_notes['update/processed']);
642 endif;
643
644 foreach ($this->imploded_settings() as $what) :
645 if (isset($to_notes[$what]) and is_array($to_notes[$what])) :
646 $to_notes[$what] = implode(
647 FEEDWORDPRESS_CAT_SEPARATOR,
648 $to_notes[$what]
649 );
650 endif;
651 endforeach;
652
653 if (isset($to_notes['terms']) and is_array($to_notes['terms'])) :
654 // Serialize it.
655 $to_notes['terms'] = serialize($to_notes['terms']);
656 endif;
657
658 // Collapse the author mapping rule structure back into a flat string
659 if (isset($to_notes['map authors'])) :
660 $ma = array();
661 foreach ($to_notes['map authors'] as $rule_type => $author_rules) :
662 foreach ($author_rules as $author_name => $author_action) :
663 $ma[] = $rule_type."\n".$author_name."\n".$author_action;
664 endforeach;
665 endforeach;
666 $to_notes['map authors'] = implode("\n\n", $ma);
667 endif;
668
669 $notes = '';
670 foreach ($to_notes as $key => $value) :
671 $notes .= $key . ": ". addcslashes($value, "\0..\37".'\\') . "\n";
672 endforeach;
673 return $notes;
674 } /* SyndicatedLink::settings_to_notes () */
675
676 public function save_settings ($reload = false) {
677 global $wpdb;
678
679 // Save channel-level meta-data
680 foreach (array('link_name', 'link_description', 'link_url') as $what) :
681 $alter[] = "{$what} = '".esc_sql($this->link->{$what})."'";
682 endforeach;
683
684 // Save settings to the notes field
685 $alter[] = "link_notes = '".esc_sql($this->settings_to_notes())."'";
686
687 // Update the properties of the link from settings changes, etc.
688 $update_set = implode(", ", $alter);
689
690 $result = $wpdb->query("
691 UPDATE $wpdb->links
692 SET $update_set
693 WHERE link_id='$this->id'
694 ");
695
696 if ( $result === false )
697 {
698 error_log("query failed: " . $wpdb->last_error);
699 }
700
701 if ($reload) :
702 // force reload of link information from DB
703 if (function_exists('clean_bookmark_cache')) :
704 clean_bookmark_cache($this->id);
705 endif;
706 endif;
707 } /* SyndicatedLink::save_settings () */
708
709 /**
710 * Retrieves the value of a setting, allowing for a global setting to be
711 * used as a fallback, or a constant value, or both.
712 *
713 * @param string $name The link setting key.
714 * @param mixed $fallback_global If the link setting is nonexistent or
715 * marked as a use-default value, fall back
716 * to the value of this global setting.
717 * @param mixed $fallback_value If the link setting and the global setting are
718 * nonexistent or marked as ause-default value,
719 * fall back to this constant value.
720 *
721 * @return mixed|null Should *not* return a boolean!!
722 */
723 public function setting( $name, $fallback_global = NULL, $fallback_value = NULL, $default = 'default' ) {
724 $ret = NULL;
725 if ( isset( $this->settings[ $name ] ) ) :
726 $ret = $this->settings[ $name ];
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_global ) ) :
735 // Avoid duplication of this correction
736 $fallback_global = preg_replace( '/^feedwordpress_/', '', $fallback_global );
737
738 // Occasionally we'll get an array back. Convert it to a string
739 if ( is_array( $fallback_global ) && sizeof( $fallback_global ) ) :
740 $fallback_global = reset( $fallback_global );
741 endif;
742
743 if ( ! empty( $fallback_global ) ) :
744 $ret = get_option( 'feedwordpress_' . $fallback_global, /*default=*/ NULL );
745 endif;
746 endif;
747
748 $no_value = (
749 is_null( $ret )
750 or ( is_string( $ret ) and strtolower( $ret ) == $default )
751 );
752
753 if ( $no_value and ! is_null( $fallback_value ) ) :
754 $ret = $fallback_value;
755 endif;
756 return $ret;
757 } /* SyndicatedLink::setting() */
758
759 /**
760 * Merge current settings with array of additional data.
761 *
762 * @param array $data Settings to merge with.
763 * @param string $prefix Hierarchical prefix (e.g. "feed/" as in "feed/a/b/c").
764 * @param string $separator Hierarchy separator (e.g., '/').
765 *
766 * @see SyndicatedLink::flatten_array()
767 */
768 public function merge_settings( $data, $prefix, $separator = '/' ) {
769 $dd = $this->flatten_array( $data, $prefix, $separator );
770 $this->settings = array_merge( $this->settings, $dd );
771 } /* SyndicatedLink::merge_settings () */
772
773 public function update_setting( $name, $value, $default = 'default' ) {
774 if ( !is_null( $value ) and $value != $default ) :
775 $this->settings[ $name ] = $value;
776 else : // Zap it.
777 unset( $this->settings[ $name ] );
778 endif;
779 } /* SyndicatedLink::update_setting () */
780
781 public function is_non_incremental () {
782 return ('complete'==$this->setting('update_incremental', 'update_incremental', 'incremental'));
783 } /* SyndicatedLink::is_non_incremental () */
784
785 public function get_feed_type () {
786 $type_code = $this->setting('link/feed_type');
787
788 // list derived from: <http://simplepie.org/api/class-SimplePie.html>, retrieved 2020/01/18
789 $bitmasks = array(
790 SIMPLEPIE_TYPE_RSS_090 => 'RSS 0.90',
791 SIMPLEPIE_TYPE_RSS_091_NETSCAPE => 'RSS 0.91 (Netscape)',
792 SIMPLEPIE_TYPE_RSS_091_USERLAND => 'RSS 0.91 (Userland)',
793 SIMPLEPIE_TYPE_RSS_091 => 'RSS 0.91',
794 SIMPLEPIE_TYPE_RSS_092 => 'RSS 0.92',
795 SIMPLEPIE_TYPE_RSS_093 => 'RSS 0.93',
796 SIMPLEPIE_TYPE_RSS_094 => 'RSS 0.94',
797 SIMPLEPIE_TYPE_RSS_10 => 'RSS 1.0',
798 SIMPLEPIE_TYPE_RSS_20 => 'RSS 2.0.x',
799 SIMPLEPIE_TYPE_RSS_RDF => 'RDF-based RSS',
800 SIMPLEPIE_TYPE_RSS_SYNDICATION => 'Non-RDF-based RSS',
801 SIMPLEPIE_TYPE_RSS_ALL => 'Any version of RSS',
802 SIMPLEPIE_TYPE_ATOM_03 => 'Atom 0.3',
803 SIMPLEPIE_TYPE_ATOM_10 => 'Atom 1.0',
804 SIMPLEPIE_TYPE_ATOM_ALL => 'Atom (any version)',
805 SIMPLEPIE_TYPE_ALL => 'Supported Feed (unspecified format)',
806 );
807
808 $type = "Unknown or unsupported format";
809 foreach ($bitmasks as $format_flag => $format_string) :
810 if (is_numeric($format_flag)) : // Guard against failure of constants to be defined.
811 if ($type_code & $format_flag) :
812 $type = $format_string;
813 break; // foreach
814 endif;
815 endif;
816 endforeach;
817
818 return $type;
819 } /* SyndicatedLink::get_feed_type () */
820
821 public function uri ($params = array()) {
822 $params = wp_parse_args($params, array(
823 'add_params' => false,
824 'fetch' => false,
825 ));
826
827 // Initialize $qp (= array for added query parameters, if any)
828 $qp = array();
829
830 $link_rss = (is_object($this->link) ? $this->link->link_rss : NULL);
831
832 // $link_rss stores the URI for the subscription as stored in the feed's record.
833 // $uri stores the effective URI of the request including any/all added query parameters
834 $uri = $link_rss;
835 if ( !is_null($uri) and strlen($uri) > 0 and $params['add_params']) :
836 $qp = maybe_unserialize($this->setting('query parameters', array()));
837
838 // For high-tech HTTP feed request kung fu
839 $qp = apply_filters('syndicated_feed_parameters', $qp, $uri, $this);
840
841 // $qp is an array of key-value pairs stored as arrays of format [$key, $value]
842 $q = array();
843 if (is_array($qp) and count($qp) > 0) :
844 foreach ($qp as $pair) :
845 $q[] = urlencode($pair[0]).'='.urlencode($pair[1]);
846 endforeach;
847
848 // Are we appending to a URI that already has params?
849 $sep = ((strpos($uri, "?")===false) ? '?' : '&');
850
851 // Tack it on
852 $uri .= $sep . implode("&", $q);
853 endif;
854 endif;
855
856 // Do we have any filters that apply here?
857 $uri = apply_filters('syndicated_link_uri', $uri, $link_rss, $qp, $params, $this);
858
859 // Return the filtered link URI.
860 return $uri;
861 } /* SyndicatedLink::uri () */
862
863 public function username () {
864 return $this->setting('http username', 'http_username', NULL);
865 } /* SyndicatedLink::username () */
866
867 public function password () {
868 return $this->setting('http password', 'http_password', NULL);
869 } /* SyndicatedLink::password () */
870
871 public function authentication_method () {
872 $auth = $this->setting('http auth method', NULL);
873 if (('-' == $auth) or (strlen($auth)==0)) :
874 $auth = NULL;
875 endif;
876 return $auth;
877 } /* SyndicatedLink::authentication_method () */
878
879 var $postmeta = array();
880 public function postmeta ($params = array()) {
881 $params = wp_parse_args($params, /*defaults=*/ array(
882 "field" => NULL,
883 "parsed" => false,
884 "force" => false,
885 ));
886
887 if ($params['force'] or !isset($this->postmeta[/*parsed = */ false])) :
888 // First, get the global settings.
889 $default_custom_settings = get_option('feedwordpress_custom_settings');
890 if ($default_custom_settings and !is_array($default_custom_settings)) :
891 $default_custom_settings = unserialize($default_custom_settings);
892 endif;
893 if ( !is_array($default_custom_settings)) :
894 $default_custom_settings = array();
895 endif;
896
897 // Next, get the settings for this particular feed.
898 $custom_settings = $this->setting('postmeta', NULL, NULL);
899 if ($custom_settings and !is_array($custom_settings)) :
900 $custom_settings = unserialize($custom_settings);
901 endif;
902 if ( !is_array($custom_settings)) :
903 $custom_settings = array();
904 endif;
905
906 $this->postmeta[/*parsed=*/ false] = array_merge($default_custom_settings, $custom_settings);
907 $this->postmeta[/*parsed=*/ true] = array();
908
909 // Now, run through and parse them all.
910 foreach ($this->postmeta[/*parsed=*/ false] as $key => $meta) :
911 $meta = apply_filters("syndicated_link_post_meta_{$key}_pre", $meta, $this);
912 $this->postmeta[/*parsed=*/ false][$key] = $meta;
913 $this->postmeta[/*parsed=*/ true][$key] = new FeedWordPressParsedPostMeta($meta);
914 endforeach;
915 endif;
916
917 $ret = $this->postmeta[!! $params['parsed']];
918 if (is_string($params['field'])) :
919 $ret = $ret[$params['field']];
920 endif;
921 return $ret;
922 } /* SyndicatedLink::postmeta () */
923
924 public function property_cascade ($fromFeed, $link_field, $setting, $method) {
925 $value = NULL;
926 if ($fromFeed) :
927 $value = $this->setting($setting, NULL, NULL, NULL);
928
929 $simp_pie = $this->simplepie;
930 $callable = ( is_object( $simp_pie ) and method_exists( $simp_pie, $method ) );
931 if ( is_null( $value ) and $callable ) :
932 // $fallback = $s->{$method}(); // what is fallback supposed to be here? (gwyneth 20230816)
933 $value = $simp_pie->{$method}(); // makes more sense this way!
934 endif;
935 else :
936 $value = $this->link->{$link_field};
937 endif;
938 return $value;
939 } /* SyndicatedLink::property_cascade () */
940
941 public function homepage ($fromFeed = true) {
942 return $this->property_cascade($fromFeed, 'link_url', 'feed/link', 'get_link');
943 } /* SyndicatedLink::homepage () */
944
945 public function name ($fromFeed = true) {
946 return $this->property_cascade($fromFeed, 'link_name', 'feed/title', 'get_title');
947 } /* SyndicatedLink::name () */
948
949 public function guid () {
950 $ret = $this->setting('feed/id', NULL, $this->uri());
951
952 // If we can get it live from the feed, do so.
953 if (is_object($this->simplepie)) :
954 $search = array(
955 array('', 'id'),
956 array(SIMPLEPIE_NAMESPACE_ATOM_10, 'id'),
957 array(SIMPLEPIE_NAMESPACE_ATOM_03, 'id'),
958 array(SIMPLEPIE_NAMESPACE_RSS_20, 'guid'),
959 array(SIMPLEPIE_NAMESPACE_DC_11, 'identifier'),
960 array(SIMPLEPIE_NAMESPACE_DC_10, 'identifier'),
961 );
962
963 foreach ($search as $pair) :
964 if ($id_tags = $this->simplepie->get_feed_tags($pair[0], $pair[1])) :
965 $ret = $id_tags[0]['data'];
966 break;
967 elseif ($id_tags = $this->simplepie->get_channel_tags($pair[0], $pair[1])) :
968 $ret = $id_tags[0]['data'];
969 break;
970 endif;
971 endforeach;
972 endif;
973 return $ret;
974 }
975
976 public function links ($params = array()) {
977 $params = wp_parse_args($params, array(
978 "rel" => NULL,
979 ));
980
981 $fLinks = array();
982 $search = array(
983 array('', 'link'),
984 array(SIMPLEPIE_NAMESPACE_ATOM_10, 'link'),
985 array(SIMPLEPIE_NAMESPACE_ATOM_03, 'link'),
986 );
987
988 foreach ($search as $pair) :
989 if ($link_tags = $this->simplepie->get_feed_tags($pair[0], $pair[1])) :
990 $fLinks = array_merge($fLinks, $link_tags);
991 endif;
992 if ($link_tags = $this->simplepie->get_channel_tags($pair[0], $pair[1])) :
993 $fLinks = array_merge($fLinks, $link_tags);
994 endif;
995 endforeach;
996
997 $ret = array();
998 foreach ($fLinks as $link) :
999 $filter = false;
1000 if ( !is_null($params['rel'])) :
1001 $filter = true;
1002
1003 if (isset($link['attribs'])) :
1004 // Get a list of NSes from the search
1005 foreach ($search as $pair) :
1006 $ns = $pair[0];
1007
1008 if (isset($link['attribs'][$ns])
1009 and isset($link['attribs'][$ns]['rel'])
1010 ) :
1011 $rel = strtolower(trim($link['attribs'][$ns]['rel']));
1012 $fRel = strtolower(trim($params['rel']));
1013
1014 if ($rel == $fRel) :
1015 $filter = false;
1016 endif;
1017 endif;
1018 endforeach;
1019 endif;
1020 endif;
1021
1022 if ( ! $filter) :
1023 $ret[] = $link;
1024 endif;
1025 endforeach;
1026
1027 return $ret;
1028 }
1029
1030 public function ttl ($return_element = false) {
1031 if (is_object($this->magpie)) :
1032 $channel = $this->magpie->channel;
1033 else :
1034 $channel = array();
1035 endif;
1036
1037 if (isset($channel['ttl'])) :
1038 // "ttl stands for time to live. It's a number of
1039 // minutes that indicates how long a channel can be
1040 // cached before refreshing from the source."
1041 // <http://blogs.law.harvard.edu/tech/rss#ltttlgtSubelementOfLtchannelgt>
1042 $xml = 'rss:ttl';
1043 $ret = $channel['ttl'];
1044 elseif (isset($channel['sy']['updatefrequency']) or isset($channel['sy']['updateperiod'])) :
1045 $period_minutes = array (
1046 'hourly' => 60, /* minutes in an hour */
1047 'daily' => 1440, /* minutes in a day */
1048 'weekly' => 10080, /* minutes in a week */
1049 'monthly' => 43200, /* minutes in a month */
1050 'yearly' => 525600, /* minutes in a year */
1051 );
1052
1053 // "sy:updatePeriod: Describes the period over which the
1054 // channel format is updated. Acceptable values are:
1055 // hourly, daily, weekly, monthly, yearly. If omitted,
1056 // daily is assumed." <http://web.resource.org/rss/1.0/modules/syndication/>
1057 if (isset($channel['sy']['updateperiod'])) : $period = trim($channel['sy']['updateperiod']);
1058 else : $period = 'daily';
1059 endif;
1060
1061 // "sy:updateFrequency: Used to describe the frequency
1062 // of updates in relation to the update period. A
1063 // positive integer indicates how many times in that
1064 // period the channel is updated. ... If omitted a value
1065 // of 1 is assumed." <http://web.resource.org/rss/1.0/modules/syndication/>
1066 if (isset($channel['sy']['updatefrequency'])) : $freq = (int) $channel['sy']['updatefrequency'];
1067 else : $freq = 1;
1068 endif;
1069
1070 // normalize the period name...
1071 $period = strtolower(trim($period));
1072
1073 // do we recognize the alphanumeric period name? if not, then guess
1074 // a responsible default, e.g. roughly hourly
1075 $mins = (isset($period_minutes[$period]) ? $period_minutes[$period] : 67);
1076
1077 $xml = 'sy:updateFrequency';
1078 $ret = (int) ($mins / $freq);
1079
1080 else :
1081 $xml = NULL;
1082 $ret = NULL;
1083 endif;
1084
1085 if ('yes'==$this->setting('update/minimum', 'update_minimum', 'no')) :
1086 $min = (int) $this->setting('update/window', 'update_window', DEFAULT_UPDATE_PERIOD);
1087
1088 if ($min > $ret) :
1089 $ret = NULL;
1090 endif;
1091 endif;
1092 return ( $return_element ? array( $ret, $xml ) : $ret );
1093 } /* SyndicatedLink::ttl() */
1094
1095 /**
1096 * Sets the automatic time interval for
1097 *
1098 * @return
1099 */
1100 public function automatic_ttl() {
1101 // spread out over a time interval for staggered updates
1102 $updateWindow = $this->setting( 'update/window', 'update_window', DEFAULT_UPDATE_PERIOD );
1103 if ( ! is_numeric( $updateWindow ) or ( $updateWindow < 1 ) ) :
1104 $updateWindow = DEFAULT_UPDATE_PERIOD;
1105 endif;
1106
1107 // We get a fudge of 1/3 of window from elsewhere. We'll do some more
1108 // fudging here.
1109 $fudgedInterval = $updateWindow + rand( - ( $updateWindow / 6 ), 5 * ( $updateWindow / 12 ) );
1110 return apply_filters( 'syndicated_feed_automatic_ttl', $fudgedInterval, $this );
1111 } /* SyndicatedLink::automatic_ttl() */
1112
1113 /**
1114 * SyndicatedLink::flatten_array (): flatten an array. Useful for
1115 * hierarchical and namespaced elements.
1116 *
1117 * Given an array which may contain array or object elements in it,
1118 * return a "flattened" array: a one-dimensional array of scalars
1119 * containing each of the scalar elements contained within the array
1120 * structure. Thus, for example, if $a['b']['c']['d'] == 'e', then the
1121 * returned array for FeedWordPress::flatten_array($a) will contain a key
1122 * $a['feed/b/c/d'] with value 'e'.
1123 *
1124 * @param array $arr Array to flatten.
1125 * @param string $prefix Hierarchical prefix (e.g. "feed/" as in "feed/a/b/c").
1126 * @param string $separator Hierarchy separator (e.g., '/').
1127 * @return array Flattened one-dimensional array.
1128 */
1129 public function flatten_array( $arr, $prefix = 'feed/', $separator = '/' ) {
1130 $ret = array();
1131 if ( is_array( $arr ) ) :
1132 foreach ( $arr as $key => $value ) :
1133 if ( is_scalar( $value ) ) :
1134 $ret[ $prefix.$key ] = $value;
1135 else :
1136 $ret = array_merge( $ret, $this->flatten_array( $value, $prefix.$key.$separator, $separator ) );
1137 endif;
1138 endforeach;
1139 endif;
1140 return $ret;
1141 } /* SyndicatedLink::flatten_array() */
1142
1143 /**
1144 * Check if a setting is hardcoded or not.
1145 *
1146 * @param string $what The setting we're checking for.
1147 *
1148 * @return bool True if the setting's value is 'yes', false otherwise.
1149 */
1150 public function hardcode( $what ) {
1151 return 'yes' == $this->setting( 'hardcode ' . $what, 'hardcode_' . $what, NULL );
1152 // Simplified logic. (gwyneth 20230920)
1153 } /* SyndicatedLink::hardcode () */
1154
1155 /**
1156 * Returns the syndicated status for one item.
1157 *
1158 * @param string $what Item's name.
1159 * @param mixed $default Default value for this item.
1160 * @param boolean $fallback If true, falls back to global setting.
1161 *
1162 * @return string SQL-ready escaped status.
1163 */
1164 public function syndicated_status( $what, $default, $fallback = true ) {
1165 $g_set = ( $fallback ? 'syndicated_' . $what . '_status' : NULL );
1166 $ret = $this->setting( $what . ' status', $g_set, $default );
1167
1168 return esc_sql( trim( strtolower( $ret ) ) );
1169 } /* SyndicatedLink:syndicated_status () */
1170
1171 public function taxonomies () {
1172 $post_type = $this->setting('syndicated post type', 'syndicated_post_type', 'post');
1173 return get_object_taxonomies(array('object_type' => $post_type), 'names');
1174 } /* SyndicatedLink::taxonomies () */
1175
1176 /**
1177 * category_ids: look up (and create) category ids from a list of
1178 * categories
1179 *
1180 * @param array $cats
1181 * @param string $unfamiliar_category
1182 * @param array|null $taxonomies
1183 * @return array
1184 */
1185 public function category_ids ($post, $cats, $unfamiliar_category = 'create', $taxonomies = NULL, $params = array()) {
1186 $singleton = (isset($params['singleton']) ? $params['singleton'] : true);
1187 $allowFilters = (isset($params['filters']) ? $params['filters'] : false);
1188
1189 $catTax = 'category';
1190
1191 if (is_null($taxonomies)) :
1192 $taxonomies = array('category');
1193 endif;
1194
1195 // We need to normalize whitespace because (1) trailing
1196 // whitespace can cause PHP and MySQL not to see eye to eye on
1197 // VARCHAR comparisons for some versions of MySQL (cf.
1198 // <http://dev.mysql.com/doc/mysql/en/char.html>), and (2)
1199 // because I doubt most people want to make a semantic
1200 // distinction between 'Computers' and 'Computers '
1201 $cats = array_map('trim', $cats);
1202
1203 $terms = array();
1204 foreach ($taxonomies as $tax) :
1205 $terms[$tax] = array();
1206 endforeach;
1207
1208 foreach ($cats as $cat_name) :
1209 if (strlen(trim($cat_name)) < 1) :
1210 continue;
1211 endif;
1212
1213 $oTerm = new SyndicatedPostTerm($cat_name, $taxonomies, $post);
1214
1215 if ($oTerm->is_familiar()) :
1216
1217 $tax = $oTerm->taxonomy();
1218 if ( !isset($terms[$tax])) :
1219 $terms[$tax] = array();
1220 endif;
1221 $terms[$tax][] = $oTerm->id();
1222
1223 else :
1224
1225 if ('tag'==$unfamiliar_category) :
1226 $unfamiliar_category = 'create:post_tag';
1227 endif;
1228
1229 if (preg_match('/^create(:(.*))?$/i', $unfamiliar_category, $ref)) :
1230 $tax = $catTax; // Default
1231
1232 if (isset($ref[2])
1233 and strlen($ref[2]) > 2) :
1234 $tax = $ref[2];
1235 endif;
1236
1237 $inserted = $oTerm->insert($tax);
1238 if ( !is_null($inserted)) :
1239 if ( !isset($terms[$tax])) :
1240 $terms[$tax] = array();
1241 endif;
1242 $terms[$tax][] = $inserted;
1243 else :
1244
1245 endif; // !is_null($inserted)
1246 endif; // preg_match(...)
1247
1248 endif; /* ($oTerm->is_familiar()) */
1249 endforeach;
1250
1251 $filtersOn = $allowFilters;
1252 if ($allowFilters) :
1253 $filters = array_filter(
1254 $this->setting('match/filter', 'match_filter', array()),
1255 'remove_dummy_zero'
1256 );
1257 $filtersOn = ($filtersOn and is_array($filters) and (count($filters) > 0));
1258 endif;
1259
1260 // Check for filter conditions
1261 foreach ($terms as $tax => $term_ids) :
1262 if ($filtersOn
1263 and (count($term_ids)==0)
1264 and in_array($tax, $filters)) :
1265 $terms = NULL; // Drop the post
1266 break;
1267 else :
1268 $terms[$tax] = array_unique($term_ids);
1269 endif;
1270 endforeach;
1271
1272 if ($singleton and count($terms)==1) : // If we only searched one, just return the term IDs
1273 $terms = end($terms);
1274 endif;
1275
1276 FeedWordPress::diagnostic(
1277 'syndicated_posts:categories',
1278 'Category: MAPPED term names '.json_encode($cats).' to IDs: '.json_encode($terms)
1279 );
1280 return $terms;
1281 } /* SyndicatedLink::category_ids () */
1282 } /* class SyndicatedLink */
1283
1284