->edittime}'" . " (article time '{$timestamp}')\n" ); $content = $this->mArticle->replaceSectionContent( $this->section, $textbox_content, $sectionTitle, $this->edittime ); } else { wfDebug( __METHOD__ . ": getting section '{$this->section}'\n" ); $content = $this->mArticle->replaceSectionContent( $this->section, $textbox_content, $sectionTitle ); } if ( is_null( $content ) ) { wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" ); $this->isConflict = true; $content = $textbox_content; // do not try to merge here! } elseif ( $this->isConflict ) { # Attempt merge if ( $this->mergeChangesIntoContent( $content ) ) { // Successful merge! Maybe we should tell the user the good news? $this->isConflict = false; wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" ); } else { $this->section = ''; $this->textbox1 = ContentHandler::getContentText( $content ); wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" ); } } if ( $this->isConflict ) { $status->setResult( false, self::AS_CONFLICT_DETECTED ); return $status; } if ( !$this->runPostMergeFilters( $content, $status, $wgUser ) ) { return $status; } if ( $this->section == 'new' ) { // Handle the user preference to force summaries here if ( !$this->allowBlankSummary && trim( $this->summary ) == '' ) { $this->missingSummary = true; $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh $status->value = self::AS_SUMMARY_NEEDED; return $status; } // Do not allow the user to post an empty comment if ( $this->textbox1 == '' ) { $this->missingComment = true; $status->fatal( 'missingcommenttext' ); $status->value = self::AS_TEXTBOX_EMPTY; return $status; } } elseif ( !$this->allowBlankSummary && !$content->equals( $this->getOriginalContent( $wgUser ) ) && !$content->isRedirect() && md5( $this->summary ) == $this->autoSumm ) { $this->missingSummary = true; $status->fatal( 'missingsummary' ); $status->value = self::AS_SUMMARY_NEEDED; return $status; } # All's well $sectionanchor = ''; if ( $this->section == 'new' ) { $this->summary = $this->newSectionSummary( $sectionanchor ); } elseif ( $this->section != '' ) { # Try to get a section anchor from the section source, redirect # to edited section if header found. # XXX: Might be better to integrate this into Article::replaceSection # for duplicate heading checking and maybe parsing. $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches ); # We can't deal with anchors, includes, html etc in the header for now, # headline would need to be parsed to improve this. if ( $hasmatch && strlen( $matches[2] ) > 0 ) { $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $matches[2] ); } } $result['sectionanchor'] = $sectionanchor; // Save errors may fall down to the edit form, but we've now // merged the section into full text. Clear the section field // so that later submission of conflict forms won't try to // replace that into a duplicated mess. $this->textbox1 = $this->toEditText( $content ); $this->section = ''; $status->value = self::AS_SUCCESS_UPDATE; } if ( !$this->allowSelfRedirect && $content->isRedirect() && $content->getRedirectTarget()->equals( $this->getTitle() ) ) { // If the page already redirects to itself, don't warn. $currentTarget = $this->getCurrentContent()->getRedirectTarget(); if ( !$currentTarget || !$currentTarget->equals( $this->getTitle() ) ) { $this->selfRedirect = true; $status->fatal( 'selfredirect' ); $status->value = self::AS_SELF_REDIRECT; return $status; } } // Check for length errors again now that the section is merged in $this->kblength = (int)( strlen( $this->toEditText( $content ) ) / 1024 ); if ( $this->kblength > $wgMaxArticleSize ) { $this->tooBig = true; $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED ); return $status; } $flags = EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY | ( $new ? EDIT_NEW : EDIT_UPDATE ) | ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) | ( $bot ? EDIT_FORCE_BOT : 0 ); $doEditStatus = $this->mArticle->doEditContent( $content, $this->summary, $flags, false, null, $content->getDefaultFormat() ); if ( !$doEditStatus->isOK() ) { // Failure from doEdit() // Show the edit conflict page for certain recognized errors from doEdit(), // but don't show it for errors from extension hooks $errors = $doEditStatus->getErrorsArray(); if ( in_array( $errors[0][0], array( 'edit-gone-missing', 'edit-conflict', 'edit-already-exists' ) ) ) { $this->isConflict = true; // Destroys data doEdit() put in $status->value but who cares $doEditStatus->value = self::AS_END; } return $doEditStatus; } $result['nullEdit'] = $doEditStatus->hasMessage( 'edit-no-change' ); if ( $result['nullEdit'] ) { // We don't know if it was a null edit until now, so increment here $wgUser->pingLimiter( 'linkpurge' ); } $result['redirect'] = $content->isRedirect(); $this->updateWatchlist(); if ( $this->changeTags && isset( $doEditStatus->value['revision'] ) ) { // If a revision was created, apply any change tags that were requested ChangeTags::addTags( $this->changeTags, isset( $doEditStatus->value['rc'] ) ? $doEditStatus->value['rc']->mAttribs['rc_id'] : null, $doEditStatus->value['revision']->getId() ); } return $status; } /** * Register the change of watch status */ protected function updateWatchlist() { global $wgUser; if ( $wgUser->isLoggedIn() && $this->watchthis != $wgUser->isWatched( $this->mTitle, WatchedItem::IGNORE_USER_RIGHTS ) ) { $fname = __METHOD__; $title = $this->mTitle; $watch = $this->watchthis; // Do this in its own transaction to reduce contention... $dbw = wfGetDB( DB_MASTER ); $dbw->onTransactionIdle( function () use ( $dbw, $title, $watch, $wgUser, $fname ) { WatchAction::doWatchOrUnwatch( $watch, $title, $wgUser ); } ); } } /** * Attempts to do 3-way merge of edit content with a base revision * and current content, in case of edit conflict, in whichever way appropriate * for the content type. * * @since 1.21 * * @param Content $editContent * * @return bool */ private function mergeChangesIntoContent( &$editContent ) { $db = wfGetDB( DB_MASTER ); // This is the revision the editor started from $baseRevision = $this->getBaseRevision(); $baseContent = $baseRevision ? $baseRevision->getContent() : null; if ( is_null( $baseContent ) ) { return false; } // The current state, we want to merge updates into it $currentRevision = Revision::loadFromTitle( $db, $this->mTitle ); $currentContent = $currentRevision ? $currentRevision->getContent() : null; if ( is_null( $currentContent ) ) { return false; } $handler = ContentHandler::getForModelID( $baseContent->getModel() ); $result = $handler->merge3( $baseContent, $editContent, $currentContent ); if ( $result ) { $editContent = $result; return true; } return false; } /** * @return Revision */ function getBaseRevision() { if ( !$this->mBaseRevision ) { $db = wfGetDB( DB_MASTER ); $this->mBaseRevision = Revision::loadFromTimestamp( $db, $this->mTitle, $this->edittime ); } return $this->mBaseRevision; } /** * Check given input text against $wgSpamRegex, and return the text of the first match. * * @param string $text * * @return string|bool Matching string or false */ public static function matchSpamRegex( $text ) { global $wgSpamRegex; // For back compatibility, $wgSpamRegex may be a single string or an array of regexes. $regexes = (array)$wgSpamRegex; return self::matchSpamRegexInternal( $text, $regexes ); } /** * Check given input text against $wgSummarySpamRegex, and return the text of the first match. * * @param string $text * * @return string|bool Matching string or false */ public static function matchSummarySpamRegex( $text ) { global $wgSummarySpamRegex; $regexes = (array)$wgSummarySpamRegex; return self::matchSpamRegexInternal( $text, $regexes ); } /** * @param string $text * @param array $regexes * @return bool|string */ protected static function matchSpamRegexInternal( $text, $regexes ) { foreach ( $regexes as $regex ) { $matches = array(); if ( preg_match( $regex, $text, $matches ) ) { return $matches[0]; } } return false; } function setHeaders() { global $wgOut, $wgUser, $wgAjaxEditStash; $wgOut->addModules( 'mediawiki.action.edit' ); $wgOut->addModuleStyles( 'mediawiki.action.edit.styles' ); if ( $wgUser->getOption( 'showtoolbar' ) ) { // The addition of default buttons is handled by getEditToolbar() which // has its own dependency on this module. The call here ensures the module // is loaded in time (it has position "top") for other modules to register // buttons (e.g. extensions, gadgets, user scripts). $wgOut->addModules( 'mediawiki.toolbar' ); } if ( $wgUser->getOption( 'uselivepreview' ) ) { $wgOut->addModules( 'mediawiki.action.edit.preview' ); } if ( $wgUser->getOption( 'useeditwarning' ) ) { $wgOut->addModules( 'mediawiki.action.edit.editWarning' ); } if ( $wgAjaxEditStash ) { $wgOut->addModules( 'mediawiki.action.edit.stash' ); } $wgOut->setRobotPolicy( 'noindex,nofollow' ); # Enabled article-related sidebar, toplinks, etc. $wgOut->setArticleRelated( true ); $contextTitle = $this->getContextTitle(); if ( $this->isConflict ) { $msg = 'editconflict'; } elseif ( $contextTitle->exists() && $this->section != '' ) { $msg = $this->section == 'new' ? 'editingcomment' : 'editingsection'; } else { $msg = $contextTitle->exists() || ( $contextTitle->getNamespace() == NS_MEDIAWIKI && $contextTitle->getDefaultMessageText() !== false ) ? 'editing' : 'creating'; } # Use the title defined by DISPLAYTITLE magic word when present $displayTitle = isset( $this->mParserOutput ) ? $this->mParserOutput->getDisplayTitle() : false; if ( $displayTitle === false ) { $displayTitle = $contextTitle->getPrefixedText(); } $wgOut->setPageTitle( wfMessage( $msg, $displayTitle ) ); # Transmit the name of the message to JavaScript for live preview # Keep Resources.php/mediawiki.action.edit.preview in sync with the possible keys $wgOut->addJsConfigVars( 'wgEditMessage', $msg ); } /** * Show all applicable editing introductions */ protected function showIntro() { global $wgOut, $wgUser; if ( $this->suppressIntro ) { return; } $namespace = $this->mTitle->getNamespace(); if ( $namespace == NS_MEDIAWIKI ) { # Show a warning if editing an interface message $wgOut->wrapWikiMsg( "
\n$1\n
", 'editinginterface' ); # If this is a default message (but not css or js), # show a hint that it is translatable on translatewiki.net if ( !$this->mTitle->hasContentModel( CONTENT_MODEL_CSS ) && !$this->mTitle->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) { $defaultMessageText = $this->mTitle->getDefaultMessageText(); if ( $defaultMessageText !== false ) { $wgOut->wrapWikiMsg( "
\n$1\n
", 'translateinterface' ); } } } elseif ( $namespace == NS_FILE ) { # Show a hint to shared repo $file = wfFindFile( $this->mTitle ); if ( $file && !$file->isLocal() ) { $descUrl = $file->getDescriptionUrl(); # there must be a description url to show a hint to shared repo if ( $descUrl ) { if ( !$this->mTitle->exists() ) { $wgOut->wrapWikiMsg( "
\n$1\n
", array( 'sharedupload-desc-create', $file->getRepo()->getDisplayName(), $descUrl ) ); } else { $wgOut->wrapWikiMsg( "
\n$1\n
", array( 'sharedupload-desc-edit', $file->getRepo()->getDisplayName(), $descUrl ) ); } } } } # Show a warning message when someone creates/edits a user (talk) page but the user does not exist # Show log extract when the user is currently blocked if ( $namespace == NS_USER || $namespace == NS_USER_TALK ) { $parts = explode( '/', $this->mTitle->getText(), 2 ); $username = $parts[0]; $user = User::newFromName( $username, false /* allow IP users*/ ); $ip = User::isIP( $username ); $block = Block::newFromTarget( $user, $user ); if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist $wgOut->wrapWikiMsg( "
\n$1\n
", array( 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ) ); } elseif ( !is_null( $block ) && $block->getType() != Block::TYPE_AUTO ) { # Show log extract if the user is currently blocked LogEventsList::showLogExtract( $wgOut, 'block', MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget(), '', array( 'lim' => 1, 'showIfEmpty' => false, 'msgKey' => array( 'blocked-notice-logextract', $user->getName() # Support GENDER in notice ) ) ); } } # Try to add a custom edit intro, or use the standard one if this is not possible. if ( !$this->showCustomIntro() && !$this->mTitle->exists() ) { $helpLink = wfExpandUrl( Skin::makeInternalOrExternalUrl( wfMessage( 'helppage' )->inContentLanguage()->text() ) ); if ( $wgUser->isLoggedIn() ) { $wgOut->wrapWikiMsg( // Suppress the external link icon, consider the help url an internal one "
\n$1\n
", array( 'newarticletext', $helpLink ) ); } else { $wgOut->wrapWikiMsg( // Suppress the external link icon, consider the help url an internal one "
\n$1\n
", array( 'newarticletextanon', $helpLink ) ); } } # Give a notice if the user is editing a deleted/moved page... if ( !$this->mTitle->exists() ) { LogEventsList::showLogExtract( $wgOut, array( 'delete', 'move' ), $this->mTitle, '', array( 'lim' => 10, 'conds' => array( "log_action != 'revision'" ), 'showIfEmpty' => false, 'msgKey' => array( 'recreate-moveddeleted-warn' ) ) ); } } /** * Attempt to show a custom editing introduction, if supplied * * @return bool */ protected function showCustomIntro() { if ( $this->editintro ) { $title = Title::newFromText( $this->editintro ); if ( $title instanceof Title && $title->exists() && $title->userCan( 'read' ) ) { global $wgOut; // Added using template syntax, to take 's into account. $wgOut->addWikiTextTitleTidy( '
{{:' . $title->getFullText() . '}}
', $this->mTitle ); return true; } } return false; } /** * Gets an editable textual representation of $content. * The textual representation can be turned by into a Content object by the * toEditContent() method. * * If $content is null or false or a string, $content is returned unchanged. * * If the given Content object is not of a type that can be edited using * the text base EditPage, an exception will be raised. Set * $this->allowNonTextContent to true to allow editing of non-textual * content. * * @param Content|null|bool|string $content * @return string The editable text form of the content. * * @throws MWException If $content is not an instance of TextContent and * $this->allowNonTextContent is not true. */ protected function toEditText( $content ) { if ( $content === null || $content === false || is_string( $content ) ) { return $content; } if ( !$this->isSupportedContentModel( $content->getModel() ) ) { throw new MWException( 'This content model is not supported: ' . ContentHandler::getLocalizedName( $content->getModel() ) ); } return $content->serialize( $this->contentFormat ); } /** * Turns the given text into a Content object by unserializing it. * * If the resulting Content object is not of a type that can be edited using * the text base EditPage, an exception will be raised. Set * $this->allowNonTextContent to true to allow editing of non-textual * content. * * @param string|null|bool $text Text to unserialize * @return Content The content object created from $text. If $text was false * or null, false resp. null will be returned instead. * * @throws MWException If unserializing the text results in a Content * object that is not an instance of TextContent and * $this->allowNonTextContent is not true. */ protected function toEditContent( $text ) { if ( $text === false || $text === null ) { return $text; } $content = ContentHandler::makeContent( $text, $this->getTitle(), $this->contentModel, $this->contentFormat ); if ( !$this->isSupportedContentModel( $content->getModel() ) ) { throw new MWException( 'This content model is not supported: ' . ContentHandler::getLocalizedName( $content->getModel() ) ); } return $content; } /** * Send the edit form and related headers to $wgOut * @param callable|null $formCallback That takes an OutputPage parameter; will be called * during form output near the top, for captchas and the like. * * The $formCallback parameter is deprecated since MediaWiki 1.25. Please * use the EditPage::showEditForm:fields hook instead. */ function showEditForm( $formCallback = null ) { global $wgOut, $wgUser; # need to parse the preview early so that we know which templates are used, # otherwise users with "show preview after edit box" will get a blank list # we parse this near the beginning so that setHeaders can do the title # setting work instead of leaving it in getPreviewText $previewOutput = ''; if ( $this->formtype == 'preview' ) { $previewOutput = $this->getPreviewText(); } Hooks::run( 'EditPage::showEditForm:initial', array( &$this, &$wgOut ) ); $this->setHeaders(); if ( $this->showHeader() === false ) { return; } $wgOut->addHTML( $this->editFormPageTop ); if ( $wgUser->getOption( 'previewontop' ) ) { $this->displayPreviewArea( $previewOutput, true ); } $wgOut->addHTML( $this->editFormTextTop ); $showToolbar = true; if ( $this->wasDeletedSinceLastEdit() ) { if ( $this->formtype == 'save' ) { // Hide the toolbar and edit area, user can click preview to get it back // Add an confirmation checkbox and explanation. $showToolbar = false; } else { $wgOut->wrapWikiMsg( "
\n$1\n
", 'deletedwhileediting' ); } } // @todo add EditForm plugin interface and use it here! // search for textarea1 and textares2, and allow EditForm to override all uses. $wgOut->addHTML( Html::openElement( 'form', array( 'id' => self::EDITFORM_ID, 'name' => self::EDITFORM_ID, 'method' => 'post', 'action' => $this->getActionURL( $this->getContextTitle() ), 'enctype' => 'multipart/form-data' ) ) ); if ( is_callable( $formCallback ) ) { wfWarn( 'The $formCallback parameter to ' . __METHOD__ . 'is deprecated' ); call_user_func_array( $formCallback, array( &$wgOut ) ); } // Add an empty field to trip up spambots $wgOut->addHTML( Xml::openElement( 'div', array( 'id' => 'antispam-container', 'style' => 'display: none;' ) ) . Html::rawElement( 'label', array( 'for' => 'wpAntiSpam' ), wfMessage( 'simpleantispam-label' )->parse() ) . Xml::element( 'input', array( 'type' => 'text', 'name' => 'wpAntispam', 'id' => 'wpAntispam', 'value' => '' ) ) . Xml::closeElement( 'div' ) ); Hooks::run( 'EditPage::showEditForm:fields', array( &$this, &$wgOut ) ); // Put these up at the top to ensure they aren't lost on early form submission $this->showFormBeforeText(); if ( $this->wasDeletedSinceLastEdit() && 'save' == $this->formtype ) { $username = $this->lastDelete->user_name; $comment = $this->lastDelete->log_comment; // It is better to not parse the comment at all than to have templates expanded in the middle // TODO: can the checkLabel be moved outside of the div so that wrapWikiMsg could be used? $key = $comment === '' ? 'confirmrecreate-noreason' : 'confirmrecreate'; $wgOut->addHTML( '
' . wfMessage( $key, $username, "$comment" )->parse() . Xml::checkLabel( wfMessage( 'recreate' )->text(), 'wpRecreate', 'wpRecreate', false, array( 'title' => Linker::titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ) ) . '
' ); } # When the summary is hidden, also hide them on preview/show changes if ( $this->nosummary ) { $wgOut->addHTML( Html::hidden( 'nosummary', true ) ); } # If a blank edit summary was previously provided, and the appropriate # user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the # user being bounced back more than once in the event that a summary # is not required. ##### # For a bit more sophisticated detection of blank summaries, hash the # automatic one and pass that in the hidden field wpAutoSummary. if ( $this->missingSummary || ( $this->section == 'new' && $this->nosummary ) ) { $wgOut->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) ); } if ( $this->undidRev ) { $wgOut->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) ); } if ( $this->selfRedirect ) { $wgOut->addHTML( Html::hidden( 'wpIgnoreSelfRedirect', true ) ); } if ( $this->hasPresetSummary ) { // If a summary has been preset using &summary= we don't want to prompt for // a different summary. Only prompt for a summary if the summary is blanked. // (Bug 17416) $this->autoSumm = md5( '' ); } $autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary ); $wgOut->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) ); $wgOut->addHTML( Html::hidden( 'oldid', $this->oldid ) ); $wgOut->addHTML( Html::hidden( 'parentRevId', $this->parentRevId ?: $this->mArticle->getRevIdFetched() ) ); $wgOut->addHTML( Html::hidden( 'format', $this->contentFormat ) ); $wgOut->addHTML( Html::hidden( 'model', $this->contentModel ) ); if ( $this->section == 'new' ) { $this->showSummaryInput( true, $this->summary ); $wgOut->addHTML( $this->getSummaryPreview( true, $this->summary ) ); } $wgOut->addHTML( $this->editFormTextBeforeContent ); if ( !$this->isCssJsSubpage && $showToolbar && $wgUser->getOption( 'showtoolbar' ) ) { $wgOut->addHTML( EditPage::getEditToolbar() ); } if ( $this->blankArticle ) { $wgOut->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) ); } if ( $this->isConflict ) { // In an edit conflict bypass the overridable content form method // and fallback to the raw wpTextbox1 since editconflicts can't be // resolved between page source edits and custom ui edits using the // custom edit ui. $this->textbox2 = $this->textbox1; $content = $this->getCurrentContent(); $this->textbox1 = $this->toEditText( $content ); $this->showTextbox1(); } else { $this->showContentForm(); } $wgOut->addHTML( $this->editFormTextAfterContent ); $this->showStandardInputs(); $this->showFormAfterText(); $this->showTosSummary(); $this->showEditTools(); $wgOut->addHTML( $this->editFormTextAfterTools . "\n" ); $wgOut->addHTML( Html::rawElement( 'div', array( 'class' => 'templatesUsed' ), Linker::formatTemplates( $this->getTemplates(), $this->preview, $this->section != '' ) ) ); $wgOut->addHTML( Html::rawElement( 'div', array( 'class' => 'hiddencats' ), Linker::formatHiddenCategories( $this->mArticle->getHiddenCategories() ) ) ); $wgOut->addHTML( Html::rawElement( 'div', array( 'class' => 'limitreport' ), self::getPreviewLimitReport( $this->mParserOutput ) ) ); $wgOut->addModules( 'mediawiki.action.edit.collapsibleFooter' ); if ( $this->isConflict ) { try { $this->showConflict(); } catch ( MWContentSerializationException $ex ) { // this can't really happen, but be nice if it does. $msg = wfMessage( 'content-failed-to-parse', $this->contentModel, $this->contentFormat, $ex->getMessage() ); $wgOut->addWikiText( '
' . $msg->text() . '
' ); } } // Marker for detecting truncated form data. This must be the last // parameter sent in order to be of use, so do not move me. $wgOut->addHTML( Html::hidden( 'wpUltimateParam', true ) ); $wgOut->addHTML( $this->editFormTextBottom . "\n\n" ); if ( !$wgUser->getOption( 'previewontop' ) ) { $this->displayPreviewArea( $previewOutput, false ); } } /** * Extract the section title from current section text, if any. * * @param string $text * @return string|bool String or false */ public static function extractSectionTitle( $text ) { preg_match( "/^(=+)(.+)\\1\\s*(\n|$)/i", $text, $matches ); if ( !empty( $matches[2] ) ) { global $wgParser; return $wgParser->stripSectionName( trim( $matches[2] ) ); } else { return false; } } /** * @return bool */ protected function showHeader() { global $wgOut, $wgUser, $wgMaxArticleSize, $wgLang; global $wgAllowUserCss, $wgAllowUserJs; if ( $this->mTitle->isTalkPage() ) { $wgOut->addWikiMsg( 'talkpagetext' ); } // Add edit notices $editNotices = $this->mTitle->getEditNotices( $this->oldid ); if ( count( $editNotices ) ) { $wgOut->addHTML( implode( "\n", $editNotices ) ); } else { $msg = wfMessage( 'editnotice-notext' ); if ( !$msg->isDisabled() ) { $wgOut->addHTML( '
' . $msg->parseAsBlock() . '
' ); } } if ( $this->isConflict ) { $wgOut->wrapWikiMsg( "
\n$1\n
", 'explainconflict' ); $this->edittime = $this->mArticle->getTimestamp(); } else { if ( $this->section != '' && !$this->isSectionEditSupported() ) { // We use $this->section to much before this and getVal('wgSection') directly in other places // at this point we can't reset $this->section to '' to fallback to non-section editing. // Someone is welcome to try refactoring though $wgOut->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' ); return false; } if ( $this->section != '' && $this->section != 'new' ) { if ( !$this->summary && !$this->preview && !$this->diff ) { $sectionTitle = self::extractSectionTitle( $this->textbox1 ); //FIXME: use Content object if ( $sectionTitle !== false ) { $this->summary = "/* $sectionTitle */ "; } } } if ( $this->missingComment ) { $wgOut->wrapWikiMsg( "
\n$1\n
", 'missingcommenttext' ); } if ( $this->missingSummary && $this->section != 'new' ) { $wgOut->wrapWikiMsg( "
\n$1\n
", 'missingsummary' ); } if ( $this->missingSummary && $this->section == 'new' ) { $wgOut->wrapWikiMsg( "
\n$1\n
", 'missingcommentheader' ); } if ( $this->blankArticle ) { $wgOut->wrapWikiMsg( "
\n$1\n
", 'blankarticle' ); } if ( $this->selfRedirect ) { $wgOut->wrapWikiMsg( "
\n$1\n
", 'selfredirect' ); } if ( $this->hookError !== '' ) { $wgOut->addWikiText( $this->hookError ); } if ( !$this->checkUnicodeCompliantBrowser() ) { $wgOut->addWikiMsg( 'nonunicodebrowser' ); } if ( $this->section != 'new' ) { $revision = $this->mArticle->getRevisionFetched(); if ( $revision ) { // Let sysop know that this will make private content public if saved if ( !$revision->userCan( Revision::DELETED_TEXT, $wgUser ) ) { $wgOut->wrapWikiMsg( "\n", 'rev-deleted-text-permission' ); } elseif ( $revision->isDeleted( Revision::DELETED_TEXT ) ) { $wgOut->wrapWikiMsg( "\n", 'rev-deleted-text-view' ); } if ( !$revision->isCurrent() ) { $this->mArticle->setOldSubtitle( $revision->getId() ); $wgOut->addWikiMsg( 'editingold' ); } } elseif ( $this->mTitle->exists() ) { // Something went wrong $wgOut->wrapWikiMsg( "
\n$1\n
\n", array( 'missing-revision', $this->oldid ) ); } } } if ( wfReadOnly() ) { $wgOut->wrapWikiMsg( "
\n$1\n
", array( 'readonlywarning', wfReadOnlyReason() ) ); } elseif ( $wgUser->isAnon() ) { if ( $this->formtype != 'preview' ) { $wgOut->wrapWikiMsg( "
\n$1\n
", array( 'anoneditwarning', // Log-in link '{{fullurl:Special:UserLogin|returnto={{FULLPAGENAMEE}}}}', // Sign-up link '{{fullurl:Special:UserLogin/signup|returnto={{FULLPAGENAMEE}}}}' ) ); } else { $wgOut->wrapWikiMsg( "
\n$1
", 'anonpreviewwarning' ); } } else { if ( $this->isCssJsSubpage ) { # Check the skin exists if ( $this->isWrongCaseCssJsPage ) { $wgOut->wrapWikiMsg( "
\n$1\n
", array( 'userinvalidcssjstitle', $this->mTitle->getSkinFromCssJsSubpage() ) ); } if ( $this->getTitle()->isSubpageOf( $wgUser->getUserPage() ) ) { if ( $this->formtype !== 'preview' ) { if ( $this->isCssSubpage && $wgAllowUserCss ) { $wgOut->wrapWikiMsg( "
\n$1\n
", array( 'usercssyoucanpreview' ) ); } if ( $this->isJsSubpage && $wgAllowUserJs ) { $wgOut->wrapWikiMsg( "
\n$1\n
", array( 'userjsyoucanpreview' ) ); } } } } } if ( $this->mTitle->isProtected( 'edit' ) && MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== array( '' ) ) { # Is the title semi-protected? if ( $this->mTitle->isSemiProtected() ) { $noticeMsg = 'semiprotectedpagewarning'; } else { # Then it must be protected based on static groups (regular) $noticeMsg = 'protectedpagewarning'; } LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle, '', array( 'lim' => 1, 'msgKey' => array( $noticeMsg ) ) ); } if ( $this->mTitle->isCascadeProtected() ) { # Is this page under cascading protection from some source pages? list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources(); $notice = "
\n$1\n"; $cascadeSourcesCount = count( $cascadeSources ); if ( $cascadeSourcesCount > 0 ) { # Explain, and list the titles responsible foreach ( $cascadeSources as $page ) { $notice .= '* [[:' . $page->getPrefixedText() . "]]\n"; } } $notice .= '
'; $wgOut->wrapWikiMsg( $notice, array( 'cascadeprotectedwarning', $cascadeSourcesCount ) ); } if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) { LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle, '', array( 'lim' => 1, 'showIfEmpty' => false, 'msgKey' => array( 'titleprotectedwarning' ), 'wrap' => "
\n$1
" ) ); } if ( $this->kblength === false ) { $this->kblength = (int)( strlen( $this->textbox1 ) / 1024 ); } if ( $this->tooBig || $this->kblength > $wgMaxArticleSize ) { $wgOut->wrapWikiMsg( "
\n$1\n
", array( 'longpageerror', $wgLang->formatNum( $this->kblength ), $wgLang->formatNum( $wgMaxArticleSize ) ) ); } else { if ( !wfMessage( 'longpage-hint' )->isDisabled() ) { $wgOut->wrapWikiMsg( "
\n$1\n
", array( 'longpage-hint', $wgLang->formatSize( strlen( $this->textbox1 ) ), strlen( $this->textbox1 ) ) ); } } # Add header copyright warning $this->showHeaderCopyrightWarning(); return true; } /** * Standard summary input and label (wgSummary), abstracted so EditPage * subclasses may reorganize the form. * Note that you do not need to worry about the label's for=, it will be * inferred by the id given to the input. You can remove them both by * passing array( 'id' => false ) to $userInputAttrs. * * @param string $summary The value of the summary input * @param string $labelText The html to place inside the label * @param array $inputAttrs Array of attrs to use on the input * @param array $spanLabelAttrs Array of attrs to use on the span inside the label * * @return array An array in the format array( $label, $input ) */ function getSummaryInput( $summary = "", $labelText = null, $inputAttrs = null, $spanLabelAttrs = null ) { // Note: the maxlength is overridden in JS to 255 and to make it use UTF-8 bytes, not characters. $inputAttrs = ( is_array( $inputAttrs ) ? $inputAttrs : array() ) + array( 'id' => 'wpSummary', 'maxlength' => '200', 'tabindex' => '1', 'size' => 60, 'spellcheck' => 'true', ) + Linker::tooltipAndAccesskeyAttribs( 'summary' ); $spanLabelAttrs = ( is_array( $spanLabelAttrs ) ? $spanLabelAttrs : array() ) + array( 'class' => $this->missingSummary ? 'mw-summarymissed' : 'mw-summary', 'id' => "wpSummaryLabel" ); $label = null; if ( $labelText ) { $label = Xml::tags( 'label', $inputAttrs['id'] ? array( 'for' => $inputAttrs['id'] ) : null, $labelText ); $label = Xml::tags( 'span', $spanLabelAttrs, $label ); } $input = Html::input( 'wpSummary', $summary, 'text', $inputAttrs ); return array( $label, $input ); } /** * @param bool $isSubjectPreview True if this is the section subject/title * up top, or false if this is the comment summary * down below the textarea * @param string $summary The text of the summary to display */ protected function showSummaryInput( $isSubjectPreview, $summary = "" ) { global $wgOut, $wgContLang; # Add a class if 'missingsummary' is triggered to allow styling of the summary line $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary'; if ( $isSubjectPreview ) { if ( $this->nosummary ) { return; } } else { if ( !$this->mShowSummaryField ) { return; } } $summary = $wgContLang->recodeForEdit( $summary ); $labelText = wfMessage( $isSubjectPreview ? 'subject' : 'summary' )->parse(); list( $label, $input ) = $this->getSummaryInput( $summary, $labelText, array( 'class' => $summaryClass ), array() ); $wgOut->addHTML( "{$label} {$input}" ); } /** * @param bool $isSubjectPreview True if this is the section subject/title * up top, or false if this is the comment summary * down below the textarea * @param string $summary The text of the summary to display * @return string */ protected function getSummaryPreview( $isSubjectPreview, $summary = "" ) { // avoid spaces in preview, gets always trimmed on save $summary = trim( $summary ); if ( !$summary || ( !$this->preview && !$this->diff ) ) { return ""; } global $wgParser; if ( $isSubjectPreview ) { $summary = wfMessage( 'newsectionsummary' )->rawParams( $wgParser->stripSectionName( $summary ) ) ->inContentLanguage()->text(); } $message = $isSubjectPreview ? 'subject-preview' : 'summary-preview'; $summary = wfMessage( $message )->parse() . Linker::commentBlock( $summary, $this->mTitle, $isSubjectPreview ); return Xml::tags( 'div', array( 'class' => 'mw-summary-preview' ), $summary ); } protected function showFormBeforeText() { global $wgOut; $section = htmlspecialchars( $this->section ); $wgOut->addHTML( << HTML ); if ( !$this->checkUnicodeCompliantBrowser() ) { $wgOut->addHTML( Html::hidden( 'safemode', '1' ) ); } } protected function showFormAfterText() { global $wgOut, $wgUser; /** * To make it harder for someone to slip a user a page * which submits an edit form to the wiki without their * knowledge, a random token is associated with the login * session. If it's not passed back with the submission, * we won't save the page, or render user JavaScript and * CSS previews. * * For anon editors, who may not have a session, we just * include the constant suffix to prevent editing from * broken text-mangling proxies. */ $wgOut->addHTML( "\n" . Html::hidden( "wpEditToken", $wgUser->getEditToken() ) . "\n" ); } /** * Subpage overridable method for printing the form for page content editing * By default this simply outputs wpTextbox1 * Subclasses can override this to provide a custom UI for editing; * be it a form, or simply wpTextbox1 with a modified content that will be * reverse modified when extracted from the post data. * Note that this is basically the inverse for importContentFormData */ protected function showContentForm() { $this->showTextbox1(); } /** * Method to output wpTextbox1 * The $textoverride method can be used by subclasses overriding showContentForm * to pass back to this method. * * @param array $customAttribs Array of html attributes to use in the textarea * @param string $textoverride Optional text to override $this->textarea1 with */ protected function showTextbox1( $customAttribs = null, $textoverride = null ) { if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) { $attribs = array( 'style' => 'display:none;' ); } else { $classes = array(); // Textarea CSS if ( $this->mTitle->isProtected( 'edit' ) && MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== array( '' ) ) { # Is the title semi-protected? if ( $this->mTitle->isSemiProtected() ) { $classes[] = 'mw-textarea-sprotected'; } else { # Then it must be protected based on static groups (regular) $classes[] = 'mw-textarea-protected'; } # Is the title cascade-protected? if ( $this->mTitle->isCascadeProtected() ) { $classes[] = 'mw-textarea-cprotected'; } } $attribs = array( 'tabindex' => 1 ); if ( is_array( $customAttribs ) ) { $attribs += $customAttribs; } if ( count( $classes ) ) { if ( isset( $attribs['class'] ) ) { $classes[] = $attribs['class']; } $attribs['class'] = implode( ' ', $classes ); } } $this->showTextbox( $textoverride !== null ? $textoverride : $this->textbox1, 'wpTextbox1', $attribs ); } protected function showTextbox2() { $this->showTextbox( $this->textbox2, 'wpTextbox2', array( 'tabindex' => 6, 'readonly' ) ); } protected function showTextbox( $text, $name, $customAttribs = array() ) { global $wgOut, $wgUser; $wikitext = $this->safeUnicodeOutput( $text ); if ( strval( $wikitext ) !== '' ) { // Ensure there's a newline at the end, otherwise adding lines // is awkward. // But don't add a newline if the ext is empty, or Firefox in XHTML // mode will show an extra newline. A bit annoying. $wikitext .= "\n"; } $attribs = $customAttribs + array( 'accesskey' => ',', 'id' => $name, 'cols' => $wgUser->getIntOption( 'cols' ), 'rows' => $wgUser->getIntOption( 'rows' ), // Avoid PHP notices when appending preferences // (appending allows customAttribs['style'] to still work). 'style' => '' ); $pageLang = $this->mTitle->getPageLanguage(); $attribs['lang'] = $pageLang->getHtmlCode(); $attribs['dir'] = $pageLang->getDir(); $wgOut->addHTML( Html::textarea( $name, $wikitext, $attribs ) ); } protected function displayPreviewArea( $previewOutput, $isOnTop = false ) { global $wgOut; $classes = array(); if ( $isOnTop ) { $classes[] = 'ontop'; } $attribs = array( 'id' => 'wikiPreview', 'class' => implode( ' ', $classes ) ); if ( $this->formtype != 'preview' ) { $attribs['style'] = 'display: none;'; } $wgOut->addHTML( Xml::openElement( 'div', $attribs ) ); if ( $this->formtype == 'preview' ) { $this->showPreview( $previewOutput ); } else { // Empty content container for LivePreview $pageViewLang = $this->mTitle->getPageViewLanguage(); $attribs = array( 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(), 'class' => 'mw-content-' . $pageViewLang->getDir() ); $wgOut->addHTML( Html::rawElement( 'div', $attribs ) ); } $wgOut->addHTML( '' ); if ( $this->formtype == 'diff' ) { try { $this->showDiff(); } catch ( MWContentSerializationException $ex ) { $msg = wfMessage( 'content-failed-to-parse', $this->contentModel, $this->contentFormat, $ex->getMessage() ); $wgOut->addWikiText( '
' . $msg->text() . '
' ); } } } /** * Append preview output to $wgOut. * Includes category rendering if this is a category page. * * @param string $text The HTML to be output for the preview. */ protected function showPreview( $text ) { global $wgOut; if ( $this->mTitle->getNamespace() == NS_CATEGORY ) { $this->mArticle->openShowCategory(); } # This hook seems slightly odd here, but makes things more # consistent for extensions. Hooks::run( 'OutputPageBeforeHTML', array( &$wgOut, &$text ) ); $wgOut->addHTML( $text ); if ( $this->mTitle->getNamespace() == NS_CATEGORY ) { $this->mArticle->closeShowCategory(); } } /** * Get a diff between the current contents of the edit box and the * version of the page we're editing from. * * If this is a section edit, we'll replace the section as for final * save and then make a comparison. */ function showDiff() { global $wgUser, $wgContLang, $wgOut; $oldtitlemsg = 'currentrev'; # if message does not exist, show diff against the preloaded default if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && !$this->mTitle->exists() ) { $oldtext = $this->mTitle->getDefaultMessageText(); if ( $oldtext !== false ) { $oldtitlemsg = 'defaultmessagetext'; $oldContent = $this->toEditContent( $oldtext ); } else { $oldContent = null; } } else { $oldContent = $this->getCurrentContent(); } $textboxContent = $this->toEditContent( $this->textbox1 ); $newContent = $this->mArticle->replaceSectionContent( $this->section, $textboxContent, $this->summary, $this->edittime ); if ( $newContent ) { ContentHandler::runLegacyHooks( 'EditPageGetDiffText', array( $this, &$newContent ) ); Hooks::run( 'EditPageGetDiffContent', array( $this, &$newContent ) ); $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang ); $newContent = $newContent->preSaveTransform( $this->mTitle, $wgUser, $popts ); } if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) { $oldtitle = wfMessage( $oldtitlemsg )->parse(); $newtitle = wfMessage( 'yourtext' )->parse(); if ( !$oldContent ) { $oldContent = $newContent->getContentHandler()->makeEmptyContent(); } if ( !$newContent ) { $newContent = $oldContent->getContentHandler()->makeEmptyContent(); } $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->mArticle->getContext() ); $de->setContent( $oldContent, $newContent ); $difftext = $de->getDiff( $oldtitle, $newtitle ); $de->showDiffStyle(); } else { $difftext = ''; } $wgOut->addHTML( '
' . $difftext . '
' ); } /** * Show the header copyright warning. */ protected function showHeaderCopyrightWarning() { $msg = 'editpage-head-copy-warn'; if ( !wfMessage( $msg )->isDisabled() ) { global $wgOut; $wgOut->wrapWikiMsg( "
\n$1\n
", 'editpage-head-copy-warn' ); } } /** * Give a chance for site and per-namespace customizations of * terms of service summary link that might exist separately * from the copyright notice. * * This will display between the save button and the edit tools, * so should remain short! */ protected function showTosSummary() { $msg = 'editpage-tos-summary'; Hooks::run( 'EditPageTosSummary', array( $this->mTitle, &$msg ) ); if ( !wfMessage( $msg )->isDisabled() ) { global $wgOut; $wgOut->addHTML( '
' ); $wgOut->addWikiMsg( $msg ); $wgOut->addHTML( '
' ); } } protected function showEditTools() { global $wgOut; $wgOut->addHTML( '
' . wfMessage( 'edittools' )->inContentLanguage()->parse() . '
' ); } /** * Get the copyright warning * * Renamed to getCopyrightWarning(), old name kept around for backwards compatibility * @return string */ protected function getCopywarn() { return self::getCopyrightWarning( $this->mTitle ); } /** * Get the copyright warning, by default returns wikitext * * @param Title $title * @param string $format Output format, valid values are any function of a Message object * @return string */ public static function getCopyrightWarning( $title, $format = 'plain' ) { global $wgRightsText; if ( $wgRightsText ) { $copywarnMsg = array( 'copyrightwarning', '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]', $wgRightsText ); } else { $copywarnMsg = array( 'copyrightwarning2', '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]' ); } // Allow for site and per-namespace customization of contribution/copyright notice. Hooks::run( 'EditPageCopyrightWarning', array( $title, &$copywarnMsg ) ); return "
\n" . call_user_func_array( 'wfMessage', $copywarnMsg )->$format() . "\n
"; } /** * Get the Limit report for page previews * * @since 1.22 * @param ParserOutput $output ParserOutput object from the parse * @return string HTML */ public static function getPreviewLimitReport( $output ) { if ( !$output || !$output->getLimitReportData() ) { return ''; } $limitReport = Html::rawElement( 'div', array( 'class' => 'mw-limitReportExplanation' ), wfMessage( 'limitreport-title' )->parseAsBlock() ); // Show/hide animation doesn't work correctly on a table, so wrap it in a div. $limitReport .= Html::openElement( 'div', array( 'class' => 'preview-limit-report-wrapper' ) ); $limitReport .= Html::openElement( 'table', array( 'class' => 'preview-limit-report wikitable' ) ) . Html::openElement( 'tbody' ); foreach ( $output->getLimitReportData() as $key => $value ) { if ( Hooks::run( 'ParserLimitReportFormat', array( $key, &$value, &$limitReport, true, true ) ) ) { $keyMsg = wfMessage( $key ); $valueMsg = wfMessage( array( "$key-value-html", "$key-value" ) ); if ( !$valueMsg->exists() ) { $valueMsg = new RawMessage( '$1' ); } if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) { $limitReport .= Html::openElement( 'tr' ) . Html::rawElement( 'th', null, $keyMsg->parse() ) . Html::rawElement( 'td', null, $valueMsg->params( $value )->parse() ) . Html::closeElement( 'tr' ); } } } $limitReport .= Html::closeElement( 'tbody' ) . Html::closeElement( 'table' ) . Html::closeElement( 'div' ); return $limitReport; } protected function showStandardInputs( &$tabindex = 2 ) { global $wgOut; $wgOut->addHTML( "
\n" ); if ( $this->section != 'new' ) { $this->showSummaryInput( false, $this->summary ); $wgOut->addHTML( $this->getSummaryPreview( false, $this->summary ) ); } $checkboxes = $this->getCheckboxes( $tabindex, array( 'minor' => $this->minoredit, 'watch' => $this->watchthis ) ); $wgOut->addHTML( "
" . implode( $checkboxes, "\n" ) . "
\n" ); // Show copyright warning. $wgOut->addWikiText( $this->getCopywarn() ); $wgOut->addHTML( $this->editFormTextAfterWarn ); $wgOut->addHTML( "
\n" ); $wgOut->addHTML( implode( $this->getEditButtons( $tabindex ), "\n" ) . "\n" ); $cancel = $this->getCancelLink(); if ( $cancel !== '' ) { $cancel .= Html::element( 'span', array( 'class' => 'mw-editButtons-pipe-separator' ), wfMessage( 'pipe-separator' )->text() ); } $message = wfMessage( 'edithelppage' )->inContentLanguage()->text(); $edithelpurl = Skin::makeInternalOrExternalUrl( $message ); $attrs = array( 'target' => 'helpwindow', 'href' => $edithelpurl, ); $edithelp = Html::linkButton( wfMessage( 'edithelp' )->text(), $attrs, array( 'mw-ui-quiet' ) ) . wfMessage( 'word-separator' )->escaped() . wfMessage( 'newwindow' )->parse(); $wgOut->addHTML( " {$cancel}\n" ); $wgOut->addHTML( " {$edithelp}\n" ); $wgOut->addHTML( "
\n" ); Hooks::run( 'EditPage::showStandardInputs:options', array( $this, $wgOut, &$tabindex ) ); $wgOut->addHTML( "
\n" ); } /** * Show an edit conflict. textbox1 is already shown in showEditForm(). * If you want to use another entry point to this function, be careful. */ protected function showConflict() { global $wgOut; if ( Hooks::run( 'EditPageBeforeConflictDiff', array( &$this, &$wgOut ) ) ) { $wgOut->wrapWikiMsg( '

$1

', "yourdiff" ); $content1 = $this->toEditContent( $this->textbox1 ); $content2 = $this->toEditContent( $this->textbox2 ); $handler = ContentHandler::getForModelID( $this->contentModel ); $de = $handler->createDifferenceEngine( $this->mArticle->getContext() ); $de->setContent( $content2, $content1 ); $de->showDiff( wfMessage( 'yourtext' )->parse(), wfMessage( 'storedversion' )->text() ); $wgOut->wrapWikiMsg( '

$1

', "yourtext" ); $this->showTextbox2(); } } /** * @return string */ public function getCancelLink() { $cancelParams = array(); if ( !$this->isConflict && $this->oldid > 0 ) { $cancelParams['oldid'] = $this->oldid; } $attrs = array( 'id' => 'mw-editform-cancel' ); return Linker::linkKnown( $this->getContextTitle(), wfMessage( 'cancel' )->parse(), Html::buttonAttributes( $attrs, array( 'mw-ui-quiet' ) ), $cancelParams ); } /** * Returns the URL to use in the form's action attribute. * This is used by EditPage subclasses when simply customizing the action * variable in the constructor is not enough. This can be used when the * EditPage lives inside of a Special page rather than a custom page action. * * @param Title $title Title object for which is being edited (where we go to for &action= links) * @return string */ protected function getActionURL( Title $title ) { return $title->getLocalURL( array( 'action' => $this->action ) ); } /** * Check if a page was deleted while the user was editing it, before submit. * Note that we rely on the logging table, which hasn't been always there, * but that doesn't matter, because this only applies to brand new * deletes. * @return bool */ protected function wasDeletedSinceLastEdit() { if ( $this->deletedSinceEdit !== null ) { return $this->deletedSinceEdit; } $this->deletedSinceEdit = false; if ( $this->mTitle->isDeletedQuick() ) { $this->lastDelete = $this->getLastDelete(); if ( $this->lastDelete ) { $deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp ); if ( $deleteTime > $this->starttime ) { $this->deletedSinceEdit = true; } } } return $this->deletedSinceEdit; } /** * @return bool|stdClass */ protected function getLastDelete() { $dbr = wfGetDB( DB_SLAVE ); $data = $dbr->selectRow( array( 'logging', 'user' ), array( 'log_type', 'log_action', 'log_timestamp', 'log_user', 'log_namespace', 'log_title', 'log_comment', 'log_params', 'log_deleted', 'user_name' ), array( 'log_namespace' => $this->mTitle->getNamespace(), 'log_title' => $this->mTitle->getDBkey(), 'log_type' => 'delete', 'log_action' => 'delete', 'user_id=log_user' ), __METHOD__, array( 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ) ); // Quick paranoid permission checks... if ( is_object( $data ) ) { if ( $data->log_deleted & LogPage::DELETED_USER ) { $data->user_name = wfMessage( 'rev-deleted-user' )->escaped(); } if ( $data->log_deleted & LogPage::DELETED_COMMENT ) { $data->log_comment = wfMessage( 'rev-deleted-comment' )->escaped(); } } return $data; } /** * Get the rendered text for previewing. * @throws MWException * @return string */ function getPreviewText() { global $wgOut, $wgUser, $wgRawHtml, $wgLang; global $wgAllowUserCss, $wgAllowUserJs; if ( $wgRawHtml && !$this->mTokenOk ) { // Could be an offsite preview attempt. This is very unsafe if // HTML is enabled, as it could be an attack. $parsedNote = ''; if ( $this->textbox1 !== '' ) { // Do not put big scary notice, if previewing the empty // string, which happens when you initially edit // a category page, due to automatic preview-on-open. $parsedNote = $wgOut->parse( "
" . wfMessage( 'session_fail_preview_html' )->text() . "
", true, /* interface */true ); } return $parsedNote; } $note = ''; try { $content = $this->toEditContent( $this->textbox1 ); $previewHTML = ''; if ( !Hooks::run( 'AlternateEditPreview', array( $this, &$content, &$previewHTML, &$this->mParserOutput ) ) ) { return $previewHTML; } # provide a anchor link to the editform $continueEditing = '' . '[[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . wfMessage( 'continue-editing' )->text() . ']]'; if ( $this->mTriedSave && !$this->mTokenOk ) { if ( $this->mTokenOkExceptSuffix ) { $note = wfMessage( 'token_suffix_mismatch' )->plain(); } else { $note = wfMessage( 'session_fail_preview' )->plain(); } } elseif ( $this->incompleteForm ) { $note = wfMessage( 'edit_form_incomplete' )->plain(); } else { $note = wfMessage( 'previewnote' )->plain() . ' ' . $continueEditing; } $parserOptions = $this->mArticle->makeParserOptions( $this->mArticle->getContext() ); $parserOptions->setIsPreview( true ); $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' ); # don't parse non-wikitext pages, show message about preview if ( $this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage() ) { if ( $this->mTitle->isCssJsSubpage() ) { $level = 'user'; } elseif ( $this->mTitle->isCssOrJsPage() ) { $level = 'site'; } else { $level = false; } if ( $content->getModel() == CONTENT_MODEL_CSS ) { $format = 'css'; if ( $level === 'user' && !$wgAllowUserCss ) { $format = false; } } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) { $format = 'js'; if ( $level === 'user' && !$wgAllowUserJs ) { $format = false; } } else { $format = false; } # Used messages to make sure grep find them: # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview if ( $level && $format ) { $note = "
" . wfMessage( "{$level}{$format}preview" )->text() . ' ' . $continueEditing . "
"; } } # If we're adding a comment, we need to show the # summary as the headline if ( $this->section === "new" && $this->summary !== "" ) { $content = $content->addSectionHeader( $this->summary ); } $hook_args = array( $this, &$content ); ContentHandler::runLegacyHooks( 'EditPageGetPreviewText', $hook_args ); Hooks::run( 'EditPageGetPreviewContent', $hook_args ); $parserOptions->enableLimitReport(); # For CSS/JS pages, we should have called the ShowRawCssJs hook here. # But it's now deprecated, so never mind $pstContent = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions ); $scopedCallback = $parserOptions->setupFakeRevision( $this->mTitle, $pstContent, $wgUser ); $parserOutput = $pstContent->getParserOutput( $this->mTitle, null, $parserOptions ); # Try to stash the edit for the final submission step # @todo: different date format preferences cause cache misses ApiStashEdit::stashEditFromPreview( $this->getArticle(), $content, $pstContent, $parserOutput, $parserOptions, $parserOptions, wfTimestampNow() ); $parserOutput->setEditSectionTokens( false ); // no section edit links $previewHTML = $parserOutput->getText(); $this->mParserOutput = $parserOutput; $wgOut->addParserOutputMetadata( $parserOutput ); if ( count( $parserOutput->getWarnings() ) ) { $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() ); } } catch ( MWContentSerializationException $ex ) { $m = wfMessage( 'content-failed-to-parse', $this->contentModel, $this->contentFormat, $ex->getMessage() ); $note .= "\n\n" . $m->parse(); $previewHTML = ''; } if ( $this->isConflict ) { $conflict = '

' . wfMessage( 'previewconflict' )->escaped() . "

\n"; } else { $conflict = '
'; } $previewhead = "
\n" . '

' . wfMessage( 'preview' )->escaped() . "

" . $wgOut->parse( $note, true, /* interface */true ) . $conflict . "
\n"; $pageViewLang = $this->mTitle->getPageViewLanguage(); $attribs = array( 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(), 'class' => 'mw-content-' . $pageViewLang->getDir() ); $previewHTML = Html::rawElement( 'div', $attribs, $previewHTML ); return $previewhead . $previewHTML . $this->previewTextAfterContent; } /** * @return array */ function getTemplates() { if ( $this->preview || $this->section != '' ) { $templates = array(); if ( !isset( $this->mParserOutput ) ) { return $templates; } foreach ( $this->mParserOutput->getTemplates() as $ns => $template ) { foreach ( array_keys( $template ) as $dbk ) { $templates[] = Title::makeTitle( $ns, $dbk ); } } return $templates; } else { return $this->mTitle->getTemplateLinksFrom(); } } /** * Shows a bulletin board style toolbar for common editing functions. * It can be disabled in the user preferences. * * @return string */ static function getEditToolbar() { global $wgContLang, $wgOut; global $wgEnableUploads, $wgForeignFileRepos; $imagesAvailable = $wgEnableUploads || count( $wgForeignFileRepos ); /** * $toolarray is an array of arrays each of which includes the * opening tag, the closing tag, optionally a sample text that is * inserted between the two when no selection is highlighted * and. The tip text is shown when the user moves the mouse * over the button. * * Images are defined in ResourceLoaderEditToolbarModule. */ $toolarray = array( array( 'id' => 'mw-editbutton-bold', 'open' => '\'\'\'', 'close' => '\'\'\'', 'sample' => wfMessage( 'bold_sample' )->text(), 'tip' => wfMessage( 'bold_tip' )->text(), ), array( 'id' => 'mw-editbutton-italic', 'open' => '\'\'', 'close' => '\'\'', 'sample' => wfMessage( 'italic_sample' )->text(), 'tip' => wfMessage( 'italic_tip' )->text(), ), array( 'id' => 'mw-editbutton-link', 'open' => '[[', 'close' => ']]', 'sample' => wfMessage( 'link_sample' )->text(), 'tip' => wfMessage( 'link_tip' )->text(), ), array( 'id' => 'mw-editbutton-extlink', 'open' => '[', 'close' => ']', 'sample' => wfMessage( 'extlink_sample' )->text(), 'tip' => wfMessage( 'extlink_tip' )->text(), ), array( 'id' => 'mw-editbutton-headline', 'open' => "\n== ", 'close' => " ==\n", 'sample' => wfMessage( 'headline_sample' )->text(), 'tip' => wfMessage( 'headline_tip' )->text(), ), $imagesAvailable ? array( 'id' => 'mw-editbutton-image', 'open' => '[[' . $wgContLang->getNsText( NS_FILE ) . ':', 'close' => ']]', 'sample' => wfMessage( 'image_sample' )->text(), 'tip' => wfMessage( 'image_tip' )->text(), ) : false, $imagesAvailable ? array( 'id' => 'mw-editbutton-media', 'open' => '[[' . $wgContLang->getNsText( NS_MEDIA ) . ':', 'close' => ']]', 'sample' => wfMessage( 'media_sample' )->text(), 'tip' => wfMessage( 'media_tip' )->text(), ) : false, array( 'id' => 'mw-editbutton-nowiki', 'open' => "", 'close' => "", 'sample' => wfMessage( 'nowiki_sample' )->text(), 'tip' => wfMessage( 'nowiki_tip' )->text(), ), array( 'id' => 'mw-editbutton-signature', 'open' => '--~~~~', 'close' => '', 'sample' => '', 'tip' => wfMessage( 'sig_tip' )->text(), ), array( 'id' => 'mw-editbutton-hr', 'open' => "\n----\n", 'close' => '', 'sample' => '', 'tip' => wfMessage( 'hr_tip' )->text(), ) ); $script = 'mw.loader.using("mediawiki.toolbar", function () {'; foreach ( $toolarray as $tool ) { if ( !$tool ) { continue; } $params = array( // Images are defined in ResourceLoaderEditToolbarModule false, // Note that we use the tip both for the ALT tag and the TITLE tag of the image. // Older browsers show a "speedtip" type message only for ALT. // Ideally these should be different, realistically they // probably don't need to be. $tool['tip'], $tool['open'], $tool['close'], $tool['sample'], $tool['id'], ); $script .= Xml::encodeJsCall( 'mw.toolbar.addButton', $params, ResourceLoader::inDebugMode() ); } $script .= '});'; $wgOut->addScript( Html::inlineScript( ResourceLoader::makeLoaderConditionalScript( $script ) ) ); $toolbar = '
'; Hooks::run( 'EditPageBeforeEditToolbar', array( &$toolbar ) ); return $toolbar; } /** * Returns an array of html code of the following checkboxes: * minor and watch * * @param int $tabindex Current tabindex * @param array $checked Array of checkbox => bool, where bool indicates the checked * status of the checkbox * * @return array */ public function getCheckboxes( &$tabindex, $checked ) { global $wgUser, $wgUseMediaWikiUIEverywhere; $checkboxes = array(); // don't show the minor edit checkbox if it's a new page or section if ( !$this->isNew ) { $checkboxes['minor'] = ''; $minorLabel = wfMessage( 'minoredit' )->parse(); if ( $wgUser->isAllowed( 'minoredit' ) ) { $attribs = array( 'tabindex' => ++$tabindex, 'accesskey' => wfMessage( 'accesskey-minoredit' )->text(), 'id' => 'wpMinoredit', ); $minorEditHtml = Xml::check( 'wpMinoredit', $checked['minor'], $attribs ) . " "; if ( $wgUseMediaWikiUIEverywhere ) { $checkboxes['minor'] = Html::openElement( 'div', array( 'class' => 'mw-ui-checkbox' ) ) . $minorEditHtml . Html::closeElement( 'div' ); } else { $checkboxes['minor'] = $minorEditHtml; } } } $watchLabel = wfMessage( 'watchthis' )->parse(); $checkboxes['watch'] = ''; if ( $wgUser->isLoggedIn() ) { $attribs = array( 'tabindex' => ++$tabindex, 'accesskey' => wfMessage( 'accesskey-watch' )->text(), 'id' => 'wpWatchthis', ); $watchThisHtml = Xml::check( 'wpWatchthis', $checked['watch'], $attribs ) . " "; if ( $wgUseMediaWikiUIEverywhere ) { $checkboxes['watch'] = Html::openElement( 'div', array( 'class' => 'mw-ui-checkbox' ) ) . $watchThisHtml . Html::closeElement( 'div' ); } else { $checkboxes['watch'] = $watchThisHtml; } } Hooks::run( 'EditPageBeforeEditChecks', array( &$this, &$checkboxes, &$tabindex ) ); return $checkboxes; } /** * Returns an array of html code of the following buttons: * save, diff, preview and live * * @param int $tabindex Current tabindex * * @return array */ public function getEditButtons( &$tabindex ) { $buttons = array(); $attribs = array( 'id' => 'wpSave', 'name' => 'wpSave', 'tabindex' => ++$tabindex, ) + Linker::tooltipAndAccesskeyAttribs( 'save' ); $buttons['save'] = Html::submitButton( wfMessage( 'savearticle' )->text(), $attribs, array( 'mw-ui-constructive' ) ); ++$tabindex; // use the same for preview and live preview $attribs = array( 'id' => 'wpPreview', 'name' => 'wpPreview', 'tabindex' => $tabindex, ) + Linker::tooltipAndAccesskeyAttribs( 'preview' ); $buttons['preview'] = Html::submitButton( wfMessage( 'showpreview' )->text(), $attribs ); $buttons['live'] = ''; $attribs = array( 'id' => 'wpDiff', 'name' => 'wpDiff', 'tabindex' => ++$tabindex, ) + Linker::tooltipAndAccesskeyAttribs( 'diff' ); $buttons['diff'] = Html::submitButton( wfMessage( 'showdiff' )->text(), $attribs ); Hooks::run( 'EditPageBeforeEditButtons', array( &$this, &$buttons, &$tabindex ) ); return $buttons; } /** * Creates a basic error page which informs the user that * they have attempted to edit a nonexistent section. */ function noSuchSectionPage() { global $wgOut; $wgOut->prepareErrorPage( wfMessage( 'nosuchsectiontitle' ) ); $res = wfMessage( 'nosuchsectiontext', $this->section )->parseAsBlock(); Hooks::run( 'EditPageNoSuchSection', array( &$this, &$res ) ); $wgOut->addHTML( $res ); $wgOut->returnToMain( false, $this->mTitle ); } /** * Show "your edit contains spam" page with your diff and text * * @param string|array|bool $match Text (or array of texts) which triggered one or more filters */ public function spamPageWithContent( $match = false ) { global $wgOut, $wgLang; $this->textbox2 = $this->textbox1; if ( is_array( $match ) ) { $match = $wgLang->listToText( $match ); } $wgOut->prepareErrorPage( wfMessage( 'spamprotectiontitle' ) ); $wgOut->addHTML( '
' ); $wgOut->addWikiMsg( 'spamprotectiontext' ); if ( $match ) { $wgOut->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) ); } $wgOut->addHTML( '
' ); $wgOut->wrapWikiMsg( '

$1

', "yourdiff" ); $this->showDiff(); $wgOut->wrapWikiMsg( '

$1

', "yourtext" ); $this->showTextbox2(); $wgOut->addReturnTo( $this->getContextTitle(), array( 'action' => 'edit' ) ); } /** * Check if the browser is on a blacklist of user-agents known to * mangle UTF-8 data on form submission. Returns true if Unicode * should make it through, false if it's known to be a problem. * @return bool */ private function checkUnicodeCompliantBrowser() { global $wgBrowserBlackList, $wgRequest; $currentbrowser = $wgRequest->getHeader( 'User-Agent' ); if ( $currentbrowser === false ) { // No User-Agent header sent? Trust it by default... return true; } foreach ( $wgBrowserBlackList as $browser ) { if ( preg_match( $browser, $currentbrowser ) ) { return false; } } return true; } /** * Filter an input field through a Unicode de-armoring process if it * came from an old browser with known broken Unicode editing issues. * * @param WebRequest $request * @param string $field * @return string */ protected function safeUnicodeInput( $request, $field ) { $text = rtrim( $request->getText( $field ) ); return $request->getBool( 'safemode' ) ? $this->unmakeSafe( $text ) : $text; } /** * Filter an output field through a Unicode armoring process if it is * going to an old browser with known broken Unicode editing issues. * * @param string $text * @return string */ protected function safeUnicodeOutput( $text ) { global $wgContLang; $codedText = $wgContLang->recodeForEdit( $text ); return $this->checkUnicodeCompliantBrowser() ? $codedText : $this->makeSafe( $codedText ); } /** * A number of web browsers are known to corrupt non-ASCII characters * in a UTF-8 text editing environment. To protect against this, * detected browsers will be served an armored version of the text, * with non-ASCII chars converted to numeric HTML character references. * * Preexisting such character references will have a 0 added to them * to ensure that round-trips do not alter the original data. * * @param string $invalue * @return string */ private function makeSafe( $invalue ) { // Armor existing references for reversibility. $invalue = strtr( $invalue, array( "&#x" => "�" ) ); $bytesleft = 0; $result = ""; $working = 0; $valueLength = strlen( $invalue ); for ( $i = 0; $i < $valueLength; $i++ ) { $bytevalue = ord( $invalue[$i] ); if ( $bytevalue <= 0x7F ) { // 0xxx xxxx $result .= chr( $bytevalue ); $bytesleft = 0; } elseif ( $bytevalue <= 0xBF ) { // 10xx xxxx $working = $working << 6; $working += ( $bytevalue & 0x3F ); $bytesleft--; if ( $bytesleft <= 0 ) { $result .= "&#x" . strtoupper( dechex( $working ) ) . ";"; } } elseif ( $bytevalue <= 0xDF ) { // 110x xxxx $working = $bytevalue & 0x1F; $bytesleft = 1; } elseif ( $bytevalue <= 0xEF ) { // 1110 xxxx $working = $bytevalue & 0x0F; $bytesleft = 2; } else { // 1111 0xxx $working = $bytevalue & 0x07; $bytesleft = 3; } } return $result; } /** * Reverse the previously applied transliteration of non-ASCII characters * back to UTF-8. Used to protect data from corruption by broken web browsers * as listed in $wgBrowserBlackList. * * @param string $invalue * @return string */ private function unmakeSafe( $invalue ) { $result = ""; $valueLength = strlen( $invalue ); for ( $i = 0; $i < $valueLength; $i++ ) { if ( ( substr( $invalue, $i, 3 ) == "&#x" ) && ( $invalue[$i + 3] != '0' ) ) { $i += 3; $hexstring = ""; do { $hexstring .= $invalue[$i]; $i++; } while ( ctype_xdigit( $invalue[$i] ) && ( $i < strlen( $invalue ) ) ); // Do some sanity checks. These aren't needed for reversibility, // but should help keep the breakage down if the editor // breaks one of the entities whilst editing. if ( ( substr( $invalue, $i, 1 ) == ";" ) && ( strlen( $hexstring ) <= 6 ) ) { $codepoint = hexdec( $hexstring ); $result .= UtfNormal\Utils::codepointToUtf8( $codepoint ); } else { $result .= "&#x" . $hexstring . substr( $invalue, $i, 1 ); } } else { $result .= substr( $invalue, $i, 1 ); } } // reverse the transform that we made for reversibility reasons. return strtr( $result, array( "�" => "&#x" ) ); } } ô¼=ffff8¥I–X checked if ( is_dir( $dir ) ) { return true; } // PHP doesn't report the path in its warning message, so add our own to aid in diagnosis. wfLogWarning( sprintf( "failed to mkdir \"%s\" mode 0%o", $dir, $mode ) ); } return $ok; } /** * Remove a directory and all its content. * Does not hide error. * @param string $dir */ function wfRecursiveRemoveDir( $dir ) { wfDebug( __FUNCTION__ . "( $dir )\n" ); // taken from http://de3.php.net/manual/en/function.rmdir.php#98622 if ( is_dir( $dir ) ) { $objects = scandir( $dir ); foreach ( $objects as $object ) { if ( $object != "." && $object != ".." ) { if ( filetype( $dir . '/' . $object ) == "dir" ) { wfRecursiveRemoveDir( $dir . '/' . $object ); } else { unlink( $dir . '/' . $object ); } } } reset( $objects ); rmdir( $dir ); } } /** * @param int $nr The number to format * @param int $acc The number of digits after the decimal point, default 2 * @param bool $round Whether or not to round the value, default true * @return string */ function wfPercent( $nr, $acc = 2, $round = true ) { $ret = sprintf( "%.${acc}f", $nr ); return $round ? round( $ret, $acc ) . '%' : "$ret%"; } /** * Safety wrapper around ini_get() for boolean settings. * The values returned from ini_get() are pre-normalized for settings * set via php.ini or php_flag/php_admin_flag... but *not* * for those set via php_value/php_admin_value. * * It's fairly common for people to use php_value instead of php_flag, * which can leave you with an 'off' setting giving a false positive * for code that just takes the ini_get() return value as a boolean. * * To make things extra interesting, setting via php_value accepts * "true" and "yes" as true, but php.ini and php_flag consider them false. :) * Unrecognized values go false... again opposite PHP's own coercion * from string to bool. * * Luckily, 'properly' set settings will always come back as '0' or '1', * so we only have to worry about them and the 'improper' settings. * * I frickin' hate PHP... :P * * @param string $setting * @return bool */ function wfIniGetBool( $setting ) { $val = strtolower( ini_get( $setting ) ); // 'on' and 'true' can't have whitespace around them, but '1' can. return $val == 'on' || $val == 'true' || $val == 'yes' || preg_match( "/^\s*[+-]?0*[1-9]/", $val ); // approx C atoi() function } /** * Windows-compatible version of escapeshellarg() * Windows doesn't recognise single-quotes in the shell, but the escapeshellarg() * function puts single quotes in regardless of OS. * * Also fixes the locale problems on Linux in PHP 5.2.6+ (bug backported to * earlier distro releases of PHP) * * @param string ... strings to escape and glue together, or a single array of strings parameter * @return string */ function wfEscapeShellArg( /*...*/ ) { wfInitShellLocale(); $args = func_get_args(); if ( count( $args ) === 1 && is_array( reset( $args ) ) ) { // If only one argument has been passed, and that argument is an array, // treat it as a list of arguments $args = reset( $args ); } $first = true; $retVal = ''; foreach ( $args as $arg ) { if ( !$first ) { $retVal .= ' '; } else { $first = false; } if ( wfIsWindows() ) { // Escaping for an MSVC-style command line parser and CMD.EXE // @codingStandardsIgnoreStart For long URLs // Refs: // * http://web.archive.org/web/20020708081031/http://mailman.lyra.org/pipermail/scite-interest/2002-March/000436.html // * http://technet.microsoft.com/en-us/library/cc723564.aspx // * Bug #13518 // * CR r63214 // Double the backslashes before any double quotes. Escape the double quotes. // @codingStandardsIgnoreEnd $tokens = preg_split( '/(\\\\*")/', $arg, -1, PREG_SPLIT_DELIM_CAPTURE ); $arg = ''; $iteration = 0; foreach ( $tokens as $token ) { if ( $iteration % 2 == 1 ) { // Delimiter, a double quote preceded by zero or more slashes $arg .= str_replace( '\\', '\\\\', substr( $token, 0, -1 ) ) . '\\"'; } elseif ( $iteration % 4 == 2 ) { // ^ in $token will be outside quotes, need to be escaped $arg .= str_replace( '^', '^^', $token ); } else { // $iteration % 4 == 0 // ^ in $token will appear inside double quotes, so leave as is $arg .= $token; } $iteration++; } // Double the backslashes before the end of the string, because // we will soon add a quote $m = array(); if ( preg_match( '/^(.*?)(\\\\+)$/', $arg, $m ) ) { $arg = $m[1] . str_replace( '\\', '\\\\', $m[2] ); } // Add surrounding quotes $retVal .= '"' . $arg . '"'; } else { $retVal .= escapeshellarg( $arg ); } } return $retVal; } /** * Check if wfShellExec() is effectively disabled via php.ini config * * @return bool|string False or one of (safemode,disabled) * @since 1.22 */ function wfShellExecDisabled() { static $disabled = null; if ( is_null( $disabled ) ) { if ( wfIniGetBool( 'safe_mode' ) ) { wfDebug( "wfShellExec can't run in safe_mode, PHP's exec functions are too broken.\n" ); $disabled = 'safemode'; } elseif ( !function_exists( 'proc_open' ) ) { wfDebug( "proc_open() is disabled\n" ); $disabled = 'disabled'; } else { $disabled = false; } } return $disabled; } /** * Execute a shell command, with time and memory limits mirrored from the PHP * configuration if supported. * * @param string|string[] $cmd If string, a properly shell-escaped command line, * or an array of unescaped arguments, in which case each value will be escaped * Example: [ 'convert', '-font', 'font name' ] would produce "'convert' '-font' 'font name'" * @param null|mixed &$retval Optional, will receive the program's exit code. * (non-zero is usually failure). If there is an error from * read, select, or proc_open(), this will be set to -1. * @param array $environ Optional environment variables which should be * added to the executed command environment. * @param array $limits Optional array with limits(filesize, memory, time, walltime) * this overwrites the global wgMaxShell* limits. * @param array $options Array of options: * - duplicateStderr: Set this to true to duplicate stderr to stdout, * including errors from limit.sh * - profileMethod: By default this function will profile based on the calling * method. Set this to a string for an alternative method to profile from * * @return string Collected stdout as a string */ function wfShellExec( $cmd, &$retval = null, $environ = array(), $limits = array(), $options = array() ) { global $IP, $wgMaxShellMemory, $wgMaxShellFileSize, $wgMaxShellTime, $wgMaxShellWallClockTime, $wgShellCgroup; $disabled = wfShellExecDisabled(); if ( $disabled ) { $retval = 1; return $disabled == 'safemode' ? 'Unable to run external programs in safe mode.' : 'Unable to run external programs, proc_open() is disabled.'; } $includeStderr = isset( $options['duplicateStderr'] ) && $options['duplicateStderr']; $profileMethod = isset( $options['profileMethod'] ) ? $options['profileMethod'] : wfGetCaller(); wfInitShellLocale(); $envcmd = ''; foreach ( $environ as $k => $v ) { if ( wfIsWindows() ) { /* Surrounding a set in quotes (method used by wfEscapeShellArg) makes the quotes themselves * appear in the environment variable, so we must use carat escaping as documented in * http://technet.microsoft.com/en-us/library/cc723564.aspx * Note however that the quote isn't listed there, but is needed, and the parentheses * are listed there but doesn't appear to need it. */ $envcmd .= "set $k=" . preg_replace( '/([&|()<>^"])/', '^\\1', $v ) . '&& '; } else { /* Assume this is a POSIX shell, thus required to accept variable assignments before the command * http://www.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_09_01 */ $envcmd .= "$k=" . escapeshellarg( $v ) . ' '; } } if ( is_array( $cmd ) ) { $cmd = wfEscapeShellArg( $cmd ); } $cmd = $envcmd . $cmd; $useLogPipe = false; if ( is_executable( '/bin/bash' ) ) { $time = intval ( isset( $limits['time'] ) ? $limits['time'] : $wgMaxShellTime ); if ( isset( $limits['walltime'] ) ) { $wallTime = intval( $limits['walltime'] ); } elseif ( isset( $limits['time'] ) ) { $wallTime = $time; } else { $wallTime = intval( $wgMaxShellWallClockTime ); } $mem = intval ( isset( $limits['memory'] ) ? $limits['memory'] : $wgMaxShellMemory ); $filesize = intval ( isset( $limits['filesize'] ) ? $limits['filesize'] : $wgMaxShellFileSize ); if ( $time > 0 || $mem > 0 || $filesize > 0 || $wallTime > 0 ) { $cmd = '/bin/bash ' . escapeshellarg( "$IP/includes/limit.sh" ) . ' ' . escapeshellarg( $cmd ) . ' ' . escapeshellarg( "MW_INCLUDE_STDERR=" . ( $includeStderr ? '1' : '' ) . ';' . "MW_CPU_LIMIT=$time; " . 'MW_CGROUP=' . escapeshellarg( $wgShellCgroup ) . '; ' . "MW_MEM_LIMIT=$mem; " . "MW_FILE_SIZE_LIMIT=$filesize; " . "MW_WALL_CLOCK_LIMIT=$wallTime; " . "MW_USE_LOG_PIPE=yes" ); $useLogPipe = true; } elseif ( $includeStderr ) { $cmd .= ' 2>&1'; } } elseif ( $includeStderr ) { $cmd .= ' 2>&1'; } wfDebug( "wfShellExec: $cmd\n" ); $desc = array( 0 => array( 'file', 'php://stdin', 'r' ), 1 => array( 'pipe', 'w' ), 2 => array( 'file', 'php://stderr', 'w' ) ); if ( $useLogPipe ) { $desc[3] = array( 'pipe', 'w' ); } $pipes = null; $scoped = Profiler::instance()->scopedProfileIn( __FUNCTION__ . '-' . $profileMethod ); $proc = proc_open( $cmd, $desc, $pipes ); if ( !$proc ) { wfDebugLog( 'exec', "proc_open() failed: $cmd" ); $retval = -1; return ''; } $outBuffer = $logBuffer = ''; $emptyArray = array(); $status = false; $logMsg = false; // According to the documentation, it is possible for stream_select() // to fail due to EINTR. I haven't managed to induce this in testing // despite sending various signals. If it did happen, the error // message would take the form: // // stream_select(): unable to select [4]: Interrupted system call (max_fd=5) // // where [4] is the value of the macro EINTR and "Interrupted system // call" is string which according to the Linux manual is "possibly" // localised according to LC_MESSAGES. $eintr = defined( 'SOCKET_EINTR' ) ? SOCKET_EINTR : 4; $eintrMessage = "stream_select(): unable to select [$eintr]"; // Build a table mapping resource IDs to pipe FDs to work around a // PHP 5.3 issue in which stream_select() does not preserve array keys // . $fds = array(); foreach ( $pipes as $fd => $pipe ) { $fds[(int)$pipe] = $fd; } $running = true; $timeout = null; $numReadyPipes = 0; while ( $running === true || $numReadyPipes !== 0 ) { if ( $running ) { $status = proc_get_status( $proc ); // If the process has terminated, switch to nonblocking selects // for getting any data still waiting to be read. if ( !$status['running'] ) { $running = false; $timeout = 0; } } $readyPipes = $pipes; // Clear last error // @codingStandardsIgnoreStart Generic.PHP.NoSilencedErrors.Discouraged @trigger_error( '' ); $numReadyPipes = @stream_select( $readyPipes, $emptyArray, $emptyArray, $timeout ); if ( $numReadyPipes === false ) { // @codingStandardsIgnoreEnd $error = error_get_last(); if ( strncmp( $error['message'], $eintrMessage, strlen( $eintrMessage ) ) == 0 ) { continue; } else { trigger_error( $error['message'], E_USER_WARNING ); $logMsg = $error['message']; break; } } foreach ( $readyPipes as $pipe ) { $block = fread( $pipe, 65536 ); $fd = $fds[(int)$pipe]; if ( $block === '' ) { // End of file fclose( $pipes[$fd] ); unset( $pipes[$fd] ); if ( !$pipes ) { break 2; } } elseif ( $block === false ) { // Read error $logMsg = "Error reading from pipe"; break 2; } elseif ( $fd == 1 ) { // From stdout $outBuffer .= $block; } elseif ( $fd == 3 ) { // From log FD $logBuffer .= $block; if ( strpos( $block, "\n" ) !== false ) { $lines = explode( "\n", $logBuffer ); $logBuffer = array_pop( $lines ); foreach ( $lines as $line ) { wfDebugLog( 'exec', $line ); } } } } } foreach ( $pipes as $pipe ) { fclose( $pipe ); } // Use the status previously collected if possible, since proc_get_status() // just calls waitpid() which will not return anything useful the second time. if ( $running ) { $status = proc_get_status( $proc ); } if ( $logMsg !== false ) { // Read/select error $retval = -1; proc_close( $proc ); } elseif ( $status['signaled'] ) { $logMsg = "Exited with signal {$status['termsig']}"; $retval = 128 + $status['termsig']; proc_close( $proc ); } else { if ( $status['running'] ) { $retval = proc_close( $proc ); } else { $retval = $status['exitcode']; proc_close( $proc ); } if ( $retval == 127 ) { $logMsg = "Possibly missing executable file"; } elseif ( $retval >= 129 && $retval <= 192 ) { $logMsg = "Probably exited with signal " . ( $retval - 128 ); } } if ( $logMsg !== false ) { wfDebugLog( 'exec', "$logMsg: $cmd" ); } return $outBuffer; } /** * Execute a shell command, returning both stdout and stderr. Convenience * function, as all the arguments to wfShellExec can become unwieldy. * * @note This also includes errors from limit.sh, e.g. if $wgMaxShellFileSize is exceeded. * @param string|string[] $cmd If string, a properly shell-escaped command line, * or an array of unescaped arguments, in which case each value will be escaped * Example: [ 'convert', '-font', 'font name' ] would produce "'convert' '-font' 'font name'" * @param null|mixed &$retval Optional, will receive the program's exit code. * (non-zero is usually failure) * @param array $environ Optional environment variables which should be * added to the executed command environment. * @param array $limits Optional array with limits(filesize, memory, time, walltime) * this overwrites the global wgMaxShell* limits. * @return string Collected stdout and stderr as a string */ function wfShellExecWithStderr( $cmd, &$retval = null, $environ = array(), $limits = array() ) { return wfShellExec( $cmd, $retval, $environ, $limits, array( 'duplicateStderr' => true, 'profileMethod' => wfGetCaller() ) ); } /** * Workaround for http://bugs.php.net/bug.php?id=45132 * escapeshellarg() destroys non-ASCII characters if LANG is not a UTF-8 locale */ function wfInitShellLocale() { static $done = false; if ( $done ) { return; } $done = true; global $wgShellLocale; if ( !wfIniGetBool( 'safe_mode' ) ) { putenv( "LC_CTYPE=$wgShellLocale" ); setlocale( LC_CTYPE, $wgShellLocale ); } } /** * Generate a shell-escaped command line string to run a MediaWiki cli script. * Note that $parameters should be a flat array and an option with an argument * should consist of two consecutive items in the array (do not use "--option value"). * * @param string $script MediaWiki cli script path * @param array $parameters Arguments and options to the script * @param array $options Associative array of options: * 'php': The path to the php executable * 'wrapper': Path to a PHP wrapper to handle the maintenance script * @return string */ function wfShellWikiCmd( $script, array $parameters = array(), array $options = array() ) { global $wgPhpCli; // Give site config file a chance to run the script in a wrapper. // The caller may likely want to call wfBasename() on $script. Hooks::run( 'wfShellWikiCmd', array( &$script, &$parameters, &$options ) ); $cmd = isset( $options['php'] ) ? array( $options['php'] ) : array( $wgPhpCli ); if ( isset( $options['wrapper'] ) ) { $cmd[] = $options['wrapper']; } $cmd[] = $script; // Escape each parameter for shell return wfEscapeShellArg( array_merge( $cmd, $parameters ) ); } /** * wfMerge attempts to merge differences between three texts. * Returns true for a clean merge and false for failure or a conflict. * * @param string $old * @param string $mine * @param string $yours * @param string $result * @return bool */ function wfMerge( $old, $mine, $yours, &$result ) { global $wgDiff3; # This check may also protect against code injection in # case of broken installations. wfSuppressWarnings(); $haveDiff3 = $wgDiff3 && file_exists( $wgDiff3 ); wfRestoreWarnings(); if ( !$haveDiff3 ) { wfDebug( "diff3 not found\n" ); return false; } # Make temporary files $td = wfTempDir(); $oldtextFile = fopen( $oldtextName = tempnam( $td, 'merge-old-' ), 'w' ); $mytextFile = fopen( $mytextName = tempnam( $td, 'merge-mine-' ), 'w' ); $yourtextFile = fopen( $yourtextName = tempnam( $td, 'merge-your-' ), 'w' ); # NOTE: diff3 issues a warning to stderr if any of the files does not end with # a newline character. To avoid this, we normalize the trailing whitespace before # creating the diff. fwrite( $oldtextFile, rtrim( $old ) . "\n" ); fclose( $oldtextFile ); fwrite( $mytextFile, rtrim( $mine ) . "\n" ); fclose( $mytextFile ); fwrite( $yourtextFile, rtrim( $yours ) . "\n" ); fclose( $yourtextFile ); # Check for a conflict $cmd = wfEscapeShellArg( $wgDiff3, '-a', '--overlap-only', $mytextName, $oldtextName, $yourtextName ); $handle = popen( $cmd, 'r' ); if ( fgets( $handle, 1024 ) ) { $conflict = true; } else { $conflict = false; } pclose( $handle ); # Merge differences $cmd = wfEscapeShellArg( $wgDiff3, '-a', '-e', '--merge', $mytextName, $oldtextName, $yourtextName ); $handle = popen( $cmd, 'r' ); $result = ''; do { $data = fread( $handle, 8192 ); if ( strlen( $data ) == 0 ) { break; } $result .= $data; } while ( true ); pclose( $handle ); unlink( $mytextName ); unlink( $oldtextName ); unlink( $yourtextName ); if ( $result === '' && $old !== '' && !$conflict ) { wfDebug( "Unexpected null result from diff3. Command: $cmd\n" ); $conflict = true; } return !$conflict; } /** * Returns unified plain-text diff of two texts. * "Useful" for machine processing of diffs. * * @deprecated since 1.25, use DiffEngine/UnifiedDiffFormatter directly * * @param string $before The text before the changes. * @param string $after The text after the changes. * @param string $params Command-line options for the diff command. * @return string Unified diff of $before and $after */ function wfDiff( $before, $after, $params = '-u' ) { if ( $before == $after ) { return ''; } global $wgDiff; wfSuppressWarnings(); $haveDiff = $wgDiff && file_exists( $wgDiff ); wfRestoreWarnings(); # This check may also protect against code injection in # case of broken installations. if ( !$haveDiff ) { wfDebug( "diff executable not found\n" ); $diffs = new Diff( explode( "\n", $before ), explode( "\n", $after ) ); $format = new UnifiedDiffFormatter(); return $format->format( $diffs ); } # Make temporary files $td = wfTempDir(); $oldtextFile = fopen( $oldtextName = tempnam( $td, 'merge-old-' ), 'w' ); $newtextFile = fopen( $newtextName = tempnam( $td, 'merge-your-' ), 'w' ); fwrite( $oldtextFile, $before ); fclose( $oldtextFile ); fwrite( $newtextFile, $after ); fclose( $newtextFile ); // Get the diff of the two files $cmd = "$wgDiff " . $params . ' ' . wfEscapeShellArg( $oldtextName, $newtextName ); $h = popen( $cmd, 'r' ); if ( !$h ) { unlink( $oldtextName ); unlink( $newtextName ); throw new Exception( __METHOD__ . '(): popen() failed' ); } $diff = ''; do { $data = fread( $h, 8192 ); if ( strlen( $data ) == 0 ) { break; } $diff .= $data; } while ( true ); // Clean up pclose( $h ); unlink( $oldtextName ); unlink( $newtextName ); // Kill the --- and +++ lines. They're not useful. $diff_lines = explode( "\n", $diff ); if ( isset( $diff_lines[0] ) && strpos( $diff_lines[0], '---' ) === 0 ) { unset( $diff_lines[0] ); } if ( isset( $diff_lines[1] ) && strpos( $diff_lines[1], '+++' ) === 0 ) { unset( $diff_lines[1] ); } $diff = implode( "\n", $diff_lines ); return $diff; } /** * This function works like "use VERSION" in Perl, the program will die with a * backtrace if the current version of PHP is less than the version provided * * This is useful for extensions which due to their nature are not kept in sync * with releases, and might depend on other versions of PHP than the main code * * Note: PHP might die due to parsing errors in some cases before it ever * manages to call this function, such is life * * @see perldoc -f use * * @param string|int|float $req_ver The version to check, can be a string, an integer, or a float * @throws MWException */ function wfUsePHP( $req_ver ) { $php_ver = PHP_VERSION; if ( version_compare( $php_ver, (string)$req_ver, '<' ) ) { throw new MWException( "PHP $req_ver required--this is only $php_ver" ); } } /** * This function works like "use VERSION" in Perl except it checks the version * of MediaWiki, the program will die with a backtrace if the current version * of MediaWiki is less than the version provided. * * This is useful for extensions which due to their nature are not kept in sync * with releases * * Note: Due to the behavior of PHP's version_compare() which is used in this * function, if you want to allow the 'wmf' development versions add a 'c' (or * any single letter other than 'a', 'b' or 'p') as a post-fix to your * targeted version number. For example if you wanted to allow any variation * of 1.22 use `wfUseMW( '1.22c' )`. Using an 'a' or 'b' instead of 'c' will * not result in the same comparison due to the internal logic of * version_compare(). * * @see perldoc -f use * * @param string|int|float $req_ver The version to check, can be a string, an integer, or a float * @throws MWException */ function wfUseMW( $req_ver ) { global $wgVersion; if ( version_compare( $wgVersion, (string)$req_ver, '<' ) ) { throw new MWException( "MediaWiki $req_ver required--this is only $wgVersion" ); } } /** * Return the final portion of a pathname. * Reimplemented because PHP5's "basename()" is buggy with multibyte text. * http://bugs.php.net/bug.php?id=33898 * * PHP's basename() only considers '\' a pathchar on Windows and Netware. * We'll consider it so always, as we don't want '\s' in our Unix paths either. * * @param string $path * @param string $suffix String to remove if present * @return string */ function wfBaseName( $path, $suffix = '' ) { if ( $suffix == '' ) { $encSuffix = ''; } else { $encSuffix = '(?:' . preg_quote( $suffix, '#' ) . ')?'; } $matches = array(); if ( preg_match( "#([^/\\\\]*?){$encSuffix}[/\\\\]*$#", $path, $matches ) ) { return $matches[1]; } else { return ''; } } /** * Generate a relative path name to the given file. * May explode on non-matching case-insensitive paths, * funky symlinks, etc. * * @param string $path Absolute destination path including target filename * @param string $from Absolute source path, directory only * @return string */ function wfRelativePath( $path, $from ) { // Normalize mixed input on Windows... $path = str_replace( '/', DIRECTORY_SEPARATOR, $path ); $from = str_replace( '/', DIRECTORY_SEPARATOR, $from ); // Trim trailing slashes -- fix for drive root $path = rtrim( $path, DIRECTORY_SEPARATOR ); $from = rtrim( $from, DIRECTORY_SEPARATOR ); $pieces = explode( DIRECTORY_SEPARATOR, dirname( $path ) ); $against = explode( DIRECTORY_SEPARATOR, $from ); if ( $pieces[0] !== $against[0] ) { // Non-matching Windows drive letters? // Return a full path. return $path; } // Trim off common prefix while ( count( $pieces ) && count( $against ) && $pieces[0] == $against[0] ) { array_shift( $pieces ); array_shift( $against ); } // relative dots to bump us to the parent while ( count( $against ) ) { array_unshift( $pieces, '..' ); array_shift( $against ); } array_push( $pieces, wfBaseName( $path ) ); return implode( DIRECTORY_SEPARATOR, $pieces ); } /** * Convert an arbitrarily-long digit string from one numeric base * to another, optionally zero-padding to a minimum column width. * * Supports base 2 through 36; digit values 10-36 are represented * as lowercase letters a-z. Input is case-insensitive. * * @param string $input Input number * @param int $sourceBase Base of the input number * @param int $destBase Desired base of the output * @param int $pad Minimum number of digits in the output (pad with zeroes) * @param bool $lowercase Whether to output in lowercase or uppercase * @param string $engine Either "gmp", "bcmath", or "php" * @return string|bool The output number as a string, or false on error */ function wfBaseConvert( $input, $sourceBase, $destBase, $pad = 1, $lowercase = true, $engine = 'auto' ) { $input = (string)$input; if ( $sourceBase < 2 || $sourceBase > 36 || $destBase < 2 || $destBase > 36 || $sourceBase != (int)$sourceBase || $destBase != (int)$destBase || $pad != (int)$pad || !preg_match( "/^[" . substr( '0123456789abcdefghijklmnopqrstuvwxyz', 0, $sourceBase ) . "]+$/i", $input ) ) { return false; } static $baseChars = array( 10 => 'a', 11 => 'b', 12 => 'c', 13 => 'd', 14 => 'e', 15 => 'f', 16 => 'g', 17 => 'h', 18 => 'i', 19 => 'j', 20 => 'k', 21 => 'l', 22 => 'm', 23 => 'n', 24 => 'o', 25 => 'p', 26 => 'q', 27 => 'r', 28 => 's', 29 => 't', 30 => 'u', 31 => 'v', 32 => 'w', 33 => 'x', 34 => 'y', 35 => 'z', '0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4, '5' => 5, '6' => 6, '7' => 7, '8' => 8, '9' => 9, 'a' => 10, 'b' => 11, 'c' => 12, 'd' => 13, 'e' => 14, 'f' => 15, 'g' => 16, 'h' => 17, 'i' => 18, 'j' => 19, 'k' => 20, 'l' => 21, 'm' => 22, 'n' => 23, 'o' => 24, 'p' => 25, 'q' => 26, 'r' => 27, 's' => 28, 't' => 29, 'u' => 30, 'v' => 31, 'w' => 32, 'x' => 33, 'y' => 34, 'z' => 35 ); if ( extension_loaded( 'gmp' ) && ( $engine == 'auto' || $engine == 'gmp' ) ) { // Removing leading zeros works around broken base detection code in // some PHP versions (see and // ). $result = gmp_strval( gmp_init( ltrim( $input, '0' ) ?: '0', $sourceBase ), $destBase ); } elseif ( extension_loaded( 'bcmath' ) && ( $engine == 'auto' || $engine == 'bcmath' ) ) { $decimal = '0'; foreach ( str_split( strtolower( $input ) ) as $char ) { $decimal = bcmul( $decimal, $sourceBase ); $decimal = bcadd( $decimal, $baseChars[$char] ); } // @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed for ( $result = ''; bccomp( $decimal, 0 ); $decimal = bcdiv( $decimal, $destBase, 0 ) ) { $result .= $baseChars[bcmod( $decimal, $destBase )]; } // @codingStandardsIgnoreEnd $result = strrev( $result ); } else { $inDigits = array(); foreach ( str_split( strtolower( $input ) ) as $char ) { $inDigits[] = $baseChars[$char]; } // Iterate over the input, modulo-ing out an output digit // at a time until input is gone. $result = ''; while ( $inDigits ) { $work = 0; $workDigits = array(); // Long division... foreach ( $inDigits as $digit ) { $work *= $sourceBase; $work += $digit; if ( $workDigits || $work >= $destBase ) { $workDigits[] = (int)( $work / $destBase ); } $work %= $destBase; } // All that division leaves us with a remainder, // which is conveniently our next output digit. $result .= $baseChars[$work]; // And we continue! $inDigits = $workDigits; } $result = strrev( $result ); } if ( !$lowercase ) { $result = strtoupper( $result ); } return str_pad( $result, $pad, '0', STR_PAD_LEFT ); } /** * Check if there is sufficient entropy in php's built-in session generation * * @return bool True = there is sufficient entropy */ function wfCheckEntropy() { return ( ( wfIsWindows() && version_compare( PHP_VERSION, '5.3.3', '>=' ) ) || ini_get( 'session.entropy_file' ) ) && intval( ini_get( 'session.entropy_length' ) ) >= 32; } /** * Override session_id before session startup if php's built-in * session generation code is not secure. */ function wfFixSessionID() { // If the cookie or session id is already set we already have a session and should abort if ( isset( $_COOKIE[session_name()] ) || session_id() ) { return; } // PHP's built-in session entropy is enabled if: // - entropy_file is set or you're on Windows with php 5.3.3+ // - AND entropy_length is > 0 // We treat it as disabled if it doesn't have an entropy length of at least 32 $entropyEnabled = wfCheckEntropy(); // If built-in entropy is not enabled or not sufficient override PHP's // built in session id generation code if ( !$entropyEnabled ) { wfDebug( __METHOD__ . ": PHP's built in entropy is disabled or not sufficient, " . "overriding session id generation using our cryptrand source.\n" ); session_id( MWCryptRand::generateHex( 32 ) ); } } /** * Reset the session_id * * @since 1.22 */ function wfResetSessionID() { global $wgCookieSecure; $oldSessionId = session_id(); $cookieParams = session_get_cookie_params(); if ( wfCheckEntropy() && $wgCookieSecure == $cookieParams['secure'] ) { session_regenerate_id( false ); } else { $tmp = $_SESSION; session_destroy(); wfSetupSession( MWCryptRand::generateHex( 32 ) ); $_SESSION = $tmp; } $newSessionId = session_id(); Hooks::run( 'ResetSessionID', array( $oldSessionId, $newSessionId ) ); } /** * Initialise php session * * @param bool $sessionId */ function wfSetupSession( $sessionId = false ) { global $wgSessionsInMemcached, $wgSessionsInObjectCache, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookieHttpOnly, $wgSessionHandler; if ( $wgSessionsInObjectCache || $wgSessionsInMemcached ) { ObjectCacheSessionHandler::install(); } elseif ( $wgSessionHandler && $wgSessionHandler != ini_get( 'session.save_handler' ) ) { # Only set this if $wgSessionHandler isn't null and session.save_handler # hasn't already been set to the desired value (that causes errors) ini_set( 'session.save_handler', $wgSessionHandler ); } session_set_cookie_params( 0, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookieHttpOnly ); session_cache_limiter( 'private, must-revalidate' ); if ( $sessionId ) { session_id( $sessionId ); } else { wfFixSessionID(); } wfSuppressWarnings(); session_start(); wfRestoreWarnings(); } /** * Get an object from the precompiled serialized directory * * @param string $name * @return mixed The variable on success, false on failure */ function wfGetPrecompiledData( $name ) { global $IP; $file = "$IP/serialized/$name"; if ( file_exists( $file ) ) { $blob = file_get_contents( $file ); if ( $blob ) { return unserialize( $blob ); } } return false; } /** * Get a cache key * * @param string $args,... * @return string */ function wfMemcKey( /*...*/ ) { global $wgCachePrefix; $prefix = $wgCachePrefix === false ? wfWikiID() : $wgCachePrefix; $args = func_get_args(); $key = $prefix . ':' . implode( ':', $args ); $key = str_replace( ' ', '_', $key ); return $key; } /** * Get a cache key for a foreign DB * * @param string $db * @param string $prefix * @param string $args,... * @return string */ function wfForeignMemcKey( $db, $prefix /*...*/ ) { $args = array_slice( func_get_args(), 2 ); if ( $prefix ) { $key = "$db-$prefix:" . implode( ':', $args ); } else { $key = $db . ':' . implode( ':', $args ); } return str_replace( ' ', '_', $key ); } /** * Get an ASCII string identifying this wiki * This is used as a prefix in memcached keys * * @return string */ function wfWikiID() { global $wgDBprefix, $wgDBname; if ( $wgDBprefix ) { return "$wgDBname-$wgDBprefix"; } else { return $wgDBname; } } /** * Split a wiki ID into DB name and table prefix * * @param string $wiki * * @return array */ function wfSplitWikiID( $wiki ) { $bits = explode( '-', $wiki, 2 ); if ( count( $bits ) < 2 ) { $bits[] = ''; } return $bits; } /** * Get a Database object. * * @param int $db Index of the connection to get. May be DB_MASTER for the * master (for write queries), DB_SLAVE for potentially lagged read * queries, or an integer >= 0 for a particular server. * * @param string|string[] $groups Query groups. An array of group names that this query * belongs to. May contain a single string if the query is only * in one group. * * @param string|bool $wiki The wiki ID, or false for the current wiki * * Note: multiple calls to wfGetDB(DB_SLAVE) during the course of one request * will always return the same object, unless the underlying connection or load * balancer is manually destroyed. * * Note 2: use $this->getDB() in maintenance scripts that may be invoked by * updater to ensure that a proper database is being updated. * * @return DatabaseBase */ function wfGetDB( $db, $groups = array(), $wiki = false ) { return wfGetLB( $wiki )->getConnection( $db, $groups, $wiki ); } /** * Get a load balancer object. * * @param string|bool $wiki Wiki ID, or false for the current wiki * @return LoadBalancer */ function wfGetLB( $wiki = false ) { return wfGetLBFactory()->getMainLB( $wiki ); } /** * Get the load balancer factory object * * @return LBFactory */ function wfGetLBFactory() { return LBFactory::singleton(); } /** * Find a file. * Shortcut for RepoGroup::singleton()->findFile() * * @param string $title String or Title object * @param array $options Associative array of options (see RepoGroup::findFile) * @return File|bool File, or false if the file does not exist */ function wfFindFile( $title, $options = array() ) { return RepoGroup::singleton()->findFile( $title, $options ); } /** * Get an object referring to a locally registered file. * Returns a valid placeholder object if the file does not exist. * * @param Title|string $title * @return LocalFile|null A File, or null if passed an invalid Title */ function wfLocalFile( $title ) { return RepoGroup::singleton()->getLocalRepo()->newFile( $title ); } /** * Should low-performance queries be disabled? * * @return bool * @codeCoverageIgnore */ function wfQueriesMustScale() { global $wgMiserMode; return $wgMiserMode || ( SiteStats::pages() > 100000 && SiteStats::edits() > 1000000 && SiteStats::users() > 10000 ); } /** * Get the path to a specified script file, respecting file * extensions; this is a wrapper around $wgScriptExtension etc. * except for 'index' and 'load' which use $wgScript/$wgLoadScript * * @param string $script Script filename, sans extension * @return string */ function wfScript( $script = 'index' ) { global $wgScriptPath, $wgScriptExtension, $wgScript, $wgLoadScript; if ( $script === 'index' ) { return $wgScript; } elseif ( $script === 'load' ) { return $wgLoadScript; } else { return "{$wgScriptPath}/{$script}{$wgScriptExtension}"; } } /** * Get the script URL. * * @return string Script URL */ function wfGetScriptUrl() { if ( isset( $_SERVER['SCRIPT_NAME'] ) ) { # # as it was called, minus the query string. # # Some sites use Apache rewrite rules to handle subdomains, # and have PHP set up in a weird way that causes PHP_SELF # to contain the rewritten URL instead of the one that the # outside world sees. # # If in this mode, use SCRIPT_URL instead, which mod_rewrite # provides containing the "before" URL. return $_SERVER['SCRIPT_NAME']; } else { return $_SERVER['URL']; } } /** * Convenience function converts boolean values into "true" * or "false" (string) values * * @param bool $value * @return string */ function wfBoolToStr( $value ) { return $value ? 'true' : 'false'; } /** * Get a platform-independent path to the null file, e.g. /dev/null * * @return string */ function wfGetNull() { return wfIsWindows() ? 'NUL' : '/dev/null'; } /** * Waits for the slaves to catch up to the master position * * Use this when updating very large numbers of rows, as in maintenance scripts, * to avoid causing too much lag. Of course, this is a no-op if there are no slaves. * * By default this waits on the main DB cluster of the current wiki. * If $cluster is set to "*" it will wait on all DB clusters, including * external ones. If the lag being waiting on is caused by the code that * does this check, it makes since to use $ifWritesSince, particularly if * cluster is "*", to avoid excess overhead. * * Never call this function after a big DB write that is still in a transaction. * This only makes sense after the possible lag inducing changes were committed. * * @param float|null $ifWritesSince Only wait if writes were done since this UNIX timestamp * @param string|bool $wiki Wiki identifier accepted by wfGetLB * @param string|bool $cluster Cluster name accepted by LBFactory. Default: false. * @param int|null $timeout Max wait time. Default: 1 day (cli), ~10 seconds (web) * @return bool Success (able to connect and no timeouts reached) */ function wfWaitForSlaves( $ifWritesSince = null, $wiki = false, $cluster = false, $timeout = null ) { // B/C: first argument used to be "max seconds of lag"; ignore such values $ifWritesSince = ( $ifWritesSince > 1e9 ) ? $ifWritesSince : null; if ( $timeout === null ) { $timeout = ( PHP_SAPI === 'cli' ) ? 86400 : 10; } // Figure out which clusters need to be checked $lbs = array(); if ( $cluster === '*' ) { wfGetLBFactory()->forEachLB( function ( LoadBalancer $lb ) use ( &$lbs ) { $lbs[] = $lb; } ); } elseif ( $cluster !== false ) { $lbs[] = wfGetLBFactory()->getExternalLB( $cluster ); } else { $lbs[] = wfGetLB( $wiki ); } // Get all the master positions of applicable DBs right now. // This can be faster since waiting on one cluster reduces the // time needed to wait on the next clusters. $masterPositions = array_fill( 0, count( $lbs ), false ); foreach ( $lbs as $i => $lb ) { // bug 27975 - Don't try to wait for slaves if there are none // Prevents permission error when getting master position if ( $lb->getServerCount() > 1 ) { if ( $ifWritesSince && !$lb->hasMasterConnection() ) { continue; // assume no writes done } // Use the empty string to not trigger selectDB() since the connection // may have been to a server that does not have a DB for the current wiki. $dbw = $lb->getConnection( DB_MASTER, array(), '' ); if ( $ifWritesSince && $dbw->lastDoneWrites() < $ifWritesSince ) { continue; // no writes since the last wait } $masterPositions[$i] = $dbw->getMasterPos(); } } $ok = true; foreach ( $lbs as $i => $lb ) { if ( $masterPositions[$i] ) { // The DBMS may not support getMasterPos() or the whole // load balancer might be fake (e.g. $wgAllDBsAreLocalhost). $ok = $lb->waitForAll( $masterPositions[$i], $timeout ) && $ok; } } return $ok; } /** * Count down from $seconds to zero on the terminal, with a one-second pause * between showing each number. For use in command-line scripts. * * @codeCoverageIgnore * @param int $seconds */ function wfCountDown( $seconds ) { for ( $i = $seconds; $i >= 0; $i-- ) { if ( $i != $seconds ) { echo str_repeat( "\x08", strlen( $i + 1 ) ); } echo $i; flush(); if ( $i ) { sleep( 1 ); } } echo "\n"; } /** * Replace all invalid characters with - * Additional characters can be defined in $wgIllegalFileChars (see bug 20489) * By default, $wgIllegalFileChars = ':' * * @param string $name Filename to process * @return string */ function wfStripIllegalFilenameChars( $name ) { global $wgIllegalFileChars; $illegalFileChars = $wgIllegalFileChars ? "|[" . $wgIllegalFileChars . "]" : ''; $name = wfBaseName( $name ); $name = preg_replace( "/[^" . Title::legalChars() . "]" . $illegalFileChars . "/", '-', $name ); return $name; } /** * Set PHP's memory limit to the larger of php.ini or $wgMemoryLimit; * * @return int Value the memory limit was set to. */ function wfMemoryLimit() { global $wgMemoryLimit; $memlimit = wfShorthandToInteger( ini_get( 'memory_limit' ) ); if ( $memlimit != -1 ) { $conflimit = wfShorthandToInteger( $wgMemoryLimit ); if ( $conflimit == -1 ) { wfDebug( "Removing PHP's memory limit\n" ); wfSuppressWarnings(); ini_set( 'memory_limit', $conflimit ); wfRestoreWarnings(); return $conflimit; } elseif ( $conflimit > $memlimit ) { wfDebug( "Raising PHP's memory limit to $conflimit bytes\n" ); wfSuppressWarnings(); ini_set( 'memory_limit', $conflimit ); wfRestoreWarnings(); return $conflimit; } } return $memlimit; } /** * Converts shorthand byte notation to integer form * * @param string $string * @return int */ function wfShorthandToInteger( $string = '' ) { $string = trim( $string ); if ( $string === '' ) { return -1; } $last = $string[strlen( $string ) - 1]; $val = intval( $string ); switch ( $last ) { case 'g': case 'G': $val *= 1024; // break intentionally missing case 'm': case 'M': $val *= 1024; // break intentionally missing case 'k': case 'K': $val *= 1024; } return $val; } /** * Get the normalised IETF language tag * See unit test for examples. * * @param string $code The language code. * @return string The language code which complying with BCP 47 standards. */ function wfBCP47( $code ) { $codeSegment = explode( '-', $code ); $codeBCP = array(); foreach ( $codeSegment as $segNo => $seg ) { // when previous segment is x, it is a private segment and should be lc if ( $segNo > 0 && strtolower( $codeSegment[( $segNo - 1 )] ) == 'x' ) { $codeBCP[$segNo] = strtolower( $seg ); // ISO 3166 country code } elseif ( ( strlen( $seg ) == 2 ) && ( $segNo > 0 ) ) { $codeBCP[$segNo] = strtoupper( $seg ); // ISO 15924 script code } elseif ( ( strlen( $seg ) == 4 ) && ( $segNo > 0 ) ) { $codeBCP[$segNo] = ucfirst( strtolower( $seg ) ); // Use lowercase for other cases } else { $codeBCP[$segNo] = strtolower( $seg ); } } $langCode = implode( '-', $codeBCP ); return $langCode; } /** * Get a cache object. * * @param int $inputType Cache type, one of the CACHE_* constants. * @return BagOStuff */ function wfGetCache( $inputType ) { return ObjectCache::getInstance( $inputType ); } /** * Get the main cache object * * @return BagOStuff */ function wfGetMainCache() { global $wgMainCacheType; return ObjectCache::getInstance( $wgMainCacheType ); } /** * Get the cache object used by the message cache * * @return BagOStuff */ function wfGetMessageCacheStorage() { global $wgMessageCacheType; return ObjectCache::getInstance( $wgMessageCacheType ); } /** * Get the cache object used by the parser cache * * @return BagOStuff */ function wfGetParserCacheStorage() { global $wgParserCacheType; return ObjectCache::getInstance( $wgParserCacheType ); } /** * Call hook functions defined in $wgHooks * * @param string $event Event name * @param array $args Parameters passed to hook functions * @param string|null $deprecatedVersion Optionally mark hook as deprecated with version number * * @return bool True if no handler aborted the hook * @deprecated 1.25 - use Hooks::run */ function wfRunHooks( $event, array $args = array(), $deprecatedVersion = null ) { return Hooks::run( $event, $args, $deprecatedVersion ); } /** * Wrapper around php's unpack. * * @param string $format The format string (See php's docs) * @param string $data A binary string of binary data * @param int|bool $length The minimum length of $data or false. This is to * prevent reading beyond the end of $data. false to disable the check. * * Also be careful when using this function to read unsigned 32 bit integer * because php might make it negative. * * @throws MWException If $data not long enough, or if unpack fails * @return array Associative array of the extracted data */ function wfUnpack( $format, $data, $length = false ) { if ( $length !== false ) { $realLen = strlen( $data ); if ( $realLen < $length ) { throw new MWException( "Tried to use wfUnpack on a " . "string of length $realLen, but needed one " . "of at least length $length." ); } } wfSuppressWarnings(); $result = unpack( $format, $data ); wfRestoreWarnings(); if ( $result === false ) { // If it cannot extract the packed data. throw new MWException( "unpack could not unpack binary data" ); } return $result; } /** * Determine if an image exists on the 'bad image list'. * * The format of MediaWiki:Bad_image_list is as follows: * * Only list items (lines starting with "*") are considered * * The first link on a line must be a link to a bad image * * Any subsequent links on the same line are considered to be exceptions, * i.e. articles where the image may occur inline. * * @param string $name The image name to check * @param Title|bool $contextTitle The page on which the image occurs, if known * @param string $blacklist Wikitext of a file blacklist * @return bool */ function wfIsBadImage( $name, $contextTitle = false, $blacklist = null ) { static $badImageCache = null; // based on bad_image_list msg # Handle redirects $redirectTitle = RepoGroup::singleton()->checkRedirect( Title::makeTitle( NS_FILE, $name ) ); if ( $redirectTitle ) { $name = $redirectTitle->getDBkey(); } # Run the extension hook $bad = false; if ( !Hooks::run( 'BadImage', array( $name, &$bad ) ) ) { return $bad; } $cacheable = ( $blacklist === null ); if ( $cacheable && $badImageCache !== null ) { $badImages = $badImageCache; } else { // cache miss if ( $blacklist === null ) { $blacklist = wfMessage( 'bad_image_list' )->inContentLanguage()->plain(); // site list } # Build the list now $badImages = array(); $lines = explode( "\n", $blacklist ); foreach ( $lines as $line ) { # List items only if ( substr( $line, 0, 1 ) !== '*' ) { continue; } # Find all links $m = array(); if ( !preg_match_all( '/\[\[:?(.*?)\]\]/', $line, $m ) ) { continue; } $exceptions = array(); $imageDBkey = false; foreach ( $m[1] as $i => $titleText ) { $title = Title::newFromText( $titleText ); if ( !is_null( $title ) ) { if ( $i == 0 ) { $imageDBkey = $title->getDBkey(); } else { $exceptions[$title->getPrefixedDBkey()] = true; } } } if ( $imageDBkey !== false ) { $badImages[$imageDBkey] = $exceptions; } } if ( $cacheable ) { $badImageCache = $badImages; } } $contextKey = $contextTitle ? $contextTitle->getPrefixedDBkey() : false; $bad = isset( $badImages[$name] ) && !isset( $badImages[$name][$contextKey] ); return $bad; } /** * Determine whether the client at a given source IP is likely to be able to * access the wiki via HTTPS. * * @param string $ip The IPv4/6 address in the normal human-readable form * @return bool */ function wfCanIPUseHTTPS( $ip ) { $canDo = true; Hooks::run( 'CanIPUseHTTPS', array( $ip, &$canDo ) ); return !!$canDo; } /** * Determine input string is represents as infinity * * @param string $str The string to determine * @return bool * @since 1.25 */ function wfIsInfinity( $str ) { $infinityValues = array( 'infinite', 'indefinite', 'infinity', 'never' ); return in_array( $str, $infinityValues ); } /** * Work out the IP address based on various globals * For trusted proxies, use the XFF client IP (first of the chain) * * @deprecated since 1.19; call $wgRequest->getIP() directly. * @return string */ function wfGetIP() { wfDeprecated( __METHOD__, '1.19' ); global $wgRequest; return $wgRequest->getIP(); } /** * Checks if an IP is a trusted proxy provider. * Useful to tell if X-Forwarded-For data is possibly bogus. * Squid cache servers for the site are whitelisted. * @deprecated Since 1.24, use IP::isTrustedProxy() * * @param string $ip * @return bool */ function wfIsTrustedProxy( $ip ) { wfDeprecated( __METHOD__, '1.24' ); return IP::isTrustedProxy( $ip ); } /** * Checks if an IP matches a proxy we've configured. * @deprecated Since 1.24, use IP::isConfiguredProxy() * * @param string $ip * @return bool * @since 1.23 Supports CIDR ranges in $wgSquidServersNoPurge */ function wfIsConfiguredProxy( $ip ) { wfDeprecated( __METHOD__, '1.24' ); return IP::isConfiguredProxy( $ip ); } /** * Returns true if these thumbnail parameters match one that MediaWiki * requests from file description pages and/or parser output. * * $params is considered non-standard if they involve a non-standard * width or any non-default parameters aside from width and page number. * The number of possible files with standard parameters is far less than * that of all combinations; rate-limiting for them can thus be more generious. * * @param File $file * @param array $params * @return bool * @since 1.24 Moved from thumb.php to GlobalFunctions in 1.25 */ function wfThumbIsStandard( File $file, array $params ) { global $wgThumbLimits, $wgImageLimits, $wgResponsiveImages; $multipliers = array( 1 ); if ( $wgResponsiveImages ) { // These available sizes are hardcoded currently elsewhere in MediaWiki. // @see Linker::processResponsiveImages $multipliers[] = 1.5; $multipliers[] = 2; } $handler = $file->getHandler(); if ( !$handler || !isset( $params['width'] ) ) { return false; } $basicParams = array(); if ( isset( $params['page'] ) ) { $basicParams['page'] = $params['page']; } $thumbLimits = array(); $imageLimits = array(); // Expand limits to account for multipliers foreach ( $multipliers as $multiplier ) { $thumbLimits = array_merge( $thumbLimits, array_map( function ( $width ) use ( $multiplier ) { return round( $width * $multiplier ); }, $wgThumbLimits ) ); $imageLimits = array_merge( $imageLimits, array_map( function ( $pair ) use ( $multiplier ) { return array( round( $pair[0] * $multiplier ), round( $pair[1] * $multiplier ), ); }, $wgImageLimits ) ); } // Check if the width matches one of $wgThumbLimits if ( in_array( $params['width'], $thumbLimits ) ) { $normalParams = $basicParams + array( 'width' => $params['width'] ); // Append any default values to the map (e.g. "lossy", "lossless", ...) $handler->normaliseParams( $file, $normalParams ); } else { // If not, then check if the width matchs one of $wgImageLimits $match = false; foreach ( $imageLimits as $pair ) { $normalParams = $basicParams + array( 'width' => $pair[0], 'height' => $pair[1] ); // Decide whether the thumbnail should be scaled on width or height. // Also append any default values to the map (e.g. "lossy", "lossless", ...) $handler->normaliseParams( $file, $normalParams ); // Check if this standard thumbnail size maps to the given width if ( $normalParams['width'] == $params['width'] ) { $match = true; break; } } if ( !$match ) { return false; // not standard for description pages } } // Check that the given values for non-page, non-width, params are just defaults foreach ( $params as $key => $value ) { if ( !isset( $normalParams[$key] ) || $normalParams[$key] != $value ) { return false; } } return true; }
Fatal error: Call to undefined function wfLoadSkin() in C:\inetpub\wwwroot\DevZone\evolutionsdk\wiki\LocalSettings.php on line 126