TYPO3CMS  8
 All Classes Namespaces Files Functions Variables Pages
CleanerCommand.php
Go to the documentation of this file.
1 <?php
2 namespace TYPO3\CMS\Lowlevel;
3 
4 /*
5  * This file is part of the TYPO3 CMS project.
6  *
7  * It is free software; you can redistribute it and/or modify it under
8  * the terms of the GNU General Public License, either version 2
9  * of the License, or any later version.
10  *
11  * For the full copyright and license information, please read the
12  * LICENSE.txt file that was distributed with this source code.
13  *
14  * The TYPO3 project - inspiring people to share!
15  */
16 
24 
29 {
33  public $genTree_traverseDeleted = true;
34 
39 
43  public $label_infoString = 'The list of records is organized as [table]:[uid]:[field]:[flexpointer]:[softref_key]';
44 
48  public $pagetreePlugins = [];
49 
53  public $cleanerModules = [];
54 
58  protected $recStats = [];
59 
63  protected $workspaceIndex = [0 => true];
64 
68  public function __construct()
69  {
70  // Running parent class constructor
71  parent::__construct();
72  $this->cleanerModules = (array)$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['lowlevel']['cleanerModules'];
73  // Adding options to help archive:
74  $this->cli_options[] = ['-r', 'Execute this tool, otherwise help is shown'];
75  $this->cli_options[] = ['-v level', 'Verbosity level 0-3', 'The value of level can be:
76  0 = all output
77  1 = info and greater (default)
78  2 = warnings and greater
79  3 = errors'];
80  $this->cli_options[] = ['--refindex mode', 'Mode for reference index handling for operations that require a clean reference index ("update"/"ignore")', 'Options are "check" (default), "update" and "ignore". By default, the reference index is checked before running analysis that require a clean index. If the check fails, the analysis is not run. You can choose to bypass this completely (using value "ignore") or ask to have the index updated right away before the analysis (using value "update")'];
81  $this->cli_options[] = ['--AUTOFIX [testName]', 'Repairs errors that can be automatically fixed.', 'Only add this option after having run the test without it so you know what will happen when you add this option! The optional parameter "[testName]" works for some tool keys to limit the fixing to a particular test.'];
82  $this->cli_options[] = ['--dryrun', 'With --AUTOFIX it will only simulate a repair process', 'You may like to use this to see what the --AUTOFIX option will be doing. It will output the whole process like if a fix really occurred but nothing is in fact happening'];
83  $this->cli_options[] = ['--YES', 'Implicit YES to all questions', 'Use this with EXTREME care. The option "-i" is not affected by this option.'];
84  $this->cli_options[] = ['-i', 'Interactive', 'Will ask you before running the AUTOFIX on each element.'];
85  $this->cli_options[] = ['--filterRegex expr', 'Define an expression for preg_match() that must match the element ID in order to auto repair it', 'The element ID is the string in quotation marks when the text \'Cleaning ... in "ELEMENT ID"\'. "expr" is the expression for preg_match(). To match for example "Nature3.JPG" and "Holiday3.JPG" you can use "/.*3.JPG/". To match for example "Image.jpg" and "Image.JPG" you can use "/.*.jpg/i". Try a --dryrun first to see what the matches are!'];
86  $this->cli_options[] = ['--showhowto', 'Displays HOWTO file for cleaner script.'];
87  // Setting help texts:
88  $this->cli_help['name'] = 'lowlevel_cleaner -- Analysis and clean-up tools for TYPO3 installations';
89  $this->cli_help['synopsis'] = 'toolkey ###OPTIONS###';
90  $this->cli_help['description'] = 'Dispatches to various analysis and clean-up tools which can plug into the API of this script. Typically you can run tests that will take longer than the usual max execution time of PHP. Such tasks could be checking for orphan records in the page tree or flushing all published versions in the system. For the complete list of options, please explore each of the \'toolkey\' keywords below:
91 
92  ' . implode('
93  ', array_keys($this->cleanerModules));
94  $this->cli_help['examples'] = '/.../cli_dispatch.phpsh lowlevel_cleaner missing_files -s -r
95 This will show you missing files in the TYPO3 system and only report back if errors were found.';
96  $this->cli_help['author'] = 'Kasper Skaarhoej, (c) 2006';
97  }
98 
99  /**************************
100  *
101  * CLI functionality
102  *
103  *************************/
110  public function cli_main($argv)
111  {
112  $this->cli_setArguments($argv);
113 
114  // Force user to admin state and set workspace to "Live":
115  $GLOBALS['BE_USER']->user['admin'] = 1;
116  $GLOBALS['BE_USER']->setWorkspace(0);
117  // Print Howto:
118  if ($this->cli_isArg('--showhowto')) {
119  $howto = file_get_contents(ExtensionManagementUtility::extPath('lowlevel') . 'README.rst');
120  echo wordwrap($howto, 120) . LF;
121  die;
122  }
123  // Print help
124  $analysisType = (string)$this->cli_args['_DEFAULT'][1];
125  if (!$analysisType) {
126  $this->cli_validateArgs();
127  $this->cli_help();
128  die;
129  }
130 
131  if (is_array($this->cleanerModules[$analysisType])) {
132  $cleanerMode = GeneralUtility::getUserObj($this->cleanerModules[$analysisType][0]);
133  $cleanerMode->cli_validateArgs();
134  // Run it...
135  if ($this->cli_isArg('-r')) {
136  if (!$cleanerMode->checkRefIndex || $this->cli_referenceIndexCheck()) {
137  $res = $cleanerMode->main();
138  $this->cli_printInfo($analysisType, $res);
139  // Autofix...
140  if ($this->cli_isArg('--AUTOFIX')) {
141  if ($this->cli_isArg('--YES') || $this->cli_keyboardInput_yes('
142 
143 NOW Running --AUTOFIX on result. OK?' . ($this->cli_isArg('--dryrun') ? ' (--dryrun simulation)' : ''))) {
144  $cleanerMode->main_autofix($res);
145  } else {
146  $this->cli_echo('ABORTING AutoFix...
147 ', 1);
148  }
149  }
150  }
151  } else {
152  // Help only...
153  $cleanerMode->cli_help();
154  die;
155  }
156  } else {
157  $this->cli_echo('ERROR: Analysis Type \'' . $analysisType . '\' is unknown.
158 ', 1);
159  die;
160  }
161  }
162 
168  public function cli_referenceIndexCheck()
169  {
170  // Reference index option:
171  $refIndexMode = isset($this->cli_args['--refindex']) ? $this->cli_args['--refindex'][0] : 'check';
172  switch ($refIndexMode) {
173  case 'check':
174 
175  case 'update':
176  $refIndexObj = GeneralUtility::makeInstance(ReferenceIndex::class);
177  list($headerContent, $bodyContent, $errorCount) = $refIndexObj->updateIndex($refIndexMode === 'check', $this->cli_echo());
178  if ($errorCount && $refIndexMode === 'check') {
179  $ok = false;
180  $this->cli_echo('ERROR: Reference Index Check failed! (run with \'--refindex update\' to fix)
181 ', 1);
182  } else {
183  $ok = true;
184  }
185  break;
186  case 'ignore':
187  $this->cli_echo('Reference Index Check: Bypassing reference index check...
188 ');
189  $ok = true;
190  break;
191  default:
192  $this->cli_echo('ERROR: Wrong value for --refindex argument.
193 ', 1);
194  die();
195 
196  }
197  return $ok;
198  }
199 
204  public function cli_noExecutionCheck($matchString)
205  {
206  // Check for filter:
207  if ($this->cli_isArg('--filterRegex') && ($regex = $this->cli_argValue('--filterRegex', 0))) {
208  if (!preg_match($regex, $matchString)) {
209  return 'BYPASS: Filter Regex "' . $regex . '" did not match string "' . $matchString . '"';
210  }
211  }
212  // Check for interactive mode
213  if ($this->cli_isArg('-i')) {
214  if (!$this->cli_keyboardInput_yes(' EXECUTE?')) {
215  return 'BYPASS...';
216  }
217  }
218  // Check for
219  if ($this->cli_isArg('--dryrun')) {
220  return 'BYPASS: --dryrun set';
221  }
222  }
223 
231  public function cli_printInfo($header, $res)
232  {
233  $detailLevel = MathUtility::forceIntegerInRange($this->cli_isArg('-v') ? $this->cli_argValue('-v') : 1, 0, 3);
234  $silent = !$this->cli_echo();
235  $severity = [
236  0 => 'MESSAGE',
237  1 => 'INFO',
238  2 => 'WARNING',
239  3 => 'ERROR'
240  ];
241  // Header output:
242  if ($detailLevel <= 1) {
243  $this->cli_echo('*********************************************
244 ' . $header . LF . '*********************************************
245 ');
246  $this->cli_echo(wordwrap(trim($res['message'])) . LF . LF);
247  }
248  // Traverse headers for output:
249  if (is_array($res['headers'])) {
250  foreach ($res['headers'] as $key => $value) {
251  if ($detailLevel <= (int)$value[2]) {
252  if (is_array($res[$key]) && (count($res[$key]) || !$silent)) {
253  // Header and explanaion:
254  $this->cli_echo('---------------------------------------------' . LF, 1);
255  $this->cli_echo('[' . $header . ']' . LF, 1);
256  $this->cli_echo($value[0] . ' [' . $severity[$value[2]] . ']' . LF, 1);
257  $this->cli_echo('---------------------------------------------' . LF, 1);
258  if (trim($value[1])) {
259  $this->cli_echo('Explanation: ' . wordwrap(trim($value[1])) . LF . LF, 1);
260  }
261  }
262  // Content:
263  if (is_array($res[$key])) {
264  if (count($res[$key])) {
265  if ($this->cli_echo('', 1)) {
266  print_r($res[$key]);
267  }
268  } else {
269  $this->cli_echo('(None)' . LF . LF);
270  }
271  } else {
272  $this->cli_echo($res[$key] . LF . LF);
273  }
274  }
275  }
276  }
277  }
278 
279  /**************************
280  *
281  * Page tree traversal
282  *
283  *************************/
294  public function genTree($rootID, $depth = 1000, $echoLevel = 0, $callBack = '')
295  {
296  // Initialize:
297  if (ExtensionManagementUtility::isLoaded('workspaces')) {
298  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
299  ->getQueryBuilderForTable('sys_workspace');
300 
301  $queryBuilder->getRestrictions()
302  ->removeAll()
303  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
304 
305  $workspaceRecords = $queryBuilder
306  ->select('uid', 'title')
307  ->from('sys_workspace')
308  ->execute()
309  ->fetchAll();
310 
311  foreach ($workspaceRecords as $workspaceRecord) {
312  $this->workspaceIndex[$workspaceRecord['uid']] = true;
313  }
314  }
315 
316  $this->recStats = [
317  'all' => [],
318  // All records connected in tree including versions (the reverse are orphans). All Info and Warning categories below are included here (and therefore safe if you delete the reverse of the list)
319  'deleted' => [],
320  // Subset of "alL" that are deleted-flagged [Info]
321  'versions' => [],
322  // Subset of "all" which are offline versions (pid=-1). [Info]
323  'versions_published' => [],
324  // Subset of "versions" that is a count of 1 or more (has been published) [Info]
325  'versions_liveWS' => [],
326  // Subset of "versions" that exists in live workspace [Info]
327  'versions_lost_workspace' => [],
328  // Subset of "versions" that doesn't belong to an existing workspace [Warning: Fix by move to live workspace]
329  'versions_inside_versioned_page' => [],
330  // Subset of "versions" This is versions of elements found inside an already versioned branch / page. In real life this can work out, but is confusing and the backend should prevent this from happening to people. [Warning: Fix by deleting those versions (or publishing them)]
331  'illegal_record_under_versioned_page' => [],
332  // If a page is "element" or "page" version and records are found attached to it, they might be illegally attached, so this will tell you. [Error: Fix by deleting orphans since they are not registered in "all" category]
333  'misplaced_at_rootlevel' => [],
334  // Subset of "all": Those that should not be at root level but are. [Warning: Fix by moving record into page tree]
335  'misplaced_inside_tree' => []
336  ];
337  // Start traversal:
338  $this->genTree_traverse($rootID, $depth, $echoLevel, $callBack);
339  // Sort recStats (for diff'able displays)
340  foreach ($this->recStats as $kk => $vv) {
341  foreach ($this->recStats[$kk] as $tables => $recArrays) {
342  ksort($this->recStats[$kk][$tables]);
343  }
344  ksort($this->recStats[$kk]);
345  }
346  if ($echoLevel > 0) {
347  echo LF . LF;
348  }
349  }
350 
364  public function genTree_traverse($rootID, $depth, $echoLevel = 0, $callBack = '', $versionSwapmode = false, $rootIsVersion = 0, $accumulatedPath = '')
365  {
366  // Register page:
367  $this->recStats['all']['pages'][$rootID] = $rootID;
368  $pageRecord = BackendUtility::getRecordRaw('pages', 'uid=' . (int)$rootID, 'deleted,title,t3ver_count,t3ver_wsid');
369  $accumulatedPath .= '/' . $pageRecord['title'];
370  // Register if page is deleted:
371  if ($pageRecord['deleted']) {
372  $this->recStats['deleted']['pages'][$rootID] = $rootID;
373  }
374  // If rootIsVersion is set it means that the input rootID is that of a version of a page. See below where the recursive call is made.
375  if ($rootIsVersion) {
376  $this->recStats['versions']['pages'][$rootID] = $rootID;
377  // If it has been published and is in archive now...
378  if ($pageRecord['t3ver_count'] >= 1 && $pageRecord['t3ver_wsid'] == 0) {
379  $this->recStats['versions_published']['pages'][$rootID] = $rootID;
380  }
381  // If it has been published and is in archive now...
382  if ($pageRecord['t3ver_wsid'] == 0) {
383  $this->recStats['versions_liveWS']['pages'][$rootID] = $rootID;
384  }
385  // If it doesn't belong to a workspace...
386  if (!isset($this->workspaceIndex[$pageRecord['t3ver_wsid']])) {
387  $this->recStats['versions_lost_workspace']['pages'][$rootID] = $rootID;
388  }
389  // In case the rootID is a version inside a versioned page
390  if ($rootIsVersion == 2) {
391  $this->recStats['versions_inside_versioned_page']['pages'][$rootID] = $rootID;
392  }
393  }
394  if ($echoLevel > 0) {
395  echo LF . $accumulatedPath . ' [' . $rootID . ']' . ($pageRecord['deleted'] ? ' (DELETED)' : '') . ($this->recStats['versions_published']['pages'][$rootID] ? ' (PUBLISHED)' : '');
396  }
397  if ($echoLevel > 1 && $this->recStats['versions_lost_workspace']['pages'][$rootID]) {
398  echo LF . ' ERROR! This version belongs to non-existing workspace (' . $pageRecord['t3ver_wsid'] . ')!';
399  }
400  if ($echoLevel > 1 && $this->recStats['versions_inside_versioned_page']['pages'][$rootID]) {
401  echo LF . ' WARNING! This version is inside an already versioned page or branch!';
402  }
403  // Call back:
404  if ($callBack) {
405  $this->{$callBack}('pages', $rootID, $echoLevel, $versionSwapmode, $rootIsVersion);
406  }
407  // Traverse tables of records that belongs to page:
408  foreach ($GLOBALS['TCA'] as $tableName => $cfg) {
409  if ($tableName !== 'pages') {
410  // Select all records belonging to page:
411  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
412  ->getQueryBuilderForTable($tableName);
413 
414  $queryBuilder->getRestrictions()->removeAll();
415 
416  $queryBuilder
417  ->select('uid')
418  ->from($tableName)
419  ->where(
420  $queryBuilder->expr()->eq(
421  'pid',
422  $queryBuilder->createNamedParameter($rootID, \PDO::PARAM_INT)
423  )
424  );
425 
426  if ($GLOBALS['TCA'][$tableName]['ctrl']['delete']) {
427  $queryBuilder->addSelect($GLOBALS['TCA'][$tableName]['ctrl']['delete']);
428  }
429 
430  if (!$this->genTree_traverseDeleted) {
431  $queryBuilder->getRestrictions()->add(DeletedRestriction::class);
432  }
433 
434  $result = $queryBuilder->execute();
435 
436  $count = $result->rowCount();
437  if ($count) {
438  if ($echoLevel == 2) {
439  echo LF . ' \\-' . $tableName . ' (' . $count . ')';
440  }
441  }
442  while ($rowSub = $result->fetch()) {
443  if ($echoLevel == 3) {
444  echo LF . ' \\-' . $tableName . ':' . $rowSub['uid'];
445  }
446  // If the rootID represents an "element" or "page" version type, we must check if the record from this table is allowed to belong to this:
447  if ($versionSwapmode) {
448  // This is illegal records under a versioned page - therefore not registered in $this->recStats['all'] so they should be orphaned:
449  $this->recStats['illegal_record_under_versioned_page'][$tableName][$rowSub['uid']] = $rowSub['uid'];
450  if ($echoLevel > 1) {
451  echo LF . ' ERROR! Illegal record (' . $tableName . ':' . $rowSub['uid'] . ') under versioned page!';
452  }
453  } else {
454  $this->recStats['all'][$tableName][$rowSub['uid']] = $rowSub['uid'];
455  // Register deleted:
456  if ($GLOBALS['TCA'][$tableName]['ctrl']['delete'] && $rowSub[$GLOBALS['TCA'][$tableName]['ctrl']['delete']]) {
457  $this->recStats['deleted'][$tableName][$rowSub['uid']] = $rowSub['uid'];
458  if ($echoLevel == 3) {
459  echo ' (DELETED)';
460  }
461  }
462  // Check location of records regarding tree root:
463  if (!$GLOBALS['TCA'][$tableName]['ctrl']['rootLevel'] && $rootID == 0) {
464  $this->recStats['misplaced_at_rootlevel'][$tableName][$rowSub['uid']] = $rowSub['uid'];
465  if ($echoLevel > 1) {
466  echo LF . ' ERROR! Misplaced record (' . $tableName . ':' . $rowSub['uid'] . ') on rootlevel!';
467  }
468  }
469  if ($GLOBALS['TCA'][$tableName]['ctrl']['rootLevel'] == 1 && $rootID > 0) {
470  $this->recStats['misplaced_inside_tree'][$tableName][$rowSub['uid']] = $rowSub['uid'];
471  if ($echoLevel > 1) {
472  echo LF . ' ERROR! Misplaced record (' . $tableName . ':' . $rowSub['uid'] . ') inside page tree!';
473  }
474  }
475  // Traverse plugins:
476  if ($callBack) {
477  $this->{$callBack}($tableName, $rowSub['uid'], $echoLevel, $versionSwapmode, $rootIsVersion);
478  }
479  // Add any versions of those records:
480  if ($this->genTree_traverseVersions) {
481  $versions = BackendUtility::selectVersionsOfRecord($tableName, $rowSub['uid'], 'uid,t3ver_wsid,t3ver_count' . ($GLOBALS['TCA'][$tableName]['ctrl']['delete'] ? ',' . $GLOBALS['TCA'][$tableName]['ctrl']['delete'] : ''), null, true);
482  if (is_array($versions)) {
483  foreach ($versions as $verRec) {
484  if (!$verRec['_CURRENT_VERSION']) {
485  if ($echoLevel == 3) {
486  echo LF . ' \\-[#OFFLINE VERSION: WS#' . $verRec['t3ver_wsid'] . '/Cnt:' . $verRec['t3ver_count'] . '] ' . $tableName . ':' . $verRec['uid'] . ')';
487  }
488  $this->recStats['all'][$tableName][$verRec['uid']] = $verRec['uid'];
489  // Register deleted:
490  if ($GLOBALS['TCA'][$tableName]['ctrl']['delete'] && $verRec[$GLOBALS['TCA'][$tableName]['ctrl']['delete']]) {
491  $this->recStats['deleted'][$tableName][$verRec['uid']] = $verRec['uid'];
492  if ($echoLevel == 3) {
493  echo ' (DELETED)';
494  }
495  }
496  // Register version:
497  $this->recStats['versions'][$tableName][$verRec['uid']] = $verRec['uid'];
498  if ($verRec['t3ver_count'] >= 1 && $verRec['t3ver_wsid'] == 0) {
499  // Only register published versions in LIVE workspace (published versions in draft workspaces are allowed)
500  $this->recStats['versions_published'][$tableName][$verRec['uid']] = $verRec['uid'];
501  if ($echoLevel == 3) {
502  echo ' (PUBLISHED)';
503  }
504  }
505  if ($verRec['t3ver_wsid'] == 0) {
506  $this->recStats['versions_liveWS'][$tableName][$verRec['uid']] = $verRec['uid'];
507  }
508  if (!isset($this->workspaceIndex[$verRec['t3ver_wsid']])) {
509  $this->recStats['versions_lost_workspace'][$tableName][$verRec['uid']] = $verRec['uid'];
510  if ($echoLevel > 1) {
511  echo LF . ' ERROR! Version (' . $tableName . ':' . $verRec['uid'] . ') belongs to non-existing workspace (' . $verRec['t3ver_wsid'] . ')!';
512  }
513  }
514  // In case we are inside a versioned branch, there should not exists versions inside that "branch".
515  if ($versionSwapmode) {
516  $this->recStats['versions_inside_versioned_page'][$tableName][$verRec['uid']] = $verRec['uid'];
517  if ($echoLevel > 1) {
518  echo LF . ' ERROR! This version (' . $tableName . ':' . $verRec['uid'] . ') is inside an already versioned page or branch!';
519  }
520  }
521  // Traverse plugins:
522  if ($callBack) {
523  $this->{$callBack}($tableName, $verRec['uid'], $echoLevel, $versionSwapmode, $rootIsVersion);
524  }
525  }
526  }
527  }
528  unset($versions);
529  }
530  }
531  }
532  }
533  }
534  unset($resSub);
535  unset($rowSub);
536  // Find subpages to root ID and traverse (only when rootID is not a version or is a branch-version):
537  if ($depth > 0) {
538  $depth--;
539  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
540  ->getQueryBuilderForTable('pages');
541 
542  $queryBuilder->getRestrictions()->removeAll();
543  if (!$this->genTree_traverseDeleted) {
544  $queryBuilder->getRestrictions()->add(DeletedRestriction::class);
545  }
546 
547  $queryBuilder
548  ->select('uid')
549  ->from('pages')
550  ->where(
551  $queryBuilder->expr()->eq(
552  'pid',
553  $queryBuilder->createNamedParameter($rootID, \PDO::PARAM_INT)
554  )
555  )
556  ->orderBy('sorting');
557 
558  $result = $queryBuilder->execute();
559  while ($row = $result->fetch()) {
560  $this->genTree_traverse($row['uid'], $depth, $echoLevel, $callBack, $versionSwapmode, 0, $accumulatedPath);
561  }
562  }
563  // Add any versions of pages
564  if ($rootID > 0 && $this->genTree_traverseVersions) {
565  $versions = BackendUtility::selectVersionsOfRecord('pages', $rootID, 'uid,t3ver_oid,t3ver_wsid,t3ver_count', null, true);
566  if (is_array($versions)) {
567  foreach ($versions as $verRec) {
568  if (!$verRec['_CURRENT_VERSION']) {
569  $this->genTree_traverse($verRec['uid'], $depth, $echoLevel, $callBack, true, $versionSwapmode ? 2 : 1, $accumulatedPath . ' [#OFFLINE VERSION: WS#' . $verRec['t3ver_wsid'] . '/Cnt:' . $verRec['t3ver_count'] . ']');
570  }
571  }
572  }
573  }
574  }
575 
576  /**************************
577  *
578  * Helper functions
579  *
580  *************************/
587  public function infoStr($rec)
588  {
589  return $rec['tablename'] . ':' . $rec['recuid'] . ':' . $rec['field'] . ':' . $rec['flexpointer'] . ':' . $rec['softref_key'] . ($rec['deleted'] ? ' (DELETED)' : '');
590  }
591 }
genTree($rootID, $depth=1000, $echoLevel=0, $callBack= '')
static selectVersionsOfRecord($table, $uid, $fields= '*', $workspace=0, $includeDeletedRecords=false, $row=null)
genTree_traverse($rootID, $depth, $echoLevel=0, $callBack= '', $versionSwapmode=false, $rootIsVersion=0, $accumulatedPath= '')
if(TYPO3_MODE=== 'BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']
static makeInstance($className,...$constructorArguments)
static forceIntegerInRange($theInt, $min, $max=2000000000, $defaultValue=0)
Definition: MathUtility.php:31