1<?php 2/** 3 * XML feed export 4 * 5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 6 * @author Andreas Gohr <andi@splitbrain.org> 7 * 8 * @global array $conf 9 * @global Input $INPUT 10 */ 11 12if(!defined('DOKU_INC')) define('DOKU_INC', dirname(__FILE__).'/'); 13require_once(DOKU_INC.'inc/init.php'); 14 15//close session 16session_write_close(); 17 18//feed disabled? 19if(!actionOK('rss')) { 20 echo '<error>RSS feed is disabled.</error>'; 21 exit; 22} 23 24// get params 25$opt = rss_parseOptions(); 26 27// the feed is dynamic - we need a cache for each combo 28// (but most people just use the default feed so it's still effective) 29$key = join('', array_values($opt)).'$'.$_SERVER['REMOTE_USER'].'$'.$_SERVER['HTTP_HOST'].$_SERVER['SERVER_PORT']; 30$cache = new cache($key, '.feed'); 31 32// prepare cache depends 33$depends['files'] = getConfigFiles('main'); 34$depends['age'] = $conf['rss_update']; 35$depends['purge'] = $INPUT->bool('purge'); 36 37// check cacheage and deliver if nothing has changed since last 38// time or the update interval has not passed, also handles conditional requests 39header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); 40header('Pragma: public'); 41header('Content-Type: application/xml; charset=utf-8'); 42header('X-Robots-Tag: noindex'); 43if($cache->useCache($depends)) { 44 http_conditionalRequest($cache->_time); 45 if($conf['allowdebug']) header("X-CacheUsed: $cache->cache"); 46 print $cache->retrieveCache(); 47 exit; 48} else { 49 http_conditionalRequest(time()); 50} 51 52// create new feed 53$rss = new DokuWikiFeedCreator(); 54$rss->title = $conf['title'].(($opt['namespace']) ? ' '.$opt['namespace'] : ''); 55$rss->link = DOKU_URL; 56$rss->syndicationURL = DOKU_URL.'feed.php'; 57$rss->cssStyleSheet = DOKU_URL.'lib/exe/css.php?s=feed'; 58 59$image = new FeedImage(); 60$image->title = $conf['title']; 61$image->url = tpl_getMediaFile(array(':wiki:favicon.ico', ':favicon.ico', 'images/favicon.ico'), true); 62$image->link = DOKU_URL; 63$rss->image = $image; 64 65$data = null; 66$modes = array( 67 'list' => 'rssListNamespace', 68 'search' => 'rssSearch', 69 'recent' => 'rssRecentChanges' 70); 71if(isset($modes[$opt['feed_mode']])) { 72 $data = $modes[$opt['feed_mode']]($opt); 73} else { 74 $eventData = array( 75 'opt' => &$opt, 76 'data' => &$data, 77 ); 78 $event = new Doku_Event('FEED_MODE_UNKNOWN', $eventData); 79 if($event->advise_before(true)) { 80 echo sprintf('<error>Unknown feed mode %s</error>', hsc($opt['feed_mode'])); 81 exit; 82 } 83 $event->advise_after(); 84} 85 86rss_buildItems($rss, $data, $opt); 87$feed = $rss->createFeed($opt['feed_type'], 'utf-8'); 88 89// save cachefile 90$cache->storeCache($feed); 91 92// finally deliver 93print $feed; 94 95// ---------------------------------------------------------------- // 96 97/** 98 * Get URL parameters and config options and return an initialized option array 99 * 100 * @author Andreas Gohr <andi@splitbrain.org> 101 */ 102function rss_parseOptions() { 103 global $conf; 104 global $INPUT; 105 106 $opt = array(); 107 108 foreach(array( 109 // Basic feed properties 110 // Plugins may probably want to add new values to these 111 // properties for implementing own feeds 112 113 // One of: list, search, recent 114 'feed_mode' => array('str', 'mode', 'recent'), 115 // One of: diff, page, rev, current 116 'link_to' => array('str', 'linkto', $conf['rss_linkto']), 117 // One of: abstract, diff, htmldiff, html 118 'item_content' => array('str', 'content', $conf['rss_content']), 119 120 // Special feed properties 121 // These are only used by certain feed_modes 122 123 // String, used for feed title, in list and rc mode 124 'namespace' => array('str', 'ns', null), 125 // Positive integer, only used in rc mode 126 'items' => array('int', 'num', $conf['recent']), 127 // Boolean, only used in rc mode 128 'show_minor' => array('bool', 'minor', false), 129 // String, only used in search mode 130 'search_query' => array('str', 'q', null), 131 // One of: pages, media, both 132 'content_type' => array('str', 'view', $conf['rss_media']) 133 134 ) as $name => $val) { 135 $opt[$name] = $INPUT->$val[0]($val[1], $val[2], true); 136 } 137 138 $opt['items'] = max(0, (int) $opt['items']); 139 $opt['show_minor'] = (bool) $opt['show_minor']; 140 141 $opt['guardmail'] = ($conf['mailguard'] != '' && $conf['mailguard'] != 'none'); 142 143 $type = valid_input_set( 144 'type', array( 145 'rss', 'rss2', 'atom', 'atom1', 'rss1', 146 'default' => $conf['rss_type'] 147 ), 148 $_REQUEST 149 ); 150 switch($type) { 151 case 'rss': 152 $opt['feed_type'] = 'RSS0.91'; 153 $opt['mime_type'] = 'text/xml'; 154 break; 155 case 'rss2': 156 $opt['feed_type'] = 'RSS2.0'; 157 $opt['mime_type'] = 'text/xml'; 158 break; 159 case 'atom': 160 $opt['feed_type'] = 'ATOM0.3'; 161 $opt['mime_type'] = 'application/xml'; 162 break; 163 case 'atom1': 164 $opt['feed_type'] = 'ATOM1.0'; 165 $opt['mime_type'] = 'application/atom+xml'; 166 break; 167 default: 168 $opt['feed_type'] = 'RSS1.0'; 169 $opt['mime_type'] = 'application/xml'; 170 } 171 172 $eventData = array( 173 'opt' => &$opt, 174 ); 175 trigger_event('FEED_OPTS_POSTPROCESS', $eventData); 176 return $opt; 177} 178 179/** 180 * Add recent changed pages to a feed object 181 * 182 * @author Andreas Gohr <andi@splitbrain.org> 183 * @param FeedCreator $rss the FeedCreator Object 184 * @param array $data the items to add 185 * @param array $opt the feed options 186 */ 187function rss_buildItems(&$rss, &$data, $opt) { 188 global $conf; 189 global $lang; 190 /* @var DokuWiki_Auth_Plugin $auth */ 191 global $auth; 192 193 $eventData = array( 194 'rss' => &$rss, 195 'data' => &$data, 196 'opt' => &$opt, 197 ); 198 $event = new Doku_Event('FEED_DATA_PROCESS', $eventData); 199 if($event->advise_before(false)) { 200 foreach($data as $ditem) { 201 if(!is_array($ditem)) { 202 // not an array? then only a list of IDs was given 203 $ditem = array('id' => $ditem); 204 } 205 206 $item = new FeedItem(); 207 $id = $ditem['id']; 208 if(!$ditem['media']) { 209 $meta = p_get_metadata($id); 210 } else { 211 $meta = array(); 212 } 213 214 // add date 215 if($ditem['date']) { 216 $date = $ditem['date']; 217 } elseif ($ditem['media']) { 218 $date = @filemtime(mediaFN($id)); 219 } elseif (@file_exists(wikiFN($id))) { 220 $date = @filemtime(wikiFN($id)); 221 } elseif($meta['date']['modified']) { 222 $date = $meta['date']['modified']; 223 } else { 224 $date = 0; 225 } 226 if($date) $item->date = date('r', $date); 227 228 // add title 229 if($conf['useheading'] && $meta['title']) { 230 $item->title = $meta['title']; 231 } else { 232 $item->title = $ditem['id']; 233 } 234 if($conf['rss_show_summary'] && !empty($ditem['sum'])) { 235 $item->title .= ' - '.strip_tags($ditem['sum']); 236 } 237 238 // add item link 239 switch($opt['link_to']) { 240 case 'page': 241 if($ditem['media']) { 242 $item->link = media_managerURL( 243 array( 244 'image' => $id, 245 'ns' => getNS($id), 246 'rev' => $date 247 ), '&', true 248 ); 249 } else { 250 $item->link = wl($id, 'rev='.$date, true, '&'); 251 } 252 break; 253 case 'rev': 254 if($ditem['media']) { 255 $item->link = media_managerURL( 256 array( 257 'image' => $id, 258 'ns' => getNS($id), 259 'rev' => $date, 260 'tab_details' => 'history' 261 ), '&', true 262 ); 263 } else { 264 $item->link = wl($id, 'do=revisions&rev='.$date, true, '&'); 265 } 266 break; 267 case 'current': 268 if($ditem['media']) { 269 $item->link = media_managerURL( 270 array( 271 'image' => $id, 272 'ns' => getNS($id) 273 ), '&', true 274 ); 275 } else { 276 $item->link = wl($id, '', true, '&'); 277 } 278 break; 279 case 'diff': 280 default: 281 if($ditem['media']) { 282 $item->link = media_managerURL( 283 array( 284 'image' => $id, 285 'ns' => getNS($id), 286 'rev' => $date, 287 'tab_details' => 'history', 288 'mediado' => 'diff' 289 ), '&', true 290 ); 291 } else { 292 $item->link = wl($id, 'rev='.$date.'&do=diff', true, '&'); 293 } 294 } 295 296 // add item content 297 switch($opt['item_content']) { 298 case 'diff': 299 case 'htmldiff': 300 if($ditem['media']) { 301 $medialog = new MediaChangeLog($id); 302 $revs = $medialog->getRevisions(0, 1); 303 $rev = $revs[0]; 304 $src_r = ''; 305 $src_l = ''; 306 307 if($size = media_image_preview_size($id, false, new JpegMeta(mediaFN($id)), 300)) { 308 $more = 'w='.$size[0].'&h='.$size[1].'&t='.@filemtime(mediaFN($id)); 309 $src_r = ml($id, $more, true, '&', true); 310 } 311 if($rev && $size = media_image_preview_size($id, $rev, new JpegMeta(mediaFN($id, $rev)), 300)) { 312 $more = 'rev='.$rev.'&w='.$size[0].'&h='.$size[1]; 313 $src_l = ml($id, $more, true, '&', true); 314 } 315 $content = ''; 316 if($src_r) { 317 $content = '<table>'; 318 $content .= '<tr><th width="50%">'.$rev.'</th>'; 319 $content .= '<th width="50%">'.$lang['current'].'</th></tr>'; 320 $content .= '<tr align="center"><td><img src="'.$src_l.'" alt="" /></td><td>'; 321 $content .= '<img src="'.$src_r.'" alt="'.$id.'" /></td></tr>'; 322 $content .= '</table>'; 323 } 324 325 } else { 326 require_once(DOKU_INC.'inc/DifferenceEngine.php'); 327 $pagelog = new PageChangeLog($id); 328 $revs = $pagelog->getRevisions(0, 1); 329 $rev = $revs[0]; 330 331 if($rev) { 332 $df = new Diff(explode("\n", rawWiki($id, $rev)), 333 explode("\n", rawWiki($id, ''))); 334 } else { 335 $df = new Diff(array(''), 336 explode("\n", rawWiki($id, ''))); 337 } 338 339 if($opt['item_content'] == 'htmldiff') { 340 // note: no need to escape diff output, TableDiffFormatter provides 'safe' html 341 $tdf = new TableDiffFormatter(); 342 $content = '<table>'; 343 $content .= '<tr><th colspan="2" width="50%">'.$rev.'</th>'; 344 $content .= '<th colspan="2" width="50%">'.$lang['current'].'</th></tr>'; 345 $content .= $tdf->format($df); 346 $content .= '</table>'; 347 } else { 348 // note: diff output must be escaped, UnifiedDiffFormatter provides plain text 349 $udf = new UnifiedDiffFormatter(); 350 $content = "<pre>\n".hsc($udf->format($df))."\n</pre>"; 351 } 352 } 353 break; 354 case 'html': 355 if($ditem['media']) { 356 if($size = media_image_preview_size($id, false, new JpegMeta(mediaFN($id)))) { 357 $more = 'w='.$size[0].'&h='.$size[1].'&t='.@filemtime(mediaFN($id)); 358 $src = ml($id, $more, true, '&', true); 359 $content = '<img src="'.$src.'" alt="'.$id.'" />'; 360 } else { 361 $content = ''; 362 } 363 } else { 364 if (@filemtime(wikiFN($id)) === $date) { 365 $content = p_wiki_xhtml($id, '', false); 366 } else { 367 $content = p_wiki_xhtml($id, $date, false); 368 } 369 // no TOC in feeds 370 $content = preg_replace('/(<!-- TOC START -->).*(<!-- TOC END -->)/s', '', $content); 371 372 // add alignment for images 373 $content = preg_replace('/(<img .*?class="medialeft")/s', '\\1 align="left"', $content); 374 $content = preg_replace('/(<img .*?class="mediaright")/s', '\\1 align="right"', $content); 375 376 // make URLs work when canonical is not set, regexp instead of rerendering! 377 if(!$conf['canonical']) { 378 $base = preg_quote(DOKU_REL, '/'); 379 $content = preg_replace('/(<a href|<img src)="('.$base.')/s', '$1="'.DOKU_URL, $content); 380 } 381 } 382 383 break; 384 case 'abstract': 385 default: 386 if($ditem['media']) { 387 if($size = media_image_preview_size($id, false, new JpegMeta(mediaFN($id)))) { 388 $more = 'w='.$size[0].'&h='.$size[1].'&t='.@filemtime(mediaFN($id)); 389 $src = ml($id, $more, true, '&', true); 390 $content = '<img src="'.$src.'" alt="'.$id.'" />'; 391 } else { 392 $content = ''; 393 } 394 } else { 395 $content = $meta['description']['abstract']; 396 } 397 } 398 $item->description = $content; //FIXME a plugin hook here could be senseful 399 400 // add user 401 # FIXME should the user be pulled from metadata as well? 402 $user = @$ditem['user']; // the @ spares time repeating lookup 403 $item->author = ''; 404 if($user && $conf['useacl'] && $auth) { 405 $userInfo = $auth->getUserData($user); 406 if($userInfo) { 407 switch($conf['showuseras']) { 408 case 'username': 409 $item->author = $userInfo['name']; 410 break; 411 default: 412 $item->author = $user; 413 break; 414 } 415 } else { 416 $item->author = $user; 417 } 418 if($userInfo && !$opt['guardmail']) { 419 $item->authorEmail = $userInfo['mail']; 420 } else { 421 //cannot obfuscate because some RSS readers may check validity 422 $item->authorEmail = $user.'@'.$ditem['ip']; 423 } 424 } elseif($user) { 425 // this happens when no ACL but some Apache auth is used 426 $item->author = $user; 427 $item->authorEmail = $user.'@'.$ditem['ip']; 428 } else { 429 $item->authorEmail = 'anonymous@'.$ditem['ip']; 430 } 431 432 // add category 433 if(isset($meta['subject'])) { 434 $item->category = $meta['subject']; 435 } else { 436 $cat = getNS($id); 437 if($cat) $item->category = $cat; 438 } 439 440 // finally add the item to the feed object, after handing it to registered plugins 441 $evdata = array( 442 'item' => &$item, 443 'opt' => &$opt, 444 'ditem' => &$ditem, 445 'rss' => &$rss 446 ); 447 $evt = new Doku_Event('FEED_ITEM_ADD', $evdata); 448 if($evt->advise_before()) { 449 $rss->addItem($item); 450 } 451 $evt->advise_after(); // for completeness 452 } 453 } 454 $event->advise_after(); 455} 456 457/** 458 * Add recent changed pages to the feed object 459 * 460 * @author Andreas Gohr <andi@splitbrain.org> 461 */ 462function rssRecentChanges($opt) { 463 global $conf; 464 $flags = RECENTS_SKIP_DELETED; 465 if(!$opt['show_minor']) $flags += RECENTS_SKIP_MINORS; 466 if($opt['content_type'] == 'media' && $conf['mediarevisions']) $flags += RECENTS_MEDIA_CHANGES; 467 if($opt['content_type'] == 'both' && $conf['mediarevisions']) $flags += RECENTS_MEDIA_PAGES_MIXED; 468 469 $recents = getRecents(0, $opt['items'], $opt['namespace'], $flags); 470 return $recents; 471} 472 473/** 474 * Add all pages of a namespace to the feed object 475 * 476 * @author Andreas Gohr <andi@splitbrain.org> 477 */ 478function rssListNamespace($opt) { 479 require_once(DOKU_INC.'inc/search.php'); 480 global $conf; 481 482 $ns = ':'.cleanID($opt['namespace']); 483 $ns = str_replace(':', '/', $ns); 484 485 $data = array(); 486 $search_opts = array( 487 'depth' => 1, 488 'pagesonly' => true, 489 'listfiles' => true 490 ); 491 search($data, $conf['datadir'], 'search_universal', $search_opts, $ns); 492 493 return $data; 494} 495 496/** 497 * Add the result of a full text search to the feed object 498 * 499 * @author Andreas Gohr <andi@splitbrain.org> 500 */ 501function rssSearch($opt) { 502 if(!$opt['search_query']) return array(); 503 504 require_once(DOKU_INC.'inc/fulltext.php'); 505 $data = ft_pageSearch($opt['search_query'], $poswords); 506 $data = array_keys($data); 507 508 return $data; 509} 510 511//Setup VIM: ex: et ts=4 : 512