Merge pull request #4156 from dokuwiki/feedrefactor
Feed creation refactoring
This commit is contained in:
commit
754eefa383
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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'));
|
||||
}
|
||||
|
||||
}
|
61
feed.php
61
feed.php
|
@ -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;
|
||||
|
||||
// ---------------------------------------------------------------- //
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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'];
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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(), '')));
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue