1fae5cfd5SAndreas Gohr<?php 2*a6bdb2dfSAndreas Gohr 3fae5cfd5SAndreas Gohr/** 4fae5cfd5SAndreas Gohr * DokuWiki Plugin elasticsearch (DocParser Helper Component) 5fae5cfd5SAndreas Gohr * 6fae5cfd5SAndreas Gohr * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 7fae5cfd5SAndreas Gohr * @author Andreas Gohr <gohr@cosmocode.de> 8fae5cfd5SAndreas Gohr */ 9fae5cfd5SAndreas Gohr 10*a6bdb2dfSAndreas Gohruse dokuwiki\Extension\Plugin; 11*a6bdb2dfSAndreas Gohruse LanguageDetection\Language; 12c2676747SAndreas Gohruse dokuwiki\HTTP\DokuHTTPClient; 13c2676747SAndreas Gohr 14*a6bdb2dfSAndreas Gohrrequire_once __DIR__ . '/../vendor/autoload.php'; 15fae5cfd5SAndreas Gohr 16fae5cfd5SAndreas Gohr/** 17fae5cfd5SAndreas Gohr * Convert a file to text and metainfos 18fae5cfd5SAndreas Gohr */ 19*a6bdb2dfSAndreas Gohrclass helper_plugin_elasticsearch_docparser extends Plugin 20fae5cfd5SAndreas Gohr{ 21*a6bdb2dfSAndreas Gohr public const CONFFILE = DOKU_CONF . 'elasticsearch.conf'; 22d4874922SAndreas Gohr 23fae5cfd5SAndreas Gohr /** 24fae5cfd5SAndreas Gohr * @var array maps extensions to parsers. A parser may be a local cli tool (file is passed as argument) 25fae5cfd5SAndreas Gohr * or an URL accepting input by PUT (like Apache Tika). They need to return plain text or a JSON response. 26fae5cfd5SAndreas Gohr */ 27fae5cfd5SAndreas Gohr protected $parsers; 28fae5cfd5SAndreas Gohr 29fae5cfd5SAndreas Gohr /** 300125d09fSAnna Dabrowska * Maps fields returned by Tika or other JSON returning parsers to our own field names. 31fae5cfd5SAndreas Gohr * Order does matter. Last non-empty field wins. 32fae5cfd5SAndreas Gohr */ 33*a6bdb2dfSAndreas Gohr protected const FIELDMAP = [ 34fae5cfd5SAndreas Gohr 'title' => 'title', 35fae5cfd5SAndreas Gohr 'dc:title' => 'title', 360125d09fSAnna Dabrowska 'content' => 'content', 370125d09fSAnna Dabrowska 'body' => 'content', 380125d09fSAnna Dabrowska 'dc:description' => 'content', 390125d09fSAnna Dabrowska 'X-TIKA:content' => 'content', 40c30eb178SAnna Dabrowska 'Creation-Date' => 'created', 41c30eb178SAnna Dabrowska 'dcterms:created' => 'created', 42c30eb178SAnna Dabrowska 'meta:creation-date' => 'created', 43fae5cfd5SAndreas Gohr ]; 44fae5cfd5SAndreas Gohr 45fae5cfd5SAndreas Gohr /** 46fae5cfd5SAndreas Gohr * Load the parser setup 47fae5cfd5SAndreas Gohr */ 48fae5cfd5SAndreas Gohr public function __construct() 49fae5cfd5SAndreas Gohr { 50d4874922SAndreas Gohr $parsers = confToHash(self::CONFFILE); 51b30042c1SAnna Dabrowska 52beb3e085SAnna Dabrowska if (empty($parsers)) { 53d4874922SAndreas Gohr throw new RuntimeException( 54d4874922SAndreas Gohr 'Cannot process media, the parser configuration in ' . self::CONFFILE . ' is missing.' 55d4874922SAndreas Gohr ); 56b30042c1SAnna Dabrowska } 57b30042c1SAnna Dabrowska 58b30042c1SAnna Dabrowska $this->parsers = $parsers; 59fae5cfd5SAndreas Gohr } 60fae5cfd5SAndreas Gohr 61fae5cfd5SAndreas Gohr /** 62fae5cfd5SAndreas Gohr * Parse the given file 63fae5cfd5SAndreas Gohr * 64fae5cfd5SAndreas Gohr * Returns an array with the following keys 65fae5cfd5SAndreas Gohr * 66fae5cfd5SAndreas Gohr * title - will be filled with the basename if no title could be extracted 67fae5cfd5SAndreas Gohr * content - the content to index 68fae5cfd5SAndreas Gohr * mime - the mime type as determined by us 69fae5cfd5SAndreas Gohr * ext - the extension of the file 70c30eb178SAnna Dabrowska * language - the language code the file is written in 71c30eb178SAnna Dabrowska * created - creation time 72fae5cfd5SAndreas Gohr * 73fae5cfd5SAndreas Gohr * Returns false if the file can not be parsed and thus should not be indexed 74fae5cfd5SAndreas Gohr * 75fae5cfd5SAndreas Gohr * @param string $file 76d463b075SAnna Dabrowska * @return array 77d463b075SAnna Dabrowska * @fixme throw smarter exceptions 78fae5cfd5SAndreas Gohr */ 79fae5cfd5SAndreas Gohr public function parse($file) 80fae5cfd5SAndreas Gohr { 81d463b075SAnna Dabrowska if (!file_exists($file)) { 82d463b075SAnna Dabrowska throw new RuntimeException('File ' . $file . 'does not exist'); 83d463b075SAnna Dabrowska } 84*a6bdb2dfSAndreas Gohr [$ext, $mime] = mimetype($file); 85d463b075SAnna Dabrowska if (!$ext) { 86d463b075SAnna Dabrowska throw new RuntimeException('Cannot parse file with unidentified extension'); 87d463b075SAnna Dabrowska } 88d463b075SAnna Dabrowska if (!isset($this->parsers[$ext])) { 89d463b075SAnna Dabrowska throw new RuntimeException('No parser configured for files of type ' . $ext); 90d463b075SAnna Dabrowska }; 91fae5cfd5SAndreas Gohr 92fae5cfd5SAndreas Gohr $result = $this->runParser($file, $this->parsers[$ext]); 93d463b075SAnna Dabrowska if ($result === false) { 94d463b075SAnna Dabrowska throw new RuntimeException('No response from parser'); 95d463b075SAnna Dabrowska } 96fae5cfd5SAndreas Gohr 97fae5cfd5SAndreas Gohr // defaults 98fae5cfd5SAndreas Gohr $data = [ 99fae5cfd5SAndreas Gohr 'title' => basename($file), 100fae5cfd5SAndreas Gohr 'content' => '', 101fae5cfd5SAndreas Gohr 'mime' => $mime, 102fae5cfd5SAndreas Gohr 'ext' => $ext, 1030125d09fSAnna Dabrowska 'language' => '', 104c30eb178SAnna Dabrowska 'created' => date('Y-m-d\TH:i:s\Z', filemtime($file)), 105fae5cfd5SAndreas Gohr ]; 106fae5cfd5SAndreas Gohr 107fae5cfd5SAndreas Gohr // add what we got from the parser 108fae5cfd5SAndreas Gohr $data = array_merge($data, $this->processParserResult($result)); 109fae5cfd5SAndreas Gohr 110fae5cfd5SAndreas Gohr // add language info 1110125d09fSAnna Dabrowska $data['language'] = $this->detectLanguage($data['content']); 112fae5cfd5SAndreas Gohr 113fae5cfd5SAndreas Gohr return $data; 114fae5cfd5SAndreas Gohr } 115fae5cfd5SAndreas Gohr 116fae5cfd5SAndreas Gohr /** 117fae5cfd5SAndreas Gohr * Execute the parser on the given file 118fae5cfd5SAndreas Gohr * 119fae5cfd5SAndreas Gohr * The parser can be an URL accepting a PUT request or a local command 120fae5cfd5SAndreas Gohr * 121fae5cfd5SAndreas Gohr * @param string $file 122fae5cfd5SAndreas Gohr * @param string $parser 123fae5cfd5SAndreas Gohr * @return bool|string 124fae5cfd5SAndreas Gohr */ 125fae5cfd5SAndreas Gohr protected function runParser($file, $parser) 126fae5cfd5SAndreas Gohr { 127539441a7SAnna Dabrowska if (preg_match('/^https?:\/\//', $parser)) { 128539441a7SAnna Dabrowska $http = new DokuHTTPClient(); 129fae5cfd5SAndreas Gohr $http->timeout = 90; 130fae5cfd5SAndreas Gohr $ok = $http->sendRequest($parser, io_readFile($file, false), 'PUT'); 131fae5cfd5SAndreas Gohr if ($ok) { 132fae5cfd5SAndreas Gohr return $http->resp_body; 133fae5cfd5SAndreas Gohr } 134fae5cfd5SAndreas Gohr return false; 135beb3e085SAnna Dabrowska } elseif (is_executable(strtok($parser, ' '))) { 136fae5cfd5SAndreas Gohr $output = []; 137fae5cfd5SAndreas Gohr $ok = 0; 138beb3e085SAnna Dabrowska exec(str_replace('%in%', escapeshellarg($file), $parser), $output, $ok); 139fae5cfd5SAndreas Gohr if ($ok === 0) { 140*a6bdb2dfSAndreas Gohr return implode(' ', $output); 141fae5cfd5SAndreas Gohr } 142fae5cfd5SAndreas Gohr return false; 143fae5cfd5SAndreas Gohr } 144fae5cfd5SAndreas Gohr 145fae5cfd5SAndreas Gohr return false; 146fae5cfd5SAndreas Gohr } 147fae5cfd5SAndreas Gohr 148fae5cfd5SAndreas Gohr /** 149fae5cfd5SAndreas Gohr * @param string $result The string returned by the parser, might be json 150fae5cfd5SAndreas Gohr * @return array 151fae5cfd5SAndreas Gohr */ 152fae5cfd5SAndreas Gohr protected function processParserResult($result) 153fae5cfd5SAndreas Gohr { 154fae5cfd5SAndreas Gohr // decode json responses 155fae5cfd5SAndreas Gohr if ( 156fae5cfd5SAndreas Gohr ( 157c5cad0a6SAnna Dabrowska $result[0] !== '[' && $result[0] !== '{' 158fae5cfd5SAndreas Gohr ) 159fae5cfd5SAndreas Gohr || 160fae5cfd5SAndreas Gohr ( 161c5cad0a6SAnna Dabrowska ($decoded = json_decode($result, true)) === null 162fae5cfd5SAndreas Gohr ) 163fae5cfd5SAndreas Gohr ) { 164fae5cfd5SAndreas Gohr return [ 165c5cad0a6SAnna Dabrowska 'content' => $result, 166fae5cfd5SAndreas Gohr ]; 167fae5cfd5SAndreas Gohr }; 168fae5cfd5SAndreas Gohr // we only want the first result from an Apache Tika response 169fae5cfd5SAndreas Gohr if (isset($decoded[0]) && is_array($decoded[0])) { 170fae5cfd5SAndreas Gohr $decoded = $decoded[0]; 171fae5cfd5SAndreas Gohr } 172fae5cfd5SAndreas Gohr 173fae5cfd5SAndreas Gohr $data = []; 174*a6bdb2dfSAndreas Gohr foreach (self::FIELDMAP as $from => $to) { 175fae5cfd5SAndreas Gohr if (!blank($decoded[$from])) $data[$to] = trim($decoded[$from]); 176fae5cfd5SAndreas Gohr } 177fae5cfd5SAndreas Gohr return $data; 178fae5cfd5SAndreas Gohr } 179fae5cfd5SAndreas Gohr 180fae5cfd5SAndreas Gohr /** 181fae5cfd5SAndreas Gohr * Return the language the given body was written in 182fae5cfd5SAndreas Gohr * 183fae5cfd5SAndreas Gohr * Will always return the wiki default language unless the translation plugin is installed. 184fae5cfd5SAndreas Gohr * 185fae5cfd5SAndreas Gohr * @param string $body 186fae5cfd5SAndreas Gohr * @return string The detected language 187fae5cfd5SAndreas Gohr * @fixme handle languages like 'pt-br' correctly 188fae5cfd5SAndreas Gohr * @fixme maybe make this optional in favor of the namespace method 189fae5cfd5SAndreas Gohr */ 190fae5cfd5SAndreas Gohr protected function detectLanguage($body) 191fae5cfd5SAndreas Gohr { 192fae5cfd5SAndreas Gohr global $conf; 193fae5cfd5SAndreas Gohr 194fae5cfd5SAndreas Gohr /** @var helper_plugin_translation $trans */ 195fae5cfd5SAndreas Gohr $trans = plugin_load('helper', 'translation'); 196fae5cfd5SAndreas Gohr if ($trans === null) return $conf['lang']; 197fae5cfd5SAndreas Gohr 198*a6bdb2dfSAndreas Gohr $ld = new Language(); 199fae5cfd5SAndreas Gohr 200dccf1ecdSAnna Dabrowska $langs = array_keys($ld->detect($body)->whitelist(...$trans->translations)->close()); 201fae5cfd5SAndreas Gohr return array_shift($langs); 202fae5cfd5SAndreas Gohr } 203fae5cfd5SAndreas Gohr} 204