Test Failed
Push — master ( 319840...0bb54c )
by Dispositif
06:21
created

WikiPageAction::isPageEditedAfterGetText()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 12
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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