PluginProbe
FeedWordPress / 2010.0531
FeedWordPress v2010.0531
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 / syndicatedpost.class.php

syndicatedpost.class.php in FeedWordPress 2010.0531, at syndicatedpost.class.php

1,705 lines 59.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 require_once(dirname(__FILE__).'/feedtime.class.php');
3
4 /**
5 * class SyndicatedPost: FeedWordPress uses to manage the conversion of
6 * incoming items from the feed parser into posts for the WordPress
7 * database. It contains several internal management methods primarily
8 * of interest to someone working on the FeedWordPress source, as well
9 * as some utility methods for extracting useful data from many
10 * different feed formats, which may be useful to FeedWordPress users
11 * who make use of feed data in PHP add-ons and filters.
12 *
13 * @version 2010.0531
14 */
15 class SyndicatedPost {
16 var $item = null; // MagpieRSS representation
17 var $entry = null; // SimplePie_Item representation
18
19 var $link = null;
20 var $feed = null;
21 var $feedmeta = null;
22
23 var $xmlns = array ();
24
25 var $post = array ();
26
27 var $_freshness = null;
28 var $_wp_id = null;
29
30 /**
31 * SyndicatedPost constructor: Given a feed item and the source from
32 * which it was taken, prepare a post that can be inserted into the
33 * WordPress database on request, or updated in place if it has already
34 * been syndicated.
35 *
36 * @param array $item The item syndicated from the feed.
37 * @param SyndicatedLink $source The feed it was syndicated from.
38 */
39 function SyndicatedPost ($item, $source) {
40 global $wpdb;
41
42 if (is_array($item)
43 and isset($item['simplepie'])
44 and isset($item['magpie'])) :
45 $this->entry = $item['simplepie'];
46 $this->item = $item['magpie'];
47 $item = $item['magpie'];
48 else :
49 $this->item = $item;
50 endif;
51
52 FeedWordPress::diagnostic('feed_items', 'Considering item ['.$this->guid().'] "'.$this->entry->get_title().'"');
53
54 $this->link = $source;
55 $this->feed = $source->magpie;
56 $this->feedmeta = $source->settings;
57
58 # Dealing with namespaces can get so fucking fucked.
59 $this->xmlns['forward'] = $source->magpie->_XMLNS_FAMILIAR;
60 $this->xmlns['reverse'] = array();
61 foreach ($this->xmlns['forward'] as $url => $ns) :
62 if (!isset($this->xmlns['reverse'][$ns])) :
63 $this->xmlns['reverse'][$ns] = array();
64 endif;
65 $this->xmlns['reverse'][$ns][] = $url;
66 endforeach;
67
68 // Fucking SimplePie.
69 $this->xmlns['reverse']['rss'][] = '';
70
71 # These globals were originally an ugly kludge around a bug in
72 # apply_filters from WordPress 1.5. The bug was fixed in 1.5.1,
73 # and I sure hope at this point that nobody writing filters for
74 # FeedWordPress is still relying on them.
75 #
76 # Anyway, I hereby declare them DEPRECATED as of 8 February
77 # 2010. I'll probably remove the globals within 1-2 releases in
78 # the interests of code hygiene and memory usage. If you
79 # currently use them in your filters, I advise you switch off to
80 # accessing the public members SyndicatedPost::feed and
81 # SyndicatedPost::feedmeta.
82
83 global $fwp_channel, $fwp_feedmeta;
84 $fwp_channel = $this->feed; $fwp_feedmeta = $this->feedmeta;
85
86 // Trigger global syndicated_item filter.
87 $this->item = apply_filters('syndicated_item', $this->item, $this);
88
89 // Allow for feed-specific syndicated_item filters.
90 $this->item = apply_filters(
91 "syndicated_item_".$source->uri(),
92 $this->item,
93 $this
94 );
95
96 # Filters can halt further processing by returning NULL
97 if (is_null($this->item)) :
98 $this->post = NULL;
99 else :
100 # Note that nothing is run through $wpdb->escape() here.
101 # That's deliberate. The escaping is done at the point
102 # of insertion, not here, to avoid double-escaping and
103 # to avoid screwing with syndicated_post filters
104
105 $this->post['post_title'] = apply_filters(
106 'syndicated_item_title',
107 $this->entry->get_title(), $this
108 );
109
110 $this->post['named']['author'] = apply_filters(
111 'syndicated_item_author',
112 $this->author(), $this
113 );
114 // This just gives us an alphanumeric name for the author.
115 // We look up (or create) the numeric ID for the author
116 // in SyndicatedPost::add().
117
118 $this->post['post_content'] = apply_filters(
119 'syndicated_item_content',
120 $this->content(), $this
121 );
122
123 $excerpt = apply_filters('syndicated_item_excerpt', $this->excerpt(), $this);
124 if (!is_null($excerpt)):
125 $this->post['post_excerpt'] = $excerpt;
126 endif;
127
128 $this->post['epoch']['issued'] = apply_filters('syndicated_item_published', $this->published(), $this);
129 $this->post['epoch']['created'] = apply_filters('syndicated_item_created', $this->created(), $this);
130 $this->post['epoch']['modified'] = apply_filters('syndicated_item_updated', $this->updated(), $this);
131
132 // Dealing with timestamps in WordPress is so fucking fucked.
133 $offset = (int) get_option('gmt_offset') * 60 * 60;
134 $this->post['post_date'] = gmdate('Y-m-d H:i:s', $this->published(/*fallback=*/ true, /*default=*/ -1) + $offset);
135 $this->post['post_modified'] = gmdate('Y-m-d H:i:s', $this->updated(/*fallback=*/ true, /*default=*/ -1) + $offset);
136 $this->post['post_date_gmt'] = gmdate('Y-m-d H:i:s', $this->published(/*fallback=*/ true, /*default=*/ -1));
137 $this->post['post_modified_gmt'] = gmdate('Y-m-d H:i:s', $this->updated(/*fallback=*/ true, /*default=*/ -1));
138
139 // Use feed-level preferences or the global default.
140 $this->post['post_status'] = $this->link->syndicated_status('post', 'publish');
141 $this->post['comment_status'] = $this->link->syndicated_status('comment', 'closed');
142 $this->post['ping_status'] = $this->link->syndicated_status('ping', 'closed');
143
144 // Unique ID (hopefully a unique tag: URI); failing that, the permalink
145 $this->post['guid'] = apply_filters('syndicated_item_guid', $this->guid(), $this);
146
147 // User-supplied custom settings to apply to each post. Do first so that FWP-generated custom settings will overwrite if necessary; thus preventing any munging
148 $default_custom_settings = get_option('feedwordpress_custom_settings');
149 if ($default_custom_settings and !is_array($default_custom_settings)) :
150 $default_custom_settings = unserialize($default_custom_settings);
151 endif;
152 if (!is_array($default_custom_settings)) :
153 $default_custom_settings = array();
154 endif;
155
156 $custom_settings = (isset($this->link->settings['postmeta']) ? $this->link->settings['postmeta'] : null);
157 if ($custom_settings and !is_array($custom_settings)) :
158 $custom_settings = unserialize($custom_settings);
159 endif;
160 if (!is_array($custom_settings)) :
161 $custom_settings = array();
162 endif;
163
164 $postMetaIn = array_merge($default_custom_settings, $custom_settings);
165 $postMetaOut = array();
166
167 // Big ugly fuckin loop to do any element substitutions
168 // that we may need.
169 foreach ($postMetaIn as $key => $values) :
170 if (is_string($values)) : $values = array($values); endif;
171
172 $postMetaOut[$key] = array();
173 foreach ($values as $value) :
174 if (preg_match('/\$\( ([^)]+) \)/x', $value, $ref)) :
175 $elements = $this->query($ref[1]);
176 foreach ($elements as $element) :
177 $postMetaOut[$key][] = str_replace(
178 $ref[0],
179 $element,
180 $value
181 );
182 endforeach;
183 else :
184 $postMetaOut[$key][] = $value;
185 endif;
186 endforeach;
187 endforeach;
188
189 foreach ($postMetaOut as $key => $values) :
190 $this->post['meta'][$key] = array();
191 foreach ($values as $value) :
192 $this->post['meta'][$key][] = apply_filters("syndicated_post_meta_{$key}", $value, $this);
193 endforeach;
194 endforeach;
195
196 // RSS 2.0 / Atom 1.0 enclosure support
197 $enclosures = $this->entry->get_enclosures();
198 if (is_array($enclosures)) : foreach ($enclosures as $enclosure) :
199 $this->post['meta']['enclosure'][] =
200 apply_filters('syndicated_item_enclosure_url', $enclosure->get_link(), $this)."\n".
201 apply_filters('syndicated_item_enclosure_length', $enclosure->get_length(), $this)."\n".
202 apply_filters('syndicated_item_enclosure_type', $enclosure->get_type(), $this);
203 endforeach; endif;
204
205 // In case you want to point back to the blog this was syndicated from
206 if (isset($this->feed->channel['title'])) :
207 $this->post['meta']['syndication_source'] = apply_filters('syndicated_item_source_title', $this->feed->channel['title'], $this);
208 endif;
209
210 if (isset($this->feed->channel['link'])) :
211 $this->post['meta']['syndication_source_uri'] = apply_filters('syndicated_item_source_link', $this->feed->channel['link'], $this);
212 endif;
213
214 // Make use of atom:source data, if present in an aggregated feed
215 if (isset($this->item['source_title'])) :
216 $this->post['meta']['syndication_source_original'] = $this->item['source_title'];
217 endif;
218
219 if (isset($this->item['source_link'])) :
220 $this->post['meta']['syndication_source_uri_original'] = $this->item['source_link'];
221 endif;
222
223 if (isset($this->item['source_id'])) :
224 $this->post['meta']['syndication_source_id_original'] = $this->item['source_id'];
225 endif;
226
227 // Store information on human-readable and machine-readable comment URIs
228
229 // Human-readable comment URI
230 $commentLink = apply_filters('syndicated_item_comments', $this->comment_link(), $this);
231 if (!is_null($commentLink)) : $this->post['meta']['rss:comments'] = $commentLink; endif;
232
233 // Machine-readable content feed URI
234 $commentFeed = apply_filters('syndicated_item_commentrss', $this->comment_feed(), $this);
235 if (!is_null($commentFeed)) : $this->post['meta']['wfw:commentRSS'] = $commentFeed; endif;
236 // Yeah, yeah, now I know that it's supposed to be
237 // wfw:commentRss. Oh well. Path dependence, sucka.
238
239 // Store information to identify the feed that this came from
240 if (isset($this->feedmeta['link/uri'])) :
241 $this->post['meta']['syndication_feed'] = $this->feedmeta['link/uri'];
242 endif;
243 if (isset($this->feedmeta['link/id'])) :
244 $this->post['meta']['syndication_feed_id'] = $this->feedmeta['link/id'];
245 endif;
246
247 if (isset($this->item['source_link_self'])) :
248 $this->post['meta']['syndication_feed_original'] = $this->item['source_link_self'];
249 endif;
250
251 // In case you want to know the external permalink...
252 $this->post['meta']['syndication_permalink'] = apply_filters('syndicated_item_link', $this->permalink());
253
254 // Store a hash of the post content for checking whether something needs to be updated
255 $this->post['meta']['syndication_item_hash'] = $this->update_hash();
256
257 // Feed-by-feed options for author and category creation
258 $this->post['named']['unfamiliar']['author'] = (isset($this->feedmeta['unfamiliar author']) ? $this->feedmeta['unfamiliar author'] : null);
259 $this->post['named']['unfamiliar']['category'] = (isset($this->feedmeta['unfamiliar category']) ? $this->feedmeta['unfamiliar category'] : null);
260
261 // Categories: start with default categories, if any
262 $fc = get_option("feedwordpress_syndication_cats");
263 if ($fc) :
264 $this->post['named']['preset/category'] = explode("\n", $fc);
265 else :
266 $this->post['named']['preset/category'] = array();
267 endif;
268
269 if (isset($this->feedmeta['cats']) and is_array($this->feedmeta['cats'])) :
270 $this->post['named']['preset/category'] = array_merge($this->post['named']['preset/category'], $this->feedmeta['cats']);
271 endif;
272
273 // Now add categories from the post, if we have 'em
274 $this->post['named']['category'] = array();
275 if ( isset($this->item['category#']) ) :
276 for ($i = 1; $i <= $this->item['category#']; $i++) :
277 $cat_idx = (($i > 1) ? "#{$i}" : "");
278 $cat = $this->item["category{$cat_idx}"];
279
280 if ( isset($this->feedmeta['cat_split']) and strlen($this->feedmeta['cat_split']) > 0) :
281 $pcre = "\007".$this->feedmeta['cat_split']."\007";
282 $this->post['named']['category'] = array_merge($this->post['named']['category'], preg_split($pcre, $cat, -1 /*=no limit*/, PREG_SPLIT_NO_EMPTY));
283 else :
284 $this->post['named']['category'][] = $cat;
285 endif;
286 endfor;
287 endif;
288 $this->post['named']['category'] = apply_filters('syndicated_item_categories', $this->post['named']['category'], $this);
289
290 // Tags: start with default tags, if any
291 $ft = get_option("feedwordpress_syndication_tags");
292 if ($ft) :
293 $this->post['tags_input'] = explode(FEEDWORDPRESS_CAT_SEPARATOR, $ft);
294 else :
295 $this->post['tags_input'] = array();
296 endif;
297
298 if (isset($this->feedmeta['tags']) and is_array($this->feedmeta['tags'])) :
299 $this->post['tags_input'] = array_merge($this->post['tags_input'], $this->feedmeta['tags']);
300 endif;
301 $this->post['tags_input'] = apply_filters('syndicated_item_tags', $this->post['tags_input'], $this);
302 endif;
303 } /* SyndicatedPost::SyndicatedPost() */
304
305 #####################################
306 #### EXTRACT DATA FROM FEED ITEM ####
307 #####################################
308
309 /**
310 * SyndicatedPost::query uses an XPath-like syntax to query arbitrary
311 * elements within the syndicated item.
312 *
313 * @param string $path
314 * @returns array of string values representing contents of matching
315 * elements or attributes
316 */
317 function query ($path) {
318 $urlHash = array();
319
320 // Allow {url} notation for namespaces. URLs will contain : and /, so...
321 preg_match_all('/{([^}]+)}/', $path, $match, PREG_SET_ORDER);
322 foreach ($match as $ref) :
323 $urlHash[md5($ref[1])] = $ref[1];
324 endforeach;
325
326 foreach ($urlHash as $hash => $url) :
327 $path = str_replace('{'.$url.'}', '{#'.$hash.'}', $path);
328 endforeach;
329
330 $path = explode('/', $path);
331 foreach ($path as $index => $node) :
332 if (preg_match('/{#([^}]+)}/', $node, $ref)) :
333 if (isset($urlHash[$ref[1]])) :
334 $path[$index] = str_replace(
335 '{#'.$ref[1].'}',
336 '{'.$urlHash[$ref[1]].'}',
337 $node
338 );
339 endif;
340 endif;
341 endforeach;
342
343 // Start out with a get_item_tags query.
344 $node = '';
345 while (strlen($node)==0 and !is_null($node)) :
346 $node = array_shift($path);
347 endwhile;
348
349 switch ($node) :
350 case 'feed' :
351 case 'channel' :
352 $method = "get_${node}_tags";
353 $node = array_shift($path);
354 break;
355 case 'item' :
356 $node = array_shift($path);
357 default :
358 $method = NULL;
359 endswitch;
360
361 $data = array();
362 if (!is_null($node)) :
363 list($namespaces, $element) = $this->xpath_extended_name($node);
364
365 $matches = array();
366 foreach ($namespaces as $ns) :
367 if (!is_null($method)) :
368 $el = $this->link->simplepie->{$method}($ns, $element);
369 else :
370 $el = $this->entry->get_item_tags($ns, $element);
371 endif;
372
373 if (!is_null($el)) :
374 $matches = array_merge($matches, $el);
375 endif;
376 endforeach;
377 $data = $matches;
378
379 $node = array_shift($path);
380 endif;
381
382 while (!is_null($node)) :
383 if (strlen($node) > 0) :
384 $matches = array();
385
386 list($ns, $element) = $this->xpath_extended_name($node);
387
388 if (preg_match('/^@(.*)$/', $element, $ref)) :
389 $element = $ref[1];
390 $axis = 'attribs';
391 else :
392 $axis = 'child';
393 endif;
394
395 foreach ($data as $datum) :
396 foreach ($namespaces as $ns) :
397 if (!is_string($datum)
398 and isset($datum[$axis][$ns][$element])) :
399 if (is_string($datum[$axis][$ns][$element])) :
400 $matches[] = $datum[$axis][$ns][$element];
401 else :
402 $matches = array_merge($matches, $datum[$axis][$ns][$element]);
403 endif;
404 endif;
405 endforeach;
406 endforeach;
407
408 $data = $matches;
409 endif;
410 $node = array_shift($path);
411 endwhile;
412
413 $matches = array();
414 foreach ($data as $datum) :
415 if (is_string($datum)) :
416 $matches[] = $datum;
417 elseif (isset($datum['data'])) :
418 $matches[] = $datum['data'];
419 endif;
420 endforeach;
421 return $matches;
422 } /* SyndicatedPost::query() */
423
424 function xpath_default_namespace () {
425 // Get the default namespace.
426 $type = $this->link->simplepie->get_type();
427 if ($type & SIMPLEPIE_TYPE_ATOM_10) :
428 $defaultNS = SIMPLEPIE_NAMESPACE_ATOM_10;
429 elseif ($type & SIMPLEPIE_TYPE_ATOM_03) :
430 $defaultNS = SIMPLEPIE_NAMESPACE_ATOM_03;
431 elseif ($type & SIMPLEPIE_TYPE_RSS_090) :
432 $defaultNS = SIMPLEPIE_NAMESPACE_RSS_090;
433 elseif ($type & SIMPLEPIE_TYPE_RSS_10) :
434 $defaultNS = SIMPLEPIE_NAMESPACE_RSS_10;
435 elseif ($type & SIMPLEPIE_TYPE_RSS_20) :
436 $defaultNS = SIMPLEPIE_NAMESPACE_RSS_20;
437 else :
438 $defaultNS = SIMPLEPIE_NAMESPACE_RSS_20;
439 endif;
440 return $defaultNS;
441 } /* SyndicatedPost::xpath_default_namespace() */
442
443 function xpath_extended_name ($node) {
444 $ns = NULL; $element = NULL;
445
446 if (substr($node, 0, 1)=='@') :
447 $attr = '@'; $node = substr($node, 1);
448 else :
449 $attr = '';
450 endif;
451
452 if (preg_match('/^{([^}]*)}(.*)$/', $node, $ref)) :
453 $ns = array($ref[1]); $element = $ref[2];
454 elseif (strpos($node, ':') !== FALSE) :
455 list($xmlns, $element) = explode(':', $node, 2);
456 if (isset($this->xmlns['reverse'][$xmlns])) :
457 $ns = $this->xmlns['reverse'][$xmlns];
458 else :
459 $ns = array($xmlns);
460 endif;
461
462 // Fucking SimplePie. For attributes in default xmlns.
463 if ($xmlns==$this->xmlns['forward'][$defaultNS[0]]) :
464 $ns[] = '';
465 endif;
466 else :
467 // Often in SimplePie, the default namespace gets stored
468 // as an empty string rather than a URL.
469 $ns = array($this->xpath_default_namespace(), '');
470 $element = $node;
471 endif;
472 return array(array_unique($ns), $attr.$element);
473 } /* SyndicatedPost::xpath_extended_name () */
474
475 function content () {
476 $content = NULL;
477 if (isset($this->item['atom_content'])) :
478 $content = $this->item['atom_content'];
479 elseif (isset($this->item['xhtml']['body'])) :
480 $content = $this->item['xhtml']['body'];
481 elseif (isset($this->item['xhtml']['div'])) :
482 $content = $this->item['xhtml']['div'];
483 elseif (isset($this->item['content']['encoded']) and $this->item['content']['encoded']):
484 $content = $this->item['content']['encoded'];
485 else:
486 $content = $this->item['description'];
487 endif;
488 return $content;
489 } /* SyndicatedPost::content() */
490
491 function excerpt () {
492 # Identify and sanitize excerpt: atom:summary, or rss:description
493 $excerpt = $this->entry->get_description();
494
495 # Many RSS feeds use rss:description, inadvisably, to
496 # carry the entire post (typically with escaped HTML).
497 # If that's what happened, we don't want the full
498 # content for the excerpt.
499 $content = $this->content();
500 if ( is_null($excerpt) or $excerpt == $content ) :
501 # If content is available, generate an excerpt.
502 if ( strlen(trim($content)) > 0 ) :
503 $excerpt = strip_tags($content);
504 if (strlen($excerpt) > 255) :
505 $excerpt = substr($excerpt,0,252).'...';
506 endif;
507 endif;
508 endif;
509 return $excerpt;
510 } /* SyndicatedPost::excerpt() */
511
512 function permalink () {
513 // Handles explicit <link> elements and also RSS 2.0 cases with
514 // <guid isPermaLink="true">, etc. Hooray!
515 $permalink = $this->entry->get_link();
516 return $permalink;
517 }
518
519 function created () {
520 $date = '';
521 if (isset($this->item['dc']['created'])) :
522 $date = $this->item['dc']['created'];
523 elseif (isset($this->item['dcterms']['created'])) :
524 $date = $this->item['dcterms']['created'];
525 elseif (isset($this->item['created'])): // Atom 0.3
526 $date = $this->item['created'];
527 endif;
528
529 $epoch = new FeedTime($date);
530 return $epoch->timestamp();
531 } /* SyndicatedPost::created() */
532
533 function published ($fallback = true, $default = NULL) {
534 $date = '';
535
536 # RSS is a fucking mess. Figure out whether we have a date in
537 # <dc:date>, <issued>, <pubDate>, etc., and get it into Unix
538 # epoch format for reformatting. If we can't find anything,
539 # we'll use the last-updated time.
540 if (isset($this->item['dc']['date'])): // Dublin Core
541 $date = $this->item['dc']['date'];
542 elseif (isset($this->item['dcterms']['issued'])) : // Dublin Core extensions
543 $date = $this->item['dcterms']['issued'];
544 elseif (isset($this->item['published'])) : // Atom 1.0
545 $date = $this->item['published'];
546 elseif (isset($this->item['issued'])): // Atom 0.3
547 $date = $this->item['issued'];
548 elseif (isset($this->item['pubdate'])): // RSS 2.0
549 $date = $this->item['pubdate'];
550 endif;
551
552 if (strlen($date) > 0) :
553 $time = new FeedTime($date);
554 $epoch = $time->timestamp();
555 elseif ($fallback) : // Fall back to <updated> / <modified> if present
556 $epoch = $this->updated(/*fallback=*/ false, /*default=*/ $default);
557 endif;
558
559 # If everything failed, then default to the current time.
560 if (is_null($epoch)) :
561 if (-1 == $default) :
562 $epoch = time();
563 else :
564 $epoch = $default;
565 endif;
566 endif;
567
568 return $epoch;
569 } /* SyndicatedPost::published() */
570
571 function updated ($fallback = true, $default = -1) {
572 $date = '';
573
574 # As far as I know, only dcterms and Atom have reliable ways to
575 # specify when something was *modified* last. If neither is
576 # available, then we'll try to get the time of publication.
577 if (isset($this->item['dc']['modified'])) : // Not really correct
578 $date = $this->item['dc']['modified'];
579 elseif (isset($this->item['dcterms']['modified'])) : // Dublin Core extensions
580 $date = $this->item['dcterms']['modified'];
581 elseif (isset($this->item['modified'])): // Atom 0.3
582 $date = $this->item['modified'];
583 elseif (isset($this->item['updated'])): // Atom 1.0
584 $date = $this->item['updated'];
585 endif;
586
587 if (strlen($date) > 0) :
588 $time = new FeedTime($date);
589 $epoch = $time->timestamp();
590 elseif ($fallback) : // Fall back to issued / dc:date
591 $epoch = $this->published(/*fallback=*/ false, /*default=*/ $default);
592 endif;
593
594 # If everything failed, then default to the current time.
595 if (is_null($epoch)) :
596 if (-1 == $default) :
597 $epoch = time();
598 else :
599 $epoch = $default;
600 endif;
601 endif;
602
603 return $epoch;
604 } /* SyndicatedPost::updated() */
605
606 function update_hash () {
607 return md5(serialize($this->item));
608 } /* SyndicatedPost::update_hash() */
609
610 function guid () {
611 $guid = null;
612 if (isset($this->item['id'])): // Atom 0.3 / 1.0
613 $guid = $this->item['id'];
614 elseif (isset($this->item['atom']['id'])) : // Namespaced Atom
615 $guid = $this->item['atom']['id'];
616 elseif (isset($this->item['guid'])) : // RSS 2.0
617 $guid = $this->item['guid'];
618 elseif (isset($this->item['dc']['identifier'])) :// yeah, right
619 $guid = $this->item['dc']['identifier'];
620 else :
621 // The feed does not seem to have provided us with a
622 // unique identifier, so we'll have to cobble together
623 // a tag: URI that might work for us. The base of the
624 // URI will be the host name of the feed source ...
625 $bits = parse_url($this->feedmeta['link/uri']);
626 $guid = 'tag:'.$bits['host'];
627
628 // If we have a date of creation, then we can use that
629 // to uniquely identify the item. (On the other hand, if
630 // the feed producer was consicentious enough to
631 // generate dates of creation, she probably also was
632 // conscientious enough to generate unique identifiers.)
633 if (!is_null($this->created())) :
634 $guid .= '://post.'.date('YmdHis', $this->created());
635
636 // Otherwise, use both the URI of the item, *and* the
637 // item's title. We have to use both because titles are
638 // often not unique, and sometimes links aren't unique
639 // either (e.g. Bitch (S)HITLIST, Mozilla Dot Org news,
640 // some podcasts). But it's rare to have *both* the same
641 // title *and* the same link for two different items. So
642 // this is about the best we can do.
643 else :
644 $guid .= '://'.md5($this->item['link'].'/'.$this->item['title']);
645 endif;
646 endif;
647 return $guid;
648 } /* SyndicatedPost::guid() */
649
650 function author () {
651 $author = array ();
652
653 if (isset($this->item['author_name'])):
654 $author['name'] = $this->item['author_name'];
655 elseif (isset($this->item['dc']['creator'])):
656 $author['name'] = $this->item['dc']['creator'];
657 elseif (isset($this->item['dc']['contributor'])):
658 $author['name'] = $this->item['dc']['contributor'];
659 elseif (isset($this->feed->channel['dc']['creator'])) :
660 $author['name'] = $this->feed->channel['dc']['creator'];
661 elseif (isset($this->feed->channel['dc']['contributor'])) :
662 $author['name'] = $this->feed->channel['dc']['contributor'];
663 elseif (isset($this->feed->channel['author_name'])) :
664 $author['name'] = $this->feed->channel['author_name'];
665 elseif ($this->feed->is_rss() and isset($this->item['author'])) :
666 // The author element in RSS is allegedly an
667 // e-mail address, but lots of people don't use
668 // it that way. So let's make of it what we can.
669 $author = parse_email_with_realname($this->item['author']);
670
671 if (!isset($author['name'])) :
672 if (isset($author['email'])) :
673 $author['name'] = $author['email'];
674 else :
675 $author['name'] = $this->feed->channel['title'];
676 endif;
677 endif;
678 else :
679 $author['name'] = $this->feed->channel['title'];
680 endif;
681
682 if (isset($this->item['author_email'])):
683 $author['email'] = $this->item['author_email'];
684 elseif (isset($this->feed->channel['author_email'])) :
685 $author['email'] = $this->feed->channel['author_email'];
686 endif;
687
688 if (isset($this->item['author_url'])):
689 $author['uri'] = $this->item['author_url'];
690 elseif (isset($this->feed->channel['author_url'])) :
691 $author['uri'] = $this->item['author_url'];
692 else:
693 $author['uri'] = $this->feed->channel['link'];
694 endif;
695
696 return $author;
697 } /* SyndicatedPost::author() */
698
699 /**
700 * SyndicatedPost::isTaggedAs: Test whether a feed item is
701 * tagged / categorized with a given string. Case and leading and
702 * trailing whitespace are ignored.
703 *
704 * @param string $tag Tag to check for
705 *
706 * @return bool Whether or not at least one of the categories / tags on
707 * $this->item is set to $tag (modulo case and leading and trailing
708 * whitespace)
709 */
710 function isTaggedAs ($tag) {
711 $desiredTag = strtolower(trim($tag)); // Normalize case and whitespace
712
713 // Check to see if this is tagged with $tag
714 $currentCategory = 'category';
715 $currentCategoryNumber = 1;
716
717 // If we have the new MagpieRSS, the number of category elements
718 // on this item is stored under index "category#".
719 if (isset($this->item['category#'])) :
720 $numberOfCategories = (int) $this->item['category#'];
721
722 // We REALLY shouldn't have the old and busted MagpieRSS, but in
723 // case we do, it doesn't support multiple categories, but there
724 // might still be a single value under the "category" index.
725 elseif (isset($this->item['category'])) :
726 $numberOfCategories = 1;
727
728 // No standard category or tag elements on this feed item.
729 else :
730 $numberOfCategories = 0;
731
732 endif;
733
734 $isSoTagged = false; // Innocent until proven guilty
735
736 // Loop through category elements; if there are multiple
737 // elements, they are indexed as category, category#2,
738 // category#3, ... category#N
739 while ($currentCategoryNumber <= $numberOfCategories) :
740 if ($desiredTag == strtolower(trim($this->item[$currentCategory]))) :
741 $isSoTagged = true; // Got it!
742 break;
743 endif;
744
745 $currentCategoryNumber += 1;
746 $currentCategory = 'category#'.$currentCategoryNumber;
747 endwhile;
748
749 return $isSoTagged;
750 } /* SyndicatedPost::isTaggedAs() */
751
752 /**
753 * SyndicatedPost::enclosures: returns an array with any enclosures
754 * that may be attached to this syndicated item.
755 *
756 * @param string $type If you only want enclosures that match a certain
757 * MIME type or group of MIME types, you can limit the enclosures
758 * that will be returned to only those with a MIME type which
759 * matches this regular expression.
760 * @return array
761 */
762 function enclosures ($type = '/.*/') {
763 $enclosures = array();
764
765 if (isset($this->item['enclosure#'])) :
766 // Loop through enclosure, enclosure#2, enclosure#3, ....
767 for ($i = 1; $i <= $this->item['enclosure#']; $i++) :
768 $eid = (($i > 1) ? "#{$id}" : "");
769
770 // Does it match the type we want?
771 if (preg_match($type, $this->item["enclosure{$eid}@type"])) :
772 $enclosures[] = array(
773 "url" => $this->item["enclosure{$eid}@url"],
774 "type" => $this->item["enclosure{$eid}@type"],
775 "length" => $this->item["enclosure{$eid}@length"],
776 );
777 endif;
778 endfor;
779 endif;
780 return $enclosures;
781 } /* SyndicatedPost::enclosures() */
782
783 function comment_link () {
784 $url = null;
785
786 // RSS 2.0 has a standard <comments> element:
787 // "<comments> is an optional sub-element of <item>. If present,
788 // it is the url of the comments page for the item."
789 // <http://cyber.law.harvard.edu/rss/rss.html#ltcommentsgtSubelementOfLtitemgt>
790 if (isset($this->item['comments'])) :
791 $url = $this->item['comments'];
792 endif;
793
794 // The convention in Atom feeds is to use a standard <link>
795 // element with @rel="replies" and @type="text/html".
796 // Unfortunately, SimplePie_Item::get_links() allows us to filter
797 // by the value of @rel, but not by the value of @type. *sigh*
798
799 // Try Atom 1.0 first
800 $linkElements = $this->entry->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'link');
801
802 // Fall back and try Atom 0.3
803 if (is_null($linkElements)) : $linkElements = $this->entry->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'link'); endif;
804
805 // Now loop through the elements, screening by @rel and @type
806 if (is_array($linkElements)) : foreach ($linkElements as $link) :
807 $rel = (isset($link['attribs']['']['rel']) ? $link['attribs']['']['rel'] : 'alternate');
808 $type = (isset($link['attribs']['']['type']) ? $link['attribs']['']['type'] : NULL);
809 $href = (isset($link['attribs']['']['href']) ? $link['attribs']['']['href'] : NULL);
810
811 if (strtolower($rel)=='replies' and $type=='text/html' and !is_null($href)) :
812 $url = $href;
813 endif;
814 endforeach; endif;
815
816 return $url;
817 }
818
819 function comment_feed () {
820 $feed = null;
821
822 // Well Formed Web comment feeds extension for RSS 2.0
823 // <http://www.sellsbrothers.com/spout/default.aspx?content=archive.htm#exposingRssComments>
824 //
825 // N.B.: Correct capitalization is wfw:commentRss, but
826 // wfw:commentRSS is common in the wild (partly due to a typo in
827 // the original spec). In any case, our item array is normalized
828 // to all lowercase anyways.
829 if (isset($this->item['wfw']['commentrss'])) :
830 $feed = $this->item['wfw']['commentrss'];
831 endif;
832
833 // In Atom 1.0, the convention is to use a standard link element
834 // with @rel="replies". Sometimes this is also used to pass a
835 // link to the human-readable comments page, so we also need to
836 // check link/@type for a feed MIME type.
837 //
838 // Which is why I'm not using the SimplePie_Item::get_links()
839 // method here, incidentally: it doesn't allow you to filter by
840 // @type. *sigh*
841 if (isset($this->item['link_replies'])) :
842 // There may be multiple <link rel="replies"> elements; feeds have a feed MIME type
843 $N = isset($this->item['link_replies#']) ? $this->item['link_replies#'] : 1;
844 for ($i = 1; $i <= $N; $i++) :
845 $currentElement = 'link_replies'.(($i > 1) ? '#'.$i : '');
846 if (isset($this->item[$currentElement.'@type'])
847 and preg_match("\007application/(atom|rss|rdf)\+xml\007i", $this->item[$currentElement.'@type'])) :
848 $feed = $this->item[$currentElement];
849 endif;
850 endfor;
851 endif;
852 return $feed;
853 } /* SyndicatedPost::comment_feed() */
854
855 ##################################
856 #### BUILT-IN CONTENT FILTERS ####
857 ##################################
858
859 var $uri_attrs = array (
860 array('a', 'href'),
861 array('applet', 'codebase'),
862 array('area', 'href'),
863 array('blockquote', 'cite'),
864 array('body', 'background'),
865 array('del', 'cite'),
866 array('form', 'action'),
867 array('frame', 'longdesc'),
868 array('frame', 'src'),
869 array('iframe', 'longdesc'),
870 array('iframe', 'src'),
871 array('head', 'profile'),
872 array('img', 'longdesc'),
873 array('img', 'src'),
874 array('img', 'usemap'),
875 array('input', 'src'),
876 array('input', 'usemap'),
877 array('ins', 'cite'),
878 array('link', 'href'),
879 array('object', 'classid'),
880 array('object', 'codebase'),
881 array('object', 'data'),
882 array('object', 'usemap'),
883 array('q', 'cite'),
884 array('script', 'src')
885 ); /* var SyndicatedPost::$uri_attrs */
886
887 var $_base = null;
888
889 function resolve_single_relative_uri ($refs) {
890 $tag = FeedWordPressHTML::attributeMatch($refs);
891 $url = Relative_URI::resolve($tag['value'], $this->_base);
892 return $tag['prefix'] . $url . $tag['suffix'];
893 } /* function SyndicatedPost::resolve_single_relative_uri() */
894
895 function resolve_relative_uris ($content, $obj) {
896 $set = $obj->link->setting('resolve relative', 'resolve_relative', 'yes');
897 if ($set and $set != 'no') :
898 // Fallback: if we don't have anything better, use the
899 // item link from the feed
900 $obj->_base = $obj->item['link']; // Reset the base for resolving relative URIs
901
902 // What we should do here, properly, is to use
903 // SimplePie_Item::get_base() -- but that method is
904 // currently broken. Or getting down and dirty in the
905 // SimplePie representation of the content tags and
906 // grabbing the xml_base member for the content element.
907 // Maybe someday...
908
909 foreach ($obj->uri_attrs as $pair) :
910 list($tag, $attr) = $pair;
911 $pattern = FeedWordPressHTML::attributeRegex($tag, $attr);
912 $content = preg_replace_callback (
913 $pattern,
914 array(&$obj, 'resolve_single_relative_uri'),
915 $content
916 );
917 endforeach;
918 endif;
919
920 return $content;
921 } /* function SyndicatedPost::resolve_relative_uris () */
922
923 var $strip_attrs = array (
924 array('[a-z]+', 'target'),
925 // array('[a-z]+', 'style'),
926 // array('[a-z]+', 'on[a-z]+'),
927 );
928
929 function strip_attribute_from_tag ($refs) {
930 $tag = FeedWordPressHTML::attributeMatch($refs);
931 return $tag['before_attribute'].$tag['after_attribute'];
932 }
933
934 function sanitize_content ($content, $obj) {
935 # This kind of sucks. I intend to replace it with
936 # lib_filter sometime soon.
937 foreach ($obj->strip_attrs as $pair):
938 list($tag,$attr) = $pair;
939 $pattern = FeedWordPressHTML::attributeRegex($tag, $attr);
940
941 $content = preg_replace_callback (
942 $pattern,
943 array(&$obj, 'strip_attribute_from_tag'),
944 $content
945 );
946 endforeach;
947 return $content;
948 } /* SyndicatedPost::sanitize() */
949
950 #####################
951 #### POST STATUS ####
952 #####################
953
954 /**
955 * SyndicatedPost::filtered: check whether or not this post has been
956 * screened out by a registered filter.
957 *
958 * @return bool TRUE iff post has been filtered out by a previous filter
959 */
960 function filtered () {
961 return is_null($this->post);
962 } /* SyndicatedPost::filtered() */
963
964 /**
965 * SyndicatedPost::freshness: check whether post is a new post to be
966 * inserted, a previously syndicated post that needs to be updated to
967 * match the latest revision, or a previously syndicated post that is
968 * still up-to-date.
969 *
970 * @return int A status code representing the freshness of the post
971 * 0 = post already syndicated; no update needed
972 * 1 = post already syndicated, but needs to be updated to latest
973 * 2 = post has not yet been syndicated; needs to be created
974 */
975 function freshness () {
976 global $wpdb;
977
978 if ($this->filtered()) : // This should never happen.
979 FeedWordPress::critical_bug('SyndicatedPost', $this, __LINE__);
980 endif;
981
982 if (is_null($this->_freshness)) :
983 $guid = $wpdb->escape($this->guid());
984
985 $result = $wpdb->get_row("
986 SELECT id, guid, post_modified_gmt
987 FROM $wpdb->posts WHERE guid='$guid'
988 ");
989
990 if (!$result) :
991 $this->_freshness = 2; // New content
992 else:
993 $stored_update_hashes = get_post_custom_values('syndication_item_hash', $result->id);
994 if (count($stored_update_hashes) > 0) :
995 $stored_update_hash = $stored_update_hashes[0];
996 $update_hash_changed = ($stored_update_hash != $this->update_hash());
997 else :
998 $update_hash_changed = true; // Can't find syndication meta-data
999 endif;
1000
1001 preg_match('/([0-9]+)-([0-9]+)-([0-9]+) ([0-9]+):([0-9]+):([0-9]+)/', $result->post_modified_gmt, $backref);
1002
1003 $last_rev_ts = gmmktime($backref[4], $backref[5], $backref[6], $backref[2], $backref[3], $backref[1]);
1004 $updated_ts = $this->updated(/*fallback=*/ true, /*default=*/ NULL);
1005
1006 $frozen_values = get_post_custom_values('_syndication_freeze_updates', $result->id);
1007 $frozen_post = (count($frozen_values) > 0 and 'yes' == $frozen_values[0]);
1008 $frozen_feed = ('yes' == $this->link->setting('freeze updates', 'freeze_updates', NULL));
1009
1010 // Check timestamps...
1011 $updated = (
1012 !is_null($updated_ts)
1013 and ($updated_ts > $last_rev_ts)
1014 );
1015
1016
1017 // Or the hash...
1018 $updated = ($updated or $update_hash_changed);
1019
1020 // But only if the post is not frozen.
1021 $updated = (
1022 $updated
1023 and !$frozen_post
1024 and !$frozen_feed
1025 );
1026
1027 if ($updated) :
1028 $this->_freshness = 1; // Updated content
1029 $this->_wp_id = $result->id;
1030 else :
1031 $this->_freshness = 0; // Same old, same old
1032 $this->_wp_id = $result->id;
1033 endif;
1034 endif;
1035 endif;
1036 return $this->_freshness;
1037 }
1038
1039 #################################################
1040 #### INTERNAL STORAGE AND MANAGEMENT METHODS ####
1041 #################################################
1042
1043 function wp_id () {
1044 if ($this->filtered()) : // This should never happen.
1045 FeedWordPress::critical_bug('SyndicatedPost', $this, __LINE__);
1046 endif;
1047
1048 if (is_null($this->_wp_id) and is_null($this->_freshness)) :
1049 $fresh = $this->freshness(); // sets WP DB id in the process
1050 endif;
1051 return $this->_wp_id;
1052 }
1053
1054 function store () {
1055 global $wpdb;
1056
1057 if ($this->filtered()) : // This should never happen.
1058 FeedWordPress::critical_bug('SyndicatedPost', $this, __LINE__);
1059 endif;
1060
1061 $freshness = $this->freshness();
1062 if ($freshness > 0) :
1063 # -- Look up, or create, numeric ID for author
1064 $this->post['post_author'] = $this->author_id (
1065 FeedWordPress::on_unfamiliar('author', $this->post['named']['unfamiliar']['author'])
1066 );
1067
1068 if (is_null($this->post['post_author'])) :
1069 $this->post = NULL;
1070 endif;
1071 endif;
1072
1073 if (!$this->filtered() and $freshness > 0) :
1074 # -- Look up, or create, numeric ID for categories
1075 list($pcats, $ptags) = $this->category_ids (
1076 $this->post['named']['category'],
1077 FeedWordPress::on_unfamiliar('category', $this->post['named']['unfamiliar']['category']),
1078 /*tags_too=*/ true
1079 );
1080
1081 $this->post['post_category'] = $pcats;
1082 $this->post['tags_input'] = array_merge($this->post['tags_input'], $ptags);
1083
1084 if (is_null($this->post['post_category'])) :
1085 // filter mode on, no matching categories; drop the post
1086 $this->post = NULL;
1087 else :
1088 // filter mode off or at least one match; now add on the feed and global presets
1089 $this->post['post_category'] = array_merge (
1090 $this->post['post_category'],
1091 $this->category_ids (
1092 $this->post['named']['preset/category'],
1093 'default'
1094 )
1095 );
1096
1097 if (count($this->post['post_category']) < 1) :
1098 $this->post['post_category'][] = 1; // Default to category 1 ("Uncategorized" / "General") if nothing else
1099 endif;
1100 endif;
1101 endif;
1102
1103 if (!$this->filtered() and $freshness > 0) :
1104 unset($this->post['named']);
1105 $this->post = apply_filters('syndicated_post', $this->post, $this);
1106
1107 // Allow for feed-specific syndicated_post filters.
1108 $this->post = apply_filters(
1109 "syndicated_post_".$this->link->uri(),
1110 $this->post,
1111 $this
1112 );
1113 endif;
1114
1115 // Hook in early to make sure these get inserted if at all possible
1116 add_action(
1117 /*hook=*/ 'transition_post_status',
1118 /*callback=*/ array(&$this, 'add_rss_meta'),
1119 /*priority=*/ -10000, /* very early */
1120 /*arguments=*/ 3
1121 );
1122
1123 if (!$this->filtered() and $freshness == 2) :
1124 // The item has not yet been added. So let's add it.
1125 FeedWordPress::diagnostic('syndicated_posts', 'Inserting new post "'.$this->post['post_title'].'"');
1126
1127 $this->insert_new();
1128 do_action('post_syndicated_item', $this->wp_id(), $this);
1129
1130 $ret = 'new';
1131 elseif (!$this->filtered() and $freshness == 1) :
1132 FeedWordPress::diagnostic('syndicated_posts', 'Updating existing post # '.$this->wp_id().', "'.$this->post['post_title'].'"');
1133
1134 $this->post['ID'] = $this->wp_id();
1135 $this->update_existing();
1136 do_action('update_syndicated_item', $this->wp_id(), $this);
1137
1138 $ret = 'updated';
1139 else :
1140 $ret = false;
1141 endif;
1142
1143 // Remove add_rss_meta hook
1144 remove_action(
1145 /*hook=*/ 'transition_post_status',
1146 /*callback=*/ array(&$this, 'add_rss_meta'),
1147 /*priority=*/ -10000, /* very early */
1148 /*arguments=*/ 3
1149 );
1150
1151 return $ret;
1152 } /* function SyndicatedPost::store () */
1153
1154 function insert_new () {
1155 global $wpdb, $wp_db_version;
1156
1157 $dbpost = $this->normalize_post(/*new=*/ true);
1158 if (!is_null($dbpost)) :
1159 if ($this->use_api('wp_insert_post')) :
1160 $dbpost['post_pingback'] = false; // Tell WP 2.1 and 2.2 not to process for pingbacks
1161
1162 // This is a ridiculous fucking kludge necessitated by WordPress 2.6 munging authorship meta-data
1163 add_action('_wp_put_post_revision', array($this, 'fix_revision_meta'));
1164
1165 // Kludge to prevent kses filters from stripping the
1166 // content of posts when updating without a logged in
1167 // user who has `unfiltered_html` capability.
1168 add_filter('content_save_pre', array($this, 'avoid_kses_munge'), 11);
1169
1170 $this->_wp_id = wp_insert_post($dbpost);
1171
1172 // Turn off ridiculous fucking kludges #1 and #2
1173 remove_action('_wp_put_post_revision', array($this, 'fix_revision_meta'));
1174 remove_filter('content_save_pre', array($this, 'avoid_kses_munge'), 11);
1175
1176 $this->validate_post_id($dbpost, array(__CLASS__, __FUNCTION__));
1177
1178 // Unfortunately, as of WordPress 2.3, wp_insert_post()
1179 // *still* offers no way to use a guid of your choice,
1180 // and munges your post modified timestamp, too.
1181 $result = $wpdb->query("
1182 UPDATE $wpdb->posts
1183 SET
1184 guid='{$dbpost['guid']}',
1185 post_modified='{$dbpost['post_modified']}',
1186 post_modified_gmt='{$dbpost['post_modified_gmt']}'
1187 WHERE ID='{$this->_wp_id}'
1188 ");
1189 else :
1190 # The right way to do this is the above. But, alas,
1191 # in earlier versions of WordPress, wp_insert_post has
1192 # too much behavior (mainly related to pings) that can't
1193 # be overridden. In WordPress 1.5, it's enough of a
1194 # resource hog to make PHP segfault after inserting
1195 # 50-100 posts. This can get pretty annoying, especially
1196 # if you are trying to update your feeds for the first
1197 # time.
1198
1199 $result = $wpdb->query("
1200 INSERT INTO $wpdb->posts
1201 SET
1202 guid = '{$dbpost['guid']}',
1203 post_author = '{$dbpost['post_author']}',
1204 post_date = '{$dbpost['post_date']}',
1205 post_date_gmt = '{$dbpost['post_date_gmt']}',
1206 post_content = '{$dbpost['post_content']}',"
1207 .(isset($dbpost['post_excerpt']) ? "post_excerpt = '{$dbpost['post_excerpt']}'," : "")."
1208 post_title = '{$dbpost['post_title']}',
1209 post_name = '{$dbpost['post_name']}',
1210 post_modified = '{$dbpost['post_modified']}',
1211 post_modified_gmt = '{$dbpost['post_modified_gmt']}',
1212 comment_status = '{$dbpost['comment_status']}',
1213 ping_status = '{$dbpost['ping_status']}',
1214 post_status = '{$dbpost['post_status']}'
1215 ");
1216 $this->_wp_id = $wpdb->insert_id;
1217
1218 $this->validate_post_id($dbpost, array(__CLASS__, __FUNCTION__));
1219
1220 // WordPress 1.5.x - 2.0.x
1221 wp_set_post_cats('1', $this->wp_id(), $this->post['post_category']);
1222
1223 // Since we are not going through official channels, we need to
1224 // manually tell WordPress that we've published a new post.
1225 // We need to make sure to do this in order for FeedWordPress
1226 // to play well with the staticize-reloaded plugin (something
1227 // that a large aggregator website is going to *want* to be
1228 // able to use).
1229 do_action('publish_post', $this->_wp_id);
1230 endif;
1231 endif;
1232 } /* SyndicatedPost::insert_new() */
1233
1234 function update_existing () {
1235 global $wpdb;
1236
1237 // Why the fuck doesn't wp_insert_post already do this?
1238 $dbpost = $this->normalize_post(/*new=*/ false);
1239 if (!is_null($dbpost)) :
1240 if ($this->use_api('wp_insert_post')) :
1241 $dbpost['post_pingback'] = false; // Tell WP 2.1 and 2.2 not to process for pingbacks
1242
1243 // This is a ridiculous fucking kludge necessitated by WordPress 2.6 munging authorship meta-data
1244 add_action('_wp_put_post_revision', array($this, 'fix_revision_meta'));
1245
1246 // Kludge to prevent kses filters from stripping the
1247 // content of posts when updating without a logged in
1248 // user who has `unfiltered_html` capability.
1249 add_filter('content_save_pre', array($this, 'avoid_kses_munge'), 11);
1250
1251 // Don't munge status fields that the user may have reset manually
1252 if (function_exists('get_post_field')) :
1253 $doNotMunge = array('post_status', 'comment_status', 'ping_status');
1254 foreach ($doNotMunge as $field) :
1255 $dbpost[$field] = get_post_field($field, $this->wp_id());
1256 endforeach;
1257 endif;
1258
1259 $this->_wp_id = wp_insert_post($dbpost);
1260
1261 // Turn off ridiculous fucking kludges #1 and #2
1262 remove_action('_wp_put_post_revision', array($this, 'fix_revision_meta'));
1263 remove_filter('content_save_pre', array($this, 'avoid_kses_munge'), 11);
1264
1265 $this->validate_post_id($dbpost, array(__CLASS__, __FUNCTION__));
1266
1267 // Unfortunately, as of WordPress 2.3, wp_insert_post()
1268 // munges your post modified timestamp.
1269 $result = $wpdb->query("
1270 UPDATE $wpdb->posts
1271 SET
1272 post_modified='{$dbpost['post_modified']}',
1273 post_modified_gmt='{$dbpost['post_modified_gmt']}'
1274 WHERE ID='{$this->_wp_id}'
1275 ");
1276 else :
1277
1278 $result = $wpdb->query("
1279 UPDATE $wpdb->posts
1280 SET
1281 post_author = '{$dbpost['post_author']}',
1282 post_content = '{$dbpost['post_content']}',"
1283 .(isset($dbpost['post_excerpt']) ? "post_excerpt = '{$dbpost['post_excerpt']}'," : "")."
1284 post_title = '{$dbpost['post_title']}',
1285 post_name = '{$dbpost['post_name']}',
1286 post_modified = '{$dbpost['post_modified']}',
1287 post_modified_gmt = '{$dbpost['post_modified_gmt']}'
1288 WHERE guid='{$dbpost['guid']}'
1289 ");
1290
1291 // WordPress 2.1.x and up
1292 if (function_exists('wp_set_post_categories')) :
1293 wp_set_post_categories($this->wp_id(), $this->post['post_category']);
1294 // WordPress 1.5.x - 2.0.x
1295 elseif (function_exists('wp_set_post_cats')) :
1296 wp_set_post_cats('1', $this->wp_id(), $this->post['post_category']);
1297 // This should never happen.
1298 else :
1299 FeedWordPress::critical_bug(__CLASS__.'::'.__FUNCTION.'(): no post categorizing function', array("dbpost" => $dbpost, "this" => $this), __LINE__);
1300 endif;
1301
1302 // Since we are not going through official channels, we need to
1303 // manually tell WordPress that we've published a new post.
1304 // We need to make sure to do this in order for FeedWordPress
1305 // to play well with the staticize-reloaded plugin (something
1306 // that a large aggregator website is going to *want* to be
1307 // able to use).
1308 do_action('edit_post', $this->post['ID']);
1309 endif;
1310 endif;
1311 } /* SyndicatedPost::update_existing() */
1312
1313 /**
1314 * SyndicatedPost::normalize_post()
1315 *
1316 * @param bool $new If true, this post is to be inserted anew. If false, it is an update of an existing post.
1317 * @return array A normalized representation of the post ready to be inserted into the database or sent to the WordPress API functions
1318 */
1319 function normalize_post ($new = true) {
1320 global $wpdb;
1321
1322 $out = array();
1323
1324 // Why the fuck doesn't wp_insert_post already do this?
1325 foreach ($this->post as $key => $value) :
1326 if (is_string($value)) :
1327 $out[$key] = $wpdb->escape($value);
1328 else :
1329 $out[$key] = $value;
1330 endif;
1331 endforeach;
1332
1333 if (strlen($out['post_title'].$out['post_content'].$out['post_excerpt']) == 0) :
1334 // FIXME: Option for filtering out empty posts
1335 endif;
1336 if (strlen($out['post_title'])==0) :
1337 $offset = (int) get_option('gmt_offset') * 60 * 60;
1338 $out['post_title'] =
1339 $this->post['meta']['syndication_source']
1340 .' '.gmdate('Y-m-d H:i:s', $this->published() + $offset);
1341 // FIXME: Option for what to fill a blank title with...
1342 endif;
1343
1344 return $out;
1345 }
1346
1347 /**
1348 * SyndicatedPost::validate_post_id()
1349 *
1350 * @param array $dbpost An array representing the post we attempted to insert or update
1351 * @param mixed $ns A string or array representing the namespace (class, method) whence this method was called.
1352 */
1353 function validate_post_id ($dbpost, $ns) {
1354 if (is_array($ns)) : $ns = implode('::', $ns);
1355 else : $ns = (string) $ns; endif;
1356
1357 // This should never happen.
1358 if (!is_numeric($this->_wp_id) or ($this->_wp_id == 0)) :
1359 FeedWordPress::critical_bug(
1360 /*name=*/ $ns.'::_wp_id',
1361 /*var =*/ array(
1362 "\$this->_wp_id" => $this->_wp_id,
1363 "\$dbpost" => $dbpost,
1364 "\$this" => $this
1365 ),
1366 /*line # =*/ __LINE__
1367 );
1368 endif;
1369 } /* SyndicatedPost::validate_post_id() */
1370
1371 /**
1372 * SyndicatedPost::fix_revision_meta() - Fixes the way WP 2.6+ fucks up
1373 * meta-data (authorship, etc.) when storing revisions of an updated
1374 * syndicated post.
1375 *
1376 * In their infinite wisdom, the WordPress coders have made it completely
1377 * impossible for a plugin that uses wp_insert_post() to set certain
1378 * meta-data (such as the author) when you store an old revision of an
1379 * updated post. Instead, it uses the WordPress defaults (= currently
1380 * active user ID if the process is running with a user logged in, or
1381 * = #0 if there is no user logged in). This results in bogus authorship
1382 * data for revisions that are syndicated from off the feed, unless we
1383 * use a ridiculous kludge like this to end-run the munging of meta-data
1384 * by _wp_put_post_revision.
1385 *
1386 * @param int $revision_id The revision ID to fix up meta-data
1387 */
1388 function fix_revision_meta ($revision_id) {
1389 global $wpdb;
1390
1391 $post_author = (int) $this->post['post_author'];
1392
1393 $revision_id = (int) $revision_id;
1394 $wpdb->query("
1395 UPDATE $wpdb->posts
1396 SET post_author={$this->post['post_author']}
1397 WHERE post_type = 'revision' AND ID='$revision_id'
1398 ");
1399 } /* SyndicatedPost::fix_revision_meta () */
1400
1401 /**
1402 * SyndicatedPost::avoid_kses_munge() -- If FeedWordPress is processing
1403 * an automatic update, that generally means that wp_insert_post() is
1404 * being called under the user credentials of whoever is viewing the
1405 * blog at the time -- usually meaning no user at all. But if WordPress
1406 * gets a wp_insert_post() when current_user_can('unfiltered_html') is
1407 * false, it will run the content of the post through a kses function
1408 * that strips out lots of HTML tags -- notably <object> and some others.
1409 * This causes problems for syndicating (for example) feeds that contain
1410 * YouTube videos. It also produces an unexpected asymmetry between
1411 * automatically-initiated updates and updates initiated manually from
1412 * the WordPress Dashboard (which are usually initiated under the
1413 * credentials of a logged-in admin, and so don't get run through the
1414 * kses function). So, to avoid the whole mess, what we do here is
1415 * just forcibly disable the kses munging for a single syndicated post,
1416 * by restoring the contents of the `post_content` field.
1417 *
1418 * @param string $content The content of the post, after other filters have gotten to it
1419 * @return string The original content of the post, before other filters had a chance to munge it.
1420 */
1421 function avoid_kses_munge ($content) {
1422 global $wpdb;
1423 return $wpdb->escape($this->post['post_content']);
1424 }
1425
1426 // SyndicatedPost::add_rss_meta: adds interesting meta-data to each entry
1427 // using the space for custom keys. The set of keys and values to add is
1428 // specified by the keys and values of $post['meta']. This is used to
1429 // store anything that the WordPress user might want to access from a
1430 // template concerning the post's original source that isn't provided
1431 // for by standard WP meta-data (i.e., any interesting data about the
1432 // syndicated post other than author, title, timestamp, categories, and
1433 // guid). It's also used to hook into WordPress's support for
1434 // enclosures.
1435 function add_rss_meta ($new_status, $old_status, $post) {
1436 FeedWordPress::diagnostic('syndicated_posts:meta_data', 'Adding post meta-data: {'.implode(", ", array_keys($this->post['meta'])).'}');
1437
1438 global $wpdb;
1439 if ( is_array($this->post) and isset($this->post['meta']) and is_array($this->post['meta']) ) :
1440 $postId = $post->ID;
1441
1442 // Aggregated posts should NOT send out pingbacks.
1443 // WordPress 2.1-2.2 claim you can tell them not to
1444 // using $post_pingback, but they don't listen, so we
1445 // make sure here.
1446 $result = $wpdb->query("
1447 DELETE FROM $wpdb->postmeta
1448 WHERE post_id='$postId' AND meta_key='_pingme'
1449 ");
1450
1451 foreach ( $this->post['meta'] as $key => $values ) :
1452 $eKey = $wpdb->escape($key);
1453
1454 // If this is an update, clear out the old
1455 // values to avoid duplication.
1456 $result = $wpdb->query("
1457 DELETE FROM $wpdb->postmeta
1458 WHERE post_id='$postId' AND meta_key='$eKey'
1459 ");
1460
1461 // Allow for either a single value or an array
1462 if (!is_array($values)) $values = array($values);
1463 foreach ( $values as $value ) :
1464 FeedWordPress::diagnostic('syndicated_posts:meta_data', "Adding post meta-datum to post [$postId]: [$key] = ".FeedWordPress::val($value, /*no newlines=*/ true));
1465 add_post_meta($postId, $key, $value, /*unique=*/ false);
1466 endforeach;
1467 endforeach;
1468 endif;
1469 } /* SyndicatedPost::add_rss_meta () */
1470
1471 // SyndicatedPost::author_id (): get the ID for an author name from
1472 // the feed. Create the author if necessary.
1473 function author_id ($unfamiliar_author = 'create') {
1474 global $wpdb;
1475
1476 $a = $this->author();
1477 $author = $a['name'];
1478 $email = (isset($a['email']) ? $a['email'] : NULL);
1479 $url = (isset($a['uri']) ? $a['uri'] : NULL);
1480
1481 $match_author_by_email = !('yes' == get_option("feedwordpress_do_not_match_author_by_email"));
1482 if ($match_author_by_email and !FeedWordPress::is_null_email($email)) :
1483 $test_email = $email;
1484 else :
1485 $test_email = NULL;
1486 endif;
1487
1488 // Never can be too careful...
1489 $login = sanitize_user($author, /*strict=*/ true);
1490 $login = apply_filters('pre_user_login', $login);
1491
1492 $nice_author = sanitize_title($author);
1493 $nice_author = apply_filters('pre_user_nicename', $nice_author);
1494
1495 $reg_author = $wpdb->escape(preg_quote($author));
1496 $author = $wpdb->escape($author);
1497 $email = $wpdb->escape($email);
1498 $test_email = $wpdb->escape($test_email);
1499 $url = $wpdb->escape($url);
1500
1501 // Check for an existing author rule....
1502 if (isset($this->link->settings['map authors']['name'][strtolower(trim($author))])) :
1503 $author_rule = $this->link->settings['map authors']['name'][strtolower(trim($author))];
1504 else :
1505 $author_rule = NULL;
1506 endif;
1507
1508 // User name is mapped to a particular author. If that author ID exists, use it.
1509 if (is_numeric($author_rule) and get_userdata((int) $author_rule)) :
1510 $id = (int) $author_rule;
1511
1512 // User name is filtered out
1513 elseif ('filter' == $author_rule) :
1514 $id = NULL;
1515
1516 else :
1517 // Check the database for an existing author record that might fit
1518
1519 // First try the user core data table.
1520 $id = $wpdb->get_var(
1521 "SELECT ID FROM $wpdb->users
1522 WHERE
1523 TRIM(LCASE(user_login)) = TRIM(LCASE('$login'))
1524 OR (
1525 LENGTH(TRIM(LCASE(user_email))) > 0
1526 AND TRIM(LCASE(user_email)) = TRIM(LCASE('$test_email'))
1527 )
1528 OR TRIM(LCASE(user_nicename)) = TRIM(LCASE('$nice_author'))
1529 ");
1530
1531 // If that fails, look for aliases in the user meta data table
1532 if (is_null($id)) :
1533 $id = $wpdb->get_var(
1534 "SELECT user_id FROM $wpdb->usermeta
1535 WHERE
1536 (meta_key = 'description' AND TRIM(LCASE(meta_value)) = TRIM(LCASE('$author')))
1537 OR (
1538 meta_key = 'description'
1539 AND TRIM(LCASE(meta_value))
1540 RLIKE CONCAT(
1541 '(^|\\n)a\\.?k\\.?a\\.?( |\\t)*:?( |\\t)*',
1542 TRIM(LCASE('$reg_author')),
1543 '( |\\t|\\r)*(\\n|\$)'
1544 )
1545 )
1546 ");
1547 endif;
1548
1549 // ... if you don't find one, then do what you need to do
1550 if (is_null($id)) :
1551 if ($unfamiliar_author === 'create') :
1552 $userdata = array();
1553
1554 // WordPress 3 is going to pitch a fit if we attempt to register
1555 // more than one user account with an empty e-mail address, so we
1556 // need *something* here. Ugh.
1557 if (strlen($email) == 0 or FeedWordPress::is_null_email($email)) :
1558 $url = parse_url($this->feed->channel['link']);
1559 $email = $nice_author.'@'.$url['host'];
1560 endif;
1561
1562 #-- user table data
1563 $userdata['ID'] = NULL; // new user
1564 $userdata['user_login'] = $login;
1565 $userdata['user_nicename'] = $nice_author;
1566 $userdata['user_pass'] = substr(md5(uniqid(microtime())), 0, 6); // just something random to lock it up
1567 $userdata['user_email'] = $email;
1568 $userdata['user_url'] = $url;
1569 $userdata['display_name'] = $author;
1570
1571 $id = wp_insert_user($userdata);
1572 elseif (is_numeric($unfamiliar_author) and get_userdata((int) $unfamiliar_author)) :
1573 $id = (int) $unfamiliar_author;
1574 elseif ($unfamiliar_author === 'default') :
1575 $id = 1;
1576 endif;
1577 endif;
1578 endif;
1579
1580 if ($id) :
1581 $this->link->settings['map authors']['name'][strtolower(trim($author))] = $id;
1582 endif;
1583 return $id;
1584 } // function SyndicatedPost::author_id ()
1585
1586 // look up (and create) category ids from a list of categories
1587 function category_ids ($cats, $unfamiliar_category = 'create', $tags_too = false) {
1588 global $wpdb;
1589
1590 // We need to normalize whitespace because (1) trailing
1591 // whitespace can cause PHP and MySQL not to see eye to eye on
1592 // VARCHAR comparisons for some versions of MySQL (cf.
1593 // <http://dev.mysql.com/doc/mysql/en/char.html>), and (2)
1594 // because I doubt most people want to make a semantic
1595 // distinction between 'Computers' and 'Computers '
1596 $cats = array_map('trim', $cats);
1597
1598 $tags = array();
1599
1600 $cat_ids = array ();
1601 foreach ($cats as $cat_name) :
1602 if (preg_match('/^{#([0-9]+)}$/', $cat_name, $backref)) :
1603 $cat_id = (int) $backref[1];
1604 if (function_exists('is_term') and is_term($cat_id, 'category')) :
1605 $cat_ids[] = $cat_id;
1606 elseif (get_category($cat_id)) :
1607 $cat_ids[] = $cat_id;
1608 endif;
1609 elseif (strlen($cat_name) > 0) :
1610 $esc = $wpdb->escape($cat_name);
1611 $resc = $wpdb->escape(preg_quote($cat_name));
1612
1613 // WordPress 2.3+
1614 if (function_exists('is_term')) :
1615 $cat_id = is_term($cat_name, 'category');
1616 if ($cat_id) :
1617 $cat_ids[] = $cat_id['term_id'];
1618 // There must be a better way to do this...
1619 elseif ($results = $wpdb->get_results(
1620 "SELECT term_id
1621 FROM $wpdb->term_taxonomy
1622 WHERE
1623 LOWER(description) RLIKE
1624 CONCAT('(^|\\n)a\\.?k\\.?a\\.?( |\\t)*:?( |\\t)*', LOWER('{$resc}'), '( |\\t|\\r)*(\\n|\$)')"
1625 )) :
1626 foreach ($results AS $term) :
1627 $cat_ids[] = (int) $term->term_id;
1628 endforeach;
1629 elseif ('tag'==$unfamiliar_category) :
1630 $tags[] = $cat_name;
1631 elseif ('create'===$unfamiliar_category) :
1632 $term = wp_insert_term($cat_name, 'category');
1633 if (is_wp_error($term)) :
1634 FeedWordPress::noncritical_bug('term insertion problem', array('cat_name' => $cat_name, 'term' => $term, 'this' => $this), __LINE__);
1635 else :
1636 $cat_ids[] = $term['term_id'];
1637 endif;
1638 endif;
1639
1640 // WordPress 1.5.x - 2.2.x
1641 else :
1642 $results = $wpdb->get_results(
1643 "SELECT cat_ID
1644 FROM $wpdb->categories
1645 WHERE
1646 (LOWER(cat_name) = LOWER('$esc'))
1647 OR (LOWER(category_description)
1648 RLIKE CONCAT('(^|\\n)a\\.?k\\.?a\\.?( |\\t)*:?( |\\t)*', LOWER('{$resc}'), '( |\\t|\\r)*(\\n|\$)'))
1649 ");
1650 if ($results) :
1651 foreach ($results as $term) :
1652 $cat_ids[] = (int) $term->cat_ID;
1653 endforeach;
1654 elseif ('create'===$unfamiliar_category) :
1655 if (function_exists('wp_insert_category')) :
1656 $cat_id = wp_insert_category(array('cat_name' => $esc));
1657 // And into the database we go.
1658 else :
1659 $nice_kitty = sanitize_title($cat_name);
1660 $wpdb->query(sprintf("
1661 INSERT INTO $wpdb->categories
1662 SET
1663 cat_name='%s',
1664 category_nicename='%s'
1665 ", $esc, $nice_kitty
1666 ));
1667 $cat_id = $wpdb->insert_id;
1668 endif;
1669 $cat_ids[] = $cat_id;
1670 endif;
1671 endif;
1672 endif;
1673 endforeach;
1674
1675 if ((count($cat_ids) == 0) and ($unfamiliar_category === 'filter')) :
1676 $cat_ids = NULL; // Drop the post
1677 else :
1678 $cat_ids = array_unique($cat_ids);
1679 endif;
1680
1681 if ($tags_too) : $ret = array($cat_ids, $tags);
1682 else : $ret = $cat_ids;
1683 endif;
1684
1685 return $ret;
1686 } // function SyndicatedPost::category_ids ()
1687
1688 function use_api ($tag) {
1689 global $wp_db_version;
1690 switch ($tag) :
1691 case 'wp_insert_post':
1692 // Before 2.2, wp_insert_post does too much of the wrong stuff to use it
1693 // In 1.5 it was such a resource hog it would make PHP segfault on big updates
1694 $ret = (isset($wp_db_version) and $wp_db_version > FWP_SCHEMA_21);
1695 break;
1696 case 'post_status_pending':
1697 $ret = (isset($wp_db_version) and $wp_db_version > FWP_SCHEMA_23);
1698 break;
1699 endswitch;
1700 return $ret;
1701 } // function SyndicatedPost::use_api ()
1702
1703 } /* class SyndicatedPost */
1704
1705