MediaWiki  REL1_31
findHooks.php
Go to the documentation of this file.
1 <?php
37 require_once __DIR__ . '/Maintenance.php';
38 
44 class FindHooks extends Maintenance {
45  const FIND_NON_RECURSIVE = 0;
46  const FIND_RECURSIVE = 1;
47 
48  /*
49  * Hooks that are ignored
50  */
51  protected static $ignore = [ 'Test' ];
52 
53  public function __construct() {
54  parent::__construct();
55  $this->addDescription( 'Find hooks that are undocumented, missing, or just plain wrong' );
56  $this->addOption( 'online', 'Check against MediaWiki.org hook documentation' );
57  }
58 
59  public function getDbType() {
60  return Maintenance::DB_NONE;
61  }
62 
63  public function execute() {
64  global $IP;
65 
66  $documentedHooks = $this->getHooksFromDoc( $IP . '/docs/hooks.txt' );
67  $potentialHooks = [];
68  $badHooks = [];
69 
70  $recurseDirs = [
71  "$IP/includes/",
72  "$IP/mw-config/",
73  "$IP/languages/",
74  "$IP/maintenance/",
75  // Omit $IP/tests/phpunit as it contains hook tests that shouldn't be documented
76  "$IP/tests/parser",
77  "$IP/tests/phpunit/suites",
78  ];
79  $nonRecurseDirs = [
80  "$IP/",
81  ];
82  $extraFiles = [
83  "$IP/tests/phpunit/MediaWikiTestCase.php",
84  ];
85 
86  foreach ( $recurseDirs as $dir ) {
87  $ret = $this->getHooksFromDir( $dir, self::FIND_RECURSIVE );
88  $potentialHooks = array_merge( $potentialHooks, $ret['good'] );
89  $badHooks = array_merge( $badHooks, $ret['bad'] );
90  }
91  foreach ( $nonRecurseDirs as $dir ) {
92  $ret = $this->getHooksFromDir( $dir );
93  $potentialHooks = array_merge( $potentialHooks, $ret['good'] );
94  $badHooks = array_merge( $badHooks, $ret['bad'] );
95  }
96  foreach ( $extraFiles as $file ) {
97  $potentialHooks = array_merge( $potentialHooks, $this->getHooksFromFile( $file ) );
98  $badHooks = array_merge( $badHooks, $this->getBadHooksFromFile( $file ) );
99  }
100 
101  $documented = array_keys( $documentedHooks );
102  $potential = array_keys( $potentialHooks );
103  $potential = array_unique( $potential );
104  $badHooks = array_diff( array_unique( $badHooks ), self::$ignore );
105  $todo = array_diff( $potential, $documented, self::$ignore );
106  $deprecated = array_diff( $documented, $potential, self::$ignore );
107 
108  // Check parameter count and references
109  $badParameterCount = $badParameterReference = [];
110  foreach ( $potentialHooks as $hook => $args ) {
111  if ( !isset( $documentedHooks[$hook] ) ) {
112  // Not documented, but that will also be in $todo
113  continue;
114  }
115  $argsDoc = $documentedHooks[$hook];
116  if ( $args === 'unknown' || $argsDoc === 'unknown' ) {
117  // Could not get parameter information
118  continue;
119  }
120  if ( count( $argsDoc ) !== count( $args ) ) {
121  $badParameterCount[] = $hook . ': Doc: ' . count( $argsDoc ) . ' vs. Code: ' . count( $args );
122  } else {
123  // Check if & is equal
124  foreach ( $argsDoc as $index => $argDoc ) {
125  $arg = $args[$index];
126  if ( ( $arg[0] === '&' ) !== ( $argDoc[0] === '&' ) ) {
127  $badParameterReference[] = $hook . ': References different: Doc: ' . $argDoc .
128  ' vs. Code: ' . $arg;
129  }
130  }
131  }
132  }
133 
134  // Print the results
135  $this->printArray( 'Undocumented', $todo );
136  $this->printArray( 'Documented and not found', $deprecated );
137  $this->printArray( 'Unclear hook calls', $badHooks );
138  $this->printArray( 'Different parameter count', $badParameterCount );
139  $this->printArray( 'Different parameter reference', $badParameterReference );
140 
141  if ( !$todo && !$deprecated && !$badHooks
142  && !$badParameterCount && !$badParameterReference
143  ) {
144  $this->output( "Looks good!\n" );
145  } else {
146  $this->fatalError( 'The script finished with errors.' );
147  }
148  }
149 
155  private function getHooksFromDoc( $doc ) {
156  if ( $this->hasOption( 'online' ) ) {
157  return $this->getHooksFromOnlineDoc();
158  } else {
159  return $this->getHooksFromLocalDoc( $doc );
160  }
161  }
162 
168  private function getHooksFromLocalDoc( $doc ) {
169  $m = [];
170  $content = file_get_contents( $doc );
171  preg_match_all(
172  "/\n'(.*?)':.*((?:\n.+)*)/",
173  $content,
174  $m,
175  PREG_SET_ORDER
176  );
177 
178  // Extract the documented parameter
179  $hooks = [];
180  foreach ( $m as $match ) {
181  $args = [];
182  if ( isset( $match[2] ) ) {
183  $n = [];
184  if ( preg_match_all( "/\n(&?\\$\w+):.+/", $match[2], $n ) ) {
185  $args = $n[1];
186  }
187  }
188  $hooks[$match[1]] = $args;
189  }
190  return $hooks;
191  }
192 
197  private function getHooksFromOnlineDoc() {
198  $allhooks = $this->getHooksFromOnlineDocCategory( 'MediaWiki_hooks' );
199  $removed = $this->getHooksFromOnlineDocCategory( 'Removed_hooks' );
200  return array_diff_key( $allhooks, $removed );
201  }
202 
207  private function getHooksFromOnlineDocCategory( $title ) {
208  $params = [
209  'action' => 'query',
210  'list' => 'categorymembers',
211  'cmtitle' => "Category:$title",
212  'cmlimit' => 500,
213  'format' => 'json',
214  'continue' => '',
215  ];
216 
217  $retval = [];
218  while ( true ) {
219  $json = Http::get(
220  wfAppendQuery( 'https://www.mediawiki.org/w/api.php', $params ),
221  [],
222  __METHOD__
223  );
224  $data = FormatJson::decode( $json, true );
225  foreach ( $data['query']['categorymembers'] as $page ) {
226  if ( preg_match( '/Manual\:Hooks\/([a-zA-Z0-9- :]+)/', $page['title'], $m ) ) {
227  // parameters are unknown, because that needs parsing of wikitext
228  $retval[str_replace( ' ', '_', $m[1] )] = 'unknown';
229  }
230  }
231  if ( !isset( $data['continue'] ) ) {
232  return $retval;
233  }
234  $params = array_replace( $params, $data['continue'] );
235  }
236  }
237 
243  private function getHooksFromFile( $filePath ) {
244  $content = file_get_contents( $filePath );
245  $m = [];
246  preg_match_all(
247  // All functions which runs hooks
248  '/(?:wfRunHooks|Hooks\:\:run|Hooks\:\:runWithoutAbort)\s*\‍(\s*' .
249  // First argument is the hook name as string
250  '([\'"])(.*?)\1' .
251  // Comma for second argument
252  '(?:\s*(,))?' .
253  // Second argument must start with array to be processed
254  '(?:\s*(?:array\s*\‍(|\[)' .
255  // Matching inside array - allows one deep of brackets
256  '((?:[^\‍(\‍)\[\]]|\‍((?-1)\‍)|\[(?-1)\])*)' .
257  // End
258  '[\‍)\]])?/',
259  $content,
260  $m,
261  PREG_SET_ORDER
262  );
263 
264  // Extract parameter
265  $hooks = [];
266  foreach ( $m as $match ) {
267  $args = [];
268  if ( isset( $match[4] ) ) {
269  $n = [];
270  if ( preg_match_all( '/((?:[^,\‍(\‍)]|\‍([^\‍(\‍)]*\‍))+)/', $match[4], $n ) ) {
271  $args = array_map( 'trim', $n[1] );
272  // remove empty entries from trailing spaces
273  $args = array_filter( $args );
274  }
275  } elseif ( isset( $match[3] ) ) {
276  // Found a parameter for Hooks::run,
277  // but could not extract the hooks argument,
278  // because there are given by a variable
279  $args = 'unknown';
280  }
281  $hooks[$match[2]] = $args;
282  }
283 
284  return $hooks;
285  }
286 
292  private function getBadHooksFromFile( $filePath ) {
293  $content = file_get_contents( $filePath );
294  $m = [];
295  // We want to skip the "function wfRunHooks()" one. :)
296  preg_match_all( '/(?<!function )wfRunHooks\‍(\s*[^\s\'"].*/', $content, $m );
297  $list = [];
298  foreach ( $m[0] as $match ) {
299  $list[] = $match . "(" . $filePath . ")";
300  }
301 
302  return $list;
303  }
304 
311  private function getHooksFromDir( $dir, $recurse = 0 ) {
312  $good = [];
313  $bad = [];
314 
315  if ( $recurse === self::FIND_RECURSIVE ) {
316  $iterator = new RecursiveIteratorIterator(
317  new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS ),
318  RecursiveIteratorIterator::SELF_FIRST
319  );
320  } else {
321  $iterator = new DirectoryIterator( $dir );
322  }
323 
324  foreach ( $iterator as $info ) {
325  // Ignore directories, work only on php files,
326  if ( $info->isFile() && in_array( $info->getExtension(), [ 'php', 'inc' ] )
327  // Skip this file as it contains text that looks like a bad wfRunHooks() call
328  && $info->getRealPath() !== __FILE__
329  ) {
330  $good = array_merge( $good, $this->getHooksFromFile( $info->getRealPath() ) );
331  $bad = array_merge( $bad, $this->getBadHooksFromFile( $info->getRealPath() ) );
332  }
333  }
334 
335  return [ 'good' => $good, 'bad' => $bad ];
336  }
337 
343  private function printArray( $msg, $arr ) {
344  asort( $arr );
345 
346  foreach ( $arr as $v ) {
347  $this->output( "$msg: $v\n" );
348  }
349  }
350 }
351 
353 require_once RUN_MAINTENANCE_IF_MAIN;
FindHooks\getHooksFromFile
getHooksFromFile( $filePath)
Get hooks from a PHP file.
Definition: findHooks.php:243
FindHooks\getHooksFromOnlineDocCategory
getHooksFromOnlineDocCategory( $title)
Definition: findHooks.php:207
Maintenance\fatalError
fatalError( $msg, $exitCode=1)
Output a message and terminate the current script.
Definition: Maintenance.php:439
Maintenance\addDescription
addDescription( $text)
Set the description text.
Definition: Maintenance.php:291
$ret
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses & $ret
Definition: hooks.txt:2005
FindHooks\getHooksFromDoc
getHooksFromDoc( $doc)
Get the hook documentation, either locally or from MediaWiki.org.
Definition: findHooks.php:155
RUN_MAINTENANCE_IF_MAIN
require_once RUN_MAINTENANCE_IF_MAIN
Definition: maintenance.txt:50
$params
$params
Definition: styleTest.css.php:40
FindHooks
Maintenance script that compares documented and actually present mismatches.
Definition: findHooks.php:44
FindHooks\FIND_RECURSIVE
const FIND_RECURSIVE
Definition: findHooks.php:46
Maintenance
Abstract maintenance class for quickly writing and churning out maintenance scripts with minimal effo...
Definition: maintenance.txt:39
FindHooks\getHooksFromOnlineDoc
getHooksFromOnlineDoc()
Get hooks from www.mediawiki.org using the API.
Definition: findHooks.php:197
php
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:37
wfAppendQuery
wfAppendQuery( $url, $query)
Append a query string to an existing URL, which may or may not already have query string parameters a...
Definition: GlobalFunctions.php:469
FindHooks\getHooksFromDir
getHooksFromDir( $dir, $recurse=0)
Get hooks from a directory of PHP files.
Definition: findHooks.php:311
FindHooks\FIND_NON_RECURSIVE
const FIND_NON_RECURSIVE
Definition: findHooks.php:45
FormatJson\decode
static decode( $value, $assoc=false)
Decodes a JSON string.
Definition: FormatJson.php:187
FindHooks\__construct
__construct()
Default constructor.
Definition: findHooks.php:53
FindHooks\getHooksFromLocalDoc
getHooksFromLocalDoc( $doc)
Get hooks from a local file (for example docs/hooks.txt)
Definition: findHooks.php:168
FindHooks\$ignore
static $ignore
Definition: findHooks.php:51
Maintenance\addOption
addOption( $name, $description, $required=false, $withArg=false, $shortName=false, $multiOccurrence=false)
Add a parameter to the script.
Definition: Maintenance.php:219
$title
namespace and then decline to actually register it file or subcat img or subcat $title
Definition: hooks.txt:964
global
when a variable name is used in a it is silently declared as a new masking the global
Definition: design.txt:95
Http\get
static get( $url, $options=[], $caller=__METHOD__)
Simple wrapper for Http::request( 'GET' )
Definition: Http.php:98
Maintenance\DB_NONE
const DB_NONE
Constants for DB access type.
Definition: Maintenance.php:66
$args
if( $line===false) $args
Definition: cdb.php:64
FindHooks\getBadHooksFromFile
getBadHooksFromFile( $filePath)
Get bad hooks (where the hook name could not be determined) from a PHP file.
Definition: findHooks.php:292
as
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:22
FindHooks\printArray
printArray( $msg, $arr)
Nicely sort an print an array.
Definition: findHooks.php:343
$maintClass
$maintClass
Definition: findHooks.php:352
Maintenance\output
output( $out, $channel=null)
Throw some output to the user.
Definition: Maintenance.php:388
class
you have access to all of the normal MediaWiki so you can get a DB use the etc For full docs on the Maintenance class
Definition: maintenance.txt:56
Maintenance\hasOption
hasOption( $name)
Checks to see if a particular param exists.
Definition: Maintenance.php:240
FindHooks\getDbType
getDbType()
Does the script need different DB access? By default, we give Maintenance scripts normal rights to th...
Definition: findHooks.php:59
$IP
$IP
Definition: WebStart.php:52
$retval
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a account incomplete not yet checked for validity & $retval
Definition: hooks.txt:266
FindHooks\execute
execute()
Do the actual work.
Definition: findHooks.php:63