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
|
|||
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 |
In PHP, under loose comparison (like
==
, or!=
, orswitch
conditions), values of different types might be equal.For
integer
values, zero is a special case, in particular the following results might be unexpected: