]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/EditPage.php
MediaWiki 1.17.1
[autoinstallsdev/mediawiki.git] / includes / EditPage.php
1 <?php
2 /**
3  * Contains the EditPage class
4  * @file
5  */
6
7 /**
8  * The edit page/HTML interface (split from Article)
9  * The actual database and text munging is still in Article,
10  * but it should get easier to call those from alternate
11  * interfaces.
12  *
13  * EditPage cares about two distinct titles:
14  * $wgTitle is the page that forms submit to, links point to,
15  * redirects go to, etc. $this->mTitle (as well as $mArticle) is the
16  * page in the database that is actually being edited. These are
17  * usually the same, but they are now allowed to be different.
18  */
19 class EditPage {
20         const AS_SUCCESS_UPDATE            = 200;
21         const AS_SUCCESS_NEW_ARTICLE       = 201;
22         const AS_HOOK_ERROR                = 210;
23         const AS_FILTERING                 = 211;
24         const AS_HOOK_ERROR_EXPECTED       = 212;
25         const AS_BLOCKED_PAGE_FOR_USER     = 215;
26         const AS_CONTENT_TOO_BIG           = 216;
27         const AS_USER_CANNOT_EDIT          = 217;
28         const AS_READ_ONLY_PAGE_ANON       = 218;
29         const AS_READ_ONLY_PAGE_LOGGED     = 219;
30         const AS_READ_ONLY_PAGE            = 220;
31         const AS_RATE_LIMITED              = 221;
32         const AS_ARTICLE_WAS_DELETED       = 222;
33         const AS_NO_CREATE_PERMISSION      = 223;
34         const AS_BLANK_ARTICLE             = 224;
35         const AS_CONFLICT_DETECTED         = 225;
36         const AS_SUMMARY_NEEDED            = 226;
37         const AS_TEXTBOX_EMPTY             = 228;
38         const AS_MAX_ARTICLE_SIZE_EXCEEDED = 229;
39         const AS_OK                        = 230;
40         const AS_END                       = 231;
41         const AS_SPAM_ERROR                = 232;
42         const AS_IMAGE_REDIRECT_ANON       = 233;
43         const AS_IMAGE_REDIRECT_LOGGED     = 234;
44
45         var $mArticle;
46         var $mTitle;
47         var $action;
48         var $isConflict = false;
49         var $isCssJsSubpage = false;
50         var $isCssSubpage = false;
51         var $isJsSubpage = false;
52         var $deletedSinceEdit = false;
53         var $formtype;
54         var $firsttime;
55         var $lastDelete;
56         var $mTokenOk = false;
57         var $mTokenOkExceptSuffix = false;
58         var $mTriedSave = false;
59         var $tooBig = false;
60         var $kblength = false;
61         var $missingComment = false;
62         var $missingSummary = false;
63         var $allowBlankSummary = false;
64         var $autoSumm = '';
65         var $hookError = '';
66         #var $mPreviewTemplates;
67         var $mParserOutput;
68         var $mBaseRevision = false;
69         var $mShowSummaryField = true;
70
71         # Form values
72         var $save = false, $preview = false, $diff = false;
73         var $minoredit = false, $watchthis = false, $recreate = false;
74         var $textbox1 = '', $textbox2 = '', $summary = '', $nosummary = false;
75         var $edittime = '', $section = '', $starttime = '';
76         var $oldid = 0, $editintro = '', $scrolltop = null, $bot = true;
77
78         # Placeholders for text injection by hooks (must be HTML)
79         # extensions should take care to _append_ to the present value
80         public $editFormPageTop; // Before even the preview
81         public $editFormTextTop;
82         public $editFormTextBeforeContent;
83         public $editFormTextAfterWarn;
84         public $editFormTextAfterTools;
85         public $editFormTextBottom;
86         public $editFormTextAfterContent;
87         public $previewTextAfterContent;
88
89         /* $didSave should be set to true whenever an article was succesfully altered. */
90         public $didSave = false;
91         public $undidRev = 0;
92
93         public $suppressIntro = false;
94
95         /**
96          * @todo document
97          * @param $article
98          */
99         function __construct( $article ) {
100                 $this->mArticle =& $article;
101                 $this->mTitle = $article->getTitle();
102                 $this->action = 'submit';
103
104                 # Placeholders for text injection by hooks (empty per default)
105                 $this->editFormPageTop =
106                 $this->editFormTextTop =
107                 $this->editFormTextBeforeContent =
108                 $this->editFormTextAfterWarn =
109                 $this->editFormTextAfterTools =
110                 $this->editFormTextBottom =
111                 $this->editFormTextAfterContent =
112                 $this->previewTextAfterContent =
113                 $this->mPreloadText = "";
114         }
115
116         function getArticle() {
117                 return $this->mArticle;
118         }
119
120
121         /**
122          * Fetch initial editing page content.
123          * @returns mixed string on success, $def_text for invalid sections
124          * @private
125          */
126         function getContent( $def_text = '' ) {
127                 global $wgOut, $wgRequest, $wgParser, $wgContLang, $wgMessageCache;
128
129                 wfProfileIn( __METHOD__ );
130                 # Get variables from query string :P
131                 $section = $wgRequest->getVal( 'section' );
132
133                 $preload = $wgRequest->getVal( 'preload',
134                         // Custom preload text for new sections
135                         $section === 'new' ? 'MediaWiki:addsection-preload' : '' );
136                 $undoafter = $wgRequest->getVal( 'undoafter' );
137                 $undo = $wgRequest->getVal( 'undo' );
138
139                 // For message page not locally set, use the i18n message.
140                 // For other non-existent articles, use preload text if any.
141                 if ( !$this->mTitle->exists() ) {
142                         if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
143                                 # If this is a system message, get the default text.
144                                 list( $message, $lang ) = $wgMessageCache->figureMessage( $wgContLang->lcfirst( $this->mTitle->getText() ) );
145                                 $text = wfMsgGetKey( $message, false, $lang, false );
146                                 if( wfEmptyMsg( $message, $text ) )
147                                         $text = $this->getPreloadedText( $preload );
148                         } else {
149                                 # If requested, preload some text.
150                                 $text = $this->getPreloadedText( $preload );
151                         }
152                 // For existing pages, get text based on "undo" or section parameters.
153                 } else {
154                         $text = $this->mArticle->getContent();
155                         if ( $undo > 0 && $undoafter > 0 && $undo < $undoafter ) {
156                                 # If they got undoafter and undo round the wrong way, switch them
157                                 list( $undo, $undoafter ) = array( $undoafter, $undo );
158                         }
159                         if ( $undo > 0 && $undo > $undoafter ) {
160                                 # Undoing a specific edit overrides section editing; section-editing
161                                 # doesn't work with undoing.
162                                 if ( $undoafter ) {
163                                         $undorev = Revision::newFromId( $undo );
164                                         $oldrev = Revision::newFromId( $undoafter );
165                                 } else {
166                                         $undorev = Revision::newFromId( $undo );
167                                         $oldrev = $undorev ? $undorev->getPrevious() : null;
168                                 }
169
170                                 # Sanity check, make sure it's the right page,
171                                 # the revisions exist and they were not deleted.
172                                 # Otherwise, $text will be left as-is.
173                                 if ( !is_null( $undorev ) && !is_null( $oldrev ) &&
174                                         $undorev->getPage() == $oldrev->getPage() &&
175                                         $undorev->getPage() == $this->mArticle->getID() &&
176                                         !$undorev->isDeleted( Revision::DELETED_TEXT ) &&
177                                         !$oldrev->isDeleted( Revision::DELETED_TEXT ) ) {
178
179                                         $undotext = $this->mArticle->getUndoText( $undorev, $oldrev );
180                                         if ( $undotext === false ) {
181                                                 # Warn the user that something went wrong
182                                                 $this->editFormPageTop .= $wgOut->parse( '<div class="error mw-undo-failure">' . wfMsgNoTrans( 'undo-failure' ) . '</div>' );
183                                         } else {
184                                                 $text = $undotext;
185                                                 # Inform the user of our success and set an automatic edit summary
186                                                 $this->editFormPageTop .= $wgOut->parse( '<div class="mw-undo-success">' . wfMsgNoTrans( 'undo-success' ) . '</div>' );
187                                                 $firstrev = $oldrev->getNext();
188                                                 # If we just undid one rev, use an autosummary
189                                                 if ( $firstrev->mId == $undo ) {
190                                                         $this->summary = wfMsgForContent( 'undo-summary', $undo, $undorev->getUserText() );
191                                                         $this->undidRev = $undo;
192                                                 }
193                                                 $this->formtype = 'diff';
194                                         }
195                                 } else {
196                                         // Failed basic sanity checks.
197                                         // Older revisions may have been removed since the link
198                                         // was created, or we may simply have got bogus input.
199                                         $this->editFormPageTop .= $wgOut->parse( '<div class="error mw-undo-norev">' . wfMsgNoTrans( 'undo-norev' ) . '</div>' );
200                                 }
201                         } else if ( $section != '' ) {
202                                 if ( $section == 'new' ) {
203                                         $text = $this->getPreloadedText( $preload );
204                                 } else {
205                                         // Get section edit text (returns $def_text for invalid sections)
206                                         $text = $wgParser->getSection( $text, $section, $def_text );
207                                 }
208                         }
209                 }
210
211                 wfProfileOut( __METHOD__ );
212                 return $text;
213         }
214
215         /** Use this method before edit() to preload some text into the edit box */
216         public function setPreloadedText( $text ) {
217                 $this->mPreloadText = $text;
218         }
219
220         /**
221          * Get the contents to be preloaded into the box, either set by
222          * an earlier setPreloadText() or by loading the given page.
223          *
224          * @param $preload String: representing the title to preload from.
225          * @return String
226          */
227         protected function getPreloadedText( $preload ) {
228                 global $wgUser, $wgParser;
229                 if ( !empty( $this->mPreloadText ) ) {
230                         return $this->mPreloadText;
231                 } elseif ( $preload !== '' ) {
232                         $title = Title::newFromText( $preload );
233                         # Check for existence to avoid getting MediaWiki:Noarticletext
234                         if ( isset( $title ) && $title->exists() && $title->userCanRead() ) {
235                                 $article = new Article( $title );
236
237                                 if ( $article->isRedirect() ) {
238                                         $title = Title::newFromRedirectRecurse( $article->getContent() );
239                                         # Redirects to missing titles are displayed, to hidden pages are followed
240                                         # Copying observed behaviour from ?action=view
241                                         if ( $title->exists() ) {
242                                                 if ($title->userCanRead() ) {
243                                                         $article = new Article( $title );
244                                                 } else {
245                                                         return "";
246                                                 }
247                                         }
248                                 }
249                                 $parserOptions = ParserOptions::newFromUser( $wgUser );
250                                 return $wgParser->getPreloadText( $article->getContent(), $title, $parserOptions );
251                         }
252                 }
253                 return '';
254         }
255
256         /*
257          * Check if a page was deleted while the user was editing it, before submit.
258          * Note that we rely on the logging table, which hasn't been always there,
259          * but that doesn't matter, because this only applies to brand new
260          * deletes.
261          */
262         protected function wasDeletedSinceLastEdit() {
263                 if ( $this->deletedSinceEdit )
264                         return true;
265                 if ( $this->mTitle->isDeletedQuick() ) {
266                         $this->lastDelete = $this->getLastDelete();
267                         if ( $this->lastDelete ) {
268                                 $deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp );
269                                 if ( $deleteTime > $this->starttime ) {
270                                         $this->deletedSinceEdit = true;
271                                 }
272                         }
273                 }
274                 return $this->deletedSinceEdit;
275         }
276
277         /**
278          * Checks whether the user entered a skin name in uppercase,
279          * e.g. "User:Example/Monobook.css" instead of "monobook.css"
280          */
281         protected function isWrongCaseCssJsPage() {
282                 if( $this->mTitle->isCssJsSubpage() ) {
283                         $name = $this->mTitle->getSkinFromCssJsSubpage();
284                         $skins = array_merge(
285                                 array_keys( Skin::getSkinNames() ),
286                                 array( 'common' )
287                         );
288                         return !in_array( $name, $skins )
289                                 && in_array( strtolower( $name ), $skins );
290                 } else {
291                         return false;
292                 }
293         }
294
295         function submit() {
296                 $this->edit();
297         }
298
299         /**
300          * This is the function that gets called for "action=edit". It
301          * sets up various member variables, then passes execution to
302          * another function, usually showEditForm()
303          *
304          * The edit form is self-submitting, so that when things like
305          * preview and edit conflicts occur, we get the same form back
306          * with the extra stuff added.  Only when the final submission
307          * is made and all is well do we actually save and redirect to
308          * the newly-edited page.
309          */
310         function edit() {
311                 global $wgOut, $wgRequest, $wgUser;
312                 // Allow extensions to modify/prevent this form or submission
313                 if ( !wfRunHooks( 'AlternateEdit', array( $this ) ) ) {
314                         return;
315                 }
316
317                 wfProfileIn( __METHOD__ );
318                 wfDebug( __METHOD__.": enter\n" );
319
320                 // This is not an article
321                 $wgOut->setArticleFlag( false );
322
323                 $this->importFormData( $wgRequest );
324                 $this->firsttime = false;
325
326                 if ( $this->live ) {
327                         $this->livePreview();
328                         wfProfileOut( __METHOD__ );
329                         return;
330                 }
331
332                 if ( wfReadOnly() && $this->save ) {
333                         // Force preview
334                         $this->save = false;
335                         $this->preview = true;
336                 }
337
338                 $wgOut->addModules( array( 'mediawiki.legacy.edit', 'mediawiki.action.edit' ) );
339
340                 if ( $wgUser->getOption( 'uselivepreview', false ) ) {
341                         $wgOut->addModules( 'mediawiki.legacy.preview' );
342                 }
343                 // Bug #19334: textarea jumps when editing articles in IE8
344                 $wgOut->addStyle( 'common/IE80Fixes.css', 'screen', 'IE 8' );
345
346                 $permErrors = $this->getEditPermissionErrors();
347                 if ( $permErrors ) {
348                         wfDebug( __METHOD__ . ": User can't edit\n" );
349                         $content = $this->getContent( null );
350                         $content = $content === '' ? null : $content;
351                         $this->readOnlyPage( $content, true, $permErrors, 'edit' );
352                         wfProfileOut( __METHOD__ );
353                         return;
354                 } else {
355                         if ( $this->save ) {
356                                 $this->formtype = 'save';
357                         } else if ( $this->preview ) {
358                                 $this->formtype = 'preview';
359                         } else if ( $this->diff ) {
360                                 $this->formtype = 'diff';
361                         } else { # First time through
362                                 $this->firsttime = true;
363                                 if ( $this->previewOnOpen() ) {
364                                         $this->formtype = 'preview';
365                                 } else {
366                                         $this->formtype = 'initial';
367                                 }
368                         }
369                 }
370
371                 // If they used redlink=1 and the page exists, redirect to the main article
372                 if ( $wgRequest->getBool( 'redlink' ) && $this->mTitle->exists() ) {
373                         $wgOut->redirect( $this->mTitle->getFullURL() );
374                 }
375
376                 wfProfileIn( __METHOD__."-business-end" );
377
378                 $this->isConflict = false;
379                 // css / js subpages of user pages get a special treatment
380                 $this->isCssJsSubpage      = $this->mTitle->isCssJsSubpage();
381                 $this->isCssSubpage        = $this->mTitle->isCssSubpage();
382                 $this->isJsSubpage         = $this->mTitle->isJsSubpage();
383                 $this->isWrongCaseCssJsPage = $this->isWrongCaseCssJsPage();
384
385                 # Show applicable editing introductions
386                 if ( $this->formtype == 'initial' || $this->firsttime )
387                         $this->showIntro();
388
389                 if ( $this->mTitle->isTalkPage() ) {
390                         $wgOut->addWikiMsg( 'talkpagetext' );
391                 }
392
393                 # Optional notices on a per-namespace and per-page basis
394                 $editnotice_ns   = 'editnotice-'.$this->mTitle->getNamespace();
395                 if ( !wfEmptyMsg( $editnotice_ns, wfMsgForContent( $editnotice_ns ) ) ) {
396                         $wgOut->addWikiText( wfMsgForContent( $editnotice_ns )  );
397                 }
398                 if ( MWNamespace::hasSubpages( $this->mTitle->getNamespace() ) ) {
399                         $parts = explode( '/', $this->mTitle->getDBkey() );
400                         $editnotice_base = $editnotice_ns;
401                         while ( count( $parts ) > 0 ) {
402                                 $editnotice_base .= '-'.array_shift( $parts );
403                                 if ( !wfEmptyMsg( $editnotice_base, wfMsgForContent( $editnotice_base ) ) ) {
404                                         $wgOut->addWikiText( wfMsgForContent( $editnotice_base )  );
405                                 }
406                         }
407                 }
408
409                 # Attempt submission here.  This will check for edit conflicts,
410                 # and redundantly check for locked database, blocked IPs, etc.
411                 # that edit() already checked just in case someone tries to sneak
412                 # in the back door with a hand-edited submission URL.
413
414                 if ( 'save' == $this->formtype ) {
415                         if ( !$this->attemptSave() ) {
416                                 wfProfileOut( __METHOD__."-business-end" );
417                                 wfProfileOut( __METHOD__ );
418                                 return;
419                         }
420                 }
421
422                 # First time through: get contents, set time for conflict
423                 # checking, etc.
424                 if ( 'initial' == $this->formtype || $this->firsttime ) {
425                         if ( $this->initialiseForm() === false ) {
426                                 $this->noSuchSectionPage();
427                                 wfProfileOut( __METHOD__."-business-end" );
428                                 wfProfileOut( __METHOD__ );
429                                 return;
430                         }
431                         if ( !$this->mTitle->getArticleId() )
432                                 wfRunHooks( 'EditFormPreloadText', array( &$this->textbox1, &$this->mTitle ) );
433                         else
434                                 wfRunHooks( 'EditFormInitialText', array( $this ) );
435                 }
436
437                 $this->showEditForm();
438                 wfProfileOut( __METHOD__."-business-end" );
439                 wfProfileOut( __METHOD__ );
440         }
441
442         protected function getEditPermissionErrors() {
443                 global $wgUser;
444                 $permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser );
445                 # Can this title be created?
446                 if ( !$this->mTitle->exists() ) {
447                         $permErrors = array_merge( $permErrors,
448                                 wfArrayDiff2( $this->mTitle->getUserPermissionsErrors( 'create', $wgUser ), $permErrors ) );
449                 }
450                 # Ignore some permissions errors when a user is just previewing/viewing diffs
451                 $remove = array();
452                 foreach( $permErrors as $error ) {
453                         if ( ( $this->preview || $this->diff ) &&
454                                 ( $error[0] == 'blockedtext' || $error[0] == 'autoblockedtext' ) )
455                         {
456                                 $remove[] = $error;
457                         }
458                 }
459                 $permErrors = wfArrayDiff2( $permErrors, $remove );
460                 return $permErrors;
461         }
462
463         /**
464          * Show a read-only error
465          * Parameters are the same as OutputPage:readOnlyPage()
466          * Redirect to the article page if redlink=1
467          */
468         function readOnlyPage( $source = null, $protected = false, $reasons = array(), $action = null ) {
469                 global $wgRequest, $wgOut;
470                 if ( $wgRequest->getBool( 'redlink' ) ) {
471                         // The edit page was reached via a red link.
472                         // Redirect to the article page and let them click the edit tab if
473                         // they really want a permission error.
474                         $wgOut->redirect( $this->mTitle->getFullUrl() );
475                 } else {
476                         $wgOut->readOnlyPage( $source, $protected, $reasons, $action );
477                 }
478         }
479
480         /**
481          * Should we show a preview when the edit form is first shown?
482          *
483          * @return bool
484          */
485         protected function previewOnOpen() {
486                 global $wgRequest, $wgUser, $wgPreviewOnOpenNamespaces;
487                 if ( $wgRequest->getVal( 'preview' ) == 'yes' ) {
488                         // Explicit override from request
489                         return true;
490                 } elseif ( $wgRequest->getVal( 'preview' ) == 'no' ) {
491                         // Explicit override from request
492                         return false;
493                 } elseif ( $this->section == 'new' ) {
494                         // Nothing *to* preview for new sections
495                         return false;
496                 } elseif ( ( $wgRequest->getVal( 'preload' ) !== null || $this->mTitle->exists() ) && $wgUser->getOption( 'previewonfirst' ) ) {
497                         // Standard preference behaviour
498                         return true;
499                 } elseif ( !$this->mTitle->exists() &&
500                   isset($wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()]) &&
501                   $wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()] )
502                 {
503                         // Categories are special
504                         return true;
505                 } else {
506                         return false;
507                 }
508         }
509
510         /**
511          * Does this EditPage class support section editing?
512          * This is used by EditPage subclasses to indicate their ui cannot handle section edits
513          *
514          * @return bool
515          */
516         protected function isSectionEditSupported() {
517                 return true;
518         }
519
520         /**
521          * Returns the URL to use in the form's action attribute.
522          * This is used by EditPage subclasses when simply customizing the action
523          * variable in the constructor is not enough. This can be used when the
524          * EditPage lives inside of a Special page rather than a custom page action.
525          *
526          * @param $title Title object for which is being edited (where we go to for &action= links)
527          * @return string
528          */
529         protected function getActionURL( Title $title ) {
530                 return $title->getLocalURL( array( 'action' => $this->action ) );
531         }
532
533         /**
534          * @todo document
535          * @param $request
536          */
537         function importFormData( &$request ) {
538                 global $wgLang, $wgUser;
539
540                 wfProfileIn( __METHOD__ );
541
542                 # Section edit can come from either the form or a link
543                 $this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) );
544
545                 if ( $request->wasPosted() ) {
546                         # These fields need to be checked for encoding.
547                         # Also remove trailing whitespace, but don't remove _initial_
548                         # whitespace from the text boxes. This may be significant formatting.
549                         $this->textbox1 = $this->safeUnicodeInput( $request, 'wpTextbox1' );
550                         if ( !$request->getCheck('wpTextbox2') ) {
551                                 // Skip this if wpTextbox2 has input, it indicates that we came
552                                 // from a conflict page with raw page text, not a custom form
553                                 // modified by subclasses
554                                 wfProfileIn( get_class($this)."::importContentFormData" );
555                                 $textbox1 = $this->importContentFormData( $request );
556                                 if ( isset($textbox1) )
557                                         $this->textbox1 = $textbox1;
558                                 wfProfileOut( get_class($this)."::importContentFormData" );
559                         }
560
561                         # Truncate for whole multibyte characters. +5 bytes for ellipsis
562                         $this->summary = $wgLang->truncate( $request->getText( 'wpSummary' ), 250, '' );
563
564                         # Remove extra headings from summaries and new sections.
565                         $this->summary = preg_replace('/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary);
566
567                         $this->edittime = $request->getVal( 'wpEdittime' );
568                         $this->starttime = $request->getVal( 'wpStarttime' );
569
570                         $this->scrolltop = $request->getIntOrNull( 'wpScrolltop' );
571
572                         if ( is_null( $this->edittime ) ) {
573                                 # If the form is incomplete, force to preview.
574                                 wfDebug( __METHOD__ . ": Form data appears to be incomplete\n" );
575                                 wfDebug( "POST DATA: " . var_export( $_POST, true ) . "\n" );
576                                 $this->preview = true;
577                         } else {
578                                 /* Fallback for live preview */
579                                 $this->preview = $request->getCheck( 'wpPreview' ) || $request->getCheck( 'wpLivePreview' );
580                                 $this->diff = $request->getCheck( 'wpDiff' );
581
582                                 // Remember whether a save was requested, so we can indicate
583                                 // if we forced preview due to session failure.
584                                 $this->mTriedSave = !$this->preview;
585
586                                 if ( $this->tokenOk( $request ) ) {
587                                         # Some browsers will not report any submit button
588                                         # if the user hits enter in the comment box.
589                                         # The unmarked state will be assumed to be a save,
590                                         # if the form seems otherwise complete.
591                                         wfDebug( __METHOD__ . ": Passed token check.\n" );
592                                 } else if ( $this->diff ) {
593                                         # Failed token check, but only requested "Show Changes".
594                                         wfDebug( __METHOD__ . ": Failed token check; Show Changes requested.\n" );
595                                 } else {
596                                         # Page might be a hack attempt posted from
597                                         # an external site. Preview instead of saving.
598                                         wfDebug( __METHOD__ . ": Failed token check; forcing preview\n" );
599                                         $this->preview = true;
600                                 }
601                         }
602                         $this->save = !$this->preview && !$this->diff;
603                         if ( !preg_match( '/^\d{14}$/', $this->edittime ) ) {
604                                 $this->edittime = null;
605                         }
606
607                         if ( !preg_match( '/^\d{14}$/', $this->starttime ) ) {
608                                 $this->starttime = null;
609                         }
610
611                         $this->recreate  = $request->getCheck( 'wpRecreate' );
612
613                         $this->minoredit = $request->getCheck( 'wpMinoredit' );
614                         $this->watchthis = $request->getCheck( 'wpWatchthis' );
615
616                         # Don't force edit summaries when a user is editing their own user or talk page
617                         if ( ( $this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK ) &&
618                                 $this->mTitle->getText() == $wgUser->getName() )
619                         {
620                                 $this->allowBlankSummary = true;
621                         } else {
622                                 $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' ) || !$wgUser->getOption( 'forceeditsummary');
623                         }
624
625                         $this->autoSumm = $request->getText( 'wpAutoSummary' );
626                 } else {
627                         # Not a posted form? Start with nothing.
628                         wfDebug( __METHOD__ . ": Not a posted form.\n" );
629                         $this->textbox1  = '';
630                         $this->summary   = '';
631                         $this->edittime  = '';
632                         $this->starttime = wfTimestampNow();
633                         $this->edit      = false;
634                         $this->preview   = false;
635                         $this->save      = false;
636                         $this->diff      = false;
637                         $this->minoredit = false;
638                         $this->watchthis = $request->getBool( 'watchthis', false ); // Watch may be overriden by request parameters
639                         $this->recreate  = false;
640
641                         if ( $this->section == 'new' && $request->getVal( 'preloadtitle' ) ) {
642                                 $this->summary = $request->getVal( 'preloadtitle' );
643                         }
644                         elseif ( $this->section != 'new' && $request->getVal( 'summary' ) ) {
645                                 $this->summary = $request->getText( 'summary' );
646                         }
647
648                         if ( $request->getVal( 'minor' ) ) {
649                                 $this->minoredit = true;
650                         }
651                 }
652
653                 $this->bot = $request->getBool( 'bot', true );
654                 $this->nosummary = $request->getBool( 'nosummary' );
655
656                 // FIXME: unused variable?
657                 $this->oldid = $request->getInt( 'oldid' );
658
659                 $this->live = $request->getCheck( 'live' );
660                 $this->editintro = $request->getText( 'editintro',
661                         // Custom edit intro for new sections
662                         $this->section === 'new' ? 'MediaWiki:addsection-editintro' : '' );
663
664                 // Allow extensions to modify form data
665                 wfRunHooks( 'EditPage::importFormData', array( $this, $request ) );
666
667                 wfProfileOut( __METHOD__ );
668         }
669
670         /**
671          * Subpage overridable method for extracting the page content data from the
672          * posted form to be placed in $this->textbox1, if using customized input
673          * this method should be overrided and return the page text that will be used
674          * for saving, preview parsing and so on...
675          *
676          * @param $request WebRequest
677          */
678         protected function importContentFormData( &$request ) {
679                 return; // Don't do anything, EditPage already extracted wpTextbox1
680         }
681
682         /**
683          * Make sure the form isn't faking a user's credentials.
684          *
685          * @param $request WebRequest
686          * @return bool
687          * @private
688          */
689         function tokenOk( &$request ) {
690                 global $wgUser;
691                 $token = $request->getVal( 'wpEditToken' );
692                 $this->mTokenOk = $wgUser->matchEditToken( $token );
693                 $this->mTokenOkExceptSuffix = $wgUser->matchEditTokenNoSuffix( $token );
694                 return $this->mTokenOk;
695         }
696
697         /**
698          * Show all applicable editing introductions
699          */
700         protected function showIntro() {
701                 global $wgOut, $wgUser;
702                 if ( $this->suppressIntro ) {
703                         return;
704                 }
705
706                 $namespace = $this->mTitle->getNamespace();
707
708                 if ( $namespace == NS_MEDIAWIKI ) {
709                         # Show a warning if editing an interface message
710                         $wgOut->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' );
711                 }
712
713                 # Show a warning message when someone creates/edits a user (talk) page but the user does not exist
714                 # Show log extract when the user is currently blocked
715                 if ( $namespace == NS_USER || $namespace == NS_USER_TALK ) {
716                         $parts = explode( '/', $this->mTitle->getText(), 2 );
717                         $username = $parts[0];
718                         $user = User::newFromName( $username, false /* allow IP users*/ );
719                         $ip = User::isIP( $username );
720                         if ( !$user->isLoggedIn() && !$ip ) { # User does not exist
721                                 $wgOut->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
722                                         array( 'userpage-userdoesnotexist', $username ) );
723                         } else if ( $user->isBlocked() ) { # Show log extract if the user is currently blocked
724                                 LogEventsList::showLogExtract(
725                                         $wgOut,
726                                         'block',
727                                         $user->getUserPage()->getPrefixedText(),
728                                         '',
729                                         array(
730                                                 'lim' => 1,
731                                                 'showIfEmpty' => false,
732                                                 'msgKey' => array(
733                                                         'blocked-notice-logextract',
734                                                         $user->getName() # Support GENDER in notice
735                                                 )
736                                         )
737                                 );
738                         }
739                 }
740                 # Try to add a custom edit intro, or use the standard one if this is not possible.
741                 if ( !$this->showCustomIntro() && !$this->mTitle->exists() ) {
742                         if ( $wgUser->isLoggedIn() ) {
743                                 $wgOut->wrapWikiMsg( "<div class=\"mw-newarticletext\">\n$1\n</div>", 'newarticletext' );
744                         } else {
745                                 $wgOut->wrapWikiMsg( "<div class=\"mw-newarticletextanon\">\n$1\n</div>", 'newarticletextanon' );
746                         }
747                 }
748                 # Give a notice if the user is editing a deleted/moved page...
749                 if ( !$this->mTitle->exists() ) {
750                         LogEventsList::showLogExtract( $wgOut, array( 'delete', 'move' ), $this->mTitle->getPrefixedText(),
751                                 '', array( 'lim' => 10,
752                                            'conds' => array( "log_action != 'revision'" ),
753                                            'showIfEmpty' => false,
754                                            'msgKey' => array( 'recreate-moveddeleted-warn') )
755                         );
756                 }
757         }
758
759         /**
760          * Attempt to show a custom editing introduction, if supplied
761          *
762          * @return bool
763          */
764         protected function showCustomIntro() {
765                 if ( $this->editintro ) {
766                         $title = Title::newFromText( $this->editintro );
767                         if ( $title instanceof Title && $title->exists() && $title->userCanRead() ) {
768                                 global $wgOut;
769                                 $revision = Revision::newFromTitle( $title );
770                                 $wgOut->addWikiTextTitleTidy( $revision->getText(), $this->mTitle );
771                                 return true;
772                         } else {
773                                 return false;
774                         }
775                 } else {
776                         return false;
777                 }
778         }
779
780         /**
781          * Attempt submission (no UI)
782          * @return one of the constants describing the result
783          */
784         function internalAttemptSave( &$result, $bot = false ) {
785                 global $wgFilterCallback, $wgUser, $wgParser;
786                 global $wgMaxArticleSize;
787
788                 wfProfileIn( __METHOD__  );
789                 wfProfileIn( __METHOD__ . '-checks' );
790
791                 if ( !wfRunHooks( 'EditPage::attemptSave', array( $this ) ) ) {
792                         wfDebug( "Hook 'EditPage::attemptSave' aborted article saving\n" );
793                         wfProfileOut( __METHOD__ . '-checks' );
794                         wfProfileOut( __METHOD__  );
795                         return self::AS_HOOK_ERROR;
796                 }
797
798                 # Check image redirect
799                 if ( $this->mTitle->getNamespace() == NS_FILE &&
800                         Title::newFromRedirect( $this->textbox1 ) instanceof Title &&
801                         !$wgUser->isAllowed( 'upload' ) ) {
802                                 $isAnon = $wgUser->isAnon();
803
804                                 wfProfileOut( __METHOD__ . '-checks' );
805                                 wfProfileOut( __METHOD__  );
806
807                                 return $isAnon ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED;
808                 }
809
810                 # Check for spam
811                 $match = self::matchSummarySpamRegex( $this->summary );
812                 if ( $match === false ) {
813                         $match = self::matchSpamRegex( $this->textbox1 );
814                 }
815                 if ( $match !== false ) {
816                         $result['spam'] = $match;
817                         $ip = wfGetIP();
818                         $pdbk = $this->mTitle->getPrefixedDBkey();
819                         $match = str_replace( "\n", '', $match );
820                         wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" );
821                         wfProfileOut( __METHOD__ . '-checks' );
822                         wfProfileOut( __METHOD__ );
823                         return self::AS_SPAM_ERROR;
824                 }
825                 if ( $wgFilterCallback && $wgFilterCallback( $this->mTitle, $this->textbox1, $this->section, $this->hookError, $this->summary ) ) {
826                         # Error messages or other handling should be performed by the filter function
827                         wfProfileOut( __METHOD__ . '-checks' );
828                         wfProfileOut( __METHOD__ );
829                         return self::AS_FILTERING;
830                 }
831                 if ( !wfRunHooks( 'EditFilter', array( $this, $this->textbox1, $this->section, &$this->hookError, $this->summary ) ) ) {
832                         # Error messages etc. could be handled within the hook...
833                         wfProfileOut( __METHOD__ . '-checks' );
834                         wfProfileOut( __METHOD__ );
835                         return self::AS_HOOK_ERROR;
836                 } elseif ( $this->hookError != '' ) {
837                         # ...or the hook could be expecting us to produce an error
838                         wfProfileOut( __METHOD__ . '-checks' );
839                         wfProfileOut( __METHOD__ );
840                         return self::AS_HOOK_ERROR_EXPECTED;
841                 }
842                 if ( $wgUser->isBlockedFrom( $this->mTitle, false ) ) {
843                         # Check block state against master, thus 'false'.
844                         wfProfileOut( __METHOD__ . '-checks' );
845                         wfProfileOut( __METHOD__ );
846                         return self::AS_BLOCKED_PAGE_FOR_USER;
847                 }
848                 $this->kblength = (int)( strlen( $this->textbox1 ) / 1024 );
849                 if ( $this->kblength > $wgMaxArticleSize ) {
850                         // Error will be displayed by showEditForm()
851                         $this->tooBig = true;
852                         wfProfileOut( __METHOD__ . '-checks' );
853                         wfProfileOut( __METHOD__ );
854                         return self::AS_CONTENT_TOO_BIG;
855                 }
856
857                 if ( !$wgUser->isAllowed( 'edit' ) ) {
858                         if ( $wgUser->isAnon() ) {
859                                 wfProfileOut( __METHOD__ . '-checks' );
860                                 wfProfileOut( __METHOD__ );
861                                 return self::AS_READ_ONLY_PAGE_ANON;
862                         } else {
863                                 wfProfileOut( __METHOD__ . '-checks' );
864                                 wfProfileOut( __METHOD__ );
865                                 return self::AS_READ_ONLY_PAGE_LOGGED;
866                         }
867                 }
868
869                 if ( wfReadOnly() ) {
870                         wfProfileOut( __METHOD__ . '-checks' );
871                         wfProfileOut( __METHOD__ );
872                         return self::AS_READ_ONLY_PAGE;
873                 }
874                 if ( $wgUser->pingLimiter() ) {
875                         wfProfileOut( __METHOD__ . '-checks' );
876                         wfProfileOut( __METHOD__ );
877                         return self::AS_RATE_LIMITED;
878                 }
879
880                 # If the article has been deleted while editing, don't save it without
881                 # confirmation
882                 if ( $this->wasDeletedSinceLastEdit() && !$this->recreate ) {
883                         wfProfileOut( __METHOD__ . '-checks' );
884                         wfProfileOut( __METHOD__ );
885                         return self::AS_ARTICLE_WAS_DELETED;
886                 }
887
888                 wfProfileOut( __METHOD__ . '-checks' );
889
890                 # If article is new, insert it.
891                 $aid = $this->mTitle->getArticleID( Title::GAID_FOR_UPDATE );
892                 if ( 0 == $aid ) {
893                         // Late check for create permission, just in case *PARANOIA*
894                         if ( !$this->mTitle->userCan( 'create' ) ) {
895                                 wfDebug( __METHOD__ . ": no create permission\n" );
896                                 wfProfileOut( __METHOD__ );
897                                 return self::AS_NO_CREATE_PERMISSION;
898                         }
899
900                         # Don't save a new article if it's blank.
901                         if ( $this->textbox1 == '' ) {
902                                 wfProfileOut( __METHOD__ );
903                                 return self::AS_BLANK_ARTICLE;
904                         }
905
906                         // Run post-section-merge edit filter
907                         if ( !wfRunHooks( 'EditFilterMerged', array( $this, $this->textbox1, &$this->hookError, $this->summary ) ) ) {
908                                 # Error messages etc. could be handled within the hook...
909                                 wfProfileOut( __METHOD__ );
910                                 return self::AS_HOOK_ERROR;
911                         } elseif ( $this->hookError != '' ) {
912                                 # ...or the hook could be expecting us to produce an error
913                                 wfProfileOut( __METHOD__ );
914                                 return self::AS_HOOK_ERROR_EXPECTED;
915                         }
916
917                         # Handle the user preference to force summaries here. Check if it's not a redirect.
918                         if ( !$this->allowBlankSummary && !Title::newFromRedirect( $this->textbox1 ) ) {
919                                 if ( md5( $this->summary ) == $this->autoSumm ) {
920                                         $this->missingSummary = true;
921                                         wfProfileOut( __METHOD__ );
922                                         return self::AS_SUMMARY_NEEDED;
923                                 }
924                         }
925
926                         $isComment = ( $this->section == 'new' );
927
928                         $this->mArticle->insertNewArticle( $this->textbox1, $this->summary,
929                                 $this->minoredit, $this->watchthis, false, $isComment, $bot );
930
931                         wfProfileOut( __METHOD__ );
932                         return self::AS_SUCCESS_NEW_ARTICLE;
933                 }
934
935                 # Article exists. Check for edit conflict.
936
937                 $this->mArticle->clear(); # Force reload of dates, etc.
938                 $this->mArticle->forUpdate( true ); # Lock the article
939
940                 wfDebug( "timestamp: {$this->mArticle->getTimestamp()}, edittime: {$this->edittime}\n" );
941
942                 if ( $this->mArticle->getTimestamp() != $this->edittime ) {
943                         $this->isConflict = true;
944                         if ( $this->section == 'new' ) {
945                                 if ( $this->mArticle->getUserText() == $wgUser->getName() &&
946                                         $this->mArticle->getComment() == $this->summary ) {
947                                         // Probably a duplicate submission of a new comment.
948                                         // This can happen when squid resends a request after
949                                         // a timeout but the first one actually went through.
950                                         wfDebug( __METHOD__ . ": duplicate new section submission; trigger edit conflict!\n" );
951                                 } else {
952                                         // New comment; suppress conflict.
953                                         $this->isConflict = false;
954                                         wfDebug( __METHOD__ .": conflict suppressed; new section\n" );
955                                 }
956                         }
957                 }
958                 $userid = $wgUser->getId();
959
960                 # Suppress edit conflict with self, except for section edits where merging is required.
961                 if ( $this->isConflict && $this->section == '' && $this->userWasLastToEdit( $userid, $this->edittime ) ) {
962                         wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" );
963                         $this->isConflict = false;
964                 }
965
966                 if ( $this->isConflict ) {
967                         wfDebug( __METHOD__ . ": conflict! getting section '$this->section' for time '$this->edittime' (article time '" .
968                                 $this->mArticle->getTimestamp() . "')\n" );
969                         $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary, $this->edittime );
970                 } else {
971                         wfDebug( __METHOD__ . ": getting section '$this->section'\n" );
972                         $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary );
973                 }
974                 if ( is_null( $text ) ) {
975                         wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
976                         $this->isConflict = true;
977                         $text = $this->textbox1; // do not try to merge here!
978                 } else if ( $this->isConflict ) {
979                         # Attempt merge
980                         if ( $this->mergeChangesInto( $text ) ) {
981                                 // Successful merge! Maybe we should tell the user the good news?
982                                 $this->isConflict = false;
983                                 wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
984                         } else {
985                                 $this->section = '';
986                                 $this->textbox1 = $text;
987                                 wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
988                         }
989                 }
990
991                 if ( $this->isConflict ) {
992                         wfProfileOut( __METHOD__ );
993                         return self::AS_CONFLICT_DETECTED;
994                 }
995
996                 $oldtext = $this->mArticle->getContent();
997
998                 // Run post-section-merge edit filter
999                 if ( !wfRunHooks( 'EditFilterMerged', array( $this, $text, &$this->hookError, $this->summary ) ) ) {
1000                         # Error messages etc. could be handled within the hook...
1001                         wfProfileOut( __METHOD__ );
1002                         return self::AS_HOOK_ERROR;
1003                 } elseif ( $this->hookError != '' ) {
1004                         # ...or the hook could be expecting us to produce an error
1005                         wfProfileOut( __METHOD__ );
1006                         return self::AS_HOOK_ERROR_EXPECTED;
1007                 }
1008
1009                 # Handle the user preference to force summaries here, but not for null edits
1010                 if ( $this->section != 'new' && !$this->allowBlankSummary && 0 != strcmp( $oldtext, $text )
1011                         && !Title::newFromRedirect( $text ) ) # check if it's not a redirect
1012                 {
1013                         if ( md5( $this->summary ) == $this->autoSumm ) {
1014                                 $this->missingSummary = true;
1015                                 wfProfileOut( __METHOD__ );
1016                                 return self::AS_SUMMARY_NEEDED;
1017                         }
1018                 }
1019
1020                 # And a similar thing for new sections
1021                 if ( $this->section == 'new' && !$this->allowBlankSummary ) {
1022                         if ( trim( $this->summary ) == '' ) {
1023                                 $this->missingSummary = true;
1024                                 wfProfileOut( __METHOD__ );
1025                                 return self::AS_SUMMARY_NEEDED;
1026                         }
1027                 }
1028
1029                 # All's well
1030                 wfProfileIn( __METHOD__ . '-sectionanchor' );
1031                 $sectionanchor = '';
1032                 if ( $this->section == 'new' ) {
1033                         if ( $this->textbox1 == '' ) {
1034                                 $this->missingComment = true;
1035                                 wfProfileOut( __METHOD__ . '-sectionanchor' );
1036                                 wfProfileOut( __METHOD__ );
1037                                 return self::AS_TEXTBOX_EMPTY;
1038                         }
1039                         if ( $this->summary != '' ) {
1040                                 $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
1041                                 # This is a new section, so create a link to the new section
1042                                 # in the revision summary.
1043                                 $cleanSummary = $wgParser->stripSectionName( $this->summary );
1044                                 $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary );
1045                         }
1046                 } elseif ( $this->section != '' ) {
1047                         # Try to get a section anchor from the section source, redirect to edited section if header found
1048                         # XXX: might be better to integrate this into Article::replaceSection
1049                         # for duplicate heading checking and maybe parsing
1050                         $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
1051                         # we can't deal with anchors, includes, html etc in the header for now,
1052                         # headline would need to be parsed to improve this
1053                         if ( $hasmatch and strlen( $matches[2] ) > 0 ) {
1054                                 $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $matches[2] );
1055                         }
1056                 }
1057                 wfProfileOut( __METHOD__ . '-sectionanchor' );
1058
1059                 // Save errors may fall down to the edit form, but we've now
1060                 // merged the section into full text. Clear the section field
1061                 // so that later submission of conflict forms won't try to
1062                 // replace that into a duplicated mess.
1063                 $this->textbox1 = $text;
1064                 $this->section = '';
1065
1066                 // Check for length errors again now that the section is merged in
1067                 $this->kblength = (int)( strlen( $text ) / 1024 );
1068                 if ( $this->kblength > $wgMaxArticleSize ) {
1069                         $this->tooBig = true;
1070                         wfProfileOut( __METHOD__ );
1071                         return self::AS_MAX_ARTICLE_SIZE_EXCEEDED;
1072                 }
1073
1074                 # update the article here
1075                 if ( $this->mArticle->updateArticle( $text, $this->summary, $this->minoredit,
1076                         $this->watchthis, $bot, $sectionanchor ) )
1077                 {
1078                         wfProfileOut( __METHOD__ );
1079                         return self::AS_SUCCESS_UPDATE;
1080                 } else {
1081                         $this->isConflict = true;
1082                 }
1083                 wfProfileOut( __METHOD__ );
1084                 return self::AS_END;
1085         }
1086
1087         /**
1088          * Check if no edits were made by other users since
1089          * the time a user started editing the page. Limit to
1090          * 50 revisions for the sake of performance.
1091          */
1092         protected function userWasLastToEdit( $id, $edittime ) {
1093                 if( !$id ) return false;
1094                 $dbw = wfGetDB( DB_MASTER );
1095                 $res = $dbw->select( 'revision',
1096                         'rev_user',
1097                         array(
1098                                 'rev_page' => $this->mArticle->getId(),
1099                                 'rev_timestamp > '.$dbw->addQuotes( $dbw->timestamp($edittime) )
1100                         ),
1101                         __METHOD__,
1102                         array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ) );
1103                 foreach ( $res as $row ) {
1104                         if( $row->rev_user != $id ) {
1105                                 return false;
1106                         }
1107                 }
1108                 return true;
1109         }
1110
1111         /**
1112          * Check given input text against $wgSpamRegex, and return the text of the first match.
1113          * @return mixed -- matching string or false
1114          */
1115         public static function matchSpamRegex( $text ) {
1116                 global $wgSpamRegex;
1117                 // For back compatibility, $wgSpamRegex may be a single string or an array of regexes.
1118                 $regexes = (array)$wgSpamRegex;
1119                 return self::matchSpamRegexInternal( $text, $regexes );
1120         }
1121
1122         /**
1123          * Check given input text against $wgSpamRegex, and return the text of the first match.
1124          * @return mixed -- matching string or false
1125          */
1126         public static function matchSummarySpamRegex( $text ) {
1127                 global $wgSummarySpamRegex;
1128                 $regexes = (array)$wgSummarySpamRegex;
1129                 return self::matchSpamRegexInternal( $text, $regexes );
1130         }
1131
1132         protected static function matchSpamRegexInternal( $text, $regexes ) {
1133                 foreach( $regexes as $regex ) {
1134                         $matches = array();
1135                         if( preg_match( $regex, $text, $matches ) ) {
1136                                 return $matches[0];
1137                         }
1138                 }
1139                 return false;
1140         }
1141
1142         /**
1143          * Initialise form fields in the object
1144          * Called on the first invocation, e.g. when a user clicks an edit link
1145          * @returns bool -- if the requested section is valid
1146          */
1147         function initialiseForm() {
1148                 global $wgUser;
1149                 $this->edittime = $this->mArticle->getTimestamp();
1150                 $this->textbox1 = $this->getContent( false );
1151                 // activate checkboxes if user wants them to be always active
1152                 # Sort out the "watch" checkbox
1153                 if ( $wgUser->getOption( 'watchdefault' ) ) {
1154                         # Watch all edits
1155                         $this->watchthis = true;
1156                 } elseif ( $wgUser->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
1157                         # Watch creations
1158                         $this->watchthis = true;
1159                 } elseif ( $this->mTitle->userIsWatching() ) {
1160                         # Already watched
1161                         $this->watchthis = true;
1162                 }
1163                 if ( $wgUser->getOption( 'minordefault' ) ) $this->minoredit = true;
1164                 if ( $this->textbox1 === false ) return false;
1165                 wfProxyCheck();
1166                 return true;
1167         }
1168
1169         function setHeaders() {
1170                 global $wgOut, $wgTitle;
1171                 $wgOut->setRobotPolicy( 'noindex,nofollow' );
1172                 if ( $this->formtype == 'preview' ) {
1173                         $wgOut->setPageTitleActionText( wfMsg( 'preview' ) );
1174                 }
1175                 if ( $this->isConflict ) {
1176                         $wgOut->setPageTitle( wfMsg( 'editconflict', $wgTitle->getPrefixedText() ) );
1177                 } elseif ( $this->section != '' ) {
1178                         $msg = $this->section == 'new' ? 'editingcomment' : 'editingsection';
1179                         $wgOut->setPageTitle( wfMsg( $msg, $wgTitle->getPrefixedText() ) );
1180                 } else {
1181                         # Use the title defined by DISPLAYTITLE magic word when present
1182                         if ( isset( $this->mParserOutput )
1183                          && ( $dt = $this->mParserOutput->getDisplayTitle() ) !== false ) {
1184                                 $title = $dt;
1185                         } else {
1186                                 $title = $wgTitle->getPrefixedText();
1187                         }
1188                         $wgOut->setPageTitle( wfMsg( 'editing', $title ) );
1189                 }
1190         }
1191
1192         /**
1193          * Send the edit form and related headers to $wgOut
1194          * @param $formCallback Optional callable that takes an OutputPage
1195          *                      parameter; will be called during form output
1196          *                      near the top, for captchas and the like.
1197          */
1198         function showEditForm( $formCallback = null ) {
1199                 global $wgOut, $wgUser, $wgTitle;
1200
1201                 # If $wgTitle is null, that means we're in API mode.
1202                 # Some hook probably called this function  without checking
1203                 # for is_null($wgTitle) first. Bail out right here so we don't
1204                 # do lots of work just to discard it right after.
1205                 if ( is_null( $wgTitle ) )
1206                         return;
1207
1208                 wfProfileIn( __METHOD__ );
1209
1210                 $sk = $wgUser->getSkin();
1211
1212                 #need to parse the preview early so that we know which templates are used,
1213                 #otherwise users with "show preview after edit box" will get a blank list
1214                 #we parse this near the beginning so that setHeaders can do the title
1215                 #setting work instead of leaving it in getPreviewText
1216                 $previewOutput = '';
1217                 if ( $this->formtype == 'preview' ) {
1218                         $previewOutput = $this->getPreviewText();
1219                 }
1220
1221                 wfRunHooks( 'EditPage::showEditForm:initial', array( &$this ) );
1222
1223                 $this->setHeaders();
1224
1225                 # Enabled article-related sidebar, toplinks, etc.
1226                 $wgOut->setArticleRelated( true );
1227
1228                 if ( $this->showHeader() === false ) {
1229                         wfProfileOut( __METHOD__ );
1230                         return;
1231                 }
1232
1233                 $action = htmlspecialchars( $this->getActionURL( $wgTitle ) );
1234
1235                 if ( $wgUser->getOption( 'showtoolbar' ) and !$this->isCssJsSubpage ) {
1236                         # prepare toolbar for edit buttons
1237                         $toolbar = EditPage::getEditToolbar();
1238                 } else {
1239                         $toolbar = '';
1240                 }
1241
1242
1243                 $wgOut->addHTML( $this->editFormPageTop );
1244
1245                 if ( $wgUser->getOption( 'previewontop' ) ) {
1246                         $this->displayPreviewArea( $previewOutput, true );
1247                 }
1248
1249                 $wgOut->addHTML( $this->editFormTextTop );
1250
1251                 $templates = $this->getTemplates();
1252                 $formattedtemplates = $sk->formatTemplates( $templates, $this->preview, $this->section != '');
1253
1254                 $hiddencats = $this->mArticle->getHiddenCategories();
1255                 $formattedhiddencats = $sk->formatHiddenCategories( $hiddencats );
1256
1257                 if ( $this->wasDeletedSinceLastEdit() && 'save' != $this->formtype ) {
1258                         $wgOut->wrapWikiMsg(
1259                                 "<div class='error mw-deleted-while-editing'>\n$1\n</div>",
1260                                 'deletedwhileediting' );
1261                 } elseif ( $this->wasDeletedSinceLastEdit() ) {
1262                         // Hide the toolbar and edit area, user can click preview to get it back
1263                         // Add an confirmation checkbox and explanation.
1264                         $toolbar = '';
1265                         // @todo move this to a cleaner conditional instead of blanking a variable
1266                 }
1267                 $wgOut->addHTML( <<<HTML
1268 {$toolbar}
1269 <form id="editform" name="editform" method="post" action="$action" enctype="multipart/form-data">
1270 HTML
1271 );
1272
1273                 if ( is_callable( $formCallback ) ) {
1274                         call_user_func_array( $formCallback, array( &$wgOut ) );
1275                 }
1276
1277                 wfRunHooks( 'EditPage::showEditForm:fields', array( &$this, &$wgOut ) );
1278
1279                 // Put these up at the top to ensure they aren't lost on early form submission
1280                 $this->showFormBeforeText();
1281
1282                 if ( $this->wasDeletedSinceLastEdit() && 'save' == $this->formtype ) {
1283                         $wgOut->addHTML(
1284                                 '<div class="mw-confirm-recreate">' .
1285                                 $wgOut->parse( wfMsg( 'confirmrecreate',  $this->lastDelete->user_name , $this->lastDelete->log_comment ) ) .
1286                                 Xml::checkLabel( wfMsg( 'recreate' ), 'wpRecreate', 'wpRecreate', false,
1287                                         array( 'title' => $sk->titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' )
1288                                 ) .
1289                                 '</div>'
1290                         );
1291                 }
1292
1293                 # If a blank edit summary was previously provided, and the appropriate
1294                 # user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the
1295                 # user being bounced back more than once in the event that a summary
1296                 # is not required.
1297                 #####
1298                 # For a bit more sophisticated detection of blank summaries, hash the
1299                 # automatic one and pass that in the hidden field wpAutoSummary.
1300                 if ( $this->missingSummary ||
1301                         ( $this->section == 'new' && $this->nosummary ) )
1302                                 $wgOut->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
1303                 $autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary );
1304                 $wgOut->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
1305
1306                 $wgOut->addHTML( Html::hidden( 'oldid', $this->mArticle->getOldID() ) );
1307
1308                 if ( $this->section == 'new' ) {
1309                         $this->showSummaryInput( true, $this->summary );
1310                         $wgOut->addHTML( $this->getSummaryPreview( true, $this->summary ) );
1311                 }
1312
1313                 $wgOut->addHTML( $this->editFormTextBeforeContent );
1314
1315                 if ( $this->isConflict ) {
1316                         // In an edit conflict bypass the overrideable content form method
1317                         // and fallback to the raw wpTextbox1 since editconflicts can't be
1318                         // resolved between page source edits and custom ui edits using the
1319                         // custom edit ui.
1320                         $this->showTextbox1( null, $this->getContent() );
1321                 } else {
1322                         $this->showContentForm();
1323                 }
1324
1325                 $wgOut->addHTML( $this->editFormTextAfterContent );
1326
1327                 $wgOut->addWikiText( $this->getCopywarn() );
1328                 if ( isset($this->editFormTextAfterWarn) && $this->editFormTextAfterWarn !== '' )
1329                         $wgOut->addHTML( $this->editFormTextAfterWarn );
1330
1331                 $this->showStandardInputs();
1332
1333                 $this->showFormAfterText();
1334
1335                 $this->showTosSummary();
1336                 $this->showEditTools();
1337
1338                 $wgOut->addHTML( <<<HTML
1339 {$this->editFormTextAfterTools}
1340 <div class='templatesUsed'>
1341 {$formattedtemplates}
1342 </div>
1343 <div class='hiddencats'>
1344 {$formattedhiddencats}
1345 </div>
1346 HTML
1347 );
1348
1349                 if ( $this->isConflict )
1350                         $this->showConflict();
1351
1352                 $wgOut->addHTML( $this->editFormTextBottom );
1353                 $wgOut->addHTML( "</form>\n" );
1354                 if ( !$wgUser->getOption( 'previewontop' ) ) {
1355                         $this->displayPreviewArea( $previewOutput, false );
1356                 }
1357
1358                 wfProfileOut( __METHOD__ );
1359         }
1360
1361         protected function showHeader() {
1362                 global $wgOut, $wgUser, $wgTitle, $wgMaxArticleSize, $wgLang;
1363                 if ( $this->isConflict ) {
1364                         $wgOut->wrapWikiMsg( "<div class='mw-explainconflict'>\n$1\n</div>", 'explainconflict' );
1365                         $this->edittime = $this->mArticle->getTimestamp();
1366                 } else {
1367                         if ( $this->section != '' && !$this->isSectionEditSupported() ) {
1368                                 // We use $this->section to much before this and getVal('wgSection') directly in other places
1369                                 // at this point we can't reset $this->section to '' to fallback to non-section editing.
1370                                 // Someone is welcome to try refactoring though
1371                                 $wgOut->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
1372                                 return false;
1373                         }
1374
1375                         if ( $this->section != '' && $this->section != 'new' ) {
1376                                 $matches = array();
1377                                 if ( !$this->summary && !$this->preview && !$this->diff ) {
1378                                         preg_match( "/^(=+)(.+)\\1/mi", $this->textbox1, $matches );
1379                                         if ( !empty( $matches[2] ) ) {
1380                                                 global $wgParser;
1381                                                 $this->summary = "/* " .
1382                                                         $wgParser->stripSectionName(trim($matches[2])) .
1383                                                         " */ ";
1384                                         }
1385                                 }
1386                         }
1387
1388                         if ( $this->missingComment ) {
1389                                 $wgOut->wrapWikiMsg( "<div id='mw-missingcommenttext'>\n$1\n</div>", 'missingcommenttext' );
1390                         }
1391
1392                         if ( $this->missingSummary && $this->section != 'new' ) {
1393                                 $wgOut->wrapWikiMsg( "<div id='mw-missingsummary'>\n$1\n</div>", 'missingsummary' );
1394                         }
1395
1396                         if ( $this->missingSummary && $this->section == 'new' ) {
1397                                 $wgOut->wrapWikiMsg( "<div id='mw-missingcommentheader'>\n$1\n</div>", 'missingcommentheader' );
1398                         }
1399
1400                         if ( $this->hookError !== '' ) {
1401                                 $wgOut->addWikiText( $this->hookError );
1402                         }
1403
1404                         if ( !$this->checkUnicodeCompliantBrowser() ) {
1405                                 $wgOut->addWikiMsg( 'nonunicodebrowser' );
1406                         }
1407
1408                         if ( isset( $this->mArticle ) && isset( $this->mArticle->mRevision ) ) {
1409                         // Let sysop know that this will make private content public if saved
1410
1411                                 if ( !$this->mArticle->mRevision->userCan( Revision::DELETED_TEXT ) ) {
1412                                         $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", 'rev-deleted-text-permission' );
1413                                 } else if ( $this->mArticle->mRevision->isDeleted( Revision::DELETED_TEXT ) ) {
1414                                         $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", 'rev-deleted-text-view' );
1415                                 }
1416
1417                                 if ( !$this->mArticle->mRevision->isCurrent() ) {
1418                                         $this->mArticle->setOldSubtitle( $this->mArticle->mRevision->getId() );
1419                                         $wgOut->addWikiMsg( 'editingold' );
1420                                 }
1421                         }
1422                 }
1423
1424                 if ( wfReadOnly() ) {
1425                         $wgOut->wrapWikiMsg( "<div id=\"mw-read-only-warning\">\n$1\n</div>", array( 'readonlywarning', wfReadOnlyReason() ) );
1426                 } elseif ( $wgUser->isAnon() ) {
1427                         if ( $this->formtype != 'preview' ) {
1428                                 $wgOut->wrapWikiMsg( "<div id=\"mw-anon-edit-warning\">\n$1</div>", 'anoneditwarning' );
1429                         } else {
1430                                 $wgOut->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\">\n$1</div>", 'anonpreviewwarning' );
1431                         }
1432                 } else {
1433                         if ( $this->isCssJsSubpage ) {
1434                                 # Check the skin exists
1435                                 if ( $this->isWrongCaseCssJsPage ) {
1436                                         $wgOut->wrapWikiMsg( "<div class='error' id='mw-userinvalidcssjstitle'>\n$1\n</div>", array( 'userinvalidcssjstitle', $wgTitle->getSkinFromCssJsSubpage() ) );
1437                                 }
1438                                 if ( $this->formtype !== 'preview' ) {
1439                                         if ( $this->isCssSubpage )
1440                                                 $wgOut->wrapWikiMsg( "<div id='mw-usercssyoucanpreview'>\n$1\n</div>", array( 'usercssyoucanpreview' ) );
1441                                         if ( $this->isJsSubpage )
1442                                                 $wgOut->wrapWikiMsg( "<div id='mw-userjsyoucanpreview'>\n$1\n</div>", array( 'userjsyoucanpreview' ) );
1443                                 }
1444                         }
1445                 }
1446
1447                 if ( $this->mTitle->getNamespace() != NS_MEDIAWIKI && $this->mTitle->isProtected( 'edit' ) ) {
1448                         # Is the title semi-protected?
1449                         if ( $this->mTitle->isSemiProtected() ) {
1450                                 $noticeMsg = 'semiprotectedpagewarning';
1451                         } else {
1452                                 # Then it must be protected based on static groups (regular)
1453                                 $noticeMsg = 'protectedpagewarning';
1454                         }
1455                         LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle->getPrefixedText(), '',
1456                                 array( 'lim' => 1, 'msgKey' => array( $noticeMsg ) ) );
1457                 }
1458                 if ( $this->mTitle->isCascadeProtected() ) {
1459                         # Is this page under cascading protection from some source pages?
1460                         list($cascadeSources, /* $restrictions */) = $this->mTitle->getCascadeProtectionSources();
1461                         $notice = "<div class='mw-cascadeprotectedwarning'>\n$1\n";
1462                         $cascadeSourcesCount = count( $cascadeSources );
1463                         if ( $cascadeSourcesCount > 0 ) {
1464                                 # Explain, and list the titles responsible
1465                                 foreach( $cascadeSources as $page ) {
1466                                         $notice .= '* [[:' . $page->getPrefixedText() . "]]\n";
1467                                 }
1468                         }
1469                         $notice .= '</div>';
1470                         $wgOut->wrapWikiMsg( $notice, array( 'cascadeprotectedwarning', $cascadeSourcesCount ) );
1471                 }
1472                 if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) {
1473                         LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle->getPrefixedText(), '',
1474                                 array(  'lim' => 1,
1475                                         'showIfEmpty' => false,
1476                                         'msgKey' => array( 'titleprotectedwarning' ),
1477                                         'wrap' => "<div class=\"mw-titleprotectedwarning\">\n$1</div>" ) );
1478                 }
1479
1480                 if ( $this->kblength === false ) {
1481                         $this->kblength = (int)( strlen( $this->textbox1 ) / 1024 );
1482                 }
1483
1484                 if ( $this->tooBig || $this->kblength > $wgMaxArticleSize ) {
1485                         $wgOut->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>",
1486                                 array( 'longpageerror', $wgLang->formatNum( $this->kblength ), $wgLang->formatNum( $wgMaxArticleSize ) ) );
1487                 } else {
1488                         $msg = 'longpage-hint';
1489                         $text = wfMsg( $msg );
1490                         if( !wfEmptyMsg( $msg, $text ) && $text !== '-' ) {
1491                                 $wgOut->wrapWikiMsg( "<div id='mw-edit-longpage-hint'>\n$1\n</div>",
1492                                         array( 'longpage-hint', $wgLang->formatSize( strlen( $this->textbox1 ) ), strlen( $this->textbox1 ) )
1493                                 );
1494                         }
1495                 }
1496         }
1497
1498         /**
1499          * Standard summary input and label (wgSummary), abstracted so EditPage
1500          * subclasses may reorganize the form.
1501          * Note that you do not need to worry about the label's for=, it will be
1502          * inferred by the id given to the input. You can remove them both by
1503          * passing array( 'id' => false ) to $userInputAttrs.
1504          *
1505          * @param $summary The value of the summary input
1506          * @param $labelText The html to place inside the label
1507          * @param $inputAttrs An array of attrs to use on the input
1508          * @param $spanLabelAttrs An array of attrs to use on the span inside the label
1509          *
1510          * @return array An array in the format array( $label, $input )
1511          */
1512         function getSummaryInput($summary = "", $labelText = null, $inputAttrs = null, $spanLabelAttrs = null) {
1513                 global $wgUser;
1514                 //Note: the maxlength is overriden in JS to 250 and to make it use UTF-8 bytes, not characters.
1515                 $inputAttrs = ( is_array($inputAttrs) ? $inputAttrs : array() ) + array(
1516                         'id' => 'wpSummary',
1517                         'maxlength' => '200',
1518                         'tabindex' => '1',
1519                         'size' => 60,
1520                         'spellcheck' => 'true',
1521                 ) + $wgUser->getSkin()->tooltipAndAccessKeyAttribs( 'summary' );
1522
1523                 $spanLabelAttrs = ( is_array($spanLabelAttrs) ? $spanLabelAttrs : array() ) + array(
1524                         'class' => $this->missingSummary ? 'mw-summarymissed' : 'mw-summary',
1525                         'id' => "wpSummaryLabel"
1526                 );
1527
1528                 $label = null;
1529                 if ( $labelText ) {
1530                         $label = Xml::tags( 'label', $inputAttrs['id'] ? array( 'for' => $inputAttrs['id'] ) : null, $labelText );
1531                         $label = Xml::tags( 'span', $spanLabelAttrs, $label );
1532                 }
1533
1534                 $input = Html::input( 'wpSummary', $summary, 'text', $inputAttrs );
1535
1536                 return array( $label, $input );
1537         }
1538
1539         /**
1540          * @param $isSubjectPreview Boolean: true if this is the section subject/title
1541          *                          up top, or false if this is the comment summary
1542          *                          down below the textarea
1543          * @param $summary String: The text of the summary to display
1544          * @return String
1545          */
1546         protected function showSummaryInput( $isSubjectPreview, $summary = "" ) {
1547                 global $wgOut, $wgContLang;
1548                 # Add a class if 'missingsummary' is triggered to allow styling of the summary line
1549                 $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
1550                 if ( $isSubjectPreview ) {
1551                         if ( $this->nosummary )
1552                                 return;
1553                 } else {
1554                         if ( !$this->mShowSummaryField )
1555                                 return;
1556                 }
1557                 $summary = $wgContLang->recodeForEdit( $summary );
1558                 $labelText = wfMsgExt( $isSubjectPreview ? 'subject' : 'summary', 'parseinline' );
1559                 list($label, $input) = $this->getSummaryInput($summary, $labelText, array( 'class' => $summaryClass ), array());
1560                 $wgOut->addHTML("{$label} {$input}");
1561         }
1562
1563         /**
1564          * @param $isSubjectPreview Boolean: true if this is the section subject/title
1565          *                          up top, or false if this is the comment summary
1566          *                          down below the textarea
1567          * @param $summary String: the text of the summary to display
1568          * @return String
1569          */
1570         protected function getSummaryPreview( $isSubjectPreview, $summary = "" ) {
1571                 if ( !$summary || ( !$this->preview && !$this->diff ) )
1572                         return "";
1573
1574                 global $wgParser, $wgUser;
1575                 $sk = $wgUser->getSkin();
1576
1577                 if ( $isSubjectPreview )
1578                         $summary = wfMsgForContent( 'newsectionsummary', $wgParser->stripSectionName( $summary ) );
1579
1580                 $message = $isSubjectPreview ? 'subject-preview' : 'summary-preview';
1581
1582                 $summary = wfMsgExt( $message, 'parseinline' ) . $sk->commentBlock( $summary, $this->mTitle, $isSubjectPreview );
1583                 return Xml::tags( 'div', array( 'class' => 'mw-summary-preview' ), $summary );
1584         }
1585
1586         protected function showFormBeforeText() {
1587                 global $wgOut;
1588                 $section = htmlspecialchars( $this->section );
1589                 $wgOut->addHTML( <<<HTML
1590 <input type='hidden' value="{$section}" name="wpSection" />
1591 <input type='hidden' value="{$this->starttime}" name="wpStarttime" />
1592 <input type='hidden' value="{$this->edittime}" name="wpEdittime" />
1593 <input type='hidden' value="{$this->scrolltop}" name="wpScrolltop" id="wpScrolltop" />
1594
1595 HTML
1596                 );
1597                 if ( !$this->checkUnicodeCompliantBrowser() )
1598                         $wgOut->addHTML(Html::hidden( 'safemode', '1' ));
1599         }
1600
1601         protected function showFormAfterText() {
1602                 global $wgOut, $wgUser;
1603                 /**
1604                  * To make it harder for someone to slip a user a page
1605                  * which submits an edit form to the wiki without their
1606                  * knowledge, a random token is associated with the login
1607                  * session. If it's not passed back with the submission,
1608                  * we won't save the page, or render user JavaScript and
1609                  * CSS previews.
1610                  *
1611                  * For anon editors, who may not have a session, we just
1612                  * include the constant suffix to prevent editing from
1613                  * broken text-mangling proxies.
1614                  */
1615                 $wgOut->addHTML( "\n" . Html::hidden( "wpEditToken", $wgUser->editToken() ) . "\n" );
1616         }
1617
1618         /**
1619          * Subpage overridable method for printing the form for page content editing
1620          * By default this simply outputs wpTextbox1
1621          * Subclasses can override this to provide a custom UI for editing;
1622          * be it a form, or simply wpTextbox1 with a modified content that will be
1623          * reverse modified when extracted from the post data.
1624          * Note that this is basically the inverse for importContentFormData
1625          */
1626         protected function showContentForm() {
1627                 $this->showTextbox1();
1628         }
1629
1630         /**
1631          * Method to output wpTextbox1
1632          * The $textoverride method can be used by subclasses overriding showContentForm
1633          * to pass back to this method.
1634          *
1635          * @param $customAttribs An array of html attributes to use in the textarea
1636          * @param $textoverride String: optional text to override $this->textarea1 with
1637          */
1638         protected function showTextbox1($customAttribs = null, $textoverride = null) {
1639                 $classes = array(); // Textarea CSS
1640                 if ( $this->mTitle->getNamespace() != NS_MEDIAWIKI && $this->mTitle->isProtected( 'edit' ) ) {
1641                         # Is the title semi-protected?
1642                         if ( $this->mTitle->isSemiProtected() ) {
1643                                 $classes[] = 'mw-textarea-sprotected';
1644                         } else {
1645                                 # Then it must be protected based on static groups (regular)
1646                                 $classes[] = 'mw-textarea-protected';
1647                         }
1648                 }
1649                 $attribs = array( 'tabindex' => 1 );
1650                 if ( is_array($customAttribs) )
1651                         $attribs += $customAttribs;
1652
1653                 if ( $this->wasDeletedSinceLastEdit() )
1654                         $attribs['type'] = 'hidden';
1655                 if ( !empty( $classes ) ) {
1656                         if ( isset($attribs['class']) )
1657                                 $classes[] = $attribs['class'];
1658                         $attribs['class'] = implode( ' ', $classes );
1659                 }
1660
1661                 $this->showTextbox( isset($textoverride) ? $textoverride : $this->textbox1, 'wpTextbox1', $attribs );
1662         }
1663
1664         protected function showTextbox2() {
1665                 $this->showTextbox( $this->textbox2, 'wpTextbox2', array( 'tabindex' => 6 ) );
1666         }
1667
1668         protected function showTextbox( $content, $name, $customAttribs = array() ) {
1669                 global $wgOut, $wgUser;
1670
1671                 $wikitext = $this->safeUnicodeOutput( $content );
1672                 if ( $wikitext !== '' ) {
1673                         // Ensure there's a newline at the end, otherwise adding lines
1674                         // is awkward.
1675                         // But don't add a newline if the ext is empty, or Firefox in XHTML
1676                         // mode will show an extra newline. A bit annoying.
1677                         $wikitext .= "\n";
1678                 }
1679
1680                 $attribs = $customAttribs + array(
1681                         'accesskey' => ',',
1682                         'id'   => $name,
1683                         'cols' => $wgUser->getIntOption( 'cols' ),
1684                         'rows' => $wgUser->getIntOption( 'rows' ),
1685                         'style' => '' // avoid php notices when appending preferences (appending allows customAttribs['style'] to still work
1686                 );
1687
1688                 $wgOut->addHTML( Html::textarea( $name, $wikitext, $attribs ) );
1689         }
1690
1691         protected function displayPreviewArea( $previewOutput, $isOnTop = false ) {
1692                 global $wgOut;
1693                 $classes = array();
1694                 if ( $isOnTop )
1695                         $classes[] = 'ontop';
1696
1697                 $attribs = array( 'id' => 'wikiPreview', 'class' => implode( ' ', $classes ) );
1698
1699                 if ( $this->formtype != 'preview' )
1700                         $attribs['style'] = 'display: none;';
1701
1702                 $wgOut->addHTML( Xml::openElement( 'div', $attribs ) );
1703
1704                 if ( $this->formtype == 'preview' ) {
1705                         $this->showPreview( $previewOutput );
1706                 }
1707
1708                 $wgOut->addHTML( '</div>' );
1709
1710                 if ( $this->formtype == 'diff') {
1711                         $this->showDiff();
1712                 }
1713         }
1714
1715         /**
1716          * Append preview output to $wgOut.
1717          * Includes category rendering if this is a category page.
1718          *
1719          * @param $text String: the HTML to be output for the preview.
1720          */
1721         protected function showPreview( $text ) {
1722                 global $wgOut;
1723                 if ( $this->mTitle->getNamespace() == NS_CATEGORY) {
1724                         $this->mArticle->openShowCategory();
1725                 }
1726                 # This hook seems slightly odd here, but makes things more
1727                 # consistent for extensions.
1728                 wfRunHooks( 'OutputPageBeforeHTML',array( &$wgOut, &$text ) );
1729                 $wgOut->addHTML( $text );
1730                 if ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
1731                         $this->mArticle->closeShowCategory();
1732                 }
1733         }
1734
1735         /**
1736          * Give a chance for site and per-namespace customizations of
1737          * terms of service summary link that might exist separately
1738          * from the copyright notice.
1739          *
1740          * This will display between the save button and the edit tools,
1741          * so should remain short!
1742          */
1743         protected function showTosSummary() {
1744                 $msg = 'editpage-tos-summary';
1745                 wfRunHooks( 'EditPageTosSummary', array( $this->mTitle, &$msg ) );
1746                 $text = wfMsg( $msg );
1747                 if( !wfEmptyMsg( $msg, $text ) && $text !== '-' ) {
1748                         global $wgOut;
1749                         $wgOut->addHTML( '<div class="mw-tos-summary">' );
1750                         $wgOut->addWikiMsgArray( $msg, array() );
1751                         $wgOut->addHTML( '</div>' );
1752                 }
1753         }
1754
1755         protected function showEditTools() {
1756                 global $wgOut;
1757                 $wgOut->addHTML( '<div class="mw-editTools">' );
1758                 $wgOut->addWikiMsgArray( 'edittools', array(), array( 'content' ) );
1759                 $wgOut->addHTML( '</div>' );
1760         }
1761
1762         protected function getCopywarn() {
1763                 global $wgRightsText;
1764                 if ( $wgRightsText ) {
1765                         $copywarnMsg = array( 'copyrightwarning',
1766                                 '[[' . wfMsgForContent( 'copyrightpage' ) . ']]',
1767                                 $wgRightsText );
1768                 } else {
1769                         $copywarnMsg = array( 'copyrightwarning2',
1770                                 '[[' . wfMsgForContent( 'copyrightpage' ) . ']]' );
1771                 }
1772                 // Allow for site and per-namespace customization of contribution/copyright notice.
1773                 wfRunHooks( 'EditPageCopyrightWarning', array( $this->mTitle, &$copywarnMsg ) );
1774
1775                 return "<div id=\"editpage-copywarn\">\n" . call_user_func_array("wfMsgNoTrans", $copywarnMsg) . "\n</div>";
1776         }
1777
1778         protected function showStandardInputs( &$tabindex = 2 ) {
1779                 global $wgOut, $wgUser;
1780                 $wgOut->addHTML( "<div class='editOptions'>\n" );
1781
1782                 if ( $this->section != 'new' ) {
1783                         $this->showSummaryInput( false, $this->summary );
1784                         $wgOut->addHTML( $this->getSummaryPreview( false, $this->summary ) );
1785                 }
1786
1787                 $checkboxes = $this->getCheckboxes( $tabindex, $wgUser->getSkin(),
1788                         array( 'minor' => $this->minoredit, 'watch' => $this->watchthis ) );
1789                 $wgOut->addHTML( "<div class='editCheckboxes'>" . implode( $checkboxes, "\n" ) . "</div>\n" );
1790                 $wgOut->addHTML( "<div class='editButtons'>\n" );
1791                 $wgOut->addHTML( implode( $this->getEditButtons( $tabindex ), "\n" ) . "\n" );
1792
1793                 $cancel = $this->getCancelLink();
1794                 $separator = wfMsgExt( 'pipe-separator' , 'escapenoentities' );
1795                 $edithelpurl = Skin::makeInternalOrExternalUrl( wfMsgForContent( 'edithelppage' ) );
1796                 $edithelp = '<a target="helpwindow" href="'.$edithelpurl.'">'.
1797                         htmlspecialchars( wfMsg( 'edithelp' ) ).'</a> '.
1798                         htmlspecialchars( wfMsg( 'newwindow' ) );
1799                 $wgOut->addHTML( "      <span class='editHelp'>{$cancel}{$separator}{$edithelp}</span>\n" );
1800                 $wgOut->addHTML( "</div><!-- editButtons -->\n</div><!-- editOptions -->\n" );
1801         }
1802
1803         /*
1804          * Show an edit conflict. textbox1 is already shown in showEditForm().
1805          * If you want to use another entry point to this function, be careful.
1806          */
1807         protected function showConflict() {
1808                 global $wgOut;
1809                 $this->textbox2 = $this->textbox1;
1810                 $this->textbox1 = $this->getContent();
1811                 if ( wfRunHooks( 'EditPageBeforeConflictDiff', array( &$this, &$wgOut ) ) ) {
1812                         $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
1813
1814                         $de = new DifferenceEngine( $this->mTitle );
1815                         $de->setText( $this->textbox2, $this->textbox1 );
1816                         $de->showDiff( wfMsg( "yourtext" ), wfMsg( "storedversion" ) );
1817
1818                         $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
1819                         $this->showTextbox2();
1820                 }
1821         }
1822
1823         protected function getLastDelete() {
1824                 $dbr = wfGetDB( DB_SLAVE );
1825                 $data = $dbr->selectRow(
1826                         array( 'logging', 'user' ),
1827                         array( 'log_type',
1828                                'log_action',
1829                                'log_timestamp',
1830                                'log_user',
1831                                'log_namespace',
1832                                'log_title',
1833                                'log_comment',
1834                                'log_params',
1835                                'log_deleted',
1836                                'user_name' ),
1837                         array( 'log_namespace' => $this->mTitle->getNamespace(),
1838                                'log_title' => $this->mTitle->getDBkey(),
1839                                'log_type' => 'delete',
1840                                'log_action' => 'delete',
1841                                'user_id=log_user' ),
1842                         __METHOD__,
1843                         array( 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' )
1844                 );
1845                 // Quick paranoid permission checks...
1846                 if( is_object( $data ) ) {
1847                         if( $data->log_deleted & LogPage::DELETED_USER )
1848                                 $data->user_name = wfMsgHtml( 'rev-deleted-user' );
1849                         if( $data->log_deleted & LogPage::DELETED_COMMENT )
1850                                 $data->log_comment = wfMsgHtml( 'rev-deleted-comment' );
1851                 }
1852                 return $data;
1853         }
1854
1855         /**
1856          * Get the rendered text for previewing.
1857          * @return string
1858          */
1859         function getPreviewText() {
1860                 global $wgOut, $wgUser, $wgParser, $wgMessageCache;
1861
1862                 wfProfileIn( __METHOD__ );
1863
1864                 if ( $this->mTriedSave && !$this->mTokenOk ) {
1865                         if ( $this->mTokenOkExceptSuffix ) {
1866                                 $note = wfMsg( 'token_suffix_mismatch' );
1867                         } else {
1868                                 $note = wfMsg( 'session_fail_preview' );
1869                         }
1870                 } else {
1871                         $note = wfMsg( 'previewnote' );
1872                 }
1873
1874                 $parserOptions = ParserOptions::newFromUser( $wgUser );
1875                 $parserOptions->setEditSection( false );
1876                 $parserOptions->setIsPreview( true );
1877                 $parserOptions->setIsSectionPreview( !is_null($this->section) && $this->section !== '' );
1878
1879                 global $wgRawHtml;
1880                 if ( $wgRawHtml && !$this->mTokenOk ) {
1881                         // Could be an offsite preview attempt. This is very unsafe if
1882                         // HTML is enabled, as it could be an attack.
1883                         $parsedNote = $wgOut->parse( "<div class='previewnote'>" .
1884                                 wfMsg( 'session_fail_preview_html' ) . "</div>" );
1885                         wfProfileOut( __METHOD__ );
1886                         return $parsedNote;
1887                 }
1888
1889                 # don't parse user css/js, show message about preview
1890                 # XXX: stupid php bug won't let us use $wgTitle->isCssJsSubpage() here -- This note has been there since r3530. Sure the bug was fixed time ago?
1891
1892                 if ( $this->isCssJsSubpage || $this->mTitle->isCssOrJsPage() ) {
1893                         $level = 'user';
1894                         if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
1895                                 $level = 'site';
1896                         }
1897
1898                         # Used messages to make sure grep find them:
1899                         # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
1900                         if (preg_match( "/\\.css$/", $this->mTitle->getText() ) ) {
1901                                 $previewtext = "<div id='mw-{$level}csspreview'>\n" . wfMsg( "{$level}csspreview" ) . "\n</div>";
1902                                 $class = "mw-code mw-css";
1903                         } elseif (preg_match( "/\\.js$/", $this->mTitle->getText() ) ) {
1904                                 $previewtext = "<div id='mw-{$level}jspreview'>\n" . wfMsg( "{$level}jspreview" ) . "\n</div>";
1905                                 $class = "mw-code mw-js";
1906                         } else {
1907                                 throw new MWException( 'A CSS/JS (sub)page but which is not css nor js!' );
1908                         }
1909
1910                         $parserOptions->setTidy( true );
1911                         $parserOutput = $wgParser->parse( $previewtext, $this->mTitle, $parserOptions );
1912                         $previewHTML = $parserOutput->mText;
1913                         $previewHTML .= "<pre class=\"$class\" dir=\"ltr\">\n" . htmlspecialchars( $this->textbox1 ) . "\n</pre>\n";
1914                 } else {
1915                         $rt = Title::newFromRedirectArray( $this->textbox1 );
1916                         if ( $rt ) {
1917                                 $previewHTML = $this->mArticle->viewRedirect( $rt, false );
1918                         } else {
1919                                 $toparse = $this->textbox1;
1920
1921                                 # If we're adding a comment, we need to show the
1922                                 # summary as the headline
1923                                 if ( $this->section == "new" && $this->summary != "" ) {
1924                                         $toparse = "== {$this->summary} ==\n\n" . $toparse;
1925                                 }
1926
1927                                 wfRunHooks( 'EditPageGetPreviewText', array( $this, &$toparse ) );
1928
1929                                 // Parse mediawiki messages with correct target language
1930                                 if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
1931                                         list( /* $unused */, $lang ) = $wgMessageCache->figureMessage( $this->mTitle->getText() );
1932                                         $obj = wfGetLangObj( $lang );
1933                                         $parserOptions->setTargetLanguage( $obj );
1934                                 }
1935
1936                                 $parserOptions->setTidy( true );
1937                                 $parserOptions->enableLimitReport();
1938                                 $parserOutput = $wgParser->parse( $this->mArticle->preSaveTransform( $toparse ),
1939                                         $this->mTitle, $parserOptions );
1940
1941                                 $previewHTML = $parserOutput->getText();
1942                                 $this->mParserOutput = $parserOutput;
1943                                 $wgOut->addParserOutputNoText( $parserOutput );
1944
1945                                 if ( count( $parserOutput->getWarnings() ) ) {
1946                                         $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
1947                                 }
1948                         }
1949                 }
1950
1951                 if( $this->isConflict ) {
1952                         $conflict = '<h2 id="mw-previewconflict">' . htmlspecialchars( wfMsg( 'previewconflict' ) ) . "</h2>\n";
1953                 } else {
1954                         $conflict = '<hr />';
1955                 }
1956
1957                 $previewhead = "<div class='previewnote'>\n" .
1958                         '<h2 id="mw-previewheader">' . htmlspecialchars( wfMsg( 'preview' ) ) . "</h2>" .
1959                         $wgOut->parse( $note ) . $conflict . "</div>\n";
1960
1961                 wfProfileOut( __METHOD__ );
1962                 return $previewhead . $previewHTML . $this->previewTextAfterContent;
1963         }
1964
1965         function getTemplates() {
1966                 if ( $this->preview || $this->section != '' ) {
1967                         $templates = array();
1968                         if ( !isset( $this->mParserOutput ) ) return $templates;
1969                         foreach( $this->mParserOutput->getTemplates() as $ns => $template) {
1970                                 foreach( array_keys( $template ) as $dbk ) {
1971                                         $templates[] = Title::makeTitle($ns, $dbk);
1972                                 }
1973                         }
1974                         return $templates;
1975                 } else {
1976                         return $this->mArticle->getUsedTemplates();
1977                 }
1978         }
1979
1980         /**
1981          * Call the stock "user is blocked" page
1982          */
1983         function blockedPage() {
1984                 global $wgOut;
1985                 $wgOut->blockedPage( false ); # Standard block notice on the top, don't 'return'
1986
1987                 # If the user made changes, preserve them when showing the markup
1988                 # (This happens when a user is blocked during edit, for instance)
1989                 $first = $this->firsttime || ( !$this->save && $this->textbox1 == '' );
1990                 if ( $first ) {
1991                         $source = $this->mTitle->exists() ? $this->getContent() : false;
1992                 } else {
1993                         $source = $this->textbox1;
1994                 }
1995
1996                 # Spit out the source or the user's modified version
1997                 if ( $source !== false ) {
1998                         $wgOut->addHTML( '<hr />' );
1999                         $wgOut->addWikiMsg( $first ? 'blockedoriginalsource' : 'blockededitsource', $this->mTitle->getPrefixedText() );
2000                         $this->showTextbox1( array( 'readonly' ), $source );
2001                 }
2002         }
2003
2004         /**
2005          * Produce the stock "please login to edit pages" page
2006          */
2007         function userNotLoggedInPage() {
2008                 global $wgUser, $wgOut, $wgTitle;
2009                 $skin = $wgUser->getSkin();
2010
2011                 $loginTitle = SpecialPage::getTitleFor( 'Userlogin' );
2012                 $loginLink = $skin->link(
2013                         $loginTitle,
2014                         wfMsgHtml( 'loginreqlink' ),
2015                         array(),
2016                         array( 'returnto' => $wgTitle->getPrefixedText() ),
2017                         array( 'known', 'noclasses' )
2018                 );
2019
2020                 $wgOut->setPageTitle( wfMsg( 'whitelistedittitle' ) );
2021                 $wgOut->setRobotPolicy( 'noindex,nofollow' );
2022                 $wgOut->setArticleRelated( false );
2023
2024                 $wgOut->addHTML( wfMsgWikiHtml( 'whitelistedittext', $loginLink ) );
2025                 $wgOut->returnToMain( false, $wgTitle );
2026         }
2027
2028         /**
2029          * Creates a basic error page which informs the user that
2030          * they have attempted to edit a nonexistent section.
2031          */
2032         function noSuchSectionPage() {
2033                 global $wgOut;
2034
2035                 $wgOut->setPageTitle( wfMsg( 'nosuchsectiontitle' ) );
2036                 $wgOut->setRobotPolicy( 'noindex,nofollow' );
2037                 $wgOut->setArticleRelated( false );
2038
2039                 $res = wfMsgExt( 'nosuchsectiontext', 'parse', $this->section );
2040                 wfRunHooks( 'EditPageNoSuchSection', array( &$this, &$res ) );
2041                 $wgOut->addHTML( $res );
2042
2043                 $wgOut->returnToMain( false, $this->mTitle );
2044         }
2045
2046         /**
2047          * Produce the stock "your edit contains spam" page
2048          *
2049          * @param $match Text which triggered one or more filters
2050          * @deprecated Use method spamPageWithContent() instead
2051          */
2052         static function spamPage( $match = false ) {
2053                 global $wgOut, $wgTitle;
2054
2055                 $wgOut->setPageTitle( wfMsg( 'spamprotectiontitle' ) );
2056                 $wgOut->setRobotPolicy( 'noindex,nofollow' );
2057                 $wgOut->setArticleRelated( false );
2058
2059                 $wgOut->addHTML( '<div id="spamprotected">' );
2060                 $wgOut->addWikiMsg( 'spamprotectiontext' );
2061                 if ( $match ) {
2062                         $wgOut->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) );
2063                 }
2064                 $wgOut->addHTML( '</div>' );
2065
2066                 $wgOut->returnToMain( false, $wgTitle );
2067         }
2068
2069         /**
2070          * Show "your edit contains spam" page with your diff and text
2071          *
2072          * @param $match Text which triggered one or more filters
2073          */
2074         public function spamPageWithContent( $match = false ) {
2075                 global $wgOut, $wgTitle;
2076                 $this->textbox2 = $this->textbox1;
2077
2078                 $wgOut->setPageTitle( wfMsg( 'spamprotectiontitle' ) );
2079                 $wgOut->setRobotPolicy( 'noindex,nofollow' );
2080                 $wgOut->setArticleRelated( false );
2081
2082                 $wgOut->addHTML( '<div id="spamprotected">' );
2083                 $wgOut->addWikiMsg( 'spamprotectiontext' );
2084                 if ( $match ) {
2085                         $wgOut->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) );
2086                 }
2087                 $wgOut->addHTML( '</div>' );
2088
2089                 $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
2090                 $de = new DifferenceEngine( $this->mTitle );
2091                 $de->setText( $this->getContent(), $this->textbox2 );
2092                 $de->showDiff( wfMsg( "storedversion" ), wfMsg( "yourtext" ) );
2093
2094                 $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
2095                 $this->showTextbox2();
2096
2097                 $wgOut->addReturnTo( $wgTitle, array( 'action' => 'edit' ) );
2098         }
2099
2100
2101         /**
2102          * @private
2103          * @todo document
2104          */
2105         function mergeChangesInto( &$editText ){
2106                 wfProfileIn( __METHOD__ );
2107
2108                 $db = wfGetDB( DB_MASTER );
2109
2110                 // This is the revision the editor started from
2111                 $baseRevision = $this->getBaseRevision();
2112                 if ( is_null( $baseRevision ) ) {
2113                         wfProfileOut( __METHOD__ );
2114                         return false;
2115                 }
2116                 $baseText = $baseRevision->getText();
2117
2118                 // The current state, we want to merge updates into it
2119                 $currentRevision = Revision::loadFromTitle( $db, $this->mTitle );
2120                 if ( is_null( $currentRevision ) ) {
2121                         wfProfileOut( __METHOD__ );
2122                         return false;
2123                 }
2124                 $currentText = $currentRevision->getText();
2125
2126                 $result = '';
2127                 if ( wfMerge( $baseText, $editText, $currentText, $result ) ) {
2128                         $editText = $result;
2129                         wfProfileOut( __METHOD__ );
2130                         return true;
2131                 } else {
2132                         wfProfileOut( __METHOD__ );
2133                         return false;
2134                 }
2135         }
2136
2137         /**
2138          * Check if the browser is on a blacklist of user-agents known to
2139          * mangle UTF-8 data on form submission. Returns true if Unicode
2140          * should make it through, false if it's known to be a problem.
2141          * @return bool
2142          * @private
2143          */
2144         function checkUnicodeCompliantBrowser() {
2145                 global $wgBrowserBlackList;
2146                 if ( empty( $_SERVER["HTTP_USER_AGENT"] ) ) {
2147                         // No User-Agent header sent? Trust it by default...
2148                         return true;
2149                 }
2150                 $currentbrowser = $_SERVER["HTTP_USER_AGENT"];
2151                 foreach ( $wgBrowserBlackList as $browser ) {
2152                         if ( preg_match($browser, $currentbrowser) ) {
2153                                 return false;
2154                         }
2155                 }
2156                 return true;
2157         }
2158
2159         /**
2160          * @deprecated use $wgParser->stripSectionName()
2161          */
2162         function pseudoParseSectionAnchor( $text ) {
2163                 global $wgParser;
2164                 return $wgParser->stripSectionName( $text );
2165         }
2166
2167         /**
2168          * Format an anchor fragment as it would appear for a given section name
2169          * @param $text String
2170          * @return String
2171          * @private
2172          */
2173         function sectionAnchor( $text ) {
2174                 global $wgParser;
2175                 return $wgParser->guessSectionNameFromWikiText( $text );
2176         }
2177
2178         /**
2179          * Shows a bulletin board style toolbar for common editing functions.
2180          * It can be disabled in the user preferences.
2181          * The necessary JavaScript code can be found in skins/common/edit.js.
2182          *
2183          * @return string
2184          */
2185         static function getEditToolbar() {
2186                 global $wgStylePath, $wgContLang, $wgLang, $wgOut;
2187                 global $wgUseTeX, $wgEnableUploads, $wgForeignFileRepos;
2188
2189                 $imagesAvailable = $wgEnableUploads || count( $wgForeignFileRepos );
2190
2191                 /**
2192
2193                  * toolarray an array of arrays which each include the filename of
2194                  * the button image (without path), the opening tag, the closing tag,
2195                  * and optionally a sample text that is inserted between the two when no
2196                  * selection is highlighted.
2197                  * The tip text is shown when the user moves the mouse over the button.
2198                  *
2199                  * Already here are accesskeys (key), which are not used yet until someone
2200                  * can figure out a way to make them work in IE. However, we should make
2201                  * sure these keys are not defined on the edit page.
2202                  */
2203                 $toolarray = array(
2204                         array(
2205                                 'image'  => $wgLang->getImageFile( 'button-bold' ),
2206                                 'id'     => 'mw-editbutton-bold',
2207                                 'open'   => '\'\'\'',
2208                                 'close'  => '\'\'\'',
2209                                 'sample' => wfMsg( 'bold_sample' ),
2210                                 'tip'    => wfMsg( 'bold_tip' ),
2211                                 'key'    => 'B'
2212                         ),
2213                         array(
2214                                 'image'  => $wgLang->getImageFile( 'button-italic' ),
2215                                 'id'     => 'mw-editbutton-italic',
2216                                 'open'   => '\'\'',
2217                                 'close'  => '\'\'',
2218                                 'sample' => wfMsg( 'italic_sample' ),
2219                                 'tip'    => wfMsg( 'italic_tip' ),
2220                                 'key'    => 'I'
2221                         ),
2222                         array(
2223                                 'image'  => $wgLang->getImageFile( 'button-link' ),
2224                                 'id'     => 'mw-editbutton-link',
2225                                 'open'   => '[[',
2226                                 'close'  => ']]',
2227                                 'sample' => wfMsg( 'link_sample' ),
2228                                 'tip'    => wfMsg( 'link_tip' ),
2229                                 'key'    => 'L'
2230                         ),
2231                         array(
2232                                 'image'  => $wgLang->getImageFile( 'button-extlink' ),
2233                                 'id'     => 'mw-editbutton-extlink',
2234                                 'open'   => '[',
2235                                 'close'  => ']',
2236                                 'sample' => wfMsg( 'extlink_sample' ),
2237                                 'tip'    => wfMsg( 'extlink_tip' ),
2238                                 'key'    => 'X'
2239                         ),
2240                         array(
2241                                 'image'  => $wgLang->getImageFile( 'button-headline' ),
2242                                 'id'     => 'mw-editbutton-headline',
2243                                 'open'   => "\n== ",
2244                                 'close'  => " ==\n",
2245                                 'sample' => wfMsg( 'headline_sample' ),
2246                                 'tip'    => wfMsg( 'headline_tip' ),
2247                                 'key'    => 'H'
2248                         ),
2249                         $imagesAvailable ? array(
2250                                 'image'  => $wgLang->getImageFile( 'button-image' ),
2251                                 'id'     => 'mw-editbutton-image',
2252                                 'open'   => '[[' . $wgContLang->getNsText( NS_FILE ) . ':',
2253                                 'close'  => ']]',
2254                                 'sample' => wfMsg( 'image_sample' ),
2255                                 'tip'    => wfMsg( 'image_tip' ),
2256                                 'key'    => 'D'
2257                         ) : false,
2258                         $imagesAvailable ? array(
2259                                 'image'  => $wgLang->getImageFile( 'button-media' ),
2260                                 'id'     => 'mw-editbutton-media',
2261                                 'open'   => '[[' . $wgContLang->getNsText( NS_MEDIA ) . ':',
2262                                 'close'  => ']]',
2263                                 'sample' => wfMsg( 'media_sample' ),
2264                                 'tip'    => wfMsg( 'media_tip' ),
2265                                 'key'    => 'M'
2266                         ) : false,
2267                         $wgUseTeX ?     array(
2268                                 'image'  => $wgLang->getImageFile( 'button-math' ),
2269                                 'id'     => 'mw-editbutton-math',
2270                                 'open'   => "<math>",
2271                                 'close'  => "</math>",
2272                                 'sample' => wfMsg( 'math_sample' ),
2273                                 'tip'    => wfMsg( 'math_tip' ),
2274                                 'key'    => 'C'
2275                         ) : false,
2276                         array(
2277                                 'image'  => $wgLang->getImageFile( 'button-nowiki' ),
2278                                 'id'     => 'mw-editbutton-nowiki',
2279                                 'open'   => "<nowiki>",
2280                                 'close'  => "</nowiki>",
2281                                 'sample' => wfMsg( 'nowiki_sample' ),
2282                                 'tip'    => wfMsg( 'nowiki_tip' ),
2283                                 'key'    => 'N'
2284                         ),
2285                         array(
2286                                 'image'  => $wgLang->getImageFile( 'button-sig' ),
2287                                 'id'     => 'mw-editbutton-signature',
2288                                 'open'   => '--~~~~',
2289                                 'close'  => '',
2290                                 'sample' => '',
2291                                 'tip'    => wfMsg( 'sig_tip' ),
2292                                 'key'    => 'Y'
2293                         ),
2294                         array(
2295                                 'image'  => $wgLang->getImageFile( 'button-hr' ),
2296                                 'id'     => 'mw-editbutton-hr',
2297                                 'open'   => "\n----\n",
2298                                 'close'  => '',
2299                                 'sample' => '',
2300                                 'tip'    => wfMsg( 'hr_tip' ),
2301                                 'key'    => 'R'
2302                         )
2303                 );
2304                 $toolbar = "<div id='toolbar'>\n";
2305
2306                 $script = '';
2307                 foreach ( $toolarray as $tool ) {
2308                         if ( !$tool ) {
2309                                 continue;
2310                         }
2311
2312                         $params = array(
2313                                 $image = $wgStylePath . '/common/images/' . $tool['image'],
2314                                 // Note that we use the tip both for the ALT tag and the TITLE tag of the image.
2315                                 // Older browsers show a "speedtip" type message only for ALT.
2316                                 // Ideally these should be different, realistically they
2317                                 // probably don't need to be.
2318                                 $tip = $tool['tip'],
2319                                 $open = $tool['open'],
2320                                 $close = $tool['close'],
2321                                 $sample = $tool['sample'],
2322                                 $cssId = $tool['id'],
2323                         );
2324
2325                         $paramList = implode( ',',
2326                                 array_map( array( 'Xml', 'encodeJsVar' ), $params ) );
2327                         $script .= "addButton($paramList);\n";
2328                 }
2329                 
2330                 $wgOut->addScript( Html::inlineScript(
2331                         "if ( window.mediaWiki ) { $script }"
2332                 ) );
2333                 
2334                 $toolbar .= "\n</div>";
2335
2336                 wfRunHooks( 'EditPageBeforeEditToolbar', array( &$toolbar ) );
2337
2338                 return $toolbar;
2339         }
2340
2341         /**
2342          * Returns an array of html code of the following checkboxes:
2343          * minor and watch
2344          *
2345          * @param $tabindex Current tabindex
2346          * @param $skin Skin object
2347          * @param $checked Array of checkbox => bool, where bool indicates the checked
2348          *                 status of the checkbox
2349          *
2350          * @return array
2351          */
2352         public function getCheckboxes( &$tabindex, $skin, $checked ) {
2353                 global $wgUser;
2354
2355                 $checkboxes = array();
2356
2357                 $checkboxes['minor'] = '';
2358                 $minorLabel = wfMsgExt( 'minoredit', array( 'parseinline' ) );
2359                 if ( $wgUser->isAllowed( 'minoredit' ) ) {
2360                         $attribs = array(
2361                                 'tabindex'  => ++$tabindex,
2362                                 'accesskey' => wfMsg( 'accesskey-minoredit' ),
2363                                 'id'        => 'wpMinoredit',
2364                         );
2365                         $checkboxes['minor'] =
2366                                 Xml::check( 'wpMinoredit', $checked['minor'], $attribs ) .
2367                                 "&#160;<label for='wpMinoredit' id='mw-editpage-minoredit'" .
2368                                 Xml::expandAttributes( array( 'title' => $skin->titleAttrib( 'minoredit', 'withaccess' ) ) ) .
2369                                 ">{$minorLabel}</label>";
2370                 }
2371
2372                 $watchLabel = wfMsgExt( 'watchthis', array( 'parseinline' ) );
2373                 $checkboxes['watch'] = '';
2374                 if ( $wgUser->isLoggedIn() ) {
2375                         $attribs = array(
2376                                 'tabindex'  => ++$tabindex,
2377                                 'accesskey' => wfMsg( 'accesskey-watch' ),
2378                                 'id'        => 'wpWatchthis',
2379                         );
2380                         $checkboxes['watch'] =
2381                                 Xml::check( 'wpWatchthis', $checked['watch'], $attribs ) .
2382                                 "&#160;<label for='wpWatchthis' id='mw-editpage-watch'" .
2383                                 Xml::expandAttributes( array( 'title' => $skin->titleAttrib( 'watch', 'withaccess' ) ) ) .
2384                                 ">{$watchLabel}</label>";
2385                 }
2386                 wfRunHooks( 'EditPageBeforeEditChecks', array( &$this, &$checkboxes, &$tabindex ) );
2387                 return $checkboxes;
2388         }
2389
2390         /**
2391          * Returns an array of html code of the following buttons:
2392          * save, diff, preview and live
2393          *
2394          * @param $tabindex Current tabindex
2395          *
2396          * @return array
2397          */
2398         public function getEditButtons( &$tabindex ) {
2399                 $buttons = array();
2400
2401                 $temp = array(
2402                         'id'        => 'wpSave',
2403                         'name'      => 'wpSave',
2404                         'type'      => 'submit',
2405                         'tabindex'  => ++$tabindex,
2406                         'value'     => wfMsg( 'savearticle' ),
2407                         'accesskey' => wfMsg( 'accesskey-save' ),
2408                         'title'     => wfMsg( 'tooltip-save' ).' ['.wfMsg( 'accesskey-save' ).']',
2409                 );
2410                 $buttons['save'] = Xml::element('input', $temp, '');
2411
2412                 ++$tabindex; // use the same for preview and live preview
2413                 $temp = array(
2414                         'id'        => 'wpPreview',
2415                         'name'      => 'wpPreview',
2416                         'type'      => 'submit',
2417                         'tabindex'  => $tabindex,
2418                         'value'     => wfMsg( 'showpreview' ),
2419                         'accesskey' => wfMsg( 'accesskey-preview' ),
2420                         'title'     => wfMsg( 'tooltip-preview' ) . ' [' . wfMsg( 'accesskey-preview' ) . ']',
2421                 );
2422                 $buttons['preview'] = Xml::element( 'input', $temp, '' );
2423                 $buttons['live'] = '';
2424
2425                 $temp = array(
2426                         'id'        => 'wpDiff',
2427                         'name'      => 'wpDiff',
2428                         'type'      => 'submit',
2429                         'tabindex'  => ++$tabindex,
2430                         'value'     => wfMsg( 'showdiff' ),
2431                         'accesskey' => wfMsg( 'accesskey-diff' ),
2432                         'title'     => wfMsg( 'tooltip-diff' ) . ' [' . wfMsg( 'accesskey-diff' ) . ']',
2433                 );
2434                 $buttons['diff'] = Xml::element( 'input', $temp, '' );
2435
2436                 wfRunHooks( 'EditPageBeforeEditButtons', array( &$this, &$buttons, &$tabindex ) );
2437                 return $buttons;
2438         }
2439
2440         /**
2441          * Output preview text only. This can be sucked into the edit page
2442          * via JavaScript, and saves the server time rendering the skin as
2443          * well as theoretically being more robust on the client (doesn't
2444          * disturb the edit box's undo history, won't eat your text on
2445          * failure, etc).
2446          *
2447          * @todo This doesn't include category or interlanguage links.
2448          *       Would need to enhance it a bit, <s>maybe wrap them in XML
2449          *       or something...</s> that might also require more skin
2450          *       initialization, so check whether that's a problem.
2451          */
2452         function livePreview() {
2453                 global $wgOut;
2454                 $wgOut->disable();
2455                 header( 'Content-type: text/xml; charset=utf-8' );
2456                 header( 'Cache-control: no-cache' );
2457
2458                 $previewText = $this->getPreviewText();
2459                 #$categories = $skin->getCategoryLinks();
2460
2461                 $s =
2462                 '<?xml version="1.0" encoding="UTF-8" ?>' . "\n" .
2463                 Xml::tags( 'livepreview', null,
2464                         Xml::element( 'preview', null, $previewText )
2465                         #.      Xml::element( 'category', null, $categories )
2466                 );
2467                 echo $s;
2468         }
2469
2470
2471         public function getCancelLink() {
2472                 global $wgUser, $wgTitle;
2473
2474                 $cancelParams = array();
2475                 if ( !$this->isConflict && $this->mArticle->getOldID() > 0 ) {
2476                         $cancelParams['oldid'] = $this->mArticle->getOldID();
2477                 }
2478
2479                 return $wgUser->getSkin()->link(
2480                         $wgTitle,
2481                         wfMsgExt( 'cancel', array( 'parseinline' ) ),
2482                         array( 'id' => 'mw-editform-cancel' ),
2483                         $cancelParams,
2484                         array( 'known', 'noclasses' )
2485                 );
2486         }
2487
2488         /**
2489          * Get a diff between the current contents of the edit box and the
2490          * version of the page we're editing from.
2491          *
2492          * If this is a section edit, we'll replace the section as for final
2493          * save and then make a comparison.
2494          */
2495         function showDiff() {
2496                 $oldtext = $this->mArticle->fetchContent();
2497                 $newtext = $this->mArticle->replaceSection(
2498                         $this->section, $this->textbox1, $this->summary, $this->edittime );
2499
2500                 wfRunHooks( 'EditPageGetDiffText', array( $this, &$newtext ) );
2501
2502                 $newtext = $this->mArticle->preSaveTransform( $newtext );
2503                 $oldtitle = wfMsgExt( 'currentrev', array( 'parseinline' ) );
2504                 $newtitle = wfMsgExt( 'yourtext', array( 'parseinline' ) );
2505                 if ( $oldtext !== false  || $newtext != '' ) {
2506                         $de = new DifferenceEngine( $this->mTitle );
2507                         $de->setText( $oldtext, $newtext );
2508                         $difftext = $de->getDiff( $oldtitle, $newtitle );
2509                         $de->showDiffStyle();
2510                 } else {
2511                         $difftext = '';
2512                 }
2513
2514                 global $wgOut;
2515                 $wgOut->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' );
2516         }
2517
2518         /**
2519          * Filter an input field through a Unicode de-armoring process if it
2520          * came from an old browser with known broken Unicode editing issues.
2521          *
2522          * @param $request WebRequest
2523          * @param $field String
2524          * @return String
2525          * @private
2526          */
2527         function safeUnicodeInput( $request, $field ) {
2528                 $text = rtrim( $request->getText( $field ) );
2529                 return $request->getBool( 'safemode' )
2530                         ? $this->unmakesafe( $text )
2531                         : $text;
2532         }
2533
2534         function safeUnicodeText( $request, $text ) {
2535                 $text = rtrim( $text );
2536                 return $request->getBool( 'safemode' )
2537                         ? $this->unmakesafe( $text )
2538                         : $text;
2539         }
2540
2541         /**
2542          * Filter an output field through a Unicode armoring process if it is
2543          * going to an old browser with known broken Unicode editing issues.
2544          *
2545          * @param $text String
2546          * @return String
2547          * @private
2548          */
2549         function safeUnicodeOutput( $text ) {
2550                 global $wgContLang;
2551                 $codedText = $wgContLang->recodeForEdit( $text );
2552                 return $this->checkUnicodeCompliantBrowser()
2553                         ? $codedText
2554                         : $this->makesafe( $codedText );
2555         }
2556
2557         /**
2558          * A number of web browsers are known to corrupt non-ASCII characters
2559          * in a UTF-8 text editing environment. To protect against this,
2560          * detected browsers will be served an armored version of the text,
2561          * with non-ASCII chars converted to numeric HTML character references.
2562          *
2563          * Preexisting such character references will have a 0 added to them
2564          * to ensure that round-trips do not alter the original data.
2565          *
2566          * @param $invalue String
2567          * @return String
2568          * @private
2569          */
2570         function makesafe( $invalue ) {
2571                 // Armor existing references for reversability.
2572                 $invalue = strtr( $invalue, array( "&#x" => "&#x0" ) );
2573
2574                 $bytesleft = 0;
2575                 $result = "";
2576                 $working = 0;
2577                 for( $i = 0; $i < strlen( $invalue ); $i++ ) {
2578                         $bytevalue = ord( $invalue{$i} );
2579                         if ( $bytevalue <= 0x7F ) { //0xxx xxxx
2580                                 $result .= chr( $bytevalue );
2581                                 $bytesleft = 0;
2582                         } elseif ( $bytevalue <= 0xBF ) { //10xx xxxx
2583                                 $working = $working << 6;
2584                                 $working += ($bytevalue & 0x3F);
2585                                 $bytesleft--;
2586                                 if ( $bytesleft <= 0 ) {
2587                                         $result .= "&#x" . strtoupper( dechex( $working ) ) . ";";
2588                                 }
2589                         } elseif ( $bytevalue <= 0xDF ) { //110x xxxx
2590                                 $working = $bytevalue & 0x1F;
2591                                 $bytesleft = 1;
2592                         } elseif ( $bytevalue <= 0xEF ) { //1110 xxxx
2593                                 $working = $bytevalue & 0x0F;
2594                                 $bytesleft = 2;
2595                         } else { //1111 0xxx
2596                                 $working = $bytevalue & 0x07;
2597                                 $bytesleft = 3;
2598                         }
2599                 }
2600                 return $result;
2601         }
2602
2603         /**
2604          * Reverse the previously applied transliteration of non-ASCII characters
2605          * back to UTF-8. Used to protect data from corruption by broken web browsers
2606          * as listed in $wgBrowserBlackList.
2607          *
2608          * @param $invalue String
2609          * @return String
2610          * @private
2611          */
2612         function unmakesafe( $invalue ) {
2613                 $result = "";
2614                 for( $i = 0; $i < strlen( $invalue ); $i++ ) {
2615                         if ( ( substr( $invalue, $i, 3 ) == "&#x" ) && ( $invalue{$i+3} != '0' ) ) {
2616                                 $i += 3;
2617                                 $hexstring = "";
2618                                 do {
2619                                         $hexstring .= $invalue{$i};
2620                                         $i++;
2621                                 } while( ctype_xdigit( $invalue{$i} ) && ( $i < strlen( $invalue ) ) );
2622
2623                                 // Do some sanity checks. These aren't needed for reversability,
2624                                 // but should help keep the breakage down if the editor
2625                                 // breaks one of the entities whilst editing.
2626                                 if ( (substr($invalue,$i,1)==";") and (strlen($hexstring) <= 6) ) {
2627                                         $codepoint = hexdec($hexstring);
2628                                         $result .= codepointToUtf8( $codepoint );
2629                                 } else {
2630                                         $result .= "&#x" . $hexstring . substr( $invalue, $i, 1 );
2631                                 }
2632                         } else {
2633                                 $result .= substr( $invalue, $i, 1 );
2634                         }
2635                 }
2636                 // reverse the transform that we made for reversability reasons.
2637                 return strtr( $result, array( "&#x0" => "&#x" ) );
2638         }
2639
2640         function noCreatePermission() {
2641                 global $wgOut;
2642                 $wgOut->setPageTitle( wfMsg( 'nocreatetitle' ) );
2643                 $wgOut->addWikiMsg( 'nocreatetext' );
2644         }
2645
2646         /**
2647          * Attempt submission
2648          * @return bool false if output is done, true if the rest of the form should be displayed
2649          */
2650         function attemptSave() {
2651                 global $wgUser, $wgOut, $wgTitle;
2652
2653                 $resultDetails = false;
2654                 # Allow bots to exempt some edits from bot flagging
2655                 $bot = $wgUser->isAllowed( 'bot' ) && $this->bot;
2656                 $value = $this->internalAttemptSave( $resultDetails, $bot );
2657
2658                 if ( $value == self::AS_SUCCESS_UPDATE || $value == self::AS_SUCCESS_NEW_ARTICLE ) {
2659                         $this->didSave = true;
2660                 }
2661
2662                 switch ( $value ) {
2663                         case self::AS_HOOK_ERROR_EXPECTED:
2664                         case self::AS_CONTENT_TOO_BIG:
2665                         case self::AS_ARTICLE_WAS_DELETED:
2666                         case self::AS_CONFLICT_DETECTED:
2667                         case self::AS_SUMMARY_NEEDED:
2668                         case self::AS_TEXTBOX_EMPTY:
2669                         case self::AS_MAX_ARTICLE_SIZE_EXCEEDED:
2670                         case self::AS_END:
2671                                 return true;
2672
2673                         case self::AS_HOOK_ERROR:
2674                         case self::AS_FILTERING:
2675                         case self::AS_SUCCESS_NEW_ARTICLE:
2676                         case self::AS_SUCCESS_UPDATE:
2677                                 return false;
2678
2679                         case self::AS_SPAM_ERROR:
2680                                 $this->spamPageWithContent( $resultDetails['spam'] );
2681                                 return false;
2682
2683                         case self::AS_BLOCKED_PAGE_FOR_USER:
2684                                 $this->blockedPage();
2685                                 return false;
2686
2687                         case self::AS_IMAGE_REDIRECT_ANON:
2688                                 $wgOut->showErrorPage( 'uploadnologin', 'uploadnologintext' );
2689                                 return false;
2690
2691                         case self::AS_READ_ONLY_PAGE_ANON:
2692                                 $this->userNotLoggedInPage();
2693                                 return false;
2694
2695                         case self::AS_READ_ONLY_PAGE_LOGGED:
2696                         case self::AS_READ_ONLY_PAGE:
2697                                 $wgOut->readOnlyPage();
2698                                 return false;
2699
2700                         case self::AS_RATE_LIMITED:
2701                                 $wgOut->rateLimited();
2702                                 return false;
2703
2704                         case self::AS_NO_CREATE_PERMISSION:
2705                                 $this->noCreatePermission();
2706                                 return;
2707
2708                         case self::AS_BLANK_ARTICLE:
2709                                 $wgOut->redirect( $wgTitle->getFullURL() );
2710                                 return false;
2711
2712                         case self::AS_IMAGE_REDIRECT_LOGGED:
2713                                 $wgOut->permissionRequired( 'upload' );
2714                                 return false;
2715                 }
2716         }
2717
2718         function getBaseRevision() {
2719                 if ( !$this->mBaseRevision ) {
2720                         $db = wfGetDB( DB_MASTER );
2721                         $baseRevision = Revision::loadFromTimestamp(
2722                                 $db, $this->mTitle, $this->edittime );
2723                         return $this->mBaseRevision = $baseRevision;
2724                 } else {
2725                         return $this->mBaseRevision;
2726                 }
2727         }
2728 }