1<?php 2/** 3 * Copyright © 2011 Sam Reed 4 * 5 * This program is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation; either version 2 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License along 16 * with this program; if not, write to the Free Software Foundation, Inc., 17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 * http://www.gnu.org/copyleft/gpl.html 19 * 20 * @file 21 */ 22 23use MediaWiki\Api\ApiHookRunner; 24use MediaWiki\Cache\LinkBatchFactory; 25use MediaWiki\HookContainer\HookContainer; 26use MediaWiki\Linker\LinkRenderer; 27use MediaWiki\ParamValidator\TypeDef\UserDef; 28use MediaWiki\Revision\RevisionAccessException; 29use MediaWiki\Revision\RevisionRecord; 30use MediaWiki\Revision\RevisionStore; 31use MediaWiki\Revision\SlotRecord; 32use MediaWiki\User\UserFactory; 33use Wikimedia\Rdbms\ILoadBalancer; 34 35/** 36 * @ingroup API 37 */ 38class ApiFeedContributions extends ApiBase { 39 40 /** @var RevisionStore */ 41 private $revisionStore; 42 43 /** @var TitleParser */ 44 private $titleParser; 45 46 /** @var LinkRenderer */ 47 private $linkRenderer; 48 49 /** @var LinkBatchFactory */ 50 private $linkBatchFactory; 51 52 /** @var HookContainer */ 53 private $hookContainer; 54 55 /** @var ILoadBalancer */ 56 private $loadBalancer; 57 58 /** @var NamespaceInfo */ 59 private $namespaceInfo; 60 61 /** @var ActorMigration */ 62 private $actorMigration; 63 64 /** @var UserFactory */ 65 private $userFactory; 66 67 /** @var ApiHookRunner */ 68 private $hookRunner; 69 70 /** 71 * @param ApiMain $main 72 * @param string $action 73 * @param RevisionStore $revisionStore 74 * @param TitleParser $titleParser 75 * @param LinkRenderer $linkRenderer 76 * @param LinkBatchFactory $linkBatchFactory 77 * @param HookContainer $hookContainer 78 * @param ILoadBalancer $loadBalancer 79 * @param NamespaceInfo $namespaceInfo 80 * @param ActorMigration $actorMigration 81 * @param UserFactory $userFactory 82 */ 83 public function __construct( 84 ApiMain $main, 85 $action, 86 RevisionStore $revisionStore, 87 TitleParser $titleParser, 88 LinkRenderer $linkRenderer, 89 LinkBatchFactory $linkBatchFactory, 90 HookContainer $hookContainer, 91 ILoadBalancer $loadBalancer, 92 NamespaceInfo $namespaceInfo, 93 ActorMigration $actorMigration, 94 UserFactory $userFactory 95 ) { 96 parent::__construct( $main, $action ); 97 $this->revisionStore = $revisionStore; 98 $this->titleParser = $titleParser; 99 $this->linkRenderer = $linkRenderer; 100 $this->linkBatchFactory = $linkBatchFactory; 101 $this->hookContainer = $hookContainer; 102 $this->loadBalancer = $loadBalancer; 103 $this->namespaceInfo = $namespaceInfo; 104 $this->actorMigration = $actorMigration; 105 $this->userFactory = $userFactory; 106 107 $this->hookRunner = new ApiHookRunner( $hookContainer ); 108 } 109 110 /** 111 * This module uses a custom feed wrapper printer. 112 * 113 * @return ApiFormatFeedWrapper 114 */ 115 public function getCustomPrinter() { 116 return new ApiFormatFeedWrapper( $this->getMain() ); 117 } 118 119 public function execute() { 120 $params = $this->extractRequestParams(); 121 122 $config = $this->getConfig(); 123 if ( !$config->get( 'Feed' ) ) { 124 $this->dieWithError( 'feed-unavailable' ); 125 } 126 127 $feedClasses = $config->get( 'FeedClasses' ); 128 if ( !isset( $feedClasses[$params['feedformat']] ) ) { 129 $this->dieWithError( 'feed-invalid' ); 130 } 131 132 if ( $params['showsizediff'] && $this->getConfig()->get( 'MiserMode' ) ) { 133 $this->dieWithError( 'apierror-sizediffdisabled' ); 134 } 135 136 $msg = wfMessage( 'Contributions' )->inContentLanguage()->text(); 137 $feedTitle = $config->get( 'Sitename' ) . ' - ' . $msg . 138 ' [' . $config->get( 'LanguageCode' ) . ']'; 139 140 $target = $params['user']; 141 if ( ExternalUserNames::isExternal( $target ) ) { 142 // Interwiki names make invalid titles, so put the target in the query instead. 143 $feedUrl = SpecialPage::getTitleFor( 'Contributions' )->getFullURL( [ 'target' => $target ] ); 144 } else { 145 $feedUrl = SpecialPage::getTitleFor( 'Contributions', $target )->getFullURL(); 146 } 147 148 $feed = new $feedClasses[$params['feedformat']] ( 149 $feedTitle, 150 htmlspecialchars( $msg ), 151 $feedUrl 152 ); 153 154 // Convert year/month parameters to end parameter 155 $params['start'] = ''; 156 $params['end'] = ''; 157 $params = ContribsPager::processDateFilter( $params ); 158 159 $targetUser = $this->userFactory->newFromName( $target, UserFactory::RIGOR_NONE ); 160 161 $pager = new ContribsPager( 162 $this->getContext(), [ 163 'target' => $target, 164 'namespace' => $params['namespace'], 165 'start' => $params['start'], 166 'end' => $params['end'], 167 'tagFilter' => $params['tagfilter'], 168 'deletedOnly' => $params['deletedonly'], 169 'topOnly' => $params['toponly'], 170 'newOnly' => $params['newonly'], 171 'hideMinor' => $params['hideminor'], 172 'showSizeDiff' => $params['showsizediff'], 173 ], 174 $this->linkRenderer, 175 $this->linkBatchFactory, 176 $this->hookContainer, 177 $this->loadBalancer, 178 $this->actorMigration, 179 $this->revisionStore, 180 $this->namespaceInfo, 181 $targetUser 182 ); 183 184 $feedLimit = $this->getConfig()->get( 'FeedLimit' ); 185 if ( $pager->getLimit() > $feedLimit ) { 186 $pager->setLimit( $feedLimit ); 187 } 188 189 $feedItems = []; 190 if ( $pager->getNumRows() > 0 ) { 191 $count = 0; 192 $limit = $pager->getLimit(); 193 foreach ( $pager->mResult as $row ) { 194 // ContribsPager selects one more row for navigation, skip that row 195 if ( ++$count > $limit ) { 196 break; 197 } 198 $item = $this->feedItem( $row ); 199 if ( $item !== null ) { 200 $feedItems[] = $item; 201 } 202 } 203 } 204 205 ApiFormatFeedWrapper::setResult( $this->getResult(), $feed, $feedItems ); 206 } 207 208 protected function feedItem( $row ) { 209 // This hook is the api contributions equivalent to the 210 // ContributionsLineEnding hook. Hook implementers may cancel 211 // the hook to signal the user is not allowed to read this item. 212 $feedItem = null; 213 $hookResult = $this->hookRunner->onApiFeedContributions__feedItem( 214 $row, $this->getContext(), $feedItem ); 215 // Hook returned a valid feed item 216 if ( $feedItem instanceof FeedItem ) { 217 return $feedItem; 218 // Hook was canceled and did not return a valid feed item 219 } elseif ( !$hookResult ) { 220 return null; 221 } 222 223 // Hook completed and did not return a valid feed item 224 $title = Title::makeTitle( (int)$row->page_namespace, $row->page_title ); 225 226 if ( $title && $this->getAuthority()->authorizeRead( 'read', $title ) ) { 227 $date = $row->rev_timestamp; 228 $comments = $title->getTalkPage()->getFullURL(); 229 $revision = $this->revisionStore->newRevisionFromRow( $row, 0, $title ); 230 231 return new FeedItem( 232 $title->getPrefixedText(), 233 $this->feedItemDesc( $revision ), 234 $title->getFullURL( [ 'diff' => $revision->getId() ] ), 235 $date, 236 $this->feedItemAuthor( $revision ), 237 $comments 238 ); 239 } 240 241 return null; 242 } 243 244 /** 245 * @since 1.32, takes a RevisionRecord instead of a Revision 246 * @param RevisionRecord $revision 247 * @return string 248 */ 249 protected function feedItemAuthor( RevisionRecord $revision ) { 250 $user = $revision->getUser(); 251 return $user ? $user->getName() : ''; 252 } 253 254 /** 255 * @since 1.32, takes a RevisionRecord instead of a Revision 256 * @param RevisionRecord $revision 257 * @return string 258 */ 259 protected function feedItemDesc( RevisionRecord $revision ) { 260 $msg = wfMessage( 'colon-separator' )->inContentLanguage()->text(); 261 try { 262 $content = $revision->getContent( SlotRecord::MAIN ); 263 } catch ( RevisionAccessException $e ) { 264 $content = null; 265 } 266 267 if ( $content instanceof TextContent ) { 268 // only textual content has a "source view". 269 $html = nl2br( htmlspecialchars( $content->getText() ) ); 270 } else { 271 // XXX: we could get an HTML representation of the content via getParserOutput, but that may 272 // contain JS magic and generally may not be suitable for inclusion in a feed. 273 // Perhaps Content should have a getDescriptiveHtml method and/or a getSourceText method. 274 // Compare also FeedUtils::formatDiffRow. 275 $html = ''; 276 } 277 278 $comment = $revision->getComment(); 279 280 return '<p>' . htmlspecialchars( $this->feedItemAuthor( $revision ) ) . $msg . 281 htmlspecialchars( FeedItem::stripComment( $comment ? $comment->text : '' ) ) . 282 "</p>\n<hr />\n<div>" . $html . '</div>'; 283 } 284 285 public function getAllowedParams() { 286 $feedFormatNames = array_keys( $this->getConfig()->get( 'FeedClasses' ) ); 287 288 $ret = [ 289 'feedformat' => [ 290 ApiBase::PARAM_DFLT => 'rss', 291 ApiBase::PARAM_TYPE => $feedFormatNames 292 ], 293 'user' => [ 294 ApiBase::PARAM_TYPE => 'user', 295 UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'cidr', 'id', 'interwiki' ], 296 ApiBase::PARAM_REQUIRED => true, 297 ], 298 'namespace' => [ 299 ApiBase::PARAM_TYPE => 'namespace' 300 ], 301 'year' => [ 302 ApiBase::PARAM_TYPE => 'integer' 303 ], 304 'month' => [ 305 ApiBase::PARAM_TYPE => 'integer' 306 ], 307 'tagfilter' => [ 308 ApiBase::PARAM_ISMULTI => true, 309 ApiBase::PARAM_TYPE => array_values( ChangeTags::listDefinedTags() ), 310 ApiBase::PARAM_DFLT => '', 311 ], 312 'deletedonly' => false, 313 'toponly' => false, 314 'newonly' => false, 315 'hideminor' => false, 316 'showsizediff' => [ 317 ApiBase::PARAM_DFLT => false, 318 ], 319 ]; 320 321 if ( $this->getConfig()->get( 'MiserMode' ) ) { 322 $ret['showsizediff'][ApiBase::PARAM_HELP_MSG] = 'api-help-param-disabled-in-miser-mode'; 323 } 324 325 return $ret; 326 } 327 328 protected function getExamplesMessages() { 329 return [ 330 'action=feedcontributions&user=Example' 331 => 'apihelp-feedcontributions-example-simple', 332 ]; 333 } 334} 335