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

includes/Category.php (1 issue)

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
 * Representation for a category.
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 Simetrical
22
 */
23
24
/**
25
 * Category objects are immutable, strictly speaking. If you call methods that change the database,
26
 * like to refresh link counts, the objects will be appropriately reinitialized.
27
 * Member variables are lazy-initialized.
28
 *
29
 * @todo Move some stuff from CategoryPage.php to here, and use that.
30
 */
31
class Category {
32
	/** Name of the category, normalized to DB-key form */
33
	private $mName = null;
34
	private $mID = null;
35
	/**
36
	 * Category page title
37
	 * @var Title
38
	 */
39
	private $mTitle = null;
40
	/** Counts of membership (cat_pages, cat_subcats, cat_files) */
41
	private $mPages = null, $mSubcats = null, $mFiles = null;
42
43
	private function __construct() {
44
	}
45
46
	/**
47
	 * Set up all member variables using a database query.
48
	 * @throws MWException
49
	 * @return bool True on success, false on failure.
50
	 */
51
	protected function initialize() {
52
		if ( $this->mName === null && $this->mID === null ) {
53
			throw new MWException( __METHOD__ . ' has both names and IDs null' );
54
		} elseif ( $this->mID === null ) {
55
			$where = [ 'cat_title' => $this->mName ];
56
		} elseif ( $this->mName === null ) {
57
			$where = [ 'cat_id' => $this->mID ];
58
		} else {
59
			# Already initialized
60
			return true;
61
		}
62
63
		$dbr = wfGetDB( DB_REPLICA );
64
		$row = $dbr->selectRow(
65
			'category',
66
			[ 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files' ],
67
			$where,
68
			__METHOD__
69
		);
70
71
		if ( !$row ) {
72
			# Okay, there were no contents.  Nothing to initialize.
73
			if ( $this->mTitle ) {
74
				# If there is a title object but no record in the category table,
75
				# treat this as an empty category.
76
				$this->mID = false;
77
				$this->mName = $this->mTitle->getDBkey();
78
				$this->mPages = 0;
79
				$this->mSubcats = 0;
80
				$this->mFiles = 0;
81
82
				# If the title exists, call refreshCounts to add a row for it.
83
				if ( $this->mTitle->exists() ) {
84
					DeferredUpdates::addCallableUpdate( [ $this, 'refreshCounts' ] );
85
				}
86
87
				return true;
88
			} else {
89
				return false; # Fail
90
			}
91
		}
92
93
		$this->mID = $row->cat_id;
94
		$this->mName = $row->cat_title;
95
		$this->mPages = $row->cat_pages;
96
		$this->mSubcats = $row->cat_subcats;
97
		$this->mFiles = $row->cat_files;
98
99
		# (bug 13683) If the count is negative, then 1) it's obviously wrong
100
		# and should not be kept, and 2) we *probably* don't have to scan many
101
		# rows to obtain the correct figure, so let's risk a one-time recount.
102
		if ( $this->mPages < 0 || $this->mSubcats < 0 || $this->mFiles < 0 ) {
103
			$this->mPages = max( $this->mPages, 0 );
104
			$this->mSubcats = max( $this->mSubcats, 0 );
105
			$this->mFiles = max( $this->mFiles, 0 );
106
107
			DeferredUpdates::addCallableUpdate( [ $this, 'refreshCounts' ] );
108
		}
109
110
		return true;
111
	}
112
113
	/**
114
	 * Factory function.
115
	 *
116
	 * @param array $name A category name (no "Category:" prefix).  It need
117
	 *   not be normalized, with spaces replaced by underscores.
118
	 * @return mixed Category, or false on a totally invalid name
119
	 */
120
	public static function newFromName( $name ) {
121
		$cat = new self();
122
		$title = Title::makeTitleSafe( NS_CATEGORY, $name );
123
124
		if ( !is_object( $title ) ) {
125
			return false;
126
		}
127
128
		$cat->mTitle = $title;
129
		$cat->mName = $title->getDBkey();
130
131
		return $cat;
132
	}
133
134
	/**
135
	 * Factory function.
136
	 *
137
	 * @param Title $title Title for the category page
138
	 * @return Category|bool On a totally invalid name
139
	 */
140
	public static function newFromTitle( $title ) {
141
		$cat = new self();
142
143
		$cat->mTitle = $title;
144
		$cat->mName = $title->getDBkey();
145
146
		return $cat;
147
	}
148
149
	/**
150
	 * Factory function.
151
	 *
152
	 * @param int $id A category id
153
	 * @return Category
154
	 */
155
	public static function newFromID( $id ) {
156
		$cat = new self();
157
		$cat->mID = intval( $id );
158
		return $cat;
159
	}
160
161
	/**
162
	 * Factory function, for constructing a Category object from a result set
163
	 *
164
	 * @param object $row Result set row, must contain the cat_xxx fields. If the
165
	 *   fields are null, the resulting Category object will represent an empty
166
	 *   category if a title object was given. If the fields are null and no
167
	 *   title was given, this method fails and returns false.
168
	 * @param Title $title Optional title object for the category represented by
169
	 *   the given row. May be provided if it is already known, to avoid having
170
	 *   to re-create a title object later.
171
	 * @return Category
172
	 */
173
	public static function newFromRow( $row, $title = null ) {
174
		$cat = new self();
175
		$cat->mTitle = $title;
176
177
		# NOTE: the row often results from a LEFT JOIN on categorylinks. This may result in
178
		#       all the cat_xxx fields being null, if the category page exists, but nothing
179
		#       was ever added to the category. This case should be treated link an empty
180
		#       category, if possible.
181
182
		if ( $row->cat_title === null ) {
183
			if ( $title === null ) {
184
				# the name is probably somewhere in the row, for example as page_title,
185
				# but we can't know that here...
186
				return false;
187
			} else {
188
				# if we have a title object, fetch the category name from there
189
				$cat->mName = $title->getDBkey();
190
			}
191
192
			$cat->mID = false;
193
			$cat->mSubcats = 0;
194
			$cat->mPages = 0;
195
			$cat->mFiles = 0;
196
		} else {
197
			$cat->mName = $row->cat_title;
198
			$cat->mID = $row->cat_id;
199
			$cat->mSubcats = $row->cat_subcats;
200
			$cat->mPages = $row->cat_pages;
201
			$cat->mFiles = $row->cat_files;
202
		}
203
204
		return $cat;
205
	}
206
207
	/**
208
	 * @return mixed DB key name, or false on failure
209
	 */
210
	public function getName() {
211
		return $this->getX( 'mName' );
212
	}
213
214
	/**
215
	 * @return mixed Category ID, or false on failure
216
	 */
217
	public function getID() {
218
		return $this->getX( 'mID' );
219
	}
220
221
	/**
222
	 * @return mixed Total number of member pages, or false on failure
223
	 */
224
	public function getPageCount() {
225
		return $this->getX( 'mPages' );
226
	}
227
228
	/**
229
	 * @return mixed Number of subcategories, or false on failure
230
	 */
231
	public function getSubcatCount() {
232
		return $this->getX( 'mSubcats' );
233
	}
234
235
	/**
236
	 * @return mixed Number of member files, or false on failure
237
	 */
238
	public function getFileCount() {
239
		return $this->getX( 'mFiles' );
240
	}
241
242
	/**
243
	 * @return Title|bool Title for this category, or false on failure.
244
	 */
245
	public function getTitle() {
246
		if ( $this->mTitle ) {
247
			return $this->mTitle;
248
		}
249
250
		if ( !$this->initialize() ) {
251
			return false;
252
		}
253
254
		$this->mTitle = Title::makeTitleSafe( NS_CATEGORY, $this->mName );
255
		return $this->mTitle;
256
	}
257
258
	/**
259
	 * Fetch a TitleArray of up to $limit category members, beginning after the
260
	 * category sort key $offset.
261
	 * @param int $limit
262
	 * @param string $offset
263
	 * @return TitleArray TitleArray object for category members.
264
	 */
265
	public function getMembers( $limit = false, $offset = '' ) {
266
267
		$dbr = wfGetDB( DB_REPLICA );
268
269
		$conds = [ 'cl_to' => $this->getName(), 'cl_from = page_id' ];
270
		$options = [ 'ORDER BY' => 'cl_sortkey' ];
271
272
		if ( $limit ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $limit of type false|integer is loosely compared to true; this is ambiguous if the integer can be zero. 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...
273
			$options['LIMIT'] = $limit;
274
		}
275
276
		if ( $offset !== '' ) {
277
			$conds[] = 'cl_sortkey > ' . $dbr->addQuotes( $offset );
278
		}
279
280
		$result = TitleArray::newFromResult(
281
			$dbr->select(
282
				[ 'page', 'categorylinks' ],
283
				[ 'page_id', 'page_namespace', 'page_title', 'page_len',
284
					'page_is_redirect', 'page_latest' ],
285
				$conds,
286
				__METHOD__,
287
				$options
288
			)
289
		);
290
291
		return $result;
292
	}
293
294
	/**
295
	 * Generic accessor
296
	 * @param string $key
297
	 * @return bool
298
	 */
299
	private function getX( $key ) {
300
		if ( !$this->initialize() ) {
301
			return false;
302
		}
303
		return $this->{$key};
304
	}
305
306
	/**
307
	 * Refresh the counts for this category.
308
	 *
309
	 * @return bool True on success, false on failure
310
	 */
311
	public function refreshCounts() {
312
		if ( wfReadOnly() ) {
313
			return false;
314
		}
315
316
		# If we have just a category name, find out whether there is an
317
		# existing row. Or if we have just an ID, get the name, because
318
		# that's what categorylinks uses.
319
		if ( !$this->initialize() ) {
320
			return false;
321
		}
322
323
		$dbw = wfGetDB( DB_MASTER );
324
		$dbw->startAtomic( __METHOD__ );
325
326
		$cond1 = $dbw->conditional( [ 'page_namespace' => NS_CATEGORY ], 1, 'NULL' );
327
		$cond2 = $dbw->conditional( [ 'page_namespace' => NS_FILE ], 1, 'NULL' );
328
		$result = $dbw->selectRow(
329
			[ 'categorylinks', 'page' ],
330
			[ 'pages' => 'COUNT(*)',
331
				'subcats' => "COUNT($cond1)",
332
				'files' => "COUNT($cond2)"
333
			],
334
			[ 'cl_to' => $this->mName, 'page_id = cl_from' ],
335
			__METHOD__,
336
			[ 'LOCK IN SHARE MODE' ]
337
		);
338
339
		$shouldExist = $result->pages > 0 || $this->getTitle()->exists();
340
341
		if ( $this->mID ) {
342
			if ( $shouldExist ) {
343
				# The category row already exists, so do a plain UPDATE instead
344
				# of INSERT...ON DUPLICATE KEY UPDATE to avoid creating a gap
345
				# in the cat_id sequence. The row may or may not be "affected".
346
				$dbw->update(
347
					'category',
348
					[
349
						'cat_pages' => $result->pages,
350
						'cat_subcats' => $result->subcats,
351
						'cat_files' => $result->files
352
					],
353
					[ 'cat_title' => $this->mName ],
354
					__METHOD__
355
				);
356
			} else {
357
				# The category is empty and has no description page, delete it
358
				$dbw->delete(
359
					'category',
360
					[ 'cat_title' => $this->mName ],
361
					__METHOD__
362
				);
363
				$this->mID = false;
364
			}
365
		} elseif ( $shouldExist ) {
366
			# The category row doesn't exist but should, so create it. Use
367
			# upsert in case of races.
368
			$dbw->upsert(
369
				'category',
370
				[
371
					'cat_title' => $this->mName,
372
					'cat_pages' => $result->pages,
373
					'cat_subcats' => $result->subcats,
374
					'cat_files' => $result->files
375
				],
376
				[ 'cat_title' ],
377
				[
378
					'cat_pages' => $result->pages,
379
					'cat_subcats' => $result->subcats,
380
					'cat_files' => $result->files
381
				],
382
				__METHOD__
383
			);
384
			// @todo: Should we update $this->mID here? Or not since Category
385
			// objects tend to be short lived enough to not matter?
386
		}
387
388
		$dbw->endAtomic( __METHOD__ );
389
390
		# Now we should update our local counts.
391
		$this->mPages = $result->pages;
392
		$this->mSubcats = $result->subcats;
393
		$this->mFiles = $result->files;
394
395
		return true;
396
	}
397
}
398