Passed
Push — master ( a73e13...03a489 )
by Dispositif
15:32
created

VoteAdmin::getAdminScoreHtml()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 9
nc 2
nop 0
dl 0
loc 16
rs 9.9666
c 1
b 0
f 0
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\Infrastructure\ServiceFactory;
14
use Exception;
15
use GuzzleHttp\Client;
16
use Mediawiki\DataModel\EditInfo;
17
18
/**
19
 * See also https://github.com/enterprisey/AAdminScore/blob/master/js/aadminscore.js
20
 * https://fr.wikipedia.org/wiki/Cat%C3%A9gorie:%C3%89lection_administrateur_en_cours
21
 * Class VoteAdmin
22
 */
23
class VoteAdmin
24
{
25
    const SUMMARY               = '/* Approbation :*/ 🗳️ 🕊';
26
    const FILENAME_PHRASES_POUR = __DIR__.'/resources/phrases_voteAdmin_pour.txt';
27
    const FILENAME_BLACKLIST    = __DIR__.'/resources/blacklist.txt';
28
    const MIN_VALUE_POUR        = 0.65;
29
    const MIN_COUNT_POUR        = 7;
30
    const MIN_ADMIN_SCORE       = 500;
31
    const BOURRAGE_DETECT_REGEX = '#\[\[(?:User|Utilisateur|Utilisatrice):(Irønie|CodexBot|ZiziBot)#i';
32
33
    /**
34
     * @var string
35
     */
36
    private $voteText;
37
    /**
38
     * @var string
39
     */
40
    private $votePage;
41
    /**
42
     * @var WikiPageAction
43
     */
44
    private $pageAction;
45
    private $pageText;
46
    /**
47
     * @var string
48
     */
49
    private $comment = '';
50
51
    public function __construct(string $AdminVotePage)
52
    {
53
        $this->votePage = $AdminVotePage;
54
        $this->process();
55
    }
56
57
    private function process()
58
    {
59
        if (!$this->checkBlacklist()) {
60
            return false;
61
        }
62
63
        if (!$this->checkPourContre()) {
64
            echo "check pour/contre => false";
65
66
            return false;
67
        }
68
69
        $adminScore = $this->getAdminScore();
70
        if ($adminScore && $adminScore < self::MIN_ADMIN_SCORE) {
1 ignored issue
show
Bug Best Practice introduced by
The expression $adminScore of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
71
            echo "Admin score => false";
72
73
            return false;
74
        }
75
76
        $this->comment .= ' – adminScore: '.$adminScore;
77
78
        $this->voteText = sprintf("%s ~~~~\n", $this->selectVoteText());
79
80
        dump($this->comment);
81
        dump($this->voteText);
82
        sleep(5);
83
84
        $insertResult = $this->generateVoteInsertionText();
85
        if (empty($insertResult)) {
86
            echo "insertResult vide\n";
87
88
            return false;
89
        }
90
91
        dump($insertResult);
92
93
        sleep(20);
94
95
        return $this->editVote($insertResult);
96
    }
97
98
    private function selectVoteText(): string
99
    {
100
        $sentences = file(self::FILENAME_PHRASES_POUR, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
101
        if (!$sentences) {
102
            throw new ConfigException('Pas de phrases disponibles');
103
        }
104
105
        return (string)trim($sentences[array_rand($sentences)]);
106
    }
107
108
    private function generateVoteInsertionText(): ?string
109
    {
110
        $wikiText = $this->getText();
111
112
        if (empty($wikiText)) {
113
            echo "Page vide\n";
114
115
            return null;
116
        }
117
118
        if (!$this->isAllowedToVote($wikiText)) {
119
            echo "Not allowed to vote\n";
120
121
            return null;
122
        }
123
124
        // insertion texte {{pour}}
125
        if (!preg_match('/(# \{\{Pour\}\}[^#\n]+\n)\n*==== Opposition ====/im', $wikiText, $matches)) {
126
            return null;
127
        }
128
129
        if (empty($matches[1])) {
130
            return null;
131
        }
132
133
        // note : \n déjà inclus
134
        return str_replace($matches[1], $matches[1].$this->voteText, $wikiText);
135
    }
136
137
    private function getText(): ?string
138
    {
139
        if ($this->pageText) {
140
            // cache
141
            return $this->pageText;
142
        }
143
144
        $this->pageAction = ServiceFactory::wikiPageAction($this->votePage);
145
        $this->pageText = $this->pageAction->getText();
146
147
        return $this->pageText;
148
    }
149
150
    private function editVote(string $insertVote): bool
151
    {
152
        if (!empty($insertVote)) {
153
            $summary = sprintf(
154
                '%s (%s)',
155
                self::SUMMARY,
156
                $this->comment
157
            );
158
159
            return $this->pageAction->editPage($insertVote, new EditInfo($summary, false, false), true);
160
        }
1 ignored issue
show
Bug Best Practice introduced by
The function implicitly returns null when the if condition on line 152 is false. This 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...
161
    }
162
163
    private function isAllowedToVote(string $wikitext): bool
164
    {
165
        // bourrage d'urne
166
        if (preg_match(self::BOURRAGE_DETECT_REGEX, $wikitext)) {
167
            echo "Bourrage d'urne ! ;) \n";
168
169
            return false;
170
        }
171
        if (!preg_match('#\{\{Élection administrateur en cours#i', $wikitext)) {
172
            return false;
173
        }
174
175
        return true;
176
    }
177
178
    /**
179
     * Return true if 15 {{pour}} and 60% {{pour}}
180
     *
181
     * @return bool
182
     */
183
    private function checkPourContre(): bool
184
    {
185
        $lowerText = strtolower($this->getText());
186
        $pour = substr_count($lowerText, '{{pour}}');
187
        $contre = substr_count($lowerText, '{{contre}}');
188
        $neutre = substr_count($lowerText, '{{neutre}}');
189
        $stat = $pour / ($pour + $contre + $neutre);
190
191
        echo "Stat {pour} : $pour \n";
192
        echo 'Stat pour/contre+neutre : '.(100 * $stat)." % \n";
193
194
        $this->comment .= $pour.';'.number_format($stat, 1).';false';
195
196
        if ($pour >= self::MIN_COUNT_POUR && $stat >= self::MIN_VALUE_POUR) {
197
            return true;
198
        }
199
200
        return false;
201
    }
202
203
    private function getBlacklist(): array
204
    {
205
        $list = file(self::FILENAME_BLACKLIST, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
206
207
        return $list ?? [];
1 ignored issue
show
Bug Best Practice introduced by
The expression return $list ?? array() could return the type false which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
208
    }
209
210
    /**
211
     * TODO move
212
     * TODO gérer espace "_"
213
     *
214
     * @return bool
215
     * @throws Exception
216
     */
217
    private function checkBlacklist(): bool
218
    {
219
        // Wikipédia:Administrateur/Ariel_Provost
220
        $blacklist = $this->getBlacklist();
221
222
        $user = $this->getUsername();
223
224
        if (in_array($user, $blacklist)) {
225
            echo "USER IS BLACKLISTED !! \n";
226
227
            return false;
228
        }
229
230
        return true;
231
    }
232
233
    private function getUsername()
234
    {
235
        if (!preg_match('#^Wikipédia:(?:Administrateur|Administratrice)/(.+)$#', $this->votePage, $matches)) {
236
            throw new Exception('username not found');
237
        }
238
239
        return str_replace('_', ' ', $matches[1]);
240
    }
241
242
    /**
243
     * Extract the Xtools "admin score" calculation.
244
     * See https://xtools.wmflabs.org/adminscore
245
     * also :
246
     * https://xtools.wmflabs.org/adminscore/fr.wikipedia.org/Ariel%20Provost
247
     * https://tools.wmflabs.org/supercount/index.php?user=Jennenke&project=fr.wikipedia.org
248
     * https://xtools.wmflabs.org/adminscore/fr.wikipedia.org/Jennenke
249
     * https://github.com/x-tools/xtools/blob/master/src/AppBundle/Controller/AdminScoreController.php
250
     * https://github.com/x-tools/xtools/blob/b39e4b114418784c6adce4a4e892b4711000a847/src/AppBundle/Model/AdminScore.php#L16
251
     * https://en.wikipedia.org/wiki/Wikipedia:WikiProject_Admin_Nominators/Nomination_checklist
252
     * Copy en JS : https://github.com/enterprisey/AAdminScore/blob/master/js/aadminscore.js
253
     * Class AdminScore
254
     */
255
    public function getAdminScore(): ?int
256
    {
257
        $html = $this->getAdminScoreHtml();
258
259
        if (!empty($html) && preg_match('#<th>Total</th>[^<]*<th>([0-9]+)</th>#', $html, $matches)) {
260
            return (int)$matches[1];
261
        }
262
263
        return null;
264
    }
265
266
    /**
267
     * @return string|null
268
     * @throws Exception
269
     */
270
    private function getAdminScoreHtml(): ?string
271
    {
272
        $client = new Client(
273
            [
274
                'timeout' => 300,
275
                'headers' => ['User-Agent' => getenv('USER_AGENT')],
276
                'verify' => false,
277
            ]
278
        );
279
        $url = 'https://xtools.wmflabs.org/adminscore/fr.wikipedia.org/'.str_replace(' ', '_', $this->getUsername());
280
        $resp = $client->get($url);
281
        if ($resp->getStatusCode() === 200) {
282
            return $resp->getBody()->getContents();
283
        }
284
285
        return null;
286
    }
287
288
}
289
290