r49419 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r49418‎ | r49419 | r49420 >
Date:13:39, 12 April 2009
Author:jimbojw
Status:deferred
Tags:
Comment:
adding WikiArticleFeeds to wikimedia svn repo at GerardM's request
Modified paths:
  • /trunk/extensions/WikiArticleFeeds (added) (history)
  • /trunk/extensions/WikiArticleFeeds/WikiArticleFeeds.php (added) (history)

Diff [purge]

Index: trunk/extensions/WikiArticleFeeds/WikiArticleFeeds.php
@@ -0,0 +1,588 @@
 2+<?php
 3+/*
 4+ * WikiArticleFeeds.php - A MediaWiki extension for converting regular pages into feeds.
 5+ * @author Jim R. Wilson
 6+ * @version 0.6.3
 7+ * @copyright Copyright (C) 2007 Jim R. Wilson
 8+ * @license The MIT License - http://www.opensource.org/licenses/mit-license.php
 9+ * -----------------------------------------------------------------------
 10+ * Description:
 11+ * This is a MediaWiki (http://www.mediawiki.org/) extension which adds support
 12+ * for publishing RSS or Atom feeds generated from standard wiki articles.
 13+ * Requirements:
 14+ * MediaWiki 1.6.x or higher
 15+ * PHP 4.x, 5.x or higher
 16+ * Installation:
 17+ * 1. Drop this script (WikiArticleFeeds.php) in $IP/extensions
 18+ * Note: $IP is your MediaWiki install dir.
 19+ * 2. Enable the extension by adding this line to your LocalSettings.php:
 20+ * require_once('extensions/WikiArticleFeeds.php');
 21+ * Usage:
 22+ * Once installed, you may utilize WikiArticleFeeds by invoking the 'feed' action of an article:
 23+ * $wgScript?title=Some_Article&action=feed
 24+ * Note: You may optionally supply a feed type. Acceptable values inculde 'rss' and 'atom'.
 25+ * If no feed type is specified, the default is 'atom'. For example:
 26+ * $wgScript?title=Some_Article&action=feed&feed=atom
 27+ * Creating a Feed:
 28+ * To delimit a section of an article as containing feed items, use the <startFeed />
 29+ * and <endFeed /> tags respectively. These tags are merely flags, and any attributes
 30+ * specified, or content inside the tags themselves will be ignored.
 31+ * Tagging a Feed item:
 32+ * To tag a feed item, insert either the <itemTags> tag, or the a call to the {{#itemTags}} parser
 33+ * function somewhere between the opening header of the item (== Item Title ==) and the header of
 34+ * the next item. For example, to mark an item about dogs and cats, you could do any of the following:
 35+ * <itemTags>dogs, cats</itemTags>
 36+ * {{#itemTags:dogs, cats}}
 37+ * {{#itemTags:dogs|cats}}
 38+ * Version Notes:
 39+ * version 0.6.4:
 40+ * Small fix for MW 1.14 in which section header anchors changed format.
 41+ * First version to be checked into wikimedia SVN.
 42+ * version 0.6.3:
 43+ * Wrapped extension points for versions less than 1.7 (old version compatibility)
 44+ * version 0.6.2:
 45+ * Added support for alternate signatures (when users are not in the User namespace)
 46+ * Tied purge of xml feeds to purge of page - helps with consumers of semantic and DPL
 47+ * (Thanks to Charlie Huggard of charlie.huggardlee.net for these contributions)
 48+ * version 0.6.1:
 49+ * Fixed stale feed problem by expiring the feed cache when any of an article's transcluded pages change.
 50+ * version 0.6:
 51+ * Added support for "tagging" feed items by way of <itemTags> or {{itemTags}}
 52+ * Added support for filtering generated feed based on item tags.
 53+ * Added global ($wgWikiArticleFeedsSkipCache) used for skipping object-cache while debugging.
 54+ * Fixed namespace restriction - now works for namespaces other than NS_MAIN
 55+ * version 0.5:
 56+ * Now supports offloading to FeedBurner (http://feedburner.com/) via <feedBurner> tag.
 57+ * Capitalized RSS and Atom links in the toolbox.
 58+ * version 0.4:
 59+ * Added wgForceArticleFeedSectionLinks to override default link behavior (set in LocalSettings).
 60+ * Feed generator now follows Article redirects.
 61+ * version 0.3:
 62+ * Added Version Notes.
 63+ * Fixed relative-links bug (all links in item descriptions are now fully qualified).
 64+ * Fixed date-overwrite bug (previously, items with the exact same timestamp would be ignored).
 65+ * Improved W3C validation. Feeds validate, but there are still warnings.
 66+ * version 0.2:
 67+ * Renamed from WikiFeeds.php to WikiArticleFeeds.php
 68+ * Corrected credits and copyright info.
 69+ * Numerous minor fixes and tweaks.
 70+ * Expanded support for versions 1.6.x, 1.8.x and 1.9.x.
 71+ * Improved return values from hooking functions (plays better with other extensions).
 72+ * version 0.1:
 73+ * Initial release.
 74+ * -----------------------------------------------------------------------
 75+ * Copyright (c) 2007 Jim R. Wilson
 76+ *
 77+ * Permission is hereby granted, free of charge, to any person obtaining a copy
 78+ * of this software and associated documentation files (the "Software"), to deal
 79+ * in the Software without restriction, including without limitation the rights to
 80+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 81+ * the Software, and to permit persons to whom the Software is furnished to do
 82+ * so, subject to the following conditions:
 83+ *
 84+ * The above copyright notice and this permission notice shall be included in all
 85+ * copies or substantial portions of the Software.
 86+ *
 87+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 88+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 89+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 90+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 91+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 92+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 93+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 94+ * OTHER DEALINGS IN THE SOFTWARE.
 95+ * -----------------------------------------------------------------------
 96+ */
 97+
 98+ # Confirm MW environment
 99+if (!defined('MEDIAWIKI')) die();
 100+
 101+define('WIKIARTICLEFEEDS_VERSION','0.6.4');
 102+
 103+# Bring in supporting classes
 104+require_once("$IP/includes/Feed.php");
 105+require_once("$IP/includes/Sanitizer.php");
 106+
 107+# Credits
 108+$wgExtensionCredits['specialpage'][] = array(
 109+ 'name'=>'WikiArticleFeeds',
 110+ 'author'=>'Jim Wilson (wilson.jim.r&lt;at&gt;gmail.com)',
 111+ 'url'=>'http://jimbojw.com/wiki/index.php?title=WikiArticleFeeds',
 112+ 'description'=>'Produces feeds generated from MediaWiki articles.',
 113+ 'version'=>WIKIARTICLEFEEDS_VERSION
 114+);
 115+
 116+/**
 117+ * Wrapper class for consolidating all WAF related parser methods
 118+ */
 119+class WikiArticleFeedsParser {
 120+
 121+ function feedStart( $text, $params = array() ) {
 122+ return '<!-- FEED_START -->';
 123+ }
 124+
 125+ function feedEnd( $text, $params = array() ) {
 126+ return '<!-- FEED_END -->';
 127+ }
 128+
 129+ function burnFeed( $text, $params = array() ) {
 130+ return ($params['name']?'<!-- BURN_FEED '.base64_encode(serialize($params['name'])).' -->':'');
 131+ }
 132+
 133+ function itemTagsTag( $text, $params = array() ) {
 134+ return ($text?'<!-- ITEM_TAGS '.base64_encode(serialize($text)).' -->':'');
 135+ }
 136+
 137+ function itemTagsFunction( $parser ) {
 138+ $tags = func_get_args();
 139+ array_shift( $tags );
 140+ return (!empty($tags)?'<pre>@ITEMTAGS@'.base64_encode(serialize(implode(',',$tags))).'@ITEMTAGS@</pre>':'');
 141+ }
 142+
 143+ function itemTagsMagic( &$magicWords, $langCode=null ) {
 144+ $magicWords['itemtags'] = array( 0, 'itemtags' );
 145+ return true;
 146+ }
 147+
 148+ function itemTagsPlaceholderCorrections( $parser, &$text ) {
 149+ $text = preg_replace(
 150+ '|<pre>@ITEMTAGS@([0-9a-zA-Z\\+\\/]+=*)@ITEMTAGS@</pre>|',
 151+ '<!-- ITEM_TAGS $1 -->',
 152+ $text
 153+ );
 154+ return true;
 155+ }
 156+}
 157+
 158+# Create global instance
 159+$wgWikiArticleFeedsParser = new WikiArticleFeedsParser();
 160+if (version_compare($wgVersion, '1.7', '<')) {
 161+ # Hack solution to resolve 1.6 array parameter nullification for hook args
 162+ function wfWAFParserItemTagsMagic( &$magicWords ) {
 163+ global $wgWikiArticleFeedsParser;
 164+ $wgWikiArticleFeedsParser->itemTagsMagic( $magicWords );
 165+ return true;
 166+ }
 167+ function wfWAFParserPlaceholderCorrections( $parser, &$text ) {
 168+ global $wgWikiArticleFeedsParser;
 169+ $wgWikiArticleFeedsParser->itemTagsPlaceholderCorrections( $parser, $text );
 170+ return true;
 171+ }
 172+ $wgHooks['LanguageGetMagic'][] = 'wfWAFParserItemTagsMagic';
 173+ $wgHooks['ParserBeforeTidy'][] = 'wfWAFParserPlaceholderCorrections';
 174+} else {
 175+ $wgHooks['LanguageGetMagic'][] = array($wgWikiArticleFeedsParser, 'itemTagsMagic');
 176+ $wgHooks['ParserBeforeTidy'][] = array($wgWikiArticleFeedsParser, 'itemTagsPlaceholderCorrections');
 177+}
 178+
 179+# Add Extension Functions
 180+$wgExtensionFunctions[] = 'wfWikiArticleFeedsParserSetup';
 181+
 182+# Sets up the WikiArticleFeeds Parser hooks
 183+function wfWikiArticleFeedsParserSetup() {
 184+ global $wgParser, $wgWikiArticleFeedsParser;
 185+
 186+ $wgParser->setHook( 'startFeed', array($wgWikiArticleFeedsParser, 'feedStart') );
 187+ $wgParser->setHook( 'endFeed', array($wgWikiArticleFeedsParser, 'feedEnd') );
 188+ $wgParser->setHook( 'feedBurner', array($wgWikiArticleFeedsParser, 'burnFeed') );
 189+ $wgParser->setHook( 'itemTags', array($wgWikiArticleFeedsParser, 'itemTagsTag') );
 190+
 191+ $wgParser->setFunctionHook( 'itemtags', array($wgWikiArticleFeedsParser, 'itemTagsFunction') );
 192+}
 193+
 194+# Attach Hooks
 195+$wgHooks['OutputPageBeforeHTML'][] = 'wfAddWikiFeedHeaders';
 196+$wgHooks['MonoBookTemplateToolboxEnd'][] = 'wfWikiArticleFeedsToolboxLinks';
 197+$wgHooks['UnknownAction'][] = 'wfWikiArticleFeedsAction';
 198+$wgHooks['ArticlePurge'][] = 'wfPurgeFeedsOnArticlePurge';
 199+
 200+/**
 201+ * Adds the Wiki feed link headers.
 202+ * Usage: $wgHooks['OutputPageBeforeHTML'][] = 'wfAddWikiFeedHeaders';
 203+ * @param $out Handle to an OutputPage object (presumably $wgOut).
 204+ * @param $text Article/Output text.
 205+ */
 206+function wfAddWikiFeedHeaders($out, $text) {
 207+
 208+ # Short-circuit if this article contains no feeds
 209+ if (!preg_match('/<!-- FEED_START -->/m', $text)) return true;
 210+
 211+ $rssArr = array(
 212+ 'rel' => 'alternate',
 213+ 'type' => 'application/rss+xml',
 214+ 'title' => 'RSS 2.0',
 215+ );
 216+ $atomArr = array(
 217+ 'rel' => 'alternate',
 218+ 'type' => 'application/atom+xml',
 219+ 'title' => 'Atom 0.3',
 220+ );
 221+
 222+ # Test for feedBurner presence
 223+ if (preg_match('/<!-- BURN_FEED ([0-9a-zA-Z\\+\\/]+=*) -->/m', $text, $matches)) {
 224+ $name = @unserialize(@base64_decode($matches[1]));
 225+ if ($name) {
 226+ $rssArr['href'] = 'http://feeds.feedburner.com/'.urlencode($name).'?format=xml';
 227+ $atomArr['href'] = 'http://feeds.feedburner.com/'.urlencode($name).'?format=xml';
 228+ }
 229+ }
 230+
 231+ # If its not being fed by feedBurner, do it ourselves!
 232+ if (!array_key_exists('href', $rssArr) || !$rssArr['href']) {
 233+
 234+ global $wgServer, $wgScript, $wgTitle;
 235+
 236+ $baseUrl = $wgServer.$wgScript.'?title='.$wgTitle->getDBkey().'&action=feed&feed=';
 237+ $rssArr['href'] = $baseUrl.'rss';
 238+ $atomArr['href'] = $baseUrl.'atom';
 239+ }
 240+
 241+ $out->addLink($rssArr);
 242+ $out->addLink($atomArr);
 243+
 244+ global $wgWikiFeedPresent;
 245+ $wgWikiFeedPresent = true;
 246+
 247+ # True to indicate that other action handlers should continue to process this page
 248+ return true;
 249+}
 250+
 251+/**
 252+ * Adds the Wiki feed links to the bottom of the toolbox in Monobook or like-minded skins.
 253+ * Usage: $wgHooks['MonoBookTemplateToolboxEnd'][] = 'wfWikiArticleFeedsToolboxLinks';
 254+ * @param QuickTemplate $template Instance of MonoBookTemplate or other QuickTemplate
 255+ */
 256+function wfWikiArticleFeedsToolboxLinks($template) {
 257+
 258+ global $wgOut, $wgServer, $wgScript, $wgArticle, $wgWikiFeedPresent;
 259+
 260+ # Short-circuit if wgArticle has not been set or there are no Feeds present
 261+ if (!$wgArticle || !$wgWikiFeedPresent) return true;
 262+
 263+ $result = '<li id="feedlinks">';
 264+
 265+ # Test for feedBurner presence
 266+ $burned = false;
 267+ ob_start();
 268+ $template->html('bodytext');
 269+ $text = ob_get_contents();
 270+ ob_end_clean();
 271+ if (preg_match('/<!-- BURN_FEED ([0-9a-zA-Z\\+\\/]+=*) -->/m', $text, $matches)) {
 272+ $feedBurnerName = @unserialize(@base64_decode($matches[1]));
 273+ if ($feedBurnerName) {
 274+ $feeds = array( 'rss'=>'RSS', 'atom'=>'Atom' );
 275+ foreach($feeds as $feed=>$name) {
 276+ $result .=
 277+ '<span id="feed-'.htmlspecialchars($feed).'">'.
 278+ '<a href="http://feeds.feedburner.com/'.urlencode($feedBurnerName).'?format=xml">'.
 279+ htmlspecialchars($name).'</a>&nbsp;</span>';
 280+ }
 281+ $burned = true;
 282+ }
 283+ }
 284+
 285+ # Generate regular RSS and Atom feeds if not fed by feedBurner
 286+ if (!$burned) {
 287+ $title = $wgArticle->getTitle();
 288+ $dbKey = $title->getPrefixedDBkey();
 289+ $baseUrl = $wgServer.$wgScript.'?title='.$dbKey.'&action=feed&feed=';
 290+ $feeds = array( 'rss'=>'RSS', 'atom'=>'Atom' );
 291+ foreach($feeds as $feed=>$name) {
 292+ $result .=
 293+ '<span id="feed-'.htmlspecialchars($feed).'">'.
 294+ '<a href="'.htmlspecialchars($baseUrl.$feed).'">'.
 295+ htmlspecialchars($name).'</a>&nbsp;</span>';
 296+ }
 297+ }
 298+
 299+ echo ($result.'</li>');
 300+
 301+ # True to indicate that other action handlers should continue to process this page
 302+ return true;
 303+}
 304+
 305+/**
 306+ * Injects handling of the 'feed' action.
 307+ * Usage: $wgHooks['UnknownAction'][] = 'wfWikiArticleFeedsAction';
 308+ * @param $action Handle to an action string (presumably same as global $action).
 309+ * @param $article Article to be converted to rss or atom feed (presumably same as $wgArticle).
 310+ */
 311+function wfWikiArticleFeedsAction($action, $article) {
 312+
 313+ # If some other action is in the works, cut and run!
 314+ if ($action!='feed') return true;
 315+
 316+ global $wgOut, $wgRequest, $wgFeedClasses, $wgFeedCacheTimeout, $wgDBname, $messageMemc, $wgSitename;
 317+
 318+ # Get query parameters
 319+ $feedFormat = $wgRequest->getVal( 'feed', 'atom');
 320+ $filterTags = $wgRequest->getVal('tags', null);
 321+
 322+ # Process requested tags for use in keys
 323+ if ($filterTags) {
 324+ $filterTags = explode(',',$filterTags);
 325+ array_walk($filterTags, 'trim');
 326+ sort($filterTags);
 327+ }
 328+
 329+ if( !isset( $wgFeedClasses[$feedFormat] ) ) {
 330+ wfHttpError( 500, "Internal Server Error", "Unsupported feed type." );
 331+ return false;
 332+ }
 333+
 334+ # Setup cache-checking vars
 335+ $title = $article->getTitle();
 336+ $titleDBkey = $title->getPrefixedDBkey();
 337+ $tags = (is_array($filterTags)?':'.implode(',',$filterTags):'');
 338+ $key = "{$wgDBname}:wikiarticlefeedsextension:{$titleDBkey}:{$feedFormat}{$tags}";
 339+ $timekey = $key.":timestamp";
 340+ $cachedFeed = false;
 341+ $feedLastmod = $messageMemc->get($timekey);
 342+
 343+ # Dermine last modification time for either the article itself or an included template
 344+ $lastmod = $article->getTimestamp();
 345+ $templates = $article->getUsedTemplates();
 346+ foreach ($templates as $tTitle) {
 347+ $tArticle = new Article($tTitle);
 348+ $tmod = $tArticle->getTimestamp();
 349+ $lastmod = ($lastmod>$tmod?$lastmod:$tmod);
 350+ }
 351+
 352+ # Check for availability of existing cached version
 353+ if( ( $wgFeedCacheTimeout > 0 ) && $feedLastmod ) {
 354+ $feedLastmodTS = wfTimestamp( TS_UNIX, $feedLastmod );
 355+ if(
 356+ time() - $feedLastmodTS < $wgFeedCacheTimeout ||
 357+ $feedLastmodTS > wfTimestamp( TS_UNIX, $lastmod )
 358+ ) {
 359+ wfDebug( "WikiArticleFeedsExtension: Loading feed from cache ($key; $feedLastmod; $lastmod)...\n" );
 360+ $cachedFeed = $messageMemc->get( $key );
 361+ } else {
 362+ wfDebug( "WikiArticleFeedsExtension: Cached feed timestamp check failed ($feedLastmod; $lastmod)\n" );
 363+ }
 364+ }
 365+
 366+ # Display cachedFeed, or generate one from scratch
 367+ global $wgWikiArticleFeedsSkipCache;
 368+ if(!$wgWikiArticleFeedsSkipCache && is_string( $cachedFeed ) ) {
 369+ wfDebug( "WikiArticleFeedsExtension: Outputting cached feed\n" );
 370+ $feed = new $wgFeedClasses[$feedFormat]($wgSitename.' - '.$title->getText(),'', $title->getFullURL());
 371+ $feed->httpHeaders();
 372+ echo $cachedFeed;
 373+ } else {
 374+ wfDebug( "WikiArticleFeedsExtension: Rendering new feed and caching it\n" );
 375+ ob_start();
 376+ wfGenerateWikiFeed( $article, $feedFormat, $filterTags );
 377+ $cachedFeed = ob_get_contents();
 378+ ob_end_flush();
 379+
 380+ $expire = 3600 * 24; # One day
 381+ $messageMemc->set( $key, $cachedFeed );
 382+ $messageMemc->set( $timekey, wfTimestamp( TS_MW ), $expire );
 383+ }
 384+
 385+ # False to indicate that other action handlers should not process this page
 386+ return false;
 387+}
 388+
 389+/**
 390+ * Purges all associated feeds when an Article is purged.
 391+ * Usage: $wgHooks['ArticlePurge'][] = 'wfPurgeFeedsOnArticlePurge';
 392+ * @param Article $article The Article which is being purged.
 393+ * @return boolean Always true to permit additional hook processing.
 394+ */
 395+function wfPurgeFeedsOnArticlePurge($article) {
 396+ global $messageMemc,$wgDBname;
 397+ $titleDBKey = $article->mTitle->getPrefixedDBkey();
 398+ $keyPrefix = "{$wgDBname}:wikiarticlefeedsextension:{$titleDBKey}";
 399+ $messageMemc->delete("{$keyPrefix}:atom:timestamp");
 400+ $messageMemc->delete("{$keyPrefix}:atom");
 401+ $messageMemc->delete("{$keyPrefix}:rss");
 402+ $messageMemc->delete("{$keyPrefix}:rss:timestamp");
 403+ return true;
 404+}
 405+
 406+/**
 407+ * Converts an MArticle into a feed, echoing generated content directly.
 408+ * @param Article $article Article to be converted to RSS or Atom feed.
 409+ * @param String $feedFormat A format type - must be either 'rss' or 'atom'
 410+ * @param Array $filterTags Tags to use in filtering out items.
 411+ */
 412+function wfGenerateWikiFeed($article, $feedFormat = 'atom', $filterTags = null) {
 413+
 414+ global $wgOut, $wgScript, $wgServer, $wgFeedClasses, $wgMessageCache, $wgVersion, $wgSitename;
 415+
 416+ # Setup, handle redirects
 417+ if ($article->isRedirect()) {
 418+ $rtitle = Title::newFromRedirect($article->getContent());
 419+ if ($rtitle) {
 420+ $article = new Article($rtitle);
 421+ }
 422+ }
 423+ $title = $article->getTitle();
 424+ $feedUrl = $title->getFullUrl();
 425+
 426+ $wgMessageCache->addMessage('wikiarticlefeeds_combined_description', 'This is a combination feed, containing items from several feed sources.');
 427+
 428+ # Parse page into feed items.
 429+ $content = $wgOut->parse($article->getContent()."\n__NOEDITSECTION__ __NOTOC__");
 430+ preg_match_all(
 431+ '/<!--\\s*FEED_START\\s*-->(.*?)<!--\\s*FEED_END\\s*-->/s',
 432+ $content,
 433+ $matches
 434+ );
 435+ $feedContentSections = $matches[1];
 436+
 437+ # Parse and process all feeds, collecting feed items
 438+ $items = array();
 439+ $feedDescription = '';
 440+ foreach ($feedContentSections as $feedKey=>$feedContent) {
 441+
 442+ # Determine Feed item depth (what header level defines a feed)
 443+ preg_match_all( '/<h(\\d)>/m', $feedContent, $matches);
 444+ if (empty($matches[1])) next;
 445+ $lvl = $matches[1][0];
 446+ foreach ($matches[1] as $match) {
 447+ if ($match < $lvl) $lvl = $match;
 448+ }
 449+
 450+ # Determine the item titles and default item links
 451+ preg_match_all(
 452+ '/<a[^>]*\\s+name=([\'"])(.*?)\\1[^>]*><\\/a><h'.$lvl.'>\\s*(.*?)\\s*<\\/h'.$lvl.'>/m',
 453+ $feedContent,
 454+ $matches
 455+ );
 456+ $itemLinks = $matches[2];
 457+ $itemTitles = $matches[3];
 458+
 459+ # Split content into segments
 460+ $segments = preg_split( '/<a name=([\'"]).*?\\1\\s*><\\/a><h'.$lvl.'>.*?<\\/h'.$lvl.'>/m', $feedContent);
 461+ $segDesc = trim(strip_tags(array_shift($segments)));
 462+ if ($segDesc) {
 463+ if (!$feedDescription) {
 464+ $feedDescription = $segDesc;
 465+ } else {
 466+ $feedDescription = wfMsg('wikiarticlefeeds_combined_description');
 467+ }
 468+ }
 469+
 470+ # Loop over parsed segments and add all items to item array
 471+ foreach ($segments as $key=>$seg) {
 472+
 473+ # Filter by tag (if any are present)
 474+ $skip = false;
 475+ $tags = null;
 476+ if (is_array($filterTags) && !empty($filterTags)) {
 477+ if (preg_match_all('/<!-- ITEM_TAGS ([0-9a-zA-Z\\+\\/]+=*) -->/m', $seg, $matches)) {
 478+ $tags = array();
 479+ foreach ($matches[1] as $encodedString) {
 480+ $t = @unserialize(@base64_decode($encodedString));
 481+ if ($t) {
 482+ $t = explode(',',$t);
 483+ array_walk($t, 'trim');
 484+ sort($t);
 485+ $tags = array_merge($tags, $t);
 486+ }
 487+ }
 488+ $tags = array_unique($tags);
 489+ if (!count(array_intersect($tags, $filterTags))) $skip = true;
 490+ $seg = preg_replace('/<!-- ITEM_TAGS ([0-9a-zA-Z\\+\\/]+=*) -->/m', '', $seg);
 491+ } else {
 492+ $skip = true;
 493+ }
 494+ }
 495+ if ($skip) continue;
 496+
 497+ # Determine the item author and date
 498+ $author = null;
 499+ $date = null;
 500+
 501+ # Look for a regular ~~~~ sig
 502+ $isAttributable = preg_match(
 503+ '%<a [^>]*href=([\'"])'.preg_quote($wgScript).'(/|\\?title=)User:.*?\\1[^>]*>(.*?)</a> (\\d\\d):(\\d\\d), (\\d+) ([A-z][a-z]+) (\\d{4}) \\([A-Z]+\\)%m',
 504+ $seg,
 505+ $matches
 506+ );
 507+
 508+ # As a fallback - look for a --~~~~ like sig with a user page outside the User NS
 509+ if (!$isAttributable) {
 510+ $isAttributable = preg_match(
 511+ '%--<a [^>]*href=([\'"])'.preg_quote($wgScript).'(/|\\?title=).*?\\1[^>]*>(.*?)</a> (\\d\\d):(\\d\\d), (\\d+) ([A-z][a-z]+) (\\d{4}) \\([A-Z]+\\)%m',
 512+ $seg,
 513+ $matches
 514+ );
 515+ }
 516+
 517+ # Parse it out - if we can
 518+ if ($isAttributable) {
 519+ list($author, $hour, $min, $day, $monthName, $year) = array_slice($matches, 3);
 520+ $months = array(
 521+ 'January' => '01', 'February' => '02', 'March' => '03', 'April' => '04',
 522+ 'May' => '05', 'June' => '06', 'July' => '07', 'August' => '08',
 523+ 'September' => '09', 'October' => '10', 'November' => '11', 'December' => '12'
 524+ );
 525+ $month = $months[$monthName];
 526+ $day = str_pad($day,2,'0',STR_PAD_LEFT);
 527+ $date = $year.$month.$day.$hour.$min.'00';
 528+ }
 529+
 530+ # Set default 'article section' feed-link
 531+ $url = $feedUrl.'#'.$itemLinks[$key];
 532+
 533+ # Look for an alternative to the default link (unless default 'section linking' has been forced)
 534+ global $wgForceArticleFeedSectionLinks;
 535+ if (!$wgForceArticleFeedSectionLinks) {
 536+ $strippedSeg = preg_replace(
 537+ array(
 538+ '%<a [^>]*href=([\'"])'.preg_quote($wgScript).'(/|\\?title=)User:.*?\\1[^>]*>(.*?)</a> (\\d\\d:\\d\\d, \\d+ [A-z][a-z]+ \\d{4} \\([A-Z]+\\))%m',
 539+ '%--<a [^>]*href=([\'"])'.preg_quote($wgScript).'(/|\\?title=).*?\\1[^>]*>(.*?)</a> (\\d\\d:\\d\\d, \\d+ [A-z][a-z]+ \\d{4} \\([A-Z]+\\))%m'
 540+ ),
 541+ '',
 542+ $seg
 543+ );
 544+ preg_match(
 545+ '%<a [^>]*href=([\'"])(.*?)\\1[^>]*>(.*?)</a>%m',
 546+ $strippedSeg,
 547+ $matches
 548+ );
 549+ if ($matches[2]) {
 550+ $url = $matches[2];
 551+ if (preg_match('%^/%', $url)) {
 552+ $url = $wgServer.$url;
 553+ }
 554+ }
 555+ }
 556+
 557+ # Create 'absolutified' segment - where all URLs are fully qualified
 558+ $seg = preg_replace('/ (href|src)=([\'"])\\//',' $1=$2'.$wgServer.'/',$seg);
 559+
 560+ # Create item and push onto item list
 561+ $items[$date][] = new FeedItem(strip_tags($itemTitles[$key]), $seg, $url, $date, $author);
 562+ }
 563+ }
 564+
 565+ # Programmatically determine the feed title and id.
 566+ $feedTitle = $wgSitename.' - '.$title->getPrefixedText();
 567+ $feedId = $title->getFullUrl();
 568+
 569+ # Create feed
 570+ $feed = new $wgFeedClasses[$feedFormat]($feedTitle, $feedDescription, $feedId);
 571+
 572+ # Push feed header
 573+ $tempWgVersion = $wgVersion;
 574+ $wgVersion .= ' via WikiArticleFeeds '.WIKIARTICLEFEEDS_VERSION;
 575+ $feed->outHeader();
 576+ $wgVersion = $tempWgVersion;
 577+
 578+ # Sort all items by date and push onto feed
 579+ krsort($items);
 580+ foreach ($items as $itemGroup) {
 581+ foreach ($itemGroup as $item) {
 582+ $feed->outItem($item);
 583+ }
 584+ }
 585+
 586+ # Feed footer
 587+ $feed->outFooter();
 588+}
 589+

Status & tagging log