Passed
Branch dev3 (493baa)
by Dispositif
02:30
created

OuvrageEditWorker::pageProcess()   D

Complexity

Conditions 18
Paths 14

Size

Total Lines 110
Code Lines 64

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 64
c 0
b 0
f 0
dl 0
loc 110
rs 4.8666
cc 18
nc 14
nop 0

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
/*
4
 * This file is part of dispositif/wikibot application (@github)
5
 * 2019-2023 © Philippe M./Irønie  <[email protected]>
6
 * For the full copyright and MIT license information, view the license file.
7
 */
8
9
declare(strict_types=1);
10
11
namespace App\Application\OuvrageEdit;
12
13
use App\Application\InfrastructurePorts\DbAdapterInterface;
14
use App\Application\InfrastructurePorts\MemoryInterface;
15
use App\Application\OuvrageEdit\Validators\CitationsNotEmptyValidator;
16
use App\Application\OuvrageEdit\Validators\CitationValidator;
17
use App\Application\OuvrageEdit\Validators\PageValidatorComposite;
18
use App\Application\OuvrageEdit\Validators\TalkStopValidator;
19
use App\Application\OuvrageEdit\Validators\WikiTextValidator;
20
use App\Application\WikiBotConfig;
21
use App\Application\WikiPageAction;
22
use App\Infrastructure\ServiceFactory;
23
use Codedungeon\PHPCliColors\Color;
24
use Exception;
25
use LogicException;
26
use Mediawiki\Api\UsageException;
27
use Normalizer;
28
use Psr\Log\LoggerInterface;
29
use Psr\Log\NullLogger;
30
use Throwable;
31
32
/**
33
 * Legacy class, to be refactored. Too big, too many responsibilities.
34
 * todo use PageOuvrageCollectionDTO.
35
 * todo chain of responsibility pattern + log decorator (+ events ?)
36
 */
37
class OuvrageEditWorker
38
{
39
    use OuvrageEditSummaryTrait, TalkPageEditTrait;
40
41
    public const TASK_NAME = '📗 Amélioration bibliographique'; // 📖📔📘📗
42
    public const LUCKY_MESSAGE = ' 🇺🇦'; // ☘️
43
    /**
44
     * poster ou pas le message en PD signalant les erreurs à résoudre
45
     */
46
    public const EDIT_SIGNALEMENT = true;
47
    public const CITATION_LIMIT = 150;
48
    public const DELAY_BOTFLAG_SECONDS = 60;
49
    public const DELAY_NO_BOTFLAG_SECONDS = 60;
50
    public const DELAY_MINUTES_AFTER_HUMAN_EDIT = 10;
51
    public const ERROR_MSG_TEMPLATE = __DIR__ . '/templates/message_errors.wiki';
52
53
    /**
54
     * @var PageWorkStatus
55
     */
56
    protected $pageWorkStatus;
57
58
    /**
59
     * @var WikiPageAction
60
     */
61
    protected $wikiPageAction = null;
62
63
    protected $db;
64
    /**
65
     * @var WikiBotConfig
66
     */
67
    protected $bot;
68
    /**
69
     * @var MemoryInterface
70
     */
71
    protected $memory;
72
73
    /**
74
     * @var LoggerInterface
75
     */
76
    protected $log;
77
78
    public function __construct(
79
        DbAdapterInterface $dbAdapter,
80
        WikiBotConfig      $bot,
81
        MemoryInterface    $memory,
82
        ?LoggerInterface   $log = null
83
    )
84
    {
85
        $this->db = $dbAdapter;
86
        $this->bot = $bot;
87
        $this->memory = $memory;
88
        $this->log = $log ?? new NullLogger();
89
    }
90
91
    /**
92
     * @throws Exception
93
     */
94
    public function run(): void
95
    {
96
        while (true) {
97
            echo "\n-------------------------------------\n\n";
98
            echo date("Y-m-d H:i:s") . " ";
99
            $this->log->info($this->memory->getMemory(true));
100
            $this->pageProcess();
101
            sleep(2); // précaution boucle infinie
102
        }
103
    }
104
105
    /**
106
     * @throws UsageException
107
     * @throws Exception
108
     */
109
    protected function pageProcess(): bool
110
    {
111
        $e = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $e is dead and can be removed.
Loading history...
112
        $this->pageWorkStatus = new PageWorkStatus();
113
114
        if ((new TalkStopValidator($this->bot))->validate() === false) {
1 ignored issue
show
introduced by
The condition new App\Application\Ouvr...)->validate() === false is always false.
Loading history...
115
            return false;
116
        }
117
118
        // get a random queue line
119
        $json = $this->db->getAllRowsOfOneTitleToEdit(self::CITATION_LIMIT);
120
        $pageCitationCollection = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
0 ignored issues
show
Bug introduced by
It seems like $json can also be of type null; however, parameter $json of json_decode() 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

120
        $pageCitationCollection = json_decode(/** @scrutinizer ignore-type */ $json, true, 512, JSON_THROW_ON_ERROR);
Loading history...
121
122
        if ((new CitationsNotEmptyValidator($pageCitationCollection, $this->log))->validate() === false) {
1 ignored issue
show
introduced by
The condition new App\Application\Ouvr...)->validate() === false is always false.
Loading history...
123
            return false;
124
        }
125
126
        $title = $pageCitationCollection[0]['page']; // not used by validators
127
        $this->pageWorkStatus->title = $title;
128
        $this->printTitle($title);
129
130
        // Find on wikipedia the page to edit
131
        try {
132
            $this->wikiPageAction = ServiceFactory::wikiPageAction($title); // , true ?
133
        } catch (Exception $e) {
134
            $this->log->warning("*** WikiPageAction error : " . $title . " \n");
135
            sleep(20);
136
137
            return false;
138
        }
139
        $pageValidator = new PageValidatorComposite(
140
            $this->bot,
141
            $pageCitationCollection,
142
            $this->db,
143
            $this->wikiPageAction
144
        );
145
        if ($pageValidator->validate() === false) {
146
            return false;
147
        }
148
149
        $this->pageWorkStatus->wikiText = $this->wikiPageAction->getText();
150
        // or do that at the end of ArticleValidForEditValidator if PageWorkStatus injected ?
151
152
        $this->checkArticleLabels($title);
153
        // or do that at the end of ArticleValidForEditValidator if PageWorkStatus injected ?
154
155
        // print total number of rows completed in db
156
        $this->log->info(sprintf("%s rows to process\n", is_countable($pageCitationCollection) ? count($pageCitationCollection) : 0));
157
158
159
        // foreach line
160
        $changed = false;
161
        foreach ($pageCitationCollection as $dat) {
162
            if (!$this->isAllCitationCompleted($dat)) {
163
                $this->log->notice("SKIP : Amélioration incomplet de l'article. sleep 10min");
164
                sleep(600);
165
166
                return false;
167
            }
168
            $success = $this->processOneCitation($dat);
169
            $changed = ($success) ? true : $changed;
170
        }
171
        if (!$changed) {
172
            $this->log->debug("Rien à changer...");
173
            $this->db->skipArticle($title);
0 ignored issues
show
Bug introduced by
The method skipArticle() does not exist on App\Application\Infrastr...orts\DbAdapterInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to App\Application\Infrastr...orts\DbAdapterInterface. ( Ignorable by Annotation )

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

173
            $this->db->/** @scrutinizer ignore-call */ 
174
                       skipArticle($title);
Loading history...
174
175
            return false;
176
        }
177
178
179
        // EDIT THE PAGE
180
        if (!(new WikiTextValidator($this->pageWorkStatus->wikiText))->validate()) {
181
            return false;
182
        }
183
184
        $miniSummary = $this->generateFinalSummary();
185
        $this->log->notice($miniSummary);
186
        $this->log->debug("sleep 2...");
187
        sleep(2); // todo ???
188
189
        dump($miniSummary);
190
        die("\n*** STOP ON OuvrageEditWorker:191 FOR REFACTORING ***\n");
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return boolean. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
191
192
        $success = $this->editPage($miniSummary);
0 ignored issues
show
Unused Code introduced by
$success = $this->editPage($miniSummary) is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
193
        if ($success) {
194
            // updata DB
195
            foreach ($pageCitationCollection as $dat) {
196
                $this->db->sendEditedData(['id' => $dat['id']]);
197
            }
198
199
            try {
200
                if (self::EDIT_SIGNALEMENT && !empty($this->pageWorkStatus->errorWarning[$title])) {
201
                    $this->sendOuvrageErrorsOnTalkPage($pageCitationCollection, $this->log);
202
                }
203
            } catch (Throwable $e) {
204
                $this->log->warning('Exception in editPage() ' . $e->getMessage());
205
                unset($e);
206
            }
207
208
            if (!$this->pageWorkStatus->botFlag) {
209
                $this->log->debug("sleep " . self::DELAY_NO_BOTFLAG_SECONDS);
210
                sleep(self::DELAY_NO_BOTFLAG_SECONDS);
211
            }
212
            if ($this->pageWorkStatus->botFlag) {
213
                $this->log->debug("sleep " . self::DELAY_BOTFLAG_SECONDS);
214
                sleep(self::DELAY_BOTFLAG_SECONDS);
215
            }
216
        }
217
218
        return $success;
219
    }
220
221
    protected function printTitle(string $title): void
222
    {
223
        echo Color::BG_CYAN . $title . Color::NORMAL . " \n";
224
    }
225
226
    protected function checkArticleLabels($title): void
227
    {
228
        // Featured/Good article (AdQ/BA) todo event listener
229
        if (preg_match('#{{ ?En-tête label ?\| ?AdQ#i', $this->pageWorkStatus->wikiText)) {
230
            $this->db->setLabel($title, 2);
0 ignored issues
show
Bug introduced by
The method setLabel() does not exist on App\Application\Infrastr...orts\DbAdapterInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to App\Application\Infrastr...orts\DbAdapterInterface. ( Ignorable by Annotation )

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

230
            $this->db->/** @scrutinizer ignore-call */ 
231
                       setLabel($title, 2);
Loading history...
231
            $this->log->warning("Article de Qualité !\n");
232
            $this->pageWorkStatus->botFlag = false;
233
            $this->pageWorkStatus->featured_article = true; // to add star in edit summary
234
        }
235
        if (preg_match('#{{ ?En-tête label ?\| ?BA#i', $this->pageWorkStatus->wikiText)) {
236
            $this->db->setLabel($title, 1);
237
            $this->pageWorkStatus->botFlag = false;
238
            $this->pageWorkStatus->featured_article = true; // to add star in edit summary
239
            $this->log->warning("Bon article !!\n");
240
        }
241
    }
242
243
    protected function isAllCitationCompleted(array $dat): bool
244
    {
245
        // hack temporaire pour éviter articles dont CompleteProcess incomplet
246
        if (empty($dat['opti']) || empty($dat['optidate']) || $dat['optidate'] < $this->db->getOptiValidDate()) {
247
            return false;
248
        }
249
        return true;
250
    }
251
252
    /**
253
     * @throws Exception
254
     */
255
    protected function processOneCitation(array $ouvrageData): bool
256
    {
257
        $origin = $ouvrageData['raw'];
258
        $completed = $ouvrageData['opti'];
259
        $this->printDebug($ouvrageData);
260
261
        $citationValidator = new CitationValidator(
262
            $ouvrageData,
263
            $this->pageWorkStatus->wikiText,
264
            $this->log,
265
            $this->db
266
        );
267
        if ($citationValidator->validate() === false) {
268
            return false;
269
        }
270
271
        $this->generateSummaryOnPageWorkStatus($ouvrageData);
272
273
        // Replace text
274
        $newText = WikiPageAction::replaceTemplateInText($this->pageWorkStatus->wikiText, $origin, $completed);
275
276
        if (!$newText || $newText === $this->pageWorkStatus->wikiText) {
277
            $this->log->warning("newText error");
278
279
            return false;
280
        }
281
        $this->pageWorkStatus->wikiText = $newText;
282
        $this->pageWorkStatus->minorFlag = ('1' === $ouvrageData['major']) ? false : $this->pageWorkStatus->minorFlag;
283
        $this->pageWorkStatus->citationVersion = $ouvrageData['version']; // todo gérer versions différentes
284
        $this->pageWorkStatus->citationSummary[] = $ouvrageData['modifs'];
285
        $this->pageWorkStatus->nbRows++;
286
287
        return true;
288
    }
289
290
    protected function printDebug(array $data)
291
    {
292
        $this->log->debug('origin: ' . $data['raw']);
293
        $this->log->debug('completed: ' . $data['opti']);
294
        $this->log->debug('modifs: ' . $data['modifs']);
295
        $this->log->debug('version: ' . $data['version']);
296
    }
297
298
    /**
299
     * todo extract. => responsability : pageWorkStatus + summary
300
     * Vérifie alerte d'erreurs humaines.
301
     */
302
    protected function generateSummaryOnPageWorkStatus(array $ouvrageData): void
303
    {
304
        if (!isset($ouvrageData['opti'])) { // todo move to CitationValidator
305
            throw new LogicException('Opti NULL');
306
        }
307
308
        // paramètre inconnu
309
        if (preg_match_all(
310
                "#\|[^|]+<!-- ?(PARAMETRE [^>]+ N'EXISTE PAS|VALEUR SANS NOM DE PARAMETRE|ERREUR [^>]+) ?-->#",
311
                $ouvrageData['opti'],
312
                $matches
313
            ) > 0
314
        ) {
315
            foreach ($matches[0] as $line) {
316
                $this->addErrorWarning($ouvrageData['page'], $line);
317
            }
318
            //  $this->pageWorkStatus->botFlag = false;
319
            $this->addSummaryTag('paramètre non corrigé');
320
        }
321
322
        // ISBN invalide
323
        if (preg_match("#isbn invalide ?=[^|}]+#i", $ouvrageData['opti'], $matches) > 0) {
324
            $this->addErrorWarning($ouvrageData['page'], $matches[0]);
325
            $this->pageWorkStatus->botFlag = false;
326
            $this->addSummaryTag('ISBN invalide 💩');
327
        }
328
329
        // Edits avec ajout conséquent de donnée
330
        if (preg_match('#distinction des auteurs#', $ouvrageData['modifs']) > 0) {
331
            $this->pageWorkStatus->botFlag = false;
332
            $this->addSummaryTag('distinction auteurs 🧠');
333
        }
334
        // prédiction paramètre correct
335
        if (preg_match('#[^,]+(=>|⇒)[^,]+#', $ouvrageData['modifs'], $matches) > 0) {
336
            $this->pageWorkStatus->botFlag = false;
337
            $this->addSummaryTag($matches[0]);
338
        }
339
        if (preg_match('#\+\+sous-titre#', $ouvrageData['modifs']) > 0) {
340
            $this->pageWorkStatus->botFlag = false;
341
            $this->addSummaryTag('+sous-titre');
342
        }
343
        if (preg_match('#\+lieu#', $ouvrageData['modifs']) > 0) {
344
            $this->addSummaryTag('+lieu');
345
        }
346
        if (preg_match('#tracking#', $ouvrageData['modifs']) > 0) {
347
            $this->addSummaryTag('tracking');
348
        }
349
        if (preg_match('#présentation en ligne#', $ouvrageData['modifs']) > 0) {
350
            $this->addSummaryTag('+présentation en ligne✨');
351
        }
352
        if (preg_match('#distinction auteurs#', $ouvrageData['modifs']) > 0) {
353
            $this->addSummaryTag('distinction auteurs 🧠');
354
        }
355
        if (preg_match('#\+lire en ligne#', $ouvrageData['modifs']) > 0) {
356
            $this->addSummaryTag('+lire en ligne✨');
357
        }
358
        if (preg_match('#\+lien #', $ouvrageData['modifs']) > 0) {
359
            $this->addSummaryTag('wikif');
360
        }
361
362
        if (preg_match('#\+éditeur#', $ouvrageData['modifs']) > 0) {
363
            $this->addSummaryTag('éditeur');
364
        }
365
        //        if (preg_match('#\+langue#', $data['modifs']) > 0) {
366
        //            $this->addSummaryTag('langue');
367
        //        }
368
369
        // mention BnF si ajout donnée + ajout identifiant bnf=
370
        if (!empty($this->pageWorkStatus->importantSummary) && preg_match('#BnF#i', $ouvrageData['modifs'], $matches) > 0) {
371
            $this->addSummaryTag('©[[BnF]]');
372
        }
373
    }
374
375
    /**
376
     * todo extract
377
     * Pour éviter les doublons dans signalements d'erreur.
378
     */
379
    protected function addErrorWarning(string $title, string $text): void
380
    {
381
        if (!isset($this->pageWorkStatus->errorWarning[$title]) || !in_array($text, $this->pageWorkStatus->errorWarning[$title])) {
382
            $this->pageWorkStatus->errorWarning[$title][] = $text;
383
        }
384
    }
385
386
    private function editPage(string $miniSummary): bool
387
    {
388
        try {
389
            $editInfo = ServiceFactory::editInfo($miniSummary, $this->pageWorkStatus->minorFlag, $this->pageWorkStatus->botFlag);
390
            $success = $this->wikiPageAction->editPage(Normalizer::normalize($this->pageWorkStatus->wikiText), $editInfo);
391
        } catch (Throwable $e) {
392
            // Invalid CSRF token.
393
            if (strpos($e->getMessage(), 'Invalid CSRF token') !== false) {
394
                $this->log->alert("*** Invalid CSRF token \n");
395
                throw new Exception('Invalid CSRF token', $e->getCode(), $e);
396
            } else {
397
                $this->log->warning('Exception in editPage() ' . $e->getMessage());
398
                sleep(10);
399
400
                return false;
401
            }
402
        }
403
        $this->log->info($success ? "Edition Ok\n" : "***** Edition KO !\n");
404
405
        return $success;
406
    }
407
}
408