New infrastructure for actions, as discussed on wikitech-l. Fairly huge commit.
authorHappy-melon <happy-melon@users.mediawiki.org>
Wed, 13 Apr 2011 23:04:07 +0000 (23:04 +0000)
committerHappy-melon <happy-melon@users.mediawiki.org>
Wed, 13 Apr 2011 23:04:07 +0000 (23:04 +0000)
* Actions come in two flavours: the show-a-form-then-do-something-with-the-result (delete, protect, edit, etc) and the just-do-something (watch, rollback, patrol, etc).  Create abstract base classes Action and FormlessAction to support these two cases.  HTMLForm is an integral part of the form-based structure.
* Look mum, no globals!  :D  Fully context-based.
* Implement watch/unwatch, credits and delete actions in the new system as proof-of-concept.  This also gives the delete frontend a much-needed overhaul.
* Stub out the newly-deprecated functions from Article.php.  This already reduces its linecount by about 15%, and there are plenty more actions still to do.
* Centralising actions like this is going to render a lot of hooks type-incompatible.  There's simply nowhere you can put the ArticleConfirmDelete hook, for instance, where it can be passed an OutputPage as the second parameter.  On the other hand, we can implement new hooks like ActionModifyFormFields and ActionBeforeFormDisplay, which can do much prettier stuff to the forms, like adding extra fields the 'right' way.  Update LiquidThreads to use these new hooks where appropriate.

19 files changed:
docs/hooks.txt
includes/Action.php [new file with mode: 0644]
includes/Article.php
includes/AutoLoader.php
includes/Credits.php [deleted file]
includes/DefaultSettings.php
includes/EditPage.php
includes/FileDeleteForm.php
includes/ProtectionForm.php
includes/Setup.php
includes/Wiki.php
includes/actions/CreditsAction.php [new file with mode: 0644]
includes/actions/DeleteAction.php [new file with mode: 0644]
includes/actions/WatchAction.php [new file with mode: 0644]
includes/api/ApiBase.php
includes/api/ApiDelete.php
includes/api/ApiWatch.php
includes/specials/SpecialMovepage.php
languages/messages/MessagesEn.php

index 9935299..1fe061c 100644 (file)
@@ -264,6 +264,17 @@ $reason: the reason for the move (added in 1.13)
 $user: the User object about to be created (read-only, incomplete)
 $message: out parameter: error message to display on abort
 
+'ActionBeforeFormDisplay': Modify the form shown for an action (added 1.18)
+$action: String
+$form: HTMLForm
+$page: Article
+
+'ActionModifyFormFields': Modify the descriptor array which will be used to create an
+action form
+$action: String
+$fields: Array
+$page: Article
+
 'AddNewAccount': after a user account is created
 $user: the User object that was created. (Parameter added in 1.7)
 $byEmail: true when account was created "by email" (added in 1.12)
@@ -384,12 +395,6 @@ the database
 $article: the article (object) being loaded from the database
 $content: the content (string) of the article
 
-'ArticleConfirmDelete': before writing the confirmation form for article
-       deletion
-$article: the article (object) being deleted
-$output: the OutputPage object ($wgOut)
-&$reason: the reason (string) the article is being deleted
-
 'ArticleContentOnDiff': before showing the article content below a diff.
 Use this to change the content in this area or how it is loaded.
  $diffEngine: the DifferenceEngine
diff --git a/includes/Action.php b/includes/Action.php
new file mode 100644 (file)
index 0000000..44fafca
--- /dev/null
@@ -0,0 +1,440 @@
+<?php
+/**
+ * Actions are things which can be done to pages (edit, delete, rollback, etc).  They
+ * are distinct from Special Pages because an action must apply to exactly one page.
+ *
+ * To add an action in an extension, create a subclass of Action, and add the key to
+ * $wgActions.  There is also the deprecated UnknownAction hook
+ *
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * @file
+ */
+abstract class Action {
+
+       // Page on which we're performing the action
+       // @var Article
+       protected $page;
+
+       // RequestContext if specified; otherwise we'll use the Context from the Page
+       // @var RequestContext
+       protected $context;
+
+       // The fields used to create the HTMLForm
+       // @var Array
+       protected $fields;
+
+       /**
+        * Get the Action subclass which should be used to handle this action, false if
+        * the action is disabled, or null if it's not recognised
+        * @param $action String
+        * @return bool|null|string
+        */
+       private final static function getClass( $action ){
+               global $wgActions;
+               $action = strtolower( $action );
+
+               if( !isset( $wgActions[$action] ) ){
+                       return null;
+               }
+
+               if( $wgActions[$action] === false ){
+                       return false;
+               }
+
+               elseif( $wgActions[$action] === true ){
+                       return ucfirst( $action ) . 'Action';
+               }
+
+               else {
+                       return $wgActions[$action];
+               }
+       }
+
+       /**
+        * Get an appropriate Action subclass for the given action
+        * @param $action String
+        * @param $page Article
+        * @return Action|false|null false if the action is disabled, null
+        *     if it is not recognised
+        */
+       public final static function factory( $action, Article $page ){
+               $class = self::getClass( $action );
+               if( $class ){
+                       $obj = new $class( $page );
+                       return $obj;
+               }
+               return null;
+       }
+
+       /**
+        * Check if a given action is recognised, even if it's disabled
+        *
+        * @param $name String: name of an action
+        * @return Bool
+        */
+       public final static function exists( $name ) {
+               return self::getClass( $name ) !== null;
+       }
+
+       /**
+        * Get the RequestContext in use here
+        * @return RequestContext
+        */
+       protected final function getContext(){
+               if( $this->context instanceof RequestContext ){
+                       return $this->context;
+               }
+               return $this->page->getContext();
+       }
+
+       /**
+        * Get the WebRequest being used for this instance
+        *
+        * @return WebRequest
+        */
+       protected final function getRequest() {
+               return $this->getContext()->request;
+       }
+
+       /**
+        * Get the OutputPage being used for this instance
+        *
+        * @return OutputPage
+        */
+       protected final function getOutput() {
+               return $this->getContext()->output;
+       }
+
+       /**
+        * Shortcut to get the skin being used for this instance
+        *
+        * @return User
+        */
+       protected final function getUser() {
+               return $this->getContext()->user;
+       }
+
+       /**
+        * Shortcut to get the skin being used for this instance
+        *
+        * @return Skin
+        */
+       protected final function getSkin() {
+               return $this->getContext()->skin;
+       }
+
+       /**
+        * Shortcut to get the Title object from the page
+        * @return Title
+        */
+       protected final function getTitle(){
+               return $this->page->getTitle();
+       }
+
+       /**
+        * Protected constructor: use Action::factory( $action, $page ) to actually build
+        * these things in the real world
+        * @param Article $page
+        */
+       protected function __construct( Article $page ){
+               $this->page = $page;
+       }
+
+       /**
+        * Return the name of the action this object responds to
+        * @return String lowercase
+        */
+       public abstract function getName();
+
+       /**
+        * Get the permission required to perform this action.  Often, but not always,
+        * the same as the action name
+        */
+       public abstract function getRestriction();
+
+       /**
+        * Checks if the given user (identified by an object) can perform this action.  Can be
+        * overridden by sub-classes with more complicated permissions schemes.  Failures here
+        * must throw subclasses of ErrorPageError
+        *
+        * @param $user User: the user to check, or null to use the context user
+        * @throws ErrorPageError
+        */
+       protected function checkCanExecute( User $user ) {
+               if( $this->requiresWrite() && wfReadOnly() ){
+                       throw new ReadOnlyError();
+               }
+
+               if( $this->getRestriction() !== null && !$user->isAllowed( $this->getRestriction() ) ){
+                       throw new PermissionsError( $this->getRestriction() );
+               }
+
+               if( $this->requiresUnblock() && $user->isBlocked() ){
+                       $block = $user->mBlock;
+                       throw new UserBlockedError( $block );
+               }
+       }
+
+       /**
+        * Whether this action requires the wiki not to be locked
+        * @return Bool
+        */
+       public function requiresWrite(){
+               return true;
+       }
+
+       /**
+        * Whether this action can still be executed by a blocked user
+        * @return Bool
+        */
+       public function requiresUnblock(){
+               return true;
+       }
+
+       /**
+        * Set output headers for noindexing etc.  This function will not be called through
+        * the execute() entry point, so only put UI-related stuff in here.
+        */
+       protected function setHeaders() {
+               $out = $this->getOutput();
+               $out->setRobotPolicy( "noindex,nofollow" );
+               $out->setPageTitle( $this->getTitle()->getPrefixedText() );
+               $this->getOutput()->setSubtitle( $this->getDescription() );
+               $out->setArticleRelated( true );
+       }
+
+       /**
+        * Returns the name that goes in the \<h1\> page title
+        *
+        * Derived classes can override this, but usually it is easier to keep the
+        * default behaviour. Messages can be added at run-time, see
+        * MessageCache.php.
+        *
+        * @return String
+        */
+       protected function getDescription() {
+               return wfMsg( strtolower( $this->getName() ) );
+       }
+
+       /**
+        * The basic pattern for actions is to display some sort of HTMLForm UI, maybe with
+        * some stuff underneath (history etc); to do some processing on submission of that
+        * form (delete, protect, etc) and to do something exciting on 'success', be that
+        * display something new or redirect to somewhere.  Some actions have more exotic
+        * behaviour, but that's what subclassing is for :D
+        */
+       public function show(){
+               $this->setHeaders();
+
+               // This will throw exceptions if there's a problem
+               $this->checkCanExecute( $this->getUser() );
+
+               $form = $this->getForm();
+               if( $form instanceof HTMLForm ){
+                       if( $form->show() ){
+                               $this->onSuccess();
+                       }
+               } else {
+                       // You're using the wrong type of Action
+                       throw new MWException( "Action::getFormFields() must produce a form.  Use GetAction if you don't want one." );
+               }
+       }
+
+       /**
+        * Execute the action in a silent fashion: do not display anything or release any errors.
+        * @param $data Array values that would normally be in the POST request
+        * @param $captureErrors Bool whether to catch exceptions and just return false
+        * @return Bool whether execution was successful
+        */
+       public function execute( array $data = null, $captureErrors = true ){
+               try {
+                       // Set a new context so output doesn't leak.
+                       $this->context = clone $this->page->getContext();
+
+                       // This will throw exceptions if there's a problem
+                       $this->checkCanExecute( $this->getUser() );
+
+                       $form = $this->getForm();
+                       if( $form instanceof HTMLForm ){
+                               // Great, so there's a form.  Ignore it and go straight to the submission callback
+                               $fields = array();
+                               foreach( $this->fields as $key => $params ){
+                                       if( isset( $data[$key] ) ){
+                                               $fields[$key] = $data[$key];
+                                       } elseif( isset( $params['default'] ) ) {
+                                               $fields[$key] = $params['default'];
+                                       } else {
+                                               $fields[$key] = null;
+                                       }
+                               }
+                               $status = $this->onSubmit( $fields );
+                               if( $status === true ){
+                                       // This might do permanent stuff
+                                       $this->onSuccess();
+                                       return true;
+                               } else {
+                                       return false;
+                               }
+                       } else {
+                               // You're using the wrong type of Action
+                               throw new MWException( "Action::getFormFields() must produce a form.  Use GetAction if you don't want one." );
+                       }
+               }
+               catch ( ErrorPageError $e ){
+                       if( $captureErrors ){
+                               return false;
+                       } else {
+                               throw $e;
+                       }
+               }
+       }
+
+       /**
+        * Get an HTMLForm descriptor array, or false if you don't want a form
+        * @return Array
+        */
+       protected abstract function getFormFields();
+
+       /**
+        * Add pre- or post-text to the form
+        * @return String
+        */
+       protected function preText(){ return ''; }
+       protected function postText(){ return ''; }
+
+       /**
+        * Play with the HTMLForm if you need to more substantially
+        * @param &$form HTMLForm
+        */
+       protected function alterForm( HTMLForm &$form ){}
+
+       /**
+        * Get the HTMLForm to control behaviour
+        * @return HTMLForm|null
+        */
+       protected function getForm(){
+               $this->fields = $this->getFormFields();
+
+               // Give hooks a chance to alter the form, adding extra fields or text etc
+               wfRunHooks( 'ActionModifyFormFields', array( $this->getName(), &$this->fields, $this->page ) );
+
+               if( $this->fields === false ){
+                       return null;
+               }
+
+               $form = new HTMLForm( $this->fields, $this->getContext() );
+               $form->setSubmitCallback( array( $this, 'onSubmit' ) );
+               $form->addHiddenField( 'action', $this->getName() );
+
+               $form->addPreText( $this->preText() );
+               $form->addPostText( $this->postText() );
+               $this->alterForm( $form );
+
+               // Give hooks a chance to alter the form, adding extra fields or text etc
+               wfRunHooks( 'ActionBeforeFormDisplay', array( $this->getName(), &$form, $this->page ) );
+
+               return $form;
+       }
+
+       /**
+        * Process the form on POST submission.  If you return false from getFormFields(),
+        * this will obviously never be reached.  If you don't want to do anything with the
+        * form, just return false here
+        * @param  $data Array
+        * @return Bool|Array true for success, false for didn't-try, array of errors on failure
+        */
+       public abstract function onSubmit( $data );
+
+       /**
+        * Do something exciting on successful processing of the form.  This might be to show
+        * a confirmation message (watch, rollback, etc) or to redirect somewhere else (edit,
+        * protect, etc).
+        */
+       public abstract function onSuccess();
+
+}
+
+/**
+ * Actions generally fall into two groups: the show-a-form-then-do-something-with-the-input
+ * format (protect, delete, move, etc), and the just-do-something format (watch, rollback,
+ * patrol, etc).
+ */
+abstract class FormlessAction extends Action {
+
+       /**
+        * Show something on GET request. This is displayed as the postText() of the HTMLForm
+        * if there is one; you can always use alterForm() to add pre text if you need it.  If
+        * you call addPostText() from alterForm() as well as overriding this function, you
+        * might get strange ordering.
+        * @return String|null will be added to the HTMLForm if present, or just added to the
+        *     output if not.  Return null to not add anything
+        */
+       public abstract function onView();
+
+       /**
+        * We don't want an HTMLForm
+        */
+       protected function getFormFields(){
+               return false;
+       }
+
+       public function onSubmit( $data ){
+               return false;
+       }
+
+       public function onSuccess(){
+               return false;
+       }
+
+       public function show(){
+               $this->setHeaders();
+
+               // This will throw exceptions if there's a problem
+               $this->checkCanExecute( $this->getUser() );
+               $this->getOutput()->addHTML( $this->onView() );
+       }
+
+       /**
+        * Execute the action silently, not giving any output.  Since these actions don't have
+        * forms, they probably won't have any data, but some (eg rollback) may do
+        * @param $data Array values that would normally be in the GET request
+        * @param $captureErrors Bool whether to catch exceptions and just return false
+        * @return Bool whether execution was successful
+        */
+       public function execute( array $data = null, $captureErrors = true){
+               try {
+                       // Set a new context so output doesn't leak.
+                       $this->context = clone $this->page->getContext();
+                       if( is_array( $data ) ){
+                               $this->context->setRequest( new FauxRequest( $data, false ) );
+                       }
+
+                       // This will throw exceptions if there's a problem
+                       $this->checkCanExecute( $this->getUser() );
+
+                       $this->onView();
+                       return true;
+               }
+               catch ( ErrorPageError $e ){
+                       if( $captureErrors ){
+                               return false;
+                       } else {
+                               throw $e;
+                       }
+               }
+       }
+}
\ No newline at end of file
index b9288c2..97eb17b 100644 (file)
@@ -2345,27 +2345,10 @@ class Article {
 
        /**
         * User-interface handler for the "watch" action
+        * @deprecated since 1.18
         */
        public function watch() {
-               global $wgOut;
-
-               if ( $wgOut->getUser()->isAnon() ) {
-                       $wgOut->showErrorPage( 'watchnologin', 'watchnologintext' );
-                       return;
-               }
-
-               if ( wfReadOnly() ) {
-                       $wgOut->readOnlyPage();
-                       return;
-               }
-
-               if ( $this->doWatch() ) {
-                       $wgOut->setPagetitle( wfMsg( 'addedwatch' ) );
-                       $wgOut->setRobotPolicy( 'noindex,nofollow' );
-                       $wgOut->addWikiMsg( 'addedwatchtext', $this->mTitle->getPrefixedText() );
-               }
-
-               $wgOut->returnToMain( true, $this->mTitle->getPrefixedText() );
+               Action::factory( 'watch', $this )->show();
        }
 
        /**
@@ -2374,64 +2357,27 @@ class Article {
         * This is safe to be called multiple times
         *
         * @return bool true on successful watch operation
+        * @deprecated since 1.18
         */
        public function doWatch() {
-               global $wgUser;
-
-               if ( $wgUser->isAnon() ) {
-                       return false;
-               }
-
-               if ( wfRunHooks( 'WatchArticle', array( &$wgUser, &$this ) ) ) {
-                       $wgUser->addWatch( $this->mTitle );
-                       return wfRunHooks( 'WatchArticleComplete', array( &$wgUser, &$this ) );
-               }
-
-               return false;
+               return Action::factory( 'watch', $this )->execute();
        }
 
        /**
         * User interface handler for the "unwatch" action.
+        * @deprecated since 1.18
         */
        public function unwatch() {
-               global $wgOut;
-
-               if ( $wgOut->getUser()->isAnon() ) {
-                       $wgOut->showErrorPage( 'watchnologin', 'watchnologintext' );
-                       return;
-               }
-
-               if ( wfReadOnly() ) {
-                       $wgOut->readOnlyPage();
-                       return;
-               }
-
-               if ( $this->doUnwatch() ) {
-                       $wgOut->setPagetitle( wfMsg( 'removedwatch' ) );
-                       $wgOut->setRobotPolicy( 'noindex,nofollow' );
-                       $wgOut->addWikiMsg( 'removedwatchtext', $this->mTitle->getPrefixedText() );
-               }
-
-               $wgOut->returnToMain( true, $this->mTitle->getPrefixedText() );
+               Action::factory( 'unwatch', $this )->show();
        }
 
        /**
         * Stop watching a page
         * @return bool true on successful unwatch
+        * @deprecated since 1.18
         */
        public function doUnwatch() {
-               global $wgUser;
-
-               if ( $wgUser->isAnon() ) {
-                       return false;
-               }
-
-               if ( wfRunHooks( 'UnwatchArticle', array( &$wgUser, &$this ) ) ) {
-                       $wgUser->removeWatch( $this->mTitle );
-                       return wfRunHooks( 'UnwatchArticleComplete', array( &$wgUser, &$this ) );
-               }
-
-               return false;
+               return Action::factory( 'unwatch', $this )->execute();
        }
 
        /**
@@ -2663,229 +2609,28 @@ class Article {
         * @param &$hasHistory Boolean: whether the page has a history
         * @return mixed String containing deletion reason or empty string, or boolean false
         *    if no revision occurred
+        * @deprecated since 1.18
         */
        public function generateReason( &$hasHistory ) {
-               global $wgContLang;
-
-               $dbw = wfGetDB( DB_MASTER );
-               // Get the last revision
-               $rev = Revision::newFromTitle( $this->mTitle );
-
-               if ( is_null( $rev ) ) {
-                       return false;
-               }
-
-               // Get the article's contents
-               $contents = $rev->getText();
-               $blank = false;
-
-               // If the page is blank, use the text from the previous revision,
-               // which can only be blank if there's a move/import/protect dummy revision involved
-               if ( $contents == '' ) {
-                       $prev = $rev->getPrevious();
-
-                       if ( $prev )    {
-                               $contents = $prev->getText();
-                               $blank = true;
-                       }
-               }
-
-               // Find out if there was only one contributor
-               // Only scan the last 20 revisions
-               $res = $dbw->select( 'revision', 'rev_user_text',
-                       array( 'rev_page' => $this->getID(), $dbw->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' ),
-                       __METHOD__,
-                       array( 'LIMIT' => 20 )
-               );
-
-               if ( $res === false ) {
-                       // This page has no revisions, which is very weird
-                       return false;
-               }
-
-               $hasHistory = ( $res->numRows() > 1 );
-               $row = $dbw->fetchObject( $res );
-
-               if ( $row ) { // $row is false if the only contributor is hidden
-                       $onlyAuthor = $row->rev_user_text;
-                       // Try to find a second contributor
-                       foreach ( $res as $row ) {
-                               if ( $row->rev_user_text != $onlyAuthor ) { // Bug 22999
-                                       $onlyAuthor = false;
-                                       break;
-                               }
-                       }
-               } else {
-                       $onlyAuthor = false;
-               }
-
-               // Generate the summary with a '$1' placeholder
-               if ( $blank ) {
-                       // The current revision is blank and the one before is also
-                       // blank. It's just not our lucky day
-                       $reason = wfMsgForContent( 'exbeforeblank', '$1' );
-               } else {
-                       if ( $onlyAuthor ) {
-                               $reason = wfMsgForContent( 'excontentauthor', '$1', $onlyAuthor );
-                       } else {
-                               $reason = wfMsgForContent( 'excontent', '$1' );
-                       }
-               }
-
-               if ( $reason == '-' ) {
-                       // Allow these UI messages to be blanked out cleanly
-                       return '';
-               }
-
-               // Replace newlines with spaces to prevent uglyness
-               $contents = preg_replace( "/[\n\r]/", ' ', $contents );
-               // Calculate the maximum amount of chars to get
-               // Max content length = max comment length - length of the comment (excl. $1)
-               $maxLength = 255 - ( strlen( $reason ) - 2 );
-               $contents = $wgContLang->truncate( $contents, $maxLength );
-               // Remove possible unfinished links
-               $contents = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $contents );
-               // Now replace the '$1' placeholder
-               $reason = str_replace( '$1', $contents, $reason );
-
-               return $reason;
+               return DeleteAction::getAutoReason( $this );
        }
 
 
        /*
         * UI entry point for page deletion
+        * @deprecated since 1.18
         */
        public function delete() {
-               global $wgOut, $wgRequest;
-
-               $confirm = $wgRequest->wasPosted() &&
-                               $wgOut->getUser()->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) );
-
-               $this->DeleteReasonList = $wgRequest->getText( 'wpDeleteReasonList', 'other' );
-               $this->DeleteReason = $wgRequest->getText( 'wpReason' );
-
-               $reason = $this->DeleteReasonList;
-
-               if ( $reason != 'other' && $this->DeleteReason != '' ) {
-                       // Entry from drop down menu + additional comment
-                       $reason .= wfMsgForContent( 'colon-separator' ) . $this->DeleteReason;
-               } elseif ( $reason == 'other' ) {
-                       $reason = $this->DeleteReason;
-               }
-
-               # Flag to hide all contents of the archived revisions
-               $suppress = $wgRequest->getVal( 'wpSuppress' ) && $wgOut->getUser()->isAllowed( 'suppressrevision' );
-
-               # This code desperately needs to be totally rewritten
-
-               # Read-only check...
-               if ( wfReadOnly() ) {
-                       $wgOut->readOnlyPage();
-
-                       return;
-               }
-
-               # Check permissions
-               $permission_errors = $this->mTitle->getUserPermissionsErrors( 'delete', $wgOut->getUser() );
-
-               if ( count( $permission_errors ) > 0 ) {
-                       $wgOut->showPermissionsErrorPage( $permission_errors );
-
-                       return;
-               }
-
-               $wgOut->setPagetitle( wfMsg( 'delete-confirm', $this->mTitle->getPrefixedText() ) );
-
-               # Better double-check that it hasn't been deleted yet!
-               $dbw = wfGetDB( DB_MASTER );
-               $conds = $this->mTitle->pageCond();
-               $latest = $dbw->selectField( 'page', 'page_latest', $conds, __METHOD__ );
-               if ( $latest === false ) {
-                       $wgOut->showFatalError(
-                               Html::rawElement(
-                                       'div',
-                                       array( 'class' => 'error mw-error-cannotdelete' ),
-                                       wfMsgExt( 'cannotdelete', array( 'parse' ), $this->mTitle->getPrefixedText() )
-                               )
-                       );
-                       $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) );
-                       LogEventsList::showLogExtract(
-                               $wgOut,
-                               'delete',
-                               $this->mTitle->getPrefixedText()
-                       );
-
-                       return;
-               }
-
-               # Hack for big sites
-               $bigHistory = $this->isBigDeletion();
-               if ( $bigHistory && !$this->mTitle->userCan( 'bigdelete' ) ) {
-                       global $wgLang, $wgDeleteRevisionsLimit;
-
-                       $wgOut->wrapWikiMsg( "<div class='error'>\n$1\n</div>\n",
-                               array( 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ) );
-
-                       return;
-               }
-
-               if ( $confirm ) {
-                       $this->doDelete( $reason, $suppress );
-
-                       if ( $wgRequest->getCheck( 'wpWatch' ) && $wgOut->getUser()->isLoggedIn() ) {
-                               $this->doWatch();
-                       } elseif ( $this->mTitle->userIsWatching() ) {
-                               $this->doUnwatch();
-                       }
-
-                       return;
-               }
-
-               // Generate deletion reason
-               $hasHistory = false;
-               if ( !$reason ) {
-                       $reason = $this->generateReason( $hasHistory );
-               }
-
-               // If the page has a history, insert a warning
-               if ( $hasHistory && !$confirm ) {
-                       global $wgLang;
-
-                       $skin = $wgOut->getSkin();
-                       $revisions = $this->estimateRevisionCount();
-                       //FIXME: lego
-                       $wgOut->addHTML( '<strong class="mw-delete-warning-revisions">' .
-                               wfMsgExt( 'historywarning', array( 'parseinline' ), $wgLang->formatNum( $revisions ) ) .
-                               wfMsgHtml( 'word-separator' ) . $skin->link( $this->mTitle,
-                                       wfMsgHtml( 'history' ),
-                                       array( 'rel' => 'archives' ),
-                                       array( 'action' => 'history' ) ) .
-                               '</strong>'
-                       );
-
-                       if ( $bigHistory ) {
-                               global $wgDeleteRevisionsLimit;
-                               $wgOut->wrapWikiMsg( "<div class='error'>\n$1\n</div>\n",
-                                       array( 'delete-warning-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ) );
-                       }
-               }
-
-               return $this->confirmDelete( $reason );
+               return Action::factory( 'delete', $this )->show();
        }
 
        /**
         * @return bool whether or not the page surpasses $wgDeleteRevisionsLimit revisions
+        * @deprecated since 1.18
         */
        public function isBigDeletion() {
                global $wgDeleteRevisionsLimit;
-
-               if ( $wgDeleteRevisionsLimit ) {
-                       $revCount = $this->estimateRevisionCount();
-
-                       return $revCount > $wgDeleteRevisionsLimit;
-               }
-
-               return false;
+               return $wgDeleteRevisionsLimit && $this->estimateRevisionCount() > $wgDeleteRevisionsLimit;
        }
 
        /**
@@ -2953,151 +2698,20 @@ class Article {
                return $authors;
        }
 
-       /**
-        * Output deletion confirmation dialog
-        * FIXME: Move to another file?
-        * @param $reason String: prefilled reason
-        */
-       public function confirmDelete( $reason ) {
-               global $wgOut;
-
-               wfDebug( "Article::confirmDelete\n" );
-
-               $deleteBackLink = $wgOut->getSkin()->linkKnown( $this->mTitle );
-               $wgOut->setSubtitle( wfMsgHtml( 'delete-backlink', $deleteBackLink ) );
-               $wgOut->setRobotPolicy( 'noindex,nofollow' );
-               $wgOut->addWikiMsg( 'confirmdeletetext' );
-
-               wfRunHooks( 'ArticleConfirmDelete', array( $this, $wgOut, &$reason ) );
-
-               if ( $wgOut->getUser()->isAllowed( 'suppressrevision' ) ) {
-                       $suppress = "<tr id=\"wpDeleteSuppressRow\">
-                                       <td></td>
-                                       <td class='mw-input'><strong>" .
-                                               Xml::checkLabel( wfMsg( 'revdelete-suppress' ),
-                                                       'wpSuppress', 'wpSuppress', false, array( 'tabindex' => '4' ) ) .
-                                       "</strong></td>
-                               </tr>";
-               } else {
-                       $suppress = '';
-               }
-               $checkWatch = $wgOut->getUser()->getBoolOption( 'watchdeletion' ) || $this->mTitle->userIsWatching();
-
-               $form = Xml::openElement( 'form', array( 'method' => 'post',
-                       'action' => $this->mTitle->getLocalURL( 'action=delete' ), 'id' => 'deleteconfirm' ) ) .
-                       Xml::openElement( 'fieldset', array( 'id' => 'mw-delete-table' ) ) .
-                       Xml::tags( 'legend', null, wfMsgExt( 'delete-legend', array( 'parsemag', 'escapenoentities' ) ) ) .
-                       Xml::openElement( 'table', array( 'id' => 'mw-deleteconfirm-table' ) ) .
-                       "<tr id=\"wpDeleteReasonListRow\">
-                               <td class='mw-label'>" .
-                                       Xml::label( wfMsg( 'deletecomment' ), 'wpDeleteReasonList' ) .
-                               "</td>
-                               <td class='mw-input'>" .
-                                       Xml::listDropDown( 'wpDeleteReasonList',
-                                               wfMsgForContent( 'deletereason-dropdown' ),
-                                               wfMsgForContent( 'deletereasonotherlist' ), '', 'wpReasonDropDown', 1 ) .
-                               "</td>
-                       </tr>
-                       <tr id=\"wpDeleteReasonRow\">
-                               <td class='mw-label'>" .
-                                       Xml::label( wfMsg( 'deleteotherreason' ), 'wpReason' ) .
-                               "</td>
-                               <td class='mw-input'>" .
-                               Html::input( 'wpReason', $reason, 'text', array(
-                                       'size' => '60',
-                                       'maxlength' => '255',
-                                       'tabindex' => '2',
-                                       'id' => 'wpReason',
-                                       'autofocus'
-                               ) ) .
-                               "</td>
-                       </tr>";
-
-               # Disallow watching if user is not logged in
-               if ( $wgOut->getUser()->isLoggedIn() ) {
-                       $form .= "
-                       <tr>
-                               <td></td>
-                               <td class='mw-input'>" .
-                                       Xml::checkLabel( wfMsg( 'watchthis' ),
-                                               'wpWatch', 'wpWatch', $checkWatch, array( 'tabindex' => '3' ) ) .
-                               "</td>
-                       </tr>";
-               }
-
-               $form .= "
-                       $suppress
-                       <tr>
-                               <td></td>
-                               <td class='mw-submit'>" .
-                                       Xml::submitButton( wfMsg( 'deletepage' ),
-                                               array( 'name' => 'wpConfirmB', 'id' => 'wpConfirmB', 'tabindex' => '5' ) ) .
-                               "</td>
-                       </tr>" .
-                       Xml::closeElement( 'table' ) .
-                       Xml::closeElement( 'fieldset' ) .
-                       Html::hidden( 'wpEditToken', $wgOut->getUser()->editToken() ) .
-                       Xml::closeElement( 'form' );
-
-                       if ( $wgOut->getUser()->isAllowed( 'editinterface' ) ) {
-                               $skin = $wgOut->getSkin();
-                               $title = Title::makeTitle( NS_MEDIAWIKI, 'Deletereason-dropdown' );
-                               $link = $skin->link(
-                                       $title,
-                                       wfMsgHtml( 'delete-edit-reasonlist' ),
-                                       array(),
-                                       array( 'action' => 'edit' )
-                               );
-                               $form .= '<p class="mw-delete-editreasons">' . $link . '</p>';
-                       }
-
-               $wgOut->addHTML( $form );
-               $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) );
-               LogEventsList::showLogExtract( $wgOut, 'delete',
-                       $this->mTitle->getPrefixedText()
-               );
-       }
-
        /**
         * Perform a deletion and output success or failure messages
+        * @deprecated since 1.18
         */
        public function doDelete( $reason, $suppress = false ) {
-               global $wgOut;
-
-               $id = $this->mTitle->getArticleID( Title::GAID_FOR_UPDATE );
-
-               $error = '';
-               if ( $this->doDeleteArticle( $reason, $suppress, $id, $error ) ) {
-                       $deleted = $this->mTitle->getPrefixedText();
-
-                       $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) );
-                       $wgOut->setRobotPolicy( 'noindex,nofollow' );
-
-                       $loglink = '[[Special:Log/delete|' . wfMsgNoTrans( 'deletionlog' ) . ']]';
-
-                       $wgOut->addWikiMsg( 'deletedtext', $deleted, $loglink );
-                       $wgOut->returnToMain( false );
-               } else {
-                       if ( $error == '' ) {
-                               $wgOut->showFatalError(
-                                       Html::rawElement(
-                                               'div',
-                                               array( 'class' => 'error mw-error-cannotdelete' ),
-                                               wfMsgExt( 'cannotdelete', array( 'parse' ), $this->mTitle->getPrefixedText() )
-                                       )
-                               );
-
-                               $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) );
-
-                               LogEventsList::showLogExtract(
-                                       $wgOut,
-                                       'delete',
-                                       $this->mTitle->getPrefixedText()
-                               );
-                       } else {
-                               $wgOut->showFatalError( $error );
-                       }
-               }
+               return DeleteAction::doDeleteArticle(
+                       $this,
+                       $this->getContext(),
+                       array(
+                               'Suppress' => $suppress !== false,
+                               'Reason' => $reason,
+                       ),
+                       true
+               );
        }
 
        /**
@@ -3113,143 +2727,19 @@ class Article {
         * @param $id int article ID
         * @param $commit boolean defaults to true, triggers transaction end
         * @return boolean true if successful
+        *
+        * @deprecated since 1.18
         */
        public function doDeleteArticle( $reason, $suppress = false, $id = 0, $commit = true, &$error = '' ) {
-               global $wgDeferredUpdateList, $wgUseTrackbacks;
-               global $wgUser;
-
-               wfDebug( __METHOD__ . "\n" );
-
-               if ( ! wfRunHooks( 'ArticleDelete', array( &$this, &$wgUser, &$reason, &$error ) ) ) {
-                       return false;
-               }
-               $dbw = wfGetDB( DB_MASTER );
-               $t = $this->mTitle->getDBkey();
-               $id = $id ? $id : $this->mTitle->getArticleID( Title::GAID_FOR_UPDATE );
-
-               if ( $t === '' || $id == 0 ) {
-                       return false;
-               }
-
-               $u = new SiteStatsUpdate( 0, 1, - (int)$this->isCountable( $this->getRawText() ), -1 );
-               array_push( $wgDeferredUpdateList, $u );
-
-               // Bitfields to further suppress the content
-               if ( $suppress ) {
-                       $bitfield = 0;
-                       // This should be 15...
-                       $bitfield |= Revision::DELETED_TEXT;
-                       $bitfield |= Revision::DELETED_COMMENT;
-                       $bitfield |= Revision::DELETED_USER;
-                       $bitfield |= Revision::DELETED_RESTRICTED;
-               } else {
-                       $bitfield = 'rev_deleted';
-               }
-
-               $dbw->begin();
-               // For now, shunt the revision data into the archive table.
-               // Text is *not* removed from the text table; bulk storage
-               // is left intact to avoid breaking block-compression or
-               // immutable storage schemes.
-               //
-               // For backwards compatibility, note that some older archive
-               // table entries will have ar_text and ar_flags fields still.
-               //
-               // In the future, we may keep revisions and mark them with
-               // the rev_deleted field, which is reserved for this purpose.
-               $dbw->insertSelect( 'archive', array( 'page', 'revision' ),
+               return DeleteAction::doDeleteArticle(
+                       $this,
+                       $this->getContext(),
                        array(
-                               'ar_namespace'  => 'page_namespace',
-                               'ar_title'      => 'page_title',
-                               'ar_comment'    => 'rev_comment',
-                               'ar_user'       => 'rev_user',
-                               'ar_user_text'  => 'rev_user_text',
-                               'ar_timestamp'  => 'rev_timestamp',
-                               'ar_minor_edit' => 'rev_minor_edit',
-                               'ar_rev_id'     => 'rev_id',
-                               'ar_text_id'    => 'rev_text_id',
-                               'ar_text'       => '\'\'', // Be explicit to appease
-                               'ar_flags'      => '\'\'', // MySQL's "strict mode"...
-                               'ar_len'        => 'rev_len',
-                               'ar_page_id'    => 'page_id',
-                               'ar_deleted'    => $bitfield
-                       ), array(
-                               'page_id' => $id,
-                               'page_id = rev_page'
-                       ), __METHOD__
+                               'Suppress' => $suppress !== false,
+                               'Reason' => $reason,
+                       ),
+                       $commit
                );
-
-               # Delete restrictions for it
-               $dbw->delete( 'page_restrictions', array ( 'pr_page' => $id ), __METHOD__ );
-
-               # Now that it's safely backed up, delete it
-               $dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__ );
-               $ok = ( $dbw->affectedRows() > 0 ); // getArticleId() uses slave, could be laggy
-
-               if ( !$ok ) {
-                       $dbw->rollback();
-                       return false;
-               }
-
-               # Fix category table counts
-               $cats = array();
-               $res = $dbw->select( 'categorylinks', 'cl_to', array( 'cl_from' => $id ), __METHOD__ );
-
-               foreach ( $res as $row ) {
-                       $cats [] = $row->cl_to;
-               }
-
-               $this->updateCategoryCounts( array(), $cats );
-
-               # If using cascading deletes, we can skip some explicit deletes
-               if ( !$dbw->cascadingDeletes() ) {
-                       $dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ );
-
-                       if ( $wgUseTrackbacks )
-                               $dbw->delete( 'trackbacks', array( 'tb_page' => $id ), __METHOD__ );
-
-                       # Delete outgoing links
-                       $dbw->delete( 'pagelinks', array( 'pl_from' => $id ) );
-                       $dbw->delete( 'imagelinks', array( 'il_from' => $id ) );
-                       $dbw->delete( 'categorylinks', array( 'cl_from' => $id ) );
-                       $dbw->delete( 'templatelinks', array( 'tl_from' => $id ) );
-                       $dbw->delete( 'externallinks', array( 'el_from' => $id ) );
-                       $dbw->delete( 'langlinks', array( 'll_from' => $id ) );
-                       $dbw->delete( 'redirect', array( 'rd_from' => $id ) );
-               }
-
-               # If using cleanup triggers, we can skip some manual deletes
-               if ( !$dbw->cleanupTriggers() ) {
-                       # Clean up recentchanges entries...
-                       $dbw->delete( 'recentchanges',
-                               array( 'rc_type != ' . RC_LOG,
-                                       'rc_namespace' => $this->mTitle->getNamespace(),
-                                       'rc_title' => $this->mTitle->getDBkey() ),
-                               __METHOD__ );
-                       $dbw->delete( 'recentchanges',
-                               array( 'rc_type != ' . RC_LOG, 'rc_cur_id' => $id ),
-                               __METHOD__ );
-               }
-
-               # Clear caches
-               Article::onArticleDelete( $this->mTitle );
-
-               # Clear the cached article id so the interface doesn't act like we exist
-               $this->mTitle->resetArticleID( 0 );
-
-               # Log the deletion, if the page was suppressed, log it at Oversight instead
-               $logtype = $suppress ? 'suppress' : 'delete';
-               $log = new LogPage( $logtype );
-
-               # Make sure logging got through
-               $log->addEntry( 'delete', $this->mTitle, $reason, array() );
-
-               if ( $commit ) {
-                       $dbw->commit();
-               }
-
-               wfRunHooks( 'ArticleDeleteComplete', array( &$this, &$wgUser, $reason, $id ) );
-               return true;
        }
 
        /**
index 9ba9d86..ccf3123 100644 (file)
@@ -14,6 +14,7 @@ global $wgAutoloadLocalClasses;
 
 $wgAutoloadLocalClasses = array(
        # Includes
+       'Action' => 'includes/Action.php',
        'AjaxDispatcher' => 'includes/AjaxDispatcher.php',
        'AjaxResponse' => 'includes/AjaxResponse.php',
        'AlphabeticPager' => 'includes/Pager.php',
@@ -51,7 +52,6 @@ $wgAutoloadLocalClasses = array(
        'ConfEditorToken' => 'includes/ConfEditor.php',
        'ConstantDependency' => 'includes/CacheDependency.php',
        'CreativeCommonsRdf' => 'includes/Metadata.php',
-       'Credits' => 'includes/Credits.php',
        'CSSJanus' => 'includes/libs/CSSJanus.php',
        'CSSMin' => 'includes/libs/CSSMin.php',
        'DependencyWrapper' => 'includes/CacheDependency.php',
@@ -274,6 +274,12 @@ $wgAutoloadLocalClasses = array(
        'ZhClient' => 'includes/ZhClient.php',
        'ZipDirectoryReader' => 'includes/ZipDirectoryReader.php',
 
+       # includes/actions
+       'CreditsAction' => 'includes/actions/CreditsAction.php',
+       'DeleteAction' => 'includes/actions/DeleteAction.php',
+       'UnwatchAction' => 'includes/actions/WatchAction.php',
+       'WatchAction' => 'includes/actions/WatchAction.php',
+
        # includes/api
        'ApiBase' => 'includes/api/ApiBase.php',
        'ApiBlock' => 'includes/api/ApiBlock.php',
diff --git a/includes/Credits.php b/includes/Credits.php
deleted file mode 100644 (file)
index e4c8be5..0000000
+++ /dev/null
@@ -1,238 +0,0 @@
-<?php
-/**
- * Formats credits for articles
- *
- * Copyright 2004, Evan Prodromou <evan@wikitravel.org>.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
- *
- * @file
- * @author <evan@wikitravel.org>
- */
-
-class Credits {
-       /**
-        * This is largely cadged from PageHistory::history
-        * @param $article Article object
-        */
-       public static function showPage( Article $article ) {
-               global $wgOut;
-
-               wfProfileIn( __METHOD__ );
-
-               $wgOut->setPageTitle( $article->mTitle->getPrefixedText() );
-               $wgOut->setSubtitle( wfMsg( 'creditspage' ) );
-               $wgOut->setArticleFlag( false );
-               $wgOut->setArticleRelated( true );
-               $wgOut->setRobotPolicy( 'noindex,nofollow' );
-
-               if ( $article->mTitle->getArticleID() == 0 ) {
-                       $s = wfMsg( 'nocredits' );
-               } else {
-                       $s = self::getCredits( $article, -1 );
-               }
-
-               $wgOut->addHTML( $s );
-
-               wfProfileOut( __METHOD__ );
-       }
-
-       /**
-        * Get a list of contributors of $article
-        * @param $article Article object
-        * @param $cnt Int: maximum list of contributors to show
-        * @param $showIfMax Bool: whether to contributors if there more than $cnt
-        * @return String: html
-        */
-       public static function getCredits( Article $article, $cnt, $showIfMax = true ) {
-               wfProfileIn( __METHOD__ );
-               $s = '';
-
-               if ( isset( $cnt ) && $cnt != 0 ) {
-                       $s = self::getAuthor( $article );
-                       if ( $cnt > 1 || $cnt < 0 ) {
-                               $s .= ' ' . self::getContributors( $article, $cnt - 1, $showIfMax );
-                       }
-               }
-
-               wfProfileOut( __METHOD__ );
-               return $s;
-       }
-
-       /**
-        * Get the last author with the last modification time
-        * @param $article Article object
-        */
-       protected static function getAuthor( Article $article ) {
-               global $wgLang;
-
-               $user = User::newFromId( $article->getUser() );
-
-               $timestamp = $article->getTimestamp();
-               if ( $timestamp ) {
-                       $d = $wgLang->date( $article->getTimestamp(), true );
-                       $t = $wgLang->time( $article->getTimestamp(), true );
-               } else {
-                       $d = '';
-                       $t = '';
-               }
-               return wfMsgExt( 'lastmodifiedatby', 'parsemag', $d, $t, self::userLink( $user ), $user->getName() );
-       }
-
-       /**
-        * Get a list of contributors of $article
-        * @param $article Article object
-        * @param $cnt Int: maximum list of contributors to show
-        * @param $showIfMax Bool: whether to contributors if there more than $cnt
-        * @return String: html
-        */
-       protected static function getContributors( Article $article, $cnt, $showIfMax ) {
-               global $wgLang, $wgHiddenPrefs;
-
-               $contributors = $article->getContributors();
-
-               $others_link = false;
-
-               # Hmm... too many to fit!
-               if ( $cnt > 0 && $contributors->count() > $cnt ) {
-                       $others_link = self::othersLink( $article );
-                       if ( !$showIfMax )
-                               return wfMsgExt( 'othercontribs', 'parsemag', $others_link, $contributors->count() );
-               }
-
-               $real_names = array();
-               $user_names = array();
-               $anon_ips = array();
-
-               # Sift for real versus user names
-               foreach ( $contributors as $user ) {
-                       $cnt--;
-                       if ( $user->isLoggedIn() ) {
-                               $link = self::link( $user );
-                               if ( !in_array( 'realname', $wgHiddenPrefs ) && $user->getRealName() ) {
-                                       $real_names[] = $link;
-                               } else {
-                                       $user_names[] = $link;
-                               }
-                       } else {
-                               $anon_ips[] = self::link( $user );
-                       }
-
-                       if ( $cnt == 0 ) {
-                               break;
-                       }
-               }
-
-               if ( count( $real_names ) ) {
-                       $real = $wgLang->listToText( $real_names );
-               } else {
-                       $real = false;
-               }
-
-               # "ThisSite user(s) A, B and C"
-               if ( count( $user_names ) ) {
-                       $user = wfMsgExt(
-                               'siteusers',
-                               'parsemag',
-                               $wgLang->listToText( $user_names ), count( $user_names )
-                       );
-               } else {
-                       $user = false;
-               }
-
-               if ( count( $anon_ips ) ) {
-                       $anon = wfMsgExt(
-                               'anonusers',
-                               'parsemag',
-                               $wgLang->listToText( $anon_ips ), count( $anon_ips )
-                       );
-               } else {
-                       $anon = false;
-               }
-
-               # This is the big list, all mooshed together. We sift for blank strings
-               $fulllist = array();
-               foreach ( array( $real, $user, $anon, $others_link ) as $s ) {
-                       if ( $s ) {
-                               array_push( $fulllist, $s );
-                       }
-               }
-
-               # Make the list into text...
-               $creds = $wgLang->listToText( $fulllist );
-
-               # "Based on work by ..."
-               return strlen( $creds )
-                       ? wfMsgExt( 'othercontribs', 'parsemag', $creds, count( $fulllist ) )
-                       : '';
-       }
-
-       /**
-        * Get a link to $user's user page
-        * @param $user User object
-        * @return String: html
-        */
-       protected static function link( User $user ) {
-               global $wgUser, $wgHiddenPrefs;
-               if ( !in_array( 'realname', $wgHiddenPrefs ) && !$user->isAnon() ) {
-                       $real = $user->getRealName();
-               } else {
-                       $real = false;
-               }
-
-               $skin = $wgUser->getSkin();
-               $page = $user->isAnon() ?
-                       SpecialPage::getTitleFor( 'Contributions', $user->getName() ) :
-                       $user->getUserPage();
-
-               return $skin->link( $page, htmlspecialchars( $real ? $real : $user->getName() ) );
-       }
-
-       /**
-        * Get a link to $user's user page
-        * @param $user User object
-        * @return String: html
-        */
-       protected static function userLink( User $user ) {
-               $link = self::link( $user );
-               if ( $user->isAnon() ) {
-                       return wfMsgExt( 'anonuser', array( 'parseinline', 'replaceafter' ), $link );
-               } else {
-                       global $wgHiddenPrefs;
-                       if ( !in_array( 'realname', $wgHiddenPrefs ) && $user->getRealName() ) {
-                               return $link;
-                       } else {
-                               return wfMsgExt( 'siteuser', 'parsemag', $link, $user->getName() );
-                       }
-               }
-       }
-
-       /**
-        * Get a link to action=credits of $article page
-        * @param $article Article object
-        * @return String: html
-        */
-       protected static function othersLink( Article $article ) {
-               global $wgUser;
-               $skin = $wgUser->getSkin();
-               return $skin->link(
-                       $article->getTitle(),
-                       wfMsgHtml( 'others' ),
-                       array(),
-                       array( 'action' => 'credits' ),
-                       array( 'known' )
-               );
-       }
-}
index ba72d76..e447983 100644 (file)
@@ -27,12 +27,10 @@ if( !defined( 'MEDIAWIKI' ) ) {
 }
 
 # Create a site configuration object. Not used for much in a default install
-if ( !defined( 'MW_PHP4' ) ) {
-       if ( !defined( 'MW_COMPILED' ) ) {
-               require_once( "$IP/includes/SiteConfiguration.php" );
-       }
-       $wgConf = new SiteConfiguration;
+if ( !defined( 'MW_COMPILED' ) ) {
+       require_once( "$IP/includes/SiteConfiguration.php" );
 }
+$wgConf = new SiteConfiguration;
 /** @endcond */
 
 /** MediaWiki version number */
@@ -5023,6 +5021,38 @@ $wgMaxRedirectLinksRetrieved = 500;
 
 /** @} */ # end special pages }
 
+/*************************************************************************//**
+ * @name   Actions
+ * @{
+ */
+
+/**
+ * Array of allowed values for the title=foo&action=<action> parameter. Syntax is:
+ *     'foo' => 'ClassName'    Load the specified class which subclasses Action
+ *     'foo' => true           Load the class FooAction which subclasses Action
+ *     'foo' => false          The action is disabled; show an error message
+ * Unsetting core actions will probably cause things to complain loudly.
+ */
+$wgActions = array(
+       'credits' => true,
+       'delete' => true,
+       'unwatch' => true,
+       'watch' => true,
+);
+
+/**
+ * Array of disabled article actions, e.g. view, edit, dublincore, delete, etc.
+ * @deprecated since 1.18; just set $wgActions['action'] = false instead
+ */
+$wgDisabledActions = array();
+
+/**
+ * Allow the "info" action, very inefficient at the moment
+ */
+$wgAllowPageInfo = false;
+
+/** @} */ # end actions }
+
 /*************************************************************************//**
  * @name   Robot (search engine crawler) policy
  * See also $wgNoFollowLinks.
@@ -5288,17 +5318,9 @@ $wgUpdateRowsPerQuery = 100;
  * @{
  */
 
-/** Allow the "info" action, very inefficient at the moment */
-$wgAllowPageInfo = false;
-
 /** Name of the external diff engine to use */
 $wgExternalDiffEngine = false;
 
-/**
- * Array of disabled article actions, e.g. view, edit, dublincore, delete, etc.
- */
-$wgDisabledActions = array();
-
 /**
  * Disable redirects to special pages and interwiki redirects, which use a 302
  * and have no "redirected from" link. Note this is only for articles with #Redirect
index cafb641..fb6f2bc 100644 (file)
@@ -1153,9 +1153,9 @@ class EditPage {
                        $dbw = wfGetDB( DB_MASTER );
                        $dbw->begin();
                        if ( $this->watchthis ) {
-                               $this->mArticle->doWatch();
+                               Action::factory( 'watch', $this->mArticle )->execute();
                        } else {
-                               $this->mArticle->doUnwatch();
+                               Action::factory( 'watch', $this->mArticle )->execute();
                        }
                        $dbw->commit();
                }
index f77d697..9101d6e 100644 (file)
@@ -125,9 +125,9 @@ class FileDeleteForm {
                                if( $article->doDeleteArticle( $reason, $suppress, $id, false ) ) {
                                        global $wgRequest;
                                        if( $wgRequest->getCheck( 'wpWatch' ) && $wgUser->isLoggedIn() ) {
-                                               $article->doWatch();
+                                               Action::factory( 'watch', $article )->execute();
                                        } elseif( $title->userIsWatching() ) {
-                                               $article->doUnwatch();
+                                               Action::factory( 'unwatch', $article )->execute();
                                        }
                                        $status = $file->delete( $reason, $suppress );
                                        if( $status->ok ) {
index 86aca3e..6f04e97 100644 (file)
@@ -317,9 +317,9 @@ class ProtectionForm {
                }
 
                if( $wgRequest->getCheck( 'mwProtectWatch' ) && $wgUser->isLoggedIn() ) {
-                       $this->mArticle->doWatch();
+                       Action::factory( 'watch', $this->mArticle )->execute();
                } elseif( $this->mTitle->userIsWatching() ) {
-                       $this->mArticle->doUnwatch();
+                       Action::factory( 'unwatch', $this->mArticle )->execute();
                }
                return $ok;
        }
index 837740e..a7bf629 100644 (file)
@@ -270,6 +270,14 @@ if ( !$wgEnotifMinorEdits ) {
        $wgHiddenPrefs[] = 'enotifminoredits';
 }
 
+# $wgDisabledActions is deprecated as of 1.18
+foreach( $wgDisabledActions as $action ){
+       $wgActions[$action] = false;
+}
+if( !$wgAllowPageInfo ){
+       $wgActions['info'] = false;
+}
+
 if ( !$wgHtml5Version && $wgHtml5 && $wgAllowRdfaAttributes ) {
        # see http://www.w3.org/TR/rdfa-in-html/#document-conformance
        if ( $wgMimeType == 'application/xhtml+xml' ) {
index f54e2b3..a095a36 100644 (file)
@@ -471,9 +471,16 @@ class MediaWiki {
                        return;
                }
 
-               $action = $this->getAction();
+               $act = $this->getAction();
 
-               switch( $action ) {
+               $action = Action::factory( $this->getAction(), $article );
+               if( $action instanceof Action ){
+                       $action->show();
+                       wfProfileOut( __METHOD__ );
+                       return;
+               }
+
+               switch( $act ) {
                        case 'view':
                                $this->context->output->setSquidMaxage( $this->getVal( 'SquidMaxage' ) );
                                $article->view();
@@ -484,9 +491,6 @@ class MediaWiki {
                                $raw->view();
                                wfProfileOut( __METHOD__ . '-raw' );
                                break;
-                       case 'watch':
-                       case 'unwatch':
-                       case 'delete':
                        case 'revert':
                        case 'rollback':
                        case 'protect':
@@ -496,7 +500,7 @@ class MediaWiki {
                        case 'render':
                        case 'deletetrackback':
                        case 'purge':
-                               $article->$action();
+                               $article->$act();
                                break;
                        case 'print':
                                $article->view();
@@ -517,9 +521,6 @@ class MediaWiki {
                                        $rdf->show();
                                }
                                break;
-                       case 'credits':
-                               Credits::showPage( $article );
-                               break;
                        case 'submit':
                                if ( session_id() == '' ) {
                                        // Send a cookie so anons get talk message notifications
@@ -532,7 +533,7 @@ class MediaWiki {
                                        $external = $this->context->request->getVal( 'externaledit' );
                                        $section = $this->context->request->getVal( 'section' );
                                        $oldid = $this->context->request->getVal( 'oldid' );
-                                       if ( !$this->getVal( 'UseExternalEditor' ) || $action == 'submit' || $internal ||
+                                       if ( !$this->getVal( 'UseExternalEditor' ) || $act == 'submit' || $internal ||
                                           $section || $oldid || ( !$this->context->user->getOption( 'externaleditor' ) && !$external ) ) {
                                                $editor = new EditPage( $article );
                                                $editor->submit();
@@ -561,7 +562,7 @@ class MediaWiki {
                                $special->execute( '' );
                                break;
                        default:
-                               if ( wfRunHooks( 'UnknownAction', array( $action, $article ) ) ) {
+                               if ( wfRunHooks( 'UnknownAction', array( $act, $article ) ) ) {
                                        $this->context->output->showErrorPage( 'nosuchaction', 'nosuchactiontext' );
                                }
                }
diff --git a/includes/actions/CreditsAction.php b/includes/actions/CreditsAction.php
new file mode 100644 (file)
index 0000000..576834f
--- /dev/null
@@ -0,0 +1,237 @@
+<?php
+/**
+ * Formats credits for articles
+ *
+ * Copyright 2004, Evan Prodromou <evan@wikitravel.org>.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * @file
+ * @ingroup Actions
+ * @author <evan@wikitravel.org>
+ */
+
+class CreditsAction extends FormlessAction {
+
+       public function getName(){
+               return 'credits';
+       }
+
+       public function getRestriction(){
+               return null;
+       }
+
+       /**
+        * This is largely cadged from PageHistory::history
+        */
+       public function onView() {
+               wfProfileIn( __METHOD__ );
+
+               if ( $this->page->getID() == 0 ) {
+                       $s = wfMsg( 'nocredits' );
+               } else {
+                       $s = $this->getCredits( -1 );
+               }
+
+               wfProfileOut( __METHOD__ );
+
+               return $s;
+       }
+
+       /**
+        * Get a list of contributors of $article
+        * @param $article Article object
+        * @param $cnt Int: maximum list of contributors to show
+        * @param $showIfMax Bool: whether to contributors if there more than $cnt
+        * @return String: html
+        */
+       protected function getCredits( $cnt, $showIfMax = true ) {
+               wfProfileIn( __METHOD__ );
+               $s = '';
+
+               if ( isset( $cnt ) && $cnt != 0 ) {
+                       $s = self::getAuthor( $this->page );
+                       if ( $cnt > 1 || $cnt < 0 ) {
+                               $s .= ' ' . $this->getContributors( $cnt - 1, $showIfMax );
+                       }
+               }
+
+               wfProfileOut( __METHOD__ );
+               return $s;
+       }
+
+       /**
+        * Get the last author with the last modification time
+        * @param $article Article object
+        */
+       protected static function getAuthor( Article $article ) {
+               global $wgLang;
+
+               $user = User::newFromId( $article->getUser() );
+
+               $timestamp = $article->getTimestamp();
+               if ( $timestamp ) {
+                       $d = $wgLang->date( $article->getTimestamp(), true );
+                       $t = $wgLang->time( $article->getTimestamp(), true );
+               } else {
+                       $d = '';
+                       $t = '';
+               }
+               return wfMsgExt( 'lastmodifiedatby', 'parsemag', $d, $t, self::userLink( $user ), $user->getName() );
+       }
+
+       /**
+        * Get a list of contributors of $article
+        * @param $article Article object
+        * @param $cnt Int: maximum list of contributors to show
+        * @param $showIfMax Bool: whether to contributors if there more than $cnt
+        * @return String: html
+        */
+       protected function getContributors( $cnt, $showIfMax ) {
+               global $wgLang, $wgHiddenPrefs;
+
+               $contributors = $this->page->getContributors();
+
+               $others_link = false;
+
+               # Hmm... too many to fit!
+               if ( $cnt > 0 && $contributors->count() > $cnt ) {
+                       $others_link = $this->othersLink();
+                       if ( !$showIfMax )
+                               return wfMsgExt( 'othercontribs', 'parsemag', $others_link, $contributors->count() );
+               }
+
+               $real_names = array();
+               $user_names = array();
+               $anon_ips = array();
+
+               # Sift for real versus user names
+               foreach ( $contributors as $user ) {
+                       $cnt--;
+                       if ( $user->isLoggedIn() ) {
+                               $link = self::link( $user );
+                               if ( !in_array( 'realname', $wgHiddenPrefs ) && $user->getRealName() ) {
+                                       $real_names[] = $link;
+                               } else {
+                                       $user_names[] = $link;
+                               }
+                       } else {
+                               $anon_ips[] = self::link( $user );
+                       }
+
+                       if ( $cnt == 0 ) {
+                               break;
+                       }
+               }
+
+               if ( count( $real_names ) ) {
+                       $real = $wgLang->listToText( $real_names );
+               } else {
+                       $real = false;
+               }
+
+               # "ThisSite user(s) A, B and C"
+               if ( count( $user_names ) ) {
+                       $user = wfMsgExt(
+                               'siteusers',
+                               'parsemag',
+                               $wgLang->listToText( $user_names ), count( $user_names )
+                       );
+               } else {
+                       $user = false;
+               }
+
+               if ( count( $anon_ips ) ) {
+                       $anon = wfMsgExt(
+                               'anonusers',
+                               'parsemag',
+                               $wgLang->listToText( $anon_ips ), count( $anon_ips )
+                       );
+               } else {
+                       $anon = false;
+               }
+
+               # This is the big list, all mooshed together. We sift for blank strings
+               $fulllist = array();
+               foreach ( array( $real, $user, $anon, $others_link ) as $s ) {
+                       if ( $s ) {
+                               array_push( $fulllist, $s );
+                       }
+               }
+
+               # Make the list into text...
+               $creds = $wgLang->listToText( $fulllist );
+
+               # "Based on work by ..."
+               return strlen( $creds )
+                       ? wfMsgExt( 'othercontribs', 'parsemag', $creds, count( $fulllist ) )
+                       : '';
+       }
+
+       /**
+        * Get a link to $user's user page
+        * @param $user User object
+        * @return String: html
+        */
+       protected static function link( User $user ) {
+               global $wgUser, $wgHiddenPrefs;
+               if ( !in_array( 'realname', $wgHiddenPrefs ) && !$user->isAnon() ) {
+                       $real = $user->getRealName();
+               } else {
+                       $real = false;
+               }
+
+               $page = $user->isAnon()
+                       ? SpecialPage::getTitleFor( 'Contributions', $user->getName() )
+                       : $user->getUserPage();
+
+               return Linker::link( $page, htmlspecialchars( $real ? $real : $user->getName() ) );
+       }
+
+       /**
+        * Get a link to $user's user page
+        * @param $user User object
+        * @return String: html
+        */
+       protected static function userLink( User $user ) {
+               $link = self::link( $user );
+               if ( $user->isAnon() ) {
+                       return wfMsgExt( 'anonuser', array( 'parseinline', 'replaceafter' ), $link );
+               } else {
+                       global $wgHiddenPrefs;
+                       if ( !in_array( 'realname', $wgHiddenPrefs ) && $user->getRealName() ) {
+                               return $link;
+                       } else {
+                               return wfMsgExt( 'siteuser', 'parsemag', $link, $user->getName() );
+                       }
+               }
+       }
+
+       /**
+        * Get a link to action=credits of $article page
+        * @param $article Article object
+        * @return String: html
+        */
+       protected function othersLink() {
+               global $wgUser;
+               return Linker::link(
+                       $this->getTitle(),
+                       wfMsgHtml( 'others' ),
+                       array(),
+                       array( 'action' => 'credits' ),
+                       array( 'known' )
+               );
+       }
+}
diff --git a/includes/actions/DeleteAction.php b/includes/actions/DeleteAction.php
new file mode 100644 (file)
index 0000000..3f8097f
--- /dev/null
@@ -0,0 +1,476 @@
+<?php
+/**
+ * Performs the watch and unwatch actions on a page
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * @file
+ * @ingroup Actions
+ */
+
+class DeleteAction extends Action {
+
+       public function getName(){
+               return 'delete';
+       }
+
+       public function getRestriction(){
+               return 'delete';
+       }
+
+       protected function getDescription(){
+               return wfMsg( 'delete-confirm', $this->getTitle()->getPrefixedText() );
+       }
+
+       /**
+        * Check that the deletion can be executed.  In addition to checking the user permissions,
+        * check that the page is not too big and has not already been deleted.
+        * @throws ErrorPageError
+        * @see Action::checkCanExecute
+        */
+       protected function checkCanExecute( User $user ){
+
+               // Check that the article hasn't already been deleted
+               $dbw = wfGetDB( DB_MASTER );
+               $conds = $this->getTitle()->pageCond();
+               $latest = $dbw->selectField( 'page', 'page_latest', $conds, __METHOD__ );
+               if ( $latest === false ) {
+                       // Get the deletion log
+                       $log = '';
+                       LogEventsList::showLogExtract(
+                               $log,
+                               'delete',
+                               $this->getTitle()->getPrefixedText()
+                       );
+
+                       $msg = new Message( 'cannotdelete' );
+                       $msg->params( $this->getTitle()->getPrefixedText() ); // This parameter is parsed
+                       $msg->rawParams( $log ); // This is not
+
+                       throw new ErrorPageError( 'internalerror', $msg );
+               }
+
+               // Limit deletions of big pages
+               $bigHistory = $this->isBigDeletion();
+               if ( $bigHistory && !$user->isAllowed( 'bigdelete' ) ) {
+                       global $wgDeleteRevisionsLimit;
+                       throw new ErrorPageError(
+                               'internalerror',
+                               'delete-toobig',
+                               $this->getContext()->lang->formatNum( $wgDeleteRevisionsLimit )
+                       );
+               }
+
+               return parent::checkCanExecute( $user );
+       }
+
+       protected function getFormFields(){
+               // TODO: add more useful things here?
+               $infoText = Html::rawElement(
+                       'strong',
+                       array(),
+                       Linker::link( $this->getTitle(), $this->getTitle()->getText() )
+               );
+
+               $arr = array(
+                       'Page' => array(
+                               'type' => 'info',
+                               'raw' => true,
+                               'default' => $infoText,
+                       ),
+                       'Reason' => array(
+                               'type' => 'selectandother',
+                               'label-message' => 'deletecomment',
+                               'options-message' => 'deletereason-dropdown',
+                               'size' => '60',
+                               'maxlength' => '255',
+                               'default' => self::getAutoReason( $this->page),
+                       ),
+               );
+
+               if( $this->getUser()->isLoggedIn() ){
+                       $arr['Watch'] = array(
+                               'type' => 'check',
+                               'label-message' => 'watchthis',
+                               'default' => $this->getUser()->getBoolOption( 'watchdeletion' ) || $this->getTitle()->userIsWatching()
+                       );
+               }
+
+               if( $this->getUser()->isAllowed( 'suppressrevision' ) ){
+                       $arr['Suppress'] = array(
+                               'type' => 'check',
+                               'label-message' => 'revdelete-suppress',
+                               'default' => false,
+                       );
+               }
+
+               return $arr;
+       }
+
+       /**
+        * Text to go at the top of the form, before the opening fieldset
+        * @see Action::preText()
+        * @return String
+        */
+       protected function preText() {
+
+               // If the page has a history, insert a warning
+               if ( $this->page->estimateRevisionCount() ) {
+                       global $wgLang;
+
+                       $link = Linker::link(
+                               $this->getTitle(),
+                               wfMsgHtml( 'history' ),
+                               array( 'rel' => 'archives' ),
+                               array( 'action' => 'history' )
+                       );
+
+                       return Html::rawElement(
+                               'strong',
+                               array( 'class' => 'mw-delete-warning-revisions' ),
+                               wfMessage(
+                                       'historywarning',
+                                       $wgLang->formatNum( $this->page->estimateRevisionCount() )
+                               )->rawParams( $link )->parse()
+                       );
+               }
+       }
+
+       /**
+        * Text to go at the bottom of the form, below the closing fieldset
+        * @see Action::postText()
+        * @return string
+        */
+       protected function postText(){
+               $s = '';
+               LogEventsList::showLogExtract(
+                       $s,
+                       'delete',
+                       $this->getTitle()->getPrefixedText()
+               );
+               return Html::element( 'h2', array(), LogPage::logName( 'delete' ) ) . $s;
+       }
+
+       protected function alterForm( HTMLForm &$form ){
+               $form->setWrapperLegend( wfMsgExt( 'delete-legend', array( 'parsemag', 'escapenoentities' ) ) );
+
+               if ( $this->getUser()->isAllowed( 'editinterface' ) ) {
+                       $link = Linker::link(
+                               Title::makeTitle( NS_MEDIAWIKI, 'Deletereason-dropdown' ),
+                               wfMsgHtml( 'delete-edit-reasonlist' ),
+                               array(),
+                               array( 'action' => 'edit' )
+                       );
+                       $form->addHeaderText( '<p class="mw-delete-editreasons">' . $link . '</p>' );
+               }
+       }
+
+       /**
+        * Function called on form submission.  Privilege checks and validation have already been
+        * completed by this point; we just need to jump out to the heavy-lifting function,
+        * which is implemented as a static method so it can be called from other places
+        * TODO: make those other places call $action->execute() properly
+        * @see Action::onSubmit()
+        * @param  $data Array
+        * @return Array|Bool
+        */
+       public function onSubmit( $data ){
+               $status = self::doDeleteArticle( $this->page, $this->getContext(), $data, true );
+               return $status;
+       }
+
+       public function onSuccess(){
+               // Watch or unwatch, if requested
+               if( $this->getRequest()->getCheck( 'wpWatch' ) && $this->getUser()->isLoggedIn() ) {
+                       Action::factory( 'watch', $this->page )->execute();
+               } elseif ( $this->getTitle()->userIsWatching() ) {
+                       Action::factory( 'unwatch', $this->page )->execute();
+               }
+
+               $this->getOutput()->setPagetitle( wfMsg( 'actioncomplete' ) );
+               $this->getOutput()->addWikiMsg(
+                       'deletedtext',
+                       $this->getTitle()->getPrefixedText(),
+                       '[[Special:Log/delete|' . wfMsgNoTrans( 'deletionlog' ) . ']]'
+               );
+               $this->getOutput()->returnToMain( false );
+       }
+
+       /**
+        * @return bool whether or not the page surpasses $wgDeleteRevisionsLimit revisions
+        */
+       protected function isBigDeletion() {
+               global $wgDeleteRevisionsLimit;
+               return $wgDeleteRevisionsLimit && $this->page->estimateRevisionCount() > $wgDeleteRevisionsLimit;
+       }
+
+       /**
+        * Back-end article deletion
+        * Deletes the article with database consistency, writes logs, purges caches
+        *
+        * @param $commit boolean defaults to true, triggers transaction end
+        * @return Bool|Array true if successful, error array on failure
+        */
+       public static function doDeleteArticle( Article $page, RequestContext $context, array $data, $commit = true ) {
+               global $wgDeferredUpdateList, $wgUseTrackbacks;
+
+               wfDebug( __METHOD__ . "\n" );
+
+               // The normal syntax from HTMLSelectAndOtherField is for the reason to be in the form
+               // 'Reason' => array( <full reason>, <dropdown>, <custom> ), but it's reasonable for other
+               // functions to just pass 'Reason' => <reason>
+               $data['Reason'] = (array)$data['Reason'];
+
+               $error = null;
+               if ( !wfRunHooks( 'ArticleDelete', array( &$page, &$context->user, &$data['Reason'][0], &$error ) ) ) {
+                       return $error;
+               }
+
+               $title = $page->getTitle();
+               $id = $page->getID( Title::GAID_FOR_UPDATE );
+
+               if ( $title->getDBkey() === '' || $id == 0 ) {
+                       return false;
+               }
+
+               $updates = new SiteStatsUpdate( 0, 1, - (int)$page->isCountable( $page->getRawText() ), -1 );
+               array_push( $wgDeferredUpdateList, $updates );
+
+               // Bitfields to further suppress the content
+               if ( isset( $data['Suppress'] ) && $data['Suppress'] ) {
+                       $bitfield = 0;
+                       // This should be 15...
+                       $bitfield |= Revision::DELETED_TEXT;
+                       $bitfield |= Revision::DELETED_COMMENT;
+                       $bitfield |= Revision::DELETED_USER;
+                       $bitfield |= Revision::DELETED_RESTRICTED;
+
+                       $logtype = 'suppress';
+               } else {
+                       // Otherwise, leave it unchanged
+                       $bitfield = 'rev_deleted';
+                       $logtype = 'delete';
+               }
+
+               $dbw = wfGetDB( DB_MASTER );
+               $dbw->begin();
+               // For now, shunt the revision data into the archive table.
+               // Text is *not* removed from the text table; bulk storage
+               // is left intact to avoid breaking block-compression or
+               // immutable storage schemes.
+               //
+               // For backwards compatibility, note that some older archive
+               // table entries will have ar_text and ar_flags fields still.
+               //
+               // In the future, we may keep revisions and mark them with
+               // the rev_deleted field, which is reserved for this purpose.
+               $dbw->insertSelect(
+                       'archive',
+                       array( 'page', 'revision' ),
+                       array(
+                               'ar_namespace'  => 'page_namespace',
+                               'ar_title'      => 'page_title',
+                               'ar_comment'    => 'rev_comment',
+                               'ar_user'       => 'rev_user',
+                               'ar_user_text'  => 'rev_user_text',
+                               'ar_timestamp'  => 'rev_timestamp',
+                               'ar_minor_edit' => 'rev_minor_edit',
+                               'ar_rev_id'     => 'rev_id',
+                               'ar_text_id'    => 'rev_text_id',
+                               'ar_text'       => "''", // Be explicit to appease
+                               'ar_flags'      => "''", // MySQL's "strict mode"...
+                               'ar_len'        => 'rev_len',
+                               'ar_page_id'    => 'page_id',
+                               'ar_deleted'    => $bitfield
+                       ),
+                       array(
+                               'page_id' => $id,
+                               'page_id = rev_page'
+                       ),
+                       __METHOD__
+               );
+
+               // Delete restrictions for it
+               $dbw->delete( 'page_restrictions', array ( 'pr_page' => $id ), __METHOD__ );
+
+               // Now that it's safely backed up, delete it
+               $dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__ );
+
+               // getArticleId() uses slave, could be laggy
+               if ( $dbw->affectedRows() == 0 ) {
+                       $dbw->rollback();
+                       return false;
+               }
+
+               // Fix category table counts
+               $res = $dbw->select( 'categorylinks', 'cl_to', array( 'cl_from' => $id ), __METHOD__ );
+               $cats = array();
+               foreach ( $res as $row ) {
+                       $cats[] = $row->cl_to;
+               }
+               $page->updateCategoryCounts( array(), $cats );
+
+               // If using cascading deletes, we can skip some explicit deletes
+               if ( !$dbw->cascadingDeletes() ) {
+                       $dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ );
+
+                       if ( $wgUseTrackbacks ){
+                               $dbw->delete( 'trackbacks', array( 'tb_page' => $id ), __METHOD__ );
+                       }
+
+                       // Delete outgoing links
+                       $dbw->delete( 'pagelinks', array( 'pl_from' => $id ) );
+                       $dbw->delete( 'imagelinks', array( 'il_from' => $id ) );
+                       $dbw->delete( 'categorylinks', array( 'cl_from' => $id ) );
+                       $dbw->delete( 'templatelinks', array( 'tl_from' => $id ) );
+                       $dbw->delete( 'externallinks', array( 'el_from' => $id ) );
+                       $dbw->delete( 'langlinks', array( 'll_from' => $id ) );
+                       $dbw->delete( 'redirect', array( 'rd_from' => $id ) );
+               }
+
+               // If using cleanup triggers, we can skip some manual deletes
+               if ( !$dbw->cleanupTriggers() ) {
+                       // Clean up recentchanges entries...
+                       $dbw->delete( 'recentchanges',
+                               array(
+                                       'rc_type != ' . RC_LOG,
+                                       'rc_namespace' => $title->getNamespace(),
+                                       'rc_title' => $title->getDBkey() ),
+                               __METHOD__
+                       );
+                       $dbw->delete(
+                               'recentchanges',
+                               array( 'rc_type != ' . RC_LOG, 'rc_cur_id' => $id ),
+                               __METHOD__
+                       );
+               }
+
+               // Clear caches
+               // TODO: should this be in here or left in Article?
+               Article::onArticleDelete( $title );
+
+               // Clear the cached article id so the interface doesn't act like we exist
+               $title->resetArticleID( 0 );
+
+               // Log the deletion, if the page was suppressed, log it at Oversight instead
+               $log = new LogPage( $logtype );
+
+               // Make sure logging got through
+               $log->addEntry( 'delete', $title, $data['Reason'][0], array() );
+
+               if ( $commit ) {
+                       $dbw->commit();
+               }
+
+               wfRunHooks( 'ArticleDeleteComplete', array( &$page, &$context->user, $data['Reason'][0], $id ) );
+               return true;
+       }
+
+       /**
+        * Auto-generates a deletion reason.  Also sets $this->hasHistory if the page has old
+        * revisions.
+        *
+        * @return mixed String containing default reason or empty string, or boolean false
+        *    if no revision was found
+        */
+       public static function getAutoReason( Article $page ) {
+               global $wgContLang;
+
+               $dbw = wfGetDB( DB_MASTER );
+               // Get the last revision
+               $rev = Revision::newFromTitle( $page->getTitle() );
+
+               if ( is_null( $rev ) ) {
+                       return false;
+               }
+
+               // Get the article's contents
+               $contents = $rev->getText();
+               $blank = false;
+
+               // If the page is blank, use the text from the previous revision,
+               // which can only be blank if there's a move/import/protect dummy revision involved
+               if ( $contents == '' ) {
+                       $prev = $rev->getPrevious();
+
+                       if ( $prev )    {
+                               $contents = $prev->getText();
+                               $blank = true;
+                       }
+               }
+
+               // Find out if there was only one contributor
+               // Only scan the last 20 revisions
+               $res = $dbw->select( 'revision', 'rev_user_text',
+                       array(
+                               'rev_page' => $page->getID(),
+                               $dbw->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0'
+                       ),
+                       __METHOD__,
+                       array( 'LIMIT' => 20 )
+               );
+
+               if ( $res === false ) {
+                       // This page has no revisions, which is very weird
+                       return false;
+               }
+
+               $row = $dbw->fetchObject( $res );
+
+               if ( $row ) { // $row is false if the only contributor is hidden
+                       $onlyAuthor = $row->rev_user_text;
+                       // Try to find a second contributor
+                       foreach ( $res as $row ) {
+                               if ( $row->rev_user_text != $onlyAuthor ) { // Bug 22999
+                                       $onlyAuthor = false;
+                                       break;
+                               }
+                       }
+               } else {
+                       $onlyAuthor = false;
+               }
+
+               // Generate the summary with a '$1' placeholder
+               if ( $blank ) {
+                       // The current revision is blank and the one before is also
+                       // blank. It's just not our lucky day
+                       $reason = wfMessage( 'exbeforeblank', '$1' )->inContentLanguage()->text();
+               } else {
+                       if ( $onlyAuthor ) {
+                               $reason = wfMessage( 'excontentauthor', '$1', $onlyAuthor )->inContentLanguage()->text();
+                       } else {
+                               $reason = wfMessage( 'excontent', '$1' )->inContentLanguage()->text();
+                       }
+               }
+
+               if ( $reason == '-' ) {
+                       // Allow these UI messages to be blanked out cleanly
+                       return '';
+               }
+
+               // Replace newlines with spaces to prevent uglyness
+               $contents = preg_replace( "/[\n\r]/", ' ', $contents );
+               // Calculate the maximum number of chars to get
+               // Max content length = max comment length - length of the comment (excl. $1)
+               $maxLength = 255 - ( strlen( $reason ) - 2 );
+               $contents = $wgContLang->truncate( $contents, $maxLength );
+               // Remove possible unfinished links
+               $contents = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $contents );
+               // Now replace the '$1' placeholder
+               $reason = str_replace( '$1', $contents, $reason );
+
+               return $reason;
+       }
+}
diff --git a/includes/actions/WatchAction.php b/includes/actions/WatchAction.php
new file mode 100644 (file)
index 0000000..e3fb31b
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+/**
+ * Performs the watch and unwatch actions on a page
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * @file
+ * @ingroup Actions
+ */
+
+class WatchAction extends FormlessAction {
+
+       public function getName(){
+               return 'watch';
+       }
+
+       public function getRestriction(){
+               return 'read';
+       }
+
+       protected function getDescription(){
+               return wfMsg( 'addedwatch' );
+       }
+
+       protected function checkCanExecute( User $user ){
+               if ( $user->isAnon() ) {
+                       throw new ErrorPageError( 'watchnologin', 'watchnologintext' );
+               }
+               return parent::checkCanExecute( $user );
+       }
+
+       public function onView() {
+               wfProfileIn( __METHOD__ );
+
+               $user = $this->getUser();
+               if ( wfRunHooks( 'WatchArticle', array( &$user, &$this->page ) ) ) {
+                       $this->getUser()->addWatch( $this->getTitle() );
+                       wfRunHooks( 'WatchArticleComplete', array( &$user, &$this->page ) );
+               }
+
+               wfProfileOut( __METHOD__ );
+
+               return wfMessage( 'addedwatchtext', $this->getTitle()->getPrefixedText() )->parse();
+       }
+}
+
+class UnwatchAction extends WatchAction {
+
+       public function getName(){
+               return 'unwatch';
+       }
+
+       protected function getDescription(){
+               return wfMsg( 'removedwatch' );
+       }
+
+       public function onView() {
+               wfProfileIn( __METHOD__ );
+
+               $user = $this->getUser();
+               if ( wfRunHooks( 'UnwatchArticle', array( &$user, &$this->page ) ) ) {
+                       $this->getUser()->removeWatch( $this->getTitle() );
+                       wfRunHooks( 'UnwatchArticleComplete', array( &$user, &$this->page ) );
+               }
+
+               wfProfileOut( __METHOD__ );
+
+               return wfMessage( 'removedwatchtext', $this->getTitle()->getPrefixedText() )->parse();
+       }
+}
index 336bc79..fe6ce54 100644 (file)
@@ -645,9 +645,9 @@ abstract class ApiBase {
 
                $articleObj = new Article( $titleObj );
                if ( $value ) {
-                       $articleObj->doWatch();
+                       Action::factory( 'watch', $articleObj )->execute();
                } else {
-                       $articleObj->doUnwatch();
+                       Action::factory( 'unwatch', $articleObj )->execute();
                }
        }
 
index 0eba219..3a61ccd 100644 (file)
@@ -123,11 +123,6 @@ class ApiDelete extends ApiBase {
         * @return Title::getUserPermissionsErrors()-like array
         */
        public static function delete( &$article, $token, &$reason = null ) {
-               global $wgUser;
-               if ( $article->isBigDeletion() && !$wgUser->isAllowed( 'bigdelete' ) ) {
-                       global $wgDeleteRevisionsLimit;
-                       return array( array( 'delete-toobig', $wgDeleteRevisionsLimit ) );
-               }
                $title = $article->getTitle();
                $errors = self::getPermissionsError( $title, $token );
                if ( count( $errors ) ) {
@@ -136,21 +131,28 @@ class ApiDelete extends ApiBase {
 
                // Auto-generate a summary, if necessary
                if ( is_null( $reason ) ) {
-                       // Need to pass a throwaway variable because generateReason expects
-                       // a reference
-                       $hasHistory = false;
-                       $reason = $article->generateReason( $hasHistory );
+                       $reason = DeleteAction::getAutoReason( $article );
                        if ( $reason === false ) {
                                return array( array( 'cannotdelete' ) );
                        }
                }
 
-               $error = '';
-               // Luckily, Article.php provides a reusable delete function that does the hard work for us
-               if ( $article->doDeleteArticle( $reason, false, 0, true, $error ) ) {
-                       return array();
-               } else {
-                       return array( array( 'cannotdelete', $article->mTitle->getPrefixedText() ) );
+               $action = Action::factory( 'delete', $article );
+               $data = array(
+                       'Reason' => $reason,
+                       'Suppress' => false, // The thought of people doing this through the API is scary...
+               );
+
+               try {
+                       $action->execute( $data, false );
+               }
+               catch ( ErrorPageError $e ){
+                       if( $e->msg == 'delete-toobig' ){
+                               global $wgDeleteRevisionsLimit;
+                               return array( array( 'delete-toobig', $wgDeleteRevisionsLimit ) );
+                       } else {
+                               array( array( 'cannotdelete', $article->mTitle->getPrefixedText() ) );
+                       }
                }
        }
 
index fe52328..685306d 100644 (file)
@@ -59,11 +59,11 @@ class ApiWatch extends ApiBase {
                if ( $params['unwatch'] ) {
                        $res['unwatched'] = '';
                        $res['message'] = wfMsgExt( 'removedwatchtext', array( 'parse' ), $title->getPrefixedText() );
-                       $success = $article->doUnwatch();
+                       $success = Action::factory( 'unwatch', $article )->execute();
                } else {
                        $res['watched'] = '';
                        $res['message'] = wfMsgExt( 'addedwatchtext', array( 'parse' ), $title->getPrefixedText() );
-                       $success = $article->doWatch();
+                       $success = Action::factory( 'watch', $article )->execute();
                }
                if ( !$success ) {
                        $this->dieUsageMsg( array( 'hookaborted' ) );
index 2be33d0..f6259c2 100644 (file)
@@ -359,8 +359,11 @@ class MovePageForm extends UnlistedSpecialPage {
                        $article = new Article( $nt );
 
                        # Disallow deletions of big articles
-                       $bigHistory = $article->isBigDeletion();
-                       if( $bigHistory && !$nt->userCan( 'bigdelete' ) ) {
+                       global $wgDeleteRevisionsLimit;
+                       if ( $wgDeleteRevisionsLimit
+                                       && $this->estimateRevisionCount() > $wgDeleteRevisionsLimit
+                                       && !$nt->userCan( 'bigdelete' ) )
+                       {
                                global $wgDeleteRevisionsLimit;
                                $this->showForm( array('delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ) );
                                return;
@@ -373,7 +376,10 @@ class MovePageForm extends UnlistedSpecialPage {
                        }
 
                        // This may output an error message and exit
-                       $article->doDelete( wfMsgForContent( 'delete_and_move_reason' ) );
+                       Action::factory( 'delete', $article )->execute(
+                               array( 'Reason' => wfMsgForContent( 'delete_and_move_reason' ) ),
+                               false // Do not capture exceptions
+                       );
                }
 
                # don't allow moving to pages with # in
index d00d960..5239e65 100644 (file)
@@ -998,8 +998,9 @@ Please report this to an [[Special:ListUsers/sysop|administrator]], making note
 'unexpected'           => 'Unexpected value: "$1"="$2".',
 'formerror'            => 'Error: could not submit form',
 'badarticleerror'      => 'This action cannot be performed on this page.',
-'cannotdelete'         => 'The page or file "$1" could not be deleted.
-It may have already been deleted by someone else.',
+'cannotdelete'         => 'The page or file "$1" could not be deleted.  It may have already been deleted by someone else.  The deletion log is provided below for convenience.
+
+$2',
 'badtitle'             => 'Bad title',
 'badtitletext'         => 'The requested page title was invalid, empty, or an incorrectly linked inter-language or inter-wiki title.
 It may contain one or more characters which cannot be used in titles.',
@@ -2783,7 +2784,7 @@ Feedback and further assistance:
 'delete-confirm'         => 'Delete "$1"',
 'delete-backlink'        => '← $1', # only translate this message to other languages if you have to change it
 'delete-legend'          => 'Delete',
-'historywarning'         => "'''Warning:''' The page you are about to delete has a history with approximately $1 {{PLURAL:$1|revision|revisions}}:",
+'historywarning'         => "'''Warning:''' The page you are about to delete has a $2 with approximately $1 {{PLURAL:$1|revision|revisions}}:",
 'confirmdeletetext'      => 'You are about to delete a page along with all of its history.
 Please confirm that you intend to do this, that you understand the consequences, and that you are doing this in accordance with [[{{MediaWiki:Policy-url}}|the policy]].',
 'actioncomplete'         => 'Action complete',