Merge pull request #4156 from dokuwiki/feedrefactor

Feed creation refactoring
This commit is contained in:
Andreas Gohr 2024-01-26 14:43:14 +01:00 committed by GitHub
commit 754eefa383
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 1282 additions and 45 deletions

View File

@ -0,0 +1,85 @@
<?php
namespace dokuwiki\test\Feed;
use dokuwiki\Feed\FeedCreator;
use dokuwiki\Feed\FeedCreatorOptions;
use dokuwiki\HTTP\DokuHTTPClient;
/**
* @group internet
*/
class FeedCreatorTest extends \DokuWikiTest
{
/**
* @todo This only test the default feed, various configurations could be tested
*
* @return void
* @throws \Exception
*/
public function testValidate()
{
$options = new FeedCreatorOptions();
$creator = new FeedCreator($options);
$feed = $creator->build();
$http = new DokuHTTPClient();
$result = $http->post('https://validator.w3.org/feed/check.cgi', [
'rawdata' => $feed,
'output' => 'soap12',
]);
if (!$result) {
$this->markTestSkipped('Could not validate feed');
}
//print($result);
$xml = new \SimpleXMLElement($result);
$ns = $xml->getNamespaces(true);
foreach ($ns as $key => $value) {
$xml->registerXPathNamespace($key, $value);
}
$warningCount = (int)$xml->xpath('//m:warnings/m:warningcount')[0];
if ($warningCount > 0) {
$line = (int)$xml->xpath('//m:warnings/m:warninglist/warning/line')[0];
$text = (string)$xml->xpath('//m:warnings/m:warninglist/warning/text')[0];
$element = (string)$xml->xpath('//m:warnings/m:warninglist/warning/element')[0];
$parent = (string)$xml->xpath('//m:warnings/m:warninglist/warning/parent')[0];
$lines = explode("\n", $feed);
$show = trim($lines[$line - 1]);
$this->addWarning(
"Feed validation produced a warning:\n" .
"Line $line: $text\n" .
"$parent -> $element\n" .
$show
);
}
$errorCount = (int)$xml->xpath('//m:errors/m:errorcount')[0];
if ($errorCount > 0) {
$line = (int)$xml->xpath('//m:errors/m:errorlist/error/line')[0];
$text = (string)$xml->xpath('//m:errors/m:errorlist/error/text')[0];
$element = (string)$xml->xpath('//m:errors/m:errorlist/error/element')[0];
$parent = (string)$xml->xpath('//m:errors/m:errorlist/error/parent')[0];
$lines = explode("\n", $feed);
$show = trim($lines[$line - 1]);
$this->fail(
"Feed validation produced an error:\n" .
"Line $line: $text\n" .
"$parent -> $element\n" .
$show
);
}
$this->assertTrue(true);
}
}

View File

@ -0,0 +1,93 @@
<?php
namespace dokuwiki\test\Feed;
use dokuwiki\Feed\FeedMediaProcessor;
use DOMWrap\Document;
class FeedMediaProcessorTest extends \DokuWikiTest
{
public function provideData()
{
// an Item returned by FeedCreator::fetchItemsFromRecentChanges()
yield ([
array(
'date' => 1705511543,
'ip' => '::1',
'type' => 'C',
'id' => 'wiki:dokuwiki-128.png',
'user' => 'testuser',
'sum' => 'created',
'extra' => '',
'sizechange' => 52618,
'perms' => 8,
'mode' => 'media',
),
1705511543, // fixed revision
['testuser@undisclosed.example.com', 'Arthur Dent'], // proper author
'created', // summary
]);
// FeedCreator::fetchItemsFromNamespace() currently does not support media files
// FeedCreator::fetchItemsFromSearch() currently does not support media files
}
/**
* @dataProvider provideData
*/
public function testProcessing($data, $expectedMtime, $expectedAuthor, $expectedSummary)
{
global $conf;
$conf['useacl'] = 1;
$conf['showuseras'] = 'username';
$conf['useheading'] = 1;
$proc = new FeedMediaProcessor($data);
$this->assertEquals('wiki:dokuwiki-128.png', $proc->getId());
$this->assertEquals('dokuwiki-128.png', $proc->getTitle());
$this->assertEquals($expectedAuthor, $proc->getAuthor());
$this->assertEquals($expectedMtime, $proc->getRev());
$this->assertEquals(null, $proc->getPrev());
$this->assertTrue($proc->isExisting());
$this->assertTrue($proc->isExisting());
$this->assertEquals(['wiki'], $proc->getCategory());
$this->assertEquals($expectedSummary, $proc->getSummary());
$this->assertEquals(
"http://wiki.example.com/doku.php?image=wiki%3Adokuwiki-128.png&ns=wiki&rev=$expectedMtime&do=media",
$proc->getURL('page')
);
$this->assertEquals(
"http://wiki.example.com/doku.php?image=wiki%3Adokuwiki-128.png&ns=wiki&rev=$expectedMtime&tab_details=history&do=media",
$proc->getURL('rev')
);
$this->assertEquals(
"http://wiki.example.com/doku.php?image=wiki%3Adokuwiki-128.png&ns=wiki&do=media",
$proc->getURL('current')
);
$this->assertEquals(
"http://wiki.example.com/doku.php?image=wiki%3Adokuwiki-128.png&ns=wiki&rev=$expectedMtime&tab_details=history&media_do=diff&do=media",
$proc->getURL('diff')
);
$doc = new Document();
$doc->html($proc->getBody('diff'));
$th = $doc->find('table th');
$this->assertGreaterThanOrEqual(2, $th->count());
$doc = new Document();
$doc->html($proc->getBody('htmldiff'));
$th = $doc->find('table th');
$this->assertGreaterThanOrEqual(2, $th->count());
$doc = new Document();
$doc->html($proc->getBody('html'));
$home = $doc->find('img');
$this->assertEquals(1, $home->count());
}
}

View File

@ -0,0 +1,122 @@
<?php
namespace dokuwiki\test\Feed;
use dokuwiki\Feed\FeedPageProcessor;
use DOMWrap\Document;
class FeedPageProcessorTest extends \DokuWikiTest
{
public function provideData()
{
// an Item returned by FeedCreator::fetchItemsFromRecentChanges()
yield ([
[
'date' => 1705501370,
'ip' => '::1',
'type' => 'E',
'id' => 'wiki:dokuwiki',
'user' => 'testuser',
'sum' => 'test editing',
'extra' => '',
'sizechange' => 41,
'perms' => 8,
'mode' => 'page',
],
1705501370, // fixed revision
['testuser@undisclosed.example.com', 'Arthur Dent'], // proper author
'test editing', // summary
]);
// an Item returned by FeedCreator::fetchItemsFromNamespace()
yield ([
[
'id' => 'wiki:dokuwiki',
'ns' => 'wiki',
'perm' => 8,
'type' => 'f',
'level' => 1,
'open' => true,
],
null, // current revision
['anonymous@undisclosed.example.com', 'Anonymous'], // unknown author
'', // no summary
]);
// an Item returned by FeedCreator::fetchItemsFromSearch()
yield ([
[
'id' => 'wiki:dokuwiki',
],
null, // current revision
['anonymous@undisclosed.example.com', 'Anonymous'], // unknown author
'', // no summary
]);
}
/**
* @dataProvider provideData
*/
public function testProcessing($data, $expectedMtime, $expectedAuthor, $expectedSummary)
{
global $conf;
$conf['useacl'] = 1;
$conf['showuseras'] = 'username';
$conf['useheading'] = 1;
// if no expected mtime is given, we expect the filemtime of the page
// see https://github.com/dokuwiki/dokuwiki/pull/4156#issuecomment-1911842452 why we can't
// create this in the data provider
if ($expectedMtime === null) {
$expectedMtime = filemtime(wikiFN($data['id']));
}
$proc = new FeedPageProcessor($data);
$this->assertEquals('wiki:dokuwiki', $proc->getId());
$this->assertEquals('DokuWiki', $proc->getTitle());
$this->assertEquals($expectedAuthor, $proc->getAuthor());
$this->assertEquals($expectedMtime, $proc->getRev());
$this->assertEquals(null, $proc->getPrev());
$this->assertTrue($proc->isExisting());
$this->assertEquals(['wiki'], $proc->getCategory());
$this->assertStringContainsString('standards compliant', $proc->getAbstract());
$this->assertEquals($expectedSummary, $proc->getSummary());
$this->assertEquals(
"http://wiki.example.com/doku.php?id=wiki:dokuwiki&rev=$expectedMtime",
$proc->getURL('page')
);
$this->assertEquals(
"http://wiki.example.com/doku.php?id=wiki:dokuwiki&rev=$expectedMtime&do=revisions",
$proc->getURL('rev')
);
$this->assertEquals(
'http://wiki.example.com/doku.php?id=wiki:dokuwiki',
$proc->getURL('current')
);
$this->assertEquals(
"http://wiki.example.com/doku.php?id=wiki:dokuwiki&rev=$expectedMtime&do=diff",
$proc->getURL('diff')
);
$diff = explode("\n", $proc->getBody('diff'));
$this->assertEquals('<pre>', $diff[0]);
$this->assertStringStartsWith('@@', $diff[1]);
$doc = new Document();
$doc->html($proc->getBody('htmldiff'));
$th = $doc->find('table th');
$this->assertGreaterThanOrEqual(2, $th->count());
$doc = new Document();
$doc->html($proc->getBody('html'));
$home = $doc->find('a[href^="https://www.dokuwiki.org/manual"]');
$this->assertGreaterThanOrEqual(1, $home->count());
$this->assertStringContainsString('standards compliant', $proc->getBody('abstract'));
}
}

View File

@ -29,13 +29,16 @@ if (!actionOK('rss')) {
exit;
}
// get params
$opt = rss_parseOptions();
$options = new \dokuwiki\Feed\FeedCreatorOptions();
// the feed is dynamic - we need a cache for each combo
// (but most people just use the default feed so it's still effective)
$key = implode('', array_values($opt)) . '$' . $INPUT->server->str('REMOTE_USER')
. '$' . $INPUT->server->str('HTTP_HOST') . $INPUT->server->str('SERVER_PORT');
$key = implode('$', [
$options->getCacheKey(),
$INPUT->server->str('REMOTE_USER'),
$INPUT->server->str('HTTP_HOST'),
$INPUT->server->str('SERVER_PORT')
]);
$cache = new Cache($key, '.feed');
// prepare cache depends
@ -47,7 +50,7 @@ $depends['purge'] = $INPUT->bool('purge');
// time or the update interval has not passed, also handles conditional requests
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
header('Content-Type: application/xml; charset=utf-8');
header('Content-Type: ' . $options->get('mime_type'));
header('X-Robots-Tag: noindex');
if ($cache->useCache($depends)) {
http_conditionalRequest($cache->getTime());
@ -59,48 +62,16 @@ if ($cache->useCache($depends)) {
}
// create new feed
$rss = new UniversalFeedCreator();
$rss->title = $conf['title'] . (($opt['namespace']) ? ' ' . $opt['namespace'] : '');
$rss->link = DOKU_URL;
$rss->syndicationURL = DOKU_URL . 'feed.php';
$rss->cssStyleSheet = DOKU_URL . 'lib/exe/css.php?s=feed';
$image = new FeedImage();
$image->title = $conf['title'];
$image->url = tpl_getMediaFile([':wiki:favicon.ico', ':favicon.ico', 'images/favicon.ico'], true);
$image->link = DOKU_URL;
$rss->image = $image;
$data = null;
$modes = [
'list' => 'rssListNamespace',
'search' => 'rssSearch',
'recent' => 'rssRecentChanges'
];
if (isset($modes[$opt['feed_mode']])) {
$data = $modes[$opt['feed_mode']]($opt);
} else {
$eventData = [
'opt' => &$opt,
'data' => &$data,
];
$event = new Event('FEED_MODE_UNKNOWN', $eventData);
if ($event->advise_before(true)) {
echo sprintf('<error>Unknown feed mode %s</error>', hsc($opt['feed_mode']));
exit;
}
$event->advise_after();
try {
$feed = (new \dokuwiki\Feed\FeedCreator($options))->build();
$cache->storeCache($feed);
echo $feed;
} catch (Exception $e) {
http_status(500);
echo '<error>' . hsc($e->getMessage()) . '</error>';
exit;
}
rss_buildItems($rss, $data, $opt);
$feed = $rss->createFeed($opt['feed_type']);
// save cachefile
$cache->storeCache($feed);
// finally deliver
echo $feed;
// ---------------------------------------------------------------- //

222
inc/Feed/FeedCreator.php Normal file
View File

@ -0,0 +1,222 @@
<?php
namespace dokuwiki\Feed;
use dokuwiki\Extension\Event;
class FeedCreator
{
/** @var \UniversalFeedCreator */
protected $feed;
/** @var FeedCreatorOptions */
protected $options;
/**
* @param FeedCreatorOptions $options
*/
public function __construct(FeedCreatorOptions $options)
{
$this->options = $options;
$this->feed = new \UniversalFeedCreator();
$this->feed->title = $this->options->get('title');
$this->feed->description = $this->options->get('subtitle');
$this->feed->link = DOKU_URL;
$this->feed->syndicationURL = DOKU_URL . 'feed.php';
$this->feed->cssStyleSheet = DOKU_URL . 'lib/exe/css.php?s=feed';
$this->initLogo();
}
/**
* Build the feed
*
* @return string The raw XML for the feed
*/
public function build()
{
switch ($this->options->get('feed_mode')) {
case 'list':
$items = $this->fetchItemsFromNamespace();
break;
case 'search':
$items = $this->fetchItemsFromSearch();
break;
case 'recent':
$items = $this->fetchItemsFromRecentChanges();
break;
default:
$items = $this->fetchItemsFromPlugin();
}
foreach ($items as $item) {
$this->createAndAddItem($item);
}
return $this->feed->createFeed($this->options->get('type'));
}
/**
* Process the raw data, create feed item and add it to the feed
*
* @param array|string $data raw item data
* @return \FeedItem
* @triggers FEED_ITEM_ADD
*/
protected function createAndAddItem($data)
{
if (is_string($data)) {
$data = ['id' => $data];
}
if (($data['mode'] ?? '') == 'media' || isset($data['media'])) {
$data['id'] = $data['media'] ?? $data['id'];
$proc = new FeedMediaProcessor($data);
} else {
$proc = new FeedPageProcessor($data);
}
$item = new \FeedItem();
$item->title = $proc->getTitle();
if ($this->options->get('show_summary') && $proc->getSummary()) {
$item->title .= ' - ' . $proc->getSummary();
}
$item->date = $proc->getRev();
[$item->authorEmail, $item->author] = $proc->getAuthor();
$item->link = $proc->getURL($this->options->get('link_to'));
$item->description = $proc->getBody($this->options->get('item_content'));
$evdata = [
'item' => $item,
'opt' => &$this->options->options,
'ditem' => &$data,
'rss' => $this->feed,
];
$evt = new Event('FEED_ITEM_ADD', $evdata);
if ($evt->advise_before()) {
$this->feed->addItem($item);
}
$evt->advise_after();
return $item;
}
/**
* Read all pages from a namespace
*
* @todo this currently does not honor the rss_media setting and only ever lists pages
* @return array
*/
protected function fetchItemsFromNamespace()
{
global $conf;
$ns = ':' . cleanID($this->options->get('namespace'));
$ns = utf8_encodeFN(str_replace(':', '/', $ns));
$data = [];
$search_opts = [
'depth' => 1,
'pagesonly' => true,
'listfiles' => true
];
search(
$data,
$conf['datadir'],
'search_universal',
$search_opts,
$ns,
$lvl = 1,
$this->options->get('sort')
);
return $data;
}
/**
* Add the result of a full text search to the feed object
*
* @return array
*/
protected function fetchItemsFromSearch()
{
if (!actionOK('search')) throw new \RuntimeException('search is disabled');
if (!$this->options->get('search_query')) return [];
$data = ft_pageSearch($this->options->get('search_query'), $poswords);
return array_keys($data);
}
/**
* Add recent changed pages to the feed object
*
* @return array
*/
protected function fetchItemsFromRecentChanges()
{
global $conf;
$flags = 0;
if (!$this->options->get('show_deleted')) $flags += RECENTS_SKIP_DELETED;
if (!$this->options->get('show_minor')) $flags += RECENTS_SKIP_MINORS;
if ($this->options->get('only_new')) $flags += RECENTS_ONLY_CREATION;
if ($this->options->get('content_type') == 'media' && $conf['mediarevisions']) {
$flags += RECENTS_MEDIA_CHANGES;
}
if ($this->options->get('content_type') == 'both' && $conf['mediarevisions']) {
$flags += RECENTS_MEDIA_PAGES_MIXED;
}
return getRecents(0, $this->options->get('items'), $this->options->get('namespace'), $flags);
}
/**
* Add items from a plugin to the feed object
*
* @triggers FEED_MODE_UNKNOWN
* @return array
*/
protected function fetchItemsFromPlugin()
{
$eventData = [
'opt' => $this->options->options,
'data' => [],
];
$event = new Event('FEED_MODE_UNKNOWN', $eventData);
if ($event->advise_before(true)) {
throw new \RuntimeException('unknown feed mode');
}
$event->advise_after();
return $eventData['data'];
}
/**
* Add a logo to the feed
*
* Looks at different possible candidates for a logo and adds the first one
*
* @return void
*/
protected function initLogo()
{
global $conf;
$this->feed->image = new \FeedImage();
$this->feed->image->title = $conf['title'];
$this->feed->image->link = DOKU_URL;
$this->feed->image->url = tpl_getMediaFile([
':wiki:logo.svg',
':logo.svg',
':wiki:logo.png',
':logo.png',
':wiki:logo.jpg',
':logo.jpg',
':wiki:favicon.ico',
':favicon.ico',
':wiki:dokuwiki.svg',
':wiki:dokuwiki-128.png',
'images/favicon.ico'
], true);
}
}

View File

@ -0,0 +1,160 @@
<?php
namespace dokuwiki\Feed;
use dokuwiki\Extension\Event;
/**
* Hold the options for feed generation
*/
class FeedCreatorOptions
{
/** @var array[] supported feed types */
protected $types = [
'rss' => [
'name' => 'RSS0.91',
'mime' => 'text/xml; charset=utf-8',
],
'rss2' => [
'name' => 'RSS2.0',
'mime' => 'text/xml; charset=utf-8',
],
'atom' => [
'name' => 'ATOM0.3',
'mime' => 'application/xml; charset=utf-8',
],
'atom1' => [
'name' => 'ATOM1.0',
'mime' => 'application/atom+xml; charset=utf-8',
],
];
/** @var array[] the set options */
public $options = [
'type' => 'rss',
'feed_mode' => 'recent',
'link_to' => 'page',
'item_content' => 'diff',
'namespace' => '',
'items' => 15,
'show_minor' => false,
'show_deleted' => false,
'show_summary' => false,
'only_new' => false,
'sort' => 'natural',
'search_query' => '',
'content_type' => 'pages',
'guardmail' => 'none',
'title' => '',
];
/**
* Initialize the options from the request, falling back to config defaults
*
* @triggers FEED_OPTS_POSTPROCESS
* @param array $options additional options to set (for testing)
*/
public function __construct($options = [])
{
global $conf;
global $INPUT;
$this->options['type'] = $INPUT->valid(
'type',
array_keys($this->types),
$conf['rss_type']
);
// we only support 'list', 'search', 'recent' but accept anything so plugins can take over
$this->options['feed_mode'] = $INPUT->str('mode', 'recent');
$this->options['link_to'] = $INPUT->valid(
'linkto',
['diff', 'page', 'rev', 'current'],
$conf['rss_linkto']
);
$this->options['item_content'] = $INPUT->valid(
'content',
['abstract', 'diff', 'htmldiff', 'html'],
$conf['rss_content']
);
$this->options['namespace'] = $INPUT->filter('cleanID')->str('ns');
$this->options['items'] = max(0, $INPUT->int('num', $conf['recent']));
$this->options['show_minor'] = $INPUT->bool('minor');
$this->options['show_deleted'] = $conf['rss_show_deleted'];
$this->options['show_summary'] = $conf['rss_show_summary'];
$this->options['only_new'] = $INPUT->bool('onlynewpages');
$this->options['sort'] = $INPUT->valid(
'sort',
['natural', 'date'],
'natural'
);
$this->options['search_query'] = $INPUT->str('q');
$this->options['content_type'] = $INPUT->valid(
'view',
['pages', 'media', 'both'],
$conf['rss_media']
);
$this->options['guardmail'] = $conf['mailguard'];
$this->options['title'] = $conf['title'];
if ($this->options['namespace']) {
$this->options['title'] .= ' - ' . $this->options['namespace'];
}
$this->options['subtitle'] = $conf['tagline'];
$this->options = array_merge($this->options, $options);
// initialization finished, let plugins know
$eventData = [
'opt' => &$this->options,
];
Event::createAndTrigger('FEED_OPTS_POSTPROCESS', $eventData);
}
/**
* The cache key to use for a feed with these options
*
* Does not contain user or host specific information yet
*
* @return string
*/
public function getCacheKey()
{
return implode('', array_values($this->options));
}
/**
* Return a feed option by name
*
* @param string $option The name of the option
* @param mixed $default default value if option is not set (should usually not happen)
* @return mixed
*/
public function get($option, $default = null)
{
if (isset($this->options[$option])) {
return $this->options[$option];
}
return $default;
}
/**
* Return the feed type for UniversalFeedCreator
*
* This returns the apropriate type for UniversalFeedCreator
*
* @return string
*/
public function getType()
{
return $this->types[$this->options['type']]['name'];
}
/**
* Return the feed mime type
*
* @return string
*/
public function getMimeType()
{
return $this->types[$this->options['type']]['mime'];
}
}

View File

@ -0,0 +1,172 @@
<?php
namespace dokuwiki\Feed;
use dokuwiki\Extension\AuthPlugin;
use RuntimeException;
/**
* Accept more or less arbitrary data to represent data to later construct a feed item from.
* Provide lazy loading accessors to all the data we need for feed generation.
*/
abstract class FeedItemProcessor
{
/** @var string This page's ID */
protected $id;
/** @var array bag of holding */
protected $data;
/**
* Constructor
*
* @param array $data Needs to have at least an 'id' key
*/
public function __construct($data)
{
if (!isset($data['id'])) throw new RuntimeException('Missing ID');
$this->id = cleanID($data['id']);
$this->data = $data;
}
/**
* Get the page ID
*
* @return string
*/
public function getId()
{
return $this->id;
}
/**
* Get the revision timestamp of this page
*
* If the input gave us a revision, date or lastmodified already, we trust that it is correct.
*
* Note: we only handle most current revisions in feeds, so the revision is usually just the
* lastmodifed timestamp of the page file. However, if the item does not exist, we need to
* determine the revision from the changelog.
*
* @return int
*/
public function getRev()
{
if ($this->data['rev'] ?? 0) return $this->data['rev'];
if (isset($this->data['date'])) {
$this->data['rev'] = (int)$this->data['date'];
}
if (isset($this->data['lastmodified'])) {
$this->data['rev'] = (int)$this->data['lastmodified'];
}
return $this->data['rev'] ?? 0;
}
/**
* Construct the URL for the feed item based on the link_to option
*
* @param string $linkto The link_to option
* @return string URL
*/
abstract public function getURL($linkto);
/**
* @return string
*/
public function getTitle()
{
return $this->data['title'] ?? noNS($this->getId());
}
/**
* Construct the body of the feed item based on the item_content option
*
* @param string $content The item_content option
* @return string
*/
abstract public function getBody($content);
/**
* Get the change summary for this item if any
*
* @return string
*/
public function getSummary()
{
return (string)($this->data['sum'] ?? '');
}
/**
* Get the author info for this item
*
* @return string[] [email, author]
*/
public function getAuthor()
{
global $conf;
global $auth;
$user = $this->data['user'] ?? '';
$author = 'Anonymous';
$email = 'anonymous@undisclosed.example.com';
if (!$user) return [$email, $author];
$author = $user;
$email = $user . '@undisclosed.example.com';
if ($conf['useacl'] && $auth instanceof AuthPlugin) {
$userInfo = $auth->getUserData($user);
if ($userInfo) {
switch ($conf['showuseras']) {
case 'username':
case 'username_link':
$author = $userInfo['name'];
break;
}
}
}
return [$email, $author];
}
/**
* Get the categories for this item
*
* @return string[]
*/
abstract public function getCategory();
/**
* Clean HTML for the use in feeds
*
* @param string $html
* @return string
*/
protected function cleanHTML($html)
{
global $conf;
// no TOC in feeds
$html = preg_replace('/(<!-- TOC START -->).*(<!-- TOC END -->)/s', '', $html);
// add alignment for images
$html = preg_replace('/(<img .*?class="medialeft")/s', '\\1 align="left"', $html);
$html = preg_replace('/(<img .*?class="mediaright")/s', '\\1 align="right"', $html);
// make URLs work when canonical is not set, regexp instead of rerendering!
if (!$conf['canonical']) {
$base = preg_quote(DOKU_REL, '/');
$html = preg_replace(
'/(<a href|<img src)="(' . $base . ')/s',
'$1="' . DOKU_URL,
$html
);
}
return $html;
}
}

View File

@ -0,0 +1,189 @@
<?php
namespace dokuwiki\Feed;
use dokuwiki\ChangeLog\MediaChangeLog;
use dokuwiki\File\MediaFile;
use dokuwiki\Ui\Media\Display;
class FeedMediaProcessor extends FeedItemProcessor
{
/** @inheritdoc */
public function getURL($linkto)
{
switch ($linkto) {
case 'page':
$opt = [
'image' => $this->getId(),
'ns' => getNS($this->getId()),
'rev' => $this->getRev()
];
break;
case 'rev':
$opt = [
'image' => $this->getId(),
'ns' => getNS($this->getId()),
'rev' => $this->getRev(),
'tab_details' => 'history'
];
break;
case 'current':
$opt = [
'image' => $this->getId(),
'ns' => getNS($this->getId())
];
break;
case 'diff':
default:
$opt = [
'image' => $this->getId(),
'ns' => getNS($this->getId()),
'rev' => $this->getRev(),
'tab_details' => 'history',
'media_do' => 'diff'
];
}
return media_managerURL($opt, '&', true);
}
public function getBody($content)
{
switch ($content) {
case 'diff':
case 'htmldiff':
$prev = $this->getPrev();
if ($prev) {
if ($this->isExisting()) {
$src1 = new MediaFile($this->getId(), $prev);
$src2 = new MediaFile($this->getId());
} else {
$src1 = new MediaFile($this->getId(), $prev);
$src2 = null;
}
} else {
$src1 = null;
$src2 = new MediaFile($this->getId());
}
return $this->createDiffTable($src1, $src2);
case 'abstract':
case 'html':
default:
$src = new Display(new MediaFile($this->getId()));
return $this->cleanHTML($src->getPreviewHtml(500, 500));
}
}
/**
* @inheritdoc
* @todo read exif keywords
*/
public function getCategory()
{
return (array)getNS($this->getId());
}
/**
* Get the revision timestamp of this page
*
* Note: we only handle most current revisions in feeds, so the revision is usually just the
* lastmodifed timestamp of the page file. However, if the page does not exist, we need to
* determine the revision from the changelog.
* @return int
*/
public function getRev()
{
$rev = parent::getRev();
if ($rev) return $rev;
if (media_exists($this->id)) {
$this->data['rev'] = filemtime(mediaFN($this->id));
$this->data['exists'] = true;
} else {
$this->loadRevisions();
}
return $this->data['rev'];
}
/**
* Get the previous revision timestamp of this page
*
* @return int|null The previous revision or null if there is none
*/
public function getPrev()
{
if ($this->data['prev'] ?? 0) return $this->data['prev'];
$this->loadRevisions();
return $this->data['prev'];
}
/**
* Does this page exist?
*
* @return bool
*/
public function isExisting()
{
if (!isset($this->data['exists'])) {
$this->data['exists'] = media_exists($this->id);
}
return $this->data['exists'];
}
/**
* Load the current and previous revision from the changelog
* @return void
*/
protected function loadRevisions()
{
$changelog = new MediaChangeLog($this->id);
$revs = $changelog->getRevisions(0, 2); // FIXME check that this returns the current one correctly
if (!isset($this->data['rev'])) {
// prefer an already set date, only set if missing
// it should usally not happen that neither is available
$this->data['rev'] = $revs[0] ?? 0;
}
// a previous revision might not exist
$this->data['prev'] = $revs[1] ?? null;
}
/**
* Create a table showing the two media files
*
* @param MediaFile|null $src1
* @param MediaFile|null $src2
* @return string
*/
protected function createDiffTable($src1, $src2)
{
global $lang;
$content = '<table>';
$content .= '<tr>';
$content .= '<th width="50%">' . ($src1 ? $src1->getRev() : '') . '</th>';
$content .= '<th width="50%">' . $lang['current'] . '</th>';
$content .= '</tr>';
$content .= '<tr>';
$content .= '<td align="center">';
if ($src1) {
$display = new Display($src1);
$display->getPreviewHtml(300, 300);
}
$content .= '</td>';
$content .= '<td align="center">';
if ($src2) {
$display = new Display($src2);
$display->getPreviewHtml(300, 300);
}
$content .= '</td>';
$content .= '</tr>';
$content .= '</table>';
return $this->cleanHTML($content);
}
}

View File

@ -0,0 +1,215 @@
<?php
namespace dokuwiki\Feed;
use Diff;
use dokuwiki\ChangeLog\PageChangeLog;
use TableDiffFormatter;
use UnifiedDiffFormatter;
/**
* Accept more or less arbitrary data to represent a page and provide lazy loading accessors
* to all the data we need for feed generation.
*/
class FeedPageProcessor extends FeedItemProcessor
{
/** @var array[] metadata */
protected $meta;
// region data processors
/** @inheritdoc */
public function getURL($linkto)
{
switch ($linkto) {
case 'page':
$opt = ['rev' => $this->getRev()];
break;
case 'rev':
$opt = ['rev' => $this->getRev(), 'do' => 'revisions'];
break;
case 'current':
$opt = [];
break;
case 'diff':
default:
$opt = ['rev' => $this->getRev(), 'do' => 'diff'];
}
return wl($this->getId(), $opt, true, '&');
}
/** @inheritdoc */
public function getBody($content)
{
global $lang;
switch ($content) {
case 'diff':
$diff = $this->getDiff();
// note: diff output must be escaped, UnifiedDiffFormatter provides plain text
$udf = new UnifiedDiffFormatter();
return "<pre>\n" . hsc($udf->format($diff)) . "\n</pre>";
case 'htmldiff':
$diff = $this->getDiff();
// note: no need to escape diff output, TableDiffFormatter provides 'safe' html
$tdf = new TableDiffFormatter();
$content = '<table>';
$content .= '<tr><th colspan="2" width="50%">' . dformat($this->getPrev()) . '</th>';
$content .= '<th colspan="2" width="50%">' . $lang['current'] . '</th></tr>';
$content .= $tdf->format($diff);
$content .= '</table>';
return $content;
case 'html':
if ($this->isExisting()) {
$html = p_wiki_xhtml($this->getId(), '', false);
} else {
$html = p_wiki_xhtml($this->getId(), $this->getRev(), false);
}
return $this->cleanHTML($html);
case 'abstract':
default:
return $this->getAbstract();
}
}
/** @inheritdoc */
public function getCategory()
{
$meta = $this->getMetaData();
return (array)($meta['subject'] ?? (string)getNS($this->getId()));
}
// endregion
// region data accessors
/**
* Get the page abstract
*
* @return string
*/
public function getAbstract()
{
if (!isset($this->data['abstract'])) {
$meta = $this->getMetaData();
if (isset($meta['description']['abstract'])) {
$this->data['abstract'] = (string)$meta['description']['abstract'];
} else {
$this->data['abstract'] = '';
}
}
return $this->data['abstract'];
}
/** @inheritdoc */
public function getRev()
{
$rev = parent::getRev();
if ($rev) return $rev;
if (page_exists($this->id)) {
$this->data['rev'] = filemtime(wikiFN($this->id));
$this->data['exists'] = true;
} else {
$this->loadRevisions();
}
return $this->data['rev'];
}
/**
* Get the previous revision timestamp of this page
*
* @return int|null The previous revision or null if there is none
*/
public function getPrev()
{
if ($this->data['prev'] ?? 0) return $this->data['prev'];
$this->loadRevisions();
return $this->data['prev'];
}
/**
* Does this page exist?
*
* @return bool
*/
public function isExisting()
{
if (!isset($this->data['exists'])) {
$this->data['exists'] = page_exists($this->id);
}
return $this->data['exists'];
}
/**
* Get the title of this page
*
* @return string
*/
public function getTitle()
{
global $conf;
if (!isset($this->data['title'])) {
if ($conf['useheading']) {
$this->data['title'] = p_get_first_heading($this->id);
} else {
$this->data['title'] = noNS($this->id);
}
}
return $this->data['title'];
}
// endregion
/**
* Get the metadata of this page
*
* @return array[]
*/
protected function getMetaData()
{
if (!isset($this->meta)) {
$this->meta = (array)p_get_metadata($this->id);
}
return $this->meta;
}
/**
* Load the current and previous revision from the changelog
* @return void
*/
protected function loadRevisions()
{
$changelog = new PageChangeLog($this->id);
$revs = $changelog->getRevisions(0, 2); // FIXME check that this returns the current one correctly
if (!isset($this->data['rev'])) {
// prefer an already set date, only set if missing
// it should usally not happen that neither is available
$this->data['rev'] = $revs[0] ?? 0;
}
// a previous revision might not exist
$this->data['prev'] = $revs[1] ?? null;
}
/**
* Get a diff between this and the previous revision
*
* @return Diff
*/
protected function getDiff()
{
$prev = $this->getPrev();
if ($prev) {
return new Diff(
explode("\n", rawWiki($this->getId(), $prev)),
explode("\n", rawWiki($this->getId(), ''))
);
}
return new Diff([''], explode("\n", rawWiki($this->getId(), '')));
}
}

View File

@ -7,6 +7,7 @@ use JpegMeta;
class MediaFile
{
protected $id;
protected $rev;
protected $path;
protected $mime;
@ -26,6 +27,7 @@ class MediaFile
{
$this->id = $id; //FIXME should it be cleaned?
$this->path = mediaFN($id, $rev);
$this->rev = $rev;
[$this->ext, $this->mime, $this->downloadable] = mimetype($this->path, false);
}
@ -36,6 +38,12 @@ class MediaFile
return $this->id;
}
/** @return string|int Empty string for current version */
public function getRev()
{
return $this->rev;
}
/** @return string */
public function getPath()
{