Passed
Push — master ( 1bcf8b...aa70d7 )
by Dispositif
05:36
created

AbstractBotTaskWorker::doEdition()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 14
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 25
rs 9.7998
ccs 0
cts 0
cp 0
crap 12
1
<?php
2
/**
3
 * This file is part of dispositif/wikibot application (@github)
4
 * 2019/2020 © 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\Exceptions\ConfigException;
13
use App\Domain\Models\Summary;
14
use App\Infrastructure\Logger;
15
use App\Infrastructure\PageListInterface;
16
use App\Infrastructure\ServiceFactory;
17
use Codedungeon\PHPCliColors\Color;
18
use Exception;
19
use Mediawiki\Api\MediawikiFactory;
20
use Mediawiki\Api\UsageException;
21
use Mediawiki\DataModel\EditInfo;
22
use Psr\Log\LoggerInterface;
23
use Throwable;
24
25
abstract class AbstractBotTaskWorker
26
{
27
    const TASK_BOT_FLAG                       = false;
28
    const SLEEP_AFTER_EDITION                 = 60;
29
    const MINUTES_DELAY_AFTER_LAST_HUMAN_EDIT = 15;
30
    const CHECK_EDIT_CONFLICT                 = true;
31
    const ARTICLE_ANALYZED_FILENAME           = __DIR__.'/resources/article_edited.txt';
32
    const SKIP_LASTEDIT_BY_BOT                = true;
33
    const SKIP_NOT_IN_MAIN_WIKISPACE          = true;
34
35
    /**
36
     * @var PageListInterface
37
     */
38
    protected $pageListGenerator;
39
    /**
40
     * @var WikiBotConfig
41
     */
42
    protected $bot;
43
    /**
44
     * @var MediawikiFactory
45
     */
46
    protected $wiki;
47
    /**
48
     * @var WikiPageAction
49
     */
50
    protected $pageAction;
51
    protected $defaultTaskname;
52
    protected $titleTaskname;
53
54
    // todo move (modeAuto, maxLag) to BotConfig
55
    protected $modeAuto = false;
56
    protected $maxLag = 5;
57
    /**
58
     * @var Logger|LoggerInterface
59
     */
60
    protected $log;
61
    /**
62
     * array des articles déjà anal
63
     */
64
    protected $pastAnalyzed;
65
    /**
66
     * @var Summary
67
     */
68
    protected $summary;
69
70
    /**
71
     * Goo2ouvrageWorker constructor.
72
     *
73
     * @param WikiBotConfig     $bot
74
     * @param MediawikiFactory  $wiki
75
     * @param PageListInterface $pagesGen
76
     */
77
    public function __construct(WikiBotConfig $bot, MediawikiFactory $wiki, ?PageListInterface $pagesGen = null)
78
    {
79
        $this->log = $bot->log;
80
        $this->wiki = $wiki;
81
        $this->bot = $bot;
82
        if ($pagesGen) {
83
            $this->pageListGenerator = $pagesGen;
84
        }
85
        $this->setUpInConstructor();
86
87
        $this->defaultTaskname = $bot->taskName;
88
89
        $analyzed = @file(static::ARTICLE_ANALYZED_FILENAME, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
90
        $this->pastAnalyzed = ($analyzed !== false) ? $analyzed : [];
91
92
        // @throw exception on "Invalid CSRF token"
93
        $this->run();//todo delete that and use (Worker)->run($duration) or process management
94
    }
95
96
    protected function setUpInConstructor(): void
97
    {
98
        // optional implementation
99
    }
100
101
    /**
102
     * @throws ConfigException
103
     * @throws Throwable
104
     * @throws UsageException
105
     */
106
    public function run()
107
    {
108
        $titles = $this->getTitles();
109
        echo date('d-m-Y H:i')." *** NEW WORKER ***\n";
110
        foreach ($titles as $title) {
111
            $this->titleProcess($title);
112
            sleep(3);
113
        }
114
    }
115
116
    /**
117
     * @return array
118
     * @throws ConfigException
119
     */
120
    protected function getTitles(): array
121
    {
122
        if ($this->pageListGenerator === null) {
123
            throw new ConfigException('Empty PageListGenerator');
124
        }
125
126
        return $this->pageListGenerator->getPageTitles();
127
    }
128
129
    /**
130
     * @param string $title
131
     *
132
     * @throws UsageException
133
     * @throws Throwable
134
     */
135
    protected function titleProcess(string $title): void
136
    {
137
        echo "---------------------\n";
138
        echo date('d-m-Y H:i').' '.Color::BG_CYAN."  $title ".Color::NORMAL."\n";
139
        sleep(1);
140
141
        if (in_array($title, $this->pastAnalyzed)) {
142
            echo "Skip : déjà analysé\n";
143
144
            return;
145
        }
146
147
        $this->titleTaskname = $this->defaultTaskname;
148
149
        $text = $this->getText($title);
150
        if (static::SKIP_LASTEDIT_BY_BOT && $this->pageAction->getLastEditor() === getenv('BOT_NAME')) {
151
            echo "Skip : déjà édité par le bot\n";
152
153
            return;
154
        }
155
        if (empty($text) || !$this->checkAllowedEdition($title, $text)) {
156
            return;
157
        }
158
159
        $this->summary = new Summary($this->defaultTaskname);
160
        $this->summary->setBotFlag(static::TASK_BOT_FLAG);
161
162
        $newText = $this->processDomain($title, $text);
163
164
        $this->memorizeAndSaveAnalyzedPage($title);
165
166
        if (empty($newText) || $newText === $text) {
167
            echo "Skip identique ou vide\n";
168
169
            return;
170
        }
171
172
        if (!$this->modeAuto) {
173
            $ask = readline(Color::LIGHT_MAGENTA."*** ÉDITION ? [y/n/auto]".Color::NORMAL);
174
            if ('auto' === $ask) {
175
                $this->modeAuto = true;
176
            } elseif ('y' !== $ask) {
177
                return;
178
            }
179
        }
180
181
        $this->doEdition($newText);
182
    }
183
184
    /**
185
     * todo DI
186
     *
187
     * @param string $title
188
     *
189
     * @return string|null
190
     * @throws Exception
191
     * @throws Exception
192
     */
193
    protected function getText(string $title): ?string
194
    {
195
        $this->pageAction = ServiceFactory::wikiPageAction($title);
196
        if (static::SKIP_NOT_IN_MAIN_WIKISPACE && $this->pageAction->getNs() !== 0) {
197
            throw new Exception("La page n'est pas dans Main (ns!==0)");
198
        }
199
200
        return $this->pageAction->getText();
201
    }
202
203
    /**
204
     * todo distinguer 2 methodes : ban temporaire et permanent (=> logAnalyzed)
205
     * Controle droit d'edition.
206
     *
207
     * @param string $title
208
     * @param string $text
209
     *
210
     * @return bool
211
     * @throws UsageException
212
     */
213
    protected function checkAllowedEdition(string $title, string $text): bool
214
    {
215
        $this->bot->checkStopOnTalkpage(true);
216
217
        if (WikiBotConfig::isEditionRestricted($text)) {
218
            echo "SKIP : protection/3R/travaux.\n";
219
220
            return false;
221
        }
222
        if ($this->bot->minutesSinceLastEdit($title) < static::MINUTES_DELAY_AFTER_LAST_HUMAN_EDIT) {
223
            echo "SKIP : édition humaine dans les dernières ".static::MINUTES_DELAY_AFTER_LAST_HUMAN_EDIT." minutes.\n";
224
225
            return false;
226
        }
227
        if (preg_match('#{{ ?En-tête label ?\| ?AdQ#i', $text)) {
228
            echo "SKIP : AdQ.\n"; // BA ??
229
230
            return false;
231
        }
232
233
        return true;
234
    }
235
236
    /**
237
     * return $newText for editing
238
     *
239
     * @param string $title
240
     * @param string $text
241
     *
242
     * @return string|null
243
     */
244
    abstract protected function processDomain(string $title, string $text): ?string;
245
246
    protected function doEdition(string $newText): void
247
    {
248
        $summaryText = $this->generateSummaryText();
249
250
        try {
251
            $result = $this->pageAction->editPage(
252
                $newText,
253
                new EditInfo($summaryText, $this->summary->isMinorFlag(), $this->summary->isBotFlag(), $this->maxLag),
254
                static::CHECK_EDIT_CONFLICT
255
            );
256
        } catch (Throwable $e) {
257
            if (preg_match('#Invalid CSRF token#', $e->getMessage())) {
258
                throw new \Exception('Invalid CSRF token');
259
            }
260
261
            // If not a critical edition error
262
            // example : Wiki Conflict : Page has been edited after getText()
263
            $this->log->warning($e->getMessage());
264
265
            return;
266
        }
267
268
        dump($result);
269
        echo "Sleep ".(string)static::SLEEP_AFTER_EDITION."\n";
270
        sleep(static::SLEEP_AFTER_EDITION);
271
    }
272
273
    /**
274
     * @param string $title
275
     */
276
    private function memorizeAndSaveAnalyzedPage(string $title): void
277
    {
278
        if (!in_array($title, $this->pastAnalyzed)) {
279
            $this->pastAnalyzed[] = $title;
280
            @file_put_contents(static::ARTICLE_ANALYZED_FILENAME, $title.PHP_EOL, FILE_APPEND);
1 ignored issue
show
Security Best Practice introduced by
It seems like you do not handle an error condition for file_put_contents(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

280
            /** @scrutinizer ignore-unhandled */ @file_put_contents(static::ARTICLE_ANALYZED_FILENAME, $title.PHP_EOL, FILE_APPEND);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
281
            sleep(1);
282
        }
283
    }
284
285
    /**
286
     * @return string
287
     */
288
    protected function generateSummaryText(): string
289
    {
290
        return $this->summary->serialize();
291
    }
292
}
293