xref: /plugin/elasticsearch/helper/docparser.php (revision a6bdb2dfdaca0d0dddbe75d010df26405faa1324) !
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