[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

/includes/ -> EditPage.php (source)

   1  <?php
   2  /**
   3   * User interface for page editing.
   4   *
   5   * This program is free software; you can redistribute it and/or modify
   6   * it under the terms of the GNU General Public License as published by
   7   * the Free Software Foundation; either version 2 of the License, or
   8   * (at your option) any later version.
   9   *
  10   * This program is distributed in the hope that it will be useful,
  11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13   * GNU General Public License for more details.
  14   *
  15   * You should have received a copy of the GNU General Public License along
  16   * with this program; if not, write to the Free Software Foundation, Inc.,
  17   * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18   * http://www.gnu.org/copyleft/gpl.html
  19   *
  20   * @file
  21   */
  22  
  23  /**
  24   * The edit page/HTML interface (split from Article)
  25   * The actual database and text munging is still in Article,
  26   * but it should get easier to call those from alternate
  27   * interfaces.
  28   *
  29   * EditPage cares about two distinct titles:
  30   * $this->mContextTitle is the page that forms submit to, links point to,
  31   * redirects go to, etc. $this->mTitle (as well as $mArticle) is the
  32   * page in the database that is actually being edited. These are
  33   * usually the same, but they are now allowed to be different.
  34   *
  35   * Surgeon General's Warning: prolonged exposure to this class is known to cause
  36   * headaches, which may be fatal.
  37   */
  38  class EditPage {
  39      /**
  40       * Status: Article successfully updated
  41       */
  42      const AS_SUCCESS_UPDATE = 200;
  43  
  44      /**
  45       * Status: Article successfully created
  46       */
  47      const AS_SUCCESS_NEW_ARTICLE = 201;
  48  
  49      /**
  50       * Status: Article update aborted by a hook function
  51       */
  52      const AS_HOOK_ERROR = 210;
  53  
  54      /**
  55       * Status: A hook function returned an error
  56       */
  57      const AS_HOOK_ERROR_EXPECTED = 212;
  58  
  59      /**
  60       * Status: User is blocked from editing this page
  61       */
  62      const AS_BLOCKED_PAGE_FOR_USER = 215;
  63  
  64      /**
  65       * Status: Content too big (> $wgMaxArticleSize)
  66       */
  67      const AS_CONTENT_TOO_BIG = 216;
  68  
  69      /**
  70       * Status: this anonymous user is not allowed to edit this page
  71       */
  72      const AS_READ_ONLY_PAGE_ANON = 218;
  73  
  74      /**
  75       * Status: this logged in user is not allowed to edit this page
  76       */
  77      const AS_READ_ONLY_PAGE_LOGGED = 219;
  78  
  79      /**
  80       * Status: wiki is in readonly mode (wfReadOnly() == true)
  81       */
  82      const AS_READ_ONLY_PAGE = 220;
  83  
  84      /**
  85       * Status: rate limiter for action 'edit' was tripped
  86       */
  87      const AS_RATE_LIMITED = 221;
  88  
  89      /**
  90       * Status: article was deleted while editing and param wpRecreate == false or form
  91       * was not posted
  92       */
  93      const AS_ARTICLE_WAS_DELETED = 222;
  94  
  95      /**
  96       * Status: user tried to create this page, but is not allowed to do that
  97       * ( Title->userCan('create') == false )
  98       */
  99      const AS_NO_CREATE_PERMISSION = 223;
 100  
 101      /**
 102       * Status: user tried to create a blank page and wpIgnoreBlankArticle == false
 103       */
 104      const AS_BLANK_ARTICLE = 224;
 105  
 106      /**
 107       * Status: (non-resolvable) edit conflict
 108       */
 109      const AS_CONFLICT_DETECTED = 225;
 110  
 111      /**
 112       * Status: no edit summary given and the user has forceeditsummary set and the user is not
 113       * editing in his own userspace or talkspace and wpIgnoreBlankSummary == false
 114       */
 115      const AS_SUMMARY_NEEDED = 226;
 116  
 117      /**
 118       * Status: user tried to create a new section without content
 119       */
 120      const AS_TEXTBOX_EMPTY = 228;
 121  
 122      /**
 123       * Status: article is too big (> $wgMaxArticleSize), after merging in the new section
 124       */
 125      const AS_MAX_ARTICLE_SIZE_EXCEEDED = 229;
 126  
 127      /**
 128       * Status: WikiPage::doEdit() was unsuccessful
 129       */
 130      const AS_END = 231;
 131  
 132      /**
 133       * Status: summary contained spam according to one of the regexes in $wgSummarySpamRegex
 134       */
 135      const AS_SPAM_ERROR = 232;
 136  
 137      /**
 138       * Status: anonymous user is not allowed to upload (User::isAllowed('upload') == false)
 139       */
 140      const AS_IMAGE_REDIRECT_ANON = 233;
 141  
 142      /**
 143       * Status: logged in user is not allowed to upload (User::isAllowed('upload') == false)
 144       */
 145      const AS_IMAGE_REDIRECT_LOGGED = 234;
 146  
 147      /**
 148       * Status: user tried to modify the content model, but is not allowed to do that
 149       * ( User::isAllowed('editcontentmodel') == false )
 150       */
 151      const AS_NO_CHANGE_CONTENT_MODEL = 235;
 152  
 153      /**
 154       * Status: can't parse content
 155       */
 156      const AS_PARSE_ERROR = 240;
 157  
 158      /**
 159       * HTML id and name for the beginning of the edit form.
 160       */
 161      const EDITFORM_ID = 'editform';
 162  
 163      /**
 164       * Prefix of key for cookie used to pass post-edit state.
 165       * The revision id edited is added after this
 166       */
 167      const POST_EDIT_COOKIE_KEY_PREFIX = 'PostEditRevision';
 168  
 169      /**
 170       * Duration of PostEdit cookie, in seconds.
 171       * The cookie will be removed instantly if the JavaScript runs.
 172       *
 173       * Otherwise, though, we don't want the cookies to accumulate.
 174       * RFC 2109 ( https://www.ietf.org/rfc/rfc2109.txt ) specifies a possible
 175       * limit of only 20 cookies per domain. This still applies at least to some
 176       * versions of IE without full updates:
 177       * https://blogs.msdn.com/b/ieinternals/archive/2009/08/20/wininet-ie-cookie-internals-faq.aspx
 178       *
 179       * A value of 20 minutes should be enough to take into account slow loads and minor
 180       * clock skew while still avoiding cookie accumulation when JavaScript is turned off.
 181       */
 182      const POST_EDIT_COOKIE_DURATION = 1200;
 183  
 184      /** @var Article */
 185      public $mArticle;
 186  
 187      /** @var Title */
 188      public $mTitle;
 189  
 190      /** @var null|Title */
 191      private $mContextTitle = null;
 192  
 193      /** @var string */
 194      public $action = 'submit';
 195  
 196      /** @var bool */
 197      public $isConflict = false;
 198  
 199      /** @var bool */
 200      public $isCssJsSubpage = false;
 201  
 202      /** @var bool */
 203      public $isCssSubpage = false;
 204  
 205      /** @var bool */
 206      public $isJsSubpage = false;
 207  
 208      /** @var bool */
 209      public $isWrongCaseCssJsPage = false;
 210  
 211      /** @var bool New page or new section */
 212      public $isNew = false;
 213  
 214      /** @var bool */
 215      public $deletedSinceEdit;
 216  
 217      /** @var string */
 218      public $formtype;
 219  
 220      /** @var bool */
 221      public $firsttime;
 222  
 223      /** @var bool|stdClass */
 224      public $lastDelete;
 225  
 226      /** @var bool */
 227      public $mTokenOk = false;
 228  
 229      /** @var bool */
 230      public $mTokenOkExceptSuffix = false;
 231  
 232      /** @var bool */
 233      public $mTriedSave = false;
 234  
 235      /** @var bool */
 236      public $incompleteForm = false;
 237  
 238      /** @var bool */
 239      public $tooBig = false;
 240  
 241      /** @var bool */
 242      public $kblength = false;
 243  
 244      /** @var bool */
 245      public $missingComment = false;
 246  
 247      /** @var bool */
 248      public $missingSummary = false;
 249  
 250      /** @var bool */
 251      public $allowBlankSummary = false;
 252  
 253      /** @var bool */
 254      protected $blankArticle = false;
 255  
 256      /** @var bool */
 257      protected $allowBlankArticle = false;
 258  
 259      /** @var string */
 260      public $autoSumm = '';
 261  
 262      /** @var string */
 263      public $hookError = '';
 264  
 265      /** @var ParserOutput */
 266      public $mParserOutput;
 267  
 268      /** @var bool Has a summary been preset using GET parameter &summary= ? */
 269      public $hasPresetSummary = false;
 270  
 271      /** @var bool */
 272      public $mBaseRevision = false;
 273  
 274      /** @var bool */
 275      public $mShowSummaryField = true;
 276  
 277      # Form values
 278  
 279      /** @var bool */
 280      public $save = false;
 281  
 282      /** @var bool */
 283      public $preview = false;
 284  
 285      /** @var bool */
 286      public $diff = false;
 287  
 288      /** @var bool */
 289      public $minoredit = false;
 290  
 291      /** @var bool */
 292      public $watchthis = false;
 293  
 294      /** @var bool */
 295      public $recreate = false;
 296  
 297      /** @var string */
 298      public $textbox1 = '';
 299  
 300      /** @var string */
 301      public $textbox2 = '';
 302  
 303      /** @var string */
 304      public $summary = '';
 305  
 306      /** @var bool */
 307      public $nosummary = false;
 308  
 309      /** @var string */
 310      public $edittime = '';
 311  
 312      /** @var string */
 313      public $section = '';
 314  
 315      /** @var string */
 316      public $sectiontitle = '';
 317  
 318      /** @var string */
 319      public $starttime = '';
 320  
 321      /** @var int */
 322      public $oldid = 0;
 323  
 324      /** @var string */
 325      public $editintro = '';
 326  
 327      /** @var null */
 328      public $scrolltop = null;
 329  
 330      /** @var bool */
 331      public $bot = true;
 332  
 333      /** @var null|string */
 334      public $contentModel = null;
 335  
 336      /** @var null|string */
 337      public $contentFormat = null;
 338  
 339      # Placeholders for text injection by hooks (must be HTML)
 340      # extensions should take care to _append_ to the present value
 341  
 342      /** @var string Before even the preview */
 343      public $editFormPageTop = '';
 344      public $editFormTextTop = '';
 345      public $editFormTextBeforeContent = '';
 346      public $editFormTextAfterWarn = '';
 347      public $editFormTextAfterTools = '';
 348      public $editFormTextBottom = '';
 349      public $editFormTextAfterContent = '';
 350      public $previewTextAfterContent = '';
 351      public $mPreloadContent = null;
 352  
 353      /* $didSave should be set to true whenever an article was successfully altered. */
 354      public $didSave = false;
 355      public $undidRev = 0;
 356  
 357      public $suppressIntro = false;
 358  
 359      /** @var bool Set to true to allow editing of non-text content types. */
 360      public $allowNonTextContent = false;
 361  
 362      /** @var bool */
 363      protected $edit;
 364  
 365      /** @var bool */
 366      public $live;
 367  
 368      /**
 369       * @param Article $article
 370       */
 371  	public function __construct( Article $article ) {
 372          $this->mArticle = $article;
 373          $this->mTitle = $article->getTitle();
 374  
 375          $this->contentModel = $this->mTitle->getContentModel();
 376  
 377          $handler = ContentHandler::getForModelID( $this->contentModel );
 378          $this->contentFormat = $handler->getDefaultFormat();
 379      }
 380  
 381      /**
 382       * @return Article
 383       */
 384  	public function getArticle() {
 385          return $this->mArticle;
 386      }
 387  
 388      /**
 389       * @since 1.19
 390       * @return Title
 391       */
 392  	public function getTitle() {
 393          return $this->mTitle;
 394      }
 395  
 396      /**
 397       * Set the context Title object
 398       *
 399       * @param Title|null $title Title object or null
 400       */
 401  	public function setContextTitle( $title ) {
 402          $this->mContextTitle = $title;
 403      }
 404  
 405      /**
 406       * Get the context title object.
 407       * If not set, $wgTitle will be returned. This behavior might change in
 408       * the future to return $this->mTitle instead.
 409       *
 410       * @return Title
 411       */
 412  	public function getContextTitle() {
 413          if ( is_null( $this->mContextTitle ) ) {
 414              global $wgTitle;
 415              return $wgTitle;
 416          } else {
 417              return $this->mContextTitle;
 418          }
 419      }
 420  
 421      /**
 422       * Returns if the given content model is editable.
 423       *
 424       * @param string $modelId The ID of the content model to test. Use CONTENT_MODEL_XXX constants.
 425       * @return bool
 426       * @throws MWException If $modelId has no known handler
 427       */
 428  	public function isSupportedContentModel( $modelId ) {
 429          return $this->allowNonTextContent ||
 430              ContentHandler::getForModelID( $modelId ) instanceof TextContentHandler;
 431      }
 432  
 433  	function submit() {
 434          $this->edit();
 435      }
 436  
 437      /**
 438       * This is the function that gets called for "action=edit". It
 439       * sets up various member variables, then passes execution to
 440       * another function, usually showEditForm()
 441       *
 442       * The edit form is self-submitting, so that when things like
 443       * preview and edit conflicts occur, we get the same form back
 444       * with the extra stuff added.  Only when the final submission
 445       * is made and all is well do we actually save and redirect to
 446       * the newly-edited page.
 447       */
 448  	function edit() {
 449          global $wgOut, $wgRequest, $wgUser;
 450          // Allow extensions to modify/prevent this form or submission
 451          if ( !wfRunHooks( 'AlternateEdit', array( $this ) ) ) {
 452              return;
 453          }
 454  
 455          wfProfileIn( __METHOD__ );
 456          wfDebug( __METHOD__ . ": enter\n" );
 457  
 458          // If they used redlink=1 and the page exists, redirect to the main article
 459          if ( $wgRequest->getBool( 'redlink' ) && $this->mTitle->exists() ) {
 460              $wgOut->redirect( $this->mTitle->getFullURL() );
 461              wfProfileOut( __METHOD__ );
 462              return;
 463          }
 464  
 465          $this->importFormData( $wgRequest );
 466          $this->firsttime = false;
 467  
 468          if ( $this->live ) {
 469              $this->livePreview();
 470              wfProfileOut( __METHOD__ );
 471              return;
 472          }
 473  
 474          if ( wfReadOnly() && $this->save ) {
 475              // Force preview
 476              $this->save = false;
 477              $this->preview = true;
 478          }
 479  
 480          if ( $this->save ) {
 481              $this->formtype = 'save';
 482          } elseif ( $this->preview ) {
 483              $this->formtype = 'preview';
 484          } elseif ( $this->diff ) {
 485              $this->formtype = 'diff';
 486          } else { # First time through
 487              $this->firsttime = true;
 488              if ( $this->previewOnOpen() ) {
 489                  $this->formtype = 'preview';
 490              } else {
 491                  $this->formtype = 'initial';
 492              }
 493          }
 494  
 495          $permErrors = $this->getEditPermissionErrors();
 496          if ( $permErrors ) {
 497              wfDebug( __METHOD__ . ": User can't edit\n" );
 498              // Auto-block user's IP if the account was "hard" blocked
 499              $wgUser->spreadAnyEditBlock();
 500  
 501              $this->displayPermissionsError( $permErrors );
 502  
 503              wfProfileOut( __METHOD__ );
 504              return;
 505          }
 506  
 507          wfProfileIn( __METHOD__ . "-business-end" );
 508  
 509          $this->isConflict = false;
 510          // css / js subpages of user pages get a special treatment
 511          $this->isCssJsSubpage = $this->mTitle->isCssJsSubpage();
 512          $this->isCssSubpage = $this->mTitle->isCssSubpage();
 513          $this->isJsSubpage = $this->mTitle->isJsSubpage();
 514          // @todo FIXME: Silly assignment.
 515          $this->isWrongCaseCssJsPage = $this->isWrongCaseCssJsPage();
 516  
 517          # Show applicable editing introductions
 518          if ( $this->formtype == 'initial' || $this->firsttime ) {
 519              $this->showIntro();
 520          }
 521  
 522          # Attempt submission here.  This will check for edit conflicts,
 523          # and redundantly check for locked database, blocked IPs, etc.
 524          # that edit() already checked just in case someone tries to sneak
 525          # in the back door with a hand-edited submission URL.
 526  
 527          if ( 'save' == $this->formtype ) {
 528              if ( !$this->attemptSave() ) {
 529                  wfProfileOut( __METHOD__ . "-business-end" );
 530                  wfProfileOut( __METHOD__ );
 531                  return;
 532              }
 533          }
 534  
 535          # First time through: get contents, set time for conflict
 536          # checking, etc.
 537          if ( 'initial' == $this->formtype || $this->firsttime ) {
 538              if ( $this->initialiseForm() === false ) {
 539                  $this->noSuchSectionPage();
 540                  wfProfileOut( __METHOD__ . "-business-end" );
 541                  wfProfileOut( __METHOD__ );
 542                  return;
 543              }
 544  
 545              if ( !$this->mTitle->getArticleID() ) {
 546                  wfRunHooks( 'EditFormPreloadText', array( &$this->textbox1, &$this->mTitle ) );
 547              } else {
 548                  wfRunHooks( 'EditFormInitialText', array( $this ) );
 549              }
 550  
 551          }
 552  
 553          $this->showEditForm();
 554          wfProfileOut( __METHOD__ . "-business-end" );
 555          wfProfileOut( __METHOD__ );
 556      }
 557  
 558      /**
 559       * @return array
 560       */
 561  	protected function getEditPermissionErrors() {
 562          global $wgUser;
 563          $permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser );
 564          # Can this title be created?
 565          if ( !$this->mTitle->exists() ) {
 566              $permErrors = array_merge( $permErrors,
 567                  wfArrayDiff2( $this->mTitle->getUserPermissionsErrors( 'create', $wgUser ), $permErrors ) );
 568          }
 569          # Ignore some permissions errors when a user is just previewing/viewing diffs
 570          $remove = array();
 571          foreach ( $permErrors as $error ) {
 572              if ( ( $this->preview || $this->diff )
 573                  && ( $error[0] == 'blockedtext' || $error[0] == 'autoblockedtext' )
 574              ) {
 575                  $remove[] = $error;
 576              }
 577          }
 578          $permErrors = wfArrayDiff2( $permErrors, $remove );
 579          return $permErrors;
 580      }
 581  
 582      /**
 583       * Display a permissions error page, like OutputPage::showPermissionsErrorPage(),
 584       * but with the following differences:
 585       * - If redlink=1, the user will be redirected to the page
 586       * - If there is content to display or the error occurs while either saving,
 587       *   previewing or showing the difference, it will be a
 588       *   "View source for ..." page displaying the source code after the error message.
 589       *
 590       * @since 1.19
 591       * @param array $permErrors Array of permissions errors, as returned by
 592       *    Title::getUserPermissionsErrors().
 593       * @throws PermissionsError
 594       */
 595  	protected function displayPermissionsError( array $permErrors ) {
 596          global $wgRequest, $wgOut;
 597  
 598          if ( $wgRequest->getBool( 'redlink' ) ) {
 599              // The edit page was reached via a red link.
 600              // Redirect to the article page and let them click the edit tab if
 601              // they really want a permission error.
 602              $wgOut->redirect( $this->mTitle->getFullURL() );
 603              return;
 604          }
 605  
 606          $content = $this->getContentObject();
 607  
 608          # Use the normal message if there's nothing to display
 609          if ( $this->firsttime && ( !$content || $content->isEmpty() ) ) {
 610              $action = $this->mTitle->exists() ? 'edit' :
 611                  ( $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage' );
 612              throw new PermissionsError( $action, $permErrors );
 613          }
 614  
 615          wfRunHooks( 'EditPage::showReadOnlyForm:initial', array( $this, &$wgOut ) );
 616  
 617          $wgOut->setRobotPolicy( 'noindex,nofollow' );
 618          $wgOut->setPageTitle( wfMessage(
 619              'viewsource-title',
 620              $this->getContextTitle()->getPrefixedText()
 621          ) );
 622          $wgOut->addBacklinkSubtitle( $this->getContextTitle() );
 623          $wgOut->addWikiText( $wgOut->formatPermissionsErrorMessage( $permErrors, 'edit' ) );
 624          $wgOut->addHTML( "<hr />\n" );
 625  
 626          # If the user made changes, preserve them when showing the markup
 627          # (This happens when a user is blocked during edit, for instance)
 628          if ( !$this->firsttime ) {
 629              $text = $this->textbox1;
 630              $wgOut->addWikiMsg( 'viewyourtext' );
 631          } else {
 632              $text = $this->toEditText( $content );
 633              $wgOut->addWikiMsg( 'viewsourcetext' );
 634          }
 635  
 636          $this->showTextbox( $text, 'wpTextbox1', array( 'readonly' ) );
 637  
 638          $wgOut->addHTML( Html::rawElement( 'div', array( 'class' => 'templatesUsed' ),
 639              Linker::formatTemplates( $this->getTemplates() ) ) );
 640  
 641          $wgOut->addModules( 'mediawiki.action.edit.collapsibleFooter' );
 642  
 643          if ( $this->mTitle->exists() ) {
 644              $wgOut->returnToMain( null, $this->mTitle );
 645          }
 646      }
 647  
 648      /**
 649       * Should we show a preview when the edit form is first shown?
 650       *
 651       * @return bool
 652       */
 653  	protected function previewOnOpen() {
 654          global $wgRequest, $wgUser, $wgPreviewOnOpenNamespaces;
 655          if ( $wgRequest->getVal( 'preview' ) == 'yes' ) {
 656              // Explicit override from request
 657              return true;
 658          } elseif ( $wgRequest->getVal( 'preview' ) == 'no' ) {
 659              // Explicit override from request
 660              return false;
 661          } elseif ( $this->section == 'new' ) {
 662              // Nothing *to* preview for new sections
 663              return false;
 664          } elseif ( ( $wgRequest->getVal( 'preload' ) !== null || $this->mTitle->exists() )
 665              && $wgUser->getOption( 'previewonfirst' )
 666          ) {
 667              // Standard preference behavior
 668              return true;
 669          } elseif ( !$this->mTitle->exists()
 670              && isset( $wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()] )
 671              && $wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()]
 672          ) {
 673              // Categories are special
 674              return true;
 675          } else {
 676              return false;
 677          }
 678      }
 679  
 680      /**
 681       * Checks whether the user entered a skin name in uppercase,
 682       * e.g. "User:Example/Monobook.css" instead of "monobook.css"
 683       *
 684       * @return bool
 685       */
 686  	protected function isWrongCaseCssJsPage() {
 687          if ( $this->mTitle->isCssJsSubpage() ) {
 688              $name = $this->mTitle->getSkinFromCssJsSubpage();
 689              $skins = array_merge(
 690                  array_keys( Skin::getSkinNames() ),
 691                  array( 'common' )
 692              );
 693              return !in_array( $name, $skins )
 694                  && in_array( strtolower( $name ), $skins );
 695          } else {
 696              return false;
 697          }
 698      }
 699  
 700      /**
 701       * Returns whether section editing is supported for the current page.
 702       * Subclasses may override this to replace the default behavior, which is
 703       * to check ContentHandler::supportsSections.
 704       *
 705       * @return bool True if this edit page supports sections, false otherwise.
 706       */
 707  	protected function isSectionEditSupported() {
 708          $contentHandler = ContentHandler::getForTitle( $this->mTitle );
 709          return $contentHandler->supportsSections();
 710      }
 711  
 712      /**
 713       * This function collects the form data and uses it to populate various member variables.
 714       * @param WebRequest $request
 715       * @throws ErrorPageError
 716       */
 717  	function importFormData( &$request ) {
 718          global $wgContLang, $wgUser;
 719  
 720          wfProfileIn( __METHOD__ );
 721  
 722          # Section edit can come from either the form or a link
 723          $this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) );
 724  
 725          if ( $this->section !== null && $this->section !== '' && !$this->isSectionEditSupported() ) {
 726              wfProfileOut( __METHOD__ );
 727              throw new ErrorPageError( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
 728          }
 729  
 730          $this->isNew = !$this->mTitle->exists() || $this->section == 'new';
 731  
 732          if ( $request->wasPosted() ) {
 733              # These fields need to be checked for encoding.
 734              # Also remove trailing whitespace, but don't remove _initial_
 735              # whitespace from the text boxes. This may be significant formatting.
 736              $this->textbox1 = $this->safeUnicodeInput( $request, 'wpTextbox1' );
 737              if ( !$request->getCheck( 'wpTextbox2' ) ) {
 738                  // Skip this if wpTextbox2 has input, it indicates that we came
 739                  // from a conflict page with raw page text, not a custom form
 740                  // modified by subclasses
 741                  wfProfileIn( get_class( $this ) . "::importContentFormData" );
 742                  $textbox1 = $this->importContentFormData( $request );
 743                  if ( $textbox1 !== null ) {
 744                      $this->textbox1 = $textbox1;
 745                  }
 746  
 747                  wfProfileOut( get_class( $this ) . "::importContentFormData" );
 748              }
 749  
 750              # Truncate for whole multibyte characters
 751              $this->summary = $wgContLang->truncate( $request->getText( 'wpSummary' ), 255 );
 752  
 753              # If the summary consists of a heading, e.g. '==Foobar==', extract the title from the
 754              # header syntax, e.g. 'Foobar'. This is mainly an issue when we are using wpSummary for
 755              # section titles.
 756              $this->summary = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary );
 757  
 758              # Treat sectiontitle the same way as summary.
 759              # Note that wpSectionTitle is not yet a part of the actual edit form, as wpSummary is
 760              # currently doing double duty as both edit summary and section title. Right now this
 761              # is just to allow API edits to work around this limitation, but this should be
 762              # incorporated into the actual edit form when EditPage is rewritten (Bugs 18654, 26312).
 763              $this->sectiontitle = $wgContLang->truncate( $request->getText( 'wpSectionTitle' ), 255 );
 764              $this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle );
 765  
 766              $this->edittime = $request->getVal( 'wpEdittime' );
 767              $this->starttime = $request->getVal( 'wpStarttime' );
 768  
 769              $undidRev = $request->getInt( 'wpUndidRevision' );
 770              if ( $undidRev ) {
 771                  $this->undidRev = $undidRev;
 772              }
 773  
 774              $this->scrolltop = $request->getIntOrNull( 'wpScrolltop' );
 775  
 776              if ( $this->textbox1 === '' && $request->getVal( 'wpTextbox1' ) === null ) {
 777                  // wpTextbox1 field is missing, possibly due to being "too big"
 778                  // according to some filter rules such as Suhosin's setting for
 779                  // suhosin.request.max_value_length (d'oh)
 780                  $this->incompleteForm = true;
 781              } else {
 782                  // If we receive the last parameter of the request, we can fairly
 783                  // claim the POST request has not been truncated.
 784  
 785                  // TODO: softened the check for cutover.  Once we determine
 786                  // that it is safe, we should complete the transition by
 787                  // removing the "edittime" clause.
 788                  $this->incompleteForm = ( !$request->getVal( 'wpUltimateParam' ) && is_null( $this->edittime ) );
 789              }
 790              if ( $this->incompleteForm ) {
 791                  # If the form is incomplete, force to preview.
 792                  wfDebug( __METHOD__ . ": Form data appears to be incomplete\n" );
 793                  wfDebug( "POST DATA: " . var_export( $_POST, true ) . "\n" );
 794                  $this->preview = true;
 795              } else {
 796                  /* Fallback for live preview */
 797                  $this->preview = $request->getCheck( 'wpPreview' ) || $request->getCheck( 'wpLivePreview' );
 798                  $this->diff = $request->getCheck( 'wpDiff' );
 799  
 800                  // Remember whether a save was requested, so we can indicate
 801                  // if we forced preview due to session failure.
 802                  $this->mTriedSave = !$this->preview;
 803  
 804                  if ( $this->tokenOk( $request ) ) {
 805                      # Some browsers will not report any submit button
 806                      # if the user hits enter in the comment box.
 807                      # The unmarked state will be assumed to be a save,
 808                      # if the form seems otherwise complete.
 809                      wfDebug( __METHOD__ . ": Passed token check.\n" );
 810                  } elseif ( $this->diff ) {
 811                      # Failed token check, but only requested "Show Changes".
 812                      wfDebug( __METHOD__ . ": Failed token check; Show Changes requested.\n" );
 813                  } else {
 814                      # Page might be a hack attempt posted from
 815                      # an external site. Preview instead of saving.
 816                      wfDebug( __METHOD__ . ": Failed token check; forcing preview\n" );
 817                      $this->preview = true;
 818                  }
 819              }
 820              $this->save = !$this->preview && !$this->diff;
 821              if ( !preg_match( '/^\d{14}$/', $this->edittime ) ) {
 822                  $this->edittime = null;
 823              }
 824  
 825              if ( !preg_match( '/^\d{14}$/', $this->starttime ) ) {
 826                  $this->starttime = null;
 827              }
 828  
 829              $this->recreate = $request->getCheck( 'wpRecreate' );
 830  
 831              $this->minoredit = $request->getCheck( 'wpMinoredit' );
 832              $this->watchthis = $request->getCheck( 'wpWatchthis' );
 833  
 834              # Don't force edit summaries when a user is editing their own user or talk page
 835              if ( ( $this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK )
 836                  && $this->mTitle->getText() == $wgUser->getName()
 837              ) {
 838                  $this->allowBlankSummary = true;
 839              } else {
 840                  $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' )
 841                      || !$wgUser->getOption( 'forceeditsummary' );
 842              }
 843  
 844              $this->autoSumm = $request->getText( 'wpAutoSummary' );
 845  
 846              $this->allowBlankArticle = $request->getBool( 'wpIgnoreBlankArticle' );
 847          } else {
 848              # Not a posted form? Start with nothing.
 849              wfDebug( __METHOD__ . ": Not a posted form.\n" );
 850              $this->textbox1 = '';
 851              $this->summary = '';
 852              $this->sectiontitle = '';
 853              $this->edittime = '';
 854              $this->starttime = wfTimestampNow();
 855              $this->edit = false;
 856              $this->preview = false;
 857              $this->save = false;
 858              $this->diff = false;
 859              $this->minoredit = false;
 860              // Watch may be overridden by request parameters
 861              $this->watchthis = $request->getBool( 'watchthis', false );
 862              $this->recreate = false;
 863  
 864              // When creating a new section, we can preload a section title by passing it as the
 865              // preloadtitle parameter in the URL (Bug 13100)
 866              if ( $this->section == 'new' && $request->getVal( 'preloadtitle' ) ) {
 867                  $this->sectiontitle = $request->getVal( 'preloadtitle' );
 868                  // Once wpSummary isn't being use for setting section titles, we should delete this.
 869                  $this->summary = $request->getVal( 'preloadtitle' );
 870              } elseif ( $this->section != 'new' && $request->getVal( 'summary' ) ) {
 871                  $this->summary = $request->getText( 'summary' );
 872                  if ( $this->summary !== '' ) {
 873                      $this->hasPresetSummary = true;
 874                  }
 875              }
 876  
 877              if ( $request->getVal( 'minor' ) ) {
 878                  $this->minoredit = true;
 879              }
 880          }
 881  
 882          $this->oldid = $request->getInt( 'oldid' );
 883  
 884          $this->bot = $request->getBool( 'bot', true );
 885          $this->nosummary = $request->getBool( 'nosummary' );
 886  
 887          // May be overridden by revision.
 888          $this->contentModel = $request->getText( 'model', $this->contentModel );
 889          // May be overridden by revision.
 890          $this->contentFormat = $request->getText( 'format', $this->contentFormat );
 891  
 892          if ( !ContentHandler::getForModelID( $this->contentModel )
 893              ->isSupportedFormat( $this->contentFormat )
 894          ) {
 895              throw new ErrorPageError(
 896                  'editpage-notsupportedcontentformat-title',
 897                  'editpage-notsupportedcontentformat-text',
 898                  array( $this->contentFormat, ContentHandler::getLocalizedName( $this->contentModel ) )
 899              );
 900          }
 901  
 902          /**
 903           * @todo Check if the desired model is allowed in this namespace, and if
 904           *   a transition from the page's current model to the new model is
 905           *   allowed.
 906           */
 907  
 908          $this->live = $request->getCheck( 'live' );
 909          $this->editintro = $request->getText( 'editintro',
 910              // Custom edit intro for new sections
 911              $this->section === 'new' ? 'MediaWiki:addsection-editintro' : '' );
 912  
 913          // Allow extensions to modify form data
 914          wfRunHooks( 'EditPage::importFormData', array( $this, $request ) );
 915  
 916          wfProfileOut( __METHOD__ );
 917      }
 918  
 919      /**
 920       * Subpage overridable method for extracting the page content data from the
 921       * posted form to be placed in $this->textbox1, if using customized input
 922       * this method should be overridden and return the page text that will be used
 923       * for saving, preview parsing and so on...
 924       *
 925       * @param WebRequest $request
 926       */
 927  	protected function importContentFormData( &$request ) {
 928          return; // Don't do anything, EditPage already extracted wpTextbox1
 929      }
 930  
 931      /**
 932       * Initialise form fields in the object
 933       * Called on the first invocation, e.g. when a user clicks an edit link
 934       * @return bool If the requested section is valid
 935       */
 936  	function initialiseForm() {
 937          global $wgUser;
 938          $this->edittime = $this->mArticle->getTimestamp();
 939  
 940          $content = $this->getContentObject( false ); #TODO: track content object?!
 941          if ( $content === false ) {
 942              return false;
 943          }
 944          $this->textbox1 = $this->toEditText( $content );
 945  
 946          // activate checkboxes if user wants them to be always active
 947          # Sort out the "watch" checkbox
 948          if ( $wgUser->getOption( 'watchdefault' ) ) {
 949              # Watch all edits
 950              $this->watchthis = true;
 951          } elseif ( $wgUser->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
 952              # Watch creations
 953              $this->watchthis = true;
 954          } elseif ( $wgUser->isWatched( $this->mTitle ) ) {
 955              # Already watched
 956              $this->watchthis = true;
 957          }
 958          if ( $wgUser->getOption( 'minordefault' ) && !$this->isNew ) {
 959              $this->minoredit = true;
 960          }
 961          if ( $this->textbox1 === false ) {
 962              return false;
 963          }
 964          return true;
 965      }
 966  
 967      /**
 968       * @param Content|null $def_content The default value to return
 969       *
 970       * @return Content|null Content on success, $def_content for invalid sections
 971       *
 972       * @since 1.21
 973       */
 974  	protected function getContentObject( $def_content = null ) {
 975          global $wgOut, $wgRequest, $wgUser, $wgContLang;
 976  
 977          wfProfileIn( __METHOD__ );
 978  
 979          $content = false;
 980  
 981          // For message page not locally set, use the i18n message.
 982          // For other non-existent articles, use preload text if any.
 983          if ( !$this->mTitle->exists() || $this->section == 'new' ) {
 984              if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && $this->section != 'new' ) {
 985                  # If this is a system message, get the default text.
 986                  $msg = $this->mTitle->getDefaultMessageText();
 987  
 988                  $content = $this->toEditContent( $msg );
 989              }
 990              if ( $content === false ) {
 991                  # If requested, preload some text.
 992                  $preload = $wgRequest->getVal( 'preload',
 993                      // Custom preload text for new sections
 994                      $this->section === 'new' ? 'MediaWiki:addsection-preload' : '' );
 995                  $params = $wgRequest->getArray( 'preloadparams', array() );
 996  
 997                  $content = $this->getPreloadedContent( $preload, $params );
 998              }
 999          // For existing pages, get text based on "undo" or section parameters.
1000          } else {
1001              if ( $this->section != '' ) {
1002                  // Get section edit text (returns $def_text for invalid sections)
1003                  $orig = $this->getOriginalContent( $wgUser );
1004                  $content = $orig ? $orig->getSection( $this->section ) : null;
1005  
1006                  if ( !$content ) {
1007                      $content = $def_content;
1008                  }
1009              } else {
1010                  $undoafter = $wgRequest->getInt( 'undoafter' );
1011                  $undo = $wgRequest->getInt( 'undo' );
1012  
1013                  if ( $undo > 0 && $undoafter > 0 ) {
1014  
1015                      $undorev = Revision::newFromId( $undo );
1016                      $oldrev = Revision::newFromId( $undoafter );
1017  
1018                      # Sanity check, make sure it's the right page,
1019                      # the revisions exist and they were not deleted.
1020                      # Otherwise, $content will be left as-is.
1021                      if ( !is_null( $undorev ) && !is_null( $oldrev ) &&
1022                          !$undorev->isDeleted( Revision::DELETED_TEXT ) &&
1023                          !$oldrev->isDeleted( Revision::DELETED_TEXT ) ) {
1024  
1025                          $content = $this->mArticle->getUndoContent( $undorev, $oldrev );
1026  
1027                          if ( $content === false ) {
1028                              # Warn the user that something went wrong
1029                              $undoMsg = 'failure';
1030                          } else {
1031                              $oldContent = $this->mArticle->getPage()->getContent( Revision::RAW );
1032                              $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
1033                              $newContent = $content->preSaveTransform( $this->mTitle, $wgUser, $popts );
1034  
1035                              if ( $newContent->equals( $oldContent ) ) {
1036                                  # Tell the user that the undo results in no change,
1037                                  # i.e. the revisions were already undone.
1038                                  $undoMsg = 'nochange';
1039                                  $content = false;
1040                              } else {
1041                                  # Inform the user of our success and set an automatic edit summary
1042                                  $undoMsg = 'success';
1043  
1044                                  # If we just undid one rev, use an autosummary
1045                                  $firstrev = $oldrev->getNext();
1046                                  if ( $firstrev && $firstrev->getId() == $undo ) {
1047                                      $userText = $undorev->getUserText();
1048                                      if ( $userText === '' ) {
1049                                          $undoSummary = wfMessage(
1050                                              'undo-summary-username-hidden',
1051                                              $undo
1052                                          )->inContentLanguage()->text();
1053                                      } else {
1054                                          $undoSummary = wfMessage(
1055                                              'undo-summary',
1056                                              $undo,
1057                                              $userText
1058                                          )->inContentLanguage()->text();
1059                                      }
1060                                      if ( $this->summary === '' ) {
1061                                          $this->summary = $undoSummary;
1062                                      } else {
1063                                          $this->summary = $undoSummary . wfMessage( 'colon-separator' )
1064                                              ->inContentLanguage()->text() . $this->summary;
1065                                      }
1066                                      $this->undidRev = $undo;
1067                                  }
1068                                  $this->formtype = 'diff';
1069                              }
1070                          }
1071                      } else {
1072                          // Failed basic sanity checks.
1073                          // Older revisions may have been removed since the link
1074                          // was created, or we may simply have got bogus input.
1075                          $undoMsg = 'norev';
1076                      }
1077  
1078                      // Messages: undo-success, undo-failure, undo-norev, undo-nochange
1079                      $class = ( $undoMsg == 'success' ? '' : 'error ' ) . "mw-undo-{$undoMsg}";
1080                      $this->editFormPageTop .= $wgOut->parse( "<div class=\"{$class}\">" .
1081                          wfMessage( 'undo-' . $undoMsg )->plain() . '</div>', true, /* interface */true );
1082                  }
1083  
1084                  if ( $content === false ) {
1085                      $content = $this->getOriginalContent( $wgUser );
1086                  }
1087              }
1088          }
1089  
1090          wfProfileOut( __METHOD__ );
1091          return $content;
1092      }
1093  
1094      /**
1095       * Get the content of the wanted revision, without section extraction.
1096       *
1097       * The result of this function can be used to compare user's input with
1098       * section replaced in its context (using WikiPage::replaceSection())
1099       * to the original text of the edit.
1100       *
1101       * This differs from Article::getContent() that when a missing revision is
1102       * encountered the result will be null and not the
1103       * 'missing-revision' message.
1104       *
1105       * @since 1.19
1106       * @param User $user The user to get the revision for
1107       * @return Content|null
1108       */
1109  	private function getOriginalContent( User $user ) {
1110          if ( $this->section == 'new' ) {
1111              return $this->getCurrentContent();
1112          }
1113          $revision = $this->mArticle->getRevisionFetched();
1114          if ( $revision === null ) {
1115              if ( !$this->contentModel ) {
1116                  $this->contentModel = $this->getTitle()->getContentModel();
1117              }
1118              $handler = ContentHandler::getForModelID( $this->contentModel );
1119  
1120              return $handler->makeEmptyContent();
1121          }
1122          $content = $revision->getContent( Revision::FOR_THIS_USER, $user );
1123          return $content;
1124      }
1125  
1126      /**
1127       * Get the current content of the page. This is basically similar to
1128       * WikiPage::getContent( Revision::RAW ) except that when the page doesn't exist an empty
1129       * content object is returned instead of null.
1130       *
1131       * @since 1.21
1132       * @return Content
1133       */
1134  	protected function getCurrentContent() {
1135          $rev = $this->mArticle->getRevision();
1136          $content = $rev ? $rev->getContent( Revision::RAW ) : null;
1137  
1138          if ( $content === false || $content === null ) {
1139              if ( !$this->contentModel ) {
1140                  $this->contentModel = $this->getTitle()->getContentModel();
1141              }
1142              $handler = ContentHandler::getForModelID( $this->contentModel );
1143  
1144              return $handler->makeEmptyContent();
1145          } else {
1146              # nasty side-effect, but needed for consistency
1147              $this->contentModel = $rev->getContentModel();
1148              $this->contentFormat = $rev->getContentFormat();
1149  
1150              return $content;
1151          }
1152      }
1153  
1154      /**
1155       * Use this method before edit() to preload some content into the edit box
1156       *
1157       * @param Content $content
1158       *
1159       * @since 1.21
1160       */
1161  	public function setPreloadedContent( Content $content ) {
1162          $this->mPreloadContent = $content;
1163      }
1164  
1165      /**
1166       * Get the contents to be preloaded into the box, either set by
1167       * an earlier setPreloadText() or by loading the given page.
1168       *
1169       * @param string $preload Representing the title to preload from.
1170       * @param array $params Parameters to use (interface-message style) in the preloaded text
1171       *
1172       * @return Content
1173       *
1174       * @since 1.21
1175       */
1176  	protected function getPreloadedContent( $preload, $params = array() ) {
1177          global $wgUser;
1178  
1179          if ( !empty( $this->mPreloadContent ) ) {
1180              return $this->mPreloadContent;
1181          }
1182  
1183          $handler = ContentHandler::getForTitle( $this->getTitle() );
1184  
1185          if ( $preload === '' ) {
1186              return $handler->makeEmptyContent();
1187          }
1188  
1189          $title = Title::newFromText( $preload );
1190          # Check for existence to avoid getting MediaWiki:Noarticletext
1191          if ( $title === null || !$title->exists() || !$title->userCan( 'read', $wgUser ) ) {
1192              //TODO: somehow show a warning to the user!
1193              return $handler->makeEmptyContent();
1194          }
1195  
1196          $page = WikiPage::factory( $title );
1197          if ( $page->isRedirect() ) {
1198              $title = $page->getRedirectTarget();
1199              # Same as before
1200              if ( $title === null || !$title->exists() || !$title->userCan( 'read', $wgUser ) ) {
1201                  //TODO: somehow show a warning to the user!
1202                  return $handler->makeEmptyContent();
1203              }
1204              $page = WikiPage::factory( $title );
1205          }
1206  
1207          $parserOptions = ParserOptions::newFromUser( $wgUser );
1208          $content = $page->getContent( Revision::RAW );
1209  
1210          if ( !$content ) {
1211              //TODO: somehow show a warning to the user!
1212              return $handler->makeEmptyContent();
1213          }
1214  
1215          if ( $content->getModel() !== $handler->getModelID() ) {
1216              $converted = $content->convert( $handler->getModelID() );
1217  
1218              if ( !$converted ) {
1219                  //TODO: somehow show a warning to the user!
1220                  wfDebug( "Attempt to preload incompatible content: "
1221                          . "can't convert " . $content->getModel()
1222                          . " to " . $handler->getModelID() );
1223  
1224                  return $handler->makeEmptyContent();
1225              }
1226  
1227              $content = $converted;
1228          }
1229  
1230          return $content->preloadTransform( $title, $parserOptions, $params );
1231      }
1232  
1233      /**
1234       * Make sure the form isn't faking a user's credentials.
1235       *
1236       * @param WebRequest $request
1237       * @return bool
1238       * @private
1239       */
1240  	function tokenOk( &$request ) {
1241          global $wgUser;
1242          $token = $request->getVal( 'wpEditToken' );
1243          $this->mTokenOk = $wgUser->matchEditToken( $token );
1244          $this->mTokenOkExceptSuffix = $wgUser->matchEditTokenNoSuffix( $token );
1245          return $this->mTokenOk;
1246      }
1247  
1248      /**
1249       * Sets post-edit cookie indicating the user just saved a particular revision.
1250       *
1251       * This uses a temporary cookie for each revision ID so separate saves will never
1252       * interfere with each other.
1253       *
1254       * The cookie is deleted in the mediawiki.action.view.postEdit JS module after
1255       * the redirect.  It must be clearable by JavaScript code, so it must not be
1256       * marked HttpOnly. The JavaScript code converts the cookie to a wgPostEdit config
1257       * variable.
1258       *
1259       * If the variable were set on the server, it would be cached, which is unwanted
1260       * since the post-edit state should only apply to the load right after the save.
1261       *
1262       * @param int $statusValue The status value (to check for new article status)
1263       */
1264  	protected function setPostEditCookie( $statusValue ) {
1265          $revisionId = $this->mArticle->getLatest();
1266          $postEditKey = self::POST_EDIT_COOKIE_KEY_PREFIX . $revisionId;
1267  
1268          $val = 'saved';
1269          if ( $statusValue == self::AS_SUCCESS_NEW_ARTICLE ) {
1270              $val = 'created';
1271          } elseif ( $this->oldid ) {
1272              $val = 'restored';
1273          }
1274  
1275          $response = RequestContext::getMain()->getRequest()->response();
1276          $response->setcookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION, array(
1277              'httpOnly' => false,
1278          ) );
1279      }
1280  
1281      /**
1282       * Attempt submission
1283       * @throws UserBlockedError|ReadOnlyError|ThrottledError|PermissionsError
1284       * @return bool False if output is done, true if the rest of the form should be displayed
1285       */
1286  	public function attemptSave() {
1287          global $wgUser;
1288  
1289          $resultDetails = false;
1290          # Allow bots to exempt some edits from bot flagging
1291          $bot = $wgUser->isAllowed( 'bot' ) && $this->bot;
1292          $status = $this->internalAttemptSave( $resultDetails, $bot );
1293  
1294          return $this->handleStatus( $status, $resultDetails );
1295      }
1296  
1297      /**
1298       * Handle status, such as after attempt save
1299       *
1300       * @param Status $status
1301       * @param array|bool $resultDetails
1302       *
1303       * @throws ErrorPageError
1304       * @return bool False, if output is done, true if rest of the form should be displayed
1305       */
1306  	private function handleStatus( Status $status, $resultDetails ) {
1307          global $wgUser, $wgOut;
1308  
1309          /**
1310           * @todo FIXME: once the interface for internalAttemptSave() is made
1311           *   nicer, this should use the message in $status
1312           */
1313          if ( $status->value == self::AS_SUCCESS_UPDATE
1314              || $status->value == self::AS_SUCCESS_NEW_ARTICLE
1315          ) {
1316              $this->didSave = true;
1317              if ( !$resultDetails['nullEdit'] ) {
1318                  $this->setPostEditCookie( $status->value );
1319              }
1320          }
1321  
1322          switch ( $status->value ) {
1323              case self::AS_HOOK_ERROR_EXPECTED:
1324              case self::AS_CONTENT_TOO_BIG:
1325              case self::AS_ARTICLE_WAS_DELETED:
1326              case self::AS_CONFLICT_DETECTED:
1327              case self::AS_SUMMARY_NEEDED:
1328              case self::AS_TEXTBOX_EMPTY:
1329              case self::AS_MAX_ARTICLE_SIZE_EXCEEDED:
1330              case self::AS_END:
1331              case self::AS_BLANK_ARTICLE:
1332                  return true;
1333  
1334              case self::AS_HOOK_ERROR:
1335                  return false;
1336  
1337              case self::AS_PARSE_ERROR:
1338                  $wgOut->addWikiText( '<div class="error">' . $status->getWikiText() . '</div>' );
1339                  return true;
1340  
1341              case self::AS_SUCCESS_NEW_ARTICLE:
1342                  $query = $resultDetails['redirect'] ? 'redirect=no' : '';
1343                  $anchor = isset( $resultDetails['sectionanchor'] ) ? $resultDetails['sectionanchor'] : '';
1344                  $wgOut->redirect( $this->mTitle->getFullURL( $query ) . $anchor );
1345                  return false;
1346  
1347              case self::AS_SUCCESS_UPDATE:
1348                  $extraQuery = '';
1349                  $sectionanchor = $resultDetails['sectionanchor'];
1350  
1351                  // Give extensions a chance to modify URL query on update
1352                  wfRunHooks(
1353                      'ArticleUpdateBeforeRedirect',
1354                      array( $this->mArticle, &$sectionanchor, &$extraQuery )
1355                  );
1356  
1357                  if ( $resultDetails['redirect'] ) {
1358                      if ( $extraQuery == '' ) {
1359                          $extraQuery = 'redirect=no';
1360                      } else {
1361                          $extraQuery = 'redirect=no&' . $extraQuery;
1362                      }
1363                  }
1364                  $wgOut->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor );
1365                  return false;
1366  
1367              case self::AS_SPAM_ERROR:
1368                  $this->spamPageWithContent( $resultDetails['spam'] );
1369                  return false;
1370  
1371              case self::AS_BLOCKED_PAGE_FOR_USER:
1372                  throw new UserBlockedError( $wgUser->getBlock() );
1373  
1374              case self::AS_IMAGE_REDIRECT_ANON:
1375              case self::AS_IMAGE_REDIRECT_LOGGED:
1376                  throw new PermissionsError( 'upload' );
1377  
1378              case self::AS_READ_ONLY_PAGE_ANON:
1379              case self::AS_READ_ONLY_PAGE_LOGGED:
1380                  throw new PermissionsError( 'edit' );
1381  
1382              case self::AS_READ_ONLY_PAGE:
1383                  throw new ReadOnlyError;
1384  
1385              case self::AS_RATE_LIMITED:
1386                  throw new ThrottledError();
1387  
1388              case self::AS_NO_CREATE_PERMISSION:
1389                  $permission = $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage';
1390                  throw new PermissionsError( $permission );
1391  
1392              case self::AS_NO_CHANGE_CONTENT_MODEL:
1393                  throw new PermissionsError( 'editcontentmodel' );
1394  
1395              default:
1396                  // We don't recognize $status->value. The only way that can happen
1397                  // is if an extension hook aborted from inside ArticleSave.
1398                  // Render the status object into $this->hookError
1399                  // FIXME this sucks, we should just use the Status object throughout
1400                  $this->hookError = '<div class="error">' . $status->getWikitext() .
1401                      '</div>';
1402                  return true;
1403          }
1404      }
1405  
1406      /**
1407       * Run hooks that can filter edits just before they get saved.
1408       *
1409       * @param Content $content The Content to filter.
1410       * @param Status $status For reporting the outcome to the caller
1411       * @param User $user The user performing the edit
1412       *
1413       * @return bool
1414       */
1415  	protected function runPostMergeFilters( Content $content, Status $status, User $user ) {
1416          // Run old style post-section-merge edit filter
1417          if ( !ContentHandler::runLegacyHooks( 'EditFilterMerged',
1418              array( $this, $content, &$this->hookError, $this->summary ) ) ) {
1419  
1420              # Error messages etc. could be handled within the hook...
1421              $status->fatal( 'hookaborted' );
1422              $status->value = self::AS_HOOK_ERROR;
1423              return false;
1424          } elseif ( $this->hookError != '' ) {
1425              # ...or the hook could be expecting us to produce an error
1426              $status->fatal( 'hookaborted' );
1427              $status->value = self::AS_HOOK_ERROR_EXPECTED;
1428              return false;
1429          }
1430  
1431          // Run new style post-section-merge edit filter
1432          if ( !wfRunHooks( 'EditFilterMergedContent',
1433              array( $this->mArticle->getContext(), $content, $status, $this->summary,
1434                  $user, $this->minoredit ) ) ) {
1435  
1436              # Error messages etc. could be handled within the hook...
1437              // XXX: $status->value may already be something informative...
1438              $this->hookError = $status->getWikiText();
1439              $status->fatal( 'hookaborted' );
1440              $status->value = self::AS_HOOK_ERROR;
1441              return false;
1442          } elseif ( !$status->isOK() ) {
1443              # ...or the hook could be expecting us to produce an error
1444              // FIXME this sucks, we should just use the Status object throughout
1445              $this->hookError = $status->getWikiText();
1446              $status->fatal( 'hookaborted' );
1447              $status->value = self::AS_HOOK_ERROR_EXPECTED;
1448              return false;
1449          }
1450  
1451          return true;
1452      }
1453  
1454      /**
1455       * Return the summary to be used for a new section.
1456       *
1457       * @param string $sectionanchor Set to the section anchor text
1458       * @return string
1459       */
1460  	private function newSectionSummary( &$sectionanchor = null ) {
1461          global $wgParser;
1462  
1463          if ( $this->sectiontitle !== '' ) {
1464              $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
1465              // If no edit summary was specified, create one automatically from the section
1466              // title and have it link to the new section. Otherwise, respect the summary as
1467              // passed.
1468              if ( $this->summary === '' ) {
1469                  $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
1470                  return wfMessage( 'newsectionsummary' )
1471                      ->rawParams( $cleanSectionTitle )->inContentLanguage()->text();
1472              }
1473          } elseif ( $this->summary !== '' ) {
1474              $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
1475              # This is a new section, so create a link to the new section
1476              # in the revision summary.
1477              $cleanSummary = $wgParser->stripSectionName( $this->summary );
1478              return wfMessage( 'newsectionsummary' )
1479                  ->rawParams( $cleanSummary )->inContentLanguage()->text();
1480          }
1481          return $this->summary;
1482      }
1483  
1484      /**
1485       * Attempt submission (no UI)
1486       *
1487       * @param array $result Array to add statuses to, currently with the
1488       *   possible keys:
1489       *   - spam (string): Spam string from content if any spam is detected by
1490       *     matchSpamRegex.
1491       *   - sectionanchor (string): Section anchor for a section save.
1492       *   - nullEdit (boolean): Set if doEditContent is OK.  True if null edit,
1493       *     false otherwise.
1494       *   - redirect (bool): Set if doEditContent is OK. True if resulting
1495       *     revision is a redirect.
1496       * @param bool $bot True if edit is being made under the bot right.
1497       *
1498       * @return Status Status object, possibly with a message, but always with
1499       *   one of the AS_* constants in $status->value,
1500       *
1501       * @todo FIXME: This interface is TERRIBLE, but hard to get rid of due to
1502       *   various error display idiosyncrasies. There are also lots of cases
1503       *   where error metadata is set in the object and retrieved later instead
1504       *   of being returned, e.g. AS_CONTENT_TOO_BIG and
1505       *   AS_BLOCKED_PAGE_FOR_USER. All that stuff needs to be cleaned up some
1506       * time.
1507       */
1508  	function internalAttemptSave( &$result, $bot = false ) {
1509          global $wgUser, $wgRequest, $wgParser, $wgMaxArticleSize;
1510  
1511          $status = Status::newGood();
1512  
1513          wfProfileIn( __METHOD__ );
1514          wfProfileIn( __METHOD__ . '-checks' );
1515  
1516          if ( !wfRunHooks( 'EditPage::attemptSave', array( $this ) ) ) {
1517              wfDebug( "Hook 'EditPage::attemptSave' aborted article saving\n" );
1518              $status->fatal( 'hookaborted' );
1519              $status->value = self::AS_HOOK_ERROR;
1520              wfProfileOut( __METHOD__ . '-checks' );
1521              wfProfileOut( __METHOD__ );
1522              return $status;
1523          }
1524  
1525          $spam = $wgRequest->getText( 'wpAntispam' );
1526          if ( $spam !== '' ) {
1527              wfDebugLog(
1528                  'SimpleAntiSpam',
1529                  $wgUser->getName() .
1530                  ' editing "' .
1531                  $this->mTitle->getPrefixedText() .
1532                  '" submitted bogus field "' .
1533                  $spam .
1534                  '"'
1535              );
1536              $status->fatal( 'spamprotectionmatch', false );
1537              $status->value = self::AS_SPAM_ERROR;
1538              wfProfileOut( __METHOD__ . '-checks' );
1539              wfProfileOut( __METHOD__ );
1540              return $status;
1541          }
1542  
1543          try {
1544              # Construct Content object
1545              $textbox_content = $this->toEditContent( $this->textbox1 );
1546          } catch ( MWContentSerializationException $ex ) {
1547              $status->fatal(
1548                  'content-failed-to-parse',
1549                  $this->contentModel,
1550                  $this->contentFormat,
1551                  $ex->getMessage()
1552              );
1553              $status->value = self::AS_PARSE_ERROR;
1554              wfProfileOut( __METHOD__ . '-checks' );
1555              wfProfileOut( __METHOD__ );
1556              return $status;
1557          }
1558  
1559          # Check image redirect
1560          if ( $this->mTitle->getNamespace() == NS_FILE &&
1561              $textbox_content->isRedirect() &&
1562              !$wgUser->isAllowed( 'upload' )
1563          ) {
1564                  $code = $wgUser->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED;
1565                  $status->setResult( false, $code );
1566  
1567                  wfProfileOut( __METHOD__ . '-checks' );
1568                  wfProfileOut( __METHOD__ );
1569  
1570                  return $status;
1571          }
1572  
1573          # Check for spam
1574          $match = self::matchSummarySpamRegex( $this->summary );
1575          if ( $match === false && $this->section == 'new' ) {
1576              # $wgSpamRegex is enforced on this new heading/summary because, unlike
1577              # regular summaries, it is added to the actual wikitext.
1578              if ( $this->sectiontitle !== '' ) {
1579                  # This branch is taken when the API is used with the 'sectiontitle' parameter.
1580                  $match = self::matchSpamRegex( $this->sectiontitle );
1581              } else {
1582                  # This branch is taken when the "Add Topic" user interface is used, or the API
1583                  # is used with the 'summary' parameter.
1584                  $match = self::matchSpamRegex( $this->summary );
1585              }
1586          }
1587          if ( $match === false ) {
1588              $match = self::matchSpamRegex( $this->textbox1 );
1589          }
1590          if ( $match !== false ) {
1591              $result['spam'] = $match;
1592              $ip = $wgRequest->getIP();
1593              $pdbk = $this->mTitle->getPrefixedDBkey();
1594              $match = str_replace( "\n", '', $match );
1595              wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" );
1596              $status->fatal( 'spamprotectionmatch', $match );
1597              $status->value = self::AS_SPAM_ERROR;
1598              wfProfileOut( __METHOD__ . '-checks' );
1599              wfProfileOut( __METHOD__ );
1600              return $status;
1601          }
1602          if ( !wfRunHooks(
1603              'EditFilter',
1604              array( $this, $this->textbox1, $this->section, &$this->hookError, $this->summary ) )
1605          ) {
1606              # Error messages etc. could be handled within the hook...
1607              $status->fatal( 'hookaborted' );
1608              $status->value = self::AS_HOOK_ERROR;
1609              wfProfileOut( __METHOD__ . '-checks' );
1610              wfProfileOut( __METHOD__ );
1611              return $status;
1612          } elseif ( $this->hookError != '' ) {
1613              # ...or the hook could be expecting us to produce an error
1614              $status->fatal( 'hookaborted' );
1615              $status->value = self::AS_HOOK_ERROR_EXPECTED;
1616              wfProfileOut( __METHOD__ . '-checks' );
1617              wfProfileOut( __METHOD__ );
1618              return $status;
1619          }
1620  
1621          if ( $wgUser->isBlockedFrom( $this->mTitle, false ) ) {
1622              // Auto-block user's IP if the account was "hard" blocked
1623              $wgUser->spreadAnyEditBlock();
1624              # Check block state against master, thus 'false'.
1625              $status->setResult( false, self::AS_BLOCKED_PAGE_FOR_USER );
1626              wfProfileOut( __METHOD__ . '-checks' );
1627              wfProfileOut( __METHOD__ );
1628              return $status;
1629          }
1630  
1631          $this->kblength = (int)( strlen( $this->textbox1 ) / 1024 );
1632          if ( $this->kblength > $wgMaxArticleSize ) {
1633              // Error will be displayed by showEditForm()
1634              $this->tooBig = true;
1635              $status->setResult( false, self::AS_CONTENT_TOO_BIG );
1636              wfProfileOut( __METHOD__ . '-checks' );
1637              wfProfileOut( __METHOD__ );
1638              return $status;
1639          }
1640  
1641          if ( !$wgUser->isAllowed( 'edit' ) ) {
1642              if ( $wgUser->isAnon() ) {
1643                  $status->setResult( false, self::AS_READ_ONLY_PAGE_ANON );
1644                  wfProfileOut( __METHOD__ . '-checks' );
1645                  wfProfileOut( __METHOD__ );
1646                  return $status;
1647              } else {
1648                  $status->fatal( 'readonlytext' );
1649                  $status->value = self::AS_READ_ONLY_PAGE_LOGGED;
1650                  wfProfileOut( __METHOD__ . '-checks' );
1651                  wfProfileOut( __METHOD__ );
1652                  return $status;
1653              }
1654          }
1655  
1656          if ( $this->contentModel !== $this->mTitle->getContentModel()
1657              && !$wgUser->isAllowed( 'editcontentmodel' )
1658          ) {
1659              $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
1660              wfProfileOut( __METHOD__ . '-checks' );
1661              wfProfileOut( __METHOD__ );
1662              return $status;
1663          }
1664  
1665          if ( wfReadOnly() ) {
1666              $status->fatal( 'readonlytext' );
1667              $status->value = self::AS_READ_ONLY_PAGE;
1668              wfProfileOut( __METHOD__ . '-checks' );
1669              wfProfileOut( __METHOD__ );
1670              return $status;
1671          }
1672          if ( $wgUser->pingLimiter() || $wgUser->pingLimiter( 'linkpurge', 0 ) ) {
1673              $status->fatal( 'actionthrottledtext' );
1674              $status->value = self::AS_RATE_LIMITED;
1675              wfProfileOut( __METHOD__ . '-checks' );
1676              wfProfileOut( __METHOD__ );
1677              return $status;
1678          }
1679  
1680          # If the article has been deleted while editing, don't save it without
1681          # confirmation
1682          if ( $this->wasDeletedSinceLastEdit() && !$this->recreate ) {
1683              $status->setResult( false, self::AS_ARTICLE_WAS_DELETED );
1684              wfProfileOut( __METHOD__ . '-checks' );
1685              wfProfileOut( __METHOD__ );
1686              return $status;
1687          }
1688  
1689          wfProfileOut( __METHOD__ . '-checks' );
1690  
1691          # Load the page data from the master. If anything changes in the meantime,
1692          # we detect it by using page_latest like a token in a 1 try compare-and-swap.
1693          $this->mArticle->loadPageData( 'fromdbmaster' );
1694          $new = !$this->mArticle->exists();
1695  
1696          if ( $new ) {
1697              // Late check for create permission, just in case *PARANOIA*
1698              if ( !$this->mTitle->userCan( 'create', $wgUser ) ) {
1699                  $status->fatal( 'nocreatetext' );
1700                  $status->value = self::AS_NO_CREATE_PERMISSION;
1701                  wfDebug( __METHOD__ . ": no create permission\n" );
1702                  wfProfileOut( __METHOD__ );
1703                  return $status;
1704              }
1705  
1706              // Don't save a new page if it's blank or if it's a MediaWiki:
1707              // message with content equivalent to default (allow empty pages
1708              // in this case to disable messages, see bug 50124)
1709              $defaultMessageText = $this->mTitle->getDefaultMessageText();
1710              if ( $this->mTitle->getNamespace() === NS_MEDIAWIKI && $defaultMessageText !== false ) {
1711                  $defaultText = $defaultMessageText;
1712              } else {
1713                  $defaultText = '';
1714              }
1715  
1716              if ( !$this->allowBlankArticle && $this->textbox1 === $defaultText ) {
1717                  $this->blankArticle = true;
1718                  $status->fatal( 'blankarticle' );
1719                  $status->setResult( false, self::AS_BLANK_ARTICLE );
1720                  wfProfileOut( __METHOD__ );
1721                  return $status;
1722              }
1723  
1724              if ( !$this->runPostMergeFilters( $textbox_content, $status, $wgUser ) ) {
1725                  wfProfileOut( __METHOD__ );
1726                  return $status;
1727              }
1728  
1729              $content = $textbox_content;
1730  
1731              $result['sectionanchor'] = '';
1732              if ( $this->section == 'new' ) {
1733                  if ( $this->sectiontitle !== '' ) {
1734                      // Insert the section title above the content.
1735                      $content = $content->addSectionHeader( $this->sectiontitle );
1736                  } elseif ( $this->summary !== '' ) {
1737                      // Insert the section title above the content.
1738                      $content = $content->addSectionHeader( $this->summary );
1739                  }
1740                  $this->summary = $this->newSectionSummary( $result['sectionanchor'] );
1741              }
1742  
1743              $status->value = self::AS_SUCCESS_NEW_ARTICLE;
1744  
1745          } else { # not $new
1746  
1747              # Article exists. Check for edit conflict.
1748  
1749              $this->mArticle->clear(); # Force reload of dates, etc.
1750              $timestamp = $this->mArticle->getTimestamp();
1751  
1752              wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
1753  
1754              if ( $timestamp != $this->edittime ) {
1755                  $this->isConflict = true;
1756                  if ( $this->section == 'new' ) {
1757                      if ( $this->mArticle->getUserText() == $wgUser->getName() &&
1758                          $this->mArticle->getComment() == $this->newSectionSummary()
1759                      ) {
1760                          // Probably a duplicate submission of a new comment.
1761                          // This can happen when squid resends a request after
1762                          // a timeout but the first one actually went through.
1763                          wfDebug( __METHOD__
1764                              . ": duplicate new section submission; trigger edit conflict!\n" );
1765                      } else {
1766                          // New comment; suppress conflict.
1767                          $this->isConflict = false;
1768                          wfDebug( __METHOD__ . ": conflict suppressed; new section\n" );
1769                      }
1770                  } elseif ( $this->section == ''
1771                      && Revision::userWasLastToEdit(
1772                          DB_MASTER, $this->mTitle->getArticleID(),
1773                          $wgUser->getId(), $this->edittime
1774                      )
1775                  ) {
1776                      # Suppress edit conflict with self, except for section edits where merging is required.
1777                      wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" );
1778                      $this->isConflict = false;
1779                  }
1780              }
1781  
1782              // If sectiontitle is set, use it, otherwise use the summary as the section title.
1783              if ( $this->sectiontitle !== '' ) {
1784                  $sectionTitle = $this->sectiontitle;
1785              } else {
1786                  $sectionTitle = $this->summary;
1787              }
1788  
1789              $content = null;
1790  
1791              if ( $this->isConflict ) {
1792                  wfDebug( __METHOD__
1793                      . ": conflict! getting section '{$this->section}' for time '{$this->edittime}'"
1794                      . " (article time '{$timestamp}')\n" );
1795  
1796                  $content = $this->mArticle->replaceSectionContent(
1797                      $this->section,
1798                      $textbox_content,
1799                      $sectionTitle,
1800                      $this->edittime
1801                  );
1802              } else {
1803                  wfDebug( __METHOD__ . ": getting section '{$this->section}'\n" );
1804                  $content = $this->mArticle->replaceSectionContent(
1805                      $this->section,
1806                      $textbox_content,
1807                      $sectionTitle
1808                  );
1809              }
1810  
1811              if ( is_null( $content ) ) {
1812                  wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
1813                  $this->isConflict = true;
1814                  $content = $textbox_content; // do not try to merge here!
1815              } elseif ( $this->isConflict ) {
1816                  # Attempt merge
1817                  if ( $this->mergeChangesIntoContent( $content ) ) {
1818                      // Successful merge! Maybe we should tell the user the good news?
1819                      $this->isConflict = false;
1820                      wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
1821                  } else {
1822                      $this->section = '';
1823                      $this->textbox1 = ContentHandler::getContentText( $content );
1824                      wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
1825                  }
1826              }
1827  
1828              if ( $this->isConflict ) {
1829                  $status->setResult( false, self::AS_CONFLICT_DETECTED );
1830                  wfProfileOut( __METHOD__ );
1831                  return $status;
1832              }
1833  
1834              if ( !$this->runPostMergeFilters( $content, $status, $wgUser ) ) {
1835                  wfProfileOut( __METHOD__ );
1836                  return $status;
1837              }
1838  
1839              if ( $this->section == 'new' ) {
1840                  // Handle the user preference to force summaries here
1841                  if ( !$this->allowBlankSummary && trim( $this->summary ) == '' ) {
1842                      $this->missingSummary = true;
1843                      $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
1844                      $status->value = self::AS_SUMMARY_NEEDED;
1845                      wfProfileOut( __METHOD__ );
1846                      return $status;
1847                  }
1848  
1849                  // Do not allow the user to post an empty comment
1850                  if ( $this->textbox1 == '' ) {
1851                      $this->missingComment = true;
1852                      $status->fatal( 'missingcommenttext' );
1853                      $status->value = self::AS_TEXTBOX_EMPTY;
1854                      wfProfileOut( __METHOD__ );
1855                      return $status;
1856                  }
1857              } elseif ( !$this->allowBlankSummary
1858                  && !$content->equals( $this->getOriginalContent( $wgUser ) )
1859                  && !$content->isRedirect()
1860                  && md5( $this->summary ) == $this->autoSumm
1861              ) {
1862                  $this->missingSummary = true;
1863                  $status->fatal( 'missingsummary' );
1864                  $status->value = self::AS_SUMMARY_NEEDED;
1865                  wfProfileOut( __METHOD__ );
1866                  return $status;
1867              }
1868  
1869              # All's well
1870              wfProfileIn( __METHOD__ . '-sectionanchor' );
1871              $sectionanchor = '';
1872              if ( $this->section == 'new' ) {
1873                  $this->summary = $this->newSectionSummary( $sectionanchor );
1874              } elseif ( $this->section != '' ) {
1875                  # Try to get a section anchor from the section source, redirect
1876                  # to edited section if header found.
1877                  # XXX: Might be better to integrate this into Article::replaceSection
1878                  # for duplicate heading checking and maybe parsing.
1879                  $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
1880                  # We can't deal with anchors, includes, html etc in the header for now,
1881                  # headline would need to be parsed to improve this.
1882                  if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
1883                      $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $matches[2] );
1884                  }
1885              }
1886              $result['sectionanchor'] = $sectionanchor;
1887              wfProfileOut( __METHOD__ . '-sectionanchor' );
1888  
1889              // Save errors may fall down to the edit form, but we've now
1890              // merged the section into full text. Clear the section field
1891              // so that later submission of conflict forms won't try to
1892              // replace that into a duplicated mess.
1893              $this->textbox1 = $this->toEditText( $content );
1894              $this->section = '';
1895  
1896              $status->value = self::AS_SUCCESS_UPDATE;
1897          }
1898  
1899          // Check for length errors again now that the section is merged in
1900          $this->kblength = (int)( strlen( $this->toEditText( $content ) ) / 1024 );
1901          if ( $this->kblength > $wgMaxArticleSize ) {
1902              $this->tooBig = true;
1903              $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
1904              wfProfileOut( __METHOD__ );
1905              return $status;
1906          }
1907  
1908          $flags = EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY |
1909              ( $new ? EDIT_NEW : EDIT_UPDATE ) |
1910              ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
1911              ( $bot ? EDIT_FORCE_BOT : 0 );
1912  
1913          $doEditStatus = $this->mArticle->doEditContent(
1914              $content,
1915              $this->summary,
1916              $flags,
1917              false,
1918              null,
1919              $content->getDefaultFormat()
1920          );
1921  
1922          if ( !$doEditStatus->isOK() ) {
1923              // Failure from doEdit()
1924              // Show the edit conflict page for certain recognized errors from doEdit(),
1925              // but don't show it for errors from extension hooks
1926              $errors = $doEditStatus->getErrorsArray();
1927              if ( in_array( $errors[0][0],
1928                      array( 'edit-gone-missing', 'edit-conflict', 'edit-already-exists' ) )
1929              ) {
1930                  $this->isConflict = true;
1931                  // Destroys data doEdit() put in $status->value but who cares
1932                  $doEditStatus->value = self::AS_END;
1933              }
1934              wfProfileOut( __METHOD__ );
1935              return $doEditStatus;
1936          }
1937  
1938          $result['nullEdit'] = $doEditStatus->hasMessage( 'edit-no-change' );
1939          if ( $result['nullEdit'] ) {
1940              // We don't know if it was a null edit until now, so increment here
1941              $wgUser->pingLimiter( 'linkpurge' );
1942          }
1943          $result['redirect'] = $content->isRedirect();
1944          $this->updateWatchlist();
1945          wfProfileOut( __METHOD__ );
1946          return $status;
1947      }
1948  
1949      /**
1950       * Register the change of watch status
1951       */
1952  	protected function updateWatchlist() {
1953          global $wgUser;
1954  
1955          if ( $wgUser->isLoggedIn()
1956              && $this->watchthis != $wgUser->isWatched( $this->mTitle, WatchedItem::IGNORE_USER_RIGHTS )
1957          ) {
1958              $fname = __METHOD__;
1959              $title = $this->mTitle;
1960              $watch = $this->watchthis;
1961  
1962              // Do this in its own transaction to reduce contention...
1963              $dbw = wfGetDB( DB_MASTER );
1964              $dbw->onTransactionIdle( function () use ( $dbw, $title, $watch, $wgUser, $fname ) {
1965                  WatchAction::doWatchOrUnwatch( $watch, $title, $wgUser );
1966              } );
1967          }
1968      }
1969  
1970      /**
1971       * Attempts to do 3-way merge of edit content with a base revision
1972       * and current content, in case of edit conflict, in whichever way appropriate
1973       * for the content type.
1974       *
1975       * @since 1.21
1976       *
1977       * @param Content $editContent
1978       *
1979       * @return bool
1980       */
1981  	private function mergeChangesIntoContent( &$editContent ) {
1982          wfProfileIn( __METHOD__ );
1983  
1984          $db = wfGetDB( DB_MASTER );
1985  
1986          // This is the revision the editor started from
1987          $baseRevision = $this->getBaseRevision();
1988          $baseContent = $baseRevision ? $baseRevision->getContent() : null;
1989  
1990          if ( is_null( $baseContent ) ) {
1991              wfProfileOut( __METHOD__ );
1992              return false;
1993          }
1994  
1995          // The current state, we want to merge updates into it
1996          $currentRevision = Revision::loadFromTitle( $db, $this->mTitle );
1997          $currentContent = $currentRevision ? $currentRevision->getContent() : null;
1998  
1999          if ( is_null( $currentContent ) ) {
2000              wfProfileOut( __METHOD__ );
2001              return false;
2002          }
2003  
2004          $handler = ContentHandler::getForModelID( $baseContent->getModel() );
2005  
2006          $result = $handler->merge3( $baseContent, $editContent, $currentContent );
2007  
2008          if ( $result ) {
2009              $editContent = $result;
2010              wfProfileOut( __METHOD__ );
2011              return true;
2012          }
2013  
2014          wfProfileOut( __METHOD__ );
2015          return false;
2016      }
2017  
2018      /**
2019       * @return Revision
2020       */
2021  	function getBaseRevision() {
2022          if ( !$this->mBaseRevision ) {
2023              $db = wfGetDB( DB_MASTER );
2024              $this->mBaseRevision = Revision::loadFromTimestamp(
2025                  $db, $this->mTitle, $this->edittime );
2026          }
2027          return $this->mBaseRevision;
2028      }
2029  
2030      /**
2031       * Check given input text against $wgSpamRegex, and return the text of the first match.
2032       *
2033       * @param string $text
2034       *
2035       * @return string|bool Matching string or false
2036       */
2037  	public static function matchSpamRegex( $text ) {
2038          global $wgSpamRegex;
2039          // For back compatibility, $wgSpamRegex may be a single string or an array of regexes.
2040          $regexes = (array)$wgSpamRegex;
2041          return self::matchSpamRegexInternal( $text, $regexes );
2042      }
2043  
2044      /**
2045       * Check given input text against $wgSummarySpamRegex, and return the text of the first match.
2046       *
2047       * @param string $text
2048       *
2049       * @return string|bool Matching string or false
2050       */
2051  	public static function matchSummarySpamRegex( $text ) {
2052          global $wgSummarySpamRegex;
2053          $regexes = (array)$wgSummarySpamRegex;
2054          return self::matchSpamRegexInternal( $text, $regexes );
2055      }
2056  
2057      /**
2058       * @param string $text
2059       * @param array $regexes
2060       * @return bool|string
2061       */
2062  	protected static function matchSpamRegexInternal( $text, $regexes ) {
2063          foreach ( $regexes as $regex ) {
2064              $matches = array();
2065              if ( preg_match( $regex, $text, $matches ) ) {
2066                  return $matches[0];
2067              }
2068          }
2069          return false;
2070      }
2071  
2072  	function setHeaders() {
2073          global $wgOut, $wgUser;
2074  
2075          $wgOut->addModules( 'mediawiki.action.edit' );
2076          $wgOut->addModuleStyles( 'mediawiki.action.edit.styles' );
2077  
2078          if ( $wgUser->getOption( 'uselivepreview', false ) ) {
2079              $wgOut->addModules( 'mediawiki.action.edit.preview' );
2080          }
2081  
2082          if ( $wgUser->getOption( 'useeditwarning', false ) ) {
2083              $wgOut->addModules( 'mediawiki.action.edit.editWarning' );
2084          }
2085  
2086          $wgOut->setRobotPolicy( 'noindex,nofollow' );
2087  
2088          # Enabled article-related sidebar, toplinks, etc.
2089          $wgOut->setArticleRelated( true );
2090  
2091          $contextTitle = $this->getContextTitle();
2092          if ( $this->isConflict ) {
2093              $msg = 'editconflict';
2094          } elseif ( $contextTitle->exists() && $this->section != '' ) {
2095              $msg = $this->section == 'new' ? 'editingcomment' : 'editingsection';
2096          } else {
2097              $msg = $contextTitle->exists()
2098                  || ( $contextTitle->getNamespace() == NS_MEDIAWIKI
2099                      && $contextTitle->getDefaultMessageText() !== false
2100                  )
2101                  ? 'editing'
2102                  : 'creating';
2103          }
2104  
2105          # Use the title defined by DISPLAYTITLE magic word when present
2106          $displayTitle = isset( $this->mParserOutput ) ? $this->mParserOutput->getDisplayTitle() : false;
2107          if ( $displayTitle === false ) {
2108              $displayTitle = $contextTitle->getPrefixedText();
2109          }
2110          $wgOut->setPageTitle( wfMessage( $msg, $displayTitle ) );
2111      }
2112  
2113      /**
2114       * Show all applicable editing introductions
2115       */
2116  	protected function showIntro() {
2117          global $wgOut, $wgUser;
2118          if ( $this->suppressIntro ) {
2119              return;
2120          }
2121  
2122          $namespace = $this->mTitle->getNamespace();
2123  
2124          if ( $namespace == NS_MEDIAWIKI ) {
2125              # Show a warning if editing an interface message
2126              $wgOut->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' );
2127          } elseif ( $namespace == NS_FILE ) {
2128              # Show a hint to shared repo
2129              $file = wfFindFile( $this->mTitle );
2130              if ( $file && !$file->isLocal() ) {
2131                  $descUrl = $file->getDescriptionUrl();
2132                  # there must be a description url to show a hint to shared repo
2133                  if ( $descUrl ) {
2134                      if ( !$this->mTitle->exists() ) {
2135                          $wgOut->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>", array(
2136                                      'sharedupload-desc-create', $file->getRepo()->getDisplayName(), $descUrl
2137                          ) );
2138                      } else {
2139                          $wgOut->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>", array(
2140                                      'sharedupload-desc-edit', $file->getRepo()->getDisplayName(), $descUrl
2141                          ) );
2142                      }
2143                  }
2144              }
2145          }
2146  
2147          # Show a warning message when someone creates/edits a user (talk) page but the user does not exist
2148          # Show log extract when the user is currently blocked
2149          if ( $namespace == NS_USER || $namespace == NS_USER_TALK ) {
2150              $parts = explode( '/', $this->mTitle->getText(), 2 );
2151              $username = $parts[0];
2152              $user = User::newFromName( $username, false /* allow IP users*/ );
2153              $ip = User::isIP( $username );
2154              $block = Block::newFromTarget( $user, $user );
2155              if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist
2156                  $wgOut->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
2157                      array( 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ) );
2158              } elseif ( !is_null( $block ) && $block->getType() != Block::TYPE_AUTO ) { # Show log extract if the user is currently blocked
2159                  LogEventsList::showLogExtract(
2160                      $wgOut,
2161                      'block',
2162                      MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget(),
2163                      '',
2164                      array(
2165                          'lim' => 1,
2166                          'showIfEmpty' => false,
2167                          'msgKey' => array(
2168                              'blocked-notice-logextract',
2169                              $user->getName() # Support GENDER in notice
2170                          )
2171                      )
2172                  );
2173              }
2174          }
2175          # Try to add a custom edit intro, or use the standard one if this is not possible.
2176          if ( !$this->showCustomIntro() && !$this->mTitle->exists() ) {
2177              $helpLink = wfExpandUrl( Skin::makeInternalOrExternalUrl(
2178                  wfMessage( 'helppage' )->inContentLanguage()->text()
2179              ) );
2180              if ( $wgUser->isLoggedIn() ) {
2181                  $wgOut->wrapWikiMsg(
2182                      // Suppress the external link icon, consider the help url an internal one
2183                      "<div class=\"mw-newarticletext plainlinks\">\n$1\n</div>",
2184                      array(
2185                          'newarticletext',
2186                          $helpLink
2187                      )
2188                  );
2189              } else {
2190                  $wgOut->wrapWikiMsg(
2191                      // Suppress the external link icon, consider the help url an internal one
2192                      "<div class=\"mw-newarticletextanon plainlinks\">\n$1\n</div>",
2193                      array(
2194                          'newarticletextanon',
2195                          $helpLink
2196                      )
2197                  );
2198              }
2199          }
2200          # Give a notice if the user is editing a deleted/moved page...
2201          if ( !$this->mTitle->exists() ) {
2202              LogEventsList::showLogExtract( $wgOut, array( 'delete', 'move' ), $this->mTitle,
2203                  '',
2204                  array(
2205                      'lim' => 10,
2206                      'conds' => array( "log_action != 'revision'" ),
2207                      'showIfEmpty' => false,
2208                      'msgKey' => array( 'recreate-moveddeleted-warn' )
2209                  )
2210              );
2211          }
2212      }
2213  
2214      /**
2215       * Attempt to show a custom editing introduction, if supplied
2216       *
2217       * @return bool
2218       */
2219  	protected function showCustomIntro() {
2220          if ( $this->editintro ) {
2221              $title = Title::newFromText( $this->editintro );
2222              if ( $title instanceof Title && $title->exists() && $title->userCan( 'read' ) ) {
2223                  global $wgOut;
2224                  // Added using template syntax, to take <noinclude>'s into account.
2225                  $wgOut->addWikiTextTitleTidy( '<div class="mw-editintro">{{:' . $title->getFullText() . '}}</div>', $this->mTitle );
2226                  return true;
2227              }
2228          }
2229          return false;
2230      }
2231  
2232      /**
2233       * Gets an editable textual representation of $content.
2234       * The textual representation can be turned by into a Content object by the
2235       * toEditContent() method.
2236       *
2237       * If $content is null or false or a string, $content is returned unchanged.
2238       *
2239       * If the given Content object is not of a type that can be edited using
2240       * the text base EditPage, an exception will be raised. Set
2241       * $this->allowNonTextContent to true to allow editing of non-textual
2242       * content.
2243       *
2244       * @param Content|null|bool|string $content
2245       * @return string The editable text form of the content.
2246       *
2247       * @throws MWException If $content is not an instance of TextContent and
2248       *   $this->allowNonTextContent is not true.
2249       */
2250  	protected function toEditText( $content ) {
2251          if ( $content === null || $content === false ) {
2252              return $content;
2253          }
2254  
2255          if ( is_string( $content ) ) {
2256              return $content;
2257          }
2258  
2259          if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2260              throw new MWException( 'This content model is not supported: '
2261                  . ContentHandler::getLocalizedName( $content->getModel() ) );
2262          }
2263  
2264          return $content->serialize( $this->contentFormat );
2265      }
2266  
2267      /**
2268       * Turns the given text into a Content object by unserializing it.
2269       *
2270       * If the resulting Content object is not of a type that can be edited using
2271       * the text base EditPage, an exception will be raised. Set
2272       * $this->allowNonTextContent to true to allow editing of non-textual
2273       * content.
2274       *
2275       * @param string|null|bool $text Text to unserialize
2276       * @return Content The content object created from $text. If $text was false
2277       *   or null, false resp. null will be  returned instead.
2278       *
2279       * @throws MWException If unserializing the text results in a Content
2280       *   object that is not an instance of TextContent and
2281       *   $this->allowNonTextContent is not true.
2282       */
2283  	protected function toEditContent( $text ) {
2284          if ( $text === false || $text === null ) {
2285              return $text;
2286          }
2287  
2288          $content = ContentHandler::makeContent( $text, $this->getTitle(),
2289              $this->contentModel, $this->contentFormat );
2290  
2291          if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2292              throw new MWException( 'This content model is not supported: '
2293                  . ContentHandler::getLocalizedName( $content->getModel() ) );
2294          }
2295  
2296          return $content;
2297      }
2298  
2299      /**
2300       * Send the edit form and related headers to $wgOut
2301       * @param callable|null $formCallback That takes an OutputPage parameter; will be called
2302       *     during form output near the top, for captchas and the like.
2303       */
2304  	function showEditForm( $formCallback = null ) {
2305          global $wgOut, $wgUser;
2306  
2307          wfProfileIn( __METHOD__ );
2308  
2309          # need to parse the preview early so that we know which templates are used,
2310          # otherwise users with "show preview after edit box" will get a blank list
2311          # we parse this near the beginning so that setHeaders can do the title
2312          # setting work instead of leaving it in getPreviewText
2313          $previewOutput = '';
2314          if ( $this->formtype == 'preview' ) {
2315              $previewOutput = $this->getPreviewText();
2316          }
2317  
2318          wfRunHooks( 'EditPage::showEditForm:initial', array( &$this, &$wgOut ) );
2319  
2320          $this->setHeaders();
2321  
2322          if ( $this->showHeader() === false ) {
2323              wfProfileOut( __METHOD__ );
2324              return;
2325          }
2326  
2327          $wgOut->addHTML( $this->editFormPageTop );
2328  
2329          if ( $wgUser->getOption( 'previewontop' ) ) {
2330              $this->displayPreviewArea( $previewOutput, true );
2331          }
2332  
2333          $wgOut->addHTML( $this->editFormTextTop );
2334  
2335          $showToolbar = true;
2336          if ( $this->wasDeletedSinceLastEdit() ) {
2337              if ( $this->formtype == 'save' ) {
2338                  // Hide the toolbar and edit area, user can click preview to get it back
2339                  // Add an confirmation checkbox and explanation.
2340                  $showToolbar = false;
2341              } else {
2342                  $wgOut->wrapWikiMsg( "<div class='error mw-deleted-while-editing'>\n$1\n</div>",
2343                      'deletedwhileediting' );
2344              }
2345          }
2346  
2347          // @todo add EditForm plugin interface and use it here!
2348          //       search for textarea1 and textares2, and allow EditForm to override all uses.
2349          $wgOut->addHTML( Html::openElement(
2350              'form',
2351              array(
2352                  'id' => self::EDITFORM_ID,
2353                  'name' => self::EDITFORM_ID,
2354                  'method' => 'post',
2355                  'action' => $this->getActionURL( $this->getContextTitle() ),
2356                  'enctype' => 'multipart/form-data'
2357              )
2358          ) );
2359  
2360          if ( is_callable( $formCallback ) ) {
2361              call_user_func_array( $formCallback, array( &$wgOut ) );
2362          }
2363  
2364          // Add an empty field to trip up spambots
2365          $wgOut->addHTML(
2366              Xml::openElement( 'div', array( 'id' => 'antispam-container', 'style' => 'display: none;' ) )
2367              . Html::rawElement(
2368                  'label',
2369                  array( 'for' => 'wpAntiSpam' ),
2370                  wfMessage( 'simpleantispam-label' )->parse()
2371              )
2372              . Xml::element(
2373                  'input',
2374                  array(
2375                      'type' => 'text',
2376                      'name' => 'wpAntispam',
2377                      'id' => 'wpAntispam',
2378                      'value' => ''
2379                  )
2380              )
2381              . Xml::closeElement( 'div' )
2382          );
2383  
2384          wfRunHooks( 'EditPage::showEditForm:fields', array( &$this, &$wgOut ) );
2385  
2386          // Put these up at the top to ensure they aren't lost on early form submission
2387          $this->showFormBeforeText();
2388  
2389          if ( $this->wasDeletedSinceLastEdit() && 'save' == $this->formtype ) {
2390              $username = $this->lastDelete->user_name;
2391              $comment = $this->lastDelete->log_comment;
2392  
2393              // It is better to not parse the comment at all than to have templates expanded in the middle
2394              // TODO: can the checkLabel be moved outside of the div so that wrapWikiMsg could be used?
2395              $key = $comment === ''
2396                  ? 'confirmrecreate-noreason'
2397                  : 'confirmrecreate';
2398              $wgOut->addHTML(
2399                  '<div class="mw-confirm-recreate">' .
2400                      wfMessage( $key, $username, "<nowiki>$comment</nowiki>" )->parse() .
2401                  Xml::checkLabel( wfMessage( 'recreate' )->text(), 'wpRecreate', 'wpRecreate', false,
2402                      array( 'title' => Linker::titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' )
2403                  ) .
2404                  '</div>'
2405              );
2406          }
2407  
2408          # When the summary is hidden, also hide them on preview/show changes
2409          if ( $this->nosummary ) {
2410              $wgOut->addHTML( Html::hidden( 'nosummary', true ) );
2411          }
2412  
2413          # If a blank edit summary was previously provided, and the appropriate
2414          # user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the
2415          # user being bounced back more than once in the event that a summary
2416          # is not required.
2417          #####
2418          # For a bit more sophisticated detection of blank summaries, hash the
2419          # automatic one and pass that in the hidden field wpAutoSummary.
2420          if ( $this->missingSummary || ( $this->section == 'new' && $this->nosummary ) ) {
2421              $wgOut->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
2422          }
2423  
2424          if ( $this->undidRev ) {
2425              $wgOut->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
2426          }
2427  
2428          if ( $this->hasPresetSummary ) {
2429              // If a summary has been preset using &summary= we don't want to prompt for
2430              // a different summary. Only prompt for a summary if the summary is blanked.
2431              // (Bug 17416)
2432              $this->autoSumm = md5( '' );
2433          }
2434  
2435          $autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary );
2436          $wgOut->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
2437  
2438          $wgOut->addHTML( Html::hidden( 'oldid', $this->oldid ) );
2439  
2440          $wgOut->addHTML( Html::hidden( 'format', $this->contentFormat ) );
2441          $wgOut->addHTML( Html::hidden( 'model', $this->contentModel ) );
2442  
2443          if ( $this->section == 'new' ) {
2444              $this->showSummaryInput( true, $this->summary );
2445              $wgOut->addHTML( $this->getSummaryPreview( true, $this->summary ) );
2446          }
2447  
2448          $wgOut->addHTML( $this->editFormTextBeforeContent );
2449  
2450          if ( !$this->isCssJsSubpage && $showToolbar && $wgUser->getOption( 'showtoolbar' ) ) {
2451              $wgOut->addHTML( EditPage::getEditToolbar() );
2452          }
2453  
2454          if ( $this->blankArticle ) {
2455              $wgOut->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
2456          }
2457  
2458          if ( $this->isConflict ) {
2459              // In an edit conflict bypass the overridable content form method
2460              // and fallback to the raw wpTextbox1 since editconflicts can't be
2461              // resolved between page source edits and custom ui edits using the
2462              // custom edit ui.
2463              $this->textbox2 = $this->textbox1;
2464  
2465              $content = $this->getCurrentContent();
2466              $this->textbox1 = $this->toEditText( $content );
2467  
2468              $this->showTextbox1();
2469          } else {
2470              $this->showContentForm();
2471          }
2472  
2473          $wgOut->addHTML( $this->editFormTextAfterContent );
2474  
2475          $this->showStandardInputs();
2476  
2477          $this->showFormAfterText();
2478  
2479          $this->showTosSummary();
2480  
2481          $this->showEditTools();
2482  
2483          $wgOut->addHTML( $this->editFormTextAfterTools . "\n" );
2484  
2485          $wgOut->addHTML( Html::rawElement( 'div', array( 'class' => 'templatesUsed' ),
2486              Linker::formatTemplates( $this->getTemplates(), $this->preview, $this->section != '' ) ) );
2487  
2488          $wgOut->addHTML( Html::rawElement( 'div', array( 'class' => 'hiddencats' ),
2489              Linker::formatHiddenCategories( $this->mArticle->getHiddenCategories() ) ) );
2490  
2491          $wgOut->addHTML( Html::rawElement( 'div', array( 'class' => 'limitreport' ),
2492              self::getPreviewLimitReport( $this->mParserOutput ) ) );
2493  
2494          $wgOut->addModules( 'mediawiki.action.edit.collapsibleFooter' );
2495  
2496          if ( $this->isConflict ) {
2497              try {
2498                  $this->showConflict();
2499              } catch ( MWContentSerializationException $ex ) {
2500                  // this can't really happen, but be nice if it does.
2501                  $msg = wfMessage(
2502                      'content-failed-to-parse',
2503                      $this->contentModel,
2504                      $this->contentFormat,
2505                      $ex->getMessage()
2506                  );
2507                  $wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
2508              }
2509          }
2510  
2511          // Marker for detecting truncated form data.  This must be the last
2512          // parameter sent in order to be of use, so do not move me.
2513          $wgOut->addHTML( Html::hidden( 'wpUltimateParam', true ) );
2514          $wgOut->addHTML( $this->editFormTextBottom . "\n</form>\n" );
2515  
2516          if ( !$wgUser->getOption( 'previewontop' ) ) {
2517              $this->displayPreviewArea( $previewOutput, false );
2518          }
2519  
2520          wfProfileOut( __METHOD__ );
2521      }
2522  
2523      /**
2524       * Extract the section title from current section text, if any.
2525       *
2526       * @param string $text
2527       * @return string|bool String or false
2528       */
2529  	public static function extractSectionTitle( $text ) {
2530          preg_match( "/^(=+)(.+)\\1\\s*(\n|$)/i", $text, $matches );
2531          if ( !empty( $matches[2] ) ) {
2532              global $wgParser;
2533              return $wgParser->stripSectionName( trim( $matches[2] ) );
2534          } else {
2535              return false;
2536          }
2537      }
2538  
2539      /**
2540       * @return bool
2541       */
2542  	protected function showHeader() {
2543          global $wgOut, $wgUser, $wgMaxArticleSize, $wgLang;
2544          global $wgAllowUserCss, $wgAllowUserJs;
2545  
2546          if ( $this->mTitle->isTalkPage() ) {
2547              $wgOut->addWikiMsg( 'talkpagetext' );
2548          }
2549  
2550          // Add edit notices
2551          $wgOut->addHTML( implode( "\n", $this->mTitle->getEditNotices( $this->oldid ) ) );
2552  
2553          if ( $this->isConflict ) {
2554              $wgOut->wrapWikiMsg( "<div class='mw-explainconflict'>\n$1\n</div>", 'explainconflict' );
2555              $this->edittime = $this->mArticle->getTimestamp();
2556          } else {
2557              if ( $this->section != '' && !$this->isSectionEditSupported() ) {
2558                  // We use $this->section to much before this and getVal('wgSection') directly in other places
2559                  // at this point we can't reset $this->section to '' to fallback to non-section editing.
2560                  // Someone is welcome to try refactoring though
2561                  $wgOut->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
2562                  return false;
2563              }
2564  
2565              if ( $this->section != '' && $this->section != 'new' ) {
2566                  if ( !$this->summary && !$this->preview && !$this->diff ) {
2567                      $sectionTitle = self::extractSectionTitle( $this->textbox1 ); //FIXME: use Content object
2568                      if ( $sectionTitle !== false ) {
2569                          $this->summary = "/* $sectionTitle */ ";
2570                      }
2571                  }
2572              }
2573  
2574              if ( $this->missingComment ) {
2575                  $wgOut->wrapWikiMsg( "<div id='mw-missingcommenttext'>\n$1\n</div>", 'missingcommenttext' );
2576              }
2577  
2578              if ( $this->missingSummary && $this->section != 'new' ) {
2579                  $wgOut->wrapWikiMsg( "<div id='mw-missingsummary'>\n$1\n</div>", 'missingsummary' );
2580              }
2581  
2582              if ( $this->missingSummary && $this->section == 'new' ) {
2583                  $wgOut->wrapWikiMsg( "<div id='mw-missingcommentheader'>\n$1\n</div>", 'missingcommentheader' );
2584              }
2585  
2586              if ( $this->blankArticle ) {
2587                  $wgOut->wrapWikiMsg( "<div id='mw-blankarticle'>\n$1\n</div>", 'blankarticle' );
2588              }
2589  
2590              if ( $this->hookError !== '' ) {
2591                  $wgOut->addWikiText( $this->hookError );
2592              }
2593  
2594              if ( !$this->checkUnicodeCompliantBrowser() ) {
2595                  $wgOut->addWikiMsg( 'nonunicodebrowser' );
2596              }
2597  
2598              if ( $this->section != 'new' ) {
2599                  $revision = $this->mArticle->getRevisionFetched();
2600                  if ( $revision ) {
2601                      // Let sysop know that this will make private content public if saved
2602  
2603                      if ( !$revision->userCan( Revision::DELETED_TEXT, $wgUser ) ) {
2604                          $wgOut->wrapWikiMsg(
2605                              "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
2606                              'rev-deleted-text-permission'
2607                          );
2608                      } elseif ( $revision->isDeleted( Revision::DELETED_TEXT ) ) {
2609                          $wgOut->wrapWikiMsg(
2610                              "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
2611                              'rev-deleted-text-view'
2612                          );
2613                      }
2614  
2615                      if ( !$revision->isCurrent() ) {
2616                          $this->mArticle->setOldSubtitle( $revision->getId() );
2617                          $wgOut->addWikiMsg( 'editingold' );
2618                      }
2619                  } elseif ( $this->mTitle->exists() ) {
2620                      // Something went wrong
2621  
2622                      $wgOut->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>\n",
2623                          array( 'missing-revision', $this->oldid ) );
2624                  }
2625              }
2626          }
2627  
2628          if ( wfReadOnly() ) {
2629              $wgOut->wrapWikiMsg(
2630                  "<div id=\"mw-read-only-warning\">\n$1\n</div>",
2631                  array( 'readonlywarning', wfReadOnlyReason() )
2632              );
2633          } elseif ( $wgUser->isAnon() ) {
2634              if ( $this->formtype != 'preview' ) {
2635                  $wgOut->wrapWikiMsg(
2636                      "<div id='mw-anon-edit-warning'>\n$1\n</div>",
2637                      array( 'anoneditwarning',
2638                          // Log-in link
2639                          '{{fullurl:Special:UserLogin|returnto={{FULLPAGENAMEE}}}}',
2640                          // Sign-up link
2641                          '{{fullurl:Special:UserLogin/signup|returnto={{FULLPAGENAMEE}}}}' )
2642                  );
2643              } else {
2644                  $wgOut->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\">\n$1</div>",
2645                      'anonpreviewwarning'
2646                  );
2647              }
2648          } else {
2649              if ( $this->isCssJsSubpage ) {
2650                  # Check the skin exists
2651                  if ( $this->isWrongCaseCssJsPage ) {
2652                      $wgOut->wrapWikiMsg(
2653                          "<div class='error' id='mw-userinvalidcssjstitle'>\n$1\n</div>",
2654                          array( 'userinvalidcssjstitle', $this->mTitle->getSkinFromCssJsSubpage() )
2655                      );
2656                  }
2657                  if ( $this->formtype !== 'preview' ) {
2658                      if ( $this->isCssSubpage && $wgAllowUserCss ) {
2659                          $wgOut->wrapWikiMsg(
2660                              "<div id='mw-usercssyoucanpreview'>\n$1\n</div>",
2661                              array( 'usercssyoucanpreview' )
2662                          );
2663                      }
2664  
2665                      if ( $this->isJsSubpage && $wgAllowUserJs ) {
2666                          $wgOut->wrapWikiMsg(
2667                              "<div id='mw-userjsyoucanpreview'>\n$1\n</div>",
2668                              array( 'userjsyoucanpreview' )
2669                          );
2670                      }
2671                  }
2672              }
2673          }
2674  
2675          if ( $this->mTitle->isProtected( 'edit' ) &&
2676              MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== array( '' )
2677          ) {
2678              # Is the title semi-protected?
2679              if ( $this->mTitle->isSemiProtected() ) {
2680                  $noticeMsg = 'semiprotectedpagewarning';
2681              } else {
2682                  # Then it must be protected based on static groups (regular)
2683                  $noticeMsg = 'protectedpagewarning';
2684              }
2685              LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle, '',
2686                  array( 'lim' => 1, 'msgKey' => array( $noticeMsg ) ) );
2687          }
2688          if ( $this->mTitle->isCascadeProtected() ) {
2689              # Is this page under cascading protection from some source pages?
2690              list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources();
2691              $notice = "<div class='mw-cascadeprotectedwarning'>\n$1\n";
2692              $cascadeSourcesCount = count( $cascadeSources );
2693              if ( $cascadeSourcesCount > 0 ) {
2694                  # Explain, and list the titles responsible
2695                  foreach ( $cascadeSources as $page ) {
2696                      $notice .= '* [[:' . $page->getPrefixedText() . "]]\n";
2697                  }
2698              }
2699              $notice .= '</div>';
2700              $wgOut->wrapWikiMsg( $notice, array( 'cascadeprotectedwarning', $cascadeSourcesCount ) );
2701          }
2702          if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) {
2703              LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle, '',
2704                  array( 'lim' => 1,
2705                      'showIfEmpty' => false,
2706                      'msgKey' => array( 'titleprotectedwarning' ),
2707                      'wrap' => "<div class=\"mw-titleprotectedwarning\">\n$1</div>" ) );
2708          }
2709  
2710          if ( $this->kblength === false ) {
2711              $this->kblength = (int)( strlen( $this->textbox1 ) / 1024 );
2712          }
2713  
2714          if ( $this->tooBig || $this->kblength > $wgMaxArticleSize ) {
2715              $wgOut->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>",
2716                  array(
2717                      'longpageerror',
2718                      $wgLang->formatNum( $this->kblength ),
2719                      $wgLang->formatNum( $wgMaxArticleSize )
2720                  )
2721              );
2722          } else {
2723              if ( !wfMessage( 'longpage-hint' )->isDisabled() ) {
2724                  $wgOut->wrapWikiMsg( "<div id='mw-edit-longpage-hint'>\n$1\n</div>",
2725                      array(
2726                          'longpage-hint',
2727                          $wgLang->formatSize( strlen( $this->textbox1 ) ),
2728                          strlen( $this->textbox1 )
2729                      )
2730                  );
2731              }
2732          }
2733          # Add header copyright warning
2734          $this->showHeaderCopyrightWarning();
2735  
2736          return true;
2737      }
2738  
2739      /**
2740       * Standard summary input and label (wgSummary), abstracted so EditPage
2741       * subclasses may reorganize the form.
2742       * Note that you do not need to worry about the label's for=, it will be
2743       * inferred by the id given to the input. You can remove them both by
2744       * passing array( 'id' => false ) to $userInputAttrs.
2745       *
2746       * @param string $summary The value of the summary input
2747       * @param string $labelText The html to place inside the label
2748       * @param array $inputAttrs Array of attrs to use on the input
2749       * @param array $spanLabelAttrs Array of attrs to use on the span inside the label
2750       *
2751       * @return array An array in the format array( $label, $input )
2752       */
2753  	function getSummaryInput( $summary = "", $labelText = null,
2754          $inputAttrs = null, $spanLabelAttrs = null
2755      ) {
2756          // Note: the maxlength is overridden in JS to 255 and to make it use UTF-8 bytes, not characters.
2757          $inputAttrs = ( is_array( $inputAttrs ) ? $inputAttrs : array() ) + array(
2758              'id' => 'wpSummary',
2759              'maxlength' => '200',
2760              'tabindex' => '1',
2761              'size' => 60,
2762              'spellcheck' => 'true',
2763          ) + Linker::tooltipAndAccesskeyAttribs( 'summary' );
2764  
2765          $spanLabelAttrs = ( is_array( $spanLabelAttrs ) ? $spanLabelAttrs : array() ) + array(
2766              'class' => $this->missingSummary ? 'mw-summarymissed' : 'mw-summary',
2767              'id' => "wpSummaryLabel"
2768          );
2769  
2770          $label = null;
2771          if ( $labelText ) {
2772              $label = Xml::tags(
2773                  'label',
2774                  $inputAttrs['id'] ? array( 'for' => $inputAttrs['id'] ) : null,
2775                  $labelText
2776              );
2777              $label = Xml::tags( 'span', $spanLabelAttrs, $label );
2778          }
2779  
2780          $input = Html::input( 'wpSummary', $summary, 'text', $inputAttrs );
2781  
2782          return array( $label, $input );
2783      }
2784  
2785      /**
2786       * @param bool $isSubjectPreview True if this is the section subject/title
2787       *   up top, or false if this is the comment summary
2788       *   down below the textarea
2789       * @param string $summary The text of the summary to display
2790       */
2791  	protected function showSummaryInput( $isSubjectPreview, $summary = "" ) {
2792          global $wgOut, $wgContLang;
2793          # Add a class if 'missingsummary' is triggered to allow styling of the summary line
2794          $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
2795          if ( $isSubjectPreview ) {
2796              if ( $this->nosummary ) {
2797                  return;
2798              }
2799          } else {
2800              if ( !$this->mShowSummaryField ) {
2801                  return;
2802              }
2803          }
2804          $summary = $wgContLang->recodeForEdit( $summary );
2805          $labelText = wfMessage( $isSubjectPreview ? 'subject' : 'summary' )->parse();
2806          list( $label, $input ) = $this->getSummaryInput(
2807              $summary,
2808              $labelText,
2809              array( 'class' => $summaryClass ),
2810              array()
2811          );
2812          $wgOut->addHTML( "{$label} {$input}" );
2813      }
2814  
2815      /**
2816       * @param bool $isSubjectPreview True if this is the section subject/title
2817       *   up top, or false if this is the comment summary
2818       *   down below the textarea
2819       * @param string $summary The text of the summary to display
2820       * @return string
2821       */
2822  	protected function getSummaryPreview( $isSubjectPreview, $summary = "" ) {
2823          // avoid spaces in preview, gets always trimmed on save
2824          $summary = trim( $summary );
2825          if ( !$summary || ( !$this->preview && !$this->diff ) ) {
2826              return "";
2827          }
2828  
2829          global $wgParser;
2830  
2831          if ( $isSubjectPreview ) {
2832              $summary = wfMessage( 'newsectionsummary' )->rawParams( $wgParser->stripSectionName( $summary ) )
2833                  ->inContentLanguage()->text();
2834          }
2835  
2836          $message = $isSubjectPreview ? 'subject-preview' : 'summary-preview';
2837  
2838          $summary = wfMessage( $message )->parse()
2839              . Linker::commentBlock( $summary, $this->mTitle, $isSubjectPreview );
2840          return Xml::tags( 'div', array( 'class' => 'mw-summary-preview' ), $summary );
2841      }
2842  
2843  	protected function showFormBeforeText() {
2844          global $wgOut;
2845          $section = htmlspecialchars( $this->section );
2846          $wgOut->addHTML( <<<HTML
2847  <input type='hidden' value="{$section}" name="wpSection" />
2848  <input type='hidden' value="{$this->starttime}" name="wpStarttime" />
2849  <input type='hidden' value="{$this->edittime}" name="wpEdittime" />
2850  <input type='hidden' value="{$this->scrolltop}" name="wpScrolltop" id="wpScrolltop" />
2851  
2852  HTML
2853          );
2854          if ( !$this->checkUnicodeCompliantBrowser() ) {
2855              $wgOut->addHTML( Html::hidden( 'safemode', '1' ) );
2856          }
2857      }
2858  
2859  	protected function showFormAfterText() {
2860          global $wgOut, $wgUser;
2861          /**
2862           * To make it harder for someone to slip a user a page
2863           * which submits an edit form to the wiki without their
2864           * knowledge, a random token is associated with the login
2865           * session. If it's not passed back with the submission,
2866           * we won't save the page, or render user JavaScript and
2867           * CSS previews.
2868           *
2869           * For anon editors, who may not have a session, we just
2870           * include the constant suffix to prevent editing from
2871           * broken text-mangling proxies.
2872           */
2873          $wgOut->addHTML( "\n" . Html::hidden( "wpEditToken", $wgUser->getEditToken() ) . "\n" );
2874      }
2875  
2876      /**
2877       * Subpage overridable method for printing the form for page content editing
2878       * By default this simply outputs wpTextbox1
2879       * Subclasses can override this to provide a custom UI for editing;
2880       * be it a form, or simply wpTextbox1 with a modified content that will be
2881       * reverse modified when extracted from the post data.
2882       * Note that this is basically the inverse for importContentFormData
2883       */
2884  	protected function showContentForm() {
2885          $this->showTextbox1();
2886      }
2887  
2888      /**
2889       * Method to output wpTextbox1
2890       * The $textoverride method can be used by subclasses overriding showContentForm
2891       * to pass back to this method.
2892       *
2893       * @param array $customAttribs Array of html attributes to use in the textarea
2894       * @param string $textoverride Optional text to override $this->textarea1 with
2895       */
2896  	protected function showTextbox1( $customAttribs = null, $textoverride = null ) {
2897          if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) {
2898              $attribs = array( 'style' => 'display:none;' );
2899          } else {
2900              $classes = array(); // Textarea CSS
2901              if ( $this->mTitle->isProtected( 'edit' ) &&
2902                  MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== array( '' )
2903              ) {
2904                  # Is the title semi-protected?
2905                  if ( $this->mTitle->isSemiProtected() ) {
2906                      $classes[] = 'mw-textarea-sprotected';
2907                  } else {
2908                      # Then it must be protected based on static groups (regular)
2909                      $classes[] = 'mw-textarea-protected';
2910                  }
2911                  # Is the title cascade-protected?
2912                  if ( $this->mTitle->isCascadeProtected() ) {
2913                      $classes[] = 'mw-textarea-cprotected';
2914                  }
2915              }
2916  
2917              $attribs = array( 'tabindex' => 1 );
2918  
2919              if ( is_array( $customAttribs ) ) {
2920                  $attribs += $customAttribs;
2921              }
2922  
2923              if ( count( $classes ) ) {
2924                  if ( isset( $attribs['class'] ) ) {
2925                      $classes[] = $attribs['class'];
2926                  }
2927                  $attribs['class'] = implode( ' ', $classes );
2928              }
2929          }
2930  
2931          $this->showTextbox(
2932              $textoverride !== null ? $textoverride : $this->textbox1,
2933              'wpTextbox1',
2934              $attribs
2935          );
2936      }
2937  
2938  	protected function showTextbox2() {
2939          $this->showTextbox( $this->textbox2, 'wpTextbox2', array( 'tabindex' => 6, 'readonly' ) );
2940      }
2941  
2942  	protected function showTextbox( $text, $name, $customAttribs = array() ) {
2943          global $wgOut, $wgUser;
2944  
2945          $wikitext = $this->safeUnicodeOutput( $text );
2946          if ( strval( $wikitext ) !== '' ) {
2947              // Ensure there's a newline at the end, otherwise adding lines
2948              // is awkward.
2949              // But don't add a newline if the ext is empty, or Firefox in XHTML
2950              // mode will show an extra newline. A bit annoying.
2951              $wikitext .= "\n";
2952          }
2953  
2954          $attribs = $customAttribs + array(
2955              'accesskey' => ',',
2956              'id' => $name,
2957              'cols' => $wgUser->getIntOption( 'cols' ),
2958              'rows' => $wgUser->getIntOption( 'rows' ),
2959              // Avoid PHP notices when appending preferences
2960              // (appending allows customAttribs['style'] to still work).
2961              'style' => ''
2962          );
2963  
2964          $pageLang = $this->mTitle->getPageLanguage();
2965          $attribs['lang'] = $pageLang->getCode();
2966          $attribs['dir'] = $pageLang->getDir();
2967  
2968          $wgOut->addHTML( Html::textarea( $name, $wikitext, $attribs ) );
2969      }
2970  
2971  	protected function displayPreviewArea( $previewOutput, $isOnTop = false ) {
2972          global $wgOut;
2973          $classes = array();
2974          if ( $isOnTop ) {
2975              $classes[] = 'ontop';
2976          }
2977  
2978          $attribs = array( 'id' => 'wikiPreview', 'class' => implode( ' ', $classes ) );
2979  
2980          if ( $this->formtype != 'preview' ) {
2981              $attribs['style'] = 'display: none;';
2982          }
2983  
2984          $wgOut->addHTML( Xml::openElement( 'div', $attribs ) );
2985  
2986          if ( $this->formtype == 'preview' ) {
2987              $this->showPreview( $previewOutput );
2988          }
2989  
2990          $wgOut->addHTML( '</div>' );
2991  
2992          if ( $this->formtype == 'diff' ) {
2993              try {
2994                  $this->showDiff();
2995              } catch ( MWContentSerializationException $ex ) {
2996                  $msg = wfMessage(
2997                      'content-failed-to-parse',
2998                      $this->contentModel,
2999                      $this->contentFormat,
3000                      $ex->getMessage()
3001                  );
3002                  $wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
3003              }
3004          }
3005      }
3006  
3007      /**
3008       * Append preview output to $wgOut.
3009       * Includes category rendering if this is a category page.
3010       *
3011       * @param string $text The HTML to be output for the preview.
3012       */
3013  	protected function showPreview( $text ) {
3014          global $wgOut;
3015          if ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
3016              $this->mArticle->openShowCategory();
3017          }
3018          # This hook seems slightly odd here, but makes things more
3019          # consistent for extensions.
3020          wfRunHooks( 'OutputPageBeforeHTML', array( &$wgOut, &$text ) );
3021          $wgOut->addHTML( $text );
3022          if ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
3023              $this->mArticle->closeShowCategory();
3024          }
3025      }
3026  
3027      /**
3028       * Get a diff between the current contents of the edit box and the
3029       * version of the page we're editing from.
3030       *
3031       * If this is a section edit, we'll replace the section as for final
3032       * save and then make a comparison.
3033       */
3034  	function showDiff() {
3035          global $wgUser, $wgContLang, $wgOut;
3036  
3037          $oldtitlemsg = 'currentrev';
3038          # if message does not exist, show diff against the preloaded default
3039          if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && !$this->mTitle->exists() ) {
3040              $oldtext = $this->mTitle->getDefaultMessageText();
3041              if ( $oldtext !== false ) {
3042                  $oldtitlemsg = 'defaultmessagetext';
3043                  $oldContent = $this->toEditContent( $oldtext );
3044              } else {
3045                  $oldContent = null;
3046              }
3047          } else {
3048              $oldContent = $this->getCurrentContent();
3049          }
3050  
3051          $textboxContent = $this->toEditContent( $this->textbox1 );
3052  
3053          $newContent = $this->mArticle->replaceSectionContent(
3054                              $this->section, $textboxContent,
3055                              $this->summary, $this->edittime );
3056  
3057          if ( $newContent ) {
3058              ContentHandler::runLegacyHooks( 'EditPageGetDiffText', array( $this, &$newContent ) );
3059              wfRunHooks( 'EditPageGetDiffContent', array( $this, &$newContent ) );
3060  
3061              $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
3062              $newContent = $newContent->preSaveTransform( $this->mTitle, $wgUser, $popts );
3063          }
3064  
3065          if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
3066              $oldtitle = wfMessage( $oldtitlemsg )->parse();
3067              $newtitle = wfMessage( 'yourtext' )->parse();
3068  
3069              if ( !$oldContent ) {
3070                  $oldContent = $newContent->getContentHandler()->makeEmptyContent();
3071              }
3072  
3073              if ( !$newContent ) {
3074                  $newContent = $oldContent->getContentHandler()->makeEmptyContent();
3075              }
3076  
3077              $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->mArticle->getContext() );
3078              $de->setContent( $oldContent, $newContent );
3079  
3080              $difftext = $de->getDiff( $oldtitle, $newtitle );
3081              $de->showDiffStyle();
3082          } else {
3083              $difftext = '';
3084          }
3085  
3086          $wgOut->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' );
3087      }
3088  
3089      /**
3090       * Show the header copyright warning.
3091       */
3092  	protected function showHeaderCopyrightWarning() {
3093          $msg = 'editpage-head-copy-warn';
3094          if ( !wfMessage( $msg )->isDisabled() ) {
3095              global $wgOut;
3096              $wgOut->wrapWikiMsg( "<div class='editpage-head-copywarn'>\n$1\n</div>",
3097                  'editpage-head-copy-warn' );
3098          }
3099      }
3100  
3101      /**
3102       * Give a chance for site and per-namespace customizations of
3103       * terms of service summary link that might exist separately
3104       * from the copyright notice.
3105       *
3106       * This will display between the save button and the edit tools,
3107       * so should remain short!
3108       */
3109  	protected function showTosSummary() {
3110          $msg = 'editpage-tos-summary';
3111          wfRunHooks( 'EditPageTosSummary', array( $this->mTitle, &$msg ) );
3112          if ( !wfMessage( $msg )->isDisabled() ) {
3113              global $wgOut;
3114              $wgOut->addHTML( '<div class="mw-tos-summary">' );
3115              $wgOut->addWikiMsg( $msg );
3116              $wgOut->addHTML( '</div>' );
3117          }
3118      }
3119  
3120  	protected function showEditTools() {
3121          global $wgOut;
3122          $wgOut->addHTML( '<div class="mw-editTools">' .
3123              wfMessage( 'edittools' )->inContentLanguage()->parse() .
3124              '</div>' );
3125      }
3126  
3127      /**
3128       * Get the copyright warning
3129       *
3130       * Renamed to getCopyrightWarning(), old name kept around for backwards compatibility
3131       * @return string
3132       */
3133  	protected function getCopywarn() {
3134          return self::getCopyrightWarning( $this->mTitle );
3135      }
3136  
3137      /**
3138       * Get the copyright warning, by default returns wikitext
3139       *
3140       * @param Title $title
3141       * @param string $format Output format, valid values are any function of a Message object
3142       * @return string
3143       */
3144  	public static function getCopyrightWarning( $title, $format = 'plain' ) {
3145          global $wgRightsText;
3146          if ( $wgRightsText ) {
3147              $copywarnMsg = array( 'copyrightwarning',
3148                  '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]',
3149                  $wgRightsText );
3150          } else {
3151              $copywarnMsg = array( 'copyrightwarning2',
3152                  '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]' );
3153          }
3154          // Allow for site and per-namespace customization of contribution/copyright notice.
3155          wfRunHooks( 'EditPageCopyrightWarning', array( $title, &$copywarnMsg ) );
3156  
3157          return "<div id=\"editpage-copywarn\">\n" .
3158              call_user_func_array( 'wfMessage', $copywarnMsg )->$format() . "\n</div>";
3159      }
3160  
3161      /**
3162       * Get the Limit report for page previews
3163       *
3164       * @since 1.22
3165       * @param ParserOutput $output ParserOutput object from the parse
3166       * @return string HTML
3167       */
3168  	public static function getPreviewLimitReport( $output ) {
3169          if ( !$output || !$output->getLimitReportData() ) {
3170              return '';
3171          }
3172  
3173          wfProfileIn( __METHOD__ );
3174  
3175          $limitReport = Html::rawElement( 'div', array( 'class' => 'mw-limitReportExplanation' ),
3176              wfMessage( 'limitreport-title' )->parseAsBlock()
3177          );
3178  
3179          // Show/hide animation doesn't work correctly on a table, so wrap it in a div.
3180          $limitReport .= Html::openElement( 'div', array( 'class' => 'preview-limit-report-wrapper' ) );
3181  
3182          $limitReport .= Html::openElement( 'table', array(
3183              'class' => 'preview-limit-report wikitable'
3184          ) ) .
3185              Html::openElement( 'tbody' );
3186  
3187          foreach ( $output->getLimitReportData() as $key => $value ) {
3188              if ( wfRunHooks( 'ParserLimitReportFormat',
3189                  array( $key, &$value, &$limitReport, true, true )
3190              ) ) {
3191                  $keyMsg = wfMessage( $key );
3192                  $valueMsg = wfMessage( array( "$key-value-html", "$key-value" ) );
3193                  if ( !$valueMsg->exists() ) {
3194                      $valueMsg = new RawMessage( '$1' );
3195                  }
3196                  if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
3197                      $limitReport .= Html::openElement( 'tr' ) .
3198                          Html::rawElement( 'th', null, $keyMsg->parse() ) .
3199                          Html::rawElement( 'td', null, $valueMsg->params( $value )->parse() ) .
3200                          Html::closeElement( 'tr' );
3201                  }
3202              }
3203          }
3204  
3205          $limitReport .= Html::closeElement( 'tbody' ) .
3206              Html::closeElement( 'table' ) .
3207              Html::closeElement( 'div' );
3208  
3209          wfProfileOut( __METHOD__ );
3210  
3211          return $limitReport;
3212      }
3213  
3214  	protected function showStandardInputs( &$tabindex = 2 ) {
3215          global $wgOut, $wgUseMediaWikiUIEverywhere;
3216          $wgOut->addHTML( "<div class='editOptions'>\n" );
3217  
3218          if ( $this->section != 'new' ) {
3219              $this->showSummaryInput( false, $this->summary );
3220              $wgOut->addHTML( $this->getSummaryPreview( false, $this->summary ) );
3221          }
3222  
3223          $checkboxes = $this->getCheckboxes( $tabindex,
3224              array( 'minor' => $this->minoredit, 'watch' => $this->watchthis ) );
3225          $wgOut->addHTML( "<div class='editCheckboxes'>" . implode( $checkboxes, "\n" ) . "</div>\n" );
3226  
3227          // Show copyright warning.
3228          $wgOut->addWikiText( $this->getCopywarn() );
3229          $wgOut->addHTML( $this->editFormTextAfterWarn );
3230  
3231          $wgOut->addHTML( "<div class='editButtons'>\n" );
3232          $wgOut->addHTML( implode( $this->getEditButtons( $tabindex ), "\n" ) . "\n" );
3233  
3234          $cancel = $this->getCancelLink();
3235          if ( $cancel !== '' ) {
3236              $cancel .= Html::element( 'span',
3237                  array( 'class' => 'mw-editButtons-pipe-separator' ),
3238                  wfMessage( 'pipe-separator' )->text() );
3239          }
3240  
3241          $message = wfMessage( 'edithelppage' )->inContentLanguage()->text();
3242          $edithelpurl = Skin::makeInternalOrExternalUrl( $message );
3243          $attrs = array(
3244              'target' => 'helpwindow',
3245              'href' => $edithelpurl,
3246          );
3247          if ( $wgUseMediaWikiUIEverywhere ) {
3248              $attrs['class'] = 'mw-ui-button mw-ui-quiet';
3249          }
3250          $edithelp = Html::element( 'a', $attrs, wfMessage( 'edithelp' )->text() ) .
3251              wfMessage( 'word-separator' )->escaped() .
3252              wfMessage( 'newwindow' )->parse();
3253  
3254          $wgOut->addHTML( "    <span class='cancelLink'>{$cancel}</span>\n" );
3255          $wgOut->addHTML( "    <span class='editHelp'>{$edithelp}</span>\n" );
3256          $wgOut->addHTML( "</div><!-- editButtons -->\n" );
3257  
3258          wfRunHooks( 'EditPage::showStandardInputs:options', array( $this, $wgOut, &$tabindex ) );
3259  
3260          $wgOut->addHTML( "</div><!-- editOptions -->\n" );
3261      }
3262  
3263      /**
3264       * Show an edit conflict. textbox1 is already shown in showEditForm().
3265       * If you want to use another entry point to this function, be careful.
3266       */
3267  	protected function showConflict() {
3268          global $wgOut;
3269  
3270          if ( wfRunHooks( 'EditPageBeforeConflictDiff', array( &$this, &$wgOut ) ) ) {
3271              $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
3272  
3273              $content1 = $this->toEditContent( $this->textbox1 );
3274              $content2 = $this->toEditContent( $this->textbox2 );
3275  
3276              $handler = ContentHandler::getForModelID( $this->contentModel );
3277              $de = $handler->createDifferenceEngine( $this->mArticle->getContext() );
3278              $de->setContent( $content2, $content1 );
3279              $de->showDiff(
3280                  wfMessage( 'yourtext' )->parse(),
3281                  wfMessage( 'storedversion' )->text()
3282              );
3283  
3284              $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
3285              $this->showTextbox2();
3286          }
3287      }
3288  
3289      /**
3290       * @return string
3291       */
3292  	public function getCancelLink() {
3293          global $wgUseMediaWikiUIEverywhere;
3294          $cancelParams = array();
3295          if ( !$this->isConflict && $this->oldid > 0 ) {
3296              $cancelParams['oldid'] = $this->oldid;
3297          }
3298          $attrs = array( 'id' => 'mw-editform-cancel' );
3299          if ( $wgUseMediaWikiUIEverywhere ) {
3300              $attrs['class'] = 'mw-ui-button mw-ui-quiet';
3301          }
3302  
3303          return Linker::linkKnown(
3304              $this->getContextTitle(),
3305              wfMessage( 'cancel' )->parse(),
3306              $attrs,
3307              $cancelParams
3308          );
3309      }
3310  
3311      /**
3312       * Returns the URL to use in the form's action attribute.
3313       * This is used by EditPage subclasses when simply customizing the action
3314       * variable in the constructor is not enough. This can be used when the
3315       * EditPage lives inside of a Special page rather than a custom page action.
3316       *
3317       * @param Title $title Title object for which is being edited (where we go to for &action= links)
3318       * @return string
3319       */
3320  	protected function getActionURL( Title $title ) {
3321          return $title->getLocalURL( array( 'action' => $this->action ) );
3322      }
3323  
3324      /**
3325       * Check if a page was deleted while the user was editing it, before submit.
3326       * Note that we rely on the logging table, which hasn't been always there,
3327       * but that doesn't matter, because this only applies to brand new
3328       * deletes.
3329       * @return bool
3330       */
3331  	protected function wasDeletedSinceLastEdit() {
3332          if ( $this->deletedSinceEdit !== null ) {
3333              return $this->deletedSinceEdit;
3334          }
3335  
3336          $this->deletedSinceEdit = false;
3337  
3338          if ( $this->mTitle->isDeletedQuick() ) {
3339              $this->lastDelete = $this->getLastDelete();
3340              if ( $this->lastDelete ) {
3341                  $deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp );
3342                  if ( $deleteTime > $this->starttime ) {
3343                      $this->deletedSinceEdit = true;
3344                  }
3345              }
3346          }
3347  
3348          return $this->deletedSinceEdit;
3349      }
3350  
3351      /**
3352       * @return bool|stdClass
3353       */
3354  	protected function getLastDelete() {
3355          $dbr = wfGetDB( DB_SLAVE );
3356          $data = $dbr->selectRow(
3357              array( 'logging', 'user' ),
3358              array(
3359                  'log_type',
3360                  'log_action',
3361                  'log_timestamp',
3362                  'log_user',
3363                  'log_namespace',
3364                  'log_title',
3365                  'log_comment',
3366                  'log_params',
3367                  'log_deleted',
3368                  'user_name'
3369              ), array(
3370                  'log_namespace' => $this->mTitle->getNamespace(),
3371                  'log_title' => $this->mTitle->getDBkey(),
3372                  'log_type' => 'delete',
3373                  'log_action' => 'delete',
3374                  'user_id=log_user'
3375              ),
3376              __METHOD__,
3377              array( 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' )
3378          );
3379          // Quick paranoid permission checks...
3380          if ( is_object( $data ) ) {
3381              if ( $data->log_deleted & LogPage::DELETED_USER ) {
3382                  $data->user_name = wfMessage( 'rev-deleted-user' )->escaped();
3383              }
3384  
3385              if ( $data->log_deleted & LogPage::DELETED_COMMENT ) {
3386                  $data->log_comment = wfMessage( 'rev-deleted-comment' )->escaped();
3387              }
3388          }
3389  
3390          return $data;
3391      }
3392  
3393      /**
3394       * Get the rendered text for previewing.
3395       * @throws MWException
3396       * @return string
3397       */
3398  	function getPreviewText() {
3399          global $wgOut, $wgUser, $wgRawHtml, $wgLang;
3400          global $wgAllowUserCss, $wgAllowUserJs;
3401  
3402          wfProfileIn( __METHOD__ );
3403  
3404          if ( $wgRawHtml && !$this->mTokenOk ) {
3405              // Could be an offsite preview attempt. This is very unsafe if
3406              // HTML is enabled, as it could be an attack.
3407              $parsedNote = '';
3408              if ( $this->textbox1 !== '' ) {
3409                  // Do not put big scary notice, if previewing the empty
3410                  // string, which happens when you initially edit
3411                  // a category page, due to automatic preview-on-open.
3412                  $parsedNote = $wgOut->parse( "<div class='previewnote'>" .
3413                      wfMessage( 'session_fail_preview_html' )->text() . "</div>", true, /* interface */true );
3414              }
3415              wfProfileOut( __METHOD__ );
3416              return $parsedNote;
3417          }
3418  
3419          $note = '';
3420  
3421          try {
3422              $content = $this->toEditContent( $this->textbox1 );
3423  
3424              $previewHTML = '';
3425              if ( !wfRunHooks(
3426                  'AlternateEditPreview',
3427                  array( $this, &$content, &$previewHTML, &$this->mParserOutput ) )
3428              ) {
3429                  wfProfileOut( __METHOD__ );
3430                  return $previewHTML;
3431              }
3432  
3433              # provide a anchor link to the editform
3434              $continueEditing = '<span class="mw-continue-editing">' .
3435                  '[[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' .
3436                  wfMessage( 'continue-editing' )->text() . ']]</span>';
3437              if ( $this->mTriedSave && !$this->mTokenOk ) {
3438                  if ( $this->mTokenOkExceptSuffix ) {
3439                      $note = wfMessage( 'token_suffix_mismatch' )->plain();
3440                  } else {
3441                      $note = wfMessage( 'session_fail_preview' )->plain();
3442                  }
3443              } elseif ( $this->incompleteForm ) {
3444                  $note = wfMessage( 'edit_form_incomplete' )->plain();
3445              } else {
3446                  $note = wfMessage( 'previewnote' )->plain() . ' ' . $continueEditing;
3447              }
3448  
3449              $parserOptions = $this->mArticle->makeParserOptions( $this->mArticle->getContext() );
3450              $parserOptions->setEditSection( false );
3451              $parserOptions->setIsPreview( true );
3452              $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
3453  
3454              # don't parse non-wikitext pages, show message about preview
3455              if ( $this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage() ) {
3456                  if ( $this->mTitle->isCssJsSubpage() ) {
3457                      $level = 'user';
3458                  } elseif ( $this->mTitle->isCssOrJsPage() ) {
3459                      $level = 'site';
3460                  } else {
3461                      $level = false;
3462                  }
3463  
3464                  if ( $content->getModel() == CONTENT_MODEL_CSS ) {
3465                      $format = 'css';
3466                      if ( $level === 'user' && !$wgAllowUserCss ) {
3467                          $format = false;
3468                      }
3469                  } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) {
3470                      $format = 'js';
3471                      if ( $level === 'user' && !$wgAllowUserJs ) {
3472                          $format = false;
3473                      }
3474                  } else {
3475                      $format = false;
3476                  }
3477  
3478                  # Used messages to make sure grep find them:
3479                  # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
3480                  if ( $level && $format ) {
3481                      $note = "<div id='mw-{$level}{$format}preview'>" .
3482                          wfMessage( "{$level}{$format}preview" )->text() .
3483                          ' ' . $continueEditing . "</div>";
3484                  }
3485              }
3486  
3487              # If we're adding a comment, we need to show the
3488              # summary as the headline
3489              if ( $this->section === "new" && $this->summary !== "" ) {
3490                  $content = $content->addSectionHeader( $this->summary );
3491              }
3492  
3493              $hook_args = array( $this, &$content );
3494              ContentHandler::runLegacyHooks( 'EditPageGetPreviewText', $hook_args );
3495              wfRunHooks( 'EditPageGetPreviewContent', $hook_args );
3496  
3497              $parserOptions->enableLimitReport();
3498  
3499              # For CSS/JS pages, we should have called the ShowRawCssJs hook here.
3500              # But it's now deprecated, so never mind
3501  
3502              $content = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions );
3503              $parserOutput = $content->getParserOutput(
3504                  $this->getArticle()->getTitle(),
3505                  null,
3506                  $parserOptions
3507              );
3508  
3509              $previewHTML = $parserOutput->getText();
3510              $this->mParserOutput = $parserOutput;
3511              $wgOut->addParserOutputMetadata( $parserOutput );
3512  
3513              if ( count( $parserOutput->getWarnings() ) ) {
3514                  $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
3515              }
3516          } catch ( MWContentSerializationException $ex ) {
3517              $m = wfMessage(
3518                  'content-failed-to-parse',
3519                  $this->contentModel,
3520                  $this->contentFormat,
3521                  $ex->getMessage()
3522              );
3523              $note .= "\n\n" . $m->parse();
3524              $previewHTML = '';
3525          }
3526  
3527          if ( $this->isConflict ) {
3528              $conflict = '<h2 id="mw-previewconflict">'
3529                  . wfMessage( 'previewconflict' )->escaped() . "</h2>\n";
3530          } else {
3531              $conflict = '<hr />';
3532          }
3533  
3534          $previewhead = "<div class='previewnote'>\n" .
3535              '<h2 id="mw-previewheader">' . wfMessage( 'preview' )->escaped() . "</h2>" .
3536              $wgOut->parse( $note, true, /* interface */true ) . $conflict . "</div>\n";
3537  
3538          $pageViewLang = $this->mTitle->getPageViewLanguage();
3539          $attribs = array( 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3540              'class' => 'mw-content-' . $pageViewLang->getDir() );
3541          $previewHTML = Html::rawElement( 'div', $attribs, $previewHTML );
3542  
3543          wfProfileOut( __METHOD__ );
3544          return $previewhead . $previewHTML . $this->previewTextAfterContent;
3545      }
3546  
3547      /**
3548       * @return array
3549       */
3550  	function getTemplates() {
3551          if ( $this->preview || $this->section != '' ) {
3552              $templates = array();
3553              if ( !isset( $this->mParserOutput ) ) {
3554                  return $templates;
3555              }
3556              foreach ( $this->mParserOutput->getTemplates() as $ns => $template ) {
3557                  foreach ( array_keys( $template ) as $dbk ) {
3558                      $templates[] = Title::makeTitle( $ns, $dbk );
3559                  }
3560              }
3561              return $templates;
3562          } else {
3563              return $this->mTitle->getTemplateLinksFrom();
3564          }
3565      }
3566  
3567      /**
3568       * Shows a bulletin board style toolbar for common editing functions.
3569       * It can be disabled in the user preferences.
3570       *
3571       * @return string
3572       */
3573  	static function getEditToolbar() {
3574          global $wgContLang, $wgOut;
3575          global $wgEnableUploads, $wgForeignFileRepos;
3576  
3577          $imagesAvailable = $wgEnableUploads || count( $wgForeignFileRepos );
3578  
3579          /**
3580           * $toolarray is an array of arrays each of which includes the
3581           * opening tag, the closing tag, optionally a sample text that is
3582           * inserted between the two when no selection is highlighted
3583           * and.  The tip text is shown when the user moves the mouse
3584           * over the button.
3585           *
3586           * Images are defined in ResourceLoaderEditToolbarModule.
3587           */
3588          $toolarray = array(
3589              array(
3590                  'id'     => 'mw-editbutton-bold',
3591                  'open'   => '\'\'\'',
3592                  'close'  => '\'\'\'',
3593                  'sample' => wfMessage( 'bold_sample' )->text(),
3594                  'tip'    => wfMessage( 'bold_tip' )->text(),
3595              ),
3596              array(
3597                  'id'     => 'mw-editbutton-italic',
3598                  'open'   => '\'\'',
3599                  'close'  => '\'\'',
3600                  'sample' => wfMessage( 'italic_sample' )->text(),
3601                  'tip'    => wfMessage( 'italic_tip' )->text(),
3602              ),
3603              array(
3604                  'id'     => 'mw-editbutton-link',
3605                  'open'   => '[[',
3606                  'close'  => ']]',
3607                  'sample' => wfMessage( 'link_sample' )->text(),
3608                  'tip'    => wfMessage( 'link_tip' )->text(),
3609              ),
3610              array(
3611                  'id'     => 'mw-editbutton-extlink',
3612                  'open'   => '[',
3613                  'close'  => ']',
3614                  'sample' => wfMessage( 'extlink_sample' )->text(),
3615                  'tip'    => wfMessage( 'extlink_tip' )->text(),
3616              ),
3617              array(
3618                  'id'     => 'mw-editbutton-headline',
3619                  'open'   => "\n== ",
3620                  'close'  => " ==\n",
3621                  'sample' => wfMessage( 'headline_sample' )->text(),
3622                  'tip'    => wfMessage( 'headline_tip' )->text(),
3623              ),
3624              $imagesAvailable ? array(
3625                  'id'     => 'mw-editbutton-image',
3626                  'open'   => '[[' . $wgContLang->getNsText( NS_FILE ) . ':',
3627                  'close'  => ']]',
3628                  'sample' => wfMessage( 'image_sample' )->text(),
3629                  'tip'    => wfMessage( 'image_tip' )->text(),
3630              ) : false,
3631              $imagesAvailable ? array(
3632                  'id'     => 'mw-editbutton-media',
3633                  'open'   => '[[' . $wgContLang->getNsText( NS_MEDIA ) . ':',
3634                  'close'  => ']]',
3635                  'sample' => wfMessage( 'media_sample' )->text(),
3636                  'tip'    => wfMessage( 'media_tip' )->text(),
3637              ) : false,
3638              array(
3639                  'id'     => 'mw-editbutton-nowiki',
3640                  'open'   => "<nowiki>",
3641                  'close'  => "</nowiki>",
3642                  'sample' => wfMessage( 'nowiki_sample' )->text(),
3643                  'tip'    => wfMessage( 'nowiki_tip' )->text(),
3644              ),
3645              array(
3646                  'id'     => 'mw-editbutton-signature',
3647                  'open'   => '--~~~~',
3648                  'close'  => '',
3649                  'sample' => '',
3650                  'tip'    => wfMessage( 'sig_tip' )->text(),
3651              ),
3652              array(
3653                  'id'     => 'mw-editbutton-hr',
3654                  'open'   => "\n----\n",
3655                  'close'  => '',
3656                  'sample' => '',
3657                  'tip'    => wfMessage( 'hr_tip' )->text(),
3658              )
3659          );
3660  
3661          $script = 'mw.loader.using("mediawiki.action.edit", function() {';
3662          foreach ( $toolarray as $tool ) {
3663              if ( !$tool ) {
3664                  continue;
3665              }
3666  
3667              $params = array(
3668                  // Images are defined in ResourceLoaderEditToolbarModule
3669                  false,
3670                  // Note that we use the tip both for the ALT tag and the TITLE tag of the image.
3671                  // Older browsers show a "speedtip" type message only for ALT.
3672                  // Ideally these should be different, realistically they
3673                  // probably don't need to be.
3674                  $tool['tip'],
3675                  $tool['open'],
3676                  $tool['close'],
3677                  $tool['sample'],
3678                  $tool['id'],
3679              );
3680  
3681              $script .= Xml::encodeJsCall( 'mw.toolbar.addButton', $params );
3682          }
3683  
3684          // This used to be called on DOMReady from mediawiki.action.edit, which
3685          // ended up causing race conditions with the setup code above.
3686          $script .= "\n" .
3687              "// Create button bar\n" .
3688              "$(function() { mw.toolbar.init(); } );\n";
3689  
3690          $script .= '});';
3691          $wgOut->addScript( Html::inlineScript( ResourceLoader::makeLoaderConditionalScript( $script ) ) );
3692  
3693          $toolbar = '<div id="toolbar"></div>';
3694  
3695          wfRunHooks( 'EditPageBeforeEditToolbar', array( &$toolbar ) );
3696  
3697          return $toolbar;
3698      }
3699  
3700      /**
3701       * Returns an array of html code of the following checkboxes:
3702       * minor and watch
3703       *
3704       * @param int $tabindex Current tabindex
3705       * @param array $checked Array of checkbox => bool, where bool indicates the checked
3706       *                 status of the checkbox
3707       *
3708       * @return array
3709       */
3710  	public function getCheckboxes( &$tabindex, $checked ) {
3711          global $wgUser, $wgUseMediaWikiUIEverywhere;
3712  
3713          $checkboxes = array();
3714  
3715          // don't show the minor edit checkbox if it's a new page or section
3716          if ( !$this->isNew ) {
3717              $checkboxes['minor'] = '';
3718              $minorLabel = wfMessage( 'minoredit' )->parse();
3719              if ( $wgUser->isAllowed( 'minoredit' ) ) {
3720                  $attribs = array(
3721                      'tabindex' => ++$tabindex,
3722                      'accesskey' => wfMessage( 'accesskey-minoredit' )->text(),
3723                      'id' => 'wpMinoredit',
3724                  );
3725                  $minorEditHtml =
3726                      Xml::check( 'wpMinoredit', $checked['minor'], $attribs ) .
3727                      "&#160;<label for='wpMinoredit' id='mw-editpage-minoredit'" .
3728                      Xml::expandAttributes( array( 'title' => Linker::titleAttrib( 'minoredit', 'withaccess' ) ) ) .
3729                      ">{$minorLabel}</label>";
3730  
3731                  if ( $wgUseMediaWikiUIEverywhere ) {
3732                      $checkboxes['minor'] = Html::openElement( 'div', array( 'class' => 'mw-ui-checkbox' ) ) .
3733                          $minorEditHtml .
3734                      Html::closeElement( 'div' );
3735                  } else {
3736                      $checkboxes['minor'] = $minorEditHtml;
3737                  }
3738              }
3739          }
3740  
3741          $watchLabel = wfMessage( 'watchthis' )->parse();
3742          $checkboxes['watch'] = '';
3743          if ( $wgUser->isLoggedIn() ) {
3744              $attribs = array(
3745                  'tabindex' => ++$tabindex,
3746                  'accesskey' => wfMessage( 'accesskey-watch' )->text(),
3747                  'id' => 'wpWatchthis',
3748              );
3749              $watchThisHtml =
3750                  Xml::check( 'wpWatchthis', $checked['watch'], $attribs ) .
3751                  "&#160;<label for='wpWatchthis' id='mw-editpage-watch'" .
3752                  Xml::expandAttributes( array( 'title' => Linker::titleAttrib( 'watch', 'withaccess' ) ) ) .
3753                  ">{$watchLabel}</label>";
3754              if ( $wgUseMediaWikiUIEverywhere ) {
3755                  $checkboxes['watch'] = Html::openElement( 'div', array( 'class' => 'mw-ui-checkbox' ) ) .
3756                      $watchThisHtml .
3757                      Html::closeElement( 'div' );
3758              } else {
3759                  $checkboxes['watch'] = $watchThisHtml;
3760              }
3761          }
3762          wfRunHooks( 'EditPageBeforeEditChecks', array( &$this, &$checkboxes, &$tabindex ) );
3763          return $checkboxes;
3764      }
3765  
3766      /**
3767       * Returns an array of html code of the following buttons:
3768       * save, diff, preview and live
3769       *
3770       * @param int $tabindex Current tabindex
3771       *
3772       * @return array
3773       */
3774  	public function getEditButtons( &$tabindex ) {
3775          global $wgUseMediaWikiUIEverywhere;
3776  
3777          $buttons = array();
3778  
3779          $attribs = array(
3780              'id' => 'wpSave',
3781              'name' => 'wpSave',
3782              'type' => 'submit',
3783              'tabindex' => ++$tabindex,
3784              'value' => wfMessage( 'savearticle' )->text(),
3785          ) + Linker::tooltipAndAccesskeyAttribs( 'save' );
3786          if ( $wgUseMediaWikiUIEverywhere ) {
3787              $attribs['class'] = 'mw-ui-button mw-ui-constructive';
3788          }
3789          $buttons['save'] = Xml::element( 'input', $attribs, '' );
3790  
3791          ++$tabindex; // use the same for preview and live preview
3792          $attribs = array(
3793              'id' => 'wpPreview',
3794              'name' => 'wpPreview',
3795              'type' => 'submit',
3796              'tabindex' => $tabindex,
3797              'value' => wfMessage( 'showpreview' )->text(),
3798          ) + Linker::tooltipAndAccesskeyAttribs( 'preview' );
3799          if ( $wgUseMediaWikiUIEverywhere ) {
3800              $attribs['class'] = 'mw-ui-button mw-ui-progressive';
3801          }
3802          $buttons['preview'] = Xml::element( 'input', $attribs, '' );
3803          $buttons['live'] = '';
3804  
3805          $attribs = array(
3806              'id' => 'wpDiff',
3807              'name' => 'wpDiff',
3808              'type' => 'submit',
3809              'tabindex' => ++$tabindex,
3810              'value' => wfMessage( 'showdiff' )->text(),
3811          ) + Linker::tooltipAndAccesskeyAttribs( 'diff' );
3812          if ( $wgUseMediaWikiUIEverywhere ) {
3813              $attribs['class'] = 'mw-ui-button mw-ui-progressive';
3814          }
3815          $buttons['diff'] = Xml::element( 'input', $attribs, '' );
3816  
3817          wfRunHooks( 'EditPageBeforeEditButtons', array( &$this, &$buttons, &$tabindex ) );
3818          return $buttons;
3819      }
3820  
3821      /**
3822       * Output preview text only. This can be sucked into the edit page
3823       * via JavaScript, and saves the server time rendering the skin as
3824       * well as theoretically being more robust on the client (doesn't
3825       * disturb the edit box's undo history, won't eat your text on
3826       * failure, etc).
3827       *
3828       * @todo This doesn't include category or interlanguage links.
3829       *       Would need to enhance it a bit, "<s>maybe wrap them in XML
3830       *       or something...</s>" that might also require more skin
3831       *       initialization, so check whether that's a problem.
3832       */
3833  	function livePreview() {
3834          global $wgOut;
3835          $wgOut->disable();
3836          header( 'Content-type: text/xml; charset=utf-8' );
3837          header( 'Cache-control: no-cache' );
3838  
3839          $previewText = $this->getPreviewText();
3840          #$categories = $skin->getCategoryLinks();
3841  
3842          $s =
3843              '<?xml version="1.0" encoding="UTF-8" ?>' . "\n" .
3844              Xml::tags( 'livepreview', null,
3845                  Xml::element( 'preview', null, $previewText )
3846                  #.    Xml::element( 'category', null, $categories )
3847              );
3848          echo $s;
3849      }
3850  
3851      /**
3852       * Creates a basic error page which informs the user that
3853       * they have attempted to edit a nonexistent section.
3854       */
3855  	function noSuchSectionPage() {
3856          global $wgOut;
3857  
3858          $wgOut->prepareErrorPage( wfMessage( 'nosuchsectiontitle' ) );
3859  
3860          $res = wfMessage( 'nosuchsectiontext', $this->section )->parseAsBlock();
3861          wfRunHooks( 'EditPageNoSuchSection', array( &$this, &$res ) );
3862          $wgOut->addHTML( $res );
3863  
3864          $wgOut->returnToMain( false, $this->mTitle );
3865      }
3866  
3867      /**
3868       * Show "your edit contains spam" page with your diff and text
3869       *
3870       * @param string|array|bool $match Text (or array of texts) which triggered one or more filters
3871       */
3872  	public function spamPageWithContent( $match = false ) {
3873          global $wgOut, $wgLang;
3874          $this->textbox2 = $this->textbox1;
3875  
3876          if ( is_array( $match ) ) {
3877              $match = $wgLang->listToText( $match );
3878          }
3879          $wgOut->prepareErrorPage( wfMessage( 'spamprotectiontitle' ) );
3880  
3881          $wgOut->addHTML( '<div id="spamprotected">' );
3882          $wgOut->addWikiMsg( 'spamprotectiontext' );
3883          if ( $match ) {
3884              $wgOut->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) );
3885          }
3886          $wgOut->addHTML( '</div>' );
3887  
3888          $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
3889          $this->showDiff();
3890  
3891          $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
3892          $this->showTextbox2();
3893  
3894          $wgOut->addReturnTo( $this->getContextTitle(), array( 'action' => 'edit' ) );
3895      }
3896  
3897      /**
3898       * Check if the browser is on a blacklist of user-agents known to
3899       * mangle UTF-8 data on form submission. Returns true if Unicode
3900       * should make it through, false if it's known to be a problem.
3901       * @return bool
3902       */
3903  	private function checkUnicodeCompliantBrowser() {
3904          global $wgBrowserBlackList, $wgRequest;
3905  
3906          $currentbrowser = $wgRequest->getHeader( 'User-Agent' );
3907          if ( $currentbrowser === false ) {
3908              // No User-Agent header sent? Trust it by default...
3909              return true;
3910          }
3911  
3912          foreach ( $wgBrowserBlackList as $browser ) {
3913              if ( preg_match( $browser, $currentbrowser ) ) {
3914                  return false;
3915              }
3916          }
3917          return true;
3918      }
3919  
3920      /**
3921       * Filter an input field through a Unicode de-armoring process if it
3922       * came from an old browser with known broken Unicode editing issues.
3923       *
3924       * @param WebRequest $request
3925       * @param string $field
3926       * @return string
3927       */
3928  	protected function safeUnicodeInput( $request, $field ) {
3929          $text = rtrim( $request->getText( $field ) );
3930          return $request->getBool( 'safemode' )
3931              ? $this->unmakeSafe( $text )
3932              : $text;
3933      }
3934  
3935      /**
3936       * Filter an output field through a Unicode armoring process if it is
3937       * going to an old browser with known broken Unicode editing issues.
3938       *
3939       * @param string $text
3940       * @return string
3941       */
3942  	protected function safeUnicodeOutput( $text ) {
3943          global $wgContLang;
3944          $codedText = $wgContLang->recodeForEdit( $text );
3945          return $this->checkUnicodeCompliantBrowser()
3946              ? $codedText
3947              : $this->makeSafe( $codedText );
3948      }
3949  
3950      /**
3951       * A number of web browsers are known to corrupt non-ASCII characters
3952       * in a UTF-8 text editing environment. To protect against this,
3953       * detected browsers will be served an armored version of the text,
3954       * with non-ASCII chars converted to numeric HTML character references.
3955       *
3956       * Preexisting such character references will have a 0 added to them
3957       * to ensure that round-trips do not alter the original data.
3958       *
3959       * @param string $invalue
3960       * @return string
3961       */
3962  	private function makeSafe( $invalue ) {
3963          // Armor existing references for reversibility.
3964          $invalue = strtr( $invalue, array( "&#x" => "&#x0" ) );
3965  
3966          $bytesleft = 0;
3967          $result = "";
3968          $working = 0;
3969          $valueLength = strlen( $invalue );
3970          for ( $i = 0; $i < $valueLength; $i++ ) {
3971              $bytevalue = ord( $invalue[$i] );
3972              if ( $bytevalue <= 0x7F ) { // 0xxx xxxx
3973                  $result .= chr( $bytevalue );
3974                  $bytesleft = 0;
3975              } elseif ( $bytevalue <= 0xBF ) { // 10xx xxxx
3976                  $working = $working << 6;
3977                  $working += ( $bytevalue & 0x3F );
3978                  $bytesleft--;
3979                  if ( $bytesleft <= 0 ) {
3980                      $result .= "&#x" . strtoupper( dechex( $working ) ) . ";";
3981                  }
3982              } elseif ( $bytevalue <= 0xDF ) { // 110x xxxx
3983                  $working = $bytevalue & 0x1F;
3984                  $bytesleft = 1;
3985              } elseif ( $bytevalue <= 0xEF ) { // 1110 xxxx
3986                  $working = $bytevalue & 0x0F;
3987                  $bytesleft = 2;
3988              } else { // 1111 0xxx
3989                  $working = $bytevalue & 0x07;
3990                  $bytesleft = 3;
3991              }
3992          }
3993          return $result;
3994      }
3995  
3996      /**
3997       * Reverse the previously applied transliteration of non-ASCII characters
3998       * back to UTF-8. Used to protect data from corruption by broken web browsers
3999       * as listed in $wgBrowserBlackList.
4000       *
4001       * @param string $invalue
4002       * @return string
4003       */
4004  	private function unmakeSafe( $invalue ) {
4005          $result = "";
4006          $valueLength = strlen( $invalue );
4007          for ( $i = 0; $i < $valueLength; $i++ ) {
4008              if ( ( substr( $invalue, $i, 3 ) == "&#x" ) && ( $invalue[$i + 3] != '0' ) ) {
4009                  $i += 3;
4010                  $hexstring = "";
4011                  do {
4012                      $hexstring .= $invalue[$i];
4013                      $i++;
4014                  } while ( ctype_xdigit( $invalue[$i] ) && ( $i < strlen( $invalue ) ) );
4015  
4016                  // Do some sanity checks. These aren't needed for reversibility,
4017                  // but should help keep the breakage down if the editor
4018                  // breaks one of the entities whilst editing.
4019                  if ( ( substr( $invalue, $i, 1 ) == ";" ) and ( strlen( $hexstring ) <= 6 ) ) {
4020                      $codepoint = hexdec( $hexstring );
4021                      $result .= codepointToUtf8( $codepoint );
4022                  } else {
4023                      $result .= "&#x" . $hexstring . substr( $invalue, $i, 1 );
4024                  }
4025              } else {
4026                  $result .= substr( $invalue, $i, 1 );
4027              }
4028          }
4029          // reverse the transform that we made for reversibility reasons.
4030          return strtr( $result, array( "&#x0" => "&#x" ) );
4031      }
4032  }


Generated: Fri Nov 28 14:03:12 2014 Cross-referenced by PHPXref 0.7.1