PluginProbe
FeedWordPress / trunk
FeedWordPress vtrunk
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 trunk, at syndicatedlink.class.php

1,294 lines 40.7 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 $parts = array_pad(explode("\n", $rule), 3, '');
622 list($rule_type, $author_name, $author_action) = $parts;
623
624 // Normalize for case and whitespace
625 $rule_type = strtolower(trim($rule_type));
626 $author_name = strtolower(trim($author_name));
627 $author_action = strtolower(trim($author_action));
628
629 $ma[$rule_type][$author_name] = $author_action;
630 endforeach;
631 $this->settings['map authors'] = $ma;
632 endif;
633
634 } /* SyndicatedLink::get_settings_from_notes () */
635
636 protected function settings_to_notes () {
637 $to_notes = $this->settings;
638
639 unset($to_notes['link/id']); // Magic setting; don't save
640 unset($to_notes['link/uri']); // Magic setting; don't save
641 unset($to_notes['link/name']); // Magic setting; don't save
642 unset($to_notes['hardcode categories']); // Deprecated
643 unset($to_notes['unfamiliar categories']); // Deprecated
644
645 // Collapse array settings
646 if (isset($to_notes['update/processed']) and (is_array($to_notes['update/processed']))) :
647 $to_notes['update/processed'] = implode("\n", $to_notes['update/processed']);
648 endif;
649
650 foreach ($this->imploded_settings() as $what) :
651 if (isset($to_notes[$what]) and is_array($to_notes[$what])) :
652 $to_notes[$what] = implode(
653 FEEDWORDPRESS_CAT_SEPARATOR,
654 $to_notes[$what]
655 );
656 endif;
657 endforeach;
658
659 if (isset($to_notes['terms']) and is_array($to_notes['terms'])) :
660 // Serialize it.
661 $to_notes['terms'] = serialize($to_notes['terms']);
662 endif;
663
664 // Collapse the author mapping rule structure back into a flat string
665 if (isset($to_notes['map authors'])) :
666 $ma = array();
667 foreach ($to_notes['map authors'] as $rule_type => $author_rules) :
668 foreach ($author_rules as $author_name => $author_action) :
669 $ma[] = $rule_type."\n".$author_name."\n".$author_action;
670 endforeach;
671 endforeach;
672 $to_notes['map authors'] = implode("\n\n", $ma);
673 endif;
674
675 $notes = '';
676 foreach ($to_notes as $key => $value) :
677 $notes .= $key . ": ". addcslashes($value, "\0..\37".'\\') . "\n";
678 endforeach;
679 return $notes;
680 } /* SyndicatedLink::settings_to_notes () */
681
682 public function save_settings ($reload = false) {
683 global $wpdb;
684
685 // Save channel-level meta-data
686 foreach (array('link_name', 'link_description', 'link_url') as $what) :
687 $alter[] = "{$what} = '".esc_sql($this->link->{$what})."'";
688 endforeach;
689
690 // Save settings to the notes field
691 $alter[] = "link_notes = '".esc_sql($this->settings_to_notes())."'";
692
693 // Update the properties of the link from settings changes, etc.
694 $update_set = implode(", ", $alter);
695
696 $result = $wpdb->query("
697 UPDATE $wpdb->links
698 SET $update_set
699 WHERE link_id='$this->id'
700 ");
701
702 if ( $result === false )
703 {
704 error_log("query failed: " . $wpdb->last_error);
705 }
706
707 if ($reload) :
708 // force reload of link information from DB
709 if (function_exists('clean_bookmark_cache')) :
710 clean_bookmark_cache($this->id);
711 endif;
712 endif;
713 } /* SyndicatedLink::save_settings () */
714
715 /**
716 * Retrieves the value of a setting, allowing for a global setting to be
717 * used as a fallback, or a constant value, or both.
718 *
719 * @param string $name The link setting key.
720 * @param mixed $fallback_global If the link setting is nonexistent or
721 * marked as a use-default value, fall back
722 * to the value of this global setting.
723 * @param mixed $fallback_value If the link setting and the global setting are
724 * nonexistent or marked as ause-default value,
725 * fall back to this constant value.
726 *
727 * @return mixed|null Should *not* return a boolean!!
728 */
729 public function setting( $name, $fallback_global = NULL, $fallback_value = NULL, $default = 'default' ) {
730 $ret = NULL;
731 if ( isset( $this->settings[ $name ] ) ) :
732 $ret = $this->settings[ $name ];
733 endif;
734
735 $no_value = (
736 is_null( $ret )
737 or ( is_string( $ret ) and strtolower( $ret ) == $default )
738 );
739
740 if ( $no_value and ! is_null( $fallback_global ) ) :
741 // Avoid duplication of this correction
742 $fallback_global = preg_replace( '/^feedwordpress_/', '', $fallback_global );
743
744 // Occasionally we'll get an array back. Convert it to a string
745 if ( is_array( $fallback_global ) && sizeof( $fallback_global ) ) :
746 $fallback_global = reset( $fallback_global );
747 endif;
748
749 if ( ! empty( $fallback_global ) ) :
750 $ret = get_option( 'feedwordpress_' . $fallback_global, /*default=*/ NULL );
751 endif;
752 endif;
753
754 $no_value = (
755 is_null( $ret )
756 or ( is_string( $ret ) and strtolower( $ret ) == $default )
757 );
758
759 if ( $no_value and ! is_null( $fallback_value ) ) :
760 $ret = $fallback_value;
761 endif;
762 return $ret;
763 } /* SyndicatedLink::setting() */
764
765 /**
766 * Merge current settings with array of additional data.
767 *
768 * @param array $data Settings to merge with.
769 * @param string $prefix Hierarchical prefix (e.g. "feed/" as in "feed/a/b/c").
770 * @param string $separator Hierarchy separator (e.g., '/').
771 *
772 * @see SyndicatedLink::flatten_array()
773 */
774 public function merge_settings( $data, $prefix, $separator = '/' ) {
775 $dd = $this->flatten_array( $data, $prefix, $separator );
776 $this->settings = array_merge( $this->settings, $dd );
777 } /* SyndicatedLink::merge_settings () */
778
779 public function update_setting( $name, $value, $default = 'default' ) {
780 if ( !is_null( $value ) and $value != $default ) :
781 $this->settings[ $name ] = $value;
782 else : // Zap it.
783 unset( $this->settings[ $name ] );
784 endif;
785 } /* SyndicatedLink::update_setting () */
786
787 public function is_non_incremental () {
788 return ('complete'==$this->setting('update_incremental', 'update_incremental', 'incremental'));
789 } /* SyndicatedLink::is_non_incremental () */
790
791 public function get_feed_type () {
792 $type_code = $this->setting('link/feed_type');
793
794 // list derived from: <http://simplepie.org/api/class-SimplePie.html>, retrieved 2020/01/18
795 $bitmasks = array(
796 SIMPLEPIE_TYPE_RSS_090 => 'RSS 0.90',
797 SIMPLEPIE_TYPE_RSS_091_NETSCAPE => 'RSS 0.91 (Netscape)',
798 SIMPLEPIE_TYPE_RSS_091_USERLAND => 'RSS 0.91 (Userland)',
799 SIMPLEPIE_TYPE_RSS_091 => 'RSS 0.91',
800 SIMPLEPIE_TYPE_RSS_092 => 'RSS 0.92',
801 SIMPLEPIE_TYPE_RSS_093 => 'RSS 0.93',
802 SIMPLEPIE_TYPE_RSS_094 => 'RSS 0.94',
803 SIMPLEPIE_TYPE_RSS_10 => 'RSS 1.0',
804 SIMPLEPIE_TYPE_RSS_20 => 'RSS 2.0.x',
805 SIMPLEPIE_TYPE_RSS_RDF => 'RDF-based RSS',
806 SIMPLEPIE_TYPE_RSS_SYNDICATION => 'Non-RDF-based RSS',
807 SIMPLEPIE_TYPE_RSS_ALL => 'Any version of RSS',
808 SIMPLEPIE_TYPE_ATOM_03 => 'Atom 0.3',
809 SIMPLEPIE_TYPE_ATOM_10 => 'Atom 1.0',
810 SIMPLEPIE_TYPE_ATOM_ALL => 'Atom (any version)',
811 SIMPLEPIE_TYPE_ALL => 'Supported Feed (unspecified format)',
812 );
813
814 $type = "Unknown or unsupported format";
815 foreach ($bitmasks as $format_flag => $format_string) :
816 if (is_numeric($format_flag)) : // Guard against failure of constants to be defined.
817 if ($type_code & $format_flag) :
818 $type = $format_string;
819 break; // foreach
820 endif;
821 endif;
822 endforeach;
823
824 return $type;
825 } /* SyndicatedLink::get_feed_type () */
826
827 public function uri ($params = array()) {
828 $params = wp_parse_args($params, array(
829 'add_params' => false,
830 'fetch' => false,
831 ));
832
833 // Initialize $qp (= array for added query parameters, if any)
834 $qp = array();
835
836 $link_rss = (is_object($this->link) ? $this->link->link_rss : NULL);
837
838 // $link_rss stores the URI for the subscription as stored in the feed's record.
839 // $uri stores the effective URI of the request including any/all added query parameters
840 $uri = $link_rss;
841 if ( !is_null($uri) and strlen($uri) > 0 and $params['add_params']) :
842 $qp = maybe_unserialize($this->setting('query parameters', array()));
843
844 // For high-tech HTTP feed request kung fu
845 $qp = apply_filters('syndicated_feed_parameters', $qp, $uri, $this);
846
847 // $qp is an array of key-value pairs stored as arrays of format [$key, $value]
848 $q = array();
849 if (is_array($qp) and count($qp) > 0) :
850 foreach ($qp as $pair) :
851 $q[] = urlencode($pair[0]).'='.urlencode($pair[1]);
852 endforeach;
853
854 // Are we appending to a URI that already has params?
855 $sep = ((strpos($uri, "?")===false) ? '?' : '&');
856
857 // Tack it on
858 $uri .= $sep . implode("&", $q);
859 endif;
860 endif;
861
862 // Do we have any filters that apply here?
863 $uri = apply_filters('syndicated_link_uri', $uri, $link_rss, $qp, $params, $this);
864
865 // Return the filtered link URI.
866 return $uri;
867 } /* SyndicatedLink::uri () */
868
869 public function username () {
870 return $this->setting('http username', 'http_username', NULL);
871 } /* SyndicatedLink::username () */
872
873 public function password () {
874 return $this->setting('http password', 'http_password', NULL);
875 } /* SyndicatedLink::password () */
876
877 public function authentication_method () {
878 // Retrieve the authentication method from settings
879 $auth = $this->setting( 'http auth method', null );
880
881 // If the value is '-' or empty, treat it as NULL
882 if ( '-' === $auth || empty( $auth ) ) {
883 $auth = null;
884 }
885
886 return $auth;
887 } /* SyndicatedLink::authentication_method () */
888
889 var $postmeta = array();
890 public function postmeta ($params = array()) {
891 $params = wp_parse_args($params, /*defaults=*/ array(
892 "field" => NULL,
893 "parsed" => false,
894 "force" => false,
895 ));
896
897 if ($params['force'] or !isset($this->postmeta[/*parsed = */ false])) :
898 // First, get the global settings.
899 $default_custom_settings = get_option('feedwordpress_custom_settings');
900 if ($default_custom_settings and !is_array($default_custom_settings)) :
901 $default_custom_settings = unserialize($default_custom_settings);
902 endif;
903 if ( !is_array($default_custom_settings)) :
904 $default_custom_settings = array();
905 endif;
906
907 // Next, get the settings for this particular feed.
908 $custom_settings = $this->setting('postmeta', NULL, NULL);
909 if ($custom_settings and !is_array($custom_settings)) :
910 $custom_settings = unserialize($custom_settings);
911 endif;
912 if ( !is_array($custom_settings)) :
913 $custom_settings = array();
914 endif;
915
916 $this->postmeta[/*parsed=*/ false] = array_merge($default_custom_settings, $custom_settings);
917 $this->postmeta[/*parsed=*/ true] = array();
918
919 // Now, run through and parse them all.
920 foreach ($this->postmeta[/*parsed=*/ false] as $key => $meta) :
921 $meta = apply_filters("syndicated_link_post_meta_{$key}_pre", $meta, $this);
922 $this->postmeta[/*parsed=*/ false][$key] = $meta;
923 $this->postmeta[/*parsed=*/ true][$key] = new FeedWordPressParsedPostMeta($meta);
924 endforeach;
925 endif;
926
927 $ret = $this->postmeta[!! $params['parsed']];
928 if (is_string($params['field'])) :
929 $ret = $ret[$params['field']];
930 endif;
931 return $ret;
932 } /* SyndicatedLink::postmeta () */
933
934 public function property_cascade ($fromFeed, $link_field, $setting, $method) {
935 $value = NULL;
936 if ($fromFeed) :
937 $value = $this->setting($setting, NULL, NULL, NULL);
938
939 $simp_pie = $this->simplepie;
940 $callable = ( is_object( $simp_pie ) and method_exists( $simp_pie, $method ) );
941 if ( is_null( $value ) and $callable ) :
942 // $fallback = $s->{$method}(); // what is fallback supposed to be here? (gwyneth 20230816)
943 $value = $simp_pie->{$method}(); // makes more sense this way!
944 endif;
945 else :
946 $value = $this->link->{$link_field};
947 endif;
948 return $value;
949 } /* SyndicatedLink::property_cascade () */
950
951 public function homepage ($fromFeed = true) {
952 return $this->property_cascade($fromFeed, 'link_url', 'feed/link', 'get_link');
953 } /* SyndicatedLink::homepage () */
954
955 public function name ($fromFeed = true) {
956 return $this->property_cascade($fromFeed, 'link_name', 'feed/title', 'get_title');
957 } /* SyndicatedLink::name () */
958
959 public function guid () {
960 $ret = $this->setting('feed/id', NULL, $this->uri());
961
962 // If we can get it live from the feed, do so.
963 if (is_object($this->simplepie)) :
964 $search = array(
965 array('', 'id'),
966 array(SIMPLEPIE_NAMESPACE_ATOM_10, 'id'),
967 array(SIMPLEPIE_NAMESPACE_ATOM_03, 'id'),
968 array(SIMPLEPIE_NAMESPACE_RSS_20, 'guid'),
969 array(SIMPLEPIE_NAMESPACE_DC_11, 'identifier'),
970 array(SIMPLEPIE_NAMESPACE_DC_10, 'identifier'),
971 );
972
973 foreach ($search as $pair) :
974 if ($id_tags = $this->simplepie->get_feed_tags($pair[0], $pair[1])) :
975 $ret = $id_tags[0]['data'];
976 break;
977 elseif ($id_tags = $this->simplepie->get_channel_tags($pair[0], $pair[1])) :
978 $ret = $id_tags[0]['data'];
979 break;
980 endif;
981 endforeach;
982 endif;
983 return $ret;
984 }
985
986 public function links ($params = array()) {
987 $params = wp_parse_args($params, array(
988 "rel" => NULL,
989 ));
990
991 $fLinks = array();
992 $search = array(
993 array('', 'link'),
994 array(SIMPLEPIE_NAMESPACE_ATOM_10, 'link'),
995 array(SIMPLEPIE_NAMESPACE_ATOM_03, 'link'),
996 );
997
998 foreach ($search as $pair) :
999 if ($link_tags = $this->simplepie->get_feed_tags($pair[0], $pair[1])) :
1000 $fLinks = array_merge($fLinks, $link_tags);
1001 endif;
1002 if ($link_tags = $this->simplepie->get_channel_tags($pair[0], $pair[1])) :
1003 $fLinks = array_merge($fLinks, $link_tags);
1004 endif;
1005 endforeach;
1006
1007 $ret = array();
1008 foreach ($fLinks as $link) :
1009 $filter = false;
1010 if ( !is_null($params['rel'])) :
1011 $filter = true;
1012
1013 if (isset($link['attribs'])) :
1014 // Get a list of NSes from the search
1015 foreach ($search as $pair) :
1016 $ns = $pair[0];
1017
1018 if (isset($link['attribs'][$ns])
1019 and isset($link['attribs'][$ns]['rel'])
1020 ) :
1021 $rel = strtolower(trim($link['attribs'][$ns]['rel']));
1022 $fRel = strtolower(trim($params['rel']));
1023
1024 if ($rel == $fRel) :
1025 $filter = false;
1026 endif;
1027 endif;
1028 endforeach;
1029 endif;
1030 endif;
1031
1032 if ( ! $filter) :
1033 $ret[] = $link;
1034 endif;
1035 endforeach;
1036
1037 return $ret;
1038 }
1039
1040 public function ttl ($return_element = false) {
1041 if (is_object($this->magpie)) :
1042 $channel = $this->magpie->channel;
1043 else :
1044 $channel = array();
1045 endif;
1046
1047 if (isset($channel['ttl'])) :
1048 // "ttl stands for time to live. It's a number of
1049 // minutes that indicates how long a channel can be
1050 // cached before refreshing from the source."
1051 // <http://blogs.law.harvard.edu/tech/rss#ltttlgtSubelementOfLtchannelgt>
1052 $xml = 'rss:ttl';
1053 $ret = $channel['ttl'];
1054 elseif (isset($channel['sy']['updatefrequency']) or isset($channel['sy']['updateperiod'])) :
1055 $period_minutes = array (
1056 'hourly' => 60, /* minutes in an hour */
1057 'daily' => 1440, /* minutes in a day */
1058 'weekly' => 10080, /* minutes in a week */
1059 'monthly' => 43200, /* minutes in a month */
1060 'yearly' => 525600, /* minutes in a year */
1061 );
1062
1063 // "sy:updatePeriod: Describes the period over which the
1064 // channel format is updated. Acceptable values are:
1065 // hourly, daily, weekly, monthly, yearly. If omitted,
1066 // daily is assumed." <http://web.resource.org/rss/1.0/modules/syndication/>
1067 if (isset($channel['sy']['updateperiod'])) : $period = trim($channel['sy']['updateperiod']);
1068 else : $period = 'daily';
1069 endif;
1070
1071 // "sy:updateFrequency: Used to describe the frequency
1072 // of updates in relation to the update period. A
1073 // positive integer indicates how many times in that
1074 // period the channel is updated. ... If omitted a value
1075 // of 1 is assumed." <http://web.resource.org/rss/1.0/modules/syndication/>
1076 if (isset($channel['sy']['updatefrequency'])) : $freq = (int) $channel['sy']['updatefrequency'];
1077 else : $freq = 1;
1078 endif;
1079
1080 // normalize the period name...
1081 $period = strtolower(trim($period));
1082
1083 // do we recognize the alphanumeric period name? if not, then guess
1084 // a responsible default, e.g. roughly hourly
1085 $mins = (isset($period_minutes[$period]) ? $period_minutes[$period] : 67);
1086
1087 $xml = 'sy:updateFrequency';
1088 $ret = (int) ($mins / $freq);
1089
1090 else :
1091 $xml = NULL;
1092 $ret = NULL;
1093 endif;
1094
1095 if ('yes'==$this->setting('update/minimum', 'update_minimum', 'no')) :
1096 $min = (int) $this->setting('update/window', 'update_window', DEFAULT_UPDATE_PERIOD);
1097
1098 if ($min > $ret) :
1099 $ret = NULL;
1100 endif;
1101 endif;
1102 return ( $return_element ? array( $ret, $xml ) : $ret );
1103 } /* SyndicatedLink::ttl() */
1104
1105 /**
1106 * Sets the automatic time interval for
1107 *
1108 * @return
1109 */
1110 public function automatic_ttl() {
1111 // spread out over a time interval for staggered updates
1112 $updateWindow = $this->setting( 'update/window', 'update_window', DEFAULT_UPDATE_PERIOD );
1113 if ( ! is_numeric( $updateWindow ) or ( $updateWindow < 1 ) ) :
1114 $updateWindow = DEFAULT_UPDATE_PERIOD;
1115 endif;
1116
1117 // We get a fudge of 1/3 of window from elsewhere. We'll do some more
1118 // fudging here.
1119 $fudgedInterval = $updateWindow + rand( - ( $updateWindow / 6 ), 5 * ( $updateWindow / 12 ) );
1120 return apply_filters( 'syndicated_feed_automatic_ttl', $fudgedInterval, $this );
1121 } /* SyndicatedLink::automatic_ttl() */
1122
1123 /**
1124 * SyndicatedLink::flatten_array (): flatten an array. Useful for
1125 * hierarchical and namespaced elements.
1126 *
1127 * Given an array which may contain array or object elements in it,
1128 * return a "flattened" array: a one-dimensional array of scalars
1129 * containing each of the scalar elements contained within the array
1130 * structure. Thus, for example, if $a['b']['c']['d'] == 'e', then the
1131 * returned array for FeedWordPress::flatten_array($a) will contain a key
1132 * $a['feed/b/c/d'] with value 'e'.
1133 *
1134 * @param array $arr Array to flatten.
1135 * @param string $prefix Hierarchical prefix (e.g. "feed/" as in "feed/a/b/c").
1136 * @param string $separator Hierarchy separator (e.g., '/').
1137 * @return array Flattened one-dimensional array.
1138 */
1139 public function flatten_array( $arr, $prefix = 'feed/', $separator = '/' ) {
1140 $ret = array();
1141 if ( is_array( $arr ) ) :
1142 foreach ( $arr as $key => $value ) :
1143 if ( is_scalar( $value ) ) :
1144 $ret[ $prefix.$key ] = $value;
1145 else :
1146 $ret = array_merge( $ret, $this->flatten_array( $value, $prefix.$key.$separator, $separator ) );
1147 endif;
1148 endforeach;
1149 endif;
1150 return $ret;
1151 } /* SyndicatedLink::flatten_array() */
1152
1153 /**
1154 * Check if a setting is hardcoded or not.
1155 *
1156 * @param string $what The setting we're checking for.
1157 *
1158 * @return bool True if the setting's value is 'yes', false otherwise.
1159 */
1160 public function hardcode( $what ) {
1161 return 'yes' == $this->setting( 'hardcode ' . $what, 'hardcode_' . $what, NULL );
1162 // Simplified logic. (gwyneth 20230920)
1163 } /* SyndicatedLink::hardcode () */
1164
1165 /**
1166 * Returns the syndicated status for one item.
1167 *
1168 * @param string $what Item's name.
1169 * @param mixed $default Default value for this item.
1170 * @param boolean $fallback If true, falls back to global setting.
1171 *
1172 * @return string SQL-ready escaped status.
1173 */
1174 public function syndicated_status( $what, $default, $fallback = true ) {
1175 $g_set = ( $fallback ? 'syndicated_' . $what . '_status' : NULL );
1176 $ret = $this->setting( $what . ' status', $g_set, $default );
1177
1178 return esc_sql( trim( strtolower( $ret ) ) );
1179 } /* SyndicatedLink:syndicated_status () */
1180
1181 public function taxonomies () {
1182 $post_type = $this->setting('syndicated post type', 'syndicated_post_type', 'post');
1183 return get_object_taxonomies(array('object_type' => $post_type), 'names');
1184 } /* SyndicatedLink::taxonomies () */
1185
1186 /**
1187 * category_ids: look up (and create) category ids from a list of
1188 * categories
1189 *
1190 * @param array $cats
1191 * @param string $unfamiliar_category
1192 * @param array|null $taxonomies
1193 * @return array
1194 */
1195 public function category_ids ($post, $cats, $unfamiliar_category = 'create', $taxonomies = NULL, $params = array()) {
1196 $singleton = (isset($params['singleton']) ? $params['singleton'] : true);
1197 $allowFilters = (isset($params['filters']) ? $params['filters'] : false);
1198
1199 $catTax = 'category';
1200
1201 if (is_null($taxonomies)) :
1202 $taxonomies = array('category');
1203 endif;
1204
1205 // We need to normalize whitespace because (1) trailing
1206 // whitespace can cause PHP and MySQL not to see eye to eye on
1207 // VARCHAR comparisons for some versions of MySQL (cf.
1208 // <http://dev.mysql.com/doc/mysql/en/char.html>), and (2)
1209 // because I doubt most people want to make a semantic
1210 // distinction between 'Computers' and 'Computers '
1211 $cats = array_map('trim', $cats);
1212
1213 $terms = array();
1214 foreach ($taxonomies as $tax) :
1215 $terms[$tax] = array();
1216 endforeach;
1217
1218 foreach ($cats as $cat_name) :
1219 if (strlen(trim($cat_name)) < 1) :
1220 continue;
1221 endif;
1222
1223 $oTerm = new SyndicatedPostTerm($cat_name, $taxonomies, $post);
1224
1225 if ($oTerm->is_familiar()) :
1226
1227 $tax = $oTerm->taxonomy();
1228 if ( !isset($terms[$tax])) :
1229 $terms[$tax] = array();
1230 endif;
1231 $terms[$tax][] = $oTerm->id();
1232
1233 else :
1234
1235 if ('tag'==$unfamiliar_category) :
1236 $unfamiliar_category = 'create:post_tag';
1237 endif;
1238
1239 if (preg_match('/^create(:(.*))?$/i', $unfamiliar_category, $ref)) :
1240 $tax = $catTax; // Default
1241
1242 if (isset($ref[2])
1243 and strlen($ref[2]) > 2) :
1244 $tax = $ref[2];
1245 endif;
1246
1247 $inserted = $oTerm->insert($tax);
1248 if ( !is_null($inserted)) :
1249 if ( !isset($terms[$tax])) :
1250 $terms[$tax] = array();
1251 endif;
1252 $terms[$tax][] = $inserted;
1253 else :
1254
1255 endif; // !is_null($inserted)
1256 endif; // preg_match(...)
1257
1258 endif; /* ($oTerm->is_familiar()) */
1259 endforeach;
1260
1261 $filtersOn = $allowFilters;
1262 if ($allowFilters) :
1263 $filters = array_filter(
1264 $this->setting('match/filter', 'match_filter', array()),
1265 'remove_dummy_zero'
1266 );
1267 $filtersOn = ($filtersOn and is_array($filters) and (count($filters) > 0));
1268 endif;
1269
1270 // Check for filter conditions
1271 foreach ($terms as $tax => $term_ids) :
1272 if ($filtersOn
1273 and (count($term_ids)==0)
1274 and in_array($tax, $filters)) :
1275 $terms = NULL; // Drop the post
1276 break;
1277 else :
1278 $terms[$tax] = array_unique($term_ids);
1279 endif;
1280 endforeach;
1281
1282 if ($singleton and count($terms)==1) : // If we only searched one, just return the term IDs
1283 $terms = end($terms);
1284 endif;
1285
1286 FeedWordPress::diagnostic(
1287 'syndicated_posts:categories',
1288 'Category: MAPPED term names '.json_encode($cats).' to IDs: '.json_encode($terms)
1289 );
1290 return $terms;
1291 } /* SyndicatedLink::category_ids () */
1292 } /* class SyndicatedLink */
1293
1294