Passed
Branch dev3 (ae391d)
by Dispositif
02:29
created

OuvrageEditWorker::processOneCitation()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 33
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 21
c 0
b 0
f 0
dl 0
loc 33
rs 9.2728
cc 5
nc 4
nop 1
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 Mediawiki\Api\UsageException;
26
use Normalizer;
27
use Psr\Log\LoggerInterface;
28
use Psr\Log\NullLogger;
29
use Throwable;
30
31
/**
32
 * Legacy class, to be refactored. Too big, too many responsibilities.
33
 * todo use PageOuvrageCollectionDTO.
34
 * todo chain of responsibility pattern + log decorator (+ events ?)
35
 */
36
class OuvrageEditWorker
37
{
38
    use OuvrageEditSummaryTrait, TalkPageEditTrait;
39
40
    public const TASK_NAME = '📗 Amélioration bibliographique'; // 📖📔📘📗
41
    public const LUCKY_MESSAGE = ' 🇺🇦'; // ☘️
42
    /**
43
     * poster ou pas le message en PD signalant les erreurs à résoudre
44
     */
45
    public const EDIT_SIGNALEMENT = true;
46
    public const CITATION_LIMIT = 150;
47
    public const DELAY_BOTFLAG_SECONDS = 60;
48
    public const DELAY_NO_BOTFLAG_SECONDS = 60;
49
    public const DELAY_MINUTES_AFTER_HUMAN_EDIT = 10;
50
    public const ERROR_MSG_TEMPLATE = __DIR__ . '/templates/message_errors.wiki';
51
52
    /**
53
     * @var PageWorkStatus
54
     */
55
    protected $pageWorkStatus;
56
57
    /**
58
     * @var WikiPageAction
59
     */
60
    protected $wikiPageAction = null;
61
62
    protected $db;
63
    /**
64
     * @var WikiBotConfig
65
     */
66
    protected $bot;
67
    /**
68
     * @var MemoryInterface
69
     */
70
    protected $memory;
71
72
    /**
73
     * @var LoggerInterface
74
     */
75
    protected $log;
76
77
    public function __construct(
78
        DbAdapterInterface $dbAdapter,
79
        WikiBotConfig      $bot,
80
        MemoryInterface    $memory,
81
        ?LoggerInterface   $log = null
82
    )
83
    {
84
        $this->db = $dbAdapter;
85
        $this->bot = $bot;
86
        $this->memory = $memory;
87
        $this->log = $log ?? new NullLogger();
88
    }
89
90
    /**
91
     * @throws Exception
92
     */
93
    public function run(): void
94
    {
95
        while (true) {
96
            echo "\n-------------------------------------\n\n";
97
            echo date("Y-m-d H:i:s") . " ";
98
            $this->log->info($this->memory->getMemory(true));
99
            $this->pageProcess();
100
            sleep(2); // précaution boucle infinie
101
        }
102
    }
103
104
    /**
105
     * @throws UsageException
106
     * @throws Exception
107
     */
108
    protected function pageProcess(): bool
109
    {
110
        $e = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $e is dead and can be removed.
Loading history...
111
112
        if ((new TalkStopValidator($this->bot))->validate() === false) { // move up ?
113
            return false;
114
        }
115
116
        // get a random queue line
117
        $json = $this->db->getAllRowsOfOneTitleToEdit(self::CITATION_LIMIT);
118
        $pageCitationCollection = $json ? json_decode($json, true, 512, JSON_THROW_ON_ERROR) : [];
119
120
        if ((new CitationsNotEmptyValidator($pageCitationCollection, $this->log))->validate() === false) {
121
            return false;
122
        }
123
124
        $this->pageWorkStatus = new PageWorkStatus($pageCitationCollection[0]['page']);
125
        $this->printTitle($this->pageWorkStatus->getTitle());
126
127
        // Find on wikipedia the page to edit
128
        try {
129
            $this->wikiPageAction = ServiceFactory::wikiPageAction($this->pageWorkStatus->getTitle()); // , true ?
130
        } catch (Exception $e) {
131
            $this->log->warning("*** WikiPageAction error : " . $this->pageWorkStatus->getTitle() . " \n");
132
            sleep(20);
133
134
            return false;
135
        }
136
        $pageValidator = new PageValidatorComposite(
137
            $this->bot, $pageCitationCollection, $this->db, $this->wikiPageAction
138
        );
139
        if ($pageValidator->validate() === false) {
140
            return false;
141
        }
142
143
        $this->pageWorkStatus->wikiText = $this->wikiPageAction->getText();
144
        $this->checkArticleLabels($this->pageWorkStatus->getTitle());
145
        // or do that at the end of ArticleValidForEditValidator if PageWorkStatus injected ?
146
147
        // print total number of rows completed in db
148
        $rowNumber = is_countable($pageCitationCollection) ? count($pageCitationCollection) : 0;
149
        $this->log->info(sprintf("%s rows to process\n", $rowNumber));
150
151
        // Make citations replacements
152
        if ($this->makeCitationsReplacements($pageCitationCollection) === false) {
153
            return false;
154
        }
155
156
        if ($this->editPage()) {
157
            $this->updateDb($pageCitationCollection);
158
159
            return true;
160
        }
161
162
        return false;
163
    }
164
165
    protected function printTitle(string $title): void
166
    {
167
        echo Color::BG_CYAN . $title . Color::NORMAL . " \n";
168
    }
169
170
    protected function checkArticleLabels($title): void
171
    {
172
        // Featured/Good article (AdQ/BA) todo event listener
173
        if (preg_match('#{{ ?En-tête label ?\| ?AdQ#i', $this->pageWorkStatus->wikiText)) {
174
            $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

174
            $this->db->/** @scrutinizer ignore-call */ 
175
                       setLabel($title, 2);
Loading history...
175
            $this->log->warning("Article de Qualité !\n");
176
            $this->pageWorkStatus->botFlag = false;
177
            $this->pageWorkStatus->featured_article = true; // to add star in edit summary
178
        }
179
        if (preg_match('#{{ ?En-tête label ?\| ?BA#i', $this->pageWorkStatus->wikiText)) {
180
            $this->db->setLabel($title, 1);
181
            $this->pageWorkStatus->botFlag = false;
182
            $this->pageWorkStatus->featured_article = true; // to add star in edit summary
183
            $this->log->warning("Bon article !!\n");
184
        }
185
    }
186
187
    protected function makeCitationsReplacements(array $pageCitationCollection): bool
188
    {
189
        $oldText = $this->pageWorkStatus->wikiText;
190
        foreach ($pageCitationCollection as $dat) {
191
            $this->processOneCitation($dat); // that modify PageWorkStatus->wikiText
192
        }
193
        $newWikiTextValidator = new WikiTextValidator(
194
            $this->pageWorkStatus->wikiText, $oldText, $this->log, $this->pageWorkStatus->getTitle(), $this->db
195
        );
196
197
        return $newWikiTextValidator->validate();
198
    }
199
200
    /**
201
     * @throws Exception
202
     */
203
    protected function processOneCitation(array $ouvrageData): bool
204
    {
205
        $origin = $ouvrageData['raw'];
206
        $completed = $ouvrageData['opti'];
207
        $this->printDebug($ouvrageData);
208
209
        $citationValidator = new CitationValidator(
210
            $ouvrageData,
211
            $this->pageWorkStatus->wikiText,
212
            $this->log,
213
            $this->db
214
        );
215
        if ($citationValidator->validate() === false) {
216
            return false;
217
        }
218
219
        $this->generateSummaryOnPageWorkStatus($ouvrageData);
220
221
        // Replace text
222
        $newText = WikiPageAction::replaceTemplateInText($this->pageWorkStatus->wikiText, $origin, $completed);
223
224
        if (!$newText || $newText === $this->pageWorkStatus->wikiText) {
225
            $this->log->warning("newText error");
226
227
            return false;
228
        }
229
        $this->pageWorkStatus->wikiText = $newText;
230
        $this->pageWorkStatus->minorFlag = ('1' === $ouvrageData['major']) ? false : $this->pageWorkStatus->minorFlag;
231
        $this->pageWorkStatus->citationVersion = $ouvrageData['version']; // todo gérer versions différentes
232
        $this->pageWorkStatus->citationSummary[] = $ouvrageData['modifs'];
233
        $this->pageWorkStatus->nbRows++;
234
235
        return true;
236
    }
237
238
    protected function printDebug(array $data)
239
    {
240
        $this->log->debug('origin: ' . $data['raw']);
241
        $this->log->debug('completed: ' . $data['opti']);
242
        $this->log->debug('modifs: ' . $data['modifs']);
243
        $this->log->debug('version: ' . $data['version']);
244
    }
245
246
    protected function editPage(): bool
247
    {
248
        $miniSummary = $this->generateFinalSummary();
0 ignored issues
show
Unused Code introduced by
The assignment to $miniSummary is dead and can be removed.
Loading history...
249
250
        $this->log->debug("sleep 2...");
251
        sleep(2); // todo ???
252
253
        die("\n*** STOP ON OuvrageEditWorker:253 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...
254
255
        try {
0 ignored issues
show
Unused Code introduced by
TryCatchNode 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...
256
            $editInfo = ServiceFactory::editInfo($miniSummary, $this->pageWorkStatus->minorFlag, $this->pageWorkStatus->botFlag);
257
            $success = $this->wikiPageAction->editPage(Normalizer::normalize($this->pageWorkStatus->wikiText), $editInfo);
258
        } catch (Throwable $e) {
259
            // Invalid CSRF token.
260
            if (strpos($e->getMessage(), 'Invalid CSRF token') !== false) {
261
                $this->log->alert("*** Invalid CSRF token \n");
262
                throw new Exception('Invalid CSRF token', $e->getCode(), $e);
263
            } else {
264
                $this->log->warning('Exception in editPage() ' . $e->getMessage());
265
                sleep(10);
266
267
                return false;
268
            }
269
        }
270
        $this->log->info($success ? "Edition Ok\n" : "***** Edition KO !\n");
271
272
        return $success;
273
    }
274
275
    protected function updateDb(array $pageOuvrageCollection)
276
    {
277
        $title = $pageOuvrageCollection[0]['page'];
278
        foreach ($pageOuvrageCollection as $ouvrageData) {
279
            $this->db->sendEditedData(['id' => $ouvrageData['id']]);
0 ignored issues
show
Bug introduced by
The method sendEditedData() 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

279
            $this->db->/** @scrutinizer ignore-call */ 
280
                       sendEditedData(['id' => $ouvrageData['id']]);
Loading history...
280
        }
281
        try {
282
            if (self::EDIT_SIGNALEMENT && !empty($this->pageWorkStatus->errorWarning[$title])) {
283
                $this->sendOuvrageErrorsOnTalkPage($pageOuvrageCollection, $this->log);
284
            }
285
        } catch (Throwable $e) {
286
            $this->log->warning('Exception in editPage() ' . $e->getMessage());
287
            unset($e);
288
        }
289
290
        if (!$this->pageWorkStatus->botFlag) {
291
            $this->log->debug("sleep " . self::DELAY_NO_BOTFLAG_SECONDS);
292
            sleep(self::DELAY_NO_BOTFLAG_SECONDS);
293
        }
294
        if ($this->pageWorkStatus->botFlag) {
295
            $this->log->debug("sleep " . self::DELAY_BOTFLAG_SECONDS);
296
            sleep(self::DELAY_BOTFLAG_SECONDS);
297
        }
298
    }
299
300
    /**
301
     * todo extract
302
     * Pour éviter les doublons dans signalements d'erreur.
303
     */
304
    protected function addErrorWarning(string $title, string $text): void
305
    {
306
        if (!isset($this->pageWorkStatus->errorWarning[$title]) || !in_array($text, $this->pageWorkStatus->errorWarning[$title])) {
307
            $this->pageWorkStatus->errorWarning[$title][] = $text;
308
        }
309
    }
310
}
311