MediaWiki r3120 - Code Review

Jump to: navigation, search
Repository:MediaWiki
Revision:r3119‎ | r3120 (on ViewVC)‎ | r3121 >
Date:16:46, 11 April 2004
Author:timstarling
Status:old
Tags:
Comment:
template arguments, various improvements to handling of recursive inclusion
Modified paths:

Diff [purge]

Index: trunk/phase3/includes/Parser.php
@@ -33,11 +33,9 @@
3434 # page would be proportional to the square of the input size. Hence, we limit the number
3535 # of inclusions of any given page, thus bringing any attack back to O(N).
3636 #
 37+
3738 define( "MAX_INCLUDE_REPEAT", 5 );
3839
39 -# Recursion depth of variable/inclusion evaluation
40 -define( "MAX_INCLUDE_PASSES", 3 );
41 -
4240 # Allowed values for $mOutputType
4341 define( "OT_HTML", 1 );
4442 define( "OT_WIKI", 2 );
@@ -50,7 +48,7 @@
5149 {
5250 # Cleared with clearState():
5351 var $mOutput, $mAutonumber, $mLastSection, $mDTopen, $mStripState = array();
54 - var $mVariables, $mIncludeCount;
 52+ var $mVariables, $mIncludeCount, $mArgStack;
5553
5654 # Temporary:
5755 var $mOptions, $mTitle, $mOutputType;
@@ -69,10 +67,11 @@
7068 $this->mVariables = false;
7169 $this->mIncludeCount = array();
7270 $this->mStripState = array();
 71+ $this->mArgStack = array();
7372 }
7473
7574 # First pass--just handle <nowiki> sections, pass the rest off
76 - # to doWikiPass2() which does all the real work.
 75+ # to internalParse() which does all the real work.
7776 #
7877 # Returns a ParserOutput
7978 #
@@ -91,21 +90,8 @@
9291
9392 $stripState = NULL;
9493 $text = $this->strip( $text, $this->mStripState );
95 - $text = $this->doWikiPass2( $text, $linestart );
96 - # needs to be called last
97 - $text = $this->doBlockLevels( $text, $linestart );
 94+ $text = $this->internalParse( $text, $linestart );
9895 $text = $this->unstrip( $text, $this->mStripState );
99 - # Clean up special characters
100 - $fixtags = array(
101 - "/<hr *>/i" => '<hr/>',
102 - "/<br *>/i" => '<br/>',
103 - "/<center *>/i"=>'<span style="text-align:center;">',
104 - "/<\\/center *>/i" => '</span>',
105 - # Clean up spare ampersands; note that we probably ought to be
106 - # more careful about named entities.
107 - '/&(?!:amp;|#[Xx][0-9A-fa-f]+;|#[0-9]+;|[a-zA-Z0-9]+;)/' => '&amp;'
108 - );
109 - $text = preg_replace( array_keys($fixtags), array_values($fixtags), $text );
11096
11197 $this->mOutput->setText( $text );
11298 wfProfileOut( $fname );
@@ -155,23 +141,15 @@
156142 function strip( $text, &$state )
157143 {
158144 $render = ($this->mOutputType == OT_HTML);
159 - if ( $state ) {
160 - $nowiki_content = $state['nowiki'];
161 - $hiero_content = $state['hiero'];
162 - $math_content = $state['math'];
163 - $pre_content = $state['pre'];
164 - $item_content = $state['item'];
165 - } else {
166 - $nowiki_content = array();
167 - $hiero_content = array();
168 - $math_content = array();
169 - $pre_content = array();
170 - $item_content = array();
171 - }
 145+ $nowiki_content = array();
 146+ $hiero_content = array();
 147+ $math_content = array();
 148+ $pre_content = array();
 149+ $item_content = array();
172150
173151 # Replace any instances of the placeholders
174152 $uniq_prefix = UNIQ_PREFIX;
175 - $text = str_replace( $uniq_prefix, wfHtmlEscapeFirst( $uniq_prefix ), $text );
 153+ #$text = str_replace( $uniq_prefix, wfHtmlEscapeFirst( $uniq_prefix ), $text );
176154
177155 $text = Parser::extractTags("nowiki", $text, $nowiki_content, $uniq_prefix);
178156 foreach( $nowiki_content as $marker => $content ){
@@ -213,35 +191,34 @@
214192 }
215193 }
216194
217 - $state = array(
218 - 'nowiki' => $nowiki_content,
219 - 'hiero' => $hiero_content,
220 - 'math' => $math_content,
221 - 'pre' => $pre_content,
222 - 'item' => $item_content
223 - );
 195+ # Merge state with the pre-existing state, if there is one
 196+ if ( $state ) {
 197+ $state['nowiki'] = $state['nowiki'] + $nowiki_content;
 198+ $state['hiero'] = $state['hiero'] + $hiero_content;
 199+ $state['math'] = $state['math'] + $math_content;
 200+ $state['pre'] = $state['pre'] + $pre_content;
 201+ } else {
 202+ $state = array(
 203+ 'nowiki' => $nowiki_content,
 204+ 'hiero' => $hiero_content,
 205+ 'math' => $math_content,
 206+ 'pre' => $pre_content,
 207+ 'item' => $item_content
 208+ );
 209+ }
224210 return $text;
225211 }
226212
227213 function unstrip( $text, &$state )
228214 {
229215 # Must expand in reverse order, otherwise nested tags will be corrupted
230 - /*
231 - $dicts = array( 'item', 'pre', 'math', 'hiero', 'nowiki' );
232 - foreach ( $dicts as $dictName ) {
233 - $content_dict = $state[$dictName];
234 - foreach( $content_dict as $marker => $content ){
235 - $text = str_replace( $marker, $content, $text );
236 - }
237 - }*/
238 -
239216 $contentDict = end( $state );
240217 for ( $contentDict = end( $state ); $contentDict !== false; $contentDict = prev( $state ) ) {
241218 for ( $content = end( $contentDict ); $content !== false; $content = prev( $contentDict ) ) {
242219 $text = str_replace( key( $contentDict ), $content, $text );
243220 }
244221 }
245 -
 222+
246223 return $text;
247224 }
248225
@@ -471,39 +448,47 @@
472449 return $t ;
473450 }
474451
475 - # Well, OK, it's actually about 14 passes. But since all the
476 - # hard lifting is done inside PHP's regex code, it probably
477 - # wouldn't speed things up much to add a real parser.
478 - #
479 - function doWikiPass2( $text, $linestart )
 452+ function internalParse( $text, $linestart, $args = array() )
480453 {
481 - $fname = "Parser::doWikiPass2";
 454+ $fname = "Parser::internalParse";
482455 wfProfileIn( $fname );
483 -
 456+
484457 $text = $this->removeHTMLtags( $text );
485 - $text = $this->replaceVariables( $text );
 458+ $text = $this->replaceVariables( $text, $args );
486459
487460 # $text = preg_replace( "/(^|\n)-----*/", "\\1<hr>", $text );
488461
489462 $text = $this->doHeadings( $text );
490 -
491463 if($this->mOptions->getUseDynamicDates()) {
492464 global $wgDateFormatter;
493465 $text = $wgDateFormatter->reformat( $this->mOptions->getDateFormat(), $text );
494466 }
495 -
496467 $text = $this->replaceExternalLinks( $text );
497468 $text = $this->doTokenizedParser ( $text );
498 -
499469 $text = $this->doTableStuff ( $text ) ;
500 -
501470 $text = $this->formatHeadings( $text );
502 -
503471 $sk =& $this->mOptions->getSkin();
504472 $text = $sk->transformContent( $text );
505 -
 473+
 474+ $fixtags = array(
 475+ "/<hr *>/i" => '<hr/>',
 476+ "/<br *>/i" => '<br/>',
 477+ "/<center *>/i"=>'<span style="text-align:center;">',
 478+ "/<\\/center *>/i" => '</span>'
 479+ );
 480+ $text = preg_replace( array_keys($fixtags), array_values($fixtags), $text );
 481+ // another round, but without regex
 482+ $fixtags = array(
 483+ '& ' => '&amp;',
 484+ '&<' => '&amp;<',
 485+ );
 486+ $text = str_replace( array_keys($fixtags), array_values($fixtags), $text );
 487+
506488 $text .= $this->categoryMagic () ;
507489
 490+ # needs to be called last
 491+ $text = $this->doBlockLevels( $text, $linestart );
 492+
508493 wfProfileOut( $fname );
509494 return $text;
510495 }
@@ -736,8 +721,7 @@
737722 $nextToken = $tokenizer->nextToken();
738723 $txt .= $nextToken["text"];
739724 }
740 - $fakestate = $this->mStripState;
741 - $txt = $this->handleInternalLink( $this->unstrip($txt,$fakestate), $prefix );
 725+ $txt = $this->handleInternalLink( $this->unstrip($txt,$this->mStripState), $prefix );
742726
743727 # did the tag start with 3 [ ?
744728 if($threeopen) {
@@ -1035,12 +1019,14 @@
10361020 # and making lists from lines starting with * # : etc.
10371021 #
10381022 $a = explode( "\n", $text );
1039 - $lastPref = $text = '';
1040 - $this->mDTopen = $inBlockElem = $pstack = false;
10411023
 1024+ $lastPref = $text = $lastLine = '';
 1025+ $this->mDTopen = $inBlockElem = false;
 1026+ $npl = 0;
 1027+ $pstack = false;
 1028+
10421029 if ( ! $linestart ) { $text .= array_shift( $a ); }
10431030 foreach ( $a as $t ) {
1044 -
10451031 $oLine = $t;
10461032 $opl = strlen( $lastPref );
10471033 $npl = strspn( $t, "*#:;" );
@@ -1054,7 +1040,7 @@
10551041
10561042 if ( ";" == substr( $pref, -1 ) ) {
10571043 $cpos = strpos( $t, ":" );
1058 - if ( ! ( false === $cpos ) ) {
 1044+ if ( false !== $cpos ) {
10591045 $term = substr( $t, 0, $cpos );
10601046 $text .= $term . $this->nextItem( ":" );
10611047 $t = substr( $t, $cpos + 1 );
@@ -1151,6 +1137,7 @@
11521138 $text .= "</" . $this->mLastSection . ">";
11531139 $this->mLastSection = "";
11541140 }
 1141+
11551142 wfProfileOut( $fname );
11561143 return $text;
11571144 }
@@ -1194,10 +1181,9 @@
11951182 }
11961183 }
11971184
1198 - /* private */ function replaceVariables( $text )
 1185+ /* private */ function replaceVariables( $text, $args = array() )
11991186 {
1200 - global $wgLang, $wgCurParser;
1201 - global $wgScript, $wgArticlePath;
 1187+ global $wgLang, $wgScript, $wgArticlePath;
12021188
12031189 $fname = "Parser::replaceVariables";
12041190 wfProfileIn( $fname );
@@ -1207,68 +1193,45 @@
12081194 $this->initialiseVariables();
12091195 }
12101196 $titleChars = Title::legalChars();
1211 - $regex = "/{{([$titleChars\\|]*?)}}/s";
 1197+ $regex = "/(\\n?){{([$titleChars]*?)(\\|.*?|)}}/s";
 1198+
 1199+ # This function is called recursively. To keep track of arguments we need a stack:
 1200+ array_push( $this->mArgStack, $args );
12121201
1213 - # "Recursive" variable expansion: run it through a couple of passes
1214 - for ( $i=0; $i<MAX_INCLUDE_REPEAT && !$bail; $i++ ) {
1215 - $oldText = $text;
1216 -
1217 - # It's impossible to rebind a global in PHP
1218 - # Instead, we run the substitution on a copy, then merge the changed fields back in
1219 - $wgCurParser = $this->fork();
1220 -
1221 - $text = preg_replace_callback( $regex, "wfBraceSubstitution", $text );
1222 - if ( $oldText == $text ) {
1223 - $bail = true;
1224 - }
1225 - $this->merge( $wgCurParser );
1226 - }
1227 -
 1202+ # PHP global rebinding syntax is a bit weird, need to use the GLOBALS array
 1203+ $GLOBALS['wgCurParser'] =& $this;
 1204+ $text = preg_replace_callback( $regex, "wfBraceSubstitution", $text );
 1205+
 1206+ array_pop( $this->mArgStack );
 1207+
12281208 return $text;
12291209 }
12301210
1231 - # Returns a copy of this object except with various variables cleared
1232 - # This copy can be re-merged with the parent after operations on the copy
1233 - function fork()
1234 - {
1235 - $copy = $this;
1236 - $copy->mOutput = new ParserOutput;
1237 - return $copy;
1238 - }
1239 -
1240 - # Merges a copy split off with fork()
1241 - function merge( &$copy )
1242 - {
1243 - # Output objects
1244 - $this->mOutput->merge( $copy->mOutput );
1245 -
1246 - # Include throttling arrays
1247 - foreach( $copy->mIncludeCount as $dbk => $count ) {
1248 - if ( array_key_exists( $dbk, $this->mIncludeCount ) ) {
1249 - $this->mIncludeCount[$dbk] += $count;
1250 - } else {
1251 - $this->mIncludeCount[$dbk] = $count;
1252 - }
1253 - }
1254 -
1255 - # Strip states
1256 - foreach( $copy->mStripState as $dictName => $contentDict ) {
1257 - $this->mStripState[$dictName] += $contentDict;
1258 - }
1259 - }
1260 -
12611211 function braceSubstitution( $matches )
12621212 {
12631213 global $wgLinkCache, $wgLang;
12641214 $fname = "Parser::braceSubstitution";
12651215 $found = false;
12661216 $nowiki = false;
1267 -
1268 - $text = $matches[1];
 1217+ $title = NULL;
 1218+
 1219+ # $newline is an optional newline character before the braces
 1220+ # $part1 is the bit before the first |, and must contain only title characters
 1221+ # $args is a list of arguments, starting from index 0, not including $part1
 1222+
 1223+ $newline = $matches[1];
 1224+ $part1 = $matches[2];
 1225+ # If the third subpattern matched anything, it will start with |
 1226+ if ( $matches[3] !== "" ) {
 1227+ $args = explode( "|", substr( $matches[3], 1 ) );
 1228+ } else {
 1229+ $args = array();
 1230+ }
 1231+ $argc = count( $args );
12691232
12701233 # SUBST
12711234 $mwSubst =& MagicWord::get( MAG_SUBST );
1272 - if ( $mwSubst->matchStartAndRemove( $text ) ) {
 1235+ if ( $mwSubst->matchStartAndRemove( $part1 ) ) {
12731236 if ( $this->mOutputType != OT_WIKI ) {
12741237 # Invalid SUBST not replaced at PST time
12751238 # Return without further processing
@@ -1285,19 +1248,21 @@
12861249 if ( !$found ) {
12871250 # Check for MSGNW:
12881251 $mwMsgnw =& MagicWord::get( MAG_MSGNW );
1289 - if ( $mwMsgnw->matchStartAndRemove( $text ) ) {
 1252+ if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
12901253 $nowiki = true;
12911254 } else {
12921255 # Remove obsolete MSG:
12931256 $mwMsg =& MagicWord::get( MAG_MSG );
1294 - $mwMsg->matchStartAndRemove( $text );
 1257+ $mwMsg->matchStartAndRemove( $part1 );
12951258 }
12961259
12971260 # Check if it is an internal message
12981261 $mwInt =& MagicWord::get( MAG_INT );
1299 - if ( $mwInt->matchStartAndRemove( $text ) ) {
1300 - $text = wfMsg( $text );
1301 - $found = true;
 1262+ if ( $mwInt->matchStartAndRemove( $part1 ) ) {
 1263+ if ( $this->incrementIncludeCount( "int:$part1" ) ) {
 1264+ $text = wfMsgReal( $part1, $args, true );
 1265+ $found = true;
 1266+ }
13021267 }
13031268 }
13041269
@@ -1305,12 +1270,12 @@
13061271 if ( !$found ) {
13071272 # Check for NS: (namespace expansion)
13081273 $mwNs = MagicWord::get( MAG_NS );
1309 - if ( $mwNs->matchStartAndRemove( $text ) ) {
1310 - if ( intval( $text ) ) {
1311 - $text = $wgLang->getNsText( intval( $text ) );
 1274+ if ( $mwNs->matchStartAndRemove( $part1 ) ) {
 1275+ if ( intval( $part1 ) ) {
 1276+ $text = $wgLang->getNsText( intval( $part1 ) );
13121277 $found = true;
13131278 } else {
1314 - $index = Namespace::getCanonicalIndex( strtolower( $text ) );
 1279+ $index = Namespace::getCanonicalIndex( strtolower( $part1 ) );
13151280 if ( !is_null( $index ) ) {
13161281 $text = $wgLang->getNsText( $index );
13171282 $found = true;
@@ -1324,78 +1289,54 @@
13251290 $mwLocal = MagicWord::get( MAG_LOCALURL );
13261291 $mwLocalE = MagicWord::get( MAG_LOCALURLE );
13271292
1328 - if ( $mwLocal->matchStartAndRemove( $text ) ) {
 1293+ if ( $mwLocal->matchStartAndRemove( $part1 ) ) {
13291294 $func = 'getLocalURL';
1330 - } elseif ( $mwLocalE->matchStartAndRemove( $text ) ) {
 1295+ } elseif ( $mwLocalE->matchStartAndRemove( $part1 ) ) {
13311296 $func = 'escapeLocalURL';
13321297 } else {
13331298 $func = '';
13341299 }
13351300
13361301 if ( $func !== '' ) {
1337 - $args = explode( "|", $text );
1338 - $n = count( $args );
1339 - if ( $n > 0 ) {
1340 - $title = Title::newFromText( $args[0] );
1341 - if ( !is_null( $title ) ) {
1342 - if ( $n > 1 ) {
1343 - $text = $title->$func( $args[1] );
1344 - } else {
1345 - $text = $title->$func();
1346 - }
1347 - $found = true;
 1302+ $title = Title::newFromText( $part1 );
 1303+ if ( !is_null( $title ) ) {
 1304+ if ( $argc > 0 ) {
 1305+ $text = $title->$func( $args[0] );
 1306+ } else {
 1307+ $text = $title->$func();
13481308 }
 1309+ $found = true;
13491310 }
1350 - }
 1311+ }
13511312 }
13521313
1353 - # Check for a match against internal variables
1354 - if ( !$found && array_key_exists( $text, $this->mVariables ) ) {
1355 - $text = $this->mVariables[$text];
 1314+ # Internal variables
 1315+ if ( !$found && array_key_exists( $part1, $this->mVariables ) ) {
 1316+ $text = $this->mVariables[$part1];
13561317 $found = true;
13571318 $this->mOutput->mContainsOldMagic = true;
13581319 }
13591320
 1321+ # Arguments input from the caller
 1322+ $inputArgs = end( $this->mArgStack );
 1323+ if ( !$found && array_key_exists( $part1, $inputArgs ) ) {
 1324+ $text = $inputArgs[$part1];
 1325+ $found = true;
 1326+ }
 1327+
13601328 # Load from database
13611329 if ( !$found ) {
1362 - $title = Title::newFromText( $text, NS_TEMPLATE );
1363 - if ( is_object( $title ) && !$title->isExternal() ) {
 1330+ $title = Title::newFromText( $part1, NS_TEMPLATE );
 1331+ if ( !is_null( $title ) && !$title->isExternal() ) {
13641332 # Check for excessive inclusion
13651333 $dbk = $title->getPrefixedDBkey();
1366 - if ( !array_key_exists( $dbk, $this->mIncludeCount ) ) {
1367 - $this->mIncludeCount[$dbk] = 0;
1368 - }
1369 - if ( ++$this->mIncludeCount[$dbk] <= MAX_INCLUDE_REPEAT ) {
 1334+ if ( $this->incrementIncludeCount( $dbk ) ) {
13701335 $article = new Article( $title );
13711336 $articleContent = $article->getContentWithoutUsingSoManyDamnGlobals();
13721337 if ( $articleContent !== false ) {
13731338 $found = true;
13741339 $text = $articleContent;
13751340
1376 - # Escaping and link table handling
1377 - # Not required for preSaveTransform()
1378 - if ( $this->mOutputType == OT_HTML ) {
1379 - if ( $nowiki ) {
1380 - $text = wfEscapeWikiText( $text );
1381 - } else {
1382 - $text = $this->removeHTMLtags( $text );
1383 - }
1384 - # Do not enter included links in link table
1385 - $wgLinkCache->suspend();
1386 -
1387 - # Run full parser on the included text
1388 - $text = $this->strip( $text, $this->mStripState );
1389 - $text = $this->doWikiPass2( $text, true );
1390 -
1391 - # Add the result to the strip state for re-inclusion after
1392 - # the rest of the processing
1393 - $text = $this->insertStripItem( $text, $this->mStripState );
1394 -
1395 - # Resume the link cache and register the inclusion as a link
1396 - $wgLinkCache->resume();
1397 - $wgLinkCache->addLinkObj( $title );
1398 -
1399 - }
14001341 }
14011342 }
14021343
@@ -1406,14 +1347,72 @@
14071348 }
14081349 }
14091350 }
 1351+
 1352+ # Recursive parsing, escaping and link table handling
 1353+ # Only for HTML output
 1354+ if ( $nowiki && $found && $this->mOutputType == OT_HTML ) {
 1355+ $text = wfEscapeWikiText( $text );
 1356+ } elseif ( $this->mOutputType == OT_HTML && $found ) {
 1357+ # Clean up argument array
 1358+ $assocArgs = array();
 1359+ $index = 1;
 1360+ foreach( $args as $arg ) {
 1361+ $eqpos = strpos( $arg, "=" );
 1362+ if ( $eqpos === false ) {
 1363+ $assocArgs[$index++] = $arg;
 1364+ } else {
 1365+ $name = trim( substr( $arg, 0, $eqpos ) );
 1366+ $value = trim( substr( $arg, $eqpos+1 ) );
 1367+ if ( $value === false ) {
 1368+ $value = "";
 1369+ }
 1370+ if ( $name !== false ) {
 1371+ $assocArgs[$name] = $value;
 1372+ }
 1373+ }
 1374+ }
14101375
 1376+ # Do not enter included links in link table
 1377+ if ( !is_null( $title ) ) {
 1378+ $wgLinkCache->suspend();
 1379+ }
 1380+
 1381+ # Run full parser on the included text
 1382+ $text = $this->strip( $text, $this->mStripState );
 1383+ $text = $this->internalParse( $text, (bool)$newline, $assocArgs );
 1384+
 1385+ # Add the result to the strip state for re-inclusion after
 1386+ # the rest of the processing
 1387+ $text = $this->insertStripItem( $text, $this->mStripState );
 1388+
 1389+ # Resume the link cache and register the inclusion as a link
 1390+ if ( !is_null( $title ) ) {
 1391+ $wgLinkCache->resume();
 1392+ $wgLinkCache->addLinkObj( $title );
 1393+ }
 1394+ }
 1395+
14111396 if ( !$found ) {
14121397 return $matches[0];
14131398 } else {
1414 - return $text;
 1399+ return $newline . $text;
14151400 }
14161401 }
14171402
 1403+ # Returns true if the function is allowed to include this entity
 1404+ function incrementIncludeCount( $dbk )
 1405+ {
 1406+ if ( !array_key_exists( $dbk, $this->mIncludeCount ) ) {
 1407+ $this->mIncludeCount[$dbk] = 0;
 1408+ }
 1409+ if ( ++$this->mIncludeCount[$dbk] <= MAX_INCLUDE_REPEAT ) {
 1410+ return true;
 1411+ } else {
 1412+ return false;
 1413+ }
 1414+ }
 1415+
 1416+
14181417 # Cleans up HTML, removes dangerous tags and attributes
14191418 /* private */ function removeHTMLtags( $text )
14201419 {
@@ -1617,7 +1616,7 @@
16181617
16191618 # The canonized header is a version of the header text safe to use for links
16201619 # Avoid insertion of weird stuff like <math> by expanding the relevant sections
1621 - $canonized_headline = Parser::unstrip( $headline, $this->mStripState );
 1620+ $canonized_headline = $this->unstrip( $headline, $this->mStripState );
16221621
16231622 # strip out HTML
16241623 $canonized_headline = preg_replace( "/<.*?" . ">/","",$canonized_headline );
@@ -1682,7 +1681,10 @@
16831682 if( $showEditLink && $headlineCount > 0 && $i == 0 && $block != "\n" ) {
16841683 # This is the [edit] link that appears for the top block of text when
16851684 # section editing is enabled
1686 - $full .= $sk->editSectionLink(0);
 1685+
 1686+ # Disabled because it broke block formatting
 1687+ # For example, a bullet point in the top line
 1688+ # $full .= $sk->editSectionLink(0);
16871689 }
16881690 $full .= $block;
16891691 if( $doShowToc && !$i) {

Status & tagging log

  • 01:56, 13 October 2010 ^demon (Talk | contribs) changed the status of r3120 [removed: new added: old]
Personal tools
Namespaces

Variants
Views
Actions
Navigation
Support
Download
Development
Communication
Toolbox