CategoryMembershipChange.php 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. <?php
  2. use MediaWiki\MediaWikiServices;
  3. use MediaWiki\Revision\RevisionRecord;
  4. /**
  5. * Helper class for category membership changes
  6. *
  7. * This program is free software; you can redistribute it and/or modify
  8. * it under the terms of the GNU General Public License as published by
  9. * the Free Software Foundation; either version 2 of the License, or
  10. * (at your option) any later version.
  11. *
  12. * This program is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU General Public License along
  18. * with this program; if not, write to the Free Software Foundation, Inc.,
  19. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  20. * http://www.gnu.org/copyleft/gpl.html
  21. *
  22. * @file
  23. * @author Kai Nissen
  24. * @author Addshore
  25. * @since 1.27
  26. */
  27. class CategoryMembershipChange {
  28. const CATEGORY_ADDITION = 1;
  29. const CATEGORY_REMOVAL = -1;
  30. /**
  31. * @var string Current timestamp, set during CategoryMembershipChange::__construct()
  32. */
  33. private $timestamp;
  34. /**
  35. * @var Title Title instance of the categorized page
  36. */
  37. private $pageTitle;
  38. /**
  39. * @var Revision|null Latest Revision instance of the categorized page
  40. */
  41. private $revision;
  42. /**
  43. * @var int
  44. * Number of pages this WikiPage is embedded by
  45. * Set by CategoryMembershipChange::checkTemplateLinks()
  46. */
  47. private $numTemplateLinks = 0;
  48. /**
  49. * @var callable|null
  50. */
  51. private $newForCategorizationCallback = null;
  52. /**
  53. * @param Title $pageTitle Title instance of the categorized page
  54. * @param Revision|null $revision Latest Revision instance of the categorized page
  55. *
  56. * @throws MWException
  57. */
  58. public function __construct( Title $pageTitle, Revision $revision = null ) {
  59. $this->pageTitle = $pageTitle;
  60. if ( $revision === null ) {
  61. $this->timestamp = wfTimestampNow();
  62. } else {
  63. $this->timestamp = $revision->getTimestamp();
  64. }
  65. $this->revision = $revision;
  66. $this->newForCategorizationCallback = [ RecentChange::class, 'newForCategorization' ];
  67. }
  68. /**
  69. * Overrides the default new for categorization callback
  70. * This is intended for use while testing and will fail if MW_PHPUNIT_TEST is not defined.
  71. *
  72. * @param callable $callback
  73. * @see RecentChange::newForCategorization for callback signiture
  74. *
  75. * @throws MWException
  76. */
  77. public function overrideNewForCategorizationCallback( callable $callback ) {
  78. if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
  79. throw new MWException( 'Cannot override newForCategorization callback in operation.' );
  80. }
  81. $this->newForCategorizationCallback = $callback;
  82. }
  83. /**
  84. * Determines the number of template links for recursive link updates
  85. */
  86. public function checkTemplateLinks() {
  87. $this->numTemplateLinks = $this->pageTitle->getBacklinkCache()->getNumLinks( 'templatelinks' );
  88. }
  89. /**
  90. * Create a recentchanges entry for category additions
  91. *
  92. * @param Title $categoryTitle
  93. */
  94. public function triggerCategoryAddedNotification( Title $categoryTitle ) {
  95. $this->createRecentChangesEntry( $categoryTitle, self::CATEGORY_ADDITION );
  96. }
  97. /**
  98. * Create a recentchanges entry for category removals
  99. *
  100. * @param Title $categoryTitle
  101. */
  102. public function triggerCategoryRemovedNotification( Title $categoryTitle ) {
  103. $this->createRecentChangesEntry( $categoryTitle, self::CATEGORY_REMOVAL );
  104. }
  105. /**
  106. * Create a recentchanges entry using RecentChange::notifyCategorization()
  107. *
  108. * @param Title $categoryTitle
  109. * @param int $type
  110. */
  111. private function createRecentChangesEntry( Title $categoryTitle, $type ) {
  112. $this->notifyCategorization(
  113. $this->timestamp,
  114. $categoryTitle,
  115. $this->getUser(),
  116. $this->getChangeMessageText(
  117. $type,
  118. $this->pageTitle->getPrefixedText(),
  119. $this->numTemplateLinks
  120. ),
  121. $this->pageTitle,
  122. $this->getPreviousRevisionTimestamp(),
  123. $this->revision,
  124. $type === self::CATEGORY_ADDITION
  125. );
  126. }
  127. /**
  128. * @param string $timestamp Timestamp of the recent change to occur in TS_MW format
  129. * @param Title $categoryTitle Title of the category a page is being added to or removed from
  130. * @param User|null $user User object of the user that made the change
  131. * @param string $comment Change summary
  132. * @param Title $pageTitle Title of the page that is being added or removed
  133. * @param string $lastTimestamp Parent revision timestamp of this change in TS_MW format
  134. * @param Revision|null $revision
  135. * @param bool $added true, if the category was added, false for removed
  136. *
  137. * @throws MWException
  138. */
  139. private function notifyCategorization(
  140. $timestamp,
  141. Title $categoryTitle,
  142. User $user = null,
  143. $comment,
  144. Title $pageTitle,
  145. $lastTimestamp,
  146. $revision,
  147. $added
  148. ) {
  149. $deleted = $revision ? $revision->getVisibility() & RevisionRecord::SUPPRESSED_USER : 0;
  150. $newRevId = $revision ? $revision->getId() : 0;
  151. /**
  152. * T109700 - Default bot flag to true when there is no corresponding RC entry
  153. * This means all changes caused by parser functions & Lua on reparse are marked as bot
  154. * Also in the case no RC entry could be found due to replica DB lag
  155. */
  156. $bot = 1;
  157. $lastRevId = 0;
  158. $ip = '';
  159. # If no revision is given, the change was probably triggered by parser functions
  160. if ( $revision !== null ) {
  161. $correspondingRc = $this->revision->getRecentChange();
  162. if ( $correspondingRc === null ) {
  163. $correspondingRc = $this->revision->getRecentChange( Revision::READ_LATEST );
  164. }
  165. if ( $correspondingRc !== null ) {
  166. $bot = $correspondingRc->getAttribute( 'rc_bot' ) ?: 0;
  167. $ip = $correspondingRc->getAttribute( 'rc_ip' ) ?: '';
  168. $lastRevId = $correspondingRc->getAttribute( 'rc_last_oldid' ) ?: 0;
  169. }
  170. }
  171. /** @var RecentChange $rc */
  172. $rc = ( $this->newForCategorizationCallback )(
  173. $timestamp,
  174. $categoryTitle,
  175. $user,
  176. $comment,
  177. $pageTitle,
  178. $lastRevId,
  179. $newRevId,
  180. $lastTimestamp,
  181. $bot,
  182. $ip,
  183. $deleted,
  184. $added
  185. );
  186. $rc->save();
  187. }
  188. /**
  189. * Get the user associated with this change.
  190. *
  191. * If there is no revision associated with the change and thus no editing user
  192. * fallback to a default.
  193. *
  194. * False will be returned if the user name specified in the
  195. * 'autochange-username' message is invalid.
  196. *
  197. * @return User|bool
  198. */
  199. private function getUser() {
  200. if ( $this->revision ) {
  201. $userId = $this->revision->getUser( RevisionRecord::RAW );
  202. if ( $userId === 0 ) {
  203. return User::newFromName( $this->revision->getUserText( RevisionRecord::RAW ), false );
  204. } else {
  205. return User::newFromId( $userId );
  206. }
  207. }
  208. $username = wfMessage( 'autochange-username' )->inContentLanguage()->text();
  209. $user = User::newFromName( $username );
  210. # User::newFromName() can return false on a badly configured wiki.
  211. if ( $user && !$user->isLoggedIn() ) {
  212. $user->addToDatabase();
  213. }
  214. return $user;
  215. }
  216. /**
  217. * Returns the change message according to the type of category membership change
  218. *
  219. * The message keys created in this method may be one of:
  220. * - recentchanges-page-added-to-category
  221. * - recentchanges-page-added-to-category-bundled
  222. * - recentchanges-page-removed-from-category
  223. * - recentchanges-page-removed-from-category-bundled
  224. *
  225. * @param int $type may be CategoryMembershipChange::CATEGORY_ADDITION
  226. * or CategoryMembershipChange::CATEGORY_REMOVAL
  227. * @param string $prefixedText result of Title::->getPrefixedText()
  228. * @param int $numTemplateLinks
  229. *
  230. * @return string
  231. */
  232. private function getChangeMessageText( $type, $prefixedText, $numTemplateLinks ) {
  233. $array = [
  234. self::CATEGORY_ADDITION => 'recentchanges-page-added-to-category',
  235. self::CATEGORY_REMOVAL => 'recentchanges-page-removed-from-category',
  236. ];
  237. $msgKey = $array[$type];
  238. if ( intval( $numTemplateLinks ) > 0 ) {
  239. $msgKey .= '-bundled';
  240. }
  241. return wfMessage( $msgKey, $prefixedText )->inContentLanguage()->text();
  242. }
  243. /**
  244. * Returns the timestamp of the page's previous revision or null if the latest revision
  245. * does not refer to a parent revision
  246. *
  247. * @return null|string
  248. */
  249. private function getPreviousRevisionTimestamp() {
  250. $rl = MediaWikiServices::getInstance()->getRevisionLookup();
  251. $latestRev = $rl->getRevisionByTitle( $this->pageTitle );
  252. if ( $latestRev ) {
  253. $previousRev = $rl->getPreviousRevision( $latestRev );
  254. if ( $previousRev ) {
  255. return $previousRev->getTimestamp();
  256. }
  257. }
  258. return null;
  259. }
  260. }