MediaWiki  1.34.0
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  if ( count( $argsDoc ) !== count( $args ) ) {
123  $badParameterCount[] = $hook . ': Doc: ' . count( $argsDoc ) . ' vs. Code: ' . count( $args );
124  } else {
125  // Check if & is equal
126  foreach ( $argsDoc as $index => $argDoc ) {
127  $arg = $args[$index];
128  if ( ( $arg[0] === '&' ) !== ( $argDoc[0] === '&' ) ) {
129  $badParameterReference[] = $hook . ': References different: Doc: ' . $argDoc .
130  ' vs. Code: ' . $arg;
131  }
132  }
133  }
134  }
135 
136  // Print the results
137  $this->printArray( 'Undocumented', $todo );
138  $this->printArray( 'Documented and not found', $deprecated );
139  $this->printArray( 'Unclear hook calls', $badHooks );
140  $this->printArray( 'Different parameter count', $badParameterCount );
141  $this->printArray( 'Different parameter reference', $badParameterReference );
142 
143  if ( !$todo && !$deprecated && !$badHooks
144  && !$badParameterCount && !$badParameterReference
145  ) {
146  $this->output( "Looks good!\n" );
147  } else {
148  $this->fatalError( 'The script finished with errors.' );
149  }
150  }
151 
157  private function getHooksFromDoc( $doc ) {
158  if ( $this->hasOption( 'online' ) ) {
159  return $this->getHooksFromOnlineDoc();
160  } else {
161  return $this->getHooksFromLocalDoc( $doc );
162  }
163  }
164 
170  private function getHooksFromLocalDoc( $doc ) {
171  $m = [];
172  $content = file_get_contents( $doc );
173  preg_match_all(
174  "/\n'(.*?)':.*((?:\n.+)*)/",
175  $content,
176  $m,
177  PREG_SET_ORDER
178  );
179 
180  // Extract the documented parameter
181  $hooks = [];
182  foreach ( $m as $match ) {
183  $args = [];
184  if ( isset( $match[2] ) ) {
185  $n = [];
186  if ( preg_match_all( "/\n(&?\\$\w+):.+/", $match[2], $n ) ) {
187  $args = $n[1];
188  }
189  }
190  $hooks[$match[1]] = $args;
191  }
192  return $hooks;
193  }
194 
199  private function getHooksFromOnlineDoc() {
200  $allhooks = $this->getHooksFromOnlineDocCategory( 'MediaWiki_hooks' );
201  $removed = $this->getHooksFromOnlineDocCategory( 'Removed_hooks' );
202  return array_diff_key( $allhooks, $removed );
203  }
204 
209  private function getHooksFromOnlineDocCategory( $title ) {
210  $params = [
211  'action' => 'query',
212  'list' => 'categorymembers',
213  'cmtitle' => "Category:$title",
214  'cmlimit' => 500,
215  'format' => 'json',
216  'continue' => '',
217  ];
218 
219  $retval = [];
220  while ( true ) {
221  $json = MediaWikiServices::getInstance()->getHttpRequestFactory()->get(
222  wfAppendQuery( 'https://www.mediawiki.org/w/api.php', $params ),
223  [],
224  __METHOD__
225  );
226  $data = FormatJson::decode( $json, true );
227  foreach ( $data['query']['categorymembers'] as $page ) {
228  if ( preg_match( '/Manual\:Hooks\/([a-zA-Z0-9- :]+)/', $page['title'], $m ) ) {
229  // parameters are unknown, because that needs parsing of wikitext
230  $retval[str_replace( ' ', '_', $m[1] )] = 'unknown';
231  }
232  }
233  if ( !isset( $data['continue'] ) ) {
234  return $retval;
235  }
236  $params = array_replace( $params, $data['continue'] );
237  }
238  }
239 
245  private function getHooksFromFile( $filePath ) {
246  $content = file_get_contents( $filePath );
247  $m = [];
248  preg_match_all(
249  // All functions which runs hooks
250  '/(?:Hooks\:\:run|Hooks\:\:runWithoutAbort)\s*\(\s*' .
251  // First argument is the hook name as string
252  '([\'"])(.*?)\1' .
253  // Comma for second argument
254  '(?:\s*(,))?' .
255  // Second argument must start with array to be processed
256  '(?:\s*(?:array\s*\(|\[)' .
257  // Matching inside array - allows one deep of brackets
258  '((?:[^\(\)\[\]]|\((?-1)\)|\[(?-1)\])*)' .
259  // End
260  '[\)\]])?/',
261  $content,
262  $m,
263  PREG_SET_ORDER
264  );
265 
266  // Extract parameter
267  $hooks = [];
268  foreach ( $m as $match ) {
269  $args = [];
270  if ( isset( $match[4] ) ) {
271  $n = [];
272  if ( preg_match_all( '/((?:[^,\(\)]|\([^\(\)]*\))+)/', $match[4], $n ) ) {
273  $args = array_map( 'trim', $n[1] );
274  // remove empty entries from trailing spaces
275  $args = array_filter( $args );
276  }
277  } elseif ( isset( $match[3] ) ) {
278  // Found a parameter for Hooks::run,
279  // but could not extract the hooks argument,
280  // because there are given by a variable
281  $args = 'unknown';
282  }
283  $hooks[$match[2]] = $args;
284  }
285 
286  return $hooks;
287  }
288 
294  private function getBadHooksFromFile( $filePath ) {
295  $content = file_get_contents( $filePath );
296  $m = [];
297  preg_match_all( '/(?:Hooks\:\:run|Hooks\:\:runWithoutAbort)\(\s*[^\s\'"].*/', $content, $m );
298  $list = [];
299  foreach ( $m[0] as $match ) {
300  $list[] = $match . "(" . $filePath . ")";
301  }
302 
303  return $list;
304  }
305 
312  private function getHooksFromDir( $dir, $recurse = 0 ) {
313  $good = [];
314  $bad = [];
315 
316  if ( $recurse === self::FIND_RECURSIVE ) {
317  $iterator = new RecursiveIteratorIterator(
318  new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS ),
319  RecursiveIteratorIterator::SELF_FIRST
320  );
321  } else {
322  $iterator = new DirectoryIterator( $dir );
323  }
324 
326  foreach ( $iterator as $info ) {
327  // Ignore directories, work only on php files,
328  if ( $info->isFile() && in_array( $info->getExtension(), [ 'php', 'inc' ] )
329  // Skip this file as it contains text that looks like a bad wfRunHooks() call
330  && $info->getRealPath() !== __FILE__
331  ) {
332  $good = array_merge( $good, $this->getHooksFromFile( $info->getRealPath() ) );
333  $bad = array_merge( $bad, $this->getBadHooksFromFile( $info->getRealPath() ) );
334  }
335  }
336 
337  return [ 'good' => $good, 'bad' => $bad ];
338  }
339 
345  private function printArray( $msg, $arr ) {
346  asort( $arr );
347 
348  foreach ( $arr as $v ) {
349  $this->output( "$msg: $v\n" );
350  }
351  }
352 }
353 
354 $maintClass = FindHooks::class;
355 require_once RUN_MAINTENANCE_IF_MAIN;
RUN_MAINTENANCE_IF_MAIN
const RUN_MAINTENANCE_IF_MAIN
Definition: Maintenance.php:39
FindHooks\getHooksFromFile
getHooksFromFile( $filePath)
Get hooks from a PHP file.
Definition: findHooks.php:245
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:117
FindHooks\getHooksFromOnlineDocCategory
getHooksFromOnlineDocCategory( $title)
Definition: findHooks.php:209
Maintenance\fatalError
fatalError( $msg, $exitCode=1)
Output a message and terminate the current script.
Definition: Maintenance.php:504
Maintenance\addDescription
addDescription( $text)
Set the description text.
Definition: Maintenance.php:348
FindHooks\getHooksFromDoc
getHooksFromDoc( $doc)
Get the hook documentation, either locally or from MediaWiki.org.
Definition: findHooks.php:157
$file
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
FindHooks
Maintenance script that compares documented and actually present mismatches.
Definition: findHooks.php:46
FindHooks\FIND_RECURSIVE
const FIND_RECURSIVE
Definition: findHooks.php:48
Maintenance
Abstract maintenance class for quickly writing and churning out maintenance scripts with minimal effo...
Definition: Maintenance.php:82
FindHooks\getHooksFromOnlineDoc
getHooksFromOnlineDoc()
Get hooks from www.mediawiki.org using the API.
Definition: findHooks.php:199
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:439
FindHooks\getHooksFromDir
getHooksFromDir( $dir, $recurse=0)
Get hooks from a directory of PHP files.
Definition: findHooks.php:312
FindHooks\FIND_NON_RECURSIVE
const FIND_NON_RECURSIVE
Definition: findHooks.php:47
FormatJson\decode
static decode( $value, $assoc=false)
Decodes a JSON string.
Definition: FormatJson.php:174
FindHooks\__construct
__construct()
Default constructor.
Definition: findHooks.php:55
FindHooks\getHooksFromLocalDoc
getHooksFromLocalDoc( $doc)
Get hooks from a local file (for example docs/hooks.txt)
Definition: findHooks.php:170
FindHooks\$ignore
static $ignore
Definition: findHooks.php:53
Maintenance\addOption
addOption( $name, $description, $required=false, $withArg=false, $shortName=false, $multiOccurrence=false)
Add a parameter to the script.
Definition: Maintenance.php:267
$IP
$IP
Definition: update.php:3
$title
$title
Definition: testCompression.php:34
$content
$content
Definition: router.php:78
Maintenance\DB_NONE
const DB_NONE
Constants for DB access type.
Definition: Maintenance.php:87
$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:294
FindHooks\printArray
printArray( $msg, $arr)
Nicely sort an print an array.
Definition: findHooks.php:345
$maintClass
$maintClass
Definition: findHooks.php:354
Maintenance\output
output( $out, $channel=null)
Throw some output to the user.
Definition: Maintenance.php:453
Maintenance\hasOption
hasOption( $name)
Checks to see if a particular option exists.
Definition: Maintenance.php:288
FindHooks\getDbType
getDbType()
Does the script need different DB access? By default, we give Maintenance scripts normal rights to th...
Definition: findHooks.php:61
FindHooks\execute
execute()
Do the actual work.
Definition: findHooks.php:65