parseDataFromQueryString( $this->mOptions, $options );
}
/**
* @return array
*/
function getOptions() {
return $this->mOptions;
}
/**
* Returns the action performed by the module.
*
* Return value is either null or one of ACTION_SAVE, ACTION_PREVIEW,
* ACTION_FORMEDIT
*
* @return null|number
*/
function getAction() {
return $this->mAction;
}
/**
* @param array $options
*/
function setOptions( $options ) {
$this->mOptions = $options;
}
/**
* @param string $option
* @param mixed $value
*/
function setOption( $option, $value ) {
$this->mOptions[$option] = $value;
}
/**
* Returns the HTTP status
*
* 200 - ok
* 400 - error
*
* @return number
*/
function getStatus() {
return $this->mStatus;
}
/**
* Evaluates the parameters, performs the requested API query, and sets up
* the result.
*
* The execute() method will be invoked when an API call is processed.
*
* The result data is stored in the ApiResult object available through
* getResult().
*/
function execute() {
$this->prepareAction();
try {
$this->doAction();
} catch ( MWException $e ) {
$this->logMessage( $e->getMessage(), $e->getCode() );
}
$this->finalizeResults();
$this->setHeaders();
}
/**
*
*/
function prepareAction() {
// Get options from the request, but keep the explicitly set options.
$data = $this->getRequest()->getValues();
$this->mOptions = PFUtils::array_merge_recursive_distinct( $data, $this->mOptions );
global $wgParser;
if ( $wgParser === null ) {
$wgParser = new Parser();
}
$wgParser->startExternalParse(
null,
ParserOptions::newFromUser( $this->getUser() ),
Parser::OT_WIKI
);
// MW uses the parameter 'title' instead of 'target' when submitting
// data for formedit action => use that
if ( !array_key_exists( 'target', $this->mOptions ) && array_key_exists( 'title', $this->mOptions ) ) {
$this->mOptions['target'] = $this->mOptions['title'];
unset( $this->mOptions['title'] );
}
// if the 'query' parameter was used, unpack the param string
if ( array_key_exists( 'query', $this->mOptions ) ) {
$this->addOptionsFromString( $this->mOptions['query'] );
unset( $this->mOptions['query'] );
}
// if an action is explicitly set in the form data, use that
if ( array_key_exists( 'wpSave', $this->mOptions ) ) {
// set action to 'save' if requested
$this->mAction = self::ACTION_SAVE;
unset( $this->mOptions['wpSave'] );
} elseif ( array_key_exists( 'wpPreview', $this->mOptions ) ) {
// set action to 'preview' if requested
$this->mAction = self::ACTION_PREVIEW;
unset( $this->mOptions['wpPreview'] );
} elseif ( array_key_exists( 'wpDiff', $this->mOptions ) ) {
// set action to 'preview' if requested
$this->mAction = self::ACTION_DIFF;
unset( $this->mOptions['wpDiff'] );
} elseif ( array_key_exists( 'action', $this->mOptions ) ) {
switch ( $this->mOptions['action'] ) {
case 'pfautoedit' :
$this->mIsAutoEdit = true;
$this->mAction = self::ACTION_SAVE;
break;
case 'preview' :
$this->mAction = self::ACTION_PREVIEW;
break;
default :
$this->mAction = self::ACTION_FORMEDIT;
}
} else {
// set default action
$this->mAction = self::ACTION_FORMEDIT;
}
$hookQuery = null;
// ensure 'form' key exists
if ( array_key_exists( 'form', $this->mOptions ) ) {
$hookQuery = $this->mOptions['form'];
} else {
$this->mOptions['form'] = '';
}
// ensure 'target' key exists
if ( array_key_exists( 'target', $this->mOptions ) ) {
if ( $hookQuery !== null ) {
$hookQuery .= '/' . $this->mOptions['target'];
}
} else {
$this->mOptions['target'] = '';
}
// Normalize form and target names
$form = Title::newFromText( $this->mOptions['form'] );
if ( $form !== null ) {
$this->mOptions['form'] = $form->getPrefixedText();
}
$target = Title::newFromText( $this->mOptions['target'] );
if ( $target !== null ) {
$this->mOptions['target'] = $target->getPrefixedText();
}
Hooks::run( 'PageForms::SetTargetName', array( &$this->mOptions['target'], $hookQuery ) );
// set html return status. If all goes well, this will not be changed
$this->mStatus = 200;
}
/**
* Get the Title object of a form suitable for editing the target page.
*
* @return Title
* @throws MWException
*/
protected function getFormTitle() {
// if no form was explicitly specified, try for explicitly set alternate forms
if ( $this->mOptions['form'] === '' ) {
$this->logMessage( 'No form specified. Will try to find the default form for the target page.', self::DEBUG );
$formNames = array();
// try explicitly set alternative forms
if ( array_key_exists( 'alt_form', $this->mOptions ) ) {
$formNames = (array)$this->mOptions['alt_form']; // cast to array to make sure we get an array, even if only a string was sent
}
// if no alternate forms were explicitly set, try finding a default form for the target page
if ( count( $formNames ) === 0 ) {
// if no form and and no alt forms and no target page was specified, give up
if ( $this->mOptions['target'] === '' ) {
throw new MWException( wfMessage( 'pf_autoedit_notargetspecified' )->parse() );
}
$targetTitle = Title::newFromText( $this->mOptions['target'] );
// if the specified target title is invalid, give up
if ( !$targetTitle instanceof Title ) {
throw new MWException( wfMessage( 'pf_autoedit_invalidtargetspecified', $this->mOptions['target'] )->parse() );
}
$formNames = PFFormLinker::getDefaultFormsForPage( $targetTitle );
if ( count( $formNames ) === 0 ) {
throw new MWException( wfMessage( 'pf_autoedit_noformfound' )->parse() );
}
}
// if more than one form was found, issue a notice and give up
// this happens if no default form but several alternate forms are defined
if ( count( $formNames ) > 1 ) {
throw new MWException( wfMessage( 'pf_autoedit_toomanyformsfound' )->parse(), self::DEBUG );
}
$this->mOptions['form'] = $formNames[0];
$this->logMessage( 'Using ' . $this->mOptions['form'] . ' as default form.', self::DEBUG );
}
$formTitle = Title::makeTitleSafe( PF_NS_FORM, $this->mOptions['form'] );
// If the given form is not a valid title, give up.
if ( !( $formTitle instanceof Title ) ) {
throw new MWException( wfMessage( 'pf_autoedit_invalidform', $this->mOptions['form'] )->parse() );
}
// If the form page is a redirect, follow the redirect.
if ( $formTitle->isRedirect() ) {
$this->logMessage( 'Form ' . $this->mOptions['form'] . ' is a redirect. Finding target.', self::DEBUG );
$formWikiPage = WikiPage::factory( $formTitle );
$formTitle = $formWikiPage->getContent( Revision::RAW )->getUltimateRedirectTarget();
// if we exeeded $wgMaxRedirects or encountered an invalid redirect target, give up
if ( $formTitle->isRedirect() ) {
$newTitle = WikiPage::factory( $formTitle )->getRedirectTarget();
if ( $newTitle instanceof Title && $newTitle->isValidRedirectTarget() ) {
throw new MWException( wfMessage( 'pf_autoedit_redirectlimitexeeded', $this->mOptions['form'] )->parse() );
} else {
throw new MWException( wfMessage( 'pf_autoedit_invalidredirecttarget', $newTitle->getFullText(), $this->mOptions['form'] )->parse() );
}
}
}
// if specified or found form does not exist (e.g. is a red link), give up
// FIXME: Throw specialized error message, so a list of alternative forms can be shown
if ( !$formTitle->exists() ) {
throw new MWException( wfMessage( 'pf_autoedit_invalidform', $this->mOptions['form'] )->parse() );
}
return $formTitle;
}
protected function setupEditPage( $targetContent ) {
// Find existing target article if it exists, or create a new one.
$targetTitle = Title::newFromText( $this->mOptions['target'] );
// If the specified target title is invalid, give up.
if ( !$targetTitle instanceof Title ) {
throw new MWException( wfMessage( 'pf_autoedit_invalidtargetspecified', $this->mOptions['target'] )->parse() );
}
$article = new Article( $targetTitle );
// set up a normal edit page
// we'll feed it our data to simulate a normal edit
$editor = new EditPage( $article );
// set up form data:
// merge data coming from the web request on top of some defaults
$data = array_merge(
array(
'wpTextbox1' => $targetContent,
'wpUnicodeCheck' => 'β³π²β₯πππΎπΈβ΄πΉβ―',
'wpSummary' => '',
'wpStarttime' => wfTimestampNow(),
'wpEdittime' => '',
'wpEditToken' => isset( $this->mOptions[ 'token' ] ) ? $this->mOptions[ 'token' ] : $this->getUser()->getEditToken(),
'action' => 'submit',
),
$this->mOptions
);
if ( array_key_exists( 'format', $data ) ) {
unset( $data['format'] );
}
// set up a faux request with the simulated data
$request = new FauxRequest( $data, true );
// and import it into the edit page
$editor->importFormData( $request );
$editor->pfFauxRequest = $request;
return $editor;
}
/**
* Sets the output HTML of wgOut as the module's result
*/
protected function setResultFromOutput() {
// turn on output buffering
ob_start();
// generate preview document and write it to output buffer
$this->getOutput()->output();
// retrieve the preview document from output buffer
$targetHtml = ob_get_contents();
// clean output buffer, so MW can use it again
ob_clean();
// store the document as result
$this->getResult()->addValue( null, 'result', $targetHtml );
}
protected function doPreview( $editor ) {
global $wgOut;
$previewOutput = $editor->getPreviewText();
Hooks::run( 'EditPage::showEditForm:initial', array( &$editor, &$wgOut ) );
$this->getOutput()->setRobotPolicy( 'noindex,nofollow' );
// This hook seems slightly odd here, but makes things more
// consistent for extensions.
Hooks::run( 'OutputPageBeforeHTML', array( &$wgOut, &$previewOutput ) );
$this->getOutput()->addHTML( Html::rawElement( 'div', array( 'id' => 'wikiPreview' ), $previewOutput ) );
$this->setResultFromOutput();
}
protected function doDiff( $editor ) {
$editor->showDiff();
$this->setResultFromOutput();
}
protected function doStore( EditPage $editor ) {
$title = $editor->getTitle();
// If they used redlink=1 and the page exists, redirect to the main article and send notice
if ( $this->getRequest()->getBool( 'redlink' ) && $title->exists() ) {
$this->logMessage( wfMessage( 'pf_autoedit_redlinkexists' )->parse(), self::WARNING );
}
$permErrors = $title->getUserPermissionsErrors( 'edit', $this->getUser() );
// if this title needs to be created, user needs create rights
if ( !$title->exists() ) {
$permErrors = array_merge( $permErrors, wfArrayDiff2( $title->getUserPermissionsErrors( 'create', $this->getUser() ), $permErrors ) );
}
if ( $permErrors ) {
// Auto-block user's IP if the account was "hard" blocked
$this->getUser()->spreadAnyEditBlock();
foreach ( $permErrors as $error ) {
$this->logMessage( call_user_func_array( 'wfMessage', $error )->parse() );
}
return;
}
$resultDetails = false;
# Allow bots to exempt some edits from bot flagging
$bot = $this->getUser()->isAllowed( 'bot' ) && $editor->bot;
$request = $editor->pfFauxRequest;
if ( $editor->tokenOk( $request ) ) {
$ctx = RequestContext::getMain();
$tempTitle = $ctx->getTitle();
$ctx->setTitle( $title );
$status = $editor->internalAttemptSave( $resultDetails, $bot );
$ctx->setTitle( $tempTitle );
} else {
throw new MWException( wfMessage( 'session_fail_preview' )->parse() );
}
switch ( $status->value ) {
case EditPage::AS_HOOK_ERROR_EXPECTED: // A hook function returned an error
// show normal Edit page
// remove Preview and Diff standard buttons from editor page
Hooks::register( 'EditPageBeforeEditButtons', function ( &$editor, &$buttons, &$tabindex ){
foreach ( array_keys( $buttons ) as $key ) {
if ( $key !== 'save' ) {
unset( $buttons[$key] );
}
}
} );
// Context title needed for correct Cancel link
$editor->setContextTitle( $title );
$editor->showEditForm();
return false; // success
case EditPage::AS_CONTENT_TOO_BIG: // Content too big (> $wgMaxArticleSize)
case EditPage::AS_ARTICLE_WAS_DELETED: // article was deleted while editing and param wpRecreate == false or form was not posted
case EditPage::AS_CONFLICT_DETECTED: // (non-resolvable) edit conflict
case EditPage::AS_SUMMARY_NEEDED: // no edit summary given and the user has forceeditsummary set and the user is not editting in his own userspace or talkspace and wpIgnoreBlankSummary == false
case EditPage::AS_TEXTBOX_EMPTY: // user tried to create a new section without content
case EditPage::AS_MAX_ARTICLE_SIZE_EXCEEDED: // article is too big (> $wgMaxArticleSize), after merging in the new section
case EditPage::AS_END: // WikiPage::doEdit() was unsuccessfull
throw new MWException( wfMessage( 'pf_autoedit_fail', $this->mOptions['target'] )->parse() );
case EditPage::AS_HOOK_ERROR: // Article update aborted by a hook function
$this->logMessage( 'Article update aborted by a hook function', self::DEBUG );
return false; // success
case EditPage::AS_PARSE_ERROR: // Can't parse content
throw new MWException( $status->getHTML() );
return true; // fail
case EditPage::AS_SUCCESS_NEW_ARTICLE: // Article successfully created
$query = $resultDetails['redirect'] ? 'redirect=no' : '';
$anchor = isset( $resultDetails['sectionanchor'] ) ? $resultDetails['sectionanchor'] : '';
$redirect = $title->getFullURL( $query ) . $anchor;
$returnto = Title::newFromText( $this->getRequest()->getText( 'returnto' ) );
if ( $returnto !== null ) {
$redirect = $returnto->getFullURL();
}
$this->getOutput()->redirect( $redirect );
$this->getResult()->addValue( null, 'redirect', $redirect );
return false; // success
case EditPage::AS_SUCCESS_UPDATE: // Article successfully updated
$extraQuery = '';
$sectionanchor = $resultDetails['sectionanchor'];
// Give extensions a chance to modify URL query on update
Hooks::run( 'ArticleUpdateBeforeRedirect', array( $editor->getArticle(), &$sectionanchor, &$extraQuery ) );
if ( $resultDetails['redirect'] ) {
if ( $extraQuery == '' ) {
$extraQuery = 'redirect=no';
} else {
$extraQuery = 'redirect=no&' . $extraQuery;
}
}
$redirect = $title->getFullURL( $extraQuery ) . $sectionanchor;
$returnto = Title::newFromText( $this->getRequest()->getText( 'returnto' ) );
if ( $returnto !== null ) {
$redirect = $returnto->getFullURL();
}
$this->getOutput()->redirect( $redirect );
$this->getResult()->addValue( null, 'redirect', $redirect );
return false; // success
case EditPage::AS_BLANK_ARTICLE: // user tried to create a blank page
$this->logMessage( 'User tried to create a blank page', self::DEBUG );
$this->getOutput()->redirect( $editor->getContextTitle()->getFullURL() );
$this->getResult()->addValue( null, 'redirect', $editor->getContextTitle()->getFullURL() );
return false; // success
case EditPage::AS_SPAM_ERROR: // summary contained spam according to one of the regexes in $wgSummarySpamRegex
$match = $resultDetails['spam'];
if ( is_array( $match ) ) {
$match = $this->getLanguage()->listToText( $match );
}
throw new MWException( wfMessage( 'spamprotectionmatch', wfEscapeWikiText( $match ) )->parse() ); // FIXME: Include better error message
case EditPage::AS_BLOCKED_PAGE_FOR_USER: // User is blocked from editing editor page
throw new UserBlockedError( $this->getUser()->getBlock() );
case EditPage::AS_IMAGE_REDIRECT_ANON: // anonymous user is not allowed to upload (User::isAllowed('upload') == false)
case EditPage::AS_IMAGE_REDIRECT_LOGGED: // logged in user is not allowed to upload (User::isAllowed('upload') == false)
throw new PermissionsError( 'upload' );
case EditPage::AS_READ_ONLY_PAGE_ANON: // editor anonymous user is not allowed to edit editor page
case EditPage::AS_READ_ONLY_PAGE_LOGGED: // editor logged in user is not allowed to edit editor page
throw new PermissionsError( 'edit' );
case EditPage::AS_READ_ONLY_PAGE: // wiki is in readonly mode (wfReadOnly() == true)
throw new ReadOnlyError;
case EditPage::AS_RATE_LIMITED: // rate limiter for action 'edit' was tripped
throw new ThrottledError();
case EditPage::AS_NO_CREATE_PERMISSION: // user tried to create editor page, but is not allowed to do that ( Title->usercan('create') == false )
$permission = $title->isTalkPage() ? 'createtalk' : 'createpage';
throw new PermissionsError( $permission );
default:
// We don't recognize $status->value. The only way that can happen
// is if an extension hook aborted from inside ArticleSave.
// Render the status object into $editor->hookError
$editor->hookError = '
' . $status->getWikitext() . '
';
throw new MWException( $status->getHTML() );
}
}
protected function finalizeResults() {
// set response text depending on the status and the requested action
if ( $this->mStatus === 200 ) {
if ( array_key_exists( 'ok text', $this->mOptions ) ) {
$responseText = MessageCache::singleton()->parse( $this->mOptions['ok text'], Title::newFromText( $this->mOptions['target'] ) )->getText();
} elseif ( $this->mAction === self::ACTION_SAVE ) {
$responseText = wfMessage( 'pf_autoedit_success', $this->mOptions['target'], $this->mOptions['form'] )->parse();
} else {
$responseText = null;
}
} else {
// get errortext (or use default)
if ( array_key_exists( 'error text', $this->mOptions ) ) {
$responseText = MessageCache::singleton()->parse( $this->mOptions['error text'], Title::newFromText( $this->mOptions['target'] ) )->getText();
} elseif ( $this->mAction === self::ACTION_SAVE ) {
$responseText = wfMessage( 'pf_autoedit_fail', $this->mOptions['target'] )->parse();
} else {
$responseText = null;
}
}
$result = $this->getResult();
if ( $responseText !== null ) {
$result->addValue( null, 'responseText', $responseText );
}
$result->addValue( null, 'status', $this->mStatus, true );
$result->addValue( array( 'form' ), 'title', $this->mOptions['form'] );
$result->addValue( null, 'target', $this->mOptions['target'], true );
}
/**
* Set custom headers to attach to the answer
*/
protected function setHeaders() {
if ( !headers_sent() ) {
header( 'X-Status: ' . $this->mStatus, true, $this->mStatus );
header( 'X-Form: ' . $this->mOptions['form'] );
header( 'X-Target: ' . $this->mOptions['target'] );
$redirect = $this->getOutput()->getRedirect();
if ( $redirect ) {
header( 'X-Location: ' . $redirect );
}
}
}
/**
* Generates a target name from the given target name formula
*
* This parses the formula and replaces <unique number> tags
*
* @param type $targetNameFormula
*
* @throws MWException
* @return type
*/
protected function generateTargetName( $targetNameFormula ) {
$targetName = $targetNameFormula;
// Prepend a super-page, if one was specified.
if ( $this->getRequest()->getCheck( 'super_page' ) ) {
$targetName = $this->getRequest()->getVal( 'super_page' ) . '/' . $targetName;
}
// Prepend a namespace, if one was specified.
if ( $this->getRequest()->getCheck( 'namespace' ) ) {
$targetName = $this->getRequest()->getVal( 'namespace' ) . ':' . $targetName;
}
// replace "unique number" tag with one that won't get erased by the next line
$targetName = preg_replace( '//', '{num\1}', $targetName, 1 );
// If any formula stuff is still in the name after the parsing,
// just remove it.
// FIXME: This is wrong. If anything is still left, something
// should have been present in the form and wasn't. An error
// should be raised.
// $targetName = StringUtils::delimiterReplace( '<', '>', '', $targetName );
// Replace spaces back with underlines, in case a magic word or
// parser function name contains underlines - hopefully this
// won't cause problems of its own.
$targetName = str_replace( ' ', '_', $targetName );
// now run the parser on it
global $wgParser, $wgTitle;
$targetName = $wgParser->transformMsg( $targetName, new ParserOptions(), $wgTitle );
$titleNumber = '';
$isRandom = false;
$randomNumHasPadding = false;
$randomNumDigits = 6;
if ( preg_match( '/{num.*}/', $targetName, $matches ) && strpos( $targetName, '{num' ) !== false ) {
// Random number
if ( preg_match( '/{num;random(;(0)?([1-9][0-9]*))?}/', $targetName, $matches ) ) {
$isRandom = true;
$randomNumHasPadding = array_key_exists( 2, $matches );
$randomNumDigits = ( array_key_exists( 3, $matches ) ? $matches[3] : $randomNumDigits );
$titleNumber = self::makeRandomNumber( $randomNumDigits, $randomNumHasPadding );
} elseif ( preg_match( '/{num.*start[_]*=[_]*([^;]*).*}/', $targetName, $matches ) ) {; // get unique number start value
// from target name; if it's not
// there, or it's not a positive
// number, start it out as blank
if ( count( $matches ) == 2 && is_numeric( $matches[1] ) && $matches[1] >= 0 ) {
// the "start" value"
$titleNumber = $matches[1];
}
} elseif ( preg_match( '/^(_?{num.*}?)*$/', $targetName, $matches ) ) {
// the target name contains only underscores and number fields,
// i.e. would result in an empty title without the number set
$titleNumber = '1';
} else {
$titleNumber = '';
}
// set target title
$targetTitle = Title::newFromText( preg_replace( '/{num.*}/', $titleNumber, $targetName ) );
// if the specified target title is invalid, give up
if ( !$targetTitle instanceof Title ) {
throw new MWException( wfMessage( 'pf_autoedit_invalidtargetspecified', trim( preg_replace( '//', $titleNumber, $targetNameFormula ) ) )->parse() );
}
// If title exists already, cycle through numbers for
// this tag until we find one that gives a nonexistent
// page title.
// We cannot use $targetTitle->exists(); it does not use
// Title::GAID_FOR_UPDATE, which is needed to get
// correct data from cache; use
// $targetTitle->getArticleID() instead.
$numAttemptsAtTitle = 0;
while ( $targetTitle->getArticleID( Title::GAID_FOR_UPDATE ) !== 0 ) {
$numAttemptsAtTitle++;
if ( $isRandom ) {
// If the set of pages is "crowded"
// already, go one digit higher.
if ( $numAttemptsAtTitle > 20 ) {
$randomNumDigits++;
}
$titleNumber = self::makeRandomNumber( $randomNumDigits, $randomNumHasPadding );
} elseif ( $titleNumber == "" ) {
// If title number is blank, change it to 2;
// otherwise, increment it, and if necessary
// pad it with leading 0s as well.
$titleNumber = 2;
} else {
$titleNumber = str_pad( $titleNumber + 1, strlen( $titleNumber ), '0', STR_PAD_LEFT );
}
$targetTitle = Title::newFromText( preg_replace( '/{num.*}/', $titleNumber, $targetName ) );
}
$targetName = $targetTitle->getPrefixedText();
}
return $targetName;
}
/**
* Helper function..
* @param Title $title
* @return string
*/
function getTextForPage( $title ) {
$wikiPage = WikiPage::factory( $title );
return $wikiPage->getContent( Revision::RAW )->getNativeData();
}
/**
* Returns a formatted (pseudo) random number
*
* @param number $numDigits the min width of the random number
* @param bool $hasPadding should the number should be padded with zeros instead of spaces?
* @return number
*/
static function makeRandomNumber( $numDigits = 1, $hasPadding = false ) {
$maxValue = pow( 10, $numDigits ) - 1;
if ( $maxValue > getrandmax() ) {
$maxValue = getrandmax();
}
$value = rand( 0, $maxValue );
$format = '%' . ( $hasPadding ? '0' : '' ) . $numDigits . 'd';
return trim( sprintf( $format, $value ) ); // trim needed, when $hasPadding == false
}
/**
* Depending on the requested action this method will try to
* store/preview the data in mOptions or retrieve the edit form.
*
* The form and target page will be available in mOptions after
* execution of the method.
*
* Errors and warnings are logged in the API result under the 'errors'
* key. The general request status is maintained in mStatus.
*
* @global $wgRequest
* @global $wgOut
* @global PFFormPrinter $wgPageFormsFormPrinter
* @throws MWException
*/
public function doAction() {
global $wgOut, $wgParser, $wgRequest, $wgPageFormsFormPrinter;
// If the wiki is read-only, do not save.
if ( wfReadOnly() ) {
if ( $this->mAction === self::ACTION_SAVE ) {
throw new MWException( wfMessage( 'pf_autoedit_readonly', wfReadOnlyReason() )->parse() );
}
// even if not saving notify client anyway. Might want to dislay a notice
$this->logMessage( wfMessage( 'pf_autoedit_readonly', wfReadOnlyReason() )->parse(), self::NOTICE );
}
// find the title of the form to be used
$formTitle = $this->getFormTitle();
// get the form content
$formContent = StringUtils::delimiterReplace(
'', // start delimiter
'', // end delimiter
'', // replace by
$this->getTextForPage( $formTitle ) // subject
);
// signals that the form was submitted
// always true, else we would not be here
$isFormSubmitted = $this->mAction === self::ACTION_SAVE || $this->mAction === self::ACTION_PREVIEW || $this->mAction === self::ACTION_DIFF;
// the article id of the form to be used
$formArticleId = $formTitle->getArticleID();
// the name of the target page; might be empty when using the one-step-process
$targetName = $this->mOptions['target'];
// if the target page was not specified, try finding the page name formula
// (Why is this not done in PFFormPrinter::formHTML?)
if ( $targetName === '' ) {
// Parse the form to see if it has a 'page name' value set.
if ( preg_match( '/{{{\s*info.*page name\s*=\s*(.*)}}}/msU', $formContent, $matches ) ) {
$pageNameElements = PFUtils::getFormTagComponents( trim( $matches[1] ) );
$targetNameFormula = $pageNameElements[0];
} else {
throw new MWException( wfMessage( 'pf_autoedit_notargetspecified' )->parse() );
}
$targetTitle = null;
} else {
$targetNameFormula = null;
$targetTitle = Title::newFromText( $targetName );
}
$preloadContent = '';
// save $wgRequest for later restoration
$oldRequest = $wgRequest;
$pageExists = false;
// preload data if not explicitly excluded and if the preload page exists
if ( !isset( $this->mOptions['preload'] ) || $this->mOptions['preload'] !== false ) {
if ( isset( $this->mOptions['preload'] ) && is_string( $this->mOptions['preload'] ) ) {
$preloadTitle = Title::newFromText( $this->mOptions['preload'] );
} else {
$preloadTitle = Title::newFromText( $targetName );
}
if ( $preloadTitle !== null && $preloadTitle->exists() ) {
// the content of the page that was specified to be used for preloading
$preloadContent = $this->getTextForPage( $preloadTitle );
$pageExists = true;
} else {
if ( isset( $this->mOptions['preload'] ) ) {
$this->logMessage( wfMessage( 'pf_autoedit_invalidpreloadspecified', $this->mOptions['preload'] )->parse(), self::WARNING );
}
}
}
// Allow extensions to set/change the preload text, for new
// pages.
if ( !$pageExists ) {
Hooks::run( 'PageForms::EditFormPreloadText', array( &$preloadContent, $targetTitle, $formTitle ) );
} else {
Hooks::run( 'PageForms::EditFormInitialText', array( &$preloadContent, $targetTitle, $formTitle ) );
}
Hooks::run( 'PageForms::EditFormInitContent', array( &$preloadContent, $targetTitle, $formTitle ) );
// Flag to keep track of formHTML() runs.
$formHtmlHasRun = false;
if ( $preloadContent !== '' ) {
// @HACK - we need to set this for the preload to take
// effect in the form.
$pageExists = true;
// Spoof $wgRequest for PFFormPrinter::formHTML().
if ( isset( $_SESSION ) ) {
$wgRequest = new FauxRequest( $this->mOptions, true, $_SESSION );
} else {
$wgRequest = new FauxRequest( $this->mOptions, true );
}
// Call PFFormPrinter::formHTML() to get at the form
// HTML of the existing page.
list( $formHTML, $targetContent, $form_page_title, $generatedTargetNameFormula ) =
$wgPageFormsFormPrinter->formHTML(
// Special handling for autoedit edits -
// otherwise, multi-instance templates
// don't get saved, for some convoluted
// reason.
$formContent, ( $isFormSubmitted && !$this->mIsAutoEdit ), $pageExists,
$formArticleId, $preloadContent, $targetName, $targetNameFormula
);
$formHtmlHasRun = true;
// Parse the data to be preloaded from the form HTML of
// the existing page.
$data = $this->parseDataFromHTMLFrag( $formHTML );
// ...and merge/overwrite it with the new data.
$this->mOptions = PFUtils::array_merge_recursive_distinct( $data, $this->mOptions );
}
// We already preloaded stuff for saving/previewing -
// do not do this again.
if ( $isFormSubmitted && !$wgRequest->getCheck( 'partial' ) ) {
$preloadContent = '';
$pageExists = false;
} else {
// Source of the data is a page.
$pageExists = ( is_a( $targetTitle, 'Title' ) && $targetTitle->exists() );
}
// Spoof $wgRequest for PFFormPrinter::formHTML().
if ( isset( $_SESSION ) ) {
$wgRequest = new FauxRequest( $this->mOptions, true, $_SESSION );
} else {
$wgRequest = new FauxRequest( $this->mOptions, true );
}
// Get wikitext for submitted data and form - call formHTML(),
// if we haven't called it already.
if ( $preloadContent == '' ) {
list( $formHTML, $targetContent, $generatedFormName, $generatedTargetNameFormula ) =
$wgPageFormsFormPrinter->formHTML( $formContent, $isFormSubmitted, $pageExists, $formArticleId, $preloadContent, $targetName, $targetNameFormula );
} else {
$generatedFormName = $form_page_title;
}
// Restore original request.
$wgRequest = $oldRequest;
if ( $generatedFormName !== '' ) {
$this->mOptions['formtitle'] = $generatedFormName;
}
$this->mOptions['formHTML'] = $formHTML;
if ( $isFormSubmitted ) {
// If the target page was not specified, see if
// something was generated from the target name formula.
if ( $this->mOptions['target'] === '' ) {
// If no name was generated, we cannot save => give up
if ( $generatedTargetNameFormula === '' ) {
throw new MWException( wfMessage( 'pf_autoedit_notargetspecified' )->parse() );
}
$this->mOptions['target'] = $this->generateTargetName( $generatedTargetNameFormula );
}
// Lets other code process additional form-definition syntax
Hooks::run( 'PageForms::WritePageData', array( $this->mOptions['form'], Title::newFromText( $this->mOptions['target'] ), &$targetContent ) );
$editor = $this->setupEditPage( $targetContent );
// Perform the requested action.
if ( $this->mAction === self::ACTION_PREVIEW ) {
$this->doPreview( $editor );
} elseif ( $this->mAction === self::ACTION_DIFF ) {
$this->doDiff( $editor );
} else {
$this->doStore( $editor );
}
} elseif ( $this->mAction === self::ACTION_FORMEDIT ) {
$parserOutput = $wgParser->getOutput();
if ( method_exists( $wgOut, 'addParserOutputMetadata' ) ) {
$wgOut->addParserOutputMetadata( $parserOutput );
} else {
$wgOut->addParserOutputNoText( $parserOutput );
}
$this->getResult()->addValue( array( 'form' ), 'HTML', $formHTML );
}
}
private function parseDataFromHTMLFrag( $html ) {
$data = array();
$doc = new DOMDocument();
$oldVal = libxml_disable_entity_loader( true );
@$doc->loadHTML(
''
. $html
. ''
);
libxml_disable_entity_loader( $oldVal );
// Process input tags.
$inputs = $doc->getElementsByTagName( 'input' );
for ( $i = 0; $i < $inputs->length; $i++ ) {
$input = $inputs->item( $i );
$type = $input->getAttribute( 'type' );
$name = trim( $input->getAttribute( 'name' ) );
if ( !$name || $input->hasAttribute( 'disabled' ) ) {
continue;
}
if ( $type === '' ) {
$type = 'text';
}
switch ( $type ) {
case 'checkbox':
case 'radio':
if ( $input->hasAttribute( 'checked' ) ) {
self::addToArray( $data, $name, $input->getAttribute( 'value' ) );
}
break;
// case 'button':
case 'hidden':
case 'image':
case 'password':
// case 'reset':
// case 'submit':
case 'text':
self::addToArray( $data, $name, $input->getAttribute( 'value' ) );
break;
}
}
// Process select tags
$selects = $doc->getElementsByTagName( 'select' );
for ( $i = 0; $i < $selects->length; $i++ ) {
$select = $selects->item( $i );
$name = trim( $select->getAttribute( 'name' ) );
if ( !$name || $select->hasAttribute( 'disabled' ) ) {
continue;
}
$options = $select->getElementsByTagName( 'option' );
// If the current $select is a radio button select
// (i.e. not multiple) set the first option to selected
// as default. This may be overwritten in the loop below.
if ( $options->length > 0 && ( !$select->hasAttribute( 'multiple' ) ) ) {
self::addToArray( $data, $name, $options->item( 0 )->getAttribute( 'value' ) );
}
for ( $o = 0; $o < $options->length; $o++ ) {
if ( $options->item( $o )->hasAttribute( 'selected' ) ) {
if ( $options->item( $o )->getAttribute( 'value' ) ) {
self::addToArray( $data, $name, $options->item( $o )->getAttribute( 'value' ) );
} else {
self::addToArray( $data, $name, $options->item( $o )->nodeValue );
}
}
}
}
// Process textarea tags
$textareas = $doc->getElementsByTagName( 'textarea' );
for ( $i = 0; $i < $textareas->length; $i++ ) {
$textarea = $textareas->item( $i );
$name = trim( $textarea->getAttribute( 'name' ) );
if ( !$name ) {
continue;
}
self::addToArray( $data, $name, $textarea->textContent );
}
return $data;
}
/**
* Parses data from a query string into the $data array
*
* @param Array $data
* @param String $queryString
* @return Array
*/
private function parseDataFromQueryString( &$data, $queryString ) {
$params = explode( '&', $queryString );
foreach ( $params as $param ) {
$elements = explode( '=', $param, 2 );
$key = trim( urldecode( $elements[0] ) );
$value = count( $elements ) > 1 ? urldecode( $elements[1] ) : null;
if ( $key == "query" || $key == "query string" ) {
$this->parseDataFromQueryString( $data, $value );
} else {
self::addToArray( $data, $key, $value );
}
}
return $data;
}
/**
* This function recursively inserts the value into a tree.
*
* @param array &$array is root
* @param string $key identifies path to position in tree.
* Format: 1stLevelName[2ndLevel][3rdLevel][...], i.e. normal array notation
* @param mixed $value the value to insert
* @param bool $toplevel if this is a toplevel value.
*/
public static function addToArray( &$array, $key, $value, $toplevel = true ) {
$matches = array();
if ( preg_match( '/^([^\[\]]*)\[([^\[\]]*)\](.*)/', $key, $matches ) ) {
// for some reason toplevel keys get their spaces encoded by MW.
// We have to imitate that.
if ( $toplevel ) {
$key = str_replace( ' ', '_', $matches[1] );
} else {
$key = $matches[1];
}
// if subsequent element does not exist yet or is a string (we prefer arrays over strings)
if ( !array_key_exists( $key, $array ) || is_string( $array[$key] ) ) {
$array[$key] = array();
}
self::addToArray( $array[$key], $matches[2] . $matches[3], $value, false );
} else {
if ( $key ) {
// only add the string value if there is no child array present
if ( !array_key_exists( $key, $array ) || !is_array( $array[$key] ) ) {
$array[$key] = $value;
}
} else {
array_push( $array, $value );
}
}
}
/**
* Add error message to the ApiResult
*
* @param string $msg
* @param int $errorLevel
*
* @return string
*/
private function logMessage( $msg, $errorLevel = self::ERROR ) {
if ( $errorLevel === self::ERROR ) {
$this->mStatus = 400;
}
$this->getResult()->addValue( array( 'errors' ), null, array( 'level' => $errorLevel, 'message' => $msg ) );
return $msg;
}
/**
* Indicates whether this module requires write mode
* @return bool
*/
public function isWriteMode() {
return true;
}
/**
* Returns the array of allowed parameters (parameter name) => (default
* value) or (parameter name) => (array with PARAM_* constants as keys)
* Don't call this function directly: use getFinalParams() to allow
* hooks to modify parameters as needed.
*
* @return array or false
*/
function getAllowedParams() {
return array(
'form' => null,
'target' => null,
'query' => null,
'preload' => null
);
}
/**
* Returns an array of parameter descriptions.
* Don't call this function directly: use getFinalParamDescription() to
* allow hooks to modify descriptions as needed.
*
* @return array or false
*/
function getParamDescription() {
return array(
'form' => 'The form to use.',
'target' => 'The target page.',
'query' => 'The query string.',
'preload' => 'The name of a page to preload'
);
}
/**
* Returns the description string for this module
*
* @return string|string[]
*/
function getDescription() {
return <<