From 8779f4b55fe1753f762c8477f344f8c4267562ea Mon Sep 17 00:00:00 2001 From: Happy-melon Date: Thu, 14 Apr 2011 10:38:29 +0000 Subject: [PATCH] r86001, now with less scariness :P I took out the delete action and did purge instead, which is a much more self-contained action-with-a-form. Also implement a few changes suggested by Brion on IRC last night. --- includes/Action.php | 441 ++++++++++++++++ includes/Article.php | 101 +--- includes/AutoLoader.php | 8 +- includes/DefaultSettings.php | 48 +- includes/EditPage.php | 4 +- includes/FileDeleteForm.php | 4 +- includes/ProtectionForm.php | 4 +- includes/Setup.php | 8 + includes/Wiki.php | 23 +- .../CreditsAction.php} | 59 ++- includes/actions/DeleteAction.php | 476 ++++++++++++++++++ includes/actions/PurgeAction.php | 93 ++++ includes/actions/WatchAction.php | 86 ++++ includes/api/ApiBase.php | 4 +- includes/api/ApiWatch.php | 4 +- 15 files changed, 1210 insertions(+), 153 deletions(-) create mode 100644 includes/Action.php rename includes/{Credits.php => actions/CreditsAction.php} (80%) create mode 100644 includes/actions/DeleteAction.php create mode 100644 includes/actions/PurgeAction.php create mode 100644 includes/actions/WatchAction.php diff --git a/includes/Action.php b/includes/Action.php new file mode 100644 index 0000000000..609e8e0941 --- /dev/null +++ b/includes/Action.php @@ -0,0 +1,441 @@ +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 User 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 \ 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 main action entry point. Do all output for display and send it to the context + * output. Do not use globals $wgOut, $wgRequest, etc, in implementations; use + * $this->getOutput(), etc. + * @throws ErrorPageError + */ + public abstract function show(); + + /** + * 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 abstract function execute(); +} + +abstract class FormAction extends Action { + + /** + * Get an HTMLForm descriptor array + * @return Array + */ + protected abstract function getFormFields(); + + /** + * Add pre- or post-text to the form + * @return String HTML which will be sent to $form->addPreText() + */ + 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 ) ); + + $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(); + + /** + * 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->show() ){ + $this->onSuccess(); + } + } + + /** + * @see Action::execute() + * @throws ErrorPageError + * @param array|null $data + * @param bool $captureErrors + * @return bool + */ + 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() ); + + $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; + } + } + catch ( ErrorPageError $e ){ + if( $captureErrors ){ + return false; + } else { + throw $e; + } + } + } +} + +/** + * 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. + * @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 diff --git a/includes/Article.php b/includes/Article.php index b9288c21e5..55cf08d727 100644 --- a/includes/Article.php +++ b/includes/Article.php @@ -1672,32 +1672,7 @@ class Article { * Handle action=purge */ public function purge() { - global $wgRequest, $wgOut; - - if ( $wgOut->getUser()->isAllowed( 'purge' ) || $wgRequest->wasPosted() ) { - //FIXME: shouldn't this be in doPurge()? - if ( wfRunHooks( 'ArticlePurge', array( &$this ) ) ) { - $this->doPurge(); - $this->view(); - } - } else { - $formParams = array( - 'method' => 'post', - 'action' => $wgRequest->getRequestURL(), - ); - - $wgOut->addWikiMsg( 'confirm-purge-top' ); - - $form = Html::openElement( 'form', $formParams ); - $form .= Xml::submitButton( wfMsg( 'confirm_purge_button' ) ); - $form .= Html::closeElement( 'form' ); - - $wgOut->addHTML( $form ); - $wgOut->addWikiMsg( 'confirm-purge-bottom' ); - - $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); - $wgOut->setRobotPolicy( 'noindex,nofollow' ); - } + return Action::factory( 'purge', $this )->show(); } /** @@ -1706,6 +1681,10 @@ class Article { public function doPurge() { global $wgUseSquid; + if( !wfRunHooks( 'ArticlePurge', array( &$this ) ) ){ + return false; + } + // Invalidate the cache $this->mTitle->invalidateCache(); $this->clear(); @@ -2345,27 +2324,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 +2336,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(); } /** diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 9ba9d863c5..54d62e86d6 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -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', + 'PurgeAction' => 'includes/actions/PurgeAction.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/DefaultSettings.php b/includes/DefaultSettings.php index ba72d76540..33884fd601 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -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= 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, + 'purge' => 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 diff --git a/includes/EditPage.php b/includes/EditPage.php index cafb641426..fb6f2bc71e 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -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(); } diff --git a/includes/FileDeleteForm.php b/includes/FileDeleteForm.php index f77d6978c1..9101d6e4f0 100644 --- a/includes/FileDeleteForm.php +++ b/includes/FileDeleteForm.php @@ -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 ) { diff --git a/includes/ProtectionForm.php b/includes/ProtectionForm.php index 86aca3e249..6f04e97274 100644 --- a/includes/ProtectionForm.php +++ b/includes/ProtectionForm.php @@ -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; } diff --git a/includes/Setup.php b/includes/Setup.php index 837740ee61..a7bf62991e 100644 --- a/includes/Setup.php +++ b/includes/Setup.php @@ -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' ) { diff --git a/includes/Wiki.php b/includes/Wiki.php index f54e2b35fa..43b2a88c86 100644 --- a/includes/Wiki.php +++ b/includes/Wiki.php @@ -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,8 +491,6 @@ class MediaWiki { $raw->view(); wfProfileOut( __METHOD__ . '-raw' ); break; - case 'watch': - case 'unwatch': case 'delete': case 'revert': case 'rollback': @@ -495,8 +500,7 @@ class MediaWiki { case 'markpatrolled': 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/Credits.php b/includes/actions/CreditsAction.php similarity index 80% rename from includes/Credits.php rename to includes/actions/CreditsAction.php index e4c8be54e2..576834faaa 100644 --- a/includes/Credits.php +++ b/includes/actions/CreditsAction.php @@ -19,34 +19,35 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA * * @file + * @ingroup Actions * @author */ -class Credits { +class CreditsAction extends FormlessAction { + + public function getName(){ + return 'credits'; + } + + public function getRestriction(){ + return null; + } + /** * This is largely cadged from PageHistory::history - * @param $article Article object */ - public static function showPage( Article $article ) { - global $wgOut; - + public function onView() { 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 ) { + if ( $this->page->getID() == 0 ) { $s = wfMsg( 'nocredits' ); } else { - $s = self::getCredits( $article, -1 ); + $s = $this->getCredits( -1 ); } - $wgOut->addHTML( $s ); - wfProfileOut( __METHOD__ ); + + return $s; } /** @@ -56,14 +57,14 @@ class Credits { * @param $showIfMax Bool: whether to contributors if there more than $cnt * @return String: html */ - public static function getCredits( Article $article, $cnt, $showIfMax = true ) { + protected function getCredits( $cnt, $showIfMax = true ) { wfProfileIn( __METHOD__ ); $s = ''; if ( isset( $cnt ) && $cnt != 0 ) { - $s = self::getAuthor( $article ); + $s = self::getAuthor( $this->page ); if ( $cnt > 1 || $cnt < 0 ) { - $s .= ' ' . self::getContributors( $article, $cnt - 1, $showIfMax ); + $s .= ' ' . $this->getContributors( $cnt - 1, $showIfMax ); } } @@ -98,16 +99,16 @@ class Credits { * @param $showIfMax Bool: whether to contributors if there more than $cnt * @return String: html */ - protected static function getContributors( Article $article, $cnt, $showIfMax ) { + protected function getContributors( $cnt, $showIfMax ) { global $wgLang, $wgHiddenPrefs; - $contributors = $article->getContributors(); + $contributors = $this->page->getContributors(); $others_link = false; # Hmm... too many to fit! if ( $cnt > 0 && $contributors->count() > $cnt ) { - $others_link = self::othersLink( $article ); + $others_link = $this->othersLink(); if ( !$showIfMax ) return wfMsgExt( 'othercontribs', 'parsemag', $others_link, $contributors->count() ); } @@ -192,12 +193,11 @@ class Credits { $real = false; } - $skin = $wgUser->getSkin(); - $page = $user->isAnon() ? - SpecialPage::getTitleFor( 'Contributions', $user->getName() ) : - $user->getUserPage(); + $page = $user->isAnon() + ? SpecialPage::getTitleFor( 'Contributions', $user->getName() ) + : $user->getUserPage(); - return $skin->link( $page, htmlspecialchars( $real ? $real : $user->getName() ) ); + return Linker::link( $page, htmlspecialchars( $real ? $real : $user->getName() ) ); } /** @@ -224,11 +224,10 @@ class Credits { * @param $article Article object * @return String: html */ - protected static function othersLink( Article $article ) { + protected function othersLink() { global $wgUser; - $skin = $wgUser->getSkin(); - return $skin->link( - $article->getTitle(), + return Linker::link( + $this->getTitle(), wfMsgHtml( 'others' ), array(), array( 'action' => 'credits' ), diff --git a/includes/actions/DeleteAction.php b/includes/actions/DeleteAction.php new file mode 100644 index 0000000000..3f8097f3ac --- /dev/null +++ b/includes/actions/DeleteAction.php @@ -0,0 +1,476 @@ +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( '

' . $link . '

' ); + } + } + + /** + * 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( , , ), but it's reasonable for other + // functions to just pass '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/PurgeAction.php b/includes/actions/PurgeAction.php new file mode 100644 index 0000000000..e212fbf6dd --- /dev/null +++ b/includes/actions/PurgeAction.php @@ -0,0 +1,93 @@ +. + * + * 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 + */ + +class PurgeAction extends FormAction { + + public function getName(){ + return 'purge'; + } + + public function getRestriction(){ + return null; + } + + public function requiresUnblock(){ + return false; + } + + public function getDescription(){ + return ''; + } + + /** + * Just get an empty form with a single submit button + * @return array + */ + protected function getFormFields(){ + return array(); + } + + public function onSubmit( $data ){ + $this->page->doPurge(); + return true; + } + + /** + * purge is slightly wierd because it can be either formed or formless depending + * on user permissions + */ + public function show(){ + $this->setHeaders(); + + // This will throw exceptions if there's a problem + $this->checkCanExecute( $this->getUser() ); + + if( $this->getUser()->isAllowed( 'purge' ) ){ + $this->onSubmit( array() ); + $this->onSuccess(); + } else { + $form = $this->getForm(); + if( $form->show() ){ + $this->onSuccess(); + } + } + } + + protected function alterForm( HTMLForm $form ){ + $form->setSubmitText( wfMsg( 'confirm_purge_button' ) ); + } + + protected function preText(){ + return wfMessage( 'confirm-purge-top' )->parse(); + } + + protected function postText(){ + return wfMessage( 'confirm-purge-bottom' )->parse(); + } + + public function onSuccess(){ + $this->getOutput()->redirect( $this->getTitle() ); + } +} diff --git a/includes/actions/WatchAction.php b/includes/actions/WatchAction.php new file mode 100644 index 0000000000..43e0b47d74 --- /dev/null +++ b/includes/actions/WatchAction.php @@ -0,0 +1,86 @@ +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(); + } +} diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php index 336bc79e03..fe6ce54645 100644 --- a/includes/api/ApiBase.php +++ b/includes/api/ApiBase.php @@ -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(); } } diff --git a/includes/api/ApiWatch.php b/includes/api/ApiWatch.php index fe52328ef5..685306dff3 100644 --- a/includes/api/ApiWatch.php +++ b/includes/api/ApiWatch.php @@ -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' ) ); -- 2.20.1