/** * Return the text of a template, after recursively * replacing any variables or templates within the template. * * @param array $piece The parts of the template * $piece['title']: the title, i.e. the part before the | * $piece['parts']: the parameter array * $piece['lineStart']: whether the brace was at the start of a line * @param PPFrame The current frame, contains template arguments * @return string the text of the template * @private */ function braceSubstitution($piece, $frame) { global $wgContLang, $wgNonincludableNamespaces; wfProfileIn(__METHOD__); wfProfileIn(__METHOD__ . '-setup'); # Flags $found = false; # $text has been filled $nowiki = false; # wiki markup in $text should be escaped $isHTML = false; # $text is HTML, armour it against wikitext transformation $forceRawInterwiki = false; # Force interwiki transclusion to be done in raw mode not rendered $isChildObj = false; # $text is a DOM node needing expansion in a child frame $isLocalObj = false; # $text is a DOM node needing expansion in the current frame # Title object, where $text came from $title = NULL; # $part1 is the bit before the first |, and must contain only title characters. # Various prefixes will be stripped from it later. $titleWithSpaces = $frame->expand($piece['title']); $part1 = trim($titleWithSpaces); $titleText = false; # Original title text preserved for various purposes $originalTitle = $part1; # $args is a list of argument nodes, starting from index 0, not including $part1 $args = null == $piece['parts'] ? array() : $piece['parts']; wfProfileOut(__METHOD__ . '-setup'); # SUBST wfProfileIn(__METHOD__ . '-modifiers'); if (!$found) { $mwSubst = MagicWord::get('subst'); if ($mwSubst->matchStartAndRemove($part1) xor $this->ot['wiki']) { # One of two possibilities is true: # 1) Found SUBST but not in the PST phase # 2) Didn't find SUBST and in the PST phase # In either case, return without further processing $text = $frame->virtualBracketedImplode('{{', '|', '}}', $titleWithSpaces, $args); $isLocalObj = true; $found = true; } } # Variables if (!$found && $args->getLength() == 0) { $id = $this->mVariables->matchStartToEnd($part1); if ($id !== false) { $text = $this->getVariableValue($id); if (MagicWord::getCacheTTL($id) > -1) { $this->mOutput->mContainsOldMagic = true; } $found = true; } } # MSG, MSGNW and RAW if (!$found) { # Check for MSGNW: $mwMsgnw = MagicWord::get('msgnw'); if ($mwMsgnw->matchStartAndRemove($part1)) { $nowiki = true; } else { # Remove obsolete MSG: $mwMsg = MagicWord::get('msg'); $mwMsg->matchStartAndRemove($part1); } # Check for RAW: $mwRaw = MagicWord::get('raw'); if ($mwRaw->matchStartAndRemove($part1)) { $forceRawInterwiki = true; } } wfProfileOut(__METHOD__ . '-modifiers'); # Parser functions if (!$found) { wfProfileIn(__METHOD__ . '-pfunc'); $colonPos = strpos($part1, ':'); if ($colonPos !== false) { # Case sensitive functions $function = substr($part1, 0, $colonPos); if (isset($this->mFunctionSynonyms[1][$function])) { $function = $this->mFunctionSynonyms[1][$function]; } else { # Case insensitive functions $function = strtolower($function); if (isset($this->mFunctionSynonyms[0][$function])) { $function = $this->mFunctionSynonyms[0][$function]; } else { $function = false; } } if ($function) { list($callback, $flags) = $this->mFunctionHooks[$function]; $initialArgs = array(&$this); $funcArgs = array(trim(substr($part1, $colonPos + 1))); if ($flags & SFH_OBJECT_ARGS) { # Add a frame parameter, and pass the arguments as an array $allArgs = $initialArgs; $allArgs[] = $frame; for ($i = 0; $i < $args->getLength(); $i++) { $funcArgs[] = $args->item($i); } $allArgs[] = $funcArgs; } else { # Convert arguments to plain text for ($i = 0; $i < $args->getLength(); $i++) { $funcArgs[] = trim($frame->expand($args->item($i))); } $allArgs = array_merge($initialArgs, $funcArgs); } # Workaround for PHP bug 35229 and similar if (!is_callable($callback)) { throw new MWException("Tag hook for {$function} is not callable\n"); } $result = call_user_func_array($callback, $allArgs); $found = true; $noparse = true; $preprocessFlags = 0; if (is_array($result)) { if (isset($result[0])) { $text = $result[0]; unset($result[0]); } // Extract flags into the local scope // This allows callers to set flags such as nowiki, found, etc. extract($result); } else { $text = $result; } if (!$noparse) { $text = $this->preprocessToDom($text, $preprocessFlags); $isChildObj = true; } } } wfProfileOut(__METHOD__ . '-pfunc'); } # Finish mangling title and then check for loops. # Set $title to a Title object and $titleText to the PDBK if (!$found) { $ns = NS_TEMPLATE; # Split the title into page and subpage $subpage = ''; $part1 = $this->maybeDoSubpageLink($part1, $subpage); if ($subpage !== '') { $ns = $this->mTitle->getNamespace(); } $title = Title::newFromText($part1, $ns); if ($title) { $titleText = $title->getPrefixedText(); # Check for language variants if the template is not found if ($wgContLang->hasVariants() && $title->getArticleID() == 0) { $wgContLang->findVariantLink($part1, $title, true); } # Do recursion depth check $limit = $this->mOptions->getMaxTemplateDepth(); if ($frame->depth >= $limit) { $found = true; $text = '<span class="error">' . wfMsgForContent('parser-template-recursion-depth-warning', $limit) . '</span>'; } } } # Load from database if (!$found && $title) { wfProfileIn(__METHOD__ . '-loadtpl'); if (!$title->isExternal()) { if ($title->getNamespace() == NS_SPECIAL && $this->mOptions->getAllowSpecialInclusion() && $this->ot['html']) { $text = SpecialPage::capturePath($title); if (is_string($text)) { $found = true; $isHTML = true; $this->disableCache(); } } else { if ($wgNonincludableNamespaces && in_array($title->getNamespace(), $wgNonincludableNamespaces)) { $found = false; //access denied wfDebug(__METHOD__ . ": template inclusion denied for " . $title->getPrefixedDBkey()); } else { list($text, $title) = $this->getTemplateDom($title); if ($text !== false) { $found = true; $isChildObj = true; } } } # If the title is valid but undisplayable, make a link to it if (!$found && ($this->ot['html'] || $this->ot['pre'])) { $text = "[[:{$titleText}]]"; $found = true; } } elseif ($title->isTrans()) { // Interwiki transclusion if ($this->ot['html'] && !$forceRawInterwiki) { $text = $this->interwikiTransclude($title, 'render'); $isHTML = true; } else { $text = $this->interwikiTransclude($title, 'raw'); // Preprocess it like a template $text = $this->preprocessToDom($text, self::PTD_FOR_INCLUSION); $isChildObj = true; } $found = true; } # Do infinite loop check # This has to be done after redirect resolution to avoid infinite loops via redirects if (!$frame->loopCheck($title)) { $found = true; $text = '<span class="error">' . wfMsgForContent('parser-template-loop-warning', $titleText) . '</span>'; wfDebug(__METHOD__ . ": template loop broken at '{$titleText}'\n"); } wfProfileOut(__METHOD__ . '-loadtpl'); } # If we haven't found text to substitute by now, we're done # Recover the source wikitext and return it if (!$found) { $text = $frame->virtualBracketedImplode('{{', '|', '}}', $titleWithSpaces, $args); wfProfileOut(__METHOD__); return array('object' => $text); } # Expand DOM-style return values in a child frame if ($isChildObj) { # Clean up argument array $newFrame = $frame->newChild($args, $title); if ($nowiki) { $text = $newFrame->expand($text, PPFrame::RECOVER_ORIG); } elseif ($titleText !== false && $newFrame->isEmpty()) { # Expansion is eligible for the empty-frame cache if (isset($this->mTplExpandCache[$titleText])) { $text = $this->mTplExpandCache[$titleText]; } else { $text = $newFrame->expand($text); $this->mTplExpandCache[$titleText] = $text; } } else { # Uncached expansion $text = $newFrame->expand($text); } } if ($isLocalObj && $nowiki) { $text = $frame->expand($text, PPFrame::RECOVER_ORIG); $isLocalObj = false; } # Replace raw HTML by a placeholder # Add a blank line preceding, to prevent it from mucking up # immediately preceding headings if ($isHTML) { $text = "\n\n" . $this->insertStripItem($text); } elseif ($nowiki && ($this->ot['html'] || $this->ot['pre'])) { $text = wfEscapeWikiText($text); } elseif (is_string($text) && !$piece['lineStart'] && preg_match('/^(?:{\\||:|;|#|\\*)/', $text)) { $text = "\n" . $text; } if (is_string($text) && !$this->incrementIncludeSize('post-expand', strlen($text))) { # Error, oversize inclusion $text = "[[{$originalTitle}]]" . $this->insertStripItem('<!-- WARNING: template omitted, post-expand include size too large -->'); $this->limitationWarn('post-expand-template-inclusion'); } if ($isLocalObj) { $ret = array('object' => $text); } else { $ret = array('text' => $text); } wfProfileOut(__METHOD__); return $ret; }
/** * Return the text of a template, after recursively * replacing any variables or templates within the template. * * @param array $piece The parts of the template * $piece['text']: matched text * $piece['title']: the title, i.e. the part before the | * $piece['parts']: the parameter array * @return string the text of the template * @private */ function braceSubstitution($piece) { global $wgContLang, $wgLang, $wgAllowDisplayTitle, $wgNonincludableNamespaces; $fname = __METHOD__; wfProfileIn($fname); wfProfileIn(__METHOD__ . '-setup'); # Flags $found = false; # $text has been filled $nowiki = false; # wiki markup in $text should be escaped $noparse = false; # Unsafe HTML tags should not be stripped, etc. $noargs = false; # Don't replace triple-brace arguments in $text $replaceHeadings = false; # Make the edit section links go to the template not the article $headingOffset = 0; # Skip headings when number, to account for those that weren't transcluded. $isHTML = false; # $text is HTML, armour it against wikitext transformation $forceRawInterwiki = false; # Force interwiki transclusion to be done in raw mode not rendered # Title object, where $text came from $title = NULL; $linestart = ''; # $part1 is the bit before the first |, and must contain only title characters # $args is a list of arguments, starting from index 0, not including $part1 $titleText = $part1 = $piece['title']; # If the third subpattern matched anything, it will start with | if (null == $piece['parts']) { $replaceWith = $this->variableSubstitution(array($piece['text'], $piece['title'])); if ($replaceWith != $piece['text']) { $text = $replaceWith; $found = true; $noparse = true; $noargs = true; } } $args = null == $piece['parts'] ? array() : $piece['parts']; wfProfileOut(__METHOD__ . '-setup'); # SUBST wfProfileIn(__METHOD__ . '-modifiers'); if (!$found) { $mwSubst =& MagicWord::get('subst'); if ($mwSubst->matchStartAndRemove($part1) xor $this->ot['wiki']) { # One of two possibilities is true: # 1) Found SUBST but not in the PST phase # 2) Didn't find SUBST and in the PST phase # In either case, return without further processing $text = $piece['text']; $found = true; $noparse = true; $noargs = true; } } # MSG, MSGNW and RAW if (!$found) { # Check for MSGNW: $mwMsgnw =& MagicWord::get('msgnw'); if ($mwMsgnw->matchStartAndRemove($part1)) { $nowiki = true; } else { # Remove obsolete MSG: $mwMsg =& MagicWord::get('msg'); $mwMsg->matchStartAndRemove($part1); } # Check for RAW: $mwRaw =& MagicWord::get('raw'); if ($mwRaw->matchStartAndRemove($part1)) { $forceRawInterwiki = true; } } wfProfileOut(__METHOD__ . '-modifiers'); //save path level before recursing into functions & templates. $lastPathLevel = $this->mTemplatePath; # Parser functions if (!$found) { wfProfileIn(__METHOD__ . '-pfunc'); $colonPos = strpos($part1, ':'); if ($colonPos !== false) { # Case sensitive functions $function = substr($part1, 0, $colonPos); if (isset($this->mFunctionSynonyms[1][$function])) { $function = $this->mFunctionSynonyms[1][$function]; } else { # Case insensitive functions $function = strtolower($function); if (isset($this->mFunctionSynonyms[0][$function])) { $function = $this->mFunctionSynonyms[0][$function]; } else { $function = false; } } if ($function) { $funcArgs = array_map('trim', $args); $funcArgs = array_merge(array(&$this, trim(substr($part1, $colonPos + 1))), $funcArgs); $result = call_user_func_array($this->mFunctionHooks[$function], $funcArgs); $found = true; // The text is usually already parsed, doesn't need triple-brace tags expanded, etc. //$noargs = true; //$noparse = true; if (is_array($result)) { if (isset($result[0])) { $text = $linestart . $result[0]; unset($result[0]); } // Extract flags into the local scope // This allows callers to set flags such as nowiki, noparse, found, etc. extract($result); } else { $text = $linestart . $result; } } } wfProfileOut(__METHOD__ . '-pfunc'); } # Template table test # Did we encounter this template already? If yes, it is in the cache # and we need to check for loops. if (!$found && isset($this->mTemplates[$piece['title']])) { $found = true; # Infinite loop test if (isset($this->mTemplatePath[$part1])) { $noparse = true; $noargs = true; $found = true; $text = $linestart . "[[{$part1}]]<!-- WARNING: template loop detected -->"; wfDebug(__METHOD__ . ": template loop broken at '{$part1}'\n"); } else { # set $text to cached message. $text = $linestart . $this->mTemplates[$piece['title']]; #treat title for cached page the same as others $ns = NS_TEMPLATE; $subpage = ''; $part1 = $this->maybeDoSubpageLink($part1, $subpage); if ($subpage !== '') { $ns = $this->mTitle->getNamespace(); } $title = Title::newFromText($part1, $ns); //used by include size checking $titleText = $title->getPrefixedText(); //used by edit section links $replaceHeadings = true; } } # Load from database if (!$found) { wfProfileIn(__METHOD__ . '-loadtpl'); $ns = NS_TEMPLATE; # declaring $subpage directly in the function call # does not work correctly with references and breaks # {{/subpage}}-style inclusions $subpage = ''; $part1 = $this->maybeDoSubpageLink($part1, $subpage); if ($subpage !== '') { $ns = $this->mTitle->getNamespace(); } $title = Title::newFromText($part1, $ns); if (!is_null($title)) { $titleText = $title->getPrefixedText(); # Check for language variants if the template is not found if ($wgContLang->hasVariants() && $title->getArticleID() == 0) { $wgContLang->findVariantLink($part1, $title); } if (!$title->isExternal()) { if ($title->getNamespace() == NS_SPECIAL && $this->mOptions->getAllowSpecialInclusion() && $this->ot['html']) { $text = SpecialPage::capturePath($title); if (is_string($text)) { $found = true; $noparse = true; $noargs = true; $isHTML = true; $this->disableCache(); } } else { if ($wgNonincludableNamespaces && in_array($title->getNamespace(), $wgNonincludableNamespaces)) { $found = false; //access denied wfDebug("{$fname}: template inclusion denied for " . $title->getPrefixedDBkey()); } else { list($articleContent, $title) = $this->fetchTemplateAndtitle($title); if ($articleContent !== false) { $found = true; $text = $articleContent; $replaceHeadings = true; } } } # If the title is valid but undisplayable, make a link to it if (!$found && ($this->ot['html'] || $this->ot['pre'])) { $text = "[[:{$titleText}]]"; $found = true; } } elseif ($title->isTrans()) { // Interwiki transclusion if ($this->ot['html'] && !$forceRawInterwiki) { $text = $this->interwikiTransclude($title, 'render'); $isHTML = true; $noparse = true; } else { $text = $this->interwikiTransclude($title, 'raw'); $replaceHeadings = true; } $found = true; } # Template cache array insertion # Use the original $piece['title'] not the mangled $part1, so that # modifiers such as RAW: produce separate cache entries if ($found) { if ($isHTML) { // A special page; don't store it in the template cache. } else { $this->mTemplates[$piece['title']] = $text; } $text = $linestart . $text; } } wfProfileOut(__METHOD__ . '-loadtpl'); } if ($found && !$this->incrementIncludeSize('pre-expand', strlen($text))) { # Error, oversize inclusion $text = $linestart . "[[{$titleText}]]<!-- WARNING: template omitted, pre-expand include size too large -->"; $noparse = true; $noargs = true; } # Recursive parsing, escaping and link table handling # Only for HTML output if ($nowiki && $found && ($this->ot['html'] || $this->ot['pre'])) { $text = wfEscapeWikiText($text); } elseif (!$this->ot['msg'] && $found) { if ($noargs) { $assocArgs = array(); } else { # Clean up argument array $assocArgs = self::createAssocArgs($args); # Add a new element to the templace recursion path $this->mTemplatePath[$part1] = 1; } if (!$noparse) { # If there are any <onlyinclude> tags, only include them if (in_string('<onlyinclude>', $text) && in_string('</onlyinclude>', $text)) { $replacer = new OnlyIncludeReplacer(); StringUtils::delimiterReplaceCallback('<onlyinclude>', '</onlyinclude>', array(&$replacer, 'replace'), $text); $text = $replacer->output; } # Remove <noinclude> sections and <includeonly> tags $text = StringUtils::delimiterReplace('<noinclude>', '</noinclude>', '', $text); $text = strtr($text, array('<includeonly>' => '', '</includeonly>' => '')); if ($this->ot['html'] || $this->ot['pre']) { # Strip <nowiki>, <pre>, etc. $text = $this->strip($text, $this->mStripState); if ($this->ot['html']) { $text = Sanitizer::removeHTMLtags($text, array(&$this, 'replaceVariables'), $assocArgs); } elseif ($this->ot['pre'] && $this->mOptions->getRemoveComments()) { $text = Sanitizer::removeHTMLcomments($text); } } $text = $this->replaceVariables($text, $assocArgs); # If the template begins with a table or block-level # element, it should be treated as beginning a new line. if (!$piece['lineStart'] && preg_match('/^(?:{\\||:|;|#|\\*)/', $text)) { $text = "\n" . $text; } } elseif (!$noargs) { # $noparse and !$noargs # Just replace the arguments, not any double-brace items # This is used for rendered interwiki transclusion $text = $this->replaceVariables($text, $assocArgs, true); } } # Prune lower levels off the recursion check path $this->mTemplatePath = $lastPathLevel; if ($found && !$this->incrementIncludeSize('post-expand', strlen($text))) { # Error, oversize inclusion $text = $linestart . "[[{$titleText}]]<!-- WARNING: template omitted, post-expand include size too large -->"; $noparse = true; $noargs = true; } if (!$found) { wfProfileOut($fname); return $piece['text']; } else { wfProfileIn(__METHOD__ . '-placeholders'); if ($isHTML) { # Replace raw HTML by a placeholder # Add a blank line preceding, to prevent it from mucking up # immediately preceding headings $text = "\n\n" . $this->insertStripItem($text, $this->mStripState); } else { # replace ==section headers== # XXX this needs to go away once we have a better parser. if (!$this->ot['wiki'] && !$this->ot['pre'] && $replaceHeadings) { if (!is_null($title)) { $encodedname = base64_encode($title->getPrefixedDBkey()); } else { $encodedname = base64_encode(""); } $m = preg_split('/(^={1,6}.*?={1,6}\\s*?$)/m', $text, -1, PREG_SPLIT_DELIM_CAPTURE); $text = ''; $nsec = $headingOffset; for ($i = 0; $i < count($m); $i += 2) { $text .= $m[$i]; if (!isset($m[$i + 1]) || $m[$i + 1] == "") { continue; } $hl = $m[$i + 1]; if (strstr($hl, "<!--MWTEMPLATESECTION")) { $text .= $hl; continue; } $m2 = array(); preg_match('/^(={1,6})(.*?)(={1,6}\\s*?)$/m', $hl, $m2); $text .= $m2[1] . $m2[2] . "<!--MWTEMPLATESECTION=" . $encodedname . "&" . base64_encode("{$nsec}") . "-->" . $m2[3]; $nsec++; } } } wfProfileOut(__METHOD__ . '-placeholders'); } # Prune lower levels off the recursion check path $this->mTemplatePath = $lastPathLevel; if (!$found) { wfProfileOut($fname); return $piece['text']; } else { wfProfileOut($fname); return $text; } }