DynamicPageList3/includes/UpdateArticle.php
CosmicAlpha 8dd8d03d61
Support MediaWiki 1.39 for actor and templatelinks migration (#188)
This adds support for 1.39, and drops support for older versions, since 1.39 is the current LTS release.
2022-12-06 17:10:46 -07:00

782 lines
20 KiB
PHP

<?php
namespace MediaWiki\Extension\DynamicPageList3;
use Article;
use CommentStoreComment;
use MediaWiki\MediaWikiServices;
use MediaWiki\Revision\SlotRecord;
use ReadOnlyError;
use RequestContext;
use Title;
class UpdateArticle {
/**
* This fucntion hast three tasks (depending on $exec):
* (1) show an edit dialogue for template fields (exec = edit)
* (2) set template parameters to values specified in the query (exec=set)v
* (2) preview the source code including any changes of these parameters made in the edit form or with other changes (exec=preview)
* (3) save the article with the changed value set or with other changes (exec=save)
* "other changes" means that a regexp can be applied to the source text or arbitrary text can be
* inserted before or after a pattern occuring in the text
*
* @param string $title
* @param string $text
* @param string $rulesText
* @return string
*/
public static function updateArticleByRule( $title, $text, $rulesText ) {
// 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 = 'edit';
$replaceThis = '';
$replacement = '';
$after = '';
$insertionAfter = '';
$before = '';
$insertionBefore = '';
$template = '';
$parameter = [];
$value = [];
$afterparm = [];
$format = [];
$preview = [];
$save = [];
$tooltip = [];
$optional = [];
$lastCmd = '';
$summary = '';
$editForm = false;
$action = '';
$hidden = [];
$legendPage = '';
$instructionPage = '';
$table = '';
$fieldFormat = '';
$nr = -1;
foreach ( $rules as $rule ) {
if ( preg_match( '/^\s*#/', $rule ) > 0 ) {
continue;
}
$rule = preg_replace( '/^[\s]*/', '', $rule );
$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' ) {
// desired action (set or edit or preview)
$exec = $arg;
}
}
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";
}
}
// 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 =================================================
$user = RequestContext::getMain()->getUser();
if ( $template != '' ) {
if ( $exec == 'edit' ) {
$tpv = self::getTemplateParmValues( $text, $template );
$legendText = '';
if ( $legendPage != '' ) {
$legendTitle = '';
$parser = clone MediaWikiServices::getInstance()->getParser();
LST::text( $parser, $legendPage, $legendTitle, $legendText );
$legendText = preg_replace( '/^.*?\<section\s+begin\s*=\s*legend\s*\/\>/s', '', $legendText );
$legendText = preg_replace( '/\<section\s+end\s*=\s*legend\s*\/\>.*/s', '', $legendText );
}
$instructionText = '';
$instructions = [];
if ( $instructionPage != '' ) {
$instructionTitle = '';
$parser = clone MediaWikiServices::getInstance()->getParser();
LST::text( $parser, $instructionPage, $instructionTitle, $instructionText );
$instructions = self::getTemplateParmValues( $instructionText, 'Template field' );
}
// construct an edit form containing all template invocations
$form = "<html><form method=post action=\"$action\" $editForm>\n";
foreach ( $tpv as $call => $tplValues ) {
$form .= "<table $table>\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];
}
if ( $legendText != '' && $myToolTip == '' ) {
$myToolTip = preg_replace( '/^.*\<section\s+begin\s*=\s*' . preg_quote( $parm, '/' ) . '\s*\/\>/s', '', $legendText );
if ( strlen( $myToolTip ) == strlen( $legendText ) ) {
$myToolTip = '';
} else {
$myToolTip = preg_replace( '/\<section\s+end\s*=\s*' . preg_quote( $parm, '/' ) . '\s*\/\>.*/s', '', $myToolTip );
}
}
$myValue = '';
if ( array_key_exists( $parm, $tpv[$call] ) ) {
$myValue = $tpv[$call][$parm];
}
$form .= self::editTemplateCall( $call, $parm, $myType, $myValue, $myFormat, $myToolTip, $myInstruction, $fieldFormat );
}
$form .= "</table>\n<br/><br/>";
}
foreach ( $hidden as $hide ) {
$form .= "<input type='hidden' " . $hide . " />";
}
$csrfTokenSet = RequestContext::getMain()->getCsrfTokenSet();
$form .= "<input type='hidden' name='wpEditToken' value='{$csrfTokenSet->getToken()}'/>";
foreach ( $preview as $prev ) {
$form .= "<input type='submit' " . $prev . " /> ";
}
$form .= "</form></html>\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;
}
$request = RequestContext::getMain()->getRequest();
$myValue = $request->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;
}
}
}
}
if ( $exec == 'set' ) {
return self::doUpdateArticle( $title, $text, $summary );
} elseif ( $exec == 'preview' ) {
global $wgScriptPath;
$request = RequestContext::getMain()->getRequest();
$titleX = Title::newFromText( $title );
$articleX = new Article( $titleX );
$userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
$form = '<html>
<form id="editform" name="editform" method="post" action="' . $wgScriptPath . '/index.php?title=' . urlencode( $title ) . '&action=submit" enctype="multipart/form-data">
<input type="hidden" value="" name="wpSection" />
<input type="hidden" value="' . wfTimestampNow() . '" name="wpStarttime" />
<input type="hidden" value="' . $articleX->getPage()->getTimestamp() . '" name="wpEdittime" />
<input type="hidden" value="" name="wpScrolltop" id="wpScrolltop" />
<textarea tabindex="1" accesskey="," name="wpTextbox1" id="wpTextbox1" rows="' . $userOptionsLookup->getIntOption( $user, 'rows' ) . '" cols="' . $userOptionsLookup->getIntOption( $user, 'cols' ) . '" >' . htmlspecialchars( $text ) . '</textarea>
<input type="hidden" name="wpSummary value="' . $summary . '" id="wpSummary" />
<input name="wpAutoSummary" type="hidden" value="" />
<input id="wpSave" name="wpSave" type="submit" value="Save page" accesskey="s" title="Save your changes [s]" />
<input type="hidden" value="' . $request->getVal( 'token' ) . '" name="wpEditToken" />
</form>
</html>';
return $form;
}
return "exec must be one of the following: edit, preview, set";
}
/**
* @param string $title
* @param string $text
* @param string $summary
* @return string
*/
private static function doUpdateArticle( $title, $text, $summary ) {
$context = RequestContext::getMain();
$out = $context->getOutput();
$user = $context->getUser();
if ( !$context->getCsrfTokenSet()->matchTokenField( 'wpEditToken' ) ) {
$out->addWikiMsg( 'sessionfailure' );
return 'session failure';
}
$titleX = Title::newFromText( $title );
$permission_errors = MediaWikiServices::getInstance()->getPermissionManager()->getPermissionErrors( 'edit', $user, $titleX );
if ( count( $permission_errors ) == 0 ) {
$services = MediaWikiServices::getInstance();
$wikiPageFactory = $services->getWikiPageFactory();
$page = $wikiPageFactory->newFromTitle( $titleX );
$updater = $page->newPageUpdater( $user );
$content = $page->getContentHandler()->makeContent( $text, $titleX );
$updater->setContent( SlotRecord::MAIN, $content );
$comment = CommentStoreComment::newUnsavedComment( $summary );
$updater->saveRevision(
$comment,
EDIT_UPDATE | EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY
);
$out->redirect( $titleX->getFullUrl( $page->isRedirect() ? 'redirect=no' : '' ) );
return '';
} else {
$out->showPermissionsErrorPage( $permission_errors );
return 'permission error';
}
}
/**
* @param int $call
* @param string $parameter
* @param string $type
* @param string $value
* @param string $format
* @param string $legend
* @param string $instruction
* @param string $fieldFormat
* @return string
*/
private static function editTemplateCall( $call, $parameter, $type, $value, $format, $legend, $instruction, $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 = "<textarea name=\"" . urlencode( $call . '_' . $parameter ) . "\" $format/>" . htmlspecialchars( $value ) . "</textarea>";
return str_replace( '%NAME%', htmlspecialchars( str_replace( '_', ' ', $parameter ) ), str_replace( '%TYPE%', $type, str_replace( '%INPUT%', $textArea, str_replace( '%LEGEND%', "</html>" . htmlspecialchars( $legend ) . "<html>", str_replace( '%INSTRUCTION%', "</html>" . htmlspecialchars( $instruction ) . "<html>", $fieldFormat ) ) ) ) );
}
/**
* @param string $text
* @param string $template
* @return array|string
*/
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 = [];
$call = -1;
foreach ( $matches as $matchA ) {
foreach ( $matchA as $matchB ) {
$match = $matchB[0];
$start = $matchB[1];
$tval[++$call] = [];
$nr = 0;
$parmValue = '';
$parmName = '';
$parm = '';
if ( $match[strlen( $match ) - 1] == '}' ) {
break;
}
// 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++;
}
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;
}
}
}
}
return $tval;
}
/**
* Changes a single parameter value within a certain call of a template
*
* @param int &$matchCount
* @param string $text
* @param string $template
* @param int $call
* @param string $parameter
* @param string $value
* @param array $afterParm
* @param bool $optional
* @return string
*/
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;
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 = 0;
$endSubst = 0;
$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 == '[' ) {
// we count both types of brackets
$cbrackets++;
}
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 ) {
// no need to change when values are identical
break;
}
// 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 );
}
/**
* @param string $title
* @param string $text
* @param string $rulesText
* @return string
*/
public static function deleteArticleByRule( $title, $text, $rulesText ) {
// 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 ) {
// # is comment symbol
continue;
}
$rule = preg_replace( '/^[\s]*/', '', $rule );
$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 ) {
$context = RequestContext::getMain();
$out = $context->getOutput();
$user = $context->getUser();
# Check permissions
$permission_errors = MediaWikiServices::getInstance()->getPermissionManager()->getPermissionErrors( 'delete', $user, $titleX );
$isReadOnly = MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly();
if ( count( $permission_errors ) > 0 ) {
$out->showPermissionsErrorPage( $permission_errors );
return 'permission error';
} elseif ( $isReadOnly ) {
throw new ReadOnlyError;
} else {
$wikiPageFactory = MediaWikiServices::getInstance()->getWikiPageFactory();
$deletePageFactory = MediaWikiServices::getInstance()->getDeletePageFactory();
$deletePage = $deletePageFactory->newDeletePage(
$wikiPageFactory->newFromTitle( $titleX ),
$user
);
$deletePage->deleteIfAllowed( $reason );
}
} else {
$message .= "set 'exec yes' to delete &#160; &#160; <big>'''$title'''</big>\n";
}
$message .= "<pre><nowiki>\n{$text}</nowiki></pre>";
return $message;
}
}