diff --git a/classes/Article.php b/classes/Article.php new file mode 100644 index 0000000..216bea7 --- /dev/null +++ b/classes/Article.php @@ -0,0 +1,395 @@ +mTitle = $title; + $this->mNamespace = $namespace; + } + + /** + * Initialize a new instance from a database row. + * + * @access public + * @param array Database Row + * @param object \DPL\Parameters Object + * @param object Mediawiki Title Object + * @param integer Page Namespace ID + * @param string Page Title as Selected from Query + * @return object \DPL\Article Object + */ + public static function newFromRow($row, Parameters $parameters, \Title $title, $pageNamespace, $pageTitle) { + global $wgLang, $wgContLang; + + $article = new Article($title, $pageNamespace); + $revActorName = User::newFromActorId( $row['rev_actor'] )->getName(); + + $titleText = $title->getText(); + if ($parameters->getParameter('shownamespace') === true) { + $titleText = $title->getPrefixedText(); + } + $replaceInTitle = $parameters->getParameter('replaceintitle'); + if (is_array($replaceInTitle) && count($replaceInTitle) === 2) { + $titleText = preg_replace($replaceInTitle[0], $replaceInTitle[1], $titleText); + } + + //Chop off title if longer than the 'titlemaxlen' parameter. + if ($parameters->getParameter('titlemaxlen') !== null && strlen($titleText) > $parameters->getParameter('titlemaxlen')) { + $titleText = substr($titleText, 0, $parameters->getParameter('titlemaxlen')) . '...'; + } + if ($parameters->getParameter('showcurid') === true && isset($row['page_id'])) { + $articleLink = '[' . $title->getLinkURL(['curid' => $row['page_id']]) . ' ' . htmlspecialchars($titleText) . ']'; + } else { + $articleLink = '[[' . ($parameters->getParameter('escapelinks') && ($pageNamespace == NS_CATEGORY || $pageNamespace == NS_FILE) ? ':' : '') . $title->getFullText() . '|' . htmlspecialchars($titleText) . ']]'; + } + + $article->mLink = $articleLink; + + //get first char used for category-style output + if (isset($row['sortkey'])) { + $article->mStartChar = $wgContLang->convert($wgContLang->firstChar($row['sortkey'])); + } else { + $article->mStartChar = $wgContLang->convert($wgContLang->firstChar($pageTitle)); + } + + $article->mID = intval($row['page_id']); + + //External link + if (isset($row['el_to'])) { + $article->mExternalLink = $row['el_to']; + } + + //SHOW PAGE_COUNTER + if (isset($row['page_counter'])) { + $article->mCounter = intval($row['page_counter']); + } + + //SHOW PAGE_SIZE + if (isset($row['page_len'])) { + $article->mSize = intval($row['page_len']); + } + //STORE initially selected PAGE + if (is_array($parameters->getParameter('linksto')) && (count($parameters->getParameter('linksto')) || count($parameters->getParameter('linksfrom')))) { + if (!isset($row['sel_title'])) { + $article->mSelTitle = 'unknown page'; + $article->mSelNamespace = 0; + } else { + $article->mSelTitle = $row['sel_title']; + $article->mSelNamespace = $row['sel_ns']; + } + } + + //STORE selected image + if (is_array($parameters->getParameter('imageused')) && count($parameters->getParameter('imageused')) > 0) { + if (!isset($row['image_sel_title'])) { + $article->mImageSelTitle = 'unknown image'; + } else { + $article->mImageSelTitle = $row['image_sel_title']; + } + } + + if ($parameters->getParameter('goal') != 'categories') { + //REVISION SPECIFIED + if ($parameters->getParameter('lastrevisionbefore') || $parameters->getParameter('allrevisionsbefore') || $parameters->getParameter('firstrevisionsince') || $parameters->getParameter('allrevisionssince')) { + $article->mRevision = $row['rev_id']; + $article->mUser = $revActorName; + $article->mDate = $row['rev_timestamp']; + $article->mComment = $row['rev_comment_id']; + } + + //SHOW "PAGE_TOUCHED" DATE, "FIRSTCATEGORYDATE" OR (FIRST/LAST) EDIT DATE + if ($parameters->getParameter('addpagetoucheddate')) { + $article->mDate = $row['page_touched']; + } elseif ($parameters->getParameter('addfirstcategorydate')) { + $article->mDate = $row['cl_timestamp']; + } elseif ($parameters->getParameter('addeditdate') && isset($row['rev_timestamp'])) { + $article->mDate = $row['rev_timestamp']; + } elseif ($parameters->getParameter('addeditdate') && isset($row['page_touched'])) { + $article->mDate = $row['page_touched']; + } + + //Time zone adjustment + if ($article->mDate) { + $article->mDate = $wgLang->userAdjust($article->mDate); + } + + if ($article->mDate && $parameters->getParameter('userdateformat')) { + //Apply the userdateformat + $article->myDate = gmdate($parameters->getParameter('userdateformat'), wfTimeStamp(TS_UNIX, $article->mDate)); + } + // CONTRIBUTION, CONTRIBUTOR + if ($parameters->getParameter('addcontribution')) { + $article->mContribution = $row['contribution']; + $article->mContributor = User::newFromActorId( $row['contributor'] )->getName(); + $article->mContrib = substr('*****************', 0, (int) round(log($row['contribution']))); + } + + //USER/AUTHOR(S) + // because we are going to do a recursive parse at the end of the output phase + // we have to generate wiki syntax for linking to a user´s homepage + if ($parameters->getParameter('adduser') || $parameters->getParameter('addauthor') || $parameters->getParameter('addlasteditor')) { + $article->mUserLink = '[[User:' . $revActorName . '|' . $revActorName . ']]'; + $article->mUser = $revActorName; + } + + //CATEGORY LINKS FROM CURRENT PAGE + if ($parameters->getParameter('addcategories') && ($row['cats'])) { + $artCatNames = explode(' | ', $row['cats']); + foreach ($artCatNames as $artCatName) { + $article->mCategoryLinks[] = '[[:Category:' . $artCatName . '|' . str_replace('_', ' ', $artCatName) . ']]'; + $article->mCategoryTexts[] = str_replace('_', ' ', $artCatName); + } + } + // PARENT HEADING (category of the page, editor (user) of the page, etc. Depends on ordermethod param) + if ($parameters->getParameter('headingmode') != 'none') { + switch ($parameters->getParameter('ordermethod')[0]) { + case 'category': + //Count one more page in this heading + self::$headings[$row['cl_to']] = (isset(self::$headings[$row['cl_to']]) ? self::$headings[$row['cl_to']] + 1 : 1); + if ($row['cl_to'] == '') { + //uncategorized page (used if ordermethod=category,...) + $article->mParentHLink = '[[:Special:Uncategorizedpages|' . wfMessage('uncategorizedpages') . ']]'; + } else { + $article->mParentHLink = '[[:Category:' . $row['cl_to'] . '|' . str_replace('_', ' ', $row['cl_to']) . ']]'; + } + break; + case 'user': + self::$headings[$revActorName] = (isset(self::$headings[$revActorName]) ? self::$headings[$revActorName] + 1 : 1); + if ($row['rev_actor'] == 0) { //anonymous user + $article->mParentHLink = '[[User:' . $revActorName . '|' . $revActorName . ']]'; + } else { + $article->mParentHLink = '[[User:' . $revActorName . '|' . $revActorName . ']]'; + } + break; + } + } + } + + return $article; + } + + /** + * Returns all heading information processed from all newly instantiated article objects. + * + * @access public + * @return array Headings + */ + public static function getHeadings() { + return self::$headings; + } + + /** + * Reset the headings to their initial state. + * Ideally this Article class should not exist and be handled by the built in MediaWiki class. + * Bug: https://jira/browse/HYD-913 + * + * @access public + * @return void + */ + public static function resetHeadings() { + self::$headings = []; + } + + /** + * Get the formatted date for this article if available. + * + * @access public + * @return mixed Formatted string or null for none set. + */ + public function getDate() { + global $wgLang; + if ($this->myDate !== null) { + return $this->myDate; + } elseif ($this->mDate !== null) { + return $wgLang->timeanddate($article->mDate, true); + } + return null; + } +} \ No newline at end of file diff --git a/classes/Config.php b/classes/Config.php new file mode 100644 index 0000000..73cf943 --- /dev/null +++ b/classes/Config.php @@ -0,0 +1,77 @@ + wfDplLst..). + * So any version of LabeledSectionTransclusion can be installed together with DPL + * + * Enhancements were made to + * - allow inclusion of templates ("template swapping") + * - reduce the size of the transcluded text to a limit of characters + * + * + * Thanks to Steve for his great work! + * -- Algorithmix + */ +namespace DPL; + +use DPL\Lister\Lister; + +class LST { + ############################################################## + # To do transclusion from an extension, we need to interact with the parser + # at a low level. This is the general transclusion functionality + ############################################################## + + /** + * Register what we're working on in the parser, so we don't fall into a trap. + * @param $parser Parser + * @param $part1 + * @return bool + */ + public static function open($parser, $part1) { + // Infinite loop test + if (isset($parser->mTemplatePath[$part1])) { + wfDebug(__METHOD__ . ": template loop broken at '$part1'\n"); + return false; + } else { + $parser->mTemplatePath[$part1] = 1; + return true; + } + } + + /** + * Finish processing the function. + * @param $parser Parser + * @param $part1 + * @return bool + */ + public static function close($parser, $part1) { + // Infinite loop test + if (isset($parser->mTemplatePath[$part1])) { + unset($parser->mTemplatePath[$part1]); + } else { + wfDebug(__METHOD__ . ": close unopened template loop at '$part1'\n"); + } + } + + /** + * Handle recursive substitution here, so we can break cycles, and set up + * return values so that edit sections will resolve correctly. + **/ + private static function parse($parser, $title, $text, $part1, $skiphead = 0, $recursionCheck = true, $maxLength = -1, $link = '', $trim = false, $skipPattern = []) { + // if someone tries something like
lst only
+ // text, may as well do the right thing. + $text = str_replace('', '', $text); + + // if desired we remove portions of the text, esp. template calls + foreach ($skipPattern as $skipPat) { + $text = preg_replace($skipPat, '', $text); + } + + if (self::open($parser, $part1)) { + + //Handle recursion here, so we can break cycles. + if ($recursionCheck == false) { + $text = $parser->preprocess($text, $parser->mTitle, $parser->mOptions); + self::close($parser, $part1); + } + + if ($maxLength > 0) { + $text = self::limitTranscludedText($text, $maxLength, $link); + } + if ($trim) { + return trim($text); + } else { + return $text; + } + } else { + return "[[" . $title->getPrefixedText() . "]]" . ""; + } + } + + ############################################################## + # And now, the labeled section transclusion + ############################################################## + + /** + * Parser tag hook for
. + * The section markers aren't paired, so we only need to remove them. + * + * @param string $in + * @param array $assocArgs + * @param Parser $parser + * @return string HTML output + */ + private static function noop($in, $assocArgs = [], $parser = null) { + return ''; + } + + ///Generate a regex to match the section(s) we're interested in. + private static function createSectionPattern($sec, $to, &$any) { + $any = false; + $to_sec = ($to == '') ? $sec : $to; + if ($sec[0] == '*') { + $any = true; + if ($sec == '**') { + $sec = '[^\/>"' . "']+"; + } else { + $sec = str_replace('/', '\/', substr($sec, 1)); + } + } else { + $sec = preg_quote($sec, '/'); + } + if ($to_sec[0] == '*') { + if ($to_sec == '**') { + $to_sec = '[^\/>"' . "']+"; + } else { + $to_sec = str_replace('/', '\/', substr($to_sec, 1)); + } + } else { + $to_sec = preg_quote($to_sec, '/'); + } + $ws = "(?:\s+[^>]+)?"; //was like $ws="\s*" + return "/(.*?)\n?]+\s+)?(?i:end)=" . "['\"]?\\1['\"]?" . "$ws\/?>/s"; + } + + /** + * Count headings in skipped text. + * + * Count skipped headings, so parser (as of r18218) can skip them, to + * prevent wrong heading links (see bug 6563). + * + * @param string $text + * @param int $limit Cutoff point in the text to stop searching + * @return int Number of matches + * @private + */ + private static function countHeadings($text, $limit) { + $pat = '^(={1,6}).+\1\s*$()'; + + $count = 0; + $offset = 0; + $m = []; + while (preg_match("/$pat/im", $text, $m, PREG_OFFSET_CAPTURE, $offset)) { + if ($m[2][1] > $limit) { + break; + } + + $count++; + $offset = $m[2][1]; + } + + return $count; + } + + public static function text($parser, $page, &$title, &$text) { + $title = \Title::newFromText($page); + + if (is_null($title)) { + $text = ''; + return true; + } else { + $text = $parser->fetchTemplate($title); + } + + //if article doesn't exist, return a red link. + if ($text == false) { + $text = "[[" . $title->getPrefixedText() . "]]"; + return false; + } else { + return true; + } + } + + ///section inclusion - include all matching sections + public static function includeSection($parser, $page = '', $sec = '', $to = '', $recursionCheck = true, $trim = false, $skipPattern = []) { + $output = []; + if (self::text($parser, $page, $title, $text) == false) { + $output[] = $text; + return $output; + } + $any = false; + $pat = self::createSectionPattern($sec, $to, $any); + + preg_match_all($pat, $text, $m, PREG_PATTERN_ORDER); + + foreach ($m[2] as $nr => $piece) { + $piece = self::parse($parser, $title, $piece, "#lst:${page}|${sec}", 0, $recursionCheck, $trim, $skipPattern); + if ($any) { + $output[] = $m[1][$nr] . '::' . $piece; + } else { + $output[] = $piece; + } + } + return $output; + } + + /** + * Truncate a portion of wikitext so that .. + * ... does not contain (open) html comments + * ... it is not larger that $lim characters + * ... it is balanced in terms of braces, brackets and tags + * ... it is cut at a word boundary (white space) if possible + * ... can be used as content of a wikitable field without spoiling the whole surrounding wikitext structure + * @param $lim limit of character count for the result + * @param $text the wikitext to be truncated + * @param $link an optional link which will be appended to the text if it was truncatedt + * @return the truncated text; + * note that the returned text may be longer than the limit if this is necessary + * to return something at all. We do not want to return an empty string if the input is not empty + * if the text is already shorter than the limit, the text + * will be returned without any checks for balance of tags + */ + public static function limitTranscludedText($text, $limit, $link = '') { + // if text is smaller than limit return complete text + if ($limit >= strlen($text)) { + return $text; + } + + // otherwise strip html comments and check again + $text = preg_replace('//s', '', $text); + if ($limit >= strlen($text)) { + return $text; + } + + // search latest position with balanced brackets/braces + // store also the position of the last preceding space + + $brackets = 0; + $cbrackets = 0; + $n0 = -1; + $nb = 0; + for ($i = 0; $i < $limit; $i++) { + $c = $text[$i]; + if ($c == '[') { + $brackets++; + } + if ($c == ']') { + $brackets--; + } + if ($c == '{') { + $cbrackets++; + } + if ($c == '}') { + $cbrackets--; + } + // we store the position if it is valid in terms of parentheses balancing + if ($brackets == 0 && $cbrackets == 0) { + $n0 = $i; + if ($c == ' ') { + $nb = $i; + } + } + } + + // if there is a valid cut-off point we use it; it will be the largest one which is not above the limit + if ($n0 >= 0) { + // we try to cut off at a word boundary, this may lead to a shortening of max. 15 chars + if ($nb > 0 && $nb + 15 > $n0) { + $n0 = $nb; + } + $cut = substr($text, 0, $n0 + 1); + + // an open html comment would be fatal, but this should not happen as we already have + // eliminated html comments at the beginning + + // some tags are critical: ref, pre, nowiki + // if these tags were not balanced they would spoil the result completely + // we enforce balance by appending the necessary amount of corresponding closing tags + // currently we ignore the nesting, i.e. all closing tags are appended at the end. + // This simple approach may fail in some cases ... + + $matches = []; + $noMatches = preg_match_all('#<\s*(/?ref|/?pre|/?nowiki)(\s+[^>]*?)*>#im', $cut, $matches); + $tags = [ + 'ref' => 0, + 'pre' => 0, + 'nowiki' => 0 + ]; + + if ($noMatches > 0) { + // calculate tag count (ignoring nesting) + foreach ($matches[1] as $mm) { + if ($mm[0] == '/') { + $tags[substr($mm, 1)]--; + } else { + $tags[$mm]++; + } + } + // append missing closing tags - should the tags be ordered by precedence ? + foreach ($tags as $tagName => $level) { + while ($level > 0) { + // avoid empty ref tag + if ($tagName == 'ref' && substr($cut, strlen($cut) - 5) == '') { + $cut = substr($cut, 0, strlen($cut) - 5); + } else { + $cut .= ''; + } + $level--; + } + } + } + return $cut . $link; + } elseif ($limit == 0) { + return $link; + } else { + // otherwise we recurse and try again with twice the limit size; this will lead to bigger output but + // it will at least produce some output at all; otherwise the reader might think that there + // is no information at all + return self::limitTranscludedText($text, $limit * 2, $link); + } + } + + public static function includeHeading($parser, $page = '', $sec = '', $to = '', &$sectionHeading, $recursionCheck = true, $maxLength = -1, $link = 'default', $trim = false, $skipPattern = []) { + $output = []; + if (self::text($parser, $page, $title, $text) == false) { + $output[0] = $text; + return $output; + } + /* throw away comments */ + $text = preg_replace('//s', '', $text); + return self::extractHeadingFromText($parser, $page, $title, $text, $sec, $to, $sectionHeading, $recursionCheck, $maxLength, $link, $trim, $skipPattern); + } + + //section inclusion - include all matching sections (return array) + public static function extractHeadingFromText($parser, $page, $title, $text, $sec = '', $to = '', &$sectionHeading, $recursionCheck = true, $maxLength = -1, $cLink = 'default', $trim = false, $skipPattern = []) { + $continueSearch = true; + $n = 0; + $output[$n] = ''; + $nr = 0; + // check if we are going to fetch the n-th section + if (preg_match('/^%-?[1-9][0-9]*$/', $sec)) { + $nr = substr($sec, 1); + } + if (preg_match('/^%0$/', $sec)) { + $nr = -2; // transclude text before the first section + } + + // if the section name starts with a # or with a @ we use it as regexp, otherwise as plain string + $isPlain = true; + if ($sec != '' && ($sec[0] == '#' || $sec[0] == '@')) { + $sec = substr($sec, 1); + $isPlain = false; + } + do { + //Generate a regex to match the === classical heading section(s) === we're + //interested in. + $headLine = ''; + if ($sec == '') { + $begin_off = 0; + $head_len = 6; + } else { + if ($nr != 0) { + $pat = '^(={1,6})\s*[^=\s\n][^\n=]*\s*\1\s*($)'; + } elseif ($isPlain) { + $pat = '^(={1,6})\s*' . preg_quote($sec, '/') . '\s*\1\s*($)'; + } else { + $pat = '^(={1,6})\s*' . str_replace('/', '\/', $sec) . '\s*\1\s*($)'; + } + if (preg_match("/$pat/im", $text, $m, PREG_OFFSET_CAPTURE)) { + $mata = []; + $no_parenthesis = preg_match_all('/\(/', $pat, $mata); + $begin_off = $m[$no_parenthesis][1]; + $head_len = strlen($m[1][0]); + $headLine = trim($m[0][0], "\n =\t"); + } elseif ($nr == -2) { + $m[1][1] = strlen($text) + 1; // take whole article if no heading found + } else { + // match failed + return $output; + } + } + // create a link symbol (arrow, img, ...) in case we have to cut the text block to maxLength + $link = $cLink; + if ($link == 'default') { + $link = ' [[' . $page . '#' . $headLine . '|..→]]'; + } elseif (strstr($link, 'img=') != false) { + $link = str_replace('img=', "page=" . $page . '#' . $headLine . "\nimg=Image:", $link) . "\n"; + } elseif (strstr($link, '%SECTION%') == false) { + $link = ' [[' . $page . '#' . $headLine . '|' . $link . ']]'; + } else { + $link = str_replace('%SECTION%', $page . '#' . $headLine, $link); + } + if ($nr == -2) { + // output text before first section and done + $piece = substr($text, 0, $m[1][1] - 1); + $output[0] = self::parse($parser, $title, $piece, "#lsth:${page}|${sec}", 0, $recursionCheck, $maxLength, $link, $trim, $skipPattern); + return $output; + } + + if (isset($end_off)) { + unset($end_off); + } + if ($to != '') { + //if $to is supplied, try and match it. If we don't match, just ignore it. + if ($isPlain) { + $pat = '^(={1,6})\s*' . preg_quote($to, '/') . '\s*\1\s*$'; + } else { + $pat = '^(={1,6})\s*' . str_replace('/', '\/', $to) . '\s*\1\s*$'; + } + if (preg_match("/$pat/im", $text, $mm, PREG_OFFSET_CAPTURE, $begin_off)) { + $end_off = $mm[0][1] - 1; + } + } + if (!isset($end_off)) { + if ($nr != 0) { + $pat = '^(={1,6})\s*[^\s\n=][^\n=]*\s*\1\s*$'; + } else { + $pat = '^(={1,' . $head_len . '})(?!=)\s*.*?\1\s*$'; + } + if (preg_match("/$pat/im", $text, $mm, PREG_OFFSET_CAPTURE, $begin_off)) { + $end_off = $mm[0][1] - 1; + } elseif ($sec == '') { + $end_off = -1; + } + } + + $nhead = self::countHeadings($text, $begin_off); + wfDebug("LSTH: head offset = $nhead"); + + if (isset($end_off)) { + if ($end_off == -1) { + return $output; + } + $piece = substr($text, $begin_off, $end_off - $begin_off); + if ($sec == '') { + $continueSearch = false; + } else { + $text = substr($text, $end_off); + } + } else { + $piece = substr($text, $begin_off); + $continueSearch = false; + } + + if ($nr > 1) { + // skip until we reach the n-th section + $nr--; + continue; + } + + if (isset($m[0][0])) { + $sectionHeading[$n] = $headLine; + //$sectionHeading[$n]=preg_replace("/^=+\s*/","",$m[0][0]); + //$sectionHeading[$n]=preg_replace("/\s*=+\s*$/","",$sectionHeading[$n]); + } else { + // $sectionHeading[$n] = ''; + $sectionHeading[0] = $headLine; + } + + if ($nr == 1) { + // output n-th section and done + $output[0] = self::parse($parser, $title, $piece, "#lsth:${page}|${sec}", $nhead, $recursionCheck, $maxLength, $link, $trim, $skipPattern); + break; + } + if ($nr == -1) { + if (!isset($end_off)) { + // output last section and done + $output[0] = self::parse($parser, $title, $piece, "#lsth:${page}|${sec}", $nhead, $recursionCheck, $maxLength, $link, $trim, $skipPattern); + break; + } + } else { + // output section by name and continue search for another section with the same name + $output[$n++] = self::parse($parser, $title, $piece, "#lsth:${page}|${sec}", $nhead, $recursionCheck, $maxLength, $link, $trim, $skipPattern); + } + } while ($continueSearch); + + return $output; + } + + // template inclusion - find the place(s) where template1 is called, + // replace its name by template2, then expand template2 and return the result + // we return an array containing all occurences of the template call which match the condition "$mustMatch" + // and do NOT match the condition "$mustNotMatch" (if specified) + // we use a callback function to format retrieved parameters, accessible via $lister->formatTemplateArg() + public static function includeTemplate($parser, Lister $lister, $dplNr, $article, $template1 = '', $template2 = '', $defaultTemplate, $mustMatch, $mustNotMatch, $matchParsed, $catlist) { + $page = $article->mTitle->getPrefixedText(); + $date = $article->myDate; + $user = $article->mUserLink; + $title = \Title::newFromText($page); + /* get text and throw away html comments */ + $text = preg_replace('//s', '', $parser->fetchTemplate($title)); + + if ($template1 != '' && $template1[0] == '#') { + // --------------------------------------------- looking for a parser function call + $template1 = substr($template1, 1); + $template2 = substr($template2, 1); + $defaultTemplate = substr($defaultTemplate, 1); + // when looking for parser function calls we accept regexp search patterns + $text2 = preg_replace("/\{\{\s*#(" . $template1 . ')(\s*[:}])/i', '°³²|%PFUNC%=\1\2|', $text); + $tCalls = preg_split('/°³²/', ' ' . $text2); + foreach ($tCalls as $i => $tCall) { + if (($n = strpos($tCall, ':')) !== false) { + $tCalls[$i][$n] = ' '; + } + } + } elseif ($template1 != '' && $template1[0] == '~') { + // --------------------------------------------- looking for an xml-tag extension call + $template1 = substr($template1, 1); + $template2 = substr($template2, 1); + $defaultTemplate = substr($defaultTemplate, 1); + // looking for tags + $text2 = preg_replace('/\<\s*(' . $template1 . ')\s*\>/i', '°³²|%TAG%=\1|%TAGBODY%=', $text); + $tCalls = preg_split('/°³²/', ' ' . $text2); + foreach ($tCalls as $i => $tCall) { + $tCalls[$i] = preg_replace('/\<\s*\/' . $template1 . '\s*\>.*/is', '}}', $tCall); + } + } else { + // --------------------------------------------- looking for template call + // we accept plain text as a template name, space or underscore are the same + // the localized name for "Template:" may preceed the template name + // the name may start with a different namespace for the surrogate template, followed by :: + global $wgContLang; + $nsNames = $wgContLang->getNamespaces(); + $tCalls = preg_split('/\{\{\s*(Template:|' . $nsNames[10] . ':)?' . self::spaceOrUnderscore(preg_quote($template1, '/')) . '\s*[|}]/i', ' ' . $text); + // We restore the first separator symbol (we had to include that symbol into the SPLIT, because we must make + // sure that we only accept exact matches of the complete template name + // (e.g. when looking for "foo" we must not accept "foo xyz") + foreach ($tCalls as $nr => $tCall) { + if ($tCall[0] == '}') { + $tCalls[$nr] = '}' . $tCall; + } else { + $tCalls[$nr] = '|' . $tCall; + } + } + } + + $output = []; + $extractParm = []; + + // check if we want to extract parameters directly from the call + // in that case we won´t invoke template2 but will directly return the extracted parameters + // as a sequence of table columns; + if (strlen($template2) > strlen($template1) && substr($template2, 0, strlen($template1) + 1) == ($template1 . ':')) { + $extractParm = preg_split('/:\s*/s', trim(substr($template2, strlen($template1) + 1))); + } + + if (count($tCalls) <= 1) { + // template was not called (note that count will be 1 if there is no template invocation) + if (count($extractParm) > 0) { + // if parameters are required directly: return empty columns + if (count($extractParm) > 1) { + $output[0] = $lister->formatTemplateArg('', $dplNr, 0, true, -1, $article); + for ($i = 1; $i < count($extractParm); $i++) { + $output[0] .= "\n|" . $lister->formatTemplateArg('', $dplNr, $i, true, -1, $article); + } + } else { + $output[0] = $lister->formatTemplateArg('', $dplNr, 0, true, -1, $article); + } + } else { + // put a red link into the output + $output[0] = $parser->preprocess('{{' . $defaultTemplate . '|%PAGE%=' . $page . '|%TITLE%=' . $title->getText() . '|%DATE%=' . $date . '|%USER%=' . $user . '}}', $parser->mTitle, $parser->mOptions); + } + unset($title); + return $output; + } + + $output[0] = ''; + $n = -2; + // loop for all template invocations + $firstCall = true; + foreach ($tCalls as $iii => $tCall) { + if ($n == -2) { + $n++; + continue; + } + $c = $tCall[0]; + // normally we construct a call for template2 with the parameters of template1 + if (count($extractParm) == 0) { + // find the end of the call: bracket level must be zero + $cbrackets = 0; + $templateCall = '{{' . $template2 . $tCall; + $size = strlen($templateCall); + + for ($i = 0; $i < $size; $i++) { + $c = $templateCall[$i]; + if ($c == '{') { + $cbrackets++; + } + if ($c == '}') { + $cbrackets--; + } + if ($cbrackets == 0) { + // if we must match a condition: test against it + if (($mustMatch == '' || preg_match($mustMatch, substr($templateCall, 0, $i - 1))) && ($mustNotMatch == '' || !preg_match($mustNotMatch, substr($templateCall, 0, $i - 1)))) { + $invocation = substr($templateCall, 0, $i - 1); + $argChain = $invocation . '|%PAGE%=' . $page . '|%TITLE%=' . $title->getText(); + if ($catlist != '') { + $argChain .= "|%CATLIST%=$catlist"; + } + $argChain .= '|%DATE%=' . $date . '|%USER%=' . $user . '|%ARGS%=' . str_replace('|', '§', preg_replace('/[}]+/', '}', preg_replace('/[{]+/', '{', substr($invocation, strlen($template2) + 2)))) . '}}'; + $output[++$n] = $parser->preprocess($argChain, $parser->mTitle, $parser->mOptions); + } + break; + } + } + } else { + // if the user wants parameters directly from the call line of template1 we return just those + $cbrackets = 2; + $templateCall = $tCall; + $size = strlen($templateCall); + $parms = []; + $parm = ''; + $hasParm = false; + + for ($i = 0; $i < $size; $i++) { + $c = $templateCall[$i]; + if ($c == '{' || $c == '[') { + $cbrackets++; // we count both types of brackets + } + if ($c == '}' || $c == ']') { + $cbrackets--; + } + if ($cbrackets == 2 && $c == '|') { + $parms[] = trim($parm); + $hasParm = true; + $parm = ''; + } else { + $parm .= $c; + } + if ($cbrackets == 0) { + if ($hasParm) { + $parms[] = trim(substr($parm, 0, strlen($parm) - 2)); + } + array_splice($parms, 0, 1); // remove artifact; + // if we must match a condition: test against it + $callText = substr($templateCall, 0, $i - 1); + if (($mustMatch == '' || (($matchParsed && preg_match($mustMatch, $parser->recursiveTagParse($callText))) || (!$matchParsed && preg_match($mustMatch, $callText)))) && ($mustNotMatch == '' || (($matchParsed && !preg_match($mustNotMatch, $parser->recursiveTagParse($callText))) || (!$matchParsed && !preg_match($mustNotMatch, $callText))))) { + $output[++$n] = ''; + $second = false; + foreach ($extractParm as $exParmKey => $exParm) { + $maxlen = -1; + if (($limpos = strpos($exParm, '[')) > 0 && $exParm[strlen($exParm) - 1] == ']') { + $maxlen = intval(substr($exParm, $limpos + 1, strlen($exParm) - $limpos - 2)); + $exParm = substr($exParm, 0, $limpos); + } + if ($second) { + if ($output[$n] == '' || $output[$n][strlen($output[$n]) - 1] != "\n") { + $output[$n] .= "\n"; + } + $output[$n] .= "|"; // \n"; + } + $found = false; + // % in parameter name + if (strpos($exParm, '%') !== false) { + // %% is a short form for inclusion of %PAGE% and %TITLE% + $found = true; + $output[$n] .= $lister->formatTemplateArg($exParm, $dplNr, $exParmKey, $firstCall, $maxlen, $article); + } + if (!$found) { + // named parameter + $exParmQuote = str_replace('/', '\/', $exParm); + foreach ($parms as $parm) { + if (!preg_match("/^\s*$exParmQuote\s*=/", $parm)) { + continue; + } + $found = true; + $output[$n] .= $lister->formatTemplateArg(preg_replace("/^$exParmQuote\s*=\s*/", "", $parm), $dplNr, $exParmKey, $firstCall, $maxlen, $article); + break; + } + } + if (!$found && is_numeric($exParm) && intval($exParm) == $exParm) { + // numeric parameter + $np = 0; + foreach ($parms as $parm) { + if (strstr($parm, '=') === false) { + ++$np; + } + if ($np != $exParm) { + continue; + } + $found = true; + $output[$n] .= $lister->formatTemplateArg($parm, $dplNr, $exParmKey, $firstCall, $maxlen, $article); + break; + } + } + if (!$found) { + $output[$n] .= $lister->formatTemplateArg('', $dplNr, $exParmKey, $firstCall, $maxlen, $article); + } + $second = true; + } + } + break; + } + } + } + $firstCall = false; + } + + return $output; + } + + public static function spaceOrUnderscore($pattern) { + // returns a pettern that matches underscores as well as spaces + return str_replace(' ', '[ _]', $pattern); + } +} diff --git a/classes/Logger.php b/classes/Logger.php new file mode 100644 index 0000000..3891cd6 --- /dev/null +++ b/classes/Logger.php @@ -0,0 +1,80 @@ +buffer; + if ($clearBuffer === true) { + $this->buffer = []; + } + return $buffer; + } + + /** + * Get a message, with optional parameters + * Parameters from user input must be escaped for HTML *before* passing to this function + * + * @access public + * @param integer Message ID + * @return string + */ + public function msg() { + $args = func_get_args(); + $errorId = array_shift($args); + $errorLevel = floor($errorId / 1000); + $errorMessageId = $errorId % 1000; + if (\DynamicPageListHooks::getDebugLevel() >= $errorLevel) { + if (\DynamicPageListHooks::isLikeIntersection()) { + if ($errorId == \DynamicPageListHooks::FATAL_TOOMANYCATS) { + $text = wfMessage('intersection_toomanycats', $args)->text(); + } elseif ($errorId == \DynamicPageListHooks::FATAL_TOOFEWCATS) { + $text = wfMessage('intersection_toofewcats', $args)->text(); + } elseif ($errorId == \DynamicPageListHooks::WARN_NORESULTS) { + $text = wfMessage('intersection_noresults', $args)->text(); + } elseif ($errorId == \DynamicPageListHooks::FATAL_NOSELECTION) { + $text = wfMessage('intersection_noincludecats', $args)->text(); + } + } + if (empty($text)) { + $text = wfMessage('dpl_log_' . $errorMessageId, $args)->text(); + } + $this->buffer[] = '

Extension:DynamicPageList (DPL), version ' . DPL_VERSION . ': ' . $text . '

'; + } + return false; + } +} diff --git a/classes/Parameters.php b/classes/Parameters.php new file mode 100644 index 0000000..844402d --- /dev/null +++ b/classes/Parameters.php @@ -0,0 +1,1332 @@ +setDefaults(); + } + + /** + * Handle simple parameter functions. + * + * @access public + * @param string Function(Parameter) Called + * @param string Function Arguments + * @return boolean Successful + */ + public function __call($parameter, $arguments) { + $parameterData = $this->getData($parameter); + + if ($parameterData === false) { + return false; + } + + //Check permission to use this parameter. + if (array_key_exists('permission', $parameterData)) { + global $wgUser; + if (!$wgUser->isAllowed($parameterData['permission'])) { + throw new \PermissionsError($parameterData['permission']); + return; + } + } + + //Subvert to the real function if it exists. This keeps code elsewhere clean from needed to check if it exists first. + $function = "_" . $parameter; + $this->parametersProcessed[$parameter] = true; + if (method_exists($this, $function)) { + return call_user_func_array([$this, $function], $arguments); + } + $option = $arguments[0]; + $parameter = strtolower($parameter); + + //Assume by default that these simple parameter options should not failed, but if they do we will set $success to false below. + $success = true; + if ($parameterData !== false) { + //If a parameter specifies options then enforce them. + if (array_key_exists('values', $parameterData) && is_array($parameterData['values']) === true && !in_array(strtolower($option), $parameterData['values'])) { + $success = false; + } else { + if ((array_key_exists('preserve_case', $parameterData) && !$parameterData['preserve_case']) && (array_key_exists('page_name_list', $parameterData) && $parameterData['page_name_list'] !== true)) { + $option = strtolower($option); + } + } + + //Strip tag. + if (array_key_exists('strip_html', $parameterData) && $parameterData['strip_html'] === true) { + $option = $this->stripHtmlTags($option); + } + + //Simple integer intval(). + if (array_key_exists('integer', $parameterData) && $parameterData['integer'] === true) { + if (!is_numeric($option)) { + if ($parameterData['default'] !== null) { + $option = intval($parameterData['default']); + } else { + $success = false; + } + } else { + $option = intval($option); + } + } + + //Booleans + if (array_key_exists('boolean', $parameterData) && $parameterData['boolean'] === true) { + $option = $this->filterBoolean($option); + if ($option === null) { + $success = false; + } + } + + //Timestamps + if (array_key_exists('timestamp', $parameterData) && $parameterData['timestamp'] === true) { + $option = strtolower($option); + switch ($option) { + case 'today': + case 'last hour': + case 'last day': + case 'last week': + case 'last month': + case 'last year': + break; + default: + $option = str_pad(preg_replace('#[^0-9]#', '', $option), 14, '0'); + $option = wfTimestamp(TS_MW, $option); + + if ($option === false) { + $success = false; + } + break; + } + } + + //List of Pages + if (array_key_exists('page_name_list', $parameterData) && $parameterData['page_name_list'] === true) { + $pageGroups = $this->getParameter($parameter); + if (!is_array($pageGroups)) { + $pageGroups = []; + } + $pages = $this->getPageNameList($option, (bool)$parameterData['page_name_must_exist']); + if ($pages === false) { + $success = false; + } else { + $pageGroups[] = $pages; + $option = $pageGroups; + } + } + + //Regex Pattern Matching + if (array_key_exists('pattern', $parameterData)) { + if (preg_match($parameterData['pattern'], $option, $matches)) { + //Nuke the total pattern match off the beginning of the array. + array_shift($matches); + $option = $matches; + } else { + $success = false; + } + } + + //Database Key Formatting + if (array_key_exists('db_format', $parameterData) && $parameterData['db_format'] === true) { + $option = str_replace(' ', '_', $option); + } + + //If none of the above checks marked this as a failure then set it. + if ($success === true) { + $this->setParameter($parameter, $option); + + //Set that criteria was found for a selection. + if (array_key_exists('set_criteria_found', $parameterData) && $parameterData['set_criteria_found'] === true) { + $this->setSelectionCriteriaFound(true); + } + + //Set open references conflict possibility. + if (array_key_exists('open_ref_conflict', $parameterData) && $parameterData['open_ref_conflict'] === true) { + $this->setOpenReferencesConflict(true); + } + } + } + return $success; + } + + /** + * Sort cleaned parameter arrays by priority. + * Users can not be told to put the parameters into a specific order each time. Some parameters are dependent on each other coming in a certain order due to some procedural legacy issues. + * + * @access public + * @param array Unsorted Parameters + * @return array Sorted Parameters + */ + public static function sortByPriority($parameters) { + if (!is_array($parameters)) { + throw new \MWException(__METHOD__ . ': A non-array was passed.'); + } + //'category' to get category headings first for ordermethod. + //'include'/'includepage' to make sure section labels are ready for 'table'. + $priority = [ + 'distinct' => 1, + 'openreferences' => 2, + 'ignorecase' => 3, + 'category' => 4, + 'title' => 5, + 'goal' => 6, + 'ordercollation' => 7, + 'ordermethod' => 8, + 'includepage' => 9, + 'include' => 10 + ]; + + $_first = []; + foreach ($priority as $parameter => $order) { + if (isset($parameters[$parameter])) { + $_first[$parameter] = $parameters[$parameter]; + unset($parameters[$parameter]); + } + } + $parameters = $_first + $parameters; + + return $parameters; + } + + /** + * Set Selection Criteria Found + * + * @access public + * @param boolean Is Found? + * @return void + */ + private function setSelectionCriteriaFound($found = true) { + if (!is_bool($found)) { + throw new MWException(__METHOD__ . ': A non-boolean was passed.'); + } + $this->selectionCriteriaFound = $found; + } + + /** + * Get Selection Criteria Found + * + * @access public + * @return boolean Is Conflict? + */ + public function isSelectionCriteriaFound() { + return $this->selectionCriteriaFound; + } + + /** + * Set Open References Conflict - See 'openreferences' parameter. + * + * @access public + * @param boolean References Conflict? + * @return void + */ + private function setOpenReferencesConflict($conflict = true) { + if (!is_bool($conflict)) { + throw new MWException(__METHOD__ . ': A non-boolean was passed.'); + } + $this->openReferencesConflict = $conflict; + } + + /** + * Get Open References Conflict - See 'openreferences' parameter. + * + * @access public + * @return boolean Is Conflict? + */ + public function isOpenReferencesConflict() { + return $this->openReferencesConflict; + } + + /** + * Set default parameters based on ParametersData. + * + * @access private + * @return void + */ + private function setDefaults() { + $this->setParameter('defaulttemplatesuffix', '.default'); + + $parameters = $this->getParametersForRichness(); + foreach ($parameters as $parameter) { + if ($this->getData($parameter)['default'] !== null && !($this->getData($parameter)['default'] === false && $this->getData($parameter)['boolean'] === true)) { + if ($parameter == 'debug') { + \DynamicPageListHooks::setDebugLevel($this->getData($parameter)['default']); + } + $this->setParameter($parameter, $this->getData($parameter)['default']); + } + } + } + + /** + * Set a parameter's option. + * + * @access public + * @param string Parameter to set + * @param mixed Option to set + * @return void + */ + public function setParameter($parameter, $option) { + $this->parameterOptions[$parameter] = $option; + } + + /** + * Get a parameter's option. + * + * @access public + * @param string Parameter to get + * @return mixed Option for specified parameter. + */ + public function getParameter($parameter) { + return array_key_exists($parameter, $this->parameterOptions) ? $this->parameterOptions[$parameter] : null; + } + + /** + * Get all parameters. + * + * @access public + * @return array Parameter => Options + */ + public function getAllParameters() { + return self::sortByPriority($this->parameterOptions); + } + + /** + * Filter a standard boolean like value into an actual boolean. + * + * @access public + * @param mixed Integer or string to evaluated through filter_var(). + * @return bool + */ + public function filterBoolean($boolean) { + return filter_var($boolean, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); + } + + /** + * Strip tags. + * + * @access private + * @param string Dirty Text + * @return string Clean Text + */ + private function stripHtmlTags($text) { + $text = preg_replace("#<.*?html.*?>#is", "", $text); + + return $text; + } + + /** + * Get a list of valid page names. + * + * @access private + * @param string Raw Text of Pages + * @param boolean [Optional] Each Title MUST Exist + * @return mixed List of page titles or false on error. + */ + private function getPageNameList($text, $mustExist = true) { + $list = []; + $pages = explode('|', trim($text)); + foreach ($pages as $page) { + $page = trim($page); + $page = rtrim($page, '\\'); //This was fixed from the original code, but I am not sure what its intended purpose was. + if (empty($page)) { + continue; + } + if ($mustExist === true) { + $title = \Title::newFromText($page); + if (!$title) { + return false; + } + $list[] = $title; + } else { + $list[] = $page; + } + } + + return $list; + } + + /** + * Check if a regular expression is valid. + * + * @access private + * @param mixed Regular Expression(s) in an array or a single expression in a string. + * @param boolean Is this a database REGEXP? + * @return boolean + */ + private function isRegexValid($regexes, $forDb = false) { + if (!is_array($regexes)) { + $regexes = [$regexes]; + } + + foreach ($regexes as $regex) { + if (empty(trim($regex))) { + continue; + } + if ($forDb) { + $regex = '#' . str_replace('#', '\#', $regex) . '#'; + } + //Purposely silencing the errors here since we are testing if preg_match would throw an error due to a bad regex from user input. + if (@preg_match($regex, null) === false) { + return false; + } + } + + return true; + } + + /** + * Clean and test 'category' parameter. + * + * @access public + * @param string Options passed to parameter. + * @return boolean Success + */ + public function _category($option) { + $option = trim($option); + if (empty($option)) { + return false; + } + // Init array of categories to include + $categories = []; + $heading = false; + $notHeading = false; + if (substr($option, 0, 1) == '+') { // categories are headings + $heading = true; + $option = ltrim($option, '+'); + } + if (substr($option, 0, 1) == '-') { // categories are NOT headings + $notHeading = true; + $option = ltrim($option, '-'); + } + + //We expand html entities because they contain an '& 'which would be interpreted as an AND condition + $option = html_entity_decode($option, ENT_QUOTES); + if (strpos($option, '|') !== false) { + $parameters = explode('|', $option); + $operator = 'OR'; + } else { + $parameters = explode('&', $option); + $operator = 'AND'; + } + foreach ($parameters as $parameter) { + $parameter = trim($parameter); + if ($parameter === '_none_' || $parameter === '') { + $this->setParameter('includeuncat', true); + $categories[] = ''; + } elseif (!empty($parameter)) { + if (strpos($parameter, '*') === 0 && strlen($parameter) >= 2) { + if (strpos($parameter, '*', 1) === 1) { + $parameter = substr($parameter, 2); + $subCategories = Query::getSubcategories($parameter, 2); + } else { + $parameter = substr($parameter, 1); + $subCategories = Query::getSubcategories($parameter, 1); + } + $subCategories[] = $parameter; + foreach ($subCategories as $subCategory) { + $title = \Title::newFromText($subCategory); + if (!is_null($title)) { + //The * helper is just like listing "Category1|SubCategory1". This gets hard coded here for this purpose. + $categories['OR'][] = $title->getDbKey(); + } + } + } else { + $title = \Title::newFromText($parameter); + if (!is_null($title)) { + $categories[$operator][] = $title->getDbKey(); + } + } + } + } + if (!empty($categories)) { + $data = $this->getParameter('category'); + //Do a bunch of data integrity checks to avoid E_NOTICE. + if (!is_array($data)) { + $data = []; + } + if (!array_key_exists('=', $data) || !is_array($data['='])) { + $data['='] = []; + } + foreach ($categories as $_operator => $_categories) { + if (!array_key_exists($_operator, $data['=']) || !is_array($data['='][$_operator])) { + $data['='][$_operator] = []; + } + $data['='][$_operator][] = $_categories; + } + $this->setParameter('category', $data); + if ($heading) { + $this->setParameter('catheadings', array_unique(array_merge((is_array($this->getParameter('catheadings')) ? $this->getParameter('catheadings') : []), $categories))); + } + if ($notHeading) { + $this->setParameter('catnotheadings', array_unique(array_merge((is_array($this->getParameter('catnotheadings')) ? $this->getParameter('catnotheadings') : []), $categories))); + } + $this->setOpenReferencesConflict(true); + return true; + } + return false; + } + + /** + * Clean and test 'categoryregexp' parameter. + * + * @access public + * @param string Options passed to parameter. + * @return boolean Success + */ + public function _categoryregexp($option) { + if (!$this->isRegexValid($option, true)) { + return false; + } + + $data = $this->getParameter('category'); + //REGEXP input only supports AND operator. + $data['REGEXP']['AND'][] = [$option]; //Wrapped in an array since the category Query handler expects an array. + $this->setParameter('category', $data); + $this->setOpenReferencesConflict(true); + return true; + } + + /** + * Clean and test 'categorymatch' parameter. + * + * @access public + * @param string Options passed to parameter. + * @return boolean Success + */ + public function _categorymatch($option) { + if (strpos($option, '|') !== false) { + $newMatches = explode('|', $option); + $operator = 'OR'; + } else { + $newMatches = explode('&', $option); + $operator = 'AND'; + } + + $data = $this->getParameter('category'); + if (isset($data['LIKE']) && !is_array($data['LIKE'][$operator])) { + $data['LIKE'][$operator] = []; + } + + $data['LIKE'][$operator][] = $newMatches; + $this->setParameter('category', $data); + $this->setOpenReferencesConflict(true); + return true; + } + + /** + * Clean and test 'notcategory' parameter. + * + * @access public + * @param string Options passed to parameter. + * @return boolean Success + */ + public function _notcategory($option) { + $title = \Title::newFromText($option); + if (!is_null($title)) { + $data = $this->getParameter('notcategory'); + $data['='][] = $title->getDbKey(); + $this->setParameter('notcategory', $data); + $this->setOpenReferencesConflict(true); + return true; + } + return false; + } + + /** + * Clean and test 'notcategoryregexp' parameter. + * + * @access public + * @param string Options passed to parameter. + * @return boolean Success + */ + public function _notcategoryregexp($option) { + if (!$this->isRegexValid($option, true)) { + return false; + } + + $data = $this->getParameter('notcategory'); + $data['regexp'][] = $option; + $this->setParameter('notcategory', $data); + $this->setOpenReferencesConflict(true); + return true; + } + + /** + * Clean and test 'notcategorymatch' parameter. + * + * @access public + * @param string Options passed to parameter. + * @return boolean Success + */ + public function _notcategorymatch($option) { + $data = $this->getParameter('notcategory'); + if (!isset($data['like']) || !is_array($data['like'])) { + $data['like'] = []; + } + $newMatches = explode('|', $option); + $data['like'] = array_merge($data['like'], $newMatches); + $this->setParameter('notcategory', $data); + $this->setOpenReferencesConflict(true); + return true; + } + + /** + * Clean and test 'count' parameter. + * + * @access public + * @param string Options passed to parameter. + * @return boolean Success + */ + public function _count($option) { + if (!Config::getSetting('allowUnlimitedResults') && $option <= Config::getSetting('maxResultCount') && $option > 0) { + $this->setParameter('count', intval($option)); + return true; + } + return false; + } + + /** + * Clean and test 'namespace' parameter. + * + * @access public + * @param string Option passed to parameter. + * @return boolean Success + */ + public function _namespace($option) { + global $wgContLang; + $extraParams = explode('|', $option); + foreach ($extraParams as $parameter) { + $parameter = trim($parameter); + $namespaceId = $wgContLang->getNsIndex($parameter); + if ($namespaceId === false || (is_array(Config::getSetting('allowedNamespaces')) && !in_array($parameter, Config::getSetting('allowedNamespaces')))) { + //Let the user know this namespace is not allowed or does not exist. + return false; + } + $data = $this->getParameter('namespace'); + $data[] = $namespaceId; + $data = array_unique($data); + $this->setParameter('namespace', $data); + $this->setSelectionCriteriaFound(true); + } + return true; + } + + /** + * Clean and test 'notnamespace' parameter. + * + * @access public + * @param string Options passed to parameter. + * @return boolean Success + */ + public function _notnamespace($option) { + global $wgContLang; + $extraParams = explode('|', $option); + foreach ($extraParams as $parameter) { + $parameter = trim($parameter); + $namespaceId = $wgContLang->getNsIndex($parameter); + if ($namespaceId === false) { + //Let the user know this namespace is not allowed or does not exist. + return false; + } + $data = $this->getParameter('notnamespace'); + $data[] = $namespaceId; + $data = array_unique($data); + $this->setParameter('notnamespace', $data); + $this->setSelectionCriteriaFound(true); + } + return true; + } + + /** + * Clean and test 'openreferences' parameter. + * + * @access public + * @param string Options passed to parameter. + * @return boolean Success + */ + public function _openreferences($option) { + $option = $this->filterBoolean($option); + if ($option === null) { + return false; + } + + //Force 'ordermethod' back to none. + $this->setParameter('ordermethod', ['none']); + $this->setParameter('openreferences', $option); + + return true; + } + + /** + * Clean and test 'ordermethod' parameter. + * + * @access public + * @param string Options passed to parameter. + * @return boolean Success + */ + public function _ordermethod($option) { + $methods = explode(',', $option); + $success = true; + foreach ($methods as $method) { + if (!in_array($method, $this->getData('ordermethod')['values'])) { + return false; + } + } + + $this->setParameter('ordermethod', $methods); + if ($methods[0] !== 'none') { + $this->setOpenReferencesConflict(true); + } + + return true; + } + + /** + * Clean and test 'mode' parameter. + * + * @access public + * @param string Options passed to parameter. + * @return boolean Success + */ + public function _mode($option) { + if (in_array($option, $this->getData('mode')['values'])) { + //'none' mode is implemented as a specific submode of 'inline' with
as inline text + if ($option == 'none') { + $this->setParameter('mode', 'inline'); + $this->setParameter('inlinetext', '
'); + } elseif ($option == 'userformat') { + // userformat resets inline text to empty string + $this->setParameter('inlinetext', ''); + $this->setParameter('mode', $option); + } else { + $this->setParameter('mode', $option); + } + return true; + } else { + return false; + } + } + + /** + * Clean and test 'distinct' parameter. + * + * @access public + * @param string Options passed to parameter. + * @return boolean Success + */ + public function _distinct($option) { + $boolean = $this->filterBoolean($option); + if ($option == 'strict') { + $this->setParameter('distinctresultset', 'strict'); + } elseif ($boolean !== null) { + $this->setParameter('distinctresultset', $boolean); + } else { + return false; + } + return true; + } + + /** + * Clean and test 'ordercollation' parameter. + * + * @access public + * @param string Options passed to parameter. + * @return boolean Success + */ + public function _ordercollation($option) { + if ($option == 'bridge') { + $this->setParameter('ordersuitsymbols', true); + } elseif (!empty($option)) { + $this->setParameter('ordercollation', $option); + } else { + return false; + } + return true; + } + + /** + * Short cut to _format(); + * + * @access public + * @return mixed + */ + public function _listseparators() { + return call_user_func_array([$this, '_format'], func_get_args()); + } + + /** + * Clean and test 'format' parameter. + * + * @access public + * @param string Options passed to parameter. + * @return boolean Success + */ + public function _format($option) { + //Parsing of wikitext will happen at the end of the output phase. Replace '\n' in the input by linefeed because wiki syntax depends on linefeeds. + $option = $this->stripHtmlTags($option); + $option = Parse::replaceNewLines($option); + $this->setParameter('listseparators', explode(',', $option, 4)); + //Set the 'mode' parameter to userformat automatically. + $this->setParameter('mode', 'userformat'); + $this->setParameter('inlinetext', ''); + return true; + } + + /** + * Clean and test 'title' parameter. + * + * @access public + * @param string Options passed to parameter. + * @return boolean Success + */ + public function _title($option) { + $title = \Title::newFromText($option); + if ($title) { + $data = $this->getParameter('title'); + $data['='][] = str_replace(' ', '_', $title->getText()); + $this->setParameter('title', $data); + + $data = $this->getParameter('namespace'); + $data[] = $title->getNamespace(); + $data = array_unique($data); + $this->setParameter('namespace', $data); + + $this->setParameter('mode', 'userformat'); + $this->setSelectionCriteriaFound(true); + $this->setOpenReferencesConflict(true); + return true; + } + return false; + } + + /** + * Clean and test 'titleregexp' parameter. + * + * @access public + * @param string Options passed to parameter. + * @return boolean Success + */ + public function _titleregexp($option) { + $data = $this->getParameter('title'); + if (!is_array($data['regexp'])) { + $data['regexp'] = []; + } + $newMatches = explode('|', str_replace(' ', '\_', $option)); + + if (!$this->isRegexValid($newMatches, true)) { + return false; + } + + $data['regexp'] = array_merge($data['regexp'], $newMatches); + $this->setParameter('title', $data); + $this->setSelectionCriteriaFound(true); + return true; + } + + /** + * Clean and test 'titlematch' parameter. + * + * @access public + * @param string Options passed to parameter. + * @return boolean Success + */ + public function _titlematch($option) { + $data = $this->getParameter('title'); + if (!is_array($data['like'])) { + $data['like'] = []; + } + $newMatches = explode('|', str_replace(' ', '\_', $option)); + $data['like'] = array_merge($data['like'], $newMatches); + $this->setParameter('title', $data); + $this->setSelectionCriteriaFound(true); + return true; + } + + /** + * Clean and test 'nottitleregexp' parameter. + * + * @access public + * @param string Options passed to parameter. + * @return boolean Success + */ + public function _nottitleregexp($option) { + $data = $this->getParameter('nottitle'); + if (!is_array($data['regexp'])) { + $data['regexp'] = []; + } + $newMatches = explode('|', str_replace(' ', '\_', $option)); + $data['regexp'] = array_merge($data['regexp'], $newMatches); + + if (!$this->isRegexValid($newMatches, true)) { + return false; + } + + $this->setParameter('nottitle', $data); + $this->setSelectionCriteriaFound(true); + return true; + } + + /** + * Clean and test 'nottitlematch' parameter. + * + * @access public + * @param string Options passed to parameter. + * @return boolean Success + */ + public function _nottitlematch($option) { + $data = $this->getParameter('nottitle'); + if (!is_array($data['like'])) { + $data['like'] = []; + } + $newMatches = explode('|', str_replace(' ', '\_', $option)); + $data['like'] = array_merge($data['like'], $newMatches); + $this->setParameter('nottitle', $data); + $this->setSelectionCriteriaFound(true); + return true; + } + + /** + * Clean and test 'scroll' parameter. + * + * @access public + * @param string Options passed to parameter. + * @return boolean Success + */ + public function _scroll($option) { + $option = $this->filterBoolean($option); + $this->setParameter('scroll', $option); + //If scrolling is active we adjust the values for certain other parameters based on URL arguments + if ($option === true) { + global $wgRequest; + + //The 'findTitle' option has argument over the 'fromTitle' argument. + $titlegt = $wgRequest->getVal('DPL_findTitle', ''); + if (!empty($titlegt)) { + $titlegt = '=_' . ucfirst($titlegt); + } else { + $titlegt = $wgRequest->getVal('DPL_fromTitle', ''); + $titlegt = ucfirst($titlegt); + } + $this->setParameter('titlegt', str_replace(' ', '_', $titlegt)); + + //Lets get the 'toTitle' argument. + $titlelt = $wgRequest->getVal('DPL_toTitle', ''); + $titlelt = ucfirst($titlelt); + $this->setParameter('titlelt', str_replace(' ', '_', $titlelt)); + + //Make sure the 'scrollDir' arugment is captured. This is mainly used for the Variables extension and in the header/footer replacements. + $this->setParameter('scrolldir', $wgRequest->getVal('DPL_scrollDir', '')); + + //Also set count limit from URL if not otherwise set. + $this->_count($wgRequest->getInt('DPL_count')); + } + //We do not return false since they could have just left it out. Who knows why they put the parameter in the list in the first place. + return true; + } + + /** + * Clean and test 'replaceintitle' parameter. + * + * @access public + * @param string Options passed to parameter. + * @return boolean Success + */ + public function _replaceintitle($option) { + //We offer a possibility to replace some part of the title + $replaceInTitle = explode(',', $option, 2); + if (isset($replaceInTitle[1])) { + $replaceInTitle[1] = $this->stripHtmlTags($replaceInTitle[1]); + } + + $this->setParameter('replaceintitle', $replaceInTitle); + + return true; + } + + /** + * Clean and test 'debug' parameter. + * + * @access public + * @param string Options passed to parameter. + * @return boolean Success + */ + public function _debug($option) { + if (in_array($option, $this->getData('debug')['values'])) { + \DynamicPageListHooks::setDebugLevel($option); + } else { + return false; + } + + return true; + } + + /** + * Short cut to _include(); + * + * @access public + * @return mixed + */ + public function _includepage() { + return call_user_func_array([$this, '_include'], func_get_args()); + } + + /** + * Clean and test 'include' parameter. + * + * @access public + * @param string Options passed to parameter. + * @return boolean Success + */ + public function _include($option) { + if (!empty($option)) { + $this->setParameter('incpage', true); + $this->setParameter('seclabels', explode(',', $option)); + } else { + return false; + } + return true; + } + + /** + * Clean and test 'includematch' parameter. + * + * @access public + * @param string Options passed to parameter. + * @return boolean Success + */ + public function _includematch($option) { + $regexes = explode(',', $option); + + if (!$this->isRegexValid($regexes)) { + return false; + } + + $this->setParameter('seclabelsmatch', $regexes); + return true; + } + + /** + * Clean and test 'includematchparsed' parameter. + * + * @access public + * @param string Options passed to parameter. + * @return boolean Success + */ + public function _includematchparsed($option) { + $regexes = explode(',', $option); + + if (!$this->isRegexValid($regexes)) { + return false; + } + + $this->setParameter('incparsed', true); + $this->setParameter('seclabelsmatch', $regexes); + return true; + } + + /** + * Clean and test 'includenotmatch' parameter. + * + * @access public + * @param string Options passed to parameter. + * @return boolean Success + */ + public function _includenotmatch($option) { + $regexes = explode(',', $option); + + if (!$this->isRegexValid($regexes)) { + return false; + } + + $this->setParameter('seclabelsnotmatch', $regexes); + return true; + } + + /** + * Clean and test 'includenotmatchparsed' parameter. + * + * @access public + * @param string Options passed to parameter. + * @return boolean Success + */ + public function _includenotmatchparsed($option) { + $regexes = explode(',', $option); + + if (!$this->isRegexValid($regexes)) { + return false; + } + + $this->setParameter('incparsed', true); + $this->setParameter('seclabelsnotmatch', $regexes); + return true; + } + + /** + * Clean and test 'secseparators' parameter. + * + * @access public + * @param string Options passed to parameter. + * @return boolean Success + */ + public function _secseparators($option) { + //We replace '\n' by newline to support wiki syntax within the section separators + $this->setParameter('secseparators', explode(',', Parse::replaceNewLines($option))); + return true; + } + + /** + * Clean and test 'multisecseparators' parameter. + * + * @access public + * @param string Options passed to parameter. + * @return boolean Success + */ + public function _multisecseparators($option) { + //We replace '\n' by newline to support wiki syntax within the section separators + $this->setParameter('multisecseparators', explode(',', Parse::replaceNewLines($option))); + return true; + } + + /** + * Clean and test 'table' parameter. + * + * @access public + * @param string Options passed to parameter. + * @return boolean Success + */ + public function _table($option) { + $this->setParameter('defaulttemplatesuffix', ''); + $this->setParameter('mode', 'userformat'); + $this->setParameter('inlinetext', ''); + $withHLink = "[[%PAGE%|%TITLE%]]\n|"; + + foreach (explode(',', $option) as $tabnr => $tab) { + if ($tabnr == 0) { + if ($tab == '') { + $tab = 'class=wikitable'; + } + $listSeparators[0] = '{|' . $tab; + } else { + if ($tabnr == 1 && $tab == '-') { + $withHLink = ''; + continue; + } + if ($tabnr == 1 && $tab == '') { + $tab = wfMessage('article')->text(); + } + $listSeparators[0] .= "\n!{$tab}"; + } + } + $listSeparators[1] = ''; + // the user may have specified the third parameter of 'format' to add meta attributes of articles to the table + if (!array_key_exists(2, $listSeparators)) { + $listSeparators[2] = ''; + } + $listSeparators[3] = "\n|}"; + //Overwrite 'listseparators'. + $this->setParameter('listseparators', $listSeparators); + + $sectionLabels = (array)$this->getParameter('seclabels'); + $sectionSeparators = $this->getParameter('secseparators'); + $multiSectionSeparators = $this->getParameter('multisecseparators'); + for ($i = 0; $i < count($sectionLabels); $i++) { + if ($i == 0) { + $sectionSeparators[0] = "\n|-\n|" . $withHLink; //."\n"; + $sectionSeparators[1] = ''; + $multiSectionSeparators[0] = "\n|-\n|" . $withHLink; // ."\n"; + } else { + $sectionSeparators[2 * $i] = "\n|"; // ."\n"; + $sectionSeparators[2 * $i + 1] = ''; + if (is_array($sectionLabels[$i]) && $sectionLabels[$i][0] == '#') { + $multiSectionSeparators[$i] = "\n----\n"; + } + if ($sectionLabels[$i][0] == '#') { + $multiSectionSeparators[$i] = "\n----\n"; + } else { + $multiSectionSeparators[$i] = "
\n"; + } + } + } + //Overwrite 'secseparators' and 'multisecseparators'. + $this->setParameter('secseparators', $sectionSeparators); + $this->setParameter('multisecseparators', $multiSectionSeparators); + + $this->setParameter('table', Parse::replaceNewLines($option)); + return true; + } + + /** + * Clean and test 'tablerow' parameter. + * + * @access public + * @param string Options passed to parameter. + * @return boolean Success + */ + public function _tablerow($option) { + $option = Parse::replaceNewLines(trim($option)); + if (empty($option)) { + $this->setParameter('tablerow', []); + } else { + $this->setParameter('tablerow', explode(',', $option)); + } + return true; + } + + /** + * Clean and test 'allowcachedresults' parameter. + * This function is necessary for the custom 'yes+warn' option that sets 'warncachedresults'. + * + * @access public + * @param string Options passed to parameter. + * @return boolean Success + */ + public function _allowcachedresults($option) { + //If execAndExit was previously set (i.e. if it is not empty) we will ignore all cache settings which are placed AFTER the execandexit statement thus we make sure that the cache will only become invalid if the query is really executed. + if ($this->getParameter('execandexit') === null) { + if ($option === 'yes+warn') { + $this->setParameter('allowcachedresults', true); + $this->setParameter('warncachedresults', true); + return true; + } + $option = $this->filterBoolean($option); + if ($option !== null) { + $this->setParameter('allowcachedresults', $this->filterBoolean($option)); + } else { + return false; + } + } else { + $this->setParameter('allowcachedresults', false); + } + return true; + } + + /** + * Clean and test 'fixcategory' parameter. + * + * @access public + * @param string Options passed to parameter. + * @return boolean Success + */ + public function _fixcategory($option) { + \DynamicPageListHooks::fixCategory($option); + return true; + } + + /** + * Clean and test 'reset' parameter. + * + * @access public + * @param string Options passed to parameter. + * @return boolean Success + */ + public function _reset($option) { + $arguments = explode(',', $option); + $reset = []; + foreach ($arguments as $argument) { + $argument = trim($argument); + if (empty($argument)) { + continue; + } + + $values = $this->getData('reset')['values']; + if (!in_array($argument, $values)) { + return false; + } else { + if ($argument == 'all' || $argument == 'none') { + $boolean = ($argument == 'all' ? true : false); + $values = array_diff($values, ['all', 'none']); + $reset = array_flip($values); + foreach ($reset as $value => $key) { + $reset[$value] = $boolean; + } + } else { + $reset[$argument] = true; + } + } + } + $data = $this->getParameter('reset'); + $data = array_merge($data, $reset); + $this->setParameter('reset', $data); + return true; + } + + /** + * Clean and test 'eliminate' parameter. + * + * @access public + * @param string Options passed to parameter. + * @return boolean Success + */ + public function _eliminate($option) { + $arguments = explode(',', $option); + $eliminate = []; + foreach ($arguments as $argument) { + $argument = trim($argument); + if (empty($argument)) { + continue; + } + + $values = $this->getData('eliminate')['values']; + if (!in_array($argument, $values)) { + return false; + } else { + if ($argument == 'all' || $argument == 'none') { + $boolean = ($argument == 'all' ? true : false); + $values = array_diff($values, ['all', 'none']); + $eliminate = array_flip($values); + foreach ($eliminate as $value => $key) { + $eliminate[$value] = $boolean; + } + } else { + $eliminate[$argument] = true; + } + } + } + $data = $this->getParameter('eliminate'); + $data = array_merge($data, $eliminate); + $this->setParameter('eliminate', $data); + return true; + } +} diff --git a/classes/ParametersData.php b/classes/ParametersData.php new file mode 100644 index 0000000..8d9f386 --- /dev/null +++ b/classes/ParametersData.php @@ -0,0 +1,1379 @@ + [ + 'addfirstcategorydate', + 'category', + 'count', + 'hiddencategories', + 'mode', + 'namespace', + 'notcategory', + 'order', + 'ordermethod', + 'qualitypages', + 'redirects', + 'showcurid', + 'shownamespace', + 'stablepages', + 'suppresserrors' + ], + 1 => [ + 'allowcachedresults', + 'execandexit', + 'columns', + 'debug', + 'distinct', + 'escapelinks', + 'format', + 'inlinetext', + 'listseparators', + 'notnamespace', + 'offset', + 'oneresultfooter', + 'oneresultheader', + 'ordercollation', + 'noresultsfooter', + 'noresultsheader', + 'randomcount', + 'replaceintitle', + 'resultsfooter', + 'resultsheader', + 'rowcolformat', + 'rows', + 'rowsize', + 'scroll', + 'title', + 'titlelt', + 'titlegt', + 'titlemaxlength', + 'userdateformat' + ], + 2 => [ + 'addauthor', + 'addcategories', + 'addcontribution', + 'addeditdate', + 'addexternallink', + 'addlasteditor', + 'addpagecounter', + 'addpagesize', + 'addpagetoucheddate', + 'adduser', + 'cacheperiod', + 'categoriesminmax', + 'createdby', + 'dominantsection', + 'eliminate', + 'fixcategory', + 'headingcount', + 'headingmode', + 'hitemattr', + 'hlistattr', + 'ignorecase', + 'imagecontainer', + 'imageused', + 'include', + 'includematch', + 'includematchparsed', + 'includemaxlength', + 'includenotmatch', + 'includenotmatchparsed', + 'includepage', + 'includesubpages', + 'includetrim', + 'itemattr', + 'lastmodifiedby', + 'linksfrom', + 'linksto', + 'linkstoexternal', + 'listattr', + 'minoredits', + 'modifiedby', + 'multisecseparators', + 'notcreatedby', + 'notlastmodifiedby', + 'notlinksfrom', + 'notlinksto', + 'notmodifiedby', + 'notuses', + 'reset', + 'secseparators', + 'skipthispage', + 'table', + 'tablerow', + 'tablesortcol', + 'titlematch', + 'usedby', + 'uses' + ], + 3 => [ + 'allrevisionsbefore', + 'allrevisionssince', + 'articlecategory', + 'categorymatch', + 'categoryregexp', + 'firstrevisionsince', + 'lastrevisionbefore', + 'maxrevisions', + 'minrevisions', + 'notcategorymatch', + 'notcategoryregexp', + 'nottitlematch', + 'nottitleregexp', + 'openreferences', + 'titleregexp' + ], + 4 => [ + 'deleterules', + 'goal', + 'updaterules' + ] + ]; + + /** + * Map parameters to possible values. + * A 'default' key indicates the default value for the parameter. + * A 'pattern' key indicates a pattern for regular expressions (that the value must match). + * A 'values' key is the set of possible values. + * For some options (e.g. 'namespace'), possible values are not yet defined, but will be if necessary (for debugging). + * + * @var array + */ + private $data = [ + 'addauthor' => [ + 'default' => false, + 'boolean' => true, + 'open_ref_conflict' => true + ], + 'addcategories' => [ + 'default' => false, + 'boolean' => true, + 'open_ref_conflict' => true + ], + 'addcontribution' => [ + 'default' => false, + 'boolean' => true, + 'open_ref_conflict' => true + ], + 'addeditdate' => [ + 'default' => false, + 'boolean' => true, + 'open_ref_conflict' => true + ], + 'addexternallink' => [ + 'default' => false, + 'boolean' => true, + 'open_ref_conflict' => true + ], + 'addfirstcategorydate' => [ + 'default' => false, + 'boolean' => true, + 'open_ref_conflict' => true + ], + 'addlasteditor' => [ + 'default' => false, + 'boolean' => true, + 'open_ref_conflict' => true + ], + 'addpagecounter' => [ + 'default' => false, + 'boolean' => true, + 'open_ref_conflict' => true + ], + 'addpagesize' => [ + 'default' => false, + 'boolean' => true, + 'open_ref_conflict' => true + ], + 'addpagetoucheddate' => [ + 'default' => false, + 'boolean' => true, + 'open_ref_conflict' => true + ], + 'adduser' => [ + 'default' => false, + 'boolean' => true, + 'open_ref_conflict' => true + ], + + // default of allowcachedresults depends on behaveasIntersetion and on LocalSettings ... + 'allowcachedresults' => [ + 'default' => true, + 'boolean' => true + ], + /** + * search for a page with the same title in another namespace (this is normally the article to a talk page) + */ + 'articlecategory' => [ + 'default' => null, + 'db_format' => true + ], + + /** + * category= Cat11 | Cat12 | ... + * category= Cat21 | Cat22 | ... + * ... + * [Special value] catX='' (empty string without quotes) means pseudo-categoy of Uncategorized pages + * Means pages have to be in category (Cat11 OR (inclusive) Cat2 OR...) AND (Cat21 OR Cat22 OR...) AND... + * If '+' prefixes the list of categories (e.g. category=+ Cat1 | Cat 2 ...), only these categories can be used as headings in the DPL. See 'headingmode' param. + * If '-' prefixes the list of categories (e.g. category=- Cat1 | Cat 2 ...), these categories will not appear as headings in the DPL. See 'headingmode' param. + * Magic words allowed. + * @todo define 'category' options (retrieve list of categories from 'categorylinks' table?) + */ + 'category' => [ + 'default' => null, + ], + 'categorymatch' => [ + 'default' => null, + ], + 'categoryregexp' => [ + 'default' => null, + ], + /** + * Min and Max of categories allowed for an article + */ + 'categoriesminmax' => [ + 'default' => null, + 'pattern' => '#^(\d*)(?:,(\d*))?$#' + ], + /** + * hiddencategories + */ + 'hiddencategories' => [ + 'default' => 'include', + 'values' => ['include', 'exclude', 'only'] + ], + /** + * Perform the command and do not query the database. + */ + 'execandexit' => [ + 'default' => null + ], + + /** + * number of results which shall be skipped before display starts + * default is 0 + */ + 'offset' => [ + 'default' => 0, + 'integer' => true + ], + /** + * Max of results to display, selection is based on random. + */ + 'count' => [ + 'default' => 500, + 'integer' => true + ], + /** + * Max number of results to display, selection is based on random. + */ + 'randomcount' => [ + 'default' => null, + 'integer' => true + ], + /** + * shall the result set be distinct (=default) or not? + */ + 'distinct' => [ + 'default' => true, + 'values' => ['strict'] + ], + 'cacheperiod' => [ + 'default' => 86400, //Number of seconds, default one day at 86400 seconds. + 'integer' => true + ], + /** + * number of columns for output, default is 1 + */ + 'columns' => [ + 'default' => 1, + 'integer' => true + ], + + /** + * debug=... + * - 0: displays no debug message; + * - 1: displays fatal errors only; + * - 2: fatal errors + warnings only; + * - 3: every debug message. + * - 4: The SQL statement as an echo before execution. + * - 5: tags around the ouput + */ + 'debug' => [ + 'default' => 1, + 'values' => [0, 1, 2, 3, 4, 5] + ], + + /** + * eliminate=.. avoid creating unnecessary backreferences which point to to DPL results. + * it is expensive (in terms of performance) but more precise than "reset" + * categories: eliminate all category links which result from a DPL call (by transcluded contents) + * templates: the same with templates + * images: the same with images + * links: the same with internal and external links + * all all of the above + */ + 'eliminate' => [ + 'default' => [], + 'values' => [ + 'categories', + 'templates', + 'links', + 'images', + 'all', + 'none' + ] + ], + + 'format' => [ + 'default' => null, + ], + + 'goal' => [ + 'default' => 'pages', + 'values' => [ + 'pages', + 'categories' + ], + 'open_ref_conflict' => true + ], + + //Include the lowercase variants of header tiers for ease of use. + 'headingmode' => [ + 'default' => 'none', + 'values' => [ + 'H1', + 'H2', + 'H3', + 'H4', + 'H5', + 'H6', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + //'header', + 'definition', + 'none', + 'ordered', + 'unordered' + ], + 'open_ref_conflict' => true + ], + + /** + * we can display the number of articles within a heading group + */ + 'headingcount' => [ + 'default' => false, + 'boolean' => true, + 'open_ref_conflict' => true + ], + + /** + * Attributes for HTML list items (headings) at the heading level, depending on 'headingmode' (e.g. 'li' for ordered/unordered) + * Not yet applicable to 'headingmode=none | definition | H2 | H3 | H4'. + * @todo Make 'hitemattr' param applicable to 'none', 'definition', 'H2', 'H3', 'H4' headingmodes. + * Example: hitemattr= class="topmenuli" style="color: red;" + */ + 'hitemattr' => [ + 'default' => null + ], + + /** + * Attributes for the HTML list element at the heading/top level, depending on 'headingmode' (e.g. 'ol' for ordered, 'ul' for unordered, 'dl' for definition) + * Not yet applicable to 'headingmode=none'. + * @todo Make 'hlistattr' param applicable to headingmode=none. + * Example: hlistattr= class="topmenul" id="dmenu" + */ + 'hlistattr' => [ + 'default' => null + ], + + /** + * PAGE TRANSCLUSION: includepage=... or include=... + * To include the whole page, use a wildcard: + * includepage =* + * To include sections labeled 'sec1' or 'sec2' or... from the page (see the doc of the LabeledSectionTransclusion extension for more info): + * includepage = sec1,sec2,.. + * To include from the first occurrence of the heading 'heading1' (resp. 'heading2') until the next heading of the same or lower level. Note that this comparison is case insensitive. (See http://www.mediawiki.org/wiki/Extension:Labeled_Section_Transclusion#Transcluding_visual_headings.) : + * includepage = #heading1,#heading2,.... + * You can combine: + * includepage= sec1,#heading1,... + * To include nothing from the page (no transclusion), leave empty: + * includepage = + */ + + 'includepage' => [ + 'default' => null + ], + + /** + * make comparisons (linksto, linksfrom ) case insensitive + */ + 'ignorecase' => [ + 'default' => false, + 'boolean' => true + ], + 'include' => [ + 'default' => null + ], + + /** + * includesubpages + */ + 'includesubpages' => [ + 'default' => true, + 'boolean' => true + ], + + /** + * includematch=..,.. allows to specify regular expressions which must match the included contents + */ + 'includematch' => [ + 'default' => null + ], + 'includematchparsed' => [ + 'default' => false, + 'boolean' => true + ], + /** + * includenotmatch=..,.. allows to specify regular expressions which must NOT match the included contents + */ + 'includenotmatch' => [ + 'default' => null + ], + 'includenotmatchparsed' => [ + 'default' => false, + 'boolean' => true + ], + 'includetrim' => [ + 'default' => false, + 'boolean' => true + ], + /** + * Inline text is some wiki text used to separate list items with 'mode=inline'. + */ + 'inlinetext' => [ + 'default' => ' - ', + 'strip_html' => true + ], + /** + * Max # characters of included page to display. + * Null means no limit. + * If we include sections the limit will apply to each section. + */ + 'includemaxlength' => [ + 'default' => null, + 'integer' => true + ], + /** + * Attributes for HTML list items, depending on 'mode' ('li' for ordered/unordered, 'span' for others). + * Not applicable to 'mode=category'. + * @todo Make 'itemattr' param applicable to 'mode=category'. + * Example: itemattr= class="submenuli" style="color: red;" + */ + 'itemattr' => [ + 'default' => null + ], + /** + * listseparators is an array of four tags (in wiki syntax) which defines the output of DPL + * if mode = 'userformat' was specified. + * '\n' or '¶' in the input will be interpreted as a newline character. + * '%xxx%' in the input will be replaced by a corresponding value (xxx= PAGE, NR, COUNT etc.) + * t1 and t4 are the "outer envelope" for the whole result list, + * t2,t3 form an inner envelope around the article name of each entry. + * Examples: listseparators={|,,\n#[[%PAGE%]] + * Note: use of html tags was abolished from version 2.0; the first example must be written as: + * : listseparators={|,\n|-\n|[[%PAGE%]],,\n|} + */ + 'listseparators' => [ + 'default' => [] + ], + /** + * sequence of four wiki tags (separated by ",") to be used together with mode = 'userformat' + * t1 and t4 define an outer frame for the article list + * t2 and t3 build an inner frame for each article name + * example: listattr=
    ,
  • ,
  • ,
+ */ + 'listattr' => [ + 'default' => null + ], + /** + * this parameter restricts the output to articles which can reached via a link from the specified pages. + * Examples: linksfrom=my article|your article + */ + 'linksfrom' => [ + 'default' => null, + 'page_name_list' => true, + 'page_name_must_exist' => true, + 'set_criteria_found' => true + ], + /** + * this parameter restricts the output to articles which cannot be reached via a link from the specified pages. + * Examples: notlinksfrom=my article|your article + */ + 'notlinksfrom' => [ + 'default' => null, + 'page_name_list' => true, + 'page_name_must_exist' => true, + 'set_criteria_found' => true + ], + /** + * this parameter restricts the output to articles which contain a reference to one of the specified pages. + * Examples: linksto=my article|your article , linksto=Template:my template , linksto = {{FULLPAGENAME}} + */ + 'linksto' => [ + 'default' => null, + 'open_ref_conflict' => true, + 'page_name_list' => true, + 'page_name_must_exist' => true, + 'set_criteria_found' => true + ], + /** + * this parameter restricts the output to articles which do not contain a reference to the specified page. + */ + 'notlinksto' => [ + 'default' => null, + 'open_ref_conflict' => true, + 'page_name_list' => true, + 'page_name_must_exist' => true, + 'set_criteria_found' => true + ], + /** + * this parameter restricts the output to articles which contain an external reference that conatins a certain pattern + * Examples: linkstoexternal= www.xyz.com|www.xyz2.com + */ + 'linkstoexternal' => [ + 'default' => null, + 'open_ref_conflict' => true, + 'page_name_list' => true, + 'page_name_must_exist' => false, + 'set_criteria_found' => true + ], + /** + * this parameter restricts the output to articles which use one of the specified images. + * Examples: imageused=Image:my image|Image:your image + */ + 'imageused' => [ + 'default' => null, + 'open_ref_conflict' => true, + 'page_name_list' => true, + 'page_name_must_exist' => true, + 'set_criteria_found' => true + ], + /** + * this parameter restricts the output to images which are used (contained) by one of the specified pages. + * Examples: imagecontainer=my article|your article + */ + 'imagecontainer' => [ + 'default' => null, + 'open_ref_conflict' => false, + 'page_name_list' => true, + 'page_name_must_exist' => true, + 'set_criteria_found' => true + ], + /** + * this parameter restricts the output to articles which use the specified template. + * Examples: uses=Template:my template + */ + 'uses' => [ + 'default' => null, + 'open_ref_conflict' => true, + 'page_name_list' => true, + 'page_name_must_exist' => true, + 'set_criteria_found' => true + ], + /** + * this parameter restricts the output to articles which do not use the specified template. + * Examples: notuses=Template:my template + */ + 'notuses' => [ + 'default' => null, + 'open_ref_conflict' => true, + 'page_name_list' => true, + 'page_name_must_exist' => true, + 'set_criteria_found' => true + ], + /** + * this parameter restricts the output to the template used by the specified page. + */ + 'usedby' => [ + 'default' => null, + 'open_ref_conflict' => true, + 'page_name_list' => true, + 'page_name_must_exist' => true, + 'set_criteria_found' => true + ], + /** + * allows to specify a username who must be the first editor of the pages we select + */ + 'createdby' => [ + 'default' => null, + 'set_criteria_found' => true, + 'open_ref_conflict' => true, + 'preserve_case' => true + ], + /** + * allows to specify a username who must not be the first editor of the pages we select + */ + 'notcreatedby' => [ + 'default' => null, + 'set_criteria_found' => true, + 'open_ref_conflict' => true, + 'preserve_case' => true + ], + /** + * allows to specify a username who must be among the editors of the pages we select + */ + 'modifiedby' => [ + 'default' => null, + 'set_criteria_found' => true, + 'open_ref_conflict' => true, + 'preserve_case' => true + ], + /** + * allows to specify a username who must not be among the editors of the pages we select + */ + 'notmodifiedby' => [ + 'default' => null, + 'set_criteria_found' => true, + 'open_ref_conflict' => true, + 'preserve_case' => true + ], + /** + * allows to specify a username who must be the last editor of the pages we select + */ + 'lastmodifiedby' => [ + 'default' => null, + 'set_criteria_found' => true, + 'open_ref_conflict' => true, + 'preserve_case' => true + ], + /** + * allows to specify a username who must not be the last editor of the pages we select + */ + 'notlastmodifiedby' => [ + 'default' => null, + 'set_criteria_found' => true, + 'open_ref_conflict' => true, + 'preserve_case' => true + ], + /** + * Mode for list of pages (possibly within a heading, see 'headingmode' param). + * 'none' mode is implemented as a specific submode of 'inline' with
as inline text + * 'userformat' does not produce any html tags unless 'listseparators' are specified + */ + 'mode' => [ + 'default' => 'unordered', + 'values' => [ + 'category', + 'definition', + 'gallery', + 'inline', + 'none', + 'ordered', + 'subpage', + 'unordered', + 'userformat' + ] + ], + /** + * by default links to articles of type image or category are escaped (i.e. they appear as a link and do not + * actually assign the category or show the image; this can be changed. + * 'true' default + * 'false' images are shown, categories are assigned to the current document + */ + 'escapelinks' => [ + 'default' => true, + 'boolean' => true + ], + /** + * By default the page containingthe query will not be part of the result set. + * This can be changed via 'skipthispage=no'. This should be used with care as it may lead to + * problems which are hard to track down, esp. in combination with contents transclusion. + */ + 'skipthispage' => [ + 'default' => true, + 'boolean' => true + ], + /** + * namespace= Ns1 | Ns2 | ... + * [Special value] NsX='' (empty string without quotes) means Main namespace + * Means pages have to be in namespace Ns1 OR Ns2 OR... + * Magic words allowed. + */ + 'namespace' => [ + 'default' => null, + ], + /** + * notcategory= Cat1 + * notcategory = Cat2 + * ... + * Means pages can be NEITHER in category Cat1 NOR in Cat2 NOR... + * @todo define 'notcategory' options (retrieve list of categories from 'categorylinks' table?) + */ + 'notcategory' => [ + 'default' => null, + ], + 'notcategorymatch' => [ + 'default' => null, + ], + 'notcategoryregexp' => [ + 'default' => null, + ], + /** + * notnamespace= Ns1 + * notnamespace= Ns2 + * ... + * [Special value] NsX='' (empty string without quotes) means Main namespace + * Means pages have to be NEITHER in namespace Ns1 NOR Ns2 NOR... + * Magic words allowed. + */ + 'notnamespace' => [ + 'default' => null, + ], + /** + * title is the exact name of a page; this is useful if you want to use DPL + * just for contents inclusion; mode=userformat is automatically implied with title= + */ + 'title' => [ + 'default' => null, + ], + /** + * titlematch is a (SQL-LIKE-expression) pattern + * which restricts the result to pages matching that pattern + */ + 'titlelt' => [ + 'default' => null, + 'db_format' => true, + 'set_criteria_found' => true + ], + 'titlegt' => [ + 'default' => null, + 'db_format' => true, + 'set_criteria_found' => true + ], + 'scroll' => [ + 'default' => false, + 'boolean' => true + ], + 'titlematch' => [ + 'default' => null + ], + 'titleregexp' => [ + 'default' => null + ], + 'userdateformat' => [ + 'default' => 'Y-m-d H:i:s', + 'strip_html' => true + ], + 'updaterules' => [ + 'default' => null, + 'permission' => 'dpl_param_update_rules' + ], + 'deleterules' => [ + 'default' => null, + 'permission' => 'dpl_param_delete_rules' + ], + + /** + * nottitlematch is a (SQL-LIKE-expression) pattern + * which excludes pages matching that pattern from the result + */ + 'nottitlematch' => [ + 'default' => null + ], + 'nottitleregexp' => [ + 'default' => null + ], + 'order' => [ + 'default' => 'ascending', + 'values' => ['ascending', 'descending', 'asc', 'desc'] + ], + /** + * we can specify something like "latin1_swedish_ci" for case insensitive sorting + */ + 'ordercollation' => [ + 'default' => null + ], + /** + * 'ordermethod=param1,param2' means ordered by param1 first, then by param2. + * @todo: add 'ordermethod=category,categoryadd' (for each category CAT, pages ordered by date when page was added to CAT). + */ + 'ordermethod' => [ + 'default' => ['none'], + 'values' => [ + 'counter', + 'size', + 'category', + 'sortkey', + 'categoryadd', + 'firstedit', + 'lastedit', + 'pagetouched', + 'pagesel', + 'title', + 'titlewithoutnamespace', + 'user', + 'none' + ] + ], + /** + * minoredits =... (compatible with ordermethod=...,firstedit | lastedit only) + * - exclude: ignore minor edits (rev_minor_edit = 0 only) + * - include: include minor edits + */ + 'minoredits' => [ + 'default' => null, + 'values' => ['include', 'exclude'], + 'open_ref_conflict' => true + ], + /** + * lastrevisionbefore = select the latest revision which was existent before the specified point in time + */ + 'lastrevisionbefore' => [ + 'default' => null, + 'timestamp' => true, + 'open_ref_conflict' => true + ], + /** + * allrevisionsbefore = select the revisions which were created before the specified point in time + */ + 'allrevisionsbefore' => [ + 'default' => null, + 'timestamp' => true, + 'open_ref_conflict' => true + ], + /** + * firstrevisionsince = select the first revision which was created after the specified point in time + */ + 'firstrevisionsince' => [ + 'default' => null, + 'timestamp' => true, + 'open_ref_conflict' => true + ], + /** + * allrevisionssince = select the latest revisions which were created after the specified point in time + */ + 'allrevisionssince' => [ + 'default' => null, + 'timestamp' => true, + 'open_ref_conflict' => true + ], + /** + * Minimum/Maximum number of revisions required + */ + 'minrevisions' => [ + 'default' => null, + 'integer' => true + ], + 'maxrevisions' => [ + 'default' => null, + 'integer' => true + ], + 'suppresserrors' => [ + 'default' => false, + 'boolean' => true + ], + /** + * noresultsheader / footer is some wiki text which will be output (instead of a warning message) + * if the result set is empty; setting 'noresultsheader' to something like ' ' will suppress + * the warning about empty result set. + */ + 'noresultsheader' => [ + 'default' => null, + 'strip_html' => true, + 'preserve_case' => true + ], + 'noresultsfooter' => [ + 'default' => null, + 'strip_html' => true, + 'preserve_case' => true + ], + /** + * oneresultsheader / footer is some wiki text which will be output + * if the result set contains exactly one entry. + */ + 'oneresultheader' => [ + 'default' => null, + 'strip_html' => true, + 'preserve_case' => true + ], + 'oneresultfooter' => [ + 'default' => null, + 'strip_html' => true, + 'preserve_case' => true + ], + /** + * openreferences =... + * - no: excludes pages which do not exist (=default) + * - yes: includes pages which do not exist -- this conflicts with some other options + */ + 'openreferences' => [ + 'default' => false, + 'boolean' => true + ], + /** + * redirects =... + * - exclude: excludes redirect pages from lists (page_is_redirect = 0 only) + * - include: allows redirect pages to appear in lists + * - only: lists only redirect pages in lists (page_is_redirect = 1 only) + */ + 'redirects' => [ + 'default' => 'exclude', + 'values' => ['include', 'exclude', 'only'] + ], + /** + * stablepages =... + * - exclude: excludes stable pages from lists + * - include: allows stable pages to appear in lists + * - only: lists only stable pages in lists + */ + 'stablepages' => [ + 'default' => null, + 'values' => ['exclude', 'only'] + ], + /** + * qualitypages =... + * - exclude: excludes quality pages from lists + * - include: allows quality pages to appear in lists + * - only: lists only quality pages in lists + */ + 'qualitypages' => [ + 'default' => null, + 'values' => ['exclude', 'only'] + ], + /** + * resultsheader / footer is some wiki text which will be output before / after the result list + * (if there is at least one result); if 'oneresultheader / footer' is specified it will only be + * used if there are at least TWO results + */ + 'resultsheader' => [ + 'default' => null, + 'strip_html' => true, + 'preserve_case' => true + ], + 'resultsfooter' => [ + 'default' => null, + 'strip_html' => true, + 'preserve_case' => true + ], + /** + * reset=.. + * categories: remove all category links which have been defined before the dpl call, + * typically resulting from template calls or transcluded contents + * templates: the same with templates + * images: the same with images + * links: the same with internal and external links, throws away ALL links, not only DPL generated links! + * all all of the above + */ + 'reset' => [ + 'default' => [], + 'values' => [ + 'categories', + 'templates', + 'links', + 'images', + 'all', + 'none' + ] + ], + + /** + * fixcategory=.. prevents a category from being reset + */ + 'fixcategory' => [ + 'default' => null + ], + + /** + * Number of rows for output, default is 1 + * Note: a "row" is a group of lines for which the heading tags defined in listseparators/format will be repeated + */ + 'rows' => [ + 'default' => 1, + 'integer' => true + ], + + /** + * Number of elements in a rows for output, default is "all" + * Note: a "row" is a group of lines for which the heading tags defined in listeseparators will be repeated + */ + 'rowsize' => [ + 'default' => 0, + 'integer' => true + ], + + /** + * The HTML attribute tags(class, cellspacing) used for columns and rows in MediaWiki table markup. + */ + 'rowcolformat' => [ + 'default' => null, + 'strip_html' => true + ], + /** + * secseparators is a sequence of pairs of tags used to separate sections (see "includepage=name1, name2, ..") + * each pair corresponds to one entry in the includepage command + * if only one tag is given it will be used for all sections as a start tag (end tag will be empty then) + */ + 'secseparators' => [ + 'default' => [] + ], + /** + * multisecseparators is a list of tags (which correspond to the items in includepage) + * and which are put between identical sections included from the same file + */ + 'multisecseparators' => [ + 'default' => [] + ], + /** + * dominantSection is the number (starting from 1) of an includepage argument which shall be used + * as a dominant value set for the creation of additional output rows (one per value of the + * dominant column + */ + 'dominantsection' => [ + 'default' => 0, + 'integer' => true + ], + /** + * showcurid creates a stable link to the current revision of a page + */ + 'showcurid' => [ + 'default' => false, + 'boolean' => true, + 'open_ref_conflict' => true + ], + /** + * shownamespace decides whether to show the namespace prefix or not + */ + 'shownamespace' => [ + 'default' => true, + 'boolean' => true + ], + /** + * replaceintitle applies a regex replacement to %TITLE% + */ + 'replaceintitle' => [ + 'default' => null + ], + /** + * table is a short hand for combined values of listseparators, colseparators and mulicolseparators + */ + 'table' => [ + 'default' => null + ], + /** + * tablerow allows to define individual formats for table columns + */ + 'tablerow' => [ + 'default' => [] + ], + /** + * The number (starting with 1) of the column to be used for sorting + */ + 'tablesortcol' => [ + 'default' => null, + 'integer' => true + ], + /** + * Max # characters of page title to display. + * Empty value (default) means no limit. + * Not applicable to mode=category. + */ + 'titlemaxlength' => [ + 'default' => null, + 'integer' => true + ] + ]; + + /** + * Main Constructor + * + * @access public + * @return void + */ + public function __construct() { + $this->setRichness(Config::getSetting('functionalRichness')); + + if (\DynamicPageListHooks::isLikeIntersection()) { + $this->data['ordermethod'] = [ + 'default' => 'categoryadd', + 'values' => [ + 'categoryadd', + 'lastedit', + 'none' + ] + ]; + $this->data['order'] = [ + 'default' => 'descending', + 'values' => [ + 'ascending', + 'descending' + ] + ]; + $this->data['mode'] = [ + 'default' => 'unordered', + 'values' => [ + 'none', + 'ordered', + 'unordered' + ] + ]; + $this->data['userdateformat'] = [ + 'default' => 'Y-m-d: ' + ]; + $this->data['allowcachedresults']['default'] = 'true'; + } + } + + /** + * Return if the parameter exists. + * + * @access public + * @param string Parameter name. + * @return boolean Exists + */ + public function exists($parameter) { + return array_key_exists($parameter, $this->data); + } + + /** + * Return data for the supplied parameter. + * + * @access public + * @param string Parameter name. + * @return mixed Parameter array or false if it does not exist. + */ + public function getData($parameter) { + if (array_key_exists($parameter, $this->data)) { + return $this->data[$parameter]; + } else { + return false; + } + } + + /** + * Sets the current parameter richness. + * + * @access public + * @param integer Integer level. + * @return void + */ + public function setRichness($level) { + $this->parameterRichness = intval($level); + } + + /** + * Returns the current parameter richness. + * + * @access public + * @return integer + */ + public function getRichness() { + return $this->parameterRichness; + } + + /** + * Tests if the function is valid for the current functional richness level. + * + * @access public + * @param string Function to test. + * @return boolean Valid for this functional richness level. + */ + public function testRichness($function) { + $valid = false; + for ($i = 0; $i <= $this->getRichness(); $i++) { + if (in_array($function, self::$parametersForRichnessLevel[$i])) { + $valid = true; + break; + } + } + return $valid; + } + + /** + * Returns all parameters for the current richness level or limited to the optional maximum richness. + * + * @access public + * @param integer [Optional] Maximum richness level + * @return array The functional richness parameters list. + */ + public function getParametersForRichness($level = null) { + if ($level === null) { + $level = $this->getRichness(); + } + + $parameters = []; + for ($i = 0; $i <= $level; $i++) { + $parameters = array_merge($parameters, self::$parametersForRichnessLevel[$i]); + } + sort($parameters); + + return $parameters; + } + + /** + * Return the default value for the parameter. + * + * @access public + * @param string Parameter Name + * @return mixed + */ + public function getDefault($parameter) { + if (array_key_exists($parameter, $this->data)) { + if (array_key_exists('default', $this->data[$parameter])) { + return (bool)$this->data[$parameter]['default']; + } + return null; + } + throw new MWException(__METHOD__ . ": Attempted to load a parameter that does not exist."); + } + + /** + * Return the acceptable values for the parameter. + * + * @access public + * @param string Parameter Name + * @return mixed Array of allowed values or false that the parameter allows any. + */ + public function getValues($parameter) { + if (array_key_exists($parameter, $this->data)) { + if (array_key_exists('values', $this->data[$parameter])) { + return (bool)$this->data[$parameter]['values']; + } + return false; + } + throw new MWException(__METHOD__ . ": Attempted to load a parameter that does not exist."); + } + + /** + * Does the parameter set that criteria for selection was found? + * + * @access public + * @param string Parameter Name + * @return bool + */ + public function setsCriteriaFound($parameter) { + if (array_key_exists($parameter, $this->data)) { + if (array_key_exists('set_criteria_found', $this->data[$parameter])) { + return (bool)$this->data[$parameter]['set_criteria_found']; + } + return false; + } + throw new MWException(__METHOD__ . ": Attempted to load a parameter that does not exist."); + } + + /** + * Does the parameter cause an open reference conflict? + * + * @access public + * @param string Parameter Name + * @return bool + */ + public function isOpenReferenceConflict($parameter) { + if (array_key_exists($parameter, $this->data)) { + if (array_key_exists('open_ref_conflict', $this->data[$parameter])) { + return (bool)$this->data[$parameter]['open_ref_conflict']; + } + return false; + } + throw new MWException(__METHOD__ . ": Attempted to load a parameter that does not exist."); + } + + /** + * Should this parameter preserve the case of the user supplied input? + * + * @access public + * @param string Parameter Name + * @return bool + */ + public function shouldPreserveCase($parameter) { + if (array_key_exists($parameter, $this->data)) { + if (array_key_exists('preserve_case', $this->data[$parameter])) { + return (bool)$this->data[$parameter]['preserve_case']; + } + return false; + } + throw new MWException(__METHOD__ . ": Attempted to load a parameter that does not exist."); + } + + /** + * Does this parameter take a list of page names? + * + * @access public + * @param string Parameter Name + * @return bool + */ + public function isPageNameList($parameter) { + if (array_key_exists($parameter, $this->data)) { + if (array_key_exists('page_name_list', $this->data[$parameter])) { + return (bool)$this->data[$parameter]['page_name_list']; + } + return false; + } + throw new MWException(__METHOD__ . ": Attempted to load a parameter that does not exist."); + } + + /** + * Is the parameter supposed to be parsed as a boolean? + * + * @access public + * @param string Parameter Name + * @return bool + */ + public function isBoolean($parameter) { + if (array_key_exists($parameter, $this->data)) { + if (array_key_exists('boolean', $this->data[$parameter])) { + return (bool)$this->data[$parameter]['boolean']; + } + return false; + } + throw new MWException(__METHOD__ . ": Attempted to load a parameter that does not exist."); + } + + /** + * Is the parameter supposed to be parsed as a Mediawiki timestamp? + * + * @access public + * @param string Parameter Name + * @return bool + */ + public function isTimestamp($parameter) { + if (array_key_exists($parameter, $this->data)) { + if (array_key_exists('timestamp', $this->data[$parameter])) { + return (bool)$this->data[$parameter]['timestamp']; + } + return false; + } + throw new MWException(__METHOD__ . ": Attempted to load a parameter that does not exist."); + } +} diff --git a/classes/Parse.php b/classes/Parse.php new file mode 100644 index 0000000..6f89fb2 --- /dev/null +++ b/classes/Parse.php @@ -0,0 +1,1006 @@ +DB = wfGetDB(DB_REPLICA, 'dpl'); + $this->parameters = new Parameters(); + $this->logger = new Logger($this->parameters->getData('debug')['default']); + $this->tableNames = Query::getTableNames(); + $this->wgRequest = $wgRequest; + } + + /** + * The real callback function for converting the input text to wiki text output + * + * @access public + * @param string Raw User Input + * @param object Mediawiki Parser object. + * @param array End Reset Booleans + * @param array End Eliminate Booleans + * @param boolean [Optional] Called as a parser tag + * @return string Wiki/HTML Output + */ + public function parse($input, \Parser $parser, &$reset, &$eliminate, $isParserTag = false) { + $dplStartTime = microtime(true); + $this->parser = $parser; + + //Reset headings when being ran more than once in the same page load. + Article::resetHeadings(); + + //Check that we are not in an infinite transclusion loop + if (isset($this->parser->mTemplatePath[$this->parser->mTitle->getPrefixedText()])) { + $this->logger->addMessage(\DynamicPageListHooks::WARN_TRANSCLUSIONLOOP, $this->parser->mTitle->getPrefixedText()); + return $this->getFullOutput(); + } + + //Check if DPL shall only be executed from protected pages. + if (Config::getSetting('runFromProtectedPagesOnly') === true && !$this->parser->mTitle->isProtected('edit')) { + //Ideally we would like to allow using a DPL query if the query istelf is coded on a template page which is protected. Then there would be no need for the article to be protected. However, how can one find out from which wiki source an extension has been invoked??? + $this->logger->addMessage(\DynamicPageListHooks::FATAL_NOTPROTECTED, $this->parser->mTitle->getPrefixedText()); + return $this->getFullOutput(); + } + + /************************************/ + /* Check for URL Arguments in Input */ + /************************************/ + if (strpos($input, '{%DPL_') >= 0) { + for ($i = 1; $i <= 5; $i++) { + $this->urlArguments[] = 'DPL_arg' . $i; + } + } + $input = $this->resolveUrlArguments($input, $this->urlArguments); + $this->getUrlArgs($this->parser); + + $this->parameters->setParameter('offset', $this->wgRequest->getInt('DPL_offset', $this->parameters->getData('offset')['default'])); + $offset = $this->parameters->getParameter('offset'); + + /***************************************/ + /* User Input preparation and parsing. */ + /***************************************/ + $cleanParameters = $this->prepareUserInput($input); + if (!is_array($cleanParameters)) { + //Short circuit for dumb things. + $this->logger->addMessage(\DynamicPageListHooks::FATAL_NOSELECTION); + return $this->getFullOutput(); + } + $cleanParameters = Parameters::sortByPriority($cleanParameters); + $this->parameters->setParameter('includeuncat', false); // to check if pseudo-category of Uncategorized pages is included + + foreach ($cleanParameters as $parameter => $option) { + foreach ($option as $_option) { + //Parameter functions return true or false. The full parameter data will be passed into the Query object later. + if ($this->parameters->$parameter($_option) === false) { + //Do not build this into the output just yet. It will be collected at the end. + $this->logger->addMessage(\DynamicPageListHooks::WARN_WRONGPARAM, $parameter, $_option); + } + } + } + + /*************************/ + /* Execute and Exit Only */ + /*************************/ + if ($this->parameters->getParameter('execandexit') !== null) { + //The keyword "geturlargs" is used to return the Url arguments and do nothing else. + if ($this->parameters->getParameter('execandexit') == 'geturlargs') { + return; + } + //In all other cases we return the value of the argument which may contain parser function calls. + return $this->parameters->getParameter('execandexit'); + } + + //Construct internal keys for TableRow according to the structure of "include". This will be needed in the output phase. + $secLabels = $this->parameters->getParameter('seclabels'); + if (is_array($secLabels) && !empty($this->parameters->getParameter('seclabels'))) { + $this->parameters->setParameter('tablerow', $this->updateTableRowKeys($this->parameters->getParameter('tablerow'), $this->parameters->getParameter('seclabels'))); + } + + /****************/ + /* Check Errors */ + /****************/ + $errors = $this->doQueryErrorChecks(); + if ($errors === false) { + //WHAT HAS HAPPENED OH NOOOOOOOOOOOOO. + return $this->getFullOutput(); + } + + $calcRows = false; + if (!Config::getSetting('allowUnlimitedResults') && $this->parameters->getParameter('goal') != 'categories' && strpos($this->parameters->getParameter('resultsheader') . $this->parameters->getParameter('noresultsheader') . $this->parameters->getParameter('resultsfooter'), '%TOTALPAGES%') !== false) { + $calcRows = true; + } + + /*********/ + /* Query */ + /*********/ + try { + $this->query = new Query($this->parameters); + $result = $this->query->buildAndSelect($calcRows); + } catch (MWException $e) { + $this->logger->addMessage(\DynamicPageListHooks::FATAL_SQLBUILDERROR, $e->getMessage()); + return $this->getFullOutput(); + } + + $numRows = $this->DB->numRows($result); + $articles = $this->processQueryResults($result); + + global $wgDebugDumpSql; + if (\DynamicPageListHooks::getDebugLevel() >= 4 && $wgDebugDumpSql) { + $this->addOutput($this->query->getSqlQuery() . "\n"); + } + + $this->addOutput('{{Extension DPL}}'); + + //Preset these to defaults. + $this->setVariable('TOTALPAGES', 0); + $this->setVariable('PAGES', 0); + $this->setVariable('VERSION', DPL_VERSION); + + /*********************/ + /* Handle No Results */ + /*********************/ + if ($numRows <= 0 || empty($articles)) { + //Shortcut out since there is no processing to do. + $this->DB->freeResult($result); + return $this->getFullOutput(0, false); + } + + $foundRows = null; + if ($calcRows) { + $foundRows = $this->query->getFoundRows(); + } + + //Backward scrolling: If the user specified only titlelt with descending reverse the output order. + if ($this->parameters->getParameter('titlelt') && !$this->parameters->getParameter('titlegt') && $this->parameters->getParameter('order') == 'descending') { + $articles = array_reverse($articles); + } + + //Special sort for card suits (Bridge) + if ($this->parameters->getParameter('ordersuitsymbols')) { + $articles = $this->cardSuitSort($articles); + } + + /*******************/ + /* Generate Output */ + /*******************/ + $lister = Lister::newFromStyle($this->parameters->getParameter('mode'), $this->parameters, $this->parser); + $heading = Heading::newFromStyle($this->parameters->getParameter('headingmode'), $this->parameters); + if ($heading !== null) { + $this->addOutput($heading->format($articles, $lister)); + } else { + $this->addOutput($lister->format($articles)); + } + + //$this->addOutput($lister->format($articles)); + if ($foundRows === null) { + $foundRows = $lister->getRowCount(); //Get row count after calling format() otherwise the count will be inaccurate. + } + + /*******************************/ + /* Replacement Variables */ + /*******************************/ + $this->setVariable('TOTALPAGES', $foundRows); //Guaranteed to be an accurate count if SQL_CALC_FOUND_ROWS was used. Otherwise only accurate if results are less than the SQL LIMIT. + $this->setVariable('PAGES', $lister->getRowCount()); //This could be different than TOTALPAGES. PAGES represents the total results within the constraints of SQL LIMIT. + + //Replace %DPLTIME% by execution time and timestamp in header and footer + $nowTimeStamp = date('Y/m/d H:i:s'); + $dplElapsedTime = sprintf('%.3f sec.', microtime(true) - $dplStartTime); + $dplTime = "{$dplElapsedTime} ({$nowTimeStamp})"; + $this->setVariable('DPLTIME', $dplTime); + + //Replace %LASTTITLE% / %LASTNAMESPACE% by the last title found in header and footer + if (($n = count($articles)) > 0) { + $firstNamespaceFound = str_replace(' ', '_', $articles[0]->mTitle->getNamespace()); + $firstTitleFound = str_replace(' ', '_', $articles[0]->mTitle->getText()); + $lastNamespaceFound = str_replace(' ', '_', $articles[$n - 1]->mTitle->getNamespace()); + $lastTitleFound = str_replace(' ', '_', $articles[$n - 1]->mTitle->getText()); + } + $this->setVariable('FIRSTNAMESPACE', $firstNamespaceFound); + $this->setVariable('FIRSTTITLE', $firstTitleFound); + $this->setVariable('LASTNAMESPACE', $lastNamespaceFound); + $this->setVariable('LASTTITLE', $lastTitleFound); + $this->setVariable('SCROLLDIR', $this->parameters->getParameter('scrolldir')); + + /*******************************/ + /* Scroll Variables */ + /*******************************/ + $scrollVariables = [ + 'DPL_firstNamespace' => $firstNamespaceFound, + 'DPL_firstTitle' => $firstTitleFound, + 'DPL_lastNamespace' => $lastNamespaceFound, + 'DPL_lastTitle' => $lastTitleFound, + 'DPL_scrollDir' => $this->parameters->getParameter('scrolldir'), + 'DPL_time' => $dplTime, + 'DPL_count' => $this->parameters->getParameter('count'), + 'DPL_totalPages' => $foundRows, + 'DPL_pages' => $lister->getRowCount() + ]; + $this->defineScrollVariables($scrollVariables); + + if ($this->parameters->getParameter('allowcachedresults') || Config::getSetting('alwaysCacheResults')) { + $this->parser->getOutput()->updateCacheExpiry($this->parameters->getParameter('cacheperiod') ? $this->parameters->getParameter('cacheperiod') : 3600); + } else { + $this->parser->getOutput()->updateCacheExpiry( 0 ); + } + + $finalOutput = $this->getFullOutput($foundRows, false); + + $this->triggerEndResets($finalOutput, $reset, $eliminate, $isParserTag); + + return $finalOutput; + } + + /** + * Process Query Results + * + * @access private + * @param object Mediawiki Result Object + * @return array Array of Article objects. + */ + private function processQueryResults($result) { + /*******************************/ + /* Random Count Pick Generator */ + /*******************************/ + $randomCount = $this->parameters->getParameter('randomcount'); + if ($randomCount > 0) { + $nResults = $this->DB->numRows($result); + //mt_srand() seeding was removed due to PHP 5.2.1 and above no longer generating the same sequence for the same seed. + //Constrain the total amount of random results to not be greater than the total results. + if ($randomCount > $nResults) { + $randomCount = $nResults; + } + + //This is 50% to 150% faster than the old while (true) version that could keep rechecking the same random key over and over again. + //Generate pick numbers for results. + $pick = range(1, $nResults); + //Shuffle the pick numbers. + shuffle($pick); + //Select pick numbers from the beginning to the maximum of $randomCount. + $pick = array_slice($pick, 0, $randomCount); + } + + $articles = []; + + /**********************/ + /* Article Processing */ + /**********************/ + $i = 0; + while ($row = $result->fetchRow()) { + $i++; + + //In random mode skip articles which were not chosen. + if ($randomCount > 0 && !in_array($i, $pick)) { + continue; + } + + if ($this->parameters->getParameter('goal') == 'categories') { + $pageNamespace = NS_CATEGORY; + $pageTitle = $row['cl_to']; + } elseif ($this->parameters->getParameter('openreferences')) { + if (count($this->parameters->getParameter('imagecontainer')) > 0) { + $pageNamespace = NS_FILE; + $pageTitle = $row['il_to']; + } else { + //Maybe non-existing title + $pageNamespace = $row['pl_namespace']; + $pageTitle = $row['pl_title']; + } + } else { + //Existing PAGE TITLE + $pageNamespace = $row['page_namespace']; + $pageTitle = $row['page_title']; + } + + // if subpages are to be excluded: skip them + if (!$this->parameters->getParameter('includesubpages') && strpos($pageTitle, '/') !== false) { + continue; + } + + $title = \Title::makeTitle($pageNamespace, $pageTitle); + $thisTitle = $this->parser->getTitle(); + + //Block recursion from happening by seeing if this result row is the page the DPL query was ran from. + if ($this->parameters->getParameter('skipthispage') && $thisTitle->equals($title)) { + continue; + } + + $articles[] = Article::newFromRow($row, $this->parameters, $title, $pageNamespace, $pageTitle); + } + $this->DB->freeResult($result); + + return $articles; + } + + /** + * Do basic clean up and structuring of raw user input. + * + * @access private + * @param string Raw User Input + * @return array Array of raw text parameter => option. + */ + private function prepareUserInput($input) { + //We replace double angle brackets with single angle brackets to avoid premature tag expansion in the input. + //The ¦ symbol is an alias for |. + //The combination '²{' and '}²'will be translated to double curly braces; this allows postponed template execution which is crucial for DPL queries which call other DPL queries. + $input = str_replace(['«', '»', '¦', '²{', '}²'], ['<', '>', '|', '{{', '}}'], $input); + + //Standard new lines into the standard \n and clean up any hanging new lines. + $input = str_replace(["\r\n", "\r"], "\n", $input); + $input = trim($input, "\n"); + $rawParameters = explode("\n", $input); + + $parameters = false; + foreach ($rawParameters as $parameterOption) { + if (empty($parameterOption)) { + //Softly ignore blank lines. + continue; + } + + if (strpos($parameterOption, '=') === false) { + $this->logger->addMessage(\DynamicPageListHooks::WARN_PARAMNOOPTION, $parameterOption); + continue; + } + + list($parameter, $option) = explode('=', $parameterOption, 2); + $parameter = trim($parameter); + $option = trim($option); + + if (strpos($parameter, '<') !== false || strpos($parameter, '>') !== false) { + //Having the actual less than and greater than symbols is nasty for programatic look up. The old parameter is still supported along with the new, but we just fix it here before calling it. + $parameter = str_replace('<', 'lt', $parameter); + $parameter = str_replace('>', 'gt', $parameter); + } + + $parameter = strtolower($parameter); //Force lower case for ease of use. + if (empty($parameter) || substr($parameter, 0, 1) == '#' || ($this->parameters->exists($parameter) && !$this->parameters->testRichness($parameter))) { + continue; + } + + if (!$this->parameters->exists($parameter)) { + $this->logger->addMessage(\DynamicPageListHooks::WARN_UNKNOWNPARAM, $parameter, implode(', ', $this->parameters->getParametersForRichness())); + continue; + } + + //Ignore parameter settings without argument (except namespace and category). + if (!strlen($option)) { + if ($parameter != 'namespace' && $parameter != 'notnamespace' && $parameter != 'category' && $this->parameters->exists($parameter)) { + continue; + } + } + $parameters[$parameter][] = $option; + } + return $parameters; + } + + /** + * Concatenate output + * + * @access private + * @param string Output to add + * @return void + */ + private function addOutput($output) { + $this->output .= $output; + } + + /** + * Set the output text. + * + * @access private + * @return string Output Text + */ + private function getOutput() { + //@TODO: 2015-08-28 Consider calling $this->replaceVariables() here. Might cause issues with text returned in the results. + return $this->output; + } + + /** + * Return output optionally including header and footer. + * + * @access private + * @param boolean [Optional] Total results. + * @param boolean [Optional] Skip adding the header and footer. + * @return string Output + */ + private function getFullOutput($totalResults = false, $skipHeaderFooter = true) { + if (!$skipHeaderFooter) { + $header = ''; + $footer = ''; + //Only override header and footers if specified. + $_headerType = $this->getHeaderFooterType('header', $totalResults); + if ($_headerType !== false) { + $header = $this->parameters->getParameter($_headerType); + } + $_footerType = $this->getHeaderFooterType('footer', $totalResults); + if ($_footerType !== false) { + $footer = $this->parameters->getParameter($_footerType); + } + + $this->setHeader($header); + $this->setFooter($footer); + } + + if (!$totalResults && !strlen($this->getHeader()) && !strlen($this->getFooter())) { + $this->logger->addMessage(\DynamicPageListHooks::WARN_NORESULTS); + } + $messages = $this->logger->getMessages(false); + + return (count($messages) ? implode("
\n", $messages) : null) . $this->getHeader() . $this->getOutput() . $this->getFooter(); + } + + /** + * Set the header text. + * + * @access private + * @param string Header Text + * @return void + */ + private function setHeader($header) { + if (\DynamicPageListHooks::getDebugLevel() == 5) { + $header = '
' . $header;
+		}
+		$this->header = $this->replaceVariables($header);
+	}
+
+	/**
+	 * Set the header text.
+	 *
+	 * @access	private
+	 * @return	string	Header Text
+	 */
+	private function getHeader() {
+		return $this->header;
+	}
+
+	/**
+	 * Set the footer text.
+	 *
+	 * @access	private
+	 * @param	string	Footer Text
+	 * @return	void
+	 */
+	private function setFooter($footer) {
+		if (\DynamicPageListHooks::getDebugLevel() == 5) {
+			$footer .= '
'; + } + $this->footer = $this->replaceVariables($footer); + } + + /** + * Set the footer text. + * + * @access private + * @return string Footer Text + */ + private function getFooter() { + return $this->footer; + } + + /** + * Determine the header/footer type to use based on what output format parameters were chosen and the number of results. + * + * @access private + * @param string Page position to check: 'header' or 'footer'. + * @param integer Count of pages. + * @return mixed Type to use: 'results', 'oneresult', or 'noresults'. False if invalid or none should be used. + */ + private function getHeaderFooterType($position, $count) { + $count = intval($count); + if ($position != 'header' && $position != 'footer') { + return false; + } + + if ($this->parameters->getParameter('results' . $position) !== null && ($count >= 2 || ($this->parameters->getParameter('oneresult' . $position) === null && $count >= 1))) { + $_type = 'results' . $position; + } elseif ($count === 1 && $this->parameters->getParameter('oneresult' . $position) !== null) { + $_type = 'oneresult' . $position; + } elseif ($count === 0 && $this->parameters->getParameter('noresults' . $position) !== null) { + $_type = 'noresults' . $position; + } else { + $_type = false; + } + return $_type; + } + + /** + * Set a variable to be replaced with the provided text later at the end of the output. + * + * @access private + * @param string Variable name, will be transformed to uppercase and have leading and trailing percent signs added. + * @param string Text to replace the variable with. + * @return void + */ + private function setVariable($variable, $replacement) { + $variable = "%" . mb_strtoupper($variable, "UTF-8") . "%"; + $this->replacementVariables[$variable] = $replacement; + } + + /** + * Return text with variables replaced. + * + * @access private + * @param string Text to perform replacements on. + * @return string Replaced Text + */ + private function replaceVariables($text) { + $text = self::replaceNewLines($text); + foreach ($this->replacementVariables as $variable => $replacement) { + $text = str_replace($variable, $replacement, $text); + } + return $text; + } + + /** + * Return text with custom new line characters replaced. + * + * @access private + * @param string Text + * @return string New Lined Text + */ + public static function replaceNewLines($text) { + return str_replace(['\n', "¶"], "\n", $text); + } + + /** + * Work through processed parameters and check for potential issues. + * + * @access private + * @return void + */ + private function doQueryErrorChecks() { + /**************************/ + /* Parameter Error Checks */ + /**************************/ + + $totalCategories = 0; + if (is_array($this->parameters->getParameter('category'))) { + foreach ($this->parameters->getParameter('category') as $comparisonType => $operatorTypes) { + foreach ($operatorTypes as $operatorType => $categoryGroups) { + foreach ($categoryGroups as $categories) { + if (is_array($categories)) { + $totalCategories += count($categories); + } + } + } + } + } + if (is_array($this->parameters->getParameter('notcategory'))) { + foreach ($this->parameters->getParameter('notcategory') as $comparisonType => $operatorTypes) { + foreach ($operatorTypes as $operatorType => $categories) { + if (is_array($categories)) { + $totalCategories += count($categories); + } + } + } + } + + //Too many categories. + if ($totalCategories > Config::getSetting('maxCategoryCount') && !Config::getSetting('allowUnlimitedCategories')) { + $this->logger->addMessage(\DynamicPageListHooks::FATAL_TOOMANYCATS, Config::getSetting('maxCategoryCount')); + return false; + } + + //Not enough categories.(Really?) + if ($totalCategories < Config::getSetting('minCategoryCount')) { + $this->logger->addMessage(\DynamicPageListHooks::FATAL_TOOFEWCATS, Config::getSetting('minCategoryCount')); + return false; + } + + //Selection criteria needs to be found. + if (!$totalCategories && !$this->parameters->isSelectionCriteriaFound()) { + $this->logger->addMessage(\DynamicPageListHooks::FATAL_NOSELECTION); + return false; + } + + //ordermethod=sortkey requires ordermethod=category + //Delayed to the construction of the SQL query, see near line 2211, gs + //if (in_array('sortkey',$aOrderMethods) && ! in_array('category',$aOrderMethods)) $aOrderMethods[] = 'category'; + + $orderMethods = (array)$this->parameters->getParameter('ordermethod'); + //Throw an error in no categories were selected when using category sorting modes or requesting category information. + if ($totalCategories == 0 && (in_array('categoryadd', $orderMethods) || $this->parameters->getParameter('addfirstcategorydate') === true)) { + $this->logger->addMessage(\DynamicPageListHooks::FATAL_CATDATEBUTNOINCLUDEDCATS); + return false; + } + + //No more than one type of date at a time! + //@TODO: Can this be fixed to allow all three later after fixing the article class? + if ((intval($this->parameters->getParameter('addpagetoucheddate')) + intval($this->parameters->getParameter('addfirstcategorydate')) + intval($this->parameters->getParameter('addeditdate'))) > 1) { + $this->logger->addMessage(\DynamicPageListHooks::FATAL_MORETHAN1TYPEOFDATE); + return false; + } + + // the dominant section must be one of the sections mentioned in includepage + if ($this->parameters->getParameter('dominantsection') > 0 && count($this->parameters->getParameter('seclabels')) < $this->parameters->getParameter('dominantsection')) { + $this->logger->addMessage(\DynamicPageListHooks::FATAL_DOMINANTSECTIONRANGE, count($this->parameters->getParameter('seclabels'))); + return false; + } + + // category-style output requested with not compatible order method + if ($this->parameters->getParameter('mode') == 'category' && !array_intersect($orderMethods, ['sortkey', 'title', 'titlewithoutnamespace'])) { + $this->logger->addMessage(\DynamicPageListHooks::FATAL_WRONGORDERMETHOD, 'mode=category', 'sortkey | title | titlewithoutnamespace'); + return false; + } + + // addpagetoucheddate=true with unappropriate order methods + if ($this->parameters->getParameter('addpagetoucheddate') && !array_intersect($orderMethods, ['pagetouched', 'title'])) { + $this->logger->addMessage(\DynamicPageListHooks::FATAL_WRONGORDERMETHOD, 'addpagetoucheddate=true', 'pagetouched | title'); + return false; + } + + // addeditdate=true but not (ordermethod=...,firstedit or ordermethod=...,lastedit) + //firstedit (resp. lastedit) -> add date of first (resp. last) revision + if ($this->parameters->getParameter('addeditdate') && !array_intersect($orderMethods, ['firstedit', 'lastedit']) && ($this->parameters->getParameter('allrevisionsbefore') || $this->parameters->getParameter('allrevisionssince') || $this->parameters->getParameter('firstrevisionsince') || $this->parameters->getParameter('lastrevisionbefore'))) { + $this->logger->addMessage(\DynamicPageListHooks::FATAL_WRONGORDERMETHOD, 'addeditdate=true', 'firstedit | lastedit'); + return false; + } + + // adduser=true but not (ordermethod=...,firstedit or ordermethod=...,lastedit) + /** + * @todo allow to add user for other order methods. + * The fact is a page may be edited by multiple users. Which user(s) should we show? all? the first or the last one? + * Ideally, we could use values such as 'all', 'first' or 'last' for the adduser parameter. + */ + if ($this->parameters->getParameter('adduser') && !array_intersect($orderMethods, ['firstedit', 'lastedit']) && !$this->parameters->getParameter('allrevisionsbefore') && !$this->parameters->getParameter('allrevisionssince') && !$this->parameters->getParameter('firstrevisionsince') && !$this->parameters->getParameter('lastrevisionbefore')) { + $this->logger->addMessage(\DynamicPageListHooks::FATAL_WRONGORDERMETHOD, 'adduser=true', 'firstedit | lastedit'); + return false; + } + if ($this->parameters->getParameter('minoredits') && !array_intersect($orderMethods, ['firstedit', 'lastedit'])) { + $this->logger->addMessage(\DynamicPageListHooks::FATAL_WRONGORDERMETHOD, 'minoredits', 'firstedit | lastedit'); + return false; + } + + //add*** parameters have no effect with 'mode=category' (only namespace/title can be viewed in this mode) + if ($this->parameters->getParameter('mode') == 'category' && ($this->parameters->getParameter('addcategories') || $this->parameters->getParameter('addeditdate') || $this->parameters->getParameter('addfirstcategorydate') || $this->parameters->getParameter('addpagetoucheddate') || $this->parameters->getParameter('incpage') || $this->parameters->getParameter('adduser') || $this->parameters->getParameter('addauthor') || $this->parameters->getParameter('addcontribution') || $this->parameters->getParameter('addlasteditor'))) { + $this->logger->addMessage(\DynamicPageListHooks::WARN_CATOUTPUTBUTWRONGPARAMS); + } + + //headingmode has effects with ordermethod on multiple components only + if ($this->parameters->getParameter('headingmode') !== 'none' && count($orderMethods) < 2) { + $this->logger->addMessage(\DynamicPageListHooks::WARN_HEADINGBUTSIMPLEORDERMETHOD, $this->parameters->getParameter('headingmode'), 'none'); + $this->parameters->setParameter('headingmode', 'none'); + } + + //The 'openreferences' parameter is incompatible with many other options. + if ($this->parameters->isOpenReferencesConflict() && $this->parameters->getParameter('openreferences') === true) { + $this->logger->addMessage(\DynamicPageListHooks::FATAL_OPENREFERENCES); + return false; + } + return true; + } + + /** + * Create keys for TableRow which represent the structure of the "include=" arguments. + * + * @access public + * @param array Array of 'tablerow' parameter data. + * @param array Array of 'include' parameter data. + * @return array Updated 'tablerow' parameter. + */ + private static function updateTableRowKeys($tableRow, $sectionLabels) { + $_tableRow = (array)$tableRow; + $tableRow = []; + $groupNr = -1; + $t = -1; + foreach ($sectionLabels as $label) { + $t++; + $groupNr++; + $cols = explode('}:', $label); + if (count($cols) <= 1) { + if (array_key_exists($t, $_tableRow)) { + $tableRow[$groupNr] = $_tableRow[$t]; + } + } else { + $n = count(explode(':', $cols[1])); + $colNr = -1; + $t--; + for ($i = 1; $i <= $n; $i++) { + $colNr++; + $t++; + if (array_key_exists($t, $_tableRow)) { + $tableRow[$groupNr . '.' . $colNr] = $_tableRow[$t]; + } + } + } + } + return $tableRow; + } + + /** + * Resolve arguments in the input that would normally be in the URL. + * + * @access public + * @param string Raw Uncleaned User Input + * @param array Array of URL arguments to resolve. Non-arrays will be casted to an array. + * @return string Raw input with variables replaced + */ + private function resolveUrlArguments($input, $arguments) { + $arguments = (array)$arguments; + foreach ($arguments as $arg) { + $dplArg = $this->wgRequest->getVal($arg, ''); + if ($dplArg == '') { + $input = preg_replace('/\{%' . $arg . ':(.*)%\}/U', '\1', $input); + $input = str_replace('{%' . $arg . '%}', '', $input); + } else { + $input = preg_replace('/\{%' . $arg . ':.*%\}/U ', $dplArg, $input); + $input = str_replace('{%' . $arg . '%}', $dplArg, $input); + } + } + return $input; + } + + /** + * This function uses the Variables extension to provide URL-arguments like &DPL_xyz=abc in the form of a variable which can be accessed as {{#var:xyz}} if Extension:Variables is installed. + * + * @access public + * @return void + */ + private function getUrlArgs() { + $args = $this->wgRequest->getValues(); + foreach ($args as $argName => $argValue) { + if (strpos($argName, 'DPL_') === false) { + continue; + } + Variables::setVar(['', '', $argName, $argValue]); + if (defined('ExtVariables::VERSION')) { + \ExtVariables::get($this->parser)->setVarValue($argName, $argValue); + } + } + } + + /** + * This function uses the Variables extension to provide navigation aids such as DPL_firstTitle, DPL_lastTitle, or DPL_findTitle. These variables can be accessed as {{#var:DPL_firstTitle}} if Extension:Variables is installed. + * + * @access public + * @param array Array of scroll variables with the key as the variable name and the value as the value. Non-arrays will be casted to arrays. + * @return void + */ + private function defineScrollVariables($scrollVariables) { + $scrollVariables = (array)$scrollVariables; + + foreach ($scrollVariables as $variable => $value) { + Variables::setVar(['', '', $variable, $value]); + if (defined('ExtVariables::VERSION')) { + \ExtVariables::get($this->parser)->setVarValue($variable, $value); + } + } + } + + /** + * Trigger Resets and Eliminates that run at the end of parsing. + * + * @access private + * @param string Full output including header, footer, and any warnings. + * @param array End Reset Booleans + * @param array End Eliminate Booleans + * @param boolean Call as a parser tag + * @return void + */ + private function triggerEndResets($output, &$reset, &$eliminate, $isParserTag) { + global $wgHooks; + + $localParser = new \Parser(); + $parserOutput = $localParser->parse($output, $this->parser->mTitle, $this->parser->mOptions); + + if (!is_array($reset)) { + $reset = []; + } + $reset = array_merge($reset, (array)$this->parameters->getParameter('reset')); + + if (!is_array($eliminate)) { + $eliminate = []; + } + $eliminate = array_merge($eliminate, (array)$this->parameters->getParameter('eliminate')); + if ($isParserTag === true) { + //In tag mode 'eliminate' is the same as 'reset' for templates, categories, and images. + if (isset($eliminate['templates']) && $eliminate['templates']) { + $reset['templates'] = true; + $eliminate['templates'] = false; + } + if (isset($eliminate['categories']) && $eliminate['categories']) { + $reset['categories'] = true; + $eliminate['categories'] = false; + } + if (isset($eliminate['images']) && $eliminate['images']) { + $reset['images'] = true; + $eliminate['images'] = false; + } + } else { + if (isset($reset['templates']) && $reset['templates']) { + \DynamicPageListHooks::$createdLinks['resetTemplates'] = true; + } + if (isset($reset['categories']) && $reset['categories']) { + \DynamicPageListHooks::$createdLinks['resetCategories'] = true; + } + if (isset($reset['images']) && $reset['images']) { + \DynamicPageListHooks::$createdLinks['resetImages'] = true; + } + } + if (($isParserTag === true && isset($reset['links'])) || $isParserTag === false) { + if (isset($reset['links'])) { + \DynamicPageListHooks::$createdLinks['resetLinks'] = true; + } + //Register a hook to reset links which were produced during parsing DPL output. + if (!isset($wgHooks['ParserAfterTidy']) || !is_array($wgHooks['ParserAfterTidy']) || !in_array('DynamicPageListHooks::endReset', $wgHooks['ParserAfterTidy'])) { + $wgHooks['ParserAfterTidy'][] = 'DynamicPageListHooks::endReset'; + } + } + + if (array_sum($eliminate)) { + //Register a hook to reset links which were produced during parsing DPL output + if (!isset($wgHooks['ParserAfterTidy']) || !is_array($wgHooks['ParserAfterTidy']) || !in_array('DynamicPageListHooks::endEliminate', $wgHooks['ParserAfterTidy'])) { + $wgHooks['ParserAfterTidy'][] = 'DynamicPageListHooks::endEliminate'; + } + + if (isset($eliminate['links']) && $eliminate['links']) { + //Trigger the mediawiki parser to find links, images, categories etc. which are contained in the DPL output. This allows us to remove these links from the link list later. If the article containing the DPL statement itself uses one of these links they will be thrown away! + \DynamicPageListHooks::$createdLinks[0] = []; + foreach ($parserOutput->getLinks() as $nsp => $link) { + \DynamicPageListHooks::$createdLinks[0][$nsp] = $link; + } + } + if (isset($eliminate['templates']) && $eliminate['templates']) { + \DynamicPageListHooks::$createdLinks[1] = []; + foreach ($parserOutput->getTemplates() as $nsp => $tpl) { + \DynamicPageListHooks::$createdLinks[1][$nsp] = $tpl; + } + } + if (isset($eliminate['categories']) && $eliminate['categories']) { + \DynamicPageListHooks::$createdLinks[2] = $parserOutput->mCategories; + } + if (isset($eliminate['images']) && $eliminate['images']) { + \DynamicPageListHooks::$createdLinks[3] = $parserOutput->mImages; + } + } + } + + /** + * Sort an array of Article objects by the card suit symbol. + * + * @access private + * @param array Article objects in an array. + * @return array Sorted objects + */ + private function cardSuitSort($articles) { + $sortKeys = []; + foreach ($articles as $key => $article) { + $title = preg_replace('/.*:/', '', $article->mTitle); + $tokens = preg_split('/ - */', $title); + $newKey = ''; + foreach ($tokens as $token) { + $initial = substr($token, 0, 1); + if ($initial >= '1' && $initial <= '7') { + $newKey .= $initial; + $suit = substr($token, 1); + if ($suit == '♣') { + $newKey .= '1'; + } elseif ($suit == '♦') { + $newKey .= '2'; + } elseif ($suit == '♥') { + $newKey .= '3'; + } elseif ($suit == '♠') { + $newKey .= '4'; + } elseif (strtolower($suit) == 'sa' || strtolower($suit) == 'nt') { + $newKey .= '5 '; + } else { + $newKey .= $suit; + } + } elseif (strtolower($initial) == 'p') { + $newKey .= '0 '; + } elseif (strtolower($initial) == 'x') { + $newKey .= '8 '; + } else { + $newKey .= $token; + } + } + $sortKeys[$key] = $newKey; + } + asort($sortKeys); + foreach ($sortKeys as $oldKey => $newKey) { + $sortedArticles[] = $articles[$oldKey]; + } + return $sortedArticles; + } +} diff --git a/classes/Query.php b/classes/Query.php new file mode 100644 index 0000000..0dce8d5 --- /dev/null +++ b/classes/Query.php @@ -0,0 +1,2142 @@ +parameters = $parameters; + + $this->tableNames = self::getTableNames(); + + $this->DB = wfGetDB(DB_REPLICA, 'dpl'); + } + + /** + * Start a query build. + * + * @access public + * @param boolean Calculate Found Rows + * @return mixed Mediawiki Result Object or False + */ + public function buildAndSelect($calcRows = false) { + global $wgNonincludableNamespaces; + + $options = []; + + $parameters = $this->parameters->getAllParameters(); + foreach ($parameters as $parameter => $option) { + $function = "_" . $parameter; + //Some parameters do not modifiy the query so we check if the function to modify the query exists first. + $success = true; + if (method_exists($this, $function)) { + $success = $this->$function($option); + } + if ($success === false) { + throw new \MWException(__METHOD__ . ": SQL Build Error returned from {$function} for " . serialize($option) . "."); + } + $this->parametersProcessed[$parameter] = true; + } + + if (!$this->parameters->getParameter('openreferences')) { + //Add things that are always part of the query. + $this->addTable('page', $this->tableNames['page']); + $this->addSelect( + [ + 'page_namespace' => $this->tableNames['page'] . '.page_namespace', + 'page_id' => $this->tableNames['page'] . '.page_id', + 'page_title' => $this->tableNames['page'] . '.page_title' + ] + ); + } + //Always add nonincludeable namespaces. + if (is_array($wgNonincludableNamespaces) && count($wgNonincludableNamespaces)) { + $this->addNotWhere( + [ + $this->tableNames['page'] . '.page_namespace' => $wgNonincludableNamespaces + ] + ); + } + + if ($this->offset !== false) { + $options['OFFSET'] = $this->offset; + } + if ($this->limit !== false) { + $options['LIMIT'] = $this->limit; + } elseif ($this->offset !== false && $this->limit === false) { + $options['LIMIT'] = $this->parameters->getData('count')['default']; + } + + if ($this->parameters->getParameter('openreferences')) { + if (count($this->parameters->getParameter('imagecontainer')) > 0) { + //$sSqlSelectFrom = $sSqlCl_to.'ic.il_to, '.$sSqlSelPage."ic.il_to AS sortkey".' FROM '.$this->tableNames['imagelinks'].' AS ic'; + $tables = [ + 'ic' => 'imagelinks' + ]; + } else { + //$sSqlSelectFrom = "SELECT $sSqlCalcFoundRows $sSqlDistinct ".$sSqlCl_to.'pl_namespace, pl_title'.$sSqlSelPage.$sSqlSortkey.' FROM '.$this->tableNames['pagelinks']; + $this->addSelect( + [ + 'pl_namespace', + 'pl_title' + ] + ); + $tables = [ + 'pagelinks' + ]; + } + } else { + $tables = $this->tables; + if (count($this->groupBy)) { + $options['GROUP BY'] = $this->groupBy; + } + if (count($this->orderBy)) { + $options['ORDER BY'] = $this->orderBy; + foreach ($options['ORDER BY'] as $key => $value) { + $options['ORDER BY'][$key] .= " " . $this->direction; + } + } + } + if ($this->parameters->getParameter('goal') == 'categories') { + $categoriesGoal = true; + $select = [ + $this->tableNames['page'] . '.page_id' + ]; + $options[] = 'DISTINCT'; + } else { + if ($calcRows) { + $options[] = 'SQL_CALC_FOUND_ROWS'; + } + if ($this->distinct) { + $options[] = 'DISTINCT'; + } + $categoriesGoal = false; + $select = $this->select; + } + + $queryError = false; + try { + if ($categoriesGoal) { + $result = $this->DB->select( + $tables, + $select, + $this->where, + __METHOD__, + $options, + $this->join + ); + + while ($row = $result->fetchRow()) { + $pageIds[] = $row['page_id']; + } + $sql = $this->DB->selectSQLText( + [ + 'clgoal' => 'categorylinks' + ], + [ + 'clgoal.cl_to' + ], + [ + 'clgoal.cl_from' => $pageIds + ], + __METHOD__, + [ + 'ORDER BY' => 'clgoal.cl_to ' . $this->direction + ] + ); + } else { + $sql = $this->DB->selectSQLText( + $tables, + $select, + $this->where, + __METHOD__, + $options, + $this->join + ); + } + + $this->sqlQuery = $sql; + $result = $this->DB->query($sql, __METHOD__); + + if ($calcRows) { + $calcRowsResult = $this->DB->query('SELECT FOUND_ROWS() AS rowcount', __METHOD__); + $total = $this->DB->fetchRow($calcRowsResult); + $this->foundRows = intval($total['rowcount']); + $this->DB->freeResult($calcRowsResult); + } + } catch (Exception $e) { + $queryError = true; + } + if ($queryError == true || $result === false) { + throw new \MWException(__METHOD__ . ": " . wfMessage('dpl_query_error', DPL_VERSION, $this->DB->lastError())->text()); + } + + return $result; + } + + /** + * Return the number of found rows. + * + * @access public + * @return integer Number of Found Rows + */ + public function getFoundRows() { + return $this->foundRows; + } + + /** + * Returns the generated SQL Query + * + * @access public + * @return string SQL Query + */ + public function getSqlQuery() { + return $this->sqlQuery; + } + + /** + * Return prefixed and quoted tables that are needed. + * + * @access public + * @return array Prepared table names. + */ + public static function getTableNames() { + $DB = wfGetDB(DB_REPLICA, 'dpl'); + $tables = [ + 'categorylinks', + 'dpl_clview', + 'externallinks', + 'flaggedpages', + 'imagelinks', + 'page', + 'pagelinks', + 'recentchanges', + 'revision', + 'templatelinks' + ]; + + $tableNames = []; + foreach ($tables as $table) { + $tableNames[$table] = $DB->tableName($table); + } + return $tableNames; + } + + /** + * Add a table to the output. + * + * @access public + * @param string Raw Table Name - Will be ran through tableName(). + * @param string Table Alias + * @return boolean Success - Added, false if the table alias already exists. + */ + public function addTable($table, $alias) { + if (empty($table)) { + throw new \MWException(__METHOD__ . ': An empty table name was passed.'); + } + if (empty($alias) || is_numeric($alias)) { + throw new \MWException(__METHOD__ . ': An empty or numeric table alias was passed.'); + } + if (!isset($this->tables[$alias])) { + $this->tables[$alias] = $this->DB->tableName($table); + return true; + } else { + return false; + } + } + + /** + * Add a where clause to the output. + * Where clauses get imploded together with AND at the end. Any custom where clauses should be preformed before placed into here. + * + * @access public + * @param string Where clause + * @return boolean Success + */ + public function addWhere($where) { + if (empty($where)) { + throw new \MWException(__METHOD__ . ': An empty where clause was passed.'); + } + if (is_string($where)) { + $this->where[] = $where; + } elseif (is_array($where)) { + $this->where = array_merge($this->where, $where); + } else { + throw new \MWException(__METHOD__ . ': An invalid where clause was passed.'); + return false; + } + return true; + } + + /** + * Add a where clause to the output that uses NOT IN or !=. + * + * @access public + * @param array Field => Value(s) + * @return boolean Success + */ + public function addNotWhere($where) { + if (empty($where)) { + throw new \MWException(__METHOD__ . ': An empty not where clause was passed.'); + return false; + } + if (is_array($where)) { + foreach ($where as $field => $values) { + $this->where[] = $field . (count($values) > 1 ? ' NOT IN(' . $this->DB->makeList($values) . ')' : ' != ' . $this->DB->addQuotes(current($values))); + } + } else { + throw new \MWException(__METHOD__ . ': An invalid not where clause was passed.'); + return false; + } + return true; + } + + /** + * Add a field to select. + * Will ignore duplicate values if the exact same alias and exact same field are passed. + * + * @access public + * @param array Array of fields with the array key being the field alias. Leave the array key as a numeric index to not specify an alias. + * @return boolean Success + */ + public function addSelect($fields) { + if (!is_array($fields)) { + throw new \MWException(__METHOD__ . ': A non-array was passed.'); + } + foreach ($fields as $alias => $field) { + if (!is_numeric($alias) && array_key_exists($alias, $this->select) && $this->select[$alias] != $field) { + //In case of a code bug that is overwriting an existing field alias throw an exception. + throw new \MWException(__METHOD__ . ": Attempted to overwrite existing field alias `{$this->select[$alias]}` AS `{$alias}` with `{$field}` AS `{$alias}`."); + } + //String alias and does not exist already. + if (!is_numeric($alias) && !array_key_exists($alias, $this->select)) { + $this->select[$alias] = $field; + } + + //Speed up by not using in_array() or array_key_exists(). Toss the field names into their own array as keys => true to exploit a speedy look up with isset(). + if (is_numeric($alias) && !isset($this->selectedFields[$field])) { + $this->select[] = $field; + $this->selectedFields[$field] = true; + } + } + return true; + } + + /** + * Add a GROUP BY clause to the output. + * + * @access public + * @param string Group By Clause + * @return boolean Success + */ + public function addGroupBy($groupBy) { + if (empty($groupBy)) { + throw new \MWException(__METHOD__ . ': An empty group by clause was passed.'); + } + $this->groupBy[] = $groupBy; + return true; + } + + /** + * Add a ORDER BY clause to the output. + * + * @access public + * @param string Order By Clause + * @return boolean Success + */ + public function addOrderBy($orderBy) { + if (empty($orderBy)) { + throw new \MWException(__METHOD__ . ': An empty order by clause was passed.'); + } + $this->orderBy[] = $orderBy; + return true; + } + + /** + * Add a JOIN clause to the output. + * + * @access public + * @param string Table Alias + * @param array Join Conditions in the format of the join type to the on where condition. Example: ['JOIN TYPE' => 'this = that'] + * @return boolean Success + */ + public function addJoin($tableAlias, $joinConditions) { + if (empty($tableAlias) || empty($joinConditions)) { + throw new \MWException(__METHOD__ . ': An empty join clause was passed.'); + } + if (isset($this->join[$tableAlias])) { + throw new \MWException(__METHOD__ . ': Attempted to overwrite existing join clause.'); + } + $this->join[$tableAlias] = $joinConditions; + return true; + } + + /** + * Set the limit. + * + * @access public + * @param mixed Integer limit or false to unset. + * @return boolean Success + */ + public function setLimit($limit) { + if (is_numeric($limit)) { + $this->limit = intval($limit); + } else { + $this->limit = false; + } + return true; + } + + /** + * Set the offset. + * + * @access public + * @param mixed Integer offset or false to unset. + * @return boolean Success + */ + public function setOffset($offset) { + if (is_numeric($offset)) { + $this->offset = intval($offset); + } else { + $this->offset = false; + } + return true; + } + + /** + * Set the ORDER BY direction + * + * @access public + * @param string SQL direction key word. + * @return boolean Success + */ + public function setOrderDir($direction) { + $this->direction = $direction; + return true; + } + + /** + * Set the character set collation. + * + * @access public + * @param string Collation + * @return void + */ + public function setCollation($collation) { + $this->collation = $collation; + } + + /** + * Return SQL prefixed collation. + * + * @access public + * @return string SQL Collation + */ + public function getCollateSQL() { + return ($this->collation !== false ? 'COLLATE ' . $this->collation : null); + } + + /** + * Recursively get and return an array of subcategories. + * + * @access public + * @param string Category Name + * @param integer [Optional] Maximum Depth + * @return array Subcategories + */ + public static function getSubcategories($categoryName, $depth = 1) { + $DB = wfGetDB(DB_REPLICA, 'dpl'); + + if ($depth > 2) { + //Hard constrain depth because lots of recursion is bad. + $depth = 2; + } + $categories = []; + $result = $DB->select( + ['page', 'categorylinks'], + ['page_title'], + [ + 'page_namespace' => intval(NS_CATEGORY), + 'categorylinks.cl_to' => str_replace(' ', '_', $categoryName) + ], + __METHOD__, + ['DISTINCT'], + [ + 'categorylinks' => [ + 'INNER JOIN', + 'page.page_id = categorylinks.cl_from' + ] + ] + ); + while ($row = $result->fetchRow()) { + $categories[] = $row['page_title']; + if ($depth > 1) { + $categories = array_merge($categories, self::getSubcategories($row['page_title'], $depth - 1)); + } + } + $categories = array_unique($categories); + $DB->freeResult($result); + return $categories; + } + + /** + * Helper method to handle relative timestamps. + * + * @access private + * @param mixed Integer or string + * @return integer + */ + private function convertTimestamp($inputDate) { + $timestamp = $inputDate; + switch ($inputDate) { + case 'today': + $timestamp = date('YmdHis'); + break; + case 'last hour': + $date = new \DateTime(); + $date->sub(new \DateInterval('P1H')); + $timestamp = $date->format('YmdHis'); + break; + case 'last day': + $date = new \DateTime(); + $date->sub(new \DateInterval('P1D')); + $timestamp = $date->format('YmdHis'); + break; + case 'last week': + $date = new \DateTime(); + $date->sub(new \DateInterval('P7D')); + $timestamp = $date->format('YmdHis'); + break; + case 'last month': + $date = new \DateTime(); + $date->sub(new \DateInterval('P1M')); + $timestamp = $date->format('YmdHis'); + break; + case 'last year': + $date = new \DateTime(); + $date->sub(new \DateInterval('P1Y')); + $timestamp = $date->format('YmdHis'); + break; + } + + if (is_numeric($timestamp)) { + return $this->DB->addQuotes($timestamp); + } + return 0; + } + + /** + * Set SQL for 'addauthor' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _addauthor($option) { + //Addauthor can not be used with addlasteditor. + if (!isset($this->parametersProcessed['addlasteditor']) || !$this->parametersProcessed['addlasteditor']) { + $this->addTable('revision', 'rev'); + $this->addWhere( + [ + $this->tableNames['page'] . '.page_id = rev.rev_page', + 'rev.rev_timestamp = (SELECT MIN(rev_aux_min.rev_timestamp) FROM ' . $this->tableNames['revision'] . ' AS rev_aux_min WHERE rev_aux_min.rev_page = rev.rev_page)' + ] + ); + $this->_adduser(null, 'rev'); + } + } + + /** + * Set SQL for 'addcategories' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _addcategories($option) { + $this->addTable('categorylinks', 'cl_gc'); + $this->addSelect( + [ + 'cats' => "GROUP_CONCAT(DISTINCT cl_gc.cl_to ORDER BY cl_gc.cl_to ASC SEPARATOR ' | ')" + ] + ); + $this->addJoin( + 'cl_gc', + [ + 'LEFT OUTER JOIN', + 'page_id = cl_gc.cl_from' + ] + ); + $this->addGroupBy($this->tableNames['page'] . '.page_id'); + } + + /** + * Set SQL for 'addcontribution' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _addcontribution($option) { + $this->addTable('recentchanges', 'rc'); + + $field = 'rc.rc_actor'; + + $this->addSelect( + [ + 'contribution' => 'SUM(ABS(rc.rc_new_len - rc.rc_old_len))', + 'contributor' => $field + ] + ); + $this->addWhere( + [ + $this->tableNames['page'] . '.page_id = rc.rc_cur_id' + ] + ); + $this->addGroupBy('rc.rc_cur_id'); + } + + /** + * Set SQL for 'addeditdate' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _addeditdate($option) { + $this->addTable('revision', 'rev'); + $this->addSelect(['rev.rev_timestamp']); + $this->addWhere( + [ + $this->tableNames['page'] . '.page_id = rev.rev_page', + ] + ); + } + + /** + * Set SQL for 'addfirstcategorydate' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _addfirstcategorydate($option) { + //@TODO: This should be programmatically determining which categorylink table to use instead of assuming the first one. + $this->addSelect( + [ + 'cl_timestamp' => "DATE_FORMAT(cl1.cl_timestamp, '%Y%m%d%H%i%s')" + ] + ); + } + + /** + * Set SQL for 'addlasteditor' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _addlasteditor($option) { + //Addlasteditor can not be used with addauthor. + if (!isset($this->parametersProcessed['addauthor']) || !$this->parametersProcessed['addauthor']) { + $this->addTable('revision', 'rev'); + $this->addWhere( + [ + $this->tableNames['page'] . '.page_id = rev.rev_page', + 'rev.rev_timestamp = (SELECT MAX(rev_aux_max.rev_timestamp) FROM ' . $this->tableNames['revision'] . ' AS rev_aux_max WHERE rev_aux_max.rev_page = rev.rev_page)' + ] + ); + $this->_adduser(null, 'rev'); + } + } + + /** + * Set SQL for 'addpagecounter' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _addpagecounter($option) { + if (class_exists("\\HitCounters\\Hooks")) { + $this->addTable('hit_counter', 'hit_counter'); + $this->addSelect( + [ + "page_counter" => "hit_counter.page_counter" + ] + ); + if (!isset($this->join['hit_counter'])) { + $this->addJoin( + 'hit_counter', + [ + "LEFT JOIN", + "hit_counter.page_id = " . $this->tableNames['page'] . '.page_id' + ] + ); + } + } + } + + /** + * Set SQL for 'addpagesize' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _addpagesize($option) { + $this->addSelect( + [ + "page_len" => "{$this->tableNames['page']}.page_len" + ] + ); + } + + /** + * Set SQL for 'addpagetoucheddate' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _addpagetoucheddate($option) { + $this->addSelect( + [ + "page_touched" => "{$this->tableNames['page']}.page_touched" + ] + ); + } + + /** + * Set SQL for 'adduser' parameter. + * + * @access private + * @param mixed Parameter Option + * @param string [Optional] Table Alias + * @return void + */ + private function _adduser($option, $tableAlias = '') { + $tableAlias = (!empty($tableAlias) ? $tableAlias . '.' : ''); + $this->addSelect( + [ + $tableAlias . 'rev_actor', + $tableAlias . 'rev_comment_id' + ] + ); + } + + /** + * Set SQL for 'allrevisionsbefore' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _allrevisionsbefore($option) { + $this->addTable('revision', 'rev'); + $this->addSelect( + [ + 'rev.rev_id', + 'rev.rev_timestamp' + ] + ); + $this->addOrderBy('rev.rev_id'); + $this->setOrderDir('DESC'); + $this->addWhere( + [ + $this->tableNames['page'] . '.page_id = rev.rev_page', + 'rev.rev_timestamp < ' . $this->convertTimestamp($option) + ] + ); + } + + /** + * Set SQL for 'allrevisionssince' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _allrevisionssince($option) { + $this->addTable('revision', 'rev'); + $this->addSelect( + [ + 'rev.rev_id', + 'rev.rev_timestamp' + ] + ); + $this->addOrderBy('rev.rev_id'); + $this->setOrderDir('DESC'); + $this->addWhere( + [ + $this->tableNames['page'] . '.page_id = rev.rev_page', + 'rev.rev_timestamp >= ' . $this->convertTimestamp($option) + ] + ); + } + + /** + * Set SQL for 'articlecategory' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _articlecategory($option) { + $this->addWhere("{$this->tableNames['page']}.page_title IN (SELECT p2.page_title FROM {$this->tableNames['page']} p2 INNER JOIN {$this->tableNames['categorylinks']} clstc ON (clstc.cl_from = p2.page_id AND clstc.cl_to = " . $this->DB->addQuotes($option) . ") WHERE p2.page_namespace = 0)"); + } + + /** + * Set SQL for 'categoriesminmax' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _categoriesminmax($option) { + if (is_numeric($option[0])) { + $this->addWhere(intval($option[0]) . ' <= (SELECT count(*) FROM ' . $this->tableNames['categorylinks'] . ' WHERE ' . $this->tableNames['categorylinks'] . '.cl_from=page_id)'); + } + if (is_numeric($option[1])) { + $this->addWhere(intval($option[1]) . ' >= (SELECT count(*) FROM ' . $this->tableNames['categorylinks'] . ' WHERE ' . $this->tableNames['categorylinks'] . '.cl_from=page_id)'); + } + } + + /** + * Set SQL for 'category' parameter. This includes 'category', 'categorymatch', and 'categoryregexp'. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _category($option) { + $i = 0; + foreach ($option as $comparisonType => $operatorTypes) { + foreach ($operatorTypes as $operatorType => $categoryGroups) { + foreach ($categoryGroups as $categories) { + if (!is_array($categories)) { + continue; + } + $tableName = (in_array('', $categories) ? 'dpl_clview' : 'categorylinks'); + if ($operatorType == 'AND') { + foreach ($categories as $category) { + $i++; + $tableAlias = "cl{$i}"; + $this->addTable($tableName, $tableAlias); + $this->addJoin( + $tableAlias, + [ + 'INNER JOIN', + "{$this->tableNames['page']}.page_id = {$tableAlias}.cl_from AND $tableAlias.cl_to {$comparisonType} " . $this->DB->addQuotes(str_replace(' ', '_', $category)) + ] + ); + } + } elseif ($operatorType == 'OR') { + $i++; + $tableAlias = "cl{$i}"; + $this->addTable($tableName, $tableAlias); + + $joinOn = "{$this->tableNames['page']}.page_id = {$tableAlias}.cl_from AND ("; + $ors = []; + foreach ($categories as $category) { + $ors[] = "{$tableAlias}.cl_to {$comparisonType} " . $this->DB->addQuotes(str_replace(' ', '_', $category)); + } + $joinOn .= implode(" {$operatorType} ", $ors); + $joinOn .= ')'; + + $this->addJoin( + $tableAlias, + [ + 'INNER JOIN', + $joinOn + ] + ); + } + } + } + } + } + + /** + * Set SQL for 'notcategory' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _notcategory($option) { + $i = 0; + foreach ($option as $operatorType => $categories) { + foreach ($categories as $category) { + $i++; + + $tableAlias = "ecl{$i}"; + $this->addTable('categorylinks', $tableAlias); + + $this->addJoin( + $tableAlias, + [ + 'LEFT OUTER JOIN', + "{$this->tableNames['page']}.page_id = {$tableAlias}.cl_from AND {$tableAlias}.cl_to {$operatorType}" . $this->DB->addQuotes(str_replace(' ', '_', $category)) + ] + ); + $this->addWhere( + [ + "{$tableAlias}.cl_to" => null + ] + ); + } + } + } + + /** + * Set SQL for 'createdby' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _createdby($option) { + $this->addTable('revision', 'creation_rev'); + $this->_adduser(null, 'creation_rev'); + $this->addWhere( + [ + $this->DB->addQuotes($option) . ' = creation_rev.rev_actor', + 'creation_rev.rev_page = page_id', + 'creation_rev.rev_parent_id = 0' + ] + ); + } + + /** + * Set SQL for 'distinct' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _distinct($option) { + if ($option == 'strict' || $option === true) { + $this->distinct = true; + } else { + $this->distinct = false; + } + } + + /** + * Set SQL for 'firstrevisionsince' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _firstrevisionsince($option) { + $this->addTable('revision', 'rev'); + $this->addSelect( + [ + 'rev.rev_id', + 'rev.rev_timestamp' + ] + ); + // tell the query optimizer not to look at rows that the following subquery will filter out anyway + $this->addWhere( + [ + $this->tableNames['page'] . '.page_id = rev.rev_page', + 'rev.rev_timestamp >= ' . $this->DB->addQuotes($option) + ] + ); + $this->addWhere( + [ + $this->tableNames['page'] . '.page_id = rev.rev_page', + 'rev.rev_timestamp = (SELECT MIN(rev_aux_snc.rev_timestamp) FROM ' . $this->tableNames['revision'] . ' AS rev_aux_snc WHERE rev_aux_snc.rev_page=rev.rev_page AND rev_aux_snc.rev_timestamp >= ' . $this->convertTimestamp($option) . ')' + ] + ); + } + + /** + * Set SQL for 'goal' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _goal($option) { + if ($option == 'categories') { + $this->setLimit(false); + $this->setOffset(false); + } + } + + /** + * Set SQL for 'hiddencategories' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _hiddencategories($option) { + //@TODO: Unfinished functionality! Never implemented by original author. + } + + /** + * Set SQL for 'imagecontainer' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _imagecontainer($option) { + $this->addTable('imagelinks', 'ic'); + $this->addSelect( + [ + 'sortkey' => 'ic.il_to' + ] + ); + if (!$this->parameters->getParameter('openreferences')) { + $where = [ + "{$this->tableNames['page']}.page_namespace = " . intval(NS_FILE), + "{$this->tableNames['page']}.page_title = ic.il_to" + ]; + } + $ors = []; + foreach ($option as $linkGroup) { + foreach ($linkGroup as $link) { + if ($this->parameters->getParameter('ignorecase')) { + $ors[] = "LOWER(CAST(ic.il_from AS char) = LOWER(" . $this->DB->addQuotes($link->getArticleID()) . ')'; + } else { + $ors[] = "ic.il_from = " . $this->DB->addQuotes($link->getArticleID()); + } + } + } + $where[] = '(' . implode(' OR ', $ors) . ')'; + $this->addWhere($where); + } + + /** + * Set SQL for 'imageused' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _imageused($option) { + if ($this->parameters->getParameter('distinct') == 'strict') { + $this->addGroupBy('page_title'); + } + $this->addTable('imagelinks', 'il'); + $this->addSelect( + [ + 'image_sel_title' => 'il.il_to' + ] + ); + $where[] = $this->tableNames['page'] . '.page_id = il.il_from'; + $ors = []; + foreach ($option as $linkGroup) { + foreach ($linkGroup as $link) { + if ($this->parameters->getParameter('ignorecase')) { + $ors[] = "LOWER(CAST(il.il_to AS char))=LOWER(" . $this->DB->addQuotes($link->getDbKey()) . ')'; + } else { + $ors[] = "il.il_to=" . $this->DB->addQuotes($link->getDbKey()); + } + } + } + $where[] = '(' . implode(' OR ', $ors) . ')'; + $this->addWhere($where); + } + + /** + * Set SQL for 'lastmodifiedby' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _lastmodifiedby($option) { + $this->addWhere($this->DB->addQuotes($option) . ' = (SELECT rev_actor FROM ' . $this->tableNames['revision'] . ' WHERE ' . $this->tableNames['revision'] . '.rev_page=page_id ORDER BY ' . $this->tableNames['revision'] . '.rev_timestamp DESC LIMIT 1)'); + } + + /** + * Set SQL for 'lastrevisionbefore' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _lastrevisionbefore($option) { + $this->addTable('revision', 'rev'); + $this->addSelect(['rev.rev_id', 'rev.rev_timestamp']); + // tell the query optimizer not to look at rows that the following subquery will filter out anyway + $this->addWhere( + [ + $this->tableNames['page'] . '.page_id = rev.rev_page', + 'rev.rev_timestamp < ' . $this->convertTimestamp($option) + ] + ); + $this->addWhere( + [ + $this->tableNames['page'] . '.page_id = rev.rev_page', + 'rev.rev_timestamp = (SELECT MAX(rev_aux_bef.rev_timestamp) FROM ' . $this->tableNames['revision'] . ' AS rev_aux_bef WHERE rev_aux_bef.rev_page=rev.rev_page AND rev_aux_bef.rev_timestamp < ' . $this->convertTimestamp($option) . ')' + ] + ); + } + + /** + * Set SQL for 'linksfrom' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _linksfrom($option) { + if ($this->parameters->getParameter('distinct') == 'strict') { + $this->addGroupBy('page_title'); + } + if ($this->parameters->getParameter('openreferences')) { + $ors = []; + foreach ($option as $linkGroup) { + foreach ($linkGroup as $link) { + $ors[] = '(pl_from = ' . $link->getArticleID() . ')'; + } + } + $where[] = '(' . implode(' OR ', $ors) . ')'; + } else { + $this->addTable('pagelinks', 'plf'); + $this->addTable('page', 'pagesrc'); + $this->addSelect( + [ + 'sel_title' => 'pagesrc.page_title', + 'sel_ns' => 'pagesrc.page_namespace' + ] + ); + $where = [ + $this->tableNames['page'] . '.page_namespace = plf.pl_namespace', + $this->tableNames['page'] . '.page_title = plf.pl_title', + 'pagesrc.page_id = plf.pl_from' + ]; + $ors = []; + foreach ($option as $linkGroup) { + foreach ($linkGroup as $link) { + $ors[] = 'plf.pl_from = ' . $link->getArticleID(); + } + } + $where[] = '(' . implode(' OR ', $ors) . ')'; + } + $this->addWhere($where); + } + + /** + * Set SQL for 'linksto' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _linksto($option) { + if ($this->parameters->getParameter('distinct') == 'strict') { + $this->addGroupBy('page_title'); + } + if (count($option) > 0) { + $this->addTable('pagelinks', 'pl'); + $this->addSelect(['sel_title' => 'pl.pl_title', 'sel_ns' => 'pl.pl_namespace']); + foreach ($option as $index => $linkGroup) { + if ($index == 0) { + $where = $this->tableNames['page'] . '.page_id=pl.pl_from AND '; + $ors = []; + foreach ($linkGroup as $link) { + $_or = '(pl.pl_namespace=' . intval($link->getNamespace()); + if (strpos($link->getDbKey(), '%') >= 0) { + $operator = 'LIKE'; + } else { + $operator = '='; + } + if ($this->parameters->getParameter('ignorecase')) { + $_or .= ' AND LOWER(CAST(pl.pl_title AS char)) ' . $operator . ' LOWER(' . $this->DB->addQuotes($link->getDbKey()) . ')'; + } else { + $_or .= ' AND pl.pl_title ' . $operator . ' ' . $this->DB->addQuotes($link->getDbKey()); + } + $_or .= ')'; + $ors[] = $_or; + } + $where .= '(' . implode(' OR ', $ors) . ')'; + } else { + $where = 'EXISTS(select pl_from FROM ' . $this->tableNames['pagelinks'] . ' WHERE (' . $this->tableNames['pagelinks'] . '.pl_from=page_id AND '; + $ors = []; + foreach ($linkGroup as $link) { + $_or = '(' . $this->tableNames['pagelinks'] . '.pl_namespace=' . intval($link->getNamespace()); + if (strpos($link->getDbKey(), '%') >= 0) { + $operator = 'LIKE'; + } else { + $operator = '='; + } + if ($this->parameters->getParameter('ignorecase')) { + $_or .= ' AND LOWER(CAST(' . $this->tableNames['pagelinks'] . '.pl_title AS char)) ' . $operator . ' LOWER(' . $this->DB->addQuotes($link->getDbKey()) . ')'; + } else { + $_or .= ' AND ' . $this->tableNames['pagelinks'] . '.pl_title ' . $operator . ' ' . $this->DB->addQuotes($link->getDbKey()); + } + $_or .= ')'; + $ors[] = $_or; + } + $where .= '(' . implode(' OR ', $ors) . ')'; + $where .= '))'; + } + $this->addWhere($where); + } + } + } + + /** + * Set SQL for 'notlinksfrom' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _notlinksfrom($option) { + if ($this->parameters->getParameter('distinct') == 'strict') { + $this->addGroupBy('page_title'); + } + if ($this->parameters->getParameter('openreferences')) { + $ands = []; + foreach ($option as $linkGroup) { + foreach ($linkGroup as $link) { + $ands[] = 'pl_from <> ' . intval($link->getArticleID()) . ' '; + } + } + $where = '(' . implode(' AND ', $ands) . ')'; + } else { + $where = 'CONCAT(page_namespace,page_title) NOT IN (SELECT CONCAT(' . $this->tableNames['pagelinks'] . '.pl_namespace,' . $this->tableNames['pagelinks'] . '.pl_title) FROM ' . $this->tableNames['pagelinks'] . ' WHERE '; + $ors = []; + foreach ($option as $linkGroup) { + foreach ($linkGroup as $link) { + $ors[] = $this->tableNames['pagelinks'] . '.pl_from = ' . intval($link->getArticleID()); + } + } + $where .= implode(' OR ', $ors) . ')'; + } + $this->addWhere($where); + } + + /** + * Set SQL for 'notlinksto' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _notlinksto($option) { + if ($this->parameters->getParameter('distinct') == 'strict') { + $this->addGroupBy('page_title'); + } + if (count($option)) { + $where = $this->tableNames['page'] . '.page_id NOT IN (SELECT ' . $this->tableNames['pagelinks'] . '.pl_from FROM ' . $this->tableNames['pagelinks'] . ' WHERE '; + $ors = []; + foreach ($option as $linkGroup) { + foreach ($linkGroup as $link) { + $_or = '(' . $this->tableNames['pagelinks'] . '.pl_namespace=' . intval($link->getNamespace()); + if (strpos($link->getDbKey(), '%') >= 0) { + $operator = 'LIKE'; + } else { + $operator = '='; + } + if ($this->parameters->getParameter('ignorecase')) { + $_or .= ' AND LOWER(CAST(' . $this->tableNames['pagelinks'] . '.pl_title AS char)) ' . $operator . ' LOWER(' . $this->DB->addQuotes($link->getDbKey()) . '))'; + } else { + $_or .= ' AND ' . $this->tableNames['pagelinks'] . '.pl_title ' . $operator . ' ' . $this->DB->addQuotes($link->getDbKey()) . ')'; + } + $ors[] = $_or; + } + } + $where .= '(' . implode(' OR ', $ors) . '))'; + } + $this->addWhere($where); + } + + /** + * Set SQL for 'linkstoexternal' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _linkstoexternal($option) { + if ($this->parameters->getParameter('distinct') == 'strict') { + $this->addGroupBy('page_title'); + } + if (count($option) > 0) { + $this->addTable('externallinks', 'el'); + $this->addSelect(['el_to' => 'el.el_to']); + foreach ($option as $index => $linkGroup) { + if ($index == 0) { + $where = $this->tableNames['page'] . '.page_id=el.el_from AND '; + $ors = []; + foreach ($linkGroup as $link) { + $ors[] = 'el.el_to LIKE ' . $this->DB->addQuotes($link); + } + $where .= '(' . implode(' OR ', $ors) . ')'; + } else { + $where = 'EXISTS(SELECT el_from FROM ' . $this->tableNames['externallinks'] . ' WHERE (' . $this->tableNames['externallinks'] . '.el_from=page_id AND '; + $ors = []; + foreach ($linkGroup as $link) { + $ors[] = $this->tableNames['externallinks'] . '.el_to LIKE ' . $this->DB->addQuotes($link); + } + $where .= '(' . implode(' OR ', $ors) . ')'; + $where .= '))'; + } + $this->addWhere($where); + } + } + } + + /** + * Set SQL for 'maxrevisions' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _maxrevisions($option) { + $this->addWhere("((SELECT count(rev_aux3.rev_page) FROM {$this->tableNames['revision']} AS rev_aux3 WHERE rev_aux3.rev_page = {$this->tableNames['page']}.page_id) <= {$option})"); + } + + /** + * Set SQL for 'minoredits' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _minoredits($option) { + if (isset($option) && $option == 'exclude') { + $this->addTable('revision', 'rev'); + $this->addWhere('rev.rev_minor_edit = 0'); + } + } + + /** + * Set SQL for 'minrevisions' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _minrevisions($option) { + $this->addWhere("((SELECT count(rev_aux2.rev_page) FROM {$this->tableNames['revision']} AS rev_aux2 WHERE rev_aux2.rev_page = {$this->tableNames['page']}.page_id) >= {$option})"); + } + + /** + * Set SQL for 'modifiedby' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _modifiedby($option) { + $this->addTable('revision', 'change_rev'); + $this->addWhere($this->DB->addQuotes($option) . ' = change_rev.rev_actor AND change_rev.rev_page = page_id'); + } + + /** + * Set SQL for 'namespace' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _namespace($option) { + if (is_array($option) && count($option)) { + if ($this->parameters->getParameter('openreferences')) { + $this->addWhere( + [ + "{$this->tableNames['pagelinks']}.pl_namespace" => $option + ] + ); + } else { + $this->addWhere( + [ + "{$this->tableNames['page']}.page_namespace" => $option + ] + ); + } + } + } + + /** + * Set SQL for 'notcreatedby' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _notcreatedby($option) { + $this->addTable('revision', 'no_creation_rev'); + $this->addWhere($this->DB->addQuotes($option) . ' != no_creation_rev.rev_actor AND no_creation_rev.rev_page = page_id AND no_creation_rev.rev_parent_id = 0'); + } + + /** + * Set SQL for 'notlastmodifiedby' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _notlastmodifiedby($option) { + $this->addWhere($this->DB->addQuotes($option) . ' != (SELECT rev_actor FROM ' . $this->tableNames['revision'] . ' WHERE ' . $this->tableNames['revision'] . '.rev_page=page_id ORDER BY ' . $this->tableNames['revision'] . '.rev_timestamp DESC LIMIT 1)'); + } + + /** + * Set SQL for 'notmodifiedby' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _notmodifiedby($option) { + $this->addWhere('NOT EXISTS (SELECT 1 FROM ' . $this->tableNames['revision'] . ' WHERE ' . $this->tableNames['revision'] . '.rev_page=page_id AND ' . $this->tableNames['revision'] . '.rev_actor = ' . $this->DB->addQuotes($option) . ' LIMIT 1)'); + } + + /** + * Set SQL for 'notnamespace' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _notnamespace($option) { + if (is_array($option) && count($option)) { + if ($this->parameters->getParameter('openreferences')) { + $this->addNotWhere( + [ + "{$this->tableNames['pagelinks']}.pl_namespace" => $option + ] + ); + } else { + $this->addNotWhere( + [ + "{$this->tableNames['page']}.page_namespace" => $option + ] + ); + } + } + } + + /** + * Set SQL for 'count' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _count($option) { + $this->setLimit($option); + } + + /** + * Set SQL for 'offset' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _offset($option) { + $this->setOffset($option); + } + + /** + * Set SQL for 'order' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _order($option) { + $orderMethod = $this->parameters->getParameter('ordermethod'); + if (!empty($orderMethod) && is_array($orderMethod) && $orderMethod[0] !== 'none') { + if ($option === 'descending' || $option === 'desc') { + $this->setOrderDir('DESC'); + } else { + $this->setOrderDir('ASC'); + } + } + } + + /** + * Set SQL for 'ordercollation' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _ordercollation($option) { + $option = mb_strtolower($option); + + $results = $this->DB->query('SHOW CHARACTER SET'); + if (!$results) { + return false; + } + + while ($row = $results->fetchRow()) { + if ($option == $row['Default collation']) { + $this->setCollation($option); + break; + } + } + return true; + } + + /** + * Set SQL for 'ordermethod' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _ordermethod($option) { + global $wgContLang; + + if ($this->parameters->getParameter('goal') == 'categories') { + //No order methods for returning categories. + return true; + } + + $namespaces = $wgContLang->getNamespaces(); + //$aStrictNs = array_slice((array) Config::getSetting('allowedNamespaces'), 1, count(Config::getSetting('allowedNamespaces')), true); + $namespaces = array_slice($namespaces, 3, count($namespaces), true); + $_namespaceIdToText = "CASE {$this->tableNames['page']}.page_namespace"; + foreach ($namespaces as $id => $name) { + $_namespaceIdToText .= ' WHEN ' . intval($id) . " THEN " . $this->DB->addQuotes($name . ':'); + } + $_namespaceIdToText .= ' END'; + + $option = (array)$option; + foreach ($option as $orderMethod) { + switch ($orderMethod) { + case 'category': + $this->addOrderBy('cl_head.cl_to'); + $this->addSelect(['cl_head.cl_to']); //Gives category headings in the result. + if ((is_array($this->parameters->getParameter('catheadings')) && in_array('', $this->parameters->getParameter('catheadings'))) || (is_array($this->parameters->getParameter('catnotheadings')) && in_array('', $this->parameters->getParameter('catnotheadings')))) { + $_clTableName = 'dpl_clview'; + $_clTableAlias = $_clTableName; + } else { + $_clTableName = 'categorylinks'; + $_clTableAlias = 'cl_head'; + } + $this->addTable($_clTableName, $_clTableAlias); + $this->addJoin( + $_clTableAlias, + [ + "LEFT OUTER JOIN", + "page_id = cl_head.cl_from" + ] + ); + if (is_array($this->parameters->getParameter('catheadings')) && count($this->parameters->getParameter('catheadings'))) { + $this->addWhere( + [ + "cl_head.cl_to" => $this->parameters->getParameter('catheadings') + ] + ); + } + if (is_array($this->parameters->getParameter('catnotheadings')) && count($this->parameters->getParameter('catnotheadings'))) { + $this->addNotWhere( + [ + 'cl_head.cl_to' => $this->parameters->getParameter('catnotheadings') + ] + ); + } + break; + case 'categoryadd': + //@TODO: See TODO in __addfirstcategorydate(). + $this->addOrderBy('cl1.cl_timestamp'); + break; + case 'counter': + if (class_exists("\\HitCounters\\Hooks")) { + //If the "addpagecounter" parameter was not used the table and join need to be added now. + if (!array_key_exists('hit_counter', $this->tables)) { + $this->addTable('hit_counter', 'hit_counter'); + + if (!isset($this->join['hit_counter'])) { + $this->addJoin( + 'hit_counter', + [ + "LEFT JOIN", + "hit_counter.page_id = " . $this->tableNames['page'] . '.page_id' + ] + ); + } + } + $this->addOrderBy('hit_counter.page_counter'); + } + break; + case 'firstedit': + $this->addOrderBy('rev.rev_timestamp'); + $this->addTable('revision', 'rev'); + $this->addSelect( + [ + 'rev.rev_timestamp' + ] + ); + if (!$this->revisionAuxWhereAdded) { + $this->addWhere( + [ + "{$this->tableNames['page']}.page_id = rev.rev_page", + "rev.rev_timestamp = (SELECT MIN(rev_aux.rev_timestamp) FROM {$this->tableNames['revision']} AS rev_aux WHERE rev_aux.rev_page=rev.rev_page)" + ] + ); + } + $this->revisionAuxWhereAdded = true; + break; + case 'lastedit': + if (\DynamicPageListHooks::isLikeIntersection()) { + $this->addOrderBy('page_touched'); + $this->addSelect( + [ + "page_touched" => "{$this->tableNames['page']}.page_touched" + ] + ); + } else { + $this->addOrderBy('rev.rev_timestamp'); + $this->addTable('revision', 'rev'); + $this->addSelect(['rev.rev_timestamp']); + if (!$this->revisionAuxWhereAdded) { + $this->addWhere( + [ + "{$this->tableNames['page']}.page_id = rev.rev_page", + "rev.rev_timestamp = (SELECT MAX(rev_aux.rev_timestamp) FROM {$this->tableNames['revision']} AS rev_aux WHERE rev_aux.rev_page = rev.rev_page)" + ] + ); + } + $this->revisionAuxWhereAdded = true; + } + break; + case 'pagesel': + $this->addOrderBy('sortkey'); + $this->addSelect( + [ + 'sortkey' => 'CONCAT(pl.pl_namespace, pl.pl_title) ' . $this->getCollateSQL() + ] + ); + break; + case 'pagetouched': + $this->addOrderBy('page_touched'); + $this->addSelect( + [ + "page_touched" => "{$this->tableNames['page']}.page_touched" + ] + ); + break; + case 'size': + $this->addOrderBy('page_len'); + break; + case 'sortkey': + $this->addOrderBy('sortkey'); + // If cl_sortkey is null (uncategorized page), generate a sortkey in the usual way (full page name, underscores replaced with spaces). + // UTF-8 created problems with non-utf-8 MySQL databases + $replaceConcat = "REPLACE(CONCAT({$_namespaceIdToText}, " . $this->tableNames['page'] . ".page_title), '_', ' ')"; + + $category = (array)$this->parameters->getParameter('category'); + $notCategory = (array)$this->parameters->getParameter('notcategory'); + if (count($category) + count($notCategory) > 0) { + if (in_array('category', $this->parameters->getParameter('ordermethod'))) { + $this->addSelect( + [ + 'sortkey' => "IFNULL(cl_head.cl_sortkey, {$replaceConcat}) " . $this->getCollateSQL() + ] + ); + } else { + //This runs on the assumption that at least one category parameter was used and that numbering starts at 1. + $this->addSelect( + [ + 'sortkey' => "IFNULL(cl1.cl_sortkey, {$replaceConcat}) " . $this->getCollateSQL() + ] + ); + } + } else { + $this->addSelect( + [ + 'sortkey' => $replaceConcat . $this->getCollateSQL() + ] + ); + } + break; + case 'titlewithoutnamespace': + if ($this->parameters->getParameter('openreferences')) { + $this->addOrderBy("pl_title"); + } else { + $this->addOrderBy("page_title"); + } + $this->addSelect( + [ + 'sortkey' => "{$this->tableNames['page']}.page_title " . $this->getCollateSQL() + ] + ); + break; + case 'title': + $this->addOrderBy('sortkey'); + if ($this->parameters->getParameter('openreferences')) { + $this->addSelect( + [ + 'sortkey' => "REPLACE(CONCAT(IF(pl_namespace =0, '', CONCAT(" . $_namespaceIdToText . ", ':')), pl_title), '_', ' ') " . $this->getCollateSQL() + ] + ); + } else { + //Generate sortkey like for category links. UTF-8 created problems with non-utf-8 MySQL databases. + $this->addSelect( + [ + 'sortkey' => "REPLACE(CONCAT(IF(" . $this->tableNames['page'] . ".page_namespace = 0, '', CONCAT(" . $_namespaceIdToText . ", ':')), " . $this->tableNames['page'] . ".page_title), '_', ' ') " . $this->getCollateSQL() + ] + ); + } + break; + case 'user': + $this->addOrderBy('rev.rev_actor'); + $this->addTable('revision', 'rev'); + $this->_adduser(null, 'rev'); + break; + case 'none': + break; + } + } + } + + /** + * Set SQL for 'redirects' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _redirects($option) { + if (!$this->parameters->getParameter('openreferences')) { + switch ($option) { + case 'only': + $this->addWhere( + [ + $this->tableNames['page'] . ".page_is_redirect" => 1 + ] + ); + break; + case 'exclude': + $this->addWhere( + [ + $this->tableNames['page'] . ".page_is_redirect" => 0 + ] + ); + break; + } + } + } + + /** + * Set SQL for 'stablepages' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _stablepages($option) { + if (function_exists('efLoadFlaggedRevs')) { + //Do not add this again if 'qualitypages' has already added it. + if (!$this->parametersProcessed['qualitypages']) { + $this->addJoin( + 'flaggedpages', + [ + "LEFT JOIN", + "page_id = fp_page_id" + ] + ); + } + switch ($option) { + case 'only': + $this->addWhere( + [ + 'fp_stable IS NOT NULL' + ] + ); + break; + case 'exclude': + $this->addWhere( + [ + 'fp_stable' => null + ] + ); + break; + } + } + } + + /** + * Set SQL for 'qualitypages' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _qualitypages($option) { + if (function_exists('efLoadFlaggedRevs')) { + //Do not add this again if 'stablepages' has already added it. + if (!$this->parametersProcessed['stablepages']) { + $this->addJoin( + 'flaggedpages', + [ + "LEFT JOIN", + "page_id = fp_page_id" + ] + ); + } + switch ($option) { + case 'only': + $this->addWhere('fp_quality >= 1'); + break; + case 'exclude': + $this->addWhere('fp_quality = 0'); + break; + } + } + } + + /** + * Set SQL for 'title' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _title($option) { + $ors = []; + foreach ($option as $comparisonType => $titles) { + foreach ($titles as $title) { + if ($this->parameters->getParameter('openreferences')) { + if ($this->parameters->getParameter('ignorecase')) { + $_or = "LOWER(CAST(pl_title AS char)) {$comparisonType}" . strtolower($this->DB->addQuotes($title)); + } else { + $_or = "pl_title {$comparisonType} " . $this->DB->addQuotes($title); + } + } else { + if ($this->parameters->getParameter('ignorecase')) { + $_or = "LOWER(CAST({$this->tableNames['page']}.page_title AS char)) {$comparisonType}" . strtolower($this->DB->addQuotes($title)); + } else { + $_or = "{$this->tableNames['page']}.page_title {$comparisonType}" . $this->DB->addQuotes($title); + } + } + $ors[] = $_or; + } + } + $where = '(' . implode(' OR ', $ors) . ')'; + $this->addWhere($where); + } + + /** + * Set SQL for 'nottitle' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _nottitle($option) { + $ors = []; + foreach ($option as $comparisonType => $titles) { + foreach ($titles as $title) { + if ($this->parameters->getParameter('openreferences')) { + if ($this->parameters->getParameter('ignorecase')) { + $_or = "LOWER(CAST(pl_title AS char)) {$comparisonType}" . strtolower($this->DB->addQuotes($title)); + } else { + $_or = "pl_title {$comparisonType} " . $this->DB->addQuotes($title); + } + } else { + if ($this->parameters->getParameter('ignorecase')) { + $_or = "LOWER(CAST({$this->tableNames['page']}.page_title AS char)) {$comparisonType}" . strtolower($this->DB->addQuotes($title)); + } else { + $_or = "{$this->tableNames['page']}.page_title {$comparisonType}" . $this->DB->addQuotes($title); + } + } + $ors[] = $_or; + } + } + $where = 'NOT (' . implode(' OR ', $ors) . ')'; + $this->addWhere($where); + } + + /** + * Set SQL for 'titlegt' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _titlegt($option) { + $where = '('; + if (substr($option, 0, 2) == '=_') { + if ($this->parameters->getParameter('openreferences')) { + $where .= 'pl_title >= ' . $this->DB->addQuotes(substr($sTitleGE, 2)); + } else { + $where .= $this->tableNames['page'] . '.page_title >= ' . $this->DB->addQuotes(substr($option, 2)); + } + } else { + if ($this->parameters->getParameter('openreferences')) { + $where .= 'pl_title > ' . $this->DB->addQuotes($option); + } else { + $where .= $this->tableNames['page'] . '.page_title > ' . $this->DB->addQuotes($option); + } + } + $where .= ')'; + $this->addWhere($where); + } + + /** + * Set SQL for 'titlelt' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _titlelt($option) { + $where = '('; + if (substr($option, 0, 2) == '=_') { + if ($this->parameters->getParameter('openreferences')) { + $where .= 'pl_title <= ' . $this->DB->addQuotes(substr($option, 2)); + } else { + $where .= $this->tableNames['page'] . '.page_title <= ' . $this->DB->addQuotes(substr($option, 2)); + } + } else { + if ($this->parameters->getParameter('openreferences')) { + $where .= 'pl_title < ' . $this->DB->addQuotes($option); + } else { + $where .= $this->tableNames['page'] . '.page_title < ' . $this->DB->addQuotes($option); + } + } + $where .= ')'; + $this->addWhere($where); + } + + /** + * Set SQL for 'usedby' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _usedby($option) { + if ($this->parameters->getParameter('openreferences')) { + $ors = []; + foreach ($option as $linkGroup) { + foreach ($linkGroup as $link) { + $ors[] = 'tpl_from = ' . intval($link->getArticleID()); + } + } + $where = '(' . implode(' OR ', $ors) . ')'; + } else { + $this->addTable('templatelinks', 'tpl'); + $this->addTable('page', 'tplsrc'); + $this->addSelect(['tpl_sel_title' => 'tplsrc.page_title', 'tpl_sel_ns' => 'tplsrc.page_namespace']); + $where = $this->tableNames['page'] . '.page_namespace = tpl.tl_namespace AND ' . + $this->tableNames['page'] . '.page_title = tpl.tl_title AND tplsrc.page_id = tpl.tl_from AND '; + $ors = []; + foreach ($option as $linkGroup) { + foreach ($linkGroup as $link) { + $ors[] = 'tpl.tl_from = ' . intval($link->getArticleID()); + } + } + $where .= '(' . implode(' OR ', $ors) . ')'; + } + $this->addWhere($where); + } + + /** + * Set SQL for 'uses' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _uses($option) { + $this->addTable('templatelinks', 'tl'); + $where = $this->tableNames['page'] . '.page_id=tl.tl_from AND ('; + $ors = []; + foreach ($option as $linkGroup) { + foreach ($linkGroup as $link) { + $_or = '(tl.tl_namespace=' . intval($link->getNamespace()); + if ($this->parameters->getParameter('ignorecase')) { + $_or .= " AND LOWER(CAST(tl.tl_title AS char))=LOWER(" . $this->DB->addQuotes($link->getDbKey()) . '))'; + } else { + $_or .= " AND tl.tl_title=" . $this->DB->addQuotes($link->getDbKey()) . ')'; + } + $ors[] = $_or; + } + } + $where .= implode(' OR ', $ors) . ')'; + $this->addWhere($where); + } + + /** + * Set SQL for 'notuses' parameter. + * + * @access private + * @param mixed Parameter Option + * @return void + */ + private function _notuses($option) { + if (count($option) > 0) { + $where = $this->tableNames['page'] . '.page_id NOT IN (SELECT ' . $this->tableNames['templatelinks'] . '.tl_from FROM ' . $this->tableNames['templatelinks'] . ' WHERE ('; + $ors = []; + foreach ($option as $linkGroup) { + foreach ($linkGroup as $link) { + $_or = '(' . $this->tableNames['templatelinks'] . '.tl_namespace=' . intval($link->getNamespace()); + if ($this->parameters->getParameter('ignorecase')) { + $_or .= ' AND LOWER(CAST(' . $this->tableNames['templatelinks'] . '.tl_title AS char))=LOWER(' . $this->DB->addQuotes($link->getDbKey()) . '))'; + } else { + $_or .= ' AND ' . $this->tableNames['templatelinks'] . '.tl_title=' . $this->DB->addQuotes($link->getDbKey()) . ')'; + } + $ors[] = $_or; + } + } + $where .= implode(' OR ', $ors) . '))'; + } + $this->addWhere($where); + } +} diff --git a/classes/UpdateArticle.php b/classes/UpdateArticle.php new file mode 100644 index 0000000..d4f1607 --- /dev/null +++ b/classes/UpdateArticle.php @@ -0,0 +1,623 @@ +'; + $nr = -1; + foreach ($rules as $rule) { + if (preg_match('/^\s*#/', $rule) > 0) { + continue; // # is comment symbol + } + + $rule = preg_replace('/^[\s]*/', '', $rule); // strip leading white space + $cmd = preg_split("/ +/", $rule, 2); + if (count($cmd) > 1) { + $arg = $cmd[1]; + } else { + $arg = ''; + } + $cmd[0] = trim($cmd[0]); + + // after ... insert ... , before ... insert ... + if ($cmd[0] == 'before') { + $before = $arg; + $lastCmd = 'B'; + } + if ($cmd[0] == 'after') { + $after = $arg; + $lastCmd = 'A'; + } + if ($cmd[0] == 'insert' && $lastCmd != '') { + if ($lastCmd == 'A') { + $insertionAfter = $arg; + } + if ($lastCmd == 'B') { + $insertionBefore = $arg; + } + } + if ($cmd[0] == 'template') { + $template = $arg; + } + + if ($cmd[0] == 'parameter') { + $nr++; + $parameter[$nr] = $arg; + if ($nr > 0) { + $afterparm[$nr] = [ + $parameter[$nr - 1] + ]; + $n = $nr - 1; + while ($n > 0 && array_key_exists($n, $optional)) { + $n--; + $afterparm[$nr][] = $parameter[$n]; + } + } + } + if ($cmd[0] == 'value') { + $value[$nr] = $arg; + } + if ($cmd[0] == 'format') { + $format[$nr] = $arg; + } + if ($cmd[0] == 'tooltip') { + $tooltip[$nr] = $arg; + } + if ($cmd[0] == 'optional') { + $optional[$nr] = true; + } + if ($cmd[0] == 'afterparm') { + $afterparm[$nr] = [ + $arg + ]; + } + if ($cmd[0] == 'legend') { + $legendPage = $arg; + } + if ($cmd[0] == 'instruction') { + $instructionPage = $arg; + } + if ($cmd[0] == 'table') { + $table = $arg; + } + if ($cmd[0] == 'field') { + $fieldFormat = $arg; + } + + if ($cmd[0] == 'replace') { + $replaceThis = $arg; + } + if ($cmd[0] == 'by') { + $replacement = $arg; + } + + if ($cmd[0] == 'editform') { + $editForm = $arg; + } + if ($cmd[0] == 'action') { + $action = $arg; + } + if ($cmd[0] == 'hidden') { + $hidden[] = $arg; + } + if ($cmd[0] == 'preview') { + $preview[] = $arg; + } + if ($cmd[0] == 'save') { + $save[] = $arg; + } + + if ($cmd[0] == 'summary') { + $summary = $arg; + } + if ($cmd[0] == 'exec') { + $exec = $arg; // desired action (set or edit or preview) + } + } + + if ($summary == '') { + $summary .= "\nbulk update:"; + if ($replaceThis != '') { + $summary .= "\n replace $replaceThis\n by $replacement"; + } + if ($before != '') { + $summary .= "\n before $before\n insertionBefore"; + } + if ($after != '') { + $summary .= "\n after $after\n insertionAfter"; + } + } + + // $message.= ''; + + // perform changes to the wiki source text ======================================= + + if ($replaceThis != '') { + $text = preg_replace("$replaceThis", $replacement, $text); + } + + if ($insertionBefore != '' && $before != '') { + $text = preg_replace("/($before)/", $insertionBefore . '\1', $text); + } + + if ($insertionAfter != '' && $after != '') { + $text = preg_replace("/($after)/", '\1' . $insertionAfter, $text); + } + + // deal with template parameters ================================================= + + global $wgRequest, $wgUser; + + if ($template != '') { + + if ($exec == 'edit') { + $tpv = self::getTemplateParmValues($text, $template); + $legendText = ''; + if ($legendPage != '') { + $legendTitle = ''; + global $wgParser, $wgUser; + $parser = clone $wgParser; + LST::text($parser, $legendPage, $legendTitle, $legendText); + $legendText = preg_replace('/^.*?\/s', '', $legendText); + $legendText = preg_replace('/\.*/s', '', $legendText); + } + $instructionText = ''; + $instructions = []; + if ($instructionPage != '') { + $instructionTitle = ''; + global $wgParser, $wgUser; + $parser = clone $wgParser; + LST::text($parser, $instructionPage, $instructionTitle, $instructionText); + $instructions = self::getTemplateParmValues($instructionText, 'Template field'); + } + // construct an edit form containing all template invocations + $form = "
\n"; + foreach ($tpv as $call => $tplValues) { + $form .= "\n"; + foreach ($parameter as $nr => $parm) { + // try to extract legend from the docs of the template + $myToolTip = ''; + if (array_key_exists($nr, $tooltip)) { + $myToolTip = $tooltip[$nr]; + } + $myInstruction = ''; + $myType = ''; + foreach ($instructions as $instruct) { + if (array_key_exists('field', $instruct) && $instruct['field'] == $parm) { + if (array_key_exists('doc', $instruct)) { + $myInstruction = $instruct['doc']; + } + if (array_key_exists('type', $instruct)) { + $myType = $instruct['type']; + } + break; + } + } + $myFormat = ''; + if (array_key_exists($nr, $format)) { + $myFormat = $format[$nr]; + } + $myOptional = array_key_exists($nr, $optional); + if ($legendText != '' && $myToolTip == '') { + $myToolTip = preg_replace('/^.*\/s', '', $legendText); + if (strlen($myToolTip) == strlen($legendText)) { + $myToolTip = ''; + } else { + $myToolTip = preg_replace('/\.*/s', '', $myToolTip); + } + } + $myValue = ''; + if (array_key_exists($parm, $tpv[$call])) { + $myValue = $tpv[$call][$parm]; + } + $form .= self::editTemplateCall($text, $template, $call, $parm, $myType, $myValue, $myFormat, $myToolTip, $myInstruction, $myOptional, $fieldFormat); + } + $form .= "
\n

"; + } + foreach ($hidden as $hide) { + $form .= ""; + } + $form .= ""; + foreach ($preview as $prev) { + $form .= " "; + } + $form .= "
\n"; + return $form; + } elseif ($exec == 'set' || $exec == 'preview') { + // loop over all invocations and parameters, this could be improved to enhance performance + $matchCount = 10; + for ($call = 0; $call < 10; $call++) { + foreach ($parameter as $nr => $parm) { + // set parameters to values specified in the dpl source or get them from the http request + if ($exec == 'set') { + $myvalue = $value[$nr]; + } else { + if ($call >= $matchCount) { + break; + } + $myValue = $wgRequest->getVal(urlencode($call . '_' . $parm), ''); + } + $myOptional = array_key_exists($nr, $optional); + $myAfterParm = []; + if (array_key_exists($nr, $afterparm)) { + $myAfterParm = $afterparm[$nr]; + } + $text = self::updateTemplateCall($matchCount, $text, $template, $call, $parm, $myValue, $myAfterParm, $myOptional); + } + if ($exec == 'set') { + break; // values taken from dpl text only populate the first invocation + } + } + } + } + + if ($exec == 'set') { + return self::updateArticle($title, $text, $summary); + } elseif ($exec == 'preview') { + global $wgScriptPath, $wgRequest; + $titleX = \Title::newFromText($title); + $articleX = new \Article($titleX); + $form = ' +
+ + + + + + + + + +
+'; + return $form; + } + return "exec must be one of the following: edit, preview, set"; + } + + private static function updateArticle($title, $text, $summary) { + global $wgUser, $wgRequest, $wgOut; + + if (!$wgUser->matchEditToken($wgRequest->getVal('wpEditToken'))) { + $wgOut->addWikiMsg('sessionfailure'); + return 'session failure'; + } + + $titleX = \Title::newFromText($title); + $permission_errors = $titleX->getUserPermissionsErrors('edit', $wgUser); + if (count($permission_errors) == 0) { + $articleX = \WikiPage::factory($titleX); + $articleXContent = \ContentHandler::makeContent($text, $titleX); + $articleX->doEditContent($articleXContent, $summary, EDIT_UPDATE | EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY); + $wgOut->redirect($titleX->getFullUrl($articleX->isRedirect() ? 'redirect=no' : '')); + return ''; + } else { + $wgOut->showPermissionsErrorPage($permission_errors); + return 'permission error'; + } + } + + private static function editTemplateCall($text, $template, $call, $parameter, $type, $value, $format, $legend, $instruction, $optional, $fieldFormat) { + $matches = []; + $nlCount = preg_match_all('/\n/', $value, $matches); + if ($nlCount > 0) { + $rows = $nlCount + 1; + } else { + $rows = floor(strlen($value) / 50) + 1; + } + if (preg_match('/rows\s*=/', $format) <= 0) { + $format .= " rows=$rows"; + } + $cols = 50; + if (preg_match('/cols\s*=/', $format) <= 0) { + $format .= " cols=$cols"; + } + $textArea = ""; + return str_replace('%NAME%', htmlspecialchars(str_replace('_', ' ', $parameter)), str_replace('%TYPE%', $type, str_replace('%INPUT%', $textArea, str_replace('%LEGEND%', "" . htmlspecialchars($legend) . "", str_replace('%INSTRUCTION%', "" . htmlspecialchars($instruction) . "", $fieldFormat))))); + } + + /** + * return an array of template invocations; each element is an associative array of parameter and value + */ + private static function getTemplateParmValues($text, $template) { + $matches = []; + $noMatches = preg_match_all('/\{\{\s*' . preg_quote($template, '/') . '\s*[|}]/i', $text, $matches, PREG_OFFSET_CAPTURE); + if ($noMatches <= 0) { + return ''; + } + $textLen = strlen($text); + $tval = []; // the result array of template values + $call = -1; // index for tval + + foreach ($matches as $matchA) { + foreach ($matchA as $matchB) { + $match = $matchB[0]; + $start = $matchB[1]; + $tval[++$call] = []; + $nr = 0; // number of parameter if no name given + $parmValue = ''; + $parmName = ''; + $parm = ''; + + if ($match[strlen($match) - 1] == '}') { + break; // template was called without parameters, continue with next invocation + } + + // search to the end of the template call + $cbrackets = 2; + for ($i = $start + strlen($match); $i < $textLen; $i++) { + $c = $text[$i]; + if ($c == '{' || $c == '[') { + $cbrackets++; // we count both types of brackets + } + if ($c == '}' || $c == ']') { + $cbrackets--; + } + if (($cbrackets == 2 && $c == '|') || ($cbrackets == 1 && $c == '}')) { + // parameter (name or value) found + if ($parmName == '') { + $tval[$call][++$nr] = trim($parm); + } else { + $tval[$call][$parmName] = trim($parmValue); + } + $parmName = ''; + $parmValue = ''; + $parm = ''; + continue; + } else { + if ($parmName == '') { + if ($c == '=') { + $parmName = trim($parm); + } + } else { + $parmValue .= $c; + } + } + $parm .= $c; + if ($cbrackets == 0) { + break; // end of parameter list + } + } + } + } + return $tval; + } + + /* + * Changes a single parameter value within a certain call of a template + */ + private static function updateTemplateCall(&$matchCount, $text, $template, $call, $parameter, $value, $afterParm, $optional) { + // if parameter is optional and value is empty we leave everything as it is (i.e. we do not remove the parm) + if ($optional && $value == '') { + return $text; + } + + $matches = []; + $noMatches = preg_match_all('/\{\{\s*' . preg_quote($template, '/') . '\s*[|}]/i', $text, $matches, PREG_OFFSET_CAPTURE); + if ($noMatches <= 0) { + return $text; + } + $beginSubst = -1; + $endSubst = -1; + $posInsertAt = 0; + $apNrLast = 1000; // last (optional) predecessor + + foreach ($matches as $matchA) { + $matchCount = count($matchA); + foreach ($matchA as $occurence => $matchB) { + if ($occurence < $call) { + continue; + } + $match = $matchB[0]; + $start = $matchB[1]; + + if ($match[strlen($match) - 1] == '}') { + // template was called without parameters, add new parameter and value + // append parameter and value + $beginSubst = $i; + $endSubst = $i; + $substitution = "|$parameter = $value"; + break; + } else { + // there is already a list of parameters; we search to the end of the template call + $cbrackets = 2; + $parm = ''; + $pos = $start + strlen($match) - 1; + $textLen = strlen($text); + for ($i = $pos + 1; $i < $textLen; $i++) { + $c = $text[$i]; + if ($c == '{' || $c == '[') { + $cbrackets++; // we count both types of brackets + } + if ($c == '}' || $c == ']') { + $cbrackets--; + } + if (($cbrackets == 2 && $c == '|') || ($cbrackets == 1 && $c == '}')) { + // parameter (name / value) found + + $token = explode('=', $parm, 2); + if (count($token) == 2) { + // we need a pair of name / value + $parmName = trim($token[0]); + if ($parmName == $parameter) { + // we found the parameter, now replace the current value + $parmValue = trim($token[1]); + if ($parmValue == $value) { + break; // no need to change when values are identical + } + // keep spaces; + if ($parmValue == '') { + if (strlen($token[1]) > 0 && $token[1][strlen($token[1]) - 1] == "\n") { + $substitution = str_replace("\n", $value . "\n", $token[1]); + } else { + $substitution = $value . $token[1]; + } + } else { + $substitution = str_replace($parmValue, $value, $token[1]); + } + $beginSubst = $pos + strlen($token[0]) + 2; + $endSubst = $i; + break; + } else { + foreach ($afterParm as $apNr => $ap) { + // store position for insertion + if ($parmName == $ap && $apNr < $apNrLast) { + $posInsertAt = $i; + $apNrLast = $apNr; + break; + } + } + } + } + + if ($c == '}') { + // end of template call reached, insert at stored position or here + if ($posInsertAt != 0) { + $beginSubst = $posInsertAt; + } else { + $beginSubst = $i; + } + $substitution = "|$parameter = $value"; + if ($text[$beginSubst - 1] == "\n") { + --$beginSubst; + $substitution = "\n" . $substitution; + } + $endSubst = $beginSubst; + break; + } + + $pos = $i; + $parm = ''; + } else { + $parm .= $c; + } + if ($cbrackets == 0) { + break; + } + } + } + break; + } + break; + } + + if ($beginSubst < 0) { + return $text; + } + + return substr($text, 0, $beginSubst) . $substitution . substr($text, $endSubst); + } + + public function deleteArticleByRule($title, $text, $rulesText) { + global $wgUser, $wgOut; + + // return "deletion of articles by DPL is disabled."; + + // we use ; as command delimiter; \; stands for a semicolon + // \n is translated to a real linefeed + $rulesText = str_replace(";", '°', $rulesText); + $rulesText = str_replace('\°', ';', $rulesText); + $rulesText = str_replace("\\n", "\n", $rulesText); + $rules = explode('°', $rulesText); + $exec = false; + $message = ''; + $reason = ''; + + foreach ($rules as $rule) { + if (preg_match('/^\s*#/', $rule) > 0) { + continue; // # is comment symbol + } + + $rule = preg_replace('/^[\s]*/', '', $rule); // strip leading white space + $cmd = preg_split("/ +/", $rule, 2); + if (count($cmd) > 1) { + $arg = $cmd[1]; + } else { + $arg = ''; + } + $cmd[0] = trim($cmd[0]); + + if ($cmd[0] == 'reason') { + $reason = $arg; + } + + // we execute only if "exec" is given, otherwise we merely show what would be done + if ($cmd[0] == 'exec') { + $exec = true; + } + } + $reason .= "\nbulk delete by DPL query"; + + $titleX = \Title::newFromText($title); + if ($exec) { + # Check permissions + $permission_errors = $titleX->getUserPermissionsErrors('delete', $wgUser); + if (count($permission_errors) > 0) { + $wgOut->showPermissionsErrorPage($permission_errors); + return 'permission error'; + } elseif (wfReadOnly()) { + $wgOut->readOnlyPage(); + return 'DPL: read only mode'; + } else { + $articleX = new \Article($titleX); + $articleX->doDelete($reason); + } + } else { + $message .= "set 'exec yes' to delete     '''$title'''\n"; + } + $message .= "
\n{$text}
"; //
\n"; // .$text."\n
\n"; + return $message; + } +} diff --git a/classes/Variables.php b/classes/Variables.php new file mode 100644 index 0000000..7fa9849 --- /dev/null +++ b/classes/Variables.php @@ -0,0 +1,136 @@ += 3 && $arg[2] == '') { + $start = 3; + } else { + $start = 2; + } + for ($i = $start; $i < $numargs; $i++) { + $var = $arg[$i]; + if (++$i <= $numargs - 1) { + self::$memoryVar[$var] = $arg[$i]; + } else { + self::$memoryVar[$var] = ''; + } + } + return ''; + } + + public static function setVarDefault($arg) { + $numargs = count($arg); + if ($numargs > 3) { + $value = $arg[3]; + } else { + return ''; + } + $var = $arg[2]; + if (!array_key_exists($var, self::$memoryVar) || self::$memoryVar[$var] == '') { + self::$memoryVar[$var] = $value; + } + return ''; + } + + public static function getVar($var) { + if (array_key_exists($var, self::$memoryVar)) { + return self::$memoryVar[$var]; + } + return ''; + } + + public static function setArray($arg) { + $numargs = count($arg); + if ($numargs < 5) { + return ''; + } + $var = trim($arg[2]); + $value = $arg[3]; + $delimiter = $arg[4]; + if ($var == '') { + return ''; + } + if ($value == '') { + self::$memoryArray[$var] = []; + return; + } + if ($delimiter == '') { + self::$memoryArray[$var] = [ + $value + ]; + return; + } + if (0 !== strpos($delimiter, '/') || (strlen($delimiter) - 1) !== strrpos($delimiter, '/')) { + $delimiter = '/\s*' . $delimiter . '\s*/'; + } + self::$memoryArray[$var] = preg_split($delimiter, $value); + return "value={$value}, delimiter={$delimiter}," . count(self::$memoryArray[$var]); + } + + public static function dumpArray($arg) { + $numargs = count($arg); + if ($numargs < 3) { + return ''; + } + $var = trim($arg[2]); + $text = " array {$var} = {"; + $n = 0; + if (array_key_exists($var, self::$memoryArray)) { + foreach (self::$memoryArray[$var] as $value) { + if ($n++ > 0) { + $text .= ', '; + } + $text .= "{$value}"; + } + } + return $text . "}\n"; + } + + public static function printArray($var, $delimiter, $search, $subject) { + $var = trim($var); + if ($var == '') { + return ''; + } + if (!array_key_exists($var, self::$memoryArray)) { + return ''; + } + $values = self::$memoryArray[$var]; + $rendered_values = []; + foreach ($values as $v) { + $temp_result_value = str_replace($search, $v, $subject); + $rendered_values[] = $temp_result_value; + } + return [ + implode($delimiter, $rendered_values), + 'noparse' => false, + 'isHTML' => false + ]; + } +}