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

WikiPageAction::replaceTemplateInText()   C

Complexity

Conditions 13
Paths 5

Size

Total Lines 80
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 38.0748

Importance

Changes 8
Bugs 0 Features 2
Metric Value
cc 13
eloc 36
c 8
b 0
f 2
nc 5
nop 3
dl 0
loc 80
ccs 16
cts 34
cp 0.4706
crap 38.0748
rs 6.6166

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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