Passed
Pull Request — master (#157)
by MusikAnimal
05:00
created

Edit::wikifyString()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 47
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 5.9256

Importance

Changes 0
Metric Value
cc 5
eloc 26
nc 4
nop 4
dl 0
loc 47
ccs 18
cts 27
cp 0.6667
crap 5.9256
rs 8.5125
c 0
b 0
f 0
1
<?php
2
/**
3
 * This file contains only the Edit class.
4
 */
5
6
namespace Xtools;
7
8
use Xtools\User;
9
use Symfony\Component\DependencyInjection\Container;
10
use DateTime;
11
12
/**
13
 * An Edit is a single edit to a page on one project.
14
 */
15
class Edit extends Model
16
{
17
18
    /** @var Page the page associated with this edit */
19
    protected $page;
20
21
    /** @var int ID of the revision */
22
    protected $id;
23
24
    /** @var DateTime Timestamp of the revision */
25
    protected $timestamp;
26
27
    /** @var bool Whether or not this edit was a minor edit */
28
    protected $minor;
29
30
    /** @var int|string|null Length of the page as of this edit, in bytes */
31
    protected $length;
32
33
    /** @var int|string|null The diff size of this edit */
34
    protected $lengthChange;
35
36
    /** @var User - User object of who made the edit */
37
    protected $user;
38
39
    /** @var string The edit summary */
40
    protected $comment;
41
42
    /** @var string The SHA-1 of the wikitext as of the revision. */
43
    protected $sha;
44
45
    /**
46
     * Edit constructor.
47
     * @param Page $page
48
     * @param string[] $attrs Attributes, as retrieved by PageRepository::getRevisions()
49
     */
50 13
    public function __construct(Page $page, $attrs)
51
    {
52 13
        $this->page = $page;
53
54
        // Copy over supported attributes
55 13
        $this->id = (int) $attrs['id'];
56
57
        // Allow DateTime or string (latter assumed to be of format YmdHis)
58 13
        if ($attrs['timestamp'] instanceof DateTime) {
0 ignored issues
show
introduced by
The condition $attrs['timestamp'] instanceof DateTime can never be true since $attrs['timestamp'] is never a sub-type of DateTime.
Loading history...
59
            $this->timestamp = $attrs['timestamp'];
60
        } else {
61 13
            $this->timestamp = DateTime::createFromFormat('YmdHis', $attrs['timestamp']);
0 ignored issues
show
Documentation Bug introduced by
It seems like DateTime::createFromForm...', $attrs['timestamp']) can also be of type false. However, the property $timestamp is declared as type DateTime. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
62
        }
63
64 13
        $this->minor = $attrs['minor'] === '1';
65
66
        // NOTE: Do not type cast into an integer. Null values are
67
        //   our indication that the revision was revision-deleted.
68 13
        $this->length = $attrs['length'];
69 13
        $this->lengthChange = $attrs['length_change'];
70
71 13
        $this->user = new User($attrs['username']);
72 13
        $this->comment = $attrs['comment'];
73
74 13
        if (isset($attrs['rev_sha1']) || isset($attrs['sha'])) {
75 4
            $this->sha = isset($attrs['rev_sha1'])
76 4
                ? $attrs['rev_sha1']
77
                : $attrs['sha'];
78
        }
79 13
    }
80
81
    /**
82
     * Unique identifier for this Edit, to be used in cache keys.
83
     * @see Repository::getCacheKey()
84
     * @return string
85
     */
86
    public function getCacheKey()
87
    {
88
        return $this->id;
89
    }
90
91
    /**
92
     * Get the page to which this edit belongs.
93
     * @return Page
94
     */
95 11
    public function getPage()
96
    {
97 11
        return $this->page;
98
    }
99
100
    /**
101
     * ID of the edit.
102
     * @return int
103
     */
104 5
    public function getId()
105
    {
106 5
        return $this->id;
107
    }
108
109
    /**
110
     * Get the edit's timestamp.
111
     * @return DateTime
112
     */
113 5
    public function getTimestamp()
114
    {
115 5
        return $this->timestamp;
116
    }
117
118
    /**
119
     * Year the revision was made.
120
     * @return string
121
     */
122 5
    public function getYear()
123
    {
124 5
        return $this->timestamp->format('Y');
125
    }
126
127
    /**
128
     * Get the numeric representation of the month the revision was made, with leading zeros.
129
     * @return string
130
     */
131 5
    public function getMonth()
132
    {
133 5
        return $this->timestamp->format('m');
134
    }
135
136
    /**
137
     * Whether or not this edit was a minor edit.
138
     * @return bool
139
     */
140 5
    public function getMinor()
141
    {
142 5
        return $this->minor;
143
    }
144
145
    /**
146
     * Alias of getMinor()
147
     * @return bool Whether or not this edit was a minor edit
148
     */
149 5
    public function isMinor()
150
    {
151 5
        return $this->getMinor();
152
    }
153
154
    /**
155
     * Length of the page as of this edit, in bytes.
156
     * @see Edit::getSize() Edit::getSize() for the size <em>change</em>.
157
     * @return int
158
     */
159 5
    public function getLength()
160
    {
161 5
        return $this->length;
162
    }
163
164
    /**
165
     * The diff size of this edit.
166
     * @return int Signed length change in bytes.
167
     */
168 5
    public function getSize()
169
    {
170 5
        return $this->lengthChange;
171
    }
172
173
    /**
174
     * Alias of getSize()
175
     * @return int The diff size of this edit
176
     */
177 1
    public function getLengthChange()
178
    {
179 1
        return $this->getSize();
180
    }
181
182
    /**
183
     * Get the user who made the edit.
184
     * @return User
185
     */
186 6
    public function getUser()
187
    {
188 6
        return $this->user;
189
    }
190
191
    /**
192
     * Get the edit summary.
193
     * @return string
194
     */
195 1
    public function getComment()
196
    {
197 1
        return $this->comment;
198
    }
199
200
    /**
201
     * Get the edit summary (alias of Edit::getComment()).
202
     * @return string
203
     */
204 1
    public function getSummary()
205
    {
206 1
        return $this->getComment();
207
    }
208
209
    /**
210
     * Get the SHA-1 of the revision.
211
     * @return string
212
     */
213 4
    public function getSha()
214
    {
215 4
        return $this->sha;
216
    }
217
218
    /**
219
     * Get edit summary as 'wikified' HTML markup
220
     * @param bool $useUnnormalizedPageTitle Use the unnormalized page title to avoid
221
     *   an API call. This should be used only if you fetched the page title via other
222
     *   means (SQL query), and is not from user input alone.
223
     * @return string Safe HTML
224
     */
225 1
    public function getWikifiedComment($useUnnormalizedPageTitle = false)
226
    {
227 1
        return self::wikifyString(
228 1
            $this->getSummary(),
229 1
            $this->getProject(),
230 1
            $this->page,
231 1
            $useUnnormalizedPageTitle
232
        );
233
    }
234
235
    /**
236
     * Public static method to wikify a summary, can be used on any arbitrary string.
237
     * Does NOT support section links unless you specify a page.
238
     * @param string $summary
239
     * @param Project $project
240
     * @param Page $page
241
     * @param bool $useUnnormalizedPageTitle Use the unnormalized page title to avoid
242
     *   an API call. This should be used only if you fetched the page title via other
243
     *   means (SQL query), and is not from user input alone.
244
     * @static
245
     * @return string
246
     */
247 2
    public static function wikifyString(
248
        $summary,
249
        Project $project,
250
        Page $page = null,
251
        $useUnnormalizedPageTitle = false
252
    ) {
253 2
        $summary = htmlspecialchars($summary, ENT_NOQUOTES);
254 2
        $sectionMatch = null;
255 2
        $isSection = preg_match_all("/^\/\* (.*?) \*\//", $summary, $sectionMatch);
256
257 2
        if ($isSection && isset($page)) {
258
            $pageUrl = $project->getUrl(false) . str_replace(
259
                '$1',
260
                $page->getTitle($useUnnormalizedPageTitle),
261
                $project->getArticlePath()
262
            );
263
            $sectionTitle = $sectionMatch[1][0];
264
265
            // Must have underscores for the link to properly go to the section.
266
            $sectionTitleLink = htmlspecialchars(str_replace(' ', '_', $sectionTitle));
267
268
            $sectionWikitext = "<a target='_blank' href='$pageUrl#$sectionTitleLink'>&rarr;</a>" .
269
                "<em class='text-muted'>" . htmlspecialchars($sectionTitle) . ":</em> ";
270
            $summary = str_replace($sectionMatch[0][0], $sectionWikitext, $summary);
271
        }
272
273 2
        $linkMatch = null;
274
275 2
        while (preg_match_all("/\[\[:?(.*?)\]\]/", $summary, $linkMatch)) {
276 1
            $wikiLinkParts = explode('|', $linkMatch[1][0]);
277 1
            $wikiLinkPath = htmlspecialchars($wikiLinkParts[0]);
278 1
            $wikiLinkText = htmlspecialchars(
279 1
                isset($wikiLinkParts[1]) ? $wikiLinkParts[1] : $wikiLinkPath
280
            );
281
282
            // Use normalized page title (underscored, capitalized).
283 1
            $pageUrl = $project->getUrl(false) . str_replace(
284 1
                '$1',
285 1
                ucfirst(str_replace(' ', '_', $wikiLinkPath)),
286 1
                $project->getArticlePath()
287
            );
288
289 1
            $link = "<a target='_blank' href='$pageUrl'>$wikiLinkText</a>";
290 1
            $summary = str_replace($linkMatch[0][0], $link, $summary);
291
        }
292
293 2
        return $summary;
294
    }
295
296
    /**
297
     * Get edit summary as 'wikified' HTML markup (alias of Edit::getWikifiedSummary()).
298
     * @return string
299
     */
300 1
    public function getWikifiedSummary()
301
    {
302 1
        return $this->getWikifiedComment();
303
    }
304
305
    /**
306
     * Get the project this edit was made on
307
     * @return Project
308
     */
309 11
    public function getProject()
310
    {
311 11
        return $this->getPage()->getProject();
312
    }
313
314
    /**
315
     * Get the full URL to the diff of the edit
316
     * @return string
317
     */
318 1
    public function getDiffUrl()
319
    {
320 1
        $project = $this->getProject();
321 1
        $path = str_replace('$1', 'Special:Diff/' . $this->id, $project->getArticlePath());
322 1
        return rtrim($project->getUrl(), '/') . $path;
323
    }
324
325
    /**
326
     * Get the full permanent URL to the page at the time of the edit
327
     * @return string
328
     */
329 1
    public function getPermaUrl()
330
    {
331 1
        $project = $this->getProject();
332 1
        $path = str_replace('$1', 'Special:PermaLink/' . $this->id, $project->getArticlePath());
333 1
        return rtrim($project->getUrl(), '/') . $path;
334
    }
335
336
    /**
337
     * Was the edit a revert, based on the edit summary?
338
     * @param Container $container The DI container.
339
     * @return bool
340
     */
341 5
    public function isRevert(Container $container)
342
    {
343 5
        $automatedEditsHelper = $container->get('app.automated_edits_helper');
344 5
        return $automatedEditsHelper->isRevert($this->comment, $this->getProject()->getDomain());
345
    }
346
347
    /**
348
     * Get the name of the tool that was used to make this edit.
349
     * @param Container $container The DI container.
350
     * @return string|false The name of the tool that was used to make the edit
351
     */
352 6
    public function getTool(Container $container)
353
    {
354 6
        $automatedEditsHelper = $container->get('app.automated_edits_helper');
355 6
        return $automatedEditsHelper->getTool($this->comment, $this->getProject()->getDomain());
356
    }
357
358
    /**
359
     * Was the edit (semi-)automated, based on the edit summary?
360
     * @param  Container $container [description]
361
     * @return bool
362
     */
363 1
    public function isAutomated(Container $container)
364
    {
365 1
        return (bool) $this->getTool($container);
366
    }
367
368
    /**
369
     * Was the edit made by a logged out user?
370
     * @return bool
371
     */
372 5
    public function isAnon()
373
    {
374 5
        return $this->getUser()->isAnon();
375
    }
376
}
377