<?php class MoinMoinBridge extends BridgeAbstract { const MAINTAINER = 'logmanoriginal'; const NAME = 'MoinMoin Bridge'; const URI = 'https://moinmo.in'; const DESCRIPTION = 'Generates feeds for pages of a MoinMoin (compatible) wiki'; const PARAMETERS = [ [ 'source' => [ 'name' => 'Source', 'type' => 'text', 'required' => true, 'title' => 'Insert wiki page URI (e.g.: https://moinmo.in/MoinMoin)', 'exampleValue' => 'https://moinmo.in/MoinMoin' ], 'separator' => [ 'name' => 'Separator', 'type' => 'list', 'requied' => true, 'title' => 'Defines the separtor for splitting content into feeds', 'defaultValue' => 'h2', 'values' => [ 'Header (h1)' => 'h1', 'Header (h2)' => 'h2', 'Header (h3)' => 'h3', 'List element (li)' => 'li', 'Anchor (a)' => 'a' ] ], 'limit' => [ 'name' => 'Limit', 'type' => 'number', 'required' => false, 'title' => 'Number of items to return (from top)', 'defaultValue' => -1 ], 'content' => [ 'name' => 'Content', 'type' => 'list', 'required' => false, 'title' => 'Defines how feed contents are build', 'defaultValue' => 'separator', 'values' => [ 'By separator' => 'separator', 'Follow link (only for anchor)' => 'follow', 'None' => 'none' ] ] ] ]; private $title = ''; public function collectData() { /* MoinMoin uses a rather unpleasent representation of HTML. Instead of * using tags like <article/>, <navigation/>, <header/>, etc... it uses * <div/>, <span/> and <p/>. Also each line is literaly identified via * IDs. The only way to distinguish content is via headers, though not * in all cases. * * Example (indented for the sake of readability): * ... * <span class="anchor" id="line-1"></span> * <span class="anchor" id="line-2"></span> * <span class="anchor" id="line-3"></span> * <span class="anchor" id="line-4"></span> * <span class="anchor" id="line-5"></span> * <span class="anchor" id="line-6"></span> * <span class="anchor" id="line-7"></span> * <span class="anchor" id="line-8"></span> * <span class="anchor" id="line-9"></span> * <p class="line867">MoinMoin is a Wiki software implemented in * <a class="interwiki" href="/Python" title="MoinMoin">Python</a> * and distributed as Free Software under * <a class="interwiki" href="/GPL" title="MoinMoin">GNU GPL license</a>. * ... */ $html = getSimpleHTMLDOM($this->getInput('source')); // Some anchors link to local sites or local IDs (both don't work well // in feeds) $html = $this->fixAnchors($html); $this->title = $html->find('title', 0)->innertext . ' | ' . self::NAME; // Here we focus on simple author and timestamp information from the given // page. Later we update this information in case the anchor is followed. $author = $this->findAuthor($html); $timestamp = $this->findTimestamp($html); $sections = $this->splitSections($html); foreach ($sections as $section) { $item = []; $item['uri'] = $this->findSectionAnchor($section[0]); switch ($this->getInput('content')) { case 'none': // Do not return any content break; case 'follow': // Follow the anchor // We can only follow anchors (use default otherwise) if ($this->getInput('separator') === 'a') { $content = $this->followAnchor($item['uri']); // Return only actual content $item['content'] = $content->find('div#page', 0)->innertext; // Each page could have its own author and timestamp $author = $this->findAuthor($content); $timestamp = $this->findTimestamp($content); break; } // fall-through case 'separator': default: // Use contents from the current page $item['content'] = $this->cleanArticle($section[2]); } if (!is_null($author)) { $item['author'] = $author; } if (!is_null($timestamp)) { $item['timestamp'] = $timestamp; } $item['title'] = strip_tags($section[1]); // Skip items with empty title if (empty(trim($item['title']))) { continue; } $this->items[] = $item; if ( $this->getInput('limit') > 0 && count($this->items) >= $this->getInput('limit') ) { break; } } } public function getName() { return $this->title ?: parent::getName(); } public function getURI() { return $this->getInput('source') ?: parent::getURI(); } /** * Splits the html into sections. * * Returns an array with one element per section. Each element consists of: * [0] The entire section * [1] The section title * [2] The section content */ private function splitSections($html) { $content = $html->find('div#page', 0)->innertext or returnServerError('Unable to find <div id="page"/>!'); $sections = []; $regex = implode( '', [ "\<{$this->getInput('separator')}.+?(?=\>)\>", "(.+?)(?=\<\/{$this->getInput('separator')}\>)", "\<\/{$this->getInput('separator')}\>", "(.+?)((?=\<{$this->getInput('separator')})|(?=\<div\sid=\"pagebottom\")){1}" ] ); preg_match_all( '/' . $regex . '/m', $content, $sections, PREG_SET_ORDER ); // Some pages don't use headers, return page as one feed if (count($sections) === 0) { return [ [ $content, $html->find('title', 0)->innertext, $content ] ]; } return $sections; } /** * Returns the anchor for a given section */ private function findSectionAnchor($section) { $html = str_get_html($section); // For IDs $anchor = $html->find($this->getInput('separator') . '[id=]', 0); if (!is_null($anchor)) { return $this->getInput('source') . '#' . $anchor->id; } // For actual anchors $anchor = $html->find($this->getInput('separator') . '[href=]', 0); if (!is_null($anchor)) { return $anchor->href; } // Nothing found return $this->getInput('source'); } /** * Returns the author * * Notice: Some pages don't provide author information */ private function findAuthor($html) { /* Example: * <p id="pageinfo" class="info" dir="ltr" lang="en">MoinMoin: LocalSpellingWords * (last edited 2017-02-16 15:36:31 by <span title="??? @ hosted-by.leaseweb.com * [178.162.199.143]">hosted-by</span>)</p> */ $pageinfo = $html->find('[id="pageinfo"]', 0); if (is_null($pageinfo)) { return null; } else { $author = $pageinfo->find('[title=]', 0); if (is_null($author)) { return null; } else { return trim(explode('@', $author->title)[0]); } } } /** * Returns the time of last edit * * Notice: Some pages don't provide this information */ private function findTimestamp($html) { // See example of findAuthor() $pageinfo = $html->find('[id="pageinfo"]', 0); if (is_null($pageinfo)) { return null; } else { $timestamp = $pageinfo->innertext; $matches = []; preg_match('/.+?(?=\().+?(?=\d)([0-9\-\s\:]+)/m', $pageinfo, $matches); return strtotime($matches[1]); } } /** * Returns the original HTML with all anchors fixed (makes relative anchors * absolute) */ private function fixAnchors($html, $source = null) { $source = $source ?: $this->getURI(); foreach ($html->find('a') as $anchor) { switch (substr($anchor->href, 0, 1)) { case 'h': // http or https, no actions required break; case '/': // some relative path $anchor->href = $this->findDomain($source) . $anchor->href; break; case '#': // it's an ID default: // probably something like ? or &, skip empty ones if (!isset($anchor->href)) { break; } $anchor->href = $source . $anchor->href; } } return $html; } /** * Loads the full article of a given anchor (if the anchor is from the same * wiki domain) */ private function followAnchor($anchor) { if (strrpos($anchor, $this->findDomain($this->getInput('source')) === false)) { return null; } $html = getSimpleHTMLDOMCached($anchor); if (!$html) { // Cannot load article return null; } return $this->fixAnchors($html, $anchor); } /** * Finds the domain for a given URI */ private function findDomain($uri) { $matches = []; preg_match('/(http[s]{0,1}:\/\/.+?(?=\/))/', $uri, $matches); return $matches[1]; } /* This function is a copy from CNETBridge */ private function stripWithDelimiters($string, $start, $end) { while (strpos($string, $start) !== false) { $section_to_remove = substr($string, strpos($string, $start)); $section_to_remove = substr($section_to_remove, 0, strpos($section_to_remove, $end) + strlen($end)); $string = str_replace($section_to_remove, '', $string); } return $string; } /* This function is based on CNETBridge */ private function cleanArticle($article_html) { $article_html = $this->stripWithDelimiters($article_html, '<script', '</script>'); return $article_html; } }