Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like SpecialPage often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use SpecialPage, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
36 | class SpecialPage { |
||
37 | // The canonical name of this special page |
||
38 | // Also used for the default <h1> heading, @see getDescription() |
||
39 | protected $mName; |
||
40 | |||
41 | // The local name of this special page |
||
42 | private $mLocalName; |
||
43 | |||
44 | // Minimum user level required to access this page, or "" for anyone. |
||
45 | // Also used to categorise the pages in Special:Specialpages |
||
46 | protected $mRestriction; |
||
47 | |||
48 | // Listed in Special:Specialpages? |
||
49 | private $mListed; |
||
50 | |||
51 | // Whether or not this special page is being included from an article |
||
52 | protected $mIncluding; |
||
53 | |||
54 | // Whether the special page can be included in an article |
||
55 | protected $mIncludable; |
||
56 | |||
57 | /** |
||
58 | * Current request context |
||
59 | * @var IContextSource |
||
60 | */ |
||
61 | protected $mContext; |
||
62 | |||
63 | /** |
||
64 | * @var \MediaWiki\Linker\LinkRenderer|null |
||
65 | */ |
||
66 | private $linkRenderer; |
||
67 | |||
68 | /** |
||
69 | * Get a localised Title object for a specified special page name |
||
70 | * If you don't need a full Title object, consider using TitleValue through |
||
71 | * getTitleValueFor() below. |
||
72 | * |
||
73 | * @since 1.9 |
||
74 | * @since 1.21 $fragment parameter added |
||
75 | * |
||
76 | * @param string $name |
||
77 | * @param string|bool $subpage Subpage string, or false to not use a subpage |
||
78 | * @param string $fragment The link fragment (after the "#") |
||
79 | * @return Title |
||
80 | * @throws MWException |
||
81 | */ |
||
82 | public static function getTitleFor( $name, $subpage = false, $fragment = '' ) { |
||
83 | return Title::newFromTitleValue( |
||
84 | self::getTitleValueFor( $name, $subpage, $fragment ) |
||
85 | ); |
||
86 | } |
||
87 | |||
88 | /** |
||
89 | * Get a localised TitleValue object for a specified special page name |
||
90 | * |
||
91 | * @since 1.28 |
||
92 | * @param string $name |
||
93 | * @param string|bool $subpage Subpage string, or false to not use a subpage |
||
94 | * @param string $fragment The link fragment (after the "#") |
||
95 | * @return TitleValue |
||
96 | */ |
||
97 | public static function getTitleValueFor( $name, $subpage = false, $fragment = '' ) { |
||
98 | $name = SpecialPageFactory::getLocalNameFor( $name, $subpage ); |
||
99 | |||
100 | return new TitleValue( NS_SPECIAL, $name, $fragment ); |
||
101 | } |
||
102 | |||
103 | /** |
||
104 | * Get a localised Title object for a page name with a possibly unvalidated subpage |
||
105 | * |
||
106 | * @param string $name |
||
107 | * @param string|bool $subpage Subpage string, or false to not use a subpage |
||
108 | * @return Title|null Title object or null if the page doesn't exist |
||
109 | */ |
||
110 | public static function getSafeTitleFor( $name, $subpage = false ) { |
||
118 | |||
119 | /** |
||
120 | * Default constructor for special pages |
||
121 | * Derivative classes should call this from their constructor |
||
122 | * Note that if the user does not have the required level, an error message will |
||
123 | * be displayed by the default execute() method, without the global function ever |
||
124 | * being called. |
||
125 | * |
||
126 | * If you override execute(), you can recover the default behavior with userCanExecute() |
||
127 | * and displayRestrictionError() |
||
128 | * |
||
129 | * @param string $name Name of the special page, as seen in links and URLs |
||
130 | * @param string $restriction User right required, e.g. "block" or "delete" |
||
131 | * @param bool $listed Whether the page is listed in Special:Specialpages |
||
132 | * @param callable|bool $function Unused |
||
133 | * @param string $file Unused |
||
134 | * @param bool $includable Whether the page can be included in normal pages |
||
135 | */ |
||
136 | public function __construct( |
||
145 | |||
146 | /** |
||
147 | * Get the name of this Special Page. |
||
148 | * @return string |
||
149 | */ |
||
150 | function getName() { |
||
153 | |||
154 | /** |
||
155 | * Get the permission that a user must have to execute this page |
||
156 | * @return string |
||
157 | */ |
||
158 | function getRestriction() { |
||
161 | |||
162 | // @todo FIXME: Decide which syntax to use for this, and stick to it |
||
163 | /** |
||
164 | * Whether this special page is listed in Special:SpecialPages |
||
165 | * @since 1.3 (r3583) |
||
166 | * @return bool |
||
167 | */ |
||
168 | function isListed() { |
||
171 | |||
172 | /** |
||
173 | * Set whether this page is listed in Special:Specialpages, at run-time |
||
174 | * @since 1.3 |
||
175 | * @param bool $listed |
||
176 | * @return bool |
||
177 | */ |
||
178 | function setListed( $listed ) { |
||
181 | |||
182 | /** |
||
183 | * Get or set whether this special page is listed in Special:SpecialPages |
||
184 | * @since 1.6 |
||
185 | * @param bool $x |
||
186 | * @return bool |
||
187 | */ |
||
188 | function listed( $x = null ) { |
||
191 | |||
192 | /** |
||
193 | * Whether it's allowed to transclude the special page via {{Special:Foo/params}} |
||
194 | * @return bool |
||
195 | */ |
||
196 | public function isIncludable() { |
||
199 | |||
200 | /** |
||
201 | * How long to cache page when it is being included. |
||
202 | * |
||
203 | * @note If cache time is not 0, then the current user becomes an anon |
||
204 | * if you want to do any per-user customizations, than this method |
||
205 | * must be overriden to return 0. |
||
206 | * @since 1.26 |
||
207 | * @return int Time in seconds, 0 to disable caching altogether, |
||
208 | * false to use the parent page's cache settings |
||
209 | */ |
||
210 | public function maxIncludeCacheTime() { |
||
213 | |||
214 | /** |
||
215 | * @return int Seconds that this page can be cached |
||
216 | */ |
||
217 | protected function getCacheTTL() { |
||
220 | |||
221 | /** |
||
222 | * Whether the special page is being evaluated via transclusion |
||
223 | * @param bool $x |
||
224 | * @return bool |
||
225 | */ |
||
226 | function including( $x = null ) { |
||
229 | |||
230 | /** |
||
231 | * Get the localised name of the special page |
||
232 | * @return string |
||
233 | */ |
||
234 | function getLocalName() { |
||
241 | |||
242 | /** |
||
243 | * Is this page expensive (for some definition of expensive)? |
||
244 | * Expensive pages are disabled or cached in miser mode. Originally used |
||
245 | * (and still overridden) by QueryPage and subclasses, moved here so that |
||
246 | * Special:SpecialPages can safely call it for all special pages. |
||
247 | * |
||
248 | * @return bool |
||
249 | */ |
||
250 | public function isExpensive() { |
||
253 | |||
254 | /** |
||
255 | * Is this page cached? |
||
256 | * Expensive pages are cached or disabled in miser mode. |
||
257 | * Used by QueryPage and subclasses, moved here so that |
||
258 | * Special:SpecialPages can safely call it for all special pages. |
||
259 | * |
||
260 | * @return bool |
||
261 | * @since 1.21 |
||
262 | */ |
||
263 | public function isCached() { |
||
266 | |||
267 | /** |
||
268 | * Can be overridden by subclasses with more complicated permissions |
||
269 | * schemes. |
||
270 | * |
||
271 | * @return bool Should the page be displayed with the restricted-access |
||
272 | * pages? |
||
273 | */ |
||
274 | public function isRestricted() { |
||
278 | |||
279 | /** |
||
280 | * Checks if the given user (identified by an object) can execute this |
||
281 | * special page (as defined by $mRestriction). Can be overridden by sub- |
||
282 | * classes with more complicated permissions schemes. |
||
283 | * |
||
284 | * @param User $user The user to check |
||
285 | * @return bool Does the user have permission to view the page? |
||
286 | */ |
||
287 | public function userCanExecute( User $user ) { |
||
290 | |||
291 | /** |
||
292 | * Output an error message telling the user what access level they have to have |
||
293 | * @throws PermissionsError |
||
294 | */ |
||
295 | function displayRestrictionError() { |
||
298 | |||
299 | /** |
||
300 | * Checks if userCanExecute, and if not throws a PermissionsError |
||
301 | * |
||
302 | * @since 1.19 |
||
303 | * @return void |
||
304 | * @throws PermissionsError |
||
305 | */ |
||
306 | public function checkPermissions() { |
||
311 | |||
312 | /** |
||
313 | * If the wiki is currently in readonly mode, throws a ReadOnlyError |
||
314 | * |
||
315 | * @since 1.19 |
||
316 | * @return void |
||
317 | * @throws ReadOnlyError |
||
318 | */ |
||
319 | public function checkReadOnly() { |
||
324 | |||
325 | /** |
||
326 | * If the user is not logged in, throws UserNotLoggedIn error |
||
327 | * |
||
328 | * The user will be redirected to Special:Userlogin with the given message as an error on |
||
329 | * the form. |
||
330 | * |
||
331 | * @since 1.23 |
||
332 | * @param string $reasonMsg [optional] Message key to be displayed on login page |
||
333 | * @param string $titleMsg [optional] Passed on to UserNotLoggedIn constructor |
||
334 | * @throws UserNotLoggedIn |
||
335 | */ |
||
336 | public function requireLogin( |
||
343 | |||
344 | /** |
||
345 | * Tells if the special page does something security-sensitive and needs extra defense against |
||
346 | * a stolen account (e.g. a reauthentication). What exactly that will mean is decided by the |
||
347 | * authentication framework. |
||
348 | * @return bool|string False or the argument for AuthManager::securitySensitiveOperationStatus(). |
||
349 | * Typically a special page needing elevated security would return its name here. |
||
350 | */ |
||
351 | protected function getLoginSecurityLevel() { |
||
354 | |||
355 | /** |
||
356 | * Verifies that the user meets the security level, possibly reauthenticating them in the process. |
||
357 | * |
||
358 | * This should be used when the page does something security-sensitive and needs extra defense |
||
359 | * against a stolen account (e.g. a reauthentication). The authentication framework will make |
||
360 | * an extra effort to make sure the user account is not compromised. What that exactly means |
||
361 | * will depend on the system and user settings; e.g. the user might be required to log in again |
||
362 | * unless their last login happened recently, or they might be given a second-factor challenge. |
||
363 | * |
||
364 | * Calling this method will result in one if these actions: |
||
365 | * - return true: all good. |
||
366 | * - return false and set a redirect: caller should abort; the redirect will take the user |
||
367 | * to the login page for reauthentication, and back. |
||
368 | * - throw an exception if there is no way for the user to meet the requirements without using |
||
369 | * a different access method (e.g. this functionality is only available from a specific IP). |
||
370 | * |
||
371 | * Note that this does not in any way check that the user is authorized to use this special page |
||
372 | * (use checkPermissions() for that). |
||
373 | * |
||
374 | * @param string $level A security level. Can be an arbitrary string, defaults to the page name. |
||
375 | * @return bool False means a redirect to the reauthentication page has been set and processing |
||
376 | * of the special page should be aborted. |
||
377 | * @throws ErrorPageError If the security level cannot be met, even with reauthentication. |
||
378 | */ |
||
379 | protected function checkLoginSecurityLevel( $level = null ) { |
||
403 | |||
404 | /** |
||
405 | * Return an array of subpages beginning with $search that this special page will accept. |
||
406 | * |
||
407 | * For example, if a page supports subpages "foo", "bar" and "baz" (as in Special:PageName/foo, |
||
408 | * etc.): |
||
409 | * |
||
410 | * - `prefixSearchSubpages( "ba" )` should return `array( "bar", "baz" )` |
||
411 | * - `prefixSearchSubpages( "f" )` should return `array( "foo" )` |
||
412 | * - `prefixSearchSubpages( "z" )` should return `array()` |
||
413 | * - `prefixSearchSubpages( "" )` should return `array( foo", "bar", "baz" )` |
||
414 | * |
||
415 | * @param string $search Prefix to search for |
||
416 | * @param int $limit Maximum number of results to return (usually 10) |
||
417 | * @param int $offset Number of results to skip (usually 0) |
||
418 | * @return string[] Matching subpages |
||
419 | */ |
||
420 | public function prefixSearchSubpages( $search, $limit, $offset ) { |
||
428 | |||
429 | /** |
||
430 | * Return an array of subpages that this special page will accept for prefix |
||
431 | * searches. If this method requires a query you might instead want to implement |
||
432 | * prefixSearchSubpages() directly so you can support $limit and $offset. This |
||
433 | * method is better for static-ish lists of things. |
||
434 | * |
||
435 | * @return string[] subpages to search from |
||
436 | */ |
||
437 | protected function getSubpagesForPrefixSearch() { |
||
440 | |||
441 | /** |
||
442 | * Perform a regular substring search for prefixSearchSubpages |
||
443 | * @param string $search Prefix to search for |
||
444 | * @param int $limit Maximum number of results to return (usually 10) |
||
445 | * @param int $offset Number of results to skip (usually 0) |
||
446 | * @return string[] Matching subpages |
||
447 | */ |
||
448 | protected function prefixSearchString( $search, $limit, $offset ) { |
||
463 | |||
464 | /** |
||
465 | * Helper function for implementations of prefixSearchSubpages() that |
||
466 | * filter the values in memory (as opposed to making a query). |
||
467 | * |
||
468 | * @since 1.24 |
||
469 | * @param string $search |
||
470 | * @param int $limit |
||
471 | * @param array $subpages |
||
472 | * @param int $offset |
||
473 | * @return string[] |
||
474 | */ |
||
475 | protected static function prefixSearchArray( $search, $limit, array $subpages, $offset ) { |
||
480 | |||
481 | /** |
||
482 | * Sets headers - this should be called from the execute() method of all derived classes! |
||
483 | */ |
||
484 | function setHeaders() { |
||
497 | |||
498 | /** |
||
499 | * Entry point. |
||
500 | * |
||
501 | * @since 1.20 |
||
502 | * |
||
503 | * @param string|null $subPage |
||
504 | */ |
||
505 | final public function run( $subPage ) { |
||
535 | |||
536 | /** |
||
537 | * Gets called before @see SpecialPage::execute. |
||
538 | * Return false to prevent calling execute() (since 1.27+). |
||
539 | * |
||
540 | * @since 1.20 |
||
541 | * |
||
542 | * @param string|null $subPage |
||
543 | * @return bool|void |
||
544 | */ |
||
545 | protected function beforeExecute( $subPage ) { |
||
548 | |||
549 | /** |
||
550 | * Gets called after @see SpecialPage::execute. |
||
551 | * |
||
552 | * @since 1.20 |
||
553 | * |
||
554 | * @param string|null $subPage |
||
555 | */ |
||
556 | protected function afterExecute( $subPage ) { |
||
559 | |||
560 | /** |
||
561 | * Default execute method |
||
562 | * Checks user permissions |
||
563 | * |
||
564 | * This must be overridden by subclasses; it will be made abstract in a future version |
||
565 | * |
||
566 | * @param string|null $subPage |
||
567 | */ |
||
568 | public function execute( $subPage ) { |
||
574 | |||
575 | /** |
||
576 | * Outputs a summary message on top of special pages |
||
577 | * Per default the message key is the canonical name of the special page |
||
578 | * May be overridden, i.e. by extensions to stick with the naming conventions |
||
579 | * for message keys: 'extensionname-xxx' |
||
580 | * |
||
581 | * @param string $summaryMessageKey Message key of the summary |
||
582 | */ |
||
583 | function outputHeader( $summaryMessageKey = '' ) { |
||
596 | |||
597 | /** |
||
598 | * Returns the name that goes in the \<h1\> in the special page itself, and |
||
599 | * also the name that will be listed in Special:Specialpages |
||
600 | * |
||
601 | * Derived classes can override this, but usually it is easier to keep the |
||
602 | * default behavior. |
||
603 | * |
||
604 | * @return string |
||
605 | */ |
||
606 | function getDescription() { |
||
609 | |||
610 | /** |
||
611 | * Get a self-referential title object |
||
612 | * |
||
613 | * @param string|bool $subpage |
||
614 | * @return Title |
||
615 | * @deprecated since 1.23, use SpecialPage::getPageTitle |
||
616 | */ |
||
617 | function getTitle( $subpage = false ) { |
||
620 | |||
621 | /** |
||
622 | * Get a self-referential title object |
||
623 | * |
||
624 | * @param string|bool $subpage |
||
625 | * @return Title |
||
626 | * @since 1.23 |
||
627 | */ |
||
628 | function getPageTitle( $subpage = false ) { |
||
631 | |||
632 | /** |
||
633 | * Sets the context this SpecialPage is executed in |
||
634 | * |
||
635 | * @param IContextSource $context |
||
636 | * @since 1.18 |
||
637 | */ |
||
638 | public function setContext( $context ) { |
||
641 | |||
642 | /** |
||
643 | * Gets the context this SpecialPage is executed in |
||
644 | * |
||
645 | * @return IContextSource|RequestContext |
||
646 | * @since 1.18 |
||
647 | */ |
||
648 | View Code Duplication | public function getContext() { |
|
658 | |||
659 | /** |
||
660 | * Get the WebRequest being used for this instance |
||
661 | * |
||
662 | * @return WebRequest |
||
663 | * @since 1.18 |
||
664 | */ |
||
665 | public function getRequest() { |
||
668 | |||
669 | /** |
||
670 | * Get the OutputPage being used for this instance |
||
671 | * |
||
672 | * @return OutputPage |
||
673 | * @since 1.18 |
||
674 | */ |
||
675 | public function getOutput() { |
||
678 | |||
679 | /** |
||
680 | * Shortcut to get the User executing this instance |
||
681 | * |
||
682 | * @return User |
||
683 | * @since 1.18 |
||
684 | */ |
||
685 | public function getUser() { |
||
688 | |||
689 | /** |
||
690 | * Shortcut to get the skin being used for this instance |
||
691 | * |
||
692 | * @return Skin |
||
693 | * @since 1.18 |
||
694 | */ |
||
695 | public function getSkin() { |
||
698 | |||
699 | /** |
||
700 | * Shortcut to get user's language |
||
701 | * |
||
702 | * @return Language |
||
703 | * @since 1.19 |
||
704 | */ |
||
705 | public function getLanguage() { |
||
708 | |||
709 | /** |
||
710 | * Shortcut to get main config object |
||
711 | * @return Config |
||
712 | * @since 1.24 |
||
713 | */ |
||
714 | public function getConfig() { |
||
717 | |||
718 | /** |
||
719 | * Return the full title, including $par |
||
720 | * |
||
721 | * @return Title |
||
722 | * @since 1.18 |
||
723 | */ |
||
724 | public function getFullTitle() { |
||
727 | |||
728 | /** |
||
729 | * Return the robot policy. Derived classes that override this can change |
||
730 | * the robot policy set by setHeaders() from the default 'noindex,nofollow'. |
||
731 | * |
||
732 | * @return string |
||
733 | * @since 1.23 |
||
734 | */ |
||
735 | protected function getRobotPolicy() { |
||
738 | |||
739 | /** |
||
740 | * Wrapper around wfMessage that sets the current context. |
||
741 | * |
||
742 | * @since 1.16 |
||
743 | * @return Message |
||
744 | * @see wfMessage |
||
745 | */ |
||
746 | public function msg( /* $args */ ) { |
||
761 | |||
762 | /** |
||
763 | * Adds RSS/atom links |
||
764 | * |
||
765 | * @param array $params |
||
766 | */ |
||
767 | protected function addFeedLinks( $params ) { |
||
776 | |||
777 | /** |
||
778 | * Adds help link with an icon via page indicators. |
||
779 | * Link target can be overridden by a local message containing a wikilink: |
||
780 | * the message key is: lowercase special page name + '-helppage'. |
||
781 | * @param string $to Target MediaWiki.org page title or encoded URL. |
||
782 | * @param bool $overrideBaseUrl Whether $url is a full URL, to avoid MW.o. |
||
783 | * @since 1.25 |
||
784 | */ |
||
785 | View Code Duplication | public function addHelpLink( $to, $overrideBaseUrl = false ) { |
|
796 | |||
797 | /** |
||
798 | * Get the group that the special page belongs in on Special:SpecialPage |
||
799 | * Use this method, instead of getGroupName to allow customization |
||
800 | * of the group name from the wiki side |
||
801 | * |
||
802 | * @return string Group of this special page |
||
803 | * @since 1.21 |
||
804 | */ |
||
805 | public function getFinalGroupName() { |
||
819 | |||
820 | /** |
||
821 | * Indicates whether this special page may perform database writes |
||
822 | * |
||
823 | * @return bool |
||
824 | * @since 1.27 |
||
825 | */ |
||
826 | public function doesWrites() { |
||
829 | |||
830 | /** |
||
831 | * Under which header this special page is listed in Special:SpecialPages |
||
832 | * See messages 'specialpages-group-*' for valid names |
||
833 | * This method defaults to group 'other' |
||
834 | * |
||
835 | * @return string |
||
836 | * @since 1.21 |
||
837 | */ |
||
838 | protected function getGroupName() { |
||
841 | |||
842 | /** |
||
843 | * Call wfTransactionalTimeLimit() if this request was POSTed |
||
844 | * @since 1.26 |
||
845 | */ |
||
846 | protected function useTransactionalTimeLimit() { |
||
851 | |||
852 | /** |
||
853 | * @since 1.28 |
||
854 | * @return \MediaWiki\Linker\LinkRenderer |
||
855 | */ |
||
856 | protected function getLinkRenderer() { |
||
863 | |||
864 | /** |
||
865 | * @since 1.28 |
||
866 | * @param \MediaWiki\Linker\LinkRenderer $linkRenderer |
||
867 | */ |
||
868 | public function setLinkRenderer( LinkRenderer $linkRenderer ) { |
||
871 | } |
||
872 |
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.