Passed
Push — master ( 5eccc7...1faa52 )
by Dispositif
02:45
created

WikiPageAction::addToBottomOrCreatePage()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 2
Bugs 0 Features 1
Metric Value
cc 2
eloc 3
nc 2
nop 2
dl 0
loc 7
ccs 0
cts 4
cp 0
crap 6
rs 10
c 2
b 0
f 1
1
<?php
2
/**
3
 * This file is part of dispositif/wikibot application
4
 * 2019 : Philippe M. <[email protected]>
5
 * For the full copyright and MIT license information, please view the LICENSE file.
6
 */
7
8
declare(strict_types=1);
9
10
namespace App\Application;
11
12
use App\Domain\Enums\Language;
13
use App\Infrastructure\TagParser;
14
use Exception;
15
use Mediawiki\Api\MediawikiFactory;
16
use Mediawiki\DataModel\Content;
17
use Mediawiki\DataModel\EditInfo;
18
use Mediawiki\DataModel\Page;
19
use Mediawiki\DataModel\PageIdentifier;
20
use Mediawiki\DataModel\Revision;
21
use Mediawiki\DataModel\Title;
22
use Throwable;
23
24
class WikiPageAction
25
{
26
    const SKIP_LANG_INDICATOR = 'fr'; // skip {{fr}} before template
27
28
    /**
29
     * @var Page
30
     */
31
    public $page; // public for debug
32
33
    public $wiki; // api ?
34
35
    /**
36
     * @var string
37
     */
38
    private $title;
39
    /**
40
     * Wiki namespace
41
     *
42
     * @var int
43
     */
44
    private $ns;
45
46
    /**
47
     * WikiPageAction constructor.
48
     *
49
     * @param MediawikiFactory $wiki
50
     * @param string           $title
51
     *
52
     * @throws Exception
53
     */
54
    public function __construct(MediawikiFactory $wiki, string $title)
55
    {
56
        $this->wiki = $wiki;
57
        $this->title = $title;
58
59
        try {
60
            $this->page = $wiki->newPageGetter()->getFromTitle($title);
61
            $this->ns = $this->page->getPageIdentifier()->getTitle()->getNs();
62
        } catch (Throwable $e) {
63
            throw new Exception('Erreur construct WikiPageAction '.$e->getMessage().$e->getFile().$e->getLine());
64
        }
65
    }
66
67
    /**
68
     * Get wiki text from the page.
69
     *
70
     * @return string|null
71
     */
72
    public function getText(): ?string
73
    {
74
        // page doesn't exist
75
        if (empty($this->page->getRevisions()->getLatest())) {
76
            return null;
77
        }
78
79
        $latest = $this->page->getRevisions()->getLatest();
80
81
        return ($latest) ? $latest->getContent()->getData() : null;
82
    }
83
84
    public function getNs(): ?int
85
    {
86
        return $this->ns;
87
    }
88
89
    public function getLastRevision(): ?Revision
90
    {
91
        // page doesn't exist
92
        if (empty($this->page->getRevisions()->getLatest())) {
93
            return null;
94
        }
95
96
        return $this->page->getRevisions()->getLatest();
97
    }
98
99
    public function getLastEditor(): ?string
100
    {
101
        // page doesn't exist
102
        if (empty($this->page->getRevisions()->getLatest())) {
103
            return null;
104
        }
105
106
        $latest = $this->page->getRevisions()->getLatest();
107
108
        return ($latest) ? $latest->getUser() : null;
109
    }
110
111
    /**
112
     * Check if a frwiki disambiguation page.
113
     *
114
     * @return bool
115
     */
116
    public function isPageHomonymie(): bool
117
    {
118
        return false !== stristr($this->getText(), '{{homonymie');
119
    }
120
121
    /**
122
     * Is it page with a redirection link ?
123
     *
124
     * @return bool
125
     */
126
    public function isRedirect(): bool
127
    {
128
        return !empty($this->getRedirect());
129
    }
130
131
    /**
132
     * Get redirection page title or null.
133
     *
134
     * @return string|null
135
     */
136
    public function getRedirect(): ?string
137
    {
138
        if ($this->getText() && preg_match('/^#REDIRECT(?:ION)? ?\[\[([^]]+)]]/i', $this->getText(), $matches)) {
139
            return (string)trim($matches[1]);
140
        }
141
142
        return null;
143
    }
144
145
    /**
146
     * Edit the page with new text.
147
     * Opti : EditInfo optional param ?
148
     *
149
     * @param string   $newText
150
     * @param EditInfo $editInfo
151
     *
152
     * @return bool
153
     */
154
    public function editPage(string $newText, EditInfo $editInfo): bool
155
    {
156
        $revision = $this->page->getPageIdentifier();
157
158
        $content = new Content($newText);
159
        $revision = new Revision($content, $revision);
160
161
        // TODO try/catch UsageExceptions badtoken
162
        return $this->wiki->newRevisionSaver()->save($revision, $editInfo);
163
    }
164
165
    /**
166
     * Create a new page.
167
     *
168
     * @param string $text
169
     *
170
     * @return bool
171
     * @throws Exception
172
     */
173
    public function createPage(string $text, ?EditInfo $editInfo = null): bool
174
    {
175
        if (!empty($this->page->getRevisions()->getLatest())) {
176
            throw new Exception('That page already exists');
177
        }
178
179
        $newContent = new Content($text);
180
        // $identifier = $this->page->getPageIdentifier()
181
        $title = new Title($this->title);
182
        $identifier = new PageIdentifier($title);
183
        $revision = new Revision($newContent, $identifier);
184
185
        return $this->wiki->newRevisionSaver()->save($revision, $editInfo);
186
    }
187
188
    /**
189
     * @param string   $addText
190
     * @param EditInfo $editInfo
191
     *
192
     * @return bool success
193
     * @throws Exception
194
     */
195
    public function addToBottomOrCreatePage(string $addText, EditInfo $editInfo): bool
196
    {
197
        if (empty($this->page->getRevisions()->getLatest())) {
198
            return $this->createPage($addText, $editInfo);
199
        }
200
201
        return $this->addToBottomOfThePage($addText, $editInfo);
202
    }
203
204
    /**
205
     * Add text to the bottom of the article.
206
     *
207
     * @param string   $addText
208
     * @param EditInfo $editInfo
209
     *
210
     * @return bool success
211
     * @throws Exception
212
     */
213
    public function addToBottomOfThePage(string $addText, EditInfo $editInfo): bool
214
    {
215
        if (empty($this->page->getRevisions()->getLatest())) {
216
            throw new Exception('That page does not exist');
217
        }
218
        $oldText = $this->getText();
219
        $newText = $oldText."\n".$addText;
220
221
        return $this->editPage($newText, $editInfo);
222
    }
223
224
    /**
225
     * todo Move to WikiTextUtil ?
226
     * Replace serialized template and manage {{en}} prefix.
227
     * Don't delete {{fr}} on frwiki.
228
     *
229
     * @param string $text       wikitext of the page
230
     * @param string $tplOrigin  template text to replace
231
     * @param string $tplReplace new template text
232
     *
233
     * @return string|null
234
     */
235 6
    public static function replaceTemplateInText(string $text, string $tplOrigin, string $tplReplace): string
236
    {
237
        // "{{en}} {{zh}} {{ouvrage...}}"
238 6
        if (preg_match_all(
239 6
            '#(?<langTemp>{{[a-z][a-z]}} ?{{[a-z][a-z]}}) ?'.preg_quote($tplOrigin, '#').'#i',
240 6
            $text,
241 6
            $matches
242
        )
243
        ) {
244
            // Skip double lang prefix (like in "{{fr}} {{en}} {template}")
245
            echo 'SKIP ! double lang prefix !';
246
247
            return $text;
248
        }
249
250
        // hack // todo: autres patterns {{en}} ?
251
        // OK : {{en}} \n {{ouvrage}}
252 6
        if (preg_match_all(
253 6
                "#(?<langTemp>{{(?<lang>[a-z][a-z])}} *\n?)?".preg_quote($tplOrigin, '#').'#i',
254 6
                $text,
255 6
                $matches
256 6
            ) > 0
257
        ) {
258 6
            foreach ($matches[0] as $num => $mention) {
259 6
                $lang = $matches['lang'][$num] ?? '';
260 6
                if (!empty($lang)) {
261 6
                    $lang = Language::all2wiki($lang);
262
                }
263
264
                // detect inconsistency between lang indicator and lang param
265
                // example : {{en}} {{template|lang=ru}}
266
                // BUG: prefix {{de}}  incompatible avec langue de {{Ouvrage |langue= |prénom1=Hartmut |nom1=Atsma
267 6
                if (!empty($lang) && self::SKIP_LANG_INDICATOR !== $lang
268 6
                    && !preg_match('#lang(ue)?='.$lang.'#i', $tplReplace)
269 6
                    && !preg_match('#\| ?langue= ?\|#', $tplReplace)
270
                ) {
271
                    echo sprintf(
272
                        'prefix %s incompatible avec langue de %s',
273
                        $matches['langTemp'][$num],
274
                        $tplReplace
275
                    );
276
277
                    // skip all the replacements of that template
278
                    return $text; // return null ?
279
                }
280
281
                // FIX dirty : {{en}} mais pas de langue sur template...
282 6
                if ($lang && preg_match('#\| ?langue= ?\|#', $tplReplace) > 0) {
283 2
                    $previousTpl = $tplReplace;
284 2
                    $tplReplace = str_replace('langue=', 'langue='.$lang, $tplReplace);
285 2
                    $text = str_replace($previousTpl, $tplReplace, $text);
286
                }
287
288
                // don't delete {{fr}} before {template} on frwiki
289 6
                if (self::SKIP_LANG_INDICATOR === $lang) {
290 3
                    $text = str_replace($tplOrigin, $tplReplace, $text);
291
292 3
                    continue;
293
                }
294
295
                // replace {template} and {{lang}} {template}
296 6
                $text = str_replace($mention, $tplReplace, $text);
297 6
                $text = str_replace(
298 6
                    $matches['langTemp'][$num].$tplReplace,
299 6
                    $tplReplace,
300 6
                    $text
301
                ); // si 1er replace global sans
302
                // {{en}}
303
            }
304
        }
305
306 6
        return $text;
307
    }
308
309
    /**
310
     * Extract <ref> data from text.
311
     *
312
     * @param $text string
313
     *
314
     * @return array
315
     * @throws Exception
316
     */
317
    public function extractRefFromText(string $text): ?array
318
    {
319
        $parser = new TagParser(); // todo ParserFactory
320
        $refs = $parser->importHtml($text)->getRefValues(); // []
321
322
        return (array)$refs;
323
    }
324
325
    /**
326
     * TODO $url parameter
327
     * TODO? refactor with : parse_str() + parse_url($url, PHP_URL_QUERY)
328
     * check if any ref contains a targeted website/URL.
329
     *
330
     * @param array $refs
331
     *
332
     * @return array
333
     */
334
    public function filterRefByURL(array $refs): array
335
    {
336
        $validRef = [];
337
        foreach ($refs as $ref) {
338
            if (preg_match(
339
                    '#(?<url>https?://(?:www\.)?lemonde\.fr/[^ \]]+)#i',
340
                    $ref,
341
                    $matches
342
                ) > 0
343
            ) {
344
                $validRef[] = ['url' => $matches['url'], 'raw' => $ref];
345
            }
346
        }
347
348
        return $validRef;
349
    }
350
}
351