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 <<