Passed
Push — master ( d0964c...b8fbf6 )
by Dispositif
06:57
created

WikiPageAction   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 363
Duplicated Lines 0 %

Test Coverage

Coverage 28.29%

Importance

Changes 16
Bugs 0 Features 3
Metric Value
eloc 105
c 16
b 0
f 3
dl 0
loc 363
ccs 30
cts 106
cp 0.2829
rs 8.8
wmc 45

16 Methods

Rating   Name   Duplication   Size   Complexity  
A extractRefFromText() 0 6 1
A isPageHomonymie() 0 3 1
A isPageEditedAfterGetText() 0 12 3
A getNs() 0 3 1
A getText() 0 10 3
A getRedirect() 0 7 3
A editPage() 0 14 3
A filterRefByURL() 0 15 3
A getLastRevision() 0 8 2
A addToBottomOrCreatePage() 0 7 2
A __construct() 0 10 2
A addToBottomOfThePage() 0 9 2
A getLastEditor() 0 10 3
A createPage() 0 13 2
C replaceTemplateInText() 0 80 13
A isRedirect() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like WikiPageAction often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use WikiPageAction, and based on these observations, apply Extract Interface, too.

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
     * todo Move to WikiTextUtil ?
257
     * Replace serialized template and manage {{en}} prefix.
258 6
     * Don't delete {{fr}} on frwiki.
259 6
     *
260 6
     * @param string $text       wikitext of the page
261 6
     * @param string $tplOrigin  template text to replace
262
     * @param string $tplReplace new template text
263
     *
264
     * @return string|null
265
     */
266
    public static function replaceTemplateInText(string $text, string $tplOrigin, string $tplReplace): string
267 6
    {
268 6
        // "{{en}} {{zh}} {{ouvrage...}}"
269 6
        if (preg_match_all(
270
            '#(?<langTemp>{{[a-z][a-z]}} ?{{[a-z][a-z]}}) ?'.preg_quote($tplOrigin, '#').'#i',
271
            $text,
272
            $matches
273
        )
274
        ) {
275
            // Skip double lang prefix (like in "{{fr}} {{en}} {template}")
276
            echo 'SKIP ! double lang prefix !';
277
278
            return $text;
279
        }
280
281
        // hack // todo: autres patterns {{en}} ?
282 6
        // OK : {{en}} \n {{ouvrage}}
283 2
        if (preg_match_all(
284 2
                "#(?<langTemp>{{(?<lang>[a-z][a-z])}} *\n?)?".preg_quote($tplOrigin, '#').'#i',
285 2
                $text,
286
                $matches
287
            ) > 0
288
        ) {
289 6
            foreach ($matches[0] as $num => $mention) {
290 3
                $lang = $matches['lang'][$num] ?? '';
291
                if (!empty($lang)) {
292 3
                    $lang = Language::all2wiki($lang);
293
                }
294
295
                // detect inconsistency between lang indicator and lang param
296 6
                // example : {{en}} {{template|lang=ru}}
297 6
                // BUG: prefix {{de}}  incompatible avec langue de {{Ouvrage |langue= |prénom1=Hartmut |nom1=Atsma
298 6
                if (!empty($lang) && self::SKIP_LANG_INDICATOR !== $lang
299 6
                    && preg_match('#langue *=#', $tplReplace)
300 6
                    && !preg_match('#langue *= ?'.$lang.'#i', $tplReplace)
301
                    && !preg_match('#\| ?langue *= ?\n?\|#', $tplReplace)
302
                ) {
303
                    echo sprintf(
304
                        'prefix %s incompatible avec langue de %s',
305
                        $matches['langTemp'][$num],
306 6
                        $tplReplace
307
                    );
308
309
                    // skip all the replacements of that template
310
                    return $text; // return null ?
311
                }
312
313
                //                // FIX dirty mai 2020 : {{en}} mais pas de paramètre sur template...
314
                //                if ($lang && !preg_match('#\| ?langue *= ?\n?\|#', $tplReplace) > 0) {
315
                //                    $previousTpl = $tplReplace;
316
                //                    $tplReplace = str_replace('langue=', 'langue='.$lang, $tplReplace);
317
                //                    $text = str_replace($previousTpl, $tplReplace, $text);
318
                //                }
319
320
                // FIX dirty : {{en}} mais pas langue= non définie sur new template...
321
                if ($lang && preg_match('#\| ?langue *= ?\n?\|#', $tplReplace) > 0) {
322
                    $previousTpl = $tplReplace;
323
                    $tplReplace = str_replace('langue=', 'langue='.$lang, $tplReplace);
324
                    $text = str_replace($previousTpl, $tplReplace, $text);
325
                }
326
327
                // don't delete {{fr}} before {template} on frwiki
328
                if (self::SKIP_LANG_INDICATOR === $lang) {
329
                    $text = str_replace($tplOrigin, $tplReplace, $text);
330
331
                    continue;
332
                }
333
334
                // replace {template} and {{lang}} {template}
335
                $text = str_replace($mention, $tplReplace, $text);
336
                $text = str_replace(
337
                    $matches['langTemp'][$num].$tplReplace,
338
                    $tplReplace,
339
                    $text
340
                ); // si 1er replace global sans
341
                // {{en}}
342
            }
343
        }
344
345
        return $text;
346
    }
347
348
    /**
349
     * Extract <ref> data from text.
350
     *
351
     * @param $text string
352
     *
353
     * @return array
354
     * @throws Exception
355
     */
356
    public function extractRefFromText(string $text): ?array
357
    {
358
        $parser = new TagParser(); // todo ParserFactory
359
        $refs = $parser->importHtml($text)->getRefValues(); // []
360
361
        return (array)$refs;
362
    }
363
364
    /**
365
     * TODO $url parameter
366
     * TODO? refactor with : parse_str() + parse_url($url, PHP_URL_QUERY)
367
     * check if any ref contains a targeted website/URL.
368
     *
369
     * @param array $refs
370
     *
371
     * @return array
372
     */
373
    public function filterRefByURL(array $refs): array
374
    {
375
        $validRef = [];
376
        foreach ($refs as $ref) {
377
            if (preg_match(
378
                    '#(?<url>https?://(?:www\.)?lemonde\.fr/[^ \]]+)#i',
379
                    $ref,
380
                    $matches
381
                ) > 0
382
            ) {
383
                $validRef[] = ['url' => $matches['url'], 'raw' => $ref];
384
            }
385
        }
386
387
        return $validRef;
388
    }
389
}
390