MediaWiki  master
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  '/(?: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  preg_match_all( '/(?:Hooks\:\:run|Hooks\:\:runWithoutAbort)\(\s*[^\s\'"].*/', $content, $m );
296  $list = [];
297  foreach ( $m[0] as $match ) {
298  $list[] = $match . "(" . $filePath . ")";
299  }
300 
301  return $list;
302  }
303 
310  private function getHooksFromDir( $dir, $recurse = 0 ) {
311  $good = [];
312  $bad = [];
313 
314  if ( $recurse === self::FIND_RECURSIVE ) {
315  $iterator = new RecursiveIteratorIterator(
316  new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS ),
317  RecursiveIteratorIterator::SELF_FIRST
318  );
319  } else {
320  $iterator = new DirectoryIterator( $dir );
321  }
322 
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;
const DB_NONE
Constants for DB access type.
Definition: Maintenance.php:84
$data
Utility to generate mapping file used in mw.Title (phpCharToUpper.json)
$IP
Definition: WebStart.php:41
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:1982
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Definition: router.php:42
Abstract maintenance class for quickly writing and churning out maintenance scripts with minimal effo...
Definition: maintenance.txt:39
Maintenance script that compares documented and actually present mismatches.
Definition: findHooks.php:44
getHooksFromLocalDoc( $doc)
Get hooks from a local file (for example docs/hooks.txt)
Definition: findHooks.php:168
const FIND_RECURSIVE
Definition: findHooks.php:46
hasOption( $name)
Checks to see if a particular option exists.
require_once RUN_MAINTENANCE_IF_MAIN
Definition: maintenance.txt:50
if( $line===false) $args
Definition: cdb.php:64
const FIND_NON_RECURSIVE
Definition: findHooks.php:45
wfAppendQuery( $url, $query)
Append a query string to an existing URL, which may or may not already have query string parameters a...
getBadHooksFromFile( $filePath)
Get bad hooks (where the hook name could not be determined) from a PHP file.
Definition: findHooks.php:292
static decode( $value, $assoc=false)
Decodes a JSON string.
Definition: FormatJson.php:174
addDescription( $text)
Set the description text.
$params
getHooksFromDir( $dir, $recurse=0)
Get hooks from a directory of PHP files.
Definition: findHooks.php:310
__construct()
Definition: findHooks.php:53
namespace and then decline to actually register it file or subcat img or subcat $title
Definition: hooks.txt:925
getHooksFromDoc( $doc)
Get the hook documentation, either locally or from MediaWiki.org.
Definition: findHooks.php:155
output( $out, $channel=null)
Throw some output to the user.
getHooksFromOnlineDoc()
Get hooks from www.mediawiki.org using the API.
Definition: findHooks.php:197
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:9
getHooksFromOnlineDocCategory( $title)
Definition: findHooks.php:207
printArray( $msg, $arr)
Nicely sort an print an array.
Definition: findHooks.php:343
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:35
static $ignore
Definition: findHooks.php:51
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:52
getHooksFromFile( $filePath)
Get hooks from a PHP file.
Definition: findHooks.php:243
$maintClass
Definition: findHooks.php:352
addOption( $name, $description, $required=false, $withArg=false, $shortName=false, $multiOccurrence=false)
Add a parameter to the script.
fatalError( $msg, $exitCode=1)
Output a message and terminate the current script.
$content
Definition: pageupdater.txt:72
static get( $url, array $options=[], $caller=__METHOD__)
Simple wrapper for Http::request( &#39;GET&#39; )
Definition: Http.php:98