Passed
Push — master ( 0a41a5...99b995 )
by Dispositif
08:05
created

EditProcess::addErrorWarning()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 2
nc 2
nop 2
dl 0
loc 4
rs 10
c 0
b 0
f 0
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\Utils\WikiTextUtil;
13
use App\Infrastructure\DbAdapter;
14
use App\Infrastructure\ServiceFactory;
15
use Exception;
16
use LogicException;
17
use Mediawiki\DataModel\EditInfo;
18
use Normalizer;
19
use Throwable;
20
21
/**
22
 * Class EditProcess
23
 *
24
 * @package App\Application\Examples
25
 */
26
class EditProcess
27
{
28
    const TASK_NAME = 'Amélioration bibliographique';
29
    /**
30
     * poster ou pas le message en PD signalant les erreurs à résoudre
31
     */
32
    const EDIT_SIGNALEMENT = true;
33
34
    const CITATION_LIMIT         = 150;
35
    const DELAY_BOTFLAG_SECONDS  = 30;
36
    const DELAY_NOBOT_IN_SECONDS = 120;
37
    const ERROR_MSG_TEMPLATE     = __DIR__.'/../templates/message_errors.wiki';
38
39
    public $verbose = false;
40
    private $db;
41
    private $bot;
42
    private $wiki;
43
    private $wikiText;
44
45
    private $citationSummary;
46
    private $citationVersion = '';
47
    private $errorWarning = [];
48
    private $importantSummary = [];
49
50
    private $nbRows;
51
52
    // Minor flag on edit
53
    private $minorFlag = true;
54
    // Bot flag on edit
55
    private $botFlag = true;
56
57
    public function __construct()
58
    {
59
        $this->db = new DbAdapter();
60
        $this->bot = new Bot();
61
62
        $this->wikiLogin(true);
63
    }
64
65
    public function run(): void
66
    {
67
        $memory = new Memory();
68
        while (true) {
69
            echo "\n-------------------------------------\n\n";
70
            echo date("Y-m-d H:i")."\n";
71
            if ($this->verbose) {
72
                $memory->echoMemory(true);
73
            }
74
75
            $this->pageProcess();
76
        }
77
    }
78
79
    private function pageProcess()
80
    {
81
        $this->initialize();
82
83
        // get a random queue line
84
        $json = $this->db->getAllRowsToEdit(self::CITATION_LIMIT);
85
        $data = json_decode($json, true);
86
87
        if (empty($data)) {
88
            echo "SKIP : no row to process\n";
89
            throw new \Exception('no row to process');
90
        }
91
92
        try {
93
            $title = $data[0]['page'];
94
            echo "$title \n";
95
            $page = new WikiPageAction($this->wiki, $title);
96
        } catch (Exception $e) {
97
            echo "*** WikiPageAction error : $title \n";
98
            sleep(20);
99
100
            return false;
101
        }
102
103
        // TODO : HACK
104
        if (in_array($page->getLastEditor(), [getenv('BOT_NAME'), getenv('BOT_OWNER')])) {
105
            echo "SKIP : édité recemment par bot/dresseur.\n";
106
            $this->db->skipArticle($title);
107
108
            return false;
109
        }
110
        $this->wikiText = $page->getText();
111
112
        if (BOT::isEditionRestricted($this->wikiText)) {
0 ignored issues
show
Bug introduced by
It seems like $this->wikiText can also be of type null; however, parameter $text of App\Application\Bot::isEditionRestricted() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

112
        if (BOT::isEditionRestricted(/** @scrutinizer ignore-type */ $this->wikiText)) {
Loading history...
113
            echo "SKIP : protection/3R.\n";
114
            $this->db->skipArticle($title);
115
        }
116
117
        if ($this->bot->minutesSinceLastEdit($title) < 15) {
118
            echo "SKIP : édition humaine dans les dernières 15 minutes.\n";
119
120
            return false;
121
        }
122
123
        // Skip AdQ
124
        if (preg_match('#{{ ?En-tête label#i', $this->wikiText) > 0) {
125
            echo "SKIP : AdQ ou BA.\n";
126
            $this->db->skipArticle($title);
127
128
            return false;
129
        }
130
131
        // GET all article lines from db
132
        echo sprintf(">> %s rows to process\n", count($data));
133
134
        // foreach line
135
        $changed = false;
136
        foreach ($data as $dat) {
137
            // hack temporaire pour éviter articles dont CompleteProcess incomplet
138
            if (empty($dat['opti']) || empty($dat['optidate']) || $dat['optidate'] < DbAdapter::OPTI_VALID_DATE) {
139
                echo "SKIP : Complètement incomplet de l'article \n";
140
141
                return false;
142
            }
143
            $success = $this->dataProcess($dat);
144
            $changed = ($success) ? true : $changed;
145
        }
146
        if (!$changed) {
147
            echo "Rien à changer...\n\n";
148
            $this->db->skipArticle($title);
149
150
            return false;
151
        }
152
153
        // EDIT THE PAGE
154
        if (!$this->wikiText) {
155
            return false;
156
        }
157
158
        $miniSummary = $this->generateSummary();
159
        echo $miniSummary."\n\n";
160
        if ($this->verbose) {
161
            echo "sleep 20...\n";
162
        }
163
        sleep(30);
164
165
        pageEdit:
166
167
        try {
168
            $editInfo = new EditInfo($miniSummary, $this->minorFlag, $this->botFlag);
169
            $success = $page->editPage(Normalizer::normalize($this->wikiText), $editInfo);
170
        } catch (\Throwable $e) {
171
            // Invalid CSRF token.
172
            if (strpos($e->getMessage(), 'Invalid CSRF token') !== false) {
173
                echo "*** Invalid CSRF token \n";
174
                throw new \Exception('Invalid CSRF token');
175
            } else {
176
                dump($e); // todo log
177
                sleep(60);
178
179
                return false;
180
            }
181
        }
182
183
        if ($this->verbose) {
184
            echo ($success) ? "Ok\n" : "***** Erreur edit\n";
185
        }
186
187
        if ($success) {
188
            // updata DB
189
            foreach ($data as $dat) {
190
                $this->db->sendEditedData(['id' => $dat['id']]);
191
            }
192
193
            try {
194
                if (self::EDIT_SIGNALEMENT) {
195
                    $this->sendErrorMessage($data);
196
                }
197
            } catch (Throwable $e) {
198
                dump($e);
199
                unset($e);
200
            }
201
202
            if (!$this->botFlag) {
203
                if ($this->verbose) {
204
                    echo "sleep ".self::DELAY_NOBOT_IN_SECONDS."\n";
205
                }
206
                sleep(self::DELAY_NOBOT_IN_SECONDS);
207
            }
208
            if ($this->botFlag) {
209
                if ($this->verbose) {
210
                    echo "sleep ".self::DELAY_BOTFLAG_SECONDS."\n";
211
                }
212
                sleep(self::DELAY_BOTFLAG_SECONDS);
213
            }
214
        }
215
216
        return $success;
217
    }
218
219
    private function dataProcess(array $data): bool
220
    {
221
        $origin = $data['raw'];
222
        $completed = $data['opti'];
223
224
        dump($origin, $completed, $data['modifs'], $data['version']);
225
226
        if (WikiTextUtil::isCommented($origin)) {
227
            echo "SKIP: template avec commentaire HTML\n";
228
            $this->db->skipRow(intval($data['id']));
229
230
            return false;
231
        }
232
233
        $find = mb_strpos($this->wikiText, $origin);
234
        if ($find === false) {
235
            echo "String non trouvée. \n\n";
236
            $this->db->skipRow(intval($data['id']));
237
238
            return false;
239
        }
240
241
        $this->checkErrorWarning($data);
242
243
        // Replace text
244
        $newText = WikiPageAction::replaceTemplateInText($this->wikiText, $origin, $completed);
245
246
        if (!$newText || $newText === $this->wikiText) {
247
            echo "newText error\n";
248
249
            return false;
250
        }
251
        $this->wikiText = $newText;
252
        $this->minorFlag = ('1' === $data['major']) ? false : $this->minorFlag;
253
        $this->citationVersion = $data['version'];
254
        $this->citationSummary[] = $data['modifs'];
255
        $this->nbRows++;
256
257
        return true;
258
    }
259
260
    /**
261
     * Generate wiki edition summary.
262
     *
263
     * @return string
264
     */
265
    public function generateSummary(): string
266
    {
267
        // Start summary with "Bot" when using botflag, else "*"
268
        $prefix = ($this->botFlag) ? 'bot' : '☆';
269
        // add "/!\" when errorWarning
270
        $prefix .= (!empty($this->errorWarning)) ? ' ⚠' : '';
271
272
273
        // basic modifs
274
        $citeSummary = implode(' ', $this->citationSummary);
275
        // replace by list of modifs to verify by humans
276
        if (!empty($this->importantSummary)) {
277
            $citeSummary = implode(', ', $this->importantSummary);
278
        }
279
280
        $summary = sprintf(
281
            '%s [%s/%s] %s %s : %s',
282
            $prefix,
283
            str_replace('v', '', $this->bot::getGitVersion()),
284
            str_replace(['v0.', 'v1.'], '', $this->citationVersion),
285
            self::TASK_NAME,
286
            $this->nbRows,
287
            $citeSummary
288
        );
289
290
        if (!empty($this->importantSummary)) {
291
            $summary .= '...';
292
        }
293
294
        // shrink long summary if no important details to verify
295
        if (empty($this->importantSummary)) {
296
            $length = strlen($summary);
297
            $summary = mb_substr($summary, 0, 80);
298
            $summary .= ($length > strlen($summary)) ? '…' : '';
299
        }
300
301
        return $summary;
302
    }
303
304
    /**
305
     * Vérifie alerte d'erreurs humaines.
306
     *
307
     * @param array $data
308
     *
309
     * @throws Exception
310
     */
311
    private function checkErrorWarning(array $data): void
312
    {
313
        if (!isset($data['opti'])) {
314
            throw new LogicException('Opti NULL');
315
        }
316
317
        // paramètre inconnu
318
        if (preg_match_all(
319
                "#\|[^|]+<!-- ?(PARAMETRE [^>]+ N'EXISTE PAS|VALEUR SANS NOM DE PARAMETRE) ?-->#",
320
                $data['opti'],
321
                $matches
322
            ) > 0
323
        ) {
324
            foreach ($matches[0] as $line) {
325
                $this->addErrorWarning($data['page'], $line);
326
            }
327
            //  $this->botFlag = false;
328
            $this->addSummaryTag('paramètre non corrigé');
329
        }
330
331
        // ISBN invalide
332
        if (preg_match("#isbn invalide ?=[^|}]+#i", $data['opti'], $matches) > 0) {
333
            $this->addErrorWarning($data['page'], $matches[0]);
334
            $this->botFlag = false;
335
            $this->addSummaryTag('ISBN invalide');
336
        }
337
338
        // Edits avec ajout conséquent de donnée
339
        if (preg_match('#distinction des auteurs#', $data['modifs']) > 0) {
340
            $this->botFlag = false;
341
            $this->addSummaryTag('distinction des auteurs');
342
        }
343
        // prédiction paramètre correct
344
        if (preg_match('#[^,]+(=>|⇒)[^,]+#', $data['modifs'], $matches) > 0) {
345
            $this->botFlag = false;
346
            $this->addSummaryTag(sprintf('%s', $matches[0]));
347
        }
348
        if (preg_match('#\+\+sous-titre#', $data['modifs']) > 0) {
349
            $this->botFlag = false;
350
            $this->addSummaryTag('+sous-titre');
351
        }
352
        if (preg_match('#\+lieu#', $data['modifs']) > 0) {
353
            $this->addSummaryTag('+lieu');
354
        }
355
        if (preg_match('#présentation en ligne#', $data['modifs']) > 0) {
356
            $this->addSummaryTag('+présentation en ligne');
357
        }
358
        if (preg_match('#distinction auteurs#', $data['modifs']) > 0) {
359
            $this->addSummaryTag('distinction auteurs');
360
        }
361
        if (preg_match('#\+lire en ligne#', $data['modifs']) > 0) {
362
            $this->addSummaryTag('+lire en ligne');
363
        }
364
        if (preg_match('#\+lien #', $data['modifs']) > 0) {
365
            $this->addSummaryTag('wikif');
366
        }
367
368
        if (preg_match('#\+éditeur#', $data['modifs']) > 0) {
369
            $this->addSummaryTag('éditeur');
370
        }
371
        //        if (preg_match('#\+langue#', $data['modifs']) > 0) {
372
        //            $this->addSummaryTag('langue');
373
        //        }
374
375
        // mention BnF si ajout donnée + ajout identifiant bnf=
376
        if (!empty($this->importantSummary) && preg_match('#BnF#i', $data['modifs'], $matches) > 0) {
377
            $this->addSummaryTag('(BnF)');
378
        }
379
    }
380
381
    /**
382
     * Pour éviter les doublons dans signalements d'erreur.
383
     *
384
     * @param string $page
385
     * @param string $text
386
     */
387
    private function addErrorWarning(string $page, string $text): void
388
    {
389
        if (!isset($this->errorWarning[$page]) || !in_array($text, $this->errorWarning[$page])) {
390
            $this->errorWarning[$page][] = $text;
391
        }
392
    }
393
394
    /**
395
     * For substantive or ambiguous modifications done.
396
     *
397
     * @param string $tag
398
     */
399
    private function addSummaryTag(string $tag)
400
    {
401
        if (!in_array($tag, $this->importantSummary)) {
402
            $this->importantSummary[] = $tag;
403
        }
404
    }
405
406
    /**
407
     * @param array $rows Collection of citations
408
     *
409
     * @return bool
410
     */
411
    private function sendErrorMessage(array $rows): bool
412
    {
413
        if (!isset($rows[0]) || empty($this->errorWarning[$rows[0]['page']])) {
414
            return false;
415
        }
416
        $mainTitle = $rows[0]['page'];
417
        if (!$this->botFlag) {
418
            echo "** Send Error Message on talk page. Wait 3... \n";
419
        }
420
        sleep(3);
421
422
        // format wiki message
423
        $errorList = '';
424
        foreach ($this->errorWarning[$mainTitle] as $error) {
425
            $errorList .= sprintf("* <span style=\"background:#FCDFE8\"><nowiki>%s</nowiki></span> \n", $error);
426
        }
427
428
        $diffStr = '';
429
        try {
430
            // get last bot revision ID
431
            $main = new WikiPageAction($this->wiki, $mainTitle);
432
            if (getenv('BOT_NAME') === $main->getLastRevision()->getUser()) {
433
                $id = $main->getLastRevision()->getId();
434
                $diffStr = sprintf(
435
                    ' ([https://fr.wikipedia.org/w/index.php?title=%s&diff=%s diff])',
436
                    str_replace(' ', '_', $mainTitle),
437
                    $id
438
                );
439
            }
440
        } catch (\Throwable $e) {
441
            unset($e);
442
        }
443
444
        $errorCategoryName = sprintf('Signalement %s', getenv('BOT_NAME'));
445
446
        $errorMessage = file_get_contents(self::ERROR_MSG_TEMPLATE);
447
        $errorMessage = str_replace('##CATEGORY##', $errorCategoryName, $errorMessage);
448
        $errorMessage = str_replace('##ERROR LIST##', trim($errorList), $errorMessage);
449
        $errorMessage = str_replace('##ARTICLE##', $mainTitle, $errorMessage);
450
        $errorMessage = str_replace('##DIFF##', $diffStr, $errorMessage);
451
452
        // Edit wiki talk page
453
        try {
454
            $talkPage = new WikiPageAction($this->wiki, 'Discussion:'.$mainTitle);
455
            $editInfo = new EditInfo('Signalement erreur {ouvrage}', false, false);
456
457
            return $talkPage->addToBottomOrCreatePage($errorMessage, $editInfo);
458
        } catch (Throwable $e) {
459
            dump($e);
460
461
            return false;
462
        }
463
    }
464
465
    /**
466
     * @throws \Mediawiki\Api\UsageException
467
     */
468
    private function initialize(): void
469
    {
470
        // initialisation vars
471
        $this->botFlag = true;
472
        $this->errorWarning = [];
473
        $this->wikiText = null;
474
        $this->citationSummary = [];
475
        $this->importantSummary = [];
476
        $this->minorFlag = true;
477
        $this->nbRows = 0;
478
479
        $this->bot->checkStopOnTalkpage(true);
480
    }
481
482
    /**
483
     * @param bool $forceLogin
484
     *
485
     * @throws \Mediawiki\Api\UsageException
486
     */
487
    private function wikiLogin($forceLogin = false): void
488
    {
489
        $this->wiki = ServiceFactory::wikiApi($forceLogin);
490
    }
491
492
}
493