MediaWiki  master
findHooks.php
Go to the documentation of this file.
1 <?php
38 
39 require_once __DIR__ . '/Maintenance.php';
40 
46 class FindHooks extends Maintenance {
47  const FIND_NON_RECURSIVE = 0;
48  const FIND_RECURSIVE = 1;
49 
50  /*
51  * Hooks that are ignored
52  */
53  protected static $ignore = [ 'Test' ];
54 
55  public function __construct() {
56  parent::__construct();
57  $this->addDescription( 'Find hooks that are undocumented, missing, or just plain wrong' );
58  $this->addOption( 'online', 'Check against MediaWiki.org hook documentation' );
59  }
60 
61  public function getDbType() {
62  return Maintenance::DB_NONE;
63  }
64 
65  public function execute() {
66  global $IP;
67 
68  $documentedHooks = $this->getHooksFromDoc( $IP . '/docs/hooks.txt' );
69  $potentialHooks = [];
70  $badHooks = [];
71 
72  $recurseDirs = [
73  "$IP/includes/",
74  "$IP/mw-config/",
75  "$IP/languages/",
76  "$IP/maintenance/",
77  // Omit $IP/tests/phpunit as it contains hook tests that shouldn't be documented
78  "$IP/tests/parser",
79  "$IP/tests/phpunit/suites",
80  ];
81  $nonRecurseDirs = [
82  "$IP/",
83  ];
84  $extraFiles = [
85  "$IP/tests/phpunit/MediaWikiIntegrationTestCase.php",
86  ];
87 
88  foreach ( $recurseDirs as $dir ) {
89  $ret = $this->getHooksFromDir( $dir, self::FIND_RECURSIVE );
90  $potentialHooks = array_merge( $potentialHooks, $ret['good'] );
91  $badHooks = array_merge( $badHooks, $ret['bad'] );
92  }
93  foreach ( $nonRecurseDirs as $dir ) {
94  $ret = $this->getHooksFromDir( $dir );
95  $potentialHooks = array_merge( $potentialHooks, $ret['good'] );
96  $badHooks = array_merge( $badHooks, $ret['bad'] );
97  }
98  foreach ( $extraFiles as $file ) {
99  $potentialHooks = array_merge( $potentialHooks, $this->getHooksFromFile( $file ) );
100  $badHooks = array_merge( $badHooks, $this->getBadHooksFromFile( $file ) );
101  }
102 
103  $documented = array_keys( $documentedHooks );
104  $potential = array_keys( $potentialHooks );
105  $potential = array_unique( $potential );
106  $badHooks = array_diff( array_unique( $badHooks ), self::$ignore );
107  $todo = array_diff( $potential, $documented, self::$ignore );
108  $deprecated = array_diff( $documented, $potential, self::$ignore );
109 
110  // Check parameter count and references
111  $badParameterCount = $badParameterReference = [];
112  foreach ( $potentialHooks as $hook => $args ) {
113  if ( !isset( $documentedHooks[$hook] ) ) {
114  // Not documented, but that will also be in $todo
115  continue;
116  }
117  $argsDoc = $documentedHooks[$hook];
118  if ( $args === 'unknown' || $argsDoc === 'unknown' ) {
119  // Could not get parameter information
120  continue;
121  }
122  '@phan-var array $args';
123  if ( count( $argsDoc ) !== count( $args ) ) {
124  $badParameterCount[] = $hook . ': Doc: ' . count( $argsDoc ) . ' vs. Code: ' . count( $args );
125  } else {
126  // Check if & is equal
127  foreach ( $argsDoc as $index => $argDoc ) {
128  $arg = $args[$index];
129  if ( ( $arg[0] === '&' ) !== ( $argDoc[0] === '&' ) ) {
130  $badParameterReference[] = $hook . ': References different: Doc: ' . $argDoc .
131  ' vs. Code: ' . $arg;
132  }
133  }
134  }
135  }
136 
137  // Print the results
138  $this->printArray( 'Undocumented', $todo );
139  $this->printArray( 'Documented and not found', $deprecated );
140  $this->printArray( 'Unclear hook calls', $badHooks );
141  $this->printArray( 'Different parameter count', $badParameterCount );
142  $this->printArray( 'Different parameter reference', $badParameterReference );
143 
144  if ( !$todo && !$deprecated && !$badHooks
145  && !$badParameterCount && !$badParameterReference
146  ) {
147  $this->output( "Looks good!\n" );
148  } else {
149  $this->fatalError( 'The script finished with errors.' );
150  }
151  }
152 
158  private function getHooksFromDoc( $doc ) {
159  if ( $this->hasOption( 'online' ) ) {
160  return $this->getHooksFromOnlineDoc();
161  } else {
162  return $this->getHooksFromLocalDoc( $doc );
163  }
164  }
165 
171  private function getHooksFromLocalDoc( $doc ) {
172  $m = [];
173  $content = file_get_contents( $doc );
174  preg_match_all(
175  "/\n'(.*?)':.*((?:\n.+)*)/",
176  $content,
177  $m,
178  PREG_SET_ORDER
179  );
180 
181  // Extract the documented parameter
182  $hooks = [];
183  foreach ( $m as $match ) {
184  $args = [];
185  if ( isset( $match[2] ) ) {
186  $n = [];
187  if ( preg_match_all( "/\n(&?\\$\w+):.+/", $match[2], $n ) ) {
188  $args = $n[1];
189  }
190  }
191  $hooks[$match[1]] = $args;
192  }
193  return $hooks;
194  }
195 
200  private function getHooksFromOnlineDoc() {
201  $allhooks = $this->getHooksFromOnlineDocCategory( 'MediaWiki_hooks' );
202  $removed = $this->getHooksFromOnlineDocCategory( 'Removed_hooks' );
203  return array_diff_key( $allhooks, $removed );
204  }
205 
210  private function getHooksFromOnlineDocCategory( $title ) {
211  $params = [
212  'action' => 'query',
213  'list' => 'categorymembers',
214  'cmtitle' => "Category:$title",
215  'cmlimit' => 500,
216  'format' => 'json',
217  'continue' => '',
218  ];
219 
220  $retval = [];
221  while ( true ) {
222  $json = MediaWikiServices::getInstance()->getHttpRequestFactory()->get(
223  wfAppendQuery( 'https://www.mediawiki.org/w/api.php', $params ),
224  [],
225  __METHOD__
226  );
227  $data = FormatJson::decode( $json, true );
228  foreach ( $data['query']['categorymembers'] as $page ) {
229  if ( preg_match( '/Manual\:Hooks\/([a-zA-Z0-9- :]+)/', $page['title'], $m ) ) {
230  // parameters are unknown, because that needs parsing of wikitext
231  $retval[str_replace( ' ', '_', $m[1] )] = 'unknown';
232  }
233  }
234  if ( !isset( $data['continue'] ) ) {
235  return $retval;
236  }
237  $params = array_replace( $params, $data['continue'] );
238  }
239  }
240 
246  private function getHooksFromFile( $filePath ) {
247  $content = file_get_contents( $filePath );
248  $m = [];
249  preg_match_all(
250  // All functions which runs hooks
251  '/(?:Hooks\:\:run|Hooks\:\:runWithoutAbort)\s*\(\s*' .
252  // First argument is the hook name as string
253  '([\'"])(.*?)\1' .
254  // Comma for second argument
255  '(?:\s*(,))?' .
256  // Second argument must start with array to be processed
257  '(?:\s*(?:array\s*\(|\[)' .
258  // Matching inside array - allows one deep of brackets
259  '((?:[^\(\)\[\]]|\((?-1)\)|\[(?-1)\])*)' .
260  // End
261  '[\)\]])?/',
262  $content,
263  $m,
264  PREG_SET_ORDER
265  );
266 
267  // Extract parameter
268  $hooks = [];
269  foreach ( $m as $match ) {
270  $args = [];
271  if ( isset( $match[4] ) ) {
272  $n = [];
273  if ( preg_match_all( '/((?:[^,\(\)]|\([^\(\)]*\))+)/', $match[4], $n ) ) {
274  $args = array_map( 'trim', $n[1] );
275  // remove empty entries from trailing spaces
276  $args = array_filter( $args );
277  }
278  } elseif ( isset( $match[3] ) ) {
279  // Found a parameter for Hooks::run,
280  // but could not extract the hooks argument,
281  // because there are given by a variable
282  $args = 'unknown';
283  }
284  $hooks[$match[2]] = $args;
285  }
286 
287  return $hooks;
288  }
289 
295  private function getBadHooksFromFile( $filePath ) {
296  $content = file_get_contents( $filePath );
297  $m = [];
298  preg_match_all( '/(?:Hooks\:\:run|Hooks\:\:runWithoutAbort)\(\s*[^\s\'"].*/', $content, $m );
299  $list = [];
300  foreach ( $m[0] as $match ) {
301  $list[] = $match . "(" . $filePath . ")";
302  }
303 
304  return $list;
305  }
306 
313  private function getHooksFromDir( $dir, $recurse = 0 ) {
314  $good = [];
315  $bad = [];
316 
317  if ( $recurse === self::FIND_RECURSIVE ) {
318  $iterator = new RecursiveIteratorIterator(
319  new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS ),
320  RecursiveIteratorIterator::SELF_FIRST
321  );
322  } else {
323  $iterator = new DirectoryIterator( $dir );
324  }
325 
327  foreach ( $iterator as $info ) {
328  // Ignore directories, work only on php files,
329  if ( $info->isFile() && in_array( $info->getExtension(), [ 'php', 'inc' ] )
330  // Skip this file as it contains text that looks like a bad wfRunHooks() call
331  && $info->getRealPath() !== __FILE__
332  ) {
333  $good = array_merge( $good, $this->getHooksFromFile( $info->getRealPath() ) );
334  $bad = array_merge( $bad, $this->getBadHooksFromFile( $info->getRealPath() ) );
335  }
336  }
337 
338  return [ 'good' => $good, 'bad' => $bad ];
339  }
340 
346  private function printArray( $msg, $arr ) {
347  asort( $arr );
348 
349  foreach ( $arr as $v ) {
350  $this->output( "$msg: $v\n" );
351  }
352  }
353 }
354 
355 $maintClass = FindHooks::class;
356 require_once RUN_MAINTENANCE_IF_MAIN;
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
const DB_NONE
Constants for DB access type.
Definition: Maintenance.php:91
const RUN_MAINTENANCE_IF_MAIN
Definition: Maintenance.php:39
$IP
Definition: WebStart.php:41
Abstract maintenance class for quickly writing and churning out maintenance scripts with minimal effo...
Definition: Maintenance.php:86
Maintenance script that compares documented and actually present mismatches.
Definition: findHooks.php:46
getHooksFromLocalDoc( $doc)
Get hooks from a local file (for example docs/hooks.txt)
Definition: findHooks.php:171
const FIND_RECURSIVE
Definition: findHooks.php:48
hasOption( $name)
Checks to see if a particular option exists.
const FIND_NON_RECURSIVE
Definition: findHooks.php:47
wfAppendQuery( $url, $query)
Append a query string to an existing URL, which may or may not already have query string parameters a...
if( $line===false) $args
Definition: mcc.php:124
getBadHooksFromFile( $filePath)
Get bad hooks (where the hook name could not be determined) from a PHP file.
Definition: findHooks.php:295
static decode( $value, $assoc=false)
Decodes a JSON string.
Definition: FormatJson.php:174
addDescription( $text)
Set the description text.
getHooksFromDir( $dir, $recurse=0)
Get hooks from a directory of PHP files.
Definition: findHooks.php:313
__construct()
Definition: findHooks.php:55
getHooksFromDoc( $doc)
Get the hook documentation, either locally or from MediaWiki.org.
Definition: findHooks.php:158
output( $out, $channel=null)
Throw some output to the user.
getHooksFromOnlineDoc()
Get hooks from www.mediawiki.org using the API.
Definition: findHooks.php:200
getHooksFromOnlineDocCategory( $title)
Definition: findHooks.php:210
printArray( $msg, $arr)
Nicely sort an print an array.
Definition: findHooks.php:346
static $ignore
Definition: findHooks.php:53
getHooksFromFile( $filePath)
Get hooks from a PHP file.
Definition: findHooks.php:246
$maintClass
Definition: findHooks.php:355
addOption( $name, $description, $required=false, $withArg=false, $shortName=false, $multiOccurrence=false)
Add a parameter to the script.
$content
Definition: router.php:78
fatalError( $msg, $exitCode=1)
Output a message and terminate the current script.