Completed
Branch master (939199)
by
unknown
39:35
created

includes/changes/CategoryMembershipChange.php (5 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Helper class for category membership changes
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 * @author Kai Nissen
22
 * @author Addshore
23
 * @since 1.27
24
 */
25
26
use Wikimedia\Assert\Assert;
27
28
class CategoryMembershipChange {
29
30
	const CATEGORY_ADDITION = 1;
31
	const CATEGORY_REMOVAL = -1;
32
33
	/**
34
	 * @var string Current timestamp, set during CategoryMembershipChange::__construct()
35
	 */
36
	private $timestamp;
37
38
	/**
39
	 * @var Title Title instance of the categorized page
40
	 */
41
	private $pageTitle;
42
43
	/**
44
	 * @var Revision|null Latest Revision instance of the categorized page
45
	 */
46
	private $revision;
47
48
	/**
49
	 * @var int
50
	 * Number of pages this WikiPage is embedded by
51
	 * Set by CategoryMembershipChange::checkTemplateLinks()
52
	 */
53
	private $numTemplateLinks = 0;
54
55
	/**
56
	 * @var callable|null
57
	 */
58
	private $newForCategorizationCallback = null;
59
60
	/**
61
	 * @param Title $pageTitle Title instance of the categorized page
62
	 * @param Revision $revision Latest Revision instance of the categorized page
63
	 *
64
	 * @throws MWException
65
	 */
66
	public function __construct( Title $pageTitle, Revision $revision = null ) {
67
		$this->pageTitle = $pageTitle;
68
		if ( $revision === null ) {
69
			$this->timestamp = wfTimestampNow();
0 ignored issues
show
Documentation Bug introduced by
It seems like wfTimestampNow() can also be of type false. However, the property $timestamp is declared as type string. 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...
70
		} else {
71
			$this->timestamp = $revision->getTimestamp();
0 ignored issues
show
Documentation Bug introduced by
It seems like $revision->getTimestamp() can also be of type false. However, the property $timestamp is declared as type string. 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...
72
		}
73
		$this->revision = $revision;
74
		$this->newForCategorizationCallback = [ 'RecentChange', 'newForCategorization' ];
75
	}
76
77
	/**
78
	 * Overrides the default new for categorization callback
79
	 * This is intended for use while testing and will fail if MW_PHPUNIT_TEST is not defined.
80
	 *
81
	 * @param callable $callback
82
	 * @see RecentChange::newForCategorization for callback signiture
83
	 *
84
	 * @throws MWException
85
	 */
86
	public function overrideNewForCategorizationCallback( $callback ) {
87
		if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
88
			throw new MWException( 'Cannot override newForCategorization callback in operation.' );
89
		}
90
		Assert::parameterType( 'callable', $callback, '$callback' );
91
		$this->newForCategorizationCallback = $callback;
92
	}
93
94
	/**
95
	 * Determines the number of template links for recursive link updates
96
	 */
97
	public function checkTemplateLinks() {
98
		$this->numTemplateLinks = $this->pageTitle->getBacklinkCache()->getNumLinks( 'templatelinks' );
99
	}
100
101
	/**
102
	 * Create a recentchanges entry for category additions
103
	 *
104
	 * @param Title $categoryTitle
105
	 */
106
	public function triggerCategoryAddedNotification( Title $categoryTitle ) {
107
		$this->createRecentChangesEntry( $categoryTitle, self::CATEGORY_ADDITION );
108
	}
109
110
	/**
111
	 * Create a recentchanges entry for category removals
112
	 *
113
	 * @param Title $categoryTitle
114
	 */
115
	public function triggerCategoryRemovedNotification( Title $categoryTitle ) {
116
		$this->createRecentChangesEntry( $categoryTitle, self::CATEGORY_REMOVAL );
117
	}
118
119
	/**
120
	 * Create a recentchanges entry using RecentChange::notifyCategorization()
121
	 *
122
	 * @param Title $categoryTitle
123
	 * @param int $type
124
	 */
125
	private function createRecentChangesEntry( Title $categoryTitle, $type ) {
126
		$this->notifyCategorization(
127
			$this->timestamp,
128
			$categoryTitle,
129
			$this->getUser(),
0 ignored issues
show
It seems like $this->getUser() targeting CategoryMembershipChange::getUser() can also be of type false; however, CategoryMembershipChange::notifyCategorization() does only seem to accept null|object<User>, did you maybe forget to handle an error condition?
Loading history...
130
			$this->getChangeMessageText(
131
				$type,
132
				[ 'prefixedText' => $this->pageTitle->getPrefixedText() ],
133
				$this->numTemplateLinks
134
			),
135
			$this->pageTitle,
136
			$this->getPreviousRevisionTimestamp(),
137
			$this->revision
138
		);
139
	}
140
141
	/**
142
	 * @param string $timestamp Timestamp of the recent change to occur in TS_MW format
143
	 * @param Title $categoryTitle Title of the category a page is being added to or removed from
144
	 * @param User $user User object of the user that made the change
145
	 * @param string $comment Change summary
146
	 * @param Title $pageTitle Title of the page that is being added or removed
147
	 * @param string $lastTimestamp Parent revision timestamp of this change in TS_MW format
148
	 * @param Revision|null $revision
149
	 *
150
	 * @throws MWException
151
	 */
152
	private function notifyCategorization(
153
		$timestamp,
154
		Title $categoryTitle,
155
		User $user = null,
156
		$comment,
157
		Title $pageTitle,
158
		$lastTimestamp,
159
		$revision
160
	) {
161
		$deleted = $revision ? $revision->getVisibility() & Revision::SUPPRESSED_USER : 0;
162
		$newRevId = $revision ? $revision->getId() : 0;
163
164
		/**
165
		 * T109700 - Default bot flag to true when there is no corresponding RC entry
166
		 * This means all changes caused by parser functions & Lua on reparse are marked as bot
167
		 * Also in the case no RC entry could be found due to replica DB lag
168
		 */
169
		$bot = 1;
170
		$lastRevId = 0;
171
		$ip = '';
172
173
		# If no revision is given, the change was probably triggered by parser functions
174
		if ( $revision !== null ) {
175
			$correspondingRc = $this->revision->getRecentChange();
176
			if ( $correspondingRc === null ) {
177
				$correspondingRc = $this->revision->getRecentChange( Revision::READ_LATEST );
178
			}
179
			if ( $correspondingRc !== null ) {
180
				$bot = $correspondingRc->getAttribute( 'rc_bot' ) ?: 0;
181
				$ip = $correspondingRc->getAttribute( 'rc_ip' ) ?: '';
182
				$lastRevId = $correspondingRc->getAttribute( 'rc_last_oldid' ) ?: 0;
183
			}
184
		}
185
186
		/** @var RecentChange $rc */
187
		$rc = call_user_func_array(
188
			$this->newForCategorizationCallback,
189
			[
190
				$timestamp,
191
				$categoryTitle,
192
				$user,
193
				$comment,
194
				$pageTitle,
195
				$lastRevId,
196
				$newRevId,
197
				$lastTimestamp,
198
				$bot,
199
				$ip,
200
				$deleted
201
			]
202
		);
203
		$rc->save();
204
	}
205
206
	/**
207
	 * Get the user associated with this change.
208
	 *
209
	 * If there is no revision associated with the change and thus no editing user
210
	 * fallback to a default.
211
	 *
212
	 * False will be returned if the user name specified in the
213
	 * 'autochange-username' message is invalid.
214
	 *
215
	 * @return User|bool
216
	 */
217
	private function getUser() {
218
		if ( $this->revision ) {
219
			$userId = $this->revision->getUser( Revision::RAW );
220
			if ( $userId === 0 ) {
221
				return User::newFromName( $this->revision->getUserText( Revision::RAW ), false );
0 ignored issues
show
It seems like $this->revision->getUserText(\Revision::RAW) targeting Revision::getUserText() can also be of type boolean; however, User::newFromName() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
222
			} else {
223
				return User::newFromId( $userId );
224
			}
225
		}
226
227
		$username = wfMessage( 'autochange-username' )->inContentLanguage()->text();
228
		$user = User::newFromName( $username );
229
		# User::newFromName() can return false on a badly configured wiki.
230
		if ( $user && !$user->isLoggedIn() ) {
231
			$user->addToDatabase();
232
		}
233
234
		return $user;
235
	}
236
237
	/**
238
	 * Returns the change message according to the type of category membership change
239
	 *
240
	 * The message keys created in this method may be one of:
241
	 * - recentchanges-page-added-to-category
242
	 * - recentchanges-page-added-to-category-bundled
243
	 * - recentchanges-page-removed-from-category
244
	 * - recentchanges-page-removed-from-category-bundled
245
	 *
246
	 * @param int $type may be CategoryMembershipChange::CATEGORY_ADDITION
247
	 * or CategoryMembershipChange::CATEGORY_REMOVAL
248
	 * @param array $params
249
	 * - prefixedText: result of Title::->getPrefixedText()
250
	 * @param int $numTemplateLinks
251
	 *
252
	 * @return string
253
	 */
254
	private function getChangeMessageText( $type, array $params, $numTemplateLinks ) {
255
		$array = [
256
			self::CATEGORY_ADDITION => 'recentchanges-page-added-to-category',
257
			self::CATEGORY_REMOVAL => 'recentchanges-page-removed-from-category',
258
		];
259
260
		$msgKey = $array[$type];
261
262
		if ( intval( $numTemplateLinks ) > 0 ) {
263
			$msgKey .= '-bundled';
264
		}
265
266
		return wfMessage( $msgKey, $params )->inContentLanguage()->text();
267
	}
268
269
	/**
270
	 * Returns the timestamp of the page's previous revision or null if the latest revision
271
	 * does not refer to a parent revision
272
	 *
273
	 * @return null|string
274
	 */
275
	private function getPreviousRevisionTimestamp() {
276
		$previousRev = Revision::newFromId(
277
				$this->pageTitle->getPreviousRevisionID( $this->pageTitle->getLatestRevID() )
0 ignored issues
show
It seems like $this->pageTitle->getPre...itle->getLatestRevID()) targeting Title::getPreviousRevisionID() can also be of type false; however, Revision::newFromId() does only seem to accept integer, did you maybe forget to handle an error condition?
Loading history...
278
			);
279
280
		return $previousRev ? $previousRev->getTimestamp() : null;
281
	}
282
283
}
284