diff --git a/.gitignore b/.gitignore index f324311..001164d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,18 @@ # -*- mode: gitignore; -*- ### vi ### .*.swp ### Emacs ### *~ \#*\# .\#* +.tramp_history # misc PHPTAGS.sqlite # misc MW /node_modules /vendor /composer.lock diff --git a/CategoryWatch.php b/CategoryWatch.php deleted file mode 100644 index a5a8061..0000000 --- a/CategoryWatch.php +++ /dev/null @@ -1,458 +0,0 @@ -before = []; - $dbr = wfGetDB( DB_MASTER ); - $cl = $dbr->tableName( 'categorylinks' ); - $id = $wikiPage->getID(); - wfDebugLog( 'CategoryWatch', "tablename = $cl" ); - wfDebugLog( 'CategoryWatch', "page id=$id" ); - $res = $dbr->select( - $cl, 'cl_to', "cl_from = $id", __METHOD__, - [ 'ORDER BY' => 'cl_sortkey' ] - ); - $row = $dbr->fetchRow( $res ); - while ( $row ) { - self::$watcher->before[] = $row[0]; - $row = $dbr->fetchRow( $res ); - } - $dbr->freeResult( $res ); - wfDebugLog( 'CategoryWatch', 'Categories before page saved' ); - wfDebugLog( 'CategoryWatch', join( ', ', self::$watcher->before ) ); - - # If using the automatically watched category feature, ensure - # that all users are watching it - if ( $wgCategoryWatchUseAutoCat ) { - $dbr = wfGetDB( DB_SLAVE ); - - # Find all users not watching the autocat - $like = str_replace( - ' ', '_', - trim( wfMessage( 'categorywatch-autocat', '' )->text() ) - ); - $utbl = $dbr->tableName( 'user' ); - $wtbl = $dbr->tableName( 'watchlist' ); - $sql = "SELECT user_id FROM $utbl LEFT JOIN $wtbl ON " - . "user_id=wl_user AND wl_title LIKE '%$like%' " - . "WHERE wl_user IS NULL"; - - # Insert an entry into watchlist for each - $row = $dbr->fetchRow( $res ); - while ( $row ) { - $user = User::newFromId( $row[0] ); - $name = $wgCategoryWatchUseAutoCatRealName - ? $user->getRealName() - : $user->getName(); - $wl_title = str_replace( - ' ', '_', wfMessage( 'categorywatch-autocat', $name )->text() - ); - $dbr->insert( - $wtbl, - [ - 'wl_user' => $row[0], 'wl_namespace' => NS_CATEGORY, - 'wl_title' => $wl_title - ] - ); - $row = $dbr->fetchRow( $res ); - } - $dbr->freeResult( $res ); - } - } - - /** - * the proper hook for save page request. - * @see https://www.mediawiki.org/wiki/Manual:Hooks/PageContentSaveComplete - * - * @param WikiPage $wikiPage WikiPage modified - * @param User $user who edited - * @param Content $content New article text - * @param string $summary Edit summary - * @param bool $isMinor Minor edit or not - * @param bool $isWatch Watch this article? - * @param string $section Section that was edited - * @param int $flags Edit flags - * @param Revision $revision that was created - * @param Status $status of activities - * @param int $baseRevId base revision - */ - public static function onPageContentSaveComplete( - WikiPage $wikiPage, $user, $content, $summary, $isMinor, $isWatch, $section, - $flags, $revision, $status, $baseRevId - ) { - # Get cats after update - self::$watcher->after = []; - - $parseTimestamp = $revision->getTimestamp(); - $content = $revision->getContent(); - $title = $wikiPage->getTitle(); - $options = $content->getContentHandler()->makeParserOptions( 'canonical' ); - $options->setTimestamp( $parseTimestamp ); - $output = $content->getParserOutput( $title, $revision->getId(), $options ); - self::$watcher->after = array_map( - 'strval', array_keys( $output->getCategories() ) - ); - wfDebugLog( 'CategoryWatch', 'Categories after page saved' ); - wfDebugLog( 'CategoryWatch', join( ', ', self::$watcher->after ) ); - - # Get list of added and removed cats - $add = array_diff( self::$watcher->after, self::$watcher->before ); - $sub = array_diff( self::$watcher->before, self::$watcher->after ); - - # Notify watchers of each cat about the addition or removal of this article - if ( count( $add ) > 0 || count( $sub ) > 0 ) { - $page = $wikiPage->getTitle(); - $pagename = $page->getPrefixedText(); - $pageurl = $page->getFullUrl(); - $page = "$pagename ($pageurl)"; - - if ( count( $add ) == 1 && count( $sub ) == 1 ) { - $add = array_shift( $add ); - $sub = array_shift( $sub ); - - $title = Title::newFromText( $add, NS_CATEGORY ); - $message = wfMessage( - 'categorywatch-catmovein', $page, - self::$watcher->friendlyCat( $add ), - self::$watcher->friendlyCat( $sub ) - )->text(); - self::$watcher->notifyWatchers( - $title, $user, $message, $summary, $medit, $pageurl - ); - - $title = Title::newFromText( $sub, NS_CATEGORY ); - $message = wfMessage( - 'categorywatch-catmoveout', $page, - self::$watcher->friendlyCat( $sub ), - self::$watcher->friendlyCat( $add ) - )->text(); - self::$watcher->notifyWatchers( - $title, $user, $message, $summary, $medit, $pageurl - ); - } else { - - foreach ( $add as $cat ) { - $title = Title::newFromText( $cat, NS_CATEGORY ); - $message = wfMessage( - 'categorywatch-catadd', $page, - self::$watcher->friendlyCat( $cat ) - )->text(); - self::$watcher->notifyWatchers( - $title, $user, $message, $summary, $medit, $pageurl - ); - } - - foreach ( $sub as $cat ) { - $title = Title::newFromText( $cat, NS_CATEGORY ); - $message = wfMessage( - 'categorywatch-catsub', $page, - self::$watcher->friendlyCat( $cat ) - )->text(); - self::$watcher->notifyWatchers( - $title, $user, $message, $summary, $medit, $pageurl - ); - } - } - } - - global $wgCategoryWatchNotifyParentWatchers; - if ( $wgCategoryWatchNotifyParentWatchers ) { - self::notifyParentWatchers(); - } - } - - /** - * Notify the watchers of parent categories - */ - protected static function notifyParentWatchers() { - self::$watcher->allparents = []; - self::$watcher->i = 0; - self::$watcher->findCategoryParents( self::$watcher->after ); - ## For each active parent category, send the mail - if ( self::$watcher->allparents ) { - $page = $article->getTitle(); - $pageurl = $page->getFullUrl(); - foreach ( self::$watcher->allparents as $cat ) { - $title = Title::newFromText( $cat, NS_CATEGORY ); - $message = wfMessage( - 'categorywatch-catchange', $page, - self::$watcher->friendlyCat( $cat ) - ); - self::$watcher->notifyWatchers( - $title, $user, $message, $summary, $medit, $pageurl - ); - } - } - } - - /** - * Recursively find all parents of the given categories - * - * @param array $catarray the categories - */ - protected function findCategoryParents( array $catarray ) { - $this->i++; - if ( $this->i == 200 ) { - return; - } - - if ( $catarray ) { - foreach ( $catarray as $catname ) { - self::$watcher->allparents[] = $catname; - $id = self::$watcher->getCategoryArticleId( $catname ); - if ( is_numeric( $id ) ) { - $parentCat = self::$watcher->getParentCategories( $id ); - if ( $parentCat ) { - self::$watcher->allparents[] = $parentCat; - self::$watcher->findCategoryParents( [ $parentCat ] ); - } - } - } - self::$watcher->allparents = array_unique( self::$watcher->allparents ); - } - } - - /** - * Return the parent categories - * @param int $id Category Article id - * @return parents - */ - protected function getParentCategories( $id ) { - $dbr = wfGetDB( DB_SLAVE ); - $cl = $dbr->tableName( 'categorylinks' ); - $res = $dbr->select( - $cl, 'cl_to', "cl_from = $id", __METHOD__, - [ 'ORDER BY' => 'cl_sortkey' ] - ); - $row = $dbr->fetchRow( $res ); - $dbr->freeResult( $res ); - if ( empty( $row[0] ) ) { - return false; - } - return $row[0]; - } - - /** - * Load page ID of one category - * - * @param string $catname name of category - * @return int - */ - protected function getCategoryArticleId( $catname ) { - $dbr = wfGetDB( DB_SLAVE ); - $cl = $dbr->tableName( 'page' ); - $res = $dbr->select( $cl, 'page_id', "page_title = '$catname'", __METHOD__ ); - $row = $dbr->fetchRow( $res ); - $dbr->freeResult( $res ); - return $row[0]; - } - - /** - * Return "Category:Cat (URL)" from "Cat" - * @param string $cat name of category - * @return string - */ - protected function friendlyCat( $cat ) { - $cat = Title::newFromText( $cat, NS_CATEGORY ); - $catname = $cat->getPrefixedText(); - $caturl = $cat->getFullUrl(); - return "$catname ($caturl)"; - } - - /** - * Notify any watchers - * @param Title $title of article - * @param User $editor of article - * @param string $message for user - * @param string $summary editor gave - * @param bool $medit true if minor - * @param string $pageurl of page - */ - function notifyWatchers( $title, $editor, $message, $summary, $medit, $pageurl ) { - global $wgLang, $wgNoReplyAddress, $wgCategoryWatchNotifyEditor, - $wgEnotifRevealEditorAddress, $wgEnotifUseRealName, $wgPasswordSender, - $wgEnotifFromEditor, $wgPasswordSenderName; - - # Get list of users watching this category - $dbr = wfGetDB( DB_SLAVE ); - $conds = [ - 'wl_title' => $title->getDBkey(), 'wl_namespace' => $title->getNamespace() - ]; - if ( !$wgCategoryWatchNotifyEditor ) { - $conds[] = 'wl_user <> ' . intval( $editor->getId() ); - } - $res = $dbr->select( 'watchlist', [ 'wl_user' ], $conds, __METHOD__ ); - - # Wrap message with common body and send to each watcher - $page = $title->getPrefixedText(); - $adminAddress = new MailAddress( - $wgPasswordSender, - isset( $wgPasswordSenderName ) - ? $wgPasswordSenderName - : 'WikiAdmin' - ); - $editorAddress = new MailAddress( $editor ); - $summary = $summary - ? $summary - : ' - '; - $medit = $medit - ? wfMessage( 'minoredit' )->text() - : ''; - $row = $dbr->fetchRow( $res ); - while ( $row ) { - $watchingUser = User::newFromId( $row[0] ); - $timecorrection = $watchingUser->getOption( 'timecorrection' ); - $editdate = $wgLang->timeanddate( - wfTimestampNow(), true, false, $timecorrection - ); - - if ( - $watchingUser->getOption( 'enotifwatchlistpages' ) - && $watchingUser->isEmailConfirmed() - ) { - $to = new MailAddress( $watchingUser ); - $subject = wfMessage( 'categorywatch-emailsubject', $page )->text(); - $body = wfMessage( 'enotif_body' )->inContentLanguage()->text(); - - # Reveal the page editor's address as REPLY-TO address only if - # the user has not opted-out and the option is enabled at the - # global configuration level. - if ( $wgCategoryWatchNoRealName ) { - $name = $watchingUser->getName(); - } - $name = $wgEnotifUseRealName - ? $watchingUser->getRealName() - : $watchingUser->getName(); - if ( $wgEnotifRevealEditorAddress - && ( $editor->getEmail() != '' ) - && $editor->getOption( 'enotifrevealaddr' ) - ) { - if ( $wgEnotifFromEditor ) { - $from = $editorAddress; - } else { - $from = $adminAddress; - $replyto = $editorAddress; - } - } else { - $from = $adminAddress; - $replyto = new MailAddress( $wgNoReplyAddress ); - } - - # Define keys for body message - # body message is defined in page MediaWiki:Enotif_body - # set both $PAGEINTRO and $HELPPAGE to empty string for now. - $userPage = $editor->getUserPage(); - $keys = [ - '$WATCHINGUSERNAME' => $name, - '$PAGEINTRO' => '', - '$NEWPAGE' => $message, - '$PAGETITLE' => $page, - '$PAGEEDITDATE' => $editdate, - '$CHANGEDORCREATED' => wfMessage( 'changed' ) - ->inContentLanguage()->text(), - '$PAGETITLE_URL' => $title->getFullUrl(), - '$PAGEEDITOR_WIKI' => $userPage->getFullUrl(), - '$PAGESUMMARY' => $summary, - '$PAGEMINOREDIT' => $medit, - '$HELPPAGE' => '', - '$OLDID' => '' - ]; - if ( $editor->isIP( $name ) ) { - $utext = wfMessage( - 'enotif_anon_editor', $name - )->inContentLanguage()->text(); - $subject = str_replace( '$PAGEEDITOR', $utext, $subject ); - $keys['$PAGEEDITOR'] = $utext; - $keys['$PAGEEDITOR_EMAIL'] = wfMmessage( - 'noemailtitle' - )->inContentLanguage()->text(); - } else { - $subject = str_replace( '$PAGEEDITOR', $name, $subject ); - $keys['$PAGEEDITOR'] = $name; - $emailPage = SpecialPage::getSafeTitleFor( 'Emailuser', $name ); - $keys['$PAGEEDITOR_EMAIL'] = $emailPage->getFullUrl(); - } - $keys['$PAGESUMMARY'] = $summary; - - # Replace keys, wrap text and send - $body = strtr( $body, $keys ); - $body = wordwrap( $body, 72 ); - $options = []; - $options['replyTo'] = $replyto; - UserMailer::send( $to, $from, $subject, $body, $options ); - } - } - - $dbr->freeResult( $res ); - } -} diff --git a/assets/catwatch.svg b/assets/catwatch.svg new file mode 100644 index 0000000..704e616 --- /dev/null +++ b/assets/catwatch.svg @@ -0,0 +1,62 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/extension.json b/extension.json index cbf8e7d..1e04ff3 100644 --- a/extension.json +++ b/extension.json @@ -1,38 +1,47 @@ { "name": "CategoryWatch", - "version": "1.2.2, 2011-12-03", + "version": "2.0, 2017-09-19", "author": [ "[http://www.organicdesign.co.nz/User:Nad User:Nad]", "Sean Chen", "[http://mwstake.org/ Mark A. Hershberger]" ], "url": "https://www.mediawiki.org/wiki/Extension:CategoryWatch", "descriptionmsg": "categorywatch-desc", "license-name": "GPL-2.0-or-later", "type": "other", "AutoloadClasses": { - "CategoryWatch": "CategoryWatch.php" + "CategoryWatch\\CategoryWatch": "src/CategoryWatch.php", + "CategoryWatch\\EchoEventPresentationModel": "src/EchoEventPresentationModel.php", + "CategoryWatch\\Hook": "src/Hook.php" }, - "ExtensionFunctions": [ - "CategoryWatch::setupCategoryWatch" - ], "MessagesDirs": { "CategoryWatch": "i18n" }, + "DefaultUserOptions": { + "echo-subscriptions-email-categorywatch": true, + "echo-subscriptions-web-categorywatch": true + }, "Hooks": { - "PageContentSave": [ - "CategoryWatch::onPageContentSave" + "BeforeCreateEchoEvent": [ + "CategoryWatch\\Hook::onBeforeCreateEchoEvent" + ], + "EchoGetBundleRules": [ + "CategoryWatch\\Hook::onEchoGetBundleRules" + ], + "CategoryAfterPageAdded": [ + "CategoryWatch\\Hook::onCategoryAfterPageAdded" ], - "PageContentSaveComplete": [ - "CategoryWatch::onPageContentSaveComplete" + "CategoryAfterPageRemoved": [ + "CategoryWatch\\Hook::onCategoryAfterPageRemoved" ] }, "config": { "_prefix": "wgCategoryWatch", "NotifyEditor": true, "NotifyParentWatchers": false, "UseAutoCat": false, "UseAutoCatRealName": false }, "manifest_version": 1 } diff --git a/i18n/en.json b/i18n/en.json index 27ed439..eba5be2 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1,14 +1,26 @@ { "@metadata": { "authors": [ "Nad" ] }, "categorywatch-desc": "Extends watchlist functionality to include notification about membership changes of watched categories", "categorywatch-emailsubject": "Activity involving watched category \"$1\"", "categorywatch-catmovein": "$1 has moved into $2 from $3", "categorywatch-catmoveout": "$1 has moved out of $2 into $3", "categorywatch-catadd": "$1 has been added to $2", "categorywatch-catsub": "$1 has been removed from $2", - "categorywatch-autocat": "Automatically watched by $1" + "categorywatch-autocat": "Automatically watched by $1", + "categorywatch-notification-link": "Category: [[:$1]]", + "categorywatch-notification-categorywatch-add-header": "[[:$1|$2]] added to [[:$3|$4]]", + "categorywatch-notification-categorywatch-add-summary": "[[:$3|$4]] added to [[:$5|$6]]", + "categorywatch-notification-categorywatch-add-body": "[[User:$1|$1]] added [[:$3|$3]] to [[:$4|$4]]", + "categorywatch-notification-categorywatch-remove-header": "[[:$1|$2]] removed from [[:$3|$4]]", + "categorywatch-notification-categorywatch-remove-summary": "[[:$3|$4]] removed from [[:$5|$6]]", + "categorywatch-notification-categorywatch-remove-body": "[[User:$1|$1]] removed [[:$3|$3]] from [[:$4|$4]]", + "categorywatch-notification-bundle": "$1 changes in categorization on {{SITENAME}}", + "categorywatch-add-title": "Title added to watched category", + "categorywatch-remove-title": "Title removed from watched category", + "echo-category-title-categorywatch": "Category watch", + "echo-pref-tooltip-categorywatch": "Notify me when someone categorizes a page into or out of a category that I'm watching." } diff --git a/i18n/qqq.json b/i18n/qqq.json index abea5f9..103f02c 100644 --- a/i18n/qqq.json +++ b/i18n/qqq.json @@ -1,18 +1,31 @@ { "@metadata": { "authors": [ "Fryed-peach", "Purodha", "Siebrand", "The Evil IP address", "Shirayuki" ] }, "categorywatch-desc": "{{desc}}", - "categorywatch-emailsubject": "Subject of notification E-mail\n* $1 - page title", + "categorywatch-add-title": "Title added to watched category", + "categorywatch-autocat": "If the \"automatically watching\" feature is enabled, this message is used as a page title in the watchlist.\n* $1 is a username (or a realname)", + "categorywatch-catadd": "Substituted as $5 in {{msg-mw|categorywatch-emailbody}}.\n* $1 is a page name\n* $2 is a category name", "categorywatch-catmovein": "Substituted as $5 in {{msg-mw|categorywatch-emailbody}}.\n* $1 is a page name\n* $2 is the target category name\n* $3 is the source category name", "categorywatch-catmoveout": "Substituted as $5 in {{msg-mw|categorywatch-emailbody}}.\n* $1 is a page name\n* $2 is the source category name\n* $3 is the target category name", - "categorywatch-catadd": "Substituted as $5 in {{msg-mw|categorywatch-emailbody}}.\n* $1 is a page name\n* $2 is a category name", "categorywatch-catsub": "Substituted as $5 in {{msg-mw|categorywatch-emailbody}}.\n* $1 is a page name\n* $2 is a category name", - "categorywatch-autocat": "If the \"automatically watching\" feature is enabled, this message is used as a page title in the watchlist.\n* $1 is a username (or a realname)" + "categorywatch-emailsubject": "Subject of notification E-mail\n* $1 - page title", + "categorywatch-notification-bundle": "Name of bundle for CategoryWatch notifications", + "categorywatch-notification-categorywatch-add-body": "Tell which user added a page to the category", + "categorywatch-notification-categorywatch-add-header": "Header for when a page is added to the category", + "categorywatch-notification-categorywatch-add-summary": "Summary when a page is added to the category", + "categorywatch-notification-categorywatch-remove-body": "Tell which user removed a page from the category", + "categorywatch-notification-categorywatch-remove-header": "Header for when a page is removed from the category", + "categorywatch-notification-categorywatch-remove-summary": "Summary for when a page is removed from the category", + "categorywatch-notification-link": "Category: [[:$1]]", + "categorywatch-remove-title": "Title removed from watched category", + + "echo-category-title-categorywatch": "Membership changes in categories watched", + "echo-pref-tooltip-categorywatch": "Tooltip for categorywatch notification preference" } diff --git a/src/CategoryWatch.php b/src/CategoryWatch.php new file mode 100644 index 0000000..cdcdc7f --- /dev/null +++ b/src/CategoryWatch.php @@ -0,0 +1,403 @@ +. + * + * See https://www.mediawiki.org/Extension:CategoryWatch + * for installation and usage details + * See http://www.organicdesign.co.nz/Extension_talk:CategoryWatch + * for development notes and disucssion + * + * @file + * @ingroup Extensions + * @author Aran Dunkley [http://www.organicdesign.co.nz/nad User:Nad] + * @copyright © 2008 Aran Dunkley + * @licence GNU General Public Licence 2.0 or later + */ + +namespace CategoryWatch; + +class CategoryWatch { + public $before = []; + public $after = []; + + protected $count = 0; + protected $allParents = []; + + protected $wikiPage; + protected $editor; + protected $content; + protected $summary; + protected $minorEdit; + protected $flags; + + /** + * Construction + * @param WikiPage $wikiPage the page + * @param User $user who is modifying + * @param Content $content the new article content + * @param string $summary the article summary (comment) + * @param bool $isMinor minor flag + * @param int $flags see WikiPage::doEditContent documentation for flags' definition + */ + public function __construct( + WikiPage $wikiPage, User $user, Content $content, $summary, $isMinor, $flags + ) { + wfDebugLog( 'CategoryWatch', __METHOD__ ); + $this->wikiPage; + $this->editor; + $this->content; + $this->summary; + $this->minorEdit; + $this->flags; + + $this->before = $this->wikiPage->getTitle()->getParentCategories(); + $this->doAutoCat(); + } + + /** + * Notify all category watchers + * + * @param Revision $revision that was created + * @param int $baseRevId base revision + */ + public function notifyCategoryWatchers( + Revision $revision, $baseRevId + ) { + wfDebugLog( 'CategoryWatch', __METHOD__ ); + # Get cats after update + $this->after = $this->wikiPage->getTitle()->getParentCategories(); + + # Get list of added and removed cats + $add = array_diff( $this->after, $this->before ); + $sub = array_diff( $this->before, $this->after ); + wfDebugLog( 'CategoryWatch', 'Categories after page saved' ); + wfDebugLog( 'CategoryWatch', join( ', ', $this->after ) ); + wfDebugLog( 'CategoryWatch', 'Categories added' ); + wfDebugLog( 'CategoryWatch', join( ', ', $add ) ); + wfDebugLog( 'CategoryWatch', 'Categories removed' ); + wfDebugLog( 'CategoryWatch', join( ', ', $sub ) ); + + # Notify watchers of each cat about the addition or removal of this article + if ( count( $add ) > 0 || count( $sub ) > 0 ) { + $page = $article->getTitle(); + $pagename = $page->getPrefixedText(); + $pageurl = $page->getFullUrl(); + $page = "$pagename ($pageurl)"; + + if ( count( $add ) == 1 && count( $sub ) == 1 ) { + $this->notifyMove( $sub[0], $add[0] ); + } else { + $this->notifyAdd( $add ); + + foreach ( $sub as $cat ) { + $title = Title::newFromText( $cat, NS_CATEGORY ); + $message = wfMessage( + 'categorywatch-catsub', $page, + $this->friendlyCat( $cat ) + )->text(); + $this->notifyWatchers( + $title, $user, $message, $summary, $medit, $pageurl + ); + } + } + } + + if ( $this->shouldNotifyParentWatchers() ) { + $this->notifyParentWatchers(); + } + } + + /** + * Should watchers of parent categories be notified? + * @return bool + */ + protected function shouldNotifyParentWatchers() { + wfDebugLog( 'CategoryWatch', __METHOD__ ); + global $wgCategoryWatchNotifyParentWatchers; + return $wgCategoryWatchNotifyParentWatchers; + } + + /** + * Should the editor be notified of his own edits? + * @return bool + */ + protected function shouldNotifyEditor() { + wfDebugLog( 'CategoryWatch', __METHOD__ ); + global $wgCategoryWatchNotifyEditor; + return $wgCategoryWatchNotifyEditor; + } + + /** + * Should CategoryWatch use the user's real name in email? + * @return bool + */ + protected function useRealName() { + wfDebugLog( 'CategoryWatch', __METHOD__ ); + global $wgCategoryWatchNoRealName; + return !$wgCategoryWatchNoRealName; + } + + /** + * Return "Category:Cat (URL)" from "Cat" + * @param string $cat name of category + * @return string + */ + protected function friendlyCat( $cat ) { + wfDebugLog( 'CategoryWatch', __METHOD__ ); + $cat = Title::newFromText( $cat, NS_CATEGORY ); + $catname = $cat->getPrefixedText(); + $caturl = $cat->getFullUrl(); + return "$catname ($caturl)"; + } + + /** + * Notify any watchers + * @param Title $title of article + * @param User $editor of article + * @param string $message for user + * @param string $summary editor gave + * @param bool $medit true if minor + * @param string $pageurl of page + */ + protected function notifyWatchers( + $title, $editor, $message, $summary, $medit, $pageurl + ) { + wfDebugLog( 'CategoryWatch', __METHOD__ ); + global $wgLang, $wgNoReplyAddress, + $wgEnotifRevealEditorAddress, $wgEnotifUseRealName, $wgPasswordSender, + $wgEnotifFromEditor, $wgPasswordSenderName; + + # Get list of users watching this category + $dbr = wfGetDB( DB_SLAVE ); + $conds = [ + 'wl_title' => $title->getDBkey(), 'wl_namespace' => $title->getNamespace() + ]; + if ( !$this->shouldNotifyEditor() ) { + $conds[] = 'wl_user <> ' . intval( $editor->getId() ); + } + $res = $dbr->select( 'watchlist', [ 'wl_user' ], $conds, __METHOD__ ); + + # Wrap message with common body and send to each watcher + $page = $title->getPrefixedText(); + $adminAddress = new MailAddress( + $wgPasswordSender, + isset( $wgPasswordSenderName ) + ? $wgPasswordSenderName + : 'WikiAdmin' + ); + $editorAddress = new MailAddress( $editor ); + $summary = $summary + ? $summary + : ' - '; + $medit = $medit + ? wfMessage( 'minoredit' )->text() + : ''; + $row = $dbr->fetchRow( $res ); + while ( $row ) { + $watchingUser = User::newFromId( $row[0] ); + $timecorrection = $watchingUser->getOption( 'timecorrection' ); + $editdate = $wgLang->timeanddate( + wfTimestampNow(), true, false, $timecorrection + ); + + if ( + $watchingUser->getOption( 'enotifwatchlistpages' ) + && $watchingUser->isEmailConfirmed() + ) { + $to = new MailAddress( $watchingUser ); + $subject = wfMessage( 'categorywatch-emailsubject', $page )->text(); + $body = wfMessage( 'enotif_body' )->inContentLanguage()->text(); + + # Reveal the page editor's address as REPLY-TO address only if + # the user has not opted-out and the option is enabled at the + # global configuration level. + $name = $wgEnotifUseRealName + ? $watchingUser->getRealName() + : $watchingUser->getName(); + if ( $wgEnotifRevealEditorAddress + && ( $editor->getEmail() != '' ) + && $editor->getOption( 'enotifrevealaddr' ) + ) { + if ( $wgEnotifFromEditor ) { + $from = $editorAddress; + } else { + $from = $adminAddress; + $replyto = $editorAddress; + } + } else { + $from = $adminAddress; + $replyto = new MailAddress( $wgNoReplyAddress ); + } + + # Define keys for body message + $userPage = $editor->getUserPage(); + $keys = [ + '$WATCHINGUSERNAME' => $name, + '$NEWPAGE' => $message, + '$PAGETITLE' => $page, + '$PAGEEDITDATE' => $editdate, + '$CHANGEDORCREATED' => wfMessage( 'changed' ) + ->inContentLanguage()->text(), + '$PAGETITLE_URL' => $title->getFullUrl(), + '$PAGEEDITOR_WIKI' => $userPage->getFullUrl(), + '$PAGESUMMARY' => $summary, + '$PAGEMINOREDIT' => $medit, + '$OLDID' => '' + ]; + if ( $editor->isIP( $name ) ) { + $utext = wfMessage( + 'enotif_anon_editor', $name + )->inContentLanguage()->text(); + $subject = str_replace( '$PAGEEDITOR', $utext, $subject ); + $keys['$PAGEEDITOR'] = $utext; + $keys['$PAGEEDITOR_EMAIL'] = wfMmessage( + 'noemailtitle' + )->inContentLanguage()->text(); + } else { + $subject = str_replace( '$PAGEEDITOR', $name, $subject ); + $keys['$PAGEEDITOR'] = $name; + $emailPage = SpecialPage::getSafeTitleFor( 'Emailuser', $name ); + $keys['$PAGEEDITOR_EMAIL'] = $emailPage->getFullUrl(); + } + $keys['$PAGESUMMARY'] = $summary; + + # Replace keys, wrap text and send + $body = strtr( $body, $keys ); + $body = wordwrap( $body, 72 ); + $options = []; + $options['replyTo'] = $replyto; + UserMailer::send( $to, $from, $subject, $body, $options ); + } + } + + $dbr->freeResult( $res ); + } + + /** + * Notify the watchers of parent categories + */ + protected function notifyParentWatchers() { + wfDebugLog( 'CategoryWatch', __METHOD__ ); + $this->allparents = $this->wikiPage->getTitle()->getParentCategoryTree(); + $page = $this->wikiPage->getTitle(); + $pageUrl = $page->getFullUrl(); + foreach ( (array)$this->allparents as $cat ) { + $title = Title::newFromText( $cat, NS_CATEGORY ); + $message = wfMessage( + 'categorywatch-catchange', $page, + $this->friendlyCat( $cat ) + ); + $this->notifyWatchers( + $title, $user, $message, $summary, $medit, $pageurl + ); + } + } + + /** + * Handle autocat option + */ + protected function doAutoCat() { + wfDebugLog( 'CategoryWatch', __METHOD__ ); + global $wgCategoryWatchUseAutoCat; + if ( $wgCategoryWatchUseAutoCat ) { + $dbr = wfGetDB( DB_SLAVE ); + + # Find all users not watching the autocat + $like = '%' . str_replace( + ' ', '_', trim( wfMessage( 'categorywatch-autocat', '' )->text() ) + ) . '%'; + $res = $dbr->select( [ 'user', 'watchlist' ], 'user_id', + 'wl_user IS NULL', __METHOD__, [], + [ 'watchlist' => [ 'LEFT JOIN', + [ + 'user_id=wl_user', + 'wl_tile', $dbr->buildLike( $like ) + ] ] ] ); + + # Insert an entry into watchlist for each + $row = $dbr->fetchRow( $res ); + while ( $row ) { + $user = User::newFromId( $row[0] ); + $name = $user->getName(); + $wl_title = str_replace( + ' ', '_', wfMessage( 'categorywatch-autocat', $name )->text() + ); + $dbr->insert( + 'watchlist', + [ + 'wl_user' => $row[0], 'wl_namespace' => NS_CATEGORY, + 'wl_title' => $wl_title + ] + ); + $row = $dbr->fetchRow( $res ); + } + $dbr->freeResult( $res ); + } + } + + /** + * Send a notification that the page's categorization has moved. + * @param string $from Category moving from + * @param string $to Category moving to + */ + protected function notifyMove( $from, $to ) { + wfDebugLog( 'CategoryWatch', __METHOD__ ); + $title = Title::newFromText( $to, NS_CATEGORY ); + $message = wfMessage( + 'categorywatch-catmovein', $page, + $this->friendlyCat( $to ), + $this->friendlyCat( $from ) + )->text(); + $this->notifyWatchers( + $title, $user, $message, $summary, $medit, $pageurl + ); + + $title = Title::newFromText( $from, NS_CATEGORY ); + $message = wfMessage( + 'categorywatch-catmoveout', $page, + $this->friendlyCat( $from ), + $this->friendlyCat( $to ) + )->text(); + $this->notifyWatchers( + $title, $user, $message, $summary, $medit, $pageurl + ); + } + + /** + * Send a notification that a page has been added to the category + * @param array $add Category being added + */ + protected function notifyAdd( $add ) { + wfDebugLog( 'CategoryWatch', __METHOD__ ); + foreach ( $add as $cat ) { + $title = Title::newFromText( $cat, NS_CATEGORY ); + $message = wfMessage( + 'categorywatch-catadd', $page, + $this->friendlyCat( $cat ) + )->text(); + $this->notifyWatchers( + $title, $user, $message, $summary, $medit, $pageurl + ); + } + } +} diff --git a/src/EchoEventPresentationModel.php b/src/EchoEventPresentationModel.php new file mode 100644 index 0000000..049e928 --- /dev/null +++ b/src/EchoEventPresentationModel.php @@ -0,0 +1,180 @@ +. + */ + +namespace CategoryWatch; + +use RawMessage; +use Title; +use WikiPage; + +class EchoEventPresentationModel extends \EchoEventPresentationModel { + /** + * Tell the caller if this event can be rendered. + * + * @return bool + */ + public function canRender() { + wfDebugLog( 'CategoryWatch', __METHOD__ ); + return (bool)$this->event->getTitle(); + } + + /** + * Which of the registered icons to use. + * + * @return string + */ + public function getIconType() { + wfDebugLog( 'CategoryWatch', __METHOD__ ); + return 'categorywatch'; + } + + /** + * The header of this event's display + * + * @return Message + */ + public function getHeaderMessage() { + wfDebugLog( 'CategoryWatch', __METHOD__ ); + if ( $this->isBundled() ) { + $msg = $this->msg( 'categorywatch-notification-bundle' ); + $msg->params( $this->getBundleCount() ); + $msg->params( $this->getTruncatedTitleText( $this->event->getTitle(), true ) ); + $msg->params( $this->getViewingUserForGender() ); + } else { + $msg = $this->msg( 'categorywatch-notification-' . $this->event->getType() . '-header' ); + $msg->params( $this->getPageTitle() ); + $msg->params( $this->getTruncatedTitleText( $this->getPageTitle(), true ) ); + $msg->params( $this->event->getTitle() ); + $msg->params( $this->getTruncatedTitleText( $this->event->getTitle(), true ) ); + } + return $msg; + } + + /** + * Shorter display + * + * @return Message + */ + public function getCompactHeaderMessage() { + wfDebugLog( 'CategoryWatch', __METHOD__ ); + $msg = parent::getCompactHeaderMessage(); + $msg->params( $this->getViewingUserForGender() ); + return $msg; + } + + /** + * Summary of edit + * + * @return string + */ + public function getRevisionEditSummary() { + wfDebugLog( 'CategoryWatch', __METHOD__ ); + $msg = $this->getMessageWithAgent( 'categorywatch-notification-' . $this->event->getType() . '-summary' ); + $msg->params( $this->getPageTitle() ); + $msg->params( $this->getTruncatedTitleText( $this->getPageTitle(), true ) ); + $msg->params( $this->event->getTitle() ); + $msg->params( $this->getTruncatedTitleText( $this->event->getTitle(), true ) ); + return $msg; + } + + /** + * Body to display + * + * @return Message + */ + public function getBodyMessage() { + wfDebugLog( 'CategoryWatch', __METHOD__ ); + $msg = $this->getMessageWithAgent( 'categorywatch-notification-' . + $this->event->getType() . '-body' ); + $msg->params( $this->getPageTitle() ); + $msg->params( $this->event->getTitle() ); + return $msg; + } + + /** + * Title of page + * + * @return Title|string + */ + public function getPageTitle() { + wfDebugLog( 'CategoryWatch', __METHOD__ ); + $page = WikiPage::newFromId( $this->event->getExtraParam( "pageid" ) ); + return $page ? $page->getTitle() : new Title(); + } + + /** + * Provide the main link + * + * @return string + */ + public function getPrimaryLink() { + wfDebugLog( 'CategoryWatch', __METHOD__ ); + $title = $this->event->getTitle(); + $msg = $this->msg( 'categorywatch-notification-link' ); + $msg->params( $title ); + return [ + 'url' => $title->getFullURL(), + 'label' => $title->getPrefixedText() + ]; + } + + /** + * Aux links + * + * @return array + */ + public function getSecondaryLinks() { + wfDebugLog( 'CategoryWatch', __METHOD__ ); + if ( $this->isBundled() ) { + // For the bundle, we don't need secondary actions + return []; + } else { + return [ + $this->getAgentLink(), + [ + 'url' => $this->getPageTitle()->getFullURL(), + 'label' => $this->getPageTitle()->getPrefixedText() + ] + ]; + } + } + + /** + * override parent + * @return array + * @throws TimestampException + */ + public function jsonSerialize() { + wfDebugLog( 'CategoryWatch', __METHOD__ ); + $body = $this->getBodyMessage(); + + return [ + 'header' => $this->getHeaderMessage()->parse(), + 'compactHeader' => $this->getCompactHeaderMessage()->parse(), + 'body' => $body ? $body->toString() : '', + 'icon' => $this->getIconType(), + 'links' => [ + 'primary' => $this->getPrimaryLinkWithMarkAsRead() ?: [], + 'secondary' => array_values( array_filter( $this->getSecondaryLinks() ) ), + ], + ]; + } +} diff --git a/src/Hook.php b/src/Hook.php new file mode 100644 index 0000000..e9af889 --- /dev/null +++ b/src/Hook.php @@ -0,0 +1,225 @@ +. + */ +namespace CategoryWatch; + +use Category; +use Content; +use EchoDiscussionParser; +use EchoEvent; +use MediaWiki\MediaWikiServices; +use WatchedItemStore; +use Status; +use Title; +use User; +use WikiPage; + +class Hook { + // Instance + protected static $watcher; + + /** + * Explain bundling + * + * @param Event $event to bundle + * @param string &$bundleString to use + */ + public static function onEchoGetBundleRules( EchoEvent $event, &$bundleString ) { + wfDebugLog( 'CategoryWatch', __METHOD__ ); + switch ( $event->getType() ) { + case 'categorywatch-add': + case 'categorywatch-remove': + $bundleString = 'categorywatch'; + break; + } + } + + /** + * Define the CategoryWatch notifications + * + * @param array &$notifications assoc array of notification types + * @param array &$notificationCategories assoc array describing + * categories + * @param array &$icons assoc array of icons we define + */ + public static function onBeforeCreateEchoEvent( + array &$notifications, array &$notificationCategories, array &$icons + ) { + wfDebugLog( 'CategoryWatch', __METHOD__ ); + $icons['categorywatch']['path'] = 'CategoryWatch/assets/catwatch.svg'; + + $notifications['categorywatch-add'] = [ + 'bundle' => [ + 'web' => true, + 'email' => true, + 'expandable' => true, + ], + 'title-message' => 'categorywatch-add-title', + 'category' => 'categorywatch', + 'group' => 'neutral', + 'user-locators' => [ 'CategoryWatch\\Hook::userLocater' ], + 'user-filters' => [ 'CategoryWatch\\Hook::userFilter' ], + 'presentation-model' => 'CategoryWatch\\EchoEventPresentationModel', + ]; + + $notifications['categorywatch-remove'] = [ + 'bundle' => [ + 'web' => true, + 'email' => true, + 'expandable' => true, + ], + 'title-message' => 'categorywatch-remove-title', + 'category' => 'categorywatch', + 'group' => 'neutral', + 'user-locators' => [ 'CategoryWatch\\Hook::userLocater' ], + 'user-filters' => [ 'CategoryWatch\\Hook::userFilter' ], + 'presentation-model' => 'CategoryWatch\\EchoEventPresentationModel', + ]; + + $notificationCategories['categorywatch'] = [ + 'priority' => 2, + 'tooltip' => 'echo-pref-tooltip-categorywatch' + ]; + } + + /** + * Internal compatibility function + * @return WatchedItemStore + */ + protected static function getWatchedItemStore() { + wfDebugLog( 'CategoryWatch', __METHOD__ ); + if ( method_exists( 'WatchedItemStore', 'getDefaultInstance' ) ) { + return WatchedItemStore::getDefaultInstance(); + } else { + return MediaWikiServices::getInstance()->getWatchedItemStore(); + } + } + + /** + * Hook for page being added to a category. + * + * @param Category $cat that page is being add to + * @param WikiPage $page that is being added + */ + public static function onCategoryAfterPageAdded( + Category $cat, WikiPage $page + ) { + wfDebugLog( 'CategoryWatch', __METHOD__ ); + # Is anyone watching the category? + if ( + self::getWatchedItemStore() + ->countWatchers( $cat->getTitle() ) > 0 + ) { + # Send them a notification! + $user = User::newFromId( $page->getUser() ); + + EchoEvent::create( [ + 'type' => 'categorywatch-add', + 'title' => $cat->getTitle(), + 'agent' => $user, + 'extra' => [ + 'pageid' => $page->getId(), + 'revid' => $page->getRevision()->getId(), + ], + ] ); + } + } + + /** + * Hook for page being taken out of a category. + * + * @param Category $cat that page is being removed from + * @param WikiPage $page that is being removed + * @param int $id that this happened in. (not given pre 1.27ish) + */ + public static function onCategoryAfterPageRemoved( + Category $cat, WikiPage $page, $id = 0 + ) { + wfDebugLog( 'CategoryWatch', __METHOD__ ); + # Is anyone watching the category? + if ( + self::getWatchedItemStore() + ->countWatchers( $cat->getTitle() ) > 0 + ) { + # Send them a notification! + $user = User::newFromId( $page->getUser() ); + + $revId = null; + $rev = $page->getRevision(); + if ( $rev ) { + $revId = $rev->getId(); + } + EchoEvent::create( [ + 'type' => 'categorywatch-remove', + 'title' => $cat->getTitle(), + 'agent' => $user, + 'extra' => [ + 'pageid' => $page->getId(), + 'revid' => $revId + ], + ] ); + } + } + + /** + * Find the watchers for a title + * + * @param Title $target to check + * + * @return array + */ + protected static function getWatchers( Title $target ) { + $dbr = wfGetDB( DB_SLAVE ); + $return = $dbr->selectFieldValues( + 'watchlist', + 'wl_user', + [ + 'wl_namespace' => $target->getNamespace(), + 'wl_title' => $target->getDBkey(), + ], + __METHOD__ + ); + + return array_map( function ( $id ) { + return User::newFromID( $id ); + }, $return ); + } + + /** + * Get users that should be notified for this event. + * + * @param EchoEvent $event to be looked at + * @return array + */ + public static function userLocater( EchoEvent $event ) { + wfDebugLog( 'CategoryWatch', __METHOD__ ); + return self::getWatchers( $event->getTitle() ); + } + + /** + * Filter out the person performing the action + * + * @param EchoEvent $event to be looked at + * @return array + */ + public static function userFilter( EchoEvent $event ) { + wfDebugLog( 'CategoryWatch', __METHOD__ ); + return [ $event->getAgent() ]; + } +}